Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
33390cb
Preserve Earn eligibility on unsupported payouts and deduplicate claims
bobodread876 Oct 23, 2026
93b29dd
Fix identity document presigning with nonempty uploads
bobodread876 Oct 23, 2026
149cd22
Harden owned Bridge KYC link retrieval and credential boundaries
bobodread876 Oct 23, 2026
8cf5671
Add isolated native parity unit runner and service boundary mocks
bobodread876 Oct 23, 2026
07f4131
Prevent automatic trace argument and GraphQL source capture
bobodread876 Oct 23, 2026
86daba8
Stop tracing IBEX request payloads and webhook credentials
bobodread876 Oct 23, 2026
24132c6
Limit Redis and identity banking failure telemetry to safe metadata
bobodread876 Oct 23, 2026
189b357
Project GraphQL error traces onto closed safe metadata
bobodread876 Oct 23, 2026
70b1e32
Keep HTTP access and tracing telemetry free of private requests
bobodread876 Oct 23, 2026
9be395e
Bound registration and remaining ERP failure telemetry
bobodread876 Oct 23, 2026
69bb652
Make device account bootstrap owned and safely resumable
bobodread876 Oct 23, 2026
85d0949
Expose authenticated raw owned wallet snapshots
bobodread876 Oct 23, 2026
9d3e4f7
Gate native parity backend changes with isolated contracts
bobodread876 Oct 23, 2026
94d0090
Use spelling-safe language in telemetry docs
bobodread876 Oct 23, 2026
5bc51c5
Order access logger imports for CI lint
bobodread876 Oct 23, 2026
476cae5
Record physical device App Check gate acceptance
bobodread876 Oct 23, 2026
e11964b
Prevent unrecoverable device-only TOTP enrollment
bobodread876 Oct 23, 2026
0d856f5
fix: keep push notification credentials out of telemetry
bobodread876 Oct 23, 2026
0b475cd
Close adjacent notification logs and select policy privacy tests
bobodread876 Oct 23, 2026
86a25a8
fix: close notification and preregistration telemetry inputs
bobodread876 Oct 23, 2026
7a5985f
Include notifier privacy regressions in guarded parity runner
bobodread876 Oct 23, 2026
d3cb94c
Keep payer attribution diagnostics private
bobodread876 Oct 23, 2026
0ab8c62
fix: remove private auth and API key trace attributes
bobodread876 Oct 23, 2026
8fc4a43
Select auth privacy regressions in guarded runner
bobodread876 Oct 23, 2026
def2da6
Keep custom IBEX failure bodies and paths out of telemetry
bobodread876 Oct 23, 2026
356a57e
Route fixed cash Lightning sends to owned settlement wallet
bobodread876 Oct 23, 2026
3028c25
Guard auth privacy and Lightning source routing contracts
bobodread876 Oct 23, 2026
ba04cb0
Keep auth provider diagnostics private and fail closed on IP errors
bobodread876 Oct 23, 2026
0d7bfed
Select provider privacy and fail-closed auth regressions
bobodread876 Oct 23, 2026
c53f2dc
Use spelling-safe source reference in parity test record
bobodread876 Oct 23, 2026
7d0c5e3
fix: fence ambiguous Bridge withdrawal crypto sends
bobodread876 Oct 23, 2026
0f414b6
Guard ambiguous Bridge send recovery contracts
bobodread876 Oct 23, 2026
5433ffa
Scope CSV export to requested owned wallets
bobodread876 Oct 23, 2026
25f4747
Guard owned CSV export and completeness contracts
bobodread876 Oct 23, 2026
5ce406f
Escape spreadsheet formulas in transaction CSV exports
bobodread876 Oct 23, 2026
85e75b6
Guard CSV formula safety with parity runner
bobodread876 Oct 23, 2026
922fa31
Bind IBEX on-chain receive addresses to owned cash wallets
bobodread876 Oct 23, 2026
3e1ff83
Bind Bridge withdrawal deposit instructions before crypto debit
bobodread876 Oct 23, 2026
1f89798
Record expanded isolated backend suite selection
bobodread876 Oct 23, 2026
4ffbafc
Route owned cash on-chain receive to settlement wallet
bobodread876 Oct 23, 2026
eefb5c3
Remove duplicate on-chain refusal assertion
bobodread876 Oct 23, 2026
27ceabc
Freeze pending Bridge withdrawal fee snapshots
bobodread876 Oct 23, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions .github/workflows/native-parity-unit.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
name: Isolated native parity contracts

