Skip to content

feat: TideError sweep, error-reporting v1, reservation ProblemDetails fix#77

Open
DomValladolid wants to merge 34 commits into
main-mergefrom
staging
Open

feat: TideError sweep, error-reporting v1, reservation ProblemDetails fix#77
DomValladolid wants to merge 34 commits into
main-mergefrom
staging

Conversation

@DomValladolid

Copy link
Copy Markdown
Contributor

Summary

This PR brings the following completed initiatives from staging into main-merge:

  • SWE Error Reporting v1 — Full TideError sweep across Cryptide, Models, Tools, and Clients tiers (81 raw throw Error(...) converted to structured TideError under CRYPTO_*/SERIAL_*/MODEL_*/MEM_* code families). New RecentRequestsBuffer for recent-request metadata. Top-level re-exports for ORK SWE consumption.
  • v1.1 TideError sweep — Extended sweep covering Tools/Utils, additional Models and Cryptide sites.
  • Reservation ProblemDetails fix (Plan A v2) — Case-tolerant PascalCase ProblemDetails parsing in ClientBase (fixes silent degradation of every ORK problem+json to PARSE_PROBLEM_JSON_INVALID). Opt-in unanimous-code promotion in WaitForNumberofORKs (promoteUnanimousCodes: true) so cohort 409s surface the real code. Security hardening: true-unanimity gate, median representative selection, full field sanitization, promoted-messageKey namespace guard.

Test plan

  • TideError structured errors propagate from Cryptide operations
  • ORK ProblemDetails with PascalCase fields parse correctly (Code/MessageKey/MessageParams)
  • Username reservation 409 → cohort unanimity promoted to TIDE-ORK-KEYGEN-USERNAME_TAKEN or USERNAME_RESERVED
  • Single-ORK failure with honest ORKs unreachable does NOT fake unanimity
  • npm test passes (29 reservation + 69 buildReportPayload tests)

🤖 Generated with Claude Code

sundayScoop and others added 30 commits December 1, 2025 14:56
For the SWE error-reporting initiative:
- Upgrade 81 raw throw Error() sites in Cryptide/Components, TideKey,
  AES, DH, Serialization, Doken, TideMemoryObjects, Tools/TideMemory
  to structured TideError under new CRYPTO_/SERIAL_/MODEL_/MEM_ code
  families
- Add Clients/RecentRequestsBuffer for SWE error-report context
  (FIFO bounded ring, in-memory only, no payloads)
- Auto-push entries from ClientBase._get/_post/_put/_postJSON with
  performance.now() timing
- Add tideJsVersion placeholder export for build correlation
- 21 new error code constants in Errors/codes.ts

All displayMessages carry only metadata (lengths, offsets, type names);
zero key bytes / ciphertext / plaintext.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Independent security review found that buildReportPayload.js in the ORK
Enclave does `import { TideError } from "@tideorg/js"` (named import),
but tide-js/index.ts only exposed TideError via the `Errors` namespace
(`export * as Errors from './Errors'`). The named import resolved to
undefined, so TideError.isTideError(error) crashed on first error
modal open.

Add top-level re-exports of TideError, TideJsErrorCodes,
RecentRequestsBuffer + their accompanying types. Preserves the existing
`Errors` namespace export (keycloak-IGA consumes Errors.TideError per
the Phase 1 audit comments).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Converts 20 raw `throw Error(...)` sites in BaseScheme + Ed25519
component classes + Ed25519 scheme functions to TideError, plus
adds 11 new code constants in Errors/codes.ts used by the rest
of the v1.1 sweep:

  CRYPTO_ED25519_BAD_POINT, CRYPTO_INVERSE_NOT_EXIST,
  CRYPTO_INVALID_BIGINT_INPUT, CRYPTO_HASH_TO_POINT_INVALID_INPUT,
  SIG_VERIFY_FAILED, MODEL_UNKNOWN_MODEL, MODEL_UNKNOWN_PARAM_TYPE,
  MODEL_PARAM_NOT_FOUND, MODEL_DEV_ERROR,
  MODEL_REQUEST_NOT_INITIALIZED, MODEL_VERSION_MISMATCH

