@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/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +2 -2
- package/CHANGELOG.md +83 -0
- package/README.md +5 -5
- package/dist/module.js +118 -284
- package/dist/src/lib/artifacts-api.d.ts +16 -0
- package/dist/src/lib/credentials.d.ts +1 -1
- package/dist/src/lib/login-flow.d.ts +0 -10
- package/dist/src/tools/preview-web.d.ts +0 -12
- package/extensions/live-preview/chromium/action/index.css +1 -1
- package/extensions/live-preview/chromium/action/index.js +1 -1
- package/extensions/live-preview/chromium/background/service_worker.js +4 -4
- package/extensions/live-preview/chromium/manifest.json +0 -1
- package/package.json +1 -1
- package/server.json +2 -2
- package/dist/src/lib/github-device.d.ts +0 -31
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":"
|
|
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
|
|
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 || "
|
|
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:
|
|
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://
|
|
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://
|
|
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 (
|
|
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
|
|
4579
|
-
|
|
4580
|
-
|
|
4581
|
-
|
|
4582
|
-
|
|
4583
|
-
|
|
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
|
|
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.
|
|
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
|
|
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
|
|
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 =
|
|
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 ${
|
|
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}${
|
|
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: `${
|
|
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 ${
|
|
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,
|
|
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.
|
|
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 (
|
|
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 (
|
|
10313
|
-
|
|
10314
|
-
|
|
10315
|
-
|
|
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
|
|
10331
|
-
|
|
10332
|
-
|
|
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
|
|
10338
|
-
|
|
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)
|
|
10344
|
-
|
|
10345
|
-
|
|
10346
|
-
|
|
10347
|
-
|
|
10348
|
-
|
|
10349
|
-
|
|
10350
|
-
}
|
|
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 ?? "
|
|
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
|
-
|
|
11227
|
-
|
|
11228
|
-
|
|
11229
|
-
|
|
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
|
|
11260
|
-
|
|
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
|
|
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
|
-
|
|
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));
|