@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.
- package/README.md +8 -2
- package/dist/agent.d.ts +8 -0
- package/dist/agent.js +1 -1
- package/dist/cli.d.ts +20 -0
- package/dist/cli.js +165 -8
- package/dist/desk.d.ts +50 -0
- package/dist/desk.js +215 -56
- package/dist/gateway.d.ts +10 -2
- package/dist/gateway.js +20 -1
- package/dist/policy.d.ts +9 -0
- package/dist/policy.js +2 -2
- package/package.json +10 -2
- package/phone-app/app.js +653 -224
- package/phone-app/icon-192.png +0 -0
- package/phone-app/icon-512.png +0 -0
- package/phone-app/index.html +195 -53
- package/phone-app/manifest.webmanifest +19 -0
- package/phone-app/xdr.js +45 -5
|
Binary file
|
|
Binary file
|
package/phone-app/index.html
CHANGED
|
@@ -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
|
|
12
|
-
It
|
|
13
|
-
the transaction hash. Signing is a purely local operation: no
|
|
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
|
|
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
|
|
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
|
-
|
|
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;
|
|
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
|
-
|
|
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);
|
|
41
|
-
|
|
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
|
-
|
|
45
|
-
|
|
46
|
-
.
|
|
47
|
-
|
|
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
|
|
50
|
-
border-radius:
|
|
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 {
|
|
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:
|
|
56
|
-
background:var(--accent); color:#04121f; font-size:
|
|
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);
|
|
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
|
-
|
|
64
|
-
.kv
|
|
65
|
-
.kv
|
|
66
|
-
|
|
67
|
-
.
|
|
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(
|
|
71
|
-
.err { color:var(--bad); font-size:
|
|
72
|
-
.hint { color:var(--dim); font-size:
|
|
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
|
|
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:
|
|
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
|
-
<
|
|
81
|
-
<
|
|
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">↑</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…)</label>
|
|
87
|
-
<input id="seed" type="password" autocomplete="off" autocorrect="off" autocapitalize="off"
|
|
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:
|
|
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
|
-
<!--
|
|
108
|
-
<div class="panel">
|
|
109
|
-
<
|
|
110
|
-
<
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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 — 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…</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
|
-
<
|
|
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 — a changed amount or payee produces a different hash.
|
|
130
248
|
</div>
|
|
131
|
-
<button id="approve">
|
|
132
|
-
|
|
249
|
+
<button class="hold" id="approve">
|
|
250
|
+
<span class="fill" id="holdFill"></span>
|
|
251
|
+
<span class="lbl" id="holdLbl">Hold to approve & 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
|
-
<!--
|
|
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 — send this back to the seller</label>
|
|
279
|
+
<div class="tag ok" id="resultTag">Signed</div>
|
|
280
|
+
<label id="resultLabel">Signed transaction — 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
|
-
|
|
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
|
};
|