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
- Produces RPCs: `create_account_with_owner(p_wallet text, p_account_type text, p_name text, p_sub_type text default null, p_bio text default null, p_avatar_url text default null) returns accounts`, `get_invite_by_token(p_token text) returns invite_tokens`, `list_pending_invites(p_account_id uuid, p_wallet text) returns setof invite_tokens`, `has_pending_invite(p_account_id uuid, p_wallet text) returns boolean`. Tasks 4 and 6 call these via `supabase.rpc(...)`.
135
+
- Produces ONE anon-callable RPC: `get_invite_by_token(p_token text) returns invite_tokens` (Tasks 5/6 call it via `supabase.rpc(...)`). Account creation and every other invite read are edge-function actions (Task 3) — NOT RPCs.
136
136
137
-
Design notes baked in: `create_account_with_owner` only ever creates a NEW account with `p_wallet`as its first owner — it cannot touch an existing org, so the trusted-wallet posture is safe here.`get_invite_by_token`makes knowledge-of-token the credential (closes the enumeration hole that `invite_tokens_select USING (true)` is today). Everything else becomes deny-by-default; the edge function writes with service role.
137
+
Design notes (amended after task review, 2026-07-31): the v1 draft granted `create_account_with_owner`, `list_pending_invites` and `has_pending_invite` to anon with a client-supplied `p_wallet`— the reviewer showed that leaks bearer tokens (owner wallets are public in `account_owners`, so anyone can impersonate an owner parameter) and lets anon attach any registered wallet as owner of a new account. **Rule: no anon-granted function may take a wallet parameter as an authorization input.**`get_invite_by_token`survives because knowledge-of-token IS the credential (closes the enumeration hole that `invite_tokens_select USING (true)` is today). Everything else is deny-by-default; the signature-verified edge function reads/writes with service role.
138
138
139
139
-[ ]**Step 1: Write the migration**
140
140
@@ -146,58 +146,21 @@ Design notes baked in: `create_account_with_owner` only ever creates a NEW accou
146
146
-- edge function (service role).
147
147
-- ⚠️ APPLY ONLY AFTER the org-membership edge function and rewired clients are live.
-`create_account``{ accountType: "personal"|"organisation", name, subType?, bio?, avatarUrl? }` — creates the account and inserts the SIGNER (never a passed wallet) as its first `owner` row, atomically (insert account, then owner; on owner-insert failure delete the account row). Validate: `name` 1–80 chars after trim, `bio` ≤ 500 chars, `accountType` in the two values, `subType` (when set) in `('restaurant','unternehmen','verein','stadt','fraktion','journalist')`. Returns the account row. (Amended 2026-07-31: replaces the withdrawn `create_account_with_owner` RPC — creation requires the creator's signature so no one can attach a stranger's wallet as owner.)
209
+
-`list_invites``{ accountId }` — signer owner/admin of `accountId`; returns pending invite rows (incl. tokens — the signer is entitled to them). (Amended: replaces the withdrawn `list_pending_invites` RPC.)
210
+
-`has_pending_invite``{ accountId }` — returns `{ pending: boolean }` for the SIGNER's wallet only. (Amended: replaces the withdrawn RPC.)
211
+
212
+
Extend the `ACTIONS` array and `OrgAction` union (here and in Task 1's module) with `create_account`, `list_invites`, `has_pending_invite` — the Task 1 test file is the contract; update it in the same commit as the module.
245
213
246
214
-[ ]**Step 1: Implement the function.** Skeleton (verify + dispatch; each handler is a small service-role query following the checks above — write them all, they are listed exhaustively in the Interfaces block):
247
215
@@ -337,7 +305,7 @@ it("requestBody signs the canonical message and echoes fields", async () => {
337
305
-[ ]**Step 2: Run to verify fail** — `pnpm test:web` → FAIL (no `requestBody`).
338
306
339
307
-[ ]**Step 3: Implement `client.ts`** — `requestBody(account, action, payload, timestampSec = Math.floor(Date.now()/1000))` builds `{action, wallet, timestampSec, payload, signature}` via `buildOrgMessage` + `account.signMessage`; `callOrgMembership` wraps it in `fetch`. Then rewire `supabase-accounts.ts`:
340
-
-`createPersonalAccount` / `createOrgAccount`: replace the two-step insert (:163–177 + :187–190) with `supabase.rpc("create_account_with_owner", { p_wallet, p_account_type, p_name, p_sub_type, p_bio, p_avatar_url })` (returns the account row; keep the existing return shapes).
308
+
-`createPersonalAccount(account, …)` / `createOrgAccount(account, …)`: replace the two-step insert (:163–177 + :187–190) with `callOrgMembership(account, "create_account", { accountType, name, subType, bio, avatarUrl })` (amended 2026-07-31 — creation is signature-verified; the signer becomes first owner; keep the existing return shapes by re-fetching or using the returned row). Update the call chains: `apps/web/src/lib/supabase-users.ts:87` (first-login personal account — the thirdweb account object is in scope there) and `AccountContext.createOrgAccount` (`apps/web/src/lib/context/AccountContext.tsx:153,161`).
341
309
-`updateAccount(account, accountId, updates)`: `callOrgMembership(account, "update_account", { accountId, updates })`, then re-fetch the row via the (still open) `accounts_select` for the return value.
- Produces (changed signatures, expo mirrors them in Task 6): `createInAppInvite(account, accountId, invitedWallet, role, expiresInDays?)`, `createLinkInvite(account, accountId, role, expiresInDays?)`, `acceptInvite(account, inviteId)`, `declineInvite(account, inviteId)`, `revokeInvite(account, inviteId)`, `leaveOrg(account, accountId)` — `invitedBy` is now derived server-side from the verified signer, so the parameter disappears. Reads: `fetchInviteByToken(token)` → `supabase.rpc("get_invite_by_token", { p_token })`, `fetchPendingInvites(accountId, wallet)` → `rpc("list_pending_invites", …)`,`hasPendingInvite(accountId, wallet)` → `rpc("has_pending_invite", …)`.
332
+
- Produces (changed signatures, expo mirrors them in Task 6): `createInAppInvite(account, accountId, invitedWallet, role, expiresInDays?)`, `createLinkInvite(account, accountId, role, expiresInDays?)`, `acceptInvite(account, inviteId)`, `declineInvite(account, inviteId)`, `revokeInvite(account, inviteId)`, `leaveOrg(account, accountId)` — `invitedBy` is now derived server-side from the verified signer, so the parameter disappears. Reads (amended 2026-07-31): `fetchInviteByToken(token)` → `supabase.rpc("get_invite_by_token", { p_token })` (the one anon RPC); `fetchPendingInvites(account, accountId)` → `callOrgMembership(account, "list_invites", { accountId })`;`hasPendingInvite(account, accountId)` → `callOrgMembership(account, "has_pending_invite", { accountId })` (answers for the signer only).
365
333
366
334
-[ ]**Step 1: Rewire each export** to the contract above (writes → `callOrgMembership`; reads → RPCs). Update every call site the exploration listed: `apps/web/src/app/invite/[token]/page.tsx:67,80` plus any `fetchPendingInvites` dashboards (`git grep -n "fetchPendingInvites\|createInAppInvite\|createLinkInvite" apps/web/src` and fix all hits).
367
335
-[ ]**Step 2: Verify** — `pnpm test:web` still green; `git grep -n 'from("invite_tokens")' apps/web/src` returns ZERO hits (every touchpoint goes through RPC/edge fn now); same for `.from("account_owners").insert` and `.from("accounts").update`.
@@ -612,4 +580,4 @@ git commit -m "docs: findings 1, 2 and 4 close — the workspace launch gate lif
612
580
613
581
-**Spec coverage:** W0 = Tasks 1–8 + 13 (findings §1 §2 §4, invite corollary; offsite backups + firewall stay user one-liners, listed in Task 13). W1 = Tasks 9–12 + 13 (role-based write, mobile route, flag flip). AI-Act disclosure: already shipped 2026-07-30 (parallel session) — excluded on purpose.
614
582
-**Ordering constraint restated:** Tasks 3–7 MUST be live in production before the Task 2 migration is applied. The plan encodes this by deferring application to Task 13.
615
-
-**Known accepted risks:** (1) old Expo builds break on membership writes after the lockdown until the EAS build ships — coordinated in Task 13; (2) `create_account_with_owner` keeps the repo's trusted-wallet posture for NEW accounts only (documented in Task 2); (3) `list_pending_invites` is read-only trusted-wallet (mild exposure: invited wallets/roles of one org).
583
+
-**Known accepted risks:** (1) old Expo builds break on membership writes after the lockdown until the EAS build ships — coordinated in Task 13; (2) `get_invite_by_token` is bearer-semantics by design — anyone holding a link token can read that one invite row (that IS the link-invite product behavior). (Amendment 2026-07-31: the v1 trusted-wallet RPCs `create_account_with_owner` / `list_pending_invites` / `has_pending_invite` were withdrawn after task review showed the wallet parameter is attacker-controlled; those flows are signature-verified edge-fn actions now.)
0 commit comments