open-index/open-npm huggingface dataset:
npm package: @justwant/actor
Description: Canonical Actor type and utilities. defineActor, actorKey, toRepo. No storage, agnostic.
Latest version: 0.1.0

# @justwant/actor

Canonical Actor type and utilities. No storage, agnostic. defineActor, actorKey, toRepo, parseActorKey.

## Installation

```bash
bun add @justwant/actor
```

## Usage

```ts
import {
  defineActor,
  actorKey,
  toRepo,
  parseActorKey,
  type Actor,
} from "@justwant/actor";

// Simple actor
const userActor = defineActor({ name: "user" });
const actor = userActor("usr_1");
// → { type: "user", id: "usr_1" }

// Actor with within (scoped context)
const userInOrg = defineActor({ name: "user", within: "org" });
const scopedActor = userInOrg("org_1", "usr_1");
// → { type: "user", id: "usr_1", within: { type: "org", id: "org_1" } }

// Composition with defineUser (from @justwant/user)
// defineActor({ from: defineUser() })

// Serialization
actorKey(actor); // → "user:usr_1"
actorKey(scopedActor); // → "user:usr_1:org:org_1"

// Repo shape for packages that persist (waitlist, lock, preference)
toRepo(scopedActor);
// → { actorType, actorId, actorWithinType, actorWithinId, actorOrgId? }

// Parse back
parseActorKey("user:usr_1:org:org_1");
// → { type: "user", id: "usr_1", within: { type: "org", id: "org_1" } }
```

## API

| Export | Description |
|--------|-------------|
| `defineActor` | Define actor type. Options: `{ name }`, `{ name, within }`, `{ from: IdentityLike }` |
| `actorKey` | Serialize actor to string. Format: `type:id` or `type:id:withinType:withinId` |
| `toRepo` | Flatten to repo shape (actorType, actorId, actorWithinType?, actorWithinId?, actorOrgId?) |
| `parseActorKey` | Parse actorKey back to Actor |
| `Actor` | Canonical identity type |
| `ActorWithin` | Optional locality (type, id) |
| `IdentityLike` | Structural interface for composition |
| `RepoShape` | Shape returned by toRepo |

## Agnostic within

The `within` field is agnostic of domain. Use any type that fits your context:

- `within: { type: "org", id: "org_1" }` — user in organisation
- `within: { type: "team", id: "team_1" }` — user in team
- `within: { type: "workspace", id: "ws_1" }` — user in workspace

When `within.type === "org"`, `toRepo` also sets `actorOrgId` for legacy compat with waitlist/lock.

## Exports

| Path | Contents |
|------|----------|
| `@justwant/actor` | defineActor, actorKey, toRepo, parseActorKey, types |
| `@justwant/actor/types` | Actor, ActorWithin, IdentityLike |
npm package: @justwant/bezier
Description: Cubic Bézier curves for easing. Core minimal, presets in subpaths (classic, feature, animation).
Latest version: 0.1.0

# @justwant/bezier

Cubic Bézier curves for easing. Core minimal (no presets), presets in subpaths by usage domain.

## Installation

```bash
bun add @justwant/bezier
```

## Usage

### Core (`@justwant/bezier`)

Custom params only — no presets in core.

```ts
import { createBezierCurve } from "@justwant/bezier";

const curve = createBezierCurve({ p1x: 0.42, p1y: 0, p2x: 0.58, p2y: 1 });
curve.at(0);   // 0
curve.at(1);   // 1
curve.at(0.5); // ~0.5 (y clamped to [0,1])
```

### Classic (`@justwant/bezier/classic`)

CSS-like presets: linear, ease-in, ease-out, ease-in-out.

```ts
import { linear, easeIn, easeOut, easeInOut } from "@justwant/bezier/classic";

linear.at(0.5);    // 0.5
easeIn.at(0.5);   // < 0.5 (slow at start)
easeOut.at(0.5);  // > 0.5 (slow at end)
easeInOut.at(0.5); // ≈ 0.5
```

### Feature (`@justwant/bezier/feature`)

Rollout/adoption curves for feature flags (used by `@justwant/flag` for `diffusionBezier`).

```ts
import { slowRollout, fastRollout, gradual } from "@justwant/bezier/feature";
```

### Animation (`@justwant/bezier/animation`)

Design/UI easing presets.

```ts
import { easeIn, easeOut, easeInOut } from "@justwant/bezier/animation";
```

