okengine 0.14.1 → 0.15.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (205) hide show
  1. package/package.json +3 -4
  2. package/site/content/docs/elements/ai.mdx +2 -1
  3. package/site/content/docs/elements/channel.mdx +1 -0
  4. package/site/content/docs/elements/clock.mdx +3 -0
  5. package/site/content/docs/elements/flow.mdx +3 -0
  6. package/site/content/docs/elements/gate.mdx +25 -22
  7. package/site/content/docs/elements/signal.mdx +4 -4
  8. package/site/content/docs/elements/store.mdx +86 -27
  9. package/site/content/docs/elements/vault.mdx +1 -0
  10. package/site/content/docs/get-started/basic-usage.mdx +22 -13
  11. package/site/content/docs/get-started/index.mdx +5 -0
  12. package/site/content/docs/get-started/introduction.mdx +11 -3
  13. package/site/content/docs/get-started/meta.json +1 -1
  14. package/site/content/docs/get-started/project-structure.mdx +932 -0
  15. package/site/content/docs/reference/cli.md +13 -10
  16. package/site/content/docs/reference/client.mdx +25 -21
  17. package/site/content/docs/reference/security.md +1 -1
  18. package/src/auth/bindings.ts +14 -5
  19. package/src/auth/cross-plane.ts +10 -3
  20. package/src/cli/ai-setup/ai-setup.test.ts +62 -0
  21. package/src/cli/ai-setup/apply.ts +32 -1
  22. package/src/cli/attach-host-console.test.ts +3 -2
  23. package/src/cli/attach-host-console.ts +3 -0
  24. package/src/cli/build.test.ts +4 -2
  25. package/src/cli/db-seed.ts +48 -15
  26. package/src/cli/db.test.ts +104 -0
  27. package/src/cli/db.ts +122 -10
  28. package/src/cli/dev-controls.test.ts +20 -0
  29. package/src/cli/dev-controls.ts +13 -0
  30. package/src/cli/dev.test.ts +15 -4
  31. package/src/cli/dev.ts +102 -29
  32. package/src/cli/ensure-drizzle-config.ts +2 -1
  33. package/src/cli/tui/DevLive.tsx +12 -2
  34. package/src/cli/tui/keys.test.ts +2 -1
  35. package/src/cli/tui/keys.ts +3 -2
  36. package/src/compiler/aot.test.ts +1 -1
  37. package/src/compiler/extract.test.ts +409 -33
  38. package/src/compiler/extract.ts +318 -67
  39. package/src/compiler/flow-path.test.ts +96 -0
  40. package/src/compiler/flow-path.ts +173 -0
  41. package/src/compiler/generate-adopt.test.ts +112 -3
  42. package/src/compiler/generate-adopt.ts +280 -26
  43. package/src/compiler/schema-from-ast.ts +609 -0
  44. package/src/console/server/bind.ts +2 -2
  45. package/src/console/server/console.test.ts +3 -0
  46. package/src/console/server/flows.ts +120 -40
  47. package/src/console/server/invoke-user-flow.test.ts +3 -1
  48. package/src/console/server/invoke-user-flow.ts +17 -2
  49. package/src/console/server/iso-at.test.ts +9 -0
  50. package/src/console/server/iso-at.ts +11 -0
  51. package/src/console/server/security-headers.ts +6 -3
  52. package/src/console/server/spa-proxy.test.ts +1 -0
  53. package/src/console/server/sql-catalog.ts +147 -7
  54. package/src/console/server/sql-rls.test.ts +16 -0
  55. package/src/console/server/state.ts +8 -0
  56. package/src/console/server/store.test.ts +7 -4
  57. package/src/console/server/store.ts +142 -58
  58. package/src/console/ui-next/dist/assets/agent-disclosure-C0X1fbWF.js +1 -0
  59. package/src/console/ui-next/dist/assets/{cache-glyph-BanhLsEY.js → cache-glyph-F1FI122b.js} +1 -1
  60. package/src/console/ui-next/dist/assets/{call-pii-button-bqkxMrJH.js → call-pii-button-DEDCl_j3.js} +1 -1
  61. package/src/console/ui-next/dist/assets/collapsible-LPqGvfoz.js +1 -0
  62. package/src/console/ui-next/dist/assets/{confirm-sheet-DDCRmG62.js → confirm-sheet-3ptDzXbz.js} +1 -1
  63. package/src/console/ui-next/dist/assets/{duration-tone-oiRxPVsZ.js → duration-tone-sC3lGABz.js} +1 -1
  64. package/src/console/ui-next/dist/assets/{explorer-empty-HjTnVQoR.js → explorer-empty-BvYrygyO.js} +1 -1
  65. package/src/console/ui-next/dist/assets/flows-page-Dluo1Bd7.js +1 -0
  66. package/src/console/ui-next/dist/assets/{highlighted-json-CvDPvveV.js → highlighted-json-CS_O8L-r.js} +1 -1
  67. package/src/console/ui-next/dist/assets/http-method--sWDdXSB.js +1 -0
  68. package/src/console/ui-next/dist/assets/index-Ca3HZMVq.js +66 -0
  69. package/src/console/ui-next/dist/assets/index-UtTDRQpS.css +2 -0
  70. package/src/console/ui-next/dist/assets/{link-DF7SZ9Ek.js → link-COeyggt-.js} +1 -1
  71. package/src/console/ui-next/dist/assets/{observability-page-BEZDzyYh.js → observability-page-HK9-BO8z.js} +2 -2
  72. package/src/console/ui-next/dist/assets/react-D8E3mtu1.js +1 -0
  73. package/src/console/ui-next/dist/assets/replica-lag-DBIFFf7d.js +18 -0
  74. package/src/console/ui-next/dist/assets/sheet-form-Dnwa5oOR.js +1 -0
  75. package/src/console/ui-next/dist/assets/shortcut-keys-CoqwEIi0.js +1 -0
  76. package/src/console/ui-next/dist/assets/skeleton-BHcfDYcb.js +1 -0
  77. package/src/console/ui-next/dist/assets/store-page-BSohH3wM.js +41 -0
  78. package/src/console/ui-next/dist/assets/trace-detail-sheet-CFIGRnnA.js +2 -0
  79. package/src/console/ui-next/dist/assets/tree-expand-toggle-BgLl34w7.js +54 -0
  80. package/src/console/ui-next/dist/assets/units-page-B0cFE76A.js +1 -0
  81. package/src/console/ui-next/dist/assets/{use-vault-list-Cl79j_ku.js → use-vault-list-BLbZhzyF.js} +1 -1
  82. package/src/console/ui-next/dist/assets/vault-page-B1SbYe10.js +2 -0
  83. package/src/console/ui-next/dist/index.html +6 -6
  84. package/src/console/ui-next/src/client.ts +35 -7
  85. package/src/console/ui-next/src/components/motion/table/index.tsx +16 -0
  86. package/src/console/ui-next/src/components/motion/table/types.ts +4 -0
  87. package/src/console/ui-next/src/components/ui/kbd.tsx +2 -2
  88. package/src/console/ui-next/src/features/flows/traces/http-method.ts +19 -0
  89. package/src/console/ui-next/src/features/flows/traces/trace-detail.test.ts +19 -1
  90. package/src/console/ui-next/src/features/flows/traces/traces-pane.tsx +10 -3
  91. package/src/console/ui-next/src/features/store/detail/browse-section.tsx +45 -1
  92. package/src/console/ui-next/src/features/store/detail/resource-panel.tsx +78 -16
  93. package/src/console/ui-next/src/features/store/detail/store-row-detail-sheet.tsx +155 -14
  94. package/src/console/ui-next/src/features/store/explorer/store-tree.tsx +33 -133
  95. package/src/console/ui-next/src/features/store/files/file-preview.tsx +4 -1
  96. package/src/console/ui-next/src/features/store/grid/rls-policy-sheet.tsx +326 -165
  97. package/src/console/ui-next/src/features/store/grid/sql-insert-sheet.tsx +6 -0
  98. package/src/console/ui-next/src/features/store/grid/store-data-grid.tsx +50 -3
  99. package/src/console/ui-next/src/features/store/lib/files-meta.test.ts +3 -0
  100. package/src/console/ui-next/src/features/store/lib/files-meta.ts +12 -0
  101. package/src/console/ui-next/src/features/store/lib/query-defaults.test.ts +25 -0
  102. package/src/console/ui-next/src/features/store/lib/query-defaults.ts +85 -3
  103. package/src/console/ui-next/src/features/store/lib/query-gate.ts +30 -4
  104. package/src/console/ui-next/src/features/store/lib/rls-policy.test.ts +139 -0
  105. package/src/console/ui-next/src/features/store/lib/rls-policy.ts +223 -21
  106. package/src/console/ui-next/src/features/store/lib/schema-graph.ts +4 -0
  107. package/src/console/ui-next/src/features/store/lib/sql-catalog.test.ts +24 -0
  108. package/src/console/ui-next/src/features/store/lib/sql-catalog.ts +19 -0
  109. package/src/console/ui-next/src/features/store/lib/store-tree.test.ts +6 -0
  110. package/src/console/ui-next/src/features/store/lib/store-tree.ts +13 -0
  111. package/src/console/ui-next/src/features/store/query/query-console.tsx +48 -14
  112. package/src/console/ui-next/src/features/store/query/query-gate-menu.tsx +19 -20
  113. package/src/console/ui-next/src/features/store/query/query-gate-parts.tsx +2 -1
  114. package/src/console/ui-next/src/features/store/query/query-results.tsx +110 -83
  115. package/src/console/ui-next/src/features/store/schema/schema-table-node.tsx +17 -0
  116. package/src/console/ui-next/src/features/units/call/call-api-panel.tsx +112 -72
  117. package/src/console/ui-next/src/features/units/lib/contract-input.test.ts +206 -1
  118. package/src/console/ui-next/src/features/units/lib/contract-input.ts +55 -0
  119. package/src/console/ui-next/src/features/vault/lib/types.ts +1 -1
  120. package/src/docker/compose-health.test.ts +27 -0
  121. package/src/docker/compose-health.ts +19 -3
  122. package/src/docker/docker.test.ts +3 -0
  123. package/src/docker/recipes/pgdog.ts +2 -0
  124. package/src/drivers/clock-postgres.ts +13 -9
  125. package/src/drivers/instances-postgres.ts +4 -6
  126. package/src/drivers/journal-postgres.ts +13 -9
  127. package/src/drivers/pg-rls.test.ts +87 -0
  128. package/src/drivers/pg-rls.ts +233 -7
  129. package/src/drivers/pglite.ts +26 -3
  130. package/src/drivers/postgres.test.ts +23 -0
  131. package/src/drivers/postgres.ts +133 -9
  132. package/src/drivers/types.ts +10 -0
  133. package/src/elements/clock/declare.ts +26 -1
  134. package/src/elements/gate/boot.ts +5 -5
  135. package/src/elements/gate/declare.ts +36 -4
  136. package/src/elements/gate/flatten.ts +1 -1
  137. package/src/elements/gate/runtime.ts +1 -1
  138. package/src/elements/store/declare.ts +9 -6
  139. package/src/elements/store/emit-drizzle.ts +44 -8
  140. package/src/elements/store/resource.test.ts +1 -1
  141. package/src/elements/store/rls-identity.test.ts +95 -0
  142. package/src/elements/store/rls-identity.ts +116 -0
  143. package/src/elements/store/runtime.ts +4 -0
  144. package/src/elements/store/schema-decl.test.ts +37 -0
  145. package/src/elements/store/schema-decl.ts +161 -0
  146. package/src/elements/store/seed.ts +1 -1
  147. package/src/elements/store/sql-condition.test.ts +12 -0
  148. package/src/elements/store/sql-condition.ts +26 -2
  149. package/src/elements/store/sql-rls-isolation.test.ts +165 -0
  150. package/src/elements/store/sql-rls-stamp.test.ts +117 -0
  151. package/src/elements/store/sql-session.ts +81 -3
  152. package/src/elements/store/upsert-app.test.ts +1 -1
  153. package/src/elements/store.ts +14 -0
  154. package/src/http.ts +20 -3
  155. package/src/index.ts +10 -1
  156. package/src/kernel/adopt-barrel-fresh.test.ts +1 -1
  157. package/src/kernel/adopt-routes.ts +30 -2
  158. package/src/kernel/app.ts +131 -14
  159. package/src/kernel/auto-cache.test.ts +8 -8
  160. package/src/kernel/auto-registry.test.ts +49 -9
  161. package/src/kernel/boot.test.ts +4 -4
  162. package/src/kernel/boot.ts +2 -2
  163. package/src/kernel/effects-stamping.test.ts +3 -3
  164. package/src/kernel/element-registries.ts +12 -5
  165. package/src/kernel/errors.ts +24 -0
  166. package/src/kernel/flow-units.ts +64 -0
  167. package/src/kernel/flow.ts +29 -17
  168. package/src/kernel/fx.ts +23 -0
  169. package/src/kernel/horizontal-child.ts +4 -4
  170. package/src/kernel/http-path-pending.ts +20 -0
  171. package/src/kernel/http-resource.ts +4 -0
  172. package/src/kernel/http-stream.test.ts +4 -4
  173. package/src/kernel/pipeline.test.ts +2 -2
  174. package/src/kernel/plugin-elements.test.ts +1 -1
  175. package/src/kernel/plugin-needs.test.ts +1 -1
  176. package/src/kernel/ready.test.ts +2 -2
  177. package/src/kernel/resource-mount.test.ts +14 -0
  178. package/src/kernel/run-duration.test.ts +2 -2
  179. package/src/kernel/stamp-http.test.ts +79 -0
  180. package/src/kernel/stamp-http.ts +75 -0
  181. package/src/kernel/triggers.ts +52 -27
  182. package/src/manifest/types.ts +13 -0
  183. package/src/release/measure.ts +2 -2
  184. package/src/runtime/json-code-block.test.ts +2 -2
  185. package/src/runtime/serve.test.ts +4 -4
  186. package/src/term.test.ts +52 -4
  187. package/src/term.ts +125 -17
  188. package/src/test/reset-element-registries.ts +8 -3
  189. package/src/cli/ask-seed.test.ts +0 -96
  190. package/src/cli/ask-seed.ts +0 -82
  191. package/src/console/ui-next/dist/assets/collapsible-DYb0xU8C.js +0 -1
  192. package/src/console/ui-next/dist/assets/flows-page-DxDsOd4f.js +0 -1
  193. package/src/console/ui-next/dist/assets/http-method-CJCBYL2j.js +0 -1
  194. package/src/console/ui-next/dist/assets/index-Ce6WKWKM.js +0 -66
  195. package/src/console/ui-next/dist/assets/index-D4Ldtj79.css +0 -2
  196. package/src/console/ui-next/dist/assets/query-gate-parts-1m8m1iNp.js +0 -2
  197. package/src/console/ui-next/dist/assets/react-B1ML8gxg.js +0 -1
  198. package/src/console/ui-next/dist/assets/replica-lag-C8_BRt2x.js +0 -18
  199. package/src/console/ui-next/dist/assets/sheet-form-D-ata7jy.js +0 -1
  200. package/src/console/ui-next/dist/assets/shortcut-keys-DO4IsVqv.js +0 -1
  201. package/src/console/ui-next/dist/assets/skeleton-CL_X0GCj.js +0 -1
  202. package/src/console/ui-next/dist/assets/store-page-v3LXdYpr.js +0 -45
  203. package/src/console/ui-next/dist/assets/trace-detail-sheet-DFLFfUUX.js +0 -2
  204. package/src/console/ui-next/dist/assets/units-page-C0gW6Kdo.js +0 -1
  205. package/src/console/ui-next/dist/assets/vault-page-DuKzqwzW.js +0 -2
