Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ jobs:
permissions:
contents: read
pull-requests: write
env:
CHAIN_1_ID: 1337
CHAIN_1_RPC_URL: http://localhost:8545
defaults:
run:
working-directory: ./
Expand Down
1 change: 1 addition & 0 deletions cspell-config/cspell-misc.txt
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ ethereum
sepolia
foundryup
unpermitted
reauthentication

// auth-server
oidc
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,5 +57,6 @@
"dependencies": {
"prettier-plugin-solidity": "^1.4.1",
"snarkjs": "^0.7.5"
}
},
"pnpm": {}
}
22 changes: 19 additions & 3 deletions packages/auth-server-api/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,23 @@ DEPLOYER_PRIVATE_KEY=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf
# Blockchain Configuration
RPC_URL=http://127.0.0.1:8545

# ===================================================================
# Supported Chains Configuration (REQUIRED)
# ===================================================================
# Define chains using numbered variables: CHAIN_1_*, CHAIN_2_*, etc.
# The system will automatically discover all chains starting from CHAIN_1.
#
# Required per chain:
# CHAIN_N_ID - Chain ID (positive integer)
# CHAIN_N_RPC_URL - RPC endpoint URL (optional in Prividium mode)
#
# Optional per chain:
# CHAIN_N_BASE_TOKEN_DECIMALS - Native token decimals (defaults to 18)
#
# ===================================================================
CHAIN_1_ID=1337
CHAIN_1_RPC_URL=http://localhost:8545

# Rate Limiting Configuration
RATE_LIMIT_DEPLOY_MAX=20
RATE_LIMIT_DEPLOY_WINDOW_MS=3600000 # 1 hour
Expand All @@ -24,10 +41,9 @@ RATE_LIMIT_DEPLOY_WINDOW_MS=3600000 # 1 hour
# SESSION_VALIDATOR_ADDRESS=0x...

# Prividium Mode Configuration (OPTIONAL)
# When enabled, requires user authentication via Prividium and routes deployments through Prividium RPC proxy
# When enabled, requires user authentication via Prividium and uses the Prividium SDK for RPC and auth
# PRIVIDIUM_MODE=true
# PRIVIDIUM_RPC_PROXY_BASE_URL=https://rpc.prividium.io
# PRIVIDIUM_PERMISSIONS_BASE_URL=https://permissions.prividium.io
# PRIVIDIUM_API_URL=https://api.prividium.io
# PRIVIDIUM_ADMIN_PRIVATE_KEY=0x... # Private key of a user with 'admin' role in Prividium
# PRIVIDIUM_TEMPLATE_KEY=sso-smart-account # Template key for whitelisting deployed contracts
# SSO_AUTH_SERVER_BASE_URL=https://sso.example.com # Base URL of the SSO auth server frontend (used as SIWE domain for admin authorization)
1 change: 1 addition & 0 deletions packages/auth-server-api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"dotenv": "^16.4.7",
"express": "^4.21.2",
"express-rate-limit": "^7.5.0",
"prividium": "^0.7.0",
"viem": "2.30.0",
"zksync-sso-4337": "workspace:*",
"zod": "^3.24.1"
Expand Down
3 changes: 2 additions & 1 deletion packages/auth-server-api/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,9 @@ app.use(express.json());

