handoff-app 2.0.0-2 → 2.0.0-3

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 (161) hide show
  1. package/README.md +140 -83
  2. package/dist/app-builder/build.js +21 -4
  3. package/dist/app-builder/client-config.js +9 -8
  4. package/dist/cli/auth/browser.d.ts +2 -0
  5. package/dist/cli/auth/browser.js +33 -0
  6. package/dist/cli/auth/device.d.ts +11 -0
  7. package/dist/cli/auth/device.js +155 -0
  8. package/dist/cli/auth/store.d.ts +29 -0
  9. package/dist/cli/auth/store.js +146 -0
  10. package/dist/cli/scaffold.js +20 -154
  11. package/dist/commands/index.js +4 -0
  12. package/dist/commands/login/index.d.ts +8 -0
  13. package/dist/commands/login/index.js +53 -0
  14. package/dist/commands/logout/index.d.ts +7 -0
  15. package/dist/commands/logout/index.js +54 -0
  16. package/dist/config/entries.d.ts +47 -0
  17. package/dist/config/entries.js +162 -0
  18. package/dist/config/index.d.ts +2 -0
  19. package/dist/config/index.js +5 -1
  20. package/dist/config/runtime.d.ts +9 -9
  21. package/dist/config/runtime.js +15 -15
  22. package/dist/registry/auth/access-tokens.d.ts +46 -0
  23. package/dist/registry/auth/access-tokens.js +165 -0
  24. package/dist/registry/auth/action-tokens.d.ts +66 -0
  25. package/dist/registry/auth/action-tokens.js +178 -0
  26. package/dist/registry/auth/crypto.d.ts +4 -0
  27. package/dist/registry/auth/crypto.js +16 -0
  28. package/dist/registry/auth/database.d.ts +7 -0
  29. package/dist/registry/auth/database.js +22 -0
  30. package/dist/registry/auth/device-authorizations.d.ts +51 -0
  31. package/dist/registry/auth/device-authorizations.js +170 -0
  32. package/dist/registry/auth/gravatar.d.ts +1 -0
  33. package/dist/registry/auth/gravatar.js +8 -0
  34. package/dist/registry/auth/index.d.ts +11 -0
  35. package/dist/registry/auth/index.js +72 -0
  36. package/dist/registry/auth/installation.d.ts +42 -0
  37. package/dist/registry/auth/installation.js +105 -0
  38. package/dist/registry/auth/password.d.ts +12 -0
  39. package/dist/registry/auth/password.js +58 -0
  40. package/dist/registry/auth/rate-limits.d.ts +22 -0
  41. package/dist/registry/auth/rate-limits.js +75 -0
  42. package/dist/registry/auth/types.d.ts +26 -0
  43. package/dist/registry/auth/types.js +9 -0
  44. package/dist/registry/auth/user-record.d.ts +13 -0
  45. package/dist/registry/auth/user-record.js +16 -0
  46. package/dist/registry/auth/users.d.ts +31 -0
  47. package/dist/registry/auth/users.js +135 -0
  48. package/dist/registry/auth/validation.d.ts +3 -0
  49. package/dist/registry/auth/validation.js +28 -0
  50. package/dist/registry/checkout/assets.js +4 -2
  51. package/dist/registry/checkout/index.d.ts +1 -1
  52. package/dist/registry/checkout/index.js +101 -23
  53. package/dist/registry/checkout/tokens.js +4 -2
  54. package/dist/registry/connection.d.ts +9 -0
  55. package/dist/registry/connection.js +29 -1
  56. package/dist/registry/db/driver.d.ts +5 -4
  57. package/dist/registry/db/driver.js +5 -4
  58. package/dist/registry/db/index.d.ts +1 -1
  59. package/dist/registry/db/index.js +7 -1
  60. package/dist/registry/db/schema.d.ts +2521 -750
  61. package/dist/registry/db/schema.js +126 -1
  62. package/dist/registry/publish/assets.js +1 -1
  63. package/dist/registry/publish/errors.js +3 -3
  64. package/dist/registry/publish/index.d.ts +1 -1
  65. package/dist/registry/publish/index.js +9 -9
  66. package/dist/registry/publish/tokens.js +1 -1
  67. package/dist/types/config.d.ts +4 -1
  68. package/docs/cli.md +4 -0
  69. package/drizzle/0005_add_registry_auth.sql +95 -0
  70. package/drizzle/meta/0005_snapshot.json +1926 -0
  71. package/drizzle/meta/_journal.json +8 -1
  72. package/package.json +5 -4
  73. package/src/app/components/Auth/AccountLayout.tsx +87 -0
  74. package/src/app/components/Auth/AuthControls.tsx +99 -0
  75. package/src/app/components/Auth/AuthShell.tsx +73 -0
  76. package/src/app/components/Auth/RegistryInstallGate.tsx +23 -0
  77. package/src/app/components/Auth/RegistrySessionProvider.tsx +15 -0
  78. package/src/app/components/Auth/api.ts +20 -0
  79. package/src/app/components/Layout/Header.tsx +2 -0
  80. package/src/app/components/Navigation/MobileNav.tsx +2 -0
  81. package/src/app/lib/auth/api.ts +113 -0
  82. package/src/app/lib/auth/config.ts +175 -0
  83. package/src/app/lib/auth/email.ts +34 -0
  84. package/src/app/lib/docs-api/runtime-config.ts +0 -14
  85. package/src/app/lib/registry-api/auth.ts +29 -46
  86. package/src/app/lib/registry-api/errors.ts +2 -2
  87. package/src/app/lib/registry-api/handler.ts +19 -14
  88. package/src/app/lib/registry-api/token-transfer.ts +1 -1
  89. package/src/app/lib/registry-api/transfer.ts +4 -4
  90. package/src/app/next.config.mjs +12 -3
  91. package/src/app/pages/_app.tsx +10 -4
  92. package/src/app/pages/account/index.tsx +153 -0
  93. package/src/app/pages/account/tokens.tsx +230 -0
  94. package/src/app/pages/account/users.tsx +310 -0
  95. package/src/app/pages/api/account/index.ts +26 -0
  96. package/src/app/pages/api/account/tokens/[id].ts +17 -0
  97. package/src/app/pages/api/account/tokens/index.ts +44 -0
  98. package/src/app/pages/api/admin/users/[id]/[action].ts +89 -0
  99. package/src/app/pages/api/admin/users/index.ts +53 -0
  100. package/src/app/pages/api/auth/[...nextauth].ts +4 -0
  101. package/src/app/pages/api/auth/request-reset.ts +53 -0
  102. package/src/app/pages/api/auth/reset-password.ts +29 -0
  103. package/src/app/pages/api/install.ts +102 -0
  104. package/src/app/pages/api/oauth/device/approve.ts +23 -0
  105. package/src/app/pages/api/oauth/device/index.ts +41 -0
  106. package/src/app/pages/api/oauth/revoke.ts +24 -0
  107. package/src/app/pages/api/oauth/token.ts +31 -0
  108. package/src/app/pages/api/registry/health.ts +4 -3
  109. package/src/app/pages/api/registry/transfer/[kind]/[id].ts +3 -3
  110. package/src/app/pages/api/registry/transfer/assets/[collection].ts +2 -1
  111. package/src/app/pages/api/registry/transfer/assets/blobs/[hash].ts +2 -1
  112. package/src/app/pages/api/registry/transfer/tokens/[...setId].ts +2 -2
  113. package/src/app/pages/cli/device.tsx +143 -0
  114. package/src/app/pages/install.tsx +336 -0
  115. package/src/app/pages/login.tsx +117 -0
  116. package/src/app/pages/reset-password.tsx +159 -0
  117. package/src/app/types/next-auth.d.ts +27 -0
  118. package/src/app-builder/build.ts +22 -5
  119. package/src/app-builder/client-config.ts +10 -9
  120. package/src/cli/auth/browser.ts +29 -0
  121. package/src/cli/auth/device.ts +181 -0
  122. package/src/cli/auth/store.ts +137 -0
  123. package/src/cli/scaffold.ts +20 -197
  124. package/src/commands/index.ts +4 -0
  125. package/src/commands/login/index.ts +48 -0
  126. package/src/commands/logout/index.ts +51 -0
  127. package/src/config/entries.ts +175 -0
  128. package/src/config/index.ts +4 -0
  129. package/src/config/runtime.ts +15 -14
  130. package/src/registry/auth/access-tokens.ts +198 -0
  131. package/src/registry/auth/action-tokens.ts +225 -0
  132. package/src/registry/auth/crypto.ts +13 -0
  133. package/src/registry/auth/database.ts +15 -0
  134. package/src/registry/auth/device-authorizations.ts +223 -0
  135. package/src/registry/auth/gravatar.ts +6 -0
  136. package/src/registry/auth/index.ts +89 -0
  137. package/src/registry/auth/installation.ts +117 -0
  138. package/src/registry/auth/password.ts +46 -0
  139. package/src/registry/auth/rate-limits.ts +84 -0
  140. package/src/registry/auth/types.ts +34 -0
  141. package/src/registry/auth/user-record.ts +25 -0
  142. package/src/registry/auth/users.ts +145 -0
  143. package/src/registry/auth/validation.ts +24 -0
  144. package/src/registry/checkout/assets.ts +4 -2
  145. package/src/registry/checkout/index.ts +114 -24
  146. package/src/registry/checkout/tokens.ts +4 -2
  147. package/src/registry/connection.ts +23 -0
  148. package/src/registry/db/driver.ts +5 -4
  149. package/src/registry/db/index.ts +12 -0
  150. package/src/registry/db/schema.ts +174 -1
  151. package/src/registry/publish/assets.ts +1 -1
  152. package/src/registry/publish/errors.ts +3 -3
  153. package/src/registry/publish/index.ts +9 -9
  154. package/src/registry/publish/tokens.ts +1 -1
  155. package/src/types/config.ts +4 -1
  156. package/templates/default/README.md.tpl +18 -0
  157. package/templates/default/gitignore.tpl +1 -0
  158. package/templates/default/handoff.config.js.tpl +0 -1
  159. package/templates/default/handoff.config.ts.tpl +0 -1
  160. package/templates/default/handoff.config.with-examples.js.tpl +0 -1
  161. package/templates/default/handoff.config.with-examples.ts.tpl +0 -1
