okengine 0.2.8 → 0.3.2

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 (180) hide show
  1. package/AGENTS.md +17 -15
  2. package/README.md +57 -29
  3. package/package.json +11 -16
  4. package/site/content/docs/ai/llms-txt.mdx +54 -0
  5. package/site/content/docs/ai/mcp.mdx +123 -0
  6. package/site/content/docs/ai/meta.json +5 -0
  7. package/site/content/docs/ai/skills.mdx +53 -0
  8. package/site/content/docs/console/access.mdx +29 -0
  9. package/site/content/docs/console/ai.mdx +35 -0
  10. package/site/content/docs/console/architecture.mdx +35 -0
  11. package/site/content/docs/console/channels.mdx +37 -0
  12. package/site/content/docs/console/clock.mdx +31 -0
  13. package/site/content/docs/console/flows.mdx +31 -0
  14. package/site/content/docs/console/gates.mdx +35 -0
  15. package/site/content/docs/console/manifest-diff.mdx +34 -0
  16. package/site/content/docs/console/meta.json +23 -0
  17. package/site/content/docs/console/overview.mdx +40 -0
  18. package/site/content/docs/console/plugins.mdx +41 -0
  19. package/site/content/docs/console/privacy.mdx +32 -0
  20. package/site/content/docs/console/runs.mdx +40 -0
  21. package/site/content/docs/console/signals.mdx +31 -0
  22. package/site/content/docs/console/store.mdx +32 -0
  23. package/site/content/docs/console/tenancy.mdx +32 -0
  24. package/site/content/docs/console/traces.mdx +34 -0
  25. package/site/content/docs/console/vault.mdx +37 -0
  26. package/site/content/docs/elements/ai.mdx +180 -0
  27. package/site/content/docs/elements/channel.mdx +167 -0
  28. package/site/content/docs/elements/clock.mdx +182 -0
  29. package/site/content/docs/elements/flow.mdx +288 -0
  30. package/site/content/docs/elements/gate.mdx +171 -0
  31. package/site/content/docs/elements/meta.json +5 -0
  32. package/site/content/docs/elements/signal.mdx +171 -0
  33. package/site/content/docs/elements/store.mdx +320 -0
  34. package/site/content/docs/elements/vault.mdx +263 -0
  35. package/site/content/docs/get-started/basic-usage.mdx +124 -0
  36. package/site/content/docs/get-started/comparison.mdx +65 -0
  37. package/site/content/docs/get-started/installation.mdx +113 -0
  38. package/site/content/docs/get-started/introduction.mdx +123 -0
  39. package/site/content/docs/get-started/meta.json +5 -0
  40. package/site/content/docs/index.mdx +63 -0
  41. package/site/content/docs/meta.json +5 -0
  42. package/site/content/docs/plugins/compression.mdx +60 -0
  43. package/site/content/docs/plugins/cors.mdx +92 -0
  44. package/site/content/docs/plugins/csrf.mdx +96 -0
  45. package/site/content/docs/plugins/ip-allowlist.mdx +92 -0
  46. package/site/content/docs/plugins/maintenance-mode.mdx +101 -0
  47. package/site/content/docs/plugins/meta.json +15 -0
  48. package/site/content/docs/plugins/security-headers.mdx +136 -0
  49. package/site/content/docs/reference/cli.md +101 -0
  50. package/site/content/docs/reference/configuration.mdx +159 -0
  51. package/site/content/docs/reference/environment-variables.mdx +87 -0
  52. package/site/content/docs/reference/errors.mdx +80 -0
  53. package/site/content/docs/reference/fx.mdx +117 -0
  54. package/site/content/docs/reference/meta.json +5 -0
  55. package/site/content/docs/reference/plugins.mdx +249 -0
  56. package/site/content/docs/reference/security.md +63 -0
  57. package/src/auth/auth.test.ts +3 -0
  58. package/src/cli/ask-dev-mode.ts +1 -1
  59. package/src/cli/db.ts +87 -17
  60. package/src/cli/dev-db-push.test.ts +32 -2
  61. package/src/cli/dev-schema-sync.test.ts +66 -0
  62. package/src/cli/dev-schema-sync.ts +139 -0
  63. package/src/cli/dev.test.ts +123 -1
  64. package/src/cli/dev.ts +150 -8
  65. package/src/cli/doc-staleness.test.ts +4 -4
  66. package/src/cli/docker-cli.test.ts +20 -0
  67. package/src/cli/docker.ts +10 -0
  68. package/src/cli/drizzle-env.test.ts +67 -0
  69. package/src/cli/drizzle-env.ts +78 -0
  70. package/src/cli/ensure-drizzle-config.ts +50 -0
  71. package/src/cli/hero-meta.test.ts +1 -1
  72. package/src/cli/load-config.ts +6 -0
  73. package/src/cli/mode.ts +24 -4
  74. package/src/cli/openbao-bootstrap.test.ts +147 -0
  75. package/src/cli/openbao-bootstrap.ts +280 -0
  76. package/src/cli/openbao-restart.integration.test.ts +136 -0
  77. package/src/cli/ports.test.ts +7 -5
  78. package/src/cli/ports.ts +6 -2
  79. package/src/cli/resolve-dev-sql-env.test.ts +48 -0
  80. package/src/cli/resolve-dev-sql-env.ts +42 -0
  81. package/src/cli/stack.ts +7 -4
  82. package/src/cli/vault-cmd.ts +63 -0
  83. package/src/client/types.ts +7 -1
  84. package/src/compiler/extract.test.ts +40 -0
  85. package/src/compiler/extract.ts +123 -1
  86. package/src/compiler/fixtures/skyport/oke.config.ts +2 -2
  87. package/src/compiler/fixtures/skyport.expected.json +1 -1
  88. package/src/compiler/response.ts +12 -0
  89. package/src/config/define-config.test.ts +4 -6
  90. package/src/config/index.ts +2 -15
  91. package/src/console/server/app.ts +2 -0
  92. package/src/console/server/vault.ts +21 -6
  93. package/src/docker/compose.ts +112 -16
  94. package/src/docker/derive.ts +7 -1
  95. package/src/docker/docker.test.ts +103 -0
  96. package/src/docker/index.ts +11 -1
  97. package/src/docker/recipes/index.ts +3 -2
  98. package/src/docker/recipes/openbao.ts +47 -0
  99. package/src/docker/recipes/redis.ts +5 -1
  100. package/src/docker/recipes/rustfs.ts +2 -3
  101. package/src/docker/stack-id.test.ts +43 -8
  102. package/src/docker/stack-id.ts +99 -20
  103. package/src/docker/stack.ts +36 -4
  104. package/src/docker/types.ts +3 -0
  105. package/src/docs-origin.ts +4 -4
  106. package/src/drivers/drizzle-dialect.test.ts +20 -0
  107. package/src/drivers/drizzle-dialect.ts +37 -0
  108. package/src/drivers/index.ts +1 -2
  109. package/src/drivers/memory.ts +278 -39
  110. package/src/drivers/s3.ts +10 -1
  111. package/src/drivers/vault-driver-removal.test.ts +55 -0
  112. package/src/drivers/vault-openbao.test.ts +97 -0
  113. package/src/drivers/vault-openbao.ts +102 -35
  114. package/src/drivers/vault-types.ts +3 -10
  115. package/src/elements/store/declare.ts +4 -1
  116. package/src/elements/store/resource-list-docs.fixture.ts +56 -0
  117. package/src/elements/store/resource-list-docs.test.ts +79 -0
  118. package/src/elements/store/resource.test.ts +253 -0
  119. package/src/elements/store/resource.ts +786 -0
  120. package/src/elements/store/sql-condition.test.ts +132 -0
  121. package/src/elements/store/sql-condition.ts +284 -46
  122. package/src/elements/store/sql-session.test.ts +86 -1
  123. package/src/elements/store/sql-session.ts +187 -27
  124. package/src/elements/store/table.ts +34 -4
  125. package/src/elements/store.ts +16 -0
  126. package/src/elements/vault/runtime.ts +1 -1
  127. package/src/elements/vault.test.ts +1 -28
  128. package/src/elements/vault.ts +1 -1
  129. package/src/kernel/app.ts +59 -25
  130. package/src/kernel/boot-bind/channel.test.ts +60 -0
  131. package/src/kernel/boot-bind/channel.ts +64 -2
  132. package/src/kernel/boot-bind/store.test.ts +10 -1
  133. package/src/kernel/boot-bind/store.ts +49 -2
  134. package/src/kernel/boot.test.ts +0 -1
  135. package/src/kernel/boot.ts +1 -1
  136. package/src/kernel/edge.test.ts +68 -0
  137. package/src/kernel/errors.registry.test.ts +1 -1
  138. package/src/kernel/flow.ts +8 -0
  139. package/src/kernel/fx.test.ts +23 -3
  140. package/src/kernel/fx.ts +115 -18
  141. package/src/kernel/hooks.test.ts +33 -0
  142. package/src/kernel/hooks.ts +22 -0
  143. package/src/kernel/index.ts +7 -0
  144. package/src/kernel/on.ts +44 -3
  145. package/src/kernel/plugin.ts +33 -3
  146. package/src/kernel/registry-isolation.test.ts +74 -0
  147. package/src/kernel/registry.ts +22 -1
  148. package/src/kernel/triggers.ts +59 -0
  149. package/src/manifest/fixtures/skyport.excerpt.json +1 -1
  150. package/src/manifest/fixtures/skyport.manifest.json +1 -1
  151. package/src/manifest/index.ts +1 -1
  152. package/src/manifest/types.ts +1 -1
  153. package/src/manifest/validate.ts +2 -2
  154. package/src/plugins/compression.test.ts +127 -0
  155. package/src/plugins/compression.ts +94 -0
  156. package/src/plugins/config-source.test.ts +204 -0
  157. package/src/plugins/config-source.ts +209 -0
  158. package/src/plugins/cors.test.ts +138 -0
  159. package/src/plugins/cors.ts +129 -0
  160. package/src/plugins/csrf.test.ts +102 -0
  161. package/src/plugins/csrf.ts +86 -0
  162. package/src/plugins/headers.ts +54 -0
  163. package/src/plugins/index.ts +26 -0
  164. package/src/plugins/ip-allowlist.test.ts +105 -0
  165. package/src/plugins/ip-allowlist.ts +76 -0
  166. package/src/plugins/maintenance-mode.test.ts +91 -0
  167. package/src/plugins/maintenance-mode.ts +85 -0
  168. package/src/plugins/security-headers.test.ts +243 -0
  169. package/src/plugins/security-headers.ts +255 -0
  170. package/src/release/measure.ts +1 -2
  171. package/src/test/create-test-app.ts +14 -2
  172. package/docs/spec/console.md +0 -762
  173. package/docs/spec/example.md +0 -1374
  174. package/docs/spec/four-applications.md +0 -1376
  175. package/docs/spec/unified-theory.md +0 -498
  176. package/src/cli/doc-drift.test.ts +0 -147
  177. package/src/cli/doc-drift.ts +0 -401
  178. package/src/cli/doctor-diff-examples.ts +0 -90
  179. package/src/drivers/vault-sops.ts +0 -246
  180. /package/{spec/manifest.v1.schema.json → manifest.v1.schema.json} +0 -0
