@sun-asterisk/sungen 3.1.2-beta.101 → 3.1.2-beta.103

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.
@@ -69,6 +69,20 @@ A screen often matches several patterns at once — a login screen is *both* a f
69
69
  - VP-LOGIC = outcome depends on the user's *action* (click, submit, navigate)
70
70
  - VP-SEC = checks access control and malicious input
71
71
 
72
+ ### Domain category codes — required for the coverage-balance gate
73
+
74
+ The 4 viewpoints above are the *generic* axes. On a domain screen, the `VP-<CAT>` code must use the **canonical short code** for what the scenario exercises, so the audit's coverage-balance gate buckets it correctly. Use these exact codes — **never long-form or freeform** (`VP-NAV` not `VP-NAVIGATION`, `VP-SUB` not `VP-SUBSCRIPTION`, `VP-FILTER` not `VP-FILTERING`):
75
+
76
+ | Bucket | Codes | Use for |
77
+ |---|---|---|
78
+ | **business-core** | `LIST` · `CART` · `PRODUCT` · `FILTER` · `CHECKOUT` · `ORDER` | the screen's core domain data/actions (product list, cart, checkout, order, filtered results) |
79
+ | presentation | `UI` | layout / visual state |
80
+ | validation-security | `VAL` · `SEC` · `SUB` | input validation · access/injection · subscribe/newsletter |
81
+ | behavior | `LOGIC` | action-driven state changes |
82
+ | navigation | `NAV` | landing on / moving between pages |
83
+
84
+ **On a business-core page** (product list, cart, checkout, search results), the core data scenarios MUST carry a **business-core** code (`VP-LIST-*`, `VP-CART-*`, `VP-PRODUCT-*`, …) — not a generic `VP-UI`/`VP-LOGIC` or a freeform `VP-<word>`. A freeform/long-form prefix parses as `NONE`, scores **0 on the balance axis**, and drops the audit score (~9.3 → ~7.7 in practice). Keep `VP-UI/VAL/LOGIC/SEC` for the cross-cutting checks; give the domain scenarios their domain code.
85
+
72
86
  ---
73
87
 
74
88
  ## Shared Checks
@@ -69,6 +69,20 @@ A screen often matches several patterns at once — a login screen is *both* a f
69
69
  - VP-LOGIC = outcome depends on the user's *action* (click, submit, navigate)
70
70
  - VP-SEC = checks access control and malicious input
71
71
 
72
+ ### Domain category codes — required for the coverage-balance gate
73
+
74
+ The 4 viewpoints above are the *generic* axes. On a domain screen, the `VP-<CAT>` code must use the **canonical short code** for what the scenario exercises, so the audit's coverage-balance gate buckets it correctly. Use these exact codes — **never long-form or freeform** (`VP-NAV` not `VP-NAVIGATION`, `VP-SUB` not `VP-SUBSCRIPTION`, `VP-FILTER` not `VP-FILTERING`):
75
+
76
+ | Bucket | Codes | Use for |
77
+ |---|---|---|
78
+ | **business-core** | `LIST` · `CART` · `PRODUCT` · `FILTER` · `CHECKOUT` · `ORDER` | the screen's core domain data/actions (product list, cart, checkout, order, filtered results) |
79
+ | presentation | `UI` | layout / visual state |
80
+ | validation-security | `VAL` · `SEC` · `SUB` | input validation · access/injection · subscribe/newsletter |
81
+ | behavior | `LOGIC` | action-driven state changes |
82
+ | navigation | `NAV` | landing on / moving between pages |
83
+
84
+ **On a business-core page** (product list, cart, checkout, search results), the core data scenarios MUST carry a **business-core** code (`VP-LIST-*`, `VP-CART-*`, `VP-PRODUCT-*`, …) — not a generic `VP-UI`/`VP-LOGIC` or a freeform `VP-<word>`. A freeform/long-form prefix parses as `NONE`, scores **0 on the balance axis**, and drops the audit score (~9.3 → ~7.7 in practice). Keep `VP-UI/VAL/LOGIC/SEC` for the cross-cutting checks; give the domain scenarios their domain code.
85
+
72
86
  ---
73
87
 
74
88
  ## Shared Checks
