@freeappstore/compliance 0.2.0 → 0.3.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.
Files changed (37) hide show
  1. package/dist/checks/no-scroll.d.ts +24 -0
  2. package/dist/checks/no-scroll.d.ts.map +1 -0
  3. package/dist/checks/no-scroll.js +143 -0
  4. package/dist/checks/no-scroll.js.map +1 -0
  5. package/dist/checks/no-scroll.test.d.ts +2 -0
  6. package/dist/checks/no-scroll.test.d.ts.map +1 -0
  7. package/dist/checks/no-scroll.test.js +105 -0
  8. package/dist/checks/no-scroll.test.js.map +1 -0
  9. package/dist/checks/unsafe-vh.d.ts +24 -0
  10. package/dist/checks/unsafe-vh.d.ts.map +1 -0
  11. package/dist/checks/unsafe-vh.js +110 -0
  12. package/dist/checks/unsafe-vh.js.map +1 -0
  13. package/dist/checks/unsafe-vh.test.d.ts +2 -0
  14. package/dist/checks/unsafe-vh.test.d.ts.map +1 -0
  15. package/dist/checks/unsafe-vh.test.js +115 -0
  16. package/dist/checks/unsafe-vh.test.js.map +1 -0
  17. package/dist/checks/viewport-support.d.ts +23 -0
  18. package/dist/checks/viewport-support.d.ts.map +1 -0
  19. package/dist/checks/viewport-support.js +87 -0
  20. package/dist/checks/viewport-support.js.map +1 -0
  21. package/dist/checks/viewport-support.test.d.ts +2 -0
  22. package/dist/checks/viewport-support.test.d.ts.map +1 -0
  23. package/dist/checks/viewport-support.test.js +63 -0
  24. package/dist/checks/viewport-support.test.js.map +1 -0
  25. package/dist/index.d.ts +6 -1
  26. package/dist/index.d.ts.map +1 -1
  27. package/dist/index.js +11 -1
  28. package/dist/index.js.map +1 -1
  29. package/dist/live/index.d.ts +50 -0
  30. package/dist/live/index.d.ts.map +1 -0
  31. package/dist/live/index.js +311 -0
  32. package/dist/live/index.js.map +1 -0
  33. package/dist/live/index.test.d.ts +2 -0
  34. package/dist/live/index.test.d.ts.map +1 -0
  35. package/dist/live/index.test.js +259 -0
  36. package/dist/live/index.test.js.map +1 -0
  37. package/package.json +1 -1
