turbine-orm 0.81.0 → 0.90.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.
Files changed (295) hide show
  1. package/README.md +26 -25
  2. package/dist/checkout.d.ts +27 -2
  3. package/dist/checkout.js +75 -4
  4. package/dist/cjs/checkout.d.ts +27 -2
  5. package/dist/cjs/checkout.js +76 -3
  6. package/dist/cjs/cli/code-first-tags.d.ts +62 -0
  7. package/dist/cjs/cli/code-first-tags.js +124 -0
  8. package/dist/cjs/cli/compile-query.js +9 -0
  9. package/dist/cjs/cli/config.d.ts +35 -6
  10. package/dist/cjs/cli/config.js +17 -1
  11. package/dist/cjs/cli/destructive.d.ts +7 -4
  12. package/dist/cjs/cli/destructive.js +224 -20
  13. package/dist/cjs/cli/error-catalog.js +36 -0
  14. package/dist/cjs/cli/error-text.d.ts +15 -0
  15. package/dist/cjs/cli/error-text.js +40 -0
  16. package/dist/cjs/cli/index.d.ts +62 -4
  17. package/dist/cjs/cli/index.js +568 -108
  18. package/dist/cjs/cli/local-server-guard.d.ts +53 -0
  19. package/dist/cjs/cli/local-server-guard.js +119 -0
  20. package/dist/cjs/cli/local-server-output.d.ts +41 -0
  21. package/dist/cjs/cli/local-server-output.js +103 -0
  22. package/dist/cjs/cli/mcp.d.ts +8 -0
  23. package/dist/cjs/cli/mcp.js +121 -32
  24. package/dist/cjs/cli/migrate.d.ts +113 -5
  25. package/dist/cjs/cli/migrate.js +270 -21
  26. package/dist/cjs/cli/observe.d.ts +6 -1
  27. package/dist/cjs/cli/observe.js +85 -55
  28. package/dist/cjs/cli/pii-predicate-guard.d.ts +43 -23
  29. package/dist/cjs/cli/pii-predicate-guard.js +98 -37
  30. package/dist/cjs/cli/prisma-resolve.js +17 -13
  31. package/dist/cjs/cli/prisma-schema.js +12 -4
  32. package/dist/cjs/cli/studio-ui.generated.js +1 -1
  33. package/dist/cjs/cli/studio.d.ts +14 -2
  34. package/dist/cjs/cli/studio.js +244 -69
  35. package/dist/cjs/cli/ui.d.ts +1 -1
  36. package/dist/cjs/client.d.ts +558 -85
  37. package/dist/cjs/client.js +1270 -395
  38. package/dist/cjs/connection-guard.d.ts +13 -0
  39. package/dist/cjs/connection-guard.js +28 -0
  40. package/dist/cjs/connection-url.d.ts +51 -16
  41. package/dist/cjs/connection-url.js +67 -34
  42. package/dist/cjs/diagnostics.d.ts +81 -0
  43. package/dist/cjs/diagnostics.js +147 -0
  44. package/dist/cjs/dialect.d.ts +449 -20
  45. package/dist/cjs/dialect.js +117 -76
  46. package/dist/cjs/engine-config.d.ts +7 -6
  47. package/dist/cjs/errors.d.ts +126 -6
  48. package/dist/cjs/errors.js +256 -13
  49. package/dist/cjs/generate.d.ts +57 -7
  50. package/dist/cjs/generate.js +635 -279
  51. package/dist/cjs/global-filters-config.d.ts +59 -0
  52. package/dist/cjs/global-filters-config.js +367 -0
  53. package/dist/cjs/index.d.ts +9 -4
  54. package/dist/cjs/index.js +10 -2
  55. package/dist/cjs/introspect.d.ts +31 -6
  56. package/dist/cjs/introspect.js +108 -50
  57. package/dist/cjs/log-codes.d.ts +12 -0
  58. package/dist/cjs/log-codes.js +77 -0
  59. package/dist/cjs/logger.d.ts +87 -0
  60. package/dist/cjs/logger.js +98 -0
  61. package/dist/cjs/mssql.d.ts +8 -5
  62. package/dist/cjs/mssql.js +352 -65
  63. package/dist/cjs/mysql.d.ts +3 -5
  64. package/dist/cjs/mysql.js +420 -70
  65. package/dist/cjs/nested-write.d.ts +3 -0
  66. package/dist/cjs/nested-write.js +179 -114
  67. package/dist/cjs/pipeline-submittable.d.ts +12 -0
  68. package/dist/cjs/pipeline-submittable.js +118 -44
  69. package/dist/cjs/pipeline.js +21 -26
  70. package/dist/cjs/powdb-introspect.d.ts +5 -2
  71. package/dist/cjs/powdb-introspect.js +20 -15
  72. package/dist/cjs/powdb-shared.d.ts +36 -5
  73. package/dist/cjs/powdb-shared.js +67 -8
  74. package/dist/cjs/powdb.d.ts +4 -0
  75. package/dist/cjs/powdb.js +183 -51
  76. package/dist/cjs/powql.d.ts +394 -51
  77. package/dist/cjs/powql.js +1829 -284
  78. package/dist/cjs/prisma-compat.d.ts +23 -15
  79. package/dist/cjs/prisma-compat.js +507 -372
  80. package/dist/cjs/query/aggregates.d.ts +17 -1
  81. package/dist/cjs/query/aggregates.js +261 -29
  82. package/dist/cjs/query/batched-loader.d.ts +25 -6
  83. package/dist/cjs/query/batched-loader.js +103 -20
  84. package/dist/cjs/query/builder.d.ts +325 -184
  85. package/dist/cjs/query/builder.js +1452 -490
  86. package/dist/cjs/query/column-ref.d.ts +61 -0
  87. package/dist/cjs/query/column-ref.js +73 -0
  88. package/dist/cjs/query/compound-unique.d.ts +21 -0
  89. package/dist/cjs/query/compound-unique.js +98 -18
  90. package/dist/cjs/query/deferred.d.ts +53 -16
  91. package/dist/cjs/query/expr.d.ts +217 -0
  92. package/dist/cjs/query/expr.js +859 -0
  93. package/dist/cjs/query/filters.d.ts +155 -53
  94. package/dist/cjs/query/filters.js +613 -76
  95. package/dist/cjs/query/index.d.ts +2 -0
  96. package/dist/cjs/query/index.js +7 -1
  97. package/dist/cjs/query/json-value.d.ts +43 -0
  98. package/dist/cjs/query/json-value.js +59 -0
  99. package/dist/cjs/query/normalize-args.d.ts +87 -0
  100. package/dist/cjs/query/normalize-args.js +527 -0
  101. package/dist/cjs/query/option-surface.d.ts +16 -4
  102. package/dist/cjs/query/option-surface.js +57 -14
  103. package/dist/cjs/query/relation-names.d.ts +14 -0
  104. package/dist/cjs/query/relation-names.js +77 -18
  105. package/dist/cjs/query/relations.d.ts +30 -3
  106. package/dist/cjs/query/relations.js +219 -63
  107. package/dist/cjs/query/row-lock.d.ts +49 -0
  108. package/dist/cjs/query/row-lock.js +138 -0
  109. package/dist/cjs/query/sql-comment.d.ts +161 -0
  110. package/dist/cjs/query/sql-comment.js +381 -0
  111. package/dist/cjs/query/stream-idle.d.ts +132 -0
  112. package/dist/cjs/query/stream-idle.js +212 -0
  113. package/dist/cjs/query/table-client.d.ts +101 -0
  114. package/dist/cjs/query/table-client.js +74 -0
  115. package/dist/cjs/query/types.d.ts +677 -298
  116. package/dist/cjs/query/utils.d.ts +105 -6
  117. package/dist/cjs/query/utils.js +426 -19
  118. package/dist/cjs/query/warn-registry.d.ts +18 -0
  119. package/dist/cjs/query/warn-registry.js +18 -0
  120. package/dist/cjs/query/where-compile.d.ts +138 -14
  121. package/dist/cjs/query/where-compile.js +513 -29
  122. package/dist/cjs/query/where.d.ts +159 -59
  123. package/dist/cjs/query/where.js +716 -246
  124. package/dist/cjs/query/writes.d.ts +112 -50
  125. package/dist/cjs/query/writes.js +1498 -200
  126. package/dist/cjs/query-events.d.ts +4 -2
  127. package/dist/cjs/query-events.js +3 -1
  128. package/dist/cjs/realtime.d.ts +7 -3
  129. package/dist/cjs/realtime.js +13 -2
  130. package/dist/cjs/schema-builder.d.ts +55 -4
  131. package/dist/cjs/schema-builder.js +382 -18
  132. package/dist/cjs/schema-metadata.d.ts +21 -1
  133. package/dist/cjs/schema-metadata.js +29 -7
  134. package/dist/cjs/schema-sql.d.ts +75 -5
  135. package/dist/cjs/schema-sql.js +497 -45
  136. package/dist/cjs/schema.d.ts +26 -5
  137. package/dist/cjs/schema.js +110 -13
  138. package/dist/cjs/sql-placeholders.d.ts +56 -0
  139. package/dist/cjs/sql-placeholders.js +204 -0
  140. package/dist/cjs/sqlite.d.ts +75 -20
  141. package/dist/cjs/sqlite.js +593 -95
  142. package/dist/cjs/supplied-pool-types.d.ts +48 -0
  143. package/dist/cjs/supplied-pool-types.js +256 -0
  144. package/dist/cjs/transaction-control.d.ts +51 -0
  145. package/dist/cjs/transaction-control.js +316 -0
  146. package/dist/cjs/transaction-state.d.ts +342 -0
  147. package/dist/cjs/transaction-state.js +707 -0
  148. package/dist/cjs/typed-sql.d.ts +37 -3
  149. package/dist/cjs/typed-sql.js +102 -8
  150. package/dist/cli/code-first-tags.d.ts +62 -0
  151. package/dist/cli/code-first-tags.js +120 -0
  152. package/dist/cli/compile-query.js +9 -0
  153. package/dist/cli/config.d.ts +35 -6
  154. package/dist/cli/config.js +16 -1
  155. package/dist/cli/destructive.d.ts +7 -4
  156. package/dist/cli/destructive.js +224 -20
  157. package/dist/cli/error-catalog.js +36 -0
  158. package/dist/cli/error-text.d.ts +15 -0
  159. package/dist/cli/error-text.js +37 -0
  160. package/dist/cli/index.d.ts +62 -4
  161. package/dist/cli/index.js +569 -112
  162. package/dist/cli/local-server-guard.d.ts +53 -0
  163. package/dist/cli/local-server-guard.js +113 -0
  164. package/dist/cli/local-server-output.d.ts +41 -0
  165. package/dist/cli/local-server-output.js +98 -0
  166. package/dist/cli/mcp.d.ts +8 -0
  167. package/dist/cli/mcp.js +121 -34
  168. package/dist/cli/migrate.d.ts +113 -5
  169. package/dist/cli/migrate.js +261 -21
  170. package/dist/cli/observe.d.ts +6 -1
  171. package/dist/cli/observe.js +84 -54
  172. package/dist/cli/pii-predicate-guard.d.ts +43 -23
  173. package/dist/cli/pii-predicate-guard.js +100 -35
  174. package/dist/cli/prisma-resolve.js +15 -11
  175. package/dist/cli/prisma-schema.js +12 -4
  176. package/dist/cli/studio-ui.generated.js +1 -1
  177. package/dist/cli/studio.d.ts +14 -2
  178. package/dist/cli/studio.js +246 -71
  179. package/dist/cli/ui.d.ts +1 -1
  180. package/dist/client.d.ts +558 -85
  181. package/dist/client.js +1275 -400
  182. package/dist/connection-guard.d.ts +13 -0
  183. package/dist/connection-guard.js +27 -0
  184. package/dist/connection-url.d.ts +51 -16
  185. package/dist/connection-url.js +63 -34
  186. package/dist/diagnostics.d.ts +81 -0
  187. package/dist/diagnostics.js +144 -0
  188. package/dist/dialect.d.ts +449 -20
  189. package/dist/dialect.js +115 -43
  190. package/dist/engine-config.d.ts +7 -6
  191. package/dist/errors.d.ts +126 -6
  192. package/dist/errors.js +252 -12
  193. package/dist/generate.d.ts +57 -7
  194. package/dist/generate.js +635 -281
  195. package/dist/global-filters-config.d.ts +59 -0
  196. package/dist/global-filters-config.js +361 -0
  197. package/dist/index.d.ts +9 -4
  198. package/dist/index.js +4 -2
  199. package/dist/introspect.d.ts +31 -6
  200. package/dist/introspect.js +106 -50
  201. package/dist/log-codes.d.ts +12 -0
  202. package/dist/log-codes.js +74 -0
  203. package/dist/logger.d.ts +87 -0
  204. package/dist/logger.js +93 -0
  205. package/dist/mssql.d.ts +8 -5
  206. package/dist/mssql.js +354 -67
  207. package/dist/mysql.d.ts +3 -5
  208. package/dist/mysql.js +423 -73
  209. package/dist/nested-write.d.ts +3 -0
  210. package/dist/nested-write.js +177 -112
  211. package/dist/pipeline-submittable.d.ts +12 -0
  212. package/dist/pipeline-submittable.js +117 -44
  213. package/dist/pipeline.js +21 -26
  214. package/dist/powdb-introspect.d.ts +5 -2
  215. package/dist/powdb-introspect.js +21 -16
  216. package/dist/powdb-shared.d.ts +36 -5
  217. package/dist/powdb-shared.js +65 -8
  218. package/dist/powdb.d.ts +4 -0
  219. package/dist/powdb.js +183 -51
  220. package/dist/powql.d.ts +394 -51
  221. package/dist/powql.js +1836 -291
  222. package/dist/prisma-compat.d.ts +23 -15
  223. package/dist/prisma-compat.js +508 -373
  224. package/dist/query/aggregates.d.ts +17 -1
  225. package/dist/query/aggregates.js +263 -32
  226. package/dist/query/batched-loader.d.ts +25 -6
  227. package/dist/query/batched-loader.js +101 -21
  228. package/dist/query/builder.d.ts +325 -184
  229. package/dist/query/builder.js +1458 -495
  230. package/dist/query/column-ref.d.ts +61 -0
  231. package/dist/query/column-ref.js +66 -0
  232. package/dist/query/compound-unique.d.ts +21 -0
  233. package/dist/query/compound-unique.js +97 -19
  234. package/dist/query/deferred.d.ts +53 -16
  235. package/dist/query/expr.d.ts +217 -0
  236. package/dist/query/expr.js +848 -0
  237. package/dist/query/filters.d.ts +155 -53
  238. package/dist/query/filters.js +597 -72
  239. package/dist/query/index.d.ts +2 -0
  240. package/dist/query/index.js +4 -0
  241. package/dist/query/json-value.d.ts +43 -0
  242. package/dist/query/json-value.js +54 -0
  243. package/dist/query/normalize-args.d.ts +87 -0
  244. package/dist/query/normalize-args.js +517 -0
  245. package/dist/query/option-surface.d.ts +16 -4
  246. package/dist/query/option-surface.js +56 -13
  247. package/dist/query/relation-names.d.ts +14 -0
  248. package/dist/query/relation-names.js +77 -19
  249. package/dist/query/relations.d.ts +30 -3
  250. package/dist/query/relations.js +220 -67
  251. package/dist/query/row-lock.d.ts +49 -0
  252. package/dist/query/row-lock.js +131 -0
  253. package/dist/query/sql-comment.d.ts +161 -0
  254. package/dist/query/sql-comment.js +374 -0
  255. package/dist/query/stream-idle.d.ts +132 -0
  256. package/dist/query/stream-idle.js +203 -0
  257. package/dist/query/table-client.d.ts +101 -0
  258. package/dist/query/table-client.js +70 -0
  259. package/dist/query/types.d.ts +677 -298
  260. package/dist/query/utils.d.ts +105 -6
  261. package/dist/query/utils.js +414 -18
  262. package/dist/query/warn-registry.d.ts +18 -0
  263. package/dist/query/warn-registry.js +18 -0
  264. package/dist/query/where-compile.d.ts +138 -14
  265. package/dist/query/where-compile.js +508 -31
  266. package/dist/query/where.d.ts +159 -59
  267. package/dist/query/where.js +711 -248
  268. package/dist/query/writes.d.ts +112 -50
  269. package/dist/query/writes.js +1496 -202
  270. package/dist/query-events.d.ts +4 -2
  271. package/dist/query-events.js +3 -1
  272. package/dist/realtime.d.ts +7 -3
  273. package/dist/realtime.js +13 -2
  274. package/dist/schema-builder.d.ts +55 -4
  275. package/dist/schema-builder.js +381 -18
  276. package/dist/schema-metadata.d.ts +21 -1
  277. package/dist/schema-metadata.js +28 -7
  278. package/dist/schema-sql.d.ts +75 -5
  279. package/dist/schema-sql.js +494 -44
  280. package/dist/schema.d.ts +26 -5
  281. package/dist/schema.js +109 -13
  282. package/dist/sql-placeholders.d.ts +56 -0
  283. package/dist/sql-placeholders.js +200 -0
  284. package/dist/sqlite.d.ts +75 -20
  285. package/dist/sqlite.js +597 -99
  286. package/dist/supplied-pool-types.d.ts +48 -0
  287. package/dist/supplied-pool-types.js +247 -0
  288. package/dist/transaction-control.d.ts +51 -0
  289. package/dist/transaction-control.js +311 -0
  290. package/dist/transaction-state.d.ts +342 -0
  291. package/dist/transaction-state.js +696 -0
  292. package/dist/typed-sql.d.ts +37 -3
  293. package/dist/typed-sql.js +100 -8
  294. package/package.json +7 -4
  295. package/skills/turbine-orm/SKILL.md +56 -6
