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.
Files changed (75) hide show
  1. package/README.md +6 -0
  2. package/dist/cjs/index.js +3 -1
  3. package/dist/cjs/link/index.js +5 -0
  4. package/dist/cjs/link/v5/capabilities.js +6 -0
  5. package/dist/cjs/link/v5/client.js +324 -0
  6. package/dist/cjs/link/v5/deeplink.js +123 -0
  7. package/dist/cjs/link/v5/encoding.js +95 -0
  8. package/dist/cjs/link/v5/envelope.js +73 -0
  9. package/dist/cjs/link/v5/errors.js +60 -0
  10. package/dist/cjs/link/v5/index.js +33 -0
  11. package/dist/cjs/link/v5/loopback-transport.js +70 -0
  12. package/dist/cjs/link/v5/methods.js +4 -0
  13. package/dist/cjs/link/v5/pairing.js +95 -0
  14. package/dist/cjs/link/v5/protocol.js +61 -0
  15. package/dist/cjs/link/v5/relay-transport.js +303 -0
  16. package/dist/cjs/link/v5/session-crypto.js +120 -0
  17. package/dist/cjs/link/v5/transport.js +208 -0
  18. package/dist/cjs/link/v5/web-deeplink.js +141 -0
  19. package/dist/cjs/public.js +37 -1
  20. package/dist/esm/index.js +2 -0
  21. package/dist/esm/link/index.js +5 -0
  22. package/dist/esm/link/v5/capabilities.js +5 -0
  23. package/dist/esm/link/v5/client.js +320 -0
  24. package/dist/esm/link/v5/deeplink.js +115 -0
  25. package/dist/esm/link/v5/encoding.js +87 -0
  26. package/dist/esm/link/v5/envelope.js +65 -0
  27. package/dist/esm/link/v5/errors.js +56 -0
  28. package/dist/esm/link/v5/index.js +17 -0
  29. package/dist/esm/link/v5/loopback-transport.js +66 -0
  30. package/dist/esm/link/v5/methods.js +3 -0
  31. package/dist/esm/link/v5/pairing.js +91 -0
  32. package/dist/esm/link/v5/protocol.js +58 -0
  33. package/dist/esm/link/v5/relay-transport.js +299 -0
  34. package/dist/esm/link/v5/session-crypto.js +104 -0
  35. package/dist/esm/link/v5/transport.js +204 -0
  36. package/dist/esm/link/v5/web-deeplink.js +133 -0
  37. package/dist/esm/public.js +3 -0
  38. package/dist/types/index.d.ts +1 -0
  39. package/dist/types/index.d.ts.map +1 -1
  40. package/dist/types/link/v5/capabilities.d.ts +80 -0
  41. package/dist/types/link/v5/capabilities.d.ts.map +1 -0
  42. package/dist/types/link/v5/client.d.ts +119 -0
  43. package/dist/types/link/v5/client.d.ts.map +1 -0
  44. package/dist/types/link/v5/deeplink.d.ts +52 -0
  45. package/dist/types/link/v5/deeplink.d.ts.map +1 -0
  46. package/dist/types/link/v5/encoding.d.ts +15 -0
  47. package/dist/types/link/v5/encoding.d.ts.map +1 -0
  48. package/dist/types/link/v5/envelope.d.ts +48 -0
  49. package/dist/types/link/v5/envelope.d.ts.map +1 -0
  50. package/dist/types/link/v5/errors.d.ts +39 -0
  51. package/dist/types/link/v5/errors.d.ts.map +1 -0
  52. package/dist/types/link/v5/index.d.ts +15 -0
  53. package/dist/types/link/v5/index.d.ts.map +1 -0
  54. package/dist/types/link/v5/loopback-transport.d.ts +43 -0
  55. package/dist/types/link/v5/loopback-transport.d.ts.map +1 -0
  56. package/dist/types/link/v5/methods.d.ts +83 -0
  57. package/dist/types/link/v5/methods.d.ts.map +1 -0
  58. package/dist/types/link/v5/pairing.d.ts +37 -0
  59. package/dist/types/link/v5/pairing.d.ts.map +1 -0
  60. package/dist/types/link/v5/protocol.d.ts +60 -0
  61. package/dist/types/link/v5/protocol.d.ts.map +1 -0
  62. package/dist/types/link/v5/relay-transport.d.ts +73 -0
  63. package/dist/types/link/v5/relay-transport.d.ts.map +1 -0
  64. package/dist/types/link/v5/session-crypto.d.ts +51 -0
  65. package/dist/types/link/v5/session-crypto.d.ts.map +1 -0
  66. package/dist/types/link/v5/transport.d.ts +77 -0
  67. package/dist/types/link/v5/transport.d.ts.map +1 -0
  68. package/dist/types/link/v5/web-deeplink.d.ts +77 -0
  69. package/dist/types/link/v5/web-deeplink.d.ts.map +1 -0
  70. package/dist/types/public.d.ts +1 -0
  71. package/dist/types/public.d.ts.map +1 -1
  72. package/examples/web-deeplink-dapp.ts +53 -0
  73. package/package.json +8 -2
  74. package/spec/CHANGELOG.md +9 -0
  75. package/spec/phantasma-link-v5.md +701 -0
