You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit ca3bcb3
Browse filesBrowse the repository at this point in the historyBrowse files
Mutating endpoints accept an optional `Idempotency-Key` header (any client-generated string). The API caches the response in-memory keyed by `(personId, idempotencyKey)` for 24 hours; repeat requests with the same key return the same response. In-memory by design — single replica, restart-tolerant: a key that hasn't seen a duplicate within 24h won't see one after a restart either. This matters for cases like "post project update" where a double-tap shouldn't create two updates.
172
172
173
+
> **Status:** the idempotency plugin (`apps/api/src/plugins/idempotency.ts`) is implemented and registered, but not yet wired into any route handler — mutating endpoints don't honor the header *yet*. Wiring the at-risk endpoints (starting with `POST /api/projects/:slug/updates`) and the per-route tests are tracked as a follow-up; until then, treat this section as the target contract, not current behavior.
174
+
173
175
## Logging and trace IDs
174
176
175
177
Every request has a `traceId` (UUIDv7). It's included in logs and surfaced in error responses' `error.traceId`. If a user reports a problem, the traceId is the link to the server logs.
|`accountLevel`| enum |`user`\|`staff`\|`administrator`. Staff-only filter — for a non-staff caller it returns an empty list (a 200 with no items), not a `403`, so the filter's existence isn't a signal. |
@@ -102,14 +104,15 @@ Fields visible only to self or staff:
102
104
-`email` (not in the shape above; added only when authorized)
103
105
-`firstName`, `lastName` (visible to all but editable only by self/staff)
104
106
-`accountLevel` value beyond a generic "user" — public callers always see `"user"` regardless of true level
107
+
-`deletedAt` — the real timestamp is shown to self + staff; everyone else always sees `null`
105
108
106
109
### Errors
107
110
108
111
-`404 not_found` — slug doesn't exist, or person is soft-deleted and caller is not staff
109
112
110
113
## PATCH /api/people/:slug
111
114
112
-
Self or staff. Self cannot change their own `accountLevel`; only administrators can change account levels (and only via a staff-only sub-endpoint, not by passing `accountLevel` in a generic PATCH).
115
+
Self or staff. Self cannot change their own `accountLevel`; only administrators can change account levels (and only via the dedicated [`POST /api/people/:slug/account-level`](#post-apipeopleslugaccount-level)endpoint — `accountLevel`passed in a generic PATCH body is rejected by the schema, not silently applied).
Copy file name to clipboardExpand all lines: specs/behaviors/legacy-id-mapping.md
+1-1Lines changed: 1 addition & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -25,7 +25,7 @@ If we ever need to re-import (e.g., catching up on changes made after cutover),
25
25
26
26
## Lookup
27
27
28
-
`legacyId` lookups go through the in-memory `byLegacyId.<entity>` indices documented in [data-model.md](../data-model.md). These indices are built at boot by iterating the sheet and skipping records where `legacyId` is absent.
28
+
`legacyId` lookups that back a live redirect go through in-memory `byLegacyId` indices, built at boot by iterating the sheet and skipping records where `legacyId` is absent. These indices exist only for the entities whose laddr URLs actually referenced numeric IDs — **`people`, `projects`, and `blog-posts`**. The other migrated sheets (`tags`, `project-buzz`, `project-updates`) carry `legacyId` for *import idempotence* but have no runtime `byLegacyId` index, because no live URL resolves them by numeric ID (their legacy URLs were always slug-based). If a future redirect ever needs one of those by numeric ID, add the index then.
29
29
30
30
Uniqueness of `legacyId` per sheet is enforced by the API's write mutex: a record's `legacyId` is checked against the index before commit, just like `slug` and `email`.
Copy file name to clipboardExpand all lines: specs/behaviors/storage.md
+5-5Lines changed: 5 additions & 5 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -2,7 +2,7 @@
2
2
3
3
## Rule
4
4
5
-
**Public** persistent data lives in a **gitsheets-backed git repository** — TOML records in templated paths, committed atomically, pushed to a publicly cloneable GitHub remote. There is no relational database. At runtime the API loads all records into typed in-memory structures and serves reads from there; mutations write to the gitsheets repo and update the in-memory state synchronously.
5
+
**Public-by-design** persistent data lives in a **gitsheets-backed git repository** — TOML records in templated paths, committed atomically, pushed to the `codeforphilly-data`GitHub remote. The repo is access-controlled (private) today, but its contents are curated to be publicly shareable (civic transparency), and the commit log is redacted of PII accordingly — treat everything in it as forever-public (see [PII-aware redaction](#pii-aware-redaction)). There is no relational database. At runtime the API loads all records into typed in-memory structures and serves reads from there; mutations write to the gitsheets repo and update the in-memory state synchronously.
6
6
7
7
**Private** data — emails, password hashes during migration, newsletter subscription state — lives in a separate S3-compatible bucket. See [behaviors/private-storage.md](private-storage.md). This spec covers only the public side.
8
8
@@ -54,7 +54,7 @@ There are two git repositories:
54
54
| Repo | Purpose | Visibility |
55
55
| ---- | ------- | ---------- |
56
56
|`codeforphilly-rewrite`| The application code (this repo) | Public |
57
-
|`codeforphilly-data`| The live gitsheets data |**Private** — contains emails, real names, IPs |
57
+
|`codeforphilly-data`| The live gitsheets data |**Private repo today**, but **public-by-design** content — member names + projects are public-facing; the commit log is redacted (no emails, IPs, or password hashes — those live in the private store). Treat as forever-public.|
58
58
59
59
The code repo references the data repo by env (`CFP_DATA_REPO_PATH`). They are not git submodules — too much friction. Sibling clones, locally and in production.
60
60
@@ -81,7 +81,7 @@ A scrubbed snapshot of the data repo is published as a public tag (e.g., `snapsh
81
81
-`slackHandle` → null
82
82
-`bio`, `overview`, `body` content → unchanged (assumed safe; staff may flag specific records for redaction)
83
83
84
-
A `scripts/scrub-data.ts` in the code repo produces the snapshot. The contributor bootstrap is:
84
+
An `apps/api/scripts/scrub-data.ts` in the code repo produces the snapshot. The contributor bootstrap is:
The public data repo is, well, public — redaction is essential, not defense-in-depth:
267
+
The data repo is public-by-design (and may be made public outright) — redaction is essential, not defense-in-depth:
268
268
269
269
-**No real emails as commit authors.** Author = `<slug>@users.noreply.codeforphilly.org`, never the user's actual email.
270
270
-**No `User-Ip` or `User-Agent` trailers in public commits** (rule above).
271
271
-`Authorization` and `Cookie` request headers are never embedded anywhere.
272
-
-Any request body field matching `/email|password|token|secret/i`is replaced with `[REDACTED]` before embedding in the commit message body.
272
+
-**Commit message bodies carry only the caller-provided `summary` string — never the raw request body** — so request PII can't reach the commit message in the first place. If a `summary` is ever derived from request input, fields matching `/email|password|token|secret/i`must be `[REDACTED]` before embedding.
273
273
-`Set-Cookie` response headers and JWT bodies are not embedded.
274
274
- Trailers for actions on the private store (`PrivateProfile`, `LegacyPasswordCredential`) appear in the public log as semantic action only — `Action: private-profile.update`, with no field-level diff. The actual change details live in the private bucket's object-version history (see [private-storage.md](private-storage.md)).
0 commit comments