Skip to content

Commit e39575a

Browse files
Merge pull request #10 from web3settle/develop
C3 (SDK): trust-boundary hardening (signed config + allowlists)
2 parents dd9c97c + 2c7028e commit e39575a

23 files changed

Lines changed: 1248 additions & 92 deletions

.github/dependabot.yml

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# Premortem F4: Dependabot opens dependency PRs but the runtime block is
2+
# pinned (no carets), so any update is intentional. We forbid auto-merge for
3+
# runtime deps via the `assignees` + `reviewers` gates so a PII-touching
4+
# package change always lands a human review.
5+
version: 2
6+
updates:
7+
- package-ecosystem: "npm"
8+
directory: "/"
9+
schedule:
10+
interval: "weekly"
11+
day: "monday"
12+
open-pull-requests-limit: 5
13+
# Runtime deps require human review — never auto-merge.
14+
allow:
15+
- dependency-type: "all"
16+
groups:
17+
runtime-deps:
18+
dependency-type: "production"
19+
dev-deps:
20+
dependency-type: "development"
21+
labels:
22+
- "dependencies"
23+
- "supply-chain"
24+
commit-message:
25+
prefix: "chore(deps)"
26+
include: "scope"
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
name: SDK supply chain
2+
3+
# Premortem F4 follow-on: even with caret-pinning fixed and a lockfile, npm
4+
# publishes can be hijacked by transitive compromise or registry spoofing.
5+
# This workflow:
6+
# - asserts that runtime deps in package.json are exact-version (no `^`/`~`),
7+
# - performs a `npm pack --provenance` dry-run so each PR proves the build
8+
# can produce a provenance-signed tarball,
9+
# - runs `npm ci --ignore-scripts` so postinstall scripts can't smuggle
10+
# side-effects into CI worker images.
11+
on:
12+
push:
13+
branches: [develop, main]
14+
pull_request:
15+
branches: [develop, main]
16+
workflow_dispatch:
17+
18+
permissions:
19+
contents: read
20+
id-token: write # required for npm provenance
21+
22+
concurrency:
23+
group: sdk-supplychain-${{ github.ref }}
24+
cancel-in-progress: true
25+
26+
jobs:
27+
enforce-pin:
28+
name: Runtime deps must be exact-version
29+
runs-on: ubuntu-latest
30+
steps:
31+
- uses: actions/checkout@v4
32+
- name: Reject `^` / `~` in runtime dependencies
33+
run: |
34+
# Premortem F4: caret-pinning lets transitively-compromised minors
35+
# land without review. This grep-fails fast on any caret/tilde in
36+
# the runtime block — devDeps may still float.
37+
node -e "
38+
const pkg = require('./package.json');
39+
const bad = Object.entries(pkg.dependencies || {}).filter(([_, v]) => /^[\^~]/.test(v));
40+
if (bad.length) {
41+
console.error('Floating runtime deps found (must be exact-version):');
42+
for (const [n, v] of bad) console.error(' ' + n + ': ' + v);
43+
process.exit(1);
44+
}
45+
console.log('All runtime deps are exact-version.');
46+
"
47+
48+
provenance-pack:
49+
name: npm pack --provenance dry run
50+
runs-on: ubuntu-latest
51+
steps:
52+
- uses: actions/checkout@v4
53+
- uses: actions/setup-node@v4
54+
with:
55+
node-version: '22'
56+
cache: 'npm'
57+
registry-url: 'https://npm.pkg.github.com'
58+
- name: Install with locked, scriptless deps
59+
run: npm ci --ignore-scripts
60+
- name: Build dist/
61+
run: npm run build
62+
- name: Pack with provenance
63+
# `--dry-run` so we don't push to the registry from PRs; the publish
64+
# workflow (out-of-scope here) reuses the same flags without --dry-run.
65+
run: npm pack --provenance --dry-run
66+
env:
67+
NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
68+
69+
- name: Assert tarball ships no source / secrets
70+
# SEC-DEP-006: the published tarball must carry only built `dist/`
71+
# (JS/CJS + `.d.ts` types). A stray `.ts` SOURCE file or a `.env` would
72+
# leak internals/secrets to every consumer. We inspect the dry-run file
73+
# list and fail the job on any violation.
74+
run: |
75+
npm pack --dry-run --json > /tmp/pack.json
76+
node -e "
77+
const arr = JSON.parse(require('fs').readFileSync('/tmp/pack.json', 'utf8'));
78+
const files = arr.flatMap((p) => (p.files || []).map((f) => f.path));
79+
const tsSource = files.filter((f) => f.endsWith('.ts') && !f.endsWith('.d.ts'));
80+
const envFiles = files.filter((f) => /(^|\/)\.env(\.|$)/.test(f));
81+
const bad = [...tsSource, ...envFiles];
82+
if (bad.length) {
83+
console.error('Forbidden files in publish tarball (no .ts source / .env allowed):');
84+
for (const f of bad) console.error(' ' + f);
85+
process.exit(1);
86+
}
87+
console.log('Tarball clean: ' + files.length + ' files, no .ts source or .env.');
88+
"