@@ -0,0 +1,120 @@
1
+ "use strict";
2
+ // Phantasma Link v5 - session channel cryptography (spec §8, §18).
3
+ //
4
+ // Construction (AGREED, FINAL): NaCl `secretbox` (XSalsa20-Poly1305) for every session
5
+ // message, under one 32-byte session key, with a 24-byte random nonce per message
6
+ // (XSalsa20's 192-bit nonce makes random nonces collision-safe - no counter coordination).
7
+ // The session key is either delivered directly as a symmetric key over a safe channel
8
+ // (universal link / QR) or derived via X25519 ECDH (NaCl `box.before`) on the
9
+ // custom-scheme fallback. Either way the channel is uniform `secretbox` afterwards.
10
+ //
11
+ // These are EPHEMERAL channel keys only; they are never the account signing key
12
+ // (Ed25519/ECDSA stay strictly for signing). All primitives are tweetnacl, already a SDK
13
+ // dependency and wire-interoperable with crypto_box / x/crypto/nacl / libsodium / NaCl.Net.
14
+ var __importDefault = (this && this.__importDefault) || function (mod) {
15
+ return (mod && mod.__esModule) ? mod : { "default": mod };
16
+ };
17
+ Object.defineProperty(exports, "__esModule", { value: true });
18
+ exports.SIGN_MESSAGE_RANDOM_LENGTH = exports.SIGN_MESSAGE_DOMAIN_TAG = exports.PUBLIC_KEY_LENGTH = exports.NONCE_LENGTH = exports.SESSION_KEY_LENGTH = void 0;
19
+ exports.generateSessionKey = generateSessionKey;
20
+ exports.randomToken = randomToken;
21
+ exports.generateEphemeralKeyPair = generateEphemeralKeyPair;
22
+ exports.deriveSessionKey = deriveSessionKey;
23
+ exports.seal = seal;
24
+ exports.open = open;
25
+ exports.sealEnvelopeText = sealEnvelopeText;
26
+ exports.openEnvelopeText = openEnvelopeText;
27
+ exports.generateSignMessageRandom = generateSignMessageRandom;
28
+ exports.buildSignMessagePayload = buildSignMessagePayload;
29
+ const tweetnacl_1 = __importDefault(require("tweetnacl"));
30
+ const errors_js_1 = require("./errors.js");
31
+ const encoding_js_1 = require("./encoding.js");
32
+ /** Session/channel key length (32 bytes). */
33
+ exports.SESSION_KEY_LENGTH = tweetnacl_1.default.secretbox.keyLength;
34
+ /** Per-message nonce length (24 bytes). */
35
+ exports.NONCE_LENGTH = tweetnacl_1.default.secretbox.nonceLength;
36
+ /** X25519 public-key length (32 bytes). */
37
+ exports.PUBLIC_KEY_LENGTH = tweetnacl_1.default.box.publicKeyLength;
38
+ /** Domain separator prepended before signing a message. It can never be the prefix of a
39
+ * valid serialized Phantasma transaction, so a `signMessage` signature can never be
40
+ * replayed as a transaction signature (spec §8). */
41
+ exports.SIGN_MESSAGE_DOMAIN_TAG = (0, encoding_js_1.utf8ToBytes)('PHANTASMA_LINK_V5_MSG\n');
42
+ /** Length of the CSPRNG random the wallet prepends in `signMessage` (spec §8). */
43
+ exports.SIGN_MESSAGE_RANDOM_LENGTH = 32;
44
+ /** Generate a fresh 32-byte symmetric session key (primary universal-link / QR path). */
45
+ function generateSessionKey() {
46
+ return tweetnacl_1.default.randomBytes(exports.SESSION_KEY_LENGTH);
47
+ }
48
+ /** Generate a random url-safe token (default 16 bytes), used for request `id`s and pairing
49
+ * topics. Crypto-random so ids are unguessable as well as unique. */
50
+ function randomToken(byteLength = 16) {
51
+ return (0, encoding_js_1.bytesToBase64Url)(tweetnacl_1.default.randomBytes(byteLength));
52
+ }
53
+ /** Generate an ephemeral X25519 keypair (custom-scheme ECDH fallback path). */
54
+ function generateEphemeralKeyPair() {
55
+ const pair = tweetnacl_1.default.box.keyPair();
56
+ return { publicKey: pair.publicKey, secretKey: pair.secretKey };
57
+ }
58
+ /** Derive the 32-byte session key from the peer's X25519 public key and our secret key
59
+ * (ECDH; NaCl `box.before`). The result is directly usable as a `secretbox` key. */
60
+ function deriveSessionKey(theirPublicKey, mySecretKey) {
61
+ if (theirPublicKey.length !== exports.PUBLIC_KEY_LENGTH || mySecretKey.length !== exports.PUBLIC_KEY_LENGTH) {
62
+ throw new errors_js_1.LinkError(errors_js_1.LinkErrorCode.InternalError, 'Invalid X25519 key length');
63
+ }
64
+ return tweetnacl_1.default.box.before(theirPublicKey, mySecretKey);
65
+ }
66
+ function assertKey(key) {
67
+ if (key.length !== exports.SESSION_KEY_LENGTH) {
68
+ throw new errors_js_1.LinkError(errors_js_1.LinkErrorCode.InternalError, 'Invalid session key length');
69
+ }
70
+ }
71
+ /** Encrypt arbitrary plaintext bytes into a frame with a fresh random nonce. */
72
+ function seal(plaintext, key) {
73
+ assertKey(key);
74
+ const nonce = tweetnacl_1.default.randomBytes(exports.NONCE_LENGTH);
75
+ const ct = tweetnacl_1.default.secretbox(plaintext, nonce, key);
76
+ return { nonce: (0, encoding_js_1.bytesToBase64)(nonce), ct: (0, encoding_js_1.bytesToBase64)(ct) };
77
+ }
78
+ /** Decrypt a frame back to plaintext bytes. Throws on any tampering/wrong-key (authenticated
79
+ * encryption: `secretbox.open` returns null, which we surface as an error). */
80
+ function open(frame, key) {
81
+ assertKey(key);
82
+ const nonce = (0, encoding_js_1.base64ToBytes)(frame.nonce);
83
+ if (nonce.length !== exports.NONCE_LENGTH) {
84
+ throw new errors_js_1.LinkError(errors_js_1.LinkErrorCode.InvalidRequest, 'Invalid frame nonce length');
85
+ }
86
+ const ct = (0, encoding_js_1.base64ToBytes)(frame.ct);
87
+ const plaintext = tweetnacl_1.default.secretbox.open(ct, nonce, key);
88
+ if (plaintext === null) {
89
+ throw new errors_js_1.LinkError(errors_js_1.LinkErrorCode.InvalidRequest, 'Failed to decrypt Phantasma Link frame');
90
+ }
91
+ return plaintext;
92
+ }
93
+ /** Encrypt an envelope's JSON text into a frame. */
94
+ function sealEnvelopeText(json, key) {
95
+ return seal((0, encoding_js_1.utf8ToBytes)(json), key);
96
+ }
97
+ /** Decrypt a frame back into the envelope's JSON text. */
98
+ function openEnvelopeText(frame, key) {
99
+ return (0, encoding_js_1.bytesToUtf8)(open(frame, key));
100
+ }
101
+ /** Generate the 32 random bytes a wallet prepends in `signMessage`. */
102
+ function generateSignMessageRandom() {
103
+ return tweetnacl_1.default.randomBytes(exports.SIGN_MESSAGE_RANDOM_LENGTH);
104
+ }
105
+ /**
106
+ * Build the exact byte string a wallet signs for `pha_signMessage`:
107
+ * `DOMAIN_TAG || random || message` (spec §8). A verifier reconstructs the same bytes from
108
+ * the returned `random` + the original `message`. Keeping this in one place guarantees the
109
+ * wallet and any verifier agree on the layout.
110
+ */
111
+ function buildSignMessagePayload(message, random) {
112
+ if (random.length !== exports.SIGN_MESSAGE_RANDOM_LENGTH) {
113
+ throw new errors_js_1.LinkError(errors_js_1.LinkErrorCode.InvalidParams, `signMessage random must be ${exports.SIGN_MESSAGE_RANDOM_LENGTH} bytes`);
114
+ }
115
+ const out = new Uint8Array(exports.SIGN_MESSAGE_DOMAIN_TAG.length + random.length + message.length);
116
+ out.set(exports.SIGN_MESSAGE_DOMAIN_TAG, 0);
117
+ out.set(random, exports.SIGN_MESSAGE_DOMAIN_TAG.length);
118
+ out.set(message, exports.SIGN_MESSAGE_DOMAIN_TAG.length + random.length);
119
+ return out;
120
+ }
@@ -0,0 +1,208 @@
1
+ "use strict";
2
+ // Phantasma Link v5 - transport abstraction + session client (spec §6). The SAME envelope
3
+ // rides over any transport (injected / loopback / deeplink / relay); a transport only moves
4
+ // opaque frame strings. The session client owns request/response correlation by `id`, event
5
+ // dispatch, optional channel encryption, and timeouts - so each concrete transport stays
6
+ // tiny. Concrete transports are implemented in later phases.
7
+ Object.defineProperty(exports, "__esModule", { value: true });
8
+ exports.LinkSessionClient = void 0;
9
+ const protocol_js_1 = require("./protocol.js");
10
+ const errors_js_1 = require("./errors.js");
11
+ const envelope_js_1 = require("./envelope.js");
12
+ const session_crypto_js_1 = require("./session-crypto.js");
13
+ /**
14
+ * Drives a v5 session over a {@link LinkTransport}. `request()` sends a typed envelope and
15
+ * resolves with the wallet's `result` (or rejects with a {@link LinkError}); incoming events
16
+ * are dispatched to {@link onEvent} handlers. Encryption is transparent: when a session key
17
+ * is set, every outgoing envelope is sealed and every incoming frame is opened.
18
+ */
19
+ class LinkSessionClient {
20
+ constructor(transport, options = {}) {
21
+ this.pending = new Map();
22
+ this.eventHandlers = new Set();
23
+ this.closed = false;
24
+ this.transport = transport;
25
+ this.sessionKey = options.sessionKey;
26
+ this.requireSessionKey = options.requireSessionKey ?? false;
27
+ this.sessionId = options.sessionId;
28
+ this.requestTimeoutMs = options.requestTimeoutMs ?? 60000;
29
+ this.onUnmatchedResponse = options.onUnmatchedResponse;
30
+ transport.onMessage((frame) => this.handleFrame(frame));
31
+ transport.onClose?.((reason) => this.handleClose(reason));
32
+ }
33
+ /** Set/refresh the session id sent on subsequent requests (e.g. after `pha_connect`),
34
+ * or clear it with `undefined` (e.g. after `pha_disconnect`). */
35
+ setSessionId(id) {
36
+ this.sessionId = id;
37
+ }
38
+ /** The current session id, if a session is established. */
39
+ getSessionId() {
40
+ return this.sessionId;
41
+ }
42
+ /** Install the channel key once it is established (ecdh pairing derives it only after
43
+ * the wallet's ephemeral public key arrives, spec §18.1). From here on every frame is
44
+ * sealed/opened with it, exactly as if it had been passed at construction. */
45
+ setSessionKey(key) {
46
+ if (key.length !== 32) {
47
+ throw new errors_js_1.LinkError(errors_js_1.LinkErrorCode.InternalError, 'Invalid session key length');
48
+ }
49
+ this.sessionKey = key;
50
+ }
51
+ /** Subscribe to wallet->dApp events; returns an unsubscribe function. */
52
+ onEvent(handler) {
53
+ this.eventHandlers.add(handler);
54
+ return () => {
55
+ this.eventHandlers.delete(handler);
56
+ };
57
+ }
58
+ /** Send a request and await its result. Rejects with {@link LinkError}. */
59
+ request(method, params) {
60
+ if (this.closed) {
61
+ return Promise.reject(new errors_js_1.LinkError(errors_js_1.LinkErrorCode.Disconnected, 'Phantasma Link transport is closed'));
62
+ }
63
+ // The ecdh flow forbids plaintext: until the wallet's key hop arrives there is no
64
+ // channel key, and nothing may be sent (spec §8: relay payloads are always sealed).
65
+ if (this.requireSessionKey && !this.sessionKey) {
66
+ return Promise.reject(new errors_js_1.LinkError(errors_js_1.LinkErrorCode.Unauthorized, 'Channel key not established yet; complete the pairing first'));
67
+ }
68
+ const id = (0, session_crypto_js_1.randomToken)();
69
+ const envelope = {
70
+ plv: protocol_js_1.PLV,
71
+ id,
72
+ method,
73
+ ...(this.sessionId ? { session: this.sessionId } : {}),
74
+ ...(params ? { params } : {}),
75
+ };
76
+ const promise = new Promise((resolve, reject) => {
77
+ const pending = { resolve, reject };
78
+ if (this.requestTimeoutMs > 0) {
79
+ pending.timer = setTimeout(() => {
80
+ this.pending.delete(id);
81
+ reject(new errors_js_1.LinkError(errors_js_1.LinkErrorCode.InternalError, `Phantasma Link request "${method}" timed out`));
82
+ }, this.requestTimeoutMs);
83
+ }
84
+ this.pending.set(id, pending);
85
+ });
86
+ // Send after registering the pending entry so a synchronous transport error settles it.
87
+ Promise.resolve(this.transport.send(this.encodeOutgoing(envelope))).catch((err) => {
88
+ this.settleError(id, new errors_js_1.LinkError(errors_js_1.LinkErrorCode.Disconnected, err instanceof Error ? err.message : 'Failed to send request to wallet'));
89
+ });
90
+ return promise;
91
+ }
92
+ /** Close the session, rejecting all in-flight requests. */
93
+ close() {
94
+ if (this.closed) {
95
+ return;
96
+ }
97
+ this.closed = true;
98
+ this.transport.close();
99
+ this.rejectAll('Phantasma Link transport closed');
100
+ }
101
+ encodeOutgoing(message) {
102
+ const json = (0, envelope_js_1.encodeEnvelope)(message);
103
+ if (!this.sessionKey) {
104
+ return json;
105
+ }
106
+ return JSON.stringify((0, session_crypto_js_1.sealEnvelopeText)(json, this.sessionKey));
107
+ }
108
+ decodeIncoming(frame) {
109
+ if (!this.sessionKey) {
110
+ // Mirror of the sending guard: a key-requiring channel must not accept plaintext
111
+ // either - anything arriving before the key hop is forged by definition.
112
+ if (this.requireSessionKey) {
113
+ throw new errors_js_1.LinkError(errors_js_1.LinkErrorCode.Unauthorized, 'Channel key not established yet');
114
+ }
115
+ return (0, envelope_js_1.decodeEnvelope)(frame);
116
+ }
117
+ let parsed;
118
+ try {
119
+ parsed = JSON.parse(frame);
120
+ }
121
+ catch {
122
+ throw new errors_js_1.LinkError(errors_js_1.LinkErrorCode.ParseError, 'Encrypted frame is not valid JSON');
123
+ }
124
+ return (0, envelope_js_1.decodeEnvelope)((0, session_crypto_js_1.openEnvelopeText)(parsed, this.sessionKey));
125
+ }
126
+ handleFrame(frame) {
127
+ let message;
128
+ try {
129
+ message = this.decodeIncoming(frame);
130
+ }
131
+ catch {
132
+ // Drop undecodable/forged frames; a real authenticated peer never sends these.
133
+ return;
134
+ }
135
+ if ((0, envelope_js_1.isLinkEvent)(message)) {
136
+ this.emitEvent(message);
137
+ return;
138
+ }
139
+ const id = message.id;
140
+ if (typeof id !== 'string') {
141
+ return;
142
+ }
143
+ const pending = this.pending.get(id);
144
+ if (!pending) {
145
+ // No in-flight request matches this id. On deeplink this means the page reloaded while
146
+ // the wallet was open, so the original request promise was lost with the old page. Hand
147
+ // the answer to the optional sink instead of dropping it; same-page flows always have a
148
+ // pending entry, so this path is reload-only.
149
+ if (this.onUnmatchedResponse) {
150
+ if ((0, envelope_js_1.isLinkErrorResponse)(message)) {
151
+ this.onUnmatchedResponse({ id, ok: false, error: message.error });
152
+ }
153
+ else if ((0, envelope_js_1.isLinkSuccessResponse)(message)) {
154
+ this.onUnmatchedResponse({ id, ok: true, result: message.result });
155
+ }
156
+ }
157
+ return;
158
+ }
159
+ this.pending.delete(id);
160
+ if (pending.timer) {
161
+ clearTimeout(pending.timer);
162
+ }
163
+ if ((0, envelope_js_1.isLinkErrorResponse)(message)) {
164
+ pending.reject(errors_js_1.LinkError.fromObject(message.error));
165
+ }
166
+ else if ((0, envelope_js_1.isLinkSuccessResponse)(message)) {
167
+ pending.resolve(message.result);
168
+ }
169
+ else {
170
+ pending.reject(new errors_js_1.LinkError(errors_js_1.LinkErrorCode.InternalError, 'Malformed Phantasma Link response'));
171
+ }
172
+ }
173
+ emitEvent(message) {
174
+ for (const handler of this.eventHandlers) {
175
+ try {
176
+ handler(message.event, message.data, message.session);
177
+ }
178
+ catch {
179
+ // Isolate handler exceptions so one bad listener can't break dispatch.
180
+ }
181
+ }
182
+ }
183
+ handleClose(reason) {
184
+ this.closed = true;
185
+ this.rejectAll(reason ?? 'Phantasma Link transport closed');
186
+ }
187
+ settleError(id, err) {
188
+ const pending = this.pending.get(id);
189
+ if (!pending) {
190
+ return;
191
+ }
192
+ this.pending.delete(id);
193
+ if (pending.timer) {
194
+ clearTimeout(pending.timer);
195
+ }
196
+ pending.reject(err);
197
+ }
198
+ rejectAll(reason) {
199
+ for (const [id, pending] of this.pending) {
200
+ if (pending.timer) {
201
+ clearTimeout(pending.timer);
202
+ }
203
+ pending.reject(new errors_js_1.LinkError(errors_js_1.LinkErrorCode.Disconnected, reason));
204
+ this.pending.delete(id);
205
+ }
206
+ }
207
+ }
208
+ exports.LinkSessionClient = LinkSessionClient;
@@ -0,0 +1,141 @@
1
+ "use strict";
2
+ // Phantasma Link v5 - web-dApp deeplink glue (spec §15/§17). A web page that talks to the
3
+ // wallet over deeplink needs the same few pieces every time: pairing material (topic +
4
+ // session key), a pairing URI to show, persistence that survives the page being unloaded
5
+ // while the wallet is in the foreground, and intake of the response URLs the wallet opens
6
+ // back at the page. This module holds those pieces; the orchestration is the
7
+ // `PhantasmaLink5.webDeeplink()` factory (client.ts), NOT a separate wrapper class.
8
+ //
9
+ // Everything platform-specific is injectable so the flow is fully testable without a
10
+ // browser; the defaults bind to the real browser surface (localStorage, location.href,
11
+ // hashchange, history.replaceState).
12
+ Object.defineProperty(exports, "__esModule", { value: true });
13
+ exports.WEB_DEEPLINK_STORAGE_KEY = void 0;
14
+ exports.resolveWebDeeplinkHooks = resolveWebDeeplinkHooks;
15
+ exports.stripUrlFragment = stripUrlFragment;
16
+ exports.createWebDeeplinkRecord = createWebDeeplinkRecord;
17
+ exports.loadWebDeeplinkRecord = loadWebDeeplinkRecord;
18
+ exports.saveWebDeeplinkRecord = saveWebDeeplinkRecord;
19
+ const errors_js_1 = require("./errors.js");
20
+ const encoding_js_1 = require("./encoding.js");
21
+ const session_crypto_js_1 = require("./session-crypto.js");
22
+ /** Default storage key for the persisted {@link WebDeeplinkRecord}. */
23
+ exports.WEB_DEEPLINK_STORAGE_KEY = 'phantasma.link.v5.webdeeplink';
24
+ /** Merge options with browser defaults; throw a clear error when a required hook has no
25
+ * default in the current environment (e.g. running outside a browser without injection). */
26
+ function resolveWebDeeplinkHooks(options) {
27
+ const browser = globalThis;
28
+ // Reading `localStorage` itself can throw in some privacy modes; treat that as absent.
29
+ let defaultStorage;
30
+ try {
31
+ defaultStorage = browser.localStorage;
32
+ }
33
+ catch {
34
+ defaultStorage = undefined;
35
+ }
36
+ const storage = options.storage ?? defaultStorage;
37
+ if (!storage) {
38
+ throw new errors_js_1.LinkError(errors_js_1.LinkErrorCode.InvalidParams, 'webDeeplink requires `storage` when localStorage is unavailable');
39
+ }
40
+ const location = browser.location;
41
+ const opener = options.opener ??
42
+ (location
43
+ ? (url) => {
44
+ location.href = url;
45
+ }
46
+ : undefined);
47
+ if (!opener) {
48
+ throw new errors_js_1.LinkError(errors_js_1.LinkErrorCode.InvalidParams, 'webDeeplink requires `opener` when location is unavailable');
49
+ }
50
+ const pageUrl = options.pageUrl ?? (location ? () => location.href : undefined);
51
+ if (!pageUrl) {
52
+ throw new errors_js_1.LinkError(errors_js_1.LinkErrorCode.InvalidParams, 'webDeeplink requires `pageUrl` when location is unavailable');
53
+ }
54
+ // Optional hooks: missing defaults degrade features (no automatic intake / no URL
55
+ // cleanup) instead of failing - a non-browser embedder can drive deliverUrl() manually.
56
+ let onUrlChange = options.onUrlChange;
57
+ if (!onUrlChange && browser.addEventListener && browser.removeEventListener) {
58
+ const add = browser.addEventListener.bind(globalThis);
59
+ const remove = browser.removeEventListener.bind(globalThis);
60
+ onUrlChange = (handler) => {
61
+ add('hashchange', handler);
62
+ return () => remove('hashchange', handler);
63
+ };
64
+ }
65
+ let replaceUrl = options.replaceUrl;
66
+ const history = browser.history;
67
+ if (!replaceUrl && history) {
68
+ replaceUrl = (url) => history.replaceState(null, '', url);
69
+ }
70
+ return {
71
+ storage,
72
+ storageKey: options.storageKey ?? exports.WEB_DEEPLINK_STORAGE_KEY,
73
+ opener,
74
+ pageUrl,
75
+ onUrlChange,
76
+ replaceUrl,
77
+ };
78
+ }
79
+ /** A URL with its fragment removed (the callback base / the cleaned page URL). */
80
+ function stripUrlFragment(url) {
81
+ const hashIndex = url.indexOf('#');
82
+ return hashIndex < 0 ? url : url.slice(0, hashIndex);
83
+ }
84
+ /** Generate fresh pairing material for a new channel (spec §15: 32-byte topic + key). */
85
+ function createWebDeeplinkRecord(callback) {
86
+ return {
87
+ v: 1,
88
+ topic: (0, session_crypto_js_1.randomToken)(32),
89
+ key: (0, encoding_js_1.bytesToBase64Url)((0, session_crypto_js_1.generateSessionKey)()),
90
+ callback,
91
+ };
92
+ }
93
+ /** Load and validate the persisted record; any malformed/corrupt state is discarded (the
94
+ * caller re-pairs fresh) rather than surfaced as an error - storage is self-healing. */
95
+ function loadWebDeeplinkRecord(storage, storageKey) {
96
+ const raw = storage.getItem(storageKey);
97
+ if (!raw) {
98
+ return null;
99
+ }
100
+ let parsed;
101
+ try {
102
+ parsed = JSON.parse(raw);
103
+ }
104
+ catch {
105
+ return null;
106
+ }
107
+ if (typeof parsed !== 'object' || parsed === null) {
108
+ return null;
109
+ }
110
+ const record = parsed;
111
+ if (record.v !== 1) {
112
+ return null;
113
+ }
114
+ if (typeof record.topic !== 'string' || record.topic.length === 0) {
115
+ return null;
116
+ }
117
+ if (typeof record.callback !== 'string' || record.callback.length === 0) {
118
+ return null;
119
+ }
120
+ if (record.sessionId !== undefined && typeof record.sessionId !== 'string') {
121
+ return null;
122
+ }
123
+ // The key must decode to exactly one secretbox key, or nothing sealed with it could
124
+ // ever be opened - regenerating the pairing is the only way forward.
125
+ if (typeof record.key !== 'string') {
126
+ return null;
127
+ }
128
+ try {
129
+ if ((0, encoding_js_1.base64UrlToBytes)(record.key).length !== session_crypto_js_1.SESSION_KEY_LENGTH) {
130
+ return null;
131
+ }
132
+ }
133
+ catch {
134
+ return null;
135
+ }
136
+ return record;
137
+ }
138
+ /** Persist the record (single JSON blob under the storage key). */
139
+ function saveWebDeeplinkRecord(storage, storageKey, record) {
140
+ storage.setItem(storageKey, JSON.stringify(record));
141
+ }
@@ -1,9 +1,42 @@
1
1
  "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
