@holdyapp/sdk 0.2.1 → 1.0.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.
Files changed (75) hide show
  1. package/README.md +20 -147
  2. package/dist/auto-capture.d.ts +12 -16
  3. package/dist/auto-capture.d.ts.map +1 -0
  4. package/dist/browser.d.ts +23 -0
  5. package/dist/browser.d.ts.map +1 -0
  6. package/dist/core.d.ts +42 -0
  7. package/dist/core.d.ts.map +1 -0
  8. package/dist/holdy.js +2 -0
  9. package/dist/holdy.js.map +7 -0
  10. package/dist/index.cjs.js +422 -0
  11. package/dist/index.cjs.js.map +7 -0
  12. package/dist/index.d.ts +5 -46
  13. package/dist/index.d.ts.map +1 -0
  14. package/dist/index.mjs +399 -0
  15. package/dist/index.mjs.map +7 -0
  16. package/dist/server.cjs.js +171 -0
  17. package/dist/server.cjs.js.map +7 -0
  18. package/dist/server.d.ts +15 -0
  19. package/dist/server.d.ts.map +1 -0
  20. package/dist/server.mjs +148 -0
  21. package/dist/server.mjs.map +7 -0
  22. package/dist/types.d.ts +41 -109
  23. package/dist/types.d.ts.map +1 -0
  24. package/dist/web-context.d.ts +51 -0
  25. package/dist/web-context.d.ts.map +1 -0
  26. package/dist/web.d.ts +41 -0
  27. package/dist/web.d.ts.map +1 -0
  28. package/dist/web.mjs +389 -0
  29. package/dist/web.mjs.map +7 -0
  30. package/package.json +24 -20
  31. package/src/auto-capture.ts +51 -78
  32. package/src/browser.ts +55 -0
  33. package/src/core.test.ts +147 -0
  34. package/src/core.ts +172 -0
  35. package/src/index.ts +4 -427
  36. package/src/server.ts +19 -0
  37. package/src/types.ts +38 -162
  38. package/src/web-context.test.ts +71 -0
  39. package/src/web-context.ts +227 -0
  40. package/src/web.ts +108 -0
  41. package/LICENSE +0 -21
  42. package/dist/anchor.d.ts +0 -8
  43. package/dist/holdy.cjs.js +0 -255
  44. package/dist/holdy.cjs.js.map +0 -7
  45. package/dist/holdy.esm.js +0 -255
  46. package/dist/holdy.esm.js.map +0 -7
  47. package/dist/holdy.iife.js +0 -255
  48. package/dist/holdy.iife.js.map +0 -7
  49. package/dist/outcome.d.ts +0 -19
  50. package/dist/queue.d.ts +0 -54
  51. package/dist/render/kinds/banner.d.ts +0 -3
  52. package/dist/render/kinds/checklist.d.ts +0 -19
  53. package/dist/render/kinds/cta.d.ts +0 -8
  54. package/dist/render/kinds/modal.d.ts +0 -16
  55. package/dist/render/kinds/shared.d.ts +0 -16
  56. package/dist/render/kinds/spotlight.d.ts +0 -17
  57. package/dist/render/kinds/tooltip.d.ts +0 -3
  58. package/dist/render/root.d.ts +0 -3
  59. package/dist/render/styles.d.ts +0 -5
  60. package/dist/store.d.ts +0 -36
  61. package/dist/transport.d.ts +0 -59
  62. package/src/anchor.ts +0 -72
  63. package/src/outcome.ts +0 -51
  64. package/src/queue.ts +0 -296
  65. package/src/render/kinds/banner.ts +0 -52
  66. package/src/render/kinds/checklist.ts +0 -175
  67. package/src/render/kinds/cta.ts +0 -53
  68. package/src/render/kinds/modal.ts +0 -129
  69. package/src/render/kinds/shared.ts +0 -67
  70. package/src/render/kinds/spotlight.ts +0 -176
  71. package/src/render/kinds/tooltip.ts +0 -82
  72. package/src/render/root.ts +0 -69
  73. package/src/render/styles.ts +0 -280
  74. package/src/store.ts +0 -106
  75. package/src/transport.ts +0 -182
