> ## Documentation Index
> Fetch the complete documentation index at: https://cofhe-docs.fhenix.zone/llms.txt
> Use this file to discover all available pages before exploring further.

# Access Control Permissions (ACP)

> Create and manage the EIP-712 signatures that authorize decryption

An Access Control Permission (ACP) is an EIP-712 signature that authorizes decryption of confidential data. The `issuer` field identifies who is reading the data, and that address must already have been granted access onchain with `FHE.allow(handle, address)`. When you use an ACP, CoFHE validates it against the ACL contract to confirm the issuer really holds that access.

Every ACP carries a sealing keypair. The public key goes to CoFHE so it can re-encrypt the result for the holder. The private key stays on the client and unseals the value when it comes back.

<Note>
  ACPs were called permits before `0.7`. The name changed so they are not confused with an ERC-2612 permit, which is a different thing entirely. If you are upgrading, see [migrating to 0.7](/client-sdk/introduction/migrating-to-0-7). This page uses "ACP" throughout.
</Note>

## When you need one

* `decryptForView` always requires an ACP.
* `decryptForTx` depends on the contract's ACL policy for that handle. If the policy lets anyone decrypt, use `.withoutACP()`. If it restricts decryption, use `.withACP(...)`.

## Prerequisites

[Create and connect a client](/client-sdk/guides/client-setup). ACPs are scoped to a chainId and account pair.

## Quick start

<Info>
  `client.acp` is the recommended API. It signs with the connected wallet and manages the store for you. `ACPUtils` is the lower-level alternative when you want direct control over signing and storage. Note the namespace is singular: `client.acp`, not `client.acps`.
</Info>

<CodeGroup>
  ```typescript client.acp (recommended) theme={null}
  await client.connect(publicClient, walletClient);

  // Returns the active self ACP if there is one, otherwise creates and signs it.
  const acp = await client.acp.getOrCreateSelfACP();
  ```

  ```typescript ACPUtils theme={null}
  import { ACPUtils, setACP, setActiveACPHash } from '@cofhe/sdk/acps';

  const acp = await ACPUtils.createSelfAndSign(
    { issuer: walletClient.account.address },
    publicClient,
    walletClient
  );

  const chainId = await publicClient.getChainId();
  const account = walletClient.account.address;
  setACP(chainId, account, acp);
  setActiveACPHash(chainId, account, acp.hash);
  ```
</CodeGroup>

After this the active ACP is picked up automatically by `decryptForView(...).execute()` and by `decryptForTx(...).withACP().execute()`.

## The three types

| Type        | Who signs                                | Use for                                              |
| ----------- | ---------------------------------------- | ---------------------------------------------------- |
| `self`      | Issuer only                              | Decrypting your own data. The common case.           |
| `sharing`   | Issuer only                              | A shareable offer the issuer creates for a recipient |
| `recipient` | Recipient, carrying the issuer signature | The imported ACP after the recipient signs it        |

`expiration` is a unix timestamp in seconds and defaults to 7 days from creation. The client-wide `defaultACPExpiration` config key is a separate setting that defaults to 30 days. Creating an ACP through `client.acp.*` stores it and makes it active.

## Creating a self ACP

A self ACP lets you decrypt data that was allowed to your own address.

```typescript theme={null}
await client.connect(publicClient, walletClient);

const acp = await client.acp.createSelf({
  issuer: walletClient.account.address,
  name: 'My self ACP',
});

acp.type; // 'self'
acp.hash; // deterministic hash
```

`createSelf` always creates a new one. `getOrCreateSelfACP()` reuses the active ACP when there is one, which is what most applications want.

## Narrowing what an ACP can read

`0.7` adds a scope to every ACP. An unscoped ACP covers everything the issuer can read, which is rarely what you want to hand to someone else.

| Scope      | Value | Covers                                                 |
| ---------- | ----- | ------------------------------------------------------ |
| `Global`   | `0`   | Every value the issuer can read                        |
| `Contract` | `1`   | The issuer's values readable by the listed `contracts` |
| `Handles`  | `2`   | Only the listed `handles`, as bytes32 hex strings      |

```typescript theme={null}
const acp = await client.acp.createSharing({
  issuer: walletClient.account.address,
  recipient,
  contracts: [auctionAddress],   // scope narrows to this contract
  name: 'Auction results',
});
```

<Warning>
  A scope only ever narrows the issuer's existing access. It cannot grant access the issuer does not already hold, so scoping is not a way to delegate something you were never allowed to read. It also does not retroactively narrow ACPs you already issued.
</Warning>

Handles are bytes32 hex strings here, not bigints. If you are carrying handle values around as bigints, convert before putting them in an ACP.

## Sharing with another account

An issuer can delegate their ACL access to a recipient, who can then decrypt the issuer's data without holding their own `FHE.allow` grant. There are two routes: pass the offer yourself, or post it onchain.

### Passing the offer yourself

