@rubytech/create-maxy-code 0.1.484 → 0.1.485

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rubytech/create-maxy-code",
3
- "version": "0.1.484",
3
+ "version": "0.1.485",
4
4
  "description": "Install Maxy — AI for Productive People",
5
5
  "bin": {
6
6
  "create-maxy-code": "./dist/index.js"
@@ -144,3 +144,29 @@ describe('device binding', () => {
144
144
  expect(text).toMatch(/accountId\s+TEXT NOT NULL DEFAULT ''/)
145
145
  })
146
146
  })
147
+
148
+ const portalJs = () => readFileSync(join(TEMPLATE, 'portal.js'), 'utf8')
149
+ const indexHtml = () => readFileSync(join(TEMPLATE, 'index.html'), 'utf8')
150
+
151
+ describe('login error surfacing and passcode reveal (Task 1872)', () => {
152
+ it('maps each auth failure to a distinct message, not one opaque string', () => {
153
+ const js = portalJs()
154
+ expect(js).not.toMatch(/That did not work\./)
155
+ expect(js).toContain('Too many attempts. Try again later.')
156
+ expect(js).toContain('Enter both your name and your passcode.')
157
+ expect(js).toContain('That name and passcode did not match.')
158
+ expect(js).toContain('Something went wrong. Please try again in a moment.')
159
+ expect(js).toContain('Could not reach the server. Check your connection and try again.')
160
+ })
161
+
162
+ it('logs the status and error on the auth failure path', () => {
163
+ expect(portalJs()).toMatch(/console\.error\(\s*'\[data-portal\] auth failed'/)
164
+ })
165
+
166
+ it('ships a reveal control bound to the passcode field', () => {
167
+ const html = indexHtml()
168
+ expect(html).toMatch(/id="dp-reveal"/)
169
+ expect(html).toMatch(/aria-controls="dp-passcode"/)
170
+ expect(portalJs()).toMatch(/dp-reveal/)
171
+ })
172
+ })
@@ -18,14 +18,22 @@
18
18
  <label class="dp-label" for="dp-owner">Your name</label>
19
19
  <input class="dp-input" id="dp-owner" name="owner" autocomplete="username" required />
20
20
  <label class="dp-label" for="dp-passcode">Passcode</label>
21
- <input
22
- class="dp-input"
23
- id="dp-passcode"
24
- name="passcode"
25
- type="password"
26
- autocomplete="current-password"
27
- required
28
- />
21
+ <div class="dp-field">
22
+ <input
23
+ class="dp-input"
24
+ id="dp-passcode"
25
+ name="passcode"
26
+ type="password"
27
+ autocomplete="current-password"
28
+ required
29
+ />
30
+ <button
31
+ type="button"
32
+ class="dp-reveal"
33
+ id="dp-reveal"
34
+ aria-controls="dp-passcode"
35
+ >Show passcode</button>
36
+ </div>
29
37
  <button class="dp-btn" type="submit">Sign in</button>
30
38
  </form>
31
39
 
@@ -178,3 +178,28 @@ body {
178
178
  cursor: pointer;
179
179
  text-align: left;
180
180
  }
181
+
182
+ /* Passcode field with its reveal control. The passcode is transcribed out of
183
+ band, so being able to check what was typed is the point of the toggle.
184
+ Reuses existing variables and declares no new :root value, which
185
+ portal-brand-css.mjs would drop. */
186
+ .dp-field {
187
+ display: flex;
188
+ gap: 0.5rem;
189
+ align-items: stretch;
190
+ }
191
+
192
+ .dp-field .dp-input {
193
+ flex: 1;
194
+ }
195
+
196
+ .dp-reveal {
197
+ border: 1px solid var(--dp-rule);
198
+ border-radius: 0.375rem;
199
+ background: transparent;
200
+ color: var(--dp-muted);
201
+ padding: 0.375rem 0.625rem;
202
+ font: inherit;
203
+ cursor: pointer;
204
+ white-space: nowrap;
205
+ }
@@ -192,6 +192,13 @@
192
192
  });
193
193
  }
194
194
 
195
+ function authFailureMessage(status) {
196
+ if (status === 429) return 'Too many attempts. Try again later.';
197
+ if (status === 400) return 'Enter both your name and your passcode.';
198
+ if (status === 401) return 'That name and passcode did not match.';
199
+ return 'Something went wrong. Please try again in a moment.';
200
+ }
201
+
195
202
  loginEl.addEventListener('submit', function (e) {
196
203
  e.preventDefault();
197
204
  say('Signing in…');
@@ -209,12 +216,12 @@
209
216
  });
210
217
  })