@@ -0,0 +1,53 @@
1
+ /**
2
+ * turbine-orm CLI: shared guards for the local servers
3
+ *
4
+ * A leaf module (Node's crypto and nothing else), the same role as
5
+ * `cli/rate-limit.ts`: Studio and Observe both bind a loopback port behind a
6
+ * random token, and the checks below are the parts of that perimeter that are
7
+ * identical for both, so neither server grows its own copy.
8
+ *
9
+ * - {@link isAllowedHostHeader}: a request whose `Host` names anything but this
10
+ * server is refused. This is what stops DNS rebinding: a page on an attacker's
11
+ * domain that re-resolves to 127.0.0.1 still sends `Host: attacker.example`.
12
+ * - {@link createTicketGate} / {@link redeemTicket}: the URL printed in the
13
+ * terminal carries a ONE-TIME login token, separate from the credential the
14
+ * session then uses, so a URL that lands in browser history, a process
15
+ * listing or a log is a spent ticket and not a standing key.
16
+ * - {@link constantTimeEqual}: the one token comparison.
17
+ */
18
+ /**
19
+ * Compare two secrets without leaking length or content through timing. Both
20
+ * sides are hashed to fixed-length digests first, so `timingSafeEqual` never
21
+ * throws on a length mismatch.
22
+ */
23
+ export declare function constantTimeEqual(a: string, b: string): boolean;
24
+ /**
25
+ * Is this `Host` header one this server should answer to?
26
+ *
27
+ * Allowed: the loopback names (`127.0.0.1`, `localhost`, `[::1]`) and the host
28
+ * the server was bound to, each on the bound port. A server bound to a wildcard
29
+ * address (`0.0.0.0`, `::`) also accepts an IP literal, because it has no single
30
+ * name and a literal address cannot be rebound: rebinding needs a DNS name the
31
+ * attacker controls.
32
+ *
33
+ * An ABSENT header is allowed. HTTP/1.1 requires it and Node answers 400 itself
34
+ * when it is missing, so the only request that reaches this check without one is
35
+ * an HTTP/1.0 client, which is not a browser and can say anything anyway. The
36
+ * attack this refuses needs a browser, and a browser always sends one.
37
+ */
38
+ export declare function isAllowedHostHeader(header: string | string[] | undefined, boundHost: string, port: number): boolean;
39
+ /** The one-time ticket carried by the URL a server prints. */
40
+ export interface TicketGate {
41
+ /** The value in the printed URL's `?token=`. Never a session credential. */
42
+ readonly token: string;
43
+ /** True once a GET carrying the token has been exchanged for a session cookie. */
44
+ used: boolean;
45
+ }
46
+ export type TicketOutcome = 'granted' | 'used' | 'invalid';
47
+ /** Mint a gate. `token` is only passed by a caller that has no separate one to give. */
48
+ export declare function createTicketGate(token?: string): TicketGate;
49
+ /**
50
+ * Try to exchange `candidate` for a session. `granted` happens at most once per
51
+ * gate; afterwards the same token reports `used`, which a server must refuse.
52
+ */
53
+ export declare function redeemTicket(gate: TicketGate, candidate: string): TicketOutcome;
@@ -0,0 +1,119 @@
1
+ "use strict";
2
+ /**
3
+ * turbine-orm CLI: shared guards for the local servers
4
+ *
5
+ * A leaf module (Node's crypto and nothing else), the same role as
6
+ * `cli/rate-limit.ts`: Studio and Observe both bind a loopback port behind a
7
+ * random token, and the checks below are the parts of that perimeter that are
8
+ * identical for both, so neither server grows its own copy.
9
+ *
10
+ * - {@link isAllowedHostHeader}: a request whose `Host` names anything but this
11
+ * server is refused. This is what stops DNS rebinding: a page on an attacker's
12
+ * domain that re-resolves to 127.0.0.1 still sends `Host: attacker.example`.
13
+ * - {@link createTicketGate} / {@link redeemTicket}: the URL printed in the
14
+ * terminal carries a ONE-TIME login token, separate from the credential the
15
+ * session then uses, so a URL that lands in browser history, a process
16
+ * listing or a log is a spent ticket and not a standing key.
17
+ * - {@link constantTimeEqual}: the one token comparison.
18
+ */
19
+ Object.defineProperty(exports, "__esModule", { value: true });
20
+ exports.constantTimeEqual = constantTimeEqual;
21
+ exports.isAllowedHostHeader = isAllowedHostHeader;
22
+ exports.createTicketGate = createTicketGate;
23
+ exports.redeemTicket = redeemTicket;
24
+ const node_crypto_1 = require("node:crypto");
25
+ /**
26
+ * Compare two secrets without leaking length or content through timing. Both
27
+ * sides are hashed to fixed-length digests first, so `timingSafeEqual` never
28
+ * throws on a length mismatch.
29
+ */
30
+ function constantTimeEqual(a, b) {
31
+ const ah = (0, node_crypto_1.createHash)('sha256').update(a).digest();
32
+ const bh = (0, node_crypto_1.createHash)('sha256').update(b).digest();
33
+ return (0, node_crypto_1.timingSafeEqual)(ah, bh);
34
+ }
35
+ // ---------------------------------------------------------------------------
36
+ // Host header allowlist
37
+ // ---------------------------------------------------------------------------
38
+ const LOOPBACK_HOSTNAMES = ['127.0.0.1', 'localhost', '[::1]'];
39
+ const WILDCARD_HOSTNAMES = new Set(['0.0.0.0', '[::]', '::']);
40
+ const IPV4_LITERAL = /^\d{1,3}(?:\.\d{1,3}){3}$/;
41
+ /** `host`, `host:port`, `[v6]` or `[v6]:port`, or null when it is none of those. */
42
+ function parseHostHeader(value) {
43
+ const trimmed = value.trim().toLowerCase();
44
+ if (trimmed === '')
45
+ return null;
46
+ if (trimmed.startsWith('[')) {
47
+ const end = trimmed.indexOf(']');
48
+ if (end < 0)
49
+ return null;
50
+ const rest = trimmed.slice(end + 1);
51
+ if (rest === '')
52
+ return { hostname: trimmed.slice(0, end + 1), port: null };
53
+ if (!/^:\d+$/.test(rest))
54
+ return null;
55
+ return { hostname: trimmed.slice(0, end + 1), port: rest.slice(1) };
56
+ }
57
+ const colon = trimmed.indexOf(':');
58
+ if (colon < 0)
59
+ return { hostname: trimmed, port: null };
60
+ // A bare name never contains a second colon: that would be an unbracketed IPv6.
61
+ if (trimmed.indexOf(':', colon + 1) >= 0)
62
+ return null;
63
+ const port = trimmed.slice(colon + 1);
64
+ if (!/^\d+$/.test(port))
65
+ return null;
66
+ return { hostname: trimmed.slice(0, colon), port };
67
+ }
68
+ function bracketIfIpv6(host) {
69
+ const lower = host.toLowerCase();
70
+ return lower.includes(':') && !lower.startsWith('[') ? `[${lower}]` : lower;
71
+ }
72
+ /**
73
+ * Is this `Host` header one this server should answer to?
74
+ *
75
+ * Allowed: the loopback names (`127.0.0.1`, `localhost`, `[::1]`) and the host
76
+ * the server was bound to, each on the bound port. A server bound to a wildcard
77
+ * address (`0.0.0.0`, `::`) also accepts an IP literal, because it has no single
78
+ * name and a literal address cannot be rebound: rebinding needs a DNS name the
79
+ * attacker controls.
80
+ *
81
+ * An ABSENT header is allowed. HTTP/1.1 requires it and Node answers 400 itself
82
+ * when it is missing, so the only request that reaches this check without one is
83
+ * an HTTP/1.0 client, which is not a browser and can say anything anyway. The
84
+ * attack this refuses needs a browser, and a browser always sends one.
85
+ */
86
+ function isAllowedHostHeader(header, boundHost, port) {
87
+ if (header === undefined)
88
+ return true;
89
+ if (typeof header !== 'string')
90
+ return false;
91
+ const parsed = parseHostHeader(header);
92
+ if (!parsed)
93
+ return false;
94
+ if (parsed.port === null ? port !== 80 : parsed.port !== String(port))
95
+ return false;
96
+ const bound = bracketIfIpv6(boundHost);
97
+ if (parsed.hostname === bound || LOOPBACK_HOSTNAMES.includes(parsed.hostname))
98
+ return true;
99
+ if (WILDCARD_HOSTNAMES.has(boundHost.toLowerCase()) || WILDCARD_HOSTNAMES.has(bound)) {
100
+ return IPV4_LITERAL.test(parsed.hostname) || parsed.hostname.startsWith('[');
101
+ }
102
+ return false;
103
+ }
104
+ /** Mint a gate. `token` is only passed by a caller that has no separate one to give. */
105
+ function createTicketGate(token = (0, node_crypto_1.randomBytes)(24).toString('hex')) {
106
+ return { token, used: false };
107
+ }
108
+ /**
109
+ * Try to exchange `candidate` for a session. `granted` happens at most once per
110
+ * gate; afterwards the same token reports `used`, which a server must refuse.
111
+ */
112
+ function redeemTicket(gate, candidate) {
113
+ if (!constantTimeEqual(candidate, gate.token))
114
+ return 'invalid';
115
+ if (gate.used)
116
+ return 'used';
117
+ gate.used = true;
118
+ return 'granted';
119
+ }
@@ -0,0 +1,41 @@
1
+ /**
2
+ * What the two local servers (Studio and Observe) send OUT of the process: the
3
+ * text of an error response, and the command that opens a browser. A leaf that
4
+ * imports only `node:child_process`, so both servers share one copy.
5
+ *
6
+ * Both exist because the input to them is not entirely ours. A caught value can
7
+ * be anything a dependency threw, and the host in the printed URL comes from a
8
+ * command-line flag.
9
+ */
10
+ import { spawn } from 'node:child_process';
11
+ /**
12
+ * The text a Studio or Observe error response may carry for a caught value.
13
+ *
14
+ * An `Error` contributes its own message, and only that: never `stack`, and
15
+ * never the result of stringifying the object (a `toString` on a foreign error
16
+ * type is free to print one). When the message is empty, which is what Node's
17
+ * `AggregateError` has for a refused connection, the messages of the errors it
18
+ * aggregates or its `cause` are used, then its `code`, then its name. A thrown
19
+ * value that is not an `Error` has nothing safe to say, so it gets a fixed
20
+ * sentence rather than a rendering of whatever it was.
21
+ */
22
+ export declare function responseErrorText(err: unknown): string;
23
+ /** @internal exported for tests. */
24
+ export declare function isLaunchableUrl(url: string): boolean;
25
+ /** @internal the seams the tests replace; production passes neither. */
26
+ export interface BrowserLaunch {
27
+ platform: string;
28
+ spawn: typeof spawn;
29
+ }
30
+ /**
31
+ * Open `url` in the user's browser. Best effort: false when the URL is not one
32
+ * of ours to launch or the launcher could not be started, and the caller has
33
+ * already printed the URL either way.
34
+ *
35
+ * The launcher is started directly with an argument list and no shell
36
+ * (`xdg-open <url>`, `open <url>`, and on Windows `cmd /c start "" <url>`).
37
+ * A launcher that is not installed is reported as an 'error' EVENT rather than
38
+ * a throw, and with no listener Node turns it into an uncaught exception that
39
+ * ends the server right after it printed its address.
40
+ */
41
+ export declare function openInBrowser(url: string, launch?: BrowserLaunch): boolean;
@@ -0,0 +1,103 @@
1
+ "use strict";
2
+ /**
3
+ * What the two local servers (Studio and Observe) send OUT of the process: the
4
+ * text of an error response, and the command that opens a browser. A leaf that
5
+ * imports only `node:child_process`, so both servers share one copy.
6
+ *
7
+ * Both exist because the input to them is not entirely ours. A caught value can
8
+ * be anything a dependency threw, and the host in the printed URL comes from a
9
+ * command-line flag.
10
+ */
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.responseErrorText = responseErrorText;
13
+ exports.isLaunchableUrl = isLaunchableUrl;
14
+ exports.openInBrowser = openInBrowser;
15
+ const node_child_process_1 = require("node:child_process");
16
+ // ---------------------------------------------------------------------------
17
+ // Error text for an HTTP response
18
+ // ---------------------------------------------------------------------------
19
+ const UNEXPECTED_ERROR = 'unexpected error';
20
+ /**
21
+ * The text a Studio or Observe error response may carry for a caught value.
22
+ *
23
+ * An `Error` contributes its own message, and only that: never `stack`, and
24
+ * never the result of stringifying the object (a `toString` on a foreign error
25
+ * type is free to print one). When the message is empty, which is what Node's
26
+ * `AggregateError` has for a refused connection, the messages of the errors it
27
+ * aggregates or its `cause` are used, then its `code`, then its name. A thrown
28
+ * value that is not an `Error` has nothing safe to say, so it gets a fixed
29
+ * sentence rather than a rendering of whatever it was.
30
+ */
31
+ function responseErrorText(err) {
32
+ return err instanceof Error ? errorMessage(err, 0) : UNEXPECTED_ERROR;
33
+ }
34
+ function errorMessage(err, depth) {
35
+ if (err.message)
36
+ return err.message;
37
+ if (depth < 4) {
38
+ const inner = err.errors;
39
+ if (Array.isArray(inner)) {
40
+ const parts = new Set();
41
+ for (const e of inner) {
42
+ const text = e instanceof Error ? errorMessage(e, depth + 1) : '';
43
+ if (text !== '')
44
+ parts.add(text);
45
+ }
46
+ if (parts.size > 0)
47
+ return [...parts].join('; ');
48
+ }
49
+ if (err.cause instanceof Error) {
50
+ const cause = errorMessage(err.cause, depth + 1);
51
+ if (cause !== '')
52
+ return cause;
53
+ }
54
+ }
55
+ const code = err.code;
56
+ if (typeof code === 'string' && code !== '')
57
+ return code;
58
+ return err.name || 'Error';
59
+ }
60
+ // ---------------------------------------------------------------------------
61
+ // Browser launch
62
+ // ---------------------------------------------------------------------------
63
+ /**
64
+ * The only URL shape a server launches: plain `http`, a host name or IP
65
+ * literal, a port, and optionally the one-time `?token=`. The host comes from
66
+ * `--host`, and on Windows the URL is an argument of `cmd /c start`, where `&`,
67
+ * `|`, `^`, `%` and `"` are command syntax rather than data, so anything outside
68
+ * this set is not handed to a launcher at all. The URL is still printed.
69
+ */
70
+ const LAUNCHABLE_URL = /^http:\/\/(?:[A-Za-z0-9._-]+|\[[0-9A-Fa-f:.]+\]):\d{1,5}\/(?:\?token=[A-Za-z0-9_-]+)?$/;
71
+ /** @internal exported for tests. */
72
+ function isLaunchableUrl(url) {
73
+ return LAUNCHABLE_URL.test(url);
74
+ }
75
+ /**
76
+ * Open `url` in the user's browser. Best effort: false when the URL is not one
77
+ * of ours to launch or the launcher could not be started, and the caller has
78
+ * already printed the URL either way.
79
+ *
80
+ * The launcher is started directly with an argument list and no shell
81
+ * (`xdg-open <url>`, `open <url>`, and on Windows `cmd /c start "" <url>`).
82
+ * A launcher that is not installed is reported as an 'error' EVENT rather than
83
+ * a throw, and with no listener Node turns it into an uncaught exception that
84
+ * ends the server right after it printed its address.
85
+ */
86
+ function openInBrowser(url, launch = { platform: process.platform, spawn: node_child_process_1.spawn }) {
87
+ if (!isLaunchableUrl(url))
88
+ return false;
89
+ const [command, args] = launch.platform === 'darwin'
90
+ ? ['open', [url]]
91
+ : launch.platform === 'win32'
92
+ ? ['cmd', ['/c', 'start', '', url]]
93
+ : ['xdg-open', [url]];
94
+ try {
95
+ const child = launch.spawn(command, args, { stdio: 'ignore', detached: true });
96
+ child.on('error', () => { });
97
+ child.unref();
98
+ return true;
99
+ }
100
+ catch {
101
+ return false;
102
+ }
103
+ }
@@ -43,6 +43,14 @@ export interface McpServerHandle {
43
43
  */
44
44
  closed: Promise<McpCloseReason>;
45
45
  }