@@ -64,6 +64,7 @@ exports.checkoutEntities = exports.checkoutEntity = exports.resolveConnectionOrT
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"));
67
+ const entries_1 = require("../../config/entries");
67
68
  const runtime_1 = require("../../config/runtime");
68
69
  const logger_1 = require("../../utils/logger");
69
70
  const client_1 = require("../client");
@@ -92,24 +93,24 @@ const COMPONENT_EXTENSION = /\.(tsx|jsx|ts|js|cjs|mjs)$/i;
92
93
  * host has no local workspace to write into) and a resolved registry URL + access token. Throws an
93
94
  * actionable {@link CheckoutError} naming the exact misconfiguration.
94
95
  */
95
- const resolveConnectionOrThrow = (handoff) => {
96
+ const resolveConnectionOrThrow = (handoff) => __awaiter(void 0, void 0, void 0, function* () {
96
97
  var _a, _b, _c;
97
98
  const mode = (_c = (_b = (_a = handoff.config) === null || _a === void 0 ? void 0 : _a.runtime) === null || _b === void 0 ? void 0 : _b.mode) !== null && _c !== void 0 ? _c : 'workspace';
98
99
  if (mode !== 'workspace') {
99
100
  throw new CheckoutError(`checkout is only available from a connected workspace (runtime.mode: "workspace"); this project is "${mode}". ` +
100
101
  'A registry host stores and serves entities; it has no local workspace to check them out into.');
101
102
  }
102
- const connection = (0, connection_1.resolveRegistryConnection)(handoff.config);
103
+ const connection = yield (0, connection_1.resolveAuthenticatedRegistryConnection)(handoff.config, handoff.workingPath);
103
104
  if (!connection.url) {
104
- throw new CheckoutError(`No registry URL is configured. Set runtime.registryConnection.url, or the "${connection.urlEnv}" environment variable, ` +
105
- 'to the base URL of the registry to checkout from.');
105
+ throw new CheckoutError(`No registry is configured. Run \`handoff-app login --url <registry-url>\`, set runtime.registryConnection.url, ` +
106
+ `or set the "${connection.urlEnv}" environment variable to the base URL of the registry to checkout from.`);
106
107
  }
107
108
  if (!connection.accessToken) {
108
- throw new CheckoutError(`No registry access token is configured. Set the "${connection.accessTokenEnv}" environment variable to the ` +
109
- "registry's bearer token to authorize checkout.");
109
+ throw new CheckoutError(`No registry access token is configured. Run \`handoff-app login --url ${connection.url}\`, or set the ` +
110
+ `"${connection.accessTokenEnv}" environment variable to a user-issued token for CI.`);
110
111
  }
111
112
  return connection;
112
- };
113
+ });
113
114
  exports.resolveConnectionOrThrow = resolveConnectionOrThrow;
