safe-content-frame 0.0.4 → 0.0.5
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 +5 -6
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +109 -122
- package/dist/index.js.map +1 -0
- package/dist/shadow_dom.d.ts +3 -4
- package/dist/shadow_dom.d.ts.map +1 -0
- package/dist/shadow_dom.js +3 -7
- package/dist/shadow_dom.js.map +1 -0
- package/package.json +22 -7
- package/shadow_dom/package.json +5 -0
- package/src/index.ts +199 -0
- package/src/shadow_dom.ts +2 -0
package/dist/index.d.ts
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
|
-
type SandboxOption = "allow-same-origin" | "allow-scripts" | "allow-forms" | "allow-popups" | "allow-modals" | "allow-downloads" | "allow-popups-to-escape-sandbox";
|
|
2
|
-
interface SafeContentFrameOptions {
|
|
1
|
+
export type SandboxOption = "allow-same-origin" | "allow-scripts" | "allow-forms" | "allow-popups" | "allow-modals" | "allow-downloads" | "allow-popups-to-escape-sandbox";
|
|
2
|
+
export interface SafeContentFrameOptions {
|
|
3
3
|
useShadowDom?: boolean;
|
|
4
4
|
enableBrowserCaching?: boolean;
|
|
5
5
|
sandbox?: SandboxOption[];
|
|
6
6
|
salt?: string;
|
|
7
7
|
}
|
|
8
|
-
interface RenderedFrame {
|
|
8
|
+
export interface RenderedFrame {
|
|
9
9
|
iframe: HTMLIFrameElement;
|
|
10
10
|
origin: string;
|
|
11
11
|
sendMessage(data: unknown, transfer?: Transferable[]): void;
|
|
12
12
|
fullyLoadedPromiseWithTimeout(timeoutMs: number): Promise<void>;
|
|
13
13
|
dispose(): void;
|
|
14
14
|
}
|
|
15
|
-
declare class SafeContentFrame {
|
|
15
|
+
export declare class SafeContentFrame {
|
|
16
16
|
private product;
|
|
17
17
|
private options;
|
|
18
18
|
constructor(product: string, options?: SafeContentFrameOptions);
|
|
@@ -24,5 +24,4 @@ declare class SafeContentFrame {
|
|
|
24
24
|
private render;
|
|
25
25
|
private getSandbox;
|
|
26
26
|
}
|
|
27
|
-
|
|
28
|
-
export { type RenderedFrame, SafeContentFrame, type SafeContentFrameOptions, type SandboxOption };
|
|
27
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,aAAa,GACrB,mBAAmB,GACnB,eAAe,GACf,aAAa,GACb,cAAc,GACd,cAAc,GACd,iBAAiB,GACjB,gCAAgC,CAAC;AAErC,MAAM,WAAW,uBAAuB;IACtC,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAC/B,OAAO,CAAC,EAAE,aAAa,EAAE,CAAC;IAC1B,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,iBAAiB,CAAC;IAC1B,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,IAAI,EAAE,OAAO,EAAE,QAAQ,CAAC,EAAE,YAAY,EAAE,GAAG,IAAI,CAAC;IAC5D,6BAA6B,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAChE,OAAO,IAAI,IAAI,CAAC;CACjB;AA2DD,qBAAa,gBAAgB;IAEzB,OAAO,CAAC,OAAO;IACf,OAAO,CAAC,OAAO;gBADP,OAAO,EAAE,MAAM,EACf,OAAO,GAAE,uBAA4B;IAGzC,UAAU,CACd,IAAI,EAAE,MAAM,EACZ,SAAS,EAAE,WAAW,EACtB,IAAI,CAAC,EAAE;QAAE,mBAAmB,CAAC,EAAE,OAAO,CAAA;KAAE,GACvC,OAAO,CAAC,aAAa,CAAC;IASnB,SAAS,CACb,OAAO,EAAE,UAAU,GAAG,MAAM,EAC5B,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,WAAW,GACrB,OAAO,CAAC,aAAa,CAAC;IAMnB,SAAS,CACb,OAAO,EAAE,UAAU,EACnB,SAAS,EAAE,WAAW,GACrB,OAAO,CAAC,aAAa,CAAC;YAIX,MAAM;IA2EpB,OAAO,CAAC,UAAU;CAMnB"}
|
package/dist/index.js
CHANGED
|
@@ -1,132 +1,119 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
var PRODUCT_HASH = "h184756";
|
|
1
|
+
const SCF_HOST = "scf.auiusercontent.com";
|
|
2
|
+
const PRODUCT_HASH = "h184756";
|
|
4
3
|
async function sha256(data) {
|
|
5
|
-
|
|
4
|
+
return crypto.subtle.digest("SHA-256", data);
|
|
6
5
|
}
|
|
7
6
|
async function computeOriginHash(product, salt, origin) {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
(
|
|
26
|
-
BigInt(0)
|
|
27
|
-
);
|
|
28
|
-
return bigint.toString(36).padStart(50, "0").slice(0, 50);
|
|
7
|
+
const enc = new TextEncoder();
|
|
8
|
+
const sep = enc.encode("$@#|");
|
|
9
|
+
const parts = [
|
|
10
|
+
enc.encode(product),
|
|
11
|
+
sep,
|
|
12
|
+
new Uint8Array(salt),
|
|
13
|
+
sep,
|
|
14
|
+
enc.encode(origin),
|
|
15
|
+
];
|
|
16
|
+
const combined = new Uint8Array(parts.reduce((n, p) => n + p.length, 0));
|
|
17
|
+
let offset = 0;
|
|
18
|
+
for (const p of parts) {
|
|
19
|
+
combined.set(p, offset);
|
|
20
|
+
offset += p.length;
|
|
21
|
+
}
|
|
22
|
+
const hash = new Uint8Array(await sha256(combined.buffer));
|
|
23
|
+
const bigint = hash.reduce((acc, b) => BigInt(256) * acc + BigInt(b), BigInt(0));
|
|
24
|
+
return bigint.toString(36).padStart(50, "0").slice(0, 50);
|
|
29
25
|
}
|
|
30
26
|
function randomSalt() {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
27
|
+
const arr = new Uint8Array(10);
|
|
28
|
+
crypto.getRandomValues(arr);
|
|
29
|
+
return arr.buffer;
|
|
34
30
|
}
|
|
35
31
|
async function contentSalt(content, pathname) {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
content
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
combined.set(enc.encode(pathname), content.length + sep.length);
|
|
44
|
-
return sha256(combined.buffer);
|
|
32
|
+
const enc = new TextEncoder();
|
|
33
|
+
const sep = enc.encode("$@#|");
|
|
34
|
+
const combined = new Uint8Array(content.length + sep.length + pathname.length);
|
|
35
|
+
combined.set(content, 0);
|
|
36
|
+
combined.set(sep, content.length);
|
|
37
|
+
combined.set(enc.encode(pathname), content.length + sep.length);
|
|
38
|
+
return sha256(combined.buffer);
|
|
45
39
|
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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);
|
|
40
|
+
export class SafeContentFrame {
|
|
41
|
+
product;
|
|
42
|
+
options;
|
|
43
|
+
constructor(product, options = {}) {
|
|
44
|
+
this.product = product;
|
|
45
|
+
this.options = options;
|
|
46
|
+
}
|
|
47
|
+
async renderHtml(html, container, opts) {
|
|
48
|
+
return this.render(new TextEncoder().encode(html), "text/html; charset=utf-8", container, opts);
|
|
49
|
+
}
|
|
50
|
+
async renderRaw(content, mimeType, container) {
|
|
51
|
+
const data = typeof content === "string" ? new TextEncoder().encode(content) : content;
|
|
52
|
+
return this.render(data, mimeType, container);
|
|
53
|
+
}
|
|
54
|
+
async renderPdf(content, container) {
|
|
55
|
+
return this.render(content, "application/pdf", container);
|
|
81
56
|
}
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
iframe.
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
)
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
57
|
+
async render(content, mimeType, container, opts) {
|
|
58
|
+
const origin = window.location.origin;
|
|
59
|
+
const salt = this.options.salt
|
|
60
|
+
? new TextEncoder().encode(this.options.salt).buffer
|
|
61
|
+
: this.options.enableBrowserCaching
|
|
62
|
+
? await contentSalt(content, location.pathname)
|
|
63
|
+
: randomSalt();
|
|
64
|
+
const hash = await computeOriginHash(this.product, salt, origin);
|
|
65
|
+
const shimUrl = `https://${hash}-${PRODUCT_HASH}.${SCF_HOST}/${this.product}/shim.html?origin=${encodeURIComponent(origin)}${this.options.enableBrowserCaching ? "&cache=1" : ""}`;
|
|
66
|
+
const iframeOrigin = new URL(shimUrl).origin;
|
|
67
|
+
const iframe = document.createElement("iframe");
|
|
68
|
+
iframe.setAttribute("sandbox", this.getSandbox());
|
|
69
|
+
iframe.style.cssText = "border:none;width:100%;height:100%";
|
|
70
|
+
if (this.options.useShadowDom) {
|
|
71
|
+
const host = document.createElement("div");
|
|
72
|
+
host.attachShadow({ mode: "closed" }).appendChild(iframe);
|
|
73
|
+
container.appendChild(host);
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
container.appendChild(iframe);
|
|
77
|
+
}
|
|
78
|
+
return new Promise((resolve, reject) => {
|
|
79
|
+
const channel = new MessageChannel();
|
|
80
|
+
let onLoaded;
|
|
81
|
+
const loaded = new Promise((r) => {
|
|
82
|
+
onLoaded = r;
|
|
83
|
+
});
|
|
84
|
+
channel.port1.onmessage = (e) => {
|
|
85
|
+
if (e.data?.type === "msg")
|
|
86
|
+
onLoaded();
|
|
87
|
+
else if (e.data?.type === "error")
|
|
88
|
+
reject(new Error(e.data.message));
|
|
89
|
+
};
|
|
90
|
+
iframe.onload = () => {
|
|
91
|
+
iframe.contentWindow?.postMessage({
|
|
92
|
+
body: content.buffer.slice(content.byteOffset, content.byteOffset + content.byteLength),
|
|
93
|
+
mimeType,
|
|
94
|
+
salt,
|
|
95
|
+
unsafeDocumentWrite: opts?.unsafeDocumentWrite,
|
|
96
|
+
}, iframeOrigin, [channel.port2]);
|
|
97
|
+
resolve({
|
|
98
|
+
iframe,
|
|
99
|
+
origin: iframeOrigin,
|
|
100
|
+
sendMessage: (data, transfer) => iframe.contentWindow?.postMessage(data, iframeOrigin, transfer),
|
|
101
|
+
fullyLoadedPromiseWithTimeout: (ms) => Promise.race([
|
|
102
|
+
loaded,
|
|
103
|
+
new Promise((_, rej) => setTimeout(() => rej(new Error("Timeout")), ms)),
|
|
104
|
+
]),
|
|
105
|
+
dispose: () => iframe.remove(),
|
|
106
|
+
});
|
|
107
|
+
};
|
|
108
|
+
iframe.onerror = () => reject(new Error("Failed to load iframe"));
|
|
109
|
+
iframe.src = shimUrl;
|
|
117
110
|
});
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
return [...s].join(" ");
|
|
128
|
-
}
|
|
129
|
-
};
|
|
130
|
-
export {
|
|
131
|
-
SafeContentFrame
|
|
132
|
-
};
|
|
111
|
+
}
|
|
112
|
+
getSandbox() {
|
|
113
|
+
const s = new Set(this.options.sandbox || []);
|
|
114
|
+
s.add("allow-same-origin");
|
|
115
|
+
s.add("allow-scripts");
|
|
116
|
+
return [...s].join(" ");
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAwBA,MAAM,QAAQ,GAAG,wBAAwB,CAAC;AAC1C,MAAM,YAAY,GAAG,SAAS,CAAC;AAE/B,KAAK,UAAU,MAAM,CAAC,IAAiB;IACrC,OAAO,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,SAAS,EAAE,IAAI,CAAC,CAAC;AAC/C,CAAC;AAED,KAAK,UAAU,iBAAiB,CAC9B,OAAe,EACf,IAAiB,EACjB,MAAc;IAEd,MAAM,GAAG,GAAG,IAAI,WAAW,EAAE,CAAC;IAC9B,MAAM,GAAG,GAAG,GAAG,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;IAC/B,MAAM,KAAK,GAAG;QACZ,GAAG,CAAC,MAAM,CAAC,OAAO,CAAC;QACnB,GAAG;QACH,IAAI,UAAU,CAAC,IAAI,CAAC;QACpB,GAAG;QACH,GAAG,CAAC,MAAM,CAAC,MAAM,CAAC;KACnB,CAAC;IACF,MAAM,QAAQ,GAAG,IAAI,UAAU,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,CAAC;IACzE,IAAI,MAAM,GAAG,CAAC,CAAC;IACf,KAAK,MAAM,CAAC,IAAI,KAAK,EAAE,CAAC;QACtB,QAAQ,CAAC,GAAG,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC;QACxB,MAAM,IAAI,CAAC,CAAC,MAAM,CAAC;IACrB,CAAC;IAED,MAAM,IAAI,GAAG,IAAI,UAAU,CAAC,MAAM,MAAM,CAAC,QAAQ,CAAC,MAAqB,CAAC,CAAC,CAAC;IAC1E,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,CACxB,CAAC,GAAG,EAAE,CAAC,EAAE,EAAE,CAAC,MAAM,CAAC,GAAG,CAAC,GAAG,GAAG,GAAG,MAAM,CAAC,CAAC,CAAC,EACzC,MAAM,CAAC,CAAC,CAAC,CACV,CAAC;IACF,OAAO,MAAM,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,EAAE,EAAE,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;AAC5D,CAAC;AAED,SAAS,UAAU;IACjB,MAAM,GAAG,GAAG,IAAI,UAAU,CAAC,EAAE,CAAC,CAAC;IAC/B,MAAM,CAAC,eAAe,CAAC,GAAG,CAAC,CAAC;IAC5B,OAAO,GAAG,CAAC,MAAqB,CAAC;AACnC,CAAC;AAED,KAAK,UAAU,WAAW,CACxB,OAAmB,EACnB,QAAgB;IAEhB,MAAM,GAAG,GAAG,IAAI,WAAW,EAAE,CAAC;IAC9B,MAAM,GAAG,GAAG,GAAG,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;IAC/B,MAAM,QAAQ,GAAG,IAAI,UAAU,CAC7B,OAAO,CAAC,MAAM,GAAG,GAAG,CAAC,MAAM,GAAG,QAAQ,CAAC,MAAM,CAC9C,CAAC;IACF,QAAQ,CAAC,GAAG,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC;IACzB,QAAQ,CAAC,GAAG,CAAC,GAAG,EAAE,OAAO,CAAC,MAAM,CAAC,CAAC;IAClC,QAAQ,CAAC,GAAG,CAAC,GAAG,CAAC,MAAM,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAC,MAAM,GAAG,GAAG,CAAC,MAAM,CAAC,CAAC;IAChE,OAAO,MAAM,CAAC,QAAQ,CAAC,MAAqB,CAAC,CAAC;AAChD,CAAC;AAED,MAAM,OAAO,gBAAgB;IAEjB;IACA;IAFV,YACU,OAAe,EACf,UAAmC,EAAE;QADrC,YAAO,GAAP,OAAO,CAAQ;QACf,YAAO,GAAP,OAAO,CAA8B;IAC5C,CAAC;IAEJ,KAAK,CAAC,UAAU,CACd,IAAY,EACZ,SAAsB,EACtB,IAAwC;QAExC,OAAO,IAAI,CAAC,MAAM,CAChB,IAAI,WAAW,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,EAC9B,0BAA0B,EAC1B,SAAS,EACT,IAAI,CACL,CAAC;IACJ,CAAC;IAED,KAAK,CAAC,SAAS,CACb,OAA4B,EAC5B,QAAgB,EAChB,SAAsB;QAEtB,MAAM,IAAI,GACR,OAAO,OAAO,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,WAAW,EAAE,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC;QAC5E,OAAO,IAAI,CAAC,MAAM,CAAC,IAAI,EAAE,QAAQ,EAAE,SAAS,CAAC,CAAC;IAChD,CAAC;IAED,KAAK,CAAC,SAAS,CACb,OAAmB,EACnB,SAAsB;QAEtB,OAAO,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,iBAAiB,EAAE,SAAS,CAAC,CAAC;IAC5D,CAAC;IAEO,KAAK,CAAC,MAAM,CAClB,OAAmB,EACnB,QAAgB,EAChB,SAAsB,EACtB,IAAwC;QAExC,MAAM,MAAM,GAAG,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC;QACtC,MAAM,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI;YAC5B,CAAC,CAAE,IAAI,WAAW,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,MAAsB;YACrE,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,oBAAoB;gBACjC,CAAC,CAAC,MAAM,WAAW,CAAC,OAAO,EAAE,QAAQ,CAAC,QAAQ,CAAC;gBAC/C,CAAC,CAAC,UAAU,EAAE,CAAC;QAEnB,MAAM,IAAI,GAAG,MAAM,iBAAiB,CAAC,IAAI,CAAC,OAAO,EAAE,IAAI,EAAE,MAAM,CAAC,CAAC;QACjE,MAAM,OAAO,GAAG,WAAW,IAAI,IAAI,YAAY,IAAI,QAAQ,IAAI,IAAI,CAAC,OAAO,qBAAqB,kBAAkB,CAAC,MAAM,CAAC,GAAG,IAAI,CAAC,OAAO,CAAC,oBAAoB,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC;QACnL,MAAM,YAAY,GAAG,IAAI,GAAG,CAAC,OAAO,CAAC,CAAC,MAAM,CAAC;QAE7C,MAAM,MAAM,GAAG,QAAQ,CAAC,aAAa,CAAC,QAAQ,CAAC,CAAC;QAChD,MAAM,CAAC,YAAY,CAAC,SAAS,EAAE,IAAI,CAAC,UAAU,EAAE,CAAC,CAAC;QAClD,MAAM,CAAC,KAAK,CAAC,OAAO,GAAG,oCAAoC,CAAC;QAE5D,IAAI,IAAI,CAAC,OAAO,CAAC,YAAY,EAAE,CAAC;YAC9B,MAAM,IAAI,GAAG,QAAQ,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC;YAC3C,IAAI,CAAC,YAAY,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC;YAC1D,SAAS,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC;QAC9B,CAAC;aAAM,CAAC;YACN,SAAS,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC;QAChC,CAAC;QAED,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;YACrC,MAAM,OAAO,GAAG,IAAI,cAAc,EAAE,CAAC;YACrC,IAAI,QAAoB,CAAC;YACzB,MAAM,MAAM,GAAG,IAAI,OAAO,CAAO,CAAC,CAAC,EAAE,EAAE;gBACrC,QAAQ,GAAG,CAAC,CAAC;YACf,CAAC,CAAC,CAAC;YAEH,OAAO,CAAC,KAAK,CAAC,SAAS,GAAG,CAAC,CAAC,EAAE,EAAE;gBAC9B,IAAI,CAAC,CAAC,IAAI,EAAE,IAAI,KAAK,KAAK;oBAAE,QAAQ,EAAE,CAAC;qBAClC,IAAI,CAAC,CAAC,IAAI,EAAE,IAAI,KAAK,OAAO;oBAAE,MAAM,CAAC,IAAI,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC;YACvE,CAAC,CAAC;YAEF,MAAM,CAAC,MAAM,GAAG,GAAG,EAAE;gBACnB,MAAM,CAAC,aAAa,EAAE,WAAW,CAC/B;oBACE,IAAI,EAAE,OAAO,CAAC,MAAM,CAAC,KAAK,CACxB,OAAO,CAAC,UAAU,EAClB,OAAO,CAAC,UAAU,GAAG,OAAO,CAAC,UAAU,CACxC;oBACD,QAAQ;oBACR,IAAI;oBACJ,mBAAmB,EAAE,IAAI,EAAE,mBAAmB;iBAC/C,EACD,YAAY,EACZ,CAAC,OAAO,CAAC,KAAK,CAAC,CAChB,CAAC;gBACF,OAAO,CAAC;oBACN,MAAM;oBACN,MAAM,EAAE,YAAY;oBACpB,WAAW,EAAE,CAAC,IAAI,EAAE,QAAQ,EAAE,EAAE,CAC9B,MAAM,CAAC,aAAa,EAAE,WAAW,CAAC,IAAI,EAAE,YAAY,EAAE,QAAQ,CAAC;oBACjE,6BAA6B,EAAE,CAAC,EAAE,EAAE,EAAE,CACpC,OAAO,CAAC,IAAI,CAAC;wBACX,MAAM;wBACN,IAAI,OAAO,CAAO,CAAC,CAAC,EAAE,GAAG,EAAE,EAAE,CAC3B,UAAU,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,IAAI,KAAK,CAAC,SAAS,CAAC,CAAC,EAAE,EAAE,CAAC,CAChD;qBACF,CAAC;oBACJ,OAAO,EAAE,GAAG,EAAE,CAAC,MAAM,CAAC,MAAM,EAAE;iBAC/B,CAAC,CAAC;YACL,CAAC,CAAC;YACF,MAAM,CAAC,OAAO,GAAG,GAAG,EAAE,CAAC,MAAM,CAAC,IAAI,KAAK,CAAC,uBAAuB,CAAC,CAAC,CAAC;YAClE,MAAM,CAAC,GAAG,GAAG,OAAO,CAAC;QACvB,CAAC,CAAC,CAAC;IACL,CAAC;IAEO,UAAU;QAChB,MAAM,CAAC,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,OAAO,CAAC,OAAO,IAAI,EAAE,CAAC,CAAC;QAC9C,CAAC,CAAC,GAAG,CAAC,mBAAmB,CAAC,CAAC;QAC3B,CAAC,CAAC,GAAG,CAAC,eAAe,CAAC,CAAC;QACvB,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IAC1B,CAAC;CACF"}
|
package/dist/shadow_dom.d.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
declare const enableShadowDom: () => boolean;
|
|
2
|
-
declare const unsafeDisableShadowDom: () => boolean;
|
|
3
|
-
|
|
4
|
-
export { enableShadowDom, unsafeDisableShadowDom };
|
|
1
|
+
export declare const enableShadowDom: () => boolean;
|
|
2
|
+
export declare const unsafeDisableShadowDom: () => boolean;
|
|
3
|
+
//# sourceMappingURL=shadow_dom.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"shadow_dom.d.ts","sourceRoot":"","sources":["../src/shadow_dom.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,eAAe,eAAa,CAAC;AAC1C,eAAO,MAAM,sBAAsB,eAAc,CAAC"}
|
package/dist/shadow_dom.js
CHANGED
|
@@ -1,7 +1,3 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
export {
|
|
5
|
-
enableShadowDom,
|
|
6
|
-
unsafeDisableShadowDom
|
|
7
|
-
};
|
|
1
|
+
export const enableShadowDom = () => true;
|
|
2
|
+
export const unsafeDisableShadowDom = () => false;
|
|
3
|
+
//# sourceMappingURL=shadow_dom.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"shadow_dom.js","sourceRoot":"","sources":["../src/shadow_dom.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,eAAe,GAAG,GAAG,EAAE,CAAC,IAAI,CAAC;AAC1C,MAAM,CAAC,MAAM,sBAAsB,GAAG,GAAG,EAAE,CAAC,KAAK,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,15 +1,26 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "safe-content-frame",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.5",
|
|
4
4
|
"description": "Secure iframe rendering for untrusted content using SafeContentFrame",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"iframe",
|
|
7
|
+
"sandbox",
|
|
8
|
+
"security",
|
|
9
|
+
"html",
|
|
10
|
+
"content",
|
|
11
|
+
"safe"
|
|
12
|
+
],
|
|
13
|
+
"author": "AgentbaseAI Inc.",
|
|
5
14
|
"license": "MIT",
|
|
6
15
|
"type": "module",
|
|
7
16
|
"exports": {
|
|
8
17
|
".": {
|
|
18
|
+
"aui-source": "./src/index.ts",
|
|
9
19
|
"types": "./dist/index.d.ts",
|
|
10
20
|
"default": "./dist/index.js"
|
|
11
21
|
},
|
|
12
22
|
"./shadow_dom": {
|
|
23
|
+
"aui-source": "./src/shadow_dom.ts",
|
|
13
24
|
"types": "./dist/shadow_dom.d.ts",
|
|
14
25
|
"default": "./dist/shadow_dom.js"
|
|
15
26
|
}
|
|
@@ -17,27 +28,31 @@
|
|
|
17
28
|
"main": "./dist/index.js",
|
|
18
29
|
"types": "./dist/index.d.ts",
|
|
19
30
|
"files": [
|
|
20
|
-
"dist"
|
|
31
|
+
"dist",
|
|
32
|
+
"src",
|
|
33
|
+
"shadow_dom",
|
|
34
|
+
"README.md"
|
|
21
35
|
],
|
|
36
|
+
"sideEffects": false,
|
|
22
37
|
"devDependencies": {
|
|
23
|
-
"
|
|
24
|
-
"typescript": "^5.9.3",
|
|
25
|
-
"vite": "^7.2.7",
|
|
38
|
+
"vite": "^7.3.0",
|
|
26
39
|
"@assistant-ui/x-buildutils": "0.0.1"
|
|
27
40
|
},
|
|
28
41
|
"publishConfig": {
|
|
29
42
|
"access": "public",
|
|
30
43
|
"provenance": true
|
|
31
44
|
},
|
|
45
|
+
"homepage": "https://www.assistant-ui.com/safe-content-frame",
|
|
32
46
|
"repository": {
|
|
33
47
|
"type": "git",
|
|
34
|
-
"url": "https://github.com/assistant-ui/assistant-ui
|
|
48
|
+
"url": "git+https://github.com/assistant-ui/assistant-ui.git",
|
|
49
|
+
"directory": "packages/safe-content-frame"
|
|
35
50
|
},
|
|
36
51
|
"bugs": {
|
|
37
52
|
"url": "https://github.com/assistant-ui/assistant-ui/issues"
|
|
38
53
|
},
|
|
39
54
|
"scripts": {
|
|
40
|
-
"build": "
|
|
55
|
+
"build": "aui-build",
|
|
41
56
|
"dev": "vite demo"
|
|
42
57
|
}
|
|
43
58
|
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
export type SandboxOption =
|
|
2
|
+
| "allow-same-origin"
|
|
3
|
+
| "allow-scripts"
|
|
4
|
+
| "allow-forms"
|
|
5
|
+
| "allow-popups"
|
|
6
|
+
| "allow-modals"
|
|
7
|
+
| "allow-downloads"
|
|
8
|
+
| "allow-popups-to-escape-sandbox";
|
|
9
|
+
|
|
10
|
+
export interface SafeContentFrameOptions {
|
|
11
|
+
useShadowDom?: boolean;
|
|
12
|
+
enableBrowserCaching?: boolean;
|
|
13
|
+
sandbox?: SandboxOption[];
|
|
14
|
+
salt?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface RenderedFrame {
|
|
18
|
+
iframe: HTMLIFrameElement;
|
|
19
|
+
origin: string;
|
|
20
|
+
sendMessage(data: unknown, transfer?: Transferable[]): void;
|
|
21
|
+
fullyLoadedPromiseWithTimeout(timeoutMs: number): Promise<void>;
|
|
22
|
+
dispose(): void;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const SCF_HOST = "scf.auiusercontent.com";
|
|
26
|
+
const PRODUCT_HASH = "h184756";
|
|
27
|
+
|
|
28
|
+
async function sha256(data: ArrayBuffer): Promise<ArrayBuffer> {
|
|
29
|
+
return crypto.subtle.digest("SHA-256", data);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function computeOriginHash(
|
|
33
|
+
product: string,
|
|
34
|
+
salt: ArrayBuffer,
|
|
35
|
+
origin: string,
|
|
36
|
+
): Promise<string> {
|
|
37
|
+
const enc = new TextEncoder();
|
|
38
|
+
const sep = enc.encode("$@#|");
|
|
39
|
+
const parts = [
|
|
40
|
+
enc.encode(product),
|
|
41
|
+
sep,
|
|
42
|
+
new Uint8Array(salt),
|
|
43
|
+
sep,
|
|
44
|
+
enc.encode(origin),
|
|
45
|
+
];
|
|
46
|
+
const combined = new Uint8Array(parts.reduce((n, p) => n + p.length, 0));
|
|
47
|
+
let offset = 0;
|
|
48
|
+
for (const p of parts) {
|
|
49
|
+
combined.set(p, offset);
|
|
50
|
+
offset += p.length;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const hash = new Uint8Array(await sha256(combined.buffer as ArrayBuffer));
|
|
54
|
+
const bigint = hash.reduce(
|
|
55
|
+
(acc, b) => BigInt(256) * acc + BigInt(b),
|
|
56
|
+
BigInt(0),
|
|
57
|
+
);
|
|
58
|
+
return bigint.toString(36).padStart(50, "0").slice(0, 50);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function randomSalt(): ArrayBuffer {
|
|
62
|
+
const arr = new Uint8Array(10);
|
|
63
|
+
crypto.getRandomValues(arr);
|
|
64
|
+
return arr.buffer as ArrayBuffer;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function contentSalt(
|
|
68
|
+
content: Uint8Array,
|
|
69
|
+
pathname: string,
|
|
70
|
+
): Promise<ArrayBuffer> {
|
|
71
|
+
const enc = new TextEncoder();
|
|
72
|
+
const sep = enc.encode("$@#|");
|
|
73
|
+
const combined = new Uint8Array(
|
|
74
|
+
content.length + sep.length + pathname.length,
|
|
75
|
+
);
|
|
76
|
+
combined.set(content, 0);
|
|
77
|
+
combined.set(sep, content.length);
|
|
78
|
+
combined.set(enc.encode(pathname), content.length + sep.length);
|
|
79
|
+
return sha256(combined.buffer as ArrayBuffer);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export class SafeContentFrame {
|
|
83
|
+
constructor(
|
|
84
|
+
private product: string,
|
|
85
|
+
private options: SafeContentFrameOptions = {},
|
|
86
|
+
) {}
|
|
87
|
+
|
|
88
|
+
async renderHtml(
|
|
89
|
+
html: string,
|
|
90
|
+
container: HTMLElement,
|
|
91
|
+
opts?: { unsafeDocumentWrite?: boolean },
|
|
92
|
+
): Promise<RenderedFrame> {
|
|
93
|
+
return this.render(
|
|
94
|
+
new TextEncoder().encode(html),
|
|
95
|
+
"text/html; charset=utf-8",
|
|
96
|
+
container,
|
|
97
|
+
opts,
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async renderRaw(
|
|
102
|
+
content: Uint8Array | string,
|
|
103
|
+
mimeType: string,
|
|
104
|
+
container: HTMLElement,
|
|
105
|
+
): Promise<RenderedFrame> {
|
|
106
|
+
const data =
|
|
107
|
+
typeof content === "string" ? new TextEncoder().encode(content) : content;
|
|
108
|
+
return this.render(data, mimeType, container);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async renderPdf(
|
|
112
|
+
content: Uint8Array,
|
|
113
|
+
container: HTMLElement,
|
|
114
|
+
): Promise<RenderedFrame> {
|
|
115
|
+
return this.render(content, "application/pdf", container);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
private async render(
|
|
119
|
+
content: Uint8Array,
|
|
120
|
+
mimeType: string,
|
|
121
|
+
container: HTMLElement,
|
|
122
|
+
opts?: { unsafeDocumentWrite?: boolean },
|
|
123
|
+
): Promise<RenderedFrame> {
|
|
124
|
+
const origin = window.location.origin;
|
|
125
|
+
const salt = this.options.salt
|
|
126
|
+
? (new TextEncoder().encode(this.options.salt).buffer as ArrayBuffer)
|
|
127
|
+
: this.options.enableBrowserCaching
|
|
128
|
+
? await contentSalt(content, location.pathname)
|
|
129
|
+
: randomSalt();
|
|
130
|
+
|
|
131
|
+
const hash = await computeOriginHash(this.product, salt, origin);
|
|
132
|
+
const shimUrl = `https://${hash}-${PRODUCT_HASH}.${SCF_HOST}/${this.product}/shim.html?origin=${encodeURIComponent(origin)}${this.options.enableBrowserCaching ? "&cache=1" : ""}`;
|
|
133
|
+
const iframeOrigin = new URL(shimUrl).origin;
|
|
134
|
+
|
|
135
|
+
const iframe = document.createElement("iframe");
|
|
136
|
+
iframe.setAttribute("sandbox", this.getSandbox());
|
|
137
|
+
iframe.style.cssText = "border:none;width:100%;height:100%";
|
|
138
|
+
|
|
139
|
+
if (this.options.useShadowDom) {
|
|
140
|
+
const host = document.createElement("div");
|
|
141
|
+
host.attachShadow({ mode: "closed" }).appendChild(iframe);
|
|
142
|
+
container.appendChild(host);
|
|
143
|
+
} else {
|
|
144
|
+
container.appendChild(iframe);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return new Promise((resolve, reject) => {
|
|
148
|
+
const channel = new MessageChannel();
|
|
149
|
+
let onLoaded: () => void;
|
|
150
|
+
const loaded = new Promise<void>((r) => {
|
|
151
|
+
onLoaded = r;
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
channel.port1.onmessage = (e) => {
|
|
155
|
+
if (e.data?.type === "msg") onLoaded();
|
|
156
|
+
else if (e.data?.type === "error") reject(new Error(e.data.message));
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
iframe.onload = () => {
|
|
160
|
+
iframe.contentWindow?.postMessage(
|
|
161
|
+
{
|
|
162
|
+
body: content.buffer.slice(
|
|
163
|
+
content.byteOffset,
|
|
164
|
+
content.byteOffset + content.byteLength,
|
|
165
|
+
),
|
|
166
|
+
mimeType,
|
|
167
|
+
salt,
|
|
168
|
+
unsafeDocumentWrite: opts?.unsafeDocumentWrite,
|
|
169
|
+
},
|
|
170
|
+
iframeOrigin,
|
|
171
|
+
[channel.port2],
|
|
172
|
+
);
|
|
173
|
+
resolve({
|
|
174
|
+
iframe,
|
|
175
|
+
origin: iframeOrigin,
|
|
176
|
+
sendMessage: (data, transfer) =>
|
|
177
|
+
iframe.contentWindow?.postMessage(data, iframeOrigin, transfer),
|
|
178
|
+
fullyLoadedPromiseWithTimeout: (ms) =>
|
|
179
|
+
Promise.race([
|
|
180
|
+
loaded,
|
|
181
|
+
new Promise<void>((_, rej) =>
|
|
182
|
+
setTimeout(() => rej(new Error("Timeout")), ms),
|
|
183
|
+
),
|
|
184
|
+
]),
|
|
185
|
+
dispose: () => iframe.remove(),
|
|
186
|
+
});
|
|
187
|
+
};
|
|
188
|
+
iframe.onerror = () => reject(new Error("Failed to load iframe"));
|
|
189
|
+
iframe.src = shimUrl;
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
private getSandbox(): string {
|
|
194
|
+
const s = new Set(this.options.sandbox || []);
|
|
195
|
+
s.add("allow-same-origin");
|
|
196
|
+
s.add("allow-scripts");
|
|
197
|
+
return [...s].join(" ");
|
|
198
|
+
}
|
|
199
|
+
}
|