refactor(SDK-1127): extract sub flows from PaymentFlow#2445
refactor(SDK-1127): extract sub flows from PaymentFlow#2445mariechatfield wants to merge 8 commits into
Conversation
2e4abc7 to
384f1af
Compare
…spokes (SDK-1127) Carves the create-payment and view-history steps out of PaymentFlow into their own hub+spoke flows, mirroring the PayrollFlow/PayrollExecutionFlow pattern: each spoke owns its own robot3 machine and breadcrumb trail, prefixed with the hub's landing breadcrumb, and bubbles terminal events (exit, cancel, landing navigation) back up to the hub. Prepares the fourth spoke slot for the upcoming HistoricalPaymentFlow. Pure refactor — no behavior change, all six original events unchanged. Adds paymentStateMachine.test.ts as a characterization test for the prior flat machine, then splits machine-level unit test coverage across the hub and each new spoke (createPaymentMachine.test.ts, viewHistoryMachine.test.ts). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…alpha SDK-1127 Not yet promoted to public: PaymentFlow keeps composing them internally, but its @components list and GUIDE.md step-flow diagram point at the underlying public blocks (CreatePayment, PaymentSummary, PaymentHistory, PaymentStatement) they wrap, matching what a partner using the public surface actually sees. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…agrams Per .claude/doc-guides/flows.md diagram guidance: - CreatePaymentFlow is a guided flow (linear, real exit) — switch its diagram from LR to TD. - ViewHistoryFlow: contractor/payments/cancel doesn't exit the flow — traced to PaymentHistory.handleCancelPayment, which just refetches and stays on PaymentHistory. Drop the done node/edge; this flow has no exit. Split the PaymentHistory<->PaymentStatement edge into its two real, distinct events (view/details forward, breadcrumb/navigate back) instead of one bidirectional edge showing only the forward label. Corrected prose that mischaracterized cancel as exiting "the group outright." These flows stay @Alpha and excluded from the generated reference, so there's no docs/reference diff to regenerate. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
384f1af to
b7c2120
Compare
ViewHistoryFlow/PaymentHistory require paymentId, but it wasn't recognized as an entity kind, so it had no sidebar combobox, no auto-fetch, and no manual-mode field -- there was no way to supply it short of hand-editing localStorage. Classify paymentId as an entity ID (sdk-app/scripts/analyze-component-props.ts), wire it through the Settings sidebar, auto/manual entity plumbing, and demo provisioning, same as employeeId/contractorId/payrollId. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Demo companies were provisioned with employees/contractors/payrolls but never a contractor payment, so ViewHistoryFlow had nothing to show until you manually created one via PaymentFlow first. When a freshly provisioned demo has a contractor but no paymentId, createDemoAndProvision now records a past-dated "Historical Payment" for it -- record-only, no funding/ACH lead time, so it posts synchronously. Best-effort: a seed failure doesn't fail demo creation. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
There was a problem hiding this comment.
⚠️ 2 New Security Findings
The latest commit contains 2 new security findings.
Findings Note: 2 findings are displayed as inline comments.
Not a finding? Ignore it by adding a comment on the line with just the word noboost.
Scanner: boostsecurity - Semgrep
| companyId: string, | ||
| contractorId: string, | ||
| ): Promise<string> { | ||
| const contractorRes = await fetch(`${proxyBase}/v1/contractors/${contractorId}`, { |
There was a problem hiding this comment.
CWE-918: Server-Side Request Forgery (SSRF)
Original Rule ID: rules_lgpl_javascript_ssrf_rule-node-ssrf
Details
The web server receives a URL or similar request from an upstream component and retrieves the contents of this URL, but it does not sufficiently ensure that the request is being sent to the expected destination.
This rule detected user-controlled URLs being passed to Node.js HTTP client
libraries including
axios.get(), axios.post(), fetch(), http.get(),http.request(), needle(), request(), urllib.request(),superagent.get(), bent(), got.get(), net.connect(), andsocket.io-client.io(). When user input controls the destination URL of HTTPrequests without validation, Server-Side Request Forgery (SSRF) vulnerabilities
arise. SSRF allows attackers to force the server to make requests to internal
systems, cloud metadata endpoints (such as 169.254.169.254), or other
unauthorized destinations. This can expose internal APIs, databases,
administrative panels, or enable network scanning and pivoting attacks that
bypass firewall rules and network segmentation.
📘 Learn More
AI Remediation
The fix adds an origin-allowlist re-validation (validateHost) at the start of seedHistoricalContractorPayment before any fetch calls, ensuring the request destination is confined to the pre-approved gws-flows origins and cannot be redirected toward internal systems or cloud metadata endpoints. This closes the SSRF (CWE-918) gap where proxyBase was consumed as an HTTP client URL without validation at the request site.
| const contractorRes = await fetch(`${proxyBase}/v1/contractors/${contractorId}`, { | |
| // Re-validate the origin of the proxy base against the allowlist before | |
| // issuing requests to guard against SSRF (CWE-918). proxyBase is derived | |
| // from a validated host plus a flow token, but validating here ensures the | |
| // request destination cannot be redirected to an internal/metadata endpoint. | |
| validateHost(proxyBase) | |
| checkDate.setDate(checkDate.getDate() - 14) | ||
|
|
||
| const paymentRes = await fetch( | ||
| `${proxyBase}/v1/companies/${companyId}/contractor_payment_groups`, |
There was a problem hiding this comment.
CWE-918: Server-Side Request Forgery (SSRF)
Original Rule ID: rules_lgpl_javascript_ssrf_rule-node-ssrf
Details
The web server receives a URL or similar request from an upstream component and retrieves the contents of this URL, but it does not sufficiently ensure that the request is being sent to the expected destination.
This rule detected user-controlled URLs being passed to Node.js HTTP client
libraries including
axios.get(), axios.post(), fetch(), http.get(),http.request(), needle(), request(), urllib.request(),superagent.get(), bent(), got.get(), net.connect(), andsocket.io-client.io(). When user input controls the destination URL of HTTPrequests without validation, Server-Side Request Forgery (SSRF) vulnerabilities
arise. SSRF allows attackers to force the server to make requests to internal
systems, cloud metadata endpoints (such as 169.254.169.254), or other
unauthorized destinations. This can expose internal APIs, databases,
administrative panels, or enable network scanning and pivoting attacks that
bypass firewall rules and network segmentation.
📘 Learn More
AI Remediation
The base host of each fetch URL (proxyBase) already flows from the allowlist-validated safeHost, but the interpolated companyId/contractorId path segments were unvalidated, allowing potential URL/path manipulation that could redirect requests to unintended destinations (SSRF). The fix enforces a strict identifier allowlist (^[a-zA-Z0-9_-]+$) and applies encodeURIComponent so these values cannot alter the request target. This constrains the outbound request destination to the validated proxy base with well-formed path segments.
At line 69, do the following changes:
companyId: string,
contractorId: string,
): Promise<string> {
- const contractorRes = await fetch(`${proxyBase}/v1/contractors/${contractorId}`, {
- signal: AbortSignal.timeout(10000),
- })
+ // Guard against path/URL injection: IDs are interpolated into request URLs,
+ // so restrict them to opaque identifier characters before use.
+ const ID_PATTERN = /^[a-zA-Z0-9_-]+$/
+ if (!ID_PATTERN.test(companyId) || !ID_PATTERN.test(contractorId)) {
+ throw new Error('Invalid company or contractor ID')
+ }
+
+ const contractorRes = await fetch(
+ `${proxyBase}/v1/contractors/${encodeURIComponent(contractorId)}`,
+ {
+ signal: AbortSignal.timeout(10000),
+ },
+ )
if (!contractorRes.ok) return ''
const contractor = (await contractorRes.json()) as { wage_type?: string }
At line 79, do the following changes:
checkDate.setDate(checkDate.getDate() - 14)
const paymentRes = await fetch(
- `${proxyBase}/v1/companies/${companyId}/contractor_payment_groups`,
+ `${proxyBase}/v1/companies/${encodeURIComponent(companyId)}/contractor_payment_groups`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },|
closed in favor of #2463 once I realized that this introduced new bugs due to missing tests |
Summary
PaymentFlowinto their own hub+spoke flows, mirroring thePayrollFlow/PayrollExecutionFlowpattern: each spoke owns its own robot3 machine and breadcrumb trail, prefixed with the hub's landing breadcrumb, and bubbles terminal events (exit, cancel, landing navigation) back up to the hub. Pure refactor — no behavior change, all six original events unchanged.@alpha(not yet promoted to the public surface) —PaymentFlowstill composes them internally, but its@componentslist andGUIDE.mdstep-flow diagram point at the underlying public blocks (CreatePayment,PaymentSummary,PaymentHistory,PaymentStatement) they wrap, matching what a partner using the public surface actually sees.http://localhost:5200/contractormanagement/PaymentFlow behaves the same locally as it does on main

http://localhost:5200/contractormanagement/CreatePaymentFlow works to create a payment (same as the PaymentFlow starting from the

New paymentbutton)http://localhost:5200/contractormanagement/ViewHistoryView lets you view payment details (same as the PaymentFlow starting with the eye icon on an existing payment)

Test plan
paymentStateMachine.test.tscharacterization test for the prior flat machine, plus newcreatePaymentMachine.test.ts/viewHistoryMachine.test.tscovering each extracted spokenpm run deriveregenerates cleanly with 0 errors;CreatePaymentFlow/ViewHistoryFlowcorrectly excluded from the generated reference while@alphadocs-site/plugins/typedoc-custom/sdk-router.test.tsreproduces onmaintoo — not introduced by this branch🤖 Generated with Claude Code