@volter/world-core 3.0.70 → 3.0.71

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.
@@ -67,6 +67,7 @@ export { acceptH2Connection, type H2Handler, type Http1Handler } from './h2.js';
67
67
  export { placeholderPng } from './placeholder-image.js';
68
68
  export { presignSigV4, signSigV4, verifySigV4, type SigV4Verdict } from './sigv4.js';
69
69
  export * as redis from './redis/index.js';
70
+ export * as mysql from './mysql/index.js';
70
71
  export * as git from './git/index.js';
71
72
  export { parseScenarioDocument, scenarioFaultResult, ScenarioEngine, ScenarioError } from './scenario.js';
72
73
  export type { PackScenarioAdapter, ScenarioDecision, ScenarioDocument, ScenarioExtractorSpec, ScenarioFault, ScenarioFaultResult, ScenarioFeatures, ScenarioHandler, ScenarioMatcher, ScenarioMissRecord, ScenarioStatus } from './scenario.js';
package/dist/src/index.js CHANGED
@@ -59,6 +59,7 @@ export { acceptH2Connection } from "./h2.js";
59
59
  export { placeholderPng } from "./placeholder-image.js";
60
60
  export { presignSigV4, signSigV4, verifySigV4 } from "./sigv4.js";
61
61
  export * as redis from "./redis/index.js";
62
+ export * as mysql from "./mysql/index.js";
62
63
  export * as git from "./git/index.js";
63
64
  // ── the scenario grammar (architecture, "Behaviour") ───────────────────────────────────────────
64
65
  export { parseScenarioDocument, scenarioFaultResult, ScenarioEngine, ScenarioError } from "./scenario.js";
