handoff-app 2.0.0-4 → 2.0.0-6

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 (234) hide show
  1. package/AGENTS.md +7 -0
  2. package/CLAUDE.md +7 -0
  3. package/README.md +346 -377
  4. package/dist/app-builder/build.js +11 -4
  5. package/dist/app-builder/client-config.js +1 -0
  6. package/dist/app-builder/nav-shell.js +8 -15
  7. package/dist/app-builder/vercel-output.js +4 -4
  8. package/dist/cli/make.js +2 -2
  9. package/dist/cli.js +38 -6
  10. package/dist/commands/build/app.js +1 -5
  11. package/dist/commands/build/components.js +1 -5
  12. package/dist/commands/build/index.js +1 -5
  13. package/dist/commands/checkout/index.d.ts +15 -12
  14. package/dist/commands/checkout/index.js +15 -54
  15. package/dist/commands/compat/deprecations.d.ts +46 -0
  16. package/dist/commands/compat/deprecations.js +62 -0
  17. package/dist/commands/compat/push-args.d.ts +48 -0
  18. package/dist/commands/compat/push-args.js +73 -0
  19. package/dist/commands/db/migrate.js +1 -5
  20. package/dist/commands/dev/index.js +1 -5
  21. package/dist/commands/eject/config.js +1 -5
  22. package/dist/commands/eject/pages.js +1 -5
  23. package/dist/commands/eject/theme.js +1 -5
  24. package/dist/commands/fetch/index.js +1 -5
  25. package/dist/commands/index.js +4 -0
  26. package/dist/commands/login/index.js +1 -5
  27. package/dist/commands/logout/index.js +1 -5
  28. package/dist/commands/make/component.js +1 -5
  29. package/dist/commands/make/page.js +3 -7
  30. package/dist/commands/make/template.js +1 -5
  31. package/dist/commands/publish/index.d.ts +18 -12
  32. package/dist/commands/publish/index.js +18 -54
  33. package/dist/commands/pull/index.d.ts +13 -0
  34. package/dist/commands/pull/index.js +20 -0
  35. package/dist/commands/push/index.d.ts +23 -0
  36. package/dist/commands/push/index.js +58 -0
  37. package/dist/commands/scaffold/index.js +1 -5
  38. package/dist/commands/start/index.js +1 -5
  39. package/dist/commands/types.d.ts +10 -0
  40. package/dist/commands/utils.d.ts +74 -6
  41. package/dist/commands/utils.js +89 -10
  42. package/dist/commands/validate/components.js +1 -5
  43. package/dist/config/defaults.d.ts +2 -0
  44. package/dist/config/defaults.js +4 -1
  45. package/dist/config/index.d.ts +3 -2
  46. package/dist/config/index.js +3 -1
  47. package/dist/config/loader.d.ts +19 -6
  48. package/dist/config/loader.js +43 -13
  49. package/dist/config/normalizers/page.d.ts +4 -4
  50. package/dist/config/normalizers/page.js +2 -2
  51. package/dist/config/runtime.js +9 -25
  52. package/dist/index.d.ts +69 -22
  53. package/dist/index.js +148 -35
  54. package/dist/nav/resolver.js +3 -1
  55. package/dist/nav/workspace.js +2 -2
  56. package/dist/registry/auth/access-tokens.d.ts +11 -3
  57. package/dist/registry/auth/access-tokens.js +29 -3
  58. package/dist/registry/auth/index.d.ts +4 -3
  59. package/dist/registry/auth/index.js +10 -2
  60. package/dist/registry/auth/sync-secret.d.ts +27 -0
  61. package/dist/registry/auth/sync-secret.js +65 -0
  62. package/dist/registry/auth/types.d.ts +20 -1
  63. package/dist/registry/auth/types.js +3 -1
  64. package/dist/registry/auth/validation.d.ts +11 -0
  65. package/dist/registry/auth/validation.js +18 -1
  66. package/dist/registry/checkout/assets.d.ts +1 -1
  67. package/dist/registry/checkout/assets.js +21 -6
  68. package/dist/registry/checkout/index.d.ts +8 -1
  69. package/dist/registry/checkout/index.js +39 -6
  70. package/dist/registry/checkout/tokens.d.ts +1 -1
  71. package/dist/registry/checkout/tokens.js +12 -5
  72. package/dist/registry/connection.d.ts +10 -0
  73. package/dist/registry/connection.js +52 -7
  74. package/dist/registry/content-kinds.d.ts +27 -0
  75. package/dist/registry/content-kinds.js +33 -0
  76. package/dist/registry/db/client.d.ts +6 -1
  77. package/dist/registry/db/client.js +16 -3
  78. package/dist/registry/db/schema.d.ts +37 -3
  79. package/dist/registry/db/schema.js +9 -7
  80. package/dist/registry/publish/assets.d.ts +2 -2
  81. package/dist/registry/publish/assets.js +46 -35
  82. package/dist/registry/publish/index.d.ts +29 -5
  83. package/dist/registry/publish/index.js +87 -34
  84. package/dist/registry/publish/tokens.d.ts +2 -2
  85. package/dist/registry/publish/tokens.js +30 -24
  86. package/dist/registry/selection.d.ts +12 -0
  87. package/dist/registry/selection.js +22 -0
  88. package/dist/store/registry.d.ts +48 -0
  89. package/dist/store/registry.js +75 -12
  90. package/dist/transformers/preview/types.d.ts +2 -2
  91. package/dist/types/config.d.ts +7 -0
  92. package/dist/utils/markdown.d.ts +10 -0
  93. package/dist/utils/markdown.js +17 -0
  94. package/dist/utils/menu-shell.js +14 -7
  95. package/dist/utils/pages.d.ts +22 -0
  96. package/dist/utils/pages.js +73 -0
  97. package/dist/utils/path.d.ts +5 -0
  98. package/dist/utils/path.js +9 -0
  99. package/docs/api.md +1 -3
  100. package/docs/cli.md +1 -1
  101. package/drizzle/0002_add_pages.sql +5 -3
  102. package/drizzle/meta/0002_snapshot.json +17 -8
  103. package/drizzle/meta/0003_snapshot.json +17 -8
  104. package/drizzle/meta/0004_snapshot.json +17 -8
  105. package/drizzle/meta/0005_snapshot.json +17 -8
  106. package/package.json +4 -2
  107. package/src/app/components/CopyToClipboard.tsx +27 -26
  108. package/src/app/components/Layout/Header.tsx +4 -12
  109. package/src/app/components/Layout/RuntimeModeBadge.tsx +18 -0
  110. package/src/app/components/McpIntegration/McpConfigDialog.tsx +349 -0
  111. package/src/app/components/McpIntegration/McpIcon.tsx +10 -0
  112. package/src/app/components/Navigation/MobileNav.tsx +3 -1
  113. package/src/app/components/ui/dialog.tsx +73 -0
  114. package/src/app/components/util/index.ts +5 -23
  115. package/src/app/lib/docs-api/backend.ts +16 -1
  116. package/src/app/lib/docs-api/errors.ts +8 -7
  117. package/src/app/lib/docs-api/handler.ts +2 -3
  118. package/src/app/lib/docs-api/index.ts +2 -0
  119. package/src/app/lib/docs-api/page-discovery.ts +121 -0
  120. package/src/app/lib/docs-api/page-rendering.ts +37 -0
  121. package/src/app/lib/docs-api/page-search.ts +191 -0
  122. package/src/app/lib/docs-api/records.ts +31 -24
  123. package/src/app/lib/docs-api/registry.ts +92 -11
  124. package/src/app/lib/docs-api/runtime-config.ts +5 -0
  125. package/src/app/lib/docs-api/search.ts +120 -0
  126. package/src/app/lib/mcp/auth.ts +64 -0
  127. package/src/app/lib/mcp/index.ts +2 -0
  128. package/src/app/lib/mcp/server.ts +343 -0
  129. package/src/app/lib/mcp/shape.ts +595 -0
  130. package/src/app/lib/registry-api/allowlist.ts +7 -5
  131. package/src/app/lib/registry-api/asset-transfer.ts +58 -31
  132. package/src/app/lib/registry-api/auth.ts +89 -9
  133. package/src/app/lib/registry-api/entity-routes.ts +17 -7
  134. package/src/app/lib/registry-api/errors.ts +10 -4
  135. package/src/app/lib/registry-api/handler.ts +13 -15
  136. package/src/app/lib/registry-api/meta.ts +4 -0
  137. package/src/app/lib/registry-api/revalidate.ts +31 -12
  138. package/src/app/lib/registry-api/store.ts +89 -26
  139. package/src/app/lib/registry-api/sync-compat.ts +166 -0
  140. package/src/app/lib/registry-api/token-transfer.ts +44 -12
  141. package/src/app/lib/registry-api/transfer-batch.ts +116 -0
  142. package/src/app/lib/registry-api/transfer.ts +62 -35
  143. package/src/app/lib/registry-api/validation.ts +33 -1
  144. package/src/app/lib/registry-connection.ts +1 -1
  145. package/src/app/next.config.mjs +9 -0
  146. package/src/app/pages/[...slug]/index.tsx +52 -51
  147. package/src/app/pages/api/account/tokens/index.ts +4 -13
  148. package/src/app/pages/api/docs/search/pages.json.ts +25 -0
  149. package/src/app/pages/api/mcp.ts +56 -0
  150. package/src/app/pages/api/oauth/revoke.ts +14 -7
  151. package/src/app/pages/api/registry/components/[id]/files/[...filePath].ts +1 -1
  152. package/src/app/pages/api/registry/components/[id].ts +2 -2
  153. package/src/app/pages/api/registry/components/index.ts +1 -1
  154. package/src/app/pages/api/registry/pages/[...id].ts +20 -0
  155. package/src/app/pages/api/registry/pages/index.ts +19 -0
  156. package/src/app/pages/api/registry/patterns/[id]/files/[...filePath].ts +1 -1
  157. package/src/app/pages/api/registry/patterns/[id].ts +2 -2
  158. package/src/app/pages/api/registry/patterns/index.ts +1 -1
  159. package/src/app/pages/api/registry/transfer/assets/blobs/have.ts +2 -2
  160. package/src/app/pages/api/sync/upload.ts +21 -0
  161. package/src/app/pages/index/index.tsx +14 -0
  162. package/src/app/pages/index.tsx +31 -5
  163. package/src/app-builder/build.ts +11 -4
  164. package/src/app-builder/client-config.ts +2 -1
  165. package/src/app-builder/nav-shell.ts +8 -13
  166. package/src/app-builder/vercel-output.ts +4 -4
  167. package/src/cli/make.ts +2 -2
  168. package/src/cli.ts +30 -6
  169. package/src/commands/build/app.ts +2 -3
  170. package/src/commands/build/components.ts +2 -3
  171. package/src/commands/build/index.ts +2 -3
  172. package/src/commands/checkout/index.ts +18 -44
  173. package/src/commands/compat/deprecations.ts +79 -0
  174. package/src/commands/compat/push-args.ts +106 -0
  175. package/src/commands/db/migrate.ts +2 -3
  176. package/src/commands/dev/index.ts +2 -3
  177. package/src/commands/eject/config.ts +2 -3
  178. package/src/commands/eject/pages.ts +2 -3
  179. package/src/commands/eject/theme.ts +2 -3
  180. package/src/commands/fetch/index.ts +2 -3
  181. package/src/commands/index.ts +4 -0
  182. package/src/commands/login/index.ts +2 -3
  183. package/src/commands/logout/index.ts +2 -3
  184. package/src/commands/make/component.ts +2 -3
  185. package/src/commands/make/page.ts +4 -5
  186. package/src/commands/make/template.ts +2 -3
  187. package/src/commands/publish/index.ts +21 -44
  188. package/src/commands/pull/index.ts +32 -0
  189. package/src/commands/push/index.ts +115 -0
  190. package/src/commands/scaffold/index.ts +2 -3
  191. package/src/commands/start/index.ts +2 -3
  192. package/src/commands/types.ts +11 -1
  193. package/src/commands/utils.ts +80 -11
  194. package/src/commands/validate/components.ts +2 -3
  195. package/src/config/defaults.ts +3 -0
  196. package/src/config/index.ts +3 -2
  197. package/src/config/loader.ts +57 -12
  198. package/src/config/normalizers/page.ts +4 -4
  199. package/src/config/runtime.ts +9 -24
  200. package/src/index.ts +183 -42
  201. package/src/nav/resolver.ts +3 -1
  202. package/src/nav/workspace.ts +2 -2
  203. package/src/registry/auth/access-tokens.ts +29 -5
  204. package/src/registry/auth/index.ts +7 -1
  205. package/src/registry/auth/sync-secret.ts +67 -0
  206. package/src/registry/auth/types.ts +24 -1
  207. package/src/registry/auth/validation.ts +18 -0
  208. package/src/registry/checkout/assets.ts +24 -7
  209. package/src/registry/checkout/index.ts +41 -5
  210. package/src/registry/checkout/tokens.ts +12 -6
  211. package/src/registry/connection.ts +53 -5
  212. package/src/registry/content-kinds.ts +39 -0
  213. package/src/registry/db/client.ts +22 -2
  214. package/src/registry/db/schema.ts +9 -7
  215. package/src/registry/publish/assets.ts +47 -37
  216. package/src/registry/publish/index.ts +102 -38
  217. package/src/registry/publish/tokens.ts +35 -26
  218. package/src/registry/selection.ts +19 -0
  219. package/src/store/registry.ts +118 -13
  220. package/src/transformers/preview/types.ts +2 -2
  221. package/src/types/config.ts +7 -0
  222. package/src/utils/README.md +1 -0
  223. package/src/utils/markdown.ts +11 -0
  224. package/src/utils/menu-shell.ts +14 -7
  225. package/src/utils/pages.ts +52 -0
  226. package/src/utils/path.ts +15 -6
  227. package/templates/default/README.md.tpl +10 -0
  228. package/templates/default/handoff.config.js.tpl +6 -3
  229. package/templates/default/handoff.config.ts.tpl +6 -3
  230. package/templates/default/handoff.config.with-examples.js.tpl +6 -3
  231. package/templates/default/handoff.config.with-examples.ts.tpl +6 -3
  232. package/templates/default/package.json.tpl +4 -0
  233. package/config/docs/foundations/logos.md +0 -7
  234. package/src/app/lib/api/redact.ts +0 -46
