Skip to main content

Overview

The revoke_access transition allows patients to immediately revoke a healthcare provider’s access to a medical record before the natural expiration time. This is useful if the patient changes their mind, suspects misuse, or no longer wants to share their medical information with a specific provider.

Function Signature

async transition revoke_access(
    access_token: field
) -> Future
Source: main.leo:386-390

Parameters

access_token
field
required
The cryptographic access token that was generated by grant_access. This is the same token that was encoded in the QR code given to the doctor. Only the patient who originally granted the access can revoke it.

Returns

Future
Future
Async finalization that:
  • Retrieves the existing AccessGrant from the access_grants mapping
  • Verifies the caller is the patient who granted the access
  • Updates the grant with is_revoked: true
  • Marks the token as invalid in access_token_valid mapping
The transaction succeeds if the caller is the grant owner, otherwise it fails.

Validation Rules

Patient Authorization

let grant: AccessGrant = access_grants.get(access_token);
assert_eq(grant.patient, caller);
Only the patient who originally granted the access can revoke it. The transaction fails if:
  • The caller is not the patient in the grant
  • The access token doesn’t exist

Revocation Process

let revoked_grant: AccessGrant = AccessGrant {
    patient: grant.patient,
    doctor: grant.doctor,
    record_id: grant.record_id,
    access_token: grant.access_token,
    granted_at: grant.granted_at,
    expires_at: grant.expires_at,
    is_revoked: true,  // Changed to true
};

access_grants.set(access_token, revoked_grant);
access_token_valid.set(access_token, false);
The revocation:
  1. Preserves all original grant data
  2. Sets is_revoked: true
  3. Marks token as invalid
  4. Takes effect immediately

Code Example

import { AleoClient } from '@aleo/sdk';

// Initialize Aleo client
const client = new AleoClient();

// Get the access token to revoke
// (from patient's local database of active grants)
const accessToken = await getActiveAccessToken({
  doctorAddress: 'aleo1doctor...',
  recordId: 'record_xyz'
});

try {
  // Revoke the access
  await client.execute(
    'salud_health_records_v6.aleo',
    'revoke_access',
    [accessToken]
  );

  console.log('Access revoked successfully');
  
  // Update local database
  await updateLocalGrant(accessToken, { status: 'revoked' });
  
  // Optionally notify the doctor
  await notifyDoctor({
    message: 'Patient has revoked access to the medical record',
    recordId: 'record_xyz',
    revokedAt: new Date()
  });

} catch (error) {
  if (error.message.includes('assert_eq')) {
    console.error('You are not authorized to revoke this access');
  } else if (error.message.includes('get')) {
    console.error('Access token not found');
  } else {
    console.error('Failed to revoke access:', error);
  }
}

Patient Dashboard Integration

// Display all active access grants to patient
interface ActiveGrant {
  token: string;
  doctor: string;
  recordId: string;
  recordType: number;
  grantedAt: number;
  expiresAt: number;
  isRevoked: boolean;
}

class PatientAccessManager {
  private client: AleoClient;
  private activeGrants: Map<string, ActiveGrant>;

  // List all active grants
  async listActiveGrants(): Promise<ActiveGrant[]> {
    // Query from local database or on-chain
    const grants = await this.queryPatientGrants();
    
    return grants.filter(g => 
      !g.isRevoked && 
      this.getCurrentBlockHeight() <= g.expiresAt
    );
  }

  // Revoke a specific grant
  async revokeGrant(accessToken: string): Promise<void> {
    const grant = this.activeGrants.get(accessToken);
    
    if (!grant) {
      throw new Error('Grant not found');
    }

    // Confirm with patient
    const confirmed = await this.confirmRevocation({
      doctor: grant.doctor,
      recordType: this.getRecordTypeName(grant.recordType),
      grantedAt: new Date(grant.grantedAt * 15000) // block to time
    });

    if (!confirmed) return;

    // Execute revocation on-chain
    await this.client.execute(
      'salud_health_records_v6.aleo',
      'revoke_access',
      [accessToken]
    );

    // Update local state
    grant.isRevoked = true;
    this.activeGrants.set(accessToken, grant);

    // Show confirmation
    this.showNotification('Access revoked successfully');
  }

  // Revoke all grants for a specific record
  async revokeAllForRecord(recordId: string): Promise<void> {
    const grants = Array.from(this.activeGrants.values())
      .filter(g => g.recordId === recordId && !g.isRevoked);

    if (grants.length === 0) {
      console.log('No active grants for this record');
      return;
    }

    const confirmed = await this.confirmBulkRevocation(
      grants.length,
      recordId
    );

    if (!confirmed) return;

    // Revoke all grants in parallel
    const revocations = grants.map(grant =>
      this.client.execute(
        'salud_health_records_v6.aleo',
        'revoke_access',
        [grant.token]
      )
    );

    await Promise.all(revocations);

    console.log(`Revoked ${grants.length} access grants`);
  }
}

UI Component Example

// React component for managing access grants
import React, { useState, useEffect } from 'react';