114
115
  /** Map a registry client error to an actionable checkout message. */
115
116
  const describeFetchFailure = (error, kind, id, registryUrl) => {
@@ -119,28 +120,69 @@ const describeFetchFailure = (error, kind, id, registryUrl) => {
119
120
  case 'runtime_mode_conflict':
120
121
  return `The registry at ${registryUrl} is not running in registry mode, so it cannot serve a checkout: ${error.message}`;
121
122
  case 'unauthorized':
122
- return `The registry rejected the access token (401). Check the configured access token matches the registry's token.`;
123
+ return `The registry rejected the access token (401). Run \`handoff-app login --url ${registryUrl}\` again, or replace the user-issued CI token.`;
124
+ case 'forbidden':
125
+ return `The registry token does not have permission to checkout this content (403). Authorize a token with registry:read access.`;
123
126
  default:
124
127
  return error.message;
125
128
  }
126
129
  };
127
130
  /**
128
- * Resolve the local directory the entity is written into: an already-checked-out / locally-declared
129
- * entity keeps its existing source directory (matched by stable `id`); otherwise it lands under the
130
- * first configured `entries.{components|patterns}` root (falling back to `components`/`patterns`).
131
+ * Ask the user which collection directory a new entity should land in when the config is ambiguous.
132
+ * Under `--force` (or any non-interactive run) there's no one to ask, so we fail with an actionable
133
+ * message rather than guessing a root.
131
134
  */
