repoaccess-core 0.2.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "repoaccess-core",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "type": "module",
5
5
  "license": "AGPL-3.0-or-later",
6
6
  "exports": {
@@ -9,7 +9,8 @@
9
9
  },
10
10
  "files": [
11
11
  "src",
12
- "docs",
12
+ "docs/setup-guide.md",
13
+ "docs/user-guide-stripe.md",
13
14
  "README.md",
14
15
  "LICENSE"
15
16
  ],
@@ -1,139 +0,0 @@
1
- # GitHub + Core: the validated manual walkthrough (source for the Agentic Setup wizard)
2
-
3
- Purpose: the exact procedure for the **GitHub side** of a self-hosted RepoAccess core: the org, the
4
- teams that carry repo access, the per-product isolation setting, the org hardening, and the
5
- fine-grained PAT the worker uses. Run for real (2026-06). Companion to the Stripe walkthrough; together
6
- they cover the full core setup. This is the base script for the Agentic Setup wizard's GitHub path.
7
-
8
- ## Legend
9
-
10
- - **[USER]** a GitHub dashboard click or value only the user can do. (The agent cannot click GitHub or
11
- mint a PAT.) The agent guides precisely and reads back the non-secret results (org slug, team name).
12
- - **[AGENT]** code or CLI the coding agent does.
13
- - **[SECRET]** a secret value (the PAT). The user pastes it into `.dev.vars` themselves; the agent must
14
- never read, request, or echo it. It references the secret by name only.
15
-
16
- ## 0. Prerequisites
17
-
18
- - A GitHub **organization** you own (Owner role). Personal accounts have no teams, so an org is
19
- required; a Free org is fine.
20
- - The private repo(s) you sell live in this org.
21
-
22
- ## 1. Create a team per product tier [USER]
23
-
24
- - Org, then Teams, then New team. Name it after the tier (e.g. `pro`). One team per product or tier.
25
- - The worker adds buyers to this team, and the team carries the repo access (Step 2). The worker never
26
- adds direct collaborators.
27
-
28
- ## 2. Attach the private repo(s) to the team [USER]
29
-
30
- - Team, then Repositories, then Add repository. Add the repo(s) this tier grants. Give the team `Read`
31
- (buyers clone, they do not push) or `Write` if a tier genuinely needs it.
32
- - Keep the repos **private**.
33
-
34
- ## 3. Set org Base permissions to `No permission` [USER] (the isolation setting)
35
-
36
- - Org, then Settings, then Member privileges, then Base permissions, set to **No permission**, Save.
37
- - Why it matters: Base permissions are the floor that every org member gets to every repo in the org.
38
- Many orgs default to `Read`, which means any buyer of any product could see all your private repos and
39
- team scoping buys you nothing. With `No permission`, members get access ONLY through their team(s):
40
- buying product A (team A maps to repo A) grants repo A alone; other products' repos stay invisible.
41
-
42
- ## 4. Harden the org (members are paying customers, not teammates) [USER]
43
-
44
- Treat members as untrusted; access comes only through teams. Owners keep full access regardless (these
45
- toggles restrict members, never you).
46
-
47
- **Org, Settings, Member privileges** (each block has its own Save):
48
-
49
- - Base permissions, set to No permission (Step 3, the critical one).
50
- - Repository creation, uncheck Public and Private (members do not create repos).
51
- - Repository forking, off (no forking private repos into member accounts).
52
- - Projects base permissions, No access.
53
- - Pages creation, uncheck Public and Private.
54
- - App access requests, Disable app access requests.
55
- - GitHub Apps ("Allow repository admins to install..."), off.
56
- - Admin repository permissions, off for all: Repository visibility change (else a member-admin could
57
- flip a private paid repo to public), Repository deletion and transfer, Issue deletion, Branch renames.
58
- - Member team permissions, Team creation, off.
59
-
60
- **Org, Settings, Authentication security:**
61
-
62
- - Do NOT "Require two-factor authentication for everyone". It removes members without 2FA (your buyers)
63
- and blocks them from accepting invites. The same risk applies to an IP allow list. Enable 2FA on your
64
- own Owner account instead.
65
-
66
- **Org, Settings, Third-party Access:**
67
-
68
- - OAuth app policy, keep Access restricted (approved apps only).
69
- - Personal access tokens, Fine-grained tokens, **Allow access via fine-grained PATs** (the worker's
70
- `GITHUB_TOKEN` needs this; "Restrict" breaks grants). Optionally Require administrator approval for
71
- members' tokens.
72
- - Token expiry, leave "Fine-grained PATs must expire" on (good hygiene). This means the worker token
73
- expires too; see rotation below.
74
-
75
- **Optional, Discussions as a feedback channel.** Enabling "Allow users with read access to create
76
- discussions" gives buyers a built-in Q&A space. Note: discussions in a private repo are visible to
77
- everyone with access to that repo (your other buyers), so it is a shared space, not private 1:1 support.
78
-
79
- ## 5. Create the worker's fine-grained PAT [USER] + [SECRET]
80
-
81
- First enable fine-grained PATs for the org if needed: Org, Settings, Personal access tokens.
82
-
83
- - Your account, Settings, Developer settings, Fine-grained tokens, Generate new token.
84
- - **Resource owner:** your **org** (not your personal account).
85
- - **Repository access:** **None** (the token only manages membership; it never touches repos).
86
- - **Organization permissions:** **Members, Read and write** (and nothing else).
87
- - **Expiration:** pick a date. The org caps fine-grained PAT lifetime (often 366 days). Note it for
88
- rotation.
89
- - Generate. **[SECRET]** copy the token (`github_pat_…`) and paste it into `.dev.vars` as `GITHUB_TOKEN`
90
- yourself. The agent never sees it.
91
- - If the org requires admin approval for tokens: an **Owner-created token is ready immediately** (no
92
- pending step for you, the owner). A member's token would wait for approval.
93
-
94
- ## 6. Wire it [AGENT] + [SECRET]
95
-
96
- - `config.githubOrg` = your org slug. **[AGENT]**
97
- - `.dev.vars`, `GITHUB_TOKEN` = the PAT (user pastes). **[SECRET]**
98
- - `config.productTeamMap` maps each product id to the team(s) from Step 1.
99
-
100
- ## 7. Verify [USER] + watch
101
-
102
- - The cleanest verification is the first live grant (see the Stripe walkthrough): a test purchase, the
103
- worker adds the buyer to the team, GitHub emails an invite, accept it, the buyer shows under Org,
104
- People as a member in the team.
105
- - Grant fails 401/403: the PAT is missing Members Read and write, or fine-grained PATs are Restricted at
106
- the org, or the token is pending approval, or it expired.
107
- - Grant fails 404 (user not found): the GitHub username does not exist (a buyer typo), not a token
108
- problem. In username mode the buyer gets a claim link to self-correct.
109
-
110
- ## Token rotation (operational)
111
-
112
- The worker's `GITHUB_TOKEN` is a fine-grained PAT and **will expire** (the org caps the lifetime).
113
- Before it does: issue a new token with the same scope (Resource owner org, Repository access none,
114
- Members Read and write), update the `GITHUB_TOKEN` secret (`wrangler secret put GITHUB_TOKEN`, or
115
- re-deploy with the new `.dev.vars`), and re-approve it if approval is required. GitHub emails a warning
116
- before expiry; set a calendar reminder too. If it lapses, grants and revokes stop until you rotate.
117
-
118
- ## Gotchas
119
-
120
- - **New-org invite cap:** 50 invitations per 24h for the first month. Age the org before a big launch.
121
- - **The invite is the identity check:** the worker sends an invitation only the real account owner can
122
- accept. A wrong handle just means the invite sits unaccepted (harmless). There is deliberately no
123
- "Login with GitHub".
124
- - **Org must allow fine-grained PATs** (Step 4, Third-party Access), or grants break.
125
- - **Do not require org-wide 2FA** (Step 4); it locks out buyers.
126
- - **Owner token is immediate** even on approval-required orgs.
127
-
128
- ## Agentic Setup framing (for the wizard build)
129
-
130
- - Every GitHub step here is [USER] (the agent cannot click GitHub or mint a PAT). The agent's job is to
131
- guide precisely (exact menu paths and toggle values), then read back the non-secret results (org slug,
132
- team name) to wire into `config`.
133
- - **Secret-safe:** the agent tells the user to paste the PAT into `.dev.vars` as `GITHUB_TOKEN`, and
134
- never reads, requests, or echoes the token.
135
- - **Checkpoint:** confirm the team exists and carries the repo, confirm Base permissions = No permission,
136
- and confirm the PAT's resource owner and permissions before the user generates it. Verify the whole
137
- chain only via the first live grant (Stripe walkthrough).
138
- - **Order:** do the GitHub side first (org, team, repo attach, base permissions, hardening, PAT) before
139
- the worker config and deploy, so `config.githubOrg` and `GITHUB_TOKEN` are ready when you deploy.
@@ -1,223 +0,0 @@
1
- # Stripe + Core: the validated manual walkthrough (source for the Agentic Setup wizard)
2
-
3
- Purpose: the exact, end-to-end procedure for standing up a self-hosted RepoAccess **core** worker with
4
- **Stripe** and proving it live. Every step below was validated for real (in Stripe test mode). This is
5
- the base script the **Agentic Setup wizard** walks a user through.
6
-
7
- ## Legend
8
-
9
- - **[USER]** a dashboard click or value only the user can do (Stripe / GitHub / Cloudflare web UI). The
10
- agent cannot click these; it must guide the user and read back the non-secret results.
11
- - **[AGENT]** code or CLI the coding agent does (edit files, run `wrangler`).
12
- - **[SECRET]** a secret value. The user pastes it into `.dev.vars` themselves. The agent must NEVER
13
- read, request, or echo a secret value. It only references the secret by name.
14
-
15
- ## 0. Prerequisites
16
-
17
- - A **Stripe account in Test mode** (toggle, top of the dashboard). Do all setup and testing in test
18
- mode; no live activation needed for core.
19
- - A **Cloudflare account** + `wrangler` CLI (`npm i -g wrangler`, then `wrangler login`).
20
- - A **GitHub org** that owns the private repo(s) you sell, hardened per the setup guide (base
21
- permissions `No permission`, fine-grained PAT with Members Read and write).
22
- - The worker scaffolded: `npm install repoaccess-core`, the entry (`src/index.ts`),
23
- `src/repoaccess.config.ts`, and the `wrangler.jsonc` bindings (Workflow + KV + Durable Object), per
24
- the setup guide.
25
-
26
- ## 1. Stripe: create the product [USER]
27
-
28
- 1. Product catalog, then Create product. Name it (e.g. "My Boilerplate"); set a one-time price.
29
- (Stripe renamed this: it is **Product catalog -> Create product**, not the older "Products -> Add
30
- product".)
31
- 2. Open the product and copy its **product id** (`prod_…`). You map it to a team in Step 5.
32
-
33
- ## 2. Stripe: create a Payment Link [USER]
34
-
35
- A Payment Link is the no-code checkout. (A coded Checkout Session maps 1:1; see the notes in Step 3.)
36
-
37
- 1. Payment Links, then New, then **Products or subscriptions**, select your product, quantity 1.
38
- 2. **Options:** leave everything OFF. Do not enable Managed Payments, tax collection, name/address/phone
39
- collection, or payment limits. They add friction and complexity you do not need.
40
- 3. **Advanced options**, by grant mode:
41
- - **username mode** (the buyer types their GitHub handle at checkout): check **Add custom fields**,
42
- Type **Text**, Label **GitHub username**. Do not mark it optional. Stripe auto-derives the field
43
- **key** from the label; because the label contains "github", the worker reads it. No manual key is
44
- needed (the no-code builder does not expose the key field anyway).
45
- - **claim mode** (the buyer gets a one-time claim link after paying): add NO custom field.
46
- 4. **After payment:** Show confirmation page (default).
47
- 5. Create the link and copy its URL (`buy.stripe.com/test_…`).
48
-
49
- ## 3. Stripe: set `product_id` metadata on the link [USER]
50
-
51
- This is the step people miss. The `checkout.session.completed` webhook omits line items, so the worker
52
- reads the product from **`metadata.product_id`**. The no-code link _builder_ has no metadata field, but
53
- the **created link's detail page** does:
54
-
55
- 1. Open the Payment Link's detail page, scroll to **Metadata**, click **Edit metadata**.
56
- 2. Add key `product_id`, value `prod_…` (from Step 1). Save.
57
- 3. Stripe copies a Payment Link's metadata onto every Checkout Session it creates, so it reaches the
58
- webhook. (Verified live: the worker read `prod_…` and mapped to the team.)
59
-
60
- Coded Checkout Session variant: set `metadata.product_id` when you create the session. If you collect
61
- the GitHub handle server-side instead of via a custom field, also set `metadata.github_username` (the
62
- adapter reads metadata first, then the "github" custom field).
63
-
64
- ## 4. Stripe: create the webhook destination [USER] + [SECRET]
65
-
66
- In Stripe's current **Event destinations** flow you select the EVENTS FIRST, then configure the
67
- destination (endpoint URL). Order:
68
-
69
- 1. Developers, then Webhooks (Event destinations), then add a destination.
70
- 2. **Select the events FIRST:** exactly these three: `checkout.session.completed`, `charge.refunded`,
71
- `charge.dispute.created`. (The current flow asks for events before the URL.)
72
- 3. **Payload style:** if a payload-style choice appears, pick **Snapshot**. The current Event
73
- destinations flow may not surface this option (it defaults to the full snapshot payload); if you do
74
- not see it, just continue.
75
- 4. **Configure destination - Endpoint URL:** `https://<your-worker>.workers.dev/wh/stripe/<SECRET_PATH>`
76
- - `<SECRET_PATH>` is a random hard-to-guess string you generate with
77
- `node -e "console.log(require('crypto').randomBytes(16).toString('hex'))"` (cross-platform; node is
78
- already required). It is NOT a worker secret (the HMAC signature is the real gate); it only lives in
79
- this URL. Keep it in your
80
- notes; a handy place is a commented `STRIPE_WEBHOOK_PATH` line in `.dev.vars` (the `.dev.vars.example`
81
- template has the slot) so you do not lose it.
82
- - `<your-worker>` is known after the first deploy (Step 6). The URL is predictable
83
- (`https://<worker-name>.<account>.workers.dev/...`), so you can create the endpoint up front, or
84
- come back after deploy.
85
- 5. Create it, then reveal the **Signing secret** (`whsec_…`). **[SECRET]** paste this into `.dev.vars`
86
- as `STRIPE_WEBHOOK_SECRET` yourself. The agent never sees it.
87
-
88
- ## 5. Worker: config [AGENT]
89
-
90
- `src/repoaccess.config.ts` (typed object, no escaped JSON):
91
-
92
- ```ts
93
- export const config: RepoAccessConfig = {
94
- githubOrg: 'your-org',
95
- productTeamMap: {
96
- stripe: { 'prod_…': { teams: ['pro'], grant_mode: 'username' } }, // or grant_mode: "claim"
97
- defaults: {
98
- teams: [],
99
- grant_mode: 'claim',
100
- revoke_policy: { mode: 'log_only' },
101
- },
102
- },
103
- }
104
- ```
105
-
106
- - Map the **product id** from Step 1 to the GitHub team(s) that carry the repo.
107
- - `grant_mode`: `username` (use the custom-field handle; auto-falls-back to a claim link if the handle
108
- is missing, malformed, or does not exist on GitHub) or `claim` (always send a claim link).
109
- - `revoke_policy`: `{ mode: "auto_revoke" }` to remove access on refund/chargeback, else
110
- `{ mode: "log_only" }`.
111
- - Keep `defaults.teams` empty unless you intend a catch-all (an unmapped product or a stray webhook
112
- falls through to `defaults`).
113
-
114
- ## 6. Worker: secrets + deploy [USER/SECRET] + [AGENT]
115
-
116
- 1. `.dev.vars` holds ONLY secrets (the user pastes the values):
117
- - `GITHUB_TOKEN` the fine-grained PAT (Members Read and write on the org). **[SECRET]**
118
- - `STRIPE_WEBHOOK_SECRET` the `whsec_…` from Step 4. **[SECRET]**
119
- 2. Deploy: `wrangler deploy --secrets-file .dev.vars`. This uploads code plus secrets together; the
120
- first deploy creates the worker and prints its `https://<worker>.workers.dev` URL.
121
- 3. Open `/health`, expect `{"status":"ok"}` (confirms the worker booted).
122
- 4. Back in Stripe (Step 4), make sure the webhook Endpoint URL matches the deployed worker URL plus your
123
- secret path.
124
-
125
- ## 7. Test: grant [USER] + watch
126
-
127
- 1. In a terminal: `wrangler tail <worker-name>` (keep it streaming, started before you pay).
128
- 2. Open the Payment Link, pay with test card **4242 4242 4242 4242**, any future expiry, any CVC, any
129
- ZIP, any email.
130
- - **username mode:** type a real GitHub handle in the "GitHub username" field. Expect in tail:
131
- `POST /wh/stripe/…` then `checkout.session.completed` then a **direct grant**, with `access.granted`
132
- and the buyer added to the team. GitHub emails them an invite to accept. No claim page.
133
- - **claim mode:** no field. Expect `claim.pending`. The claim link is **not in tail** (the token is
134
- redacted for safety). **[AGENT] fetches the token directly from KV and hands the buyer the full
135
- clickable link** - PREFERRED: `wrangler kv key get "claim_txn:stripe:<transaction_id>" --binding
136
- ENTITLEMENTS` returns the token (the `<transaction_id>` is the `pi_...` from the `claim.pending`
137
- event), then present `https://<worker>.workers.dev/claim/<token>`. FALLBACK (if the KV read is
138
- unavailable): read `claim_url` from the Workflow dashboard `emit:claim.pending` step output. Never
139
- make the buyer hunt for it. Open it, enter the handle, get granted.
140
- 3. **Accept the invite [USER] - the worker cannot do this for you.** `access.granted` means the worker
141
- **created** the GitHub invitation; it does NOT mean the buyer has joined. GitHub emails the buyer an
142
- invitation - open it and **accept** (or accept at `https://github.com/orgs/<org>/invitation`). The
143
- buyer becomes a member only after accepting. (Agent: surface this step the moment `access.granted`
144
- appears; do NOT silently poll the Workflow or Org, then People for membership beforehand - membership
145
- is gated on this human accept, so waiting for it without telling the deployer to accept will hang.)
146
- 4. Verify in GitHub: Org, then People, the buyer is now a member in the right team (it shows only after
147
- they accept the invite in step 3).
148
-
149
- ## 8. Test: refund / revoke [USER]
150
-
151
- Do the refund test BEFORE the typo/claim test (Step 9), so revoke runs against a single clean grant from
152
- Step 7. Refunding first means the same handle is free to reuse on the claim page in Step 9 without
153
- muddying it - and one GitHub account is enough for the whole run. (Refund last, and the Step-7 handle has
154
- been granted by two transactions, so a per-transaction revoke looks confusing.)
155
-
156
- 1. Stripe, then Payments, open the Step-7 test payment, then **Refund payment**, full amount, Refund.
157
- 2. Expect: `POST /wh/stripe/…` (`charge.refunded`), then `access.revoked`, the buyer removed from the
158
- team and any pending invite cancelled.
159
- 3. Note: `product_id` is empty on the refund event, but the revoke resolves the policy from the stored
160
- grant record, so it still works. `is_full_refund: true` is reliable for Stripe (it compares
161
- `amount_refunded` vs `amount`).
162
-
163
- ## 9. Test: the typo path (username mode) [USER]
164
-
165
- 1. Pay again, type a **valid-format-but-nonexistent** handle (e.g. `someone-nope-xyz`).
166
- 2. Expect: team-add 404 (user not found), then **not** `access.failed`, but a `grant → claim fallback`,
167
- then `claim.pending`. A typo never strands a paying buyer. (Validated live.)
168
- 3. **[AGENT] hands the buyer the claim link** - do not make them find it. PREFERRED: fetch the token from
169
- KV with `wrangler kv key get "claim_txn:stripe:<transaction_id>" --binding ENTITLEMENTS` (the
170
- `<transaction_id>` is the `pi_...` from `claim.pending`), then present
171
- `https://<worker>.workers.dev/claim/<token>`. FALLBACK: the Workflow dashboard `emit:claim.pending`
172
- step output (`claim_url`). Open it, enter a real handle (the Step-7 handle is free to reuse now that
173
- Step 8 revoked it), and submit.
174
- 4. **Accept the invite [USER].** The claim-page grant creates a NEW GitHub invitation, exactly like Step 7. Open your email and **accept** it (or accept at `https://github.com/orgs/<org>/invitation`); the
175
- buyer becomes a member only after accepting. Then verify in GitHub (Org, then People). Do not skip
176
- this - the same accept-the-invite step applies to every grant, claim or direct.
177
-
178
- ## 10. What is normal, and gotchas (so the wizard does not false-alarm)
179
-
180
- - **"AccessWorkflow.run - Exception Thrown" / "Cancelled" in tail are benign.** That is how Cloudflare
181
- Workflows logs durable step suspension between steps; it appears on fully successful runs too. The
182
- proof a run is fine is the Workflow dashboard showing Status: **Completed**, not Failed.
183
- - **Tail batches a Workflow's logs until the instance settles, so `access.granted` may lag.** Do not
184
- treat a not-yet-seen `access.granted` as a failure or sit waiting on the stream. Confirm a grant
185
- directly from the KV grant record:
186
- `wrangler kv key get "grant:stripe:<transaction_id>" --binding ENTITLEMENTS` (the `<transaction_id>`
187
- is the `pi_...`); a returned record means the grant succeeded even if tail has not flushed it yet.
188
- - **`transaction_id` = `payment_intent`** (`pi_…`), stable across the order and its refund/dispute. (Not
189
- the checkout session id, which the refund event lacks.) This is what correlates a refund back to its
190
- grant.
191
- - **Custom-field key** auto-derives from the label and must contain "github" (label "GitHub username"
192
- yields a key like `githubusername`). If a valid handle still routes to a claim page, the key did not
193
- match: use `metadata.github_username` instead.
194
- - **KV + workflow naming:** the workflow `name` is account-global; if you run more than one RepoAccess
195
- worker on the account, give each a distinct name, or the later deploy reassigns the workflow and
196
- breaks the other binding. (Core's is already env-aware - `oss-access-workflow` /
197
- `oss-access-workflow-production` - reused idempotently; leave it.) The KV namespace id in
198
- `wrangler.jsonc` must be the real one from `wrangler kv namespace create` (a placeholder will not bind).
199
- Create it env-correctly so the **title** follows the worker-derived convention -
200
- `<worker-name>-ENTITLEMENTS` (sandbox) or `<worker-name>-production-ENTITLEMENTS` (`--env production`):
201
- that prefix is what `wrangler` produces when the create picks up the worker `name` and `--env`. A bare
202
- `ENTITLEMENTS` title means the prefix did not apply - recreate or rename to the convention. The
203
- **binding stays `ENTITLEMENTS`** (the code reads `env.ENTITLEMENTS`); only the namespace title follows
204
- the convention.
205
- - **Test card only in test mode.** `4242 4242 4242 4242` works in test mode; real cards do not.
206
-
207
- ## Agentic Setup framing (for the wizard build)
208
-
209
- Split each step by who can do it, so the wizard hands off cleanly:
210
-
211
- - The agent CAN: scaffold and edit `repoaccess.config.ts`, `wrangler.jsonc`, and the `.dev.vars` key
212
- names (names only); run `wrangler kv namespace create`, `wrangler deploy`, `wrangler tail`; check
213
- `/health`; and read the Workflow dashboard via the user.
214
- - The agent CANNOT (must guide the user to click): everything in Stripe (product, link, metadata,
215
- webhook), the GitHub PAT and org hardening, and accepting GitHub invites.
216
- - **Secret-safe:** for every `whsec_…` and PAT, the agent tells the user to paste it into `.dev.vars`
217
- itself, and must never read, request, or echo a secret value. It references secrets by name only.
218
- - **Checkpoint and verify after each block** (product, link, metadata, webhook, config, deploy, test)
219
- before moving on: confirm the product id, confirm `/health`, confirm `access.granted` in the Workflow
220
- run. Do not advance on an unverified step.
221
- - **Order that minimizes back-and-forth:** GitHub org + PAT, then the worker scaffold + config, then the
222
- first deploy (to get the worker URL), then the Stripe product + link + metadata + webhook (URL now
223
- known), then re-deploy with the secret, then the test purchases.
@@ -1,339 +0,0 @@
1
- # RepoAccess core setup wizard (shared orchestrator)
2
-
3
- This is the agent-agnostic setup orchestrator. Any coding agent runs it the same way: the Claude Code
4
- command (`.claude/commands/repoaccess-setup.md`) and the OpenCode command
5
- (`.opencode/command/repoaccess-setup.md`) are thin wrappers that point here, and any other agent
6
- (Codex, Cursor, ...) is told to open the repo and follow this file. This document is the single source
7
- of truth for the wizard; drive the deployer through it exactly as written.
8
-
9
- You are an interactive setup wizard. Your job is to hand-walk the person you are helping (the
10
- **deployer**) through standing up their own RepoAccess **core** worker and proving it works with a real
11
- test purchase. You orchestrate; you do not re-teach. The exact, validated procedures live in three
12
- shipped docs in this repo and they are the single source of truth. Read them and **drive** the deployer
13
- through them; never paraphrase or re-derive their steps (that causes drift).
14
-
15
- This is a deployer onboarding wizard, not a contributor guide. Do not explain the architecture, the
16
- adapter contract, or how to add a new adapter. (That is `AGENTS.md`, a different audience.) Stay on the
17
- one job: get this deployer to a working, live-tested worker.
18
-
19
- ## Session rules (read first, apply for the whole run)
20
-
21
- These are hard rules. Internalize them before the role split and before you ask the mode question.
22
-
23
- 1. **Assume a fresh, blank-slate, first-time setup.** Ignore ALL recalled memory, prior-run state,
24
- saved deploy details, and "you've done this before" signals. They are noise from a re-used machine,
25
- not this deployer's reality. Nothing is set up yet unless the deployer EXPLICITLY says otherwise.
26
- 2. **Ask ONLY the one mode question** (sandbox/test vs production; recommend sandbox for a first run).
27
- Do NOT detect or infer prior deploy state, and do NOT present any "production / re-run / re-test /
28
- what now" menu. There is exactly one path: hand-hold a fresh setup of the chosen mode.
29
- 3. **Do not deliberate aloud** about reusing or clobbering resources, "ephemeral-edit patterns", or
30
- whether "resources still exist". Treat the cloud side as empty; create what the steps say to create.
31
- 4. **Do not persist deployer-specific deploy details** (worker URL, org, team, product id, KV id) to
32
- long-lived or project memory. Keep anything you track session-scoped only.
33
- 5. **Tone: lead by the hand** - concise, action-oriented ("do X, then tell me"). First-time eyes; no
34
- long internal-reasoning dumps, no gotcha questions.
35
- 6. **Re-run is a separate, opt-in path.** ONLY if the deployer states up front "I already deployed
36
- this, help me re-run" do you verify live state (probe `/health`, `wrangler kv namespace list`) and
37
- reuse. NEVER infer a re-run from memory.
38
-
39
- Then confirm the environment as a plain guided step (not a gotcha): check Cloudflare auth with
40
- `wrangler whoami`; if the deployer is not logged in, have them run `wrangler login`, then continue.
41
-
42
- ## Source docs (read these first, then drive them)
43
-
44
- - **`docs/agentic-setup-github-core-walkthrough.md`**: THE authority for the GitHub path AND the full
45
- org-hardening checklist. Use it verbatim for every GitHub step and every hardening toggle. Do not
46
- source the hardening list from anywhere else.
47
- - **`docs/agentic-setup-stripe-core-walkthrough.md`**: the Stripe path (product, payment link,
48
- `metadata.product_id`, webhook, config, deploy, the test flows, gotchas) with the [USER]/[AGENT]/[SECRET]
49
- role split.
50
- - **`docs/setup-guide.md`**: general reference only. Do NOT use it as the source for the hardening
51
- checklist; the GitHub walkthrough owns that.
52
-
53
- Start by reading all three so you can drive accurately.
54
-
55
- ## Role split (who does what, never blur this)
56
-
57
- - **[USER]**: every click in GitHub, Stripe, and the Cloudflare dashboard, plus accepting GitHub
58
- invites. You cannot click these and you cannot mint a GitHub PAT. Give the deployer the exact menu
59
- path and the exact value, then wait and read back the non-secret result.
60
- - **[AGENT] (you)**: you edit `src/repoaccess.config.ts`, `src/index.ts` / `src/index.production.ts`,
61
- `wrangler.jsonc`, and the `.dev.vars` / `.dev.vars.production` **key names only**; you run
62
- `wrangler kv namespace create`, `wrangler deploy`, `wrangler tail`; you check `/health`; and you read
63
- the Workflow dashboard through the deployer.
64
- - **[SECRET]**: secret VALUES (the GitHub PAT `github_pat_…`, the Stripe signing secret `whsec_…`).
65
- **Never read, request, echo, or write a secret value.** You COPY the placeholder template
66
- (`.dev.vars.example` or `.dev.vars.production.example`) into the real secrets file; the **deployer**
67
- replaces each `__REPLACE_ME__` with their own value, pasting it themselves. You only ever reference a
68
- secret by its NAME. The real secrets file is HARD-WALLED from every tool you have (Read, Grep,
69
- Test-Path all return permission denied) - that is a deliberate secret-safety boundary, working as
70
- intended, not to be worked around. So you CANNOT check the file's contents and you must NOT ask the
71
- deployer to grep or run any shell on it. You rely on the deployer's plain [USER] confirmation that the
72
- placeholders are filled, and the real proof is the live grant test in Step 6.
73
-
74
- ## The one question to ask up front
75
-
76
- Ask exactly one mode question before anything else:
77
-
78
- > **Production or sandbox/test?**
79
-
80
- It drives three things for the whole run; lock it in and stay consistent:
81
-
82
- | | sandbox/test | production |
83
- | ---------------------- | ------------------------------------------ | ---------------------------------------------------------------------- |
84
- | entry / config profile | `src/index.ts` (`sandbox`) | `src/index.production.ts` (`production`) |
85
- | secrets template | `.dev.vars.example` | `.dev.vars.production.example` |
86
- | secrets file (real) | `.dev.vars` | `.dev.vars.production` |
87
- | deploy command | `wrangler deploy --secrets-file .dev.vars` | `wrangler deploy --env production --secrets-file .dev.vars.production` |
88
- | GitHub org | your sandbox/test org | your real selling org |
89
-
90
- Do NOT ask anything about GitHub being automatic vs manual; the GitHub side is always fully guided
91
- [USER] (you cannot click GitHub). Recommend sandbox/test for a first run (Stripe Test mode, test cards,
92
- no live activation needed).
93
-
94
- ## Step order (this is the validated order; follow it, do not run ahead)
95
-
96
- Work one block at a time. After each block, run the checkpoint and **do not advance until it passes**.
97
-
98
- ### 0. Prep the secrets file ([AGENT])
99
-
100
- COPY (do not move) the matching placeholder template to the real secrets file so it exists before the
101
- deployer pastes their first secret. Keep the `.example` file in place as reference.
102
-
103
- - sandbox/test: copy `.dev.vars.example` → `.dev.vars`
104
- - production: copy `.dev.vars.production.example` → `.dev.vars.production`
105
-
106
- Both real files are gitignored, so they never get committed. The deployer fills the `__REPLACE_ME__`
107
- placeholders themselves as you reach each secret (the GitHub PAT in step 1, the Stripe secret in step 4).
108
- Once you copy the template you cannot inspect the real file again: it is secret-walled from every tool
109
- you have, by design. From here on you rely on the deployer's [USER] confirmation that each secret is
110
- filled, never on reading or grepping the file.
111
-
112
- ### 1. GitHub side first (fully guided [USER])
113
-
114
- Drive `docs/agentic-setup-github-core-walkthrough.md` end to end, in its order:
115
-
116
- 1. Create a **team per product tier** (org → Teams → New team). Read back the team name(s).
117
- 2. **Attach the private repo(s)** to the team with `Read` (buyers clone, not push). Keep repos private.
118
- 3. Set org **Base permissions = `No permission`** (org → Settings → Member privileges). This is the
119
- isolation floor; without it, every buyer can see every private repo.
120
- 4. **Harden the org**: present and verify the WHOLE checklist from the walkthrough's "Harden the org"
121
- section, toggle by toggle: member repo/fork/Pages/Projects creation off, app access requests
122
- disabled, GitHub Apps off, admin repository permissions all off (especially Repository visibility
123
- change), team creation off; OAuth restricted; **fine-grained PATs allowed** (and optional admin
124
- approval + expiry on); and **do NOT require org-wide 2FA** (it ejects your buyers). Confirm each.
125
- 5. **Create the worker's fine-grained PAT** [USER]+[SECRET]: resource owner = the **org**, repository
126
- access = **None**, organization permissions = **Members: Read and write** (nothing else), an
127
- expiration date noted for rotation. Verify the resource owner and permission set with the deployer
128
- **before** they generate it. Then have them replace the `GITHUB_TOKEN=__REPLACE_ME__` placeholder in
129
- the real secrets file (from step 0) with the PAT, pasting it themselves. You never see it.
130
-
131
- **Checkpoint:** the team exists and carries the repo; Base permissions = No permission; the PAT's
132
- resource owner and permissions were confirmed before minting. Read back the two config values you wire
133
- next ONE AT A TIME, a separate prompt for each, never a combined list: first ask for the **org slug** and
134
- wait for the answer; THEN ask for the **team name(s)** (comma-separated if more than one). You will wire
135
- them into config next.
136
-
137
- ### 2. Worker config ([AGENT], you do this)
138
-
139
- Edit `src/repoaccess.config.ts` (the profile for the chosen mode, `sandbox` or `production`):
140
-
141
- - `githubOrg` = the org slug read back in step 1.
142
- - `productTeamMap.stripe[<product_id>]` = `{ teams: [<team(s)>], grant_mode: 'username' | 'claim' }`.
143
- The `<product_id>` comes from Stripe in step 4; until then, scaffold the shape and fill the id in once
144
- you have it. Keep `defaults` neutral (`teams: []`, `grant_mode: 'claim'`, `revoke_policy` `log_only`)
145
- so an unmapped product grants nothing.
146
- - The Stripe adapter is already composed in the entry (`const adapters = [stripe]`). Leave it.
147
-
148
- Provision KV and wire it. The namespace **title** follows an env-aware convention derived from the
149
- worker name (so it matches the rest of the platform and you can tell two deploys apart), while the
150
- **binding stays `ENTITLEMENTS`** (the code reads `env.ENTITLEMENTS` - never rename the binding):
151
-
152
- - sandbox/dev: `<worker-name>-ENTITLEMENTS`
153
- - production: `<worker-name>-production-ENTITLEMENTS`
154
-
155
- This is exactly what `wrangler` produces when the create picks up the worker `name` and `--env`. First
156
- check whether a namespace with the convention title for THIS worker+env already exists
157
- (`wrangler kv namespace list`); if one does, REUSE its id rather than creating a duplicate. Otherwise
158
- create it env-correctly:
159
-
160
- - sandbox/dev: `wrangler kv namespace create ENTITLEMENTS` → title `<worker-name>-ENTITLEMENTS`
161
- - production: `wrangler kv namespace create ENTITLEMENTS --env production` → title
162
- `<worker-name>-production-ENTITLEMENTS`
163
-
164
- **VERIFY the resulting title** from the create output (or `wrangler kv namespace list`) matches
165
- `<worker>-ENTITLEMENTS` (sandbox) or `<worker>-production-ENTITLEMENTS` (production). If it came out as a
166
- BARE `ENTITLEMENTS` with no worker prefix, the convention did not apply - recreate or rename it to the
167
- convention before wiring. Then paste the **real** id into `wrangler.jsonc` (`kv_namespaces` for the
168
- matching env), replacing the `PLACEHOLDER_…` value. A placeholder id will not bind.
169
-
170
- (The Workflow `name` is already env-aware and reused idempotently - `oss-access-workflow` /
171
- `oss-access-workflow-production`. Leave it as is; no rename needed.)
172
-
173
- **Checkpoint:** `npm run typecheck` is clean (run `npm run typegen` first if you changed any binding).
174
- The KV id is real (not a placeholder) and its title matches the `<worker>[-production]-ENTITLEMENTS`
175
- convention.
176
-
177
- ### 3. First deploy ([AGENT]), to learn the worker URL
178
-
179
- Deploy with the mode's secrets-file command from the table (sandbox
180
- `wrangler deploy --secrets-file .dev.vars`, production
181
- `wrangler deploy --env production --secrets-file .dev.vars.production`). Passing the secrets file lets
182
- Cloudflare's required-secret validation pass on this first deploy: `GITHUB_TOKEN` is already filled
183
- (step 1) and `STRIPE_WEBHOOK_SECRET` is still its `__REPLACE_ME__` placeholder, which counts as present
184
- (you replace it with the real value in step 4, then re-deploy in step 5). The first deploy creates the
185
- worker and prints its `https://<worker>.workers.dev` URL. Open `/health` and confirm `{"status":"ok"}`.
186
-
187
- **Checkpoint:** `/health` returns ok. Record the worker URL; Stripe's webhook endpoint needs it.
188
-
189
- ### 4. Stripe side (guided [USER], with your [AGENT] config edits)
190
-
191
- Drive `docs/agentic-setup-stripe-core-walkthrough.md`:
192
-
193
- 1. Create the **product** [USER] (**Product catalog -> Create product**, not the older "Products -> Add
194
- product"); read back its **product id** (`prod_…`) and wire it into the `productTeamMap` you
195
- scaffolded in step 2.
196
- 2. Create a **Payment Link** [USER]; for `username` mode add the **Text custom field labelled "GitHub
197
- username"** (the field key must contain "github"); for `claim` mode add no field.
198
- 3. Set **`metadata.product_id`** on the link's detail page [USER] (the step people miss: the
199
- `checkout.session.completed` webhook omits line items, so the worker reads the product from metadata).
200
- 4. Create the **webhook destination** [USER]+[SECRET]. In Stripe's current Event destinations flow you
201
- pick the EVENTS FIRST, then configure the destination (endpoint URL). Events: exactly
202
- `checkout.session.completed` + `charge.refunded` + `charge.dispute.created`. Payload style: **Snapshot
203
- if the option is shown** (the current flow may not surface it; if you do not see it, just continue).
204
- Then configure the destination: have the deployer GENERATE the path tail `<SECRET_PATH>` with
205
- `node -e "console.log(require('crypto').randomBytes(16).toString('hex'))"`; the SAME value goes in BOTH
206
- the worker route and the Stripe endpoint URL:
207
- `https://<your-worker>.workers.dev/wh/stripe/<SECRET_PATH>`. It is obscurity only (NOT a worker secret;
208
- the HMAC signature is the real gate, and the worker never reads the path), so it does not go in
209
- `secrets.required` - but offer to keep it as a commented `STRIPE_WEBHOOK_PATH` line in the secrets file
210
- (the `.example` has the slot) so the deployer does not lose it. The deployer reveals the signing secret
211
- (`whsec_…`) and replaces the `STRIPE_WEBHOOK_SECRET=__REPLACE_ME__` placeholder in the real secrets
212
- file with it, pasting it themselves.
213
-
214
- **Checkpoint:** product id wired into config; webhook endpoint URL matches the deployed worker URL plus
215
- the secret path. Then get ONE clean [USER] confirmation that the secrets are filled: ask the deployer to
216
- make sure both `GITHUB_TOKEN` (from step 1) and `STRIPE_WEBHOOK_SECRET` (from step 4) hold their real
217
- values so neither line still says `__REPLACE_ME__`, then reply "done". Say it plainly: "I can't read that
218
- file - it's secret-walled by design - so I'm trusting your confirmation." Do NOT ask them to grep or run
219
- any shell on the file, and do NOT claim to have verified it yourself. The real check is the live grant in
220
- Step 6 (a 401 there flags a missing or wrong `STRIPE_WEBHOOK_SECRET`).
221
-
222
- ### 5. Re-deploy with the secret ([AGENT])
223
-
224
- Deploy with the mode's secrets-file command so the now-filled values upload with the code:
225
-
226
- - sandbox/test: `wrangler deploy --secrets-file .dev.vars`
227
- - production: `wrangler deploy --env production --secrets-file .dev.vars.production`
228
-
229
- **Checkpoint:** deploy succeeded. Cloudflare validates the required secrets are PRESENT before a deploy
230
- succeeds (`GITHUB_TOKEN`, `STRIPE_WEBHOOK_SECRET`), but presence is not correctness: a still-placeholder
231
- or wrong value deploys just fine, and `wrangler secret list` shows only secret NAMES, never whether a
232
- value is real or the `__REPLACE_ME__` placeholder, so neither can catch a bad secret. The REAL check is
233
- the live grant in Step 6: if `STRIPE_WEBHOOK_SECRET` is missing or wrong the webhook returns 401 there,
234
- which flags it cleanly. That retry is free (test mode + test card), and a 401 creates no GitHub invite, so
235
- it never touches the new-org 50-invite/24h cap.
236
-
237
- ### 6. Test live (guided [USER] + you watch)
238
-
239
- Run `wrangler tail <worker-name>` (start it before paying) and walk the three flows from the Stripe
240
- walkthrough:
241
-
242
- 1. **Grant**: pay with test card `4242 4242 4242 4242`. username mode: a real handle → direct grant,
243
- `access.granted`. claim mode: `claim.pending`; the claim link is redacted from tail, so YOU fetch the
244
- token directly from KV - PREFERRED:
245
- `wrangler kv key get "claim_txn:stripe:<transaction_id>" --binding ENTITLEMENTS` returns the token (the
246
- `<transaction_id>` is the `pi_...` from the `claim.pending` event), then present the full clickable
247
- `https://<worker>.workers.dev/claim/<token>`. FALLBACK (only if the KV read is unavailable): the
248
- Workflow dashboard `emit:claim.pending` step output (`claim_url`). Do not ask the buyer to find or
249
- assemble it, and do not dig through the dashboard when KV has it. Open it, enter the handle - the grant
250
- then runs.
251
- Now confirm the grant and move STRAIGHT to the accept step - do not visibly stall. Tail batches a
252
- Workflow's logs until the instance settles, so `access.granted` can lag in the stream; do NOT narrate
253
- "I don't see access.granted yet, let me check KV" and do NOT sit waiting on tail. Confirm the grant
254
- directly from the KV grant record (`wrangler kv key get "grant:stripe:<transaction_id>" --binding
255
- ENTITLEMENTS` - a returned record means the grant succeeded), then immediately, as crisply as for a
256
- direct grant, state this explicit [USER] step yourself: "GitHub has emailed you the invitation - open
257
- it and accept it (or accept at `https://github.com/orgs/<org>/invitation`, substituting your real org).
258
- Tell me once you have accepted." `access.granted` means the worker CREATED the invitation, not that the
259
- buyer joined. WAIT for the deployer's confirmation, and only THEN verify Org → People membership.
260
- **Hard rule:** do NOT poll the Workflow or Org → People for membership before you have given the
261
- accept instruction - membership is gated on that human accept step, so checking first will hang (this
262
- is exactly what went wrong in testing). `access.granted` is the Workflow success signal; the buyer
263
- accepting the invite is a separate [USER] step that you must surface, never wait on silently.
264
- 2. **Refund / revoke**: refund the Step-1 test payment in Stripe → `access.revoked`, buyer removed and
265
- any pending invite cancelled. Do this BEFORE the typo test so revoke hits a single clean grant; the
266
- Step-1 handle is then reusable on the claim page, and one GitHub account is enough for the whole run.
267
- 3. **Typo path / claim fallback** (username mode): pay again with a valid-format-but-nonexistent handle;
268
- it falls back to `claim.pending` (a typo never strands a paying buyer). As in the claim-mode branch,
269
- YOU fetch the link from KV - PREFERRED:
270
- `wrangler kv key get "claim_txn:stripe:<transaction_id>" --binding ENTITLEMENTS` (the
271
- `<transaction_id>` is the `pi_...` from `claim.pending`) returns the token, then present the full
272
- clickable `https://<worker>.workers.dev/claim/<token>`; FALLBACK is the dashboard `emit:claim.pending`
273
- step output (`claim_url`). Then open it and enter a real handle (the Step-1 handle is free now that the
274
- refund revoked it). The claim-page grant creates a NEW invitation too, so do NOT end the flow at
275
- `access.granted`: confirm the grant from the KV grant record
276
- (`wrangler kv key get "grant:stripe:<transaction_id>" --binding ENTITLEMENTS`) rather than waiting on
277
- batched tail, then give the SAME accept-the-invite [USER] instruction as the grant flow - "open the
278
- GitHub email and accept it (or accept at `https://github.com/orgs/<org>/invitation`); tell me once you
279
- have accepted" - and wait for confirmation BEFORE you verify membership.
280
-
281
- The order is the same and unmissable in both flows: **pay/submit → `access.granted` (invite created) →
282
- [USER] accept the email invite → verify membership**. You state the accept step yourself the moment
283
- `access.granted` appears; you never wait on Org → People membership without first telling the deployer to
284
- accept. Once the deployer confirms acceptance, verify in GitHub (Org → People) that the buyer landed in
285
- the right team and was removed on revoke.
286
-
287
- **Checkpoint (the whole-chain verification):** the first live grant shows `access.granted` in the
288
- Workflow run and the buyer is in the right team. This single live grant verifies the entire chain
289
- (GitHub PAT, hardening, config, webhook signature, deploy).
290
-
291
- ## Gotchas (bake these in so you do not false-alarm the deployer)
292
-
293
- - **Benign tail noise:** "AccessWorkflow.run - Exception Thrown" / "Cancelled" in `wrangler tail` are
294
- how Cloudflare Workflows logs durable step suspension; they appear on fully successful runs. Judge a
295
- run by the Workflow dashboard **Status: Completed** plus the emitted `access.granted` / `access.revoked`
296
- events, never by raw tail outcomes.
297
- - **Tail batches Workflow logs, so `access.granted` can lag.** A grant is not slow just because the
298
- stream has not shown `access.granted` yet - tail flushes a Workflow's logs once the instance settles.
299
- Confirm a grant straight from the KV grant record
300
- (`wrangler kv key get "grant:stripe:<transaction_id>" --binding ENTITLEMENTS`, the `<transaction_id>`
301
- is the `pi_...`) rather than waiting on the stream. Likewise fetch a claim token from KV
302
- (`wrangler kv key get "claim_txn:stripe:<transaction_id>" --binding ENTITLEMENTS`) instead of the
303
- dashboard.
304
- - **`transaction_id` = `payment_intent`** (`pi_…`), stable across the order and its refund/dispute. It
305
- is what correlates a refund back to its grant; it is not the checkout session id.
306
- - **Stripe custom-field key must contain "github."** The label "GitHub username" auto-derives a key like
307
- `githubusername`. If a valid handle still routes to a claim page, the key did not match; use
308
- `metadata.github_username` instead.
309
- - **Real KV id + a per-account-unique Workflow name.** The KV namespace id in `wrangler.jsonc` must be
310
- the real one from `wrangler kv namespace create` (a placeholder will not bind). The Workflow `name` is
311
- account-global; if you run more than one RepoAccess worker on the account, each needs a distinct name
312
- or a later deploy reassigns the workflow and breaks the other binding. (Core already carries an
313
- `oss-` prefix for this reason.)
314
- - **New-org invite cap:** 50 invitations per 24h for the first month. Age the org before a big launch.
315
- - **Do not require org-wide 2FA**: it removes buyers without 2FA and blocks them from accepting invites.
316
- - **Owner PAT is immediate** even on approval-required orgs (a member's token would wait for approval).
317
-
318
- ## Final readiness checklist (all green before you declare done)
319
-
320
- - [ ] GitHub: team(s) created and carrying the private repo(s) at `Read`.
321
- - [ ] GitHub: org Base permissions = `No permission`.
322
- - [ ] GitHub: full org hardening applied; org allows fine-grained PATs; org-wide 2FA NOT required.
323
- - [ ] GitHub: worker PAT minted (org owner, repo access None, Members Read and write) and pasted as
324
- `GITHUB_TOKEN` by the deployer.
325
- - [ ] Worker: `githubOrg` + `productTeamMap` wired in the right config profile; real KV id in
326
- `wrangler.jsonc`; `npm run typecheck` clean.
327
- - [ ] Stripe: product + payment link + `metadata.product_id` + webhook (3 events; Snapshot if shown);
328
- `whsec_…` pasted as `STRIPE_WEBHOOK_SECRET` by the deployer.
329
- - [ ] Deploy succeeded; `/health` returns ok.
330
- - [ ] Live grant verified (`access.granted`, buyer in the right team); refund → revoke verified.
331
-
332
- When every box is green, the worker is live and proven. The deployer can now add more products by
333
- mapping each new Stripe product id to a team in `productTeamMap` and re-deploying.
334
-
335
- ---
336
-
337
- Advanced (no wizard): you can also consume core as a dependency. `npm install repoaccess-core` and
338
- compose `createWorker({ adapters: [stripe], config })` in your own Worker. That path is text-docs only;
339
- see `docs/setup-guide.md` and the two walkthroughs above. This wizard targets the clone-and-run path.