handoff-app 2.0.0-3 → 2.0.0-5

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 (209) hide show
  1. package/AGENTS.md +7 -0
  2. package/CLAUDE.md +7 -0
  3. package/README.md +302 -378
  4. package/dist/app-builder/build.js +2 -2
  5. package/dist/app-builder/nav-shell.js +8 -15
  6. package/dist/app-builder/vercel-output.js +4 -4
  7. package/dist/cli/make.js +2 -2
  8. package/dist/cli.js +39 -7
  9. package/dist/commands/build/app.js +1 -5
  10. package/dist/commands/build/components.js +1 -5
  11. package/dist/commands/build/index.js +1 -5
  12. package/dist/commands/checkout/index.d.ts +15 -12
  13. package/dist/commands/checkout/index.js +15 -54
  14. package/dist/commands/compat/deprecations.d.ts +46 -0
  15. package/dist/commands/compat/deprecations.js +62 -0
  16. package/dist/commands/compat/push-args.d.ts +48 -0
  17. package/dist/commands/compat/push-args.js +73 -0
  18. package/dist/commands/db/migrate.js +1 -5
  19. package/dist/commands/dev/index.js +1 -5
  20. package/dist/commands/eject/config.js +1 -5
  21. package/dist/commands/eject/pages.js +1 -5
  22. package/dist/commands/eject/theme.js +1 -5
  23. package/dist/commands/fetch/index.js +1 -5
  24. package/dist/commands/index.js +4 -0
  25. package/dist/commands/login/index.js +1 -5
  26. package/dist/commands/logout/index.js +1 -5
  27. package/dist/commands/make/component.js +1 -5
  28. package/dist/commands/make/page.js +3 -7
  29. package/dist/commands/make/template.js +1 -5
  30. package/dist/commands/publish/index.d.ts +18 -12
  31. package/dist/commands/publish/index.js +18 -54
  32. package/dist/commands/pull/index.d.ts +13 -0
  33. package/dist/commands/pull/index.js +20 -0
  34. package/dist/commands/push/index.d.ts +23 -0
  35. package/dist/commands/push/index.js +58 -0
  36. package/dist/commands/scaffold/index.js +1 -5
  37. package/dist/commands/start/index.js +1 -5
  38. package/dist/commands/types.d.ts +10 -0
  39. package/dist/commands/utils.d.ts +74 -6
  40. package/dist/commands/utils.js +89 -10
  41. package/dist/commands/validate/components.js +1 -5
  42. package/dist/config/index.d.ts +2 -1
  43. package/dist/config/index.js +2 -1
  44. package/dist/config/loader.d.ts +19 -6
  45. package/dist/config/loader.js +43 -13
  46. package/dist/config/normalizers/page.d.ts +4 -4
  47. package/dist/config/normalizers/page.js +2 -2
  48. package/dist/config/runtime.js +9 -25
  49. package/dist/index.d.ts +69 -22
  50. package/dist/index.js +148 -35
  51. package/dist/nav/resolver.js +3 -1
  52. package/dist/nav/workspace.js +2 -2
  53. package/dist/registry/auth/access-tokens.d.ts +11 -3
  54. package/dist/registry/auth/access-tokens.js +29 -3
  55. package/dist/registry/auth/index.d.ts +4 -3
  56. package/dist/registry/auth/index.js +10 -2
  57. package/dist/registry/auth/sync-secret.d.ts +27 -0
  58. package/dist/registry/auth/sync-secret.js +65 -0
  59. package/dist/registry/auth/types.d.ts +20 -1
  60. package/dist/registry/auth/types.js +3 -1
  61. package/dist/registry/auth/validation.d.ts +11 -0
  62. package/dist/registry/auth/validation.js +18 -1
  63. package/dist/registry/checkout/assets.d.ts +1 -1
  64. package/dist/registry/checkout/assets.js +21 -6
  65. package/dist/registry/checkout/index.d.ts +8 -1
  66. package/dist/registry/checkout/index.js +39 -6
  67. package/dist/registry/checkout/tokens.d.ts +1 -1
  68. package/dist/registry/checkout/tokens.js +12 -5
  69. package/dist/registry/connection.d.ts +10 -0
  70. package/dist/registry/connection.js +52 -7
  71. package/dist/registry/content-kinds.d.ts +27 -0
  72. package/dist/registry/content-kinds.js +33 -0
  73. package/dist/registry/db/client.d.ts +6 -1
  74. package/dist/registry/db/client.js +16 -3
  75. package/dist/registry/db/schema.d.ts +37 -3
  76. package/dist/registry/db/schema.js +9 -7
  77. package/dist/registry/publish/assets.d.ts +2 -2
  78. package/dist/registry/publish/assets.js +46 -35
  79. package/dist/registry/publish/index.d.ts +29 -5
  80. package/dist/registry/publish/index.js +87 -34
  81. package/dist/registry/publish/tokens.d.ts +2 -2
  82. package/dist/registry/publish/tokens.js +30 -24
  83. package/dist/registry/selection.d.ts +12 -0
  84. package/dist/registry/selection.js +22 -0
  85. package/dist/store/registry.d.ts +48 -0
  86. package/dist/store/registry.js +75 -12
  87. package/dist/transformers/preview/types.d.ts +2 -2
  88. package/dist/utils/markdown.d.ts +10 -0
  89. package/dist/utils/markdown.js +17 -0
  90. package/dist/utils/menu-shell.js +14 -7
  91. package/dist/utils/pages.d.ts +22 -0
  92. package/dist/utils/pages.js +73 -0
  93. package/dist/utils/path.d.ts +5 -0
  94. package/dist/utils/path.js +9 -0
  95. package/docs/api.md +1 -3
  96. package/docs/cli.md +1 -1
  97. package/drizzle/0002_add_pages.sql +5 -3
  98. package/drizzle/meta/0002_snapshot.json +17 -8
  99. package/drizzle/meta/0003_snapshot.json +17 -8
  100. package/drizzle/meta/0004_snapshot.json +17 -8
  101. package/drizzle/meta/0005_snapshot.json +17 -8
  102. package/package.json +1 -1
  103. package/src/app/components/util/index.ts +5 -23
  104. package/src/app/lib/docs-api/backend.ts +11 -0
  105. package/src/app/lib/docs-api/errors.ts +8 -7
  106. package/src/app/lib/docs-api/handler.ts +2 -3
  107. package/src/app/lib/docs-api/index.ts +2 -0
  108. package/src/app/lib/docs-api/page-discovery.ts +126 -0
  109. package/src/app/lib/docs-api/page-rendering.ts +17 -0
  110. package/src/app/lib/docs-api/page-search.ts +191 -0
  111. package/src/app/lib/docs-api/records.ts +11 -19
  112. package/src/app/lib/docs-api/registry.ts +90 -11
  113. package/src/app/lib/docs-api/search.ts +115 -0
  114. package/src/app/lib/registry-api/allowlist.ts +7 -5
  115. package/src/app/lib/registry-api/asset-transfer.ts +58 -31
  116. package/src/app/lib/registry-api/auth.ts +89 -9
  117. package/src/app/lib/registry-api/entity-routes.ts +17 -7
  118. package/src/app/lib/registry-api/errors.ts +10 -4
  119. package/src/app/lib/registry-api/handler.ts +13 -15
  120. package/src/app/lib/registry-api/meta.ts +4 -0
  121. package/src/app/lib/registry-api/revalidate.ts +31 -12
  122. package/src/app/lib/registry-api/store.ts +89 -26
  123. package/src/app/lib/registry-api/sync-compat.ts +166 -0
  124. package/src/app/lib/registry-api/token-transfer.ts +44 -12
  125. package/src/app/lib/registry-api/transfer-batch.ts +116 -0
  126. package/src/app/lib/registry-api/transfer.ts +62 -35
  127. package/src/app/lib/registry-api/validation.ts +33 -1
  128. package/src/app/lib/registry-connection.ts +1 -1
  129. package/src/app/next.config.mjs +6 -0
  130. package/src/app/pages/[...slug]/index.tsx +37 -22
  131. package/src/app/pages/api/account/tokens/index.ts +4 -13
  132. package/src/app/pages/api/docs/search/pages.json.ts +25 -0
  133. package/src/app/pages/api/oauth/revoke.ts +14 -7
  134. package/src/app/pages/api/registry/components/[id]/files/[...filePath].ts +1 -1
  135. package/src/app/pages/api/registry/components/[id].ts +2 -2
  136. package/src/app/pages/api/registry/components/index.ts +1 -1
  137. package/src/app/pages/api/registry/pages/[...id].ts +20 -0
  138. package/src/app/pages/api/registry/pages/index.ts +19 -0
  139. package/src/app/pages/api/registry/patterns/[id]/files/[...filePath].ts +1 -1
  140. package/src/app/pages/api/registry/patterns/[id].ts +2 -2
  141. package/src/app/pages/api/registry/patterns/index.ts +1 -1
  142. package/src/app/pages/api/registry/transfer/assets/blobs/have.ts +2 -2
  143. package/src/app/pages/api/sync/upload.ts +21 -0
  144. package/src/app/pages/index/index.tsx +14 -0
  145. package/src/app/pages/index.tsx +30 -5
  146. package/src/app-builder/build.ts +2 -2
  147. package/src/app-builder/nav-shell.ts +8 -13
  148. package/src/app-builder/vercel-output.ts +4 -4
  149. package/src/cli/make.ts +2 -2
  150. package/src/cli.ts +31 -7
  151. package/src/commands/build/app.ts +2 -3
  152. package/src/commands/build/components.ts +2 -3
  153. package/src/commands/build/index.ts +2 -3
  154. package/src/commands/checkout/index.ts +18 -44
  155. package/src/commands/compat/deprecations.ts +79 -0
  156. package/src/commands/compat/push-args.ts +106 -0
  157. package/src/commands/db/migrate.ts +2 -3
  158. package/src/commands/dev/index.ts +2 -3
  159. package/src/commands/eject/config.ts +2 -3
  160. package/src/commands/eject/pages.ts +2 -3
  161. package/src/commands/eject/theme.ts +2 -3
  162. package/src/commands/fetch/index.ts +2 -3
  163. package/src/commands/index.ts +4 -0
  164. package/src/commands/login/index.ts +2 -3
  165. package/src/commands/logout/index.ts +2 -3
  166. package/src/commands/make/component.ts +2 -3
  167. package/src/commands/make/page.ts +4 -5
  168. package/src/commands/make/template.ts +2 -3
  169. package/src/commands/publish/index.ts +21 -44
  170. package/src/commands/pull/index.ts +32 -0
  171. package/src/commands/push/index.ts +115 -0
  172. package/src/commands/scaffold/index.ts +2 -3
  173. package/src/commands/start/index.ts +2 -3
  174. package/src/commands/types.ts +11 -1
  175. package/src/commands/utils.ts +80 -11
  176. package/src/commands/validate/components.ts +2 -3
  177. package/src/config/index.ts +2 -1
  178. package/src/config/loader.ts +57 -12
  179. package/src/config/normalizers/page.ts +4 -4
  180. package/src/config/runtime.ts +9 -24
  181. package/src/index.ts +183 -42
  182. package/src/nav/resolver.ts +3 -1
  183. package/src/nav/workspace.ts +2 -2
  184. package/src/registry/auth/access-tokens.ts +29 -5
  185. package/src/registry/auth/index.ts +7 -1
  186. package/src/registry/auth/sync-secret.ts +67 -0
  187. package/src/registry/auth/types.ts +24 -1
  188. package/src/registry/auth/validation.ts +18 -0
  189. package/src/registry/checkout/assets.ts +24 -7
  190. package/src/registry/checkout/index.ts +41 -5
  191. package/src/registry/checkout/tokens.ts +12 -6
  192. package/src/registry/connection.ts +53 -5
  193. package/src/registry/content-kinds.ts +39 -0
  194. package/src/registry/db/client.ts +22 -2
  195. package/src/registry/db/schema.ts +9 -7
  196. package/src/registry/publish/assets.ts +47 -37
  197. package/src/registry/publish/index.ts +102 -38
  198. package/src/registry/publish/tokens.ts +35 -26
  199. package/src/registry/selection.ts +19 -0
  200. package/src/store/registry.ts +118 -13
  201. package/src/transformers/preview/types.ts +2 -2
  202. package/src/utils/README.md +1 -0
  203. package/src/utils/markdown.ts +11 -0
  204. package/src/utils/menu-shell.ts +14 -7
  205. package/src/utils/pages.ts +52 -0
  206. package/src/utils/path.ts +15 -6
  207. package/templates/default/package.json.tpl +4 -0
  208. package/config/docs/foundations/logos.md +0 -7
  209. package/src/app/lib/api/redact.ts +0 -46