<Steps>
  <Step title="Issuer creates a sharing ACP">
    ```typescript theme={null}
    const sharingAcp = await client.acp.createSharing({
      issuer: walletClient.account.address,
      recipient,
      name: 'Share with recipient',
    });
    ```
  </Step>

  <Step title="Issuer exports it">
    ```typescript theme={null}
    import { ACPUtils } from '@cofhe/sdk/acps';

    const exported = ACPUtils.export(sharingAcp);
    ```

    The exported JSON holds no sensitive data and can travel over any channel.

    <Warning>
      `ACPUtils.export` throws unless the ACP is a signed sharing ACP. In `0.6` the equivalent call serialized anything you gave it. A call that used to always work, such as one made during a render, now always throws on a self ACP. Gate it on `acp.type === 'sharing'`.

      Never share the output of `serialize(acp)`. That is for local persistence and contains the sealing private key.
    </Warning>
  </Step>

  <Step title="Recipient imports and signs">
    ```typescript theme={null}
    const recipientAcp = await client.acp.importShared(exported);

    recipientAcp.type; // 'recipient'
    ```

    Importing generates a fresh sealing key for the recipient.
  </Step>
</Steps>

### Sharing onchain

The issuer can instead post the signed offer to a registry, so the recipient discovers it without a side channel.

```typescript theme={null}
// Issuer
await client.acp.shareOnChain(sharingAcp);

// Recipient
const shares = await client.acp.getIncomingShares();
const recipientAcp = await client.acp.importFromChain(shares[0]);
```

`dismissShare(shareId)` clears an entry the recipient does not want, and `cancelShare` withdraws one the issuer posted.

## Revoking access

An issuer can revoke an ACP they created, which matters when a sharing ACP has left their control.

```typescript theme={null}
await client.acp.revokeACP(acp.hash);
await client.acp.revokeAllACPs();

const revoked = await client.acp.isACPRevoked(acp.hash);
```

Revocation is checked when the ACP is used, so it applies to copies the issuer no longer holds.

## Managing stored ACPs

The SDK keeps every stored ACP and one active ACP hash per chainId and account.

```typescript theme={null}
const acps = client.acp.getACPs();
Object.keys(acps); // ACP hashes

const active = client.acp.getActiveACP();
client.acp.selectActiveACP(someACPHash);

client.acp.removeACP(acpHash);
client.acp.removeActiveACP();
```

<h2 id="validating-permits">
  Validating
</h2>

`ACPUtils.validate` enforces the full check: schema, signed, and not expired. The decrypt flows call it for you and surface failures as typed errors, so validate manually only when you want to inspect or filter ACPs first.

| Function                       | Checks                          | On failure                                              |
| ------------------------------ | ------------------------------- | ------------------------------------------------------- |
| `ACPUtils.validate(acp)`       | Schema, signed, and not expired | Throws                                                  |
| `ACPUtils.validateSchema(acp)` | Schema only                     | Throws on schema failure, ignores expiry and signatures |

Use `validateSchema` on an ACP that arrived over the wire, when you want to reject a malformed payload before caring about expiry.

For inspection without exception handling, `ValidationUtils` returns a typed result:

```typescript theme={null}
import { ValidationUtils } from '@cofhe/sdk/acps';

const result = ValidationUtils.isValid(acp);

result.valid; // boolean
result.error; // 'invalid-schema' | 'expired' | 'not-signed' | null
```

| Function                                     | Returns            | Use for                                                    |
| -------------------------------------------- | ------------------ | ---------------------------------------------------------- |
| `ValidationUtils.isValid(acp)`               | `ValidationResult` | The full check without throwing                            |
| `ValidationUtils.isSignedAndNotExpired(acp)` | `ValidationResult` | Skipping the schema parse when you already validated shape |
| `ValidationUtils.isSigned(acp)`              | `boolean`          | Whether it carries a signature on the appropriate side     |
| `ValidationUtils.isExpired(acp)`             | `boolean`          | Comparing `acp.expiration` to now                          |

<Tip>
  Match on `result.error` to render a precise message:

  ```typescript theme={null}
  switch (ValidationUtils.isValid(acp).error) {
    case 'expired':        return 'This ACP has expired. Please sign again.';
    case 'not-signed':     return 'ACP is awaiting signature.';
    case 'invalid-schema': return 'Imported ACP is malformed.';
    case null:             return null;
  }
  ```
</Tip>

## Persistence and security

* ACPs are stored per chainId and account. On the web the store is `localStorage` under the key `cofhesdk-acps`.
* A stored ACP contains the sealing private key. Treat it as a secret, and never hand a serialized ACP to another user.
* To share access, use `ACPUtils.export`, which strips the sensitive fields.

<Warning>
  Permits stored by `0.6` are dropped on upgrade. They were signed with EIP-712 types the upgraded ACL no longer accepts, so they cannot verify and are discarded when the store loads. Your users are prompted to sign again. Nothing needs migrating, but it looks like data loss if you are not expecting it.
</Warning>
