@revfleet/hscli 0.8.11 → 0.9.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 (154) hide show
  1. package/CHANGELOG.md +186 -0
  2. package/README.md +223 -10
  3. package/dist/cli.js +29 -3
  4. package/dist/cli.js.map +1 -1
  5. package/dist/commands/agent/contracts.d.ts +3 -0
  6. package/dist/commands/agent/contracts.js +139 -0
  7. package/dist/commands/agent/contracts.js.map +1 -0
  8. package/dist/commands/agent/index.d.ts +3 -0
  9. package/dist/commands/agent/index.js +188 -0
  10. package/dist/commands/agent/index.js.map +1 -0
  11. package/dist/commands/agent/recipes.d.ts +13 -0
  12. package/dist/commands/agent/recipes.js +108 -0
  13. package/dist/commands/agent/recipes.js.map +1 -0
  14. package/dist/commands/agent/runner.d.ts +16 -0
  15. package/dist/commands/agent/runner.js +165 -0
  16. package/dist/commands/agent/runner.js.map +1 -0
  17. package/dist/commands/auth/index.js +64 -7
  18. package/dist/commands/auth/index.js.map +1 -1
  19. package/dist/commands/automation/index.js +30 -0
  20. package/dist/commands/automation/index.js.map +1 -1
  21. package/dist/commands/backup/index.d.ts +12 -0
  22. package/dist/commands/backup/index.js +459 -0
  23. package/dist/commands/backup/index.js.map +1 -0
  24. package/dist/commands/cms/content-ops.js +1 -1
  25. package/dist/commands/cms/content-ops.js.map +1 -1
  26. package/dist/commands/communication-preferences/index.js +207 -7
  27. package/dist/commands/communication-preferences/index.js.map +1 -1
  28. package/dist/commands/content-permissions/index.d.ts +22 -0
  29. package/dist/commands/content-permissions/index.js +141 -0
  30. package/dist/commands/content-permissions/index.js.map +1 -0
  31. package/dist/commands/context/index.d.ts +3 -0
  32. package/dist/commands/context/index.js +182 -0
  33. package/dist/commands/context/index.js.map +1 -0
  34. package/dist/commands/crm/associations.js +38 -0
  35. package/dist/commands/crm/associations.js.map +1 -1
  36. package/dist/commands/crm/custom-objects.js +39 -2
  37. package/dist/commands/crm/custom-objects.js.map +1 -1
  38. package/dist/commands/crm/dated.js +3 -3
  39. package/dist/commands/crm/dated.js.map +1 -1
  40. package/dist/commands/crm/migration.js +210 -2
  41. package/dist/commands/crm/migration.js.map +1 -1
  42. package/dist/commands/crm/properties.js +2 -1
  43. package/dist/commands/crm/properties.js.map +1 -1
  44. package/dist/commands/crm/property-batch.d.ts +1 -0
  45. package/dist/commands/crm/property-batch.js +44 -5
  46. package/dist/commands/crm/property-batch.js.map +1 -1
  47. package/dist/commands/crm/shared.js +21 -2
  48. package/dist/commands/crm/shared.js.map +1 -1
  49. package/dist/commands/crm/sync.js +1 -1
  50. package/dist/commands/crm/sync.js.map +1 -1
  51. package/dist/commands/doctor/index.js +193 -0
  52. package/dist/commands/doctor/index.js.map +1 -1
  53. package/dist/commands/email-tracking/index.d.ts +16 -0
  54. package/dist/commands/email-tracking/index.js +314 -0
  55. package/dist/commands/email-tracking/index.js.map +1 -0
  56. package/dist/commands/extensions/index.js +1 -1
  57. package/dist/commands/extensions/index.js.map +1 -1
  58. package/dist/commands/forms/index.js +15 -2
  59. package/dist/commands/forms/index.js.map +1 -1
  60. package/dist/commands/forms/legacy-v2.d.ts +7 -2
  61. package/dist/commands/forms/legacy-v2.js +103 -7
  62. package/dist/commands/forms/legacy-v2.js.map +1 -1
  63. package/dist/commands/guide/index.d.ts +3 -0
  64. package/dist/commands/guide/index.js +75 -16
  65. package/dist/commands/guide/index.js.map +1 -1
  66. package/dist/commands/internal/catalog.d.ts +1 -0
  67. package/dist/commands/internal/catalog.js +2 -0
  68. package/dist/commands/internal/catalog.js.map +1 -0
  69. package/dist/commands/internal/index.d.ts +26 -0
  70. package/dist/commands/internal/index.js +823 -0
  71. package/dist/commands/internal/index.js.map +1 -0
  72. package/dist/commands/internal-session.d.ts +15 -0
  73. package/dist/commands/internal-session.js +152 -0
  74. package/dist/commands/internal-session.js.map +1 -0
  75. package/dist/commands/lists/index.js +1 -1
  76. package/dist/commands/lists/index.js.map +1 -1
  77. package/dist/commands/marketing/email-audit.d.ts +32 -0
  78. package/dist/commands/marketing/email-audit.js +121 -0
  79. package/dist/commands/marketing/email-audit.js.map +1 -0
  80. package/dist/commands/marketing/index.js +2 -0
  81. package/dist/commands/marketing/index.js.map +1 -1
  82. package/dist/commands/mcp.js +5 -3
  83. package/dist/commands/mcp.js.map +1 -1
  84. package/dist/commands/playbooks/index.d.ts +3 -0
  85. package/dist/commands/playbooks/index.js +228 -0
  86. package/dist/commands/playbooks/index.js.map +1 -0
  87. package/dist/commands/portals/index.d.ts +3 -0
  88. package/dist/commands/portals/index.js +132 -0
  89. package/dist/commands/portals/index.js.map +1 -0
  90. package/dist/commands/reports/index.d.ts +3 -0
  91. package/dist/commands/reports/index.js +479 -0
  92. package/dist/commands/reports/index.js.map +1 -0
  93. package/dist/commands/sales-content/index.d.ts +13 -0
  94. package/dist/commands/sales-content/index.js +59 -0
  95. package/dist/commands/sales-content/index.js.map +1 -0
  96. package/dist/commands/sales-templates/index.d.ts +3 -0
  97. package/dist/commands/sales-templates/index.js +88 -0
  98. package/dist/commands/sales-templates/index.js.map +1 -0
  99. package/dist/commands/seed/cms.js +28 -45
  100. package/dist/commands/seed/cms.js.map +1 -1
  101. package/dist/commands/sequences/index.d.ts +18 -0
  102. package/dist/commands/sequences/index.js +263 -0
  103. package/dist/commands/sequences/index.js.map +1 -0
  104. package/dist/commands/settings/index.js +450 -102
  105. package/dist/commands/settings/index.js.map +1 -1
  106. package/dist/commands/settings/site-settings.d.ts +3 -0
  107. package/dist/commands/settings/site-settings.js +106 -0
  108. package/dist/commands/settings/site-settings.js.map +1 -0
  109. package/dist/commands/ui/index.d.ts +3 -0
  110. package/dist/commands/ui/index.js +422 -0
  111. package/dist/commands/ui/index.js.map +1 -0
  112. package/dist/commands/workflows/index.js +2 -0
  113. package/dist/commands/workflows/index.js.map +1 -1
  114. package/dist/commands/workflows/preflight.d.ts +23 -0
  115. package/dist/commands/workflows/preflight.js +295 -0
  116. package/dist/commands/workflows/preflight.js.map +1 -0
  117. package/dist/core/action-contract.d.ts +43 -0
  118. package/dist/core/action-contract.js +207 -0
  119. package/dist/core/action-contract.js.map +1 -0
  120. package/dist/core/auth.d.ts +6 -0
  121. package/dist/core/auth.js +85 -0
  122. package/dist/core/auth.js.map +1 -1
  123. package/dist/core/http.d.ts +8 -0
  124. package/dist/core/http.js +37 -5
  125. package/dist/core/http.js.map +1 -1
  126. package/dist/core/id-maps.d.ts +27 -0
  127. package/dist/core/id-maps.js +204 -0
  128. package/dist/core/id-maps.js.map +1 -0
  129. package/dist/core/output.d.ts +2 -1
  130. package/dist/core/output.js +34 -0
  131. package/dist/core/output.js.map +1 -1
  132. package/dist/mcp/profiles.d.ts +14 -0
  133. package/dist/mcp/profiles.js +68 -0
  134. package/dist/mcp/profiles.js.map +1 -0
  135. package/dist/mcp/server.d.ts +5 -3
  136. package/dist/mcp/server.js +22 -5
  137. package/dist/mcp/server.js.map +1 -1
  138. package/docs/CAPABILITY_LIBRARY.md +29 -8
  139. package/docs/COMMAND_TREE.md +98 -17
  140. package/docs/COMPARISON.md +8 -5
  141. package/docs/COOKBOOK.md +127 -5
  142. package/docs/INTERNAL-ENDPOINTS-DISCOVERY.md +813 -0
  143. package/docs/INTERNAL-ENDPOINTS.md +371 -0
  144. package/docs/INTERNAL_ENDPOINTS.md +243 -0
  145. package/docs/MARKETING_SETUP.md +21 -1
  146. package/docs/MCP.md +28 -89
  147. package/docs/MCP_TOOL_CATALOG.md +192 -0
  148. package/docs/MIGRATION_ADAPTERS.md +115 -0
  149. package/docs/PORTAL_BACKUP.md +212 -0
  150. package/docs/PUBLISHING.md +1 -1
  151. package/docs/SALES_CONTENT_INTERNAL_PUNCH_LIST.md +504 -0
  152. package/docs/TUTORIALS/secure-agent-writes.md +49 -0
  153. package/docs/WHY_HOW_WHAT.md +10 -4
  154. package/package.json +4 -2
