-
Notifications
You must be signed in to change notification settings - Fork 2
Pact ApiKeys
Self-contained API key minting: PACT issues a public id and a one-time
secret, and asks you to store only a SHA-256 hash, so no external key service
is needed. Minting draws on @tundralibs/id (nanoID) and @tundralibs/crypt
(SHA-256) with zero external dependencies.
generateAPIKey(options?) returns a three-field pair — { id, secret, secretHash }:
import { PACT } from '@tundralibs/pact';
const pact = new PACT({ bits: { READ: 1n } }); // bits is the required registry
const key = await pact.generateAPIKey();
// {
// id: 'pact_ak_V1StGXR8Z5jdHi6B', // public — safe to store/index
// secret: 'pact_sk_IpoRWTff6Qw9y8xKn2…', // show once, never store
// secretHash: 'd2e1f0a3c7b4…', // SHA-256 hex — persist this
// }-
id— the public identifier, formatted<prefix>_ak_…. Safe to store, index, and log; use it to look the record up on verification. -
secret— the credential, formatted<prefix>_sk_…. Return it to the caller exactly once; it is never recoverable afterwards. -
secretHash— the SHA-256 hex ofsecret. This is the only part you persist for later verification.
generateAPIKey(options?) accepts:
| Option | Type | Default | Description |
|---|---|---|---|
prefix |
string |
'pact' |
Stamped on both parts (<prefix>_ak_… / <prefix>_sk_…) so keys are recognizable in logs and secret scanners. |
idLength |
number |
16 |
Random length of the id portion. |
secretLength |
number |
32 |
Random length of the secret. At the default this is ~168 bits of entropy over nanoID's web-safe alphabet. |
The consumer stores the id and the secretHash, and never the secret:
| Field | Persist? | Notes |
|---|---|---|
id |
Yes | Public identifier; safe to index and log. Look the record up by it. |
secretHash |
Yes | SHA-256 hex; the only value you compare against on verification. |
secret |
Never | Shown once at mint time. Not recoverable — if it is lost, mint a new key. |
The secret lives only in the response from generateAPIKey and in whatever you
hand to the caller — PACT keeps no copy. A leaked hash cannot be turned back
into a working secret.
verifyAPIKey(secret, secretHash) re-hashes the presented secret and compares
it against the stored hash in constant time, returning a boolean:
import { PACT } from '@tundralibs/pact';
const pact = new PACT({ bits: { READ: 1n } });
declare const presented: string;
declare const stored: { secretHash: string };
// `presented` comes off the request; `stored.secretHash` from your database
const ok = await pact.verifyAPIKey(presented, stored.secretHash);
if (!ok) {
// reject the request — the secret does not match the stored hash
}The comparison is length-checked and then digest-for-digest constant-time, so
it leaks no timing signal about how much of the secret matched. Neither
generateAPIKey nor verifyAPIKey throws for malformed input — a wrong or
empty secret simply returns false.
Mint on issue, store the id + hash, verify on each request:
import { PACT } from '@tundralibs/pact';
const pact = new PACT({ bits: { READ: 1n } });
// your own storage — PACT keeps no records
declare const db: {
apiKeys: {
insert(row: { id: string; secretHash: string }): Promise<void>;
findById(id: string): Promise<{ secretHash: string } | null>;
};
};
// 1. Mint — when a user creates an API key
async function issueKey() {
const key = await pact.generateAPIKey({ prefix: 'acme' });
// 2. Store — only the id + secretHash ever reach the database
await db.apiKeys.insert({ id: key.id, secretHash: key.secretHash });
// Show key.secret to the user exactly once; you will never see it again
return { id: key.id, secret: key.secret };
}
// 3. Verify — on an incoming request carrying its id and secret
async function authenticate(incoming: { id: string; secret: string }) {
const row = await db.apiKeys.findById(incoming.id);
return row !== null &&
await pact.verifyAPIKey(incoming.secret, row.secretHash);
}The minted secret is an ordinary string, so a caller can use it as the HMAC
key for request signing via sign() / verify():
import { PACT } from '@tundralibs/pact';
const pact = new PACT({ bits: { READ: 1n } });
const key = await pact.generateAPIKey();
declare const body: string;
// The client signs a request body with its secret…
const signature = await pact.sign(body, key.secret);
// …and the server verifies with the same secret.
const valid = await pact.verify(body, signature, key.secret);See Tokens for the full sign() / verify() and JWT
surface.
-
Tokens — JWT issue/verify/refresh and HMAC request signing with
sign()/verify().