extension 4.0.27 → 4.0.28

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/cli.cjs CHANGED
@@ -7653,8 +7653,9 @@ Cross-browser compatibility
7653
7653
  command: input.command,
7654
7654
  output: input.opts.output
7655
7655
  };
7656
+ const unlockFlag = 'eval' === input.op ? '--allow-eval' : '--allow-control';
7656
7657
  const ready = readReadyContract(projectPath, browser);
7657
- if (!ready) fail(`No active control channel found for ${browser}. Run \`extension dev --browser=${browser} --allow-control\` first.`, {
7658
+ if (!ready) fail(`No active control channel found for ${browser}. Run \`extension dev --browser=${browser} ${unlockFlag}\` first.`, {
7658
7659
  ...outputFrame,
7659
7660
  code: messaging.Lp.E_SESSION_NOT_FOUND
7660
7661
  });
@@ -7662,7 +7663,8 @@ Cross-browser compatibility
7662
7663
  const controller = new BridgeController({
7663
7664
  controlPort: ready.controlPort,
7664
7665
  instanceId: ready.instanceId,
7665
- token: token ?? void 0
7666
+ token: token ?? void 0,
7667
+ unlockFlag
7666
7668
  });
7667
7669
  try {
7668
7670
  await controller.connect();
@@ -8506,7 +8508,7 @@ Cross-browser compatibility
8506
8508
  return Number.isFinite(parsed) && parsed >= 0 ? Math.floor(parsed) : 8080;
8507
8509
  }
8508
8510
  function registerDevCommand(program) {
8509
- program.command('dev').arguments('[project-path|remote-url]').usage('[project-path|remote-url] [options]').description(commandDescriptions.dev).addHelpText('after', "\nAdditional options:\n --no-browser do not launch the browser (dev server still starts)\n --no-reload emit a dev-mode dist without the content-script reload runtime; tabs need manual reload to see changes\n --wait wait for ready contract and exit; pair with --output json for machine output\n").option('--profile <path-to-file | boolean>', 'what path to use for the browser profile. A boolean value of false sets the profile to the default user profile. Defaults to a fresh profile').option('-b, --browser <chrome | chromium | edge | firefox | chromium-based | gecko-based | firefox-based | safari | webkit-based>', 'specify a browser/engine to run. Defaults to `chromium`. `safari` builds and opens a Safari app via Xcode (macOS only; no live reload)').option('--chromium-binary <path-to-binary>', 'specify a path to the Chromium binary. This option overrides the --browser setting. Defaults to the system default').option('--gecko-binary, --firefox-binary <path-to-binary>', 'specify a path to the Gecko binary. This option overrides the --browser setting. Defaults to the system default').option('--safari-binary <path-to-binary>', 'specify the Safari binary to open after packaging (safari targets only)').option('--app-name <name>', 'override the Safari app name (safari targets only). Defaults to the manifest `name`').option('--bundle-id <reverse.dns>', 'set a user-owned Safari bundle identifier (safari targets only). Defaults to a generated dev.extensionjs.* id').option('--macos-only [boolean]', 'generate a macOS-only Safari Xcode project (safari targets only). Pass `false` for a universal macOS + iOS project. Defaults to `true`', parseOptionalBoolean).option('--force-regenerate', 'regenerate the Safari Xcode project even when up to date (safari targets only)').option('--polyfill [boolean]', 'whether or not to apply the cross-browser polyfill. Defaults to `true`', parseOptionalBoolean).option('--no-polyfill', 'disable the cross-browser polyfill').option('--no-open', 'do not open the browser automatically (default: open)').option('--starting-url <url>', 'specify the starting URL for the browser. Defaults to `undefined`').option('--port <port>', 'specify the port to use for the development server. Defaults to `8080`').option('--host <host>', 'specify the host to bind the dev server to. Use 0.0.0.0 for Docker/devcontainers. Defaults to `127.0.0.1`').option('--public-host <host>', 'connectable host the browser (HMR + reload bridge) dials when it differs from the bind host (e.g. a remote/devcontainer). Defaults to the bind host, or 127.0.0.1 when bound to 0.0.0.0').option('--log-context <list>', '[experimental] comma-separated contexts to include (background,content,page,sidebar,popup,options,devtools). Use `all` to include all contexts (default)').option('--logs <off|error|warn|info|debug|trace|all>', '[experimental] minimum centralized logger level to display in terminal (default: off)').option('--log-format <pretty|json|ndjson>', '[experimental] output format for logger events. Defaults to `pretty`').option('--no-log-timestamps', 'disable ISO timestamps in pretty output').option('--no-log-color', 'disable color in pretty output').option('--log-url <pattern>', '[experimental] only show logs where event.url matches this substring or regex (/re/i)').option('--log-tab <id>', 'only show logs for a specific tabId (number)').option('--extensions <list>', 'comma-separated list of companion extensions or store URLs to load').option('--install [boolean]', '[internal] install project dependencies when missing', parseOptionalBoolean).option('--wait [boolean]', 'wait for dist/extension-js/<browser>/ready.json and exit', parseOptionalBoolean).option('--wait-timeout <ms>', 'timeout in milliseconds when using --wait (default: 60000)').option('--output <pretty|json>', 'result format. Use json for a schema-1 envelope on stdout').addOption(new external_commander_namespaceObject.Option('--wait-format <pretty|json>').hideHelp()).addOption(new external_commander_namespaceObject.Option('--debug', 'print maintainer diagnostics alongside normal output')).addOption(new external_commander_namespaceObject.Option('--author, --author-mode', 'deprecated alias for --debug').hideHelp()).option('--allow-control', 'enable the agent-bridge control channel for bounded act (storage/reload/open): see `extension reload|storage|open`').option('--allow-eval', 'additionally enable `extension eval` (runs arbitrary code in a context; writes a 0600 session token)').option('--parent-pid <pid>', 'exit when the given process dies. For wrappers that spawn `extension dev`, so a leaked dev server can never outlive its owner').action(async (pathOrRemoteUrl, options, command)=>{
8511
+ program.command('dev').arguments('[project-path|remote-url]').usage('[project-path|remote-url] [options]').description(commandDescriptions.dev).addHelpText('after', "\nAdditional options:\n --no-browser do not launch the browser (dev server still starts)\n --no-reload emit a dev-mode dist without the content-script reload runtime; tabs need manual reload to see changes\n --wait wait for ready contract and exit; pair with --output json for machine output\n").option('--profile <path-to-file | boolean>', 'what path to use for the browser profile. A boolean value of false sets the profile to the default user profile. Defaults to a fresh profile').option('-b, --browser <chrome | chromium | edge | firefox | chromium-based | gecko-based | firefox-based | safari | webkit-based>', 'specify a browser/engine to run. Defaults to `chromium`. `safari` builds and opens a Safari app via Xcode (macOS only; no live reload)').option('--chromium-binary <path-to-binary>', 'specify a path to the Chromium binary. This option overrides the --browser setting. Defaults to the system default').option('--gecko-binary, --firefox-binary <path-to-binary>', 'specify a path to the Gecko binary. This option overrides the --browser setting. Defaults to the system default').option('--safari-binary <path-to-binary>', 'specify the Safari binary to open after packaging (safari targets only)').option('--app-name <name>', 'override the Safari app name (safari targets only). Defaults to the manifest `name`').option('--bundle-id <reverse.dns>', 'set a user-owned Safari bundle identifier (safari targets only). Defaults to a generated dev.extensionjs.* id').option('--macos-only [boolean]', 'generate a macOS-only Safari Xcode project (safari targets only). Pass `false` for a universal macOS + iOS project. Defaults to `true`', parseOptionalBoolean).option('--force-regenerate', 'regenerate the Safari Xcode project even when up to date (safari targets only)').option('--polyfill [boolean]', 'whether or not to apply the cross-browser polyfill. Defaults to `true`', parseOptionalBoolean).option('--no-polyfill', 'disable the cross-browser polyfill').option('--no-open', 'do not open the browser automatically (default: open)').option('--starting-url <url>', 'specify the starting URL for the browser. Defaults to `undefined`').option('--port <port>', 'specify the port to use for the development server. Defaults to `8080`').option('--host <host>', 'specify the host to bind the dev server to. Use 0.0.0.0 for Docker/devcontainers. Defaults to `127.0.0.1`').option('--public-host <host>', 'connectable host the browser (HMR + reload bridge) dials when it differs from the bind host (e.g. a remote/devcontainer). Defaults to the bind host, or 127.0.0.1 when bound to 0.0.0.0').option('--log-context <list>', '[experimental] comma-separated contexts to include (background,content,page,sidebar,popup,options,devtools). Use `all` to include all contexts (default)').option('--logs <off|error|warn|info|debug|trace|all>', '[experimental] minimum centralized logger level to display in terminal (default: off)').option('--log-format <pretty|json|ndjson>', '[experimental] output format for logger events. Defaults to `pretty`').option('--no-log-timestamps', 'disable ISO timestamps in pretty output').option('--no-log-color', 'disable color in pretty output').option('--log-url <pattern>', '[experimental] only show logs where event.url matches this substring or regex (/re/i)').option('--log-tab <id>', 'only show logs for a specific tabId (number)').option('--extensions <list>', 'comma-separated list of companion extensions or store URLs to load').option('--install [boolean]', '[internal] install project dependencies when missing', parseOptionalBoolean).option('--wait [boolean]', 'wait for dist/extension-js/<browser>/ready.json and exit', parseOptionalBoolean).option('--wait-timeout <ms>', 'timeout in milliseconds when using --wait (default: 60000)').option('--output <pretty|json>', 'result format. Use json for a schema-1 envelope on stdout').addOption(new external_commander_namespaceObject.Option('--wait-format <pretty|json>').hideHelp()).addOption(new external_commander_namespaceObject.Option('--debug', 'print maintainer diagnostics alongside normal output')).addOption(new external_commander_namespaceObject.Option('--author, --author-mode', 'deprecated alias for --debug').hideHelp()).option('--allow-control', 'enable the agent-bridge control channel for bounded act (storage/reload/open): see `extension reload|storage|open`').option('--allow-eval', 'additionally enable `extension eval` (implies --allow-control, runs arbitrary code in a context, writes a 0600 session token)').option('--parent-pid <pid>', 'exit when the given process dies. For wrappers that spawn `extension dev`, so a leaked dev server can never outlive its owner').action(async (pathOrRemoteUrl, options, command)=>{
8510
8512
  const { browser = 'chromium', ...devOptions } = options;
8511
8513
  if (devOptions.debug || devOptions.author || devOptions.authorMode) {
8512
8514
  process.env.EXTENSION_DEBUG = '1';
@@ -9340,39 +9342,75 @@ Cross-browser compatibility
9340
9342
  const base = xdg || external_node_path_default().join(external_node_os_default().homedir(), '.config');
9341
9343
  return external_node_path_default().join(base, 'extension-dev', 'auth.json');
9342
9344
  }
9343
- function readStoredLoginToken() {
9345
+ function readStoredLogin() {
9344
9346
  try {
9345
9347
  const raw = external_node_fs_default().readFileSync(storedLoginPath(), 'utf8');
9346
9348
  const data = JSON.parse(raw);
9347
- if (!data || 'object' != typeof data) return '';
9348
- if (1 !== data.version) return '';
9349
+ if (!data || 'object' != typeof data) return null;
9350
+ if (1 !== data.version) return null;
9349
9351
  const token = String(data.token || '').trim();
9350
- if (!token) return '';
9352
+ if (!token) return null;
9351
9353
  const expiresAt = Number(data.expiresAt || 0);
9352
- if (expiresAt && expiresAt <= Math.floor(Date.now() / 1000)) return '';
9353
- return token;
9354
+ if (expiresAt && expiresAt <= Math.floor(Date.now() / 1000)) return null;
9355
+ return {
9356
+ token,
9357
+ projectSlug: String(data.projectSlug || '').trim(),
9358
+ workspaceSlug: String(data.workspaceSlug || '').trim()
9359
+ };
9354
9360
  } catch {
9355
- return '';
9361
+ return null;
9356
9362
  }
9357
9363
  }
9358
- function buildPublishRequest(opts) {
9359
- const token = String(opts.token || process.env.EXTENSION_DEV_TOKEN || readStoredLoginToken()).trim();
9364
+ function readLocalProjectName(projectPath) {
9365
+ const fromJson = (file)=>{
9366
+ try {
9367
+ const data = JSON.parse(external_node_fs_default().readFileSync(file, 'utf8'));
9368
+ return String(data?.name || '').trim();
9369
+ } catch {
9370
+ return '';
9371
+ }
9372
+ };
9373
+ return fromJson(external_node_path_default().join(projectPath, 'package.json')) || fromJson(external_node_path_default().join(projectPath, 'manifest.json')) || fromJson(external_node_path_default().join(projectPath, 'src', 'manifest.json')) || external_node_path_default().basename(external_node_path_default().resolve(projectPath));
9374
+ }
9375
+ function slugish(value) {
9376
+ return value.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
9377
+ }
9378
+ function buildPublishPlan(opts) {
9379
+ const stored = readStoredLogin();
9380
+ const flagToken = String(opts.token || '').trim();
9381
+ const envToken = String(process.env.EXTENSION_DEV_TOKEN || '').trim();
9382
+ const token = flagToken || envToken || stored?.token || '';
9383
+ const source = flagToken ? 'flag' : envToken ? 'env' : 'stored-login';
9360
9384
  if (!token) throw new Error(`No token. Publishing needs an extension.dev access token.\nGet one: ${PUBLISH_DOCS_URL}\n` + NO_TOKEN_REMEDY);
9385
+ const projectPath = external_node_path_default().resolve(opts.projectPath || process.cwd());
9386
+ const localName = readLocalProjectName(projectPath);
9387
+ const confirmed = String(opts.project || '').trim();
9388
+ const scopedSlug = 'stored-login' === source ? stored?.projectSlug || '' : '';
9389
+ if (confirmed && scopedSlug && slugish(confirmed) !== slugish(scopedSlug)) throw new Error(`You asked to publish "${confirmed}" but your stored login is scoped to "${scopedSlug}".\nPass --token for ${confirmed}, or run npx @extension.dev/mcp login for it.`);
9390
+ if (scopedSlug && slugish(scopedSlug) !== slugish(localName) && slugish(confirmed) !== slugish(scopedSlug)) throw new Error(`Your stored login is scoped to the "${scopedSlug}" project${stored?.workspaceSlug ? ` in workspace "${stored.workspaceSlug}"` : ''}, but ${projectPath} is "${localName}".\nPublishing here would share ${scopedSlug}, not ${localName}.\nRun extension publish inside ${scopedSlug}, pass --project ${scopedSlug} to publish that project on purpose, or pass --token / EXTENSION_DEV_TOKEN for this one.`);
9361
9391
  const base = String(opts.api || process.env.EXTENSION_DEV_API_URL || DEFAULT_API).replace(/\/+$/, '');
9362
9392
  const body = {};
9363
9393
  if (null != opts.ttl && '' !== opts.ttl) body.ttlHours = Number(opts.ttl);
9364
9394
  if (opts.buildSha) body.buildSha = opts.buildSha;
9365
9395
  return {
9366
- url: `${base}/api/cli/publish`,
9367
- headers: {
9368
- authorization: `Bearer ${token}`,
9369
- 'content-type': 'application/json'
9396
+ request: {
9397
+ url: `${base}/api/cli/publish`,
9398
+ headers: {
9399
+ authorization: `Bearer ${token}`,
9400
+ 'content-type': 'application/json'
9401
+ },
9402
+ body: JSON.stringify(body)
9370
9403
  },
9371
- body: JSON.stringify(body)
9404
+ scope: {
9405
+ source,
9406
+ actsFor: scopedSlug || confirmed || localName,
9407
+ workspace: 'stored-login' === source ? stored?.workspaceSlug || '' : '',
9408
+ localName
9409
+ }
9372
9410
  };
9373
9411
  }
9374
9412
  function registerPublishCommand(program) {
9375
- program.command('publish').arguments('[project-path]').usage('[project-path] [options]').description(commandDescriptions.publish).option('--token <token>', 'extension.dev access token (or EXTENSION_DEV_TOKEN, or the stored login)').option('--api <url>', 'platform base URL (or EXTENSION_DEV_API_URL)').option('--ttl <hours>', 'share-link lifetime in hours (1–168, default 24)').option('--build-sha <sha>', 'pin the share URL to a specific build').option('--output <pretty|json>', 'output format (default pretty)').action(async (_projectPathArg, opts)=>{
9413
+ program.command('publish').arguments('[project-path]').usage('[project-path] [options]').description(commandDescriptions.publish).option('--token <token>', 'extension.dev access token (or EXTENSION_DEV_TOKEN, or the stored login)').option('--api <url>', 'platform base URL (or EXTENSION_DEV_API_URL)').option('--ttl <hours>', 'share-link lifetime in hours (1–168, default 24)').option('--build-sha <sha>', 'pin the share URL to a specific build').option('--project <slug>', 'name the project this publish is for, when it is not the directory you are in').option('--output <pretty|json>', 'output format (default pretty)').action(async (projectPathArg, opts)=>{
9376
9414
  const asJson = 'json' === opts.output;
9377
9415
  const failWith = async (status, error, prose, hint)=>{
9378
9416
  if (asJson) console.log(JSON.stringify(messaging.Pr.fail('publish', status, error, hint ? {
@@ -9382,8 +9420,14 @@ Cross-browser compatibility
9382
9420
  await exitAfterDrain(1);
9383
9421
  };
9384
9422
  let req;
9423
+ let scope;
9385
9424
  try {
9386
- req = buildPublishRequest(opts);
9425
+ const plan = buildPublishPlan({
9426
+ ...opts,
9427
+ projectPath: projectPathArg
9428
+ });
9429
+ req = plan.request;
9430
+ scope = plan.scope;
9387
9431
  } catch (err) {
9388
9432
  const message = err?.message || 'publish failed: no token';
9389
9433
  await failWith('denied', {
@@ -9425,8 +9469,16 @@ Cross-browser compatibility
9425
9469
  }, message);
9426
9470
  return;
9427
9471
  }
9428
- if (asJson) console.log(JSON.stringify(messaging.Pr.ok('publish', 'published', data)));
9429
- else console.log(data.shareUrl || JSON.stringify(data));
9472
+ if (asJson) console.log(JSON.stringify(messaging.Pr.ok('publish', 'published', {
9473
+ ...data,
9474
+ project: scope.actsFor,
9475
+ workspace: scope.workspace || void 0,
9476
+ tokenSource: scope.source
9477
+ })));
9478
+ else {
9479
+ console.error(`Published ${scope.actsFor}${scope.workspace ? ` (workspace ${scope.workspace})` : ''}:`);
9480
+ console.log(data.shareUrl || JSON.stringify(data));
9481
+ }
9430
9482
  await exitAfterDrain(0);
9431
9483
  });
9432
9484
  }
@@ -35,7 +35,7 @@ export interface BrowserConfig {
35
35
  *
36
36
  * Each flag disables or modifies a specific browser feature for a more controlled development environment.
37
37
  */
38
- export type DefaultBrowserFlags = '--no-first-run' | '--disable-client-side-phishing-detection' | '--disable-component-extensions-with-background-pages' | '--disable-default-apps' | '--disable-features=InterestFeedContentSuggestions' | '--disable-features=Translate' | '--hide-scrollbars' | '--mute-audio' | '--no-default-browser-check' | '--ash-no-nudges' | '--disable-search-engine-choice-screen' | '--disable-features=MediaRoute' | '--use-mock-keychain' | '--disable-background-networking' | '--disable-breakpad' | '--disable-component-update' | '--disable-domain-reliability' | '--disable-features=AutofillServerCommunicatio' | '--disable-features=CertificateTransparencyComponentUpdate' | '--disable-sync' | '--disable-features=OptimizationHints' | '--disable-features=DialMediaRouteProvider' | '--no-pings' | '--enable-features=SidePanelUpdates' | '--enable-unsafe-extension-debugging' | '--silent-debugger-extension-api';
38
+ export type DefaultBrowserFlags = '--no-first-run' | '--disable-client-side-phishing-detection' | '--disable-component-extensions-with-background-pages' | '--disable-default-apps' | '--disable-features=InterestFeedContentSuggestions' | '--disable-features=Translate' | '--hide-scrollbars' | '--mute-audio' | '--no-default-browser-check' | '--ash-no-nudges' | '--disable-search-engine-choice-screen' | '--disable-features=MediaRoute' | '--use-mock-keychain' | '--disable-background-networking' | '--disable-breakpad' | '--disable-component-update' | '--disable-domain-reliability' | '--disable-features=AutofillServerCommunicatio' | '--disable-features=CertificateTransparencyComponentUpdate' | '--disable-sync' | '--disable-features=OptimizationHints' | '--disable-features=DialMediaRouteProvider' | '--no-pings' | '--enable-features=SidePanelUpdates' | '--disable-features=DisableLoadExtensionCommandLineSwitch' | '--disable-features=ExtensionDisableUnsupportedDeveloper' | '--enable-unsafe-extension-debugging' | '--silent-debugger-extension-api';
39
39
  /**
40
40
  * Options for the browser plugin.
41
41
  */
@@ -1,5 +1,12 @@
1
1
  import type { Command } from 'commander';
2
+ export interface StoredLogin {
3
+ token: string;
4
+ projectSlug: string;
5
+ workspaceSlug: string;
6
+ }
7
+ export declare function readStoredLogin(): StoredLogin | null;
2
8
  export declare function readStoredLoginToken(): string;
9
+ export declare function readLocalProjectName(projectPath: string): string;
3
10
  export interface PublishRequest {
4
11
  url: string;
5
12
  headers: Record<string, string>;
@@ -10,7 +17,24 @@ export interface PublishInput {
10
17
  api?: string;
11
18
  ttl?: string | number;
12
19
  buildSha?: string;
20
+ projectPath?: string;
21
+ project?: string;
22
+ }
23
+ /**
24
+ * The project this publish will act for, and where that answer came from. A
25
+ * stored login is scoped to one project, so a publish run anywhere else answers
26
+ * for the wrong one; `actsFor` is what the command prints and refuses on.
27
+ */
28
+ export interface PublishScope {
29
+ source: 'flag' | 'env' | 'stored-login';
30
+ actsFor: string;
31
+ workspace: string;
32
+ localName: string;
13
33
  }
14
34
  /** Build the HTTP request (pure, unit-testable, no network). */
15
35
  export declare function buildPublishRequest(opts: PublishInput): PublishRequest;
36
+ export declare function buildPublishPlan(opts: PublishInput): {
37
+ request: PublishRequest;
38
+ scope: PublishScope;
39
+ };
16
40
  export declare function registerPublishCommand(program: Command): void;
package/package.json CHANGED
@@ -38,7 +38,7 @@
38
38
  "extension": "./bin/extension.cjs"
39
39
  },
40
40
  "name": "extension",
41
- "version": "4.0.27",
41
+ "version": "4.0.28",
42
42
  "description": "The cross-browser extension framework. Build Chrome, Edge, Firefox, and Safari extensions with no build configuration.",
43
43
  "homepage": "https://extension.js.org/",
44
44
  "bugs": {
@@ -106,9 +106,9 @@
106
106
  "vivaldi-location2": "2.1.1",
107
107
  "waterfox-location": "2.1.1",
108
108
  "yandex-location": "2.1.1",
109
- "extension-create": "4.0.27",
110
- "extension-develop": "4.0.27",
111
- "extension-install": "4.0.27",
109
+ "extension-create": "4.0.28",
110
+ "extension-develop": "4.0.28",
111
+ "extension-install": "4.0.28",
112
112
  "commander": "^15.0.0",
113
113
  "pintor": "0.3.0",
114
114
  "semver": "^7.7.3",