Skip to content

Commit d11a18f

Browse files
committed
feat: publish passkety-bundler
1 parent 3c341f0 commit d11a18f

File tree

6 files changed

+279
-4
lines changed

6 files changed

+279
-4
lines changed
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
name: Passkey Bundler Docker
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
tags:
8+
- "passkey-bundler/v*"
9+
workflow_dispatch:
10+
11+
permissions:
12+
contents: read
13+
packages: write
14+
15+
concurrency:
16+
group: passkey-bundler-docker-${{ github.ref }}
17+
cancel-in-progress: true
18+
19+
jobs:
20+
changes:
21+
name: Detect changes
22+
runs-on: ubuntu-latest
23+
outputs:
24+
bundler: ${{ steps.determine.outputs.bundler }}
25+
steps:
26+
- name: Checkout
27+
uses: actions/checkout@v6
28+
with:
29+
fetch-depth: 0
30+
31+
- name: Determine whether to build
32+
id: determine
33+
run: |
34+
set -euo pipefail
35+
36+
if [[ "${GITHUB_EVENT_NAME}" == "workflow_dispatch" ]]; then
37+
echo "bundler=true" >> "$GITHUB_OUTPUT"
38+
exit 0
39+
fi
40+
41+
if [[ "${GITHUB_REF}" == refs/tags/passkey-bundler/v* ]]; then
42+
echo "bundler=true" >> "$GITHUB_OUTPUT"
43+
exit 0
44+
fi
45+
46+
before="${{ github.event.before }}"
47+
if [[ -z "${before}" || "${before}" == "0000000000000000000000000000000000000000" ]]; then
48+
echo "bundler=true" >> "$GITHUB_OUTPUT"
49+
exit 0
50+
fi
51+
52+
if git diff --name-only "${before}" "${GITHUB_SHA}" -- "passkey-bundler" ".github/workflows/passkey-bundler-docker.yml" | grep -q .; then
53+
echo "bundler=true" >> "$GITHUB_OUTPUT"
54+
exit 0
55+
fi
56+
57+
echo "bundler=false" >> "$GITHUB_OUTPUT"
58+
59+
build:
60+
name: Build (${{ matrix.build.platform }})
61+
needs: [changes]
62+
if: |
63+
github.event_name == 'workflow_dispatch' ||
64+
startsWith(github.ref, 'refs/tags/passkey-bundler/v') ||
65+
needs.changes.outputs.bundler == 'true'
66+
strategy:
67+
fail-fast: true
68+
matrix:
69+
build:
70+
- platform: linux/amd64
71+
runner: ubuntu-22.04
72+
- platform: linux/arm64
73+
runner: ubuntu-22.04-arm
74+
runs-on: ${{ matrix.build.runner }}
75+
76+
steps:
77+
- name: Checkout
78+
uses: actions/checkout@v6
79+
with:
80+
fetch-depth: 0
81+
82+
- name: Prepare env vars
83+
run: |
84+
ARCH="$(echo "${{ matrix.build.platform }}" | cut -d '/' -f 2)"
85+
echo "ARCH=$ARCH" >> "$GITHUB_ENV"
86+
echo "SHORT_SHA=${GITHUB_SHA::7}" >> "$GITHUB_ENV"
87+
OWNER="${{ github.repository_owner }}"
88+
OWNER="${OWNER,,}"
89+
echo "IMAGE=ghcr.io/${OWNER}/passkey-bundler" >> "$GITHUB_ENV"
90+
if [[ "${GITHUB_REF}" == refs/tags/passkey-bundler/v* ]]; then
91+
VERSION="${GITHUB_REF#refs/tags/passkey-bundler/v}"
92+
echo "TAG_PREFIX=$VERSION" >> "$GITHUB_ENV"
93+
else
94+
echo "TAG_PREFIX=main" >> "$GITHUB_ENV"
95+
fi
96+
97+
- name: Set up Docker Buildx
98+
uses: docker/setup-buildx-action@v3
99+
100+
- name: Login to GHCR
101+
uses: docker/login-action@v3
102+
with:
103+
registry: ghcr.io
104+
username: ${{ github.actor }}
105+
password: ${{ secrets.GITHUB_TOKEN }}
106+
107+
- name: Build and push (arch)
108+
uses: docker/build-push-action@v6
109+
with:
110+
context: passkey-bundler
111+
push: true
112+
platforms: ${{ matrix.build.platform }}
113+
tags: ${{ env.IMAGE }}:${{ env.TAG_PREFIX }}-${{ env.ARCH }}
114+
115+
merge:
116+
name: Create multi-arch image
117+
needs: [build]
118+
runs-on: ubuntu-latest
119+
120+
steps:
121+
- name: Prepare env vars
122+
run: |
123+
echo "SHORT_SHA=${GITHUB_SHA::7}" >> "$GITHUB_ENV"
124+
OWNER="${{ github.repository_owner }}"
125+
OWNER="${OWNER,,}"
126+
echo "IMAGE=ghcr.io/${OWNER}/passkey-bundler" >> "$GITHUB_ENV"
127+
if [[ "${GITHUB_REF}" == refs/tags/passkey-bundler/v* ]]; then
128+
VERSION="${GITHUB_REF#refs/tags/passkey-bundler/v}"
129+
echo "TAG_PREFIX=$VERSION" >> "$GITHUB_ENV"
130+
echo "IS_RELEASE=true" >> "$GITHUB_ENV"
131+
else
132+
echo "TAG_PREFIX=main" >> "$GITHUB_ENV"
133+
echo "IS_RELEASE=false" >> "$GITHUB_ENV"
134+
fi
135+
136+
- name: Set up Docker Buildx
137+
uses: docker/setup-buildx-action@v3
138+
139+
- name: Login to GHCR
140+
uses: docker/login-action@v3
141+
with:
142+
registry: ghcr.io
143+
username: ${{ github.actor }}
144+
password: ${{ secrets.GITHUB_TOKEN }}
145+
146+
- name: Create and push manifest list
147+
run: |
148+
set -euo pipefail
149+
if [[ "${IS_RELEASE}" == "true" ]]; then
150+
docker buildx imagetools create \
151+
--tag "${IMAGE}:${TAG_PREFIX}" \
152+
--tag "${IMAGE}:latest" \
153+
"${IMAGE}:${TAG_PREFIX}-amd64" \
154+
"${IMAGE}:${TAG_PREFIX}-arm64"
155+
else
156+
docker buildx imagetools create \
157+
--tag "${IMAGE}:main" \
158+
--tag "${IMAGE}:sha-${SHORT_SHA}" \
159+
"${IMAGE}:main-amd64" \
160+
"${IMAGE}:main-arm64"
161+
fi

