Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
9abb396
docs: mainnet v1 launch program design spec
y4hyya Aug 18, 2026
0ff9acb
docs(audit): seed the dossier — almanax + scout scans triaged at 01dfdea
y4hyya Aug 18, 2026
e5f2bd4
fix(contracts): re-arm storage rent on every hot path — R-1
y4hyya Aug 18, 2026
e9171ea
feat(contracts): admin rotation on market, referral, vault_factory — …
y4hyya Aug 18, 2026
4e6d998
fix(vault): fail-closed receipt check on market-credited inflows — R-4
y4hyya Aug 18, 2026
d0cdfed
refactor(contracts): typed overflow errors in share math + volume gua…
y4hyya Aug 18, 2026
b7366bb
feat(vault): LP slippage bounds + global principal cap — R-6
y4hyya Aug 18, 2026
992d693
fix(shim): SEP-40 twap age-bounds with the vendor lastprice tick — R-7
y4hyya Aug 18, 2026
da632de
feat(web): derive on-chain LP min-out bounds from a simulation pre-qu…
y4hyya Aug 18, 2026
973b406
feat(contracts): events on every admin config setter — R-13
y4hyya Aug 18, 2026
fa6f65d
docs(audit): register close-out + post-fix scout delta (416 -> 390)
y4hyya Aug 18, 2026
10e8232
feat(api): waitlist, access grants, unlock verify + admin surface — w…
y4hyya Aug 19, 2026
058b7d7
feat(web): waitlist + approved-wallet unlock on the gated teaser — wo…
y4hyya Aug 19, 2026
d6b655c
feat(web): /admin waitlist approvals panel — workstream A
y4hyya Aug 19, 2026
609ab8a
fix(web): pass NEXT_PUBLIC_TURNSTILE_SITE_KEY through the Docker build
y4hyya Aug 19, 2026
9f1d8be
fix(web): stale-wallet escape hatch on admin sign-in + unlock
y4hyya Aug 19, 2026
7cf917e
fix(api): ADMIN_WALLETS join the closed-beta issuance union
y4hyya Aug 19, 2026
7edbcfc
fix(web): kill the admin panel's infinite fetch loop
y4hyya Aug 19, 2026
912afed
feat(api): hype up the approval email (plain + html, CTA button)
y4hyya Aug 19, 2026
28ab48d
chore: lockfile for @azure/communication-email
y4hyya Aug 19, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
27 changes: 27 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -87,8 +87,35 @@ API_HMAC_PEPPER=
# Closed-beta allowlist — comma-separated Stellar G... addresses permitted to
# mint API keys via /v1/keys. Unset = key issuance open to anyone (dev only).
# REQUIRED in production: the API refuses to boot when this is unset/empty.
# Workstream A transition: wallets APPROVED in access_grants pass the gate
# too (union) — this env var retires once the DB migration is verified.
API_KEY_ALLOWLIST=

# ── Workstream A access system (waitlist + admin approvals) ──

# Cloudflare Turnstile SERVER secret for POST /v1/waitlist. Unset = the join
# endpoint answers 503 (fail loud, never silently bot-open). Cloudflare's
# always-pass test secret for local dev: 1x0000000000000000000000000000000AA
TURNSTILE_SECRET=

# Azure Communication Services — approval notification email. Both unset =
# approvals simply never email (admin panel shows unsent; resend later).
# Connection string: az communication list-key; sender: the MailFrom address
# of the provisioned email domain (DoNotReply@<id>.azurecomm.net for the
# Azure-managed domain).
ACS_CONNECTION_STRING=
ACS_SENDER=

# Wallets allowed into /v1/admin/* (comma-separated G... addresses — the
# founders' PERSONAL wallets, never the contract admin key). Empty = every
# admin route fails closed with 403.
ADMIN_WALLETS=

# Cloudflare Turnstile SITE key, inlined into the web build for the waitlist
# widget on /audit. Dev fallback (unset outside production) is Cloudflare's
# always-pass test sitekey.
NEXT_PUBLIC_TURNSTILE_SITE_KEY=

# CORS — comma-separated origins, or "*" for dev.
# REQUIRED in production: the API refuses to boot with the "*" wildcard;
# set the real frontend origin(s), e.g. https://noether.exchange
Expand Down
1 change: 1 addition & 0 deletions api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"clean": "rm -rf dist"
},
"dependencies": {
"@azure/communication-email": "^1.1.0",
"@fastify/cors": "^10.0.1",
"@fastify/swagger": "^9.4.0",
"@fastify/swagger-ui": "^5.2.0",
Expand Down
26 changes: 26 additions & 0 deletions api/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,25 @@ export interface ApiConfig {
* requirement.
*/
keeperHeartbeatSecret?: string;
/**
* Cloudflare Turnstile server secret for POST /v1/waitlist. Optional:
* unset makes the join endpoint 503 (fail loud, never silently
* bot-open). Cloudflare's always-pass test secret works for dev.
*/
turnstileSecret?: string;
/**
* Azure Communication Services connection string + verified sender for
* the approval email. Optional: unset means approvals simply never
* email (email_sent_at stays null; admin can resend later).
*/
acsConnectionString?: string;
acsSender?: string;
/**
* Wallets allowed into the /v1/admin/* surface (comma-separated env
* ADMIN_WALLETS — the founders' PERSONAL wallets, never the contract
* admin key). Empty = every admin route fails closed with 403.
*/
adminWallets: string[];
}

/** WebSocket abuse controls (audit A-5). All overridable via env. */
Expand Down Expand Up @@ -76,6 +95,13 @@ export function loadConfig(): ApiConfig {
sourceAccount: process.env.API_SOURCE_ACCOUNT ?? contracts.admin,
databaseUrl,
keeperHeartbeatSecret: process.env.KEEPER_HEARTBEAT_SECRET || undefined,
turnstileSecret: process.env.TURNSTILE_SECRET || undefined,
acsConnectionString: process.env.ACS_CONNECTION_STRING || undefined,
acsSender: process.env.ACS_SENDER || undefined,
adminWallets: (process.env.ADMIN_WALLETS ?? '')
.split(',')
.map((s) => s.trim())
.filter((s) => s.length === 56 && s.startsWith('G')),
contracts,
ws: {
maxConnections: Number(process.env.WS_MAX_CONNECTIONS ?? 1000),
Expand Down
22 changes: 22 additions & 0 deletions api/src/plugins/rateLimit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,29 @@ async function rateLimitPluginImpl(app: FastifyInstance, opts: RateLimitPluginOp
reply.header('Retry-After', String(decision.retryAfterSec));
return reply.code(429).send({ error: 'rate_limited', retry_after_sec: decision.retryAfterSec });
}

// Workstream A: the public waitlist join gets a much tighter per-IP
// budget than the generic 60/min — a signup is a once-per-human action,
// and each join costs a Turnstile verification round-trip.
if (
request.method === 'POST' &&
request.url.startsWith('/v1/waitlist') &&
!request.user
) {
const join = await opts.limiter.checkAndConsume(
`join:ip:${request.ip}`,
'public',
WAITLIST_JOIN_PER_MINUTE,
);
if (!join.allowed) {
reply.header('Retry-After', String(join.retryAfterSec));
return reply.code(429).send({ error: 'rate_limited', retry_after_sec: join.retryAfterSec });
}
}
});
}

/** Per-IP joins per minute — far below the public tier's 60. */
const WAITLIST_JOIN_PER_MINUTE = 5;

export const rateLimitPlugin = fp(rateLimitPluginImpl, { name: 'noether-rate-limit' });
Loading
Loading