Skip to content

fix(web): polyfill Buffer for Privy transfers - #126

Merged
kapustazh merged 2 commits into
developfrom
fix/privy-browser-buffer
Sep 13, 2026
Merged

kapustazh merged 2 commits into
developfrom
fix/privy-browser-buffer

Conversation

@kapustazh

Copy link
Copy Markdown
Collaborator

Summary

Privy transaction approval reached a browser SDK path that referenced Node's
Buffer, so the transfer failed after approval with Buffer is not defined.
The web entry now installs the existing buffer polyfill before Privy's lazy
wallet module loads.

Scope and acceptance criteria

  • The change is limited to the Privy browser compatibility failure.
  • The browser exposes globalThis.Buffer before wallet code runs.
  • Wallet selection and transaction payload behavior remain unchanged.
  • No unrelated cleanup is included.

Product and security invariants

  • Tenant isolation remains fail-closed.
  • Sponsor authorization, auditability, and caps remain unchanged.
  • Recipients cannot modify controls or access sponsor-only data.
  • No secret, token, production identifier, or personal data is committed.

Invariant notes: this frontend-only compatibility fix does not change Business
Intent identity, settlement ownership, retry behavior, network, token, amount,
recipient validation, or Privy authorization.

Validation

pnpm --filter @oneshot/web typecheck: PASS
pnpm --filter @oneshot/web test: PASS (17 files, 93 tests)
pnpm --filter @oneshot/web build: PASS
pnpm --filter @oneshot/web test:browser: PASS (8 Chromium tests)
pnpm lint: PASS
pnpm format:check: PASS
git diff --check: PASS

Local Node 22.23.2 differs from the repository's requested Node 24.19.0; CI
validates the pinned runtime.

Independent review evidence

Gate A — exact candidate tree before push

  • Base commit SHA: 5e1c9e9210ef22416ee8b62713e9a3e597bb4577
  • Candidate tree SHA: 48cf89bfb0071076eacd742bb8c5ab3c820f75d4
  • Candidate commit SHA: 4721961d7e55047d120e2d4e51dc3a40b4d520bb
  • Reviewer tool: not run under the user's explicit standing instruction to continue without FreePi
  • Verdict: NOT RUN; no PASS claimed

Gate B — exact remote PR head

  • Verdict: NOT RUN

Risk and rollback

  • Residual risk: final confirmation requires one user-approved Privy transfer in the deployed testnet UI.
  • Rollback: revert 4721961d7e55047d120e2d4e51dc3a40b4d520bb.

Human merge

  • A human owner reviews and performs the merge.

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Sep 13, 2026 •

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
oneshot dcc7307 Sep 13 2026, 06:59 AM

@kapustazh kapustazh left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Review: PR #126 — fix(web): polyfill Buffer for Privy transfers

Verdict: Approve — correct, minimal, well-scoped. All 5 checks green (ESLint/TS, browser acceptance, Markdown, Workers build, repo policy).

What's good

  • Right fix, right place. buffer@6.0.3 as a direct dep + globalThis.Buffer ??= Buffer in main.tsx is the standard remedy for Privy's transaction path referencing Node's Buffer in the browser. The assignment executes at entry-point evaluation, before Privy's lazy wallet chunk loads, and the error occurred at transaction time (after approval) — so ordering is sound.
  • ??= is the right operator. It won't clobber a Buffer if a bundler or another polyfill already provided one. Defensive and correct.
  • Regression guard. p5.spec.ts now asserts typeof globalThis.Buffer === 'function' in real Chromium, so a future refactor that drops the polyfill fails CI instead of failing a user's transfer.
  • Repo hygiene. Exact-pinned version matches the lockfile style of the repo; lockfile updated consistently; scope matches the PR description; security invariants untouched (frontend-only, no auth/settlement change).

Minor notes (non-blocking)

  1. Test coupling — the Buffer assertion lives inside the p5 test named "keeps the public landing separate from the authenticated cabinet". A one-line dedicated test (or a rename) would be cleaner, but it works.
  2. Expect possible follow-ups. Only Buffer is polyfilled. This dependency chain (Privy/WalletConnect/viem) sometimes trips next on process is not defined or global. If the deployed testnet run surfaces one of those, the same entry-point pattern applies — or adopt vite-plugin-node-polyfills if it becomes more than one global. Worth watching, not blocking.
  3. Typing fragility — globalThis.Buffer ??= Buffer typechecks today (CI confirms), but it depends on Buffer being visible on globalThis via transitive @types/node. If that ever disappears from the graph, a declare global shim in apps/web/src makes it explicit.
  4. Residual risk is honestly declared in the PR: the real confirmation is one user-approved Privy testnet transfer against the deployed Worker. Do that before merge per the repo's own handoff note.