2
35
  Object.defineProperty(exports, "__esModule", { value: true });
3
36
  exports.PollChoice = exports.PhantasmaKeys = exports.PBinaryWriter = exports.PBinaryReader = exports.OrganizationTrigger = exports.Entropy = exports.Ed25519Signature = exports.DomainSettings = exports.Describer = exports.CustomSerializer = exports.ContractParameter = exports.ContractMethod = exports.ContractInterface = exports.ContractEvent = exports.ConsensusMode = exports.CarbonBinaryWriter = exports.CarbonBinaryReader = exports.Base16 = exports.AddressKind = exports.Address = exports.AccountTrigger = exports.getTransactionSettleEventData = exports.getTokenEventData = exports.getString = exports.getMarketEventData = exports.getInfusionEventData = exports.getGasEventData = exports.getChainValueEventData = exports.decodeVMObject = exports.VMType = exports.VMObject = exports.TypeAuction = exports.ScriptBuilder = exports.Opcode = exports.EventKind = exports.Decoder = exports.Contracts = exports.Transaction = exports.ExecutionState = exports.normalizeContractName = exports.coerceContractBytes = exports.buildContractArtifactManifest = exports.buildContractArtifactBundle = exports.ContractTxHelper = exports.vmVariableSchemaFromRpcResult = exports.vmStructSchemaFromRpcResult = exports.unwrapRpcResult = exports.isRpcErrorResult = exports.getRpcErrorMessage = exports.PhantasmaAPI = void 0;