46
+ /**
47
+ * Split an identifier into lowercase words on separators, case changes
48
+ * (`refreshToken`), acronym edges (`APIKey` -> `api`, `key`) and letter/digit
49
+ * edges (`token2`).
50
+ */
51
+ export declare function secretNameWords(name: string): string[];
52
+ /** True when `name` looks like it holds a credential. See {@link SECRET_WORDS}. */
53
+ export declare function looksLikeSecretName(name: string): boolean;
46
54
  /**
47
55
  * The most bytes the stdio reader will hold WITHOUT seeing a newline.
48
56
  *
@@ -4,6 +4,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.MAX_STDIO_BUFFER_BYTES = void 0;
7
+ exports.secretNameWords = secretNameWords;
8
+ exports.looksLikeSecretName = looksLikeSecretName;
7
9
  exports.startMcpServer = startMcpServer;
8
10
  exports.shortestJoinPaths = shortestJoinPaths;
9
11
  exports.buildRelations = buildRelations;
@@ -61,6 +63,12 @@ function readOwnVersion() {
61
63
  const PROTOCOL_VERSION = '2025-06-18';
62
64
  const STATEMENT_TIMEOUT = '30s';
63
65
  const TRACKING_TABLE = '_turbine_migrations';
66
+ /**
67
+ * A name made safe to quote in a message. Catalog names can carry control or
68
+ * bidi characters, and an error message or stderr line can reach a terminal or
69
+ * a log. Same helper `generate`, `status` and `doctor` print through.
70
+ */
71
+ const shown = introspect_js_1.printableCatalogName;
64
72
  /**
65
73
  * Marker written in place of a hidden cell. Never `null` and never the value,
66
74
  * so the agent can tell "hidden" from "empty" (same string Studio uses).
@@ -88,36 +96,93 @@ const REDACTED = '•• redacted ••';
88
96
  /**
89
97
  * Column names that look like they hold a credential.
90
98
  *
91
- * Anchored to IDENTIFIER SEGMENT boundaries (start, end, or an underscore), not
92
- * a bare substring. Unanchored, `secret` matched `secretary_id` and `token`
93
- * matched a perfectly ordinary `token_count`, so the guard refused legitimate
94
- * queries: a false refusal is not free here, it degrades the tool and trains
95
- * people to route around it.
99
+ * A name is split into WORDS (see {@link secretNameWords}) and tested by word
100
+ * membership, never by substring. Unanchored, `secret` matched `secretary_id`
101
+ * and `token` matched a perfectly ordinary `token_count`, so the guard refused
102
+ * legitimate queries: a false refusal is not free here, it degrades the tool
103
+ * and trains people to route around it. Word splitting reads snake_case,
104
+ * kebab-case, camelCase, PascalCase and SCREAMING_CASE the same way, which the
105
+ * earlier underscore-anchored pattern did not: on a camelCase database (the
106
+ * default naming of a Prisma-created one) it caught `apiKey` only by accident
107
+ * of the whole name being a listed word and returned `refreshToken`,
108
+ * `passwordHash` and `clientSecret` verbatim.
96
109
  *
97
110
  * Deliberately still conservative in the other direction. A column whose name
98
- * genuinely carries a segment like `token` or `secret` is refused even when it
111
+ * genuinely carries a word like `token` or `secret` is refused even when it
99
112
  * holds nothing sensitive, because this gate decides whether raw bytes, or a
100
113
  * row-count oracle over them, reach an LLM context. Renaming the column is a
101
114
  * cheaper fix than the disclosure it prevents.
102
115
  */
