@extenshi/cli 0.8.1 → 0.10.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/cli.js +141 -21
- package/dist/cli.js.map +1 -1
- package/dist/icon-preview.js +1 -1
- package/dist/login-bff.d.ts +29 -0
- package/dist/login-bff.d.ts.map +1 -0
- package/dist/login-bff.js +78 -0
- package/dist/login-bff.js.map +1 -0
- package/dist/login-flow.d.ts +109 -0
- package/dist/login-flow.d.ts.map +1 -0
- package/dist/login-flow.js +144 -0
- package/dist/login-flow.js.map +1 -0
- package/dist/login-runtime.d.ts +38 -0
- package/dist/login-runtime.d.ts.map +1 -0
- package/dist/login-runtime.js +186 -0
- package/dist/login-runtime.js.map +1 -0
- package/dist/risk.d.ts +76 -0
- package/dist/risk.d.ts.map +1 -0
- package/dist/risk.js +214 -0
- package/dist/risk.js.map +1 -0
- package/dist/scan.d.ts +1 -1
- package/dist/scan.js +1 -1
- package/dist/svg-scrub.d.ts +13 -6
- package/dist/svg-scrub.d.ts.map +1 -1
- package/dist/svg-scrub.js +13 -6
- package/dist/svg-scrub.js.map +1 -1
- package/package.json +3 -3
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The real I/O behind `runBrowserLogin`: a one-shot loopback listener, a
|
|
3
|
+
* browser launcher, and an interruptible sleep.
|
|
4
|
+
*
|
|
5
|
+
* Kept apart from `login-flow.ts` so the state machine can be tested without
|
|
6
|
+
* binding sockets or launching applications.
|
|
7
|
+
*
|
|
8
|
+
* TWIN FILE — byte-identical to `tools/extenshi-cli/src/login-runtime.ts`, and
|
|
9
|
+
* pinned equal by `login-twin-sync.test.ts`. See the note in `login-flow.ts`
|
|
10
|
+
* for why the two published packages duplicate rather than share.
|
|
11
|
+
*/
|
|
12
|
+
import type { LoopbackListener } from './login-flow.js';
|
|
13
|
+
/**
|
|
14
|
+
* Bind a loopback listener for the browser to come back to.
|
|
15
|
+
*
|
|
16
|
+
* Returns null — never throws — when binding is not possible: a sandbox that
|
|
17
|
+
* forbids listening, an SSH session with no browser on this machine, an
|
|
18
|
+
* exhausted port range. The login then completes by polling alone, so a null
|
|
19
|
+
* here costs the user a tab they have to close by hand and nothing else.
|
|
20
|
+
*
|
|
21
|
+
* Binds to 127.0.0.1 explicitly rather than all interfaces: it must not become
|
|
22
|
+
* a socket anything off-box can reach, and on macOS binding to 0.0.0.0 is what
|
|
23
|
+
* triggers the "accept incoming connections?" firewall prompt.
|
|
24
|
+
*/
|
|
25
|
+
export declare function openLoopback(): Promise<LoopbackListener | null>;
|
|
26
|
+
/**
|
|
27
|
+
* Launch the browser, reporting whether it plausibly worked.
|
|
28
|
+
*
|
|
29
|
+
* A false return is not an error: SSH sessions, containers and bare servers
|
|
30
|
+
* legitimately have nothing to launch, and the caller prints the URL for the
|
|
31
|
+
* user to open elsewhere. Env vars that mean "no browser here" are honoured
|
|
32
|
+
* before trying, because launching under `CI` would hang a pipeline on a
|
|
33
|
+
* spawned process nobody can see.
|
|
34
|
+
*/
|
|
35
|
+
export declare function openBrowser(url: string): Promise<boolean>;
|
|
36
|
+
/** Sleep, cut short if `wakeup` settles first. */
|
|
37
|
+
export declare function sleep(ms: number, wakeup?: Promise<unknown>): Promise<void>;
|
|
38
|
+
//# sourceMappingURL=login-runtime.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"login-runtime.d.ts","sourceRoot":"","sources":["../src/login-runtime.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAKH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAA;AAsCvD;;;;;;;;;;;GAWG;AACH,wBAAsB,YAAY,IAAI,OAAO,CAAC,gBAAgB,GAAG,IAAI,CAAC,CAkErE;AAcD;;;;;;;;GAQG;AACH,wBAAsB,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CA6B/D;AAED,kDAAkD;AAClD,wBAAgB,KAAK,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,OAAO,CAAC,OAAO,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAgB1E"}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The real I/O behind `runBrowserLogin`: a one-shot loopback listener, a
|
|
3
|
+
* browser launcher, and an interruptible sleep.
|
|
4
|
+
*
|
|
5
|
+
* Kept apart from `login-flow.ts` so the state machine can be tested without
|
|
6
|
+
* binding sockets or launching applications.
|
|
7
|
+
*
|
|
8
|
+
* TWIN FILE — byte-identical to `tools/extenshi-cli/src/login-runtime.ts`, and
|
|
9
|
+
* pinned equal by `login-twin-sync.test.ts`. See the note in `login-flow.ts`
|
|
10
|
+
* for why the two published packages duplicate rather than share.
|
|
11
|
+
*/
|
|
12
|
+
import { spawn } from 'node:child_process';
|
|
13
|
+
import http from 'node:http';
|
|
14
|
+
/** Give up on binding rather than hang a login on a firewall prompt. */
|
|
15
|
+
const BIND_TIMEOUT_MS = 2_000;
|
|
16
|
+
const CALLBACK_PATH = '/callback';
|
|
17
|
+
/**
|
|
18
|
+
* The page the browser lands on after approving.
|
|
19
|
+
*
|
|
20
|
+
* Self-contained (no network, no fonts): it is served from a throwaway local
|
|
21
|
+
* server that is about to close, so anything it references would fail to load.
|
|
22
|
+
* `window.close()` only works for script-opened tabs, so the text has to stand
|
|
23
|
+
* on its own when the tab stays put.
|
|
24
|
+
*/
|
|
25
|
+
function resultPage(result) {
|
|
26
|
+
const approved = result !== 'denied';
|
|
27
|
+
const title = approved ? 'Terminal connected' : 'Login declined';
|
|
28
|
+
const body = approved
|
|
29
|
+
? 'You can close this tab and go back to your terminal.'
|
|
30
|
+
: 'Nothing was connected. You can close this tab.';
|
|
31
|
+
return `<!doctype html>
|
|
32
|
+
<html lang="en"><head><meta charset="utf-8"><title>${title} · Extenshi</title>
|
|
33
|
+
<style>
|
|
34
|
+
:root { color-scheme: light dark }
|
|
35
|
+
body { font: 16px/1.5 system-ui, -apple-system, "Segoe UI", sans-serif; display: grid;
|
|
36
|
+
place-items: center; min-height: 100vh; margin: 0; text-align: center; padding: 24px }
|
|
37
|
+
.card { max-width: 26rem }
|
|
38
|
+
h1 { font-size: 1.25rem; margin: 0 0 .5rem }
|
|
39
|
+
p { margin: 0; opacity: .75 }
|
|
40
|
+
.mark { font-size: 2.5rem; line-height: 1; margin-bottom: .75rem }
|
|
41
|
+
</style></head>
|
|
42
|
+
<body><div class="card">
|
|
43
|
+
<div class="mark">${approved ? '✓' : '✕'}</div>
|
|
44
|
+
<h1>${title}</h1><p>${body}</p>
|
|
45
|
+
</div><script>setTimeout(function(){window.close()},1200)</script></body></html>`;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Bind a loopback listener for the browser to come back to.
|
|
49
|
+
*
|
|
50
|
+
* Returns null — never throws — when binding is not possible: a sandbox that
|
|
51
|
+
* forbids listening, an SSH session with no browser on this machine, an
|
|
52
|
+
* exhausted port range. The login then completes by polling alone, so a null
|
|
53
|
+
* here costs the user a tab they have to close by hand and nothing else.
|
|
54
|
+
*
|
|
55
|
+
* Binds to 127.0.0.1 explicitly rather than all interfaces: it must not become
|
|
56
|
+
* a socket anything off-box can reach, and on macOS binding to 0.0.0.0 is what
|
|
57
|
+
* triggers the "accept incoming connections?" firewall prompt.
|
|
58
|
+
*/
|
|
59
|
+
export async function openLoopback() {
|
|
60
|
+
let resolveHit = () => { };
|
|
61
|
+
const hit = new Promise((resolve) => {
|
|
62
|
+
resolveHit = resolve;
|
|
63
|
+
});
|
|
64
|
+
const server = http.createServer((req, res) => {
|
|
65
|
+
const url = new URL(req.url ?? '/', 'http://127.0.0.1');
|
|
66
|
+
if (url.pathname !== CALLBACK_PATH) {
|
|
67
|
+
res.writeHead(404, { 'content-type': 'text/plain' });
|
|
68
|
+
res.end('Not found');
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
// `result` is display-only: what actually happened is decided by the poll
|
|
72
|
+
// that follows, so a hand-crafted request to this port can change the page
|
|
73
|
+
// wording and nothing else.
|
|
74
|
+
const result = url.searchParams.get('result') ?? 'approved';
|
|
75
|
+
res.writeHead(200, {
|
|
76
|
+
'content-type': 'text/html; charset=utf-8',
|
|
77
|
+
// The page states the outcome of a one-time login; caching it would
|
|
78
|
+
// show a stale verdict on the next one.
|
|
79
|
+
'cache-control': 'no-store',
|
|
80
|
+
});
|
|
81
|
+
res.end(resultPage(result));
|
|
82
|
+
resolveHit();
|
|
83
|
+
});
|
|
84
|
+
const bound = await new Promise((resolve) => {
|
|
85
|
+
const settle = (ok) => {
|
|
86
|
+
clearTimeout(timer);
|
|
87
|
+
server.removeAllListeners('error');
|
|
88
|
+
resolve(ok);
|
|
89
|
+
};
|
|
90
|
+
const timer = setTimeout(() => settle(false), BIND_TIMEOUT_MS);
|
|
91
|
+
server.once('error', () => settle(false));
|
|
92
|
+
try {
|
|
93
|
+
server.listen(0, '127.0.0.1', () => settle(true));
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
settle(false);
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
if (!bound) {
|
|
100
|
+
server.close();
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
const address = server.address();
|
|
104
|
+
if (!address || typeof address.port !== 'number') {
|
|
105
|
+
server.close();
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
// Don't hold the process open: if the browser never comes back, the poll loop
|
|
109
|
+
// (and Ctrl-C) must still be what ends the command.
|
|
110
|
+
server.unref();
|
|
111
|
+
return {
|
|
112
|
+
redirectUri: `http://127.0.0.1:${address.port}${CALLBACK_PATH}`,
|
|
113
|
+
hit,
|
|
114
|
+
close: () => {
|
|
115
|
+
server.close();
|
|
116
|
+
// A tab kept alive by keep-alive would otherwise delay the close.
|
|
117
|
+
server.closeAllConnections?.();
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
/** Per-platform "open this URL in whatever the user uses". */
|
|
122
|
+
function openCommand(url) {
|
|
123
|
+
if (process.platform === 'darwin')
|
|
124
|
+
return { command: 'open', args: [url] };
|
|
125
|
+
if (process.platform === 'win32') {
|
|
126
|
+
// `start` is a cmd builtin, and its first quoted argument is the WINDOW
|
|
127
|
+
// TITLE — hence the empty "" before the URL, without which a quoted URL
|
|
128
|
+
// silently becomes a title and no browser opens.
|
|
129
|
+
return { command: 'cmd', args: ['/c', 'start', '""', url] };
|
|
130
|
+
}
|
|
131
|
+
return { command: 'xdg-open', args: [url] };
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Launch the browser, reporting whether it plausibly worked.
|
|
135
|
+
*
|
|
136
|
+
* A false return is not an error: SSH sessions, containers and bare servers
|
|
137
|
+
* legitimately have nothing to launch, and the caller prints the URL for the
|
|
138
|
+
* user to open elsewhere. Env vars that mean "no browser here" are honoured
|
|
139
|
+
* before trying, because launching under `CI` would hang a pipeline on a
|
|
140
|
+
* spawned process nobody can see.
|
|
141
|
+
*/
|
|
142
|
+
export async function openBrowser(url) {
|
|
143
|
+
if (process.env.EXTENSHI_NO_BROWSER || process.env.CI)
|
|
144
|
+
return false;
|
|
145
|
+
// A Linux box with no display has no browser to open — but WSL does, via the
|
|
146
|
+
// Windows host, so don't treat "no DISPLAY" as decisive there.
|
|
147
|
+
if (process.platform === 'linux' &&
|
|
148
|
+
!process.env.DISPLAY &&
|
|
149
|
+
!process.env.WAYLAND_DISPLAY &&
|
|
150
|
+
!process.env.WSL_DISTRO_NAME) {
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
const { command, args } = openCommand(url);
|
|
154
|
+
return new Promise((resolve) => {
|
|
155
|
+
try {
|
|
156
|
+
const child = spawn(command, args, { stdio: 'ignore', detached: true });
|
|
157
|
+
// An ENOENT (`xdg-open` not installed) arrives asynchronously, so resolve
|
|
158
|
+
// on whichever comes first: the error, or the process getting far enough
|
|
159
|
+
// to be disowned.
|
|
160
|
+
child.once('error', () => resolve(false));
|
|
161
|
+
child.once('spawn', () => {
|
|
162
|
+
child.unref();
|
|
163
|
+
resolve(true);
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
catch {
|
|
167
|
+
resolve(false);
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
/** Sleep, cut short if `wakeup` settles first. */
|
|
172
|
+
export function sleep(ms, wakeup) {
|
|
173
|
+
return new Promise((resolve) => {
|
|
174
|
+
const timer = setTimeout(resolve, ms);
|
|
175
|
+
// Never let a pending sleep be the reason the CLI won't exit.
|
|
176
|
+
timer.unref?.();
|
|
177
|
+
void wakeup?.then(() => {
|
|
178
|
+
clearTimeout(timer);
|
|
179
|
+
resolve();
|
|
180
|
+
}, () => {
|
|
181
|
+
clearTimeout(timer);
|
|
182
|
+
resolve();
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
//# sourceMappingURL=login-runtime.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"login-runtime.js","sourceRoot":"","sources":["../src/login-runtime.ts"],"names":[],"mappings":"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"}
|
package/dist/risk.d.ts
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `extenshi risk` — bulk safety lookup for extensions you already have,
|
|
3
|
+
* addressed by their STORE ids.
|
|
4
|
+
*
|
|
5
|
+
* This is the inventory case: you hold a list of installed extensions (from a
|
|
6
|
+
* fleet report, an MDM export, `chrome://extensions`) and want to know which of
|
|
7
|
+
* them are risky, without knowing their catalog ids.
|
|
8
|
+
*
|
|
9
|
+
* Why it exists as its own command
|
|
10
|
+
* --------------------------------
|
|
11
|
+
* Doing this one extension at a time cost three metered catalog reads each
|
|
12
|
+
* (resolve the store id → follow the cluster canonical → fetch the risk
|
|
13
|
+
* summary). This command hits `security.getSecuritySummaryBatch`, which answers
|
|
14
|
+
* up to {@link MAX_EXTENSIONS_PER_REQUEST} extensions in ONE metered read — an
|
|
15
|
+
* 87-extension inventory drops from 261 credits to 3.
|
|
16
|
+
*
|
|
17
|
+
* Cluster correctness is handled server-side: each store id is resolved through
|
|
18
|
+
* its cross-store cluster to the same listing the extension page renders, so a
|
|
19
|
+
* score printed here matches the page a user can open. When the score comes
|
|
20
|
+
* from a DIFFERENT store's listing of the same extension (cluster members are
|
|
21
|
+
* scanned independently and genuinely disagree), the row says so rather than
|
|
22
|
+
* presenting a sibling's number as this listing's own.
|
|
23
|
+
*/
|
|
24
|
+
/**
|
|
25
|
+
* Max extensions per request.
|
|
26
|
+
*
|
|
27
|
+
* CROSS-PACKAGE SYNC POINT — this is a published npm package and cannot import
|
|
28
|
+
* from the BFF. The authority is `METERED_BATCH_MAX_EXTENSIONS` in
|
|
29
|
+
* `catalog/catalog-bff/src/lib/metered-batch-cap.ts`, which rejects an over-cap
|
|
30
|
+
* request; this constant only lets the CLI fail fast with a useful message
|
|
31
|
+
* instead of round-tripping. Pinned by `risk.test.ts`.
|
|
32
|
+
*/
|
|
33
|
+
export declare const MAX_EXTENSIONS_PER_REQUEST = 40;
|
|
34
|
+
export type Store = 'CHROME' | 'FIREFOX' | 'EDGE';
|
|
35
|
+
export interface StoreRef {
|
|
36
|
+
storeId: string;
|
|
37
|
+
store: Store;
|
|
38
|
+
}
|
|
39
|
+
/** Normalise a user-supplied store name, or undefined when unrecognised. */
|
|
40
|
+
export declare function parseStore(value: string | undefined): Store | undefined;
|
|
41
|
+
/**
|
|
42
|
+
* Parse the id list a caller supplied (argv and/or a `--file`) into store refs.
|
|
43
|
+
*
|
|
44
|
+
* Accepted per entry: a bare id (uses `defaultStore`) or `store:id`, so one
|
|
45
|
+
* file can mix browsers — which is the normal shape of a real inventory.
|
|
46
|
+
* Blank lines and `#` comments are skipped. Duplicates collapse: a user with
|
|
47
|
+
* the same extension in two profiles should not pay for it twice.
|
|
48
|
+
*/
|
|
49
|
+
export declare function parseRefs(entries: string[], defaultStore: Store): {
|
|
50
|
+
refs: StoreRef[];
|
|
51
|
+
invalid: string[];
|
|
52
|
+
};
|
|
53
|
+
/** One row as returned by the BFF batch (fields beyond these are ignored). */
|
|
54
|
+
interface RiskRow {
|
|
55
|
+
storeId?: string;
|
|
56
|
+
store?: string;
|
|
57
|
+
extensionId?: number | null;
|
|
58
|
+
canonicalExtensionId?: number | null;
|
|
59
|
+
extensionName?: string | null;
|
|
60
|
+
riskCategory?: string | null;
|
|
61
|
+
riskScore?: number | string | null;
|
|
62
|
+
scoredExtensionId?: number | null;
|
|
63
|
+
scoredStore?: string | null;
|
|
64
|
+
totalFindings?: number | null;
|
|
65
|
+
lastScanDate?: string | null;
|
|
66
|
+
}
|
|
67
|
+
export declare function rankRow(row: RiskRow): number;
|
|
68
|
+
export interface RunRiskOptions {
|
|
69
|
+
refs: StoreRef[];
|
|
70
|
+
bffUrl: string;
|
|
71
|
+
apiKey?: string;
|
|
72
|
+
outputJson: boolean;
|
|
73
|
+
}
|
|
74
|
+
export declare function runRisk(opts: RunRiskOptions): Promise<void>;
|
|
75
|
+
export {};
|
|
76
|
+
//# sourceMappingURL=risk.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"risk.d.ts","sourceRoot":"","sources":["../src/risk.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAOH;;;;;;;;GAQG;AACH,eAAO,MAAM,0BAA0B,KAAK,CAAA;AAE5C,MAAM,MAAM,KAAK,GAAG,QAAQ,GAAG,SAAS,GAAG,MAAM,CAAA;AAEjD,MAAM,WAAW,QAAQ;IACxB,OAAO,EAAE,MAAM,CAAA;IACf,KAAK,EAAE,KAAK,CAAA;CACZ;AAQD,4EAA4E;AAC5E,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,KAAK,GAAG,SAAS,CAGvE;AAED;;;;;;;GAOG;AACH,wBAAgB,SAAS,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE,YAAY,EAAE,KAAK,GAAG;IAAE,IAAI,EAAE,QAAQ,EAAE,CAAC;IAAC,OAAO,EAAE,MAAM,EAAE,CAAA;CAAE,CA8BzG;AAED,8EAA8E;AAC9E,UAAU,OAAO;IAChB,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC3B,oBAAoB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACpC,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC7B,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC5B,SAAS,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAA;IAClC,iBAAiB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACjC,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC3B,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC7B,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CAC5B;AA0CD,wBAAgB,OAAO,CAAC,GAAG,EAAE,OAAO,GAAG,MAAM,CAE5C;AAED,MAAM,WAAW,cAAc;IAC9B,IAAI,EAAE,QAAQ,EAAE,CAAA;IAChB,MAAM,EAAE,MAAM,CAAA;IACd,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,UAAU,EAAE,OAAO,CAAA;CACnB;AAED,wBAAsB,OAAO,CAAC,IAAI,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC,CAmDjE"}
|
package/dist/risk.js
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `extenshi risk` — bulk safety lookup for extensions you already have,
|
|
3
|
+
* addressed by their STORE ids.
|
|
4
|
+
*
|
|
5
|
+
* This is the inventory case: you hold a list of installed extensions (from a
|
|
6
|
+
* fleet report, an MDM export, `chrome://extensions`) and want to know which of
|
|
7
|
+
* them are risky, without knowing their catalog ids.
|
|
8
|
+
*
|
|
9
|
+
* Why it exists as its own command
|
|
10
|
+
* --------------------------------
|
|
11
|
+
* Doing this one extension at a time cost three metered catalog reads each
|
|
12
|
+
* (resolve the store id → follow the cluster canonical → fetch the risk
|
|
13
|
+
* summary). This command hits `security.getSecuritySummaryBatch`, which answers
|
|
14
|
+
* up to {@link MAX_EXTENSIONS_PER_REQUEST} extensions in ONE metered read — an
|
|
15
|
+
* 87-extension inventory drops from 261 credits to 3.
|
|
16
|
+
*
|
|
17
|
+
* Cluster correctness is handled server-side: each store id is resolved through
|
|
18
|
+
* its cross-store cluster to the same listing the extension page renders, so a
|
|
19
|
+
* score printed here matches the page a user can open. When the score comes
|
|
20
|
+
* from a DIFFERENT store's listing of the same extension (cluster members are
|
|
21
|
+
* scanned independently and genuinely disagree), the row says so rather than
|
|
22
|
+
* presenting a sibling's number as this listing's own.
|
|
23
|
+
*/
|
|
24
|
+
import chalk from 'chalk';
|
|
25
|
+
import { fetch } from 'undici';
|
|
26
|
+
const FETCH_TIMEOUT_MS = 20_000;
|
|
27
|
+
/**
|
|
28
|
+
* Max extensions per request.
|
|
29
|
+
*
|
|
30
|
+
* CROSS-PACKAGE SYNC POINT — this is a published npm package and cannot import
|
|
31
|
+
* from the BFF. The authority is `METERED_BATCH_MAX_EXTENSIONS` in
|
|
32
|
+
* `catalog/catalog-bff/src/lib/metered-batch-cap.ts`, which rejects an over-cap
|
|
33
|
+
* request; this constant only lets the CLI fail fast with a useful message
|
|
34
|
+
* instead of round-tripping. Pinned by `risk.test.ts`.
|
|
35
|
+
*/
|
|
36
|
+
export const MAX_EXTENSIONS_PER_REQUEST = 40;
|
|
37
|
+
const STORE_ALIASES = {
|
|
38
|
+
chrome: 'CHROME',
|
|
39
|
+
firefox: 'FIREFOX',
|
|
40
|
+
edge: 'EDGE',
|
|
41
|
+
};
|
|
42
|
+
/** Normalise a user-supplied store name, or undefined when unrecognised. */
|
|
43
|
+
export function parseStore(value) {
|
|
44
|
+
if (!value)
|
|
45
|
+
return undefined;
|
|
46
|
+
return STORE_ALIASES[value.trim().toLowerCase()];
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Parse the id list a caller supplied (argv and/or a `--file`) into store refs.
|
|
50
|
+
*
|
|
51
|
+
* Accepted per entry: a bare id (uses `defaultStore`) or `store:id`, so one
|
|
52
|
+
* file can mix browsers — which is the normal shape of a real inventory.
|
|
53
|
+
* Blank lines and `#` comments are skipped. Duplicates collapse: a user with
|
|
54
|
+
* the same extension in two profiles should not pay for it twice.
|
|
55
|
+
*/
|
|
56
|
+
export function parseRefs(entries, defaultStore) {
|
|
57
|
+
const refs = [];
|
|
58
|
+
const invalid = [];
|
|
59
|
+
const seen = new Set();
|
|
60
|
+
for (const raw of entries) {
|
|
61
|
+
const line = raw.trim();
|
|
62
|
+
if (!line || line.startsWith('#'))
|
|
63
|
+
continue;
|
|
64
|
+
let store = defaultStore;
|
|
65
|
+
let storeId = line;
|
|
66
|
+
const sep = line.indexOf(':');
|
|
67
|
+
if (sep > 0) {
|
|
68
|
+
const named = parseStore(line.slice(0, sep));
|
|
69
|
+
if (named) {
|
|
70
|
+
store = named;
|
|
71
|
+
storeId = line.slice(sep + 1).trim();
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
if (!storeId || storeId.length > 100) {
|
|
75
|
+
invalid.push(line);
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
const key = `${store}:${storeId}`;
|
|
79
|
+
if (seen.has(key))
|
|
80
|
+
continue;
|
|
81
|
+
seen.add(key);
|
|
82
|
+
refs.push({ storeId, store });
|
|
83
|
+
}
|
|
84
|
+
return { refs, invalid };
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* The website's coefficient: 100 = safest. The backend stores RISK (0 = safest),
|
|
88
|
+
* so every user-facing surface inverts it — printing the raw number here would
|
|
89
|
+
* make the CLI contradict the extension page by construction.
|
|
90
|
+
*/
|
|
91
|
+
function toSafetyScore(risk) {
|
|
92
|
+
if (risk === undefined || risk === null)
|
|
93
|
+
return null;
|
|
94
|
+
const value = Number(risk);
|
|
95
|
+
if (!Number.isFinite(value))
|
|
96
|
+
return null;
|
|
97
|
+
return Math.max(0, Math.min(100, Math.round((100 - value) * 100) / 100));
|
|
98
|
+
}
|
|
99
|
+
function colorForCategory(category) {
|
|
100
|
+
switch (category) {
|
|
101
|
+
case 'CRITICAL':
|
|
102
|
+
return chalk.bgRed.white;
|
|
103
|
+
case 'HIGH':
|
|
104
|
+
return chalk.red;
|
|
105
|
+
case 'MEDIUM':
|
|
106
|
+
return chalk.yellow;
|
|
107
|
+
case 'LOW':
|
|
108
|
+
return chalk.cyan;
|
|
109
|
+
case 'NONE':
|
|
110
|
+
return chalk.green;
|
|
111
|
+
default:
|
|
112
|
+
return chalk.gray;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
/** Severity order for the printed table: worst first, unscanned last. */
|
|
116
|
+
const CATEGORY_RANK = {
|
|
117
|
+
CRITICAL: 0,
|
|
118
|
+
HIGH: 1,
|
|
119
|
+
MEDIUM: 2,
|
|
120
|
+
REVIEW_REQUIRED: 3,
|
|
121
|
+
LOW: 4,
|
|
122
|
+
NONE: 5,
|
|
123
|
+
UNKNOWN: 6,
|
|
124
|
+
};
|
|
125
|
+
export function rankRow(row) {
|
|
126
|
+
return CATEGORY_RANK[row.riskCategory ?? ''] ?? 7;
|
|
127
|
+
}
|
|
128
|
+
export async function runRisk(opts) {
|
|
129
|
+
if (opts.refs.length === 0) {
|
|
130
|
+
throw new Error('no extension ids given. Pass ids as arguments or use --file <path>.');
|
|
131
|
+
}
|
|
132
|
+
if (opts.refs.length > MAX_EXTENSIONS_PER_REQUEST) {
|
|
133
|
+
throw new Error(`${opts.refs.length} extensions given; the limit is ${MAX_EXTENSIONS_PER_REQUEST} per request.\n` +
|
|
134
|
+
`Split the list into batches of ${MAX_EXTENSIONS_PER_REQUEST} or fewer — each request costs ` +
|
|
135
|
+
'1 read credit, whatever its size.');
|
|
136
|
+
}
|
|
137
|
+
// tRPC queries travel as a GET query string, so the batch size bounds the URL
|
|
138
|
+
// length. Worst case the schema allows — 40 entries of 100-char Firefox ids —
|
|
139
|
+
// measured at 7,060 bytes and verified against the live BFF (HTTP 200), which
|
|
140
|
+
// leaves ~1 KB under nginx's default 8 KB header buffer. That margin is the
|
|
141
|
+
// real constraint on raising MAX_EXTENSIONS_PER_REQUEST: a larger cap needs
|
|
142
|
+
// the header buffer raised (or a POST transport) first, not just a new number.
|
|
143
|
+
const input = encodeURIComponent(JSON.stringify({ extensions: opts.refs }));
|
|
144
|
+
const url = `${opts.bffUrl}/api/trpc/security.getSecuritySummaryBatch?input=${input}`;
|
|
145
|
+
let res;
|
|
146
|
+
try {
|
|
147
|
+
res = await fetch(url, {
|
|
148
|
+
headers: opts.apiKey ? { authorization: `Bearer ${opts.apiKey}` } : {},
|
|
149
|
+
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
catch (err) {
|
|
153
|
+
const e = err;
|
|
154
|
+
const why = e.name === 'TimeoutError' ? `timed out after ${FETCH_TIMEOUT_MS / 1000}s` : e.message;
|
|
155
|
+
throw new Error(`could not reach the catalog (${why})`);
|
|
156
|
+
}
|
|
157
|
+
const body = (await res.json().catch(() => null));
|
|
158
|
+
if (body?.error?.message)
|
|
159
|
+
throw new Error(body.error.message);
|
|
160
|
+
if (!res.ok)
|
|
161
|
+
throw new Error(`catalog returned HTTP ${res.status}`);
|
|
162
|
+
const rows = (Array.isArray(body?.result?.data) ? body.result.data : []);
|
|
163
|
+
if (opts.outputJson) {
|
|
164
|
+
console.log(JSON.stringify({ requested: opts.refs.length, matched: rows.length, extensions: rows }, null, 2));
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
printTable(opts.refs, rows);
|
|
168
|
+
}
|
|
169
|
+
function printTable(refs, rows) {
|
|
170
|
+
const byKey = new Map(rows.map((r) => [`${r.store}:${r.storeId}`, r]));
|
|
171
|
+
const sorted = [...rows].sort((a, b) => rankRow(a) - rankRow(b));
|
|
172
|
+
console.log('');
|
|
173
|
+
console.log(chalk.bold(`Safety for ${refs.length} extension${refs.length === 1 ? '' : 's'} (100 = safest)`));
|
|
174
|
+
console.log('');
|
|
175
|
+
if (sorted.length === 0) {
|
|
176
|
+
console.log(chalk.gray(' None of these ids matched a catalog listing.'));
|
|
177
|
+
}
|
|
178
|
+
for (const row of sorted) {
|
|
179
|
+
const color = colorForCategory(row.riskCategory);
|
|
180
|
+
const score = toSafetyScore(row.riskScore);
|
|
181
|
+
const name = row.extensionName ?? row.storeId ?? '(unnamed)';
|
|
182
|
+
const scoreText = score === null ? chalk.gray(' —') : color(String(score).padStart(3));
|
|
183
|
+
const category = row.riskCategory ? color(row.riskCategory) : chalk.gray('not scanned');
|
|
184
|
+
console.log(` ${scoreText} ${chalk.bold(name)} ${category}`);
|
|
185
|
+
const details = [];
|
|
186
|
+
const canonical = row.canonicalExtensionId ?? row.extensionId;
|
|
187
|
+
if (canonical != null)
|
|
188
|
+
details.push(`https://catalog.extenshi.io/extensions/${canonical}`);
|
|
189
|
+
if (row.totalFindings != null)
|
|
190
|
+
details.push(`${row.totalFindings} findings`);
|
|
191
|
+
if (details.length > 0)
|
|
192
|
+
console.log(chalk.gray(` ${details.join(' · ')}`));
|
|
193
|
+
// Cluster members are scanned independently and their scores disagree,
|
|
194
|
+
// so a borrowed number is labelled rather than presented as this
|
|
195
|
+
// listing's own.
|
|
196
|
+
if (row.scoredExtensionId != null && row.scoredExtensionId !== row.extensionId && row.scoredStore) {
|
|
197
|
+
console.log(chalk.gray(` score measured on this extension's ${row.scoredStore} listing`));
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
const missing = refs.filter((ref) => !byKey.has(`${ref.store}:${ref.storeId}`));
|
|
201
|
+
if (missing.length > 0) {
|
|
202
|
+
console.log('');
|
|
203
|
+
console.log(chalk.gray(` ${missing.length} id(s) have no catalog listing yet (not a safety verdict):`));
|
|
204
|
+
for (const ref of missing)
|
|
205
|
+
console.log(chalk.gray(` ${ref.store.toLowerCase()}:${ref.storeId}`));
|
|
206
|
+
}
|
|
207
|
+
const unscanned = rows.filter((r) => r.riskCategory == null).length;
|
|
208
|
+
if (unscanned > 0) {
|
|
209
|
+
console.log('');
|
|
210
|
+
console.log(chalk.gray(` ${unscanned} listed but never scanned — no score is not a clean bill of health.`));
|
|
211
|
+
}
|
|
212
|
+
console.log('');
|
|
213
|
+
}
|
|
214
|
+
//# sourceMappingURL=risk.js.map
|
package/dist/risk.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"risk.js","sourceRoot":"","sources":["../src/risk.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,KAAK,MAAM,OAAO,CAAA;AACzB,OAAO,EAAE,KAAK,EAAE,MAAM,QAAQ,CAAA;AAE9B,MAAM,gBAAgB,GAAG,MAAM,CAAA;AAE/B;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,0BAA0B,GAAG,EAAE,CAAA;AAS5C,MAAM,aAAa,GAA0B;IAC5C,MAAM,EAAE,QAAQ;IAChB,OAAO,EAAE,SAAS;IAClB,IAAI,EAAE,MAAM;CACZ,CAAA;AAED,4EAA4E;AAC5E,MAAM,UAAU,UAAU,CAAC,KAAyB;IACnD,IAAI,CAAC,KAAK;QAAE,OAAO,SAAS,CAAA;IAC5B,OAAO,aAAa,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC,CAAA;AACjD,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,SAAS,CAAC,OAAiB,EAAE,YAAmB;IAC/D,MAAM,IAAI,GAAe,EAAE,CAAA;IAC3B,MAAM,OAAO,GAAa,EAAE,CAAA;IAC5B,MAAM,IAAI,GAAG,IAAI,GAAG,EAAU,CAAA;IAE9B,KAAK,MAAM,GAAG,IAAI,OAAO,EAAE,CAAC;QAC3B,MAAM,IAAI,GAAG,GAAG,CAAC,IAAI,EAAE,CAAA;QACvB,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC;YAAE,SAAQ;QAE3C,IAAI,KAAK,GAAG,YAAY,CAAA;QACxB,IAAI,OAAO,GAAG,IAAI,CAAA;QAClB,MAAM,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAA;QAC7B,IAAI,GAAG,GAAG,CAAC,EAAE,CAAC;YACb,MAAM,KAAK,GAAG,UAAU,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,CAAA;YAC5C,IAAI,KAAK,EAAE,CAAC;gBACX,KAAK,GAAG,KAAK,CAAA;gBACb,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,CAAA;YACrC,CAAC;QACF,CAAC;QAED,IAAI,CAAC,OAAO,IAAI,OAAO,CAAC,MAAM,GAAG,GAAG,EAAE,CAAC;YACtC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;YAClB,SAAQ;QACT,CAAC;QACD,MAAM,GAAG,GAAG,GAAG,KAAK,IAAI,OAAO,EAAE,CAAA;QACjC,IAAI,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC;YAAE,SAAQ;QAC3B,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,CAAA;QACb,IAAI,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC,CAAA;IAC9B,CAAC;IACD,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,CAAA;AACzB,CAAC;AAiBD;;;;GAIG;AACH,SAAS,aAAa,CAAC,IAAa;IACnC,IAAI,IAAI,KAAK,SAAS,IAAI,IAAI,KAAK,IAAI;QAAE,OAAO,IAAI,CAAA;IACpD,MAAM,KAAK,GAAG,MAAM,CAAC,IAAI,CAAC,CAAA;IAC1B,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC;QAAE,OAAO,IAAI,CAAA;IACxC,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,GAAG,GAAG,KAAK,CAAC,GAAG,GAAG,CAAC,GAAG,GAAG,CAAC,CAAC,CAAA;AACzE,CAAC;AAED,SAAS,gBAAgB,CAAC,QAAmC;IAC5D,QAAQ,QAAQ,EAAE,CAAC;QAClB,KAAK,UAAU;YACd,OAAO,KAAK,CAAC,KAAK,CAAC,KAAK,CAAA;QACzB,KAAK,MAAM;YACV,OAAO,KAAK,CAAC,GAAG,CAAA;QACjB,KAAK,QAAQ;YACZ,OAAO,KAAK,CAAC,MAAM,CAAA;QACpB,KAAK,KAAK;YACT,OAAO,KAAK,CAAC,IAAI,CAAA;QAClB,KAAK,MAAM;YACV,OAAO,KAAK,CAAC,KAAK,CAAA;QACnB;YACC,OAAO,KAAK,CAAC,IAAI,CAAA;IACnB,CAAC;AACF,CAAC;AAED,yEAAyE;AACzE,MAAM,aAAa,GAA2B;IAC7C,QAAQ,EAAE,CAAC;IACX,IAAI,EAAE,CAAC;IACP,MAAM,EAAE,CAAC;IACT,eAAe,EAAE,CAAC;IAClB,GAAG,EAAE,CAAC;IACN,IAAI,EAAE,CAAC;IACP,OAAO,EAAE,CAAC;CACV,CAAA;AAED,MAAM,UAAU,OAAO,CAAC,GAAY;IACnC,OAAO,aAAa,CAAC,GAAG,CAAC,YAAY,IAAI,EAAE,CAAC,IAAI,CAAC,CAAA;AAClD,CAAC;AASD,MAAM,CAAC,KAAK,UAAU,OAAO,CAAC,IAAoB;IACjD,IAAI,IAAI,CAAC,IAAI,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC5B,MAAM,IAAI,KAAK,CAAC,qEAAqE,CAAC,CAAA;IACvF,CAAC;IACD,IAAI,IAAI,CAAC,IAAI,CAAC,MAAM,GAAG,0BAA0B,EAAE,CAAC;QACnD,MAAM,IAAI,KAAK,CACd,GAAG,IAAI,CAAC,IAAI,CAAC,MAAM,mCAAmC,0BAA0B,iBAAiB;YAChG,kCAAkC,0BAA0B,iCAAiC;YAC7F,mCAAmC,CACpC,CAAA;IACF,CAAC;IAED,8EAA8E;IAC9E,8EAA8E;IAC9E,8EAA8E;IAC9E,4EAA4E;IAC5E,4EAA4E;IAC5E,+EAA+E;IAC/E,MAAM,KAAK,GAAG,kBAAkB,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,UAAU,EAAE,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC,CAAA;IAC3E,MAAM,GAAG,GAAG,GAAG,IAAI,CAAC,MAAM,oDAAoD,KAAK,EAAE,CAAA;IAErF,IAAI,GAAsC,CAAA;IAC1C,IAAI,CAAC;QACJ,GAAG,GAAG,MAAM,KAAK,CAAC,GAAG,EAAE;YACtB,OAAO,EAAE,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,aAAa,EAAE,UAAU,IAAI,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE;YACtE,MAAM,EAAE,WAAW,CAAC,OAAO,CAAC,gBAAgB,CAAC;SAC7C,CAAC,CAAA;IACH,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACd,MAAM,CAAC,GAAG,GAAY,CAAA;QACtB,MAAM,GAAG,GAAG,CAAC,CAAC,IAAI,KAAK,cAAc,CAAC,CAAC,CAAC,mBAAmB,gBAAgB,GAAG,IAAI,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAA;QACjG,MAAM,IAAI,KAAK,CAAC,gCAAgC,GAAG,GAAG,CAAC,CAAA;IACxD,CAAC;IAED,MAAM,IAAI,GAAG,CAAC,MAAM,GAAG,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,CAGxC,CAAA;IAER,IAAI,IAAI,EAAE,KAAK,EAAE,OAAO;QAAE,MAAM,IAAI,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAA;IAC7D,IAAI,CAAC,GAAG,CAAC,EAAE;QAAE,MAAM,IAAI,KAAK,CAAC,yBAAyB,GAAG,CAAC,MAAM,EAAE,CAAC,CAAA;IAEnE,MAAM,IAAI,GAAG,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAc,CAAA;IAErF,IAAI,IAAI,CAAC,UAAU,EAAE,CAAC;QACrB,OAAO,CAAC,GAAG,CACV,IAAI,CAAC,SAAS,CAAC,EAAE,SAAS,EAAE,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,OAAO,EAAE,IAAI,CAAC,MAAM,EAAE,UAAU,EAAE,IAAI,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC,CAChG,CAAA;QACD,OAAM;IACP,CAAC;IAED,UAAU,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,CAAA;AAC5B,CAAC;AAED,SAAS,UAAU,CAAC,IAAgB,EAAE,IAAe;IACpD,MAAM,KAAK,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,CAAA;IACtE,MAAM,MAAM,GAAG,CAAC,GAAG,IAAI,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC,CAAA;IAEhE,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,CAAA;IACf,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,cAAc,IAAI,CAAC,MAAM,aAAa,IAAI,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,iBAAiB,CAAC,CAAC,CAAA;IAC5G,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,CAAA;IAEf,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACzB,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,gDAAgD,CAAC,CAAC,CAAA;IAC1E,CAAC;IAED,KAAK,MAAM,GAAG,IAAI,MAAM,EAAE,CAAC;QAC1B,MAAM,KAAK,GAAG,gBAAgB,CAAC,GAAG,CAAC,YAAY,CAAC,CAAA;QAChD,MAAM,KAAK,GAAG,aAAa,CAAC,GAAG,CAAC,SAAS,CAAC,CAAA;QAC1C,MAAM,IAAI,GAAG,GAAG,CAAC,aAAa,IAAI,GAAG,CAAC,OAAO,IAAI,WAAW,CAAA;QAC5D,MAAM,SAAS,GAAG,KAAK,KAAK,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAA;QACvF,MAAM,QAAQ,GAAG,GAAG,CAAC,YAAY,CAAC,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,aAAa,CAAC,CAAA;QACvF,OAAO,CAAC,GAAG,CAAC,KAAK,SAAS,KAAK,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,QAAQ,EAAE,CAAC,CAAA;QAE/D,MAAM,OAAO,GAAa,EAAE,CAAA;QAC5B,MAAM,SAAS,GAAG,GAAG,CAAC,oBAAoB,IAAI,GAAG,CAAC,WAAW,CAAA;QAC7D,IAAI,SAAS,IAAI,IAAI;YAAE,OAAO,CAAC,IAAI,CAAC,0CAA0C,SAAS,EAAE,CAAC,CAAA;QAC1F,IAAI,GAAG,CAAC,aAAa,IAAI,IAAI;YAAE,OAAO,CAAC,IAAI,CAAC,GAAG,GAAG,CAAC,aAAa,WAAW,CAAC,CAAA;QAC5E,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC;YAAE,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,UAAU,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC,CAAA;QAElF,uEAAuE;QACvE,iEAAiE;QACjE,iBAAiB;QACjB,IAAI,GAAG,CAAC,iBAAiB,IAAI,IAAI,IAAI,GAAG,CAAC,iBAAiB,KAAK,GAAG,CAAC,WAAW,IAAI,GAAG,CAAC,WAAW,EAAE,CAAC;YACnG,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,6CAA6C,GAAG,CAAC,WAAW,UAAU,CAAC,CAAC,CAAA;QAChG,CAAC;IACF,CAAC;IAED,MAAM,OAAO,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,GAAG,CAAC,KAAK,IAAI,GAAG,CAAC,OAAO,EAAE,CAAC,CAAC,CAAA;IAC/E,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACxB,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,CAAA;QACf,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,KAAK,OAAO,CAAC,MAAM,4DAA4D,CAAC,CAAC,CAAA;QACxG,KAAK,MAAM,GAAG,IAAI,OAAO;YAAE,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,OAAO,GAAG,CAAC,KAAK,CAAC,WAAW,EAAE,IAAI,GAAG,CAAC,OAAO,EAAE,CAAC,CAAC,CAAA;IACpG,CAAC;IAED,MAAM,SAAS,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,YAAY,IAAI,IAAI,CAAC,CAAC,MAAM,CAAA;IACnE,IAAI,SAAS,GAAG,CAAC,EAAE,CAAC;QACnB,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,CAAA;QACf,OAAO,CAAC,GAAG,CACV,KAAK,CAAC,IAAI,CAAC,KAAK,SAAS,qEAAqE,CAAC,CAC/F,CAAA;IACF,CAAC;IACD,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,CAAA;AAChB,CAAC"}
|
package/dist/scan.d.ts
CHANGED
|
@@ -31,7 +31,7 @@ import { type ComplianceReviewPayload } from './compliance-review.js';
|
|
|
31
31
|
* ownership you have verified, and ONLY when you name one with
|
|
32
32
|
* `--extension-id <id>` (the numeric catalog ID of a verified extension). A
|
|
33
33
|
* bare `extenshi scan <artifact>` running on free credits therefore 403s —
|
|
34
|
-
* which is confusing when dojo just says "
|
|
34
|
+
* which is confusing when dojo just says "3 free scans left".
|
|
35
35
|
*
|
|
36
36
|
* The BFF signals this case via a stable `errorCode`
|
|
37
37
|
* (`FREE_REQUIRES_CLAIM` / `FREE_REQUIRES_VERIFIED_OWNERSHIP`). The CLI ships
|
package/dist/scan.js
CHANGED
|
@@ -44,7 +44,7 @@ const DOJO_URL = 'https://dojo.extenshi.io';
|
|
|
44
44
|
* ownership you have verified, and ONLY when you name one with
|
|
45
45
|
* `--extension-id <id>` (the numeric catalog ID of a verified extension). A
|
|
46
46
|
* bare `extenshi scan <artifact>` running on free credits therefore 403s —
|
|
47
|
-
* which is confusing when dojo just says "
|
|
47
|
+
* which is confusing when dojo just says "3 free scans left".
|
|
48
48
|
*
|
|
49
49
|
* The BFF signals this case via a stable `errorCode`
|
|
50
50
|
* (`FREE_REQUIRES_CLAIM` / `FREE_REQUIRES_VERIFIED_OWNERSHIP`). The CLI ships
|
package/dist/svg-scrub.d.ts
CHANGED
|
@@ -1,19 +1,26 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Canonical regex-based SVG scrubber, shared by the offline CLI preview
|
|
3
|
-
* hosted dojo generator
|
|
2
|
+
* Canonical regex-based SVG scrubber, shared by the offline CLI preview, the
|
|
3
|
+
* hosted dojo generator and the catalog-bff persistence path so all three strip
|
|
4
|
+
* the exact same constructs.
|
|
4
5
|
*
|
|
5
6
|
* Regex-based (not DOM-based) on purpose: it must run in Node with no DOMParser
|
|
6
7
|
* — the CLI runs offline in a terminal, and the dojo API route runs server-side.
|
|
7
8
|
* Patterns intentionally over-remove rather than under-remove: an icon has no
|
|
8
9
|
* legitimate use for scripts, styles, animation, or external references.
|
|
9
10
|
*
|
|
10
|
-
* SINGLE SOURCE,
|
|
11
|
-
* tools/extenshi-cli/src/svg-scrub.ts
|
|
11
|
+
* SINGLE SOURCE, THREE COPIES: this file exists byte-identical at
|
|
12
|
+
* tools/extenshi-cli/src/svg-scrub.ts (canonical — edit here)
|
|
12
13
|
* dojo/src/lib/tools/svg-scrub.ts (synced copy for the dojo build)
|
|
14
|
+
* catalog/catalog-bff/src/lib/svg-scrub.ts (synced copy for the BFF build)
|
|
13
15
|
* because the published @extenshi/cli npm package cannot depend on unpublished
|
|
14
|
-
* workspace packages, and dojo's
|
|
16
|
+
* workspace packages, and neither dojo's nor the BFF's Docker build builds the
|
|
17
|
+
* CLI package. The BFF copy is the load-bearing one for welcome pages: it
|
|
18
|
+
* sanitizes agent-authored SVG *before it is persisted*, so a hostile block can
|
|
19
|
+
* never reach the database, let alone a rendered page.
|
|
15
20
|
* A test in tools/extenshi-cli/src/icon.test.ts fails CI when the copies drift;
|
|
16
|
-
* after editing, run:
|
|
21
|
+
* after editing, run:
|
|
22
|
+
* cp tools/extenshi-cli/src/svg-scrub.ts dojo/src/lib/tools/svg-scrub.ts
|
|
23
|
+
* cp tools/extenshi-cli/src/svg-scrub.ts catalog/catalog-bff/src/lib/svg-scrub.ts
|
|
17
24
|
*
|
|
18
25
|
* This is the same rationale as icon-preview.ts (see the note there).
|
|
19
26
|
*/
|
package/dist/svg-scrub.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"svg-scrub.d.ts","sourceRoot":"","sources":["../src/svg-scrub.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"svg-scrub.d.ts","sourceRoot":"","sources":["../src/svg-scrub.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAMH,eAAO,MAAM,kBAAkB,8LAiBrB,CAAA;AAIV;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CA0BlD"}
|
package/dist/svg-scrub.js
CHANGED
|
@@ -1,19 +1,26 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Canonical regex-based SVG scrubber, shared by the offline CLI preview
|
|
3
|
-
* hosted dojo generator
|
|
2
|
+
* Canonical regex-based SVG scrubber, shared by the offline CLI preview, the
|
|
3
|
+
* hosted dojo generator and the catalog-bff persistence path so all three strip
|
|
4
|
+
* the exact same constructs.
|
|
4
5
|
*
|
|
5
6
|
* Regex-based (not DOM-based) on purpose: it must run in Node with no DOMParser
|
|
6
7
|
* — the CLI runs offline in a terminal, and the dojo API route runs server-side.
|
|
7
8
|
* Patterns intentionally over-remove rather than under-remove: an icon has no
|
|
8
9
|
* legitimate use for scripts, styles, animation, or external references.
|
|
9
10
|
*
|
|
10
|
-
* SINGLE SOURCE,
|
|
11
|
-
* tools/extenshi-cli/src/svg-scrub.ts
|
|
11
|
+
* SINGLE SOURCE, THREE COPIES: this file exists byte-identical at
|
|
12
|
+
* tools/extenshi-cli/src/svg-scrub.ts (canonical — edit here)
|
|
12
13
|
* dojo/src/lib/tools/svg-scrub.ts (synced copy for the dojo build)
|
|
14
|
+
* catalog/catalog-bff/src/lib/svg-scrub.ts (synced copy for the BFF build)
|
|
13
15
|
* because the published @extenshi/cli npm package cannot depend on unpublished
|
|
14
|
-
* workspace packages, and dojo's
|
|
16
|
+
* workspace packages, and neither dojo's nor the BFF's Docker build builds the
|
|
17
|
+
* CLI package. The BFF copy is the load-bearing one for welcome pages: it
|
|
18
|
+
* sanitizes agent-authored SVG *before it is persisted*, so a hostile block can
|
|
19
|
+
* never reach the database, let alone a rendered page.
|
|
15
20
|
* A test in tools/extenshi-cli/src/icon.test.ts fails CI when the copies drift;
|
|
16
|
-
* after editing, run:
|
|
21
|
+
* after editing, run:
|
|
22
|
+
* cp tools/extenshi-cli/src/svg-scrub.ts dojo/src/lib/tools/svg-scrub.ts
|
|
23
|
+
* cp tools/extenshi-cli/src/svg-scrub.ts catalog/catalog-bff/src/lib/svg-scrub.ts
|
|
17
24
|
*
|
|
18
25
|
* This is the same rationale as icon-preview.ts (see the note there).
|
|
19
26
|
*/
|
package/dist/svg-scrub.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"svg-scrub.js","sourceRoot":"","sources":["../src/svg-scrub.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"svg-scrub.js","sourceRoot":"","sources":["../src/svg-scrub.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH,8EAA8E;AAC9E,+EAA+E;AAC/E,0EAA0E;AAC1E,4CAA4C;AAC5C,MAAM,CAAC,MAAM,kBAAkB,GAAG;IACjC,QAAQ;IACR,OAAO;IACP,eAAe;IACf,QAAQ;IACR,QAAQ;IACR,OAAO;IACP,QAAQ;IACR,MAAM;IACN,OAAO;IACP,UAAU;IACV,QAAQ;IACR,QAAQ;IACR,SAAS;IACT,kBAAkB;IAClB,eAAe;IACf,KAAK;CACI,CAAA;AAEV,MAAM,QAAQ,GAAG,kBAAkB,CAAC,IAAI,CAAC,GAAG,CAAC,CAAA;AAE7C;;;;;;GAMG;AACH,MAAM,UAAU,cAAc,CAAC,GAAW;IACzC,OAAO,CACN,GAAG;QACF,mEAAmE;QACnE,gEAAgE;QAChE,oEAAoE;QACpE,sEAAsE;SACrE,OAAO,CAAC,mCAAmC,EAAE,EAAE,CAAC;SAChD,OAAO,CAAC,mBAAmB,EAAE,EAAE,CAAC;SAChC,OAAO,CAAC,qBAAqB,EAAE,EAAE,CAAC;QACnC,2EAA2E;QAC3E,oEAAoE;QACpE,wEAAwE;QACxE,yEAAyE;QACzE,wEAAwE;SACvE,OAAO,CAAC,IAAI,MAAM,CAAC,SAAS,QAAQ,kCAAkC,EAAE,IAAI,CAAC,EAAE,EAAE,CAAC;SAClF,OAAO,CAAC,IAAI,MAAM,CAAC,SAAS,QAAQ,cAAc,EAAE,IAAI,CAAC,EAAE,EAAE,CAAC;QAC/D,8DAA8D;SAC7D,OAAO,CAAC,4BAA4B,EAAE,EAAE,CAAC;SACzC,OAAO,CAAC,4BAA4B,EAAE,EAAE,CAAC;SACzC,OAAO,CAAC,4BAA4B,EAAE,EAAE,CAAC;QAC1C,sDAAsD;SACrD,OAAO,CAAC,4DAA4D,EAAE,EAAE,CAAC;SACzE,OAAO,CAAC,4DAA4D,EAAE,EAAE,CAAC;SACzE,IAAI,EAAE,CACR,CAAA;AACF,CAAC"}
|