@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,51 @@
1
+ /**
2
+ * @softize/opus/audit — shared helpers/types for audit drivers.
3
+ *
4
+ * Drivers (console, pg) são thin wrappers que implementam AuditSink.
5
+ * Código compartilhado entre drivers mora aqui — hoje, o redator
6
+ * recursivo (`redactDeep`) e a aplicação dele num record (`redactRecord`).
7
+ */
8
+
9
+ import type { AuditRecord } from '../core/index.ts'
10
+
11
+ /** Chaves sensíveis por convenção — o default do `redactDeep`. */
12
+ export const SENSITIVE_KEY_PATTERN = /password|passwd|secret|token|authorization|api[-_]?key|credential/i
13
+
14
+ /**
15
+ * Redator RECURSIVO por nome de chave: em qualquer profundidade (objetos e
16
+ * arrays), valor cuja CHAVE casa o padrão vira '[REDACTED]'. É o complemento do
17
+ * `AuditConfig.redact` por action (dot-paths estáticos): aqui a cobertura é
18
+ * global — `user.create`/`setPassword` não grava senha em claro por esquecimento.
19
+ * Não muta o original; objetos não-planos (Date, Map, class) passam intactos.
20
+ */
21
+ export function redactDeep(value: unknown, keyPattern: RegExp = SENSITIVE_KEY_PATTERN): unknown {
22
+ const seen = new WeakSet<object>()
23
+ const matches = (key: string): boolean => {
24
+ keyPattern.lastIndex = 0 // padrão /g do chamador não pode ficar stateful
25
+ return keyPattern.test(key)
26
+ }
27
+ const walk = (v: unknown): unknown => {
28
+ if (v === null || typeof v !== 'object') return v
29
+ if (seen.has(v)) return '[CIRCULAR]'
30
+ seen.add(v)
31
+ if (Array.isArray(v)) return v.map(walk)
32
+ const proto: unknown = Object.getPrototypeOf(v)
33
+ if (proto !== Object.prototype && proto !== null) return v
34
+ const out: Record<string, unknown> = {}
35
+ for (const [k, val] of Object.entries(v)) {
36
+ out[k] = matches(k) ? '[REDACTED]' : walk(val)
37
+ }
38
+ return out
39
+ }
40
+ return walk(value)
41
+ }
42
+
43
+ /** Aplica um redator a input/output de um record (o que os drivers persistem). */
44
+ export function redactRecord(
45
+ record: AuditRecord,
46
+ redact: (value: unknown) => unknown,
47
+ ): AuditRecord {
48
+ const next: AuditRecord = { ...record, input: redact(record.input) }
49
+ if (record.output !== undefined) next.output = redact(record.output)
50
+ return next
51
+ }
@@ -0,0 +1,103 @@
1
+ /**
2
+ * @softize/opus/auth/better-auth — driver de sessão do better-auth (lado consumidor)
3
+ *
4
+ * Valida a sessão de um IdP (better-auth) encaminhando o cookie da request pro
5
+ * `${baseURL}/api/auth/get-session`. O IdP é a fonte da verdade — este adapter não
6
+ * guarda segredo: os apps confiam no IdP.
7
+ *
8
+ * Uso:
9
+ * import { betterAuthSession } from '@softize/opus/auth/better-auth'
10
+ *
11
+ * const auth = betterAuthSession({
12
+ * baseURL: process.env.AUTH_IDP_URL!, // https://auth.example.com
13
+ * can: (user) => (perm) => user.staff === true, // staff-only
14
+ * })
15
+ */
16
+
17
+ import type { AuthAdapter, CanFn, User } from '../../core/index.ts'
18
+
19
+ export interface BetterAuthSession {
20
+ user:
21
+ | ({ id: string; email: string; name: string; staff?: boolean } & Record<string, unknown>)
22
+ | null
23
+ session?: unknown
24
+ }
25
+
26
+ export interface BetterAuthDriverOptions {
27
+ /** Base URL do IdP, ex: `https://auth.example.com`. */
28
+ baseURL: string
29
+ /** Mapeia a sessão do IdP → `User` do core. Default espalha `session.user`. */
30
+ mapUser?: (session: BetterAuthSession) => User
31
+ /** Mapeia a sessão → tenantId. Default null. */
32
+ mapTenant?: (session: BetterAuthSession) => string | null
33
+ /** Constrói o `CanFn`. Default nega tudo (consumer deve plugar). */
34
+ can?: (user: User, session: BetterAuthSession) => CanFn
35
+ /** Timeout do fetch ao IdP (ms). Default 5000. */
36
+ timeoutMs?: number
37
+ /** Nome do adapter (default 'better-auth'). */
38
+ name?: string
39
+ }
40
+
41
+ const denyAll: CanFn = () => false
42
+
43
+ export function betterAuthSession(options: BetterAuthDriverOptions): AuthAdapter {
44
+ const { baseURL, mapUser, mapTenant, can, timeoutMs = 5000, name = 'better-auth' } = options
45
+ const base = baseURL.replace(/\/+$/, '')
46
+
47
+ return {
48
+ name,
49
+ kind: 'auth',
50
+ async resolveContext(req) {
51
+ const headers = forwardHeaders(req)
52
+ if (headers === null) return { user: null, can: denyAll }
53
+
54
+ let session: BetterAuthSession
55
+ try {
56
+ const res = await fetch(`${base}/api/auth/get-session`, {
57
+ headers,
58
+ signal: AbortSignal.timeout(timeoutMs),
59
+ })
60
+ if (!res.ok) return { user: null, can: denyAll }
61
+ session = (await res.json()) as BetterAuthSession
62
+ } catch {
63
+ // IdP fora do ar / timeout → fail-closed (sem user).
64
+ return { user: null, can: denyAll }
65
+ }
66
+
67
+ if (session === null || session.user === null || session.user === undefined) {
68
+ return { user: null, can: denyAll }
69
+ }
70
+
71
+ const user = mapUser ? mapUser(session) : ({ ...session.user } as User)
72
+ const tenantId = mapTenant?.(session) ?? null
73
+ return { user, tenantId, can: can ? can(user, session) : denyAll }
74
+ },
75
+ }
76
+ }
77
+
78
+ /** Encaminha `cookie` + `authorization` pro IdP — sem eles o get-session não autentica. */
79
+ function forwardHeaders(req: unknown): Record<string, string> | null {
80
+ if (typeof req !== 'object' || req === null) return null
81
+ const headers = (req as { headers?: Record<string, string | string[] | undefined> }).headers
82
+ if (headers === undefined) return null
83
+ const out: Record<string, string> = {}
84
+ const cookie = pickHeader(headers, 'cookie')
85
+ const authorization = pickHeader(headers, 'authorization')
86
+ if (cookie !== null) out.cookie = cookie
87
+ if (authorization !== null) out.authorization = authorization
88
+ return Object.keys(out).length === 0 ? null : out
89
+ }
90
+
91
+ function pickHeader(
92
+ headers: Record<string, string | string[] | undefined>,
93
+ name: string,
94
+ ): string | null {
95
+ const target = name.toLowerCase()
96
+ for (const key of Object.keys(headers)) {
97
+ if (key.toLowerCase() !== target) continue
98
+ const value = headers[key]
99
+ if (value === undefined) return null
100
+ return Array.isArray(value) ? (value[0] ?? null) : value
101
+ }
102
+ return null
103
+ }
@@ -0,0 +1,188 @@
1
+ /**
2
+ * @softize/opus/auth/jwt — JWT auth driver
3
+ *
4
+ * Implementa `AuthAdapter` extraindo + validando JWT da request. Padrão
5
+ * comum em backends Node (matchando o que Conversya faz manualmente
6
+ * em `services/*\/middleware/auth.ts`):
7
+ *
8
+ * 1. Extrai token do header `Authorization: Bearer <token>` ou cookie.
9
+ * 2. Valida assinatura + expiração via `jsonwebtoken`.
10
+ * 3. Mapeia payload → `User` + `tenantId` via funções fornecidas.
11
+ * 4. Delega `can()` a um permission engine plugável.
12
+ *
13
+ * Não persiste sessão, não revoga token, não emite — esses são concerns
14
+ * de session store (DB/Redis) que o consumer pluga via `validatePayload`.
15
+ *
16
+ * Uso:
17
+ * import { jwtAuth } from '@softize/opus/auth/jwt'
18
+ *
19
+ * const auth = jwtAuth({
20
+ * secret: process.env.JWT_SECRET!,
21
+ * mapUser: (payload) => ({ id: payload.userId, role: payload.role }),
22
+ * mapTenant: (payload) => payload.tenantId ?? null,
23
+ * can: (user, payload) => (perm) =>
24
+ * hasPermission(user.id, perm, payload),
25
+ * })
26
+ */
27
+
28
+ import jwt, { type JwtPayload, type Algorithm } from 'jsonwebtoken'
29
+ const { verify } = jwt
30
+ import type { AuthAdapter, CanFn, User } from '../../core/index.ts'
31
+
32
+ // =============================================================================
33
+ // Options
34
+ // =============================================================================
35
+
36
+ export interface JwtAuthOptions<P extends JwtPayload = JwtPayload> {
37
+ /** Secret HMAC ou public key. Pode ser string sync ou async resolver. */
38
+ secret: string | Buffer | ((kid?: string) => Promise<string | Buffer> | string | Buffer)
39
+
40
+ /** Algorithms aceitos (default ['HS256']). */
41
+ algorithms?: Algorithm[]
42
+
43
+ /**
44
+ * Extração customizada de token. Se ausente, busca em:
45
+ * 1. Header `Authorization: Bearer <token>`
46
+ * 2. Cookie `cookieName` (default 'auth_token')
47
+ * Retorna `null` se não encontrar.
48
+ */
49
+ tokenExtractor?: (req: unknown) => string | null
50
+
51
+ /** Nome do cookie pra extração default. Default 'auth_token'. */
52
+ cookieName?: string
53
+
54
+ /**
55
+ * Validação custom **após** verify do JWT. Útil pra checar sessão
56
+ * em DB (Redis cache, blacklist, etc). Retornar false rejeita.
57
+ */
58
+ validatePayload?: (payload: P) => boolean | Promise<boolean>
59
+
60
+ /** Mapeia JWT payload pro shape `User` do core. */
61
+ mapUser: (payload: P) => User
62
+
63
+ /**
64
+ * Mapeia JWT payload pra tenantId. Default tenta `payload.tenantId`.
65
+ */
66
+ mapTenant?: (payload: P) => string | null
67
+
68
+ /**
69
+ * Constrói o `CanFn` baseado em user + payload. Default retorna
70
+ * função que sempre nega (consumer **deve** plugar engine de permissões).
71
+ */
72
+ can?: (user: User, payload: P) => CanFn
73
+
74
+ /** Nome do adapter (default 'jwt'). */
75
+ name?: string
76
+ }
77
+
78
+ // =============================================================================
79
+ // Adapter factory
80
+ // =============================================================================
81
+
82
+ export function jwtAuth<P extends JwtPayload = JwtPayload>(
83
+ options: JwtAuthOptions<P>,
84
+ ): AuthAdapter {
85
+ const {
86
+ secret,
87
+ algorithms = ['HS256'],
88
+ tokenExtractor,
89
+ cookieName = 'auth_token',
90
+ validatePayload,
91
+ mapUser,
92
+ mapTenant,
93
+ can,
94
+ name = 'jwt',
95
+ } = options
96
+
97
+ return {
98
+ name,
99
+ kind: 'auth',
100
+
101
+ async resolveContext(req) {
102
+ const token =
103
+ tokenExtractor !== undefined
104
+ ? tokenExtractor(req)
105
+ : defaultExtract(req, cookieName)
106
+
107
+ if (token === null) {
108
+ return { user: null, can: denyAll }
109
+ }
110
+
111
+ const resolvedSecret =
112
+ typeof secret === 'function' ? await secret() : secret
113
+
114
+ let payload: P
115
+ try {
116
+ payload = verify(token, resolvedSecret, { algorithms }) as P
117
+ } catch {
118
+ return { user: null, can: denyAll }
119
+ }
120
+
121
+ if (validatePayload !== undefined) {
122
+ const valid = await validatePayload(payload)
123
+ if (!valid) return { user: null, can: denyAll }
124
+ }
125
+
126
+ const user = mapUser(payload)
127
+ const tenantId = mapTenant?.(payload) ?? null
128
+ const userCan = can?.(user, payload) ?? denyAll
129
+
130
+ return { user, tenantId, can: userCan }
131
+ },
132
+ }
133
+ }
134
+
135
+ // =============================================================================
136
+ // Default token extractor
137
+ // =============================================================================
138
+
139
+ function defaultExtract(req: unknown, cookieName: string): string | null {
140
+ if (typeof req !== 'object' || req === null) return null
141
+
142
+ const headers = (req as { headers?: Record<string, string | string[] | undefined> })
143
+ .headers
144
+
145
+ // Bearer header
146
+ const authHeader = pickHeader(headers, 'authorization')
147
+ if (authHeader !== null && authHeader.toLowerCase().startsWith('bearer ')) {
148
+ return authHeader.slice('Bearer '.length).trim() || null
149
+ }
150
+
151
+ // Cookie
152
+ const cookieHeader = pickHeader(headers, 'cookie')
153
+ if (cookieHeader !== null) {
154
+ const fromCookie = parseCookie(cookieHeader, cookieName)
155
+ if (fromCookie !== null) return fromCookie
156
+ }
157
+
158
+ return null
159
+ }
160
+
161
+ function pickHeader(
162
+ headers: Record<string, string | string[] | undefined> | undefined,
163
+ name: string,
164
+ ): string | null {
165
+ if (headers === undefined) return null
166
+ const target = name.toLowerCase()
167
+ for (const key of Object.keys(headers)) {
168
+ if (key.toLowerCase() !== target) continue
169
+ const value = headers[key]
170
+ if (value === undefined) return null
171
+ if (Array.isArray(value)) return value[0] ?? null
172
+ return value
173
+ }
174
+ return null
175
+ }
176
+
177
+ function parseCookie(header: string, name: string): string | null {
178
+ const parts = header.split(';')
179
+ for (const part of parts) {
180
+ const eq = part.indexOf('=')
181
+ if (eq < 0) continue
182
+ const k = part.slice(0, eq).trim()
183
+ if (k === name) return decodeURIComponent(part.slice(eq + 1).trim())
184
+ }
185
+ return null
186
+ }
187
+
188
+ const denyAll: CanFn = () => false
@@ -0,0 +1,9 @@
1
+ /**
2
+ * @softize/opus/auth — shared helpers/types for auth drivers.
3
+ *
4
+ * Drivers (jwt, futuro: better-auth/clerk/etc) implementam AuthAdapter
5
+ * do core. Espaço reservado pra helpers comuns (token extractors,
6
+ * permission engines, etc).
7
+ */
8
+
9
+ export {}
@@ -0,0 +1,202 @@
1
+ /**
2
+ * @softize/opus/client/fetch — fetch-based client driver.
3
+ *
4
+ * Implementa `ClientAdapter` usando o `fetch` global. Constrói URLs
5
+ * seguindo a mesma convenção do server adapter (path = name → segments,
6
+ * method = POST simple/form, GET search/view).
7
+ *
8
+ * Uso:
9
+ * import { fetchClient } from '@softize/opus/client/fetch'
10
+ *
11
+ * const client = fetchClient({
12
+ * baseUrl: 'http://localhost:3001',
13
+ * headers: { 'x-user-id': 'u_alice' },
14
+ * })
15
+ *
16
+ * const result = await client.run(archiveDeal, { dealId: 'd1' })
17
+ */
18
+
19
+ import type {
20
+ ActionDef,
21
+ ActionResult,
22
+ ClientAdapter,
23
+ ResultMeta,
24
+ } from '../../core/index.ts'
25
+ import { error } from '../../core/index.ts'
26
+ import { inputSourceFor, methodFor, pathFor } from '../index.ts'
27
+
28
+ // =============================================================================
29
+ // Options
30
+ // =============================================================================
31
+
32
+ export interface FetchClientOptions {
33
+ /** URL base do server. Ex: 'http://localhost:3001'. */
34
+ baseUrl: string
35
+
36
+ /** Prefixo do path (espelha apiPrefix do server adapter). Default '/api'. */
37
+ apiPrefix?: string
38
+
39
+ /** Headers fixos enviados em toda request (ex: auth). */
40
+ headers?: Record<string, string>
41
+
42
+ /** Hook pra injetar headers dinâmicos (ex: token rotativo) por request. */
43
+ beforeRequest?: (init: RequestInit, action: ActionDef) => RequestInit | Promise<RequestInit>
44
+
45
+ /** Override do `fetch` global. Útil pra testes ou edge runtimes. */
46
+ fetch?: typeof globalThis.fetch
47
+ }
48
+
49
+ // =============================================================================
50
+ // Adapter factory
51
+ // =============================================================================
52
+
53
+ export function fetchClient(options: FetchClientOptions): ClientAdapter {
54
+ const {
55
+ baseUrl,
56
+ apiPrefix = '/api',
57
+ headers: baseHeaders = {},
58
+ beforeRequest,
59
+ fetch: fetchImpl = globalThis.fetch,
60
+ } = options
61
+
62
+ return {
63
+ name: 'fetch',
64
+ kind: 'client',
65
+
66
+ async run<T>(action: ActionDef, input: unknown): Promise<ActionResult<T>> {
67
+ const method = methodFor(action)
68
+ const source = inputSourceFor(action)
69
+ const path = pathFor(action, apiPrefix)
70
+ const url = buildUrl(baseUrl, path, source === 'query' ? input : undefined)
71
+
72
+ let init: RequestInit = {
73
+ method,
74
+ headers: {
75
+ 'content-type': 'application/json',
76
+ accept: 'application/json',
77
+ ...baseHeaders,
78
+ },
79
+ }
80
+ if (source === 'body') init.body = JSON.stringify(input ?? {})
81
+
82
+ if (beforeRequest !== undefined) {
83
+ init = await beforeRequest(init, action)
84
+ }
85
+
86
+ let response: Response
87
+ try {
88
+ response = await fetchImpl(url, init)
89
+ } catch (cause) {
90
+ return errorResult<T>(
91
+ action.name,
92
+ error({
93
+ code: 'client.network',
94
+ category: 'dependency',
95
+ message: 'Network request failed',
96
+ cause: String(cause),
97
+ }),
98
+ )
99
+ }
100
+
101
+ const text = await response.text()
102
+ const parsed = parseJson(text)
103
+
104
+ // Server falou opus? passthrough — inclusive em status != 200
105
+ // (not_found=404, validation=422, conflict=409 etc chegam como envelope).
106
+ if (isActionResultEnvelope(parsed)) {
107
+ return parsed as ActionResult<T>
108
+ }
109
+
110
+ // Daqui pra baixo a resposta NÃO é um envelope opus. Classificamos por
111
+ // status — NUNCA como `internal.unhandled`. `internal.unhandled` é erro de
112
+ // HANDLER do server, e esse só chega via envelope acima. Falha de
113
+ // transporte/gateway é `dependency`, pra UI distinguir "servidor
114
+ // indisponível" (retriable) de "erro interno do app".
115
+ if (!response.ok) {
116
+ const isServerSide = response.status >= 500
117
+ return errorResult<T>(
118
+ action.name,
119
+ error({
120
+ code: isServerSide ? 'client.unreachable' : 'client.http_error',
121
+ category: 'dependency',
122
+ message: isServerSide
123
+ ? `Servidor indisponível (HTTP ${response.status}) em ${action.name}`
124
+ : `HTTP ${response.status} em ${action.name} (resposta não-opus)`,
125
+ retriable: isServerSide,
126
+ meta: { httpStatus: response.status },
127
+ }),
128
+ response.status,
129
+ )
130
+ }
131
+
132
+ // Status 2xx mas corpo não é envelope opus (JSON alheio, HTML, vazio) —
133
+ // contrato quebrado entre client e server, não erro interno do server.
134
+ return errorResult<T>(
135
+ action.name,
136
+ error({
137
+ code: 'client.invalid_response',
138
+ category: 'dependency',
139
+ message: `Resposta de ${action.name} não é um ActionResult opus válido (status ${response.status})`,
140
+ }),
141
+ response.status,
142
+ )
143
+ },
144
+ }
145
+ }
146
+
147
+ // =============================================================================
148
+ // Helpers
149
+ // =============================================================================
150
+
151
+ function buildUrl(
152
+ baseUrl: string,
153
+ path: string,
154
+ query?: unknown,
155
+ ): string {
156
+ const trimmed = baseUrl.replace(/\/$/, '')
157
+ const url = `${trimmed}${path}`
158
+ if (query === undefined || query === null) return url
159
+ if (typeof query !== 'object') return url
160
+ const params = new URLSearchParams()
161
+ for (const [key, value] of Object.entries(query as Record<string, unknown>)) {
162
+ if (value === undefined || value === null) continue
163
+ params.set(key, String(value))
164
+ }
165
+ const qs = params.toString()
166
+ return qs.length > 0 ? `${url}?${qs}` : url
167
+ }
168
+
169
+ /** Resposta é um `ActionResult` serializado (envelope opus)? */
170
+ function isActionResultEnvelope(value: unknown): boolean {
171
+ return (
172
+ typeof value === 'object' &&
173
+ value !== null &&
174
+ 'ok' in value &&
175
+ 'meta' in value
176
+ )
177
+ }
178
+
179
+ function parseJson(text: string): unknown {
180
+ if (text.length === 0) return null
181
+ try {
182
+ return JSON.parse(text)
183
+ } catch {
184
+ return undefined
185
+ }
186
+ }
187
+
188
+ function errorResult<T>(
189
+ actionName: string,
190
+ err: ReturnType<typeof error>,
191
+ httpStatus?: number,
192
+ ): ActionResult<T> {
193
+ const meta: ResultMeta = {
194
+ actionId: 'client',
195
+ action: actionName,
196
+ durationMs: 0,
197
+ }
198
+ if (httpStatus !== undefined) {
199
+ meta.actionId = `client:${httpStatus}`
200
+ }
201
+ return { ok: false, error: err, meta }
202
+ }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * @softize/opus/client — shared helpers for client drivers.
3
+ *
4
+ * Drivers (fetch, tanstack) usam pra:
5
+ * - construir URL de uma action (mesma convenção do server adapter)
6
+ * - decidir HTTP method + input source (body vs query)
7
+ * - desserializar response → ActionResult
8
+ *
9
+ * Lógica específica de driver (HTTP fetch, cache adapter, etc) vive em
10
+ * `src/drivers/<driver>.ts`.
11
+ *
12
+ * Importa de `@softize/opus/server` pra manter convenção alinhada (mesmo path,
13
+ * mesmo método). Isso garante que client + server sempre falam o mesmo
14
+ * dialeto sem duplicação de regra.
15
+ */
16
+
17
+ export {
18
+ inputSourceFor,
19
+ methodFor,
20
+ pathFor,
21
+ type HttpMethod,
22
+ } from '../server/index.ts'
@@ -0,0 +1,110 @@
1
+ /**
2
+ * tbdlib — `defineAction` factory + type guards
3
+ *
4
+ * `defineAction` é identity function tipada que aceita qualquer variante
5
+ * de `ActionDef` e preserva o subtipo exato pra type narrowing posterior.
6
+ *
7
+ * Validação estrutural acontece no `Runtime.register()`, não aqui. Esta função
8
+ * é puramente sobre **declaração**.
9
+ *
10
+ * Ver §2 do protocolo.
11
+ */
12
+
13
+ import type {
14
+ ActionDef,
15
+ FormAction,
16
+ ListAction,
17
+ SimpleAction,
18
+ ViewAction,
19
+ } from './types.ts'
20
+
21
+ /* eslint-disable @typescript-eslint/no-explicit-any */
22
+
23
+ // =============================================================================
24
+ // defineAction (overloads per kind para inferência precisa)
25
+ // =============================================================================
26
+
27
+ /**
28
+ * Constrói uma `SimpleAction`. `In` é o lado do fio do schema de input;
29
+ * `ParsedIn` (inferido do output do schema) é o que o handler recebe —
30
+ * ver `ActionBase` em `types.ts`.
31
+ */
32
+ export function defineAction<In, Out, ParsedIn = In>(
33
+ spec: SimpleAction<In, Out, ParsedIn>,
34
+ ): SimpleAction<In, Out, ParsedIn>
35
+
36
+ /**
37
+ * Constrói uma `FormAction`. `In` deve ser um objeto cujas chaves casam com
38
+ * a estrutura de `fields`.
39
+ */
40
+ export function defineAction<In extends Record<string, unknown>, Out, ParsedIn = In>(
41
+ spec: FormAction<In, Out, ParsedIn>,
42
+ ): FormAction<In, Out, ParsedIn>
43
+
44
+ /**
45
+ * Constrói uma `ListAction`. Output é envelopado em `Paginated<Out>`.
46
+ */
47
+ export function defineAction<In, Out, ParsedIn = In>(
48
+ spec: ListAction<In, Out, ParsedIn>,
49
+ ): ListAction<In, Out, ParsedIn>
50
+
51
+ /**
52
+ * Constrói uma `ViewAction`.
53
+ */
54
+ export function defineAction<In, Out, ParsedIn = In>(
55
+ spec: ViewAction<In, Out, ParsedIn>,
56
+ ): ViewAction<In, Out, ParsedIn>
57
+
58
+ export function defineAction(spec: any): any {
59
+ return spec
60
+ }
61
+
62
+ // =============================================================================
63
+ // Type guards
64
+ // =============================================================================
65
+
66
+ /**
67
+ * `true` se a action é do kind `simple`.
68
+ */
69
+ export function isSimpleAction(
70
+ action: ActionDef,
71
+ ): action is SimpleAction<any, any> {
72
+ return action.kind === 'simple'
73
+ }
74
+
75
+ /**
76
+ * `true` se a action é do kind `form`.
77
+ */
78
+ export function isFormAction(
79
+ action: ActionDef,
80
+ ): action is FormAction<any, any> {
81
+ return action.kind === 'form'
82
+ }
83
+
84
+ /**
85
+ * `true` se a action é do kind `search`.
86
+ */
87
+ export function isListAction(
88
+ action: ActionDef,
89
+ ): action is ListAction<any, any> {
90
+ return action.kind === 'list'
91
+ }
92
+
93
+ /**
94
+ * `true` se a action é do kind `view`.
95
+ */
96
+ export function isViewAction(
97
+ action: ActionDef,
98
+ ): action is ViewAction<any, any> {
99
+ return action.kind === 'view'
100
+ }
101
+
102
+ /**
103
+ * `true` se a action declarou execução em background (apenas `simple` e `form`).
104
+ */
105
+ export function isBackgroundAction(action: ActionDef): boolean {
106
+ return (
107
+ (action.kind === 'simple' || action.kind === 'form') &&
108
+ action.background?.enabled === true
109
+ )
110
+ }