The .agent/context/ session doc follows the repo's established convention (repository-policy check enforces it), and the disclosed Worker version ID / Privy app ID are not secrets.

Suggested merge checklist: ✅ CI green → ⬜ one live approved testnet transfer succeeds → ⬜ human owner merges (per repo rule: agents don't merge).

@kapustazh kapustazh mentioned this pull request Sep 13, 2026
9 tasks done
@kapustazh
kapustazh marked this pull request as ready for review September 13, 2026 07:31
@kapustazh
kapustazh merged commit 8432029 into develop Sep 13, 2026
5 checks passed
kapustazh added a commit that referenced this pull request Sep 13, 2026
* fix(subgraph): remove trailing empty line in schema.graphql

* docs: plan Arc Circle qualification path

* feat(web): compose P5 frontend acceptance

* feat(web): merge P5 frontend acceptance implementations

* feat(worker): add production runtime

* test(worker): wire integration ledger dependencies

* feat: package gate p6 demo

* fix: use pnpm version

* feat(web): define the operator session port

* feat(web): gate the console behind an operator sign-in surface

* feat(api): verify Privy access tokens against an operator allowlist

* feat(api): route credentials to one authenticator by token shape

* feat(api): load and validate Privy operator auth configuration

* feat(api): compose Privy and service-bearer authenticators at runtime

* feat(web): require an operator session before the console renders

* style(web): format operator session wiring

* feat(web): mount the Privy provider lazily behind the session port

* docs: describe the Privy operator sign-in boundary

* feat(web): Behance-grade minimalist dark glassmorphism landing and console

* feat(auth): open wallet authentication with wildcard allowlist and UI polish

* fix(web): buffer proxy request body, enforce CORS headers, and default production Privy App ID

* fix(worker): preserve production runtime dependencies

* fix(worker): sign and broadcast on custom Arc chain without relayer

* fix(recovery): verify hashless graph candidates on Arc

* fix(worker): persist provider request identity before submit

* fix(recovery): preserve verified receipt metadata

* fix(worker): require production subgraph mcp admission

* fix(api): harden production operator authentication

* feat(storage): persist operational metric events

* docs: document recovery hardening and fix integration fixtures

* test(worker): reset operational metrics between integration cases

* fix: harden settlement safety boundaries

* fix(recovery): query Arc subgraph via Studio

* fix(recovery): allow delivered reconciliation retries

* docs(web): design for brand frontend repaint and wallet picker

The brand direction settled on the design canvas lives nowhere in the
repository, and the palette is restated independently in three
stylesheets. Record the approved design for putting it in code: a shared
token and asset package, light and dark boards switched by data-theme,
the commit-ring mark and derived hero geometry, and a searchable wallet
picker built on Privy's headless SIWE flow.

Privy's own login modal has no search hook and login() cannot be
narrowed to a single wallet, so the picker is ours; useLoginWithSiwe is
the supported headless path it uses.

* docs(web): add the brand frontend implementation plan

* docs(web): correct four defects found in the brand frontend plan preflight

* docs(web): give the lime field its own fixed ink token

* feat(brand): add the brand token package with a two-theme contrast audit

* docs(plan): graph studio agent recovery plan and durable handoff context

* feat(brand): add the commit-ring mark with its reduced cut

* fix: harden Arc Testnet settlement

* docs(web): correct the hero vertex count to four per shape

* feat(brand): derive the diagonal hero cut from the box size

* docs(web): restore globals before clearing storage in the theme teardown

* feat(web): add theme selection with a pre-paint guard

* docs(web): wrap the brand alias to the repo print width

* feat: implement Graph Studio agent recovery

* feat(settlement-ui): remove allowlist display from PolicySummaryPanel

* feat(worker): remove recipient allowlist restriction and support wildcard

* fix(worker,ui): wait for transaction receipt on submission and improve authorization status inference

* style: format settlement-ui contract and cloudbuild config

* fix: add shared API rate limiting and complete worker deploy config

* fix: harden Privy fallback and integration cleanup

* docs: reconcile production runtime and qualification status

* test: avoid migration rollback fixture collision

* feat(web): repaint the console shell onto the brand tokens

- Replace the navy/cyan literal palette in apps/web/src/styles.css with
  @oneshot/brand's --os-* tokens throughout; zero hex/rgb literals remain,
  enforced by a new guard test (test/styles.test.ts).
- Apply the task-5 mapping table rule by rule: page ink on page/surface
  context, panel ink on panel/card/console context, signal for accents,
  the three ledger-state tokens for success/warning/error, os-line for
  borders and dividers. Delete glows, translucent-navy washes, and
  gradients rather than inventing replacement tokens.
- Flatten every control to font-weight 300 and either a 999px pill
  (buttons, badges, inputs, chips) or var(--os-radius-lg) (panels,
  cards, the console container, the top nav bar).
- Replace the button, tab-strip, and state-card rules verbatim per the
  brief, and append the new .theme-toggle, .hero-cut/-figure/-panel/
  -copy/-plain, and .wallet-* classes for tasks 6, 7, and 10.
- Fix the 101-char @oneshot/brand alias line in vite.config.ts and
  vitest.config.ts (prettier printWidth 100), carried over from task 4.

* fix(web): correct ink/panel-ink contrast bugs in the stylesheet

Review found three Critical contrast bugs in the Task 5 token repaint,
caused by --os-ink/--os-panel (and their muted/panel-ink counterparts)
colliding in the light theme while the dark theme masks the mistake:

- button.secondary took --os-ink but both real usages (IntentStatusView's
  Refresh, IntentForm's New obligation ID) sit inside .panel containers,
  making the label invisible in light theme. Switched to color: inherit
  so it always takes whatever ink its container sets.
- .machine-token used panel-ink unconditionally, correct for its
  .login-gate usage but wrong for its second usage inside
  .operator-identity (a page-surface container). Added
  .operator-identity-scoped overrides using the page ink tokens, ordered
  so the summary's :hover state still wins in both contexts.
- .surface-empty (page-ink) and .panel (panel-ink) tie in specificity and
  both apply to FrontendSurfaces' <section className="panel
  surface-empty">, reached by opening Settlement/Recovery before
  selecting an intent. Repointed .surface-empty at --os-panel-ink-muted.

Also, three smaller findings:

- Added a .state-failed_safe strong rule (--os-state-failed) — FAILED_SAFE
  is a real terminal state the state-card block had no color for.
- Added missing .muted and .field-label rules (both previously unstyled),
  scoped to the panel ink tokens matching their current usages in
  IntentStatusView and IntentForm.
- Tightened styles.test.ts's fonts/tokens import-order assertion to a
  single anchored regex so a rule inserted between the two @imports
  (silently disabling the whole token layer) now fails the test, instead
  of slipping past the old indexOf/toContain pair.

Verified: pnpm --filter @oneshot/web test (11 files, 58 tests, all pass),
pnpm typecheck, pnpm --filter @oneshot/web lint, and npx prettier --check
on both changed files all clean. No hex/rgb literal in styles.css.

* feat(web): put the commit-ring mark and the theme toggle in the shell

Adds afterEach cleanup() to app-brand.test.tsx (missing from the brief's
verbatim test code); without it, jsdom's document persists across the file's
three render(<App />) calls (no globals:true/setupFiles wires up RTL's
auto-cleanup in this repo), and the third test's getByRole('button', { name:
/theme/iu }) matches multiple stacked toggles. Every other multi-render test
file in apps/web already calls cleanup() for the same reason.