@@ -1,41 +1,14 @@
1
- import { readFileSync } from "node:fs";
2
- import { getProfile } from "../../core/auth.js";
3
- import { createBrowserSessionClient, createClient } from "../../core/http.js";
1
+ import { createClient } from "../../core/http.js";
4
2
  import { CliError, printResult } from "../../core/output.js";
5
3
  import { encodePathSegment, maybeWrite, parseJsonPayload, parseNumberFlag } from "../crm/shared.js";
6
- function addBrowserSessionOptions(command) {
7
- return command
8
- .option("--portal-id <id>", "HubSpot portal ID (or HSCLI_PORTAL_ID / profile.portalId)")
9
- .option("--ui-domain <domain>", "HubSpot app domain, e.g. app.hubspot.com or app-eu1.hubspot.com")
10
- .option("--cookie <header>", "Browser Cookie header for app.hubspot.com")
11
- .option("--cookie-file <path>", "Cookie header, Netscape cookie jar, or JSON cookie export")
12
- .option("--csrf <token>", "x-hubspot-csrf-hubspotapi header value");
13
- }
4
+ import { addBrowserSessionOptions, resolveBrowserSession } from "../internal-session.js";
5
+ import { registerSiteSettings } from "./site-settings.js";
14
6
  function resolvePermissionSetSession(ctx, opts) {
15
- const profile = safeProfile(ctx.profile);
16
- const portalId = firstString(opts.portalId, process.env.HSCLI_PORTAL_ID, profile?.portalId);
17
- if (!portalId) {
18
- throw new CliError("SESSION_PORTAL_ID_REQUIRED", "Permission-set commands require --portal-id, HSCLI_PORTAL_ID, or profile.portalId.");
19
- }
20
- const uiDomain = normalizeUiDomain(firstString(opts.uiDomain, process.env.HSCLI_HUBSPOT_UI_DOMAIN, profile?.uiDomain, "app.hubspot.com") ?? "app.hubspot.com");
21
- const cookie = firstString(opts.cookie, process.env.HSCLI_HUBSPOT_COOKIE) ?? readCookieFile(firstString(opts.cookieFile, process.env.HSCLI_HUBSPOT_COOKIE_FILE));
22
- if (!cookie) {
23
- throw new CliError("SESSION_COOKIE_REQUIRED", "Permission-set commands require --cookie, --cookie-file, HSCLI_HUBSPOT_COOKIE, or HSCLI_HUBSPOT_COOKIE_FILE.");
24
- }
25
- const csrfToken = firstString(opts.csrf, process.env.HSCLI_HUBSPOT_CSRF, extractCsrfToken(cookie));
26
- if (!csrfToken) {
27
- throw new CliError("SESSION_CSRF_REQUIRED", "Permission-set commands require --csrf or HSCLI_HUBSPOT_CSRF. A csrf.app cookie is also accepted when present.");
28
- }
29
- return {
30
- portalId,
31
- client: createBrowserSessionClient(ctx.profile, {
32
- apiBaseUrl: `https://${uiDomain}`,
33
- cookie,
34
- csrfToken,
35
- telemetryFile: ctx.telemetryFile,
36
- strictCapabilities: ctx.strictCapabilities,
37
- }),
38
- };
7
+ const session = resolveBrowserSession(ctx, opts, "Permission-set commands", true);
8
+ return { client: session.client, portalId: session.portalId };
9
+ }
10
+ function resolveBusinessUnitSession(ctx, opts, requirePortalId = false) {
11
+ return resolveBrowserSession(ctx, opts, "Business-unit internal commands", requirePortalId);
39
12
  }