package/dist/index.js CHANGED
@@ -45,11 +45,68 @@ const eject_1 = require("./cli/eject");
45
45
  const make_1 = require("./cli/make");
46
46
  const config_1 = require("./config");
47
47
  const pipeline_1 = __importStar(require("./pipeline"));
48
+ const content_kinds_1 = require("./registry/content-kinds");
48
49
  const store_1 = require("./store");
49
50
  const builder_1 = __importStar(require("./transformers/preview/component/builder"));
50
51
  const logger_1 = require("./utils/logger");
51
52
  const path_2 = require("./utils/path");
52
53
  const path_3 = require("./utils/path");
54
+ /**
55
+ * Reject a kind we do not know. Plural kinds arrive from CLI arguments and HTTP payloads, so an
56
+ * unrecognized one has to stop here: the entity paths below pick their store by elimination, and an
57
+ * unchecked value would quietly resolve to pages.
58
+ */
59
+ const assertContentKind = (kind) => {
60
+ if (!(0, content_kinds_1.isRegistryEntityKind)(kind)) {
61
+ throw new Error(`Unknown content kind "${kind}". Supported kinds: ${content_kinds_1.REGISTRY_ENTITY_KINDS.join(', ')}.`);
62
+ }
63
+ };
64
+ const normalizeTarget = (target) => typeof target === 'string' ? { kind: target } : target;
65
+ /**
66
+ * Normalize an optional single-or-list selection to a list, or `undefined` for "everything". An empty
67
+ * list means "everything" too: yargs defaults an omitted variadic positional to `[]`, and that has to
68
+ * read as "no narrowing", not "no targets".
69
+ */
70
+ const toSelection = (value) => {
71
+ if (value === undefined)
72
+ return undefined;
73
+ const selection = Array.isArray(value) ? value : [value];
74
+ return selection.length > 0 ? selection : undefined;
75
+ };
76
+ /**
77
+ * The one id a selection names, if it names exactly one. Lets a single-target request keep the
78
+ * cheaper targeted-build path whether it arrived as a string or a one-element list.
79
+ */
80
+ const onlyId = (value) => {
81
+ const selection = toSelection(value);
82
+ return (selection === null || selection === void 0 ? void 0 : selection.length) === 1 ? selection[0] : undefined;
83
+ };
84
+ /**
85
+ * Run one operation across several targets in order, reporting each failure as it happens and throwing
86
+ * once at the end. Shared by {@link Handoff.publishAll} and {@link Handoff.checkoutAll} so a
87
+ * multi-kind run behaves the same in both directions.
88
+ */
89
+ const runKinds = (targets, verb, dryRun, run) => __awaiter(void 0, void 0, void 0, function* () {
90
+ const kinds = targets.map(normalizeTarget);
91
+ const failed = [];
92
+ for (const { kind, ids } of kinds) {
93
+ try {
94
+ yield run(kind, ids);
95
+ }
96
+ catch (error) {
97
+ const message = error instanceof Error ? error.message : String(error);
98
+ failed.push({ kind, message });
99
+ logger_1.Logger.error(`Failed to ${verb} ${kind}: ${message}`);
100
+ }
101
+ }
102
+ const succeeded = kinds.length - failed.length;
103
+ const outcome = verb === 'publish' ? 'published' : 'checked out';
104
+ logger_1.Logger.success(`${dryRun ? 'Dry run' : `${verb[0].toUpperCase()}${verb.slice(1)} complete`} — ${succeeded}/${kinds.length} kind(s) ` +
105
+ `${dryRun ? `would be ${outcome}` : outcome}${failed.length ? `, ${failed.length} failed` : ''}.`);
106
+ if (failed.length > 0) {
107
+ throw new Error(`${failed.length} kind(s) failed to ${verb}: ${failed.map((entry) => entry.kind).join(', ')}.`);
108
+ }
109
+ });
53
110
  class Handoff {
54
111
  /**
55
112
  * Normalized store over the current workspace runtime for components, patterns, pages, tokens,
@@ -61,30 +118,38 @@ class Handoff {
61
118
  }
62
119
  return this._store;
63
120
  }
64
- constructor(debug, force, config) {
121
+ constructor(options = {}) {
65
122
  this.debug = false;
66
123
  this.force = false;
124
+ this.dryRun = false;
125
+ this.skipBuild = false;
67
126
  this.modulePath = path_1.default.resolve(__filename, '../..');
68
- this.workingPath = process.cwd();
127
+ this.workingPath = (0, path_2.resolveWorkingPath)();
69
128
  this.exportsDirectory = 'exported';
70
129
  this.sitesDirectory = 'out';
71
- this._initialArgs = {};
130
+ this._options = {};
72
131
  this._configFilePaths = [];
73
132
  this._configFileIndex = new Map();
74
- this._initialArgs = { debug, force, config };
75
- this.construct(debug, force, config);
133
+ this._options = options;
134
+ this.construct(options);
76
135
  }
77
- construct(debug, force, config) {
136
+ construct(options) {
137
+ var _a, _b, _c, _d;
78
138
  this.config = null;
79
- this.debug = debug !== null && debug !== void 0 ? debug : false;
80
- this.force = force !== null && force !== void 0 ? force : false;
139
+ this.debug = (_a = options.debug) !== null && _a !== void 0 ? _a : false;
140
+ this.force = (_b = options.force) !== null && _b !== void 0 ? _b : false;
141
+ this.dryRun = (_c = options.dryRun) !== null && _c !== void 0 ? _c : false;
142
+ this.skipBuild = (_d = options.skipBuild) !== null && _d !== void 0 ? _d : false;
81
143
  logger_1.Logger.init({ debug: this.debug });
82
- this.init(config);
144
+ this.init(options.config);
83
145
  global.handoff = this;
84
146
  }
85
147
  init(configOverride) {
86
148
  var _a, _b, _c;
87
- const configResult = (0, config_1.initConfigWithMetadata)(configOverride !== null && configOverride !== void 0 ? configOverride : {});
149
+ const configResult = (0, config_1.initConfigWithMetadata)(configOverride !== null && configOverride !== void 0 ? configOverride : {}, {
150
+ workingPath: this.workingPath,
151
+ configPath: this._options.configPath,
152
+ });
88
153
  const config = configResult.config;
89
154
  this.config = config;
90
155
  this._mainConfigFilePath = configResult.configPath;
@@ -97,7 +162,7 @@ class Handoff {
97
162
  return this;
98
163
  }
99
164
  reload() {
100
- this.construct(this._initialArgs.debug, this._initialArgs.force, this._initialArgs.config);
165
+ this.construct(this._options);
101
166
  return this;
102
167
  }
103
168
  preRunner(validate) {
@@ -160,42 +225,45 @@ class Handoff {
160
225
  }
161
226
  /**
162
227
  * Publish components, patterns, or pages from this connected workspace to the configured remote
163
- * registry. With `id` it publishes that one entity after a targeted build; without `id` it builds
164
- * the kind once and publishes every declared entity, skipping ones whose content is unchanged on
165
- * the registry (`--force` re-uploads all). Pages upload their record and markdown source because
166
- * they render at runtime. The publish module is loaded lazily so the registry client and build code
167
- * never enter the docs app bundle.
228
+ * registry. With a single `id` it publishes that one entity after a targeted build; with a list of
229
+ * ids, or none, it builds the kind once and publishes the selected (or every declared) entity,
230
+ * skipping ones whose content is unchanged on the registry (`--force` re-uploads all). Pages upload
231
+ * their record and markdown source because they render at runtime. The publish module is loaded
232
+ * lazily so the registry client and build code never enter the docs app bundle.
168
233
  */
169
234
  publish(kind, id) {
170
235
  return __awaiter(this, void 0, void 0, function* () {
171
236
  this.preRunner();
172
- if (id) {
237
+ const single = onlyId(id);
238
+ if (single) {
173
239
  const { publishEntity } = yield Promise.resolve().then(() => __importStar(require('./registry/publish')));
174
- yield publishEntity(this, kind, id);
240
+ yield publishEntity(this, kind, single);
175
241
  }
176
242
  else {
177
243
  const { publishEntities } = yield Promise.resolve().then(() => __importStar(require('./registry/publish')));
178
- yield publishEntities(this, kind);
244
+ yield publishEntities(this, kind, toSelection(id));
179
245
  }
180
246
  return this;
181
247
  });
182
248
  }
183
249
  /**
184
250
  * Checkout components, patterns, or pages from the connected remote registry into this workspace.
185
- * With `id` it checks out that one entity; without `id` it checks out every published entity of the
186
- * kind. Overwriting existing local files requires `--force` or an interactive confirmation. The
187
- * checkout module is loaded lazily so the registry client never enters the docs app bundle.
251
+ * With a single `id` it checks out that one entity; with a list of ids, or none, it checks out the
252
+ * selected (or every published) entity of the kind. Overwriting existing local files requires
253
+ * `--force` or an interactive confirmation. The checkout module is loaded lazily so the registry
254
+ * client never enters the docs app bundle.
188
255
  */
189
256
  checkout(kind, id) {
190
257
  return __awaiter(this, void 0, void 0, function* () {
191
258
  this.preRunner();
192
- if (id) {
259
+ const single = onlyId(id);
260
+ if (single) {
193
261
  const { checkoutEntity } = yield Promise.resolve().then(() => __importStar(require('./registry/checkout')));
194
- yield checkoutEntity(this, kind, id);
262
+ yield checkoutEntity(this, kind, single);
195
263
  }
196
264
  else {
197
265
  const { checkoutEntities } = yield Promise.resolve().then(() => __importStar(require('./registry/checkout')));
198
- yield checkoutEntities(this, kind);
266
+ yield checkoutEntities(this, kind, toSelection(id));
199
267
  }
200
268
  return this;
201
269
  });
@@ -204,28 +272,28 @@ class Handoff {
204
272
  * Publish design token sets from this connected workspace to the configured remote registry. Runs a
205
273
  * fresh token build (Figma extract + style transformers), discovers the logical sets, and uploads
206
274
  * each changed set (its extracted record + generated artifacts). Publishes every set when `setId` is
207
- * omitted, or only the named set (`foundation/colors`, `component/<id>`). Loaded lazily so the
275
+ * omitted, or only the named set(s) (`foundation/colors`, `component/<id>`). Loaded lazily so the
208
276
  * registry client/build code never enters the docs app bundle.
209
277
  */
210
278
  publishTokens(setId) {
211
279
  return __awaiter(this, void 0, void 0, function* () {
212
280
  this.preRunner();
213
281
  const { publishTokens } = yield Promise.resolve().then(() => __importStar(require('./registry/publish/tokens')));
214
- yield publishTokens(this, setId);
282
+ yield publishTokens(this, toSelection(setId));
215
283
  return this;
216
284
  });
217
285
  }
218
286
  /**
219
287
  * Checkout design token sets from the connected remote registry into this workspace: reconstruct
220
288
  * the canonical local `tokens.json` and restore the generated token files to their configured output
221
- * paths. Checks out every published set when `setId` is omitted, or only the named set. Loaded lazily
222
- * so the registry client never enters the docs app bundle.
289
+ * paths. Checks out every published set when `setId` is omitted, or only the named set(s). Loaded
290
+ * lazily so the registry client never enters the docs app bundle.
223
291
  */
224
292
  checkoutTokens(setId) {
225
293
  return __awaiter(this, void 0, void 0, function* () {
226
294
  this.preRunner();
227
295
  const { checkoutTokens } = yield Promise.resolve().then(() => __importStar(require('./registry/checkout/tokens')));
228
- yield checkoutTokens(this, setId);
296
+ yield checkoutTokens(this, toSelection(setId));
229
297
  return this;
230
298
  });
231
299
  }
@@ -233,28 +301,73 @@ class Handoff {
233
301
  * Publish asset collections (icons/logos/fonts) from this connected workspace to the configured
234
302
  * remote registry. Runs a fresh build (Figma extract + asset generation), discovers the collections,
235
303
  * uploads only content hashes the registry lacks, and finalizes each collection manifest atomically.
236
- * Publishes every collection when `collection` is omitted, or only the named one. Loaded lazily so
304
+ * Publishes every collection when `collection` is omitted, or only the named one(s). Loaded lazily so
237
305
  * the registry client/build code never enters the docs app bundle.
238
306
  */
239
307
  publishAssets(collection) {
240
308
  return __awaiter(this, void 0, void 0, function* () {
241
309
  this.preRunner();
242
310
  const { publishAssets } = yield Promise.resolve().then(() => __importStar(require('./registry/publish/assets')));
243
- yield publishAssets(this, collection);
311
+ yield publishAssets(this, toSelection(collection));
244
312
  return this;
245
313
  });
246
314
  }
247
315
  /**
248
316
  * Checkout asset collections from the connected remote registry into this workspace: recreate the
249
317
  * standard workspace asset files, collection JSON, icon sprite/manifest, and downloadable archives.
250
- * Checks out every published collection when `collection` is omitted, or only the named one. Loaded
251
- * lazily so the registry client never enters the docs app bundle.
318
+ * Checks out every published collection when `collection` is omitted, or only the named one(s).
319
+ * Loaded lazily so the registry client never enters the docs app bundle.
252
320
  */
253
321
  checkoutAssets(collection) {
254
322
  return __awaiter(this, void 0, void 0, function* () {
255
323
  this.preRunner();
256
324
  const { checkoutAssets } = yield Promise.resolve().then(() => __importStar(require('./registry/checkout/assets')));
257
- yield checkoutAssets(this, collection);
325
+ yield checkoutAssets(this, toSelection(collection));
326
+ return this;
327
+ });
328
+ }
329
+ /**
330
+ * Publish one content kind named by its plural form (`components`, `tokens`, …). The entry point for
331
+ * callers working in the plural vocabulary, so the plural-to-transfer-path mapping lives in one
332
+ * place.
333
+ */
334
+ publishKind(kind, id) {
335
+ return __awaiter(this, void 0, void 0, function* () {
336
+ assertContentKind(kind);
337
+ if (kind === 'tokens')
338
+ return this.publishTokens(id);
339
+ if (kind === 'assets')
340
+ return this.publishAssets(id);
341
+ return this.publish(content_kinds_1.ENTITY_WIRE_KIND[kind], id);
342
+ });
343
+ }
344
+ /** Checkout one content kind named by its plural form. Mirrors {@link publishKind}. */
345
+ checkoutKind(kind, id) {
346
+ return __awaiter(this, void 0, void 0, function* () {
347
+ assertContentKind(kind);
348
+ if (kind === 'tokens')
349
+ return this.checkoutTokens(id);
350
+ if (kind === 'assets')
351
+ return this.checkoutAssets(id);
352
+ return this.checkout(content_kinds_1.ENTITY_WIRE_KIND[kind], id);
353
+ });
354
+ }
355
+ /**
356
+ * Publish every content kind, or the given subset of kinds and ids, in dependency order: tokens and
357
+ * assets first (rendered component artifacts reference the CSS variables and asset URLs they
358
+ * produce), then components, patterns and pages. A failing kind is reported and never aborts the
359
+ * rest; the run throws at the end if any kind failed.
360
+ */
361
+ publishAll() {
362
+ return __awaiter(this, arguments, void 0, function* (targets = content_kinds_1.ALL_KIND_ORDER) {
363
+ yield runKinds(targets, 'publish', this.dryRun, (kind, ids) => this.publishKind(kind, ids));
364
+ return this;
365
+ });
366
+ }
367
+ /** Checkout every content kind, or the given subset, in the same order. Mirrors {@link publishAll}. */
368
+ checkoutAll() {
369
+ return __awaiter(this, arguments, void 0, function* (targets = content_kinds_1.ALL_KIND_ORDER) {
370
+ yield runKinds(targets, 'checkout', this.dryRun, (kind, ids) => this.checkoutKind(kind, ids));
258
371
  return this;
259
372
  });
260
373
  }
@@ -13,6 +13,7 @@ var __rest = (this && this.__rest) || function (s, e) {
13
13
  Object.defineProperty(exports, "__esModule", { value: true });
14
14
  exports.getNavData = void 0;
15
15
  const lodash_1 = require("lodash");
16
+ const content_kinds_1 = require("../registry/content-kinds");
16
17
  const normalizeBasePath = (basePath) => {
17
18
  if (!basePath)
18
19
  return '';
@@ -142,7 +143,8 @@ const buildChildPageItems = (parentId, pages) => {
142
143
  const buildPageSections = (sources, keepEmpty) => {
143
144
  const pages = sources.pages.filter((page) => page.enabled !== false);
144
145
  return pages
145
- .filter((page) => !page.id.includes('/'))
146
+ // The home page is reachable through the header logo and must not become a duplicate nav section.
147
+ .filter((page) => page.id !== content_kinds_1.HOME_PAGE_ID && !page.id.includes('/'))
146
148
  .map((page) => {
147
149
  var _a, _b;
148
150
  const declared = page.menu
@@ -37,8 +37,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
37
37
  Object.defineProperty(exports, "__esModule", { value: true });
38
38
  exports.getWorkspaceNavData = void 0;
39
39
  const fs = __importStar(require("fs"));
40
- const gray_matter_1 = __importDefault(require("gray-matter"));
41
40
  const path_1 = __importDefault(require("path"));
41
+ const markdown_1 = require("../utils/markdown");
42
42
  const menu_shell_1 = require("../utils/menu-shell");
43
43
  const resolver_1 = require("./resolver");
44
44
  const safeLoad = (load) => __awaiter(void 0, void 0, void 0, function* () {
@@ -58,7 +58,7 @@ const markdownTitle = (id, docRoot, workingPagesDir) => {
58
58
  for (const candidate of candidates) {
59
59
  if (!candidate || !fs.existsSync(candidate))
60
60
  continue;
61
- const title = (0, gray_matter_1.default)(fs.readFileSync(candidate, 'utf8')).data.title;
61
+ const title = (0, markdown_1.parseMarkdown)(fs.readFileSync(candidate, 'utf8')).data.title;
62
62
  if (typeof title === 'string' && title.trim())
63
63
  return title;
64
64
  }
@@ -1,6 +1,6 @@
1
1
  import type { RegistryDatabase } from '../db/client';
2
2
  import { type RegistryAccessScope } from '../db/schema';
3
- import { type RegistryPrincipal, type RegistryUserRole } from './types';
3
+ import { type RegistryTokenPrincipal, type RegistryUserRole } from './types';
4
4
  export interface RegistryAccessTokenSummary {
5
5
  id: string;
6
6
  name: string;
@@ -18,7 +18,15 @@ export type CreateRegistryAccessTokenResult = {
18
18
  ok: false;
19
19
  reason: 'user_not_found' | 'user_inactive' | 'scope_forbidden' | 'invalid_name' | 'invalid_expiry';
20
20
  };
21
- export declare const normalizeRegistryScopes: (scopes: readonly RegistryAccessScope[]) => RegistryAccessScope[];
21
+ /** Resolve one canonical scope or documented alias; anything else is not a scope we grant. */
22
+ export declare const resolveRegistryScope: (value: unknown) => RegistryAccessScope | null;
23
+ /** Resolve, dedupe and canonically order a scope list, dropping anything unrecognized. */
24
+ export declare const normalizeRegistryScopes: (scopes: readonly unknown[]) => RegistryAccessScope[];
25
+ /**
26
+ * Normalize a requested scope list into what a token is issued with: write implies read (a write-only
27
+ * token would be rejected by every read endpoint), and an empty request means read.
28
+ */
29
+ export declare const expandRegistryScopes: (scopes: readonly unknown[]) => RegistryAccessScope[];
22
30
  export declare const registryRoleAllowsScopes: (role: RegistryUserRole, scopes: readonly RegistryAccessScope[]) => boolean;
23
31
  export declare const formatRegistryAccessToken: (id: string, secret: string) => string;
24
32
  export declare const parseRegistryAccessToken: (token: string) => {
@@ -43,4 +51,4 @@ export declare const revokeAllRegistryAccessTokens: (db: RegistryDatabase, userI
43
51
  * Resolve an opaque bearer credential into a live principal. User state is joined on every call,
44
52
  * making deactivation effective even if a defensive token-revocation write was interrupted.
45
53
  */
46
- export declare const authenticateRegistryAccessToken: (db: RegistryDatabase, token: string, now?: Date) => Promise<RegistryPrincipal | null>;
54
+ export declare const authenticateRegistryAccessToken: (db: RegistryDatabase, token: string, now?: Date) => Promise<RegistryTokenPrincipal | null>;
@@ -9,7 +9,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
9
9
  });
10
10
  };
11
11
  Object.defineProperty(exports, "__esModule", { value: true });
12
- exports.authenticateRegistryAccessToken = exports.revokeAllRegistryAccessTokens = exports.revokeRegistryAccessToken = exports.listRegistryAccessTokens = exports.createRegistryAccessToken = exports.parseRegistryAccessToken = exports.formatRegistryAccessToken = exports.registryRoleAllowsScopes = exports.normalizeRegistryScopes = void 0;
12
+ exports.authenticateRegistryAccessToken = exports.revokeAllRegistryAccessTokens = exports.revokeRegistryAccessToken = exports.listRegistryAccessTokens = exports.createRegistryAccessToken = exports.parseRegistryAccessToken = exports.formatRegistryAccessToken = exports.registryRoleAllowsScopes = exports.expandRegistryScopes = exports.normalizeRegistryScopes = exports.resolveRegistryScope = void 0;
13
13
  const drizzle_orm_1 = require("drizzle-orm");
14
14
  const schema_1 = require("../db/schema");
15
15
  const crypto_1 = require("./crypto");
@@ -17,12 +17,37 @@ const database_1 = require("./database");
17
17
  const types_1 = require("./types");
18
18
  const ACCESS_TOKEN_PREFIX = 'hnd';
19
19
  const DEFAULT_ACCESS_TOKEN_LIFETIME_MS = 365 * 24 * 60 * 60 * 1000;
20
- const isRegistryScope = (value) => value === types_1.REGISTRY_READ_SCOPE || value === types_1.REGISTRY_WRITE_SCOPE;
20
+ /**
21
+ * Documented scope names that mean the same as ours. The published API vocabulary calls these
22
+ * `sync:*`; they are resolved on the way in and never stored, so rows and responses stay canonical.
23
+ */
24
+ const SCOPE_ALIASES = {
25
+ 'sync:read': types_1.REGISTRY_READ_SCOPE,
26
+ 'sync:write': types_1.REGISTRY_WRITE_SCOPE,
27
+ };
28
+ /** Resolve one canonical scope or documented alias; anything else is not a scope we grant. */
29
+ const resolveRegistryScope = (value) => {
30
+ var _a;
31
+ if (value === types_1.REGISTRY_READ_SCOPE || value === types_1.REGISTRY_WRITE_SCOPE)
32
+ return value;
33
+ return typeof value === 'string' ? ((_a = SCOPE_ALIASES[value]) !== null && _a !== void 0 ? _a : null) : null;
34
+ };
35
+ exports.resolveRegistryScope = resolveRegistryScope;
36
+ /** Resolve, dedupe and canonically order a scope list, dropping anything unrecognized. */
21
37
  const normalizeRegistryScopes = (scopes) => {
22
- const unique = new Set(scopes.filter(isRegistryScope));
38
+ const unique = new Set(scopes.map(exports.resolveRegistryScope).filter((scope) => scope !== null));
23
39
  return [types_1.REGISTRY_READ_SCOPE, types_1.REGISTRY_WRITE_SCOPE].filter((scope) => unique.has(scope));
24
40
  };
25
41
  exports.normalizeRegistryScopes = normalizeRegistryScopes;
42
+ /**
43
+ * Normalize a requested scope list into what a token is issued with: write implies read (a write-only
44
+ * token would be rejected by every read endpoint), and an empty request means read.
45
+ */
46
+ const expandRegistryScopes = (scopes) => {
47
+ const normalized = (0, exports.normalizeRegistryScopes)(scopes);
48
+ return normalized.includes(types_1.REGISTRY_WRITE_SCOPE) ? [types_1.REGISTRY_READ_SCOPE, types_1.REGISTRY_WRITE_SCOPE] : [types_1.REGISTRY_READ_SCOPE];
49
+ };
50
+ exports.expandRegistryScopes = expandRegistryScopes;
26
51
  const registryRoleAllowsScopes = (role, scopes) => role === 'admin' || !scopes.includes(types_1.REGISTRY_WRITE_SCOPE);
27
52
  exports.registryRoleAllowsScopes = registryRoleAllowsScopes;
28
53
  const formatRegistryAccessToken = (id, secret) => `${ACCESS_TOKEN_PREFIX}_${id}_${secret}`;
@@ -154,6 +179,7 @@ const authenticateRegistryAccessToken = (db_1, token_1, ...args_1) => __awaiter(
154
179
  if (!updated.length)
155
180
  return null;
156
181
  return {
182
+ kind: 'access_token',
157
183
  tokenId: row.tokenId,
158
184
  userId: row.userId,
159
185
  email: row.email,
@@ -1,11 +1,12 @@
1
- export { createRegistryAccessToken, listRegistryAccessTokens, revokeRegistryAccessToken, revokeAllRegistryAccessTokens, authenticateRegistryAccessToken, formatRegistryAccessToken, parseRegistryAccessToken, normalizeRegistryScopes, registryRoleAllowsScopes, type RegistryAccessTokenSummary, type CreateRegistryAccessTokenResult, } from './access-tokens';
1
+ export { createRegistryAccessToken, listRegistryAccessTokens, revokeRegistryAccessToken, revokeAllRegistryAccessTokens, authenticateRegistryAccessToken, formatRegistryAccessToken, parseRegistryAccessToken, normalizeRegistryScopes, expandRegistryScopes, resolveRegistryScope, registryRoleAllowsScopes, type RegistryAccessTokenSummary, type CreateRegistryAccessTokenResult, } from './access-tokens';
2
2
  export { createUserInvitation, resendUserInvitation, acceptUserInvitation, createPasswordReset, resetPassword, INVITATION_LIFETIME_MS, PASSWORD_RESET_LIFETIME_MS, type CreateUserInvitationResult, type ResendUserInvitationResult, type ConsumeAuthActionResult, } from './action-tokens';
3
3
  export { createOpaqueSecret, hashSecret, normalizeEmail, secretHashMatches } from './crypto';
4
4
  export { buildGravatarUrl } from './gravatar';
5
+ export { authenticateRegistrySyncSecret, resolveRegistrySyncSecret, SYNC_SECRET_ENV } from './sync-secret';
5
6
  export { createRegistryDeviceAuthorization, approveRegistryDeviceAuthorization, denyRegistryDeviceAuthorization, exchangeRegistryDeviceAuthorization, purgeExpiredRegistryDeviceAuthorizations, generateRegistryUserCode, normalizeRegistryUserCode, DEVICE_AUTHORIZATION_LIFETIME_SECONDS, DEVICE_AUTHORIZATION_POLL_INTERVAL_SECONDS, type CreateRegistryDeviceAuthorizationResult, type RegistryDeviceApprovalResult, type ExchangeRegistryDeviceAuthorizationResult, } from './device-authorizations';
6
7
  export { getRegistryInstallation, getRegistryInstallationState, installRegistry, REGISTRY_AUTH_SCHEMA_VERSION, REGISTRY_INSTALLATION_ID, type RegistryInstallation, type RegistryInstallationState, type RegistryInstallInput, type RegistryInstallResult, } from './installation';
7
8
  export { hashPassword, verifyPassword, validatePassword, MINIMUM_PASSWORD_LENGTH, MAXIMUM_PASSWORD_LENGTH, type PasswordValidationResult, } from './password';
8
9
  export { consumeAuthRateLimit, clearAuthRateLimit, purgeExpiredAuthRateLimits, hashRateLimitIdentifier, type ConsumeAuthRateLimitInput, type AuthRateLimitResult, } from './rate-limits';
9
10
  export { getRegistryUserById, listRegistryUsers, authenticateRegistryCredentials, updateRegistryUserProfile, updateRegistryUserRole, setRegistryUserStatus, type RegistryUserMutationResult, } from './users';
10
- export { REGISTRY_READ_SCOPE, REGISTRY_WRITE_SCOPE, scopesForRegistryRole, registryPrincipalHasScope, type RegistryAccessScope, type RegistryPrincipal, type RegistryUser, type RegistryUserRole, type RegistryUserStatus, } from './types';
11
- export { validateRegistryDisplayName, validateRegistryEmail, validateRegistryImageUrl } from './validation';
11
+ export { REGISTRY_READ_SCOPE, REGISTRY_WRITE_SCOPE, scopesForRegistryRole, registryPrincipalHasScope, isRegistryTokenPrincipal, type RegistryAccessScope, type RegistryPrincipal, type RegistryTokenPrincipal, type RegistrySyncSecretPrincipal, type RegistryUser, type RegistryUserRole, type RegistryUserStatus, } from './types';
12
+ export { isTransmittableCredential, validateRegistryDisplayName, validateRegistryEmail, validateRegistryImageUrl } from './validation';
@@ -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);