Files
dohooo__helmor/cloud/scripts/d1-migrations.ts
Caspian Zhao ecc41ef7c0 fix(team-cloud): container gets ONLY the forge creator's tokens (P1-2a)
Cold start previously minted EVERY member's ForgeIdentity DO and wrote
the whole team's gh/glab tokens into the shared container — one induced
'cat' away from a full-team credential leak. Per the user product
ruling the creator-identity model stays (everyone's pushes reuse the
creator's forge identity); only the storage narrows:

- teams.forge_identity_member_id: the forge 'creator', bound
  FIRST-authorizer-wins on PUT /team/forge-identity (mirrors
  cloud_identity_member_id's single-row upsert, but COALESCE keeps an
  existing binding; a non-creator racing to authorize first is the same
  accepted TOFU edge as cloud identity). Added via the P1-4c idempotent
  D1 ALTER runner (0002) + schema.sql for fresh DBs.
- collectMemberForgeCreds injects tokens ONLY for the creator entry
  (flagged creator:true); every other member is login-only (kept for
  per-member commit authorship). Local-docker launcher mirrors the same
  narrowing.
- member_creds.rs (CONTAINER RUST — needs the next team-image bump):
  token lookups fall back to the creator's tokens when the acting
  member has none, and when no acting member is bound at all (the
  detached auto-push thread). Old injected files without the creator
  flag still parse (no fallback). Authorship lookups (member_for) do
  NOT fall back.

NOTE: P1-8a (scrubbing the token out of .git/config) depends on this
creator fallback for push auth — rolling back this commit requires
rolling back the P1-8a commit with it.
2026-07-10 20:04:27 -07:00

50 lines
2.4 KiB
TypeScript

// Versionless, duplicate-tolerant D1 migrations for PRE-EXISTING team
// databases (round6 P1-4c). schema.sql's `CREATE TABLE IF NOT EXISTS` only
// shapes FRESH databases — it never backfills a column onto an
// already-bootstrapped table — and D1/SQLite has no `ALTER TABLE … ADD COLUMN
// IF NOT EXISTS`. So additive columns land here, and provision-team.ts
// executes every entry against the live D1 on each provision / re-provision:
//
// fresh DB (column already in the CREATE TABLE) → "duplicate column name"
// → classified already-applied, skipped
// pre-existing DB missing the column → the ALTER applies it
// anything else → fatal; the provision fails
//
// Adding a migration: append an entry with a unique id, keep it additive and
// duplicate-classifiable, and add the column to schema.sql's CREATE TABLE too
// (fresh DBs read only that file). If migrations ever stop fitting this shape
// (backfills, type changes, data moves), upgrade to a versioned
// schema_migrations table instead of stretching this list past its design.
export type D1Migration = { id: string; sql: string };
export const D1_MIGRATIONS: readonly D1Migration[] = [
{
// Phase 1 cloud-run identity anchor. Teams whose D1 predates the column
// (long-lived dev DBs; re-provision onto an already-set-up account, where
// `d1 create` and the schema apply both no-op) hit "no such column" on
// team.ts's INSERT/SELECT without it.
id: "0001-teams-cloud-identity-member-id",
sql: "ALTER TABLE teams ADD COLUMN cloud_identity_member_id TEXT",
},
{
// Round6 P1-2a: the team's forge "creator" binding
// (first-forge-authorizer-wins; see schema.sql). Pre-existing DBs hit
// "no such column" on putForgeIdentity / collectMemberForgeCreds
// without it.
id: "0002-teams-forge-identity-member-id",
sql: "ALTER TABLE teams ADD COLUMN forge_identity_member_id TEXT",
},
];
/** Classify a failed `wrangler d1 execute --command` output. SQLite's
* "duplicate column name" means the column is already there (a fresh DB, or a
* re-run) — that's the desired end state, not an error. Anything else is
* fatal: the provision must fail loudly rather than continue against a DB in
* an unknown shape. */
export function classifyMigrationError(
output: string,
): "already-applied" | "fatal" {
return /duplicate column name/i.test(output) ? "already-applied" : "fatal";
}