## API

| Export | Description |
|--------|-------------|
| `createBezierCurve({ p1x, p1y, p2x, p2y })` | Create curve from control points (all ∈ [0,1]) |
| `BezierCurve.at(t)` | Evaluate at t ∈ [0,1], returns y ∈ [0,1] (clamped) |
| `InvalidBezierParamsError` | Thrown when params outside [0,1] |

## Subpaths

| Path | Presets |
|------|---------|
| `@justwant/bezier` | createBezierCurve, types, errors |
| `@justwant/bezier/classic` | linear, easeIn, easeOut, easeInOut |
| `@justwant/bezier/feature` | slowRollout, fastRollout, gradual |
| `@justwant/bezier/animation` | easeIn, easeOut, easeInOut |
| `@justwant/bezier/errors` | BezierError, InvalidBezierParamsError |

## License

MIT
npm package: @justwant/cache
Description: Unified key-value cache with interchangeable adapters. TTL, tag invalidation, namespacing, plugins.
Latest version: 0.1.0

# @justwant/cache

Interface unifiée de cache avec adapters interchangeables. Clé/valeur, TTL, invalidation par tag, namespacing.

## Installation

```bash
bun add @justwant/cache
```

## Usage

```ts
import { createCache } from "@justwant/cache";
import { memoryAdapter } from "@justwant/cache/adapters/memory";
import { namespacePlugin } from "@justwant/cache/plugins/namespace";

const cache = createCache({
  adapter: memoryAdapter(),
  plugins: [namespacePlugin({ prefix: "app" })],
  defaults: { ttl: "1h" },
});

await cache.set("user:123", { id: "123", name: "Alice" }, { ttl: "5m", tags: ["user:123"] });
const user = await cache.get("user:123");

await cache.wrap("user:456", () => db.findUser("456"), { ttl: "5m" });
await cache.invalidateTag("user:123");
```

## Subpaths

| Path | Description |
|------|-------------|
| `@justwant/cache` | createCache, types |
| `@justwant/cache/entry` | createCacheEntry |
| `@justwant/cache/collection` | createCacheCollection |
| `@justwant/cache/store` | createCacheStore |
| `@justwant/cache/adapters/memory` | In-memory adapter |
| `@justwant/cache/adapters/storage` | Simple Map adapter |
| `@justwant/cache/adapters/redis` | ioredis adapter |
| `@justwant/cache/adapters/upstash` | Upstash Redis adapter |
| `@justwant/cache/adapters/cf-kv` | Cloudflare KV adapter |
| `@justwant/cache/adapters/vercel-kv` | Vercel KV adapter |
| `@justwant/cache/adapters/tiered` | L1 + L2 tiered adapter |
| `@justwant/cache/plugins/namespace` | Key prefix plugin |
| `@justwant/cache/plugins/serialize` | Custom serializer plugin |
| `@justwant/cache/plugins/encrypt` | Encryption plugin |
| `@justwant/cache/plugins/stats` | Hit/miss stats plugin |
| `@justwant/cache/plugins/stale` | Stale-while-revalidate |
| `@justwant/cache/plugins/dedupe` | Concurrent deduplication |
| `@justwant/cache/plugins/prefetch` | Prefetch plugin |
| `@justwant/cache/plugins/audit` | Audit hooks plugin |
| `@justwant/cache/serializers/superjson` | Superjson serializer |
| `@justwant/cache/serializers/msgpack` | MessagePack serializer |

## Typed entries

```ts
import { createCache } from "@justwant/cache";
import { memoryAdapter } from "@justwant/cache/adapters/memory";
import { createCacheEntry } from "@justwant/cache/entry";
import * as v from "valibot";

const cache = createCache({ adapter: memoryAdapter() });

const userEntry = createCacheEntry({
  cache,
  key: (id) => `user:${id}`,
  schema: v.object({ id: v.string(), email: v.string() }),
  ttl: "5m",
  tags: (id) => [`user:${id}`, "users"] as const,
});

await userEntry.get("123");  // User | null
await userEntry.set("123", { id: "123", email: "a@b.com" });
await userEntry.wrap("123", () => db.findUser("123"));
```

## License

MIT
npm package: @justwant/config
Description: Config multi-source avec merge ordonné et typage fort
Latest version: 0.1.3

# @justwant/config

Multi-source config with ordered merge and strong typing. Define environments, sources, and values; resolve at runtime via waterfall lookup.

