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.
- package/AGENTS.md +17 -15
- package/README.md +57 -29
- package/package.json +11 -16
- package/site/content/docs/ai/llms-txt.mdx +54 -0
- package/site/content/docs/ai/mcp.mdx +123 -0
- package/site/content/docs/ai/meta.json +5 -0
- package/site/content/docs/ai/skills.mdx +53 -0
- package/site/content/docs/console/access.mdx +29 -0
- package/site/content/docs/console/ai.mdx +35 -0
- package/site/content/docs/console/architecture.mdx +35 -0
- package/site/content/docs/console/channels.mdx +37 -0
- package/site/content/docs/console/clock.mdx +31 -0
- package/site/content/docs/console/flows.mdx +31 -0
- package/site/content/docs/console/gates.mdx +35 -0
- package/site/content/docs/console/manifest-diff.mdx +34 -0
- package/site/content/docs/console/meta.json +23 -0
- package/site/content/docs/console/overview.mdx +40 -0
- package/site/content/docs/console/plugins.mdx +41 -0
- package/site/content/docs/console/privacy.mdx +32 -0
- package/site/content/docs/console/runs.mdx +40 -0
- package/site/content/docs/console/signals.mdx +31 -0
- package/site/content/docs/console/store.mdx +32 -0
- package/site/content/docs/console/tenancy.mdx +32 -0
- package/site/content/docs/console/traces.mdx +34 -0
- package/site/content/docs/console/vault.mdx +37 -0
- package/site/content/docs/elements/ai.mdx +180 -0
- package/site/content/docs/elements/channel.mdx +167 -0
- package/site/content/docs/elements/clock.mdx +182 -0
- package/site/content/docs/elements/flow.mdx +288 -0
- package/site/content/docs/elements/gate.mdx +171 -0
- package/site/content/docs/elements/meta.json +5 -0
- package/site/content/docs/elements/signal.mdx +171 -0
- package/site/content/docs/elements/store.mdx +320 -0
- package/site/content/docs/elements/vault.mdx +263 -0
- package/site/content/docs/get-started/basic-usage.mdx +124 -0
- package/site/content/docs/get-started/comparison.mdx +65 -0
- package/site/content/docs/get-started/installation.mdx +113 -0
- package/site/content/docs/get-started/introduction.mdx +123 -0
- package/site/content/docs/get-started/meta.json +5 -0
- package/site/content/docs/index.mdx +63 -0
- package/site/content/docs/meta.json +5 -0
- package/site/content/docs/plugins/compression.mdx +60 -0
- package/site/content/docs/plugins/cors.mdx +92 -0
- package/site/content/docs/plugins/csrf.mdx +96 -0
- package/site/content/docs/plugins/ip-allowlist.mdx +92 -0
- package/site/content/docs/plugins/maintenance-mode.mdx +101 -0
- package/site/content/docs/plugins/meta.json +15 -0
- package/site/content/docs/plugins/security-headers.mdx +136 -0
- package/site/content/docs/reference/cli.md +101 -0
- package/site/content/docs/reference/configuration.mdx +159 -0
- package/site/content/docs/reference/environment-variables.mdx +87 -0
- package/site/content/docs/reference/errors.mdx +80 -0
- package/site/content/docs/reference/fx.mdx +117 -0
- package/site/content/docs/reference/meta.json +5 -0
- package/site/content/docs/reference/plugins.mdx +249 -0
- package/site/content/docs/reference/security.md +63 -0
- package/src/auth/auth.test.ts +3 -0
- package/src/cli/ask-dev-mode.ts +1 -1
- package/src/cli/db.ts +87 -17
- package/src/cli/dev-db-push.test.ts +32 -2
- package/src/cli/dev-schema-sync.test.ts +66 -0
- package/src/cli/dev-schema-sync.ts +139 -0
- package/src/cli/dev.test.ts +123 -1
- package/src/cli/dev.ts +150 -8
- package/src/cli/doc-staleness.test.ts +4 -4
- package/src/cli/docker-cli.test.ts +20 -0
- package/src/cli/docker.ts +10 -0
- package/src/cli/drizzle-env.test.ts +67 -0
- package/src/cli/drizzle-env.ts +78 -0
- package/src/cli/ensure-drizzle-config.ts +50 -0
- package/src/cli/hero-meta.test.ts +1 -1
- package/src/cli/load-config.ts +6 -0
- package/src/cli/mode.ts +24 -4
- package/src/cli/openbao-bootstrap.test.ts +147 -0
- package/src/cli/openbao-bootstrap.ts +280 -0
- package/src/cli/openbao-restart.integration.test.ts +136 -0
- package/src/cli/ports.test.ts +7 -5
- package/src/cli/ports.ts +6 -2
- package/src/cli/resolve-dev-sql-env.test.ts +48 -0
- package/src/cli/resolve-dev-sql-env.ts +42 -0
- package/src/cli/stack.ts +7 -4
- package/src/cli/vault-cmd.ts +63 -0
- package/src/client/types.ts +7 -1
- package/src/compiler/extract.test.ts +40 -0
- package/src/compiler/extract.ts +123 -1
- package/src/compiler/fixtures/skyport/oke.config.ts +2 -2
- package/src/compiler/fixtures/skyport.expected.json +1 -1
- package/src/compiler/response.ts +12 -0
- package/src/config/define-config.test.ts +4 -6
- package/src/config/index.ts +2 -15
- package/src/console/server/app.ts +2 -0
- package/src/console/server/vault.ts +21 -6
- package/src/docker/compose.ts +112 -16
- package/src/docker/derive.ts +7 -1
- package/src/docker/docker.test.ts +103 -0
- package/src/docker/index.ts +11 -1
- package/src/docker/recipes/index.ts +3 -2
- package/src/docker/recipes/openbao.ts +47 -0
- package/src/docker/recipes/redis.ts +5 -1
- package/src/docker/recipes/rustfs.ts +2 -3
- package/src/docker/stack-id.test.ts +43 -8
- package/src/docker/stack-id.ts +99 -20
- package/src/docker/stack.ts +36 -4
- package/src/docker/types.ts +3 -0
- package/src/docs-origin.ts +4 -4
- package/src/drivers/drizzle-dialect.test.ts +20 -0
- package/src/drivers/drizzle-dialect.ts +37 -0
- package/src/drivers/index.ts +1 -2
- package/src/drivers/memory.ts +278 -39
- package/src/drivers/s3.ts +10 -1
- package/src/drivers/vault-driver-removal.test.ts +55 -0
- package/src/drivers/vault-openbao.test.ts +97 -0
- package/src/drivers/vault-openbao.ts +102 -35
- package/src/drivers/vault-types.ts +3 -10
- package/src/elements/store/declare.ts +4 -1
- package/src/elements/store/resource-list-docs.fixture.ts +56 -0
- package/src/elements/store/resource-list-docs.test.ts +79 -0
- package/src/elements/store/resource.test.ts +253 -0
- package/src/elements/store/resource.ts +786 -0
- package/src/elements/store/sql-condition.test.ts +132 -0
- package/src/elements/store/sql-condition.ts +284 -46
- package/src/elements/store/sql-session.test.ts +86 -1
- package/src/elements/store/sql-session.ts +187 -27
- package/src/elements/store/table.ts +34 -4
- package/src/elements/store.ts +16 -0
- package/src/elements/vault/runtime.ts +1 -1
- package/src/elements/vault.test.ts +1 -28
- package/src/elements/vault.ts +1 -1
- package/src/kernel/app.ts +59 -25
- package/src/kernel/boot-bind/channel.test.ts +60 -0
- package/src/kernel/boot-bind/channel.ts +64 -2
- package/src/kernel/boot-bind/store.test.ts +10 -1
- package/src/kernel/boot-bind/store.ts +49 -2
- package/src/kernel/boot.test.ts +0 -1
- package/src/kernel/boot.ts +1 -1
- package/src/kernel/edge.test.ts +68 -0
- package/src/kernel/errors.registry.test.ts +1 -1
- package/src/kernel/flow.ts +8 -0
- package/src/kernel/fx.test.ts +23 -3
- package/src/kernel/fx.ts +115 -18
- package/src/kernel/hooks.test.ts +33 -0
- package/src/kernel/hooks.ts +22 -0
- package/src/kernel/index.ts +7 -0
- package/src/kernel/on.ts +44 -3
- package/src/kernel/plugin.ts +33 -3
- package/src/kernel/registry-isolation.test.ts +74 -0
- package/src/kernel/registry.ts +22 -1
- package/src/kernel/triggers.ts +59 -0
- package/src/manifest/fixtures/skyport.excerpt.json +1 -1
- package/src/manifest/fixtures/skyport.manifest.json +1 -1
- package/src/manifest/index.ts +1 -1
- package/src/manifest/types.ts +1 -1
- package/src/manifest/validate.ts +2 -2
- package/src/plugins/compression.test.ts +127 -0
- package/src/plugins/compression.ts +94 -0
- package/src/plugins/config-source.test.ts +204 -0
- package/src/plugins/config-source.ts +209 -0
- package/src/plugins/cors.test.ts +138 -0
- package/src/plugins/cors.ts +129 -0
- package/src/plugins/csrf.test.ts +102 -0
- package/src/plugins/csrf.ts +86 -0
- package/src/plugins/headers.ts +54 -0
- package/src/plugins/index.ts +26 -0
- package/src/plugins/ip-allowlist.test.ts +105 -0
- package/src/plugins/ip-allowlist.ts +76 -0
- package/src/plugins/maintenance-mode.test.ts +91 -0
- package/src/plugins/maintenance-mode.ts +85 -0
- package/src/plugins/security-headers.test.ts +243 -0
- package/src/plugins/security-headers.ts +255 -0
- package/src/release/measure.ts +1 -2
- package/src/test/create-test-app.ts +14 -2
- package/docs/spec/console.md +0 -762
- package/docs/spec/example.md +0 -1374
- package/docs/spec/four-applications.md +0 -1376
- package/docs/spec/unified-theory.md +0 -498
- package/src/cli/doc-drift.test.ts +0 -147
- package/src/cli/doc-drift.ts +0 -401
- package/src/cli/doctor-diff-examples.ts +0 -90
- package/src/drivers/vault-sops.ts +0 -246
- /package/{spec/manifest.v1.schema.json → manifest.v1.schema.json} +0 -0
package/src/drivers/memory.ts
CHANGED
|
@@ -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+(
|
|
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
|
|
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
|
|
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 (
|
|
66
|
-
|
|
67
|
-
|
|
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
|
|
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
|
-
/**
|
|
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
|
-
|
|
249
|
-
|
|
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
|
-
|
|
253
|
-
}
|
|
462
|
+
cols.push(n.column);
|
|
463
|
+
};
|
|
464
|
+
walk(node);
|
|
465
|
+
return cols;
|
|
254
466
|
}
|
|
255
467
|
|
|
256
|
-
/**
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
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
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
const
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
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:
|
|
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
|
-
|
|
277
|
-
|
|
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
|
|
510
|
+
return cmp < 0;
|
|
281
511
|
case ">":
|
|
282
|
-
return
|
|
512
|
+
return cmp > 0;
|
|
283
513
|
case "<=":
|
|
284
|
-
return
|
|
514
|
+
return cmp <= 0;
|
|
285
515
|
case ">=":
|
|
286
|
-
return
|
|
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({
|
|
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
|
+
});
|