Files
kandev/apps/backend/internal/plugins/webapp/runtime_bootstrap.go
T
2026-09-11 14:51:32 +00:00

165 lines
4.7 KiB
Go

package webapp
import (
"bytes"
"errors"
"io"
"strings"
"golang.org/x/net/html"
)
var ErrRuntimeBootstrapUnavailable = errors.New("webapp: runtime startup bootstrap unavailable")
const hostRuntimeBootstrap = `(() => {
"use strict";
const VERSION = 1;
const PROBE_TYPE = "kandev.web_app.startup_probe";
const RESULT_TYPE = "kandev.web_app.startup_result";
const MAX_NONCE_BYTES = 128;
let outcome = null;
let pendingNonce = null;
let outcomeSent = false;
const sendOutcome = () => {
if (outcomeSent || !pendingNonce || !outcome || window.parent === window) return;
outcomeSent = true;
const message = {
type: RESULT_TYPE,
version: VERSION,
nonce: pendingNonce,
result: outcome.result,
};
if (outcome.code) message.code = outcome.code;
window.parent.postMessage(message, "*"); // Sandboxed iframes have a null origin, so "*" is the only viable target.
};
const finish = (result, code) => {
if (outcome) return;
outcome = code ? { result, code } : { result };
sendOutcome();
};
const failDocument = () => finish("failed", "document_error");
window.addEventListener("error", (event) => {
if (event.target !== window || event instanceof ErrorEvent) failDocument();
}, true);
window.addEventListener("unhandledrejection", failDocument, true);
window.addEventListener("message", (event) => {
if (event.source !== window.parent || outcomeSent) return;
const data = event.data;
if (!data || typeof data !== "object" || Array.isArray(data)) return;
if (data.type !== PROBE_TYPE || data.version !== VERSION) return;
if (typeof data.nonce !== "string" || data.nonce.length === 0 || data.nonce.length > MAX_NONCE_BYTES) return;
if (pendingNonce) return;
pendingNonce = data.nonce;
sendOutcome();
});
const checkContext = () => {
if (outcome) return;
fetch("./_kandev/v1/context", { credentials: "omit", cache: "no-store" })
.then((response) => {
if (!response.ok) throw new Error("context unavailable");
return response.json();
})
.then(() => finish("ready"))
.catch(() => finish("failed", "context_unavailable"));
};
if (document.readyState === "complete") checkContext();
else window.addEventListener("load", checkContext, { once: true });
})();
`
const hostRuntimeBootstrapTag = `<script src="./_kandev/host-runtime.js"></script>`
func injectRuntimeBootstrap(entry []byte) ([]byte, error) {
if int64(len(entry)) > MaxFileBytes {
return nil, ErrRuntimeBootstrapUnavailable
}
if len(entry) == 0 {
return nil, ErrRuntimeBootstrapUnavailable
}
insertion, err := runtimeBootstrapInsertion(entry)
if err != nil {
return nil, err
}
result := make([]byte, 0, len(entry)+len(hostRuntimeBootstrapTag))
result = append(result, entry[:insertion]...)
result = append(result, hostRuntimeBootstrapTag...)
result = append(result, entry[insertion:]...)
return result, nil
}
func runtimeBootstrapInsertion(entry []byte) (int, error) {
tokenizer := html.NewTokenizer(bytes.NewReader(entry))
tokenizer.SetMaxBuf(len(entry) + 1)
position := 0
insertion := -1
fallback := -1
for {
tokenType := tokenizer.Next()
raw := tokenizer.Raw()
if tokenType == html.ErrorToken {
if errors.Is(tokenizer.Err(), io.EOF) {
break
}
return -1, ErrRuntimeBootstrapUnavailable
}
if len(raw) == 0 {
return -1, ErrRuntimeBootstrapUnavailable
}
insertion, fallback = updateRuntimeBootstrapPosition(tokenizer, tokenType, raw, position, insertion, fallback)
position += len(raw)
}
if position != len(entry) {
return -1, ErrRuntimeBootstrapUnavailable
}
if insertion < 0 {
insertion = fallback
}
if insertion < 0 || insertion > len(entry) {
return -1, ErrRuntimeBootstrapUnavailable
}
return insertion, nil
}
func updateRuntimeBootstrapPosition(tokenizer *html.Tokenizer, tokenType html.TokenType, raw []byte, position, insertion, fallback int) (int, int) {
name, hasTagName := runtimeBootstrapTagName(tokenizer, tokenType)
if !hasTagName {
return insertion, fallback
}
if tokenType == html.StartTagToken {
switch strings.ToLower(name) {
case "script":
if insertion < 0 {
insertion = position
}
case "head":
if fallback < 0 {
fallback = position + len(raw)
}
case "body":
if fallback < 0 {
fallback = position
}
}
return insertion, fallback
}
if fallback < 0 && (strings.EqualFold(name, "head") || strings.EqualFold(name, "body") || strings.EqualFold(name, "html")) {
fallback = position
}
return insertion, fallback
}
func runtimeBootstrapTagName(tokenizer *html.Tokenizer, tokenType html.TokenType) (string, bool) {
if tokenType != html.StartTagToken && tokenType != html.EndTagToken {
return "", false
}
name, _ := tokenizer.TagName()
return string(name), true
}