103
- const SECRET_WORDS = [
116
+ const SECRET_WORDS = new Set([
104
117
  'passwd',
105
118
  'password',
119
+ 'passphrase',
120
+ 'passcode',
106
121
  'secret',
107
122
  'token',
108
123
  'apikey',
109
- 'api_key',
110
124
  'privatekey',
111
- 'private_key',
125
+ 'accesskey',
112
126
  'credential',
113
- 'credentials',
114
127
  'sessionid',
115
- 'session_id',
116
128
  'otp',
117
129
  'mfa',
118
130
  'totp',
119
- ];
120
- const SECRET_NAME_PATTERN = new RegExp(`(^|_)(${SECRET_WORDS.join('|')})(_|$)`, 'i');
131
+ 'jwt',
132
+ 'salt',
133
+ ]);
134
+ /** Word pairs that are secret only together (`api` + `key`), as one joined string. */
135
+ const SECRET_WORD_PAIRS = new Set(['apikey', 'privatekey', 'accesskey', 'sessionid']);
136
+ /**
137
+ * Suffixes and prefixes for a name that arrives as ONE lowercase word
138
+ * (`refreshtoken`, `passwordhash`), where there is no boundary left to split
139
+ * on. Suffix-only for the short, common words so `secretary` and `tokenizer`
140
+ * stay unflagged; the prefix list is limited to words nothing else starts with.
141
+ */
142
+ const SECRET_SUFFIXES = ['token', 'secret', 'password', 'passwd', 'passphrase', 'apikey', 'privatekey', 'sessionid'];
143
+ const SECRET_PREFIXES = ['password', 'passwd', 'passphrase', 'apikey', 'privatekey', 'accesskey'];
144
+ /**
145
+ * Split an identifier into lowercase words on separators, case changes
146
+ * (`refreshToken`), acronym edges (`APIKey` -> `api`, `key`) and letter/digit
147
+ * edges (`token2`).
148
+ */
149
+ function secretNameWords(name) {
150
+ return name
151
+ .replace(/([a-z\d])([A-Z])/g, '$1 $2')
152
+ .replace(/([A-Z]+)([A-Z][a-z])/g, '$1 $2')
153
+ .replace(/([A-Za-z])(\d)/g, '$1 $2')
154
+ .replace(/(\d)([A-Za-z])/g, '$1 $2')
155
+ .toLowerCase()
156
+ .split(/[^a-z0-9]+/)
157
+ .filter((word) => word !== '');
158
+ }
159
+ function isSecretWord(word) {
160
+ if (SECRET_WORDS.has(word))
161
+ return true;
162
+ // Plural (`tokens`, `api_keys` via the pair join, `credentials`).
163
+ return word.length > 3 && word.endsWith('s') && SECRET_WORDS.has(word.slice(0, -1));
164
+ }
165
+ /** True when `name` looks like it holds a credential. See {@link SECRET_WORDS}. */
166
+ function looksLikeSecretName(name) {
167
+ const words = secretNameWords(name);
168
+ for (let i = 0; i < words.length; i++) {
169
+ const word = words[i];
170
+ if (isSecretWord(word))
171
+ return true;
172
+ const next = words[i + 1];
173
+ if (next !== undefined && SECRET_WORD_PAIRS.has(`${word}${next.replace(/s$/, '')}`))
174
+ return true;
175
+ // One unsplittable lowercase word: only worth a second look when it is long
176
+ // enough to be a compound, and only against the safe suffix/prefix lists.
177
+ if (word.length > 5 && /^[a-z]+$/.test(word)) {
178
+ if (SECRET_SUFFIXES.some((suffix) => word.length > suffix.length && word.endsWith(suffix)))
179
+ return true;
180
+ if (SECRET_PREFIXES.some((prefix) => word.length > prefix.length && word.startsWith(prefix)))
181
+ return true;
182
+ }
183
+ }
184
+ return false;
185
+ }
121
186
  /**
122
187
  * The most bytes the stdio reader will hold WITHOUT seeing a newline.
123
188
  *
@@ -159,7 +224,7 @@ const TOOLS = [
159
224
  },
160
225
  {
161
226
  name: 'explain_query',
162
- description: 'Run EXPLAIN (FORMAT JSON) for a schema-validated findMany query. Pass table + optional where/orderBy/limit/select, free-form SQL is rejected. A where or orderBy on a PII-tagged or secret-named column is refused: the planner row estimate would leak the value. Naming such a column in select is allowed, since EXPLAIN returns no rows.',
227
+ description: 'Run EXPLAIN (FORMAT JSON) for a schema-validated findMany query. Pass table + optional where/orderBy/limit/select, free-form SQL is rejected. A where or orderBy on a PII-tagged or secret-named column is refused: the planner row estimate would leak the value. Naming a PII-tagged column in select is refused, as it is in the library without includePii: UNSAFE, which this server cannot pass. A `lock` option is refused: this server is read-only and a locking read needs a write-capable transaction in your application.',
163
228
  inputSchema: {
164
229
  type: 'object',
165
230
  properties: {
@@ -184,7 +249,7 @@ const TOOLS = [
184
249
  },
185
250
  {
186
251
  name: 'compile_query',
187
- description: 'Compile a Turbine read query to the EXACT SQL it would send, WITHOUT running it: no statement is executed and no row is read, so this is safe to call on production and safe to call in a loop. Use it before you write the query into code. Pass `table`, an `operation` (findMany / findUnique / findFirst / count / aggregate / groupBy, default findMany) and `args`, the same object you would pass to that method. Returns the SQL, the bound parameters, how many STATEMENTS the query costs at execution (a `with` clause can be one join or one follow-up per relation), which relation-load strategy it takes, whether it is bounded by a LIMIT or reads the whole table, the relation depth, and warnings such as a relation whose correlation column has no index. A query that FAILS to compile is a successful answer, not an error: an unknown column (TURBINE_E003), an unknown relation (TURBINE_E005) or the empty-where guard comes back as `ok: false` with the code, the message and how to fix it. READ OPERATIONS ONLY, deliberately: this server has no write surface and compiling one would be the first. A where or orderBy on a PII-tagged or secret-named column is refused, exactly as in explain_query.',
252
+ description: 'Compile a Turbine read query to the EXACT SQL it would send, WITHOUT running it: no statement is executed and no row is read, so this is safe to call on production and safe to call in a loop. Use it before you write the query into code. Pass `table`, an `operation` (findMany / findUnique / findFirst / count / aggregate / groupBy, default findMany) and `args`, the same object you would pass to that method. Returns the SQL, the bound parameters, how many STATEMENTS the query costs at execution (a `with` clause can be one join or one follow-up per relation), which relation-load strategy it takes, whether it is bounded by a LIMIT or reads the whole table, the relation depth, and warnings such as a relation whose correlation column has no index. A query that FAILS to compile is a successful answer, not an error: an unknown column (TURBINE_E003), an unknown relation (TURBINE_E005) or the empty-where guard comes back as `ok: false` with the code, the message and how to fix it. READ OPERATIONS ONLY, deliberately: this server has no write surface and compiling one would be the first. A where or orderBy on a PII-tagged or secret-named column is refused, exactly as in explain_query, and so is a `lock` option (on the query or on any `with` entry): compile the query without it.',
188
253
  inputSchema: {
189
254
  type: 'object',
190
255
  properties: {
@@ -763,6 +828,9 @@ async function explainQuery(ctx, args) {
763
828
  throw jsonRpcError(-32602, 'explain_query no longer accepts free-form SQL; pass table + findMany-style args (where/orderBy/limit/select)');
764
829
  }
765
830
  const tableName = requiredString(args, 'table');
831
+ // Before the allowlist below, which would otherwise drop `lock` unseen and
832
+ // plan an UNLOCKED query for a caller who asked for a locking one.
833
+ assertNoLockingRead(args, 'explain_query');
766
834
  const findManyArgs = parseExplainFindManyArgs(args);
767
835
  return withReadOnly(ctx, async (client) => {
768
836
  const { metadata, piiTags } = await loadSchemaMetadata(client, ctx.options);
@@ -799,7 +867,7 @@ async function explainQuery(ctx, args) {
799
867
  }
800
868
  /**
801
869
  * Refuse an `explain_query` that filters or sorts on a hidden column: one that
802
- * is PII-tagged, or one whose NAME matches {@link SECRET_NAME_PATTERN}.
870
+ * is PII-tagged, or one whose NAME matches {@link looksLikeSecretName}.
803
871
  *
804
872
  * The name half is not a second-best approximation of the tag half, it is the
805
873
  * same rule `sample_rows` applies, applied to the other value-bearing path.
@@ -825,8 +893,10 @@ async function explainQuery(ctx, args) {
825
893
  * What stays here is the part that is genuinely the MCP server's: the two
826
894
  * reasons a column is hidden, and a JSON-RPC refusal.
827
895
  *
828
- * `select` is NOT refused: explain returns no rows, and naming a column reveals
829
- * nothing about its contents.
896
+ * `select` is not judged HERE: explain returns no rows, and naming a column reveals
897
+ * nothing about its contents. The library itself refuses a PII-tagged column in
898
+ * `select` without `includePii: UNSAFE`, which no JSON-RPC caller can supply, so
899
+ * the builder's own E003 reaches the caller as an invalid-params error.
830
900
  *
831
901
  * FAILS CLOSED when the tag scan failed: with no trustworthy tag list, no
832
902
  * column can be shown to be safe, so every where/orderBy is refused rather than
@@ -849,7 +919,7 @@ async function explainQuery(ctx, args) {
849
919
  function hiddenColumnReason(owner, column) {
850
920
  if (owner.columns.some((col) => col.name === column && col.pii === true))
851
921
  return 'is PII-tagged';
852
- if (SECRET_NAME_PATTERN.test(column))
922
+ if (looksLikeSecretName(column))
853
923
  return 'has a secret-looking name';
854
924
  return null;
855
925
  }
@@ -864,7 +934,7 @@ function assertNoPiiPredicates(args, table, metadata, piiTags) {
864
934
  metadata,
865
935
  hiddenReason: hiddenColumnReason,
866
936
  refuseColumn: (owner, column, why) => {
867
- throw jsonRpcError(-32602, `Column "${column}" on "${owner.name}" ${why}, so it cannot be used in a where or orderBy here: ` +
937
+ throw jsonRpcError(-32602, `Column "${shown(column)}" on "${shown(owner.name)}" ${why}, so it cannot be used in a where or orderBy here: ` +
868
938
  `EXPLAIN reports the planner's row estimate, and the estimate for a predicate on a hidden value ` +
869
939
  `reveals that value one character at a time. Filter on a visible column instead.`);
870
940
  },
@@ -873,7 +943,7 @@ function assertNoPiiPredicates(args, table, metadata, piiTags) {
873
943
  `prove it does not filter or sort on a tagged column, so it is refused. Flatten the query.`);
874
944
  },
875
945
  refuseShape: (owner, key) => {
876
- throw jsonRpcError(-32602, `The PII guard does not recognize "${key}" in a query on "${owner.name}", so it cannot prove the ` +
946
+ throw jsonRpcError(-32602, `The PII guard does not recognize "${shown(key)}" in a query on "${shown(owner.name)}", so it cannot prove the ` +
877
947
  `query does not filter or sort on a hidden column, and refuses it rather than guessing. ` +
878
948
  `Remove it and explain the query without it.`);
879
949
  },
@@ -919,6 +989,10 @@ async function compileQuery(ctx, args) {
919
989
  const tableName = requiredString(args, 'table');
920
990
  const operation = parseCompileOperation(args.operation);
921
991
  const queryArgs = parseCompileArgs(args.args);
992
+ // Whatever the operation: count / aggregate / groupBy take no `lock`, and
993
+ // compiling them would drop it unseen, so the SQL shown would not be the
994
+ // query the caller wrote. Refused before the catalog read.
995
+ assertNoLockingRead(queryArgs, 'compile_query');
922
996
  const { metadata, piiTags } = await withReadOnly(ctx, (client) => loadSchemaMetadata(client, ctx.options));
923
997
  const table = requireTable(metadata, tableName);
924
998
  assertCompileHidesNothing(queryArgs, operation, table, metadata, piiTags);
@@ -996,7 +1070,7 @@ function assertCompileHidesNothing(args, operation, table, metadata, piiTags) {
996
1070
  metadata,
997
1071
  hiddenReason: hiddenColumnReason,
998
1072
  refuseColumn: (owner, column, why) => {
999
- throw jsonRpcError(-32602, `Column "${column}" on "${owner.name}" ${why}, so compile_query will not build a statement that filters, ` +
1073
+ throw jsonRpcError(-32602, `Column "${shown(column)}" on "${shown(owner.name)}" ${why}, so compile_query will not build a statement that filters, ` +
1000
1074
  `sorts, pages or groups on it. This server does not author queries that interrogate a hidden value, ` +
1001
1075
  `wherever that statement would eventually run. Use a visible column.`);
1002
1076
  },
@@ -1005,7 +1079,7 @@ function assertCompileHidesNothing(args, operation, table, metadata, piiTags) {
1005
1079
  `not name a hidden column, so it is refused. Flatten the query.`);
1006
1080
  },
1007
1081
  refuseShape: (owner, key) => {
1008
- throw jsonRpcError(-32602, `The PII guard does not recognize "${key}" in a query on "${owner.name}", so it cannot prove the query ` +
1082
+ throw jsonRpcError(-32602, `The PII guard does not recognize "${shown(key)}" in a query on "${shown(owner.name)}", so it cannot prove the query ` +
1009
1083
  `does not name a hidden column, and refuses it rather than guessing. Remove it and compile without it.`);
1010
1084
  },
1011
1085
  });
@@ -1025,11 +1099,26 @@ function assertCompileHidesNothing(args, operation, table, metadata, piiTags) {
1025
1099
  const why = hiddenColumnReason(table, column);
1026
1100
  if (!why)
1027
1101
  continue;
1028
- throw jsonRpcError(-32602, `Column "${column}" on "${table.name}" ${why}, so it cannot be used as a group key, an aggregate target, a ` +
1102
+ throw jsonRpcError(-32602, `Column "${shown(column)}" on "${shown(table.name)}" ${why}, so it cannot be used as a group key, an aggregate target, a ` +
1029
1103
  `HAVING term or an ordering in compile_query. _count over the table is unaffected; group and aggregate on ` +
1030
1104
  `a visible column instead.`);
1031
1105
  }
1032
1106
  }
1107
+ /**
1108
+ * Refuse a `lock` option, at the top level or on any `with` entry, by name.
1109
+ *
1110
+ * Both tools that take query args are read-only, and a locking read takes row
1111
+ * locks, which needs a write-capable transaction in the application. The search
1112
+ * and the wording are the shared ones in `cli/pii-predicate-guard.ts`, the same
1113
+ * pair Studio's Query tab uses, so the rule is stated once. It runs on the RAW
1114
+ * args, ahead of every parser and guard that might drop or reshape the key.
1115
+ */
1116
+ function assertNoLockingRead(args, tool) {
1117
+ const locked = (0, pii_predicate_guard_js_1.findRowLockOption)(args);
1118
+ if (locked === null)
1119
+ return;
1120
+ throw jsonRpcError(-32602, (0, pii_predicate_guard_js_1.rowLockRefusalMessage)(shown(locked), tool));
1121
+ }
1033
1122
  /**
1034
1123
  * Extract the allowed findMany subset for explain_query (no `with` / raw SQL).
1035
1124
  * Returns a plain object cast at the buildFindMany call site, same pattern as Studio.
@@ -1084,7 +1173,7 @@ function classifyHiddenColumns(table, piiTags) {
1084
1173
  ? 'tags-unreadable'
1085
1174
  : col.pii === true
1086
1175
  ? 'pii'
1087
- : SECRET_NAME_PATTERN.test(col.name)
1176
+ : looksLikeSecretName(col.name)
1088
1177
  ? 'secret-name'
1089
1178
  : null;
1090
1179
  if (reason) {
@@ -1479,7 +1568,7 @@ async function findJoinPath(ctx, args) {
1479
1568
  paths,
1480
1569
  pathsTruncated: false,
1481
1570
  notes: [
1482
- `"${fromTable.name}" is both ends of this query, so no join is needed to read its own columns.`,
1571
+ `"${shown(fromTable.name)}" is both ends of this query, so no join is needed to read its own columns.`,
1483
1572
  paths.length > 0
1484
1573
  ? 'The paths below are its SELF-relations: a relation whose target is the same table, which is the only way to join it to itself.'
1485
1574
  : 'It declares no self-relation, so there is nothing to join it to itself through.',
@@ -1501,12 +1590,12 @@ async function findJoinPath(ctx, args) {
1501
1590
  pathsTruncated: false,
1502
1591
  searchExhausted: exhausted,
1503
1592
  reason: exhausted
1504
- ? `The search for a chain from "${fromTable.name}" to "${toTable.name}" hit this tool's node budget before ` +
1593
+ ? `The search for a chain from "${shown(fromTable.name)}" to "${shown(toTable.name)}" hit this tool's node budget before ` +
1505
1594
  `it finished, so this is NOT an answer that they are unconnected. Lower maxDepth (it is ${maxDepth}) to ` +
1506
- `bound the search, or call relation_graph on "${fromTable.name}" and walk it a hop at a time.`
1507
- : `No relation chain connects "${fromTable.name}" to "${toTable.name}" within ${maxDepth} hop(s). Either the ` +
1595
+ `bound the search, or call relation_graph on "${shown(fromTable.name)}" and walk it a hop at a time.`
1596
+ : `No relation chain connects "${shown(fromTable.name)}" to "${shown(toTable.name)}" within ${maxDepth} hop(s). Either the ` +
1508
1597
  `schema declares no foreign key path between them, or the path is longer than the search depth. Raise ` +
1509
- `maxDepth, or call relation_graph on "${fromTable.name}" to see what it does reach.`,
1598
+ `maxDepth, or call relation_graph on "${shown(fromTable.name)}" to see what it does reach.`,
1510
1599
  notes: exhausted
1511
1600
  ? ['The search did not complete. Do not report these tables as unconnected on the strength of this reply.']
1512
1601
  : [
@@ -2026,8 +2115,8 @@ function estimatedRowCount(stats) {
2026
2115
  function requireTable(metadata, tableName) {
2027
2116
  const table = (0, utils_js_1.ownLookup)(metadata.tables, tableName);
2028
2117
  if (!table) {
2029
- const available = Object.keys(metadata.tables).join(', ') || '(none)';
2030
- throw jsonRpcError(-32602, `Unknown table "${tableName}". Available: ${available}`);
2118
+ const available = Object.keys(metadata.tables).map(shown).join(', ') || '(none)';
2119
+ throw jsonRpcError(-32602, `Unknown table "${shown(tableName)}". Available: ${available}`);
2031
2120
  }
2032
2121
  return table;
2033
2122
  }