on:
pull_request:
branches: [main]
push:
branches: [main]

permissions:
contents: read

jobs:
isolated-unit:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version: "24"
- name: Install pinned package manager and locked dependencies
run: |
npm install --global --ignore-scripts yarn@1.22.22
yarn install --frozen-lockfile --ignore-scripts --non-interactive
- name: Check isolated runner boundaries
run: node --test scripts/native-parity-unit.test.cjs
- name: Check TypeScript contracts
run: |
yarn tsc-check
yarn tsc-check-noimplicitany
- name: Compile production package
run: yarn build
- name: Run isolated contracts and privacy regressions
run: node scripts/native-parity-unit.cjs
35 changes: 35 additions & 0 deletions dev/apollo-federation/supergraph.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -1058,6 +1058,11 @@ type ConsumerAccount implements Account
limits: AccountLimits!
notificationSettings: NotificationSettings!

"""
Raw metadata for this authenticated owner's wallets, without presentation filtering or repair.
"""
ownedWalletSnapshot: OwnedWalletSnapshot

"""List the quiz questions of the consumer account"""
quiz: [Quiz!]!
realtimePrice: RealtimePrice!
Expand Down Expand Up @@ -2462,6 +2467,36 @@ type OneDayAccountLimit implements AccountLimit
scalar OneTimeAuthCode
@join__type(graph: PUBLIC)

"""
Actual owned wallet metadata without balances, payment authority or cash presentation aliases.
"""
type OwnedWalletMetadata
@join__type(graph: PUBLIC)
{
"""Internal wallet-owner ID, distinct from the public account UUID."""
accountId: ID!
id: WalletId!
isExternal: Boolean!
walletCurrency: WalletCurrency!
}

"""
Authenticated raw owner metadata; never performs cash cutover, provisioning or balance reads.
"""
type OwnedWalletSnapshot
@join__type(graph: PUBLIC)
{
"""Actual stored owned default; null means no stored default."""
defaultWalletId: WalletId

"""Public account UUID, matching the enclosing ConsumerAccount.id."""
publicAccountId: ID!

"""Internal owner ID shared by these wallet rows."""
walletAccountId: ID!
wallets: [OwnedWalletMetadata!]!
}

"""Information about pagination in a connection."""
type PageInfo
@join__type(graph: PUBLIC)
Expand Down
13 changes: 13 additions & 0 deletions docs/auth-api-key-telemetry-privacy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Authentication and API-key application telemetry privacy

This leaf is based on integrated backend `86a25a84224eb151c8fa8a25943405b910fe8bb9`, after the preceding telemetry and notifier privacy fixes. It changes explicit telemetry in `login.ts`, `request-code.ts` and `create-api-key.ts`. No real provider calls, credentials, exported spans or deployment were used.

Onboarding no longer JSON-serializes IP or carrier metadata into application spans. Existing validation-enabled flags remain, and successful metadata reads emit fixed boolean markers. The two explicit IP-fetch failure records now use a fresh fixed-message `IpFetcherServiceError` with the original critical severity and no attached IP. The original domain error is still returned to the caller; authentication and refusal decisions are unchanged.

Email-code requests record only a fixed operation instead of the email address and IP. The original values still reach their exact IP/login-identifier limiter keys and the email-code provider. Limiter refusals still stop provider dispatch, and provider results/errors retain their original identity. Existing blocked-country controls, coalescing and deliberately separate operational notifications are untouched.

API-key creation spans contain only started/success/error flags. They no longer inspect or record the caller's name, account identifier, scopes, requested expiry or generated public key ID. Name/scope/IP/rate validation, account limits, key generation, hash-only persistence and the one-time raw-key response are unchanged. The deliberate lifecycle audit remains intact: `auditApiKeyCreated` still receives the account, public key ID, validated scopes, expiry and rate limit. It receives neither the raw API key nor its hash. This distinguishes the existing security audit contract from general application tracing; the audit retention policy is outside this change.