Site → code assignments (curve-scheme tier):
  - BaseScheme.ts:22-25 (x4) → CRYPTO_NOT_IMPLEMENTED
  - Ed25519Components.ts:42 → SERIAL_INVALID_TYPE
  - Ed25519Components.ts:46 → MODEL_INVALID_FIELD
  - Ed25519Components.ts:52 → MODEL_INVALID_FIELD
  - Ed25519Components.ts:60 → CRYPTO_COMPONENT_MISMATCH (Add)
  - Ed25519Components.ts:66 → CRYPTO_COMPONENT_MISMATCH (Multiply)
  - Ed25519Components.ts:72 → CRYPTO_COMPONENT_MISMATCH (Minus)
  - Ed25519Components.ts:78 → CRYPTO_COMPONENT_MISMATCH (Equals)
  - Ed25519Components.ts:98 → MODEL_INVALID_FIELD (private.priv)
  - Ed25519Components.ts:104 → MODEL_INVALID_FIELD (private.rawBytes)
  - Ed25519Components.ts:114 → SERIAL_INVALID_TYPE (private ctor)
  - Ed25519Components.ts:144 → SERIAL_INVALID_TYPE (seed ctor)
  - Ed25519Scheme.ts:36 → SERIAL_INVALID_TYPE (sign type)
  - Ed25519Scheme.ts:44 → SIG_VERIFY_FAILED
  - Ed25519Scheme.ts:46 → SERIAL_INVALID_TYPE (verify type)
  - Ed25519Scheme.ts:55 → SERIAL_INVALID_TYPE (encrypt type)
  - Ed25519Scheme.ts:64 → SERIAL_INVALID_TYPE (decrypt type)

Follows v1 conventions documented in commit 665677f-style
(structured fields, no banned-token leakage - displayMessages
carry only metadata, never raw key bytes or component contents).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Converts 13 raw `throw Error(...)` sites in the low-level math
and Ed25519 primitives to TideError:

  - Interpolation.ts:46 → CRYPTO_ORK_ARRAY_LENGTH_MISMATCH
  - Interpolation.ts:55 → CRYPTO_ORK_ARRAY_LENGTH_MISMATCH
  - Math.ts:77 → CRYPTO_INVERSE_NOT_EXIST (mod_inv zero/neg)
  - Math.ts:92 → CRYPTO_INVERSE_NOT_EXIST (gcd != 1)
  - Ed25519.ts:63 → CRYPTO_ED25519_BAD_POINT (err helper, routes
      ~15 internal `err(...)` callsites through TideError)
  - Ed25519.ts:133 → CRYPTO_ED25519_BAD_POINT (bad point: ZERO)
  - Ed25519.ts:145 → CRYPTO_ED25519_BAD_POINT (left != right (1))
  - Ed25519.ts:150 → CRYPTO_ED25519_BAD_POINT (left != right (2))
  - Ed25519.ts:295 → CRYPTO_INVALID_BIGINT_INPUT (invert typeof)
  - H2P.ts:82 → CRYPTO_HASH_TO_POINT_INVALID_INPUT (negative power)
  - H2P.ts:166 → CRYPTO_HASH_TO_POINT_INVALID_INPUT (i2osp range)
  - H2P.ts:188 → CRYPTO_HASH_TO_POINT_INVALID_INPUT (xmd length)
  - TideSignature.ts:35 → SERIAL_INVALID_TYPE

Sanitisation: Math.ts:77 and Ed25519.ts:297 originally embedded
the raw `number`/`num` bigint (which can be a private scalar)
into displayMessage. Replaced with sign-only metadata
("zero" / "non-zero", "non-positive" / "positive").

Follows v1 conventions documented in commit 665677f-style
(structured fields, cause-chain, no banned-token leakage -
buildReportPayload.test.js still passes).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Converts 16 raw `throw Error(...)` sites in the Models tier to
TideError, completing the Models-side coverage so all request
construction / Policy parsing flows surface structured errors:

  - ModelRegistry.ts:34 → MODEL_UNKNOWN_MODEL
  - BaseTideRequest.ts:174 → MODEL_REQUEST_NOT_INITIALIZED (addApproval)
  - BaseTideRequest.ts:246 → MODEL_REQUEST_NOT_INITIALIZED (encode authorizer)
  - BaseTideRequest.ts:247 → MODEL_REQUEST_NOT_INITIALIZED (encode authorizerCert)
  - BaseTideRequest.ts:248 → MODEL_REQUEST_NOT_INITIALIZED (encode authorization)
  - BaseTideRequest.ts:287 → MODEL_INVALID_FIELD (decode name/version)
  - BaseTideRequest.ts:332 → SERIAL_INVALID_LENGTH (uint8ArrayToUint32LE)
  - Policy.ts:75 → MODEL_VERSION_MISMATCH (unknown version)
  - Policy.ts:176 → MODEL_UNKNOWN_PARAM_TYPE (fromBytes)
  - Policy.ts:195 → MODEL_PARAM_NOT_FOUND
  - Policy.ts:219 → MODEL_INVALID_FIELD (unexpected param type)
  - Policy.ts:253 → MODEL_UNKNOWN_PARAM_TYPE (toBytes)
  - Policy.ts:273 → MODEL_DEV_ERROR (PolicyV2 version assertion)
  - Policy.ts:325 → MODEL_DEV_ERROR (PolicyV1 version assertion)
  - SerializedField.ts:41 → MODEL_VERSION_MISMATCH
  - PolicyProtectedSerializedField.ts:41 → MODEL_VERSION_MISMATCH

