1. auth
temper
  • Temper documentation
  • 2026-07-06-cogmap-telos-differentiation
  • handoffs
  • plans
    • archived
    • evidence
  • reviews
  • specs
  • spikes
  • agents
  • auth
    • Temper auth & security
    • The authorization seam (`temper-services::auth`)
    • Cognitive-map & resource authoring authorization
    • JWT verification
    • The issuer / resource-server boundary & the machine-token contract
    • The internal reconcile channel
  • code-reviews
  • cognitive-maps
  • decisions
  • development
  • guides
    • Building a cognitive map from a large corpus
    • Using temper from Claude Desktop (and claude.ai)
    • Cloud Agent Development Guide
    • Ingesting a corpus into a context
    • Development setup
    • Drain operator queries (TraceQL)
    • Enterprise Install — Ground Up
    • Operator guide: read-only GitHub credential via a BYO App + Vercel Connect
    • Operator guide: provisioning a GitHub connection in temper
    • Installing Temper
    • Delivering L0 Kernel Cogmap Content
    • Machine Credentials
    • OpenTelemetry Setup
    • Operational Memory
    • Bootstrapping a Temper org
    • Releasing Temper
    • Self-Hosting Temper with Okta
    • Self-Hosting Temper with a SAML IdP
    • Self-Hosting Temper
    • The Slack Integration — identity, credentials, and what revocation actually means
    • Operator guide: `@temper` on Slack — end-to-end setup
    • Bootstrapping a team's self-cognition cognitive map
    • Working with Teams
    • Integrating Ruby with temper-rb
    • Deploying an Eve agent to Vercel (the steward and the citation auditor)
    • WS6 Collapsed-Schema Dev Environment
  • research
  • security
  • specs
  • doors
    • Building against Temper
    • Running Temper
    • Using Temper
  • Temper /api/query — the compositional read surface
    • Run a declared composition of situated acts.
    • Schemas
      • Composition
      • OutcomeDeclaration
      • ReturnSpec
      • Intention
      • StageNode
      • ActInvocation
      • CombineNode
      • StageInput
      • StageRelation
      • Disclosure
      • QueryResponse
      • StageResult
      • StageOutput
      • ScoreKind
      • Scoring
      • ResourceHit
      • CompositionTrace
      • StageTrace
      • InputSource
      • NarrowedBy
      • StageName
      • ActName
      • IdKind
      • IdSet
      • BoundTerm
      • Extent
      • StageDisposition
      • ActRefusal
      • PlanRefusal
      • RefusalReason
      • ActQuantity
      • QuantityScale
      • ResourceFilter
      • EdgeFilter
      • PropertyPredicate
      • PropertyOp
      • ResourceSection
      • EdgeKind
      • OrdOp
      • CogmapId
      • ContextId
      • IdProvenance
      • FacetPredicate
      • CombineOp
      • BlockId
      • MatchLocation
      • BodyStorage
      • ResourceId
      • IngestState
      • ManagedMeta
      • ProfileId
      • ResourceView
      • Polarity
      • ViaEntry
      • LensId
      • RegionId
      • CogmapRegionRow
      • RegionHit
      • StageInputTrace
      • Standing
      • ActorAuthority
      • Refusal
      • SystemAccessDetails
      • PlanRefusalDetails
      • ErrorDetails
      • ErrorDetail
      • ErrorBody
  • Temper Cloud API
    • Access
      • POST /api/access/requests — submit a join request for the gating team.
      • GET /api/access/requests/me — check own join request status.
      • DELETE /api/access/requests/me — withdraw a pending join request.
      • POST /api/access/reviews — a revoked principal asks an admin to reconsider (spec D15).
      • GET /api/access/settings — read public system settings.
    • Slack Link
      • Disconnect any principal. Operator path — offboarding and stuck users.
      • Disconnect EVERY Slack principal bound to the caller's own profile.
    • Auditor
      • auditor_dispatch
      • auditor_sweep
      • complete_auditor_job
    • Graph
      • POST /api/cogmaps/{id}/graph/slice — R4 cogmap-scoped neighborhood slice.
      • GET /api/graph/cogmaps/{id}/panorama — enter-a-cogmap Tier-0 interior.
      • GET /api/graph/contexts/composition — Beat E Tier-1: the force-graph composition of a container's (or a residual bucket's) members.
      • GET /api/graph/contexts/panorama — Beat E Tier-0: goal-container territories + residual tray.
      • GET /api/graph/home — the you→teams→cogmaps membership home.
      • GET /api/graph/regions/composition — Beat D region→resources composition drill.
    • Cognitive Maps
      • list_cognitive_maps
      • genesis
      • get_cognitive_map
      • reconcile
      • analytics
      • grant_cogmap_access
      • revoke_cogmap_access
      • materialize
      • materialize_delta
      • region_metrics
      • shape
      • bind_team
      • unbind_team
    • Contexts
      • list_contexts
      • create_context
      • get_context
      • context_materialize
      • reassign
      • context_region_metrics
      • rename
      • context_shape
      • share_team
      • unshare_team
    • Events
      • cursor
      • GET /api/graph/elements/{kind}/{id}/trail — R5 element event-trail. kind ∈ {node, edge}.
    • Facets
      • set_facet
      • Read the live facets of one edge.
      • Set a facet whose owner is an **edge** rather than a resource.
      • Read the live facets of one resource — the confirming read for a write that steers region formation and Atlas grouping.
    • Health
      • health_check
    • Ingest
      • create_ingest
      • update_ingest
      • list_blocks
      • append_block
      • finalize_resource
    • Invitations
      • accept
      • decline
      • list_mine
      • list_team_invitations
      • revoke_team_invitation
      • create_team_invitation
    • Invocations
      • list_invocations
      • open
      • show
      • close
    • Profile
      • get_profile
      • update_profile
      • list_auth_links
    • Query
      • `POST /api/query`.
    • Relationships
      • assert
      • fold
      • retype
      • reweight
    • Resources
      • `GET /api/resources` — **one response type, unconditionally.**
      • create_resource
      • get_resource
      • delete_resource
      • update_resource
      • List the finding at `{id}`'s citation-audit trail — one row per audit, each naming its auditor.
      • Record an auditor's signed defensibility verdict on one `(block, source)` citation of the finding at `{id}`. CONFORM to `handlers::edges::assert` (the sibling authored-write handler): thin — build the command, dispatch it, map the error. No persistence he
      • get_content
      • list_resource_edges
      • resource_evidence
      • grant_resource_access
      • revoke_resource_access
      • resource_lineage
      • provenance
      • annotate_resource
    • Meta
      • get_meta
      • update_meta
    • Reassign
      • reassign_resource
      • reassign_team
    • Search
      • `POST /api/search`.
    • Steward
      • candidates
      • dispatch
      • sweep
      • delta
      • advance
    • Teams
      • list_teams
      • create_team
      • detail
      • delete_team
      • update_team
      • add_member
      • remove_member
      • change_role
    • Schemas
      • AcceptInvitationResponse
      • ActInput
      • ActInvocation
      • ActName
      • ActQuantity
      • ActRefusal
      • ActorAuthority
      • AddMemberRequest
      • AdvanceWatermarkAck
      • AdvanceWatermarkRequest
      • AgentAuthorship
      • AppendBlockPayload
      • AssertRelationshipRequest
      • AtlasEdge
      • AtlasHome
      • AtlasNode
      • AtlasSubgraph
      • AuditCitation
      • AuditSweepRow
      • AuditorDispatchTickRequest
      • AuditorDispatchTickResponse
      • AuditorJobCompleteAck
      • BindTeamOutcome
      • BindTeamRequest
      • BlockId
      • BlockProvenanceRow
      • BlocksResponse
      • BodyStorage
      • Bridge
      • BulkReassignAck
      • BulkReassignRequest
      • ChangeRoleRequest
      • CharterBlock
      • CharterDisposition
      • CitationAuditRequest
      • CitationAuditRow
      • ClaimedAuditJob
      • ClaimedJob
      • CloseInvocationRequest
      • CogmapAnalyticsRow
      • CogmapDetail
      • CogmapFoundationRow
      • CogmapGrantBody
      • CogmapId
      • CogmapRegionMetricsRow
      • CogmapRegionRow
      • CogmapRegulationRow
      • CogmapRevokeBody
      • CogmapRow
      • CogmapStaleness
      • CombineNode
      • CombineOp
      • Composition
      • CompositionTrace
      • ConfidenceBand
      • ContentResponse
      • ContextCreateRequest
      • ContextId
      • ContextOwnerRef
      • ContextPanorama
      • ContextRow
      • ContextRowWithCounts
      • CorrelationId
      • CreateCogmapOutcome
      • CreateCogmapRequest
      • CreateInvitationRequest
      • CreateRequestBody
      • CreateReviewBody
      • DeleteResponse
      • DeviceOverrides
      • DispatchTickRequest
      • DispatchTickResponse
      • Disposition
      • DriftSweepRow
      • EdgeFacetRow
      • EdgeFacetSetRequest
      • EdgeFacetsResponse
      • EdgeFilter
      • EdgeId
      • EdgeKind
      • ElementEvent
      • ElementKind
      • EmbedDispatchSummary
      • Entitlements
      • ErrorBody
      • ErrorDetail
      • ErrorDetails
      • EventCursorResponse
      • EventTrail
      • ExactArm
      • ExactHit
      • Extent
      • FacetAck
      • FacetPredicate
      • FacetSetRequest
      • FinalizePayload
      • FoldRelationshipRequest
      • GrantOutcome
      • GraphEdgeRow
      • GroupKeyMeta
      • HealthResponse
      • HomeCogmap
      • HomeContext
      • IdKind
      • IdProvenance
      • IdSet
      • IdpRevocation
      • IngestCreateResponse
      • IngestDelta
      • IngestPayload
      • IngestState
      • InheritedReadGrant
      • InheritedShare
      • InputSource
      • Intention
      • InvitationStatus
      • InvitationTokenRequest
      • InviteeInvitation
      • InvocationAck
      • InvocationActRow
      • InvocationCloseAck
      • InvocationId
      • InvocationSummary
      • InvocationView
      • JoinRequest
      • JoinRequestStatus
      • LensId
      • LineageNode
      • ManagedMeta
      • MatchLocation
      • MaterializeAck
      • MaterializeDelta
      • MaterializeRequest
      • MergePolicy
      • MetaUpdatePayload
      • NarrowedBy
      • NodeHome
      • OpenInvocationRequest
      • OrdOp
      • OrphanNode
      • OutcomeDeclaration
      • PlanRefusal
      • PlanRefusalDetails
      • Polarity
      • Profile
      • ProfileAuthLink
      • ProfileId
      • ProfileUpdateRequest
      • ProfileWithEntitlements
      • PropertyOp
      • PropertyPredicate
      • ProvenanceSource
      • PublicSystemSettings
      • QuantityScale
      • QueryResponse
      • ReassignAck
      • ReassignContextOutcome
      • ReassignContextRequest
      • ReassignResourceRequest
      • ReconcileCogmapRequest
      • ReconcileEdge
      • ReconcileEdgeTombstone
      • ReconcileEntry
      • ReconcileOutcome
      • ReconcileTelos
      • ReconcileTelosBlock
      • ReconcileTombstone
      • Refusal
      • RefusalReason
      • RegionHit
      • RegionId
      • RelationshipAck
      • RemoveMemberOutcome
      • RenameContextOutcome
      • RenameContextRequest
      • ResidualBucket
      • ResidualContext
      • ResidualGroups
      • ResidualOwnedReach
      • ResourceAnnotateRequest
      • ResourceCreateRequest
      • ResourceFacetRow
      • ResourceFacets
      • ResourceFacetsResponse
      • ResourceFilter
      • ResourceGrantBody
      • ResourceHit
      • ResourceId
      • ResourceLineage
      • ResourceListResponse
      • ResourceRevokeBody
      • ResourceSection
      • ResourceSortField
      • ResourceUpdateRequest
      • ResourceView
      • ReturnSpec
      • RetypeRelationshipRequest
      • RevokeOutcome
      • ReweightRelationshipRequest
      • ScoreKind
      • Scoring
      • SearchParams
      • SearchReason
      • SearchResponse
      • SearchResultRow
      • SearchScope
      • SearchScopeInfo
      • SegmentInfo
      • SegmentedBegin
      • SegmentedBeginResponse
      • ShareContextOutcome
      • ShareContextRequest
      • SlackDisconnectRequest
      • SlackDisconnectResponse
      • SlackDisconnectedPrincipal
      • SliceRequest
      • SortOrder
      • StageDisposition
      • StageInput
      • StageInputTrace
      • StageName
      • StageNode
      • StageOutput
      • StageRelation
      • StageResult
      • StageTrace
      • Standing
      • StandingShape
      • Subscription
      • SubscriptionOverride
      • SystemAccessDetails
      • TeamCreateRequest
      • TeamDetail
      • TeamInvitation
      • TeamMemberDetail
      • TeamMemberRow
      • TeamMemberSource
      • TeamRole
      • TeamRow
      • TeamUpdateRequest
      • Territory
      • TerritoryKind
      • TerritoryOverview
      • UnbindTeamOutcome
      • UnshareContextOutcome
      • VaultConfig
      • ViaEntry
      • WideArm
      • WideHit
  1. auth

