@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
package/src/index.ts CHANGED
@@ -1,152 +1,12 @@
1
- // @volter/twin-hubspot — the HubSpot CRM twin (one vendor, one package), built on the shared
2
- // @volter/world-core kernel. REST transport over HubSpot's real v3/v4 CRM paths, the vendor's own
3
- // `{ results, paging: { next: { after } } }` collection envelope and
4
- // `{ status:'error', message, correlationId, category }` error envelope, stateful CRM records
5
- // (contacts / companies / deals / tickets) with properties, associations, pipelines and owners.
6
- // The CRM API accepts any bearer; the OAuth install (consent and token endpoints) mints the tokens an app is given.
7
- // Served as a derived pack (Protocol 3, src/manifest.ts). Conformance tooling lives in
8
- // @volter/world-tooling (a dev dependency) — not shipped in the runtime API.
9
- export {
10
- handleHubspotTwinRequest,
11
- correlationIdFor,
12
- resolveObjectType,
13
- HUBSPOT_RATE_LIMIT_HEADERS,
14
- LIST_LIMIT_MAX,
15
- OBJECT_TYPES,
16
- RESOURCE_TYPES,
17
- SEARCH_FILTER_OPERATORS,
18
- SEARCH_LIMIT_MAX,
19
- SEARCH_MAX_FILTERS_PER_GROUP,
20
- SEARCH_MAX_FILTERS_TOTAL,
21
- SEARCH_MAX_FILTER_GROUPS,
22
- SEARCH_RESULT_CAP,
23
- } from './hubspot-twin.ts';
24
- export type { HubspotRequest, HubspotResponse, ObjectType } from './hubspot-twin.ts';
25
- export { createHubspotTwinFetch, createHubspotTwinServer, type HubspotTwinFetchOptions } from './hubspot-server.ts';
26
- export {
27
- hubspotRequestForAction,
28
- liveHubspotExecute,
29
- mapCrmObject,
30
- mapOwner,
31
- pollTimestamp,
32
- pullHubspotAll,
33
- pullHubspotObjects,
34
- pullHubspotOwners,
35
- pushPendingHubspotActions,
36
- syncHubspotFromReal,
37
- } from './hubspot-connector.ts';
38
- export type { HubspotErrorBody, HubspotExecute, LiveHubspotOptions } from './hubspot-connector.ts';
39
- // The client-side rate budget — the fail-closed backstop `liveHubspotExecute` routes every live
40
- // request through. The MECHANISM is the kernel's shared, vendor-agnostic `RateBudget`; what lives
41
- // here is HubSpot's DECLARATION (window/ceiling/per-endpoint weights) plus the vendor-bound
42
- // bindings. Exported so an operator can inspect spend (`snapshot`) and so a caller can catch
43
- // `HubspotBudgetError` by type; there is deliberately no export that disables the guard.
44
- export {
45
- HUBSPOT_BUDGET_CEILING,
46
- HUBSPOT_BUDGET_MAX_RETRY_AFTER_S,
47
- HUBSPOT_BUDGET_WINDOW_MS,
48
- HUBSPOT_CALL_WEIGHTS,
49
- HUBSPOT_RATE_BUDGET,
50
- HubspotBudget,
51
- HubspotBudgetError,
52
- hubspotBudgetPath,
53
- hubspotCallWeight,
54
- } from './hubspot-budget.ts';
55
- export type { HubspotBudgetErrorKind, HubspotBudgetOptions, HubspotBudgetReservation, HubspotBudgetSnapshot } from './hubspot-budget.ts';
56
- export {
57
- MIRROR_SECTIONS,
58
- SECTION_COLUMNS,
59
- TITLE_PROPERTY,
60
- buildHubspotMirrorClient,
61
- createHubspotMirrorServer,
62
- formatAmount,
63
- groupByStage,
64
- hubspotMirrorHtml,
65
- recordTitle,
66
- stageTone,
67
- } from './hubspot-mirror-ui.ts';
68
- export type { HubspotRow, MirrorSectionKey, PillTone } from './hubspot-mirror-ui.ts';
69
-
70
- // Registry descriptor: the pack self-describes so tooling can discover it.
71
- import { registerPack, type TwinPack } from '@volter/world-core';
72
- import { performHubspotAction, syncHubspotFromRemote } from './hubspot-connector.ts';
73
- import { HUBSPOT_RATE_BUDGET as RATE_BUDGET } from './hubspot-budget.ts';
74
- export const pack: TwinPack = {
75
- // PROTOCOL 2 (docs/contributing/architecture.md#protocol-2-the-pack-is-a-plugin): the pack is a plugin — its wire, its tree, and its half of
76
- // the real state system. Moved 2026-09-08.
77
- protocol: '2',
78
- // HubSpot publishes webhooks for CRM object changes, which is what actually moves; the refresh
79
- // walks every object type and is the backstop.
80
- refresh: { every: '15m', webhook: true, onDemand: { atMost: '30s' } },
81
- stateSystem: { perform: performHubspotAction, refresh: syncHubspotFromRemote },
82
- // The round trip is a CONTACT create — HubSpot's own write, and the portal mints a fresh record
83
- // id for every one, so a second send on a branch is a second contact rather than a collision.
84
- roundTrip: { method: 'POST', path: '/crm/v3/objects/contacts', body: { properties: { email: 'round@trip.test', firstname: 'Round', lastname: 'Trip' } }, headers: { authorization: 'Bearer twin' } },
85
- parityOrigin: 'http://twin',
86
-
87
- vendor: 'hubspot',
88
- // The SAME object hubspot-budget.ts declares at module load — one source of truth, so
89
- // registering the pack and importing the connector can never arm two different ceilings.
90
- rateBudget: RATE_BUDGET,
91
- transport: 'rest',
92
- archetype: 'crud',
93
- bin: 'world-hubspot',
94
- // R2 adopted-as-debt, legibly: declared types no replay can create, each with its reason.
95
- resourcesUnreachable: {
96
- 'owner': "a portal user provisioned in HubSpot's admin, never through the CRM API",
97
- },
98
- resources: ['crm_object', 'property', 'property_group', 'pipeline', 'association', 'owner'],
99
- specSource:
100
- "nine documents of HubSpot's public spec catalog (api.hubspot.com/public/api/spec/v1/specs): Contacts, Companies, "
101
- + 'Deals, Tickets, Crm Owners, Properties and Pipelines v3, Associations v4 and OAuth v1, vendored under spec/openapi/ '
102
- + '(provenance in spec/SOURCE.md) and read as their union; the calls dubinc/dub makes (apps/web/lib/integrations/hubspot/).',
103
- description: 'HubSpot CRM twin — v3/v4 objects, properties, associations, pipelines and owners, with the vendor error envelope, paging.next.after and a CRM mirror.',
104
- // A browser-side HubSpot call goes to api.hubapi.com/crm/… — the dev proxy forwards '/crm/'
105
- // to the twin and strips the absolute host so calls come back same-origin.
106
- browserRouting: { apiPathPrefix: '/crm/', loaderHost: 'https://api.hubapi.com' },
107
-
108
- // ADOPTION — how an app repo betrays that it talks to HubSpot.
109
- // • `@hubspot/api-client` is the ONE official npm client of the surface modeled here.
110
- // • The `@hubspot/` scope is NOT claimed wholesale: it also carries the CMS/CLI toolchain
111
- // (`@hubspot/cli`, `@hubspot/local-dev-lib`, the `@hubspot/cms-components` family), which
112
- // are build tooling for a product area this twin explicitly does not model. Claiming the
113
- // scope would report a CMS-theme repo as covered by a CRM twin — the exact lie `covers`
114
- // exists to prevent (the `googleapis` precedent in covers.ts).
115
- // • `HUBSPOT` is the credential stem: HUBSPOT_ACCESS_TOKEN / HUBSPOT_API_KEY /
116
- // HUBSPOT_CLIENT_ID / HUBSPOT_CLIENT_SECRET all normalize to it under covers.ts's
117
- // ENV_SUFFIX rules (framework prefix stripped, suffix matched, remainder lowercased with
118
- // punctuation removed). `HUBSPOT_PRIVATE_APP_TOKEN` — the name HubSpot's own private-app
119
- // docs use — stems to `hubspotprivateapp` instead, because `_TOKEN` is the matched suffix,
120
- // so it is claimed explicitly rather than assumed.
121
- adoption: {
122
- // HubSpot's official Python client, the counterpart of `@hubspot/api-client`.
123
- pypi: ['hubspot-api-client'],
124
- sdks: ['@hubspot/api-client'],
125
- envStems: ['HUBSPOT', 'HUBSPOTPRIVATEAPP'],
126
- },
127
-
128
- // INTERCEPTION — api.hubapi.com is the SDK's only API server: every generated
129
- // `servers.ts` in @hubspot/api-client 14.0.1 declares exactly
130
- // `new ServerConfiguration("https://api.hubapi.com", {})`, and a sweep of the whole published
131
- // tarball finds no other API host. `app.hubspot.com` appears once, in
132
- // `OauthDiscovery.getAuthorizationUrl`: the URL a person is sent to for consent, which the twin serves
133
- // (hubspot-oauth.tsx).
134
- // app.hubspot.com carries every page the twin builds (sign-in, consent, apps, records, forms, settings) and
135
- // api.hsforms.com a form's submission (src/screens/).
136
- hosts: [{ host: 'api.hubapi.com' }, { host: 'app.hubspot.com' }, { host: 'api.hsforms.com' }],
137
-
138
- // WORLD WIRING — `endpointEnvNone`, and this is a ruling, not an omission. The SDK IS
139
- // base-URL-configurable, but through a CONSTRUCTOR OPTION (`IConfiguration.basePath`, read by
140
- // `ApiClientConfigurator.getBaseServer`), never an environment variable: nothing in
141
- // @hubspot/api-client 14.0.1 reads `process.env`, and HubSpot documents no `HUBSPOT_BASE_URL`.
142
- // Inventing one would make `covers` report a world covered while the app, reading no such var,
143
- // still talked to the real portal. Interception is the `hosts` entry above.
144
- endpointEnvNone:
145
- 'the @hubspot/api-client SDK takes its base URL from the `basePath` CONSTRUCTOR option '
146
- + '(IConfiguration.basePath → ApiClientConfigurator.getBaseServer), not from any environment '
147
- + 'variable — it reads no process.env at all, and HubSpot documents no base-URL env var. '
148
- + 'Interception is the api.hubapi.com host entry; inventing a HUBSPOT_BASE_URL the app does '
149
- + 'not read would report a world covered while its traffic still left the machine.',
150
- };
151
-
152
- registerPack(pack);
1
+ // @volter/twin-hubspot — the hubspot twin (Protocol 3). A fixed file (create-pack): the descriptor is the manifest's, and the
2
+ // kernel derives the vendor-backed half from the generated surface, unless the manifest declares none (vendorBacked.none;
3
+ // architecture, "The real-system adapters").
4
+ import { packOf, registerPack, type DerivedSurface } from '@volter/world-core';
5
+ import { manifest } from './manifest.ts';
6
+ import surface from './generated/surface.gen.json' with { type: 'json' };
7
+
8
+ export { createHubspotFetch } from './fetch.ts';
9
+ export { createHubspotTwinServer } from './server.ts';
10
+ export { manifest };
11
+
12
+ export const pack = registerPack(packOf(manifest, surface as unknown as DerivedSurface));
package/src/manifest.ts CHANGED
@@ -1,96 +1,37 @@
1
- // HubSpot's manifest: the vendor facts its published documents do not carry (docs/contributing/architecture.md,
2
- // "Protocol 3"). The surface is generated (./generated/surface.gen.json, from ../spec by scripts/derive-pack.ts): the
3
- // union of nine documents from HubSpot's public spec catalog (the eight CRM documents the pack serves and OAuth v1,
4
- // ../spec/SOURCE.md), their one OAuth scheme's scope lists made one (../spec/patches.json). The documents name every
5
- // operation, so an operation's id is HubSpot's own (`post-/crm/v3/properties/{objectType}/batch/create_create`).
6
- //
7
- // THE HANDLERS. Every operation the twin serves is a semantics handler (semantics/crm.ts) over the pack's request
8
- // handler (hubspot-twin.ts), whose store is the vendor's behaviour, or over the OAuth install's token endpoints
9
- // (hubspot-oauth.tsx). The derived core owns no operation, so no resource is declared here and no state candidate is
10
- // ruled: the spec's resources (a record, a property, a pipeline and its stages, an owner) are stored by the request
11
- // handler in its own shape. An operation no handler serves answers the gap (hubspot-server.ts).
12
- //
13
- // WHY NO `auth`. HubSpot reads `Authorization: Bearer <access token or static app token>`. A token the twin issued (an
14
- // install's access token, a static app's token) acts in its account (portal.ts) and is refused once expired, its app
15
- // uninstalled or its account closed (hubspot-oauth.tsx, tokenPortal); any other bearer acts in the World's own account,
16
- // as it did before accounts were modelled, and the kernel's `auth` names keys the twin refuses, never the ones it
17
- // accepts. Where the twin stops: no operation is refused for a scope its token lacks.
18
- //
19
- // THE ERROR BODY. `{status: "error", message, correlationId, category}`, HubSpot's documented error envelope
20
- // (https://developers.hubspot.com/docs/api-reference/error-handling), which the request handler answers with a
21
- // correlationId derived from the request (so a replay is byte-identical). The template below is the core's, which
22
- // serves nothing.
23
- //
24
- // THE STATE. The request handler's kernel subjects: `crm_object` (a record, keyed `<objectType>_<id>`), `property`,
25
- // `property_group`, `pipeline` (its stages inside), `owner` (pulled or the seeded portal owner) and `association`;
26
- // the OAuth install's `oauth_code` and `oauth_token`.
1
+ // Demand is recorded in journeys/demand.json; retained operations still answer the vendor-shaped gap.
27
2
  import type { DerivedManifest } from '@volter/world-core';
