Skip to content

Add the client-set Edge Cookie value path - #1346

Open
jwrosewell wants to merge 60 commits into
feature/3-permissionsfrom
feature/4-client-resolve
Open

jwrosewell wants to merge 60 commits into
feature/3-permissionsfrom
feature/4-client-resolve

Conversation

@jwrosewell

@jwrosewell jwrosewell commented Oct 14, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Changes

Area Change
crates/trusted-server-core/src 9 files, 2333 lines added, 89 removed
crates/trusted-server-js/lib 2 files, 217 lines added, 0 removed
crates/trusted-server-adapter-fastly/src 1 file, 34 lines added, 2 removed
docs/guide 1 file, 12 lines added, 0 removed
trusted-server.example.toml 1 file, 10 lines added, 0 removed
crates/trusted-server-core/Cargo.toml 1 file, 3 lines added, 0 removed
crates/trusted-server-adapter-spin/src 1 file, 2 lines added, 1 removed
AGENTS.md 1 file, 1 line added, 1 removed

Closes

Closes #778

Spec

Spec: client-cycle-ec-resolve

Test plan

CI on this pull request at 04d9284 passed all 18 checks.

  • cargo test-fastly && cargo test-axum
  • cargo clippy-fastly && cargo clippy-axum
  • cargo fmt --all -- --check
  • JS tests: cd crates/trusted-server-js/lib && npx vitest run
  • JS format: cd crates/trusted-server-js/lib && npm run format
  • Docs format: cd docs && npm run format
  • WASM build: cargo build --package trusted-server-adapter-fastly --release --target wasm32-wasip1
  • Manual testing via fastly compute serve
  • Other: the full workflow set of this repository, run on this pull request at 04d9284

Checklist

  • Feature PRs: spec linked above, ending in a review log of two or more review rounds
  • Changes follow AGENTS.md conventions
  • No unwrap() in production code (clippy denies unwrap_used in CI)
  • Uses log macros as AGENTS.md asks (not println!)
  • New code has tests
  • No secrets or credentials committed

