{
 "id": "9.6",
 "title": "Access rules",
 "department": "Security, privacy and legal",
 "department_slug": "security-privacy-and-legal",
 "direction": null,
 "branded": false,
 "status": "draft",
 "owner": "Param",
 "last_verified": "Mon Oct 26",
 "holds": "Who opens what: domains, invites, guests, staff",
 "urls": {
  "html": "https://handbook.joinsquareup.com/security-privacy-and-legal/access-rules",
  "markdown": "https://handbook.joinsquareup.com/security-privacy-and-legal/access-rules.md",
  "json": "https://handbook.joinsquareup.com/security-privacy-and-legal/access-rules.json"
 },
 "other_directions": [],
 "tldr": {
  "headline": "A brand opens only to its own people, and staff see everything",
  "points": [
   {
    "icon": "people",
    "text": "Eight roles, from root admin to demo visitor; outsiders see one wall pointing to the demo."
   },
   {
    "icon": "lock",
    "text": "A brand's work email, a 14 day invite or a jam guest link gets you in."
   },
   {
    "icon": "warn",
    "text": "Anyone at two whole staff domains can open every brand today."
   },
   {
    "icon": "flag",
    "text": "Arata's Meta and Shopify are protected from disconnect unless a root admin forces it."
   },
   {
    "icon": "check",
    "text": "Every new route must pass the role check and the closed-store gate."
   }
  ],
  "stat": {
   "value": "14",
   "label": "day invite life"
  },
  "decide": "Limit staff to a named address list on production and turn on STAFF_REQUIRE_HD?",
  "mood": "waiting"
 },
 "sources": [
  "brew/auth.js:2-12, 16-36, 37-38, 89-119, 139-171, 180-193, 239-246, 254-257, 264-305",
  "brew/closed-gate.js:1-75",
  "brew/connectors/protected.js:1-4",
  "brew/sessions.js:1-36",
  "brew/server.js:657-666",
  "brew/tools/prod-env.sh:9",
  "brew/SCOPE.md:24",
  "memory brew-access-rules, brew-closed-store-gate, brew-shopify-app-review, brew-real-connectors, brew-prod-backend, never-disconnect-arata-meta",
  "handbook content/engineering/changelog.md:58, 81"
 ],
 "open_questions": [
  "**Should every `shopos.ai` address see every brand?** Proposal: no; a named list of staff addresses on production, and the two domains only on staging.",
  "**Turn on `STAFF_REQUIRE_HD=1` on production?** Proposal: yes, so a personal Google account made with a staff address never counts as staff.",
  "**Should guest links expire?** Proposal: 30 days, with a Revoke button for brand admins."
 ],
 "parts": {
  "Answer": "Who opens what: our own staff see every brand, a brand opens only to its own work email, an invite or a single jam guest link, and everyone else is sent to the demo stores; this page settles those rules so no new feature adds a way in that skips them.",
  "Today": "### The roles\n\n| Who | How they get in | What they may do | Source |\n|---|---|---|---|\n| Root admin | `ADMIN_EMAILS`; on production `param@joinsquareup.com` only | Everything staff do, plus sign someone out everywhere and force a change to a protected connection | [src: brew/auth.js:89-91] [src: brew/tools/prod-env.sh:9] |\n| Staff | Any address at `joinsquareup.com` or `shopos.ai`, subdomains too | Every brand, every view, every make; never claim a brand that is not at their own domain | [src: brew/auth.js:10-11, 33-36, 166] |\n| Brand admin, the owner | First official email to tap Claim, connect an account or confirm the read | Invite, set views, connect and disconnect, hand over; a brand always keeps one admin | [src: brew/auth.js:6-7, 159-171, 194-215] |\n| Member, same company | Signs in with the claimed brand's own domain; joins on their own with every view; admins are emailed | Everything but team changes; may invite others to their own views only | [src: brew/auth.js:104-109, 294-296] |\n| Member, invited | An emailed invite, 14 days, valid only for that Google account | The views the invite named | [src: brew/auth.js:78, 180-193, 264-276] |\n| Jam guest | A shared jam link and a sign in | Chat, edit the brief and make drafts in that one jam; never share, send, approve, assign, discard or make paid media | [src: brew/SCOPE.md:24] [src: handbook content/engineering/changelog.md:58] |\n| Outsider | Signed in with Gmail or another domain | One wall pointing to demo.joinsquareup.com; cannot start a new read or sign up for Plays emails | [src: memory brew-access-rules] |\n| Visitor on the demo | Anyone | A throwaway sandbox of a prepared brand; makes are open there | [src: brew/auth.js:37-38, 148] |\n\n### What counts as the brand's own email\n\n- Same domain as the store, a subdomain of it, or the company domain the store sits under. [src: brew/auth.js:27-31]\n- Free mail and shared platforms such as `myshopify.com`, `wixsite.com` or `vercel.app` never count. [src: brew/auth.js:16, 22-24]\n- A brand our staff claimed for it, such as Arata, lets the store's own domain join. [src: brew/auth.js:106-108]\n- Starting a read gives nothing on its own since 23 Oct. [src: brew/auth.js:139-140] [src: memory brew-access-rules]\n\n### Special cases\n\n- **A closed Shopify store**, read from its admin while behind the password page, is shown to its team and staff only; everyone else gets \"This store isn't open yet\". [src: brew/closed-gate.js:1-12, 50-71] [src: memory brew-closed-store-gate]\n- **Inside the Shopify admin**, Shopify's session token names the person; the installer becomes admin of an unclaimed brand, the store owner becomes admin, other staff members. [src: memory brew-shopify-app-review]\n- **Connecting accounts** is for brand admins and staff only; teammates may sync. [src: memory brew-real-connectors]\n- **Protected connections**, `PROTECTED_CONNECTIONS=arata-in`, refuse disconnect, re-select or reconnect unless a root admin forces it. [src: brew/connectors/protected.js:1-4] [src: memory never-disconnect-arata-meta]\n\n### Sessions and logging\n\n- Google sign in uses PKCE and a nonce; only a Google verified email is trusted. [src: brew/auth.js:3-4, 220-238]\n- The session is a signed cookie for 30 days with a session row; signing out ends it on every instance. [src: brew/auth.js:243-244, 254-257] [src: brew/sessions.js:1-4]\n- A sign out link from another site does nothing. [src: brew/auth.js:254-256]\n- `STAFF_REQUIRE_HD=1` would accept a staff address only from that domain's own Google Workspace; production does not set it. [src: brew/auth.js:239-241] [src: brew/tools/prod-env.sh:9]\n- Every signed in email that opens a brand is logged with its access kind; only root admins read the list. [src: brew/server.js:657-666, 817] [src: handbook content/engineering/changelog.md:81]\n\n### Decided exceptions\n\n- A personal Gmail stays on staging's admin list, by Param's choice on 23 Oct; do not flag it again. [src: memory brew-access-rules]",
  "Tomorrow": "| Item | Version | Tag |\n|---|---|---|\n| Official email, invite or guest link as the only ways in | v1 | Stays |\n| `STAFF_REQUIRE_HD=1` on production | v1 | New, proposed |\n| Staff by named address, not two whole domains | v1 | Changes, proposed |\n| Guest links that expire and can be revoked | v1 | New, proposed |\n| Proof of ownership by store admin or DNS, beside the email domain | v2 | New |\n| Agency seats: one person on several brands, invited by each | v2 | New |\n| Who may approve a post or a schedule, per brand | v2 | New |\n| Who may set ad rules and undo a run | v4 | New |",
  "How we do it": "1. Never add a route, page or file that opens brand data without going through `auth.roleOf`, `genOk` and the closed store gate.\n2. Every new brand route falls under the closed gate's `slugIn` patterns. [src: memory brew-closed-store-gate]\n3. When touching auth, test the outsider paths: Gmail, another domain, a guest, signed out.\n\n```bash\ncd /Users/param/Documents/ChatGPT/brew && AI_MOCK=1 node --test test/access.test.js test/qa-access.test.js test/qa-final-access.test.js test/qv1-roles-security.test.js\n```\n\n4. To see who opened a brand, a root admin calls `GET /api/admin/visitors?slug=<slug>`.\n5. To end someone's sessions everywhere, a root admin uses `/api/admin/sessions`.",
  "Numbers": "| Metric | Definition | Where measured | Status |\n|---|---|---|---|\n| Blocked opens | Signed in emails refused per brand per week | `access_log`, access blocked | Proposed |\n| Staff with full access | Distinct staff emails seen in `access_log` | `access_log`, access brew | Proposed |\n| Unclaimed partner brands | Partners with no brand admin of their own | `members.json` per brand | Proposed |",
  "Open": "1. **Should every `shopos.ai` address see every brand?** Proposal: no; a named list of staff addresses on production, and the two domains only on staging.\n2. **Turn on `STAFF_REQUIRE_HD=1` on production?** Proposal: yes, so a personal Google account made with a staff address never counts as staff.\n3. **Should guest links expire?** Proposal: 30 days, with a Revoke button for brand admins."
 }
}