28
3
 
29
4
  export const manifest: DerivedManifest = {
30
- vendor: 'hubspot',
31
- service: 'hubspot',
32
- body: {},
33
- // the core mints no id (it owns no operation); a record's id is the request handler's
34
- ids: { template: '{prefix}_{n}' },
35
- time: 'iso',
36
- error: { status: 'error', message: '{message}', category: '{code}' },
37
- readOnly: { status: 405, message: 'this twin was started read-only; omit readOnly to accept writes', code: 'TWIN_READ_ONLY' },
38
- malformedBody: { status: 400, message: 'Invalid input JSON', code: 'VALIDATION_ERROR' },
39
- notFound: { status: 404, message: 'resource not found', code: 'OBJECT_NOT_FOUND' },
40
- // `{results, paging: {next: {after}}}`, paged by `after` and `limit` (10 by default, 100 at most: the documents'
41
- // getPage operations). Required by the type; the core lists nothing.
42
- list: { style: 'envelope', envelope: { results: '{data}' }, limit: { param: 'limit', default: 10, max: 100 }, after: 'after' },
43
- deleted: {},
44
- // HubSpot's pages the life walks (src/screens/, src/hubspot-oauth.tsx), each built from @volter/world-ui's pieces.
45
- screens: [
46
- {
47
- id: 'sign-in', kind: 'flow', host: 'app.hubspot.com', path: '/login',
48
- demand: 'every person on a HubSpot page is who signed in there', status: 'done', controls: ['Email address', 'Password', 'Log in'],
49
- source: 'https://knowledge.hubspot.com/account-management/log-in-to-hubspot',
50
- },
51
- {
52
- id: 'oauth-consent', kind: 'flow', host: 'app.hubspot.com', path: '/oauth/authorize',
53
- demand: "Dub's HubSpot integration sends a workspace owner here to connect a portal (apps/web/lib/integrations/hubspot/oauth.ts, authUrl)",
54
- status: 'done', controls: ['Select an account', 'Connect app', 'Cancel', 'Type I accept the risk', 'Connect'],
55
- source: 'https://developers.hubspot.com/docs/apps/developer-platform/build-apps/manage-apps-in-hubspot',
56
- },
57
- {
58
- id: 'app-auth', kind: 'workspace', host: 'app.hubspot.com', path: '/developer-projects/:hubId/project/:name/component/:uid/auth',
59
- demand: "an app's client ID and secret are read here", status: 'done',
60
- source: 'https://developers.hubspot.com/docs/apps/developer-platform/build-apps/manage-apps-in-hubspot',
61
- },
62
- {
63
- id: 'app-distribution', kind: 'workspace', host: 'app.hubspot.com', path: '/developer-projects/:hubId/project/:name/component/:uid/distribution',
64
- demand: 'an OAuth marketplace app is published (Begin publishing, the AUP) before any install; a static app is installed and its token shown here',
65
- status: 'done', controls: ['Begin publishing', 'Sign', 'Install now', 'Show'],
66
- source: 'https://developers.hubspot.com/docs/apps/developer-platform/build-apps/manage-apps-in-hubspot',
67
- },
68
- {
69
- id: 'records', kind: 'workspace', host: 'app.hubspot.com', path: '/contacts/:hubId/record/:objectTypeId/:id',
70
- demand: "reps create deals on a contact's record and move a deal's stage; account managers add companies", status: 'done',
71
- controls: ['Create deal', 'Deal stage', 'Save', 'Create company'], source: 'https://knowledge.hubspot.com/records/create-records-on-the-updated-index-page',
72
- },
73
- {
74
- id: 'pipelines', kind: 'workspace', host: 'app.hubspot.com', path: '/settings/:hubId/objects/deals/pipelines',
75
- demand: "a custom pipeline's closed-won stage ID is read here (Dub's guide: \"enter the stage ID corresponding to your custom Closed Won stage\")",
76
- status: 'done', source: 'https://knowledge.hubspot.com/object-settings/set-up-and-customize-pipelines',
77
- },
78
- {
79
- id: 'forms', kind: 'workspace', host: 'app.hubspot.com', path: '/forms/:hubId',
80
- demand: "Dub's guide: \"In the HubSpot form builder, add a hidden field and map it to the Dub Id contact property\"", status: 'done',
81
- controls: ['Create form', 'Add hidden field'], source: 'https://dub.co/docs/integrations/hubspot',
82
- },
83
- {
84
- id: 'connected-apps', kind: 'workspace', host: 'app.hubspot.com', path: '/connected-apps/:hubId/installed',
85
- demand: 'a client uninstalls the app it connected', status: 'done', controls: ['Uninstall', 'Type uninstall'],
86
- source: 'https://knowledge.hubspot.com/marketplace/install-apps-in-the-hubspot-marketplace',
87
- },
88
- {
89
- id: 'account-and-billing', kind: 'workspace', host: 'app.hubspot.com', path: '/account-and-billing/:hubId/subscriptions',
90
- demand: "a company leaving HubSpot cancels its subscription's auto-renewal, then deletes the account once the subscription has ended",
91
- status: 'done', controls: ['Cancel auto-renewal', 'Next', 'Delete account', 'Enter your Hub ID to confirm', 'Yes, delete'],
92
- source: 'https://knowledge.hubspot.com/account/how-do-i-cancel-my-hubspot-account',
93
- },
94
- ],
95
- resources: {},
5
+ vendor: 'hubspot', service: 'hubspot', body: { json: 'always', form: { coerce: false } },
6
+ ids: { template: '{n}' }, time: 'iso',
7
+ // source: https://developers.hubspot.com/docs/api-reference/error-handling "return a JSON formatted response with details about the error"
8
+ error: { status: 'error', message: '{message}', correlationId: '{id}' },
9
+ readOnly: { status: 403, message: 'This World is read-only.' },
10
+ // source: https://developers.hubspot.com/docs/api-reference/webhooks-webhooks-v3/guide "404 Not Found : requested resource not found"
11
+ // The documentation gives the not-found class, not words for every unknown path; use that class in its documented JSON envelope.
12
+ notFound: { status: 404, message: 'requested resource not found' },
13
+ gap: { status: 404, message: 'requested resource not found' },
14
+ // source: https://developers.hubspot.com/docs/api-reference/webhooks-webhooks-v3/guide "Authorization : Bearer YOUR_ACCESS_TOKEN"
15
+ // source: https://developers.hubspot.com/docs/api-reference/error-handling "Returned when the authentication provided is invalid."
16
+ // Where documentation stops: The 401 rule describes invalid authentication, not missing-key versus unknown-route precedence; the gap stands.
17
+ auth: { header: 'authorization', scheme: 'Bearer', paths: '^/crm/', gateWhenAbsent: true,
18
+ missing: { status: 401, message: 'Unauthorized' }, invalid: { status: 401, message: 'Unauthorized' }, invalidKeys: ['invalid'] },
19
+ deleted: {}, resources: {},
20
+ unmodeled: ["get-/crm/v3/objects/0-3_getPage", "post-/crm/v3/objects/0-3_create", "get-/crm/v3/objects/0-3/{dealId}_getById", "patch-/crm/v3/objects/0-3/{dealId}_update", "delete-/crm/v3/objects/0-3/{dealId}_archive", "post-/crm/v3/objects/0-3/batch/archive_archive", "post-/crm/v3/objects/0-3/batch/create_create", "post-/crm/v3/objects/0-3/batch/read_read", "post-/crm/v3/objects/0-3/batch/update_update", "post-/crm/v3/objects/0-3/batch/upsert_upsert", "post-/crm/v3/objects/0-3/merge_merge", "post-/crm/v3/objects/0-3/search_doSearch", "get-/crm/v3/objects/companies_getPage", "post-/crm/v3/objects/companies_create", "get-/crm/v3/objects/companies/{companyId}_getById", "patch-/crm/v3/objects/companies/{companyId}_update", "delete-/crm/v3/objects/companies/{companyId}_archive", "post-/crm/v3/objects/companies/batch/archive_archive", "post-/crm/v3/objects/companies/batch/create_create", "post-/crm/v3/objects/companies/batch/read_read", "post-/crm/v3/objects/companies/batch/update_update", "post-/crm/v3/objects/companies/batch/upsert_upsert", "post-/crm/v3/objects/companies/merge_merge", "post-/crm/v3/objects/companies/search_doSearch", "get-/crm/v3/objects/contacts_getPage", "post-/crm/v3/objects/contacts_create", "get-/crm/v3/objects/contacts/{contactId}_getById", "patch-/crm/v3/objects/contacts/{contactId}_update", "delete-/crm/v3/objects/contacts/{contactId}_archive", "post-/crm/v3/objects/contacts/batch/archive_archive", "post-/crm/v3/objects/contacts/batch/create_create", "post-/crm/v3/objects/contacts/batch/read_read", "post-/crm/v3/objects/contacts/batch/update_update", "post-/crm/v3/objects/contacts/batch/upsert_upsert", "post-/crm/v3/objects/contacts/gdpr-delete_purge", "post-/crm/v3/objects/contacts/merge_merge", "post-/crm/v3/objects/contacts/search_doSearch", "get-/crm/v3/objects/meetings_getPage", "post-/crm/v3/objects/meetings_create", "get-/crm/v3/objects/meetings/{meetingId}_getById", "patch-/crm/v3/objects/meetings/{meetingId}_update", "delete-/crm/v3/objects/meetings/{meetingId}_archive", "post-/crm/v3/objects/meetings/batch/archive_archive", "post-/crm/v3/objects/meetings/batch/create_create", "post-/crm/v3/objects/meetings/batch/read_read", "post-/crm/v3/objects/meetings/batch/update_update", "post-/crm/v3/objects/meetings/batch/upsert_upsert", "post-/crm/v3/objects/meetings/search_doSearch", "get-/crm/v3/objects/tickets_getPage", "post-/crm/v3/objects/tickets_create", "get-/crm/v3/objects/tickets/{ticketId}_getById", "patch-/crm/v3/objects/tickets/{ticketId}_update", "delete-/crm/v3/objects/tickets/{ticketId}_archive", "post-/crm/v3/objects/tickets/batch/archive_archive", "post-/crm/v3/objects/tickets/batch/create_create", "post-/crm/v3/objects/tickets/batch/read_read", "post-/crm/v3/objects/tickets/batch/update_update", "post-/crm/v3/objects/tickets/batch/upsert_upsert", "post-/crm/v3/objects/tickets/merge_merge", "post-/crm/v3/objects/tickets/search_doSearch", "get-/crm/v3/owners_/crm/v3/owners", "get-/crm/v3/owners/{ownerId}_getById", "get-/crm/v3/pipelines/{objectType}_getAll", "post-/crm/v3/pipelines/{objectType}_create", "get-/crm/v3/pipelines/{objectType}/{pipelineId}_getById", "put-/crm/v3/pipelines/{objectType}/{pipelineId}_replace", "patch-/crm/v3/pipelines/{objectType}/{pipelineId}_update", "delete-/crm/v3/pipelines/{objectType}/{pipelineId}_archive", "get-/crm/v3/pipelines/{objectType}/{pipelineId}/audit_getAudit", "get-/crm/v3/pipelines/{objectType}/{pipelineId}/stages_getAll", "post-/crm/v3/pipelines/{objectType}/{pipelineId}/stages_create", "get-/crm/v3/pipelines/{objectType}/{pipelineId}/stages/{stageId}_getById", "put-/crm/v3/pipelines/{objectType}/{pipelineId}/stages/{stageId}_replace", "patch-/crm/v3/pipelines/{objectType}/{pipelineId}/stages/{stageId}_update", "delete-/crm/v3/pipelines/{objectType}/{pipelineId}/stages/{stageId}_archive", "get-/crm/v3/pipelines/{objectType}/{pipelineId}/stages/{stageId}/audit_getAudit", "get-/crm/v3/properties/{objectType}_getAll", "post-/crm/v3/properties/{objectType}_create", "get-/crm/v3/properties/{objectType}/{propertyName}_getByName", "patch-/crm/v3/properties/{objectType}/{propertyName}_update", "delete-/crm/v3/properties/{objectType}/{propertyName}_archive", "post-/crm/v3/properties/{objectType}/batch/archive_archive", "post-/crm/v3/properties/{objectType}/batch/create_create", "post-/crm/v3/properties/{objectType}/batch/read_read", "get-/crm/v3/properties/{objectType}/groups_getAll", "post-/crm/v3/properties/{objectType}/groups_create", "get-/crm/v3/properties/{objectType}/groups/{groupName}_getByName", "patch-/crm/v3/properties/{objectType}/groups/{groupName}_update", "delete-/crm/v3/properties/{objectType}/groups/{groupName}_archive", "post-/crm/v4/associations/{fromObjectType}/{toObjectType}/batch/archive_archive", "post-/crm/v4/associations/{fromObjectType}/{toObjectType}/batch/associate/default_createDefault", "post-/crm/v4/associations/{fromObjectType}/{toObjectType}/batch/create_create", "post-/crm/v4/associations/{fromObjectType}/{toObjectType}/batch/labels/archive_archiveLabels", "post-/crm/v4/associations/{fromObjectType}/{toObjectType}/batch/read_getPage", "post-/crm/v4/associations/usage/high-usage-report/{userId}_request", "put-/crm/v4/objects/{fromObjectType}/{fromObjectId}/associations/default/{toObjectType}/{toObjectId}_createDefault", "get-/crm/v4/objects/{objectType}/{objectId}/associations/{toObjectType}_getPage", "put-/crm/v4/objects/{objectType}/{objectId}/associations/{toObjectType}/{toObjectId}_create", "delete-/crm/v4/objects/{objectType}/{objectId}/associations/{toObjectType}/{toObjectId}_archive", "get-/oauth/v1/access-tokens/{token}_get", "get-/oauth/v1/refresh-tokens/{token}_get", "delete-/oauth/v1/refresh-tokens/{token}_archive", "post-/oauth/v1/token_create"],
21
+ vendorBacked: { none: 'No vendor resource or operation is served; every spec operation answers the gap.' },
22
+ // source: https://developers.hubspot.com/docs/developer-tooling/platform/usage-guidelines "110 requests every 10 seconds"
23
+ rateBudget: { windowMs: 60000, ceiling: 660, defaultWeight: 1, maxRetryAfterSeconds: 60, reason: 'Public OAuth allowance of 110 requests per 10 seconds, expressed as 660 per minute.',
24
+ allowance: { perMinute: 660, source: 'https://developers.hubspot.com/docs/developer-tooling/platform/usage-guidelines' } },
25
+ discovery: { twinOf: 'HubSpot CRM, Meetings and OAuth v1 APIs', stores: 'No vendor resources',
26
+ behavior: 'Every spec operation answers the vendor-shaped gap; Cal.com and Dub require an optional HubSpot app installation.' },
27
+ descriptor: {
28
+ protocol: '3', transport: 'rest', archetype: 'crud', bin: 'world-hubspot', resources: [],
29
+ specSource: 'HubSpot CRM, Meetings and OAuth v1 OpenAPI documents; provenance in spec/SOURCE.md',
30
+ description: 'HubSpot API twin: every spec operation answers the vendor-shaped gap.',
31
+ adoption: { sdks: ['@hubspot/api-client'], pypi: ['hubspot-api-client'], envStems: ['HUBSPOT', 'HUBSPOTPRIVATEAPP'] },
32
+ hosts: [{ host: 'api.hubapi.com' }, { host: 'app.hubspot.com', demandPathPattern: false }, { host: 'api.hsforms.com' }],
33
+ endpointEnvNone: 'The optional integrations address HubSpot hosts; no SDK base-URL environment variable is injected.',
34
+ // source: https://raw.githubusercontent.com/dubinc/dub/7e0101363b2ab30afc537c1893f159d1dcba513a/apps/web/lib/integrations/hubspot/oauth.ts "process.env.HUBSPOT_CLIENT_ID"
35
+ clientCasesNone: 'Optional integration calls remain unserved; no SDK case is claimed for the retained gap.',
36
+ },
96
37
  };