The internal reconcile channel

When the Temper Authorization Server (native SAML) is about to mint a token, it first calls
temper-api at POST /internal/saml/reconcile to reconcile the user's team memberships
from their SAML-asserted groups. This is a server-to-server call between two co-deployed
siblings — not a browser-facing endpoint, not a JWT path.
Source: crates/temper-api/src/middleware/internal_auth.rs (the HMAC gate),
crates/temper-api/src/handlers/internal_saml.rs (the handler). Operator setup:
../guides/self-hosting-saml.md.

Trust model: an HMAC signature over the body#

The AS signs each request with HMAC-SHA256(secret, "{timestamp}.{raw_body}") and sends two
headers; the API (require_internal_signature) recomputes the MAC over the bytes it received
and rejects a stale timestamp. Two wins over sending the secret in a header:
The secret never crosses the wire. Only a signature derived from it travels, so a
captured request never leaks the secret.
Captured requests are replay-proof. The signed timestamp must be within ±30s of the
verifier's clock (temper_core::internal_sig::MAX_SKEW_SECS); a replayed request is stale.
HeaderValue
X-Temper-TimestampUnix seconds the signature was computed at
X-Temper-Signaturelowercase-hex HMAC-SHA256(secret, "{timestamp}.{body}")
Still fail-closed: if INTERNAL_RECONCILE_SECRET is unset the endpoint is disabled and
every request is rejected, so an unconfigured instance simply does no group provisioning
(authentication still works). The AS and the API share one Vercel project env, so the secret
is the same value on both by construction.
We MAC the raw body bytes, not a re-serialized form. The signer HMACs the exact JSON
bytes it sends; the verifier buffers the exact bytes it received and HMACs those (before
deserializing). Because both operate on identical bytes, there is no cross-language
canonicalization to drift on — the same discipline every major webhook signature uses
(GitHub X-Hub-Signature-256, Stripe). The signing scheme lives once in
temper_core::internal_sig (shared home for the header names, the message format, and the
skew window); the TS signer (packages/temper-cloud/src/oauth/reconcile.ts) and the Rust
verifier are pinned together by a shared known-answer test vector (asserted in both
internal_sig.rs and tests/oauth/wire-contract.test.ts), so they cannot drift on the HMAC
construction. Rejection is verified end-to-end for wrong-secret, stale-timestamp, and
tampered-body in crates/temper-api/tests/internal_saml_test.rs.

