@mysten-incubation/dev-wallet 0.5.0 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +8 -0
- package/README.md +5 -4
- package/dist/inject/index.d.mts +4 -0
- package/dist/inject/index.d.mts.map +1 -1
- package/dist/inject/index.mjs +31 -0
- package/dist/inject/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/inject/index.ts +55 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# @mysten-incubation/dev-wallet
|
|
2
2
|
|
|
3
|
+
## 0.5.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 8119fac: Expose dev-wallet allowed origins to the injected adapter, reject forbidden page origins
|
|
8
|
+
with a browser-visible diagnostic, and allow same-stack routed app endpoints without allowing raw
|
|
9
|
+
loopback origins.
|
|
10
|
+
|
|
3
11
|
## 0.5.0
|
|
4
12
|
|
|
5
13
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -28,7 +28,7 @@ A development-only wallet for building and testing Sui dApps.
|
|
|
28
28
|
## Installation
|
|
29
29
|
|
|
30
30
|
```bash
|
|
31
|
-
|
|
31
|
+
pnpm add @mysten-incubation/dev-wallet
|
|
32
32
|
```
|
|
33
33
|
|
|
34
34
|
## Quick start
|
|
@@ -91,7 +91,7 @@ Run a self-contained wallet as a separate web app — signing works via popup, s
|
|
|
91
91
|
browser wallet.
|
|
92
92
|
|
|
93
93
|
```bash
|
|
94
|
-
|
|
94
|
+
pnpm dlx @mysten-incubation/dev-wallet serve
|
|
95
95
|
```
|
|
96
96
|
|
|
97
97
|
Register it through `walletInitializers` using `devWalletClientInitializer`:
|
|
@@ -114,7 +114,8 @@ import { DevWalletClient } from '@mysten-incubation/dev-wallet/client';
|
|
|
114
114
|
DevWalletClient.register({ origin: 'http://localhost:5174' });
|
|
115
115
|
```
|
|
116
116
|
|
|
117
|
-
The default port is 5174. Use `--port` to set a different port
|
|
117
|
+
The default port is 5174. Use `--port` to set a different port — the `origin` you pass to
|
|
118
|
+
`devWalletClientInitializer` / `DevWalletClient.register` must match whatever port `serve` runs on.
|
|
118
119
|
|
|
119
120
|
The standalone wallet includes WebCrypto and InMemory adapters by default. If `sui` is on your PATH,
|
|
120
121
|
the CLI adapter is also available — so you can sign with the same address you published contracts
|
|
@@ -202,7 +203,7 @@ The Remote CLI adapter connects to your local `sui` CLI over HTTP. Private keys
|
|
|
202
203
|
The easiest way is the standalone wallet:
|
|
203
204
|
|
|
204
205
|
```bash
|
|
205
|
-
|
|
206
|
+
pnpm dlx @mysten-incubation/dev-wallet serve
|
|
206
207
|
```
|
|
207
208
|
|
|
208
209
|
The terminal prints a URL with an auth token (e.g. `http://localhost:5174/?token=abc123`). Open it
|
package/dist/inject/index.d.mts
CHANGED
|
@@ -26,6 +26,10 @@ interface DevstackNetworkInfo {
|
|
|
26
26
|
interface RegisterDevstackDevWalletConfig {
|
|
27
27
|
/** Wallet-app origin (the dev-wallet connection's `walletUrl`). */
|
|
28
28
|
readonly serverOrigin: string;
|
|
29
|
+
/** Browser page origins accepted by the devstack wallet server. When the
|
|
30
|
+
* current page is not in this list, the HTTP adapter will be CORS-blocked;
|
|
31
|
+
* this is non-secret metadata used only for a local diagnostic. */
|
|
32
|
+
readonly allowedOrigins?: ReadonlyArray<string>;
|
|
29
33
|
/** Bearer pairing token (read by the Vite dev server from the wallet's
|
|
30
34
|
* `0o600` side-channel token file), or null. */
|
|
31
35
|
readonly token?: string | null;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.mts","names":[],"sources":["../../src/inject/index.ts"],"mappings":";;;;;AA8BA;UAAiB,mBAAA;EAAA,SACP,OAAA;EAAA,SACA,IAAI;AAAA;AAWd;;;;AAKgB;AAGhB;;;AARA,UAAiB,mBAAA;
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../../src/inject/index.ts"],"mappings":";;;;;AA8BA;UAAiB,mBAAA;EAAA,SACP,OAAA;EAAA,SACA,IAAI;AAAA;AAWd;;;;AAKgB;AAGhB;;;AARA,UAAiB,mBAAA;EAoB2B;EAAA,SAlBlC,GAAA;EAkBU;;EAAA,SAfV,MAAM;AAAA;AAAA,UAGC,+BAAA;EA2BwB;EAAA,SAzB/B,YAAA;EAAA;;;EAAA,SAIA,cAAA,GAAiB,aAAA;EAMjB;;EAAA,SAHA,KAAA;EAGkC;;EAAA,SAAlC,QAAA,EAAU,QAAA,CAAS,MAAA,SAAe,mBAAA;EASd;;;;;;;;EAAA,SAApB,QAAA,GAAW,QAAA,CAAS,MAAA,SAAe,mBAAA;EAa5B;;;EAAA,SATP,cAAA;EAUA;EAAA,SARA,WAAA,GAAc,iBAAA;EAUd;EAAA,SARA,OAAA;EAQO;EAAA,SANP,IAAA;AAAA;AAAA,UAGO,+BAAA;EAAA,SACP,MAAA,EAAQ,SAAS;EAMnB;EAAA,SAJE,OAAA;AAAA;;QAIF,MAAA;EAAA,IAEH,uBAAA;EAMkF;AAAA;AA+DvF;;EA/DuF,IAAlF,4BAAA,EAA8B,OAAO,CAAC,+BAAA;AAAA;;;;;;;;;;AAiED;iBAF1B,yBAAA,CACf,MAAA,EAAQ,+BAAA,GACN,OAAA,CAAQ,+BAAA"}
|
package/dist/inject/index.mjs
CHANGED
|
@@ -1,6 +1,36 @@
|
|
|
1
1
|
import { mountAndRegisterDevWallet } from "../wallet/mount-and-register.mjs";
|
|
2
2
|
import { DevstackSignerAdapter } from "../adapters/devstack-adapter.mjs";
|
|
3
3
|
//#region src/inject/index.ts
|
|
4
|
+
const currentPageOrigin = () => {
|
|
5
|
+
const location = globalThis.location;
|
|
6
|
+
return typeof location?.origin === "string" && location.origin.length > 0 ? location.origin : null;
|
|
7
|
+
};
|
|
8
|
+
const originMatchesAllowed = (origin, allowed) => {
|
|
9
|
+
if (!allowed.includes("*")) return origin === allowed;
|
|
10
|
+
let originUrl;
|
|
11
|
+
let patternUrl;
|
|
12
|
+
try {
|
|
13
|
+
originUrl = new URL(origin);
|
|
14
|
+
patternUrl = new URL(allowed.replace("*", "devstack-origin-probe"));
|
|
15
|
+
} catch {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
if (originUrl.origin !== origin) return false;
|
|
19
|
+
if (originUrl.protocol !== patternUrl.protocol || originUrl.port !== patternUrl.port) return false;
|
|
20
|
+
const originLabels = originUrl.hostname.toLowerCase().split(".");
|
|
21
|
+
const patternLabels = patternUrl.hostname.toLowerCase().split(".");
|
|
22
|
+
return originLabels.length === patternLabels.length && patternLabels[0] === "devstack-origin-probe" && originLabels[0].length > 0 && originLabels.slice(1).join(".") === patternLabels.slice(1).join(".");
|
|
23
|
+
};
|
|
24
|
+
const assertPageOriginAllowed = (allowedOrigins) => {
|
|
25
|
+
if (allowedOrigins === void 0 || allowedOrigins.length === 0) return;
|
|
26
|
+
const origin = currentPageOrigin();
|
|
27
|
+
if (origin === null || origin === "null") return;
|
|
28
|
+
if (allowedOrigins.some((allowed) => originMatchesAllowed(origin, allowed))) return;
|
|
29
|
+
const recommended = allowedOrigins.find((allowed) => !allowed.includes("*")) ?? allowedOrigins[0];
|
|
30
|
+
const message = `[devstack] dev wallet is not available from page origin ${origin}. Allowed origins: ${allowedOrigins.join(", ")}.${recommended === void 0 ? "" : ` Open ${recommended}, or add wallet({ allowedOrigins: ['${origin}'] }) if this origin is intentional.`}`;
|
|
31
|
+
console.error(message);
|
|
32
|
+
throw new Error(`devstack dev wallet page origin ${origin} is not allowlisted`);
|
|
33
|
+
};
|
|
4
34
|
/**
|
|
5
35
|
* Construct + register the devstack dev wallet on the current page and wire
|
|
6
36
|
* the Playwright `connectAs` slot. Idempotent: a second call is a no-op and
|
|
@@ -25,6 +55,7 @@ function registerDevstackDevWallet(config) {
|
|
|
25
55
|
async function registerDevstackDevWalletImpl(config) {
|
|
26
56
|
const { mountUI = true, autoApprove = false } = config;
|
|
27
57
|
globalThis.__DEV_WALLET_INJECTED__ = true;
|
|
58
|
+
assertPageOriginAllowed(config.allowedOrigins);
|
|
28
59
|
const networkInfos = config.networks !== void 0 && Object.keys(config.networks).length > 0 ? config.networks : { localnet: { rpc: "http://127.0.0.1:9000" } };
|
|
29
60
|
const networkNames = Object.keys(networkInfos);
|
|
30
61
|
const adapter = new DevstackSignerAdapter({
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":[],"sources":["../../src/inject/index.ts"],"sourcesContent":["// Copyright (c) Mysten Labs, Inc.\n// SPDX-License-Identifier: Apache-2.0\n//\n// Page-register entry for the devstack dev wallet.\n//\n// This is the single function the devstack Vite plugin injects into a dev\n// page. It builds the token-based `DevstackSignerAdapter` (headless — signs\n// over HTTP, no popup) + the routed-RPC network map, then hands those to the\n// shared `mountAndRegisterDevWallet` core helper, which constructs the\n// `DevWallet`, registers it via wallet-standard (so dApp Kit auto-discovers\n// it), and mounts the floating wallet UI. The ONLY logic that lives here is\n// the devstack-integration glue: the adapter-from-config and the routed-RPC\n// network map.\n//\n// The wallet does NOT pre-connect or seed dApp Kit storage: a fresh page\n// loads disconnected, dApp Kit's own `autoConnect` only re-connects a genuine\n// prior session, and the Playwright `connectAs` helper performs an explicit\n// connection through dApp Kit's public API. That test bridge is registered\n// app-side via `@mysten-incubation/devstack/dapp-kit`'s\n// `registerDAppKitForTesting(dAppKit)` (DEV-only) — it needs the app's dApp\n// Kit instance, which the wallet has no reference to.\n\nimport type { SignerAdapter } from '../types.js';\nimport type { AutoApprovePolicy, DevWallet } from '../wallet/dev-wallet.js';\nimport { mountAndRegisterDevWallet } from '../wallet/mount-and-register.js';\nimport { DevstackSignerAdapter } from '../adapters/devstack-adapter.js';\n\n/** Shape of a single entry in the dev `accounts` map (resolved off the\n * injected deployment envelope via `resolveAccounts()`). Only `address` is\n * consumed here. */\nexport interface DevstackAccountInfo {\n\treadonly address: string;\n\treadonly name?: string;\n}\n\n/** One network the dev wallet operates on — its routed RPC endpoint plus an\n * optional faucet. The wallet advertises every key as the wallet-standard\n * chain `sui:<name>` and routes signing/execution to whichever network dApp\n * Kit has selected; the active network's `faucet` drives a fund flow. The\n * `rpc` MUST be the SAME routed RPC the app's dApp Kit client uses for that\n * network (a raw `127.0.0.1:9000` is CORS-blocked from the routed page\n * origin). The wallet is agnostic to live-vs-local — local and live networks\n * are passed through the same map. */\nexport interface DevstackNetworkInfo {\n\t/** Routed RPC endpoint for this network. */\n\treadonly rpc: string;\n\t/** Optional faucet endpoint for this network (absent on live mainnet /\n\t * fork stacks). */\n\treadonly faucet?: string | null;\n}\n\nexport interface RegisterDevstackDevWalletConfig {\n\t/** Wallet-app origin (the dev-wallet connection's `walletUrl`). */\n\treadonly serverOrigin: string;\n\t/** Bearer pairing token (read by the Vite dev server from the wallet's\n\t * `0o600` side-channel token file), or null. */\n\treadonly token?: string | null;\n\t/** Dev name→account map (resolved off the injected deployment envelope via\n\t * `resolveAccounts()`). */\n\treadonly accounts: Readonly<Record<string, DevstackAccountInfo>>;\n\t/** The FULL network set the app supports — `{ <name>: { rpc, faucet? } }`,\n\t * covering BOTH the live local network and any live (devnet/testnet/…)\n\t * networks the deployment envelope carries. The wallet advertises each as\n\t * the wallet-standard chain `sui:<name>` and operates on whichever one\n\t * dApp Kit has selected, so it persists across a UI `switchNetwork` (only\n\t * the active chain changes — the wallet is registered once). When omitted\n\t * the wallet falls back to a single localnet entry (the non-routed default\n\t * — only correct for a bare localnet setup). */\n\treadonly networks?: Readonly<Record<string, DevstackNetworkInfo>>;\n\t/** The network the wallet opens on (its initial active network). Must be a\n\t * key of `networks`; defaults to the first `networks` key (`'localnet'`\n\t * when `networks` is omitted). */\n\treadonly defaultNetwork?: string;\n\t/** Auto-approve all signing requests (headless e2e). Defaults to false. */\n\treadonly autoApprove?: AutoApprovePolicy;\n\t/** Mount the floating wallet drawer UI. Defaults to true. */\n\treadonly mountUI?: boolean;\n\t/** Wallet display name. Defaults to `'Devstack'`. */\n\treadonly name?: string;\n}\n\nexport interface RegisterDevstackDevWalletResult {\n\treadonly wallet: DevWallet;\n\t/** Unregister the wallet, unmount the UI, and tear down the adapter. */\n\treadonly dispose: () => void;\n}\n\n/** Global marker so tests / the plugin can assert injection happened. */\ndeclare global {\n\t// eslint-disable-next-line no-var\n\tvar __DEV_WALLET_INJECTED__: boolean | undefined;\n\t/** In-flight (or settled) registration promise. Published BEFORE the first\n\t * `await` so concurrent evaluations (HMR / double import) coalesce onto a\n\t * single registration instead of each constructing + registering + mounting\n\t * their own wallet. See `registerDevstackDevWallet`. */\n\t// eslint-disable-next-line no-var\n\tvar __devstackDevWalletPromise__: Promise<RegisterDevstackDevWalletResult> | undefined;\n}\n\n/**\n * Construct + register the devstack dev wallet on the current page and wire\n * the Playwright `connectAs` slot. Idempotent: a second call is a no-op and\n * returns the existing instance.\n *\n * Idempotency is enforced SYNCHRONOUSLY: an in-flight registration promise is\n * published on `globalThis.__devstackDevWalletPromise__` before the first\n * `await`, so two evaluations that race in before the first one finishes both\n * resolve to the SAME wallet instead of double-registering.\n */\nexport function registerDevstackDevWallet(\n\tconfig: RegisterDevstackDevWalletConfig,\n): Promise<RegisterDevstackDevWalletResult> {\n\tconst g = globalThis as {\n\t\t__devstackDevWallet__?: RegisterDevstackDevWalletResult;\n\t\t__devstackDevWalletPromise__?: Promise<RegisterDevstackDevWalletResult>;\n\t};\n\t// Fast path: a prior call already completed.\n\tif (g.__devstackDevWallet__ !== undefined) return Promise.resolve(g.__devstackDevWallet__);\n\t// In-flight path: a prior call is still booting — share its promise. This\n\t// guard runs before any `await`, so it closes the double-init window that a\n\t// post-registration-only marker leaves open under HMR / double import.\n\tif (g.__devstackDevWalletPromise__ !== undefined) return g.__devstackDevWalletPromise__;\n\n\tconst promise = registerDevstackDevWalletImpl(config);\n\tg.__devstackDevWalletPromise__ = promise;\n\t// On failure, clear the in-flight promise so a later call can retry instead\n\t// of being stuck awaiting a rejected registration.\n\tpromise.catch(() => {\n\t\tif (g.__devstackDevWalletPromise__ === promise) {\n\t\t\tg.__devstackDevWalletPromise__ = undefined;\n\t\t}\n\t});\n\treturn promise;\n}\n\nasync function registerDevstackDevWalletImpl(\n\tconfig: RegisterDevstackDevWalletConfig,\n): Promise<RegisterDevstackDevWalletResult> {\n\tconst { mountUI = true, autoApprove = false } = config;\n\tglobalThis.__DEV_WALLET_INJECTED__ = true;\n\n\t// Resolve the FULL network set up front so it can be advertised both on\n\t// the wallet (its `chains`) and on every devstack account (per-account\n\t// `chains`) — fork/custom networks must be signable from both surfaces.\n\tconst networkInfos: Readonly<Record<string, DevstackNetworkInfo>> =\n\t\tconfig.networks !== undefined && Object.keys(config.networks).length > 0\n\t\t\t? config.networks\n\t\t\t: { localnet: { rpc: 'http://127.0.0.1:9000' } };\n\tconst networkNames = Object.keys(networkInfos);\n\n\tconst adapter = new DevstackSignerAdapter({\n\t\tserverOrigin: config.serverOrigin,\n\t\ttoken: config.token ?? null,\n\t\tname: config.name ?? 'Devstack',\n\t\tnetworks: networkNames,\n\t});\n\n\t// The wallet EXECUTES (and simulates) `signAndExecuteTransaction` with\n\t// its OWN client — adapter signing is HTTP (server-side keys), but\n\t// execution goes through the wallet's network client. That client MUST\n\t// hit the same routed RPC the app's dApp Kit uses (a raw 127.0.0.1 RPC\n\t// is CORS-blocked from the routed page origin).\n\t//\n\t// Expose the FULL network set the app supports (local + any live networks\n\t// from the deployment envelope) so the wallet operates on whichever\n\t// network dApp Kit has selected. The wallet is registered ONCE via\n\t// wallet-standard and advertises each key as the wallet-standard chain\n\t// `sui:<network>` (matching the `sui:<network>` dApp Kit forwards for\n\t// signing — the `sui:` prefix lives only at the wallet-standard boundary);\n\t// a UI `switchNetwork` just changes the active chain, it never\n\t// re-registers. The wallet does NOT know live-vs-local — both flow through\n\t// the same map. Omitting `networks` falls back to a single localnet entry\n\t// (the legacy non-routed default — resolved as `networkInfos` above).\n\tconst networks: Record<string, string> = {};\n\tconst faucets: Record<string, string> = {};\n\tfor (const [net, info] of Object.entries(networkInfos)) {\n\t\tnetworks[net] = info.rpc;\n\t\tif (info.faucet !== undefined && info.faucet !== null && info.faucet.length > 0) {\n\t\t\tfaucets[net] = info.faucet;\n\t\t}\n\t}\n\t// The wallet opens on the requested default (the dApp-Kit default network),\n\t// falling back to the first declared network.\n\tconst activeNetwork =\n\t\tconfig.defaultNetwork !== undefined && networks[config.defaultNetwork] !== undefined\n\t\t\t? config.defaultNetwork\n\t\t\t: (Object.keys(networks)[0] ?? 'localnet');\n\n\t// Delegate the construct → init-adapter → mount-UI → register → dispose\n\t// sequence to the shared dev-wallet core helper. The `DevstackSignerAdapter`\n\t// brings the stack's server-resolved accounts (alice/bob/carol — headless\n\t// HTTP signing) and is always present. The optional `WebCryptoSignerAdapter`\n\t// lets the user create their OWN accounts in the wallet UI, persisted in\n\t// IndexedDB across reloads (non-extractable WebCrypto keys, NOT in-memory) —\n\t// but it depends on the OPTIONAL `@mysten/signers` peer. Load it dynamically\n\t// and gate on its presence: an app that doesn't install `@mysten/signers`\n\t// still gets a working dev wallet (just without create-your-own-account)\n\t// instead of a hard inject crash on the unresolved peer. `createInitialAccount`\n\t// stays off — the devstack adapter already supplies accounts, so we never\n\t// fabricate a throwaway key; the WebCrypto adapter starts empty until the\n\t// user adds one.\n\tconst adapters: SignerAdapter[] = [adapter];\n\ttry {\n\t\tconst { WebCryptoSignerAdapter } = await import('../adapters/webcrypto-adapter.js');\n\t\tadapters.push(new WebCryptoSignerAdapter());\n\t} catch (error) {\n\t\tconsole.info(\n\t\t\t'[dev-wallet] @mysten/signers is not installed — WebCrypto account creation is ' +\n\t\t\t\t'disabled. Install @mysten/signers to enable creating your own accounts in the ' +\n\t\t\t\t'dev wallet.',\n\t\t\terror,\n\t\t);\n\t}\n\n\tconst { wallet, dispose: disposeWallet } = await mountAndRegisterDevWallet({\n\t\tadapters,\n\t\tname: config.name ?? 'Devstack',\n\t\tautoApprove,\n\t\t// Auto-approve `standard:connect` whenever signing is auto-approved — both\n\t\t// are the headless-e2e signal (`DEVSTACK_AUTO_APPROVE`). The test bridge's\n\t\t// explicit `connectWallet(...)` invokes the wallet's connect; without this\n\t\t// it would queue a pending request and block on UI approval that never\n\t\t// comes. In normal dev (no auto-approve) a human approves the connect.\n\t\tautoConnect: Boolean(autoApprove),\n\t\tnetworks,\n\t\tfaucets,\n\t\tactiveNetwork,\n\t\tmountUI,\n\t});\n\n\tconst result: RegisterDevstackDevWalletResult = {\n\t\twallet,\n\t\tdispose() {\n\t\t\tdisposeWallet();\n\t\t\tglobalThis.__DEV_WALLET_INJECTED__ = false;\n\t\t\tdelete (globalThis as { __devstackDevWallet__?: unknown }).__devstackDevWallet__;\n\t\t\t// Clear the in-flight/settled registration promise too, so a\n\t\t\t// subsequent `registerDevstackDevWallet` re-initializes rather than\n\t\t\t// handing back the disposed instance.\n\t\t\tdelete (globalThis as { __devstackDevWalletPromise__?: unknown })\n\t\t\t\t.__devstackDevWalletPromise__;\n\t\t},\n\t};\n\t(\n\t\tglobalThis as { __devstackDevWallet__?: RegisterDevstackDevWalletResult }\n\t).__devstackDevWallet__ = result;\n\treturn result;\n}\n"],"mappings":";;;;;;;;;;;;;AA6GA,SAAgB,0BACf,QAC2C;CAC3C,MAAM,IAAI;CAKV,IAAI,EAAE,0BAA0B,QAAW,OAAO,QAAQ,QAAQ,EAAE,qBAAqB;CAIzF,IAAI,EAAE,iCAAiC,QAAW,OAAO,EAAE;CAE3D,MAAM,UAAU,8BAA8B,MAAM;CACpD,EAAE,+BAA+B;CAGjC,QAAQ,YAAY;EACnB,IAAI,EAAE,iCAAiC,SACtC,EAAE,+BAA+B;CAEnC,CAAC;CACD,OAAO;AACR;AAEA,eAAe,8BACd,QAC2C;CAC3C,MAAM,EAAE,UAAU,MAAM,cAAc,UAAU;CAChD,WAAW,0BAA0B;CAKrC,MAAM,eACL,OAAO,aAAa,UAAa,OAAO,KAAK,OAAO,QAAQ,CAAC,CAAC,SAAS,IACpE,OAAO,WACP,EAAE,UAAU,EAAE,KAAK,wBAAwB,EAAE;CACjD,MAAM,eAAe,OAAO,KAAK,YAAY;CAE7C,MAAM,UAAU,IAAI,sBAAsB;EACzC,cAAc,OAAO;EACrB,OAAO,OAAO,SAAS;EACvB,MAAM,OAAO,QAAQ;EACrB,UAAU;CACX,CAAC;CAkBD,MAAM,WAAmC,CAAC;CAC1C,MAAM,UAAkC,CAAC;CACzC,KAAK,MAAM,CAAC,KAAK,SAAS,OAAO,QAAQ,YAAY,GAAG;EACvD,SAAS,OAAO,KAAK;EACrB,IAAI,KAAK,WAAW,UAAa,KAAK,WAAW,QAAQ,KAAK,OAAO,SAAS,GAC7E,QAAQ,OAAO,KAAK;CAEtB;CAGA,MAAM,gBACL,OAAO,mBAAmB,UAAa,SAAS,OAAO,oBAAoB,SACxE,OAAO,iBACN,OAAO,KAAK,QAAQ,CAAC,CAAC,MAAM;CAejC,MAAM,WAA4B,CAAC,OAAO;CAC1C,IAAI;EACH,MAAM,EAAE,2BAA2B,MAAM,OAAO;EAChD,SAAS,KAAK,IAAI,uBAAuB,CAAC;CAC3C,SAAS,OAAO;EACf,QAAQ,KACP,2KAGA,KACD;CACD;CAEA,MAAM,EAAE,QAAQ,SAAS,kBAAkB,MAAM,0BAA0B;EAC1E;EACA,MAAM,OAAO,QAAQ;EACrB;EAMA,aAAa,QAAQ,WAAW;EAChC;EACA;EACA;EACA;CACD,CAAC;CAED,MAAM,SAA0C;EAC/C;EACA,UAAU;GACT,cAAc;GACd,WAAW,0BAA0B;GACrC,OAAQ,WAAmD;GAI3D,OAAQ,WACN;EACH;CACD;CACA,WAEE,wBAAwB;CAC1B,OAAO;AACR"}
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../../src/inject/index.ts"],"sourcesContent":["// Copyright (c) Mysten Labs, Inc.\n// SPDX-License-Identifier: Apache-2.0\n//\n// Page-register entry for the devstack dev wallet.\n//\n// This is the single function the devstack Vite plugin injects into a dev\n// page. It builds the token-based `DevstackSignerAdapter` (headless — signs\n// over HTTP, no popup) + the routed-RPC network map, then hands those to the\n// shared `mountAndRegisterDevWallet` core helper, which constructs the\n// `DevWallet`, registers it via wallet-standard (so dApp Kit auto-discovers\n// it), and mounts the floating wallet UI. The ONLY logic that lives here is\n// the devstack-integration glue: the adapter-from-config and the routed-RPC\n// network map.\n//\n// The wallet does NOT pre-connect or seed dApp Kit storage: a fresh page\n// loads disconnected, dApp Kit's own `autoConnect` only re-connects a genuine\n// prior session, and the Playwright `connectAs` helper performs an explicit\n// connection through dApp Kit's public API. That test bridge is registered\n// app-side via `@mysten-incubation/devstack/dapp-kit`'s\n// `registerDAppKitForTesting(dAppKit)` (DEV-only) — it needs the app's dApp\n// Kit instance, which the wallet has no reference to.\n\nimport type { SignerAdapter } from '../types.js';\nimport type { AutoApprovePolicy, DevWallet } from '../wallet/dev-wallet.js';\nimport { mountAndRegisterDevWallet } from '../wallet/mount-and-register.js';\nimport { DevstackSignerAdapter } from '../adapters/devstack-adapter.js';\n\n/** Shape of a single entry in the dev `accounts` map (resolved off the\n * injected deployment envelope via `resolveAccounts()`). Only `address` is\n * consumed here. */\nexport interface DevstackAccountInfo {\n\treadonly address: string;\n\treadonly name?: string;\n}\n\n/** One network the dev wallet operates on — its routed RPC endpoint plus an\n * optional faucet. The wallet advertises every key as the wallet-standard\n * chain `sui:<name>` and routes signing/execution to whichever network dApp\n * Kit has selected; the active network's `faucet` drives a fund flow. The\n * `rpc` MUST be the SAME routed RPC the app's dApp Kit client uses for that\n * network (a raw `127.0.0.1:9000` is CORS-blocked from the routed page\n * origin). The wallet is agnostic to live-vs-local — local and live networks\n * are passed through the same map. */\nexport interface DevstackNetworkInfo {\n\t/** Routed RPC endpoint for this network. */\n\treadonly rpc: string;\n\t/** Optional faucet endpoint for this network (absent on live mainnet /\n\t * fork stacks). */\n\treadonly faucet?: string | null;\n}\n\nexport interface RegisterDevstackDevWalletConfig {\n\t/** Wallet-app origin (the dev-wallet connection's `walletUrl`). */\n\treadonly serverOrigin: string;\n\t/** Browser page origins accepted by the devstack wallet server. When the\n\t * current page is not in this list, the HTTP adapter will be CORS-blocked;\n\t * this is non-secret metadata used only for a local diagnostic. */\n\treadonly allowedOrigins?: ReadonlyArray<string>;\n\t/** Bearer pairing token (read by the Vite dev server from the wallet's\n\t * `0o600` side-channel token file), or null. */\n\treadonly token?: string | null;\n\t/** Dev name→account map (resolved off the injected deployment envelope via\n\t * `resolveAccounts()`). */\n\treadonly accounts: Readonly<Record<string, DevstackAccountInfo>>;\n\t/** The FULL network set the app supports — `{ <name>: { rpc, faucet? } }`,\n\t * covering BOTH the live local network and any live (devnet/testnet/…)\n\t * networks the deployment envelope carries. The wallet advertises each as\n\t * the wallet-standard chain `sui:<name>` and operates on whichever one\n\t * dApp Kit has selected, so it persists across a UI `switchNetwork` (only\n\t * the active chain changes — the wallet is registered once). When omitted\n\t * the wallet falls back to a single localnet entry (the non-routed default\n\t * — only correct for a bare localnet setup). */\n\treadonly networks?: Readonly<Record<string, DevstackNetworkInfo>>;\n\t/** The network the wallet opens on (its initial active network). Must be a\n\t * key of `networks`; defaults to the first `networks` key (`'localnet'`\n\t * when `networks` is omitted). */\n\treadonly defaultNetwork?: string;\n\t/** Auto-approve all signing requests (headless e2e). Defaults to false. */\n\treadonly autoApprove?: AutoApprovePolicy;\n\t/** Mount the floating wallet drawer UI. Defaults to true. */\n\treadonly mountUI?: boolean;\n\t/** Wallet display name. Defaults to `'Devstack'`. */\n\treadonly name?: string;\n}\n\nexport interface RegisterDevstackDevWalletResult {\n\treadonly wallet: DevWallet;\n\t/** Unregister the wallet, unmount the UI, and tear down the adapter. */\n\treadonly dispose: () => void;\n}\n\n/** Global marker so tests / the plugin can assert injection happened. */\ndeclare global {\n\t// eslint-disable-next-line no-var\n\tvar __DEV_WALLET_INJECTED__: boolean | undefined;\n\t/** In-flight (or settled) registration promise. Published BEFORE the first\n\t * `await` so concurrent evaluations (HMR / double import) coalesce onto a\n\t * single registration instead of each constructing + registering + mounting\n\t * their own wallet. See `registerDevstackDevWallet`. */\n\t// eslint-disable-next-line no-var\n\tvar __devstackDevWalletPromise__: Promise<RegisterDevstackDevWalletResult> | undefined;\n}\n\nconst currentPageOrigin = (): string | null => {\n\tconst location = (globalThis as { location?: { origin?: unknown } }).location;\n\treturn typeof location?.origin === 'string' && location.origin.length > 0\n\t\t? location.origin\n\t\t: null;\n};\n\nconst originMatchesAllowed = (origin: string, allowed: string): boolean => {\n\tif (!allowed.includes('*')) return origin === allowed;\n\tlet originUrl: URL;\n\tlet patternUrl: URL;\n\ttry {\n\t\toriginUrl = new URL(origin);\n\t\tpatternUrl = new URL(allowed.replace('*', 'devstack-origin-probe'));\n\t} catch {\n\t\treturn false;\n\t}\n\tif (originUrl.origin !== origin) return false;\n\tif (originUrl.protocol !== patternUrl.protocol || originUrl.port !== patternUrl.port) {\n\t\treturn false;\n\t}\n\tconst originLabels = originUrl.hostname.toLowerCase().split('.');\n\tconst patternLabels = patternUrl.hostname.toLowerCase().split('.');\n\treturn (\n\t\toriginLabels.length === patternLabels.length &&\n\t\tpatternLabels[0] === 'devstack-origin-probe' &&\n\t\toriginLabels[0]!.length > 0 &&\n\t\toriginLabels.slice(1).join('.') === patternLabels.slice(1).join('.')\n\t);\n};\n\nconst assertPageOriginAllowed = (allowedOrigins: ReadonlyArray<string> | undefined): void => {\n\tif (allowedOrigins === undefined || allowedOrigins.length === 0) return;\n\tconst origin = currentPageOrigin();\n\tif (origin === null || origin === 'null') return;\n\tif (allowedOrigins.some((allowed) => originMatchesAllowed(origin, allowed))) return;\n\n\tconst recommended = allowedOrigins.find((allowed) => !allowed.includes('*')) ?? allowedOrigins[0];\n\tconst allowedList = allowedOrigins.join(', ');\n\tconst guidance =\n\t\trecommended === undefined\n\t\t\t? ''\n\t\t\t: ` Open ${recommended}, or add wallet({ allowedOrigins: ['${origin}'] }) if this origin is intentional.`;\n\tconst message =\n\t\t`[devstack] dev wallet is not available from page origin ${origin}. ` +\n\t\t`Allowed origins: ${allowedList}.${guidance}`;\n\tconsole.error(message);\n\tthrow new Error(`devstack dev wallet page origin ${origin} is not allowlisted`);\n};\n\n/**\n * Construct + register the devstack dev wallet on the current page and wire\n * the Playwright `connectAs` slot. Idempotent: a second call is a no-op and\n * returns the existing instance.\n *\n * Idempotency is enforced SYNCHRONOUSLY: an in-flight registration promise is\n * published on `globalThis.__devstackDevWalletPromise__` before the first\n * `await`, so two evaluations that race in before the first one finishes both\n * resolve to the SAME wallet instead of double-registering.\n */\nexport function registerDevstackDevWallet(\n\tconfig: RegisterDevstackDevWalletConfig,\n): Promise<RegisterDevstackDevWalletResult> {\n\tconst g = globalThis as {\n\t\t__devstackDevWallet__?: RegisterDevstackDevWalletResult;\n\t\t__devstackDevWalletPromise__?: Promise<RegisterDevstackDevWalletResult>;\n\t};\n\t// Fast path: a prior call already completed.\n\tif (g.__devstackDevWallet__ !== undefined) return Promise.resolve(g.__devstackDevWallet__);\n\t// In-flight path: a prior call is still booting — share its promise. This\n\t// guard runs before any `await`, so it closes the double-init window that a\n\t// post-registration-only marker leaves open under HMR / double import.\n\tif (g.__devstackDevWalletPromise__ !== undefined) return g.__devstackDevWalletPromise__;\n\n\tconst promise = registerDevstackDevWalletImpl(config);\n\tg.__devstackDevWalletPromise__ = promise;\n\t// On failure, clear the in-flight promise so a later call can retry instead\n\t// of being stuck awaiting a rejected registration.\n\tpromise.catch(() => {\n\t\tif (g.__devstackDevWalletPromise__ === promise) {\n\t\t\tg.__devstackDevWalletPromise__ = undefined;\n\t\t}\n\t});\n\treturn promise;\n}\n\nasync function registerDevstackDevWalletImpl(\n\tconfig: RegisterDevstackDevWalletConfig,\n): Promise<RegisterDevstackDevWalletResult> {\n\tconst { mountUI = true, autoApprove = false } = config;\n\tglobalThis.__DEV_WALLET_INJECTED__ = true;\n\tassertPageOriginAllowed(config.allowedOrigins);\n\n\t// Resolve the FULL network set up front so it can be advertised both on\n\t// the wallet (its `chains`) and on every devstack account (per-account\n\t// `chains`) — fork/custom networks must be signable from both surfaces.\n\tconst networkInfos: Readonly<Record<string, DevstackNetworkInfo>> =\n\t\tconfig.networks !== undefined && Object.keys(config.networks).length > 0\n\t\t\t? config.networks\n\t\t\t: { localnet: { rpc: 'http://127.0.0.1:9000' } };\n\tconst networkNames = Object.keys(networkInfos);\n\n\tconst adapter = new DevstackSignerAdapter({\n\t\tserverOrigin: config.serverOrigin,\n\t\ttoken: config.token ?? null,\n\t\tname: config.name ?? 'Devstack',\n\t\tnetworks: networkNames,\n\t});\n\n\t// The wallet EXECUTES (and simulates) `signAndExecuteTransaction` with\n\t// its OWN client — adapter signing is HTTP (server-side keys), but\n\t// execution goes through the wallet's network client. That client MUST\n\t// hit the same routed RPC the app's dApp Kit uses (a raw 127.0.0.1 RPC\n\t// is CORS-blocked from the routed page origin).\n\t//\n\t// Expose the FULL network set the app supports (local + any live networks\n\t// from the deployment envelope) so the wallet operates on whichever\n\t// network dApp Kit has selected. The wallet is registered ONCE via\n\t// wallet-standard and advertises each key as the wallet-standard chain\n\t// `sui:<network>` (matching the `sui:<network>` dApp Kit forwards for\n\t// signing — the `sui:` prefix lives only at the wallet-standard boundary);\n\t// a UI `switchNetwork` just changes the active chain, it never\n\t// re-registers. The wallet does NOT know live-vs-local — both flow through\n\t// the same map. Omitting `networks` falls back to a single localnet entry\n\t// (the legacy non-routed default — resolved as `networkInfos` above).\n\tconst networks: Record<string, string> = {};\n\tconst faucets: Record<string, string> = {};\n\tfor (const [net, info] of Object.entries(networkInfos)) {\n\t\tnetworks[net] = info.rpc;\n\t\tif (info.faucet !== undefined && info.faucet !== null && info.faucet.length > 0) {\n\t\t\tfaucets[net] = info.faucet;\n\t\t}\n\t}\n\t// The wallet opens on the requested default (the dApp-Kit default network),\n\t// falling back to the first declared network.\n\tconst activeNetwork =\n\t\tconfig.defaultNetwork !== undefined && networks[config.defaultNetwork] !== undefined\n\t\t\t? config.defaultNetwork\n\t\t\t: (Object.keys(networks)[0] ?? 'localnet');\n\n\t// Delegate the construct → init-adapter → mount-UI → register → dispose\n\t// sequence to the shared dev-wallet core helper. The `DevstackSignerAdapter`\n\t// brings the stack's server-resolved accounts (alice/bob/carol — headless\n\t// HTTP signing) and is always present. The optional `WebCryptoSignerAdapter`\n\t// lets the user create their OWN accounts in the wallet UI, persisted in\n\t// IndexedDB across reloads (non-extractable WebCrypto keys, NOT in-memory) —\n\t// but it depends on the OPTIONAL `@mysten/signers` peer. Load it dynamically\n\t// and gate on its presence: an app that doesn't install `@mysten/signers`\n\t// still gets a working dev wallet (just without create-your-own-account)\n\t// instead of a hard inject crash on the unresolved peer. `createInitialAccount`\n\t// stays off — the devstack adapter already supplies accounts, so we never\n\t// fabricate a throwaway key; the WebCrypto adapter starts empty until the\n\t// user adds one.\n\tconst adapters: SignerAdapter[] = [adapter];\n\ttry {\n\t\tconst { WebCryptoSignerAdapter } = await import('../adapters/webcrypto-adapter.js');\n\t\tadapters.push(new WebCryptoSignerAdapter());\n\t} catch (error) {\n\t\tconsole.info(\n\t\t\t'[dev-wallet] @mysten/signers is not installed — WebCrypto account creation is ' +\n\t\t\t\t'disabled. Install @mysten/signers to enable creating your own accounts in the ' +\n\t\t\t\t'dev wallet.',\n\t\t\terror,\n\t\t);\n\t}\n\n\tconst { wallet, dispose: disposeWallet } = await mountAndRegisterDevWallet({\n\t\tadapters,\n\t\tname: config.name ?? 'Devstack',\n\t\tautoApprove,\n\t\t// Auto-approve `standard:connect` whenever signing is auto-approved — both\n\t\t// are the headless-e2e signal (`DEVSTACK_AUTO_APPROVE`). The test bridge's\n\t\t// explicit `connectWallet(...)` invokes the wallet's connect; without this\n\t\t// it would queue a pending request and block on UI approval that never\n\t\t// comes. In normal dev (no auto-approve) a human approves the connect.\n\t\tautoConnect: Boolean(autoApprove),\n\t\tnetworks,\n\t\tfaucets,\n\t\tactiveNetwork,\n\t\tmountUI,\n\t});\n\n\tconst result: RegisterDevstackDevWalletResult = {\n\t\twallet,\n\t\tdispose() {\n\t\t\tdisposeWallet();\n\t\t\tglobalThis.__DEV_WALLET_INJECTED__ = false;\n\t\t\tdelete (globalThis as { __devstackDevWallet__?: unknown }).__devstackDevWallet__;\n\t\t\t// Clear the in-flight/settled registration promise too, so a\n\t\t\t// subsequent `registerDevstackDevWallet` re-initializes rather than\n\t\t\t// handing back the disposed instance.\n\t\t\tdelete (globalThis as { __devstackDevWalletPromise__?: unknown })\n\t\t\t\t.__devstackDevWalletPromise__;\n\t\t},\n\t};\n\t(\n\t\tglobalThis as { __devstackDevWallet__?: RegisterDevstackDevWalletResult }\n\t).__devstackDevWallet__ = result;\n\treturn result;\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
package/src/inject/index.ts
CHANGED
|
@@ -52,6 +52,10 @@ export interface DevstackNetworkInfo {
|
|
|
52
52
|
export interface RegisterDevstackDevWalletConfig {
|
|
53
53
|
/** Wallet-app origin (the dev-wallet connection's `walletUrl`). */
|
|
54
54
|
readonly serverOrigin: string;
|
|
55
|
+
/** Browser page origins accepted by the devstack wallet server. When the
|
|
56
|
+
* current page is not in this list, the HTTP adapter will be CORS-blocked;
|
|
57
|
+
* this is non-secret metadata used only for a local diagnostic. */
|
|
58
|
+
readonly allowedOrigins?: ReadonlyArray<string>;
|
|
55
59
|
/** Bearer pairing token (read by the Vite dev server from the wallet's
|
|
56
60
|
* `0o600` side-channel token file), or null. */
|
|
57
61
|
readonly token?: string | null;
|
|
@@ -97,6 +101,56 @@ declare global {
|
|
|
97
101
|
var __devstackDevWalletPromise__: Promise<RegisterDevstackDevWalletResult> | undefined;
|
|
98
102
|
}
|
|
99
103
|
|
|
104
|
+
const currentPageOrigin = (): string | null => {
|
|
105
|
+
const location = (globalThis as { location?: { origin?: unknown } }).location;
|
|
106
|
+
return typeof location?.origin === 'string' && location.origin.length > 0
|
|
107
|
+
? location.origin
|
|
108
|
+
: null;
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const originMatchesAllowed = (origin: string, allowed: string): boolean => {
|
|
112
|
+
if (!allowed.includes('*')) return origin === allowed;
|
|
113
|
+
let originUrl: URL;
|
|
114
|
+
let patternUrl: URL;
|
|
115
|
+
try {
|
|
116
|
+
originUrl = new URL(origin);
|
|
117
|
+
patternUrl = new URL(allowed.replace('*', 'devstack-origin-probe'));
|
|
118
|
+
} catch {
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
if (originUrl.origin !== origin) return false;
|
|
122
|
+
if (originUrl.protocol !== patternUrl.protocol || originUrl.port !== patternUrl.port) {
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
const originLabels = originUrl.hostname.toLowerCase().split('.');
|
|
126
|
+
const patternLabels = patternUrl.hostname.toLowerCase().split('.');
|
|
127
|
+
return (
|
|
128
|
+
originLabels.length === patternLabels.length &&
|
|
129
|
+
patternLabels[0] === 'devstack-origin-probe' &&
|
|
130
|
+
originLabels[0]!.length > 0 &&
|
|
131
|
+
originLabels.slice(1).join('.') === patternLabels.slice(1).join('.')
|
|
132
|
+
);
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
const assertPageOriginAllowed = (allowedOrigins: ReadonlyArray<string> | undefined): void => {
|
|
136
|
+
if (allowedOrigins === undefined || allowedOrigins.length === 0) return;
|
|
137
|
+
const origin = currentPageOrigin();
|
|
138
|
+
if (origin === null || origin === 'null') return;
|
|
139
|
+
if (allowedOrigins.some((allowed) => originMatchesAllowed(origin, allowed))) return;
|
|
140
|
+
|
|
141
|
+
const recommended = allowedOrigins.find((allowed) => !allowed.includes('*')) ?? allowedOrigins[0];
|
|
142
|
+
const allowedList = allowedOrigins.join(', ');
|
|
143
|
+
const guidance =
|
|
144
|
+
recommended === undefined
|
|
145
|
+
? ''
|
|
146
|
+
: ` Open ${recommended}, or add wallet({ allowedOrigins: ['${origin}'] }) if this origin is intentional.`;
|
|
147
|
+
const message =
|
|
148
|
+
`[devstack] dev wallet is not available from page origin ${origin}. ` +
|
|
149
|
+
`Allowed origins: ${allowedList}.${guidance}`;
|
|
150
|
+
console.error(message);
|
|
151
|
+
throw new Error(`devstack dev wallet page origin ${origin} is not allowlisted`);
|
|
152
|
+
};
|
|
153
|
+
|
|
100
154
|
/**
|
|
101
155
|
* Construct + register the devstack dev wallet on the current page and wire
|
|
102
156
|
* the Playwright `connectAs` slot. Idempotent: a second call is a no-op and
|
|
@@ -138,6 +192,7 @@ async function registerDevstackDevWalletImpl(
|
|
|
138
192
|
): Promise<RegisterDevstackDevWalletResult> {
|
|
139
193
|
const { mountUI = true, autoApprove = false } = config;
|
|
140
194
|
globalThis.__DEV_WALLET_INJECTED__ = true;
|
|
195
|
+
assertPageOriginAllowed(config.allowedOrigins);
|
|
141
196
|
|
|
142
197
|
// Resolve the FULL network set up front so it can be advertised both on
|
|
143
198
|
// the wallet (its `chains`) and on every devstack account (per-account
|