@loopress/cli 0.24.0 → 0.24.1

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.
@@ -31,3 +31,33 @@ export function redactArgv(argv) {
31
31
  return eqIndex === -1 ? arg : arg.slice(0, eqIndex);
32
32
  });
33
33
  }
34
+ // The site origin in front of a `/wp-json/` REST path: collapse it but keep the path, which is
35
+ // Loopress's own API surface (identical on every site, no user data) and useful for triage.
36
+ const SITE_PREFIX = /\bhttps?:\/\/[^\s"'<>)]*?\/wp-json\//gi;
37
+ // Any other absolute URL (bare site root, a package URL on the summary line): blank it whole.
38
+ const ANY_URL = /\bhttps?:\/\/[^\s"'<>)]+/gi;
39
+ // A WordPress request failure (`formatWpError`) puts the site URL on the first line and, for a
40
+ // 4xx/5xx, appends the server's raw response body after a newline: a Composer trace, a WP fatal,
41
+ // sometimes absolute server paths, package URLs or a connection string. None of that should
42
+ // leave the user's machine. Keep the first line (the "which endpoint, which status" summary)
43
+ // with the site host blanked, and drop everything after it. The terminal still prints the
44
+ // full, unmodified message.
45
+ export function scrubErrorMessage(value) {
46
+ const firstLine = value.split('\n', 1)[0] ?? '';
47
+ return firstLine.replaceAll(SITE_PREFIX, '<site>/wp-json/').replaceAll(ANY_URL, '<site>').slice(0, 300);
48
+ }
49
+ // Sentry `beforeSend`: runs on every event before it is sent. Scrubs the exception message(s)
50
+ // (including chained `cause` errors, which the linkedErrors integration copies in) and drops
51
+ // any request context. `extra.argv` is already redacted upstream by `redactArgv`.
52
+ export function scrubEvent(event) {
53
+ if (typeof event.message === 'string') {
54
+ event.message = scrubErrorMessage(event.message);
55
+ }
56
+ for (const exception of event.exception?.values ?? []) {
57
+ if (typeof exception.value === 'string') {
58
+ exception.value = scrubErrorMessage(exception.value);
59
+ }
60
+ }
61
+ delete event.request;
62
+ return event;
63
+ }
@@ -18,5 +18,9 @@ export type AuthorizeResult = {
18
18
  * 5. The API callback returns an HTML page that POSTs a form with the
19
19
  * Application Password to the local callback server.
20
20
  * 6. The local server extracts the credentials and resolves the promise.
21
+ *
22
+ * The `state` value is threaded through `callbackUrl` (the relay copies it verbatim into the
23
+ * form's POST target), so it comes back as a query param on the credential POST and
24
+ * `waitForLocalCallback` can match it before this handler runs.
21
25
  */
22
26
  export declare function authorizeWithBrowser(siteUrl: string, log: (message: string) => void): Promise<AuthorizeResult>;
@@ -15,13 +15,18 @@ import { renderResultPage, waitForLocalCallback } from './local-callback-server.
15
15
  * 5. The API callback returns an HTML page that POSTs a form with the
16
16
  * Application Password to the local callback server.
17
17
  * 6. The local server extracts the credentials and resolves the promise.
18
+ *
19
+ * The `state` value is threaded through `callbackUrl` (the relay copies it verbatim into the
20
+ * form's POST target), so it comes back as a query param on the credential POST and
21
+ * `waitForLocalCallback` can match it before this handler runs.
18
22
  */
19
23
  export async function authorizeWithBrowser(siteUrl, log) {
20
24
  return waitForLocalCallback({
21
- buildUrl(callbackBaseUrl) {
25
+ allowedOrigins: ['https://api.loopress.dev'],
26
+ buildUrl(callbackBaseUrl, state) {
22
27
  const relayUrl = 'https://api.loopress.dev/auth/wp-authorize';
23
28
  const params = new URLSearchParams({
24
- callbackUrl: callbackBaseUrl,
29
+ callbackUrl: `${callbackBaseUrl}?state=${state}`,
25
30
  wpUrl: siteUrl,
26
31
  });
27
32
  return `${relayUrl}?${params}`;
@@ -31,12 +36,9 @@ export async function authorizeWithBrowser(siteUrl, log) {
31
36
  rejectWithPage(REJECTED_PAGE, new Error('Authorization rejected in WordPress.'));
32
37
  return;
33
38
  }
34
- // `||`, not `??`: an empty-string field counts as absent here, same as the `!password`
35
- // check below, so falls through to the query-param fallback instead of stopping at "".
36
- // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
37
- const password = body.password || url.searchParams.get('password') || '';
38
- // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
39
- const userLogin = body.user_login || url.searchParams.get('user_login') || '';
39
+ // Credentials come from the relay's form POST only. Reading them from the query string
40
+ // would let them land in shell history, proxy logs and the browser address bar (F24).
41
+ const { password, user_login: userLogin } = body;
40
42
  if (!password || !userLogin) {
41
43
  respondBadRequest('Missing password or user_login');
42
44
  return;
@@ -1,7 +1,7 @@
1
1
  export declare const OPTIONS_ENDPOINT = "loopress/v1/options";
2
2
  export declare function optionEndpoint(name: string): string;
3
3
  export declare const RESERVED_OPTION_NAMES: readonly ["active_plugins", "stylesheet", "template"];
4
- export declare const READONLY_BY_DEFAULT_OPTION_NAMES: readonly ["siteurl", "home", "db_version", "cron", "rewrite_rules", "WPLANG"];
4
+ export declare const READONLY_BY_DEFAULT_OPTION_NAMES: readonly ["siteurl", "home", "db_version", "initial_db_version", "cron", "rewrite_rules", "WPLANG", "default_role", "users_can_register", "uninstall_plugins", "mailserver_url", "mailserver_login", "mailserver_pass", "mailserver_port"];
5
5
  export type LocalOption = {
6
6
  autoload: string;
7
7
  name: string;
@@ -6,11 +6,29 @@ export function optionEndpoint(name) {
6
6
  // enforcement lives there). Checked here too so `option add` fails fast with a clear pointer to
7
7
  // the right command, instead of a round trip just to hit the server's 409.
8
8
  export const RESERVED_OPTION_NAMES = ['active_plugins', 'stylesheet', 'template'];
9
- // Options WordPress core owns or regenerates itself: legitimate to track for visibility (`diff`
10
- // still shows drift), but never something `push` should overwrite by default. siteurl/home in
9
+ // Options WordPress core owns or regenerates itself, plus the ones OptionsService::
10
+ // DENY_WRITE_NAMES refuses server-side (behaviour-changing core options: default_role,
11
+ // users_can_register, mailserver_*, ...). Legitimate to track for visibility (`diff` still
12
+ // shows drift), but never something `push` should overwrite by default. siteurl/home in
11
13
  // particular: pushing a value copied from another environment would repoint the target site's
12
- // own URLs, potentially locking out wp-admin.
13
- export const READONLY_BY_DEFAULT_OPTION_NAMES = ['siteurl', 'home', 'db_version', 'cron', 'rewrite_rules', 'WPLANG'];
14
+ // own URLs, potentially locking out wp-admin. Flip `readonly` in the file to push one anyway;
15
+ // for the server-denied names that then needs the `loopress_option_writable` filter too.
16
+ export const READONLY_BY_DEFAULT_OPTION_NAMES = [
17
+ 'siteurl',
18
+ 'home',
19
+ 'db_version',
20
+ 'initial_db_version',
21
+ 'cron',
22
+ 'rewrite_rules',
23
+ 'WPLANG',
24
+ 'default_role',
25
+ 'users_can_register',
26
+ 'uninstall_plugins',
27
+ 'mailserver_url',
28
+ 'mailserver_login',
29
+ 'mailserver_pass',
30
+ 'mailserver_port',
31
+ ];
14
32
  export function isReservedOptionName(name) {
15
33
  return RESERVED_OPTION_NAMES.includes(name);
16
34
  }
@@ -9,9 +9,15 @@ export type SyncPayload = {
9
9
  intent: SyncIntent;
10
10
  lock: null | string;
11
11
  };
12
+ export type LockDriftEntry = {
13
+ from: null | string;
14
+ name: string;
15
+ to: null | string;
16
+ };
12
17
  export type SyncResponse = {
13
18
  composerJson: string;
14
19
  composerLock: string;
20
+ lockDrift?: LockDriftEntry[];
15
21
  message: string;
16
22
  output: string;
17
23
  removed: string[];