/** * Every `meta.errorCode` the API emits, grouped by the surface that emits it. * * `meta.errorCode` is the mechanism this API chose for machine-readable errors * — the envelope's own description tells a client to branch on it rather than * on the prose — and until this file existed a client could not enumerate it. * When this file was written, sixteen of its two hundred and forty-two codes * appeared somewhere in `docs/api/openapi.yaml`; the rest existed only in * server source, so the only way to learn one was to trigger the error in * testing and the only way to learn a new one had appeared was to trigger * that too. * * The list is generated from the code and then committed as a literal, so it is * reviewable in a diff and stable across a release. `error-code-catalogue.test.ts` * re-derives it from the tree on every run and fails in both directions: a code * emitted and not listed, and a code listed and no longer emitted. * * Naming: four conventions coexist here — dot.case (the majority and the one * to use), bare snake_case, a camelCase segment, and a hyphen. Nothing is * renamed: a code is a wire value and a shipped client branches on it. New * codes are dot.case. * * Not in this list, and deliberately: * * - `assistant.disabled.` is built from a template at runtime * (`src/lib/ai/capabilities/refusal.ts`, and the retired * `src/lib/feature-flags/index.ts` gate), so the last segment is open. It * is one of `coach`, `briefing`, `insightStatus`, `documentAi` and * `enabled`; `assistant.disabled.coach` is the one the iOS contract names. * - The integration-probe classes — `credentials_rejected`, `rate_limited`, * `upstream_error`, `timeout`, `connection_failed`, and the per-provider * additions beside them — are chosen by each `/test` route's own * categoriser and are already enumerated in those operations' * descriptions, where the per-provider differences can be stated. * - `IntegrationStatus.errorCode` is a different field with the same name: it * records an upstream HTTP status in the sync ledger and never reaches a * response envelope. */ export const ERROR_CODE_CATALOGUE: Readonly> = { ai: [ "ai.budget.exceeded", "ai.provider.none", "ai.record.notPermitted", "ai.unavailable", "ai.usage.aboveCeiling", "ai.usage.defaultAboveCeiling", "ai.usage.invalid", "ai.usage.levelTooLow", "ai.usage.rateLimited", ], ai_provider: ["ai_provider.invalid", "ai_provider.no_fields"], aiRuns: [ "aiRuns.failed", "aiRuns.notFound", "aiRuns.timedOut", "aiRuns.workerUnavailable", ], allergy: ["allergy.invalid"], analytics: ["analytics.invalid_query"], anamnesis: [ "anamnesis.auditC.invalid", "anamnesis.auditC.notFound", "anamnesis.auditC.rateLimited", "anamnesis.fact.conflict", "anamnesis.fact.currentExists", "anamnesis.fact.invalidValue", "anamnesis.lifeConfirm.invalid", "anamnesis.lifeSections.invalid", ], auth: [ "auth.admin.required", "auth.already_authenticated", "auth.mfa.code_invalid", "auth.missing", "auth.refresh.invalid", "auth.refresh.reuse", "auth.refresh.revoked", "auth.reproof.failed", "auth.reproof.required", "auth.reproof.too_weak", "auth.scope.insufficient", "auth.session.insecure_transport", "auth.session.unavailable", "auth.stepup.required", "auth.token.expired", "auth.token.invalid", ], backup: [ "backup.foreign_reference", "backup.key.missing", "backup.payload.undecryptable", "backup.restore.active", "backup.section.missing", "backup.upload.too_large", ], biomarkers: ["biomarkers.unit.locked"], "body-site": ["body-site.invalid"], coach: [ "coach.budget.exceeded", "coach.conversation.invalidTitle", "coach.conversation.notFound", "coach.fenced.attachmentConflict", "coach.fenced.attachmentLimit", "coach.fenced.attachmentNotFound", "coach.fenced.attachmentNotIndexed", "coach.fenced.attachmentRequired", "coach.fenced.attachmentUnavailable", "coach.fenced.invalid", "coach.request.invalid", ], "coach-prefs": [ "coach-prefs.body.invalid_json", "coach-prefs.body.invalid_shape", ], consent: ["consent.ai.confirmRequired", "consent.ai.required"], cycle: [ "cycle.bulk.invalid", "cycle.bulk.too_large", "cycle.calendar.invalid", "cycle.calendar.range", "cycle.cycles.invalid", "cycle.day-log.conflict", "cycle.day-log.invalid", "cycle.disabled", "cycle.period.invalid", "cycle.symptom.custom.invalid", "cycle.symptom.custom.limit", ], "cycle-prefs": ["cycle-prefs.body.invalid_json", "cycle-prefs.invalid"], "disable-coach": ["disable-coach.body.invalid_json"], dawarich: [ "dawarich.credentials_rejected", "dawarich.home_place_unknown", "dawarich.invalid", "dawarich.not_connected", "dawarich.rate_limited", "dawarich.unreachable", ], day: ["day.invalid"], demo: ["demo.readOnly"], documents: [ "documents.chat.conversationNotFound", "documents.chat.invalid", "documents.inbound.alreadyConfirmed", "documents.inbound.alreadyPartlyConfirmed", "documents.inbound.budgetExceeded", "documents.inbound.decryptFailed", "documents.inbound.encounterNotFound", "documents.inbound.episodeNotFound", "documents.inbound.extractFailed", "documents.inbound.factNotFound", "documents.inbound.factNotPending", "documents.inbound.factTypeMismatch", "documents.inbound.fileTooLarge", "documents.inbound.fileType", "documents.inbound.invalidMetadata", "documents.inbound.invalidQuery", "documents.inbound.invalidUpdate", "documents.inbound.localOcrDisabled", "documents.inbound.notFound", "documents.inbound.notIndexed", "documents.inbound.pdfNeedsAnthropic", "documents.inbound.providerUnsupported", "documents.inbound.quotaExceeded", "documents.inbound.rateLimited", "documents.inbound.restoreDuplicate", "documents.inbound.restoreGone", "documents.inbound.sourceAliasLimit", "documents.inbound.uploadBusy", "documents.inbound.uploadTimeout", "documents.inbound.vaccinationNotFound", "documents.sources.authRefused", "documents.sources.badResponse", "documents.sources.browserOnly", "documents.sources.invalidAddress", "documents.sources.linkTargetNotFound", "documents.sources.notConnected", "documents.sources.notFound", "documents.sources.organizationRequired", "documents.sources.originNotAllowed", "documents.sources.permissionMissing", "documents.sources.rateLimited", "documents.sources.redirected", "documents.sources.tokenRequired", "documents.sources.unavailable", "documents.sources.unreachable", "documents.sources.versionTooOld", ], encounter: [ "encounter.invalid", "encounter.link.invalid", "encounter.practitioner-not-found", "encounter.reminder-conflict", "encounter.reminder-not-found", ], encryption: ["encryption.keyBackup.stale", "encryption.key_mismatch"], environment: [ "environment.backfill_pending", "environment.backfill_rate_limited", "environment.invalid", "environment.no_home", "environment.range_too_large", "environment.travel.not_found", "environment.travel_suggestion.not_found", ], export: ["export.selection.empty", "export.selection.unknown_leaf"], familyHistory: ["familyHistory.invalid"], feedback: ["feedback.body.invalid"], health_score_config: [ "health_score_config.invalid", "health_score_config.too_narrow", ], http: [ "http.bad_gateway", "http.bad_request", "http.client_error", "http.conflict", "http.forbidden", "http.gateway_timeout", "http.gone", "http.internal", "http.invalid", "http.method_not_allowed", "http.not_found", "http.payload_too_large", "http.rate_limited", "http.server_error", "http.unauthenticated", "http.unavailable", "http.unsupported_media_type", ], illness: [ "illness.day-log.invalid", "illness.disabled", "illness.episode.chronic-no-resolve", "illness.episode.invalid", "illness.episode.invalid-window", "illness.episode.parent-not-found", "illness.insights.invalid", ], import: ["import.apple_health.busy", "import.write_failed"], ingest: ["ingest.rate_limited"], insights: [ "insights.generate.budgetExceeded", "insights.generate.outboundScreened", "insights.generate.profileScopeChanged", "insights.generate.scopeChanged", ], labs: [ "labs.biomarker.alias.taken", "labs.biomarker.create.invalid", "labs.biomarker.duplicate", "labs.biomarker.merge.invalid", "labs.biomarker.merge.unitMismatch", "labs.biomarker.notFound", "labs.biomarker.update.invalid", "labs.biomarker.update.referenceRangeInvalid", "labs.create.invalid", "labs.link.invalid", "labs.ocr.budgetExceeded", "labs.ocr.extractFailed", "labs.ocr.fileTooLarge", "labs.ocr.fileType", "labs.ocr.localOcrDisabled", "labs.ocr.pdfNeedsAnthropic", "labs.ocr.providerUnsupported", "labs.ocr.rateLimited", "labs.restore.invalid", "labs.result.notFound", "labs.unit.mismatch", "labs.update.invalid", "labs.update.linkedFieldsImmutable", "labs.update.numericExpected", "labs.update.qualitativeExpected", "labs.update.referenceRangeInvalid", "labs.update.sourceReferenceRangeInvalid", ], life_event: ["life_event.invalid", "life_event.not_found"], managed_profile: [ "managed_profile.guardian.duplicate", "managed_profile.guardian.managed_invitee", "managed_profile.guardian.required", "managed_profile.handover.no_pending", "managed_profile.handover.unknown_guardian", "managed_profile.not_found", ], maintenance: [ "maintenance.pulseDayMean.busy", "maintenance.pulseDayMean.dryRunRequired", ], measurement: [ "measurement.batch.invalid", "measurement.batch.source_not_permitted", "measurement.batch.too_large", "measurement.bulk-delete.invalid", "measurement.create.invalid", "measurement.create.source_not_permitted", "measurement.delete.too_large", "measurement.duplicate_timestamp", "measurement.restore.invalid", "measurement.update.server_owned_source", ], medication: [ "medication.intake.import.invalid_format", "medication.intake.notTracked", "medication.link.invalid", ], medications: [ "medications.category.invalid", "medications.category.limitReached", "medications.category.unknown", "medications.course.currentOpen", "medications.course.invalid", "medications.course.invalidRange", "medications.course.notFound", "medications.course.oneShot", "medications.course.overlap", "medications.course.windowRequired", "medications.ingredients.extract.budgetExceeded", "medications.ingredients.extract.failed", "medications.ingredients.extract.fileTooLarge", "medications.ingredients.extract.fileType", "medications.ingredients.extract.rateLimited", "medications.ingredients.invalid", "medications.ingredients.unitMismatch", "medications.intake.bulk.apple_health_not_mirrored", "medications.intake.bulk.invalid", "medications.intake.bulk.too_large", "medications.intake.force_slot.invalid", "medications.intake.force_slot.occupied", "medications.intake.injection_site.disallowed", "medications.intake.taken_at.before_start", "medications.mirror.limit_exceeded", "medications.notFound", ], mentalHealth: ["mentalHealth.rateLimited"], module: ["module.disabled"], modules: ["modules.body.invalid_json", "modules.invalid"], mood: [ "mood.bulk-delete.invalid", "mood.bulk.invalid", "mood.bulk.too_large", "mood.create.external_id_not_delegable", "mood.create.invalid", "mood.duplicate_timestamp", "mood.linkedContext.invalid", "mood.list.invalid", "mood.not_found", "mood.ratedFactor.out_of_range", "mood.restore.invalid", "mood.update.invalid", ], "notification-prefs": ["notification-prefs.body.invalid_json"], notification: [ "notification.action.already_used", "notification.action.expired", "notification.action.invalid", "notification.action.module_disabled", ], nutrient: [ "nutrient.batch.invalid", "nutrient.batch.too_large", "nutrient.daily.invalid", "nutrient.read.invalid", "nutrient.water.invalid", "nutrient.water.invalid_day", ], onboarding: [ "onboarding.answers.invalid", "onboarding.answers.rateLimited", "onboarding.complete.invalid", "onboarding.complete.recordNotFound", "onboarding.complete.recordTargetMismatch", "onboarding.disclaimer.invalid", "onboarding.restart.invalid", "onboarding.restart.rateLimited", "onboarding.tour.invalid", ], passkey: [ "passkey.challenge.expired", "passkey.response.invalid", "passkey.unknown", "passkey.verification.failed", ], personal_records: ["personal_records.invalid_query"], practitioner: ["practitioner.invalid"], profile: [ "profile.update.emailInUse", "profile.update.emailRateLimited", "profile.update.emailSecondFactorRequired", "profile.update.invalidBody", "profile.update.nothingSaved", ], profile_claim: [ "profile_claim.invalid", "profile_claim.oidc_only_unsupported", "profile_claim.sso_unavailable", "profile_claim.taken", ], record_write: ["record_write.rate_limited"], "report-selection": [ "report-selection.body.invalid_json", "report-selection.body.invalid_shape", "report-selection.leaves.unknown", ], route: ["route.retired"], search: ["search.invalid"], "share-link": [ "share-link.document.decryptFailed", "share-link.document.notFound", "share-link.document.rateLimited", "share-link.documents.invalid", "share-link.report.notFound", "share-link.report.rateLimited", "share-link.selection.forbidden_leaf", "share-link.selection.unknown_leaf", ], sharing: [ "sharing.accept.expired", "sharing.accept.not_found", "sharing.accept.not_pending", "sharing.accept.revoked", "sharing.access.denied", "sharing.invite.duplicate", "sharing.invite.invalid", "sharing.invite.invalid_scope", "sharing.invite.manage_browser_only", "sharing.invite.refused", "sharing.invite.self", "sharing.not_permitted", "sharing.renounce.already_ended", "sharing.renounce.not_found", "sharing.revoke.already_ended", "sharing.revoke.not_found", "sharing.session.changed", "sharing.switch.invalid", "sharing.switch.wrong_transport", ], "source-priority": [ "source-priority.body.invalid_json", "source-priority.body.invalid_shape", ], symptoms: [ "symptoms.definition.limitReached", "symptoms.definition.notFound", "symptoms.episode.notOpen", "symptoms.event.notFound", "symptoms.invalid", ], timeline: ["timeline.invalid"], tokens: [ "tokens.documents.ceiling_reached", "tokens.measurements.ceiling_reached", "tokens.workouts.ceiling_reached", ], vaccination: [ "vaccination.booster-invalid", "vaccination.booster-no-antigen", "vaccination.booster-out-of-scope", "vaccination.custom-vaccine-not-found", "vaccination.custom.invalid", "vaccination.custom.name-taken", "vaccination.custom.not-found", "vaccination.encounter-not-found", "vaccination.identity-required", "vaccination.invalid", "vaccination.link.invalid", "vaccination.not-found", "vaccination.practitioner-not-found", ], workout: [ "workout.batch.invalid", "workout.batch.payload_too_large", "workout.batch.source_not_permitted", "workout.batch.too_large", "workout.delete.synced_source", ], "(unprefixed)": [ "about_me_conflict", "ai_response_truncated", "coach_prefs_conflict", "dashboard_layout_conflict", "empty_file", "health_score_config_conflict", "insights_layout_conflict", "invalid_base_updated_at", "json_carries_no_intake_time", "json_not_an_array", "medication_layout_conflict", "missing_required_columns", "modules_conflict", "mood_tag_layout_conflict", "not_configured", "notification_prefs_conflict", "oidc_only", "private_origin_not_approved", "private_origin_not_grantable", "rate_limited_self", "sync_in_progress", "too_many_rows", "unreadable_json", "upstream_invalid_json", "vapid_not_configured", ], } as const; /** * The catalogue as one sentence for the published `meta.errorCode` description. * * Rendered rather than hand-written so the document cannot drift from the list * the guard checks: there is one source, and it is the object above. */ export function renderErrorCodeCatalogue(): string { return Object.entries(ERROR_CODE_CATALOGUE) .map(([surface, codes]) => `${surface} — ${codes.join(", ")}`) .join("; "); }