mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-07 04:12:04 +00:00
528 lines
18 KiB
TypeScript
528 lines
18 KiB
TypeScript
import { DEFAULT_ERROR_CODES } from "@/lib/api/default-error-codes";
|
|
|
|
/**
|
|
* 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.<switch>` 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<Record<string, readonly string[]>> =
|
|
{
|
|
account: ["account.deletion.no_guardian", "account.deletion.not_pending"],
|
|
activity: ["activity.calendar.future_date", "activity.calendar.too_old"],
|
|
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.readerBusy",
|
|
"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.account.deletion_pending",
|
|
"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"],
|
|
data_wipe: ["data_wipe.copy_failed"],
|
|
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.notDeleted",
|
|
"documents.inbound.notFound",
|
|
"documents.inbound.notIndexed",
|
|
"documents.inbound.outputLimit",
|
|
"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.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",
|
|
],
|
|
// The status defaults, derived from the table `apiHandler` applies so the
|
|
// two cannot drift apart.
|
|
http: [...new Set(Object.values(DEFAULT_ERROR_CODES))].sort(),
|
|
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.csv.content_type",
|
|
"import.csv.empty",
|
|
"import.csv.too_large",
|
|
"import.rate_limited",
|
|
"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.outputLimit",
|
|
"labs.ocr.pdfNeedsAnthropic",
|
|
"labs.ocr.providerUnsupported",
|
|
"labs.ocr.rateLimited",
|
|
"labs.ocr.tooLarge",
|
|
"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.notRunning",
|
|
"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"],
|
|
recovery: ["recovery.kit.stale"],
|
|
"report-selection": [
|
|
"report-selection.body.invalid_json",
|
|
"report-selection.body.invalid_shape",
|
|
"report-selection.leaves.unknown",
|
|
],
|
|
request: ["request.cross_site"],
|
|
route: ["route.retired"],
|
|
search: ["search.invalid"],
|
|
security_activity: ["security_activity.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.no_account",
|
|
"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.mirror.not_measurement_token",
|
|
"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",
|
|
"provider_retired",
|
|
"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("; ");
|
|
}
|