@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,156 @@
1
+ import Foundation
2
+
3
+ /// Dynamic capability client mirroring `src/lib/api/client.ts` (requestCapability over bundled api-contracts.json).
4
+ public final class TodoAPIClient: @unchecked Sendable {
5
+ private let session: URLSession
6
+ let contracts: [String: Any]
7
+ public init(session: URLSession = .shared, contractsData: Data) throws {
8
+ self.session = session
9
+ guard let root = try JSONSerialization.jsonObject(with: contractsData) as? [String: Any] else {
10
+ throw TodoAPIError.invalidContracts
11
+ }
12
+ self.contracts = root
13
+ }
14
+
15
+ public static func loadBundledContracts() throws -> Data {
16
+ guard let url = Bundle.module.url(forResource: "api-contracts", withExtension: "json"),
17
+ let data = try? Data(contentsOf: url) else {
18
+ throw TodoAPIError.missingResource("api-contracts.json")
19
+ }
20
+ return data
21
+ }
22
+
23
+ private func apiBase() -> String {
24
+ let env = ProcessInfo.processInfo.environment
25
+ return env["PUBLIC_TOPOGRAM_API_BASE_URL"] ?? "http://localhost:3000"
26
+ }
27
+
28
+ private func authToken() -> String {
29
+ ProcessInfo.processInfo.environment["PUBLIC_TOPOGRAM_DEMO_AUTH_TOKEN"] ?? ""
30
+ }
31
+
32
+ private func buildPath(contract: [String: Any], input: [String: Any]) throws -> String {
33
+ guard let endpoint = contract["endpoint"] as? [String: Any],
34
+ let rawPath = endpoint["path"] as? String else {
35
+ throw TodoAPIError.invalidContracts
36
+ }
37
+ var path = rawPath
38
+ let requestContract = contract["requestContract"] as? [String: Any]
39
+ let transport = requestContract?["transport"] as? [String: Any]
40
+ let pathFields = transport?["path"] as? [[String: Any]] ?? []
41
+ for field in pathFields {
42
+ guard let name = field["name"] as? String else { continue }
43
+ let wire = (field["transport"] as? [String: Any])?["wireName"] as? String ?? name
44
+ let raw = input[name]
45
+ path = path.replacingOccurrences(of: ":\(wire)", with: encodePathSegment(raw))
46
+ }
47
+ var query: [String] = []
48
+ let queryFields = transport?["query"] as? [[String: Any]] ?? []
49
+ for field in queryFields {
50
+ guard let name = field["name"] as? String else { continue }
51
+ let wire = (field["transport"] as? [String: Any])?["wireName"] as? String ?? name
52
+ if let raw = input[name] {
53
+ if raw is NSNull { continue }
54
+ let s = String(describing: raw)
55
+ if !s.isEmpty {
56
+ query.append("\(wire)=\(encodeQueryComponent(s))")
57
+ }
58
+ }
59
+ }
60
+ if !query.isEmpty {
61
+ path += "?" + query.joined(separator: "&")
62
+ }
63
+ return path
64
+ }
65
+
66
+ private func encodePathSegment(_ value: Any?) -> String {
67
+ let s = value.map { String(describing: $0) } ?? ""
68
+ return s.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? s
69
+ }
70
+
71
+ private func encodeQueryComponent(_ value: String) -> String {
72
+ value.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? value
73
+ }
74
+
75
+ public func requestCapability(
76
+ _ capabilityId: String,
77
+ input: [String: Any] = [:],
78
+ extraHeaders: [String: String] = [:]
79
+ ) async throws -> Any {
80
+ guard let contract = contracts[capabilityId] as? [String: Any] else {
81
+ throw TodoAPIError.unknownCapability(capabilityId)
82
+ }
83
+ guard let endpoint = contract["endpoint"] as? [String: Any],
84
+ let method = endpoint["method"] as? String else {
85
+ throw TodoAPIError.invalidContracts
86
+ }
87
+ let path = try buildPath(contract: contract, input: input)
88
+ guard let url = URL(string: path, relativeTo: URL(string: apiBase()))?.absoluteURL else {
89
+ throw TodoAPIError.badURL(path)
90
+ }
91
+ var request = URLRequest(url: url)
92
+ request.httpMethod = method
93
+
94
+ var headers = extraHeaders
95
+ let authz = endpoint["authz"] as? [[String: Any]] ?? []
96
+ if !authz.isEmpty, !authToken().isEmpty {
97
+ if headers["Authorization"] == nil {
98
+ headers["Authorization"] = "Bearer " + authToken()
99
+ }
100
+ }
101
+ for (k, v) in headers {
102
+ request.setValue(v, forHTTPHeaderField: k)
103
+ }
104
+
105
+ let requestContract = contract["requestContract"] as? [String: Any]
106
+ let transport = requestContract?["transport"] as? [String: Any]
107
+ let bodyFields = transport?["body"] as? [[String: Any]] ?? []
108
+ if !bodyFields.isEmpty {
109
+ request.setValue("application/json", forHTTPHeaderField: "Content-Type")
110
+ var payload: [String: Any] = [:]
111
+ for field in bodyFields {
112
+ guard let name = field["name"] as? String else { continue }
113
+ let wire = (field["transport"] as? [String: Any])?["wireName"] as? String ?? name
114
+ if let v = input[name] {
115
+ payload[wire] = v
116
+ }
117
+ }
118
+ request.httpBody = try JSONSerialization.data(withJSONObject: payload)
119
+ }
120
+
121
+ let downloadable = endpoint["download"] as? [[String: Any]] ?? []
122
+ let (data, response) = try await session.data(for: request)
123
+ guard let http = response as? HTTPURLResponse else {
124
+ throw TodoAPIError.invalidResponse
125
+ }
126
+ guard (200 ..< 300).contains(http.statusCode) else {
127
+ let text = String(data: data, encoding: .utf8) ?? ""
128
+ throw TodoAPIError.http(http.statusCode, text)
129
+ }
130
+ if http.statusCode == 204 {
131
+ return NSNull()
132
+ }
133
+ if !downloadable.isEmpty {
134
+ return data
135
+ }
136
+ return try JSONSerialization.jsonObject(with: data)
137
+ }
138
+
139
+ public func extractRows(from json: Any) -> [[String: Any]] {
140
+ if let rows = json as? [[String: Any]] { return rows }
141
+ if let dict = json as? [String: Any] {
142
+ if let items = dict["items"] as? [[String: Any]] { return items }
143
+ if let data = dict["data"] as? [[String: Any]] { return data }
144
+ }
145
+ return []
146
+ }
147
+ }
148
+
149
+ public enum TodoAPIError: Error {
150
+ case invalidContracts
151
+ case missingResource(String)
152
+ case unknownCapability(String)
153
+ case badURL(String)
154
+ case invalidResponse
155
+ case http(Int, String)
156
+ }
@@ -0,0 +1,44 @@
1
+ import SwiftUI
2
+
3
+ @main
4
+ struct TodoSwiftUIApp: App {
5
+ @StateObject private var contractHolder = ContractHolder()
6
+
7
+ var body: some Scene {
8
+ WindowGroup {
9
+ Group {
10
+ if let contract = contractHolder.contract, let client = contractHolder.client {
11
+ RootTabView(contract: contract, client: client)
12
+ } else if let err = contractHolder.error {
13
+ Text("Failed to load Topogram UI contract: \(err)")
14
+ .padding()
15
+ } else {
16
+ ProgressView("Loading Topogram Todo…")
17
+ }
18
+ }
19
+ .task {
20
+ await contractHolder.bootstrap()
21
+ }
22
+ }
23
+ }
24
+ }
25
+
26
+ @MainActor
27
+ final class ContractHolder: ObservableObject {
28
+ @Published var contract: TodoUiContract?
29
+ @Published var client: TodoAPIClient?
30
+ @Published var error: String?
31
+
32
+ func bootstrap() async {
33
+ do {
34
+ let uiData = try TodoUiContract.loadBundled()
35
+ let apiData = try TodoAPIClient.loadBundledContracts()
36
+ let ui = try TodoUiContract(data: uiData)
37
+ let cli = try TodoAPIClient(contractsData: apiData)
38
+ contract = ui
39
+ client = cli
40
+ } catch {
41
+ self.error = String(describing: error)
42
+ }
43
+ }
44
+ }
@@ -0,0 +1,183 @@
1
+ import Foundation
2
+
3
+ /// Port of web `visibility.ts` predicates used by dynamic screens.
4
+ public enum Visibility {
5
+ public struct Rule {
6
+ public let predicate: String?
7
+ public let value: String?
8
+ public let claimValue: String?
9
+ public let ownershipField: String?
10
+ public let capabilityId: String?
11
+ }
12
+
13
+ public struct PrincipalOverride {
14
+ public var userId: String?
15
+ public var permissions: [String]?
16
+ public var roles: [String]?
17
+ public var claims: [String: Any]?
18
+ public var isAdmin: Bool?
19
+ }
20
+
21
+ public static func canShow(
22
+ rule: Rule?,
23
+ resource: [String: Any]?,
24
+ overrides: PrincipalOverride? = nil
25
+ ) -> Bool {
26
+ guard let rule else { return true }
27
+ guard let principal = currentPrincipal(overrides: overrides) else { return true }
28
+
29
+ switch rule.predicate {
30
+ case "permission":
31
+ guard let value = rule.value else { return true }
32
+ return principal.permissions.contains("*") || principal.permissions.contains(value)
33
+ case "ownership":
34
+ guard let value = rule.value else { return true }
35
+ if value == "none" { return true }
36
+ if value == "owner_or_admin", principal.isAdmin { return true }
37
+ let owner = ownerId(from: resource, field: rule.ownershipField)
38
+ return owner == principal.userId
39
+ case "claim":
40
+ let claim = rule.value
41
+ let expected = rule.claimValue
42
+ guard let claim else { return true }
43
+ let raw = principal.claims[claim]
44
+ if expected == nil || expected?.isEmpty == true {
45
+ if raw == nil { return false }
46
+ if let b = raw as? Bool { return b }
47
+ if let s = raw as? String { return !s.isEmpty }
48
+ return true
49
+ }
50
+ return String(describing: raw ?? "") == expected
51
+ default:
52
+ return true
53
+ }
54
+ }
55
+
56
+ private static func env(_ key: String) -> String {
57
+ ProcessInfo.processInfo.environment[key] ?? ""
58
+ }
59
+
60
+ private static func csv(_ raw: String) -> [String] {
61
+ raw.split(separator: ",").map { String($0).trimmingCharacters(in: .whitespaces) }.filter { !$0.isEmpty }
62
+ }
63
+
64
+ private static func parseClaims(_ raw: String) -> [String: Any] {
65
+ guard let data = raw.data(using: .utf8),
66
+ let obj = try? JSONSerialization.jsonObject(with: data),
67
+ let dict = obj as? [String: Any] else {
68
+ return [:]
69
+ }
70
+ return dict
71
+ }
72
+
73
+ private static func decodeJwtPayload(_ token: String) -> [String: Any]? {
74
+ let parts = token.split(separator: ".")
75
+ guard parts.count > 1 else { return nil }
76
+ var body = String(parts[1])
77
+ body = body.replacingOccurrences(of: "-", with: "+").replacingOccurrences(of: "_", with: "/")
78
+ let pad = 4 - body.count % 4
79
+ if pad < 4 { body += String(repeating: "=", count: pad) }
80
+ guard let data = Data(base64Encoded: body),
81
+ let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else {
82
+ return nil
83
+ }
84
+ return json
85
+ }
86
+
87
+ private static func principalFromJwt(_ token: String) -> Principal? {
88
+ guard let payload = decodeJwtPayload(token) else { return nil }
89
+ let sub = payload["sub"] as? String ?? ""
90
+ let permissions = Set((payload["permissions"] as? [String]) ?? [])
91
+ let roles = Set((payload["roles"] as? [String]) ?? [])
92
+ let admin = payload["admin"] as? Bool ?? false
93
+ return Principal(
94
+ userId: sub,
95
+ permissions: permissions,
96
+ roles: roles,
97
+ claims: payload,
98
+ isAdmin: admin
99
+ )
100
+ }
101
+
102
+ private struct Principal {
103
+ let userId: String
104
+ let permissions: Set<String>
105
+ let roles: Set<String>
106
+ let claims: [String: Any]
107
+ let isAdmin: Bool
108
+ }
109
+
110
+ private static func currentPrincipal(overrides: PrincipalOverride?) -> Principal? {
111
+ let token = env("PUBLIC_TOPOGRAM_DEMO_AUTH_TOKEN")
112
+ let jwtPrincipal = token.isEmpty ? nil : principalFromJwt(token)
113
+ let envClaims = parseClaims(env("PUBLIC_TOPOGRAM_AUTH_CLAIMS"))
114
+ let userId = overrides?.userId
115
+ ?? env("PUBLIC_TOPOGRAM_AUTH_USER_ID")
116
+ ?? jwtPrincipal?.userId
117
+ ?? ""
118
+
119
+ var permissions = Set(csv(env("PUBLIC_TOPOGRAM_AUTH_PERMISSIONS")))
120
+ permissions.formUnion(jwtPrincipal?.permissions ?? [])
121
+ permissions.formUnion(overrides?.permissions ?? [])
122
+
123
+ var roles = Set(csv(env("PUBLIC_TOPOGRAM_AUTH_ROLES")))
124
+ if roles.isEmpty { roles = Set(csv(env("PUBLIC_TOPOGRAM_AUTH_ROLE"))) }
125
+ roles.formUnion(jwtPrincipal?.roles ?? [])
126
+ roles.formUnion(overrides?.roles ?? [])
127
+
128
+ let adminFlag =
129
+ (overrides?.isAdmin ?? false)
130
+ || env("PUBLIC_TOPOGRAM_AUTH_ADMIN").lowercased() == "true"
131
+ || jwtPrincipal?.isAdmin == true
132
+
133
+ var claims = jwtPrincipal?.claims ?? [:]
134
+ claims.merge(envClaims) { _, new in new }
135
+ if let o = overrides?.claims { claims.merge(o) { _, new in new } }
136
+
137
+ if token.isEmpty && userId.isEmpty && permissions.isEmpty && roles.isEmpty && claims.isEmpty && !adminFlag {
138
+ return nil
139
+ }
140
+
141
+ return Principal(
142
+ userId: userId,
143
+ permissions: permissions,
144
+ roles: roles,
145
+ claims: claims,
146
+ isAdmin: adminFlag
147
+ )
148
+ }
149
+
150
+ private static func ownerId(from resource: [String: Any]?, field: String?) -> String {
151
+ guard let resource else { return "" }
152
+ if let field {
153
+ if let v = resource[field] as? String { return v }
154
+ }
155
+ for key in ["owner_id", "assignee_id", "author_id", "user_id", "created_by_user_id"] {
156
+ if let v = resource[key] as? String { return v }
157
+ }
158
+ return ""
159
+ }
160
+
161
+ public static func visibilityRules(from screen: [String: Any]) -> [Rule] {
162
+ guard let raw = screen["visibility"] as? [[String: Any]] else { return [] }
163
+ return raw.map { entry in
164
+ let cap = entry["capability"] as? [String: Any]
165
+ return Rule(
166
+ predicate: entry["predicate"] as? String,
167
+ value: entry["value"] as? String,
168
+ claimValue: entry["claimValue"] as? String,
169
+ ownershipField: entry["ownershipField"] as? String,
170
+ capabilityId: cap?["id"] as? String
171
+ )
172
+ }
173
+ }
174
+
175
+ public static func canShowAction(_ capabilityId: String?, screen: [String: Any], resource: [String: Any]?) -> Bool {
176
+ guard let capabilityId else { return true }
177
+ let rules = visibilityRules(from: screen).filter { $0.capabilityId == capabilityId }
178
+ for rule in rules {
179
+ if !canShow(rule: rule, resource: resource) { return false }
180
+ }
181
+ return true
182
+ }
183
+ }
@@ -0,0 +1 @@
1
+ export function generateExpressServer(graph: any, options?: any): any;