40
13
  function permissionSetsPath(portalId) {
41
14
  return `/api/app-users/v1/permission-sets?portalId=${encodeURIComponent(portalId)}`;
@@ -43,6 +16,87 @@ function permissionSetsPath(portalId) {
43
16
  function permissionSetPath(portalId, id) {
44
17
  return `/api/app-users/v1/permission-sets/${encodePathSegment(id, "permissionSetId")}?portalId=${encodeURIComponent(portalId)}`;
45
18
  }
19
+ function permissionSetClonePath(portalId, id) {
20
+ return `/api/app-users/v1/permission-sets/${encodePathSegment(id, "permissionSetId")}/clone?portalId=${encodeURIComponent(portalId)}`;
21
+ }
22
+ function appUserPath(portalId, userId) {
23
+ return `/api/app-users/v1/users/${encodePathSegment(userId, "userId")}?portalId=${encodeURIComponent(portalId)}`;
24
+ }
25
+ function appUsersPath(portalId, limit, offset) {
26
+ const params = new URLSearchParams();
27
+ params.set("portalId", portalId);
28
+ if (limit)
29
+ params.set("limit", String(parseNumberFlag(limit, "--limit")));
30
+ if (offset)
31
+ params.set("offset", offset);
32
+ return `/api/app-users/v1/users?${params.toString()}`;
33
+ }
34
+ function appUsersBatchPath(portalId) {
35
+ return `/api/app-users/v1/users/partial/batch?portalId=${encodeURIComponent(portalId)}`;
36
+ }
37
+ function roleCatalogPath(portalId) {
38
+ return `/api/app-users/v1/roles?portalId=${encodeURIComponent(portalId)}`;
39
+ }
40
+ function roleCatalogItemPath(portalId, roleName) {
41
+ return `/api/app-users/v1/roles/${encodePathSegment(roleName, "roleName")}?portalId=${encodeURIComponent(portalId)}`;
42
+ }
43
+ function roleCategoriesPath(portalId) {
44
+ return `/api/app-users/v1/role-categories?portalId=${encodeURIComponent(portalId)}`;
45
+ }
46
+ function roleScopesPath(portalId) {
47
+ return `/api/app-users/v1/scopes?portalId=${encodeURIComponent(portalId)}`;
48
+ }
49
+ function seatsPath(portalId) {
50
+ return `/api/app-users/v1/seats?portalId=${encodeURIComponent(portalId)}`;
51
+ }
52
+ function seatAssignmentsPath(portalId) {
53
+ return `/api/app-users/v1/seat-assignments?portalId=${encodeURIComponent(portalId)}`;
54
+ }
55
+ function seatAssignmentPath(portalId, userId) {
56
+ return `/api/app-users/v1/seat-assignments/${encodePathSegment(userId, "userId")}?portalId=${encodeURIComponent(portalId)}`;
57
+ }
58
+ function permissionTemplatesPath(portalId) {
59
+ return `/api/permission-templates/v1/templates?portalId=${encodeURIComponent(portalId)}`;
60
+ }
61
+ function permissionTemplatePath(portalId, templateId) {
62
+ return `/api/permission-templates/v1/templates/${encodePathSegment(templateId, "templateId")}?portalId=${encodeURIComponent(portalId)}`;
63
+ }
64
+ function permissionSetAssignmentsPath(portalId) {
65
+ return `/api/app-users/v1/permission-sets/permission-set-user-assignments?portalId=${encodeURIComponent(portalId)}`;
66
+ }
67
+ function permissionSetAssignUserRpcPath(portalId) {
68
+ return `/api/chirp-frontend-app/v1/gateway/com.hubspot.appusers.rpc.PermissionSetAssignmentsRpc/assignUserToPermissionSets?portalId=${encodeURIComponent(portalId)}`;
69
+ }
70
+ const PERMISSION_AUDIT_RPC = {
71
+ "assignment-history": "getPermissionSetAssignmentHistory",
72
+ "role-change-history": "getRoleChangeHistory",
73
+ "user-access": "getUserAccessAudit",
74
+ };
75
+ function permissionAuditRpcPath(name) {
76
+ return `/api/chirp-frontend-app/v1/gateway/com.hubspot.app.user.audits.service.rpc.AppUserAuditsRpc/${PERMISSION_AUDIT_RPC[name]}`;
77
+ }
78
+ function flpAllowlistingsPath() {
79
+ return "/api/crm-permissions/v1/field-level-permissions/allowlistings";
80
+ }
81
+ function flpObjectAllowlistingsPath(typeId) {
82
+ return `/api/crm-permissions/v1/field-level-permissions/allowlistings/${encodePathSegment(typeId, "typeId")}`;
83
+ }
84
+ function flpPropertyAllowlistingsPath(typeId, propertyName) {
85
+ return `${flpObjectAllowlistingsPath(typeId)}/${encodePathSegment(propertyName, "propertyName")}`;
86
+ }
87
+ function flpAccessStatePath() {
88
+ return "/api/crm-permissions/v1/field-level-permissions/allowlistings/access-state/multi-get";
89
+ }
90
+ function flpBlocklistPath() {
91
+ return "/api/crm-permissions/v1/field-level-permissions/blocklist/including-global";
92
+ }
93
+ function flpBypassUsersPath() {
94
+ return "/api/permissions-skynet/v1/scopes/app/field-level-permissions-bypass/user-ids";
95
+ }
96
+ function businessUnitsInternalPath(portalId) {
97
+ const query = portalId ? `?portalId=${encodeURIComponent(portalId)}` : "";
98
+ return `/api/business-units/v1/business-units${query}`;
99
+ }
46
100
  async function maybeWritePermissionSetWithRoleRetry(ctx, client, method, path, body, stripUnknownRoles) {
47
101
  try {
48
102
  return await maybeWrite(ctx, client, method, path, body);
@@ -75,6 +129,65 @@ function extractPermissionSets(response) {
75
129
  }
76
130
  return [];
77
131
  }
132
+ function buildBusinessUnitIdMapSeed(response) {
133
+ const seed = {};
134
+ for (const unit of extractBusinessUnits(response)) {
135
+ const id = firstString(stringFromUnknown(unit.id), stringFromUnknown(unit.businessUnitId), stringFromUnknown(unit.unitId));
136
+ if (!id)
137
+ continue;
138
+ seed[id] = {
139
+ sourceName: firstString(stringFromUnknown(unit.name), stringFromUnknown(unit.label)),
140
+ target_id: "",
141
+ };
142
+ }
143
+ return seed;
144
+ }
145
+ function extractBusinessUnits(response) {
146
+ if (Array.isArray(response))
147
+ return response.filter(isRecord);
148
+ if (!isRecord(response))
149
+ return [];
150
+ for (const key of ["results", "businessUnits", "items"]) {
151
+ const value = response[key];
152
+ if (Array.isArray(value))
153
+ return value.filter(isRecord);
154
+ }
155
+ const data = isRecord(response.data) ? response.data : undefined;
156
+ if (data) {
157
+ for (const key of ["results", "businessUnits", "items"]) {
158
+ const value = data[key];
159
+ if (Array.isArray(value))
160
+ return value.filter(isRecord);
161
+ }
162
+ }
163
+ return [];
164
+ }
165
+ async function findExistingBusinessUnitByName(client, name, portalId) {
166
+ const response = await client.request(businessUnitsInternalPath(portalId));
167
+ return extractBusinessUnits(response).find((unit) => unit.name === name);
168
+ }
169
+ function normalizeBusinessUnitCreatePayload(raw) {
170
+ if (!isRecord(raw)) {
171
+ throw new CliError("INVALID_BUSINESS_UNIT_PAYLOAD", "Business-unit create payload must be a JSON object.");
172
+ }
173
+ const name = typeof raw.name === "string" ? raw.name.trim() : "";
174
+ if (!name) {
175
+ throw new CliError("BUSINESS_UNIT_NAME_REQUIRED", "Business-unit create payload requires a non-empty name.");
176
+ }
177
+ const payload = { name };
178
+ for (const key of ["label", "description", "logoMetadata"]) {
179
+ if (raw[key] !== undefined && raw[key] !== null && raw[key] !== "")
180
+ payload[key] = raw[key];
181
+ }
182
+ return payload;
183
+ }
184
+ function stringFromUnknown(value) {
185
+ if (typeof value === "string")
186
+ return value;
187
+ if (typeof value === "number" && Number.isFinite(value))
188
+ return String(value);
189
+ return undefined;
190
+ }
78
191
  function extractMissingRoles(error) {
79
192
  if (!(error instanceof CliError) || error.status !== 400)
80
193
  return [];
@@ -97,14 +210,6 @@ function stripRoles(body, rolesToStrip) {
97
210
  return undefined;
98
211
  return { ...body, roleNames: kept };
99
212
  }
100
- function safeProfile(profile) {
101
- try {
102
- return getProfile(profile);
103
- }
104
- catch {
105
- return undefined;
106
- }
107
- }
108
213
  function firstString(...values) {
109
214
  for (const value of values) {
110
215
  const trimmed = value?.trim();
@@ -113,73 +218,87 @@ function firstString(...values) {
113
218
  }
114
219
  return undefined;
115
220
  }
116
- function normalizeUiDomain(raw) {
117
- return raw.replace(/^https?:\/\//, "").replace(/\/.*$/, "");
221
+ function isRecord(value) {
222
+ return Boolean(value && typeof value === "object" && !Array.isArray(value));
118
223
  }
119
- function readCookieFile(path) {
120
- if (!path)
121
- return undefined;
122
- const raw = readFileSync(path, "utf8").trim();
123
- if (!raw)
124
- return undefined;
125
- return normalizeCookieFile(raw);
126
- }
127
- function normalizeCookieFile(raw) {
128
- const parsed = parseCookieJson(raw);
129
- if (parsed)
130
- return parsed;
131
- const lines = raw.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
132
- const netscapeCookies = lines
133
- .filter((line) => !line.startsWith("#"))
134
- .map((line) => line.split(/\t+/))
135
- .filter((parts) => parts.length >= 7)
136
- .map((parts) => `${parts[5]}=${parts.slice(6).join("\t")}`);
137
- if (netscapeCookies.length > 0)
138
- return netscapeCookies.join("; ");
139
- return lines.filter((line) => !line.startsWith("#")).join("; ");
140
- }
141
- function parseCookieJson(raw) {
142
- try {
143
- const parsed = JSON.parse(raw);
144
- if (typeof parsed === "string")
145
- return parsed;
146
- if (isRecord(parsed) && typeof parsed.cookie === "string")
147
- return parsed.cookie;
148
- const cookies = Array.isArray(parsed) ? parsed : isRecord(parsed) && Array.isArray(parsed.cookies) ? parsed.cookies : undefined;
149
- if (!cookies)
150
- return undefined;
151
- const pairs = cookies
152
- .filter(isRecord)
153
- .map((cookie) => {
154
- const name = typeof cookie.name === "string" ? cookie.name.trim() : "";
155
- const value = typeof cookie.value === "string" ? cookie.value : "";
156
- return name ? `${name}=${value}` : "";
157
- })
158
- .filter(Boolean);
159
- return pairs.length > 0 ? pairs.join("; ") : undefined;
224
+ function normalizeUserCreatePayload(raw, allowInviteEmail) {
225
+ if (!isRecord(raw)) {
226
+ throw new CliError("INVALID_USER_PAYLOAD", "User create payload must be a JSON object.");
160
227
  }
161
- catch {
162
- return undefined;
228
+ const payload = { ...raw };
229
+ const inviteFields = [
230
+ payload.sendWelcomeEmail === true ? "sendWelcomeEmail=true" : undefined,
231
+ payload.sendInviteEmail === true ? "sendInviteEmail=true" : undefined,
232
+ payload.skipInviteEmail === false ? "skipInviteEmail=false" : undefined,
233
+ ].filter(Boolean);
234
+ if (!allowInviteEmail && inviteFields.length > 0) {
235
+ throw new CliError("USER_INVITE_EMAIL_BLOCKED", `User invite email blocked by default (${inviteFields.join(", ")}). Set sendWelcomeEmail:false or pass --allow-invite-email.`);
236
+ }
237
+ if (!allowInviteEmail) {
238
+ payload.sendWelcomeEmail = false;
163
239
  }
240
+ return payload;
164
241
  }
165
- function extractCsrfToken(cookieHeader) {
166
- const candidates = new Set(["csrf.app", "csrf", "hubspotapi-csrf", "hs-csrf"]);
167
- for (const part of cookieHeader.split(";")) {
168
- const [rawName, ...rawValue] = part.trim().split("=");
169
- if (!rawName || rawValue.length === 0)
170
- continue;
171
- if (candidates.has(rawName.trim().toLowerCase())) {
172
- const value = rawValue.join("=").trim();
173
- return value ? decodeURIComponent(value) : undefined;
174
- }
242
+ function permissionSetIdsFromOptions(opts) {
243
+ const single = firstString(opts.permissionSetId);
244
+ const many = firstString(opts.permissionSetIds);
245
+ if (single && many) {
246
+ throw new CliError("INVALID_PERMISSION_SET_ASSIGNMENT", "Use either --permission-set-id or --permission-set-ids, not both.");
175
247
  }
176
- return undefined;
248
+ if (single)
249
+ return [single];
250
+ if (many) {
251
+ const ids = many.split(",").map((id) => id.trim()).filter(Boolean);
252
+ if (ids.length === 0)
253
+ throw new CliError("INVALID_PERMISSION_SET_ASSIGNMENT", "--permission-set-ids must contain at least one id.");
254
+ return ids;
255
+ }
256
+ throw new CliError("INVALID_PERMISSION_SET_ASSIGNMENT", "Assign requires --permission-set-id or --permission-set-ids.");
177
257
  }
178
- function isRecord(value) {
179
- return Boolean(value && typeof value === "object" && !Array.isArray(value));
258
+ function idStringFromAssignmentValue(value, fieldName) {
259
+ const id = stringFromUnknown(value)?.trim();
260
+ if (!id) {
261
+ throw new CliError("INVALID_PERMISSION_SET_ASSIGNMENT", `${fieldName} must be a non-empty string or number.`);
262
+ }
263
+ return id;
264
+ }
265
+ function buildPermissionSetAssignmentPayload(userId, opts) {
266
+ return {
267
+ assignments: [
268
+ {
269
+ userId,
270
+ permissionSetIds: permissionSetIdsFromOptions(opts),
271
+ },
272
+ ],
273
+ };
274
+ }
275
+ function normalizePermissionSetAssignmentsPayload(raw) {
276
+ if (!isRecord(raw)) {
277
+ throw new CliError("INVALID_PERMISSION_SET_ASSIGNMENT", "Assignment payload must be a JSON object.");
278
+ }
279
+ const assignments = raw.assignments;
280
+ if (!Array.isArray(assignments) || assignments.length === 0) {
281
+ throw new CliError("INVALID_PERMISSION_SET_ASSIGNMENT", "Assignment payload requires a non-empty assignments array.");
282
+ }
283
+ return {
284
+ assignments: assignments.map((assignment, index) => {
285
+ if (!isRecord(assignment)) {
286
+ throw new CliError("INVALID_PERMISSION_SET_ASSIGNMENT", `assignments[${index}] must be a JSON object.`);
287
+ }
288
+ const permissionSetIds = assignment.permissionSetIds;
289
+ if (!Array.isArray(permissionSetIds) || permissionSetIds.length === 0) {
290
+ throw new CliError("INVALID_PERMISSION_SET_ASSIGNMENT", `assignments[${index}].permissionSetIds must be a non-empty array.`);
291
+ }
292
+ return {
293
+ userId: idStringFromAssignmentValue(assignment.userId, `assignments[${index}].userId`),
294
+ permissionSetIds: permissionSetIds.map((id, idIndex) => idStringFromAssignmentValue(id, `assignments[${index}].permissionSetIds[${idIndex}]`)),
295
+ };
296
+ }),
297
+ };
180
298
  }
181
299
  export function registerSettings(program, getCtx) {
182
300
  const settings = program.command("settings").description("HubSpot Settings APIs");
301
+ registerSiteSettings(settings, getCtx);
183
302
  // Users
184
303
  const users = settings.command("users").description("User provisioning");
185
304
  users
@@ -205,10 +324,11 @@ export function registerSettings(program, getCtx) {
205
324
  users
206
325
  .command("create")
207
326
  .requiredOption("--data <payload>", "User creation payload JSON")
327
+ .option("--allow-invite-email", "Allow the create payload to send welcome/invite email")
208
328
  .action(async (opts) => {
209
329
  const ctx = getCtx();
210
330
  const client = createClient(ctx.profile);
211
- const payload = parseJsonPayload(opts.data);
331
+ const payload = normalizeUserCreatePayload(parseJsonPayload(opts.data), Boolean(opts.allowInviteEmail));
212
332
  const res = await maybeWrite(ctx, client, "POST", "/settings/v3/users/", payload);
213
333
  printResult(ctx, res);
214
334
  });
@@ -257,6 +377,33 @@ export function registerSettings(program, getCtx) {
257
377
  const res = await client.request(`/settings/v3/business-units/?${params.toString()}`);
258
378
  printResult(ctx, res);
259
379
  });
380
+ addBrowserSessionOptions(businessUnits
381
+ .command("capture")
382
+ .description("Capture full business-unit metadata via internal HubSpot session-auth endpoint")
383
+ .option("--include-id-map-seed", "Include a source-ID keyed business-unit map seed")).action(async (opts) => {
384
+ const ctx = getCtx();
385
+ const { client, portalId } = resolveBusinessUnitSession(ctx, opts);
386
+ const res = await client.request(businessUnitsInternalPath(portalId));
387
+ printResult(ctx, opts.includeIdMapSeed ? { result: res, idMapSeed: buildBusinessUnitIdMapSeed(res) } : res);
388
+ });
389
+ addBrowserSessionOptions(businessUnits
390
+ .command("create-internal")
391
+ .description("Create a business unit via the internal browser-session endpoint")
392
+ .requiredOption("--data <payload>", "Business-unit payload JSON: { name, label?, description?, logoMetadata? }")
393
+ .option("--skip-existing", "Skip create when a target business unit with the same name already exists")).action(async (opts) => {
394
+ const ctx = getCtx();
395
+ const { client, portalId } = resolveBusinessUnitSession(ctx, opts, true);
396
+ const payload = normalizeBusinessUnitCreatePayload(parseJsonPayload(opts.data));
397
+ if (!ctx.dryRun && opts.skipExisting) {
398
+ const existing = await findExistingBusinessUnitByName(client, String(payload.name), portalId);
399
+ if (existing) {
400
+ printResult(ctx, { skipped: true, reason: "business-unit-exists", matchKey: "name", existing });
401
+ return;
402
+ }
403
+ }
404
+ const res = await maybeWrite(ctx, client, "POST", businessUnitsInternalPath(portalId), payload);
405
+ printResult(ctx, res);
406
+ });
260
407
  // Teams
261
408
  const teams = settings.command("teams").description("Team management");
262
409
  teams
@@ -326,6 +473,207 @@ export function registerSettings(program, getCtx) {
326
473
  const res = await maybeWrite(ctx, client, "DELETE", permissionSetPath(portalId, id));
327
474
  printResult(ctx, res);
328
475
  });
476
+ addBrowserSessionOptions(permissionSets.command("clone")
477
+ .argument("<id>")
478
+ .description("Clone a permission set through the internal browser-session endpoint")
479
+ .option("--data <payload>", "Optional clone payload JSON, e.g. { name }")).action(async (id, opts) => {
480
+ const ctx = getCtx();
481
+ const { client, portalId } = resolvePermissionSetSession(ctx, opts);
482
+ const payload = opts.data ? parseJsonPayload(opts.data) : {};
483
+ const res = await maybeWrite(ctx, client, "POST", permissionSetClonePath(portalId, id), payload);
484
+ printResult(ctx, res);
485
+ });
486
+ const permissionSetRoles = permissionSets.command("roles").description("Internal atomic role catalog used by permission sets");
487
+ addBrowserSessionOptions(permissionSetRoles.command("list"))
488
+ .action(async (opts) => {
489
+ const ctx = getCtx();
490
+ const { client, portalId } = resolvePermissionSetSession(ctx, opts);
491
+ const res = await client.request(roleCatalogPath(portalId));
492
+ printResult(ctx, res);
493
+ });
494
+ addBrowserSessionOptions(permissionSetRoles.command("get").argument("<roleName>"))
495
+ .action(async (roleName, opts) => {
496
+ const ctx = getCtx();
497
+ const { client, portalId } = resolvePermissionSetSession(ctx, opts);
498
+ const res = await client.request(roleCatalogItemPath(portalId, roleName));
499
+ printResult(ctx, res);
500
+ });
501
+ const permissionSetAssignments = permissionSets.command("assignments").description("Internal permission-set assignment map");
502
+ addBrowserSessionOptions(permissionSetAssignments.command("list"))
503
+ .action(async (opts) => {
504
+ const ctx = getCtx();
505
+ const { client, portalId } = resolvePermissionSetSession(ctx, opts);
506
+ const res = await client.request(permissionSetAssignmentsPath(portalId));
507
+ printResult(ctx, res);
508
+ });
509
+ const permissionSetAudit = permissionSets.command("audit").description("Read-only internal permission audit RPCs");
510
+ for (const name of Object.keys(PERMISSION_AUDIT_RPC)) {
511
+ addBrowserSessionOptions(permissionSetAudit.command(name)
512
+ .requiredOption("--data <payload>", "Audit RPC payload JSON")).action(async (opts) => {
513
+ const ctx = getCtx();
514
+ const { client } = resolvePermissionSetSession(ctx, opts);
515
+ const payload = parseJsonPayload(opts.data);
516
+ const res = await client.request(permissionAuditRpcPath(name), {
517
+ method: "POST",
518
+ body: payload,
519
+ permissionMode: "read",
520
+ });
521
+ printResult(ctx, res);
522
+ });
523
+ }
524
+ addBrowserSessionOptions(permissionSets.command("role-categories").description("List internal permission role categories"))
525
+ .action(async (opts) => {
526
+ const ctx = getCtx();
527
+ const { client, portalId } = resolvePermissionSetSession(ctx, opts);
528
+ const res = await client.request(roleCategoriesPath(portalId));
529
+ printResult(ctx, res);
530
+ });
531
+ addBrowserSessionOptions(permissionSets.command("scopes").description("List internal permission role scope buckets"))
532
+ .action(async (opts) => {
533
+ const ctx = getCtx();
534
+ const { client, portalId } = resolvePermissionSetSession(ctx, opts);
535
+ const res = await client.request(roleScopesPath(portalId));
536
+ printResult(ctx, res);
537
+ });
538
+ const permissionSetUsers = permissionSets.command("users").description("Internal app-users reads and permission-set assignments");
539
+ addBrowserSessionOptions(permissionSetUsers.command("list")
540
+ .option("--limit <n>", "Max records", "200")
541
+ .option("--offset <n>", "Offset cursor")).action(async (opts) => {
542
+ const ctx = getCtx();
543
+ const { client, portalId } = resolvePermissionSetSession(ctx, opts);
544
+ const res = await client.request(appUsersPath(portalId, opts.limit, opts.offset));
545
+ printResult(ctx, res);
546
+ });
547
+ addBrowserSessionOptions(permissionSetUsers.command("get").argument("<userId>"))
548
+ .action(async (userId, opts) => {
549
+ const ctx = getCtx();
550
+ const { client, portalId } = resolvePermissionSetSession(ctx, opts);
551
+ const res = await client.request(appUserPath(portalId, userId));
552
+ printResult(ctx, res);
553
+ });
554
+ addBrowserSessionOptions(permissionSetUsers.command("batch-read")
555
+ .requiredOption("--data <payload>", "Batch-read payload JSON")).action(async (opts) => {
556
+ const ctx = getCtx();
557
+ const { client, portalId } = resolvePermissionSetSession(ctx, opts);
558
+ const payload = parseJsonPayload(opts.data);
559
+ const res = await maybeWrite(ctx, client, "POST", appUsersBatchPath(portalId), payload);
560
+ printResult(ctx, res);
561
+ });
562
+ addBrowserSessionOptions(permissionSetUsers.command("assign")
563
+ .argument("<userId>")
564
+ .option("--permission-set-id <id>", "Assign one permission set id")
565
+ .option("--permission-set-ids <ids>", "Assign multiple permission set ids, comma-separated")).action(async (userId, opts) => {
566
+ const ctx = getCtx();
567
+ const { client, portalId } = resolvePermissionSetSession(ctx, opts);
568
+ const payload = buildPermissionSetAssignmentPayload(userId, opts);
569
+ const res = await maybeWrite(ctx, client, "POST", permissionSetAssignUserRpcPath(portalId), payload);
570
+ printResult(ctx, res);
571
+ });
572
+ addBrowserSessionOptions(permissionSetUsers.command("assign-batch")
573
+ .description("Assign permission sets to one or more users through the allowlisted Chirp RPC")
574
+ .requiredOption("--data <payload>", "Assignment payload JSON: { assignments: [{ userId, permissionSetIds }] }")).action(async (opts) => {
575
+ const ctx = getCtx();
576
+ const { client, portalId } = resolvePermissionSetSession(ctx, opts);
577
+ const payload = normalizePermissionSetAssignmentsPayload(parseJsonPayload(opts.data));
578
+ const res = await maybeWrite(ctx, client, "POST", permissionSetAssignUserRpcPath(portalId), payload);
579
+ printResult(ctx, res);
580
+ });
581
+ const permissionSetSeatAssignments = permissionSets.command("seat-assignments").description("Internal seat assignment inventory and updates");
582
+ addBrowserSessionOptions(permissionSetSeatAssignments.command("list"))
583
+ .action(async (opts) => {
584
+ const ctx = getCtx();
585
+ const { client, portalId } = resolvePermissionSetSession(ctx, opts);
586
+ const res = await client.request(seatAssignmentsPath(portalId));
587
+ printResult(ctx, res);
588
+ });
589
+ addBrowserSessionOptions(permissionSetSeatAssignments.command("update")
590
+ .argument("<userId>")
591
+ .requiredOption("--data <payload>", "Seat assignment update payload JSON")).action(async (userId, opts) => {
592
+ const ctx = getCtx();
593
+ const { client, portalId } = resolvePermissionSetSession(ctx, opts);
594
+ const payload = parseJsonPayload(opts.data);
595
+ const res = await maybeWrite(ctx, client, "PATCH", seatAssignmentPath(portalId, userId), payload);
596
+ printResult(ctx, res);
597
+ });
598
+ addBrowserSessionOptions(permissionSets.command("seats").description("List internal seat catalog"))
599
+ .action(async (opts) => {
600
+ const ctx = getCtx();
601
+ const { client, portalId } = resolvePermissionSetSession(ctx, opts);
602
+ const res = await client.request(seatsPath(portalId));
603
+ printResult(ctx, res);
604
+ });
605
+ const permissionSetTemplates = permissionSets.command("templates").description("Internal permission template catalog");
606
+ addBrowserSessionOptions(permissionSetTemplates.command("list"))
607
+ .action(async (opts) => {
608
+ const ctx = getCtx();
609
+ const { client, portalId } = resolvePermissionSetSession(ctx, opts);
610
+ const res = await client.request(permissionTemplatesPath(portalId));
611
+ printResult(ctx, res);
612
+ });
613
+ addBrowserSessionOptions(permissionSetTemplates.command("get").argument("<templateId>"))
614
+ .action(async (templateId, opts) => {
615
+ const ctx = getCtx();
616
+ const { client, portalId } = resolvePermissionSetSession(ctx, opts);
617
+ const res = await client.request(permissionTemplatePath(portalId, templateId));
618
+ printResult(ctx, res);
619
+ });
620
+ addBrowserSessionOptions(permissionSetTemplates.command("create")
621
+ .requiredOption("--data <payload>", "Permission template payload JSON")).action(async (opts) => {
622
+ const ctx = getCtx();
623
+ const { client, portalId } = resolvePermissionSetSession(ctx, opts);
624
+ const payload = parseJsonPayload(opts.data);
625
+ const res = await maybeWrite(ctx, client, "POST", permissionTemplatesPath(portalId), payload);
626
+ printResult(ctx, res);
627
+ });
628
+ const flp = settings.command("flp").description("Field-level permission inventory via internal HubSpot session-auth endpoint");
629
+ addBrowserSessionOptions(flp.command("list").description("List all field-level permission allowlistings"))
630
+ .action(async (opts) => {
631
+ const ctx = getCtx();
632
+ const { client } = resolvePermissionSetSession(ctx, opts);
633
+ const res = await client.request(flpAllowlistingsPath());
634
+ printResult(ctx, res);
635
+ });
636
+ addBrowserSessionOptions(flp.command("object").argument("<typeId>").description("List field-level permission allowlistings for one object type"))
637
+ .action(async (typeId, opts) => {
638
+ const ctx = getCtx();
639
+ const { client } = resolvePermissionSetSession(ctx, opts);
640
+ const res = await client.request(flpObjectAllowlistingsPath(typeId));
641
+ printResult(ctx, res);
642
+ });
643
+ addBrowserSessionOptions(flp.command("property").argument("<typeId>").argument("<propertyName>").description("Get field-level permission allowlisting for one property"))
644
+ .action(async (typeId, propertyName, opts) => {
645
+ const ctx = getCtx();
646
+ const { client } = resolvePermissionSetSession(ctx, opts);
647
+ const res = await client.request(flpPropertyAllowlistingsPath(typeId, propertyName));
648
+ printResult(ctx, res);
649
+ });
650
+ addBrowserSessionOptions(flp.command("access-state")
651
+ .description("Bulk-read field-level permission access state")
652
+ .requiredOption("--data <payload>", "Access-state payload JSON")).action(async (opts) => {
653
+ const ctx = getCtx();
654
+ const { client } = resolvePermissionSetSession(ctx, opts);
655
+ const payload = parseJsonPayload(opts.data);
656
+ const res = await client.request(flpAccessStatePath(), {
657
+ method: "POST",
658
+ body: payload,
659
+ permissionMode: "read",
660
+ });
661
+ printResult(ctx, res);
662
+ });
663
+ addBrowserSessionOptions(flp.command("blocklist").description("List field-level permission blocklist including global entries"))
664
+ .action(async (opts) => {
665
+ const ctx = getCtx();
666
+ const { client } = resolvePermissionSetSession(ctx, opts);
667
+ const res = await client.request(flpBlocklistPath());
668
+ printResult(ctx, res);
669
+ });
670
+ addBrowserSessionOptions(flp.command("bypass-users").description("List users who bypass field-level permissions"))
671
+ .action(async (opts) => {
672
+ const ctx = getCtx();
673
+ const { client } = resolvePermissionSetSession(ctx, opts);
674
+ const res = await client.request(flpBypassUsersPath());
675
+ printResult(ctx, res);
676
+ });
329
677
  // Currencies
330
678
  const currencies = settings.command("currencies").description("Multi-currency management");
331
679
  currencies