@@ -1,7 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.setRegistryUserStatus = exports.updateRegistryUserRole = exports.updateRegistryUserProfile = exports.authenticateRegistryCredentials = exports.listRegistryUsers = exports.getRegistryUserById = exports.hashRateLimitIdentifier = exports.purgeExpiredAuthRateLimits = exports.clearAuthRateLimit = exports.consumeAuthRateLimit = exports.MAXIMUM_PASSWORD_LENGTH = exports.MINIMUM_PASSWORD_LENGTH = exports.validatePassword = exports.verifyPassword = exports.hashPassword = exports.REGISTRY_INSTALLATION_ID = exports.REGISTRY_AUTH_SCHEMA_VERSION = exports.installRegistry = exports.getRegistryInstallationState = exports.getRegistryInstallation = exports.DEVICE_AUTHORIZATION_POLL_INTERVAL_SECONDS = exports.DEVICE_AUTHORIZATION_LIFETIME_SECONDS = exports.normalizeRegistryUserCode = exports.generateRegistryUserCode = exports.purgeExpiredRegistryDeviceAuthorizations = exports.exchangeRegistryDeviceAuthorization = exports.denyRegistryDeviceAuthorization = exports.approveRegistryDeviceAuthorization = exports.createRegistryDeviceAuthorization = exports.buildGravatarUrl = exports.secretHashMatches = exports.normalizeEmail = exports.hashSecret = exports.createOpaqueSecret = exports.PASSWORD_RESET_LIFETIME_MS = exports.INVITATION_LIFETIME_MS = exports.resetPassword = exports.createPasswordReset = exports.acceptUserInvitation = exports.resendUserInvitation = exports.createUserInvitation = exports.registryRoleAllowsScopes = exports.normalizeRegistryScopes = exports.parseRegistryAccessToken = exports.formatRegistryAccessToken = exports.authenticateRegistryAccessToken = exports.revokeAllRegistryAccessTokens = exports.revokeRegistryAccessToken = exports.listRegistryAccessTokens = exports.createRegistryAccessToken = void 0;
4
- exports.validateRegistryImageUrl = exports.validateRegistryEmail = exports.validateRegistryDisplayName = exports.registryPrincipalHasScope = exports.scopesForRegistryRole = exports.REGISTRY_WRITE_SCOPE = exports.REGISTRY_READ_SCOPE = void 0;
3
+ exports.getRegistryUserById = exports.hashRateLimitIdentifier = exports.purgeExpiredAuthRateLimits = exports.clearAuthRateLimit = exports.consumeAuthRateLimit = exports.MAXIMUM_PASSWORD_LENGTH = exports.MINIMUM_PASSWORD_LENGTH = exports.validatePassword = exports.verifyPassword = exports.hashPassword = exports.REGISTRY_INSTALLATION_ID = exports.REGISTRY_AUTH_SCHEMA_VERSION = exports.installRegistry = exports.getRegistryInstallationState = exports.getRegistryInstallation = exports.DEVICE_AUTHORIZATION_POLL_INTERVAL_SECONDS = exports.DEVICE_AUTHORIZATION_LIFETIME_SECONDS = exports.normalizeRegistryUserCode = exports.generateRegistryUserCode = exports.purgeExpiredRegistryDeviceAuthorizations = exports.exchangeRegistryDeviceAuthorization = exports.denyRegistryDeviceAuthorization = exports.approveRegistryDeviceAuthorization = exports.createRegistryDeviceAuthorization = exports.SYNC_SECRET_ENV = exports.resolveRegistrySyncSecret = exports.authenticateRegistrySyncSecret = exports.buildGravatarUrl = exports.secretHashMatches = exports.normalizeEmail = exports.hashSecret = exports.createOpaqueSecret = exports.PASSWORD_RESET_LIFETIME_MS = exports.INVITATION_LIFETIME_MS = exports.resetPassword = exports.createPasswordReset = exports.acceptUserInvitation = exports.resendUserInvitation = exports.createUserInvitation = exports.registryRoleAllowsScopes = exports.resolveRegistryScope = exports.expandRegistryScopes = exports.normalizeRegistryScopes = exports.parseRegistryAccessToken = exports.formatRegistryAccessToken = exports.authenticateRegistryAccessToken = exports.revokeAllRegistryAccessTokens = exports.revokeRegistryAccessToken = exports.listRegistryAccessTokens = exports.createRegistryAccessToken = void 0;
4
+ exports.validateRegistryImageUrl = exports.validateRegistryEmail = exports.validateRegistryDisplayName = exports.isTransmittableCredential = exports.isRegistryTokenPrincipal = exports.registryPrincipalHasScope = exports.scopesForRegistryRole = exports.REGISTRY_WRITE_SCOPE = exports.REGISTRY_READ_SCOPE = exports.setRegistryUserStatus = exports.updateRegistryUserRole = exports.updateRegistryUserProfile = exports.authenticateRegistryCredentials = exports.listRegistryUsers = void 0;
5
5
  var access_tokens_1 = require("./access-tokens");
