@dukkan.one/app-sdk/tokens
The TokenStore contract the client refreshes through, the in-memory and key-value adapters, and the AES-GCM sealer for tokens at rest.
- الحزمة
@dukkan.one/app-sdk@0.3.0- مراجعة الواجهة
2026-09-10- الاستيراد
import { … } from "@dukkan.one/app-sdk/tokens"
في هذه الصفحة
الدوال
createAesGcmSealer#
AES-256-GCM sealer with zero-downtime key rotation: seal with the current
key, open with current then previous. Keys are 32 bytes, base64. The wire
format (iv.tag.ciphertext, each base64) matches the first-party apps'
lib/crypto.mjs, so migrating an existing token table needs no rewrite.
createKeyValueTokenStore#
Token store over a key-value backend. Token rows are sealed before they
reach the backend; the refresh lock is a short lease taken with
putIfAbsent and polled by waiters.
tokensFromResponse#
Builds the StoredTokens row from a token response, computing accessExpiresAt from expires_in and keeping the previous store id and scopes when the response omits them.
الأصناف
MemoryTokenStore#
new MemoryTokenStore(initial?
Single-process store for tests, CLIs and scripts. The lock is a per-install promise chain, which is exactly what one process needs and nothing a second process could rely on: never use this behind more than one instance.
| الاسم | النوع | الوصف |
|---|---|---|
loadمطلوب | (installId: string): Promise<StoredTokens | null> | |
saveمطلوب | (tokens: StoredTokens): Promise<void> | |
withRefreshLockمطلوب | <T>(installId: string, fn: (locked: TokenStore) => Promise<T>): Promise<T> | Run |
markReconnectRequiredمطلوب | (installId: string, reason: string): Promise<void> | Record that the install must go through consent again; subsequent loads carry the reason. |
snapshotمطلوب | (installId: string): StoredTokens | null |
TokenManager#
new TokenManager(options
Hands out a valid access token for one install.
Refresh discipline (ADR 0012): one refresh at a time per install in this
process (single flight), and one at a time across processes through the
store's lock. Inside the lock the row is re-read: if a sibling already
rotated, its pair is used and no request is made. An invalid_grant gets
one more re-read (the platform answers it for a race it detected within
its 30 s leeway). If the row still holds the same refresh token, the lock
is released, the manager waits past the leeway, and presents the token
once more: a sibling that really rotated has saved by then, while a thief
who rotated our token is caught by the platform's reuse detection, which
revokes the whole family instead of leaving the stolen pair alive. Only
then is the install marked reconnect-required.
| الاسم | النوع | الوصف |
|---|---|---|
installIdمطلوب | string | |
getAccessTokenمطلوب | (options?: { forceRefresh?: boolean; }): Promise<string> | A token that is valid for at least the leeway; refreshes when needed. |
getTokensمطلوب | (): Promise<StoredTokens> | The stored row, refreshed when stale. |
الواجهات
KeyValueBackend#
The smallest surface a key-value backend needs (Cloudflare KV, Redis, Deno
KV, Upstash...). putIfAbsent is what makes the refresh lock real across
instances; without it the adapter falls back to an in-process mutex and
says so at construction.
| الاسم | النوع | الوصف |
|---|---|---|
getمطلوب | (key: string): Promise<string | null> | |
putمطلوب | (key: string, value: string, options?: { ttlSeconds?: number; }): Promise<void> | |
deleteمطلوب | (key: string): Promise<void> | |
putIfAbsent | ((key: string, value: string, options: { ttlSeconds: number; }) => Promise<boolean>) | undefined | Atomic "set if not present"; returns false when the key already exists. |
KeyValueTokenStoreOptions#
Configuration of createKeyValueTokenStore: the backend, an optional key prefix, the sealer, and the lock lease.
| الاسم | النوع | الوصف |
|---|---|---|
backendمطلوب | KeyValueBackend | |
prefix | string | undefined | Key prefix (default |
sealer | SecretSealer | undefined | |
lockTtlSeconds | number | undefined | Lease length for the refresh lock (default 20 s; refreshes take well under that). |
lockWaitMs | number | undefined | How long to wait for a held lock before giving up (default 25 s). |
onNoAtomicLock | ((message: string) => void) | undefined | |
onNoSealer | ((message: string) => void) | undefined | Called once when no sealer is configured (default: console.warn). |
SecretSealer#
Envelope encryption for tokens at rest (AES-256-GCM helper shipped in ./sealer).
| الاسم | النوع | الوصف |
|---|---|---|
sealمطلوب | (plaintext: string): Promise<string> | |
openمطلوب | (envelope: string): Promise<string> |
StoredTokens#
What an app persists per install. Tokens are stored by the app (encrypted
at rest through a SecretSealer), never by the SDK: the SDK only reads,
refreshes and writes back through the TokenStore contract.
| الاسم | النوع | الوصف |
|---|---|---|
installIdمطلوب | string | |
storeIdمطلوب | string | |
accessTokenمطلوب | string | |
accessExpiresAtمطلوب | string | RFC 3339 instant after which the access token is refused. |
refreshTokenمطلوب | string | |
refreshExpiresAt | string | null | undefined | |
scopes | Array<string> | undefined | |
reconnectRequired | string | null | undefined | Set when the platform said the install needs re-consent; calls throw until re-authorized. |
TokenManagerOptions#
Configuration of TokenManager: the install, the store, the credentials, and the refresh leeway.
| الاسم | النوع | الوصف |
|---|---|---|
installIdمطلوب | string | |
storeمطلوب | TokenStore | |
credentialsمطلوب | OAuthClientCredentials | |
apiUrlمطلوب | string | |
fetch | ((input: RequestInfo | URL, init?: RequestInit) => Promise<Response>) | undefined | |
refreshLeewaySeconds | number | undefined | Refresh this many seconds before the access token expires (default 60). |
raceRetryDelayMs | number | undefined | How long to wait before presenting the same refresh token a second time
after an |
timeoutMs | number | undefined | |
logger | DukkanLogger | undefined | |
now | (() => number) | undefined |
TokenStore#
Persistence contract every app implements once (adapters for Postgres and key-value stores ship with the SDK).
withRefreshLock is the important one: the platform revokes an install
when a rotated refresh token is presented again (theft detection), so two
instances must never refresh the same install concurrently. The lock
serialises refreshes across processes; the SDK re-reads inside it and
skips the refresh when a sibling already rotated.
| الاسم | النوع | الوصف |
|---|---|---|
loadمطلوب | (installId: string): Promise<StoredTokens | null> | |
saveمطلوب | (tokens: StoredTokens): Promise<void> | |
withRefreshLockمطلوب | <T>(installId: string, fn: (locked: TokenStore) => Promise<T>): Promise<T> | Run |
markReconnectRequiredمطلوب | (installId: string, reason: string): Promise<void> | Record that the install must go through consent again; subsequent loads carry the reason. |
الثوابت
IDENTITY_SEALER#
A sealer that stores plaintext. Only for tests and throwaway tools; production stores use createAesGcmSealer or their own.