Stacks on the permission model (#1345), and depends on it because the resolve
endpoint applies the same permission gate as organic generation. Through the
stack this pull request also depends on the Edge Cookie module seam (#1343),
whose EdgeCookieModule trait gains the client resolve method here. It is the fourth of the five pull requests (#1343 to #1347) that decompose
the combined module change (#838), as the review of the earlier design specs
(#986) asked.

The design specs for the series are carried by the spec pull request (#1342).
The spec for this pull request is
2026-07-30-client-cycle-ec-resolve-design.md,
the threat-model draft revised to the implemented state. Its revision record
maps each requirement to what v1 implements and what deliberately waits for the
first vendor scheme.

What changed after the 51Degrees PR review

Agreed at the review on 23 October, point 1. No configuration or permission policy is compiled into the WASM, and a WASM without a policy does not start. Since #1345 a loaded domain carries its permission policy and every services graph needs one, so the tests below give theirs the sample policy.

  • The resolve tests' services carry the sample policy.

Kept current

Why this path matters

A client-cycle module establishes the identifier through a browser round
trip. The page script obtains or derives a value, which for a real vendor is a
signed envelope from that vendor's identity system, posts it to
POST /_ts/api/v1/ec/resolve, and the module verifies that value before the
edge sets it as the Edge Cookie. The vendor Edge Cookie module proposed in the
51Degrees module issue (#1072) works client-side by design, so this endpoint is
on the series' critical path rather than deferred.

What this pull request does

  • EdgeCookieModule gains resolve_from_client with a default that creates
    nothing, so server-side modules are untouched.
  • The resolve endpoint enforces the same rules as organic generation plus its
    own. The permission gate applies unchanged, being the module's complete
    declaration rather than a hard-coded storage check, and the module's
    response headers pass the same reserved-surface check before any is kept. The
    request must carry an Origin on the publisher's domain (403 otherwise) and
    a text/plain or application/json body (415 otherwise, 413 over 64 KiB,
    refused from the advertised Content-Length before the body is read). A
    created identifier must fit the global identifier bounds (400) and must not
    silently replace a different identity already on the request (409).
  • The endpoint answers with the module the request's own context selected
    when the page was served, so a module an adapter threads into the request
    is the one that verifies the posted value.
  • A created identifier is persisted to the identity graph before the cookie is
    set, keyed by the module's canonical form, so withdrawal reaches a
    client-set identity the same way it reaches an edge-created one. With no
    graph available the endpoint sets no cookie at all, which is stricter than
    organic generation, where the identifier is kept and only the row write is
    skipped. A graph write failure answers 503 with no cookie. A closed
    permission gate, no configured module, no graph, or a module that creates
    nothing answers 204 with no Edge Cookie. The module's own response headers
    go out only with the 200 that sets the cookie, or with the 204 when the
    module creates nothing. Every response the handler builds carries
    Cache-Control: no-store, and a module or configuration error goes to the
    adapter's error response instead.
  • The Edge Cookie stays HttpOnly. A non-HttpOnly companion marker cookie
    (ts-ecr=1, no identity content) tells the page script a resolve succeeded,
    fixing the earlier defect where the script checked for a cookie it could
    never read and so posted on every page view. The marker expires together with
    the Edge Cookie on withdrawal, and when the request carries an Edge Cookie
    the selected module does not own, so a switch between client-cycle modules
    resolves again. A Rust test checks that the marker name and the
    demonstration's fixed word match the page-script source.
  • [ec] resolve_allowed_origins lists the extra origins the endpoint accepts
    beyond the publisher's own, each a bare origin, checked when the settings
    load.
  • The client_fixed demonstration module is compiled only behind the
    client-fixed-demo cargo feature, and production builds reject the selection
    at startup. A fixed shared word is not an identity. The demonstration
    module's cookie carries the registry code (cfix~an-ec), so even
    demonstration identities are module-namespaced. The cargo feature keeps its
    own spelling, because a build feature is not something a deployment writes
    in trusted-server.toml.
  • The Fastly adapter routes the endpoint with the same bot-gated identity graph
    as generation. The other adapters deliberately do not route the endpoint yet,
    matching identify and batch-sync, which need the same platform key-value
    wiring those adapters lack. The Spin adapter's route list records it.
  • The full anti-replay reservation design from the spec is deliberately not in
    v1. The reservation design needs the real vendor envelope's unique id and
    session binding, and the spec keeps the design verbatim as the bar for that
    implementation.

How it was verified

Endpoint tests in
crates/trusted-server-core/src/ec/resolve.rs
cover the origin, content type, advertised and actual body size, identifier
bounds, conflict, no-graph, failed graph write, marker and threaded-module
behaviors, and client_set_value_round_trips_through_the_ec_scenario drives a
client identifier through deferral, resolve, cookie set and verbatim
read-back.

References #778. Decomposes #838. Spec baseline from #986.

Fourth slice of the PR 838 decomposition. A client-cycle Edge Cookie
provider defers at the edge and lets the page derive the identifier in
the browser; the page posts it to POST /_ts/api/v1/ec/resolve and the
provider verifies it before the edge mints it as the Edge Cookie:

- EdgeCookieProvider gains resolve_from_client with a no-op default, so
  server-side providers are untouched. ClientResolveInput carries the
  posted payload plus the request's resolved permissions and consent.
- The resolve endpoint enforces the same rules as organic generation
  and several of its own. The permission gate applies unchanged. The
  request must carry an Origin on the publisher's domain (an
  identity-setting POST must not be drivable from a foreign page) and a
  text/plain or application/json body. A minted identifier must fit the
  identifier bounds (400), must not silently replace a different
  identity already on the request (409), and is persisted to the
  identity graph before the cookie is set, keyed by the provider's
  canonical form, so withdrawal reaches a client-set identity the same
  way it reaches an edge-minted one. With no graph available nothing is
  minted, matching the organic rule against phantom cookies; a graph
  write failure is 503. Every response carries Cache-Control: no-store.
- The Edge Cookie stays HttpOnly. A non-HttpOnly companion marker
  cookie (ts-ecr=1, no identity content) tells the page script a
  resolve succeeded, so it does not re-post on every page view; the
  marker expires together with the Edge Cookie on withdrawal. A Rust
  test asserts the marker name and the demo's fixed word stay in sync
  with the page script source.
- The client-fixed demonstration provider (fixed shared word,
  verify-before-mint) is compiled only behind the trusted-server-core
  client-fixed-demo cargo feature. Production builds reject the
  selection at startup: a fixed shared word is not an identity.
- The Fastly adapter routes the endpoint and passes the same bot-gated
  identity graph as generation, so unrecognized clients cannot mint
  through resolve either. The other adapters deliberately do not route
  it yet, matching identify and batch-sync, which need the same
  platform KV wiring those adapters lack.

The design spec for this slice lives at
docs/superpowers/specs/2026-07-30-client-cycle-ec-resolve-design.md,
the 2026-07-31 threat-model draft revised to the implemented state,
with the deferred reservation design retained verbatim as the bar for
the first vendor scheme.
The spec used em dashes throughout; they are replaced with plain
punctuation. The resolve handler's doc claimed every response carries
Cache-Control: no-store, but a provider or configuration error leaves
through the error path to the adapter's own response, so the doc now
says every response the handler builds.
`ec/provider.rs` documents that core checks every provider response header
against its reserved surface before applying it, so a provider may set its
own cookies and headers but cannot reach into the surface core manages.
The organic mint path honours that in
`EcContext::generate_with_provider`. The resolve endpoint applied provider
headers with no check at all, on both the 204 path where the provider
mints nothing and the 200 path where it does.

That let a browser-side provider set the managed `ts-ec` cookie directly
and bypass the identifier bounds check, the conflict check and the
row-before-cookie rule that sit a few lines below it. Removing the new
check and re-running the tests shows the 204 response going out as
`204` with `set-cookie: ts-ec=forged-value; Path=/` and no identity-graph
row behind it, which is an external reviewer's own finding on this stack,
answered on the Edge Cookie provider branch and reintroduced here. His
words were that a provider can set the managed cookie "including when it
returns no identifier", which is exactly that path.

The check is the same call the organic path makes, placed immediately
after `resolve_from_client` and before the identifier is read, so one
check covers both paths for the same reason the organic one sits there: a
provider can return headers with no identifier at all. A breach fails the
request rather than producing a status, matching the organic path and the
module's existing rule that a provider error propagates to the adapter's
error response, because a provider reaching into the reserved surface has
broken its contract rather than sent a bad request.

Tested on both paths: a managed `ts-ec` cookie with nothing minted, and an
`x-ts-ec` header on the minted path.
The two sites in the resolve handler had the same defect the EC
finalization loop had: `HeaderMap::insert` replaces every existing value
for a header name, so a provider setting more than one cookie kept only
the last, and a provider setting a header this handler already wrote
silently replaced it.

On this path the replaced header is core's own rather than an origin's,
and the one that matters is `Cache-Control: no-store`. Every response the
handler builds carries it, because an identity response must never be
cached, and an inserting write let a provider drop it. Removing the fix
and re-running the new test shows both: the provider's first cookie gone,
and `no-store` replaced by the provider's `max-age=600`.

Both sites now go through the shared `apply_provider_response_headers`,
which carries the rule and is the same helper EC finalization uses, so the
two paths cannot drift.
`ProviderCode::new` is fallible and returns `Option<ProviderCode>`, but
this test provider passed its result straight back from `code()`, which
returns `ProviderCode`. The test module therefore did not compile.

Nothing caught it because the only checks run against this branch were
`cargo check`, which builds the library and not the `#[cfg(test)]`
modules, so the branch looked healthy while its own tests could not be
built.

The macro is the established form for a fixed code and is already used
by the neighbouring test provider in this same file. It resolves the
code in a `const` block, so a malformed code fails the build rather than
the request, and there is no `Option` left to unwrap.

Addresses: crates/trusted-server-core/src/ec/resolve.rs, where the
`ResolveHeaderProvider` test double could not compile.
The Edge Cookie context carried the raw injected provider so the resolve
endpoint could rebuild it later. That slot is gone, so the context now
carries the provider the composition root threaded, which is the one seam
section 3.6 specifies.
…e resolve cycle on a provider switch

Two review findings from Christian Pavilonis on #1084.

The resolve endpoint authorized any origin that was a suffix match on the
publisher domain, and it also accepted an http:// origin and discarded the
port before comparing. The Edge Cookie is scoped to the parent domain, so a
delegated or compromised sibling subdomain could fix an identity that lands
on the apex and every sibling with it. Authorization is now a comparison of
the whole serialized origin, being the scheme, the lowercased host and the
effective port. The default accepted origin is https://<publisher.domain>
and nothing else, and [ec] resolve_allowed_origins adds further exact
origins for a publisher whose pages are served elsewhere.

The resolved marker is not namespaced by the provider code the way the
cookie value, the identity-graph key and withdrawal are, so after a switch
between client-cycle providers it outlived the identity it was set for and
the new provider's page script would skip the resolve it should perform.
Core now expires the marker whenever the request carries an identifier the
selected provider does not own, which does not depend on any vendor page
script comparing a marker value correctly.
The client resolve endpoint compared the request Origin and each allowed origin as lowercased strings, which matched what browsers send but was not the RFC's rule, so a configured https://www.example.com:443 could never match the https://www.example.com a browser serializes. origins_match now parses both sides as RFC 6454 §6.1 serialized origins and compares the scheme, host and port triple of §4 under the §5 same-origin test, with a missing port standing for the scheme's default and the opaque null origin never matching. A unit test covers the equal and unequal cases, and the settings and example config wording cite the RFC.
The client-fixed page script posted the known word whenever the resolved marker was absent, relying on the server's gate alone. A page module is treated like any other provider: it declares the permissions it requires and checks them against the resolved state the server hands the page before it does anything. The script now declares necessary.operations.storage, the same Data Use the server-side provider requires, waits on tsjs.whenPermissions() so the check works whether the state arrived at head open or at the body seam, and posts only when that permission is set. With no permission state on the page it does not post. The server still enforces the same gate on the resolve endpoint, so this is the page's half of one decision rather than a second policy.

The Rust test that keeps the script's constants in step with the provider now also asserts that REQUIRED_PERMISSION matches Permission::StoreOnDevice.as_str(). Vitest covers the permission unset, no state, and state-arrives-later cases.
Brings the permission signal work on split/3-permissions up to this
branch, so the client-set value path builds on the permission model as
it now stands. The merge has no conflicts.
Brings the documentation and assertion message fixes up from
split/3-permissions. The merge has no conflicts.
Brings upd/split/3-permissions at 3c9f4b0, which carries main at
066ea3c through split/1, split/2 and split/3, into
split/4-client-resolve. Two files conflicted. Each resolution keeps both
sides' changes.

ec/finalize.rs takes main's imports for identity-graph writes and EID
collection, and keeps this branch's expire_ec_resolved_marker beside
them. This branch's two resolved-marker tests from ee07418 sit beside
main's orphan-recovery tests from #885, and now pass main's mutable
context to ec_finalize_response.

docs/guide/api-reference.md keeps this branch's section for the
POST /_ts/api/v1/ec/resolve endpoint and drops the POST /third-party/ad
section, which main's #1016 removed.
Brings upd/split/3-permissions at e3bb4b9 into split/4-client-resolve.
That carries the split/1 review follow-ups (0980b73, 0e7f7eb,
fb1bc28, cb6f717 and 1b88e42), their merge into split/3 under
that branch's permission gate, and split/3's be0ed2d, which lets the
Cloudflare and Spin startup tests from main's #1016 load under the
single-jurisdiction rule. This branch carries the same four settings
blocks, so it takes that fix here.

The merge had no conflicts. The resolve endpoint already applies a
provider's response headers only with the 200 that sets the cookie or
the 204 when the provider creates nothing, which is the rule cb6f717
brings to organic generation, so nothing in resolve.rs changes.

Tests. The merged tree passes a native all-targets check, 2,885 core
tests, the Axum, Cloudflare and Spin adapter tests, the permission
signal crate tests, and the Fastly, Cloudflare and Spin wasm checks.
Brings upd/split/3-permissions at 88a7482 into split/4-client-resolve. That
carries the one commit which makes `tools/permissions-inspector/wasm` pass the
request evidence and the signal providers to `assemble_permissions`, and which
formats the crate. This branch carries the same inspector and the same
`Permissions Inspector` workflow, so it failed the same way.

The merge had no conflicts and touches only the four files under
`tools/permissions-inspector/wasm`.

Tests. `./scripts/build-inspector-wasm.sh` builds clean for
wasm32-unknown-unknown on this tree, and `cargo fmt --check` on the crate is
clean.
Brings upd/split/3-permissions at a2a1dfc into split/4-client-resolve. That
renames `config/permissions/vanilla.yaml` to `config/permissions/sample.yaml`
and says in the file, in the guide and on the constant that the sample is for
testing and evaluation only, is not a production policy and is not legal
advice.

The merge had no conflicts. `trusted-server.example.toml` auto-merged, because
this branch changes other parts of the same file.

Tests. `cargo fmt --all --check` is clean and the core suite passes 2,885
tests.
Every name an operator types into configuration is snake_case, so the
demonstration provider is now selected with `[ec] provider = "client_fixed"`.
The key constant, the two startup messages that spell the key, the page
script's log lines, the cargo feature comment, the provider table in AGENTS.md
and the resolve endpoint in the API reference all follow. This commit changes
only that one name.

The cargo feature that compiles the provider in keeps the name
`client-fixed-demo`, because a cargo feature name is not a configuration value.
The tsjs module id `ec_client_fixed` is unchanged as well, being already
snake_case and never compared against the key.

The old name is refused when settings load, which every adapter does through
`settings_from_config_blob` before it serves a request. The message names
`client_fixed` and says what to write, so a deployment still configured with
`client-fixed` stops at startup rather than being told to add a provider block
that this provider does not need. A block written under the old name does not
make the old name acceptable either, because the name is refused before any
block is looked for. That is a breaking change, accepted for a major release.

Tests. `cargo fmt --all -- --check` is clean and `cargo clippy -p
trusted-server-core --all-targets --all-features -- -D warnings` passes. The
core suite passes 2,886 tests and 8 doc-tests, with and without the
`client-fixed-demo` feature, the new test among them. The Axum adapter passes
62 tests, `cargo clippy-axum` is clean, and `cargo check -p
trusted-server-adapter-fastly --target wasm32-wasip1` builds. The page
script's own vitest file passes 6 tests and ESLint is clean on it.
…rom split/3

Brings up the Edge Cookie provider block layout from split/1, the
host_signals rename from split/2 and the permission signal provider key
from split/3, under the client_fixed rename this branch already carried.
All four are the same decision, so this branch's demonstration provider
now sits in the same layout as the rest.

Two files conflicted.

crates/trusted-server-core/src/ec/provider.rs. Both branches added
constants and resolution arms. The client_fixed constants and its
resolution arm are kept, listed in BUILTIN_PROVIDER_KEYS beside the two
providers that derive an identifier at the edge, under split/3's wording,
which calls these implementation ids rather than names. The arm now
matches on the implementation the selected block names rather than the
selector itself, which is how the other arms read since the block layout
landed, so the demonstration provider can be configured under a label as
any other provider can.

check_named_provider_configuration needed a real decision. This branch
wrote it to answer two questions, which are whether the build compiles a
name in and whether the name needs a settings block, the second by
looking every name up in [ec.providers]. The block layout removed that
lookup and moved the block question back into the settings, where it is
now asked only of the implementations core knows take settings, because a
provider an adapter injects has its block read by that adapter and core
cannot say whether it needs one. Reinstating a block requirement for
every name would undo that, so the function keeps only the question this
branch added it for, which is whether the demonstration provider is
compiled into this build, and the settings ask the block question. The
function's own reasoning still holds for what it keeps, which is that
only the resolution knows what is compiled out.

The refusal of the old client-fixed spelling moved with it into the
settings, beside the refusal of the old host-signals spelling, and both
now run before anything else. They have to, because the block layout
holds every provider name to snake_case and both old spellings break that
rule, so leaving either refusal later meant an operator got the general
rule instead of the message naming the spelling to write. Both refusals
cover a block left behind under the old name as well as the selector,
through one names_retired_provider helper.

crates/trusted-server-core/src/settings.rs. The Ec struct keeps this
branch's resolve_allowed_origins field and loses the providers field the
block layout replaced. resolve_allowed_origins is a fixed key of the [ec]
section, so it joins EC_SECTION_KEYS, without which the section would read
it as a provider block.

Four of this branch's tests followed the new layout. The one that proves
a provider with no block validates now says why the settings no longer
demand one, the old-spelling test builds its configuration through the
[ec] section helper rather than by deleting the hmac block by text, the
missing-block error names [ec.hmac], and the stale-block test builds its
block through the shared test helper.

Tests. cargo fmt --all -- --check is clean and cargo clippy -p
trusted-server-core --all-targets --all-features -- -D warnings passes.
The core suite passes 2,909 tests and 8 doc-tests with 4 ignored. The
Axum adapter passes 64 tests across its five binaries, cargo clippy-axum
is clean, and cargo check -p trusted-server-adapter-fastly --target
wasm32-wasip1 builds.
Carries up the resolutions from split/3. Nothing on this branch touches the
code main changed, so the merge needed no decision of its own.
Brings the refreshed permission model branch, and through it the device
and geo seams, the Edge Cookie seam and upstream main at 7a0ecb4, into
the client-resolve branch. The lower branches' names say module, so every
line here that used one of those names says module too, and most
conflicts settled as that rename applied to this branch's own text. The
rest, settled by hand:

- The Fastly entry point keeps this branch's resolve route import beside
  upstream's EcContext and EidSyncSource imports, and constants keeps the
  resolved-cookie marker beside upstream's pull-complete cookie.
- The seam keeps this branch's client_fixed implementation id and its
  retired spelling, with the built-in list naming all three ids, under the
  trimmed docs the seam now carries.
- The settings keep this branch's refusal of the client-fixed spelling and
  its startup compilation check, with the host-signals refusal gone as the
  device and geo branch removed it.

This branch's own names still say provider, and are renamed in a commit
of their own.
An [ec] resolve_allowed_origins entry such as "https://www.example.com/"
loaded cleanly, then matched no request, because origins_match treats
any value that is not a serialized origin as matching nothing. Every POST
to the resolve endpoint from that origin answered 403, with only a warn
line saying why.

The parse inside origins_match is now parse_serialized_origin in
ec/resolve.rs, and it returns the reason a value is not a serialized
origin. origins_match reads the request's Origin and each entry through
it, and Ec::validate_resolve_allowed_origins refuses an entry it rejects,
naming the entry and the reason. The check runs in
Settings::finalize_deserialized beside the module selection check, which
covers from_toml, from_json_value and the runtime config store load, and
in validate_settings_for_deploy_with, so ts config validate and ts config
push refuse the entry before it can stop an instance loading its
settings.

parse_serialized_origin is stricter than the old closure on both sides.
It requires http:// or https:// and then nothing from a slash, a
backslash, a question mark, a hash, an at sign, whitespace or a control
character before handing the value to the url crate for the host and
port. The old closure let through userinfo, the ftp, ws and wss schemes,
a path that normalizes to a slash, a backslash, a missing double slash
and surrounding spaces, each of which matched itself. A browser sends
only http or https as the Origin of a page's POST, so no real request
that matched before stops matching.

The field doc, docs/guide/configuration.md and trusted-server.example.toml
say each entry must be a bare origin and is refused otherwise.
handle_ec_resolve rebuilt its module with build_module, passing the
module the composition root had threaded into the request as the
injected one. On Fastly that is the built-in module itself when hmac is
selected, so build_module found the built-in name claimed twice and the
endpoint answered 500 for a valid selection, where it should answer 204
for a module that creates nothing from a client post. The same happened
to client_fixed in a build that compiles it, which is the one built-in
module whose page script posts here.

The endpoint now uses EcContext::selected_module, the module the
request's own context resolved. The context's host signal and injected
module fields existed only for the rebuild, so they and their accessors
are removed.

The resolve tests built contexts with no module and relied on the
rebuild, so their helper now resolves the selected module the way a
request's context does. Tests
resolve_answers_204_for_a_threaded_module_that_creates_nothing and
resolve_sets_the_cookie_for_a_threaded_client_fixed_module read a
context with the module threaded through the services, as Fastly does,
and fail without the change.
The Edge Cookie settings refused the old client-fixed spelling with a
message naming the spelling to write. No configuration in this repository
uses it, so the constant, the refusal in validate_module_selection,
names_retired_provider, the test of the refusal and the doc paragraphs
describing it are gone.

An old spelling now fails as any unknown implementation does when the
module is built at startup, with a message listing the implementations
the deployment has, which an_unknown_implementation_fails_naming_the_known_ones
covers.
set_provider_ec_cookie validated and wrote the same cookie as
set_ec_cookie, which no longer checks the HMAC identifier shape either,
so the resolve endpoint had a second copy of one function. The copy is
deleted, handle_ec_resolve calls set_ec_cookie, and the client_fixed
module's doc links to it.

The three tests of the copy fold into the set_ec_cookie tests, as two
tables. One writes an HMAC identifier, an opaque value and a base64url
value verbatim with the security attributes, and the other refuses a
semicolon, a carriage return and line feed, a line feed and a space with
no cookie at all. The second fails when set_ec_cookie stops validating.
Ten resolve tests built the same request and context and differed only
in a value and the answer they expected. They become two tables that
name the case in every assertion and keep every case.

resolve_answers_204_with_no_cookie_when_it_creates_nothing covers the
closed permission gate, no module, no identity graph and a value that
fails verification. resolve_refuses_a_request_it_cannot_take covers no
Origin, a foreign origin, a sibling subdomain, plain http, a port, a
form body and a body over the limit, and now also asserts that each sets
no cookie. Both fail when the origin and gate checks are taken out.
The header double's mint flag becomes the identifier it returns, so a
test can post an identifier the cookie cannot carry, and a second helper
takes a request the test builds itself.

Three tests are added, each failing with the behavior it covers broken:
an identifier outside the cookie bounds is refused with 400 and leaves
no cookie and no identity-graph row, a failed identity-graph write
answers 503 with no cookie, and a Content-Length over the limit is
refused before the body is read.

The reserved-effect tests say created where they said minted, and the
comments say what the code does rather than what it once did.
@jwrosewell
jwrosewell removed this pull request from stack #1408 October 18, 2026 20:15
@jwrosewell
jwrosewell added this pull request to stack #1418 October 19, 2026 12:18
@jwrosewell
jwrosewell removed this pull request from stack #1418 October 19, 2026 16:30
@jwrosewell
jwrosewell added this pull request to stack #1421 October 19, 2026 17:38
@jwrosewell
jwrosewell removed this pull request from stack #1421 October 19, 2026 18:29
@jwrosewell
jwrosewell added this pull request to stack #1422 October 19, 2026 19:20
@jwrosewell
jwrosewell marked this pull request as ready for review October 19, 2026 20:22
@jwrosewell
jwrosewell removed this pull request from stack #1422 October 28, 2026 11:47
@jwrosewell
jwrosewell added this pull request to stack #1465 October 28, 2026 18:28

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Make Edge Cookie identity pluggable and opt-in

2 participants