@noodleseed/one 0.141.1 → 0.141.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/bin.js +0 -0
- package/node_modules/@noodle-borg/assistant-gateway/dist/portable.d.ts +0 -1
- package/node_modules/@noodle-borg/assistant-gateway/dist/portable.js +0 -1
- package/node_modules/@noodle-borg/service/dist/routes/assistant-dispatch.js +1 -4
- package/node_modules/@noodle-borg/service/dist/routes/assistant-public-session.js +1 -1
- package/node_modules/@noodle-borg/service/dist/routes/assistant-route-http.js +2 -2
- package/node_modules/@noodle-borg/service/dist/routes/assistant-sandbox.js +47 -12
- package/node_modules/@noodle-borg/service/dist/routes/assistant.js +2 -2
- package/node_modules/@noodle-borg/service/dist/service.js +1 -15
- package/node_modules/@noodleseed/assistant/README.md +9 -4
- package/package.json +1 -1
- package/node_modules/@noodle-borg/assistant-gateway/dist/assistant-app-sandbox.d.ts +0 -13
- package/node_modules/@noodle-borg/assistant-gateway/dist/assistant-app-sandbox.js +0 -204
package/dist/bin.js
CHANGED
|
File without changes
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
/** Assistant gateway contracts and in-memory adapters safe for the published CLI runtime. */
|
|
2
2
|
export * from './artifact-projection.js';
|
|
3
|
-
export * from './assistant-app-sandbox.js';
|
|
4
3
|
export * from './assistant-context.js';
|
|
5
4
|
export * from './assistant-customer-issuer.js';
|
|
6
5
|
export * from './assistant-customer-routing.js';
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
/** Assistant gateway contracts and in-memory adapters safe for the published CLI runtime. */
|
|
2
2
|
export * from './artifact-projection.js';
|
|
3
|
-
export * from './assistant-app-sandbox.js';
|
|
4
3
|
export * from './assistant-context.js';
|
|
5
4
|
export * from './assistant-customer-issuer.js';
|
|
6
5
|
export * from './assistant-customer-routing.js';
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { isAssistantSandboxHost, } from '@noodle-borg/assistant-gateway/portable';
|
|
2
1
|
import { handleAssistantAppRequest, handleAssistantPreflight, handleAssistantSession, handleAssistantTurn, handleConsoleApprovalNonce, } from './assistant.js';
|
|
3
2
|
import { handleAssistantClients } from './assistant-clients.js';
|
|
4
3
|
import { handleAssistantDoctor } from './assistant-doctor.js';
|
|
@@ -36,9 +35,7 @@ export function dispatchAssistantRoutes(req, res, url, deps) {
|
|
|
36
35
|
deps.applySecurityHeaders(res, deps.tls);
|
|
37
36
|
if (deps.enforceHttps(req, res, deps.tls))
|
|
38
37
|
return true;
|
|
39
|
-
|
|
40
|
-
const isolatedOrigin = isAssistantSandboxHost(req.headers.host, sandboxBase);
|
|
41
|
-
handleAssistantSandbox(req, res, { isolatedOrigin });
|
|
38
|
+
handleAssistantSandbox(req, res);
|
|
42
39
|
return true;
|
|
43
40
|
}
|
|
44
41
|
if (url.pathname === '/v1/assistant/public-sessions' && req.method === 'POST') {
|
|
@@ -98,7 +98,7 @@ export async function handlePublicAssistantSession(req, res, deps) {
|
|
|
98
98
|
const sessionBody = {
|
|
99
99
|
token: result.token,
|
|
100
100
|
expiresAt: result.expiresAt,
|
|
101
|
-
endpoints: assistantSessionEndpoints(deps.serviceBase(req)
|
|
101
|
+
endpoints: assistantSessionEndpoints(deps.serviceBase(req)),
|
|
102
102
|
...(configuration ? { configuration } : {}),
|
|
103
103
|
};
|
|
104
104
|
// Parse rather than trust: every published @noodleseed/assistant widget consumes this shape, and the
|
|
@@ -48,13 +48,13 @@ export function now(deps) {
|
|
|
48
48
|
return deps.clock?.() ?? new Date();
|
|
49
49
|
}
|
|
50
50
|
/** The one endpoint map every session mint returns; three mints, one source of truth. */
|
|
51
|
-
export function assistantSessionEndpoints(base
|
|
51
|
+
export function assistantSessionEndpoints(base) {
|
|
52
52
|
return {
|
|
53
53
|
turns: `${base}/v1/assistant/turns`,
|
|
54
54
|
toolConfirmations: `${base}/v1/assistant/tool-confirmations`,
|
|
55
55
|
interactions: `${base}/v1/assistant/interactions`,
|
|
56
56
|
apps: `${base}/v1/assistant/apps`,
|
|
57
|
-
sandbox: `${
|
|
57
|
+
sandbox: `${base}/v1/assistant/sandbox`,
|
|
58
58
|
transcript: `${base}/v1/assistant/transcript`,
|
|
59
59
|
};
|
|
60
60
|
}
|
|
@@ -1,23 +1,58 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Hosted widget sandbox document (ADR 0151 addendum). The embedded assistant renders MCP App
|
|
3
|
+
* widgets in a double iframe; when the outer frame is an `about:srcdoc` document it inherits the
|
|
4
|
+
* customer page's CSP, so a strict `script-src` silently blanks every widget. Serving this relay
|
|
5
|
+
* document from the service origin gives it — and the inner `srcdoc` widget document that inherits
|
|
6
|
+
* from it — a CSP the platform controls. The body is pinned byte-for-byte to
|
|
7
|
+
* `contract/v1/assistant-sandbox-document.html` and to the published client's srcdoc fallback; any
|
|
8
|
+
* relay-protocol change is a coordinated contract event, never a local edit.
|
|
9
|
+
*/
|
|
10
|
+
export const ASSISTANT_SANDBOX_DOCUMENT = `<!doctype html><meta charset="utf-8"><style>html,body,iframe{border:0;margin:0;width:100%;height:100%;overflow:hidden}body{background:transparent}</style><script>
|
|
11
|
+
let inner;
|
|
12
|
+
const ready={jsonrpc:'2.0',method:'ui/notifications/sandbox-proxy-ready',params:{}};
|
|
13
|
+
// Re-announce until the host acknowledges with the resource: when this document is hosted
|
|
14
|
+
// cross-origin, the first announcement can arrive before the host's load handler is listening.
|
|
15
|
+
const announce=setInterval(()=>parent.postMessage(ready,'*'),120);
|
|
16
|
+
setTimeout(()=>clearInterval(announce),15000);
|
|
17
|
+
addEventListener('message',(event)=>{
|
|
18
|
+
if(event.source===parent){
|
|
19
|
+
const message=event.data;
|
|
20
|
+
if(message?.method==='ui/notifications/sandbox-resource-ready'){
|
|
21
|
+
clearInterval(announce);
|
|
22
|
+
if(inner)return;
|
|
23
|
+
inner=document.createElement('iframe');
|
|
24
|
+
inner.setAttribute('sandbox',message.params?.sandbox||'allow-scripts');
|
|
25
|
+
inner.setAttribute('referrerpolicy','no-referrer');
|
|
26
|
+
inner.srcdoc=String(message.params?.html||'');
|
|
27
|
+
document.body.replaceChildren(inner);
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
inner?.contentWindow?.postMessage(message,'*');
|
|
31
|
+
} else if(inner && event.source===inner.contentWindow) {
|
|
32
|
+
parent.postMessage(event.data,'*');
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
parent.postMessage(ready,'*');
|
|
36
|
+
</script>`;
|
|
37
|
+
/**
|
|
38
|
+
* The sandbox document's own CSP. `sandbox allow-scripts` forces an opaque origin server-side even
|
|
39
|
+
* if a future embedder frames the URL without a sandbox attribute (the document can never read
|
|
40
|
+
* service cookies or storage); inline script/style must be allowed because the relay script and the
|
|
41
|
+
* inner widget document's injected bridge are inline by design; everything else stays closed —
|
|
42
|
+
* widget data flows only over the postMessage bridge, never direct fetch.
|
|
43
|
+
*/
|
|
44
|
+
const SANDBOX_CSP = "sandbox allow-scripts; default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; img-src https: data: blob:; font-src https: data:; frame-src about:; form-action 'none'; base-uri 'none'; frame-ancestors *";
|
|
45
|
+
/** Serve the static, secret-free sandbox document. GET-only; cacheable despite the no-store baseline. */
|
|
46
|
+
export function handleAssistantSandbox(req, res) {
|
|
4
47
|
if (req.method !== 'GET') {
|
|
5
48
|
res.statusCode = 405;
|
|
6
49
|
res.setHeader('Allow', 'GET');
|
|
7
50
|
res.end();
|
|
8
51
|
return;
|
|
9
52
|
}
|
|
10
|
-
const policy = resolveAssistantSandboxResponsePolicy(req.url ?? '/', options.isolatedOrigin === true);
|
|
11
|
-
res.setHeader('Content-Security-Policy', policy.contentSecurityPolicy);
|
|
12
|
-
if (!policy.ok) {
|
|
13
|
-
res.statusCode = 400;
|
|
14
|
-
res.setHeader('Content-Type', 'text/plain; charset=utf-8');
|
|
15
|
-
res.setHeader('Cache-Control', 'no-store');
|
|
16
|
-
res.end('Invalid sandbox policy');
|
|
17
|
-
return;
|
|
18
|
-
}
|
|
19
53
|
res.statusCode = 200;
|
|
20
54
|
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
|
55
|
+
res.setHeader('Content-Security-Policy', SANDBOX_CSP);
|
|
21
56
|
res.setHeader('Cache-Control', 'public, max-age=300');
|
|
22
57
|
res.end(ASSISTANT_SANDBOX_DOCUMENT);
|
|
23
58
|
}
|
|
@@ -142,7 +142,7 @@ export async function handleAssistantSession(req, res, deps) {
|
|
|
142
142
|
: {}),
|
|
143
143
|
caller,
|
|
144
144
|
configuration,
|
|
145
|
-
endpoints: assistantSessionEndpoints(deps.serviceBase(req)
|
|
145
|
+
endpoints: assistantSessionEndpoints(deps.serviceBase(req)),
|
|
146
146
|
});
|
|
147
147
|
}
|
|
148
148
|
const session = await deps.store.createSession({
|
|
@@ -166,7 +166,7 @@ export async function handleAssistantSession(req, res, deps) {
|
|
|
166
166
|
const sessionBody = {
|
|
167
167
|
token: session.token,
|
|
168
168
|
expiresAt: session.session.expiresAt,
|
|
169
|
-
endpoints: assistantSessionEndpoints(deps.serviceBase(req)
|
|
169
|
+
endpoints: assistantSessionEndpoints(deps.serviceBase(req)),
|
|
170
170
|
...(configuration ? { configuration } : {}),
|
|
171
171
|
};
|
|
172
172
|
// Every published @noodleseed/assistant widget consumes this shape; parse (don't just test)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { InMemoryAssistantStore
|
|
1
|
+
import { InMemoryAssistantStore } from '@noodle-borg/assistant-gateway/portable';
|
|
2
2
|
import { allowAllGate, InMemoryControlPlaneStore } from '@noodle-borg/control-plane/portable';
|
|
3
3
|
import { dispatchKnowledgeRequest } from '@noodle-borg/knowledge-operations/portable';
|
|
4
4
|
import { OPENAI_APPS_CHALLENGE_PATH } from '@noodle-borg/module';
|
|
@@ -50,9 +50,6 @@ const DEFAULT_MAX_BODY = 1 << 20;
|
|
|
50
50
|
const DEFAULT_MAX_DEPLOY_BODY = 32 * 1024 * 1024;
|
|
51
51
|
/** The combined service listener: tenant-scoped deploy management plus the multi-tenant MCP router. */
|
|
52
52
|
export function createServiceHandler(registry, options = {}) {
|
|
53
|
-
const assistantSandboxBaseUrl = options.assistantSandboxBaseUrl === undefined
|
|
54
|
-
? undefined
|
|
55
|
-
: validateAssistantSandboxBaseUrl(options.assistantSandboxBaseUrl, options.publicBaseUrl);
|
|
56
53
|
const logger = options.logger ?? noopLogger;
|
|
57
54
|
const tls = options.tls ?? {};
|
|
58
55
|
const buildInfo = options.buildInfo ?? resolveBuildInfo();
|
|
@@ -158,14 +155,6 @@ export function createServiceHandler(registry, options = {}) {
|
|
|
158
155
|
};
|
|
159
156
|
return (req, res) => {
|
|
160
157
|
const url = new URL(req.url ?? '/', 'http://localhost');
|
|
161
|
-
const onAssistantSandboxHost = isAssistantSandboxHost(req.headers.host, assistantSandboxBaseUrl);
|
|
162
|
-
if (onAssistantSandboxHost && url.pathname !== '/v1/assistant/sandbox') {
|
|
163
|
-
applySecurityHeaders(res, tls);
|
|
164
|
-
if (enforceHttps(req, res, tls))
|
|
165
|
-
return;
|
|
166
|
-
sendJson(res, 404, { error: 'not found' });
|
|
167
|
-
return;
|
|
168
|
-
}
|
|
169
158
|
// Liveness/readiness probes: un-gated and **not** HTTPS-enforced — Cloud Run's internal probe is plain
|
|
170
159
|
// HTTP, so enforcing HTTPS here would `426` the probe and the revision would never go healthy (ADR 0034).
|
|
171
160
|
// `/healthz` is a pure liveness 200 (no store touch); `/readyz` reflects the injected readiness probe.
|
|
@@ -261,9 +250,6 @@ export function createServiceHandler(registry, options = {}) {
|
|
|
261
250
|
audit: activeAudit,
|
|
262
251
|
maxBody,
|
|
263
252
|
serviceBase: (request) => options.publicBaseUrl ?? baseFromRequest(request, tls),
|
|
264
|
-
...(assistantSandboxBaseUrl === undefined
|
|
265
|
-
? {}
|
|
266
|
-
: { sandboxBase: () => assistantSandboxBaseUrl }),
|
|
267
253
|
...(options.assistantModelFetch !== undefined
|
|
268
254
|
? { modelFetch: options.assistantModelFetch }
|
|
269
255
|
: {}),
|
|
@@ -35,8 +35,8 @@ troubleshooting) lives at <https://docs.noodleseed.dev/guides/embedded-assistant
|
|
|
35
35
|
the server itself is `@noodleseed/one` (`npm install -g @noodleseed/one`).
|
|
36
36
|
|
|
37
37
|
If your page sends a `Content-Security-Policy`, allow the Noodle service origin in `connect-src` (turns and
|
|
38
|
-
event streams) and
|
|
39
|
-
|
|
38
|
+
event streams) **and** `frame-src` (app widgets render inside a hosted sandbox document served from the
|
|
39
|
+
service origin, `endpoints.sandbox`, so your `script-src` can stay strict). Details are in the guide's
|
|
40
40
|
"Content-Security-Policy on your page" section.
|
|
41
41
|
|
|
42
42
|
## Quick start
|
|
@@ -410,8 +410,13 @@ import "@noodleseed/assistant/app-view";
|
|
|
410
410
|
</template>
|
|
411
411
|
```
|
|
412
412
|
|
|
413
|
-
|
|
414
|
-
[
|
|
413
|
+
Configure Vue's `isCustomElement` for `noodle-app-view`. Angular uses the same element with `[client]`,
|
|
414
|
+
`[view]`, and `[theme]` property bindings. Do not serialize `client` or `view` into attributes.
|
|
415
|
+
|
|
416
|
+
`<noodle-app-view>` is the canonical host; `NoodleAppView` delegates to it. Both use the
|
|
417
|
+
service-advertised sandbox URL, route App calls through the supplied client, publish theme changes without
|
|
418
|
+
replacing the iframe, and request standard teardown on semantic replacement, disconnect, or an App teardown
|
|
419
|
+
request.
|
|
415
420
|
|
|
416
421
|
The App document owns its action intent: it calls standard `tools/call` after connecting and never relies on
|
|
417
422
|
native form navigation. If that call needs input or confirmation, the same `client` publishes the normal
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@noodleseed/one",
|
|
3
|
-
"version": "0.141.
|
|
3
|
+
"version": "0.141.2",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Noodle CLI by Noodle Seed — author, run, and deploy declarative MCP servers. Embedding the assistant in your own web app is @noodleseed/assistant.",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
/** Relay body pinned to `contract/v1/assistant-sandbox-document.html` and the browser client. */
|
|
2
|
-
export declare const ASSISTANT_SANDBOX_DOCUMENT = "<!doctype html><meta charset=\"utf-8\"><style>html,body,iframe{border:0;margin:0;width:100%;height:100%;overflow:hidden}body{background:transparent}</style><body><script>\nlet inner,loaded=false;\nif(self===top)throw new Error('The assistant sandbox relay must be embedded');\nconst standardSandbox='allow-scripts allow-same-origin allow-forms';\nconst rawParentOrigin=new URLSearchParams(location.hash.slice(1)).get('parentOrigin');\nlet expectedParentOrigin;\ntry{const parsed=new URL(rawParentOrigin||'');if(parsed.origin===rawParentOrigin&&/^https?:$/.test(parsed.protocol))expectedParentOrigin=parsed.origin}catch{}\nconst parentTarget=expectedParentOrigin||'*';\nconst ownOrigin=self.origin;\nconst ready={jsonrpc:'2.0',method:'ui/notifications/sandbox-proxy-ready',params:{}};\n// Re-announce until the host acknowledges with the resource: when this document is hosted\n// cross-origin, the first announcement can arrive before the host's load handler is listening.\nconst announce=setInterval(()=>parent.postMessage(ready,parentTarget),120);\nsetTimeout(()=>clearInterval(announce),15000);\naddEventListener('message',(event)=>{\n if(event.source===parent){\n if(expectedParentOrigin&&event.origin!==expectedParentOrigin)return;\n const message=event.data;\n if(message?.method==='ui/notifications/sandbox-resource-ready'){\n clearInterval(announce);\n if(loaded)return;\n loaded=true;\n const requestedSandbox=message.params?.sandbox;\n inner=document.createElement('iframe');\n inner.setAttribute('sandbox',requestedSandbox===standardSandbox?standardSandbox:'allow-scripts');\n inner.setAttribute('referrerpolicy','no-referrer');\n const permissions=message.params?.permissions;\n const allow=[];\n if(permissions?.camera)allow.push('camera');\n if(permissions?.microphone)allow.push('microphone');\n if(permissions?.geolocation)allow.push('geolocation');\n if(permissions?.clipboardWrite)allow.push('clipboard-write');\n if(allow.length)inner.setAttribute('allow',allow.join('; '));\n document.body.append(inner);\n let html=String(message.params?.html||'');\n const csp=message.params?.csp;\n if(csp&&typeof csp==='object'){\n const list=(value,fallback)=>Array.isArray(value)&&value.length?value.join(' '):fallback;\n const resources=Array.isArray(csp.resourceDomains)?csp.resourceDomains:[];\n const suffix=resources.length?' '+resources.join(' '):'';\n const policy=[\"default-src 'none'\",\"script-src 'unsafe-inline'\"+suffix,\"style-src 'unsafe-inline'\"+suffix,'img-src data: blob:'+suffix,'font-src data: blob:'+suffix,'media-src data: blob:'+suffix,'worker-src blob:'+suffix,'connect-src '+list(csp.connectDomains,\"'none'\"),'frame-src '+list(csp.frameDomains,\"'none'\"),\"object-src 'none'\",\"form-action 'none'\",'base-uri '+list(csp.baseUriDomains,\"'none'\")].join('; ');\n const meta='<meta http-equiv=\"Content-Security-Policy\" content=\"'+policy.replace(/&/g,'&').replace(/\"/g,'"')+'\">';\n const doctype=html.match(/^\\s*<!doctype[^>]*>/i);\n html=doctype?html.slice(0,doctype[0].length)+meta+html.slice(doctype[0].length):meta+html;\n }\n try{\n const doc=inner.contentDocument||inner.contentWindow?.document;\n if(!doc)throw new Error('document unavailable');\n doc.open();doc.write(html);doc.close();\n }catch{inner.srcdoc=html}\n return;\n }\n inner?.contentWindow?.postMessage(message,'*');\n } else if(inner && event.source===inner.contentWindow) {\n if(event.origin!==ownOrigin)return;\n parent.postMessage(event.data,parentTarget);\n }\n});\nparent.postMessage(ready,parentTarget);\n</script></body>";
|
|
3
|
-
export interface AssistantSandboxResponsePolicy {
|
|
4
|
-
readonly ok: boolean;
|
|
5
|
-
readonly contentSecurityPolicy: string;
|
|
6
|
-
}
|
|
7
|
-
/** Match a request authority to the configured sandbox origin, normalizing default ports. */
|
|
8
|
-
export declare function isAssistantSandboxHost(requestHost: string | undefined, sandboxBaseUrl: string | undefined): boolean;
|
|
9
|
-
/** Validate an untrusted query and return the CSP response header to apply before navigation. */
|
|
10
|
-
export declare function resolveAssistantSandboxResponsePolicy(requestUrl: string, isolatedOrigin?: boolean): AssistantSandboxResponsePolicy;
|
|
11
|
-
/** Normalize the operator-provided dedicated sandbox origin and keep it off the service origin. */
|
|
12
|
-
export declare function validateAssistantSandboxBaseUrl(value: string, serviceBase: string | undefined): string;
|
|
13
|
-
//# sourceMappingURL=assistant-app-sandbox.d.ts.map
|
|
@@ -1,204 +0,0 @@
|
|
|
1
|
-
/** Relay body pinned to `contract/v1/assistant-sandbox-document.html` and the browser client. */
|
|
2
|
-
export const ASSISTANT_SANDBOX_DOCUMENT = `<!doctype html><meta charset="utf-8"><style>html,body,iframe{border:0;margin:0;width:100%;height:100%;overflow:hidden}body{background:transparent}</style><body><script>
|
|
3
|
-
let inner,loaded=false;
|
|
4
|
-
if(self===top)throw new Error('The assistant sandbox relay must be embedded');
|
|
5
|
-
const standardSandbox='allow-scripts allow-same-origin allow-forms';
|
|
6
|
-
const rawParentOrigin=new URLSearchParams(location.hash.slice(1)).get('parentOrigin');
|
|
7
|
-
let expectedParentOrigin;
|
|
8
|
-
try{const parsed=new URL(rawParentOrigin||'');if(parsed.origin===rawParentOrigin&&/^https?:$/.test(parsed.protocol))expectedParentOrigin=parsed.origin}catch{}
|
|
9
|
-
const parentTarget=expectedParentOrigin||'*';
|
|
10
|
-
const ownOrigin=self.origin;
|
|
11
|
-
const ready={jsonrpc:'2.0',method:'ui/notifications/sandbox-proxy-ready',params:{}};
|
|
12
|
-
// Re-announce until the host acknowledges with the resource: when this document is hosted
|
|
13
|
-
// cross-origin, the first announcement can arrive before the host's load handler is listening.
|
|
14
|
-
const announce=setInterval(()=>parent.postMessage(ready,parentTarget),120);
|
|
15
|
-
setTimeout(()=>clearInterval(announce),15000);
|
|
16
|
-
addEventListener('message',(event)=>{
|
|
17
|
-
if(event.source===parent){
|
|
18
|
-
if(expectedParentOrigin&&event.origin!==expectedParentOrigin)return;
|
|
19
|
-
const message=event.data;
|
|
20
|
-
if(message?.method==='ui/notifications/sandbox-resource-ready'){
|
|
21
|
-
clearInterval(announce);
|
|
22
|
-
if(loaded)return;
|
|
23
|
-
loaded=true;
|
|
24
|
-
const requestedSandbox=message.params?.sandbox;
|
|
25
|
-
inner=document.createElement('iframe');
|
|
26
|
-
inner.setAttribute('sandbox',requestedSandbox===standardSandbox?standardSandbox:'allow-scripts');
|
|
27
|
-
inner.setAttribute('referrerpolicy','no-referrer');
|
|
28
|
-
const permissions=message.params?.permissions;
|
|
29
|
-
const allow=[];
|
|
30
|
-
if(permissions?.camera)allow.push('camera');
|
|
31
|
-
if(permissions?.microphone)allow.push('microphone');
|
|
32
|
-
if(permissions?.geolocation)allow.push('geolocation');
|
|
33
|
-
if(permissions?.clipboardWrite)allow.push('clipboard-write');
|
|
34
|
-
if(allow.length)inner.setAttribute('allow',allow.join('; '));
|
|
35
|
-
document.body.append(inner);
|
|
36
|
-
let html=String(message.params?.html||'');
|
|
37
|
-
const csp=message.params?.csp;
|
|
38
|
-
if(csp&&typeof csp==='object'){
|
|
39
|
-
const list=(value,fallback)=>Array.isArray(value)&&value.length?value.join(' '):fallback;
|
|
40
|
-
const resources=Array.isArray(csp.resourceDomains)?csp.resourceDomains:[];
|
|
41
|
-
const suffix=resources.length?' '+resources.join(' '):'';
|
|
42
|
-
const policy=["default-src 'none'","script-src 'unsafe-inline'"+suffix,"style-src 'unsafe-inline'"+suffix,'img-src data: blob:'+suffix,'font-src data: blob:'+suffix,'media-src data: blob:'+suffix,'worker-src blob:'+suffix,'connect-src '+list(csp.connectDomains,"'none'"),'frame-src '+list(csp.frameDomains,"'none'"),"object-src 'none'","form-action 'none'",'base-uri '+list(csp.baseUriDomains,"'none'")].join('; ');
|
|
43
|
-
const meta='<meta http-equiv="Content-Security-Policy" content="'+policy.replace(/&/g,'&').replace(/"/g,'"')+'">';
|
|
44
|
-
const doctype=html.match(/^\\s*<!doctype[^>]*>/i);
|
|
45
|
-
html=doctype?html.slice(0,doctype[0].length)+meta+html.slice(doctype[0].length):meta+html;
|
|
46
|
-
}
|
|
47
|
-
try{
|
|
48
|
-
const doc=inner.contentDocument||inner.contentWindow?.document;
|
|
49
|
-
if(!doc)throw new Error('document unavailable');
|
|
50
|
-
doc.open();doc.write(html);doc.close();
|
|
51
|
-
}catch{inner.srcdoc=html}
|
|
52
|
-
return;
|
|
53
|
-
}
|
|
54
|
-
inner?.contentWindow?.postMessage(message,'*');
|
|
55
|
-
} else if(inner && event.source===inner.contentWindow) {
|
|
56
|
-
if(event.origin!==ownOrigin)return;
|
|
57
|
-
parent.postMessage(event.data,parentTarget);
|
|
58
|
-
}
|
|
59
|
-
});
|
|
60
|
-
parent.postMessage(ready,parentTarget);
|
|
61
|
-
</script></body>`;
|
|
62
|
-
const COMPATIBILITY_CSP = "sandbox allow-scripts; default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; img-src https: data: blob:; font-src https: data:; frame-src about:; form-action 'none'; base-uri 'none'; frame-ancestors *";
|
|
63
|
-
const MAX_CSP_QUERY_LENGTH = 8_192;
|
|
64
|
-
const MAX_DOMAINS_PER_DIRECTIVE = 32;
|
|
65
|
-
const CSP_KEYS = ['connectDomains', 'resourceDomains', 'frameDomains', 'baseUriDomains'];
|
|
66
|
-
const CSP_KEY_SET = new Set(CSP_KEYS);
|
|
67
|
-
/** Match a request authority to the configured sandbox origin, normalizing default ports. */
|
|
68
|
-
export function isAssistantSandboxHost(requestHost, sandboxBaseUrl) {
|
|
69
|
-
if (requestHost === undefined || sandboxBaseUrl === undefined)
|
|
70
|
-
return false;
|
|
71
|
-
try {
|
|
72
|
-
const sandboxUrl = new URL(sandboxBaseUrl);
|
|
73
|
-
const requestUrl = new URL(`${sandboxUrl.protocol}//${requestHost}`);
|
|
74
|
-
return requestUrl.href === `${sandboxUrl.origin}/`;
|
|
75
|
-
}
|
|
76
|
-
catch {
|
|
77
|
-
return false;
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
function isRecord(value) {
|
|
81
|
-
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
82
|
-
}
|
|
83
|
-
function isLoopback(hostname) {
|
|
84
|
-
return hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '[::1]';
|
|
85
|
-
}
|
|
86
|
-
function normalizeCspDomain(value, connect) {
|
|
87
|
-
if (value.length > 512 || /[;\r\n'"\s]/u.test(value))
|
|
88
|
-
return undefined;
|
|
89
|
-
try {
|
|
90
|
-
const parsed = new URL(value);
|
|
91
|
-
const secure = parsed.protocol === 'https:' || (connect && parsed.protocol === 'wss:');
|
|
92
|
-
const local = isLoopback(parsed.hostname) &&
|
|
93
|
-
(parsed.protocol === 'http:' || (connect && parsed.protocol === 'ws:'));
|
|
94
|
-
if (!secure && !local)
|
|
95
|
-
return undefined;
|
|
96
|
-
if (parsed.username ||
|
|
97
|
-
parsed.password ||
|
|
98
|
-
parsed.pathname !== '/' ||
|
|
99
|
-
parsed.search ||
|
|
100
|
-
parsed.hash) {
|
|
101
|
-
return undefined;
|
|
102
|
-
}
|
|
103
|
-
return parsed.origin;
|
|
104
|
-
}
|
|
105
|
-
catch {
|
|
106
|
-
return undefined;
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
function parseSandboxCsp(requestUrl) {
|
|
110
|
-
const url = new URL(requestUrl, 'http://sandbox.invalid');
|
|
111
|
-
const values = url.searchParams.getAll('csp');
|
|
112
|
-
if (values.length === 0)
|
|
113
|
-
return { ok: true };
|
|
114
|
-
const serialized = values[0];
|
|
115
|
-
if (values.length !== 1 || serialized === undefined || serialized.length > MAX_CSP_QUERY_LENGTH) {
|
|
116
|
-
return { ok: false };
|
|
117
|
-
}
|
|
118
|
-
let parsed;
|
|
119
|
-
try {
|
|
120
|
-
parsed = JSON.parse(serialized);
|
|
121
|
-
}
|
|
122
|
-
catch {
|
|
123
|
-
return { ok: false };
|
|
124
|
-
}
|
|
125
|
-
if (!isRecord(parsed) || Object.keys(parsed).some((key) => !CSP_KEY_SET.has(key))) {
|
|
126
|
-
return { ok: false };
|
|
127
|
-
}
|
|
128
|
-
const csp = {};
|
|
129
|
-
for (const key of CSP_KEYS) {
|
|
130
|
-
const raw = parsed[key];
|
|
131
|
-
if (raw === undefined)
|
|
132
|
-
continue;
|
|
133
|
-
if (!Array.isArray(raw) || raw.length > MAX_DOMAINS_PER_DIRECTIVE)
|
|
134
|
-
return { ok: false };
|
|
135
|
-
const normalized = [];
|
|
136
|
-
for (const entry of raw) {
|
|
137
|
-
if (typeof entry !== 'string')
|
|
138
|
-
return { ok: false };
|
|
139
|
-
const domain = normalizeCspDomain(entry, key === 'connectDomains');
|
|
140
|
-
if (!domain)
|
|
141
|
-
return { ok: false };
|
|
142
|
-
normalized.push(domain);
|
|
143
|
-
}
|
|
144
|
-
csp[key] = [...new Set(normalized)];
|
|
145
|
-
}
|
|
146
|
-
return { ok: true, value: csp };
|
|
147
|
-
}
|
|
148
|
-
function sources(values, fallback) {
|
|
149
|
-
return values && values.length > 0 ? values.join(' ') : fallback;
|
|
150
|
-
}
|
|
151
|
-
function sandboxCsp(csp, isolatedOrigin) {
|
|
152
|
-
const resources = csp.resourceDomains ?? [];
|
|
153
|
-
const resourceSuffix = resources.length > 0 ? ` ${resources.join(' ')}` : '';
|
|
154
|
-
return [
|
|
155
|
-
isolatedOrigin
|
|
156
|
-
? 'sandbox allow-scripts allow-same-origin allow-forms'
|
|
157
|
-
: 'sandbox allow-scripts',
|
|
158
|
-
"default-src 'none'",
|
|
159
|
-
`script-src 'self' 'unsafe-inline'${resourceSuffix}`,
|
|
160
|
-
`style-src 'self' 'unsafe-inline'${resourceSuffix}`,
|
|
161
|
-
`img-src 'self' data: blob:${resourceSuffix}`,
|
|
162
|
-
`font-src 'self' data: blob:${resourceSuffix}`,
|
|
163
|
-
`media-src 'self' data: blob:${resourceSuffix}`,
|
|
164
|
-
`worker-src 'self' blob:${resourceSuffix}`,
|
|
165
|
-
`connect-src 'self'${csp.connectDomains && csp.connectDomains.length > 0 ? ` ${csp.connectDomains.join(' ')}` : ''}`,
|
|
166
|
-
`frame-src ${sources(csp.frameDomains, "'none'")}`,
|
|
167
|
-
"object-src 'none'",
|
|
168
|
-
"form-action 'none'",
|
|
169
|
-
`base-uri ${sources(csp.baseUriDomains, "'none'")}`,
|
|
170
|
-
'frame-ancestors *',
|
|
171
|
-
].join('; ');
|
|
172
|
-
}
|
|
173
|
-
/** Validate an untrusted query and return the CSP response header to apply before navigation. */
|
|
174
|
-
export function resolveAssistantSandboxResponsePolicy(requestUrl, isolatedOrigin = false) {
|
|
175
|
-
const parsed = parseSandboxCsp(requestUrl);
|
|
176
|
-
if (!parsed.ok)
|
|
177
|
-
return { ok: false, contentSecurityPolicy: COMPATIBILITY_CSP };
|
|
178
|
-
return {
|
|
179
|
-
ok: true,
|
|
180
|
-
contentSecurityPolicy: parsed.value !== undefined || isolatedOrigin
|
|
181
|
-
? sandboxCsp(parsed.value ?? {}, isolatedOrigin)
|
|
182
|
-
: COMPATIBILITY_CSP,
|
|
183
|
-
};
|
|
184
|
-
}
|
|
185
|
-
/** Normalize the operator-provided dedicated sandbox origin and keep it off the service origin. */
|
|
186
|
-
export function validateAssistantSandboxBaseUrl(value, serviceBase) {
|
|
187
|
-
let parsed;
|
|
188
|
-
try {
|
|
189
|
-
parsed = new URL(value);
|
|
190
|
-
}
|
|
191
|
-
catch {
|
|
192
|
-
throw new Error('assistantSandboxBaseUrl must be an exact HTTPS or HTTP loopback origin');
|
|
193
|
-
}
|
|
194
|
-
const loopback = isLoopback(parsed.hostname);
|
|
195
|
-
if (parsed.origin !== value ||
|
|
196
|
-
(parsed.protocol !== 'https:' && !(parsed.protocol === 'http:' && loopback))) {
|
|
197
|
-
throw new Error('assistantSandboxBaseUrl must be an exact HTTPS or HTTP loopback origin');
|
|
198
|
-
}
|
|
199
|
-
if (serviceBase !== undefined && new URL(serviceBase).origin === parsed.origin) {
|
|
200
|
-
throw new Error('assistantSandboxBaseUrl must use an origin separate from publicBaseUrl');
|
|
201
|
-
}
|
|
202
|
-
return parsed.origin;
|
|
203
|
-
}
|
|
204
|
-
//# sourceMappingURL=assistant-app-sandbox.js.map
|