@xiboplayer/cache 0.5.16 → 0.5.18

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,9 +1,10 @@
1
1
  {
2
2
  "name": "@xiboplayer/cache",
3
- "version": "0.5.16",
3
+ "version": "0.5.18",
4
4
  "description": "Offline caching and download management with parallel chunk downloads",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
7
+ "types": "./src/index.d.ts",
7
8
  "exports": {
8
9
  ".": "./src/index.js",
9
10
  "./cache": "./src/cache.js",
@@ -15,7 +16,7 @@
15
16
  },
16
17
  "dependencies": {
17
18
  "spark-md5": "^3.0.2",
18
- "@xiboplayer/utils": "0.5.16"
19
+ "@xiboplayer/utils": "0.5.18"
19
20
  },
20
21
  "devDependencies": {
21
22
  "vitest": "^2.0.0",
@@ -45,6 +45,18 @@ const URGENT_CONCURRENCY = 2; // Slots when urgent chunk is active (bandwidth fo
45
45
  const FETCH_TIMEOUT_MS = 600_000; // 10 minutes — 100MB chunk at ~2 Mbps
46
46
  const HEAD_TIMEOUT_MS = 15_000; // 15 seconds for HEAD requests
47
47
 
48
+ // CMS origin for proxy filtering — set via setCmsOrigin() at init
49
+ let _cmsOrigin = null;
50
+
51
+ /**
52
+ * Set the CMS origin so toProxyUrl() only proxies CMS URLs.
53
+ * External URLs (CDNs, Google Fonts, geolocation APIs) pass through unchanged.
54
+ * @param {string} origin - e.g. 'https://cms.example.com'
55
+ */
56
+ export function setCmsOrigin(origin) {
57
+ _cmsOrigin = origin;
58
+ }
59
+
48
60
  /**
49
61
  * Infer Content-Type from file path extension.
50
62
  * Used when we skip HEAD (size already known from RequiredFiles).
@@ -114,8 +126,9 @@ export function toProxyUrl(url) {
114
126
  const loc = typeof self !== 'undefined' ? self.location : undefined;
115
127
  if (!loc || loc.hostname !== 'localhost') return url;
116
128
  const parsed = new URL(url);
117
- const cmsOrigin = parsed.origin;
118
- return `/file-proxy?cms=${encodeURIComponent(cmsOrigin)}&url=${encodeURIComponent(parsed.pathname + parsed.search)}`;
129
+ // Only proxy URLs belonging to the CMS server; external URLs pass through
130
+ if (_cmsOrigin && parsed.origin !== _cmsOrigin) return url;
131
+ return `/file-proxy?cms=${encodeURIComponent(parsed.origin)}&url=${encodeURIComponent(parsed.pathname + parsed.search)}`;
119
132
  }
120
133
 
121
134
  /**
package/src/index.d.ts ADDED
@@ -0,0 +1,40 @@
1
+ export const VERSION: string;
2
+
3
+ export class StoreClient {
4
+ has(type: string, id: string | number): Promise<boolean>;
5
+ get(type: string, id: string | number): Promise<Blob | null>;
6
+ put(type: string, id: string | number, body: Blob | ArrayBuffer | string, contentType?: string): Promise<boolean>;
7
+ remove(files: Array<{ type: string; id: string | number }>): Promise<{ deleted: number; total: number }>;
8
+ list(): Promise<Array<{ id: string; type: string; size: number }>>;
9
+ }
10
+
11
+ export class DownloadClient {
12
+ controller: ServiceWorker | null;
13
+ fetchReady: boolean;
14
+ init(): Promise<void>;
15
+ download(payload: object | any[]): Promise<void>;
16
+ prioritize(fileType: string, fileId: string): Promise<void>;
17
+ prioritizeLayout(mediaIds: string[]): Promise<void>;
18
+ getProgress(): Promise<Record<string, any>>;
19
+ cleanup(): void;
20
+ }
21
+
22
+ export class DownloadManager {
23
+ constructor(options?: { concurrency?: number; chunkSize?: number; maxChunksPerFile?: number });
24
+ enqueue(fileInfo: any): any;
25
+ prioritizeLayoutFiles(mediaIds: string[]): void;
26
+ }
27
+
28
+ export class FileDownload {}
29
+ export class LayoutTaskBuilder {}
30
+ export class CacheManager {}
31
+ export class CacheAnalyzer {
32
+ constructor(store: StoreClient);
33
+ }
34
+
35
+ export const cacheManager: CacheManager;
36
+
37
+ export function isUrlExpired(url: string): boolean;
38
+ export function toProxyUrl(url: string): string;
39
+ export function setCmsOrigin(origin: string): void;
40
+ export function cacheWidgetHtml(...args: any[]): any;
package/src/index.js CHANGED
@@ -4,6 +4,6 @@ export const VERSION = pkg.version;
4
4
  export { CacheManager, cacheManager } from './cache.js';
5
5
  export { StoreClient } from './store-client.js';
6
6
  export { DownloadClient } from './download-client.js';
7
- export { DownloadManager, FileDownload, LayoutTaskBuilder, isUrlExpired, toProxyUrl } from './download-manager.js';
7
+ export { DownloadManager, FileDownload, LayoutTaskBuilder, isUrlExpired, toProxyUrl, setCmsOrigin } from './download-manager.js';
8
8
  export { CacheAnalyzer } from './cache-analyzer.js';
9
9
  export { cacheWidgetHtml } from './widget-html.js';
@@ -4,10 +4,13 @@
4
4
  * Handles:
5
5
  * - <base> tag injection for relative path resolution
6
6
  * - CMS signed URL → local store path rewriting
7
- * - CSS font URL rewriting and font file caching
8
7
  * - Interactive Control hostAddress rewriting
9
8
  * - CSS object-position fix for CMS template alignment
10
9
  *
10
+ * Note: CSS font URL rewriting is primarily handled by the proxy layer (proxy.js).
11
+ * As defense-in-depth, this module also rewrites font URLs inside CSS files
12
+ * before storing them, in case the proxy misses the CSS detection.
13
+ *
11
14
  * Runs on the main thread (needs window.location for URL construction).
12
15
  * Stores content via PUT /store/... — no Cache API needed.
13
16
  */
@@ -117,60 +120,65 @@ export async function cacheWidgetHtml(layoutId, regionId, mediaId, html) {
117
120
  'svg': 'image/svg+xml'
118
121
  }[ext] || 'application/octet-stream';
119
122
 
120
- // For CSS files, rewrite font URLs and store referenced font files
123
+ // Defense-in-depth: rewrite font URLs inside CSS files before storing.
124
+ // The proxy normally handles this, but if it misses CSS detection
125
+ // (e.g. CMS returns unexpected Content-Type), we catch it here.
126
+ // Also handles truncated CSS from CMS (missing closing ');}) due to
127
+ // Content-Length mismatch after URL expansion).
121
128
  if (ext === 'css') {
122
129
  let cssText = await resp.text();
123
- const fontResources = [];
124
- const fontUrlRegex = /url\((['"]?)(https?:\/\/[^'")\s]+\?[^'")\s]*file=([^&'")\s]+\.(?:woff2?|ttf|otf|eot|svg))[^'")\s]*)\1\)/gi;
125
- cssText = cssText.replace(fontUrlRegex, (_match, quote, fullUrl, fontFilename) => {
126
- fontResources.push({ filename: fontFilename, originalUrl: fullUrl });
127
- log.info(`Rewrote font URL in CSS: ${fontFilename}`);
128
- return `url(${quote}${BASE}/cache/static/${encodeURIComponent(fontFilename)}${quote})`;
130
+ // Match any CMS signed URL with a file= query param in the CSS text.
131
+ // Uses a simple approach: find all https:// URLs with file=<name>, then
132
+ // filter to fonts by extension or fileType=font. This handles both normal
133
+ // url('...') and truncated CSS (where the closing '); is missing).
134
+ const CMS_SIGNED_URL_RE = /https?:\/\/[^\s'")\]]+\?[^\s'")\]]*file=([^&\s'")\]]+)[^\s'")\]]*/g;
135
+ const FONT_EXTS = /\.(?:woff2?|ttf|otf|eot|svg)$/i;
136
+ const fontJobs = [];
137
+
138
+ cssText = cssText.replace(CMS_SIGNED_URL_RE, (fullUrl, fontFilename) => {
139
+ if (!FONT_EXTS.test(fontFilename) && !fullUrl.includes('fileType=font')) return fullUrl;
140
+ fontJobs.push({ filename: fontFilename, originalUrl: fullUrl });
141
+ log.info(`Rewrote CSS font URL: ${fontFilename}`);
142
+ return `${BASE}/cache/static/${encodeURIComponent(fontFilename)}`;
129
143
  });
130
144
 
131
- const cssResp = await fetch(`/store/static/${filename}`, {
132
- method: 'PUT',
133
- headers: { 'Content-Type': 'text/css' },
134
- body: cssText,
135
- });
136
- cssResp.body?.cancel();
137
- log.info(`Stored CSS with ${fontResources.length} rewritten font URLs: ${filename}`);
138
-
139
- // Fetch and store referenced font files
140
- await Promise.all(fontResources.map(async ({ filename: fontFile, originalUrl: fontUrl }) => {
141
- // Check if already stored
145
+ // Fetch and store each referenced font file
146
+ await Promise.all(fontJobs.map(async (font) => {
142
147
  try {
143
- const headResp = await fetch(`/store/static/${encodeURIComponent(fontFile)}`, { method: 'HEAD' });
144
- if (headResp.ok) return;
148
+ const headResp = await fetch(`/store/static/${font.filename}`, { method: 'HEAD' });
149
+ if (headResp.ok) return; // Already stored
145
150
  } catch { /* proceed */ }
146
-
147
151
  try {
148
- const fontResp = await fetch(toProxyUrl(fontUrl));
149
- if (!fontResp.ok) {
150
- fontResp.body?.cancel();
151
- log.warn(`Failed to fetch font: ${fontFile} (HTTP ${fontResp.status})`);
152
- return;
153
- }
152
+ const fontResp = await fetch(toProxyUrl(font.originalUrl));
153
+ if (!fontResp.ok) { fontResp.body?.cancel(); return; }
154
154
  const fontBlob = await fontResp.blob();
155
- const fontExt = fontFile.split('.').pop().toLowerCase();
155
+ const fontExt = font.filename.split('.').pop().toLowerCase();
156
156
  const fontContentType = {
157
157
  'otf': 'font/otf', 'ttf': 'font/ttf',
158
158
  'woff': 'font/woff', 'woff2': 'font/woff2',
159
- 'eot': 'application/vnd.ms-fontobject',
160
- 'svg': 'image/svg+xml'
159
+ 'eot': 'application/vnd.ms-fontobject', 'svg': 'image/svg+xml',
161
160
  }[fontExt] || 'application/octet-stream';
162
-
163
- const fontPutResp = await fetch(`/store/static/${encodeURIComponent(fontFile)}`, {
161
+ const putFont = await fetch(`/store/static/${font.filename}`, {
164
162
  method: 'PUT',
165
163
  headers: { 'Content-Type': fontContentType },
166
164
  body: fontBlob,
167
165
  });
168
- fontPutResp.body?.cancel();
169
- log.info(`Stored font: ${fontFile} (${fontContentType}, ${fontBlob.size} bytes)`);
170
- } catch (fontErr) {
171
- log.warn(`Failed to store font: ${fontFile}`, fontErr);
166
+ putFont.body?.cancel();
167
+ log.info(`Stored font: ${font.filename} (${fontBlob.size} bytes)`);
168
+ } catch (e) {
169
+ log.warn(`Failed to store font: ${font.filename}`, e);
172
170
  }
173
171
  }));
172
+
173
+ // Store the rewritten CSS
174
+ const cssBlob = new Blob([cssText], { type: 'text/css' });
175
+ const staticResp = await fetch(`/store/static/${filename}`, {
176
+ method: 'PUT',
177
+ headers: { 'Content-Type': 'text/css' },
178
+ body: cssBlob,
179
+ });
180
+ staticResp.body?.cancel();
181
+ log.info(`Stored CSS with ${fontJobs.length} rewritten font URLs: ${filename} (${cssText.length} bytes)`);
174
182
  } else {
175
183
  const blob = await resp.blob();
176
184
  const staticResp = await fetch(`/store/static/${filename}`, {
@@ -196,12 +196,15 @@ describe('cacheWidgetHtml', () => {
196
196
  expect(widgetKey).toBeTruthy();
197
197
  });
198
198
 
199
- it('caches font files referenced in fonts.css', async () => {
199
+ it('stores fonts.css as a blob (font rewriting handled by proxy)', async () => {
200
200
  await cacheWidgetHtml('472', '223', '193', makeRssTickerHtml());
201
201
 
202
- // fonts.css contains url("...Poppins-Regular.ttf") which should be fetched
203
- const fontFetched = fetchedUrls.some(u => u.includes('Poppins-Regular.ttf'));
204
- expect(fontFetched).toBe(true);
202
+ // fonts.css should be fetched and stored, but font files inside it
203
+ // are NOT parsed/fetched here — the proxy rewrites CSS font URLs
204
+ const cssFetched = fetchedUrls.some(u => u.includes('fonts.css'));
205
+ expect(cssFetched).toBe(true);
206
+ const cssStored = [...storeContents.keys()].some(k => k.includes('fonts.css'));
207
+ expect(cssStored).toBe(true);
205
208
  });
206
209
  });
207
210