handoff-app 1.2.2-7 → 2.0.0-1

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 (351) 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/paths.js +25 -1
  14. package/dist/app-builder/static-export.d.ts +17 -0
  15. package/dist/app-builder/static-export.js +206 -0
  16. package/dist/app-builder/vercel-output.d.ts +39 -0
  17. package/dist/app-builder/vercel-output.js +259 -0
  18. package/dist/app-builder/watchers/utils.d.ts +2 -0
  19. package/dist/app-builder/watchers.d.ts +14 -4
  20. package/dist/app-builder/watchers.js +123 -28
  21. package/dist/app-builder/websocket.js +1 -1
  22. package/dist/artifacts/index.d.ts +2 -0
  23. package/dist/artifacts/index.js +16 -0
  24. package/dist/artifacts/types.d.ts +136 -0
  25. package/dist/artifacts/types.js +24 -0
  26. package/dist/artifacts/url.d.ts +67 -0
  27. package/dist/artifacts/url.js +116 -0
  28. package/dist/cache/build-cache.js +48 -4
  29. package/dist/cli.js +0 -0
  30. package/dist/commands/build/app.js +1 -2
  31. package/dist/commands/build/index.d.ts +17 -0
  32. package/dist/commands/build/index.js +66 -0
  33. package/dist/commands/checkout/index.d.ts +24 -0
  34. package/dist/commands/checkout/index.js +71 -0
  35. package/dist/commands/db/migrate.d.ts +11 -0
  36. package/dist/commands/db/migrate.js +33 -0
  37. package/dist/commands/index.js +8 -0
  38. package/dist/commands/publish/index.d.ts +21 -0
  39. package/dist/commands/publish/index.js +68 -0
  40. package/dist/config/defaults.js +15 -1
  41. package/dist/config/loader.js +6 -1
  42. package/dist/config/normalizers/declaration.js +4 -1
  43. package/dist/config/normalizers/page.d.ts +18 -0
  44. package/dist/config/normalizers/page.js +34 -0
  45. package/dist/config/normalizers/pattern.js +7 -1
  46. package/dist/config/runtime.d.ts +4 -0
  47. package/dist/config/runtime.js +53 -5
  48. package/dist/index.d.ts +65 -1
  49. package/dist/index.js +123 -3
  50. package/dist/nav/index.d.ts +83 -0
  51. package/dist/nav/index.js +9 -0
  52. package/dist/nav/registry.d.ts +15 -0
  53. package/dist/nav/registry.js +42 -0
  54. package/dist/nav/resolver.d.ts +8 -0
  55. package/dist/nav/resolver.js +192 -0
  56. package/dist/nav/workspace.d.ts +18 -0
  57. package/dist/nav/workspace.js +103 -0
  58. package/dist/pipeline/styles.js +7 -35
  59. package/dist/pipeline/token-transformers.d.ts +45 -0
  60. package/dist/pipeline/token-transformers.js +71 -0
  61. package/dist/registry/asset-storage/adapters/vercel-blob.d.ts +17 -0
  62. package/dist/registry/asset-storage/adapters/vercel-blob.js +87 -0
  63. package/dist/registry/asset-storage/define.d.ts +10 -0
  64. package/dist/registry/asset-storage/define.js +13 -0
  65. package/dist/registry/asset-storage/resolve.d.ts +45 -0
  66. package/dist/registry/asset-storage/resolve.js +36 -0
  67. package/dist/registry/asset-storage/types.d.ts +51 -0
  68. package/dist/registry/asset-storage/types.js +9 -0
  69. package/dist/registry/assets/layout.d.ts +22 -0
  70. package/dist/registry/assets/layout.js +30 -0
  71. package/dist/registry/assets/sets.d.ts +18 -0
  72. package/dist/registry/assets/sets.js +44 -0
  73. package/dist/registry/assets/transfer.d.ts +45 -0
  74. package/dist/registry/assets/transfer.js +11 -0
  75. package/dist/registry/checkout/assets.d.ts +16 -0
  76. package/dist/registry/checkout/assets.js +150 -0
  77. package/dist/registry/checkout/index.d.ts +33 -0
  78. package/dist/registry/checkout/index.js +610 -0
  79. package/dist/registry/checkout/tokens.d.ts +15 -0
  80. package/dist/registry/checkout/tokens.js +171 -0
  81. package/dist/registry/client.d.ts +61 -0
  82. package/dist/registry/client.js +211 -0
  83. package/dist/registry/connection.d.ts +30 -0
  84. package/dist/registry/connection.js +36 -0
  85. package/dist/registry/db/adapter.d.ts +43 -0
  86. package/dist/registry/db/adapter.js +59 -0
  87. package/dist/registry/db/client.d.ts +35 -0
  88. package/dist/registry/db/client.js +94 -0
  89. package/dist/registry/db/index.d.ts +5 -0
  90. package/dist/registry/db/index.js +24 -0
  91. package/dist/registry/db/migrate.d.ts +23 -0
  92. package/dist/registry/db/migrate.js +64 -0
  93. package/dist/registry/db/schema.d.ts +4869 -0
  94. package/dist/registry/db/schema.js +347 -0
  95. package/dist/registry/path.d.ts +4 -0
  96. package/dist/registry/path.js +31 -0
  97. package/dist/registry/publish/artifacts.d.ts +16 -0
  98. package/dist/registry/publish/artifacts.js +132 -0
  99. package/dist/registry/publish/assets.d.ts +16 -0
  100. package/dist/registry/publish/assets.js +129 -0
  101. package/dist/registry/publish/errors.d.ts +5 -0
  102. package/dist/registry/publish/errors.js +26 -0
  103. package/dist/registry/publish/index.d.ts +27 -0
  104. package/dist/registry/publish/index.js +126 -0
  105. package/dist/registry/publish/package.d.ts +27 -0
  106. package/dist/registry/publish/package.js +257 -0
  107. package/dist/registry/publish/publish-build.d.ts +12 -0
  108. package/dist/registry/publish/publish-build.js +43 -0
  109. package/dist/registry/publish/tokens.d.ts +18 -0
  110. package/dist/registry/publish/tokens.js +112 -0
  111. package/dist/registry/tokens/sets.d.ts +59 -0
  112. package/dist/registry/tokens/sets.js +109 -0
  113. package/dist/registry/tokens/transfer.d.ts +48 -0
  114. package/dist/registry/tokens/transfer.js +10 -0
  115. package/dist/registry/transfer.d.ts +75 -0
  116. package/dist/registry/transfer.js +11 -0
  117. package/dist/store/filesystem-assets.d.ts +17 -0
  118. package/dist/store/filesystem-assets.js +147 -0
  119. package/dist/store/filesystem.d.ts +76 -0
  120. package/dist/store/filesystem.js +208 -0
  121. package/dist/store/index.d.ts +20 -0
  122. package/dist/store/index.js +38 -0
  123. package/dist/store/registry.d.ts +77 -0
  124. package/dist/store/registry.js +350 -0
  125. package/dist/store/source-files.d.ts +24 -0
  126. package/dist/store/source-files.js +147 -0
  127. package/dist/store/types.d.ts +185 -0
  128. package/dist/store/types.js +12 -0
  129. package/dist/transformers/plugins/csf-render.js +17 -8
  130. package/dist/transformers/plugins/handlebars-previews.js +10 -3
  131. package/dist/transformers/plugins/ssr-render.js +116 -49
  132. package/dist/transformers/preview/component/api.js +3 -1
  133. package/dist/transformers/preview/component/artifacts.js +8 -1
  134. package/dist/transformers/preview/component/builder.js +13 -5
  135. package/dist/transformers/preview/component/css.js +9 -1
  136. package/dist/transformers/preview/component/javascript.d.ts +9 -3
  137. package/dist/transformers/preview/component/javascript.js +45 -17
  138. package/dist/transformers/preview/component/shared-artifacts.d.ts +83 -0
  139. package/dist/transformers/preview/component/shared-artifacts.js +92 -0
  140. package/dist/transformers/preview/component.js +5 -2
  141. package/dist/transformers/preview/pattern/builder.js +28 -9
  142. package/dist/transformers/preview/pattern/html.d.ts +13 -10
  143. package/dist/transformers/preview/pattern/html.js +62 -27
  144. package/dist/transformers/preview/types.d.ts +38 -0
  145. package/dist/transformers/utils/handlebars.d.ts +3 -0
  146. package/dist/transformers/utils/handlebars.js +19 -5
  147. package/dist/types/config.d.ts +124 -31
  148. package/dist/utils/menu-shell.d.ts +60 -0
  149. package/dist/utils/menu-shell.js +221 -0
  150. package/dist/utils/stable-stringify.d.ts +2 -0
  151. package/dist/utils/stable-stringify.js +8 -0
  152. package/docs/cli.md +4 -2
  153. package/drizzle/0000_init_registry_storage.sql +95 -0
  154. package/drizzle/0001_add_review_metadata.sql +2 -0
  155. package/drizzle/0002_add_pages.sql +28 -0
  156. package/drizzle/0003_add_tokens.sql +29 -0
  157. package/drizzle/0004_add_assets.sql +37 -0
  158. package/drizzle/meta/0000_snapshot.json +642 -0
  159. package/drizzle/meta/0001_snapshot.json +654 -0
  160. package/drizzle/meta/0002_snapshot.json +835 -0
  161. package/drizzle/meta/0003_snapshot.json +1023 -0
  162. package/drizzle/meta/0004_snapshot.json +1252 -0
  163. package/drizzle/meta/_journal.json +41 -0
  164. package/drizzle.config.ts +16 -0
  165. package/package.json +10 -1
  166. package/src/app/components/Component/Preview.tsx +3 -2
  167. package/src/app/components/Component/RegistryActions.tsx +150 -0
  168. package/src/app/components/Foundations/DisplayLogo.tsx +11 -3
  169. package/src/app/components/Layout/Header.tsx +21 -3
  170. package/src/app/components/Layout/Main.tsx +11 -3
  171. package/src/app/components/Navigation/MainNav.tsx +4 -2
  172. package/src/app/components/Navigation/MobileNav.tsx +4 -2
  173. package/src/app/components/Navigation/SideNav.tsx +46 -11
  174. package/src/app/components/NotFound.tsx +24 -0
  175. package/src/app/components/context/NavProvider.tsx +71 -0
  176. package/src/app/components/old/Header.tsx +3 -3
  177. package/src/app/components/util/index.ts +203 -380
  178. package/src/app/components/util/useCollectionAssets.ts +125 -0
  179. package/src/app/components/util/useFoundationTokens.ts +67 -0
  180. package/src/app/generated/default-pages.json +1 -0
  181. package/src/app/generated/nav-shell.json +1 -0
  182. package/src/app/lib/api/query.ts +6 -0
  183. package/src/app/lib/api/redact.ts +46 -0
  184. package/src/app/lib/asset-storage.ts +81 -0
  185. package/src/app/lib/docs-api/artifacts.ts +116 -0
  186. package/src/app/lib/docs-api/backend.ts +151 -0
  187. package/src/app/lib/docs-api/errors.ts +52 -0
  188. package/src/app/lib/docs-api/handler.ts +34 -0
  189. package/src/app/lib/docs-api/index.ts +7 -0
  190. package/src/app/lib/docs-api/records.ts +189 -0
  191. package/src/app/lib/docs-api/registry.ts +130 -0
  192. package/src/app/lib/docs-api/runtime-config.ts +173 -0
  193. package/src/app/lib/docs-api/serve.ts +37 -0
  194. package/src/app/lib/docs-api/token-detail.ts +19 -0
  195. package/src/app/lib/registry-api/allowlist.ts +163 -0
  196. package/src/app/lib/registry-api/asset-transfer.ts +363 -0
  197. package/src/app/lib/registry-api/auth.ts +68 -0
  198. package/src/app/lib/registry-api/entity-routes.ts +197 -0
  199. package/src/app/lib/registry-api/errors.ts +51 -0
  200. package/src/app/lib/registry-api/files.ts +135 -0
  201. package/src/app/lib/registry-api/handler.ts +96 -0
  202. package/src/app/lib/registry-api/index.ts +4 -0
  203. package/src/app/lib/registry-api/meta.ts +87 -0
  204. package/src/app/lib/registry-api/revalidate.ts +36 -0
  205. package/src/app/lib/registry-api/store.ts +348 -0
  206. package/src/app/lib/registry-api/token-transfer.ts +231 -0
  207. package/src/app/lib/registry-api/transfer.ts +513 -0
  208. package/src/app/lib/registry-api/validation.ts +74 -0
  209. package/src/app/lib/registry-connection.ts +58 -0
  210. package/src/app/lib/utils.ts +14 -0
  211. package/src/app/next.config.mjs +72 -13
  212. package/src/app/pages/[...slug]/index.tsx +74 -20
  213. package/src/app/pages/_app.tsx +5 -2
  214. package/src/app/pages/api/docs/artifacts/[...artifactPath].ts +16 -0
  215. package/src/app/pages/api/docs/assets/[collection]/[...path].ts +50 -0
  216. package/src/app/pages/api/docs/assets/[collection]/index.ts +22 -0
  217. package/src/app/pages/api/docs/components/[id]/assets/[asset].ts +33 -0
  218. package/src/app/pages/api/docs/components/[id]/inspect/[previewId].ts +20 -0
  219. package/src/app/pages/api/docs/components/[id]/previews/[previewId].ts +21 -0
  220. package/src/app/pages/api/docs/components/[id].ts +24 -0
  221. package/src/app/pages/api/docs/components.json.ts +12 -0
  222. package/src/app/pages/api/docs/nav.json.ts +48 -0
  223. package/src/app/pages/api/docs/patterns/[id]/preview.ts +19 -0
  224. package/src/app/pages/api/docs/patterns/[id].ts +24 -0
  225. package/src/app/pages/api/docs/patterns.json.ts +12 -0
  226. package/src/app/pages/api/docs/tokens/components/[id].ts +30 -0
  227. package/src/app/pages/api/docs/tokens/foundations/[type].ts +32 -0
  228. package/src/app/pages/api/registry/components/[id]/files/[...filePath].ts +19 -0
  229. package/src/app/pages/api/registry/components/[id]/files/index.ts +19 -0
  230. package/src/app/pages/api/registry/components/[id].ts +20 -0
  231. package/src/app/pages/api/registry/components/index.ts +19 -0
  232. package/src/app/pages/api/registry/health.ts +20 -0
  233. package/src/app/pages/api/registry/patterns/[id]/files/[...filePath].ts +19 -0
  234. package/src/app/pages/api/registry/patterns/[id]/files/index.ts +19 -0
  235. package/src/app/pages/api/registry/patterns/[id].ts +20 -0
  236. package/src/app/pages/api/registry/patterns/index.ts +19 -0
  237. package/src/app/pages/api/registry/transfer/[kind]/[id].ts +39 -0
  238. package/src/app/pages/api/registry/transfer/assets/[collection].ts +24 -0
  239. package/src/app/pages/api/registry/transfer/assets/blobs/[hash].ts +23 -0
  240. package/src/app/pages/api/registry/transfer/assets/blobs/have.ts +11 -0
  241. package/src/app/pages/api/registry/transfer/assets/index.ts +10 -0
  242. package/src/app/pages/api/registry/transfer/tokens/[...setId].ts +26 -0
  243. package/src/app/pages/api/registry/transfer/tokens/index.ts +11 -0
  244. package/src/app/pages/assets/fonts.tsx +41 -17
  245. package/src/app/pages/assets/index.tsx +2 -2
  246. package/src/app/pages/assets/logos.tsx +6 -3
  247. package/src/app/pages/foundations/colors.tsx +18 -8
  248. package/src/app/pages/foundations/effects.tsx +18 -8
  249. package/src/app/pages/foundations/icons/[name]/index.tsx +28 -16
  250. package/src/app/pages/foundations/icons/index.tsx +18 -12
  251. package/src/app/pages/foundations/index.tsx +1 -1
  252. package/src/app/pages/foundations/logo.tsx +12 -8
  253. package/src/app/pages/foundations/typography.tsx +19 -9
  254. package/src/app/pages/index.tsx +1 -1
  255. package/src/app/pages/system/component/[component]/index.tsx +87 -17
  256. package/src/app/pages/system/index.tsx +13 -4
  257. package/src/app/pages/system/pattern/[pattern]/index.tsx +70 -14
  258. package/src/app/pages/system/tokens/components/[component]/index.tsx +42 -14
  259. package/src/app/pages/system/tokens/foundations/colors.tsx +7 -5
  260. package/src/app/pages/system/tokens/foundations/effects.tsx +7 -5
  261. package/src/app/pages/system/tokens/foundations/typography.tsx +8 -4
  262. package/src/app/tsconfig.json +2 -3
  263. package/src/app-builder/build.ts +555 -11
  264. package/src/app-builder/client-config.ts +61 -4
  265. package/src/app-builder/config-diff/snapshot.ts +1 -11
  266. package/src/app-builder/index.ts +1 -1
  267. package/src/app-builder/nav-shell.ts +74 -0
  268. package/src/app-builder/paths.ts +27 -1
  269. package/src/app-builder/static-export.ts +242 -0
  270. package/src/app-builder/vercel-output.ts +297 -0
  271. package/src/app-builder/watchers/utils.ts +2 -0
  272. package/src/app-builder/watchers.ts +149 -25
  273. package/src/app-builder/websocket.ts +2 -2
  274. package/src/artifacts/README.md +14 -0
  275. package/src/artifacts/index.ts +24 -0
  276. package/src/artifacts/types.ts +146 -0
  277. package/src/artifacts/url.ts +122 -0
  278. package/src/cache/build-cache.ts +47 -2
  279. package/src/commands/build/app.ts +1 -2
  280. package/src/commands/build/index.ts +61 -0
  281. package/src/commands/checkout/index.ts +66 -0
  282. package/src/commands/db/migrate.ts +25 -0
  283. package/src/commands/index.ts +8 -0
  284. package/src/commands/publish/index.ts +63 -0
  285. package/src/config/defaults.ts +15 -0
  286. package/src/config/loader.ts +10 -1
  287. package/src/config/normalizers/declaration.ts +3 -0
  288. package/src/config/normalizers/page.ts +45 -0
  289. package/src/config/normalizers/pattern.ts +4 -1
  290. package/src/config/runtime.ts +48 -2
  291. package/src/index.ts +126 -3
  292. package/src/nav/index.ts +94 -0
  293. package/src/nav/registry.ts +54 -0
  294. package/src/nav/resolver.ts +217 -0
  295. package/src/nav/workspace.ts +87 -0
  296. package/src/pipeline/styles.ts +4 -35
  297. package/src/pipeline/token-transformers.ts +97 -0
  298. package/src/registry/asset-storage/adapters/vercel-blob.ts +51 -0
  299. package/src/registry/asset-storage/define.ts +15 -0
  300. package/src/registry/asset-storage/resolve.ts +72 -0
  301. package/src/registry/asset-storage/types.ts +46 -0
  302. package/src/registry/assets/layout.ts +38 -0
  303. package/src/registry/assets/sets.ts +48 -0
  304. package/src/registry/assets/transfer.ts +50 -0
  305. package/src/registry/checkout/assets.ts +126 -0
  306. package/src/registry/checkout/index.ts +596 -0
  307. package/src/registry/checkout/tokens.ts +152 -0
  308. package/src/registry/client.ts +234 -0
  309. package/src/registry/connection.ts +50 -0
  310. package/src/registry/db/adapter.ts +74 -0
  311. package/src/registry/db/client.ts +88 -0
  312. package/src/registry/db/index.ts +35 -0
  313. package/src/registry/db/migrate.ts +56 -0
  314. package/src/registry/db/schema.ts +444 -0
  315. package/src/registry/path.ts +26 -0
  316. package/src/registry/publish/artifacts.ts +147 -0
  317. package/src/registry/publish/assets.ts +129 -0
  318. package/src/registry/publish/errors.ts +34 -0
  319. package/src/registry/publish/index.ts +128 -0
  320. package/src/registry/publish/package.ts +272 -0
  321. package/src/registry/publish/publish-build.ts +51 -0
  322. package/src/registry/publish/tokens.ts +119 -0
  323. package/src/registry/tokens/sets.ts +132 -0
  324. package/src/registry/tokens/transfer.ts +53 -0
  325. package/src/registry/transfer.ts +82 -0
  326. package/src/store/filesystem-assets.ts +152 -0
  327. package/src/store/filesystem.ts +237 -0
  328. package/src/store/index.ts +62 -0
  329. package/src/store/registry.ts +334 -0
  330. package/src/store/source-files.ts +155 -0
  331. package/src/store/types.ts +210 -0
  332. package/src/transformers/plugins/csf-render.ts +31 -7
  333. package/src/transformers/plugins/handlebars-previews.ts +22 -3
  334. package/src/transformers/plugins/ssr-render.ts +145 -57
  335. package/src/transformers/preview/component/api.ts +2 -1
  336. package/src/transformers/preview/component/artifacts.ts +10 -1
  337. package/src/transformers/preview/component/builder.ts +9 -2
  338. package/src/transformers/preview/component/css.ts +10 -1
  339. package/src/transformers/preview/component/javascript.ts +55 -20
  340. package/src/transformers/preview/component/shared-artifacts.ts +146 -0
  341. package/src/transformers/preview/component.ts +5 -2
  342. package/src/transformers/preview/pattern/builder.ts +33 -7
  343. package/src/transformers/preview/pattern/html.ts +71 -27
  344. package/src/transformers/preview/types.ts +42 -0
  345. package/src/transformers/utils/handlebars.ts +31 -5
  346. package/src/types/config.ts +129 -31
  347. package/src/utils/menu-shell.ts +241 -0
  348. package/src/utils/stable-stringify.ts +7 -0
  349. package/templates/default/package.json.tpl +1 -1
  350. package/handoff.config.js +0 -90
  351. package/src/app/components/ComponentSearch.tsx +0 -93
