| status | open |
|---|---|
| summary | Assessment of the TDXLU Gate Contract (artifact 06da3418, written at fe5f4d4) against the gate as it stands after the 2026-08-29 walk — who accommodates whom, which of its premises the weekend dissolved, the four pieces of gate surface we accept building, and the conditions attached. |
| since | 2026-08-29 |
| skill | fns-packaging |
Reviewed: the "TDXLU Gate Contract" artifact (rendered from the
launcher repo's docs/fns-gate.md at fe5f4d4). Verdict up front,
because it is the question that was asked:
Both sides move, and the asymmetry is exactly right. The contract is architected to CONSUME the gate rather than fork it — one Worker, one keypair, one Patreon client, their own KV namespace — which means most of the accommodation is theirs by design and consists of using what exists unchanged. We accept four bounded pieces of new gate surface (§3 below), each of which is generically useful rather than TDXLU-shaped. And their document needs a facts refresh: it was written before the weekend the gate went to production, and several of its premises — including its own blocker — are now stale in TDXLU's favor.
The contract says "blocked-on: the gate is BUILT, NOT DEPLOYED … deploy it for FNSTools alone first; TDXLU comes after it has run in production" and repeats it as Open #1. As of 2026-08-29 evening:
- The gate is deployed (worker version
3730cc04+) with the real tier map, and serves production traffic. - The paid path has been walked end to end on a customer-shaped
install:
/get/→ paste rail → bootstrap → sign-in → entitlement → gated artifactsha-true through the token rail → installed. - v3.0.5/v3.0.6 are live in the bucket; the
plus/prefix is canary-verified private on every upload.
"Let it prove itself" has begun. The remaining maturity gate on OUR
side is EntitlementFunnelPlan.md 0.5 (the bootstrap instance's
in-pass download wedge — observed, uncaptured, launch-blocking for
strangers). TDXLU integration work on the Worker can start before 0.5
closes (different code paths), but TDXLU should not LAUNCH on the gate
until 0.5 is dead — same spirit as their own deploy-order rule.
Stale facts, each one line to fix on their side:
- Host. Every URL says
gate.functionstr.com. The domain migrated: the gate lives atgate.functionstore.tools, storage atstorage.functionstore.tools. Their §5 Patreon-client redirect addition must register the NEW callback host (it already is — sign-ins ran through it on 2026-08-29). - Redeem shape. Their route table shows
{key, product}. The API is{license_key, product_id}— and since128e3afalso{license_key, package}: the gate resolves Gumroad's product id from the package name through its own one-to-one map. The launcher should send the package name; a buyer knows the tool, not the id. (Response carries{ok, device_token, products}— nousesfield.) /session/recheckexists and is absent from their table. It forces past the 6-hour cache (throttled), and since8574a64its answer carries structure theirLicenseStatusshould consume verbatim:connected: false= the Patreon grant is dead and only a re-sign-in helps (do NOT keep offering "check again");stale: true+verified_at= this is the last known answer served during a Patreon outage, not a real "no". This is also the natural trigger for their §3.1 "renewed on any successful re-check" claim renewal.- The manifest carries a routes projection (
718743c): the toolkit block ships the tier ladder WITH labels (Base/Pro/Coaching),support_url, and per-packagekey_available. Their UI copy ("unlocks at the Pro tier") can read the same projection instead of hardcoding labels. - The creator short-circuit is their test rail too. The ladder
grants upward and the creator holds the top tier, so the moment
TDXLU_Proenters theTIERSlists, the creator account is entitled to it — the launcher's whole flow can be walked by its builder before any customer. Their Open list should claim this explicitly. - A methodology warning they have earned the right to skip, once.
The first real walk of OUR client flow found three
measured-not-assumed platform bugs (query parsing, response
routing, auth headers) — every one in a leg that had never executed.
Their
licensing.rsrework is a new client against the same gate: its legs have also never executed. Their §8 should add "walk every leg against the deployed gate with the creator account before shipping" as a numbered step, not an assumption.
Four pieces, all on OUR worker, all generically useful. Conditions apply to each (see §4).
POST /entitlement— the signed long-lived claim (/token/downloadgeneralized: same session load, samerefreshEntitlement, EdDSA JWT with per-kindexp). Accepted, and noted: their blockquote is right that this answers OUR openEntitlementLifecycle.md §2.2(how literal is offline) and dissolves the Gumroad KV-durability worry — the signed claim becomes a second copy of the licence that the client holds.POST /trial/start— Turnstile-gated,trial:<fingerprint>KV anchor written once and never rewritten, minting an ordinarykind:'trial'session. Accepted. This is the "gate-minted trial" our funnel plan's trial-lane section explicitly deferred "to its own research doc if ever wanted" — this contract is that request, and their abuse analysis (Turnstile makes minting uneconomic; it is a 14-day trial, not a vault) matches our honesty-box doctrine.- Config + namespace:
TDXLU_Proadded to the threeTIERSlists, aGUMROAD_PRODUCTSrow when the product exists, and a SECOND KV namespace bound for launcher sessions/trials. Verified on our side:publish.py's entitlement preflight checks only the package→grant direction, so foreign product names inTIERSpass clean — no toolkit release breakage. - A second gated prefix (
utility/plus/…) served by the same worker with the same fail-closed rule — ONLY if their §4 (gated companion modules) is ever decided for. Their own document keeps it separable; we keep it conditional.
- Tests before deploy, every route.
gate.test.mjsis the gate's spine (70+ offline checks, grown four times this weekend). New surface lands with its sections: trial anchor never rewrites, Turnstile refusals, per-kindexpon/entitlement, the second prefix failing closed. No exceptions — this weekend demonstrated exactly what untested legs cost. - Name the irrevocability. A signed claim with a 10-year (Gumroad)
or 180-day (Patreon)
expis IRREVOCABLE by construction once minted — revoking the session stops new claims, never outstanding ones. For Gumroad that matches the perpetual-licence decision; for a refunded purchase it means the refunded buyer keeps the claim window. That is consistent with "purchase gate, not DRM", but it must be a sentence in the contract, not a surprise: their §7 list should gain "an issued claim outlives revocation until its exp". - The machine binding is honesty-box.
machinein the claim is a hash of a CLIENT-computed fingerprint; the gate cannot verify it. Fine — same tier of protection as the HMAC scheme it replaces — but their Open #2 (fingerprint derivation) should note the gate treats it as opaque and unverifiable by design. - Secret rotation becomes production discipline. Their §5.1 point
stands and we co-sign it: with two products on one gate, a botched
PATREON_CLIENT_SECRETrotation silently freezes BOTH. Before TDXLU lands, the rotation procedure gets a runbook entry on our side (EntitlementLifecycle §5.1 is the source). - Sequencing. Worker-side routes may be built and tested any time; TDXLU goes live on the gate only after funnel plan 0.5 is closed and v3.0.x has survived its first strangers.
- The trial lane forks, deliberately. The funnel plan's trial
section documents TDXMap as tool-governed (the tool holds the
clock); TDXLU commits to gate-minted trials. Both models now exist
in the family. That is acceptable — different products, different
offline promises — but the funnel plan's trial section should note
the fork and that
/trial/start, once built, is available to any family product that prefers the gate-minted model (TDXMap could converge later; its choice stays its own). - CMS observability gap. The CMS entitlement table renders
per-toolkit-package from
catalog.json;TDXLU_ProinTIERSwill be invisible there. The one place that grants it would have no authoring surface showing it. Small, but exactly the drift channelgate_package.pyexists to close — its--statusand the CMS table should learn to show foreign (non-package) grants as their own rows before TDXLU's row lands. - One Patreon client, three consumers. Their §5 confirms TDMap shares the client and its loopback redirect must not be removed. With the gate's callback added, the client has three registered redirect URIs and a leak in ANY consumer compromises all — their §1 already says this about the shipped secret; retiring the launcher's embedded copy makes the gate the only secret holder, which is the point.
In order; each lands as its own commit with its tests. G1–G4 can be built and merged any time; nothing DEPLOYS for TDXLU until the gate precondition (funnel plan 0.5 closed, v3.0.x survived strangers) is met.
- G1 —
POST /entitlement(LANDEDb9025b1, deployed9ea13ff82026-08-30, live-verified;GET /pubkeyadded alongside so clients pinGATE_PUBLIC_KEYfrom one authoritative endpoint; G3's TIERS rows rode the same deploy — the launcher's L5 walk and the joint G7 legs are UNBLOCKED, their L3+L4 being already live per tdxlpp-a9. G7 itself was refined on tdxlpp-2f's review,04da4b9: the shared file lives at the machine-DEFAULT palette path always, never the Storefolder override, and Sign in adopts before it browses.):worker/src/index.js: session load +refreshEntitlementas in/token/download, but returns the EdDSA claim (iss,sub,machinepassthrough,kind,products,trial_expires_at, per-kindexp: gumroad +10y, patreon +180d, trial = window end) and does NOT require non-empty products. Tests: per-kindexp; a lapsed session gets its lapse;machineis opaque passthrough; a claim verifies againstJWT_PUBLIC_KEY. - G2 —
POST /trial/start(PENDING DECISION 2026-08-30: the owner is leaning toward dropping the TDXLU trial entirely. Dropping it deletes this item, the Turnstile secret and its desktop-UX question, the fingerprint question, and the one route that mints entitlement from nothing — the audit's trial finding then closes by deleting Firebase rather than replacing it, and the funnel plan's trial-lane fork un-forks. Reversible later: the route is separable by construction. Do not build until decided.) Turnstile verify (newTURNSTILE_SECRETwrangler secret),trial:<fingerprint>KV row written once and NEVER rewritten, second start returns the same anchor and remaining time, mints an ordinarykind:'trial'session. Throttled like redeem. Tests: anchor immutability, same window on re-start, Turnstile refusal, throttle,/entitlementover a trial session, expiry refusing new claims. - G3 — config + namespace.
wrangler.toml:TDXLU_Proin all threeTIERSlists;GUMROAD_PRODUCTSrow when the product id exists; second KV namespace bound (e.g.TDXLU_SESSIONS) with the trial/session code parameterized over the binding. Tests: FNSTools sessions never land in the launcher namespace and vice versa. - G4 — rotation runbook.
EntitlementLifecycle.md §5.1becomes a numbered procedure (stage new secret, verify with a live sign-in, retire old) inworker/README.md— required before a second product shares the blast radius. - G5 — CMS shows foreign grants.
gate_package.py --statusand the CMS entitlement table gain rows for grants that are not toolkit packages, soTDXLU_Prois visible where it is authored. - G7 — the shared machine session (LANDED
1f9fec5, 2026-08-30; acceptance recorded here per the §5 sequencing bullet — drafted this side on the launcher session's green light, no formal acceptance round per the amended contract):gate-session.jsonat<palette>/FNSTools/config/(beside the config, never understore/), schema{schema:1, device_token, written_by, written_at}, per the launcher contract's amended §5: atomic write on every successful sign-in and redeem; adopt when holding no session (once per extension lifetime, entitlements filled by a deferred token request); delete on gatesigned_outand on sign-out — in both cases only while the file still holds the token in question, so a newer sign-in by another product survives; sign-out revokes first and signs the machine out of every product. Drafted this side under the contract's either-side clause, landed as1f9fec5, sandbox- verified (schema round-trip, guarded deletes, adoption). Two design points flagged to the launcher side for their §5 to mirror: a REDEEM also publishes the machine token (a licence key mints or extends the session too), and adoption stores a placeholder label until the first token response corrects it. Named cost, accepted pre-release: the opaque revocable token sits in plaintext JSON beside the config — weaker at rest than the DPAPI keystore that remains our own copy. Launcher-side worklist note (2026-08-30, via tdxlpp-a9): their dependency is G1 + TIERS-only G3 (no Gumroad row yet); their L5 walk waits on those two, not on G7. Ship lag (first L5 walk finding, 2026-08-30, via tdxlpp-2f): G7 reaches customers only through a FNSTools RELEASE — a sign-in running through a shipped pre-G7 updater tox writes no shared file, and that walk leg reads as failed when it is merely early. First release carrying it: v3.0.7 (updater 3.0.4); installed projects gain it via a normal update. Backfill (adopted from the same finding): a session signed in before G7 never published — publish only fired on sign-in/redeem.Account()now backfills once per extension lifetime: token held + shared file absent → publish the held token. Idempotent; makes G7 retroactive for every existing session the moment the new ExtAuth loads. Launcher mirrors inlicensing.rsfor symmetry. Adoption can hand over a session that entitles NOTHING (field report 2026-09-17, a Mac): a fresh FNSTools drop adopted the sessiontdxluwrote, the gate tied it to no Patreon grant, and the record then read as signed in with no products, so every picker surface offered only Join and I just pledged and nothing could start OAuth. Two changes: adoption now asks for a RECHECK rather than a token (a recheck answer carriesconnected, a token refusal carries only products), and the picker offers the sign-in whenever a session entitles nothing, keeping I just pledged beside it. Its status sentence never says a link is "no longer active" for a session that never had one. An entitled account also gets Use a different account, because signing out in FNS_Updater is not a route the picker may rely on. - G6 (conditional, only if their §4 is decided FOR) — second gated
prefix. Route
storage.functionstore.tools/utility/plus/*through the worker; generalize the prefix check; fail-closed tests against a claim that names no launcher module.
Handed to the TDXLU session as their worklist; L1–L2 are edits to
their docs/fns-gate.md, the rest is licensing.rs work from their
own §6, amended by the weekend.
- L1 — facts refresh in
fns-gate.md: host →gate.functionstore.tools; blocker paragraph → "deployed and walked 2026-08-29, precondition met, launch gated on funnel plan 0.5"; redeem →{license_key, package}(response has nouses); route table gains/session/recheck. - L2 — two sentences added: an issued claim outlives revocation
until its
exp(purchase gate, not DRM — say it); and the machine binding is client-computed and unverifiable by the gate, by design. - L3 — client flow per their §6, unchanged: loopback answers
/fns-auth, verifycn, claim → device token,/entitlement→ cached claim, atomic auth-file write,SignOutrevokes first. - L4 — consume recheck structure:
connected:false→ the re-sign-in path, never "check again";stale:true+verified_at→ surfaced as staleness, not a refusal; successful recheck renews the cached claim via/entitlement. - L5 — walk every leg before ship: with the creator account
(entitled to
TDXLU_Provia the ladder the moment G3 lands), run sign-in, claim, entitlement, trial-start (second machine or reset fingerprint), recheck, revoke — against the deployed gate, before any customer does. Budget for finding bugs: our equivalent walk found six. - L6 — trial UX decision (their Open #3): Turnstile surface (loopback page vs webview) — their call, no gate dependency.
Both sides are done up to the interactive leg. Gate: G1 + /pubkey +
G3 TIERS rows deployed (9ea13ff8), G7 landed and refined. Launcher
(per tdxlpp-a9): GATE_PUBLIC_KEY filled from /pubkey and committed
(TDXLPP d9a3573), 136 tests passing, their contract updated —
/pubkey in its route table, G1+G3 recorded live, the §5 path caveat
RESOLVED. The joint walk needs Dan — the sign-in leg is an
interactive browser flow with the creator account. The staged ladder:
sign-in → claim → /entitlement → recheck → revoke, plus the G7
legs (adopt both directions, machine-wide sign-out). After a green
walk, TDXLU's go-live gate remains: funnel plan 0.5 closed, v3.0.x
soaked, then the Patreon secret rotation per the runbook (G4, still
to write).
Coordination note (2026-08-30, TDXLPP a198b33): the launcher's
contract mirror is reciprocal — their §8.3 names TD-adopts-a-
launcher-written-file as the cross-test leg to watch, and the
closing-entry-in-both-docs agreement is written down on both sides.
Launcher-side routing: fns-gate.md and licensing.rs edits go
through session tdxlpp-2f (or whichever session Dan points at);
tdxlpp-a9 remains a relay. Address contract-mirror requests
accordingly.
Symmetry closure (2026-08-30, first live walk night, via tdxlpp-2f):
backfill mirrored in licensing.rs (TDXLPP 4b16cca) with the
dead-token caveat; sign-out semantics sharpened in their §5 — a
sign-out revokes only the token it holds, divergent own-sessions
survive and sign out separately; their background refresh now honors
a definitive signed_out immediately (clears local record + shared
file, typed errors keep outages separate), matching our _sessionDied
on 401 — machine-wide sign-out reaches the launcher at its next
refresh/launch. Launcher download rails were audited against the
byte-eating trap fixed this side (70f5403) and already stage +
verify before rename; their store rail is free-only today. Launcher
open item, on record: fns_store.rs has no notion of access, so a
gated row in their store panel would surface as a digest-ish error —
FNSTools-Plus work-order territory.
They built their side to accommodate us — correctly, and the document shows real fluency in the gate's paid-for decisions (fail-closed, product lists not booleans, one-time grant codes, the two ExtAuth traps). We accommodate them with two routes, one config change, one namespace, and conditionally one prefix — each with tests, each generically useful, none TDXLU-shaped. Their document owes a facts refresh (host, redeem shape, recheck, the dissolved blocker) and two added sentences (claim irrevocability, walk-before-ship). Neither side bends its architecture; that is what makes this the cheap version of the integration.