@@ -1,8 +1,11 @@
1
1
  declare class DataSource {
2
2
  private configs;
3
3
  private engines;
4
+ private tunnels;
4
5
  private cfg;
5
6
  private engine;
7
+ /** Close any open SSH tunnels (optional explicit teardown; tunnels are unref'd so the process exits regardless). */
8
+ close(): void;
6
9
  private build;
7
10
  /** A row matching `filter` must exist; if `expected` given, assert those columns on the first match. */
8
11
  assertRow(table: string, filter: Record<string, any>, expected?: Record<string, any>, datasource?: string): Promise<void>;
@@ -1 +1 @@
1
- {"version":3,"file":"specs-db.d.ts","sourceRoot":"","sources":["../../../src/orchestrator/templates/specs-db.ts"],"names":[],"mappings":"AA+DA,cAAM,UAAU;IACd,OAAO,CAAC,OAAO,CAAiD;IAChE,OAAO,CAAC,OAAO,CAA6B;IAE5C,OAAO,CAAC,GAAG;YAQG,MAAM;IAoBpB,OAAO,CAAC,KAAK;IAOb,wGAAwG;IAClG,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAAE,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAc/H,0CAA0C;IACpC,WAAW,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAOjG,gDAAgD;IAC1C,WAAW,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAUhH,qDAAqD;IACrD,OAAO,CAAC,MAAM;IAKd,4FAA4F;IAC5F,OAAO,CAAC,gBAAgB;IASxB;;;;OAIG;IACG,UAAU,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,GAAG,EAAE,CAAC;CAKjG;AAMD,eAAO,MAAM,EAAE,YAAmB,CAAC"}
1
+ {"version":3,"file":"specs-db.d.ts","sourceRoot":"","sources":["../../../src/orchestrator/templates/specs-db.ts"],"names":[],"mappings":"AA4HA,cAAM,UAAU;IACd,OAAO,CAAC,OAAO,CAAiD;IAChE,OAAO,CAAC,OAAO,CAA6B;IAC5C,OAAO,CAAC,OAAO,CAAoC;IAEnD,OAAO,CAAC,GAAG;YAQG,MAAM;IA6BpB,oHAAoH;IACpH,KAAK,IAAI,IAAI;IAKb,OAAO,CAAC,KAAK;IAOb,wGAAwG;IAClG,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAAE,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAc/H,0CAA0C;IACpC,WAAW,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAOjG,gDAAgD;IAC1C,WAAW,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAUhH,qDAAqD;IACrD,OAAO,CAAC,MAAM;IAKd,4FAA4F;IAC5F,OAAO,CAAC,gBAAgB;IASxB;;;;OAIG;IACG,UAAU,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,GAAG,EAAE,CAAC;CAKjG;AAMD,eAAO,MAAM,EAAE,YAAmB,CAAC"}
@@ -56,6 +56,65 @@ const ident = (s) => {
56
56
  throw new Error(`Unsafe identifier: ${JSON.stringify(s)} (allowed: [A-Za-z_][A-Za-z0-9_]*)`);
57
57
  return s;
58
58
  };
59
+ /**
60
+ * Open a local TCP forward (127.0.0.1:<ephemeral> → ssh bastion → dstHost:dstPort) for a DB socket.
61
+ * Sockets are unref()'d so a dangling tunnel never keeps the test process alive after the run.
62
+ */
63
+ async function openSshTunnel(ssh, dstHost, dstPort) {
64
+ const { Client } = require('ssh2');
65
+ const net = require('net');
66
+ const privateKey = ssh.private_key
67
+ ? ssh.private_key
68
+ : ssh.private_key_path
69
+ ? fs.readFileSync(ssh.private_key_path.replace(/^~(?=\/)/, process.env.HOME || ''), 'utf8')
70
+ : undefined;
71
+ if (!privateKey)
72
+ throw new Error('Data Driver: datasource `ssh` requires `private_key` or `private_key_path`.');
73
+ const conn = new Client();
74
+ await new Promise((resolve, reject) => {
75
+ conn.on('ready', resolve).on('error', reject).connect({
76
+ host: ssh.host,
77
+ port: ssh.port ?? 22,
78
+ username: ssh.user,
79
+ privateKey,
80
+ passphrase: ssh.passphrase,
81
+ hostVerifier: (key) => {
82
+ const got = Buffer.isBuffer(key) ? key.toString('base64') : String(key);
83
+ if (ssh.known_host) {
84
+ if (got === ssh.known_host.trim())
85
+ return true;
86
+ throw new Error(`Data Driver: SSH host-key mismatch for ${ssh.host} — refused (known_host pin).`);
87
+ }
88
+ console.warn(`Data Driver: SSH host key for ${ssh.host} is not pinned (set datasource ssh.known_host to verify). Proceeding (TOFU).`);
89
+ return true;
90
+ },
91
+ });
92
+ });
93
+ const server = net.createServer((sock) => {
94
+ conn.forwardOut(sock.remoteAddress || '127.0.0.1', sock.remotePort || 0, dstHost, dstPort, (err, stream) => {
95
+ if (err) {
96
+ sock.destroy();
97
+ return;
98
+ }
99
+ sock.pipe(stream).pipe(sock);
100
+ });
101
+ });
102
+ await new Promise((resolve, reject) => server.on('error', reject).listen(0, '127.0.0.1', () => resolve()));
103
+ const addr = server.address();
104
+ const port = addr && typeof addr === 'object' ? addr.port : 0;
105
+ server.unref(); // don't keep the event loop alive after tests
106
+ try {
107
+ conn._sock?.unref?.();
108
+ }
109
+ catch { /* best-effort */ }
110
+ return { host: '127.0.0.1', port, close: () => { try {
111
+ server.close();
112
+ }
113
+ catch { } try {
114
+ conn.end();
115
+ }
116
+ catch { } } };
117
+ }
59
118
  function loadEnvQa() {
60
119
  for (const name of ['.env.qa', `.env.qa.${process.env.SUNGEN_ENV || ''}`]) {
61
120
  const p = path.join(process.cwd(), name);
@@ -89,6 +148,7 @@ class DataSource {
89
148
  constructor() {
90
149
  this.configs = null;
91
150
  this.engines = new Map();
151
+ this.tunnels = [];
92
152
  }
93
153
  cfg(name) {
94
154
  if (!this.configs)
@@ -107,11 +167,22 @@ class DataSource {
107
167
  throw new Error(`Data Driver: datasource "${key}" has no url (set it in .env.qa).`);
108
168
  let engine;
109
169
  if (conf.engine === 'postgres') {
170
+ let connectionString = conf.url;
171
+ if (conf.ssh) { // Cách B: tunnel the DB socket through a bastion
172
+ const u = new URL(conf.url);
173
+ const t = await openSshTunnel(conf.ssh, u.hostname, Number(u.port || 5432));
174
+ this.tunnels.push(t);
175
+ u.hostname = t.host;
176
+ u.port = String(t.port); // rewrite host:port → 127.0.0.1:<tunnel> (keep user/pass/db/query)
177
+ connectionString = u.toString();
178
+ }
110
179
  const { Pool } = require('pg');
111
- const pool = new Pool({ connectionString: conf.url, max: 2, statement_timeout: conf.statement_timeout_ms ?? 4000 });
180
+ const pool = new Pool({ connectionString, max: 2, statement_timeout: conf.statement_timeout_ms ?? 4000 });
112
181
  engine = { query: async (sql, params) => (await pool.query(sql, params)).rows };
113
182
  }
114
183
  else if (conf.engine === 'sqlite') {
184
+ if (conf.ssh)
185
+ console.warn(`Data Driver: datasource "${key}" sets ssh: but engine is sqlite (file-based) — ssh ignored.`);
115
186
  const Database = require('better-sqlite3');
116
187
  const db = new Database(conf.url.replace(/^sqlite:/, ''), { readonly: conf.readonly !== false });
117
188
  engine = { query: async (sql, params) => db.prepare(sql).all(...params) };
@@ -122,6 +193,12 @@ class DataSource {
122
193
  this.engines.set(key, engine);
123
194
  return { engine, conf };
124
195
  }
196
+ /** Close any open SSH tunnels (optional explicit teardown; tunnels are unref'd so the process exits regardless). */
197
+ close() {
198
+ for (const t of this.tunnels)
199
+ t.close();
200
+ this.tunnels = [];
201
+ }
125
202
  build(table, filter) {
126
203
  const cols = Object.keys(filter);
127
204
  const where = cols.map((c, i) => `${ident(c)} = $${i + 1}`).join(' AND ');
@@ -1 +1 @@
1
- {"version":3,"file":"specs-db.js","sourceRoot":"","sources":["../../../src/orchestrator/templates/specs-db.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"specs-db.js","sourceRoot":"","sources":["../../../src/orchestrator/templates/specs-db.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,oBAAoB;AACpB;;;;;;;;;;;GAWG;AACH,2CAA0C;AAC1C,uCAAyB;AACzB,2CAA6B;AAE7B,MAAM,KAAK,GAAG,0BAA0B,CAAC;AACzC,MAAM,KAAK,GAAG,CAAC,CAAS,EAAU,EAAE;IAClC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC;QAAE,MAAM,IAAI,KAAK,CAAC,sBAAsB,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,oCAAoC,CAAC,CAAC;IACjH,OAAO,CAAC,CAAC;AACX,CAAC,CAAC;AAuBF;;;GAGG;AACH,KAAK,UAAU,aAAa,CAAC,GAAc,EAAE,OAAe,EAAE,OAAe;IAC3E,MAAM,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IACnC,MAAM,GAAG,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC;IAC3B,MAAM,UAAU,GAAG,GAAG,CAAC,WAAW;QAChC,CAAC,CAAC,GAAG,CAAC,WAAW;QACjB,CAAC,CAAC,GAAG,CAAC,gBAAgB;YACpB,CAAC,CAAC,EAAE,CAAC,YAAY,CAAC,GAAG,CAAC,gBAAgB,CAAC,OAAO,CAAC,UAAU,EAAE,OAAO,CAAC,GAAG,CAAC,IAAI,IAAI,EAAE,CAAC,EAAE,MAAM,CAAC;YAC3F,CAAC,CAAC,SAAS,CAAC;IAChB,IAAI,CAAC,UAAU;QAAE,MAAM,IAAI,KAAK,CAAC,6EAA6E,CAAC,CAAC;IAEhH,MAAM,IAAI,GAAG,IAAI,MAAM,EAAE,CAAC;IAC1B,MAAM,IAAI,OAAO,CAAO,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;QAC1C,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC,OAAO,CAAC;YACpD,IAAI,EAAE,GAAG,CAAC,IAAI;YACd,IAAI,EAAE,GAAG,CAAC,IAAI,IAAI,EAAE;YACpB,QAAQ,EAAE,GAAG,CAAC,IAAI;YAClB,UAAU;YACV,UAAU,EAAE,GAAG,CAAC,UAAU;YAC1B,YAAY,EAAE,CAAC,GAAW,EAAE,EAAE;gBAC5B,MAAM,GAAG,GAAG,MAAM,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;gBACxE,IAAI,GAAG,CAAC,UAAU,EAAE,CAAC;oBACnB,IAAI,GAAG,KAAK,GAAG,CAAC,UAAU,CAAC,IAAI,EAAE;wBAAE,OAAO,IAAI,CAAC;oBAC/C,MAAM,IAAI,KAAK,CAAC,0CAA0C,GAAG,CAAC,IAAI,8BAA8B,CAAC,CAAC;gBACpG,CAAC;gBACD,OAAO,CAAC,IAAI,CAAC,iCAAiC,GAAG,CAAC,IAAI,8EAA8E,CAAC,CAAC;gBACtI,OAAO,IAAI,CAAC;YACd,CAAC;SACF,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,MAAM,MAAM,GAAG,GAAG,CAAC,YAAY,CAAC,CAAC,IAAS,EAAE,EAAE;QAC5C,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,aAAa,IAAI,WAAW,EAAE,IAAI,CAAC,UAAU,IAAI,CAAC,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC,GAAQ,EAAE,MAAW,EAAE,EAAE;YACnH,IAAI,GAAG,EAAE,CAAC;gBAAC,IAAI,CAAC,OAAO,EAAE,CAAC;gBAAC,OAAO;YAAC,CAAC;YACpC,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAC/B,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IACH,MAAM,IAAI,OAAO,CAAO,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC,MAAM,CAAC,CAAC,EAAE,WAAW,EAAE,GAAG,EAAE,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC;IACjH,MAAM,IAAI,GAAG,MAAM,CAAC,OAAO,EAAE,CAAC;IAC9B,MAAM,IAAI,GAAG,IAAI,IAAI,OAAO,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC;IAC9D,MAAM,CAAC,KAAK,EAAE,CAAC,CAA0B,8CAA8C;IACvF,IAAI,CAAC;QAAE,IAAY,CAAC,KAAK,EAAE,KAAK,EAAE,EAAE,CAAC;IAAC,CAAC;IAAC,MAAM,CAAC,CAAC,iBAAiB,CAAC,CAAC;IACnE,OAAO,EAAE,IAAI,EAAE,WAAW,EAAE,IAAI,EAAE,KAAK,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC;YAAC,MAAM,CAAC,KAAK,EAAE,CAAC;QAAC,CAAC;QAAC,MAAM,CAAC,CAAA,CAAC,CAAC,IAAI,CAAC;YAAC,IAAI,CAAC,GAAG,EAAE,CAAC;QAAC,CAAC;QAAC,MAAM,CAAC,CAAA,CAAC,CAAC,CAAC,EAAE,CAAC;AACrH,CAAC;AAED,SAAS,SAAS;IAChB,KAAK,MAAM,IAAI,IAAI,CAAC,SAAS,EAAE,WAAW,OAAO,CAAC,GAAG,CAAC,UAAU,IAAI,EAAE,EAAE,CAAC,EAAE,CAAC;QAC1E,MAAM,CAAC,GAAG,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,IAAI,CAAC,CAAC;QACzC,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,UAAU,CAAC,CAAC,CAAC,EAAE,CAAC;YAC5C,KAAK,MAAM,IAAI,IAAI,EAAE,CAAC,YAAY,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC;gBAC1D,MAAM,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,8CAA8C,CAAC,CAAC;gBACrE,IAAI,CAAC,IAAI,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,SAAS,EAAE,CAAC;oBACzC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,cAAc,EAAE,EAAE,CAAC,CAAC;gBACvD,CAAC;YACH,CAAC;QACH,CAAC;IACH,CAAC;AACH,CAAC;AAED,SAAS,UAAU;IACjB,SAAS,EAAE,CAAC;IACZ,MAAM,IAAI,GAAG;QACX,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,kBAAkB,CAAC;QAC5C,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,IAAI,EAAE,kBAAkB,CAAC;KACnD,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,EAAE,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC;IAChC,IAAI,CAAC,IAAI;QAAE,MAAM,IAAI,KAAK,CAAC,+DAA+D,CAAC,CAAC;IAC5F,IAAI,GAAG,GAAG,EAAE,CAAC,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,OAAO,CAAC,iCAAiC,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC;IACnH,MAAM,EAAE,KAAK,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAClC,MAAM,GAAG,GAAG,KAAK,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC;IAC7B,IAAI,CAAC,GAAG,CAAC,WAAW,IAAI,OAAO,GAAG,CAAC,WAAW,KAAK,QAAQ,EAAE,CAAC;QAC5D,MAAM,IAAI,KAAK,CAAC,2EAA2E,CAAC,CAAC;IAC/F,CAAC;IACD,OAAO,GAAG,CAAC,WAAW,CAAC;AACzB,CAAC;AAID,MAAM,UAAU;IAAhB;QACU,YAAO,GAA4C,IAAI,CAAC;QACxD,YAAO,GAAG,IAAI,GAAG,EAAkB,CAAC;QACpC,YAAO,GAAiC,EAAE,CAAC;IAgHrD,CAAC;IA9GS,GAAG,CAAC,IAAa;QACvB,IAAI,CAAC,IAAI,CAAC,OAAO;YAAE,IAAI,CAAC,OAAO,GAAG,UAAU,EAAE,CAAC;QAC/C,MAAM,GAAG,GAAG,IAAI,IAAI,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC;QACjD,MAAM,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;QAC/B,IAAI,CAAC,IAAI;YAAE,MAAM,IAAI,KAAK,CAAC,4BAA4B,GAAG,iCAAiC,CAAC,CAAC;QAC7F,OAAO,EAAE,GAAG,EAAE,IAAI,EAAE,CAAC;IACvB,CAAC;IAEO,KAAK,CAAC,MAAM,CAAC,IAAa;QAChC,MAAM,EAAE,GAAG,EAAE,IAAI,EAAE,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QACrC,IAAI,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC;YAAE,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,CAAE,EAAE,IAAI,EAAE,CAAC;QAC3E,IAAI,CAAC,IAAI,CAAC,GAAG;YAAE,MAAM,IAAI,KAAK,CAAC,4BAA4B,GAAG,mCAAmC,CAAC,CAAC;QACnG,IAAI,MAAc,CAAC;QACnB,IAAI,IAAI,CAAC,MAAM,KAAK,UAAU,EAAE,CAAC;YAC/B,IAAI,gBAAgB,GAAG,IAAI,CAAC,GAAG,CAAC;YAChC,IAAI,IAAI,CAAC,GAAG,EAAE,CAAC,CAAmC,iDAAiD;gBACjG,MAAM,CAAC,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;gBAC5B,MAAM,CAAC,GAAG,MAAM,aAAa,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC,CAAC,IAAI,IAAI,IAAI,CAAC,CAAC,CAAC;gBAC5E,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;gBACrB,CAAC,CAAC,QAAQ,GAAG,CAAC,CAAC,IAAI,CAAC;gBAAC,CAAC,CAAC,IAAI,GAAG,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAG,mEAAmE;gBACnH,gBAAgB,GAAG,CAAC,CAAC,QAAQ,EAAE,CAAC;YAClC,CAAC;YACD,MAAM,EAAE,IAAI,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;YAC/B,MAAM,IAAI,GAAG,IAAI,IAAI,CAAC,EAAE,gBAAgB,EAAE,GAAG,EAAE,CAAC,EAAE,iBAAiB,EAAE,IAAI,CAAC,oBAAoB,IAAI,IAAI,EAAE,CAAC,CAAC;YAC1G,MAAM,GAAG,EAAE,KAAK,EAAE,KAAK,EAAE,GAAG,EAAE,MAAM,EAAE,EAAE,CAAC,CAAC,MAAM,IAAI,CAAC,KAAK,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;QAClF,CAAC;aAAM,IAAI,IAAI,CAAC,MAAM,KAAK,QAAQ,EAAE,CAAC;YACpC,IAAI,IAAI,CAAC,GAAG;gBAAE,OAAO,CAAC,IAAI,CAAC,4BAA4B,GAAG,8DAA8D,CAAC,CAAC;YAC1H,MAAM,QAAQ,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAAC;YAC3C,MAAM,EAAE,GAAG,IAAI,QAAQ,CAAC,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC,UAAU,EAAE,EAAE,CAAC,EAAE,EAAE,QAAQ,EAAE,IAAI,CAAC,QAAQ,KAAK,KAAK,EAAE,CAAC,CAAC;YACjG,MAAM,GAAG,EAAE,KAAK,EAAE,KAAK,EAAE,GAAG,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,GAAG,MAAM,CAAC,EAAE,CAAC;QAC5E,CAAC;aAAM,CAAC;YACN,MAAM,IAAI,KAAK,CAAC,wBAAwB,IAAI,CAAC,MAAM,0CAA0C,CAAC,CAAC;QACjG,CAAC;QACD,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC;QAC9B,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC;IAC1B,CAAC;IAED,oHAAoH;IACpH,KAAK;QACH,KAAK,MAAM,CAAC,IAAI,IAAI,CAAC,OAAO;YAAE,CAAC,CAAC,KAAK,EAAE,CAAC;QACxC,IAAI,CAAC,OAAO,GAAG,EAAE,CAAC;IACpB,CAAC;IAEO,KAAK,CAAC,KAAa,EAAE,MAA2B;QACtD,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QACjC,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,GAAG,KAAK,CAAC,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QAC1E,MAAM,GAAG,GAAG,iBAAiB,KAAK,CAAC,KAAK,CAAC,GAAG,KAAK,CAAC,CAAC,CAAC,SAAS,GAAG,KAAK,CAAC,CAAC,CAAC,EAAE,WAAW,CAAC;QACtF,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;IACrD,CAAC;IAED,wGAAwG;IACxG,KAAK,CAAC,SAAS,CAAC,KAAa,EAAE,MAA2B,EAAE,QAA8B,EAAE,UAAmB;QAC7G,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC;QACvD,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC;QAClD,MAAM,IAAI,GAAG,MAAM,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,EAAE,GAAG,CAAC,EAAE,MAAM,CAAC,CAAC;QAChE,IAAA,aAAM,EAAC,IAAI,CAAC,MAAM,EAAE,sBAAsB,KAAK,WAAW,IAAI,CAAC,MAAM,CAAC,YAAY,IAAI,CAAC,MAAM,EAAE,CAAC,CAAC,sBAAsB,CAAC,CAAC,CAAC,CAAC;QAC3H,IAAI,QAAQ,EAAE,CAAC;YACb,MAAM,GAAG,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC;YACpB,KAAK,MAAM,CAAC,GAAG,EAAE,GAAG,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAC;gBAClD,KAAK,CAAC,GAAG,CAAC,CAAC;gBACX,IAAA,aAAM,EAAC,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,EAAE,WAAW,GAAG,SAAS,KAAK,WAAW,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC;YACpG,CAAC;QACH,CAAC;IACH,CAAC;IAED,0CAA0C;IAC1C,KAAK,CAAC,WAAW,CAAC,KAAa,EAAE,MAA2B,EAAE,UAAmB;QAC/E,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC;QACvD,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC;QAClD,MAAM,IAAI,GAAG,MAAM,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,EAAE,GAAG,CAAC,EAAE,MAAM,CAAC,CAAC;QAChE,IAAA,aAAM,EAAC,IAAI,CAAC,MAAM,EAAE,uBAAuB,KAAK,WAAW,IAAI,CAAC,MAAM,CAAC,YAAY,IAAI,CAAC,MAAM,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAC5G,CAAC;IAED,gDAAgD;IAChD,KAAK,CAAC,WAAW,CAAC,KAAa,EAAE,MAA2B,EAAE,KAAa,EAAE,UAAmB;QAC9F,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC;QACvD,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QACjC,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,GAAG,KAAK,CAAC,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QAC1E,MAAM,GAAG,GAAG,6BAA6B,KAAK,CAAC,KAAK,CAAC,GAAG,KAAK,CAAC,CAAC,CAAC,SAAS,GAAG,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC;QACzF,MAAM,IAAI,GAAG,MAAM,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,EAAE,GAAG,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QACpF,MAAM,CAAC,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,IAAI,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC;QAC3D,IAAA,aAAM,EAAC,CAAC,EAAE,YAAY,KAAK,eAAe,KAAK,IAAI,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,SAAS,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,EAAE,YAAY,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;IACrI,CAAC;IAED,qDAAqD;IAC7C,MAAM,CAAC,IAAsB,EAAE,GAAW;QAChD,OAAO,IAAI,CAAC,MAAM,KAAK,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC;IACrE,CAAC;IAED,6FAA6F;IAC7F,4FAA4F;IACpF,gBAAgB,CAAC,KAAa,EAAE,GAAW;QACjD,MAAM,CAAC,GAAG,GAAG,CAAC,IAAI,EAAE,CAAC,OAAO,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC;QAC1C,IAAI,CAAC,mBAAmB,CAAC,IAAI,CAAC,CAAC,CAAC;YAAE,MAAM,IAAI,KAAK,CAAC,gBAAgB,KAAK,uCAAuC,CAAC,CAAC;QAChH,IAAI,CAAC,CAAC,QAAQ,CAAC,GAAG,CAAC;YAAE,MAAM,IAAI,KAAK,CAAC,gBAAgB,KAAK,0CAA0C,CAAC,CAAC;QACtG,IAAI,0HAA0H,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC;YACvI,MAAM,IAAI,KAAK,CAAC,gBAAgB,KAAK,0CAA0C,CAAC,CAAC;QACnF,CAAC;IACH,CAAC;IAED;;;;OAIG;IACH,KAAK,CAAC,UAAU,CAAC,KAAa,EAAE,GAAW,EAAE,MAAa,EAAE,UAAmB;QAC7E,IAAI,CAAC,gBAAgB,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC;QAClC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC;QACvD,OAAO,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,EAAE,GAAG,CAAC,EAAE,MAAM,CAAC,CAAC;IACtD,CAAC;CACF;AAED,SAAS,IAAI,CAAC,MAA2B;IACvC,OAAO,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,IAAI,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACxF,CAAC;AAEY,QAAA,EAAE,GAAG,IAAI,UAAU,EAAE,CAAC"}
@@ -21,12 +21,73 @@ const ident = (s: string): string => {
21
21
  return s;
22
22
  };
23
23
 
24
+ interface SshConfig {
25
+ host: string; // jump host reachable from the runner
26
+ port?: number; // default 22
27
+ user: string;
28
+ private_key?: string; // PEM contents (from ${VAR} in .env.qa) — preferred for CI
29
+ private_key_path?: string; // or a filesystem path (local dev)
30
+ passphrase?: string; // for an encrypted key
31
+ known_host?: string; // base64 of the server's host key to pin (optional; else warn-and-proceed)
32
+ }
33
+
24
34
  interface DataSourceConfig {
25
35
  engine: 'postgres' | 'mysql' | 'sqlite';
26
36
  url: string;
27
37
  readonly?: boolean;
28
38
  statement_timeout_ms?: number;
29
39
  max_rows?: number;
40
+ // Cách B (fallback): tunnel the DB SOCKET through an SSH bastion. DB-only — the browser/E2E
41
+ // still run on the runner; only PG traffic crosses. See docs/spec/sungen_data_driver_ssh_tunnel_spec.md.
42
+ ssh?: SshConfig;
43
+ }
44
+
45
+ /**
46
+ * Open a local TCP forward (127.0.0.1:<ephemeral> → ssh bastion → dstHost:dstPort) for a DB socket.
47
+ * Sockets are unref()'d so a dangling tunnel never keeps the test process alive after the run.
48
+ */
49
+ async function openSshTunnel(ssh: SshConfig, dstHost: string, dstPort: number): Promise<{ host: string; port: number; close: () => void }> {
50
+ const { Client } = require('ssh2');
51
+ const net = require('net');
52
+ const privateKey = ssh.private_key
53
+ ? ssh.private_key
54
+ : ssh.private_key_path
55
+ ? fs.readFileSync(ssh.private_key_path.replace(/^~(?=\/)/, process.env.HOME || ''), 'utf8')
56
+ : undefined;
57
+ if (!privateKey) throw new Error('Data Driver: datasource `ssh` requires `private_key` or `private_key_path`.');
58
+
59
+ const conn = new Client();
60
+ await new Promise<void>((resolve, reject) => {
61
+ conn.on('ready', resolve).on('error', reject).connect({
62
+ host: ssh.host,
63
+ port: ssh.port ?? 22,
64
+ username: ssh.user,
65
+ privateKey,
66
+ passphrase: ssh.passphrase,
67
+ hostVerifier: (key: Buffer) => {
68
+ const got = Buffer.isBuffer(key) ? key.toString('base64') : String(key);
69
+ if (ssh.known_host) {
70
+ if (got === ssh.known_host.trim()) return true;
71
+ throw new Error(`Data Driver: SSH host-key mismatch for ${ssh.host} — refused (known_host pin).`);
72
+ }
73
+ console.warn(`Data Driver: SSH host key for ${ssh.host} is not pinned (set datasource ssh.known_host to verify). Proceeding (TOFU).`);
74
+ return true;
75
+ },
76
+ });
77
+ });
78
+
79
+ const server = net.createServer((sock: any) => {
80
+ conn.forwardOut(sock.remoteAddress || '127.0.0.1', sock.remotePort || 0, dstHost, dstPort, (err: any, stream: any) => {
81
+ if (err) { sock.destroy(); return; }
82
+ sock.pipe(stream).pipe(sock);
83
+ });
84
+ });
85
+ await new Promise<void>((resolve, reject) => server.on('error', reject).listen(0, '127.0.0.1', () => resolve()));
86
+ const addr = server.address();
87
+ const port = addr && typeof addr === 'object' ? addr.port : 0;
88
+ server.unref(); // don't keep the event loop alive after tests
89
+ try { (conn as any)._sock?.unref?.(); } catch { /* best-effort */ }
90
+ return { host: '127.0.0.1', port, close: () => { try { server.close(); } catch {} try { conn.end(); } catch {} } };
30
91
  }
31
92
 
32
93
  function loadEnvQa(): void {
@@ -64,6 +125,7 @@ type Engine = { query(sql: string, params: any[]): Promise<any[]>; };
64
125
  class DataSource {
65
126
  private configs: Record<string, DataSourceConfig> | null = null;
66
127
  private engines = new Map<string, Engine>();
128
+ private tunnels: Array<{ close: () => void }> = [];
67
129
 
68
130
  private cfg(name?: string): { key: string; conf: DataSourceConfig } {
69
131
  if (!this.configs) this.configs = loadConfig();
@@ -79,10 +141,19 @@ class DataSource {
79
141
  if (!conf.url) throw new Error(`Data Driver: datasource "${key}" has no url (set it in .env.qa).`);
80
142
  let engine: Engine;
81
143
  if (conf.engine === 'postgres') {
144
+ let connectionString = conf.url;
145
+ if (conf.ssh) { // Cách B: tunnel the DB socket through a bastion
146
+ const u = new URL(conf.url);
147
+ const t = await openSshTunnel(conf.ssh, u.hostname, Number(u.port || 5432));
148
+ this.tunnels.push(t);
149
+ u.hostname = t.host; u.port = String(t.port); // rewrite host:port → 127.0.0.1:<tunnel> (keep user/pass/db/query)
150
+ connectionString = u.toString();
151
+ }
82
152
  const { Pool } = require('pg');
83
- const pool = new Pool({ connectionString: conf.url, max: 2, statement_timeout: conf.statement_timeout_ms ?? 4000 });
153
+ const pool = new Pool({ connectionString, max: 2, statement_timeout: conf.statement_timeout_ms ?? 4000 });
84
154
  engine = { query: async (sql, params) => (await pool.query(sql, params)).rows };
85
155
  } else if (conf.engine === 'sqlite') {
156
+ if (conf.ssh) console.warn(`Data Driver: datasource "${key}" sets ssh: but engine is sqlite (file-based) — ssh ignored.`);
86
157
  const Database = require('better-sqlite3');
87
158
  const db = new Database(conf.url.replace(/^sqlite:/, ''), { readonly: conf.readonly !== false });
88
159
  engine = { query: async (sql, params) => db.prepare(sql).all(...params) };
@@ -93,6 +164,12 @@ class DataSource {
93
164
  return { engine, conf };
94
165
  }
95
166
 
167
+ /** Close any open SSH tunnels (optional explicit teardown; tunnels are unref'd so the process exits regardless). */
168
+ close(): void {
169
+ for (const t of this.tunnels) t.close();
170
+ this.tunnels = [];
171
+ }
172
+
96
173
  private build(table: string, filter: Record<string, any>): { sql: string; params: any[] } {
97
174
  const cols = Object.keys(filter);
98
175
  const where = cols.map((c, i) => `${ident(c)} = $${i + 1}`).join(' AND ');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sun-asterisk/sungen",
3
- "version": "3.1.2-beta.101",
3
+ "version": "3.1.2-beta.103",
4
4
  "description": "Deterministic E2E Test Compiler - Gherkin + Selectors → Playwright tests",
5
5
  "main": "src/index.ts",
6
6
  "types": "src/index.ts",
@@ -33,7 +33,7 @@
33
33
  "node": ">=18.0.0"
34
34
  },
35
35
  "dependencies": {
36
- "@sungen/driver-ui": "3.1.2-beta.101",
36
+ "@sungen/driver-ui": "3.1.2-beta.103",
37
37
  "@anthropic-ai/sdk": "^0.71.0",
38
38
  "@babel/parser": "^7.28.5",
39
39
  "@babel/traverse": "^7.28.5",
@@ -69,6 +69,20 @@ A screen often matches several patterns at once — a login screen is *both* a f
69
69
  - VP-LOGIC = outcome depends on the user's *action* (click, submit, navigate)
70
70
  - VP-SEC = checks access control and malicious input
71
71
 
72
+ ### Domain category codes — required for the coverage-balance gate
73
+
74
+ The 4 viewpoints above are the *generic* axes. On a domain screen, the `VP-<CAT>` code must use the **canonical short code** for what the scenario exercises, so the audit's coverage-balance gate buckets it correctly. Use these exact codes — **never long-form or freeform** (`VP-NAV` not `VP-NAVIGATION`, `VP-SUB` not `VP-SUBSCRIPTION`, `VP-FILTER` not `VP-FILTERING`):
75
+
76
+ | Bucket | Codes | Use for |
77
+ |---|---|---|
78
+ | **business-core** | `LIST` · `CART` · `PRODUCT` · `FILTER` · `CHECKOUT` · `ORDER` | the screen's core domain data/actions (product list, cart, checkout, order, filtered results) |
79
+ | presentation | `UI` | layout / visual state |
80
+ | validation-security | `VAL` · `SEC` · `SUB` | input validation · access/injection · subscribe/newsletter |
81
+ | behavior | `LOGIC` | action-driven state changes |
82
+ | navigation | `NAV` | landing on / moving between pages |
83
+
84
+ **On a business-core page** (product list, cart, checkout, search results), the core data scenarios MUST carry a **business-core** code (`VP-LIST-*`, `VP-CART-*`, `VP-PRODUCT-*`, …) — not a generic `VP-UI`/`VP-LOGIC` or a freeform `VP-<word>`. A freeform/long-form prefix parses as `NONE`, scores **0 on the balance axis**, and drops the audit score (~9.3 → ~7.7 in practice). Keep `VP-UI/VAL/LOGIC/SEC` for the cross-cutting checks; give the domain scenarios their domain code.
85
+
72
86
  ---
73
87
 
74
88
  ## Shared Checks
@@ -69,6 +69,20 @@ A screen often matches several patterns at once — a login screen is *both* a f
69
69
  - VP-LOGIC = outcome depends on the user's *action* (click, submit, navigate)
70
70
  - VP-SEC = checks access control and malicious input
71
71
 
72
+ ### Domain category codes — required for the coverage-balance gate
73
+
74
+ The 4 viewpoints above are the *generic* axes. On a domain screen, the `VP-<CAT>` code must use the **canonical short code** for what the scenario exercises, so the audit's coverage-balance gate buckets it correctly. Use these exact codes — **never long-form or freeform** (`VP-NAV` not `VP-NAVIGATION`, `VP-SUB` not `VP-SUBSCRIPTION`, `VP-FILTER` not `VP-FILTERING`):
75
+
76
+ | Bucket | Codes | Use for |
77
+ |---|---|---|
78
+ | **business-core** | `LIST` · `CART` · `PRODUCT` · `FILTER` · `CHECKOUT` · `ORDER` | the screen's core domain data/actions (product list, cart, checkout, order, filtered results) |
79
+ | presentation | `UI` | layout / visual state |
80
+ | validation-security | `VAL` · `SEC` · `SUB` | input validation · access/injection · subscribe/newsletter |
81
+ | behavior | `LOGIC` | action-driven state changes |
82
+ | navigation | `NAV` | landing on / moving between pages |
83
+
84
+ **On a business-core page** (product list, cart, checkout, search results), the core data scenarios MUST carry a **business-core** code (`VP-LIST-*`, `VP-CART-*`, `VP-PRODUCT-*`, …) — not a generic `VP-UI`/`VP-LOGIC` or a freeform `VP-<word>`. A freeform/long-form prefix parses as `NONE`, scores **0 on the balance axis**, and drops the audit score (~9.3 → ~7.7 in practice). Keep `VP-UI/VAL/LOGIC/SEC` for the cross-cutting checks; give the domain scenarios their domain code.
85
+
72
86
  ---
73
87
 
74
88
  ## Shared Checks
@@ -21,12 +21,73 @@ const ident = (s: string): string => {
21
21
  return s;
22
22
  };
23
23
 
24
+ interface SshConfig {
25
+ host: string; // jump host reachable from the runner
26
+ port?: number; // default 22
27
+ user: string;
28
+ private_key?: string; // PEM contents (from ${VAR} in .env.qa) — preferred for CI
29
+ private_key_path?: string; // or a filesystem path (local dev)
30
+ passphrase?: string; // for an encrypted key
31
+ known_host?: string; // base64 of the server's host key to pin (optional; else warn-and-proceed)
32
+ }
33
+
24
34
  interface DataSourceConfig {
25
35
  engine: 'postgres' | 'mysql' | 'sqlite';
26
36
  url: string;
27
37
  readonly?: boolean;
28
38
  statement_timeout_ms?: number;
29
39
  max_rows?: number;
40
+ // Cách B (fallback): tunnel the DB SOCKET through an SSH bastion. DB-only — the browser/E2E
41
+ // still run on the runner; only PG traffic crosses. See docs/spec/sungen_data_driver_ssh_tunnel_spec.md.
42
+ ssh?: SshConfig;
43
+ }
44
+
45
+ /**
46
+ * Open a local TCP forward (127.0.0.1:<ephemeral> → ssh bastion → dstHost:dstPort) for a DB socket.
47
+ * Sockets are unref()'d so a dangling tunnel never keeps the test process alive after the run.
48
+ */
49
+ async function openSshTunnel(ssh: SshConfig, dstHost: string, dstPort: number): Promise<{ host: string; port: number; close: () => void }> {
50
+ const { Client } = require('ssh2');
51
+ const net = require('net');
52
+ const privateKey = ssh.private_key
53
+ ? ssh.private_key
54
+ : ssh.private_key_path
55
+ ? fs.readFileSync(ssh.private_key_path.replace(/^~(?=\/)/, process.env.HOME || ''), 'utf8')
56
+ : undefined;
57
+ if (!privateKey) throw new Error('Data Driver: datasource `ssh` requires `private_key` or `private_key_path`.');
58
+
59
+ const conn = new Client();
60
+ await new Promise<void>((resolve, reject) => {
61
+ conn.on('ready', resolve).on('error', reject).connect({
62
+ host: ssh.host,
63
+ port: ssh.port ?? 22,
64
+ username: ssh.user,
65
+ privateKey,
66
+ passphrase: ssh.passphrase,
67
+ hostVerifier: (key: Buffer) => {
68
+ const got = Buffer.isBuffer(key) ? key.toString('base64') : String(key);
69
+ if (ssh.known_host) {
70
+ if (got === ssh.known_host.trim()) return true;
71
+ throw new Error(`Data Driver: SSH host-key mismatch for ${ssh.host} — refused (known_host pin).`);
72
+ }
73
+ console.warn(`Data Driver: SSH host key for ${ssh.host} is not pinned (set datasource ssh.known_host to verify). Proceeding (TOFU).`);
74
+ return true;
75
+ },
76
+ });
77
+ });
78
+
79
+ const server = net.createServer((sock: any) => {
80
+ conn.forwardOut(sock.remoteAddress || '127.0.0.1', sock.remotePort || 0, dstHost, dstPort, (err: any, stream: any) => {
81
+ if (err) { sock.destroy(); return; }
82
+ sock.pipe(stream).pipe(sock);
83
+ });
84
+ });
85
+ await new Promise<void>((resolve, reject) => server.on('error', reject).listen(0, '127.0.0.1', () => resolve()));
86
+ const addr = server.address();
87
+ const port = addr && typeof addr === 'object' ? addr.port : 0;
88
+ server.unref(); // don't keep the event loop alive after tests
89
+ try { (conn as any)._sock?.unref?.(); } catch { /* best-effort */ }
90
+ return { host: '127.0.0.1', port, close: () => { try { server.close(); } catch {} try { conn.end(); } catch {} } };
30
91
  }
31
92
 
32
93
  function loadEnvQa(): void {
@@ -64,6 +125,7 @@ type Engine = { query(sql: string, params: any[]): Promise<any[]>; };
64
125
  class DataSource {
65
126
  private configs: Record<string, DataSourceConfig> | null = null;
66
127
  private engines = new Map<string, Engine>();
128
+ private tunnels: Array<{ close: () => void }> = [];
67
129
 
68
130
  private cfg(name?: string): { key: string; conf: DataSourceConfig } {
69
131
  if (!this.configs) this.configs = loadConfig();
@@ -79,10 +141,19 @@ class DataSource {
79
141
  if (!conf.url) throw new Error(`Data Driver: datasource "${key}" has no url (set it in .env.qa).`);
80
142
  let engine: Engine;
81
143
  if (conf.engine === 'postgres') {
144
+ let connectionString = conf.url;
145
+ if (conf.ssh) { // Cách B: tunnel the DB socket through a bastion
146
+ const u = new URL(conf.url);
147
+ const t = await openSshTunnel(conf.ssh, u.hostname, Number(u.port || 5432));
148
+ this.tunnels.push(t);
149
+ u.hostname = t.host; u.port = String(t.port); // rewrite host:port → 127.0.0.1:<tunnel> (keep user/pass/db/query)
150
+ connectionString = u.toString();
151
+ }
82
152
  const { Pool } = require('pg');
83
- const pool = new Pool({ connectionString: conf.url, max: 2, statement_timeout: conf.statement_timeout_ms ?? 4000 });
153
+ const pool = new Pool({ connectionString, max: 2, statement_timeout: conf.statement_timeout_ms ?? 4000 });
84
154
  engine = { query: async (sql, params) => (await pool.query(sql, params)).rows };
85
155
  } else if (conf.engine === 'sqlite') {
156
+ if (conf.ssh) console.warn(`Data Driver: datasource "${key}" sets ssh: but engine is sqlite (file-based) — ssh ignored.`);
86
157
  const Database = require('better-sqlite3');
87
158
  const db = new Database(conf.url.replace(/^sqlite:/, ''), { readonly: conf.readonly !== false });
88
159
  engine = { query: async (sql, params) => db.prepare(sql).all(...params) };
@@ -93,6 +164,12 @@ class DataSource {
93
164
  return { engine, conf };
94
165
  }
95
166
 
167
+ /** Close any open SSH tunnels (optional explicit teardown; tunnels are unref'd so the process exits regardless). */
168
+ close(): void {
169
+ for (const t of this.tunnels) t.close();
170
+ this.tunnels = [];
171
+ }
172
+
96
173
  private build(table: string, filter: Record<string, any>): { sql: string; params: any[] } {
97
174
  const cols = Object.keys(filter);
98
175
  const where = cols.map((c, i) => `${ident(c)} = $${i + 1}`).join(' AND ');