Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# Global Patient Record Project: NONSECRET SETTINGS ONLY.
# Copy to .env.local (ignored); use Node --env-file, or set process environment.
# Never put account keys, SAS URLs, private keys, tokens, or real patient data here.
STORAGE_BACKEND=file
STORAGE_FILE_DIRECTORY=.data/web
STORAGE_EMULATOR=false

# Azure: DefaultAzureCredential (az login, managed identity, or workload identity).
# AZURE_STORAGE_ACCOUNT_URL=https://REPLACE.blob.core.windows.net
# AZURE_STORAGE_CONTAINER=synthetic-records

# AWS: default SDK credential chain (aws sso login or a workload role).
# S3_BUCKET=REPLACE
# AWS_REGION=us-east-1
# STORAGE_PREFIX=synthetic-integration/
# S3_KMS_KEY_ID=REPLACE_WITH_KEY_ARN

# IPFS: private Kubo and Cluster APIs via authenticated HTTPS reverse proxies.
# IPFS_PRIVATE_NETWORK=true
# IPFS_API_URL=https://REPLACE
# IPFS_CLUSTER_API_URL=https://REPLACE
# IPFS_METADATA_BACKEND=azure
# Optional synchronous backup before metadata publication:
# IPFS_BACKUP_BACKEND=s3
# IPFS_REPLICATION_MIN=2
# IPFS_REPLICATION_MAX=3
# IPFS_TIMEOUT_MS=30000
# Authorization headers live OUTSIDE the repo in mounted secret files:
# IPFS_API_AUTH_FILE=REPLACE_WITH_ABSOLUTE_SECRET_FILE_PATH
# IPFS_CLUSTER_AUTH_FILE=REPLACE_WITH_ABSOLUTE_SECRET_FILE_PATH
# OR retrieve them from Key Vault with the same Azure workload identity:
# AZURE_KEY_VAULT_URL=https://REPLACE.vault.azure.net
# IPFS_API_AUTH_KEYVAULT_SECRET=ipfs-api-auth
# IPFS_CLUSTER_AUTH_KEYVAULT_SECRET=ipfs-cluster-auth
# OR retrieve headers using an AWS workload role:
# IPFS_API_AUTH_AWS_SECRET_ID=REPLACE_WITH_SECRET_ARN
# IPFS_CLUSTER_AUTH_AWS_SECRET_ID=REPLACE_WITH_SECRET_ARN
# AWS hybrid: IPFS_METADATA_BACKEND=dynamodb, IPFS_BACKUP_BACKEND=s3
# DYNAMODB_TABLE=REPLACE_WITH_METADATA_TABLE
40 changes: 40 additions & 0 deletions .github/workflows/storage-ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
name: Storage CI

on:
push:
branches: [main]
pull_request:

permissions:
contents: read

jobs:
tests:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
with:
persist-credentials: false
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: "22"
cache: npm
- run: npm ci
- run: npm test
- run: npm run typecheck
- run: npm run build
- run: npm run test:integration:azure
private-ipfs:
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
with:
persist-credentials: false
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: "22"
cache: npm
- run: npm ci
- run: npm run test:integration:ipfs
64 changes: 64 additions & 0 deletions .github/workflows/storage-cloud-smoke.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
name: Reviewed cloud storage smoke

on:
workflow_dispatch:
inputs:
backend:
description: Dedicated synthetic-data provider (never a patient store)
type: choice
required: true
options: [azure, s3, dynamodb, aws-hybrid]

permissions:
contents: read

jobs:
smoke:
if: github.ref == 'refs/heads/main'
runs-on: ${{ vars.STORAGE_RUNNER_LABEL || 'ubuntu-latest' }}
timeout-minutes: 15
environment: storage-integration
permissions:
contents: read
id-token: write
env:
STORAGE_INTEGRATION: "true"
STORAGE_EMULATOR: "false"
STORAGE_BACKEND: ${{ inputs.backend == 'aws-hybrid' && 'ipfs' || inputs.backend }}
AZURE_STORAGE_ACCOUNT_URL: ${{ vars.AZURE_STORAGE_ACCOUNT_URL }}
AZURE_STORAGE_CONTAINER: ${{ vars.AZURE_STORAGE_CONTAINER }}
S3_BUCKET: ${{ vars.S3_BUCKET }}
STORAGE_PREFIX: synthetic-integration/
AWS_REGION: ${{ vars.AWS_REGION }}
S3_KMS_KEY_ID: ${{ vars.S3_KMS_KEY_ID }}
DYNAMODB_TABLE: ${{ vars.DYNAMODB_TABLE }}
IPFS_PRIVATE_NETWORK: "true"
IPFS_METADATA_BACKEND: dynamodb
IPFS_BACKUP_BACKEND: s3
IPFS_API_URL: ${{ vars.IPFS_API_URL }}
IPFS_CLUSTER_API_URL: ${{ vars.IPFS_CLUSTER_API_URL }}
IPFS_API_AUTH_AWS_SECRET_ID: ${{ vars.IPFS_API_AUTH_AWS_SECRET_ID }}
IPFS_CLUSTER_AUTH_AWS_SECRET_ID: ${{ vars.IPFS_CLUSTER_AUTH_AWS_SECRET_ID }}
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
with:
persist-credentials: false
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: "22"
cache: npm
- run: npm ci
- if: inputs.backend == 'azure'
uses: azure/login@8216e11d8cd9b42fe925c852af8e76311ff067ac # v2
with:
client-id: ${{ vars.AZURE_CLIENT_ID }}
tenant-id: ${{ vars.AZURE_TENANT_ID }}
subscription-id: ${{ vars.AZURE_SUBSCRIPTION_ID }}
- if: inputs.backend != 'azure'
uses: aws-actions/configure-aws-credentials@ff717079ee2060e4bcee96c4779b553acc87447c # v4
with:
role-to-assume: ${{ vars.AWS_ROLE_ARN }}
aws-region: ${{ vars.AWS_REGION }}
role-session-name: synthetic-storage-integration
role-duration-seconds: 900
- run: npm run test:integration
11 changes: 11 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,14 @@ coverage/
.data/
*.log
*.tmp
.env
.env.*
!.env.example
.secrets/
*.pem
*.key
*.pfx
*.p12
swarm.key
identity.json
service.json
18 changes: 16 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,16 +107,30 @@ the reference-inspired layout and the patient-specific privacy boundaries.

## Commands

For Azure, AWS S3, private IPFS, account-free integration tests, and secret-free
configuration, see the short [storage quickstart](docs/STORAGE_QUICKSTART.md).
The [draft storage design](docs/STORAGE_DESIGN.md) describes concurrency,
metadata exposure, and remaining distributed-coordination limits.
Provider implementations are isolated in
[`src/storage/adapters/`](src/storage/README.md). An optional
[AWS hybrid foundation](docs/AWS_HYBRID_STORAGE.md) combines EC2/Kubo/EBS,
DynamoDB metadata, and SSE-KMS encrypted S3 backups.

| Command | Purpose |
| --- | --- |
| `npm test` | Run all unit and integration tests |
| `npm test` | Run account-free tests; external-provider integration is opt-in |
| `npm run test:coverage` | Run tests with coverage |
| `npm run typecheck` | Type-check without emitting JavaScript |
| `npm run build` | Compile TypeScript into `dist` |
| `npm start` | Run the local web UI at `http://127.0.0.1:3000` |
| `npm run dev` | Run the local UI with server restart on source changes |
| `npm run example` | Run the synthetic end-to-end example |
| `npm run example:access-denied` | Demonstrate that one factor alone cannot unlock a record |
| `npm run example:storage` | Run a synthetic flow against the configured backend |
| `npm run test:integration:azure` | Run real-SDK tests against temporary loopback Azurite |
| `npm run test:integration:ipfs` | Run the private IPFS Docker fixture (Docker engine required) |
| `npm run test:integration` | Explicit opt-in external-provider contract and encrypted-flow tests |
| `npm run storage:restore -- OBJECT_KEY` | Explicitly restore verified encrypted IPFS content from its configured backup |

## Minimal usage

Expand Down Expand Up @@ -160,7 +174,7 @@ src/
crypto/ Encryption, key splitting, signatures, and canonical hashing
domain/ FHIR and provenance types
service/ Enrollment, append, unlock, and verified assembly
storage/ Storage contract plus memory and local-file implementations
storage/ Provider-neutral contract, adapters, and runtime configuration
web/ Local HTTP API and static-file server
web/ Browser UI for enrollment, append, and timeline verification
examples/ Synthetic end-to-end flows
Expand Down
9 changes: 8 additions & 1 deletion docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,11 +83,18 @@ is displayed.

## Future milestones

Optional storage adapters now cover Azure, S3, and experimental private-IPFS
content with a separately configured CAS index. See
[storage design](STORAGE_DESIGN.md) and [setup](STORAGE_QUICKSTART.md).
The private-IPFS adapter is not a decentralized consensus implementation;
operator independence, index availability, and offline head merging remain
separate work.

- Real biometric SDK adapter with template protection and liveness evidence
- Multi-finger enrollment and recovery policy
- Multiple authorized clinicians with revocation
- Offline branch creation and deterministic merge
- Distributed object-store adapter
- Independently operated distributed storage with resilient index coordination
- FHIR profile validation and consent resources
- Metadata-hiding access protocol
- Independent cryptographic, clinical-safety, legal, and ethics reviews
Loading
Loading