* feat(web): cut the diagonal hero from derived geometry

* docs: reshape plan around resumable paid tools

* feat(ui): read the slice palettes from the brand tokens

* fix(brand): guard the contrast audit and fix panel border contrast

Two review findings against the slice-palette work:

- packages/settlement-ui/test/contrast.test.ts: the WCAG AA loop skipped
  any token resolveColour couldn't resolve instead of failing, so a
  future rename/typo in packages/brand/src/tokens.css would silently drop
  a channel from the audit while the test stayed green. Assert the value
  resolved before continuing, so an unresolvable token fails loudly.
  Verified with a deliberate break (renamed a var() target) that now
  fails the test, then reverted it.

- packages/brand/src/tokens.css: --os-panel is identical in both themes
  but --os-line flips with the theme, so a border painted with --os-line
  on a panel reads correctly in dark and disappears in light. Add
  --os-panel-line (light ink at low opacity, theme-invariant like the
  surface it borders) and repoint every border/divider drawn directly on
  --os-panel in apps/web, settlement-ui, and recovery-ui stylesheets onto
  it, tracing each declaration to its actual container. Borders on the
  page ground or --os-surface keep --os-line unchanged.

* feat: implement plan gap analysis

* test: align CI coverage with job cabinet

* test: isolate worker postgres integration suites

* feat(web): separate tools and jobs workspace

* feat: add Arc transfer quote and approval demo

* feat(web): discover installed wallets over EIP-6963

* fix(web): harden EIP-6963 announcement validation and its test coverage

- Finding 1: icon must be a data:image/ URI or it is dropped (icon now
  optional); the wallet itself is still kept. Fixes a zero-click IP/UA
  beacon leak via <img src>.
- Finding 2: replaced the vacuous short-circuited malformed-announcement
  test with targeted cases for missing/blank rdns, absent provider,
  non-callable provider.request, and the https-icon-dropped behaviour.
