@paydlux/agent 0.1.10 → 0.1.12
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 +23 -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 +34 -0
- package/dist/desk.js +97 -12
- 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 +6 -2
- package/phone-app/app.js +264 -14
- package/phone-app/index.html +134 -55
- package/phone-app/manifest.webmanifest +5 -1
package/phone-app/index.html
CHANGED
|
@@ -4,9 +4,12 @@
|
|
|
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">
|
|
7
8
|
<meta name="apple-mobile-web-app-capable" content="yes">
|
|
9
|
+
<meta name="mobile-web-app-capable" content="yes">
|
|
8
10
|
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
|
9
11
|
<meta name="apple-mobile-web-app-title" content="Paydlux">
|
|
12
|
+
<meta name="format-detection" content="telephone=no">
|
|
10
13
|
<title>Paydlux Approver</title>
|
|
11
14
|
<link rel="manifest" href="manifest.webmanifest">
|
|
12
15
|
<link rel="icon" href="icon-192.png" sizes="192x192">
|
|
@@ -25,93 +28,164 @@
|
|
|
25
28
|
|
|
26
29
|
What the key model actually guarantees:
|
|
27
30
|
- the seed becomes a NON-extractable WebCrypto key; after enrolment nothing
|
|
28
|
-
on this page, including an injected script, can read
|
|
31
|
+
on this page, including an injected script, can read the seed back;
|
|
29
32
|
- the signature covers the transaction hash, so a changed amount or payee
|
|
30
33
|
produces a different hash and a signature the ledger refuses;
|
|
31
34
|
- the approver key alone cannot move funds — the vault needs 2 signatures.
|
|
32
35
|
|
|
33
36
|
What it does NOT guarantee: a person who roots this device, or who approves a
|
|
34
37
|
transaction without reading it. The human reading the amount and payee is part
|
|
35
|
-
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.
|
|
36
40
|
|
|
37
|
-
The static shell (this file, app.js, xdr.js, the icons) carries no
|
|
38
|
-
no payment data, so the desk serves it without the portal key —
|
|
39
|
-
lets it be installed to a home screen
|
|
40
|
-
money still needs the key.
|
|
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.
|
|
41
45
|
-->
|
|
42
46
|
<style>
|
|
43
47
|
:root {
|
|
44
|
-
--bg:#0b0f14; --panel:#121821; --line:#22303f; --ink:#e8eef6;
|
|
45
|
-
--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;
|
|
46
51
|
}
|
|
47
|
-
|
|
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% }
|
|
48
56
|
body {
|
|
49
|
-
margin:0; background:var(--bg); color:var(--ink);
|
|
50
|
-
|
|
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));
|
|
51
60
|
max-width:560px; margin-inline:auto;
|
|
61
|
+
overscroll-behavior-y:none;
|
|
52
62
|
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
.
|
|
56
|
-
|
|
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 }
|
|
57
76
|
textarea, input {
|
|
58
|
-
width:100%; background
|
|
59
|
-
border-radius:
|
|
60
|
-
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;
|
|
61
80
|
}
|
|
62
|
-
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 */
|
|
63
86
|
button {
|
|
64
|
-
width:100%; margin-top:12px; padding:
|
|
65
|
-
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;
|
|
66
90
|
}
|
|
67
91
|
button.ghost { background:transparent; color:var(--dim); border:1px solid var(--line); font-weight:600 }
|
|
68
|
-
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) }
|
|
69
93
|
button:disabled { opacity:.4 }
|
|
70
94
|
.row { display:flex; gap:10px }
|
|
71
95
|
.row > * { flex:1 }
|
|
72
|
-
|
|
73
|
-
.kv
|
|
74
|
-
.kv
|
|
75
|
-
|
|
76
|
-
.
|
|
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 }
|
|
77
106
|
.tag.ok { background:rgba(61,220,151,.14); color:var(--ok) }
|
|
78
107
|
.tag.warn { background:rgba(255,180,84,.14); color:var(--warn) }
|
|
79
108
|
.tag.bad { background:rgba(257,107,107,.14); color:var(--bad) }
|
|
80
|
-
.err { color:var(--bad); font-size:
|
|
81
|
-
.hint { color:var(--dim); font-size:
|
|
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 }
|
|
82
111
|
.hidden { display:none }
|
|
83
|
-
.out { background
|
|
112
|
+
.out { background:var(--panel2); border:1px solid var(--line); border-radius:12px; padding:12px;
|
|
84
113
|
font:12px/1.4 ui-monospace,Menlo,monospace; word-break:break-all; max-height:180px; overflow:auto }
|
|
85
|
-
.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 }
|
|
86
115
|
|
|
87
116
|
/* ------------------------------------------------------------ queue */
|
|
88
|
-
.qhead { display:flex; align-items:baseline; justify-content:space-between; margin-bottom:
|
|
89
|
-
.qhead h2 { font-size:
|
|
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 }
|
|
90
120
|
.qhead .when { font-size:11px; color:var(--dim) }
|
|
91
|
-
.item { border:1px solid var(--line); border-radius:
|
|
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) }
|
|
92
124
|
.item .top { display:flex; justify-content:space-between; align-items:baseline; gap:10px }
|
|
93
|
-
.item .amt { font-size:
|
|
94
|
-
.item .uri { font-size:12px; color:var(--dim); word-break:break-all; margin-top:
|
|
95
|
-
|
|
96
|
-
.item .
|
|
97
|
-
.item .
|
|
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) }
|
|
98
131
|
.item .btns { display:flex; gap:8px }
|
|
99
|
-
.item .btns button { width:auto; margin:0; padding:
|
|
100
|
-
.empty { color:var(--dim); font-size:
|
|
101
|
-
.hist { opacity:.
|
|
102
|
-
.hist .amt { font-size:
|
|
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) }
|
|
103
157
|
</style>
|
|
104
158
|
</head>
|
|
105
159
|
<body>
|
|
106
|
-
<
|
|
107
|
-
<
|
|
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>
|
|
108
182
|
|
|
109
183
|
<!-- ---------------------------------------------------------- enrolment -->
|
|
110
184
|
<div class="panel" id="enrol">
|
|
111
185
|
<div class="tag warn">No approver key on this device</div>
|
|
112
186
|
<label for="seed">Approver secret seed (S…)</label>
|
|
113
|
-
<input id="seed" type="password" autocomplete="off" autocorrect="off" autocapitalize="off"
|
|
114
|
-
placeholder="S">
|
|
187
|
+
<input id="seed" type="password" autocomplete="off" autocorrect="off" autocapitalize="off"
|
|
188
|
+
spellcheck="false" enterkeyhint="go" placeholder="S">
|
|
115
189
|
<button id="enrolBtn">Import key</button>
|
|
116
190
|
<div class="hint">
|
|
117
191
|
The seed becomes a non-extractable signing key and is then dropped. Only that key is
|
|
@@ -124,7 +198,7 @@
|
|
|
124
198
|
<!-- ------------------------------------------------------------ ready -->
|
|
125
199
|
<div class="panel hidden" id="ready">
|
|
126
200
|
<div class="tag ok">Key loaded</div>
|
|
127
|
-
<dl class="kv" style="margin-top:
|
|
201
|
+
<dl class="kv" style="margin-top:12px">
|
|
128
202
|
<dt>Approver</dt><dd class="who" id="who"></dd>
|
|
129
203
|
</dl>
|
|
130
204
|
<button class="ghost" id="forget">Remove key from this device</button>
|
|
@@ -133,13 +207,13 @@
|
|
|
133
207
|
<!-- -------------------------------------------------------- not linked -->
|
|
134
208
|
<div class="panel hidden" id="nolink">
|
|
135
209
|
<div class="tag bad">No approval link</div>
|
|
136
|
-
<div class="hint" style="margin-top:
|
|
210
|
+
<div class="hint" style="margin-top:12px">
|
|
137
211
|
This app shows payments only to whoever holds the seller's approval link. Open the
|
|
138
212
|
link the seller sent you once — the app then remembers it on this device.
|
|
139
213
|
</div>
|
|
140
214
|
<label for="link">Approval link</label>
|
|
141
215
|
<input id="link" type="url" autocorrect="off" autocapitalize="off" spellcheck="false"
|
|
142
|
-
placeholder="https://desk.paydlux.com/phone/?k=...">
|
|
216
|
+
enterkeyhint="go" placeholder="https://desk.paydlux.com/phone/?k=...">
|
|
143
217
|
<button id="linkBtn">Use this link</button>
|
|
144
218
|
<div class="err" id="linkErr"></div>
|
|
145
219
|
</div>
|
|
@@ -147,7 +221,7 @@
|
|
|
147
221
|
<!-- ------------------------------------------------------------- queue -->
|
|
148
222
|
<div class="panel hidden" id="queue">
|
|
149
223
|
<div class="qhead">
|
|
150
|
-
<h2>Waiting for you</h2>
|
|
224
|
+
<h2 id="qtitle">Waiting for you</h2>
|
|
151
225
|
<span class="when" id="qwhen"></span>
|
|
152
226
|
</div>
|
|
153
227
|
<div id="qlist"><div class="empty">loading…</div></div>
|
|
@@ -157,7 +231,8 @@
|
|
|
157
231
|
<!-- ----------------------------------------------------------- review -->
|
|
158
232
|
<div class="panel hidden" id="review">
|
|
159
233
|
<div class="amount" id="amount"></div>
|
|
160
|
-
<
|
|
234
|
+
<div class="hint" id="wantsBig"></div>
|
|
235
|
+
<dl class="kv" style="margin-top:16px">
|
|
161
236
|
<dt>Token</dt><dd id="token"></dd>
|
|
162
237
|
<dt>Payee</dt><dd id="payee"></dd>
|
|
163
238
|
<dt>From</dt><dd id="vault"></dd>
|
|
@@ -171,8 +246,11 @@
|
|
|
171
246
|
Only the hash is signed. If anything above differs from what the seller told you,
|
|
172
247
|
do not approve — a changed amount or payee produces a different hash.
|
|
173
248
|
</div>
|
|
174
|
-
<button id="approve">
|
|
175
|
-
|
|
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>
|
|
176
254
|
<div class="err" id="signErr"></div>
|
|
177
255
|
</div>
|
|
178
256
|
|
|
@@ -183,7 +261,7 @@
|
|
|
183
261
|
seller sends a transaction by message instead of a link. -->
|
|
184
262
|
<div class="panel hidden" id="paste">
|
|
185
263
|
<details>
|
|
186
|
-
<summary style="font-size:
|
|
264
|
+
<summary style="font-size:14px;color:var(--dim)">Sign a transaction the seller sent by hand</summary>
|
|
187
265
|
<label for="xdr">Pending transaction (XDR)</label>
|
|
188
266
|
<textarea id="xdr" autocorrect="off" autocapitalize="off" spellcheck="false"
|
|
189
267
|
placeholder="Paste the transaction the seller sent you"></textarea>
|
|
@@ -206,6 +284,7 @@
|
|
|
206
284
|
</div>
|
|
207
285
|
|
|
208
286
|
<script src="xdr.js"></script>
|
|
287
|
+
<script src="verify.js"></script>
|
|
209
288
|
<script src="app.js"></script>
|
|
210
289
|
</body>
|
|
211
290
|
</html>
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
+
"id": "/phone/",
|
|
2
3
|
"name": "Paydlux Approver",
|
|
3
4
|
"short_name": "Paydlux",
|
|
4
5
|
"description": "Signs Paydlux payment approvals on this device. The approver key never leaves it.",
|
|
@@ -6,10 +7,13 @@
|
|
|
6
7
|
"scope": "/phone/",
|
|
7
8
|
"display": "standalone",
|
|
8
9
|
"orientation": "portrait",
|
|
10
|
+
"lang": "en",
|
|
9
11
|
"background_color": "#0b0f14",
|
|
10
12
|
"theme_color": "#0b0f14",
|
|
13
|
+
"categories": ["finance", "productivity"],
|
|
11
14
|
"icons": [
|
|
12
15
|
{ "src": "icon-192.png", "sizes": "192x192", "type": "image/png", "purpose": "any" },
|
|
13
|
-
{ "src": "icon-512.png", "sizes": "512x512", "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" }
|
|
14
18
|
]
|
|
15
19
|
}
|