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,339 @@
|
|
|
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.
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
# RepoAccess - Stripe setup and test guide (Core)
|
|
2
|
+
|
|
3
|
+
This is the **manual path**: set up and live-test a self-hosted RepoAccess **Core** worker with
|
|
4
|
+
**Stripe**, by hand, end to end. It covers the same ground as the `/repoaccess-setup` wizard, for people
|
|
5
|
+
who would rather do it themselves (or just want to understand each step).
|
|
6
|
+
|
|
7
|
+
What you are building: a buyer pays through your Stripe checkout, and the worker automatically adds them
|
|
8
|
+
to the GitHub team that carries your private repo. A refund or chargeback revokes that access. It runs
|
|
9
|
+
as a single Cloudflare Worker on the free tier - no server, no SaaS subscription, no per-sale cut.
|
|
10
|
+
|
|
11
|
+
Do a first run in **Stripe Test mode** with test cards. No live activation is needed to prove it works.
|
|
12
|
+
|
|
13
|
+
## Before you start
|
|
14
|
+
|
|
15
|
+
- A **Cloudflare account** (free) and the `wrangler` CLI: `npm i -g wrangler`, then `wrangler login`.
|
|
16
|
+
- A **GitHub organization** you own. Personal accounts have no teams, so an org is required (a Free org
|
|
17
|
+
is fine). The private repo(s) you sell live in this org.
|
|
18
|
+
- **Node** and **git**. Clone the repo and install dependencies:
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
git clone https://github.com/EdgeKits/repoaccess-core.git
|
|
22
|
+
cd repoaccess-core
|
|
23
|
+
npm install
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
`npm install` matters: the worker bundles a runtime dependency, so a deploy fails without it.
|
|
27
|
+
|
|
28
|
+
The order below minimizes back-and-forth: GitHub first, then the worker config and a first deploy (to
|
|
29
|
+
learn your worker URL), then Stripe (which needs that URL), then a re-deploy with the secret, then the
|
|
30
|
+
live test.
|
|
31
|
+
|
|
32
|
+
## 1. GitHub: org, team, and the worker token
|
|
33
|
+
|
|
34
|
+
1. **Create a team per product tier** (Org, then Teams, then New team), named after the tier (e.g.
|
|
35
|
+
`pro`). The worker adds buyers to this team; the team carries the repo access.
|
|
36
|
+
2. **Attach the private repo(s)** to the team (Team, then Repositories, then Add repository) with
|
|
37
|
+
**Read** (buyers clone, they do not push). Keep the repos private.
|
|
38
|
+
3. **Set org Base permissions to `No permission`** (Org, then Settings, then Member privileges). This is
|
|
39
|
+
the isolation floor: without it, every member could see every private repo in the org, and team
|
|
40
|
+
scoping buys you nothing. With it, members get access only through their team(s).
|
|
41
|
+
4. **Harden the org** - treat members as paying customers, not teammates (Org, then Settings, then
|
|
42
|
+
Member privileges; each block has its own Save):
|
|
43
|
+
- Repository creation: uncheck Public and Private. Repository forking: off. Projects base
|
|
44
|
+
permissions: No access. Pages creation: off. App access requests: disabled. GitHub Apps: off.
|
|
45
|
+
- Admin repository permissions: off for all, especially **Repository visibility change** (so a
|
|
46
|
+
member-admin cannot flip a paid private repo to public). Team creation: off.
|
|
47
|
+
- Authentication: do **NOT** require org-wide 2FA - it ejects buyers without 2FA and blocks them from
|
|
48
|
+
accepting invites. Enable 2FA on your own owner account instead.
|
|
49
|
+
- Third-party Access: keep OAuth apps restricted; **allow access via fine-grained PATs** (the worker
|
|
50
|
+
token needs this).
|
|
51
|
+
5. **Create the worker's fine-grained PAT** (your account, then Settings, then Developer settings, then
|
|
52
|
+
Fine-grained tokens, then Generate new token):
|
|
53
|
+
- **Resource owner:** your **org** (not your personal account).
|
|
54
|
+
- **Repository access:** **None** (the token only manages membership; it never touches repos).
|
|
55
|
+
- **Organization permissions:** **Members: Read and write** (and nothing else).
|
|
56
|
+
- Pick an **expiration** and note it for rotation.
|
|
57
|
+
- Generate, then copy the token (`github_pat_...`). You will paste it into `.dev.vars` in Step 3. An
|
|
58
|
+
owner-created token is ready immediately even if the org requires token approval.
|
|
59
|
+
|
|
60
|
+
## 2. Configure the worker
|
|
61
|
+
|
|
62
|
+
Edit `src/repoaccess.config.ts` (the `sandbox` profile for a test run):
|
|
63
|
+
|
|
64
|
+
```ts
|
|
65
|
+
export const config: RepoAccessConfig = {
|
|
66
|
+
githubOrg: 'your-org-slug',
|
|
67
|
+
productTeamMap: {
|
|
68
|
+
stripe: { 'prod_...': { teams: ['pro'], grant_mode: 'username' } }, // product id filled in Step 4
|
|
69
|
+
defaults: { teams: [], grant_mode: 'claim', revoke_policy: { mode: 'log_only' } },
|
|
70
|
+
},
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
- `githubOrg` is your org slug (the `github.com/orgs/<slug>` value).
|
|
75
|
+
- Map each Stripe **product id** (you get it in Step 4) to the team(s) that carry the repo.
|
|
76
|
+
- `grant_mode`: **`username`** (the buyer types their GitHub handle at checkout; auto-falls back to a
|
|
77
|
+
claim link if the handle is missing, malformed, or does not exist) or **`claim`** (always send a
|
|
78
|
+
one-time claim link).
|
|
79
|
+
- `revoke_policy`: `{ mode: 'auto_revoke' }` to remove access on refund/chargeback, or
|
|
80
|
+
`{ mode: 'log_only' }` to only log. Keep `defaults` neutral so an unmapped product grants nothing.
|
|
81
|
+
|
|
82
|
+
Provision KV and wire it:
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
wrangler kv namespace create ENTITLEMENTS
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Paste the **real** returned id into `wrangler.jsonc` (`kv_namespaces`), replacing the placeholder. If an
|
|
89
|
+
`ENTITLEMENTS` namespace already exists, reuse its id rather than creating a duplicate. A placeholder id
|
|
90
|
+
will not bind. Then confirm `npm run typecheck` is clean.
|
|
91
|
+
|
|
92
|
+
## 3. First deploy (to learn your worker URL)
|
|
93
|
+
|
|
94
|
+
Copy the secrets template and add your PAT:
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
cp .dev.vars.example .dev.vars
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
In `.dev.vars`, replace `GITHUB_TOKEN=__REPLACE_ME__` with your fine-grained PAT (paste it yourself).
|
|
101
|
+
Leave `STRIPE_WEBHOOK_SECRET` as its placeholder for now - it counts as present for this first deploy.
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
wrangler deploy --secrets-file .dev.vars
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
The first deploy creates the worker and prints its `https://<worker>.workers.dev` URL - note it; Stripe
|
|
108
|
+
needs it. Open `/health` and confirm `{"status":"ok"}`.
|
|
109
|
+
|
|
110
|
+
## 4. Stripe: product, link, metadata, webhook
|
|
111
|
+
|
|
112
|
+
1. **Create the product** (Product catalog, then Create product - this is the current label, not the
|
|
113
|
+
older "Products, then Add product"). Set a one-time price. Copy the **product id** (`prod_...`) and
|
|
114
|
+
wire it into `productTeamMap` from Step 2.
|
|
115
|
+
2. **Create a Payment Link** (Payment Links, then New, then select your product). Leave the options off
|
|
116
|
+
(no managed payments, tax, address collection). For **`username` mode**, under Advanced options add a
|
|
117
|
+
**Text custom field labelled "GitHub username"** (the field key must contain "github"); for **`claim`
|
|
118
|
+
mode**, add no field. Copy the link URL.
|
|
119
|
+
3. **Set `metadata.product_id` on the link** (open the link's detail page, then Metadata, then Edit
|
|
120
|
+
metadata; add key `product_id`, value `prod_...`). This is the step people miss: the
|
|
121
|
+
`checkout.session.completed` webhook omits line items, so the worker reads the product from metadata.
|
|
122
|
+
4. **Create the webhook destination** (Developers, then Webhooks / Event destinations). The current flow
|
|
123
|
+
takes the **events first**, then the destination URL:
|
|
124
|
+
- **Events (select these three):** `checkout.session.completed`, `charge.refunded`,
|
|
125
|
+
`charge.dispute.created`.
|
|
126
|
+
- **Payload style:** pick **Snapshot** if the option appears; the current flow may not show it (it
|
|
127
|
+
defaults to the full snapshot payload).
|
|
128
|
+
- **Endpoint URL:** `https://<your-worker>.workers.dev/wh/stripe/<SECRET_PATH>`, where `<SECRET_PATH>`
|
|
129
|
+
is a random hard-to-guess string you generate:
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
node -e "console.log(require('crypto').randomBytes(16).toString('hex'))"
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
It is obscurity only (the HMAC signature is the real gate; the worker never reads the path), so it
|
|
136
|
+
is NOT a worker secret. Keep it in your notes.
|
|
137
|
+
- Reveal the **Signing secret** (`whsec_...`) for the next step.
|
|
138
|
+
|
|
139
|
+
## 5. Add the secret and re-deploy
|
|
140
|
+
|
|
141
|
+
In `.dev.vars`, replace `STRIPE_WEBHOOK_SECRET=__REPLACE_ME__` with the `whsec_...` value (paste it
|
|
142
|
+
yourself), then re-deploy so the now-filled secret uploads with the code:
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
wrangler deploy --secrets-file .dev.vars
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Confirm the Stripe endpoint URL matches your deployed worker URL plus the secret path.
|
|
149
|
+
|
|
150
|
+
## 6. Test it live (in this order)
|
|
151
|
+
|
|
152
|
+
Start streaming logs before you pay:
|
|
153
|
+
|
|
154
|
+
```bash
|
|
155
|
+
wrangler tail <worker-name>
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
Run the three flows **in this order** - refund before the typo test, so each test stays clean:
|
|
159
|
+
|
|
160
|
+
1. **Grant.** Open the Payment Link and pay with test card `4242 4242 4242 4242` (any future expiry, any
|
|
161
|
+
CVC, any ZIP, any email). In `username` mode, type a real GitHub handle. Expect `POST /wh/stripe/...`,
|
|
162
|
+
then `checkout.session.completed`, then a direct grant with `access.granted`. **`access.granted` means
|
|
163
|
+
the worker created the GitHub invitation, not that the buyer joined.** GitHub emails the buyer the
|
|
164
|
+
invitation - **open it and accept** (or accept at `https://github.com/orgs/<org>/invitation`). Only
|
|
165
|
+
after accepting does the buyer show under Org, then People, in the right team.
|
|
166
|
+
2. **Refund and revoke.** In Stripe, open Payments, open that test payment, then Refund payment (full
|
|
167
|
+
amount). Expect `charge.refunded`, then `access.revoked`, and the buyer removed from the team (any
|
|
168
|
+
pending invite cancelled). Doing the refund **before** the typo test means revoke runs against a
|
|
169
|
+
single clean grant, and the same handle is free to reuse on the claim page next - so one GitHub
|
|
170
|
+
account is enough for the whole run.
|
|
171
|
+
3. **Typo path and claim fallback** (`username` mode). Pay again, this time typing a
|
|
172
|
+
valid-format-but-nonexistent handle (e.g. `someone-nope-xyz`). Expect a team-add 404, then NOT
|
|
173
|
+
`access.failed` but a `grant -> claim fallback`, then `claim.pending`. The claim link is redacted from
|
|
174
|
+
`wrangler tail`; find it in the **Workflow dashboard** (Cloudflare, then Workflows, then your
|
|
175
|
+
workflow, then the run, then the `emit:claim.pending` step output, which carries `claim_url`). Open
|
|
176
|
+
`https://<worker>.workers.dev/claim/<token>`, enter a real handle (reuse the Step-1 handle, now that
|
|
177
|
+
it was revoked), and submit. The claim grant creates a NEW GitHub invitation too - **accept the email
|
|
178
|
+
invite** (or at `https://github.com/orgs/<org>/invitation`) to become a member. A typo never strands a
|
|
179
|
+
paying buyer.
|
|
180
|
+
|
|
181
|
+
Benign noise: `AccessWorkflow.run - Exception Thrown` and `Cancelled` in `wrangler tail` are how
|
|
182
|
+
Cloudflare Workflows logs durable step suspension - they appear on fully successful runs too. Judge a run
|
|
183
|
+
by the Workflow dashboard **Status: Completed** plus the emitted `access.granted` / `access.revoked`, not
|
|
184
|
+
by raw tail lines.
|
|
185
|
+
|
|
186
|
+
That single live grant verifies the whole chain: the PAT, the org hardening, the config, the webhook
|
|
187
|
+
signature, and the deploy.
|
|
188
|
+
|
|
189
|
+
## 7. Going live and operating
|
|
190
|
+
|
|
191
|
+
- **Add more products:** map each new Stripe product id to a team in `productTeamMap` and re-deploy.
|
|
192
|
+
- **Production:** repeat with the `production` config profile and `.dev.vars.production`, deploying with
|
|
193
|
+
`wrangler deploy --env production --secrets-file .dev.vars.production`. Stripe stays the same flow in
|
|
194
|
+
live mode.
|
|
195
|
+
- **Token rotation:** the fine-grained PAT expires (the org caps its lifetime). Before it lapses, issue a
|
|
196
|
+
new token with the same scope (org owner, repository access None, Members Read and write) and update
|
|
197
|
+
the `GITHUB_TOKEN` secret, then re-deploy. If it lapses, grants and revokes stop until you rotate.
|
|
198
|
+
|
|
199
|
+
## Troubleshooting
|
|
200
|
+
|
|
201
|
+
- **Grant fails 401 / 403:** the PAT is missing Members Read and write, or fine-grained PATs are
|
|
202
|
+
restricted at the org, or the token is pending approval or expired.
|
|
203
|
+
- **Grant fails 404 (user not found):** the GitHub username does not exist - a buyer typo, not a token
|
|
204
|
+
problem. In `username` mode the buyer automatically gets a claim link to self-correct.
|
|
205
|
+
- **A valid handle still routes to the claim page:** the custom-field key did not contain "github". Use
|
|
206
|
+
`metadata.github_username` on the checkout instead.
|
|
207
|
+
- **Buyer paid but is not in the repo:** the invite is created right after payment and the buyer must
|
|
208
|
+
**accept** it (via the GitHub email). Confirm the sale and `access.granted` in the Workflow run; if
|
|
209
|
+
those are there, you are waiting on the buyer to accept.
|
|
210
|
+
- **New-org invite cap:** 50 invitations per 24h for the first month - age the org before a big launch.
|
|
211
|
+
|
|
212
|
+
---
|
|
213
|
+
|
|
214
|
+
RepoAccess is part of [EdgeKits](https://edgekits.dev). For the guided path, run `/repoaccess-setup` in
|
|
215
|
+
Claude Code instead (see the README).
|
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "repoaccess-core",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"license": "AGPL-3.0-or-later",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": "./src/index.ts",
|
|
8
|
+
"./adapters/stripe": "./src/adapters/stripe.ts"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"src",
|
|
12
|
+
"docs",
|
|
13
|
+
"README.md",
|
|
14
|
+
"LICENSE"
|
|
15
|
+
],
|
|
16
|
+
"scripts": {
|
|
17
|
+
"dev": "wrangler dev",
|
|
18
|
+
"deploy": "wrangler deploy --minify",
|
|
19
|
+
"typegen": "wrangler types --env-interface CloudflareBindings",
|
|
20
|
+
"typecheck": "tsc --noEmit",
|
|
21
|
+
"test": "vitest run",
|
|
22
|
+
"format": "prettier --write ."
|
|
23
|
+
},
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"hono": "^4.12.25"
|
|
26
|
+
},
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"@cloudflare/vitest-pool-workers": "^0.16.15",
|
|
29
|
+
"@types/node": "^25.9.3",
|
|
30
|
+
"prettier": "^3.8.4",
|
|
31
|
+
"typescript": "5.9.3",
|
|
32
|
+
"vitest": "^4.1.0",
|
|
33
|
+
"wrangler": "^4.104.0"
|
|
34
|
+
}
|
|
35
|
+
}
|