@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.
@@ -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 it back;
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 secrets and
38
- no payment data, so the desk serves it without the portal key — that is what
39
- lets it be installed to a home screen. Every route that carries data or moves
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; --dim:#8fa3b8;
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
- * { 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% }
48
56
  body {
49
- margin:0; background:var(--bg); color:var(--ink); font:16px/1.5 ui-sans-serif,system-ui,-apple-system,sans-serif;
50
- 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));
51
60
  max-width:560px; margin-inline:auto;
61
+ overscroll-behavior-y:none;
52
62
  }
53
- h1 { font-size:17px; margin:0 0 4px; letter-spacing:.02em }
54
- .sub { color:var(--dim); font-size:13px; margin-bottom:18px }
55
- .panel { background:var(--panel); border:1px solid var(--line); border-radius:14px; padding:14px; margin-bottom:14px }
56
- 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 }
57
76
  textarea, input {
58
- width:100%; background:#0d131a; color:var(--ink); border:1px solid var(--line);
59
- border-radius:10px; padding:10px; font:13px/1.4 ui-monospace,SFMono-Regular,Menlo,monospace;
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 { 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 */
63
86
  button {
64
- width:100%; margin-top:12px; padding:14px; border:0; border-radius:12px;
65
- 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;
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); color:#2a0707 }
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
- .kv { display:grid; grid-template-columns:96px 1fr; gap:6px 10px; font-size:14px }
73
- .kv dt { color:var(--dim); font-size:12px; text-transform:uppercase; letter-spacing:.06em; padding-top:2px }
74
- .kv dd { margin:0; font-family:ui-monospace,Menlo,monospace; word-break:break-all; font-size:13px }
75
- .amount { font-size:34px; font-weight:800; margin:2px 0 10px; font-variant-numeric:tabular-nums }
76
- .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 }
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:13px; margin-top:8px; word-break:break-word }
81
- .hint { color:var(--dim); font-size:12px; margin-top:8px }
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:#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;
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: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 }
86
115
 
87
116
  /* ------------------------------------------------------------ queue */
88
- .qhead { display:flex; align-items:baseline; justify-content:space-between; margin-bottom:6px }
89
- .qhead h2 { font-size:13px; margin:0; text-transform:uppercase; letter-spacing:.06em; color:var(--dim) }
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:12px; padding:12px; margin-top:10px; background:#0d131a }
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:22px; font-weight:800; font-variant-numeric:tabular-nums }
94
- .item .uri { font-size:12px; color:var(--dim); word-break:break-all; margin-top:2px }
95
- .item .wants { font-size:13px; margin-top:6px; word-break:break-word }
96
- .item .foot { display:flex; justify-content:space-between; align-items:center; gap:10px; margin-top:8px }
97
- .item .ago { font-size:11px; color:var(--dim) }
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:9px 16px; font-size:14px }
100
- .empty { color:var(--dim); font-size:14px; padding:14px 2px }
101
- .hist { opacity:.62 }
102
- .hist .amt { font-size:16px }
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
- <h1>Paydlux Approver</h1>
107
- <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>
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&hellip;)</label>
113
- <input id="seed" type="password" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"
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:10px">
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:10px">
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 &mdash; 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&hellip;</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
- <dl class="kv">
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 &mdash; a changed amount or payee produces a different hash.
173
248
  </div>
174
- <button id="approve">Approve &amp; sign</button>
175
- <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>
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:13px;color:var(--dim)">Sign a transaction the seller sent by hand</summary>
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
  }