package/dist/outcome.d.ts DELETED
@@ -1,19 +0,0 @@
1
- import type { OutcomeValue } from './types';
2
- import type { Transport } from './transport';
3
- /**
4
- * Outcome tracker · explicit only.
5
- *
6
- * - executed → ack via UI handler.
7
- * - dismissed → ack via UI handler.
8
- * - no_show → SDK detecta anchor não resolvido em 5s.
9
- * - failed → exception capturada pelo render boundary.
10
- * - converted → conversion cron server-side faz join workflow.completed.
11
- */
12
- export declare class OutcomeTracker {
13
- private readonly transport;
14
- private readonly pending;
15
- constructor(transport: Transport);
16
- registerPending(decisionId: string, workflow: string | undefined): void;
17
- dispatch(decisionId: string, outcome: OutcomeValue, metadata?: Record<string, unknown>): void;
18
- hasPending(decisionId: string): boolean;
19
- }
package/dist/queue.d.ts DELETED
@@ -1,54 +0,0 @@
1
- /**
2
- * Event queue · IndexedDB-backed retry com exponential backoff.
3
- *
4
- * Por que existe: SDK fire-and-forget perde eventos quando rede falha
5
- * (offline · 429 · 500 transitório · pagehide mid-flight). Sem fila, drop
6
- * silencioso · learning loop pega só os 80-90% que chegam.
7
- *
8
- * Comportamento:
9
- * · enqueue(events) tenta enviar imediatamente. Se falha (ou já tinha fila),
10
- * grava em IndexedDB com nextRetryAt = agora + 1s.
11
- * · flusher background polla a cada 5s, dequeue ordenado por nextRetryAt,
12
- * manda batch. Sucesso → delete da fila. Falha → backoff próximo.
13
- * · Backoff: 1s → 2s → 4s → 8s → 16s. Após 5 tentativas, drop com log.
14
- * · Cap 1000 itens (rough proxy 50KB cada · 50MB max). Quando cheio, drop
15
- * o mais antigo (FIFO).
16
- * · pagehide: tenta drain síncrono final via keepalive fetch (browser
17
- * entrega depois mesmo se aba fechou).
18
- *
19
- * Fallback: ambientes sem IndexedDB (SSR · Privacy mode em Safari rígido) caem
20
- * pra fila em memória — perde tudo no reload, mas SDK ainda funciona.
21
- */
22
- export interface QueuedBatch {
23
- /** Auto-increment key. */
24
- id?: number;
25
- /** Eventos serializados (já com idempotency_key gerado). */
26
- events: unknown[];
27
- attempts: number;
28
- next_retry_at: number;
29
- created_at: number;
30
- }
31
- export interface SendFn {
32
- /** Retorna true em sucesso, false em falha (caller decide retry). */
33
- (body: {
34
- events: unknown[];
35
- }): Promise<boolean>;
36
- }
37
- export declare class EventQueue {
38
- private readonly driver;
39
- private readonly send;
40
- private flushTimer;
41
- private flushing;
42
- private readonly log;
43
- constructor(send: SendFn, log?: (...args: unknown[]) => void);
44
- /**
45
- * Tenta enviar imediato; se falhar OU já houver fila pendente (ordem),
46
- * adiciona com next_retry_at = agora + 1s.
47
- */
48
- enqueue(events: unknown[]): Promise<void>;
49
- private startFlusher;
50
- private flushOnce;
51
- private bindPagehide;
52
- /** Pra testes. */
53
- destroy(): void;
54
- }
@@ -1,3 +0,0 @@
1
- import type { InterventionPayload } from '../../types';
2
- import { type RenderControl, type RenderHandlers } from './shared';
3
- export declare function renderBanner(payload: InterventionPayload, handlers: RenderHandlers): RenderControl;
@@ -1,19 +0,0 @@
1
- import type { InterventionPayload } from '../../types';
2
- import { type RenderControl, type RenderHandlers } from './shared';
3
- /**
4
- * Checklist · sticky bottom-right com lista de tarefas.
5
- *
6
- * Persistência: estado por decision_id em localStorage (chave
7
- * `holdy_checklist_{decision_id}`). Rehidratado em re-render.
8
- *
9
- * UX:
10
- * · cada item tem checkbox · clique marca/desmarca
11
- * · item com href abre em _self quando label clicado (após marcar)
12
- * · primeiro completed dispara onExecuted UMA vez (não a cada check)
13
- * · todos completed dispara onExecuted com metadata={completed:true}
14
- * · close button dispara onDismissed
15
- *
16
- * NÃO há outcome 'converted' aqui — fica pro caller (Holdy backend) detectar
17
- * via workflow.completed downstream.
18
- */
19
- export declare function renderChecklist(payload: InterventionPayload, decisionId: string, handlers: RenderHandlers): RenderControl;
@@ -1,8 +0,0 @@
1
- import type { InterventionPayload } from '../../types';
2
- import { type RenderControl, type RenderHandlers } from './shared';
3
- /**
4
- * CTA sticky bottom-right · usado quando não há anchor específico mas a
5
- * intervention precisa de presença persistente (ex: convite pra rodar
6
- * onboarding workflow).
7
- */
8
- export declare function renderCta(payload: InterventionPayload, handlers: RenderHandlers): RenderControl;
@@ -1,16 +0,0 @@
1
- import type { InterventionPayload } from '../../types';
2
- import { type RenderControl, type RenderHandlers } from './shared';
3
- /**
4
- * Modal · overlay centralizado com backdrop.
5
- *
6
- * UX:
7
- * · backdrop click dismissa (igual padrão Apple/Stripe)
8
- * · ESC dismissa
9
- * · focus trap entre primeiro/último focusable
10
- * · scroll body bloqueado enquanto aberto
11
- * · role=dialog + aria-modal pra acessibilidade
12
- *
13
- * Uso: intervention de alto peso (renewal aproximando, churn risk
14
- * crítico, expansion oferecida com 1 click). Limite global de 1.
15
- */
16
- export declare function renderModal(payload: InterventionPayload, handlers: RenderHandlers): RenderControl;
@@ -1,16 +0,0 @@
1
- import type { InterventionPayload, OutcomeValue } from '../../types';
2
- export interface RenderHandlers {
3
- onExecuted(metadata?: Record<string, unknown>): void;
4
- onDismissed(metadata?: Record<string, unknown>): void;
5
- onFailed(metadata?: Record<string, unknown>): void;
6
- }
7
- export interface RenderControl {
8
- el: HTMLElement;
9
- dismiss: (outcome?: OutcomeValue, metadata?: Record<string, unknown>) => void;
10
- }
11
- /** Cria botão fechar SDK-owned · dispatch dismissed. */
12
- export declare function buildCloseButton(onDismiss: () => void): HTMLButtonElement;
13
- /** Cria CTA button OU anchor SDK-owned · dispatch executed. */
14
- export declare function buildCtaButton(payload: InterventionPayload, onExecuted: () => void): HTMLElement | null;
15
- export declare function buildTextNode(text: string): HTMLParagraphElement;
16
- export declare function buildActionsRow(...children: Array<HTMLElement | null>): HTMLDivElement | null;
@@ -1,17 +0,0 @@
1
- import type { InterventionPayload } from '../../types';
2
- import { type RenderControl, type RenderHandlers } from './shared';
3
- /**
4
- * Spotlight · backdrop escurecido com "buraco" recortado no anchor +
5
- * tooltip apontando.
6
- *
7
- * UX:
8
- * · backdrop tem clip-path criando ring around o elemento
9
- * · tooltip aparece abaixo (ou acima se sem espaço)
10
- * · clique no anchor original (atravessa o buraco via pointer-events:none
11
- * na área do hole) executa
12
- * · backdrop click fora do hole dismissa
13
- * · ESC dismissa
14
- *
15
- * Uso: onboarding step "veja esse botão", first-time UI guidance.
16
- */
17
- export declare function renderSpotlight(payload: InterventionPayload, anchor: Element | null, handlers: RenderHandlers): RenderControl;
@@ -1,3 +0,0 @@
1
- import type { InterventionPayload } from '../../types';
2
- import { type RenderControl, type RenderHandlers } from './shared';
3
- export declare function renderTooltip(payload: InterventionPayload, anchor: Element | null, handlers: RenderHandlers): RenderControl;
@@ -1,3 +0,0 @@
1
- import type { ThemeConfig } from '../types';
2
- export declare function ensureRoot(theme: ThemeConfig | undefined): ShadowRoot | null;
3
- export declare function appendToRoot(node: HTMLElement): void;
@@ -1,5 +0,0 @@
1
- import type { ThemeConfig } from '../types';
2
- export declare function getStyleSheet(): CSSStyleSheet | null;
3
- export declare function applyThemeVars(host: HTMLElement, theme: ThemeConfig | undefined): void;
4
- /** Fallback pra browsers sem adoptedStyleSheets (Safari <16.4). */
5
- export declare function injectFallbackStyle(root: ShadowRoot): void;
package/dist/store.d.ts DELETED
@@ -1,36 +0,0 @@
1
- import type { InterventionKind } from './types';
2
- interface ActiveSlot {
3
- decisionId: string;
4
- confidence: number;
5
- kind: InterventionKind;
6
- dismiss: () => void;
7
- }
8
- /**
9
- * Active intervention slots · prioridade por confidence.
10
- *
11
- * Caps globais:
12
- * - 1 banner global
13
- * - 1 modal global (Wave 2 · UX bloqueia tudo, único)
14
- * - 1 checklist global (Wave 2 · sticky bottom-right)
15
- * - N tooltips, 1 por anchor
16
- * - N spotlights, 1 por anchor (Wave 2)
17
- * - 1 CTA global (sticky bottom-right)
18
- *
19
- * Modal e checklist + cta competem pelo mesmo "espaço mental" do usuário ·
20
- * a próxima implementação pode consolidar (1 floating element no canto).
21
- * Wave 2 mantém slots separados pra simplicidade.
22
- *
23
- * Quando 2 decisions reivindicam o mesmo slot, a de menor confidence perde.
24
- */
25
- export declare class InterventionStore {
26
- private readonly tooltipsByAnchor;
27
- private readonly spotlightsByAnchor;
28
- private banner;
29
- private modal;
30
- private checklist;
31
- private cta;
32
- /** Tenta reivindicar slot. Retorna previous slot que precisa ser dismissado (caller). */
33
- claim(kind: InterventionKind, decisionId: string, confidence: number, anchorSelector: string | undefined, dismiss: () => void): ActiveSlot | null;
34
- release(decisionId: string): void;
35
- }
36
- export {};
@@ -1,59 +0,0 @@
1
- import type { AccountIdentity, CanonicalPrimitive, DecisionResult, HoldyConfig, RuntimeConfig, UserIdentity } from './types';
2
- export declare class Transport {
3
- private readonly apiKey;
4
- private readonly apiUrl;
5
- private readonly debug;
6
- private readonly queue;
7
- constructor(config: HoldyConfig);
8
- private headers;
9
- fetchRuntimeConfig(): Promise<RuntimeConfig | null>;
10
- evaluate(input: {
11
- account_id: string;
12
- current_workflow?: string;
13
- recent_primitives?: Array<{
14
- primitive: CanonicalPrimitive;
15
- payload: Record<string, unknown>;
16
- occurred_at: string;
17
- }>;
18
- recent_sentiment?: 'positivo' | 'neutro' | 'negativo' | 'urgente';
19
- consumer?: 'ui' | 'csm' | 'agent';
20
- }): Promise<DecisionResult | null>;
21
- /**
22
- * Event ingest · delegado pra EventQueue (IndexedDB + exponential backoff).
23
- * Tenta envio imediato; se falhar, persiste e retry em background.
24
- */
25
- ingestBatch(events: Array<{
26
- primitive: CanonicalPrimitive;
27
- occurred_at: string;
28
- account_id?: string;
29
- user_id?: string;
30
- session_id?: string;
31
- payload: Record<string, unknown>;
32
- idempotency_key?: string;
33
- }>): void;
34
- /**
35
- * Send raw · usado pelo queue. Retorna true em sucesso (2xx), false em
36
- * falha (rede ou 4xx/5xx).
37
- *
38
- * IMPORTANTE: 4xx é tratado como falha permanente — não vale retry. Mas
39
- * a fila não distingue (mantém pra retry simples). MAX_ATTEMPTS = 5 limita
40
- * o desperdício. Refinamento (4xx → drop imediato) fica pra Wave 3.
41
- */
42
- private sendBatch;
43
- /**
44
- * Upsert account + opcional user via /v1/accounts/upsert.
45
- *
46
- * Best-effort fire-and-forget. Backend COALESCE não sobrescreve campos não-nulos.
47
- * Retorna account_id (UUID Holdy) pra hidratar config local; null em falha.
48
- */
49
- upsertAccount(payload: {
50
- account: AccountIdentity;
51
- user?: UserIdentity;
52
- }): Promise<{
53
- account_id: string;
54
- account_user_id: string | null;
55
- } | null>;
56
- reportOutcome(decisionId: string, outcome: 'executed' | 'dismissed' | 'converted' | 'no_show' | 'failed', metadata?: Record<string, unknown>): void;
57
- private log;
58
- destroy(): void;
59
- }
package/src/anchor.ts DELETED
@@ -1,72 +0,0 @@
1
- /**
2
- * Anchor resolver · MutationObserver-based.
3
- *
4
- * Cliente do produto pode renderizar o anchor depois do decision chegar
5
- * (SPA route, lazy mount). Resolver espera até 5s antes de desistir.
6
- * Single observer global · cap retries por selector.
7
- */
8
-
9
- const RESOLVE_TIMEOUT_MS = 5000
10
-
11
- interface PendingAnchor {
12
- selector: string
13
- resolve: (el: Element | null) => void
14
- cleanup: () => void
15
- }
16
-
17
- const pending = new Set<PendingAnchor>()
18
- let observer: MutationObserver | null = null
19
-
20
- function ensureObserver(): void {
21
- if (observer || typeof MutationObserver === 'undefined' || typeof document === 'undefined') return
22
- observer = new MutationObserver(() => {
23
- if (pending.size === 0) return
24
- pending.forEach((entry) => {
25
- const el = document.querySelector(entry.selector)
26
- if (el) {
27
- entry.cleanup()
28
- entry.resolve(el)
29
- }
30
- })
31
- if (pending.size === 0) stopObserver()
32
- })
33
- observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['id', 'class', 'data-holdy-anchor'] })
34
- }
35
-
36
- function stopObserver(): void {
37
- if (observer) {
38
- observer.disconnect()
39
- observer = null
40
- }
41
- }
42
-
43
- export function resolveAnchor(selector: string | undefined): Promise<Element | null> {
44
- if (!selector) return Promise.resolve(null)
45
- if (typeof document === 'undefined') return Promise.resolve(null)
46
-
47
- // Try sync first
48
- const existing = document.querySelector(selector)
49
- if (existing) return Promise.resolve(existing)
50
-
51
- return new Promise<Element | null>((resolve) => {
52
- let resolved = false
53
- const entry: PendingAnchor = {
54
- selector,
55
- resolve,
56
- cleanup: () => {
57
- if (resolved) return
58
- resolved = true
59
- pending.delete(entry)
60
- clearTimeout(timeoutId)
61
- if (pending.size === 0) stopObserver()
62
- },
63
- }
64
- const timeoutId = setTimeout(() => {
65
- entry.cleanup()
66
- resolve(null)
67
- }, RESOLVE_TIMEOUT_MS)
68
-
69
- pending.add(entry)
70
- ensureObserver()
71
- })
72
- }
package/src/outcome.ts DELETED
@@ -1,51 +0,0 @@
1
- import type { OutcomeValue } from './types'
2
- import type { Transport } from './transport'
3
-
4
- interface PendingDecision {
5
- id: string
6
- workflow?: string | undefined
7
- startedAt: number
8
- }
9
-
10
- /**
11
- * Outcome tracker · explicit only.
12
- *
13
- * - executed → ack via UI handler.
14
- * - dismissed → ack via UI handler.
15
- * - no_show → SDK detecta anchor não resolvido em 5s.
16
- * - failed → exception capturada pelo render boundary.
17
- * - converted → conversion cron server-side faz join workflow.completed.
18
- */
19
- export class OutcomeTracker {
20
- private readonly transport: Transport
21
- private readonly pending = new Map<string, PendingDecision>()
22
-
23
- constructor(transport: Transport) {
24
- this.transport = transport
25
- }
26
-
27
- registerPending(decisionId: string, workflow: string | undefined): void {
28
- this.pending.set(decisionId, {
29
- id: decisionId,
30
- workflow,
31
- startedAt: Date.now(),
32
- })
33
- }
34
-
35
- dispatch(decisionId: string, outcome: OutcomeValue, metadata?: Record<string, unknown>): void {
36
- if (!this.pending.has(decisionId) && outcome !== 'failed') {
37
- // unknown decision · still report failed/dismissed since they're idempotent
38
- this.transport.reportOutcome(decisionId, outcome, metadata)
39
- return
40
- }
41
- this.pending.delete(decisionId)
42
- this.transport.reportOutcome(decisionId, outcome, {
43
- ...metadata,
44
- latency_ms: metadata?.latency_ms ?? Date.now() - (this.pending.get(decisionId)?.startedAt ?? Date.now()),
45
- })
46
- }
47
-
48
- hasPending(decisionId: string): boolean {
49
- return this.pending.has(decisionId)
50
- }
51
- }