docs(lore-0156): settle the self-service auth assumptions — membership + account age, one key, ADR 0010 - #187
Open
adamkoot wants to merge 8 commits into
Open
Conversation
Five parallel research lines settle the two items the Self-Service Onboarding epic flagged "confirm before build". The barrier exists but the flow could not see it: Stellar's guild has Membership Screening enabled, yet under the `identify` scope OAuth observes only that a Discord account exists. `verified` requires the `email` scope and no phone field exists on the OAuth user object at all. SDF's own SCF Dashboard does not treat a Discord account as sufficient either. One active key per account is confirmed and structurally required: AWS charges quota per (usage plan, API key) and has no principal that aggregates keys, so multi-key would force the aggregation the rework cap exists to avoid. Every cited URL was re-fetched and its quotes checked against the original; live endpoints were re-run independently. 17 sources archived.
0169 owns the external half: agreeing production guild integration with SDF and proving the flow against the real guild. Development runs against a `stellar_test` guild instead, so this blocks launch rather than build. Its most important question is whether SDF will keep Membership Screening on and warn us if that changes — with the account age threshold at 5 minutes, screening is the entire abuse barrier. 0170 measures seven behaviours the design depends on that turn out to be undocumented. Two are corrections to text already written as fact: `nameQuery` is not documented as a prefix match, and the monthly quota reset instant is not documented at all.
Scope grows by two gates: membership of the Stellar Discord via `identify` + `guilds.members.read`, and a 5-minute account age from the snowflake, matching Stellar's own verification_level. Both are checked once at issuance, so the registry stores no membership data and the 0160 handlers make no Discord calls at all. 0164 needed rewriting rather than extending: "run from a genuinely fresh account" would now fail by design, and its evidence run must follow 0169's SSM flip — a flow gated on our private test guild is not functional for any outside developer, which is what the Tranche 3 criterion asks about. Epic ordering recorded there. Also corrects two claims the sources do not support, in the epic and in 0157/0158/0160: `nameQuery` matching semantics are undocumented, and the quota reset instant is our own product rule rather than inherited AWS behaviour.
0169 was taken on develop by the deploy-board trigger bug (PR #184), merged while this branch was in flight. The SDF integration task becomes 0170 and the undocumented-behaviour spike becomes 0171. References updated across the epic, ADR 0010, 0157-0164 and both task files. The verbatim source archives are untouched: one apparent hit there was the substring 017000 inside an OAuth token timestamp.
ADR 0010 makes the guild ID and the account-age threshold configuration,
but no task declared where they come from. Unassigned, they would have
been hard-coded — the one outcome the ADR rules out. 0159 now owns
creating both under the existing /prices/{env}/* key contract, and must
prove the threshold is tunable without a redeploy.
Also restores numeric order of 0164's checks 11 and 12.
Five independent audits of this PR. The evidence base held: 60 URLs
re-fetched, every load-bearing quote verbatim, all 22 cost figures
reproduced. The decision documents did not.
Corrected in ADR 0010:
- HIGH's 10 minutes is server dwell time, not account age. Discord
publishes no account-age threshold above five minutes.
- `customerId` has two AWS definitions; the omitted one ("or an external
system, such as a developer portal") describes our case. The
conclusion stands on a narrower warrant: no page says quota aggregates
over it.
- Added the phone-supersedes caveat, the delete-then-create derivation
the research asked the ADR to show, and lowering the quota as a
costed alternative.
- Restated the barrier honestly as two clicks and a five-minute wait.
- Hedges restored where the research was more careful than the ADR.
Recorded as Open, blocking 0159/0160/0162: whether issuance requires
`pending === false`; how the eligibility verdict reaches the issue
endpoint, since "checked once at issuance" was written meaning "at
sign-in" and the tasks split on exactly that seam; and what happens when
a departed member's session expires.
Also: the new SSM parameters are operator-seeded and read at runtime,
not CDK-created — a CloudFormation-owned parameter would revert the
launch guild flip on the next deploy. Citation slips fixed in three
research notes; 0171 renumbered seven to nine; notes promoted to mature.
Adam's decisions on the three questions the audit opened. Membership must hold at the moment a key is issued, not at sign-in. The verdict cannot ride in the session cookie: OAuth only proves a Discord account exists, so an ineligible user holds a valid session, and a signed claim would date the check to sign-in and die long before a rework. So the action carries the OAuth round-trip — `state` binds the intended action, and the callback checks with a fresh token. No bot in Stellar's guild, no persisted tokens. An issued key never expires and is never deactivated. Reveal and usage need no Discord check and work indefinitely, including for someone who has left the guild. Rework re-checks membership; account age is not re-checked, since an account old enough once stays old enough. Reading `pending: true` as not-a-member, flagged in the ADR as reversible — Discord restricts such members from acting in the guild, and it is the only reading under which screening means anything. Gated on 0171 #2: `pending`'s presence on the REST response is undocumented, and a naive test would refuse everyone if it is absent. Supersedes "checked once, at issuance" in ADR 0010 §3 and withdraws 0160's "these handlers make no Discord calls" — issue was one of them.
karczuRF
approved these changes
Aug 10, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes the two items the Self-Service Onboarding epic flagged "confirm before build". Research is in
lore/1-tasks/active/0156_.../, the decision is ADR 0010.The finding that changed the design
The epic's abuse barrier exists but the flow could not see it.
Stellar's guild (
897514728459468821) does have Membership Screening enabled, so the assumption was not baseless. But under theidentifyscope, Discord OAuth observes only that a Discord account exists —verifiedrequires theemailscope, there is no phone field on the OAuth user object at all, and guild membership sits behind a separate scope and call. The barrier was invisible to the software that depended on it.Supporting evidence that cuts the same way: SDF's own SCF Dashboard does not treat a Discord account as sufficient — its verified-member flow adds social verification and Stellar wallet auth on top.
Decisions
identify+guilds.members.read— neverguilds, which returns every server a user belongs to and whose partial guild objects carry neitherpendingnorjoined_at.verification_level: 2. Both checked once, at issuance.(usage plan, API key)with no principal that aggregates keys, so multi-key would force the aggregation the rework cap exists to avoid.Guild ID is per-environment SSM config: a
stellar_testguild for build and test, production flipped in 0170.Two corrections to text already merged
Both checked directly against AWS docs:
nameQueryis not documented as a prefix match. AWS's entire description is "The name of queried API keys." — no matching semantics at all. This makes the client-side exact-match filter in 0158/0160 load-bearing rather than defence in depth.Verification
Every URL cited across the five research notes was re-fetched and its quoted text compared against the original. Live endpoints were re-run independently — the Discord invite API and the SCF OAuth redirect chain matched byte for byte, and a snapshot taken three days earlier agrees on guild ID, verification level and screening. 17 sources archived.
One contradiction inside Discord's own documentation is recorded rather than resolved: the API reference and the support article disagree on what the MEDIUM 5-minute clock measures.
Downstream
0157-0164 and the epic updated. 0164 needed rewriting rather than extending — "run from a genuinely fresh account" would now fail by design, and its Tranche 3 evidence must follow 0170's SSM flip, because a flow gated on our private test guild is not functional for any outside developer.
Two tasks spawned: 0170 (SDF contact, verify against the real guild, flip SSM) and 0171 (measure seven undocumented Discord/AWS behaviours the design depends on).