Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
35d05c5
feat: add ERC-8128 auth plugin
Mar 1, 2026
f27db6f
test: add comprehensive ERC-8128 plugin tests
Mar 1, 2026
1074c9b
various fixes
jacopo-eth Mar 1, 2026
68cffc8
enforce request binding and non-replayability
jacopo-eth Mar 1, 2026
f3b0e7e
feat(erc8128): add well-known discovery endpoint
Mar 1, 2026
f92e754
chore(erc8128): align replayable cache with cookieCache model
Mar 1, 2026
5900076
fix(erc8128): sweep expired replayable cache entries on lookup
Mar 1, 2026
440c6c1
docs: add ERC-8128 plugin documentation
Mar 1, 2026
98f243c
feat(erc8128): route policy map, Accept-Signature headers, structured…
Mar 1, 2026
0327381
fix(erc8128): reconstruct request body for digest verification
Mar 1, 2026
e1fd923
fix(erc8128): skip route policy on plugin endpoints
Mar 1, 2026
893e230
fix(erc8128): check notBefore after full verification for replayable …
Mar 1, 2026
4152aa1
fix(erc8128): check notBefore after full verification for replayable …
Mar 1, 2026
1874f33
fix(erc8128): default notBefore to now+1 to invalidate same-second si…
Mar 1, 2026
bd17d59
docs
jacopo-eth Mar 1, 2026
3d06f16
docs(erc8128): rewrite docs - use viem instead of wagmi, match BA sty…
Mar 1, 2026
f4ce418
docs
jacopo-eth Mar 1, 2026
c7f4842
docs(erc8128): add positioning section - why ERC-8128 vs session-base…
Mar 2, 2026
fa1d14f
various fixes
jacopo-eth Mar 2, 2026
679875c
various fixes
jacopo-eth Mar 4, 2026
ef50742
feat(erc8128): add storeInDatabase option — use secondaryStorage for …
Mar 4, 2026
372027c
docs + tests: storeInDatabase option, secondaryStorage nonce/invalida…
Mar 4, 2026
58a0b42
fix: dual-write nonce store when storeInDatabase is true
Mar 4, 2026
aeab803
fixes
jacopo-eth Mar 4, 2026
c9299dc
constants
jacopo-eth Mar 4, 2026
ce7410f
add authPrecedence + fixes
jacopo-eth Mar 5, 2026
0af3f3c
various fixes
jacopo-eth Mar 5, 2026
949c90a
lib name
jacopo-eth Mar 5, 2026
1b6be85
feat(erc8128): capability-gated stateless mode with in-memory fallbacks
Mar 5, 2026
5d887da
bump
jacopo-eth Mar 5, 2026
581de74
nit
jacopo-eth Mar 5, 2026
b20fa53
wip
jacopo-eth Mar 5, 2026
8a88a8a
add support for multiple methods in routePolicy
jacopo-eth Mar 5, 2026
8cb83d5
various fixes
jacopo-eth Mar 7, 2026
0c7fa7f
refactor to use library helpers + add retry on accept-signature
jacopo-eth Mar 7, 2026
1187d0a
fix
jacopo-eth Mar 7, 2026
2143a41
added erc8128 auth api
jacopo-eth Mar 8, 2026
e90c520
wip
jacopo-eth Mar 8, 2026
632e950
fixes
jacopo-eth Mar 9, 2026
48664cb
remove in-memory map + fixes
jacopo-eth Mar 10, 2026
fb453bb
fix exports
jacopo-eth Mar 10, 2026
11038a8
wip
jacopo-eth Mar 10, 2026
2a76a8e
fix(erc8128): use atomic setIfNotExists for nonce consumption
Mar 10, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
635 changes: 635 additions & 0 deletions docs/content/docs/plugins/erc8128.mdx

Large diffs are not rendered by default.

