@closerclick/closer-click-notifications 0.2.0 → 0.3.0

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 CHANGED
@@ -153,6 +153,10 @@ receipts.start()
153
153
  await receipts.report({ toPubkey: parsed.publickey, url: sharedUrl, name: parsed.name })
154
154
  ```
155
155
 
156
+ - **Acumular (referidos)**: pasá `onReceipt(env)` para que tu app sume por acuse
157
+ fresco (además de la notificación). Para enlaces de invitación con tu pubkey
158
+ embebida usá `packPubkey(pubkey)` → token base64url para `#i=<token>`, y
159
+ `unpackPubkey(token)` del lado que abre (matchea byte a byte el ruteo del proxy).
156
160
  - **Identidad del que abre**: el sobre incluye **siempre** `from { pubkey, nick }`.
157
161
  - **Anti-spam**: `report` aplica throttle por `(toPubkey|url)` (default 24h).
158
162
  - **Propio**: `report` es no-op si el contenido es tuyo (`toPubkey === tu pubkey`).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@closerclick/closer-click-notifications",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Notificaciones compartidas del ecosistema Closer Click: controlador data-agnostic (permiso del navegador + preferencias por categoría con scope por app + disparo), el Web Component <closer-click-notifications> (panel de ajustes), helper de Web Push ligado al vault + proxy y acuses de apertura de contenido compartido (createShareReceipts). Autohosteado, Shadow DOM, sin JS de terceros ni cookies.",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
package/src/index.d.ts CHANGED
@@ -123,6 +123,8 @@ export interface ShareReceiptsConfig {
123
123
  throttleMs?: number
124
124
  /** Override del click (default: navegar a url). */
125
125
  onOpen?: (url: string, env: ShareReceiptEnvelope) => void
126
+ /** Hook por acuse fresco entrante (además de notificar): para acumular (referidos, etc.). */
127
+ onReceipt?: (env: ShareReceiptEnvelope) => void
126
128
  }
127
129
 
128
130
  export interface ShareReceipts {
@@ -140,6 +142,11 @@ export interface ShareReceipts {
140
142
  /** Motor común de acuses de apertura de contenido compartido. */
141
143
  export function createShareReceipts(cfg: ShareReceiptsConfig): ShareReceipts
142
144
 
145
+ /** Empaqueta una pubkey (JWK string del vault) para un enlace de invitación/acuse (base64url exacto). */
146
+ export function packPubkey(pubkey: string): string
147
+ /** Desempaqueta la pubkey de un token de enlace. Devuelve null si es inválido. */
148
+ export function unpackPubkey(token: string): string | null
149
+
143
150
  export class CloserClickNotifications extends HTMLElement {
144
151
  controller: NotificationsController | null
145
152
  }
package/src/index.js CHANGED
@@ -314,6 +314,7 @@ export function createShareReceipts (cfg = {}) {
314
314
  const render = typeof cfg.render === 'function' ? cfg.render : null
315
315
  const lang = cfg.lang || 'auto'
316
316
  const throttleMs = cfg.throttleMs != null ? cfg.throttleMs : 24 * 60 * 60 * 1000
317
+ const onReceipt = typeof cfg.onReceipt === 'function' ? cfg.onReceipt : null
317
318
  const onOpen = typeof cfg.onOpen === 'function'
318
319
  ? cfg.onOpen
319
320
  : (url) => { try { if (isBrowser && url) location.assign(url) } catch (_) {} }
@@ -392,6 +393,9 @@ export function createShareReceipts (cfg = {}) {
392
393
  const key = `${(env.from && env.from.pubkey) || ''}|${env.url}|${env.ts || ''}`
393
394
  if (_seen.has(key)) return
394
395
  _seen.add(key)
396
+ // Hook para que la app ACUMULE (p. ej. referidos) además de notificar. Se
397
+ // llama una vez por acuse fresco; la app hace su propio dedup durable.
398
+ if (onReceipt) { try { onReceipt(env) } catch (_) {} }
395
399
  const rendered = render ? render(env) : _defaultRender(env)
396
400
  if (!rendered) return
397
401
  const { title, body, ...rest } = rendered
@@ -427,6 +431,34 @@ export function createShareReceipts (cfg = {}) {
427
431
  return { report, start, stop, category, RECEIPT_TAG, RECEIPT_VERSION }
428
432
  }
429
433
 
434
+ /* ----------------------------------------------------------------------------
435
+ * Pubkey ↔ token compacto para enlaces de invitación/acuse.
436
+ *
437
+ * El acuse se enruta por la MISMA pubkey (JWK string) que el destinatario usó en
438
+ * `identify`. Para meterla en un enlace compartible (`#i=<token>`) la empaquetamos
439
+ * como base64url del string EXACTO (no comprimimos el punto P-256: así el ruteo
440
+ * por `sendByPubkey` matchea byte a byte sin reconstruir el JWK). Quien abre el
441
+ * enlace desempaqueta la pubkey del autor y le manda el acuse.
442
+ * --------------------------------------------------------------------------*/
443
+
444
+ function _b64urlEncode (str) {
445
+ const bytes = new TextEncoder().encode(str);
446
+ let bin = ''; for (const b of bytes) bin += String.fromCharCode(b);
447
+ return btoa(bin).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
448
+ }
449
+ function _b64urlDecode (s) {
450
+ const b64 = s.replace(/-/g, '+').replace(/_/g, '/');
451
+ const bin = atob(b64);
452
+ const bytes = new Uint8Array(bin.length);
453
+ for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
454
+ return new TextDecoder().decode(bytes);
455
+ }
456
+
457
+ /** Empaqueta una pubkey (JWK string del vault) para un enlace: base64url exacto. */
458
+ export function packPubkey (pubkey) { return _b64urlEncode(String(pubkey)); }
459
+ /** Desempaqueta la pubkey de un token de enlace. Devuelve null si es inválido. */
460
+ export function unpackPubkey (token) { try { return _b64urlDecode(String(token)); } catch (_) { return null; } }
461
+
430
462
  /* ============================================================================
431
463
  * 4) Web Component ── <closer-click-notifications> (panel de ajustes)
432
464
  * ==========================================================================*/