4
37
  exports.TxMsgBurnNonFungibleGasPayer = exports.TxMsgBurnNonFungible = exports.TxMsgBurnFungibleGasPayer = exports.TxMsgBurnFungible = exports.TxMsg = exports.TokenSeriesMetadataBuilder = exports.TokenSchemasJson = exports.TokenSchemasBuilder = exports.TokenSchemas = exports.TokenMetadataBuilder = exports.TokenInfoBuilder = exports.TokenInfo = exports.TokenHelper = exports.TokenContractMethods = exports.StandardMeta = exports.SmallString = exports.SignedTxMsg = exports.SeriesInfoBuilder = exports.SeriesInfo = exports.PhantasmaNftMintResult = exports.PhantasmaNftMintInfo = exports.PhantasmaNftRomBuilder = exports.NftRomBuilder = exports.ModuleId = exports.MintPhantasmaNonFungibleTxHelper = exports.MintNonFungibleTxHelper = exports.MetadataField = exports.IntX = exports.FieldType = exports.CreateTokenTxHelper = exports.CreateTokenSeriesTxHelper = exports.CarbonTokenFlags = exports.CarbonBlob = exports.Bytes64 = exports.Bytes32 = exports.Bytes16 = exports.writeBlob = exports.twosComplementLEToBigInt = exports.readBlob = exports.bigIntToTwosComplementLE = exports.TriggerResult = exports.TokenTrigger = exports.Timestamp = exports.StakeReward = exports.Stack = exports.Serialization = exports.PollVote = exports.PollValue = exports.PollState = exports.PollPresence = void 0;
