@everystack/cli 0.3.3 → 0.3.5

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@everystack/cli",
3
- "version": "0.3.3",
3
+ "version": "0.3.5",
4
4
  "description": "CLI and OTA updates for Expo apps on everystack",
5
5
  "license": "AGPL-3.0-only",
6
6
  "author": "Scalable Technology, Inc. <licensing@scalable.technology>",
@@ -29,6 +29,10 @@
29
29
  "types": "./src/cli/model-api.ts",
30
30
  "default": "./src/cli/model-api.ts"
31
31
  },
32
+ "./audit": {
33
+ "types": "./src/cli/audit-api.ts",
34
+ "default": "./src/cli/audit-api.ts"
35
+ },
32
36
  "./client": {
33
37
  "types": "./src/client/index.ts",
34
38
  "default": "./src/client/index.ts"
@@ -57,15 +61,22 @@
57
61
  "bin": {
58
62
  "everystack": "./src/cli/index.ts"
59
63
  },
64
+ "scripts": {
65
+ "test": "jest",
66
+ "build": "tsc --build",
67
+ "prepublishOnly": "tsc --module commonjs --moduleResolution node --target ES2022 --esModuleInterop --skipLibCheck --declaration false --outDir src src/env.ts",
68
+ "lint": "tsc --noEmit"
69
+ },
60
70
  "dependencies": {
61
71
  "@aws-sdk/client-cloudfront": "3.1053.0",
62
72
  "@aws-sdk/client-cloudfront-keyvaluestore": "3.1053.0",
63
73
  "@aws-sdk/signature-v4a": "3.1048.0",
74
+ "@everystack/model": "workspace:*",
64
75
  "glob": "13.0.6",
65
76
  "node-forge": "1.4.0",
66
77
  "structured-headers": "1.0.1",
67
78
  "tsx": "4.21.0",
68
- "@everystack/model": "0.3.3"
79
+ "typescript": "5.9.3"
69
80
  },
70
81
  "peerDependencies": {
71
82
  "@everystack/server": ">=0.1.0",
@@ -131,12 +142,6 @@
131
142
  "drizzle-orm": "0.41.0",
132
143
  "jest": "29.7.0",
133
144
  "react": "19.2.6",
134
- "ts-jest": "29.4.9",
135
- "typescript": "5.9.3"
136
- },
137
- "scripts": {
138
- "test": "jest",
139
- "build": "tsc --build",
140
- "lint": "tsc --noEmit"
145
+ "ts-jest": "29.4.9"
141
146
  }
142
- }
147
+ }
@@ -0,0 +1,97 @@
1
+ /**
2
+ * Public audit surface — `@everystack/cli/audit`.
3
+ *
4
+ * The governance MCP imports these pure scanners directly to gate authoring and
5
+ * pre-deploy: the secret-leak checks (bundle-audit) and the weight/size checks
6
+ * (bundle-weight), plus the bundle composition helpers they build on. Pure,
7
+ * filesystem-free, no CLI/IO coupling — feed them text or {path, size} lists.
8
+ */
9
+
10
+ export {
11
+ // types
12
+ type AuditFinding,
13
+ type SecretPattern,
14
+ type SourceLoc,
15
+ type FixDescriptor,
16
+ // leak scanners
17
+ SECRET_PATTERNS,
18
+ redact,
19
+ isPublicEnvKey,
20
+ scanSecretShapes,
21
+ extractBundleUrls,
22
+ extractExtraEnvKeys,
23
+ classifyPrivateEnvKeys,
24
+ scanSecretValue,
25
+ summarize,
26
+ } from './bundle-audit.js';
27
+
28
+ export {
29
+ // types
30
+ type AssetFile,
31
+ type AssetKind,
32
+ type WeightBudget,
33
+ // weight scanners
34
+ DEFAULT_WEIGHT_BUDGET,
35
+ classifyAsset,
36
+ formatBytes,
37
+ scanAssetWeights,
38
+ scanTotalWeight,
39
+ scanJsWeight,
40
+ scanSourceMapsPresent,
41
+ scanDependencyWeight,
42
+ resolveWeightBudget,
43
+ } from './bundle-weight.js';
44
+
45
+ export {
46
+ // types
47
+ type UiBloatBudget,
48
+ // UI-bloat scanners (remote / bundle side of the design-bloat loop)
49
+ DEFAULT_UI_BLOAT_BUDGET,
50
+ canonicalizeStyle,
51
+ isDesignStyle,
52
+ extractStyleLiterals,
53
+ scanRepeatedStyles,
54
+ scanHardcodedColors,
55
+ scanBundleUiBloat,
56
+ resolveUiBloatBudget,
57
+ } from './ui-bloat.js';
58
+
59
+ export {
60
+ // source-side UI-bloat checks (located + fixable + self-verifying)
61
+ type ComponentBudget,
62
+ type FileRole,
63
+ DEFAULT_COMPONENT_BUDGET,
64
+ classifyRole,
65
+ scanComponentRepeatedStyles,
66
+ scanComponentHardcodedColors,
67
+ scanUiReimplementation,
68
+ discoverUiExports,
69
+ scanComponentSize,
70
+ scanDuplicateJsx,
71
+ scanDuplicateComponents,
72
+ auditComponents,
73
+ } from './component-audit.js';
74
+
75
+ export {
76
+ // types — the faithful component model (shared with the 5b frontend Module)
77
+ type ComponentDescriptor,
78
+ type ImportRef,
79
+ type JsxNode,
80
+ type StyleLiteral,
81
+ // the source-side foundation: parse/discover .tsx into neutral descriptors
82
+ parseComponents,
83
+ discoverComponents,
84
+ } from './component-model.js';
85
+
86
+ export {
87
+ // composition (input to scanDependencyWeight)
88
+ type Composition,
89
+ type ChunkInput,
90
+ type ChunkSize,
91
+ type SourceMap,
92
+ measureChunk,
93
+ measureChunks,
94
+ totalSizes,
95
+ bucketSource,
96
+ composeSourceMap,
97
+ } from './bundle.js';
@@ -0,0 +1,225 @@
1
+ /**
2
+ * Bundle secret-leak audit — pure scanners for `everystack bundle:audit`.
3
+ *
4
+ * Scans the *served* client bundle for leaked secrets. Config-free by leaning on
5
+ * universal truths instead of a hand-maintained deny-list:
6
+ *
7
+ * 1. Generic secret SHAPES (Postgres/Mongo/Redis URLs, sk_live_, AKIA…, PEM
8
+ * blocks) — these match a secret's *value* regardless of its key name.
9
+ * 2. The Expo convention: Metro only inlines `EXPO_PUBLIC_*` env vars into the
10
+ * client bundle. Any *other* env key serialized into the bundle is, by
11
+ * definition, a private value that leaked.
12
+ * 3. (Operator-run) value canary: grep the bundle for the literal values of the
13
+ * deployed secrets — the only way to catch a Metro-inlined value whose key
14
+ * name never appears. Values are redacted in output; never printed.
15
+ *
16
+ * Nothing here reads secret values; the canary matcher receives them from the
17
+ * command's operator-run path.
18
+ */
19
+
20
+ /** A source location an agent can open and edit. */
21
+ export interface SourceLoc {
22
+ file: string;
23
+ line: number;
24
+ }
25
+
26
+ /** A machine-actionable remediation for a finding. Source-side findings carry one
27
+ * so the agent can apply it without re-deriving the fix from prose. */
28
+ export interface FixDescriptor {
29
+ kind:
30
+ | 'extract-component'
31
+ | 'replace-with-token'
32
+ | 'use-ui-component'
33
+ | 'split-component'
34
+ | 'unify-component';
35
+ summary: string;
36
+ /** extract-component: a suggested name for the new component. */
37
+ suggestedName?: string;
38
+ /** replace-with-token: the raw value to replace and the token to use. */
39
+ from?: string;
40
+ to?: string;
41
+ }
42
+
43
+ export interface AuditFinding {
44
+ severity: 'fail' | 'warn';
45
+ check:
46
+ // leak checks (this file)
47
+ | 'secret-shape'
48
+ | 'source-map'
49
+ | 'private-env-key'
50
+ | 'secret-value'
51
+ // weight/size checks (bundle-weight.ts)
52
+ | 'asset-weight'
53
+ | 'total-weight'
54
+ | 'data-in-bundle'
55
+ | 'js-weight'
56
+ | 'dep-weight'
57
+ // UI-bloat checks (ui-bloat.ts / component-audit.ts)
58
+ | 'ui-repeated-style'
59
+ | 'ui-hardcoded-color'
60
+ | 'ui-reimplementation'
61
+ | 'component-size'
62
+ | 'jsx-depth'
63
+ | 'hook-count'
64
+ | 'duplicate-jsx'
65
+ | 'duplicate-component';
66
+ source: string;
67
+ detail: string;
68
+ /** Source-side only: every place the smell occurs (the remote bundle cannot give
69
+ * these — it is mangled and pathless). */
70
+ occurrences?: SourceLoc[];
71
+ /** Source-side only: how to fix it. */
72
+ fix?: FixDescriptor;
73
+ /** How to confirm the fix landed. Uniform: re-run the audit; the finding clears.
74
+ * The auditor is its own verifier. */
75
+ verify?: string;
76
+ }
77
+
78
+ export interface SecretPattern {
79
+ name: string;
80
+ re: RegExp;
81
+ }
82
+
83
+ /** Generic, app-agnostic secret shapes. A value matching any of these in a
84
+ * client bundle is a leak no matter what env key carried it. */
85
+ export const SECRET_PATTERNS: SecretPattern[] = [
86
+ { name: 'postgres-url-with-credentials', re: /postgres(?:ql)?:\/\/[^:\s'"`]+:[^@\s'"`]+@/ },
87
+ { name: 'mongodb-url-with-credentials', re: /mongodb(?:\+srv)?:\/\/[^:\s'"`]+:[^@\s'"`]+@/ },
88
+ { name: 'redis-url-with-credentials', re: /redis:\/\/[^:\s'"`]*:[^@\s'"`]+@/ },
89
+ { name: 'stripe-secret-key', re: /sk_(?:live|test)_[A-Za-z0-9]{10,}/ },
90
+ { name: 'mapbox-secret-token', re: /sk\.[A-Za-z0-9_-]{60,}/ },
91
+ { name: 'sendgrid-key', re: /SG\.[A-Za-z0-9_-]{16,}\.[A-Za-z0-9_-]{16,}/ },
92
+ { name: 'google-api-key', re: /AIza[0-9A-Za-z_-]{35}/ },
93
+ { name: 'github-token', re: /gh[pousr]_[A-Za-z0-9]{36,}/ },
94
+ { name: 'slack-token', re: /xox[baprs]-[A-Za-z0-9-]{10,}/ },
95
+ { name: 'aws-access-key-id', re: /AKIA[0-9A-Z]{16}/ },
96
+ { name: 'pem-private-key', re: /-----BEGIN (?:RSA |EC |OPENSSH |DSA |PGP )?PRIVATE KEY-----/ },
97
+ { name: 'generic-credentials-in-url', re: /\b[a-z][a-z0-9+.-]*:\/\/[A-Za-z0-9._%-]+:[^@\s'"`/]{6,}@/ },
98
+ ];
99
+
100
+ /** Mask a matched secret so the output never prints it in full. */
101
+ export function redact(s: string): string {
102
+ if (s.length <= 6) return '******';
103
+ return `${s.slice(0, 4)}…${s.slice(-2)} (${s.length} chars)`;
104
+ }
105
+
106
+ /** True if an env key is client-public by Expo convention (Metro-inlined). */
107
+ export function isPublicEnvKey(key: string): boolean {
108
+ return key.startsWith('EXPO_PUBLIC_');
109
+ }
110
+
111
+ /** Scan one artifact's text for generic secret shapes. Matches are redacted. */
112
+ export function scanSecretShapes(text: string, source: string): AuditFinding[] {
113
+ const findings: AuditFinding[] = [];
114
+ const seen = new Set<string>(); // dedupe overlapping patterns on the same span
115
+ for (const { name, re } of SECRET_PATTERNS) {
116
+ const m = re.exec(text);
117
+ if (m && !seen.has(m[0])) {
118
+ seen.add(m[0]);
119
+ findings.push({
120
+ severity: 'fail',
121
+ check: 'secret-shape',
122
+ source,
123
+ detail: `${name}: ${redact(m[0])}`,
124
+ });
125
+ }
126
+ }
127
+ return findings;
128
+ }
129
+
130
+ /** Extract client JS bundle URLs from served HTML (Expo web export convention). */
131
+ export function extractBundleUrls(html: string): string[] {
132
+ const urls = new Set<string>();
133
+ const re = /["'(]([^"'()]*_expo\/static\/js\/web\/[^"'()?]+\.js)/g;
134
+ let m: RegExpExecArray | null;
135
+ while ((m = re.exec(html)) !== null) {
136
+ urls.add(m[1].replace(/^\.?\//, ''));
137
+ }
138
+ return [...urls];
139
+ }
140
+
141
+ /**
142
+ * Extract served *static asset* references (images, data files, fonts, css)
143
+ * from a bundle/HTML — anything under `assets/` or `_expo/static/` that is not
144
+ * JS or a source map (those are crawled / gated separately). Used by the remote
145
+ * weight scan to discover what to HEAD for its size without downloading it.
146
+ */
147
+ export function extractAssetUrls(text: string): string[] {
148
+ const urls = new Set<string>();
149
+ const re = /["'(]((?:\.?\/)?(?:assets|_expo\/static)\/[^"'()\s]+?\.[A-Za-z0-9]+)(?:\?[^"'()\s]*)?["')]/g;
150
+ let m: RegExpExecArray | null;
151
+ while ((m = re.exec(text)) !== null) {
152
+ const rel = m[1].replace(/^\.?\//, '');
153
+ if (/\.(?:js|mjs|cjs|map)$/i.test(rel)) continue;
154
+ urls.add(rel);
155
+ }
156
+ return [...urls];
157
+ }
158
+
159
+ const EXTRA_KEY_SKIP = new Set([
160
+ 'JSON', 'URL', 'GET', 'POST', 'PUT', 'DELETE', 'HEAD', 'OPTIONS', 'PATCH',
161
+ 'API', 'HTTP', 'HTTPS', 'UTF', 'UUID', 'ID', 'OK', 'EAS',
162
+ ]);
163
+
164
+ /**
165
+ * Best-effort extraction of ALL-CAPS env-looking keys serialized near an
166
+ * `"extra"` region of the bundle (where Expo embeds app.config `extra`). No EAS
167
+ * projectId anchor needed — we scan a window around each `"extra"` occurrence.
168
+ */
169
+ export function extractExtraEnvKeys(text: string, window = 600): string[] {
170
+ const keys = new Set<string>();
171
+ const keyRe = /["']([A-Z][A-Z0-9_]{2,})["']\s*:/g;
172
+ let idx = text.indexOf('"extra"');
173
+ while (idx !== -1) {
174
+ const region = text.slice(idx, idx + window);
175
+ let m: RegExpExecArray | null;
176
+ keyRe.lastIndex = 0;
177
+ while ((m = keyRe.exec(region)) !== null) {
178
+ if (!EXTRA_KEY_SKIP.has(m[1])) keys.add(m[1]);
179
+ }
180
+ idx = text.indexOf('"extra"', idx + 1);
181
+ }
182
+ return [...keys];
183
+ }
184
+
185
+ /** Flag any non-EXPO_PUBLIC env key found embedded in the bundle's extra. */
186
+ export function classifyPrivateEnvKeys(keys: string[], source: string): AuditFinding[] {
187
+ return keys
188
+ .filter((k) => !isPublicEnvKey(k))
189
+ .map((k) => ({
190
+ severity: 'fail' as const,
191
+ check: 'private-env-key' as const,
192
+ source,
193
+ detail: `${k} is embedded in the bundle but is not EXPO_PUBLIC_* — a private value leaked`,
194
+ }));
195
+ }
196
+
197
+ /**
198
+ * Operator-run value canary: does the bundle contain the literal secret value?
199
+ * Returns a finding with the value REDACTED (replaced by a [KEY] marker) — the
200
+ * raw value is never placed in the output.
201
+ */
202
+ export function scanSecretValue(
203
+ text: string,
204
+ key: string,
205
+ value: string,
206
+ source: string,
207
+ ): AuditFinding | null {
208
+ if (isPublicEnvKey(key)) return null; // public by design
209
+ if (value.length < 8) return null; // too short to be a meaningful secret
210
+ const at = text.indexOf(value);
211
+ if (at === -1) return null;
212
+ return {
213
+ severity: 'fail',
214
+ check: 'secret-value',
215
+ source,
216
+ detail: `value of ${key} appears verbatim in the bundle (Metro-inlined leak) [${redact(value)}]`,
217
+ };
218
+ }
219
+
220
+ export function summarize(findings: AuditFinding[]): { fail: number; warn: number } {
221
+ return {
222
+ fail: findings.filter((f) => f.severity === 'fail').length,
223
+ warn: findings.filter((f) => f.severity === 'warn').length,
224
+ };
225
+ }