211
218
  .then(function (res) {
212
- if (res.s === 429) {
213
- say('Too many attempts. Try again later.');
214
- return;
215
- }
216
219
  if (!res.d.ok) {
217
- say('That did not work.');
220
+ // The server returns the same denial for a wrong passcode and an
221
+ // unknown owner on purpose, so the message stays single and the
222
+ // console carries only the status class, never the reason.
223
+ console.error('[data-portal] auth failed', { status: res.s, error: res.d.error });
224
+ say(authFailureMessage(res.s));
218
225
  return;
219
226
  }
220
227
  document.getElementById('dp-passcode').value = '';
@@ -223,11 +230,20 @@
223
230
  say('');
224
231
  return refresh();
225
232
  })
226
- .catch(function () {
227
- say('That did not work.');
233
+ .catch(function (err) {
234
+ console.error('[data-portal] auth failed', { status: 'network', error: err });
235
+ say('Could not reach the server. Check your connection and try again.');
228
236
  });
229
237
  });
230
238
 
239
+ var revealEl = document.getElementById('dp-reveal');
240
+ var passcodeInput = document.getElementById('dp-passcode');
241
+ revealEl.addEventListener('click', function () {
242
+ var revealed = passcodeInput.type === 'text';
243
+ passcodeInput.type = revealed ? 'password' : 'text';
244
+ revealEl.textContent = revealed ? 'Show passcode' : 'Hide passcode';
245
+ });
246
+
231
247
  document.getElementById('dp-upload').addEventListener('submit', function (e) {
232
248
  e.preventDefault();
233
249
  var input = document.getElementById('dp-file');
@@ -4107,7 +4107,8 @@ async function sendPaused(sock, to) {
4107
4107
  } catch {
4108
4108
  }
4109
4109
  }
4110
- async function sendReadReceipt(sock, chatJid, messageIds, participant) {
4110
+ async function sendReadReceipt(sock, chatJid, messageIds, participant, accountId) {
4111
+ const idTag = messageIds[0] ?? "?";
4111
4112
  try {
4112
4113
  await sock.readMessages([
4113
4114
  ...messageIds.map((id) => ({
@@ -4116,7 +4117,13 @@ async function sendReadReceipt(sock, chatJid, messageIds, participant) {
4116
4117
  participant
4117
4118
  }))
4118
4119
  ]);
4119
- } catch {
4120
+ console.error(
4121
+ `[whatsapp:receipt] op=ack account=${accountId ?? "?"} jid=${chatJid} id=${idTag} ok=true`
4122
+ );
4123
+ } catch (err) {
4124
+ console.error(
4125
+ `[whatsapp:receipt] op=ack account=${accountId ?? "?"} jid=${chatJid} id=${idTag} ok=false reason=${String(err).slice(0, 120)}`
4126
+ );
4120
4127
  }
4121
4128
  }
4122
4129
  async function sendMediaMessage(sock, to, media, opts) {
@@ -6148,7 +6155,7 @@ function monitorInbound(conn) {
6148
6155
  const sendReceipts = whatsAppConfig.accounts?.[conn.accountId]?.sendReadReceipts ?? whatsAppConfig.sendReadReceipts ?? true;
6149
6156
  const sendAppendReceipt = () => {
6150
6157
  if (sendReceipts && msgKeyId) {
6151
- sendReadReceipt(sock2, remoteJid, [msgKeyId], participant);
6158
+ sendReadReceipt(sock2, remoteJid, [msgKeyId], participant, conn.accountId);
6152
6159
  }
6153
6160
  };
6154
6161
  const accepted = appendMediaQueue.schedule(async () => {
@@ -6200,7 +6207,8 @@ function monitorInbound(conn) {
6200
6207
  sock,
6201
6208
  remoteJid,
6202
6209
  [msg.key.id],
6203
- isGroupJid(remoteJid) ? msg.key.participant ?? void 0 : void 0
6210
+ isGroupJid(remoteJid) ? msg.key.participant ?? void 0 : void 0,
6211
+ conn.accountId
6204
6212
  );
6205
6213
  }
6206
6214
  const decision = evaluateSelfChatCommand({
@@ -6361,39 +6369,12 @@ async function handleInboundMessage(conn, msg) {
6361
6369
  console.error(`${TAG17} drop: no text or media account=${conn.accountId} from=${remoteJid}`);
6362
6370
  return;
6363
6371
  }
6364
- let mediaResult;
6365
- if (extracted.mediaType && conn.sock) {
6366
- const maxMb = whatsAppConfig.mediaMaxMb ?? 50;
6367
- mediaResult = await downloadInboundMedia(msg, conn.sock, {
6368
- maxBytes: maxMb * 1024 * 1024
6369
- });
6370
- if (!mediaResult) {
6371
- console.error(`${TAG17} media download returned undefined account=${conn.accountId} type=${extracted.mediaType} from=${remoteJid}`);
6372
- }
6373
- }
6374
6372
  const isGroup = isGroupJid(remoteJid);
6375
6373
  const senderJid = isGroup ? msg.key.participant ?? remoteJid : remoteJid;
6376
6374
  const senderPhone = await resolveJidToE164(senderJid, conn.lidMapping) ?? senderJid;
6377
6375
  const lidCapture = lidCaptureFor(senderJid, senderPhone);
6378
6376
  if (lidCapture) recordLidMapping(lidCapture.numericLid, lidCapture.e164);
6379
6377
  const selfPhone = conn.selfPhone ?? "";
6380
- if (extracted.mediaType === "audio" && mediaResult?.path) {
6381
- const conversationKey = isGroup ? remoteJid : senderPhone;
6382
- const debounceKey = `${conn.accountId}:${conversationKey}:${senderPhone}`;
6383
- let resolvePending;
6384
- const sttPending = new Promise((resolve39) => {
6385
- resolvePending = resolve39;
6386
- });
6387
- if (conn.debouncer) conn.debouncer.registerPending(debounceKey, sttPending);
6388
- try {
6389
- const sttResult = await transcribe(mediaResult.path, mediaResult.mimetype);
6390
- if (sttResult?.text) {
6391
- extracted.text = sttResult.text;
6392
- }
6393
- } finally {
6394
- resolvePending();
6395
- }
6396
- }
6397
6378
  let accessResult;
6398
6379
  if (isGroup) {
6399
6380
  accessResult = checkGroupAccess({ groupJid: remoteJid });
@@ -6409,7 +6390,7 @@ async function handleInboundMessage(conn, msg) {
6409
6390
  });
6410
6391
  }
6411
6392
  console.error(
6412
- `${TAG17} inbound account=${conn.accountId} from=${senderPhone} group=${isGroup} access=${accessResult.allowed ? "allowed" : "blocked"}(${accessResult.reason}) agent=${accessResult.agentType}` + (extracted.mediaType ? ` media=${extracted.mediaType}` : "") + (mediaResult ? ` mediaPath=${mediaResult.path}` : "") + (extracted.quotedMessage ? ` replyTo=${extracted.quotedMessage.id}` : "")
6393
+ `${TAG17} inbound account=${conn.accountId} from=${senderPhone} group=${isGroup} access=${accessResult.allowed ? "allowed" : "blocked"}(${accessResult.reason}) agent=${accessResult.agentType}` + (extracted.mediaType ? ` media=${extracted.mediaType}` : "") + (extracted.quotedMessage ? ` replyTo=${extracted.quotedMessage.id}` : "")
6413
6394
  );
6414
6395
  if (!accessResult.allowed && accessResult.reason === "account-manager-unresolved") {
6415
6396
  console.error(
@@ -6431,7 +6412,34 @@ async function handleInboundMessage(conn, msg) {
6431
6412
  }
6432
6413
  const sendReceipts = whatsAppConfig.accounts?.[conn.accountId]?.sendReadReceipts ?? whatsAppConfig.sendReadReceipts ?? true;
6433
6414
  if (sendReceipts && msg.key.id) {
6434
- sendReadReceipt(conn.sock, remoteJid, [msg.key.id], isGroup ? senderJid : void 0);
6415
+ sendReadReceipt(conn.sock, remoteJid, [msg.key.id], isGroup ? senderJid : void 0, conn.accountId);
6416
+ }
6417
+ let mediaResult;
6418
+ if (extracted.mediaType && conn.sock) {
6419
+ const maxMb = whatsAppConfig.mediaMaxMb ?? 50;
6420
+ mediaResult = await downloadInboundMedia(msg, conn.sock, {
6421
+ maxBytes: maxMb * 1024 * 1024
6422
+ });
6423
+ if (!mediaResult) {
6424
+ console.error(`${TAG17} media download returned undefined account=${conn.accountId} type=${extracted.mediaType} from=${remoteJid}`);
6425
+ }
6426
+ }
6427
+ if (extracted.mediaType === "audio" && mediaResult?.path) {
6428
+ const conversationKey = isGroup ? remoteJid : senderPhone;
6429
+ const debounceKey = `${conn.accountId}:${conversationKey}:${senderPhone}`;
6430
+ let resolvePending;
6431
+ const sttPending = new Promise((resolve39) => {
6432
+ resolvePending = resolve39;
6433
+ });
6434
+ if (conn.debouncer) conn.debouncer.registerPending(debounceKey, sttPending);
6435
+ try {
6436
+ const sttResult = await transcribe(mediaResult.path, mediaResult.mimetype);
6437
+ if (sttResult?.text) {
6438
+ extracted.text = sttResult.text;
6439
+ }
6440
+ } finally {
6441
+ resolvePending();
6442
+ }
6435
6443
  }
6436
6444
  const reply = async (text) => {
6437
6445
  if (isGroupJid(remoteJid)) {