6
6
  Object.defineProperty(exports, "createRegistryAccessToken", { enumerable: true, get: function () { return access_tokens_1.createRegistryAccessToken; } });
7
7
  Object.defineProperty(exports, "listRegistryAccessTokens", { enumerable: true, get: function () { return access_tokens_1.listRegistryAccessTokens; } });
@@ -11,6 +11,8 @@ Object.defineProperty(exports, "authenticateRegistryAccessToken", { enumerable:
11
11
  Object.defineProperty(exports, "formatRegistryAccessToken", { enumerable: true, get: function () { return access_tokens_1.formatRegistryAccessToken; } });
12
12
  Object.defineProperty(exports, "parseRegistryAccessToken", { enumerable: true, get: function () { return access_tokens_1.parseRegistryAccessToken; } });
13
13
  Object.defineProperty(exports, "normalizeRegistryScopes", { enumerable: true, get: function () { return access_tokens_1.normalizeRegistryScopes; } });
14
+ Object.defineProperty(exports, "expandRegistryScopes", { enumerable: true, get: function () { return access_tokens_1.expandRegistryScopes; } });
15
+ Object.defineProperty(exports, "resolveRegistryScope", { enumerable: true, get: function () { return access_tokens_1.resolveRegistryScope; } });
14
16
  Object.defineProperty(exports, "registryRoleAllowsScopes", { enumerable: true, get: function () { return access_tokens_1.registryRoleAllowsScopes; } });
15
17
  var action_tokens_1 = require("./action-tokens");
16
18
  Object.defineProperty(exports, "createUserInvitation", { enumerable: true, get: function () { return action_tokens_1.createUserInvitation; } });
@@ -27,6 +29,10 @@ Object.defineProperty(exports, "normalizeEmail", { enumerable: true, get: functi
27
29
  Object.defineProperty(exports, "secretHashMatches", { enumerable: true, get: function () { return crypto_1.secretHashMatches; } });
28
30
  var gravatar_1 = require("./gravatar");
29
31
  Object.defineProperty(exports, "buildGravatarUrl", { enumerable: true, get: function () { return gravatar_1.buildGravatarUrl; } });
32
+ var sync_secret_1 = require("./sync-secret");
33
+ Object.defineProperty(exports, "authenticateRegistrySyncSecret", { enumerable: true, get: function () { return sync_secret_1.authenticateRegistrySyncSecret; } });
34
+ Object.defineProperty(exports, "resolveRegistrySyncSecret", { enumerable: true, get: function () { return sync_secret_1.resolveRegistrySyncSecret; } });
35
+ Object.defineProperty(exports, "SYNC_SECRET_ENV", { enumerable: true, get: function () { return sync_secret_1.SYNC_SECRET_ENV; } });
30
36
  var device_authorizations_1 = require("./device-authorizations");
31
37
  Object.defineProperty(exports, "createRegistryDeviceAuthorization", { enumerable: true, get: function () { return device_authorizations_1.createRegistryDeviceAuthorization; } });
32
38
  Object.defineProperty(exports, "approveRegistryDeviceAuthorization", { enumerable: true, get: function () { return device_authorizations_1.approveRegistryDeviceAuthorization; } });
@@ -66,7 +72,9 @@ Object.defineProperty(exports, "REGISTRY_READ_SCOPE", { enumerable: true, get: f
66
72
  Object.defineProperty(exports, "REGISTRY_WRITE_SCOPE", { enumerable: true, get: function () { return types_1.REGISTRY_WRITE_SCOPE; } });
67
73
  Object.defineProperty(exports, "scopesForRegistryRole", { enumerable: true, get: function () { return types_1.scopesForRegistryRole; } });
68
74
  Object.defineProperty(exports, "registryPrincipalHasScope", { enumerable: true, get: function () { return types_1.registryPrincipalHasScope; } });
75
+ Object.defineProperty(exports, "isRegistryTokenPrincipal", { enumerable: true, get: function () { return types_1.isRegistryTokenPrincipal; } });
69
76
  var validation_1 = require("./validation");
77
+ Object.defineProperty(exports, "isTransmittableCredential", { enumerable: true, get: function () { return validation_1.isTransmittableCredential; } });
70
78
  Object.defineProperty(exports, "validateRegistryDisplayName", { enumerable: true, get: function () { return validation_1.validateRegistryDisplayName; } });
71
79
  Object.defineProperty(exports, "validateRegistryEmail", { enumerable: true, get: function () { return validation_1.validateRegistryEmail; } });
72
80
  Object.defineProperty(exports, "validateRegistryImageUrl", { enumerable: true, get: function () { return validation_1.validateRegistryImageUrl; } });
@@ -0,0 +1,27 @@
1
+ import { type RegistrySyncSecretPrincipal } from './types';
2
+ /**
3
+ * Deployment-wide sync secret.
4
+ *
5
+ * The documented API describes a raw secret carried as a bearer credential for CI-style sync, which is
6
+ * what this module resolves. It is the credential itself, so presenting it issues no token and creates
7
+ * no session: no owner, no expiry, no usage trail, and unsetting the variable is the only way to
8
+ * withdraw it. User-issued `hnd_*` tokens stay the canonical path, and this is off whenever the
9
+ * variable is absent.
10
+ */
11
+ /** Env var holding the raw secret. Read per call so unsetting it takes effect on restart alone. */
12
+ export declare const SYNC_SECRET_ENV = "HANDOFF_SYNC_SECRET";
13
+ /**
14
+ * The configured secret, or `''` when this method is unavailable. Never returns a value that could not
15
+ * be presented in every accepted header, or one using the access-token prefix: credentials are routed
16
+ * by shape, so such a secret would be permanently unreachable. Both cases warn instead.
17
+ */
18
+ export declare const resolveRegistrySyncSecret: (env?: NodeJS.ProcessEnv) => string;
19
+ /**
20
+ * Resolve a raw credential into a sync-secret principal, granting registry read and write.
21
+ *
22
+ * The empty checks have to come first: `hashSecret('')` is a valid digest that matches itself, so
23
+ * hashing up front would authenticate an empty credential on any deployment that leaves
24
+ * {@link SYNC_SECRET_ENV} unset. The comparison runs over fixed-length digests, so it is constant time
25
+ * and leaks no length.
26
+ */
27
+ export declare const authenticateRegistrySyncSecret: (credential: string, env?: NodeJS.ProcessEnv) => RegistrySyncSecretPrincipal | null;
@@ -0,0 +1,65 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.authenticateRegistrySyncSecret = exports.resolveRegistrySyncSecret = exports.SYNC_SECRET_ENV = void 0;
4
+ const crypto_1 = require("./crypto");
5
+ const types_1 = require("./types");
6
+ const validation_1 = require("./validation");
7
+ /**
8
+ * Deployment-wide sync secret.
9
+ *
10
+ * The documented API describes a raw secret carried as a bearer credential for CI-style sync, which is
11
+ * what this module resolves. It is the credential itself, so presenting it issues no token and creates
12
+ * no session: no owner, no expiry, no usage trail, and unsetting the variable is the only way to
13
+ * withdraw it. User-issued `hnd_*` tokens stay the canonical path, and this is off whenever the
14
+ * variable is absent.
15
+ */
16
+ /** Env var holding the raw secret. Read per call so unsetting it takes effect on restart alone. */
17
+ exports.SYNC_SECRET_ENV = 'HANDOFF_SYNC_SECRET';
18
+ /** Prefix reserved for issued access tokens; see `formatRegistryAccessToken`. */
19
+ const RESERVED_TOKEN_PREFIX = 'hnd_';
20
+ /** Reasons already reported, so a misconfigured deployment warns once rather than once per request. */
21
+ const warned = new Set();
22
+ const warnOnce = (reason, detail) => {
23
+ if (warned.has(reason))
24
+ return;
25
+ warned.add(reason);
26
+ console.warn(`${exports.SYNC_SECRET_ENV} is ignored: ${detail}`);
27
+ };
28
+ /**
29
+ * The configured secret, or `''` when this method is unavailable. Never returns a value that could not
30
+ * be presented in every accepted header, or one using the access-token prefix: credentials are routed
31
+ * by shape, so such a secret would be permanently unreachable. Both cases warn instead.
32
+ */
33
+ const resolveRegistrySyncSecret = (env = process.env) => {
34
+ var _a, _b;
35
+ const configured = (_b = (_a = env[exports.SYNC_SECRET_ENV]) === null || _a === void 0 ? void 0 : _a.trim()) !== null && _b !== void 0 ? _b : '';
36
+ if (!configured)
37
+ return '';
38
+ if (!(0, validation_1.isTransmittableCredential)(configured)) {
39
+ warnOnce('untransmittable', 'the value must be printable ASCII with no spaces or commas to be sent in a request header.');
40
+ return '';
41
+ }
42
+ if (configured.startsWith(RESERVED_TOKEN_PREFIX)) {
43
+ warnOnce('token_shaped', `the value starts with the \`${RESERVED_TOKEN_PREFIX}\` prefix reserved for issued access tokens.`);
44
+ return '';
45
+ }
46
+ return configured;
47
+ };
48
+ exports.resolveRegistrySyncSecret = resolveRegistrySyncSecret;
49
+ /**
50
+ * Resolve a raw credential into a sync-secret principal, granting registry read and write.
51
+ *
52
+ * The empty checks have to come first: `hashSecret('')` is a valid digest that matches itself, so
53
+ * hashing up front would authenticate an empty credential on any deployment that leaves
54
+ * {@link SYNC_SECRET_ENV} unset. The comparison runs over fixed-length digests, so it is constant time
55
+ * and leaks no length.
56
+ */
57
+ const authenticateRegistrySyncSecret = (credential, env = process.env) => {
58
+ const configured = (0, exports.resolveRegistrySyncSecret)(env);
59
+ if (!configured || !credential)
60
+ return null;
61
+ if (!(0, crypto_1.secretHashMatches)(credential, (0, crypto_1.hashSecret)(configured)))
62
+ return null;
63
+ return { kind: 'sync_secret', scopes: [types_1.REGISTRY_READ_SCOPE, types_1.REGISTRY_WRITE_SCOPE] };
64
+ };
65
+ exports.authenticateRegistrySyncSecret = authenticateRegistrySyncSecret;
@@ -14,7 +14,9 @@ export interface RegistryUser {
14
14
  createdAt: Date;
15
15
  updatedAt: Date;
16
16
  }
17
- export interface RegistryPrincipal {
17
+ /** A request authenticated by a user-issued, revocable `hnd_*` access token. */
18
+ export interface RegistryTokenPrincipal {
19
+ kind: 'access_token';
18
20
  tokenId: string;
19
21
  userId: string;
20
22
  email: string;
@@ -22,5 +24,22 @@ export interface RegistryPrincipal {
22
24
  role: RegistryUserRole;
23
25
  scopes: RegistryAccessScope[];
24
26
  }
27
+ /**
28
+ * A request authenticated by the deployment-wide `HANDOFF_SYNC_SECRET`. The secret is the credential
29
+ * itself, so there is no token row and no owner, only the scopes it grants. Nothing derived from the
30
+ * secret lives here — no hash, no label: response bodies are serialized verbatim, so any field added
31
+ * to this principal reaches a client as written.
32
+ */
33
+ export interface RegistrySyncSecretPrincipal {
34
+ kind: 'sync_secret';
35
+ scopes: RegistryAccessScope[];
36
+ }
37
+ /**
38
+ * Whoever a registry request is acting as. Discriminated on `kind` because identity fields exist only
39
+ * for token principals, and both tsconfigs run without `strictNullChecks`: one interface with nullable
40
+ * fields would let `principal.userId` compile against a secret-authenticated request.
41
+ */
42
+ export type RegistryPrincipal = RegistryTokenPrincipal | RegistrySyncSecretPrincipal;
43
+ export declare const isRegistryTokenPrincipal: (principal: RegistryPrincipal) => principal is RegistryTokenPrincipal;
25
44
  export declare const scopesForRegistryRole: (role: RegistryUserRole) => RegistryAccessScope[];
26
45
  export declare const registryPrincipalHasScope: (principal: RegistryPrincipal, scope: RegistryAccessScope) => boolean;
@@ -1,8 +1,10 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.registryPrincipalHasScope = exports.scopesForRegistryRole = exports.REGISTRY_WRITE_SCOPE = exports.REGISTRY_READ_SCOPE = void 0;
3
+ exports.registryPrincipalHasScope = exports.scopesForRegistryRole = exports.isRegistryTokenPrincipal = exports.REGISTRY_WRITE_SCOPE = exports.REGISTRY_READ_SCOPE = void 0;
4
4
  exports.REGISTRY_READ_SCOPE = 'registry:read';
5
5
  exports.REGISTRY_WRITE_SCOPE = 'registry:write';
6
+ const isRegistryTokenPrincipal = (principal) => principal.kind === 'access_token';
7
+ exports.isRegistryTokenPrincipal = isRegistryTokenPrincipal;
6
8
  const scopesForRegistryRole = (role) => role === 'admin' ? [exports.REGISTRY_READ_SCOPE, exports.REGISTRY_WRITE_SCOPE] : [exports.REGISTRY_READ_SCOPE];
7
9
  exports.scopesForRegistryRole = scopesForRegistryRole;
8
10
  const registryPrincipalHasScope = (principal, scope) => principal.scopes.includes(scope);
@@ -1,3 +1,14 @@
1
+ /**
2
+ * Whether a credential can be presented unambiguously in a request header. One rule shared by the
3
+ * authentication headers and by whatever a deployment configures as a secret, so a value is never
4
+ * accepted through one carrier and rejected by the other.
5
+ *
6
+ * Whitespace is out so nothing can be trimmed away or smuggled past a parser, and the comma with it:
7
+ * repeats of a header Node does not de-duplicate arrive joined with `, `, and no accepted credential
8
+ * may look like that. Non-ASCII bytes are out too, since their header encoding is ambiguous and a
9
+ * credential has to reproduce exactly.
10
+ */
11
+ export declare const isTransmittableCredential: (value: string) => boolean;
1
12
  export declare const validateRegistryEmail: (email: string) => string | null;
2
13
  export declare const validateRegistryDisplayName: (name: string) => string | null;
3
14
  export declare const validateRegistryImageUrl: (image: string | null | undefined) => string | null | false;
@@ -1,8 +1,25 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.validateRegistryImageUrl = exports.validateRegistryDisplayName = exports.validateRegistryEmail = void 0;
3
+ exports.validateRegistryImageUrl = exports.validateRegistryDisplayName = exports.validateRegistryEmail = exports.isTransmittableCredential = void 0;
4
4
  const crypto_1 = require("./crypto");
5
5
  const SIMPLE_EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
6
+ /**
7
+ * Printable ASCII (`!` through `~`) except the comma: HTTP's `VCHAR`, minus the character Node joins
8
+ * duplicate header values with.
9
+ */
10
+ const TRANSMITTABLE_CREDENTIAL_PATTERN = /^[\x21-\x2B\x2D-\x7E]+$/;
11
+ /**
12
+ * Whether a credential can be presented unambiguously in a request header. One rule shared by the
13
+ * authentication headers and by whatever a deployment configures as a secret, so a value is never
14
+ * accepted through one carrier and rejected by the other.
15
+ *
16
+ * Whitespace is out so nothing can be trimmed away or smuggled past a parser, and the comma with it:
17
+ * repeats of a header Node does not de-duplicate arrive joined with `, `, and no accepted credential
18
+ * may look like that. Non-ASCII bytes are out too, since their header encoding is ambiguous and a
19
+ * credential has to reproduce exactly.
20
+ */
21
+ const isTransmittableCredential = (value) => TRANSMITTABLE_CREDENTIAL_PATTERN.test(value);
22
+ exports.isTransmittableCredential = isTransmittableCredential;
6
23
  const validateRegistryEmail = (email) => {
7
24
  const normalized = (0, crypto_1.normalizeEmail)(email);
8
25
  return normalized.length <= 320 && SIMPLE_EMAIL_PATTERN.test(normalized) ? normalized : null;
@@ -13,4 +13,4 @@ import Handoff from '../../index';
13
13
  * Checkout all published asset collections, or a single collection when `collection` is given.
14
14
  * Recreates workspace asset files; prompts before overwriting changed local files unless `--force`.
15
15
  */
16
- export declare const checkoutAssets: (handoff: Handoff, collection?: string) => Promise<void>;
16
+ export declare const checkoutAssets: (handoff: Handoff, selection?: string[]) => Promise<void>;
@@ -84,15 +84,19 @@ const wouldChange = (absolutePath, bytes) => {
84
84
  * Checkout all published asset collections, or a single collection when `collection` is given.
85
85
  * Recreates workspace asset files; prompts before overwriting changed local files unless `--force`.
86
86
  */
87
- const checkoutAssets = (handoff, collection) => __awaiter(void 0, void 0, void 0, function* () {
87
+ const checkoutAssets = (handoff, selection) => __awaiter(void 0, void 0, void 0, function* () {
88
+ var _a;
88
89
  const connection = yield (0, index_1.resolveConnectionOrThrow)(handoff);
89
- if (collection && !(0, sets_1.isAssetCollection)(collection)) {
90
- throw new index_1.CheckoutError(`Unknown asset collection "${collection}". Supported collections: ${sets_1.ASSET_COLLECTIONS.join(', ')}.`);
90
+ const unsupported = (_a = selection === null || selection === void 0 ? void 0 : selection.filter((name) => !(0, sets_1.isAssetCollection)(name))) !== null && _a !== void 0 ? _a : [];
91
+ if (unsupported.length > 0) {
92
+ throw new index_1.CheckoutError(`Unknown asset collection ${unsupported.map((name) => `"${name}"`).join(', ')}. ` +
93
+ `Supported collections: ${sets_1.ASSET_COLLECTIONS.join(', ')}.`);
91
94
  }
92
95
  const client = (0, client_1.createRegistryClient)({ baseUrl: connection.url, accessToken: connection.accessToken });
96
+ // A named selection is trusted as-is; only a full checkout has to ask the registry what exists.
93
97
  let collections;
94
- if (collection) {
95
- collections = [collection];
98
+ if (selection) {
99
+ collections = selection;
96
100
  }
97
101
  else {
98
102
  const summaries = yield client.listAssetCollections();
@@ -123,16 +127,27 @@ const checkoutAssets = (handoff, collection) => __awaiter(void 0, void 0, void 0
123
127
  // Download each referenced blob once (dedup by hash), then resolve every asset to a physical write.
124
128
  const blobCache = new Map();
125
129
  const writes = [];
130
+ const plannedPaths = [];
126
131
  for (const payload of payloads) {
127
132
  for (const asset of payload.assets) {
133
+ const absolutePath = (0, layout_1.resolveAssetPhysicalPath)(asset.path, roots);
134
+ plannedPaths.push(absolutePath);
135
+ // A dry run reports paths only, so there is no reason to pull the bytes down.
136
+ if (handoff.dryRun) {
137
+ continue;
138
+ }
128
139
  let bytes = blobCache.get(asset.contentHash);
129
140
  if (!bytes) {
130
141
  bytes = yield client.downloadAssetBlob(asset.contentHash);
131
142
  blobCache.set(asset.contentHash, bytes);
132
143
  }
133
- writes.push({ absolutePath: (0, layout_1.resolveAssetPhysicalPath)(asset.path, roots), bytes });
144
+ writes.push({ absolutePath, bytes });
134
145
  }
135
146
  }
147
+ if (handoff.dryRun) {
148
+ (0, index_1.reportPlannedWrites)(handoff, 'asset collections', plannedPaths, plannedPaths.filter((target) => fs_extra_1.default.existsSync(target)));
149
+ return;
150
+ }
136
151
  const existingChanges = writes.filter((write) => fs_extra_1.default.existsSync(write.absolutePath) && wouldChange(write.absolutePath, write.bytes));
137
152
  if (existingChanges.length > 0 && !handoff.force) {
138
153
  const proceed = yield p.confirm({
@@ -24,6 +24,13 @@ export declare class CheckoutError extends Error {
24
24
  * actionable {@link CheckoutError} naming the exact misconfiguration.
25
25
  */
26
26
  export declare const resolveConnectionOrThrow: (handoff: Handoff) => Promise<import("../connection").ResolvedRegistryConnection>;
27
+ /**
28
+ * Report the files a checkout would create or replace, and write nothing. Called once the real
29
+ * checkout has computed its targets but before it prompts or writes, so a dry run exercises the same
30
+ * path resolution and conflict detection as the real thing. Shared by the entity, page, token and
31
+ * asset checkout paths.
32
+ */
33
+ export declare const reportPlannedWrites: (handoff: Handoff, label: string, targets: string[], conflicts: string[]) => void;
27
34
  /**
28
35
  * Checkout a single component, pattern, or page from the connected registry into this workspace.
29
36
  * Overwriting existing local files requires `--force` or an interactive confirmation.
@@ -35,4 +42,4 @@ export declare const checkoutEntity: (handoff: Handoff, kind: TransferEntityKind
35
42
  * is collected and never aborts the rest; the run throws at the end if any entity failed. Overwrite
36
43
  * prompting is per entity (skipped under `--force`).
37
44
  */
38
- export declare const checkoutEntities: (handoff: Handoff, kind: TransferEntityKind) => Promise<void>;
45
+ export declare const checkoutEntities: (handoff: Handoff, kind: TransferEntityKind, ids?: string[]) => Promise<void>;
@@ -60,7 +60,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
60
60
  return (mod && mod.__esModule) ? mod : { "default": mod };
61
61
  };
62
62
  Object.defineProperty(exports, "__esModule", { value: true });
63
- exports.checkoutEntities = exports.checkoutEntity = exports.resolveConnectionOrThrow = exports.CheckoutError = void 0;
63
+ exports.checkoutEntities = exports.checkoutEntity = exports.reportPlannedWrites = exports.resolveConnectionOrThrow = exports.CheckoutError = void 0;
64
64
  const p = __importStar(require("@clack/prompts"));
65
65
  const fs_extra_1 = __importDefault(require("fs-extra"));
66
66
  const path_1 = __importDefault(require("path"));
@@ -70,6 +70,7 @@ const logger_1 = require("../../utils/logger");
70
70
  const client_1 = require("../client");
71
71
  const connection_1 = require("../connection");
72
72
  const path_2 = require("../path");
73
+ const selection_1 = require("../selection");
73
74
  /** A connected-workspace configuration or precondition failure surfaced to the CLI. */
74
75
  class CheckoutError extends Error {
75
76
  constructor(message) {
@@ -568,6 +569,21 @@ const writeSourceFile = (targetDir, file) => __awaiter(void 0, void 0, void 0, f
568
569
  yield fs_extra_1.default.writeFile(absolutePath, file.content, 'utf8');
569
570
  return absolutePath;
570
571
  });
572
+ /**
573
+ * Report the files a checkout would create or replace, and write nothing. Called once the real
574
+ * checkout has computed its targets but before it prompts or writes, so a dry run exercises the same
575
+ * path resolution and conflict detection as the real thing. Shared by the entity, page, token and
576
+ * asset checkout paths.
577
+ */
578
+ const reportPlannedWrites = (handoff, label, targets, conflicts) => {
579
+ const conflicting = new Set(conflicts);
580
+ logger_1.Logger.info(`Would checkout ${label} (${targets.length} file(s)):`);
581
+ for (const target of targets) {
582
+ const relative = path_1.default.relative(handoff.workingPath, target) || target;
583
+ logger_1.Logger.info(` ${conflicting.has(target) ? 'overwrite' : 'create'}: ${relative}`);
584
+ }
585
+ };
586
+ exports.reportPlannedWrites = reportPlannedWrites;
571
587
  /**
572
588
  * Checkout a page: write its single verbatim `.md` (transfer path `<id>.md`) back under
573
589
  * `<workingPath>/pages/`. Unlike components/patterns there is no declaration to synthesize — the
@@ -583,6 +599,10 @@ const checkoutPage = (handoff, id, payload) => __awaiter(void 0, void 0, void 0,
583
599
  return target;
584
600
  });
585
601
  const conflicts = targets.filter((file) => fs_extra_1.default.existsSync(file));
602
+ if (handoff.dryRun) {
603
+ (0, exports.reportPlannedWrites)(handoff, `page "${id}"`, targets, conflicts);
604
+ return;
605
+ }
586
606
  if (!(yield confirmOverwrite(handoff, conflicts))) {
587
607
  logger_1.Logger.warn(`Checkout of page "${id}" cancelled; no files were written.`);
588
608
  return;
@@ -633,7 +653,13 @@ const checkoutSingle = (handoff, kind, id, client, registryUrl) => __awaiter(voi
633
653
  }
634
654
  return { file, absolutePath };
635
655
  });
636
- const conflicts = [...sourceTargets.map((entry) => entry.absolutePath), declarationPath].filter((file) => fs_extra_1.default.existsSync(file));
656
+ const plannedWrites = [...sourceTargets.map((entry) => entry.absolutePath), declarationPath];
657
+ const conflicts = plannedWrites.filter((file) => fs_extra_1.default.existsSync(file));
658
+ if (handoff.dryRun) {
659
+ // Returning null also skips `registerCheckedOut`, so a dry run never edits handoff.config either.
660
+ (0, exports.reportPlannedWrites)(handoff, `${kind} "${id}"`, plannedWrites, conflicts);
661
+ return null;
662
+ }
637
663
  if (!(yield confirmOverwrite(handoff, conflicts))) {
638
664
  logger_1.Logger.warn(`Checkout of ${kind} "${id}" cancelled; no files were written.`);
639
665
  return null;
@@ -694,7 +720,7 @@ exports.checkoutEntity = checkoutEntity;
694
720
  * is collected and never aborts the rest; the run throws at the end if any entity failed. Overwrite
695
721
  * prompting is per entity (skipped under `--force`).
696
722
  */
697
- const checkoutEntities = (handoff, kind) => __awaiter(void 0, void 0, void 0, function* () {
723
+ const checkoutEntities = (handoff, kind, ids) => __awaiter(void 0, void 0, void 0, function* () {
698
724
  const connection = yield (0, exports.resolveConnectionOrThrow)(handoff);
699
725
  const client = (0, client_1.createRegistryClient)({ baseUrl: connection.url, accessToken: connection.accessToken });
700
726
  let summaries;
@@ -707,14 +733,20 @@ const checkoutEntities = (handoff, kind) => __awaiter(void 0, void 0, void 0, fu
707
733
  }
708
734
  throw error;
709
735
  }
710
- if (summaries.length === 0) {
736
+ const published = summaries.map((summary) => summary.id);
737
+ const { selected: targets, unknown } = (0, selection_1.selectIds)(published, ids);
738
+ if (unknown.length > 0) {
739
+ throw new CheckoutError(`No ${kind} named ${unknown.map((id) => `"${id}"`).join(', ')} is published in the registry. ` +
740
+ `Published ${kind}s: ${published.join(', ') || '(none)'}.`);
741
+ }
742
+ if (targets.length === 0) {
711
743
  logger_1.Logger.success(`No ${kind}s are published in the registry; nothing to checkout.`);
712
744
  return;
713
745
  }
714
746
  let checkedOut = 0;
715
747
  const failed = [];
716
748
  const targetDirs = [];
717
- for (const { id } of summaries) {
749
+ for (const id of targets) {
718
750
  try {
719
751
  const targetDir = yield checkoutSingle(handoff, kind, id, client, connection.url);
720
752
  if (targetDir) {
@@ -728,7 +760,8 @@ const checkoutEntities = (handoff, kind) => __awaiter(void 0, void 0, void 0, fu
728
760
  }
729
761
  // Register everything checked out in one pass, so the config is edited (and confirmed) once.
730
762
  yield registerCheckedOut(handoff, kind, targetDirs);
731
- logger_1.Logger.success(`${kind[0].toUpperCase()}${kind.slice(1)}s checkout complete — ${checkedOut} checked out${failed.length ? `, ${failed.length} failed` : ''}.`);
763
+ logger_1.Logger.success(`${kind[0].toUpperCase()}${kind.slice(1)}s checkout complete — ${checkedOut} ${handoff.dryRun ? 'would be checked out' : 'checked out'}` +
764
+ `${failed.length ? `, ${failed.length} failed` : ''}.`);
732
765
  if (failed.length > 0) {
733
766
  for (const failure of failed) {
734
767
  logger_1.Logger.error(` - ${failure.id}: ${failure.message}`);
@@ -12,4 +12,4 @@ import Handoff from '../../index';
12
12
  * Checkout all published token sets, or a single set when `setId` is given. Reconstructs `tokens.json`
13
13
  * and restores generated files; prompts before overwriting changed local files unless `--force`.
14
14
  */
15
- export declare const checkoutTokens: (handoff: Handoff, setId?: string) => Promise<void>;
15
+ export declare const checkoutTokens: (handoff: Handoff, selection?: string[]) => Promise<void>;
@@ -97,15 +97,18 @@ const wouldChange = (absolutePath, content) => {
97
97
  * Checkout all published token sets, or a single set when `setId` is given. Reconstructs `tokens.json`
98
98
  * and restores generated files; prompts before overwriting changed local files unless `--force`.
99
99
  */
100
- const checkoutTokens = (handoff, setId) => __awaiter(void 0, void 0, void 0, function* () {
101
- if (setId && !(0, sets_1.isTokenSetId)(setId)) {
102
- throw new index_1.CheckoutError(`Unknown or unsafe token set id "${setId}".`);
100
+ const checkoutTokens = (handoff, selection) => __awaiter(void 0, void 0, void 0, function* () {
101
+ var _a;
102
+ const unsupported = (_a = selection === null || selection === void 0 ? void 0 : selection.filter((id) => !(0, sets_1.isTokenSetId)(id))) !== null && _a !== void 0 ? _a : [];
103
+ if (unsupported.length > 0) {
104
+ throw new index_1.CheckoutError(`Unknown or unsafe token set id ${unsupported.map((id) => `"${id}"`).join(', ')}.`);
103
105
  }
104
106
  const connection = yield (0, index_1.resolveConnectionOrThrow)(handoff);
105
107
  const client = (0, client_1.createRegistryClient)({ baseUrl: connection.url, accessToken: connection.accessToken });
108
+ // A named selection is trusted as-is; only a full checkout has to ask the registry what exists.
106
109
  let ids;
107
- if (setId) {
108
- ids = [setId];
110
+ if (selection) {
111
+ ids = selection;
109
112
  }
110
113
  else {
111
114
  const summaries = yield client.listTokenSets();
@@ -153,6 +156,10 @@ const checkoutTokens = (handoff, setId) => __awaiter(void 0, void 0, void 0, fun
153
156
  }
154
157
  }
155
158
  const existingChanges = changed.filter((filePath) => fs_extra_1.default.existsSync(filePath));
159
+ if (handoff.dryRun) {
160
+ (0, index_1.reportPlannedWrites)(handoff, 'token sets', changed, existingChanges);
161
+ return;
162
+ }
156
163
  if (existingChanges.length > 0 && !handoff.force) {
157
164
  const proceed = yield p.confirm({
158
165
  message: `Checkout will overwrite ${existingChanges.length} changed local token file(s). Continue?`,
@@ -6,6 +6,9 @@
6
6
  * config + environment: the registry URL may be given inline or via an env-var *name*, and the
7
7
  * access token is always read from an env-var name (never persisted as a value). This is the single
8
8
  * source the publish (and later checkout) client reads its target + credentials from.
9
+ *
10
+ * The older `HANDOFF_CLOUD_*` names still work as deprecated fallbacks; see
11
+ * {@link DEPRECATED_ENV_ALIASES}.
9
12
  */
10
13
  import type { Config } from '../types/config';
11
14
  /** Default env-var name holding the remote registry base URL. */
@@ -26,6 +29,9 @@ export interface ResolvedRegistryConnection {
26
29
  /**
27
30
  * Resolve the connected-workspace registry connection from config + environment. Never throws —
28
31
  * Callers inspect the resolved URL and access token to surface specific configuration errors.
32
+ *
33
+ * `urlEnv` and `accessTokenEnv` always report the canonical name, even when an alias supplied the
34
+ * value, so errors built from them point at the variable to adopt.
29
35
  */
30
36
  export declare const resolveRegistryConnection: (config: Config | null | undefined) => ResolvedRegistryConnection;
31
37
  /**
@@ -35,5 +41,9 @@ export declare const resolveRegistryConnection: (config: Config | null | undefin
35
41
  * keep precedence for deterministic CI: `HANDOFF_REGISTRY_URL` (or inline config) wins over the
36
42
  * saved URL, and `HANDOFF_REGISTRY_ACCESS_TOKEN` wins over the saved token. The saved token only
37
43
  * applies to the URL it was issued for.
44
+ *
45
+ * A configured `HANDOFF_SYNC_SECRET` is the last resort. Being deployment-wide rather than personal,
46
+ * it fills in only where there is no explicit token and no login (CI, in practice), instead of quietly
47
+ * speaking for a developer who is signed in.
38
48
  */
39
49
  export declare const resolveAuthenticatedRegistryConnection: (config: Config | null | undefined, workingPath?: string) => Promise<ResolvedRegistryConnection>;
@@ -7,6 +7,9 @@
7
7
  * config + environment: the registry URL may be given inline or via an env-var *name*, and the
8
8
  * access token is always read from an env-var name (never persisted as a value). This is the single
9
9
  * source the publish (and later checkout) client reads its target + credentials from.
10
+ *
11
+ * The older `HANDOFF_CLOUD_*` names still work as deprecated fallbacks; see
12
+ * {@link DEPRECATED_ENV_ALIASES}.
10
13
  */
11
14
  var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
12
15
  function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
@@ -20,22 +23,60 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
20
23
  Object.defineProperty(exports, "__esModule", { value: true });
21
24
  exports.resolveAuthenticatedRegistryConnection = exports.resolveRegistryConnection = exports.DEFAULT_REGISTRY_ACCESS_TOKEN_ENV = exports.DEFAULT_REGISTRY_URL_ENV = void 0;
22
25
  const store_1 = require("../cli/auth/store");
26
+ // Imported straight from the module, not the `auth` barrel, to keep the database schema off the CLI
27
+ // startup path; this file is resolved for every command.
28
+ const sync_secret_1 = require("./auth/sync-secret");
29
+ const logger_1 = require("../utils/logger");
23
30
  /** Default env-var name holding the remote registry base URL. */
24
31
  exports.DEFAULT_REGISTRY_URL_ENV = 'HANDOFF_REGISTRY_URL';
25
32
  /** Default env-var name holding the connected-workspace access token. */
26
33
  exports.DEFAULT_REGISTRY_ACCESS_TOKEN_ENV = 'HANDOFF_REGISTRY_ACCESS_TOKEN';
34
+ /**
35
+ * Old env-var names we still read, keyed by the name that replaced them. Keeps workspaces set up
36
+ * from the older docs working instead of silently reporting no registry.
37
+ */
38
+ const DEPRECATED_ENV_ALIASES = {
39
+ [exports.DEFAULT_REGISTRY_URL_ENV]: 'HANDOFF_CLOUD_URL',
40
+ [exports.DEFAULT_REGISTRY_ACCESS_TOKEN_ENV]: 'HANDOFF_CLOUD_TOKEN',
41
+ };
42
+ /** Aliases we've already warned about, so each one warns once per run and not once per lookup. */
43
+ const warnedAliases = new Set();
44
+ /**
45
+ * Value of the named env var, falling back to its deprecated alias. Only the default names have
46
+ * aliases: a custom `urlEnv`/`accessTokenEnv` is a deliberate choice, so a stray `HANDOFF_CLOUD_*`
47
+ * must not stand in for it. The warning goes through {@link Logger.warn}, so
48
+ * `HANDOFF_LOG_LEVEL=error` silences it in CI.
49
+ */
50
+ const readConnectionEnv = (name) => {
51
+ var _a, _b;
52
+ const configured = (_a = process.env[name]) === null || _a === void 0 ? void 0 : _a.trim();
53
+ if (configured)
54
+ return configured;
55
+ const alias = DEPRECATED_ENV_ALIASES[name];
56
+ const inherited = alias ? (_b = process.env[alias]) === null || _b === void 0 ? void 0 : _b.trim() : '';
57
+ if (!inherited)
58
+ return '';
59
+ if (!warnedAliases.has(alias)) {
60
+ warnedAliases.add(alias);
61
+ logger_1.Logger.warn(`${alias} is deprecated and will be removed in a future release; set ${name} instead.`);
62
+ }
63
+ return inherited;
64
+ };
27
65
  /**
28
66
  * Resolve the connected-workspace registry connection from config + environment. Never throws —
29
67
  * Callers inspect the resolved URL and access token to surface specific configuration errors.
68
+ *
69
+ * `urlEnv` and `accessTokenEnv` always report the canonical name, even when an alias supplied the
70
+ * value, so errors built from them point at the variable to adopt.
30
71
  */
31
72
  const resolveRegistryConnection = (config) => {
32
- var _a, _b, _c, _d, _e, _f, _g, _h;
73
+ var _a, _b, _c, _d;
33
74
  const connection = (_a = config === null || config === void 0 ? void 0 : config.runtime) === null || _a === void 0 ? void 0 : _a.registryConnection;
34
75
  const urlEnv = ((_b = connection === null || connection === void 0 ? void 0 : connection.urlEnv) === null || _b === void 0 ? void 0 : _b.trim()) || exports.DEFAULT_REGISTRY_URL_ENV;
35
76
  const accessTokenEnv = ((_c = connection === null || connection === void 0 ? void 0 : connection.accessTokenEnv) === null || _c === void 0 ? void 0 : _c.trim()) || exports.DEFAULT_REGISTRY_ACCESS_TOKEN_ENV;
36
77
  const inlineUrl = (_d = connection === null || connection === void 0 ? void 0 : connection.url) === null || _d === void 0 ? void 0 : _d.trim();
37
- const url = (_f = (inlineUrl || ((_e = process.env[urlEnv]) === null || _e === void 0 ? void 0 : _e.trim()))) !== null && _f !== void 0 ? _f : '';
38
- const accessToken = (_h = (_g = process.env[accessTokenEnv]) === null || _g === void 0 ? void 0 : _g.trim()) !== null && _h !== void 0 ? _h : '';
78
+ const url = inlineUrl || readConnectionEnv(urlEnv);
79
+ const accessToken = readConnectionEnv(accessTokenEnv);
39
80
  return {
40
81
  url,
41
82
  accessToken,
@@ -51,14 +92,18 @@ exports.resolveRegistryConnection = resolveRegistryConnection;
51
92
  * keep precedence for deterministic CI: `HANDOFF_REGISTRY_URL` (or inline config) wins over the
52
93
  * saved URL, and `HANDOFF_REGISTRY_ACCESS_TOKEN` wins over the saved token. The saved token only
53
94
  * applies to the URL it was issued for.
95
+ *
96
+ * A configured `HANDOFF_SYNC_SECRET` is the last resort. Being deployment-wide rather than personal,
97
+ * it fills in only where there is no explicit token and no login (CI, in practice), instead of quietly
98
+ * speaking for a developer who is signed in.
54
99
  */
55
100
  const resolveAuthenticatedRegistryConnection = (config_1, ...args_1) => __awaiter(void 0, [config_1, ...args_1], void 0, function* (config, workingPath = process.cwd()) {
56
101
  const connection = (0, exports.resolveRegistryConnection)(config);
57
102
  const auth = yield (0, store_1.readCliAuth)(workingPath);
58
- if (!(0, store_1.cliAuthTokenIsValid)(auth))
59
- return connection;
60
- const url = connection.url || auth.remoteUrl;
61
- const accessToken = connection.accessToken || ((0, store_1.cliAuthMatchesRegistry)(auth, url) ? auth.accessToken : '');
103
+ const login = (0, store_1.cliAuthTokenIsValid)(auth) ? auth : null;
104
+ const url = connection.url || (login === null || login === void 0 ? void 0 : login.remoteUrl) || '';
105
+ const savedToken = login && (0, store_1.cliAuthMatchesRegistry)(login, url) ? login.accessToken : '';
106
+ const accessToken = connection.accessToken || savedToken || (0, sync_secret_1.resolveRegistrySyncSecret)();
62
107
  return Object.assign(Object.assign({}, connection), { url, accessToken });
63
108
  });
64
109
  exports.resolveAuthenticatedRegistryConnection = resolveAuthenticatedRegistryConnection;
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Logical content-kind vocabulary: the dependency-light boundary shared by the CLI, the
3
+ * publish/checkout orchestration, and the compatibility layer.
4
+ *
5
+ * A workspace publishes and checks out five kinds of content. Components, patterns and pages are
6
+ * entities with a singular wire kind ({@link TransferEntityKind}); tokens and assets travel through
7
+ * their own transfer endpoints. This module owns only the stable plural vocabulary and its ordering,
8
+ * so it stays free of fs, DB and React dependencies.
9
+ */
10
+ import type { TransferEntityKind } from './transfer';
11
+ /** Stable identity used to publish `pages/index.md` while serving it at the root route. */
12
+ export declare const HOME_PAGE_ID = "index";
13
+ export declare const HOME_PAGE_PATH = "/";
14
+ /** Plural content kinds accepted wherever a publish or checkout target is named. */
15
+ export declare const REGISTRY_ENTITY_KINDS: readonly ["components", "patterns", "pages", "tokens", "assets"];
16
+ export type RegistryEntityKind = (typeof REGISTRY_ENTITY_KINDS)[number];
17
+ /** Map the plural content kind to the singular wire kind used by entity publish/checkout. */
18
+ export declare const ENTITY_WIRE_KIND: Record<'components' | 'patterns' | 'pages', TransferEntityKind>;
19
+ /** The entity kinds, in the order a full run visits them: patterns compose components, pages reference both. */
20
+ export declare const ENTITY_KIND_ORDER: readonly ["components", "patterns", "pages"];
21
+ /**
22
+ * Every kind, in dependency order. Tokens and assets come first because rendered component
23
+ * artifacts reference the CSS variables and asset URLs they produce.
24
+ */
25
+ export declare const ALL_KIND_ORDER: readonly RegistryEntityKind[];
26
+ /** Whether a string names a supported content kind. */
27
+ export declare const isRegistryEntityKind: (value: string) => value is RegistryEntityKind;