You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Status of siblings: P0 (license consistency, 031-01) is Filed upstream per the tracker. P1 scoped-Permission / RolePermission (031-02) and
P1 multi-tenant Tenant / (email, tenant) (031-03) are the parallel siblings in this series.
This issue is the codec half of the P1 work — value types + a deterministic packed perm codec suitable for offline / WASM consumers. Upstream AccessScope / PermScope
/ AppKey entitlement shapes (cross-app grant matrices, per-app role resolution) are
explicitly deferred to the 031-05 P2 row; this P1 ask is JUST the codec + value type
primitives, nothing more.
Problem statement
A federated identity-claims codec that needs to be both deterministic (byte-stable across
re-encodes, so cache keys, CI snapshots, and bug reports reproduce) and WASM-safe (no DI,
no I/O, no async, runs identically on the server claims-issuance path and on a Blazor
WebAssembly offline decoder) does not currently ship in any reusable form. Every consumer
of wangkanai/federation who needs this property either re-implements the codec from
scratch or reaches for a non-federation dependency — both of which fracture the wire
contract across consumers and block offline-first deployments.
RiverSync ships a production implementation of exactly this primitive in Common/src/Claims/PermClaimCodec.cs and a sibling value-type Account/src/Application/Authorization/AccessScope.cs whose semantics are aligned with
epic 029's generalization plan (see 029-03-U1). We are asking upstream to expose WASM-safe pure primitives (no DI / no I/O) plus a deterministic packed perm codec
with the contracts the next section pins, so that:
The Field offline path (Blazor WebAssembly / IndexedDB) does not have to take a
hard dependency on RiverSync's Common.Claims to decode a federated perm claim
(it currently reuses our codec, not upstream).
A cross-consumer wire contract stabilizes: any IdP-issued token in the Wangkanai.Federation ecosystem carrying a perm claim decodes to the same (App, Perm, Scope) triples in every consumer, online and offline.
The narrowing semantics of AccessScope.Intersect (only-narrowing, returns null
on incompatible) become reusable across consumers instead of every consumer
re-deriving the truth table.
We respectfully ask the wangkanai/federation maintainers (or a newly-claimed shared
claims package — see §Scope below) to ship the following three coordinated primitives
so the WASM/offline consumer pattern is consumable from upstream packages, not just
from RiverSync's Common library.
1. A PermScope value type (or a Common.Claims equivalent)
Shape: readonly partial record struct PermScope(PermScopeKind Kind, string? Id)
with three cases:
OrgWide (no Id, no inline suffix on the wire)
Region(string id) (inline form @region:<id>)
Site(string id) (inline form @site:<id>)
Type system enforces the closed set — analogous to AccessScope.cs:28-30 (abstract record + private ctor + sealed nested records).
Allocation-aware: factory methods (OrgWideScoped(), RegionScoped(id), SiteScoped(id)) and a shared OrgWideInstance singleton for the common case.
Equality is structural — two PermScopes are equal iff Kind matches and
(when Kind != OrgWide) Id matches; this is the precondition for the codec's PermScopeMap order-INVARIANT set-equality.
2. A PermScopeMap (the {permission → scope} map the codec operates on)
Shape: sealed class PermScopeMap : IReadOnlyList<PermEntry>, IEquatable<PermScopeMap>
where PermEntry(App, Perm, Scope) is the atomic unit. Mirrors Common/src/Claims/PermClaimCodec.cs:92-172 (the EntryComparerSortedSet<PermEntry>
backing).
Order-INVARIANT equality — Decode(Encode(x)) == x holds regardless of original
insertion order, so duplicate (App, Perm) keys with different scopes are
distinguished as a multiset.
An Empty sentinel — round-trips to a v1:-marked empty wire form (FR-006
edge case in RiverSync's codec, asserted by Encode_EmptyMap_RoundTripsToEmpty).
3. A deterministic PermClaimCodec (pure, DI-free, WASM-safe)
Surface: a static class with Encode(PermScopeMap) → string, Decode(string) → PermScopeMap, and the same v1: / v1z: wire format RiverSync ships.
No constructor, no instance state, no service-locator.
Wire format (FR-002 / FR-005 in RiverSync's codec):
Plain markerv1: — entries grouped by app, pipe | separates both app
boundaries AND perms within a group. First segment is <app>:<perm><scope>;
subsequent segments are |<perm><scope> (in-app) OR |<app>:<perm><scope> (new app).
Overage markerv1z: — gzip+base64url form, triggered when plain UTF-8 byte
length EXCEEDS OverageThresholdBytes (RiverSync uses 1024; the upstream
contract should keep ≤ 2048 so the encrypted token stays under the 4 KB
JWE budget).
Base64url with +→-, /→_, = padding stripped; uses ArrayPool<char> to
avoid per-decode allocation.
Hard error on unknown marker — Decode NEVER silently ignores a non-v1: /
non-v1z: prefix; it throws (RiverSync pins this with Decode_UnknownMarker_Throws).
Forward-compat — a v1: fixture decodes after a future v2: ships; the v1: version marker is what makes this property testable.
Determinism contract (FR-002 in RiverSync's codec): two encodes of the same
logical map MUST yield byte-identical output. Sort keys in order: App (Ordinal) → Perm (Ordinal) → Scope.Kind (byte ordinal) → Scope.Id
(null → empty, Ordinal). The worked fixture RiverSync pins is v1:admin:manage|portal:edit@region:rid|view for { (admin, manage): OrgWide, (portal, edit): Region("rid"), (portal, view): OrgWide } —
byte-exact, asserted by Encode_WorkedFixture_ProducesByteExactV1String.
All-or-nothing decode — modeled on Common/src/Web/ViewAsContext.cs:143-158 (the 018 cross-tenant precedent):
the packed value either decodes to a complete (App, Perm, Scope) set, or yields
an empty set, with deterministic behavior for the malformed case. A
per-render WASM consumer cannot afford to throw on a corrupt token any more than MainLayout can; the upstream decoder should degrade malformed input to "no
permissions" the same way 018 degrades malformed act JSON to "no session".
WASM-safe by construction — pure input → pure output, no IServiceProvider,
no async dispose, no I/O. This is the property that makes the Field offline path
work and the same property RiverSync asks upstream to commit to (POCO, not DI,
in 018 terms).
4. An AccessScope value type with only-narrowing Intersect (deferred-to-P2
recommendation, NOT this issue)
This P1 ask does NOT request the full AccessScope / PermScope entitlement shape
(the closed-set value type with OrgWide / Region / Site cases and a
non-widening Intersect static method). That work is explicitly deferred to the
031-05 P2 row in research 009 §8 — it sits on the same architectural pattern
(Account/src/Application/Authorization/AccessScope.cs:28-149) and will be the
subject of a separate filing. The P1 codec is the precondition for the P2 value
type to be consumable in a wire-stable way, and we are sequencing accordingly.
Scope of the ask (explicit)
In scope: PermScope value type, PermScopeMap, PermClaimCodec (encode +
decode + plain + overage tiers), a worked-fixture + round-trip test suite, a
documented OverageThresholdBytes constant with a documented ceiling.
Out of scope (deferred to 031-05 P2 row): full AccessScope discriminated
type with OrgWide / Region / Site cases and only-narrowing Intersect,
per-app role / entitlement resolution (RolePermission.Allowed, AppKey
matrix), cross-app grant shapes (ResolvedAccess).
Out of scope (deferred to 031-02 P1 sibling): scoped Permission / RolePermission models with allowed-scope narrowing (separate filing in the
parallel sibling epic 031-02).
Out of scope (deferred to 031-03 P1 sibling): multi-tenant Tenant + (email, tenant) composite identity helpers + scalar-FK ApplicationUser-like
base (separate filing in the parallel sibling epic 031-03).
No timeline promise, no code contribution, no licensing-philosophy request
(per the same explicit no-promises framing as our P0 filing).
Where this should ship
Either as additions to wangkanai/federation itself, or as a new Wangkanai.Federation.Claims (or similarly-named) shared claims package —
whichever scope the maintainers prefer. The codec + value types are
DI-agnostic and have no dependency on the OpenIddict server side, so a
separate package is feasible if the maintainers prefer to keep the
federation package's surface focused on the OIDC server wire.
RiverSync usage context
RiverSync is a six-app product platform. Its OIDC identity-provider role
is centralized in a single application (Account); the other five
(Portal, Admin, Partners, Pipeline, Field) participate strictly
as OIDC relying parties. The following four anchors explain why the
WASM-safe codec primitive is load-bearing on our side — and why we are
asking for a consumable upstream primitive rather than quietly shipping
a RiverSync-only Common.Claims.
Anchor 1 — Field WASM/offline already consumes this codec
File: Field/src/Client/Auth/FieldAccountClaimsPrincipalFactory.cs.
The factory's compiled-claim decoder at lines 134-178 calls RiverSync.Common.Claims.PermClaimCodec.Decode(payload.Perm) (line 172)
and emits one permission claim per (App, Perm, Scope) triple (lines
173-176). This is the online path.
File: Field/src/Client/Auth/OfflineAuthenticationStateProvider.cs.
The offline provider's job pillar Initial setup #1 (lines 22-24) mandates
"never re-implement the codec" — it reuses the same PermClaimCodec
via the 017-02 IndexedDB cache, decoding once online and emitting the
same App:Perm@ScopeKind[:ScopeId] shape offline (lines 251-258) so
authorization policies that read permission claims work identically
online and offline.
The framing for the upstream maintainer: Field already does this; the
upstream ask is to make the consumable primitive available from upstream
packages, not to invent a new codec.
Anchor 2 — Epic 029 is generalizing AccessScope to Common.Domain
File: agile/epics/029-common-oss-foundation-layer/029-03-generalized-scope-claim-primitives/spec.md.
Status IMPLEMENTED (line 5). 029-03-U1 AC1 verbatim: "Given two
compatible scopes, When Intersect(a, b) is called, Then the result
is the narrower scope (OrgWide ∩ Region = Region; Region ∩ Region
same Id = Region)." AC2: "Given incompatible scopes, When Intersect is called, Then it returns null (no widening)." AC3:
"Given the type, When serialized/deserialized or constructed from
Guid ids, Then values round-trip deterministically."
The ask to upstream is therefore NOT "we invented this and we want
you to copy us" — it is "we need this primitive in the shared
package because we are already generalizing our own internal copy to Common.Domain and our offline decoder cannot reach into Account
for the truth table."
Anchor 3 — Epic 018's ViewAsContext is the analog precedent for the consumable shape
File: Common/src/Web/ViewAsContext.cs. ViewAsContext is a POCO constructed per render with a ClaimsPrincipal; it is NOT a DI service. Read-only properties
(IsViewAs, ActorSubject, SessionId, WriteIsViewAs) are
consumed by every product app's MainLayout — same pattern we want
upstream to commit to for the codec decoder.
Never-throw invariant (lines 53-59): the decoder is on the
per-request MainLayout render path; a throw would log out every
user on a corrupt token. All decoding is wrapped in try/catch
with malformed input degrading to "no session" — the exact same
shape we want for the scope-codec decoder the upstream ask proposes.
Both-claims-required / all-or-nothing (lines 143-158): either
alone is a malformed token and yields IsViewAs = false. Same
never-throw contract the upstream codec decoder would want.
Wire-name stability — act is the wire name (RFC 8693 §2.2.1)
even though OpenIddict's internal alias is actor; the T08
wire-remap handler renames it. The upstream codec should similarly
commit to a stable wire name for the packed perm value (and a
stable per-triple shape for decoded output) so consumers do not
need to know the producer's internal naming.
"RiverSync's epic 018 (Cross-Tenant Access) shipped a
POCO-constructed, never-throw decoder for the federated act+vas
pair at Common/src/Web/ViewAsContext.cs, with read-only IsViewAs / ActorSubject / SessionId / WriteIsViewAs
properties consumed by every product app's MainLayout. The same
shape — POCO, never-throw, both-claims-required, wire-name
stable — is the analog we want for an upstream PermScope/AccessScope value type and packed perm codec."
Account is the sole platform OIDC/OAuth2 identity provider — the
only issuer of the federated token every app trusts.
The other five product apps (Portal, Admin, Partners, Pipeline, Field)
participate strictly as OIDC relying parties; none runs its own
authorization server or credential store.
Common provides consumable shapes (claims codec, RP wiring, ViewAsContext); it does NOT host a token issuer.
The upstream ask in 031-04 follows the same pattern: the codec
decoder is the consumable shape; the issuer-side state (claim
construction, scope resolution, policy engine) is upstream's
responsibility, not RiverSync's. This framing keeps the contribution
small, well-scoped, and aligned with 018's proven precedent.
No timeline promises (explicit)
To be unambiguous about what this issue is not asking:
No timeline promise. This issue does not commit RiverSync to any
particular release date for adopting any upstream release. Internal
sequencing is governed separately by our own epic planning and may
change without notice to this issue. Concretely: no timeline or
commitment by RiverSync is tied to the resolution of this issue.
No adoption commitment. RiverSync has not committed to taking a
hard Wangkanai.Federation (or shared claims package) dependency
even after this issue is resolved. The thin Common strategy in
029/030 is OSS-agnostic and is being executed on Wangkanai.Domain
Wangkanai.System today; any deeper Federation adoption is
contingent on legal/architectural review on our side (including the
P0 license clarity from 031-01).
No code contribution attached. This is a clarification /
consumable-primitive ask, not a roadmap sketch. The P2 AccessScope / PermScope / AppKey entitlement shapes (031-05)
are a separate filing, sequenced behind this P1 codec ask.
No licensing-philosophy request. We are not asking upstream to
change their licensing philosophy — only to ship WASM-safe pure
primitives (no DI / no I/O) plus a deterministic packed perm
codec with the contracts in §Request above. License clarity is
already tracked separately in the P0 issue (031-01, Filed).
Why now
This is a near-term need, not a long-horizon roadmap item — for three
converging reasons that make the WASM/offline consumer pattern visible
upstream as a real, shipped property:
Field's WASM/offline decode is real and shipping. Epic 017
shipped the Blazor WebAssembly client with an IndexedDB-backed
offline authentication state provider (017-04-U1) and a FieldAccountClaimsPrincipalFactory (017-03-U2) that both depend
on a shared PermClaimCodec. The codec is the load-bearing
primitive that makes offline reads reconstruct authorization state
with no network round-trip. Once an upstream equivalent exists,
Field can take a hard dependency on it instead of a RiverSync-only Common.Claims reference.
Epic 029-03 has already generalized the analog type internally.
The "narrowing" semantics of AccessScope (closed-set, no
widening, null on incompatible) are now part of RiverSync's Common.Domain (029-03 status IMPLEMENTED). Asking upstream for
the codec primitive that this internal value type will eventually
project onto aligns the upstream contract with what we are
already executing internally.
Epic 018's ViewAsContext proves the consumable shape works.
Six RiverSync apps consume ViewAsContext per-render without DI,
without throws, with stable wire names — exactly the shape the
upstream codec decoder needs. The precedent is shipped, tested,
and exercised in production.
Without an upstream consumable codec primitive, every consumer of
federation-issued tokens who needs offline / WASM / deterministic
behavior either re-implements the codec (fracturing the wire
contract) or reaches for a non-federation dependency (defeating the
purpose of having an IdP library). This issue is asking for a WASM-safe, DI-free, deterministic, versioned, hard-error-on-
unknown codec that an offline consumer can depend on, with the
test surface documented in this filing.
Worked fixture (byte-exact, asserted by RiverSync's test suite)
For the maintainer's quick verification, the codec round-trips this
worked fixture byte-exact (asserted by Common/test/Unit/Claims/PermClaimCodecTests.cs:60-82):
The v1: prefix is the version marker (line 195-196 in PermClaimCodec.cs); the | pipe separates both app boundaries and
perms within a group; the @region:rid is the inline scope suffix.
Org-wide emits no suffix (admin:manage and portal:view both
omit the @<kind>:<id> form). Decode("v1:admin:manage|portal:edit@region:rid|view")
returns the original PermScopeMap byte-equivalent.
The overage persona (200 entries) and the past-threshold always-v1z:
behaviour are tested at lines 144-191 and 197-215 of the same file;
the forward-compat v1:-decodes-after-v2:-ships behaviour is
tested at lines 269-287.
Drafted by RiverSync for upstream filing against wangkanai/federation
(or a shared claims package per the maintainers' preference);
not yet filed. Once filed, the upstream URL will replace this header note
and the tracker row in docs/oss-contributions.md
will be updated to Filed.
WASM-safe
PermScope+AccessScopevalue types and deterministicv1:/v1z:packedpermcodecProblem statement
A federated identity-claims codec that needs to be both deterministic (byte-stable across
re-encodes, so cache keys, CI snapshots, and bug reports reproduce) and WASM-safe (no DI,
no I/O, no async, runs identically on the server claims-issuance path and on a Blazor
WebAssembly offline decoder) does not currently ship in any reusable form. Every consumer
of
wangkanai/federationwho needs this property either re-implements the codec fromscratch or reaches for a non-federation dependency — both of which fracture the wire
contract across consumers and block offline-first deployments.
RiverSync ships a production implementation of exactly this primitive in
Common/src/Claims/PermClaimCodec.csand a sibling value-typeAccount/src/Application/Authorization/AccessScope.cswhose semantics are aligned withepic 029's generalization plan (see 029-03-U1). We are asking upstream to expose
WASM-safe pure primitives (no DI / no I/O) plus a deterministic packed
permcodecwith the contracts the next section pins, so that:
hard dependency on RiverSync's
Common.Claimsto decode a federatedpermclaim(it currently reuses our codec, not upstream).
Wangkanai.Federationecosystem carrying apermclaim decodes to the same(App, Perm, Scope)triples in every consumer, online and offline.AccessScope.Intersect(only-narrowing, returnsnullon incompatible) become reusable across consumers instead of every consumer
re-deriving the truth table.
Concrete evidence — what RiverSync ships today
Common/src/Claims/PermClaimCodec.csCommon/src/Claims/PermClaimCodec.csPermScope)Common/src/Claims/PermClaimCodec.csPermScopeMap)EntryComparerCommon/src/Claims/PermClaimCodec.csOverageThresholdBytes = 1024(≤ 2048);PlainPrefix = "v1:",OveragePrefix = "v1z:"Common/src/Claims/PermClaimCodec.csEncodePlainCore)Common/src/Claims/PermClaimCodec.csEncodeOverage)Common/src/Claims/PermClaimCodec.cs+→-,/→_,=stripped;ArrayPool<char>Common/src/Claims/PermClaimCodec.csDecode)v1:/v1z:dispatch + hard error on unknown markerCommon/src/Claims/PermClaimCodec.csDecodePlainCore)Common/test/Unit/Claims/PermClaimCodecTests.csv1:admin:manage|portal:edit@region:rid|viewCommon/test/Unit/Claims/PermClaimCodecTests.csCommon/test/Unit/Claims/PermClaimCodecTests.csv1z:Common/test/Unit/Claims/PermClaimCodecTests.csv1:decodes after a futurev2:shipsAccount/src/Application/Authorization/AccessScope.csAccount/src/Application/Authorization/AccessScope.csOrgWide/Region(Id)/Site(Id)sealed nested recordsAccount/src/Application/Authorization/AccessScope.csAccount/src/Application/Authorization/AccessScope.csIntersecttruth table (only-narrowing, null on incompatible)Account/src/Application/Authorization/AccessScope.csAccount/src/Application/Authorization/AccessScope.cssite.RegionIdlookup)Field/src/Client/Auth/FieldAccountClaimsPrincipalFactory.csField/src/Client/Auth/FieldAccountClaimsPrincipalFactory.csField/src/Client/Auth/FieldAccountClaimsPrincipalFactory.csAuthenticationTypeguard (the "always-authenticated" pitfall)Field/src/Client/Auth/FieldAccountClaimsPrincipalFactory.csPermClaimCodec.Decodecall siteField/src/Client/Auth/FieldAccountClaimsPrincipalFactory.cspermissionclaim per(App, Perm, Scope)tripleField/src/Client/Auth/OfflineAuthenticationStateProvider.csField/src/Client/Auth/OfflineAuthenticationStateProvider.csField/src/Client/Auth/OfflineAuthenticationStateProvider.csApp:Perm@ScopeKind[:ScopeId])Field/test/Unit/Auth/FieldPrincipalFactoryTests.csPermClaimCodec.Encode/EncodeWithOverage/DecodecallsField/test/E2E/OfflineAuthFlowTests.csCommon/src/Web/ViewAsContext.csCommon/src/Web/ViewAsContext.csCommon/src/Web/ViewAsContext.csIsViewAs,ActorSubject,SessionId,WriteIsViewAs)Common/src/Web/ViewAsContext.csCommon/test/Unit/Web/ViewAsContextTests.csagile/epics/029-common-oss-foundation-layer/029-03-generalized-scope-claim-primitives/spec.md:5Common.Domain(Status IMPLEMENTED)agile/epics/029-common-oss-foundation-layer/029-03-generalized-scope-claim-primitives/spec.md:38-43agile/research/009-refactoring-shared-federation-domain-from-account/report.md §8 P1 codec rowRequest
We respectfully ask the
wangkanai/federationmaintainers (or a newly-claimed sharedclaims package — see §Scope below) to ship the following three coordinated primitives
so the WASM/offline consumer pattern is consumable from upstream packages, not just
from RiverSync's
Commonlibrary.1. A
PermScopevalue type (or a Common.Claims equivalent)readonly partial record struct PermScope(PermScopeKind Kind, string? Id)with three cases:
OrgWide(noId, no inline suffix on the wire)Region(string id)(inline form@region:<id>)Site(string id)(inline form@site:<id>)AccessScope.cs:28-30(abstract record + private ctor + sealed nested records).OrgWideScoped(),RegionScoped(id),SiteScoped(id)) and a sharedOrgWideInstancesingleton for the common case.PermScopes are equal iffKindmatches and(when
Kind != OrgWide)Idmatches; this is the precondition for the codec'sPermScopeMaporder-INVARIANT set-equality.2. A
PermScopeMap(the{permission → scope}map the codec operates on)sealed class PermScopeMap : IReadOnlyList<PermEntry>, IEquatable<PermScopeMap>where
PermEntry(App, Perm, Scope)is the atomic unit. MirrorsCommon/src/Claims/PermClaimCodec.cs:92-172(theEntryComparerSortedSet<PermEntry>backing).
Decode(Encode(x)) == xholds regardless of originalinsertion order, so duplicate
(App, Perm)keys with different scopes aredistinguished as a multiset.
Emptysentinel — round-trips to av1:-marked empty wire form (FR-006edge case in RiverSync's codec, asserted by
Encode_EmptyMap_RoundTripsToEmpty).3. A deterministic
PermClaimCodec(pure, DI-free, WASM-safe)static classwithEncode(PermScopeMap) → string,Decode(string) → PermScopeMap, and the samev1:/v1z:wire format RiverSync ships.No constructor, no instance state, no service-locator.
v1:— entries grouped by app, pipe|separates both appboundaries AND perms within a group. First segment is
<app>:<perm><scope>;subsequent segments are
|<perm><scope>(in-app) OR|<app>:<perm><scope>(new app).v1z:— gzip+base64url form, triggered when plain UTF-8 bytelength EXCEEDS
OverageThresholdBytes(RiverSync uses1024; the upstreamcontract should keep
≤ 2048so the encrypted token stays under the 4 KBJWE budget).
+→-,/→_,=padding stripped; usesArrayPool<char>toavoid per-decode allocation.
DecodeNEVER silently ignores a non-v1:/non-
v1z:prefix; it throws (RiverSync pins this withDecode_UnknownMarker_Throws).v1:fixture decodes after a futurev2:ships; thev1:version marker is what makes this property testable.logical map MUST yield byte-identical output. Sort keys in order:
App(Ordinal) →Perm(Ordinal) →Scope.Kind(byte ordinal) →Scope.Id(null → empty, Ordinal). The worked fixture RiverSync pins is
v1:admin:manage|portal:edit@region:rid|viewfor{ (admin, manage): OrgWide, (portal, edit): Region("rid"), (portal, view): OrgWide }—byte-exact, asserted by
Encode_WorkedFixture_ProducesByteExactV1String.Common/src/Web/ViewAsContext.cs:143-158(the 018 cross-tenant precedent):the packed value either decodes to a complete
(App, Perm, Scope)set, or yieldsan empty set, with deterministic behavior for the malformed case. A
per-render WASM consumer cannot afford to throw on a corrupt token any more than
MainLayoutcan; the upstream decoder should degrade malformed input to "nopermissions" the same way 018 degrades malformed
actJSON to "no session".IServiceProvider,no async dispose, no I/O. This is the property that makes the Field offline path
work and the same property RiverSync asks upstream to commit to (POCO, not DI,
in 018 terms).
4. An
AccessScopevalue type with only-narrowingIntersect(deferred-to-P2recommendation, NOT this issue)
This P1 ask does NOT request the full
AccessScope/PermScopeentitlement shape(the closed-set value type with
OrgWide/Region/Sitecases and anon-widening
Intersectstatic method). That work is explicitly deferred to the031-05 P2 row in research 009 §8 — it sits on the same architectural pattern
(
Account/src/Application/Authorization/AccessScope.cs:28-149) and will be thesubject of a separate filing. The P1 codec is the precondition for the P2 value
type to be consumable in a wire-stable way, and we are sequencing accordingly.
Scope of the ask (explicit)
PermScopevalue type,PermScopeMap,PermClaimCodec(encode +decode + plain + overage tiers), a worked-fixture + round-trip test suite, a
documented
OverageThresholdBytesconstant with a documented ceiling.AccessScopediscriminatedtype with
OrgWide/Region/Sitecases and only-narrowingIntersect,per-app role / entitlement resolution (
RolePermission.Allowed,AppKeymatrix), cross-app grant shapes (
ResolvedAccess).Permission/RolePermissionmodels with allowed-scope narrowing (separate filing in theparallel sibling epic 031-02).
Tenant+(email, tenant)composite identity helpers + scalar-FKApplicationUser-likebase (separate filing in the parallel sibling epic 031-03).
(per the same explicit no-promises framing as our P0 filing).
Where this should ship
Either as additions to
wangkanai/federationitself, or as a newWangkanai.Federation.Claims(or similarly-named) shared claims package —whichever scope the maintainers prefer. The codec + value types are
DI-agnostic and have no dependency on the OpenIddict server side, so a
separate package is feasible if the maintainers prefer to keep the
federation package's surface focused on the OIDC server wire.
RiverSync usage context
RiverSync is a six-app product platform. Its OIDC identity-provider role
is centralized in a single application (
Account); the other five(
Portal,Admin,Partners,Pipeline,Field) participate strictlyas OIDC relying parties. The following four anchors explain why the
WASM-safe codec primitive is load-bearing on our side — and why we are
asking for a consumable upstream primitive rather than quietly shipping
a RiverSync-only
Common.Claims.Anchor 1 — Field WASM/offline already consumes this codec
Field/src/Client/Auth/FieldAccountClaimsPrincipalFactory.cs.The factory's compiled-claim decoder at lines 134-178 calls
RiverSync.Common.Claims.PermClaimCodec.Decode(payload.Perm)(line 172)and emits one
permissionclaim per(App, Perm, Scope)triple (lines173-176). This is the online path.
Field/src/Client/Auth/OfflineAuthenticationStateProvider.cs.The offline provider's job pillar Initial setup #1 (lines 22-24) mandates
"never re-implement the codec" — it reuses the same
PermClaimCodecvia the 017-02 IndexedDB cache, decoding once online and emitting the
same
App:Perm@ScopeKind[:ScopeId]shape offline (lines 251-258) soauthorization policies that read
permissionclaims work identicallyonline and offline.
Field/test/Unit/Auth/FieldPrincipalFactoryTests.cs(lines 24, 66, 97, 132, 145, 168) — direct codec calls, not mocks, with
the real
PermClaimCodecas the input encoder.Field/test/E2E/OfflineAuthFlowTests.cs— exercises the offline path against a real WASM host.
The framing for the upstream maintainer: Field already does this; the
upstream ask is to make the consumable primitive available from upstream
packages, not to invent a new codec.
Anchor 2 — Epic 029 is generalizing
AccessScopetoCommon.Domainagile/epics/029-common-oss-foundation-layer/029-03-generalized-scope-claim-primitives/spec.md.Status IMPLEMENTED (line 5). 029-03-U1 AC1 verbatim: "Given two
compatible scopes, When
Intersect(a, b)is called, Then the resultis the narrower scope (
OrgWide ∩ Region = Region;Region ∩ Regionsame Id =
Region)." AC2: "Given incompatible scopes, WhenIntersectis called, Then it returns null (no widening)." AC3:"Given the type, When serialized/deserialized or constructed from
Guid ids, Then values round-trip deterministically."
you to copy us" — it is "we need this primitive in the shared
package because we are already generalizing our own internal copy to
Common.Domainand our offline decoder cannot reach into Accountfor the truth table."
Anchor 3 — Epic 018's
ViewAsContextis the analog precedent for the consumable shapeCommon/src/Web/ViewAsContext.cs.ViewAsContextis a POCO constructed per render with aClaimsPrincipal; it is NOT a DI service. Read-only properties(
IsViewAs,ActorSubject,SessionId,WriteIsViewAs) areconsumed by every product app's
MainLayout— same pattern we wantupstream to commit to for the codec decoder.
per-request
MainLayoutrender path; a throw would log out everyuser on a corrupt token. All decoding is wrapped in
try/catchwith malformed input degrading to "no session" — the exact same
shape we want for the scope-codec decoder the upstream ask proposes.
alone is a malformed token and yields
IsViewAs = false. Samenever-throw contract the upstream codec decoder would want.
actis the wire name (RFC 8693 §2.2.1)even though OpenIddict's internal alias is
actor; the T08wire-remap handler renames it. The upstream codec should similarly
commit to a stable wire name for the packed
permvalue (and astable per-triple shape for decoded output) so consumers do not
need to know the producer's internal naming.
Anchor 4 — ARCH:VI (Account is the sole writer)
Per architecture rule
.claude/rules/architecture/federation-auth.md(extract ARCH:VI):
Accountis the sole platform OIDC/OAuth2 identity provider — theonly issuer of the federated token every app trusts.
participate strictly as OIDC relying parties; none runs its own
authorization server or credential store.
Commonprovides consumable shapes (claims codec, RP wiring,ViewAsContext); it does NOT host a token issuer.The upstream ask in 031-04 follows the same pattern: the codec
decoder is the consumable shape; the issuer-side state (claim
construction, scope resolution, policy engine) is upstream's
responsibility, not RiverSync's. This framing keeps the contribution
small, well-scoped, and aligned with 018's proven precedent.
No timeline promises (explicit)
To be unambiguous about what this issue is not asking:
particular release date for adopting any upstream release. Internal
sequencing is governed separately by our own epic planning and may
change without notice to this issue. Concretely: no timeline or
commitment by RiverSync is tied to the resolution of this issue.
hard
Wangkanai.Federation(or shared claims package) dependencyeven after this issue is resolved. The thin Common strategy in
029/030 is OSS-agnostic and is being executed on
Wangkanai.DomainWangkanai.Systemtoday; any deeper Federation adoption iscontingent on legal/architectural review on our side (including the
P0 license clarity from 031-01).
consumable-primitive ask, not a roadmap sketch. The P2
AccessScope/PermScope/AppKeyentitlement shapes (031-05)are a separate filing, sequenced behind this P1 codec ask.
change their licensing philosophy — only to ship WASM-safe pure
primitives (no DI / no I/O) plus a deterministic packed
permcodec with the contracts in §Request above. License clarity is
already tracked separately in the P0 issue (031-01, Filed).
Why now
This is a near-term need, not a long-horizon roadmap item — for three
converging reasons that make the WASM/offline consumer pattern visible
upstream as a real, shipped property:
shipped the Blazor WebAssembly client with an IndexedDB-backed
offline authentication state provider (017-04-U1) and a
FieldAccountClaimsPrincipalFactory(017-03-U2) that both dependon a shared
PermClaimCodec. The codec is the load-bearingprimitive that makes offline reads reconstruct authorization state
with no network round-trip. Once an upstream equivalent exists,
Field can take a hard dependency on it instead of a RiverSync-only
Common.Claimsreference.The "narrowing" semantics of
AccessScope(closed-set, nowidening, null on incompatible) are now part of RiverSync's
Common.Domain(029-03 status IMPLEMENTED). Asking upstream forthe codec primitive that this internal value type will eventually
project onto aligns the upstream contract with what we are
already executing internally.
ViewAsContextproves the consumable shape works.Six RiverSync apps consume
ViewAsContextper-render without DI,without throws, with stable wire names — exactly the shape the
upstream codec decoder needs. The precedent is shipped, tested,
and exercised in production.
Without an upstream consumable codec primitive, every consumer of
federation-issued tokens who needs offline / WASM / deterministic
behavior either re-implements the codec (fracturing the wire
contract) or reaches for a non-federation dependency (defeating the
purpose of having an IdP library). This issue is asking for a
WASM-safe, DI-free, deterministic, versioned, hard-error-on-
unknown codec that an offline consumer can depend on, with the
test surface documented in this filing.
Worked fixture (byte-exact, asserted by RiverSync's test suite)
For the maintainer's quick verification, the codec round-trips this
worked fixture byte-exact (asserted by
Common/test/Unit/Claims/PermClaimCodecTests.cs:60-82):The
v1:prefix is the version marker (line 195-196 inPermClaimCodec.cs); the|pipe separates both app boundaries andperms within a group; the
@region:ridis the inline scope suffix.Org-wide emits no suffix (
admin:manageandportal:viewbothomit the
@<kind>:<id>form).Decode("v1:admin:manage|portal:edit@region:rid|view")returns the original
PermScopeMapbyte-equivalent.The overage persona (200 entries) and the past-threshold always-
v1z:behaviour are tested at lines 144-191 and 197-215 of the same file;
the forward-compat
v1:-decodes-after-v2:-ships behaviour istested at lines 269-287.
References
agile/research/009-refactoring-shared-federation-domain-from-account/report.mdCommon/src/Claims/PermClaimCodec.csCommon/test/Unit/Claims/PermClaimCodecTests.csAccount/src/Application/Authorization/AccessScope.csPermClaimCodec.Decodecall site) —Field/src/Client/Auth/FieldAccountClaimsPrincipalFactory.csField/src/Client/Auth/OfflineAuthenticationStateProvider.csField/test/Unit/Auth/FieldPrincipalFactoryTests.csField/test/E2E/OfflineAuthFlowTests.csCommon/src/Web/ViewAsContext.csCommon/test/Unit/Web/ViewAsContextTests.csagile/epics/029-common-oss-foundation-layer/029-03-generalized-scope-claim-primitives/spec.mdagile/epics/029-common-oss-foundation-layer/epic.mdagile/epics/018-cross-tenant-access-view-as/epic.mdARCH:VI(Account as sole identity issuer; RP-onlyfederation) —
.claude/rules/architecture/federation-auth.mdgithub.com/wangkanai/federation
docs/oss/drafts/.notes/031-04-T01-evidence.mddocs/oss/drafts/.notes/031-04-T02-evidence.mddocs/oss/drafts/.notes/031-04-T03-evidence.mddocs/oss-contributions.mdDrafted by RiverSync for upstream filing against
wangkanai/federation(or a shared claims package per the maintainers' preference);
not yet filed. Once filed, the upstream URL will replace this header note
and the tracker row in
docs/oss-contributions.mdwill be updated to
Filed.