132
- const resolveTargetDir = (handoff, kind, id) => __awaiter(void 0, void 0, void 0, function* () {
135
+ const promptForCollectionRoot = (handoff, kind, roots) => __awaiter(void 0, void 0, void 0, function* () {
136
+ const relative = (root) => path_1.default.relative(handoff.workingPath, root) || '.';
137
+ if (handoff.force) {
138
+ throw new CheckoutError(`Cannot determine where to checkout the ${kind}: entries.${kind}s declares individual ${kind}s under different ` +
139
+ `directories (${roots.map(relative).join(', ')}). Declare a single collection directory in handoff.config, ` +
140
+ `or run checkout without --force to choose interactively.`);
141
+ }
142
+ const choice = yield p.select({
143
+ message: `Where should the new ${kind} be checked out?`,
144
+ options: roots.map((root) => ({ value: root, label: relative(root) })),
145
+ initialValue: roots[0],
146
+ });
147
+ if (p.isCancel(choice)) {
148
+ throw new CheckoutError('Checkout cancelled; no target directory chosen.');
149
+ }
150
+ return choice;
151
+ });
152
+ /**
153
+ * Resolve the collection directory a new entity is cloned into from the configured
154
+ * `entries.{components|patterns}`. Each entry is either a collection directory (used as-is) or a
155
+ * single declared entity directory, in which case its parent is the collection root so the new
156
+ * entity lands as a sibling rather than nested inside. Different parents mean the config is
157
+ * ambiguous, so we ask the user; with nothing configured we fall back to the default subdir.
158
+ */
159
+ const resolveCollectionRoot = (handoff, kind) => __awaiter(void 0, void 0, void 0, function* () {
133
160
  var _a, _b, _c, _d;
161
+ const configuredRoots = kind === 'component' ? (_b = (_a = handoff.config) === null || _a === void 0 ? void 0 : _a.entries) === null || _b === void 0 ? void 0 : _b.components : (_d = (_c = handoff.config) === null || _c === void 0 ? void 0 : _c.entries) === null || _d === void 0 ? void 0 : _d.patterns;
162
+ if (!(configuredRoots === null || configuredRoots === void 0 ? void 0 : configuredRoots.length)) {
163
+ return path_1.default.resolve(handoff.workingPath, DEFAULT_ENTITY_DIR[kind]);
164
+ }
165
+ const roots = [
166
+ ...new Set(configuredRoots.map((entry) => {
167
+ const resolved = path_1.default.resolve(handoff.workingPath, entry);
168
+ return (0, runtime_1.isComponentDirectory)(resolved) ? path_1.default.dirname(resolved) : resolved;
169
+ })),
170
+ ];
171
+ return roots.length === 1 ? roots[0] : promptForCollectionRoot(handoff, kind, roots);
172
+ });
173
+ /**
174
+ * Resolve the local directory the entity is written into. An already-checked-out or locally-declared
175
+ * entity keeps its existing source directory (matched by stable `id`); otherwise it lands as a
176
+ * sibling under its configured collection root (see {@link resolveCollectionRoot}).
177
+ */
178
+ const resolveTargetDir = (handoff, kind, id) => __awaiter(void 0, void 0, void 0, function* () {
134
179
  const store = kind === 'component' ? handoff.store.components : handoff.store.patterns;
135
180
  const existing = yield store.get(id);
136
181
  const existingDir = existing === null || existing === void 0 ? void 0 : existing.path;
137
182
  if (existingDir) {
138
183
  return existingDir;
139
184
  }
140
- const configuredRoots = kind === 'component' ? (_b = (_a = handoff.config) === null || _a === void 0 ? void 0 : _a.entries) === null || _b === void 0 ? void 0 : _b.components : (_d = (_c = handoff.config) === null || _c === void 0 ? void 0 : _c.entries) === null || _d === void 0 ? void 0 : _d.patterns;
141
- const root = (configuredRoots === null || configuredRoots === void 0 ? void 0 : configuredRoots.length)
142
- ? path_1.default.resolve(handoff.workingPath, configuredRoots[0])
143
- : path_1.default.resolve(handoff.workingPath, DEFAULT_ENTITY_DIR[kind]);
185
+ const root = yield resolveCollectionRoot(handoff, kind);
144
186
  const targetDir = (0, path_2.resolvePathWithin)(root, id);
145
187
  if (!targetDir) {
146
188
  throw new CheckoutError(`Cannot checkout ${kind} with unsafe id "${id}".`);
@@ -573,10 +615,11 @@ const checkoutSingle = (handoff, kind, id, client, registryUrl) => __awaiter(voi
573
615
  }
574
616
  throw error;
575
617
  }
576
- // Pages round-trip as a single verbatim `.md` with no declaration synthesis.
618
+ // Pages round-trip as a single verbatim `.md` with no declaration synthesis, and aren't
619
+ // declared in `entries`, so there's nothing to register.
577
620
  if (kind === 'page') {
578
621
  yield checkoutPage(handoff, id, payload);
579
- return;
622
+ return null;
580
623
  }
581
624
  const targetDir = yield resolveTargetDir(handoff, kind, id);
582
625
  const format = resolveDeclarationFormat(handoff, targetDir);
@@ -593,7 +636,7 @@ const checkoutSingle = (handoff, kind, id, client, registryUrl) => __awaiter(voi
593
636
  const conflicts = [...sourceTargets.map((entry) => entry.absolutePath), declarationPath].filter((file) => fs_extra_1.default.existsSync(file));
594
637
  if (!(yield confirmOverwrite(handoff, conflicts))) {
595
638
  logger_1.Logger.warn(`Checkout of ${kind} "${id}" cancelled; no files were written.`);
596
- return;
639
+ return null;
597
640
  }
598
641
  yield fs_extra_1.default.ensureDir(targetDir);
599
642
  const written = [];
@@ -605,15 +648,44 @@ const checkoutSingle = (handoff, kind, id, client, registryUrl) => __awaiter(voi
605
648
  written.push(declarationPath);
606
649
  logger_1.Logger.success(`Checked out ${kind} "${id}" into ${path_1.default.relative(handoff.workingPath, targetDir) || '.'} ` +
607
650
  `(${payload.files.length} source file(s) + ${declarationFileName}).`);
651
+ return targetDir;
652
+ });
653
+ /**
654
+ * Declare freshly checked-out entities in `entries.{components|patterns}` so the workspace build
655
+ * picks them up. Ones already covered by a collection directory load on their own and are left
656
+ * alone; the rest are added to the config automatically. If the config can't be edited (a computed
657
+ * or unusual `entries` array), we print the paths for the user to add so nothing is silently orphaned.
658
+ */
659
+ const registerCheckedOut = (handoff, kind, targetDirs) => __awaiter(void 0, void 0, void 0, function* () {
660
+ if (kind === 'page') {
661
+ return;
662
+ }
663
+ const entryKind = kind === 'component' ? 'components' : 'patterns';
664
+ const uncovered = targetDirs.filter((dir) => !(0, entries_1.isEntryCovered)(handoff, entryKind, dir));
665
+ if (uncovered.length === 0) {
666
+ return;
667
+ }
668
+ const result = yield (0, entries_1.writeEntries)(handoff, entryKind, uncovered);
669
+ if (result.status === 'added') {
670
+ const where = result.configPath ? path_1.default.relative(handoff.workingPath, result.configPath) || path_1.default.basename(result.configPath) : 'handoff.config';
671
+ logger_1.Logger.success(`Updated ${where} with ${result.added.length} ${entryKind} path(s).`);
672
+ return;
673
+ }
674
+ const where = result.configPath ? path_1.default.relative(handoff.workingPath, result.configPath) : 'handoff.config';
675
+ logger_1.Logger.warn(`Could not update ${where} automatically. Add these to entries.${entryKind} manually:`);
676
+ result.pending.forEach((rel) => logger_1.Logger.warn(` - ${rel}`));
608
677
  });
609
678
  /**
610
679
  * Checkout a single component, pattern, or page from the connected registry into this workspace.
611
680
  * Overwriting existing local files requires `--force` or an interactive confirmation.
612
681
  */
613
682
  const checkoutEntity = (handoff, kind, id) => __awaiter(void 0, void 0, void 0, function* () {
614
- const connection = (0, exports.resolveConnectionOrThrow)(handoff);
683
+ const connection = yield (0, exports.resolveConnectionOrThrow)(handoff);
615
684
  const client = (0, client_1.createRegistryClient)({ baseUrl: connection.url, accessToken: connection.accessToken });
616
- yield checkoutSingle(handoff, kind, id, client, connection.url);
685
+ const targetDir = yield checkoutSingle(handoff, kind, id, client, connection.url);
686
+ if (targetDir) {
687
+ yield registerCheckedOut(handoff, kind, [targetDir]);
688
+ }
617
689
  });
618
690
  exports.checkoutEntity = checkoutEntity;
619
691
  /**
@@ -623,7 +695,7 @@ exports.checkoutEntity = checkoutEntity;
623
695
  * prompting is per entity (skipped under `--force`).
624
696
  */
625
697
  const checkoutEntities = (handoff, kind) => __awaiter(void 0, void 0, void 0, function* () {
626
- const connection = (0, exports.resolveConnectionOrThrow)(handoff);
698
+ const connection = yield (0, exports.resolveConnectionOrThrow)(handoff);
627
699
  const client = (0, client_1.createRegistryClient)({ baseUrl: connection.url, accessToken: connection.accessToken });
628
700
  let summaries;
629
701
  try {
@@ -641,15 +713,21 @@ const checkoutEntities = (handoff, kind) => __awaiter(void 0, void 0, void 0, fu
641
713
  }
642
714
  let checkedOut = 0;
643
715
  const failed = [];
716
+ const targetDirs = [];
644
717
  for (const { id } of summaries) {
645
718
  try {
646
- yield checkoutSingle(handoff, kind, id, client, connection.url);
719
+ const targetDir = yield checkoutSingle(handoff, kind, id, client, connection.url);
720
+ if (targetDir) {
721
+ targetDirs.push(targetDir);
722
+ }
647
723
  checkedOut += 1;
648
724
  }
649
725
  catch (error) {
650
726
  failed.push({ id, message: error instanceof Error ? error.message : String(error) });
651
727
  }
652
728
  }
729
+ // Register everything checked out in one pass, so the config is edited (and confirmed) once.
730
+ yield registerCheckedOut(handoff, kind, targetDirs);
653
731
  logger_1.Logger.success(`${kind[0].toUpperCase()}${kind.slice(1)}s checkout complete — ${checkedOut} checked out${failed.length ? `, ${failed.length} failed` : ''}.`);
654
732
  if (failed.length > 0) {
655
733
  for (const failure of failed) {
@@ -61,7 +61,9 @@ const describeFetchFailure = (error, target, registryUrl) => {
61
61
  case 'runtime_mode_conflict':
62
62
  return `The registry at ${registryUrl} is not running in registry mode, so it cannot serve a checkout: ${error.message}`;
63
63
  case 'unauthorized':
64
- return `The registry rejected the access token (401). Check the configured access token matches the registry's token.`;
64
+ return `The registry rejected the access token (401). Run \`handoff-app login --url ${registryUrl}\` again, or replace the user-issued CI token.`;
65
+ case 'forbidden':
66
+ return `The registry token does not have permission to checkout design tokens (403). Authorize a token with registry:read access.`;
65
67
  default:
66
68
  return error.message;
67
69
  }
@@ -99,7 +101,7 @@ const checkoutTokens = (handoff, setId) => __awaiter(void 0, void 0, void 0, fun
99
101
  if (setId && !(0, sets_1.isTokenSetId)(setId)) {
100
102
  throw new index_1.CheckoutError(`Unknown or unsafe token set id "${setId}".`);
101
103
  }
102
- const connection = (0, index_1.resolveConnectionOrThrow)(handoff);
104
+ const connection = yield (0, index_1.resolveConnectionOrThrow)(handoff);
103
105
  const client = (0, client_1.createRegistryClient)({ baseUrl: connection.url, accessToken: connection.accessToken });
104
106
  let ids;
105
107
  if (setId) {
@@ -28,3 +28,12 @@ export interface ResolvedRegistryConnection {
28
28
  * Callers inspect the resolved URL and access token to surface specific configuration errors.
29
29
  */
30
30
  export declare const resolveRegistryConnection: (config: Config | null | undefined) => ResolvedRegistryConnection;
31
+ /**
32
+ * Resolve the same connection for an authenticated CLI operation, letting a saved device login fill
33
+ * in what the environment does not provide. A valid login supplies both the registry URL and the
34
+ * access token, so `handoff-app login` alone is enough to publish and checkout. Environment values
35
+ * keep precedence for deterministic CI: `HANDOFF_REGISTRY_URL` (or inline config) wins over the
36
+ * saved URL, and `HANDOFF_REGISTRY_ACCESS_TOKEN` wins over the saved token. The saved token only
37
+ * applies to the URL it was issued for.
38
+ */
39
+ export declare const resolveAuthenticatedRegistryConnection: (config: Config | null | undefined, workingPath?: string) => Promise<ResolvedRegistryConnection>;
@@ -8,8 +8,18 @@
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
10
  */
11
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
12
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
13
+ return new (P || (P = Promise))(function (resolve, reject) {
14
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
15
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
16
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
17
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
18
+ });
19
+ };
11
20
  Object.defineProperty(exports, "__esModule", { value: true });
12
- exports.resolveRegistryConnection = exports.DEFAULT_REGISTRY_ACCESS_TOKEN_ENV = exports.DEFAULT_REGISTRY_URL_ENV = void 0;
21
+ exports.resolveAuthenticatedRegistryConnection = exports.resolveRegistryConnection = exports.DEFAULT_REGISTRY_ACCESS_TOKEN_ENV = exports.DEFAULT_REGISTRY_URL_ENV = void 0;
22
+ const store_1 = require("../cli/auth/store");
13
23
  /** Default env-var name holding the remote registry base URL. */
14
24
  exports.DEFAULT_REGISTRY_URL_ENV = 'HANDOFF_REGISTRY_URL';
15
25
  /** Default env-var name holding the connected-workspace access token. */
@@ -34,3 +44,21 @@ const resolveRegistryConnection = (config) => {
34
44
  };
35
45
  };
36
46
  exports.resolveRegistryConnection = resolveRegistryConnection;
47
+ /**
48
+ * Resolve the same connection for an authenticated CLI operation, letting a saved device login fill
49
+ * in what the environment does not provide. A valid login supplies both the registry URL and the
50
+ * access token, so `handoff-app login` alone is enough to publish and checkout. Environment values
51
+ * keep precedence for deterministic CI: `HANDOFF_REGISTRY_URL` (or inline config) wins over the
52
+ * saved URL, and `HANDOFF_REGISTRY_ACCESS_TOKEN` wins over the saved token. The saved token only
53
+ * applies to the URL it was issued for.
54
+ */
55
+ const resolveAuthenticatedRegistryConnection = (config_1, ...args_1) => __awaiter(void 0, [config_1, ...args_1], void 0, function* (config, workingPath = process.cwd()) {
56
+ const connection = (0, exports.resolveRegistryConnection)(config);
57
+ 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 : '');
62
+ return Object.assign(Object.assign({}, connection), { url, accessToken });
63
+ });
64
+ exports.resolveAuthenticatedRegistryConnection = resolveAuthenticatedRegistryConnection;
@@ -14,7 +14,7 @@ import type { Config } from '../../types/config';
14
14
  export type RegistryDatabaseDriver = 'pg' | 'neon';
15
15
  /** Default env-var name holding the database connection string. */
16
16
  export declare const DEFAULT_DATABASE_URL_ENV = "DATABASE_URL";
17
- /** Default env-var name holding the registry management API bearer token. */
17
+ /** @deprecated Fixed registry server secrets are no longer authorized. */
18
18
  export declare const DEFAULT_REGISTRY_API_TOKEN_ENV = "HANDOFF_REGISTRY_API_TOKEN";
19
19
  /** Default registry database driver when none is configured. */
20
20
  export declare const DEFAULT_REGISTRY_DRIVER: RegistryDatabaseDriver;
@@ -30,9 +30,10 @@ export interface ResolvedRegistryDatabase {
30
30
  /** Resolve the env-var name holding the database URL (defaults to `DATABASE_URL`). */
31
31
  export declare const resolveDatabaseUrlEnv: (config: Config | null | undefined) => string;
32
32
  /**
33
- * Resolve the env-var *name* holding the registry management API bearer token (defaults to
34
- * `HANDOFF_REGISTRY_API_TOKEN`). Like the database URL, only the name is ever persisted — the token
35
- * value is resolved from the environment at request time and never written to config.
33
+ * Resolve the legacy fixed-token env-var name for configuration compatibility.
34
+ *
35
+ * @deprecated The returned env var is intentionally ignored by registry authorization. Use a
36
+ * user-issued access token or `handoff-app login`.
36
37
  */
37
38
  export declare const resolveApiTokenEnv: (config: Config | null | undefined) => string;
38
39
  /** Resolve the configured registry database driver (defaults to `pg`). */
@@ -14,7 +14,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
14
14
  exports.resolveRegistryDatabase = exports.resolveRegistryDriver = exports.resolveApiTokenEnv = exports.resolveDatabaseUrlEnv = exports.DEFAULT_REGISTRY_DRIVER = exports.DEFAULT_REGISTRY_API_TOKEN_ENV = exports.DEFAULT_DATABASE_URL_ENV = void 0;
15
15
  /** Default env-var name holding the database connection string. */
16
16
  exports.DEFAULT_DATABASE_URL_ENV = 'DATABASE_URL';
17
- /** Default env-var name holding the registry management API bearer token. */
17
+ /** @deprecated Fixed registry server secrets are no longer authorized. */
18
18
  exports.DEFAULT_REGISTRY_API_TOKEN_ENV = 'HANDOFF_REGISTRY_API_TOKEN';
19
19
  /** Default registry database driver when none is configured. */
20
20
  exports.DEFAULT_REGISTRY_DRIVER = 'pg';
@@ -26,9 +26,10 @@ const resolveDatabaseUrlEnv = (config) => {
26
26
  };
27
27
  exports.resolveDatabaseUrlEnv = resolveDatabaseUrlEnv;
28
28
  /**
29
- * Resolve the env-var *name* holding the registry management API bearer token (defaults to
30
- * `HANDOFF_REGISTRY_API_TOKEN`). Like the database URL, only the name is ever persisted — the token
31
- * value is resolved from the environment at request time and never written to config.
29
+ * Resolve the legacy fixed-token env-var name for configuration compatibility.
30
+ *
31
+ * @deprecated The returned env var is intentionally ignored by registry authorization. Use a
32
+ * user-issued access token or `handoff-app login`.
32
33
  */
33
34
  const resolveApiTokenEnv = (config) => {
34
35
  var _a, _b, _c;
@@ -1,4 +1,4 @@
1
- export { buildMetadata, componentFiles, components, docsArtifacts, patternFiles, patterns, registrySchema, type BuildMetadataEntityKind, type DocsArtifactEntityKind, type RegistryReviewMetadata, } from './schema';
1
+ export { buildMetadata, componentFiles, components, docsArtifacts, patternFiles, patterns, registryAccessTokens, registryAuthActionTokens, registryAuthRateLimits, registryDeviceAuthorizations, registryInstallations, registrySchema, registryUsers, type BuildMetadataEntityKind, type DocsArtifactEntityKind, type RegistryAccessScope, type RegistryAuthActionPurpose, type RegistryDeviceAuthorizationStatus, type RegistryRateLimitBucket, type RegistryReviewMetadata, type RegistryUserRole, type RegistryUserStatus, } from './schema';
2
2
  export type { ArtifactBuildStatus as RegistryBuildStatus } from '../../artifacts/types';
3
3
  export { DEFAULT_DATABASE_URL_ENV, DEFAULT_REGISTRY_DRIVER, DEFAULT_REGISTRY_API_TOKEN_ENV, resolveApiTokenEnv, resolveDatabaseUrlEnv, resolveRegistryDriver, resolveRegistryDatabase, type RegistryDatabaseDriver, type ResolvedRegistryDatabase, } from './driver';
4
4
  export { createRegistryDbConnection, type CreateRegistryDbConnectionParams, type RegistryDatabase, type RegistryDbConnection, } from './client';
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.runRegistryMigrations = exports.getMigrationsFolder = exports.createRegistryDbConnection = exports.resolveRegistryDatabase = exports.resolveRegistryDriver = exports.resolveDatabaseUrlEnv = exports.resolveApiTokenEnv = exports.DEFAULT_REGISTRY_API_TOKEN_ENV = exports.DEFAULT_REGISTRY_DRIVER = exports.DEFAULT_DATABASE_URL_ENV = exports.registrySchema = exports.patterns = exports.patternFiles = exports.docsArtifacts = exports.components = exports.componentFiles = exports.buildMetadata = void 0;
3
+ exports.runRegistryMigrations = exports.getMigrationsFolder = exports.createRegistryDbConnection = exports.resolveRegistryDatabase = exports.resolveRegistryDriver = exports.resolveDatabaseUrlEnv = exports.resolveApiTokenEnv = exports.DEFAULT_REGISTRY_API_TOKEN_ENV = exports.DEFAULT_REGISTRY_DRIVER = exports.DEFAULT_DATABASE_URL_ENV = exports.registryUsers = exports.registrySchema = exports.registryInstallations = exports.registryDeviceAuthorizations = exports.registryAuthRateLimits = exports.registryAuthActionTokens = exports.registryAccessTokens = exports.patterns = exports.patternFiles = exports.docsArtifacts = exports.components = exports.componentFiles = exports.buildMetadata = void 0;
4
4
  var schema_1 = require("./schema");
5
5
  Object.defineProperty(exports, "buildMetadata", { enumerable: true, get: function () { return schema_1.buildMetadata; } });
6
6
  Object.defineProperty(exports, "componentFiles", { enumerable: true, get: function () { return schema_1.componentFiles; } });
@@ -8,7 +8,13 @@ Object.defineProperty(exports, "components", { enumerable: true, get: function (
8
8
  Object.defineProperty(exports, "docsArtifacts", { enumerable: true, get: function () { return schema_1.docsArtifacts; } });
9
9
  Object.defineProperty(exports, "patternFiles", { enumerable: true, get: function () { return schema_1.patternFiles; } });
10
10
  Object.defineProperty(exports, "patterns", { enumerable: true, get: function () { return schema_1.patterns; } });
11
+ Object.defineProperty(exports, "registryAccessTokens", { enumerable: true, get: function () { return schema_1.registryAccessTokens; } });
12
+ Object.defineProperty(exports, "registryAuthActionTokens", { enumerable: true, get: function () { return schema_1.registryAuthActionTokens; } });
13
+ Object.defineProperty(exports, "registryAuthRateLimits", { enumerable: true, get: function () { return schema_1.registryAuthRateLimits; } });
14
+ Object.defineProperty(exports, "registryDeviceAuthorizations", { enumerable: true, get: function () { return schema_1.registryDeviceAuthorizations; } });
15
+ Object.defineProperty(exports, "registryInstallations", { enumerable: true, get: function () { return schema_1.registryInstallations; } });
11
16
  Object.defineProperty(exports, "registrySchema", { enumerable: true, get: function () { return schema_1.registrySchema; } });
17
+ Object.defineProperty(exports, "registryUsers", { enumerable: true, get: function () { return schema_1.registryUsers; } });
12
18
  var driver_1 = require("./driver");
13
19
  Object.defineProperty(exports, "DEFAULT_DATABASE_URL_ENV", { enumerable: true, get: function () { return driver_1.DEFAULT_DATABASE_URL_ENV; } });
14
20
  Object.defineProperty(exports, "DEFAULT_REGISTRY_DRIVER", { enumerable: true, get: function () { return driver_1.DEFAULT_REGISTRY_DRIVER; } });