Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion .changeset/align-mcp-zod.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
'@sei-js/mcp-server': patch
---

Align `zod` with `@modelcontextprotocol/sdk` so both resolve the same schema types.
Align `zod` with `@modelcontextprotocol/sdk` so both resolve the same schema types. Raise the SDK floor to `^1.23.0`, the first release that declares zod 4 support, so the pairing is unresolvable rather than silently broken.
4 changes: 2 additions & 2 deletions .changeset/fix-global-wallet-release-blockers.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ Harden Sei Global Wallet browser, EIP-6963, and packaging behavior without chang
New exports: `registerEIP6963Provider`, `unregisterEIP6963Provider`, `eip6963ProviderInfo`, and `registerSolanaStandard`.

- Raise `@dynamic-labs/global-wallet-client` to `^4.96.3`, so applications inherit Dynamic's transitive fixes without waiting for a release here.
- Initialize Dynamic's required `global` and `process` aliases before loading its modules in browsers and edge-like SSR runtimes, without consumer bundler configuration and without replacing consumer-defined values. `process.env.NODE_ENV` defaults to `production` so libraries loaded afterwards do not take a development branch inside a production bundle.
- Dispatch the initial EIP-6963 announcement, re-announce on every provider request, expose cleanup helpers, use stable UUIDv4 provider metadata, and replace the non-square wordmark with the unmodified square black mark from the official Sei brand kit.
- Initialize Dynamic's required `global` and `process` aliases before loading its modules in browsers and edge-like SSR runtimes, without consumer bundler configuration and without replacing consumer-defined values. Import mutates `globalThis` only when those values are absent. The `process` shim installed on `globalThis` is a copy, so the `process/browser.js` module singleton is not mutated. `process.env.NODE_ENV` defaults to `production` so libraries loaded afterwards do not take a development branch inside a production bundle.
- Dispatch the initial EIP-6963 announcement, re-announce on every provider request, expose cleanup helpers, keep the EIP-6963 uuid equal to `environmentId` (the same identity Dynamic previously used), and replace the non-square wordmark with the unmodified square black mark from the official Sei brand kit.
- Keep the root, `./eip6963`, and `./ethereum` entrypoints resolvable with no optional peer installed, including for types.
- Verify real npm and Bun consumers, all five entrypoints, EIP-6963 and Solana registration, ZeroDev resolution, esbuild and Vite browser runtimes, SSR imports, types, audits, and package contents.

Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/sei-global-wallet-smoke.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ jobs:
consumers:
name: npm, Bun, edge, and browser consumers
runs-on: ubuntu-latest
timeout-minutes: 20
timeout-minutes: 30

