Skip to content

feat(guard): add Eve 0.34+ request/response approval support #4860

feat(guard): add Eve 0.34+ request/response approval support

feat(guard): add Eve 0.34+ request/response approval support #4860

Workflow file for this run

name: Guard
on:
push:
branches:
- main
- release-please--branches--main
paths:
- arcjet-guard/**
- .github/workflows/guard.yml
pull_request:
paths:
- arcjet-guard/**
- .github/workflows/guard.yml
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
env:
DO_NOT_TRACK: "1"
defaults:
run:
working-directory: arcjet-guard
jobs:
build:
name: "Build & Unit Test (Node ${{ matrix.node }})"
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
node: [22, 24, 26]
permissions:
contents: read
steps:
- name: Harden Runner
uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0
with:
disable-sudo-and-containers: true
egress-policy: audit
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Setup Node.js
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
with:
node-version: ${{ matrix.node }}
- name: Pin npm
uses: ./.github/actions/pin-npm
- name: Install dependencies
run: npm install
working-directory: ${{ github.workspace }}
# Build the whole workspace so @arcjet/guard's workspace dependencies
# (e.g. @arcjet/analyze) have their `dist/` output for guard's
# source tests and type checking.
- name: Build
run: npm run build
working-directory: ${{ github.workspace }}
- name: Unit tests (with coverage)
run: npm run test-unit
# The `eve` peer range advertises a floor, and development happens against
# the newest release — so nothing would notice if the namespace quietly
# started needing something the floor does not have. This pins the lowest
# version the range claims and runs the namespace against it.
#
# The install itself is half the check: npm resolves the peer range while
# installing, so a floor below what package.json advertises fails here
# rather than in a user's project.
#
# Runs before the eve-absent step because that one deletes eve outright.
- name: Unit tests at the lowest supported eve
run: |
floor=$(node -e "
const r = require('./arcjet-guard/package.json').peerDependencies.eve;
const m = /^>=\s*([0-9]+\.[0-9]+\.[0-9]+)/.exec(r);
if (!m) { console.error('cannot read a floor from peer range: ' + r); process.exit(1) }
process.stdout.write(m[1]);
")
echo "lowest supported eve: $floor"
npm install "eve@$floor" --no-save --workspace @arcjet/guard
# Assert what the namespace itself resolves, not merely what landed on
# disk: npm may nest the install, and eve's exports map does not expose
# ./package.json, so walk up from a subpath it does export.
node -e "
const { createRequire } = require('node:module');
const { dirname, join } = require('node:path');
const { existsSync } = require('node:fs');
const resolver = createRequire(join(process.cwd(), 'arcjet-guard/src/vercel-eve/v0/index.ts'));
let dir = dirname(resolver.resolve('eve/tools'));
while (!existsSync(join(dir, 'package.json'))) dir = dirname(dir);
const found = require(join(dir, 'package.json')).version;
if (found !== process.argv[1]) {
console.error('namespace resolves eve ' + found + ', expected the floor ' + process.argv[1]);
process.exit(1);
}
console.log('namespace resolves eve ' + found);
" "$floor"
node --test 'arcjet-guard/src/vercel-eve/**/*.test.ts'
working-directory: ${{ github.workspace }}
# The claude-agent-sdk namespace imports the SDK for types only. A static
# scan proves the imports are written `import type`; this proves the
# consequence, catching a build step that re-emits one as a value import.
#
# Runs before the eve-absent step because that one deletes a different
# optional peer. Typecheck legitimately fails without the SDK.
- name: Unit tests with claude-agent-sdk absent
run: |
rm -rf node_modules/@anthropic-ai/claude-agent-sdk arcjet-guard/node_modules/@anthropic-ai/claude-agent-sdk
node -e "try { require.resolve('@anthropic-ai/claude-agent-sdk', { paths: ['arcjet-guard/src/claude-agent-sdk/v0'] }); console.error('claude-agent-sdk still resolves'); process.exit(1) } catch (error) { if (error.code !== 'MODULE_NOT_FOUND') throw error }"
count=$(find arcjet-guard/src/claude-agent-sdk -name '*.test.ts' | wc -l)
test "$count" -ge 5 || { echo "only $count claude-agent-sdk test files matched; the glob has gone stale" >&2; exit 1; }
node --test 'arcjet-guard/src/claude-agent-sdk/**/*.test.ts'
working-directory: ${{ github.workspace }}
# The langgraph namespace imports @langchain/langgraph and
# @langchain/core for types only. A static scan proves the imports are
# written `import type`; this proves the consequence, catching a build
# step that re-emits one as a value import.
#
# Only `src/langgraph` is globbed. The suite that exercises the real
# ToolNode value-imports both peers, so it lives in `test/langgraph`
# and runs in the unit-test step above, where the peers are installed.
#
# Runs before the eve-absent step because that one deletes a different
# optional peer. Typecheck legitimately fails without the peers.
- name: Unit tests with langgraph absent
run: |
rm -rf node_modules/@langchain/langgraph node_modules/@langchain/core arcjet-guard/node_modules/@langchain/langgraph arcjet-guard/node_modules/@langchain/core
node -e "try { require.resolve('@langchain/langgraph', { paths: ['arcjet-guard/src/langgraph/v1'] }); console.error('langgraph still resolves'); process.exit(1) } catch (error) { if (error.code !== 'MODULE_NOT_FOUND') throw error }"
node -e "try { require.resolve('@langchain/core', { paths: ['arcjet-guard/src/langgraph/v1'] }); console.error('langchain core still resolves'); process.exit(1) } catch (error) { if (error.code !== 'MODULE_NOT_FOUND') throw error }"
count=$(find arcjet-guard/src/langgraph -name '*.test.ts' | wc -l)
test "$count" -ge 5 || { echo "only $count langgraph test files matched; the glob has gone stale" >&2; exit 1; }
node --test 'arcjet-guard/src/langgraph/**/*.test.ts'
working-directory: ${{ github.workspace }}
# The mastra namespace imports @mastra/core for types only. A static scan
# proves the imports are written `import type`; this proves the
# consequence, catching a build step that re-emits one as a value import.
#
# Runs before the eve-absent step because that one deletes a different
# optional peer. Typecheck legitimately fails without @mastra/core.
- name: Unit tests with mastra absent
run: |
rm -rf node_modules/@mastra/core arcjet-guard/node_modules/@mastra/core
node -e "try { require.resolve('@mastra/core', { paths: ['arcjet-guard/src/mastra/v1'] }); console.error('mastra still resolves'); process.exit(1) } catch (error) { if (error.code !== 'MODULE_NOT_FOUND') throw error }"
count=$(find arcjet-guard/src/mastra -name '*.test.ts' | wc -l)
test "$count" -ge 5 || { echo "only $count mastra test files matched; the glob has gone stale" >&2; exit 1; }
node --test 'arcjet-guard/src/mastra/**/*.test.ts'
working-directory: ${{ github.workspace }}
# The vercel-eve namespace imports eve for types only, which is what lets
# guard support Node 22 at all — eve declares engines.node ">=24". A static
# scan proves the imports are written `import type`; this proves the
# consequence, catching a build step that re-emits one as a value import.
#
# Runs on every leg of the matrix rather than just Node 22, because the
# criterion is about all three. Runs last because it deletes a
# devDependency: the typecheck legitimately fails without eve, since types
# are a build-time dependency.
- name: Unit tests with eve absent
run: |
rm -rf node_modules/eve arcjet-guard/node_modules/eve
node -e "try { require.resolve('eve', { paths: ['arcjet-guard/src/vercel-eve/v0'] }); console.error('eve still resolves'); process.exit(1) } catch (error) { if (error.code !== 'MODULE_NOT_FOUND') throw error }"
count=$(find arcjet-guard/src/vercel-eve -name '*.test.ts' | wc -l)
test "$count" -ge 5 || { echo "only $count vercel-eve test files matched; the glob has gone stale" >&2; exit 1; }
node --test 'arcjet-guard/src/vercel-eve/**/*.test.ts'
working-directory: ${{ github.workspace }}
lint:
name: Lint & Typecheck
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Harden Runner
uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0
with:
disable-sudo-and-containers: true
egress-policy: audit
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Setup Node.js
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
with:
node-version: 24
- name: Pin npm
uses: ./.github/actions/pin-npm
- name: Install dependencies
run: npm install
working-directory: ${{ github.workspace }}
# Build the whole workspace so @arcjet/guard's workspace dependencies
# (e.g. @arcjet/analyze) have their `dist/` output for guard's
# source tests and type checking.
- name: Build
run: npm run build
working-directory: ${{ github.workspace }}
- name: Lint (oxlint)
run: npm run lint
- name: Type check (tsgo)
run: npm run typecheck
runtime:
name: "Runtime (${{ matrix.runtime }}${{ matrix.node-version && format(' node{0}', matrix.node-version) || matrix.deno-version && format(' {0}', matrix.deno-version) || matrix.bun-version && format(' {0}', matrix.bun-version) || '' }})"
runs-on: ubuntu-latest
needs: build
strategy:
fail-fast: false
matrix:
include:
- runtime: node
script: test-runtime-node
node-version: 22
- runtime: node
script: test-runtime-node
node-version: 24
- runtime: node
script: test-runtime-node
node-version: 26
- runtime: fetch
script: test-runtime-fetch
node-version: 22
- runtime: fetch
script: test-runtime-fetch
node-version: 24
- runtime: fetch
script: test-runtime-fetch
node-version: 26
- runtime: cloudflare
script: test-runtime-cloudflare
- runtime: bun
script: test-runtime-bun
bun-version: 1.3.0
- runtime: bun
script: test-runtime-bun
bun-version: latest
- runtime: deno
script: test-runtime-deno
deno-version: lts
- runtime: deno
script: test-runtime-deno
deno-version: latest
permissions:
contents: read
steps:
- name: Harden Runner
uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0
with:
disable-sudo-and-containers: true
egress-policy: audit
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Setup Node.js
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
with:
node-version: ${{ matrix.node-version || '24' }}
- name: Pin npm
uses: ./.github/actions/pin-npm
- name: Setup Bun
if: matrix.runtime == 'bun'
uses: oven-sh/setup-bun@3d267786b128fe76c2f16a390aa2448b815359f3 # v2.1.2
with:
bun-version: ${{ matrix.bun-version }}
- name: Setup Deno
if: matrix.runtime == 'deno'
uses: denoland/setup-deno@e95548e56dfa95d4e1a28d6f422fafe75c4c26fb # v2.0.3
with:
deno-version: ${{ matrix.deno-version }}
- name: Install dependencies
run: npm install
working-directory: ${{ github.workspace }}
# Build the whole workspace so guard's workspace dependencies are
# available to the runtime tests.
- name: Build
run: npm run build
working-directory: ${{ github.workspace }}
- name: Runtime test (${{ matrix.runtime }})
run: npm run ${{ matrix.script }}