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
wangkanai/federation (and the underlying wangkanai/foundation base) does
not today expose cross-tenant / view-as primitives — neither the
customer-managed AccessGrant row, nor the staff ViewAsSession lifecycle,
nor a consumable dual-principal claim decoder that an RP can mount on
its layout pipeline. Federation's published models are still NotImplementedException stubs under "Planned Features" (per 009 §5 F6);
Foundation ships Entity / AuditableEntity bases but no delegation-shaped
or audit-boundary-shaped entities.
Downstream OIDC platforms that need a "RiverSync-style" cross-tenant /
view-as pattern (a customer grants a partner scoped cross-tenant access, a
staff user impersonates a customer member under audit, both flow through
the same dual-principal token) end up re-declaring the entire pair of
primitives locally — including the most load-bearing wire-format detail
(RFC 8693 act + a session-id companion claim) and the never-throw decoder
contract that keeps a corrupt token from logging every user out.
This is the P2 sibling of the P0 license issue (F7 / 031-01), the P1
permission model ask (031-02 / #43), the P1 tenant identity ask (031-03 / #41), and the P1 codec/claim ask (031-04 / #42). All five sit in the same
upstream gap: the base shapes a multi-app platform needs are either
missing or planned, not shipped.
RiverSync's three-half evidence (one matrix, one decoder)
The cross-tenant pattern is three shapes bound together by one decoder:
AccessGrant — the customer-managed partner grant row. A customer's
scoped, region-limited, revocable grant of cross-tenant access to a
partner organization. Four coarse scope booleans (ScopeTelemetry, ScopeService, ScopeSites, ScopeInvoices) + nullable RegionId
narrowing. Anchored at Account/src/Domain/AccessGrant.cs:35-105.
ViewAsSession — the staff delegation lifecycle row. A staff
principal (ActorUserId, the act) impersonates a customer member
(SubjectUserId, the sub) inside a bounded-lifetime session
(StartedAt / ExpiresAt / nullable EndedAt). Anchored at Account/src/Domain/ViewAsSession.cs:34-111.
ViewAsContext — the consumable decoder for the RP layout pipeline.
A plain POCO with IsViewAs / ActorSubject / SessionId / WriteIsViewAs. Reads the RFC 8693 act claim + the 018-03
3-char vas (view-as session id) claim off ClaimsPrincipal; never throws on a corrupt token. Anchored at Common/src/Web/ViewAsContext.cs:79-226.
All three belong on the same upstream issue because they share the
dual-principal pattern: AccessGrant is the customer-side write of
"this partner may access these scopes in this region"; ViewAsSession
is the staff-side write of "this actor is impersonating this subject
for this reason"; ViewAsContext is the read-side decoder that any RP
mounts to render a "you are viewing as ..." banner and to attribute
every write to the correct principal.
018-01 (layer-6 schema) — AccessGrant + ViewAsSession entities,
their IEntityTypeConfiguration mappings, the FK-scalar convention,
the FED-14 absence-is-contractual test
(EntityShapeTests.AuditEvent_Has_No_Fed14_FactoryDefault_Entity).
018-02 (layer-6 resolver) — the runtime that consults AccessGrant
on a partner-side request and the cross-tenant token exchange.
018-03 (view-as token exchange) — the controller that emits the act (RFC 8693 §2.2.1) + vas claim pair and the bounded-lifetime ExpiresAt cap.
018-04 (access-grant management) — the customer-side surface for
setting / revoking scopes.
018-05 (view-as banner) — the consuming MainLayout integration
that reads ViewAsContext (Common) and renders the cross-tenant
banner.
018-06 (dual-audit writer) — the audit writer that stamps every
write with both the staff actor (from act) and the customer
subject (from sub).
018-07 (cross-tenant acceptance gate) — the FE/QA gate that proves
the whole chain.
The 018 implementation is the consumer evidence. The P2 ask is for the upstream base shapes so any OIDC platform can ship the same pattern
without re-implementing the dual-principal wire format and the
never-throw decoder contract from scratch.
Why this is a composition blocker, not a feature request
No AccessGrant row upstream. Consumers wanting a customer-managed
partner grant row cannot bind to a Foundation base that does not exist.
They re-declare the 4-scope boolean pattern + the nullable RegionId
narrowing + the lifecycle status enum locally. Every consumer
invents a different shape; the wire-level interop that 018-02
depends on (a partner-side resource server reading the token's vas claim to look up AccessGrant) is impossible to standardize.
No ViewAsSession lifecycle row upstream. The bounded-lifetime
delegation lifecycle (StartedAt / ExpiresAt / nullable EndedAt)
is the property that makes staff impersonation auditable. Without
upstream committing to the lifecycle shape, consumers reinvent the
bounds and audit-evidence is split across N implementations.
No consumable decoder (ViewAsContext) upstream. This is the
single shape every RP needs. The decoder's three properties are
load-bearing:
POCO (no DI, no IServiceProvider, no async dispose) so it
works in both server-rendered and WASM/PWA hosts.
Never-throw invariant — a corrupt token degrades to
"no session" instead of 500-ing the layout pipeline and logging
out every user.
Both-required (act AND vas) — either claim alone is treated
as malformed; the actor identity is required to attribute writes
and the session id is required for the dual-audit writer and
the scope-narrowed API gate.
These three properties together are what makes the cross-tenant
banner safely mountable on every product app's layout.
The dual-audit pattern is locally proven. 018-06 dual-audit
writer stamps both act (actor) and sub (subject) on every AuditEvent the session produces. If upstream's base AccessGrant
/ ViewAsSession shipped without a wire-format contract for the
dual-audit pair, every consumer would have to invent their own
binding and the audit-evidence cross-platform consistency 009 §6
asks for would not exist.
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 a
customer-managed AccessGrant row, a staff ViewAsSession lifecycle,
and a consumable ViewAsContext decoder matter to us — and why we are
asking for the shapes rather than quietly forking.
Thin Common foundation (epic 029). Epic 029 — Common OSS Foundation Layer
establishes RiverSync.Common.Domain as a thin shell depending on Wangkanai.Domain and Wangkanai.System, with client-side federation
pieces (claims codec, OIDC RP wiring, ViewAsContext) already
extracted into Common/src/Claims/ and Common/src/Web/ without
issuer-side leakage. This is the Option A recommendation from
research 009 §7: thin extraction in Common, not a full internal fork.
Thin Common overlays on Foundation (epic 030). Epic 030 — Extract Federation Domain Models To Common
is the follow-on that would push consumable model shapes
(AccessGrantInfo, ViewAsSessionInfo overlays on Foundation.Entity) into Common.Domain, leaving Account as the
persisted IdP matrix and issuer. Neither 029 nor 030 pins Wangkanai.Federation today — both are blocked on the license
question being resolved first (F7, see 031-01) AND on the
cross-tenant base shapes being available upstream.
IMPLEMENTED cross-tenant precedent (epic 018). Epic 018 — Cross-Tenant Access (View-As)
is the IMPLEMENTED RiverSync epic that proves the
customer-managed grant + staff delegation + dual-audit pattern works
end-to-end on a six-app product platform. Anchored at the seven
018-NN features above. The "shape lives in Common, writer lives in
Account" pattern from ViewAsContext is exactly the pattern we are
asking upstream to ship — with the writer (Foundation) on the
upstream side and the consumer (Common.Web) on the downstream side.
Account is the sole platform OIDC/OAuth2 identity provider — and
the only writer of the persisted AccessGrant matrix and the ViewAsSession lifecycle rows.
The other five product apps (Portal, Admin, Partners, Pipeline, Field)
participate strictly as OIDC relying parties; none runs its own
authorization server, credential store, cross-tenant grant
manager, or staff delegation lifecycle.
Common provides consumable shapes (ViewAsContext decoder, AccessGrantInfo / ViewAsSessionInfo overlays, claims codec, RP
wiring); it does NOT host a token issuer or persist a grant /
session matrix. The base shapes in this issue are what Common
would overlay against.
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 Wangkanai.Foundation) 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.
No licensing-philosophy request. We are not asking upstream to
change their licensing philosophy — only that the cross-tenant
base shapes be available for thin-overlay consumption. The license
of record (Apache-2.0 vs SSPL v1 vs something else) is a project
governance decision and not ours to make; it is a sibling concern
tracked in the P0 issue (F7 / 031-01).
We respectfully ask the wangkanai/federation (and, if appropriate, wangkanai/foundation) maintainers to consider shipping the following
three base shapes — two POCOs and one consumable decoder — as thin,
composable, schema-only entities plus a no-DI decoder POCO. We have
ordered them so that the maintainer can accept, reject, or split them
as a single matrix decision.
1. AccessGrant — customer-managed partner cross-tenant grant
A schema-only base (or POCO) entity with:
Guid Id — stable key.
Guid PartnerLinkId — FK scalar to a partner-link row (the
customer↔partner pairing; the PartnerLink entity is owned by an
upstream epic and not introduced here). No navigation property
on this side (FK-scalar convention, configurable in the consumer's
mapping).
Guid TenantId — FK scalar to the customer Tenant.Id that owns
this grant (the granting customer, not the receiving partner).
Guid? RegionId — optional region-narrowing FK scalar to Region.Id. null ⇔ no region limit — the grant is org-wide
across every region the customer tenant owns.
A lifecycle status (enum) — Active (set on any scope on), Revoked (set on revoke). Default Active on insert.
CreatedAt / UpdatedAt — UTC timestamps.
Mirrors RiverSync's Account/src/Domain/AccessGrant.cs:35-105.
The four coarse scope booleans are the partner's access switches the
customer manages; the nullable RegionId further narrows the grant.
No embedded compiler / no resolver logic. The 4-scope booleans
are the data; the resolver that consults them lives in a separate
package — never on the base POCO. This is the same RolePermission_Has_No_ClaimsCompiler_Logic guard pattern
(Account/test/Unit/Authorization/RolePermissionTests.cs:39-56)
extended to the cross-tenant row.
FED-14 absence is contractual. Factory-default access is a
resolver property, NOT a row in this table. The negative invariant
is locked by a reflection-based test that scans the assembly for
"FactoryDefault" / "Fed14" type names. We would love to see upstream
ship the same absence, so the guard becomes a project convention
rather than a per-consumer tax.
2. ViewAsSession — staff delegation lifecycle
A schema-only base (or POCO) entity with:
Guid Id — stable key.
Guid ActorUserId — FK scalar to ApplicationUser.Id of the
staff principal doing the impersonation (the act per RFC 8693
§2.2.1). No navigation property.
Guid SubjectUserId — FK scalar to ApplicationUser.Id of the
customer member being impersonated (the sub).
Guid TenantId — FK scalar to the customer Tenant.Id the
session operates in.
Guid OrganizationId — FK scalar to the Organization.Id the
action crosses (the audit-boundary id).
DateTimeOffset StartedAt — UTC instant the delegation was
authorized.
DateTimeOffset? EndedAt — UTC instant the session was stopped
or revoked. null while the session is still open.
DateTimeOffset ExpiresAt — UTC instant the session is no longer
valid (the bounded-lifetime cap).
string Reason — required justification captured at session start
(the controller rejects an empty value at the store boundary).
A session lifecycle status (enum) — Active, Ended, Revoked, Expired.
CreatedAt / UpdatedAt — UTC timestamps.
Mirrors RiverSync's Account/src/Domain/ViewAsSession.cs:34-111.
The dual-principal pattern (sub = impersonated subject, act.sub = staff actor) and the bounded-lifetime cap are the
load-bearing properties; without them the cross-tenant session is
not auditable.
FK-scalar convention. All four FKs (ActorUserId, SubjectUserId, TenantId, OrganizationId) are scalar Guid FKs
with no navigation property on this side. Matches the FR-006
"Use FK scalars" convention used by every other Account.Domain
aggregate.
A plain POCO decoder (NOT a DI service, NOT a hosted background
service) with exactly four read-only properties:
bool IsViewAs — true iff the principal carries BOTH the act
(RFC 8693 §2.2.1) AND vas (view-as session id) claims AND both
decode cleanly.
string? ActorSubject — the staff user id parsed from the act
JSON value's sub key (RFC 8693 §2.2.2 minimal form). null when no session is active or the act value is malformed.
Guid? SessionId — the Guid form of the vas claim value. null when no session is active or the value does not parse as a Guid. (Guid.Empty would be a valid Guid, so null is the
unambiguous absence signal.)
bool WriteIsViewAs — write-flag mirroring IsViewAs in the
current implementation; the two are split so a future
enforcement-tier code path can flip the write-flag off without
turning the banner off (or vice-versa) once a real resource server
enforces the gate.
Construction: public ViewAsContext(ClaimsPrincipal principal)
— reads the claims once, never throws. A null principal is
tolerated and yields the no-session state. A malformed act
JSON value, a missing sub key, a malformed vas Guid, or
either-claim-alone yields IsViewAs = false and all other
properties at their null / false default.
POCO — no DI, no IServiceProvider, no async dispose.
Trivially testable; works in both server-rendered and WASM/PWA
hosts.
Never-throw invariant — the decoder is on the per-request MainLayout render path. A throw here would propagate as a
500 from the layout pipeline and log out every user on a
corrupt token. All decoding is wrapped in try/catch with the
malformed-input case degrading to "no session."
Both-required (act AND vas) — either claim alone is treated
as malformed and yields IsViewAs = false. The actor identity
is required to attribute writes, and the session id is required
for the dual-audit writer and the scope-narrowed API gate.
4. Documentation in a matrix contract note (companion to LICENSE-COMPATIBILITY)
Regardless of which package the base shapes live in, ship a short CROSS-TENANT-MATRIX.md (or equivalent) in the repo that documents:
Which entity holds the customer-managed grant
(AccessGrant + the 4 scope booleans + nullable RegionId).
Which entity holds the staff delegation lifecycle
(ViewAsSession + dual-principal pattern + bounded-lifetime cap).
The dual-principal wire contract (RFC 8693 §2.2.1 act + a
companion session-id claim, both required, with the decoder's
POCO / never-throw / both-required contract).
The FK-scalar + no-compiler-in-domain guard pattern (the RolePermission_Has_No_ClaimsCompiler_Logic reflection guard
extended to AccessGrant / ViewAsSession).
This is the same posture RiverSync needs for ARCH:VI compliance: a
written, citable contract on the upstream side that consumers can
quote in their own compliance documentation.
What we are NOT asking for
No change to upstream's licensing philosophy (tracked separately in
the P0 issue, F7 / 031-01).
No specific ORM shape (FK-scalar convention, configurable in the
consumer's mapping; no embedded navigations).
No enforcement of the cross-tenant scope at the data-layer level
on the upstream side (the RiverSync implementation defers this
to the out-of-scope data-layer/gateway initiative; we are not
asking upstream to take that on).
No commitment from RiverSync to take a hard dependency on the
shipped shape once available.
Why now
The cross-tenant / view-as gap has been a long-standing upstream
absence (Federation's permission models are still "Planned Features"
per 009 §5 F6, and the cross-tenant / view-as shape is the
sister-shipped pattern), but the timing is right for three converging
reasons that make the upstream base-shape gap visible from a
multi-app product platform's perspective:
The IMPLEMENTED 018 precedent is concrete, not hypothetical.
RiverSync's epic 018 shipped end-to-end on 2026-06-XX (status
IMPLEMENTED per the epic page). The seven 018-NN features prove
the customer-managed grant + staff delegation + dual-audit +
consumable decoder pattern works on a six-app product platform
without leaking issuer responsibilities — exactly the pattern
we are asking upstream to ship.
The thin Common overlay plan is concrete, not hypothetical.
Epic 030's AccessGrantInfo / ViewAsSessionInfo overlays on Foundation.Entity are the next concrete step in the
thin-extraction plan from 009 §7 Option A. The overlay work
surfaces exactly the missing base shapes upstream needs to ship.
The dual-audit pattern is locally proven. 018-06 dual-audit
writer stamps both act (actor) and sub (subject) on every AuditEvent the session produces. The wire-level binding is
the same PermClaimCodec pattern from 031-04 — upstream codec,
downstream decoder, identical wire format across server-rendered
and WASM/PWA hosts.
Without these base shapes upstream, multi-app OIDC platforms either
re-declare a private cross-tenant grant matrix + staff delegation
lifecycle + dual-audit wire format (duplicating what Foundation was
created to share) or invent a ViewAsContext-style decoder locally
with no upstream authority to anchor it. Neither is a good place for
the platform to be.
Drafted by RiverSync for upstream filing against wangkanai/federation
(or wangkanai/foundation if upstream prefers the split). 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.
Cross-tenant / view-as + AccessGrant primitives
Problem statement
wangkanai/federation(and the underlyingwangkanai/foundationbase) doesnot today expose cross-tenant / view-as primitives — neither the
customer-managed
AccessGrantrow, nor the staffViewAsSessionlifecycle,nor a consumable dual-principal claim decoder that an RP can mount on
its layout pipeline. Federation's published models are still
NotImplementedExceptionstubs under "Planned Features" (per 009 §5 F6);Foundation ships
Entity/AuditableEntitybases but no delegation-shapedor audit-boundary-shaped entities.
Downstream OIDC platforms that need a "RiverSync-style" cross-tenant /
view-as pattern (a customer grants a partner scoped cross-tenant access, a
staff user impersonates a customer member under audit, both flow through
the same dual-principal token) end up re-declaring the entire pair of
primitives locally — including the most load-bearing wire-format detail
(RFC 8693
act+ a session-id companion claim) and the never-throw decodercontract that keeps a corrupt token from logging every user out.
This is the P2 sibling of the P0 license issue (F7 / 031-01), the P1
permission model ask (031-02 / #43), the P1 tenant identity ask (031-03 /
#41), and the P1 codec/claim ask (031-04 / #42). All five sit in the same
upstream gap: the base shapes a multi-app platform needs are either
missing or planned, not shipped.
RiverSync's three-half evidence (one matrix, one decoder)
The cross-tenant pattern is three shapes bound together by one decoder:
AccessGrant— the customer-managed partner grant row. A customer'sscoped, region-limited, revocable grant of cross-tenant access to a
partner organization. Four coarse scope booleans (
ScopeTelemetry,ScopeService,ScopeSites,ScopeInvoices) + nullableRegionIdnarrowing. Anchored at
Account/src/Domain/AccessGrant.cs:35-105.ViewAsSession— the staff delegation lifecycle row. A staffprincipal (
ActorUserId, theact) impersonates a customer member(
SubjectUserId, thesub) inside a bounded-lifetime session(
StartedAt/ExpiresAt/ nullableEndedAt). Anchored atAccount/src/Domain/ViewAsSession.cs:34-111.ViewAsContext— the consumable decoder for the RP layout pipeline.A plain POCO with
IsViewAs/ActorSubject/SessionId/WriteIsViewAs. Reads the RFC 8693actclaim + the 018-033-char
vas(view-as session id) claim offClaimsPrincipal;never throws on a corrupt token. Anchored at
Common/src/Web/ViewAsContext.cs:79-226.All three belong on the same upstream issue because they share the
dual-principal pattern:
AccessGrantis the customer-side write of"this partner may access these scopes in this region";
ViewAsSessionis the staff-side write of "this actor is impersonating this subject
for this reason";
ViewAsContextis the read-side decoder that any RPmounts to render a "you are viewing as ..." banner and to attribute
every write to the correct principal.
The IMPLEMENTED precedent (epic 018)
This is NOT a roadmap sketch on the RiverSync side — epic
018 — Cross-Tenant Access (View-As)
is IMPLEMENTED end-to-end:
AccessGrant+ViewAsSessionentities,their
IEntityTypeConfigurationmappings, the FK-scalar convention,the FED-14 absence-is-contractual test
(
EntityShapeTests.AuditEvent_Has_No_Fed14_FactoryDefault_Entity).AccessGranton a partner-side request and the cross-tenant token exchange.
act(RFC 8693 §2.2.1) +vasclaim pair and the bounded-lifetimeExpiresAtcap.setting / revoking scopes.
MainLayoutintegrationthat reads
ViewAsContext(Common) and renders the cross-tenantbanner.
write with both the staff actor (from
act) and the customersubject (from
sub).the whole chain.
The 018 implementation is the consumer evidence. The P2 ask is for the
upstream base shapes so any OIDC platform can ship the same pattern
without re-implementing the dual-principal wire format and the
never-throw decoder contract from scratch.
Why this is a composition blocker, not a feature request
AccessGrantrow upstream. Consumers wanting a customer-managedpartner grant row cannot bind to a Foundation base that does not exist.
They re-declare the 4-scope boolean pattern + the nullable
RegionIdnarrowing + the lifecycle status enum locally. Every consumer
invents a different shape; the wire-level interop that 018-02
depends on (a partner-side resource server reading the token's
vasclaim to look upAccessGrant) is impossible to standardize.ViewAsSessionlifecycle row upstream. The bounded-lifetimedelegation lifecycle (
StartedAt/ExpiresAt/ nullableEndedAt)is the property that makes staff impersonation auditable. Without
upstream committing to the lifecycle shape, consumers reinvent the
bounds and audit-evidence is split across N implementations.
ViewAsContext) upstream. This is thesingle shape every RP needs. The decoder's three properties are
load-bearing:
IServiceProvider, no async dispose) so itworks in both server-rendered and WASM/PWA hosts.
"no session" instead of 500-ing the layout pipeline and logging
out every user.
as malformed; the actor identity is required to attribute writes
and the session id is required for the dual-audit writer and
the scope-narrowed API gate.
These three properties together are what makes the cross-tenant
banner safely mountable on every product app's layout.
writer stamps both
act(actor) andsub(subject) on everyAuditEventthe session produces. If upstream's baseAccessGrant/
ViewAsSessionshipped without a wire-format contract for thedual-audit pair, every consumer would have to invent their own
binding and the audit-evidence cross-platform consistency 009 §6
asks for would not exist.
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 a
customer-managed
AccessGrantrow, a staffViewAsSessionlifecycle,and a consumable
ViewAsContextdecoder matter to us — and why we areasking for the shapes rather than quietly forking.
Thin Common foundation (epic 029).
Epic 029 — Common OSS Foundation Layer
establishes
RiverSync.Common.Domainas a thin shell depending onWangkanai.DomainandWangkanai.System, with client-side federationpieces (claims codec, OIDC RP wiring,
ViewAsContext) alreadyextracted into
Common/src/Claims/andCommon/src/Web/withoutissuer-side leakage. This is the Option A recommendation from
research 009 §7: thin extraction in Common, not a full internal fork.
Thin Common overlays on Foundation (epic 030).
Epic 030 — Extract Federation Domain Models To Common
is the follow-on that would push consumable model shapes
(
AccessGrantInfo,ViewAsSessionInfooverlays onFoundation.Entity) intoCommon.Domain, leavingAccountas thepersisted IdP matrix and issuer. Neither 029 nor 030 pins
Wangkanai.Federationtoday — both are blocked on the licensequestion being resolved first (F7, see 031-01) AND on the
cross-tenant base shapes being available upstream.
IMPLEMENTED cross-tenant precedent (epic 018).
Epic 018 — Cross-Tenant Access (View-As)
is the IMPLEMENTED RiverSync epic that proves the
customer-managed grant + staff delegation + dual-audit pattern works
end-to-end on a six-app product platform. Anchored at the seven
018-NN features above. The "shape lives in Common, writer lives in
Account" pattern from
ViewAsContextis exactly the pattern we areasking upstream to ship — with the writer (Foundation) on the
upstream side and the consumer (Common.Web) on the downstream side.
ARCH:VI — Account is the sole writer; Common provides consumption shapes.
Per architecture rule
.claude/rules/architecture/federation-auth.md(extract ARCH:VI):
Accountis the sole platform OIDC/OAuth2 identity provider — andthe only writer of the persisted
AccessGrantmatrix and theViewAsSessionlifecycle rows.participate strictly as OIDC relying parties; none runs its own
authorization server, credential store, cross-tenant grant
manager, or staff delegation lifecycle.
Commonprovides consumable shapes (ViewAsContextdecoder,AccessGrantInfo/ViewAsSessionInfooverlays, claims codec, RPwiring); it does NOT host a token issuer or persist a grant /
session matrix. The base shapes in this issue are what
Commonwould overlay against.
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(orWangkanai.Foundation) 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.
change their licensing philosophy — only that the cross-tenant
base shapes be available for thin-overlay consumption. The license
of record (Apache-2.0 vs SSPL v1 vs something else) is a project
governance decision and not ours to make; it is a sibling concern
tracked in the P0 issue (F7 / 031-01).
AccessGrant(4 booleans + nullable
RegionId), not onPermissionitself.See 031-02 / Scoped Permission + RolePermission base shapes for Foundation overlays #43 for the permission-matrix shape.
Request
We respectfully ask the
wangkanai/federation(and, if appropriate,wangkanai/foundation) maintainers to consider shipping the followingthree base shapes — two POCOs and one consumable decoder — as thin,
composable, schema-only entities plus a no-DI decoder POCO. We have
ordered them so that the maintainer can accept, reject, or split them
as a single matrix decision.
1.
AccessGrant— customer-managed partner cross-tenant grantA schema-only base (or POCO) entity with:
Guid Id— stable key.Guid PartnerLinkId— FK scalar to a partner-link row (thecustomer↔partner pairing; the
PartnerLinkentity is owned by anupstream epic and not introduced here). No navigation property
on this side (FK-scalar convention, configurable in the consumer's
mapping).
Guid TenantId— FK scalar to the customerTenant.Idthat ownsthis grant (the granting customer, not the receiving partner).
Guid? RegionId— optional region-narrowing FK scalar toRegion.Id.null⇔ no region limit — the grant is org-wideacross every region the customer tenant owns.
bool ScopeTelemetry/bool ScopeService/bool ScopeSites/bool ScopeInvoices— coarse scope switches, all defaultfalse.Active(set on any scope on),Revoked(set on revoke). DefaultActiveon insert.CreatedAt/UpdatedAt— UTC timestamps.Mirrors RiverSync's
Account/src/Domain/AccessGrant.cs:35-105.The four coarse scope booleans are the partner's access switches the
customer manages; the nullable
RegionIdfurther narrows the grant.No embedded compiler / no resolver logic. The 4-scope booleans
are the data; the resolver that consults them lives in a separate
package — never on the base POCO. This is the same
RolePermission_Has_No_ClaimsCompiler_Logicguard pattern(
Account/test/Unit/Authorization/RolePermissionTests.cs:39-56)extended to the cross-tenant row.
FED-14 absence is contractual. Factory-default access is a
resolver property, NOT a row in this table. The negative invariant
is locked by a reflection-based test that scans the assembly for
"FactoryDefault" / "Fed14" type names. We would love to see upstream
ship the same absence, so the guard becomes a project convention
rather than a per-consumer tax.
2.
ViewAsSession— staff delegation lifecycleA schema-only base (or POCO) entity with:
Guid Id— stable key.Guid ActorUserId— FK scalar toApplicationUser.Idof thestaff principal doing the impersonation (the
actper RFC 8693§2.2.1). No navigation property.
Guid SubjectUserId— FK scalar toApplicationUser.Idof thecustomer member being impersonated (the
sub).Guid TenantId— FK scalar to the customerTenant.Idthesession operates in.
Guid OrganizationId— FK scalar to theOrganization.Idtheaction crosses (the audit-boundary id).
DateTimeOffset StartedAt— UTC instant the delegation wasauthorized.
DateTimeOffset? EndedAt— UTC instant the session was stoppedor revoked.
nullwhile the session is still open.DateTimeOffset ExpiresAt— UTC instant the session is no longervalid (the bounded-lifetime cap).
string Reason— required justification captured at session start(the controller rejects an empty value at the store boundary).
Active,Ended,Revoked,Expired.CreatedAt/UpdatedAt— UTC timestamps.Mirrors RiverSync's
Account/src/Domain/ViewAsSession.cs:34-111.The dual-principal pattern (
sub= impersonated subject,act.sub= staff actor) and the bounded-lifetime cap are theload-bearing properties; without them the cross-tenant session is
not auditable.
FK-scalar convention. All four FKs (
ActorUserId,SubjectUserId,TenantId,OrganizationId) are scalar Guid FKswith no navigation property on this side. Matches the FR-006
"Use FK scalars" convention used by every other Account.Domain
aggregate.
3.
ViewAsContext— consumable decoder (POCO, never-throw, both-required)A plain POCO decoder (NOT a DI service, NOT a hosted background
service) with exactly four read-only properties:
bool IsViewAs— true iff the principal carries BOTH theact(RFC 8693 §2.2.1) AND
vas(view-as session id) claims AND bothdecode cleanly.
string? ActorSubject— the staff user id parsed from theactJSON value's
subkey (RFC 8693 §2.2.2 minimal form).nullwhen no session is active or theactvalue is malformed.Guid? SessionId— theGuidform of thevasclaim value.nullwhen no session is active or the value does not parse as aGuid. (Guid.Emptywould be a valid Guid, sonullis theunambiguous absence signal.)
bool WriteIsViewAs— write-flag mirroringIsViewAsin thecurrent implementation; the two are split so a future
enforcement-tier code path can flip the write-flag off without
turning the banner off (or vice-versa) once a real resource server
enforces the gate.
Construction:
public ViewAsContext(ClaimsPrincipal principal)— reads the claims once, never throws. A
nullprincipal istolerated and yields the no-session state. A malformed
actJSON value, a missing
subkey, a malformedvasGuid, oreither-claim-alone yields
IsViewAs = falseand all otherproperties at their null /
falsedefault.Mirrors RiverSync's
Common/src/Web/ViewAsContext.cs:79-226.The three contract properties that bind:
IServiceProvider, no async dispose.Trivially testable; works in both server-rendered and WASM/PWA
hosts.
MainLayoutrender path. A throw here would propagate as a500 from the layout pipeline and log out every user on a
corrupt token. All decoding is wrapped in
try/catchwith themalformed-input case degrading to "no session."
as malformed and yields
IsViewAs = false. The actor identityis required to attribute writes, and the session id is required
for the dual-audit writer and the scope-narrowed API gate.
4. Documentation in a matrix contract note (companion to LICENSE-COMPATIBILITY)
Regardless of which package the base shapes live in, ship a short
CROSS-TENANT-MATRIX.md(or equivalent) in the repo that documents:(
AccessGrant+ the 4 scope booleans + nullableRegionId).(
ViewAsSession+ dual-principal pattern + bounded-lifetime cap).act+ acompanion session-id claim, both required, with the decoder's
POCO / never-throw / both-required contract).
RolePermission_Has_No_ClaimsCompiler_Logicreflection guardextended to
AccessGrant/ViewAsSession).This is the same posture RiverSync needs for ARCH:VI compliance: a
written, citable contract on the upstream side that consumers can
quote in their own compliance documentation.
What we are NOT asking for
the P0 issue, F7 / 031-01).
consumer's mapping; no embedded navigations).
on the upstream side (the RiverSync implementation defers this
to the out-of-scope data-layer/gateway initiative; we are not
asking upstream to take that on).
shipped shape once available.
Why now
The cross-tenant / view-as gap has been a long-standing upstream
absence (Federation's permission models are still "Planned Features"
per 009 §5 F6, and the cross-tenant / view-as shape is the
sister-shipped pattern), but the timing is right for three converging
reasons that make the upstream base-shape gap visible from a
multi-app product platform's perspective:
RiverSync's epic 018 shipped end-to-end on 2026-06-XX (status
IMPLEMENTED per the epic page). The seven 018-NN features prove
the customer-managed grant + staff delegation + dual-audit +
consumable decoder pattern works on a six-app product platform
without leaking issuer responsibilities — exactly the pattern
we are asking upstream to ship.
Epic 030's
AccessGrantInfo/ViewAsSessionInfooverlays onFoundation.Entityare the next concrete step in thethin-extraction plan from 009 §7 Option A. The overlay work
surfaces exactly the missing base shapes upstream needs to ship.
writer stamps both
act(actor) andsub(subject) on everyAuditEventthe session produces. The wire-level binding isthe same
PermClaimCodecpattern from 031-04 — upstream codec,downstream decoder, identical wire format across server-rendered
and WASM/PWA hosts.
Without these base shapes upstream, multi-app OIDC platforms either
re-declare a private cross-tenant grant matrix + staff delegation
lifecycle + dual-audit wire format (duplicating what Foundation was
created to share) or invent a
ViewAsContext-style decoder locallywith no upstream authority to anchor it. Neither is a good place for
the platform to be.
References
agile/research/009-refactoring-shared-federation-domain-from-account/report.mdARCH:VI(Account as sole identity issuer + solegrant/session matrix writer; RP-only federation) —
.claude/rules/architecture/federation-auth.mdagile/epics/018-cross-tenant-access-view-as/epic.mdAccessGrant+ViewAsSessionentities, FK-scalar convention, FED-14 absence-is-contractual) —
agile/epics/018-cross-tenant-access-view-as/018-01-layer6-schema/agile/epics/018-cross-tenant-access-view-as/018-02-layer6-resolver/act+vasclaim emission,bounded-lifetime
ExpiresAtcap) —agile/epics/018-cross-tenant-access-view-as/018-03-view-as-token-exchange/agile/epics/018-cross-tenant-access-view-as/018-04-access-grant-management/MainLayoutintegrationwith
ViewAsContext) —agile/epics/018-cross-tenant-access-view-as/018-05-view-as-banner/AuditEventwithboth actor and subject) —
agile/epics/018-cross-tenant-access-view-as/018-06-dual-audit-writer/Common.Web.ViewAsContexthome) —
agile/epics/029-common-oss-foundation-layer/epic.mdAccessGrantInfo/ViewAsSessionInfooverlays) —agile/epics/030-extract-federation-domain-models-to-common/epic.mdagile/epics/031-oss-contribution-parallel-track/031-01-p0-license-metadata-issue/filing, Scoped Permission + RolePermission base shapes for Foundation overlays #43) —
agile/epics/031-oss-contribution-parallel-track/031-02-p1-scoped-permission-role-permission-models/filing, Multi-tenant Tenant shape + scalar TenantId + (email, tenant) composite identity #41) —
agile/epics/031-oss-contribution-parallel-track/031-03-p1-multi-tenant-tenant-email-tenant/filing, WASM-safe
PermScope+AccessScopevalue types and deterministicv1:/v1z:packedpermcodec #42; theact/vasdecoder is consumed the same way asthe codec decoder in
Common.Web) —agile/epics/031-oss-contribution-parallel-track/031-04-p1-scope-primitives-deterministic-codec/github.com/wangkanai/federation
Entity/AuditableEntitybase shapes) —github.com/wangkanai/foundation
AccessGrant(customer-managed partner cross-tenantgrant; 4 scope booleans + nullable
RegionId) —Account/src/Domain/AccessGrant.cs:35-105ViewAsSession(staff delegation lifecycle;dual-principal pattern + bounded-lifetime cap) —
Account/src/Domain/ViewAsSession.cs:34-111ViewAsContext(consumable decoder; POCO +never-throw + both-required) —
Common/src/Web/ViewAsContext.cs:79-226RolePermission_Has_No_ClaimsCompiler_Logicreflection guard (extended to
AccessGrant/ViewAsSessionschema-only contract) —
Account/test/Unit/Authorization/RolePermissionTests.cs:39-56EntityShapeTests.AuditEvent_Has_No_Fed14_FactoryDefault_Entityreflection guard (FED-14 absence-is-contractual) —
Account/test/Unit/Domain/EntityShapeTests.cs(the FED-14 scanpattern)
Drafted by RiverSync for upstream filing against
wangkanai/federation(or
wangkanai/foundationif upstream prefers the split).Not yet filed. Once filed, the upstream URL will replace this
header note and the tracker row in
docs/oss-contributions.mdwill beupdated to
Filed.