steps:
- name: Checkout repository
Expand Down
3 changes: 1 addition & 2 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion packages/mcp-server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
"typescript": "^5.8.3"
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.17.5",
"@modelcontextprotocol/sdk": "^1.23.0",
"@noble/hashes": "^1.8.0",
"commander": "^14.0.0",
"cors": "^2.8.5",
Expand Down
16 changes: 4 additions & 12 deletions packages/sei-global-wallet/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ Complete npm root overrides when the optional AA path is not enabled:
}
```

Complete Bun root overrides when the optional AA path is not enabled:
Complete Bun root overrides. The block is the same with or without the optional AA path, because Bun cannot do nested overrides and must not globally override `bn.js` or `ws`:
```json
{
"overrides": {
Expand Down Expand Up @@ -68,15 +68,7 @@ Applications using the optional `./zerodev` / Dynamic account-abstraction path m
}
```

Bun 1.3.14 does not support nested overrides. Do **not** globally override `bn.js` or `ws`: Solana/borsh require bn5 while Jayson requires ws7. Bun AA consumers use the same complete safe root block shown above for Bun:
```json
{
"overrides": {
"axios": "1.18.0",
"uuid": "11.1.1"
}
}
```
Bun 1.3.14 does not support nested overrides. Do **not** globally override `bn.js` or `ws`: Solana/borsh require bn5 while Jayson requires ws7. Bun's complete root override block is therefore the same with or without the optional AA path.

For npm, scoped `bn.js@4.12.5` stays on the legacy dependencies' expected major while Solana resolves `bn.js@5.2.5`. Scoped `ws@8.21.0` patches Viem's ws8 subtree while Jayson resolves `ws@7.5.13` from its `^7.5.10` range. The result is audit-clean.

Expand Down Expand Up @@ -121,7 +113,7 @@ All entrypoints are ESM-only. Consumers do not need Vite, esbuild, or other bund
Dynamic 4.x reads Node-style globals, so importing any entrypoint of this package defines them on `globalThis` when they are absent:

- `globalThis.global`, aliased to `globalThis`.
- `globalThis.process`, set to the `process/browser.js` shim with `env.NODE_ENV` set to `production`.
- `globalThis.process`, set to a copy of the `process/browser.js` shim with `env.NODE_ENV` set to `production` (the `process/browser.js` module singleton is left unchanged).

Both are `configurable` and `writable`, and neither is installed when the consumer or runtime already defines it. `NODE_ENV` is set because the browser shim ships an empty `env`, and libraries that branch on `process.env.NODE_ENV !== 'production'` would otherwise take their development path inside a production bundle. These are true globals, so every library loaded afterwards observes them; if your application needs different values, define `global` and `process` before importing this package and they will be left alone.

Expand All @@ -148,7 +140,7 @@ Install only the peers needed by the subpaths your application uses. The declare
Two dependencies exist for transitive resolution rather than for this package's own source, so neither is removable despite nothing here importing them:

- `@wallet-standard/wallet`, because Dynamic's `./solana` module imports it, so `@sei-js/sei-global-wallet/solana` needs it present at runtime.
- `events`, because `@zerodev/sdk` imports the bare `events` specifier. Bundling the `./zerodev` path for the browser fails with `Could not resolve "events"` unless that polyfill is in the tree.
- `events`, because `@zerodev/sdk` imports the bare `events` specifier. Bundling the `./zerodev` path for the browser fails with `Could not resolve "events"` unless that polyfill is in the tree. This only helps hoisted layouts (npm, Bun). Under pnpm's default isolated `node_modules` or Yarn PnP, `events` installed for `@sei-js/sei-global-wallet` is not on `@zerodev/sdk`'s resolution path, so those users still need a bundler alias or an application-level `events` dependency.

The root, `./eip6963`, and `./ethereum` entrypoints need no optional peer at all, including for type resolution. The release verifier typechecks them with `skipLibCheck: false` in a consumer that installs nothing but this package, so a published declaration that referenced a type from an uninstalled peer would fail the check.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[nit] Good caveat to document. It applies equally to the @wallet-standard/wallet bullet directly above: under pnpm's isolated node_modules or Yarn PnP, a dependency declared here is likewise not on @dynamic-labs/global-wallet-client's resolution path. Since both bullets exist for the same hoisting reason, consider lifting the caveat to cover the pair rather than attaching it only to events — as written it reads as if the Solana path is unaffected.


Expand Down
1 change: 0 additions & 1 deletion packages/sei-global-wallet/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@
"@solana/web3.js": "1.98.1",
"@wallet-standard/base": "^1.0.1",
"@wallet-standard/features": "^1.0.3",
"@wallet-standard/wallet": "^1.1.0",
"@zerodev/sdk": "5.5.7",
"typescript": "^5.7.3",
"viem": "2.45.3",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ describe('browser global shim with nothing defined', () => {

try {
await import('../browserGlobal.js');
const processModule = await import('process/browser.js');
const processSingleton = processModule.default;

const runtime = globalThis as typeof globalThis & {
global?: unknown;
Expand All @@ -23,6 +25,10 @@ describe('browser global shim with nothing defined', () => {
// Without this, libraries gating on NODE_ENV take their development
// branch inside a production browser bundle.
expect(runtime.process?.env?.NODE_ENV).toBe('production');
expect(globalThis.process.env.NODE_ENV).toBe('production');

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[nit] This duplicates line 27 — runtime is globalThis widened with an optional-chained type, so both read the same property. The three assertions that follow (lines 29-31) are the ones carrying the new copy-vs-singleton guarantee; this one can go.

expect(processSingleton.env.NODE_ENV).not.toBe('production');
expect(globalThis.process).not.toBe(processSingleton);
expect(globalThis.process.env).not.toBe(processSingleton.env);
} finally {
if (originalGlobal) Object.defineProperty(globalThis, 'global', originalGlobal);
if (originalProcess) Object.defineProperty(globalThis, 'process', originalProcess);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import { afterAll, beforeAll, describe, expect, it } from 'bun:test';

// This file imports the real Dynamic client against a hand-rolled EventTarget
// window / MemoryStorage, so it will fail if Dynamic starts touching `document`
// or `navigator`.

class MemoryStorage implements Storage {
readonly #values = new Map<string, string>();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ describe('sei-global-wallet config', () => {
expect(config.environmentId).toBe('36b63d10-7ba6-49a3-9614-22f471b9283c');
expect(config.eip6963.rdns).toBe('io.sei.global-wallet');
expect(config.eip6963.uuid).toBe('36b63d10-7ba6-49a3-9614-22f471b9283c');
expect(config.eip6963.uuid).toBe(config.environmentId);
expect(config.eip6963.uuid).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i);
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ describe('public entrypoints', () => {
// solana entrypoint needs it installed rather than merely declared as a peer.
it('keeps @wallet-standard/wallet installed for the solana entrypoint', () => {
expect(packageJson.dependencies['@wallet-standard/wallet']).toBe('^1.1.0');
expect(packageJson.devDependencies['@wallet-standard/wallet']).toBeUndefined();
});

// An exact pin would make every Dynamic transitive fix wait on a release here.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { afterAll, beforeAll, describe, expect, it, jest } from 'bun:test';
import { createSolanaWallet, registerWallet } from '../dynamicSolana';
import { registerSolanaStandard } from '../registerSolanaStandard';

jest.mock('../dynamicSolana', () => ({

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[suggestion] Bun does not hoist jest.mock / mock.module the way Jest does, so the static imports on lines 2-3 run first: ../registerSolanaStandard pulls in the real ../dynamicSolana, which loads the real @dynamic-labs/global-wallet-client/solana and ./browserGlobal.js before any mock is registered. The assertions should still hold (Bun rebinds the live ESM bindings afterwards, and the SSR guard returns before touching them), but the mocks aren't actually isolating the module graph the way the file reads.

Two consequences worth weighing:

  • jest.mock('../wallet', () => ({})) supplies no default, yet registerSolanaStandard.ts does import Wallet from './wallet.js'. It only survives because the SSR guard returns early — brittle if that guard ever moves.
  • mock.module is process-global, so this depends on --isolate (which the package test script does pass) to avoid leaking the stubbed ../config into sibling specs.

A await import('../registerSolanaStandard') inside the it, after the mocks are registered, would make the intent match the behaviour.

createSolanaWallet: jest.fn(),
registerWallet: jest.fn()
}));

jest.mock('../wallet', () => ({}));
jest.mock('../config', () => ({
config: {
walletIcon: 'test-icon',
walletName: 'SEI Wallet'
}
}));

describe('registerSolanaStandard during SSR', () => {
const originalWindow = Object.getOwnPropertyDescriptor(globalThis, 'window');

beforeAll(() => {
Reflect.deleteProperty(globalThis, 'window');
});

afterAll(() => {
if (originalWindow) Object.defineProperty(globalThis, 'window', originalWindow);
else Reflect.deleteProperty(globalThis, 'window');
});

it('returns undefined without creating or registering a wallet', () => {
expect(registerSolanaStandard()).toBeUndefined();
expect(createSolanaWallet).not.toHaveBeenCalled();
expect(registerWallet).not.toHaveBeenCalled();
});
});
8 changes: 6 additions & 2 deletions packages/sei-global-wallet/src/lib/browserGlobal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ if (typeof runtime.process === 'undefined') {
// `process/browser.js` ships an empty `env`. Libraries that branch on
// `process.env.NODE_ENV !== 'production'` would otherwise take their
// development path inside a production bundle.
processShim.env.NODE_ENV ??= 'production';
install('process', processShim);
const processForGlobal = {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[suggestion] Copying the shim fixes the singleton mutation, but it also gives up the NODE_ENV guarantee in exactly the scenario cited as motivation. Under ProvidePlugin: { process: 'process/browser.js' }, webpack injects the module singleton into every module that references a free process — and that object's env is now left as the empty {} this comment says is the problem. So a library gated on process.env.NODE_ENV !== 'production' takes its development branch again unless the consumer also runs DefinePlugin (which admittedly most production webpack configs do).

A secondary effect: the bundle now holds two distinct process objects, so import process from 'process' !== globalThis.process, and a write to one env is invisible to the other.

Both may well be the right trade, but the reasoning is worth capturing here — the current comment still describes the pre-copy behaviour ("Every library loaded after this point shares the shim"), which is no longer true for ProvidePlugin consumers.

...processShim,
env: { ...processShim.env }
};
processForGlobal.env.NODE_ENV ??= 'production';
install('process', processForGlobal);
}
9 changes: 6 additions & 3 deletions packages/sei-global-wallet/src/lib/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ interface WalletConfig {
};
}

// Dynamic previously passed environmentId as the EIP-6963 uuid. Keep that
// stable identity (deliberate deviation from EIP-6963's per-session wording).
const environmentId = '36b63d10-7ba6-49a3-9614-22f471b9283c';

export const config: WalletConfig = {
// Wallet name will be seen as the Wallet name
walletName: 'Sei Global Wallet',
Expand All @@ -26,12 +30,11 @@ export const config: WalletConfig = {
// URL of your wallet domain (e.g. https://dynamic.example.com)
walletUrl: 'https://global-wallet.sei.io',
// Environment ID of your wallet (e.g. 1234567890)
environmentId: '36b63d10-7ba6-49a3-9614-22f471b9283c',
environmentId,
// EIP6963 configuration
eip6963: {
// RDNS of your wallet (e.g. com.example.wallet)
rdns: 'io.sei.global-wallet',
// Stable UUIDv4 for the EIP-6963 provider identity.
uuid: '36b63d10-7ba6-49a3-9614-22f471b9283c'
uuid: environmentId
}
};
44 changes: 31 additions & 13 deletions scripts/check-sei-global-wallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,13 @@ const assertSatisfiesDynamicRange = (version: string | undefined, label: string)
return version;
};

const assertMajor = (version: string | undefined, major: string, label: string) => {
assert(version, `${label} was not installed`);
assert(version.startsWith(`${major}.`), `${label} resolved ${version}, expected ${major}.x`);
};

const ghsaIdsIn = (value: unknown) => JSON.stringify(value).match(/GHSA-[a-z0-9-]+/gi) ?? [];

const reportWaiverProgress = (message: string) => {
console.warn(`[waiver] ${message}`);
};
Expand Down Expand Up @@ -576,28 +583,39 @@ const assertAcceptedBunAudit = (result: ProcessResult) => {
}

const report = parseJsonOutput<Record<string, Array<{ severity?: string; url?: string }>>>(result.stdout);
const serialized = JSON.stringify(report);
const reported = new Set(serialized.match(/GHSA-[a-z0-9-]+/gi) ?? []);
const auditFindings = Object.values(report).flat();
const missingGhsa = auditFindings.filter((finding) => ghsaIdsIn(finding).length === 0);
assert.deepEqual(missingGhsa, [], `Bun AA consumer findings without a GHSA id: ${JSON.stringify(missingGhsa)}`);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[suggestion] This new hard fail runs before the waiver subset check, so any Bun audit finding whose serialized form lacks a GHSA- id fails the smoke job outright — even a finding that is otherwise within the accepted waiver. Bun sources advisories from the npm registry, which occasionally surfaces entries with a non-GitHub url (e.g. npmjs.com/advisories/...), and the advisory database drifts on its own schedule.

That is the exact scenario the comment eight lines below argues against ("a withdrawn or upstream-fixed advisory must not fail an unrelated pull request"). Fail-closed on an unidentifiable finding is defensible, but it's the opposite policy from its neighbour, so it's worth being explicit about. Consider either reportWaiverProgress for these instead, or a comment stating that unidentifiable findings are intentionally treated as new exposure.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[suggestion] This turns "any audit entry without a GHSA id" into a hard release-check failure with no waiver path, and it is more sensitive to output shape than the old JSON.stringify(report) scan. Object.values(report).flat() treats every top-level value as a finding, so a non-package top-level key in bun audit --json (a metadata/summary object, or an advisory identified only by CVE) fails the check rather than being ignored.

Failing closed on a genuinely un-waivable advisory is a defensible intent, but consider narrowing it to entries that are actually finding arrays (Object.entries(report).filter(([, v]) => Array.isArray(v))) so a shape change in Bun's output doesn't block an unrelated PR, and include the owning package name in the failure message — JSON.stringify(missingGhsa) alone doesn't say which dependency to look at.


const reported = new Set(auditFindings.flatMap((finding) => ghsaIdsIn(finding)).map((advisory) => advisory.toLowerCase()));
const accepted = new Set(acceptedBunAdvisories.map((advisory) => advisory.toLowerCase()));

// A subset check, not an exact set: the advisory database changes on its own
// schedule, so a withdrawn or upstream-fixed advisory must not fail an
// unrelated pull request, while any new exposure still must.
const unwaived = [...reported].filter((advisory) => !acceptedBunAdvisories.includes(advisory)).sort();
const unwaived = [...reported].filter((advisory) => !accepted.has(advisory)).sort();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[nit] reported now holds lowercased ids, so the failure message on the next assert.deepEqual prints ghsa-378v-28hj-76wf rather than the canonical GHSA-378v-28hj-76wf. Since the message tells the reader to go update packages/sei-global-wallet/README.md, the printed id won't match a grep of that file. Mapping back to the original casing (or upper-casing the GHSA prefix when formatting) keeps the message copy-pasteable.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[nit] reported now holds lowercased ids, so the failure message a few lines below prints ghsa-xxxx-... rather than the canonical GHSA-xxxx-.... Since that message tells the maintainer to add the ids to acceptedBunAdvisories (which uses canonical casing), the output is no longer copy-pasteable. Keeping the original-cased string alongside the lowercase comparison key — or upper-casing for display — avoids the papercut.

assert.deepEqual(
unwaived,
[],
`Bun AA consumer reported advisories outside the accepted waiver: ${unwaived.join(', ')}. Assess them and update packages/sei-global-wallet/README.md before releasing.`
);

const fixed = acceptedBunAdvisories.filter((advisory) => !reported.has(advisory));
const fixed = acceptedBunAdvisories.filter((advisory) => !reported.has(advisory.toLowerCase()));
if (fixed.length > 0) {
reportWaiverProgress(
`Bun no longer reports ${fixed.join(', ')}. Narrow the waiver in packages/sei-global-wallet/README.md and acceptedBunAdvisories in this script.`
);
}

// The documented Axios and UUID overrides must still be taking effect.
assert.doesNotMatch(serialized, /axios|uuid/i);
// Match only those package names as Bun audit keys, not last path segments
// (`@lukeed/uuid`) or advisory titles that happen to contain "uuid".
const blockedOverridePackages = Object.keys(report).filter((name) => name === 'axios' || name === 'uuid');

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[nit] Scoping to exact key names is the right fix for the @lukeed/uuid false positive, but note this check is now only reachable for an axios/uuid advisory that is already in acceptedBunAdvisories — anything else fails the unwaived assertion above and never gets here. Worth a one-line note to that effect, or folding the intent into the waiver list so a future reader doesn't assume this is the primary guard that the overrides are applied.

assert.deepEqual(
blockedOverridePackages,
[],
`Bun AA consumer still reports ${blockedOverridePackages.join(', ')}; the documented Axios and UUID overrides are not taking effect.`
);
console.log(
`Bun AA consumer advisories, all within the waiver: ${Object.entries(report)
.map(([name, findings]) => `${name} (${findings.map(({ severity }) => severity).join(', ')})`)
Expand Down Expand Up @@ -747,11 +765,11 @@ try {
assertNpmDynamicGraph(npmLock);
assert.equal(npmLock.packages['node_modules/ethjs-unit/node_modules/bn.js']?.version, '4.12.5');
assert.equal(npmLock.packages['node_modules/number-to-bn/node_modules/bn.js']?.version, '4.12.5');
assert.equal(npmLock.packages['node_modules/bn.js']?.version, '5.2.5');
assertMajor(npmLock.packages['node_modules/bn.js']?.version, '5', 'hoisted bn.js');
assert.equal(npmLock.packages['node_modules/ws']?.version, '8.21.0');
assert.equal(npmLock.packages['node_modules/viem']?.dependencies?.ws, '8.18.3');
assert.equal(npmLock.packages['node_modules/jayson']?.dependencies?.ws, '^7.5.10');
assert.equal(npmLock.packages['node_modules/jayson/node_modules/ws']?.version, '7.5.13');
assertMajor(npmLock.packages['node_modules/jayson/node_modules/ws']?.version, '7', 'jayson nested ws');
await run(['node', 'check-ssr.mjs'], npmConsumerDir);
await run(['node', 'check-edge-native.mjs'], npmConsumerDir);
await run(['node', 'check-local-aa.mjs'], npmConsumerDir);
Expand Down Expand Up @@ -810,11 +828,11 @@ try {
await run(['bun', 'install'], bunConsumerDir);
const bunLock = await readFile(join(bunConsumerDir, 'bun.lock'), 'utf8');
assertBunDynamicGraph(bunLock);
assert.match(bunLock, /"bn\.js": \["bn\.js@5\.2\.5"/);
assert.match(bunLock, /"ethjs-unit\/bn\.js": \["bn\.js@4\.11\.6"/);
assert.match(bunLock, /"number-to-bn\/bn\.js": \["bn\.js@4\.11\.6"/);
assert.match(bunLock, /"jayson\/ws": \["ws@7\.5\.13"/);
assert.match(bunLock, /"ws": \["ws@8\.18\.3"/);
assert.match(bunLock, /"bn\.js": \["bn\.js@5\./);
assert.match(bunLock, /"ethjs-unit\/bn\.js": \["bn\.js@4\./);
assert.match(bunLock, /"number-to-bn\/bn\.js": \["bn\.js@4\./);
assert.match(bunLock, /"jayson\/ws": \["ws@7\./);
assert.match(bunLock, /"ws": \["ws@8\./);
await run(['bun', 'check-ssr.mjs'], bunConsumerDir);
await run(['bun', 'check-local-aa.mjs'], bunConsumerDir);
assertAcceptedBunAudit(await run(['bun', 'audit', '--json'], bunConsumerDir, true));
Expand All @@ -823,7 +841,7 @@ try {
console.log(
fastCheck
? 'Sei Global Wallet fast npm consumer checks passed.'
: `Sei Global Wallet consumer checks passed: npm scoped patched bn.js/ws8 while preserving Solana bn5/Jayson ws7 with a clean audit; Bun preserved compatible majors and accepted exactly ${acceptedBunAdvisories.join(', ')}.`
: 'Sei Global Wallet consumer checks passed: npm scoped patched bn.js/ws8 while preserving Solana bn5/Jayson ws7 with a clean audit; Bun preserved compatible majors within the accepted advisory waiver.'
);
} finally {
await rm(temporaryRoot, { force: true, recursive: true });
Expand Down
12 changes: 12 additions & 0 deletions scripts/mcp-zod-lock.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,18 @@ describe('mcp sdk zod lock', () => {
).toBeUndefined();
});

test('mcp-server SDK range excludes versions that only declare zod 3', () => {
const mcpServerPackage = JSON.parse(readFileSync(join(import.meta.dir, '..', 'packages/mcp-server/package.json'), 'utf8')) as {
dependencies: { '@modelcontextprotocol/sdk': string };
};
const sdkRange = mcpServerPackage.dependencies['@modelcontextprotocol/sdk'];

expect(Bun.semver.satisfies('1.17.5', sdkRange)).toBe(false);
expect(Bun.semver.satisfies('1.22.0', sdkRange)).toBe(false);
expect(Bun.semver.satisfies('1.23.0', sdkRange)).toBe(true);
expect(Bun.semver.satisfies('1.30.0', sdkRange)).toBe(true);
});

test('the repo lockfile keeps the MCP SDK on the same zod as mcp-server', () => {
const parseErrors: ParseError[] = [];
const lockfile = parse(readFileSync(join(import.meta.dir, '..', 'bun.lock'), 'utf8'), parseErrors, {
Expand Down
Loading