`throw 'string'` literals in Policy.ts ctor (lines 26/30/36/38/40/46)
are intentionally OUT of scope - they aren't `throw new Error(...)`
sites and the parking-lot covers `throw Error` only. Tracked for
v1.2 follow-up.

Follows v1 conventions documented in commit 665677f-style
(structured fields, cause-chain, no banned-token leakage -
buildReportPayload.test.js still passes).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Converts the 1 raw `throw Error(...)` site in removeRandomElements
to TideError:

  - Tools/Utils.ts:262 → SERIAL_INVALID_LENGTH

Follows v1 conventions documented in commit 665677f-style
(structured fields, no banned-token leakage - displayMessage
carries only array-length metadata).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…roblem+json pass-through

ORKs emit PascalCase ProblemDetails fields (Code/MessageKey/MessageParams/...)
on the wire, but _handleError only read lowercase, so every ORK problem+json
degraded to PARSE_PROBLEM_JSON_INVALID. Extract the problem+json branch into
_problemDetailsToError, which reads each envelope field case-tolerantly
(lowercase preferred for back-compat, PascalCase canonical going forward).
messageParams dictionary KEYS pass through untouched; messageKey is copied
verbatim with no prefix assumptions.

Also wire the helper into _get and _getSilent: previously they threw
NET_NON_OK_STATUS before reading the body, so structured 409s (e.g.
TIDE-ORK-KEYGEN-USERNAME_RESERVED from GetReservers) were discarded. Non-
problem+json non-ok responses keep the exact NET_NON_OK_STATUS behaviour.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Tide PM Agent (@DomValladolid) and others added 4 commits June 12, 2026 19:33
Add WaitForORKsOptions bag to WaitForNumberofORKs (no 9th positional) with
promoteUnanimousCodes (default off), threaded into PromiseRace. When the
threshold fails and every per-ORK failure is a TideError sharing one
identical non-TIDE-TIDEJS-* code, throw a promoted TideError carrying the
upstream code/messageKey/messageParams/httpStatus/problemType/traceId
instead of the generic NET_THRESHOLD_FAILURE. Representative failure is the
one with the largest Number(messageParams.expirySeconds) (wire values are
strings); falls back to failed[0] when absent/NaN.

The promotion sits AFTER the 'Too many attempts' special case (admin-ui
string-matches it) and BEFORE the generic threshold throw; behaviour is
byte-identical for all callers with the flag off or non-unanimous failures.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add Tests/ReservationErrors.test.mjs (node:test against compiled dist/)
covering: PascalCase + lowercase ProblemDetails parsing, malformed-body
fallback to PARSE_PROBLEM_JSON_INVALID, _get/_getSilent 409 problem+json
pass-through vs NET_NON_OK_STATUS for non-problem bodies, and PromiseRace
unanimous-code promotion (representative selection, mixed codes, transport
codes, flag off).

The repo had no automated test runner (Tests/ were manual browser pages,
npm test was a stub). Wire 'npm test' to clean-build via tsc then run
node --test with a small ESM resolver hook (node-esm-extension-loader.mjs)
that appends .js to the extensionless relative specifiers tsc emits under
moduleResolution: bundler. dist clean is required because rebuilding over
an existing dist fails with TS5055 (pre-existing, reproduced on staging).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Phase 4 security-review fixes for the reservation error surfacing work:

- F1 true-unanimity gate: promotion now requires the failure set to cover
  the FULL attempted cohort (failed.length === initLength, zero successes).
  A timeout-truncated set falls through to NET_THRESHOLD_FAILURE, so one
  fast malicious ORK + slow honest ORKs can no longer fake unanimity.
- F2 median representative: representative = median of numeric
  messageParams.minutes (lower median on even counts), falling back to
  median expirySeconds (older ORKs), then failed[0]. Non-finite values
  ("9e999", "Infinity", "NaN") are never candidates, so a single cohort
  member cannot control the displayed fields.
- F3 ProblemDetails field validation (all problem+json paths): envelope
  fields must be strings or are dropped; code/messageKey <= 256 chars with
  charset [A-Za-z0-9._:-] (dropped on failure, never thrown); detail/title
  truncated to 2048; traceId/source/type/instance truncated to 256;
  messageParams must be a plain object, <= 16 keys matching
  ^[A-Za-z0-9_]{1,64}$ (prototype-polluting names dropped), values
  String()-coerced and truncated to 256.
- F4 promoted-messageKey namespace guard: a promoted error carries
  messageKey only if it starts with error.tide. / errors.tide. — blocks
  arbitrary i18n-catalog key selection through the promotion path.
- F5 tests: 16 new cases (partial-set no-promote, median/outlier/non-finite
  selection, drop/truncate without throwing incl. __proto__ and 1MB bodies,
  namespace guard); 29 total passing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants