mirror of
https://github.com/temetro/temetro.git
synced 2026-08-11 02:57:55 +00:00
bb536ba6da
A clinician can push an updated record to a wallet-linked patient (permanent share). The snapshot is signed with the clinic Ed25519 key and sealed to the wallet's X25519 key — derived from its Ed25519 wallet number via the birational map, verified byte-for-byte against the wallet's own derivation. Stored pending, delivered over the /wallet relay live and on the wallet's next authenticated connect (offline catch-up). The patient approves/denies in-app; the wallet signs its decision, the backend verifies it, and the record is replaced only on approval. Wallet pins the clinic key (TOFU) and warns on change. Backend: walletRecordUpdates table + service, ed25519PubToX25519Hex helper, POST /api/patients/wallet/push, GET .../link/:fileNumber|updates|updates/:id, wallet:update-request / wallet:update-response relay events. Frontend: "Push to wallet" dialog with live status, wallet-link gating on the patient sheet, "Sent updates" list under Settings → Signing, walletPush / walletUpdatesList locale namespaces across all five languages. Bumps to v0.5.0. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
54 lines
2.3 KiB
TypeScript
54 lines
2.3 KiB
TypeScript
import { index, jsonb, pgTable, text, timestamp, uuid } from "drizzle-orm/pg-core";
|
|
|
|
import { organization, user } from "./auth.js";
|
|
|
|
export type WalletUpdateStatus =
|
|
| "pending"
|
|
| "delivered"
|
|
| "approved"
|
|
| "denied";
|
|
|
|
// One row per clinic→wallet record-update push. When a clinician edits a
|
|
// wallet-linked patient they can push the updated record to the patient's app;
|
|
// it lands here as `pending`, is sealed to the wallet's (X25519-from-Ed25519)
|
|
// key and signed with the clinic's Ed25519 key. The relay delivers it live if
|
|
// the device is connected, and again on the wallet's next authenticated connect
|
|
// (so an offline phone still receives it). The patient reviews the change,
|
|
// verifies the clinic signature, and approves/denies in-app — only then is the
|
|
// on-device record replaced. The clinic polls `status` for delivery/approval.
|
|
export const walletRecordUpdates = pgTable(
|
|
"wallet_record_updates",
|
|
{
|
|
id: uuid("id").primaryKey().defaultRandom(),
|
|
organizationId: text("organization_id")
|
|
.notNull()
|
|
.references(() => organization.id, { onDelete: "cascade" }),
|
|
createdBy: text("created_by")
|
|
.notNull()
|
|
.references(() => user.id, { onDelete: "cascade" }),
|
|
fileNumber: text("file_number").notNull(),
|
|
walletNumber: text("wallet_number").notNull(),
|
|
status: text("status")
|
|
.$type<WalletUpdateStatus>()
|
|
.notNull()
|
|
.default("pending"),
|
|
// base64 sealed box of the full updated patient snapshot (sealed to the
|
|
// wallet's derived X25519 key).
|
|
payloadSealed: text("payload_sealed").notNull(),
|
|
// The clinic's Ed25519 signature over the plaintext bundle bytes + its
|
|
// public key + fingerprint, so the wallet can verify provenance (TOFU pin).
|
|
clinicSignature: text("clinic_signature").notNull(),
|
|
clinicPublicKey: text("clinic_public_key").notNull(),
|
|
clinicFingerprint: text("clinic_fingerprint").notNull(),
|
|
// Human-readable summary of what changed (shown in the wallet inbox).
|
|
changes: jsonb("changes").$type<string[]>().notNull().default([]),
|
|
createdAt: timestamp("created_at").defaultNow().notNull(),
|
|
deliveredAt: timestamp("delivered_at"),
|
|
resolvedAt: timestamp("resolved_at"),
|
|
},
|
|
(t) => [
|
|
index("wallet_updates_org_idx").on(t.organizationId),
|
|
index("wallet_updates_wallet_idx").on(t.walletNumber),
|
|
],
|
|
);
|