5
38
  exports.reverseHex = exports.numberToByteArray = exports.isValidTicker = exports.isValidIdentifier = exports.isReservedIdentifier = exports.hexToBytes = exports.hexStringToUint8Array = exports.getDifficulty = exports.encodeBase16 = exports.decodeBase16 = exports.bytesToHex = exports.bigIntToByteArray = exports.arrayNumberToUint8Array = exports.NULL_NAME = exports.GENESIS_NAME = exports.ENTRY_CONTEXT_NAME = exports.ANONYMOUS_NAME = exports.standardMetadataFields = exports.seriesDefaultMetadataFields = exports.pushMetadataField = exports.parseTokenSchemasJson = exports.nftDefaultMetadataFields = exports.findMetadataField = exports.Witness = exports.VmVariableSchema = exports.VmType = exports.VmStructSchema = exports.VmStructFlags = exports.VmStructArray = exports.VmNamedVariableSchema = exports.VmNamedDynamicVariable = exports.VmDynamicVariable = exports.VmDynamicStruct = exports.TokenListing = exports.TxTypes = exports.TxMsgTransferNonFungibleSingleGasPayer = exports.TxMsgTransferNonFungibleSingle = exports.TxMsgTransferNonFungibleMultiGasPayer = exports.TxMsgTransferNonFungibleMulti = exports.TxMsgTransferFungibleGasPayer = exports.TxMsgTransferFungible = exports.TxMsgTrade = exports.TxMsgSpecialResolution = exports.TxMsgSigner = exports.TxMsgPhantasmaRaw = exports.TxMsgPhantasma = exports.TxMsgMintNonFungible = exports.TxMsgMintFungible = exports.TxMsgCallMulti = exports.TxMsgCall = void 0;
