safe-content-frame 0.0.0 → 0.0.2
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 +28 -0
- package/dist/index.js +132 -0
- package/dist/shadow_dom.d.ts +4 -0
- package/dist/shadow_dom.js +7 -0
- package/package.json +40 -2
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
type SandboxOption = "allow-same-origin" | "allow-scripts" | "allow-forms" | "allow-popups" | "allow-modals" | "allow-downloads" | "allow-popups-to-escape-sandbox";
|
|
2
|
+
interface SafeContentFrameOptions {
|
|
3
|
+
useShadowDom?: boolean;
|
|
4
|
+
enableBrowserCaching?: boolean;
|
|
5
|
+
sandbox?: SandboxOption[];
|
|
6
|
+
salt?: string;
|
|
7
|
+
}
|
|
8
|
+
interface RenderedFrame {
|
|
9
|
+
iframe: HTMLIFrameElement;
|
|
10
|
+
origin: string;
|
|
11
|
+
sendMessage(data: unknown, transfer?: Transferable[]): void;
|
|
12
|
+
fullyLoadedPromiseWithTimeout(timeoutMs: number): Promise<void>;
|
|
13
|
+
dispose(): void;
|
|
14
|
+
}
|
|
15
|
+
declare class SafeContentFrame {
|
|
16
|
+
private product;
|
|
17
|
+
private options;
|
|
18
|
+
constructor(product: string, options?: SafeContentFrameOptions);
|
|
19
|
+
renderHtml(html: string, container: HTMLElement, opts?: {
|
|
20
|
+
unsafeDocumentWrite?: boolean;
|
|
21
|
+
}): Promise<RenderedFrame>;
|
|
22
|
+
renderRaw(content: Uint8Array | string, mimeType: string, container: HTMLElement): Promise<RenderedFrame>;
|
|
23
|
+
renderPdf(content: Uint8Array, container: HTMLElement): Promise<RenderedFrame>;
|
|
24
|
+
private render;
|
|
25
|
+
private getSandbox;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export { type RenderedFrame, SafeContentFrame, type SafeContentFrameOptions, type SandboxOption };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
// src/index.ts
|
|
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
|
+
}
|
|
7
|
+
async function computeOriginHash(product, salt, origin) {
|
|
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);
|
|
29
|
+
}
|
|
30
|
+
function randomSalt() {
|
|
31
|
+
const arr = new Uint8Array(10);
|
|
32
|
+
crypto.getRandomValues(arr);
|
|
33
|
+
return arr.buffer;
|
|
34
|
+
}
|
|
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);
|
|
45
|
+
}
|
|
46
|
+
var SafeContentFrame = class {
|
|
47
|
+
constructor(product, options = {}) {
|
|
48
|
+
this.product = product;
|
|
49
|
+
this.options = options;
|
|
50
|
+
}
|
|
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
|
|
57
|
+
);
|
|
58
|
+
}
|
|
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);
|
|
65
|
+
}
|
|
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;
|
|
72
|
+
const iframe = document.createElement("iframe");
|
|
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);
|
|
79
|
+
} else {
|
|
80
|
+
container.appendChild(iframe);
|
|
81
|
+
}
|
|
82
|
+
return new Promise((resolve, reject) => {
|
|
83
|
+
const channel = new MessageChannel();
|
|
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 = () => {
|
|
93
|
+
iframe.contentWindow?.postMessage(
|
|
94
|
+
{
|
|
95
|
+
body: content.buffer.slice(
|
|
96
|
+
content.byteOffset,
|
|
97
|
+
content.byteOffset + content.byteLength
|
|
98
|
+
),
|
|
99
|
+
mimeType,
|
|
100
|
+
salt,
|
|
101
|
+
unsafeDocumentWrite: opts?.unsafeDocumentWrite
|
|
102
|
+
},
|
|
103
|
+
iframeOrigin,
|
|
104
|
+
[channel.port2]
|
|
105
|
+
);
|
|
106
|
+
resolve({
|
|
107
|
+
iframe,
|
|
108
|
+
origin: iframeOrigin,
|
|
109
|
+
sendMessage: (data, transfer) => iframe.contentWindow?.postMessage(data, iframeOrigin, transfer),
|
|
110
|
+
fullyLoadedPromiseWithTimeout: (ms) => Promise.race([
|
|
111
|
+
loaded,
|
|
112
|
+
new Promise(
|
|
113
|
+
(_, rej) => setTimeout(() => rej(new Error("Timeout")), ms)
|
|
114
|
+
)
|
|
115
|
+
]),
|
|
116
|
+
dispose: () => iframe.remove()
|
|
117
|
+
});
|
|
118
|
+
};
|
|
119
|
+
iframe.onerror = () => reject(new Error("Failed to load iframe"));
|
|
120
|
+
iframe.src = shimUrl;
|
|
121
|
+
});
|
|
122
|
+
}
|
|
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(" ");
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
export {
|
|
131
|
+
SafeContentFrame
|
|
132
|
+
};
|
package/package.json
CHANGED
|
@@ -1,5 +1,43 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "safe-content-frame",
|
|
3
|
-
"version": "0.0.
|
|
4
|
-
"description": ""
|
|
3
|
+
"version": "0.0.2",
|
|
4
|
+
"description": "Secure iframe rendering for untrusted content using SafeContentFrame",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"default": "./dist/index.js"
|
|
11
|
+
},
|
|
12
|
+
"./shadow_dom": {
|
|
13
|
+
"types": "./dist/shadow_dom.d.ts",
|
|
14
|
+
"default": "./dist/shadow_dom.js"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"main": "./dist/index.js",
|
|
18
|
+
"types": "./dist/index.d.ts",
|
|
19
|
+
"files": [
|
|
20
|
+
"dist"
|
|
21
|
+
],
|
|
22
|
+
"devDependencies": {
|
|
23
|
+
"tsup": "^8.0.0",
|
|
24
|
+
"typescript": "^5.0.0",
|
|
25
|
+
"vite": "^6.0.0",
|
|
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"
|
|
38
|
+
},
|
|
39
|
+
"scripts": {
|
|
40
|
+
"build": "tsup src/index.ts src/shadow_dom.ts --format esm --dts",
|
|
41
|
+
"dev": "vite demo"
|
|
42
|
+
}
|
|
5
43
|
}
|