diff --git a/.changeset/align-mcp-zod.md b/.changeset/align-mcp-zod.md index 92e04a84..0e81ca2b 100644 --- a/.changeset/align-mcp-zod.md +++ b/.changeset/align-mcp-zod.md @@ -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. diff --git a/.changeset/fix-global-wallet-release-blockers.md b/.changeset/fix-global-wallet-release-blockers.md index f94865bd..a6e6aa0e 100644 --- a/.changeset/fix-global-wallet-release-blockers.md +++ b/.changeset/fix-global-wallet-release-blockers.md @@ -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. diff --git a/.github/workflows/sei-global-wallet-smoke.yml b/.github/workflows/sei-global-wallet-smoke.yml index ff7169dd..df5476c9 100644 --- a/.github/workflows/sei-global-wallet-smoke.yml +++ b/.github/workflows/sei-global-wallet-smoke.yml @@ -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 diff --git a/bun.lock b/bun.lock index a79e83c6..d330ef35 100644 --- a/bun.lock +++ b/bun.lock @@ -35,7 +35,7 @@ "version": "0.3.3", "bin": "./bin/mcp-server.js", "dependencies": { - "@modelcontextprotocol/sdk": "^1.17.5", + "@modelcontextprotocol/sdk": "^1.23.0", "@noble/hashes": "^1.8.0", "commander": "^14.0.0", "cors": "^2.8.5", @@ -86,7 +86,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", diff --git a/packages/mcp-server/package.json b/packages/mcp-server/package.json index 34d2569b..fe4acac5 100644 --- a/packages/mcp-server/package.json +++ b/packages/mcp-server/package.json @@ -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", diff --git a/packages/sei-global-wallet/README.md b/packages/sei-global-wallet/README.md index 474286a6..c511d9df 100644 --- a/packages/sei-global-wallet/README.md +++ b/packages/sei-global-wallet/README.md @@ -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": { @@ -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. @@ -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. @@ -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. diff --git a/packages/sei-global-wallet/package.json b/packages/sei-global-wallet/package.json index 19d43758..331f0443 100644 --- a/packages/sei-global-wallet/package.json +++ b/packages/sei-global-wallet/package.json @@ -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", diff --git a/packages/sei-global-wallet/src/lib/__tests__/browserGlobal.spec.ts b/packages/sei-global-wallet/src/lib/__tests__/browserGlobal.spec.ts index ae30ae3c..ff029351 100644 --- a/packages/sei-global-wallet/src/lib/__tests__/browserGlobal.spec.ts +++ b/packages/sei-global-wallet/src/lib/__tests__/browserGlobal.spec.ts @@ -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; @@ -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'); + 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); diff --git a/packages/sei-global-wallet/src/lib/__tests__/browserIntegration.spec.ts b/packages/sei-global-wallet/src/lib/__tests__/browserIntegration.spec.ts index 609dd619..59965f25 100644 --- a/packages/sei-global-wallet/src/lib/__tests__/browserIntegration.spec.ts +++ b/packages/sei-global-wallet/src/lib/__tests__/browserIntegration.spec.ts @@ -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(); diff --git a/packages/sei-global-wallet/src/lib/__tests__/config.spec.ts b/packages/sei-global-wallet/src/lib/__tests__/config.spec.ts index 7c048ebc..bfdb595f 100644 --- a/packages/sei-global-wallet/src/lib/__tests__/config.spec.ts +++ b/packages/sei-global-wallet/src/lib/__tests__/config.spec.ts @@ -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); }); diff --git a/packages/sei-global-wallet/src/lib/__tests__/entrypoints.spec.ts b/packages/sei-global-wallet/src/lib/__tests__/entrypoints.spec.ts index ceee5d19..712fd323 100644 --- a/packages/sei-global-wallet/src/lib/__tests__/entrypoints.spec.ts +++ b/packages/sei-global-wallet/src/lib/__tests__/entrypoints.spec.ts @@ -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. diff --git a/packages/sei-global-wallet/src/lib/__tests__/registerSolanaStandard.ssr.spec.ts b/packages/sei-global-wallet/src/lib/__tests__/registerSolanaStandard.ssr.spec.ts new file mode 100644 index 00000000..c320e64d --- /dev/null +++ b/packages/sei-global-wallet/src/lib/__tests__/registerSolanaStandard.ssr.spec.ts @@ -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', () => ({ + 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(); + }); +}); diff --git a/packages/sei-global-wallet/src/lib/browserGlobal.ts b/packages/sei-global-wallet/src/lib/browserGlobal.ts index 6f2cac90..7c982f90 100644 --- a/packages/sei-global-wallet/src/lib/browserGlobal.ts +++ b/packages/sei-global-wallet/src/lib/browserGlobal.ts @@ -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 = { + ...processShim, + env: { ...processShim.env } + }; + processForGlobal.env.NODE_ENV ??= 'production'; + install('process', processForGlobal); } diff --git a/packages/sei-global-wallet/src/lib/config.ts b/packages/sei-global-wallet/src/lib/config.ts index 69f2ddd4..caf8f6cb 100644 --- a/packages/sei-global-wallet/src/lib/config.ts +++ b/packages/sei-global-wallet/src/lib/config.ts @@ -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', @@ -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 } }; diff --git a/scripts/check-sei-global-wallet.ts b/scripts/check-sei-global-wallet.ts index 00632398..a17a28a0 100644 --- a/scripts/check-sei-global-wallet.ts +++ b/scripts/check-sei-global-wallet.ts @@ -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}`); }; @@ -576,20 +583,24 @@ const assertAcceptedBunAudit = (result: ProcessResult) => { } const report = parseJsonOutput>>(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)}`); + + 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(); 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.` @@ -597,7 +608,14 @@ const assertAcceptedBunAudit = (result: ProcessResult) => { } // 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'); + 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(', ')})`) @@ -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); @@ -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)); @@ -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 }); diff --git a/scripts/mcp-zod-lock.test.ts b/scripts/mcp-zod-lock.test.ts index d87ba272..810d5cd2 100644 --- a/scripts/mcp-zod-lock.test.ts +++ b/scripts/mcp-zod-lock.test.ts @@ -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, {