@@ -0,0 +1,609 @@
1
+ /**
2
+ * Expand flow `in` / `out` AST into Manifest JSON Schema.
3
+ *
4
+ * Covers handwritten `z.object({…})` and `drizzle-orm/zod`
5
+ * (`createSelectSchema` / `createInsertSchema` / `createUpdateSchema`,
6
+ * plus Keel's `tableZod(table).select|insert|update`). Opaque identifiers
7
+ * stay `"…"` at the call site.
8
+ *
9
+ * @see https://orm.drizzle.team/docs/zod
10
+ */
11
+
12
+ import type { DeclaredColumn } from "../manifest/types.ts";
13
+ import {
14
+ identifierName,
15
+ stringArg,
16
+ type AstNode,
17
+ type CallExpression,
18
+ type Identifier,
19
+ type InferBinding,
20
+ type Literal,
21
+ } from "./effects-infer.ts";
22
+
23
+ /** Table columns already extracted from `store.schema.table`. */
24
+ export type SchemaTableColumns = Record<string, DeclaredColumn>;
25
+
26
+ /** One `const Name = <init>` and the file it was declared in. */
27
+ export type ConstInit = {
28
+ readonly node: AstNode;
29
+ readonly file: string;
30
+ };
31
+
32
+ /** Bindings the expander may follow. */
33
+ export interface SchemaExpandContext {
34
+ /** File that contains the expression being expanded. */
35
+ readonly filePath: string;
36
+ /**
37
+ * Resolve `const Name` — same-file first, then a unique project-wide
38
+ * export. Shared names like `createIn` must not leak across units.
39
+ *
40
+ * @param name - Binding name
41
+ * @param fromFile - File of the referring expression
42
+ */
43
+ readonly resolveConst: (name: string, fromFile: string) => ConstInit | undefined;
44
+ /** Store / table bindings (`spaces` → table name). */
45
+ readonly bindings: ReadonlyMap<string, InferBinding>;
46
+ /**
47
+ * Columns for a table binding or SQL name.
48
+ *
49
+ * @param name - Binding (`spaces`) or table (`spaces`)
50
+ */
51
+ readonly tableColumns: (name: string) => SchemaTableColumns | undefined;
52
+ }
53
+
54
+ const DRIZZLE_SCHEMA_FNS = {
55
+ createSelectSchema: "select",
56
+ createInsertSchema: "insert",
57
+ createUpdateSchema: "update",
58
+ } as const;
59
+
60
+ type DrizzleShape = (typeof DRIZZLE_SCHEMA_FNS)[keyof typeof DRIZZLE_SCHEMA_FNS];
61
+
62
+ const LIST_IN_PROPERTIES: Record<string, Record<string, unknown>> = {
63
+ q: { type: "string" },
64
+ search: { type: "string" },
65
+ limit: { type: "integer", minimum: 1, maximum: 100 },
66
+ order: { type: "string" },
67
+ orderBy: { type: "string" },
68
+ select: { type: "string" },
69
+ or: { type: "string" },
70
+ and: { type: "string" },
71
+ offset: { type: "integer", minimum: 0 },
72
+ cursor: { type: "string" },
73
+ };
74
+
75
+ const MAX_DEPTH = 24;
76
+
77
+ /**
78
+ * Expand a schema expression to a JSON Schema object, or `undefined`
79
+ * when the node is opaque (fixture placeholder, unknown helper).
80
+ *
81
+ * @param node - `in` / `out` AST
82
+ * @param ctx - Const inits + table columns
83
+ */
84
+ export function jsonSchemaFromAst(
85
+ node: AstNode | undefined,
86
+ ctx: SchemaExpandContext,
87
+ ): Record<string, unknown> | undefined {
88
+ return expand(node, ctx, new Set(), 0);
89
+ }
90
+
91
+ /**
92
+ * JSON Schema object for a `store.schema.table` under drizzle-orm/zod rules.
93
+ *
94
+ * - **select** — every column; `notNull` columns are required
95
+ * - **insert** — omit generated primary keys; required = not-null without default
96
+ * - **update** — every mutable column, all optional
97
+ *
98
+ * @param columns - Declared columns
99
+ * @param shape - drizzle-orm/zod factory
100
+ */
101
+ export function jsonSchemaFromTableColumns(
102
+ columns: SchemaTableColumns,
103
+ shape: DrizzleShape,
104
+ ): Record<string, unknown> {
105
+ const properties: Record<string, Record<string, unknown>> = {};
106
+ const required: string[] = [];
107
+ for (const [name, col] of Object.entries(columns)) {
108
+ if (shape !== "select" && isGeneratedPrimaryKey(col)) continue;
109
+ properties[name] = columnToProperty(col);
110
+ if (shape === "select" && col.nullable === false) required.push(name);
111
+ if (shape === "insert" && col.nullable === false && !columnHasDefault(col)) {
112
+ required.push(name);
113
+ }
114
+ }
115
+ return objectSchema(properties, required);
116
+ }
117
+
118
+ function expand(
119
+ node: AstNode | undefined,
120
+ ctx: SchemaExpandContext,
121
+ seen: Set<string>,
122
+ depth: number,
123
+ ): Record<string, unknown> | undefined {
124
+ if (!node || depth > MAX_DEPTH) return undefined;
125
+ const unwrapped = unwrapTsExpr(node);
126
+
127
+ if (unwrapped.type === "Identifier") {
128
+ const name = (unwrapped as Identifier).name;
129
+ if (seen.has(name)) return undefined;
130
+ const next = new Set(seen);
131
+ next.add(name);
132
+ const init = ctx.resolveConst(name, ctx.filePath);
133
+ if (init) return expand(init.node, { ...ctx, filePath: init.file }, next, depth + 1);
134
+ return undefined;
135
+ }
136
+
137
+ if (unwrapped.type === "MemberExpression") {
138
+ const member = unwrapped as AstNode & { object: AstNode; property: AstNode };
139
+ const prop = identifierName(member.property);
140
+ const drizzle = drizzleShapeFromMember(member, ctx, seen, depth);
141
+ if (drizzle) return drizzle;
142
+ if (prop === "shape" || prop === "def") {
143
+ return expand(member.object, ctx, seen, depth + 1);
144
+ }
145
+ return undefined;
146
+ }
147
+
148
+ if (unwrapped.type !== "CallExpression") return undefined;
149
+ return expandCall(unwrapped as CallExpression, ctx, seen, depth);
150
+ }
151
+
152
+ function expandCall(
153
+ call: CallExpression,
154
+ ctx: SchemaExpandContext,
155
+ seen: Set<string>,
156
+ depth: number,
157
+ ): Record<string, unknown> | undefined {
158
+ const callee = unwrapTsExpr(call.callee);
159
+ const fnName = identifierName(callee);
160
+
161
+ if (fnName && fnName in DRIZZLE_SCHEMA_FNS) {
162
+ const shape = DRIZZLE_SCHEMA_FNS[fnName as keyof typeof DRIZZLE_SCHEMA_FNS];
163
+ return expandDrizzleFactory(call, shape, ctx, seen, depth);
164
+ }
165
+ if (fnName === "tableZod") {
166
+ const table = resolveTableName(call.arguments[0], ctx);
167
+ if (!table) return undefined;
168
+ const cols = ctx.tableColumns(table);
169
+ return cols ? jsonSchemaFromTableColumns(cols, "select") : undefined;
170
+ }
171
+ if (fnName === "listIn") return expandListIn(call, ctx, seen, depth);
172
+ if (fnName === "pageOut" || fnName === "listPage") {
173
+ const items = expand(call.arguments[0], ctx, seen, depth + 1);
174
+ return items ? { type: "array", items } : undefined;
175
+ }
176
+
177
+ if (callee.type !== "MemberExpression") return undefined;
178
+ const member = callee as AstNode & { object: AstNode; property: AstNode };
179
+ const method = identifierName(member.property);
180
+ if (!method) return undefined;
181
+
182
+ if (identifierName(member.object) === "z") {
183
+ return expandZodRoot(method, call, ctx, seen, depth);
184
+ }
185
+
186
+ const inner = expand(member.object, ctx, seen, depth + 1);
187
+ if (!inner) return undefined;
188
+ return applyZodMethod(inner, method, call, ctx, seen, depth);
189
+ }
190
+
191
+ function expandZodRoot(
192
+ method: string,
193
+ call: CallExpression,
194
+ ctx: SchemaExpandContext,
195
+ seen: Set<string>,
196
+ depth: number,
197
+ ): Record<string, unknown> | undefined {
198
+ if (method === "object") {
199
+ const propsNode = objectArg(call.arguments[0]);
200
+ if (!propsNode) return { type: "object", properties: {} };
201
+ return objectFromZodProps(propsNode, ctx, seen, depth);
202
+ }
203
+ if (method === "array") {
204
+ const items = expand(call.arguments[0], ctx, seen, depth + 1);
205
+ return items ? { type: "array", items } : { type: "array" };
206
+ }
207
+ if (method === "enum") {
208
+ const values = stringArrayArg(call.arguments[0]);
209
+ return values ? { type: "string", enum: values } : { type: "string" };
210
+ }
211
+ if (method === "literal") {
212
+ const value = literalValue(call.arguments[0]);
213
+ if (value === undefined) return undefined;
214
+ return { const: value, type: literalJsonType(value) };
215
+ }
216
+ if (method === "intersection") {
217
+ const left = expand(call.arguments[0], ctx, seen, depth + 1);
218
+ const right = expand(call.arguments[1], ctx, seen, depth + 1);
219
+ if (!left && !right) return undefined;
220
+ return mergeObjects(left, right);
221
+ }
222
+ if (method === "union" || method === "discriminatedUnion") return undefined;
223
+ return leafFromZodType(method);
224
+ }
225
+
226
+ function applyZodMethod(
227
+ schema: Record<string, unknown>,
228
+ method: string,
229
+ call: CallExpression,
230
+ ctx: SchemaExpandContext,
231
+ seen: Set<string>,
232
+ depth: number,
233
+ ): Record<string, unknown> | undefined {
234
+ if (
235
+ method === "optional" ||
236
+ method === "nullable" ||
237
+ method === "nullish" ||
238
+ method === "default" ||
239
+ method === "catch" ||
240
+ method === "readonly" ||
241
+ method === "brand" ||
242
+ method === "describe" ||
243
+ method === "meta" ||
244
+ method === "refine" ||
245
+ method === "superRefine" ||
246
+ method === "transform" ||
247
+ method === "pipe" ||
248
+ method === "passthrough" ||
249
+ method === "strict" ||
250
+ method === "strip" ||
251
+ method === "catchall"
252
+ ) {
253
+ if (method === "describe") {
254
+ const text = stringArg(call.arguments[0]);
255
+ return text ? { ...schema, description: text } : schema;
256
+ }
257
+ return schema;
258
+ }
259
+ if (method === "min") {
260
+ const n = numberArg(call.arguments[0]);
261
+ if (n === undefined) return schema;
262
+ return schema.type === "string" ? { ...schema, minLength: n } : { ...schema, minimum: n };
263
+ }
264
+ if (method === "max") {
265
+ const n = numberArg(call.arguments[0]);
266
+ if (n === undefined) return schema;
267
+ return schema.type === "string" ? { ...schema, maxLength: n } : { ...schema, maximum: n };
268
+ }
269
+ if (method === "length") {
270
+ const n = numberArg(call.arguments[0]);
271
+ return n === undefined ? schema : { ...schema, minLength: n, maxLength: n };
272
+ }
273
+ if (method === "int") return { ...schema, type: "integer" };
274
+ if (method === "email") return { ...schema, type: "string", format: "email" };
275
+ if (method === "uuid") return { ...schema, type: "string", format: "uuid" };
276
+ if (method === "url") return { ...schema, type: "string", format: "uri" };
277
+ if (method === "datetime") return { ...schema, type: "string", format: "date-time" };
278
+ if (method === "pick") return pickOmit(schema, call.arguments[0], "pick");
279
+ if (method === "omit") return pickOmit(schema, call.arguments[0], "omit");
280
+ if (method === "extend") {
281
+ const extra = objectArg(call.arguments[0]);
282
+ if (!extra) return schema;
283
+ return mergeObjects(schema, objectFromZodProps(extra, ctx, seen, depth));
284
+ }
285
+ if (method === "partial") {
286
+ const { required: _req, ...rest } = schema;
287
+ return rest;
288
+ }
289
+ return schema;
290
+ }
291
+
292
+ function expandDrizzleFactory(
293
+ call: CallExpression,
294
+ shape: DrizzleShape,
295
+ ctx: SchemaExpandContext,
296
+ seen: Set<string>,
297
+ depth: number,
298
+ ): Record<string, unknown> | undefined {
299
+ const table = resolveTableName(call.arguments[0], ctx);
300
+ if (!table) return undefined;
301
+ const cols = ctx.tableColumns(table);
302
+ if (!cols) return undefined;
303
+ const base = jsonSchemaFromTableColumns(cols, shape);
304
+ const refine = objectArg(call.arguments[1]);
305
+ if (!refine) return base;
306
+ return applyDrizzleRefinements(base, refine, ctx, seen, depth);
307
+ }
308
+
309
+ /**
310
+ * Second-arg refinements: a Zod schema overwrites the field (docs);
311
+ * a callback is ignored (we cannot apply `.max()` without the inner schema).
312
+ */
313
+ function applyDrizzleRefinements(
314
+ base: Record<string, unknown>,
315
+ refine: AstNode,
316
+ ctx: SchemaExpandContext,
317
+ seen: Set<string>,
318
+ depth: number,
319
+ ): Record<string, unknown> {
320
+ const properties = { ...((base.properties as Record<string, Record<string, unknown>>) ?? {}) };
321
+ const required = Array.isArray(base.required) ? [...(base.required as string[])] : [];
322
+ for (const prop of objectProperties(refine)) {
323
+ const key = propKey(prop);
324
+ const value = (prop as AstNode & { value?: AstNode }).value;
325
+ if (!key || !value) continue;
326
+ if (value.type === "ArrowFunctionExpression" || value.type === "FunctionExpression") {
327
+ continue;
328
+ }
329
+ const overwritten = expand(value, ctx, seen, depth + 1);
330
+ if (!overwritten) continue;
331
+ properties[key] = overwritten;
332
+ if (!("const" in overwritten) && overwritten.type !== undefined && !required.includes(key)) {
333
+ // Overwrite replaces nullability (docs) — keep required if the replacement
334
+ // is a non-optional object field we cannot see `.optional()` on.
335
+ }
336
+ }
337
+ return objectSchema(
338
+ properties,
339
+ required.filter((name) => name in properties),
340
+ );
341
+ }
342
+
343
+ function drizzleShapeFromMember(
344
+ member: AstNode & { object: AstNode; property: AstNode },
345
+ ctx: SchemaExpandContext,
346
+ seen: Set<string>,
347
+ depth: number,
348
+ ): Record<string, unknown> | undefined {
349
+ const prop = identifierName(member.property);
350
+ if (prop !== "select" && prop !== "insert" && prop !== "update") return undefined;
351
+ const table = tableZodTable(member.object, ctx, seen, depth);
352
+ if (!table) return undefined;
353
+ const cols = ctx.tableColumns(table);
354
+ return cols ? jsonSchemaFromTableColumns(cols, prop) : undefined;
355
+ }
356
+
357
+ function tableZodTable(
358
+ node: AstNode,
359
+ ctx: SchemaExpandContext,
360
+ seen: Set<string>,
361
+ depth: number,
362
+ ): string | undefined {
363
+ const unwrapped = unwrapTsExpr(node);
364
+ if (unwrapped.type === "Identifier") {
365
+ const name = (unwrapped as Identifier).name;
366
+ if (seen.has(`zod:${name}`)) return undefined;
367
+ const init = ctx.resolveConst(name, ctx.filePath);
368
+ if (!init) return undefined;
369
+ const next = new Set(seen);
370
+ next.add(`zod:${name}`);
371
+ return tableZodTable(init.node, { ...ctx, filePath: init.file }, next, depth + 1);
372
+ }
373
+ if (unwrapped.type !== "CallExpression") return undefined;
374
+ const call = unwrapped as CallExpression;
375
+ if (identifierName(call.callee) !== "tableZod") return undefined;
376
+ return resolveTableName(call.arguments[0], ctx);
377
+ }
378
+
379
+ function expandListIn(
380
+ call: CallExpression,
381
+ ctx: SchemaExpandContext,
382
+ seen: Set<string>,
383
+ depth: number,
384
+ ): Record<string, unknown> {
385
+ const properties = { ...LIST_IN_PROPERTIES };
386
+ const extra = objectArg(call.arguments[1]);
387
+ if (extra) {
388
+ const added = objectFromZodProps(extra, ctx, seen, depth);
389
+ const extraProps = (added.properties ?? {}) as Record<string, Record<string, unknown>>;
390
+ Object.assign(properties, extraProps);
391
+ }
392
+ return { type: "object", properties };
393
+ }
394
+
395
+ function objectFromZodProps(
396
+ obj: AstNode,
397
+ ctx: SchemaExpandContext,
398
+ seen: Set<string>,
399
+ depth: number,
400
+ ): Record<string, unknown> {
401
+ const properties: Record<string, Record<string, unknown>> = {};
402
+ const required: string[] = [];
403
+ for (const prop of objectProperties(obj)) {
404
+ const key = propKey(prop);
405
+ const value = (prop as AstNode & { value?: AstNode }).value;
406
+ if (!key || !value) continue;
407
+ const schema = expand(value, ctx, seen, depth + 1);
408
+ if (!schema) continue;
409
+ properties[key] = schema;
410
+ if (!isOptionalExpr(value)) required.push(key);
411
+ }
412
+ return objectSchema(properties, required);
413
+ }
414
+
415
+ function isOptionalExpr(node: AstNode): boolean {
416
+ let cur: AstNode | undefined = unwrapTsExpr(node);
417
+ while (cur && cur.type === "CallExpression") {
418
+ const call = cur as CallExpression;
419
+ const callee = unwrapTsExpr(call.callee);
420
+ if (callee.type === "MemberExpression") {
421
+ const member = callee as AstNode & { object: AstNode; property: AstNode };
422
+ const method = identifierName(member.property);
423
+ if (method === "optional" || method === "nullish") return true;
424
+ cur = member.object;
425
+ continue;
426
+ }
427
+ break;
428
+ }
429
+ return false;
430
+ }
431
+
432
+ function resolveTableName(node: AstNode | undefined, ctx: SchemaExpandContext): string | undefined {
433
+ const id = identifierName(node ? unwrapTsExpr(node) : undefined);
434
+ if (!id) return undefined;
435
+ const binding = ctx.bindings.get(id);
436
+ if (binding?.kind === "table") return binding.ref;
437
+ if (ctx.tableColumns(id)) return id;
438
+ return undefined;
439
+ }
440
+
441
+ function columnToProperty(col: DeclaredColumn): Record<string, unknown> {
442
+ const type = col.type === "integer" ? "integer" : "string";
443
+ return {
444
+ type,
445
+ ...(col.description !== undefined ? { description: col.description } : {}),
446
+ ...(col.pii ? { pii: true } : {}),
447
+ ...(col.sensitive ? { sensitive: true } : {}),
448
+ ...(col.primaryKey ? { primaryKey: true } : {}),
449
+ ...(col.unique ? { unique: true } : {}),
450
+ ...(col.references ? { references: col.references } : {}),
451
+ };
452
+ }
453
+
454
+ function isGeneratedPrimaryKey(col: DeclaredColumn): boolean {
455
+ // OKE `field.text().primaryKey().defaultFn(id)` — same as drizzle
456
+ // `generatedAlwaysAsIdentity()`: omitted from insert / update.
457
+ return col.primaryKey === true;
458
+ }
459
+
460
+ function columnHasDefault(col: DeclaredColumn): boolean {
461
+ return col.default !== undefined;
462
+ }
463
+
464
+ function pickOmit(
465
+ schema: Record<string, unknown>,
466
+ keysNode: AstNode | undefined,
467
+ mode: "pick" | "omit",
468
+ ): Record<string, unknown> {
469
+ const keys = trueKeys(keysNode);
470
+ if (!keys) return schema;
471
+ const properties = {
472
+ ...((schema.properties as Record<string, Record<string, unknown>>) ?? {}),
473
+ };
474
+ const next: Record<string, Record<string, unknown>> = {};
475
+ for (const [name, prop] of Object.entries(properties)) {
476
+ const keep = mode === "pick" ? keys.has(name) : !keys.has(name);
477
+ if (keep) next[name] = prop;
478
+ }
479
+ const required = Array.isArray(schema.required)
480
+ ? (schema.required as string[]).filter((name) => name in next)
481
+ : [];
482
+ return objectSchema(next, required);
483
+ }
484
+
485
+ function mergeObjects(
486
+ left: Record<string, unknown> | undefined,
487
+ right: Record<string, unknown> | undefined,
488
+ ): Record<string, unknown> {
489
+ const leftProps = (left?.properties as Record<string, Record<string, unknown>> | undefined) ?? {};
490
+ const rightProps =
491
+ (right?.properties as Record<string, Record<string, unknown>> | undefined) ?? {};
492
+ const properties = { ...leftProps, ...rightProps };
493
+ const required = [
494
+ ...new Set([
495
+ ...(Array.isArray(left?.required) ? (left.required as string[]) : []),
496
+ ...(Array.isArray(right?.required) ? (right.required as string[]) : []),
497
+ ]),
498
+ ].filter((name) => name in properties);
499
+ return objectSchema(properties, required);
500
+ }
501
+
502
+ function objectSchema(
503
+ properties: Record<string, Record<string, unknown>>,
504
+ required: readonly string[],
505
+ ): Record<string, unknown> {
506
+ return {
507
+ type: "object",
508
+ properties,
509
+ ...(required.length > 0 ? { required: [...required] } : {}),
510
+ };
511
+ }
512
+
513
+ /**
514
+ * Default `listIn({ mode: "offset" })` query shape (pagination on HTTP meta).
515
+ */
516
+ export function defaultListInSchema(): Record<string, unknown> {
517
+ return { type: "object", properties: { ...LIST_IN_PROPERTIES } };
518
+ }
519
+
520
+ function leafFromZodType(method: string): Record<string, unknown> | undefined {
521
+ if (method === "string") return { type: "string" };
522
+ if (method === "number") return { type: "number" };
523
+ if (method === "int") return { type: "integer" };
524
+ if (method === "bigint") return { type: "integer" };
525
+ if (method === "boolean") return { type: "boolean" };
526
+ if (method === "date") return { type: "string", format: "date-time" };
527
+ if (method === "unknown" || method === "any") return {};
528
+ if (method === "void" || method === "undefined" || method === "null") return { type: "null" };
529
+ if (method === "coerce") return undefined;
530
+ return undefined;
531
+ }
532
+
533
+ function trueKeys(node: AstNode | undefined): Set<string> | undefined {
534
+ const obj = objectArg(node);
535
+ if (!obj) return undefined;
536
+ const keys = new Set<string>();
537
+ for (const prop of objectProperties(obj)) {
538
+ const key = propKey(prop);
539
+ const value = (prop as AstNode & { value?: AstNode }).value;
540
+ if (!key) continue;
541
+ if (!value || (value.type === "Literal" && (value as Literal).value === true)) {
542
+ keys.add(key);
543
+ }
544
+ }
545
+ return keys;
546
+ }
547
+
548
+ function stringArrayArg(node: AstNode | undefined): string[] | undefined {
549
+ if (!node || node.type !== "ArrayExpression") return undefined;
550
+ const els = (node as AstNode & { elements?: readonly (AstNode | null)[] }).elements ?? [];
551
+ const out: string[] = [];
552
+ for (const el of els) {
553
+ const s = stringArg(el ?? undefined);
554
+ if (s) out.push(s);
555
+ }
556
+ return out.length > 0 ? out : undefined;
557
+ }
558
+
559
+ function numberArg(node: AstNode | undefined): number | undefined {
560
+ if (!node || node.type !== "Literal") return undefined;
561
+ const v = (node as Literal).value;
562
+ return typeof v === "number" ? v : undefined;
563
+ }
564
+
565
+ function literalValue(node: AstNode | undefined): string | number | boolean | null | undefined {
566
+ if (!node || node.type !== "Literal") return undefined;
567
+ const v = (node as Literal).value;
568
+ if (v === null || typeof v === "string" || typeof v === "number" || typeof v === "boolean") {
569
+ return v;
570
+ }
571
+ return undefined;
572
+ }
573
+
574
+ function literalJsonType(value: string | number | boolean | null): string {
575
+ if (value === null) return "null";
576
+ if (typeof value === "string") return "string";
577
+ if (typeof value === "number") return Number.isInteger(value) ? "integer" : "number";
578
+ return "boolean";
579
+ }
580
+
581
+ function unwrapTsExpr(node: AstNode): AstNode {
582
+ if (node.type === "TSAsExpression" || node.type === "TSTypeAssertion") {
583
+ const expr = (node as AstNode & { expression?: AstNode }).expression;
584
+ if (expr) return unwrapTsExpr(expr);
585
+ }
586
+ return node;
587
+ }
588
+
589
+ function objectArg(node: AstNode | undefined): AstNode | undefined {
590
+ if (!node) return undefined;
591
+ const unwrapped = unwrapTsExpr(node);
592
+ return unwrapped.type === "ObjectExpression" ? unwrapped : undefined;
593
+ }
594
+
595
+ function objectProperties(obj: AstNode): AstNode[] {
596
+ return ((obj as AstNode & { properties?: AstNode[] }).properties ?? []).filter(
597
+ (p) => p.type === "Property" || p.type === "ObjectProperty",
598
+ );
599
+ }
600
+
601
+ function propKey(prop: AstNode): string | undefined {
602
+ const key = (prop as AstNode & { key?: AstNode }).key;
603
+ if (!key) return undefined;
604
+ if (key.type === "Identifier") return (key as Identifier).name;
605
+ if (key.type === "Literal" && typeof (key as Literal).value === "string") {
606
+ return (key as Literal).value as string;
607
+ }
608
+ return undefined;
609
+ }
@@ -10,7 +10,7 @@ import { PUBLIC_CONSOLE_FLOWS } from "./public-flows.ts";
10
10
 
