edge: carlos.js vendored, a revalidating loader and an immutable hashed asset
internal/edge/carlosjs.go: the browser snippet (spec §3.4) vendored as a Go string, never a CDN. carlosJSHash is 12 hex chars of sha256(carlosJS), computed in init; carlosJSAssetPath() is the hashed URL the loader appends. serveCarlosJS answers two GETs: /__carlos/carlos.js, a tiny loader revalidated every five minutes (max-age=300, must-revalidate, ETag, 304 on If-None-Match) that appends a <script> for the hashed asset; and the hashed asset itself, immutable for a year because its URL changes with its content. A stale hash under /__carlos/carlos.*.js 404s. Edge mode: the snippet posts window.onerror and unhandledrejection through the §3.1 sanitisation floor to /__carlos/e, and carlos.track(name, fields) to /__carlos/a via sendBeacon; it never sends a pageview, since the edge already counted the page when it served the HTML. internal/edge/reserved.go: serveReserved now tries serveCarlosJS before its 404 (Task 7 adds the POST dispatch above that). internal/edge/carlosjs_test.go: TestCarlosJSLoaderAndAsset covers the loader's headers and revalidation, the asset's headers and verbatim body, and the stale-hash 404. TestCarlosJSContract pins the snippet's contract as strings (both error handlers, sendBeacon, the two endpoints, window.carlos, track:, the three redaction placeholders, location.pathname) and asserts no "pageview" literal appears. TestCarlosJSNodeSyntax shells out to `node --check` on the snippet body when node is on PATH, skipping otherwise, so the JS is parse-checked without a JS toolchain in the build. Deviation from the brief: the brief's own Step 1 test imports "net/http" but never references it (httptest is used throughout) — dropped that import to compile; verified by diff against the brief that nothing else changed. Also added TestCarlosJSNodeSyntax (not in the brief) as the node syntax check the task asked for, kept as a real test since it was small, self-skipping, and needed no fixture beyond t.TempDir(). node --check confirms the snippet parses: both the ad hoc extraction (sed the const body to a temp file) and the new Go test pass with node v24.19.0 on PATH.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
3 files changed,
+170
−0
internal/edge/carlosjs.go+86 −0internal/edge/carlosjs_test.go+81 −0internal/edge/reserved.go+3 −0
diff --git a/internal/edge/carlosjs.go b/internal/edge/carlosjs.go| new file mode 100644 |
| index 000000000..99ff2c5f9 |
| --- /dev/null |
| +++ b/internal/edge/carlosjs.go |
| @@ -0,0 +1,86 @@ |
| +package edge |
| + |
| +import ( |
| + "crypto/sha256" |
| + "encoding/hex" |
| + "net/http" |
| + "strings" |
| +) |
| + |
| +// carlos.js is vendored here the way the console vendors its icons: in |
| +// code, never a CDN (spec §3.4). The page includes ONE line — |
| +// |
| +// <script src="/__carlos/carlos.js" defer></script> |
| +// |
| +// — the loader, revalidated every five minutes, which appends the hashed |
| +// asset below; the asset is immutable for a year because its URL changes |
| +// with its content. Edge mode: window.onerror and unhandledrejection post |
| +// to /__carlos/e after the §3.1 sanitisation floor; carlos.track(name, |
| +// fields) posts to /__carlos/a via sendBeacon. No pageview is ever sent — |
| +// the edge counted the page when it served the HTML. |
| +const carlosJS = `(function(){ |
| +if(window.carlos&&window.carlos.__v)return; |
| +var E='/__carlos/e',A='/__carlos/a'; |
| +function san(s){s=String(s==null?'':s);if(s.length>8192)s=s.slice(0,8192); |
| +s=s.replace(/[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}/g,'<email>'); |
| +s=s.replace(/\b(bearer|basic)\s+[A-Za-z0-9._~+\/=-]+/gi,'$1 <redacted>'); |
| +s=s.replace(/\b(token|key|secret|password|passwd|pwd|api_key|apikey|access_token|auth)("?\s*[=:]\s*"?)[^\s&"',;]+/gi,'$1$2<redacted>'); |
| +s=s.replace(/\b[A-Fa-f0-9]{32,}\b/g,'<hex>'); |
| +s=s.replace(/\b(?=[A-Za-z0-9+\/]*\d)(?=[A-Za-z0-9+\/]*[A-Z])(?=[A-Za-z0-9+\/]*[a-z])[A-Za-z0-9+\/]{32,}={0,2}/g,'<hex>'); |
| +return s;} |
| +function send(u,b){try{var s=JSON.stringify(b); |
| +if(navigator.sendBeacon){navigator.sendBeacon(u,new Blob([s],{type:'application/json'}));} |
| +else{fetch(u,{method:'POST',body:s,headers:{'Content-Type':'application/json'},keepalive:true,credentials:'omit'});}}catch(e){}} |
| +function fields(f){var o={},n=0;if(f&&typeof f==='object'){for(var k in f){if(!Object.prototype.hasOwnProperty.call(f,k))continue;if(n++>=32)break;var v=f[k];if(v==null)continue; |
| +o[String(k).slice(0,64)]=san(typeof v==='object'?JSON.stringify(v):v).slice(0,256);}}return o;} |
| +function err(kind,name,stack,f){send(E,{kind:String(kind||'Error').slice(0,64),name:san(name).slice(0,1024),stack:san(stack||'').slice(0,8192),path:location.pathname,fields:fields(f)});} |
| +window.addEventListener('error',function(ev){var e=ev.error;err(e&&e.name||'Error',ev.message||(e&&e.message)||'error',e&&e.stack||'',{file:ev.filename||'',line:String(ev.lineno||'')});}); |
| +window.addEventListener('unhandledrejection',function(ev){var r=ev.reason;var m=r&&r.message||(typeof r==='string'?r:'unhandled rejection');err(r&&r.name||'UnhandledRejection',m,r&&r.stack||'',{});}); |
| +window.carlos={__v:1, |
| +track:function(name,f){send(A,{name:String(name||'').slice(0,64),path:location.pathname,fields:fields(f)});}, |
| +error:function(e,f){err(e&&e.name||'Error',e&&e.message||String(e),e&&e.stack||'',f);}}; |
| +})(); |
| +` |
| + |
| +var carlosJSHash string |
| + |
| +func init() { |
| + sum := sha256.Sum256([]byte(carlosJS)) |
| + carlosJSHash = hex.EncodeToString(sum[:])[:12] |
| +} |
| + |
| +func carlosJSAssetPath() string { return ReservedPrefix + "carlos." + carlosJSHash + ".js" } |
| + |
| +// serveCarlosJS answers the loader and the hashed asset. It reports false |
| +// for any other path so serveReserved can 404 it. HEAD is served like GET |
| +// (the stdlib drops the body). |
| +func serveCarlosJS(w http.ResponseWriter, r *http.Request) bool { |
| + if r.Method != http.MethodGet && r.Method != http.MethodHead { |
| + return false |
| + } |
| + switch r.URL.Path { |
| + case ReservedPrefix + "carlos.js": |
| + etag := `"` + carlosJSHash + `"` |
| + w.Header().Set("Content-Type", "text/javascript; charset=utf-8") |
| + w.Header().Set("Cache-Control", "max-age=300, must-revalidate") |
| + w.Header().Set("ETag", etag) |
| + w.Header().Set("X-Content-Type-Options", "nosniff") |
| + if strings.Contains(r.Header.Get("If-None-Match"), carlosJSHash) { |
| + w.WriteHeader(http.StatusNotModified) |
| + return true |
| + } |
| + w.Write([]byte("(function(){var s=document.createElement('script');s.src='" + carlosJSAssetPath() + "';s.async=true;document.head.appendChild(s);})();\n")) |
| + return true |
| + case carlosJSAssetPath(): |
| + w.Header().Set("Content-Type", "text/javascript; charset=utf-8") |
| + w.Header().Set("Cache-Control", "public, max-age=31536000, immutable") |
| + w.Header().Set("X-Content-Type-Options", "nosniff") |
| + w.Write([]byte(carlosJS)) |
| + return true |
| + } |
| + if strings.HasPrefix(r.URL.Path, ReservedPrefix+"carlos.") { |
| + http.NotFound(w, r) // a stale hash from an old loader |
| + return true |
| + } |
| + return false |
| +} |
diff --git a/internal/edge/carlosjs_test.go b/internal/edge/carlosjs_test.go| new file mode 100644 |
| index 000000000..5a13d58de |
| --- /dev/null |
| +++ b/internal/edge/carlosjs_test.go |
| @@ -0,0 +1,81 @@ |
| +package edge |
| + |
| +import ( |
| + "net/http/httptest" |
| + "os" |
| + "os/exec" |
| + "strings" |
| + "testing" |
| +) |
| + |
| +func TestCarlosJSLoaderAndAsset(t *testing.T) { |
| + e := &Edge{} |
| + get := func(p string, hdr map[string]string) *httptest.ResponseRecorder { |
| + rec := httptest.NewRecorder() |
| + req := httptest.NewRequest("GET", "http://app.example.com"+p, nil) |
| + for k, v := range hdr { |
| + req.Header.Set(k, v) |
| + } |
| + e.serveReserved(rec, req, "app.example.com", upstream{}) |
| + return rec |
| + } |
| + rec := get("/__carlos/carlos.js", nil) |
| + if rec.Code != 200 || rec.Header().Get("Cache-Control") != "max-age=300, must-revalidate" { |
| + t.Fatalf("loader: %d %q", rec.Code, rec.Header().Get("Cache-Control")) |
| + } |
| + if !strings.Contains(rec.Body.String(), carlosJSAssetPath()) { |
| + t.Fatal("loader must reference the hashed asset") |
| + } |
| + if ct := rec.Header().Get("Content-Type"); !strings.HasPrefix(ct, "text/javascript") { |
| + t.Fatalf("content-type %q", ct) |
| + } |
| + etag := rec.Header().Get("ETag") |
| + if etag == "" { |
| + t.Fatal("loader carries an ETag so revalidation is a 304") |
| + } |
| + if rec := get("/__carlos/carlos.js", map[string]string{"If-None-Match": etag}); rec.Code != 304 { |
| + t.Fatalf("revalidate: %d", rec.Code) |
| + } |
| + rec = get(carlosJSAssetPath(), nil) |
| + if rec.Code != 200 || rec.Header().Get("Cache-Control") != "public, max-age=31536000, immutable" { |
| + t.Fatalf("asset: %d %q", rec.Code, rec.Header().Get("Cache-Control")) |
| + } |
| + if rec.Body.String() != carlosJS { |
| + t.Fatal("asset body is the snippet verbatim") |
| + } |
| + if rec := get("/__carlos/carlos.deadbeef0000.js", nil); rec.Code != 404 { |
| + t.Fatalf("stale hash: %d", rec.Code) |
| + } |
| + if len(carlosJSHash) != 12 { |
| + t.Fatalf("hash %q", carlosJSHash) |
| + } |
| +} |
| + |
| +// The snippet's contract (spec §3.4), pinned as strings: handlers for both |
| +// error events, sendBeacon for events, no pageview send in edge mode. |
| +func TestCarlosJSContract(t *testing.T) { |
| + for _, want := range []string{"'error'", "'unhandledrejection'", "navigator.sendBeacon", "/__carlos/e", "/__carlos/a", "window.carlos", "track:", "<email>", "<redacted>", "<hex>", "location.pathname"} { |
| + if !strings.Contains(carlosJS, want) { |
| + t.Errorf("snippet lacks %s", want) |
| + } |
| + } |
| + if strings.Contains(carlosJS, `"pageview"`) || strings.Contains(carlosJS, `'pageview'`) { |
| + t.Error("the snippet must not send pageviews in edge mode") |
| + } |
| +} |
| + |
| +func TestCarlosJSNodeSyntax(t *testing.T) { |
| + nodePath, err := exec.LookPath("node") |
| + if err != nil { |
| + t.Skip("node not on PATH") |
| + } |
| + dir := t.TempDir() |
| + file := dir + "/carlos.js" |
| + if err := os.WriteFile(file, []byte(carlosJS), 0o644); err != nil { |
| + t.Fatal(err) |
| + } |
| + out, err := exec.Command(nodePath, "--check", file).CombinedOutput() |
| + if err != nil { |
| + t.Fatalf("node --check: %v\n%s", err, out) |
| + } |
| +} |
diff --git a/internal/edge/reserved.go b/internal/edge/reserved.go| index 24aa1b2af..ee2153a42 100644 |
| --- a/internal/edge/reserved.go |
| +++ b/internal/edge/reserved.go |
| @@ -20,5 +20,8 @@ const reservedSegment = "__carlos" |
| // dispatch inside it; host and up are already threaded through because both |
| // need them for attribution. |
| func (e *Edge) serveReserved(w http.ResponseWriter, r *http.Request, host string, up upstream) { |
| + if serveCarlosJS(w, r) { |
| + return |
| + } |
| http.NotFound(w, r) |
| } |