6
- exports.getAddressPublicKeyFromPublicKey = exports.getAddressFromPublicKey = exports.getAddressFromPrivateKey = exports.getAddressFromLedger = exports.TokenSeriesMode = exports.TokenFlags = exports.NativeContractKind = exports.SignatureKind = exports.Signature = exports.ProofOfWork = exports.PhantasmaLink = exports.Nexus = exports.EasyScript = exports.EasyConnect = exports.verifyData = exports.signData = exports.getWifFromPrivateKey = exports.getPublicKeyFromPrivateKey = exports.getPrivateKeyFromWif = exports.getAddressFromWif = exports.generateNewWif = exports.generateNewSeedWords = exports.generateNewSeed = exports.uint8ArrayToStringDefault = exports.uint8ArrayToString = exports.uint8ArrayToNumberArray = exports.uint8ArrayToBytes = exports.stringToUint8Array = exports.setLogger = void 0;
39
+ exports.getAddressPublicKeyFromPublicKey = exports.getAddressFromPublicKey = exports.getAddressFromPrivateKey = exports.getAddressFromLedger = exports.TokenSeriesMode = exports.TokenFlags = exports.NativeContractKind = exports.SignatureKind = exports.Signature = exports.PhantasmaLinkV5 = exports.ProofOfWork = exports.PhantasmaLink = exports.Nexus = exports.EasyScript = exports.EasyConnect = exports.verifyData = exports.signData = exports.getWifFromPrivateKey = exports.getPublicKeyFromPrivateKey = exports.getPrivateKeyFromWif = exports.getAddressFromWif = exports.generateNewWif = exports.generateNewSeedWords = exports.generateNewSeed = exports.uint8ArrayToStringDefault = exports.uint8ArrayToString = exports.uint8ArrayToNumberArray = exports.uint8ArrayToBytes = exports.stringToUint8Array = exports.setLogger = void 0;
7
40
  var phantasma_js_1 = require("./rpc/phantasma.js");