// CORS configuration
const allowlist = env.CORS_ORIGINS.split(",").map((origin) => origin.trim());
const isAllowAll = allowlist.includes("*");
const corsOrigins = (origin: string | undefined, callback: (err: Error | null, allow?: boolean) => void) => {
if (!origin || allowlist.indexOf(origin) !== -1) {
if (!origin || isAllowAll || allowlist.indexOf(origin) !== -1) {
callback(null, true);
} else {
callback(new Error("Not allowed by CORS"));
Expand Down
145 changes: 100 additions & 45 deletions packages/auth-server-api/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import { readFileSync } from "fs";
import { dirname, join } from "path";
import { fileURLToPath } from "url";
import { type Chain, defineChain } from "viem";
import { localhost } from "viem/chains";
import { z } from "zod";

// Load environment variables
Expand Down Expand Up @@ -42,8 +41,7 @@ const envSchema = z.object({
GUARDIAN_EXECUTOR_ADDRESS: z.string().optional(),
// Prividium Mode Configuration
PRIVIDIUM_MODE: z.string().transform((v) => v === "true").default("false"),
PRIVIDIUM_PERMISSIONS_BASE_URL: z.string().optional(),
PRIVIDIUM_RPC_PROXY_BASE_URL: z.string().optional(),
PRIVIDIUM_API_URL: z.string().optional(),
PRIVIDIUM_ADMIN_PRIVATE_KEY: z.string().optional(),
PRIVIDIUM_TEMPLATE_KEY: z.string().optional(),
SSO_AUTH_SERVER_BASE_URL: z.string().optional(),
Expand All @@ -64,8 +62,7 @@ try {
// Validate Prividium configuration when enabled
if (env.PRIVIDIUM_MODE) {
const missingPrividiumVars: string[] = [];
if (!env.PRIVIDIUM_PERMISSIONS_BASE_URL) missingPrividiumVars.push("PRIVIDIUM_PERMISSIONS_BASE_URL");
if (!env.PRIVIDIUM_RPC_PROXY_BASE_URL) missingPrividiumVars.push("PRIVIDIUM_RPC_PROXY_BASE_URL");
if (!env.PRIVIDIUM_API_URL) missingPrividiumVars.push("PRIVIDIUM_API_URL");
if (!env.PRIVIDIUM_ADMIN_PRIVATE_KEY) missingPrividiumVars.push("PRIVIDIUM_ADMIN_PRIVATE_KEY");
if (!env.PRIVIDIUM_TEMPLATE_KEY) missingPrividiumVars.push("PRIVIDIUM_TEMPLATE_KEY");
if (!env.SSO_AUTH_SERVER_BASE_URL) missingPrividiumVars.push("SSO_AUTH_SERVER_BASE_URL");
Expand Down Expand Up @@ -95,42 +92,92 @@ if (!FACTORY_ADDRESS || !EOA_VALIDATOR_ADDRESS || !WEBAUTHN_VALIDATOR_ADDRESS ||
process.exit(1);
}

// Supported chains configuration
const zksyncOsTestnet = defineChain({
id: 8022833,
name: "ZKsyncOS Testnet",
nativeCurrency: {
name: "Ether",
symbol: "ETH",
decimals: 18,
},
rpcUrls: {
default: {
http: ["https://zksync-os-testnet-alpha.zksync.dev"],
},
},
blockExplorers: {
default: {
name: "ZKsyncOS Testnet Explorer",
url: "https://zksync-os-testnet-alpha.staging-scan-v2.zksync.dev",
},
},
});
const zksyncOsLocal = defineChain({
id: 6565,
name: "ZKsyncOS Local",
nativeCurrency: {
name: "Ether",
symbol: "ETH",
decimals: 18,
},
rpcUrls: {
default: {
http: ["http://localhost:5050"],
},
},
});
const SUPPORTED_CHAINS: Chain[] = [localhost, zksyncOsTestnet, zksyncOsLocal];
/**
* Dynamically discovers and parses chain configurations from environment variables.
* Looks for CHAIN_N_ID, CHAIN_N_RPC_URL, etc. starting from N=1.
* Requires at least CHAIN_1_ID to be configured.
*/
function parseSupportedChains(): Chain[] {
// Check if CHAIN_1_ID exists
if (!process.env.CHAIN_1_ID) {
console.error("CHAIN_1_ID is required. Please configure at least one chain using CHAIN_N_* environment variables.");
console.error("\nExample configuration:");
console.error(" CHAIN_1_ID=1337");
console.error(" CHAIN_1_RPC_URL=http://localhost:8545");
console.error(" CHAIN_1_BASE_TOKEN_DECIMALS=18 # Optional, defaults to 18");
console.error("\nSee .env.example for more examples.");
process.exit(1);
}

const chains: Chain[] = [];
let chainIndex = 1;

// Keep discovering chains until CHAIN_N_ID doesn't exist
while (process.env[`CHAIN_${chainIndex}_ID`]) {
const chainIdStr = process.env[`CHAIN_${chainIndex}_ID`]!;
const rpcUrl = process.env[`CHAIN_${chainIndex}_RPC_URL`];
const decimalsStr = process.env[`CHAIN_${chainIndex}_BASE_TOKEN_DECIMALS`];

// RPC URL is required in non-prividium mode (prividium uses SDK transport)
if (!rpcUrl && !env.PRIVIDIUM_MODE) {
console.error(`CHAIN_${chainIndex}_RPC_URL is required but not provided`);
process.exit(1);
}

// Parse and validate chain ID
const chainId = parseInt(chainIdStr, 10);
if (isNaN(chainId) || chainId <= 0) {
console.error(`CHAIN_${chainIndex}_ID must be a positive integer, got: ${chainIdStr}`);
process.exit(1);
}

// Parse decimals (default to 18)
let decimals = 18;
if (decimalsStr) {
decimals = parseInt(decimalsStr, 10);
if (isNaN(decimals) || decimals < 0 || decimals > 18) {
console.error(`CHAIN_${chainIndex}_BASE_TOKEN_DECIMALS must be between 0-18, got: ${decimalsStr}`);
process.exit(1);
}
}

// Validate RPC URL format if provided
if (rpcUrl) {
try {
new URL(rpcUrl);
} catch {
console.error(`CHAIN_${chainIndex}_RPC_URL is not a valid URL: ${rpcUrl}`);
process.exit(1);
}
}

// Create chain with defaults for name and currency
const chain = defineChain({
id: chainId,
name: `Chain ${chainId}`,
nativeCurrency: {
name: "Ether",
symbol: "ETH",
decimals,
},
rpcUrls: {
default: {
http: rpcUrl ? [rpcUrl] : [],
},
},
});

chains.push(chain);
chainIndex++;
}

console.log(`Loaded ${chains.length} chain(s) from environment:`, chains.map((c) => `${c.name} (${c.id})`).join(", "));

return chains;
}

// Parse supported chains from environment
const SUPPORTED_CHAINS: Chain[] = parseSupportedChains();

function getChain(chainId: number): Chain {
const chain = SUPPORTED_CHAINS.find((c) => c.id === chainId);
Expand All @@ -143,20 +190,28 @@ function getChain(chainId: number): Chain {
// Prividium configuration object for services
export interface PrividiumConfig {
enabled: boolean;
permissionsApiUrl: string;
proxyUrl: string;
apiUrl: string;
adminPrivateKey: string;
templateKey: string;
ssoAuthServerBaseUrl: string;
domain: string;
}

const prividiumConfig: PrividiumConfig = {
enabled: env.PRIVIDIUM_MODE,
permissionsApiUrl: env.PRIVIDIUM_PERMISSIONS_BASE_URL || "",
proxyUrl: env.PRIVIDIUM_RPC_PROXY_BASE_URL ? `${env.PRIVIDIUM_RPC_PROXY_BASE_URL}/rpc` : "",
apiUrl: env.PRIVIDIUM_API_URL || "",
adminPrivateKey: env.PRIVIDIUM_ADMIN_PRIVATE_KEY || "",
templateKey: env.PRIVIDIUM_TEMPLATE_KEY || "",
ssoAuthServerBaseUrl: env.SSO_AUTH_SERVER_BASE_URL || "",
domain: (() => {
if (!env.SSO_AUTH_SERVER_BASE_URL) return "";
try {
return new URL(env.SSO_AUTH_SERVER_BASE_URL).host;
} catch {
console.error(`SSO_AUTH_SERVER_BASE_URL is not a valid URL: ${env.SSO_AUTH_SERVER_BASE_URL}`);
process.exit(1);
}
})(),
};

// Rate limiting configuration
Expand Down
36 changes: 23 additions & 13 deletions packages/auth-server-api/src/handlers/deploy-account.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import type { Request, Response } from "express";
import type { PrividiumSiweChain } from "prividium/siwe";
import { type Address, createPublicClient, createWalletClient, type Hex, http, parseEther } from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { waitForTransactionReceipt } from "viem/actions";
import { getAccountAddressFromLogs, prepareDeploySmartAccount } from "zksync-sso-4337/client";

import { env, EOA_VALIDATOR_ADDRESS, FACTORY_ADDRESS, getChain, GUARDIAN_EXECUTOR_ADDRESS, prividiumConfig, SESSION_VALIDATOR_ADDRESS, WEBAUTHN_VALIDATOR_ADDRESS } from "../config.js";
import { deployAccountSchema } from "../schemas.js";
import { addAddressToUser, createProxyTransport, getAdminAuthService, whitelistContract } from "../services/prividium/index.js";
import { addAddressToUser, getAdminAuthService, whitelistContract } from "../services/prividium/index.js";

type DeployAccountRequest = {
chainId: number;
Expand Down Expand Up @@ -34,12 +35,11 @@ export const deployAccountHandler = async (req: Request, res: Response): Promise
// Get chain from request
const chain = getChain(body.chainId);

// Get admin token if in Prividium mode (needed for RPC proxy, whitelisting, and address association)
let adminToken: string | undefined;
// Get SDK instance if in Prividium mode (provides authenticated transport and headers)
let adminSdk: PrividiumSiweChain | undefined;
if (prividiumConfig.enabled && req.prividiumUser) {
try {
const adminAuth = getAdminAuthService(prividiumConfig);
adminToken = await adminAuth.getValidToken();
adminSdk = getAdminAuthService().getSdkInstance();
} catch (error) {
console.error("Admin authentication failed:", error);
res.status(500).json({
Expand All @@ -49,9 +49,9 @@ export const deployAccountHandler = async (req: Request, res: Response): Promise
}
}

// Create transport - use Prividium proxy if enabled, otherwise direct RPC
const transport = prividiumConfig.enabled && adminToken
? createProxyTransport(prividiumConfig.proxyUrl, adminToken)
// Create transport - use SDK transport if enabled, otherwise direct RPC
const transport = adminSdk
? adminSdk.transport // SDK provides authenticated transport
: http(env.RPC_URL);

// Create clients
Expand Down Expand Up @@ -166,14 +166,24 @@ export const deployAccountHandler = async (req: Request, res: Response): Promise
console.log("Account deployed at:", deployedAddress);

// Prividium post-deployment steps (all blocking)
if (prividiumConfig.enabled && req.prividiumUser && adminToken) {
if (prividiumConfig.enabled && req.prividiumUser && adminSdk) {
// Get auth headers from SDK
const authHeaders = adminSdk.getAuthHeaders();
if (!authHeaders) {
console.error("Failed to get auth headers");
res.status(500).json({
error: "Authentication error",
});
return;
}

// Step 1: Whitelist the contract with template (blocking)
try {
await whitelistContract(
deployedAddress,
prividiumConfig.templateKey,
adminToken,
prividiumConfig.permissionsApiUrl,
authHeaders,
prividiumConfig.apiUrl,
);
} catch (error) {
console.error("Failed to whitelist contract:", error);
Expand All @@ -188,8 +198,8 @@ export const deployAccountHandler = async (req: Request, res: Response): Promise
await addAddressToUser(
req.prividiumUser.userId,
[deployedAddress],
adminToken,
prividiumConfig.permissionsApiUrl,
authHeaders,
prividiumConfig.apiUrl,
);
} catch (error) {
console.error("Failed to associate address with user:", error);
Expand Down
36 changes: 27 additions & 9 deletions packages/auth-server-api/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,31 @@ import type { Hex } from "viem";
import { privateKeyToAccount } from "viem/accounts";

import { allowlist, app } from "./app.js";
import { env } from "./config.js";

// Start server
const port = parseInt(env.PORT, 10);
app.listen(port, () => {
console.log(`Auth Server API listening on port ${port}`);
console.log(`CORS origins: ${allowlist.join(", ")}`);
console.log(`Deployer address: ${privateKeyToAccount(env.DEPLOYER_PRIVATE_KEY as Hex).address}`);
console.log(`RPC URL: ${env.RPC_URL}`);
import { env, prividiumConfig, SUPPORTED_CHAINS } from "./config.js";
import { initAdminAuthService } from "./services/prividium/admin-auth.js";

async function start() {
// Initialize Prividium admin auth at startup if enabled
if (prividiumConfig.enabled) {
const chain = SUPPORTED_CHAINS[0];
if (!chain) {
console.error("Prividium mode requires at least one configured chain");
process.exit(1);
}
await initAdminAuthService(prividiumConfig, chain);
}

// Start server
const port = parseInt(env.PORT, 10);
app.listen(port, () => {
console.log(`Auth Server API listening on port ${port}`);
console.log(`CORS origins: ${allowlist.join(", ")}`);
console.log(`Deployer address: ${privateKeyToAccount(env.DEPLOYER_PRIVATE_KEY as Hex).address}`);
console.log(`RPC URL: ${env.RPC_URL}`);
});
}

start().catch((error) => {
console.error("Failed to start server:", error);
process.exit(1);
});
Loading
Loading