@@ -0,0 +1,28 @@
1
+ import { type MysqlError } from './stream.js';
2
+ /** Sends bytes and returns every byte the server answered to them. */
3
+ export type MysqlExchange = (bytes: Uint8Array) => Promise<Uint8Array>;
4
+ export type MysqlResultSet = {
5
+ columns: {
6
+ name: string;
7
+ type: number;
8
+ flags: number;
9
+ }[];
10
+ rows: (string | null)[][];
11
+ };
12
+ export type MysqlAnswer = MysqlResultSet | {
13
+ affectedRows: number;
14
+ insertId: number;
15
+ } | {
16
+ error: MysqlError;
17
+ };
18
+ /** Sign in: the server's greeting, then the handshake response (mysql_native_password), following an auth switch. */
19
+ export declare function mysqlSignIn(exchange: MysqlExchange, greeting: Uint8Array, user: string, password: string, database: string | null): Promise<{
20
+ ok: true;
21
+ } | {
22
+ error: MysqlError;
23
+ }>;
24
+ /** A text query (COM_QUERY): the first result of a script, as mysql_async reads it. */
25
+ export declare function mysqlQuery(exchange: MysqlExchange, sql: string): Promise<MysqlAnswer>;
26
+ /** A prepared statement executed once with text-typed parameters (COM_STMT_PREPARE, EXECUTE, CLOSE), as Prisma sends a
27
+ * parameterised query: binary parameters and rows. */
28
+ export declare function mysqlExecute(exchange: MysqlExchange, sql: string, params: (string | null)[]): Promise<MysqlAnswer>;
@@ -0,0 +1,220 @@
1
+ // A MySQL client over any byte exchange: the other side of ./stream.ts, so a journey speaks the wire as an application's
2
+ // driver does (mysql_async under Prisma's engines): protocol v10 with mysql_native_password, text queries, and prepared
3
+ // statements with binary parameters and rows. It reads what the protocol answers; it decides nothing.
4
+ // source: https://dev.mysql.com/doc/dev/mysql-server/latest/page_protocol_basics.html
5
+ import { MysqlType, MysqlFlag, nativePasswordToken } from "./stream.js";
6
+ const enc = new TextEncoder();
7
+ const dec = new TextDecoder();
8
+ const CLIENT_CAPS = 0x1 | 0x4 | 0x8 | 0x200 | 0x2000 | 0x8000 | 0x10000 | 0x20000 | 0x40000 | 0x80000 | 0x200000;
9
+ function packet(seq, payload) {
10
+ const out = new Uint8Array(4 + payload.length);
11
+ out[0] = payload.length & 0xff;
12
+ out[1] = (payload.length >> 8) & 0xff;
13
+ out[2] = (payload.length >> 16) & 0xff;
14
+ out[3] = seq;
15
+ out.set(payload, 4);
16
+ return out;
17
+ }
18
+ function packets(bytes) {
19
+ const out = [];
20
+ let at = 0;
21
+ while (at + 4 <= bytes.length) {
22
+ const len = bytes[at] | (bytes[at + 1] << 8) | (bytes[at + 2] << 16);
23
+ out.push(bytes.subarray(at + 4, at + 4 + len));
24
+ at += 4 + len;
25
+ }
26
+ return out;
27
+ }
28
+ class Cursor {
29
+ b;
30
+ at = 0;
31
+ constructor(b) {
32
+ this.b = b;
33
+ }
34
+ u8() { return this.b[this.at++]; }
35
+ u16() { return this.u8() | (this.u8() << 8); }
36
+ u32() { return (this.u16() | (this.u16() << 16)) >>> 0; }
37
+ u64() { let n = 0n; for (let i = 0n; i < 8n; i++)
38
+ n |= BigInt(this.u8()) << (8n * i); return n; }
39
+ take(n) { const out = this.b.subarray(this.at, this.at + n); this.at += n; return out; }
40
+ nul() { const end = this.b.indexOf(0, this.at); const s = dec.decode(this.b.subarray(this.at, end)); this.at = end + 1; return s; }
41
+ lenenc() { const f = this.u8(); if (f < 0xfb)
42
+ return f; if (f === 0xfc)
43
+ return this.u16(); if (f === 0xfd)
44
+ return this.u16() | (this.u8() << 16); return Number(this.u64()); }
45
+ lenencStr() { return dec.decode(this.take(this.lenenc())); }
46
+ }
47
+ const errorOf = (p) => {
48
+ const c = new Cursor(p);
49
+ c.u8();
50
+ const errno = c.u16();
51
+ c.u8(); // '#'
52
+ const sqlstate = dec.decode(c.take(5));
53
+ return { errno, sqlstate, message: dec.decode(c.take(p.length - c.at)) };
54
+ };
55
+ function column(p) {
56
+ const c = new Cursor(p);
57
+ c.lenencStr();
58
+ c.lenencStr();
59
+ c.lenencStr();
60
+ c.lenencStr(); // catalog, schema, table, org_table
61
+ const name = c.lenencStr();
62
+ c.lenencStr();
63
+ c.lenenc(); // org_name, fixed-length 0x0c
64
+ c.u16();
65
+ c.u32();
66
+ const type = c.u8();
67
+ const flags = c.u16();
68
+ return { name, type, flags };
69
+ }
70
+ const pad = (n, w = 2) => String(n).padStart(w, '0');
71
+ /** A binary row's value as MySQL's text protocol writes it, so a journey reads both protocols alike. */
72
+ function binaryValue(c, type, flags) {
73
+ const unsigned = (flags & MysqlFlag.UNSIGNED) !== 0;
74
+ switch (type) {
75
+ case MysqlType.TINY: {
76
+ const v = c.u8();
77
+ return String(unsigned ? v : (v << 24) >> 24);
78
+ }
79
+ case MysqlType.SHORT:
80
+ case MysqlType.YEAR: {
81
+ const v = c.u16();
82
+ return String(unsigned ? v : (v << 16) >> 16);
83
+ }
84
+ case MysqlType.LONG:
85
+ case MysqlType.INT24: {
86
+ const v = c.u32();
87
+ return String(unsigned ? v : v | 0);
88
+ }
89
+ case MysqlType.LONGLONG: {
90
+ const v = c.u64();
91
+ return String(unsigned ? v : BigInt.asIntN(64, v));
92
+ }
93
+ case MysqlType.FLOAT: {
94
+ const b = c.take(4);
95
+ return String(new DataView(b.buffer, b.byteOffset, 4).getFloat32(0, true));
96
+ }
97
+ case MysqlType.DOUBLE: {
98
+ const b = c.take(8);
99
+ return String(new DataView(b.buffer, b.byteOffset, 8).getFloat64(0, true));
100
+ }
101
+ case MysqlType.DATE:
102
+ case MysqlType.DATETIME:
103
+ case MysqlType.TIMESTAMP: {
104
+ const len = c.u8();
105
+ if (!len)
106
+ return type === MysqlType.DATE ? '0000-00-00' : '0000-00-00 00:00:00';
107
+ const y = c.u16(), m = c.u8(), d = c.u8();
108
+ const [h, i, s] = len >= 7 ? [c.u8(), c.u8(), c.u8()] : [0, 0, 0];
109
+ const us = len >= 11 ? c.u32() : 0;
110
+ const day = `${pad(y, 4)}-${pad(m)}-${pad(d)}`;
111
+ return type === MysqlType.DATE ? day : `${day} ${pad(h)}:${pad(i)}:${pad(s)}${us ? `.${pad(us, 6)}` : ''}`;
112
+ }
113
+ case MysqlType.TIME: {
114
+ const len = c.u8();
115
+ if (!len)
116
+ return '00:00:00';
117
+ const neg = c.u8(), days = c.u32(), h = c.u8(), i = c.u8(), s = c.u8();
118
+ const us = len >= 12 ? c.u32() : 0;
119
+ return `${neg ? '-' : ''}${pad(days * 24 + h)}:${pad(i)}:${pad(s)}${us ? `.${pad(us, 6)}` : ''}`;
120
+ }
121
+ default: return c.lenencStr();
122
+ }
123
+ }
124
+ /** One command's answer: an OK, an ERR, or a result set (text or binary rows). */
125
+ function answer(payloads, binary) {
126
+ const first = payloads[0];
127
+ if (!first)
128
+ return { error: { errno: 2013, sqlstate: 'HY000', message: 'Lost connection to MySQL server during query' } };
129
+ if (first[0] === 0xff)
130
+ return { error: errorOf(first) };
131
+ // a result set begins with its column count (never 0): 0x00 is an OK
132
+ if (first[0] === 0x00) {
133
+ const c = new Cursor(first);
134
+ c.u8();
135
+ return { affectedRows: c.lenenc(), insertId: c.lenenc() };
136
+ }
137
+ const c = new Cursor(first);
138
+ const count = c.lenenc();
139
+ const columns = payloads.slice(1, 1 + count).map(column);
140
+ const rows = [];
141
+ for (const p of payloads.slice(2 + count)) {
142
+ if (p[0] === 0xfe && p.length < 9)
143
+ break; // the closing EOF
144
+ if (p[0] === 0xff)
145
+ return { error: errorOf(p) };
146
+ const r = new Cursor(p);
147
+ if (!binary) {
148
+ rows.push(columns.map(() => (r.b[r.at] === 0xfb ? (r.at++, null) : r.lenencStr())));
149
+ continue;
150
+ }
151
+ r.u8();
152
+ const bitmap = r.take(Math.floor((columns.length + 9) / 8));
153
+ rows.push(columns.map((col, i) => ((bitmap[Math.floor((i + 2) / 8)] >> ((i + 2) % 8)) & 1 ? null : binaryValue(r, col.type, col.flags))));
154
+ }
155
+ return { columns, rows };
156
+ }
157
+ /** Sign in: the server's greeting, then the handshake response (mysql_native_password), following an auth switch. */
158
+ export async function mysqlSignIn(exchange, greeting, user, password, database) {
159
+ const g = new Cursor(packets(greeting)[0]);
160
+ g.u8();
161
+ g.nul();
162
+ g.u32();
163
+ const part1 = g.take(8);
164
+ g.u8();
165
+ g.u16();
166
+ g.u8();
167
+ g.u16();
168
+ g.u16();
169
+ g.u8();
170
+ g.take(10);
171
+ const part2 = g.take(12);
172
+ const scramble = new Uint8Array([...part1, ...part2]);
173
+ const token = nativePasswordToken(password, scramble);
174
+ const body = [...new Uint8Array([CLIENT_CAPS & 0xff, (CLIENT_CAPS >> 8) & 0xff, (CLIENT_CAPS >> 16) & 0xff, (CLIENT_CAPS >> 24) & 0xff, 0, 0, 0, 1, 45]), ...new Array(23).fill(0),
175
+ ...enc.encode(user), 0, token.length, ...token, ...(database ? [...enc.encode(database), 0] : [0]), ...enc.encode('mysql_native_password'), 0];
176
+ let reply = packets(await exchange(packet(1, Uint8Array.from(body))))[0];
177
+ if (reply?.[0] === 0xfe) { // the server asks for another plugin's answer: mysql_native_password with its scramble
178
+ const c = new Cursor(reply);
179
+ c.u8();
180
+ c.nul();
181
+ reply = packets(await exchange(packet(3, nativePasswordToken(password, c.take(20)))))[0];
182
+ }
183
+ if (!reply || reply[0] === 0xff)
184
+ return { error: reply ? errorOf(reply) : { errno: 2013, sqlstate: 'HY000', message: 'Lost connection to MySQL server during handshake' } };
185
+ return { ok: true };
186
+ }
187
+ /** A text query (COM_QUERY): the first result of a script, as mysql_async reads it. */
188
+ export async function mysqlQuery(exchange, sql) {
189
+ return answer(packets(await exchange(packet(0, Uint8Array.from([0x03, ...enc.encode(sql)])))), false);
190
+ }
191
+ /** A prepared statement executed once with text-typed parameters (COM_STMT_PREPARE, EXECUTE, CLOSE), as Prisma sends a
192
+ * parameterised query: binary parameters and rows. */
193
+ export async function mysqlExecute(exchange, sql, params) {
194
+ const prepared = packets(await exchange(packet(0, Uint8Array.from([0x16, ...enc.encode(sql)]))));
195
+ if (prepared[0]?.[0] === 0xff)
196
+ return { error: errorOf(prepared[0]) };
197
+ const c = new Cursor(prepared[0]);
198
+ c.u8();
199
+ const id = c.u32();
200
+ c.u16();
201
+ const count = c.u16();
202
+ const head = [0x17, id & 0xff, (id >> 8) & 0xff, (id >> 16) & 0xff, (id >>> 24) & 0xff, 0, 1, 0, 0, 0];
203
+ const values = [];
204
+ if (count) {
205
+ const bitmap = new Array(Math.floor((count + 7) / 8)).fill(0);
206
+ params.forEach((v, i) => { if (v === null)
207
+ bitmap[Math.floor(i / 8)] |= 1 << (i % 8); });
208
+ head.push(...bitmap, 1);
209
+ for (let i = 0; i < count; i++)
210
+ head.push(params[i] === null ? MysqlType.NULL : MysqlType.VAR_STRING, 0);
211
+ for (const v of params)
212
+ if (v !== null) {
213
+ const b = enc.encode(v);
214
+ values.push(...(b.length < 251 ? [b.length] : [0xfc, b.length & 0xff, b.length >> 8]), ...b);
215
+ }
216
+ }
217
+ const result = answer(packets(await exchange(packet(0, Uint8Array.from([...head, ...values])))), true);
218
+ await exchange(packet(0, Uint8Array.from([0x19, id & 0xff, (id >> 8) & 0xff, (id >> 16) & 0xff, (id >>> 24) & 0xff])));
219
+ return result;
220
+ }
@@ -0,0 +1,2 @@
1
+ export * from './stream.js';
2
+ export * from './client.js';
@@ -0,0 +1,5 @@
1
+ // THE KERNEL'S MYSQL WIRE (`@volter/world-core/mysql`): MySQL's client/server protocol as a TwinStream. A pack that serves
2
+ // MySQL passes what a statement means (its engine) and whom it admits; the kernel names no vendor.
3
+ // docs/contributing/architecture.md, "Other wires", the MySQL wire.
4
+ export * from "./stream.js";
5
+ export * from "./client.js";
@@ -0,0 +1,113 @@
1
+ import type { TwinStream } from '../twin-fetch.js';
2
+ /** A column as the pack's engine describes it: MySQL's column type code (`MysqlType`) and flags. */
3
+ export type MysqlColumn = {
4
+ name: string;
5
+ type: number;
6
+ flags?: number;
7
+ charset?: number;
8
+ length?: number;
9
+ decimals?: number;
10
+ table?: string;
11
+ orgTable?: string;
12
+ orgName?: string;
13
+ schema?: string;
14
+ };
15
+ /** A statement's outcome: rows (each value as MySQL's text protocol carries it, or null), or a count, or MySQL's error. */
16
+ export type MysqlOutcome = {
17
+ columns: MysqlColumn[];
18
+ rows: (string | Uint8Array | null)[][];
19
+ warnings?: number;
20
+ } | {
21
+ affectedRows: number;
22
+ insertId?: number | bigint;
23
+ warnings?: number;
24
+ info?: string;
25
+ } | {
26
+ error: MysqlError;
27
+ };
28
+ export type MysqlError = {
29
+ errno: number;
30
+ sqlstate: string;
31
+ message: string;
32
+ };
33
+ export type MysqlStreamOptions<Session> = {
34
+ /** The handshake's server version: the one the pack's engine reports for `VERSION()`. */
35
+ version: string;
36
+ /** Admit `user` to `database` (null: none named) when `check` accepts what the pack stores for it: the password, or
37
+ * MySQL's own stored form of it (`nativePasswordHash`, SHA1(SHA1(password)), as a server keeps it). */
38
+ connect(user: string, database: string | null, check: MysqlPasswordCheck): Promise<{
39
+ session: Session;
40
+ } | {
41
+ error: MysqlError;
42
+ }>;
43
+ /** Execute ONE statement (the kernel splits a script and binds parameters into the text) on the session. */
44
+ execute(session: Session, sql: string): Promise<MysqlOutcome>;
45
+ /** Whether the session holds an open transaction (SERVER_STATUS_IN_TRANS). */
46
+ inTransaction(session: Session): boolean;
47
+ /** Use another database on the session (COM_INIT_DB). */
48
+ useDatabase?(session: Session, database: string): Promise<MysqlError | null>;
49
+ /** The connection went away: roll back what it left open. */
50
+ close?(session: Session): void;
51
+ };
52
+ /** MySQL's column type codes. */
53
+ export declare const MysqlType: {
54
+ readonly DECIMAL: 0;
55
+ readonly TINY: 1;
56
+ readonly SHORT: 2;
57
+ readonly LONG: 3;
58
+ readonly FLOAT: 4;
59
+ readonly DOUBLE: 5;
60
+ readonly NULL: 6;
61
+ readonly TIMESTAMP: 7;
62
+ readonly LONGLONG: 8;
63
+ readonly INT24: 9;
64
+ readonly DATE: 10;
65
+ readonly TIME: 11;
66
+ readonly DATETIME: 12;
67
+ readonly YEAR: 13;
68
+ readonly VARCHAR: 15;
69
+ readonly BIT: 16;
70
+ readonly JSON: 245;
71
+ readonly NEWDECIMAL: 246;
72
+ readonly ENUM: 247;
73
+ readonly SET: 248;
74
+ readonly TINY_BLOB: 249;
75
+ readonly MEDIUM_BLOB: 250;
76
+ readonly LONG_BLOB: 251;
77
+ readonly BLOB: 252;
78
+ readonly VAR_STRING: 253;
79
+ readonly STRING: 254;
80
+ readonly GEOMETRY: 255;
81
+ };
82
+ export declare const MysqlFlag: {
83
+ readonly NOT_NULL: 1;
84
+ readonly PRI_KEY: 2;
85
+ readonly UNIQUE_KEY: 4;
86
+ readonly MULTIPLE_KEY: 8;
87
+ readonly BLOB: 16;
88
+ readonly UNSIGNED: 32;
89
+ readonly BINARY: 128;
90
+ readonly AUTO_INCREMENT: 512;
91
+ };
92
+ /** Whether the client's mysql_native_password answer proves a password: from the password itself, or from MySQL's
93
+ * stored form of it (hex SHA1(SHA1(password))), as a MySQL server checks it without holding the password. */
94
+ export type MysqlPasswordCheck = {
95
+ password(password: string): boolean;
96
+ storedHash(hex: string): boolean;
97
+ };
98
+ /** MySQL's stored form of a password for mysql_native_password: SHA1(SHA1(password)), hex. A pack keeps it when the
99
+ * password is made, beside whatever else it keeps, so the wire can admit the password without the pack holding it. */
100
+ export declare function nativePasswordHash(password: string): string;
101
+ /** mysql_native_password's token for `password` against `scramble`: SHA1(pw) XOR SHA1(scramble + SHA1(SHA1(pw))). */
102
+ export declare function nativePasswordToken(password: string, scramble: Uint8Array): Uint8Array;
103
+ /** A multi-statement script's statements, empty ones dropped. */
104
+ export declare function splitStatements(sql: string): string[];
105
+ export declare function countPlaceholders(sql: string): number;
106
+ /** A bound parameter: an integer, a float, text, bytes (a BLOB parameter, or text that is not UTF-8), a date or time. */
107
+ export type MysqlParam = null | bigint | number | string | Uint8Array | {
108
+ date: string;
109
+ };
110
+ /** `sql` with each `?` placeholder replaced by its parameter's literal. */
111
+ export declare function bindParameters(sql: string, params: MysqlParam[]): string;
112
+ /** A MySQL server as a TwinStream: the protocol here, the meaning in `options`. */
113
+ export declare function createMysqlStream<Session>(options: MysqlStreamOptions<Session>): TwinStream;