@@ -0,0 +1,6 @@
1
+ // GENERATED by scripts/create-pack.ts --index: the handler map, by operationId, from the families' files (each
2
+ // handler named as its operation). Do not edit; add a handler to its family's file and generate again.
3
+ import type { Handler } from '@volter/world-core';
4
+
5
+ export const hubspotSemantics: Record<string, Handler> = {
6
+ };
@@ -0,0 +1,2 @@
1
+ // No vendor resources are served, so there are no state machines or notState rulings.
2
+ export const states = {};
package/src/server.ts ADDED
@@ -0,0 +1,8 @@
1
+ // FETCH-FIRST (runtime contract R12b): a fixed file, the kernel's serve seam around the pack's fetch (./fetch.ts).
2
+ import { serveHttp, type PackFetchOptions } from '@volter/world-core';
3
+ import { createHubspotFetch } from './fetch.ts';
4
+
5
+ export async function createHubspotTwinServer(options: PackFetchOptions & { port?: number } = {}): Promise<{ port: number; stop: () => void }> {
6
+ const server = await serveHttp({ port: options.port ?? 0, idleTimeout: 60, fetch: createHubspotFetch(options) });
7
+ return { port: server.port ?? options.port ?? 0, stop: () => server.stop(true) };
8
+ }
@@ -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,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 />);