safe-content-frame 0.0.1 → 0.0.3

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/index.d.ts CHANGED
@@ -1,17 +1,8 @@
1
1
  type SandboxOption = "allow-same-origin" | "allow-scripts" | "allow-forms" | "allow-popups" | "allow-modals" | "allow-downloads" | "allow-popups-to-escape-sandbox";
2
- type ShadowDomOption = {
3
- enabled: true;
4
- } | {
5
- enabled: false;
6
- };
7
- type PdfViewerOption = {
8
- type: "native-with-download-fallback";
9
- };
10
2
  interface SafeContentFrameOptions {
11
- useShadowDom?: ShadowDomOption;
3
+ useShadowDom?: boolean;
12
4
  enableBrowserCaching?: boolean;
13
5
  sandbox?: SandboxOption[];
14
- pdfViewer?: PdfViewerOption;
15
6
  salt?: string;
16
7
  }
17
8
  interface RenderedFrame {
@@ -24,15 +15,14 @@ interface RenderedFrame {
24
15
  declare class SafeContentFrame {
25
16
  private product;
26
17
  private options;
27
- private static readonly PRODUCT_HASH;
28
18
  constructor(product: string, options?: SafeContentFrameOptions);
29
- private createShimUrl;
30
- private getSandboxAttribute;
31
- private createIframe;
32
- private renderContent;
33
- renderHtml(html: string, container: HTMLElement): Promise<RenderedFrame>;
19
+ renderHtml(html: string, container: HTMLElement, opts?: {
20
+ unsafeDocumentWrite?: boolean;
21
+ }): Promise<RenderedFrame>;
34
22
  renderRaw(content: Uint8Array | string, mimeType: string, container: HTMLElement): Promise<RenderedFrame>;
35
- renderPdf(content: Uint8Array, container: HTMLElement, fragment?: string): Promise<RenderedFrame>;
23
+ renderPdf(content: Uint8Array, container: HTMLElement): Promise<RenderedFrame>;
24
+ private render;
25
+ private getSandbox;
36
26
  }
37
27
 
38
- export { type PdfViewerOption, type RenderedFrame, SafeContentFrame, type SafeContentFrameOptions, type SandboxOption, type ShadowDomOption };
28
+ export { type RenderedFrame, SafeContentFrame, type SafeContentFrameOptions, type SandboxOption };
package/dist/index.js CHANGED
@@ -1,152 +1,132 @@
1
1
  // src/index.ts
2
- var SCF_HOST = "scf.usercontent.goog";
3
- var SHIM_PATH = "shim.html";
2
+ var SCF_HOST = "scf.auiusercontent.com";
3
+ var PRODUCT_HASH = "h184756";
4
+ async function sha256(data) {
5
+ return crypto.subtle.digest("SHA-256", data);
6
+ }
4
7
  async function computeOriginHash(product, salt, origin) {
5
- const input = `${product}$@#|${salt}$@#|${origin}`;
6
- const encoder = new TextEncoder();
7
- const data = encoder.encode(input);
8
- const hashBuffer = await crypto.subtle.digest("SHA-256", data);
9
- const hashArray = Array.from(new Uint8Array(hashBuffer));
10
- const hashHex = hashArray.map((b) => b.toString(16).padStart(2, "0")).join("");
11
- const hashBigInt = BigInt("0x" + hashHex);
12
- return hashBigInt.toString(36).slice(0, 50);
8
+ const enc = new TextEncoder();
9
+ const sep = enc.encode("$@#|");
10
+ const parts = [
11
+ enc.encode(product),
12
+ sep,
13
+ new Uint8Array(salt),
14
+ sep,
15
+ enc.encode(origin)
16
+ ];
17
+ const combined = new Uint8Array(parts.reduce((n, p) => n + p.length, 0));
18
+ let offset = 0;
19
+ for (const p of parts) {
20
+ combined.set(p, offset);
21
+ offset += p.length;
22
+ }
23
+ const hash = new Uint8Array(await sha256(combined.buffer));
24
+ const bigint = hash.reduce(
25
+ (acc, b) => BigInt(256) * acc + BigInt(b),
26
+ BigInt(0)
27
+ );
28
+ return bigint.toString(36).padStart(50, "0").slice(0, 50);
13
29
  }
14
- function generateRandomSalt() {
15
- const array = new Uint8Array(32);
16
- crypto.getRandomValues(array);
17
- return Array.from(array, (b) => b.toString(16).padStart(2, "0")).join("");
30
+ function randomSalt() {
31
+ const arr = new Uint8Array(10);
32
+ crypto.getRandomValues(arr);
33
+ return arr.buffer;
18
34
  }
19
- async function computeContentHash(content) {
20
- const data = typeof content === "string" ? new TextEncoder().encode(content) : content;
21
- const hashBuffer = await crypto.subtle.digest("SHA-256", data.buffer);
22
- const hashArray = Array.from(new Uint8Array(hashBuffer));
23
- const hashHex = hashArray.map((b) => b.toString(16).padStart(2, "0")).join("");
24
- const hashBigInt = BigInt("0x" + hashHex);
25
- return hashBigInt.toString(36).slice(0, 32);
35
+ async function contentSalt(content, pathname) {
36
+ const enc = new TextEncoder();
37
+ const sep = enc.encode("$@#|");
38
+ const combined = new Uint8Array(
39
+ content.length + sep.length + pathname.length
40
+ );
41
+ combined.set(content, 0);
42
+ combined.set(sep, content.length);
43
+ combined.set(enc.encode(pathname), content.length + sep.length);
44
+ return sha256(combined.buffer);
26
45
  }
27
- var _SafeContentFrame = class _SafeContentFrame {
46
+ var SafeContentFrame = class {
28
47
  constructor(product, options = {}) {
29
48
  this.product = product;
30
- this.options = {
31
- sandbox: ["allow-same-origin", "allow-scripts"],
32
- ...options
33
- };
49
+ this.options = options;
34
50
  }
35
- async createShimUrl(content) {
36
- const parentOrigin = window.location.origin;
37
- let salt;
38
- if (this.options.salt) {
39
- salt = this.options.salt;
40
- } else if (this.options.enableBrowserCaching) {
41
- const contentHash = await computeContentHash(content);
42
- salt = contentHash + window.location.pathname;
43
- } else {
44
- salt = generateRandomSalt();
45
- }
46
- const originHash = await computeOriginHash(this.product, salt, parentOrigin);
47
- const url = new URL(
48
- `https://${originHash}-${_SafeContentFrame.PRODUCT_HASH}.${SCF_HOST}/${this.product}/${SHIM_PATH}`
51
+ async renderHtml(html, container, opts) {
52
+ return this.render(
53
+ new TextEncoder().encode(html),
54
+ "text/html; charset=utf-8",
55
+ container,
56
+ opts
49
57
  );
50
- url.searchParams.set("origin", parentOrigin);
51
- if (this.options.enableBrowserCaching) {
52
- url.searchParams.set("cache", "1");
53
- }
54
- return url.toString();
55
58
  }
56
- getSandboxAttribute() {
57
- const sandbox = this.options.sandbox || ["allow-same-origin", "allow-scripts"];
58
- const required = /* @__PURE__ */ new Set(["allow-same-origin", "allow-scripts"]);
59
- const all = /* @__PURE__ */ new Set([...sandbox, ...required]);
60
- return Array.from(all).join(" ");
59
+ async renderRaw(content, mimeType, container) {
60
+ const data = typeof content === "string" ? new TextEncoder().encode(content) : content;
61
+ return this.render(data, mimeType, container);
62
+ }
63
+ async renderPdf(content, container) {
64
+ return this.render(content, "application/pdf", container);
61
65
  }
62
- createIframe(container) {
66
+ async render(content, mimeType, container, opts) {
67
+ const origin = window.location.origin;
68
+ const salt = this.options.salt ? new TextEncoder().encode(this.options.salt).buffer : this.options.enableBrowserCaching ? await contentSalt(content, location.pathname) : randomSalt();
69
+ const hash = await computeOriginHash(this.product, salt, origin);
70
+ const shimUrl = `https://${hash}-${PRODUCT_HASH}.${SCF_HOST}/${this.product}/shim.html?origin=${encodeURIComponent(origin)}${this.options.enableBrowserCaching ? "&cache=1" : ""}`;
71
+ const iframeOrigin = new URL(shimUrl).origin;
63
72
  const iframe = document.createElement("iframe");
64
- iframe.setAttribute("sandbox", this.getSandboxAttribute());
65
- iframe.style.border = "none";
66
- iframe.style.width = "100%";
67
- iframe.style.height = "100%";
68
- if (this.options.useShadowDom?.enabled) {
69
- const shadowHost = document.createElement("div");
70
- const shadow = shadowHost.attachShadow({ mode: "closed" });
71
- shadow.appendChild(iframe);
72
- container.appendChild(shadowHost);
73
+ iframe.setAttribute("sandbox", this.getSandbox());
74
+ iframe.style.cssText = "border:none;width:100%;height:100%";
75
+ if (this.options.useShadowDom) {
76
+ const host = document.createElement("div");
77
+ host.attachShadow({ mode: "closed" }).appendChild(iframe);
78
+ container.appendChild(host);
73
79
  } else {
74
80
  container.appendChild(iframe);
75
81
  }
76
- return iframe;
77
- }
78
- async renderContent(content, mimeType, container) {
79
- const shimUrl = await this.createShimUrl(content);
80
- const iframe = this.createIframe(container);
81
- const iframeOrigin = new URL(shimUrl).origin;
82
82
  return new Promise((resolve, reject) => {
83
83
  const channel = new MessageChannel();
84
- const contentData = typeof content === "string" ? new TextEncoder().encode(content) : content;
85
- const handleShimReady = (event) => {
86
- if (event.origin !== iframeOrigin) return;
87
- if (event.data?.type !== "shim-ready") return;
88
- window.removeEventListener("message", handleShimReady);
84
+ let onLoaded;
85
+ const loaded = new Promise((r) => {
86
+ onLoaded = r;
87
+ });
88
+ channel.port1.onmessage = (e) => {
89
+ if (e.data?.type === "msg") onLoaded();
90
+ else if (e.data?.type === "error") reject(new Error(e.data.message));
91
+ };
92
+ iframe.onload = () => {
89
93
  iframe.contentWindow?.postMessage(
90
94
  {
91
- type: "render",
92
- body: contentData,
95
+ body: content.buffer.slice(
96
+ content.byteOffset,
97
+ content.byteOffset + content.byteLength
98
+ ),
93
99
  mimeType,
94
- salt: this.options.salt || ""
100
+ salt,
101
+ unsafeDocumentWrite: opts?.unsafeDocumentWrite
95
102
  },
96
103
  iframeOrigin,
97
104
  [channel.port2]
98
105
  );
99
- };
100
- window.addEventListener("message", handleShimReady);
101
- let loadResolve;
102
- const fullyLoadedPromise = new Promise((res) => {
103
- loadResolve = res;
104
- });
105
- channel.port1.onmessage = (e) => {
106
- if (e.data === "Reloading iframe" || e.data?.type === "loaded") {
107
- loadResolve();
108
- }
109
- };
110
- iframe.src = shimUrl;
111
- const renderedFrame = {
112
- iframe,
113
- origin: iframeOrigin,
114
- sendMessage(data, transfer) {
115
- iframe.contentWindow?.postMessage(data, iframeOrigin, transfer);
116
- },
117
- fullyLoadedPromiseWithTimeout(timeoutMs) {
118
- return Promise.race([
119
- fullyLoadedPromise,
106
+ resolve({
107
+ iframe,
108
+ origin: iframeOrigin,
109
+ sendMessage: (data, transfer) => iframe.contentWindow?.postMessage(data, iframeOrigin, transfer),
110
+ fullyLoadedPromiseWithTimeout: (ms) => Promise.race([
111
+ loaded,
120
112
  new Promise(
121
- (_, rej) => setTimeout(() => rej(new Error("Timeout waiting for iframe to load")), timeoutMs)
113
+ (_, rej) => setTimeout(() => rej(new Error("Timeout")), ms)
122
114
  )
123
- ]);
124
- },
125
- dispose() {
126
- iframe.remove();
127
- }
128
- };
129
- iframe.onload = () => {
130
- resolve(renderedFrame);
131
- };
132
- iframe.onerror = () => {
133
- reject(new Error("Failed to load iframe"));
115
+ ]),
116
+ dispose: () => iframe.remove()
117
+ });
134
118
  };
119
+ iframe.onerror = () => reject(new Error("Failed to load iframe"));
120
+ iframe.src = shimUrl;
135
121
  });
136
122
  }
137
- async renderHtml(html, container) {
138
- return this.renderContent(html, "text/html; charset=utf-8", container);
139
- }
140
- async renderRaw(content, mimeType, container) {
141
- return this.renderContent(content, mimeType, container);
142
- }
143
- async renderPdf(content, container, fragment) {
144
- const mimeType = fragment ? `application/pdf#${fragment}` : "application/pdf";
145
- return this.renderContent(content, mimeType, container);
123
+ getSandbox() {
124
+ const s = new Set(this.options.sandbox || []);
125
+ s.add("allow-same-origin");
126
+ s.add("allow-scripts");
127
+ return [...s].join(" ");
146
128
  }
147
129
  };
148
- _SafeContentFrame.PRODUCT_HASH = "h833788197";
149
- var SafeContentFrame = _SafeContentFrame;
150
130
  export {
151
131
  SafeContentFrame
152
132
  };
@@ -1,6 +1,4 @@
1
- import { ShadowDomOption } from './index.js';
2
-
3
- declare function enableShadowDom(): ShadowDomOption;
4
- declare function unsafeDisableShadowDom(): ShadowDomOption;
1
+ declare const enableShadowDom: () => boolean;
2
+ declare const unsafeDisableShadowDom: () => boolean;
5
3
 
6
4
  export { enableShadowDom, unsafeDisableShadowDom };
@@ -1,10 +1,6 @@
1
1
  // src/shadow_dom.ts
2
- function enableShadowDom() {
3
- return { enabled: true };
4
- }
5
- function unsafeDisableShadowDom() {
6
- return { enabled: false };
7
- }
2
+ var enableShadowDom = () => true;
3
+ var unsafeDisableShadowDom = () => false;
8
4
  export {
9
5
  enableShadowDom,
10
6
  unsafeDisableShadowDom
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "safe-content-frame",
3
- "version": "0.0.1",
4
- "description": "Secure iframe rendering for untrusted content",
3
+ "version": "0.0.3",
4
+ "description": "Secure iframe rendering for untrusted content using SafeContentFrame",
5
5
  "license": "MIT",
6
6
  "type": "module",
7
7
  "exports": {
@@ -12,23 +12,32 @@
12
12
  "./shadow_dom": {
13
13
  "types": "./dist/shadow_dom.d.ts",
14
14
  "default": "./dist/shadow_dom.js"
15
- },
16
- "./pdf_rendering": {
17
- "types": "./dist/pdf_rendering.d.ts",
18
- "default": "./dist/pdf_rendering.js"
19
15
  }
20
16
  },
21
17
  "main": "./dist/index.js",
22
18
  "types": "./dist/index.d.ts",
23
19
  "files": [
24
- "dist",
25
- "README.md"
20
+ "dist"
26
21
  ],
27
22
  "devDependencies": {
28
- "tsup": "^8.0.0",
29
- "typescript": "^5.0.0"
23
+ "tsup": "^8.5.1",
24
+ "typescript": "^5.9.3",
25
+ "vite": "^7.2.6",
26
+ "@assistant-ui/x-buildutils": "0.0.1"
27
+ },
28
+ "publishConfig": {
29
+ "access": "public",
30
+ "provenance": true
31
+ },
32
+ "repository": {
33
+ "type": "git",
34
+ "url": "https://github.com/assistant-ui/assistant-ui/tree/main/packages/safe-content-frame"
35
+ },
36
+ "bugs": {
37
+ "url": "https://github.com/assistant-ui/assistant-ui/issues"
30
38
  },
31
39
  "scripts": {
32
- "build": "tsup src/index.ts src/shadow_dom.ts src/pdf_rendering.ts --format esm --dts"
40
+ "build": "tsup src/index.ts src/shadow_dom.ts --format esm --dts",
41
+ "dev": "vite demo"
33
42
  }
34
43
  }
@@ -1,5 +0,0 @@
1
- import { PdfViewerOption } from './index.js';
2
-
3
- declare function nativePdfViewerWithDownloadFallback(): PdfViewerOption;
4
-
5
- export { nativePdfViewerWithDownloadFallback };
@@ -1,7 +0,0 @@
1
- // src/pdf_rendering.ts
2
- function nativePdfViewerWithDownloadFallback() {
3
- return { type: "native-with-download-fallback" };
4
- }
5
- export {
6
- nativePdfViewerWithDownloadFallback
7
- };