@catalystiq/test-kit 0.3.2 → 0.4.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 +3 -1
- package/dist/bin/check-install-scripts.d.ts +2 -0
- package/dist/bin/check-install-scripts.js +8 -0
- package/dist/env/README.md +8 -2
- package/dist/env/dev-guard.d.ts +7 -6
- package/dist/env/dev-guard.js +28 -26
- package/dist/install/README.md +55 -0
- package/dist/install/index.d.ts +97 -0
- package/dist/install/index.js +460 -0
- package/dist/neon-endpoint/README.md +57 -0
- package/dist/neon-endpoint/index.d.ts +112 -0
- package/dist/neon-endpoint/index.js +359 -0
- package/dist/setup/README.md +1 -1
- package/dist/setup/database.d.ts +35 -3
- package/dist/setup/database.js +227 -25
- package/dist/setup/network.js +1 -1
- package/package.json +14 -4
package/dist/setup/database.js
CHANGED
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
*
|
|
4
4
|
* 1. A database URL in the test environment whose host is on the repo's production list is refused. An entry matches the
|
|
5
5
|
* host itself, any subdomain of it, and, for a host whose first label is a compute endpoint id (`ep-…`), that
|
|
6
|
-
* endpoint with or without its `-pooler` suffix.
|
|
6
|
+
* endpoint with or without its `-pooler` suffix. A host is read in every spelling a client resolves
|
|
7
|
+
* (`canonicalHost`): percent-encoded, in fullwidth letters, with a soft hyphen inside.
|
|
7
8
|
* 2. A database URL in the test environment that names the same database as any `.env*` file at the repo root is
|
|
8
9
|
* refused: those files hold a developer's own (perhaps production) connection, and a test must never reach it. Every
|
|
9
10
|
* `postgres://` text in the files counts, in any variable, quoted any way, or in a comment, except the lines whose key
|
|
@@ -14,10 +15,80 @@ import { join } from 'node:path';
|
|
|
14
15
|
const LOOPBACK = new Set(['localhost', '127.0.0.1', '::1', '[::1]']);
|
|
15
16
|
/** A Postgres URL anywhere in a text. */
|
|
16
17
|
const POSTGRES_URL = /postgres(?:ql)?:\/\/[^\s'"`]+/gi;
|
|
18
|
+
/** A value that holds a Postgres URL, or opens with a Postgres scheme however it goes on (`postgres:user:pw@host/db`). */
|
|
19
|
+
const HOLDS_SCHEME = /postgres(?:ql)?:\/\/|^postgres(?:ql)?:/i;
|
|
20
|
+
/** A value without the white space and control characters it opens with, which a URL parser drops there. */
|
|
21
|
+
function withoutLeadingControls(value) {
|
|
22
|
+
let start = 0;
|
|
23
|
+
while (start < value.length && value.charCodeAt(start) <= 0x20)
|
|
24
|
+
start++;
|
|
25
|
+
return value.slice(start);
|
|
26
|
+
}
|
|
17
27
|
/** The text's own punctuation after a URL is not part of it: `(postgres://h/db)` names `db`. */
|
|
18
28
|
function trimmedUrl(match) {
|
|
19
29
|
return match.replace(/[)>,;.]+$/, '');
|
|
20
30
|
}
|
|
31
|
+
/** The code points IDNA mapping ignores, so a host holding one resolves without it: a soft hyphen, a zero-width space. */
|
|
32
|
+
const IGNORED = /\p{Default_Ignorable_Code_Point}/gu;
|
|
33
|
+
/** The ideographic full stop, which IDNA mapping reads as a dot (NFKC has already made the fullwidth and halfwidth ones a dot or this). */
|
|
34
|
+
const FULL_STOPS = /\u3002/g;
|
|
35
|
+
/** The tab and line breaks a URL parser drops from anywhere in a URL, before it reads anything else. */
|
|
36
|
+
const URL_DROPPED = /[\t\n\r]/g;
|
|
37
|
+
/** A text with each run of `%XX` escapes decoded as UTF-8, so one malformed `%` elsewhere cannot hide the rest. */
|
|
38
|
+
function percentDecoded(text) {
|
|
39
|
+
return text.replace(/(?:%[0-9a-f]{2})+/gi, (run) => Buffer.from(run.replaceAll('%', ''), 'hex').toString('utf8'));
|
|
40
|
+
}
|
|
41
|
+
/** A text percent-decoded and IDNA-mapped, near enough: compatibility forms as ASCII (a fullwidth `ep-` is `ep-`), lowercased. */
|
|
42
|
+
function mapped(text) {
|
|
43
|
+
return percentDecoded(text).normalize('NFKC').replace(IGNORED, '').replace(FULL_STOPS, '.').toLowerCase();
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* A host as a client resolves it, in ASCII where it has an ASCII reading: without the tab and line breaks a URL
|
|
47
|
+
* parser drops (dropped first, as it does, so one inside a `%XX` escape does not break the escape), then {@link mapped}. A `postgres:` URL keeps its host as written, but the Neon driver re-reads the string as an
|
|
48
|
+
* `http:` URL and node-postgres percent-decodes it, so `ep-…`, `e%70-…` and `ep-pr<soft hyphen>od-…` all connect to
|
|
49
|
+
* `ep-prod-…`. It drops more than a URL parser does (a guard errs on refusing).
|
|
50
|
+
*/
|
|
51
|
+
export function canonicalHost(text) {
|
|
52
|
+
return mapped(text.replace(URL_DROPPED, '')).replace(URL_DROPPED, '');
|
|
53
|
+
}
|
|
54
|
+
/** A backslash escape of a libpq keyword/value string (`host=e\p-…`, `host='ep\-…'`): the client reads the character after it. */
|
|
55
|
+
const LIBPQ_ESCAPE = /\\([^\\])/g;
|
|
56
|
+
/**
|
|
57
|
+
* A host as an `http:` URL reads it, or null: an IPv4 address in any numeric spelling as dotted decimal (`0x0a.0.0.5`,
|
|
58
|
+
* `167772165` and `10.5` are `10.0.0.5`), an internationalised name in its `xn--` form. Only a bare host is read.
|
|
59
|
+
*/
|
|
60
|
+
function urlHost(host) {
|
|
61
|
+
if (host === '' || /[\s/?#@\\:[\]%]/.test(host))
|
|
62
|
+
return null;
|
|
63
|
+
try {
|
|
64
|
+
return new URL(`http://${host}/`).hostname || null;
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Every reading of a text a host is looked for in: as written, mapped, and as {@link canonicalHost} reads it (a
|
|
72
|
+
* dropped character joins what stood on either side of it, which can hide a host as well as show one), and the last
|
|
73
|
+
* two without libpq's backslash escapes. Then the same of the text percent-decoded, up to three times: a route encoded
|
|
74
|
+
* twice (`options=endpoint%253D…`) is decoded by the client that reads the parameter.
|
|
75
|
+
*/
|
|
76
|
+
export function hostSpellings(text) {
|
|
77
|
+
const spellings = new Set();
|
|
78
|
+
let form = text;
|
|
79
|
+
for (let round = 0; round < 4; round++) {
|
|
80
|
+
spellings.add(form);
|
|
81
|
+
for (const text of new Set([form, form.replace(LIBPQ_ESCAPE, '$1')])) {
|
|
82
|
+
for (const spelling of [mapped(text), canonicalHost(text)])
|
|
83
|
+
spellings.add(spelling);
|
|
84
|
+
}
|
|
85
|
+
const next = percentDecoded(form);
|
|
86
|
+
if (next === form)
|
|
87
|
+
break;
|
|
88
|
+
form = next;
|
|
89
|
+
}
|
|
90
|
+
return [...spellings];
|
|
91
|
+
}
|
|
21
92
|
/** The endpoint id of a host whose first label is one (`ep-quiet-sun-123-pooler.region.example` → `ep-quiet-sun-123`). */
|
|
22
93
|
export function endpointOf(host) {
|
|
23
94
|
const first = host.toLowerCase().split('.')[0] ?? '';
|
|
@@ -25,6 +96,15 @@ export function endpointOf(host) {
|
|
|
25
96
|
return null;
|
|
26
97
|
return first.endsWith('-pooler') ? first.slice(0, -'-pooler'.length) : first;
|
|
27
98
|
}
|
|
99
|
+
/** A URL part percent-decoded, or as written when an escape in it is malformed (`app_%s`, a template). */
|
|
100
|
+
function decodedOrAsWritten(part) {
|
|
101
|
+
try {
|
|
102
|
+
return decodeURIComponent(part);
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
return part;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
28
108
|
/**
|
|
29
109
|
* The one database a URL names, as a comparable key: `endpoint:<id>` for an endpoint host, else
|
|
30
110
|
* `<host>:<port>/<database>` with every loopback name read as `localhost`. Null when the text is not a Postgres URL.
|
|
@@ -39,11 +119,11 @@ export function databasePlace(url) {
|
|
|
39
119
|
}
|
|
40
120
|
if (parsed.protocol !== 'postgres:' && parsed.protocol !== 'postgresql:')
|
|
41
121
|
return null;
|
|
42
|
-
const host = parsed.hostname
|
|
122
|
+
const host = canonicalHost(parsed.hostname);
|
|
43
123
|
const endpoint = endpointOf(host);
|
|
44
124
|
if (endpoint)
|
|
45
125
|
return `endpoint:${endpoint}`;
|
|
46
|
-
const database =
|
|
126
|
+
const database = decodedOrAsWritten(parsed.pathname.replace(/^\//, '')) || decodedOrAsWritten(parsed.username) || 'postgres';
|
|
47
127
|
return `${LOOPBACK.has(host) || host === '' ? 'localhost' : host}:${parsed.port || '5432'}/${database}`;
|
|
48
128
|
}
|
|
49
129
|
/** Every Postgres URL in a text. */
|
|
@@ -78,40 +158,159 @@ export function protectedPlaces(root, testDatabaseEnv = []) {
|
|
|
78
158
|
}
|
|
79
159
|
return [...places].sort();
|
|
80
160
|
}
|
|
81
|
-
/**
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
161
|
+
/** A host whose every label is a number: an IPv4 address in some form, or a version, a port, a count. */
|
|
162
|
+
const NUMERIC_HOST = /^(?:0x[0-9a-f]*|\d+)(?:\.(?:0x[0-9a-f]*|\d+))*$/i;
|
|
163
|
+
/**
|
|
164
|
+
* Every reading of one host: each of its {@link hostSpellings} lowercased, and as an `http:` URL reads it (before and
|
|
165
|
+
* after lowercasing: a capital sigma lowercases by its position, and a URL maps it its own way). A trailing dot is the
|
|
166
|
+
* same name, fully qualified. With `numeric` false a host of numbers alone is not read as an address.
|
|
167
|
+
*/
|
|
168
|
+
function hostReadings(host, numeric) {
|
|
169
|
+
const readings = new Set();
|
|
170
|
+
for (const spelling of hostSpellings(host)) {
|
|
171
|
+
const written = spelling.replace(/\.+$/, '');
|
|
172
|
+
const lower = written.toLowerCase();
|
|
173
|
+
readings.add(lower);
|
|
174
|
+
for (const form of new Set([written, lower])) {
|
|
175
|
+
if (!numeric && NUMERIC_HOST.test(form))
|
|
176
|
+
continue;
|
|
177
|
+
const read = urlHost(form);
|
|
178
|
+
if (read)
|
|
179
|
+
readings.add(read);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return [...readings];
|
|
183
|
+
}
|
|
184
|
+
/** Each list entry with the forms it is compared in: as written and as a URL reads it (a name in its `xn--` form). */
|
|
185
|
+
function listedForms(productionHosts) {
|
|
186
|
+
return productionHosts
|
|
187
|
+
.map((raw) => raw.trim())
|
|
188
|
+
.filter((entry) => entry !== '')
|
|
189
|
+
.map((entry) => {
|
|
190
|
+
const lower = entry.toLowerCase();
|
|
191
|
+
return { entry, forms: [...new Set([lower, urlHost(entry) ?? lower])] };
|
|
92
192
|
});
|
|
93
193
|
}
|
|
94
|
-
/**
|
|
194
|
+
/** The list entry one of a host's readings is: the host itself, a subdomain of it, or the same endpoint id. */
|
|
195
|
+
function entryOf(readings, listed) {
|
|
196
|
+
for (const h of readings) {
|
|
197
|
+
const endpoint = endpointOf(h);
|
|
198
|
+
for (const { entry, forms } of listed) {
|
|
199
|
+
if (forms.some((form) => h === form || h.endsWith(`.${form}`) || (endpoint !== null && endpointOf(form) === endpoint)))
|
|
200
|
+
return entry;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
/** Whether `host` is on the production list, in any spelling a client resolves to a listed host ({@link hostReadings}). */
|
|
206
|
+
export function isProductionHost(host, productionHosts) {
|
|
207
|
+
return entryOf(hostReadings(host, true), listedForms(productionHosts)) !== null;
|
|
208
|
+
}
|
|
209
|
+
/** A host-shaped run of a value: labels of letters, digits and `-`, joined by dots (`ep-…-pooler.c-1.example`, `ep-…`). */
|
|
210
|
+
const HOST_TOKEN = /[a-z0-9-]+(?:\.[a-z0-9-]+)*/gi;
|
|
211
|
+
/** A character outside ASCII. */
|
|
212
|
+
export const NON_ASCII = /[\u0080-\u{10ffff}]/u;
|
|
213
|
+
/**
|
|
214
|
+
* The same with any character outside ASCII in its labels: an internationalised name, which a client resolves in its
|
|
215
|
+
* `xn--` form. Any character, not letters alone: a name may hold a middle dot or a zero-width non-joiner.
|
|
216
|
+
*/
|
|
217
|
+
const UNICODE_HOST_TOKEN = /(?:[a-z0-9-]|[\u0080-\u{10ffff}])+(?:\.(?:[a-z0-9-]|[\u0080-\u{10ffff}])+)*/giu;
|
|
218
|
+
/**
|
|
219
|
+
* Where a host stands: after a URL's `//` or `\\`, after a Postgres scheme and one slash, as a keyword/value
|
|
220
|
+
* string's `host=` or `hostaddr=`, or after another host of a list (`db.example:5432,`).
|
|
221
|
+
*/
|
|
222
|
+
const HOST_POSITION = /(?:[/\\]{2}|postgres(?:ql)?:[/\\]|host(?:addr)?\s*=\s*['"]?|[a-z](?::\d+)?,)$/i;
|
|
223
|
+
/** Straight after a Postgres scheme: a host when a path, a port or a query follows it (`postgres:10.5/db`), else an image tag (`postgres:10.5`). */
|
|
224
|
+
const SCHEME_BEFORE = /postgres(?:ql)?:$/i;
|
|
225
|
+
/** The text before a URL's `@`, back to the last white space: a URL's own (`//`, a Postgres scheme), not `name@1.2.3`. */
|
|
226
|
+
const URL_BEFORE_AT = /(?:[/\\]{2}|postgres(?:ql)?:)\S*@$/i;
|
|
227
|
+
/** Whether the token at `index` of `text` stands where a host does. */
|
|
228
|
+
function inHostPosition(text, index, length) {
|
|
229
|
+
const before = text.slice(Math.max(0, index - 512), index);
|
|
230
|
+
if (SCHEME_BEFORE.test(before))
|
|
231
|
+
return /^[/\\:?]/.test(text.slice(index + length));
|
|
232
|
+
return HOST_POSITION.test(before) || URL_BEFORE_AT.test(before);
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* The production list entry a value names anywhere in it, or null. One pass over every host-shaped token of the value,
|
|
236
|
+
* in each of its {@link hostSpellings} (the characters a URL parser drops are dropped before the tokens are cut), so
|
|
237
|
+
* every connection-string shape counts: a URL (one host or several), its `?host=` and `options=endpoint%3D…`, a
|
|
238
|
+
* keyword/value string (`host=… dbname=…`), `PGHOST` (with a port), `PGOPTIONS endpoint=…`. A token of numbers alone is
|
|
239
|
+
* read as an address only where a host stands ({@link inHostPosition}): elsewhere it is a version or a count
|
|
240
|
+
* (`npm/10.8.2` is not `10.8.0.2`, `web@10.0.5` is not `10.0.0.5`).
|
|
241
|
+
*/
|
|
242
|
+
export function productionEntryIn(value, productionHosts) {
|
|
243
|
+
const listed = listedForms(productionHosts);
|
|
244
|
+
const seen = new Set();
|
|
245
|
+
for (const text of hostSpellings(value)) {
|
|
246
|
+
const tokens = [...text.matchAll(HOST_TOKEN)];
|
|
247
|
+
// Only a token with a character outside ASCII is new here: the rest are the tokens above.
|
|
248
|
+
for (const m of text.matchAll(UNICODE_HOST_TOKEN))
|
|
249
|
+
if (NON_ASCII.test(m[0]))
|
|
250
|
+
tokens.push(m);
|
|
251
|
+
for (const m of tokens) {
|
|
252
|
+
const numeric = NUMERIC_HOST.test(m[0]) && inHostPosition(text, m.index, m[0].length);
|
|
253
|
+
const key = `${numeric ? 1 : 0}${m[0]}`;
|
|
254
|
+
if (seen.has(key))
|
|
255
|
+
continue;
|
|
256
|
+
seen.add(key);
|
|
257
|
+
const entry = entryOf(hostReadings(m[0], numeric), listed);
|
|
258
|
+
if (entry)
|
|
259
|
+
return entry;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
return null;
|
|
263
|
+
}
|
|
264
|
+
/** The refusal of a production database: it carries the matched list entry, never the value or any part of it. */
|
|
265
|
+
function productionRefusal(label, entry) {
|
|
266
|
+
return new Error(`test-kit: ${label} names a PRODUCTION database host (${entry}). Tests never connect to production.`);
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* Throws when `url` names a production list entry (as its host, or anywhere else in it: `?host=`, `options=endpoint…`;
|
|
270
|
+
* also when it does not parse as a URL at all) or a database a `.env*` file names. The message carries the matched list entry, never the URL or any part of it.
|
|
271
|
+
*/
|
|
95
272
|
export function assertSafeDatabaseUrl(url, rules, label = 'a database URL') {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
const
|
|
100
|
-
|
|
101
|
-
|
|
273
|
+
// The host as a postgres: URL holds it and as an http: URL reads the same string, which is how the Neon driver
|
|
274
|
+
// reads it: a backslash ends the host there (`prod\@other` is `prod`), and `postgres:///host` has one.
|
|
275
|
+
const hosts = [];
|
|
276
|
+
for (const reading of [url, url.replace(/^[a-z][a-z0-9+.-]*:/i, 'http:')]) {
|
|
277
|
+
try {
|
|
278
|
+
hosts.push(new URL(reading).hostname);
|
|
279
|
+
}
|
|
280
|
+
catch {
|
|
281
|
+
// A reading that does not parse names no host; the search below still reads the text.
|
|
282
|
+
}
|
|
102
283
|
}
|
|
103
|
-
|
|
284
|
+
const listed = listedForms(rules.productionHosts);
|
|
285
|
+
const entry = entryOf(hosts.filter((host) => host !== '').flatMap((host) => hostReadings(host, true)), listed) ?? productionEntryIn(url, rules.productionHosts);
|
|
286
|
+
if (entry)
|
|
287
|
+
throw productionRefusal(label, entry);
|
|
288
|
+
const place = databasePlace(url);
|
|
289
|
+
if (place !== null && rules.protectedPlaces.includes(place)) {
|
|
104
290
|
throw new Error(`test-kit: ${label} names the same database as a .env* file at the repository root. ` +
|
|
105
291
|
'A test database must be its own (a container, or a branch made for the run).');
|
|
106
292
|
}
|
|
107
293
|
}
|
|
108
|
-
/**
|
|
294
|
+
/**
|
|
295
|
+
* Runs {@link assertSafeDatabaseUrl} over every Postgres URL any variable of `env` holds: in the value as written, and
|
|
296
|
+
* without the characters a URL parser drops (a tab, a line break, a zero-width no-break space), which would otherwise
|
|
297
|
+
* end the URL where one stands.
|
|
298
|
+
*/
|
|
109
299
|
export function assertSafeDatabaseEnv(env, rules) {
|
|
110
300
|
for (const [name, value] of Object.entries(env)) {
|
|
111
301
|
if (!value)
|
|
112
302
|
continue;
|
|
113
|
-
for (const
|
|
114
|
-
|
|
303
|
+
for (const text of new Set([value, value.replace(URL_DROPPED, '').replace(IGNORED, '')])) {
|
|
304
|
+
for (const url of postgresUrlsIn(text))
|
|
305
|
+
assertSafeDatabaseUrl(url, rules, `$${name}`);
|
|
306
|
+
}
|
|
307
|
+
// The scan above ends a URL at a quote or a space (a password may hold one) and reads no scheme without `//`
|
|
308
|
+
// (`postgres:u:p@host/db`, which an `http:` reading still resolves): the whole of such a value is searched too.
|
|
309
|
+
if (HOLDS_SCHEME.test(withoutLeadingControls(value.replace(URL_DROPPED, '').replace(IGNORED, '')))) {
|
|
310
|
+
const entry = productionEntryIn(value, rules.productionHosts);
|
|
311
|
+
if (entry)
|
|
312
|
+
throw productionRefusal(`$${name}`, entry);
|
|
313
|
+
}
|
|
115
314
|
}
|
|
116
315
|
}
|
|
117
316
|
/** The hosts of every Postgres URL `env` holds: what the network guard lets through once the URLs passed the check. */
|
|
@@ -125,6 +324,9 @@ export function databaseHostsIn(env) {
|
|
|
125
324
|
const host = new URL(url).hostname;
|
|
126
325
|
if (host)
|
|
127
326
|
hosts.add(host.toLowerCase());
|
|
327
|
+
// The same host as the clients resolve it, which is where the socket goes.
|
|
328
|
+
if (host)
|
|
329
|
+
hosts.add(canonicalHost(host));
|
|
128
330
|
}
|
|
129
331
|
catch {
|
|
130
332
|
// not a URL after all
|
package/dist/setup/network.js
CHANGED
|
@@ -57,7 +57,7 @@ export function installNetworkGuard(allowHosts = []) {
|
|
|
57
57
|
s.realFetch = realFetch;
|
|
58
58
|
const guardedFetch = (async (input, init) => {
|
|
59
59
|
const url = requestUrl(input);
|
|
60
|
-
let host
|
|
60
|
+
let host;
|
|
61
61
|
try {
|
|
62
62
|
const parsed = new URL(url);
|
|
63
63
|
if (parsed.protocol === 'data:' || parsed.protocol === 'blob:')
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@catalystiq/test-kit",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Shared test setup for Vitest, a fake MCP Apps host for jsdom and Playwright, the local Neon test-database run,
|
|
3
|
+
"version": "0.4.0",
|
|
4
|
+
"description": "Shared test setup for Vitest, a fake MCP Apps host for jsdom and Playwright, the local Neon test-database run, the local environment guard, the database endpoint reader and the install hardening check.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"sideEffects": true,
|
|
@@ -42,6 +42,15 @@
|
|
|
42
42
|
"types": "./dist/env/index.d.ts",
|
|
43
43
|
"import": "./dist/env/index.js"
|
|
44
44
|
},
|
|
45
|
+
"./neon-endpoint": {
|
|
46
|
+
"types": "./dist/neon-endpoint/index.d.ts",
|
|
47
|
+
"import": "./dist/neon-endpoint/index.js",
|
|
48
|
+
"default": "./dist/neon-endpoint/index.js"
|
|
49
|
+
},
|
|
50
|
+
"./install": {
|
|
51
|
+
"types": "./dist/install/index.d.ts",
|
|
52
|
+
"import": "./dist/install/index.js"
|
|
53
|
+
},
|
|
45
54
|
"./neon-test-db": {
|
|
46
55
|
"types": "./dist/neon-test-db/neon-test-db.d.mts",
|
|
47
56
|
"import": "./dist/neon-test-db/neon-test-db.mjs"
|
|
@@ -53,6 +62,7 @@
|
|
|
53
62
|
"tk-check-allowlist": "dist/bin/check-allowlist.js",
|
|
54
63
|
"tk-dev-guard": "dist/bin/dev-guard.js",
|
|
55
64
|
"tk-check-env-example": "dist/bin/check-env-example.js",
|
|
65
|
+
"tk-check-install-scripts": "dist/bin/check-install-scripts.js",
|
|
56
66
|
"tk-neon-test-db": "dist/neon-test-db/neon-test-db.mjs"
|
|
57
67
|
},
|
|
58
68
|
"files": [
|
|
@@ -94,7 +104,7 @@
|
|
|
94
104
|
}
|
|
95
105
|
},
|
|
96
106
|
"devDependencies": {
|
|
97
|
-
"@eslint/js": "
|
|
107
|
+
"@eslint/js": "10.0.1",
|
|
98
108
|
"@modelcontextprotocol/client": "2.4.0",
|
|
99
109
|
"@modelcontextprotocol/core": "2.4.0",
|
|
100
110
|
"@modelcontextprotocol/ext-apps": "2.0.4",
|
|
@@ -107,7 +117,7 @@
|
|
|
107
117
|
"@types/react": "19.3.0",
|
|
108
118
|
"@types/react-dom": "19.3.0",
|
|
109
119
|
"esbuild": "0.28.2",
|
|
110
|
-
"eslint": "
|
|
120
|
+
"eslint": "10.12.0",
|
|
111
121
|
"jsdom": "30.1.3",
|
|
112
122
|
"react": "19.3.1",
|
|
113
123
|
"react-dom": "19.3.1",
|