SWE Enclave Errors and Error Reporting
The SWE Enclave (the browser-side surface that runs Tide flows in the user's tab) used to surface failures as throw new Error("...") strings shown verbatim in the UI. That made flow errors hard to triage on the operator side and impossible for users to report safely — the natural "copy/paste this stacktrace" path would have leaked SessionKey private components, DPoP keys, raw Dokens, and decrypted vendor responses.
The SWE error-reporting work replaces that model with three coordinated pieces:
- A single structured error type —
TideError— used everywhere on the client side, with a closed enumeration ofTIDE-TIDEJS-*codes. - A structured "Something went wrong" page rendered by
renderSomethingWentWrongin the Enclave, with copy / retry / report controls. - A report flow (modal →
POST /Diagnostics/ErrorReport) whose payload is constrained by an explicit allowlist with a unit-test canary, then a second pass of DTO-based redaction on the server.
This page documents all three. It is a sibling of:
- ORK Errors — the server-side
TIDE-ORK-*taxonomy. ORK error codes can be carried verbatim on aTideErrorwhen an ORK ProblemDetails response is the source of the failure. - ORK Metrics — the OpenTelemetry metric dictionary.
TideError — the structured error type
TideError lives in tide-js/Errors/TideError.ts and is exported from @tideorg/js. Every error thrown across Cryptide / TideKey / AES / DH / Serialization / Doken / TideMemory / Models / Tools is now a TideError; raw throw new Error("...") is gone from those paths.
Shape
class TideError extends Error {readonly code: string; // canonical code, e.g. "TIDE-TIDEJS-CRYPTO-ED25519_BAD_POINT"readonly displayMessage: string; // UI-safe human-readable messagereadonly messageKey?: string | null;readonly messageParams?: Record<string, unknown> | null;readonly traceId?: string; // W3C traceparent, propagated verbatim from upstreamreadonly source?: string; // e.g. "Ork/Voucher/Verifier.cs:142" or the tide-js call-sitereadonly httpStatus?: number;readonly problemType?: string; // RFC 9457 Problem Details "type" URIreadonly url?: string; // full request URL when knowablereadonly endpoint?: string; // path-only portion of urlreadonly method?: string;readonly details?: TideErrorDetail[]; // per-attempt failures for aggregate errors}
The code, displayMessage, and (where present) traceId form a cross-project public contract — keycloak-IGA's getTideErrorInfo and the SWE error UI both read these field names directly. Renaming any of them requires coordinating across every consumer.
TideError.isTideError(value) is a structural type guard (checks name === "TideError" plus the two required string fields) — used by external consumers that cannot rely on instanceof across module/bundling boundaries.
Throwing
import { TideError, TideJsErrorCodes } from "@tideorg/js";throw new TideError({code: TideJsErrorCodes.CRYPTO_ED25519_BAD_POINT,displayMessage: "Ed25519 point failed on-curve validation.",source: "Cryptide/Ed25519Components.ts:add",cause: underlyingError,});
The initialiser is always a single object so call-sites are self-documenting and field order can never drift.
Pass-through of upstream codes
When tide-js receives an application/problem+json body from an upstream component (ORK, tidecloak-idp-extensions, midgard), the upstream code is preserved verbatim on the resulting TideError. It is not mapped onto a TIDE-TIDEJS-* code. A consumer switching on error.code will therefore see e.g. TIDE-ORK-SIG-VERIFY_FAILED for an ORK-originated signing failure, exactly as the ORK emitted it.
The synthetic codes in TideJsErrorCodes are only used for failures that originate inside tide-js itself.
Why this replaced raw throw Error(...)
Raw Error instances lose every structured field a triage flow needs: there is no machine-readable code to switch on, no traceId for log correlation, no per-attempt details for aggregate failures, no consistent source location. They also serialise differently across realms (Node, browser, web worker) and forced the SWE error page to render [object Object] whenever a flow threw a literal. TideError standardises all of that and gives the error UI a single shape to render.
Code reference
These are the codes tide-js itself originates. They are defined as a frozen object literal in tide-js/Errors/codes.ts and exported as TideJsErrorCodes. The canonical form is TIDE-TIDEJS-<CATEGORY>-<NAME>.
These strings are a stable contract. tide-js consumers (the SWE, keycloak-IGA, idp-extensions) compare against these exact values to drive UI / triage flows — treat them like a public API surface.
Network
| Code | When emitted |
|---|---|
TIDE-TIDEJS-NET-FETCH_FAILED | Underlying fetch rejected (DNS, connection refused, TLS, CORS, ...) |
TIDE-TIDEJS-NET-TIMEOUT | The request was aborted by tide-js's internal timeout AbortController |
TIDE-TIDEJS-NET-ABORTED | The request was aborted by a caller-supplied AbortSignal |
TIDE-TIDEJS-NET-NON_OK_STATUS | response.ok === false and the body did not carry a recognisable error envelope |
TIDE-TIDEJS-NET-THRESHOLD_FAILURE | Fan-out to multiple ORKs completed but fewer succeeded than the threshold required. Carries details[] with each per-ORK underlying failure |
TIDE-TIDEJS-NET-UNKNOWN | A non-TideError value was thrown from the fetch pipeline — caught and tagged so it is still visible in the recent-requests buffer |
Parsing
| Code | When emitted |
|---|---|
TIDE-TIDEJS-PARSE-PROBLEM_JSON_INVALID | Server sent application/problem+json but the body did not parse or lacked required fields |
TIDE-TIDEJS-PARSE-UNKNOWN_FORMAT | Body shape is not understood (e.g. legacy --FAILED--: envelope, or unknown format) |
TIDE-TIDEJS-PARSE-NODECLIENT_RESPONSE_SHAPE | A NodeClient response did not contain the expected index field (used by WaitForNumberofORKs cleanup) |
TIDE-TIDEJS-PARSE-INSUFFICIENT_DATA | TideMemory buffer is too small to read the requested segment (truncated / malformed input) |
TIDE-TIDEJS-PARSE-INDEX_OUT_OF_RANGE | TideMemory segment index requested is past the end of the buffer's encoded segments |
TIDE-TIDEJS-PARSE-BUFFER_OVERFLOW | TideMemory allocation/write would exceed the destination buffer's capacity |
Validation
| Code | When emitted |
|---|---|
TIDE-TIDEJS-VAL-MISSING_SESSION_KEY | A client method required a session key but AddBearerAuthorization was never called |
TIDE-TIDEJS-VAL-INPUT_SHAPE | A flow input failed a shape/type validation (wrong type, wrong array length, missing required field) |
TIDE-TIDEJS-VAL-UID_FORBIDDEN | The supplied username (uid) is not allowed (empty reserver list returned by the network) |
TIDE-TIDEJS-VAL-INVALID_ACCOUNT | The supplied account could not be located on the network (e.g. simulator invalid-account sentinel) |
Cryptography
Low-level cryptographic primitive failures. Typically thrown from Cryptide/. These are generally not retryable — they indicate a malformed input, an on-curve check failure, or an unsupported key/scheme combination.
| Code | When emitted |
|---|---|
TIDE-TIDEJS-CRYPTO-SESSION_KEY_MISMATCH | The session key the caller supplied does not match the session key bound into the Doken |
TIDE-TIDEJS-CRYPTO-GRJ_SJ_LENGTH_MISMATCH | GRj and Sj arrays produced by a signing flow had differing lengths (should be impossible) |
TIDE-TIDEJS-CRYPTO-ORK_ARRAY_LENGTH_MISMATCH | Per-ORK response arrays had differing lengths during PreSign / Sign aggregation |
TIDE-TIDEJS-CRYPTO-NOT_IMPLEMENTED | A BaseComponent abstract method (Add / Multiply / Scheme) was invoked but not implemented on the concrete subclass |
TIDE-TIDEJS-CRYPTO-COMPONENT_MISMATCH | Two components were combined whose schemes / component-types do not match |
TIDE-TIDEJS-CRYPTO-UNKNOWN_COMPONENT_TYPE | Scheme / component-type registry lookup failed (unknown scheme or component type) |
TIDE-TIDEJS-CRYPTO-DESERIALIZE_FAILED | A serialized component could not be parsed into bytes (neither hex nor base64) |
TIDE-TIDEJS-CRYPTO-AES_UNSUPPORTED_KEY_TYPE | AES encrypt/decrypt called with a key of an unsupported JS type |
TIDE-TIDEJS-CRYPTO-DH_UNSUPPORTED_PRIV_TYPE | DH computeSharedKey called with a private value of an unsupported JS type |
TIDE-TIDEJS-CRYPTO-ED25519_BAD_POINT | An Ed25519 Point failed an on-curve / equality / non-ZERO sanity check |
TIDE-TIDEJS-CRYPTO-INVERSE_NOT_EXIST | Modular inverse does not exist (gcd != 1, or invert of 0 / non-positive modulus) |
TIDE-TIDEJS-CRYPTO-INVALID_BIGINT_INPUT | A low-level crypto primitive received a value of an unexpected JS type (e.g. invert expected a bigint) |
TIDE-TIDEJS-CRYPTO-HASH_TO_POINT_INVALID_INPUT | Hash-to-Point (RFC 9380 expand_message_xmd / i2osp) received an out-of-range input |
Signature
| Code | When emitted |
|---|---|
TIDE-TIDEJS-SIG-VERIFY_FAILED | Non-blind signature verification failed (e.g. Ed25519Scheme.verifyingFunc) |
TIDE-TIDEJS-SIG-BLIND_VERIFY_FAILED | Local blind-signature verification failed against the expected challenge |
Serialization
Helpers under tide-js/Serialization/ — length-tagged blobs, hex / base64 transcoding, type guards.
| Code | When emitted |
|---|---|
TIDE-TIDEJS-SERIAL-LENGTH_OUT_OF_RANGE | A numeric value cannot be represented in the requested width (e.g. > Int64 / > 255 byte) |
TIDE-TIDEJS-SERIAL-INVALID_TYPE | The supplied argument was not of the expected JS type (e.g. expected Uint8Array, got something else) |
TIDE-TIDEJS-SERIAL-INDEX_OOB | A serialization helper found data already present where an empty slot was expected |
TIDE-TIDEJS-SERIAL-BUFFER_OVERFLOW | A serialization write would have exceeded the destination buffer's capacity |
TIDE-TIDEJS-SERIAL-INVALID_LENGTH | A length-tagged input did not match the expected length (e.g. TIDE_KEY blob != 32 bytes) |
TIDE-TIDEJS-SERIAL-UNEXPECTED_HEADER | A header / magic value did not match the expected token (e.g. tidexxxkey prefix mismatch) |
TIDE-TIDEJS-SERIAL-INVALID_HEX | A hex string failed regex validation |
TIDE-TIDEJS-SERIAL-INVALID_BASE64 | A base64 string failed validation or decoding |
TIDE-TIDEJS-SERIAL-LENGTH_MISMATCH | Two operand arrays had unequal lengths where equal lengths were required (e.g. XOR) |
Models
Construction / parsing guards on tide-js/Models/ — Doken, AuthRequest, TideKey, Policy.
| Code | When emitted |
|---|---|
TIDE-TIDEJS-MODEL-INVALID_FIELD | A model field (Doken / AuthRequest / TideKey) failed a shape/type guard during construction or parsing |
TIDE-TIDEJS-MODEL-UNEXPECTED_HEADER | A model header value (e.g. Doken alg / typ) did not match the expected value |
TIDE-TIDEJS-MODEL-INVALID_SHAPE | A model expected a specific shape (e.g. Doken = 3 parts) and the input did not conform |
TIDE-TIDEJS-MODEL-INVALID_KEY | A TideKey was constructed/derived from a component that does not satisfy the required interface |
TIDE-TIDEJS-MODEL-VALUE_OUT_OF_RANGE | A model field's value was not in the allowed range (e.g. VRK expiry too close to now) |
TIDE-TIDEJS-MODEL-UNKNOWN_MODEL | ModelRegistry could not resolve a sign-request name:version to a builder (unknown model id) |
TIDE-TIDEJS-MODEL-UNKNOWN_PARAM_TYPE | A PolicyParameters entry carries an unrecognised type tag (not str / num / bnum / bln / byt) |
TIDE-TIDEJS-MODEL-PARAM_NOT_FOUND | Policy.getParameter was asked for a parameter key that does not exist on the policy |
TIDE-TIDEJS-MODEL-DEV_ERROR | A developer-only invariant was violated inside a Policy version handler (should be unreachable in production) |
TIDE-TIDEJS-MODEL-REQUEST_NOT_INITIALIZED | A request (e.g. BaseTideRequest) was used before a required field (authorizer / authorization / cert) had been added |
TIDE-TIDEJS-MODEL-VERSION_MISMATCH | A serialized model header carried an unsupported version tag (Policy / SerializedField) |
TideMemory
Bounds-checked memory helpers in tide-js/Tools/TideMemory.ts. These guards prevent malformed or hostile input from over-reading or over-writing a TideMemory buffer.
| Code | When emitted |
|---|---|
TIDE-TIDEJS-MEM-NEGATIVE_INDEX | Caller supplied a negative index to a TideMemory helper |
TIDE-TIDEJS-MEM-INDEX_ZERO_RESERVED | Caller attempted to overwrite the zero-index slot via WriteValue (must use Create) |
TIDE-TIDEJS-MEM-BUFFER_OVERFLOW | TideMemory write would exceed the destination buffer's capacity |
TIDE-TIDEJS-MEM-INDEX_OUT_OF_RANGE | TideMemory read sought past the encoded segments of the buffer |
TIDE-TIDEJS-MEM-INSUFFICIENT_DATA | TideMemory buffer is too small to hold even the version header |
TIDE-TIDEJS-MEM-INDEX_ALREADY_WRITTEN | TideMemory write attempted at an index already populated with data |
Proxy / pass-through
| Code | When emitted |
|---|---|
TIDE-TIDEJS-PROXY-UPSTREAM_ERROR | tide-js is wrapping an upstream failure in a way that adds semantics (e.g. "all ORKs failed", retry exhaustion). For straight pass-through of an upstream ProblemDetails body, the upstream code is preserved verbatim instead |
SWE-side synthetic code
| Code | When emitted |
|---|---|
TIDE-SWE-UNHANDLED | Synthesised by the SWE error page (convertToError and buildReportPayload) for any thrown value that is not a TideError and not a duck-typed {code, displayMessage} literal. Covers plain Error instances, strings, and other thrown values from third-party / pre-TideError call sites. Renders the original message as the displayMessage |
Error reporting flow
The journey from a thrown error to an operator-side report is:
throw TideError global handler / explicit catch│ │▼ ▼convertToError(err) ────► renderSomethingWentWrong(info)│├── copy / copy-all / retry buttons│└── "Report this error" ─► showErrorReportModal(info)│├── buildReportPayload (allowlist)│▼JSON preview│▼POST /Diagnostics/ErrorReport│├── 202 ─► toast: report id│└── 5xx / network fail ─► mailto: fallback
Each step:
convertToError(err)(inOrk/Ork/Enclave/js/index.js~line 3507) normalises any thrown value —TideError, plainError, duck-typed{code, displayMessage}literal, string, or other — into a uniform info object with all the fields the page renderer reads. Plain values becomeTIDE-SWE-UNHANDLED.messageKeyis resolved against the loaded i18n bundle; missing translations fall back to"<code>: <displayMessage>"so the user can always quote the code.renderSomethingWentWrong(info, opts)(~line 3632) hides the active.page, populates the structured error page (code,traceId,source,httpStatus,endpoint,method, and the per-attemptdetails[]list), and wires the copy / retry / report buttons.showErrorReportModal(info)(~line 3900) callsbuildReportPayloadto produce the sanitised initial payload + a set of "user field chips" (non-password user inputs from the visible page). The user can remove any chip with an X button before sending.- JSON preview gate. A
<details>element shows the exact payload that will be sent. The Send button isdisableduntil the user expands that preview at least once. This is an explicit informed-consent step — the user cannot send anything they have not had the option to inspect. POST /Diagnostics/ErrorReportwithContent-Type: application/json. On2xxthe modal closes and a transient toast shows the server-issuedreportId.mailto:fallback. On any non-2xx response (fetchreturningres.ok === false) or a network-level failure (fetchthrowing), the modal falls back to opening amailto:errors@tide.orgwith a minimal subset — code, traceId, source, displayMessage, endpoint, HTTP status, and the optional user note. No automatic retry is performed; nothing else is auto-submitted.
Security boundary — what gets sent and what doesn't
The buildReportPayload function in Ork/Ork/Enclave/js/Reporting/buildReportPayload.js is the wall. It is intentionally pure (no DOM access, no fetch, no globals) so it can be unit-tested in isolation. The "shape" of the report is defined by a strict allowlist; anything not in the allowlist is silently dropped, even if a caller hands the function an object that contains private fields.
Threat model
- The Enclave keeps highly-sensitive material in memory: SessionKey private components,
_clientDPoPKey,gPass,networkSessionKey, decrypted vendor responses, raw Dokens,prismAuthi,prkECDHi. None of this can ever cross the wire as part of an error report, even with explicit user consent. - Page inputs may contain passwords. Password inputs are always dropped — regardless of user consent and regardless of what the user typed into the chips area. Other inputs are collected into
userFieldsRawso the modal can let the user opt-in or remove them on a per-field basis before the payload is finalised.
What the allowlist permits
- TideError structured fields:
code,displayMessage,messageKey,traceId,source,httpStatus,problemType,method,endpoint,url,details[],stack. - SWE state:
?type=...parameter,voucherURL, visible page name,buildSha. - tide-js version: the
tideJsVersionconstant from@tideorg/js. - Environment:
userAgent,language,platform,screen(WIDTHxHEIGHT),viewport(WIDTHxHEIGHT). - ORK topology (PUBLIC IDs/URLs only):
sessionKeyPublic(the public component only —enclave.SessionKey.get_public_component().Serialize().ToString()),activeOrksCount,threshold,bitwise. - Recent requests (from
RecentRequestsBuffer.snapshot()): one entry per recent HTTP request observed byClientBase, withtimestamp, sanitizedurl,endpoint,method,httpStatus,durationMs, and the TideErrorcodeif the request failed. Bodies are never included. - timeSkew (from
localStorage, informational only). - User fields added by the modal after the user accepts each chip (
userFields) and an optional free-formuserNote.
URL sanitisation
- The
uidquery parameter is stripped, case-insensitive (uid/UID/Uidare all dropped) — guards against an attacker probing for an exact-match strip. vvkidis truncated to the first 12 characters (it is a public id but long; truncating shrinks the bundle and reduces fingerprinting surface).voucherURLis kept verbatim (it is public-by-design).- For URLs that fail
new URL(...)parsing, only the path portion before the first?is kept — the query string is never re-included from an unparseable input.
Stack-trace handling
- Stack traces are capped at 8 KB.
- Absolute filesystem paths in stack frames are best-effort stripped to the file basename (
/home/alice/project/ork/Ork/Ork/Enclave/js/index.jsbecomesindex.js), removing personally-identifying directory layouts.
Banned-token canary test
buildReportPayload.test.js constructs a fake Enclave / fake localStorage / fake active page that contain every category of sensitive material, calls buildReportPayload, then serialises the resulting payload to JSON and asserts that none of the following 26 tokens appear anywhere in the serialised output:
PRIVATE_SCALAR_SECRET_DO_NOT_LEAKPRIVATE_RAW_BYTES_SECRETPRIVATE_SEED_SECRETPRIVATE_RB_SECRETDPOP_PRIVATE_KEY_SECRETGPASS_SECRETNETWORK_SESS_KEY_PRIVATE_SECRETDECRYPTED_RESPONSE_SECRETDECRYPTED_CHALLENGE_SECRETDEC_PRISM_REQUEST_SECRETDOKEN_RAW_STRING_SECRETVENDOR_ENC_DATA_SECRETPRISMAUTHI_SECRETPRKECDHI_SECRETTIDE_DEVICE_KEY_PRIVATE_SECRETTIDE_SESSION_KEY_PRIVATE_SECRETTIDE_ENTRY_PRIVATE_BYTES_SECRETHUNTER2_PASSWORD_SECRETCSRF_TOKEN_SECRETVRK_SECRET_DATAUSER_UID_PRIVATE_SECRETANOTHER_UID_SECRETDETAIL_UID_SECRETUSER_UID_IN_BUFFER_SECRETCAUSE_PRIVATE_SECRETREQUEST_BODY_PRIVATE_SECRETRESPONSE_BODY_PRIVATE_SECRET
Each token represents a distinct class of material the allowlist must keep out: SessionKey private scalar / raw bytes / seed / rB, DPoP key, gPass, network session key, every decrypted response / challenge / Prism request, raw Dokens, vendor-encrypted data, PrismAuthi, PrkECDHi, Tide_Device_Key / Tide_SessionKey / Tide_Entry in localStorage, password inputs, hidden CSRF inputs, data-tide-sensitive inputs, every flavour of uid query parameter (URL, error.url, detail URLs, recent-request URLs), cause objects on error details, and request / response bodies in recent-request entries.
When extending the allowlist or adding a new piece of state to the payload, add a matching banned token to this test for any sensitive material the new state might risk surfacing. The test exits non-zero on the first failure and is intentionally dependency-free so it can run without a test framework.
Server-side second pass
The Diagnostics endpoint does not trust the inbound payload shape. The body is deserialised into a strongly-typed ErrorReportDto (with only the allowlist fields as [JsonPropertyName(...)] properties) and System.Text.Json silently drops everything else. The on-wire / on-disk shape is then re-serialised from the DTO, never echoed from the raw inbound bytes. A bug in the client-side allowlist therefore cannot leak material the DTO does not have a slot for.
DiagnosticsController — the server-side endpoint
Ork/Ork/Controllers/Diagnostics/DiagnosticsController.cs.
Endpoint
POST /Diagnostics/ErrorReport with Content-Type: application/json.
Response:
202 Acceptedwith a JSON body{ "reportId": "<guid>", "traceId": "<from-error>" }. ThereportIdis also written to a SerilogLogInformationentry with structured fieldstide.error.report.id,tide.error.code,tide.error.report.trace_id,tide.error.report.source, andtide.error.report.payload(full sanitised JSON). That log entry is the authoritative record — email is best-effort.400(TIDE-ORK-DIAGNOSTICS-INVALID_REPORT) when the body is not valid JSON or is missing required fields (schemaVersion,error.code,error.displayMessage).413(TIDE-ORK-DIAGNOSTICS-PAYLOAD_TOO_LARGE) when the body exceedsTide:Diagnostics:MaxBodyBytes(default 256 KiB). The body-size check fires before JSON parsing, so an oversized junk payload cannot trigger an expensive parse.429(TIDE-ORK-DIAGNOSTICS-RATELIMITED) when the per-IP rate limit is exceeded.503(TIDE-ORK-DIAGNOSTICS-DISABLED) whenTide:Diagnostics:ErrorReportEnabledisfalse(kill switch).
Rate limiting
A per-client-IP token bucket sized by Tide:Diagnostics:RateLimitPerMinute (default 5 reports per IP per minute). The bucket dictionary is a static ConcurrentDictionary so it survives request scopes; it lives per replica, not per cluster — for stricter cross-replica limits deploy ThrottleMiddleware in front of the controller. The dictionary opportunistically evicts stale buckets every 50 calls (any bucket whose window is older than 5 minutes is dropped) and has a hard cap of 10,000 entries to defend against IP-spraying attacks.
Email destination + SMTP config
Reports are forwarded to the destination resolved from Tide:Diagnostics:ErrorReportEmailTo (default errors@tide.org).
SMTP transport reuses the shared Ork.Shared.EmailSettings registered at startup regardless of node mode (Authenticator / Payer / Master). The runtime SMTP host + credentials (Azure Communication Services) come in via --Email:* command-line args and override the placeholders in appsettings.json. The send is fire-and-forget on a background task so a stuck SMTP server cannot keep the request thread alive — the 202 response is returned before the email is dispatched, and the Serilog entry is authoritative if SMTP fails. If EmailSettings.SmtpClient or EmailSettings.NetworkCredentials.Password is missing, the email send is skipped (with a LogWarning) rather than throwing on SmtpClient construction — a misconfigured deploy degrades gracefully.
Overriding the destination
Tide:Diagnostics:ErrorReportEmailTo can be set via the standard ASP.NET Core configuration chain — including as the environment variable Tide__Diagnostics__ErrorReportEmailTo (double underscore separators) on the node.
The known reason an operator might need this override is the M365 same-tenant routing parking lot: when Azure Communication Services sends to a destination that lives in the same M365 tenant as the sending domain, the message can be silently dropped by intra-tenant routing rather than delivered. Pointing the override at a destination outside that tenant (Gmail, an aliased mailbox) restores delivery. This is a transport-level workaround — the structured Serilog entry remains authoritative either way.
Subject-line header injection guard
SanitizeEmailHeader strips control characters (CR, LF) and DEL from Error.Code and Error.TraceId before interpolating them into the email Subject header. Without this, an attacker controlling either value could otherwise inject additional headers or a second message body via CRLF.
Vendor-iframe defense
The "Report this error" button is hidden when window.top !== window.self:
const inIframe = (function () { try { return window.top !== window.self; } catch { return true; } })();if (inIframe) reportBtn.hidden = true;
This composes with the pre-existing EnclaveBase._init clickjacking page-replacement (which fully replaces the SWE document body when loaded inside a frame), giving three independent defences against a malicious vendor coercing a user into surfacing diagnostic state:
- The
EnclaveBase._initclickjacking replacement removes the Tide DOM entirely when the page is iframed. - If that defence is bypassed by build-time or load-order quirks, the Report button is hidden in the renderer.
- Even if both defences fail and a malicious page constructs the report manually,
buildReportPayload's allowlist still constrains what can be in the payload, andDiagnosticsController's DTO still constrains what the server will accept.
The Report flow therefore only fires for top-level Tide sessions — which is correct, because the user-consent step (the "expand the preview, then Send" UX) is only meaningful when the user can actually see and trust the surrounding chrome.
Related references
- ORK server-side errors: ORK Errors
- ORK metrics reference: ORK Metrics
- Forseti concept: Programmable Policy
- RFC 9457 ProblemDetails: rfc-editor.org/rfc/rfc9457.html
- W3C Trace Context (
traceparent): w3.org/TR/trace-context