@@ -0,0 +1,348 @@
1
+ import { and, eq } from 'drizzle-orm';
2
+ import type { RegistryDatabase } from '@handoff/registry/db/client';
3
+ import {
4
+ buildMetadata,
5
+ componentFiles,
6
+ components,
7
+ docsArtifacts,
8
+ pageFiles,
9
+ pages,
10
+ patternFiles,
11
+ patterns,
12
+ type RegistryReviewMetadata,
13
+ } from '@handoff/registry/db/schema';
14
+ import type { ComponentListObject, PageListObject, PatternListObject } from '@handoff/transformers/preview/types';
15
+ import { mergeReviewMetadata, type ManagedEntityKind, type ValidatedMetadataWrite } from './allowlist';
16
+ import type { ValidatedFile } from './files';
17
+ import { resolveBuildMeta, type RegistryBuildMeta } from './meta';
18
+
19
+ /**
20
+ * Database operations behind the registry management API.
21
+ *
22
+ * Reads return the normalized record merged with registry-only review metadata; writes are limited
23
+ * to the metadata allowlist and the text-file record groups. Crucially, metadata create/update
24
+ * never touches `docs_artifacts` or `build_metadata` — published artifacts and build state are only
25
+ * changed by transfer/publish. Entity deletion removes only that entity's owned
26
+ * artifacts; shared/global artifacts (owned by `asset`) are preserved.
27
+ */
28
+
29
+ /** A registry record as served by the management API: the normalized record plus review metadata. */
30
+ export type RegistryEntityData = Record<string, unknown> & { metadata: RegistryReviewMetadata | null };
31
+
32
+ /** A single entity read result: its served data plus build/artifact state. */
33
+ export interface EntityReadResult {
34
+ data: RegistryEntityData;
35
+ build: RegistryBuildMeta;
36
+ }
37
+
38
+ /** Binds an entity kind to its tables and the conventions used to build/store its records. */
39
+ interface EntitySpec {
40
+ table: typeof components | typeof patterns | typeof pages;
41
+ filesTable: typeof componentFiles | typeof patternFiles | typeof pageFiles;
42
+ fileFkColumn: typeof componentFiles.componentId;
43
+ fileFkName: 'componentId' | 'patternId' | 'pageId';
44
+ pathPrefix: string;
45
+ }
46
+
47
+ const ENTITY: Record<ManagedEntityKind, EntitySpec> = {
48
+ component: {
49
+ table: components,
50
+ filesTable: componentFiles,
51
+ fileFkColumn: componentFiles.componentId,
52
+ fileFkName: 'componentId',
53
+ pathPrefix: 'component',
54
+ },
55
+ pattern: {
56
+ table: patterns,
57
+ filesTable: patternFiles as unknown as typeof componentFiles,
58
+ fileFkColumn: patternFiles.patternId as unknown as typeof componentFiles.componentId,
59
+ fileFkName: 'patternId',
60
+ pathPrefix: 'pattern',
61
+ },
62
+ page: {
63
+ table: pages,
64
+ filesTable: pageFiles as unknown as typeof componentFiles,
65
+ fileFkColumn: pageFiles.pageId as unknown as typeof componentFiles.componentId,
66
+ fileFkName: 'pageId',
67
+ pathPrefix: 'page',
68
+ },
69
+ };
70
+
71
+ const asString = (value: unknown): string | undefined => (typeof value === 'string' ? value : undefined);
72
+ const asStringArray = (value: unknown): string[] | undefined => (Array.isArray(value) ? (value as string[]) : undefined);
73
+
74
+ /** Attach review metadata to a normalized record for serving. */
75
+ const withMetadata = (record: unknown, metadata: RegistryReviewMetadata | null | undefined): RegistryEntityData => ({
76
+ ...(record as Record<string, unknown>),
77
+ metadata: metadata ?? null,
78
+ });
79
+
80
+ /** Build a minimal, valid metadata-only component record from allowlisted fields. */
81
+ const buildComponentRecord = (id: string, path: string, fields: Record<string, unknown>): ComponentListObject => ({
82
+ id,
83
+ path,
84
+ title: asString(fields.title) ?? id,
85
+ description: asString(fields.description) ?? '',
86
+ image: '',
87
+ group: asString(fields.group) ?? '',
88
+ type: '',
89
+ properties: {},
90
+ previews: {},
91
+ ...(asStringArray(fields.tags) ? { tags: asStringArray(fields.tags) } : {}),
92
+ ...(asStringArray(fields.categories) ? { categories: asStringArray(fields.categories) } : {}),
93
+ });
94
+
95
+ /** Build a minimal, valid metadata-only pattern record from allowlisted fields. */
96
+ const buildPatternRecord = (id: string, path: string, fields: Record<string, unknown>): PatternListObject => ({
97
+ id,
98
+ path,
99
+ title: asString(fields.title) ?? id,
100
+ description: asString(fields.description) ?? '',
101
+ group: asString(fields.group) ?? '',
102
+ components: [],
103
+ ...(asStringArray(fields.tags) ? { tags: asStringArray(fields.tags) } : {}),
104
+ });
105
+
106
+ /** Build a minimal, valid metadata-only page record from allowlisted fields. */
107
+ const buildPageRecord = (id: string, path: string, fields: Record<string, unknown>): PageListObject => ({
108
+ id,
109
+ path,
110
+ title: asString(fields.title) ?? id,
111
+ description: asString(fields.description) ?? '',
112
+ group: asString(fields.group) ?? '',
113
+ });
114
+
115
+ /** Whether an entity exists by id. */
116
+ export const entityExists = async (db: RegistryDatabase, kind: ManagedEntityKind, id: string): Promise<boolean> => {
117
+ const spec = ENTITY[kind];
118
+ const rows = await db.select({ id: spec.table.id }).from(spec.table).where(eq(spec.table.id, id)).limit(1);
119
+ return Boolean(rows[0]);
120
+ };
121
+
122
+ /** List all entities of a kind, each merged with its review metadata. */
123
+ export const listEntities = async (db: RegistryDatabase, kind: ManagedEntityKind): Promise<RegistryEntityData[]> => {
124
+ const spec = ENTITY[kind];
125
+ const rows = await db.select({ record: spec.table.record, metadata: spec.table.metadata }).from(spec.table);
126
+ return rows.map((row) => withMetadata(row.record, row.metadata));
127
+ };
128
+
129
+ /** Read a single entity with its build/artifact state, or `null` when absent. */
130
+ export const getEntity = async (db: RegistryDatabase, kind: ManagedEntityKind, id: string): Promise<EntityReadResult | null> => {
131
+ const spec = ENTITY[kind];
132
+ const rows = await db
133
+ .select({ record: spec.table.record, metadata: spec.table.metadata })
134
+ .from(spec.table)
135
+ .where(eq(spec.table.id, id))
136
+ .limit(1);
137
+ const row = rows[0];
138
+ if (!row) {
139
+ return null;
140
+ }
141
+ return { data: withMetadata(row.record, row.metadata), build: await resolveBuildMeta(db, kind, id) };
142
+ };
143
+
144
+ /** Create a metadata-only entity record. Returns `null` when an entity with that id already exists. */
145
+ export const createEntity = async (
146
+ db: RegistryDatabase,
147
+ kind: ManagedEntityKind,
148
+ write: ValidatedMetadataWrite
149
+ ): Promise<EntityReadResult | null> => {
150
+ const spec = ENTITY[kind];
151
+ const id = write.id as string;
152
+ const path = `${spec.pathPrefix}/${id}`;
153
+ const { fields } = write;
154
+ const metadata = write.metadata ?? null;
155
+ const record =
156
+ kind === 'component'
157
+ ? buildComponentRecord(id, path, fields)
158
+ : kind === 'pattern'
159
+ ? buildPatternRecord(id, path, fields)
160
+ : buildPageRecord(id, path, fields);
161
+
162
+ // `tags` is promoted on components/patterns only; the pages table has no such column.
163
+ const baseValues = {
164
+ id,
165
+ path,
166
+ title: asString(fields.title),
167
+ description: asString(fields.description),
168
+ group: asString(fields.group),
169
+ record,
170
+ metadata,
171
+ };
172
+ const values =
173
+ kind === 'component'
174
+ ? { ...baseValues, tags: asStringArray(fields.tags), type: '', categories: asStringArray(fields.categories) }
175
+ : kind === 'pattern'
176
+ ? { ...baseValues, tags: asStringArray(fields.tags), components: [] }
177
+ : baseValues;
178
+
179
+ const inserted = await db
180
+ .insert(spec.table)
181
+ .values(values as any)
182
+ .onConflictDoNothing({ target: spec.table.id })
183
+ .returning({ id: spec.table.id });
184
+ if (inserted.length === 0) {
185
+ return null;
186
+ }
187
+ return { data: withMetadata(record, metadata), build: await resolveBuildMeta(db, kind, id) };
188
+ };
189
+
190
+ /**
191
+ * Apply an allowlisted metadata update: allowlisted top-level fields are written to both the
192
+ * normalized record and their promoted columns, and review metadata is merged over the existing
193
+ * value. Artifacts and build state are deliberately untouched. Returns `null` when absent.
194
+ */
195
+ export const updateEntityMetadata = async (
196
+ db: RegistryDatabase,
197
+ kind: ManagedEntityKind,
198
+ id: string,
199
+ write: ValidatedMetadataWrite
200
+ ): Promise<EntityReadResult | null> => {
201
+ const spec = ENTITY[kind];
202
+ const rows = await db
203
+ .select({ record: spec.table.record, metadata: spec.table.metadata })
204
+ .from(spec.table)
205
+ .where(eq(spec.table.id, id))
206
+ .limit(1);
207
+ const row = rows[0];
208
+ if (!row) {
209
+ return null;
210
+ }
211
+
212
+ const record = { ...(row.record as Record<string, unknown>) };
213
+ const { fields } = write;
214
+ const promoted: Record<string, unknown> = { updatedAt: new Date() };
215
+
216
+ if ('title' in fields) {
217
+ record.title = fields.title;
218
+ promoted.title = fields.title;
219
+ }
220
+ if ('description' in fields) {
221
+ record.description = fields.description;
222
+ promoted.description = fields.description;
223
+ }
224
+ if ('group' in fields) {
225
+ record.group = fields.group;
226
+ promoted.group = fields.group;
227
+ }
228
+ if ('tags' in fields) {
229
+ record.tags = fields.tags;
230
+ promoted.tags = fields.tags;
231
+ }
232
+ if (kind === 'component' && 'categories' in fields) {
233
+ record.categories = fields.categories;
234
+ promoted.categories = fields.categories;
235
+ }
236
+
237
+ const metadata = mergeReviewMetadata(row.metadata, write.metadata);
238
+ promoted.record = record;
239
+ promoted.metadata = metadata;
240
+
241
+ await db
242
+ .update(spec.table)
243
+ .set(promoted as any)
244
+ .where(eq(spec.table.id, id));
245
+ return { data: withMetadata(record, metadata), build: await resolveBuildMeta(db, kind, id) };
246
+ };
247
+
248
+ /**
249
+ * Delete an entity and only the artifacts it owns. Cascade removes its text-file records; shared
250
+ * artifacts owned by `asset` are preserved. Returns `false` when the entity does not exist.
251
+ */
252
+ export const deleteEntity = async (db: RegistryDatabase, kind: ManagedEntityKind, id: string): Promise<boolean> => {
253
+ return db.transaction(async (tx) => {
254
+ const transactionalDb = tx as unknown as RegistryDatabase;
255
+ const spec = ENTITY[kind];
256
+ if (!(await entityExists(transactionalDb, kind, id))) {
257
+ return false;
258
+ }
259
+ await transactionalDb.delete(docsArtifacts).where(and(eq(docsArtifacts.entityKind, kind), eq(docsArtifacts.entityId, id)));
260
+ if (kind !== 'page') {
261
+ await transactionalDb.delete(docsArtifacts).where(and(eq(docsArtifacts.ownerKind, kind), eq(docsArtifacts.ownerId, id)));
262
+ }
263
+ await transactionalDb.delete(buildMetadata).where(and(eq(buildMetadata.entityKind, kind), eq(buildMetadata.entityId, id)));
264
+ await transactionalDb.delete(spec.table).where(eq(spec.table.id, id));
265
+ return true;
266
+ });
267
+ };
268
+
269
+ /** List an entity's text-file records (inline content only). */
270
+ export const listEntityFiles = async (db: RegistryDatabase, kind: ManagedEntityKind, id: string): Promise<ValidatedFile[]> => {
271
+ const spec = ENTITY[kind];
272
+ const rows = await db
273
+ .select({
274
+ path: spec.filesTable.path,
275
+ kind: spec.filesTable.kind,
276
+ content: spec.filesTable.content,
277
+ contentType: spec.filesTable.contentType,
278
+ })
279
+ .from(spec.filesTable)
280
+ .where(eq(spec.fileFkColumn, id));
281
+ return rows
282
+ .filter((row) => row.content != null)
283
+ .map((row) => ({ path: row.path, kind: row.kind, content: row.content as string, contentType: row.contentType }));
284
+ };
285
+
286
+ /** Read one text-file record by path, or `null` when absent (or stored without inline content). */
287
+ export const getEntityFile = async (
288
+ db: RegistryDatabase,
289
+ kind: ManagedEntityKind,
290
+ id: string,
291
+ filePath: string
292
+ ): Promise<ValidatedFile | null> => {
293
+ const spec = ENTITY[kind];
294
+ const rows = await db
295
+ .select({
296
+ path: spec.filesTable.path,
297
+ kind: spec.filesTable.kind,
298
+ content: spec.filesTable.content,
299
+ contentType: spec.filesTable.contentType,
300
+ })
301
+ .from(spec.filesTable)
302
+ .where(and(eq(spec.fileFkColumn, id), eq(spec.filesTable.path, filePath)))
303
+ .limit(1);
304
+ const row = rows[0];
305
+ if (!row || row.content == null) {
306
+ return null;
307
+ }
308
+ return { path: row.path, kind: row.kind, content: row.content, contentType: row.contentType };
309
+ };
310
+
311
+ /** Insert or update a text-file record for an entity. */
312
+ export const upsertEntityFile = async (
313
+ db: RegistryDatabase,
314
+ kind: ManagedEntityKind,
315
+ id: string,
316
+ file: ValidatedFile
317
+ ): Promise<ValidatedFile> => {
318
+ const spec = ENTITY[kind];
319
+ await db
320
+ .insert(spec.filesTable)
321
+ .values({
322
+ [spec.fileFkName]: id,
323
+ path: file.path,
324
+ kind: file.kind,
325
+ content: file.content,
326
+ contentType: file.contentType,
327
+ } as any)
328
+ .onConflictDoUpdate({
329
+ target: [spec.fileFkColumn, spec.filesTable.path],
330
+ set: { kind: file.kind, content: file.content, contentType: file.contentType, updatedAt: new Date() },
331
+ });
332
+ return file;
333
+ };
334
+
335
+ /** Delete a text-file record by path. Returns `false` when no such file exists. */
336
+ export const deleteEntityFile = async (db: RegistryDatabase, kind: ManagedEntityKind, id: string, filePath: string): Promise<boolean> => {
337
+ const spec = ENTITY[kind];
338
+ const rows = await db
339
+ .select({ path: spec.filesTable.path })
340
+ .from(spec.filesTable)
341
+ .where(and(eq(spec.fileFkColumn, id), eq(spec.filesTable.path, filePath)))
342
+ .limit(1);
343
+ if (!rows[0]) {
344
+ return false;
345
+ }
346
+ await db.delete(spec.filesTable).where(and(eq(spec.fileFkColumn, id), eq(spec.filesTable.path, filePath)));
347
+ return true;
348
+ };
@@ -0,0 +1,231 @@
1
+ import type { NextApiRequest, NextApiResponse } from 'next';
2
+ import { eq } from 'drizzle-orm';
3
+ import type { RegistryDatabase } from '@handoff/registry/db/client';
4
+ import { tokenArtifacts, tokenSets } from '@handoff/registry/db/schema';
5
+ import { isTokenSetId, kindForSetId } from '@handoff/registry/tokens/sets';
6
+ import type { TokenSetTransferArtifact, TokenSetTransferPackage } from '@handoff/registry/tokens/transfer';
7
+ import { joinedQueryValue } from '../api/query';
8
+ import { sendRegistryError } from './errors';
9
+ import { buildMeta } from './meta';
10
+ import { handleRegistryRoute, sendRegistryData } from './handler';
11
+ import {
12
+ asString,
13
+ invalidPackage as invalid,
14
+ isPlainObject,
15
+ normalizeSafeRelativePath,
16
+ type PackageValidation,
17
+ validateTransferBuild,
18
+ } from './validation';
19
+
20
+ /**
21
+ * Token-set transfer ingestion for the registry.
22
+ *
23
+ * - `GET /api/registry/transfer/tokens` lists set summaries (id + kind + source hash) for the CLI's
24
+ * skip-unchanged / bulk-checkout logic.
25
+ * - `GET /api/registry/transfer/tokens/:setId` is checkout: the set record + its generated artifacts.
26
+ * - `PUT /api/registry/transfer/tokens/:setId` is publish ingestion: it validates the package and
27
+ * atomically upserts the `token_sets` row and replaces its `token_artifacts` in one transaction, so
28
+ * a set is never left partially written — either the new version commits or the previous one stays.
29
+ * An unchanged set (matching `sourceHash`) is a no-op. Absent sets are never deleted.
30
+ *
31
+ * Runs behind {@link handleRegistryRoute}: runtime-mode, method, bearer-token (on PUT), and database
32
+ * guards apply before any of this executes.
33
+ */
34
+
35
+ /** Validate a token-set publish package against the transfer contract. */
36
+ const validateTokenPackage = (body: unknown, setId: string): PackageValidation<TokenSetTransferPackage> => {
37
+ if (!isPlainObject(body)) {
38
+ return invalid('Request body must be a JSON object.');
39
+ }
40
+ if (!isTokenSetId(setId)) {
41
+ return invalid(`Unknown token set id "${setId}".`, { rejectedFields: ['id'] });
42
+ }
43
+
44
+ const bodyId = asString(body.id);
45
+ if (bodyId !== undefined && bodyId !== setId) {
46
+ return invalid(`Package set id "${bodyId}" does not match the publish target "${setId}".`, { rejectedFields: ['id'] });
47
+ }
48
+
49
+ const expectedKind = kindForSetId(setId);
50
+ const kind = asString(body.kind) ?? expectedKind;
51
+ if (kind !== 'foundation' && kind !== 'component') {
52
+ return invalid('`kind` must be "foundation" or "component".', { rejectedFields: ['kind'] });
53
+ }
54
+ if (kind !== expectedKind) {
55
+ return invalid(`Package kind "${kind}" is inconsistent with set id "${setId}".`, { rejectedFields: ['kind'] });
56
+ }
57
+
58
+ if (body.record === undefined || body.record === null) {
59
+ return invalid('A token `record` is required to publish.', { rejectedFields: ['record'] });
60
+ }
61
+
62
+ const rawArtifacts = body.artifacts ?? [];
63
+ if (!Array.isArray(rawArtifacts)) {
64
+ return invalid('`artifacts` must be an array.', { rejectedFields: ['artifacts'] });
65
+ }
66
+ const artifacts: TokenSetTransferArtifact[] = [];
67
+ const seenPaths = new Set<string>();
68
+ for (let i = 0; i < rawArtifacts.length; i += 1) {
69
+ const raw = rawArtifacts[i];
70
+ const field = (name: string) => `artifacts[${i}].${name}`;
71
+ if (!isPlainObject(raw)) {
72
+ return invalid('Each artifact must be an object.', { rejectedFields: [`artifacts[${i}]`] });
73
+ }
74
+ const artifactPath = normalizeSafeRelativePath(raw.path);
75
+ if (!artifactPath) {
76
+ return invalid('Artifact path must be a registry-safe relative path.', { rejectedFields: [field('path')] });
77
+ }
78
+ if (seenPaths.has(artifactPath)) {
79
+ return invalid(`Duplicate artifact path "${artifactPath}" in package.`, { rejectedFields: [field('path')] });
80
+ }
81
+ seenPaths.add(artifactPath);
82
+ if (typeof raw.content !== 'string') {
83
+ return invalid(`Artifact "${artifactPath}" is missing string content.`, { rejectedFields: [field('content')] });
84
+ }
85
+ const format = asString(raw.format);
86
+ if (!format) {
87
+ return invalid(`Artifact "${artifactPath}" is missing a format.`, { rejectedFields: [field('format')] });
88
+ }
89
+ artifacts.push({
90
+ path: artifactPath,
91
+ format,
92
+ content: raw.content as string,
93
+ contentType: asString(raw.contentType) || 'text/plain; charset=utf-8',
94
+ hash: asString(raw.hash),
95
+ size: typeof raw.size === 'number' ? raw.size : undefined,
96
+ });
97
+ }
98
+
99
+ const buildValidation = validateTransferBuild(body.build, {
100
+ requiredHashField: 'sourceHash',
101
+ currentMessage: 'A "current" token build requires builtAt, builderVersion, and sourceHash.',
102
+ });
103
+ if (!buildValidation.ok) {
104
+ return invalid(buildValidation.message, buildValidation.details);
105
+ }
106
+ const build = buildValidation.value;
107
+
108
+ return {
109
+ ok: true,
110
+ value: {
111
+ id: setId,
112
+ kind,
113
+ record: body.record,
114
+ artifacts,
115
+ build: {
116
+ status: build.status,
117
+ builtAt: build.builtAt,
118
+ builderVersion: build.builderVersion,
119
+ sourceHash: build.sourceHash,
120
+ },
121
+ },
122
+ };
123
+ };
124
+
125
+ /** The stored source hash for a set, or `undefined` when the set does not exist yet. */
126
+ const storedSourceHash = async (db: RegistryDatabase, id: string): Promise<string | null | undefined> => {
127
+ const rows = await db.select({ sourceHash: tokenSets.sourceHash }).from(tokenSets).where(eq(tokenSets.id, id)).limit(1);
128
+ return rows.length ? rows[0].sourceHash : undefined;
129
+ };
130
+
131
+ /** Atomically upsert the set row and replace its artifacts in one transaction. */
132
+ const ingestTokenSet = async (db: RegistryDatabase, pkg: TokenSetTransferPackage): Promise<void> => {
133
+ const setValues = {
134
+ kind: pkg.kind,
135
+ record: pkg.record,
136
+ sourceHash: pkg.build.sourceHash ?? null,
137
+ status: pkg.build.status,
138
+ builtAt: pkg.build.builtAt ? new Date(pkg.build.builtAt) : null,
139
+ builderVersion: pkg.build.builderVersion ?? null,
140
+ updatedAt: new Date(),
141
+ };
142
+
143
+ await db.transaction(async (tx) => {
144
+ await tx
145
+ .insert(tokenSets)
146
+ .values({ id: pkg.id, metadata: null, ...setValues } as any)
147
+ .onConflictDoUpdate({ target: tokenSets.id, set: setValues as any });
148
+
149
+ await tx.delete(tokenArtifacts).where(eq(tokenArtifacts.tokenSetId, pkg.id));
150
+ for (const artifact of pkg.artifacts) {
151
+ await tx.insert(tokenArtifacts).values({
152
+ tokenSetId: pkg.id,
153
+ path: artifact.path,
154
+ format: artifact.format,
155
+ content: artifact.content,
156
+ contentType: artifact.contentType,
157
+ hash: artifact.hash ?? null,
158
+ size: artifact.size ?? null,
159
+ } as any);
160
+ }
161
+ });
162
+ };
163
+
164
+ /** `GET /api/registry/transfer/tokens` — list set summaries (unauthenticated read behind the guards). */
165
+ export const handleTokenSummaryRoute = (req: NextApiRequest, res: NextApiResponse): Promise<void> =>
166
+ handleRegistryRoute(req, res, ['GET'], async ({ db }) => {
167
+ const rows = await db
168
+ .select({ id: tokenSets.id, kind: tokenSets.kind, sourceHash: tokenSets.sourceHash, status: tokenSets.status })
169
+ .from(tokenSets);
170
+ sendRegistryData(res, 200, { sets: rows }, buildMeta());
171
+ });
172
+
173
+ /** `GET /api/registry/transfer/tokens/:setId` — checkout one set's record + generated artifacts. */
174
+ export const handleTokenCheckoutRoute = (req: NextApiRequest, res: NextApiResponse): Promise<void> =>
175
+ handleRegistryRoute(req, res, ['GET'], async ({ db }) => {
176
+ const id = joinedQueryValue(req.query.setId) ?? '';
177
+ if (!id) {
178
+ sendRegistryError(res, 'not_found', 'Missing token set id.');
179
+ return;
180
+ }
181
+ if (!isTokenSetId(id)) {
182
+ sendRegistryError(res, 'bad_request', `Unknown or unsafe token set id "${id}".`, {
183
+ rejectedFields: ['id'],
184
+ });
185
+ return;
186
+ }
187
+ const rows = await db.select({ kind: tokenSets.kind, record: tokenSets.record }).from(tokenSets).where(eq(tokenSets.id, id)).limit(1);
188
+ const set = rows[0];
189
+ if (!set) {
190
+ sendRegistryError(res, 'not_found', `No token set "${id}" exists in the registry.`);
191
+ return;
192
+ }
193
+ const artifacts = await db
194
+ .select({
195
+ path: tokenArtifacts.path,
196
+ format: tokenArtifacts.format,
197
+ content: tokenArtifacts.content,
198
+ contentType: tokenArtifacts.contentType,
199
+ hash: tokenArtifacts.hash,
200
+ size: tokenArtifacts.size,
201
+ })
202
+ .from(tokenArtifacts)
203
+ .where(eq(tokenArtifacts.tokenSetId, id));
204
+ sendRegistryData(res, 200, { id, kind: set.kind, record: set.record, artifacts }, buildMeta());
205
+ });
206
+
207
+ /** `PUT /api/registry/transfer/tokens/:setId` — validate + atomically ingest one set. */
208
+ export const handleTokenTransferRoute = (req: NextApiRequest, res: NextApiResponse): Promise<void> =>
209
+ handleRegistryRoute(req, res, ['PUT'], async ({ db }) => {
210
+ const id = joinedQueryValue(req.query.setId) ?? '';
211
+ if (!id) {
212
+ sendRegistryError(res, 'not_found', 'Missing token set id.');
213
+ return;
214
+ }
215
+
216
+ const validation = validateTokenPackage(req.body, id);
217
+ if (!validation.ok) {
218
+ sendRegistryError(res, 'bad_request', validation.message ?? 'Invalid token set package.', validation.details);
219
+ return;
220
+ }
221
+ const pkg = validation.value;
222
+
223
+ const existingHash = await storedSourceHash(db, id);
224
+ if (existingHash && pkg.build.sourceHash && existingHash === pkg.build.sourceHash) {
225
+ sendRegistryData(res, 200, { id, kind: pkg.kind, published: false, unchanged: true }, buildMeta());
226
+ return;
227
+ }
228
+
229
+ await ingestTokenSet(db, pkg);
230
+ sendRegistryData(res, 200, { id, kind: pkg.kind, published: true, artifacts: pkg.artifacts.length }, buildMeta());
231
+ });