From 1766780bcabc20c4cfbc09472499555f1e4bd486 Mon Sep 17 00:00:00 2001 From: Moheeb Zara Date: Sat, 1 Aug 2026 18:40:33 -0700 Subject: [PATCH] =?UTF-8?q?types:=20restore=20Supabase=20row=20typing,=20f?= =?UTF-8?q?ix=20parseInput=20inference=20(586=E2=86=9276=20tsc=20errors)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The installed supabase-js (2.99, 5 generics) is incompatible with @supabase/ssr@0.6.1 (calls it with 3), collapsing the server client's schema to `never` — the root cause of ~450 of the 586 hidden type errors (next.config typescript.ignoreBuildErrors masks them all). - lib/supabase/server.ts: annotate the factory as SupabaseClient and bridge the ssr/js impedance mismatch at the one documented boundary; type the setAll cookie params. - lib/supabase/proxy.ts, proxy.ts: type the setAll cookie params. - lib/auth-helpers.ts: parseInput now infers its result from the schema via z.infer (was a fragile ZodSchema that widened outputs to {}). - lib/validations.ts: annotate flexibleDateTime's output type. - types/database.ts: add class_signup_tx / class_cancel_tx RPC signatures (called but absent; pending full regeneration). - tsconfig.json: target ES2020 (door-log-logic uses BigInt literals). - vitest.setup.ts: import beforeAll/afterAll instead of relying on globals. No runtime behavior change; 639 tests still green. ignoreBuildErrors stays on until the remaining errors reach zero. Co-Authored-By: Claude Opus 4.8 (1M context) --- docs/AUDIT-2026-08.md | 230 +++++++++++++++++++++++++++++++++++++++++ lib/auth-actions.ts | 5 +- lib/auth-helpers.ts | 7 +- lib/supabase/proxy.ts | 7 +- lib/supabase/server.ts | 25 ++++- lib/validations.ts | 2 +- proxy.ts | 7 +- tsconfig.json | 2 +- types/database.ts | 10 ++ vitest.setup.ts | 2 +- 10 files changed, 281 insertions(+), 16 deletions(-) create mode 100644 docs/AUDIT-2026-08.md diff --git a/docs/AUDIT-2026-08.md b/docs/AUDIT-2026-08.md new file mode 100644 index 0000000..9a7064e --- /dev/null +++ b/docs/AUDIT-2026-08.md @@ -0,0 +1,230 @@ +# Audit & Architecture Plan — hackerspace-management (2026-08) + +Branch: `audit/cleanup-architecture`. Method: 14-agent adversarial audit (each security/correctness +finding independently verified against RLS policies, code authz gates, and DB constraints before it +counts). **All 16 risk findings survived verification; 0 were refuted** — the finders were disciplined, +not noise. Every finding below has file:line evidence and a concrete failure scenario. + +## Baseline health (verified today, main @ 9ac0c24) + +| Check | State | +|---|---| +| `tsc --noEmit` | **586 errors**, all hidden by `next.config.mjs typescript.ignoreBuildErrors:true` | +| `eslint .` | **fails** — no flat config present (ESLint 9 needs `eslint.config.js`) | +| `vitest run` (unit) | **639 tests / 32 files, all green** (~11s) | +| integration / e2e | present but self-skip without a DB / are largely vacuous (see Tests) | +| `build` | passes *because* type errors are ignored | +| Package manager | **pnpm** (not npm); there is **no `typecheck` script** | + +**Headline:** this is a well-built codebase. The server-action pattern (`parseInput`(Zod) → +`requireMemberWithRole` → `.eq('space_id', self.space_id)` → `logActivity`) is uniform and correct, RLS +is enabled on all 50 tables, and the layering the brief asks for is ~70% already present. The real work +is (a) a handful of concrete bugs, (b) turning type-checking back on, (c) making tests actually test, +and (d) subdividing giant files along seams that already exist — **evolve, don't rewrite.** + +--- + +## 1. Correctness & security findings (ranked, all verified) + +### HIGH + +**H1 — Invite `max_uses` is a check-then-increment TOCTOU** · `lib/auth-actions.ts:210` +Two users redeem a `max_uses=1` invite simultaneously: both read `uses_count=0`, both pass the JS cap +check, both insert a membership, and the non-atomic `uses_count = invite.uses_count + 1` write leaves +the counter at 1 instead of 2. Single-use invites become multi-use; counter corrupts. +*Fix:* one atomic guarded statement — `UPDATE space_invites SET uses_count = uses_count + 1 WHERE id = :id +AND (max_uses IS NULL OR uses_count < max_uses) RETURNING id`; only proceed if a row came back. + +### MEDIUM + +**M1 — Board can re-arm an existing admin-granting invite** · `lib/actions/invites.ts:89` (privilege escalation) +`updateInvite` runs the `canAssignInviteRole` guard **only when `role` is in the payload**. A board member +(who cannot *mint* an admin invite) can call `updateInvite(id, { is_enabled:true, max_uses:5 })` on an +exhausted/disabled admin invite — no `role` field — and re-arm it. RLS `invites_update` only checks +admin/board, so nothing stops it. Board thereby distributes admin access it can't create directly. +*Fix:* load the existing invite's role and require `canAssignInviteRole(member.role, existingRole)` +whenever any privilege-affecting field (`is_enabled→true`, `max_uses`, `expires_at`) changes. + +**M2 — `appealIncident` role gate is broader than the RLS write; silent partial failure** · `lib/actions/incidents.ts:247` +Gated by `requireMember` (any member — the docstring intends reporters to appeal), but the incident +`UPDATE` is restricted to admin/board by `incidents_update` RLS. A reporter's appeal **inserts a draft +proposal, silently updates 0 incident rows, and returns success** — orphaned proposal, incident never +linked. *Fix:* do the incident update via admin client after re-authorizing caller as reporter-or-admin, +**or** add a reporter clause to `incidents_update` RLS; and assert rows-affected before creating the proposal. + +**M3 — SSRF filter misses hex IPv4-mapped IPv6** · `lib/door-logic.ts:72` +`isBlockedDoorIp` blocks the dotted form `::ffff:169.254.169.254` but not the hex form `::ffff:a9fe:a9fe` +(= 169.254.169.254 IMDS) or `::ffff:7f00:1` (= 127.0.0.1). Defense-in-depth gap on the door egress / +metadata-endpoint boundary. Node resolvers currently emit the dotted form, so not exploitable *today*, +but it's a latent SSRF bypass. *Fix:* decode `::ffff:HHHH:HHHH` to its 32-bit IPv4 and run the same checks. + +**M4 — `logCashPayment` stamps `last_paid_at` from a caller-supplied (backdatable) date** · `lib/actions/payments.ts:56` +**M5 — `linkPaymentToMember` overwrites `last_paid_at` with the linked payment's date** · `lib/actions/payments.ts:104` +Both unconditionally set `last_paid_at` and force `payment_status='current'` with no `GREATEST` guard. +Linking/logging a backdated (e.g. CSV-imported) payment **regresses** a member's dues state — a +currently-paid member can be pushed backward or a lapsed member wrongly shown current. `flexibleDateTime` +has no lower bound, so historical dates are real inputs. *Fix:* advance-only (`GREATEST(last_paid_at, txn)`), +derive `payment_status` from the resulting effective date. + +**M6 — Null deref crashes the members list on search** · `app/(app)/members/members-client.tsx:110` +`m.display_name.toLowerCase()` / `m.email.toLowerCase()` — both columns are nullable (schema + types; +the `WHERE email IS NOT NULL` partial index proves null-email rows are by design, e.g. imported members). +Typing any character into the search box throws `TypeError` and blanks the whole members page. +*Fix:* optional-chain like the adjacent `handle` already does. + +### LOW (confirmed) + +- **L1** `reserveEquipment` doesn't verify the manager-supplied `memberId` is in the caller's space (its + sibling `signUpForClass` does) — mis-attributed reservation **within** the space; no cross-tenant leak. `equipment.ts:249` +- **L2** Unverified members (pending approval in `require_approval` spaces) can create/open proposals and + cast counted votes before an admin vets them. Arguable-by-design; RLS also permits it. `proposals.ts:150` +- **L3** `markOnboardingStepDone` read-modify-writes the `onboarding_progress` JSON in JS → concurrent + step completions can be lost (last-write-wins). `onboarding.ts:142` +- **L4** Email dates render in server-local TZ (no `timeZone` option) → off-by-one-day in renewal/booking + emails depending on the droplet's `TZ`. `notifications-logic.ts:114` +- **L5** `parseHeatSyncLog`: (a) a stale `H:M:E` time-of-day bleeds into later events (fabricated audit + timestamp); (b) one high-half token can be reused for two adjacent grants (wrong card numbers). `door-log-logic.ts:78,91` +- **L6** Null deref in the payment link-picker initials (`m.display_name.split(' ')`). `payments-client.tsx:377` +- **L7** Unparseable CSV `transaction_date` throws mid-import → nothing imports, no toast, button stuck + on "Importing…" forever (no `try/finally`). `import-client.tsx:215` + +### Public / webhook / cron surface — CLEAN + +Stripe (per-space signing secret, replay-protected via unique insert), door inbound (per-connection vault +bearer + `timingSafeEqual` behind a length pre-check, uniform 401/404), both crons (`CRON_SECRET` via +`timingSafeEqual`, fail-closed 503), public forms (`getPublicForm` excludes `members` visibility, writes +scoped to the looked-up form's space), `/track` (192-bit reporter token, redacted projection). Only nits: +two **auth-gated** DoS-hardening gaps on door inbound (trusts `Content-Length`; DB lookup before rate-limit). + +### RLS cross-check — CLEAN + +All 50 tables have RLS. A programmatic check of all 147 anon-client writes against 287 policies found +**zero** writes to a table lacking that operation's policy. The two policy-less tables +(`notification_preferences`, `stripe_webhook_events`) are admin-only by design and correctly scoped. The +sole `WITH CHECK(true)` is the intentional space-creation onboarding path. + +--- + +## 2. Type safety — the linchpin (path to remove `ignoreBuildErrors`) + +**Root cause of ~450 of the 586 errors:** version drift. `package.json` pins `supabase-js ^2.49.4` but the +install resolved to **2.99.0**, whose `SupabaseClient` grew from 3 to 5 generics. `@supabase/ssr@0.6.1`'s +`createServerClient` still passes the old 3-generic shape, feeding the *Schema object* into the *SchemaName +string* slot. The constraint violation collapses the client's `Schema` to **`never`**, so every +`.from().select()` row is `never` (→ 396 × TS2339 "property does not exist on never"), every insert value +is `never`, every rpc arg is `undefined`. This is exactly why `lib/actions/*` (admin client, supabase-js +direct) mostly type-check while every `app/**/page.tsx` (ssr client) is a wall of `never`. + +**Proven interim fix (verified compiling):** annotate the two factory returns as `SupabaseClient` +in `lib/supabase/server.ts` + `lib/supabase/proxy.ts` — re-asserting `Database` lets the 5-generic client +recompute `Schema` and restores full row typing, clearing ~450 errors. Proper fix: align versions +(`@supabase/ssr ≥0.7` for supabase-js 2.9x, or pin supabase-js to the 2.5x line) so the lockfile stops drifting. + +**Second cause:** `types/database.ts` is partially hand-maintained and **out of sync** with the 54 SQL +migrations — missing RPCs (`class_signup_tx`, `class_cancel_tx` are *called* but absent), missing the +`__InternalSupabase` marker. *Fix:* regenerate wholesale via `supabase gen types typescript`, then delete +the `as string` casts it forced throughout `lib/actions/`. + +**Ordered path to drop `ignoreBuildErrors`:** (1) align supabase versions / annotate the two factories → +clears ~450; (2) regenerate `types/database.ts` → clears the RPC/enum union noise; (3) fix ~30 enum-widening +`setState` casts + the 2 null derefs (M6, L6); (4) fix test-file types + `vitest.setup.ts` +`beforeAll/afterAll` imports; (5) remove the flag and add `tsc --noEmit` to CI. + +> Note on the brief's "split `types/database.ts` (1958 lines)": **don't physically split it.** It's a +> generated build artifact — the next `supabase gen types` overwrites it wholesale, reverting any hand-split. +> The generated/hand-written seam the brief wants **already runs between `types/database.ts` and `lib/types.ts`.** +> The right move is to keep `database.ts` pristine (add a "GENERATED — do not edit" banner) and split the +> **hand-written** side (`lib/types.ts` → `types/domain/*`). Delivered in the refactor plan below. + +--- + +## 3. Tests — green but under-testing the risk + +- **CRITICAL** No test exercises admin-client actions for cross-tenant (IDOR) scoping — the #1 stated risk + is untested. Nothing would catch a dropped `.eq('space_id', …)`. +- **HIGH** CI runs **no tests at all**; the integration suite self-skips without a DB, so "green" is empty. +- **HIGH** The authz primitives (`requireMember`/`requireMemberWithRole`/`getAuthMember`) have zero tests. +- **HIGH** `__tests__/actions.test.ts` asserts on **string literals that check themselves** (e.g. + `` const query = `.eq('space_id', …)`; expect(query).toContain('space_id') ``) — never imports an action. + The describe blocks ("Row Level Security", "Role-Based Access") give false confidence. +- **MEDIUM** e2e specs are unauthenticated/vacuous; `components.test.tsx` tests stub components defined + inside the test file; concurrency/money only covered at the DB layer, not the action layer. +- `vitest.setup.ts` calls `beforeAll/afterAll` without importing them (works only via `globals:true`, untyped). + +*Plan:* add a real action-layer IDOR test (seed spaces A/B, call each id-taking action as an A-admin with a +B id, assert rejection + B untouched); unit-test the authz primitives; replace the tautological +`actions.test.ts` with tests that drive real actions through a mock client and inspect the `.eq` calls; +make CI fail on 0 executed integration tests. + +--- + +## 4. Dead code & duplication + +- `lib/security.ts`: 8 of 12 exports unused in production (7 test-only, 1 fully dead); + `hasSqlInjectionPatterns` is security theater over parameterized queries. Prune to what's used. +- `lib/validations.ts`: 6 exported schemas never referenced (`signInSchema`, `signUpSchema`, + `createSpaceSchema`, `joinSpaceSchema`, `proposalStatuses`, `updateAreaLeadRoleSchema`) + 1 test-only. +- Orphaned files: `components/task-claim-button.tsx`, `components/theme-provider.tsx`; dead + `roleDisplayName` in `lib/role-labels.ts`; duplicate `escapeHtml` (dead one in `security.ts`). +- No shared date/number formatter — **68 ad-hoc `toLocale*` call sites** with copy-pasted option objects + across ~15 files. `classes.ts` repeats a notification fan-out 3×; `door.ts` rebuilds an adapter query 5×. + +## 5. Maintainability offenders + +- `ops-client.tsx` (956) — God component: 3 modals + 2 row renderers + 4 tab bodies + filtering, **and it + writes secrets/area-leads directly from the browser** (bypassing the existing `lib/actions/secrets.ts` / + `area-leads.ts` — skips Zod + activity log). `settings-client.tsx` / `members-client.tsx` (663 each) fuse + 4 tabs / table+toolbar+2 modals. +- **HIGH footgun:** `MemberFormFields` is declared **inside** `members-client.tsx`'s render → remounts and + **loses input focus on every keystroke**. `members-client.tsx:197` +- `forms.ts submitForm` is a ~200-line monolith with 4 overlapping email-linking functions; + `import-client.tsx` embeds a CSV parser + auto-mapper that belong in `lib/`. + +--- + +## 6. Architecture & refactor plan (evolve, not rewrite) + +The five layers already exist: **validation** (`lib/validations.ts`) · **guard/auth** (`lib/auth-helpers.ts` ++ `lib/*-guard.ts`) · **pure logic** (`lib/*-logic.ts`) · **server actions** (`lib/actions/*.ts`) · **data +access** (`lib/supabase/*`). Feature-folders already exist (`lib/door/`, `lib/stripe/`, `lib/secrets/`) and +UI decomposition already has an exemplar in `app/(app)/customize/` (thin orchestrator + `panels/` + shared +`types.ts`). The plan **promotes these existing patterns** — every giant file splits along seams that are +already marked by section comments or sub-components. + +**Target conventions:** `types/database.ts` = only generated file (pristine) · `types/domain/*` = +hand-written domain types (from `lib/types.ts`) · `lib/validations/.ts` + `primitives.ts` + +`index.ts` barrel (keeps `@/lib/validations` working for all 44 importers) · `lib//{logic,guard}.ts` +· `lib/actions//*.ts` behind `lib/actions/index.ts` barrel · `app/(app)//{page, +-client,panels/,components/,types}.tsx`. A new feature = **one file per layer, wired only through +barrels** (full add-a-feature guide goes in `ARCHITECTURE.md`). + +**Reviewable slice sequence (each gated on `tsc` + `test` + `build`):** + +0. **Enabler** — GENERATED banner on `types/database.ts`; document regen command. (no behavior change) +1. **`lib/validations.ts` → `lib/validations/.ts` + `primitives.ts` + barrel.** Proves the + barrel-preserving technique on the most-imported module (44 importers, zero call-site churn). +2. **`lib/types.ts` → `types/domain/{index,governance,reexports}.ts`**; convert governance rows to + `Tables<'proposals'>` aliases; leave `lib/types.ts` as a re-export shim (11 importers). *This is the + brief's "split the types file," done correctly.* +3. **`lib/actions/classes.ts` → `classes/{definitions,sessions,signups,instructor}.ts` + `_guard`/`_forms`.** +4. **`lib/actions/door.ts` → `door/{connections,operate,self-entry}.ts` + `_guards`/`_audit`.** +5. **`lib/actions/forms.ts` → `forms/{manage,submit,member}.ts` + `_guard`.** +6. **`ops-client.tsx` → `panels/` + `components/`** *and* replace the direct browser writes with the + existing server actions (behavior change — ships alone). +7. **`settings-client.tsx` → `panels/`.** +8. **`members-client.tsx` → `components/`** + fix the `MemberFormFields`-remount focus bug. +9. **Cleanup** — delete the `lib/types.ts` shim + dead validations/security exports once importers move. + +--- + +## Proposed execution order (fixes first, then refactor) + +- **Phase A — tooling & type safety** (no behavior change): ESLint flat config, `typecheck` script, align + supabase versions / annotate the two clients, regenerate `types/database.ts`, work toward removing + `ignoreBuildErrors`. Gate each on `tsc`/`test`/`build`. +- **Phase B — confirmed fixes** (call out the behavior-changing ones): H1, M1–M6, then the LOW batch, each + with a regression test. This is where the IDOR test + authz-primitive tests land. +- **Phase C — refactor slices 0–9** above, one reviewable commit each. +- **Phase D — `ARCHITECTURE.md`** documenting the resulting structure + add-a-feature guide. + +Nothing is committed yet beyond this document. **No pushes to `main` without your explicit approval.** diff --git a/lib/auth-actions.ts b/lib/auth-actions.ts index a910bf6..167d1cc 100644 --- a/lib/auth-actions.ts +++ b/lib/auth-actions.ts @@ -124,13 +124,12 @@ export async function joinSpace(formData: { // Try the new multi-code invites table first. Falls back to the legacy // spaces.invite_code (a permanent default) if the code isn't found there. - let invite: { id: string; space_id: string; max_uses: number | null; uses_count: number; expires_at: string | null; is_enabled: boolean; role: string } | null = null const { data: inviteRow } = await admin .from('space_invites') .select('id, space_id, max_uses, uses_count, expires_at, is_enabled, role') .eq('code', code) .maybeSingle() - if (inviteRow) invite = inviteRow as typeof invite + const invite = inviteRow let spaceId: string | null = null if (invite) { @@ -171,7 +170,7 @@ export async function joinSpace(formData: { .from('space_members') .select('id') .eq('user_id', user.id) - .in('status', ACTIVE_STATUSES as unknown as string[]) + .in('status', ACTIVE_STATUSES) .maybeSingle() if (existingMembership) { return { error: 'You are already a member of a space.' } diff --git a/lib/auth-helpers.ts b/lib/auth-helpers.ts index 4710218..b9888a3 100644 --- a/lib/auth-helpers.ts +++ b/lib/auth-helpers.ts @@ -1,6 +1,6 @@ import type { SupabaseClient } from '@supabase/supabase-js' import type { Tables } from '@/types/database' -import type { ZodSchema } from 'zod' +import type { ZodTypeAny, TypeOf } from 'zod' import { ACTIVE_STATUSES, hasRole, isPrivilegeEligible, type Role } from '@/lib/permissions' /** @@ -97,7 +97,10 @@ export async function requireMemberWithRole( * so callers can `if (!v.ok) return { error: v.error }` and have `v.data` * narrowed to T. */ -export function parseInput(schema: ZodSchema, input: unknown): Result { +export function parseInput( + schema: S, + input: unknown, +): Result> { const parsed = schema.safeParse(input) if (!parsed.success) { return { ok: false, error: parsed.error.errors[0]?.message ?? 'Invalid input' } diff --git a/lib/supabase/proxy.ts b/lib/supabase/proxy.ts index ff43f23..65ed910 100644 --- a/lib/supabase/proxy.ts +++ b/lib/supabase/proxy.ts @@ -1,6 +1,9 @@ -import { createServerClient } from '@supabase/ssr' +import { createServerClient, type CookieOptions } from '@supabase/ssr' import { NextResponse, type NextRequest } from 'next/server' +/** Shape `@supabase/ssr` passes to `setAll` (typing lost to the ssr/js version drift). */ +type CookiesToSet = { name: string; value: string; options: CookieOptions }[] + export async function updateSession(request: NextRequest) { let supabaseResponse = NextResponse.next({ request, @@ -16,7 +19,7 @@ export async function updateSession(request: NextRequest) { getAll() { return request.cookies.getAll() }, - setAll(cookiesToSet) { + setAll(cookiesToSet: CookiesToSet) { cookiesToSet.forEach(({ name, value }) => request.cookies.set(name, value), ) diff --git a/lib/supabase/server.ts b/lib/supabase/server.ts index 1e9629c..88c00da 100644 --- a/lib/supabase/server.ts +++ b/lib/supabase/server.ts @@ -1,16 +1,27 @@ -import { createServerClient } from '@supabase/ssr' +import { createServerClient, type CookieOptions } from '@supabase/ssr' import { cookies } from 'next/headers' +import type { SupabaseClient } from '@supabase/supabase-js' import type { Database } from '@/types/database' +/** Shape `@supabase/ssr` passes to `setAll` (typing lost to the ssr/js version drift). */ +type CookiesToSet = { name: string; value: string; options: CookieOptions }[] + /** * Especially important if using Fluid compute: Don't put this client in a * global variable. Always create a new client within each function when using * it. + * + * The explicit `SupabaseClient` return annotation is load-bearing: + * `@supabase/ssr`'s `createServerClient` still returns supabase-js's older + * 3-generic client shape, which — against the installed 5-generic supabase-js — + * collapses the schema to `never` (every `.from().select()` row becomes `never`). + * Re-asserting `SupabaseClient` here lets the client recompute its + * schema from `Database`, restoring full row typing across every caller. */ -export async function createClient() { +export async function createClient(): Promise> { const cookieStore = await cookies() - return createServerClient( + const client = createServerClient( process.env.NEXT_PUBLIC_SUPABASE_URL!, process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!, { @@ -18,7 +29,7 @@ export async function createClient() { getAll() { return cookieStore.getAll() }, - setAll(cookiesToSet) { + setAll(cookiesToSet: CookiesToSet) { try { cookiesToSet.forEach(({ name, value, options }) => cookieStore.set(name, value, options), @@ -32,4 +43,10 @@ export async function createClient() { }, }, ) + + // Single documented boundary cast: `createServerClient` returns the older + // 3-generic client shape; re-assert the 5-generic `SupabaseClient` + // so callers get full row typing. This is the ONE place the impedance + // mismatch is bridged. + return client as unknown as SupabaseClient } diff --git a/lib/validations.ts b/lib/validations.ts index 3b3ed75..8ac47ff 100644 --- a/lib/validations.ts +++ b/lib/validations.ts @@ -19,7 +19,7 @@ import { API_METHODS } from './api-call-logic' * Returns a Zod schema that produces a `string | null` (after preprocess + * `.datetime().nullable()`). */ -export const flexibleDateTime = () => +export const flexibleDateTime = (): z.ZodType => z.preprocess( (val) => { if (val === null || val === undefined || val === '') return null diff --git a/proxy.ts b/proxy.ts index 534c9eb..587b325 100644 --- a/proxy.ts +++ b/proxy.ts @@ -1,7 +1,10 @@ import { updateSession } from '@/lib/supabase/proxy' -import { createServerClient } from '@supabase/ssr' +import { createServerClient, type CookieOptions } from '@supabase/ssr' import { NextResponse, type NextRequest } from 'next/server' +/** Shape `@supabase/ssr` passes to `setAll` (typing lost to the ssr/js version drift). */ +type CookiesToSet = { name: string; value: string; options: CookieOptions }[] + // Public routes are reachable without an authenticated session. The // /resources subsite is the legacy hackerspace.sh content baked in; the // five individual resource routes (/zine, /governance, /space-after-dark, @@ -77,7 +80,7 @@ export async function proxy(request: NextRequest) { getAll() { return request.cookies.getAll() }, - setAll(cookiesToSet) { + setAll(cookiesToSet: CookiesToSet) { cookiesToSet.forEach(({ name, value, options }) => response.cookies.set(name, value, options) ) diff --git a/tsconfig.json b/tsconfig.json index 773fedb..2753135 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -6,7 +6,7 @@ "esnext" ], "allowJs": true, - "target": "ES6", + "target": "ES2020", "skipLibCheck": true, "strict": true, "noEmit": true, diff --git a/types/database.ts b/types/database.ts index 644234d..512ecf3 100644 --- a/types/database.ts +++ b/types/database.ts @@ -1702,6 +1702,16 @@ export type Database = { Args: { sid: string; perm: string } Returns: { member_id: string }[] } + // Hand-added to match scripts/045_class_signup_concurrency.sql until this + // file is regenerated from the live schema (`supabase gen types typescript`). + class_signup_tx: { + Args: { p_session_id: string; p_space_id: string; p_member_id: string } + Returns: { signup_id: string | null; signup_status: string | null; err: string | null }[] + } + class_cancel_tx: { + Args: { p_session_id: string; p_space_id: string; p_member_id: string } + Returns: { cancelled_id: string | null; promoted_id: string | null; err: string | null }[] + } } Enums: { area_lead_status: "active" | "vacant" | "handoff" diff --git a/vitest.setup.ts b/vitest.setup.ts index dba22a7..0517621 100644 --- a/vitest.setup.ts +++ b/vitest.setup.ts @@ -1,5 +1,5 @@ import '@testing-library/jest-dom' -import { expect, afterEach, vi } from 'vitest' +import { expect, afterEach, beforeAll, afterAll, vi } from 'vitest' import { cleanup } from '@testing-library/react' // Cleanup after each test