Validation: **136 tests across seven reviewed suites** pass under Node 24 with the integration runner's fake `safeEnvironment`, network guard and Watchman disabled. New regressions cover successful and refused phone/device onboarding, exact OTP/carrier/IP/device inputs, same-owner bootstrap order, fixed private-error diagnostics, disabled carrier-validation fallback, existing-user login, limiter ordering, exact email dispatch and API-key persistence/audit behavior. Rejected scope arrays have a throwing `join` method to prove they are not serialized for telemetry before validation. Existing destination-country, collision, key-domain and lifecycle-audit tests remain green. All three TypeScript configurations, scoped ESLint and `git diff --check` pass.

This is a scoped application-boundary fix. The source audit also found independent adapter sinks in `services/ipfetcher/index.ts` (raw Axios error and attached IP) and `services/twilio.ts` (carrier SDK result and OTP-validation error). Those files are unchanged here and need their own provider-contract tests. The IP adapter's `UnknownIpFetcherServiceError` is also a sibling of `IpFetcherServiceError`, while login currently checks the latter with `instanceof`; this leaf preserves that existing authentication behavior. It does not claim all adapter telemetry, operational alerts, domain error propagation, historical records or collector retention are sanitized.
32 changes: 32 additions & 0 deletions docs/auth-provider-adapter-privacy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Authentication provider adapter privacy and IP failure handling

The IP metadata adapter still sends the exact IP and configured key to its
provider. Successful metadata is returned unchanged to the onboarding policy;
the exported span contains only closed booleans. On provider failure, the
adapter records and returns a fixed diagnostic. It no longer includes the
queried IP, credential-bearing request URL, response body or raw exception.

`UnknownIpFetcherServiceError` now extends `IpFetcherServiceError`. Phone and
device onboarding already check for the latter class before authorizing IP
metadata. The corrected inheritance makes an unknown provider failure take
that fail-closed path, without changing the public error name or critical
severity.

Twilio Verify retains exact phone/code/channel provider inputs and its
service-wide send-rate-limit key. HTTP 429 or known rate-limit code 60203 still
maps to `PhoneProviderRateLimitExceededError`, without claiming which Twilio
bucket rejected the request. Other known failures retain their mapped classes,
including invalid code, restricted region, unavailable provider and expired
verification. Returned messages and log fields are fixed; they contain no
provider error text, phone, code or carrier response object. Carrier lookup
still returns the exact mapped metadata to the caller.

Three guarded unit suites cover 28 tests, including private provider response
and error fixtures, exact provider inputs, both onboarding paths, rate-limit
classification and fixed returned diagnostics. Whole-tree TypeScript,
no-implicit-any, scoped lint, spelling and diff checks pass. Tests use dummy
configuration and mocks, with no provider request or account mutation.

This change does not validate production Twilio delivery or proxy-provider
accuracy. Existing historical telemetry and unrelated provider adapters need
separate operator review.
36 changes: 36 additions & 0 deletions docs/bridge-owned-kyc-links.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Owned Bridge verification links

This change hardens the existing `bridgeInitiateKyc` mutation without changing GraphQL fields or input types. It is based on `7402dd7fcf49e378e4af5189232dffb76a4564d5`. It has not been deployed and no provider request or production mutation was made during implementation.

For an account with a stored `bridgeCustomerId`, the service retrieves links through the provider's current existing-customer endpoints:

- `GET /customers/{customerID}/kyc_link`
- `GET /customers/{customerID}/tos_acceptance_link`

The customer ID comes only from the fresh authenticated account record. Caller email, name and requested KYC type cannot select another customer. The service rechecks the stored association before exposing the response. Existing status is retained: in particular, `under_review`, `incomplete`, `awaiting_questionnaire` and `awaiting_ubo` are never reset to `open` or `not_started`. A provider GET failure is returned without a fallback customer-creation POST.