- Finding 3: the repeat-announcement test now re-announces the same uuid
  with a different name and provider object, and asserts the store keeps
  the first one by value and by reference.
- Finding 4: cap uuid/name/rdns at 256 chars and icon at 256 KiB; oversized
  fields are rejected the same way malformed ones are.
- Finding 5: documented that detectWallets() registers a permanent window
  listener and must be called once per app session.

No behaviour change outside parseAnnouncement's validation and the new
JSDoc; no .tsx files touched; no console/log statements added.

* feat(web): add a searchable wallet picker

* feat: add Circle x402 demo rail

* feat(web): sign in through Privy's headless SIWE flow

- OperatorSession gains an optional signInWithWallet(wallet) -> Promise<void>,
  present only on the configured Privy session. LoginGate renders the
  searchable WalletPicker when it is offered and falls back to the plain
  Privy button (and unconfiguredOperatorSession) when it is not.
- usePrivyOperatorSession implements signInWithWallet via Privy's headless
  useLoginWithSiwe(): eth_requestAccounts -> generateSiweMessage (bound to
  Arc Testnet eip155:5042002) -> personal_sign -> loginWithSiwe. Both wallet
  RPC responses are validated (non-array/empty accounts, non-string
  signature) and rejected with a detail-free error before use; nothing is
  ever logged.
- Brand Privy's fallback modal via its appearance config (theme, accentColor,
  walletList) — the only literal colours in this change, Privy's own hex API.
- Fix an EIP-6963 listener leak: detectWallets() registers a window listener
  with no removal path, and WalletPicker called it on every mount. Add a
  lazy module-level singleton getWalletStore() and switch WalletPicker to it
  so remounting across sign-out/sign-in cycles never re-registers a
  listener. detectWallets() itself is unchanged and still directly tested.

* fix(web): measure the hero before paint to stop the load flash

Hero.tsx measured its box in a useEffect, which React runs after the
browser paints. This app renders purely client-side (main.tsx uses
createRoot, no SSR), so every load at desktop width painted
.hero-plain for one frame before flipping to .hero-cut — a visible
flash on the brand's signature element on every load.

Switch to useLayoutEffect so the initial measurement runs
synchronously before paint. The ResizeObserver wiring for subsequent
resizes is unchanged. useLayoutEffect warns when it runs during SSR,
but this app has none; the existing hero tests (jsdom, a real DOM) ran
clean with no such warning.

* fix(web): time out unresponsive wallet requests during sign-in

signInWithWallet called wallet.provider.request(...) twice
(eth_requestAccounts, then personal_sign) with no timeout. A provider
that never resolves — a crashed or backgrounded extension — left
WalletPicker stuck in its busy state permanently, with no way for the
operator to retry or pick another wallet short of reloading.

Wrap each request in withWalletTimeout, a 2-minute race against a
timer. Two minutes is generous by design: the operator is interacting
with their own wallet UI for both calls (approving a connection,
reading and signing a SIWE message), so the timeout must not cut off a
slow but honest human, only a provider that will never answer at all.

The timeout's rejection carries a fixed, generic message with no
wallet data (no address, message, or signature) — WalletPicker already
replaces every thrown error with a sanitized line, and this keeps that
contract intact even if the raw error is ever read elsewhere.

Add test/privy-session.test.tsx, which mocks @privy-io/react-auth and
uses fake timers to prove a never-resolving provider rejects at
exactly the timeout instead of hanging, and that the rejection message
contains none of the wallet's identifying fields.

* docs: record x402 review evidence

* docs: fix context markdown lint

* docs(web): document the brand package and the theme guard

* docs: fix markdown lint

* docs: record final gate evidence

* feat: add job-aware activity audit

* docs: record activity audit checks

* docs: record final activity state

* docs: bind activity audit context

* docs: bind activity audit context

* docs: bind activity audit context

* feat: add r4 response-loss drill

* docs: add r5 release packet

* feat(web): make report payment inputs configurable

* feat: integrate paid API settlement into site

* fix: clear paid API CI failures

* test: include paid API records in postgres cleanup

* feat: add Circle x402 seller service

* fix(web): polish workspace loading state

* fix(web): allow seller proxy redirects safely

* fix(web): preserve wallet SIWE login

* docs: record deployment gate evidence

* fix(web): canonicalize wallet address for SIWE

* fix: persist seller worker origin

* fix(web): use Privy native login modal

* fix: bound Privy reference IDs

* fix(x402): reconcile Circle transfer UUIDs

Persist Circle transfer identity before batch confirmation and verify Gateway submitBatch receipts. Add provider_transfer_id migration for safe recovery of paid API results.

* docs: record Circle x402 PR