Secret strength & rotation#

Length. Use a INTERNAL_RECONCILE_SECRET of ≥32 random bytes
(e.g. openssl rand -hex 32). temper admin saml provision generates a strong one.
Rotation. Because both functions read the secret from one shared Vercel project env,
rotation is a single atomic swap: generate a new secret, set it on the project env, and
redeploy both functions together. There is no dual-secret overlap window to manage —
reconcile is fail-open at the ACS handler, so the brief redeploy gap at worst delays group
provisioning until the next login, never blocks authentication.

Why not an origin allow-list on Vercel#

The instinctive control — "only accept this call from our own AS's origin/IP" — is
security theater on Vercel serverless, for three concrete reasons:
1.
A server-side fetch sends no meaningful Origin. Origin is a browser
same-origin-policy artifact; the AS's outbound call is not a browser request, so there is
no Origin header to allow-list.
2.
Egress IPs aren't pinnable. Serverless functions egress from a shifting pool of
addresses; there is no stable source IP to allow.
3.
The two siblings share a deployment, not a network boundary. They co-deploy in one
Vercel project but there is no private network segment between them to gate on.
So the secret itself is the sibling-trust signal — it is the only thing that reliably
distinguishes "our AS" from any other caller in this topology. An IP/origin allow-list would
add ceremony and zero real assurance.
A true network boundary (making the API non-publicly-routable) is explicitly out of
scope
: the same API also serves public OAuth/SAML endpoints, so it must stay reachable, and
private networking is Enterprise-tier Vercel. Not worth it versus hardening the secret.