8
41
  Object.defineProperty(exports, "PhantasmaAPI", { enumerable: true, get: function () { return phantasma_js_1.PhantasmaAPI; } });
9
42
  var rpc_result_js_1 = require("./rpc/rpc-result.js");
@@ -187,6 +220,9 @@ var phantasma_link_js_1 = require("./link/phantasma-link.js");
187
220
  Object.defineProperty(exports, "PhantasmaLink", { enumerable: true, get: function () { return phantasma_link_js_1.PhantasmaLink; } });
188
221
  var proof_of_work_js_1 = require("./link/interfaces/proof-of-work.js");
189
222
  Object.defineProperty(exports, "ProofOfWork", { enumerable: true, get: function () { return proof_of_work_js_1.ProofOfWork; } });
223
+ // Phantasma Link v5 (new generation). Namespaced because v5 defines its own `SignatureKind`
224
+ // (string union) distinct from the contract `SignatureKind` enum exported flatly above.
225
+ exports.PhantasmaLinkV5 = __importStar(require("./link/v5/index.js"));
190
226
  var signature_js_1 = require("./interfaces/signature.js");
191
227
  Object.defineProperty(exports, "Signature", { enumerable: true, get: function () { return signature_js_1.Signature; } });
192
228
  Object.defineProperty(exports, "SignatureKind", { enumerable: true, get: function () { return signature_js_1.SignatureKind; } });
