@paydlux/agent 0.1.9 → 0.1.11

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.
Binary file
Binary file
@@ -4,88 +4,188 @@
4
4
  <meta charset="utf-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
6
6
  <meta name="theme-color" content="#0b0f14">
7
+ <meta name="color-scheme" content="dark">
8
+ <meta name="apple-mobile-web-app-capable" content="yes">
9
+ <meta name="mobile-web-app-capable" content="yes">
10
+ <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
11
+ <meta name="apple-mobile-web-app-title" content="Paydlux">
12
+ <meta name="format-detection" content="telephone=no">
7
13
  <title>Paydlux Approver</title>
14
+ <link rel="manifest" href="manifest.webmanifest">
15
+ <link rel="icon" href="icon-192.png" sizes="192x192">
16
+ <link rel="apple-touch-icon" href="icon-192.png">
8
17
  <!--
9
- The approver half of a Paydlux vault.
18
+ The approver half of a Paydlux vault: a phone app, not a page you paste into.
10
19
 
11
- This page holds ONE key: the approver's. It cannot spend anything by itself.
12
- It reads a pending payment, shows a human what they are approving, and signs
13
- the transaction hash. Signing is a purely local operation: no network call can
14
- change what was signed.
20
+ This app holds ONE key: the approver's. It cannot spend anything by itself.
21
+ It lists the payments waiting on you, shows a human what they are approving,
22
+ and signs the transaction hash. Signing is a purely local operation: no
23
+ network call can change what was signed.
15
24
 
16
- The page talks to one origin only — the desk that served it — to fetch the
25
+ The app talks to one origin only — the desk that served it — to fetch the
17
26
  pending transaction and hand back the signature. connect-src 'self' in the
18
27
  desk's Content-Security-Policy enforces that.
19
28
 
20
29
  What the key model actually guarantees:
21
30
  - the seed becomes a NON-extractable WebCrypto key; after enrolment nothing
22
- on this page, including an injected script, can read it back;
31
+ on this page, including an injected script, can read the seed back;
23
32
  - the signature covers the transaction hash, so a changed amount or payee
24
33
  produces a different hash and a signature the ledger refuses;
25
34
  - the approver key alone cannot move funds — the vault needs 2 signatures.
26
35
 
27
36
  What it does NOT guarantee: a person who roots this device, or who approves a
28
37
  transaction without reading it. The human reading the amount and payee is part
29
- of the security model, not a UX step.
38
+ of the security model, not a UX step. That is why Approve is a press-and-hold
39
+ and Reject is a single tap: the safe answer is always the cheap one.
30
40
 
31
- Serve it over http(s) on a device you control: npx tsx tools/phone-serve.ts
41
+ The static shell (this file, app.js, xdr.js, sw.js, the icons) carries no
42
+ secrets and no payment data, so the desk serves it without the portal key —
43
+ that is what lets it be installed to a home screen and cached for offline
44
+ launch. Every route that carries data or moves money still needs the key.
32
45
  -->
33
46
  <style>
34
47
  :root {
35
- --bg:#0b0f14; --panel:#121821; --line:#22303f; --ink:#e8eef6; --dim:#8fa3b8;
36
- --ok:#3ddc97; --warn:#ffb454; --bad:#ff6b6b; --accent:#4da3ff;
48
+ --bg:#0b0f14; --panel:#121821; --panel2:#0d131a; --line:#22303f; --ink:#e8eef6;
49
+ --dim:#8fa3b8; --ok:#3ddc97; --warn:#ffb454; --bad:#ff6b6b; --accent:#4da3ff;
50
+ --r:16px;
37
51
  }
38
- * { box-sizing:border-box }
52
+ @media (prefers-color-scheme: light) { /* the app is always dark: a light
53
+ approval screen next to a bank app is how a person taps the wrong one */ }
54
+ * { box-sizing:border-box; -webkit-tap-highlight-color:transparent }
55
+ html { -webkit-text-size-adjust:100% }
39
56
  body {
40
- margin:0; background:var(--bg); color:var(--ink); font:16px/1.5 ui-sans-serif,system-ui,-apple-system,sans-serif;
41
- padding:max(16px, env(safe-area-inset-top)) 16px max(24px, env(safe-area-inset-bottom));
57
+ margin:0; background:var(--bg); color:var(--ink);
58
+ font:17px/1.45 -apple-system,BlinkMacSystemFont,"SF Pro Text",ui-sans-serif,system-ui,sans-serif;
59
+ padding:max(14px, env(safe-area-inset-top)) 14px max(28px, env(safe-area-inset-bottom));
42
60
  max-width:560px; margin-inline:auto;
61
+ overscroll-behavior-y:none;
43
62
  }
