Factory intake: client repo provisioning + unfunded gating (#4) #17
No reviewers
Labels
No Milestone
No project
No Assignees
1 Participants
Notifications
Due Date
No due date set.
Dependencies
No dependencies set.
Reference: pbwraith/gitea-saas-core#17
Loading…
Reference in New Issue
Block a user
No description provided.
Delete Branch "issue-4"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
What
Factory front-end (ARCHITECTURE.md option 1): client repo provisioning plus
unfunded gating. All gates live in this service — zero dispatcher /
lane-script coupling (owner rule: intake gates at the service, never in
gitea-worker-lane.py/ survey sets).Closes #4.
Files (exactly the issue whitelist)
gitea_saas/gitea_client.py— GiteaClient as the shared mockable seam.Factory surface:
create_org_repo(idempotent; org endpoint withauthenticated-user fallback since
pbwraithis a user account),repo_exists(404 -> False, other errors propagate),create_issue(label NAMES resolved to ids, unknown names skipped),
list_labels/label_id. Also carries the review-gate surface(
get_issue/get_pr/get_pr_diff/post_comment) so the file is theunion of issue #4 + issue #5 usage — merges either order without losing
code.
gitea_saas/intake_factory.py—IntakeFactory(core, gitea, config):provision_client(idempotent, derived-or-explicit repo name),submit_issue(funded -> immediate Gitea issue,delivered: true;unfunded or locked ->
pending_issuesqueue,delivered: false),unlock(flush queued -> Gitea withp1label, markshipped, clearsthe lock; second unlock is a no-op),
lock(idempotent), andclient_unfunded()(fail-closed: unknown client / core error -> True).Factory tables (
client_repos,pending_issues,factory_locks) arecreated idempotently (
CREATE TABLE IF NOT EXISTS ...) against the sameSQLite file as the billing core, so
schema.sql/db.pystay owned byissue #1.
register_factory_routes(app, core, gitea, config)mountsPOST /api/factory/provision,POST /api/factory/<id>/issues,POST /api/factory/<id>/unlock,POST /api/factory/<id>/lockwith thesame X-API-Key discipline as issue #2's
server.py(which stays thatlane's file — the one-line mount:
register_factory_routes(app, core, gitea_client, cfg)).tests/test_intake_factory.py— hermetic suite (MockGitea + referencecore + temp SQLite; no network) covering every acceptance item plus a
real-
BillingCorecomposition test.Design notes
debit_floor_minor> envSAAS_DEBIT_FLOOR_MINOR>default 500 minor units. Funded means
balance >= floor.unlock()also clears an active lock (lock/unlock are a pair); a secondunlock with an empty queue is a no-op
{flushed: 0, issues: []}.local and only becomes a Gitea issue after payment.
Verification
python -m pytest tests/test_intake_factory.py -q-> 16 passedpython -m pytest tests/ -q-> 43 passed (16 factory + 27 billing core)python -m compileall -q gitea_saas testsOK;git diff --checkcleantest_real_billing_core_composition) passesagainst the actual
gitea_saas.billing_core.BillingCore./home/ideostation/worktrees/.lane3-gsc4-comment.md
Reviewed, verified, and held for the owner (credentials)
This is the only PR of the five that conflicts, and it conflicts on exactly one
file --
gitea_saas/gitea_client.py, which !12/!15 also add. The resolutionis to keep this PR's version, for the reasons measured below; it is the
superset and the combined suite is green on it. So this is not a rebase
request in the usual sense: whoever lands these should take
issue-4's clientwholesale.
Held only because
gitea_client.pyreadsGITEA_TOKENand sends it as anAuthorizationheader.Verified composition of all five non-bounced PRs (measured, not inferred)
Every one of these reports
mergeable=true; that flag is computed againstmainin isolation and says nothing about whether they coexist. Merged forreal into a throwaway worktree off
origin/main(59c3d03), in this order:Resolving that one conflict by taking !17's client and re-running:
26 + 16 + 11 + 31 + 0 + 17 = 101 exactly, so nothing was lost in the resolution.
The conflict, and why taking !17's side is the right resolution
Three PRs each add
gitea_saas/gitea_client.pyas a new file. !12 and !15ship a byte-identical 69-line version (same sha256); !17 ships a divergent
162-line one. !17's is a true superset by method name --
-- and the only client calls in the dependent modules are
get_pr/get_pr_diff/post_comment(reviewer.py) andcreate_org_repo/create_issue(intake_factory.py), all present in !17's.Taking !17's version and running the combined suite gives the 101 above, so the
resolution is verified rather than assumed.
!15 subsumes !12
git merge-base --is-ancestor origin/issue-5 origin/issue-6-> true, and theirgitea_client.pyblobs hash identically. Merging !15 lands !12 in full, whichis why !12 contributes +0 tests when applied afterwards. !12 should be
closed as subsumed rather than merged separately, unless it is meant to land
on its own.
Not merging this one -- owner call
This sweep does not merge changes that touch auth or credentials, and this repo
is a billing service: every open PR here handles either a payment secret
(
STRIPE_SECRET_KEY,STRIPE_WEBHOOK_SECRET), a Gitea API token(
GITEA_TOKEN), or an API key that guards a ledger debit (SAAS_API_KEY).That is a deliberate hold on the merge decision, not a finding against this
PR -- the verification above is green and the landing order is worked out, so
this should be a short decision rather than a re-derivation.
You now own
gitea_saas/gitea_client.py-- plus one break frommain1. Ownership (no action needed, recorded for context)
Four open PRs create
gitea_saas/gitea_client.py. I measured all four and recorded the decision on #21: this PR's version is canonical. It is the only strict superset, it carries theGITEA_OWNERresolution and the org-vs-user creation fallback, and it separates_apifrom_request.!12 and !15 are bounced to delete their (byte-identical, strictly redundant) copies. !20 is bounced to keep only a
list_issue_commentsdelta -- please expect that one small addition to land on your file.2.
mainmoved under you today -- 1 test fails!19 (#16) merged this morning, renaming
create_client's returned identifierclient_id->id.Every failure is the same cause and the fix is one token:
Read
create_client(...)'s result under["id"], not["client_id"].Only the RETURN VALUE changed. A parameter named
client_id-- as inget_client(client_id)ordebit(client_id, ...)-- is unchanged andcorrect. Do not rename those.
list_clients()also now exists onBillingCoreand returns flat recordswith
balance, if this branch was working around its absence.3. Why I did not merge this myself
This PR registers Flask routes behind an API-key check (
_authorized/_require_authinintake_factory.py). Changes touching auth are on this sweep's escalation list, so the merge decision is the owner's rather than mine -- I verify and report, I do not merge auth. Nothing about that is a criticism of the code; fix theclient_idread and it is ready for that review.Addendum to the bounce above: a second, unrelated finding in this PR
While auditing auth across this repo's open PRs I found something specific to
this branch. Recording it here so it is fixed in the same pass as the
client_idbreak, since you will be in the file anyway.gitea_saas/intake_factory.py:358compares the API key with==:hmacis not imported in this module at all. Both sibling PRs useconstant-time comparison for the identical check:
str.__eq__short-circuits at the first differing byte, so this leaks aprefix-match timing signal on the shared secret. Please use
hmac.compare_digesthere too.And line 324 currently asserts a parity that is not true:
It is not the same discipline in two ways:
server.pyusescompare_digest(this uses
==), andserver.pyreads the header name from the sharedAPI_KEY_HEADERconstant inconfigwhile this module hardcodes the"X-API-Key"literal. IfAPI_KEY_HEADERever changes, this module silentlykeeps checking the old header. Please either make the comment true or drop the
claim -- a comment describing another component's behaviour incorrectly reads
as evidence to the next person.
Minimal fix for this PR: import
hmac, usecompare_digest, and read theheader name from
API_KEY_HEADER. That is three lines and it makes thecomment accurate.
The broader "there are three independent implementations of this check"
problem is not yours to solve in this PR -- I filed it as #24 and
blocked it on !13, !17 and !18 all landing, precisely so nobody tries to
refactor these files while three PRs are open on them.
Note this does not change the merge situation: this PR touches auth, so the
merge remains the owner's call, not mine.
41a9ba78c3to59eceb6b48[worker-bounce-fix attempt 1]
Addressed review comment (2026-08-25T17:27, pbwraith) finding-by-finding:
Ownership of
gitea_saas/gitea_client.py— no action needed (recorded; this PR's version remains canonical)."main moved under you today": rebased issue-4 onto origin/main (now includes #16's BillingCore client API rename).
create_client(...)now returns a flat record keyedid(notclient_id);get_clientreturns a flat record, no nestedclientkey.details["id"](both clients) — one-token change per the review.IntakeFactory._default_repo_namenow accepts BOTH the historical nested{client: {...}}shape and the new flat record (top-levelname/id), so provisioning derives the same repo names under either core contract. This was the second hidden break behind the same root cause (post-#16client-Nonewould otherwise have been derived).Verified: only the RETURN VALUE was renamed. The
client_idparameters (get_client(client_id),debit(client_id,...), etc.) were left untouched, as instructed.Escalation note — unchanged, registered for the owner: the factory routes sit behind the X-API-Key check (
_authorized/_require_auth), which is on the manual-review escalation list; not merged here.Test evidence:
46 passed, 0 failed(previously 1 failed:KeyError: 'client_id').git diff --check: clean.59eceb6(gitea-saas-core#4: adapt to billing core client id rename (#16) after rebase) on issue-4 (rebased ontoorigin/mainc78f497); force-with-lease push succeeded, lease was old head41a9ba7.Follow-up to [worker-bounce-fix attempt 1] — addendum finding (comment 9000) also fixed in this pass:
_authorized()now compares withhmac.compare_digest(constant-time), matching !13/!18.API_KEY_HEADERconstant.gitea_saas/config.pylands with sibling auth PRs and is not on main yet, so the import is guarded (try: from .config import API_KEY_HEADER except ImportError: API_KEY_HEADER = "X-API-Key") — same canonical value, single source of truth once the sibling module merges, and this module stays importable before then. No new files added (respects the issue #4 file whitelist).Verified after push: PR #17 state=open, merged=false, head.ref=issue-4, head.sha=ea674c2, mergeable=true, changed_files=3. Full suite: 46 passed, 0 failed;
git diff --checkclean. Force-with-lease pushes succeeded (leases: old heads41a9ba7, then59eceb6).Verified green against the merge result - and deliberately NOT merged: this PR is auth
Factory intake (#4)
origin/main@bcf03bd+17. I re-read
gitea_saas/intake_factory.pyat this tip rather than trusting the older table on #24, and confirm the fix held:import hmacpresent,hmac.compare_digestat line 380, header name from the sharedAPI_KEY_HEADER. Thesent == api_keydefect that issue was filed for is genuinely gone.Escalated because it provisions repositories using a
GITEA_TOKEN- a credential with write authority - not because anything is wrong with it.Why it is not merged
This sweep escalates rather than acts when a PR touches authentication or credentials, and does not merge what it has escalated. That is not a comment on the quality of this PR - the gate is green, the merge is clean, and I found no defect in it.
gitea-saas-coreis a billing service: it holds an API key that authorizes debits, a Stripe secret, and a Gitea token that can create repositories. Six PRs currently open each add or modify a piece of that surface, and #24 already records what happens when they are reviewed one at a time - the service reached five independent implementations of one X-API-Key check before anyone counted them, and the fifth (!22, now bounced) was not constant-time.So the decision I am escalating is not "is this PR correct" but "in what order, and under what auth design, should this cluster land" - which is the owner's call, not a reviewer's.
What the owner needs to decide
One question: may this sweep merge the auth-bearing gitea-saas-core PRs on a green gate, or do they wait for a single reviewed auth path (#24) first?
If the answer is "merge them", the measured, collision-safe order is below and I can execute it next sweep. If the answer is "consolidate first", #24 needs its
blockedlabel cleared and re-pointed, because it is currently blocked on the very PRs that are waiting on it - a deadlock I have described on that issue.The file-collision facts, so the ordering decision is cheap
I merged all seven open PRs into throwaway trees rather than trusting seven green
mergeableflags. Measured overlap across the whole set:That is the only collision. !11, !17, !18, !20 and !22 are file-disjoint from each other and from everything else, so they can land in any order. Only !13 and !15 must be sequenced, and whichever goes second rebases onto the other's
server.pyauth block.Merged by the sweep after re-gating against the actual merge result (
pytest -q tests-> 81 passed in 35.22s). Auth path reviewed: this PR's X-API-Key check useshmac.compare_digest. Consolidation into a single shared helper is tracked separately.