11
11
  /**
12
12
  * Bind an HTTP trigger to a flow without registering on the global `on` list.
13
- * Attaches `.gate.public` or {@link consoleOperatorGate} when the trigger
13
+ * Attaches `.public()` or {@link consoleOperatorGate} when the trigger
14
14
  * has no declared auth posture yet.
15
15
  *
16
16
  * @param trigger - HTTP trigger
@@ -21,7 +21,7 @@ export function bindHttp(trigger: HttpTrigger, flowDef: AnyFlowDef): Binding {
21
21
  trigger.gates.length > 0
22
22
  ? trigger
23
23
  : PUBLIC_CONSOLE_FLOWS.has(flowDef.name)
24
- ? trigger.gate.public
24
+ ? trigger.public()
25
25
  : trigger.gate(consoleOperatorGate);
26
26
  const normalized = normalizeTrigger(withPosture);
27
27
  const list = flowDef.triggers as Trigger[];
@@ -391,6 +391,9 @@ describe("console serve security", () => {
391
391
  expect(res.status).toBe(200);
392
392
  const csp = res.headers.get("content-security-policy") ?? "";
393
393
  expect(csp).toContain("frame-ancestors 'none'");
394
+ expect(csp).toContain("frame-src 'self' blob:");
395
+ expect(csp).toContain("img-src 'self' data: blob:");
396
+ expect(csp).toContain("media-src 'self' blob:");
394
397
  expect(csp).toBe(CONSOLE_CSP);
395
398
  });
396
399