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.
Files changed (180) hide show
  1. package/AGENTS.md +17 -15
  2. package/README.md +59 -31
  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 +98 -0
  44. package/site/content/docs/plugins/csrf.mdx +96 -0
  45. package/site/content/docs/plugins/ip-allowlist.mdx +93 -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 +149 -0
  75. package/src/cli/openbao-bootstrap.ts +314 -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 +172 -0
  159. package/src/plugins/cors.ts +153 -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 +139 -0
  165. package/src/plugins/ip-allowlist.ts +129 -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
@@ -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 { compileWhere, resolveSelectColumns, type WhereMap } from "./sql-condition.ts";
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 `.where(...)` on a select — awaitable, optional `.limit`.
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 `.where(...)`.
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
- where: unknown | undefined,
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 (limit !== undefined) {
319
- sql += ` LIMIT ${Math.max(0, Math.floor(limit))}`;
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, mask(rows, name));
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
- const all = runSelect(table, projection, undefined);
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
- const filtered = runSelect(table, projection, where);
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, mask(rows, name));
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 = prepareInsertRow(table, row);
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, mask(rows, name));
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 SQL-keyed row back to JS keys when Drizzle/TableHandle metadata exists.
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 - Driver 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> = { ...row };
295
+ const out: Record<string, unknown> = {};
267
296
  for (const col of cols) {
268
- if (col.sqlName in row) out[col.key] = row[col.sqlName];
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
  }
@@ -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
- * Default for prod: `[sops]`; for tests callers pass memory/env layers.
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 * as age from "age-encryption";
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
  */
@@ -2,7 +2,7 @@
2
2
  * Vault element — protected knowledge.
3
3
  *
4
4
  * Physics: secrets · config · environment.
5
- * Drivers: `sops` (Typage/age) · `env` · `openbao` · `infisical` · `managed`.
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
- /** Extra bindings (in addition to the global {@link on} registry). */
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 adopted: Binding[] = [...listBindings(), ...(options.bindings ?? [])];
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(ctx, fx, hooks, async () => {
658
- try {
659
- if (!alreadyValidated) {
660
- const parsed = await validate(flowDef.in, ctx.input);
661
- if (!parsed.ok) return parsed.failure;
662
- ctx.input = parsed.value;
663
- return await flowDef.do(parsed.value as never, fx);
664
- }
665
- return await flowDef.do(ctx.input as never, fx);
666
- } catch (err) {
667
- // A durable sleep that has not yet elapsed suspends the run — this
668
- // is a park, not a pipeline failure, so it must not throw upward.
669
- if (flowDef.durable && journalSession && isJournalSuspend(err)) {
670
- ctx.state.sleeping = {
671
- wakeAt: err.wakeAt,
672
- label: err.label,
673
- runId: journalSession.runId,
674
- };
675
- return undefined;
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
- throw err;
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
+ }