Monorepo for the Guardian off-chain signer service — the v1 contract specified at gist b50021fb3ee2c059d228b4dcddbb577e.
The Guardian attests privileged on-chain actions: a caller submits the intended action, the Guardian evaluates a fixed catalogue of policy & on-chain checks, and on success returns an EIP-712 signature that the caller submits alongside its transaction.
| Package | Path | Description |
|---|---|---|
@3flabs/guardian |
packages/guardian |
HTTP shell. Hosts plug in policy, signing, and chain access via a typed GuardianAbstractions contract. |
@3flabs/guardian-defaults |
packages/guardian-defaults |
Default Logger (pino), in-memory rate limiter, and Appendix-A check builders. |
@3flabs/guardian-test-fixtures |
packages/guardian-test-fixtures |
Private, never published. Shared on-chain fixtures (prool-anvil pool + foundry artifacts + deployment script) consumed by the integration suites in both public packages. |
@3flabs/guardian-defaults depends on @3flabs/guardian via workspace:*. The shell never owns
RPCs, secrets, or signing keys; the defaults package adds opinionated implementations a host can
plug in piece-by-piece.
- Bun workspaces (no pnpm); per-package builds with
tscemitting.js+.d.ts. - Elysia for routing, Zod for schemas.
- viem for chain reads & EIP-712 signing.
- better-result for typed
Result<T, E>flow. - @noble/hashes for HMAC-SHA-256.
- pino + pino-pretty for the default logger.
- oxlint + oxfmt for lint & format.
- vitest for tests.
- Changesets for versioning + npm publish via GitHub OIDC trusted-publisher (no
NPM_TOKENsecret).
bun install
bun run typecheck # tsc --noEmit per package + scripts/
bun run lint # oxlint --deny-warnings over packages, vitest configs, scripts/
bun run format # oxfmt over packages, vitest configs, scripts/
bun run test # vitest per package (unit only)
bun run test:integration # vitest, prool-anvil per worker (requires anvil on PATH)
bun run build # tsc emit .js + .d.ts per packageThe root build / test / test:integration / typecheck scripts invoke each package
explicitly via bun run --cwd packages/<pkg> …, so a missing package script fails loudly.
Bun itself is pinned through the root packageManager field (bun@1.4.2); CI reads it
via setup-bun's bun-version-file, so bumping Bun is a single-line change.
Per-package work:
bun run --cwd packages/guardian test
bun run --cwd packages/guardian-defaults buildThe integration suites in @3flabs/guardian and @3flabs/guardian-defaults boot a real
anvil per vitest worker via prool,
deploy the Guardian-relevant slice of the 3F protocol (Facility, RequestFactory + a real Request,
PositionManagerFactory + a real PositionManager, RequestWhitelist, TransferGuard,
IntentDescriptor, two MockERC20s, an OwnableMockFund, plus Multicall3 at the canonical address),
and exercise both the @3flabs/guardian-defaults Appendix-A check runners and the
@3flabs/guardian makeSign* orchestrators against the live deployment.
Prerequisites:
anvilonPATH(install viafoundryup).- Node ≥ 22 / Bun ≥ 1.1.
Run from the repo root:
bun run test:integrationThe shared fixtures package — @3flabs/guardian-test-fixtures — is private ("private": true)
and never published. It bundles foundry artifacts (bytecode + ABI) under artifacts/ stamped
with their source commit hash (suffixed -dirty when extracted from a tree with uncommitted
changes). Regenerate them when the sibling repos (grunt, 3f-request-whitelist) advance:
bun run fixtures:extract # rewrite artifacts/*.json from sibling out/ trees
bun run fixtures:check # validate bundled artifacts + fail-fast on sibling driftfixtures:check runs in CI on every PR. It always validates the bundled artifacts'
integrity and only performs the sibling-drift comparison for repos whose forge output is
actually present (printing a [fixtures] NOTICE for each absent repo), so it passes on a
fresh clone without sibling checkouts.
- After a code change, run
bun run changesetand pick the affected package(s) and bump kind. Commit the resulting.changeset/*.md. - Push to
main. Thereleaseworkflow opens (or updates) a "Version Packages" PR. - Merge that PR. The workflow re-runs and publishes to npm with provenance via GitHub OIDC.
Before changeset publish runs, scripts/rewrite-workspace-deps.ts rewrites the workspace
manifests: workspace:* ranges become real versions, private workspace devDependencies
(e.g. @3flabs/guardian-test-fixtures) are dropped from the published manifests, and the
script errors if a private workspace package appears in dependencies, peerDependencies,
or optionalDependencies.
One-time setup on npmjs.com: add each package as an OIDC trusted publisher pointing at
3FLabs/3f-guardian repo / release.yml workflow. Without this, the publish step will
fail because no NPM_TOKEN is configured (intentional — we don't want long-lived secrets).
packages/
├─ guardian/
│ ├─ src/
│ │ ├─ abstractions.ts types-only contract a host fulfils
│ │ ├─ server.ts buildGuardianServer(abs) ⇒ Elysia
│ │ ├─ index.ts public re-exports
│ │ ├─ errors/ tagged errors + envelope mapping (§6.5)
│ │ ├─ schemas/ zod primitives, headers, per-endpoint bodies
│ │ ├─ plugins/ raw-body, request-id, version-header, content-type, logger, auth, error-handler
│ │ ├─ routes/ one file per endpoint
│ │ ├─ typed-data/ EIP-712 build*TypedData builders + makeSign* orchestrators
│ │ ├─ signers/ dev private-key envelope (host substitutes KMS/HSM in prod)
│ │ └─ lib/ hmac, time, request-id, checks, result, signing, logger, eip712-domain, deadline
│ └─ tests/
├─ guardian-defaults/
│ ├─ src/
│ │ ├─ logger/pino.ts pinoLogger() — pino + pino-pretty
│ │ ├─ rate-limit/in-memory.ts inMemoryRateLimiter() with pluggable Store
│ │ ├─ cache/ AsyncCache<V> (schema-validated reads) + inMemoryCache (used by §A.1 / §A.4)
│ │ ├─ abi/ bundled on-chain ABIs and role/state constants
│ │ └─ checks/ buildIntentRequestBindingChecks / buildIntentFundBindingChecks / buildIntentSwapChecks / buildRequestWhitelistingChecks
│ └─ tests/
└─ guardian-test-fixtures/ private, never published
├─ src/
│ ├─ anvil-pool.ts prool Server + per-worker rpc URL
│ ├─ deploy.ts deployGuardianStack(client) ⇒ AddressBook
│ ├─ accounts.ts deterministic test accounts
│ ├─ clients.ts viem public/wallet/test clients + chain config
│ ├─ artifacts.ts foundry artifact loader
│ ├─ test-fixture.ts createIntegrationFixture() helper
│ └─ global-setup.ts vitest globalSetup (publishes addresses via provide())
├─ artifacts/ stamped foundry JSONs (regenerated via fixtures:extract)
└─ contracts/ OwnableMockFund.sol + vendored Multicall3.sol
MIT.