@bzync/nextsql 0.1.0 → 0.1.1

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.
@@ -1,18 +1,20 @@
1
1
  'use strict';
2
2
 
3
- // Node-specific NSCE1 implementation. The dependency injection keeps this
3
+ // Node-specific NSCE1/NSCE2 implementation. The dependency injection keeps this
4
4
  // module on the driver's single public NextSQLError/Kind surface.
5
- const { createCipheriv, createDecipheriv, randomBytes } = require('node:crypto');
5
+ const { createCipheriv, createDecipheriv, hkdfSync, randomBytes, timingSafeEqual } = require('node:crypto');
6
6
  const fs = require('node:fs/promises');
7
7
 
8
8
  module.exports = function createClientEncryption(deps) {
9
9
  const { NextSQLError, Kind, encodeDecimalString, decodeDecimal, decodeNSJB, formatUUID, parseUUID } = deps;
10
10
  const PREFIX = 'NSCE1.';
11
+ const DETERMINISTIC_PREFIX = 'NSCE2.';
11
12
  const MAX_PLAINTEXT = 1 << 20;
12
13
  const MAX_KEY_ID = 64;
13
14
  const MAX_KEYS = 64;
14
15
  const NONCE_SIZE = 12;
15
16
  const TAG_SIZE = 16;
17
+ const DETERMINISTIC_KEY_INFO = Buffer.from('NextSQL NSCE2 AES-SIV v2', 'utf8');
16
18
 
17
19
  const FieldType = Object.freeze({
18
20
  UUID: Object.freeze({ kind: Kind.UUID, precision: 0, scale: 0, vecElem: 0 }),
@@ -334,18 +336,18 @@ module.exports = function createClientEncryption(deps) {
334
336
  return Buffer.concat([Buffer.from([tag]), u32(raw.length), raw]);
335
337
  }
336
338
 
337
- function makeHeader(keyID, type, nonce) {
339
+ function makeHeader(keyID, type, nonce = null) {
338
340
  const id = Buffer.from(keyID, 'ascii');
339
- const out = Buffer.alloc(3 + id.length + 6 + NONCE_SIZE);
340
- out[0] = 1; out[1] = 1; out[2] = id.length; id.copy(out, 3);
341
+ const out = Buffer.alloc(3 + id.length + 6 + (nonce ? NONCE_SIZE : 0));
342
+ out[0] = nonce ? 1 : 2; out[1] = nonce ? 1 : 2; out[2] = id.length; id.copy(out, 3);
341
343
  const off = 3 + id.length;
342
344
  out[off] = type.kind; out.writeUInt16LE(type.precision, off + 1); out.writeUInt16LE(type.scale, off + 3); out[off + 5] = type.vecElem;
343
- nonce.copy(out, off + 6);
345
+ if (nonce) nonce.copy(out, off + 6);
344
346
  return out;
345
347
  }
346
348
 
347
- function aad(database, table, column, publicHeader) {
348
- const parts = [Buffer.from(PREFIX, 'ascii')];
349
+ function aad(database, table, column, publicHeader, prefix = PREFIX) {
350
+ const parts = [Buffer.from(prefix, 'ascii')];
349
351
  for (const name of [database, table, column]) {
350
352
  const raw = validateName(name);
351
353
  parts.push(u16(raw.length), raw);
@@ -355,22 +357,65 @@ module.exports = function createClientEncryption(deps) {
355
357
  }
356
358
 
357
359
  function inspectField(ciphertext) {
358
- if (typeof ciphertext !== 'string' || !ciphertext.startsWith(PREFIX)) fail('invalid_format', 'invalid client ciphertext prefix');
359
- const enc = ciphertext.slice(PREFIX.length);
360
+ let prefix, version, suite, nonceSize, mode;
361
+ if (typeof ciphertext === 'string' && ciphertext.startsWith(PREFIX)) {
362
+ prefix = PREFIX; version = 1; suite = 1; nonceSize = NONCE_SIZE; mode = 'randomized';
363
+ } else if (typeof ciphertext === 'string' && ciphertext.startsWith(DETERMINISTIC_PREFIX)) {
364
+ prefix = DETERMINISTIC_PREFIX; version = 2; suite = 2; nonceSize = 0; mode = 'deterministic';
365
+ } else fail('invalid_format', 'invalid client ciphertext prefix');
366
+ const enc = ciphertext.slice(prefix.length);
360
367
  if (!enc || enc.length % 4 === 1 || !/^[A-Za-z0-9_-]+$/.test(enc) || enc.length > Math.ceil((MAX_PLAINTEXT + 101) * 4 / 3)) {
361
368
  fail('invalid_format', 'client ciphertext length out of range');
362
369
  }
363
370
  const body = Buffer.from(enc, 'base64url');
364
- if (body.toString('base64url') !== enc || body.length < 38 || body[0] !== 1 || body[1] !== 1) fail('invalid_format', 'unsupported or truncated client ciphertext');
371
+ if (body.toString('base64url') !== enc || body.length < 3 + 1 + 6 + nonceSize + TAG_SIZE || body[0] !== version || body[1] !== suite) fail('invalid_format', 'unsupported or truncated client ciphertext');
365
372
  const n = body[2];
366
- if (n < 1 || n > MAX_KEY_ID || body.length < 3 + n + 6 + NONCE_SIZE + TAG_SIZE) fail('invalid_format', 'invalid field key id length');
373
+ if (n < 1 || n > MAX_KEY_ID || body.length < 3 + n + 6 + nonceSize + TAG_SIZE) fail('invalid_format', 'invalid field key id length');
367
374
  const keyID = body.subarray(3, 3 + n).toString('ascii');
368
375
  if (!validKeyID(keyID)) fail('invalid_format', 'invalid field key id');
369
376
  const off = 3 + n;
370
377
  const type = normalizeType({ kind: body[off], precision: body.readUInt16LE(off + 1), scale: body.readUInt16LE(off + 3), vecElem: body[off + 5] }, 'invalid_format');
371
- return { keyID, type, body, headerLength: off + 6 + NONCE_SIZE };
378
+ return { keyID, type, body, headerLength: off + 6 + nonceSize, mode, prefix };
372
379
  }
373
380
 
381
+ function aesBlock(key, block) {
382
+ const cipher = createCipheriv('aes-128-ecb', key, null);
383
+ cipher.setAutoPadding(false);
384
+ return Buffer.concat([cipher.update(block), cipher.final()]);
385
+ }
386
+ function dbl(block) {
387
+ const out = Buffer.alloc(16); let carry = 0;
388
+ for (let i = 15; i >= 0; i--) { const b = block[i]; out[i] = ((b << 1) & 0xff) | carry; carry = b >>> 7; }
389
+ if (carry) out[15] ^= 0x87;
390
+ return out;
391
+ }
392
+ function xor16(a, b) { const out = Buffer.alloc(16); for (let i = 0; i < 16; i++) out[i] = a[i] ^ b[i]; return out; }
393
+ function cmac(key, msg) {
394
+ const l = aesBlock(key, Buffer.alloc(16)); const k1 = dbl(l); const k2 = dbl(k1);
395
+ const blocks = Math.max(1, Math.ceil(msg.length / 16)); let last = Buffer.alloc(16);
396
+ if (msg.length && msg.length % 16 === 0) last = xor16(msg.subarray(msg.length - 16), k1);
397
+ else { const rem = msg.length % 16; msg.copy(last, 0, msg.length - rem); last[rem] = 0x80; last = xor16(last, k2); }
398
+ let y = Buffer.alloc(16);
399
+ for (let i = 0; i < blocks - 1; i++) y = aesBlock(key, xor16(y, msg.subarray(i * 16, i * 16 + 16)));
400
+ return aesBlock(key, xor16(y, last));
401
+ }
402
+ function s2v(key, vectors) {
403
+ let d = cmac(key, Buffer.alloc(16));
404
+ for (let i = 0; i < vectors.length - 1; i++) d = xor16(dbl(d), cmac(key, vectors[i]));
405
+ const last = vectors[vectors.length - 1]; let t;
406
+ if (last.length >= 16) { t = Buffer.from(last); const off = t.length - 16; for (let i = 0; i < 16; i++) t[off + i] ^= d[i]; }
407
+ else { const padded = Buffer.alloc(16); last.copy(padded); padded[last.length] = 0x80; t = xor16(dbl(d), padded); }
408
+ return cmac(key, t);
409
+ }
410
+ function sivCrypt(material, input, tag, decrypt) {
411
+ const q = Buffer.from(tag); q[8] &= 0x7f; q[12] &= 0x7f;
412
+ const cipher = decrypt ? createDecipheriv('aes-128-ctr', material.subarray(16), q) : createCipheriv('aes-128-ctr', material.subarray(16), q);
413
+ return Buffer.concat([cipher.update(input), cipher.final()]);
414
+ }
415
+ function deterministicKey(material) {
416
+ return Buffer.from(hkdfSync('sha256', material, Buffer.alloc(0), DETERMINISTIC_KEY_INFO, 32));
417
+ }
418
+
374
419
  async function encryptField(provider, database, table, column, type, value) {
375
420
  if (value === null || value === undefined) return null;
376
421
  if (!provider || typeof provider.currentFieldKey !== 'function') fail('invalid_argument', 'field key provider is required');
@@ -395,7 +440,8 @@ module.exports = function createClientEncryption(deps) {
395
440
  const expected = normalizeType(expectedType);
396
441
  if (ciphertext === null || ciphertext === undefined) return null;
397
442
  if (!provider || typeof provider.fieldKey !== 'function') fail('invalid_argument', 'field key provider is required');
398
- const parsed = inspectField(ciphertext);
443
+ const parsed = inspectField(ciphertext);
444
+ if (parsed.mode !== 'randomized') fail('invalid_format', 'deterministic ciphertext requires decryptFieldDeterministic');
399
445
  if (!sameType(expected, parsed.type)) fail('invalid_format', 'encrypted logical type mismatch');
400
446
  let key;
401
447
  try { key = await provider.fieldKey(database, table, column, parsed.keyID); } catch { fail('crypto', 'field key unavailable or revoked'); }
@@ -415,6 +461,31 @@ module.exports = function createClientEncryption(deps) {
415
461
  }
416
462
  }
417
463
 
464
+ async function encryptFieldDeterministic(provider, database, table, column, type, value) {
465
+ if (value === null || value === undefined) return null;
466
+ if (!provider || typeof provider.currentFieldKey !== 'function') fail('invalid_argument', 'field key provider is required');
467
+ const t = normalizeType(type); const plain = encodeScalar(t, value);
468
+ if (plain.length > MAX_PLAINTEXT) fail('exhausted', 'plaintext exceeds field limit');
469
+ let key; try { key = await provider.currentFieldKey(database, table, column); } catch { fail('crypto', 'field key unavailable'); }
470
+ const material = deterministicKey(keyMaterial(key)); const hdr = makeHeader(key.id, t);
471
+ const associated = aad(database, table, column, hdr, DETERMINISTIC_PREFIX);
472
+ const tag = s2v(material.subarray(0, 16), [associated, plain]);
473
+ return DETERMINISTIC_PREFIX + Buffer.concat([hdr, tag, sivCrypt(material, plain, tag, false)]).toString('base64url');
474
+ }
475
+
476
+ async function decryptFieldDeterministic(provider, database, table, column, expectedType, ciphertext) {
477
+ const expected = normalizeType(expectedType); if (ciphertext == null) return null;
478
+ if (!provider || typeof provider.fieldKey !== 'function') fail('invalid_argument', 'field key provider is required');
479
+ const parsed = inspectField(ciphertext);
480
+ if (parsed.mode !== 'deterministic' || !sameType(expected, parsed.type)) fail('invalid_format', 'encrypted mode or logical type mismatch');
481
+ let key; try { key = await provider.fieldKey(database, table, column, parsed.keyID); } catch { fail('crypto', 'field key unavailable or revoked'); }
482
+ const material = deterministicKey(keyMaterial(key, parsed.keyID)); const tag = parsed.body.subarray(parsed.headerLength, parsed.headerLength + 16);
483
+ const plain = sivCrypt(material, parsed.body.subarray(parsed.headerLength + 16), tag, true);
484
+ const expectedTag = s2v(material.subarray(0, 16), [aad(database, table, column, parsed.body.subarray(0, parsed.headerLength), DETERMINISTIC_PREFIX), plain]);
485
+ if (!timingSafeEqual(tag, expectedTag)) fail('crypto', 'ciphertext authentication failed');
486
+ return decodeScalar(expected, plain);
487
+ }
488
+
418
489
  function generateFieldKey(id) {
419
490
  if (!validKeyID(id)) fail('invalid_argument', 'invalid field key id');
420
491
  return { id, material: randomBytes(32) };
@@ -630,5 +701,5 @@ module.exports = function createClientEncryption(deps) {
630
701
  }
631
702
  }
632
703
 
633
- return { FieldType, MemoryFieldKeyring, FileFieldKeyring, decryptField, encryptField, generateFieldKey, inspectField };
704
+ return { FieldType, MemoryFieldKeyring, FileFieldKeyring, decryptField, decryptFieldDeterministic, encryptField, encryptFieldDeterministic, generateFieldKey, inspectField };
634
705
  };
package/nextsql.d.ts CHANGED
@@ -31,9 +31,10 @@ export interface Config {
31
31
  /** Single-node entry point. Required for connect(); optional when nodes is set. */
32
32
  address?: string;
33
33
  database?: string;
34
- /** Selects which hosted realm this connection targets (M2-2). Optional:
35
- * an unset realm sends the exact pre-realm Hello and connects to the
36
- * server's configured default. */
34
+ /** Reserved; must stay empty. Multi-realm hosting was removed — a
35
+ * deployment serves exactly one database, and a server rejects a Hello
36
+ * naming any other realm. The field remains only because the wire
37
+ * frame's trailing realm slot does. */
37
38
  realm?: string;
38
39
  user: string;
39
40
  password?: string;
@@ -212,6 +213,8 @@ export interface Conn {
212
213
  encryptField(table: string, column: string, type: FieldTypeDescriptor, value: unknown): Promise<string | null>;
213
214
  /** Authenticate and decode an opaque NSCE1 STRING; null passes through. */
214
215
  decryptField(table: string, column: string, type: FieldTypeDescriptor, ciphertext: string | null): Promise<unknown>;
216
+ encryptFieldDeterministic(table: string, column: string, type: FieldTypeDescriptor, value: unknown): Promise<string | null>;
217
+ decryptFieldDeterministic(table: string, column: string, type: FieldTypeDescriptor, ciphertext: string | null): Promise<unknown>;
215
218
  }
216
219
 
217
220
  /** Routing client over every node of a NextSQL HA cluster. */
@@ -236,6 +239,8 @@ export interface HelloOK {
236
239
  version: number;
237
240
  authMethod: number;
238
241
  secret: bigint;
242
+ /** Capability bits the server accepted from Hello.flags; 0 when none. */
243
+ flags: number;
239
244
  }
240
245
 
241
246
  export interface DecodeValue {
@@ -246,7 +251,14 @@ export interface DecodeValue {
246
251
 
247
252
  export declare class NextSQLError extends Error {
248
253
  readonly code: string;
249
- constructor(code: string, message?: string);
254
+ /**
255
+ * Stable ERR_* name from docs/error-codes.md, present only on an error
256
+ * received over a connection that negotiated the public taxonomy and empty
257
+ * otherwise. `code` always carries the legacy class, so retry logic should
258
+ * keep reading that.
259
+ */
260
+ readonly publicCode: string;
261
+ constructor(code: string, message?: string, publicCode?: string);
250
262
  }
251
263
 
252
264
  export declare const Kind: {
@@ -322,11 +334,17 @@ export declare function generateFieldKey(id: string): FieldKey;
322
334
  export declare function inspectField(ciphertext: string): { keyID: string; type: FieldTypeDescriptor };
323
335
  export declare function encryptField(provider: FieldKeyProvider, database: string, table: string, column: string, type: FieldTypeDescriptor, value: unknown): Promise<string | null>;
324
336
  export declare function decryptField(provider: FieldKeyProvider, database: string, table: string, column: string, type: FieldTypeDescriptor, ciphertext: string | null): Promise<unknown>;
337
+ export declare function encryptFieldDeterministic(provider: FieldKeyProvider, database: string, table: string, column: string, type: FieldTypeDescriptor, value: unknown): Promise<string | null>;
338
+ export declare function decryptFieldDeterministic(provider: FieldKeyProvider, database: string, table: string, column: string, type: FieldTypeDescriptor, ciphertext: string | null): Promise<unknown>;
325
339
  export declare function validateConfig(cfg: Config): void;
326
340
  export declare function isLoopback(addr: string): boolean;
327
341
  export declare function encodeParam(v: Param): Uint8Array;
328
342
  export declare function struct(fields: Array<[string, unknown]>): { __struct: Array<[string, unknown]> };
329
343
  export declare function decodeValue(buf: Uint8Array, off: number): DecodeValue;
344
+ /** Hello.flags bit requesting cancellation instead of a session. */
345
+ export declare const FlagCancel: number;
346
+ /** Hello.flags bit requesting the stable ERR_* error taxonomy. */
347
+ export declare const FlagPublicErrorCodes: number;
330
348
  export declare function encodeHello(h: Hello): Uint8Array;
331
349
  export declare function decodeHelloOK(b: Uint8Array): HelloOK;
332
350
  export declare function encodeDecimalString(s: string): Uint8Array;
package/nextsql.js CHANGED
@@ -57,6 +57,12 @@ const STATUS_TTL_MS = 500;
57
57
  const AuthPassword = 1;
58
58
  const AuthPasswordKey = 2;
59
59
  const FlagCancel = 1;
60
+ // FlagPublicErrorCodes asks the server for the stable ERR_* error taxonomy
61
+ // (docs/error-codes.md). A server that does not implement it ignores the bit
62
+ // and keeps the NSQL v1 error shape, which decodeError still reads, so setting
63
+ // it is safe against any server version. The server echoes it in HelloOK.flags
64
+ // when it was accepted.
65
+ const FlagPublicErrorCodes = 2;
60
66
  const FlagNull = 0x01;
61
67
 
62
68
  const Kind = {
@@ -104,10 +110,16 @@ const MAX_STRUCT_FIELDS = 128;
104
110
  const MAX_COLLECTION_LEN = 1 << 20;
105
111
 
106
112
  class NextSQLError extends Error {
107
- constructor(code, message) {
113
+ constructor(code, message, publicCode) {
108
114
  super(message || code);
109
115
  this.name = 'NextSQLError';
110
116
  this.code = code;
117
+ // publicCode is the stable ERR_* name the server sent, present only on
118
+ // an error received over a connection that negotiated the taxonomy and
119
+ // empty otherwise. code always carries the legacy class, so existing
120
+ // retry checks keep working and nothing should branch on publicCode
121
+ // being present.
122
+ this.publicCode = publicCode || '';
111
123
  }
112
124
  }
113
125
 
@@ -116,7 +128,9 @@ const {
116
128
  MemoryFieldKeyring,
117
129
  FileFieldKeyring,
118
130
  decryptField,
131
+ decryptFieldDeterministic,
119
132
  encryptField,
133
+ encryptFieldDeterministic,
120
134
  generateFieldKey,
121
135
  inspectField,
122
136
  } = require('./client-encryption')({
@@ -1279,10 +1293,19 @@ function encodeHello(h) {
1279
1293
  }
1280
1294
 
1281
1295
  function decodeHelloOK(b) {
1282
- if (b.length !== 11) {
1296
+ if (b.length !== 11 && b.length !== 13) {
1283
1297
  throw new NextSQLError('protocol', 'bad hello-ok length');
1284
1298
  }
1285
- return { version: u16(b, 0), authMethod: b[2], secret: u64(b, 3) };
1299
+ let flags = 0;
1300
+ if (b.length === 13) {
1301
+ flags = u16(b, 11);
1302
+ // The server omits the field when it accepted no capability, so a
1303
+ // present-but-zero field is a second encoding of the v1 hello-ok.
1304
+ if (flags === 0) {
1305
+ throw new NextSQLError('protocol', 'empty hello-ok flags');
1306
+ }
1307
+ }
1308
+ return { version: u16(b, 0), authMethod: b[2], secret: u64(b, 3), flags };
1286
1309
  }
1287
1310
 
1288
1311
  function encodeAuth(password) {
@@ -1374,7 +1397,18 @@ function decodeDataBatch(b) {
1374
1397
  function decodeError(b) {
1375
1398
  const code = readU16String(b, 0, MAX_NAME);
1376
1399
  const msg = readU16String(b, code.next, MAX_NAME);
1377
- return new NextSQLError(code.value, msg.value);
1400
+ // Optional trailing field, present only from a server that accepted
1401
+ // FlagPublicErrorCodes. Its absence is normal — an older server ignores the
1402
+ // request bit — so this must never be required.
1403
+ let publicCode = '';
1404
+ if (msg.next < b.length) {
1405
+ const p = readU16String(b, msg.next, MAX_NAME);
1406
+ if (p.value === '') {
1407
+ throw new NextSQLError('protocol', 'empty public error code');
1408
+ }
1409
+ publicCode = p.value;
1410
+ }
1411
+ return new NextSQLError(code.value, msg.value, publicCode);
1378
1412
  }
1379
1413
 
1380
1414
  function decodeCommandComplete(b) {
@@ -1567,8 +1601,8 @@ function connectSocket(cfg) {
1567
1601
  if (cfg.tls.ca) {
1568
1602
  opts.ca = cfg.tls.ca;
1569
1603
  }
1570
- if (cfg.tls.rejectUnauthorized === false) {
1571
- opts.rejectUnauthorized = false;
1604
+ if (typeof cfg.tls.rejectUnauthorized === 'boolean') {
1605
+ opts.rejectUnauthorized = cfg.tls.rejectUnauthorized;
1572
1606
  }
1573
1607
  const sock = tls.connect(opts, () => resolve(sock));
1574
1608
  sock.on('error', onErr);
@@ -1724,11 +1758,13 @@ class Conn {
1724
1758
  this.wire = wire;
1725
1759
  this.secret = 0n;
1726
1760
  this.busy = false;
1761
+ this.publicErrorCodes = false;
1727
1762
  }
1728
1763
 
1729
1764
  async handshake() {
1730
1765
  await this.wire.writeFrame(Type.Hello, encodeHello({
1731
1766
  version: VERSION,
1767
+ flags: FlagPublicErrorCodes,
1732
1768
  database: this.cfg.database || '',
1733
1769
  user: this.cfg.user,
1734
1770
  realm: this.cfg.realm || '',
@@ -1739,6 +1775,9 @@ class Conn {
1739
1775
  }
1740
1776
  const ok = decodeHelloOK(msg.payload);
1741
1777
  this.secret = ok.secret;
1778
+ // Diagnostic only: decodeError reads the field whenever it is present,
1779
+ // so nothing depends on this having been echoed.
1780
+ this.publicErrorCodes = (ok.flags & FlagPublicErrorCodes) !== 0;
1742
1781
  await this.wire.writeFrame(Type.Auth, encodeAuth(this.cfg.password || ''));
1743
1782
  msg = await this.wire.readFrame();
1744
1783
  if (msg.type !== Type.AuthOK) {
@@ -1888,6 +1927,14 @@ class Conn {
1888
1927
  return decryptField(this.cfg.fieldKeys, this.cfg.database || '', table, column, type, ciphertext);
1889
1928
  }
1890
1929
 
1930
+ async encryptFieldDeterministic(table, column, type, value) {
1931
+ return encryptFieldDeterministic(this.cfg.fieldKeys, this.cfg.database || '', table, column, type, value);
1932
+ }
1933
+
1934
+ async decryptFieldDeterministic(table, column, type, ciphertext) {
1935
+ return decryptFieldDeterministic(this.cfg.fieldKeys, this.cfg.database || '', table, column, type, ciphertext);
1936
+ }
1937
+
1891
1938
  async prepare(sql) {
1892
1939
  if (!this.wire) {
1893
1940
  throw new NextSQLError('unavailable', 'connection closed');
@@ -2152,6 +2199,8 @@ async function connectCluster(cfg) {
2152
2199
 
2153
2200
  module.exports = {
2154
2201
  connect,
2202
+ FlagCancel,
2203
+ FlagPublicErrorCodes,
2155
2204
  connectCluster,
2156
2205
  Cluster,
2157
2206
  NextSQLError,
@@ -2181,7 +2230,9 @@ module.exports = {
2181
2230
  MemoryFieldKeyring,
2182
2231
  FileFieldKeyring,
2183
2232
  decryptField,
2233
+ decryptFieldDeterministic,
2184
2234
  encryptField,
2235
+ encryptFieldDeterministic,
2185
2236
  generateFieldKey,
2186
2237
  inspectField,
2187
2238
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bzync/nextsql",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Official NextSQL Node.js driver. Keys never go in URLs.",
5
5
  "license": "MIT",
6
6
  "main": "nextsql.js",