function AccessGrantsManager() {
  const [grants, setGrants] = useState<ActiveGrant[]>([]);
  const [loading, setLoading] = useState(false);

  useEffect(() => {
    loadActiveGrants();
  }, []);

  async function loadActiveGrants() {
    const activeGrants = await patientManager.listActiveGrants();
    setGrants(activeGrants);
  }

  async function handleRevoke(token: string) {
    if (!confirm('Are you sure you want to revoke this access?')) {
      return;
    }

    setLoading(true);
    try {
      await patientManager.revokeGrant(token);
      await loadActiveGrants(); // Refresh list
      alert('Access revoked successfully');
    } catch (error) {
      alert('Failed to revoke access: ' + error.message);
    } finally {
      setLoading(false);
    }
  }

  return (
    <div className="access-grants">
      <h2>Active Access Grants</h2>
      
      {grants.length === 0 ? (
        <p>No active access grants</p>
      ) : (
        <table>
          <thead>
            <tr>
              <th>Doctor</th>
              <th>Record Type</th>
              <th>Granted</th>
              <th>Expires</th>
              <th>Action</th>
            </tr>
          </thead>
          <tbody>
            {grants.map(grant => (
              <tr key={grant.token}>
                <td>{shortenAddress(grant.doctor)}</td>
                <td>{getRecordTypeName(grant.recordType)}</td>
                <td>{formatDate(grant.grantedAt)}</td>
                <td>{formatDate(grant.expiresAt)}</td>
                <td>
                  <button 
                    onClick={() => handleRevoke(grant.token)}
                    disabled={loading}
                  >
                    Revoke
                  </button>
                </td>
              </tr>
            ))}
          </tbody>
        </table>
      )}
    </div>
  );
}

Immediate Effect

Revocation takes effect as soon as the transaction is confirmed:
// Before revocation
try {
  await client.execute('verify_access', [token, doctor, recordId]);
  console.log('Access valid'); // ✓ Success
} catch {
  console.log('Access denied');
}

// Execute revocation
await client.execute('revoke_access', [token]);

// After revocation (same block or next block)
try {
  await client.execute('verify_access', [token, doctor, recordId]);
  console.log('Access valid');
} catch {
  console.log('Access denied'); // ✗ Fails: is_revoked = true
}

On-Chain State Changes

AccessGrant Update

let revoked_grant: AccessGrant = AccessGrant {
    patient: grant.patient,
    doctor: grant.doctor,
    record_id: grant.record_id,
    access_token: grant.access_token,
    granted_at: grant.granted_at,
    expires_at: grant.expires_at,
    is_revoked: true,  // ← Changed
};

access_grants.set(access_token, revoked_grant);
The original grant data is preserved with only is_revoked changed to true.

Token Invalidation

access_token_valid.set(access_token, false);
Marks the token as invalid for quick validity checks.

Error Cases

Not the Grant Owner: Transaction fails if caller is not the patient who granted access
Error: Assertion failed: assert_eq(grant.patient, caller)
Only the original patient can revoke access. Doctors cannot revoke their own access.
Token Not Found: Transaction fails if the access token doesn’t exist
Error: Mapping get failed: access token not found
The token may never have existed or there’s a typo in the token value.
Already Revoked: You can revoke an already-revoked token without error. The function will update the grant again with is_revoked: true, which is idempotent.

Use Cases

1. Emergency Revocation

// Patient suspects unauthorized access
async function emergencyRevokeAll() {
  const allGrants = await getAllActiveGrants();
  
  console.log(`Revoking ${allGrants.length} active grants...`);
  
  for (const grant of allGrants) {
    await client.execute('revoke_access', [grant.token]);
  }
  
  console.log('All access revoked');
}

2. Selective Revocation

// Revoke access from specific doctor
async function revokeDoctorAccess(doctorAddress: string) {
  const doctorGrants = await getGrantsForDoctor(doctorAddress);
  
  for (const grant of doctorGrants) {
    await client.execute('revoke_access', [grant.token]);
  }
  
  console.log(`Revoked access for ${doctorAddress}`);
}

3. Scheduled Revocation

// Revoke after specific event (e.g., treatment completed)
async function scheduleRevocation(
  accessToken: string,
  afterBlocks: number
) {
  const currentBlock = await client.getBlockHeight();
  const revokeAtBlock = currentBlock + afterBlocks;
  
  // Store in local scheduler
  await scheduleTask({
    type: 'revoke_access',
    token: accessToken,
    executeAtBlock: revokeAtBlock
  });
  
  console.log(`Scheduled revocation at block ${revokeAtBlock}`);
}

Security Considerations

Authorization

  • Patient-only operation: Only the patient who granted access can revoke it
  • Cannot be reversed: Once revoked, the same token cannot be un-revoked
  • No doctor self-revocation: Doctors cannot revoke their own access

Immediate Effect

  • Instant invalidation: Takes effect as soon as transaction confirms
  • No grace period: No delay between revocation and enforcement
  • Atomic operation: Cannot fail partially

Auditability

// The revoked grant remains in the mapping for audit purposes
const grant = await queryAccessGrant(revokedToken);

console.log({
  patient: grant.patient,
  doctor: grant.doctor,
  recordId: grant.record_id,
  grantedAt: grant.granted_at,
  expiresAt: grant.expires_at,
  isRevoked: grant.is_revoked, // true
});

// This allows tracking:
// - Who had access
// - When access was granted
// - When access was revoked
// - Original expiration time

Impact on verify_access

After revocation, verify_access will fail:
// In verify_access finalize function:
assert(!grant.is_revoked); // ← This assertion fails
This prevents the doctor from accessing the record even if:
  • The expiration time hasn’t been reached
  • The token was valid before
  • The doctor address and record ID match

Best Practices

  1. Track grants locally: Maintain a database of active grants for easy revocation
  2. Provide clear UI: Make it easy for patients to see and revoke active grants
  3. Confirm before revoking: Ask for patient confirmation to prevent accidental revocations
  4. Notify doctors: Optionally inform doctors when access is revoked (via separate channel)
  5. Log revocations: Keep audit logs of when and why access was revoked
  6. Batch revocations: If revoking multiple grants, do them in parallel for efficiency
  7. Handle errors gracefully: Token might not exist or patient might not own it

Build docs developers (and LLMs) love