@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,631 @@
1
+ import { toPascalCase } from "../databases/shared.js";
2
+ import { getExampleImplementation } from "../../../example-implementation.js";
3
+
4
+ export function renderServerHelpers() {
5
+ return `import crypto from "node:crypto";
6
+ import type { Context } from "hono";
7
+
8
+ export class HttpError extends Error {
9
+ constructor(
10
+ public readonly status: number,
11
+ public readonly code: string,
12
+ message = code
13
+ ) {
14
+ super(message);
15
+ }
16
+ }
17
+
18
+ export interface DownloadArtifact {
19
+ body: BodyInit | Uint8Array | null;
20
+ contentType?: string;
21
+ filename?: string;
22
+ }
23
+
24
+ export interface AuthorizationContext {
25
+ capabilityId?: string;
26
+ input?: Record<string, unknown>;
27
+ loadResource?: () => Promise<Record<string, unknown> | null | undefined>;
28
+ }
29
+
30
+ interface AuthPrincipal {
31
+ userId: string;
32
+ permissions: Set<string>;
33
+ roles: Set<string>;
34
+ claims: Record<string, unknown>;
35
+ isAdmin: boolean;
36
+ }
37
+
38
+ export function jsonError(error: unknown) {
39
+ if (error instanceof HttpError) {
40
+ return {
41
+ status: error.status,
42
+ body: {
43
+ error: {
44
+ code: error.code,
45
+ message: error.message
46
+ }
47
+ }
48
+ };
49
+ }
50
+
51
+ return {
52
+ status: 500,
53
+ body: {
54
+ error: {
55
+ code: "internal_server_error",
56
+ message: "Internal server error"
57
+ }
58
+ }
59
+ };
60
+ }
61
+
62
+ export function coerceValue(raw: string | undefined, schema: { type?: string; format?: string; enum?: readonly string[]; default?: unknown }) {
63
+ if (raw == null || raw === "") {
64
+ return schema.default;
65
+ }
66
+ if (schema.enum) {
67
+ return raw;
68
+ }
69
+ if (schema.type === "integer" || schema.type === "number") {
70
+ const parsed = Number(raw);
71
+ if (Number.isNaN(parsed)) {
72
+ throw new HttpError(400, "invalid_number", \`Invalid numeric value: \${raw}\`);
73
+ }
74
+ if (schema.type === "integer" && !Number.isInteger(parsed)) {
75
+ throw new HttpError(400, "invalid_integer", \`Invalid integer value: \${raw}\`);
76
+ }
77
+ return parsed;
78
+ }
79
+ if (schema.type === "boolean") {
80
+ return raw === "true";
81
+ }
82
+ return raw;
83
+ }
84
+
85
+ export function requireHeaders(c: Context, headers: ReadonlyArray<{ header: string; required?: boolean; code?: string; error?: number }>) {
86
+ for (const rule of headers) {
87
+ if (!rule.required) {
88
+ continue;
89
+ }
90
+ if (!c.req.header(rule.header)) {
91
+ throw new HttpError(rule.error || 400, rule.code || "missing_required_header", \`Missing required header \${rule.header}\`);
92
+ }
93
+ }
94
+ }
95
+
96
+ export function requireRequestFields(
97
+ route: {
98
+ capabilityId?: string;
99
+ errors?: ReadonlyArray<{ code?: string; source?: string; status?: number }>;
100
+ requestContract?: { fields?: ReadonlyArray<{ name: string; required?: boolean }> };
101
+ },
102
+ input: Record<string, unknown>
103
+ ) {
104
+ const missing = (route.requestContract?.fields || [])
105
+ .filter((field) => field.required)
106
+ .filter((field) => input[field.name] == null || input[field.name] === "")
107
+ .map((field) => field.name);
108
+
109
+ if (missing.length === 0) {
110
+ return;
111
+ }
112
+
113
+ const requestError = (route.errors || []).find((error) => error.source === "request_contract");
114
+ throw new HttpError(
115
+ requestError?.status || 400,
116
+ requestError?.code || \`\${route.capabilityId || "request"}_invalid_request\`,
117
+ \`Missing required field(s): \${missing.join(", ")}\`
118
+ );
119
+ }
120
+
121
+ function csvValues(raw: string | undefined) {
122
+ return new Set(
123
+ String(raw || "")
124
+ .split(",")
125
+ .map((value) => value.trim())
126
+ .filter(Boolean)
127
+ );
128
+ }
129
+
130
+ function readBooleanEnv(name: string) {
131
+ return ["1", "true", "yes", "on"].includes(String(process.env[name] || "").toLowerCase());
132
+ }
133
+
134
+ function parseClaimsJson(raw: string | undefined) {
135
+ if (!raw) {
136
+ return {};
137
+ }
138
+ try {
139
+ const parsed = JSON.parse(raw);
140
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed as Record<string, unknown> : {};
141
+ } catch {
142
+ return {};
143
+ }
144
+ }
145
+
146
+ function readBearerToken(c: Context) {
147
+ const header = c.req.header("Authorization") || c.req.header("authorization") || "";
148
+ const match = header.match(/^Bearer\\s+(.+)$/i);
149
+ return match?.[1]?.trim() || "";
150
+ }
151
+
152
+ function base64UrlDecode(value: string) {
153
+ const normalized = value.replace(/-/g, "+").replace(/_/g, "/");
154
+ const padding = normalized.length % 4 === 0 ? "" : "=".repeat(4 - (normalized.length % 4));
155
+ return Buffer.from(normalized + padding, "base64").toString("utf8");
156
+ }
157
+
158
+ function parseJsonSegment(segment: string, code: string) {
159
+ try {
160
+ return JSON.parse(base64UrlDecode(segment));
161
+ } catch {
162
+ throw new HttpError(401, code, "Invalid bearer token");
163
+ }
164
+ }
165
+
166
+ function readHs256Secrets() {
167
+ const plural = process.env.TOPOGRAM_AUTH_JWT_SECRETS || "";
168
+ if (plural) {
169
+ const list = plural.split(",").map((value) => value.trim()).filter(Boolean);
170
+ if (list.length > 0) {
171
+ return list;
172
+ }
173
+ }
174
+ const singular = process.env.TOPOGRAM_AUTH_JWT_SECRET || "";
175
+ return singular ? [singular] : [];
176
+ }
177
+
178
+ function readExpectedIssuer() {
179
+ return process.env.TOPOGRAM_AUTH_JWT_ISSUER || "";
180
+ }
181
+
182
+ function readExpectedAudience() {
183
+ return process.env.TOPOGRAM_AUTH_JWT_AUDIENCE || "";
184
+ }
185
+
186
+ function audienceMatches(claim: unknown, expected: string) {
187
+ if (typeof claim === "string") {
188
+ return claim === expected;
189
+ }
190
+ if (Array.isArray(claim)) {
191
+ return claim.some((value) => typeof value === "string" && value === expected);
192
+ }
193
+ return false;
194
+ }
195
+
196
+ export function contentDisposition(disposition: string, filename: string) {
197
+ const safeDisposition = disposition === "inline" ? "inline" : "attachment";
198
+ const safeFilename = filename
199
+ .replace(/[\\r\\n"]/g, "")
200
+ .replace(/[\\\\/]/g, "_")
201
+ .trim() || "download.bin";
202
+ return \`\${safeDisposition}; filename="\${safeFilename}"\`;
203
+ }
204
+
205
+ function parsePrincipalClaims(payload: Record<string, unknown>): AuthPrincipal {
206
+ const permissions = Array.isArray(payload.permissions)
207
+ ? payload.permissions.filter((value): value is string => typeof value === "string")
208
+ : typeof payload.permissions === "string"
209
+ ? String(payload.permissions).split(",").map((value) => value.trim()).filter(Boolean)
210
+ : [];
211
+ const roles = Array.isArray(payload.roles)
212
+ ? payload.roles.filter((value): value is string => typeof value === "string")
213
+ : typeof payload.roles === "string"
214
+ ? String(payload.roles).split(",").map((value) => value.trim()).filter(Boolean)
215
+ : [];
216
+
217
+ return {
218
+ userId: typeof payload.sub === "string" ? payload.sub : "",
219
+ permissions: new Set(permissions),
220
+ roles: new Set(roles),
221
+ claims: payload,
222
+ isAdmin: payload.admin === true
223
+ };
224
+ }
225
+
226
+ function principalFromEnv(): { token: string; principal: AuthPrincipal } | null {
227
+ if ((process.env.TOPOGRAM_AUTH_PROFILE || "") !== "bearer_demo") {
228
+ return null;
229
+ }
230
+ const token = process.env.TOPOGRAM_AUTH_TOKEN || "";
231
+ if (!token) {
232
+ return null;
233
+ }
234
+
235
+ return {
236
+ token,
237
+ principal: {
238
+ userId: process.env.TOPOGRAM_AUTH_USER_ID || process.env.TOPOGRAM_DEMO_USER_ID || "",
239
+ permissions: csvValues(process.env.TOPOGRAM_AUTH_PERMISSIONS),
240
+ roles: csvValues(process.env.TOPOGRAM_AUTH_ROLES || process.env.TOPOGRAM_AUTH_ROLE),
241
+ claims: parseClaimsJson(process.env.TOPOGRAM_AUTH_CLAIMS),
242
+ isAdmin: readBooleanEnv("TOPOGRAM_AUTH_ADMIN")
243
+ }
244
+ };
245
+ }
246
+
247
+ function principalFromJwtHs256(token: string): AuthPrincipal | null {
248
+ if ((process.env.TOPOGRAM_AUTH_PROFILE || "") !== "bearer_jwt_hs256") {
249
+ return null;
250
+ }
251
+
252
+ const secrets = readHs256Secrets();
253
+ if (secrets.length === 0) {
254
+ throw new HttpError(500, "missing_auth_jwt_secret", "Missing TOPOGRAM_AUTH_JWT_SECRET or TOPOGRAM_AUTH_JWT_SECRETS");
255
+ }
256
+
257
+ const segments = token.split(".");
258
+ if (segments.length !== 3) {
259
+ throw new HttpError(401, "invalid_bearer_token", "Invalid bearer token");
260
+ }
261
+
262
+ const [encodedHeader, encodedPayload, signature] = segments;
263
+ const header = parseJsonSegment(encodedHeader, "invalid_bearer_token");
264
+ const payload = parseJsonSegment(encodedPayload, "invalid_bearer_token");
265
+
266
+ if (header?.alg !== "HS256") {
267
+ throw new HttpError(401, "invalid_bearer_token", "Invalid bearer token");
268
+ }
269
+
270
+ const actualBytes = Buffer.from(signature);
271
+ const signingInput = encodedHeader + "." + encodedPayload;
272
+ let signatureMatched = false;
273
+ for (const candidate of secrets) {
274
+ const expectedSignature = crypto
275
+ .createHmac("sha256", candidate)
276
+ .update(signingInput)
277
+ .digest("base64url");
278
+ const expectedBytes = Buffer.from(expectedSignature);
279
+ if (actualBytes.length === expectedBytes.length && crypto.timingSafeEqual(actualBytes, expectedBytes)) {
280
+ signatureMatched = true;
281
+ break;
282
+ }
283
+ }
284
+ if (!signatureMatched) {
285
+ throw new HttpError(401, "invalid_bearer_signature", "Invalid bearer token signature");
286
+ }
287
+
288
+ if (typeof payload?.exp === "number" && payload.exp <= Math.floor(Date.now() / 1000)) {
289
+ throw new HttpError(401, "expired_bearer_token", "Bearer token has expired");
290
+ }
291
+
292
+ const expectedIssuer = readExpectedIssuer();
293
+ if (expectedIssuer && payload?.iss !== expectedIssuer) {
294
+ throw new HttpError(401, "invalid_bearer_issuer", "Bearer token issuer is not trusted");
295
+ }
296
+
297
+ const expectedAudience = readExpectedAudience();
298
+ if (expectedAudience && !audienceMatches(payload?.aud, expectedAudience)) {
299
+ throw new HttpError(401, "invalid_bearer_audience", "Bearer token audience does not match");
300
+ }
301
+
302
+ return parsePrincipalClaims(payload);
303
+ }
304
+
305
+ function hasPermission(principal: AuthPrincipal, permission: string | null | undefined) {
306
+ if (!permission) {
307
+ return true;
308
+ }
309
+ return principal.permissions.has("*") || principal.permissions.has(permission);
310
+ }
311
+
312
+ function hasRole(principal: AuthPrincipal, role: string | null | undefined) {
313
+ if (!role) {
314
+ return true;
315
+ }
316
+ return principal.roles.has(role);
317
+ }
318
+
319
+ function hasClaim(principal: AuthPrincipal, claim: string | null | undefined, claimValue: string | null | undefined) {
320
+ if (!claim) {
321
+ return true;
322
+ }
323
+ const value = principal.claims[claim];
324
+ if (value == null) {
325
+ return false;
326
+ }
327
+ if (!claimValue) {
328
+ return value !== false && value !== "";
329
+ }
330
+ return String(value) === claimValue;
331
+ }
332
+
333
+ function ownerIdFromResource(
334
+ resource: Record<string, unknown> | null | undefined,
335
+ ownershipField: string | null | undefined,
336
+ options: { allowHeuristicOwnership?: boolean } = {}
337
+ ) {
338
+ if (!resource || typeof resource !== "object") {
339
+ return "";
340
+ }
341
+
342
+ if (ownershipField) {
343
+ const explicitValue = resource[ownershipField];
344
+ if (typeof explicitValue === "string" && explicitValue.length > 0) {
345
+ return explicitValue;
346
+ }
347
+ return "";
348
+ }
349
+
350
+ if (!options.allowHeuristicOwnership) {
351
+ return "";
352
+ }
353
+
354
+ for (const field of ["owner_id", "assignee_id", "author_id", "user_id", "created_by_user_id"]) {
355
+ const value = resource[field];
356
+ if (typeof value === "string" && value.length > 0) {
357
+ return value;
358
+ }
359
+ }
360
+
361
+ return "";
362
+ }
363
+
364
+ async function satisfiesOwnership(
365
+ principal: AuthPrincipal,
366
+ ownership: string | null | undefined,
367
+ ownershipField: string | null | undefined,
368
+ authorizationContext: AuthorizationContext | undefined,
369
+ options: { allowHeuristicOwnership?: boolean } = {}
370
+ ) {
371
+ if (!ownership || ownership === "none") {
372
+ return true;
373
+ }
374
+ if (ownership === "owner_or_admin" && principal.isAdmin) {
375
+ return true;
376
+ }
377
+ if (!authorizationContext?.loadResource) {
378
+ throw new HttpError(
379
+ 500,
380
+ "authorization_resource_loader_missing",
381
+ \`Missing authorization resource loader for \${authorizationContext?.capabilityId || "route"}\`
382
+ );
383
+ }
384
+
385
+ const resource = await authorizationContext.loadResource();
386
+ return ownerIdFromResource(resource, ownershipField, options) === principal.userId;
387
+ }
388
+
389
+ async function authorizeWithPrincipal(
390
+ principal: AuthPrincipal,
391
+ authz: ReadonlyArray<{ role?: string | null; permission?: string | null; claim?: string | null; claimValue?: string | null; ownership?: string | null; ownershipField?: string | null }>,
392
+ authorizationContext?: AuthorizationContext,
393
+ options: { allowHeuristicOwnership?: boolean } = {}
394
+ ) {
395
+ if (!authz || authz.length === 0) {
396
+ return;
397
+ }
398
+
399
+ for (const rule of authz) {
400
+ const roleOk = hasRole(principal, rule.role);
401
+ const permissionOk = hasPermission(principal, rule.permission);
402
+ const claimOk = hasClaim(principal, rule.claim, rule.claimValue);
403
+ const ownershipOk = await satisfiesOwnership(principal, rule.ownership, rule.ownershipField, authorizationContext, options);
404
+ if (roleOk && permissionOk && claimOk && ownershipOk) {
405
+ return;
406
+ }
407
+ }
408
+
409
+ throw new HttpError(403, "forbidden", "Bearer token does not satisfy authorization requirements");
410
+ }
411
+
412
+ export async function authorizeWithBearerDemoProfile(
413
+ c: Context,
414
+ authz: ReadonlyArray<{ role?: string | null; permission?: string | null; claim?: string | null; claimValue?: string | null; ownership?: string | null; ownershipField?: string | null }>,
415
+ authorizationContext?: AuthorizationContext
416
+ ) {
417
+ const envPrincipal = principalFromEnv();
418
+ if (!envPrincipal) {
419
+ throw new HttpError(500, "missing_auth_demo_token", "Missing TOPOGRAM_AUTH_TOKEN for bearer_demo auth profile");
420
+ }
421
+
422
+ const token = readBearerToken(c);
423
+ if (!token) {
424
+ throw new HttpError(401, "missing_bearer_token", "Missing bearer token");
425
+ }
426
+ if (token !== envPrincipal.token) {
427
+ throw new HttpError(401, "invalid_bearer_token", "Invalid bearer token");
428
+ }
429
+
430
+ await authorizeWithPrincipal(envPrincipal.principal, authz, authorizationContext, { allowHeuristicOwnership: true });
431
+ }
432
+
433
+ export async function authorizeWithBearerJwtHs256Profile(
434
+ c: Context,
435
+ authz: ReadonlyArray<{ role?: string | null; permission?: string | null; claim?: string | null; claimValue?: string | null; ownership?: string | null; ownershipField?: string | null }>,
436
+ authorizationContext?: AuthorizationContext
437
+ ) {
438
+ const token = readBearerToken(c);
439
+ if (!token) {
440
+ throw new HttpError(401, "missing_bearer_token", "Missing bearer token");
441
+ }
442
+
443
+ const principal = principalFromJwtHs256(token);
444
+ if (!principal) {
445
+ throw new HttpError(401, "invalid_bearer_token", "Invalid bearer token");
446
+ }
447
+
448
+ await authorizeWithPrincipal(principal, authz, authorizationContext);
449
+ }
450
+
451
+ export async function authorizeWithGeneratedAuthProfile(
452
+ c: Context,
453
+ authz: ReadonlyArray<{ role?: string | null; permission?: string | null; claim?: string | null; claimValue?: string | null; ownership?: string | null; ownershipField?: string | null }>,
454
+ authorizationContext?: AuthorizationContext
455
+ ) {
456
+ if (!authz || authz.length === 0) {
457
+ return;
458
+ }
459
+
460
+ const profile = process.env.TOPOGRAM_AUTH_PROFILE || "";
461
+ if (profile === "bearer_demo") {
462
+ await authorizeWithBearerDemoProfile(c, authz, authorizationContext);
463
+ return;
464
+ }
465
+ if (profile === "bearer_jwt_hs256") {
466
+ await authorizeWithBearerJwtHs256Profile(c, authz, authorizationContext);
467
+ return;
468
+ }
469
+ throw new HttpError(
470
+ 500,
471
+ profile ? "unsupported_auth_profile" : "missing_auth_profile",
472
+ profile ? \`Unsupported TOPOGRAM_AUTH_PROFILE: \${profile}\` : "Missing TOPOGRAM_AUTH_PROFILE for protected route"
473
+ );
474
+ }
475
+ `;
476
+ }
477
+
478
+ export function renderServerContextTs(contract, graph, options = {}) {
479
+ const implementation = getExampleImplementation(graph, options);
480
+ const repositoryReference = implementation.backend.repositoryReference;
481
+ const repositoryInterfaceName = repositoryReference.repositoryInterfaceName;
482
+ const dependencyName = repositoryReference.dependencyName;
483
+ return `import type { Context } from "hono";
484
+ import type { ${repositoryInterfaceName} } from "../persistence/repositories";
485
+ import type { AuthorizationContext } from "./helpers";
486
+ import { serverContract } from "../topogram/server-contract";
487
+
488
+ export interface ServerDependencies {
489
+ ${dependencyName}: ${repositoryInterfaceName};
490
+ ready?: () => Promise<void> | void;
491
+ authorize?: (
492
+ ctx: Context,
493
+ authz: (typeof serverContract.routes)[number]["endpoint"]["authz"],
494
+ authorizationContext?: AuthorizationContext
495
+ ) => Promise<void> | void;
496
+ }
497
+ `;
498
+ }
499
+
500
+ export function renderServerIndexTs(graph, options = {}) {
501
+ const implementation = getExampleImplementation(graph, options);
502
+ const backendReference = implementation.backend.reference;
503
+ const runtimeReference = implementation.runtime.reference;
504
+ const repositoryReference = implementation.backend.repositoryReference;
505
+ const serviceName = backendReference.serviceName;
506
+ const defaultPort = runtimeReference?.ports?.server || 3000;
507
+ const prismaRepositoryClassName = repositoryReference.prismaRepositoryClassName;
508
+ const dependencyName = repositoryReference.dependencyName;
509
+ const dependencyDeclaration = `const ${dependencyName} = new ${prismaRepositoryClassName}(prisma);`;
510
+ return `import { serve } from "@hono/node-server";
511
+ import { PrismaClient } from "@prisma/client";
512
+ import { createApp } from "./lib/server/app";
513
+ import { ${prismaRepositoryClassName} } from "./lib/persistence/prisma/repositories";
514
+ import { authorizeWithGeneratedAuthProfile } from "./lib/server/helpers";
515
+
516
+ export function createServer() {
517
+ const prisma = new PrismaClient();
518
+ ${dependencyDeclaration}
519
+ return createApp({
520
+ ${dependencyName},
521
+ ready: async () => {
522
+ await prisma.$queryRaw\`SELECT 1\`;
523
+ },
524
+ authorize: async (ctx, authz, authorizationContext) => {
525
+ await authorizeWithGeneratedAuthProfile(ctx, authz, authorizationContext);
526
+ }
527
+ });
528
+ }
529
+
530
+ const app = createServer();
531
+ const port = Number(process.env.PORT || ${defaultPort});
532
+
533
+ serve({
534
+ fetch: app.fetch,
535
+ port
536
+ });
537
+
538
+ console.log(\`${serviceName} listening on http://localhost:\${port}\`);
539
+ `;
540
+ }
541
+
542
+ export function renderServerPackageJson() {
543
+ return `${JSON.stringify({
544
+ name: "topogram-server",
545
+ private: true,
546
+ type: "module",
547
+ scripts: {
548
+ dev: "tsx watch src/index.ts",
549
+ check: "tsc --noEmit",
550
+ build: "tsc --noEmit",
551
+ "seed:demo": "node ./scripts/seed-demo.mjs"
552
+ },
553
+ dependencies: {
554
+ "@hono/node-server": "^1.13.7",
555
+ "@prisma/client": "^5.22.0",
556
+ hono: "^4.6.12"
557
+ },
558
+ devDependencies: {
559
+ "@types/node": "^22.10.2",
560
+ prisma: "^5.22.0",
561
+ typescript: "^5.6.3",
562
+ tsx: "^4.19.2"
563
+ }
564
+ }, null, 2)}\n`;
565
+ }
566
+
567
+ export function renderServerTsconfig() {
568
+ return `${JSON.stringify({
569
+ compilerOptions: {
570
+ target: "ES2022",
571
+ module: "ESNext",
572
+ moduleResolution: "Bundler",
573
+ types: ["node"],
574
+ strict: true,
575
+ esModuleInterop: true,
576
+ forceConsistentCasingInFileNames: true,
577
+ skipLibCheck: true,
578
+ resolveJsonModule: true
579
+ },
580
+ include: ["src/**/*.ts"]
581
+ }, null, 2)}\n`;
582
+ }
583
+
584
+ export function renderServerSeedScript(graph, options = {}) {
585
+ const implementation = getExampleImplementation(graph, options);
586
+ return implementation.backend.reference.renderSeedScript();
587
+ }
588
+
589
+ export function lookupRouteSegment(entityId) {
590
+ const base = String(entityId || "").replace(/^entity_/, "");
591
+ if (!base) {
592
+ return "options";
593
+ }
594
+ if (base.endsWith("y")) {
595
+ return `${base.slice(0, -1)}ies`;
596
+ }
597
+ if (base.endsWith("s")) {
598
+ return `${base}es`;
599
+ }
600
+ return `${base}s`;
601
+ }
602
+
603
+ export function uiLookupBindings(graph) {
604
+ const bindings = [];
605
+ for (const projection of graph.byKind.projection || []) {
606
+ for (const entry of projection.uiLookups || []) {
607
+ if (entry.entity?.id) {
608
+ bindings.push(entry);
609
+ }
610
+ }
611
+ }
612
+ return bindings;
613
+ }
614
+
615
+ export function repositoryMethodName(capabilityId) {
616
+ const base = capabilityId.replace(/^cap_/, "");
617
+ return base.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase());
618
+ }
619
+
620
+ export function routeTypeNames(contract) {
621
+ const typeImportNames = new Set();
622
+ for (const route of contract.routes) {
623
+ const method = route.repositoryMethod;
624
+ typeImportNames.add(`${toPascalCase(method)}Input`);
625
+ typeImportNames.add(`${toPascalCase(method)}Result`);
626
+ if (route.responseContract?.mode && route.responseContract.mode !== "item") {
627
+ typeImportNames.add(`${toPascalCase(method)}ResultItem`);
628
+ }
629
+ }
630
+ return [...typeImportNames].sort();
631
+ }
@@ -0,0 +1,80 @@
1
+ import { generateApiContractGraph } from "../../api.js";
2
+ import { getProjection } from "../shared.js";
3
+ import { toPascalCase } from "../databases/shared.js";
4
+
5
+ function indexStatements(graph) {
6
+ const byId = new Map();
7
+ for (const statement of graph.statements) {
8
+ byId.set(statement.id, statement);
9
+ }
10
+ return byId;
11
+ }
12
+
13
+ function apiProjectionCandidates(graph) {
14
+ return (graph.byKind.projection || []).filter((projection) => (projection.http || []).length > 0);
15
+ }
16
+
17
+ function repositoryMethodName(capabilityId) {
18
+ const base = capabilityId.replace(/^cap_/, "");
19
+ return base.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase());
20
+ }
21
+
22
+ function buildServerContract(graph, projection) {
23
+ const byId = indexStatements(graph);
24
+ const realizedCapabilities = (projection.realizes || [])
25
+ .map((ref) => byId.get(ref.id))
26
+ .filter((statement) => statement?.kind === "capability");
27
+
28
+ return {
29
+ type: "server_contract_graph",
30
+ projection: {
31
+ id: projection.id,
32
+ name: projection.name || projection.id,
33
+ platform: projection.platform
34
+ },
35
+ routes: realizedCapabilities.map((capability) => {
36
+ const apiContract = generateApiContractGraph(graph, { capabilityId: capability.id });
37
+ return {
38
+ capabilityId: capability.id,
39
+ handlerName: `handle${toPascalCase(capability.id.replace(/^cap_/, ""))}`,
40
+ repositoryMethod: repositoryMethodName(capability.id),
41
+ method: apiContract.endpoint.method,
42
+ path: apiContract.endpoint.path,
43
+ successStatus: apiContract.endpoint.successStatus,
44
+ requestContract: apiContract.requestContract,
45
+ responseContract: apiContract.responseContract || null,
46
+ errors: apiContract.errors,
47
+ endpoint: {
48
+ auth: apiContract.endpoint.auth,
49
+ authz: apiContract.endpoint.authz || [],
50
+ preconditions: apiContract.endpoint.preconditions || [],
51
+ idempotency: apiContract.endpoint.idempotency || [],
52
+ cache: apiContract.endpoint.cache || [],
53
+ async: apiContract.endpoint.async || [],
54
+ status: apiContract.endpoint.status || [],
55
+ download: apiContract.endpoint.download || []
56
+ }
57
+ };
58
+ })
59
+ };
60
+ }
61
+
62
+ function renderServerContractsTs(contract) {
63
+ return `export const serverContract = ${JSON.stringify(contract, null, 2)} as const;\n`;
64
+ }
65
+
66
+ export function generateServerContract(graph, options = {}) {
67
+ if (options.projectionId) {
68
+ return buildServerContract(graph, getProjection(graph, options.projectionId));
69
+ }
70
+
71
+ const output = {};
72
+ for (const projection of apiProjectionCandidates(graph)) {
73
+ output[projection.id] = buildServerContract(graph, projection);
74
+ }
75
+ return output;
76
+ }
77
+
78
+ export function renderServerContractModule(graph, projectionId) {
79
+ return renderServerContractsTs(buildServerContract(graph, getProjection(graph, projectionId)));
80
+ }
@@ -0,0 +1 @@
1
+ export function generateStatelessServer(graph: any, options?: any): any;