* test(storage): cover transfer migration

* feat(web): simplify payment workspace UX

* fix(web): restore contrast and add rescue plan

* docs: record rescue draft PR handoff

* docs: fix handoff link formatting

* fix(x402): bind approval to quoted payment

* docs: record Arc rescue handoff

* docs: refresh rescue handoff state

* fix(web): restore workspace hero and action states

* fix(web): preserve selected tab contrast

* feat(web): redesign payment proof and recovery

Merged after Gate A and Gate B PASS with all required CI checks green.

* fix: harden Privy settlement response handling

* feat: support user-funded wallet payments

* fix: preserve legacy workspace composition

* test: align postgres fixtures with merged migrations

* fix: show verified user wallet payment proof

* fix(web): make the operator console readable in both themes (#96)

* fix(web): make the operator console readable in both themes

The console shipped unreadable text in both themes from one mistake made in
two directions. --os-panel and --os-field are the same colour in light and
dark and carry their own fixed inks; --os-ground and --os-surface flip with
the theme. Pairing one family's ink with the other family's surface is
invisible in exactly one theme, so each instance survived review done in the
other one: the tab strip used panel ink on the page ground, and the quote and
paid-API summaries used panel ink on the lime field.

Pair each surface with the ink family that owns it, and add a structural guard
that resolves every rule's nearest painting ancestor and fails on any fixed ink
over a flipping surface. Verified against the previous stylesheet: it reports
all six pairings that existed there.

Also in the cabinet:

- Remove the Wallet & permissions and Developer access sections. Both were
  read-only restatements of facts the other sections already show, and neither
  had a control behind it.
- Give .panel-heading a rule. It had none, so every panel that pairs a title
  with an action stacked them flush.
- Let the hero copy sit in normal flow and set its own height, measured and fed
  to the clip paths, instead of overflowing a fixed 268px box by an amount that
  varied with the viewport. Drop the 820px cap so the hero spans the shell like
  the nav and tabs around it.
- Lay the x402 and recovery controls out on a grid so label, field and help
  text each get a row, and style a.secondary, which was only ever styled for
  button.
- Scale type once at the root rather than per component, so the console is not
  set at laptop sizes on a large display.
- Fade surfaces on theme change and panels on tab switch, under
  prefers-reduced-motion: no-preference.

Adds --os-on-field-muted and --os-field-line: a surface that does not flip
needs a muted ink and a border line that do not flip either.

* fix(web): reconcile the readability work with the workspace redesign

Merging develop left the branch uncompilable: Hero carried both a `height`
prop and a `height` state, and the cabinet kept this branch's narrowed section
union while still rendering develop's panels, so four section names had no
type to belong to.

Reconcile rather than pick a side:

- Hero measures itself by default, which is what stops the workspace copy
  clipping at widths where the headline wraps. The `height` prop stays as an
  explicit override for the landing page, which sizes its hero to a layout
  rather than to its copy. Only a pinned hero gets an inline height; feeding
  the measured value back would rebuild the fixed box the measurement exists
  to avoid.
- Take develop's cabinet vocabulary, then apply this branch's request to it.
  The two sections asked to be removed were not gone, they were renamed:
  Spending rules was Wallet & permissions, Team & access was Developer access.
  Both are read-only restatements with no control behind them, so the cabinet
  is Overview, API services, Requests and Payment proof, and the two panels
  behind them are deleted rather than left unreferenced.
- Drop this branch's enumerated ink overrides. Develop solved the same problem
  more generally by rebinding --os-panel-ink and --os-accent-ink at each
  surface boundary, so descendants inherit the right ink instead of every
  descendant being listed; its version also gets `.quote-panel` right, which
  moved onto --os-surface in the redesign. Only the --os-field-line border,
  which develop lacks, is kept.

The structural guard survives and is what caught the regressions above. One of
its assertions was wrong rather than the stylesheet: `.console-container .tabs
button` legitimately takes panel ink because that container paints --os-panel,
and an unanchored regex was matching it as a substring.

* test(web): audit contrast on the settled page

The browser contrast checks run immediately after navigation. With motion
enabled, axe samples while the cabinet panel is still fading in and reads every
colour composited against what is behind it: panel ink measured as #bdd1c2 on
#4d6b64 rather than #a9c4b2 on #0b332c, and page ink as #728876 on #f4f7f1
rather than #3f5c46 on #edf1e9. Ninety-eight reported violations, none of them
real.

All motion in styles.css sits behind prefers-reduced-motion: no-preference, so
emulating a reduced-motion viewer settles the page before the audit instead of
suppressing a genuine failure.

* test(web): walk the four cabinet tabs the workspace now has

The browser acceptance spec iterated a six-label list and clicked each one.
Spending rules and Team & access are gone, so the click timed out on a tab that
no longer renders.

Note the markdownlint findings under .superpowers/ are pre-existing and outside
this branch.

* fix(web): render the class the paid-API grid rules depend on

Gate A caught this: taking develop's redesigned JobWorkspace.tsx wholesale also
took back `<section className="panel">`, dropping the `paid-api-panel` class.
The grid rules stayed in the stylesheet with nothing wearing them, so the x402
request key rendered inline with the text before it and its help text ran on
after it — criterion 7 undone, while every check stayed green.

Nothing caught it because the style tests assert stylesheet text and the
browser specs never look at that field. So add the structural guard: every
class this stylesheet declares must be rendered by some component. It resolves
names built by interpolation (`state-${...}`) and ignores classes the read-only
slices declare. Verified against the defect — dropping the class again fails
with exactly ['paid-api-panel'].

Also from the same review:

- Remove the .recovery-panel rules. They targeted a cabinet section develop
  replaced with PaymentProtectionPanel, which lays its own controls out; the
  markup they addressed no longer exists.
- Bind --os-on-field-muted. It was defined and audited for contrast but had no
  consumer once the enumerated overrides gave way to token rebinding, so the
  lime field had no muted ink. .paid-api-status now rebinds to it.

Four classes dead before this branch (brand-logo, fixture-toolbar, meta-code,
workspace-fact-grid) are recorded in the guard rather than removed here, so it
ratchets instead of pulling unrelated rules into this change.

* feat: add user-funded Circle x402 payments

* test: authorize paid API fixture before claim

* test: select claimed paid API attempt deterministically

* fix(web): clarify payment service labels (#102)

* Update copyright holder in LICENSE file

* fix(api): accept empty activity refresh body (#103)

* fix(web): restore the settlement link and fade late panel content

The ArcScan link in a request's settlement box was invisible in the light
theme. `.job-list li` rebinds --os-accent-ink to the page ink for the card
around it, but the settlement box inside repaints --os-panel, so the link
rendered forest ink on the forest box. It read correctly in dark, which is how
it shipped. The box now rebinds the ink tokens its own descendants inherit, the
same way `.paid-api-status` does for the lime field, and the anchor is
underlined so it still reads as a link.

Tab panels appeared instantly while the strip above them faded, for two
separate reasons. The console panel was never wrapped in `.tab-fade` and had no
key at all. The cabinet panel was wrapped, but the request list arrives from the
API after mount, so the 0.24s fade ran on the "Checking requests…" placeholder
and was over before any row existed. The console panel is now keyed on its tab,
and the list body is keyed on its loading state, so the fade runs on the content
the operator actually waited for.

* fix(circle): allow normal user wallet approval delay

* fix: recover user-wallet payments safely

* feat(web): add user-funded Circle Gateway deposits

* fix(web): bind payments to Privy wallet (#108)

Ignore external active wallets when a Privy embedded wallet is available, and fail closed when it is not.

* fix: widen Circle user-wallet authorization window

* docs: record Circle validity handoff

* docs: record merged Circle validity candidate

* fix(web): restore Privy wallet picker (#109)

Restore the Privy-controlled payment selection path removed by the previous payer binding fix. Scope remembered picker wallets to the signed-in Privy user so account changes cannot reuse a payer.

* docs: record latest develop sync

* fix(web): update website logo (#111)

* fix(web): pay with the actively selected wallet (#113)

Payments bind to the active wallet instead of silently preferring the
Privy embedded one; the picker (detected wallets + WalletConnect) opens
only when nothing is active. Raw eth_signTypedData_v4 payloads declare
EIP712Domain, which external wallets such as MetaMask require.

* fix(web): keep the dedicated Privy payer (#114)

* fix(web): pay with the actively selected wallet

Payments bind to the active wallet instead of silently preferring the
Privy embedded one; the picker (detected wallets + WalletConnect) opens
only when nothing is active. Raw eth_signTypedData_v4 payloads declare
EIP712Domain, which external wallets such as MetaMask require.

* fix(web): keep the dedicated Privy payer

MetaMask and other detected wallets authenticate through the picker,
while the Privy embedded wallet stays the dedicated payer for x402
signing and transfers; payment requests intentionally never reach
MetaMask. Restores the selection logic superseded by the previous
iteration and keeps its EIP712Domain signing fix.

* feat: persist paid API requests in request list

* fix(web): route payment approval by wallet type (#116)

The Privy embedded wallet signs the legacy EIP-712 payload its signer
accepts, while external wallets receive the EIP712Domain declaration
they validate. An actively selected browser wallet stays the payer
behind an explicit confirmation; otherwise payments fall back to the
automatic embedded payer without silent wallet switching.

* fix(settlement): remove recipient allowlist (#117)

* fix(web): pay console jobs through the server wallet (#118)

Privy login authenticates the operator only; the browser wallet is no
longer wired into the console. Report and paid-API approvals route to
the server-privy path, so the execution wallet settles without browser
confirmation or 2FA. Restores the pre-user-wallet payment behavior.

* remove: retire Circle paid API product

* test: restore postgres cleanup dependencies

* test: assert direct arc provider identity

* fix(web): restore user-wallet payment preparation

* feat: add Arc payment MCP endpoint (#120)

* plan: personal MCP and Privy agent payments (#112)

* docs: plan personal MCP payments

* docs: restructure the MCP arc_payment plan

One remote MCP tool first (arc_payment) on the existing policy-bound
execution wallet and Arc settlement worker; personal wallets stay a
separately gated milestone. Tasks carry dependencies, concrete work,
and exit conditions.

* feat(api): add the arc_payment MCP tool

One bearer-authenticated /mcp endpoint exposes a single arc_payment
tool bound to one workspace, request key, and a 1 USDC atomic cap.
Payments create or replay a durable intent settled by the worker
through the policy-bound Privy execution wallet; the Cloudflare worker
proxies the streamable HTTP transport.

* feat(web): add the MCP docs page

A static /docs/mcp page documents the MCP configuration, the
arc_payment tool, the 1 USDC cap, replay semantics, and ArcScan
verification. The hero links to it, and local Vite proxies /mcp to
the API. The page stores no credentials and sends no payments.

* docs: add the downloadable arc_payment agent skill

* docs: link the PR instead of a bare URL

* fix(web): resolve Privy wallet payment selection

* feat: add personal MCP credentials (#122)

* feat: add personal MCP credentials

* test: advance migration expectations

* docs: finalize MCP access rollout

* docs: repair the MCP access context record

* fix(web): clarify direct payment copy (#125)

* feat: capture Graph evidence for every settlement (#124)

* feat: capture graph evidence for every settlement

* docs: record graph evidence PR handoff

* test: align migration expectations with graph evidence 012

* test: capture valid graph evidence in the production runtime fixture

---------

Co-authored-by: Matvii Nesterenko <51422901+kapustazh@users.noreply.github.com>

* fix(web): polyfill Buffer for Privy transfers (#126)

* fix(web): polyfill Buffer for Privy transfers

* docs(context): record Privy buffer rollout

* feat(web): add MCP profile shortcuts (land #127 on develop) (#129)

* feat(web): add MCP profile shortcuts

* docs(context): record MCP profile rollout

* fix(mcp): let agent generate request key (#130)

The deployment-wide ONESHOT_MCP_REQUEST_KEY allowlist forced manual key
copying and rejected agent-generated keys. Callers now generate and
retain the request key (report-<purpose-slug>-<8 hex>) while the field
stays required, so replay identity and conflict detection are intact.
The obsolete one-intent demo quota is removed; worker settlement caps
and Privy policy remain authoritative.

* fix(web): auto-verify user wallet payments

* feat(recovery): backfill Graph evidence and brand proof UI (#131)

Historical committed settlements never received a Graph capture job, so
their evidence card read as a flat NOT REPORTED. A new append-only
migration 013 enqueues one idempotent capture_graph_evidence job per
settlement that has neither Graph evidence nor a pending capture job.

Missing Graph copy now distinguishes CAPTURE PENDING (committed) from
NOT YET REPORTED (non-terminal) instead of masking absent data, and both
proof surfaces adopt the shared Rubik/mono typography, brand radii, pill
actions, and lime summary tokens with light/dark awareness.

* feat: route MCP payments through user wallets

* fix: normalize MCP payload conflicts

* test: align MCP conflict assertion

* fix(web): resume supplier results and copy wallet address

* fix(web): avoid repeated resume result polling

* fix(web): wait for resumed supplier result

* feat: add MCP wallet signing handoff link

* fix(web): remove automatic resume polling

* fix: surface Graph evidence for site payment outcomes

* docs: refresh README and add front-end derived banner

README had drifted 23 commits behind develop. Bring it back to the shipped
surface and give it the product's own nav panel as a banner.

Documentation corrections:

- Add packages/brand to the repository layout and describe apps/api's MCP
  endpoint and personal-credential role.
- Replace the retired four-tab console and "Tools" section with the five
  cabinet sections that exist today, and document the /docs/mcp route.
- Add the user-wallet, MCP, and profile-credential routes to the API table,
  and note that the transport and operator routes sit outside the frozen v1
  contract pack.
- Describe the non-custodial MCP payment flow and digest-stored personal
  bearers in a new Agent access section.
- Record that a browser caller's workspace is derived from its verified Privy
  subject, and that every committed settlement captures Graph evidence through
  a durable outbox job with a backfill for older settlements.
- Repair a duplicated sentence fragment in Project status and add the
  user-wallet and MCP rows to the status table.

The banner is not hand-drawn. scripts/render-nav-panel.mjs reads the palette
from packages/brand/src/tokens.css, the mark geometry from CommitRing.tsx, and
the nav labels from apps/web/src/App.tsx, then emits one SVG per theme. GitHub
strips CSS from Markdown, so the README selects between them with a <picture>
element. tokens.css remains the only source of a brand colour.

* fix(web): re-read a pending delivery until the worker reports the result

Supplier delivery finishes in the worker, after the browser has already read
the request list, so a request whose delivery was still PENDING at read time
kept saying "Retrieving result" until someone pressed refresh — long after the
result was durably available. Whether it looked right depended only on whether
the worker beat the page load, which is why it worked intermittently.

The list now re-reads itself while any delivery is PENDING: every four seconds,
fifteen attempts, then it stops and leaves the manual refresh as the way to
look again. The reads are GET /v1/jobs only. They never call the resume
endpoint and never submit a payment, so at-most-once settlement is unaffected,
and they do not raise the loading flag, because the spinner, the disabled
button, and the tab fade belong to a read the operator asked for.

This partially reverses a2903a1, which removed automatic polling after a report
of unwanted traffic. The bound and the pending-only condition keep both reports
satisfied; the resume control that commit removed stays removed.

The browser spec asserted an exact list-read count, which a timed re-read makes
timing-dependent; it now asserts a lower bound and still asserts that no resume
request is sent.

* fix(jobs): recover a delivery stranded in PENDING

A committed job whose delivery reached PENDING could be left with nothing able
to move it. The worker no-ops a payload whose delivery_attempt no longer
matches and then marks the outbox row DELIVERED, and resumeDelivery re-queued
only NOT_REQUESTED or RETRIEVAL_FAILED, so the job kept a committed payment, an
unretrieved result, and no path forward.

PENDING alone is still not treated as resumable. The deciding evidence is
whether a fulfill_supplier_order row is still queued for that job: a row a
worker currently holds is status PENDING and so counts as queued, which means
an in-flight retrieval is never duplicated. Only a job with no such row left is
re-queued, under a fresh delivery_attempt that fences the old retrieval.

The claim is a compare-and-set against the state read under the job's FOR
UPDATE lock, so two concurrent resumes cannot both take one delivery. No
payment work is created on this path; the committed settlement is untouched.

* docs: reduce the README banner to the brand lock-up

The full-width nav strip carried the network badge, token badge, theme control
and workspace link into a README where none of them mean anything, and at
README width the whole row rendered small. Keep only the brand lock-up — the
commit-ring mark, the wordmark, and its SETTLEMENT ENGINE tag — on the same
rounded panel, and scale every length by one factor so the proportions stay
exactly those of `.top-nav`.

The banner is still generated, not drawn: the palette comes from tokens.css,
the mark geometry from CommitRing.tsx, and both labels from App.tsx. The canvas
is 381x112 and sized to its own content, so the tag clears the panel edge with
the fallback fonts GitHub renders.

* Add centered picture element to README

Updated the README to include a centered picture element for the brand lock-up.

* fix(activity): scope indexed transfers to the workspace

The Graph activity query is sent with the shared server wallet alongside
the workspace's user wallets, so a stored observation also carries
transfers made for other workspaces. Settlement and uncertain counts were
SQL-scoped to the workspace while the unmatched transfer count was taken
from the whole observation, so Payment proof reported one workspace's 11
settlements next to 40 unmatched transfers drawn from every workspace.

Filter the observed transfers to the ones this workspace can own before
they are matched, listed or counted: the sender is a workspace payer
wallet, or the transaction hash is already recorded on a workspace
settlement, job or attempt. The attempt hash keeps a stranded server
payment visible as unmatched evidence.

* docs: require min-instances and no-cpu-throttling for the worker

The worker drains the outbox on its own timer rather than per request, so it
only works while its process is alive and holding CPU. The documented deploy
omitted both flags, and a deployment without them fails quietly: payments still
settle through the API, but supplier deliveries sit in PENDING and requests stay
on "Retrieving result".

The failure is intermittent, which makes it easy to misdiagnose as a UI or
state-machine fault. Any request that reaches the service boots an instance, and
startup drains the backlog before the timer takes over, so the queue appears to
clear itself and then stalls again.

---------

Co-authored-by: SuPuHe <mizin.a.s.2006@gmail.com>
Co-authored-by: Artem <47925608+selezenart@users.noreply.github.com>
Co-authored-by: selezenart <artvs14@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant