@unifedev/thread-pages 1.0.3 → 1.1.0

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.
@@ -62,6 +62,16 @@ export const LIMITS = Object.freeze({
62
62
  watchMinMs: 2_000,
63
63
  watchMaxMs: 5 * 60 * 1000,
64
64
  /** Offline copy of the entry document kept in the host's key-value store. R2.30 */
65
+ /**
66
+ * Resolving a page's own files into its entry document (see pages/inline.ts).
67
+ * The per-file cap is generous because a page's stylesheet and data set are
68
+ * the whole point; the total is what keeps one page from becoming a document
69
+ * no phone will load. Base64 costs a third on top of both.
70
+ */
71
+ inlineFileBytes: 2 * 1024 * 1024,
72
+ inlineTotalBytes: 3 * 1024 * 1024,
73
+ /** How far `url()` inside an inlined stylesheet is followed. */
74
+ inlineCssDepth: 3,
65
75
  offlineCopyBytes: 200 * 1024,
66
76
  offlineCacheEntries: 32,
67
77
  offlineCacheBytes: 8 * 1024 * 1024,
@@ -1,3 +1,3 @@
1
1
  // Generated by scripts/build-runtime.mjs from src/runtime/kernel/main.ts.
2
2
  // Do not edit; run `npm run build:runtime`.
3
- export const KERNEL_RUNTIME = "\"use strict\";(()=>{var ee=Object.defineProperty;var te=(e,t,n)=>t in e?ee(e,t,{enumerable:!0,configurable:!0,writable:!0,value:n}):e[t]=n;var F=(e,t,n)=>te(e,typeof t!=\"symbol\"?t+\"\":t,n);var v=Object.freeze({entryDocumentBytes:5242880,uploadFileBytes:25165824,uploadsPerForm:8,submissionBodyBytes:65536,answersPerSubmission:64,answerValueChars:8e3,answerListItems:64,capabilityPayloadBytes:65536,capabilityJsonDepth:16,capabilityJsonNodes:1e4,promptChars:32768,resultTextBytes:65536,titleChars:240,storageValueBytes:32768,storageKeyChars:128,snapshotDefault:100,snapshotMax:200,activityDefault:8,activityMax:20,actionTokenMs:72e5,confirmationMs:12e4,selectionTokenMs:6e5,selectionTokens:32,idempotencyRecords:512,idempotencyMs:3e5,ratePerMinute:120,rateConcurrent:8,shellPollMs:1e4,watchDefaultMs:8e3,watchMinMs:2e3,watchMaxMs:3e5,offlineCopyBytes:204800,offlineCacheEntries:32,offlineCacheBytes:8388608,requestIdChars:96,methodNameChars:96,tokenChars:4096,errorMessageChars:512,summaryChars:512,projectsMax:200,providersMax:64,modelsPerProvider:64});var A=[\"invalid_json\",\"invalid_request\",\"invalid_params\",\"invalid_response\",\"request_too_large\",\"response_too_large\",\"unsupported_version\",\"unknown_method\",\"stale_page\",\"confirmation_required\",\"confirmation_invalid\",\"cancelled\",\"not_found\",\"conflict\",\"unavailable\",\"rate_limited\",\"handler_error\",\"invalid_result\"],he=new Set(A);var ye=Object.freeze({noPage:\"This session has no page yet. Run `bb thread-page init` in the session first.\",ineligible:\"Only visible root sessions have pages.\",pageTooLarge:`The page's entry document is larger than ${v.entryDocumentBytes/(1024*1024)} MiB and was not served.`,unavailable:\"The page's source is unreachable. Reconnect its host and try again.\",staleCopy:\"The source host is offline; this cached page is read-only.\",stalePage:\"This page changed; reload it before responding.\",handler:\"Could not execute the page action.\",rateLimited:\"Too many requests from this page; try again shortly.\",invalidSession:\"A valid session id is required.\",tokenInvalid:\"This page session is invalid or expired; reload the page.\"});var I=1,B=1,ne=new Set(A);function w(e){return typeof e==\"object\"&&e!==null&&!Array.isArray(e)}function _(e,t){return Object.keys(e).length===t.length&&t.every(o=>Object.prototype.hasOwnProperty.call(e,o))}function q(e,t){if(!w(e)||e.v!==B||typeof e.id!=\"string\"||typeof e.ok!=\"boolean\"||t!==void 0&&e.id!==t)return!1;if(e.ok===!0)return _(e,[\"v\",\"id\",\"ok\",\"result\"]);if(!_(e,[\"v\",\"id\",\"ok\",\"error\"])||!w(e.error))return!1;let n=e.error;return _(n,[\"code\",\"message\"])&&typeof n.code==\"string\"&&ne.has(n.code)&&typeof n.message==\"string\"&&n.message.length>0&&n.message.length<=512}function N(e){let t=e?.getAttribute(\"data-config\");if(!t)throw new Error(\"Thread Page runtime: configuration is missing\");return JSON.parse(t)}function re(e,t,n){let o=e.getAttribute(\"href\");if(o===null)return{kind:\"default\"};if(o.startsWith(\"#\"))return{kind:\"default\"};let s;try{s=new URL(o,n??t)}catch{return{kind:\"block\"}}return s.protocol!==\"http:\"&&s.protocol!==\"https:\"?{kind:\"block\"}:n&&s.href.startsWith(n)?{kind:\"default\"}:e.hasAttribute(\"download\")?{kind:\"default\"}:{kind:\"external\",url:s.href,label:(e.textContent||\"\").replace(/\\s+/g,\" \").trim().slice(0,160)}}function U(e,t){e.addEventListener(\"click\",n=>{if(n.defaultPrevented||n.button!==0)return;let s=n.target?.closest?.(\"a[href]\");if(!s)return;let u=e.querySelector(\"base\")?.getAttribute(\"href\")??null,l=u?new URL(u,e.baseURI).href:null,a=re(s,e.baseURI,l);a.kind!==\"default\"&&(n.preventDefault(),a.kind===\"external\"&&t(a.url,a.label))},!0)}function K(e,t){let n=Object.freeze({version:1,invoke:t.invoke,watch:t.watch,setDirty:t.setDirty});Object.defineProperty(e,\"threadPage\",{value:n,writable:!1,configurable:!1,enumerable:!0})}var T=class extends Error{constructor(n,o){super(o);F(this,\"code\");this.name=\"ThreadPageError\",this.code=n,Object.defineProperty(this,\"code\",{value:n,enumerable:!0,writable:!1})}};function j(e,t){let n=new Map,o=[],s=null,u=0;function l(){return u+=1,`tp-${typeof crypto<\"u\"&&typeof crypto.randomUUID==\"function\"?crypto.randomUUID():`${Date.now()}-${u}`}`}function a(m){let p=n.get(m);if(!(!p||!s))try{s(p.request)}catch(h){n.delete(m),p.reject(new T(\"invalid_request\",h instanceof Error?h.message:\"The request could not be sent\"))}}function f(m,p){return new Promise((h,y)=>{if(typeof m!=\"string\"){y(new T(\"invalid_request\",\"A method name is required\"));return}let b=l(),r={v:B,id:b,method:m,params:p===void 0?null:p,pageRevision:e};n.set(b,{request:r,resolve:h,reject:y}),s?a(b):o.push(b)})}function g(m,p,h,y){if(typeof h!=\"function\")throw new TypeError(\"Thread Page watch needs a listener\");let b=y?.intervalMs,r=typeof b==\"number\"&&Number.isFinite(b)?Math.max(v.watchMinMs,Math.min(v.watchMaxMs,Math.round(b))):v.watchDefaultMs,i=!1,c=!1,d=null;function E(x){i||(d!==null&&clearTimeout(d),d=setTimeout(k,x))}async function k(){if(d=null,!(i||c||t.visibilityState===\"hidden\")){c=!0;try{let x=await f(m,p);i||h(x,null)}catch(x){i||h(void 0,x)}finally{c=!1,i||E(r)}}}function R(){i||(t.visibilityState===\"hidden\"?(d!==null&&clearTimeout(d),d=null):E(0))}return t.addEventListener(\"visibilitychange\",R),E(0),()=>{i||(i=!0,d!==null&&clearTimeout(d),d=null,t.removeEventListener(\"visibilitychange\",R))}}return{invoke:f,watch:g,attach(m){for(s=m;o.length>0;){let p=o.shift();p&&a(p)}},receive(m){if(typeof m!=\"object\"||m===null)return!1;let p=m.id;if(typeof p!=\"string\")return!1;let h=n.get(p);if(!h)return!1;if(n.delete(p),!q(m,p))return h.reject(new T(\"invalid_response\",\"The Thread Page bridge returned an invalid response\")),!0;let y=m;return y.ok?h.resolve(y.result):h.reject(new T(y.error.code,y.error.message)),!0}}}function z(e){let t=new Map,n=0,o=!1,s=!1;function u(){let l=o||t.size>0;l!==s&&(s=l,e(l))}return{isDirty:()=>s,markForm(l){return n+=1,t.set(l,n),u(),n},versionOf:l=>t.get(l),clearForm(l,a){a!==void 0&&t.get(l)===a&&(t.delete(l),u())},setCustom(l){o=l===!0,u()}}}var oe=\"input,textarea,select,button,option,small,output,[data-thread-page-range],[data-thread-page-status]\";function H(e){if(!e)return\"\";let t=e.cloneNode(!0);for(let n of Array.from(t.querySelectorAll(oe)))n.remove();return(t.textContent||\"\").replace(/\\s+/g,\" \").trim()}function ie(e,t){let n=t.getAttribute(\"data-label\");if(n&&n.trim())return n.trim();let o=t.closest(\"fieldset\");if(o){let l=H(o.querySelector(\"legend\"));if(l)return l}let s=t.getAttribute(\"aria-label\");if(s&&s.trim())return s.trim();let u=t.closest(\"label\");if(u){let l=H(u);if(l)return l}if(t.id){let l=e.ownerDocument,a=Array.from(l.querySelectorAll(\"label[for]\")).find(g=>g.htmlFor===t.id),f=H(a??null);if(f)return f}return t.name}var se=new Set([\"button\",\"submit\",\"reset\",\"image\",\"file\"]);function ae(e){return Array.from(e.elements).filter(t=>{let n=t;return typeof n.name==\"string\"&&n.name.length>0&&!n.disabled&&\"type\"in n})}function $(e,t){let n=ae(e),o=[],s=new Set;if(t&&(C(t)===\"button\"||C(t)===\"input\")){let u=t,l=u.value||(u.textContent||\"\").trim();o.push({name:u.name||\"action\",label:\"Action\",value:l}),u.name&&s.add(u.name)}for(let u of n){let l=u.name,a=String(u.type||\"\").toLowerCase();if(s.has(l)||se.has(a))continue;s.add(l);let f=n.filter(g=>g.name===l);o.push({name:l,label:ie(e,u),value:le(u,f,a)})}return o}function C(e){return e.tagName.toLowerCase()}function le(e,t,n){if(n===\"checkbox\"){let o=t.filter(s=>C(s)===\"input\");return o.length===1?o[0]?.checked===!0:o.filter(s=>s.checked).map(s=>s.value)}if(n===\"radio\"){let o=t.find(s=>C(s)===\"input\"&&s.checked);return o?o.value:\"\"}return C(e)===\"select\"&&e.multiple?Array.from(e.selectedOptions).map(o=>o.value):t.length>1?t.map(o=>String(o.value??\"\")):String(e.value??\"\")}var ue=\"data-thread-page-manual\",V=\"data-thread-page-status\",de=\"data-thread-page-range\";function L(e){return e.hasAttribute(ue)}function S(e){let t=[];return\"tagName\"in e&&e.tagName.toLowerCase()===\"form\"&&t.push(e),\"querySelectorAll\"in e&&t.push(...Array.from(e.querySelectorAll(\"form\"))),t.filter(n=>!L(n))}function M(e){let t=e.querySelector(`[${V}]`);return t||(t=e.ownerDocument.createElement(\"p\"),t.setAttribute(V,\"\"),t.setAttribute(\"role\",\"status\"),e.appendChild(t)),t}var G=new WeakSet;function W(e){e.noValidate=!0;for(let t of Array.from(e.querySelectorAll('input[type=\"range\"]'))){if(G.has(t))continue;G.add(t);let n=e.ownerDocument.createElement(\"output\");n.setAttribute(de,\"\");let o=()=>{n.textContent=String(t.value)};t.addEventListener(\"input\",o),o(),t.insertAdjacentElement(\"afterend\",n)}}function P(e){return Array.from(e.querySelectorAll(\"input,textarea,select,button,fieldset\"))}function ce(e){let t=[];for(let n of Array.from(e.querySelectorAll('input[type=\"file\"]')))if(!n.disabled)for(let o of Array.from(n.files??[])){if(t.length>=v.uploadsPerForm)return t;t.push({field:n.name||\"file\",file:o})}return t}function Z(e){let t=[];for(let n of P(e))n.disabled||(n.disabled=!0,t.push(n));return t}function D(e){for(let t of e)t.disabled=!1}function me(e){let t=e.getAttribute(\"data-title\");return t&&t.trim()?t.trim().slice(0,300):(e.ownerDocument.querySelector(\"h1\")?.textContent||\"\").trim().slice(0,300)||\"Thread Page\"}function J(e,t,n){return{submissionId:n,form:e,title:me(e),answers:$(e,t),files:ce(e)}}var X=\"data-thread-page-offline\",O=\"Offline copy \\u2014 responses are disabled until the source host reconnects.\";function Y(e,t){let n=new Set,o=t;function s(){if(!e.body)return;let a=e.querySelector(`[${X}=\"host\"]`);o&&!a?(a=e.createElement(\"aside\"),a.setAttribute(X,\"host\"),a.setAttribute(\"role\",\"status\"),a.setAttribute(\"style\",\"position:relative;z-index:2147483647;margin:0;padding:.75rem 1rem;border-bottom:1px solid currentColor;font:600 14px/1.4 system-ui,sans-serif;background:Canvas;color:CanvasText\"),a.textContent=O,e.body.insertBefore(a,e.body.firstChild)):!o&&a&&a.remove()}function u(a){for(let f of S(a)){for(let g of P(f))g.disabled||(g.disabled=!0,n.add(g));M(f).textContent=O}}function l(){for(let a of n)a.disabled=!1;n.clear();for(let a of S(e)){let f=M(a);f.textContent===O&&(f.textContent=\"\")}}return{isReadOnly:()=>o,apply(a){o=a,a?u(e):l(),s()},prepare(a){o&&u(a),s()}}}function Q(e,t){let n=e.document,o=null,s=new Map,u=new WeakSet;function l(r){if(!o)return!1;try{return o.postMessage(r),!0}catch{return!1}}let a=z(r=>{l({kind:r?\"thread-page:dirty\":\"thread-page:clean\"})}),f=j(t.pageRevision,n),g=Y(n,t.stale);K(e,{version:1,invoke:(r,i)=>f.invoke(r,i),watch:(r,i,c,d)=>f.watch(r,i,c,d),setDirty:r=>a.setCustom(r!==!1)});function m(r){for(let i of S(r))W(i);g.prepare(r)}m(n),n.readyState===\"loading\"&&n.addEventListener(\"DOMContentLoaded\",()=>m(n),{once:!0}),typeof e.MutationObserver==\"function\"&&n.documentElement&&new e.MutationObserver(i=>{for(let c of i)for(let d of Array.from(c.addedNodes))d.nodeType===1&&m(d)}).observe(n.documentElement,{childList:!0,subtree:!0});function p(r){let c=r.target?.closest?.(\"form\");!c||L(c)||a.markForm(c)}n.addEventListener(\"input\",p,!0),n.addEventListener(\"change\",p,!0),n.addEventListener(\"submit\",r=>{let i=r.target;if(!i||i.tagName?.toLowerCase()!==\"form\"||L(i)||(r.preventDefault(),g.isReadOnly()||u.has(i)))return;let c=`sub-${Date.now().toString(36)}-${Math.random().toString(36).slice(2,10)}`,d=i,E=J(d,r.submitter??null,c),k={form:d,disabled:[],dirtyVersion:a.versionOf(d)};s.set(c,k),u.add(d),M(d).textContent=E.files.length>0?\"Uploading\\u2026\":\"Sending\\u2026\",k.disabled=Z(d),l({kind:\"thread-page:submit\",submissionId:c,title:E.title,answers:E.answers,files:E.files})||(s.delete(c),u.delete(d),D(k.disabled),M(d).textContent=\"Page connection is not ready; try again in a moment.\")},!0),U(n,(r,i)=>{f.invoke(\"navigation.openExternal\",i?{url:r,label:i}:{url:r}).catch(()=>{})});function h(r){if(w(r)){if(r.kind===\"thread-page:source-state\"){g.apply(r.stale===!0);return}if(r.kind===\"thread-page:submit-progress\"){let i=typeof r.submissionId==\"string\"?s.get(r.submissionId):void 0;i&&(M(i.form).textContent=String(r.message??\"Working\\u2026\").slice(0,160));return}if(r.kind===\"thread-page:submit-result\"){let i=typeof r.submissionId==\"string\"?s.get(r.submissionId):void 0;if(!i)return;s.delete(r.submissionId),u.delete(i.form);let c=r.ok===!0;M(i.form).textContent=c?String(r.message??\"Sent\").slice(0,160):String(r.error??\"Could not send\").slice(0,160),D(i.disabled),g.isReadOnly()&&g.apply(!0),c&&a.clearForm(i.form,i.dirtyVersion);return}f.receive(r)}}function y(r){o=r,r.onmessage=i=>h(i.data),r.start?.(),f.attach(i=>{r.postMessage(i)}),a.isDirty()&&l({kind:\"thread-page:dirty\"})}function b(r){if(o||r.source!==e.parent)return;let i=r.data;if(!w(i)||i.kind!==\"thread-page:connect\"||i.version!==I||!r.ports||r.ports.length!==1)return;r.stopImmediatePropagation();let c=r.ports[0];c&&y(c)}return e.addEventListener(\"message\",b,!0),t.stale&&g.apply(!0),e.parent.postMessage({kind:\"thread-page:ready\",version:I},\"*\"),{deliver:r=>h(r),connect:r=>y(r)}}Q(window,N(document.currentScript));})();";
3
+ export const KERNEL_RUNTIME = "\"use strict\";(()=>{var ee=Object.defineProperty;var te=(e,t,n)=>t in e?ee(e,t,{enumerable:!0,configurable:!0,writable:!0,value:n}):e[t]=n;var F=(e,t,n)=>te(e,typeof t!=\"symbol\"?t+\"\":t,n);var v=Object.freeze({entryDocumentBytes:5242880,uploadFileBytes:25165824,uploadsPerForm:8,submissionBodyBytes:65536,answersPerSubmission:64,answerValueChars:8e3,answerListItems:64,capabilityPayloadBytes:65536,capabilityJsonDepth:16,capabilityJsonNodes:1e4,promptChars:32768,resultTextBytes:65536,titleChars:240,storageValueBytes:32768,storageKeyChars:128,snapshotDefault:100,snapshotMax:200,activityDefault:8,activityMax:20,actionTokenMs:72e5,confirmationMs:12e4,selectionTokenMs:6e5,selectionTokens:32,idempotencyRecords:512,idempotencyMs:3e5,ratePerMinute:120,rateConcurrent:8,shellPollMs:1e4,watchDefaultMs:8e3,watchMinMs:2e3,watchMaxMs:3e5,inlineFileBytes:2097152,inlineTotalBytes:3145728,inlineCssDepth:3,offlineCopyBytes:204800,offlineCacheEntries:32,offlineCacheBytes:8388608,requestIdChars:96,methodNameChars:96,tokenChars:4096,errorMessageChars:512,summaryChars:512,projectsMax:200,providersMax:64,modelsPerProvider:64});var A=[\"invalid_json\",\"invalid_request\",\"invalid_params\",\"invalid_response\",\"request_too_large\",\"response_too_large\",\"unsupported_version\",\"unknown_method\",\"stale_page\",\"confirmation_required\",\"confirmation_invalid\",\"cancelled\",\"not_found\",\"conflict\",\"unavailable\",\"rate_limited\",\"handler_error\",\"invalid_result\"],he=new Set(A);var ye=Object.freeze({noPage:\"This session has no page yet. Run `bb thread-page init` in the session first.\",ineligible:\"Only visible root sessions have pages.\",pageTooLarge:`The page's entry document is larger than ${v.entryDocumentBytes/(1024*1024)} MiB and was not served.`,unavailable:\"The page's source is unreachable. Reconnect its host and try again.\",staleCopy:\"The source host is offline; this cached page is read-only.\",stalePage:\"This page changed; reload it before responding.\",handler:\"Could not execute the page action.\",rateLimited:\"Too many requests from this page; try again shortly.\",invalidSession:\"A valid session id is required.\",tokenInvalid:\"This page session is invalid or expired; reload the page.\"});var I=1,B=1,ne=new Set(A);function w(e){return typeof e==\"object\"&&e!==null&&!Array.isArray(e)}function _(e,t){return Object.keys(e).length===t.length&&t.every(o=>Object.prototype.hasOwnProperty.call(e,o))}function q(e,t){if(!w(e)||e.v!==B||typeof e.id!=\"string\"||typeof e.ok!=\"boolean\"||t!==void 0&&e.id!==t)return!1;if(e.ok===!0)return _(e,[\"v\",\"id\",\"ok\",\"result\"]);if(!_(e,[\"v\",\"id\",\"ok\",\"error\"])||!w(e.error))return!1;let n=e.error;return _(n,[\"code\",\"message\"])&&typeof n.code==\"string\"&&ne.has(n.code)&&typeof n.message==\"string\"&&n.message.length>0&&n.message.length<=512}function N(e){let t=e?.getAttribute(\"data-config\");if(!t)throw new Error(\"Thread Page runtime: configuration is missing\");return JSON.parse(t)}function re(e,t,n){let o=e.getAttribute(\"href\");if(o===null)return{kind:\"default\"};if(o.startsWith(\"#\"))return{kind:\"default\"};let s;try{s=new URL(o,n??t)}catch{return{kind:\"block\"}}return s.protocol!==\"http:\"&&s.protocol!==\"https:\"?{kind:\"block\"}:n&&s.href.startsWith(n)?{kind:\"default\"}:e.hasAttribute(\"download\")?{kind:\"default\"}:{kind:\"external\",url:s.href,label:(e.textContent||\"\").replace(/\\s+/g,\" \").trim().slice(0,160)}}function U(e,t){e.addEventListener(\"click\",n=>{if(n.defaultPrevented||n.button!==0)return;let s=n.target?.closest?.(\"a[href]\");if(!s)return;let u=e.querySelector(\"base\")?.getAttribute(\"href\")??null,l=u?new URL(u,e.baseURI).href:null,a=re(s,e.baseURI,l);a.kind!==\"default\"&&(n.preventDefault(),a.kind===\"external\"&&t(a.url,a.label))},!0)}function K(e,t){let n=Object.freeze({version:1,invoke:t.invoke,watch:t.watch,setDirty:t.setDirty});Object.defineProperty(e,\"threadPage\",{value:n,writable:!1,configurable:!1,enumerable:!0})}var T=class extends Error{constructor(n,o){super(o);F(this,\"code\");this.name=\"ThreadPageError\",this.code=n,Object.defineProperty(this,\"code\",{value:n,enumerable:!0,writable:!1})}};function j(e,t){let n=new Map,o=[],s=null,u=0;function l(){return u+=1,`tp-${typeof crypto<\"u\"&&typeof crypto.randomUUID==\"function\"?crypto.randomUUID():`${Date.now()}-${u}`}`}function a(m){let p=n.get(m);if(!(!p||!s))try{s(p.request)}catch(h){n.delete(m),p.reject(new T(\"invalid_request\",h instanceof Error?h.message:\"The request could not be sent\"))}}function f(m,p){return new Promise((h,y)=>{if(typeof m!=\"string\"){y(new T(\"invalid_request\",\"A method name is required\"));return}let b=l(),r={v:B,id:b,method:m,params:p===void 0?null:p,pageRevision:e};n.set(b,{request:r,resolve:h,reject:y}),s?a(b):o.push(b)})}function g(m,p,h,y){if(typeof h!=\"function\")throw new TypeError(\"Thread Page watch needs a listener\");let b=y?.intervalMs,r=typeof b==\"number\"&&Number.isFinite(b)?Math.max(v.watchMinMs,Math.min(v.watchMaxMs,Math.round(b))):v.watchDefaultMs,i=!1,c=!1,d=null;function E(x){i||(d!==null&&clearTimeout(d),d=setTimeout(k,x))}async function k(){if(d=null,!(i||c||t.visibilityState===\"hidden\")){c=!0;try{let x=await f(m,p);i||h(x,null)}catch(x){i||h(void 0,x)}finally{c=!1,i||E(r)}}}function R(){i||(t.visibilityState===\"hidden\"?(d!==null&&clearTimeout(d),d=null):E(0))}return t.addEventListener(\"visibilitychange\",R),E(0),()=>{i||(i=!0,d!==null&&clearTimeout(d),d=null,t.removeEventListener(\"visibilitychange\",R))}}return{invoke:f,watch:g,attach(m){for(s=m;o.length>0;){let p=o.shift();p&&a(p)}},receive(m){if(typeof m!=\"object\"||m===null)return!1;let p=m.id;if(typeof p!=\"string\")return!1;let h=n.get(p);if(!h)return!1;if(n.delete(p),!q(m,p))return h.reject(new T(\"invalid_response\",\"The Thread Page bridge returned an invalid response\")),!0;let y=m;return y.ok?h.resolve(y.result):h.reject(new T(y.error.code,y.error.message)),!0}}}function z(e){let t=new Map,n=0,o=!1,s=!1;function u(){let l=o||t.size>0;l!==s&&(s=l,e(l))}return{isDirty:()=>s,markForm(l){return n+=1,t.set(l,n),u(),n},versionOf:l=>t.get(l),clearForm(l,a){a!==void 0&&t.get(l)===a&&(t.delete(l),u())},setCustom(l){o=l===!0,u()}}}var oe=\"input,textarea,select,button,option,small,output,[data-thread-page-range],[data-thread-page-status]\";function H(e){if(!e)return\"\";let t=e.cloneNode(!0);for(let n of Array.from(t.querySelectorAll(oe)))n.remove();return(t.textContent||\"\").replace(/\\s+/g,\" \").trim()}function ie(e,t){let n=t.getAttribute(\"data-label\");if(n&&n.trim())return n.trim();let o=t.closest(\"fieldset\");if(o){let l=H(o.querySelector(\"legend\"));if(l)return l}let s=t.getAttribute(\"aria-label\");if(s&&s.trim())return s.trim();let u=t.closest(\"label\");if(u){let l=H(u);if(l)return l}if(t.id){let l=e.ownerDocument,a=Array.from(l.querySelectorAll(\"label[for]\")).find(g=>g.htmlFor===t.id),f=H(a??null);if(f)return f}return t.name}var se=new Set([\"button\",\"submit\",\"reset\",\"image\",\"file\"]);function ae(e){return Array.from(e.elements).filter(t=>{let n=t;return typeof n.name==\"string\"&&n.name.length>0&&!n.disabled&&\"type\"in n})}function $(e,t){let n=ae(e),o=[],s=new Set;if(t&&(C(t)===\"button\"||C(t)===\"input\")){let u=t,l=u.value||(u.textContent||\"\").trim();o.push({name:u.name||\"action\",label:\"Action\",value:l}),u.name&&s.add(u.name)}for(let u of n){let l=u.name,a=String(u.type||\"\").toLowerCase();if(s.has(l)||se.has(a))continue;s.add(l);let f=n.filter(g=>g.name===l);o.push({name:l,label:ie(e,u),value:le(u,f,a)})}return o}function C(e){return e.tagName.toLowerCase()}function le(e,t,n){if(n===\"checkbox\"){let o=t.filter(s=>C(s)===\"input\");return o.length===1?o[0]?.checked===!0:o.filter(s=>s.checked).map(s=>s.value)}if(n===\"radio\"){let o=t.find(s=>C(s)===\"input\"&&s.checked);return o?o.value:\"\"}return C(e)===\"select\"&&e.multiple?Array.from(e.selectedOptions).map(o=>o.value):t.length>1?t.map(o=>String(o.value??\"\")):String(e.value??\"\")}var ue=\"data-thread-page-manual\",V=\"data-thread-page-status\",de=\"data-thread-page-range\";function L(e){return e.hasAttribute(ue)}function S(e){let t=[];return\"tagName\"in e&&e.tagName.toLowerCase()===\"form\"&&t.push(e),\"querySelectorAll\"in e&&t.push(...Array.from(e.querySelectorAll(\"form\"))),t.filter(n=>!L(n))}function M(e){let t=e.querySelector(`[${V}]`);return t||(t=e.ownerDocument.createElement(\"p\"),t.setAttribute(V,\"\"),t.setAttribute(\"role\",\"status\"),e.appendChild(t)),t}var G=new WeakSet;function W(e){e.noValidate=!0;for(let t of Array.from(e.querySelectorAll('input[type=\"range\"]'))){if(G.has(t))continue;G.add(t);let n=e.ownerDocument.createElement(\"output\");n.setAttribute(de,\"\");let o=()=>{n.textContent=String(t.value)};t.addEventListener(\"input\",o),o(),t.insertAdjacentElement(\"afterend\",n)}}function P(e){return Array.from(e.querySelectorAll(\"input,textarea,select,button,fieldset\"))}function ce(e){let t=[];for(let n of Array.from(e.querySelectorAll('input[type=\"file\"]')))if(!n.disabled)for(let o of Array.from(n.files??[])){if(t.length>=v.uploadsPerForm)return t;t.push({field:n.name||\"file\",file:o})}return t}function Z(e){let t=[];for(let n of P(e))n.disabled||(n.disabled=!0,t.push(n));return t}function D(e){for(let t of e)t.disabled=!1}function me(e){let t=e.getAttribute(\"data-title\");return t&&t.trim()?t.trim().slice(0,300):(e.ownerDocument.querySelector(\"h1\")?.textContent||\"\").trim().slice(0,300)||\"Thread Page\"}function J(e,t,n){return{submissionId:n,form:e,title:me(e),answers:$(e,t),files:ce(e)}}var X=\"data-thread-page-offline\",O=\"Offline copy \\u2014 responses are disabled until the source host reconnects.\";function Y(e,t){let n=new Set,o=t;function s(){if(!e.body)return;let a=e.querySelector(`[${X}=\"host\"]`);o&&!a?(a=e.createElement(\"aside\"),a.setAttribute(X,\"host\"),a.setAttribute(\"role\",\"status\"),a.setAttribute(\"style\",\"position:relative;z-index:2147483647;margin:0;padding:.75rem 1rem;border-bottom:1px solid currentColor;font:600 14px/1.4 system-ui,sans-serif;background:Canvas;color:CanvasText\"),a.textContent=O,e.body.insertBefore(a,e.body.firstChild)):!o&&a&&a.remove()}function u(a){for(let f of S(a)){for(let g of P(f))g.disabled||(g.disabled=!0,n.add(g));M(f).textContent=O}}function l(){for(let a of n)a.disabled=!1;n.clear();for(let a of S(e)){let f=M(a);f.textContent===O&&(f.textContent=\"\")}}return{isReadOnly:()=>o,apply(a){o=a,a?u(e):l(),s()},prepare(a){o&&u(a),s()}}}function Q(e,t){let n=e.document,o=null,s=new Map,u=new WeakSet;function l(r){if(!o)return!1;try{return o.postMessage(r),!0}catch{return!1}}let a=z(r=>{l({kind:r?\"thread-page:dirty\":\"thread-page:clean\"})}),f=j(t.pageRevision,n),g=Y(n,t.stale);K(e,{version:1,invoke:(r,i)=>f.invoke(r,i),watch:(r,i,c,d)=>f.watch(r,i,c,d),setDirty:r=>a.setCustom(r!==!1)});function m(r){for(let i of S(r))W(i);g.prepare(r)}m(n),n.readyState===\"loading\"&&n.addEventListener(\"DOMContentLoaded\",()=>m(n),{once:!0}),typeof e.MutationObserver==\"function\"&&n.documentElement&&new e.MutationObserver(i=>{for(let c of i)for(let d of Array.from(c.addedNodes))d.nodeType===1&&m(d)}).observe(n.documentElement,{childList:!0,subtree:!0});function p(r){let c=r.target?.closest?.(\"form\");!c||L(c)||a.markForm(c)}n.addEventListener(\"input\",p,!0),n.addEventListener(\"change\",p,!0),n.addEventListener(\"submit\",r=>{let i=r.target;if(!i||i.tagName?.toLowerCase()!==\"form\"||L(i)||(r.preventDefault(),g.isReadOnly()||u.has(i)))return;let c=`sub-${Date.now().toString(36)}-${Math.random().toString(36).slice(2,10)}`,d=i,E=J(d,r.submitter??null,c),k={form:d,disabled:[],dirtyVersion:a.versionOf(d)};s.set(c,k),u.add(d),M(d).textContent=E.files.length>0?\"Uploading\\u2026\":\"Sending\\u2026\",k.disabled=Z(d),l({kind:\"thread-page:submit\",submissionId:c,title:E.title,answers:E.answers,files:E.files})||(s.delete(c),u.delete(d),D(k.disabled),M(d).textContent=\"Page connection is not ready; try again in a moment.\")},!0),U(n,(r,i)=>{f.invoke(\"navigation.openExternal\",i?{url:r,label:i}:{url:r}).catch(()=>{})});function h(r){if(w(r)){if(r.kind===\"thread-page:source-state\"){g.apply(r.stale===!0);return}if(r.kind===\"thread-page:submit-progress\"){let i=typeof r.submissionId==\"string\"?s.get(r.submissionId):void 0;i&&(M(i.form).textContent=String(r.message??\"Working\\u2026\").slice(0,160));return}if(r.kind===\"thread-page:submit-result\"){let i=typeof r.submissionId==\"string\"?s.get(r.submissionId):void 0;if(!i)return;s.delete(r.submissionId),u.delete(i.form);let c=r.ok===!0;M(i.form).textContent=c?String(r.message??\"Sent\").slice(0,160):String(r.error??\"Could not send\").slice(0,160),D(i.disabled),g.isReadOnly()&&g.apply(!0),c&&a.clearForm(i.form,i.dirtyVersion);return}f.receive(r)}}function y(r){o=r,r.onmessage=i=>h(i.data),r.start?.(),f.attach(i=>{r.postMessage(i)}),a.isDirty()&&l({kind:\"thread-page:dirty\"})}function b(r){if(o||r.source!==e.parent)return;let i=r.data;if(!w(i)||i.kind!==\"thread-page:connect\"||i.version!==I||!r.ports||r.ports.length!==1)return;r.stopImmediatePropagation();let c=r.ports[0];c&&y(c)}return e.addEventListener(\"message\",b,!0),t.stale&&g.apply(!0),e.parent.postMessage({kind:\"thread-page:ready\",version:I},\"*\"),{deliver:r=>h(r),connect:r=>y(r)}}Q(window,N(document.currentScript));})();";
@@ -1,3 +1,3 @@
1
1
  // Generated by scripts/build-runtime.mjs from src/runtime/shell/main.ts.
2
2
  // Do not edit; run `npm run build:runtime`.
3
- export const SHELL_RUNTIME = "\"use strict\";(()=>{var M=Object.freeze({entryDocumentBytes:5242880,uploadFileBytes:25165824,uploadsPerForm:8,submissionBodyBytes:65536,answersPerSubmission:64,answerValueChars:8e3,answerListItems:64,capabilityPayloadBytes:65536,capabilityJsonDepth:16,capabilityJsonNodes:1e4,promptChars:32768,resultTextBytes:65536,titleChars:240,storageValueBytes:32768,storageKeyChars:128,snapshotDefault:100,snapshotMax:200,activityDefault:8,activityMax:20,actionTokenMs:72e5,confirmationMs:12e4,selectionTokenMs:6e5,selectionTokens:32,idempotencyRecords:512,idempotencyMs:3e5,ratePerMinute:120,rateConcurrent:8,shellPollMs:1e4,watchDefaultMs:8e3,watchMinMs:2e3,watchMaxMs:3e5,offlineCopyBytes:204800,offlineCacheEntries:32,offlineCacheBytes:8388608,requestIdChars:96,methodNameChars:96,tokenChars:4096,errorMessageChars:512,summaryChars:512,projectsMax:200,providersMax:64,modelsPerProvider:64});var x=[\"invalid_json\",\"invalid_request\",\"invalid_params\",\"invalid_response\",\"request_too_large\",\"response_too_large\",\"unsupported_version\",\"unknown_method\",\"stale_page\",\"confirmation_required\",\"confirmation_invalid\",\"cancelled\",\"not_found\",\"conflict\",\"unavailable\",\"rate_limited\",\"handler_error\",\"invalid_result\"],Y=new Set(x);var Q=Object.freeze({noPage:\"This session has no page yet. Run `bb thread-page init` in the session first.\",ineligible:\"Only visible root sessions have pages.\",pageTooLarge:`The page's entry document is larger than ${M.entryDocumentBytes/(1024*1024)} MiB and was not served.`,unavailable:\"The page's source is unreachable. Reconnect its host and try again.\",staleCopy:\"The source host is offline; this cached page is read-only.\",stalePage:\"This page changed; reload it before responding.\",handler:\"Could not execute the page action.\",rateLimited:\"Too many requests from this page; try again shortly.\",invalidSession:\"A valid session id is required.\",tokenInvalid:\"This page session is invalid or expired; reload the page.\"});var E=1,_=1,$=new Set(x),j=/^[A-Za-z0-9][A-Za-z0-9._:-]{0,95}$/,F=/^[a-z][a-zA-Z0-9]*(?:\\.[a-z][a-zA-Z0-9]*)+$/;function b(e){return typeof e==\"object\"&&e!==null&&!Array.isArray(e)}function R(e,t){return Object.keys(e).length===t.length&&t.every(l=>Object.prototype.hasOwnProperty.call(e,l))}function C(e){return typeof e==\"string\"&&j.test(e)}function T(e,t){return b(e)&&R(e,[\"v\",\"id\",\"method\",\"params\",\"pageRevision\"])&&e.v===_&&C(e.id)&&typeof e.method==\"string\"&&e.method.length>=3&&e.method.length<=96&&F.test(e.method)&&e.pageRevision===t}function B(e,t){if(!b(e)||e.v!==_||typeof e.id!=\"string\"||typeof e.ok!=\"boolean\"||t!==void 0&&e.id!==t)return!1;if(e.ok===!0)return R(e,[\"v\",\"id\",\"ok\",\"result\"]);if(!R(e,[\"v\",\"id\",\"ok\",\"error\"])||!b(e.error))return!1;let o=e.error;return R(o,[\"code\",\"message\"])&&typeof o.code==\"string\"&&$.has(o.code)&&typeof o.message==\"string\"&&o.message.length>0&&o.message.length<=512}function w(e,t,o){return{v:1,id:C(e)?e:\"invalid\",ok:!1,error:{code:t,message:o.slice(0,512)||\"Request failed\"}}}function P(e){let t=e?.getAttribute(\"data-config\");if(!t)throw new Error(\"Thread Page runtime: configuration is missing\");return JSON.parse(t)}function D(e){let t=e.querySelector(\"p\"),o=e.querySelector('button[value=\"cancel\"]'),l=e.querySelector('button[value=\"confirm\"]'),c=null,d;function p(g){let y=c;if(c=null,g&&d)try{d()}catch{}d=void 0,e.open&&e.close(),y?.(g)}return o?.addEventListener(\"click\",g=>{g.preventDefault(),p(!1)}),l?.addEventListener(\"click\",g=>{g.preventDefault(),p(!0)}),e.addEventListener(\"cancel\",g=>{g.preventDefault(),p(!1)}),e.addEventListener(\"close\",()=>{c&&p(!1)}),{confirm(g,y){return new Promise(m=>{if(c&&p(!1),t&&(t.textContent=g),c=m,d=y,typeof e.showModal==\"function\")try{e.showModal()}catch{p(!1)}else p(!1)})}}}function O(e){let t=null;return{inPlace(o){e.location.assign(o)},reserveWindow(){try{if(t=e.open(\"\",\"_blank\"),t)try{t.opener=null}catch{}}catch{t=null}},external(o){let l=t;if(t=null,l&&!l.closed)try{l.location.href=o;return}catch{try{l.close()}catch{}}e.location.assign(o)},release(){let o=t;t=null;try{o?.close()}catch{}}}}function A(e,t,o,l=e.fetch.bind(e)){let c=`\"${t.pageRevision}\"`,d=!1,p=!1,g=!1,y=t.stale,m=null,v=null;function k(s){m!==null&&clearTimeout(m),m=null,!(p||e.document.visibilityState!==\"visible\")&&(m=setTimeout(()=>{m=null,i()},s))}function r(){m!==null&&clearTimeout(m),m=null,v?.abort(),v=null}function n(){d?(o.setStatus(\"Page changed \\u2014 reload when ready\",!0),o.showReload(!0)):o.reloadView()}async function i(){if(!(p||g||e.document.visibilityState!==\"visible\")){if(Date.now()>=t.expiresAt-3e4){p=!0,d?(o.setStatus(\"Session expiring \\u2014 reload when ready\",!0),o.showReload(!0)):o.reloadView();return}g=!0,v=new AbortController;try{let s=await l(t.documentUrl,{method:\"GET\",credentials:\"same-origin\",cache:\"no-store\",headers:{\"if-none-match\":c},signal:v.signal});if(s.status===401||s.status===403){p=!0,o.setStatus(\"Session expired \\u2014 reload this page\",!0),o.showReload(!0);return}if(!s.ok&&s.status!==304){o.setStatus(\"Page unavailable\",!0);return}let f=s.headers.get(\"x-thread-page-stale\")===\"true\";o.setWorking(s.headers.get(\"x-thread-page-activity\")===\"working\"),f!==y&&(y=f,o.onStaleChanged(f)),o.setStatus(f?\"Offline copy \\u2014 read-only\":\"\",f);let u=s.headers.get(\"etag\");u&&u!==c&&(c=u,n())}catch(s){s instanceof DOMException&&s.name===\"AbortError\"||o.setStatus(\"Cannot check for updates\",!0)}finally{v=null,g=!1,k(t.pollMs)}}}return e.document.addEventListener(\"visibilitychange\",()=>{e.document.visibilityState===\"visible\"?k(0):r()}),{start:()=>k(t.pollMs),setDirty:s=>{d=s},pollNow:()=>i(),isStopped:()=>p}}function I(e){let{config:t,confirmer:o,navigator:l}=e,c=e.fetchImpl??fetch;function d(r,n){r.postMessage(n)}async function p(r){return(await c(t.bridgeUrl,{method:\"POST\",credentials:\"same-origin\",cache:\"no-store\",headers:{\"content-type\":\"application/json\"},body:JSON.stringify(r)})).json().catch(()=>null)}function g(r){return!b(r)||r.kind!==\"page\"&&r.kind!==\"host\"&&r.kind!==\"external\"||typeof r.url!=\"string\"||r.kind===\"external\"&&!/^https?:\\/\\//i.test(r.url)||r.kind!==\"external\"&&!r.url.startsWith(\"/\")?null:{kind:r.kind,url:r.url}}function y(r,n,i){if(!b(i)||!B(i.response,n.id)){d(r,w(n.id,\"invalid_response\",\"The Thread Page bridge returned an invalid response\"));return}let s=i.navigate===void 0?null:g(i.navigate);if(i.response.ok&&s){d(r,i.response),s.kind===\"external\"?l.external(s.url):l.inPlace(s.url);return}l.release(),d(r,i.response)}async function m(r,n){try{let i=await p({actionToken:t.actionToken,request:n});if(b(i)&&b(i.confirm)){let s=i.confirm;if(typeof s.challenge!=\"string\"||typeof s.summary!=\"string\"||s.requestId!==n.id){d(r,w(n.id,\"invalid_response\",\"The Thread Page bridge returned an invalid confirmation\"));return}let f=n.method===\"navigation.openExternal\";if(!await o.confirm(s.summary,f?()=>l.reserveWindow():void 0)){d(r,w(n.id,\"cancelled\",\"You declined this action\"));return}let a=await p({actionToken:t.actionToken,request:n,confirmation:s.challenge});y(r,n,a);return}y(r,n,i)}catch(i){l.release(),d(r,w(n.id,\"unavailable\",i instanceof Error?i.message:\"The Thread Page bridge is unavailable\"))}}async function v(r){let n=r.file;if(!n||typeof n.size!=\"number\")throw new Error(\"Attachment is not a file\");let i=n.name||\"file\";if(n.size<=0)throw new Error(`Attachment ${i} is empty`);if(n.size>t.maxUploadBytes)throw new Error(`Attachment ${i} is larger than ${Math.round(t.maxUploadBytes/(1024*1024))} MiB`);let s=await V(n),f=await c(t.uploadUrl,{method:\"POST\",credentials:\"same-origin\",cache:\"no-store\",headers:{\"content-type\":\"application/json\"},body:JSON.stringify({actionToken:t.actionToken,pageRevision:t.pageRevision,name:i,content:s})}),u=await f.json().catch(()=>null);if(!f.ok||!u||u.ok!==!0||typeof u.name!=\"string\"||typeof u.path!=\"string\"||typeof u.sizeBytes!=\"number\")throw new Error(u&&typeof u.message==\"string\"&&u.message||`Upload failed (${f.status})`);return{field:String(r.field||\"file\").slice(0,128),name:u.name,path:u.path,sizeBytes:u.sizeBytes}}async function k(r,n){let i=typeof n.submissionId==\"string\"?n.submissionId:\"\";try{let s=(Array.isArray(n.files)?n.files:[]).slice(0,t.maxUploads),f=[];for(let S=0;S<s.length;S+=1)d(r,{kind:\"thread-page:submit-progress\",submissionId:i,message:`Uploading ${S+1} of ${s.length}\\u2026`}),f.push(await v(s[S]));f.length>0&&d(r,{kind:\"thread-page:submit-progress\",submissionId:i,message:\"Sending\\u2026\"});let u=await c(t.submitUrl,{method:\"POST\",credentials:\"same-origin\",cache:\"no-store\",headers:{\"content-type\":\"application/json\"},body:JSON.stringify({actionToken:t.actionToken,submissionId:i,pageRevision:t.pageRevision,title:n.title,answers:n.answers,files:f})}),a=await u.json().catch(()=>({ok:!1,message:\"Invalid server response\"})),h=u.ok&&a.ok===!0;d(r,{kind:\"thread-page:submit-result\",submissionId:i,ok:h,message:typeof a.delivery==\"string\"?`Sent (${a.delivery})`:\"Sent\",error:typeof a.message==\"string\"?a.message:`Request failed (${u.status})`})}catch(s){d(r,{kind:\"thread-page:submit-result\",submissionId:i,ok:!1,error:s instanceof Error?s.message:\"Request failed\"})}}return{handle(r,n){if(b(n)){if(n.kind===\"thread-page:dirty\"){e.onDirty(!0);return}if(n.kind===\"thread-page:clean\"){e.onDirty(!1);return}if(n.kind===\"thread-page:submit\"){k(r,n);return}if(!T(n,t.pageRevision)){d(r,w(n.id,\"invalid_request\",\"Invalid Thread Page bridge request\"));return}m(r,n)}}}}async function V(e){let t=new Uint8Array(await e.arrayBuffer()),o=\"\",l=32768;for(let c=0;c<t.length;c+=l)o+=String.fromCharCode.apply(null,Array.from(t.subarray(c,c+l)));return btoa(o)}function L(e,t,o,l){let{frame:c,status:d,work:p,reload:g,dialog:y}=o,m=null,v=!0,k=t.stale,n=A(e,t,{setStatus(a,h){d.textContent=a,d.dataset.tone=h?\"warn\":\"\"},setWorking(a){p.dataset.visible=a&&t.workingLabel?\"true\":\"false\"},showReload(a){g.dataset.visible=a?\"true\":\"false\"},onStaleChanged(a){k=a,m?.postMessage({kind:\"thread-page:source-state\",stale:a})},reloadView(){e.location.reload()}},l),i=O(e),s=D(y),f=I({config:t,confirmer:s,navigator:i,onDirty:a=>n.setDirty(a),...l?{fetchImpl:l}:{}});function u(){let a=new e.MessageChannel,h=a.port1;m=h,h.onmessage=S=>f.handle(h,S.data),h.start?.(),c.contentWindow?.postMessage({kind:\"thread-page:connect\",version:E},\"*\",[a.port2]),h.postMessage({kind:\"thread-page:source-state\",stale:k})}return e.addEventListener(\"message\",a=>{if(!v||a.origin!==\"null\"||a.source!==c.contentWindow)return;let h=a.data;!b(h)||h.kind!==\"thread-page:ready\"||h.version!==E||(v=!1,u())}),g.addEventListener(\"click\",()=>e.location.reload()),c.src=t.documentUrl,n.start(),{poller:n}}var W=P(document.currentScript),q=document.querySelector(\"iframe\"),N=document.querySelector(\"[data-shell-status]\"),U=document.querySelector(\"[data-shell-working]\"),H=document.querySelector(\"[data-shell-reload]\"),z=document.querySelector(\"dialog\");if(!q||!N||!U||!H||!z)throw new Error(\"Thread Page shell: chrome is incomplete\");L(window,W,{frame:q,status:N,work:U,reload:H,dialog:z});})();";
3
+ export const SHELL_RUNTIME = "\"use strict\";(()=>{var M=Object.freeze({entryDocumentBytes:5242880,uploadFileBytes:25165824,uploadsPerForm:8,submissionBodyBytes:65536,answersPerSubmission:64,answerValueChars:8e3,answerListItems:64,capabilityPayloadBytes:65536,capabilityJsonDepth:16,capabilityJsonNodes:1e4,promptChars:32768,resultTextBytes:65536,titleChars:240,storageValueBytes:32768,storageKeyChars:128,snapshotDefault:100,snapshotMax:200,activityDefault:8,activityMax:20,actionTokenMs:72e5,confirmationMs:12e4,selectionTokenMs:6e5,selectionTokens:32,idempotencyRecords:512,idempotencyMs:3e5,ratePerMinute:120,rateConcurrent:8,shellPollMs:1e4,watchDefaultMs:8e3,watchMinMs:2e3,watchMaxMs:3e5,inlineFileBytes:2097152,inlineTotalBytes:3145728,inlineCssDepth:3,offlineCopyBytes:204800,offlineCacheEntries:32,offlineCacheBytes:8388608,requestIdChars:96,methodNameChars:96,tokenChars:4096,errorMessageChars:512,summaryChars:512,projectsMax:200,providersMax:64,modelsPerProvider:64});var x=[\"invalid_json\",\"invalid_request\",\"invalid_params\",\"invalid_response\",\"request_too_large\",\"response_too_large\",\"unsupported_version\",\"unknown_method\",\"stale_page\",\"confirmation_required\",\"confirmation_invalid\",\"cancelled\",\"not_found\",\"conflict\",\"unavailable\",\"rate_limited\",\"handler_error\",\"invalid_result\"],Y=new Set(x);var Q=Object.freeze({noPage:\"This session has no page yet. Run `bb thread-page init` in the session first.\",ineligible:\"Only visible root sessions have pages.\",pageTooLarge:`The page's entry document is larger than ${M.entryDocumentBytes/(1024*1024)} MiB and was not served.`,unavailable:\"The page's source is unreachable. Reconnect its host and try again.\",staleCopy:\"The source host is offline; this cached page is read-only.\",stalePage:\"This page changed; reload it before responding.\",handler:\"Could not execute the page action.\",rateLimited:\"Too many requests from this page; try again shortly.\",invalidSession:\"A valid session id is required.\",tokenInvalid:\"This page session is invalid or expired; reload the page.\"});var E=1,_=1,$=new Set(x),j=/^[A-Za-z0-9][A-Za-z0-9._:-]{0,95}$/,F=/^[a-z][a-zA-Z0-9]*(?:\\.[a-z][a-zA-Z0-9]*)+$/;function b(e){return typeof e==\"object\"&&e!==null&&!Array.isArray(e)}function R(e,t){return Object.keys(e).length===t.length&&t.every(l=>Object.prototype.hasOwnProperty.call(e,l))}function C(e){return typeof e==\"string\"&&j.test(e)}function T(e,t){return b(e)&&R(e,[\"v\",\"id\",\"method\",\"params\",\"pageRevision\"])&&e.v===_&&C(e.id)&&typeof e.method==\"string\"&&e.method.length>=3&&e.method.length<=96&&F.test(e.method)&&e.pageRevision===t}function B(e,t){if(!b(e)||e.v!==_||typeof e.id!=\"string\"||typeof e.ok!=\"boolean\"||t!==void 0&&e.id!==t)return!1;if(e.ok===!0)return R(e,[\"v\",\"id\",\"ok\",\"result\"]);if(!R(e,[\"v\",\"id\",\"ok\",\"error\"])||!b(e.error))return!1;let o=e.error;return R(o,[\"code\",\"message\"])&&typeof o.code==\"string\"&&$.has(o.code)&&typeof o.message==\"string\"&&o.message.length>0&&o.message.length<=512}function w(e,t,o){return{v:1,id:C(e)?e:\"invalid\",ok:!1,error:{code:t,message:o.slice(0,512)||\"Request failed\"}}}function P(e){let t=e?.getAttribute(\"data-config\");if(!t)throw new Error(\"Thread Page runtime: configuration is missing\");return JSON.parse(t)}function D(e){let t=e.querySelector(\"p\"),o=e.querySelector('button[value=\"cancel\"]'),l=e.querySelector('button[value=\"confirm\"]'),c=null,d;function p(g){let y=c;if(c=null,g&&d)try{d()}catch{}d=void 0,e.open&&e.close(),y?.(g)}return o?.addEventListener(\"click\",g=>{g.preventDefault(),p(!1)}),l?.addEventListener(\"click\",g=>{g.preventDefault(),p(!0)}),e.addEventListener(\"cancel\",g=>{g.preventDefault(),p(!1)}),e.addEventListener(\"close\",()=>{c&&p(!1)}),{confirm(g,y){return new Promise(m=>{if(c&&p(!1),t&&(t.textContent=g),c=m,d=y,typeof e.showModal==\"function\")try{e.showModal()}catch{p(!1)}else p(!1)})}}}function O(e){let t=null;return{inPlace(o){e.location.assign(o)},reserveWindow(){try{if(t=e.open(\"\",\"_blank\"),t)try{t.opener=null}catch{}}catch{t=null}},external(o){let l=t;if(t=null,l&&!l.closed)try{l.location.href=o;return}catch{try{l.close()}catch{}}e.location.assign(o)},release(){let o=t;t=null;try{o?.close()}catch{}}}}function A(e,t,o,l=e.fetch.bind(e)){let c=`\"${t.pageRevision}\"`,d=!1,p=!1,g=!1,y=t.stale,m=null,v=null;function k(i){m!==null&&clearTimeout(m),m=null,!(p||e.document.visibilityState!==\"visible\")&&(m=setTimeout(()=>{m=null,s()},i))}function r(){m!==null&&clearTimeout(m),m=null,v?.abort(),v=null}function n(){d?(o.setStatus(\"Page changed \\u2014 reload when ready\",!0),o.showReload(!0)):o.reloadView()}async function s(){if(!(p||g||e.document.visibilityState!==\"visible\")){if(Date.now()>=t.expiresAt-3e4){p=!0,d?(o.setStatus(\"Session expiring \\u2014 reload when ready\",!0),o.showReload(!0)):o.reloadView();return}g=!0,v=new AbortController;try{let i=await l(t.documentUrl,{method:\"GET\",credentials:\"same-origin\",cache:\"no-store\",headers:{\"if-none-match\":c},signal:v.signal});if(i.status===401||i.status===403){p=!0,o.setStatus(\"Session expired \\u2014 reload this page\",!0),o.showReload(!0);return}if(!i.ok&&i.status!==304){o.setStatus(\"Page unavailable\",!0);return}let f=i.headers.get(\"x-thread-page-stale\")===\"true\";o.setWorking(i.headers.get(\"x-thread-page-activity\")===\"working\"),f!==y&&(y=f,o.onStaleChanged(f)),o.setStatus(f?\"Offline copy \\u2014 read-only\":\"\",f);let u=i.headers.get(\"etag\");u&&u!==c&&(c=u,n())}catch(i){i instanceof DOMException&&i.name===\"AbortError\"||o.setStatus(\"Cannot check for updates\",!0)}finally{v=null,g=!1,k(t.pollMs)}}}return e.document.addEventListener(\"visibilitychange\",()=>{e.document.visibilityState===\"visible\"?k(0):r()}),{start:()=>k(t.pollMs),setDirty:i=>{d=i},pollNow:()=>s(),isStopped:()=>p}}function I(e){let{config:t,confirmer:o,navigator:l}=e,c=e.fetchImpl??fetch;function d(r,n){r.postMessage(n)}async function p(r){return(await c(t.bridgeUrl,{method:\"POST\",credentials:\"same-origin\",cache:\"no-store\",headers:{\"content-type\":\"application/json\"},body:JSON.stringify(r)})).json().catch(()=>null)}function g(r){return!b(r)||r.kind!==\"page\"&&r.kind!==\"host\"&&r.kind!==\"external\"||typeof r.url!=\"string\"||r.kind===\"external\"&&!/^https?:\\/\\//i.test(r.url)||r.kind!==\"external\"&&!r.url.startsWith(\"/\")?null:{kind:r.kind,url:r.url}}function y(r,n,s){if(!b(s)||!B(s.response,n.id)){d(r,w(n.id,\"invalid_response\",\"The Thread Page bridge returned an invalid response\"));return}let i=s.navigate===void 0?null:g(s.navigate);if(s.response.ok&&i){d(r,s.response),i.kind===\"external\"?l.external(i.url):l.inPlace(i.url);return}l.release(),d(r,s.response)}async function m(r,n){try{let s=await p({actionToken:t.actionToken,request:n});if(b(s)&&b(s.confirm)){let i=s.confirm;if(typeof i.challenge!=\"string\"||typeof i.summary!=\"string\"||i.requestId!==n.id){d(r,w(n.id,\"invalid_response\",\"The Thread Page bridge returned an invalid confirmation\"));return}let f=n.method===\"navigation.openExternal\";if(!await o.confirm(i.summary,f?()=>l.reserveWindow():void 0)){d(r,w(n.id,\"cancelled\",\"You declined this action\"));return}let a=await p({actionToken:t.actionToken,request:n,confirmation:i.challenge});y(r,n,a);return}y(r,n,s)}catch(s){l.release(),d(r,w(n.id,\"unavailable\",s instanceof Error?s.message:\"The Thread Page bridge is unavailable\"))}}async function v(r){let n=r.file;if(!n||typeof n.size!=\"number\")throw new Error(\"Attachment is not a file\");let s=n.name||\"file\";if(n.size<=0)throw new Error(`Attachment ${s} is empty`);if(n.size>t.maxUploadBytes)throw new Error(`Attachment ${s} is larger than ${Math.round(t.maxUploadBytes/(1024*1024))} MiB`);let i=await V(n),f=await c(t.uploadUrl,{method:\"POST\",credentials:\"same-origin\",cache:\"no-store\",headers:{\"content-type\":\"application/json\"},body:JSON.stringify({actionToken:t.actionToken,pageRevision:t.pageRevision,name:s,content:i})}),u=await f.json().catch(()=>null);if(!f.ok||!u||u.ok!==!0||typeof u.name!=\"string\"||typeof u.path!=\"string\"||typeof u.sizeBytes!=\"number\")throw new Error(u&&typeof u.message==\"string\"&&u.message||`Upload failed (${f.status})`);return{field:String(r.field||\"file\").slice(0,128),name:u.name,path:u.path,sizeBytes:u.sizeBytes}}async function k(r,n){let s=typeof n.submissionId==\"string\"?n.submissionId:\"\";try{let i=(Array.isArray(n.files)?n.files:[]).slice(0,t.maxUploads),f=[];for(let S=0;S<i.length;S+=1)d(r,{kind:\"thread-page:submit-progress\",submissionId:s,message:`Uploading ${S+1} of ${i.length}\\u2026`}),f.push(await v(i[S]));f.length>0&&d(r,{kind:\"thread-page:submit-progress\",submissionId:s,message:\"Sending\\u2026\"});let u=await c(t.submitUrl,{method:\"POST\",credentials:\"same-origin\",cache:\"no-store\",headers:{\"content-type\":\"application/json\"},body:JSON.stringify({actionToken:t.actionToken,submissionId:s,pageRevision:t.pageRevision,title:n.title,answers:n.answers,files:f})}),a=await u.json().catch(()=>({ok:!1,message:\"Invalid server response\"})),h=u.ok&&a.ok===!0;d(r,{kind:\"thread-page:submit-result\",submissionId:s,ok:h,message:typeof a.delivery==\"string\"?`Sent (${a.delivery})`:\"Sent\",error:typeof a.message==\"string\"?a.message:`Request failed (${u.status})`})}catch(i){d(r,{kind:\"thread-page:submit-result\",submissionId:s,ok:!1,error:i instanceof Error?i.message:\"Request failed\"})}}return{handle(r,n){if(b(n)){if(n.kind===\"thread-page:dirty\"){e.onDirty(!0);return}if(n.kind===\"thread-page:clean\"){e.onDirty(!1);return}if(n.kind===\"thread-page:submit\"){k(r,n);return}if(!T(n,t.pageRevision)){d(r,w(n.id,\"invalid_request\",\"Invalid Thread Page bridge request\"));return}m(r,n)}}}}async function V(e){let t=new Uint8Array(await e.arrayBuffer()),o=\"\",l=32768;for(let c=0;c<t.length;c+=l)o+=String.fromCharCode.apply(null,Array.from(t.subarray(c,c+l)));return btoa(o)}function L(e,t,o,l){let{frame:c,status:d,work:p,reload:g,dialog:y}=o,m=null,v=!0,k=t.stale,n=A(e,t,{setStatus(a,h){d.textContent=a,d.dataset.tone=h?\"warn\":\"\"},setWorking(a){p.dataset.visible=a&&t.workingLabel?\"true\":\"false\"},showReload(a){g.dataset.visible=a?\"true\":\"false\"},onStaleChanged(a){k=a,m?.postMessage({kind:\"thread-page:source-state\",stale:a})},reloadView(){e.location.reload()}},l),s=O(e),i=D(y),f=I({config:t,confirmer:i,navigator:s,onDirty:a=>n.setDirty(a),...l?{fetchImpl:l}:{}});function u(){let a=new e.MessageChannel,h=a.port1;m=h,h.onmessage=S=>f.handle(h,S.data),h.start?.(),c.contentWindow?.postMessage({kind:\"thread-page:connect\",version:E},\"*\",[a.port2]),h.postMessage({kind:\"thread-page:source-state\",stale:k})}return e.addEventListener(\"message\",a=>{if(!v||a.origin!==\"null\"||a.source!==c.contentWindow)return;let h=a.data;!b(h)||h.kind!==\"thread-page:ready\"||h.version!==E||(v=!1,u())}),g.addEventListener(\"click\",()=>e.location.reload()),c.src=t.documentUrl,n.start(),{poller:n}}var W=P(document.currentScript),q=document.querySelector(\"iframe\"),N=document.querySelector(\"[data-shell-status]\"),U=document.querySelector(\"[data-shell-working]\"),H=document.querySelector(\"[data-shell-reload]\"),z=document.querySelector(\"dialog\");if(!q||!N||!U||!H||!z)throw new Error(\"Thread Page shell: chrome is incomplete\");L(window,W,{frame:q,status:N,work:U,reload:H,dialog:z});})();";
@@ -0,0 +1,277 @@
1
+ import { defaultTreeAdapter, parse as parseHtml, serialize as serializeHtml, type DefaultTreeAdapterTypes } from "parse5";
2
+ import { LIMITS } from "../domain/limits.ts";
3
+ import { isSafeRelativePath } from "./layout.ts";
4
+
5
+ /**
6
+ * Resolving a page's own files into its entry document. spec R1.2, R1.3, R4.25–R4.27
7
+ *
8
+ * A page frame is sandboxed, so it has an opaque origin, so every subresource
9
+ * it requests is cross-site and carries no host credential. A bb served over
10
+ * loopback needs none and the file arrives; a bb reached through an
11
+ * authenticated origin refuses it. The page renders, its own stylesheet and
12
+ * data do not, nothing errors, and the author cannot see any of this from the
13
+ * machine that wrote the page.
14
+ *
15
+ * So the entry document — the one artifact whose request is issued by trusted
16
+ * chrome and therefore always authorised — carries the page's own files with
17
+ * it. Each relative reference is rewritten to a `data:` URL, which the
18
+ * document's CSP already permits. Rewriting the attribute rather than moving
19
+ * the bytes into the element keeps every other attribute meaningful (`defer`,
20
+ * `type="module"`, `media`, `loading`) and avoids the escaping traps that
21
+ * inlining raw text into `<script>` and `<style>` carries.
22
+ *
23
+ * THIS IS A WORKAROUND AND IT SHOULD BE DELETED. It exists only because the
24
+ * host cannot authorise a sandboxed document's own subresource requests. Once
25
+ * it can — see docs/B1-OWN-FILES.md — a page's files should be served as
26
+ * files again, and this module and its wiring should go.
27
+ */
28
+
29
+ type HtmlElement = DefaultTreeAdapterTypes.Element;
30
+ type HtmlNode = DefaultTreeAdapterTypes.ChildNode;
31
+
32
+ export interface OwnFile {
33
+ readonly bytes: Uint8Array;
34
+ readonly mimeType?: string | undefined;
35
+ }
36
+
37
+ export type OwnFileReader = (relativePath: string) => Promise<OwnFile | null>;
38
+
39
+ export interface ResolvedFile {
40
+ readonly path: string;
41
+ readonly bytes: number;
42
+ }
43
+
44
+ export interface SkippedFile {
45
+ readonly path: string;
46
+ readonly reason: "missing" | "too-large" | "budget" | "unsafe-path";
47
+ }
48
+
49
+ export interface ResolveOutcome {
50
+ readonly html: string;
51
+ readonly resolved: readonly ResolvedFile[];
52
+ readonly skipped: readonly SkippedFile[];
53
+ }
54
+
55
+ /** Attributes that may name one of the page's own files, per element. */
56
+ const CARRIERS: ReadonlyArray<{ tag: string; attr: string; test?: (element: HtmlElement) => boolean }> = [
57
+ { tag: "link", attr: "href", test: (element) => relOf(element).some((rel) => rel === "stylesheet" || rel === "icon" || rel === "shortcut" || rel === "apple-touch-icon" || rel === "preload") },
58
+ { tag: "script", attr: "src" },
59
+ { tag: "img", attr: "src" },
60
+ { tag: "source", attr: "src" },
61
+ { tag: "audio", attr: "src" },
62
+ { tag: "video", attr: "src" },
63
+ { tag: "video", attr: "poster" },
64
+ { tag: "track", attr: "src" },
65
+ ];
66
+
67
+ function relOf(element: HtmlElement): string[] {
68
+ const rel = attributeOf(element, "rel") ?? "";
69
+ return rel.toLowerCase().split(/\s+/).filter(Boolean);
70
+ }
71
+
72
+ function attributeOf(element: HtmlElement, name: string): string | null {
73
+ return element.attrs.find((attr) => attr.name === name)?.value ?? null;
74
+ }
75
+
76
+ function setAttribute(element: HtmlElement, name: string, value: string): void {
77
+ const existing = element.attrs.find((attr) => attr.name === name);
78
+ if (existing) existing.value = value;
79
+ else element.attrs.push({ name, value });
80
+ }
81
+
82
+ /**
83
+ * True for a reference that names a file inside this page's own root. Anything
84
+ * absolute, protocol-relative, already a URL, or a bare fragment belongs to
85
+ * somebody else and is left exactly as the author wrote it.
86
+ */
87
+ export function isOwnFileReference(value: string): boolean {
88
+ const trimmed = value.trim();
89
+ if (trimmed.length === 0) return false;
90
+ if (trimmed.startsWith("#") || trimmed.startsWith("/") || trimmed.startsWith("//")) return false;
91
+ if (/^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(trimmed)) return false;
92
+ return true;
93
+ }
94
+
95
+ /** The reference's path, with any query or fragment removed and percent-escapes resolved. */
96
+ function pathOf(reference: string): string | null {
97
+ const withoutHash = reference.trim().split("#")[0] ?? "";
98
+ const withoutQuery = withoutHash.split("?")[0] ?? "";
99
+ if (withoutQuery.length === 0) return null;
100
+ try {
101
+ return decodeURIComponent(withoutQuery);
102
+ } catch {
103
+ return withoutQuery;
104
+ }
105
+ }
106
+
107
+ function mimeFor(path: string, declared: string | undefined): string {
108
+ if (declared && declared.length > 0) return declared;
109
+ const extension = path.slice(path.lastIndexOf(".") + 1).toLowerCase();
110
+ return EXTENSION_TYPES[extension] ?? "application/octet-stream";
111
+ }
112
+
113
+ const EXTENSION_TYPES: Readonly<Record<string, string>> = Object.freeze({
114
+ css: "text/css",
115
+ js: "text/javascript",
116
+ mjs: "text/javascript",
117
+ json: "application/json",
118
+ svg: "image/svg+xml",
119
+ png: "image/png",
120
+ jpg: "image/jpeg",
121
+ jpeg: "image/jpeg",
122
+ gif: "image/gif",
123
+ webp: "image/webp",
124
+ avif: "image/avif",
125
+ ico: "image/x-icon",
126
+ woff: "font/woff",
127
+ woff2: "font/woff2",
128
+ ttf: "font/ttf",
129
+ otf: "font/otf",
130
+ mp4: "video/mp4",
131
+ webm: "video/webm",
132
+ mp3: "audio/mpeg",
133
+ wav: "audio/wav",
134
+ vtt: "text/vtt",
135
+ });
136
+
137
+ function dataUrl(bytes: Uint8Array, mimeType: string): string {
138
+ return `data:${mimeType};base64,${Buffer.from(bytes).toString("base64")}`;
139
+ }
140
+
141
+ /** `url(...)` references inside an inlined stylesheet, which would otherwise resolve nowhere. */
142
+ const CSS_URL = /url\(\s*(['"]?)([^'")]+)\1\s*\)/g;
143
+
144
+ /**
145
+ * Rewrites every reference to one of the page's own files into a `data:` URL,
146
+ * one level deep inside stylesheets as well. Anything that cannot be resolved
147
+ * is left untouched and reported, so the page degrades to exactly the
148
+ * behaviour it has without this pass rather than to a broken document.
149
+ */
150
+ export async function resolveOwnFiles(html: string, read: OwnFileReader): Promise<ResolveOutcome> {
151
+ const document = parseHtml(html);
152
+ const resolved: ResolvedFile[] = [];
153
+ const skipped: SkippedFile[] = [];
154
+ const seen = new Map<string, string | null>();
155
+ let budget = LIMITS.inlineTotalBytes;
156
+
157
+ async function urlFor(path: string, depth: number): Promise<string | null> {
158
+ const memo = seen.get(path);
159
+ if (memo !== undefined) return memo;
160
+ const answer = await load(path, depth);
161
+ seen.set(path, answer);
162
+ return answer;
163
+ }
164
+
165
+ async function load(path: string, depth: number): Promise<string | null> {
166
+ if (!isSafeRelativePath(path)) {
167
+ skipped.push({ path, reason: "unsafe-path" });
168
+ return null;
169
+ }
170
+ const file = await read(path).catch(() => null);
171
+ if (!file) {
172
+ skipped.push({ path, reason: "missing" });
173
+ return null;
174
+ }
175
+ if (file.bytes.byteLength > LIMITS.inlineFileBytes) {
176
+ skipped.push({ path, reason: "too-large" });
177
+ return null;
178
+ }
179
+ if (file.bytes.byteLength > budget) {
180
+ skipped.push({ path, reason: "budget" });
181
+ return null;
182
+ }
183
+ budget -= file.bytes.byteLength;
184
+ const mimeType = mimeFor(path, file.mimeType);
185
+ const bytes = mimeType === "text/css" && depth < LIMITS.inlineCssDepth ? Buffer.from(await resolveCss(Buffer.from(file.bytes).toString("utf8"), path, depth), "utf8") : file.bytes;
186
+ resolved.push({ path, bytes: file.bytes.byteLength });
187
+ return dataUrl(bytes, mimeType);
188
+ }
189
+
190
+ /** A stylesheet becomes a `data:` URL, so its own relative `url()`s must be resolved first. */
191
+ async function resolveCss(css: string, from: string, depth: number): Promise<string> {
192
+ const base = from.includes("/") ? from.slice(0, from.lastIndexOf("/") + 1) : "";
193
+ const replacements = new Map<string, string>();
194
+ for (const match of css.matchAll(CSS_URL)) {
195
+ const reference = match[2] ?? "";
196
+ if (!isOwnFileReference(reference) || replacements.has(reference)) continue;
197
+ const path = pathOf(reference);
198
+ if (!path) continue;
199
+ const url = await urlFor(normalise(base + path), depth + 1);
200
+ if (url) replacements.set(reference, url);
201
+ }
202
+ if (replacements.size === 0) return css;
203
+ return css.replace(CSS_URL, (whole, quote: string, reference: string) => {
204
+ const url = replacements.get(reference);
205
+ return url ? `url(${quote}${url}${quote})` : whole;
206
+ });
207
+ }
208
+
209
+ const elements: HtmlElement[] = [];
210
+ const walk = (node: HtmlNode | DefaultTreeAdapterTypes.Document): void => {
211
+ if (defaultTreeAdapter.isElementNode(node as HtmlNode)) elements.push(node as HtmlElement);
212
+ for (const child of (node as { childNodes?: HtmlNode[] }).childNodes ?? []) walk(child);
213
+ };
214
+ walk(document);
215
+
216
+ let changed = false;
217
+ for (const element of elements) {
218
+ for (const carrier of CARRIERS) {
219
+ if (element.tagName !== carrier.tag) continue;
220
+ if (carrier.test && !carrier.test(element)) continue;
221
+ const reference = attributeOf(element, carrier.attr);
222
+ if (reference === null || !isOwnFileReference(reference)) continue;
223
+ const path = pathOf(reference);
224
+ if (!path) continue;
225
+ const url = await urlFor(normalise(path), 0);
226
+ if (!url) continue;
227
+ setAttribute(element, carrier.attr, url);
228
+ changed = true;
229
+ }
230
+ // srcset carries several candidates; all or nothing keeps the parsing simple.
231
+ if (element.tagName === "img" || element.tagName === "source") {
232
+ const srcset = attributeOf(element, "srcset");
233
+ if (srcset !== null) {
234
+ const rewritten = await resolveSrcset(srcset, urlFor);
235
+ if (rewritten !== null) {
236
+ setAttribute(element, "srcset", rewritten);
237
+ changed = true;
238
+ }
239
+ }
240
+ }
241
+ }
242
+
243
+ return { html: changed ? serializeHtml(document) : html, resolved, skipped };
244
+ }
245
+
246
+ async function resolveSrcset(srcset: string, urlFor: (path: string, depth: number) => Promise<string | null>): Promise<string | null> {
247
+ const candidates = srcset.split(",").map((entry) => entry.trim()).filter(Boolean);
248
+ const rewritten: string[] = [];
249
+ let changed = false;
250
+ for (const candidate of candidates) {
251
+ const [reference, ...descriptor] = candidate.split(/\s+/);
252
+ if (!reference || !isOwnFileReference(reference)) {
253
+ rewritten.push(candidate);
254
+ continue;
255
+ }
256
+ const path = pathOf(reference);
257
+ const url = path ? await urlFor(normalise(path), 0) : null;
258
+ if (!url) {
259
+ rewritten.push(candidate);
260
+ continue;
261
+ }
262
+ changed = true;
263
+ rewritten.push([url, ...descriptor].join(" "));
264
+ }
265
+ return changed ? rewritten.join(", ") : null;
266
+ }
267
+
268
+ /** Collapses `a/./b` and `a/b/../c`; a path that climbs out fails `isSafeRelativePath` later. */
269
+ function normalise(path: string): string {
270
+ const out: string[] = [];
271
+ for (const segment of path.split("/")) {
272
+ if (segment === "" || segment === ".") continue;
273
+ if (segment === ".." && out.length > 0 && out[out.length - 1] !== "..") out.pop();
274
+ else out.push(segment);
275
+ }
276
+ return out.join("/");
277
+ }
@@ -3,6 +3,7 @@ import { isRevision } from "../domain/ids.ts";
3
3
  import { LIMITS } from "../domain/limits.ts";
4
4
  import { revisionOf } from "../domain/revision.ts";
5
5
  import type { SessionHost } from "../host/contract.ts";
6
+ import type { ResolveOutcome } from "./inline.ts";
6
7
  import { ENTRY_FILE } from "./layout.ts";
7
8
 
8
9
  /**
@@ -16,6 +17,8 @@ export interface LoadedPage {
16
17
  readonly updatedAtMs: number;
17
18
  /** True when served from the offline copy. */
18
19
  readonly stale: boolean;
20
+ /** Own files carried into the document, and those that could not be. */
21
+ readonly site: { readonly resolved: number; readonly skipped: readonly { path: string; reason: string }[] };
19
22
  }
20
23
 
21
24
  interface CachedPage {
@@ -24,6 +27,13 @@ interface CachedPage {
24
27
  readonly updatedAtMs: number;
25
28
  }
26
29
 
30
+ /**
31
+ * Resolves a page's own files into its entry document so it renders on an
32
+ * origin that will not authorise the sandbox's subresource requests.
33
+ * Temporary; see pages/inline.ts. spec R1.2, R4.25-R4.27
34
+ */
35
+ export type PageResolver = (session: string, html: string) => Promise<ResolveOutcome>;
36
+
27
37
  export interface PageStore {
28
38
  load(session: string): Promise<LoadedPage>;
29
39
  /** Records a document the plugin just wrote, so the next load is warm. */
@@ -34,7 +44,7 @@ export interface PageStore {
34
44
 
35
45
  const KV_PREFIX = "cache:";
36
46
 
37
- export function createPageStore(host: SessionHost): PageStore {
47
+ export function createPageStore(host: SessionHost, resolve?: PageResolver): PageStore {
38
48
  const memory = new Map<string, CachedPage>();
39
49
  let memoryBytes = 0;
40
50
 
@@ -62,7 +72,15 @@ export function createPageStore(host: SessionHost): PageStore {
62
72
  async function persist(session: string, page: CachedPage, previousRevision: string | undefined): Promise<void> {
63
73
  if (previousRevision === page.revision) return;
64
74
  const key = KV_PREFIX + session;
65
- if (Buffer.byteLength(page.html, "utf8") > LIMITS.offlineCopyBytes) {
75
+ const bytes = Buffer.byteLength(page.html, "utf8");
76
+ if (bytes > LIMITS.offlineCopyBytes) {
77
+ // Silently dropping this is how a page stops opening offline with no
78
+ // author ever learning why. Carrying a page's own files into the
79
+ // document makes it much easier to cross. spec R2.27-R2.31
80
+ host.log.warn(
81
+ `offline copy: ${session} is ${Math.round(bytes / 1024)} KiB, over the ${LIMITS.offlineCopyBytes / 1024} KiB limit — ` +
82
+ "the page will not open while its host is unreachable",
83
+ );
66
84
  await host.kv.delete(key).catch((error: unknown) => host.log.warn(`offline copy: could not clear ${session}: ${errorText(error)}`));
67
85
  return;
68
86
  }
@@ -101,19 +119,35 @@ export function createPageStore(host: SessionHost): PageStore {
101
119
  content = await host.files.read(location, ENTRY_FILE);
102
120
  } catch (error) {
103
121
  const fallback = await cached(session);
104
- if (fallback) return { ...fallback, stale: true };
122
+ if (fallback) return { ...fallback, stale: true, site: { resolved: 0, skipped: [] } };
105
123
  throw PageError.is(error) ? error : new PageError("unavailable", PUBLIC_MESSAGES.unavailable, { cause: error });
106
124
  }
107
125
  if (!content) throw new PageError("no_page", PUBLIC_MESSAGES.noPage);
108
126
  if (content.bytes.byteLength > LIMITS.entryDocumentBytes) {
109
127
  throw new PageError("page_too_large", PUBLIC_MESSAGES.pageTooLarge);
110
128
  }
111
- const html = Buffer.from(content.bytes).toString("utf8");
112
- const page: CachedPage = { html, revision: revisionOf(content.bytes), updatedAtMs: content.modifiedAtMs ?? Date.now() };
129
+ const authored = Buffer.from(content.bytes).toString("utf8");
130
+ let html = authored;
131
+ let site: LoadedPage["site"] = { resolved: 0, skipped: [] };
132
+ if (resolve) {
133
+ try {
134
+ const outcome = await resolve(session, authored);
135
+ html = outcome.html;
136
+ site = { resolved: outcome.resolved.length, skipped: outcome.skipped };
137
+ for (const file of outcome.skipped) {
138
+ host.log.warn(`page ${session}: ${file.path} is referenced but was not carried into the document (${file.reason})`);
139
+ }
140
+ } catch (error) {
141
+ // A page that renders without its own files beats a page that does
142
+ // not render. Serve what the agent wrote.
143
+ host.log.warn(`page ${session}: could not resolve its own files: ${errorText(error)}`);
144
+ }
145
+ }
146
+ const page: CachedPage = { html, revision: revisionOf(html), updatedAtMs: content.modifiedAtMs ?? Date.now() };
113
147
  const previous = memory.get(session)?.revision;
114
148
  retain(session, page);
115
149
  await persist(session, page, previous);
116
- return { ...page, stale: false };
150
+ return { ...page, stale: false, site };
117
151
  },
118
152
  remember,
119
153
  knownRevision(session) {
package/src/plugin.ts CHANGED
@@ -7,6 +7,7 @@ import { capabilityRegistry } from "./domain/capabilities/index.ts";
7
7
  import { createRateLimiter } from "./domain/rate-limit.ts";
8
8
  import { createOutcomeMemory } from "./domain/submissions/idempotency.ts";
9
9
  import type { SessionHost } from "./host/contract.ts";
10
+ import { resolveOwnFiles } from "./pages/inline.ts";
10
11
  import { createPageStore } from "./pages/page-store.ts";
11
12
  import { createCoreStorageSite, type SiteStrategy } from "./pages/site.ts";
12
13
  import { createSelectionStore } from "./serving/bridge/selection-store.ts";
@@ -38,7 +39,16 @@ export async function createPlugin(bb: BbPluginApi, options: PluginOptions = {})
38
39
 
39
40
  const serving: ServingContext = {
40
41
  host,
41
- pages: createPageStore(host),
42
+ // Strategy A cannot serve a sandboxed document's own files on an
43
+ // authenticated origin, so the document carries them. Delete this
44
+ // argument, and pages/inline.ts, once the host can authorise them.
45
+ pages: createPageStore(host, async (session, html) => {
46
+ const location = await host.sessions.storage(session);
47
+ return resolveOwnFiles(html, async (path) => {
48
+ const file = await host.files.read(location, path);
49
+ return file ? { bytes: file.bytes } : null;
50
+ });
51
+ }),
42
52
  settings,
43
53
  signingKey,
44
54
  site,