handoff-app 1.2.2-7 → 2.0.0-0

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 (349) hide show
  1. package/Changelog.md +37 -15
  2. package/README.md +372 -118
  3. package/dist/app-builder/build.d.ts +41 -2
  4. package/dist/app-builder/build.js +455 -18
  5. package/dist/app-builder/client-config.d.ts +15 -2
  6. package/dist/app-builder/client-config.js +43 -4
  7. package/dist/app-builder/config-diff/snapshot.d.ts +1 -6
  8. package/dist/app-builder/config-diff/snapshot.js +2 -9
  9. package/dist/app-builder/index.d.ts +1 -1
  10. package/dist/app-builder/index.js +3 -1
  11. package/dist/app-builder/nav-shell.d.ts +23 -0
  12. package/dist/app-builder/nav-shell.js +80 -0
  13. package/dist/app-builder/static-export.d.ts +17 -0
  14. package/dist/app-builder/static-export.js +181 -0
  15. package/dist/app-builder/vercel-output.d.ts +39 -0
  16. package/dist/app-builder/vercel-output.js +259 -0
  17. package/dist/app-builder/watchers/utils.d.ts +2 -0
  18. package/dist/app-builder/watchers.d.ts +14 -4
  19. package/dist/app-builder/watchers.js +123 -28
  20. package/dist/app-builder/websocket.js +1 -1
  21. package/dist/artifacts/index.d.ts +2 -0
  22. package/dist/artifacts/index.js +13 -0
  23. package/dist/artifacts/types.d.ts +136 -0
  24. package/dist/artifacts/types.js +24 -0
  25. package/dist/artifacts/url.d.ts +49 -0
  26. package/dist/artifacts/url.js +86 -0
  27. package/dist/cache/build-cache.js +48 -4
  28. package/dist/cli.js +0 -0
  29. package/dist/commands/build/app.js +1 -2
  30. package/dist/commands/build/index.d.ts +17 -0
  31. package/dist/commands/build/index.js +66 -0
  32. package/dist/commands/checkout/index.d.ts +24 -0
  33. package/dist/commands/checkout/index.js +71 -0
  34. package/dist/commands/db/migrate.d.ts +11 -0
  35. package/dist/commands/db/migrate.js +33 -0
  36. package/dist/commands/index.js +8 -0
  37. package/dist/commands/publish/index.d.ts +21 -0
  38. package/dist/commands/publish/index.js +68 -0
  39. package/dist/config/defaults.js +15 -1
  40. package/dist/config/loader.js +6 -1
  41. package/dist/config/normalizers/declaration.js +4 -1
  42. package/dist/config/normalizers/page.d.ts +18 -0
  43. package/dist/config/normalizers/page.js +34 -0
  44. package/dist/config/normalizers/pattern.js +7 -1
  45. package/dist/config/runtime.d.ts +4 -0
  46. package/dist/config/runtime.js +53 -5
  47. package/dist/index.d.ts +65 -1
  48. package/dist/index.js +123 -3
  49. package/dist/nav/index.d.ts +83 -0
  50. package/dist/nav/index.js +9 -0
  51. package/dist/nav/registry.d.ts +15 -0
  52. package/dist/nav/registry.js +42 -0
  53. package/dist/nav/resolver.d.ts +8 -0
  54. package/dist/nav/resolver.js +192 -0
  55. package/dist/nav/workspace.d.ts +18 -0
  56. package/dist/nav/workspace.js +103 -0
  57. package/dist/pipeline/styles.js +7 -35
  58. package/dist/pipeline/token-transformers.d.ts +45 -0
  59. package/dist/pipeline/token-transformers.js +71 -0
  60. package/dist/registry/asset-storage/adapters/vercel-blob.d.ts +17 -0
  61. package/dist/registry/asset-storage/adapters/vercel-blob.js +87 -0
  62. package/dist/registry/asset-storage/define.d.ts +10 -0
  63. package/dist/registry/asset-storage/define.js +13 -0
  64. package/dist/registry/asset-storage/resolve.d.ts +45 -0
  65. package/dist/registry/asset-storage/resolve.js +36 -0
  66. package/dist/registry/asset-storage/types.d.ts +51 -0
  67. package/dist/registry/asset-storage/types.js +9 -0
  68. package/dist/registry/assets/layout.d.ts +22 -0
  69. package/dist/registry/assets/layout.js +30 -0
  70. package/dist/registry/assets/sets.d.ts +18 -0
  71. package/dist/registry/assets/sets.js +44 -0
  72. package/dist/registry/assets/transfer.d.ts +45 -0
  73. package/dist/registry/assets/transfer.js +11 -0
  74. package/dist/registry/checkout/assets.d.ts +16 -0
  75. package/dist/registry/checkout/assets.js +150 -0
  76. package/dist/registry/checkout/index.d.ts +33 -0
  77. package/dist/registry/checkout/index.js +610 -0
  78. package/dist/registry/checkout/tokens.d.ts +15 -0
  79. package/dist/registry/checkout/tokens.js +171 -0
  80. package/dist/registry/client.d.ts +61 -0
  81. package/dist/registry/client.js +211 -0
  82. package/dist/registry/connection.d.ts +30 -0
  83. package/dist/registry/connection.js +36 -0
  84. package/dist/registry/db/adapter.d.ts +43 -0
  85. package/dist/registry/db/adapter.js +59 -0
  86. package/dist/registry/db/client.d.ts +35 -0
  87. package/dist/registry/db/client.js +94 -0
  88. package/dist/registry/db/index.d.ts +5 -0
  89. package/dist/registry/db/index.js +24 -0
  90. package/dist/registry/db/migrate.d.ts +23 -0
  91. package/dist/registry/db/migrate.js +64 -0
  92. package/dist/registry/db/schema.d.ts +4869 -0
  93. package/dist/registry/db/schema.js +347 -0
  94. package/dist/registry/path.d.ts +4 -0
  95. package/dist/registry/path.js +31 -0
  96. package/dist/registry/publish/artifacts.d.ts +16 -0
  97. package/dist/registry/publish/artifacts.js +132 -0
  98. package/dist/registry/publish/assets.d.ts +16 -0
  99. package/dist/registry/publish/assets.js +129 -0
  100. package/dist/registry/publish/errors.d.ts +5 -0
  101. package/dist/registry/publish/errors.js +26 -0
  102. package/dist/registry/publish/index.d.ts +27 -0
  103. package/dist/registry/publish/index.js +126 -0
  104. package/dist/registry/publish/package.d.ts +27 -0
  105. package/dist/registry/publish/package.js +257 -0
  106. package/dist/registry/publish/publish-build.d.ts +12 -0
  107. package/dist/registry/publish/publish-build.js +43 -0
  108. package/dist/registry/publish/tokens.d.ts +18 -0
  109. package/dist/registry/publish/tokens.js +112 -0
  110. package/dist/registry/tokens/sets.d.ts +59 -0
  111. package/dist/registry/tokens/sets.js +109 -0
  112. package/dist/registry/tokens/transfer.d.ts +48 -0
  113. package/dist/registry/tokens/transfer.js +10 -0
  114. package/dist/registry/transfer.d.ts +75 -0
  115. package/dist/registry/transfer.js +11 -0
  116. package/dist/store/filesystem-assets.d.ts +17 -0
  117. package/dist/store/filesystem-assets.js +147 -0
  118. package/dist/store/filesystem.d.ts +76 -0
  119. package/dist/store/filesystem.js +208 -0
  120. package/dist/store/index.d.ts +20 -0
  121. package/dist/store/index.js +38 -0
  122. package/dist/store/registry.d.ts +77 -0
  123. package/dist/store/registry.js +350 -0
  124. package/dist/store/source-files.d.ts +24 -0
  125. package/dist/store/source-files.js +147 -0
  126. package/dist/store/types.d.ts +185 -0
  127. package/dist/store/types.js +12 -0
  128. package/dist/transformers/plugins/csf-render.js +17 -8
  129. package/dist/transformers/plugins/handlebars-previews.js +10 -3
  130. package/dist/transformers/plugins/ssr-render.js +116 -49
  131. package/dist/transformers/preview/component/api.js +3 -1
  132. package/dist/transformers/preview/component/artifacts.js +8 -1
  133. package/dist/transformers/preview/component/builder.js +13 -5
  134. package/dist/transformers/preview/component/css.js +9 -1
  135. package/dist/transformers/preview/component/javascript.d.ts +9 -3
  136. package/dist/transformers/preview/component/javascript.js +45 -17
  137. package/dist/transformers/preview/component/shared-artifacts.d.ts +83 -0
  138. package/dist/transformers/preview/component/shared-artifacts.js +92 -0
  139. package/dist/transformers/preview/component.js +5 -2
  140. package/dist/transformers/preview/pattern/builder.js +28 -9
  141. package/dist/transformers/preview/pattern/html.d.ts +13 -10
  142. package/dist/transformers/preview/pattern/html.js +62 -27
  143. package/dist/transformers/preview/types.d.ts +38 -0
  144. package/dist/transformers/utils/handlebars.d.ts +3 -0
  145. package/dist/transformers/utils/handlebars.js +19 -5
  146. package/dist/types/config.d.ts +118 -25
  147. package/dist/utils/menu-shell.d.ts +60 -0
  148. package/dist/utils/menu-shell.js +221 -0
  149. package/dist/utils/stable-stringify.d.ts +2 -0
  150. package/dist/utils/stable-stringify.js +8 -0
  151. package/docs/cli.md +4 -2
  152. package/drizzle/0000_init_registry_storage.sql +95 -0
  153. package/drizzle/0001_add_review_metadata.sql +2 -0
  154. package/drizzle/0002_add_pages.sql +28 -0
  155. package/drizzle/0003_add_tokens.sql +29 -0
  156. package/drizzle/0004_add_assets.sql +37 -0
  157. package/drizzle/meta/0000_snapshot.json +642 -0
  158. package/drizzle/meta/0001_snapshot.json +654 -0
  159. package/drizzle/meta/0002_snapshot.json +835 -0
  160. package/drizzle/meta/0003_snapshot.json +1023 -0
  161. package/drizzle/meta/0004_snapshot.json +1252 -0
  162. package/drizzle/meta/_journal.json +41 -0
  163. package/drizzle.config.ts +16 -0
  164. package/package.json +10 -1
  165. package/src/app/components/Component/Preview.tsx +3 -2
  166. package/src/app/components/Component/RegistryActions.tsx +150 -0
  167. package/src/app/components/Foundations/DisplayLogo.tsx +5 -3
  168. package/src/app/components/Layout/Header.tsx +21 -3
  169. package/src/app/components/Layout/Main.tsx +11 -3
  170. package/src/app/components/Navigation/MainNav.tsx +4 -2
  171. package/src/app/components/Navigation/MobileNav.tsx +4 -2
  172. package/src/app/components/Navigation/SideNav.tsx +46 -11
  173. package/src/app/components/NotFound.tsx +24 -0
  174. package/src/app/components/context/NavProvider.tsx +71 -0
  175. package/src/app/components/old/Header.tsx +3 -3
  176. package/src/app/components/util/index.ts +203 -380
  177. package/src/app/components/util/useCollectionAssets.ts +125 -0
  178. package/src/app/components/util/useFoundationTokens.ts +67 -0
  179. package/src/app/generated/default-pages.json +1 -0
  180. package/src/app/generated/nav-shell.json +1 -0
  181. package/src/app/lib/api/query.ts +6 -0
  182. package/src/app/lib/api/redact.ts +46 -0
  183. package/src/app/lib/asset-storage.ts +81 -0
  184. package/src/app/lib/docs-api/artifacts.ts +116 -0
  185. package/src/app/lib/docs-api/backend.ts +151 -0
  186. package/src/app/lib/docs-api/errors.ts +52 -0
  187. package/src/app/lib/docs-api/handler.ts +34 -0
  188. package/src/app/lib/docs-api/index.ts +7 -0
  189. package/src/app/lib/docs-api/records.ts +189 -0
  190. package/src/app/lib/docs-api/registry.ts +130 -0
  191. package/src/app/lib/docs-api/runtime-config.ts +173 -0
  192. package/src/app/lib/docs-api/serve.ts +37 -0
  193. package/src/app/lib/docs-api/token-detail.ts +19 -0
  194. package/src/app/lib/registry-api/allowlist.ts +163 -0
  195. package/src/app/lib/registry-api/asset-transfer.ts +363 -0
  196. package/src/app/lib/registry-api/auth.ts +68 -0
  197. package/src/app/lib/registry-api/entity-routes.ts +197 -0
  198. package/src/app/lib/registry-api/errors.ts +51 -0
  199. package/src/app/lib/registry-api/files.ts +135 -0
  200. package/src/app/lib/registry-api/handler.ts +96 -0
  201. package/src/app/lib/registry-api/index.ts +4 -0
  202. package/src/app/lib/registry-api/meta.ts +87 -0
  203. package/src/app/lib/registry-api/revalidate.ts +36 -0
  204. package/src/app/lib/registry-api/store.ts +348 -0
  205. package/src/app/lib/registry-api/token-transfer.ts +231 -0
  206. package/src/app/lib/registry-api/transfer.ts +513 -0
  207. package/src/app/lib/registry-api/validation.ts +74 -0
  208. package/src/app/lib/registry-connection.ts +58 -0
  209. package/src/app/lib/utils.ts +14 -0
  210. package/src/app/next.config.mjs +72 -13
  211. package/src/app/pages/[...slug]/index.tsx +74 -20
  212. package/src/app/pages/_app.tsx +5 -2
  213. package/src/app/pages/api/docs/artifacts/[...artifactPath].ts +16 -0
  214. package/src/app/pages/api/docs/assets/[collection]/[...path].ts +50 -0
  215. package/src/app/pages/api/docs/assets/[collection]/index.ts +22 -0
  216. package/src/app/pages/api/docs/components/[id]/assets/[asset].ts +33 -0
  217. package/src/app/pages/api/docs/components/[id]/inspect/[previewId].ts +20 -0
  218. package/src/app/pages/api/docs/components/[id]/previews/[previewId].ts +21 -0
  219. package/src/app/pages/api/docs/components/[id].ts +24 -0
  220. package/src/app/pages/api/docs/components.json.ts +12 -0
  221. package/src/app/pages/api/docs/nav.json.ts +48 -0
  222. package/src/app/pages/api/docs/patterns/[id]/preview.ts +19 -0
  223. package/src/app/pages/api/docs/patterns/[id].ts +24 -0
  224. package/src/app/pages/api/docs/patterns.json.ts +12 -0
  225. package/src/app/pages/api/docs/tokens/components/[id].ts +30 -0
  226. package/src/app/pages/api/docs/tokens/foundations/[type].ts +32 -0
  227. package/src/app/pages/api/registry/components/[id]/files/[...filePath].ts +19 -0
  228. package/src/app/pages/api/registry/components/[id]/files/index.ts +19 -0
  229. package/src/app/pages/api/registry/components/[id].ts +20 -0
  230. package/src/app/pages/api/registry/components/index.ts +19 -0
  231. package/src/app/pages/api/registry/health.ts +20 -0
  232. package/src/app/pages/api/registry/patterns/[id]/files/[...filePath].ts +19 -0
  233. package/src/app/pages/api/registry/patterns/[id]/files/index.ts +19 -0
  234. package/src/app/pages/api/registry/patterns/[id].ts +20 -0
  235. package/src/app/pages/api/registry/patterns/index.ts +19 -0
  236. package/src/app/pages/api/registry/transfer/[kind]/[id].ts +39 -0
  237. package/src/app/pages/api/registry/transfer/assets/[collection].ts +24 -0
  238. package/src/app/pages/api/registry/transfer/assets/blobs/[hash].ts +23 -0
  239. package/src/app/pages/api/registry/transfer/assets/blobs/have.ts +11 -0
  240. package/src/app/pages/api/registry/transfer/assets/index.ts +10 -0
  241. package/src/app/pages/api/registry/transfer/tokens/[...setId].ts +26 -0
  242. package/src/app/pages/api/registry/transfer/tokens/index.ts +11 -0
  243. package/src/app/pages/assets/fonts.tsx +41 -17
  244. package/src/app/pages/assets/index.tsx +2 -2
  245. package/src/app/pages/assets/logos.tsx +2 -2
  246. package/src/app/pages/foundations/colors.tsx +18 -8
  247. package/src/app/pages/foundations/effects.tsx +18 -8
  248. package/src/app/pages/foundations/icons/[name]/index.tsx +26 -14
  249. package/src/app/pages/foundations/icons/index.tsx +13 -9
  250. package/src/app/pages/foundations/index.tsx +1 -1
  251. package/src/app/pages/foundations/logo.tsx +7 -4
  252. package/src/app/pages/foundations/typography.tsx +19 -9
  253. package/src/app/pages/index.tsx +1 -1
  254. package/src/app/pages/system/component/[component]/index.tsx +87 -17
  255. package/src/app/pages/system/index.tsx +13 -4
  256. package/src/app/pages/system/pattern/[pattern]/index.tsx +70 -14
  257. package/src/app/pages/system/tokens/components/[component]/index.tsx +42 -14
  258. package/src/app/pages/system/tokens/foundations/colors.tsx +7 -5
  259. package/src/app/pages/system/tokens/foundations/effects.tsx +7 -5
  260. package/src/app/pages/system/tokens/foundations/typography.tsx +8 -4
  261. package/src/app/tsconfig.json +2 -3
  262. package/src/app-builder/build.ts +555 -11
  263. package/src/app-builder/client-config.ts +61 -4
  264. package/src/app-builder/config-diff/snapshot.ts +1 -11
  265. package/src/app-builder/index.ts +1 -1
  266. package/src/app-builder/nav-shell.ts +74 -0
  267. package/src/app-builder/static-export.ts +215 -0
  268. package/src/app-builder/vercel-output.ts +297 -0
  269. package/src/app-builder/watchers/utils.ts +2 -0
  270. package/src/app-builder/watchers.ts +149 -25
  271. package/src/app-builder/websocket.ts +2 -2
  272. package/src/artifacts/README.md +14 -0
  273. package/src/artifacts/index.ts +17 -0
  274. package/src/artifacts/types.ts +146 -0
  275. package/src/artifacts/url.ts +90 -0
  276. package/src/cache/build-cache.ts +47 -2
  277. package/src/commands/build/app.ts +1 -2
  278. package/src/commands/build/index.ts +61 -0
  279. package/src/commands/checkout/index.ts +66 -0
  280. package/src/commands/db/migrate.ts +25 -0
  281. package/src/commands/index.ts +8 -0
  282. package/src/commands/publish/index.ts +63 -0
  283. package/src/config/defaults.ts +15 -0
  284. package/src/config/loader.ts +10 -1
  285. package/src/config/normalizers/declaration.ts +3 -0
  286. package/src/config/normalizers/page.ts +45 -0
  287. package/src/config/normalizers/pattern.ts +4 -1
  288. package/src/config/runtime.ts +48 -2
  289. package/src/index.ts +126 -3
  290. package/src/nav/index.ts +94 -0
  291. package/src/nav/registry.ts +54 -0
  292. package/src/nav/resolver.ts +217 -0
  293. package/src/nav/workspace.ts +87 -0
  294. package/src/pipeline/styles.ts +4 -35
  295. package/src/pipeline/token-transformers.ts +97 -0
  296. package/src/registry/asset-storage/adapters/vercel-blob.ts +51 -0
  297. package/src/registry/asset-storage/define.ts +15 -0
  298. package/src/registry/asset-storage/resolve.ts +72 -0
  299. package/src/registry/asset-storage/types.ts +46 -0
  300. package/src/registry/assets/layout.ts +38 -0
  301. package/src/registry/assets/sets.ts +48 -0
  302. package/src/registry/assets/transfer.ts +50 -0
  303. package/src/registry/checkout/assets.ts +126 -0
  304. package/src/registry/checkout/index.ts +596 -0
  305. package/src/registry/checkout/tokens.ts +152 -0
  306. package/src/registry/client.ts +234 -0
  307. package/src/registry/connection.ts +50 -0
  308. package/src/registry/db/adapter.ts +74 -0
  309. package/src/registry/db/client.ts +88 -0
  310. package/src/registry/db/index.ts +35 -0
  311. package/src/registry/db/migrate.ts +56 -0
  312. package/src/registry/db/schema.ts +444 -0
  313. package/src/registry/path.ts +26 -0
  314. package/src/registry/publish/artifacts.ts +147 -0
  315. package/src/registry/publish/assets.ts +129 -0
  316. package/src/registry/publish/errors.ts +34 -0
  317. package/src/registry/publish/index.ts +128 -0
  318. package/src/registry/publish/package.ts +272 -0
  319. package/src/registry/publish/publish-build.ts +51 -0
  320. package/src/registry/publish/tokens.ts +119 -0
  321. package/src/registry/tokens/sets.ts +132 -0
  322. package/src/registry/tokens/transfer.ts +53 -0
  323. package/src/registry/transfer.ts +82 -0
  324. package/src/store/filesystem-assets.ts +152 -0
  325. package/src/store/filesystem.ts +237 -0
  326. package/src/store/index.ts +62 -0
  327. package/src/store/registry.ts +334 -0
  328. package/src/store/source-files.ts +155 -0
  329. package/src/store/types.ts +210 -0
  330. package/src/transformers/plugins/csf-render.ts +31 -7
  331. package/src/transformers/plugins/handlebars-previews.ts +22 -3
  332. package/src/transformers/plugins/ssr-render.ts +145 -57
  333. package/src/transformers/preview/component/api.ts +2 -1
  334. package/src/transformers/preview/component/artifacts.ts +10 -1
  335. package/src/transformers/preview/component/builder.ts +9 -2
  336. package/src/transformers/preview/component/css.ts +10 -1
  337. package/src/transformers/preview/component/javascript.ts +55 -20
  338. package/src/transformers/preview/component/shared-artifacts.ts +146 -0
  339. package/src/transformers/preview/component.ts +5 -2
  340. package/src/transformers/preview/pattern/builder.ts +33 -7
  341. package/src/transformers/preview/pattern/html.ts +71 -27
  342. package/src/transformers/preview/types.ts +42 -0
  343. package/src/transformers/utils/handlebars.ts +31 -5
  344. package/src/types/config.ts +123 -25
  345. package/src/utils/menu-shell.ts +241 -0
  346. package/src/utils/stable-stringify.ts +7 -0
  347. package/templates/default/package.json.tpl +1 -1
  348. package/handoff.config.js +0 -90
  349. package/src/app/components/ComponentSearch.tsx +0 -93
