okengine 0.2.8 → 0.3.4
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 +59 -31
- 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 +98 -0
- package/site/content/docs/plugins/csrf.mdx +96 -0
- package/site/content/docs/plugins/ip-allowlist.mdx +93 -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 +149 -0
- package/src/cli/openbao-bootstrap.ts +314 -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 +172 -0
- package/src/plugins/cors.ts +153 -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 +139 -0
- package/src/plugins/ip-allowlist.ts +129 -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
|
@@ -10,10 +10,17 @@ import type { ClassificationMap, SqlConnection, SqlRow } from "../../drivers/typ
|
|
|
10
10
|
import { throwOke } from "../../kernel/errors.ts";
|
|
11
11
|
import { maskRows, tableFromSql } from "./classify.ts";
|
|
12
12
|
import { isMissingDomainRelationError } from "./missing-relation.ts";
|
|
13
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
andWhere,
|
|
15
|
+
compileOrderBy,
|
|
16
|
+
compileWhere,
|
|
17
|
+
resolveSelectColumns,
|
|
18
|
+
type WhereMap,
|
|
19
|
+
} from "./sql-condition.ts";
|
|
14
20
|
import {
|
|
15
21
|
mapRowToJs,
|
|
16
22
|
prepareInsertRow,
|
|
23
|
+
prepareUpdateRow,
|
|
17
24
|
resolveColumns,
|
|
18
25
|
resolveTableName,
|
|
19
26
|
type TableHandle,
|
|
@@ -40,28 +47,80 @@ export interface SqlSessionOptions {
|
|
|
40
47
|
}
|
|
41
48
|
|
|
42
49
|
/**
|
|
43
|
-
* Continuations after `.
|
|
50
|
+
* Continuations after `.orderBy(...)` — awaitable, optional `.limit` /
|
|
51
|
+
* `.offset`.
|
|
52
|
+
*/
|
|
53
|
+
export interface SelectOrderBuilder extends PromiseLike<SqlRow[]> {
|
|
54
|
+
/**
|
|
55
|
+
* Cap the number of returned rows.
|
|
56
|
+
*
|
|
57
|
+
* @param n - Max rows
|
|
58
|
+
*/
|
|
59
|
+
limit(n: number): Promise<SqlRow[]>;
|
|
60
|
+
/**
|
|
61
|
+
* Skip the first `n` rows (`LIMIT … OFFSET …`).
|
|
62
|
+
*
|
|
63
|
+
* @param n - Rows to skip
|
|
64
|
+
*/
|
|
65
|
+
offset(n: number): Promise<SqlRow[]>;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Continuations after `.where(...)` on a select — awaitable, optional
|
|
70
|
+
* `.orderBy(...)` / `.limit(...)` / `.offset(...)`.
|
|
44
71
|
*/
|
|
45
72
|
export interface SelectWhereBuilder extends PromiseLike<SqlRow[]> {
|
|
73
|
+
/**
|
|
74
|
+
* Order rows with Drizzle `asc()` / `desc()` terms.
|
|
75
|
+
*
|
|
76
|
+
* @param orders - Order terms
|
|
77
|
+
*/
|
|
78
|
+
orderBy(...orders: readonly unknown[]): SelectOrderBuilder;
|
|
46
79
|
/**
|
|
47
80
|
* Cap the number of returned rows.
|
|
48
81
|
*
|
|
49
82
|
* @param n - Max rows
|
|
50
83
|
*/
|
|
51
84
|
limit(n: number): Promise<SqlRow[]>;
|
|
85
|
+
/**
|
|
86
|
+
* Skip the first `n` rows.
|
|
87
|
+
*
|
|
88
|
+
* @param n - Rows to skip
|
|
89
|
+
*/
|
|
90
|
+
offset(n: number): Promise<SqlRow[]>;
|
|
52
91
|
}
|
|
53
92
|
|
|
54
93
|
/**
|
|
55
|
-
* Result of `.from(table)` — awaitable (all rows) or chain
|
|
94
|
+
* Result of `.from(table)` — awaitable (all rows) or chain
|
|
95
|
+
* `.where(...)` / `.orderBy(...)` / `.limit(...)` / `.offset(...)` in any
|
|
96
|
+
* order.
|
|
56
97
|
*/
|
|
57
98
|
export interface SelectFromBuilder extends PromiseLike<SqlRow[]> {
|
|
58
99
|
/**
|
|
59
100
|
* Filter with a plain equality map or a Drizzle SQL condition
|
|
60
|
-
* (`eq`, `and`, `lt`, …).
|
|
101
|
+
* (`eq`, `and`, `or`, `lt`, `like`, …).
|
|
61
102
|
*
|
|
62
103
|
* @param where - Condition
|
|
63
104
|
*/
|
|
64
105
|
where(where: unknown): SelectWhereBuilder;
|
|
106
|
+
/**
|
|
107
|
+
* Order rows with Drizzle `asc()` / `desc()` terms.
|
|
108
|
+
*
|
|
109
|
+
* @param orders - Order terms
|
|
110
|
+
*/
|
|
111
|
+
orderBy(...orders: readonly unknown[]): SelectOrderBuilder;
|
|
112
|
+
/**
|
|
113
|
+
* Cap the number of returned rows.
|
|
114
|
+
*
|
|
115
|
+
* @param n - Max rows
|
|
116
|
+
*/
|
|
117
|
+
limit(n: number): Promise<SqlRow[]>;
|
|
118
|
+
/**
|
|
119
|
+
* Skip the first `n` rows.
|
|
120
|
+
*
|
|
121
|
+
* @param n - Rows to skip
|
|
122
|
+
*/
|
|
123
|
+
offset(n: number): Promise<SqlRow[]>;
|
|
65
124
|
}
|
|
66
125
|
|
|
67
126
|
/** Fluent select builder. */
|
|
@@ -194,6 +253,21 @@ export interface SqlStoreHandle {
|
|
|
194
253
|
* @param params - Bound parameters
|
|
195
254
|
*/
|
|
196
255
|
raw(sql: string, params?: readonly unknown[]): Promise<SqlRow[]>;
|
|
256
|
+
/**
|
|
257
|
+
* Count rows (`COUNT(*)`), optionally filtered.
|
|
258
|
+
*
|
|
259
|
+
* @param table - Table
|
|
260
|
+
* @param where - Optional condition
|
|
261
|
+
*/
|
|
262
|
+
count(table: TableHandle | unknown, where?: unknown): Promise<number>;
|
|
263
|
+
/**
|
|
264
|
+
* One page of rows — offset or keyset mode, composed through the same
|
|
265
|
+
* fluent select path (Drizzle conditions in, `compileWhere` out).
|
|
266
|
+
*
|
|
267
|
+
* @param table - Table
|
|
268
|
+
* @param options - Page options
|
|
269
|
+
*/
|
|
270
|
+
page(table: TableHandle | unknown, options: SqlPageOptions): Promise<SqlRow[]>;
|
|
197
271
|
/**
|
|
198
272
|
* Ensure a simple table exists (test / bootstrap helper).
|
|
199
273
|
*
|
|
@@ -202,6 +276,24 @@ export interface SqlStoreHandle {
|
|
|
202
276
|
ensureTable(table: TableHandle): Promise<void>;
|
|
203
277
|
}
|
|
204
278
|
|
|
279
|
+
/** Options for {@link SqlStoreHandle.page}. */
|
|
280
|
+
export interface SqlPageOptions {
|
|
281
|
+
/** Filter condition (equality map or Drizzle SQL). */
|
|
282
|
+
readonly where?: unknown;
|
|
283
|
+
/** Drizzle `asc()` / `desc()` terms (or bare columns). */
|
|
284
|
+
readonly orderBy?: readonly unknown[];
|
|
285
|
+
/** Max rows to return. */
|
|
286
|
+
readonly limit?: number;
|
|
287
|
+
/** Offset mode — skip rows (uses `OFFSET`). */
|
|
288
|
+
readonly offset?: number;
|
|
289
|
+
/**
|
|
290
|
+
* Keyset mode — a Drizzle SQL condition describing "rows strictly after
|
|
291
|
+
* the cursor" (composed by the caller / `store.resource`); ANDed onto
|
|
292
|
+
* `where`. Cannot combine with `offset`.
|
|
293
|
+
*/
|
|
294
|
+
readonly after?: unknown;
|
|
295
|
+
}
|
|
296
|
+
|
|
205
297
|
/**
|
|
206
298
|
* Create a SQL store handle over a connection.
|
|
207
299
|
*
|
|
@@ -287,11 +379,18 @@ export function createSqlStoreHandle(
|
|
|
287
379
|
return compileWhere(where);
|
|
288
380
|
}
|
|
289
381
|
|
|
382
|
+
/** Accumulated select state across the fluent chain. */
|
|
383
|
+
interface SelectPlan {
|
|
384
|
+
readonly where?: unknown;
|
|
385
|
+
readonly orders?: readonly unknown[];
|
|
386
|
+
readonly limit?: number;
|
|
387
|
+
readonly offset?: number;
|
|
388
|
+
}
|
|
389
|
+
|
|
290
390
|
async function runSelect(
|
|
291
391
|
table: TableHandle | unknown,
|
|
292
392
|
projection: ReturnType<typeof resolveSelectColumns>,
|
|
293
|
-
|
|
294
|
-
limit?: number,
|
|
393
|
+
plan: SelectPlan = {},
|
|
295
394
|
): Promise<SqlRow[]> {
|
|
296
395
|
await ensureFromMeta(table);
|
|
297
396
|
const name = resolveTableName(table);
|
|
@@ -308,20 +407,31 @@ export function createSqlStoreHandle(
|
|
|
308
407
|
|
|
309
408
|
let sql = `SELECT ${selectList} FROM ${quoteIdent(name)}`;
|
|
310
409
|
const params: unknown[] = [];
|
|
311
|
-
if (where !== undefined) {
|
|
312
|
-
const compiled = compileTableWhere(table, where);
|
|
410
|
+
if (plan.where !== undefined) {
|
|
411
|
+
const compiled = compileTableWhere(table, plan.where);
|
|
313
412
|
if (compiled.clause) {
|
|
314
413
|
sql += ` WHERE ${compiled.clause}`;
|
|
315
414
|
params.push(...compiled.params);
|
|
316
415
|
}
|
|
317
416
|
}
|
|
318
|
-
if (
|
|
319
|
-
|
|
417
|
+
if (plan.orders !== undefined && plan.orders.length > 0) {
|
|
418
|
+
const terms = compileOrderBy(plan.orders);
|
|
419
|
+
sql += ` ORDER BY ${terms.map((o) => `${quoteIdent(o.column)} ${o.direction}`).join(", ")}`;
|
|
420
|
+
}
|
|
421
|
+
if (plan.limit !== undefined) {
|
|
422
|
+
sql += ` LIMIT ${Math.max(0, Math.floor(plan.limit))}`;
|
|
423
|
+
}
|
|
424
|
+
if (plan.offset !== undefined) {
|
|
425
|
+
sql += ` OFFSET ${Math.max(0, Math.floor(plan.offset))}`;
|
|
320
426
|
}
|
|
321
427
|
|
|
322
428
|
const rows = await query(sql, params);
|
|
429
|
+
// Map to declared JS keys first, then mask — classifications are keyed by
|
|
430
|
+
// TS field names (`email`), so masking before remapping would miss a
|
|
431
|
+
// `.pii()` column whose SQL name differs (`email_addr`) and leak cleartext
|
|
432
|
+
// under the final client-facing key.
|
|
323
433
|
if (projection === null) {
|
|
324
|
-
return toJs(table,
|
|
434
|
+
return mask(toJs(table, rows), name);
|
|
325
435
|
}
|
|
326
436
|
// Projected aliases are already the returned keys.
|
|
327
437
|
return mask(
|
|
@@ -338,21 +448,37 @@ export function createSqlStoreHandle(
|
|
|
338
448
|
|
|
339
449
|
function selectFrom(table: TableHandle | unknown, columns?: unknown): SelectFromBuilder {
|
|
340
450
|
const projection = resolveSelectColumns(columns);
|
|
341
|
-
|
|
451
|
+
|
|
452
|
+
// Builders are lazy: the query runs on the first await / `.limit(n)` /
|
|
453
|
+
// `.offset(n)`, so a chained `where → orderBy → limit` issues one SELECT.
|
|
454
|
+
function tail(plan: SelectPlan): SelectOrderBuilder {
|
|
455
|
+
return {
|
|
456
|
+
limit(n) {
|
|
457
|
+
return runSelect(table, projection, { ...plan, limit: n });
|
|
458
|
+
},
|
|
459
|
+
offset(n) {
|
|
460
|
+
return runSelect(table, projection, { ...plan, offset: n });
|
|
461
|
+
},
|
|
462
|
+
then(onfulfilled, onrejected) {
|
|
463
|
+
return runSelect(table, projection, plan).then(onfulfilled, onrejected);
|
|
464
|
+
},
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
function filtered(where: unknown): SelectWhereBuilder {
|
|
469
|
+
const plan: SelectPlan = where === undefined ? {} : { where };
|
|
470
|
+
return {
|
|
471
|
+
...tail(plan),
|
|
472
|
+
orderBy(...orders) {
|
|
473
|
+
return tail({ ...plan, orders });
|
|
474
|
+
},
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
|
|
342
478
|
return {
|
|
479
|
+
...filtered(undefined),
|
|
343
480
|
where(where) {
|
|
344
|
-
|
|
345
|
-
return {
|
|
346
|
-
limit(n) {
|
|
347
|
-
return runSelect(table, projection, where, n);
|
|
348
|
-
},
|
|
349
|
-
then(onfulfilled, onrejected) {
|
|
350
|
-
return filtered.then(onfulfilled, onrejected);
|
|
351
|
-
},
|
|
352
|
-
};
|
|
353
|
-
},
|
|
354
|
-
then(onfulfilled, onrejected) {
|
|
355
|
-
return all.then(onfulfilled, onrejected);
|
|
481
|
+
return filtered(where);
|
|
356
482
|
},
|
|
357
483
|
};
|
|
358
484
|
}
|
|
@@ -394,7 +520,7 @@ export function createSqlStoreHandle(
|
|
|
394
520
|
const params = cols.map((c) => prepared[c]);
|
|
395
521
|
const sql = `INSERT INTO ${quoteIdent(name)} (${colList}) VALUES (${placeholders}) RETURNING *`;
|
|
396
522
|
const rows = await query(sql, params);
|
|
397
|
-
return toJs(table,
|
|
523
|
+
return mask(toJs(table, rows), name);
|
|
398
524
|
},
|
|
399
525
|
execute: runExecute,
|
|
400
526
|
then(onfulfilled, onrejected) {
|
|
@@ -412,7 +538,7 @@ export function createSqlStoreHandle(
|
|
|
412
538
|
return {
|
|
413
539
|
async where(where) {
|
|
414
540
|
await ensureFromMeta(table);
|
|
415
|
-
const prepared =
|
|
541
|
+
const prepared = prepareUpdateRow(table, row);
|
|
416
542
|
const setEntries = Object.entries(prepared);
|
|
417
543
|
if (setEntries.length === 0) return 0;
|
|
418
544
|
const setSql = setEntries.map(([col]) => `${quoteIdent(col)} = ?`).join(", ");
|
|
@@ -439,7 +565,7 @@ export function createSqlStoreHandle(
|
|
|
439
565
|
const rows = await query(`SELECT * FROM ${quoteIdent(name)} WHERE ${quoteIdent(pk)} = ?`, [
|
|
440
566
|
idValue,
|
|
441
567
|
]);
|
|
442
|
-
const masked = toJs(table,
|
|
568
|
+
const masked = mask(toJs(table, rows), name);
|
|
443
569
|
return masked[0] ?? null;
|
|
444
570
|
},
|
|
445
571
|
|
|
@@ -516,6 +642,40 @@ export function createSqlStoreHandle(
|
|
|
516
642
|
return mask(rows, table);
|
|
517
643
|
},
|
|
518
644
|
|
|
645
|
+
async count(table, where) {
|
|
646
|
+
await ensureFromMeta(table);
|
|
647
|
+
const name = resolveTableName(table);
|
|
648
|
+
let sql = `SELECT COUNT(*) AS "count" FROM ${quoteIdent(name)}`;
|
|
649
|
+
const params: unknown[] = [];
|
|
650
|
+
if (where !== undefined) {
|
|
651
|
+
const compiled = compileTableWhere(table, where);
|
|
652
|
+
if (compiled.clause) {
|
|
653
|
+
sql += ` WHERE ${compiled.clause}`;
|
|
654
|
+
params.push(...compiled.params);
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
const rows = await query(sql, params);
|
|
658
|
+
return asNumber(rows[0]?.count ?? rows[0]?.COUNT ?? 0, "count");
|
|
659
|
+
},
|
|
660
|
+
|
|
661
|
+
async page(table, options) {
|
|
662
|
+
if (options.offset !== undefined && options.after !== undefined) {
|
|
663
|
+
throw new Error("page(): offset and after (keyset) cannot combine");
|
|
664
|
+
}
|
|
665
|
+
const where =
|
|
666
|
+
options.after === undefined
|
|
667
|
+
? options.where
|
|
668
|
+
: options.where === undefined
|
|
669
|
+
? options.after
|
|
670
|
+
: andWhere(options.where, options.after);
|
|
671
|
+
return runSelect(table, null, {
|
|
672
|
+
where,
|
|
673
|
+
orders: options.orderBy,
|
|
674
|
+
limit: options.limit,
|
|
675
|
+
offset: options.offset,
|
|
676
|
+
});
|
|
677
|
+
},
|
|
678
|
+
|
|
519
679
|
async ensureTable(table) {
|
|
520
680
|
const cols = Object.values(table.columns);
|
|
521
681
|
const pk = resolvePkColumn(table);
|
|
@@ -252,10 +252,39 @@ export function prepareInsertRow(
|
|
|
252
252
|
}
|
|
253
253
|
|
|
254
254
|
/**
|
|
255
|
-
* Map a
|
|
255
|
+
* Map a JS-keyed partial row to SQL column names for `UPDATE … SET`.
|
|
256
|
+
* Unlike {@link prepareInsertRow}, `$defaultFn`s are never applied — column
|
|
257
|
+
* defaults are insert physics, not update physics.
|
|
256
258
|
*
|
|
257
259
|
* @param table - Table-like value
|
|
258
|
-
* @param row -
|
|
260
|
+
* @param row - Incoming values (JS keys)
|
|
261
|
+
*/
|
|
262
|
+
export function prepareUpdateRow(
|
|
263
|
+
table: unknown,
|
|
264
|
+
row: Readonly<Record<string, unknown>>,
|
|
265
|
+
): Record<string, unknown> {
|
|
266
|
+
const cols = resolveColumns(table);
|
|
267
|
+
if (cols.length === 0) return { ...row };
|
|
268
|
+
const byKey = new Map(cols.map((c) => [c.key, c]));
|
|
269
|
+
const bySql = new Map(cols.map((c) => [c.sqlName, c]));
|
|
270
|
+
const out: Record<string, unknown> = {};
|
|
271
|
+
for (const [k, v] of Object.entries(row)) {
|
|
272
|
+
const col = byKey.get(k) ?? bySql.get(k);
|
|
273
|
+
out[col?.sqlName ?? k] = v;
|
|
274
|
+
}
|
|
275
|
+
return out;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Map a SQL-keyed driver row to declared JS field names only.
|
|
280
|
+
*
|
|
281
|
+
* When Drizzle/TableHandle metadata exists, the result contains exactly the
|
|
282
|
+
* table's declared keys (`createdAt`) — never the raw SQL names
|
|
283
|
+
* (`created_at`) alongside or instead of them. Without metadata, the row is
|
|
284
|
+
* returned as-is (opaque / raw SQL).
|
|
285
|
+
*
|
|
286
|
+
* @param table - Table-like value
|
|
287
|
+
* @param row - Driver row (SQL column names, or already JS-keyed)
|
|
259
288
|
*/
|
|
260
289
|
export function mapRowToJs(
|
|
261
290
|
table: unknown,
|
|
@@ -263,9 +292,10 @@ export function mapRowToJs(
|
|
|
263
292
|
): Record<string, unknown> {
|
|
264
293
|
const cols = resolveColumns(table);
|
|
265
294
|
if (cols.length === 0) return { ...row };
|
|
266
|
-
const out: Record<string, unknown> = {
|
|
295
|
+
const out: Record<string, unknown> = {};
|
|
267
296
|
for (const col of cols) {
|
|
268
|
-
if (col.
|
|
297
|
+
if (col.key in row) out[col.key] = row[col.key];
|
|
298
|
+
else if (col.sqlName in row) out[col.key] = row[col.sqlName];
|
|
269
299
|
}
|
|
270
300
|
return out;
|
|
271
301
|
}
|
package/src/elements/store.ts
CHANGED
|
@@ -78,14 +78,30 @@ export type { SqlBindingConfig } from "./store/replica.ts";
|
|
|
78
78
|
export { defineTable, resolveTableName, classificationsFromTable, id, now } from "./store/table.ts";
|
|
79
79
|
export type { ColumnDef, TableHandle } from "./store/table.ts";
|
|
80
80
|
|
|
81
|
+
export { resource } from "./store/resource.ts";
|
|
82
|
+
export type {
|
|
83
|
+
ColumnScope,
|
|
84
|
+
ListCountMode,
|
|
85
|
+
ListPageMode,
|
|
86
|
+
ResourceColumns,
|
|
87
|
+
ResourceDef,
|
|
88
|
+
ResourceFlowDefs,
|
|
89
|
+
ResourceListOptions,
|
|
90
|
+
ResourceOptions,
|
|
91
|
+
ResolvedListConfig,
|
|
92
|
+
} from "./store/resource.ts";
|
|
93
|
+
|
|
81
94
|
export { createSqlStoreHandle, resolvePkColumn } from "./store/sql-session.ts";
|
|
82
95
|
export type {
|
|
83
96
|
SqlStoreHandle,
|
|
84
97
|
SelectBuilder,
|
|
85
98
|
SelectFromBuilder,
|
|
99
|
+
SelectWhereBuilder,
|
|
100
|
+
SelectOrderBuilder,
|
|
86
101
|
InsertBuilder,
|
|
87
102
|
InsertValuesBuilder,
|
|
88
103
|
SqlSessionOptions,
|
|
104
|
+
SqlPageOptions,
|
|
89
105
|
WhereMap,
|
|
90
106
|
} from "./store/sql-session.ts";
|
|
91
107
|
|
|
@@ -71,7 +71,7 @@ export interface CreateVaultRuntimeOptions {
|
|
|
71
71
|
readonly secrets?: readonly VaultSecretDecl[];
|
|
72
72
|
/**
|
|
73
73
|
* Resolution chain — first hit wins.
|
|
74
|
-
*
|
|
74
|
+
* Callers pass the env-appropriate layers (env / openbao / memory / …).
|
|
75
75
|
*/
|
|
76
76
|
readonly chain?: readonly VaultChainLayer[];
|
|
77
77
|
/**
|
|
@@ -4,17 +4,10 @@
|
|
|
4
4
|
* - a secret value never appears in a log or trace even when passed to fx.log
|
|
5
5
|
* - fingerprints instead of values
|
|
6
6
|
* - resolution chain (first hit wins)
|
|
7
|
-
* - sops/age via Typage (in-process, no Go binary)
|
|
8
7
|
*/
|
|
9
8
|
|
|
10
9
|
import { describe, expect, test } from "bun:test";
|
|
11
|
-
import
|
|
12
|
-
import {
|
|
13
|
-
buildSopsFixture,
|
|
14
|
-
envVaultDriver,
|
|
15
|
-
memoryVaultDriver,
|
|
16
|
-
sopsVaultDriver,
|
|
17
|
-
} from "../drivers/index.ts";
|
|
10
|
+
import { envVaultDriver, memoryVaultDriver } from "../drivers/index.ts";
|
|
18
11
|
import { createFx } from "../kernel/fx.ts";
|
|
19
12
|
import {
|
|
20
13
|
createVaultRuntime,
|
|
@@ -239,26 +232,6 @@ describe("redaction + fingerprints", () => {
|
|
|
239
232
|
});
|
|
240
233
|
});
|
|
241
234
|
|
|
242
|
-
describe("sops / age (Typage)", () => {
|
|
243
|
-
test("decrypts SOPS JSON in-process with age-encryption", async () => {
|
|
244
|
-
const identity = await age.generateIdentity();
|
|
245
|
-
const recipient = await age.identityToRecipient(identity);
|
|
246
|
-
const { json } = await buildSopsFixture({ STRIPE_KEY: "sk_sops_decrypted" }, recipient);
|
|
247
|
-
|
|
248
|
-
const runtime = createVaultRuntime({
|
|
249
|
-
secrets: [vault("STRIPE_KEY")],
|
|
250
|
-
chain: [
|
|
251
|
-
{
|
|
252
|
-
driver: sopsVaultDriver,
|
|
253
|
-
options: { ciphertext: json, ageIdentity: identity },
|
|
254
|
-
},
|
|
255
|
-
],
|
|
256
|
-
});
|
|
257
|
-
await runtime.boot();
|
|
258
|
-
expect(runtime.read("STRIPE_KEY")).toBe("sk_sops_decrypted");
|
|
259
|
-
});
|
|
260
|
-
});
|
|
261
|
-
|
|
262
235
|
/**
|
|
263
236
|
* Helper — createFx with vault runtime + log sink.
|
|
264
237
|
*/
|
package/src/elements/vault.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Vault element — protected knowledge.
|
|
3
3
|
*
|
|
4
4
|
* Physics: secrets · config · environment.
|
|
5
|
-
* Drivers: `
|
|
5
|
+
* Drivers: `env` · `openbao` · `infisical` · `managed`.
|
|
6
6
|
*
|
|
7
7
|
* A declaration is a contract, never a value. Boot validates every contract
|
|
8
8
|
* and lists all gaps at once. Logs/traces receive fingerprints and redaction
|
package/src/kernel/app.ts
CHANGED
|
@@ -62,7 +62,7 @@ import {
|
|
|
62
62
|
isJournalSuspend,
|
|
63
63
|
type JournalSession,
|
|
64
64
|
} from "./journal.ts";
|
|
65
|
-
import { listBindings, type Binding } from "./on.ts";
|
|
65
|
+
import { listBindings, resetBindings, type Binding } from "./on.ts";
|
|
66
66
|
import {
|
|
67
67
|
applyPrincipal,
|
|
68
68
|
createElementPipelineHooks,
|
|
@@ -102,8 +102,23 @@ export interface OkeOptions {
|
|
|
102
102
|
* Default `true`.
|
|
103
103
|
*/
|
|
104
104
|
readonly aot?: boolean;
|
|
105
|
-
/**
|
|
105
|
+
/**
|
|
106
|
+
* Extra bindings. Combined with the global {@link on} registry unless
|
|
107
|
+
* {@link OkeOptions.registry} is `"ignore"` — then this is the only source.
|
|
108
|
+
*/
|
|
106
109
|
readonly bindings?: readonly Binding[];
|
|
110
|
+
/**
|
|
111
|
+
* How the global {@link on} registry is treated at construction.
|
|
112
|
+
*
|
|
113
|
+
* - `"consume"` (default) — adopt registered bindings, then clear the
|
|
114
|
+
* registry so a later {@link oke} in the same process cannot inherit
|
|
115
|
+
* this app's flows.
|
|
116
|
+
* - `"keep"` — adopt registered bindings and leave the registry intact
|
|
117
|
+
* (legacy behavior).
|
|
118
|
+
* - `"ignore"` — adopt only {@link OkeOptions.bindings}; the registry is
|
|
119
|
+
* neither read nor cleared (embedding / Console).
|
|
120
|
+
*/
|
|
121
|
+
readonly registry?: "consume" | "keep" | "ignore";
|
|
107
122
|
/** Base fx options applied to every invocation. */
|
|
108
123
|
readonly fx?: Omit<CreateFxOptions, "flow" | "effects" | "capability">;
|
|
109
124
|
/**
|
|
@@ -383,7 +398,12 @@ export interface OkeApp<D extends Record<string, unknown> = {}, R extends AppRou
|
|
|
383
398
|
*/
|
|
384
399
|
export function oke(options: OkeOptions): OkeApp {
|
|
385
400
|
const aot = options.aot !== false;
|
|
386
|
-
const
|
|
401
|
+
const registry = options.registry ?? "consume";
|
|
402
|
+
const adopted: Binding[] =
|
|
403
|
+
registry === "ignore"
|
|
404
|
+
? [...(options.bindings ?? [])]
|
|
405
|
+
: [...listBindings(), ...(options.bindings ?? [])];
|
|
406
|
+
if (registry === "consume") resetBindings();
|
|
387
407
|
/** Retained for test harness / boot merges. */
|
|
388
408
|
const $options = options;
|
|
389
409
|
|
|
@@ -654,29 +674,37 @@ export function oke(options: OkeOptions): OkeApp {
|
|
|
654
674
|
|
|
655
675
|
const alreadyValidated = extras?.validated === true;
|
|
656
676
|
|
|
657
|
-
const result = await runPipeline(
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
677
|
+
const result = await runPipeline(
|
|
678
|
+
ctx,
|
|
679
|
+
fx,
|
|
680
|
+
hooks,
|
|
681
|
+
async () => {
|
|
682
|
+
try {
|
|
683
|
+
if (!alreadyValidated) {
|
|
684
|
+
const parsed = await validate(flowDef.in, ctx.input);
|
|
685
|
+
if (!parsed.ok) return parsed.failure;
|
|
686
|
+
ctx.input = parsed.value;
|
|
687
|
+
return await flowDef.do(parsed.value as never, fx);
|
|
688
|
+
}
|
|
689
|
+
return await flowDef.do(ctx.input as never, fx);
|
|
690
|
+
} catch (err) {
|
|
691
|
+
// A durable sleep that has not yet elapsed suspends the run — this
|
|
692
|
+
// is a park, not a pipeline failure, so it must not throw upward.
|
|
693
|
+
if (flowDef.durable && journalSession && isJournalSuspend(err)) {
|
|
694
|
+
ctx.state.sleeping = {
|
|
695
|
+
wakeAt: err.wakeAt,
|
|
696
|
+
label: err.label,
|
|
697
|
+
runId: journalSession.runId,
|
|
698
|
+
};
|
|
699
|
+
return undefined;
|
|
700
|
+
}
|
|
701
|
+
throw err;
|
|
676
702
|
}
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
703
|
+
},
|
|
704
|
+
// HTTP flows serialize before `onResponse` so the last stage can see
|
|
705
|
+
// and replace the final response (plugin header/middleware surfaces).
|
|
706
|
+
trigger.kind === "http" ? encodeExecuteResult : undefined,
|
|
707
|
+
);
|
|
680
708
|
|
|
681
709
|
const endedAt = now();
|
|
682
710
|
|
|
@@ -905,6 +933,12 @@ export function oke(options: OkeOptions): OkeApp {
|
|
|
905
933
|
|
|
906
934
|
const matched = router.match(method, url.pathname);
|
|
907
935
|
if (!matched) {
|
|
936
|
+
// Plugin edge handlers answer requests no flow owns (e.g. CORS
|
|
937
|
+
// preflight for a path bound to another method) — first Response wins.
|
|
938
|
+
for (const handle of pluginRegistry.edgeHandlers()) {
|
|
939
|
+
const edgeResponse = await handle(request, { method, path: url.pathname });
|
|
940
|
+
if (edgeResponse !== undefined) return respond(edgeResponse);
|
|
941
|
+
}
|
|
908
942
|
return respond(new Response("Not Found", { status: 404 }));
|
|
909
943
|
}
|
|
910
944
|
const { value: binding, params } = matched;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Channel binder — profile selection + SMTP env parsing.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { afterEach, describe, expect, test } from "bun:test";
|
|
6
|
+
import { resolveEmailDriverId, smtpOptionsFromEnv } from "./channel.ts";
|
|
7
|
+
|
|
8
|
+
const previous = {
|
|
9
|
+
url: process.env.SMTP_URL,
|
|
10
|
+
user: process.env.SMTP_USER,
|
|
11
|
+
password: process.env.SMTP_PASSWORD,
|
|
12
|
+
okeUrl: process.env.OKE_CHANNEL_EMAIL_URL,
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
afterEach(() => {
|
|
16
|
+
restoreEnv("SMTP_URL", previous.url);
|
|
17
|
+
restoreEnv("SMTP_USER", previous.user);
|
|
18
|
+
restoreEnv("SMTP_PASSWORD", previous.password);
|
|
19
|
+
restoreEnv("OKE_CHANNEL_EMAIL_URL", previous.okeUrl);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
describe("bindChannel driver resolution", () => {
|
|
23
|
+
const options = {
|
|
24
|
+
config: {
|
|
25
|
+
drivers: {
|
|
26
|
+
channel: {
|
|
27
|
+
email: { local: "console", docker: "smtp", test: "console", prod: "smtp" },
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
test("uses console locally and smtp in docker", () => {
|
|
34
|
+
expect(resolveEmailDriverId(options, "local", false)).toBe("console");
|
|
35
|
+
expect(resolveEmailDriverId(options, "docker", true)).toBe("smtp");
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test("parses SMTP_URL and optional auth overrides", () => {
|
|
39
|
+
process.env.SMTP_URL = "smtp://url-user:url-pass@127.0.0.1:20975";
|
|
40
|
+
process.env.SMTP_USER = "override-user";
|
|
41
|
+
process.env.SMTP_PASSWORD = "override-pass";
|
|
42
|
+
expect(smtpOptionsFromEnv(true)).toEqual({
|
|
43
|
+
host: "127.0.0.1",
|
|
44
|
+
port: 20975,
|
|
45
|
+
user: "override-user",
|
|
46
|
+
pass: "override-pass",
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("missing SMTP_URL includes docker recovery hint", () => {
|
|
51
|
+
delete process.env.SMTP_URL;
|
|
52
|
+
delete process.env.OKE_CHANNEL_EMAIL_URL;
|
|
53
|
+
expect(() => smtpOptionsFromEnv(true)).toThrow("docker/.env.docker");
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
function restoreEnv(key: string, value: string | undefined): void {
|
|
58
|
+
if (value === undefined) delete process.env[key];
|
|
59
|
+
else process.env[key] = value;
|
|
60
|
+
}
|