44 changes: 44 additions & 0 deletions docs/content/docs/plugins/meta.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
{
"title": "Plugins",
"pages": [
"2fa",
"admin",
"anonymous",
"api-key",
"autumn",
"bearer",
"captcha",
"commet",
"community-plugins",
"creem",
"device-authorization",
"dodopayments",
"dub",
"email-otp",
"erc8128",
"generic-oauth",
"have-i-been-pwned",
"i18n",
"jwt",
"last-login-method",
"magic-link",
"mcp",
"multi-session",
"oauth-provider",
"oauth-proxy",
"oidc-provider",
"one-tap",
"one-time-token",
"open-api",
"organization",
"passkey",
"phone-number",
"polar",
"scim",
"siwe",
"sso",
"stripe",
"test-utils",
"username"
]
}
13 changes: 11 additions & 2 deletions packages/better-auth/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "better-auth",
"version": "1.5.1-beta.1",
"name": "@slicekit/better-auth",
"version": "1.5.2-erc8128.12",
"description": "The most comprehensive authentication framework for TypeScript.",
"type": "module",
"license": "MIT",
Expand Down Expand Up @@ -319,6 +319,11 @@
"dev-source": "./src/plugins/mcp/client/adapters.ts",
"types": "./dist/plugins/mcp/client/adapters.d.mts",
"default": "./dist/plugins/mcp/client/adapters.mjs"
},
"./plugins/erc8128": {
"dev-source": "./src/plugins/erc8128/index.ts",
"types": "./dist/plugins/erc8128/index.d.mts",
"default": "./dist/plugins/erc8128/index.mjs"
}
},
"typesVersions": {
Expand Down Expand Up @@ -484,6 +489,9 @@
],
"plugins/mcp/client/adapters": [
"./dist/plugins/mcp/client/adapters.d.mts"
],
"plugins/erc8128": [
"./dist/plugins/erc8128/index.d.mts"
]
}
},
Expand All @@ -499,6 +507,7 @@
"@better-fetch/fetch": "catalog:",
"@noble/ciphers": "^2.1.1",
"@noble/hashes": "^2.0.1",
"@slicekit/erc8128": "0.3.3-beta.4",
"better-call": "catalog:",
"defu": "^6.1.4",
"jose": "^6.1.3",
Expand Down
7 changes: 7 additions & 0 deletions packages/better-auth/src/api/routes/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,13 @@ export const getSession = <Option extends BetterAuthOptions>() =>
);
}

if (ctx.context.session) {
return ctx.context.session as {
session: Session<Option["session"], Option["plugins"]>;
user: User<Option["user"], Option["plugins"]>;
};
}

