@topogram/cli 0.3.34

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 (257) hide show
  1. package/ARCHITECTURE.md +67 -0
  2. package/CHANGELOG.md +240 -0
  3. package/README.md +223 -0
  4. package/package.json +51 -0
  5. package/src/adoption/index.js +3 -0
  6. package/src/adoption/plan.js +702 -0
  7. package/src/adoption/reporting.js +464 -0
  8. package/src/adoption/review-groups.js +313 -0
  9. package/src/agent-ops/query-builders.js +5012 -0
  10. package/src/archive/archive.js +141 -0
  11. package/src/archive/compact.js +26 -0
  12. package/src/archive/jsonl.js +70 -0
  13. package/src/archive/resolver-bridge.js +82 -0
  14. package/src/archive/schema.js +87 -0
  15. package/src/archive/unarchive.js +108 -0
  16. package/src/catalog.js +752 -0
  17. package/src/cli/catalog-alias.js +166 -0
  18. package/src/cli.js +9738 -0
  19. package/src/component-behavior.js +173 -0
  20. package/src/example-implementation.js +91 -0
  21. package/src/format.js +19 -0
  22. package/src/generator/adapters.d.ts +4 -0
  23. package/src/generator/adapters.js +325 -0
  24. package/src/generator/api.d.ts +1 -0
  25. package/src/generator/api.js +1196 -0
  26. package/src/generator/check.js +355 -0
  27. package/src/generator/component-conformance.js +767 -0
  28. package/src/generator/components.js +39 -0
  29. package/src/generator/context/bundle.js +291 -0
  30. package/src/generator/context/diff.js +256 -0
  31. package/src/generator/context/digest.js +182 -0
  32. package/src/generator/context/domain-coverage.js +94 -0
  33. package/src/generator/context/domain-page.js +137 -0
  34. package/src/generator/context/index.js +42 -0
  35. package/src/generator/context/report.js +121 -0
  36. package/src/generator/context/shared.js +1397 -0
  37. package/src/generator/context/slice.js +703 -0
  38. package/src/generator/context/task-mode.js +466 -0
  39. package/src/generator/docs.js +327 -0
  40. package/src/generator/index.js +161 -0
  41. package/src/generator/native/parity-bundle.js +311 -0
  42. package/src/generator/output.js +300 -0
  43. package/src/generator/registry.js +482 -0
  44. package/src/generator/runtime/app-bundle.js +456 -0
  45. package/src/generator/runtime/bundle-shared.js +166 -0
  46. package/src/generator/runtime/compile-check.js +163 -0
  47. package/src/generator/runtime/deployment.js +287 -0
  48. package/src/generator/runtime/environment.js +635 -0
  49. package/src/generator/runtime/index.js +32 -0
  50. package/src/generator/runtime/runtime-check.js +554 -0
  51. package/src/generator/runtime/shared.js +515 -0
  52. package/src/generator/runtime/smoke.js +219 -0
  53. package/src/generator/schema.js +204 -0
  54. package/src/generator/sdlc/board.js +66 -0
  55. package/src/generator/sdlc/doc-page.js +53 -0
  56. package/src/generator/sdlc/index.js +23 -0
  57. package/src/generator/sdlc/release-notes.js +62 -0
  58. package/src/generator/sdlc/traceability-matrix.js +65 -0
  59. package/src/generator/shared.js +29 -0
  60. package/src/generator/surfaces/contracts.js +146 -0
  61. package/src/generator/surfaces/databases/contract.js +40 -0
  62. package/src/generator/surfaces/databases/index.js +84 -0
  63. package/src/generator/surfaces/databases/lifecycle-shared.d.ts +1 -0
  64. package/src/generator/surfaces/databases/lifecycle-shared.js +612 -0
  65. package/src/generator/surfaces/databases/migration-plan.js +281 -0
  66. package/src/generator/surfaces/databases/postgres/capabilities.js +14 -0
  67. package/src/generator/surfaces/databases/postgres/drizzle.js +99 -0
  68. package/src/generator/surfaces/databases/postgres/index.js +9 -0
  69. package/src/generator/surfaces/databases/postgres/lifecycle.js +16 -0
  70. package/src/generator/surfaces/databases/postgres/prisma.js +159 -0
  71. package/src/generator/surfaces/databases/postgres/sql-migration.js +102 -0
  72. package/src/generator/surfaces/databases/postgres/sql-schema.js +34 -0
  73. package/src/generator/surfaces/databases/shared.d.ts +1 -0
  74. package/src/generator/surfaces/databases/shared.js +350 -0
  75. package/src/generator/surfaces/databases/snapshot.js +96 -0
  76. package/src/generator/surfaces/databases/sqlite/capabilities.js +14 -0
  77. package/src/generator/surfaces/databases/sqlite/index.js +8 -0
  78. package/src/generator/surfaces/databases/sqlite/lifecycle.js +16 -0
  79. package/src/generator/surfaces/databases/sqlite/prisma.js +143 -0
  80. package/src/generator/surfaces/databases/sqlite/sql-migration.js +65 -0
  81. package/src/generator/surfaces/databases/sqlite/sql-schema.js +27 -0
  82. package/src/generator/surfaces/index.js +25 -0
  83. package/src/generator/surfaces/native/swiftui-app.js +38 -0
  84. package/src/generator/surfaces/native/swiftui-templates/Package.swift.txt +20 -0
  85. package/src/generator/surfaces/native/swiftui-templates/README.generated.md +26 -0
  86. package/src/generator/surfaces/native/swiftui-templates/runtime/DynamicScreens.swift +682 -0
  87. package/src/generator/surfaces/native/swiftui-templates/runtime/TodoAPIClient.swift +156 -0
  88. package/src/generator/surfaces/native/swiftui-templates/runtime/TodoSwiftUIApp.swift +44 -0
  89. package/src/generator/surfaces/native/swiftui-templates/runtime/Visibility.swift +183 -0
  90. package/src/generator/surfaces/services/express.d.ts +1 -0
  91. package/src/generator/surfaces/services/express.js +766 -0
  92. package/src/generator/surfaces/services/hono.d.ts +1 -0
  93. package/src/generator/surfaces/services/hono.js +204 -0
  94. package/src/generator/surfaces/services/index.js +42 -0
  95. package/src/generator/surfaces/services/persistence-wiring.js +240 -0
  96. package/src/generator/surfaces/services/runtime-helpers.js +631 -0
  97. package/src/generator/surfaces/services/server-contract.js +80 -0
  98. package/src/generator/surfaces/services/stateless.d.ts +1 -0
  99. package/src/generator/surfaces/services/stateless.js +97 -0
  100. package/src/generator/surfaces/shared.js +64 -0
  101. package/src/generator/surfaces/web/api-client.js +1 -0
  102. package/src/generator/surfaces/web/forms.js +1 -0
  103. package/src/generator/surfaces/web/index.d.ts +2 -0
  104. package/src/generator/surfaces/web/index.js +53 -0
  105. package/src/generator/surfaces/web/react-components.js +248 -0
  106. package/src/generator/surfaces/web/react.js +538 -0
  107. package/src/generator/surfaces/web/routes.js +1 -0
  108. package/src/generator/surfaces/web/screens.js +1 -0
  109. package/src/generator/surfaces/web/shared.js +369 -0
  110. package/src/generator/surfaces/web/sveltekit-actions.js +28 -0
  111. package/src/generator/surfaces/web/sveltekit-components.js +234 -0
  112. package/src/generator/surfaces/web/sveltekit.js +426 -0
  113. package/src/generator/surfaces/web/ui-web-contract.js +65 -0
  114. package/src/generator/surfaces/web/vanilla.js +239 -0
  115. package/src/generator/verification.js +84 -0
  116. package/src/generator.js +1 -0
  117. package/src/import/core/context.js +52 -0
  118. package/src/import/core/contracts.js +23 -0
  119. package/src/import/core/registry.js +81 -0
  120. package/src/import/core/runner.js +646 -0
  121. package/src/import/core/shared.js +910 -0
  122. package/src/import/enrichers/auth-session.js +18 -0
  123. package/src/import/enrichers/django-rest.js +226 -0
  124. package/src/import/enrichers/doc-linking.js +20 -0
  125. package/src/import/enrichers/rails-controllers.js +246 -0
  126. package/src/import/enrichers/rails-models.js +130 -0
  127. package/src/import/enrichers/workflow-target-state.js +10 -0
  128. package/src/import/extractors/api/aspnet-core.js +304 -0
  129. package/src/import/extractors/api/django-routes.js +318 -0
  130. package/src/import/extractors/api/express.js +154 -0
  131. package/src/import/extractors/api/fastify.js +371 -0
  132. package/src/import/extractors/api/flutter-dio.js +135 -0
  133. package/src/import/extractors/api/generic-route-fallback.js +90 -0
  134. package/src/import/extractors/api/graphql-code-first.js +565 -0
  135. package/src/import/extractors/api/graphql-sdl.js +309 -0
  136. package/src/import/extractors/api/jaxrs.js +303 -0
  137. package/src/import/extractors/api/micronaut.js +213 -0
  138. package/src/import/extractors/api/next-route.js +50 -0
  139. package/src/import/extractors/api/next-server-action.js +51 -0
  140. package/src/import/extractors/api/nextauth.js +52 -0
  141. package/src/import/extractors/api/openapi-code.js +242 -0
  142. package/src/import/extractors/api/openapi.js +232 -0
  143. package/src/import/extractors/api/rails-routes.js +230 -0
  144. package/src/import/extractors/api/react-native-repository.js +128 -0
  145. package/src/import/extractors/api/retrofit.js +103 -0
  146. package/src/import/extractors/api/spring-web.js +372 -0
  147. package/src/import/extractors/api/swift-webapi.js +116 -0
  148. package/src/import/extractors/api/trpc.js +212 -0
  149. package/src/import/extractors/db/django-models.js +232 -0
  150. package/src/import/extractors/db/dotnet-models.js +93 -0
  151. package/src/import/extractors/db/drizzle.js +242 -0
  152. package/src/import/extractors/db/ef-core.js +221 -0
  153. package/src/import/extractors/db/flutter-entities.js +120 -0
  154. package/src/import/extractors/db/jpa.js +120 -0
  155. package/src/import/extractors/db/liquibase.js +180 -0
  156. package/src/import/extractors/db/mybatis-xml.js +145 -0
  157. package/src/import/extractors/db/prisma.js +185 -0
  158. package/src/import/extractors/db/rails-schema.js +175 -0
  159. package/src/import/extractors/db/react-native-entities.js +95 -0
  160. package/src/import/extractors/db/room.js +193 -0
  161. package/src/import/extractors/db/snapshot.js +112 -0
  162. package/src/import/extractors/db/sql.js +180 -0
  163. package/src/import/extractors/db/swiftdata.js +137 -0
  164. package/src/import/extractors/ui/android-compose.js +230 -0
  165. package/src/import/extractors/ui/backend-only.js +70 -0
  166. package/src/import/extractors/ui/blazor.js +227 -0
  167. package/src/import/extractors/ui/flutter-screens.js +152 -0
  168. package/src/import/extractors/ui/maui-xaml.js +135 -0
  169. package/src/import/extractors/ui/next-app-router.js +83 -0
  170. package/src/import/extractors/ui/next-pages-router.js +141 -0
  171. package/src/import/extractors/ui/razor-pages.js +181 -0
  172. package/src/import/extractors/ui/react-native-screens.js +166 -0
  173. package/src/import/extractors/ui/react-router.js +139 -0
  174. package/src/import/extractors/ui/sveltekit.js +123 -0
  175. package/src/import/extractors/ui/swiftui.js +193 -0
  176. package/src/import/extractors/ui/uikit.js +175 -0
  177. package/src/import/extractors/verification/generic.js +290 -0
  178. package/src/import/extractors/workflows/generic.js +137 -0
  179. package/src/import/index.js +7 -0
  180. package/src/import/provenance.js +158 -0
  181. package/src/new-project.js +2107 -0
  182. package/src/parser.js +439 -0
  183. package/src/policy/review-boundaries.js +165 -0
  184. package/src/project-config.js +535 -0
  185. package/src/proofs/backend-parity.js +19 -0
  186. package/src/proofs/contract-audit.js +220 -0
  187. package/src/proofs/ios-parity.js +7 -0
  188. package/src/proofs/issues-parity.js +10 -0
  189. package/src/proofs/web-parity.js +50 -0
  190. package/src/realization/api/build-api-realization.js +5 -0
  191. package/src/realization/api/index.js +1 -0
  192. package/src/realization/backend/build-backend-runtime-realization.js +82 -0
  193. package/src/realization/backend/index.d.ts +1 -0
  194. package/src/realization/backend/index.js +4 -0
  195. package/src/realization/db/build-db-realization.js +17 -0
  196. package/src/realization/db/index.js +3 -0
  197. package/src/realization/db/migration-plan.js +5 -0
  198. package/src/realization/db/snapshot.js +5 -0
  199. package/src/realization/ui/build-ui-shared-realization.js +305 -0
  200. package/src/realization/ui/build-web-realization.js +189 -0
  201. package/src/realization/ui/index.js +2 -0
  202. package/src/reconcile/docs.js +280 -0
  203. package/src/reconcile/index.js +3 -0
  204. package/src/reconcile/journeys.js +441 -0
  205. package/src/resolver/docs.js +1 -0
  206. package/src/resolver/enrich/acceptance-criterion.js +14 -0
  207. package/src/resolver/enrich/bug.js +12 -0
  208. package/src/resolver/enrich/component.js +2 -0
  209. package/src/resolver/enrich/index.js +1 -0
  210. package/src/resolver/enrich/pitch.js +18 -0
  211. package/src/resolver/enrich/requirement.js +20 -0
  212. package/src/resolver/enrich/task.js +16 -0
  213. package/src/resolver/expressions.js +1 -0
  214. package/src/resolver/index.js +2422 -0
  215. package/src/resolver/normalize.js +1 -0
  216. package/src/resolver.js +1 -0
  217. package/src/sdlc/adopt.js +65 -0
  218. package/src/sdlc/check.js +86 -0
  219. package/src/sdlc/dod/acceptance-criterion.js +22 -0
  220. package/src/sdlc/dod/bug.js +26 -0
  221. package/src/sdlc/dod/document.js +23 -0
  222. package/src/sdlc/dod/index.js +25 -0
  223. package/src/sdlc/dod/pitch.js +23 -0
  224. package/src/sdlc/dod/requirement.js +34 -0
  225. package/src/sdlc/dod/task.js +39 -0
  226. package/src/sdlc/explain.js +116 -0
  227. package/src/sdlc/history.js +80 -0
  228. package/src/sdlc/paths.js +11 -0
  229. package/src/sdlc/release.js +106 -0
  230. package/src/sdlc/scaffold.js +89 -0
  231. package/src/sdlc/status-filter.js +54 -0
  232. package/src/sdlc/transition.js +112 -0
  233. package/src/sdlc/transitions/acceptance-criterion.js +28 -0
  234. package/src/sdlc/transitions/bug.js +31 -0
  235. package/src/sdlc/transitions/document.js +29 -0
  236. package/src/sdlc/transitions/index.js +56 -0
  237. package/src/sdlc/transitions/pitch.js +34 -0
  238. package/src/sdlc/transitions/requirement.js +31 -0
  239. package/src/sdlc/transitions/task.js +34 -0
  240. package/src/template-trust.js +597 -0
  241. package/src/validator/expressions.js +1 -0
  242. package/src/validator/index.js +3424 -0
  243. package/src/validator/kinds.js +346 -0
  244. package/src/validator/per-kind/acceptance-criterion.js +91 -0
  245. package/src/validator/per-kind/bug.js +77 -0
  246. package/src/validator/per-kind/component.js +274 -0
  247. package/src/validator/per-kind/domain.js +205 -0
  248. package/src/validator/per-kind/pitch.js +101 -0
  249. package/src/validator/per-kind/requirement.js +75 -0
  250. package/src/validator/per-kind/task.js +96 -0
  251. package/src/validator/registry.js +1 -0
  252. package/src/validator/utils.js +12 -0
  253. package/src/validator.js +1 -0
  254. package/src/workflows.js +7597 -0
  255. package/src/workspace-docs.js +265 -0
  256. package/template-helpers/react.js +5 -0
  257. package/template-helpers/sveltekit.js +5 -0
