@volter/twin-hubspot 0.1.37 → 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 (135) hide show
  1. package/README.md +9 -193
  2. package/dist/generated/pack-facts.json +42 -0
  3. package/dist/journeys/coverage.json +60 -0
  4. package/dist/journeys/customer-life.json +7 -0
  5. package/dist/journeys/customer-life.outline.md +6 -0
  6. package/dist/journeys/decisions.json +623 -0
  7. package/dist/journeys/demand.json +171 -0
  8. package/dist/journeys/score.json +82 -0
  9. package/dist/journeys/unreachable.json +12 -0
  10. package/dist/journeys/vendor-examples.json +7 -0
  11. package/dist/spec/SOURCE.md +40 -0
  12. package/dist/spec/openapi/associations.json.gz +0 -0
  13. package/dist/spec/openapi/companies.json.gz +0 -0
  14. package/dist/spec/openapi/contacts.json.gz +0 -0
  15. package/dist/spec/openapi/deals.json.gz +0 -0
  16. package/dist/spec/openapi/meetings.json.gz +0 -0
  17. package/dist/spec/openapi/oauth.json.gz +0 -0
  18. package/dist/spec/openapi/owners.json.gz +0 -0
  19. package/dist/spec/openapi/pipelines.json.gz +0 -0
  20. package/dist/spec/openapi/properties.json.gz +0 -0
  21. package/dist/spec/openapi/tickets.json.gz +0 -0
  22. package/dist/spec/patches.json +235 -0
  23. package/dist/spec/sources.json +32 -0
  24. package/dist/src/cli.js +10 -22
  25. package/dist/src/fetch.d.ts +2 -0
  26. package/dist/src/fetch.js +13 -0
  27. package/dist/src/generated/surface.gen.json +1 -1
  28. package/dist/src/index.d.ts +5 -11
  29. package/dist/src/index.js +10 -94
  30. package/dist/src/manifest.js +32 -66
  31. package/dist/src/semantics/index.d.ts +2 -0
  32. package/dist/src/semantics/index.js +1 -0
  33. package/dist/src/semantics/states.d.ts +1 -0
  34. package/dist/src/semantics/states.js +2 -0
  35. package/dist/src/server.d.ts +7 -0
  36. package/dist/src/server.js +7 -0
  37. package/generated/pack-facts.json +42 -0
  38. package/journeys/coverage.json +60 -0
  39. package/journeys/customer-life.json +7 -0
  40. package/journeys/customer-life.outline.md +6 -0
  41. package/journeys/decisions.json +623 -0
  42. package/journeys/demand.json +171 -0
  43. package/journeys/score.json +82 -0
  44. package/journeys/unreachable.json +12 -0
  45. package/journeys/vendor-examples.json +7 -0
  46. package/package.json +10 -23
  47. package/spec/SOURCE.md +40 -0
  48. package/spec/openapi/associations.json.gz +0 -0
  49. package/spec/openapi/companies.json.gz +0 -0
  50. package/spec/openapi/contacts.json.gz +0 -0
  51. package/spec/openapi/deals.json.gz +0 -0
  52. package/spec/openapi/meetings.json.gz +0 -0
  53. package/spec/openapi/oauth.json.gz +0 -0
  54. package/spec/openapi/owners.json.gz +0 -0
  55. package/spec/openapi/pipelines.json.gz +0 -0
  56. package/spec/openapi/properties.json.gz +0 -0
  57. package/spec/openapi/tickets.json.gz +0 -0
  58. package/spec/patches.json +235 -0
  59. package/spec/sources.json +32 -0
  60. package/src/cli.ts +10 -19
  61. package/src/fetch.ts +14 -0
  62. package/src/generated/surface.gen.json +1 -1
  63. package/src/index.ts +12 -152
  64. package/src/manifest.ts +33 -92
  65. package/src/semantics/index.ts +6 -0
  66. package/src/semantics/states.ts +2 -0
  67. package/src/server.ts +8 -0
  68. package/client/hubspot-mirror.css +0 -43
  69. package/client/hubspot-mirror.tsx +0 -132
  70. package/dist/client/hubspot-mirror.bundle.js +0 -516
  71. package/dist/client/hubspot-mirror.css +0 -43
  72. package/dist/client/hubspot-mirror.d.ts +0 -15
  73. package/dist/client/hubspot-mirror.js +0 -59
  74. package/dist/client/hubspot-mirror.tsx +0 -132
  75. package/dist/src/accounts.d.ts +0 -30
  76. package/dist/src/accounts.js +0 -122
  77. package/dist/src/hubspot-areas.d.ts +0 -10
  78. package/dist/src/hubspot-areas.js +0 -114
  79. package/dist/src/hubspot-budget.d.ts +0 -58
  80. package/dist/src/hubspot-budget.js +0 -176
  81. package/dist/src/hubspot-capabilities.d.ts +0 -3
  82. package/dist/src/hubspot-capabilities.js +0 -1588
  83. package/dist/src/hubspot-conformance.d.ts +0 -16
  84. package/dist/src/hubspot-conformance.js +0 -523
  85. package/dist/src/hubspot-connector.d.ts +0 -125
  86. package/dist/src/hubspot-connector.js +0 -390
  87. package/dist/src/hubspot-deferred-capabilities.d.ts +0 -6
  88. package/dist/src/hubspot-deferred-capabilities.js +0 -64
  89. package/dist/src/hubspot-mirror-ui.d.ts +0 -62
  90. package/dist/src/hubspot-mirror-ui.js +0 -152
  91. package/dist/src/hubspot-oauth.d.ts +0 -8
  92. package/dist/src/hubspot-oauth.js +0 -291
  93. package/dist/src/hubspot-server.d.ts +0 -24
  94. package/dist/src/hubspot-server.js +0 -116
  95. package/dist/src/hubspot-twin.d.ts +0 -65
  96. package/dist/src/hubspot-twin.js +0 -1558
  97. package/dist/src/portal.d.ts +0 -20
  98. package/dist/src/portal.js +0 -30
  99. package/dist/src/screens/account.d.ts +0 -1
  100. package/dist/src/screens/account.js +0 -139
  101. package/dist/src/screens/crm.d.ts +0 -2
  102. package/dist/src/screens/crm.js +0 -153
  103. package/dist/src/screens/developer.d.ts +0 -4
  104. package/dist/src/screens/developer.js +0 -191
  105. package/dist/src/screens/forms.d.ts +0 -5
  106. package/dist/src/screens/forms.js +0 -126
  107. package/dist/src/screens/page.d.ts +0 -21
  108. package/dist/src/screens/page.js +0 -49
  109. package/dist/src/screens/session.d.ts +0 -1
  110. package/dist/src/screens/session.js +0 -32
  111. package/dist/src/semantics/crm.d.ts +0 -8
  112. package/dist/src/semantics/crm.js +0 -101
  113. package/dist/src/webhooks.d.ts +0 -12
  114. package/dist/src/webhooks.js +0 -79
  115. package/src/accounts.ts +0 -127
  116. package/src/hubspot-areas.ts +0 -155
  117. package/src/hubspot-budget.ts +0 -202
  118. package/src/hubspot-capabilities.ts +0 -1523
  119. package/src/hubspot-conformance.ts +0 -537
  120. package/src/hubspot-connector.ts +0 -419
  121. package/src/hubspot-deferred-capabilities.ts +0 -99
  122. package/src/hubspot-journey.uitest.ts +0 -104
  123. package/src/hubspot-mirror-ui.ts +0 -166
  124. package/src/hubspot-oauth.tsx +0 -296
  125. package/src/hubspot-server.ts +0 -115
  126. package/src/hubspot-twin.ts +0 -1534
  127. package/src/portal.ts +0 -40
  128. package/src/screens/account.tsx +0 -129
  129. package/src/screens/crm.tsx +0 -154
  130. package/src/screens/developer.tsx +0 -181
  131. package/src/screens/forms.tsx +0 -117
  132. package/src/screens/page.tsx +0 -55
  133. package/src/screens/session.tsx +0 -36
  134. package/src/semantics/crm.ts +0 -116
  135. package/src/webhooks.ts +0 -82