These endpoints and response shapes are documented by [Bridge's existing-customer KYC endpoint](https://apidocs.bridge.xyz/api-reference/customers/retrieve-a-hosted-kyc-link-for-an-existing-customer) and [existing-customer terms endpoint](https://apidocs.bridge.xyz/api-reference/customers/retrieve-a-hosted-url-for-tos-acceptance-for-an-existing-customer). The identity host is exactly `bridge.withpersona.com`; terms accept exactly `dashboard.bridge.xyz` or `compliance.bridge.xyz`, reflecting Bridge's documented hosted URLs. HTTPS, bounded printable URLs and no credentials, port, fragment, encoded host or host suffix tricks are required. Returned capabilities are not logged. Provider error responses containing someone else's `existing_kyc_link` are discarded before the error leaves the service.

For an account without a customer association, an explicit initiation can create a customer. Account-level locking serializes fresh reads, provider creation and local binding. A stable per-account UUID is supplied as the provider idempotency key; changing caller email or name cannot select a new key. The verified identity repository email remains preferred over caller input, preserving the existing input contract. The repository binds the returned customer using a compare-and-set predicate that matches only a missing/null `bridgeCustomerId`; it cannot overwrite an association established concurrently by another writer or older server. A final account read checks that the association still matches before links are returned.

Duplicate-email errors do not bind the returned existing customer, return its links, or reset verification status. Such failures need an explicit account/status review or support reconciliation. There is no automatic retry. The stable UUID is useful within the provider's idempotency retention; it does not promise unlimited exactly-once behavior after provider retention expires or eliminate an orphan customer if the provider succeeds but local persistence fails.

The [Bridge status lifecycle](https://apidocs.bridge.xyz/platform/customers/customers/kyclinks) distinguishes incomplete/action-required customers from manual review, and notes that some `under_review` customers still need identity information. Returning the current customer's hosted links supports continuation without creating another customer; no redirect, page text, app return or returned URL proves approval. `bridgeKycStatus` remains the source of server status and may proactively provision a receiving account after approval in this baseline.

Native clients must not assume this behavior exists until this backend change is released. The current native UI offers retained-link continuation only and otherwise directs users to refresh/support; a null or unknown status is not presented as new-customer eligibility. The real `bridgeTopupEnabled` rollout setting remains the product gate. No new deployment-attestation switch is introduced. A client adopting the current provider terms endpoint must explicitly review the documented `compliance.bridge.xyz` origin in its own initial-URL policy; it must not broaden to arbitrary Bridge subdomains.

Validation uses Node 24 and mocked provider/repository boundaries. Focused tests cover owned GET routing, preserved statuses, no caller-email rebinding, malformed/error responses, expiry of lock authority, fresh owner-association checks, stable-key retries, concurrent starts, atomic local binding and the unchanged public resolver. No secrets or real `.env` files are needed. A real provider sandbox and real Mongo/Redis integration run remain separate release checks.

Validation is performed with Node 24 and the fresh frozen-lockfile dependency installation from the combined review worktree, not the reused original installation. The prior two Express/Application type errors reproduced only with that reused installation; they are not a source/lockfile baseline failure. Current focused and compile results are recorded below. No casts or type workarounds were added. Five broader suites need environment bootstrap; a future combined run can use an explicitly constructed nonsecret regtest environment with loopback-only hosts, without reading `.env` or inheriting provider credentials.

The authenticated Bridge request adapter now rejects every redirect centrally (`redirect: "error"`), covering both hosted-link GETs, KYC creation, and existing GET/POST/DELETE operations. It does not follow even same-origin redirects. Custom `Api-Key` headers and financial/identity request bodies must never be forwarded to a redirect target. Real loopback-server regressions with dummy keys verify that the two new GETs, KYC POST and representative existing GET/POST requests never reach the redirect destination. No provider or production request is involved.

Stored and newly returned customer IDs pass a shared bounded ASCII letters/digits/underscore/hyphen validator before dispatch or local binding. Dot segments, encoded paths, URL delimiters, Unicode and control characters are rejected. Encoding alone is insufficient because `encodeURIComponent("..")` leaves a URL-normalized dot segment.

Historical ownership is checked without a unique index or migration. The repository returns at most two IDs, projecting only `_id`, for an exact customer-ID match. Existing-link retrieval requires exactly the current account before the GETs and again before reveal. A new binding requires zero current owners and exactly the new owner afterward. New paths always acquire locks in account → `locks:bridge-kyc-customer:<id>` order, holding the customer lock across ownership checks and binding/reveal. Both lock signals are checked again after the final owner-query await, and before CAS after the no-owner query. Callback provider errors are sanitized inside the customer-lock callback before the real lock adapter can convert a thrown callback exception; only safe API class/status survive, never provider bodies or arbitrary exception fields.

The baseline schema does not uniquely index `bridgeCustomerId`, and the prior duplicate-email fallback may already have created shared or incorrect associations. Shared rows now fail closed; the service never chooses the first match or repairs ownership by email. A solitary historical association can still be wrong, and older writers bypass the new customer lock. These guards are not a global database uniqueness guarantee and do not replace an authenticated historical-data audit before rollout. No automatic reassignment, migration, cleanup or unique-index creation is performed.

Final isolated validation with the fresh exact lockfile dependencies: 81 focused tests passed across four suites, including real loopback redirect regressions with dummy credentials; the 14 independently runnable Bridge-related suites passed 233 tests. Touched-file ESLint, `git diff --check`, declaration typecheck (`tsconfig.d.json`), whole-tree `tsc --noEmit`, and production configuration typecheck (`tsconfig-build.json`) all passed with zero diagnostics. An independent reviewer reran all 81 focused tests and found no remaining scoped KYC blocker. These checks still do not substitute for provider sandbox and real Mongo/Redis release validation.

Release dependency: the generic telemetry wrappers in this source baseline capture arbitrary function arguments, including KYC name/email and other application credentials. This isolated patch sanitizes provider error payloads and its direct service logs; it does **not** fix that central user-input tracing behavior. The separately reviewed central telemetry security patch must be integrated before this KYC change is considered for release. There is no method-specific tracing bypass in this patch and no claim that its user-input tracing is sanitized on its own.
44 changes: 44 additions & 0 deletions docs/bridge-withdrawal-quote-gate.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Bridge withdrawal quote and consent gate

The existing two-step GraphQL flow stores a pending withdrawal before calling
Bridge. RN then queries that row and confirms by ID. The pinned RN confirmation
screen calculates a fixed 0.5% fee locally from `withdrawal.amount`; the backend
stores a configured Flash developer fee plus estimated Bridge and gas fees from
a changing market snapshot. No confirmation input binds the reviewed amount,
wallet, bank, fee total, or quote expiry. This is not an immutable reviewed
payment quote, and native withdrawal initiation remains closed.

This backend hardening keeps the first persisted fee snapshot on a pending
request. Retrying the same account/destination/amount no longer refreshes that
snapshot underneath an open confirmation screen. An old pending row missing a
complete estimate now refuses before transfer creation and can be cancelled
and requested anew. Initiation independently refuses malformed/non-USDT rows,
missing or fee-consuming estimates, and external accounts that are no longer
verified. The direct service entry point also refuses malformed amount strings.
The service parses up to six USDT decimal places into exact microunits and
refuses amounts above one billion USDT, a defensive representability bound
rather than a business transaction limit.
These checks do not prove that a client displayed or accepted the estimate.

The Bridge transfer receipt's `amount` and `currency` can describe the payout,
while the IBEX USDT debit uses the original pending `amount`. Bridge's official
[orchestration overview](https://www.withbridge.com/product/orchestration)
describes determining the source amount needed for a destination amount; it
does not supply an executable debit cap or a frozen provider quote in this
repository. The current code still writes the returned transfer amount/currency
over the pending amount/currency after creation. Before enabling native
withdrawal, establish source and destination units with a sandbox transfer,
persist them separately, and verify the source debit against a user-approved
maximum. Also add a short server-enforced quote expiry and a confirmation
contract that echoes the exact reviewed amount, source wallet, bank and fee
bound. Do not infer fee consent from the existence of a pending row.

A timeout during `createTransfer` can occur after Bridge created a transfer but
before Mongo recorded its ID. The same pending ID derives a stable idempotency
key, but this repository has no proven provider retention window or lookup by
that key. Do not treat a retry or a missing local transfer ID as proof no
transfer exists. Operational recovery requires authoritative Bridge evidence
or a durable creation-uncertainty fence before automatic retry. The existing
IBEX send uncertainty fence starts later, after a transfer ID is recorded.

No live Bridge or IBEX call was used for this audit or its focused tests.
Loading
Loading