@@ -0,0 +1,766 @@
1
+ import { generateDbTarget } from "../databases/index.js";
2
+ import { buildBackendRuntimeRealization } from "../../../realization/backend/index.js";
3
+ import { getProjection } from "../shared.js";
4
+ import { generatePersistenceScaffold } from "./persistence-wiring.js";
5
+ import {
6
+ renderServerContextTs,
7
+ renderServerSeedScript,
8
+ renderServerTsconfig,
9
+ routeTypeNames,
10
+ } from "./runtime-helpers.js";
11
+ import { renderServerContractModule } from "./server-contract.js";
12
+ import { toPascalCase } from "../databases/shared.js";
13
+ import { getExampleImplementation } from "../../../example-implementation.js";
14
+
15
+ function renderExpressServerHelpers() {
16
+ return `import crypto from "node:crypto";
17
+ import type { Request } from "express";
18
+
19
+ export class HttpError extends Error {
20
+ constructor(
21
+ public readonly status: number,
22
+ public readonly code: string,
23
+ message = code
24
+ ) {
25
+ super(message);
26
+ }
27
+ }
28
+
29
+ export interface DownloadArtifact {
30
+ body: BodyInit | Uint8Array | null;
31
+ contentType?: string;
32
+ filename?: string;
33
+ }
34
+
35
+ export interface AuthorizationContext {
36
+ capabilityId?: string;
37
+ input?: Record<string, unknown>;
38
+ loadResource?: () => Promise<Record<string, unknown> | null | undefined>;
39
+ }
40
+
41
+ interface AuthPrincipal {
42
+ userId: string;
43
+ permissions: Set<string>;
44
+ roles: Set<string>;
45
+ claims: Record<string, unknown>;
46
+ isAdmin: boolean;
47
+ }
48
+
49
+ export function jsonError(error: unknown) {
50
+ if (error instanceof HttpError) {
51
+ return {
52
+ status: error.status,
53
+ body: {
54
+ error: {
55
+ code: error.code,
56
+ message: error.message
57
+ }
58
+ }
59
+ };
60
+ }
61
+
62
+ return {
63
+ status: 500,
64
+ body: {
65
+ error: {
66
+ code: "internal_server_error",
67
+ message: "Internal server error"
68
+ }
69
+ }
70
+ };
71
+ }
72
+
73
+ export function coerceValue(raw: string | undefined, schema: { type?: string; format?: string; enum?: readonly string[]; default?: unknown }) {
74
+ if (raw == null || raw === "") {
75
+ return schema.default;
76
+ }
77
+ if (schema.enum) {
78
+ return raw;
79
+ }
80
+ if (schema.type === "integer" || schema.type === "number") {
81
+ const parsed = Number(raw);
82
+ if (Number.isNaN(parsed)) {
83
+ throw new HttpError(400, "invalid_number", \`Invalid numeric value: \${raw}\`);
84
+ }
85
+ if (schema.type === "integer" && !Number.isInteger(parsed)) {
86
+ throw new HttpError(400, "invalid_integer", \`Invalid integer value: \${raw}\`);
87
+ }
88
+ return parsed;
89
+ }
90
+ if (schema.type === "boolean") {
91
+ return raw === "true";
92
+ }
93
+ return raw;
94
+ }
95
+
96
+ export function requireHeaders(req: Request, headers: ReadonlyArray<{ header: string; required?: boolean; code?: string; error?: number }>) {
97
+ for (const rule of headers) {
98
+ if (!rule.required) {
99
+ continue;
100
+ }
101
+ if (!req.get(rule.header)) {
102
+ throw new HttpError(rule.error || 400, rule.code || "missing_required_header", \`Missing required header \${rule.header}\`);
103
+ }
104
+ }
105
+ }
106
+
107
+ export function requireRequestFields(
108
+ route: {
109
+ capabilityId?: string;
110
+ errors?: ReadonlyArray<{ code?: string; source?: string; status?: number }>;
111
+ requestContract?: { fields?: ReadonlyArray<{ name: string; required?: boolean }> };
112
+ },
113
+ input: Record<string, unknown>
114
+ ) {
115
+ const missing = (route.requestContract?.fields || [])
116
+ .filter((field) => field.required)
117
+ .filter((field) => input[field.name] == null || input[field.name] === "")
118
+ .map((field) => field.name);
119
+
120
+ if (missing.length === 0) {
121
+ return;
122
+ }
123
+
124
+ const requestError = (route.errors || []).find((error) => error.source === "request_contract");
125
+ throw new HttpError(
126
+ requestError?.status || 400,
127
+ requestError?.code || \`\${route.capabilityId || "request"}_invalid_request\`,
128
+ \`Missing required field(s): \${missing.join(", ")}\`
129
+ );
130
+ }
131
+
132
+ function csvValues(raw: string | undefined) {
133
+ return new Set(
134
+ String(raw || "")
135
+ .split(",")
136
+ .map((value) => value.trim())
137
+ .filter(Boolean)
138
+ );
139
+ }
140
+
141
+ function readBooleanEnv(name: string) {
142
+ return ["1", "true", "yes", "on"].includes(String(process.env[name] || "").toLowerCase());
143
+ }
144
+
145
+ function parseClaimsJson(raw: string | undefined) {
146
+ if (!raw) {
147
+ return {};
148
+ }
149
+ try {
150
+ const parsed = JSON.parse(raw);
151
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed as Record<string, unknown> : {};
152
+ } catch {
153
+ return {};
154
+ }
155
+ }
156
+
157
+ function readBearerToken(req: Request) {
158
+ const header = req.get("Authorization") || req.get("authorization") || "";
159
+ const match = header.match(/^Bearer\\s+(.+)$/i);
160
+ return match?.[1]?.trim() || "";
161
+ }
162
+
163
+ function base64UrlDecode(value: string) {
164
+ const normalized = value.replace(/-/g, "+").replace(/_/g, "/");
165
+ const padding = normalized.length % 4 === 0 ? "" : "=".repeat(4 - (normalized.length % 4));
166
+ return Buffer.from(normalized + padding, "base64").toString("utf8");
167
+ }
168
+
169
+ function parseJsonSegment(segment: string, code: string) {
170
+ try {
171
+ return JSON.parse(base64UrlDecode(segment));
172
+ } catch {
173
+ throw new HttpError(401, code, "Invalid bearer token");
174
+ }
175
+ }
176
+
177
+ function readHs256Secrets() {
178
+ const plural = process.env.TOPOGRAM_AUTH_JWT_SECRETS || "";
179
+ if (plural) {
180
+ const list = plural.split(",").map((value) => value.trim()).filter(Boolean);
181
+ if (list.length > 0) {
182
+ return list;
183
+ }
184
+ }
185
+ const singular = process.env.TOPOGRAM_AUTH_JWT_SECRET || "";
186
+ return singular ? [singular] : [];
187
+ }
188
+
189
+ function readExpectedIssuer() {
190
+ return process.env.TOPOGRAM_AUTH_JWT_ISSUER || "";
191
+ }
192
+
193
+ function readExpectedAudience() {
194
+ return process.env.TOPOGRAM_AUTH_JWT_AUDIENCE || "";
195
+ }
196
+
197
+ function audienceMatches(claim: unknown, expected: string) {
198
+ if (typeof claim === "string") {
199
+ return claim === expected;
200
+ }
201
+ if (Array.isArray(claim)) {
202
+ return claim.some((value) => typeof value === "string" && value === expected);
203
+ }
204
+ return false;
205
+ }
206
+
207
+ export function contentDisposition(disposition: string, filename: string) {
208
+ const safeDisposition = disposition === "inline" ? "inline" : "attachment";
209
+ const safeFilename = filename
210
+ .replace(/[\\r\\n"]/g, "")
211
+ .replace(/[\\\\/]/g, "_")
212
+ .trim() || "download.bin";
213
+ return \`\${safeDisposition}; filename="\${safeFilename}"\`;
214
+ }
215
+
216
+ function parsePrincipalClaims(payload: Record<string, unknown>): AuthPrincipal {
217
+ const permissions = Array.isArray(payload.permissions)
218
+ ? payload.permissions.filter((value): value is string => typeof value === "string")
219
+ : typeof payload.permissions === "string"
220
+ ? String(payload.permissions).split(",").map((value) => value.trim()).filter(Boolean)
221
+ : [];
222
+ const roles = Array.isArray(payload.roles)
223
+ ? payload.roles.filter((value): value is string => typeof value === "string")
224
+ : typeof payload.roles === "string"
225
+ ? String(payload.roles).split(",").map((value) => value.trim()).filter(Boolean)
226
+ : [];
227
+
228
+ return {
229
+ userId: typeof payload.sub === "string" ? payload.sub : "",
230
+ permissions: new Set(permissions),
231
+ roles: new Set(roles),
232
+ claims: payload,
233
+ isAdmin: payload.admin === true
234
+ };
235
+ }
236
+
237
+ function principalFromEnv(): { token: string; principal: AuthPrincipal } | null {
238
+ if ((process.env.TOPOGRAM_AUTH_PROFILE || "") !== "bearer_demo") {
239
+ return null;
240
+ }
241
+ const token = process.env.TOPOGRAM_AUTH_TOKEN || "";
242
+ if (!token) {
243
+ return null;
244
+ }
245
+
246
+ return {
247
+ token,
248
+ principal: {
249
+ userId: process.env.TOPOGRAM_AUTH_USER_ID || process.env.TOPOGRAM_DEMO_USER_ID || "",
250
+ permissions: csvValues(process.env.TOPOGRAM_AUTH_PERMISSIONS),
251
+ roles: csvValues(process.env.TOPOGRAM_AUTH_ROLES || process.env.TOPOGRAM_AUTH_ROLE),
252
+ claims: parseClaimsJson(process.env.TOPOGRAM_AUTH_CLAIMS),
253
+ isAdmin: readBooleanEnv("TOPOGRAM_AUTH_ADMIN")
254
+ }
255
+ };
256
+ }
257
+
258
+ function principalFromJwtHs256(token: string): AuthPrincipal | null {
259
+ if ((process.env.TOPOGRAM_AUTH_PROFILE || "") !== "bearer_jwt_hs256") {
260
+ return null;
261
+ }
262
+
263
+ const secrets = readHs256Secrets();
264
+ if (secrets.length === 0) {
265
+ throw new HttpError(500, "missing_auth_jwt_secret", "Missing TOPOGRAM_AUTH_JWT_SECRET or TOPOGRAM_AUTH_JWT_SECRETS");
266
+ }
267
+
268
+ const segments = token.split(".");
269
+ if (segments.length !== 3) {
270
+ throw new HttpError(401, "invalid_bearer_token", "Invalid bearer token");
271
+ }
272
+
273
+ const [encodedHeader, encodedPayload, signature] = segments;
274
+ const header = parseJsonSegment(encodedHeader, "invalid_bearer_token");
275
+ const payload = parseJsonSegment(encodedPayload, "invalid_bearer_token");
276
+
277
+ if (header?.alg !== "HS256") {
278
+ throw new HttpError(401, "invalid_bearer_token", "Invalid bearer token");
279
+ }
280
+
281
+ const actualBytes = Buffer.from(signature);
282
+ const signingInput = encodedHeader + "." + encodedPayload;
283
+ let signatureMatched = false;
284
+ for (const candidate of secrets) {
285
+ const expectedSignature = crypto
286
+ .createHmac("sha256", candidate)
287
+ .update(signingInput)
288
+ .digest("base64url");
289
+ const expectedBytes = Buffer.from(expectedSignature);
290
+ if (actualBytes.length === expectedBytes.length && crypto.timingSafeEqual(actualBytes, expectedBytes)) {
291
+ signatureMatched = true;
292
+ break;
293
+ }
294
+ }
295
+ if (!signatureMatched) {
296
+ throw new HttpError(401, "invalid_bearer_signature", "Invalid bearer token signature");
297
+ }
298
+
299
+ if (typeof payload?.exp === "number" && payload.exp <= Math.floor(Date.now() / 1000)) {
300
+ throw new HttpError(401, "expired_bearer_token", "Bearer token has expired");
301
+ }
302
+
303
+ const expectedIssuer = readExpectedIssuer();
304
+ if (expectedIssuer && payload?.iss !== expectedIssuer) {
305
+ throw new HttpError(401, "invalid_bearer_issuer", "Bearer token issuer is not trusted");
306
+ }
307
+
308
+ const expectedAudience = readExpectedAudience();
309
+ if (expectedAudience && !audienceMatches(payload?.aud, expectedAudience)) {
310
+ throw new HttpError(401, "invalid_bearer_audience", "Bearer token audience does not match");
311
+ }
312
+
313
+ return parsePrincipalClaims(payload);
314
+ }
315
+
316
+ function hasPermission(principal: AuthPrincipal, permission: string | null | undefined) {
317
+ if (!permission) {
318
+ return true;
319
+ }
320
+ return principal.permissions.has("*") || principal.permissions.has(permission);
321
+ }
322
+
323
+ function hasRole(principal: AuthPrincipal, role: string | null | undefined) {
324
+ if (!role) {
325
+ return true;
326
+ }
327
+ return principal.roles.has(role);
328
+ }
329
+
330
+ function hasClaim(principal: AuthPrincipal, claim: string | null | undefined, claimValue: string | null | undefined) {
331
+ if (!claim) {
332
+ return true;
333
+ }
334
+ const value = principal.claims[claim];
335
+ if (value == null) {
336
+ return false;
337
+ }
338
+ if (!claimValue) {
339
+ return value !== false && value !== "";
340
+ }
341
+ return String(value) === claimValue;
342
+ }
343
+
344
+ function ownerIdFromResource(
345
+ resource: Record<string, unknown> | null | undefined,
346
+ ownershipField: string | null | undefined,
347
+ options: { allowHeuristicOwnership?: boolean } = {}
348
+ ) {
349
+ if (!resource || typeof resource !== "object") {
350
+ return "";
351
+ }
352
+
353
+ if (ownershipField) {
354
+ const explicitValue = resource[ownershipField];
355
+ if (typeof explicitValue === "string" && explicitValue.length > 0) {
356
+ return explicitValue;
357
+ }
358
+ return "";
359
+ }
360
+
361
+ if (!options.allowHeuristicOwnership) {
362
+ return "";
363
+ }
364
+
365
+ for (const field of ["owner_id", "assignee_id", "author_id", "user_id", "created_by_user_id"]) {
366
+ const value = resource[field];
367
+ if (typeof value === "string" && value.length > 0) {
368
+ return value;
369
+ }
370
+ }
371
+
372
+ return "";
373
+ }
374
+
375
+ async function satisfiesOwnership(
376
+ principal: AuthPrincipal,
377
+ ownership: string | null | undefined,
378
+ ownershipField: string | null | undefined,
379
+ authorizationContext: AuthorizationContext | undefined,
380
+ options: { allowHeuristicOwnership?: boolean } = {}
381
+ ) {
382
+ if (!ownership || ownership === "none") {
383
+ return true;
384
+ }
385
+ if (ownership === "owner_or_admin" && principal.isAdmin) {
386
+ return true;
387
+ }
388
+ if (!authorizationContext?.loadResource) {
389
+ throw new HttpError(
390
+ 500,
391
+ "authorization_resource_loader_missing",
392
+ \`Missing authorization resource loader for \${authorizationContext?.capabilityId || "route"}\`
393
+ );
394
+ }
395
+
396
+ const resource = await authorizationContext.loadResource();
397
+ return ownerIdFromResource(resource, ownershipField, options) === principal.userId;
398
+ }
399
+
400
+ async function authorizeWithPrincipal(
401
+ principal: AuthPrincipal,
402
+ authz: ReadonlyArray<{ role?: string | null; permission?: string | null; claim?: string | null; claimValue?: string | null; ownership?: string | null; ownershipField?: string | null }>,
403
+ authorizationContext?: AuthorizationContext,
404
+ options: { allowHeuristicOwnership?: boolean } = {}
405
+ ) {
406
+ if (!authz || authz.length === 0) {
407
+ return;
408
+ }
409
+
410
+ for (const rule of authz) {
411
+ const roleOk = hasRole(principal, rule.role);
412
+ const permissionOk = hasPermission(principal, rule.permission);
413
+ const claimOk = hasClaim(principal, rule.claim, rule.claimValue);
414
+ const ownershipOk = await satisfiesOwnership(principal, rule.ownership, rule.ownershipField, authorizationContext, options);
415
+ if (roleOk && permissionOk && claimOk && ownershipOk) {
416
+ return;
417
+ }
418
+ }
419
+
420
+ throw new HttpError(403, "forbidden", "Bearer token does not satisfy authorization requirements");
421
+ }
422
+
423
+ export async function authorizeWithBearerDemoProfile(
424
+ req: Request,
425
+ authz: ReadonlyArray<{ role?: string | null; permission?: string | null; claim?: string | null; claimValue?: string | null; ownership?: string | null; ownershipField?: string | null }>,
426
+ authorizationContext?: AuthorizationContext
427
+ ) {
428
+ const envPrincipal = principalFromEnv();
429
+ if (!envPrincipal) {
430
+ throw new HttpError(500, "missing_auth_demo_token", "Missing TOPOGRAM_AUTH_TOKEN for bearer_demo auth profile");
431
+ }
432
+
433
+ const token = readBearerToken(req);
434
+ if (!token) {
435
+ throw new HttpError(401, "missing_bearer_token", "Missing bearer token");
436
+ }
437
+ if (token !== envPrincipal.token) {
438
+ throw new HttpError(401, "invalid_bearer_token", "Invalid bearer token");
439
+ }
440
+
441
+ await authorizeWithPrincipal(envPrincipal.principal, authz, authorizationContext, { allowHeuristicOwnership: true });
442
+ }
443
+
444
+ export async function authorizeWithBearerJwtHs256Profile(
445
+ req: Request,
446
+ authz: ReadonlyArray<{ role?: string | null; permission?: string | null; claim?: string | null; claimValue?: string | null; ownership?: string | null; ownershipField?: string | null }>,
447
+ authorizationContext?: AuthorizationContext
448
+ ) {
449
+ const token = readBearerToken(req);
450
+ if (!token) {
451
+ throw new HttpError(401, "missing_bearer_token", "Missing bearer token");
452
+ }
453
+
454
+ const principal = principalFromJwtHs256(token);
455
+ if (!principal) {
456
+ throw new HttpError(401, "invalid_bearer_token", "Invalid bearer token");
457
+ }
458
+
459
+ await authorizeWithPrincipal(principal, authz, authorizationContext);
460
+ }
461
+
462
+ export async function authorizeWithGeneratedAuthProfile(
463
+ req: Request,
464
+ authz: ReadonlyArray<{ role?: string | null; permission?: string | null; claim?: string | null; claimValue?: string | null; ownership?: string | null; ownershipField?: string | null }>,
465
+ authorizationContext?: AuthorizationContext
466
+ ) {
467
+ if (!authz || authz.length === 0) {
468
+ return;
469
+ }
470
+
471
+ const profile = process.env.TOPOGRAM_AUTH_PROFILE || "";
472
+ if (profile === "bearer_demo") {
473
+ await authorizeWithBearerDemoProfile(req, authz, authorizationContext);
474
+ return;
475
+ }
476
+ if (profile === "bearer_jwt_hs256") {
477
+ await authorizeWithBearerJwtHs256Profile(req, authz, authorizationContext);
478
+ return;
479
+ }
480
+ throw new HttpError(
481
+ 500,
482
+ profile ? "unsupported_auth_profile" : "missing_auth_profile",
483
+ profile ? \`Unsupported TOPOGRAM_AUTH_PROFILE: \${profile}\` : "Missing TOPOGRAM_AUTH_PROFILE for protected route"
484
+ );
485
+ }
486
+ `;
487
+ }
488
+
489
+ function renderExpressServerContextTs(contract, graph, options = {}) {
490
+ const implementation = getExampleImplementation(graph, options);
491
+ const repositoryReference = implementation.backend.repositoryReference;
492
+ const repositoryInterfaceName = repositoryReference.repositoryInterfaceName;
493
+ const dependencyName = repositoryReference.dependencyName;
494
+ return `import type { Request } from "express";
495
+ import type { ${repositoryInterfaceName} } from "../persistence/repositories";
496
+ import type { AuthorizationContext } from "./helpers";
497
+ import { serverContract } from "../topogram/server-contract";
498
+
499
+ export interface ServerDependencies {
500
+ ${dependencyName}: ${repositoryInterfaceName};
501
+ ready?: () => Promise<void> | void;
502
+ authorize?: (
503
+ req: Request,
504
+ authz: (typeof serverContract.routes)[number]["endpoint"]["authz"],
505
+ authorizationContext?: AuthorizationContext
506
+ ) => Promise<void> | void;
507
+ }
508
+ `;
509
+ }
510
+
511
+ function renderExpressServerIndexTs(graph, options = {}) {
512
+ const implementation = getExampleImplementation(graph, options);
513
+ const backendReference = implementation.backend.reference;
514
+ const runtimeReference = implementation.runtime.reference;
515
+ const repositoryReference = implementation.backend.repositoryReference;
516
+ const serviceName = backendReference.serviceName;
517
+ const defaultPort = runtimeReference?.ports?.server || 3000;
518
+ const prismaRepositoryClassName = repositoryReference.prismaRepositoryClassName;
519
+ const dependencyName = repositoryReference.dependencyName;
520
+ const dependencyDeclaration = `const ${dependencyName} = new ${prismaRepositoryClassName}(prisma);`;
521
+ return `import { PrismaClient } from "@prisma/client";
522
+ import { createApp } from "./lib/server/app";
523
+ import { ${prismaRepositoryClassName} } from "./lib/persistence/prisma/repositories";
524
+ import { authorizeWithGeneratedAuthProfile } from "./lib/server/helpers";
525
+
526
+ export function createServer() {
527
+ const prisma = new PrismaClient();
528
+ ${dependencyDeclaration}
529
+ return createApp({
530
+ ${dependencyName},
531
+ ready: async () => {
532
+ await prisma.$queryRaw\`SELECT 1\`;
533
+ },
534
+ authorize: async (req, authz, authorizationContext) => {
535
+ await authorizeWithGeneratedAuthProfile(req, authz, authorizationContext);
536
+ }
537
+ });
538
+ }
539
+
540
+ const app = createServer();
541
+ const port = Number(process.env.PORT || ${defaultPort});
542
+
543
+ app.listen(port, () => {
544
+ console.log(\`${serviceName} listening on http://localhost:\${port}\`);
545
+ });
546
+ `;
547
+ }
548
+
549
+ function renderExpressServerPackageJson() {
550
+ return `${JSON.stringify({
551
+ name: "topogram-express-server",
552
+ private: true,
553
+ type: "module",
554
+ scripts: {
555
+ dev: "tsx watch src/index.ts",
556
+ check: "tsc --noEmit",
557
+ build: "tsc --noEmit",
558
+ "seed:demo": "node ./scripts/seed-demo.mjs"
559
+ },
560
+ dependencies: {
561
+ "@prisma/client": "^5.22.0",
562
+ express: "^4.21.2"
563
+ },
564
+ devDependencies: {
565
+ "@types/express": "^5.0.1",
566
+ "@types/node": "^22.10.2",
567
+ prisma: "^5.22.0",
568
+ typescript: "^5.6.3",
569
+ tsx: "^4.19.2"
570
+ }
571
+ }, null, 2)}\n`;
572
+ }
573
+
574
+ function renderExpressServerAppTs(realization) {
575
+ const { contract, lookupRoutes } = realization;
576
+ const lines = [];
577
+ const typeImportNames = routeTypeNames(contract);
578
+ const serviceName = realization.backendReference.serviceName;
579
+ const defaultWebPort = realization.runtimeReference?.ports?.web || 5173;
580
+ const repositoryReference = realization.repositoryReference;
581
+ const dependencyName = repositoryReference.dependencyName;
582
+ const preconditionCapabilityIds = repositoryReference.preconditionCapabilityIds;
583
+ const preconditionResource = repositoryReference.preconditionResource;
584
+ const preconditionVariableName = preconditionResource.variableName || "currentResource";
585
+ const downloadCapabilityId = repositoryReference.downloadCapabilityId;
586
+
587
+ lines.push('import express, { type Request, type Response } from "express";');
588
+ lines.push('import { serverContract } from "../topogram/server-contract";');
589
+ lines.push('import { HttpError, coerceValue, contentDisposition, jsonError, requireHeaders, requireRequestFields } from "./helpers";');
590
+ lines.push('import type { ServerDependencies } from "./context";');
591
+ lines.push(`import type { ${typeImportNames.join(", ")} } from "../persistence/types";`);
592
+ lines.push("");
593
+ lines.push("function firstQueryValue(raw: unknown): string | undefined {");
594
+ lines.push(" if (typeof raw === \"string\") return raw;");
595
+ lines.push(" if (Array.isArray(raw)) return typeof raw[0] === \"string\" ? raw[0] : undefined;");
596
+ lines.push(" return undefined;");
597
+ lines.push("}");
598
+ lines.push("");
599
+ lines.push("function buildInput(req: Request, route: any, body: Record<string, unknown>) {");
600
+ lines.push(" const input: Record<string, unknown> = {};");
601
+ lines.push(" for (const field of (route.requestContract?.transport.path || []) as any[]) {");
602
+ lines.push(" input[field.name] = coerceValue(req.params[field.transport.wireName], field.schema);");
603
+ lines.push(" }");
604
+ lines.push(" for (const field of (route.requestContract?.transport.query || []) as any[]) {");
605
+ lines.push(" input[field.name] = coerceValue(firstQueryValue(req.query[field.transport.wireName]), field.schema);");
606
+ lines.push(" }");
607
+ lines.push(" for (const field of (route.requestContract?.transport.header || []) as any[]) {");
608
+ lines.push(" input[field.name] = coerceValue(req.get(field.transport.wireName), field.schema);");
609
+ lines.push(" }");
610
+ lines.push(" for (const field of (route.requestContract?.transport.body || []) as any[]) {");
611
+ lines.push(' const defaultValue = field.schema && typeof field.schema === "object" && "default" in field.schema ? field.schema.default : undefined;');
612
+ lines.push(" input[field.name] = body[field.transport.wireName] ?? defaultValue;");
613
+ lines.push(" }");
614
+ lines.push(" return input;");
615
+ lines.push("}");
616
+ lines.push("");
617
+ lines.push("function corsOrigin(req: Request) {");
618
+ lines.push(` const configured = process.env.TOPOGRAM_CORS_ORIGINS || "http://localhost:${defaultWebPort},http://127.0.0.1:${defaultWebPort}";`);
619
+ lines.push(" const allowed = new Set(configured.split(\",\").map((entry) => entry.trim()).filter(Boolean));");
620
+ lines.push(' const origin = req.get("Origin") || "";');
621
+ lines.push(" return allowed.has(origin) ? origin : \"\";");
622
+ lines.push("}");
623
+ lines.push("");
624
+ lines.push("export function createApp(deps: ServerDependencies) {");
625
+ lines.push(" const app = express();");
626
+ lines.push(" app.use(express.json());");
627
+ lines.push(" app.use((req, res, next) => {");
628
+ lines.push(' const allowedOrigin = corsOrigin(req);');
629
+ lines.push(' if (allowedOrigin) res.header("Access-Control-Allow-Origin", allowedOrigin);');
630
+ lines.push(' res.header("Vary", "Origin");');
631
+ lines.push(' res.header("Access-Control-Allow-Methods", "GET,POST,PATCH,DELETE,OPTIONS");');
632
+ lines.push(' res.header("Access-Control-Allow-Headers", "Content-Type,If-Match,If-None-Match,Idempotency-Key,Authorization");');
633
+ lines.push(' res.header("Access-Control-Expose-Headers", "ETag,Location,Retry-After,Content-Disposition");');
634
+ lines.push(' if (req.method === "OPTIONS") {');
635
+ lines.push(" res.status(204).end();");
636
+ lines.push(" return;");
637
+ lines.push(" }");
638
+ lines.push(" next();");
639
+ lines.push(" });");
640
+ lines.push("");
641
+ lines.push(` app.get("/health", (_req, res) => res.status(200).json({ ok: true, service: "${serviceName}" }));`);
642
+ lines.push("");
643
+ lines.push(' app.get("/ready", async (_req, res) => {');
644
+ lines.push(" try {");
645
+ lines.push(" await deps.ready?.();");
646
+ lines.push(` return res.status(200).json({ ok: true, ready: true, service: "${serviceName}" });`);
647
+ lines.push(" } catch (error) {");
648
+ lines.push(' const message = error instanceof Error ? error.message : "Readiness check failed";');
649
+ lines.push(` return res.status(503).json({ ok: false, ready: false, service: "${serviceName}", message });`);
650
+ lines.push(" }");
651
+ lines.push(" });");
652
+ lines.push("");
653
+
654
+ for (const lookup of lookupRoutes) {
655
+ lines.push(` app.get("${lookup.route}", async (_req, res) => {`);
656
+ lines.push(" try {");
657
+ lines.push(` const result = await deps.${dependencyName}.${lookup.repositoryMethod}();`);
658
+ lines.push(" return res.status(200).json(result);");
659
+ lines.push(" } catch (error) {");
660
+ lines.push(" const failure = jsonError(error);");
661
+ lines.push(" return res.status(failure.status).json(failure.body);");
662
+ lines.push(" }");
663
+ lines.push(" });");
664
+ lines.push("");
665
+ }
666
+
667
+ contract.routes.forEach((route, routeIndex) => {
668
+ const method = route.method.toLowerCase();
669
+ const routeVar = `route${routeIndex}`;
670
+ const responseMode = route.responseContract?.mode || "item";
671
+ const methodName = route.repositoryMethod;
672
+ const hasOwnershipAuthz = (route.endpoint.authz || []).some((rule) => rule.ownership && rule.ownership !== "none");
673
+ const authLoaderVar = `loadAuthorizationResource${routeIndex}`;
674
+ lines.push(` const ${routeVar} = serverContract.routes[${routeIndex}]!;`);
675
+ lines.push(` app.${method}(${routeVar}.path, async (req: Request, res: Response) => {`);
676
+ lines.push(" try {");
677
+ lines.push(' const body = req.body && typeof req.body === "object" ? req.body as Record<string, unknown> : {};');
678
+ lines.push(` const input = buildInput(req, ${routeVar}, body);`);
679
+ if ((route.endpoint.authz || []).length > 0) {
680
+ if (hasOwnershipAuthz) {
681
+ if (preconditionCapabilityIds.includes(route.capabilityId)) {
682
+ lines.push(` const ${authLoaderVar} = async () => await deps.${dependencyName}.${preconditionResource.repositoryMethod}({ ${preconditionResource.inputField}: String(input.${preconditionResource.inputField} || "") } as unknown as ${toPascalCase(preconditionResource.repositoryMethod)}Input) as unknown as Record<string, unknown>;`);
683
+ } else if (route.method === "GET" && responseMode === "item") {
684
+ lines.push(` const ${authLoaderVar} = async () => await deps.${dependencyName}.${methodName}(input as unknown as ${toPascalCase(methodName)}Input) as unknown as Record<string, unknown>;`);
685
+ } else {
686
+ lines.push(` const ${authLoaderVar} = undefined;`);
687
+ }
688
+ }
689
+ lines.push(' if (!deps.authorize) throw new HttpError(500, "authorization_handler_missing", "Missing authorization handler for protected route");');
690
+ lines.push(` await deps.authorize(req, ${routeVar}.endpoint.authz, { capabilityId: ${routeVar}.capabilityId, input, ${hasOwnershipAuthz ? `loadResource: typeof ${authLoaderVar} === "function" ? ${authLoaderVar} : undefined` : "loadResource: undefined"} });`);
691
+ }
692
+ if ((route.endpoint.preconditions || []).length > 0 || (route.endpoint.idempotency || []).length > 0) {
693
+ lines.push(` requireHeaders(req, [...${routeVar}.endpoint.preconditions, ...${routeVar}.endpoint.idempotency]);`);
694
+ }
695
+ lines.push(` requireRequestFields(${routeVar}, input);`);
696
+ if (preconditionCapabilityIds.includes(route.capabilityId)) {
697
+ lines.push(' const ifMatch = req.get("If-Match");');
698
+ lines.push(' if (ifMatch) {');
699
+ lines.push(` const ${preconditionVariableName} = await deps.${dependencyName}.${preconditionResource.repositoryMethod}({ ${preconditionResource.inputField}: String(input.${preconditionResource.inputField} || "") } as unknown as ${toPascalCase(preconditionResource.repositoryMethod)}Input);`);
700
+ lines.push(` if (${preconditionVariableName}.${preconditionResource.versionField} !== ifMatch) {`);
701
+ lines.push(' throw new HttpError(412, "stale_precondition", "If-Match does not match the current resource version");');
702
+ lines.push(" }");
703
+ lines.push(" }");
704
+ }
705
+
706
+ if (route.capabilityId === downloadCapabilityId) {
707
+ lines.push(` const artifact = await deps.${dependencyName}.${methodName}(input as unknown as ${toPascalCase(methodName)}Input);`);
708
+ lines.push(` res.setHeader("Content-Type", artifact.contentType || "${route.endpoint.download?.[0]?.media || "application/octet-stream"}");`);
709
+ lines.push(` res.setHeader("Content-Disposition", contentDisposition("${route.endpoint.download?.[0]?.disposition || "attachment"}", artifact.filename || "${route.endpoint.download?.[0]?.filename || "download.bin"}"));`);
710
+ lines.push(` return res.status(${route.successStatus}).send(artifact.body as any);`);
711
+ } else {
712
+ lines.push(` const result = await deps.${dependencyName}.${methodName}(input as unknown as ${toPascalCase(methodName)}Input);`);
713
+ if ((route.endpoint.cache || []).length > 0) {
714
+ const cacheRule = route.endpoint.cache[0];
715
+ lines.push(` const etag = (result as unknown as Record<string, unknown>)["${cacheRule.source}"];`);
716
+ lines.push(` if (etag && req.get("${cacheRule.requestHeader}") === String(etag)) {`);
717
+ lines.push(` return res.status(${cacheRule.notModified}).end();`);
718
+ lines.push(" }");
719
+ lines.push(` if (etag) res.setHeader("${cacheRule.responseHeader}", String(etag));`);
720
+ }
721
+ if ((route.endpoint.async || []).length > 0) {
722
+ const asyncRule = route.endpoint.async[0];
723
+ lines.push(` res.setHeader("${asyncRule.locationHeader}", (result as unknown as Record<string, unknown>).status_url ? String((result as unknown as Record<string, unknown>).status_url) : "${asyncRule.statusPath}".replace(":job_id", String((result as unknown as Record<string, unknown>).job_id ?? "")));`);
724
+ lines.push(` res.setHeader("${asyncRule.retryAfterHeader}", "5");`);
725
+ }
726
+ if (responseMode === "item" || responseMode === "cursor" || responseMode === "paged" || responseMode === "collection") {
727
+ lines.push(` return res.status(${route.successStatus}).json(result as ${toPascalCase(methodName)}Result);`);
728
+ } else {
729
+ lines.push(` return res.status(${route.successStatus}).json(result);`);
730
+ }
731
+ }
732
+ lines.push(" } catch (error) {");
733
+ lines.push(" const failure = jsonError(error);");
734
+ lines.push(" return res.status(failure.status).json(failure.body);");
735
+ lines.push(" }");
736
+ lines.push(" });");
737
+ lines.push("");
738
+ });
739
+
740
+ lines.push(" return app;");
741
+ lines.push("}");
742
+ return `${lines.join("\n").trimEnd()}\n`;
743
+ }
744
+
745
+ export function generateExpressServer(graph, options = {}) {
746
+ const projection = getProjection(graph, options.projectionId);
747
+ const realization = buildBackendRuntimeRealization(graph, options);
748
+ const contract = realization.contract;
749
+ const persistenceScaffold = generatePersistenceScaffold(graph, { ...options, projectionId: realization.dbProjection.id });
750
+ const prismaSchema = generateDbTarget("prisma-schema", graph, { projectionId: realization.dbProjection.id });
751
+
752
+ return {
753
+ "package.json": renderExpressServerPackageJson(),
754
+ "tsconfig.json": renderServerTsconfig(),
755
+ "scripts/seed-demo.mjs": renderServerSeedScript(graph, options),
756
+ "src/index.ts": renderExpressServerIndexTs(graph, options),
757
+ "src/lib/topogram/server-contract.ts": renderServerContractModule(graph, projection.id),
758
+ "src/lib/server/helpers.ts": renderExpressServerHelpers(),
759
+ "src/lib/server/context.ts": renderExpressServerContextTs(contract, graph, options),
760
+ "src/lib/server/app.ts": renderExpressServerAppTs(realization),
761
+ "src/lib/persistence/types.ts": persistenceScaffold["types.ts"],
762
+ "src/lib/persistence/repositories.ts": persistenceScaffold["repositories.ts"],
763
+ "src/lib/persistence/prisma/repositories.ts": persistenceScaffold["prisma/repositories.ts"],
764
+ "prisma/schema.prisma": prismaSchema
765
+ };
766
+ }