@@ -0,0 +1,197 @@
1
+ import type { NextApiRequest, NextApiResponse } from 'next';
2
+ import { isSafePathSegment, isSafeRelativePath, normalizeRelativePath } from '@handoff/registry/path';
3
+ import { joinedQueryValue, singleQueryValue } from '../api/query';
4
+ import type { ManagedEntityKind } from './allowlist';
5
+ import { validateMetadataWrite } from './allowlist';
6
+ import { sendRegistryError } from './errors';
7
+ import { validateFileBody } from './files';
8
+ import { handleRegistryRoute, sendRegistryData } from './handler';
9
+ import { buildMeta } from './meta';
10
+ import { revalidateEntityPages } from './revalidate';
11
+ import {
12
+ createEntity,
13
+ deleteEntity,
14
+ deleteEntityFile,
15
+ entityExists,
16
+ getEntity,
17
+ getEntityFile,
18
+ listEntities,
19
+ listEntityFiles,
20
+ updateEntityMetadata,
21
+ upsertEntityFile,
22
+ } from './store';
23
+
24
+ /**
25
+ * Entity-kind-parameterized route bodies for the registry management API.
26
+ *
27
+ * Components and patterns expose the identical CRUD + files surface, so the four `/api/registry/*`
28
+ * route shapes are implemented once here and bound to a kind by the thin page route files. Each runs
29
+ * behind {@link handleRegistryRoute}, which applies the runtime-mode, method, bearer-token, and
30
+ * database guards before the body executes.
31
+ */
32
+
33
+ const label = (kind: ManagedEntityKind): string => (kind === 'component' ? 'Component' : 'Pattern');
34
+
35
+ const rejectionDetails = (rejectedFields: string[]) => (rejectedFields.length ? { rejectedFields } : undefined);
36
+
37
+ /** `GET` (list) + `POST` (create metadata-only record) for a collection. */
38
+ export const handleEntityCollection = (req: NextApiRequest, res: NextApiResponse, kind: ManagedEntityKind): Promise<void> =>
39
+ handleRegistryRoute(req, res, ['GET', 'POST'], async ({ db, method }) => {
40
+ if (method === 'GET') {
41
+ sendRegistryData(res, 200, await listEntities(db, kind));
42
+ return;
43
+ }
44
+ const validation = validateMetadataWrite(req.body, kind, { allowId: true });
45
+ if (!validation.ok) {
46
+ sendRegistryError(res, 'bad_request', validation.message, rejectionDetails(validation.rejectedFields));
47
+ return;
48
+ }
49
+ const result = await createEntity(db, kind, validation.value);
50
+ if (!result) {
51
+ sendRegistryError(res, 'bad_request', `A ${kind} with id "${validation.value.id}" already exists.`);
52
+ return;
53
+ }
54
+ // A new entity appears in the system index; regenerate it on demand.
55
+ await revalidateEntityPages(res, kind, validation.value.id);
56
+ sendRegistryData(res, 201, result.data, buildMeta(result.build));
57
+ });
58
+
59
+ /** `GET` (detail) + `PUT` (allowlisted metadata update) + `DELETE` for a single entity. */
60
+ export const handleEntityItem = (req: NextApiRequest, res: NextApiResponse, kind: ManagedEntityKind): Promise<void> =>
61
+ handleRegistryRoute(req, res, ['GET', 'PUT', 'DELETE'], async ({ db, method }) => {
62
+ const id = singleQueryValue(req.query.id);
63
+ if (!id) {
64
+ sendRegistryError(res, 'not_found', `Missing ${kind} id.`);
65
+ return;
66
+ }
67
+ if (!isSafePathSegment(id)) {
68
+ sendRegistryError(res, 'bad_request', `${label(kind)} id must be a registry-safe relative path.`, {
69
+ rejectedFields: ['id'],
70
+ });
71
+ return;
72
+ }
73
+
74
+ if (method === 'GET') {
75
+ const result = await getEntity(db, kind, id);
76
+ if (!result) {
77
+ sendRegistryError(res, 'not_found', `${label(kind)} "${id}" was not found.`);
78
+ return;
79
+ }
80
+ sendRegistryData(res, 200, result.data, buildMeta(result.build));
81
+ return;
82
+ }
83
+
84
+ if (method === 'PUT') {
85
+ const validation = validateMetadataWrite(req.body, kind);
86
+ if (!validation.ok) {
87
+ sendRegistryError(res, 'bad_request', validation.message, rejectionDetails(validation.rejectedFields));
88
+ return;
89
+ }
90
+ const result = await updateEntityMetadata(db, kind, id, validation.value);
91
+ if (!result) {
92
+ sendRegistryError(res, 'not_found', `${label(kind)} "${id}" was not found.`);
93
+ return;
94
+ }
95
+ // A metadata edit (e.g. title/description) changes the server-rendered detail page; regenerate.
96
+ await revalidateEntityPages(res, kind, id);
97
+ sendRegistryData(res, 200, result.data, buildMeta(result.build));
98
+ return;
99
+ }
100
+
101
+ const deleted = await deleteEntity(db, kind, id);
102
+ if (!deleted) {
103
+ sendRegistryError(res, 'not_found', `${label(kind)} "${id}" was not found.`);
104
+ return;
105
+ }
106
+ // The entity is gone; regenerate so the detail page 404s and the index drops it.
107
+ await revalidateEntityPages(res, kind, id);
108
+ sendRegistryData(res, 200, { id, deleted: true });
109
+ });
110
+
111
+ /** `GET` (list files) + `POST` (create/replace a file record) for an entity's files. */
112
+ export const handleEntityFilesCollection = (req: NextApiRequest, res: NextApiResponse, kind: ManagedEntityKind): Promise<void> =>
113
+ handleRegistryRoute(req, res, ['GET', 'POST'], async ({ db, method }) => {
114
+ const id = singleQueryValue(req.query.id);
115
+ if (!id) {
116
+ sendRegistryError(res, 'not_found', `Missing ${kind} id.`);
117
+ return;
118
+ }
119
+ if (!isSafePathSegment(id)) {
120
+ sendRegistryError(res, 'bad_request', `${label(kind)} id must be a registry-safe relative path.`, {
121
+ rejectedFields: ['id'],
122
+ });
123
+ return;
124
+ }
125
+ if (!(await entityExists(db, kind, id))) {
126
+ sendRegistryError(res, 'not_found', `${label(kind)} "${id}" was not found.`);
127
+ return;
128
+ }
129
+
130
+ if (method === 'GET') {
131
+ sendRegistryData(res, 200, await listEntityFiles(db, kind, id));
132
+ return;
133
+ }
134
+
135
+ const validation = validateFileBody(req.body);
136
+ if (!validation.ok) {
137
+ sendRegistryError(res, 'bad_request', validation.message, rejectionDetails(validation.rejectedFields));
138
+ return;
139
+ }
140
+ sendRegistryData(res, 201, await upsertEntityFile(db, kind, id, validation.value));
141
+ });
142
+
143
+ /** `GET` + `PUT` + `DELETE` for a single text-file record addressed by its relative path. */
144
+ export const handleEntityFileItem = (req: NextApiRequest, res: NextApiResponse, kind: ManagedEntityKind): Promise<void> =>
145
+ handleRegistryRoute(req, res, ['GET', 'PUT', 'DELETE'], async ({ db, method }) => {
146
+ const id = singleQueryValue(req.query.id);
147
+ const rawPath = joinedQueryValue(req.query.filePath);
148
+ if (!id || !rawPath) {
149
+ sendRegistryError(res, 'not_found', `Missing ${kind} id or file path.`);
150
+ return;
151
+ }
152
+ if (!isSafePathSegment(id)) {
153
+ sendRegistryError(res, 'bad_request', `${label(kind)} id must be a registry-safe relative path.`, {
154
+ rejectedFields: ['id'],
155
+ });
156
+ return;
157
+ }
158
+ if (!isSafeRelativePath(rawPath)) {
159
+ sendRegistryError(res, 'bad_request', 'File path must be a registry-safe relative path.', {
160
+ rejectedFields: ['path'],
161
+ });
162
+ return;
163
+ }
164
+ const filePath = normalizeRelativePath(rawPath);
165
+
166
+ if (!(await entityExists(db, kind, id))) {
167
+ sendRegistryError(res, 'not_found', `${label(kind)} "${id}" was not found.`);
168
+ return;
169
+ }
170
+
171
+ if (method === 'GET') {
172
+ const file = await getEntityFile(db, kind, id, filePath);
173
+ if (!file) {
174
+ sendRegistryError(res, 'not_found', `File "${filePath}" was not found for ${kind} "${id}".`);
175
+ return;
176
+ }
177
+ sendRegistryData(res, 200, file);
178
+ return;
179
+ }
180
+
181
+ if (method === 'PUT') {
182
+ const validation = validateFileBody(req.body, { pathFromRoute: filePath });
183
+ if (!validation.ok) {
184
+ sendRegistryError(res, 'bad_request', validation.message, rejectionDetails(validation.rejectedFields));
185
+ return;
186
+ }
187
+ sendRegistryData(res, 200, await upsertEntityFile(db, kind, id, validation.value));
188
+ return;
189
+ }
190
+
191
+ const deleted = await deleteEntityFile(db, kind, id, filePath);
192
+ if (!deleted) {
193
+ sendRegistryError(res, 'not_found', `File "${filePath}" was not found for ${kind} "${id}".`);
194
+ return;
195
+ }
196
+ sendRegistryData(res, 200, { path: filePath, deleted: true });
197
+ });
@@ -0,0 +1,51 @@
1
+ import type { NextApiResponse } from 'next';
2
+ import { redactSecrets } from '../api/redact';
3
+ import { buildMeta, type RegistryMeta } from './meta';
4
+
5
+ /**
6
+ * Registry management API error contract.
7
+ *
8
+ * Every response — including errors — uses the `{ error, meta }` envelope (the docs read API, by
9
+ * contrast, uses a bare `{ error }`). `details.rejectedFields` carries field-level rejection detail
10
+ * for allowlist/file-kind failures.
11
+ */
12
+ export type RegistryErrorCode =
13
+ | 'bad_request'
14
+ | 'unauthorized'
15
+ | 'not_found'
16
+ | 'method_not_allowed'
17
+ | 'runtime_mode_conflict'
18
+ | 'token_not_configured'
19
+ | 'database_unavailable'
20
+ | 'unexpected_error';
21
+
22
+ const STATUS_BY_CODE: Record<RegistryErrorCode, number> = {
23
+ bad_request: 400,
24
+ unauthorized: 401,
25
+ not_found: 404,
26
+ method_not_allowed: 405,
27
+ runtime_mode_conflict: 409,
28
+ token_not_configured: 503,
29
+ database_unavailable: 503,
30
+ unexpected_error: 500,
31
+ };
32
+
33
+ /** Optional structured error details. `rejectedFields` names the fields that failed validation. */
34
+ export interface RegistryErrorDetails {
35
+ rejectedFields?: string[];
36
+ [key: string]: unknown;
37
+ }
38
+
39
+ /** Write a registry API error response with the status mapped from its code, plus the `meta` envelope. */
40
+ export const sendRegistryError = (
41
+ res: NextApiResponse,
42
+ code: RegistryErrorCode,
43
+ message: string,
44
+ details?: RegistryErrorDetails,
45
+ meta: RegistryMeta = buildMeta()
46
+ ): void => {
47
+ res.status(STATUS_BY_CODE[code]).json({
48
+ error: { code, message, ...(details ? { details: redactSecrets(details) } : {}) },
49
+ meta,
50
+ });
51
+ };
@@ -0,0 +1,135 @@
1
+ import type { RegistryTextFileKind } from '@handoff/store/types';
2
+ import { isSafeRelativePath, normalizeRelativePath } from '@handoff/registry/path';
3
+ import { isPlainObject } from './validation';
4
+
5
+ /**
6
+ * Registry text-file record validation.
7
+ *
8
+ * Text file records are checkout/inspection/management only and **exclude declaration files** —
9
+ * declarations are a workspace-only concern (synthesized on checkout). File bodies must carry a
10
+ * supported kind and a registry-safe relative path; `declaration`, unknown kinds, and unsafe paths
11
+ * are rejected with `400 bad_request`.
12
+ */
13
+
14
+ /** Supported registry file kinds (the store kinds minus the workspace-only `declaration`). */
15
+ const ALLOWED_FILE_KINDS: readonly RegistryTextFileKind[] = [
16
+ 'component',
17
+ 'template',
18
+ 'style',
19
+ 'script',
20
+ 'story',
21
+ 'docs',
22
+ 'markdown',
23
+ 'schema',
24
+ 'other',
25
+ ];
26
+
27
+ const CONTENT_TYPE_BY_EXT: Record<string, string> = {
28
+ ts: 'text/typescript; charset=utf-8',
29
+ tsx: 'text/typescript; charset=utf-8',
30
+ js: 'application/javascript; charset=utf-8',
31
+ jsx: 'application/javascript; charset=utf-8',
32
+ cjs: 'application/javascript; charset=utf-8',
33
+ mjs: 'application/javascript; charset=utf-8',
34
+ json: 'application/json; charset=utf-8',
35
+ css: 'text/css; charset=utf-8',
36
+ scss: 'text/x-scss; charset=utf-8',
37
+ sass: 'text/x-sass; charset=utf-8',
38
+ hbs: 'text/x-handlebars-template; charset=utf-8',
39
+ md: 'text/markdown; charset=utf-8',
40
+ html: 'text/html; charset=utf-8',
41
+ };
42
+
43
+ const contentTypeForPath = (filePath: string): string => {
44
+ const lastDot = filePath.lastIndexOf('.');
45
+ const ext = lastDot >= 0 ? filePath.slice(lastDot + 1).toLowerCase() : '';
46
+ return CONTENT_TYPE_BY_EXT[ext] ?? 'text/plain; charset=utf-8';
47
+ };
48
+
49
+ /** A validated, persistable registry file record. */
50
+ export interface ValidatedFile {
51
+ path: string;
52
+ kind: RegistryTextFileKind;
53
+ content: string;
54
+ contentType: string;
55
+ }
56
+
57
+ /**
58
+ * Result of {@link validateFileBody}. Flat (not a discriminated union) because the app compiles with
59
+ * `strictNullChecks` off — `ok` decides, with `value` set on success and `rejectedFields`/`message`
60
+ * on failure.
61
+ */
62
+ export interface FileValidation {
63
+ ok: boolean;
64
+ value?: ValidatedFile;
65
+ rejectedFields?: string[];
66
+ message?: string;
67
+ }
68
+
69
+ export interface ValidateFileOptions {
70
+ /**
71
+ * Path supplied by the route (e.g. the `[...filePath]` segment for PUT). When set, the body path,
72
+ * if present, must match it; the route path is authoritative.
73
+ */
74
+ pathFromRoute?: string;
75
+ }
76
+
77
+ /**
78
+ * Validate a registry file record body (`{ path, kind, content, contentType }`). Rejects the
79
+ * `declaration` kind, unknown kinds, unsafe paths, and missing content with field-level detail.
80
+ */
81
+ export const validateFileBody = (body: unknown, options: ValidateFileOptions = {}): FileValidation => {
82
+ if (!isPlainObject(body)) {
83
+ return { ok: false, rejectedFields: [], message: 'Request body must be a JSON object.' };
84
+ }
85
+
86
+ const rejectedFields: string[] = [];
87
+
88
+ const rawPath = options.pathFromRoute ?? (typeof body.path === 'string' ? body.path : undefined);
89
+ if (!rawPath || !isSafeRelativePath(rawPath)) {
90
+ rejectedFields.push('path');
91
+ } else if (
92
+ options.pathFromRoute &&
93
+ typeof body.path === 'string' &&
94
+ normalizeRelativePath(body.path) !== normalizeRelativePath(options.pathFromRoute)
95
+ ) {
96
+ // A body path that disagrees with the route path is ambiguous; reject rather than guess.
97
+ rejectedFields.push('path');
98
+ }
99
+
100
+ const kind = body.kind;
101
+ if (typeof kind !== 'string' || !ALLOWED_FILE_KINDS.includes(kind as RegistryTextFileKind)) {
102
+ // `declaration` (workspace-only) and any unknown kind land here.
103
+ rejectedFields.push('kind');
104
+ }
105
+
106
+ if (typeof body.content !== 'string') {
107
+ rejectedFields.push('content');
108
+ }
109
+
110
+ if (body.contentType !== undefined && typeof body.contentType !== 'string') {
111
+ rejectedFields.push('contentType');
112
+ }
113
+
114
+ if (rejectedFields.length > 0) {
115
+ const declarationAttempted = kind === 'declaration';
116
+ return {
117
+ ok: false,
118
+ rejectedFields,
119
+ message: declarationAttempted
120
+ ? 'Declaration files are workspace-only and are never stored as registry source records.'
121
+ : 'Invalid file record: a supported kind, a registry-safe relative path, and string content are required.',
122
+ };
123
+ }
124
+
125
+ const path = normalizeRelativePath(rawPath as string);
126
+ return {
127
+ ok: true,
128
+ value: {
129
+ path,
130
+ kind: kind as RegistryTextFileKind,
131
+ content: body.content as string,
132
+ contentType: (body.contentType as string) || contentTypeForPath(path),
133
+ },
134
+ };
135
+ };
@@ -0,0 +1,96 @@
1
+ import type { NextApiRequest, NextApiResponse } from 'next';
2
+ import type { RegistryDatabase } from '@handoff/registry/db/client';
3
+ import { getServerRuntimeConfig } from '../docs-api/runtime-config';
4
+ import { getRegistryConnection, RegistryConnectionError } from '../registry-connection';
5
+ import { authorizeMutation } from './auth';
6
+ import { sendRegistryError } from './errors';
7
+ import { buildMeta, type RegistryMeta } from './meta';
8
+ import { redactSecrets } from '../api/redact';
9
+
10
+ /**
11
+ * Shared orchestration for every `/api/registry/*` route.
12
+ *
13
+ * Enforces, in order: the registry-runtime guard (management APIs exist only when
14
+ * `runtime.mode: registry`, else `409 runtime_mode_conflict`); the per-route method allowlist
15
+ * (`405 method_not_allowed`); the bearer-token guard for mutations (`503 token_not_configured` /
16
+ * `401 unauthorized`); and database resolution (`503 database_unavailable`). The route body then
17
+ * runs against a live connection, with any thrown failure mapped to `unexpected_error`.
18
+ */
19
+
20
+ /** Methods that mutate state and therefore require the bearer token. */
21
+ const MUTATION_METHODS = new Set(['POST', 'PUT', 'PATCH', 'DELETE']);
22
+
23
+ /** Context handed to a registry route body once all guards pass. */
24
+ export interface RegistryRouteContext {
25
+ req: NextApiRequest;
26
+ res: NextApiResponse;
27
+ db: RegistryDatabase;
28
+ method: string;
29
+ }
30
+
31
+ /**
32
+ * Guard that the active runtime is `registry`. Writes `409 runtime_mode_conflict` and returns
33
+ * `false` in workspace mode. Used by routes (like health) that do not need a database connection.
34
+ */
35
+ export const ensureRegistryMode = (res: NextApiResponse): boolean => {
36
+ if (getServerRuntimeConfig().mode === 'registry') {
37
+ return true;
38
+ }
39
+ sendRegistryError(res, 'runtime_mode_conflict', 'The registry management API is available only when runtime.mode is "registry".');
40
+ return false;
41
+ };
42
+
43
+ /** Write a successful registry API response with the `{ data, meta }` envelope (data redacted). */
44
+ export const sendRegistryData = (res: NextApiResponse, status: number, data: unknown, meta: RegistryMeta = buildMeta()): void => {
45
+ res.status(status).json({ data: redactSecrets(data), meta });
46
+ };
47
+
48
+ /**
49
+ * Run a registry route body behind the full guard stack. `methods` is the route's allowlist (e.g.
50
+ * `['GET', 'POST']`); the body branches on `ctx.method`.
51
+ */
52
+ export const handleRegistryRoute = async (
53
+ req: NextApiRequest,
54
+ res: NextApiResponse,
55
+ methods: string[],
56
+ body: (ctx: RegistryRouteContext) => Promise<void>
57
+ ): Promise<void> => {
58
+ if (!ensureRegistryMode(res)) {
59
+ return;
60
+ }
61
+
62
+ const method = (req.method ?? 'GET').toUpperCase();
63
+ if (!methods.includes(method)) {
64
+ res.setHeader('Allow', methods.join(', '));
65
+ sendRegistryError(res, 'method_not_allowed', `Method ${method} not allowed for this registry endpoint.`);
66
+ return;
67
+ }
68
+
69
+ if (MUTATION_METHODS.has(method)) {
70
+ const auth = authorizeMutation(req);
71
+ if (!auth.ok) {
72
+ sendRegistryError(res, auth.code, auth.message);
73
+ return;
74
+ }
75
+ }
76
+
77
+ let db: RegistryDatabase;
78
+ try {
79
+ ({ db } = await getRegistryConnection());
80
+ } catch (error) {
81
+ if (error instanceof RegistryConnectionError) {
82
+ sendRegistryError(res, 'database_unavailable', error.message);
83
+ return;
84
+ }
85
+ console.error('Registry connection resolution failed.', error);
86
+ sendRegistryError(res, 'unexpected_error', 'Unexpected registry API error.');
87
+ return;
88
+ }
89
+
90
+ try {
91
+ await body({ req, res, db, method });
92
+ } catch (error) {
93
+ console.error('Registry API request failed.', error);
94
+ sendRegistryError(res, 'unexpected_error', 'Unexpected registry API error.');
95
+ }
96
+ };
@@ -0,0 +1,4 @@
1
+ /** Route facing registry API helpers. Internal validation and persistence stay in their own modules. */
2
+
3
+ export { ensureRegistryMode, handleRegistryRoute } from './handler';
4
+ export { sendRegistryError } from './errors';
@@ -0,0 +1,87 @@
1
+ import { and, eq, sql } from 'drizzle-orm';
2
+ import type { RuntimeMode } from '@handoff/types/config';
3
+ import type { RegistryDatabase } from '@handoff/registry/db/client';
4
+ import { buildMetadata, docsArtifacts } from '@handoff/registry/db/schema';
5
+ import { getServerRuntimeConfig } from '../docs-api/runtime-config';
6
+
7
+ /**
8
+ * Response-envelope `meta` for the registry management API.
9
+ *
10
+ * Every registry API response — success or error — carries `{ data, meta }`, where `meta` describes
11
+ * the serving runtime and (for single-entity reads) the entity's build/artifact state. The build
12
+ * block lets a reviewer see whether an entity has published artifacts without fetching them.
13
+ */
14
+
15
+ /** Build/artifact state summary for a single entity. */
16
+ export interface RegistryBuildMeta {
17
+ /** `not_built` when no build-metadata row exists yet; otherwise the stored status. */
18
+ status: 'current' | 'stale' | 'missing' | 'error' | 'not_built';
19
+ entityKind: 'component' | 'pattern' | 'page';
20
+ entityId: string;
21
+ builtAt: string | null;
22
+ artifactHash: string | null;
23
+ /** Number of entity-owned docs read-model artifacts stored. */
24
+ ingestedArtifacts: number;
25
+ /** Number of shared/global artifacts stored (path-keyed, owned by `asset`). */
26
+ sharedArtifacts: number;
27
+ }
28
+
29
+ /** Standard registry API response meta. */
30
+ export interface RegistryMeta {
31
+ runtime: RuntimeMode;
32
+ source: 'database';
33
+ generatedAt: string;
34
+ build?: RegistryBuildMeta;
35
+ }
36
+
37
+ /** Build the base response meta, optionally carrying a single entity's build state. */
38
+ export const buildMeta = (build?: RegistryBuildMeta): RegistryMeta => ({
39
+ runtime: getServerRuntimeConfig().mode,
40
+ source: 'database',
41
+ generatedAt: new Date().toISOString(),
42
+ ...(build ? { build } : {}),
43
+ });
44
+
45
+ /** Count rows matching a where-clause (used for artifact tallies). */
46
+ const countWhere = async (db: RegistryDatabase, where: ReturnType<typeof eq>): Promise<number> => {
47
+ const rows = await db
48
+ .select({ value: sql<number>`count(*)::int` })
49
+ .from(docsArtifacts)
50
+ .where(where);
51
+ return rows[0]?.value ?? 0;
52
+ };
53
+
54
+ /**
55
+ * Resolve the build/artifact state for an entity from `build_metadata` + `docs_artifacts`. Reads
56
+ * only — never triggers a build. A metadata-only entity (no build row) reports `not_built`.
57
+ */
58
+ export const resolveBuildMeta = async (
59
+ db: RegistryDatabase,
60
+ entityKind: 'component' | 'pattern' | 'page',
61
+ entityId: string
62
+ ): Promise<RegistryBuildMeta> => {
63
+ const [rows, ingestedArtifacts, sharedArtifacts] = await Promise.all([
64
+ db
65
+ .select({
66
+ status: buildMetadata.status,
67
+ builtAt: buildMetadata.builtAt,
68
+ artifactHash: buildMetadata.artifactHash,
69
+ })
70
+ .from(buildMetadata)
71
+ .where(and(eq(buildMetadata.entityKind, entityKind), eq(buildMetadata.entityId, entityId)))
72
+ .limit(1),
73
+ countWhere(db, and(eq(docsArtifacts.entityKind, entityKind), eq(docsArtifacts.entityId, entityId))),
74
+ countWhere(db, eq(docsArtifacts.entityKind, 'asset')),
75
+ ]);
76
+ const row = rows[0];
77
+
78
+ return {
79
+ status: row?.status ?? 'not_built',
80
+ entityKind,
81
+ entityId,
82
+ builtAt: row?.builtAt ? new Date(row.builtAt).toISOString() : null,
83
+ artifactHash: row?.artifactHash ?? null,
84
+ ingestedArtifacts,
85
+ sharedArtifacts,
86
+ };
87
+ };
@@ -0,0 +1,36 @@
1
+ import type { NextApiResponse } from 'next';
2
+
3
+ /** Entity kinds whose docs pages are statically generated and can be regenerated on demand. */
4
+ type RevalidatableEntityKind = 'component' | 'pattern' | 'page';
5
+
6
+ /**
7
+ * Best-effort on-demand revalidation of the statically-generated docs pages affected by a registry
8
+ * mutation.
9
+ *
10
+ * Every registry mutation — publish ingest, allowlisted metadata edit, create, delete — flows
11
+ * through this running server, so the pages those mutations affect are regenerated the instant
12
+ * content changes (Next on-demand ISR) instead of waiting for a rebuild or restart: the entity's
13
+ * detail page (correct `<head>` title, metadata, prev/next) and the system index. The component /
14
+ * pattern nav submenus are resolved at request time on the client, so they are intentionally not
15
+ * revalidated here.
16
+ *
17
+ * Revalidation is best-effort: the mutation has already persisted, so a revalidation failure (e.g. a
18
+ * page that has not been generated yet, or a runtime without ISR) must never fail the request — the
19
+ * page refreshes on its next natural regeneration.
20
+ */
21
+ export const revalidateEntityPages = async (
22
+ res: NextApiResponse,
23
+ kind: RevalidatableEntityKind,
24
+ id: string
25
+ ): Promise<void> => {
26
+ // Pages are served at their own route (`/<id>`); components/patterns live under `/system`.
27
+ const paths = kind === 'page' ? [`/${id}/`] : [`/system/${kind}/${id}/`, '/system/'];
28
+ for (const path of paths) {
29
+ try {
30
+ await res.revalidate(path);
31
+ } catch (error) {
32
+ // Best-effort: see note above. The entity is persisted regardless of revalidation outcome.
33
+ console.warn(`Failed to revalidate registry docs path "${path}" after ${kind} "${id}" changed.`, error);
34
+ }
35
+ }
36
+ };