README.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -570,12 +570,33 @@ All five use the same `MerchantPayIn` V3.0 contract model — immutable commissi
570570
## Security notes
571571

572572
- All API responses are validated with Zod schemas before use.
573+
- **Signed PaymentConfig (V0.5+).** `GET /payment-config` returns `{ data, signedAt, signature }`; the SDK refuses to build calldata unless the Ed25519 signature verifies against `WEB3SETTLE_PAYMENT_CONFIG_PUBKEY_PRIMARY` (or `_SECONDARY` during a rotation overlap) over `signedAt + canonical_json(data)`. The constants are baked into the SDK at release time so a poisoned-DNS or CDN-edge MITM that swaps the contract address cannot be silently honoured. The `.well-known/web3settle-config-pubkey` endpoint mirrors the constants for out-of-band drift checks.
574+
- **Contract allowlist.** The SDK refuses to call `payIn*` against any address that is neither in the baked-in `KNOWN_CONTRACT_ADDRESSES` nor explicitly elevated by the signed `allowedContractAddresses` map for the storefront. New canonical addresses require an SDK release.
575+
- **ABI version handshake.** The signed payload carries `contractAbiVersion`. The SDK fails closed when it sees a revision not in `SUPPORTED_ABI_VERSIONS`.
576+
- **Permit allowlist.** `permit: 'auto'` falls back to `approve()` for any token whose `(name, version, chainId, verifyingContract)` quadruple is not in the SDK's baked-in `KNOWN_PERMIT_TOKENS`. `permit: 'require'` raises `UnknownPermitTokenError` on unknown tokens. To add a token, compute `permitDomainKey(name, version, chainId, verifyingContract)` and PR the resulting hex digest into `core/config.ts`; the addition ships in the next SDK release.
577+
- **Salted telemetry digests.** `walletDigest` is salted by `(storefrontId, dayUtc)` so two storefronts of the same wallet cannot be cross-joined and the same shop's digests rotate every UTC day.
573578
- ERC-20 approvals request only the exact amount needed — **never** unlimited.
574579
- Transaction receipts are verified for `status === 'success'`; reverts surface as an `Error`.
575580
- Wallet connections use standard EIP-1193 providers via wagmi; the SDK does not read or persist private keys.
576581
- `Web3SettleApiClient` validates `storefrontId` + `sessionId` as UUIDs at construction time and builds URLs via the `URL` constructor (no string concat).
577582
- Modal: `role="dialog"` + `aria-modal` + focus restoration + ESC-key close. Click-outside closes on mouse but the Escape handler is always present for keyboard-only users.
578583

584+
### Adding a token to the permit allowlist
585+
586+
The `permit: 'auto'` path silently signs EIP-2612 permits only for tokens whose
587+
EIP-712 domain quadruple is on the SDK's allowlist. To add one:
588+
589+
```ts
590+
import { permitDomainKey } from '@web3settle/merchant-sdk';
591+
console.log(permitDomainKey('USD Coin', '2', 1, '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48'));
592+
// → "<64-char hex>"
593+
```
594+
595+
Open a PR against `merchant-sdk/src/core/config.ts` adding the digest to
596+
`KNOWN_PERMIT_TOKENS`. Include in the PR body the source you used to verify the
597+
contract address (Etherscan label, official docs link). The merge ships in the
598+
next SDK release; backend deploys alone cannot expand the set.
599+
579600
## Browser support
580601

581602
Modern evergreen browsers with ES2020 support (Chrome 94+, Firefox 93+, Safari 15+, Edge 94+).

package-lock.json

Lines changed: 5 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
},
4444
"files": [
4545
"dist",
46+
"!dist/**/*.map",
4647
"LICENSE",
4748
"README.md",
4849
"CHANGELOG.md"
@@ -59,7 +60,7 @@
5960
"lint:fix": "eslint src --fix",
6061
"typecheck": "tsc --noEmit",
6162
"audit:ci": "npm audit --audit-level=high",
62-
"prepublishOnly": "npm run typecheck && npm run lint && npm run test && npm run build"
63+
"prepublishOnly": "npm ci --ignore-scripts && npm run typecheck && npm run lint && npm run test && npm run build"
6364
},
6465
"peerDependencies": {
6566
"@solana/wallet-adapter-base": "^0.9.0",
@@ -83,9 +84,11 @@
8384
"wagmi": { "optional": true }
8485
},
8586
"dependencies": {
86-
"i18next": "^25.10.10",
87-
"react-i18next": "^16.6.6",
88-
"zod": "^3.25.0"
87+
"@noble/curves": "1.9.1",
88+
"@noble/hashes": "1.8.0",
89+
"i18next": "25.10.10",
90+
"react-i18next": "16.6.6",
91+
"zod": "3.25.76"
8992
},
9093
"devDependencies": {
9194
"@eslint/js": "^9.39.4",

0 commit comments

Comments
 (0)