Commit Graph
323 Commits
Author SHA1 Message Date
Moheeb ZaraandClaude Opus 4.8 510187a58d fix(ops): route area-lead roster writes through server actions (SoC)
The ops UI created/updated/deleted area_leads directly from the browser (the
delete had no admin gate or space scope). Add createAreaLead/updateAreaLead/
deleteAreaLead (admin-gated, space-scoped, validated, audited) and wire the
modal + delete handler to them, dropping the direct browser writes. The
distinct area_code-keyed upsertAreaLead interface is left untouched.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-08-04 19:10:42 -07:00
Moheeb ZaraandClaude Opus 4.8 46ea2f1cce docs: add ARCHITECTURE.md; mark Phase C+D complete in the audit log (Phase D)
ARCHITECTURE.md documents the layered structure, the server-action contract +
space-scope invariant, feature-folder conventions, and an add-a-feature guide.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-08-04 06:20:38 -07:00
Moheeb ZaraandClaude Opus 4.8 f775f954ad refactor: dead-code cleanup + drop the lib/types shim (slice 9)
- Delete orphaned components (theme-provider, task-claim-button), the unused
  roleDisplayName helper, the fully-dead cleanupRateLimits, the 6 never-imported
  validation schemas (signIn/signUp/createSpace/joinSpace — a replaced auth flow;
  proposalStatuses; updateAreaLeadRoleSchema).
- Migrate all @/lib/types importers to the canonical @/types/domain and delete
  the compatibility shim.

