diff --git a/README.md b/README.md index 47d1713..2232886 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ Current application stack: - Frontend: Next.js + TypeScript + Tailwind - Backend: FastAPI + SQLModel -- Data: SQLite +- Data: SQLite for local development and tests; PostgreSQL is required for live user data - External food data: Open Food Facts - Identity/authentication: server-side identity flow with session cookies @@ -53,7 +53,8 @@ CalorieApp V1 is intentionally centralized and scope-restricted. 1. Next.js frontend provides UI and user interaction flows. 2. FastAPI backend provides API behavior and business/data logic. -3. SQLite persists current application data. +3. SQLite persists local development and test data. Public user onboarding is + blocked until the PostgreSQL durable-data release gates pass. 4. Open Food Facts is used as the external food data source. 5. Identity/authentication is handled through backend-managed session flow. @@ -71,6 +72,15 @@ CalorieApp V1 is not: The V1 scope is food and nutrition tracking only. +Infrastructure follows a one-provider-per-role policy. Optional provenance is +designed for the same PostgreSQL primary store and does not add a graph database, +blockchain database or IPFS dependency to the core release. + +Repeatable tests, schema checks, staging restore drills and future scoped ledger +verification are automation-ready. Production schema changes, privacy-purpose +expansion, XRPL enablement, deployment and publication retain explicit approval +gates. + No wallet custody or financial transaction layer is claimed in V1. See [REGULATORY.md](REGULATORY.md) for the MiCA and financial-services boundary. @@ -185,10 +195,15 @@ gate can additionally run the local developer health check. ## Documentation - Versioned Identity Bridge contracts: contracts/identity-bridge/v1/ +- XRPL-linked provenance contract: contracts/provenance/v1/ - Historical image localization contract: contracts/localization/v1/ - Public architecture: docs/public/architecture.md - Public roadmap: docs/public/roadmap.md - Public deployment guide: docs/public/deployment.md +- Durable data and privacy foundation: docs/DURABLE_DATA_FOUNDATION.md +- Voluntary XRPL transaction-linking architecture: docs/XRPL_TRANSACTION_LINKING.md +- Public data-safety direction: docs/public/data-safety.md +- Public XRPL reference direction: docs/public/xrpl-linking.md - Public release readiness checklist: docs/public/release-readiness.md - Public identity overview: docs/public/identity.md diff --git a/backend/README.md b/backend/README.md index 6b1e828..300673b 100644 --- a/backend/README.md +++ b/backend/README.md @@ -56,5 +56,7 @@ If you see inconsistent API responses: ## Notes -- Data storage uses local SQLite via SQLModel for MVP persistence. +- Data storage uses local SQLite via SQLModel for development and tests only. +- Public user onboarding remains blocked until the durable PostgreSQL, + migration, persistence, export, erasure and recovery gates pass. - Open Food Facts is consumed only by backend service endpoints. diff --git a/backend/tests/test_data_safety_contract.py b/backend/tests/test_data_safety_contract.py new file mode 100644 index 0000000..6d01f1a --- /dev/null +++ b/backend/tests/test_data_safety_contract.py @@ -0,0 +1,179 @@ +from __future__ import annotations + +import json +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[2] +CONTRACT_DIR = ROOT / "contracts" / "data-safety" / "v1" + + +def _load_json(name: str) -> dict: + return json.loads((CONTRACT_DIR / name).read_text(encoding="utf-8")) + + +def test_data_safety_contract_keeps_live_history_off_sqlite() -> None: + contract = _load_json("data-safety.json") + + assert contract["contract_id"] == "calorieapp.durable-data-safety" + assert contract["release_state"] == "blocked" + assert contract["architecture"]["primary_live_store"] == "postgresql" + assert contract["architecture"]["sqlite_allowed_environments"] == ["local", "test"] + assert contract["architecture"]["sqlite_allowed_for_public_live_history"] is False + assert contract["architecture"]["provider_driven_history_expiry_allowed"] is False + assert contract["architecture"]["formal_schema_migrations_required"] is True + + +def test_data_safety_contract_forbids_public_personal_data_replication() -> None: + boundary = _load_json("data-safety.json")["decentralized_boundary"] + + assert boundary["personal_data_on_public_blockchain_allowed"] is False + assert boundary["personal_data_on_public_ipfs_allowed"] is False + assert boundary["public_cid_treated_as_private"] is False + assert boundary["current_release_dependency"] is False + assert "non-reversible" in boundary["allowed_chain_record"] + + +def test_data_classes_cover_current_and_planned_personal_flows() -> None: + data_classes = { + item["id"]: item for item in _load_json("data-safety.json")["data_classes"] + } + + assert set(data_classes) == { + "food_history", + "identity_link", + "authentication_transient", + "food_search_query", + "voluntary_profile", + "donation_contact", + "xrpl_transaction_reference", + "calorie_record_fingerprint", + } + assert data_classes["food_search_query"]["persistent_storage_allowed"] is False + assert data_classes["food_search_query"]["calorieapp_identity_forwarded"] is False + assert data_classes["voluntary_profile"]["explicit_choice_required"] is True + assert data_classes["donation_contact"]["purpose_separation_required"] is True + assert data_classes["xrpl_transaction_reference"]["primary_key"] == [ + "network", + "transaction_hash", + ] + assert ( + data_classes["calorie_record_fingerprint"][ + "plain_record_hash_allowed_on_public_ledger" + ] + is False + ) + + +def test_xrpl_linking_is_optional_off_chain_and_privacy_preserving() -> None: + contract = _load_json("data-safety.json") + linking = contract["xrpl_transaction_linking"] + memo = linking["memo"] + + assert linking["status"] == "planned-disabled-by-default" + assert linking["current_release_dependency"] is False + assert linking["initial_user_facing_feature"] is False + assert linking["canonical_anchor"] == ["network", "transaction_hash"] + assert linking["accepted_ledger_state"] == "validated" + assert linking["database_link_is_off_chain"] is True + assert linking["link_requires_explicit_user_action"] is True + assert linking["automatic_wallet_history_profiling_allowed"] is False + assert linking["wallet_key_or_custody_access_allowed"] is False + assert memo["personal_data_allowed"] is False + assert memo["stable_user_identifier_allowed"] is False + assert memo["raw_database_identifier_allowed"] is False + assert memo["plain_record_hash_allowed"] is False + relation = linking["hash_relation_model"] + assert relation["public_anchor"] == ["network", "transaction_hash"] + assert relation["paired_caloriedb_anchor"] == "calorie_anchor_hash" + assert relation["top_pair_cardinality"] == "one-to-one" + assert relation["private_record_anchor"] == "calorie_record_fingerprint_id" + assert relation["join_entity"] == "ledger_record_link" + assert relation["transaction_hash_equals_record_hash"] is False + assert relation["anchor_to_record_many_to_many_edges_allowed"] is True + assert relation["explicit_authorization_required_per_edge"] is True + assert relation["cross_purpose_auto_linking_allowed"] is False + assert relation["public_hash_resolver_may_return_private_data"] is False + assert linking["unlinking"]["off_chain_association_deletable"] is True + assert linking["unlinking"]["on_chain_transaction_deletable"] is False + + +def test_xrpl_linking_cannot_claim_automatic_worldwide_compliance() -> None: + boundary = _load_json("data-safety.json")["global_compliance_boundary"] + + assert boundary["worldwide_compliance_claim_allowed"] is False + assert boundary["jurisdiction_feature_gates_required"] is True + assert boundary["data_protection_impact_assessment_required_before_enablement"] is True + assert boundary["independent_financial_regulatory_review_required_before_enablement"] is True + assert boundary["transaction_execution_or_routing_enabled"] is False + + +def test_platform_budget_prevents_duplicate_core_services() -> None: + platforms = _load_json("data-safety.json")["platform_minimization"] + + assert platforms["duplicate_identity_platform_allowed"] is False + assert platforms["second_primary_database_allowed"] is False + assert platforms["separate_graph_database_required"] is False + assert platforms["blockchain_database_required"] is False + assert platforms["ipfs_or_filecoin_required_for_core_release"] is False + assert platforms["new_provider_requires_architecture_record"] is True + assert platforms["roles"]["optional_ledger_reference"] == "XRPL only" + + +def test_responsible_automation_keeps_human_release_and_privacy_gates() -> None: + automation = _load_json("data-safety.json")["responsible_automation"] + + assert "test-and-build-checks" in automation["automated_by_default"] + assert "scheduled-staging-restore-drills" in automation["automated_by_default"] + assert "localization-completeness-checks" in automation["automated_by_default"] + assert "identity-purpose-expansion" in automation["approval_required"] + assert "xrpl-feature-enablement" in automation["approval_required"] + assert "public-content-publication" in automation["approval_required"] + assert automation["production_automation_runs_only_after_approval"] is True + assert automation["idempotent_and_retry_safe_required"] is True + assert automation["automatic_publication_allowed"] is False + assert automation["automatic_financial_action_allowed"] is False + + +def test_all_required_durable_data_release_gates_are_explicit_and_blocking() -> None: + matrix = _load_json("release-test-matrix.json") + gates = {gate["id"]: gate for gate in matrix["gates"]} + expected = { + "provider_neutral_postgresql_configuration", + "production_sqlite_fail_closed", + "formal_schema_migrations", + "owner_isolation", + "restart_persistence", + "redeploy_persistence", + "backup_restore_drill", + "user_data_export", + "user_erasure", + "retention_policy", + "privacy_notice_alignment", + "no_personal_data_in_decentralized_public_storage", + } + + assert matrix["contract_id"] == "calorieapp.durable-data-release-gates" + assert set(gates) == expected + assert all(gate["release_blocking"] is True for gate in gates.values()) + assert all(gate["status"] in matrix["statuses"] for gate in gates.values()) + assert gates["owner_isolation"]["status"] == "verified" + assert gates["production_sqlite_fail_closed"]["status"] == "not_started" + assert gates["retention_policy"]["status"] == "decision_required" + assert matrix["release_state"] == "blocked" + + +def test_contract_release_order_ends_with_review_and_explicit_publication_go() -> None: + contract = _load_json("data-safety.json") + release_order = contract["release_order"] + + assert release_order[-1] == "showcase-preview-review-explicit-go-scheduled-publish" + assert release_order.index("automation-and-observability-foundation") < release_order.index( + "formal-migrations" + ) + assert release_order.index("privacy-review") < release_order.index( + "identity-feature-expansion" + ) + optional_order = contract["optional_future_order"] + assert optional_order[0] == "xrpl-schema-compatibility-review" + assert optional_order[-1] == "adoption-led-scaling" diff --git a/backend/tests/test_provenance_contract.py b/backend/tests/test_provenance_contract.py new file mode 100644 index 0000000..6314382 --- /dev/null +++ b/backend/tests/test_provenance_contract.py @@ -0,0 +1,109 @@ +from __future__ import annotations + +import json +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[2] +CONTRACT = ROOT / "contracts" / "provenance" / "v1" / "traceability.json" + + +def _contract() -> dict: + return json.loads(CONTRACT.read_text(encoding="utf-8")) + + +def test_provenance_is_future_ready_without_becoming_a_launch_feature() -> None: + contract = _contract() + rollout = contract["rollout"] + + assert contract["contract_id"] == "caloriedb.xrpl-linked-provenance" + assert rollout["core_public_release_dependency"] is False + assert rollout["initial_user_facing_feature"] is False + assert rollout["initial_wallet_or_ledger_scan"] is False + assert rollout["initial_transaction_or_memo_ui"] is False + assert "disabled feature flag" in rollout["phase_1"] + assert "testnet or synthetic-data" in rollout["phase_2"] + + platform = contract["platform_boundary"] + assert platform["stored_in_primary_postgresql"] is True + assert platform["separate_graph_database_required"] is False + assert platform["additional_blockchain_required"] is False + assert platform["ipfs_required"] is False + + +def test_provenance_automation_is_scoped_idempotent_and_disabled_by_default() -> None: + automation = _contract()["automation_boundary"] + + assert automation["feature_flag_default"] == "disabled" + assert automation["explicit_purpose_scoped_link_request_required"] is True + assert automation["automatic_complete_wallet_scan_allowed"] is False + assert automation["automatic_cross_purpose_linking_allowed"] is False + assert automation["single_requested_transaction_verification_may_be_automated"] is True + assert automation["idempotent_ingestion_key"] == ["network", "transaction_hash"] + assert automation["retry_safe_processing_required"] is True + assert automation["production_enablement_requires_human_approval"] is True + assert automation["jurisdiction_gate_enforced_before_processing"] is True + + +def test_top_anchor_is_one_to_one_from_xrpl_hash_to_caloriedb_hash() -> None: + anchor = _contract()["top_anchor"] + + assert anchor["xrpl_key"] == ["network", "transaction_hash"] + assert anchor["caloriedb_key"] == "calorie_anchor_hash" + assert anchor["cardinality"] == "one-to-one" + assert anchor["hashes_are_equal"] is False + assert ["network", "transaction_hash"] in anchor["unique_constraints"] + assert ["calorie_anchor_hash"] in anchor["unique_constraints"] + assert "HMAC-SHA-256" in anchor["calorie_anchor_derivation"] + + +def test_calorie_transaction_match_uses_exact_asset_identity_not_label_or_memo() -> None: + scope = _contract()["calorie_asset_scope"] + + assert scope["exact_asset_registry_required"] is True + assert scope["registry_key"] == ["network", "issuer", "currency_code"] + assert scope["symbol_or_memo_only_match_allowed"] is False + assert scope["validated_transaction_and_metadata_required"] is True + assert scope["anchor_requires_calorie_relevance_evidence"] is True + assert scope["initial_pilot_transaction_types"] == ["Payment"] + assert scope["rule_version_stored_per_anchor"] is True + + +def test_traceability_layers_start_at_hash_pair_before_events_and_lots() -> None: + layers = _contract()["layers_top_down"] + + assert [layer["level"] for layer in layers] == [0, 1, 2, 3, 4] + assert [layer["entity"] for layer in layers] == [ + "validated_xrpl_transaction", + "calorie_transaction_anchor", + "provenance_event", + "product_lot_or_batch", + "trace_view", + ] + assert layers[1]["relation_to_parent"] == "exactly-one" + + +def test_supply_trace_is_a_gap_preserving_dag() -> None: + graph = _contract()["graph_rules"] + + assert graph["shape"] == "directed-acyclic-graph" + assert graph["simple_linear_chain_assumed"] is False + assert graph["splits_supported"] is True + assert graph["merges_supported"] is True + assert graph["cycles_allowed"] is False + assert graph["missing_edges_reported_as_gaps"] is True + assert graph["missing_edges_inferred_or_fabricated"] is False + + +def test_ledger_hash_does_not_claim_to_prove_physical_food_truth() -> None: + contract = _contract() + truth = contract["truth_boundary"] + privacy = contract["privacy_and_visibility"] + + assert truth["physical_claim_requires_separate_evidence"] is True + assert "does not prove" not in truth["transaction_proves"] + assert "physical food product" in truth["transaction_does_not_prove"] + assert privacy["consumer_events_private_by_default"] is True + assert privacy["personal_food_history_public_by_default"] is False + assert privacy["public_transaction_lookup_may_return_private_record"] is False + assert privacy["cross_purpose_auto_linking_allowed"] is False diff --git a/contracts/data-safety/v1/README.md b/contracts/data-safety/v1/README.md new file mode 100644 index 0000000..72ad6f0 --- /dev/null +++ b/contracts/data-safety/v1/README.md @@ -0,0 +1,24 @@ +# Durable data safety contract v1 + +This directory defines the release-blocking data-safety boundary for CalorieApp. +It separates durable private application data from optional decentralized +research and records which controls are already verified, partial, planned or +still require an explicit decision. + +The contract does not contain credentials, provider-specific endpoints or live +user data. Passing its automated checks is necessary but is not by itself a +legal, privacy or security certification. + +Files: + +- `data-safety.json`: architecture, data classes, retention boundaries and + prohibited storage patterns. +- `release-test-matrix.json`: auditable status of every release-blocking gate. + +Status vocabulary: + +- `verified`: implemented and covered by the cited automated evidence. +- `partial`: some foundations exist, but the release gate is not complete. +- `not_started`: required implementation and verification remain outstanding. +- `decision_required`: implementation must wait for an explicit policy choice. +- `research_only`: not part of the current production architecture. diff --git a/contracts/data-safety/v1/data-safety.json b/contracts/data-safety/v1/data-safety.json new file mode 100644 index 0000000..9d8d00e --- /dev/null +++ b/contracts/data-safety/v1/data-safety.json @@ -0,0 +1,239 @@ +{ + "contract_id": "calorieapp.durable-data-safety", + "contract_version": "1.0.0", + "release_state": "blocked", + "purpose": "Keep authenticated food history durable, private, portable, exportable and deletable before public user onboarding.", + "architecture": { + "primary_live_store": "postgresql", + "provider_selection": "pending-staging-evaluation", + "provider_neutral_configuration": "DATABASE_URL", + "sqlite_allowed_environments": ["local", "test"], + "sqlite_allowed_for_public_live_history": false, + "formal_schema_migrations_required": true, + "provider_driven_history_expiry_allowed": false + }, + "data_classes": [ + { + "id": "food_history", + "status": "implemented", + "sensitivity": "personal", + "examples": ["product", "nutrition values", "portion", "timestamp"], + "owner_binding": "calorieapp_user_id", + "user_export_required": true, + "user_delete_required": true + }, + { + "id": "identity_link", + "status": "implemented", + "sensitivity": "personal-identifier", + "examples": ["internal user id", "external subject", "optional XRPL address"], + "data_minimization_required": true, + "unlink_or_account_delete_required": true + }, + { + "id": "authentication_transient", + "status": "implemented", + "sensitivity": "security-sensitive", + "examples": ["hashed session token", "hashed login state", "nonce", "authorization metadata"], + "short_lived": true, + "automatic_cleanup_required": true + }, + { + "id": "food_search_query", + "status": "implemented", + "sensitivity": "potential-free-form-personal-input", + "examples": ["product search text"], + "external_service": "Open Food Facts", + "persistent_storage_allowed": false, + "calorieapp_identity_forwarded": false + }, + { + "id": "voluntary_profile", + "status": "planned", + "sensitivity": "personal", + "examples": ["display name", "voluntary profile fields"], + "explicit_choice_required": true, + "disabled_by_default": true + }, + { + "id": "donation_contact", + "status": "planned", + "sensitivity": "personal", + "examples": ["email address", "optional donor details"], + "purpose_separation_required": true, + "data_minimization_required": true + }, + { + "id": "xrpl_transaction_reference", + "status": "planned", + "sensitivity": "public-ledger-reference-that-becomes-personal-when-linked", + "examples": ["XRPL network", "transaction hash", "validated ledger index", "participant role"], + "primary_key": ["network", "transaction_hash"], + "validated_transactions_only": true, + "raw_transaction_replication_allowed": false + }, + { + "id": "calorie_record_fingerprint", + "status": "planned", + "sensitivity": "derived-personal-data", + "examples": ["private HMAC fingerprint", "one-time salted commitment"], + "plain_record_hash_allowed_on_public_ledger": false, + "domain_separation_required": true, + "key_or_random_salt_required": true + } + ], + "retention": { + "food_history": "Retain for the user's active history until user deletion, account closure, or a separately approved and disclosed inactivity policy.", + "identity_link": "Retain only while required for the CalorieApp account and delete or unlink through the approved account-erasure flow.", + "authentication_transient": "Remove after the security retention window; expired records must not remain indefinitely.", + "backups": "Use a documented encrypted backup retention schedule and propagate erasure according to that schedule.", + "unresolved_release_decisions": [ + "inactive-account retention and notice period", + "backup retention and deletion schedule", + "account export format and delivery safeguards", + "account erasure confirmation and recovery window" + ] + }, + "backup_and_recovery": { + "encrypted_at_rest_and_in_transit_required": true, + "restricted_operator_access_required": true, + "automated_backup_required": true, + "documented_restore_procedure_required": true, + "successful_staging_restore_drill_required": true, + "backup_without_restore_test_counts_as_ready": false + }, + "decentralized_boundary": { + "personal_data_on_public_blockchain_allowed": false, + "personal_data_on_public_ipfs_allowed": false, + "public_cid_treated_as_private": false, + "optional_encrypted_user_export": "research-only and explicit opt-in", + "allowed_chain_record": "non-reversible integrity commitment without personal data or stable user identifier", + "current_release_dependency": false + }, + "xrpl_transaction_linking": { + "status": "planned-disabled-by-default", + "current_release_dependency": false, + "initial_user_facing_feature": false, + "initial_database_use": "empty migration-ready tables only after the core durable-data migrations pass", + "canonical_anchor": ["network", "transaction_hash"], + "transaction_hash_format": "64 uppercase hexadecimal characters", + "accepted_ledger_state": "validated", + "database_link_is_off_chain": true, + "link_requires_explicit_user_action": true, + "automatic_wallet_history_profiling_allowed": false, + "wallet_key_or_custody_access_allowed": false, + "memo": { + "allowed_content": "versioned one-time opaque random challenge only", + "personal_data_allowed": false, + "stable_user_identifier_allowed": false, + "raw_database_identifier_allowed": false, + "plain_record_hash_allowed": false, + "user_must_be_warned_that_memo_is_public_and_irreversible": true + }, + "database_hashes": { + "private_record_fingerprint": "HMAC-SHA-256 over versioned canonical record data with key version and domain separation", + "optional_public_commitment": "SHA-256 over domain separator, canonicalization version, private record digest and 256-bit random salt", + "commitment_salt_stored_on_chain": false, + "commitment_salt_encrypted_off_chain": true + }, + "hash_relation_model": { + "public_anchor": ["network", "transaction_hash"], + "paired_caloriedb_anchor": "calorie_anchor_hash", + "top_pair_cardinality": "one-to-one", + "private_record_anchor": "calorie_record_fingerprint_id", + "join_entity": "ledger_record_link", + "optional_private_group_entity": "calorie_link_group", + "transaction_hash_equals_record_hash": false, + "anchor_to_record_many_to_many_edges_allowed": true, + "explicit_authorization_required_per_edge": true, + "cross_purpose_auto_linking_allowed": false, + "public_hash_resolver_may_return_private_data": false, + "provenance_contract": "contracts/provenance/v1/traceability.json" + }, + "unlinking": { + "off_chain_association_deletable": true, + "on_chain_transaction_deletable": false, + "opaque_memo_must_become_orphaned_after_unlink": true + } + }, + "global_compliance_boundary": { + "worldwide_compliance_claim_allowed": false, + "jurisdiction_feature_gates_required": true, + "data_protection_impact_assessment_required_before_enablement": true, + "independent_financial_regulatory_review_required_before_enablement": true, + "functional_regulatory_classification_required": true, + "read_only_reference_mode_preferred": true, + "transaction_execution_or_routing_enabled": false + }, + "platform_minimization": { + "principle": "one provider per necessary role unless separation has a documented security or recovery benefit", + "roles": { + "website_and_identity": "existing WordPress environment", + "source_and_ci": "GitHub", + "app_runtime": "one runtime provider for frontend and backend where practical", + "primary_private_data": "one provider-neutral PostgreSQL service", + "external_food_data": "Open Food Facts", + "optional_ledger_reference": "XRPL only" + }, + "duplicate_identity_platform_allowed": false, + "second_primary_database_allowed": false, + "separate_graph_database_required": false, + "blockchain_database_required": false, + "ipfs_or_filecoin_required_for_core_release": false, + "new_provider_requires_architecture_record": true, + "backup_separation_exception": "allowed only when required for recoverability and documented before use" + }, + "responsible_automation": { + "principle": "automate repeatable evidence and operations; require explicit approval for irreversible, privacy-sensitive, financial or public actions", + "automated_by_default": [ + "contract-validation", + "schema-drift-detection", + "test-and-build-checks", + "dependency-and-secret-scanning", + "staging-migration-verification", + "database-readiness-and-persistence-checks", + "encrypted-backup-creation", + "scheduled-staging-restore-drills", + "retention-job-dry-runs-and-auditable-execution", + "localization-completeness-checks" + ], + "approval_required": [ + "production-schema-change", + "retention-or-erasure-policy-change", + "identity-purpose-expansion", + "xrpl-feature-enablement", + "transaction-execution-or-routing", + "public-content-publication", + "production-deployment" + ], + "production_automation_runs_only_after_approval": true, + "idempotent_and_retry_safe_required": true, + "audit_log_without_secrets_or_excess_personal_data_required": true, + "health_check_and_failure_alert_required": true, + "automatic_publication_allowed": false, + "automatic_financial_action_allowed": false + }, + "release_order": [ + "identity-baseline-review", + "data-policy-decisions", + "automation-and-observability-foundation", + "formal-migrations", + "production-database-guard", + "postgresql-staging", + "persistence-and-isolation-tests", + "backup-restore-drill", + "export-and-erasure", + "privacy-review", + "identity-feature-expansion", + "website-and-localization", + "showcase-preview-review-explicit-go-scheduled-publish" + ], + "optional_future_order": [ + "xrpl-schema-compatibility-review", + "disabled-empty-provenance-tables", + "testnet-or-synthetic-pilot", + "jurisdiction-and-privacy-review", + "purpose-scoped-production-pilot", + "adoption-led-scaling" + ] +} diff --git a/contracts/data-safety/v1/release-test-matrix.json b/contracts/data-safety/v1/release-test-matrix.json new file mode 100644 index 0000000..86f49ca --- /dev/null +++ b/contracts/data-safety/v1/release-test-matrix.json @@ -0,0 +1,97 @@ +{ + "contract_id": "calorieapp.durable-data-release-gates", + "contract_version": "1.0.0", + "release_state": "blocked", + "statuses": ["verified", "partial", "not_started", "decision_required", "research_only"], + "gates": [ + { + "id": "provider_neutral_postgresql_configuration", + "status": "partial", + "release_blocking": true, + "evidence": ["backend/app/database.py", "backend/requirements.txt", "backend/tests/test_database.py"] + }, + { + "id": "production_sqlite_fail_closed", + "status": "not_started", + "release_blocking": true, + "evidence": [] + }, + { + "id": "formal_schema_migrations", + "status": "not_started", + "release_blocking": true, + "evidence": [] + }, + { + "id": "owner_isolation", + "status": "verified", + "release_blocking": true, + "evidence": ["backend/tests/test_endpoints.py", "backend/tests/test_identity_endpoints.py"] + }, + { + "id": "restart_persistence", + "status": "not_started", + "release_blocking": true, + "evidence": [] + }, + { + "id": "redeploy_persistence", + "status": "not_started", + "release_blocking": true, + "evidence": [] + }, + { + "id": "backup_restore_drill", + "status": "not_started", + "release_blocking": true, + "evidence": [] + }, + { + "id": "user_data_export", + "status": "not_started", + "release_blocking": true, + "evidence": [] + }, + { + "id": "user_erasure", + "status": "partial", + "release_blocking": true, + "evidence": ["backend/app/main.py", "backend/tests/test_endpoints.py"] + }, + { + "id": "retention_policy", + "status": "decision_required", + "release_blocking": true, + "evidence": ["contracts/data-safety/v1/data-safety.json"] + }, + { + "id": "privacy_notice_alignment", + "status": "not_started", + "release_blocking": true, + "evidence": [] + }, + { + "id": "no_personal_data_in_decentralized_public_storage", + "status": "verified", + "release_blocking": true, + "evidence": ["contracts/data-safety/v1/data-safety.json", "README.md"] + } + ], + "optional_research": [ + { + "id": "encrypted_user_controlled_ipfs_export", + "status": "research_only", + "release_blocking": false + }, + { + "id": "xrpl_integrity_commitment", + "status": "research_only", + "release_blocking": false + }, + { + "id": "xrpl_transaction_reference_link", + "status": "research_only", + "release_blocking": false + } + ] +} diff --git a/contracts/provenance/v1/README.md b/contracts/provenance/v1/README.md new file mode 100644 index 0000000..c51f729 --- /dev/null +++ b/contracts/provenance/v1/README.md @@ -0,0 +1,14 @@ +# XRPL-linked provenance contract v1 + +This contract starts with a strict one-to-one pair between a validated XRPL +transaction reference and a unique CalorieDB anchor hash. All product, batch, +event and evidence relations are modelled below that pair. + +The contract is architecture-only and disabled by default. It does not claim +that an XRPL payment proves a physical food event, and it does not enable +custody, signing, transfers, exchange or order routing. + +It is intentionally small at the beginning. The first public users do not need +to see or use this feature. After the durable PostgreSQL and formal migration +gates pass, empty foundational tables can be added behind a disabled feature +flag so later adoption does not require redesigning the core database. diff --git a/contracts/provenance/v1/traceability.json b/contracts/provenance/v1/traceability.json new file mode 100644 index 0000000..4039ba8 --- /dev/null +++ b/contracts/provenance/v1/traceability.json @@ -0,0 +1,205 @@ +{ + "contract_id": "caloriedb.xrpl-linked-provenance", + "contract_version": "1.0.0", + "status": "architecture-only-disabled-by-default", + "rollout": { + "core_public_release_dependency": false, + "initial_user_facing_feature": false, + "initial_wallet_or_ledger_scan": false, + "initial_transaction_or_memo_ui": false, + "phase_0": "freeze identifiers, boundaries and migration-ready schema contract", + "phase_1": "after durable PostgreSQL and formal migrations, add empty tables behind a disabled feature flag", + "phase_2": "run an explicitly approved testnet or synthetic-data pilot", + "phase_3": "enable purpose-scoped production pilots only after privacy, jurisdiction and operational review", + "phase_4": "scale the provenance graph only when real supply-chain adoption requires it" + }, + "platform_boundary": { + "stored_in_primary_postgresql": true, + "separate_graph_database_required": false, + "additional_blockchain_required": false, + "ipfs_required": false, + "xrpl_role": "optional validated transaction anchor only" + }, + "automation_boundary": { + "feature_flag_default": "disabled", + "explicit_purpose_scoped_link_request_required": true, + "automatic_complete_wallet_scan_allowed": false, + "automatic_cross_purpose_linking_allowed": false, + "single_requested_transaction_verification_may_be_automated": true, + "idempotent_ingestion_key": ["network", "transaction_hash"], + "retry_safe_processing_required": true, + "rule_version_and_verification_evidence_recorded": true, + "production_enablement_requires_human_approval": true, + "jurisdiction_gate_enforced_before_processing": true + }, + "top_anchor": { + "xrpl_key": ["network", "transaction_hash"], + "xrpl_transaction_hash_format": "64 uppercase hexadecimal characters", + "xrpl_state_required": "validated", + "caloriedb_key": "calorie_anchor_hash", + "caloriedb_anchor_hash_format": "64 uppercase hexadecimal characters", + "cardinality": "one-to-one", + "unique_constraints": [ + ["network", "transaction_hash"], + ["calorie_anchor_hash"] + ], + "calorie_anchor_derivation": "HMAC-SHA-256 over a domain separator, network and validated XRPL transaction hash using a versioned server-side key", + "hashes_are_equal": false + }, + "calorie_asset_scope": { + "exact_asset_registry_required": true, + "registry_key": ["network", "issuer", "currency_code"], + "symbol_or_memo_only_match_allowed": false, + "validated_transaction_and_metadata_required": true, + "anchor_requires_calorie_relevance_evidence": true, + "initial_pilot_transaction_types": ["Payment"], + "future_review_transaction_types": [ + "OfferCreate", + "OfferCancel", + "AMMDeposit", + "AMMWithdraw", + "AMMBid", + "TrustSet" + ], + "rule_version_stored_per_anchor": true + }, + "layers_top_down": [ + { + "level": 0, + "entity": "validated_xrpl_transaction", + "key": ["network", "transaction_hash"] + }, + { + "level": 1, + "entity": "calorie_transaction_anchor", + "key": ["calorie_anchor_hash"], + "relation_to_parent": "exactly-one" + }, + { + "level": 2, + "entity": "provenance_event", + "key": ["event_hash"], + "relation_to_parent": "zero-or-more-explicit-links" + }, + { + "level": 3, + "entity": "product_lot_or_batch", + "key": ["lot_hash"], + "relation_to_parent": "event-input-or-output" + }, + { + "level": 4, + "entity": "trace_view", + "key": ["trace_scope"], + "relation_to_parent": "visibility-filtered-projection" + } + ], + "entities": { + "calorie_transaction_anchor": { + "required_fields": [ + "network", + "transaction_hash", + "calorie_anchor_hash", + "key_version", + "asset_registry_entry_id", + "calorie_relevance_rule_version", + "validated_ledger_index", + "verified_at" + ], + "raw_transaction_json_stored": false + }, + "provenance_event": { + "required_fields": [ + "event_hash", + "event_type", + "canonicalization_version", + "actor_role", + "occurred_at", + "verification_level", + "visibility" + ], + "event_types": [ + "production", + "harvest", + "processing", + "transformation", + "transfer", + "shipment", + "receipt", + "retail", + "consumption" + ], + "actor_roles": [ + "producer", + "processor", + "distributor", + "retailer", + "consumer", + "auditor" + ] + }, + "provenance_edge": { + "required_fields": [ + "from_event_hash", + "to_event_hash", + "relation_type", + "created_at" + ], + "relation_types": [ + "custody_transfer", + "input_to_transformation", + "output_from_transformation", + "split_from", + "merged_into", + "evidence_for" + ] + }, + "product_lot_or_batch": { + "required_fields": [ + "lot_hash", + "lot_type", + "canonicalization_version", + "visibility" + ] + } + }, + "graph_rules": { + "shape": "directed-acyclic-graph", + "simple_linear_chain_assumed": false, + "splits_supported": true, + "merges_supported": true, + "cycles_allowed": false, + "missing_edges_reported_as_gaps": true, + "missing_edges_inferred_or_fabricated": false, + "each_edge_requires_authorized_evidence": true + }, + "verification_levels": [ + "ledger-validated", + "actor-declared", + "wallet-control-verified", + "document-evidence-verified", + "independent-auditor-verified" + ], + "truth_boundary": { + "transaction_proves": "existence, contents and validated ledger result of the XRPL transaction", + "transaction_does_not_prove": "that a physical food product existed, moved, was safe, or matched an off-chain claim", + "physical_claim_requires_separate_evidence": true + }, + "privacy_and_visibility": { + "consumer_events_private_by_default": true, + "personal_food_history_public_by_default": false, + "public_provenance_requires_field_allowlist": true, + "public_transaction_lookup_may_return_private_record": false, + "cross_purpose_auto_linking_allowed": false, + "user_or_business_controls_optional_disclosure": true + }, + "trace_lookup": { + "accepted_entry_points": [ + ["network", "transaction_hash"], + ["calorie_anchor_hash"], + ["event_hash"], + ["lot_hash"] + ], + "result": "visibility-filtered verified path with explicit confidence levels and gaps" + } +} diff --git a/docs/DURABLE_DATA_FOUNDATION.md b/docs/DURABLE_DATA_FOUNDATION.md new file mode 100644 index 0000000..f3657b3 --- /dev/null +++ b/docs/DURABLE_DATA_FOUNDATION.md @@ -0,0 +1,109 @@ +# Durable Data & Privacy Foundation (DS-1) + +Status: pre-release architecture contract. Public onboarding remains blocked. + +## Outcome + +CalorieApp must retain authenticated food history independently of a web +service's ephemeral filesystem or free-tier expiry. Private application data +uses a provider-neutral PostgreSQL primary store. SQLite remains a local and +test convenience only. + +The existing Identity Bridge remains the ownership boundary: every private food +record belongs to the immutable internal CalorieApp user identifier. Identity +expansion, voluntary profile fields and donation-related personal details wait +until the durable-data and privacy gates pass. + +## Decisions already fixed + +- PostgreSQL is the primary live data architecture. +- `DATABASE_URL` remains the provider-neutral connection boundary. +- A production deployment must fail closed when configured with SQLite. +- Formal, versioned schema migrations replace startup-time ad-hoc alteration. +- A backup is not accepted until a staging restore drill succeeds. +- Production rollback uses a verified database restore or a separately tested + corrective migration, never an untested destructive automatic downgrade. +- Food history must support authenticated export and erasure. +- Infrastructure expiry must never silently define the user's retention period. +- Food search text is forwarded to Open Food Facts without a CalorieApp identity + and is not retained as CalorieApp history unless the user explicitly logs a result. +- Personal data is not written to public blockchain or public IPFS storage. +- Optional encrypted user-controlled exports and non-reversible integrity + commitments remain separate research and are not launch dependencies. +- A future voluntary XRPL reference starts with a strict one-to-one pair between + `(network, validated transaction hash)` and a unique CalorieDB anchor hash, + while every lower user/record/event association remains off-chain. +- XRPL memos may contain only a one-time opaque challenge; comparable private + CalorieDB records use keyed fingerprints or salted commitments, never a plain + public hash of personal data. + +## Current assessment + +| Area | Current state | DS-1 conclusion | +|---|---|---| +| Identity ownership | Internal user id is bound to food logs | Preserve and test on PostgreSQL | +| Cross-user access | Automated SQLite tests cover reads and deletion | Repeat against PostgreSQL staging | +| PostgreSQL support | Driver and URL normalization exist | Partial, not production-ready | +| Schema changes | `create_all` plus ad-hoc optional-column changes | Replace with formal migrations | +| Production SQLite guard | Missing | Add a startup fail-closed check | +| Durable-host tests | Missing | Automate restart and redeploy probes | +| Backup and restore | Missing | Select mechanism and prove restoration | +| User export | Missing | Add authenticated portable export | +| User erasure | Food-log deletion exists; complete account erasure does not | Complete scoped erasure workflow | +| Retention | No infrastructure expiry desired; exact policy unresolved | Explicit decision and notice required | + +## DS-2 implementation order + +1. Introduce a formal migration baseline for the exact current schema. +2. Add environment validation and reject SQLite in staging/production. +3. Add a database readiness probe that performs a safe query. +4. Run the complete identity and ownership suite against PostgreSQL. +5. Add restart and redeploy persistence tests using synthetic records. +6. Implement authenticated data export. +7. Complete account erasure, including identity links and active sessions. +8. Select an encrypted backup method and perform a documented staging restore. +9. Approve retention, backup deletion and privacy-notice wording. +10. Only after the core gates pass, implement the disabled XRPL reference tables + and verification flow described in `XRPL_TRANSACTION_LINKING.md`. + +The initial CalorieApp UI and first-user workflow do not depend on those tables. +They begin empty and disabled, then scale only when real CalorieToken settlement +or supply-chain adoption justifies a reviewed pilot. + +## Automation boundary + +Automation is a foundation step before formal migrations, not a later add-on. +The release pipeline must cover contract and schema drift, tests, build, +readiness, owner isolation, restart and redeploy persistence, export +completeness, erasure scope and localization completeness. Backups should run +automatically, while scheduled restore drills use staging and synthetic data. + +Every job must be idempotent or retry-safe, observable and auditable without +logging secrets or unnecessary personal data. A production operation may run +through an automated pipeline only after its explicit approval gate passes. +Human approval remains required for production schema changes, retention or +erasure policy changes, new Identity Bridge purposes, XRPL enablement, +production deployment and public publication. Financial execution or routing +must never be started automatically by this architecture. + +This boundary deliberately prepares the continuation: DS-2 supplies the formal +migrations and production database guard; later Identity Bridge and eleven- +language work reuse the same contract checks, feature flags, audit pattern and +approval gate. Showcase preview, review and scheduled publication stay last. + +Provider credentials, live connection strings, real user records and private +operational recovery details must not be committed. + +## Platform budget + +Use one provider per necessary role: the existing WordPress environment for the +site and Identity Bridge, GitHub for source/CI, one app runtime, one +provider-neutral PostgreSQL service and Open Food Facts as the food-data source. +XRPL remains an optional future reference layer already native to the project. + +Do not add a second identity service, primary database, graph database, +blockchain database or IPFS/Filecoin dependency to the core release. PostgreSQL +can store the future provenance graph. A new provider requires a short +architecture record explaining why an existing role cannot safely provide the +capability. Independent backup storage is the only expected exception, and only +when a documented recovery design requires it. diff --git a/docs/XRPL_TRANSACTION_LINKING.md b/docs/XRPL_TRANSACTION_LINKING.md new file mode 100644 index 0000000..2b5e436 --- /dev/null +++ b/docs/XRPL_TRANSACTION_LINKING.md @@ -0,0 +1,180 @@ +# Voluntary XRPL transaction linking architecture + +Status: prepared architecture only. Disabled by default and not a dependency of +the current CalorieApp release. + +This is intentionally a future-ready database seam, not an initial user feature. +The first users do not need CalorieToken settlement, a memo workflow or a trace +screen. Core durable storage, migrations, export, erasure and recovery remain +the priority. Only after those gates pass may the empty anchor/provenance tables +be introduced behind a disabled feature flag. + +## Purpose and boundary + +A user may eventually choose to associate an already public XRPL transaction +with a private CalorieDB record. CalorieApp remains non-custodial: it does not +hold keys, sign transactions, execute transfers, route orders or scan a user's +complete wallet history. + +The architecture begins with this strict one-to-one pair: + +```text +(XRPL network, validated transaction hash) <-> unique CalorieDB anchor hash +``` + +The network is part of the identifier because a hash alone does not document +which ledger environment was checked. Only a signed transaction in a validated +ledger is accepted; proposed or failed submissions do not establish a link. + +The CalorieDB anchor hash is a separate 256-bit value, derived with +HMAC-SHA-256 from a domain separator, network and validated XRPL transaction +hash using a versioned server-side key. Database uniqueness constraints enforce +exactly one CalorieDB anchor per XRPL network/hash pair and exactly one pair per +CalorieDB anchor. The two hashes are linked but are not equal. + +Before creating the pair, CalorieDB must prove that the validated transaction is +actually CalorieToken-related. Matching only the text `CAL`, a display name or a +memo is forbidden. A versioned asset registry supplies the exact network, +issuer and currency-code combination. Direct token payments can be checked from +the transaction fields; later DEX, AMM and trust-line cases require separate +allowlisted rules over validated transaction metadata. The first pilot should +support only a direct `Payment`, then expand one reviewed transaction type at a +time. + +## Proposed private data model + +| Entity | Essential fields | Purpose | +|---|---|---| +| `calorie_transaction_anchor` | network, transaction hash, unique CalorieDB anchor hash, key version, asset-registry entry/rule version, validated ledger index, verification time | Strict one-to-one top-level pair; no replicated raw transaction JSON | +| `calorie_record_fingerprint` | record type/key, canonicalization version, private HMAC fingerprint, key version | Detects the exact private record/version without exposing its contents | +| `ledger_record_link` | user, transaction reference, record fingerprint, purpose, consent version, verification method, status | Private typed relation between the user-authorized items | +| `calorie_link_group` | internal random id, purpose, owner/organisation scope, status | Privately groups several explicitly approved transaction-record edges into one chain or case | +| `ledger_link_challenge` | hashed random token, user, intended record/purpose, expiry, used time | Short-lived one-time proof for a new memo-assisted link | + +The link graph remains private. A transaction hash must not become a public API +for discovering a CalorieApp account, food history, donation, profile or other +private record. + +Below the one-to-one top pair, the private `ledger_record_link` joins a +`calorie_transaction_anchor` to one `calorie_record_fingerprint`. This lower +layer supports many-to-many relations: several XRPL transactions may support one +CalorieDB record, and one transaction may be related to several records, but +every edge requires its own purpose and explicit authorization. A +`calorie_link_group` can collect those edges without exposing a stable public +case, user or supply-chain identifier. + +Cross-purpose linking is forbidden by default. For example, a donation link, +merchant traceability event and consumer food log must not be combined merely +because the same public wallet or transaction appears in more than one context. + +## Top-down traceability below the hash pair + +The full food path is modelled below the anchor in this order: + +1. validated XRPL transaction hash; +2. unique CalorieDB anchor hash; +3. one or more hashed provenance events; +4. product, lot and batch input/output relations; +5. a visibility-filtered trace view for the permitted audience. + +Food supply is a directed acyclic graph rather than a forced linear chain. One +harvest can be split into multiple batches; several ingredients can be merged +into one product; processing can create several outputs. Explicit event edges +model production, harvest, processing, transfer, shipment, receipt, retail and +optional consumption. A missing edge is shown as a gap and is never inferred. + +A validated CalorieToken transaction proves the existence, contents and ledger +result of that transaction. By itself it does not prove that a physical food +item existed, moved, was safe or matched a claim. Those facts need separately +authorized event data, documents and, where appropriate, independent audit. + +The complete machine-readable graph design is in +`contracts/provenance/v1/traceability.json`. + +No separate graph platform is required. The anchor, event and edge tables fit in +the same provider-neutral PostgreSQL database as the rest of CalorieApp. XRPL is +only the optional ledger anchor; IPFS, Filecoin and another blockchain database +are not dependencies of this design. + +When this feature is eventually approved, a worker may automatically verify and +ingest the one transaction the user or authorized business has requested. The +network and transaction hash form its idempotency key, so retries cannot create +duplicate anchors. The worker records its rule version and evidence, rejects the +request before processing when its jurisdiction gate is closed, and starts with +the feature flag disabled. It may not scan a complete wallet, infer links across +purposes, publish private data or enable itself. This gives later scaling a safe +automation path without adding another platform. + +## Two voluntary link flows + +### Future transaction with memo + +1. An authenticated user selects one CalorieDB record and a controlled purpose. +2. The backend creates a high-entropy one-time challenge, stores only its hash, + and binds it to that user, record and purpose with a short expiry. +3. The user's own wallet places only a versioned opaque challenge in the XRPL + memo. No name, email, food data, database id or stable user identifier is used. +4. The user supplies the transaction hash, or explicitly asks for this one + transaction to be checked. +5. The backend retrieves the transaction, requires `validated=true`, verifies + the challenge and the purpose-specific account/currency/issuer conditions, + consumes the challenge and stores the private link. + +### Existing transaction without memo + +1. The authenticated user supplies a transaction hash and intended private record. +2. CalorieApp verifies the transaction on the selected network. +3. The user proves the relevant participant role through the already verified + Xaman identity context or a fresh wallet challenge. +4. After a separate confirmation, CalorieApp stores only the off-chain relation. + +An existing transaction cannot receive a memo retroactively. It therefore must +not be linked merely because somebody knows its public hash. + +## Comparable CalorieDB hashes + +A raw SHA-256 digest of a predictable record is not an adequate privacy barrier. +The database should use two separate mechanisms: + +- Private record fingerprint: HMAC-SHA-256 over a versioned canonical + representation, with domain separation and a rotatable server-side key. +- Optional public commitment: SHA-256 over a domain separator, + canonicalization version, private record digest and a new 256-bit random salt. + The salt remains encrypted off-chain and is never included in the memo. + +The fingerprint supports internal equality/version checks. A one-time commitment +can later prove integrity without publishing the private record and without +creating a reusable public user identifier. + +## Unlinking and erasure + +The private database association, fingerprint and retained salt can be deleted +according to the approved erasure process. The XRPL transaction and its memo +cannot be deleted. Before signing, the user must see a clear warning that the +memo is public and irreversible. After unlinking, its random challenge must not +resolve to anything and becomes an orphaned opaque value. + +## Compliance gates + +This architecture can reduce risk but cannot guarantee compliance in every +country. Before enabling it, the project requires: + +- a documented necessity assessment and DPIA where required; +- an explicit purpose, lawful basis, retention rule and user-facing notice; +- jurisdiction-specific feature gates and an independent legal review; +- a functional assessment of whether later features constitute custody, + transfer, execution, order routing, exchange, advice or another regulated + crypto-asset service; +- continued separation from market, exchange and transaction-execution UI. + +MiCA is not the only boundary. FATF guidance applies a functional test to +virtual-asset services, while privacy law may treat transaction hashes and +wallet addresses as personal data when they can identify or single out a person. + +Primary references: + +- [XRPL transactions and validated transaction hashes](https://xrpl.org/docs/concepts/transactions) +- [XRPL transaction common fields and memos](https://xrpl.org/docs/references/protocol/transactions/common-fields) +- [EDPB Guidelines 02/2025, version 2.0, on blockchain and personal data](https://www.edpb.europa.eu/system/files/2026-07/edpb_guidelines_202502_blockchain_v2_en.pdf) +- [ESMA overview of MiCA](https://www.esma.europa.eu/esmas-activities/digital-finance-and-innovation/markets-crypto-assets-regulation-mica) +- [FATF risk-based guidance for virtual assets and VASPs](https://www.fatf-gafi.org/content/dam/fatf/documents/recommendations/Updated-Guidance-VA-VASP.pdf) diff --git a/docs/public/architecture.md b/docs/public/architecture.md index c63e749..f507264 100644 --- a/docs/public/architecture.md +++ b/docs/public/architecture.md @@ -2,6 +2,10 @@ CalorieApp V1 consists of a browser frontend, an API backend, an external identity bridge and a food-data integration. Private food logs are scoped to the authenticated CalorieApp user. +SQLite is a local-development and test facility. Public user onboarding remains +blocked until a provider-neutral PostgreSQL deployment and its migration, +persistence, export, erasure and recovery gates have been verified. + The released implementation is non-custodial and non-financial. Proposed ecosystem, provenance, distributed-storage or token-related research is not represented as implemented functionality. Detailed operational topology, private configuration and unreleased architecture remain outside the public repository. diff --git a/docs/public/data-safety.md b/docs/public/data-safety.md new file mode 100644 index 0000000..d375f71 --- /dev/null +++ b/docs/public/data-safety.md @@ -0,0 +1,38 @@ +# Public data-safety direction + +CalorieApp's public-user release is blocked until authenticated food history has +a durable PostgreSQL primary store, formal schema migrations, verified user +isolation, authenticated export and deletion, and a successful backup-restore +exercise. + +SQLite is limited to local development and automated tests. A hosting +provider's filesystem lifetime or free-tier expiry must not silently determine +how long a user's history is retained. + +Product-search text is sent to Open Food Facts without the CalorieApp account +identifier and is not retained as CalorieApp history unless the user chooses to +log a result. + +Personal food history, email addresses, profile details and stable user +identifiers are not intended for public blockchain or public IPFS storage. +Optional encrypted user-controlled exports and non-reversible integrity proofs +are research directions only and are not dependencies of the current release. + +A future voluntary XRPL reference would use the network plus a validated +transaction hash as its public anchor and map it one-to-one to a unique +CalorieDB anchor hash. Lower relations to records and events would remain +private or explicitly visibility-controlled and deletable. Memos would permit only one-time opaque values, +never personal details, database identifiers or plain hashes of private records. + +The architecture intentionally limits providers: one website/identity +environment, one source/CI platform, one app runtime and one PostgreSQL primary +store. The planned provenance graph does not require a separate graph database, +blockchain database or IPFS/Filecoin service. + +Repeatable checks and recovery work are designed for automation. Production +changes, new identity or ledger purposes, financial actions and public +publication retain explicit approval gates. Automated work must be retry-safe, +observable and avoid secrets or unnecessary personal data in logs. + +Passing technical checks does not by itself constitute legal, privacy or +security certification. diff --git a/docs/public/release-readiness.md b/docs/public/release-readiness.md index dda919f..1d23530 100644 --- a/docs/public/release-readiness.md +++ b/docs/public/release-readiness.md @@ -3,6 +3,10 @@ A public CalorieApp release should proceed only when: - backend tests, frontend lint/build and relevant plugin tests pass; +- the live environment uses durable PostgreSQL rather than SQLite; +- formal migrations, restart/redeploy persistence and owner-isolation checks pass; +- an encrypted backup has passed a documented staging restore exercise; +- authenticated export, deletion and approved retention disclosures are complete; - secret, personal-data and generated-artifact checks pass; - public claims match released functionality; - licences, notices, attribution and asset provenance have been reviewed; diff --git a/docs/public/xrpl-linking.md b/docs/public/xrpl-linking.md new file mode 100644 index 0000000..fe50318 --- /dev/null +++ b/docs/public/xrpl-linking.md @@ -0,0 +1,37 @@ +# Voluntary XRPL reference direction + +CalorieApp is preparing a privacy-preserving architecture in which a user could +voluntarily associate a validated XRPL transaction hash with a private CalorieDB +record. This is a planned, disabled feature and is not part of the current app. +The first users would not be expected to use it; the design is reserved now so +the database can grow into it later without changing its core identity model. + +The canonical public reference would be the XRPL network plus transaction hash. +Each validated network/hash pair would map one-to-one to a separate unique +CalorieDB anchor hash. The actual relations below that anchor—to events, batches, +products, users or records—would remain off-chain and visibility-controlled. A +memo-assisted flow would permit only a one-time opaque random value; +names, email addresses, food information, database identifiers and plain record +hashes would be forbidden. + +A transaction would count as CalorieToken-related only after matching the exact +network, issuer and currency code from a controlled asset registry. A token +symbol, project name or memo alone would not be accepted as evidence. + +The transaction hash and private CalorieDB fingerprint would be connected by a +separate private, purpose-bound link record. Multiple approved links could form a +traceability graph from producer through processing, distribution and retail, +but CalorieApp would not expose a public resolver that turns a transaction hash +into a user profile or private record. Missing links would be shown as gaps, +never guessed, and ledger validation alone would not be presented as proof that +a physical food claim is true. + +CalorieApp would remain non-custodial and would not hold wallet keys, execute +transactions or automatically profile complete wallet histories. Enabling this +direction requires privacy impact assessment, jurisdiction-specific review and +clear user notice that XRPL transactions and memos are public and irreversible. + +After a future approval, verification of one explicitly requested transaction +could run automatically and retry safely without creating a duplicate link. +Complete-wallet scans, cross-purpose matching and self-enablement would remain +forbidden.