Skip to content

fix(realtime): scope sse streams by authorized route#786

Merged
cssbruno merged 2 commits into
mainfrom
fix/realtime-scope-sse-streams
Jun 27, 2026
Merged

fix(realtime): scope sse streams by authorized route#786
cssbruno merged 2 commits into
mainfrom
fix/realtime-scope-sse-streams

Conversation

@cssbruno

Copy link
Copy Markdown
Owner

Summary

  • Fixes generated realtime SSE fanout so page-owned subscriptions are scoped by a server-owned route audience label instead of only by global event type.
  • Configures the generated default SSE hub with WithSSEAudienceFromRequest(realtimeStreamAudience), using the same generated route matcher that selects stream guards.
  • Copies presentation events and query invalidation notices to the authorized route audiences that subscribed to each event/query, preserving broadcast behavior for non-route-owned subscriptions.
  • Adds a generated-binary regression where /patients subscribes to PatientNotice and /dashboard subscribes to OtherNotice; the /patients stream fails if the dashboard event arrives.

Issue Closure

Fixes #778

Verification

  • I ran the relevant tests, lint, and build commands.
  • I ran scripts/test-go-modules.sh when Go code or compiler behavior changed.
  • I ran go build ./cmd/gowdk when CLI, compiler, runtime, addon, or release behavior changed.
  • I ran node --check editors/vscode/extension.js when editor files changed.
  • I updated docs for behavior, setup, or architecture changes.
  • I added or updated tests for changed behavior.
  • I considered security-sensitive surfaces such as auth, CSRF, redirects, request-time handlers, logs, diagnostics, embedded assets, editor commands, WASM, contracts, and realtime behavior.

Commands run:

  • go test ./internal/appgen -run 'TestGenerateWritesRealtime|TestGenerateRealtime|TestGeneratedBinaryRealtime' -count=1
  • go test ./internal/appgen ./runtime/contracts/sse ./runtime/realtime ./addons/realtime -count=1
  • go build ./cmd/gowdk
  • go test ./...
  • git diff --check

LLM Assistance

  • LLM session summary: AI assistance added generated route audience labels for realtime SSE clients, scoped generated fanout events/query invalidations by those audiences, and added regression coverage for cross-route event leakage.
  • Human-reviewed assumptions: Route-scoped generated SSE should preserve broadcast delivery for non-route-owned subscriptions while preventing page-owned subscription events from being observed through another authorized page route.
  • Follow-up work: None for [High] Scope realtime SSE streams by authorized route or audience #778.

@github-actions github-actions Bot added the compiler Compiler internals, pipeline, and generated metadata label Jun 27, 2026
@cssbruno cssbruno marked this pull request as ready for review June 27, 2026 14:39

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: cdc2ae48d7

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +600 to +603
define([]ast.Expr{id("audiences")}, &ast.IndexExpr{X: id("realtimeSubscriptionAudiences"), Index: selExpr(id("event"), "Type")}),
&ast.IfStmt{
Cond: &ast.BinaryExpr{X: call(id("len"), id("audiences")), Op: token.EQL, Y: intLit(0)},
Body: block(&ast.ReturnStmt{Results: []ast.Expr{eventEnvelopeSliceExpr(id("event"))}}),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve broadcast delivery for mixed subscription owners

When an event type has any page-owned subscription, realtimeSubscriptionAudiences[event.Type] becomes non-empty, so this branch emits only route-labeled copies. If the same presentation event type is also subscribed from a component/non-page owner, that unscoped subscriber has no route audience and no broadcast copy is sent, so apps mixing a shared component subscription with a page-specific subscription stop receiving those realtime updates outside the labeled routes. Track whether the event type also has unscoped owners and append the original event for them.

Useful? React with 👍 / 👎.

Comment thread internal/appgen/source_realtime.go Outdated
Comment on lines +650 to +654
Body: block(&ast.RangeStmt{
Key: id("_"),
Value: id("audience"),
Tok: token.DEFINE,
X: &ast.IndexExpr{X: id("realtimeQueryAudiences"), Index: id("query")},

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve unscoped queries in mixed invalidation notices

For a generated gowdk.query.invalidate notice containing both route-owned and component/non-page query types, this loop only carries forward queries that have entries in realtimeQueryAudiences. Once at least one route audience is found, the function returns scoped notices instead of the original broadcast, so any unscoped query in the same notice is omitted from every event and those regions never receive the realtime refresh. Keep a separate broadcast notice for queries without route audiences, or otherwise include them when splitting scoped notices.

Useful? React with 👍 / 👎.

Comment on lines +593 to +594
Cond: &ast.BinaryExpr{X: selExpr(id("event"), "Type"), Op: token.EQL, Y: sel("gowdkcontracts", "QueryInvalidationPresentationEventType")},
Body: block(&ast.ReturnStmt{Results: []ast.Expr{call(id("realtimeQueryInvalidationAudienceEvents"), id("event"))}}),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Route-scope generated invalidation notices

For page-owned query invalidations, the generated notice is emitted by currentContractEventSink via QueryInvalidationCommandEventSink(fanout, realtimeQueryInvalidations) before fanoutSink, so it is sent straight to SSE with an empty Audience and never reaches this scoping branch. A client connected to another authorized route therefore still receives the invalidation notice (query type/event IDs) for this route. Route-scope the invalidation sink itself or pass it through the realtimeSubscriptionFanout wrapper.

Useful? React with 👍 / 👎.

Comment thread internal/appgen/source_realtime.go Outdated
Comment on lines 499 to 501
define([]ast.Expr{id("requestPath")}, call(
selExpr(call(selExpr(selExpr(id("request"), "URL"), "Query")), "Get"),
stringLit("path"),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Do not trust the client-supplied stream route

The route audience is derived from the path query parameter, so any client can open /_gowdk/realtime/events?path=/some/other/page and be assigned that page's audience (subject only to that selected route's guards). For public or broadly authorized routes this bypasses the route isolation this change is trying to enforce and lets a page receive another page's scoped presentation events. Derive the audience from server-owned state, or treat this parameter only as a hint rather than authorization.

Useful? React with 👍 / 👎.

@cssbruno cssbruno merged commit 419ed3f into main Jun 27, 2026
24 checks passed
@cssbruno cssbruno deleted the fix/realtime-scope-sse-streams branch June 27, 2026 15:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

compiler Compiler internals, pipeline, and generated metadata

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[High] Scope realtime SSE streams by authorized route or audience

1 participant