@extension.dev/mcp 6.2.0 → 6.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -10,7 +10,7 @@
10
10
  "name": "extension-mcp",
11
11
  "source": "./",
12
12
  "description": "MCP tools for browser extension development: scaffold from 60+ templates, run the dev server with HMR, inspect the live DOM and logs, and publish store-ready builds for Chrome, Edge, and Firefox.",
13
- "version": "6.2.0",
13
+ "version": "6.3.0",
14
14
  "category": "development",
15
15
  "author": {
16
16
  "name": "Cezar Augusto"
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "extension-mcp",
3
3
  "description": "MCP tools for browser extension development: scaffold from 60+ templates, run the dev server with HMR, inspect the live DOM and logs, and publish store-ready builds for Chrome, Edge, and Firefox. Ships /extension, /extension-add, /extension-debug, and /extension-publish commands.",
4
- "version": "6.2.0",
4
+ "version": "6.3.0",
5
5
  "author": {
6
6
  "name": "Cezar Augusto",
7
7
  "email": "hello@extension.dev",
package/CHANGELOG.md CHANGED
@@ -1,5 +1,69 @@
1
1
  # Changelog
2
2
 
3
+ ## 6.4.0
4
+
5
+ A build on your machine can now become a link someone else can open.
6
+
7
+ ### Changed
8
+
9
+ - **`extension_preview_web` with `share:true` uploads the build you just
10
+ made.** It used to route sharing through `extension_publish`, which takes no
11
+ project path and uploads nothing: it flips a share link on whatever build the
12
+ token's project last pushed from its own CI. So the link rendered a different
13
+ build than the one on disk, and said so in a caveat. It now POSTs the resolved
14
+ `dist/<browser>` to the platform's artifact store and returns a
15
+ `preview.extension.dev` link that renders those exact bytes, for a recipient
16
+ with no install, no sign-in, and no dev server. `share.serves` reports
17
+ `uploaded-local-build`; the `requiredBackend` spec that described this as
18
+ missing is gone because it is built.
19
+ - `share` now returns `expiresAt` and `revokeUrl`. Shared builds expire, and
20
+ `DELETE`ing `revokeUrl` with the same token kills the link immediately, which
21
+ is the part a TTL alone cannot do when a link reaches the wrong person.
22
+ - The `channel` argument is removed from `extension_preview_web`. It only ever
23
+ selected which promoted CI build to render, and this tool no longer renders
24
+ promoted builds. `extension_publish` and `extension_release_promote` are
25
+ unchanged and remain the path for released, channel-scoped builds.
26
+
27
+ ## 6.3.0
28
+
29
+ Private projects stop reading as empty, and the links this server hands back
30
+ stop being login-only.
31
+
32
+ ### Fixed
33
+
34
+ - **Registry reads work for PRIVATE projects.** `extension_release_list`,
35
+ `extension_store_status`, `extension_deploy` and `extension_publish` read the
36
+ project's state from `registry.extension.land`, which answers 401 for a
37
+ private project. That 401 was reported as "no registry data", so a project the
38
+ operator owns and is logged into looked like it had no builds at all. A 401 or
39
+ 403 now mints a short-lived read token and retries once. Public projects are
40
+ untouched: still one request, still no call to the platform.
41
+ - The stored login token is never used as the `?t=` URL parameter even though it
42
+ would verify. It is long-lived, and a credential in a query string is kept by
43
+ every proxy and access log on the path, so it is traded for a ten-minute token
44
+ first. Requires the bearer path on `POST /api/access-grant`.
45
+
46
+ ### Added
47
+
48
+ - **Public build links.** `extension_release_list` returns a `publicUrl` for
49
+ every channel and build plus a `publicProjectUrl`, and
50
+ `extension_release_promote` returns `publicChannelUrl` and `publicBuildUrl`.
51
+ Every link these tools returned before pointed at the console, which requires
52
+ a login and workspace membership, so it was a dead end for the teammate or
53
+ reviewer the operator wanted to send it to. The public pages carry the
54
+ per-browser downloads, the run-locally and integrity dialogs, and what's new.
55
+ For a private project the response says plainly that an outside recipient
56
+ still needs a share link from `extension_publish`.
57
+ - `api` is accepted by `extension_release_list` and `extension_store_status`,
58
+ matching the other hosted-facing tools. It picks the platform the read token
59
+ is minted against and the origin the returned links point at.
60
+
61
+ ### Changed
62
+
63
+ - Depends on `@extension.dev/urls` `^0.3.0` for the new `userland` origin and
64
+ its whole-URL builders, so a build link this server returns cannot drift from
65
+ the routes the viewer serves.
66
+
3
67
  ## 6.2.0
4
68
 
5
69
  The URL layer stops being a copy. This server used to carry byte-identical
package/dist/module.js CHANGED
@@ -17,7 +17,7 @@ import node_net from "node:net";
17
17
  import { extensionInstall, extensionUninstall, getManagedBrowsersCacheRoot } from "extension-install";
18
18
  import { promisify } from "node:util";
19
19
  __webpack_require__.add({
20
- "./node_modules/.pnpm/@extension.dev+urls@0.2.0/node_modules/@extension.dev/urls/dist/origins.cjs" (__unused_rspack_module, exports) {
20
+ "./node_modules/.pnpm/@extension.dev+urls@0.3.0/node_modules/@extension.dev/urls/dist/origins.cjs" (__unused_rspack_module, exports) {
21
21
  var __nested_rspack_require_18_37__ = {};
22
22
  (()=>{
23
23
  __nested_rspack_require_18_37__.d = (exports1, definition)=>{
@@ -55,6 +55,7 @@ __webpack_require__.add({
55
55
  preview: "https://preview.extension.dev",
56
56
  templates: "https://templates.extension.dev",
57
57
  intelligence: "https://intelligence.extension.dev",
58
+ userland: "https://extension.dev",
58
59
  registry: "https://registry.extension.land",
59
60
  media: "https://media.extension.land"
60
61
  };
@@ -65,6 +66,7 @@ __webpack_require__.add({
65
66
  preview: "http://preview.extension.localhost",
66
67
  templates: "http://templates.extension.localhost",
67
68
  intelligence: "http://intelligence.extension.localhost",
69
+ userland: "http://userland.extension.localhost",
68
70
  registry: "https://registry.extension.land",
69
71
  media: "https://media.extension.land"
70
72
  };
@@ -93,6 +95,7 @@ __webpack_require__.add({
93
95
  preview: pick("preview"),
94
96
  templates: pick("templates"),
95
97
  intelligence: pick("intelligence"),
98
+ userland: pick("userland"),
96
99
  registry: pick("registry"),
97
100
  media: pick("media")
98
101
  };
@@ -111,7 +114,7 @@ __webpack_require__.add({
111
114
  value: true
112
115
  });
113
116
  },
114
- "./node_modules/.pnpm/@extension.dev+urls@0.2.0/node_modules/@extension.dev/urls/dist/paths.cjs" (__unused_rspack_module, exports) {
117
+ "./node_modules/.pnpm/@extension.dev+urls@0.3.0/node_modules/@extension.dev/urls/dist/paths.cjs" (__unused_rspack_module, exports) {
115
118
  var __nested_rspack_require_18_37__ = {};
116
119
  (()=>{
117
120
  __nested_rspack_require_18_37__.d = (exports1, definition)=>{
@@ -143,6 +146,7 @@ __webpack_require__.add({
143
146
  inspectTabPath: ()=>inspectTabPath,
144
147
  previewSharePath: ()=>previewSharePath,
145
148
  templateTabPath: ()=>templateTabPath,
149
+ withQuery: ()=>withQuery,
146
150
  wwwDevicePath: ()=>wwwDevicePath,
147
151
  wwwImportPath: ()=>wwwImportPath,
148
152
  wwwNewPath: ()=>wwwNewPath,
@@ -212,6 +216,7 @@ __webpack_require__.add({
212
216
  exports.inspectTabPath = __nested_rspack_exports__.inspectTabPath;
213
217
  exports.previewSharePath = __nested_rspack_exports__.previewSharePath;
214
218
  exports.templateTabPath = __nested_rspack_exports__.templateTabPath;
219
+ exports.withQuery = __nested_rspack_exports__.withQuery;
215
220
  exports.wwwDevicePath = __nested_rspack_exports__.wwwDevicePath;
216
221
  exports.wwwImportPath = __nested_rspack_exports__.wwwImportPath;
217
222
  exports.wwwNewPath = __nested_rspack_exports__.wwwNewPath;
@@ -223,6 +228,7 @@ __webpack_require__.add({
223
228
  "inspectTabPath",
224
229
  "previewSharePath",
225
230
  "templateTabPath",
231
+ "withQuery",
226
232
  "wwwDevicePath",
227
233
  "wwwImportPath",
228
234
  "wwwNewPath",
@@ -231,6 +237,171 @@ __webpack_require__.add({
231
237
  Object.defineProperty(exports, "__esModule", {
232
238
  value: true
233
239
  });
240
+ },
241
+ "./node_modules/.pnpm/@extension.dev+urls@0.3.0/node_modules/@extension.dev/urls/dist/userland.cjs" (__unused_rspack_module, exports) {
242
+ var __nested_rspack_require_18_37__ = {};
243
+ (()=>{
244
+ __nested_rspack_require_18_37__.d = (exports1, definition)=>{
245
+ for(var key in definition)if (__nested_rspack_require_18_37__.o(definition, key) && !__nested_rspack_require_18_37__.o(exports1, key)) Object.defineProperty(exports1, key, {
246
+ enumerable: true,
247
+ get: definition[key]
248
+ });
249
+ };
250
+ })();
251
+ (()=>{
252
+ __nested_rspack_require_18_37__.o = (obj, prop)=>Object.prototype.hasOwnProperty.call(obj, prop);
253
+ })();
254
+ (()=>{
255
+ __nested_rspack_require_18_37__.r = (exports1)=>{
256
+ if ("u" > typeof Symbol && Symbol.toStringTag) Object.defineProperty(exports1, Symbol.toStringTag, {
257
+ value: 'Module'
258
+ });
259
+ Object.defineProperty(exports1, '__esModule', {
260
+ value: true
261
+ });
262
+ };
263
+ })();
264
+ var __nested_rspack_exports__ = {};
265
+ __nested_rspack_require_18_37__.r(__nested_rspack_exports__);
266
+ __nested_rspack_require_18_37__.d(__nested_rspack_exports__, {
267
+ userlandBuildUrl: ()=>userlandBuildUrl,
268
+ USERLAND_BUILD_TABS: ()=>USERLAND_BUILD_TABS,
269
+ userlandUrl: ()=>userlandUrl,
270
+ UserlandProjectPage: ()=>UserlandProjectPage,
271
+ UserlandDialog: ()=>UserlandDialog,
272
+ userlandChannelUrl: ()=>userlandChannelUrl,
273
+ userlandProjectPath: ()=>userlandProjectPath,
274
+ isUserlandBuildTab: ()=>isUserlandBuildTab,
275
+ userlandDialogUrl: ()=>userlandDialogUrl,
276
+ userlandHostMode: ()=>userlandHostMode,
277
+ userlandOrigin: ()=>userlandOrigin
278
+ });
279
+ const PROD_ORIGINS = {
280
+ www: "https://www.extension.dev",
281
+ console: "https://console.extension.dev",
282
+ inspect: "https://inspect.extension.dev",
283
+ preview: "https://preview.extension.dev",
284
+ templates: "https://templates.extension.dev",
285
+ intelligence: "https://intelligence.extension.dev",
286
+ userland: "https://extension.dev",
287
+ registry: "https://registry.extension.land",
288
+ media: "https://media.extension.land"
289
+ };
290
+ function strip(value) {
291
+ return String(value ?? "").trim().replace(/\/+$/, "");
292
+ }
293
+ function isLocalOrigin(url) {
294
+ const raw = strip(url);
295
+ if (!raw) return false;
296
+ let host;
297
+ try {
298
+ host = new URL(raw).hostname;
299
+ } catch {
300
+ return false;
301
+ }
302
+ return "localhost" === host || "127.0.0.1" === host || "::1" === host || "[::1]" === host || "extension.localhost" === host || host.endsWith(".extension.localhost");
303
+ }
304
+ function withQuery(path, query) {
305
+ if (!query) return path;
306
+ const params = new URLSearchParams();
307
+ for (const [key, value] of Object.entries(query))if (null != value && "" !== value) params.set(key, String(value));
308
+ const qs = params.toString();
309
+ return qs ? `${path}?${qs}` : path;
310
+ }
311
+ const userland_seg = (value)=>encodeURIComponent(String(value));
312
+ const USERLAND_BUILD_TABS = [
313
+ "preview",
314
+ "whats-new",
315
+ "usage"
316
+ ];
317
+ function isUserlandBuildTab(value) {
318
+ return USERLAND_BUILD_TABS.includes(value);
319
+ }
320
+ const UserlandDialog = {
321
+ run: "run",
322
+ integrity: "integrity"
323
+ };
324
+ const DNS_LABEL = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/i;
325
+ function userland_strip(value) {
326
+ return String(value ?? "").trim().replace(/\/+$/, "");
327
+ }
328
+ function userlandHostMode(base) {
329
+ return isLocalOrigin(userland_strip(base) || PROD_ORIGINS.userland) ? "path" : "subdomain";
330
+ }
331
+ function userlandOrigin(workspace, options = {}) {
332
+ const base = userland_strip(options.base) || PROD_ORIGINS.userland;
333
+ if ("path" === userlandHostMode(base)) return base;
334
+ const slug = String(workspace ?? "").trim().toLowerCase();
335
+ if (!DNS_LABEL.test(slug)) throw new Error(`Workspace slug ${JSON.stringify(workspace)} cannot be a subdomain label; userland production hosts are <workspace>.extension.dev.`);
336
+ const url = new URL(base);
337
+ url.hostname = `${slug}.${url.hostname}`;
338
+ return userland_strip(url.toString());
339
+ }
340
+ const UserlandProjectPage = {
341
+ overview: "",
342
+ build: (buildId)=>`builds/${userland_seg(buildId)}`,
343
+ buildTab: (buildId, tab = "preview")=>"preview" === tab ? `builds/${userland_seg(buildId)}` : `builds/${userland_seg(buildId)}/${tab}`,
344
+ browserBuild: (buildId, browser)=>`builds/${userland_seg(buildId)}/${userland_seg(browser)}`,
345
+ channel: (channel)=>`channels/${userland_seg(channel)}`,
346
+ channelShortcut: (channel)=>userland_seg(channel),
347
+ channelBrowser: (browser, channel)=>`channels/${userland_seg(browser)}/${userland_seg(channel)}`,
348
+ version: (version)=>`versions/${userland_seg(version)}`,
349
+ versionBrowser: (browser, version)=>`versions/${userland_seg(browser)}/${userland_seg(version)}`
350
+ };
351
+ function userlandProjectPath(ref, page = "", options = {}) {
352
+ const head = "subdomain" === userlandHostMode(options.base) ? `/${userland_seg(ref.project)}` : `/${userland_seg(ref.workspace)}/${userland_seg(ref.project)}`;
353
+ const tail = String(page ?? "").replace(/^\/+/, "");
354
+ return tail ? `${head}/${tail}` : head;
355
+ }
356
+ function userlandUrl(ref, page = "", options = {}) {
357
+ const origin = userlandOrigin(ref.workspace, options);
358
+ const path = userlandProjectPath(ref, page, options);
359
+ return `${origin}${withQuery(path, options.query)}`;
360
+ }
361
+ function userlandBuildUrl(ref, buildId, options = {}) {
362
+ const page = options.browser ? UserlandProjectPage.browserBuild(buildId, options.browser) : UserlandProjectPage.build(buildId);
363
+ return userlandUrl(ref, page, options);
364
+ }
365
+ function userlandDialogUrl(ref, buildId, browser, dialog, options = {}) {
366
+ return userlandUrl(ref, UserlandProjectPage.browserBuild(buildId, browser), {
367
+ ...options,
368
+ query: {
369
+ ...options.query ?? {},
370
+ dialog
371
+ }
372
+ });
373
+ }
374
+ function userlandChannelUrl(ref, channel, options = {}) {
375
+ const page = options.browser ? UserlandProjectPage.channelBrowser(options.browser, channel) : options.shortcut ? UserlandProjectPage.channelShortcut(channel) : UserlandProjectPage.channel(channel);
376
+ return userlandUrl(ref, page, options);
377
+ }
378
+ exports.USERLAND_BUILD_TABS = __nested_rspack_exports__.USERLAND_BUILD_TABS;
379
+ exports.UserlandDialog = __nested_rspack_exports__.UserlandDialog;
380
+ exports.UserlandProjectPage = __nested_rspack_exports__.UserlandProjectPage;
381
+ exports.isUserlandBuildTab = __nested_rspack_exports__.isUserlandBuildTab;
382
+ exports.userlandBuildUrl = __nested_rspack_exports__.userlandBuildUrl;
383
+ exports.userlandChannelUrl = __nested_rspack_exports__.userlandChannelUrl;
384
+ exports.userlandDialogUrl = __nested_rspack_exports__.userlandDialogUrl;
385
+ exports.userlandHostMode = __nested_rspack_exports__.userlandHostMode;
386
+ exports.userlandOrigin = __nested_rspack_exports__.userlandOrigin;
387
+ exports.userlandProjectPath = __nested_rspack_exports__.userlandProjectPath;
388
+ exports.userlandUrl = __nested_rspack_exports__.userlandUrl;
389
+ for(var __webpack_i__ in __nested_rspack_exports__)if (-1 === [
390
+ "USERLAND_BUILD_TABS",
391
+ "UserlandDialog",
392
+ "UserlandProjectPage",
393
+ "isUserlandBuildTab",
394
+ "userlandBuildUrl",
395
+ "userlandChannelUrl",
396
+ "userlandDialogUrl",
397
+ "userlandHostMode",
398
+ "userlandOrigin",
399
+ "userlandProjectPath",
400
+ "userlandUrl"
401
+ ].indexOf(__webpack_i__)) exports[__webpack_i__] = __nested_rspack_exports__[__webpack_i__];
402
+ Object.defineProperty(exports, "__esModule", {
403
+ value: true
404
+ });
234
405
  }
235
406
  });
236
407
  var add_feature_namespaceObject = {};
@@ -439,7 +610,7 @@ __webpack_require__.d(whoami_namespaceObject, {
439
610
  handler: ()=>whoami_handler,
440
611
  schema: ()=>whoami_schema
441
612
  });
442
- var package_namespaceObject = JSON.parse('{"rE":"6.1.0","El":{"OP":"4.0.16-canary.1784889479.74e12044"}}');
613
+ var package_namespaceObject = JSON.parse('{"rE":"6.3.0","El":{"OP":"4.0.16-canary.1784889479.74e12044"}}');
443
614
  function credentialsPath() {
444
615
  if ("win32" === process.platform) {
445
616
  const base = process.env.APPDATA || process.env.LOCALAPPDATA || node_path.join(node_os.homedir(), "AppData", "Roaming");
@@ -512,8 +683,240 @@ function readValidCredentials(nowSeconds = Math.floor(Date.now() / 1000)) {
512
683
  if (creds.expiresAt && creds.expiresAt <= nowSeconds) return null;
513
684
  return creds;
514
685
  }
515
- const origins = __webpack_require__("./node_modules/.pnpm/@extension.dev+urls@0.2.0/node_modules/@extension.dev/urls/dist/origins.cjs");
516
- const paths_0 = __webpack_require__("./node_modules/.pnpm/@extension.dev+urls@0.2.0/node_modules/@extension.dev/urls/dist/paths.cjs");
686
+ const origins = __webpack_require__("./node_modules/.pnpm/@extension.dev+urls@0.3.0/node_modules/@extension.dev/urls/dist/origins.cjs");
687
+ const DEFAULT_API = origins.PROD_ORIGINS.www;
688
+ function tokenTtlNote(workspaceSlug, projectSlug) {
689
+ const tokensUrl = workspaceSlug && projectSlug ? consoleProjectUrl({
690
+ workspace: workspaceSlug,
691
+ project: projectSlug
692
+ }, "settings/access-tokens") : consoleBase();
693
+ return `extension.dev CLI tokens live at most 7 days (server-enforced). CI pipelines must re-mint before expiry on the console's Access tokens page: ${tokensUrl}`;
694
+ }
695
+ function resolveApiBase(api) {
696
+ return String(api || process.env.EXTENSION_DEV_API_URL || DEFAULT_API).replace(/\/+$/, "");
697
+ }
698
+ function safeApiBase(raw) {
699
+ let parsed;
700
+ try {
701
+ parsed = new URL(raw);
702
+ } catch {
703
+ return {
704
+ ok: false,
705
+ message: `Invalid platform URL: ${raw}`
706
+ };
707
+ }
708
+ const isLocalhost = "localhost" === parsed.hostname || "127.0.0.1" === parsed.hostname || "[::1]" === parsed.hostname || "::1" === parsed.hostname;
709
+ if ("https:" !== parsed.protocol && !("http:" === parsed.protocol && isLocalhost)) return {
710
+ ok: false,
711
+ message: `Refusing to send the access token to ${raw}: use https (http is allowed only for localhost).`
712
+ };
713
+ return {
714
+ ok: true,
715
+ base: `${parsed.origin}${parsed.pathname}`.replace(/\/+$/, "")
716
+ };
717
+ }
718
+ async function fetchLoginConfig(apiBase, fetchImpl = fetch) {
719
+ const override = String(process.env.EXTENSION_DEV_GITHUB_CLIENT_ID || "").trim();
720
+ if (override) return {
721
+ provider: "github",
722
+ clientId: override,
723
+ scope: "read:user",
724
+ deviceCodeUrl: "/api/cli/device/code",
725
+ deviceTokenUrl: "/api/cli/device/token",
726
+ verificationUri: "https://github.com/login/device"
727
+ };
728
+ const res = await fetchImpl(`${apiBase}/api/cli/login/config`, {
729
+ headers: {
730
+ accept: "application/json"
731
+ }
732
+ });
733
+ if (!res.ok) throw new Error(`Could not fetch login config from ${apiBase} (${res.status}).`);
734
+ const data = await res.json().catch(()=>({}));
735
+ const provider = "extensiondev" === data.provider ? "extensiondev" : "github";
736
+ const clientId = String(data.githubClientId || "").trim();
737
+ if ("github" === provider && !clientId) throw new Error("Login is not configured on the server (no GitHub client id). Set EXTENSION_DEV_GITHUB_CLIENT_ID to override.");
738
+ return {
739
+ provider,
740
+ clientId,
741
+ scope: String(data.scope || "read:user"),
742
+ deviceCodeUrl: String(data.deviceCodeUrl || "/api/cli/device/code"),
743
+ deviceTokenUrl: String(data.deviceTokenUrl || "/api/cli/device/token"),
744
+ verificationUri: String(data.verificationUri || "https://github.com/login/device")
745
+ };
746
+ }
747
+ function persistTokenResponse(args) {
748
+ const token = String(args.data.token || "").trim();
749
+ if (!token) throw new Error("Login returned no token.");
750
+ const creds = {
751
+ version: 1,
752
+ token,
753
+ workspaceSlug: String(args.data.workspaceSlug || ""),
754
+ projectSlug: String(args.data.projectSlug || ""),
755
+ expiresAt: Number(args.data.expiresAt || 0),
756
+ api: args.apiBase,
757
+ provider: args.provider
758
+ };
759
+ writeCredentials(creds);
760
+ return creds;
761
+ }
762
+ async function exchangeAndPersist(args) {
763
+ const doFetch = args.fetchImpl ?? fetch;
764
+ const res = await doFetch(`${args.apiBase}/api/cli/login/exchange`, {
765
+ method: "POST",
766
+ headers: {
767
+ "content-type": "application/json",
768
+ accept: "application/json"
769
+ },
770
+ body: JSON.stringify({
771
+ githubToken: args.githubToken,
772
+ project: args.project
773
+ })
774
+ });
775
+ const text = await res.text();
776
+ let data;
777
+ try {
778
+ data = JSON.parse(text);
779
+ } catch {
780
+ data = {
781
+ message: text
782
+ };
783
+ }
784
+ if (!res.ok) throw new Error(`Login exchange failed (${res.status}): ${data.message || "unknown error"}`);
785
+ return persistTokenResponse({
786
+ apiBase: args.apiBase,
787
+ data,
788
+ provider: "github"
789
+ });
790
+ }
791
+ function _define_property(obj, key, value) {
792
+ if (key in obj) Object.defineProperty(obj, key, {
793
+ value: value,
794
+ enumerable: true,
795
+ configurable: true,
796
+ writable: true
797
+ });
798
+ else obj[key] = value;
799
+ return obj;
800
+ }
801
+ const REFRESH_LEAD_SECONDS = 60;
802
+ function cacheKey(ref) {
803
+ return `${ref.workspace.toLowerCase()}/${ref.project.toLowerCase()}`;
804
+ }
805
+ class RegistryAccessTokens {
806
+ peek(ref) {
807
+ const entry = this.cache.get(cacheKey(ref));
808
+ if (!entry || "fresh" !== entry.kind) return "";
809
+ return entry.expiresAt - REFRESH_LEAD_SECONDS > this.nowSeconds() ? entry.token : "";
810
+ }
811
+ async get(ref, apiHint) {
812
+ const key = cacheKey(ref);
813
+ const entry = this.cache.get(key);
814
+ if (entry?.kind === "pending") return entry.promise;
815
+ const cached = this.peek(ref);
816
+ if (cached) return {
817
+ status: "ok",
818
+ token: cached,
819
+ expiresAt: this.cache.get(key).expiresAt
820
+ };
821
+ const promise = this.mint(ref, apiHint);
822
+ this.cache.set(key, {
823
+ kind: "pending",
824
+ promise
825
+ });
826
+ const result = await promise;
827
+ if ("ok" === result.status) this.cache.set(key, {
828
+ kind: "fresh",
829
+ token: result.token,
830
+ expiresAt: result.expiresAt
831
+ });
832
+ else this.cache.delete(key);
833
+ return result;
834
+ }
835
+ async mint(ref, apiHint) {
836
+ const creds = readCredentials();
837
+ const token = String(process.env.EXTENSION_DEV_TOKEN || creds?.token || "").trim();
838
+ if (!token) return {
839
+ status: "no-credential"
840
+ };
841
+ if (creds?.workspaceSlug && creds?.projectSlug && !process.env.EXTENSION_DEV_TOKEN) {
842
+ const same = creds.workspaceSlug.toLowerCase() === ref.workspace.toLowerCase() && creds.projectSlug.toLowerCase() === ref.project.toLowerCase();
843
+ if (!same) return {
844
+ status: "no-credential"
845
+ };
846
+ }
847
+ const check = safeApiBase(resolveApiBase(apiHint || creds?.api));
848
+ if (!check.ok) return {
849
+ status: "denied",
850
+ message: check.message
851
+ };
852
+ let res;
853
+ try {
854
+ res = await this.fetchImpl(`${check.base}/api/access-grant`, {
855
+ method: "POST",
856
+ headers: {
857
+ authorization: `Bearer ${token}`,
858
+ "content-type": "application/json"
859
+ },
860
+ body: JSON.stringify({
861
+ workspaceSlug: ref.workspace,
862
+ projectSlug: ref.project
863
+ })
864
+ });
865
+ } catch (err) {
866
+ return {
867
+ status: "denied",
868
+ message: `Could not reach the access-grant endpoint: ${err?.message || err}`
869
+ };
870
+ }
871
+ if (400 === res.status) return {
872
+ status: "public"
873
+ };
874
+ if (!res.ok) return {
875
+ status: "denied",
876
+ httpStatus: res.status,
877
+ message: `access-grant returned ${res.status}`
878
+ };
879
+ let data;
880
+ try {
881
+ data = JSON.parse(await res.text());
882
+ } catch {
883
+ return {
884
+ status: "denied",
885
+ message: "access-grant did not return JSON"
886
+ };
887
+ }
888
+ const minted = String(data?.token || "").trim();
889
+ if (!minted) return {
890
+ status: "denied",
891
+ message: "access-grant returned no token"
892
+ };
893
+ return {
894
+ status: "ok",
895
+ token: minted,
896
+ expiresAt: Number(data?.expiresAt || 0)
897
+ };
898
+ }
899
+ constructor(options){
900
+ _define_property(this, "cache", new Map());
901
+ _define_property(this, "fetchImpl", void 0);
902
+ _define_property(this, "nowSeconds", void 0);
903
+ this.fetchImpl = options?.fetchImpl ?? fetch;
904
+ this.nowSeconds = options?.nowSeconds ?? (()=>Math.floor(Date.now() / 1000));
905
+ }
906
+ }
907
+ const defaultRegistryAccessTokens = new RegistryAccessTokens();
908
+ function withAccessToken(url, token) {
909
+ if (!token) return url;
910
+ try {
911
+ const u = new URL(url);
912
+ u.searchParams.set("t", token);
913
+ return u.toString();
914
+ } catch {
915
+ return url;
916
+ }
917
+ }
918
+ const paths_0 = __webpack_require__("./node_modules/.pnpm/@extension.dev+urls@0.3.0/node_modules/@extension.dev/urls/dist/paths.cjs");
919
+ const userland = __webpack_require__("./node_modules/.pnpm/@extension.dev+urls@0.3.0/node_modules/@extension.dev/urls/dist/userland.cjs");
517
920
  origins.PROD_ORIGINS.registry;
518
921
  function mcpOrigins(apiHint) {
519
922
  const www = String(apiHint || process.env.EXTENSION_DEV_API_URL || "").trim() || void 0;
@@ -522,6 +925,7 @@ function mcpOrigins(apiHint) {
522
925
  console: process.env.EXTENSION_DEV_CONSOLE_URL,
523
926
  inspect: process.env.EXTENSION_DEV_INSPECT_URL,
524
927
  preview: process.env.EXTENSION_DEV_PREVIEW_URL,
928
+ userland: process.env.EXTENSION_DEV_USERLAND_URL,
525
929
  registry: process.env.EXTENSION_DEV_REGISTRY_URL,
526
930
  media: process.env.EXTENSION_MEDIA_ORIGIN
527
931
  }, {
@@ -558,33 +962,75 @@ function consoleProjectUrl(ref, page, apiHint) {
558
962
  if (!ref) return base;
559
963
  return `${base}${(0, paths_0.consoleProjectPath)(ref, page)}`;
560
964
  }
561
- async function fetchRegistryJson(url, fetchImpl = fetch) {
965
+ function userlandProjectUrl(ref, page = "", apiHint) {
966
+ if (!ref) return "";
967
+ try {
968
+ return (0, userland.userlandUrl)(ref, page, {
969
+ base: mcpOrigins(apiHint).userland
970
+ });
971
+ } catch {
972
+ return "";
973
+ }
974
+ }
975
+ async function readJson(url, res) {
976
+ try {
977
+ const text = await res.text();
978
+ return {
979
+ ok: true,
980
+ json: JSON.parse(text)
981
+ };
982
+ } catch {
983
+ return {
984
+ ok: false,
985
+ message: `${url} did not return valid JSON`
986
+ };
987
+ }
988
+ }
989
+ async function fetchRegistryJson(url, fetchImpl = fetch, options) {
990
+ const tokens = options?.tokens ?? defaultRegistryAccessTokens;
991
+ const ref = options?.ref ?? null;
992
+ const cached = ref ? tokens.peek(ref) : "";
993
+ const firstUrl = cached ? withAccessToken(url, cached) : url;
562
994
  let res;
563
995
  try {
564
- res = await fetchImpl(url);
996
+ res = await fetchImpl(firstUrl);
565
997
  } catch (err) {
566
998
  return {
567
999
  ok: false,
568
1000
  message: `Could not reach ${url}: ${err?.message || err}`
569
1001
  };
570
1002
  }
571
- if (!res.ok) return {
1003
+ if (res.ok) return readJson(url, res);
1004
+ const authFailed = 401 === res.status || 403 === res.status;
1005
+ if (!authFailed || !ref) return {
572
1006
  ok: false,
573
1007
  status: res.status,
574
1008
  message: `${url} returned ${res.status}`
575
1009
  };
576
- try {
577
- const text = await res.text();
1010
+ const grant = await tokens.get(ref, options?.api);
1011
+ if ("ok" !== grant.status) {
1012
+ const detail = "no-credential" === grant.status ? "This project is private. Run extension_login for it, or set EXTENSION_DEV_TOKEN." : "public" === grant.status ? "The platform reports this project is public, but the registry refused the read." : grant.message;
578
1013
  return {
579
- ok: true,
580
- json: JSON.parse(text)
1014
+ ok: false,
1015
+ status: res.status,
1016
+ message: `${url} returned ${res.status}. ${detail}`
581
1017
  };
582
- } catch {
1018
+ }
1019
+ let retried;
1020
+ try {
1021
+ retried = await fetchImpl(withAccessToken(url, grant.token));
1022
+ } catch (err) {
583
1023
  return {
584
1024
  ok: false,
585
- message: `${url} did not return valid JSON`
1025
+ message: `Could not reach ${url}: ${err?.message || err}`
586
1026
  };
587
1027
  }
1028
+ if (!retried.ok) return {
1029
+ ok: false,
1030
+ status: retried.status,
1031
+ message: `${url} returned ${retried.status} even with an access token`
1032
+ };
1033
+ return readJson(url, retried);
588
1034
  }
589
1035
  function parseChannels(json) {
590
1036
  if (!json || "object" != typeof json || Array.isArray(json)) return [];
@@ -3353,7 +3799,7 @@ function summarizeConsoleMessages(messages) {
3353
3799
  topMessages: topMessages.sort((a, b)=>b.count - a.count).slice(0, 10)
3354
3800
  };
3355
3801
  }
3356
- function _define_property(obj, key, value) {
3802
+ function cdp_connection_define_property(obj, key, value) {
3357
3803
  if (key in obj) Object.defineProperty(obj, key, {
3358
3804
  value: value,
3359
3805
  enumerable: true,
@@ -3471,11 +3917,11 @@ class CDPConnection {
3471
3917
  };
3472
3918
  }
3473
3919
  constructor(){
3474
- _define_property(this, "ws", null);
3475
- _define_property(this, "messageId", 0);
3476
- _define_property(this, "eventListeners", new Set());
3477
- _define_property(this, "pendingRequests", new Map());
3478
- _define_property(this, "consoleMessages", []);
3920
+ cdp_connection_define_property(this, "ws", null);
3921
+ cdp_connection_define_property(this, "messageId", 0);
3922
+ cdp_connection_define_property(this, "eventListeners", new Set());
3923
+ cdp_connection_define_property(this, "pendingRequests", new Map());
3924
+ cdp_connection_define_property(this, "consoleMessages", []);
3479
3925
  }
3480
3926
  }
3481
3927
  const PAGE_HTML_SCRIPT = `(() => {
@@ -6205,110 +6651,6 @@ async function dom_inspect_handler(args) {
6205
6651
  return raw;
6206
6652
  }
6207
6653
  }
6208
- const DEFAULT_API = origins.PROD_ORIGINS.www;
6209
- function tokenTtlNote(workspaceSlug, projectSlug) {
6210
- const tokensUrl = workspaceSlug && projectSlug ? consoleProjectUrl({
6211
- workspace: workspaceSlug,
6212
- project: projectSlug
6213
- }, "settings/access-tokens") : consoleBase();
6214
- return `extension.dev CLI tokens live at most 7 days (server-enforced). CI pipelines must re-mint before expiry on the console's Access tokens page: ${tokensUrl}`;
6215
- }
6216
- function resolveApiBase(api) {
6217
- return String(api || process.env.EXTENSION_DEV_API_URL || DEFAULT_API).replace(/\/+$/, "");
6218
- }
6219
- function safeApiBase(raw) {
6220
- let parsed;
6221
- try {
6222
- parsed = new URL(raw);
6223
- } catch {
6224
- return {
6225
- ok: false,
6226
- message: `Invalid platform URL: ${raw}`
6227
- };
6228
- }
6229
- const isLocalhost = "localhost" === parsed.hostname || "127.0.0.1" === parsed.hostname || "[::1]" === parsed.hostname || "::1" === parsed.hostname;
6230
- if ("https:" !== parsed.protocol && !("http:" === parsed.protocol && isLocalhost)) return {
6231
- ok: false,
6232
- message: `Refusing to send the access token to ${raw}: use https (http is allowed only for localhost).`
6233
- };
6234
- return {
6235
- ok: true,
6236
- base: `${parsed.origin}${parsed.pathname}`.replace(/\/+$/, "")
6237
- };
6238
- }
6239
- async function fetchLoginConfig(apiBase, fetchImpl = fetch) {
6240
- const override = String(process.env.EXTENSION_DEV_GITHUB_CLIENT_ID || "").trim();
6241
- if (override) return {
6242
- provider: "github",
6243
- clientId: override,
6244
- scope: "read:user",
6245
- deviceCodeUrl: "/api/cli/device/code",
6246
- deviceTokenUrl: "/api/cli/device/token",
6247
- verificationUri: "https://github.com/login/device"
6248
- };
6249
- const res = await fetchImpl(`${apiBase}/api/cli/login/config`, {
6250
- headers: {
6251
- accept: "application/json"
6252
- }
6253
- });
6254
- if (!res.ok) throw new Error(`Could not fetch login config from ${apiBase} (${res.status}).`);
6255
- const data = await res.json().catch(()=>({}));
6256
- const provider = "extensiondev" === data.provider ? "extensiondev" : "github";
6257
- const clientId = String(data.githubClientId || "").trim();
6258
- if ("github" === provider && !clientId) throw new Error("Login is not configured on the server (no GitHub client id). Set EXTENSION_DEV_GITHUB_CLIENT_ID to override.");
6259
- return {
6260
- provider,
6261
- clientId,
6262
- scope: String(data.scope || "read:user"),
6263
- deviceCodeUrl: String(data.deviceCodeUrl || "/api/cli/device/code"),
6264
- deviceTokenUrl: String(data.deviceTokenUrl || "/api/cli/device/token"),
6265
- verificationUri: String(data.verificationUri || "https://github.com/login/device")
6266
- };
6267
- }
6268
- function persistTokenResponse(args) {
6269
- const token = String(args.data.token || "").trim();
6270
- if (!token) throw new Error("Login returned no token.");
6271
- const creds = {
6272
- version: 1,
6273
- token,
6274
- workspaceSlug: String(args.data.workspaceSlug || ""),
6275
- projectSlug: String(args.data.projectSlug || ""),
6276
- expiresAt: Number(args.data.expiresAt || 0),
6277
- api: args.apiBase,
6278
- provider: args.provider
6279
- };
6280
- writeCredentials(creds);
6281
- return creds;
6282
- }
6283
- async function exchangeAndPersist(args) {
6284
- const doFetch = args.fetchImpl ?? fetch;
6285
- const res = await doFetch(`${args.apiBase}/api/cli/login/exchange`, {
6286
- method: "POST",
6287
- headers: {
6288
- "content-type": "application/json",
6289
- accept: "application/json"
6290
- },
6291
- body: JSON.stringify({
6292
- githubToken: args.githubToken,
6293
- project: args.project
6294
- })
6295
- });
6296
- const text = await res.text();
6297
- let data;
6298
- try {
6299
- data = JSON.parse(text);
6300
- } catch {
6301
- data = {
6302
- message: text
6303
- };
6304
- }
6305
- if (!res.ok) throw new Error(`Login exchange failed (${res.status}): ${data.message || "unknown error"}`);
6306
- return persistTokenResponse({
6307
- apiBase: args.apiBase,
6308
- data,
6309
- provider: "github"
6310
- });
6311
- }
6312
6654
  function resolveToken() {
6313
6655
  const fromEnv = String(process.env.EXTENSION_DEV_TOKEN || "").trim();
6314
6656
  if (fromEnv) return fromEnv;
@@ -6430,7 +6772,10 @@ async function publish_handler(args) {
6430
6772
  const ref = resolveProjectRef();
6431
6773
  if (ref) {
6432
6774
  const buildsUrl = registryFileUrl(ref, "builds/index.json");
6433
- const buildsRes = await fetchRegistryJson(buildsUrl);
6775
+ const buildsRes = await fetchRegistryJson(buildsUrl, fetch, {
6776
+ ref,
6777
+ api: args.api
6778
+ });
6434
6779
  if (buildsRes.ok) {
6435
6780
  const items = parseBuildIndex(buildsRes.json);
6436
6781
  const pinned = args.buildSha ? items.find((item)=>{
@@ -6552,7 +6897,10 @@ async function release_promote_handler(args) {
6552
6897
  enrich.hint = "Run extension_release_list to see this project's channels, their promoted shas, and recent builds.";
6553
6898
  if (ref) {
6554
6899
  const channelsUrl = registryFileUrl(ref, "channels.json");
6555
- const channelsRes = await fetchRegistryJson(channelsUrl);
6900
+ const channelsRes = await fetchRegistryJson(channelsUrl, fetch, {
6901
+ ref,
6902
+ api: args.api
6903
+ });
6556
6904
  if (channelsRes.ok) {
6557
6905
  const rows = parseChannels(channelsRes.json).filter((c)=>c.sha);
6558
6906
  enrich.validChannelShas = Object.fromEntries(rows.map((c)=>[
@@ -6575,11 +6923,23 @@ async function release_promote_handler(args) {
6575
6923
  ...enrich
6576
6924
  });
6577
6925
  }
6578
- return JSON.stringify(data);
6926
+ const promotedRef = resolveProjectRef();
6927
+ const publicChannelUrl = userlandProjectUrl(promotedRef, userland.UserlandProjectPage.channel(channel), args.api);
6928
+ const publicBuildUrl = userlandProjectUrl(promotedRef, userland.UserlandProjectPage.build(buildId), args.api);
6929
+ const enriched = data && "object" == typeof data && !Array.isArray(data) ? {
6930
+ ...data,
6931
+ ...publicChannelUrl ? {
6932
+ publicChannelUrl
6933
+ } : {},
6934
+ ...publicBuildUrl ? {
6935
+ publicBuildUrl
6936
+ } : {}
6937
+ } : data;
6938
+ return JSON.stringify(enriched);
6579
6939
  }
6580
6940
  const release_list_schema = {
6581
6941
  name: "extension_release_list",
6582
- description: "List the project's release channels (channel -> promoted build sha) and recent builds from the public registry (registry.extension.land), so you can pick a valid buildSha for extension_release_promote, extension_deploy, or extension_publish. Read-only, no dispatch. Defaults to the logged-in project (extension_login); pass workspace + project to inspect another public project. Also returns the registry URLs it read and the console Builds page URL.",
6942
+ description: "List the project's release channels (channel -> promoted build sha) and recent builds from the registry (registry.extension.land), so you can pick a valid buildSha for extension_release_promote, extension_deploy, or extension_publish. Read-only, no dispatch. Defaults to the logged-in project (extension_login); pass workspace + project to inspect another project. Works for PRIVATE projects too when your stored login covers them. Returns the registry URLs it read, the console Builds page URL (needs a login), and a publicUrl per channel and build on the public viewer (no login needed for a public project).",
6583
6943
  inputSchema: {
6584
6944
  type: "object",
6585
6945
  properties: {
@@ -6590,6 +6950,10 @@ const release_list_schema = {
6590
6950
  project: {
6591
6951
  type: "string",
6592
6952
  description: "Project slug override (defaults to the stored login's project)."
6953
+ },
6954
+ api: {
6955
+ type: "string",
6956
+ description: "Platform base URL (defaults to https://www.extension.dev or EXTENSION_DEV_API_URL). Used to resolve link origins and, for a private project, to mint a short-lived read token."
6593
6957
  }
6594
6958
  },
6595
6959
  required: []
@@ -6612,12 +6976,21 @@ async function release_list_handler(args) {
6612
6976
  const metaUrl = registryFileUrl(ref, "meta.json");
6613
6977
  const buildsUrl = registryFileUrl(ref, "builds/index.json");
6614
6978
  const [channelsRes, metaRes, buildsRes] = await Promise.all([
6615
- fetchRegistryJson(channelsUrl),
6616
- fetchRegistryJson(metaUrl),
6617
- fetchRegistryJson(buildsUrl)
6979
+ fetchRegistryJson(channelsUrl, fetch, {
6980
+ ref,
6981
+ api: args.api
6982
+ }),
6983
+ fetchRegistryJson(metaUrl, fetch, {
6984
+ ref,
6985
+ api: args.api
6986
+ }),
6987
+ fetchRegistryJson(buildsUrl, fetch, {
6988
+ ref,
6989
+ api: args.api
6990
+ })
6618
6991
  ]);
6619
- const buildsPageUrl = consoleProjectUrl(ref, "builds");
6620
- if (!channelsRes.ok && !metaRes.ok && !buildsRes.ok) return release_list_fail("ReleaseListNotFound", `No registry data for ${ref.workspace}/${ref.project} (${channelsUrl} returned ${channelsRes.status ?? "no response"}). The project may have no builds yet, be private (private registry data needs a share token), or the workspace/project slugs may be wrong. The console Builds page is the authoritative view: ${buildsPageUrl}`, {
6992
+ const buildsPageUrl = consoleProjectUrl(ref, "builds", args.api);
6993
+ if (!channelsRes.ok && !metaRes.ok && !buildsRes.ok) return release_list_fail("ReleaseListNotFound", `No registry data for ${ref.workspace}/${ref.project} (${channelsUrl} returned ${channelsRes.status ?? "no response"}). The project may have no builds yet, or the workspace/project slugs may be wrong. If it is private, make sure extension_login covers this exact project (a token scoped elsewhere cannot read it). The console Builds page is the authoritative view: ${buildsPageUrl}`, {
6621
6994
  workspace: ref.workspace,
6622
6995
  project: ref.project,
6623
6996
  registryUrl: channelsUrl,
@@ -6628,6 +7001,16 @@ async function release_list_handler(args) {
6628
7001
  recentBuilds.sort((a, b)=>String(b.timestamp ?? "").localeCompare(String(a.timestamp ?? "")));
6629
7002
  const meta = metaRes.ok ? metaRes.json : void 0;
6630
7003
  const promotable = Array.from(new Set(channels.map((c)=>c.sha).filter(Boolean)));
7004
+ const isPrivate = "private" === String(meta?.visibility || "").toLowerCase();
7005
+ const publicProjectUrl = userlandProjectUrl(ref, "", args.api);
7006
+ const channelsWithUrls = channels.map((c)=>({
7007
+ ...c,
7008
+ publicUrl: userlandProjectUrl(ref, userland.UserlandProjectPage.channel(c.channel), args.api)
7009
+ }));
7010
+ const buildsWithUrls = recentBuilds.map((b)=>({
7011
+ ...b,
7012
+ publicUrl: userlandProjectUrl(ref, userland.UserlandProjectPage.build(b.sha), args.api)
7013
+ }));
6631
7014
  const result = {
6632
7015
  ok: true,
6633
7016
  workspace: ref.workspace,
@@ -6638,10 +7021,16 @@ async function release_list_handler(args) {
6638
7021
  ...meta?.visibility ? {
6639
7022
  visibility: meta.visibility
6640
7023
  } : {},
6641
- channels,
6642
- recentBuilds,
7024
+ channels: channelsWithUrls,
7025
+ recentBuilds: buildsWithUrls,
6643
7026
  registryUrl: channelsUrl,
6644
7027
  buildsPageUrl,
7028
+ ...publicProjectUrl ? {
7029
+ publicProjectUrl
7030
+ } : {},
7031
+ ...publicProjectUrl ? {
7032
+ publicUrlNote: isPrivate ? "publicUrl links open only for workspace members. This project is private, so an outside recipient needs a share link from extension_publish." : "publicUrl links are the public build pages: no login needed, and they carry the per-browser downloads and the run locally instructions."
7033
+ } : {},
6645
7034
  message: promotable.length > 0 || recentBuilds.length > 0 ? `Promotable shas: channels currently pin ${promotable.length > 0 ? promotable.join(", ") : "none"}; recent builds add ${recentBuilds.filter((b)=>"success" === b.status).map((b)=>b.sha).join(", ") || "none"}. Use one of these as buildId/buildSha for promote/deploy/publish.` : `No channels or builds are recorded on the registry yet for ${ref.workspace}/${ref.project}. Push a commit to produce a build, then check ${buildsPageUrl}.`
6646
7035
  };
6647
7036
  if (!channelsRes.ok) result.channelsUnavailable = `channels.json unreadable: ${channelsRes.message}`;
@@ -6789,8 +7178,14 @@ async function deploy_handler(args) {
6789
7178
  let channelRows = null;
6790
7179
  if (ref) {
6791
7180
  const [healthRes, channelsRes] = await Promise.all([
6792
- fetchRegistryJson(registryFileUrl(ref, "stores/health.json")),
6793
- fetchRegistryJson(registryFileUrl(ref, "channels.json"))
7181
+ fetchRegistryJson(registryFileUrl(ref, "stores/health.json"), fetch, {
7182
+ ref,
7183
+ api: args.api
7184
+ }),
7185
+ fetchRegistryJson(registryFileUrl(ref, "channels.json"), fetch, {
7186
+ ref,
7187
+ api: args.api
7188
+ })
6794
7189
  ]);
6795
7190
  if (healthRes.ok) {
6796
7191
  const stores = healthRes.json?.stores;
@@ -6873,6 +7268,10 @@ const store_status_schema = {
6873
7268
  project: {
6874
7269
  type: "string",
6875
7270
  description: "Project slug override (defaults to the stored login's project)."
7271
+ },
7272
+ api: {
7273
+ type: "string",
7274
+ description: "Platform base URL (defaults to https://www.extension.dev or EXTENSION_DEV_API_URL). Used to resolve link origins and, for a private project, to mint a short-lived read token."
6876
7275
  }
6877
7276
  },
6878
7277
  required: []
@@ -6972,11 +7371,20 @@ async function store_status_handler(args) {
6972
7371
  const healthUrl = registryFileUrl(ref, "stores/health.json");
6973
7372
  const statusUrl = registryFileUrl(ref, "stores/status.json");
6974
7373
  const submissionsUrl = registryFileUrl(ref, "stores/submissions.json");
6975
- const consoleStoresUrl = consoleProjectUrl(ref, "stores");
7374
+ const consoleStoresUrl = consoleProjectUrl(ref, "stores", args.api);
6976
7375
  const [healthRes, statusRes, submissionsRes] = await Promise.all([
6977
- fetchRegistryJson(healthUrl),
6978
- fetchRegistryJson(statusUrl),
6979
- fetchRegistryJson(submissionsUrl)
7376
+ fetchRegistryJson(healthUrl, fetch, {
7377
+ ref,
7378
+ api: args.api
7379
+ }),
7380
+ fetchRegistryJson(statusUrl, fetch, {
7381
+ ref,
7382
+ api: args.api
7383
+ }),
7384
+ fetchRegistryJson(submissionsUrl, fetch, {
7385
+ ref,
7386
+ api: args.api
7387
+ })
6980
7388
  ]);
6981
7389
  if (!healthRes.ok && !statusRes.ok && !submissionsRes.ok) return store_status_fail("StoreStatusNotFound", `No store data on the registry for ${ref.workspace}/${ref.project} (${healthUrl} returned ${healthRes.status ?? "no response"}). The project may have no stores configured yet, be private (private registry data needs a share token), or the workspace/project slugs may be wrong. Configure stores at ${consoleStoresUrl}/new; the console Stores page is the authoritative view: ${consoleStoresUrl}`, {
6982
7390
  workspace: ref.workspace,
@@ -0,0 +1,45 @@
1
+ import type { ProjectRef } from "./registry";
2
+ export type AccessGrant = {
3
+ status: "ok";
4
+ token: string;
5
+ expiresAt: number;
6
+ }
7
+ /** Project is public; no token is needed and none was minted. */
8
+ | {
9
+ status: "public";
10
+ }
11
+ /** No stored credential, or it does not cover the requested project. */
12
+ | {
13
+ status: "no-credential";
14
+ }
15
+ /** Reached the platform and it refused (401/403/5xx, network, bad body). */
16
+ | {
17
+ status: "denied";
18
+ httpStatus?: number;
19
+ message: string;
20
+ };
21
+ export interface AccessTokenCacheOptions {
22
+ fetchImpl?: typeof fetch;
23
+ /** Test-only clock override, unix epoch seconds. */
24
+ nowSeconds?: () => number;
25
+ }
26
+ /**
27
+ * Per-process cache of minted tokens. Concurrent readers of the same project
28
+ * share one in-flight mint: a single tool call fans out over meta.json,
29
+ * channels.json and builds/index.json, and three cold 401s must not become
30
+ * three mint round-trips.
31
+ */
32
+ export declare class RegistryAccessTokens {
33
+ private readonly cache;
34
+ private readonly fetchImpl;
35
+ private readonly nowSeconds;
36
+ constructor(options?: AccessTokenCacheOptions);
37
+ /** A cached, still-valid token, or "" if none. Never mints. */
38
+ peek(ref: ProjectRef): string;
39
+ get(ref: ProjectRef, apiHint?: string): Promise<AccessGrant>;
40
+ private mint;
41
+ }
42
+ /** Process-wide default, so one tool call's fan-out shares one mint. */
43
+ export declare const defaultRegistryAccessTokens: RegistryAccessTokens;
44
+ /** Append `?t=<token>` to a registry URL. */
45
+ export declare function withAccessToken(url: string, token: string): string;
@@ -1,3 +1,4 @@
1
+ import { type RegistryAccessTokens } from "./registry-access";
1
2
  import { type Origins } from "@extension.dev/urls/origins";
2
3
  export declare const REGISTRY_BASE_DEFAULT: string;
3
4
  /**
@@ -37,6 +38,20 @@ export declare function registryFileUrl(ref: ProjectRef, file: string): string;
37
38
  * `EXTENSION_DEV_API_URL`/`EXTENSION_DEV_CONSOLE_URL`.
38
39
  */
39
40
  export declare function consoleProjectUrl(ref: ProjectRef | null, page: string, apiHint?: string): string;
41
+ /**
42
+ * The PUBLIC page for a project, a build, or a channel on userland.
43
+ *
44
+ * The sibling of consoleProjectUrl, and the one to hand to a human who is not
45
+ * the operator: the console links every tool already returns require a login
46
+ * and workspace membership, so they are dead ends for a teammate, a reviewer,
47
+ * or anyone reading a PR. These carry the per-browser downloads, the run
48
+ * locally and integrity dialogs, and the what's new tab.
49
+ *
50
+ * A PRIVATE project's page still needs a `?share=` token, which only
51
+ * extension_publish can mint; callers should say so rather than imply the bare
52
+ * URL will open for anyone.
53
+ */
54
+ export declare function userlandProjectUrl(ref: ProjectRef | null, page?: string, apiHint?: string): string;
40
55
  export type RegistryFetchResult<T> = {
41
56
  ok: true;
42
57
  json: T;
@@ -46,11 +61,23 @@ export type RegistryFetchResult<T> = {
46
61
  message: string;
47
62
  };
48
63
  /**
49
- * Fetch a registry JSON file. Never throws: 404s (private or never-built
50
- * projects), network failures, and non-JSON bodies all come back as
51
- * `{ok:false}` so callers can degrade honestly instead of crashing the verb.
64
+ * Fetch a registry JSON file. Never throws: 404s (never-built projects),
65
+ * network failures, and non-JSON bodies all come back as `{ok:false}` so
66
+ * callers can degrade honestly instead of crashing the verb.
67
+ *
68
+ * PRIVATE PROJECTS: a private project answers 401 here. When `ref` is given and
69
+ * the caller holds a credential for that project, the 401 triggers one mint of
70
+ * a short-lived access token and a single retry with `?t=`. Without `ref` the
71
+ * behaviour is exactly as before, so this stays a drop-in for public reads:
72
+ * public projects still complete in one round trip and never touch the
73
+ * platform. See registry-access.ts for why the stored token is traded rather
74
+ * than sent as `?t=` directly.
52
75
  */
53
- export declare function fetchRegistryJson<T = unknown>(url: string, fetchImpl?: typeof fetch): Promise<RegistryFetchResult<T>>;
76
+ export declare function fetchRegistryJson<T = unknown>(url: string, fetchImpl?: typeof fetch, options?: {
77
+ ref?: ProjectRef | null;
78
+ api?: string;
79
+ tokens?: RegistryAccessTokens;
80
+ }): Promise<RegistryFetchResult<T>>;
54
81
  export interface ChannelEntry {
55
82
  channel: string;
56
83
  sha: string;
@@ -12,6 +12,10 @@ export declare const schema: {
12
12
  type: string;
13
13
  description: string;
14
14
  };
15
+ api: {
16
+ type: string;
17
+ description: string;
18
+ };
15
19
  };
16
20
  required: never[];
17
21
  };
@@ -19,4 +23,5 @@ export declare const schema: {
19
23
  export declare function handler(args: {
20
24
  workspace?: string;
21
25
  project?: string;
26
+ api?: string;
22
27
  }): Promise<string>;
@@ -12,6 +12,10 @@ export declare const schema: {
12
12
  type: string;
13
13
  description: string;
14
14
  };
15
+ api: {
16
+ type: string;
17
+ description: string;
18
+ };
15
19
  };
16
20
  required: never[];
17
21
  };
@@ -52,5 +56,6 @@ export declare function normalizeStoresStatus(json: unknown): NormalizedStoresSt
52
56
  export declare function handler(args: {
53
57
  workspace?: string;
54
58
  project?: string;
59
+ api?: string;
55
60
  }): Promise<string>;
56
61
  export {};
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@extension.dev/mcp",
3
3
  "type": "module",
4
- "version": "6.2.0",
4
+ "version": "6.3.0",
5
5
  "description": "MCP server that lets AI agents (Claude Code, Claude Desktop, Cursor, Copilot, Codex) build, run, inspect, and publish browser extensions. 33 tools for scaffolding, live DOM inspection, log streaming, and store-ready builds across Chrome, Edge, Firefox, Safari, and every Chromium- or Gecko-based browser (Brave, Opera, Vivaldi, Yandex, Waterfox, LibreWolf). Powered by extension.dev and Extension.js.",
6
6
  "mcpName": "io.github.extensiondev/mcp",
7
7
  "license": "Apache-2.0",
@@ -97,7 +97,7 @@
97
97
  "ws": "^8.20.0"
98
98
  },
99
99
  "devDependencies": {
100
- "@extension.dev/urls": "^0.2.0",
100
+ "@extension.dev/urls": "^0.3.0",
101
101
  "@rslib/core": "^0.19.4",
102
102
  "@types/cross-spawn": "^6.0.6",
103
103
  "@types/node": "^25.2.0",
package/server.json CHANGED
@@ -7,13 +7,13 @@
7
7
  "source": "github"
8
8
  },
9
9
  "websiteUrl": "https://extension.dev",
10
- "version": "6.2.0",
10
+ "version": "6.3.0",
11
11
  "packages": [
12
12
  {
13
13
  "registryType": "npm",
14
14
  "registryBaseUrl": "https://registry.npmjs.org",
15
15
  "identifier": "@extension.dev/mcp",
16
- "version": "6.2.0",
16
+ "version": "6.3.0",
17
17
  "runtimeHint": "npx",
18
18
  "transport": {
19
19
  "type": "stdio"