repoaccess-core 0.2.0
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/LICENSE +661 -0
- package/README.md +149 -0
- package/docs/agentic-setup-github-core-walkthrough.md +139 -0
- package/docs/agentic-setup-stripe-core-walkthrough.md +223 -0
- package/docs/setup-guide.md +422 -0
- package/docs/setup-wizard.md +339 -0
- package/docs/user-guide-stripe.md +215 -0
- package/package.json +35 -0
- package/src/adapters/stripe.ts +172 -0
- package/src/claim-guard.ts +59 -0
- package/src/claim-template.tsx +207 -0
- package/src/claim.tsx +242 -0
- package/src/config.ts +39 -0
- package/src/create-worker.ts +156 -0
- package/src/events.ts +195 -0
- package/src/fetch-entity.ts +79 -0
- package/src/github.ts +140 -0
- package/src/index.production.ts +19 -0
- package/src/index.ts +55 -0
- package/src/kv-keys.ts +22 -0
- package/src/raw-request.ts +24 -0
- package/src/repoaccess.config.ts +39 -0
- package/src/ssrf.ts +156 -0
- package/src/types.ts +238 -0
- package/src/username.ts +15 -0
- package/src/verify.ts +173 -0
- package/src/worker-env.d.ts +14 -0
- package/src/workflow-id.ts +77 -0
- package/src/workflow.ts +926 -0
package/README.md
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
# RepoAccess
|
|
2
|
+
|
|
3
|
+
**Sell access to a private GitHub repo on your own infrastructure, with the payment provider you
|
|
4
|
+
already use.**
|
|
5
|
+
|
|
6
|
+
A buyer pays, and they're automatically invited to the GitHub team that carries access to your private
|
|
7
|
+
repo. A refund or chargeback revokes it. It runs as a single **Cloudflare Worker** on the **free
|
|
8
|
+
tier**: no server, no SaaS subscription, no per-sale cut.
|
|
9
|
+
|
|
10
|
+
Use it to sell SaaS boilerplates, starter kits, courses, AI notebooks, private modules, or paid
|
|
11
|
+
community resources. Anything delivered as a repo.
|
|
12
|
+
|
|
13
|
+
`repoaccess-core` is the free, open-source (AGPL-3.0) core. It ships the **Stripe** adapter plus the
|
|
14
|
+
full grant and revoke engine. **RepoAccess Pro** (see below) adds more payment providers (including
|
|
15
|
+
Merchant-of-Record options) for sellers who can't use Stripe, premium claim-page templates, and support.
|
|
16
|
+
|
|
17
|
+
## Why this exists
|
|
18
|
+
|
|
19
|
+
Tools like Polar, Dodo, and GitHub Sponsors also solve "pay, then GitHub access", but they're **billing
|
|
20
|
+
platforms**: you adopt their checkout and they take a per-transaction cut. Two consequences:
|
|
21
|
+
|
|
22
|
+
- **They're Stripe-bound.** If you're somewhere Stripe isn't available (much of CIS, MENA, Africa,
|
|
23
|
+
Asia, and beyond), you're locked out.
|
|
24
|
+
- **They own the rail.** You can't bring the provider you already sell with, and you pay a % forever.
|
|
25
|
+
|
|
26
|
+
RepoAccess is the opposite: a light, single-purpose access-grant worker you self-host and wire to
|
|
27
|
+
**any** webhook-capable provider. Bring your own Stripe, Paddle, Lemon Squeezy, or Gumroad. Keep your
|
|
28
|
+
checkout, your margins, and your region.
|
|
29
|
+
|
|
30
|
+
## How it works
|
|
31
|
+
|
|
32
|
+
1. Your provider fires a webhook to your worker on a sale (and on refunds or chargebacks).
|
|
33
|
+
2. The worker **verifies** the signature, **normalizes** the event, and runs a durable **Workflow**.
|
|
34
|
+
3. The buyer lands in the GitHub team that carries your repo. By default they open a one-time **claim
|
|
35
|
+
link** and enter their GitHub username; if your checkout already collected the username, they're
|
|
36
|
+
added directly.
|
|
37
|
+
4. On a refund or chargeback (within a 180-day window), access is **revoked** and any pending invite is
|
|
38
|
+
cancelled.
|
|
39
|
+
|
|
40
|
+
No "Login with GitHub", no phoning home: GitHub's own invitation _is_ the identity check. Only the real
|
|
41
|
+
account owner can accept it.
|
|
42
|
+
|
|
43
|
+
## Guided setup (recommended)
|
|
44
|
+
|
|
45
|
+
The fastest way in is the built-in **setup wizard**. Clone the repo and run it in
|
|
46
|
+
[Claude Code](https://claude.com/claude-code):
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
git clone https://github.com/EdgeKits/repoaccess-core.git
|
|
50
|
+
cd repoaccess-core
|
|
51
|
+
npm install
|
|
52
|
+
claude # then type: /repoaccess-setup
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
`/repoaccess-setup` hand-walks you through the entire setup, one verified step at a time: your GitHub
|
|
56
|
+
org and team plus the privacy settings that keep your repos private, the Stripe product and webhook,
|
|
57
|
+
your secrets, deploy, and a live end-to-end test. It edits the config files for you and guides the
|
|
58
|
+
dashboard clicks (no digging through docs), and it never sees your secret values.
|
|
59
|
+
|
|
60
|
+
**Other agents (Codex, OpenCode, Cursor, ...):** the wizard is agent-agnostic. Claude Code and
|
|
61
|
+
[OpenCode](https://opencode.ai) both expose it as the `/repoaccess-setup` command; for any other coding
|
|
62
|
+
agent, open the cloned repo and ask your agent to follow `docs/setup-wizard.md` - the same shared
|
|
63
|
+
orchestrator the slash commands run.
|
|
64
|
+
|
|
65
|
+
Rather wire RepoAccess into an existing worker by hand? Use **Compose it yourself** below.
|
|
66
|
+
|
|
67
|
+
## Compose it yourself
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
npm install repoaccess-core
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Compose the adapters you use and pass a typed config:
|
|
74
|
+
|
|
75
|
+
```ts
|
|
76
|
+
// src/index.ts
|
|
77
|
+
import { createWorker, createAccessWorkflow, ClaimGuard } from 'repoaccess-core'
|
|
78
|
+
import { stripe } from 'repoaccess-core/adapters/stripe'
|
|
79
|
+
import { config } from './repoaccess.config'
|
|
80
|
+
|
|
81
|
+
// Pass the SAME adapter list to both factories.
|
|
82
|
+
const adapters = [stripe]
|
|
83
|
+
|
|
84
|
+
export default createWorker({ adapters, config })
|
|
85
|
+
export class AccessWorkflow extends createAccessWorkflow(config, adapters) {}
|
|
86
|
+
export { ClaimGuard }
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
> `adapters` is optional for hmac-only setups (Stripe), but pass it to both so adding an
|
|
90
|
+
> `api_callback` adapter later just works.
|
|
91
|
+
|
|
92
|
+
```ts
|
|
93
|
+
// src/repoaccess.config.ts
|
|
94
|
+
import type { RepoAccessConfig } from 'repoaccess-core'
|
|
95
|
+
|
|
96
|
+
export const config: RepoAccessConfig = {
|
|
97
|
+
githubOrg: 'your-org',
|
|
98
|
+
productTeamMap: {
|
|
99
|
+
stripe: { prod_ABC: { teams: ['pro'], grant_mode: 'username' } },
|
|
100
|
+
defaults: {
|
|
101
|
+
teams: [],
|
|
102
|
+
grant_mode: 'claim',
|
|
103
|
+
revoke_policy: { mode: 'log_only' },
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
}
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Add the Cloudflare bindings (Workflow, KV, Durable Object), put your secrets (`GITHUB_TOKEN`,
|
|
110
|
+
`STRIPE_WEBHOOK_SECRET`) in `.dev.vars`, and `wrangler deploy`. Full walkthrough:
|
|
111
|
+
[**setup guide**](./docs/setup-guide.md).
|
|
112
|
+
|
|
113
|
+
## What's in core
|
|
114
|
+
|
|
115
|
+
- **Stripe** adapter: HMAC-verified `checkout.session.completed`, `charge.refunded`,
|
|
116
|
+
`charge.dispute.created`.
|
|
117
|
+
- **Grant and revoke** engine: a durable Cloudflare Workflow, idempotent on retried webhooks, GitHub
|
|
118
|
+
rate-limit backoff, and reconciliation around manual changes.
|
|
119
|
+
- **Claim flow**: a one-time claim link plus a single-flight Durable Object so two submissions can't
|
|
120
|
+
over-grant. The claim page is a pluggable, seller-brandable template.
|
|
121
|
+
- **Config as code**: a typed config object, no escaped-JSON env vars.
|
|
122
|
+
- **Safe outbound events** (optional): signed `access.*` and `claim.*` webhooks with an SSRF allowlist.
|
|
123
|
+
- Stays on the **Workers free tier**.
|
|
124
|
+
|
|
125
|
+
## RepoAccess Pro
|
|
126
|
+
|
|
127
|
+
Core is everything you need to self-host with Stripe. **Pro** is for sellers who need more:
|
|
128
|
+
|
|
129
|
+
- **More payment providers**: **Paddle** and **Lemon Squeezy** (Merchant-of-Record, with tax handled
|
|
130
|
+
for you), **Gumroad**, **Razorpay** (India), and **Telegram Stars** (in-Telegram checkout). Sell from
|
|
131
|
+
the regions, and with the providers, a Stripe-only setup can't reach. More providers are on the way.
|
|
132
|
+
- **Premium claim-page templates**: polished, branded checkout-to-access pages.
|
|
133
|
+
- **Priority support** and quality-of-life extras.
|
|
134
|
+
- One-time license, your own infra, no per-sale cut.
|
|
135
|
+
|
|
136
|
+
**Pro is launching soon.** For early access and early-bird pricing, join the **Early Birds** list at
|
|
137
|
+
[edgekits.dev](https://edgekits.dev).
|
|
138
|
+
|
|
139
|
+
## License
|
|
140
|
+
|
|
141
|
+
AGPL-3.0-or-later. Copyright © 2026 Gary Stupak. See [LICENSE](LICENSE).
|
|
142
|
+
|
|
143
|
+
If you run a modified RepoAccess as a service for others, the AGPL's network-use terms apply. A
|
|
144
|
+
commercial license (without the copyleft obligations) is available if that doesn't fit:
|
|
145
|
+
[hello@edgekits.dev](mailto:hello@edgekits.dev).
|
|
146
|
+
|
|
147
|
+
---
|
|
148
|
+
|
|
149
|
+
RepoAccess is part of [EdgeKits](https://edgekits.dev).
|
|
@@ -0,0 +1,139 @@
|
|
|
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.
|
|
@@ -0,0 +1,223 @@
|
|
|
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.
|