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
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: RepoAccess Worker - Setup Guide (for sellers)
|
|
3
|
+
type: guide
|
|
4
|
+
audience: buyers / OSS users deploying their own worker
|
|
5
|
+
status: draft
|
|
6
|
+
created: 2026-06-14
|
|
7
|
+
note: >-
|
|
8
|
+
Draft of the customer-facing setup guide. Will become the OSS README / doc-site onboarding,
|
|
9
|
+
and is the seed corpus for the future support-agent (RAG). Generic - contains NO EdgeKits
|
|
10
|
+
secrets or account IDs. Re-verify each provider's dashboard steps at publish time.
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
# Deliver GitHub repo access on payment - setup guide
|
|
14
|
+
|
|
15
|
+
This guide gets your **self-hosted RepoAccess Worker** live: a buyer pays through your payment
|
|
16
|
+
provider, and they're automatically added to the right GitHub team (which carries access to your
|
|
17
|
+
private repo). Runs on the **Cloudflare Workers free tier** - no server, no SaaS fees.
|
|
18
|
+
|
|
19
|
+
**Target: ~10 minutes** once you have the accounts below.
|
|
20
|
+
|
|
21
|
+
## Prerequisites
|
|
22
|
+
|
|
23
|
+
- A **Cloudflare account** (free) + the `wrangler` CLI (`npm i -g wrangler`, then `wrangler login`).
|
|
24
|
+
- A **GitHub organization** that owns the private repo(s) you sell.
|
|
25
|
+
- An account with a **webhook-capable payment provider** (Stripe, Paddle, Gumroad, Lemon Squeezy, …).
|
|
26
|
+
- Node.js + npm.
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## Step 1 - Get the worker
|
|
31
|
+
|
|
32
|
+
Install the core (or, for the Pro adapters, use your Pro build):
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
npm install repoaccess-core
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Your entry composes the adapters you use and passes your typed config. It exports the three things the
|
|
39
|
+
Cloudflare runtime needs - the worker (`fetch`), the Workflow class, and the claim-guard Durable Object:
|
|
40
|
+
|
|
41
|
+
```ts
|
|
42
|
+
// src/index.ts
|
|
43
|
+
import { createWorker, createAccessWorkflow, ClaimGuard } from 'repoaccess-core'
|
|
44
|
+
import { stripe } from 'repoaccess-core/adapters/stripe'
|
|
45
|
+
import { config } from './repoaccess.config'
|
|
46
|
+
|
|
47
|
+
// Pass the SAME adapter list to both: createWorker (verifies + acks the webhook) and
|
|
48
|
+
// createAccessWorkflow (the Workflow that grants/revokes - and, for api_callback adapters, fetches
|
|
49
|
+
// the authoritative entity). Keep the two lists identical.
|
|
50
|
+
const adapters = [stripe]
|
|
51
|
+
|
|
52
|
+
export default createWorker({ adapters, config })
|
|
53
|
+
export class AccessWorkflow extends createAccessWorkflow(config, adapters) {}
|
|
54
|
+
export { ClaimGuard }
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Your settings live in a typed `repoaccess.config.ts` (Step 5) - no escaped-JSON env vars. Secrets stay
|
|
58
|
+
in the runtime env (Step 6).
|
|
59
|
+
|
|
60
|
+
## Step 2 - Cloudflare bindings
|
|
61
|
+
|
|
62
|
+
In `wrangler.jsonc`: add a **Workflows** binding (`AccessWorkflow`), a **KV namespace**
|
|
63
|
+
(`ENTITLEMENTS`), and a **Durable Object** binding (`CLAIM_GUARD`). There are **no `vars`** - your
|
|
64
|
+
non-secret config lives in `repoaccess.config.ts` (Step 5), not in `wrangler.jsonc`. Create the KV
|
|
65
|
+
namespace env-correctly:
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
# sandbox / dev -> title <worker-name>-ENTITLEMENTS
|
|
69
|
+
wrangler kv namespace create ENTITLEMENTS
|
|
70
|
+
# production -> title <worker-name>-production-ENTITLEMENTS
|
|
71
|
+
wrangler kv namespace create ENTITLEMENTS --env production
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
The namespace **title** follows an env-aware convention derived from the worker name -
|
|
75
|
+
`<worker-name>-ENTITLEMENTS` (sandbox) or `<worker-name>-production-ENTITLEMENTS` (production) - which is
|
|
76
|
+
what `wrangler` produces when the create picks up the worker `name` and `--env`. The **binding stays
|
|
77
|
+
`ENTITLEMENTS`** (the code reads `env.ENTITLEMENTS`; never rename the binding). Verify the created title
|
|
78
|
+
carries the worker prefix - if it came out as a bare `ENTITLEMENTS`, the convention did not apply;
|
|
79
|
+
recreate or rename it before wiring. Then paste the returned id into `wrangler.jsonc` for the matching
|
|
80
|
+
env. (Workflows are available on the free plan.)
|
|
81
|
+
|
|
82
|
+
> **The Workflow `name` must be unique per worker.** A Workflow `name` is account-global and belongs
|
|
83
|
+
> to exactly one worker. If you run more than one RepoAccess worker on the same Cloudflare account
|
|
84
|
+
> (e.g. a Pro worker alongside this one, or separate staging/production deploys), give each a
|
|
85
|
+
> **distinct** workflow `name` - otherwise the later deploy silently reassigns the Workflow and breaks
|
|
86
|
+
> the other worker's binding.
|
|
87
|
+
|
|
88
|
+
## Step 3 - GitHub
|
|
89
|
+
|
|
90
|
+
1. **Teams = product tiers.** In your org, create a team per tier (e.g. `pro`). Buyers get added here.
|
|
91
|
+
2. **Attach your private repo(s) to the team** (Team → Repositories → Add). The team carries repo
|
|
92
|
+
access - the worker never adds direct collaborators.
|
|
93
|
+
3. **⚠️ Set org Base permissions to `No permission`** (Org → Settings → Member privileges → Base
|
|
94
|
+
permissions). This is the setting that makes per-product isolation actually work. Base permissions are
|
|
95
|
+
the floor that **every** org member gets to **every** repo in the org. If it is `Read` (or higher) -
|
|
96
|
+
and on many orgs the default is `Read` - then any buyer of any product can see all your private repos,
|
|
97
|
+
and team scoping buys you nothing. With `No permission`, members get access **only** through their
|
|
98
|
+
team(s), so buying product A (team A → repo A) grants repo A alone and other products' repos stay
|
|
99
|
+
invisible. Keep the repos **private** (paid repos already are).
|
|
100
|
+
4. **Fine-grained PAT** (Settings → Developer settings → Fine-grained tokens):
|
|
101
|
+
- Resource owner = **your org** (enable fine-grained PATs in Org → Settings → Personal access tokens first).
|
|
102
|
+
- Repository access = **none** (the token only manages membership).
|
|
103
|
+
- Organization permissions → **Members: Read and write** (and nothing else).
|
|
104
|
+
- This token → secret `GITHUB_TOKEN` (Step 6). Your org slug goes in `config.githubOrg` (Step 5).
|
|
105
|
+
- ⚠️ New orgs cap invitations at **50/24h for the first month** - age the org before a big launch.
|
|
106
|
+
|
|
107
|
+
### Harden the org (members = paying customers)
|
|
108
|
+
|
|
109
|
+
In this org, **"members" are buyers, not teammates** - treat them as untrusted and disable every member
|
|
110
|
+
privilege. Access comes **only** through team membership. Org **Owners keep full access regardless** -
|
|
111
|
+
these toggles restrict members, never you.
|
|
112
|
+
|
|
113
|
+
**Org → Settings → Member privileges** (each block has its own Save):
|
|
114
|
+
|
|
115
|
+
- **Base permissions → `No permission`** - the critical one (above).
|
|
116
|
+
- **Repository creation** → uncheck **Public and Private** (members don't create repos).
|
|
117
|
+
- **Repository forking** → off (no forking of private repos into member accounts).
|
|
118
|
+
- **Projects base permissions** → `No access`.
|
|
119
|
+
- **Pages creation** → uncheck **Public and Private**.
|
|
120
|
+
- **App access requests** → `Disable app access requests`.
|
|
121
|
+
- **GitHub Apps** ("Allow repository admins to install…") → off.
|
|
122
|
+
- **Admin repository permissions** → off for all: **Repository visibility change** (else a member-admin
|
|
123
|
+
could flip a private paid repo to **public**), **Repository deletion and transfer**, **Issue deletion**,
|
|
124
|
+
**Branch renames**.
|
|
125
|
+
- **Member team permissions → Team creation** → off.
|
|
126
|
+
|
|
127
|
+
**Org → Settings → Authentication security:**
|
|
128
|
+
|
|
129
|
+
- **Do NOT** "Require two-factor authentication for everyone" - it **removes** members without 2FA (your
|
|
130
|
+
buyers) and blocks them from accepting invites. Same goes for an IP allow list. Enable 2FA on your own
|
|
131
|
+
**owner** account instead.
|
|
132
|
+
|
|
133
|
+
**Org → Settings → Third-party Access:**
|
|
134
|
+
|
|
135
|
+
- **OAuth app policy** → keep **Access restricted** (approved apps only).
|
|
136
|
+
- **Personal access tokens → Fine-grained tokens** → **Allow access via fine-grained PATs** (the worker's
|
|
137
|
+
`GITHUB_TOKEN` needs this; "Restrict" breaks grants). Optionally **Require administrator approval** for
|
|
138
|
+
members' tokens.
|
|
139
|
+
- **Token expiry** → leave **"Fine-grained PATs must expire"** on (good hygiene) - which means the worker
|
|
140
|
+
token expires too; see rotation below.
|
|
141
|
+
|
|
142
|
+
**Token rotation (operational).** The worker's `GITHUB_TOKEN` is a fine-grained PAT and **will expire**.
|
|
143
|
+
Before it does: issue a new token with the same scope → update the `GITHUB_TOKEN` secret
|
|
144
|
+
(`wrangler secret`) → re-approve it if approval is required. GitHub emails a warning before expiry; set a
|
|
145
|
+
calendar reminder too. If it lapses, grants/revokes stop until you rotate.
|
|
146
|
+
|
|
147
|
+
**Optional - Discussions as a feedback channel.** Enabling "Allow users with read access to create
|
|
148
|
+
discussions" gives buyers a built-in Q&A/feedback space. Note: discussions in a **private** repo are
|
|
149
|
+
visible to **everyone with access to that repo** (your other buyers) - a shared space, not private 1:1
|
|
150
|
+
support.
|
|
151
|
+
|
|
152
|
+
## Step 4 - Your payment provider
|
|
153
|
+
|
|
154
|
+
The pattern is the same for every provider:
|
|
155
|
+
|
|
156
|
+
1. **Create your product + price** in the provider's dashboard. Note the **product id**.
|
|
157
|
+
2. **Create a webhook** pointing at your worker:
|
|
158
|
+
`https://<your-worker-url>/wh/<adapter>/<SECRET_PATH>`
|
|
159
|
+
- `<adapter>` = `stripe` / `paddle` / `gumroad` / … ; `<SECRET_PATH>` = a random string you choose
|
|
160
|
+
(`node -e "console.log(require('crypto').randomBytes(16).toString('hex'))"`) - a hard-to-guess URL
|
|
161
|
+
segment for obscurity. It lives **only in this
|
|
162
|
+
webhook URL** (and your own notes) - it is **not** a worker secret: signature verification is the
|
|
163
|
+
real gate, so the worker doesn't validate the path. (Signature-less adapters like Gumroad are the
|
|
164
|
+
exception - there the path is a checked credential; see the per-adapter notes.)
|
|
165
|
+
- Subscribe only to the events your adapter needs (purchase + refund/chargeback). See the
|
|
166
|
+
per-adapter notes.
|
|
167
|
+
3. **Copy the webhook signing secret** → `<ADAPTER>_WEBHOOK_SECRET`.
|
|
168
|
+
|
|
169
|
+
Per-provider specifics (events, signature scheme, gotchas) are in the per-adapter reference. Quick
|
|
170
|
+
examples:
|
|
171
|
+
|
|
172
|
+
- **Stripe:** events `checkout.session.completed`, `charge.refunded`, `charge.dispute.created`;
|
|
173
|
+
signing secret `whsec_…`. (You can also test locally with the Stripe CLI.) Set
|
|
174
|
+
**`metadata.product_id`** (and, for username mode, **`metadata.github_username`**) on your Checkout
|
|
175
|
+
Session / Payment Link - Stripe's checkout webhook omits line items, so the worker reads the
|
|
176
|
+
product from metadata; if it's missing, the buyer falls through to your `defaults` mapping.
|
|
177
|
+
_No-code Payment Link:_ the link _builder_ has no metadata field, but the **created link's detail
|
|
178
|
+
page has an editable Metadata section** - add `product_id` there (Stripe copies a Payment Link's
|
|
179
|
+
metadata onto every Checkout Session it creates). One-time per link.
|
|
180
|
+
- **Paddle:** events `transaction.completed`, `adjustment.created`, `adjustment.updated`; one-time
|
|
181
|
+
prices only (no subscription events); destination secret from the notification destination. **No
|
|
182
|
+
native buyer-input checkout field** - pass `github_username` as seller `customData` or use `claim`
|
|
183
|
+
mode (see Step 5).
|
|
184
|
+
- **Lemon Squeezy:** events `order_created`, `order_refunded`; `X-Signature` HMAC-SHA256 over the raw
|
|
185
|
+
body (no timestamp); signing secret is one **you choose** when creating the webhook (6–40 chars).
|
|
186
|
+
**No native buyer-input checkout field** - pass `github_username` as seller `custom_data`
|
|
187
|
+
(`checkout[custom][github_username]=…`) or use `claim` mode (see Step 5). Chargebacks: LS's
|
|
188
|
+
`dispute_*` events are undocumented and not yet handled, so `auto_revoke` covers refunds only -
|
|
189
|
+
revoke a chargeback manually for now.
|
|
190
|
+
- **Gumroad:** **no webhook signature at all** (ignore any `x-gumroad-signature` - it doesn't exist).
|
|
191
|
+
Here the **secret path segment IS a validated credential**, so it's a real secret
|
|
192
|
+
(`GUMROAD_WEBHOOK_PATH`), not just obscurity. Register `sale` / `refund` / `dispute` via
|
|
193
|
+
`PUT /v2/resource_subscriptions` pointed at `…/wh/gumroad/<path>`. The worker confirms every event by
|
|
194
|
+
calling `GET /v2/sales/{id}` with your **`GUMROAD_ACCESS_TOKEN`** - so that token is required too.
|
|
195
|
+
`github_username` via a Gumroad **custom field** the buyer fills at checkout (Gumroad has a native
|
|
196
|
+
buyer-input field), or use `claim` mode.
|
|
197
|
+
|
|
198
|
+
## Step 5 - Configure (`repoaccess.config.ts`)
|
|
199
|
+
|
|
200
|
+
All non-secret config is a **typed object** you author - full editor autocomplete, real comments, no
|
|
201
|
+
escaped-JSON env vars:
|
|
202
|
+
|
|
203
|
+
```ts
|
|
204
|
+
// src/repoaccess.config.ts
|
|
205
|
+
import type { RepoAccessConfig } from 'repoaccess-core'
|
|
206
|
+
|
|
207
|
+
export const config: RepoAccessConfig = {
|
|
208
|
+
githubOrg: 'your-org',
|
|
209
|
+
productTeamMap: {
|
|
210
|
+
// adapter → product_id → mapping
|
|
211
|
+
stripe: { prod_ABC: { teams: ['pro'], grant_mode: 'username' } },
|
|
212
|
+
// reserved fallback for any unmapped product (keep it neutral - see the warning)
|
|
213
|
+
defaults: {
|
|
214
|
+
teams: [],
|
|
215
|
+
grant_mode: 'claim',
|
|
216
|
+
revoke_policy: { mode: 'log_only' },
|
|
217
|
+
},
|
|
218
|
+
},
|
|
219
|
+
branding: { name: 'Acme', logoUrl: '', faviconUrl: '' }, // optional - claim-page look
|
|
220
|
+
// eventWebhook: { url: "https://you.example/events", allowlist: ["you.example"] }, // optional
|
|
221
|
+
}
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
- **`productTeamMap`** is keyed **adapter → product_id → mapping**. The adapter key (`stripe`,
|
|
225
|
+
`paddle`, …) is the adapter's `name` and matches its webhook route `/wh/<adapter>/…` - there's no
|
|
226
|
+
separate "which provider" switch.
|
|
227
|
+
- **`grant_mode`:** `username` (use the buyer's GitHub username if your checkout collected it, else
|
|
228
|
+
falls back to `claim`) or `claim` (buyer enters their username on a claim page after paying).
|
|
229
|
+
- **`revoke_policy`:** `{ mode: "auto_revoke" }` (remove access on refund/chargeback) or
|
|
230
|
+
`{ mode: "log_only" }`. **Auto-revoke horizon:** the worker keeps each grant record for **180 days**,
|
|
231
|
+
so `auto_revoke` covers refunds and card chargebacks within that window (a typical refund window plus
|
|
232
|
+
the ~120-day chargeback window). A dispute after 180 days won't auto-revoke - handle it manually.
|
|
233
|
+
|
|
234
|
+
> **Chargebacks & disputes.** A chargeback (the buyer's bank reverses the charge, bypassing you) is
|
|
235
|
+
> handled separately from a refund. Under `auto_revoke` the worker revokes access **when the dispute
|
|
236
|
+
> is raised** - each adapter maps its provider's dispute-opened event to this (Stripe
|
|
237
|
+
> `charge.dispute.created`, Paddle a `chargeback` adjustment, Razorpay `payment.dispute.created`),
|
|
238
|
+
> while pre-dispute _early-warning_ signals are deliberately ignored. The worker **never auto-restores**
|
|
239
|
+
> access if you later win the dispute - re-grant manually (Team → Members → **Add a member**, or
|
|
240
|
+
> re-issue the claim link); it reconciles around manual changes. Per-provider event names and nuances
|
|
241
|
+
> live in each provider's reference.
|
|
242
|
+
|
|
243
|
+
> **Keep `defaults` empty / `log_only` unless you mean it.** Any product that isn't in the map - and
|
|
244
|
+
> any stray webhook from an adapter you composed - falls through to `defaults`. Empty `defaults.teams`
|
|
245
|
+
> is a safe no-op; a real team there becomes a **catch-all** that grants on anything. Only set a
|
|
246
|
+
> non-empty `defaults` if you genuinely want a catch-all tier.
|
|
247
|
+
|
|
248
|
+
### Sandbox vs production (optional)
|
|
249
|
+
|
|
250
|
+
`env` isn't available at module load in Workers, so pick the profile **at build time**, not from a
|
|
251
|
+
runtime var. Two clean options:
|
|
252
|
+
|
|
253
|
+
- **Single env (simplest):** export one `config` and point both `createWorker` / `createAccessWorkflow`
|
|
254
|
+
at it (as in Step 1). Most deployers need only this.
|
|
255
|
+
- **Sandbox/prod split:** export two profiles from `repoaccess.config.ts` (e.g. `sandbox` and
|
|
256
|
+
`production` sharing a base), add a second tiny entry `src/index.production.ts` that imports
|
|
257
|
+
`production`, and set `[env.production].main = "src/index.production.ts"` in `wrangler.jsonc`.
|
|
258
|
+
`wrangler deploy` then uses the sandbox profile; `wrangler deploy --env production` uses the prod one.
|
|
259
|
+
|
|
260
|
+
### Collecting the buyer's GitHub username
|
|
261
|
+
|
|
262
|
+
The buyer always supplies their own username - you never type it per-buyer. Two ways, matching the
|
|
263
|
+
two `grant_mode`s:
|
|
264
|
+
|
|
265
|
+
- **At checkout (`username` mode).** Add a **custom field** labelled e.g. "GitHub username" to your
|
|
266
|
+
checkout, so the buyer fills it in while paying. \*\*A native buyer-input field is provider-dependent
|
|
267
|
+
- check yours:\*\*
|
|
268
|
+
- _Stripe Payment Link / Checkout:_ Dashboard → your Payment Link → **Custom fields** → add a text
|
|
269
|
+
field (or set `custom_fields` when creating the Checkout Session). The worker reads it from the
|
|
270
|
+
`checkout.session.completed` event.
|
|
271
|
+
- _Gumroad:_ add a **custom field** to the product (Gumroad has a native buyer-input field) named
|
|
272
|
+
`github_username`; the worker reads it from the fetched sale entity.
|
|
273
|
+
- _Providers with no buyer-input field (e.g. Paddle, Lemon Squeezy):_ some hosted checkouts have
|
|
274
|
+
**no field the buyer can type into** - their "custom data" is **seller-passed**, meant for values
|
|
275
|
+
you already know before checkout, not buyer input. Paddle: `customData` via Paddle.js /
|
|
276
|
+
`data-custom-data` / the API. Lemon Squeezy: a `checkout[custom][github_username]=…` URL parameter
|
|
277
|
+
or the API. On these, either use **`claim` mode** (the natural fit), or collect the username on
|
|
278
|
+
**your own page before checkout** and pass it through as that seller-passed custom data.
|
|
279
|
+
- _Your own checkout page (any provider):_ collect the username yourself and pass it through as the
|
|
280
|
+
provider's custom-data/metadata field (Stripe `metadata.github_username`, Paddle `customData`,
|
|
281
|
+
LS `checkout[custom][github_username]`).
|
|
282
|
+
- **After checkout (`claim` mode).** Collect nothing at checkout. The buyer receives a one-time claim
|
|
283
|
+
link after paying and enters their username on the claim page. Use this if you can't (or don't want
|
|
284
|
+
to) add a checkout field. `username` mode automatically falls back to `claim` when no username is
|
|
285
|
+
present.
|
|
286
|
+
|
|
287
|
+
**What if the buyer mistypes it?** The worker never grants access directly - GitHub sends an
|
|
288
|
+
**invitation that only the real owner of that account can accept**. A wrong or non-existent handle
|
|
289
|
+
just means the invite is never accepted (the buyer contacts you to fix it); it can't silently let the
|
|
290
|
+
wrong person in. There's deliberately **no "Login with GitHub"** step - the invite-acceptance is the
|
|
291
|
+
ownership check, and the worker never phones home.
|
|
292
|
+
|
|
293
|
+
## Step 6 - Secrets
|
|
294
|
+
|
|
295
|
+
**Only secrets** go in the runtime env - everything non-secret is in `repoaccess.config.ts` (Step 5).
|
|
296
|
+
Local dev → `.dev.vars`; production → `wrangler secret put <NAME>` (add `--env production` for a
|
|
297
|
+
separate prod environment):
|
|
298
|
+
|
|
299
|
+
```
|
|
300
|
+
GITHUB_TOKEN # the fine-grained PAT (Step 3)
|
|
301
|
+
<ADAPTER>_WEBHOOK_SECRET # the provider's signing secret (Step 4) - one per adapter
|
|
302
|
+
EVENT_WEBHOOK_SECRET # optional - signs outbound events (32-byte hex; same node one-liner with 32)
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
Not secrets, so **not** here (they're in `repoaccess.config.ts`): `githubOrg`, the product map,
|
|
306
|
+
branding, and `eventWebhook` (url + allowlist). And `<ADAPTER>_WEBHOOK_PATH` isn't a secret either -
|
|
307
|
+
your path segment lives in the webhook URL (Step 4), not the env (HMAC adapters don't validate it).
|
|
308
|
+
Never commit `.dev.vars` (git-ignored by default).
|
|
309
|
+
|
|
310
|
+
## Step 7 - Deploy
|
|
311
|
+
|
|
312
|
+
```bash
|
|
313
|
+
wrangler deploy --secrets-file .dev.vars
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
(`secrets.required` in `wrangler.jsonc` gates the deploy, so upload the secrets inline the first time.
|
|
317
|
+
For a separate prod env: `wrangler deploy --env production --secrets-file .dev.vars.production`.)
|
|
318
|
+
|
|
319
|
+
Take the resulting `https://<worker>.workers.dev` URL and make sure your provider's webhook points at
|
|
320
|
+
`…/wh/<adapter>/<SECRET_PATH>`. (Optional: put it on a custom domain via `wrangler` routes.)
|
|
321
|
+
|
|
322
|
+
## Step 8 - Test it
|
|
323
|
+
|
|
324
|
+
Do a **sandbox/test purchase** in your provider. You should see:
|
|
325
|
+
|
|
326
|
+
- the buyer added to the right team (username mode), **or** a `claim.pending` event with a claim
|
|
327
|
+
link (claim mode) → buyer opens it, enters their GitHub username, gets added;
|
|
328
|
+
- a refund/chargeback (if `auto_revoke`) removes them and cancels any pending invite.
|
|
329
|
+
|
|
330
|
+
`wrangler tail` shows structured logs of exactly what the worker did and why.
|
|
331
|
+
|
|
332
|
+
## Troubleshooting
|
|
333
|
+
|
|
334
|
+
- **Signature verification fails:** the worker must see the raw request body byte-for-byte. If you
|
|
335
|
+
proxy/transform requests, don't re-serialize the body before it reaches the worker.
|
|
336
|
+
- **Buyer not added:** check `config.productTeamMap` has the exact provider `product_id`; check the
|
|
337
|
+
PAT has `Members: Read & write` on the org; check the team is attached to the repo.
|
|
338
|
+
- **`429` from GitHub:** invitation rate limit - the worker backs off and retries automatically;
|
|
339
|
+
if you're launching, age the org first.
|
|
340
|
+
- **Webhook never arrives:** verify the URL + secret path; re-send from the provider's dashboard
|
|
341
|
+
(the endpoint is idempotent - replays are safe).
|
|
342
|
+
- **`wrangler tail` shows `Exception Thrown` / `Canceled` on a run that actually succeeded:** expected
|
|
343
|
+
Cloudflare Workflows behavior, not a failure. The durable engine surfaces step
|
|
344
|
+
suspension/checkpointing and instance lifecycle as `exception` / `Canceled` _outcomes_ in tail (and
|
|
345
|
+
batches a step's logs until the instance finishes), even when everything worked. Judge success by the
|
|
346
|
+
**instance Status** in the Workflows dashboard (`Completed` vs `Errored`) and by the **emitted
|
|
347
|
+
events** (`access.granted` / `access.revoked` / `claim.pending`; a real failure emits `access.failed`
|
|
348
|
+
and ends `Errored`) - not by raw tail exception lines.
|
|
349
|
+
|
|
350
|
+
## A buyer says they didn't get access
|
|
351
|
+
|
|
352
|
+
Most "I paid but I'm not in" tickets are an **unaccepted invite** or a **mistyped username** - both
|
|
353
|
+
are quick to resolve. Work down this list.
|
|
354
|
+
|
|
355
|
+
**First, tell the buyer to check for the invite.** Adding someone to a team sends a GitHub
|
|
356
|
+
**invitation** they must accept - access isn't active until they do.
|
|
357
|
+
|
|
358
|
+
1. Check email (incl. spam) for a GitHub invite to your org, **and** the dashboard:
|
|
359
|
+
`https://github.com/orgs/<your-org>` shows a pending-invitation banner; the org invite link is
|
|
360
|
+
`https://github.com/orgs/<your-org>/invitation`.
|
|
361
|
+
2. Accept it → they land in the team that carries the repo. Done in the common case.
|
|
362
|
+
|
|
363
|
+
**If there's no invite, diagnose on your side:**
|
|
364
|
+
|
|
365
|
+
1. **Read the event / logs.** Your outbound events tell you exactly what happened:
|
|
366
|
+
`access.granted` (it worked - it's an unaccepted invite, see above), `access.failed` with a
|
|
367
|
+
`reason` (e.g. user not found, rate-limited), or `claim.pending` (claim mode - they never opened
|
|
368
|
+
the link). No event store? Run `wrangler tail` and replay the provider's webhook.
|
|
369
|
+
2. **`access.failed → user not found`** = mistyped/non-existent username. Cancel any stale pending
|
|
370
|
+
invite (Org → People → **Pending invitations**), then re-collect: re-send the claim link, or just
|
|
371
|
+
invite them by hand (Team → Members → **Add a member**).
|
|
372
|
+
3. **`access.failed → rate-limited` (`429`)** = new-org invite cap (50/24h in the first month). The
|
|
373
|
+
worker backs off and retries automatically - it'll land within the window. For a launch, age the
|
|
374
|
+
org first.
|
|
375
|
+
4. **`claim.pending`, never completed** = the buyer didn't finish the claim page. Claim links are
|
|
376
|
+
**single-use and expire after 30 days** - re-issue a fresh link if theirs lapsed.
|
|
377
|
+
5. **No event at all** = the webhook didn't arrive or didn't verify. Check the provider sent it
|
|
378
|
+
(re-send from its dashboard), the URL + secret path are right, and signature verification passed
|
|
379
|
+
(a transformed/re-serialized body breaks the HMAC - see above).
|
|
380
|
+
|
|
381
|
+
**Fastest manual override (any case):** as an org owner, add them directly - Team → Members → **Add a
|
|
382
|
+
member** → their GitHub username. The worker reconciles around manual changes, so this won't conflict
|
|
383
|
+
with it.
|
|
384
|
+
|
|
385
|
+
> **Why there's no "Login with GitHub":** the worker never logs the buyer in or phones home. The
|
|
386
|
+
> GitHub invitation _is_ the identity check - only the real owner of an account can accept it - which
|
|
387
|
+
> is why a wrong username is harmless (the invite simply sits unaccepted) and why fixing one is just
|
|
388
|
+
> re-issuing the invite to the correct handle.
|
|
389
|
+
|
|
390
|
+
---
|
|
391
|
+
|
|
392
|
+
> _This is a draft. Final wording lives in the OSS README / doc-site; it also seeds the support
|
|
393
|
+
> agent's knowledge base._
|
|
394
|
+
|
|
395
|
+
<!--
|
|
396
|
+
SUPPORT CATALOG - complete the exhaustive version at M2 (doc-site / README pass), grounded in
|
|
397
|
+
as-built behavior. Accrue failure modes phase-by-phase as the build reports them; do NOT write the
|
|
398
|
+
full catalog speculatively now. Categories to cover, each as symptom → cause → fix (seller + buyer
|
|
399
|
+
wording):
|
|
400
|
+
|
|
401
|
+
- Verification: signature fails (raw-body transformed, wrong secret, expired ts, clock skew);
|
|
402
|
+
per-provider quirks (Stripe v1 rotation, Paddle destination secret, form-urlencoded providers).
|
|
403
|
+
- Mapping: product_id not in PRODUCT_TEAM_MAP → silent fall-through to defaults; price-id vs
|
|
404
|
+
product-id confusion; Stripe metadata.product_id unset.
|
|
405
|
+
- Username: malformed handle; well-formed but non-existent (404 → access.failed); wrong-but-real
|
|
406
|
+
handle (invite unaccepted); already-an-org-member edge case.
|
|
407
|
+
- Invitations: invite not accepted; invite expired; pending-invite cleanup; org membership vs team
|
|
408
|
+
membership.
|
|
409
|
+
- Claim flow: link expired (30d) / already used / lost; claim.pending never completed; re-issue.
|
|
410
|
+
- GitHub auth/permissions: PAT expired; fine-grained PAT not enabled at org; missing Members:R&W;
|
|
411
|
+
wrong GITHUB_ORG slug; team not attached to repo.
|
|
412
|
+
- Rate limits: 50/24h new-org cap, 500/24h, secondary limits, 422 - durable backoff behavior.
|
|
413
|
+
- Revoke: log_only vs auto_revoke; partial refund does NOT revoke; chargeback always revokes;
|
|
414
|
+
grant record missing → reconciliation fallback.
|
|
415
|
+
- Webhooks/infra: wrong secret path; duplicate delivery (idempotent); KV/Workflows binding
|
|
416
|
+
misconfig; secrets not set in prod (wrangler secret put); custom domain/routes.
|
|
417
|
+
- Outbound events: EVENT_WEBHOOK_URL unreachable/SSRF-blocked; signature on outbound; delivery
|
|
418
|
+
failure never blocks the grant.
|
|
419
|
+
|
|
420
|
+
Cross-link each entry to the relevant docs/providers/*.md and PRD section. This block also seeds the
|
|
421
|
+
support-agent RAG corpus.
|
|
422
|
+
-->
|