.github/workflows/release.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ on:
44
push:
55
tags:
66
- "*"
7+
tags-ignore:
8+
- "passkey-bundler/v*"
79

810
permissions:
911
contents: write
@@ -289,4 +291,4 @@ jobs:
289291
body: ${{ steps.changes.outputs.changes }}
290292
files: |
291293
dist/**/*.tar.gz
292-
dist/nibid_${{ needs.version.outputs.value }}_checksums.txt
294+
dist/nibid_${{ needs.version.outputs.value }}_checksums.txt

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -362,3 +362,4 @@ debug_container.dot
362362
AGENTS.md
363363
precompile.test
364364
tx_log.json
365+
passkey-bundler/.tmp

passkey-bundler/Dockerfile

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
# syntax=docker/dockerfile:1
22

3-
FROM node:20-alpine AS build
3+
FROM node:20-bookworm-slim AS build
44
WORKDIR /app
55
COPY package*.json ./
66
RUN npm ci
77
COPY . .
88
RUN npm run build
99

10-
FROM node:20-alpine
10+
FROM node:20-bookworm-slim
1111
WORKDIR /app
1212
ENV NODE_ENV=production
1313
COPY --from=build /app/package*.json ./

passkey-bundler/src/config.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,14 @@ export function loadConfig(): BundlerConfig {
6969
}
7070