@@ -45,28 +45,42 @@ function createMemorySqlConnection(role: "primary" | "replica"): SqlConnection {
45
45
  async query(sql, params = []) {
46
46
  const text = sql.trim();
47
47
 
48
+ const countStar =
49
+ /^SELECT\s+COUNT\(\*\)\s+AS\s+"?[a-zA-Z_][a-zA-Z0-9_]*"?\s+FROM\s+("?[a-zA-Z_][a-zA-Z0-9_]*"?)\s*(?:WHERE\s+(.+?))?\s*$/i.exec(
50
+ text,
51
+ );
52
+ if (countStar) {
53
+ const table = getTable(parseIdent(countStar[1]!));
54
+ const whereClause = countStar[2]?.trim();
55
+ let rows = table.rows;
56
+ if (whereClause) {
57
+ const ast = parseWhere(whereClause);
58
+ rows = rows.filter((r) => evalWhere(ast, r, params, { i: 0 }));
59
+ }
60
+ return [{ count: rows.length }];
61
+ }
62
+
48
63
  const selectGeneric =
49
- /^SELECT\s+(.+?)\s+FROM\s+("?[a-zA-Z_][a-zA-Z0-9_]*"?)\s*(?:WHERE\s+(.+?))?(?:\s+ORDER\s+BY\s+("?[a-zA-Z_][a-zA-Z0-9_]*"?))?(?:\s+LIMIT\s+(\d+))?\s*$/i.exec(
64
+ /^SELECT\s+(.+?)\s+FROM\s+("?[a-zA-Z_][a-zA-Z0-9_]*"?)\s*(?:WHERE\s+(.+?))?(?:\s+ORDER\s+BY\s+(.+?))?(?:\s+LIMIT\s+(\d+))?(?:\s+OFFSET\s+(\d+))?\s*$/i.exec(
50
65
  text,
51
66
  );
52
67
  if (selectGeneric && !/^\s*1\s+AS\s+/i.test(selectGeneric[1]!)) {
53
68
  const table = getTable(parseIdent(selectGeneric[2]!));
54
69
  const selectList = selectGeneric[1]!.trim();
55
70
  const whereClause = selectGeneric[3]?.trim();
56
- const orderCol = selectGeneric[4] ? parseIdent(selectGeneric[4]) : undefined;
71
+ const orderClause = selectGeneric[4]?.trim();
57
72
  const limit = selectGeneric[5] ? Number(selectGeneric[5]) : undefined;
73
+ const offset = selectGeneric[6] ? Number(selectGeneric[6]) : undefined;
58
74
  let rows = table.rows.map((r) => ({ ...r }));
59
75
  if (whereClause) {
60
- const preds = parseWherePredicates(whereClause);
61
- rows = rows.filter((r) =>
62
- preds.every((p, i) => compareRow(r[p.column], params[i], p.op)),
63
- );
76
+ const ast = parseWhere(whereClause);
77
+ rows = rows.filter((r) => evalWhere(ast, r, params, { i: 0 }));
64
78
  }
65
- if (orderCol) {
66
- rows = [...rows].sort((a, b) =>
67
- String(a[orderCol] ?? "").localeCompare(String(b[orderCol] ?? "")),
68
- );
79
+ if (orderClause) {
80
+ const terms = parseOrderTerms(orderClause);
81
+ rows = [...rows].sort((a, b) => compareByTerms(a, b, terms));
69
82
  }
83
+ if (offset !== undefined) rows = rows.slice(offset);
70
84
  if (limit !== undefined) rows = rows.slice(0, limit);
71
85
  if (selectList === "*") return rows;
72
86
  const cols = selectList.split(",").map((part) => {
@@ -224,10 +238,8 @@ function createMemorySqlConnection(role: "primary" | "replica"): SqlConnection {
224
238
  if (!m) throw new Error(`memory sql: unsupported SET: ${part}`);
225
239
  return parseIdent(m[1]!);
226
240
  });
227
- const preds = parseWherePredicates(updateSet[3]!);
228
- const row = table.rows.find((r) =>
229
- preds.every((p, i) => compareRow(r[p.column], params[setCols.length + i], p.op)),
230
- );
241
+ const ast = parseWhere(updateSet[3]!);
242
+ const row = table.rows.find((r) => evalWhere(ast, r, params, { i: setCols.length }));
231
243
  if (!row) return { changes: 0 };
232
244
  setCols.forEach((col, i) => {
233
245
  row[col] = params[i];
@@ -243,52 +255,279 @@ function createMemorySqlConnection(role: "primary" | "replica"): SqlConnection {
243
255
  };
244
256
  }
245
257
 
246
- /** Parse `col = ? AND col2 = ?` into ordered column names. */
258
+ /** One comparison operator for the memory SQL driver. */
259
+ type WhereOp =
260
+ | "="
261
+ | "<"
262
+ | ">"
263
+ | "<="
264
+ | ">="
265
+ | "!="
266
+ | "like"
267
+ | "ilike"
268
+ | "in"
269
+ | "is null"
270
+ | "is not null";
271
+
272
+ /** WHERE condition AST node — leaf comparison or AND/OR group. */
273
+ type WhereNode =
274
+ | { readonly kind: "cmp"; readonly column: string; readonly op: WhereOp; readonly arity: number }
275
+ | { readonly kind: "and"; readonly children: readonly WhereNode[] }
276
+ | { readonly kind: "or"; readonly children: readonly WhereNode[] };
277
+
278
+ /** One `ORDER BY` term. */
279
+ interface OrderTerm {
280
+ readonly column: string;
281
+ readonly direction: "ASC" | "DESC";
282
+ }
283
+
284
+ /**
285
+ * Parse a WHERE clause into an AST. Supports parenthesized groups, `AND` /
286
+ * `OR`, binary comparisons, and `like` / `ilike` — the shapes the store
287
+ * session compiler emits. Anything else throws.
288
+ *
289
+ * @param clause - WHERE text (without the `WHERE` keyword)
290
+ */
291
+ function parseWhere(clause: string): WhereNode {
292
+ const tokens = tokenizeWhere(clause);
293
+ const unsupported = (): Error => new Error(`memory sql: unsupported WHERE: ${clause}`);
294
+ let pos = 0;
295
+
296
+ const peek = (): string | undefined => tokens[pos];
297
+ const parseLevel = (keyword: "AND" | "OR", down: () => WhereNode): WhereNode => {
298
+ const first = down();
299
+ if (peek()?.toUpperCase() !== keyword) return first;
300
+ const rest: WhereNode[] = [];
301
+ do {
302
+ pos++;
303
+ rest.push(down());
304
+ } while (peek()?.toUpperCase() === keyword);
305
+ const children = [first, ...rest];
306
+ return keyword === "AND" ? { kind: "and", children } : { kind: "or", children };
307
+ };
308
+ const parseFactor = (): WhereNode => {
309
+ if (peek() === "(") {
310
+ pos++;
311
+ const node = parseLevel("OR", () => parseLevel("AND", parseFactor));
312
+ if (peek() !== ")") throw unsupported();
313
+ pos++;
314
+ return node;
315
+ }
316
+ const colTok = tokens[pos++];
317
+ const opTok = tokens[pos++];
318
+ if (colTok === undefined || opTok === undefined) throw unsupported();
319
+ if (opTok.toLowerCase() === "is") {
320
+ const second = tokens[pos++]?.toLowerCase();
321
+ const nullOp =
322
+ second === "null"
323
+ ? "is null"
324
+ : second === "not" && tokens[pos++]?.toLowerCase() === "null"
325
+ ? "is not null"
326
+ : undefined;
327
+ if (nullOp === undefined) throw unsupported();
328
+ return { kind: "cmp", column: colTok.replaceAll('"', "").trim(), op: nullOp, arity: 0 };
329
+ }
330
+ if (opTok.toLowerCase() === "in") {
331
+ if (peek() !== "(") throw unsupported();
332
+ pos++;
333
+ let arity = 0;
334
+ while (peek() === "?") {
335
+ arity++;
336
+ pos++;
337
+ if (peek() === ",") pos++;
338
+ }
339
+ if (arity === 0 || peek() !== ")") throw unsupported();
340
+ pos++;
341
+ return { kind: "cmp", column: colTok.replaceAll('"', "").trim(), op: "in", arity };
342
+ }
343
+ const paramTok = tokens[pos++];
344
+ if (paramTok !== "?") throw unsupported();
345
+ const op = asWhereOp(opTok);
346
+ if (op === undefined) throw unsupported();
347
+ return { kind: "cmp", column: colTok.replaceAll('"', "").trim(), op, arity: 1 };
348
+ };
349
+
350
+ const node = parseLevel("OR", () => parseLevel("AND", parseFactor));
351
+ if (pos !== tokens.length) throw unsupported();
352
+ return node;
353
+ }
354
+
355
+ function tokenizeWhere(clause: string): string[] {
356
+ const out: string[] = [];
357
+ let i = 0;
358
+ while (i < clause.length) {
359
+ const ch = clause[i]!;
360
+ if (/\s/.test(ch)) {
361
+ i++;
362
+ continue;
363
+ }
364
+ if (ch === "(" || ch === ")" || ch === "?" || ch === ",") {
365
+ out.push(ch);
366
+ i++;
367
+ continue;
368
+ }
369
+ if (ch === '"') {
370
+ const end = clause.indexOf('"', i + 1);
371
+ if (end === -1) throw new Error(`memory sql: unsupported WHERE: ${clause}`);
372
+ out.push(clause.slice(i, end + 1));
373
+ i = end + 1;
374
+ continue;
375
+ }
376
+ const two = clause.slice(i, i + 2);
377
+ if (two === "<=" || two === ">=" || two === "<>" || two === "!=") {
378
+ out.push(two);
379
+ i += 2;
380
+ continue;
381
+ }
382
+ if (ch === "=" || ch === "<" || ch === ">") {
383
+ out.push(ch);
384
+ i++;
385
+ continue;
386
+ }
387
+ const word = /^[a-zA-Z_][a-zA-Z0-9_]*/.exec(clause.slice(i));
388
+ if (word) {
389
+ out.push(word[0]);
390
+ i += word[0].length;
391
+ continue;
392
+ }
393
+ throw new Error(`memory sql: unsupported WHERE: ${clause}`);
394
+ }
395
+ return out;
396
+ }
397
+
398
+ function asWhereOp(token: string): WhereOp | undefined {
399
+ switch (token.toLowerCase()) {
400
+ case "=":
401
+ return "=";
402
+ case "<":
403
+ return "<";
404
+ case ">":
405
+ return ">";
406
+ case "<=":
407
+ return "<=";
408
+ case ">=":
409
+ return ">=";
410
+ case "!=":
411
+ case "<>":
412
+ return "!=";
413
+ case "like":
414
+ return "like";
415
+ case "ilike":
416
+ return "ilike";
417
+ default:
418
+ return undefined;
419
+ }
420
+ }
421
+
422
+ /** Evaluate a WHERE AST against a row, consuming `?` params in order. */
423
+ function evalWhere(
424
+ node: WhereNode,
425
+ row: SqlRow,
426
+ params: readonly unknown[],
427
+ state: { i: number },
428
+ ): boolean {
429
+ if (node.kind === "and") {
430
+ return node.children.every((c) => evalWhere(c, row, params, state));
431
+ }
432
+ if (node.kind === "or") {
433
+ return node.children.some((c) => evalWhere(c, row, params, state));
434
+ }
435
+ if (node.op === "is null") {
436
+ return row[node.column] === null || row[node.column] === undefined;
437
+ }
438
+ if (node.op === "is not null") {
439
+ return row[node.column] !== null && row[node.column] !== undefined;
440
+ }
441
+ if (node.op === "in") {
442
+ const wants = params.slice(state.i, state.i + node.arity);
443
+ state.i += node.arity;
444
+ return wants.some((w) => compareRow(row[node.column], w, "="));
445
+ }
446
+ const want = params[state.i++];
447
+ return compareRow(row[node.column], want, node.op);
448
+ }
449
+
450
+ /** Parse `col = ? AND col2 = ?` into ordered column names (equality-only). */
247
451
  function parseEqualityWhere(clause: string): string[] {
248
- return parseWherePredicates(clause).map((p) => {
249
- if (p.op !== "=") {
452
+ const node = parseWhere(clause);
453
+ const cols: string[] = [];
454
+ const walk = (n: WhereNode): void => {
455
+ if (n.kind === "and") {
456
+ n.children.forEach(walk);
457
+ return;
458
+ }
459
+ if (n.kind === "or" || n.op !== "=") {
250
460
  throw new Error(`memory sql: unsupported WHERE: ${clause}`);
251
461
  }
252
- return p.column;
253
- });
462
+ cols.push(n.column);
463
+ };
464
+ walk(node);
465
+ return cols;
254
466
  }
255
467
 
256
- /** One WHERE predicate for the memory SQL driver. */
257
- interface WherePred {
258
- readonly column: string;
259
- readonly op: "=" | "<" | ">" | "<=" | ">=" | "!=";
468
+ /** Parse `"a" DESC, "b"` into ordered terms (default ASC). */
469
+ function parseOrderTerms(clause: string): OrderTerm[] {
470
+ return clause.split(",").map((part) => {
471
+ const m = /^\s*("?[a-zA-Z_][a-zA-Z0-9_]*"?)(?:\s+(ASC|DESC))?\s*$/i.exec(part);
472
+ if (!m) throw new Error(`memory sql: unsupported ORDER BY: ${clause}`);
473
+ return {
474
+ column: m[1]!.replaceAll('"', "").trim(),
475
+ direction: (m[2]?.toUpperCase() ?? "ASC") as OrderTerm["direction"],
476
+ };
477
+ });
260
478
  }
261
479
 
262
- /** Parse `col = ? AND col2 < ?` into ordered predicates. */
263
- function parseWherePredicates(clause: string): WherePred[] {
264
- return clause.split(/\s+AND\s+/i).map((part) => {
265
- const m = /^("?[a-zA-Z_][a-zA-Z0-9_]*"?)\s*(=|<=|>=|<>|!=|<|>)\s*\?$/.exec(part.trim());
266
- if (!m) throw new Error(`memory sql: unsupported WHERE: ${clause}`);
267
- const opRaw = m[2]!;
268
- const op: WherePred["op"] = opRaw === "<>" ? "!=" : (opRaw as WherePred["op"]);
269
- return { column: m[1]!.replaceAll('"', "").trim(), op };
270
- });
480
+ function compareByTerms(a: SqlRow, b: SqlRow, terms: readonly OrderTerm[]): number {
481
+ for (const term of terms) {
482
+ const av = a[term.column];
483
+ const bv = b[term.column];
484
+ let cmp: number;
485
+ if (typeof av === "number" && typeof bv === "number") {
486
+ cmp = av - bv;
487
+ } else {
488
+ cmp = String(av ?? "").localeCompare(String(bv ?? ""));
489
+ }
490
+ if (cmp !== 0) return term.direction === "DESC" ? -cmp : cmp;
491
+ }
492
+ return 0;
271
493
  }
272
494
 
273
- function compareRow(cell: unknown, want: unknown, op: WherePred["op"]): boolean {
495
+ function compareRow(cell: unknown, want: unknown, op: WhereOp): boolean {
274
496
  if (op === "=") return cell === want;
275
497
  if (op === "!=") return cell !== want;
276
- const a = Number(cell ?? 0);
277
- const b = Number(want ?? 0);
498
+ // sqlite parity: LIKE is case-insensitive for ASCII; ILIKE matches it.
499
+ if (op === "like" || op === "ilike") return likeMatch(cell, want);
500
+ let cmp: number;
501
+ if (typeof cell === "number" && typeof want === "number") {
502
+ cmp = cell - want;
503
+ } else if (typeof cell === "string" && typeof want === "string") {
504
+ cmp = cell.localeCompare(want);
505
+ } else {
506
+ cmp = Number(cell ?? 0) - Number(want ?? 0);
507
+ }
278
508
  switch (op) {
279
509
  case "<":
280
- return a < b;
510
+ return cmp < 0;
281
511
  case ">":
282
- return a > b;
512
+ return cmp > 0;
283
513
  case "<=":
284
- return a <= b;
514
+ return cmp <= 0;
285
515
  case ">=":
286
- return a >= b;
516
+ return cmp >= 0;
287
517
  default:
288
518
  return false;
289
519
  }
290
520
  }
291
521
 
522
+ function likeMatch(cell: unknown, pattern: unknown): boolean {
523
+ const text = String(cell ?? "");
524
+ const source = String(pattern ?? "")
525
+ .replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
526
+ .replace(/%/g, ".*")
527
+ .replace(/_/g, ".");
528
+ return new RegExp(`^${source}$`, "i").test(text);
529
+ }
530
+
292
531
  /** Memory SQL driver. */
293
532
  export const memorySqlDriver: SqlDriver = {
294
533
  id: "memory",
package/src/drivers/s3.ts CHANGED
@@ -46,7 +46,16 @@ export async function openS3Bucket(options: FilesOpenOptions): Promise<FilesBuck
46
46
  }
47
47
 
48
48
  function createBunS3Client(bucket: string): S3ClientLike {
49
- const s3 = new Bun.S3Client({ bucket });
49
+ const s3 = new Bun.S3Client({
50
+ bucket,
51
+ ...(process.env.S3_ENDPOINT ? { endpoint: process.env.S3_ENDPOINT } : {}),
52
+ ...(process.env.S3_ACCESS_KEY_ID ? { accessKeyId: process.env.S3_ACCESS_KEY_ID } : {}),
53
+ ...(process.env.S3_SECRET_ACCESS_KEY
54
+ ? { secretAccessKey: process.env.S3_SECRET_ACCESS_KEY }
55
+ : {}),
56
+ ...(process.env.S3_REGION ? { region: process.env.S3_REGION } : {}),
57
+ ...(process.env.S3_SESSION_TOKEN ? { sessionToken: process.env.S3_SESSION_TOKEN } : {}),
58
+ });
50
59
  return {
51
60
  file(key: string) {
52
61
  const f = s3.file(key);
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Gate: the removed vault driver id stays gone from the tracked tree.
3
+ *
4
+ * Mirrors {@link ../cli/doc-staleness.test.ts} — `git grep` must find zero
5
+ * hits (this file splits the token so it never matches itself).
6
+ */
7
+
8
+ import { describe, expect, test } from "bun:test";
9
+ import { join } from "node:path";
10
+
11
+ const ROOT = join(import.meta.dir, "../..");
12
+
13
+ /** Forbidden driver id, split so this file does not match itself. */
14
+ const FORBIDDEN_DRIVER = ["so", "ps"].join("");
15
+ /** Forbidden optional peer, split so this file does not match itself. */
16
+ const FORBIDDEN_AGE = ["age", "-", "encryption"].join("");
17
+
18
+ /** Lockfiles regenerated by `bun install` — not source residue. */
19
+ const LOCKFILE_PREFIXES = ["bun.lock:", "packages/create-oke/template/bun.lock:"];
20
+
21
+ /**
22
+ * Run `git grep -F` and assert zero matches outside allow-listed prefixes.
23
+ *
24
+ * @param pattern - Fixed-string pattern
25
+ * @param ignorePrefixes - `path:line:` prefixes to ignore
26
+ */
27
+ function assertZeroGitGrep(pattern: string, ignorePrefixes: readonly string[] = []): void {
28
+ const proc = Bun.spawnSync(["git", "grep", "-F", "-i", "-n", "-e", pattern, "--"], {
29
+ cwd: ROOT,
30
+ stdout: "pipe",
31
+ stderr: "pipe",
32
+ });
33
+ // git grep exits 1 when there are no matches.
34
+ if (proc.exitCode === 1) {
35
+ expect(proc.stdout.toString()).toBe("");
36
+ return;
37
+ }
38
+ const hits = proc.stdout
39
+ .toString()
40
+ .trim()
41
+ .split("\n")
42
+ .filter((line) => line.length > 0)
43
+ .filter((line) => !ignorePrefixes.some((prefix) => line.startsWith(prefix)));
44
+ expect(hits.join("\n")).toBe("");
45
+ }
46
+
47
+ describe("removed vault driver gate", () => {
48
+ test("tracked tree has zero matches for the removed driver id", () => {
49
+ assertZeroGitGrep(FORBIDDEN_DRIVER);
50
+ });
51
+
52
+ test("tracked tree has zero matches for the removed age peer outside lockfiles", () => {
53
+ assertZeroGitGrep(FORBIDDEN_AGE, LOCKFILE_PREFIXES);
54
+ });
55
+ });
@@ -0,0 +1,97 @@
1
+ /**
2
+ * OpenBao vault driver — KV v2 read + write/delete + fail-loud semantics.
3
+ */
4
+
5
+ import { describe, expect, test } from "bun:test";
6
+ import { OpenBaoUnavailableError, openbaoVaultDriver } from "./vault-openbao.ts";
7
+
8
+ /** Scripted fetch: LIST + per-key GET, then capture writes/deletes. */
9
+ function fakeKv(
10
+ seed: Record<string, string>,
11
+ calls: { method: string; path: string; body?: string }[],
12
+ ) {
13
+ return (async (input: string | URL | Request, init?: RequestInit) => {
14
+ const url = new URL(String(input));
15
+ const method = init?.method ?? "GET";
16
+ calls.push({
17
+ method,
18
+ path: url.pathname + url.search,
19
+ body: typeof init?.body === "string" ? init.body : undefined,
20
+ });
21
+ const mount = "/v1/secret";
22
+ if (method === "GET" && url.pathname === `${mount}/metadata`) {
23
+ return Response.json({ data: { keys: Object.keys(seed) } });
24
+ }
25
+ if (method === "GET" && url.pathname.startsWith(`${mount}/data/`)) {
26
+ const name = decodeURIComponent(url.pathname.slice(`${mount}/data/`.length));
27
+ if (!(name in seed)) return new Response("{}", { status: 404 });
28
+ return Response.json({ data: { data: { value: seed[name] } } });
29
+ }
30
+ if (method === "POST" && url.pathname.startsWith(`${mount}/data/`)) {
31
+ return new Response(null, { status: 204 });
32
+ }
33
+ if (method === "DELETE" && url.pathname.startsWith(`${mount}/data/`)) {
34
+ return new Response(null, { status: 204 });
35
+ }
36
+ return new Response("{}", { status: 404 });
37
+ }) as typeof globalThis.fetch;
38
+ }
39
+
40
+ describe("openbao vault driver", () => {
41
+ test("LIST + GET loads all keys", async () => {
42
+ const calls: { method: string; path: string; body?: string }[] = [];
43
+ const bag = await openbaoVaultDriver.open({
44
+ url: "http://127.0.0.1:8200",
45
+ token: "app-tok",
46
+ fetch: fakeKv({ STRIPE_KEY: "sk_live", DATABASE_URL: "postgres://x" }, calls),
47
+ });
48
+ expect(bag.get("STRIPE_KEY")).toBe("sk_live");
49
+ expect([...bag.names()].sort()).toEqual(["DATABASE_URL", "STRIPE_KEY"]);
50
+ });
51
+
52
+ test("set POSTs KV v2 data payload", async () => {
53
+ const calls: { method: string; path: string; body?: string }[] = [];
54
+ const bag = await openbaoVaultDriver.open({
55
+ url: "http://127.0.0.1:8200",
56
+ token: "app-tok",
57
+ fetch: fakeKv({ STRIPE_KEY: "sk_live" }, calls),
58
+ });
59
+ bag.set?.("STRIPE_KEY", "sk_rotated");
60
+ await Bun.sleep(0);
61
+ const writeCall = calls.find((c) => c.method === "POST");
62
+ expect(writeCall?.path).toBe("/v1/secret/data/STRIPE_KEY");
63
+ expect(writeCall?.body).toBe(JSON.stringify({ data: { value: "sk_rotated" } }));
64
+ expect(bag.get("STRIPE_KEY")).toBe("sk_rotated");
65
+ });
66
+
67
+ test("unreachable remote fails loud (no soft-empty bag)", async () => {
68
+ let failed: unknown;
69
+ try {
70
+ await openbaoVaultDriver.open({
71
+ url: "http://127.0.0.1:9",
72
+ token: "app-tok",
73
+ fetch: (async () => {
74
+ throw new Error("connect ECONNREFUSED");
75
+ }) as unknown as typeof globalThis.fetch,
76
+ });
77
+ } catch (err) {
78
+ failed = err;
79
+ }
80
+ expect(failed).toBeInstanceOf(OpenBaoUnavailableError);
81
+ });
82
+
83
+ test("sealed / 5xx on GET fails loud", async () => {
84
+ const fetchFn = (async (input: string | URL | Request) => {
85
+ const url = String(input);
86
+ if (url.includes("metadata")) return Response.json({ data: { keys: ["STRIPE_KEY"] } });
87
+ return new Response("sealed", { status: 503 });
88
+ }) as typeof globalThis.fetch;
89
+ let failed: unknown;
90
+ try {
91
+ await openbaoVaultDriver.open({ url: "http://127.0.0.1:8200", token: "t", fetch: fetchFn });
92
+ } catch (err) {
93
+ failed = err;
94
+ }
95
+ expect(failed).toBeInstanceOf(OpenBaoUnavailableError);
96
+ });
97
+ });