@cef-ai/wallet 1.0.0
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/README.md +79 -0
- package/dist/index.cjs +597 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +452 -0
- package/dist/index.d.ts +452 -0
- package/dist/index.js +589 -0
- package/dist/index.js.map +1 -0
- package/package.json +55 -0
package/README.md
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# @cef-ai/wallet
|
|
2
|
+
|
|
3
|
+
Embed SCP Wallet into a host application. Passkey-native authentication
|
|
4
|
+
via WebAuthn + PRF. No MetaMask, no seed phrases, no popup-blocker headaches.
|
|
5
|
+
|
|
6
|
+
For runtime topology and the popup/cross-tab channels this SDK speaks,
|
|
7
|
+
see [`ARCHITECTURE.md`](../../ARCHITECTURE.md) in the repo root.
|
|
8
|
+
|
|
9
|
+
> **Status:** v2.0.0-alpha. Public API surface is stable but pre-1.0.
|
|
10
|
+
> `private: true` in `package.json` — not yet published to npm.
|
|
11
|
+
|
|
12
|
+
## Install
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
npm install @cef-ai/wallet
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## TODO(publish) — must be addressed before flipping `private: false`
|
|
19
|
+
|
|
20
|
+
1. **Workspace dep versions.** `@cef-ai/wallet-api-client`, `@cef-ai/wallet-identity`,
|
|
21
|
+
`@cef-ai/wallet-signers` are pinned at `0.0.0` (workspace placeholder). Choose:
|
|
22
|
+
|
|
23
|
+
- **Option A**: publish those three packages with real semver versions first,
|
|
24
|
+
then bump this package's dependencies to match.
|
|
25
|
+
- **Option B**: drop `external: [/^@cef-ai\/wallet/]` from `tsup.config.ts` so
|
|
26
|
+
the SDK bundle inlines them. Smaller dep tree for consumers; larger SDK.
|
|
27
|
+
|
|
28
|
+
2. **Downstream type resolution.** Even after Option A above, the workspace
|
|
29
|
+
packages must publish their own `dist/` with `.d.ts` files. The current
|
|
30
|
+
workspace `package.json` files point `types` at `./src` (source). For publish
|
|
31
|
+
they'll need their own tsup configs and matching publishConfig.
|
|
32
|
+
|
|
33
|
+
3. **Pack validation.** Add a `pack-dry-run` CI step that runs
|
|
34
|
+
`npm pack --dry-run --json` and asserts `dist/index.js`, `dist/index.cjs`,
|
|
35
|
+
`dist/index.d.ts`, `dist/index.d.cts` are all present and no source `.ts`
|
|
36
|
+
files leak through.
|
|
37
|
+
|
|
38
|
+
## Quick start
|
|
39
|
+
|
|
40
|
+
```ts
|
|
41
|
+
import { EmbedWallet } from '@cef-ai/wallet';
|
|
42
|
+
|
|
43
|
+
const wallet = new EmbedWallet({
|
|
44
|
+
appId: 'my-app',
|
|
45
|
+
appName: 'My App',
|
|
46
|
+
walletOrigin: 'https://wallet.example.com', // required: your wallet deployment origin
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
// First-time users register a passkey; returning users call login().
|
|
50
|
+
const session = await wallet.login();
|
|
51
|
+
console.log('Cere address:', session.addresses.cere);
|
|
52
|
+
|
|
53
|
+
// Signing is per-chain. Pick a signer for the chain you need, then call its
|
|
54
|
+
// `signMessage(bytes)` (returns raw signature bytes).
|
|
55
|
+
const signer = wallet.getSigner({ chain: 'cere' });
|
|
56
|
+
const sig = await signer.signMessage(new TextEncoder().encode('Hello'));
|
|
57
|
+
console.log('Signature bytes:', sig);
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Architecture
|
|
61
|
+
|
|
62
|
+
- A popup-window-based transport (`PopupTransport`) opens `wallet.example.com/embed/wallet?origin=...`
|
|
63
|
+
- The popup runs the wallet UI, performs the WebAuthn ceremony, and posts results back
|
|
64
|
+
- Cross-tab session sharing via `BroadcastChannel('scp-wallet-v2')` so a second host tab discovers an existing session
|
|
65
|
+
|
|
66
|
+
Design specs and ADRs are maintained in the company memory bank.
|
|
67
|
+
|
|
68
|
+
## Capabilities
|
|
69
|
+
|
|
70
|
+
- `register({ label? })` / `login()` — opens the popup, performs the WebAuthn ceremony, returns the session
|
|
71
|
+
- `getSigner({ chain })` — returns an `EthersSigner` (`'evm'`), `PolkadotSigner` (`'cere'`), or `SolanaSigner` (`'solana'`); each exposes `signMessage(bytes: Uint8Array): Promise<Uint8Array>`
|
|
72
|
+
- `requestDelegation({ capabilities, ttl, appId?, agentPubkey?, constraints? })` — requests a scoped delegation token; the popup shows a consent UI
|
|
73
|
+
- `saveApplication(record)` / `findApplications(filter)` — manage app context records
|
|
74
|
+
- `logout()` — closes the session locally + broadcasts so other tabs do the same
|
|
75
|
+
- `dispose()` — tear-down hook; closes any open popup and removes listeners
|
|
76
|
+
|
|
77
|
+
## License
|
|
78
|
+
|
|
79
|
+
MIT.
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,597 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var walletApiClient = require('@cef-ai/wallet-api-client');
|
|
4
|
+
var walletIdentity = require('@cef-ai/wallet-identity');
|
|
5
|
+
var walletSigners = require('@cef-ai/wallet-signers');
|
|
6
|
+
|
|
7
|
+
var __defProp = Object.defineProperty;
|
|
8
|
+
var __getOwnPropSymbols = Object.getOwnPropertySymbols;
|
|
9
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
10
|
+
var __propIsEnum = Object.prototype.propertyIsEnumerable;
|
|
11
|
+
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
12
|
+
var __spreadValues = (a, b) => {
|
|
13
|
+
for (var prop in b || (b = {}))
|
|
14
|
+
if (__hasOwnProp.call(b, prop))
|
|
15
|
+
__defNormalProp(a, prop, b[prop]);
|
|
16
|
+
if (__getOwnPropSymbols)
|
|
17
|
+
for (var prop of __getOwnPropSymbols(b)) {
|
|
18
|
+
if (__propIsEnum.call(b, prop))
|
|
19
|
+
__defNormalProp(a, prop, b[prop]);
|
|
20
|
+
}
|
|
21
|
+
return a;
|
|
22
|
+
};
|
|
23
|
+
var __async = (__this, __arguments, generator) => {
|
|
24
|
+
return new Promise((resolve, reject) => {
|
|
25
|
+
var fulfilled = (value) => {
|
|
26
|
+
try {
|
|
27
|
+
step(generator.next(value));
|
|
28
|
+
} catch (e) {
|
|
29
|
+
reject(e);
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
var rejected = (value) => {
|
|
33
|
+
try {
|
|
34
|
+
step(generator.throw(value));
|
|
35
|
+
} catch (e) {
|
|
36
|
+
reject(e);
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
var step = (x) => x.done ? resolve(x.value) : Promise.resolve(x.value).then(fulfilled, rejected);
|
|
40
|
+
step((generator = generator.apply(__this, __arguments)).next());
|
|
41
|
+
});
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
// src/transport/origin.ts
|
|
45
|
+
var ALLOWED_ORIGIN_SCHEMES = ["https:", "http:"];
|
|
46
|
+
function isMatchingOrigin(a, b) {
|
|
47
|
+
if (!a || !b) return false;
|
|
48
|
+
try {
|
|
49
|
+
const ua = new URL(a);
|
|
50
|
+
const ub = new URL(b);
|
|
51
|
+
return ua.origin === ub.origin;
|
|
52
|
+
} catch (e) {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// src/transport/PopupTransport.ts
|
|
58
|
+
var DEFAULT_FEATURES = "width=420,height=640,resizable=yes,scrollbars=yes";
|
|
59
|
+
var PopupTransport = class {
|
|
60
|
+
constructor(options) {
|
|
61
|
+
this.popup = null;
|
|
62
|
+
this.pending = /* @__PURE__ */ new Map();
|
|
63
|
+
this.listener = null;
|
|
64
|
+
this.popupReady = false;
|
|
65
|
+
this.outbox = [];
|
|
66
|
+
this.opts = __spreadValues({
|
|
67
|
+
openPopup: defaultOpener,
|
|
68
|
+
hostWindow: globalThis.window,
|
|
69
|
+
defaultTimeoutMs: 6e4
|
|
70
|
+
}, options);
|
|
71
|
+
}
|
|
72
|
+
request(_0, _1) {
|
|
73
|
+
return __async(this, arguments, function* (message, expectedTypes, init = {}) {
|
|
74
|
+
var _a, _b, _c;
|
|
75
|
+
if (!this.popup || this.popup.closed) {
|
|
76
|
+
const hostOrigin = (_b = (_a = globalThis.location) == null ? void 0 : _a.origin) != null ? _b : "";
|
|
77
|
+
const url = `${this.opts.walletOrigin}/embed/wallet?origin=${encodeURIComponent(hostOrigin)}`;
|
|
78
|
+
this.popup = this.opts.openPopup(url, DEFAULT_FEATURES);
|
|
79
|
+
if (!this.popup) {
|
|
80
|
+
throw new walletApiClient.WalletError("popup-blocked", "popup window failed to open");
|
|
81
|
+
}
|
|
82
|
+
this.popupReady = false;
|
|
83
|
+
this.outbox = [];
|
|
84
|
+
this.ensureListening();
|
|
85
|
+
}
|
|
86
|
+
const id = message.id;
|
|
87
|
+
const timeoutMs = (_c = init.timeoutMs) != null ? _c : this.opts.defaultTimeoutMs;
|
|
88
|
+
return new Promise((resolve, reject) => {
|
|
89
|
+
const timer = setTimeout(() => {
|
|
90
|
+
if (this.pending.has(id)) {
|
|
91
|
+
this.pending.delete(id);
|
|
92
|
+
this.outbox = this.outbox.filter((m) => m.id !== id);
|
|
93
|
+
reject(new walletApiClient.WalletError("internal", `request "${message.type}" (id=${id}) timed out after ${timeoutMs}ms`));
|
|
94
|
+
}
|
|
95
|
+
}, timeoutMs);
|
|
96
|
+
this.pending.set(id, {
|
|
97
|
+
expectedTypes,
|
|
98
|
+
resolve: (msg) => resolve(msg),
|
|
99
|
+
reject,
|
|
100
|
+
timer
|
|
101
|
+
});
|
|
102
|
+
if (this.popupReady) {
|
|
103
|
+
this.popup.postMessage(message, this.opts.walletOrigin);
|
|
104
|
+
} else {
|
|
105
|
+
this.outbox.push(message);
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
/** Close the popup if open. Pending requests will time out normally. */
|
|
111
|
+
close() {
|
|
112
|
+
if (this.popup && !this.popup.closed) {
|
|
113
|
+
this.popup.close();
|
|
114
|
+
}
|
|
115
|
+
this.popup = null;
|
|
116
|
+
this.popupReady = false;
|
|
117
|
+
this.outbox = [];
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Tear down the transport entirely. Closes the popup AND removes the host-
|
|
121
|
+
* window message listener. After `destroy()`, the transport instance cannot
|
|
122
|
+
* be reused — callers should drop the reference.
|
|
123
|
+
*
|
|
124
|
+
* Use `close()` if you only want to close the popup but keep the transport
|
|
125
|
+
* alive for a later re-open.
|
|
126
|
+
*/
|
|
127
|
+
destroy() {
|
|
128
|
+
this.close();
|
|
129
|
+
if (this.listener) {
|
|
130
|
+
this.opts.hostWindow.removeEventListener("message", this.listener);
|
|
131
|
+
this.listener = null;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
// ---- internal -------------------------------------------------------------
|
|
135
|
+
ensureListening() {
|
|
136
|
+
if (this.listener) return;
|
|
137
|
+
this.listener = (event) => this.onMessage(event);
|
|
138
|
+
this.opts.hostWindow.addEventListener("message", this.listener);
|
|
139
|
+
}
|
|
140
|
+
onMessage(event) {
|
|
141
|
+
if (!isMatchingOrigin(event.origin, this.opts.walletOrigin)) {
|
|
142
|
+
const data2 = event.data;
|
|
143
|
+
if (data2 && typeof data2.id === "string" && this.pending.has(data2.id)) {
|
|
144
|
+
const pending2 = this.pending.get(data2.id);
|
|
145
|
+
this.pending.delete(data2.id);
|
|
146
|
+
clearTimeout(pending2.timer);
|
|
147
|
+
pending2.reject(
|
|
148
|
+
new walletApiClient.WalletError(
|
|
149
|
+
"origin-mismatch",
|
|
150
|
+
`message from "${event.origin}" rejected (expected "${this.opts.walletOrigin}")`
|
|
151
|
+
)
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
const data = event.data;
|
|
157
|
+
if (!data || typeof data.type !== "string" || typeof data.id !== "string") return;
|
|
158
|
+
if (data.type === "wallet:ready") {
|
|
159
|
+
if (this.popupReady) return;
|
|
160
|
+
this.popupReady = true;
|
|
161
|
+
const toFlush = this.outbox;
|
|
162
|
+
this.outbox = [];
|
|
163
|
+
if (this.popup && !this.popup.closed) {
|
|
164
|
+
for (const m of toFlush) {
|
|
165
|
+
this.popup.postMessage(m, this.opts.walletOrigin);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
const pending = this.pending.get(data.id);
|
|
171
|
+
if (!pending) return;
|
|
172
|
+
clearTimeout(pending.timer);
|
|
173
|
+
this.pending.delete(data.id);
|
|
174
|
+
if (data.type === "wallet:error") {
|
|
175
|
+
pending.reject(new walletApiClient.WalletError(data.code, data.message, data.traceId));
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
if (!pending.expectedTypes.includes(data.type)) {
|
|
179
|
+
pending.reject(
|
|
180
|
+
new walletApiClient.WalletError("internal", `received unexpected response type "${data.type}" for id "${data.id}"`)
|
|
181
|
+
);
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
pending.resolve(data);
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
function defaultOpener(url, features) {
|
|
188
|
+
const w = globalThis.window;
|
|
189
|
+
const opened = w == null ? void 0 : w.open(url, "_blank", features);
|
|
190
|
+
return opened;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// src/EmbedWallet.ts
|
|
194
|
+
var EmbedWallet = class {
|
|
195
|
+
constructor(opts) {
|
|
196
|
+
this._addresses = null;
|
|
197
|
+
this._credentialId = null;
|
|
198
|
+
this._listeners = {};
|
|
199
|
+
var _a, _b, _c, _d, _e, _f, _g;
|
|
200
|
+
const injectedTransport = (_a = opts.__internal__) == null ? void 0 : _a.transport;
|
|
201
|
+
if (!injectedTransport && !opts.walletOrigin) {
|
|
202
|
+
throw new Error("EmbedWallet: `walletOrigin` is required (e.g. https://wallet.example.com).");
|
|
203
|
+
}
|
|
204
|
+
this.appId = opts.appId;
|
|
205
|
+
this.walletOrigin = (_b = opts.walletOrigin) != null ? _b : "";
|
|
206
|
+
this.popup = { width: (_d = (_c = opts.popup) == null ? void 0 : _c.width) != null ? _d : 420, height: (_f = (_e = opts.popup) == null ? void 0 : _e.height) != null ? _f : 640 };
|
|
207
|
+
this.appName = (_g = opts.appName) != null ? _g : "";
|
|
208
|
+
this.transport = injectedTransport != null ? injectedTransport : new PopupTransport({
|
|
209
|
+
walletOrigin: this.walletOrigin
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
get isLoggedIn() {
|
|
213
|
+
return this._addresses !== null;
|
|
214
|
+
}
|
|
215
|
+
get addresses() {
|
|
216
|
+
return this._addresses;
|
|
217
|
+
}
|
|
218
|
+
get credentialId() {
|
|
219
|
+
return this._credentialId;
|
|
220
|
+
}
|
|
221
|
+
register() {
|
|
222
|
+
return __async(this, arguments, function* (opts = {}) {
|
|
223
|
+
return this.helloAndAwaitLogin("register", opts);
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
login() {
|
|
227
|
+
return __async(this, null, function* () {
|
|
228
|
+
return this.helloAndAwaitLogin("login");
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
logout() {
|
|
232
|
+
return __async(this, null, function* () {
|
|
233
|
+
if (!this._addresses) {
|
|
234
|
+
this.transport.close();
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
const id = crypto.randomUUID();
|
|
238
|
+
try {
|
|
239
|
+
yield this.transport.request({ type: "wallet:logout", id }, [
|
|
240
|
+
"wallet:result"
|
|
241
|
+
]);
|
|
242
|
+
} catch (_err) {
|
|
243
|
+
}
|
|
244
|
+
this._addresses = null;
|
|
245
|
+
this._credentialId = null;
|
|
246
|
+
this.transport.close();
|
|
247
|
+
this.emit("logout", { type: "logout" });
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
getSigner(spec) {
|
|
251
|
+
this.assertLoggedIn("getSigner");
|
|
252
|
+
const sign = (chain, payload) => __async(this, null, function* () {
|
|
253
|
+
const id = crypto.randomUUID();
|
|
254
|
+
const result = yield this.transport.request(
|
|
255
|
+
{ type: "wallet:sign", id, chain, payload, requestKind: "sdk" },
|
|
256
|
+
["wallet:result"]
|
|
257
|
+
);
|
|
258
|
+
const sig = result.result;
|
|
259
|
+
if (!(sig instanceof Uint8Array)) {
|
|
260
|
+
throw new walletApiClient.WalletError("internal", `popup returned non-Uint8Array sign result (got ${typeof sig})`);
|
|
261
|
+
}
|
|
262
|
+
return sig;
|
|
263
|
+
});
|
|
264
|
+
const addresses = this._addresses;
|
|
265
|
+
switch (spec.chain) {
|
|
266
|
+
case "evm":
|
|
267
|
+
return new walletSigners.EthersSigner({ address: addresses.evm, sign });
|
|
268
|
+
case "cere":
|
|
269
|
+
return new walletSigners.PolkadotSigner({ address: addresses.cere, sign });
|
|
270
|
+
case "solana":
|
|
271
|
+
return new walletSigners.SolanaSigner({ address: addresses.solana, sign });
|
|
272
|
+
default:
|
|
273
|
+
throw new walletApiClient.WalletError("validation", `getSigner: unknown chain "${String(spec.chain)}"`);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
/**
|
|
277
|
+
* Expose the wallet as a chain-free `@cef-ai/signer` over the Cere
|
|
278
|
+
* Ed25519 key — the pluggable-signer seam `@cef-ai/account` consumes for DDC
|
|
279
|
+
* and `@cef-ai/chain` adapts for extrinsics.
|
|
280
|
+
*
|
|
281
|
+
* `publicKey` is recovered from the (public) Solana address — no key material
|
|
282
|
+
* leaves the popup's `SessionVault`. `sign(bytes, 'extrinsic', { humanReadable })`
|
|
283
|
+
* routes through the popup's extrinsic-consent screen.
|
|
284
|
+
*/
|
|
285
|
+
asSigner() {
|
|
286
|
+
this.assertLoggedIn("asSigner");
|
|
287
|
+
const addresses = this._addresses;
|
|
288
|
+
const send = (payload, intent, humanReadable) => __async(this, null, function* () {
|
|
289
|
+
if (!this.isLoggedIn) {
|
|
290
|
+
throw new walletApiClient.WalletError("unauthorized", "asSigner: wallet is no longer logged in");
|
|
291
|
+
}
|
|
292
|
+
const id = crypto.randomUUID();
|
|
293
|
+
const result = yield this.transport.request(
|
|
294
|
+
{ type: "wallet:sign", id, chain: "cere", payload, requestKind: intent != null ? intent : "sdk", humanReadable },
|
|
295
|
+
["wallet:result"]
|
|
296
|
+
);
|
|
297
|
+
const sig = result.result;
|
|
298
|
+
if (!(sig instanceof Uint8Array)) {
|
|
299
|
+
throw new walletApiClient.WalletError("internal", `popup returned non-Uint8Array sign result (got ${typeof sig})`);
|
|
300
|
+
}
|
|
301
|
+
return sig;
|
|
302
|
+
});
|
|
303
|
+
return {
|
|
304
|
+
type: "ed25519",
|
|
305
|
+
address: addresses.cere,
|
|
306
|
+
publicKey: walletIdentity.decodeEd25519Pubkey(addresses.solana),
|
|
307
|
+
isReady: () => __async(this, null, function* () {
|
|
308
|
+
return this.isLoggedIn;
|
|
309
|
+
}),
|
|
310
|
+
sign: (bytes, intent, opts) => send(bytes, intent, opts == null ? void 0 : opts.humanReadable)
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
requestDelegation(req) {
|
|
314
|
+
return __async(this, null, function* () {
|
|
315
|
+
this.assertLoggedIn("requestDelegation");
|
|
316
|
+
const id = crypto.randomUUID();
|
|
317
|
+
const scope = {
|
|
318
|
+
capabilities: req.capabilities,
|
|
319
|
+
appId: req.appId,
|
|
320
|
+
agentPubkey: req.agentPubkey,
|
|
321
|
+
constraints: req.constraints
|
|
322
|
+
};
|
|
323
|
+
const result = yield this.transport.request(
|
|
324
|
+
{ type: "wallet:requestDelegation", id, scope, ttl: req.ttl },
|
|
325
|
+
["wallet:result"]
|
|
326
|
+
);
|
|
327
|
+
return result.result;
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
findApplications() {
|
|
331
|
+
return __async(this, arguments, function* (filter = {}) {
|
|
332
|
+
this.assertLoggedIn("findApplications");
|
|
333
|
+
const id = crypto.randomUUID();
|
|
334
|
+
const result = yield this.transport.request(
|
|
335
|
+
{ type: "wallet:findApplications", id, appId: filter.appId },
|
|
336
|
+
["wallet:result"]
|
|
337
|
+
);
|
|
338
|
+
return result.result;
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
saveApplication(body) {
|
|
342
|
+
return __async(this, null, function* () {
|
|
343
|
+
this.assertLoggedIn("saveApplication");
|
|
344
|
+
const id = crypto.randomUUID();
|
|
345
|
+
const result = yield this.transport.request(
|
|
346
|
+
{ type: "wallet:saveApplication", id, permissions: body.permissions, email: body.email },
|
|
347
|
+
["wallet:result"]
|
|
348
|
+
);
|
|
349
|
+
return result.result;
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
on(type, listener) {
|
|
353
|
+
var _a;
|
|
354
|
+
const map = this._listeners;
|
|
355
|
+
((_a = map[type]) != null ? _a : map[type] = /* @__PURE__ */ new Set()).add(listener);
|
|
356
|
+
return this;
|
|
357
|
+
}
|
|
358
|
+
off(type, listener) {
|
|
359
|
+
var _a;
|
|
360
|
+
const map = this._listeners;
|
|
361
|
+
(_a = map[type]) == null ? void 0 : _a.delete(listener);
|
|
362
|
+
return this;
|
|
363
|
+
}
|
|
364
|
+
/**
|
|
365
|
+
* Tear down the wallet instance. Closes any open popup, removes transport
|
|
366
|
+
* listeners, clears in-memory state. Call from your app's unmount hook.
|
|
367
|
+
*
|
|
368
|
+
* After dispose(), this instance is not reusable — construct a new one.
|
|
369
|
+
*/
|
|
370
|
+
dispose() {
|
|
371
|
+
this._addresses = null;
|
|
372
|
+
this._credentialId = null;
|
|
373
|
+
this._listeners = {};
|
|
374
|
+
this.transport.destroy();
|
|
375
|
+
}
|
|
376
|
+
// ---- private --------------------------------------------------------------
|
|
377
|
+
appContext() {
|
|
378
|
+
const origin = typeof window !== "undefined" ? window.location.origin : "";
|
|
379
|
+
return { appId: this.appId, name: this.appName, origin };
|
|
380
|
+
}
|
|
381
|
+
helloAndAwaitLogin(_0) {
|
|
382
|
+
return __async(this, arguments, function* (intent, opts = {}) {
|
|
383
|
+
const id = crypto.randomUUID();
|
|
384
|
+
const helloMessage = {
|
|
385
|
+
type: "wallet:hello",
|
|
386
|
+
id,
|
|
387
|
+
appContext: this.appContext(),
|
|
388
|
+
intent,
|
|
389
|
+
label: opts.label
|
|
390
|
+
};
|
|
391
|
+
const result = yield this.transport.request(helloMessage, [
|
|
392
|
+
"wallet:login:ok"
|
|
393
|
+
]);
|
|
394
|
+
this._addresses = result.addresses;
|
|
395
|
+
this._credentialId = result.credentialId;
|
|
396
|
+
this.emit("login", { type: "login", addresses: result.addresses });
|
|
397
|
+
return { addresses: result.addresses, credentialId: result.credentialId };
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
emit(type, ev) {
|
|
401
|
+
const set = this._listeners[type];
|
|
402
|
+
if (!set) return;
|
|
403
|
+
for (const listener of set) {
|
|
404
|
+
try {
|
|
405
|
+
listener(ev);
|
|
406
|
+
} catch (e) {
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
assertLoggedIn(method) {
|
|
411
|
+
if (!this._addresses) {
|
|
412
|
+
throw new walletApiClient.WalletError("unauthorized", `${method}: not logged in`);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
};
|
|
416
|
+
|
|
417
|
+
// src/protocol.ts
|
|
418
|
+
var BROADCAST_CHANNEL_NAME = "scp-wallet-v2";
|
|
419
|
+
var PopupHostBridge = class {
|
|
420
|
+
constructor(options) {
|
|
421
|
+
this.handlers = {};
|
|
422
|
+
this.listener = null;
|
|
423
|
+
this.opts = __spreadValues({
|
|
424
|
+
popupWindow: globalThis.window
|
|
425
|
+
}, options);
|
|
426
|
+
}
|
|
427
|
+
/** Register a handler for a specific message type. Replaces any prior handler. */
|
|
428
|
+
on(type, handler) {
|
|
429
|
+
this.handlers[type] = handler;
|
|
430
|
+
return this;
|
|
431
|
+
}
|
|
432
|
+
/**
|
|
433
|
+
* Send a `PopupToHost` message back to the opener. The bridge does not
|
|
434
|
+
* remember the host-window reference itself — it reads `globalThis.opener`
|
|
435
|
+
* each call, so it survives popup re-opens.
|
|
436
|
+
*/
|
|
437
|
+
send(message) {
|
|
438
|
+
const opener = globalThis.opener;
|
|
439
|
+
if (!opener || typeof opener.postMessage !== "function") {
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
opener.postMessage(message, this.opts.hostOrigin);
|
|
443
|
+
}
|
|
444
|
+
/**
|
|
445
|
+
* Start listening for inbound messages.
|
|
446
|
+
*
|
|
447
|
+
* If `announceReady` is provided, sends a `wallet:ready` message immediately.
|
|
448
|
+
* Use this when the popup wants to signal "I'm here" without waiting for the
|
|
449
|
+
* host's first call.
|
|
450
|
+
*/
|
|
451
|
+
start(opts = {}) {
|
|
452
|
+
if (this.listener) return;
|
|
453
|
+
this.listener = (event) => this.onMessage(event);
|
|
454
|
+
this.opts.popupWindow.addEventListener("message", this.listener);
|
|
455
|
+
if (opts.announceReady) {
|
|
456
|
+
this.send({ type: "wallet:ready", id: opts.announceReady.id });
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
stop() {
|
|
460
|
+
if (!this.listener) return;
|
|
461
|
+
this.opts.popupWindow.removeEventListener("message", this.listener);
|
|
462
|
+
this.listener = null;
|
|
463
|
+
}
|
|
464
|
+
// ---- internal -------------------------------------------------------------
|
|
465
|
+
onMessage(event) {
|
|
466
|
+
return __async(this, null, function* () {
|
|
467
|
+
var _a;
|
|
468
|
+
if (!isMatchingOrigin(event.origin, this.opts.hostOrigin)) {
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
const data = event.data;
|
|
472
|
+
if (!data || typeof data.type !== "string" || typeof data.id !== "string") return;
|
|
473
|
+
const handler = this.handlers[data.type];
|
|
474
|
+
if (!handler) {
|
|
475
|
+
this.send({
|
|
476
|
+
type: "wallet:error",
|
|
477
|
+
id: data.id,
|
|
478
|
+
code: "internal",
|
|
479
|
+
message: `no handler registered for "${data.type}"`
|
|
480
|
+
});
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
483
|
+
try {
|
|
484
|
+
yield handler(data, { origin: event.origin });
|
|
485
|
+
} catch (err) {
|
|
486
|
+
if (err instanceof walletApiClient.WalletError) {
|
|
487
|
+
this.send({
|
|
488
|
+
type: "wallet:error",
|
|
489
|
+
id: data.id,
|
|
490
|
+
code: err.code,
|
|
491
|
+
message: (_a = err.detail) != null ? _a : "",
|
|
492
|
+
traceId: err.traceId
|
|
493
|
+
});
|
|
494
|
+
} else {
|
|
495
|
+
this.send({
|
|
496
|
+
type: "wallet:error",
|
|
497
|
+
id: data.id,
|
|
498
|
+
code: "internal",
|
|
499
|
+
message: err instanceof Error ? err.message : String(err)
|
|
500
|
+
});
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
});
|
|
504
|
+
}
|
|
505
|
+
};
|
|
506
|
+
|
|
507
|
+
// src/transport/BroadcastSessionShare.ts
|
|
508
|
+
var DEFAULT_REQUEST_TIMEOUT_MS = 200;
|
|
509
|
+
var BroadcastSessionShare = class {
|
|
510
|
+
constructor(opts) {
|
|
511
|
+
this.listening = false;
|
|
512
|
+
this.onMessage = (ev) => {
|
|
513
|
+
var _a, _b;
|
|
514
|
+
const msg = ev.data;
|
|
515
|
+
if (msg.type === "request-session") {
|
|
516
|
+
const session = this.opts.getSession();
|
|
517
|
+
if (!session || session.expMs <= Date.now()) return;
|
|
518
|
+
this.channel.postMessage({
|
|
519
|
+
type: "offer-session",
|
|
520
|
+
requestId: msg.requestId,
|
|
521
|
+
edSeed: session.edSeed,
|
|
522
|
+
secpKey: session.secpKey,
|
|
523
|
+
addresses: session.addresses,
|
|
524
|
+
expMs: session.expMs
|
|
525
|
+
});
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
528
|
+
if (msg.type === "logout") {
|
|
529
|
+
(_b = (_a = this.opts).onLogout) == null ? void 0 : _b.call(_a);
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
};
|
|
533
|
+
this.opts = opts;
|
|
534
|
+
this.channel = new BroadcastChannel(BROADCAST_CHANNEL_NAME);
|
|
535
|
+
}
|
|
536
|
+
/**
|
|
537
|
+
* Broadcast a `request-session` and wait for an `offer-session` from another
|
|
538
|
+
* tab. Resolves with the offered session, or null if nobody offered before
|
|
539
|
+
* the timeout.
|
|
540
|
+
*/
|
|
541
|
+
request() {
|
|
542
|
+
return __async(this, arguments, function* (opts = {}) {
|
|
543
|
+
var _a;
|
|
544
|
+
const requestId = crypto.randomUUID();
|
|
545
|
+
const timeoutMs = (_a = opts.timeoutMs) != null ? _a : DEFAULT_REQUEST_TIMEOUT_MS;
|
|
546
|
+
return new Promise((resolve) => {
|
|
547
|
+
const onMsg = (ev) => {
|
|
548
|
+
if (ev.data.type === "offer-session" && ev.data.requestId === requestId && ev.data.expMs > Date.now()) {
|
|
549
|
+
this.channel.removeEventListener("message", onMsg);
|
|
550
|
+
clearTimeout(timer);
|
|
551
|
+
resolve({
|
|
552
|
+
edSeed: ev.data.edSeed,
|
|
553
|
+
secpKey: ev.data.secpKey,
|
|
554
|
+
addresses: ev.data.addresses,
|
|
555
|
+
expMs: ev.data.expMs
|
|
556
|
+
});
|
|
557
|
+
}
|
|
558
|
+
};
|
|
559
|
+
this.channel.addEventListener("message", onMsg);
|
|
560
|
+
const timer = setTimeout(() => {
|
|
561
|
+
this.channel.removeEventListener("message", onMsg);
|
|
562
|
+
resolve(null);
|
|
563
|
+
}, timeoutMs);
|
|
564
|
+
this.channel.postMessage({ type: "request-session", requestId });
|
|
565
|
+
});
|
|
566
|
+
});
|
|
567
|
+
}
|
|
568
|
+
/** Begin offering this tab's session in response to requests, and handle logouts. */
|
|
569
|
+
start() {
|
|
570
|
+
if (this.listening) return;
|
|
571
|
+
this.listening = true;
|
|
572
|
+
this.channel.addEventListener("message", this.onMessage);
|
|
573
|
+
}
|
|
574
|
+
stop() {
|
|
575
|
+
this.channel.removeEventListener("message", this.onMessage);
|
|
576
|
+
this.listening = false;
|
|
577
|
+
}
|
|
578
|
+
/** Notify peers that the user logged out. Peers wipe their sessions. */
|
|
579
|
+
broadcastLogout() {
|
|
580
|
+
this.channel.postMessage({ type: "logout" });
|
|
581
|
+
}
|
|
582
|
+
/** Free the underlying channel handle. Call from unload handlers. */
|
|
583
|
+
close() {
|
|
584
|
+
this.stop();
|
|
585
|
+
this.channel.close();
|
|
586
|
+
}
|
|
587
|
+
};
|
|
588
|
+
|
|
589
|
+
exports.ALLOWED_ORIGIN_SCHEMES = ALLOWED_ORIGIN_SCHEMES;
|
|
590
|
+
exports.BROADCAST_CHANNEL_NAME = BROADCAST_CHANNEL_NAME;
|
|
591
|
+
exports.BroadcastSessionShare = BroadcastSessionShare;
|
|
592
|
+
exports.EmbedWallet = EmbedWallet;
|
|
593
|
+
exports.PopupHostBridge = PopupHostBridge;
|
|
594
|
+
exports.PopupTransport = PopupTransport;
|
|
595
|
+
exports.isMatchingOrigin = isMatchingOrigin;
|
|
596
|
+
//# sourceMappingURL=index.cjs.map
|
|
597
|
+
//# sourceMappingURL=index.cjs.map
|