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 +8 -18
- package/dist/index.js +98 -118
- package/dist/shadow_dom.d.ts +2 -4
- package/dist/shadow_dom.js +2 -6
- package/package.json +20 -11
- package/dist/pdf_rendering.d.ts +0 -5
- package/dist/pdf_rendering.js +0 -7
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?:
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
|
23
|
+
renderPdf(content: Uint8Array, container: HTMLElement): Promise<RenderedFrame>;
|
|
24
|
+
private render;
|
|
25
|
+
private getSandbox;
|
|
36
26
|
}
|
|
37
27
|
|
|
38
|
-
export { type
|
|
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.
|
|
3
|
-
var
|
|
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
|
|
6
|
-
const
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
|
15
|
-
const
|
|
16
|
-
crypto.getRandomValues(
|
|
17
|
-
return
|
|
30
|
+
function randomSalt() {
|
|
31
|
+
const arr = new Uint8Array(10);
|
|
32
|
+
crypto.getRandomValues(arr);
|
|
33
|
+
return arr.buffer;
|
|
18
34
|
}
|
|
19
|
-
async function
|
|
20
|
-
const
|
|
21
|
-
const
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
|
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
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
-
|
|
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.
|
|
65
|
-
iframe.style.
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
-
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
-
|
|
92
|
-
|
|
95
|
+
body: content.buffer.slice(
|
|
96
|
+
content.byteOffset,
|
|
97
|
+
content.byteOffset + content.byteLength
|
|
98
|
+
),
|
|
93
99
|
mimeType,
|
|
94
|
-
salt
|
|
100
|
+
salt,
|
|
101
|
+
unsafeDocumentWrite: opts?.unsafeDocumentWrite
|
|
95
102
|
},
|
|
96
103
|
iframeOrigin,
|
|
97
104
|
[channel.port2]
|
|
98
105
|
);
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
|
113
|
+
(_, rej) => setTimeout(() => rej(new Error("Timeout")), ms)
|
|
122
114
|
)
|
|
123
|
-
])
|
|
124
|
-
|
|
125
|
-
|
|
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
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
return
|
|
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
|
};
|
package/dist/shadow_dom.d.ts
CHANGED
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
|
|
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 };
|
package/dist/shadow_dom.js
CHANGED
|
@@ -1,10 +1,6 @@
|
|
|
1
1
|
// src/shadow_dom.ts
|
|
2
|
-
|
|
3
|
-
|
|
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.
|
|
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.
|
|
29
|
-
"typescript": "^5.
|
|
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
|
|
40
|
+
"build": "tsup src/index.ts src/shadow_dom.ts --format esm --dts",
|
|
41
|
+
"dev": "vite demo"
|
|
33
42
|
}
|
|
34
43
|
}
|
package/dist/pdf_rendering.d.ts
DELETED