@@ -0,0 +1,87 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
+ /**
4
+ * Apps and games must declare which screens and orientations they
5
+ * support, so the storefront can render a coverage badge ("works on
6
+ * 92% of devices · portrait + landscape") and visitors know what to
7
+ * expect before they tap.
8
+ *
9
+ * Two declarations are required, both in `web/public/manifest.json`:
10
+ *
11
+ * "orientation": "any" | "portrait" | "landscape" |
12
+ * "portrait-primary" | "landscape-primary"
13
+ * "min_viewport_width": 320 | 360 | 414 | 600 | 768 | 1024
14
+ *
15
+ * `orientation` is a standard PWA manifest field. `min_viewport_width`
16
+ * is a custom platform field — the smallest screen width (in CSS px)
17
+ * the app renders correctly at. The storefront maps that to a global
18
+ * device-share percentile to render coverage.
19
+ *
20
+ * If you genuinely don't care about orientation, set `"any"`. Don't
21
+ * leave it off — that's the only value that returns a fail.
22
+ */
23
+ export async function checkViewportSupport(repoDir) {
24
+ const manifestPath = join(repoDir, 'web', 'public', 'manifest.json');
25
+ let manifest;
26
+ try {
27
+ const raw = await readFile(manifestPath, 'utf8');
28
+ manifest = JSON.parse(raw);
29
+ }
30
+ catch {
31
+ return {
32
+ name: 'Viewport support',
33
+ status: 'fail',
34
+ detail: 'web/public/manifest.json not found or unparseable',
35
+ };
36
+ }
37
+ const orientation = manifest['orientation'];
38
+ const minWidth = manifest['min_viewport_width'];
39
+ const issues = [];
40
+ const warnings = [];
41
+ if (typeof orientation !== 'string' || !VALID_ORIENTATIONS.has(orientation)) {
42
+ issues.push(`manifest.orientation is "${String(orientation)}" — must be one of: ${[...VALID_ORIENTATIONS].join(', ')}`);
43
+ }
44
+ if (typeof minWidth !== 'number') {
45
+ warnings.push('manifest.min_viewport_width is missing — assuming 320 (strictest). ' +
46
+ 'Set explicitly to e.g. 360 / 414 / 600 / 768 / 1024 to declare which devices the app supports.');
47
+ }
48
+ else if (minWidth < 320) {
49
+ issues.push(`manifest.min_viewport_width=${minWidth} is below the 320px minimum`);
50
+ }
51
+ else if (!RECOMMENDED_WIDTHS.includes(minWidth)) {
52
+ warnings.push(`manifest.min_viewport_width=${minWidth} is unusual — recommended values: ${RECOMMENDED_WIDTHS.join(', ')}`);
53
+ }
54
+ if (issues.length > 0) {
55
+ return {
56
+ name: 'Viewport support',
57
+ status: 'fail',
58
+ detail: `${issues.length} viewport-declaration issue${issues.length === 1 ? '' : 's'}`,
59
+ suggestions: [
60
+ ...issues,
61
+ 'Edit web/public/manifest.json: add "orientation": "any" (or portrait/landscape) and "min_viewport_width": 360.',
62
+ ],
63
+ };
64
+ }
65
+ if (warnings.length > 0) {
66
+ return {
67
+ name: 'Viewport support',
68
+ status: 'warn',
69
+ detail: warnings[0],
70
+ suggestions: warnings,
71
+ };
72
+ }
73
+ return {
74
+ name: 'Viewport support',
75
+ status: 'pass',
76
+ detail: `orientation=${orientation} · min ${minWidth}px`,
77
+ };
78
+ }
79
+ const VALID_ORIENTATIONS = new Set([
80
+ 'any',
81
+ 'portrait',
82
+ 'landscape',
83
+ 'portrait-primary',
84
+ 'landscape-primary',
85
+ ]);
86
+ const RECOMMENDED_WIDTHS = [320, 360, 414, 600, 768, 1024];
87
+ //# sourceMappingURL=viewport-support.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"viewport-support.js","sourceRoot":"","sources":["../../src/checks/viewport-support.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=viewport-support.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"viewport-support.test.d.ts","sourceRoot":"","sources":["../../src/checks/viewport-support.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,63 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { mkdtemp, mkdir, writeFile } from 'node:fs/promises';
3
+ import { tmpdir } from 'node:os';
4
+ import { join } from 'node:path';
5
+ import { checkViewportSupport } from './viewport-support.js';
6
+ async function fixture(manifest) {
7
+ const dir = await mkdtemp(join(tmpdir(), 'fas-viewport-'));
8
+ if (manifest !== null) {
9
+ await mkdir(join(dir, 'web', 'public'), { recursive: true });
10
+ await writeFile(join(dir, 'web', 'public', 'manifest.json'), JSON.stringify(manifest));
11
+ }
12
+ return dir;
13
+ }
14
+ describe('checkViewportSupport', () => {
15
+ it('fails when manifest.json is missing', async () => {
16
+ const dir = await fixture(null);
17
+ const r = await checkViewportSupport(dir);
18
+ expect(r.status).toBe('fail');
19
+ expect(r.detail).toMatch(/manifest\.json/);
20
+ });
21
+ it('fails when orientation is missing', async () => {
22
+ const dir = await fixture({ name: 'x' });
23
+ const r = await checkViewportSupport(dir);
24
+ expect(r.status).toBe('fail');
25
+ expect(r.suggestions?.join(' ')).toMatch(/orientation/);
26
+ });
27
+ it('fails when orientation is invalid', async () => {
28
+ const dir = await fixture({ name: 'x', orientation: 'rotate' });
29
+ const r = await checkViewportSupport(dir);
30
+ expect(r.status).toBe('fail');
31
+ });
32
+ it('warns when min_viewport_width is missing but orientation is valid', async () => {
33
+ const dir = await fixture({ name: 'x', orientation: 'any' });
34
+ const r = await checkViewportSupport(dir);
35
+ expect(r.status).toBe('warn');
36
+ expect(r.detail).toMatch(/min_viewport_width/);
37
+ });
38
+ it('fails when min_viewport_width is below 320', async () => {
39
+ const dir = await fixture({ name: 'x', orientation: 'any', min_viewport_width: 280 });
40
+ const r = await checkViewportSupport(dir);
41
+ expect(r.status).toBe('fail');
42
+ });
43
+ it('warns on unusual but accepted widths', async () => {
44
+ const dir = await fixture({ name: 'x', orientation: 'any', min_viewport_width: 387 });
45
+ const r = await checkViewportSupport(dir);
46
+ expect(r.status).toBe('warn');
47
+ expect(r.detail).toMatch(/unusual/);
48
+ });
49
+ it('passes for valid orientation + recommended width', async () => {
50
+ const dir = await fixture({ name: 'x', orientation: 'any', min_viewport_width: 360 });
51
+ const r = await checkViewportSupport(dir);
52
+ expect(r.status).toBe('pass');
53
+ expect(r.detail).toMatch(/any/);
54
+ expect(r.detail).toMatch(/360/);
55
+ });
56
+ it('passes for portrait-only landscape-only too', async () => {
57
+ const dir = await fixture({ name: 'x', orientation: 'portrait', min_viewport_width: 414 });
58
+ expect((await checkViewportSupport(dir)).status).toBe('pass');
59
+ const dir2 = await fixture({ name: 'x', orientation: 'landscape-primary', min_viewport_width: 768 });
60
+ expect((await checkViewportSupport(dir2)).status).toBe('pass');
61
+ });
62
+ });
63
+ //# sourceMappingURL=viewport-support.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"viewport-support.test.js","sourceRoot":"","sources":["../../src/checks/viewport-support.test.ts"],"names":[],"mappings":"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"}
package/dist/index.d.ts CHANGED
@@ -2,11 +2,16 @@ import { checkNoPlaceholders } from './checks/no-placeholders.js';
2
2
  import { checkNoTracking } from './checks/no-tracking.js';