44
- h1 { font-size:17px; margin:0 0 4px; letter-spacing:.02em }
45
- .sub { color:var(--dim); font-size:13px; margin-bottom:18px }
46
- .panel { background:var(--panel); border:1px solid var(--line); border-radius:14px; padding:14px; margin-bottom:14px }
47
- label { display:block; font-size:12px; color:var(--dim); margin:10px 0 4px; text-transform:uppercase; letter-spacing:.06em }
63
+ /* installed-to-home-screen: no browser chrome, so the header carries the
64
+ identity and the status itself */
65
+ .brand { display:flex; align-items:center; gap:10px; margin:2px 2px 14px }
66
+ .brand img { width:30px; height:30px; border-radius:8px }
67
+ .brand h1 { font-size:19px; margin:0; letter-spacing:.01em; font-weight:700 }
68
+ .brand .sub { color:var(--dim); font-size:12.5px; margin-top:1px }
69
+ .net { margin-left:auto; font-size:11px; letter-spacing:.07em; text-transform:uppercase;
70
+ color:var(--dim); border:1px solid var(--line); border-radius:999px; padding:4px 9px }
71
+
72
+ .panel { background:var(--panel); border:1px solid var(--line); border-radius:var(--r);
73
+ padding:16px; margin-bottom:14px }
74
+ label { display:block; font-size:11.5px; color:var(--dim); margin:12px 0 5px;
75
+ text-transform:uppercase; letter-spacing:.07em; font-weight:600 }
48
76
  textarea, input {
49
- width:100%; background:#0d131a; color:var(--ink); border:1px solid var(--line);
50
- border-radius:10px; padding:10px; font:13px/1.4 ui-monospace,SFMono-Regular,Menlo,monospace;
51
- word-break:break-all;
77
+ width:100%; background:var(--panel2); color:var(--ink); border:1px solid var(--line);
78
+ border-radius:12px; padding:13px; font:15px/1.4 ui-monospace,SFMono-Regular,Menlo,monospace;
79
+ word-break:break-all; -webkit-appearance:none; appearance:none;
52
80
  }
53
- textarea { min-height:84px; resize:vertical }
81
+ input:focus, textarea:focus { outline:none; border-color:var(--accent) }
82
+ textarea { min-height:92px; resize:vertical }
83
+
84
+ /* 44px minimum tap target — iOS Human Interface guideline, and a person
85
+ approving money with one thumb in the dark deserves it */
54
86
  button {
55
- width:100%; margin-top:12px; padding:14px; border:0; border-radius:12px;
56
- background:var(--accent); color:#04121f; font-size:16px; font-weight:700; cursor:pointer;
87
+ width:100%; margin-top:12px; min-height:52px; padding:15px 18px; border:0; border-radius:14px;
88
+ background:var(--accent); color:#04121f; font-size:17px; font-weight:700; cursor:pointer;
89
+ font-family:inherit; -webkit-user-select:none; user-select:none; touch-action:manipulation;
57
90
  }
58
91
  button.ghost { background:transparent; color:var(--dim); border:1px solid var(--line); font-weight:600 }
