node-firebird 2.8.0 → 2.9.0

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/README.md CHANGED
@@ -11,9 +11,9 @@
11
11
  - [Installation](#installation)
12
12
  - [Usage](#usage) — including [developing the driver](#developing-the-driver)
13
13
  - [Promises and async/await](#promises-and-asyncawait) — the `*Async` API plus `withConnection` / `withTransaction` helpers
14
- - [Connection types](#connection-types) — connection options, classic connections, pooling
14
+ - [Connection types](#connection-types) — connection options, `firebird://` URIs and traditional connection strings, classic connections, pooling
15
15
  - [Database object (db)](#database-object-db) — database, transaction and statement methods/options
16
- - [Examples](#examples) — parametrized queries, BLOBs, streaming big data, transactions, driver events, database events (POST_EVENT), service manager, charsets/encoding, Firebird 3.0–6.0 features
16
+ - [Examples](#examples) — parametrized queries, named placeholders, BLOBs, streaming big data, transactions, driver events, database events (POST_EVENT), service manager, charsets/encoding, Firebird 3.0–6.0 features
17
17
  - [Extensive Examples](#extensive-examples) — DECFLOAT/INT128, query cancellation (AbortSignal), batch execution (bulk inserts), statement timeouts, scrollable cursors, RETURNING multiple rows, SKIP LOCKED, advanced pooling
18
18
  - [Using node-firebird with Express.js](#using-node-firebird-with-expressjs)
19
19
  - [FAQ](#faq)
@@ -180,8 +180,69 @@ options.maxNegotiatedProtocols = 10; // optional; limit maximum protocol version
180
180
  options.defaultSchema = undefined; // optional; sets session CURRENT_SCHEMA at connect time (FB >= 6.0)
181
181
  options.searchPath = undefined; // optional; ordered list/array of schemas to resolve unqualified object references (FB >= 6.0)
182
182
  options.jsonAsObject = false; // optional; automatically stringify parameters and parse query results that contain JSON (FB >= 6.0)
183
+ options.namedPlaceholders = false; // set to true to allow :name placeholders in SQL with a { name: value } params object (see Named placeholders)
183
184
  ```
184
185
 
186
+ ### Connection URI strings
187
+
188
+ Everywhere an options object is accepted — `attach`, `create`,
189
+ `attachOrCreate`, `drop`, `Firebird.pool()` and their `*Async`
190
+ counterparts — a `firebird://` URI string works too, which is handy for
191
+ 12-factor apps and containers that configure the database via a single
192
+ environment variable:
193
+
194
+ ```js
195
+ const db = await Firebird.attachAsync(process.env.DATABASE_URL);
196
+ // e.g. DATABASE_URL=firebird://SYSDBA:masterkey@db.example.com:3050//var/fb/prod.fdb?encoding=UTF8
197
+
198
+ const pool = Firebird.pool(10,
199
+ 'firebird://app:secret@localhost/appdb?lowercase_keys=true&idleTimeoutMillis=30000');
200
+ ```
201
+
202
+ The database part after `host[:port]/` can be:
203
+
204
+ | URI | database |
205
+ | :--- | :--- |
206
+ | `firebird://host/employee` | the alias `employee` |
207
+ | `firebird://host//var/fb/prod.fdb` | `/var/fb/prod.fdb` (explicit absolute path) |
208
+ | `firebird://host/var/fb/prod.fdb` | `/var/fb/prod.fdb` (a database part with `/` is a path — aliases cannot contain slashes) |
209
+ | `firebird://host/C:/fbdata/prod.fdb` | the Windows path `C:/fbdata/prod.fdb` |
210
+
211
+ Query parameters map 1:1 onto the connection options above and are coerced
212
+ to the right type (`?pageSize=8192&lowercase_keys=true&wireCompression=1`).
213
+ Credentials and paths are URL-decoded, so reserved characters can be
214
+ percent-encoded (`p%40ss` for `p@ss`); `user`/`password` may alternatively
215
+ be passed as query parameters. IPv6 hosts use brackets:
216
+ `firebird://[::1]:3050/employee`. The parser is exported as
217
+ `Firebird.parseConnectionUri(uri)` if you need the resulting options object.
218
+
219
+ ### Traditional connection strings (old style)
220
+
221
+ The classic Firebird connection string format — the same
222
+ `[host[/port]:]{path | alias}` strings isql and the other Firebird tools
223
+ use — is accepted everywhere too:
224
+
225
+ ```js
226
+ const db = await Firebird.attachAsync('db.example.com/3051:/var/fb/prod.fdb');
227
+ ```
228
+
229
+ | Connection string | meaning |
230
+ | :--- | :--- |
231
+ | `employee` | the alias `employee` on `127.0.0.1:3050` |
232
+ | `/var/fb/prod.fdb` | a path on `127.0.0.1:3050` |
233
+ | `db.example.com:employee` | the alias `employee` on `db.example.com:3050` |
234
+ | `db.example.com/3051:/var/fb/prod.fdb` | host and explicit port |
235
+ | `myserver:C:\fbdata\prod.fdb` | a Windows path behind a host |
236
+ | `C:\fbdata\prod.fdb` | a single character before `:` is a drive letter, not a host (same rule as Firebird) |
237
+ | `[::1]/3050:employee` | IPv6 hosts use brackets |
238
+
239
+ Unlike `firebird://` URIs, traditional strings carry no credentials or
240
+ options — the driver defaults apply (`SYSDBA`/`masterkey`, port 3050), and
241
+ the port must be numeric (`/etc/services` names are not resolved). Use the
242
+ URI form or an options object when you need to set anything else.
243
+ `Firebird.parseConnectionString(str)` parses both forms and is what
244
+ `attach`/`create`/`pool` use internally for string arguments.
245
+
185
246
  ### Classic
186
247
 
187
248
  ```js
@@ -385,6 +446,46 @@ Firebird.attach(options, function (err, db) {
385
446
  });
386
447
  ```
387
448
 
449
+ ### Named placeholders
450
+
451
+ With the `namedPlaceholders: true` connection option, SQL may use `:name`
452
+ markers and parameters may be passed as a values-by-name object instead of
453
+ a positional array. The rewrite happens client-side before the statement is
454
+ prepared, so it works on every Firebird version; positional `?` arrays keep
455
+ working unchanged on the same connection.
456
+
457
+ ```js
458
+ const db = await Firebird.attachAsync({ ...options, namedPlaceholders: true });
459
+ // or: firebird://user:pass@host/db?namedPlaceholders=true
460
+
461
+ const rows = await db.queryAsync(
462
+ 'SELECT * FROM USERS WHERE ALIAS = :alias AND CREATED > :since',
463
+ { alias: 'Peter', since: new Date(2026, 0, 1) });
464
+
465
+ // A name may repeat — it binds once per occurrence:
466
+ await db.queryAsync(
467
+ 'SELECT * FROM T WHERE A = :v OR B = :v', { v: 42 });
468
+
469
+ // Batch rows can be objects too (Firebird 4.0+):
470
+ await db.executeBatchAsync(
471
+ 'INSERT INTO USERS (ID, ALIAS) VALUES (:id, :alias)',
472
+ [{ id: 1, alias: 'a' }, { id: 2, alias: 'b' }]);
473
+ ```
474
+
475
+ Placeholders inside string literals (`'...'`), quoted identifiers (`"..."`),
476
+ comments and `q'{...}'` alternative literals are left untouched. A key
477
+ present with value `null` binds SQL `NULL`; a *missing* key raises
478
+ `Missing value for named placeholder(s): ...`.
479
+
480
+ The scanner has no SQL grammar, so inside an `EXECUTE BLOCK` body every
481
+ PSQL `:variable` reference looks like a placeholder too. The option is
482
+ therefore off by default — and can be disabled for a single statement with
483
+ the per-query option:
484
+
485
+ ```js
486
+ await db.queryAsync(execBlockSql, [], { namedPlaceholders: false });
487
+ ```
488
+
388
489
  ### Tablespaces and Schema Partitioning (Firebird 6.0+)
389
490
 
390
491
  For Firebird 6.0+ (Protocol 20+), you can create and manage physical tablespace locations and logical schema namespaces, optionally partitioning schemas into specific physical tablespaces.
package/lib/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import Connection from './wire/connection';
2
2
  import { escape as escapeValue } from './utils';
3
+ import { parseConnectionUri, parseConnectionString } from './uri';
3
4
  import type { Options, SvcMgrOptions, DatabaseCallback, ServiceManagerCallback, SimpleCallback, ConnectionPool, Database, ServiceManager } from './types';
4
5
  export * from './types';
5
6
  export { GDSCode } from './gdscodes';
@@ -29,14 +30,16 @@ export declare const escape: typeof escapeValue;
29
30
  * the connection was stored on the module object itself.
30
31
  */
31
32
  export declare let connection: Connection | undefined;
32
- export declare function attach(options: Options, callback: DatabaseCallback): void;
33
+ export declare function attach(options: Options | string, callback: DatabaseCallback): void;
33
34
  export declare function attach(options: SvcMgrOptions, callback: ServiceManagerCallback): void;
34
- export declare function drop(options: Options, callback: SimpleCallback): void;
35
- export declare function create(options: Options, callback: DatabaseCallback): void;
36
- export declare function attachOrCreate(options: Options, callback: DatabaseCallback): void;
37
- export declare function pool(max: number, options: Options): ConnectionPool;
35
+ export declare function drop(options: Options | string, callback: SimpleCallback): void;
36
+ export declare function create(options: Options | string, callback: DatabaseCallback): void;
37
+ export declare function attachOrCreate(options: Options | string, callback: DatabaseCallback): void;
38
+ export declare function pool(max: number, options: Options | string): ConnectionPool;
39
+ export { parseConnectionUri, parseConnectionString };
40
+ export { parseNamedPlaceholders } from './named-params';
38
41
  export declare function attachAsync(options: SvcMgrOptions): Promise<ServiceManager>;
39
- export declare function attachAsync(options: Options): Promise<Database>;
40
- export declare function createAsync(options: Options): Promise<Database>;
41
- export declare function attachOrCreateAsync(options: Options): Promise<Database>;
42
- export declare function dropAsync(options: Options): Promise<void>;
42
+ export declare function attachAsync(options: Options | string): Promise<Database>;
43
+ export declare function createAsync(options: Options | string): Promise<Database>;
44
+ export declare function attachOrCreateAsync(options: Options | string): Promise<Database>;
45
+ export declare function dropAsync(options: Options | string): Promise<void>;
package/lib/index.js CHANGED
@@ -17,7 +17,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
17
17
  return (mod && mod.__esModule) ? mod : { "default": mod };
18
18
  };
19
19
  Object.defineProperty(exports, "__esModule", { value: true });
20
- exports.connection = exports.escape = exports.ISOLATION_READ_COMMITTED_READ_ONLY = exports.ISOLATION_SERIALIZABLE = exports.ISOLATION_REPEATABLE_READ = exports.ISOLATION_READ_COMMITTED = exports.ISOLATION_READ_UNCOMMITTED = exports.WIRE_CRYPT_ENABLE = exports.WIRE_CRYPT_DISABLE = exports.AUTH_PLUGIN_SRP512 = exports.AUTH_PLUGIN_SRP384 = exports.AUTH_PLUGIN_SRP256 = exports.AUTH_PLUGIN_SRP = exports.AUTH_PLUGIN_LEGACY = exports.GDSCode = void 0;
20
+ exports.parseNamedPlaceholders = exports.parseConnectionString = exports.parseConnectionUri = exports.connection = exports.escape = exports.ISOLATION_READ_COMMITTED_READ_ONLY = exports.ISOLATION_SERIALIZABLE = exports.ISOLATION_REPEATABLE_READ = exports.ISOLATION_READ_COMMITTED = exports.ISOLATION_READ_UNCOMMITTED = exports.WIRE_CRYPT_ENABLE = exports.WIRE_CRYPT_DISABLE = exports.AUTH_PLUGIN_SRP512 = exports.AUTH_PLUGIN_SRP384 = exports.AUTH_PLUGIN_SRP256 = exports.AUTH_PLUGIN_SRP = exports.AUTH_PLUGIN_LEGACY = exports.GDSCode = void 0;
21
21
  exports.attach = attach;
22
22
  exports.drop = drop;
23
23
  exports.create = create;
@@ -32,6 +32,9 @@ const callback_1 = require("./callback");
32
32
  const connection_1 = __importDefault(require("./wire/connection"));
33
33
  const pool_1 = __importDefault(require("./pool"));
34
34
  const utils_1 = require("./utils");
35
+ const uri_1 = require("./uri");
36
+ Object.defineProperty(exports, "parseConnectionUri", { enumerable: true, get: function () { return uri_1.parseConnectionUri; } });
37
+ Object.defineProperty(exports, "parseConnectionString", { enumerable: true, get: function () { return uri_1.parseConnectionString; } });
35
38
  __exportStar(require("./types"), exports);
36
39
  var gdscodes_1 = require("./gdscodes");
37
40
  Object.defineProperty(exports, "GDSCode", { enumerable: true, get: function () { return gdscodes_1.GDSCode; } });
@@ -61,6 +64,7 @@ exports.ISOLATION_SERIALIZABLE = const_1.default.ISOLATION_SERIALIZABLE;
61
64
  exports.ISOLATION_READ_COMMITTED_READ_ONLY = const_1.default.ISOLATION_READ_COMMITTED_READ_ONLY;
62
65
  exports.escape = utils_1.escape;
63
66
  function attach(options, callback) {
67
+ options = (0, uri_1.normalizeOptions)(options);
64
68
  var host = options.host || const_1.default.DEFAULT_HOST;
65
69
  var port = options.port || const_1.default.DEFAULT_PORT;
66
70
  var manager = options.manager || false;
@@ -83,7 +87,7 @@ function attach(options, callback) {
83
87
  }, options);
84
88
  }
85
89
  function drop(options, callback) {
86
- attach(options, function (err, db) {
90
+ attach((0, uri_1.normalizeOptions)(options), function (err, db) {
87
91
  if (err) {
88
92
  callback({ error: err, message: "Drop error" });
89
93
  return;
@@ -92,6 +96,7 @@ function drop(options, callback) {
92
96
  });
93
97
  }
94
98
  function create(options, callback) {
99
+ options = (0, uri_1.normalizeOptions)(options);
95
100
  var host = options.host || const_1.default.DEFAULT_HOST;
96
101
  var port = options.port || const_1.default.DEFAULT_PORT;
97
102
  var cnx = exports.connection = new connection_1.default(host, port, function (err) {
@@ -112,6 +117,7 @@ function create(options, callback) {
112
117
  }, options);
113
118
  }
114
119
  function attachOrCreate(options, callback) {
120
+ options = (0, uri_1.normalizeOptions)(options);
115
121
  var host = options.host || const_1.default.DEFAULT_HOST;
116
122
  var port = options.port || const_1.default.DEFAULT_PORT;
117
123
  var cnx = exports.connection = new connection_1.default(host, port, function (err) {
@@ -139,8 +145,10 @@ function attachOrCreate(options, callback) {
139
145
  }
140
146
  // Pooling
141
147
  function pool(max, options) {
142
- return new pool_1.default(attach, max, Object.assign({}, options, { isPool: true }));
148
+ return new pool_1.default(attach, max, Object.assign({}, (0, uri_1.normalizeOptions)(options), { isPool: true }));
143
149
  }
150
+ var named_params_1 = require("./named-params");
151
+ Object.defineProperty(exports, "parseNamedPlaceholders", { enumerable: true, get: function () { return named_params_1.parseNamedPlaceholders; } });
144
152
  function attachAsync(options) {
145
153
  return (0, callback_1.fromCallback)(function (cb) { attach(options, cb); });
146
154
  }
@@ -0,0 +1,42 @@
1
+ /***************************************
2
+ *
3
+ * Named placeholders (:name → ?)
4
+ *
5
+ ***************************************/
6
+ /**
7
+ * Result of scanning a SQL string for named placeholders.
8
+ */
9
+ export interface ParsedNamedPlaceholders {
10
+ /** SQL with every named placeholder replaced by a positional "?". */
11
+ sql: string;
12
+ /**
13
+ * Placeholder names in positional order (a repeated name appears once
14
+ * per occurrence), or null when the SQL contains none.
15
+ */
16
+ names: string[] | null;
17
+ }
18
+ /**
19
+ * Scan `sql` for named placeholders (`:name`) and rewrite them to positional
20
+ * `?` markers, returning the rewritten SQL and the names in positional
21
+ * order. Placeholders inside string literals ('...'), quoted identifiers
22
+ * ("..."), line comments (--), block comments and Firebird alternative
23
+ * string literals (q'{...}') are left untouched.
24
+ *
25
+ * Note: the scanner has no SQL grammar — inside an EXECUTE BLOCK body every
26
+ * `:variable` reference looks like a placeholder too. Use positional params
27
+ * (or per-call `namedPlaceholders: false`) for EXECUTE BLOCK.
28
+ */
29
+ export declare function parseNamedPlaceholders(sql: string): ParsedNamedPlaceholders;
30
+ /**
31
+ * True when `params` is a plain values-by-name object (and not one of the
32
+ * values the driver accepts as a single positional parameter, like Date or
33
+ * Buffer).
34
+ */
35
+ export declare function isNamedParamsObject(params: any): params is Record<string, any>;
36
+ /**
37
+ * Map a values-by-name object onto the positional order collected by
38
+ * parseNamedPlaceholders. A name may be bound multiple times; every name
39
+ * must be an own property of `params` (a present key holding null is a
40
+ * NULL parameter, a missing key is an error).
41
+ */
42
+ export declare function bindNamedParams(names: string[], params: Record<string, any>): any[];
@@ -0,0 +1,133 @@
1
+ "use strict";
2
+ /***************************************
3
+ *
4
+ * Named placeholders (:name → ?)
5
+ *
6
+ ***************************************/
7
+ Object.defineProperty(exports, "__esModule", { value: true });
8
+ exports.parseNamedPlaceholders = parseNamedPlaceholders;
9
+ exports.isNamedParamsObject = isNamedParamsObject;
10
+ exports.bindNamedParams = bindNamedParams;
11
+ const IDENT_START = /[A-Za-z_]/;
12
+ const IDENT_PART = /[A-Za-z0-9_$]/;
13
+ // Parsing is pure string work, so identical SQL (the common case with
14
+ // query builders and hot paths) is scanned only once.
15
+ const CACHE_MAX = 100;
16
+ const cache = new Map();
17
+ /**
18
+ * Scan `sql` for named placeholders (`:name`) and rewrite them to positional
19
+ * `?` markers, returning the rewritten SQL and the names in positional
20
+ * order. Placeholders inside string literals ('...'), quoted identifiers
21
+ * ("..."), line comments (--), block comments and Firebird alternative
22
+ * string literals (q'{...}') are left untouched.
23
+ *
24
+ * Note: the scanner has no SQL grammar — inside an EXECUTE BLOCK body every
25
+ * `:variable` reference looks like a placeholder too. Use positional params
26
+ * (or per-call `namedPlaceholders: false`) for EXECUTE BLOCK.
27
+ */
28
+ function parseNamedPlaceholders(sql) {
29
+ var cached = cache.get(sql);
30
+ if (cached)
31
+ return cached;
32
+ var out = '';
33
+ var names = [];
34
+ var i = 0;
35
+ var n = sql.length;
36
+ while (i < n) {
37
+ var c = sql[i];
38
+ if (c === "'" || c === '"') {
39
+ // string literal or quoted identifier; doubled quotes escape
40
+ var quote = c;
41
+ var end = i + 1;
42
+ while (end < n) {
43
+ if (sql[end] === quote) {
44
+ if (sql[end + 1] === quote) {
45
+ end += 2;
46
+ continue;
47
+ }
48
+ end++;
49
+ break;
50
+ }
51
+ end++;
52
+ }
53
+ out += sql.slice(i, end);
54
+ i = end;
55
+ }
56
+ else if (c === '-' && sql[i + 1] === '-') {
57
+ var eol = sql.indexOf('\n', i);
58
+ if (eol === -1)
59
+ eol = n;
60
+ out += sql.slice(i, eol);
61
+ i = eol;
62
+ }
63
+ else if (c === '/' && sql[i + 1] === '*') {
64
+ var close = sql.indexOf('*/', i + 2);
65
+ close = close === -1 ? n : close + 2;
66
+ out += sql.slice(i, close);
67
+ i = close;
68
+ }
69
+ else if ((c === 'q' || c === 'Q') && sql[i + 1] === "'" && i + 2 < n &&
70
+ (i === 0 || !IDENT_PART.test(sql[i - 1]))) {
71
+ // Firebird 3+ alternative string literal: q'{...}' / q'!...!'
72
+ var open = sql[i + 2];
73
+ var closer = open === '(' ? ')'
74
+ : open === '[' ? ']'
75
+ : open === '{' ? '}'
76
+ : open === '<' ? '>'
77
+ : open;
78
+ var stop = sql.indexOf(closer + "'", i + 3);
79
+ stop = stop === -1 ? n : stop + 2;
80
+ out += sql.slice(i, stop);
81
+ i = stop;
82
+ }
83
+ else if (c === ':' && i + 1 < n && IDENT_START.test(sql[i + 1])) {
84
+ var end2 = i + 2;
85
+ while (end2 < n && IDENT_PART.test(sql[end2]))
86
+ end2++;
87
+ names.push(sql.slice(i + 1, end2));
88
+ out += '?';
89
+ i = end2;
90
+ }
91
+ else {
92
+ out += c;
93
+ i++;
94
+ }
95
+ }
96
+ var result = names.length
97
+ ? { sql: out, names: names }
98
+ : { sql: sql, names: null };
99
+ if (cache.size >= CACHE_MAX) {
100
+ cache.delete(cache.keys().next().value);
101
+ }
102
+ cache.set(sql, result);
103
+ return result;
104
+ }
105
+ /**
106
+ * True when `params` is a plain values-by-name object (and not one of the
107
+ * values the driver accepts as a single positional parameter, like Date or
108
+ * Buffer).
109
+ */
110
+ function isNamedParamsObject(params) {
111
+ return params !== null &&
112
+ typeof params === 'object' &&
113
+ !Array.isArray(params) &&
114
+ !Buffer.isBuffer(params) &&
115
+ !(params instanceof Date);
116
+ }
117
+ /**
118
+ * Map a values-by-name object onto the positional order collected by
119
+ * parseNamedPlaceholders. A name may be bound multiple times; every name
120
+ * must be an own property of `params` (a present key holding null is a
121
+ * NULL parameter, a missing key is an error).
122
+ */
123
+ function bindNamedParams(names, params) {
124
+ var missing = [];
125
+ var values = names.map(function (name) {
126
+ if (!Object.prototype.hasOwnProperty.call(params, name))
127
+ missing.push(name);
128
+ return params[name];
129
+ });
130
+ if (missing.length)
131
+ throw new Error('Missing value for named placeholder(s): ' + missing.join(', '));
132
+ return values;
133
+ }
package/lib/srp.js CHANGED
@@ -117,12 +117,16 @@ function clientProof(user, password, salt, A, B, a, hashAlgo = 'sha1') {
117
117
  dump('n2', n2);
118
118
  n1 = modPow(n1, n2, PRIME.N);
119
119
  n2 = toBigInt(getHash('sha1', user));
120
- var M = toBigInt(getHash(hashAlgo, toBuffer(n1), toBuffer(n2), salt, toBuffer(A), toBuffer(B), toBuffer(K)));
120
+ // K is hashed as the raw fixed-length digest, exactly like the server's
121
+ // digest.process(sessionKey) over a 20-byte UCharBuffer. Converting it
122
+ // through bigint dropped a leading zero byte (~0.4% of connections) and
123
+ // broke the proof (issue #421).
124
+ var M = toBigInt(getHash(hashAlgo, toBuffer(n1), toBuffer(n2), salt, toBuffer(A), toBuffer(B), K));
121
125
  dump('n1-2', n1);
122
126
  dump('n2-2', n2);
123
127
  dump('proof:M', M);
124
128
  return {
125
- clientSessionKey: K,
129
+ clientSessionKey: toBigInt(K),
126
130
  authData: M,
127
131
  };
128
132
  }
@@ -162,12 +166,19 @@ function pad(n) {
162
166
  /**
163
167
  * Scramble keys.
164
168
  *
169
+ * The server hashes the minimal (stripped) magnitude bytes of A and B
170
+ * (RemotePassword::computeScramble → processStrippedInt in Firebird's
171
+ * srp.cpp, identical in 3.0 through master) — NOT the 128-byte padded
172
+ * form, which the engine only uses for k = H(N, pad(g)). Padding here
173
+ * made u diverge whenever A or B had a leading zero byte (~0.8% of
174
+ * connections), failing the proof (issue #421).
175
+ *
165
176
  * @param A BigInt Client public key.
166
177
  * @param B BigInt Server public key.
167
178
  * @returns {BigInt}
168
179
  */
169
180
  function getScramble(A, B, hashAlgo = 'sha1') {
170
- return BigInt('0x' + getHash(hashAlgo, pad(A), pad(B)));
181
+ return BigInt('0x' + getHash(hashAlgo, toBuffer(A), toBuffer(B)));
171
182
  }
172
183
  /**
173
184
  * Client session secret.
@@ -183,6 +194,8 @@ function getScramble(A, B, hashAlgo = 'sha1') {
183
194
  * @param A BigInt Client public key.
184
195
  * @param B BigInt Server public key.
185
196
  * @param a BigInt Client private key.
197
+ * @returns Buffer The raw session-key digest (fixed length, may start
198
+ * with a zero byte — significant for the proof).
186
199
  */
187
200
  function clientSession(user, password, salt, A, B, a, hashAlgo = 'sha1') {
188
201
  var u = getScramble(A, B, 'sha1');
@@ -200,7 +213,7 @@ function clientSession(user, password, salt, A, B, a, hashAlgo = 'sha1') {
200
213
  var ux = (u * x) % PRIME.N;
201
214
  var aux = (a + ux) % PRIME.N;
202
215
  var sessionSecret = modPow(diff, aux, PRIME.N);
203
- var K = toBigInt(getHash('sha1', toBuffer(sessionSecret)));
216
+ var K = Buffer.from(getHash('sha1', toBuffer(sessionSecret)), 'hex');
204
217
  dump('B', B);
205
218
  dump('u', u);
206
219
  dump('x', x);
package/lib/types.d.ts CHANGED
@@ -90,9 +90,21 @@ export type BatchOptions = {
90
90
  /** Rows per op_batch_msg packet (default 500). */
91
91
  chunkSize?: number;
92
92
  };
93
+ /**
94
+ * Positional query parameters (array), or — when named placeholders are
95
+ * enabled via the `namedPlaceholders` connection/query option — values by
96
+ * placeholder name.
97
+ */
98
+ export type QueryParams = any[] | Record<string, any>;
93
99
  export type QueryOptions = {
94
100
  timeout?: number;
95
101
  scrollable?: boolean;
102
+ /**
103
+ * Per-query override of the `namedPlaceholders` connection option
104
+ * (e.g. disable it for one EXECUTE BLOCK statement whose body uses
105
+ * `:variable` PSQL references).
106
+ */
107
+ namedPlaceholders?: boolean;
96
108
  /**
97
109
  * Abort the query when the signal fires (Firebird 2.5+ / protocol 12+).
98
110
  * If the signal is already aborted the query is not sent at all and the
@@ -107,11 +119,11 @@ export interface Database {
107
119
  detach(callback?: SimpleCallback): Database;
108
120
  transaction(options: TransactionOptions | Isolation | TransactionCallback, callback?: TransactionCallback): Database;
109
121
  newStatement(query: string, callback: (err: Error | null, statement: Statement) => void): Database;
110
- query(query: string, params: any[], callback: QueryCallback, options?: QueryOptions): Database;
111
- execute(query: string, params: any[], callback: QueryCallback, options?: QueryOptions): Database;
122
+ query(query: string, params: QueryParams, callback: QueryCallback, options?: QueryOptions): Database;
123
+ execute(query: string, params: QueryParams, callback: QueryCallback, options?: QueryOptions): Database;
112
124
  /** Bulk-execute in its own transaction, all-or-nothing (Firebird 4.0+). */
113
- executeBatch(query: string, rows: any[][], callback?: (err: any, result: BatchResult) => void, options?: BatchOptions): Database;
114
- sequentially(query: string, params: any[], rowCallback: SequentialCallback, callback: SimpleCallback, options?: QueryOptions | boolean): Database;
125
+ executeBatch(query: string, rows: QueryParams[], callback?: (err: any, result: BatchResult) => void, options?: BatchOptions): Database;
126
+ sequentially(query: string, params: QueryParams, rowCallback: SequentialCallback, callback: SimpleCallback, options?: QueryOptions | boolean): Database;
115
127
  drop(callback: SimpleCallback): void;
116
128
  escape(value: any): string;
117
129
  attachEvent(callback: any): this;
@@ -119,10 +131,10 @@ export interface Database {
119
131
  alterTablespace(name: string, filePath: string, callback?: QueryCallback): Database;
120
132
  dropTablespace(name: string, callback?: QueryCallback): Database;
121
133
  createSchema(schemaName: string, tablespaceName?: string | QueryCallback, callback?: QueryCallback): Database;
122
- queryAsync<T = any>(query: string, params?: any[], options?: QueryOptions): Promise<T[]>;
123
- executeAsync<T = any>(query: string, params?: any[], options?: QueryOptions): Promise<T[]>;
124
- executeBatchAsync(query: string, rows: any[][], options?: BatchOptions): Promise<BatchResult>;
125
- sequentiallyAsync(query: string, params: any[] | undefined, rowCallback: SequentialCallback, options?: QueryOptions | boolean): Promise<void>;
134
+ queryAsync<T = any>(query: string, params?: QueryParams, options?: QueryOptions): Promise<T[]>;
135
+ executeAsync<T = any>(query: string, params?: QueryParams, options?: QueryOptions): Promise<T[]>;
136
+ executeBatchAsync(query: string, rows: QueryParams[], options?: BatchOptions): Promise<BatchResult>;
137
+ sequentiallyAsync(query: string, params: QueryParams | undefined, rowCallback: SequentialCallback, options?: QueryOptions | boolean): Promise<void>;
126
138
  sequentiallyAsync(query: string, rowCallback: SequentialCallback, options?: QueryOptions | boolean): Promise<void>;
127
139
  transactionAsync(options?: TransactionOptions | Isolation): Promise<Transaction>;
128
140
  startTransactionAsync(options?: TransactionOptions | Isolation): Promise<Transaction>;
@@ -143,19 +155,19 @@ export interface Database {
143
155
  }
144
156
  export interface Transaction {
145
157
  newStatement(query: string, callback: (err: Error | null, statement: Statement) => void): void;
146
- query(query: string, params: any[], callback: QueryCallback, options?: QueryOptions): void;
147
- execute(query: string, params: any[], callback: QueryCallback, options?: QueryOptions): void;
158
+ query(query: string, params: QueryParams, callback: QueryCallback, options?: QueryOptions): void;
159
+ execute(query: string, params: QueryParams, callback: QueryCallback, options?: QueryOptions): void;
148
160
  /** Bulk-execute within this transaction; per-record failures do not roll back (Firebird 4.0+). */
149
- executeBatch(query: string, rows: any[][], callback?: (err: any, result: BatchResult) => void, options?: BatchOptions): void;
150
- sequentially(query: string, params: any[], rowCallback: SequentialCallback, callback: SimpleCallback, options?: QueryOptions | boolean): Database;
161
+ executeBatch(query: string, rows: QueryParams[], callback?: (err: any, result: BatchResult) => void, options?: BatchOptions): void;
162
+ sequentially(query: string, params: QueryParams, rowCallback: SequentialCallback, callback: SimpleCallback, options?: QueryOptions | boolean): Database;
151
163
  commit(callback?: SimpleCallback): void;
152
164
  commitRetaining(callback?: SimpleCallback): void;
153
165
  rollback(callback?: SimpleCallback): void;
154
166
  rollbackRetaining(callback?: SimpleCallback): void;
155
- queryAsync<T = any>(query: string, params?: any[], options?: QueryOptions): Promise<T[]>;
156
- executeAsync<T = any>(query: string, params?: any[], options?: QueryOptions): Promise<T[]>;
157
- executeBatchAsync(query: string, rows: any[][], options?: BatchOptions): Promise<BatchResult>;
158
- sequentiallyAsync(query: string, params: any[] | undefined, rowCallback: SequentialCallback, options?: QueryOptions | boolean): Promise<void>;
167
+ queryAsync<T = any>(query: string, params?: QueryParams, options?: QueryOptions): Promise<T[]>;
168
+ executeAsync<T = any>(query: string, params?: QueryParams, options?: QueryOptions): Promise<T[]>;
169
+ executeBatchAsync(query: string, rows: QueryParams[], options?: BatchOptions): Promise<BatchResult>;
170
+ sequentiallyAsync(query: string, params: QueryParams | undefined, rowCallback: SequentialCallback, options?: QueryOptions | boolean): Promise<void>;
159
171
  sequentiallyAsync(query: string, rowCallback: SequentialCallback, options?: QueryOptions | boolean): Promise<void>;
160
172
  newStatementAsync(query: string): Promise<Statement>;
161
173
  commitAsync(): Promise<void>;
@@ -167,14 +179,14 @@ export interface Statement {
167
179
  close(callback?: SimpleCallback): void;
168
180
  drop(callback?: SimpleCallback): void;
169
181
  release(callback?: SimpleCallback): void;
170
- execute(transaction: Transaction, params: any[], callback: QueryCallback, options?: QueryOptions): void;
182
+ execute(transaction: Transaction, params: QueryParams, callback: QueryCallback, options?: QueryOptions): void;
171
183
  fetch(transaction: Transaction, count: number, callback: QueryCallback): void;
172
184
  fetchScroll(transaction: Transaction, direction: 'NEXT' | 'PRIOR' | 'FIRST' | 'LAST' | 'ABSOLUTE' | 'RELATIVE' | number, offset: number, count: number, callback: QueryCallback): void;
173
185
  fetchAll(transaction: Transaction, callback: QueryCallback): void;
174
186
  /** Execute this prepared statement once per row (Firebird 4.0+ batch API). */
175
- executeBatch(transaction: Transaction, rows: any[][], callback?: (err: any, result: BatchResult) => void, options?: BatchOptions): void;
176
- executeAsync(transaction: Transaction, params?: any[], options?: QueryOptions): Promise<any>;
177
- executeBatchAsync(transaction: Transaction, rows: any[][], options?: BatchOptions): Promise<BatchResult>;
187
+ executeBatch(transaction: Transaction, rows: QueryParams[], callback?: (err: any, result: BatchResult) => void, options?: BatchOptions): void;
188
+ executeAsync(transaction: Transaction, params?: QueryParams, options?: QueryOptions): Promise<any>;
189
+ executeBatchAsync(transaction: Transaction, rows: QueryParams[], options?: BatchOptions): Promise<BatchResult>;
178
190
  fetchAsync(transaction: Transaction, count: number | 'all'): Promise<any>;
179
191
  fetchScrollAsync(transaction: Transaction, direction: 'NEXT' | 'PRIOR' | 'FIRST' | 'LAST' | 'ABSOLUTE' | 'RELATIVE' | number, offset?: number, count?: number): Promise<any>;
180
192
  fetchAllAsync(transaction: Transaction): Promise<any>;
@@ -209,6 +221,16 @@ export interface Options {
209
221
  blobReadChunkSize?: number;
210
222
  wireCrypt?: number;
211
223
  wireCompression?: boolean;
224
+ /**
225
+ * Enable named placeholders: SQL may use `:name` markers and params may
226
+ * be a values-by-name object (`db.query('... WHERE id = :id', { id: 1 })`).
227
+ * Placeholders are rewritten client-side to positional `?` before
228
+ * preparing; positional arrays keep working unchanged. Off by default
229
+ * because `EXECUTE BLOCK` bodies use `:variable` for PSQL references —
230
+ * with this option on, run such statements with positional params or a
231
+ * per-query `namedPlaceholders: false` override.
232
+ */
233
+ namedPlaceholders?: boolean;
212
234
  pluginName?: string;
213
235
  parallelWorkers?: number;
214
236
  maxInlineBlobSize?: number;
package/lib/uri.d.ts ADDED
@@ -0,0 +1,57 @@
1
+ /***************************************
2
+ *
3
+ * Connection URI strings
4
+ *
5
+ ***************************************/
6
+ import type { Options } from './types';
7
+ /**
8
+ * Parse a firebird:// connection URI into an options object.
9
+ *
10
+ * firebird://user:password@host:port/database?option=value&...
11
+ *
12
+ * The database part:
13
+ * firebird://host/employee → alias "employee"
14
+ * firebird://host//var/db/prod.fdb → absolute path "/var/db/prod.fdb"
15
+ * firebird://host/var/db/prod.fdb → "/var/db/prod.fdb" (a database
16
+ * part with slashes is a path —
17
+ * aliases cannot contain "/")
18
+ * firebird://host/C:/db/prod.fdb → Windows path "C:/db/prod.fdb"
19
+ *
20
+ * Credentials and the database path are URL-decoded, so reserved characters
21
+ * can be percent-encoded (e.g. p%40ss for "p@ss"). Query parameters map
22
+ * 1:1 to option keys and are coerced to the option's type (booleans accept
23
+ * 1/true/yes/on). `user` and `password` may be given as query parameters
24
+ * instead of in the authority.
25
+ */
26
+ export declare function parseConnectionUri(uri: string): Options;
27
+ /**
28
+ * Parse a traditional ("old style") Firebird connection string:
29
+ *
30
+ * [host[/port]:]{path | alias}
31
+ *
32
+ * employee → alias "employee" (default host)
33
+ * /var/fb/prod.fdb → local path (default host)
34
+ * C:\fbdata\prod.fdb → Windows path — a single character
35
+ * before ":" is a drive letter, not
36
+ * a host (same rule as Firebird)
37
+ * db.example.com:employee → host + alias
38
+ * db.example.com/3051:/var/fb/prod.fdb → host + port + path
39
+ * myserver:C:\fbdata\prod.fdb → host + Windows path
40
+ * [::1]/3050:employee → IPv6 host + port + alias
41
+ *
42
+ * Unlike firebird:// URIs, traditional strings carry no credentials or
43
+ * options — the driver defaults apply (SYSDBA/masterkey, port 3050).
44
+ * The port must be numeric; /etc/services names are not resolved.
45
+ */
46
+ export declare function parseOldStyleConnectionString(str: string): Options;
47
+ /**
48
+ * Parse any connection string the driver accepts: a firebird:// URI, or a
49
+ * traditional [host[/port]:]database string when there is no scheme.
50
+ */
51
+ export declare function parseConnectionString(str: string): Options;
52
+ /**
53
+ * Accept either an options object or a connection string (firebird:// URI
54
+ * or traditional host[/port]:database) everywhere options are taken.
55
+ * Strings are parsed; objects pass through unchanged.
56
+ */
57
+ export declare function normalizeOptions<T>(options: T | string): T;