@volter/twin-hubspot 0.1.36 → 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.
- package/README.md +9 -193
- package/dist/generated/pack-facts.json +42 -0
- package/dist/journeys/coverage.json +60 -0
- package/dist/journeys/customer-life.json +7 -0
- package/dist/journeys/customer-life.outline.md +6 -0
- package/dist/journeys/decisions.json +623 -0
- package/dist/journeys/demand.json +171 -0
- package/dist/journeys/score.json +82 -0
- package/dist/journeys/unreachable.json +12 -0
- package/dist/journeys/vendor-examples.json +7 -0
- package/dist/spec/SOURCE.md +40 -0
- package/dist/spec/openapi/associations.json.gz +0 -0
- package/dist/spec/openapi/companies.json.gz +0 -0
- package/dist/spec/openapi/contacts.json.gz +0 -0
- package/dist/spec/openapi/deals.json.gz +0 -0
- package/dist/spec/openapi/meetings.json.gz +0 -0
- package/dist/spec/openapi/oauth.json.gz +0 -0
- package/dist/spec/openapi/owners.json.gz +0 -0
- package/dist/spec/openapi/pipelines.json.gz +0 -0
- package/dist/spec/openapi/properties.json.gz +0 -0
- package/dist/spec/openapi/tickets.json.gz +0 -0
- package/dist/spec/patches.json +235 -0
- package/dist/spec/sources.json +32 -0
- package/dist/src/cli.js +10 -22
- package/dist/src/fetch.d.ts +2 -0
- package/dist/src/fetch.js +13 -0
- package/dist/src/generated/surface.gen.json +1 -1
- package/dist/src/index.d.ts +5 -11
- package/dist/src/index.js +10 -94
- package/dist/src/manifest.js +32 -66
- package/dist/src/semantics/index.d.ts +2 -0
- package/dist/src/semantics/index.js +1 -0
- package/dist/src/semantics/states.d.ts +1 -0
- package/dist/src/semantics/states.js +2 -0
- package/dist/src/server.d.ts +7 -0
- package/dist/src/server.js +7 -0
- package/generated/pack-facts.json +42 -0
- package/journeys/coverage.json +60 -0
- package/journeys/customer-life.json +7 -0
- package/journeys/customer-life.outline.md +6 -0
- package/journeys/decisions.json +623 -0
- package/journeys/demand.json +171 -0
- package/journeys/score.json +82 -0
- package/journeys/unreachable.json +12 -0
- package/journeys/vendor-examples.json +7 -0
- package/package.json +10 -23
- package/spec/SOURCE.md +40 -0
- package/spec/openapi/associations.json.gz +0 -0
- package/spec/openapi/companies.json.gz +0 -0
- package/spec/openapi/contacts.json.gz +0 -0
- package/spec/openapi/deals.json.gz +0 -0
- package/spec/openapi/meetings.json.gz +0 -0
- package/spec/openapi/oauth.json.gz +0 -0
- package/spec/openapi/owners.json.gz +0 -0
- package/spec/openapi/pipelines.json.gz +0 -0
- package/spec/openapi/properties.json.gz +0 -0
- package/spec/openapi/tickets.json.gz +0 -0
- package/spec/patches.json +235 -0
- package/spec/sources.json +32 -0
- package/src/cli.ts +10 -19
- package/src/fetch.ts +14 -0
- package/src/generated/surface.gen.json +1 -1
- package/src/index.ts +12 -152
- package/src/manifest.ts +33 -92
- package/src/semantics/index.ts +6 -0
- package/src/semantics/states.ts +2 -0
- package/src/server.ts +8 -0
- package/client/hubspot-mirror.css +0 -43
- package/client/hubspot-mirror.tsx +0 -132
- package/dist/client/hubspot-mirror.bundle.js +0 -516
- package/dist/client/hubspot-mirror.css +0 -43
- package/dist/client/hubspot-mirror.d.ts +0 -15
- package/dist/client/hubspot-mirror.js +0 -59
- package/dist/client/hubspot-mirror.tsx +0 -132
- package/dist/src/accounts.d.ts +0 -30
- package/dist/src/accounts.js +0 -122
- package/dist/src/hubspot-areas.d.ts +0 -10
- package/dist/src/hubspot-areas.js +0 -114
- package/dist/src/hubspot-budget.d.ts +0 -58
- package/dist/src/hubspot-budget.js +0 -176
- package/dist/src/hubspot-capabilities.d.ts +0 -3
- package/dist/src/hubspot-capabilities.js +0 -1588
- package/dist/src/hubspot-conformance.d.ts +0 -16
- package/dist/src/hubspot-conformance.js +0 -523
- package/dist/src/hubspot-connector.d.ts +0 -125
- package/dist/src/hubspot-connector.js +0 -390
- package/dist/src/hubspot-deferred-capabilities.d.ts +0 -6
- package/dist/src/hubspot-deferred-capabilities.js +0 -64
- package/dist/src/hubspot-mirror-ui.d.ts +0 -62
- package/dist/src/hubspot-mirror-ui.js +0 -152
- package/dist/src/hubspot-oauth.d.ts +0 -8
- package/dist/src/hubspot-oauth.js +0 -291
- package/dist/src/hubspot-server.d.ts +0 -24
- package/dist/src/hubspot-server.js +0 -116
- package/dist/src/hubspot-twin.d.ts +0 -65
- package/dist/src/hubspot-twin.js +0 -1558
- package/dist/src/portal.d.ts +0 -20
- package/dist/src/portal.js +0 -30
- package/dist/src/screens/account.d.ts +0 -1
- package/dist/src/screens/account.js +0 -139
- package/dist/src/screens/crm.d.ts +0 -2
- package/dist/src/screens/crm.js +0 -153
- package/dist/src/screens/developer.d.ts +0 -4
- package/dist/src/screens/developer.js +0 -191
- package/dist/src/screens/forms.d.ts +0 -5
- package/dist/src/screens/forms.js +0 -126
- package/dist/src/screens/page.d.ts +0 -21
- package/dist/src/screens/page.js +0 -49
- package/dist/src/screens/session.d.ts +0 -1
- package/dist/src/screens/session.js +0 -32
- package/dist/src/semantics/crm.d.ts +0 -8
- package/dist/src/semantics/crm.js +0 -101
- package/dist/src/webhooks.d.ts +0 -12
- package/dist/src/webhooks.js +0 -79
- package/src/accounts.ts +0 -127
- package/src/hubspot-areas.ts +0 -155
- package/src/hubspot-budget.ts +0 -202
- package/src/hubspot-capabilities.ts +0 -1523
- package/src/hubspot-conformance.ts +0 -537
- package/src/hubspot-connector.ts +0 -419
- package/src/hubspot-deferred-capabilities.ts +0 -99
- package/src/hubspot-journey.uitest.ts +0 -104
- package/src/hubspot-mirror-ui.ts +0 -166
- package/src/hubspot-oauth.tsx +0 -296
- package/src/hubspot-server.ts +0 -115
- package/src/hubspot-twin.ts +0 -1534
- package/src/portal.ts +0 -40
- package/src/screens/account.tsx +0 -129
- package/src/screens/crm.tsx +0 -154
- package/src/screens/developer.tsx +0 -181
- package/src/screens/forms.tsx +0 -117
- package/src/screens/page.tsx +0 -55
- package/src/screens/session.tsx +0 -36
- package/src/semantics/crm.ts +0 -116
- package/src/webhooks.ts +0 -82
package/dist/src/portal.d.ts
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
/** The World's own account: every request whose credential names no account the twin made acts here. */
|
|
2
|
-
export declare const TWIN_HUB_ID = 2953265;
|
|
3
|
-
/** Who is acting, in which account, for a write's webhook events: `changeSource` and `sourceId` as HubSpot names
|
|
4
|
-
* them in an event ("The source of the change", "The user or source of the triggered webhook event"). */
|
|
5
|
-
export type PortalScope = {
|
|
6
|
-
hub: number;
|
|
7
|
-
source: string;
|
|
8
|
-
sourceId?: string;
|
|
9
|
-
};
|
|
10
|
-
/** Run `fn` acting in `hub`. */
|
|
11
|
-
export declare function inPortal<T>(s: PortalScope, fn: () => T): T;
|
|
12
|
-
/** The scope the current request acts in; the World's own account, through the API, when none was opened. */
|
|
13
|
-
export declare function currentPortal(): PortalScope;
|
|
14
|
-
/** A subject's kernel id in an account: the World's own account keeps the ids it always had. */
|
|
15
|
-
export declare function scopedId(hub: number, id: string): string;
|
|
16
|
-
/** A stored subject's id as its account sees it, and the account it belongs to. */
|
|
17
|
-
export declare function unscoped(id: string): {
|
|
18
|
-
hub: number;
|
|
19
|
-
id: string;
|
|
20
|
-
};
|
package/dist/src/portal.js
DELETED
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
// THE PORTAL A REQUEST ACTS IN — HubSpot keeps each account's CRM apart: a record, a property, a pipeline and an owner
|
|
2
|
-
// belong to one account ("portal", its Hub ID), a token acts in the account its app was installed in (the token
|
|
3
|
-
// exchange answers that account's `hub_id`), and a webhook event names the account where it occurred in its `portalId`
|
|
4
|
-
// (https://developers.hubspot.com/docs/apps/legacy-apps/public-apps/create-generic-webhook-subscriptions).
|
|
5
|
-
//
|
|
6
|
-
// The request handler (hubspot-twin.ts) reads and writes through `inPortal`'s scope: a subject of an account other than
|
|
7
|
-
// the World's own is stored under the account's id and read only in its scope. A request whose credential the twin
|
|
8
|
-
// never issued acts in the World's own account, `TWIN_HUB_ID`, as every request did before accounts were modelled, so a
|
|
9
|
-
// World pointing an app at the twin with a token of its own keeps working.
|
|
10
|
-
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
11
|
-
/** The World's own account: every request whose credential names no account the twin made acts here. */
|
|
12
|
-
export const TWIN_HUB_ID = 2953265;
|
|
13
|
-
const scope = new AsyncLocalStorage();
|
|
14
|
-
/** Run `fn` acting in `hub`. */
|
|
15
|
-
export function inPortal(s, fn) {
|
|
16
|
-
return scope.run(s, fn);
|
|
17
|
-
}
|
|
18
|
-
/** The scope the current request acts in; the World's own account, through the API, when none was opened. */
|
|
19
|
-
export function currentPortal() {
|
|
20
|
-
return scope.getStore() ?? { hub: TWIN_HUB_ID, source: 'API' };
|
|
21
|
-
}
|
|
22
|
-
/** A subject's kernel id in an account: the World's own account keeps the ids it always had. */
|
|
23
|
-
export function scopedId(hub, id) {
|
|
24
|
-
return hub === TWIN_HUB_ID ? id : `p${hub}~${id}`;
|
|
25
|
-
}
|
|
26
|
-
/** A stored subject's id as its account sees it, and the account it belongs to. */
|
|
27
|
-
export function unscoped(id) {
|
|
28
|
-
const m = /^p(\d+)~(.*)$/.exec(id);
|
|
29
|
-
return m ? { hub: Number(m[1]), id: m[2] } : { hub: TWIN_HUB_ID, id };
|
|
30
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export declare function accountPages(request: Request, root: string | undefined, at: string): Promise<Response | undefined>;
|
|
@@ -1,139 +0,0 @@
|
|
|
1
|
-
// AN ACCOUNT'S OWN SETTINGS — its home, the apps connected to it (where a super admin uninstalls one), and its account
|
|
2
|
-
// and billing pages (where a super admin cancels a subscription's auto-renewal, and deletes the account).
|
|
3
|
-
//
|
|
4
|
-
// - app.hubspot.com/ : the signed-in person's account, with the pages the twin serves for it.
|
|
5
|
-
// - app.hubspot.com/connected-apps/:hubId/installed : the apps installed in the account. "Click Actions on the app you
|
|
6
|
-
// want to disconnect, then click Uninstall. In the dialog box, type "uninstall" in the text field and click Uninstall"
|
|
7
|
-
// (https://knowledge.hubspot.com/marketplace/install-apps-in-the-hubspot-marketplace, Uninstall an app). An
|
|
8
|
-
// uninstalled app's tokens stop working and it is sent no more webhook events (oauth: tokenPortal; webhooks.ts).
|
|
9
|
-
// - app.hubspot.com/account-and-billing/:hubId/subscriptions : the account's subscription and its auto-renewal terms;
|
|
10
|
-
// **Cancel auto-renewal**, choosing the products and a cancellation reason, then reviewing and confirming. "Mid-contract
|
|
11
|
-
// cancellations are not permitted": the subscription ends on its renewal date, when "your account will be downgraded to
|
|
12
|
-
// HubSpot's free tools" (accounts.ts, subscriptionAt).
|
|
13
|
-
// - app.hubspot.com/account-and-billing/:hubId/delete-account : the Delete Account tab. "Click Delete account. You'll be
|
|
14
|
-
// prompted to enter your Hub ID to confirm the deletion. Click Yes, delete"; "You cannot delete your account if any
|
|
15
|
-
// HubSpot subscriptions still have active commitments"
|
|
16
|
-
// (https://knowledge.hubspot.com/account/how-do-i-cancel-my-hubspot-account). A deleted account's pages, tokens and
|
|
17
|
-
// forms stop answering.
|
|
18
|
-
//
|
|
19
|
-
// Where the documentation stops and the twin decides: the pages' paths and layout; only a super admin may uninstall an
|
|
20
|
-
// app, cancel auto-renewal or delete the account (the page also admits billing admins, whom the twin does not model); a
|
|
21
|
-
// cancelled subscription ends at 00:00 UTC on its renewal date; the cancellation reasons offered; HubSpot's words for a
|
|
22
|
-
// refused deletion.
|
|
23
|
-
import { appById, installsIn, keep, subscriptionAt } from "../accounts.js";
|
|
24
|
-
import { formOf, personFor, portalPage, who } from "./page.js";
|
|
25
|
-
export async function accountPages(request, root, at) {
|
|
26
|
-
const path = new URL(request.url).pathname.replace(/\/+$/, '') || '/';
|
|
27
|
-
if (path === '/' && request.method === 'GET') {
|
|
28
|
-
const got = personFor(request, undefined, root);
|
|
29
|
-
if (got instanceof Response)
|
|
30
|
-
return got;
|
|
31
|
-
const hub = Number(got.person.hubId);
|
|
32
|
-
const links = [['Contacts', `/contacts/${hub}/objects/0-1/views/all/list`], ['Companies', `/contacts/${hub}/objects/0-2/views/all/list`],
|
|
33
|
-
['Forms', `/forms/${hub}`], ['Deal pipelines', `/settings/${hub}/objects/deals/pipelines`], ['Connected apps', `/connected-apps/${hub}/installed`], ['Account & billing', `/account-and-billing/${hub}/subscriptions`]];
|
|
34
|
-
return portalPage('Home', { merchant: who(got.account, got.person), sections: [{ heading: `${String(got.account.name)} · Hub ID ${hub}`, items: links.map(([title, href]) => ({ title, actions: [{ label: 'Open', action: href, method: 'get' }] })) }] });
|
|
35
|
-
}
|
|
36
|
-
const m = /^\/(connected-apps|account-and-billing)\/(\d+)(?:\/(installed|installed\/uninstall|subscriptions|subscriptions\/cancel-auto-renewal|delete-account|delete-account\/delete|delete-account\/confirm))?$/.exec(path);
|
|
37
|
-
if (!m)
|
|
38
|
-
return undefined;
|
|
39
|
-
const hub = Number(m[2]);
|
|
40
|
-
const got = personFor(request, hub, root);
|
|
41
|
-
if (got instanceof Response)
|
|
42
|
-
return got;
|
|
43
|
-
const { person, account } = got;
|
|
44
|
-
const actor = String(person.email);
|
|
45
|
-
const admin = person.superAdmin === true;
|
|
46
|
-
if (m[1] === 'connected-apps') {
|
|
47
|
-
let notice;
|
|
48
|
-
if (m[3] === 'installed/uninstall' && request.method === 'POST') {
|
|
49
|
-
const f = await formOf(request);
|
|
50
|
-
const appId = Number(f.appId);
|
|
51
|
-
const install = installsIn(hub, root).find((i) => i.appId === appId);
|
|
52
|
-
const name = String(appById(appId, root)?.name ?? appId);
|
|
53
|
-
if (!admin)
|
|
54
|
-
notice = 'Only a super admin can uninstall apps.';
|
|
55
|
-
else if (!install)
|
|
56
|
-
notice = 'This app is not installed.';
|
|
57
|
-
else if (f.confirm !== 'uninstall') {
|
|
58
|
-
return portalPage(`Uninstall ${name}`, { merchant: who(account, person), notice: `Uninstall ${name}? Its access to this account ends.`, sections: [],
|
|
59
|
-
forms: [{ heading: `Uninstall ${name}`, action: `/connected-apps/${hub}/installed/uninstall`, submit: { label: 'Uninstall' },
|
|
60
|
-
fields: [{ id: 'appId', label: '', type: 'hidden', value: String(appId) }, { id: 'confirm', label: 'Type uninstall' }] }] }, f.confirm === undefined ? 200 : 422);
|
|
61
|
-
}
|
|
62
|
-
else {
|
|
63
|
-
await keep('_install', String(install.id), { ...ownOf(install), uninstalled_at: at, uninstalled_by: actor }, root, at, actor);
|
|
64
|
-
notice = `${name} was uninstalled.`;
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
else if (m[3] !== 'installed' || request.method !== 'GET')
|
|
68
|
-
return undefined;
|
|
69
|
-
return portalPage('Connected apps', { merchant: who(account, person), ...(notice ? { notice } : {}), sections: [{ heading: 'Connected apps', empty: 'No apps are connected to this account.',
|
|
70
|
-
items: installsIn(hub, root).map((i) => ({ title: String(appById(Number(i.appId), root)?.name ?? i.appId), note: `Installed ${String(i.installed_at).slice(0, 10)} by ${String(i.installed_by ?? '')}`,
|
|
71
|
-
actions: [{ label: 'Uninstall', action: `/connected-apps/${hub}/installed/uninstall`, tone: 'danger', fields: { appId: String(i.appId) } }] })) }] });
|
|
72
|
-
}
|
|
73
|
-
const billing = `/account-and-billing/${hub}`;
|
|
74
|
-
const tab = m[3];
|
|
75
|
-
if (tab === undefined)
|
|
76
|
-
return request.method === 'GET' ? subscriptionsPage(hub, account, person, at) : undefined;
|
|
77
|
-
if (tab === 'subscriptions' && request.method === 'GET')
|
|
78
|
-
return subscriptionsPage(hub, account, person, at);
|
|
79
|
-
if (tab === 'subscriptions/cancel-auto-renewal' && request.method === 'POST') {
|
|
80
|
-
const f = await formOf(request);
|
|
81
|
-
const paid = subscriptionAt(account, at);
|
|
82
|
-
if (!admin)
|
|
83
|
-
return subscriptionsPage(hub, account, person, at, 'Only a super admin can cancel auto-renewal.', 403);
|
|
84
|
-
if (paid === 'free' || account.auto_renew === false)
|
|
85
|
-
return subscriptionsPage(hub, account, person, at, 'No subscription renews automatically.', 422);
|
|
86
|
-
if (f.product !== paid || !f.reason) {
|
|
87
|
-
return portalPage('Cancel auto-renewal', { merchant: who(account, person), ...(f.product !== undefined ? { notice: 'Select the products to cancel and a cancellation reason.' } : {}),
|
|
88
|
-
sections: [{ heading: 'Features you lose', items: [{ title: paid, detail: `Your account will be downgraded to HubSpot's free tools on ${String(account.renews_on)}.` }] }],
|
|
89
|
-
forms: [{ heading: 'Cancel auto-renewal', action: `${billing}/subscriptions/cancel-auto-renewal`, submit: { label: 'Next' }, fields: [
|
|
90
|
-
{ id: 'product', label: 'Products to cancel', options: [{ value: paid, label: paid }] },
|
|
91
|
-
{ id: 'reason', label: 'Cancellation reason', options: REASONS.map((r) => ({ value: r, label: r })) },
|
|
92
|
-
] }] }, f.product === undefined ? 200 : 422);
|
|
93
|
-
}
|
|
94
|
-
if (f.confirm !== 'yes') {
|
|
95
|
-
return portalPage('Cancel auto-renewal', { merchant: who(account, person), notice: `${paid} will be canceled on ${String(account.renews_on)}, the end of its commitment term. Reason: ${f.reason}.`, sections: [],
|
|
96
|
-
forms: [{ heading: 'Review the cancellation', action: `${billing}/subscriptions/cancel-auto-renewal`, submit: { label: 'Cancel auto-renewal' }, fields: [
|
|
97
|
-
{ id: 'product', label: '', type: 'hidden', value: paid }, { id: 'reason', label: '', type: 'hidden', value: f.reason }, { id: 'confirm', label: '', type: 'hidden', value: 'yes' },
|
|
98
|
-
] }] });
|
|
99
|
-
}
|
|
100
|
-
await keep('_account', String(account.id), { ...ownOf(account), auto_renew: false, cancellation_reason: f.reason, auto_renew_cancelled_at: at, auto_renew_cancelled_by: actor }, root, at, actor);
|
|
101
|
-
return subscriptionsPage(hub, { ...account, auto_renew: false }, person, at, `Auto-renewal canceled. ${paid} ends on ${String(account.renews_on)}; a notice of non-renewal was sent to the billing admin.`);
|
|
102
|
-
}
|
|
103
|
-
if (tab === 'delete-account' && request.method === 'GET')
|
|
104
|
-
return deletePage(hub, account, person);
|
|
105
|
-
if ((tab === 'delete-account/delete' || tab === 'delete-account/confirm') && request.method === 'POST') {
|
|
106
|
-
const paid = subscriptionAt(account, at);
|
|
107
|
-
if (!admin)
|
|
108
|
-
return deletePage(hub, account, person, 'Only a super admin can delete this account.', 403);
|
|
109
|
-
if (paid !== 'free')
|
|
110
|
-
return deletePage(hub, account, person, `You cannot delete your account while ${paid} still has an active commitment (it renews on ${String(account.renews_on)}).`, 422);
|
|
111
|
-
const typed = ((await formOf(request)).hubId ?? '').trim();
|
|
112
|
-
if (tab === 'delete-account/delete' || typed !== String(hub)) {
|
|
113
|
-
return portalPage('Delete account', { merchant: who(account, person), ...(tab === 'delete-account/confirm' ? { notice: 'Enter this account\'s Hub ID to confirm.' } : {}), sections: [],
|
|
114
|
-
forms: [{ heading: `Delete ${String(account.name)}?`, action: `${billing}/delete-account/confirm`, submit: { label: 'Yes, delete' }, fields: [{ id: 'hubId', label: 'Enter your Hub ID to confirm' }] }] }, tab === 'delete-account/confirm' ? 422 : 200);
|
|
115
|
-
}
|
|
116
|
-
await keep('_account', String(account.id), { ...ownOf(account), closed_at: at, closed_by: actor }, root, at, actor);
|
|
117
|
-
return portalPage('Account deleted', { merchant: String(account.name), notice: `${String(account.name)} (Hub ID ${hub}) has been deleted.`, sections: [] });
|
|
118
|
-
}
|
|
119
|
-
return undefined;
|
|
120
|
-
}
|
|
121
|
-
const REASONS = ['Business is closing or merging', 'Moving to another product', 'Too expensive', 'Not using it enough', 'Other'];
|
|
122
|
-
function subscriptionsPage(hub, account, person, at, notice, status = 200) {
|
|
123
|
-
const now = subscriptionAt(account, at);
|
|
124
|
-
const billing = `/account-and-billing/${hub}`;
|
|
125
|
-
return portalPage('Account & billing', { merchant: who(account, person), back: { href: `${billing}/delete-account`, label: 'Delete Account' }, ...(notice ? { notice } : {}),
|
|
126
|
-
sections: [{ heading: 'Subscriptions', items: [{ title: now === 'free' ? "HubSpot's free tools" : now, detail: `Hub ID ${hub}`,
|
|
127
|
-
...(now !== 'free' ? { note: account.auto_renew === false ? `Canceled: ends on ${String(account.renews_on)}` : `Renews automatically on ${String(account.renews_on)}` } : {}),
|
|
128
|
-
...(now !== 'free' && account.auto_renew !== false ? { actions: [{ label: 'Cancel auto-renewal', action: `${billing}/subscriptions/cancel-auto-renewal` }] } : {}) }] }] }, status);
|
|
129
|
-
}
|
|
130
|
-
function deletePage(hub, account, person, notice, status = 200) {
|
|
131
|
-
return portalPage('Delete Account', { merchant: who(account, person), back: { href: `/account-and-billing/${hub}/subscriptions`, label: 'Subscriptions' }, ...(notice ? { notice } : {}),
|
|
132
|
-
sections: [{ heading: 'Delete Account', items: [{ title: String(account.name), detail: `Hub ID ${hub}`, note: 'Deleting the account permanently removes it and its data.',
|
|
133
|
-
actions: [{ label: 'Delete account', action: `/account-and-billing/${hub}/delete-account/delete`, tone: 'danger' }] }] }] }, status);
|
|
134
|
-
}
|
|
135
|
-
/** A stored row's own fields, without the kernel's. */
|
|
136
|
-
function ownOf(r) {
|
|
137
|
-
const { id: _id, type: _type, updatedAt: _u, deleted: _d, ...rest } = r;
|
|
138
|
-
return rest;
|
|
139
|
-
}
|
package/dist/src/screens/crm.js
DELETED
|
@@ -1,153 +0,0 @@
|
|
|
1
|
-
// THE CRM PAGES PEOPLE WORK IN — an account's contacts, a contact's record (where a rep creates a deal for the contact),
|
|
2
|
-
// a deal's record (where a rep moves its stage), its companies (where an account manager adds one), and its deal
|
|
3
|
-
// pipelines' settings (where a stage's internal ID is read). HubSpot's record pages live at
|
|
4
|
-
// app.hubspot.com/contacts/:hubId/record/:objectTypeId/:id; the twin builds only the acts the life takes on them, from
|
|
5
|
-
// @volter/world-ui's portal piece under HubSpot's skin. Every act is the request handler's own operation, in the
|
|
6
|
-
// account, as the signed-in user (portal.ts: `CRM_UI`, `userId:<id>`), so its webhook events are the API's.
|
|
7
|
-
//
|
|
8
|
-
// Where the documentation stops and the twin decides: the pages' layout and paths other than a record's; a deal made
|
|
9
|
-
// on a contact's record is associated with it (HubSpot's default deal-to-contact association, type 3) and owned by the
|
|
10
|
-
// rep who made it; the pipelines settings page is at /settings/:hubId/objects/deals/pipelines and shows each stage's
|
|
11
|
-
// internal ID beside its label; a pipeline made there takes its stages in order, separated by commas, and names its closed won and closed
|
|
12
|
-
// lost stages.
|
|
13
|
-
import { handleHubspotTwinRequest } from "../hubspot-twin.js";
|
|
14
|
-
import { inPortal } from "../portal.js";
|
|
15
|
-
import { formOf, personFor, portalPage, redirect, who } from "./page.js";
|
|
16
|
-
function caller(hub, person, root, at) {
|
|
17
|
-
return async (method, path, body) => {
|
|
18
|
-
const res = await inPortal({ hub, source: 'CRM_UI', sourceId: `userId:${String(person.userId)}` }, () => handleHubspotTwinRequest({ method, path, ...(body !== undefined ? { body: JSON.stringify(body) } : {}), root, occurredAt: at }));
|
|
19
|
-
return { status: res.status, body: (res.body ?? {}) };
|
|
20
|
-
};
|
|
21
|
-
}
|
|
22
|
-
const CONTACT_PROPS = ['email', 'firstname', 'lastname', 'lifecyclestage', 'dub_id', 'dub_link', 'dub_partner_email'];
|
|
23
|
-
const nameOf = (p) => [p.firstname, p.lastname].filter(Boolean).join(' ') || String(p.email ?? '');
|
|
24
|
-
async function stageOptions(call) {
|
|
25
|
-
const pipelines = ((await call('GET', '/crm/v3/pipelines/deals')).body.results ?? []);
|
|
26
|
-
return pipelines.flatMap((p) => (p.stages ?? []).map((s) => ({ value: `${String(p.id)}/${String(s.id)}`, label: `${String(p.label)} · ${String(s.label)}` })));
|
|
27
|
-
}
|
|
28
|
-
/** app.hubspot.com/contacts/… and /settings/…/pipelines, or undefined for another path. */
|
|
29
|
-
export async function crmPages(request, root, at) {
|
|
30
|
-
const url = new URL(request.url);
|
|
31
|
-
const path = url.pathname.replace(/\/+$/, '');
|
|
32
|
-
const m = /^\/(contacts|settings)\/(\d+)\/(.*)$/.exec(path);
|
|
33
|
-
if (!m)
|
|
34
|
-
return undefined;
|
|
35
|
-
const hub = Number(m[2]);
|
|
36
|
-
const rest = m[3];
|
|
37
|
-
const got = personFor(request, hub, root);
|
|
38
|
-
if (got instanceof Response)
|
|
39
|
-
return got;
|
|
40
|
-
const { person, account } = got;
|
|
41
|
-
const call = caller(hub, person, root, at);
|
|
42
|
-
const side = who(account, person);
|
|
43
|
-
const R = `/contacts/${hub}`;
|
|
44
|
-
if (m[1] === 'settings') {
|
|
45
|
-
if (rest !== 'objects/deals/pipelines')
|
|
46
|
-
return undefined;
|
|
47
|
-
if (request.method === 'POST') {
|
|
48
|
-
const f = await formOf(request);
|
|
49
|
-
const labels = (f.stages ?? '').split(/\r?\n|,/).map((s) => s.trim()).filter(Boolean);
|
|
50
|
-
const stages = labels.map((label, i) => ({ label, displayOrder: i, metadata: label === f.won ? { isClosed: 'true', probability: '1.0' } : label === f.lost ? { isClosed: 'true', probability: '0.0' } : { isClosed: 'false', probability: String(Math.round(((i + 1) / (labels.length + 1)) * 10) / 10) } }));
|
|
51
|
-
const made = await call('POST', '/crm/v3/pipelines/deals', { label: f.label, displayOrder: 1, stages });
|
|
52
|
-
if (made.status !== 201)
|
|
53
|
-
return pipelinesPage(call, side, hub, String(made.body.message ?? 'The pipeline could not be created.'), 422);
|
|
54
|
-
return pipelinesPage(call, side, hub, `Pipeline ${String(f.label)} created.`);
|
|
55
|
-
}
|
|
56
|
-
return pipelinesPage(call, side, hub);
|
|
57
|
-
}
|
|
58
|
-
if (rest === 'objects/0-1/views/all/list' && request.method === 'GET') {
|
|
59
|
-
const list = ((await call('GET', `/crm/v3/objects/contacts?limit=100&properties=${CONTACT_PROPS.join(',')}`)).body.results ?? []);
|
|
60
|
-
return portalPage('Contacts', { merchant: side, sections: [{ heading: 'Contacts', empty: 'No contacts yet.', items: list.map((c) => {
|
|
61
|
-
const p = c.properties;
|
|
62
|
-
return { title: nameOf(p), detail: String(p.email ?? ''), note: `Record ID ${String(c.id)}`, actions: [{ label: 'Open', action: `${R}/record/0-1/${String(c.id)}`, method: 'get' }] };
|
|
63
|
-
}) }] });
|
|
64
|
-
}
|
|
65
|
-
if (rest === 'objects/0-2/views/all/list') {
|
|
66
|
-
let notice;
|
|
67
|
-
if (request.method === 'POST') {
|
|
68
|
-
const f = await formOf(request);
|
|
69
|
-
const properties = Object.fromEntries(Object.entries(f).filter(([, v]) => v !== ''));
|
|
70
|
-
const made = await call('POST', '/crm/v3/objects/companies', { properties });
|
|
71
|
-
notice = made.status === 201 ? `Company ${String(f.name)} created.` : String(made.body.message ?? 'The company could not be created.');
|
|
72
|
-
}
|
|
73
|
-
const list = ((await call('GET', '/crm/v3/objects/companies?limit=100')).body.results ?? []);
|
|
74
|
-
const owners = ((await call('GET', '/crm/v3/owners')).body.results ?? []);
|
|
75
|
-
const custom = ((await call('GET', '/crm/v3/properties/companies')).body.results ?? []);
|
|
76
|
-
return portalPage('Companies', { merchant: side, ...(notice ? { notice } : {}),
|
|
77
|
-
sections: [{ heading: 'Companies', empty: 'No companies yet.', items: list.map((c) => ({ title: String(c.properties.name ?? ''), detail: String(c.properties.domain ?? ''), note: `Record ID ${String(c.id)}` })) }],
|
|
78
|
-
forms: [{ heading: 'Create company', action: `${R}/objects/0-2/views/all/list`, submit: { label: 'Create' }, fields: [
|
|
79
|
-
{ id: 'name', label: 'Company name' }, { id: 'domain', label: 'Company domain name' },
|
|
80
|
-
{ id: 'hubspot_owner_id', label: 'Company owner', options: owners.map((o) => ({ value: String(o.id), label: `${String(o.firstName)} ${String(o.lastName)}` })) },
|
|
81
|
-
...custom.filter((p) => p.hubspotDefined !== true && p.archived !== true).map((p) => ({ id: String(p.name), label: String(p.label) })),
|
|
82
|
-
] }] });
|
|
83
|
-
}
|
|
84
|
-
const rec = /^record\/(0-1|0-3)\/(\d+)(?:\/(deals|stage))?$/.exec(rest);
|
|
85
|
-
if (!rec)
|
|
86
|
-
return undefined;
|
|
87
|
-
const [, type, id, act] = rec;
|
|
88
|
-
if (type === '0-1') {
|
|
89
|
-
if (act === 'deals' && request.method === 'POST') {
|
|
90
|
-
const f = await formOf(request);
|
|
91
|
-
const [pipeline, dealstage] = (f.stage ?? '').split('/');
|
|
92
|
-
const made = await call('POST', '/crm/v3/objects/0-3', {
|
|
93
|
-
properties: { dealname: f.dealname, amount: f.amount, pipeline, dealstage, hubspot_owner_id: String(person.ownerId) },
|
|
94
|
-
associations: [{ to: { id }, types: [{ associationCategory: 'HUBSPOT_DEFINED', associationTypeId: 3 }] }],
|
|
95
|
-
});
|
|
96
|
-
if (made.status !== 201)
|
|
97
|
-
return contactPage(call, side, hub, id, String(made.body.message ?? 'The deal could not be created.'), 422);
|
|
98
|
-
return redirect(`${R}/record/0-3/${String(made.body.id)}`);
|
|
99
|
-
}
|
|
100
|
-
if (act || request.method !== 'GET')
|
|
101
|
-
return undefined;
|
|
102
|
-
return contactPage(call, side, hub, id);
|
|
103
|
-
}
|
|
104
|
-
if (act === 'stage' && request.method === 'POST') {
|
|
105
|
-
const f = await formOf(request);
|
|
106
|
-
const [pipeline, dealstage] = (f.stage ?? '').split('/');
|
|
107
|
-
const moved = await call('PATCH', `/crm/v3/objects/0-3/${id}`, { properties: { pipeline, dealstage } });
|
|
108
|
-
if (moved.status !== 200)
|
|
109
|
-
return dealPage(call, side, hub, id, String(moved.body.message ?? 'The deal could not be saved.'), 422);
|
|
110
|
-
return redirect(`${R}/record/0-3/${id}`);
|
|
111
|
-
}
|
|
112
|
-
if (act || request.method !== 'GET')
|
|
113
|
-
return undefined;
|
|
114
|
-
return dealPage(call, side, hub, id);
|
|
115
|
-
}
|
|
116
|
-
async function contactPage(call, side, hub, id, notice, status = 200) {
|
|
117
|
-
const got = await call('GET', `/crm/v3/objects/contacts/${id}?properties=${CONTACT_PROPS.join(',')}&associations=deals`);
|
|
118
|
-
if (got.status !== 200)
|
|
119
|
-
return portalPage('Contact', { merchant: side, notice: 'This record does not exist.', sections: [] }, 404);
|
|
120
|
-
const p = got.body.properties;
|
|
121
|
-
const deals = (got.body.associations?.deals?.results ?? []);
|
|
122
|
-
return portalPage(nameOf(p), { merchant: side, back: { href: `/contacts/${hub}/objects/0-1/views/all/list`, label: 'Contacts' }, ...(notice ? { notice } : {}), sections: [
|
|
123
|
-
{ heading: 'About this contact', items: CONTACT_PROPS.filter((k) => p[k] !== undefined && p[k] !== null).map((k) => ({ title: k, detail: String(p[k]) })) },
|
|
124
|
-
{ heading: 'Deals', empty: 'No deals.', items: deals.map((d) => ({ title: `Deal ${String(d.id)}`, actions: [{ label: 'Open', action: `/contacts/${hub}/record/0-3/${String(d.id)}`, method: 'get' }] })) },
|
|
125
|
-
], forms: [{ heading: 'Create deal', action: `/contacts/${hub}/record/0-1/${id}/deals`, submit: { label: 'Create' }, fields: [
|
|
126
|
-
{ id: 'dealname', label: 'Deal name' }, { id: 'amount', label: 'Amount' },
|
|
127
|
-
{ id: 'stage', label: 'Pipeline and deal stage', options: await stageOptions(call) },
|
|
128
|
-
] }] }, status);
|
|
129
|
-
}
|
|
130
|
-
async function dealPage(call, side, hub, id, notice, status = 200) {
|
|
131
|
-
const got = await call('GET', `/crm/v3/objects/0-3/${id}?properties=dealname,amount,pipeline,dealstage,closedate&associations=contacts`);
|
|
132
|
-
if (got.status !== 200)
|
|
133
|
-
return portalPage('Deal', { merchant: side, notice: 'This record does not exist.', sections: [] }, 404);
|
|
134
|
-
const p = got.body.properties;
|
|
135
|
-
const contacts = (got.body.associations?.contacts?.results ?? []);
|
|
136
|
-
return portalPage(String(p.dealname ?? 'Deal'), { merchant: side, ...(notice ? { notice } : {}), sections: [
|
|
137
|
-
{ heading: 'About this deal', items: ['dealname', 'amount', 'pipeline', 'dealstage'].map((k) => ({ title: k, detail: String(p[k] ?? '') })) },
|
|
138
|
-
{ heading: 'Contacts', empty: 'No contacts.', items: contacts.map((c) => ({ title: `Contact ${String(c.id)}`, actions: [{ label: 'Open', action: `/contacts/${hub}/record/0-1/${String(c.id)}`, method: 'get' }] })) },
|
|
139
|
-
], forms: [{ heading: 'Deal stage', action: `/contacts/${hub}/record/0-3/${id}/stage`, submit: { label: 'Save' }, fields: [
|
|
140
|
-
{ id: 'stage', label: 'Deal stage', value: `${String(p.pipeline)}/${String(p.dealstage)}`, options: await stageOptions(call) },
|
|
141
|
-
] }] }, status);
|
|
142
|
-
}
|
|
143
|
-
async function pipelinesPage(call, side, hub, notice, status = 200) {
|
|
144
|
-
const pipelines = ((await call('GET', '/crm/v3/pipelines/deals')).body.results ?? []);
|
|
145
|
-
return portalPage('Deal pipelines', { merchant: side, ...(notice ? { notice } : {}),
|
|
146
|
-
sections: pipelines.map((p) => ({ heading: `${String(p.label)} (pipeline ID ${String(p.id)})`, items: (p.stages ?? []).map((s) => ({
|
|
147
|
-
title: String(s.label), detail: `Stage ID: ${String(s.id)}`, note: s.metadata?.isClosed === 'true' ? `Closed (probability ${String(s.metadata.probability)})` : 'Open'
|
|
148
|
-
})) })),
|
|
149
|
-
forms: [{ heading: 'Create pipeline', action: `/settings/${hub}/objects/deals/pipelines`, submit: { label: 'Create pipeline' }, fields: [
|
|
150
|
-
{ id: 'label', label: 'Pipeline name' }, { id: 'stages', label: 'Stages, in order, separated by commas' },
|
|
151
|
-
{ id: 'won', label: 'Closed won stage' }, { id: 'lost', label: 'Closed lost stage' },
|
|
152
|
-
] }] }, status);
|
|
153
|
-
}
|
|
@@ -1,4 +0,0 @@
|
|
|
1
|
-
/** The door standing in for `hs project upload`. */
|
|
2
|
-
export declare function uploadDoor(hubId: number, request: Request, root: string | undefined, at: string): Promise<Response>;
|
|
3
|
-
/** app.hubspot.com/developer-projects/…, or undefined for another path. */
|
|
4
|
-
export declare function developerPages(request: Request, root: string | undefined, at: string): Promise<Response | undefined>;
|
|
@@ -1,191 +0,0 @@
|
|
|
1
|
-
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
// AN ACCOUNT'S APPS — the developer projects an account builds and the pages it manages each app on
|
|
3
|
-
// (https://developers.hubspot.com/docs/apps/developer-platform/build-apps/manage-apps-in-hubspot).
|
|
4
|
-
//
|
|
5
|
-
// - POST /_twin/accounts/:hubId/projects {name, uploadedBy, platformVersion, app, webhooks?}: stands in for `hs project
|
|
6
|
-
// upload`, the HubSpot CLI's upload of a project to the account's developer file system, whose API HubSpot does not
|
|
7
|
-
// document (the CLI's requests are its own: a zip of the project and the intermediate representation
|
|
8
|
-
// @hubspot/project-parsing-lib makes of it, then a build and a deploy it polls). `app` is the project's
|
|
9
|
-
// `app-hsmeta.json` and `webhooks` its `webhooks/*-hsmeta.json`, as the configuration pages define them
|
|
10
|
-
// (https://developers.hubspot.com/docs/apps/developer-platform/build-apps/app-configuration,
|
|
11
|
-
// https://developers.hubspot.com/docs/apps/developer-platform/add-features/configure-webhooks); a second upload of a
|
|
12
|
-
// project updates its app, keeping the app's id, client ID and secret. 201 with the build's app.
|
|
13
|
-
// - /developer-projects/:hubId/project/:name/component/:uid/auth: the app's authentication details, "Your app's
|
|
14
|
-
// *Client ID* and *Client secret* will appear in the *Client credentials* section", and its redirect URLs and scopes.
|
|
15
|
-
// - /developer-projects/:hubId/project/:name/component/:uid/distribution: for an OAuth marketplace app, **Begin
|
|
16
|
-
// publishing** and the Acceptable Use Policy ("Until the AUP is signed, you won't be able to install your app in
|
|
17
|
-
// accounts other than developer test accounts"), then the sample install URL and the installed accounts ("Until your
|
|
18
|
-
// app is reviewed … you'll be limited to 25 installs"); for a static-token app, **Install now**, HubSpot's consent
|
|
19
|
-
// (**Connect app**), and the access token, which **Show** reveals.
|
|
20
|
-
// - POST /developer-projects/:hubId/project/:name/delete: deletes the project and its app.
|
|
21
|
-
//
|
|
22
|
-
// Where the documentation stops and the twin decides: the pages' paths and layout; an app id counts from 30000001; a
|
|
23
|
-
// client ID and secret are UUIDs (the OAuth page's `client_id=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx`), a static token is
|
|
24
|
-
// `pat-na1-` and a UUID (the authentication overview masks one as `***-***-*********-…`); only a super admin of the
|
|
25
|
-
// account may manage its apps; the AUP panel's text is the twin's summary, not HubSpot's policy; no app the World builds
|
|
26
|
-
// is ever reviewed, so every install of a marketplace app is of an unverified app.
|
|
27
|
-
import { accountOf, appsOf, hex, installOf, keep, personOf, stored } from "../accounts.js";
|
|
28
|
-
import { CSS, formOf, page, personFor, portalPage, redirect, who } from "./page.js";
|
|
29
|
-
import { Consent } from '@volter/world-ui';
|
|
30
|
-
const uuid = (seed) => { const h = hex(seed); return `${h.slice(0, 8)}-${h.slice(8, 12)}-${h.slice(12, 16)}-${h.slice(16, 20)}-${h.slice(20, 32)}`; };
|
|
31
|
-
const INSTALL_CAP = 25;
|
|
32
|
-
/** The door standing in for `hs project upload`. */
|
|
33
|
-
export async function uploadDoor(hubId, request, root, at) {
|
|
34
|
-
let body = {};
|
|
35
|
-
try {
|
|
36
|
-
body = JSON.parse(await request.text());
|
|
37
|
-
}
|
|
38
|
-
catch { /* refused below */ }
|
|
39
|
-
const refuse = (message) => Response.json({ status: 'error', message, category: 'VALIDATION_ERROR' }, { status: 400 });
|
|
40
|
-
const account = accountOf(hubId, root);
|
|
41
|
-
const uploader = personOf(String(body.uploadedBy ?? ''), root);
|
|
42
|
-
if (!account || account.closed_at)
|
|
43
|
-
return refuse(`no open account ${hubId}`);
|
|
44
|
-
if (!uploader || uploader.hubId !== hubId || uploader.superAdmin !== true)
|
|
45
|
-
return refuse('uploadedBy must be a super admin of the account');
|
|
46
|
-
const name = typeof body.name === 'string' ? body.name.trim() : '';
|
|
47
|
-
const app = body.app;
|
|
48
|
-
const c = app?.config;
|
|
49
|
-
const auth = c?.auth;
|
|
50
|
-
if (!name || !app || app.type !== 'app' || typeof app.uid !== 'string' || !c)
|
|
51
|
-
return refuse('a project name and its app component (app-hsmeta.json: uid, type "app", config) are required');
|
|
52
|
-
if (typeof c.name !== 'string' || typeof c.description !== 'string')
|
|
53
|
-
return refuse('app config requires name and description');
|
|
54
|
-
if (c.distribution !== 'marketplace' && c.distribution !== 'private')
|
|
55
|
-
return refuse('distribution must be marketplace or private');
|
|
56
|
-
if (!auth || (auth.type !== 'oauth' && auth.type !== 'static'))
|
|
57
|
-
return refuse('auth.type must be oauth or static');
|
|
58
|
-
if (c.distribution === 'marketplace' && auth.type !== 'oauth')
|
|
59
|
-
return refuse('a marketplace app must use oauth');
|
|
60
|
-
const redirectUrls = Array.isArray(auth.redirectUrls) ? auth.redirectUrls.map(String) : [];
|
|
61
|
-
// "Each app must have at least one auth redirect URL, and it must use HTTPS. The only exception is that http://localhost is allowed"
|
|
62
|
-
if (auth.type === 'oauth' && (!redirectUrls.length || redirectUrls.some((u) => !u.startsWith('https://') && !u.startsWith('http://localhost'))))
|
|
63
|
-
return refuse('an oauth app needs at least one redirect URL, each HTTPS (or http://localhost)');
|
|
64
|
-
const requiredScopes = Array.isArray(auth.requiredScopes) ? auth.requiredScopes.map(String) : [];
|
|
65
|
-
if (!requiredScopes.length)
|
|
66
|
-
return refuse('an app must include at least one required scope');
|
|
67
|
-
const hooks = body.webhooks;
|
|
68
|
-
const settings = (hooks?.config?.settings ?? {});
|
|
69
|
-
const crmObjects = ((hooks?.config?.subscriptions ?? {}).crmObjects ?? []);
|
|
70
|
-
if (hooks && (hooks.type !== 'webhooks' || typeof settings.targetUrl !== 'string'))
|
|
71
|
-
return refuse('the webhooks component needs type "webhooks" and settings.targetUrl');
|
|
72
|
-
const existing = stored('_app', root).find((a) => a.hubId === hubId && a.project === name && !a.deleted_at);
|
|
73
|
-
const appId = existing ? Number(existing.appId) : 30000000 + stored('_app', root).length + 1;
|
|
74
|
-
const fields = {
|
|
75
|
-
appId, hubId, project: name, uid: app.uid, name: c.name, description: c.description, distribution: c.distribution,
|
|
76
|
-
authType: auth.type, redirectUrls, requiredScopes,
|
|
77
|
-
optionalScopes: Array.isArray(auth.optionalScopes) ? auth.optionalScopes.map(String) : [],
|
|
78
|
-
platformVersion: typeof body.platformVersion === 'string' ? body.platformVersion : '2026.03',
|
|
79
|
-
client_id: existing?.client_id ?? uuid(`hubspot-client:${appId}`),
|
|
80
|
-
client_secret: existing?.client_secret ?? uuid(`hubspot-secret:${appId}:${at}`),
|
|
81
|
-
webhooks: hooks ? {
|
|
82
|
-
targetUrl: settings.targetUrl,
|
|
83
|
-
subscriptions: crmObjects.map((s, i) => ({ id: appId * 100 + i + 1, subscriptionType: s.subscriptionType, objectType: s.objectType, ...(s.propertyName ? { propertyName: s.propertyName } : {}), active: s.active !== false })),
|
|
84
|
-
} : null,
|
|
85
|
-
publishing: existing?.publishing ?? null, aup_signed_at: existing?.aup_signed_at ?? null, aup_signed_by: existing?.aup_signed_by ?? null,
|
|
86
|
-
created_at: existing?.created_at ?? at, uploaded_at: at, uploaded_by: uploader.email, deleted_at: null,
|
|
87
|
-
};
|
|
88
|
-
await keep('_app', `app:${appId}`, fields, root, at, String(uploader.email));
|
|
89
|
-
return Response.json({ project: name, appId, uid: app.uid, url: `/developer-projects/${hubId}/project/${encodeURIComponent(name)}/component/${encodeURIComponent(String(app.uid))}/auth` }, { status: 201 });
|
|
90
|
-
}
|
|
91
|
-
const base = (a) => `/developer-projects/${String(a.hubId)}/project/${encodeURIComponent(String(a.project))}/component/${encodeURIComponent(String(a.uid))}`;
|
|
92
|
-
function authPage(a, account, person) {
|
|
93
|
-
return portalPage(`${String(a.name)} · Auth`, {
|
|
94
|
-
merchant: who(account, person), back: { href: `${base(a)}/distribution`, label: 'Distribution' },
|
|
95
|
-
sections: [
|
|
96
|
-
{ heading: 'Client credentials', items: a.authType === 'oauth'
|
|
97
|
-
? [{ title: 'Client ID', detail: String(a.client_id) }, { title: 'Client secret', detail: String(a.client_secret) }]
|
|
98
|
-
: [], empty: 'A static-token app has no client credentials; its access token is on the Distribution tab.' },
|
|
99
|
-
{ heading: 'Redirect URLs', items: a.redirectUrls.map((u) => ({ title: u })), empty: 'None.' },
|
|
100
|
-
{ heading: 'Scopes', items: [...a.requiredScopes.map((s) => ({ title: s, detail: 'Required' })), ...a.optionalScopes.map((s) => ({ title: s, detail: 'Optional' }))] },
|
|
101
|
-
],
|
|
102
|
-
});
|
|
103
|
-
}
|
|
104
|
-
function installUrl(a) {
|
|
105
|
-
const q = new URLSearchParams({ client_id: String(a.client_id), redirect_uri: a.redirectUrls[0] ?? '', scope: ['oauth', ...a.requiredScopes].join(' ') });
|
|
106
|
-
return `https://app.hubspot.com/oauth/authorize?${q.toString()}`;
|
|
107
|
-
}
|
|
108
|
-
function distributionPage(a, account, person, root, opts = {}) {
|
|
109
|
-
const installs = stored('_install', root).filter((i) => i.appId === a.appId && !i.uninstalled_at);
|
|
110
|
-
const d = `${base(a)}/distribution`;
|
|
111
|
-
if (a.authType === 'static') {
|
|
112
|
-
const install = installOf(Number(a.appId), Number(a.hubId), root);
|
|
113
|
-
return portalPage(`${String(a.name)} · Distribution`, {
|
|
114
|
-
merchant: who(account, person), back: { href: `${base(a)}/auth`, label: 'Auth' }, ...(opts.notice ? { notice: opts.notice } : {}),
|
|
115
|
-
sections: [{ heading: 'Manage distribution', items: [install
|
|
116
|
-
? { title: String(account.name), detail: `Access token: ${opts.show ? String(install.static_token) : `${String(install.static_token).slice(0, 8)}${'•'.repeat(12)}`}`, note: `Installed ${String(install.installed_at).slice(0, 10)}`, actions: opts.show ? [] : [{ label: 'Show', action: `${d}?show=token`, method: 'get' }] }
|
|
117
|
-
: { title: String(account.name), detail: 'Not installed', actions: [{ label: 'Install now', action: `${d}/install` }] }] }],
|
|
118
|
-
});
|
|
119
|
-
}
|
|
120
|
-
const published = !!a.publishing;
|
|
121
|
-
const signed = !!a.aup_signed_at;
|
|
122
|
-
return portalPage(`${String(a.name)} · Distribution`, {
|
|
123
|
-
merchant: who(account, person), back: { href: `${base(a)}/auth`, label: 'Auth' }, ...(opts.notice ? { notice: opts.notice } : {}),
|
|
124
|
-
sections: [
|
|
125
|
-
{ heading: 'Distribution', items: [signed
|
|
126
|
-
? { title: 'Unlisted marketplace app', detail: `Install URL: ${installUrl(a)}`, note: `${installs.length} of ${INSTALL_CAP} installs before listing` }
|
|
127
|
-
: { title: 'Not published', detail: 'Until the AUP is signed, you won\'t be able to install your app in accounts other than developer test accounts.', actions: [{ label: published ? 'Continue publishing' : 'Begin publishing', action: `${d}/begin-publishing`, tone: 'primary' }] }] },
|
|
128
|
-
...(opts.aup && !signed ? [{ heading: 'Acceptable Use Policy', items: [{ title: 'HubSpot App Acceptable Use Policy', detail: 'Review and sign the policy to install this app in accounts other than developer test accounts.', actions: [{ label: 'Sign', action: `${d}/aup`, tone: 'primary', fields: { accept: 'yes' } }] }] }] : []),
|
|
129
|
-
{ heading: 'Installed accounts', items: installs.map((i) => ({ title: String(accountOf(Number(i.hubId), root)?.name ?? i.hubId), note: `Installed ${String(i.installed_at).slice(0, 10)}` })), empty: 'No installs yet.' },
|
|
130
|
-
],
|
|
131
|
-
});
|
|
132
|
-
}
|
|
133
|
-
/** app.hubspot.com/developer-projects/…, or undefined for another path. */
|
|
134
|
-
export async function developerPages(request, root, at) {
|
|
135
|
-
const url = new URL(request.url);
|
|
136
|
-
const m = /^\/developer-projects\/(\d+)\/project\/([^/]+)(?:\/component\/([^/]+)\/(auth|distribution)(?:\/(begin-publishing|aup|install|install\/confirm))?|\/(delete))$/.exec(url.pathname.replace(/\/+$/, ''));
|
|
137
|
-
if (!m)
|
|
138
|
-
return undefined;
|
|
139
|
-
const hubId = Number(m[1]);
|
|
140
|
-
const got = personFor(request, hubId, root);
|
|
141
|
-
if (got instanceof Response)
|
|
142
|
-
return got;
|
|
143
|
-
const { person, account } = got;
|
|
144
|
-
if (person.superAdmin !== true)
|
|
145
|
-
return page('HubSpot', CSS.portal, _jsx("main", { className: "portal-main", children: _jsx("h1", { children: "Only a super admin can manage this account's apps." }) }), 403);
|
|
146
|
-
const app = appsOf(hubId, root).find((a) => a.project === decodeURIComponent(m[2]) && (m[3] === undefined || a.uid === decodeURIComponent(m[3])));
|
|
147
|
-
if (!app)
|
|
148
|
-
return page('HubSpot', CSS.portal, _jsx("main", { className: "portal-main", children: _jsx("h1", { children: "This project doesn't exist." }) }), 404);
|
|
149
|
-
const actor = String(person.email);
|
|
150
|
-
if (m[6] === 'delete') {
|
|
151
|
-
if (request.method !== 'POST')
|
|
152
|
-
return undefined;
|
|
153
|
-
await keep('_app', `app:${String(app.appId)}`, { ...app, deleted_at: at, deleted_by: actor }, root, at, actor);
|
|
154
|
-
return portalPage('Projects', { merchant: who(account, person), notice: `Project ${String(app.project)} deleted.`, sections: [] });
|
|
155
|
-
}
|
|
156
|
-
const tab = m[4];
|
|
157
|
-
const act = m[5];
|
|
158
|
-
if (!act) {
|
|
159
|
-
if (request.method !== 'GET')
|
|
160
|
-
return undefined;
|
|
161
|
-
return tab === 'auth' ? authPage(app, account, person) : distributionPage(app, account, person, root, { show: url.searchParams.get('show') === 'token' });
|
|
162
|
-
}
|
|
163
|
-
if (request.method !== 'POST' || tab !== 'distribution')
|
|
164
|
-
return undefined;
|
|
165
|
-
if (act === 'begin-publishing' && app.distribution === 'marketplace') {
|
|
166
|
-
const withPanel = { ...app, publishing: app.publishing ?? at };
|
|
167
|
-
if (!app.publishing)
|
|
168
|
-
await keep('_app', `app:${String(app.appId)}`, withPanel, root, at, actor);
|
|
169
|
-
return distributionPage(withPanel, account, person, root, { aup: true });
|
|
170
|
-
}
|
|
171
|
-
if (act === 'aup' && app.distribution === 'marketplace' && app.publishing) {
|
|
172
|
-
if ((await formOf(request)).accept !== 'yes')
|
|
173
|
-
return distributionPage(app, account, person, root, { aup: true, notice: 'Sign the Acceptable Use Policy to continue.' });
|
|
174
|
-
const signed = { ...app, aup_signed_at: at, aup_signed_by: actor };
|
|
175
|
-
await keep('_app', `app:${String(app.appId)}`, signed, root, at, actor);
|
|
176
|
-
return distributionPage(signed, account, person, root, { notice: 'Acceptable Use Policy signed. You can install this app with its install URL.' });
|
|
177
|
-
}
|
|
178
|
-
if (act === 'install' && app.authType === 'static') {
|
|
179
|
-
return page(`Connect ${String(app.name)}`, CSS.consent, (_jsx(Consent, { heading: `Connect ${String(app.name)} to ${String(account.name)}`, app: { name: String(app.name) }, request: `${String(account.name)} · Hub ID ${hubId}`, permissions: app.requiredScopes.map((s) => ({ title: s, detail: 'Requested scope' })), action: `${base(app)}/distribution/install/confirm`, fields: {}, deny: { name: 'answer', value: 'cancel', label: 'Cancel' }, allow: { name: 'answer', value: 'allow', label: 'Connect app' } })));
|
|
180
|
-
}
|
|
181
|
-
if (act === 'install/confirm' && app.authType === 'static') {
|
|
182
|
-
if ((await formOf(request)).answer !== 'allow')
|
|
183
|
-
return redirect(`${base(app)}/distribution`);
|
|
184
|
-
if (installOf(Number(app.appId), hubId, root))
|
|
185
|
-
return distributionPage(app, account, person, root, { notice: 'This app is already installed.' });
|
|
186
|
-
const token = `pat-na1-${uuid(`hubspot-static:${String(app.appId)}:${at}`)}`;
|
|
187
|
-
await keep('_install', `install:${String(app.appId)}:${hubId}:${at}`, { appId: app.appId, hubId, installed_by: actor, scopes: app.requiredScopes, installed_at: at, uninstalled_at: null, static_token: token }, root, at, actor);
|
|
188
|
-
return redirect(`${base(app)}/distribution`);
|
|
189
|
-
}
|
|
190
|
-
return undefined;
|
|
191
|
-
}
|
|
@@ -1,5 +0,0 @@
|
|
|
1
|
-
export declare const FORMS_HOST = "api.hsforms.com";
|
|
2
|
-
/** app.hubspot.com/forms/…, or undefined for another path. */
|
|
3
|
-
export declare function formPages(request: Request, root: string | undefined, at: string): Promise<Response | undefined>;
|
|
4
|
-
/** api.hsforms.com's submission, or undefined for another request. */
|
|
5
|
-
export declare function formSubmission(request: Request, root: string | undefined, at: string): Promise<Response | undefined>;
|