package/dist/esm/index.js CHANGED
@@ -2,4 +2,6 @@ export * from './core/index.js';
2
2
  export * as PhantasmaTS from './core/index.js';
3
3
  export { PhantasmaLink } from './link/phantasma-link.js';
4
4
  export { EasyConnect } from './link/easy-connect.js';
5
+ // Phantasma Link v5 (new generation), namespaced to avoid flat-name collisions.
6
+ export * as PhantasmaLinkV5 from './link/v5/index.js';
5
7
  //const TransportWebUSB = require('@ledgerhq/hw-transport-webusb').default;
@@ -1 +1,6 @@
1
1
  export * from './interfaces/index.js';
2
+ // Phantasma Link v5 (new generation; parallel to the legacy v1–v4 `PhantasmaLink`).
3
+ // NOT re-exported flatly here: v5 defines its own `SignatureKind` (string union) that
4
+ // would clash with the contract `SignatureKind` enum aggregated by `src/core/index.ts`.
5
+ // Import v5 via the `phantasma-sdk-ts/link/v5` subpath or the `PhantasmaLinkV5` namespace
6
+ // (exported from the package root and `phantasma-sdk-ts/public`).
@@ -0,0 +1,5 @@
1
+ // Phantasma Link v5 - capability handshake + session/account/chain types (spec §5, §7, §9).
2
+ // Capability negotiation replaces the v1-v4 "magic version int": a dApp learns up front
3
+ // which methods, chains, tx formats, signature kinds, and payload sizes the wallet
4
+ // supports, instead of guessing from a single number.
5
+ export {};