Add SaaS pairing-code handshake endpoints#18
Merged
andrew-jon-p7a merged 3 commits intomainfrom Apr 23, 2026
Merged
Conversation
Three endpoints on the OSS side so member identity can flow OSS →
SaaS instead of being asserted by a SaaS account holder:
- POST /saas-connect/bind (auth-required)
Stores (code → memberName) in a process-local Map with 10-min
TTL. Member is read from the authenticated `LoadedMember`.
- GET /saas-connect/lookup?code=X (no auth)
Returns { memberName } if bound, 404 otherwise. Single-use:
consumed on read so a stale SaaS retry (or any replay) fails.
- GET /setup/connect-saas?code=X (no auth at the request layer;
client-side probes /session)
Vanilla-HTML confirmation page. If signed in, shows a
"Authorize AgentC7 SaaS as <member>" button that POSTs to
/saas-connect/bind with cookies. If not, shows a sign-in link
that preserves the code. Self-contained — no @agentc7/
web-shell changes so the SaaS-embedded shell doesn't
accidentally inherit the route.
Trust model (see updated docs/self-hosted-connect.mdx): the SaaS
never asserts who you are on your ac7. A SaaS user hands you a
pairing code; you confirm on your server while signed in as the
real member; the SaaS's status endpoint polls a callback to learn
which member you bound. Mallory with a SaaS account can't
impersonate Alice on acme-ac7.com because Mallory can't sign into
acme-ac7.com as Alice to complete step 2.
Tests: bind requires auth; lookup is single-use; TTL sweeps expired
bindings; HTML page embeds the code safely (HTML-escaped) and
wires /session + /saas-connect/bind. 10 new cases in
apps/server/test/saas-connect.test.ts.
Also updates the self-hosted-connect docs to describe the new
three-step handshake and drop the old "type your member name"
instructions.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
CLI (ac7-connect-saas)
* New binary `ac7-connect-saas --url <url> --member-name <name>
[--saas <origin>] [--config-path <path>]`. Runs OAuth device flow
against the SaaS: /register/start to get codes, prints the
verification URL + user code, polls /register/status until
authorized, writes the returned jwt block to a sidecar overlay
file.
* Second tsup entry + package.json bin alongside ac7-server.
* Overlay write is atomic (write→fsync→rename→chmod 0600).
Config overlay (members.ts)
* Primary `config.json` loader now checks for a sibling
`<base>.saas.json` and merges its `jwt` block into the loaded
config with overlay-beats-primary precedence.
* Overlay schema is narrow — only `{ jwt: {...} }` — so future
SaaS-managed settings can be added deliberately rather than
leaking through.
* User's primary config is never mutated by the CLI; removing
the overlay disables the SaaS connection cleanly.
* New exported helper `saasOverlayPathFor(configPath)` keeps path
derivation centralized (used by the CLI and the loader).
/setup/connect-saas iframe mode
* Page now accepts `?mode=iframe&parentOrigin=<url>` in addition
to tab mode. Render-time validates parentOrigin against the URL
parser; invalid values fall back to tab mode rather than using
a wildcard postMessage target.
* On successful bind, iframe mode postMessages
`{ type: 'saas-connect-bound', code, memberName }` to the
validated parent origin. Tab mode still renders the "close this
tab" copy.
* Used by the SaaS invite-redeem iframe flow — the SaaS-embedded
iframe listens for the postMessage + calls its own
/redeem/complete endpoint to exchange the code for the
OSS-attested memberName.
Docs
* Rewrote self-hosted-connect.mdx around the new architecture:
server registration via CLI, invite-based teammate join via
iframe, disconnect-from-either-side semantics, troubleshooting.
Tests
* 3 new iframe-mode cases in saas-connect.test.ts covering
postMessage wiring, parentOrigin validation, and the garbage-
parentOrigin fallback to tab mode.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- escape shadowed the `escape` global; biome's noShadowRestrictedNames flagged it. Renamed to escapeHtml which is clearer anyway. - connect-saas.ts: biome formatter preferred broken-out signatures for a couple of multi-line fns.
andrew-jon-p7a
added a commit
that referenced
this pull request
Apr 29, 2026
Resolves textual conflicts in the import sort order of three files where my new enrollment-related imports landed adjacent to imports added by the SaaS pairing PR (#18) and dusk styling PR (#23): - apps/server/src/app.ts (3 conflict blocks, all kept-mine) - packages/sdk/src/client.ts (4 conflict blocks, all kept-mine) - apps/server/test/channels-endpoints.test.ts (3 blocks, kept-mine) All resolutions are pure "keep my additions" — origin/main has nothing in those positions, the conflicts were textual artifacts of git's auto-merge giving up on adjacent alphabetized sorts. Also pulled in the dependabot-merged @hono/node-server 2.0.0 bump (#22). Verified no breaking-change impact: the serve() factory pattern we use is stable across the major version. All 717 tests pass and biome is clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Signed-off-by: andrew-jon-p7a <andrewprzybilla@gmail.com>
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.
Summary
Three endpoints so the OSS side is authoritative for member identity in the SaaS self-hosted connect flow:
Self-contained HTML — no `@agentc7/web-shell` changes. Keeps the SaaS-embedded shell from accidentally inheriting the route.
Trust model
SaaS user hands you a pairing code. You confirm the binding on your ac7 while signed in as the real member. Mallory with a SaaS account can't impersonate Alice on your server because she can't sign into your server as Alice to complete step 2. The callback endpoint is single-use, which removes replay as an attack vector.
Pair
Platform PR: https://github.com/agentc7/platform/pull/new/feature/pairing-code-connect — the SaaS side of the handshake + the SPA flow.
Test plan
🤖 Generated with Claude Code