@@ -1,43 +0,0 @@
1
- :root {
2
- --bg: #f5f8fa;
3
- --panel: #ffffff;
4
- --line: #dfe3eb;
5
- --text: #33475b;
6
- --muted: #7c98b6;
7
- --accent: #eaf0f6;
8
- --brand: #ff7a59;
9
- --ok: #00bda5;
10
- --warn: #f5c26b;
11
- --bad: #f2545b;
12
- }
13
- * { box-sizing: border-box; }
14
- body { margin: 0; font-family: -apple-system, system-ui, "Helvetica Neue", sans-serif; background: var(--bg); color: var(--text); }
15
- .app { display: grid; grid-template-columns: 220px 1fr; grid-template-rows: 56px 1fr; min-height: 100vh; }
16
- .topbar { grid-column: 1 / 3; display: flex; align-items: center; justify-content: space-between; padding: 0 20px; border-bottom: 1px solid var(--line); background: var(--panel); }
17
- .brand { font-weight: 700; color: var(--brand); letter-spacing: 0.2px; }
18
- .view-toggle { border: 1px solid var(--line); background: var(--panel); color: var(--text); border-radius: 4px; padding: 6px 14px; cursor: pointer; font-size: 13px; }
19
- .view-toggle:hover { background: var(--accent); }
20
- .sidebar { border-right: 1px solid var(--line); padding: 14px 10px; background: var(--panel); }
21
- .nav-item { display: block; width: 100%; text-align: left; background: none; border: none; color: var(--muted); padding: 9px 12px; border-radius: 4px; cursor: pointer; font-size: 14px; }
22
- .nav-item.active, .nav-item:hover { background: var(--accent); color: var(--text); }
23
- .content { padding: 24px; overflow: auto; }
24
- table { width: 100%; border-collapse: collapse; background: var(--panel); border: 1px solid var(--line); border-radius: 6px; }
25
- th { text-align: left; color: var(--muted); font-size: 11px; text-transform: uppercase; letter-spacing: 0.4px; padding: 10px 12px; border-bottom: 1px solid var(--line); }
26
- td { padding: 12px; border-bottom: 1px solid var(--line); font-size: 14px; }
27
- tr:last-child td { border-bottom: none; }
28
- .cell-title { font-weight: 600; }
29
- .cell-id { color: var(--muted); font-variant-numeric: tabular-nums; }
30
- .cell-prop-name { color: var(--muted); font-size: 12px; width: 40%; }
31
- .pill { display: inline-block; padding: 3px 10px; border-radius: 999px; font-size: 12px; background: var(--accent); }
32
- .pill-ok { background: rgba(0, 189, 165, 0.16); color: var(--ok); }
33
- .pill-warn { background: rgba(245, 194, 107, 0.22); color: #b07d1a; }
34
- .pill-bad { background: rgba(242, 84, 91, 0.16); color: var(--bad); }
35
- .empty { color: var(--muted); padding: 28px; background: var(--panel); border: 1px dashed var(--line); border-radius: 6px; }
36
- .deal-board { display: flex; gap: 14px; align-items: flex-start; overflow-x: auto; padding-bottom: 8px; }
37
- .board-column { min-width: 220px; background: var(--panel); border: 1px solid var(--line); border-radius: 6px; padding: 12px; }
38
- .board-column-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 10px; }
39
- .board-count { color: var(--muted); font-size: 12px; }
40
- .deal-card { border: 1px solid var(--line); border-radius: 4px; padding: 10px; margin-bottom: 8px; }
41
- .deal-amount { color: var(--muted); font-size: 13px; margin-top: 4px; }
42
- .record-detail { background: var(--panel); border: 1px solid var(--line); border-radius: 6px; padding: 20px; }
43
- .detail-title { margin: 0 0 14px; font-size: 20px; }
@@ -1,15 +0,0 @@
1
- import { type HubspotRow, type MirrorSectionKey } from '../src/hubspot-mirror-ui.js';
2
- /** A CRM record list — one `list-row` landmark per record, carrying its HubSpot record id. */
3
- export declare function RecordsTable({ section, rows }: {
4
- section: MirrorSectionKey;
5
- rows: HubspotRow[];
6
- }): import("react").JSX.Element;
7
- /** The deal pipeline board — the screen a sales rep actually works in, one column per stage. */
8
- export declare function DealBoard({ rows }: {
9
- rows: HubspotRow[];
10
- }): import("react").JSX.Element;
11
- /** Record detail — the property sheet HubSpot shows on the right of a record. */
12
- export declare function RecordDetail({ section, rows }: {
13
- section: MirrorSectionKey;
14
- rows: HubspotRow[];
15
- }): import("react").JSX.Element;
@@ -1,59 +0,0 @@
1
- import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
- // HubSpot mirror — React/TSX CRM client. Renders the twin's contacts, companies, deals and
3
- // tickets by fetching the twin's OWN CRM REST API on the same origin
4
- // (`/crm/v3/objects/contacts`, …) — HubSpot's real paths, the same ones the SDK calls.
5
- // API↔UI parity: the screen shows real twin state, not a hardcoded shell.
6
- import { createRoot } from 'react-dom/client';
7
- import { useEffect, useState } from 'react';
8
- import { MIRROR_SECTIONS, SECTION_COLUMNS, formatAmount, groupByStage, recordTitle, stageTone, } from "../src/hubspot-mirror-ui.js";
9
- // WHERE THIS MIRROR LIVES. Served at a vendor root the base is '' (fetches are root-relative, as
10
- // before); served under a path prefix with a <base> tag every read and write resolves inside that
11
- // prefix instead of escaping it.
12
- const WIRE_BASE = typeof document === 'undefined' || document.querySelector('base[href]') === null ? '' : new URL('.', document.baseURI).pathname.replace(/\/$/, '');
13
- // Map a tone to a STATIC literal class name (a template-literal class is erased by the
14
- // minifier, so `pill-${tone}` would vanish from the bundle — assert these instead).
15
- const PILL_CLASS = { ok: 'pill pill-ok', warn: 'pill pill-warn', bad: 'pill pill-bad', '': 'pill' };
16
- /** Unwrap HubSpot's collection envelope (`{ results, paging }`) to its results array. */
17
- async function fetchRows(path) {
18
- const res = await fetch(`${WIRE_BASE}${path}`);
19
- const body = (await res.json());
20
- return Array.isArray(body.results) ? body.results : [];
21
- }
22
- /** A CRM record list — one `list-row` landmark per record, carrying its HubSpot record id. */
23
- export function RecordsTable({ section, rows }) {
24
- if (rows.length === 0)
25
- return _jsxs("div", { className: "empty", children: ["No ", section, " yet."] });
26
- const columns = SECTION_COLUMNS[section];
27
- return (_jsxs("table", { className: "records-table", children: [_jsx("thead", { children: _jsxs("tr", { children: [_jsx("th", { children: "Name" }), columns.map((c) => _jsx("th", { children: c.label }, c.prop)), _jsx("th", { children: "Record ID" })] }) }), _jsx("tbody", { children: rows.map((r) => (_jsxs("tr", { className: "list-row", "data-record-id": r.id, children: [_jsx("td", { className: "cell-title", children: recordTitle(section, r.properties) }), columns.map((c) => (_jsx("td", { className: "cell-prop", children: c.prop === 'amount' ? formatAmount(r.properties?.amount)
28
- : c.prop === 'dealstage'
29
- ? _jsx("span", { className: PILL_CLASS[stageTone(r.properties?.dealstage)], children: r.properties?.dealstage ?? '—' })
30
- : (r.properties?.[c.prop] ?? '—') }, c.prop))), _jsx("td", { className: "cell-id", children: r.id })] }, r.id))) })] }));
31
- }
32
- /** The deal pipeline board — the screen a sales rep actually works in, one column per stage. */
33
- export function DealBoard({ rows }) {
34
- const groups = groupByStage(rows);
35
- if (groups.length === 0)
36
- return _jsx("div", { className: "empty", children: "No deals yet." });
37
- return (_jsx("div", { className: "deal-board", children: groups.map((g) => (_jsxs("section", { className: "board-column", "data-stage": g.stage, children: [_jsxs("header", { className: "board-column-head", children: [_jsx("span", { className: PILL_CLASS[stageTone(g.stage)], children: g.stage }), _jsx("span", { className: "board-count", children: g.rows.length })] }), g.rows.map((r) => (_jsxs("article", { className: "deal-card list-row", "data-record-id": r.id, children: [_jsx("div", { className: "cell-title", children: recordTitle('deals', r.properties) }), _jsx("div", { className: "deal-amount", children: formatAmount(r.properties?.amount) })] }, r.id)))] }, g.stage))) }));
38
- }
39
- /** Record detail — the property sheet HubSpot shows on the right of a record. */
40
- export function RecordDetail({ section, rows }) {
41
- const r = rows[0];
42
- if (!r)
43
- return _jsx("div", { className: "empty", children: "No record." });
44
- const entries = Object.entries(r.properties ?? {});
45
- return (_jsxs("section", { className: "record-detail", "data-record-id": r.id, children: [_jsx("h2", { className: "detail-title", children: recordTitle(section, r.properties) }), _jsx("table", { className: "property-table", children: _jsx("tbody", { children: entries.map(([k, v]) => (_jsxs("tr", { className: "list-row", "data-prop": k, children: [_jsx("td", { className: "cell-prop-name", children: k }), _jsx("td", { className: "cell-prop-value", children: v ?? '—' })] }, k))) }) })] }));
46
- }
47
- function App() {
48
- const [section, setSection] = useState('contacts');
49
- const [board, setBoard] = useState(false);
50
- const [rows, setRows] = useState([]);
51
- useEffect(() => {
52
- const sec = MIRROR_SECTIONS.find((s) => s.key === section);
53
- fetchRows(sec.path).then(setRows).catch(() => setRows([]));
54
- }, [section]);
55
- return (_jsxs("div", { className: "app", children: [_jsxs("header", { className: "topbar", children: [_jsx("span", { className: "brand", children: "HubSpot twin" }), section === 'deals' ? (_jsx("button", { className: "view-toggle", onClick: () => setBoard((b) => !b), children: board ? 'Table' : 'Board' })) : null] }), _jsx("nav", { className: "sidebar", children: MIRROR_SECTIONS.map((s) => (_jsx("button", { className: `nav-item${s.key === section ? ' active' : ''}`, onClick: () => { setSection(s.key); setBoard(false); }, children: s.label }, s.key))) }), _jsx("main", { className: "content", children: section === 'deals' && board ? _jsx(DealBoard, { rows: rows }) : _jsx(RecordsTable, { section: section, rows: rows }) })] }));
56
- }
57
- const el = typeof document !== 'undefined' ? document.getElementById('root') : null;
58
- if (el)
59
- createRoot(el).render(_jsx(App, {}));
@@ -1,132 +0,0 @@
1
- // HubSpot mirror — React/TSX CRM client. Renders the twin's contacts, companies, deals and
2
- // tickets by fetching the twin's OWN CRM REST API on the same origin
3
- // (`/crm/v3/objects/contacts`, …) — HubSpot's real paths, the same ones the SDK calls.
4
- // API↔UI parity: the screen shows real twin state, not a hardcoded shell.
5
- import { createRoot } from 'react-dom/client';
6
- import { useEffect, useState } from 'react';
7
- import {
8
- MIRROR_SECTIONS, SECTION_COLUMNS, formatAmount, groupByStage, recordTitle, stageTone,
9
- type HubspotRow, type MirrorSectionKey, type PillTone,
10
- } from '../src/hubspot-mirror-ui.ts';
11
-
12
- // WHERE THIS MIRROR LIVES. Served at a vendor root the base is '' (fetches are root-relative, as
13
- // before); served under a path prefix with a <base> tag every read and write resolves inside that
14
- // prefix instead of escaping it.
15
- const WIRE_BASE = typeof document === 'undefined' || document.querySelector('base[href]') === null ? '' : new URL('.', document.baseURI).pathname.replace(/\/$/, '');
16
-
17
- // Map a tone to a STATIC literal class name (a template-literal class is erased by the
18
- // minifier, so `pill-${tone}` would vanish from the bundle — assert these instead).
19
- const PILL_CLASS: Record<PillTone, string> = { ok: 'pill pill-ok', warn: 'pill pill-warn', bad: 'pill pill-bad', '': 'pill' };
20
-
21
- /** Unwrap HubSpot's collection envelope (`{ results, paging }`) to its results array. */
22
- async function fetchRows(path: string): Promise<HubspotRow[]> {
23
- const res = await fetch(`${WIRE_BASE}${path}`);
24
- const body = (await res.json()) as { results?: unknown };
25
- return Array.isArray(body.results) ? (body.results as HubspotRow[]) : [];
26
- }
27
-
28
- /** A CRM record list — one `list-row` landmark per record, carrying its HubSpot record id. */
29
- export function RecordsTable({ section, rows }: { section: MirrorSectionKey; rows: HubspotRow[] }) {
30
- if (rows.length === 0) return <div className="empty">No {section} yet.</div>;
31
- const columns = SECTION_COLUMNS[section];
32
- return (
33
- <table className="records-table">
34
- <thead>
35
- <tr><th>Name</th>{columns.map((c) => <th key={c.prop}>{c.label}</th>)}<th>Record ID</th></tr>
36
- </thead>
37
- <tbody>
38
- {rows.map((r) => (
39
- <tr className="list-row" data-record-id={r.id} key={r.id}>
40
- <td className="cell-title">{recordTitle(section, r.properties)}</td>
41
- {columns.map((c) => (
42
- <td className="cell-prop" key={c.prop}>
43
- {c.prop === 'amount' ? formatAmount(r.properties?.amount)
44
- : c.prop === 'dealstage'
45
- ? <span className={PILL_CLASS[stageTone(r.properties?.dealstage)]}>{r.properties?.dealstage ?? '—'}</span>
46
- : (r.properties?.[c.prop] ?? '—')}
47
- </td>
48
- ))}
49
- <td className="cell-id">{r.id}</td>
50
- </tr>
51
- ))}
52
- </tbody>
53
- </table>
54
- );
55
- }
56
-
57
- /** The deal pipeline board — the screen a sales rep actually works in, one column per stage. */
58
- export function DealBoard({ rows }: { rows: HubspotRow[] }) {
59
- const groups = groupByStage(rows);
60
- if (groups.length === 0) return <div className="empty">No deals yet.</div>;
61
- return (
62
- <div className="deal-board">
63
- {groups.map((g) => (
64
- <section className="board-column" data-stage={g.stage} key={g.stage}>
65
- <header className="board-column-head">
66
- <span className={PILL_CLASS[stageTone(g.stage)]}>{g.stage}</span>
67
- <span className="board-count">{g.rows.length}</span>
68
- </header>
69
- {g.rows.map((r) => (
70
- <article className="deal-card list-row" data-record-id={r.id} key={r.id}>
71
- <div className="cell-title">{recordTitle('deals', r.properties)}</div>
72
- <div className="deal-amount">{formatAmount(r.properties?.amount)}</div>
73
- </article>
74
- ))}
75
- </section>
76
- ))}
77
- </div>
78
- );
79
- }
80
-
81
- /** Record detail — the property sheet HubSpot shows on the right of a record. */
82
- export function RecordDetail({ section, rows }: { section: MirrorSectionKey; rows: HubspotRow[] }) {
83
- const r = rows[0];
84
- if (!r) return <div className="empty">No record.</div>;
85
- const entries = Object.entries(r.properties ?? {});
86
- return (
87
- <section className="record-detail" data-record-id={r.id}>
88
- <h2 className="detail-title">{recordTitle(section, r.properties)}</h2>
89
- <table className="property-table">
90
- <tbody>
91
- {entries.map(([k, v]) => (
92
- <tr className="list-row" data-prop={k} key={k}>
93
- <td className="cell-prop-name">{k}</td>
94
- <td className="cell-prop-value">{v ?? '—'}</td>
95
- </tr>
96
- ))}
97
- </tbody>
98
- </table>
99
- </section>
100
- );
101
- }
102
-
103
- function App() {
104
- const [section, setSection] = useState<MirrorSectionKey>('contacts');
105
- const [board, setBoard] = useState(false);
106
- const [rows, setRows] = useState<HubspotRow[]>([]);
107
- useEffect(() => {
108
- const sec = MIRROR_SECTIONS.find((s) => s.key === section)!;
109
- fetchRows(sec.path).then(setRows).catch(() => setRows([]));
110
- }, [section]);
111
- return (
112
- <div className="app">
113
- <header className="topbar">
114
- <span className="brand">HubSpot twin</span>
115
- {section === 'deals' ? (
116
- <button className="view-toggle" onClick={() => setBoard((b) => !b)}>{board ? 'Table' : 'Board'}</button>
117
- ) : null}
118
- </header>
119
- <nav className="sidebar">
120
- {MIRROR_SECTIONS.map((s) => (
121
- <button key={s.key} className={`nav-item${s.key === section ? ' active' : ''}`} onClick={() => { setSection(s.key); setBoard(false); }}>{s.label}</button>
122
- ))}
123
- </nav>
124
- <main className="content">
125
- {section === 'deals' && board ? <DealBoard rows={rows} /> : <RecordsTable section={section} rows={rows} />}
126
- </main>
127
- </div>
128
- );
129
- }
130
-
131
- const el = typeof document !== 'undefined' ? document.getElementById('root') : null;
132
- if (el) createRoot(el).render(<App />);
@@ -1,30 +0,0 @@
1
- export type Row = Record<string, unknown>;
2
- export declare const hex: (s: string) => string;
3
- /** Every live stored subject of a bookkeeping type (these are not scoped to an account: each names its own). */
4
- export declare function stored(type: string, root?: string): Row[];
5
- /** Write a bookkeeping subject. */
6
- export declare function keep(type: string, id: string, fields: Row, root: string | undefined, at: string, actor?: string): Promise<void>;
7
- export declare const passwordHash: (email: string, password: string) => string;
8
- export declare function accountOf(hubId: number, root?: string): Row | undefined;
9
- export declare function personOf(email: string, root?: string): Row | undefined;
10
- export declare function usersOf(hubId: number, root?: string): Row[];
11
- /** An account's owners, as GET /crm/v3/owners answers them (the spec's PublicOwner): one per user. */
12
- export declare function ownersOf(hubId: number, root?: string): Row[];
13
- /** POST /_twin/users/:email/password {password, firstName, lastName, account?, subscription?, renewsOn?, hubId?} — stands in for two pages the
14
- * twin does not build: HubSpot's sign-up (a new account, named `account`, whose first user is its super admin) and a
15
- * user's acceptance of an invitation from an account's Users & Teams settings (`hubId`: the user joins that account).
16
- * The World keeps the password's hash, never the password; 201 with the account's Hub ID and the user's ids. */
17
- export declare function signUpDoor(email: string, request: Request, root: string | undefined, at: string): Promise<Response>;
18
- /** The subscription an account has at `at`: a paid one whose auto-renewal was cancelled ends on its renewal date, when
19
- * "your account will be downgraded to HubSpot's free tools"
20
- * (https://knowledge.hubspot.com/account/how-do-i-cancel-my-hubspot-account); one that renews carries on. */
21
- export declare function subscriptionAt(account: Row, at: string): string;
22
- export declare const SESSION_COOKIE = "hs_session";
23
- /** The person the request's session cookie names, or undefined. */
24
- export declare function signedIn(request: Request, root?: string): Row | undefined;
25
- export declare function appByClientId(clientId: string, root?: string): Row | undefined;
26
- export declare function appById(appId: number, root?: string): Row | undefined;
27
- export declare function appsOf(hubId: number, root?: string): Row[];
28
- /** The live installs in an account. */
29
- export declare function installsIn(hubId: number, root?: string): Row[];
30
- export declare function installOf(appId: number, hubId: number, root?: string): Row | undefined;
@@ -1,122 +0,0 @@
1
- // HUBSPOT ACCOUNTS, THEIR USERS, AND WHAT THEY HOLD OUTSIDE THE CRM — the state HubSpot's pages and its OAuth install
2
- // keep, which no CRM operation reads or writes: an account (its Hub ID, name and subscription, closed or not), the
3
- // people who sign in to it (each a user, and so an owner of its records), their browser sessions, the apps an account
4
- // builds as developer projects (each with its client ID and secret, redirect URLs, scopes and webhook subscriptions),
5
- // and each app's installs in accounts (who installed it, when, which scopes, uninstalled or not). Kept in the kernel's
6
- // log under `_`-prefixed types that never reach the wire.
7
- //
8
- // Where HubSpot's documentation stops and the twin decides: an account the World makes has a Hub ID from 146000001 up,
9
- // a user id from 80000001 and an owner id from 90000001 (HubSpot documents their existence, not their ranges); every
10
- // user of an account the story names is its super admin or one of its users with the seats the story gives them, and
11
- // the twin keeps no finer permission than super admin or not.
12
- import { createHash } from 'node:crypto';
13
- import { applyTwinWrite, projectResources } from '@volter/world-core';
14
- const SERVICE = 'hubspot';
15
- export const hex = (s) => createHash('sha256').update(s).digest('hex');
16
- /** Every live stored subject of a bookkeeping type (these are not scoped to an account: each names its own). */
17
- export function stored(type, root) {
18
- return projectResources(SERVICE, root).filter((r) => r.type === type && r.deleted !== true);
19
- }
20
- /** Write a bookkeeping subject. */
21
- export async function keep(type, id, fields, root, at, actor = 'hubspot') {
22
- await applyTwinWrite(SERVICE, { operation: `hubspot.${type}`, subjectType: type, subjectId: id, fields, occurredAt: at, actor: { kind: 'human', id: actor } }, root);
23
- }
24
- // ── accounts and users ─────────────────────────────────────────────────────────────────────
25
- export const passwordHash = (email, password) => hex(`hubspot-password:${email.toLowerCase()}:${password}`);
26
- export function accountOf(hubId, root) {
27
- return stored('_account', root).find((a) => a.hubId === hubId);
28
- }
29
- export function personOf(email, root) {
30
- return stored('_person', root).find((p) => p.email === email.toLowerCase());
31
- }
32
- export function usersOf(hubId, root) {
33
- return stored('_person', root).filter((p) => p.hubId === hubId).sort((a, b) => Number(a.userId) - Number(b.userId));
34
- }
35
- /** An account's owners, as GET /crm/v3/owners answers them (the spec's PublicOwner): one per user. */
36
- export function ownersOf(hubId, root) {
37
- return usersOf(hubId, root).map((p) => ({
38
- id: String(p.ownerId), email: p.email, firstName: p.firstName, lastName: p.lastName, userId: p.userId,
39
- userIdIncludingInactive: p.userId, type: 'PERSON', archived: false, teams: [],
40
- createdAt: p.created_at, updatedAt: p.created_at,
41
- }));
42
- }
43
- /** POST /_twin/users/:email/password {password, firstName, lastName, account?, subscription?, renewsOn?, hubId?} — stands in for two pages the
44
- * twin does not build: HubSpot's sign-up (a new account, named `account`, whose first user is its super admin) and a
45
- * user's acceptance of an invitation from an account's Users & Teams settings (`hubId`: the user joins that account).
46
- * The World keeps the password's hash, never the password; 201 with the account's Hub ID and the user's ids. */
47
- export async function signUpDoor(email, request, root, at) {
48
- let body = {};
49
- try {
50
- body = JSON.parse(await request.text());
51
- }
52
- catch { /* refused below */ }
53
- const password = typeof body.password === 'string' ? body.password : '';
54
- const firstName = typeof body.firstName === 'string' ? body.firstName : '';
55
- const lastName = typeof body.lastName === 'string' ? body.lastName : '';
56
- const refuse = (message) => Response.json({ status: 'error', message, category: 'VALIDATION_ERROR' }, { status: 400 });
57
- if (!/^[^@\s]+@[^@\s]+$/.test(email) || password.length < 8 || !firstName || !lastName)
58
- return refuse('an email, a password of at least 8 characters, a firstName and a lastName are required');
59
- if (personOf(email, root))
60
- return refuse(`${email} already has a HubSpot login`);
61
- let hubId;
62
- let superAdmin;
63
- if (body.hubId !== undefined) {
64
- hubId = Number(body.hubId);
65
- const account = accountOf(hubId, root);
66
- if (!account || account.closed_at)
67
- return refuse(`no open account ${String(body.hubId)}`);
68
- superAdmin = body.superAdmin === true;
69
- }
70
- else {
71
- const name = typeof body.account === 'string' ? body.account.trim() : '';
72
- if (!name)
73
- return refuse('an account name (a new account) or a hubId (an account to join) is required');
74
- hubId = 146000000 + stored('_account', root).length + 1;
75
- const subscription = typeof body.subscription === 'string' ? body.subscription : 'free';
76
- // a paid subscription's commitment term ends on `renewsOn` (a date), when it renews unless its auto-renewal was cancelled
77
- const renewsOn = typeof body.renewsOn === 'string' && /^\d{4}-\d{2}-\d{2}$/.test(body.renewsOn) ? body.renewsOn : null;
78
- if (subscription !== 'free' && !renewsOn)
79
- return refuse('a paid subscription needs the date its commitment term ends (renewsOn)');
80
- await keep('_account', `account:${hubId}`, { hubId, name, subscription, renews_on: renewsOn, auto_renew: subscription !== 'free', created_at: at, closed_at: null }, root, at, email);
81
- superAdmin = true;
82
- }
83
- const n = stored('_person', root).length + 1;
84
- const person = { email: email.toLowerCase(), hash: passwordHash(email, password), hubId, userId: 80000000 + n, ownerId: String(90000000 + n), firstName, lastName, superAdmin, created_at: at };
85
- await keep('_person', `person:${email.toLowerCase()}`, person, root, at, email);
86
- return Response.json({ hubId, userId: person.userId, ownerId: person.ownerId }, { status: 201 });
87
- }
88
- /** The subscription an account has at `at`: a paid one whose auto-renewal was cancelled ends on its renewal date, when
89
- * "your account will be downgraded to HubSpot's free tools"
90
- * (https://knowledge.hubspot.com/account/how-do-i-cancel-my-hubspot-account); one that renews carries on. */
91
- export function subscriptionAt(account, at) {
92
- const paid = String(account.subscription ?? 'free');
93
- if (paid === 'free' || account.auto_renew !== false || !account.renews_on)
94
- return paid;
95
- return Date.parse(at) >= Date.parse(`${String(account.renews_on)}T00:00:00Z`) ? 'free' : paid;
96
- }
97
- // ── sessions ───────────────────────────────────────────────────────────────────────────────
98
- export const SESSION_COOKIE = 'hs_session';
99
- /** The person the request's session cookie names, or undefined. */
100
- export function signedIn(request, root) {
101
- const cookie = request.headers.get('cookie') ?? '';
102
- const token = new RegExp(`(?:^|;\\s*)${SESSION_COOKIE}=([^;]+)`).exec(cookie)?.[1];
103
- const session = token ? stored('_web_session', root).find((s) => s.token === token) : undefined;
104
- return session ? personOf(String(session.email), root) : undefined;
105
- }
106
- // ── apps and installs ──────────────────────────────────────────────────────────────────────
107
- export function appByClientId(clientId, root) {
108
- return stored('_app', root).find((a) => a.client_id === clientId && !a.deleted_at);
109
- }
110
- export function appById(appId, root) {
111
- return stored('_app', root).find((a) => a.appId === appId && !a.deleted_at);
112
- }
113
- export function appsOf(hubId, root) {
114
- return stored('_app', root).filter((a) => a.hubId === hubId && !a.deleted_at);
115
- }
116
- /** The live installs in an account. */
117
- export function installsIn(hubId, root) {
118
- return stored('_install', root).filter((i) => i.hubId === hubId && !i.uninstalled_at);
119
- }
120
- export function installOf(appId, hubId, root) {
121
- return installsIn(hubId, root).find((i) => i.appId === appId);
122
- }
@@ -1,10 +0,0 @@
1
- export type HubspotArea = {
2
- id: string;
3
- title: string;
4
- /** Present ⇒ the area is DEFERRED: a separate HubSpot product, not built here, and this is why. */
5
- reason?: string;
6
- };
7
- export declare const HUBSPOT_AREAS: readonly HubspotArea[];
8
- export declare function areaById(id: string): HubspotArea;
9
- /** The area ids `assertAreaCensus` is given. */
10
- export declare const HUBSPOT_AREA_IDS: readonly string[];
@@ -1,114 +0,0 @@
1
- export const HUBSPOT_AREAS = [
2
- // ── modeled: the CRM ──────────────────────────────────────────────────────────────────────
3
- { id: 'objects', title: 'CRM objects — contacts, companies, deals, tickets (basic + batch CRUD)' },
4
- { id: 'search', title: 'CRM Search API — filterGroups, free-text query, sorting, paging' },
5
- { id: 'properties', title: 'CRM properties and property groups' },
6
- { id: 'associations', title: 'CRM associations v4 — labeled, default, batch' },
7
- { id: 'pipelines', title: 'CRM pipelines and pipeline stages (deals, tickets)' },
8
- { id: 'owners', title: 'CRM owners' },
9
- { id: 'protocol', title: 'Protocol fidelity — error envelope, paging, rate-limit headers, read-only' },
10
- { id: 'connector', title: 'Connector lifecycle — pull from / push to a real portal' },
11
- { id: 'ui', title: 'The CRM mirror — record lists, the deal pipeline board, record detail' },
12
- // ── deferred: the other HubSpot products behind api.hubapi.com ────────────────────────────
13
- {
14
- id: 'crm-engagements',
15
- title: 'CRM engagements — calls, emails, meetings, notes, tasks, communications, postal mail, leads, feedback submissions, goals',
16
- // IN SCOPE, not built: a plain todo family. See the scope ruling above.
17
- },
18
- {
19
- id: 'crm-commerce',
20
- title: 'CRM commerce — products, line items, quotes, invoices, taxes, deal splits, subscriptions, payments',
21
- // IN SCOPE, not built: a plain todo family. See the scope ruling above.
22
- },
23
- {
24
- id: 'crm-schemas',
25
- title: 'CRM custom object schemas — object type definitions, custom object records (objectTypeId 2-N / fullyQualifiedName p{portalId}_name)',
26
- // IN SCOPE, not built: a plain todo family. See the scope ruling above.
27
- },
28
- {
29
- id: 'crm-lists',
30
- title: 'CRM lists — static and dynamic list membership, list filters (58 generated operations)',
31
- // IN SCOPE, not built: a plain todo family. See the scope ruling above.
32
- },
33
- {
34
- id: 'crm-imports-exports',
35
- title: 'CRM imports and exports — bulk file-based record import/export jobs',
36
- // IN SCOPE, not built: a plain todo family. See the scope ruling above.
37
- },
38
- {
39
- id: 'crm-extensions',
40
- title: 'CRM extensions — calling, CRM cards, video conferencing, timeline events (57 generated operations)',
41
- // IN SCOPE, not built: a plain todo family. See the scope ruling above.
42
- },
43
- {
44
- id: 'cms',
45
- title: 'CMS — pages, blogs, HubDB, domains, URL redirects, source code, site search, audit logs (306 generated operations)',
46
- reason: 'The CMS is HubSpot\'s website product: a page builder, a blog engine, a hosted database '
47
- + '(HubDB), a source-code filesystem and a rendered-site search index. It is by far the '
48
- + 'largest family in the SDK and shares nothing with the CRM object model beyond the host. A '
49
- + 'separate build; nothing answers for it today.',
50
- },
51
- {
52
- id: 'marketing',
53
- title: 'Marketing — marketing emails, forms, marketing events, transactional email (109 generated operations)',
54
- reason: 'The marketing family sends real email and hosts real forms. Its core act — DELIVERING a '
55
- + 'marketing or transactional message to a real inbox — is out of a local twin\'s reach in the '
56
- + 'same way every other vendor\'s send path is, and the surrounding CRUD is a separate build. '
57
- + 'Not started.',
58
- },
59
- {
60
- id: 'automation',
61
- title: 'Automation — custom workflow actions',
62
- reason: 'Custom workflow actions register an app\'s own callback into HubSpot\'s workflow engine; '
63
- + 'twinning them faithfully means twinning the ENGINE that invokes them. Not started.',
64
- },
65
- {
66
- id: 'conversations',
67
- title: 'Conversations — inbox threads, messages, visitor identification',
68
- reason: 'The conversations inbox is a real-time messaging product with its own thread/actor model and '
69
- + 'a websocket-backed live inbox. Not started.',
70
- },
71
- {
72
- id: 'files',
73
- title: 'Files — the file manager, folders, signed URLs (21 generated operations)',
74
- reason: 'The file manager stores and serves real binary assets from HubSpot\'s CDN. Not started.',
75
- },
76
- {
77
- id: 'analytics-events',
78
- title: 'Events — custom behavioral events, event definitions, the events API',
79
- reason: 'Behavioral events are an analytics ingest-and-aggregate product; the read side is a '
80
- + 'query engine over ingested events. Not started.',
81
- },
82
- {
83
- id: 'communication-preferences',
84
- title: 'Communication preferences — subscription definitions, subscribe/unsubscribe status',
85
- reason: 'Subscription preferences govern real marketing-email delivery consent, and are meaningful '
86
- + 'only alongside the marketing family above. Not started.',
87
- },
88
- {
89
- id: 'settings',
90
- title: 'Settings — portal users, business units',
91
- reason: 'Account settings model the PORTAL rather than its CRM data (seat management, business-unit '
92
- + 'partitioning). Not started.',
93
- },
94
- {
95
- id: 'webhooks',
96
- title: 'Webhooks — app-level event subscriptions and delivery',
97
- reason: 'Webhook subscriptions are configured per developer APP, not per portal, and their point is '
98
- + 'DELIVERY of signed events to a subscriber URL. Not started — and note the twin ships no '
99
- + 'emitter, so no HubSpot event is synthesized either.',
100
- },
101
- {
102
- id: 'oauth',
103
- title: 'OAuth — authorization code exchange, refresh tokens, access-token metadata',
104
- },
105
- ];
106
- const BY_ID = new Map(HUBSPOT_AREAS.map((a) => [a.id, a]));
107
- export function areaById(id) {
108
- const found = BY_ID.get(id);
109
- if (!found)
110
- throw new Error(`hubspot: unknown area '${id}' — add it to HUBSPOT_AREAS`);
111
- return found;
112
- }
113
- /** The area ids `assertAreaCensus` is given. */
114
- export const HUBSPOT_AREA_IDS = HUBSPOT_AREAS.map((a) => a.id);
@@ -1,58 +0,0 @@
1
- import { RateBudget, type RateBudgetDeclaration, type RateBudgetOptions, type RateBudgetReservation, type RateBudgetSnapshot } from '@volter/world-core';
2
- /** Rolling window, in ms. Spend older than this is pruned. */
3
- export declare const HUBSPOT_BUDGET_WINDOW_MS = 60000;
4
- /**
5
- * Weighted units allowed inside one window. 180/60s = 180 ordinary calls a minute — under a THIRD
6
- * of HubSpot's tightest published tier converted to a minute (Free/Starter: 100 requests per 10
7
- * seconds for a privately distributed app = 600/minute).
8
- */
9
- export declare const HUBSPOT_BUDGET_CEILING = 180;
10
- /** Seconds. A `Retry-After` above this means the token is throttled hard — fail loudly, don't sleep. */
11
- export declare const HUBSPOT_BUDGET_MAX_RETRY_AFTER_S = 300;
12
- /** Per-call cost, keyed by `"<METHOD> <path>"`. See the header for documented vs. judged. */
13
- export declare const HUBSPOT_CALL_WEIGHTS: {
14
- /** CRM Search — HubSpot's own five-requests-per-second burst limit, ~1/3 the general rate. */
15
- readonly search: 3;
16
- /** Batch object calls — one WRITE mutates up to 100 real CRM records (a human blast radius),
17
- * and the anchored rule prices the whole `/batch/` family, READS included, deliberately: a
18
- * 100-record read is the heaviest single request this API serves and pricing it as a write is
19
- * the conservative direction. Named `batchWrite` for the case that motivated the number. */
20
- readonly batchWrite: 5;
21
- /** Batch association writes — the same shape at a smaller scale. */
22
- readonly batchAssociation: 3;
23
- /** Everything else: single-record CRUD, properties, pipelines, owners, association reads. */
24
- readonly other: 1;
25
- };
26
- /** THE PACK'S DECLARATION — pure data, the only HubSpot-specific thing in the whole budget. */
27
- export declare const HUBSPOT_RATE_BUDGET: RateBudgetDeclaration;
28
- /**
29
- * Price one call. The key is `"<METHOD> <path>"` (the absolute API path the connector states,
30
- * without a host) with the query string split off, so a rule can price by method without the
31
- * kernel knowing anything about HubSpot. An unclassified endpoint still costs `defaultWeight` —
32
- * nothing is ever free.
33
- */
34
- export declare function hubspotCallWeight(method: string, path: string): number;
35
- /** Where HubSpot's ledger lives. Token-keyed and cwd-independent by default (the limit is per app
36
- * / per private-app token, so a cwd-scoped ledger would hand the same token a fresh allowance in
37
- * every checkout, worktree and CI matrix leg); pass `root` to opt into world-scoped accounting. */
38
- export declare function hubspotBudgetPath(opts?: {
39
- root?: string;
40
- token?: string;
41
- } | string): string;
42
- /** Construction options for HubSpot's budget. The vendor is fixed; everything else may only TIGHTEN. */
43
- export type HubspotBudgetOptions = Omit<RateBudgetOptions, 'vendor'>;
44
- /**
45
- * HubSpot's budget — the shared kernel guard bound to this vendor's declaration. A real subclass,
46
- * not an alias, so `budget instanceof HubspotBudget` in `liveHubspotExecute` means "a budget that
47
- * accounts against HUBSPOT's ledger under HUBSPOT's ceiling": another vendor's `RateBudget` (with
48
- * its own, possibly larger, ceiling) is NOT assignable there.
49
- */
50
- export declare class HubspotBudget extends RateBudget {
51
- constructor(opts?: HubspotBudgetOptions);
52
- }
53
- /** The typed refusal. One error class shared with every other vendor's budget; `err.vendor` says
54
- * which one refused, and `err.kind` says why. */
55
- export { RateBudgetError as HubspotBudgetError } from '@volter/world-core';
56
- export type { RateBudgetErrorKind as HubspotBudgetErrorKind } from '@volter/world-core';
57
- export type HubspotBudgetReservation = RateBudgetReservation;
58
- export type HubspotBudgetSnapshot = RateBudgetSnapshot;