@extension.dev/mcp 6.5.0 → 7.0.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.
package/dist/module.js CHANGED
@@ -629,7 +629,7 @@ __webpack_require__.d(whoami_namespaceObject, {
629
629
  handler: ()=>whoami_handler,
630
630
  schema: ()=>whoami_schema
631
631
  });
632
- var package_namespaceObject = JSON.parse('{"rE":"6.5.0","El":{"OP":"4.0.16-canary.1784889479.74e12044"}}');
632
+ var package_namespaceObject = JSON.parse('{"rE":"7.0.0","El":{"OP":"4.0.16-canary.1784889479.74e12044"}}');
633
633
  function credentialsPath() {
634
634
  if ("win32" === process.platform) {
635
635
  const base = process.env.APPDATA || process.env.LOCALAPPDATA || node_path.join(node_os.homedir(), "AppData", "Roaming");
@@ -647,7 +647,7 @@ function readCredentials() {
647
647
  if (1 !== data.version) return null;
648
648
  const token = String(data.token || "").trim();
649
649
  if (!token) return null;
650
- const provider = "extensiondev" === data.provider || "github" === data.provider ? data.provider : void 0;
650
+ const provider = "extensiondev" === data.provider ? data.provider : void 0;
651
651
  return {
652
652
  version: 1,
653
653
  token,
@@ -735,15 +735,6 @@ function safeApiBase(raw) {
735
735
  };
736
736
  }
737
737
  async function fetchLoginConfig(apiBase, fetchImpl = fetch) {
738
- const override = String(process.env.EXTENSION_DEV_GITHUB_CLIENT_ID || "").trim();
739
- if (override) return {
740
- provider: "github",
741
- clientId: override,
742
- scope: "read:user",
743
- deviceCodeUrl: "/api/cli/device/code",
744
- deviceTokenUrl: "/api/cli/device/token",
745
- verificationUri: "https://github.com/login/device"
746
- };
747
738
  const res = await fetchImpl(`${apiBase}/api/cli/login/config`, {
748
739
  headers: {
749
740
  accept: "application/json"
@@ -751,16 +742,10 @@ async function fetchLoginConfig(apiBase, fetchImpl = fetch) {
751
742
  });
752
743
  if (!res.ok) throw new Error(`Could not fetch login config from ${apiBase} (${res.status}).`);
753
744
  const data = await res.json().catch(()=>({}));
754
- const provider = "extensiondev" === data.provider ? "extensiondev" : "github";
755
- const clientId = String(data.githubClientId || "").trim();
756
- 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.");
757
745
  return {
758
- provider,
759
- clientId,
760
- scope: String(data.scope || "read:user"),
761
746
  deviceCodeUrl: String(data.deviceCodeUrl || "/api/cli/device/code"),
762
747
  deviceTokenUrl: String(data.deviceTokenUrl || "/api/cli/device/token"),
763
- verificationUri: String(data.verificationUri || "https://github.com/login/device")
748
+ verificationUri: String(data.verificationUri || `${apiBase.replace(/\/+$/, "")}/device`)
764
749
  };
765
750
  }
766
751
  function persistTokenResponse(args) {
@@ -773,40 +758,11 @@ function persistTokenResponse(args) {
773
758
  projectSlug: String(args.data.projectSlug || ""),
774
759
  expiresAt: Number(args.data.expiresAt || 0),
775
760
  api: args.apiBase,
776
- provider: args.provider
761
+ provider: "extensiondev"
777
762
  };
778
763
  writeCredentials(creds);
779
764
  return creds;
780
765
  }
781
- async function exchangeAndPersist(args) {
782
- const doFetch = args.fetchImpl ?? fetch;
783
- const res = await doFetch(`${args.apiBase}/api/cli/login/exchange`, {
784
- method: "POST",
785
- headers: {
786
- "content-type": "application/json",
787
- accept: "application/json"
788
- },
789
- body: JSON.stringify({
790
- githubToken: args.githubToken,
791
- project: args.project
792
- })
793
- });
794
- const text = await res.text();
795
- let data;
796
- try {
797
- data = JSON.parse(text);
798
- } catch {
799
- data = {
800
- message: text
801
- };
802
- }
803
- if (!res.ok) throw new Error(`Login exchange failed (${res.status}): ${data.message || "unknown error"}`);
804
- return persistTokenResponse({
805
- apiBase: args.apiBase,
806
- data,
807
- provider: "github"
808
- });
809
- }
810
766
  function _define_property(obj, key, value) {
811
767
  if (key in obj) Object.defineProperty(obj, key, {
812
768
  value: value,
@@ -2006,7 +1962,7 @@ function materializeCarrier(projectPath, browser) {
2006
1962
  ...ignored ? {
2007
1963
  gitignored: ignored
2008
1964
  } : {},
2009
- note: "Live-preview carrier placed in ./extensions; Extension.js loads it as a companion beside your extension. Open https://inspect.extension.dev/?session=live in the dev browser (any http://localhost origin works too) to watch the session's real-lane chrome.* trace on the Trace tab. It is a debug companion, never part of a release: extension_stop and extension_build remove it again" + (ignored ? `, and ${ignored} was added to .gitignore.` : "."),
1965
+ note: "Live-preview carrier placed in ./extensions; Extension.js loads it as a companion beside your extension. Open https://preview.extension.dev/?session=live in the dev browser (any http://localhost origin works too) to watch the session's real-lane chrome.* trace on the Trace tab. It is a debug companion, never part of a release: extension_stop and extension_build remove it again" + (ignored ? `, and ${ignored} was added to .gitignore.` : "."),
2010
1966
  limitations: [
2011
1967
  "The trace shows calls a PAGE bridges to the carrier. Your extension's own chrome.* calls run directly in its contexts and never cross the carrier, so they do not appear.",
2012
1968
  "Bridged calls run under the CARRIER's identity, not your extension's. The preview assumes a single active guest and does not namespace per-extension state, so storage, action/badge state, messaging delivery, offscreen documents and relative script paths belong to the carrier. Rows affected are badged carrier-scoped in the Trace tab.",
@@ -2016,7 +1972,7 @@ function materializeCarrier(projectPath, browser) {
2016
1972
  ...carrierId ? {
2017
1973
  bridgeProtocol: {
2018
1974
  carrierExtensionId: carrierId,
2019
- allowedOrigins: "https://inspect.extension.dev, https://intelligence.extension.dev, https://themes.extension.dev, http://localhost/*, http://127.0.0.1/*",
1975
+ allowedOrigins: "https://preview.extension.dev, https://intelligence.extension.dev, https://themes.extension.dev, http://localhost/*, http://127.0.0.1/*",
2020
1976
  howTo: "From a page on an allowed origin, register your guest once with a 'session' message (it declares the permissions the carrier enforces), then send 'bridge' messages to run chrome.* for real; each one streams into the Trace tab. Use the EXACT dotted wire names the bridge dispatcher accepts: storage is storage.get/set/remove/clear with the AREA AS AN ARGUMENT, NOT storage.local.get.",
2021
1977
  example: [
2022
1978
  `const id = '${carrierId}'`,
@@ -2693,7 +2649,7 @@ const dev_schema = {
2693
2649
  carrier: {
2694
2650
  type: "boolean",
2695
2651
  default: false,
2696
- description: "Load the bundled Extension.dev Live Preview carrier beside your extension (Chromium-family browsers only). It is placed in the project's ./extensions folder, which Extension.js auto-loads; allowlisted pages (inspect.extension.dev, localhost) can then pair with the session and stream its real-lane chrome.* trace. Writes extensions/extension-dev-live-preview/ into the project, gitignores it, and takes it back out on extension_stop or extension_build: it is a debug companion, never part of a release."
2652
+ description: "Load the bundled Extension.dev Live Preview carrier beside your extension (Chromium-family browsers only). It is placed in the project's ./extensions folder, which Extension.js auto-loads; allowlisted pages (preview.extension.dev, localhost) can then pair with the session and stream its real-lane chrome.* trace. Writes extensions/extension-dev-live-preview/ into the project, gitignores it, and takes it back out on extension_stop or extension_build: it is a debug companion, never part of a release."
2697
2653
  }
2698
2654
  },
2699
2655
  required: [
@@ -4573,23 +4529,13 @@ function recordSharedPreview(projectPath, entry) {
4573
4529
  } : {}
4574
4530
  };
4575
4531
  }
4576
- const DEFAULT_INSPECT_URL = "http://localhost:3106";
4577
4532
  const DEFAULT_PREVIEW_DEV_URL = "http://localhost:3110";
4578
- const SURFACES = {
4579
- preview: {
4580
- defaultOrigin: DEFAULT_PREVIEW_DEV_URL,
4581
- scheme: (encoded)=>`preview://build/${encoded}`,
4582
- fetchPath: "/__preview/fetch",
4583
- devCommand: "pnpm --filter preview.extension.dev dev",
4584
- label: "preview.extension.dev"
4585
- },
4586
- inspect: {
4587
- defaultOrigin: DEFAULT_INSPECT_URL,
4588
- scheme: (encoded)=>`inspect://path/${encoded}`,
4589
- fetchPath: "/__inspect/fetch",
4590
- devCommand: "pnpm --filter inspect.extension.dev dev",
4591
- label: "inspect.extension.dev"
4592
- }
4533
+ const SURFACE = {
4534
+ defaultOrigin: DEFAULT_PREVIEW_DEV_URL,
4535
+ scheme: (encoded)=>`preview://build/${encoded}`,
4536
+ fetchPath: "/__preview/fetch",
4537
+ devCommand: "pnpm --filter preview.extension.dev dev",
4538
+ label: "preview.extension.dev"
4593
4539
  };
4594
4540
  async function buildShare(projectPath, distDir, manifest, browser) {
4595
4541
  const result = await uploadPreview({
@@ -4674,7 +4620,7 @@ function detectSurfaces(manifest) {
4674
4620
  }
4675
4621
  const preview_web_schema = {
4676
4622
  name: "extension_preview_web",
4677
- description: "Preview an in-progress extension in the web emulator (no real browser). Builds the project (unless build:false), points preview.extension.dev at the dist directory over the dev-only preview://build scheme, and returns a deep link plus a loadability check against its dev server. preview is the author's front door: it renders YOUR build and carries the Emulated/Real lane toggle and the Trace tab. Pass surface:\"inspect\" to render in inspect.extension.dev instead (the evaluator's door, for fixture and forensic work). Pass share:true to UPLOAD the dist you just built and get back a public link (share.previewUrl) that renders those exact bytes for anyone who opens it, with no install and no dev server, and that also serves the whole build as a downloadable zip. Sharing needs a token scoped to an existing extension.dev workspace and project (extension_login or EXTENSION_DEV_TOKEN, valid up to 7 days), so a local folder with no project on the platform cannot share. Use share:true whenever the build has to reach someone who is not at this machine, since the plain deepLink only resolves locally. Shared links do not disappear when this response scrolls away: extension_shares lists every link this token has shared and revokes any of them.",
4623
+ description: "Preview an in-progress extension in the web emulator (no real browser). Builds the project (unless build:false), points preview.extension.dev at the dist directory over the dev-only preview://build scheme, and returns a deep link plus a loadability check against its dev server. preview.extension.dev is the author's door for a local build: it renders YOUR build and carries the Emulated/Real lane toggle and the Trace tab. Pass share:true to UPLOAD the dist you just built and get back a public link (share.previewUrl) that renders those exact bytes for anyone who opens it, with no install and no dev server, and that also serves the whole build as a downloadable zip. Sharing needs a token scoped to an existing extension.dev workspace and project (extension_login or EXTENSION_DEV_TOKEN, valid up to 7 days), so a local folder with no project on the platform cannot share. Use share:true whenever the build has to reach someone who is not at this machine, since the plain deepLink only resolves locally. Shared links do not disappear when this response scrolls away: extension_shares lists every link this token has shared and revokes any of them.",
4678
4624
  inputSchema: {
4679
4625
  type: "object",
4680
4626
  properties: {
@@ -4698,7 +4644,7 @@ const preview_web_schema = {
4698
4644
  "safari"
4699
4645
  ],
4700
4646
  default: "chrome",
4701
- description: "Which dist/<browser> output to preview. inspect renders it in the mocked-Chrome emulator regardless."
4647
+ description: "Which dist/<browser> output to preview. The emulator renders it as mocked Chrome regardless."
4702
4648
  },
4703
4649
  build: {
4704
4650
  type: "boolean",
@@ -4709,22 +4655,9 @@ const preview_web_schema = {
4709
4655
  type: "string",
4710
4656
  description: "Preview this built directory directly instead of resolving dist/<browser> under projectPath. Implies build:false."
4711
4657
  },
4712
- surface: {
4713
- type: "string",
4714
- enum: [
4715
- "preview",
4716
- "inspect"
4717
- ],
4718
- default: "preview",
4719
- description: "Which front door renders the build. \"preview\" (default) is preview.extension.dev, the author's door: your own in-progress build, with the Emulated/Real lane toggle and the Trace tab. \"inspect\" is inspect.extension.dev, the evaluator's door."
4720
- },
4721
4658
  hostUrl: {
4722
4659
  type: "string",
4723
- description: "Origin of the running dev server for the chosen surface (defaults: http://localhost:3110 for preview, http://localhost:3106 for inspect)."
4724
- },
4725
- inspectUrl: {
4726
- type: "string",
4727
- description: "Deprecated alias for hostUrl, kept for callers written before the preview surface existed. Only consulted when surface is \"inspect\"."
4660
+ description: "Origin of the running preview.extension.dev dev server (default http://localhost:3110)."
4728
4661
  },
4729
4662
  probe: {
4730
4663
  type: "boolean",
@@ -4766,9 +4699,7 @@ const preview_web_schema = {
4766
4699
  };
4767
4700
  async function preview_web_handler(args) {
4768
4701
  const browser = args.browser ?? "chrome";
4769
- const surfaceKey = "inspect" === args.surface ? "inspect" : "preview";
4770
- const surface = SURFACES[surfaceKey];
4771
- const hostBase = (args.hostUrl ?? ("inspect" === surfaceKey ? args.inspectUrl : void 0) ?? surface.defaultOrigin).replace(/\/+$/, "");
4702
+ const hostBase = (args.hostUrl ?? SURFACE.defaultOrigin).replace(/\/+$/, "");
4772
4703
  const shouldBuild = args.distPath ? false : false !== args.build;
4773
4704
  let buildResult = null;
4774
4705
  if (shouldBuild) {
@@ -4811,12 +4742,11 @@ async function preview_web_handler(args) {
4811
4742
  });
4812
4743
  }
4813
4744
  const encoded = Buffer.from(distDir).toString("base64url");
4814
- const internalUrl = surface.scheme(encoded);
4745
+ const internalUrl = SURFACE.scheme(encoded);
4815
4746
  const deepLink = `${hostBase}/?url=${encodeURIComponent(internalUrl)}`;
4816
4747
  const result = {
4817
4748
  ok: true,
4818
4749
  deepLink,
4819
- surface: surfaceKey,
4820
4750
  distDir,
4821
4751
  manifest: {
4822
4752
  name: manifest.name ?? node_path.basename(distDir),
@@ -4829,7 +4759,7 @@ async function preview_web_handler(args) {
4829
4759
  } : {
4830
4760
  built: false
4831
4761
  },
4832
- hint: `Open deepLink in a browser to see the extension render in ${surface.label}'s emulator. It must be running (${surface.devCommand}).${"preview" === surfaceKey ? " Once it renders, the Trace tab shows every chrome.* call it makes, and the lane toggle switches between the emulated backend and a real carrier-equipped browser." : ""}`
4762
+ hint: `Open deepLink in a browser to see the extension render in ${SURFACE.label}'s emulator. It must be running (${SURFACE.devCommand}). Once it renders, the Trace tab shows every chrome.* call it makes, and the lane toggle switches between the emulated backend and a real carrier-equipped browser.`
4833
4763
  };
4834
4764
  if (args.open) {
4835
4765
  const sessionBrowser = args.openIn ?? browser;
@@ -4849,7 +4779,7 @@ async function preview_web_handler(args) {
4849
4779
  }
4850
4780
  if (args.share) result.share = await buildShare(args.projectPath, distDir, manifest, browser);
4851
4781
  if (false === args.probe) return JSON.stringify(result);
4852
- const probeUrl = `${hostBase}${surface.fetchPath}?url=${encodeURIComponent(internalUrl)}`;
4782
+ const probeUrl = `${hostBase}${SURFACE.fetchPath}?url=${encodeURIComponent(internalUrl)}`;
4853
4783
  try {
4854
4784
  const res = await fetch(probeUrl, {
4855
4785
  headers: {
@@ -4864,7 +4794,7 @@ async function preview_web_handler(args) {
4864
4794
  probe: {
4865
4795
  status: res.status,
4866
4796
  contentType,
4867
- note: `${surface.label} answered but not with a preview payload. On the deployed host ${surface.fetchPath} does not exist (dev-only); run a local dev server (${surface.devCommand}) to use web preview.`
4797
+ note: `${SURFACE.label} answered but not with a preview payload. On the deployed host ${SURFACE.fetchPath} does not exist (dev-only); run a local dev server (${SURFACE.devCommand}) to use web preview.`
4868
4798
  }
4869
4799
  });
4870
4800
  const payload = await res.json();
@@ -4886,7 +4816,7 @@ async function preview_web_handler(args) {
4886
4816
  previewLoadable: false,
4887
4817
  probe: {
4888
4818
  error: err instanceof Error ? err.message : String(err),
4889
- note: `Could not reach ${surface.label} at ${hostBase}. Start it with '${surface.devCommand}', then open deepLink.`
4819
+ note: `Could not reach ${SURFACE.label} at ${hostBase}. Start it with '${SURFACE.devCommand}', then open deepLink.`
4890
4820
  }
4891
4821
  });
4892
4822
  }
@@ -5028,7 +4958,7 @@ async function revokeArtifact(options) {
5028
4958
  error: {
5029
4959
  name: "SharesNotFoundError",
5030
4960
  status: 404,
5031
- message: `The platform has no live share ${options.artifactId} for this token. It may already be revoked, already expired, or owned by a different project than the one this token is scoped to.`
4961
+ message: `The platform has no live share ${options.artifactId} for this token. It may already be revoked, already expired, owned by a different project than the one this token is scoped to, or a teammate's personal share, which belongs to that person alone and no project token can revoke.`
5032
4962
  }
5033
4963
  };
5034
4964
  if (!res.ok) return {
@@ -5053,7 +4983,7 @@ async function revokeArtifact(options) {
5053
4983
  const LOGIN_HINT = "Run extension_login, or set EXTENSION_DEV_TOKEN (create one in the extension.dev dashboard).";
5054
4984
  const shares_schema = {
5055
4985
  name: "extension_shares",
5056
- description: "List and revoke the public preview links this token has shared (the links extension_preview_web share:true hands out). action:\"list\" (default) asks the platform for every artifact owned by the logged-in project and returns each one's artifactId, name, version, live/dead state, createdAt, expiresAt, revokedAt, size, and its previewUrl, zipUrl and revokeUrl, so a link you lost the response for is findable again. action:\"revoke\" kills one by artifactId or by pasting any of its URLs; revocation is PERMANENT (the id is burned and re-sharing mints a different link), so it cannot be undone. Pass projectPath to reconcile the platform's answer with .extension.dev/shared-previews.json, the project's own append-only record: a share made on another machine shows up as remoteOnly, and a record with no live artifact behind it shows up under localOnly. Needs the same token as sharing (extension_login or EXTENSION_DEV_TOKEN); without one, listing still returns the local record with a login hint instead of failing. Read-only for the local file: this tool never rewrites shared-previews.json.",
4986
+ description: "List and revoke the public preview links this token has shared (the links extension_preview_web share:true hands out). action:\"list\" (default) asks the platform for every artifact owned by the logged-in project and returns each one's artifactId, name, version, live/dead state, createdAt, expiresAt, revokedAt, size, and its previewUrl, zipUrl and revokeUrl, so a link you lost the response for is findable again. Every row also carries owner and sharedBy as the platform returned them, plus an attribution block: attribution.ownership is \"project\" when the owning workspace holds the share and any member can revoke it, \"personal\" when one person holds it alone and nobody else can, and \"unknown\" when the platform disclosed no owner. attribution.credit names the publisher for credit only and never decides access; it reads \"CLI token <id>\" when the token's issuer could not be resolved and \"not recorded\" for a share made before attribution existed, and neither is a person's name. action:\"revoke\" kills one by artifactId or by pasting any of its URLs; revocation is PERMANENT (the id is burned and re-sharing mints a different link), so it cannot be undone. Pass projectPath to reconcile the platform's answer with .extension.dev/shared-previews.json, the project's own append-only record: a share made on another machine shows up as remoteOnly, and a record with no live artifact behind it shows up under localOnly. Needs the same token as sharing (extension_login or EXTENSION_DEV_TOKEN); without one, listing still returns the local record with a login hint instead of failing. Read-only for the local file: this tool never rewrites shared-previews.json.",
5057
4987
  inputSchema: {
5058
4988
  type: "object",
5059
4989
  properties: {
@@ -5099,6 +5029,52 @@ const shares_schema = {
5099
5029
  required: []
5100
5030
  }
5101
5031
  };
5032
+ const REVOCABLE_BY = {
5033
+ project: "Any member of the owning workspace, and any token scoped to the owning project, can revoke this share.",
5034
+ personal: "This is one person's personal share. Only that person can see it and revoke it, so a project token cannot pull it back.",
5035
+ unknown: "The platform did not disclose an owner for this share, so who may revoke it cannot be stated here."
5036
+ };
5037
+ function shares_text(value) {
5038
+ return "string" == typeof value ? value.trim() : "";
5039
+ }
5040
+ function ownershipOf(owner) {
5041
+ const kind = shares_text(owner?.kind);
5042
+ if ("project" === kind) return "project";
5043
+ if ("user" === kind) return "personal";
5044
+ return "unknown";
5045
+ }
5046
+ function creditOf(sharedBy) {
5047
+ const login = shares_text(sharedBy?.login);
5048
+ if (login) return {
5049
+ credit: login,
5050
+ creditSource: "login"
5051
+ };
5052
+ const tokenId = shares_text(sharedBy?.tokenId);
5053
+ if (tokenId) return {
5054
+ credit: `CLI token ${tokenId}`,
5055
+ creditSource: "tokenId"
5056
+ };
5057
+ return {
5058
+ credit: sharedBy ? "not recorded: the platform could not resolve who published this share" : "not recorded: this share predates publisher attribution",
5059
+ creditSource: "none"
5060
+ };
5061
+ }
5062
+ function attributionOf(artifact) {
5063
+ const ownership = ownershipOf(artifact.owner);
5064
+ const owner = artifact.owner;
5065
+ const ownerPath = "project" === ownership && owner && "project" === owner.kind ? [
5066
+ shares_text(owner.workspace),
5067
+ shares_text(owner.project)
5068
+ ].filter(Boolean).join("/") : "";
5069
+ return {
5070
+ ownership,
5071
+ ...ownerPath ? {
5072
+ ownerPath
5073
+ } : {},
5074
+ ...creditOf(artifact.sharedBy),
5075
+ revocableBy: REVOCABLE_BY[ownership]
5076
+ };
5077
+ }
5102
5078
  function localIndex(entries) {
5103
5079
  const index = new Map();
5104
5080
  for (const entry of entries){
@@ -5164,6 +5140,9 @@ async function listShares(args) {
5164
5140
  const entry = byId.get(artifact.artifactId);
5165
5141
  return {
5166
5142
  ...artifact,
5143
+ owner: artifact.owner ?? null,
5144
+ sharedBy: artifact.sharedBy ?? null,
5145
+ attribution: attributionOf(artifact),
5167
5146
  recordedLocally: Boolean(entry),
5168
5147
  ...entry ? {
5169
5148
  sharedAt: entry.sharedAt,
@@ -5183,6 +5162,11 @@ async function listShares(args) {
5183
5162
  status: localOnlyStatus(entry, listing.data.truncated, now)
5184
5163
  }));
5185
5164
  const liveCount = shares.filter((share)=>share.live).length;
5165
+ const ownership = {
5166
+ project: shares.filter((s)=>"project" === s.attribution.ownership).length,
5167
+ personal: shares.filter((s)=>"personal" === s.attribution.ownership).length,
5168
+ unknown: shares.filter((s)=>"unknown" === s.attribution.ownership).length
5169
+ };
5186
5170
  return JSON.stringify({
5187
5171
  ok: true,
5188
5172
  action: "list",
@@ -5193,8 +5177,9 @@ async function listShares(args) {
5193
5177
  limit: listing.data.limit,
5194
5178
  truncated: listing.data.truncated,
5195
5179
  scanned: listing.data.scanned,
5180
+ ownership,
5196
5181
  ...listing.data.truncated ? {
5197
- truncatedNote: `This is not the whole set: ${listing.data.count} of ${listing.data.matched} matched shares came back at limit ${listing.data.limit}. Raise limit (max 200) or pass status:"live" to narrow it, and do not read a missing share as revoked.`
5182
+ truncatedNote: `This is not the whole set: ${listing.data.count} of ${listing.data.matched} matched shares came back at limit ${listing.data.limit}. truncated also goes true when the server spent its budget working out which shares you are entitled to see, so matched is a floor and not a total. Raise limit (max 200) or pass status:"live" to narrow it, and do not read a missing share as revoked.`
5198
5183
  } : {}
5199
5184
  },
5200
5185
  shares,
@@ -5203,7 +5188,8 @@ async function listShares(args) {
5203
5188
  } : {},
5204
5189
  localRecord,
5205
5190
  message: `${liveCount} of ${shares.length} listed shares still resolve${local ? `; ${localOnly.length} local record ${1 === localOnly.length ? "entry has" : "entries have"} no artifact behind them` : ""}. Revoke one with action:"revoke" and its artifactId or any of its URLs.`,
5206
- note: "previewUrl and zipUrl are null for a share that is no longer live, because a revoked or expired link cannot resolve for anyone. revokeUrl stays on every row. Revocation is permanent: a revoked id is burned and re-sharing the same build mints a different link."
5191
+ note: "previewUrl and zipUrl are null for a share that is no longer live, because a revoked or expired link cannot resolve for anyone. revokeUrl stays on every row. Revocation is permanent: a revoked id is burned and re-sharing the same build mints a different link.",
5192
+ attributionNote: "attribution.ownership says who the share belongs to and therefore who may revoke it: project means the owning workspace holds it and any member can pull it back, personal means one person holds it alone. attribution.credit names the publisher and is attribution only, granting and restricting nothing. A credit of \"CLI token ...\" means the platform could not resolve which human minted that token, and a credit of \"not recorded\" means it never knew; neither is a name, and neither should be reported as one."
5207
5193
  });
5208
5194
  }
5209
5195
  async function revokeShare(args) {
@@ -10010,78 +9996,6 @@ async function add_feature_handler(args) {
10010
9996
  hint: conflicts.length ? `Warning: ${conflicts.length} file(s) already exist and would be overwritten.` : "No conflicts detected. Safe to create all files."
10011
9997
  });
10012
9998
  }
10013
- const GITHUB_DEVICE_CODE_URL = "https://github.com/login/device/code";
10014
- const GITHUB_TOKEN_URL = "https://github.com/login/oauth/access_token";
10015
- const DEVICE_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:device_code";
10016
- const defaultSleep = (ms)=>new Promise((resolve)=>setTimeout(resolve, ms));
10017
- async function startDeviceCode(args) {
10018
- const doFetch = args.fetchImpl ?? fetch;
10019
- const res = await doFetch(GITHUB_DEVICE_CODE_URL, {
10020
- method: "POST",
10021
- headers: {
10022
- accept: "application/json",
10023
- "content-type": "application/json"
10024
- },
10025
- body: JSON.stringify({
10026
- client_id: args.clientId,
10027
- scope: args.scope || "read:user"
10028
- })
10029
- });
10030
- const data = await res.json().catch(()=>({}));
10031
- if (!res.ok || data.error) throw new Error(`GitHub device-code request failed: ${data.error_description || data.error || res.status}`);
10032
- return {
10033
- deviceCode: String(data.device_code || ""),
10034
- userCode: String(data.user_code || ""),
10035
- verificationUri: String(data.verification_uri || "https://github.com/login/device"),
10036
- interval: Number(data.interval || 5),
10037
- expiresIn: Number(data.expires_in || 900)
10038
- };
10039
- }
10040
- async function pollForToken(args) {
10041
- const doFetch = args.fetchImpl ?? fetch;
10042
- const sleep = args.sleepImpl ?? defaultSleep;
10043
- let intervalMs = 1000 * Math.max(1, args.interval);
10044
- const deadline = Date.now() + Math.max(0, args.budgetMs);
10045
- while(Date.now() < deadline){
10046
- await sleep(intervalMs);
10047
- const res = await doFetch(GITHUB_TOKEN_URL, {
10048
- method: "POST",
10049
- headers: {
10050
- accept: "application/json",
10051
- "content-type": "application/json"
10052
- },
10053
- body: JSON.stringify({
10054
- client_id: args.clientId,
10055
- device_code: args.deviceCode,
10056
- grant_type: DEVICE_GRANT_TYPE
10057
- })
10058
- });
10059
- const data = await res.json().catch(()=>({}));
10060
- const token = String(data.access_token || "").trim();
10061
- if (token) return {
10062
- ok: true,
10063
- githubToken: token
10064
- };
10065
- const error = String(data.error || "");
10066
- if ("authorization_pending" === error) continue;
10067
- if ("slow_down" === error) {
10068
- intervalMs = Math.max(intervalMs + 5000, 1000 * Number(data.interval || 0));
10069
- continue;
10070
- }
10071
- if ("expired_token" === error) return {
10072
- ok: false,
10073
- reason: "expired"
10074
- };
10075
- if ("access_denied" === error) return {
10076
- ok: false,
10077
- reason: "denied"
10078
- };
10079
- }
10080
- return {
10081
- ok: false,
10082
- reason: "pending"
10083
- };
10084
- }
10085
9999
  async function requestDeviceCode(args) {
10086
10000
  const doFetch = args.fetchImpl ?? fetch;
10087
10001
  const res = await doFetch(`${args.apiBase}${args.path}`, {
@@ -10145,8 +10059,7 @@ async function pollDeviceToken(args) {
10145
10059
  if (res.ok && data.token) {
10146
10060
  const creds = persistTokenResponse({
10147
10061
  apiBase: args.apiBase,
10148
- data,
10149
- provider: "extensiondev"
10062
+ data
10150
10063
  });
10151
10064
  return {
10152
10065
  ok: true,
@@ -10177,7 +10090,7 @@ async function pollDeviceToken(args) {
10177
10090
  }
10178
10091
  const login_schema = {
10179
10092
  name: "extension_login",
10180
- description: "Authenticate to extension.dev and store a project-scoped access token locally so extension_publish can use it. Two-phase: call with `project` to get a code + URL for the user to authorize, then call again with the returned `deviceCode` to finish. The server picks the flow: the extension.dev-gated device flow (you authorize at extension.dev/device; GitHub federation happens server-side, no GitHub token on this machine) or, as a fallback, the legacy GitHub device flow. Never returns the token. Minted tokens live at most 7 days (server-enforced), so CI pipelines must re-mint before expiry on the console's Access tokens page (project settings -> Access tokens). This is the only tool besides extension_publish that talks to the hosted platform.",
10093
+ description: "Authenticate to extension.dev and store a project-scoped access token locally so extension_publish can use it. Two-phase: call with `project` to get a code + URL for the user to authorize, then call again with the returned `deviceCode` to finish. You authorize at extension.dev/device; GitHub federation happens server-side, so no GitHub token ever lands on this machine. Never returns the token. Minted tokens live at most 7 days (server-enforced), so CI pipelines must re-mint before expiry on the console's Access tokens page (project settings -> Access tokens). This is the only tool besides extension_publish that talks to the hosted platform.",
10181
10094
  inputSchema: {
10182
10095
  type: "object",
10183
10096
  properties: {
@@ -10259,99 +10172,47 @@ async function login_handler(args) {
10259
10172
  } catch (err) {
10260
10173
  return login_fail("LoginConfigError", err?.message || "Could not load login config.");
10261
10174
  }
10262
- if ("extensiondev" === config.provider) {
10263
- if (args.deviceCode) {
10264
- const poll = await pollDeviceToken({
10265
- apiBase,
10266
- path: config.deviceTokenUrl,
10267
- project,
10268
- deviceCode: String(args.deviceCode),
10269
- interval: 5,
10270
- budgetMs: RESUME_BUDGET_MS
10271
- });
10272
- if (poll.ok) return success(poll.creds);
10273
- if ("expired" === poll.reason) return login_fail("LoginExpired", "The device code expired. Run extension_login again to restart.");
10274
- if ("denied" === poll.reason) return login_fail("LoginDenied", "Authorization was denied at extension.dev/device.");
10275
- if ("error" === poll.reason) return login_fail("LoginError", poll.message || "Device login failed.");
10276
- return resumePending(String(args.deviceCode), config.verificationUri);
10277
- }
10278
- let start;
10279
- try {
10280
- start = await requestDeviceCode({
10281
- apiBase,
10282
- path: config.deviceCodeUrl,
10283
- project
10284
- });
10285
- } catch (err) {
10286
- return login_fail("LoginStartError", err?.message || "Could not start the device flow.");
10287
- }
10175
+ if (args.deviceCode) {
10288
10176
  const poll = await pollDeviceToken({
10289
10177
  apiBase,
10290
10178
  path: config.deviceTokenUrl,
10291
10179
  project,
10292
- deviceCode: start.deviceCode,
10293
- interval: start.interval,
10294
- budgetMs: FIRST_CALL_BUDGET_MS
10295
- });
10296
- if (poll.ok) return success(poll.creds);
10297
- if ("denied" === poll.reason) return login_fail("LoginDenied", "Authorization was denied at extension.dev/device.");
10298
- return login_pending({
10299
- deviceCode: start.deviceCode,
10300
- userCode: start.userCode,
10301
- verificationUri: start.verificationUri,
10302
- verificationUriComplete: start.verificationUriComplete
10303
- });
10304
- }
10305
- if (args.deviceCode) {
10306
- const poll = await pollForToken({
10307
- clientId: config.clientId,
10308
10180
  deviceCode: String(args.deviceCode),
10309
10181
  interval: 5,
10310
10182
  budgetMs: RESUME_BUDGET_MS
10311
10183
  });
10312
- if (!poll.ok) {
10313
- if ("expired" === poll.reason) return login_fail("LoginExpired", "The device code expired. Run extension_login again to restart.");
10314
- if ("denied" === poll.reason) return login_fail("LoginDenied", "Authorization was denied on GitHub.");
10315
- return resumePending(String(args.deviceCode), "https://github.com/login/device");
10316
- }
10317
- try {
10318
- const creds = await exchangeAndPersist({
10319
- apiBase,
10320
- githubToken: poll.githubToken,
10321
- project
10322
- });
10323
- return success(creds);
10324
- } catch (err) {
10325
- return login_fail("LoginExchangeError", err?.message || "Token exchange failed.");
10326
- }
10184
+ if (poll.ok) return success(poll.creds);
10185
+ if ("expired" === poll.reason) return login_fail("LoginExpired", "The device code expired. Run extension_login again to restart.");
10186
+ if ("denied" === poll.reason) return login_fail("LoginDenied", "Authorization was denied at extension.dev/device.");
10187
+ if ("error" === poll.reason) return login_fail("LoginError", poll.message || "Device login failed.");
10188
+ return resumePending(String(args.deviceCode), config.verificationUri);
10327
10189
  }
10328
10190
  let start;
10329
10191
  try {
10330
- start = await startDeviceCode({
10331
- clientId: config.clientId,
10332
- scope: config.scope
10192
+ start = await requestDeviceCode({
10193
+ apiBase,
10194
+ path: config.deviceCodeUrl,
10195
+ project
10333
10196
  });
10334
10197
  } catch (err) {
10335
- return login_fail("LoginStartError", err?.message || "Could not start device flow.");
10198
+ return login_fail("LoginStartError", err?.message || "Could not start the device flow.");
10336
10199
  }
10337
- const poll = await pollForToken({
10338
- clientId: config.clientId,
10200
+ const poll = await pollDeviceToken({
10201
+ apiBase,
10202
+ path: config.deviceTokenUrl,
10203
+ project,
10339
10204
  deviceCode: start.deviceCode,
10340
10205
  interval: start.interval,
10341
10206
  budgetMs: FIRST_CALL_BUDGET_MS
10342
10207
  });
10343
- if (poll.ok) try {
10344
- const creds = await exchangeAndPersist({
10345
- apiBase,
10346
- githubToken: poll.githubToken,
10347
- project
10348
- });
10349
- return success(creds);
10350
- } catch (err) {
10351
- return login_fail("LoginExchangeError", err?.message || "Token exchange failed.");
10352
- }
10353
- if (!poll.ok && "denied" === poll.reason) return login_fail("LoginDenied", "Authorization was denied on GitHub.");
10354
- return login_pending(start);
10208
+ if (poll.ok) return success(poll.creds);
10209
+ if ("denied" === poll.reason) return login_fail("LoginDenied", "Authorization was denied at extension.dev/device.");
10210
+ return login_pending({
10211
+ deviceCode: start.deviceCode,
10212
+ userCode: start.userCode,
10213
+ verificationUri: start.verificationUri,
10214
+ verificationUriComplete: start.verificationUriComplete
10215
+ });
10355
10216
  }
10356
10217
  const whoami_schema = {
10357
10218
  name: "extension_whoami",
@@ -10387,7 +10248,7 @@ async function whoami_handler() {
10387
10248
  apiRecordedAtLogin: recordedApi
10388
10249
  } : {},
10389
10250
  apiDefault: effectiveDefaultApi,
10390
- provider: creds.provider ?? "github",
10251
+ provider: creds.provider ?? "extensiondev",
10391
10252
  expiresAt: creds.expiresAt ? new Date(1000 * creds.expiresAt).toISOString() : null,
10392
10253
  expiresInSeconds: creds.expiresAt ? creds.expiresAt - now : null,
10393
10254
  expired,
@@ -11223,55 +11084,28 @@ async function runCli(cmd, args) {
11223
11084
  const apiBase = resolveApiBase(flag("api"));
11224
11085
  try {
11225
11086
  const config = await fetchLoginConfig(apiBase);
11226
- if ("extensiondev" === config.provider) {
11227
- const start = await requestDeviceCode({
11228
- apiBase,
11229
- path: config.deviceCodeUrl,
11230
- project
11231
- });
11232
- log("");
11233
- log(` Open ${start.verificationUri} and enter code: ${start.userCode}`);
11234
- log("");
11235
- log(" Waiting for authorization...");
11236
- const poll = await pollDeviceToken({
11237
- apiBase,
11238
- path: config.deviceTokenUrl,
11239
- project,
11240
- deviceCode: start.deviceCode,
11241
- interval: start.interval,
11242
- budgetMs: 1000 * start.expiresIn
11243
- });
11244
- if (!poll.ok) {
11245
- log("denied" === poll.reason ? "Authorization was denied at extension.dev/device." : "expired" === poll.reason ? "The device code expired. Run login again." : "Timed out waiting for authorization. Run login again.");
11246
- return 1;
11247
- }
11248
- log(`Logged in to ${poll.creds.workspaceSlug}/${poll.creds.projectSlug}.`);
11249
- return 0;
11250
- }
11251
- const start = await startDeviceCode({
11252
- clientId: config.clientId,
11253
- scope: config.scope
11087
+ const start = await requestDeviceCode({
11088
+ apiBase,
11089
+ path: config.deviceCodeUrl,
11090
+ project
11254
11091
  });
11255
11092
  log("");
11256
11093
  log(` Open ${start.verificationUri} and enter code: ${start.userCode}`);
11257
11094
  log("");
11258
11095
  log(" Waiting for authorization...");
11259
- const poll = await pollForToken({
11260
- clientId: config.clientId,
11096
+ const poll = await pollDeviceToken({
11097
+ apiBase,
11098
+ path: config.deviceTokenUrl,
11099
+ project,
11261
11100
  deviceCode: start.deviceCode,
11262
11101
  interval: start.interval,
11263
11102
  budgetMs: 1000 * start.expiresIn
11264
11103
  });
11265
11104
  if (!poll.ok) {
11266
- log("denied" === poll.reason ? "Authorization was denied on GitHub." : "Timed out waiting for authorization. Run login again.");
11105
+ log("denied" === poll.reason ? "Authorization was denied at extension.dev/device." : "expired" === poll.reason ? "The device code expired. Run login again." : "Timed out waiting for authorization. Run login again.");
11267
11106
  return 1;
11268
11107
  }
11269
- const creds = await exchangeAndPersist({
11270
- apiBase,
11271
- githubToken: poll.githubToken,
11272
- project
11273
- });
11274
- log(`Logged in to ${creds.workspaceSlug}/${creds.projectSlug}.`);
11108
+ log(`Logged in to ${poll.creds.workspaceSlug}/${poll.creds.projectSlug}.`);
11275
11109
  return 0;
11276
11110
  } catch (err) {
11277
11111
  log(err instanceof Error ? err.message : String(err));