tsc 0, 635 tests green, lint clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-08-04 06:19:11 -07:00
Moheeb ZaraandClaude Opus 4.8 71ba558522 fix(secrets): store ops-created secrets encrypted via createSecret (security)
The ops 'Add Secret' modal wrote the secret value straight to the DB from the
browser in PLAINTEXT — despite the 'Stored encrypted at rest' label — because
the AES key is server-only. Route it through the createSecret server action,
which encrypts the value (AES-256-GCM), enforces the admin/board gate, validates
input, and logs the activity. Existing plaintext rows (encryption_version 0)
still reveal correctly; new secrets are encrypted when a key is configured.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-08-04 06:15:45 -07:00
Moheeb ZaraandClaude Opus 4.8 9100532bef refactor(members): decompose members-client + fix form-field focus bug (slice 8)
663-line members-client.tsx → a 255-line orchestrator + members/components/
{member-form-dialog,members-table,members-toolbar}.tsx + types.ts. The add/edit
form fields (previously defined INSIDE render, which remounted and lost input
focus every keystroke) are now a stable top-level component in member-form-dialog
— fixing that footgun. tsc 0, build ok.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-08-04 06:14:25 -07:00
Moheeb ZaraandClaude Opus 4.8 543b1d65eb refactor(settings): decompose settings-client into panels (slice 7)
663-line settings-client.tsx → a 79-line orchestrator (activeTab + shared save
status) + settings/panels/{space,integrations,dues,webhooks}-panel.tsx each
owning its own form state + types.ts. Server-action calls and fields preserved
verbatim. tsc 0, build ok.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-08-04 06:14:24 -07:00
Moheeb ZaraandClaude Opus 4.8 7bc6609ac2 refactor(ops): decompose ops-client into panels + components (slice 6)
956-line ops-client.tsx → a 230-line orchestrator + ops/panels/{kb,secrets,
area-leads}-panel.tsx (kb+processes share kb-panel via a variant prop) +
ops/components/{kb-modal,secret-modal,area-lead-modal,secret-row,kb-entry-row}.tsx
+ types.ts, following the customize/ exemplar. Behavior preserved (direct writes
kept as-is for now — the secrets one is fixed in the next commit). tsc 0, build ok.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-08-04 06:14:24 -07:00
Moheeb ZaraandClaude Opus 4.8 f86fff62d0 refactor(actions): split forms.ts into a folder (slice 5)
805-line lib/actions/forms.ts → lib/actions/forms/{manage,submit,member}.ts
('use server') + _guard.ts (requireFormsManager/isUniqueViolation) + index.ts
barrel. All 15 actions preserved verbatim (incl. linkSubmissionsByEmail, still
imported by members.ts). tsc 0, tests green.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-08-04 06:01:02 -07:00
Moheeb ZaraandClaude Opus 4.8 39bcdd9677 refactor(actions): split door.ts into a folder (slice 4)
813-line lib/actions/door.ts → lib/actions/door/{connections,operate,self-entry}.ts
('use server') + _guards.ts + _audit.ts + index.ts barrel. All 14 actions
preserved verbatim; lib/door/ (executor/ingest/secret) untouched. tsc 0, tests green.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-08-04 06:01:02 -07:00
Moheeb ZaraandClaude Opus 4.8 dbaae4851b refactor(actions): split classes.ts into a folder (slice 3)
908-line lib/actions/classes.ts → lib/actions/classes/{definitions,sessions,
signups,instructor}.ts ('use server') + _guard.ts (requirePermission/Gate) +
_forms.ts (findSpaceForm/hasFormSubmission) + a plain index.ts barrel. All 14
actions preserved verbatim; lib/actions/index.ts unchanged. Helpers stay
internal (not re-exported). tsc 0, tests green.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-08-04 06:01:02 -07:00
Moheeb ZaraandClaude Opus 4.8 e205ce54c0 docs: record CI + Phase C slices 0-2 in the audit log
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-08-02 04:22:32 -07:00
Moheeb ZaraandClaude Opus 4.8 3f7e9d96fa refactor(types): move hand-written domain types to types/domain/* (slice 2)
lib/types.ts (aliases + governance shapes + auth re-exports) split into
types/domain/{index,governance,reexports}.ts, making the generated/hand-written
seam explicit (generated = types/database.ts, hand-written = types/domain/*).
lib/types.ts is now a re-export shim so all 11 importers are untouched. Fixed
the stale 'not yet in types/database.ts' comment — those tables/enums are now
generated; de-duplicating the governance rows into Tables<>/Enums<> is a noted
follow-up. tsc 0, 635 tests green.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-08-02 04:21:19 -07:00
Moheeb ZaraandClaude Opus 4.8 18d5e22ad4 refactor(validations): split lib/validations.ts into a barrel'd folder (slice 1)
1141-line lib/validations.ts → lib/validations/<feature>.ts + primitives.ts +
an index.ts barrel. '@/lib/validations' now resolves to the barrel, so all 40
importers are untouched. Every one of the 143 exports is preserved verbatim
(verified by symbol-diff); schemas moved unchanged. tsc 0, 635 tests green.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-08-02 04:19:11 -07:00
Moheeb ZaraandClaude Opus 4.8 c8c60358c7 refactor(types): mark types/database.ts as generated (slice 0)
Banner documenting the regenerate command and that hand-written domain types
belong in types/domain/*, not here. No code change.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-08-02 04:11:25 -07:00
Moheeb ZaraandClaude Opus 4.8 f571001ac2 ci: add a checks-only workflow (typecheck + lint + unit tests)
Runs on every PR and on main after merge, fully separate from deploy.yml so it
can never affect deploys. Catches type/lint/test regressions that previously
had no CI at all. Integration + e2e (which need a DB service) are a follow-up.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-08-02 04:10:57 -07:00
Moheeb ZaraandClaude Opus 4.8 edffb2780c docs: record Phase A + Phase B progress in the audit log
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-08-01 21:33:40 -07:00
Moheeb ZaraandClaude Opus 4.8 ad010edb41 test: cover the authz primitives (getAuthMember/requireMember/requireMemberWithRole)
These gate every server action but had no direct tests. Add cases for: no user,
no active membership, active member; role allow/deny; the require_approval
unverified gate; and late members keeping their role.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-08-01 21:33:15 -07:00
Moheeb ZaraandClaude Opus 4.8 9bd21b185d fix(import): guard unparseable CSV dates and never wedge the button (L7)
A non-empty invalid transaction_date ('N/A') passed presence-only validation,
then new Date(str).toISOString() threw mid-import: nothing imported, no toast,
and setImporting(false) never ran, sticking the button on 'Importing...'.
Extracted parsePaymentRow to lib/import-logic.ts with a date guard, and wrapped
runImport so the importing flag always clears.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-08-01 21:31:51 -07:00
Moheeb ZaraandClaude Opus 4.8 8274af7d5c fix(equipment): verify booked-for member belongs to caller's space (L1)
reserveEquipment used a manager-supplied memberId directly, so a booking could
be stamped for a member id from another space. Mirror signUpForClass: look up
the target by (id, space_id=caller) and reject if absent.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-08-01 21:31:51 -07:00
Moheeb ZaraandClaude Opus 4.8 c7a6cd3853 fix(proposals): block unverified members from governance writes (L2)
createProposal/openProposal/withdrawProposal/castVote gated only with
requireMember, which admits status='unverified' — so a member pending approval
in a require_approval space could propose and cast counted votes before vetting.
Add the isPrivilegeEligible gate (same as requireMemberWithRole) to all four.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-08-01 21:31:51 -07:00
Moheeb ZaraandClaude Opus 4.8 7fc08c5165 fix(incidents): restrict appealIncident to admin/board, no orphan proposal (M2)
appealIncident was gated by requireMember but the incident UPDATE is RLS-
restricted to admin/board, so a reporter's appeal inserted a draft proposal,
updated 0 incident rows, and returned success — an orphan proposal. Gate with
ADMIN_ROLES to match the write, and assert the link UPDATE affected a row
(.select), rolling back the draft proposal if it did not.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-08-01 21:31:51 -07:00
Moheeb ZaraandClaude Opus 4.8 7fe3ffc632 fix(invites): block re-arming an admin invite without role change (M1)
updateInvite ran the canAssignInviteRole guard only when the payload included
'role', so a board member could re-enable/extend an exhausted admin-granting
invite (is_enabled/max_uses/expires_at) and distribute admin access they cannot
mint. Load the existing invite and require canAssignInviteRole against its
effective role whenever any privilege-affecting field is (re-)armed.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-08-01 21:31:51 -07:00
Moheeb ZaraandClaude Opus 4.8 4b98c21e2b fix(payments): advance-only last_paid_at, no dues regression (M4/M5)
logCashPayment and linkPaymentToMember unconditionally set last_paid_at to the
(possibly backdated) transaction_date and forced payment_status='current', so
logging/linking a historical payment moved a member's dues state backward.
New pure resolveDuesAdvance() keeps the later of existing/incoming date and only
marks 'current' when the payment actually advances; both sites read the current
date first. Identical in the common (newest-payment) case.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-08-01 21:31:34 -07:00
Moheeb ZaraandClaude Opus 4.8 90c68faa25 fix(door): correct parseHeatSyncLog stale time + token reuse (L5)
(a) Reset h/mi/e after each event so a later event with no H:M:E of its own no
longer inherits the previous event's time-of-day (fabricated audit timestamp +
dedupeKey). (b) Track consumed high-half token indices so one g/d token can no
longer be paired with two adjacent grants (wrong reconstructed card numbers).

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-08-01 21:31:34 -07:00
Moheeb ZaraandClaude Opus 4.8 a3e9b97292 fix(door): block hex IPv4-mapped IPv6 in the SSRF filter (M3)
isBlockedDoorIp blocked the dotted IPv4-mapped form (::ffff:169.254.169.254)
but the hex form ::ffff:a9fe:a9fe (=169.254.169.254 IMDS) / ::ffff:7f00:1
(=127.0.0.1) fell through the IPv6 branch and returned false. Decode the hex
IPv4-mapped form to dotted IPv4 and run the existing blocked-range checks.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-08-01 21:31:34 -07:00
Moheeb ZaraandClaude Opus 4.8 f337a85682 build: remove typescript.ignoreBuildErrors now that tsc is clean
tsc --noEmit reports 0 errors (was 586, all masked). next build now
type-checks on every build and prod deploy, so a type regression fails CI
instead of shipping. 649 tests green.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-08-01 21:23:22 -07:00
Moheeb ZaraandClaude Opus 4.8 d18d6acaff test: replace tautological actions.test with real authz/IDOR tests
The old file asserted on string literals it defined itself (e.g.
expect(".eq('space_id', ...)").toContain('space_id')) and never invoked an
action, so it stayed green regardless of the code — false confidence on exactly
the authz/scoping it claimed to cover.

Rewrite drives the real updateMember/removeMember/logCashPayment through a
recording mock Supabase client and asserts: unauthenticated + non-privileged
callers are rejected with no write; admin/treasurer writes are pinned to the
caller's own space_id (the IDOR guard). Removing a role gate or a space_id
scope now fails a test.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-08-01 21:23:04 -07:00
Moheeb ZaraandClaude Opus 4.8 0913346741 fix(payments): guard nullable display_name in the link-payment picker (L6)
The candidate-member initials rendered m.display_name.split(' '); display_name
is nullable, so a member with no name crashed the link dialog. Fall back to
email then '?'.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-08-01 21:19:29 -07:00
Moheeb ZaraandClaude Opus 4.8 70b85a20fa fix(members): repair broken member-directory visibility gate
member_directory_visibility is the directory_visibility enum
(board_only|member_count_visible|members_visible|public_members_visible), but
the gate compared against 'board_visible'/'admin_only' — values from other
enums that never match — so every setting except members_visible silently
restricted the directory, including public_members_visible (meant to be the
most open). Map the gate to the real enum values.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-08-01 21:19:29 -07:00
Moheeb ZaraandClaude Opus 4.8 da11127ca4 fix(members): guard nullable display_name/email in the members list (M6)
Typing into the members search box called .toLowerCase() on display_name/email,
both of which are nullable (e.g. imported members) — a single null row threw a
TypeError and blanked the entire members page. Optional-chain the search match,
and handle nullable values when seeding the edit form and optimistic update.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-08-01 21:19:29 -07:00
Moheeb ZaraandClaude Opus 4.8 2ee929658e fix(invites): close single-use invite max_uses TOCTOU (H1)
Redeeming an invite read uses_count, checked the cap in JS, then did a
non-atomic uses_count = uses_count + 1 write AFTER creating the membership.
Two concurrent redemptions of a max_uses=1 invite both passed the check and
both created a member, and the counter under-counted.

Fix: claim a use ATOMICALLY before creating the membership via a
compare-and-swap on uses_count (lib/invite-claim.ts) — the UPDATE only writes
when uses_count still equals the value the cap decision was based on, so of two
racing claims exactly one wins. The loser re-reads and retries while capacity
remains (multi-use invites unaffected). On a failed membership insert the claim
is rolled back so a slot is not burned.

Extracted to lib/invite-claim.ts (out of the 'use server' surface, unit-tested).

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-08-01 21:16:12 -07:00
Moheeb ZaraandClaude Opus 4.8 624a708d8c tooling: add ESLint 9 flat config and a typecheck script
- eslint.config.mjs: flat config built on eslint-config-next's flat array
  (core-web-vitals + typescript + Next plugin), with build/generated artifacts
  ignored and the aggressive react-hooks v6 React-Compiler rules downgraded to
  warnings so `pnpm run lint` passes clean (0 errors, 88 warnings of burn-down
  debt) instead of failing. Previously `eslint .` failed outright — no config
  and no eslint binary were installed.
- Install eslint, @eslint/js, eslint-config-next, typescript-eslint, globals as
  devDeps (dev-only; prod Docker build is unaffected).
- package.json: add `typecheck` (tsc --noEmit) and `test:run` scripts.
- app/not-found.tsx: escape apostrophes (react/no-unescaped-entities).

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-08-01 20:37:56 -07:00
Moheeb ZaraandClaude Opus 4.8 b1edb5c460 types: clear remaining tsc errors across 41 files (76→14, behavior-preserving)
Per-file type-only fixes surfaced after the Supabase client was correctly
typed. All fixes preserve runtime behavior; no any/ts-ignore/ts-expect-error.

- lib/supabase/client.ts: annotate the browser client as SupabaseClient<Database>
  (same ssr/js drift as server.ts; fixes it centrally for all callers).
- Supabase embedded-select joins whose FK metadata is missing from the
  generated types are bridged with `as unknown as <target>` + a TODO to remove
  after regenerating types/database.ts.
- setState optimistic-update variance and raw <select> enum values cast to the
  precise element/union type; .insert payload fields (Json, enums) given the
  precise column type via satisfies/field casts.
- Added narrowing guards where a value was legitimately nullable (comms channel,
  public form page) without changing the intended path.

The 14 remaining errors are confined to the four files fixed in Phase B (real
bugs + the tautological actions.test rewrite). 639 tests still green.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-08-01 18:56:11 -07:00
Moheeb ZaraandClaude Opus 4.8 1766780bca types: restore Supabase row typing, fix parseInput inference (586→76 tsc errors)
The installed supabase-js (2.99, 5 generics) is incompatible with
@supabase/[email protected] (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<Database>
  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<T> 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) <[email protected]>
2026-08-01 18:40:33 -07:00
Moheeb Zara 9ac0c2486c Use the real hack.build logo mark (wrench-in-ring), not a reconstruction
The mark is the actual brand PNG (already transparent) — just trimmed and
downscaled. The prior version wrongly ran a saturation mask that stripped the
white ring and mangled the wrench into a blob.
2026-07-29 02:34:26 -07:00
Moheeb Zara ec923a845e Use the real hack.build logo and fix the showcase grid
- Replace the hand-drawn wrench with the actual hack.build mark: isolate the
  pink wrench from the brand PNG onto transparent (public/hackbuild-mark.png)
  and use it for the talk tile and the Resources card.
- Showcase grid: 7 columns so the seven projects fill one row, and draw the
  hairlines from per-tile borders instead of a grey container showing through
  gaps — no more grey empty cells on an incomplete row.
- Drop the now-unused HackBuildIcon.
2026-07-29 02:11:35 -07:00
Moheeb Zara 3f1ce77900 Add the talk to the landing showcase; point Atlas at its subdomain
- Landing 'From hackerspace.sh' showcase: add a 'Hacking Your Hackerspace'
  tile (07 projects) linking the deck on hack.build, with a new HackBuildIcon
  (the brand's pink open-end wrench).
- Point the Atlas tile at https://atlas.hackerspace.sh (now live) instead of
  the old internal /atlas route.
- Use the same hack.build wrench for the talk card on the Resources page.
2026-07-28 23:37:46 -07:00
Moheeb Zara b6e50c0950 Add talk to resources, public-site nav, and homepage link
- Resources: 7th project card linking the 'Hacking Your Hackerspace' talk
  (hack.build), with a PlayIcon; bump the section count to 07.
- Sidebar: NavLink now supports external targets; add a 'Public Site' section
  linking the homepage and the public resources page.
- Login: a back-to-homepage link.
2026-07-28 23:18:50 -07:00
Moheeb Zara 62c1c5f4b7 glitchtip: pin image to 6.1.6 for reproducibility; HANDOFF observability complete 2026-05-22 01:17:41 -07:00
Moheeb Zara 65458b71ec docs: HANDOFF observability Phase 2 complete (GlitchTip live on prod, DSN wired) 2026-05-22 01:09:16 -07:00
Moheeb Zara ad4b9f5a12 glitchtip: default to port 8100 to avoid Supabase Kong (8000) collision 2026-05-21 17:39:00 -07:00
Moheeb Zara b72e628c14 observability Phase 2: optional self-hosted GlitchTip as a committed one-command compose 2026-05-21 17:30:49 -07:00
Moheeb Zara c89e4d95e1 observability: backend-agnostic error-capture seam (Phase 1, inert until SENTRY_DSN) 2026-05-21 17:00:37 -07:00
Moheeb Zara 95f79771ec docs: record spine A1+A2 prod provisioning; fix crontab CRON_SECRET footgun 2026-05-21 00:29:05 -07:00
Moheeb Zara 0b6ef20891 add spine-validation owner runbook; lock privilege-escalation RLS test
docs/SPINE_VALIDATION.md: a precise owner runbook + checklist that provisions
the shipped-but-inert spine (Resend, CRON_SECRET + both crontabs, per-space
Stripe test mode + Billing Portal, the Supabase change-email template) and
proves each path end to end (dues cycle, notifications, prefs/inbox, /me,
door). Grounded in the actual config and route fail-closed codes; linked from
the README docs table.

integration/permission-escalation.test.ts: lock the additive permission
model's core guard, previously untested -- only admin/board may write
space_role_permissions, so a plain member cannot grant themselves a
capability, and the WITH CHECK pins the space.

No app code or migration; suite 626 unit / 54 integration green.
2026-05-20 23:22:48 -07:00
Moheeb Zara 32934124c2 mobile: fix flex-row text overflow; parallelize door-ingest poll; docs refresh
Mobile responsiveness: several flex rows had a `truncate` child without
`min-w-0`, so a long value (member/door name, channel name, published form
URL) would not shrink and pushed the row past the viewport on a phone. Added
`min-w-0` (with `flex-1`/`shrink-0` where needed) on the dashboard presence +
door-self-entry rows, the comms channel list, and the form-builder URL.

Door inbound poll: `/api/cron/door-ingest` now polls connections with
Promise.allSettled instead of a sequential loop, so one tick is bounded to the
slowest single poll rather than the sum of all per-call timeouts; each
connection stays isolated and the run is idempotent.

Docs: refreshed ARCHITECTURE (door epic marked complete P1-P5), the README
access-control feature bullet + project-status pointer, a historical banner on
the older AUDIT.md, a door-epic CHANGELOG entry, and the rolling HANDOFF audit
log (passes 65-68). No migration; no behavior change for existing users.
2026-05-20 21:55:49 -07:00
Moheeb Zara 0ac69abc8e door ingest: anchor card matching to the HeatSync hex-uid model
The inbound-ingest matcher read a stored card_uid as both decimal and hex,
which mis-attributed all-digit uids (a stored "16" is hex 0x16 = 22, but it
wrongly matched a decimal poll of card 16) and was conceptually wrong.
HeatSync stores the uid as hex (the grant encoder requires it) and the ?z
log / webhooks report the decimal card number, so cardMatchesEvent now
matches a reported decimal when hexInt(storedUid) equals it, and matches a
webhook card_uid by exact string. This removes the false positive while
keeping every legit match. Attribution is audit-only (never an access
decision) and inert until a space enables ingest; no migration.

Also records the pass-63/64 door-epic deploy state and the pass-65 audit in
the handoff log.
2026-05-20 21:31:11 -07:00
Moheeb Zara 7ec90b0f1e Door epic phases 4-5: inbound access-log ingest + universal API-call buttons
Phase 4 (migration 053): pull entry/denied events into door_access_log and
match each card to a member, via two transports sharing one ingest core.
Poll: a CRON_SECRET-guarded /api/cron/door-ingest reads each opted-in
native-HeatSync connection's ?z log through the existing hardened executor
and parses it (firmware-characterized, best-effort ring buffer). Webhook:
/api/door/inbound/[connection] accepts pushed event JSON authenticated by a
per-connection bearer secret from the vault. Both dedupe on
(connection_id, dedupe_key) so re-polls and retries are no-ops. Adds an
inbound config panel to /door/manage.

Phase 5 (migration 054): admins define api_buttons that fire a configured
HTTP call (full verbs + headers + body) when an authorized member presses
them, through the same hardened egress (now shared between callDoor and the
new callApi). Each button carries a required_permission (default the new
apicall.invoke code); members see only buttons they may press. Builder at
/door/buttons with a door-template preset; members press from /doors. Every
press is recorded in api_call_log with secrets redacted.

Both subsystems are inert until an admin enables inbound on a connection or
creates a button. 624 unit + 50 integration tests; migrations applied
top-to-bottom in schema.sql; types and docs updated.
2026-05-20 20:45:44 -07:00
Moheeb Zara 6a892cbad5 docs: HANDOFF session closeout for the passes 56-62 arc + recalibrated backlog 2026-05-20 16:22:59 -07:00
Moheeb Zara 5f22ac4cfa docs: HANDOFF deploy state for passes 60-62 (run 26153548195) 2026-05-20 02:24:51 -07:00