The federated path through the seam#

The handler does two things: resolve the profile, then reconcile its idp memberships. The
resolve half goes through the authorization seam's federated
entry point
, resolve_federated_human — this endpoint was the third site hand-building a
PrincipalKind::Human, and a surface that can construct one can forge one.
Three properties are worth naming, because they are what make a non-JWT path safe to have at
all:
There is nothing to classify. The assertion was already authenticated server-to-server
by the co-deployed AS (the HMAC above) before the token is minted. So this path skips
classify and the email ladder — the email is asserted, not resolved — and only
resolves-or-JITs the profile the minted token will later resolve to.
provider is server config, never a payload field. The seam is handed
state.config.auth_provider_name, so the profile this endpoint resolves is the same one
authenticate_token will resolve the AS's freshly minted token to. A payload-supplied
provider would let the caller land the assertion on a different profile.
The machine gate still covers it. resolve_human_from_claims's machine-shape guard is
the second, independent layer (see the machine-token contract),
and it sits on this path too: an assertion carrying an @clients-suffixed
external_user_id is refused here exactly as it is everywhere else.

Bounded blast radius#

Even if the endpoint were reached by an attacker, the damage is bounded by design:
It can only apply operator-pre-configured kb_saml_group_mappings — never arbitrary
grants. An attacker cannot invent a team or a role; they can only trigger the mappings an
operator already wrote.
It never touches native memberships (added in-app or by join-request approval) or
auto-join teams — reconcile manages only source='idp' rows.
It never deletes or deactivates a profile. It can JIT-create one — the same
resolve-or-JIT the token path performs on a first sign-in, for an identity the AS is about
to mint a token for anyway. So the worst case is a spurious profile with only
operator-mapped idp memberships, not an escalation of an existing one.

Further hardening: edge rate-limiting#

The HMAC signing above (auth-seam plan's Stage 3) is the load-bearing control and is shipped.
One operational companion remains operator config, not code: edge rate-limiting on the
reconcile path via Vercel Firewall/WAF, to blunt brute-force or flooding against the endpoint.
Configure it per instance; the code path does not enforce it.
Out of scope (explicit): a true network boundary (making the API non-publicly-routable).
The same API also serves public OAuth/SAML endpoints, so it must stay reachable, and private
networking is Enterprise-tier Vercel — not worth it versus the HMAC signing already in place.
Spec: internal/superpowers/specs/2026-07-02-shared-auth-orchestration-seam-design.md
(Stage 3).
Modified at 2026-08-19 17:59:52
Previous
The issuer / resource-server boundary & the machine-token contract
Next
Building a cognitive map from a large corpus
Built with