@softize/opus 8.6.6

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 (286) hide show
  1. package/CHANGELOG.md +1616 -0
  2. package/LICENSE +21 -0
  3. package/README.md +113 -0
  4. package/bin/cli.mjs +528 -0
  5. package/bin/lib/check.mjs +307 -0
  6. package/bin/lib/components.mjs +151 -0
  7. package/bin/lib/create.mjs +208 -0
  8. package/bin/lib/db-check-runner.mjs +86 -0
  9. package/bin/lib/db-migrate-runner.mjs +89 -0
  10. package/bin/lib/db-scaffold-runner.mjs +84 -0
  11. package/bin/lib/db.mjs +261 -0
  12. package/bin/lib/docs-include.mjs +48 -0
  13. package/bin/lib/gen-dicts.mjs +134 -0
  14. package/bin/lib/gen-docs.mjs +288 -0
  15. package/bin/lib/gen-manifest.mjs +102 -0
  16. package/bin/lib/gen-openapi.mjs +195 -0
  17. package/bin/lib/gen-runner.mjs +472 -0
  18. package/bin/lib/gen-stubs.mjs +463 -0
  19. package/bin/lib/gen.mjs +311 -0
  20. package/bin/lib/init.mjs +514 -0
  21. package/bin/lib/introspect.mjs +107 -0
  22. package/bin/lib/mcp.mjs +85 -0
  23. package/bin/lib/postinstall.mjs +56 -0
  24. package/docs/chat-event-protocol.md +85 -0
  25. package/docs/code-style.md +16 -0
  26. package/docs/data-layer.md +246 -0
  27. package/docs/ownership-vs-shadcn-lock.md +102 -0
  28. package/docs/protocol.md +2053 -0
  29. package/docs/releasing.md +110 -0
  30. package/docs/shellnav.md +131 -0
  31. package/package.json +338 -0
  32. package/registry/hooks/hooks.json +26 -0
  33. package/registry/hooks/link-memory-on-start.mjs +46 -0
  34. package/registry/hooks/opus-check-on-stop.mjs +114 -0
  35. package/registry/skills/create-action/SKILL.md +49 -0
  36. package/registry/skills/create-action/scaffold.mjs +122 -0
  37. package/registry/templates/app/_gitignore +3 -0
  38. package/registry/templates/app/_npmrc +1 -0
  39. package/registry/templates/app/_opus/_gitignore +5 -0
  40. package/registry/templates/app/_prettierrc.json +6 -0
  41. package/registry/templates/app/index.html +13 -0
  42. package/registry/templates/app/opus.config.ts +16 -0
  43. package/registry/templates/app/package.json +43 -0
  44. package/registry/templates/app/pnpm-workspace.yaml +11 -0
  45. package/registry/templates/app/public/favicon.svg +4 -0
  46. package/registry/templates/app/src/App.tsx +37 -0
  47. package/registry/templates/app/src/domains/tasks/actions/list.test.ts +34 -0
  48. package/registry/templates/app/src/domains/tasks/actions/list.ts +33 -0
  49. package/registry/templates/app/src/domains/tasks/index.ts +13 -0
  50. package/registry/templates/app/src/index.css +18 -0
  51. package/registry/templates/app/src/main.tsx +25 -0
  52. package/registry/templates/app/tsconfig.json +20 -0
  53. package/registry/templates/app/vite.config.ts +46 -0
  54. package/registry/templates/monorepo/_gitignore +3 -0
  55. package/registry/templates/monorepo/_npmrc +1 -0
  56. package/registry/templates/monorepo/package.json +9 -0
  57. package/registry/templates/monorepo/pnpm-workspace.yaml +14 -0
  58. package/src/ai/ask.ts +64 -0
  59. package/src/ai/drivers/anthropic.ts +309 -0
  60. package/src/ai/index.ts +17 -0
  61. package/src/audit/drivers/console.ts +117 -0
  62. package/src/audit/drivers/pg.ts +172 -0
  63. package/src/audit/index.ts +51 -0
  64. package/src/auth/drivers/better-auth.ts +103 -0
  65. package/src/auth/drivers/jwt.ts +188 -0
  66. package/src/auth/index.ts +9 -0
  67. package/src/client/drivers/fetch.ts +202 -0
  68. package/src/client/index.ts +22 -0
  69. package/src/core/actions.ts +110 -0
  70. package/src/core/audit.ts +239 -0
  71. package/src/core/contracts.ts +137 -0
  72. package/src/core/domain.ts +310 -0
  73. package/src/core/errors.ts +181 -0
  74. package/src/core/index.ts +174 -0
  75. package/src/core/logical-type.ts +31 -0
  76. package/src/core/reactions.ts +81 -0
  77. package/src/core/runtime.ts +1167 -0
  78. package/src/core/schedules.ts +41 -0
  79. package/src/core/types.ts +1356 -0
  80. package/src/data/drivers/kysely.ts +389 -0
  81. package/src/data/index.ts +10 -0
  82. package/src/data/readonly-pool.ts +160 -0
  83. package/src/dsl/eval.ts +136 -0
  84. package/src/dsl/index.ts +29 -0
  85. package/src/dsl/kysely.ts +230 -0
  86. package/src/dsl/loads.ts +123 -0
  87. package/src/dsl/parser.ts +423 -0
  88. package/src/dsl/types.ts +113 -0
  89. package/src/events/drivers/mitt.ts +70 -0
  90. package/src/events/index.ts +9 -0
  91. package/src/log/drivers/pino.ts +57 -0
  92. package/src/log/index.ts +9 -0
  93. package/src/mcp/index.ts +62 -0
  94. package/src/queue/drivers/bullmq.ts +190 -0
  95. package/src/queue/index.ts +9 -0
  96. package/src/scheduler/drivers/node-cron.ts +93 -0
  97. package/src/scheduler/every.ts +45 -0
  98. package/src/scheduler/index.ts +9 -0
  99. package/src/schema/drivers/zod.ts +765 -0
  100. package/src/schema/entity.ts +439 -0
  101. package/src/schema/format/locale.ts +144 -0
  102. package/src/schema/index.ts +65 -0
  103. package/src/schema/openapi.ts +302 -0
  104. package/src/schema/scaffold.ts +160 -0
  105. package/src/server/drivers/fastify.ts +224 -0
  106. package/src/server/drivers/node.ts +386 -0
  107. package/src/server/index.ts +142 -0
  108. package/src/storage/drivers/fs.ts +90 -0
  109. package/src/storage/drivers/s3.ts +117 -0
  110. package/src/storage/index.ts +27 -0
  111. package/src/testing/fake.ts +298 -0
  112. package/src/testing/index.ts +324 -0
  113. package/src/ui/components/patterns/action-form-card.tsx +48 -0
  114. package/src/ui/components/patterns/action-list-dialog.tsx +93 -0
  115. package/src/ui/components/patterns/app-shell.tsx +227 -0
  116. package/src/ui/components/patterns/confirm.tsx +226 -0
  117. package/src/ui/components/patterns/data-state.tsx +75 -0
  118. package/src/ui/components/patterns/form-dialog.tsx +64 -0
  119. package/src/ui/components/patterns/form.tsx +584 -0
  120. package/src/ui/components/patterns/list.tsx +1488 -0
  121. package/src/ui/components/patterns/page.tsx +46 -0
  122. package/src/ui/components/patterns/section-shell.tsx +246 -0
  123. package/src/ui/components/patterns/shell-nav.tsx +150 -0
  124. package/src/ui/components/patterns/sidebar.tsx +89 -0
  125. package/src/ui/components/patterns/split.tsx +93 -0
  126. package/src/ui/components/patterns/trigger.tsx +196 -0
  127. package/src/ui/components/patterns/view.tsx +84 -0
  128. package/src/ui/components/primitives/accordion.tsx +64 -0
  129. package/src/ui/components/primitives/alert-dialog.tsx +190 -0
  130. package/src/ui/components/primitives/alert.tsx +116 -0
  131. package/src/ui/components/primitives/aspect-ratio.tsx +9 -0
  132. package/src/ui/components/primitives/avatar.tsx +107 -0
  133. package/src/ui/components/primitives/badge.tsx +37 -0
  134. package/src/ui/components/primitives/breadcrumb.tsx +109 -0
  135. package/src/ui/components/primitives/button-group.tsx +83 -0
  136. package/src/ui/components/primitives/button.tsx +102 -0
  137. package/src/ui/components/primitives/calendar.tsx +218 -0
  138. package/src/ui/components/primitives/card.tsx +56 -0
  139. package/src/ui/components/primitives/carousel.tsx +239 -0
  140. package/src/ui/components/primitives/chat.tsx +407 -0
  141. package/src/ui/components/primitives/checkbox.tsx +30 -0
  142. package/src/ui/components/primitives/collapsible.tsx +31 -0
  143. package/src/ui/components/primitives/command.tsx +182 -0
  144. package/src/ui/components/primitives/composer.tsx +121 -0
  145. package/src/ui/components/primitives/copyable.tsx +50 -0
  146. package/src/ui/components/primitives/dialog.tsx +147 -0
  147. package/src/ui/components/primitives/drawer.tsx +141 -0
  148. package/src/ui/components/primitives/empty.tsx +104 -0
  149. package/src/ui/components/primitives/field.tsx +246 -0
  150. package/src/ui/components/primitives/icon-picker.tsx +180 -0
  151. package/src/ui/components/primitives/input-group.tsx +168 -0
  152. package/src/ui/components/primitives/input-otp.tsx +75 -0
  153. package/src/ui/components/primitives/input.tsx +72 -0
  154. package/src/ui/components/primitives/item.tsx +193 -0
  155. package/src/ui/components/primitives/kbd.tsx +28 -0
  156. package/src/ui/components/primitives/label.tsx +22 -0
  157. package/src/ui/components/primitives/markdown.tsx +35 -0
  158. package/src/ui/components/primitives/menu.tsx +255 -0
  159. package/src/ui/components/primitives/pagination.tsx +127 -0
  160. package/src/ui/components/primitives/popover.tsx +87 -0
  161. package/src/ui/components/primitives/progress.tsx +29 -0
  162. package/src/ui/components/primitives/radio-group.tsx +43 -0
  163. package/src/ui/components/primitives/resizable.tsx +51 -0
  164. package/src/ui/components/primitives/scroll-area.tsx +56 -0
  165. package/src/ui/components/primitives/select.tsx +479 -0
  166. package/src/ui/components/primitives/separator.tsx +26 -0
  167. package/src/ui/components/primitives/skeleton.tsx +13 -0
  168. package/src/ui/components/primitives/slider.tsx +61 -0
  169. package/src/ui/components/primitives/sonner.tsx +46 -0
  170. package/src/ui/components/primitives/spinner.tsx +29 -0
  171. package/src/ui/components/primitives/switch.tsx +33 -0
  172. package/src/ui/components/primitives/table.tsx +114 -0
  173. package/src/ui/components/primitives/tabs.tsx +104 -0
  174. package/src/ui/components/primitives/textarea.tsx +18 -0
  175. package/src/ui/components/primitives/toggle-group.tsx +81 -0
  176. package/src/ui/components/primitives/toggle.tsx +45 -0
  177. package/src/ui/components/primitives/tooltip.tsx +55 -0
  178. package/src/ui/components/primitives/truncate.tsx +49 -0
  179. package/src/ui/docs/DocBrowser.tsx +90 -0
  180. package/src/ui/docs/changelog.tsx +80 -0
  181. package/src/ui/docs/content/accordion.md +86 -0
  182. package/src/ui/docs/content/action-form-card.md +24 -0
  183. package/src/ui/docs/content/action-form-dialog.md +30 -0
  184. package/src/ui/docs/content/action-form.md +125 -0
  185. package/src/ui/docs/content/action-list-dialog.md +68 -0
  186. package/src/ui/docs/content/action-list.md +194 -0
  187. package/src/ui/docs/content/action-trigger.md +72 -0
  188. package/src/ui/docs/content/action-view.md +47 -0
  189. package/src/ui/docs/content/actions.md +138 -0
  190. package/src/ui/docs/content/ai.md +112 -0
  191. package/src/ui/docs/content/alert-dialog.md +73 -0
  192. package/src/ui/docs/content/alert.md +69 -0
  193. package/src/ui/docs/content/app-shell.md +155 -0
  194. package/src/ui/docs/content/aspect-ratio.md +66 -0
  195. package/src/ui/docs/content/audit.md +84 -0
  196. package/src/ui/docs/content/auth.md +70 -0
  197. package/src/ui/docs/content/avatar.md +94 -0
  198. package/src/ui/docs/content/badge.md +48 -0
  199. package/src/ui/docs/content/breadcrumb.md +87 -0
  200. package/src/ui/docs/content/button-group.md +71 -0
  201. package/src/ui/docs/content/button.md +60 -0
  202. package/src/ui/docs/content/calendar.md +62 -0
  203. package/src/ui/docs/content/card.md +49 -0
  204. package/src/ui/docs/content/carousel.md +85 -0
  205. package/src/ui/docs/content/chat.md +69 -0
  206. package/src/ui/docs/content/checkbox.md +75 -0
  207. package/src/ui/docs/content/cli.md +58 -0
  208. package/src/ui/docs/content/collapsible.md +64 -0
  209. package/src/ui/docs/content/command.md +56 -0
  210. package/src/ui/docs/content/composer.md +50 -0
  211. package/src/ui/docs/content/confirm.md +120 -0
  212. package/src/ui/docs/content/copyable.md +30 -0
  213. package/src/ui/docs/content/customization.md +110 -0
  214. package/src/ui/docs/content/cycle.md +34 -0
  215. package/src/ui/docs/content/data-state.md +47 -0
  216. package/src/ui/docs/content/data.md +99 -0
  217. package/src/ui/docs/content/dialog.md +60 -0
  218. package/src/ui/docs/content/drawer.md +55 -0
  219. package/src/ui/docs/content/empty.md +66 -0
  220. package/src/ui/docs/content/events.md +61 -0
  221. package/src/ui/docs/content/field.md +58 -0
  222. package/src/ui/docs/content/getting-started.md +109 -0
  223. package/src/ui/docs/content/icon-picker.md +51 -0
  224. package/src/ui/docs/content/input-group.md +78 -0
  225. package/src/ui/docs/content/input-otp.md +72 -0
  226. package/src/ui/docs/content/input.md +78 -0
  227. package/src/ui/docs/content/item.md +84 -0
  228. package/src/ui/docs/content/kbd.md +62 -0
  229. package/src/ui/docs/content/label.md +32 -0
  230. package/src/ui/docs/content/log.md +55 -0
  231. package/src/ui/docs/content/markdown.md +41 -0
  232. package/src/ui/docs/content/mcp.md +44 -0
  233. package/src/ui/docs/content/menu.md +114 -0
  234. package/src/ui/docs/content/microcopy.md +83 -0
  235. package/src/ui/docs/content/page.md +34 -0
  236. package/src/ui/docs/content/pagination.md +99 -0
  237. package/src/ui/docs/content/popover.md +49 -0
  238. package/src/ui/docs/content/progress.md +69 -0
  239. package/src/ui/docs/content/queue.md +62 -0
  240. package/src/ui/docs/content/radio-group.md +77 -0
  241. package/src/ui/docs/content/resizable.md +86 -0
  242. package/src/ui/docs/content/router.md +56 -0
  243. package/src/ui/docs/content/runtime.md +77 -0
  244. package/src/ui/docs/content/scheduler.md +66 -0
  245. package/src/ui/docs/content/scroll-area.md +89 -0
  246. package/src/ui/docs/content/section-shell.md +121 -0
  247. package/src/ui/docs/content/select.md +342 -0
  248. package/src/ui/docs/content/separator.md +33 -0
  249. package/src/ui/docs/content/sidebar.md +38 -0
  250. package/src/ui/docs/content/skeleton.md +34 -0
  251. package/src/ui/docs/content/slider.md +64 -0
  252. package/src/ui/docs/content/spinner.md +37 -0
  253. package/src/ui/docs/content/split.md +33 -0
  254. package/src/ui/docs/content/storage.md +69 -0
  255. package/src/ui/docs/content/switch.md +69 -0
  256. package/src/ui/docs/content/table.md +102 -0
  257. package/src/ui/docs/content/tabs.md +94 -0
  258. package/src/ui/docs/content/testing.md +89 -0
  259. package/src/ui/docs/content/textarea.md +30 -0
  260. package/src/ui/docs/content/toast.md +67 -0
  261. package/src/ui/docs/content/toggle-group.md +81 -0
  262. package/src/ui/docs/content/toggle.md +72 -0
  263. package/src/ui/docs/content/tokens.md +171 -0
  264. package/src/ui/docs/content/tooltip.md +50 -0
  265. package/src/ui/docs/content/truncate.md +37 -0
  266. package/src/ui/docs/content/ui.md +40 -0
  267. package/src/ui/docs/content/upgrading.md +48 -0
  268. package/src/ui/docs/doc-client.tsx +214 -0
  269. package/src/ui/docs/doc.tsx +301 -0
  270. package/src/ui/docs/folder.tsx +149 -0
  271. package/src/ui/docs/index.ts +21 -0
  272. package/src/ui/docs/markdown.tsx +130 -0
  273. package/src/ui/docs/md-raw.d.ts +4 -0
  274. package/src/ui/docs/plugin.ts +104 -0
  275. package/src/ui/docs/registry.tsx +424 -0
  276. package/src/ui/docs/standalone.tsx +107 -0
  277. package/src/ui/drivers/react.tsx +627 -0
  278. package/src/ui/index.ts +92 -0
  279. package/src/ui/lib/cn.ts +10 -0
  280. package/src/ui/lib/zod-pt-br.ts +38 -0
  281. package/src/ui/meta.ts +412 -0
  282. package/src/ui/react.tsx +235 -0
  283. package/src/ui/router.ts +96 -0
  284. package/src/ui/theme.css +234 -0
  285. package/src/vite/design.ts +652 -0
  286. package/src/vite/index.ts +8 -0