3
3
  import { checkBrandFonts } from './checks/brand-fonts.js';
4
4
  import { checkNoBrandOverrides } from './checks/no-brand-overrides.js';
5
+ import { checkNoScroll } from './checks/no-scroll.js';
6
+ import { checkViewportSupport } from './checks/viewport-support.js';
5
7
  import { checkManifest } from './checks/manifest.js';
6
8
  import { checkBundleSize } from './checks/bundle-size.js';
9
+ import { checkUnsafeVh } from './checks/unsafe-vh.js';
7
10
  import type { CheckResult } from './types.js';
8
11
  export type { CheckStatus, CheckResult } from './types.js';
9
- export { checkNoPlaceholders, checkNoTracking, checkBrandFonts, checkNoBrandOverrides, checkManifest, checkBundleSize, };
12
+ export { checkNoPlaceholders, checkNoTracking, checkBrandFonts, checkNoBrandOverrides, checkNoScroll, checkViewportSupport, checkManifest, checkBundleSize, checkUnsafeVh, };
13
+ export { auditLive, checkNoTrackingLive, checkBrandFontsLive, checkManifestLive, checkBundleSizeLive, checkUnsafeVhLive, } from './live/index.js';
14
+ export type { LiveAuditInput, LiveAuditReport } from './live/index.js';
10
15
  /**
11
16
  * Runs every compliance check against `repoDir` (the root of an app, the
12
17
  * directory containing package.json + web/). Returns results in a stable
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,MAAM,6BAA6B,CAAC;AAClE,OAAO,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAC1D,OAAO,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAC1D,OAAO,EAAE,qBAAqB,EAAE,MAAM,gCAAgC,CAAC;AACvE,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AACrD,OAAO,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAC1D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAE9C,YAAY,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAC3D,OAAO,EACL,mBAAmB,EACnB,eAAe,EACf,eAAe,EACf,qBAAqB,EACrB,aAAa,EACb,eAAe,GAChB,CAAC;AAEF;;;;GAIG;AACH,wBAAsB,SAAS,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,EAAE,CAAC,CASvE"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,MAAM,6BAA6B,CAAC;AAClE,OAAO,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAC1D,OAAO,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAC1D,OAAO,EAAE,qBAAqB,EAAE,MAAM,gCAAgC,CAAC;AACvE,OAAO,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AACtD,OAAO,EAAE,oBAAoB,EAAE,MAAM,8BAA8B,CAAC;AACpE,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AACrD,OAAO,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAC1D,OAAO,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AACtD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAE9C,YAAY,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAC3D,OAAO,EACL,mBAAmB,EACnB,eAAe,EACf,eAAe,EACf,qBAAqB,EACrB,aAAa,EACb,oBAAoB,EACpB,aAAa,EACb,eAAe,EACf,aAAa,GACd,CAAC;AAKF,OAAO,EACL,SAAS,EACT,mBAAmB,EACnB,mBAAmB,EACnB,iBAAiB,EACjB,mBAAmB,EACnB,iBAAiB,GAClB,MAAM,iBAAiB,CAAC;AACzB,YAAY,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAEvE;;;;GAIG;AACH,wBAAsB,SAAS,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,EAAE,CAAC,CAYvE"}
package/dist/index.js CHANGED
@@ -2,9 +2,16 @@ import { checkNoPlaceholders } from './checks/no-placeholders.js';
2
2
  import { checkNoTracking } from './checks/no-tracking.js';
3
3
  import { checkBrandFonts } from './checks/brand-fonts.js';
4
4
  import { checkNoBrandOverrides } from './checks/no-brand-overrides.js';
5
+ import { checkNoScroll } from './checks/no-scroll.js';
6
+ import { checkViewportSupport } from './checks/viewport-support.js';
5
7
  import { checkManifest } from './checks/manifest.js';
6
8
  import { checkBundleSize } from './checks/bundle-size.js';
7
- export { checkNoPlaceholders, checkNoTracking, checkBrandFonts, checkNoBrandOverrides, checkManifest, checkBundleSize, };
9
+ import { checkUnsafeVh } from './checks/unsafe-vh.js';
10
+ export { checkNoPlaceholders, checkNoTracking, checkBrandFonts, checkNoBrandOverrides, checkNoScroll, checkViewportSupport, checkManifest, checkBundleSize, checkUnsafeVh, };
11
+ // Live-URL audit (used by the compliance audit Worker; runs in
12
+ // browser/Workers env, no filesystem). Separate export path so callers
13
+ // don't accidentally pull node:fs in via the file-walking checks.
14
+ export { auditLive, checkNoTrackingLive, checkBrandFontsLive, checkManifestLive, checkBundleSizeLive, checkUnsafeVhLive, } from './live/index.js';
8
15
  /**
9
16
  * Runs every compliance check against `repoDir` (the root of an app, the
10
17
  * directory containing package.json + web/). Returns results in a stable
@@ -16,6 +23,9 @@ export async function runChecks(repoDir) {
16
23
  checkNoTracking(repoDir),
17
24
  checkBrandFonts(repoDir),
18
25
  checkNoBrandOverrides(repoDir),
26
+ checkNoScroll(repoDir),
27
+ checkViewportSupport(repoDir),
28
+ checkUnsafeVh(repoDir),
19
29
  checkManifest(repoDir),
20
30
  checkBundleSize(repoDir),
21
31
  ]);
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,MAAM,6BAA6B,CAAC;AAClE,OAAO,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAC1D,OAAO,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAC1D,OAAO,EAAE,qBAAqB,EAAE,MAAM,gCAAgC,CAAC;AACvE,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AACrD,OAAO,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAI1D,OAAO,EACL,mBAAmB,EACnB,eAAe,EACf,eAAe,EACf,qBAAqB,EACrB,aAAa,EACb,eAAe,GAChB,CAAC;AAEF;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,SAAS,CAAC,OAAe;IAC7C,OAAO,OAAO,CAAC,GAAG,CAAC;QACjB,mBAAmB,CAAC,OAAO,CAAC;QAC5B,eAAe,CAAC,OAAO,CAAC;QACxB,eAAe,CAAC,OAAO,CAAC;QACxB,qBAAqB,CAAC,OAAO,CAAC;QAC9B,aAAa,CAAC,OAAO,CAAC;QACtB,eAAe,CAAC,OAAO,CAAC;KACzB,CAAC,CAAC;AACL,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,MAAM,6BAA6B,CAAC;AAClE,OAAO,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAC1D,OAAO,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAC1D,OAAO,EAAE,qBAAqB,EAAE,MAAM,gCAAgC,CAAC;AACvE,OAAO,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AACtD,OAAO,EAAE,oBAAoB,EAAE,MAAM,8BAA8B,CAAC;AACpE,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AACrD,OAAO,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAC1D,OAAO,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAItD,OAAO,EACL,mBAAmB,EACnB,eAAe,EACf,eAAe,EACf,qBAAqB,EACrB,aAAa,EACb,oBAAoB,EACpB,aAAa,EACb,eAAe,EACf,aAAa,GACd,CAAC;AAEF,+DAA+D;AAC/D,uEAAuE;AACvE,kEAAkE;AAClE,OAAO,EACL,SAAS,EACT,mBAAmB,EACnB,mBAAmB,EACnB,iBAAiB,EACjB,mBAAmB,EACnB,iBAAiB,GAClB,MAAM,iBAAiB,CAAC;AAGzB;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,SAAS,CAAC,OAAe;IAC7C,OAAO,OAAO,CAAC,GAAG,CAAC;QACjB,mBAAmB,CAAC,OAAO,CAAC;QAC5B,eAAe,CAAC,OAAO,CAAC;QACxB,eAAe,CAAC,OAAO,CAAC;QACxB,qBAAqB,CAAC,OAAO,CAAC;QAC9B,aAAa,CAAC,OAAO,CAAC;QACtB,oBAAoB,CAAC,OAAO,CAAC;QAC7B,aAAa,CAAC,OAAO,CAAC;QACtB,aAAa,CAAC,OAAO,CAAC;QACtB,eAAe,CAAC,OAAO,CAAC;KACzB,CAAC,CAAC;AACL,CAAC"}
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Live-URL audit checks. Unlike the file-walking checks in ../checks/,
3
+ * these run inside a Cloudflare Worker (no filesystem) against published
4
+ * apps' live URLs to catch post-publish drift.
5
+ *
6
+ * What we can check from a live URL:
7
+ * - Tracking SDK injection (script src, inline scripts, fetched JS)
8
+ * - Brand fonts presence (link href to fonts.googleapis.com)
9
+ * - PWA manifest reachable + valid
10
+ * - Main bundle size (HEAD request)
11
+ *
12
+ * What we CAN'T check post-publish:
13
+ * - APPNAME placeholders (build erases these)
14
+ * - Brand-override CSS variables (sourcemaps not always shipped)
15
+ *
16
+ * The audit Worker runs these on a weekly cron and stores per-app
17
+ * results in D1 so creators (or platform admins) can see drift over
18
+ * time without leaving the storefront.
19
+ */
20
+ import type { CheckResult } from '../types.js';
21
+ export interface LiveAuditInput {
22
+ appId: string;
23
+ liveUrl: string;
24
+ }
25
+ export interface LiveAuditReport {
26
+ appId: string;
27
+ liveUrl: string;
28
+ checkedAt: number;
29
+ reachable: boolean;
30
+ results: CheckResult[];
31
+ }
32
+ export declare function auditLive(input: LiveAuditInput): Promise<LiveAuditReport>;
33
+ export declare function checkNoTrackingLive(html: string): CheckResult;
34
+ export declare function checkBrandFontsLive(html: string): CheckResult;
35
+ export declare function checkManifestLive(html: string, liveUrl: string): Promise<CheckResult>;
36
+ export declare function checkBundleSizeLive(html: string, liveUrl: string): Promise<CheckResult>;
37
+ /**
38
+ * Live counterpart to checkUnsafeVh. Fetches the first bundled
39
+ * stylesheet linked from the HTML and scans it for `100vh` (the
40
+ * unit, not the Tailwind class — by build time those have been
41
+ * compiled to CSS). Catches whiteboard-class iOS Safari URL-bar
42
+ * scroll bugs in apps that have already shipped.
43
+ *
44
+ * Cost: one extra subrequest per app. The audit Worker runs near the
45
+ * free-plan subrequest cap; on cap errors we degrade to `warn`
46
+ * (re-run needed) rather than `fail` (app broken) — the app isn't
47
+ * actually broken, the audit just couldn't check.
48
+ */
49
+ export declare function checkUnsafeVhLive(html: string, liveUrl: string): Promise<CheckResult>;
50
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/live/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAc/C,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,OAAO,CAAC;IACnB,OAAO,EAAE,WAAW,EAAE,CAAC;CACxB;AA2BD,wBAAsB,SAAS,CAAC,KAAK,EAAE,cAAc,GAAG,OAAO,CAAC,eAAe,CAAC,CA2D/E;AAED,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,WAAW,CAa7D;AAED,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,WAAW,CAe7D;AAED,wBAAsB,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC,CA8C3F;AAED,wBAAsB,mBAAmB,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC,CAmD7F;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC,CAgD3F"}
@@ -0,0 +1,311 @@
1
+ /**
2
+ * Live-URL audit checks. Unlike the file-walking checks in ../checks/,
3
+ * these run inside a Cloudflare Worker (no filesystem) against published
4
+ * apps' live URLs to catch post-publish drift.
5
+ *
6
+ * What we can check from a live URL:
7
+ * - Tracking SDK injection (script src, inline scripts, fetched JS)
8
+ * - Brand fonts presence (link href to fonts.googleapis.com)
9
+ * - PWA manifest reachable + valid
10
+ * - Main bundle size (HEAD request)
11
+ *
12
+ * What we CAN'T check post-publish:
13
+ * - APPNAME placeholders (build erases these)
14
+ * - Brand-override CSS variables (sourcemaps not always shipped)
15
+ *
16
+ * The audit Worker runs these on a weekly cron and stores per-app
17
+ * results in D1 so creators (or platform admins) can see drift over
18
+ * time without leaving the storefront.
19
+ */
20
+ const TRACKER_PATTERNS = [
21
+ /google-analytics\.com/i,
22
+ /gtag\(/i,
23
+ /googletagmanager\.com/i,
24
+ /\bamplitude\b/i,
25
+ /\bmixpanel\b/i,
26
+ /\bsegment\.com\b/i,
27
+ /\bhotjar\.com\b/i,
28
+ /plausible\.io/i,
29
+ /posthog\.com/i,
30
+ ];
31
+ /**
32
+ * Fetch with a timeout — Workers' default fetch waits 30s+, too slow
33
+ * when iterating 50+ apps in one cron tick.
34
+ */
35
+ async function fetchWithTimeout(url, ms = 8000) {
36
+ const ctrl = new AbortController();
37
+ const timer = setTimeout(() => ctrl.abort(), ms);
38
+ try {
39
+ return await fetch(url, { signal: ctrl.signal, redirect: 'follow' });
40
+ }
41
+ finally {
42
+ clearTimeout(timer);
43
+ }
44
+ }
45
+ /**
46
+ * Worker subrequest cap errors look like "Too many subrequests by
47
+ * single Worker invocation." We don't want to record those as "fail"
48
+ * verdicts because the app isn't actually broken — the audit just
49
+ * couldn't check. Detect and flag for the caller.
50
+ */
51
+ function isSubrequestCapError(err) {
52
+ const msg = err instanceof Error ? err.message : String(err);
53
+ return /too many subrequests|subrequest.*limit/i.test(msg);
54
+ }
55
+ export async function auditLive(input) {
56
+ const checkedAt = Date.now();
57
+ const report = {
58
+ appId: input.appId,
59
+ liveUrl: input.liveUrl,
60
+ checkedAt,
61
+ reachable: false,
62
+ results: [],
63
+ };
64
+ let html;
65
+ try {
66
+ const res = await fetchWithTimeout(input.liveUrl);
67
+ if (!res.ok) {
68
+ report.results.push({
69
+ name: 'Reachable',
70
+ status: 'fail',
71
+ detail: `HTTP ${res.status} from ${input.liveUrl}`,
72
+ });
73
+ return report;
74
+ }
75
+ html = await res.text();
76
+ report.reachable = true;
77
+ // Always record a Reachable row on success — otherwise stale
78
+ // "Reachable: fail" rows from previous failed runs (e.g.
79
+ // subrequest-cap noise) never get overwritten, because the
80
+ // upsert is keyed on (app_id, check_name).
81
+ report.results.push({
82
+ name: 'Reachable',
83
+ status: 'pass',
84
+ detail: `${input.liveUrl} → HTTP ${res.status}`,
85
+ });
86
+ }
87
+ catch (err) {
88
+ // Subrequest cap means we couldn't even fetch — that's a "warn"
89
+ // (re-check needed), not a "fail" (app broken). Distinguishing
90
+ // matters: failures show red badges in the storefront.
91
+ const skipped = isSubrequestCapError(err);
92
+ report.results.push({
93
+ name: 'Reachable',
94
+ status: skipped ? 'warn' : 'fail',
95
+ detail: skipped
96
+ ? 'audit Worker hit subrequest cap — re-run needed'
97
+ : err instanceof Error
98
+ ? err.message
99
+ : 'fetch failed',
100
+ });
101
+ return report;
102
+ }
103
+ // Run all checks in parallel — each is independent.
104
+ const [tracking, fonts, manifest, bundle, vh] = await Promise.all([
105
+ checkNoTrackingLive(html),
106
+ checkBrandFontsLive(html),
107
+ checkManifestLive(html, input.liveUrl),
108
+ checkBundleSizeLive(html, input.liveUrl),
109
+ checkUnsafeVhLive(html, input.liveUrl),
110
+ ]);
111
+ report.results.push(tracking, fonts, manifest, bundle, vh);
112
+ return report;
113
+ }
114
+ export function checkNoTrackingLive(html) {
115
+ const found = TRACKER_PATTERNS.filter((re) => re.test(html));
116
+ if (found.length === 0) {
117
+ return { name: 'No tracking SDKs (live)', status: 'pass', detail: 'no known trackers in HTML' };
118
+ }
119
+ return {
120
+ name: 'No tracking SDKs (live)',
121
+ status: 'fail',
122
+ detail: `${found.length} pattern${found.length === 1 ? '' : 's'} matched`,
123
+ suggestions: [
124
+ 'Tracking was injected after publish — remove the script tag or third-party loader.',
125
+ ],
126
+ };
127
+ }
128
+ export function checkBrandFontsLive(html) {
129
+ const hasManrope = /manrope/i.test(html);
130
+ const hasFraunces = /fraunces/i.test(html);
131
+ if (hasManrope && hasFraunces) {
132
+ return { name: 'Brand fonts (live)', status: 'pass', detail: 'Manrope + Fraunces referenced' };
133
+ }
134
+ const missing = [];
135
+ if (!hasManrope)
136
+ missing.push('Manrope');
137
+ if (!hasFraunces)
138
+ missing.push('Fraunces');
139
+ return {
140
+ name: 'Brand fonts (live)',
141
+ status: 'fail',
142
+ detail: `missing: ${missing.join(', ')}`,
143
+ suggestions: ['Restore the Google Fonts <link> in web/index.html.'],
144
+ };
145
+ }
146
+ export async function checkManifestLive(html, liveUrl) {
147
+ // Find the manifest link in the HTML, then fetch + validate it.
148
+ const m = /<link[^>]*rel=["']manifest["'][^>]*href=["']([^"']+)["']/i.exec(html);
149
+ if (!m) {
150
+ return {
151
+ name: 'PWA manifest (live)',
152
+ status: 'fail',
153
+ detail: 'no <link rel="manifest"> in HTML',
154
+ };
155
+ }
156
+ const href = m[1];
157
+ const manifestUrl = new URL(href, liveUrl).toString();
158
+ try {
159
+ const res = await fetchWithTimeout(manifestUrl);
160
+ if (!res.ok) {
161
+ return {
162
+ name: 'PWA manifest (live)',
163
+ status: 'fail',
164
+ detail: `${manifestUrl} → HTTP ${res.status}`,
165
+ };
166
+ }
167
+ const body = (await res.json());
168
+ const missing = [];
169
+ if (!body.name)
170
+ missing.push('name');
171
+ if (!body.display)
172
+ missing.push('display');
173
+ if (!body.start_url)
174
+ missing.push('start_url');
175
+ if (missing.length > 0) {
176
+ return {
177
+ name: 'PWA manifest (live)',
178
+ status: 'fail',
179
+ detail: `manifest missing fields: ${missing.join(', ')}`,
180
+ };
181
+ }
182
+ return { name: 'PWA manifest (live)', status: 'pass', detail: manifestUrl };
183
+ }
184
+ catch (err) {
185
+ const skipped = isSubrequestCapError(err);
186
+ return {
187
+ name: 'PWA manifest (live)',
188
+ status: skipped ? 'warn' : 'fail',
189
+ detail: skipped
190
+ ? 'audit Worker hit subrequest cap — re-run needed'
191
+ : err instanceof Error
192
+ ? err.message
193
+ : 'manifest fetch/parse failed',
194
+ };
195
+ }
196
+ }
197
+ export async function checkBundleSizeLive(html, liveUrl) {
198
+ // Find the largest-looking bundled JS reference. Vite output looks like
199
+ // src="/assets/index-XYZ.js" — starts with `/`, no chars before. Use
200
+ // `[^"']*` (zero or more) instead of `[^"']+` (one or more) so the
201
+ // root-relative path matches; with `+` every Vite app was misreported
202
+ // as "non-Vite layout" and the audit silently skipped bundle-size.
203
+ const m = /<script[^>]+src=["']([^"']*\/assets\/[^"']+\.js)["']/i.exec(html);
204
+ if (!m) {
205
+ // Plausible miss for non-Vite apps — not a fail, just skip.
206
+ return {
207
+ name: 'Bundle size (live)',
208
+ status: 'warn',
209
+ detail: 'no /assets/*.js found — non-Vite layout?',
210
+ };
211
+ }
212
+ const jsUrl = new URL(m[1], liveUrl).toString();
213
+ try {
214
+ const res = await fetchWithTimeout(jsUrl);
215
+ if (!res.ok) {
216
+ return { name: 'Bundle size (live)', status: 'fail', detail: `${jsUrl} → ${res.status}` };
217
+ }
218
+ const body = await res.arrayBuffer();
219
+ // Approximate gzipped size as raw / 3.5. CompressionStream is in
220
+ // Workers but adding a real gzip pass is more code; the
221
+ // approximation is conservative enough for the 300 KB threshold.
222
+ const rawKb = Math.round(body.byteLength / 1024);
223
+ const approxGzipKb = Math.round(rawKb / 3.5);
224
+ if (approxGzipKb > 300) {
225
+ return {
226
+ name: 'Bundle size (live)',
227
+ status: 'fail',
228
+ detail: `~${approxGzipKb} KB gzipped (raw ${rawKb} KB) — over 300 KB limit`,
229
+ };
230
+ }
231
+ return {
232
+ name: 'Bundle size (live)',
233
+ status: 'pass',
234
+ detail: `~${approxGzipKb} KB gzipped (raw ${rawKb} KB)`,
235
+ };
236
+ }
237
+ catch (err) {
238
+ const skipped = isSubrequestCapError(err);
239
+ return {
240
+ name: 'Bundle size (live)',
241
+ status: 'warn',
242
+ detail: skipped
243
+ ? 'audit Worker hit subrequest cap — re-run needed'
244
+ : err instanceof Error
245
+ ? err.message
246
+ : 'bundle fetch failed',
247
+ };
248
+ }
249
+ }
250
+ /**
251
+ * Live counterpart to checkUnsafeVh. Fetches the first bundled
252
+ * stylesheet linked from the HTML and scans it for `100vh` (the
253
+ * unit, not the Tailwind class — by build time those have been
254
+ * compiled to CSS). Catches whiteboard-class iOS Safari URL-bar
255
+ * scroll bugs in apps that have already shipped.
256
+ *
257
+ * Cost: one extra subrequest per app. The audit Worker runs near the
258
+ * free-plan subrequest cap; on cap errors we degrade to `warn`
259
+ * (re-run needed) rather than `fail` (app broken) — the app isn't
260
+ * actually broken, the audit just couldn't check.
261
+ */
262
+ export async function checkUnsafeVhLive(html, liveUrl) {
263
+ // Find the first bundled stylesheet. Vite output looks like
264
+ // <link rel="stylesheet" href="/assets/index-XYZ.css">. Be
265
+ // tolerant of attribute order — rel and href can appear either way.
266
+ const m = /<link[^>]+rel=["']stylesheet["'][^>]*href=["']([^"']+\.css[^"']*)["']/i.exec(html) ??
267
+ /<link[^>]+href=["']([^"']+\.css[^"']*)["'][^>]*rel=["']stylesheet["']/i.exec(html);
268
+ if (!m) {
269
+ // No stylesheet link — could be inline-only styles, or styles
270
+ // injected at runtime. Not a fail; just nothing to check.
271
+ return {
272
+ name: 'No unsafe 100vh (live)',
273
+ status: 'warn',
274
+ detail: 'no <link rel="stylesheet"> found in HTML — skipped',
275
+ };
276
+ }
277
+ const cssUrl = new URL(m[1], liveUrl).toString();
278
+ try {
279
+ const res = await fetchWithTimeout(cssUrl);
280
+ if (!res.ok) {
281
+ return { name: 'No unsafe 100vh (live)', status: 'warn', detail: `${cssUrl} → HTTP ${res.status}` };
282
+ }
283
+ const css = await res.text();
284
+ const matches = css.match(/\b100vh\b/g) ?? [];
285
+ if (matches.length === 0) {
286
+ return { name: 'No unsafe 100vh (live)', status: 'pass', detail: `scanned ${cssUrl}` };
287
+ }
288
+ return {
289
+ name: 'No unsafe 100vh (live)',
290
+ status: 'warn',
291
+ detail: `${matches.length} occurrence(s) of 100vh in ${cssUrl}`,
292
+ suggestions: [
293
+ '100vh resolves to the layout viewport on iOS Safari — when the URL bar is visible the page scrolls.',
294
+ 'Replace with 100svh (small viewport — accounts for visible browser UI) or 100dvh (dynamic — recomputes as URL bar moves).',
295
+ ],
296
+ };
297
+ }
298
+ catch (err) {
299
+ const skipped = isSubrequestCapError(err);
300
+ return {
301
+ name: 'No unsafe 100vh (live)',
302
+ status: 'warn',
303
+ detail: skipped
304
+ ? 'audit Worker hit subrequest cap — re-run needed'
305
+ : err instanceof Error
306
+ ? err.message
307
+ : 'CSS fetch failed',
308
+ };
309
+ }
310
+ }
311
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/live/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=index.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.test.d.ts","sourceRoot":"","sources":["../../src/live/index.test.ts"],"names":[],"mappings":""}