59
- button.danger { background:var(--bad); color:#2a0707 }
92
+ button.danger { background:rgba(255,107,107,.14); color:var(--bad); border:1px solid rgba(255,107,107,.3) }
60
93
  button:disabled { opacity:.4 }
61
94
  .row { display:flex; gap:10px }
62
95
  .row > * { flex:1 }
63
- .kv { display:grid; grid-template-columns:96px 1fr; gap:6px 10px; font-size:14px }
64
- .kv dt { color:var(--dim); font-size:12px; text-transform:uppercase; letter-spacing:.06em; padding-top:2px }
65
- .kv dd { margin:0; font-family:ui-monospace,Menlo,monospace; word-break:break-all; font-size:13px }
66
- .amount { font-size:34px; font-weight:800; margin:2px 0 10px; font-variant-numeric:tabular-nums }
67
- .tag { display:inline-block; font-size:11px; padding:3px 8px; border-radius:999px; letter-spacing:.06em; text-transform:uppercase }
96
+
97
+ .kv { display:grid; grid-template-columns:88px 1fr; gap:8px 12px; font-size:15px; margin:0 }
98
+ .kv dt { color:var(--dim); font-size:11.5px; text-transform:uppercase; letter-spacing:.07em;
99
+ padding-top:3px; font-weight:600 }
100
+ .kv dd { margin:0; font-family:ui-monospace,Menlo,monospace; word-break:break-all; font-size:13.5px }
101
+ .amount { font-size:44px; font-weight:800; margin:2px 0 4px; font-variant-numeric:tabular-nums;
102
+ letter-spacing:-.02em; line-height:1.1 }
103
+ .amount .tk { font-size:20px; font-weight:600; color:var(--dim); letter-spacing:0 }
104
+ .tag { display:inline-block; font-size:11px; font-weight:600; padding:4px 9px; border-radius:999px;
105
+ letter-spacing:.07em; text-transform:uppercase }
68
106
  .tag.ok { background:rgba(61,220,151,.14); color:var(--ok) }
69
107
  .tag.warn { background:rgba(255,180,84,.14); color:var(--warn) }
70
- .tag.bad { background:rgba(255,107,107,.14); color:var(--bad) }
71
- .err { color:var(--bad); font-size:13px; margin-top:8px; word-break:break-word }
72
- .hint { color:var(--dim); font-size:12px; margin-top:8px }
108
+ .tag.bad { background:rgba(257,107,107,.14); color:var(--bad) }
109
+ .err { color:var(--bad); font-size:14px; margin-top:10px; word-break:break-word; white-space:pre-wrap }
110
+ .hint { color:var(--dim); font-size:13px; margin-top:10px }
73
111
  .hidden { display:none }
74
- .out { background:#0d131a; border:1px solid var(--line); border-radius:10px; padding:10px;
112
+ .out { background:var(--panel2); border:1px solid var(--line); border-radius:12px; padding:12px;
75
113
  font:12px/1.4 ui-monospace,Menlo,monospace; word-break:break-all; max-height:180px; overflow:auto }
76
- .who { font-family:ui-monospace,Menlo,monospace; font-size:12px; color:var(--dim); word-break:break-all }
114
+ .who { font-family:ui-monospace,Menlo,monospace; font-size:12.5px; color:var(--dim); word-break:break-all }
115
+
116
+ /* ------------------------------------------------------------ queue */
117
+ .qhead { display:flex; align-items:baseline; justify-content:space-between; margin-bottom:4px }
118
+ .qhead h2 { font-size:12.5px; margin:0; text-transform:uppercase; letter-spacing:.07em;
119
+ color:var(--dim); font-weight:700 }
120
+ .qhead .when { font-size:11px; color:var(--dim) }
121
+ .item { border:1px solid var(--line); border-radius:14px; padding:14px; margin-top:12px;
122
+ background:var(--panel2) }
123
+ .item.live { border-color:rgba(77,163,255,.35); box-shadow:0 0 0 3px rgba(77,163,255,.06) }
124
+ .item .top { display:flex; justify-content:space-between; align-items:baseline; gap:10px }
125
+ .item .amt { font-size:26px; font-weight:800; font-variant-numeric:tabular-nums; letter-spacing:-.01em }
126
+ .item .uri { font-size:12px; color:var(--dim); word-break:break-all; margin-top:3px;
127
+ font-family:ui-monospace,Menlo,monospace }
128
+ .item .wants { font-size:14px; margin-top:8px; word-break:break-word }
129
+ .item .foot { display:flex; justify-content:space-between; align-items:center; gap:10px; margin-top:12px }
130
+ .item .ago { font-size:11.5px; color:var(--dim) }
131
+ .item .btns { display:flex; gap:8px }
132
+ .item .btns button { width:auto; margin:0; min-height:44px; padding:11px 18px; font-size:15px }
133
+ .empty { color:var(--dim); font-size:15px; padding:18px 2px; text-align:center }
134
+ .hist { opacity:.6 }
135
+ .hist .amt { font-size:17px }
136
+ .clock { font-size:11.5px; font-weight:600 }
137
+ .clock.low { color:var(--bad) }
138
+
139
+ /* ------------------------------------------------- hold-to-approve
140
+ The expensive action is the one that should cost attention. Reject is one
141
+ tap; Approve needs a deliberate hold, which also catches the pocket-tap and
142
+ the "I meant to read it first" case. */
143
+ .hold { position:relative; overflow:hidden; background:var(--panel2); color:var(--ink);
144
+ border:1px solid var(--line); min-height:60px }
145
+ .hold .fill { position:absolute; inset:0 auto 0 0; width:0; background:rgba(61,220,151,.28) }
146
+ .hold .lbl { position:relative; z-index:1; font-weight:700 }
147
+ .hold.armed { border-color:var(--ok); color:var(--ok) }
148
+
149
+ /* ------------------------------------------------------- install hint */
150
+ .install { display:flex; gap:12px; align-items:center; font-size:13.5px; color:var(--dim) }
151
+ .install b { color:var(--ink) }
152
+ .install .sq { width:26px; height:26px; flex:0 0 26px; border:1px solid var(--line);
153
+ border-radius:7px; display:grid; place-items:center; font-size:15px; color:var(--ink) }
154
+
155
+ .banner { border-radius:12px; padding:11px 13px; font-size:13.5px; margin-bottom:12px }
156
+ .banner.off { background:rgba(255,180,84,.12); color:var(--warn); border:1px solid rgba(255,180,84,.25) }
77
157
  </style>
78
158
  </head>
79
159
  <body>
80
- <h1>Paydlux Approver</h1>
81
- <div class="sub">Signs payments on this device. The key never leaves it.</div>
160
+ <div class="brand">
161
+ <img src="icon-192.png" alt="">
162
+ <div>
163
+ <h1>Paydlux</h1>
164
+ <div class="sub">Signs payments here. The key never leaves this device.</div>
165
+ </div>
166
+ <span class="net hidden" id="netChip"></span>
167
+ </div>
168
+
169
+ <div class="banner off hidden" id="offline">
170
+ No connection to the desk. The list below is the last one you saw — nothing new
171
+ can be approved until it is reachable again.
172
+ </div>
173
+
174
+ <!-- ---------------------------------------------------- install to home -->
175
+ <div class="panel hidden" id="install">
176
+ <div class="install">
177
+ <span class="sq">&#8593;</span>
178
+ <div><b>Install this</b> so approvals are one tap: Share, then
179
+ <b>Add to Home Screen</b>. It then opens full-screen with its own icon.</div>
180
+ </div>
181
+ </div>
82
182
 
83
183
  <!-- ---------------------------------------------------------- enrolment -->
84
184
  <div class="panel" id="enrol">
85
185
  <div class="tag warn">No approver key on this device</div>
86
186
  <label for="seed">Approver secret seed (S&hellip;)</label>
87
- <input id="seed" type="password" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"
88
- placeholder="S">
187
+ <input id="seed" type="password" autocomplete="off" autocorrect="off" autocapitalize="off"
188
+ spellcheck="false" enterkeyhint="go" placeholder="S">
89
189
  <button id="enrolBtn">Import key</button>
90
190
  <div class="hint">
91
191
  The seed becomes a non-extractable signing key and is then dropped. Only that key is
@@ -98,27 +198,45 @@
98
198
  <!-- ------------------------------------------------------------ ready -->
99
199
  <div class="panel hidden" id="ready">
100
200
  <div class="tag ok">Key loaded</div>
101
- <dl class="kv" style="margin-top:10px">
201
+ <dl class="kv" style="margin-top:12px">
102
202
  <dt>Approver</dt><dd class="who" id="who"></dd>
103
203
  </dl>
104
204
  <button class="ghost" id="forget">Remove key from this device</button>
105
205
  </div>
106
206
 
107
- <!-- --------------------------------------------------------- approval -->
108
- <div class="panel">
109
- <label for="xdr">Pending transaction (paste the XDR from the seller)</label>
110
- <textarea id="xdr" autocorrect="off" autocapitalize="off" spellcheck="false"
111
- placeholder="Paste the transaction the seller sent you"></textarea>
112
- <button id="read">Read it</button>
113
- <div class="err" id="readErr"></div>
207
+ <!-- -------------------------------------------------------- not linked -->
208
+ <div class="panel hidden" id="nolink">
209
+ <div class="tag bad">No approval link</div>
210
+ <div class="hint" style="margin-top:12px">
211
+ This app shows payments only to whoever holds the seller's approval link. Open the
212
+ link the seller sent you once &mdash; the app then remembers it on this device.
213
+ </div>
214
+ <label for="link">Approval link</label>
215
+ <input id="link" type="url" autocorrect="off" autocapitalize="off" spellcheck="false"
216
+ enterkeyhint="go" placeholder="https://desk.paydlux.com/phone/?k=...">
217
+ <button id="linkBtn">Use this link</button>
218
+ <div class="err" id="linkErr"></div>
219
+ </div>
220
+
221
+ <!-- ------------------------------------------------------------- queue -->
222
+ <div class="panel hidden" id="queue">
223
+ <div class="qhead">
224
+ <h2 id="qtitle">Waiting for you</h2>
225
+ <span class="when" id="qwhen"></span>
226
+ </div>
227
+ <div id="qlist"><div class="empty">loading&hellip;</div></div>
228
+ <div class="err" id="qerr"></div>
114
229
  </div>
115
230
 
231
+ <!-- ----------------------------------------------------------- review -->
116
232
  <div class="panel hidden" id="review">
117
233
  <div class="amount" id="amount"></div>
118
- <dl class="kv">
234
+ <div class="hint" id="wantsBig"></div>
235
+ <dl class="kv" style="margin-top:16px">
119
236
  <dt>Token</dt><dd id="token"></dd>
120
237
  <dt>Payee</dt><dd id="payee"></dd>
121
238
  <dt>From</dt><dd id="vault"></dd>
239
+ <dt>Wants</dt><dd id="wants"></dd>
122
240
  <dt>Reference</dt><dd id="memo"></dd>
123
241
  <dt>Deadline</dt><dd id="deadline"></dd>
124
242
  <dt>Network</dt><dd id="network"></dd>
@@ -128,21 +246,45 @@
128
246
  Only the hash is signed. If anything above differs from what the seller told you,
129
247
  do not approve &mdash; a changed amount or payee produces a different hash.
130
248
  </div>
131
- <button id="approve">Approve &amp; sign</button>
132
- <button class="ghost" id="reject">Reject</button>
249
+ <button class="hold" id="approve">
250
+ <span class="fill" id="holdFill"></span>
251
+ <span class="lbl" id="holdLbl">Hold to approve &amp; sign</span>
252
+ </button>
253
+ <button class="danger" id="reject">Reject</button>
133
254
  <div class="err" id="signErr"></div>
134
255
  </div>
135
256
 
136
- <!-- ----------------------------------------------------------- result -->
257
+ <!-- --------------------------------------------------------- pasted xdr -->
258
+ <!-- Hidden until there is nothing in the queue. A human who just imported a key
259
+ should see their payments, not a box asking them to paste 400 characters of
260
+ base64 they were never asked for. This path is the fallback for when the
261
+ seller sends a transaction by message instead of a link. -->
262
+ <div class="panel hidden" id="paste">
263
+ <details>
264
+ <summary style="font-size:14px;color:var(--dim)">Sign a transaction the seller sent by hand</summary>
265
+ <label for="xdr">Pending transaction (XDR)</label>
266
+ <textarea id="xdr" autocorrect="off" autocapitalize="off" spellcheck="false"
267
+ placeholder="Paste the transaction the seller sent you"></textarea>
268
+ <button id="read">Read it</button>
269
+ <div class="hint">
270
+ A hand-sent transaction has no trusted source, so the app will make you type back
271
+ the hash the seller quoted before it signs.
272
+ </div>
273
+ <div class="err" id="readErr"></div>
274
+ </details>
275
+ </div>
276
+
277
+ <!-- ------------------------------------------------------------ result -->
137
278
  <div class="panel hidden" id="result">
138
- <div class="tag ok">Signed</div>
139
- <label>Signed transaction &mdash; send this back to the seller</label>
279
+ <div class="tag ok" id="resultTag">Signed</div>
280
+ <label id="resultLabel">Signed transaction &mdash; send this back to the seller</label>
140
281
  <div class="out" id="signed"></div>
141
282
  <button id="copy">Copy</button>
142
283
  <button class="ghost" id="again">Done</button>
143
284
  </div>
144
285
 
145
286
  <script src="xdr.js"></script>
287
+ <script src="verify.js"></script>
146
288
  <script src="app.js"></script>
147
289
  </body>
148
290
  </html>
@@ -0,0 +1,19 @@
1
+ {
2
+ "id": "/phone/",
3
+ "name": "Paydlux Approver",
4
+ "short_name": "Paydlux",
5
+ "description": "Signs Paydlux payment approvals on this device. The approver key never leaves it.",
6
+ "start_url": "/phone/",
7
+ "scope": "/phone/",
8
+ "display": "standalone",
9
+ "orientation": "portrait",
10
+ "lang": "en",
11
+ "background_color": "#0b0f14",
12
+ "theme_color": "#0b0f14",
13
+ "categories": ["finance", "productivity"],
14
+ "icons": [
15
+ { "src": "icon-192.png", "sizes": "192x192", "type": "image/png", "purpose": "any" },
16
+ { "src": "icon-512.png", "sizes": "512x512", "type": "image/png", "purpose": "any" },
17
+ { "src": "icon-192.png", "sizes": "192x192", "type": "image/png", "purpose": "maskable" }
18
+ ]
19
+ }
package/phone-app/xdr.js CHANGED
@@ -172,6 +172,49 @@
172
172
  * The public half is captured at enrolment and stored separately.
173
173
  */
174
174
  async function importSecretSeed(seedStr) {
175
+ const pkcs8 = pkcs8FromSeed(seedStr);
176
+ const privateKey = await crypto.subtle.importKey(
177
+ "pkcs8", pkcs8, { name: "Ed25519" }, false, ["sign"]);
178
+ pkcs8.fill(0); // drop the plaintext copy as soon as we can
179
+ return privateKey;
180
+ }
181
+
182
+ /**
183
+ * Import a seed AND read back its public half, in one step.
184
+ *
185
+ * A non-extractable key cannot be asked for its own public bytes, and the
186
+ * approver app needs those bytes to add its signature to a transaction. So the
187
+ * seed is imported a second time as an extractable key purely to export the
188
+ * 32-byte public half; that temporary key is discarded immediately and never
189
+ * stored. What the app keeps is the non-extractable key plus a public value
190
+ * that is public anyway.
191
+ *
192
+ * The export goes through JWK rather than "raw": WebCrypto defines raw only for
193
+ * OKP *public* keys, so exporting raw from a private key is refused (verified
194
+ * on Node 26). The JWK form of an Ed25519 private key carries the public half
195
+ * in `x`, base64url.
196
+ */
197
+ async function importSecretSeedPair(seedStr) {
198
+ const key = await importSecretSeed(seedStr);
199
+ const tmp = await crypto.subtle.importKey(
200
+ "pkcs8", pkcs8FromSeed(seedStr), { name: "Ed25519" }, true, ["sign"]);
201
+ let pub;
202
+ try {
203
+ const jwk = await crypto.subtle.exportKey("jwk", tmp);
204
+ pub = base64UrlToBytes(jwk.x);
205
+ } catch {
206
+ pub = new Uint8Array(await crypto.subtle.exportKey("raw", tmp));
207
+ }
208
+ if (pub.length !== 32) throw new Error("unexpected public key length " + pub.length);
209
+ return { key, pub, address: encodeAccountEd25519(pub) };
210
+ }
211
+
212
+ function base64UrlToBytes(s) {
213
+ if (typeof s !== "string" || !s) throw new Error("missing public key in the exported key");
214
+ return base64ToBytes(s.replace(/-/g, "+").replace(/_/g, "/") + "==".slice(0, (4 - s.length % 4) % 4));
215
+ }
216
+
217
+ function pkcs8FromSeed(seedStr) {
175
218
  const seed = decodeSecretSeed(seedStr);
176
219
  const pkcs8 = new Uint8Array(16 + 32);
177
220
  pkcs8.set([
@@ -179,10 +222,7 @@
179
222
  0x03, 0x2b, 0x65, 0x70, 0x04, 0x22, 0x04, 0x20,
180
223
  ], 0);
181
224
  pkcs8.set(seed, 16);
182
- const privateKey = await crypto.subtle.importKey(
183
- "pkcs8", pkcs8, { name: "Ed25519" }, false, ["sign"]);
184
- seed.fill(0); // drop the plaintext copy as soon as we can
185
- return privateKey;
225
+ return pkcs8;
186
226
  }
187
227
 
188
228
  // ------------------------------------------------------------------ parsing
@@ -429,7 +469,7 @@
429
469
  parseEnvelope, transactionHash, addSignature, formatAmount,
430
470
  hex, hexToBytes, signaturePreimage,
431
471
  encodeAccountEd25519, decodeAccountEd25519,
432
- decodeSecretSeed, encodeSecretSeed, importSecretSeed,
472
+ decodeSecretSeed, encodeSecretSeed, importSecretSeed, importSecretSeedPair,
433
473
  NETWORK_ID, crc16Xmodem,
434
474
  ENVELOPE_TYPE_TX, OP_PAYMENT, STROOPS,
435
475
  };