@@ -0,0 +1,386 @@
1
+ /**
2
+ * @softize/opus/server/node — driver connect/node-http
3
+ *
4
+ * Implementa `ServerAdapter` sobre `IncomingMessage`/`ServerResponse` puros,
5
+ * sem framework: rotas vivem numa Map interna e o `handler` resultante encaixa
6
+ * em qualquer stack connect-style (middlewares do vite, express, ou um
7
+ * `http.createServer` direto). É o driver que o `opusDesign()` usa pra servir
8
+ * `/api` in-process no preview de design.
9
+ *
10
+ * Uso:
11
+ * import http from 'node:http'
12
+ * import { createRuntime } from '@softize/opus/core'
13
+ * import { nodeServer } from '@softize/opus/server/node'
14
+ *
15
+ * const node = nodeServer()
16
+ * const runtime = createRuntime({ server: node.adapter, auth: ... })
17
+ * runtime.register([...])
18
+ * await runtime.start()
19
+ * http.createServer(node.handler).listen(3000)
20
+ *
21
+ * Semântica do `handler`:
22
+ * - rota montada (actions sob `apiPrefix` + endpoints) → responde;
23
+ * - path sob `apiPrefix` sem rota → 404 em envelope opus;
24
+ * - fora do `apiPrefix` → `next()` quando fornecido, senão 404 em envelope.
25
+ */
26
+
27
+ import type { IncomingMessage, ServerResponse } from 'node:http'
28
+ import type {
29
+ ActionDef,
30
+ ActionResult,
31
+ ContextBase,
32
+ EndpointSpec,
33
+ ErrorInput,
34
+ RuntimeRef,
35
+ ServerAdapter,
36
+ } from '../../core/index.ts'
37
+ import { error } from '../../core/index.ts'
38
+ import {
39
+ toOpenAPISpec,
40
+ type OpenAPIInfo,
41
+ type OpenAPIServer,
42
+ } from '../../schema/openapi.ts'
43
+ import {
44
+ httpStatusFor,
45
+ inputSourceFor,
46
+ methodFor,
47
+ pathFor,
48
+ serializeResult,
49
+ shouldMountPublicly,
50
+ successStatusFor,
51
+ } from '../index.ts'
52
+
53
+ // =============================================================================
54
+ // Options
55
+ // =============================================================================
56
+
57
+ export interface NodeServerOptions {
58
+ /** Prefixo do path. Default '/api'. */
59
+ apiPrefix?: string
60
+
61
+ /**
62
+ * Endpoints (health/ready/openapi) montados automaticamente no `init` do
63
+ * runtime. Equivale a chamar `adapter.mountEndpoints(spec)` após o start.
64
+ */
65
+ endpoints?: EndpointSpec
66
+
67
+ /**
68
+ * Info do OpenAPI document (title, version, description). Quando ausente,
69
+ * usa um default genérico — sobrescreva pra publicar API decente.
70
+ */
71
+ openapiInfo?: OpenAPIInfo
72
+
73
+ /** Servers do OpenAPI document. */
74
+ openapiServers?: OpenAPIServer[]
75
+ }
76
+
77
+ export interface NodeServerHandle {
78
+ /** Vai no `createRuntime({ server })`. */
79
+ adapter: ServerAdapter
80
+
81
+ /** Middleware connect-style: pluga em vite/express/http.createServer. */
82
+ handler: (req: IncomingMessage, res: ServerResponse, next?: () => void) => void
83
+
84
+ /** True se há rota montada pra `method` + `pathname` (actions e endpoints).
85
+ * Deixa quem embala (ex.: opusDesign) distinguir 404 de rota antes de delegar. */
86
+ hasRoute: (method: string, pathname: string) => boolean
87
+ }
88
+
89
+ // =============================================================================
90
+ // Factory
91
+ // =============================================================================
92
+
93
+ type RouteFn = (req: IncomingMessage, res: ServerResponse, url: URL) => Promise<void>
94
+
95
+ export function nodeServer(options: NodeServerOptions = {}): NodeServerHandle {
96
+ const {
97
+ apiPrefix = '/api',
98
+ endpoints,
99
+ openapiInfo = { title: 'tbdlib API', version: '0.0.0' },
100
+ openapiServers,
101
+ } = options
102
+
103
+ let runtime: RuntimeRef | undefined
104
+ const routes = new Map<string, RouteFn>()
105
+ const mountedActions: ActionDef[] = []
106
+
107
+ const requireRuntime = (): RuntimeRef => {
108
+ /* v8 ignore next 3 — defensivo; init() é sempre chamado antes de mount/etc */
109
+ if (runtime === undefined) {
110
+ throw new Error('nodeServer: runtime not initialized (init not called)')
111
+ }
112
+ return runtime
113
+ }
114
+
115
+ const mountEndpoints = (spec: EndpointSpec): void => {
116
+ mountHealth(routes, spec)
117
+ mountReady(routes, spec, requireRuntime)
118
+ mountOpenAPI(routes, spec, mountedActions, {
119
+ info: openapiInfo,
120
+ ...(openapiServers !== undefined ? { servers: openapiServers } : {}),
121
+ apiPrefix,
122
+ })
123
+ }
124
+
125
+ const adapter: ServerAdapter = {
126
+ name: 'node',
127
+ kind: 'server',
128
+
129
+ init(rt) {
130
+ runtime = rt
131
+ if (endpoints !== undefined) mountEndpoints(endpoints)
132
+ },
133
+
134
+ mount(action) {
135
+ if (!shouldMountPublicly(action)) return
136
+ mountedActions.push(action)
137
+
138
+ const method = methodFor(action)
139
+ const path = pathFor(action, apiPrefix)
140
+ const source = inputSourceFor(action)
141
+
142
+ routes.set(`${method} ${path}`, async (req, res, url) => {
143
+ const rt = requireRuntime()
144
+ const requestId = crypto.randomUUID()
145
+ const ctx = await resolveCtx(rt, req, requestId)
146
+
147
+ let input: unknown
148
+ if (source === 'body') {
149
+ const raw = await readBody(req)
150
+ // Body vazio vira `undefined` — o schema de input da action decide.
151
+ if (raw.length === 0) {
152
+ input = undefined
153
+ } else {
154
+ try {
155
+ input = JSON.parse(raw)
156
+ } catch {
157
+ sendResult(
158
+ res,
159
+ 400,
160
+ driverResult(
161
+ { code: 'server.invalid_json', category: 'validation', message: 'Request body is not valid JSON' },
162
+ action.name,
163
+ requestId,
164
+ ),
165
+ )
166
+ return
167
+ }
168
+ }
169
+ } else {
170
+ // Strings cruas, mesma semântica do `req.query` do fastify — paridade
171
+ // com prod; os contratos coagem (z.coerce etc).
172
+ input = Object.fromEntries(url.searchParams)
173
+ }
174
+
175
+ const result = await rt.execute(action.name, input, ctx)
176
+ const status = result.ok
177
+ ? successStatusFor(action)
178
+ : httpStatusFor(result.error)
179
+
180
+ sendResult(res, status, result)
181
+ })
182
+ },
183
+
184
+ mountEndpoints,
185
+ }
186
+
187
+ const handler = (
188
+ req: IncomingMessage,
189
+ res: ServerResponse,
190
+ next?: () => void,
191
+ ): void => {
192
+ const url = new URL(req.url ?? '/', 'http://localhost')
193
+ const route = routes.get(`${req.method ?? 'GET'} ${url.pathname}`)
194
+
195
+ if (route !== undefined) {
196
+ route(req, res, url).catch((err) => {
197
+ /* v8 ignore next 7 — defensivo; execute() não lança (devolve envelope) */
198
+ sendResult(
199
+ res,
200
+ 500,
201
+ driverResult(
202
+ { code: 'server.unhandled', category: 'internal', message: String(err) },
203
+ deriveActionName(url.pathname, apiPrefix),
204
+ crypto.randomUUID(),
205
+ ),
206
+ )
207
+ })
208
+ return
209
+ }
210
+
211
+ if (url.pathname === apiPrefix || url.pathname.startsWith(`${apiPrefix}/`)) {
212
+ sendResult(res, 404, routeNotFound(url.pathname, apiPrefix))
213
+ return
214
+ }
215
+
216
+ if (next !== undefined) {
217
+ next()
218
+ return
219
+ }
220
+ sendResult(res, 404, routeNotFound(url.pathname, apiPrefix))
221
+ }
222
+
223
+ return {
224
+ adapter,
225
+ handler,
226
+ hasRoute: (method, pathname) => routes.has(`${method} ${pathname}`),
227
+ }
228
+ }
229
+
230
+ // =============================================================================
231
+ // Context resolution
232
+ // =============================================================================
233
+
234
+ async function resolveCtx(
235
+ rt: RuntimeRef,
236
+ req: IncomingMessage,
237
+ requestId: string,
238
+ ): Promise<ContextBase> {
239
+ const base = await resolveAuthBase(rt, req)
240
+ return {
241
+ ...base,
242
+ requestId,
243
+ provenance: {
244
+ kind: 'http',
245
+ userId: base.user?.id ?? null,
246
+ requestId,
247
+ },
248
+ }
249
+ }
250
+
251
+ async function resolveAuthBase(
252
+ rt: RuntimeRef,
253
+ req: IncomingMessage,
254
+ ): Promise<{ user: ContextBase['user']; tenantId: string | null; can: ContextBase['can'] }> {
255
+ if (rt.auth === undefined) {
256
+ return { user: null, tenantId: null, can: () => false }
257
+ }
258
+ const resolved = await rt.auth.resolveContext(req)
259
+ return {
260
+ user: resolved.user,
261
+ tenantId: resolved.tenantId ?? null,
262
+ can: resolved.can,
263
+ }
264
+ }
265
+
266
+ // =============================================================================
267
+ // Request/response plumbing
268
+ // =============================================================================
269
+
270
+ async function readBody(req: IncomingMessage): Promise<string> {
271
+ // setEncoding evita quebrar UTF-8 multi-byte na fronteira de chunks.
272
+ req.setEncoding('utf8')
273
+ let body = ''
274
+ for await (const chunk of req) body += chunk as string
275
+ return body
276
+ }
277
+
278
+ function sendResult(res: ServerResponse, status: number, result: ActionResult<unknown>): void {
279
+ res.statusCode = status
280
+ res.setHeader('content-type', 'application/json; charset=utf-8')
281
+ res.end(JSON.stringify(serializeResult(result)))
282
+ }
283
+
284
+ /**
285
+ * Envelope de erro produzido pelo DRIVER (sem passar pelo execute). Carrega
286
+ * `meta` completo — o fetch client só reconhece envelope com `meta` presente.
287
+ */
288
+ function driverResult(
289
+ input: ErrorInput,
290
+ action: string,
291
+ requestId: string,
292
+ ): ActionResult<never> {
293
+ return {
294
+ ok: false,
295
+ error: error(input),
296
+ meta: { actionId: crypto.randomUUID(), action, durationMs: 0, requestId },
297
+ }
298
+ }
299
+
300
+ function routeNotFound(pathname: string, apiPrefix: string): ActionResult<never> {
301
+ return driverResult(
302
+ { code: 'server.route_not_found', category: 'not_found', message: `No route for ${pathname}` },
303
+ deriveActionName(pathname, apiPrefix),
304
+ crypto.randomUUID(),
305
+ )
306
+ }
307
+
308
+ /** Nome de action derivado do path (pro `meta.action` de erros do driver). */
309
+ function deriveActionName(pathname: string, apiPrefix: string): string {
310
+ if (pathname !== apiPrefix && !pathname.startsWith(`${apiPrefix}/`)) return 'unknown'
311
+ const name = pathname
312
+ .slice(apiPrefix.length)
313
+ .replace(/^\/+/, '')
314
+ .split('/')
315
+ .map(decodeURIComponent)
316
+ .join('.')
317
+ return name.length > 0 ? name : 'unknown'
318
+ }
319
+
320
+ // =============================================================================
321
+ // Endpoints
322
+ // =============================================================================
323
+
324
+ function mountHealth(routes: Map<string, RouteFn>, spec: EndpointSpec): void {
325
+ const cfg = spec.health
326
+ if (cfg === false) return
327
+ const path = pathFromSpec(cfg, '/health')
328
+ routes.set(`GET ${path}`, async (_req, res) => {
329
+ sendJson(res, 200, { ok: true })
330
+ })
331
+ }
332
+
333
+ function mountReady(
334
+ routes: Map<string, RouteFn>,
335
+ spec: EndpointSpec,
336
+ getRuntime: () => RuntimeRef,
337
+ ): void {
338
+ const cfg = spec.ready
339
+ if (cfg === false) return
340
+ const path = pathFromSpec(cfg, '/ready')
341
+ routes.set(`GET ${path}`, async (_req, res) => {
342
+ const status = await getRuntime().healthCheck()
343
+ sendJson(res, status.ok ? 200 : 503, status)
344
+ })
345
+ }
346
+
347
+ function pathFromSpec(
348
+ cfg: boolean | { path?: string } | undefined,
349
+ fallback: string,
350
+ ): string {
351
+ if (typeof cfg === 'object' && cfg.path !== undefined) return cfg.path
352
+ return fallback
353
+ }
354
+
355
+ function mountOpenAPI(
356
+ routes: Map<string, RouteFn>,
357
+ spec: EndpointSpec,
358
+ actions: ActionDef[],
359
+ options: {
360
+ info: OpenAPIInfo
361
+ servers?: OpenAPIServer[]
362
+ apiPrefix: string
363
+ },
364
+ ): void {
365
+ const cfg = spec.openapi
366
+ if (cfg === false) return
367
+ const path =
368
+ typeof cfg === 'object' && cfg.path !== undefined ? cfg.path : '/openapi.json'
369
+ routes.set(`GET ${path}`, async (_req, res) => {
370
+ sendJson(
371
+ res,
372
+ 200,
373
+ toOpenAPISpec(actions, {
374
+ info: options.info,
375
+ ...(options.servers !== undefined ? { servers: options.servers } : {}),
376
+ apiPrefix: options.apiPrefix,
377
+ }),
378
+ )
379
+ })
380
+ }
381
+
382
+ function sendJson(res: ServerResponse, status: number, body: unknown): void {
383
+ res.statusCode = status
384
+ res.setHeader('content-type', 'application/json; charset=utf-8')
385
+ res.end(JSON.stringify(body))
386
+ }
@@ -0,0 +1,142 @@
1
+ /**
2
+ * @softize/opus/server — shared helpers for server drivers.
3
+ *
4
+ * Drivers (Fastify, Hono, etc) consomem este módulo pra:
5
+ * - mapear ActionError.category → HTTP status
6
+ * - derivar route path + method de uma ActionDef
7
+ * - serializar ActionResult em response body
8
+ *
9
+ * Lógica específica de driver (registro de rotas, parsing de request) vive
10
+ * em `src/drivers/<driver>.ts`.
11
+ */
12
+
13
+ import type {
14
+ ActionDef,
15
+ ActionError,
16
+ ActionResult,
17
+ ErrorCategory,
18
+ } from '../core/index.ts'
19
+
20
+ // =============================================================================
21
+ // HTTP status mapping
22
+ // =============================================================================
23
+
24
+ /**
25
+ * Mapping default `ErrorCategory` → HTTP status code.
26
+ * Ver §3 do protocolo.
27
+ */
28
+ const DEFAULT_HTTP_STATUS: Record<ErrorCategory, number> = {
29
+ validation: 400,
30
+ authentication: 401,
31
+ authorization: 403,
32
+ not_found: 404,
33
+ conflict: 409,
34
+ rate_limit: 429,
35
+ dependency: 502,
36
+ internal: 500,
37
+ }
38
+
39
+ /**
40
+ * Retorna o HTTP status code apropriado pra um `ActionError`.
41
+ */
42
+ export function httpStatusFor(error: ActionError): number {
43
+ return DEFAULT_HTTP_STATUS[error.category]
44
+ }
45
+
46
+ /**
47
+ * Retorna o HTTP status pra success result. Default 200; action pode
48
+ * declarar `successStatus` (ex: 201 pra create).
49
+ */
50
+ export function successStatusFor(action: ActionDef): number {
51
+ return action.successStatus ?? 200
52
+ }
53
+
54
+ // =============================================================================
55
+ // Route convention
56
+ // =============================================================================
57
+
58
+ /**
59
+ * Método HTTP padrão por kind:
60
+ * - simple, form → POST (mutações tipicamente)
61
+ * - search, view → GET (leitura)
62
+ */
63
+ export type HttpMethod = 'GET' | 'POST'
64
+
65
+ export function methodFor(action: ActionDef): HttpMethod {
66
+ return action.kind === 'simple' || action.kind === 'form' ? 'POST' : 'GET'
67
+ }
68
+
69
+ /**
70
+ * Path derivado do nome da action. Converte dots em segments.
71
+ * - 'deal.archive' → '/deal/archive'
72
+ * - 'crm.deal.list' → '/crm/deal/list'
73
+ * - 'auth.sessionRefresh' → '/auth/sessionRefresh'
74
+ *
75
+ * `prefix` adicionado se passado (default '/api').
76
+ */
77
+ export function pathFor(action: ActionDef, prefix = '/api'): string {
78
+ const segments = action.name.split('.').map(encodeURIComponent)
79
+ return `${prefix}/${segments.join('/')}`
80
+ }
81
+
82
+ // =============================================================================
83
+ // Body shape
84
+ // =============================================================================
85
+
86
+ /**
87
+ * Decide se o input vem do body (POST) ou query string (GET).
88
+ * Drivers usam pra extrair input do request.
89
+ */
90
+ export function inputSourceFor(action: ActionDef): 'body' | 'query' {
91
+ return methodFor(action) === 'POST' ? 'body' : 'query'
92
+ }
93
+
94
+ // =============================================================================
95
+ // Result serialization
96
+ // =============================================================================
97
+
98
+ /**
99
+ * Envelope JSON-serializável de `ActionResult`. Driver chama `JSON.stringify`
100
+ * direto no retorno desta função.
101
+ *
102
+ * `cause` em error é serializado como string (ver §3 do protocolo).
103
+ */
104
+ export function serializeResult<T>(result: ActionResult<T>): unknown {
105
+ if (result.ok) {
106
+ return { ok: true, data: result.data, meta: result.meta }
107
+ }
108
+ return {
109
+ ok: false,
110
+ error: serializeError(result.error),
111
+ meta: result.meta,
112
+ }
113
+ }
114
+
115
+ function serializeError(err: ActionError): Record<string, unknown> {
116
+ const out: Record<string, unknown> = {
117
+ code: err.code,
118
+ category: err.category,
119
+ message: err.message,
120
+ severity: err.severity,
121
+ retriable: err.retriable,
122
+ }
123
+ if (err.i18nKey !== undefined) out.i18nKey = err.i18nKey
124
+ if (err.i18nParams !== undefined) out.i18nParams = err.i18nParams
125
+ if (err.field !== undefined) out.field = err.field
126
+ if (err.issues !== undefined) out.issues = err.issues
127
+ if (err.cause !== undefined) out.cause = String(err.cause)
128
+ if (err.meta !== undefined) out.meta = err.meta
129
+ return out
130
+ }
131
+
132
+ // =============================================================================
133
+ // Internal action filter
134
+ // =============================================================================
135
+
136
+ /**
137
+ * Actions com `internal: true` não são expostas via REST público.
138
+ * Driver checa antes de chamar mount().
139
+ */
140
+ export function shouldMountPublicly(action: ActionDef): boolean {
141
+ return action.internal !== true
142
+ }
@@ -0,0 +1,90 @@
1
+ /**
2
+ * @softize/opus/storage/fs — driver de disco local (EXPERIMENTAL).
3
+ *
4
+ * Guarda cada objeto como arquivo sob `root` (a key vira o caminho relativo) e o
5
+ * `contentType` num sidecar `<arquivo>.meta.json` (o filesystem não tem metadata).
6
+ * Pra dev e single-host; produção multi-host prefere `storage/s3`.
7
+ *
8
+ * `url()` exige `baseUrl` (o app serve o `root` estaticamente por conta própria —
9
+ * o driver não sobe servidor): sem `baseUrl`, url() falha explicando.
10
+ */
11
+
12
+ import { promises as fs } from 'node:fs'
13
+ import path from 'node:path'
14
+
15
+ import type { StorageAdapter, StorageObject, StoragePutOptions } from '../../core/index.ts'
16
+ import { normalizeKey } from '../index.ts'
17
+
18
+ export interface FsStorageOptions {
19
+ /** Diretório raiz dos objetos (criado sob demanda). */
20
+ root: string
21
+ /** Base pública de `url()` (ex.: `https://app.exemplo.com/arquivos`). Sem ela, url() falha. */
22
+ baseUrl?: string
23
+ /** Nome do adapter (default 'fs'). */
24
+ name?: string
25
+ }
26
+
27
+ /** Sidecar de metadata: `<arquivo>.meta.json` ao lado do objeto. */
28
+ function metaPathOf(filePath: string): string {
29
+ return `${filePath}.meta.json`
30
+ }
31
+
32
+ export function fsStorage(options: FsStorageOptions): StorageAdapter {
33
+ const root = path.resolve(options.root)
34
+ const baseUrl = options.baseUrl?.replace(/\/+$/, '')
35
+
36
+ // normalizeKey barra traversal/absoluto; o resolve + guard abaixo é o cinto extra.
37
+ function filePathOf(key: string): string {
38
+ const full = path.resolve(root, normalizeKey(key))
39
+ if (!full.startsWith(`${root}${path.sep}`)) {
40
+ throw new Error(`Key de storage fora do root: "${key}".`)
41
+ }
42
+ return full
43
+ }
44
+
45
+ return {
46
+ name: options.name ?? 'fs',
47
+ kind: 'storage',
48
+
49
+ async put(key: string, data: Uint8Array, opts?: StoragePutOptions): Promise<void> {
50
+ const file = filePathOf(key)
51
+ await fs.mkdir(path.dirname(file), { recursive: true })
52
+ await fs.writeFile(file, data)
53
+ if (opts?.contentType !== undefined) {
54
+ await fs.writeFile(metaPathOf(file), JSON.stringify({ contentType: opts.contentType }))
55
+ }
56
+ },
57
+
58
+ async get(key: string): Promise<StorageObject | null> {
59
+ const file = filePathOf(key)
60
+ let data: Buffer
61
+ try {
62
+ data = await fs.readFile(file)
63
+ } catch {
64
+ return null
65
+ }
66
+ let contentType: string | undefined
67
+ try {
68
+ const meta = JSON.parse(await fs.readFile(metaPathOf(file), 'utf8')) as { contentType?: string }
69
+ contentType = meta.contentType
70
+ } catch {
71
+ /* Sem sidecar → sem contentType. */
72
+ }
73
+ return { data: new Uint8Array(data), ...(contentType !== undefined ? { contentType } : {}) }
74
+ },
75
+
76
+ async delete(key: string): Promise<void> {
77
+ const file = filePathOf(key)
78
+ await fs.rm(file, { force: true })
79
+ await fs.rm(metaPathOf(file), { force: true })
80
+ },
81
+
82
+ async url(key: string): Promise<string> {
83
+ const k = normalizeKey(key)
84
+ if (baseUrl === undefined) {
85
+ throw new Error('fsStorage sem baseUrl — configure-a (e sirva o root) ou use storage/s3 pra URL assinada.')
86
+ }
87
+ return `${baseUrl}/${k}`
88
+ },
89
+ }
90
+ }