feat(DT-3944): SignalWithStart system Nexus obfuscation — decode infrastructure + UI#3356
feat(DT-3944): SignalWithStart system Nexus obfuscation — decode infrastructure + UI#3356rossnelson wants to merge 15 commits into
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Add a Phase 1.5 dispatch in parseRawPayloadToJSON: when a Payload's
metadata says encoding=binary/protobuf and carries a messageType, look
the type up by full name in @temporalio/proto's static namespace and
return the typed JSON. After the outer decode, walk the result tree
and recursively decode any nested {metadata, data} Payload nodes
through the same pipeline so user-side payloads (json/plain, json/
protobuf, json/external-storage-reference, etc.) come out fully
parsed. Unknown messageTypes and decode errors fall through to the
existing path so behavior for non-binary/protobuf payloads is
unchanged.
This is the proposal for handling System Nexus operations like
SignalWithStartWorkflowExecutionRequest, where the gRPC envelope
arrives as binary protobuf and the inner Payloads carry user data.
Tests cover round-tripping a real SignalWithStart fixture and the
graceful fallback for an unknown messageType. 27/27 passing.
Caveat: protobufjs builds its decoders with Function() and so needs
'unsafe-eval' in the script-src CSP. That's relaxed in a separate
commit so reviewers can drop it before merge if we want a different
proto runtime (@bufbuild/protobuf has no eval).
@temporalio/proto uses protobufjs, which compiles per-message encoders/decoders via the Function() constructor. With the existing 'strict-dynamic' CSP and no 'unsafe-eval', binary/protobuf payload decode silently fails in the browser. Relax the dev/auto CSP to permit unsafe-eval so the new decode path works end-to-end during the LOE evaluation. If the team decides to ship browser-side proto decoding, the long-term option is to swap @temporalio/proto for @bufbuild/protobuf (which doesn't use eval) and drop this commit. Open question for review: do we keep this, or migrate the runtime?
…-binary-protobuf Moves lookupTemporalProtoType, base64ToUint8Array, looksLikeRawPayload, recursivelyDecodeNestedPayloads, and the full decode path into a new decode-binary-protobuf utility. The recursive walker now accepts a recurse callback rather than calling parseRawPayloadToJSON directly, eliminating the circular dependency. Adds unit tests covering successful decode, silent null on unknown type, warn-on-throw, looksLikeRawPayload edge cases, and the callback injection path.
…quests
Converts payload-decoder.svelte from {#await} to AbortController + $effect
so stale in-flight decodes cannot overwrite a newer result. Rewrites
codeServerRequest in data-encoder.ts to accept an optional AbortSignal and
retry transient errors (network/5xx) up to 3 attempts with 500ms/1000ms
delays, without retrying 4xx. Deletes the now-unused decode-payload-value
module (zero importers confirmed).
Adds a nexus-operation-registry that recognises the 4 system-level Nexus operation types (StartWorkflow, SignalWorkflow, SignalWithStartWorkflow, QueryWorkflow) and returns a human-readable descriptor with the embedded input payloads. NexusOperationRenderer uses the registry to show the embedded operation directly — users see "Signal With Start Workflow: EchoWorkflow" and the decoded input, not the raw protobuf wrapper. input-and-results-payload routes Nexus payloads through the renderer automatically; all other content paths are unchanged. Adds 6 registry tests covering the happy path and null returns for unknown types.
…bufbuild/protobuf Replace protobufjs runtime (which requires unsafe-eval via Function()) with @bufbuild/protobuf + @buf/temporalio_api.bufbuild_es. Static SCHEMA_REGISTRY map replaces the dynamic lookupTemporalProtoType traversal — adding a new operation type is a one-line entry. Removes unsafe-eval from the CSP.
Move the recurse callback into decodeBinaryProtobuf so the function fully owns the "decode this payload including anything nested inside it" contract. Make looksLikeRawPayload, base64ToUint8Array, and recursivelyDecodeNestedPayloads module-private. decode-payload.ts now has a single call site with no knowledge of the recursion internals.
Add *Response schemas for all four system Nexus operation types so completed event payloads (NexusOperationCompleted result, WorkflowExecutionCompleted result) decode alongside their corresponding request payloads.
f1e91c9 to
c4b99c9
Compare
Resolves axios downgrade from 1.15.2 to 1.15.0 introduced by using --theirs on the lockfile during rebase conflict resolution.
| return response.json(); | ||
|
|
||
| if (response.status >= 400 && response.status < 500) { | ||
| setLastDataEncoderFailure(err); |
There was a problem hiding this comment.
⚠️ Type 'PotentialPayloads' is not assignable to type 'IPayloads'.
| } | ||
|
|
||
| return decoderResponse; | ||
| setLastDataEncoderFailure(lastErr); |
There was a problem hiding this comment.
⚠️ Type 'PotentialPayloads' is not assignable to type 'IPayloads'.
| if (decoded) { | ||
| if (returnDataOnly) return decoded.data; | ||
| return { | ||
| metadata: { |
There was a problem hiding this comment.
⚠️ Argument of type '{ [k: string]: Uint8Array; } | null | undefined' is not assignable to parameter of type 'Record<string, unknown>'.
|
|
||
| try { | ||
| const data = parseWithBigInt(atob(String(payload?.data ?? ''))); | ||
| if (returnDataOnly) return data; |
There was a problem hiding this comment.
⚠️ Argument of type '{ [k: string]: Uint8Array; } | null | undefined' is not assignable to parameter of type 'Record<string, unknown>'.
|
) * feat(DT-3945): wire NexusOperationRenderer into event-card.svelte Detect system Nexus operation payloads in the payloads snippet and route them through NexusOperationRenderer instead of PayloadCodeBlock. Users see the embedded operation label and input instead of the raw protobuf wrapper fields. * feat(DT-3946): remap event name and suppress system fields for Nexus ops (#3499) * feat(DT-3946): remap event name and suppress system fields for Nexus ops For NexusOperationScheduled events where endpoint === '__temporal_system': - Override displayName with friendly label from NexusOperationDescriptor (e.g. "Signal With Start Workflow: target-workflow Scheduled") - Hide Endpoint, Service, Operation, RequestID from the detail fields - Suppress the Operation primary attribute badge in the compact row For NexusOperationCompleted events (detected via response payload messageType): - Override displayName with "Signal With Start Operation Completed" etc. - Hide RequestID from detail fields Updates event-card.svelte, event-summary-row.svelte, and adds getSystemNexusLabelFromResponsePayload to nexus-operation-registry.ts. * feat(DT-3947): remap timeline label and icon for system Nexus operations (#3500) * feat(DT-3947): remap timeline label and icon for system Nexus operations - get-group-name.ts: return "Signal With Start Operation" etc. for system Nexus ops (__temporal_system endpoint) instead of service.operation string - timeline-graph-row.svelte: derive effectiveCategory (signal/workflow) for system Nexus groups so they render with signal/workflow color and icon instead of the Nexus star and indigo treatment - nexus-operation-registry.ts: simplify all labels to "X Operation" (no workflow type suffix) — consistent everywhere including the renderer - event-summary-row.svelte: align compact row labels to use "Operation" - event-card.svelte: align SYSTEM_NEXUS_LABELS to use "Operation" - Update registry test to match new label format * feat(DT-3949): SystemNexusOperationRenderer — component map, dual inputs, label finalization (#3501) * feat(DT-3949): signal name sub-label and clickable workflow ID link in renderer NexusOperationRenderer now shows: - Signal name as a secondary line (Signal: test-signal) for signal kinds - Clickable workflowId link to /namespaces/{ns}/workflows/{workflowId} when the descriptor has a workflowId (resolves to latest run naturally) * feat: use 'other' category treatment for system Nexus ops in compact row * refactor(DT-3949): move workflowId and signalName to left-column detail fields Simplifies NexusOperationRenderer back to just label + embedded payload. Signal name and workflow ID are now injected into the event card's attribute map so they appear as standard left-column detail fields (Workflow ID, Signal Name) — matching the pattern used by other event types. * refactor: remove label from NexusOperationRenderer — redundant with card heading * refactor: component-map dispatcher for system Nexus operation rendering - Rename NexusOperationRenderer → SystemNexusOperationRenderer (explicit scope) - Component map keyed on NexusEmbeddedOperationKind — adding a new operation type requires only a new component file + one map entry, zero dispatcher changes - signal-with-start-renderer.svelte handles both signal and workflow start inputs with labels when both are present - default-renderer.svelte handles all other kinds (embeddedInput only) - Remove old nexus-operation-renderer.svelte Also: extract signalInput (signal payload) and workflowInput (workflow start input) separately in the SignalWithStart spec; update Go sample to include both payloads so the UI can be verified with real data. * fix: use signal category for system Nexus ops in compact row * feat: finalize SignalWithStart labels and state verbs - Label: 'Signal With Start Workflow Execution' (replaces 'Signal With Start Operation') - State suffix: Scheduled → 'Initiated', Completed → 'Delivered' - Both event-card.svelte and event-summary-row.svelte use NEXUS_STATE_VERBS map for signal/state verb mapping; other states (Failed, TimedOut, Canceled) fall back to spaceBetweenCapitalLetters * fix: update timeline group label for SignalWithStart to match new naming * fix: pass category prop to Dot so signal color applies on timeline endpoints
| const effectiveCategory = $derived(systemNexusCategory ?? group.category); | ||
|
|
||
| let decodedLocalActivity: SummaryAttribute | undefined = $state(undefined); | ||
|
|
There was a problem hiding this comment.
⚠️ 'attrs' is possibly 'null' or 'undefined'.
|
|
||
| let { descriptor, maxHeight }: Props = $props(); | ||
| </script> | ||
|
|
There was a problem hiding this comment.
⚠️ Type 'IPayload[] | null | undefined' is not assignable to type 'object | IPayloads | IPayload'.
| <p class="text-xs text-secondary/70">Workflow Input</p> | ||
| <PayloadCodeBlock value={descriptor.workflowInput} {maxHeight} /> | ||
| {/if} | ||
| {:else} |
There was a problem hiding this comment.
⚠️ Type 'null' is not assignable to type 'object | IPayloads | IPayload'.
|
|
||
| if (isNexusOperationScheduledEvent(event)) { | ||
| return `${event.nexusOperationScheduledEventAttributes.service}.${event.nexusOperationScheduledEventAttributes.operation}`; | ||
| const attrs = event.nexusOperationScheduledEventAttributes; |
There was a problem hiding this comment.
⚠️ 'attrs' is possibly 'null' or 'undefined'.⚠️ 'attrs' is possibly 'null' or 'undefined'.
| const attrs = event.nexusOperationScheduledEventAttributes; | ||
| if (String(attrs.endpoint ?? '') === '__temporal_system') { | ||
| return ( | ||
| SYSTEM_NEXUS_OPERATION_LABELS[String(attrs.operation ?? '')] ?? |
There was a problem hiding this comment.
⚠️ 'attrs' is possibly 'null' or 'undefined'.⚠️ 'attrs' is possibly 'null' or 'undefined'.
Remove Start, Signal, and Query operation entries from:
- SCHEMA_REGISTRY (decode-binary-protobuf.ts)
- NEXUS_OPERATIONS and SYSTEM_NEXUS_RESPONSE_LABELS (nexus-operation-registry.ts)
- NexusEmbeddedOperationKind union type (single member: signal-with-start-workflow)
- SYSTEM_NEXUS_LABELS maps (event-card.svelte, event-summary-row.svelte)
- SYSTEM_NEXUS_OPERATION_LABELS (get-group-name.ts)
- systemNexusCategory (timeline-graph-row.svelte — exact match instead of includes('Signal'))
Also remove unused workflowType and taskQueue from NexusOperationDescriptor
and OperationSpec — these were extracted but never read by any UI component.
| const SYSTEM_NEXUS_OPERATION_LABELS: Record<string, string> = { | ||
| SignalWithStartWorkflowExecution: 'Signal With Start Workflow Execution', | ||
| }; | ||
|
|
There was a problem hiding this comment.
⚠️ Function lacks ending return statement and return type does not include 'undefined'.
|
|
||
| if (isNexusOperationScheduledEvent(event)) { | ||
| return `${event.nexusOperationScheduledEventAttributes.service}.${event.nexusOperationScheduledEventAttributes.operation}`; | ||
| const attrs = event.nexusOperationScheduledEventAttributes; |
There was a problem hiding this comment.
⚠️ 'attrs' is possibly 'null' or 'undefined'.
| return `${event.nexusOperationScheduledEventAttributes.service}.${event.nexusOperationScheduledEventAttributes.operation}`; | ||
| const attrs = event.nexusOperationScheduledEventAttributes; | ||
| if (String(attrs.endpoint ?? '') === '__temporal_system') { | ||
| return ( |
There was a problem hiding this comment.
⚠️ 'attrs' is possibly 'null' or 'undefined'.
| return `${attrs.service}.${attrs.operation}`; | ||
| } | ||
| }; | ||
|
|
There was a problem hiding this comment.
⚠️ Function lacks ending return statement and return type does not include 'undefined'.
…ent-display utility
Move SYSTEM_NEXUS_LABELS, NEXUS_STATE_VERBS, detection logic, display name
computation, hidden fields, and extra attribute injection out of event-card.svelte
and event-summary-row.svelte into a shared utility.
Both components now call getSystemNexusEventDisplay(event) and consume the
{ displayName, hiddenFields, extraAttributes } result — no system Nexus
knowledge remains in the general-purpose components.
|
|
||
| const secondaryAttribute = $derived( | ||
| getSecondaryAttributeForEvent( | ||
| isEventGroup(event) ? event.lastEvent : event, |
There was a problem hiding this comment.
⚠️ Argument of type 'string | undefined' is not assignable to parameter of type 'string'.
| event: WorkflowEvent, | ||
| ): SystemNexusEventDisplay | null => { | ||
| if (isNexusOperationScheduledEvent(event)) { | ||
| const attrs = event.nexusOperationScheduledEventAttributes; |
There was a problem hiding this comment.
⚠️ 'attrs' is possibly 'null' or 'undefined'.
| if (isNexusOperationScheduledEvent(event)) { | ||
| const attrs = event.nexusOperationScheduledEventAttributes; | ||
| if (String(attrs.endpoint ?? '') !== '__temporal_system') return null; | ||
|
|
There was a problem hiding this comment.
⚠️ 'attrs' is possibly 'null' or 'undefined'.
| const rawState = event.name.replace('NexusOperation', ''); | ||
| const state = | ||
| NEXUS_STATE_VERBS[rawState] ?? spaceBetweenCapitalLetters(rawState); | ||
|
|
There was a problem hiding this comment.
⚠️ 'attrs' is possibly 'null' or 'undefined'.
| }; | ||
| } | ||
|
|
||
| if (isNexusOperationCompletedEvent(event)) { |
There was a problem hiding this comment.
⚠️ 'event.nexusOperationCompletedEventAttributes' is possibly 'null' or 'undefined'.
Summary
Implements browser-side decode of system Nexus operation payloads for
SignalWithStartWorkflowExecution, and replaces all raw Nexus wrapper exposure with user-friendly presentation.Decode infrastructure
decode-binary-protobuf.ts— new utility: decodesbinary/protobufpayloads via@bufbuild/protobuf+@buf/temporalio_api.bufbuild_es.decodeBinaryProtobuf(payload, recurse?)is the single public API; internal helpers are private. Schema registry keyed on fully-qualified messageType — adding a new operation type is one line.@bufbuild/protobufmigration — nounsafe-evalrequired (replaces protobufjs runtime which usedFunction())parseRawPayloadToJSON— binary/protobuf payloads decode transparently through all existing render surfaces (event card, compact row, JSON view, copy)System Nexus obfuscation (SignalWithStart only)
Event card:
Nexus Operation Scheduled→Signal With Start Workflow Execution Initiatedendpoint,service,operation,requestIdWorkflow ID,Signal Name(decoded from the binary/protobuf request payload)Compact event row:
Signal With Start Workflow Execution Initiated/DeliveredTimeline:
Signal With Start Workflow ExecutionCompleted event:
Signal With Start Workflow Execution Delivered{runId, started: true}decoded fromSignalWithStartWorkflowExecutionResponserequestIdhiddenComponent architecture
SystemNexusOperationRenderer— dispatcher keyed onNexusEmbeddedOperationKind; adding a new operation type = new component file + one map entrysystem-nexus/signal-with-start-renderer.svelte— renders signal input + workflow start inputsystem-nexus/default-renderer.svelte— fallback for other kindsScope
Only
SignalWithStartWorkflowExecutionis implemented and tested. Other system Nexus operation types (StartWorkflow, SignalWorkflow, QueryWorkflow) are not included — entries will be added when those operations are enabled server-side and testable.Closes
DT-3945, DT-3946, DT-3947, DT-3948, DT-3949. Part of DT-3944.
Test plan
pnpm test -- --runpasses