phantasma-sdk-ts 0.9.2 → 0.10.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 +6 -0
- package/dist/cjs/index.js +3 -1
- package/dist/cjs/link/index.js +5 -0
- package/dist/cjs/link/v5/capabilities.js +6 -0
- package/dist/cjs/link/v5/client.js +324 -0
- package/dist/cjs/link/v5/deeplink.js +123 -0
- package/dist/cjs/link/v5/encoding.js +95 -0
- package/dist/cjs/link/v5/envelope.js +73 -0
- package/dist/cjs/link/v5/errors.js +60 -0
- package/dist/cjs/link/v5/index.js +33 -0
- package/dist/cjs/link/v5/loopback-transport.js +70 -0
- package/dist/cjs/link/v5/methods.js +4 -0
- package/dist/cjs/link/v5/pairing.js +95 -0
- package/dist/cjs/link/v5/protocol.js +61 -0
- package/dist/cjs/link/v5/relay-transport.js +303 -0
- package/dist/cjs/link/v5/session-crypto.js +120 -0
- package/dist/cjs/link/v5/transport.js +208 -0
- package/dist/cjs/link/v5/web-deeplink.js +141 -0
- package/dist/cjs/public.js +37 -1
- package/dist/esm/index.js +2 -0
- package/dist/esm/link/index.js +5 -0
- package/dist/esm/link/v5/capabilities.js +5 -0
- package/dist/esm/link/v5/client.js +320 -0
- package/dist/esm/link/v5/deeplink.js +115 -0
- package/dist/esm/link/v5/encoding.js +87 -0
- package/dist/esm/link/v5/envelope.js +65 -0
- package/dist/esm/link/v5/errors.js +56 -0
- package/dist/esm/link/v5/index.js +17 -0
- package/dist/esm/link/v5/loopback-transport.js +66 -0
- package/dist/esm/link/v5/methods.js +3 -0
- package/dist/esm/link/v5/pairing.js +91 -0
- package/dist/esm/link/v5/protocol.js +58 -0
- package/dist/esm/link/v5/relay-transport.js +299 -0
- package/dist/esm/link/v5/session-crypto.js +104 -0
- package/dist/esm/link/v5/transport.js +204 -0
- package/dist/esm/link/v5/web-deeplink.js +133 -0
- package/dist/esm/public.js +3 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/link/v5/capabilities.d.ts +80 -0
- package/dist/types/link/v5/capabilities.d.ts.map +1 -0
- package/dist/types/link/v5/client.d.ts +119 -0
- package/dist/types/link/v5/client.d.ts.map +1 -0
- package/dist/types/link/v5/deeplink.d.ts +52 -0
- package/dist/types/link/v5/deeplink.d.ts.map +1 -0
- package/dist/types/link/v5/encoding.d.ts +15 -0
- package/dist/types/link/v5/encoding.d.ts.map +1 -0
- package/dist/types/link/v5/envelope.d.ts +48 -0
- package/dist/types/link/v5/envelope.d.ts.map +1 -0
- package/dist/types/link/v5/errors.d.ts +39 -0
- package/dist/types/link/v5/errors.d.ts.map +1 -0
- package/dist/types/link/v5/index.d.ts +15 -0
- package/dist/types/link/v5/index.d.ts.map +1 -0
- package/dist/types/link/v5/loopback-transport.d.ts +43 -0
- package/dist/types/link/v5/loopback-transport.d.ts.map +1 -0
- package/dist/types/link/v5/methods.d.ts +83 -0
- package/dist/types/link/v5/methods.d.ts.map +1 -0
- package/dist/types/link/v5/pairing.d.ts +37 -0
- package/dist/types/link/v5/pairing.d.ts.map +1 -0
- package/dist/types/link/v5/protocol.d.ts +60 -0
- package/dist/types/link/v5/protocol.d.ts.map +1 -0
- package/dist/types/link/v5/relay-transport.d.ts +73 -0
- package/dist/types/link/v5/relay-transport.d.ts.map +1 -0
- package/dist/types/link/v5/session-crypto.d.ts +51 -0
- package/dist/types/link/v5/session-crypto.d.ts.map +1 -0
- package/dist/types/link/v5/transport.d.ts +77 -0
- package/dist/types/link/v5/transport.d.ts.map +1 -0
- package/dist/types/link/v5/web-deeplink.d.ts +77 -0
- package/dist/types/link/v5/web-deeplink.d.ts.map +1 -0
- package/dist/types/public.d.ts +1 -0
- package/dist/types/public.d.ts.map +1 -1
- package/examples/web-deeplink-dapp.ts +53 -0
- package/package.json +8 -2
- package/spec/CHANGELOG.md +9 -0
- package/spec/phantasma-link-v5.md +701 -0
package/README.md
CHANGED
|
@@ -41,6 +41,12 @@ import { Transaction } from 'phantasma-sdk-ts/core/tx/Transaction';
|
|
|
41
41
|
|
|
42
42
|
Those compatibility paths are deprecated. New code should use `/public` or the lowercase module paths shown above.
|
|
43
43
|
|
|
44
|
+
## Phantasma Link v5 protocol
|
|
45
|
+
|
|
46
|
+
The dApp↔wallet connection protocol (envelope, transports, pairing, sessions, encryption)
|
|
47
|
+
is specified in [`spec/phantasma-link-v5.md`](spec/phantasma-link-v5.md). This package is
|
|
48
|
+
its reference implementation, exposed under the `phantasma-sdk-ts/link/v5` entry point.
|
|
49
|
+
|
|
44
50
|
## RPC Example
|
|
45
51
|
|
|
46
52
|
```ts
|
package/dist/cjs/index.js
CHANGED
|
@@ -36,11 +36,13 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
36
36
|
};
|
|
37
37
|
})();
|
|
38
38
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
-
exports.EasyConnect = exports.PhantasmaLink = exports.PhantasmaTS = void 0;
|
|
39
|
+
exports.PhantasmaLinkV5 = exports.EasyConnect = exports.PhantasmaLink = exports.PhantasmaTS = void 0;
|
|
40
40
|
__exportStar(require("./core/index.js"), exports);
|
|
41
41
|
exports.PhantasmaTS = __importStar(require("./core/index.js"));
|
|
42
42
|
var phantasma_link_js_1 = require("./link/phantasma-link.js");
|
|
43
43
|
Object.defineProperty(exports, "PhantasmaLink", { enumerable: true, get: function () { return phantasma_link_js_1.PhantasmaLink; } });
|
|
44
44
|
var easy_connect_js_1 = require("./link/easy-connect.js");
|
|
45
45
|
Object.defineProperty(exports, "EasyConnect", { enumerable: true, get: function () { return easy_connect_js_1.EasyConnect; } });
|
|
46
|
+
// Phantasma Link v5 (new generation), namespaced to avoid flat-name collisions.
|
|
47
|
+
exports.PhantasmaLinkV5 = __importStar(require("./link/v5/index.js"));
|
|
46
48
|
//const TransportWebUSB = require('@ledgerhq/hw-transport-webusb').default;
|
package/dist/cjs/link/index.js
CHANGED
|
@@ -15,3 +15,8 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
15
15
|
};
|
|
16
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
17
|
__exportStar(require("./interfaces/index.js"), exports);
|
|
18
|
+
// Phantasma Link v5 (new generation; parallel to the legacy v1–v4 `PhantasmaLink`).
|
|
19
|
+
// NOT re-exported flatly here: v5 defines its own `SignatureKind` (string union) that
|
|
20
|
+
// would clash with the contract `SignatureKind` enum aggregated by `src/core/index.ts`.
|
|
21
|
+
// Import v5 via the `phantasma-sdk-ts/link/v5` subpath or the `PhantasmaLinkV5` namespace
|
|
22
|
+
// (exported from the package root and `phantasma-sdk-ts/public`).
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Phantasma Link v5 - capability handshake + session/account/chain types (spec §5, §7, §9).
|
|
3
|
+
// Capability negotiation replaces the v1-v4 "magic version int": a dApp learns up front
|
|
4
|
+
// which methods, chains, tx formats, signature kinds, and payload sizes the wallet
|
|
5
|
+
// supports, instead of guessing from a single number.
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Phantasma Link v5 - the cohesive client (spec §6/§9). This IS the v5 entry point: there is
|
|
3
|
+
// NO separate "EasyConnect"-style wrapper. Transport selection, the connect handshake, and the
|
|
4
|
+
// typed `pha_*` methods are all part of this one client. Build it directly with any transport,
|
|
5
|
+
// or use the `loopback()` factory for the desktop case.
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.PhantasmaLink5 = void 0;
|
|
8
|
+
const protocol_js_1 = require("./protocol.js");
|
|
9
|
+
const transport_js_1 = require("./transport.js");
|
|
10
|
+
const loopback_transport_js_1 = require("./loopback-transport.js");
|
|
11
|
+
const deeplink_js_1 = require("./deeplink.js");
|
|
12
|
+
const relay_transport_js_1 = require("./relay-transport.js");
|
|
13
|
+
const errors_js_1 = require("./errors.js");
|
|
14
|
+
const pairing_js_1 = require("./pairing.js");
|
|
15
|
+
const encoding_js_1 = require("./encoding.js");
|
|
16
|
+
const web_deeplink_js_1 = require("./web-deeplink.js");
|
|
17
|
+
const session_crypto_js_1 = require("./session-crypto.js");
|
|
18
|
+
/**
|
|
19
|
+
* The v5 client. `connect()` runs the capability handshake and stores the session; the typed
|
|
20
|
+
* methods then forward to the wallet and return validated-by-contract results. Events
|
|
21
|
+
* (account/chain/session changes) are delivered via {@link onEvent}.
|
|
22
|
+
*/
|
|
23
|
+
class PhantasmaLink5 {
|
|
24
|
+
constructor(transport, options = {}) {
|
|
25
|
+
// Cleanup callbacks registered by factories (e.g. the web-deeplink URL listener);
|
|
26
|
+
// run once on close() so a discarded client does not keep page hooks alive.
|
|
27
|
+
this.disposers = [];
|
|
28
|
+
this.transport = transport;
|
|
29
|
+
// Capture responses that arrive with no in-flight request (the deeplink page reloaded
|
|
30
|
+
// mid-flow). The web-deeplink factory delivers any such response during construction, so a
|
|
31
|
+
// caller can recover it via takeUnmatchedResponse() right after building the client.
|
|
32
|
+
this.session = new transport_js_1.LinkSessionClient(transport, {
|
|
33
|
+
...options,
|
|
34
|
+
onUnmatchedResponse: (response) => {
|
|
35
|
+
this.lastUnmatched = response;
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
this.defaultDapp = options.dapp;
|
|
39
|
+
this.onSessionChange = options.onSessionChange;
|
|
40
|
+
// One-tap pairing (spec §15 step 3): the wallet may push the connect result as an
|
|
41
|
+
// unsolicited event right after the pairing approval, instead of waiting for an
|
|
42
|
+
// explicit pha_connect. Adopt it exactly like a connect result so the session is
|
|
43
|
+
// live (and persisted via onSessionChange) the moment the event arrives.
|
|
44
|
+
this.session.onEvent((event, data) => {
|
|
45
|
+
if (event === protocol_js_1.LinkEvent.SessionEstablished && isConnectResult(data)) {
|
|
46
|
+
this.adoptConnectResult(data);
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
/** Build a client over the desktop loopback transport (plaintext, trusted-local). */
|
|
51
|
+
static loopback(options = {}) {
|
|
52
|
+
return new PhantasmaLink5(new loopback_transport_js_1.LoopbackTransport(options));
|
|
53
|
+
}
|
|
54
|
+
/** Build a client over the deeplink transport (spec §17). The channel key from pairing is
|
|
55
|
+
* MANDATORY here: deeplink URLs are interceptable, so plaintext frames are never allowed. */
|
|
56
|
+
static deeplink(options) {
|
|
57
|
+
if (!options.sessionKey || options.sessionKey.length !== 32) {
|
|
58
|
+
throw new errors_js_1.LinkError(errors_js_1.LinkErrorCode.InvalidParams, 'Deeplink requires the 32-byte pairing session key');
|
|
59
|
+
}
|
|
60
|
+
return new PhantasmaLink5(new deeplink_js_1.DeeplinkTransport(options), {
|
|
61
|
+
sessionKey: options.sessionKey,
|
|
62
|
+
// Deeplink round-trips include an app switch + human consent; see the constant.
|
|
63
|
+
requestTimeoutMs: options.requestTimeoutMs ?? deeplink_js_1.DEEPLINK_REQUEST_TIMEOUT_MS,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
/** Build a client over the relay transport (spec §6.4/§16). The channel key from
|
|
67
|
+
* pairing is MANDATORY: relay payloads are ALWAYS encrypted (spec §8) - the relay is
|
|
68
|
+
* E2E-blind and must stay that way. */
|
|
69
|
+
static relay(options) {
|
|
70
|
+
if (!options.sessionKey || options.sessionKey.length !== 32) {
|
|
71
|
+
throw new errors_js_1.LinkError(errors_js_1.LinkErrorCode.InvalidParams, 'Relay requires the 32-byte pairing session key');
|
|
72
|
+
}
|
|
73
|
+
return new PhantasmaLink5(new relay_transport_js_1.RelayTransport(options), {
|
|
74
|
+
sessionKey: options.sessionKey,
|
|
75
|
+
// Relay round-trips include human consents too; same generous default.
|
|
76
|
+
requestTimeoutMs: options.requestTimeoutMs ?? deeplink_js_1.DEEPLINK_REQUEST_TIMEOUT_MS,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Build a client for the ecdh pairing fallback (spec §15/§18.1): the custom-scheme
|
|
81
|
+
* channel is hijackable, so NO secret rides the pairing URI - only the dApp's
|
|
82
|
+
* ephemeral X25519 PUBLIC key. The wallet answers over the relay with its own public
|
|
83
|
+
* key plus the sealed connect result; the session key is derived on arrival and the
|
|
84
|
+
* client refuses to send (or accept) anything before that. Show {@link pairingUri}
|
|
85
|
+
* (a phantasma:// URI) to start; the session then arrives one-tap.
|
|
86
|
+
*/
|
|
87
|
+
static relayEcdh(options) {
|
|
88
|
+
const topic = options.topic ?? (0, session_crypto_js_1.randomToken)(32);
|
|
89
|
+
const pair = options.keyPair ?? (0, session_crypto_js_1.generateEphemeralKeyPair)();
|
|
90
|
+
const relayUrl = options.url ?? relay_transport_js_1.DEFAULT_RELAY_URL;
|
|
91
|
+
const transport = new relay_transport_js_1.RelayTransport({
|
|
92
|
+
...options,
|
|
93
|
+
topic,
|
|
94
|
+
url: relayUrl,
|
|
95
|
+
onWalletKey: (publicKeyB64Url) => {
|
|
96
|
+
// Fires only when the wallet's hop arrives, long after `client` below exists.
|
|
97
|
+
const walletPublicKey = (0, encoding_js_1.base64UrlToBytes)(publicKeyB64Url);
|
|
98
|
+
client.session.setSessionKey((0, session_crypto_js_1.deriveSessionKey)(walletPublicKey, pair.secretKey));
|
|
99
|
+
},
|
|
100
|
+
});
|
|
101
|
+
const client = new PhantasmaLink5(transport, {
|
|
102
|
+
dapp: options.dapp,
|
|
103
|
+
requireSessionKey: true,
|
|
104
|
+
requestTimeoutMs: options.requestTimeoutMs ?? deeplink_js_1.DEEPLINK_REQUEST_TIMEOUT_MS,
|
|
105
|
+
});
|
|
106
|
+
client.pairingUriValue = (0, pairing_js_1.buildPairingUri)({
|
|
107
|
+
topic,
|
|
108
|
+
mode: 'ecdh',
|
|
109
|
+
dappPublicKey: pair.publicKey,
|
|
110
|
+
relay: relayUrl,
|
|
111
|
+
meta: options.dapp,
|
|
112
|
+
// The whole point of ecdh is the hijackable custom scheme; a safe channel
|
|
113
|
+
// (universal link / QR) should use the simpler sym mode instead.
|
|
114
|
+
scheme: 'scheme',
|
|
115
|
+
});
|
|
116
|
+
return client;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Build a ready-to-use client for a WEB dApp over the deeplink transport (spec §17),
|
|
120
|
+
* bundling the per-dApp glue: pairing material generation, the pairing URI, persistence
|
|
121
|
+
* (localStorage by default), restore + session resume across page loads, and intake of
|
|
122
|
+
* the response URLs the wallet opens back at the page (initial URL + `hashchange`).
|
|
123
|
+
*
|
|
124
|
+
* The factory itself never opens a URL: mobile browsers only allow app-opening
|
|
125
|
+
* navigation from a user gesture. The dApp drives the hops - show {@link pairingUri}
|
|
126
|
+
* (link/QR) for the one-time pairing consent, then call {@link connect} and the typed
|
|
127
|
+
* methods from click handlers; an established session resumes promptlessly (spec §7).
|
|
128
|
+
*/
|
|
129
|
+
static async webDeeplink(options) {
|
|
130
|
+
const hooks = (0, web_deeplink_js_1.resolveWebDeeplinkHooks)(options);
|
|
131
|
+
let record = (0, web_deeplink_js_1.loadWebDeeplinkRecord)(hooks.storage, hooks.storageKey);
|
|
132
|
+
if (!record) {
|
|
133
|
+
// Persist BEFORE any URL hop: opening the wallet may unload this page, and the
|
|
134
|
+
// pairing key must already be on disk to decrypt responses after it comes back.
|
|
135
|
+
record = (0, web_deeplink_js_1.createWebDeeplinkRecord)(options.callback ?? (0, web_deeplink_js_1.stripUrlFragment)(hooks.pageUrl()));
|
|
136
|
+
(0, web_deeplink_js_1.saveWebDeeplinkRecord)(hooks.storage, hooks.storageKey, record);
|
|
137
|
+
}
|
|
138
|
+
// The record is the source of truth from here on (a snapshot for the closures below).
|
|
139
|
+
const stored = record;
|
|
140
|
+
const sessionKey = (0, encoding_js_1.base64UrlToBytes)(stored.key);
|
|
141
|
+
const client = new PhantasmaLink5(new deeplink_js_1.DeeplinkTransport({
|
|
142
|
+
topic: stored.topic,
|
|
143
|
+
opener: hooks.opener,
|
|
144
|
+
walletBase: options.walletBase,
|
|
145
|
+
}), {
|
|
146
|
+
dapp: options.dapp,
|
|
147
|
+
sessionKey,
|
|
148
|
+
sessionId: stored.sessionId,
|
|
149
|
+
requestTimeoutMs: options.requestTimeoutMs ?? deeplink_js_1.DEEPLINK_REQUEST_TIMEOUT_MS,
|
|
150
|
+
onSessionChange: (connect) => {
|
|
151
|
+
// Session layer only - the pairing material stays valid across disconnects.
|
|
152
|
+
stored.sessionId = connect?.session.id;
|
|
153
|
+
stored.lastConnect = connect;
|
|
154
|
+
(0, web_deeplink_js_1.saveWebDeeplinkRecord)(hooks.storage, hooks.storageKey, stored);
|
|
155
|
+
},
|
|
156
|
+
});
|
|
157
|
+
// The pairing URI is always available (idempotent to re-show); `sym` requires the
|
|
158
|
+
// domain-verified universal link - a symmetric key never rides a custom scheme (§15).
|
|
159
|
+
client.pairingUriValue = (0, pairing_js_1.buildPairingUri)({
|
|
160
|
+
topic: stored.topic,
|
|
161
|
+
mode: 'sym',
|
|
162
|
+
symKey: sessionKey,
|
|
163
|
+
callback: stored.callback,
|
|
164
|
+
meta: options.dapp,
|
|
165
|
+
scheme: 'universal',
|
|
166
|
+
host: options.host,
|
|
167
|
+
});
|
|
168
|
+
// Hop-free hydration: a reloaded page shows the cached account immediately; the next
|
|
169
|
+
// real request (or a connect()) proves liveness and refreshes it.
|
|
170
|
+
client.lastConnect = stored.lastConnect;
|
|
171
|
+
const consumePageUrl = () => {
|
|
172
|
+
const href = hooks.pageUrl();
|
|
173
|
+
if (client.deliverUrl(href)) {
|
|
174
|
+
// Drop the consumed fragment so reload/back cannot re-deliver it and the
|
|
175
|
+
// ciphertext does not linger in the address bar (or get copied with the URL).
|
|
176
|
+
hooks.replaceUrl?.((0, web_deeplink_js_1.stripUrlFragment)(href));
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
// A response may already sit in the page URL (the wallet's callback navigation
|
|
180
|
+
// cold-loaded this page); consume it before subscribing to changes.
|
|
181
|
+
consumePageUrl();
|
|
182
|
+
const unsubscribe = hooks.onUrlChange?.(consumePageUrl);
|
|
183
|
+
if (unsubscribe) {
|
|
184
|
+
client.disposers.push(unsubscribe);
|
|
185
|
+
}
|
|
186
|
+
return client;
|
|
187
|
+
}
|
|
188
|
+
/** Feed an OS-delivered URL into a deeplink-backed client; see DeeplinkTransport.deliverUrl.
|
|
189
|
+
* The web-deeplink factory wires this automatically; SPAs whose routing swallows
|
|
190
|
+
* `hashchange` call it explicitly on route events. */
|
|
191
|
+
deliverUrl(url) {
|
|
192
|
+
return this.transport instanceof deeplink_js_1.DeeplinkTransport && this.transport.deliverUrl(url);
|
|
193
|
+
}
|
|
194
|
+
/** Take (and clear) a wallet response that arrived with no in-flight request to match it -
|
|
195
|
+
* the deeplink reload case, where the page that issued the request was discarded before the
|
|
196
|
+
* answer returned. The web-deeplink factory delivers it during construction, so read it right
|
|
197
|
+
* after building the client. Returns undefined when there is nothing to recover. */
|
|
198
|
+
takeUnmatchedResponse() {
|
|
199
|
+
const response = this.lastUnmatched;
|
|
200
|
+
this.lastUnmatched = undefined;
|
|
201
|
+
return response;
|
|
202
|
+
}
|
|
203
|
+
/** The pairing URI for this client's channel (set by pairing-capable factories such as
|
|
204
|
+
* {@link webDeeplink}); render it as a link/QR for the one-time wallet pairing consent. */
|
|
205
|
+
get pairingUri() {
|
|
206
|
+
return this.pairingUriValue;
|
|
207
|
+
}
|
|
208
|
+
/** The account from the last successful {@link connect}, if any. */
|
|
209
|
+
get account() {
|
|
210
|
+
return this.lastConnect?.account;
|
|
211
|
+
}
|
|
212
|
+
/** The capabilities granted at the last successful {@link connect}, if any. */
|
|
213
|
+
get capabilities() {
|
|
214
|
+
return this.lastConnect?.capabilities;
|
|
215
|
+
}
|
|
216
|
+
/** Pair/resume and run the capability handshake. The wallet MAY grant a subset of the
|
|
217
|
+
* requested capabilities; inspect the returned {@link ConnectResult}. `dapp` falls back
|
|
218
|
+
* to `options.dapp` (factories set it), so a configured client connects with no args. */
|
|
219
|
+
async connect(dapp, extra = {}) {
|
|
220
|
+
const identity = dapp ?? this.defaultDapp;
|
|
221
|
+
if (!identity) {
|
|
222
|
+
throw new errors_js_1.LinkError(errors_js_1.LinkErrorCode.InvalidParams, 'connect() needs dApp metadata: pass it here or set options.dapp');
|
|
223
|
+
}
|
|
224
|
+
const params = { dapp: identity, ...extra };
|
|
225
|
+
// Default to resuming the current session (spec §7). Always safe: a matching wallet
|
|
226
|
+
// resumes promptlessly, any mismatch silently falls back to a fresh consent prompt.
|
|
227
|
+
if (params.session === undefined && this.session.getSessionId() !== undefined) {
|
|
228
|
+
params.session = this.session.getSessionId();
|
|
229
|
+
}
|
|
230
|
+
const result = await this.request(protocol_js_1.LinkMethod.Connect, params);
|
|
231
|
+
this.adoptConnectResult(result);
|
|
232
|
+
return result;
|
|
233
|
+
}
|
|
234
|
+
/** Make a connect result the live session state (used by both the explicit connect and
|
|
235
|
+
* the wallet-pushed {@link LinkEvent.SessionEstablished} one-tap pairing path). */
|
|
236
|
+
adoptConnectResult(result) {
|
|
237
|
+
this.lastConnect = result;
|
|
238
|
+
this.session.setSessionId(result.session.id);
|
|
239
|
+
this.onSessionChange?.(result);
|
|
240
|
+
}
|
|
241
|
+
getAccounts() {
|
|
242
|
+
return this.request(protocol_js_1.LinkMethod.GetAccounts);
|
|
243
|
+
}
|
|
244
|
+
getChains() {
|
|
245
|
+
return this.request(protocol_js_1.LinkMethod.GetChains);
|
|
246
|
+
}
|
|
247
|
+
getWalletInfo() {
|
|
248
|
+
return this.request(protocol_js_1.LinkMethod.GetWalletInfo);
|
|
249
|
+
}
|
|
250
|
+
signMessage(params) {
|
|
251
|
+
return this.request(protocol_js_1.LinkMethod.SignMessage, params);
|
|
252
|
+
}
|
|
253
|
+
/** Sign a transaction without broadcasting; the dApp submits the returned signed tx. */
|
|
254
|
+
signTransaction(params) {
|
|
255
|
+
return this.request(protocol_js_1.LinkMethod.SignTransaction, params);
|
|
256
|
+
}
|
|
257
|
+
/** Sign AND broadcast a transaction via the format's RPC endpoint. */
|
|
258
|
+
sendTransaction(params) {
|
|
259
|
+
return this.request(protocol_js_1.LinkMethod.SendTransaction, params);
|
|
260
|
+
}
|
|
261
|
+
/** Read-only VM invoke (no keys, no approval). */
|
|
262
|
+
invokeScript(params) {
|
|
263
|
+
return this.request(protocol_js_1.LinkMethod.InvokeScript, params);
|
|
264
|
+
}
|
|
265
|
+
async disconnect() {
|
|
266
|
+
const result = await this.request(protocol_js_1.LinkMethod.Disconnect);
|
|
267
|
+
// The wallet dropped the session; clear local state so later requests do not carry a
|
|
268
|
+
// dead session id (and embedders can erase their persisted copy).
|
|
269
|
+
this.lastConnect = undefined;
|
|
270
|
+
this.session.setSessionId(undefined);
|
|
271
|
+
this.onSessionChange?.(undefined);
|
|
272
|
+
return result;
|
|
273
|
+
}
|
|
274
|
+
/** Drop the local (and persisted) session WITHOUT notifying the wallet. Use when a wallet
|
|
275
|
+
* round-trip is undesirable - notably deeplink, where pha_disconnect would navigate to the
|
|
276
|
+
* wallet and reload this page, so the cleanup after the request never runs and the session
|
|
277
|
+
* would resume on the next load. The wallet's side lapses on its own session TTL (spec §7). */
|
|
278
|
+
forgetSession() {
|
|
279
|
+
this.lastConnect = undefined;
|
|
280
|
+
this.session.setSessionId(undefined);
|
|
281
|
+
this.onSessionChange?.(undefined);
|
|
282
|
+
}
|
|
283
|
+
/** Subscribe to wallet->dApp events; returns an unsubscribe function. */
|
|
284
|
+
onEvent(handler) {
|
|
285
|
+
return this.session.onEvent(handler);
|
|
286
|
+
}
|
|
287
|
+
/** Close the underlying transport and reject any in-flight requests. */
|
|
288
|
+
close() {
|
|
289
|
+
// Release factory-registered hooks first (e.g. the web page-URL listener) so nothing
|
|
290
|
+
// can deliver into a closing client; disposers must never block the close itself.
|
|
291
|
+
for (const dispose of this.disposers.splice(0)) {
|
|
292
|
+
try {
|
|
293
|
+
dispose();
|
|
294
|
+
}
|
|
295
|
+
catch {
|
|
296
|
+
// A failing disposer must not prevent the transport from closing.
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
this.session.close();
|
|
300
|
+
}
|
|
301
|
+
// The wallet's result is `unknown` on the wire; we cast to the method's contract type. The
|
|
302
|
+
// shapes are validated structurally at the envelope layer (§4); per-field validation can be
|
|
303
|
+
// layered on later without changing call sites. `params` is `object` so typed param
|
|
304
|
+
// interfaces (which lack an index signature) are accepted, then forwarded as a plain record.
|
|
305
|
+
request(method, params) {
|
|
306
|
+
return this.session.request(method, params);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
exports.PhantasmaLink5 = PhantasmaLink5;
|
|
310
|
+
/** Structural check for a wallet-pushed connect result before adopting it as session state.
|
|
311
|
+
* The frame already authenticated via the channel key, so this guards against a malformed
|
|
312
|
+
* wallet payload, not an attacker; only the fields the client dereferences are checked. */
|
|
313
|
+
function isConnectResult(data) {
|
|
314
|
+
if (!data || typeof data !== 'object') {
|
|
315
|
+
return false;
|
|
316
|
+
}
|
|
317
|
+
const record = data;
|
|
318
|
+
return (typeof record.session === 'object' &&
|
|
319
|
+
record.session !== null &&
|
|
320
|
+
typeof record.session.id === 'string' &&
|
|
321
|
+
record.session.id.length > 0 &&
|
|
322
|
+
typeof record.account === 'object' &&
|
|
323
|
+
record.account !== null);
|
|
324
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Phantasma Link v5 - deeplink transport (spec §17). dApp and wallet live in separate apps on
|
|
3
|
+
// the SAME device and talk by opening URLs at each other:
|
|
4
|
+
// request: {walletBase}/v5/req#t=<topic>&f=<base64url(frame)> (dApp opens the wallet)
|
|
5
|
+
// response: {callback}#plv=5&t=<topic>&f=<base64url(frame)> (wallet opens the dApp back)
|
|
6
|
+
// One request = one URL hop each way ("ping-pong"), sized for SMALL operations; big payloads go
|
|
7
|
+
// over the relay (spec §16). Frames on this transport are ALWAYS the encrypted envelope
|
|
8
|
+
// {nonce, ct} sealed with the pairing session key - custom-scheme URLs are interceptable, so
|
|
9
|
+
// plaintext is never allowed here (enforced by PhantasmaLink5.deeplink()).
|
|
10
|
+
//
|
|
11
|
+
// The transport cannot "listen": responses arrive when the OS (re)opens the dApp with a new
|
|
12
|
+
// URL. The dApp wires that OS event into deliverUrl() (page load / visibilitychange /
|
|
13
|
+
// appUrlOpen, depending on platform).
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.DeeplinkTransport = exports.DEEPLINK_REQUEST_TIMEOUT_MS = exports.WALLET_SCHEME_BASE = void 0;
|
|
16
|
+
exports.buildRequestUrl = buildRequestUrl;
|
|
17
|
+
exports.parseRequestUrl = parseRequestUrl;
|
|
18
|
+
exports.buildResponseUrl = buildResponseUrl;
|
|
19
|
+
exports.parseResponseUrl = parseResponseUrl;
|
|
20
|
+
const errors_js_1 = require("./errors.js");
|
|
21
|
+
const encoding_js_1 = require("./encoding.js");
|
|
22
|
+
/** Default custom-scheme base of the wallet ("phantasma://v5/req"). */
|
|
23
|
+
exports.WALLET_SCHEME_BASE = 'phantasma://';
|
|
24
|
+
/** Default per-request timeout on deeplink transports. A round-trip spans an app switch
|
|
25
|
+
* plus a human consent, so the generic 60 s session default is far too tight here - it
|
|
26
|
+
* would expire the money path while the user is still reading the wallet's Send screen. */
|
|
27
|
+
exports.DEEPLINK_REQUEST_TIMEOUT_MS = 300000;
|
|
28
|
+
/** Build the dApp->wallet request URL. `walletBase` is e.g. `phantasma:/` (custom scheme) or
|
|
29
|
+
* `https://link.phantasma.info` (universal link); the path is fixed to /v5/req. */
|
|
30
|
+
function buildRequestUrl(walletBase, topic, frame) {
|
|
31
|
+
const base = walletBase.endsWith('/') ? walletBase : `${walletBase}/`;
|
|
32
|
+
return `${base}v5/req#t=${encodeURIComponent(topic)}&f=${(0, encoding_js_1.bytesToBase64Url)((0, encoding_js_1.utf8ToBytes)(frame))}`;
|
|
33
|
+
}
|
|
34
|
+
/** Parse a dApp->wallet request URL; null when the URL is not a v5 request deeplink. */
|
|
35
|
+
function parseRequestUrl(url) {
|
|
36
|
+
const hashIndex = url.indexOf('#');
|
|
37
|
+
if (hashIndex < 0 || !url.slice(0, hashIndex).endsWith('/v5/req')) {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
return parseFragment(url.slice(hashIndex + 1), false);
|
|
41
|
+
}
|
|
42
|
+
/** Build the wallet->dApp response URL onto the dApp's pairing callback. */
|
|
43
|
+
function buildResponseUrl(callback, topic, frame) {
|
|
44
|
+
const hashIndex = callback.indexOf('#');
|
|
45
|
+
const base = hashIndex < 0 ? callback : callback.slice(0, hashIndex);
|
|
46
|
+
return `${base}#plv=5&t=${encodeURIComponent(topic)}&f=${(0, encoding_js_1.bytesToBase64Url)((0, encoding_js_1.utf8ToBytes)(frame))}`;
|
|
47
|
+
}
|
|
48
|
+
/** Parse a wallet->dApp response URL; null when the URL is not a v5 response deeplink. */
|
|
49
|
+
function parseResponseUrl(url) {
|
|
50
|
+
const hashIndex = url.indexOf('#');
|
|
51
|
+
if (hashIndex < 0) {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
return parseFragment(url.slice(hashIndex + 1), true);
|
|
55
|
+
}
|
|
56
|
+
function parseFragment(fragment, requirePlvMarker) {
|
|
57
|
+
const params = new URLSearchParams(fragment);
|
|
58
|
+
if (requirePlvMarker && params.get('plv') !== '5') {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
const topic = params.get('t');
|
|
62
|
+
const f = params.get('f');
|
|
63
|
+
if (!topic || !f) {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
try {
|
|
67
|
+
return { topic, frame: (0, encoding_js_1.bytesToUtf8)((0, encoding_js_1.base64UrlToBytes)(f)) };
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* {@link LinkTransport} over deeplink ping-pong. `send` opens a wallet URL carrying the frame;
|
|
75
|
+
* the dApp feeds OS-delivered URLs into {@link deliverUrl}, which dispatches frames for this
|
|
76
|
+
* transport's topic to the session client.
|
|
77
|
+
*/
|
|
78
|
+
class DeeplinkTransport {
|
|
79
|
+
constructor(options) {
|
|
80
|
+
this.closed = false;
|
|
81
|
+
if (!options.topic) {
|
|
82
|
+
throw new errors_js_1.LinkError(errors_js_1.LinkErrorCode.InvalidParams, 'Deeplink transport requires a topic');
|
|
83
|
+
}
|
|
84
|
+
this.topic = options.topic;
|
|
85
|
+
this.opener = options.opener;
|
|
86
|
+
this.walletBase = options.walletBase ?? exports.WALLET_SCHEME_BASE;
|
|
87
|
+
}
|
|
88
|
+
send(frame) {
|
|
89
|
+
if (this.closed) {
|
|
90
|
+
throw new errors_js_1.LinkError(errors_js_1.LinkErrorCode.Disconnected, 'Deeplink transport is closed');
|
|
91
|
+
}
|
|
92
|
+
this.opener(buildRequestUrl(this.walletBase, this.topic, frame));
|
|
93
|
+
}
|
|
94
|
+
onMessage(handler) {
|
|
95
|
+
this.messageHandler = handler;
|
|
96
|
+
}
|
|
97
|
+
onClose(handler) {
|
|
98
|
+
this.closeHandler = handler;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Feed an OS-delivered URL into the transport. Returns true when the URL was a v5 response
|
|
102
|
+
* for THIS transport's topic and was dispatched; false lets the caller try other handlers.
|
|
103
|
+
*/
|
|
104
|
+
deliverUrl(url) {
|
|
105
|
+
if (this.closed) {
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
const parsed = parseResponseUrl(url);
|
|
109
|
+
if (!parsed || parsed.topic !== this.topic) {
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
this.messageHandler?.(parsed.frame);
|
|
113
|
+
return true;
|
|
114
|
+
}
|
|
115
|
+
close() {
|
|
116
|
+
if (this.closed) {
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
this.closed = true;
|
|
120
|
+
this.closeHandler?.('Deeplink transport closed');
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
exports.DeeplinkTransport = DeeplinkTransport;
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Phantasma Link v5 - encoding helpers. The transport carries binary (ciphertext, nonces,
|
|
3
|
+
// keys, serialized txs) as base64 (spec §4 / §18), which halves the wire size vs the
|
|
4
|
+
// v1-v4 hex. Implemented dependency-free and environment-agnostic (browser + Node), since
|
|
5
|
+
// the SDK ships to both and `Buffer`/`btoa` are not uniformly available.
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.bytesToBase64 = bytesToBase64;
|
|
8
|
+
exports.base64ToBytes = base64ToBytes;
|
|
9
|
+
exports.bytesToBase64Url = bytesToBase64Url;
|
|
10
|
+
exports.base64UrlToBytes = base64UrlToBytes;
|
|
11
|
+
exports.utf8ToBytes = utf8ToBytes;
|
|
12
|
+
exports.bytesToUtf8 = bytesToUtf8;
|
|
13
|
+
const B64_ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
|
|
14
|
+
// Reverse lookup table: byte value of each base64 character, or -1 if not a base64 char.
|
|
15
|
+
const B64_LOOKUP = (() => {
|
|
16
|
+
const table = new Int8Array(256).fill(-1);
|
|
17
|
+
for (let i = 0; i < B64_ALPHABET.length; i++) {
|
|
18
|
+
table[B64_ALPHABET.charCodeAt(i)] = i;
|
|
19
|
+
}
|
|
20
|
+
return table;
|
|
21
|
+
})();
|
|
22
|
+
/** Encode bytes as standard base64 (with `=` padding). */
|
|
23
|
+
function bytesToBase64(bytes) {
|
|
24
|
+
let out = '';
|
|
25
|
+
for (let i = 0; i < bytes.length; i += 3) {
|
|
26
|
+
const b0 = bytes[i];
|
|
27
|
+
const hasB1 = i + 1 < bytes.length;
|
|
28
|
+
const hasB2 = i + 2 < bytes.length;
|
|
29
|
+
const b1 = hasB1 ? bytes[i + 1] : 0;
|
|
30
|
+
const b2 = hasB2 ? bytes[i + 2] : 0;
|
|
31
|
+
out += B64_ALPHABET[b0 >> 2];
|
|
32
|
+
out += B64_ALPHABET[((b0 & 0x03) << 4) | (b1 >> 4)];
|
|
33
|
+
out += hasB1 ? B64_ALPHABET[((b1 & 0x0f) << 2) | (b2 >> 6)] : '=';
|
|
34
|
+
out += hasB2 ? B64_ALPHABET[b2 & 0x3f] : '=';
|
|
35
|
+
}
|
|
36
|
+
return out;
|
|
37
|
+
}
|
|
38
|
+
/** Decode standard or url-safe base64 (padding optional) to bytes. Throws on invalid input
|
|
39
|
+
* so a malformed frame fails loudly rather than silently producing garbage. */
|
|
40
|
+
function base64ToBytes(input) {
|
|
41
|
+
// Normalize url-safe alphabet and drop padding; we recompute length from content.
|
|
42
|
+
let clean = '';
|
|
43
|
+
for (let i = 0; i < input.length; i++) {
|
|
44
|
+
const c = input[i];
|
|
45
|
+
if (c === '-') {
|
|
46
|
+
clean += '+';
|
|
47
|
+
}
|
|
48
|
+
else if (c === '_') {
|
|
49
|
+
clean += '/';
|
|
50
|
+
}
|
|
51
|
+
else if (c !== '=') {
|
|
52
|
+
clean += c;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
const fullGroups = Math.floor(clean.length / 4);
|
|
56
|
+
const remainder = clean.length % 4;
|
|
57
|
+
if (remainder === 1) {
|
|
58
|
+
throw new Error('Invalid base64 string: dangling character');
|
|
59
|
+
}
|
|
60
|
+
const outLength = fullGroups * 3 + (remainder === 0 ? 0 : remainder - 1);
|
|
61
|
+
const out = new Uint8Array(outLength);
|
|
62
|
+
let o = 0;
|
|
63
|
+
let acc = 0;
|
|
64
|
+
let accBits = 0;
|
|
65
|
+
for (let i = 0; i < clean.length; i++) {
|
|
66
|
+
const v = B64_LOOKUP[clean.charCodeAt(i)];
|
|
67
|
+
if (v < 0) {
|
|
68
|
+
throw new Error('Invalid base64 character');
|
|
69
|
+
}
|
|
70
|
+
acc = (acc << 6) | v;
|
|
71
|
+
accBits += 6;
|
|
72
|
+
if (accBits >= 8) {
|
|
73
|
+
accBits -= 8;
|
|
74
|
+
out[o++] = (acc >> accBits) & 0xff;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return out;
|
|
78
|
+
}
|
|
79
|
+
/** Encode bytes as url-safe base64 without padding (for use in URL fragments, spec §15). */
|
|
80
|
+
function bytesToBase64Url(bytes) {
|
|
81
|
+
return bytesToBase64(bytes).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
|
|
82
|
+
}
|
|
83
|
+
/** Decode url-safe (or standard) base64 to bytes; alias of {@link base64ToBytes}, which
|
|
84
|
+
* already accepts both alphabets. */
|
|
85
|
+
function base64UrlToBytes(input) {
|
|
86
|
+
return base64ToBytes(input);
|
|
87
|
+
}
|
|
88
|
+
/** UTF-8 encode a string to bytes (uses TextEncoder, present in all supported runtimes). */
|
|
89
|
+
function utf8ToBytes(text) {
|
|
90
|
+
return new TextEncoder().encode(text);
|
|
91
|
+
}
|
|
92
|
+
/** UTF-8 decode bytes to a string. */
|
|
93
|
+
function bytesToUtf8(bytes) {
|
|
94
|
+
return new TextDecoder('utf-8', { fatal: false }).decode(bytes);
|
|
95
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Phantasma Link v5 - message envelope (spec §4). A JSON-RPC-2.0 profile that replaces the
|
|
3
|
+
// v1-v4 delimiter-joined string: typed named params, numeric error codes, and request
|
|
4
|
+
// correlation by `id`. One logical message = one envelope.
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.isLinkRequest = isLinkRequest;
|
|
7
|
+
exports.isLinkEvent = isLinkEvent;
|
|
8
|
+
exports.isLinkErrorResponse = isLinkErrorResponse;
|
|
9
|
+
exports.isLinkSuccessResponse = isLinkSuccessResponse;
|
|
10
|
+
exports.encodeEnvelope = encodeEnvelope;
|
|
11
|
+
exports.decodeEnvelope = decodeEnvelope;
|
|
12
|
+
const protocol_js_1 = require("./protocol.js");
|
|
13
|
+
const errors_js_1 = require("./errors.js");
|
|
14
|
+
function isLinkRequest(msg) {
|
|
15
|
+
return 'method' in msg && typeof msg.method === 'string';
|
|
16
|
+
}
|
|
17
|
+
function isLinkEvent(msg) {
|
|
18
|
+
return msg.type === 'event';
|
|
19
|
+
}
|
|
20
|
+
function isLinkErrorResponse(msg) {
|
|
21
|
+
return 'error' in msg && !!msg.error;
|
|
22
|
+
}
|
|
23
|
+
function isLinkSuccessResponse(msg) {
|
|
24
|
+
return 'result' in msg;
|
|
25
|
+
}
|
|
26
|
+
/** Serialize an envelope to its on-the-wire JSON text. */
|
|
27
|
+
function encodeEnvelope(message) {
|
|
28
|
+
return JSON.stringify(message);
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Parse and validate an on-the-wire JSON text into a {@link LinkMessage}.
|
|
32
|
+
*
|
|
33
|
+
* Throws {@link LinkError} with `ParseError` for non-JSON and `InvalidRequest` for a JSON
|
|
34
|
+
* value that is not a well-formed v5 envelope (wrong `plv`, missing `id`, or a shape that
|
|
35
|
+
* is neither request, response, nor event). Validation happens here, once, so every
|
|
36
|
+
* downstream consumer can trust the structure.
|
|
37
|
+
*/
|
|
38
|
+
function decodeEnvelope(text) {
|
|
39
|
+
let parsed;
|
|
40
|
+
try {
|
|
41
|
+
parsed = JSON.parse(text);
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
throw new errors_js_1.LinkError(errors_js_1.LinkErrorCode.ParseError, 'Phantasma Link message is not valid JSON');
|
|
45
|
+
}
|
|
46
|
+
if (!parsed || typeof parsed !== 'object') {
|
|
47
|
+
throw new errors_js_1.LinkError(errors_js_1.LinkErrorCode.InvalidRequest, 'Phantasma Link message must be an object');
|
|
48
|
+
}
|
|
49
|
+
const record = parsed;
|
|
50
|
+
if (record.plv !== protocol_js_1.PLV) {
|
|
51
|
+
throw new errors_js_1.LinkError(errors_js_1.LinkErrorCode.InvalidRequest, `Unsupported Phantasma Link protocol version: ${String(record.plv)}`);
|
|
52
|
+
}
|
|
53
|
+
// Event messages have no `id` and are matched by the `type` discriminator first.
|
|
54
|
+
if (record.type === 'event') {
|
|
55
|
+
if (typeof record.event !== 'string') {
|
|
56
|
+
throw new errors_js_1.LinkError(errors_js_1.LinkErrorCode.InvalidRequest, 'Event envelope is missing `event`');
|
|
57
|
+
}
|
|
58
|
+
return parsed;
|
|
59
|
+
}
|
|
60
|
+
if (typeof record.id !== 'string' || record.id.length === 0) {
|
|
61
|
+
throw new errors_js_1.LinkError(errors_js_1.LinkErrorCode.InvalidRequest, 'Envelope is missing a string `id`');
|
|
62
|
+
}
|
|
63
|
+
if (typeof record.method === 'string') {
|
|
64
|
+
return parsed;
|
|
65
|
+
}
|
|
66
|
+
if ('result' in record) {
|
|
67
|
+
return parsed;
|
|
68
|
+
}
|
|
69
|
+
if (record.error && typeof record.error === 'object') {
|
|
70
|
+
return parsed;
|
|
71
|
+
}
|
|
72
|
+
throw new errors_js_1.LinkError(errors_js_1.LinkErrorCode.InvalidRequest, 'Envelope is neither a request, response, nor event');
|
|
73
|
+
}
|