7171
function envOverrides(): RawConfig {
72-
const toBool = (value?: string) => (value ? ["1", "true", "yes"].includes(value.toLowerCase()) : undefined)
72+
const toBool = (value?: string) => {
73+
if (value === undefined) return undefined
74+
const normalized = value.trim().toLowerCase()
75+
if (!normalized) return undefined
76+
if (["1", "true", "yes", "y", "on"].includes(normalized)) return true
77+
if (["0", "false", "no", "n", "off"].includes(normalized)) return false
78+
return undefined
79+
}
7380
const toBigInt = (value?: string) => (value ? BigInt(value) : undefined)
7481
const toInt = (value?: string) => (value ? Number.parseInt(value, 10) : undefined)
7582

passkey-bundler/test/basic.test.ts

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,78 @@ import os from "node:os"
44
import path from "node:path"
55
import { test } from "node:test"
66

7+
import { loadConfig } from "../src/config"
78
import { RateLimiter } from "../src/rateLimiter"
89
import { SqliteStore } from "../src/store"
910
import { requiredPrefund } from "../src/userop"
1011
import { NonceManager } from "../src/submission"
1112
import { calldataGasCost, estimatePreVerificationGas } from "../src/validation"
1213

14+
function withEnv<T>(updates: Record<string, string | undefined>, fn: () => T): T {
15+
const previous: Record<string, string | undefined> = {}
16+
for (const [key, value] of Object.entries(updates)) {
17+
previous[key] = process.env[key]
18+
if (value === undefined) {
19+
delete process.env[key]
20+
} else {
21+
process.env[key] = value
22+
}
23+
}
24+
25+
try {
26+
return fn()
27+
} finally {
28+
for (const [key, value] of Object.entries(previous)) {
29+
if (value === undefined) {
30+
delete process.env[key]
31+
} else {
32+
process.env[key] = value
33+
}
34+
}
35+
}
36+
}
37+
38+
const bundlerEnvKeys = [
39+
"BUNDLER_CONFIG",
40+
"BUNDLER_MODE",
41+
"RPC_URL",
42+
"JSON_RPC_ENDPOINT",
43+
"ENTRY_POINT",
44+
"CHAIN_ID",
45+
"BUNDLER_PRIVATE_KEY",
46+
"BENEFICIARY",
47+
"BUNDLER_PORT",
48+
"METRICS_PORT",
49+
"MAX_BODY_BYTES",
50+
"BUNDLER_REQUIRE_AUTH",
51+
"RATE_LIMIT",
52+
"BUNDLER_API_KEYS",
53+
"MAX_QUEUE",
54+
"QUEUE_CONCURRENCY",
55+
"LOG_LEVEL",
56+
"ENABLE_PASSKEY_HELPERS",
57+
"DB_URL",
58+
"VALIDATION_ENABLED",
59+
"GAS_BUMP",
60+
"GAS_BUMP_WEI",
61+
"PREFUND_ENABLED",
62+
"MAX_PREFUND_WEI",
63+
"PREFUND_ALLOWLIST",
64+
"SUBMISSION_TIMEOUT_MS",
65+
"FINALITY_BLOCKS",
66+
"RECEIPT_LIMIT",
67+
"RECEIPT_POLL_INTERVAL_MS",
68+
] as const
69+
70+
function withBundlerEnv<T>(
71+
updates: Partial<Record<(typeof bundlerEnvKeys)[number], string | undefined>>,
72+
fn: () => T,
73+
): T {
74+
const reset: Record<string, string | undefined> = {}
75+
for (const key of bundlerEnvKeys) reset[key] = undefined
76+
return withEnv({ ...reset, ...updates }, fn)
77+
}
78+
1379
test("rate limiter enforces window", () => {
1480
const limiter = new RateLimiter(2, 1000)
1581
assert.equal(limiter.allow("a"), true)
@@ -148,3 +214,41 @@ test("sqlite store persists userOp records and receipts", async () => {
148214

149215
fs.rmSync(dir, { recursive: true, force: true })
150216
})
217+
218+
test("testnet mode defaults authRequired=true and requires API keys", () => {
219+
const entryPoint = "0x" + "11".repeat(20)
220+
const privateKey = "0x" + "22".repeat(32)
221+
222+
withBundlerEnv(
223+
{
224+
BUNDLER_MODE: "testnet",
225+
ENTRY_POINT: entryPoint,
226+
BUNDLER_PRIVATE_KEY: privateKey,
227+
DB_URL: "sqlite:./data/bundler.sqlite",
228+
},
229+
() => {
230+
assert.throws(
231+
() => loadConfig(),
232+
/Testnet mode requires API keys \(set BUNDLER_API_KEYS\) or disable authRequired/,
233+
)
234+
},
235+
)
236+
})
237+
238+
test("testnet mode respects BUNDLER_REQUIRE_AUTH=false", () => {
239+
const entryPoint = "0x" + "11".repeat(20)
240+
const privateKey = "0x" + "22".repeat(32)
241+
242+
const config = withBundlerEnv(
243+
{
244+
BUNDLER_MODE: "testnet",
245+
ENTRY_POINT: entryPoint,
246+
BUNDLER_PRIVATE_KEY: privateKey,
247+
DB_URL: "sqlite:./data/bundler.sqlite",
248+
BUNDLER_REQUIRE_AUTH: "false",
249+
},
250+
() => loadConfig(),
251+
)
252+
253+
assert.equal(config.authRequired, false)
254+
})

0 commit comments

Comments
 (0)