## Installation

```bash
bun add @justwant/config
```

## Usage

```ts
import {
  createConfigService,
  defineEnvironment,
  defineValue,
} from "@justwant/config";
import { defineEnvSource } from "@justwant/config/sources";

const envSource = defineEnvSource();

const prod = defineEnvironment({
  name: "production",
  sources: {
    databaseUrl: defineValue({ from: envSource, key: "DATABASE_URL" }),
    apiKey: [
      defineValue({ from: envSource, key: "API_KEY" }),
      defineValue({ from: envSource, key: "FALLBACK_KEY" }), // waterfall fallback
    ],
  },
});

const config = createConfigService({
  environments: [prod],
  defaultEnvironment: "production",
});

const dbUrl = await config.get<string>("databaseUrl");
const apiKey = await config.get("apiKey", "default"); // typed fallback
const hasKey = await config.has("apiKey");
```

## Standard Schema validation

Config supports [Standard Schema](https://github.com/standard-schema/standard-schema) for runtime validation. Pass a `schema` map (key → `StandardSchemaV1`) in `defineEnvironment`; values are validated on `get()`.

```ts
import { z } from "zod"; // or valibot, arktype, etc.

const prod = defineEnvironment({
  name: "production",
  sources: { port: defineValue({ from: envSource, key: "PORT" }) },
  schema: {
    port: z.coerce.number().min(1).max(65535),
  },
});
```

Invalid values throw `ConfigValidationError`. Use `validation: { onError: "warn" }` to log instead of throw.

## Exports

| Path | Description |
|------|-------------|
| `@justwant/config` | createConfigService, defineEnvironment, defineValue, types, errors |
| `@justwant/config/types` | ConfigSource, SourceLookup, ValueDef, EnvironmentDef, SourcesMap |
| `@justwant/config/errors` | ConfigError, ConfigValidationError |
| `@justwant/config/sources` | Source implementations |

## License

MIT
npm package: @justwant/context
Description: Modular context with explicit propagation. Typed slots, lazy/on-demand/eager resolution, per-instance cache. No AsyncLocalStorage. Node, Edge, Bun, Deno.
Latest version: 0.2.0

# @justwant/context

Modular context with **explicit propagation**. No AsyncLocalStorage — context is instantiated and passed to children. Compatible Node, Edge, Bun, Deno.

## Features

- **Explicit propagation** — context passed as parameter (Edge, browser compatible)
- **Typed slots** — `defineSlot` with lazy, on-demand, or eager resolution
- **Per-instance cache** — each `RequestContext` memoizes slot resolution
- **Framework-agnostic** — no runtime dependencies

## Installation

```bash
bun add @justwant/context
```

## Usage

### 1. Define slots

```ts
import { createContextService, defineSlot } from "@justwant/context";

const userSlot = defineSlot({
  key: "user",
  scope: "on-demand",
  resolve: async (ctx) => {
    const userId = ctx.initial.userId as string;
    return userApi.findById(userId);
  },
});

const orgSlot = defineSlot({
  key: "org",
  scope: "on-demand",
  resolve: async (ctx) => {
    const user = await ctx.get(userSlot);
    const orgId = ctx.initial.orgId as string;
    return orgApi.findById(orgId);
  },
});
```

### 2. Create context

```ts
const ctx = createContextService({ slots: [userSlot, orgSlot] });
```

### 3. Use per request

```ts
app.get("/docs", async (req, res) => {
  const requestCtx = ctx.forRequest({
    userId: req.user?.id,
    orgId: req.headers["x-org-id"],
    requestId: req.id,
  });
  const docs = await getDocuments(requestCtx);
  res.json(docs);
});

async function getDocuments(requestCtx) {
  const user = await requestCtx.get(userSlot); // resolved once, cached
  const org = await requestCtx.get(orgSlot); // depends on user, cached
  return db.findMany({ orgId: org.id });
}
```

## Slot scopes

| Scope       | When resolved      | Use case                          |
| ----------- | ------------------ | --------------------------------- |
| **eager**   | At `forRequest()`   | Values already known               |
| **request** | At `forRequest()`   | Depends on `initial` (requestId…) |
| **on-demand** | At first `get()`  | Expensive (DB, JWT, etc.)         |

## Integration with user, org, group

See [docs/INTEGRATION_USER_ORG_GROUP.md](./docs/INTEGRATION_USER_ORG_GROUP.md) for examples of flexible context injection with user, organisation, and group packages.

## Subpath exports

```ts
import { createContextService, defineSlot } from "@justwant/context";
import type { SlotDef, RequestContext } from "@justwant/context/types";
import { SlotNotFoundError, ResolutionError } from "@justwant/context/errors";
```

## License

MIT
npm package: @justwant/contract
Description: Contract definitions for defineContract, fields, conforms, validate. Schema-less, type-safe.
Latest version: 0.2.1

# @justwant/contract

Contract definitions for `defineContract`, fields, conforms, and validate. Schema-less, type-safe.

## Installation

```bash
bun add @justwant/contract
```

## Usage

```ts
import { defineContract, type Infer } from "@justwant/contract";
import { uuid, string, email } from "@justwant/contract/fields";

const UserContract = defineContract("users", {
  id: uuid().required().primaryKey(),
  email: email().required(),
  name: string().optional(),
});

type User = Infer<typeof UserContract>;
```

## Mapping (contract key → column name)

By default, contract keys map 1:1 to column names. Override with `defineContract(tableName, fields, options?)`:

```ts
defineContract("events", { user_id: string().required() });
// → column "user_id"
```

**camelCase → snake_case:** `defaultMapping: "camelToSnake"`:

```ts
defineContract("events", { userId: string().required() }, { defaultMapping: "camelToSnake" });
// → column "user_id"
```

**Override specific keys:** `mapping`:

```ts
defineContract("events", { userId: string().required() }, {
  mapping: { userId: { name: "user_id" } },
});
```

## Exports

- **Main** (`@justwant/contract`): `defineContract`, `field`, types, field builders, schemas, conforms, validate
- **Fields** (`@justwant/contract/fields`): `uuid`, `string`, `email`, etc. + schemas
- **Conforms** (`@justwant/contract/conforms`): `conformsTo`, `assertTableConforms`, `tableConforms`
- **Validate** (`@justwant/contract/validate`): `validateContractData`, `ContractValidationError`

## Peer dependency

- `@standard-schema/spec` (optional) — for schema validation
npm package: @justwant/id
Description: ID generation: sortable (by time) and short (URL-safe). Simple names, tool-agnostic. Subpath imports only.
Latest version: 0.1.0

# @justwant/id

ID generation: **sortable** (by time) and **short** (URL-safe). Simple function names, evocative export paths, tool-agnostic. Subpath imports only.

## Install

```bash
bun add @justwant/id
# or
npm i @justwant/id
```

## Usage

### Sortable IDs

IDs that sort by creation time. Good for primary keys and ordering.

```ts
import { createId, getTimestamp } from "@justwant/id/sortable";

const id = createId();
// => "01ARZ3NDEKTSV4RRFFQ69G5FAV"

getTimestamp(id); // creation time in ms
```

### Short IDs

Short, URL-safe IDs. Good for public IDs, slugs, short links.

```ts
import { createId } from "@justwant/id/short";

const id = createId();
// => "V1StGXR8_Z5jdHi6B-myT"

createId(10); // custom length
```

### Primitives

Raw lib exports for advanced use (monotonic counters, custom alphabets, etc.).

```ts
import { ulid, nanoid, decodeTime, monotonicFactory, customAlphabet } from "@justwant/id/primitives";
```

## Exports

| Subpath                 | Purpose                          |
|-------------------------|----------------------------------|
| `@justwant/id/sortable` | `createId()`, `getTimestamp(id)` — time-sortable |
| `@justwant/id/short`    | `createId(size?)` — short URL-safe |
| `@justwant/id/primitives` | `ulid`, `nanoid`, `decodeTime`, `isValid`, `monotonicFactory`, `customAlphabet` |

## License

MIT
npm package: @justwant/lock
Description: Distributed locks and semaphores. Mutex, ownership, counting semaphore. Declarative createLockable, createLockOwner, hooks.
Latest version: 0.2.0

# @justwant/lock

Distributed locks and semaphores. Mutex, ownership, counting semaphore. Declarative `createLockable`, `createLockOwner`, hooks.

## Installation

```bash
bun add @justwant/lock
# or
npm install @justwant/lock
```

## Usage

### Definitions

```ts
import {
  createLockable,
  createLockOwner,
  createLock,
  createSemaphore,
} from "@justwant/lock";

const maintenance = createLockable({ name: "maintenance", singular: true });
const order = createLockable({
  name: "order",
  singular: false,
  prefix: "app",
});

const system = createLockOwner({ name: "system" });
const user = createLockOwner({ name: "user", within: "org" });
```

### Mutex (createLock)

```ts
const lock = createLock({ repo: myLockRepository });

const owner = user("org_1", "user_42");
const lockable = order("ord_123");

const acquired = await lock.acquire(owner, lockable, { ttlMs: 5000 });
if (acquired) {
  try {
    // critical section
  } finally {
    await lock.release(owner, lockable);
  }
}
```

### Semaphore (createSemaphore)

```ts
const sem = createSemaphore({ repo: myLockRepository, max: 5 });

const acquired = await sem.acquire(owner, lockable, 2);
if (acquired) {
  try {
    // use 2 units
  } finally {
    await sem.release(owner, lockable, 2);
  }
}

const available = await sem.available(lockable);
```

### Hooks

```ts
import { createLock, auditLockHooks } from "@justwant/lock";

const hooks = auditLockHooks({
  onAcquire: (ctx, result) => log("lock.acquire", ctx.lockable.key, result.acquired),
  onRelease: (ctx, result) => log("lock.release", ctx.lockable.key, result.released),
});

const lock = createLock({ repo, hooks });
```

## Exports

| Entry | Content |
|-------|---------|
| `@justwant/lock` | Full API |
| `@justwant/lock/errors` | Error classes |
| `@justwant/lock/types` | LockOwner, Lockable, Lock, LockRepository |
| `@justwant/lock/hooks` | LockHooks, auditLockHooks |
| `@justwant/lock/lock` | createLock (mutex) |
| `@justwant/lock/semaphore` | createSemaphore |
| `@justwant/lock/define` | createLockable, createLockOwner |

## License

MIT
npm package: @justwant/membership
Description: Member–group liaison via defineMember, defineGroup, createMembershipService. No roles, no within. Member type declared in group.
Latest version: 0.3.0

# @justwant/membership

Member–group liaison via `defineMember`, `defineGroup`, and `createMembershipService`. No roles, no within. The member type is declared in the group.

## Features

- **defineMember** — defines a member type (e.g. `user`, `bot`)
- **defineGroup** — defines a group type and the member type it accepts
- **createMembershipService** — links members to groups with add/remove/has/listMembers/listGroups
- **Type safety** — a member can only be added to a group that accepts its type
- **Repo aligned** — `MembershipsRepo` follows @justwant/db pattern (findById, findOne, findMany, create, update, delete)

## Installation

```bash
bun add @justwant/membership
```

## Usage

```ts
import { defineMember, defineGroup, createMembershipService } from "@justwant/membership";

const userMember = defineMember({ name: "user" });
const orgGroup = defineGroup({ name: "org", member: userMember });
const groupGroup = defineGroup({ name: "group", member: userMember });

const membership = createMembershipService({
  repo: myMembershipsRepo, // implementation via @justwant/db
  groups: [orgGroup, groupGroup],
});

await membership.add(userMember("usr_1"), orgGroup("org_1"));
const isMember = await membership.has(userMember("usr_1"), orgGroup("org_1"));
const orgMembers = await membership.listMembers(orgGroup("org_1"));
const userOrgs = await membership.listGroups(userMember("usr_1"));
```

## API

### defineMember

```ts
const userMember = defineMember({ name: "user" });
const member = userMember("usr_1"); // { type: "user", id: "usr_1" }
```

### defineGroup

```ts
const orgGroup = defineGroup({ name: "org", member: userMember });
const group = orgGroup("org_1"); // { type: "org", id: "org_1" }
```

The `member` option indicates which member type the group accepts.

### createMembershipService

- `add(member, group)` — add member to group (throws if already member or type invalid)
- `remove(member, group)` — remove member from group (throws if not member)
- `has(member, group)` — check if member is in group
- `listMembers(group)` — list all members of a group
- `listGroups(member)` — list all groups a member belongs to

## Subpath exports

```ts
import { defineMember, defineGroup, createMembershipService } from "@justwant/membership";
import type { Member, Group, Membership, MembershipsRepo } from "@justwant/membership/types";
import {
  MembershipError,
  AlreadyMemberError,
  NotMemberError,
  InvalidMemberTypeError,
} from "@justwant/membership/errors";
```

## License

MIT
npm package: @justwant/preference
Description: Préférences utilisateur
Latest version: 0.2.0

# @justwant/preference

[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)

User preferences with typed schema. Declarative definition, actor-agnostic, storage via repository.

## Installation

```bash
bun add @justwant/preference
# or
npm install @justwant/preference
# or
pnpm add @justwant/preference
```

## Usage

```ts
import {
  definePreference,
  createPreferenceService,
  createMemoryPreferenceAdapter,
} from "@justwant/preference";

const themePref = definePreference({
  id: "theme",
  default: "system",
});

const service = createPreferenceService({
  preferences: [themePref],
  repo: createMemoryPreferenceAdapter(),
});

const user = { type: "user" as const, id: "usr_1" };

// Passer la définition directement (pas la clé en string)
await service.set(user, themePref, "dark");
const theme = await service.get(user, themePref); // "dark"
const all = await service.list(user); // { theme: "dark" }
await service.reset(user, themePref);
```

## API

| Export | Description |
|--------|-------------|
| `definePreference(options)` | Declarative preference (id, key?, schema?, default?) |
| `createPreferenceService(options)` | Service with list, get, set, setMany, reset |
| `createMemoryPreferenceAdapter()` | In-memory storage (tests/dev) |
| `createPreferenceDbAdapter(options)` | DB storage via MappedTable |

| Service method | Description |
|----------------|-------------|
| `list(actor)` | Get all preferences for actor |
| `get(actor, prefDef)` | Get single preference |
| `set(actor, prefDef, value)` | Set preference |
| `setMany(actor, entries)` | Set multiple |
| `reset(actor, prefDef)` | Reset to default |

## Actor

Identity uses `Actor` from `@justwant/actor`: `{ type, id, within? }`. Preferences are isolated per actor; an actor with `within: { type: "org", id: "org-1" }` has distinct preferences from the same actor without `within`.

## Subpaths

| Path | Description |
|------|-------------|
| `@justwant/preference` | Main API |
| `@justwant/preference/adapters/db` | DB adapter |
| `@justwant/preference/adapters/memory` | Memory adapter |
| `@justwant/preference/errors` | PreferenceError |

## Validation

Schemas must implement `StandardSchemaV1` (valibot, zod, etc.). Validation is optional; without schema, any value is accepted.

## License

MIT
npm package: @justwant/referral
Description: Referral and affiliation system. Parrainage, codes parrain, stats. Standalone package with defineReferralOffer + createReferralService.
Latest version: 0.2.2

# @justwant/referral

Referral and affiliation system. Parrainage, codes parrain, stats. Standalone package with `defineReferralOffer` + `createReferralService`.

## Terminology

| Term | Description |
|------|--------------|
| **ReferralOffer** | What can be referred (e.g. waitlist signup, first purchase) |
| **Referrer** | The actor who refers (sends the invitation) |
| **ReferralRecipient** | The referred actor (joins via the link) |
| **Referral** | The entity recording referrer → recipient for an offer |
| **ReferralCode** | Unique code/link for the referrer (optional) |

## Usage

### Define an offer

```ts
import { defineReferralOffer } from "@justwant/referral";

const waitlistOffer = defineReferralOffer({
  id: "waitlist_beta",
  name: "Waitlist Beta",
  defaultReferrerType: "user", // for resolveCode before any referral exists
  codeGenerator: (offerKey, referrer) => `ref-${referrer.id}`, // optional
});

// Parametrized offer
const listOffer = defineReferralOffer({
  id: "waitlist",
  params: ["listId"],
});
listOffer({ listId: "beta" }); // → "waitlist:listId:beta"
```

### Create the service

```ts
import { createReferralService } from "@justwant/referral";

const service = createReferralService({
  repo: myReferralRepository,
  plugins: [auditPlugin({ audit: myAudit })],
});
```

### API

- `refer(offer, referrer, recipient, metadata?)` — Register a referral
- `getReferrer(offer, recipient)` — Get referrer of a recipient
- `getRecipients(offer, referrer, opts?)` — List recipients of a referrer
- `getReferral(offer, referrer, recipient)` — Get referral if exists
- `countByReferrer(offer, referrer)` — Count referrals by referrer
- `countByOffer(offer)` — Total referrals for offer
- `getReferralCode(offer, referrer)` — Generate or retrieve referral code
- `resolveCode(offer, code)` — Resolve code → referrer

## Integration with waitlist

Use `createWaitlistReferralPlugin(referralService)` from `@justwant/waitlist/plugins/referral` to wire referral into waitlist subscriptions.