try {
const sessionCookieToken = await ctx.getSignedCookie(
ctx.context.authCookies.sessionToken.name,
Expand Down
18 changes: 18 additions & 0 deletions packages/better-auth/src/auth/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,30 @@ import {
resolveBaseURL,
} from "../utils/url";

type PluginWithServerApi = {
getServerApi?: (ctx: Promise<AuthContext> | AuthContext) => Record<string, unknown>;
};

export const createBetterAuth = <Options extends BetterAuthOptions>(
options: Options,
initFn: (options: Options) => Promise<AuthContext>,
): Auth<Options> => {
const authContext = initFn(options);
const { api } = getEndpoints(authContext, options);
const serverApi = options.plugins?.reduce<Record<string, unknown>>(
(acc, plugin) => {
const pluginWithServerApi = plugin as typeof plugin & PluginWithServerApi;
if (!pluginWithServerApi.getServerApi) {
return acc;
}
return {
...acc,
...pluginWithServerApi.getServerApi(authContext),
};
},
{},
);
Object.assign(api, serverApi);
const errorCodes = options.plugins?.reduce((acc, plugin) => {
if (plugin.$ERROR_CODES) {
return {
Expand Down
1 change: 1 addition & 0 deletions packages/better-auth/src/client/plugins/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export * from "../../plugins/anonymous/client";
export * from "../../plugins/custom-session/client";
export * from "../../plugins/device-authorization/client";
export * from "../../plugins/email-otp/client";
export * from "../../plugins/erc8128/client";
export * from "../../plugins/generic-oauth/client";
export * from "../../plugins/jwt/client";
export * from "../../plugins/last-login-method/client";
Expand Down
171 changes: 171 additions & 0 deletions packages/better-auth/src/plugins/erc8128/cleanup.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
import { describe, expect, it, vi } from "vitest";
import {
cleanupExpiredErc8128Storage,
createErc8128CleanupScheduler,
} from "./cleanup";

async function flushMicrotasks(ticks = 2) {
for (let index = 0; index < ticks; index += 1) {
await Promise.resolve();
}
}

function createMockCleanupAdapter() {
const rows = new Map<string, Array<Record<string, unknown>>>([
["erc8128Nonce", []],
["erc8128VerificationCache", []],
["erc8128Invalidation", []],
]);
const deleteMany = vi.fn(
async (args: {
model: string;
where: Array<{
field: string;
operator?: string;
value: unknown;
}>;
}) => {
const table = rows.get(args.model) ?? [];
let deleted = 0;
for (const row of [...table]) {
const matches = args.where.every((where) => {
if (where.field === "expiresAt" && where.operator === "lt") {
return (
row.expiresAt instanceof Date &&
row.expiresAt < (where.value as Date)
);
}
return row[where.field] === where.value;
});
if (matches) {
table.splice(table.indexOf(row), 1);
deleted += 1;
}
}
return deleted;
},
);

return {
rows,
adapter: {
deleteMany,
},
deleteMany,
};
}

function createMockSecondaryStorage() {
const store = new Map<string, { value: string; expiresAt: number }>();
return {
store,
storage: {
async get(key: string) {
const entry = store.get(key);
if (!entry) return null;
if (entry.expiresAt <= Date.now()) {
store.delete(key);
return null;
}
return entry.value;
},
async set(key: string, value: string, ttl?: number) {
store.set(key, {
value,
expiresAt: Date.now() + (ttl ?? 3600) * 1000,
});
},
async delete(key: string) {
store.delete(key);
},
},
};
}

describe("cleanupExpiredErc8128Storage", () => {
it("deletes expired rows across all ERC-8128 tables", async () => {
const { rows, adapter } = createMockCleanupAdapter();
const now = new Date("2026-01-01T00:00:00.000Z");
rows.get("erc8128Nonce")?.push(
{ id: "n1", expiresAt: new Date("2025-12-31T23:59:00.000Z") },
{ id: "n2", expiresAt: new Date("2026-01-01T00:01:00.000Z") },
);
rows.get("erc8128VerificationCache")?.push({
id: "c1",
expiresAt: new Date("2025-12-31T23:59:00.000Z"),
});
rows.get("erc8128Invalidation")?.push({
id: "i1",
expiresAt: new Date("2025-12-31T23:59:00.000Z"),
});

const result = await cleanupExpiredErc8128Storage({
adapter,
now,
});

expect(result).toEqual({
nonceDeleted: 1,
verificationCacheDeleted: 1,
invalidationDeleted: 1,
totalDeleted: 3,
});
expect(rows.get("erc8128Nonce")).toHaveLength(1);
expect(rows.get("erc8128VerificationCache")).toHaveLength(0);
expect(rows.get("erc8128Invalidation")).toHaveLength(0);
});
});

describe("createErc8128CleanupScheduler", () => {
it("throttles cleanup runs", async () => {
vi.useFakeTimers();
try {
const { deleteMany, adapter } = createMockCleanupAdapter();
const { storage } = createMockSecondaryStorage();
const scheduler = createErc8128CleanupScheduler({
adapter,
secondaryStorage: storage,
strategy: "auto",
throttleSec: 5 * 60,
});

await scheduler.schedule();
await scheduler.schedule();

expect(deleteMany).toHaveBeenCalledTimes(3);

vi.advanceTimersByTime(5 * 60 * 1000);
await scheduler.schedule();

expect(deleteMany).toHaveBeenCalledTimes(6);
} finally {
vi.useRealTimers();
}
});

it("does nothing when strategy is off", async () => {
const { deleteMany, adapter } = createMockCleanupAdapter();
const { storage } = createMockSecondaryStorage();
const scheduler = createErc8128CleanupScheduler({
adapter,
secondaryStorage: storage,
strategy: "off",
});

await scheduler.schedule();

expect(deleteMany).not.toHaveBeenCalled();
});

it("does nothing without secondaryStorage", async () => {
const { deleteMany, adapter } = createMockCleanupAdapter();
const scheduler = createErc8128CleanupScheduler({
adapter,
strategy: "auto",
});

await scheduler.schedule();

expect(deleteMany).not.toHaveBeenCalled();
});
});
Loading
Loading