@mason-data/cli 0.0.0-stage → 0.6.2
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/LICENSE +202 -0
- package/README.md +209 -2
- package/dist/src/api/error.js +18 -0
- package/dist/src/api/postgres/catalog-worker.js +92 -0
- package/dist/src/api/postgres/catalog.js +442 -0
- package/dist/src/api/postgres/encode.js +541 -0
- package/dist/src/api/postgres/explain.js +143 -0
- package/dist/src/api/postgres/listener.js +60 -0
- package/dist/src/api/postgres/parser-worker.js +58 -0
- package/dist/src/api/postgres/parser.js +121 -0
- package/dist/src/api/postgres/plain.js +33 -0
- package/dist/src/api/postgres/protocol.js +404 -0
- package/dist/src/api/postgres/schema.js +54 -0
- package/dist/src/api/postgres/server.js +1670 -0
- package/dist/src/api/postgres/translate.js +248 -0
- package/dist/src/api/request-log.js +50 -0
- package/dist/src/cdc/pg-wire.js +482 -0
- package/dist/src/cdc/pgoutput.js +107 -0
- package/dist/src/cdc/poll.js +67 -0
- package/dist/src/cdc/source.js +65 -0
- package/dist/src/cli/background.js +174 -0
- package/dist/src/cli/command.js +172 -0
- package/dist/src/cli/commands.js +1145 -0
- package/dist/src/cli/control.js +36 -0
- package/dist/src/cli/dev.js +576 -0
- package/dist/src/cli/doctor.js +257 -0
- package/dist/src/cli/guides.js +134 -0
- package/dist/src/cli/init.js +326 -0
- package/dist/src/cli/machine.js +157 -0
- package/dist/src/cli/main.js +83 -0
- package/dist/src/cli/pg-tls.js +100 -0
- package/dist/src/cli/pg-url.js +52 -0
- package/dist/src/cli/pin.js +36 -0
- package/dist/src/cli/ports.js +118 -0
- package/dist/src/cli/preflight.js +338 -0
- package/dist/src/cli/project.js +127 -0
- package/dist/src/cli/readiness.js +76 -0
- package/dist/src/cli/registry.js +217 -0
- package/dist/src/cli/replication-checks.js +120 -0
- package/dist/src/cli/replication-mode.js +148 -0
- package/dist/src/cli/run-server.js +388 -0
- package/dist/src/cli/runs.js +120 -0
- package/dist/src/cli/security.js +86 -0
- package/dist/src/cli/self-signed-cert.js +64 -0
- package/dist/src/cli/serve.js +296 -0
- package/dist/src/cli/sources.js +562 -0
- package/dist/src/cli/state.js +58 -0
- package/dist/src/cli/style.js +187 -0
- package/dist/src/cli/supervise.js +248 -0
- package/dist/src/cli/sync.js +94 -0
- package/dist/src/cli/ui.js +107 -0
- package/dist/src/cli/user.js +229 -0
- package/dist/src/cli/version.js +23 -0
- package/dist/src/duck/analyze.js +113 -0
- package/dist/src/duck/cancel.js +49 -0
- package/dist/src/duck/executor-worker.js +263 -0
- package/dist/src/duck/executor.js +526 -0
- package/dist/src/duck/explain.js +119 -0
- package/dist/src/duck/functions.js +764 -0
- package/dist/src/duck/gaps.js +89 -0
- package/dist/src/duck/jsonb.js +186 -0
- package/dist/src/duck/kept-route.js +219 -0
- package/dist/src/duck/kept.js +797 -0
- package/dist/src/duck/late.js +221 -0
- package/dist/src/duck/libc-order.js +190 -0
- package/dist/src/duck/pg-eval-worker.js +34 -0
- package/dist/src/duck/pg-eval.js +59 -0
- package/dist/src/duck/pgtree.js +338 -0
- package/dist/src/duck/plan.js +246 -0
- package/dist/src/duck/publication.js +207 -0
- package/dist/src/duck/requests.js +209 -0
- package/dist/src/duck/resolved.js +248 -0
- package/dist/src/duck/rewrite.js +4001 -0
- package/dist/src/duck/store.js +831 -0
- package/dist/src/duck/stream.js +518 -0
- package/dist/src/duck/sync.js +2864 -0
- package/dist/src/duck/to-char.js +482 -0
- package/dist/src/duck/token.js +31 -0
- package/dist/src/duck/types.js +427 -0
- package/dist/src/duck/users.js +231 -0
- package/dist/src/duck/verified.js +995 -0
- package/dist/src/duck/views.js +150 -0
- package/dist/src/lib/database-url.js +74 -0
- package/dist/src/lib/did-you-mean.js +43 -0
- package/dist/src/lib/dotenv.js +72 -0
- package/dist/src/lib/exit-when-written.js +20 -0
- package/dist/src/lib/float-equal.js +69 -0
- package/dist/src/lib/http-json.js +22 -0
- package/dist/src/lib/log.js +161 -0
- package/dist/src/lib/pg-numeric.js +489 -0
- package/dist/src/lib/port.js +22 -0
- package/dist/src/lib/project-file.js +49 -0
- package/dist/src/lib/project-files.js +43 -0
- package/dist/src/lib/project-version.js +76 -0
- package/dist/src/lib/runnable.js +14 -0
- package/dist/src/lib/shell-word.js +4 -0
- package/dist/src/lib/timezone.js +236 -0
- package/dist/src/lib/zone-names.js +114 -0
- package/dist/src/web/app.css +65 -0
- package/dist/src/web/app.js +336 -0
- package/dist/src/web/index.html +39 -0
- package/package.json +22 -4
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
/** A statement, or a part of one, this listener does not answer: named, placed, with a SQLSTATE and the way out. */
|
|
2
|
+
export class Unsupported extends Error {
|
|
3
|
+
/** 0-based offset into the statement's text, or null when the whole statement is meant */
|
|
4
|
+
offset;
|
|
5
|
+
/** `0A000` feature not supported; `42601` syntax; `25006` read only; `22023` an invalid value */
|
|
6
|
+
sqlstate;
|
|
7
|
+
hint;
|
|
8
|
+
constructor(message, offset, sqlstate = "0A000", hint) {
|
|
9
|
+
super(message);
|
|
10
|
+
this.offset = offset;
|
|
11
|
+
this.sqlstate = sqlstate;
|
|
12
|
+
this.hint = hint;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
const kindOf = (n) => Object.keys(n)[0] ?? "";
|
|
16
|
+
const bodyOf = (n) => n[kindOf(n)];
|
|
17
|
+
const strings = (nodes) => (nodes ?? []).map((n) => kindOf(n) === "String" ? String(bodyOf(n).sval ?? "") : "");
|
|
18
|
+
const loc = (n) => {
|
|
19
|
+
if (n === undefined)
|
|
20
|
+
return null;
|
|
21
|
+
const b = bodyOf(n); // `{}` (a plain DISTINCT's entry) has no body
|
|
22
|
+
return typeof b?.location === "number" && b.location >= 0 ? b.location : null;
|
|
23
|
+
};
|
|
24
|
+
/** every node of a kind, anywhere under `n` */
|
|
25
|
+
function collect(n, kind, out = []) {
|
|
26
|
+
if (Array.isArray(n)) {
|
|
27
|
+
for (const x of n)
|
|
28
|
+
collect(x, kind, out);
|
|
29
|
+
return out;
|
|
30
|
+
}
|
|
31
|
+
if (n === null || typeof n !== "object")
|
|
32
|
+
return out;
|
|
33
|
+
const o = n;
|
|
34
|
+
for (const [k, v] of Object.entries(o)) {
|
|
35
|
+
if (k === kind)
|
|
36
|
+
out.push({ [k]: v });
|
|
37
|
+
collect(v, kind, out);
|
|
38
|
+
}
|
|
39
|
+
return out;
|
|
40
|
+
}
|
|
41
|
+
const INFORMATION_SCHEMA = "information_schema";
|
|
42
|
+
const PG_CATALOG = "pg_catalog";
|
|
43
|
+
// ------------------------------------------------------------------ entry
|
|
44
|
+
/** A statement other than a SELECT or an EXPLAIN (`duckTranslate` takes those): the session's, or refused by name. */
|
|
45
|
+
function sessionStatement(stmt) {
|
|
46
|
+
const kind = kindOf(stmt);
|
|
47
|
+
switch (kind) {
|
|
48
|
+
case "VariableSetStmt": return setStatement(bodyOf(stmt));
|
|
49
|
+
case "VariableShowStmt": return { kind: "show", name: String(bodyOf(stmt).name ?? "").toLowerCase() };
|
|
50
|
+
case "TransactionStmt": {
|
|
51
|
+
const k = bodyOf(stmt).kind;
|
|
52
|
+
const tag = k === "TRANS_STMT_BEGIN" ? "BEGIN" : k === "TRANS_STMT_START" ? "START TRANSACTION" : k === "TRANS_STMT_COMMIT" ? "COMMIT" : k === "TRANS_STMT_ROLLBACK" ? "ROLLBACK" : null;
|
|
53
|
+
if (tag === null)
|
|
54
|
+
throw new Unsupported(`${String(k).replace("TRANS_STMT_", "").toLowerCase()}: this connection is read-only and has no transactions; BEGIN, COMMIT and ROLLBACK are accepted and do nothing`, null);
|
|
55
|
+
return { kind: "transaction", tag };
|
|
56
|
+
}
|
|
57
|
+
case "DiscardStmt": return { kind: "discard" };
|
|
58
|
+
case "DeallocateStmt": return { kind: "deallocate" };
|
|
59
|
+
case "InsertStmt":
|
|
60
|
+
case "UpdateStmt":
|
|
61
|
+
case "DeleteStmt":
|
|
62
|
+
case "MergeStmt":
|
|
63
|
+
case "CopyStmt":
|
|
64
|
+
case "TruncateStmt":
|
|
65
|
+
case "CreateStmt":
|
|
66
|
+
case "DropStmt":
|
|
67
|
+
case "AlterTableStmt":
|
|
68
|
+
case "CreateTableAsStmt":
|
|
69
|
+
case "ViewStmt":
|
|
70
|
+
case "IndexStmt":
|
|
71
|
+
case "GrantStmt":
|
|
72
|
+
case "CreateRoleStmt":
|
|
73
|
+
case "CreateFunctionStmt":
|
|
74
|
+
case "CreateSchemaStmt":
|
|
75
|
+
case "VacuumStmt":
|
|
76
|
+
throw new Unsupported(`${kind.replace(/Stmt$/, "").replace(/([a-z])([A-Z])/g, "$1 $2").toUpperCase()}: this is a read-only copy of the database's tables; only SELECT, SET, SHOW and the transaction no-ops are accepted`, null, "25006");
|
|
77
|
+
case "PrepareStmt":
|
|
78
|
+
case "ExecuteStmt": throw new Unsupported(`${kind === "PrepareStmt" ? "PREPARE" : "EXECUTE"} is not supported: use the protocol's prepared statements (every driver does)`, null);
|
|
79
|
+
default:
|
|
80
|
+
throw new Unsupported(`${kind.replace(/Stmt$/, "")}: not a statement this listener answers (SELECT, EXPLAIN, a catalog query, SET, SHOW, BEGIN/COMMIT)`, null);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* ONE STATEMENT CLASSIFIED (docs/DESIGN.md decision D): a SELECT that reads any table outside pg_catalog /
|
|
85
|
+
* information_schema is DuckDB's (`text`, the parser's slice of the message, for the plan to be made from); one that
|
|
86
|
+
* reads only the catalog, or no table at all (`SELECT version()`, `SELECT 1`), is the catalog Postgres's. Every other
|
|
87
|
+
* statement is the session's or refused (`sessionStatement`), but an EXPLAIN, which is the statement it explains.
|
|
88
|
+
* NOTHING reaches DuckDB but a SELECT Postgres analysed and the rewrite read back (src/duck/plan.ts): DuckDB's `EXPLAIN
|
|
89
|
+
* ANALYZE` runs the statement under it, and its text, unanalysed, could write to the copy or read the attached source
|
|
90
|
+
* (a review finding).
|
|
91
|
+
*/
|
|
92
|
+
export function duckTranslate(stmt, text) {
|
|
93
|
+
const kind = kindOf(stmt);
|
|
94
|
+
// an EXPLAIN is the statement it explains (src/api/postgres/explain.ts, src/duck/explain.ts): its options read, then
|
|
95
|
+
// that statement classified as it would be alone, by the server
|
|
96
|
+
if (kind === "ExplainStmt") {
|
|
97
|
+
const e = bodyOf(stmt);
|
|
98
|
+
if (e.query === undefined)
|
|
99
|
+
throw new Unsupported("EXPLAIN of nothing", null, "42601");
|
|
100
|
+
return { kind: "explain", query: e.query, options: e.options ?? [] };
|
|
101
|
+
}
|
|
102
|
+
if (kind !== "SelectStmt")
|
|
103
|
+
return sessionStatement(stmt);
|
|
104
|
+
const s = bodyOf(stmt);
|
|
105
|
+
const ctes = (s.withClause?.ctes ?? []).map((n) => bodyOf(n));
|
|
106
|
+
const names = new Set(ctes.map((c) => String(c.ctename ?? "")));
|
|
107
|
+
const relations = collect(s, "RangeVar").map((n) => bodyOf(n)).filter((r) => r.schemaname !== undefined || !names.has(String(r.relname ?? "")));
|
|
108
|
+
if (relations.every(isCatalogRelation))
|
|
109
|
+
return { kind: "catalog" };
|
|
110
|
+
return { kind: "duckdb", sql: text, names: pgColumnNames(s) };
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* THE NAMES POSTGRES GIVES A SELECT'S RESULT COLUMNS (`FigureColname`, src/backend/parser/parse_target.c), where DuckDB
|
|
114
|
+
* gives others (`count(*)` is `count_star()`, `sum(x)` is `sum(x)`; Postgres says `count`, `sum`): an alias as written,
|
|
115
|
+
* a column's name, a function's, a cast's inner name or else its type's, `case`, `coalesce` …, and `?column?` for the
|
|
116
|
+
* rest. A `*` is DuckDB's (its width is known only from the result: `null` there, one star at most). A set operation's
|
|
117
|
+
* names are its first arm's. Null when the list cannot be named (two stars): DuckDB's names stand.
|
|
118
|
+
*/
|
|
119
|
+
export function pgColumnNames(s) {
|
|
120
|
+
if (s.op !== undefined && s.op !== "SETOP_NONE" && s.larg !== undefined)
|
|
121
|
+
return pgColumnNames(s.larg);
|
|
122
|
+
if (s.valuesLists !== undefined && s.valuesLists.length > 0) {
|
|
123
|
+
const first = s.valuesLists[0];
|
|
124
|
+
const width = kindOf(first) === "List" ? (bodyOf(first).items ?? []).length : 0;
|
|
125
|
+
return Array.from({ length: width }, (_, i) => `column${i + 1}`);
|
|
126
|
+
}
|
|
127
|
+
const names = [];
|
|
128
|
+
let stars = 0;
|
|
129
|
+
for (const t of s.targetList ?? []) {
|
|
130
|
+
const rt = bodyOf(t);
|
|
131
|
+
if (rt.name !== undefined) {
|
|
132
|
+
names.push(rt.name);
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
if (rt.val !== undefined && kindOf(rt.val) === "ColumnRef" && (bodyOf(rt.val).fields ?? []).some((f) => kindOf(f) === "A_Star")) {
|
|
136
|
+
stars++;
|
|
137
|
+
names.push(null);
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
names.push(rt.val === undefined ? "?column?" : figureColname(rt.val)[0] ?? "?column?");
|
|
141
|
+
}
|
|
142
|
+
return stars > 1 ? null : names;
|
|
143
|
+
}
|
|
144
|
+
const SQL_VALUE_NAMES = {
|
|
145
|
+
SVFOP_CURRENT_DATE: "current_date", SVFOP_CURRENT_TIME: "current_time", SVFOP_CURRENT_TIME_N: "current_time", SVFOP_CURRENT_TIMESTAMP: "current_timestamp",
|
|
146
|
+
SVFOP_CURRENT_TIMESTAMP_N: "current_timestamp", SVFOP_LOCALTIME: "localtime", SVFOP_LOCALTIME_N: "localtime", SVFOP_LOCALTIMESTAMP: "localtimestamp",
|
|
147
|
+
SVFOP_LOCALTIMESTAMP_N: "localtimestamp", SVFOP_CURRENT_ROLE: "current_role", SVFOP_CURRENT_USER: "current_user", SVFOP_USER: "user",
|
|
148
|
+
SVFOP_SESSION_USER: "session_user", SVFOP_CURRENT_CATALOG: "current_catalog", SVFOP_CURRENT_SCHEMA: "current_schema",
|
|
149
|
+
};
|
|
150
|
+
/** FigureColnameInternal: the name and its strength (0 none, 1 a fallback such as a cast's type or `case`, 2 a real one) */
|
|
151
|
+
function figureColname(n) {
|
|
152
|
+
const k = kindOf(n);
|
|
153
|
+
const b = bodyOf(n);
|
|
154
|
+
switch (k) {
|
|
155
|
+
case "ColumnRef": {
|
|
156
|
+
const last = strings(b.fields).filter((x) => x !== "").pop();
|
|
157
|
+
return last === undefined ? [null, 0] : [last, 2];
|
|
158
|
+
}
|
|
159
|
+
case "A_Indirection": {
|
|
160
|
+
const ind = b.indirection ?? [];
|
|
161
|
+
const last = ind[ind.length - 1];
|
|
162
|
+
if (last !== undefined && kindOf(last) === "String")
|
|
163
|
+
return [String(bodyOf(last).sval ?? ""), 2];
|
|
164
|
+
return b.arg ? figureColname(b.arg) : [null, 0];
|
|
165
|
+
}
|
|
166
|
+
case "FuncCall": {
|
|
167
|
+
const last = strings(b.funcname).pop();
|
|
168
|
+
return last === undefined ? [null, 0] : [last, 2];
|
|
169
|
+
}
|
|
170
|
+
case "A_Expr": return b.kind === "AEXPR_NULLIF" ? ["nullif", 2] : [null, 0];
|
|
171
|
+
case "TypeCast": {
|
|
172
|
+
const inner = b.arg ? figureColname(b.arg) : [null, 0];
|
|
173
|
+
if (inner[1] > 0)
|
|
174
|
+
return inner;
|
|
175
|
+
const typeName = strings(b.typeName?.names).pop();
|
|
176
|
+
return typeName === undefined ? [null, 0] : [typeName, 1];
|
|
177
|
+
}
|
|
178
|
+
case "CollateClause": return b.arg ? figureColname(b.arg) : [null, 0];
|
|
179
|
+
case "GroupingFunc": return ["grouping", 2];
|
|
180
|
+
case "SubLink": {
|
|
181
|
+
if (b.subLinkType === "EXISTS_SUBLINK")
|
|
182
|
+
return ["exists", 2];
|
|
183
|
+
if (b.subLinkType === "ARRAY_SUBLINK")
|
|
184
|
+
return ["array", 2];
|
|
185
|
+
if (b.subLinkType === "EXPR_SUBLINK" && b.subselect !== undefined && kindOf(b.subselect) === "SelectStmt") {
|
|
186
|
+
const first = bodyOf(b.subselect).targetList?.[0];
|
|
187
|
+
if (first !== undefined) {
|
|
188
|
+
const rt = bodyOf(first);
|
|
189
|
+
if (rt.name !== undefined)
|
|
190
|
+
return [rt.name, 2];
|
|
191
|
+
if (rt.val !== undefined)
|
|
192
|
+
return figureColname(rt.val);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
return [null, 0];
|
|
196
|
+
}
|
|
197
|
+
case "CaseExpr": {
|
|
198
|
+
const d = b.defresult ? figureColname(b.defresult) : [null, 0];
|
|
199
|
+
return d[1] <= 1 ? ["case", 1] : d;
|
|
200
|
+
}
|
|
201
|
+
case "A_ArrayExpr": return ["array", 2];
|
|
202
|
+
case "RowExpr": return ["row", 2];
|
|
203
|
+
case "CoalesceExpr": return ["coalesce", 2];
|
|
204
|
+
case "MinMaxExpr": return [b.op === "IS_LEAST" ? "least" : "greatest", 2];
|
|
205
|
+
case "SQLValueFunction": {
|
|
206
|
+
const v = SQL_VALUE_NAMES[String(b.op)];
|
|
207
|
+
return v === undefined ? [null, 0] : [v, 2];
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
// every other expression has no name of its own: Postgres calls it ?column? (a naming rule, not an accepting one)
|
|
211
|
+
return [null, 0];
|
|
212
|
+
}
|
|
213
|
+
function setStatement(s) {
|
|
214
|
+
const name = String(s.name ?? "").toLowerCase();
|
|
215
|
+
if (s.kind === "VAR_RESET" || s.kind === "VAR_SET_DEFAULT")
|
|
216
|
+
return { kind: "set", name, value: null, local: s.is_local === true };
|
|
217
|
+
if (s.kind === "VAR_RESET_ALL")
|
|
218
|
+
return { kind: "set", name: "all", value: null, local: false };
|
|
219
|
+
if (s.kind === "VAR_SET_MULTI")
|
|
220
|
+
return { kind: "set", name, value: "", local: s.is_local === true }; // SET SESSION CHARACTERISTICS …
|
|
221
|
+
if (s.kind !== "VAR_SET_VALUE")
|
|
222
|
+
throw new Unsupported(`SET ${name}: ${String(s.kind)} is not supported`, s.location ?? null);
|
|
223
|
+
const parts = (s.args ?? []).map((a) => {
|
|
224
|
+
if (kindOf(a) === "A_Const")
|
|
225
|
+
return constText(bodyOf(a));
|
|
226
|
+
if (kindOf(a) === "TypeCast") {
|
|
227
|
+
const c = bodyOf(a);
|
|
228
|
+
if (c.arg !== undefined && kindOf(c.arg) === "A_Const")
|
|
229
|
+
return constText(bodyOf(c.arg));
|
|
230
|
+
}
|
|
231
|
+
throw new Unsupported(`SET ${name}: a value of kind ${kindOf(a)} is not supported (a literal is)`, loc(a));
|
|
232
|
+
});
|
|
233
|
+
return { kind: "set", name, value: parts.join(", "), local: s.is_local === true };
|
|
234
|
+
}
|
|
235
|
+
function constText(c) {
|
|
236
|
+
if (c.sval !== undefined)
|
|
237
|
+
return String(c.sval.sval ?? "");
|
|
238
|
+
if (c.ival !== undefined)
|
|
239
|
+
return String(c.ival.ival ?? 0);
|
|
240
|
+
if (c.fval !== undefined)
|
|
241
|
+
return String(c.fval.fval ?? "0");
|
|
242
|
+
if (c.boolval !== undefined)
|
|
243
|
+
return c.boolval.boolval ? "on" : "off";
|
|
244
|
+
if (c.isnull === true)
|
|
245
|
+
return "";
|
|
246
|
+
return "";
|
|
247
|
+
}
|
|
248
|
+
const isCatalogRelation = (r) => r.schemaname === PG_CATALOG || r.schemaname === INFORMATION_SCHEMA || (r.schemaname === undefined && String(r.relname ?? "").startsWith("pg_"));
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ONE ROW OF THE REQUEST LOG, as the Postgres listener builds it (src/api/postgres/server.ts `recordRequest`) and the
|
|
3
|
+
* run's ledger keeps it (src/duck/requests.ts, `_mason.requests`): every statement answered or refused, and every
|
|
4
|
+
* refused login. The helpers here say who asked without keeping the credential (`hashCaller`), cap the statement
|
|
5
|
+
* (`storedPayload`) and name an outcome from its SQLSTATE (`outcomeOf`), read by the listener's refusal line and the
|
|
6
|
+
* ledger alike.
|
|
7
|
+
*/
|
|
8
|
+
import * as crypto from "node:crypto";
|
|
9
|
+
/** Fail closed on the storage side: never let one request bloat the ledger. */
|
|
10
|
+
export const MAX_STORED_PAYLOAD_BYTES = 64 * 1024;
|
|
11
|
+
/**
|
|
12
|
+
* A Postgres-listener statement's outcome in a word, from the SQLSTATE it was sent: `failed` where the fault is Mason's
|
|
13
|
+
* or the machine's (classes XX internal, 53 resources, 57 a timeout or a cancel, 58 system), `refused` for the
|
|
14
|
+
* statement's own, `ok` for none. Read by the listener's refusal line (server.ts `refusalLine`) and the request log
|
|
15
|
+
* (src/duck/requests.ts) alike.
|
|
16
|
+
*/
|
|
17
|
+
export function outcomeOf(sqlstate) {
|
|
18
|
+
if (sqlstate === null || sqlstate === undefined || sqlstate === "")
|
|
19
|
+
return "ok";
|
|
20
|
+
return /^(XX|53|57|58)/.test(sqlstate) ? "failed" : "refused";
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* The caller's identity WITHOUT the credential: a truncated SHA-256 of what was presented. Distinct callers stay
|
|
24
|
+
* distinguishable and repeat callers stay correlatable, while the secret itself is never written down — the log must
|
|
25
|
+
* not become a place where a leaked table hands over working credentials.
|
|
26
|
+
*
|
|
27
|
+
* Truncation to 16 hex characters (64 bits) is deliberate: enough that two real callers will not collide, short enough
|
|
28
|
+
* that the column stays cheap, and far too short to brute-force a high-entropy key back out of.
|
|
29
|
+
*/
|
|
30
|
+
export function hashCaller(presented) {
|
|
31
|
+
if (presented === null || presented === "")
|
|
32
|
+
return "";
|
|
33
|
+
return crypto.createHash("sha256").update(presented).digest("hex").slice(0, 16);
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* The statement as it arrived, capped. Stored whole, not redacted: the statements most worth having are the ones that
|
|
37
|
+
* FAILED, and a normalised one tells nothing about what actually arrived. The cap is a storage bound, not a privacy
|
|
38
|
+
* one, and `payloadTruncated` says plainly when a row is not the whole story.
|
|
39
|
+
*/
|
|
40
|
+
export function storedPayload(body) {
|
|
41
|
+
const buf = Buffer.from(body, "utf8");
|
|
42
|
+
if (buf.byteLength <= MAX_STORED_PAYLOAD_BYTES)
|
|
43
|
+
return { text: body, truncated: false };
|
|
44
|
+
// Slice on a character boundary: a half-written UTF-8 sequence would be
|
|
45
|
+
// stored as a replacement character and misread as corruption in the source.
|
|
46
|
+
return {
|
|
47
|
+
text: buf.subarray(0, MAX_STORED_PAYLOAD_BYTES).toString("utf8"),
|
|
48
|
+
truncated: true,
|
|
49
|
+
};
|
|
50
|
+
}
|