kalup 0.2.0 → 0.3.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 +2 -2
- package/dist/{commands-Bk1w1oup.mjs → commands-T4j_GVBD.mjs} +312 -188
- package/dist/commands.d.mts +1 -1
- package/dist/commands.mjs +1 -1
- package/dist/{context-8pARDYRR.d.mts → context-C1tH5K0x.d.mts} +10 -0
- package/dist/{host-BoqS00po.mjs → host-CDDeLV5T.mjs} +1 -1
- package/dist/host.d.mts +1 -1
- package/dist/host.mjs +1 -1
- package/dist/index.mjs +1 -1
- package/docs/apply.md +1 -1
- package/docs/config.md +1 -1
- package/docs/errors/E_BLUEPRINT_SCHEMA.md +2 -2
- package/docs/errors/E_DEFINITION_FIELD.md +1 -1
- package/docs/errors/E_DIR_AMBIGUOUS.md +4 -4
- package/docs/errors/E_HS_PREFIX.md +3 -3
- package/docs/errors/E_HTTP.md +1 -1
- package/docs/errors/E_PULL_INVALID.md +2 -2
- package/docs/errors/W_LEGACY_DIR.md +2 -2
- package/docs/errors/W_LIMIT_UNREADABLE.md +2 -2
- package/docs/errors/W_RATE_HEADERS.md +1 -1
- package/docs/errors/W_RATE_LIMIT.md +1 -1
- package/docs/errors/W_WRITE_SCOPE.md +17 -0
- package/docs/plan.md +3 -3
- package/docs/targets.md +1 -1
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -53,7 +53,7 @@ Commit `kalup.config.ts` and `hubspot/`. Keep `.kalup/`, plan files and `.env` o
|
|
|
53
53
|
|
|
54
54
|
`kalup <command> --help` lists each command's flags.
|
|
55
55
|
|
|
56
|
-
## What
|
|
56
|
+
## What it covers
|
|
57
57
|
|
|
58
58
|
- Reads and writes properties and property groups on standard and custom objects. Custom object schemas are read and compared, not written.
|
|
59
59
|
- Every property definition field HubSpot lets you write, such as display hints, `hidden`, `displayOrder` and calculation formulas, checked against a live developer test account.
|
|
@@ -61,7 +61,7 @@ Commit `kalup.config.ts` and `hubspot/`. Keep `.kalup/`, plan files and `.env` o
|
|
|
61
61
|
- State local to your machine by default, or committed with `state: 'repo'`. Monorepos and git worktrees.
|
|
62
62
|
- Every command takes `--json` and prints one `envelope/1` document with stable issue codes and exit codes. The JSON Schemas ship as `kalup/schemas/<file>`.
|
|
63
63
|
|
|
64
|
-
The pull, plan, apply and drift workflow passed [live runs](https://github.com/scopiousdigital/kalup/blob/main/docs/hubspot.md#live-runs) on a HubSpot developer test account
|
|
64
|
+
The pull, plan, apply and drift workflow passed [live runs](https://github.com/scopiousdigital/kalup/blob/main/docs/hubspot.md#live-runs) on a HubSpot developer test account. Start on a test account or sandbox. Pipelines, custom object schema writes and association labels are next. Before 1.0, a minor release may change the config grammar or the JSON output, and its release notes say so.
|
|
65
65
|
|
|
66
66
|
## Docs
|
|
67
67
|
|
|
@@ -1252,6 +1252,15 @@ const TYPE_FIELDS = {
|
|
|
1252
1252
|
currencyPropertyName: ["number"],
|
|
1253
1253
|
textDisplayHint: ["string", "phone_number"]
|
|
1254
1254
|
};
|
|
1255
|
+
/**
|
|
1256
|
+
* The name prefixes HubSpot reserves: `hs_` for its own properties and `a<appId>_` for an integration's. A create with
|
|
1257
|
+
* either is refused (400, live runs 2026-10-01). Kalup never manages such a property; pull writes it as a reference.
|
|
1258
|
+
*/
|
|
1259
|
+
const RESERVED_PREFIX = /^(hs_|a\d+_)/;
|
|
1260
|
+
/** The reserved prefix a name carries, or undefined. */
|
|
1261
|
+
function reservedPrefix(name) {
|
|
1262
|
+
return RESERVED_PREFIX.exec(name)?.[1];
|
|
1263
|
+
}
|
|
1255
1264
|
const ADDRESS = /^[a-z]+:\S+$/;
|
|
1256
1265
|
/** Whether `value` is an address: a lowercase type, a colon and a path without whitespace. */
|
|
1257
1266
|
function isAddress(value) {
|
|
@@ -2692,7 +2701,7 @@ function validate$2(loaded, options = {}) {
|
|
|
2692
2701
|
});
|
|
2693
2702
|
if (loaded.layout.legacy) warnings.push({
|
|
2694
2703
|
code: "W_LEGACY_DIR",
|
|
2695
|
-
message: `the object files are in ${LEGACY_DIR}/, the
|
|
2704
|
+
message: `the object files are in ${LEGACY_DIR}/, the old default folder; the default is now ${DEFAULT_DIR}/`,
|
|
2696
2705
|
file: CONFIG$2$1,
|
|
2697
2706
|
fix: `add dir: '${LEGACY_DIR}' to ${CONFIG$2$1}, or move ${LEGACY_DIR}/ to ${DEFAULT_DIR}/`
|
|
2698
2707
|
});
|
|
@@ -2807,9 +2816,10 @@ function checkProperty$1(loaded, address, resource, keys, { issues, warnings })
|
|
|
2807
2816
|
fix: "list the options, or drop .strict()"
|
|
2808
2817
|
});
|
|
2809
2818
|
checkLifecycle(resource, d, at, issues);
|
|
2810
|
-
|
|
2819
|
+
const reserved = reservedPrefix(name);
|
|
2820
|
+
if (resource.managed && reserved !== void 0) issues.push({
|
|
2811
2821
|
code: "E_HS_PREFIX",
|
|
2812
|
-
message: `'${name}' starts with
|
|
2822
|
+
message: `'${name}' starts with ${reserved}, a prefix HubSpot reserves (hs_ for its own properties, a<digits>_ for an integration's)`,
|
|
2813
2823
|
...at(),
|
|
2814
2824
|
fix: "rename the property, or drop label, group and fieldType to reference it"
|
|
2815
2825
|
});
|
|
@@ -3077,7 +3087,13 @@ function definitionRules(codec, d) {
|
|
|
3077
3087
|
message: `calculationFormula needs fieldType '${CALCULATION}': HubSpot turns the property into a calculation`,
|
|
3078
3088
|
fix: `set fieldType: '${CALCULATION}', or remove calculationFormula`
|
|
3079
3089
|
});
|
|
3080
|
-
if (d.currencyPropertyName
|
|
3090
|
+
if (d.currencyPropertyName === "") rules.push({
|
|
3091
|
+
field: "currencyPropertyName",
|
|
3092
|
+
reads: ["currencyPropertyName"],
|
|
3093
|
+
message: "currencyPropertyName '' is not nothing: HubSpot stores it and then never turns showCurrencySymbol off",
|
|
3094
|
+
fix: "remove currencyPropertyName, or name a currency property"
|
|
3095
|
+
});
|
|
3096
|
+
else if (d.currencyPropertyName !== void 0 && d.showCurrencySymbol !== true) rules.push({
|
|
3081
3097
|
field: "currencyPropertyName",
|
|
3082
3098
|
reads: ["currencyPropertyName", "showCurrencySymbol"],
|
|
3083
3099
|
message: "HubSpot takes currencyPropertyName only with showCurrencySymbol: true",
|
|
@@ -3262,10 +3278,11 @@ function rules(blueprint) {
|
|
|
3262
3278
|
}
|
|
3263
3279
|
return issues;
|
|
3264
3280
|
}
|
|
3265
|
-
/** A group or property name: plain, and never HubSpot
|
|
3281
|
+
/** A group or property name: plain, and never a prefix HubSpot reserves (`hs_`, `a<digits>_`). */
|
|
3266
3282
|
function checkName(report, at, type, name) {
|
|
3283
|
+
const reserved = reservedPrefix(name);
|
|
3267
3284
|
if (!NAME.test(name)) report(at, `${type} name '${name}' is not lowercase letters, digits and underscores starting with a letter`);
|
|
3268
|
-
else if (
|
|
3285
|
+
else if (reserved !== void 0) report(at, `${type} name '${name}' starts with ${reserved}, a prefix HubSpot reserves`);
|
|
3269
3286
|
}
|
|
3270
3287
|
function checkProperty(report, at, object, resource) {
|
|
3271
3288
|
const d = resource.definition;
|
|
@@ -3454,6 +3471,16 @@ const registry = {
|
|
|
3454
3471
|
tag: "read"
|
|
3455
3472
|
} }
|
|
3456
3473
|
},
|
|
3474
|
+
tokenInfo: {
|
|
3475
|
+
family: "oauth.private-apps",
|
|
3476
|
+
version: "v2",
|
|
3477
|
+
status: "ga",
|
|
3478
|
+
paths: { read: {
|
|
3479
|
+
method: "POST",
|
|
3480
|
+
path: "/oauth/v2/private-apps/get/access-token-info",
|
|
3481
|
+
tag: "read"
|
|
3482
|
+
} }
|
|
3483
|
+
},
|
|
3457
3484
|
limits: {
|
|
3458
3485
|
family: "crm.limits",
|
|
3459
3486
|
version: "2026-09",
|
|
@@ -3501,9 +3528,10 @@ const scopeExceptions = {
|
|
|
3501
3528
|
users: "crm.objects.users.read"
|
|
3502
3529
|
};
|
|
3503
3530
|
/**
|
|
3504
|
-
* The write counterpart of each read exception.
|
|
3505
|
-
*
|
|
3506
|
-
*
|
|
3531
|
+
* The write counterpart of each read exception. The scope list of HubSpot's 2026-09 create-property reference names
|
|
3532
|
+
* crm.schemas.commercepayments.write, e-commerce and crm.objects.users.write, and none of the others, so they mirror
|
|
3533
|
+
* the read exceptions until a live write checks them. A key with crm.schemas.companies.write alone creates, updates
|
|
3534
|
+
* and archives company properties and reads nothing (live runs, 2026-10-01).
|
|
3507
3535
|
*/
|
|
3508
3536
|
const writeScopeExceptions = {
|
|
3509
3537
|
commerce_payments: "crm.schemas.commercepayments.write",
|
|
@@ -3606,6 +3634,8 @@ const MILESTONE_3_WRITES = [
|
|
|
3606
3634
|
path: "delete"
|
|
3607
3635
|
}
|
|
3608
3636
|
];
|
|
3637
|
+
/** How many `errors` entries an outcome keeps: a property used in hundreds of places must not balloon it. */
|
|
3638
|
+
const ERRORS_MAX = 50;
|
|
3609
3639
|
var HubSpotApiError = class extends KalupError {
|
|
3610
3640
|
status;
|
|
3611
3641
|
category;
|
|
@@ -3919,14 +3949,16 @@ function outcomeOf(answer, context) {
|
|
|
3919
3949
|
const REJECTED_MESSAGE_MAX = 400;
|
|
3920
3950
|
function rejectedOf(status, body, context) {
|
|
3921
3951
|
const { key, method, path, scope } = context;
|
|
3922
|
-
const { category, correlationId, subCategory } = body;
|
|
3952
|
+
const { category, correlationId, subCategory, context: fields, errors } = body;
|
|
3923
3953
|
return {
|
|
3924
3954
|
kind: "rejected",
|
|
3925
3955
|
status,
|
|
3926
3956
|
...category === void 0 ? {} : { category },
|
|
3927
3957
|
...subCategory === void 0 ? {} : { subCategory },
|
|
3928
3958
|
...correlationId === void 0 ? {} : { correlationId },
|
|
3929
|
-
message: issueOf(status, body, method, path, key, scope, REJECTED_MESSAGE_MAX).message
|
|
3959
|
+
message: issueOf(status, body, method, path, key, scope, REJECTED_MESSAGE_MAX).message,
|
|
3960
|
+
...fields === void 0 ? {} : { context: fields },
|
|
3961
|
+
...errors === void 0 ? {} : { errors }
|
|
3930
3962
|
};
|
|
3931
3963
|
}
|
|
3932
3964
|
function retryAfterMs(headers) {
|
|
@@ -3966,13 +3998,33 @@ function errorBody(text, key) {
|
|
|
3966
3998
|
}
|
|
3967
3999
|
function fieldsOf(body, key) {
|
|
3968
4000
|
if (typeof body !== "object" || body === null) return {};
|
|
3969
|
-
const { category, correlationId, message, policyName, subCategory } = body;
|
|
4001
|
+
const { category, context, correlationId, errors, message, policyName, subCategory } = body;
|
|
4002
|
+
const fields = contextOf(context, key);
|
|
4003
|
+
const details = Array.isArray(errors) ? errors.slice(0, ERRORS_MAX).map((item) => detailOf(item, key)) : void 0;
|
|
3970
4004
|
return {
|
|
3971
4005
|
...typeof category === "string" ? { category: quote(category, key) } : {},
|
|
3972
4006
|
...typeof subCategory === "string" ? { subCategory: quote(subCategory, key) } : {},
|
|
3973
4007
|
...typeof correlationId === "string" ? { correlationId: quote(correlationId, key) } : {},
|
|
3974
4008
|
...typeof message === "string" ? { message } : {},
|
|
3975
|
-
...typeof policyName === "string" ? { policyName } : {}
|
|
4009
|
+
...typeof policyName === "string" ? { policyName } : {},
|
|
4010
|
+
...fields === void 0 ? {} : { context: fields },
|
|
4011
|
+
...details === void 0 ? {} : { errors: details }
|
|
4012
|
+
};
|
|
4013
|
+
}
|
|
4014
|
+
function contextOf(context, key) {
|
|
4015
|
+
if (typeof context !== "object" || context === null || Array.isArray(context)) return;
|
|
4016
|
+
const out = {};
|
|
4017
|
+
for (const [name, value] of Object.entries(context)) if (Array.isArray(value) && value.every((item) => typeof item === "string")) out[quote(name, key)] = value.map((item) => quote(item, key));
|
|
4018
|
+
return out;
|
|
4019
|
+
}
|
|
4020
|
+
function detailOf(item, key) {
|
|
4021
|
+
if (typeof item !== "object" || item === null) return {};
|
|
4022
|
+
const { context, message, subCategory } = item;
|
|
4023
|
+
const fields = contextOf(context, key);
|
|
4024
|
+
return {
|
|
4025
|
+
...typeof subCategory === "string" ? { subCategory: quote(subCategory, key) } : {},
|
|
4026
|
+
...typeof message === "string" ? { message: quote(message, key) } : {},
|
|
4027
|
+
...fields === void 0 ? {} : { context: fields }
|
|
3976
4028
|
};
|
|
3977
4029
|
}
|
|
3978
4030
|
function issueOf(status, body, method, path, key, scope, max) {
|
|
@@ -4013,6 +4065,114 @@ function portalMidnight(now, timeZone) {
|
|
|
4013
4065
|
const offset = Date.UTC(part.year ?? 0, (part.month ?? 1) - 1, part.day, part.hour, part.minute, part.second) - Math.floor(now.getTime() / 1e3) * 1e3;
|
|
4014
4066
|
return new Date(Date.UTC(part.year ?? 0, (part.month ?? 1) - 1, (part.day ?? 1) + 1) - offset).toISOString();
|
|
4015
4067
|
}
|
|
4068
|
+
/**
|
|
4069
|
+
* The objects whose properties the properties API lists by name. A config key outside this set names a custom object
|
|
4070
|
+
* schema. The set is the 2026-09 `crm.schemas.<object>.read` scope list plus the objects that carry their own scopes
|
|
4071
|
+
* (products, leads, goals, marketing events, feedback submissions, users).
|
|
4072
|
+
*/
|
|
4073
|
+
const STANDARD_OBJECTS = /* @__PURE__ */ new Set([
|
|
4074
|
+
"appointments",
|
|
4075
|
+
"calls",
|
|
4076
|
+
"carts",
|
|
4077
|
+
"commerce_payments",
|
|
4078
|
+
"communications",
|
|
4079
|
+
"companies",
|
|
4080
|
+
"contacts",
|
|
4081
|
+
"courses",
|
|
4082
|
+
"deals",
|
|
4083
|
+
"emails",
|
|
4084
|
+
"feedback_submissions",
|
|
4085
|
+
"goals",
|
|
4086
|
+
"invoices",
|
|
4087
|
+
"leads",
|
|
4088
|
+
"line_items",
|
|
4089
|
+
"listings",
|
|
4090
|
+
"marketing_events",
|
|
4091
|
+
"meetings",
|
|
4092
|
+
"notes",
|
|
4093
|
+
"orders",
|
|
4094
|
+
"postal_mail",
|
|
4095
|
+
"products",
|
|
4096
|
+
"projects",
|
|
4097
|
+
"quotes",
|
|
4098
|
+
"services",
|
|
4099
|
+
"subscriptions",
|
|
4100
|
+
"tasks",
|
|
4101
|
+
"tickets",
|
|
4102
|
+
"users"
|
|
4103
|
+
]);
|
|
4104
|
+
/**
|
|
4105
|
+
* The object type ID of each standard object, by config key. Limits Tracking keys its per-object entries by these.
|
|
4106
|
+
* Source: "Object type ID values", retrieved 2026-09-24:
|
|
4107
|
+
* https://developers.hubspot.com/docs/api-reference/latest/crm/understanding-the-crm
|
|
4108
|
+
* A plain record: look a key up with `Object.hasOwn`, so `constructor` finds nothing.
|
|
4109
|
+
*/
|
|
4110
|
+
const STANDARD_OBJECT_TYPE_IDS = {
|
|
4111
|
+
appointments: "0-421",
|
|
4112
|
+
calls: "0-48",
|
|
4113
|
+
carts: "0-142",
|
|
4114
|
+
commerce_payments: "0-101",
|
|
4115
|
+
communications: "0-18",
|
|
4116
|
+
companies: "0-2",
|
|
4117
|
+
contacts: "0-1",
|
|
4118
|
+
courses: "0-410",
|
|
4119
|
+
deals: "0-3",
|
|
4120
|
+
emails: "0-49",
|
|
4121
|
+
feedback_submissions: "0-19",
|
|
4122
|
+
goals: "0-74",
|
|
4123
|
+
invoices: "0-53",
|
|
4124
|
+
leads: "0-136",
|
|
4125
|
+
line_items: "0-8",
|
|
4126
|
+
listings: "0-420",
|
|
4127
|
+
marketing_events: "0-54",
|
|
4128
|
+
meetings: "0-47",
|
|
4129
|
+
notes: "0-46",
|
|
4130
|
+
orders: "0-123",
|
|
4131
|
+
postal_mail: "0-116",
|
|
4132
|
+
products: "0-7",
|
|
4133
|
+
projects: "0-970",
|
|
4134
|
+
quotes: "0-14",
|
|
4135
|
+
services: "0-162",
|
|
4136
|
+
subscriptions: "0-69",
|
|
4137
|
+
tasks: "0-27",
|
|
4138
|
+
tickets: "0-5",
|
|
4139
|
+
users: "0-115"
|
|
4140
|
+
};
|
|
4141
|
+
/** One object's pull scope from its settings under `objects` and the property names its object file defines. */
|
|
4142
|
+
function scopeOf(scope = {}, defined = []) {
|
|
4143
|
+
return {
|
|
4144
|
+
custom: scope.custom ?? true,
|
|
4145
|
+
defined: new Set(defined),
|
|
4146
|
+
exclude: excluder(scope.exclude),
|
|
4147
|
+
include: new Set(scope.include ?? [])
|
|
4148
|
+
};
|
|
4149
|
+
}
|
|
4150
|
+
/** The names of the properties the object files define on `object`, references included. */
|
|
4151
|
+
function definedOn(ir, object) {
|
|
4152
|
+
const prefix = `property:${object}/`;
|
|
4153
|
+
return Object.keys(ir.resources).filter((address) => address.startsWith(prefix)).map((address) => address.slice(prefix.length));
|
|
4154
|
+
}
|
|
4155
|
+
/**
|
|
4156
|
+
* A portal property is in scope when the object files define it or `include` names it, whatever `custom` and
|
|
4157
|
+
* `exclude` say, or when it is custom, `custom` is on and `exclude` does not name it. validate refuses a name both
|
|
4158
|
+
* `include` and `exclude` hold.
|
|
4159
|
+
*/
|
|
4160
|
+
function inScope(scope, property) {
|
|
4161
|
+
const { name } = property;
|
|
4162
|
+
return scope.defined.has(name) || scope.include.has(name) || scope.custom && !property.hubspotDefined && !scope.exclude(name);
|
|
4163
|
+
}
|
|
4164
|
+
/** The `exclude` patterns of one object as a predicate over internal names. */
|
|
4165
|
+
function excluder(patterns = []) {
|
|
4166
|
+
const matchers = patterns.map(addressMatcher);
|
|
4167
|
+
return (name) => matchers.some((matches) => matches(name));
|
|
4168
|
+
}
|
|
4169
|
+
/** The --only glob as a predicate over addresses. `*` matches any run of characters, `/` included. */
|
|
4170
|
+
function addressMatcher(glob) {
|
|
4171
|
+
if (glob === void 0) return () => true;
|
|
4172
|
+
const literals = glob.split("*").map((part) => part.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
|
|
4173
|
+
const pattern = new RegExp(`^${literals.join(".*")}$`);
|
|
4174
|
+
return (address) => pattern.test(address);
|
|
4175
|
+
}
|
|
4016
4176
|
const SETS$1 = /* @__PURE__ */ new Set(["requiredProperties", "searchableProperties"]);
|
|
4017
4177
|
/** Every unit desired owns that observed holds, sorted by unit. */
|
|
4018
4178
|
function classify(base, desired, observed, rules) {
|
|
@@ -4934,7 +5094,7 @@ function normalizeProperties(object, raw, issues, warn = () => true) {
|
|
|
4934
5094
|
for (const p of raw) {
|
|
4935
5095
|
if (p.archived) continue;
|
|
4936
5096
|
const hubspotDefined = Boolean(p.hubspotDefined);
|
|
4937
|
-
const reference = Boolean(p.hubspotDefined || p.calculated && p.fieldType !== "calculation_equation");
|
|
5097
|
+
const reference = Boolean(p.hubspotDefined || p.calculated && p.fieldType !== "calculation_equation" || RESERVED_PREFIX.test(p.name));
|
|
4938
5098
|
const external = p.externalOptions === true || p.referencedObjectType === "OWNER";
|
|
4939
5099
|
const kind = builderOf(p, external);
|
|
4940
5100
|
if (kind === void 0 || !(reference || writable(p, kind, external))) {
|
|
@@ -5011,7 +5171,8 @@ function metaOf(p) {
|
|
|
5011
5171
|
modificationMetadata,
|
|
5012
5172
|
createdAt: text(p.createdAt),
|
|
5013
5173
|
updatedAt: text(p.updatedAt),
|
|
5014
|
-
options: options?.length ? options : void 0
|
|
5174
|
+
options: options?.length ? options : void 0,
|
|
5175
|
+
currencyPropertyName: text(p.currencyPropertyName)
|
|
5015
5176
|
});
|
|
5016
5177
|
}
|
|
5017
5178
|
function definitionOf$1(p, reference, options) {
|
|
@@ -5074,114 +5235,6 @@ function normalizeSchema(schema) {
|
|
|
5074
5235
|
function compact$4(value) {
|
|
5075
5236
|
return Object.fromEntries(Object.entries(value).filter(([, v]) => v !== void 0));
|
|
5076
5237
|
}
|
|
5077
|
-
/**
|
|
5078
|
-
* The objects whose properties the properties API lists by name. A config key outside this set names a custom object
|
|
5079
|
-
* schema. The set is the 2026-09 `crm.schemas.<object>.read` scope list plus the objects that carry their own scopes
|
|
5080
|
-
* (products, leads, goals, marketing events, feedback submissions, users).
|
|
5081
|
-
*/
|
|
5082
|
-
const STANDARD_OBJECTS = /* @__PURE__ */ new Set([
|
|
5083
|
-
"appointments",
|
|
5084
|
-
"calls",
|
|
5085
|
-
"carts",
|
|
5086
|
-
"commerce_payments",
|
|
5087
|
-
"communications",
|
|
5088
|
-
"companies",
|
|
5089
|
-
"contacts",
|
|
5090
|
-
"courses",
|
|
5091
|
-
"deals",
|
|
5092
|
-
"emails",
|
|
5093
|
-
"feedback_submissions",
|
|
5094
|
-
"goals",
|
|
5095
|
-
"invoices",
|
|
5096
|
-
"leads",
|
|
5097
|
-
"line_items",
|
|
5098
|
-
"listings",
|
|
5099
|
-
"marketing_events",
|
|
5100
|
-
"meetings",
|
|
5101
|
-
"notes",
|
|
5102
|
-
"orders",
|
|
5103
|
-
"postal_mail",
|
|
5104
|
-
"products",
|
|
5105
|
-
"projects",
|
|
5106
|
-
"quotes",
|
|
5107
|
-
"services",
|
|
5108
|
-
"subscriptions",
|
|
5109
|
-
"tasks",
|
|
5110
|
-
"tickets",
|
|
5111
|
-
"users"
|
|
5112
|
-
]);
|
|
5113
|
-
/**
|
|
5114
|
-
* The object type ID of each standard object, by config key. Limits Tracking keys its per-object entries by these.
|
|
5115
|
-
* Source: "Object type ID values", retrieved 2026-09-24:
|
|
5116
|
-
* https://developers.hubspot.com/docs/api-reference/latest/crm/understanding-the-crm
|
|
5117
|
-
* A plain record: look a key up with `Object.hasOwn`, so `constructor` finds nothing.
|
|
5118
|
-
*/
|
|
5119
|
-
const STANDARD_OBJECT_TYPE_IDS = {
|
|
5120
|
-
appointments: "0-421",
|
|
5121
|
-
calls: "0-48",
|
|
5122
|
-
carts: "0-142",
|
|
5123
|
-
commerce_payments: "0-101",
|
|
5124
|
-
communications: "0-18",
|
|
5125
|
-
companies: "0-2",
|
|
5126
|
-
contacts: "0-1",
|
|
5127
|
-
courses: "0-410",
|
|
5128
|
-
deals: "0-3",
|
|
5129
|
-
emails: "0-49",
|
|
5130
|
-
feedback_submissions: "0-19",
|
|
5131
|
-
goals: "0-74",
|
|
5132
|
-
invoices: "0-53",
|
|
5133
|
-
leads: "0-136",
|
|
5134
|
-
line_items: "0-8",
|
|
5135
|
-
listings: "0-420",
|
|
5136
|
-
marketing_events: "0-54",
|
|
5137
|
-
meetings: "0-47",
|
|
5138
|
-
notes: "0-46",
|
|
5139
|
-
orders: "0-123",
|
|
5140
|
-
postal_mail: "0-116",
|
|
5141
|
-
products: "0-7",
|
|
5142
|
-
projects: "0-970",
|
|
5143
|
-
quotes: "0-14",
|
|
5144
|
-
services: "0-162",
|
|
5145
|
-
subscriptions: "0-69",
|
|
5146
|
-
tasks: "0-27",
|
|
5147
|
-
tickets: "0-5",
|
|
5148
|
-
users: "0-115"
|
|
5149
|
-
};
|
|
5150
|
-
/** One object's pull scope from its settings under `objects` and the property names its object file defines. */
|
|
5151
|
-
function scopeOf(scope = {}, defined = []) {
|
|
5152
|
-
return {
|
|
5153
|
-
custom: scope.custom ?? true,
|
|
5154
|
-
defined: new Set(defined),
|
|
5155
|
-
exclude: excluder(scope.exclude),
|
|
5156
|
-
include: new Set(scope.include ?? [])
|
|
5157
|
-
};
|
|
5158
|
-
}
|
|
5159
|
-
/** The names of the properties the object files define on `object`, references included. */
|
|
5160
|
-
function definedOn(ir, object) {
|
|
5161
|
-
const prefix = `property:${object}/`;
|
|
5162
|
-
return Object.keys(ir.resources).filter((address) => address.startsWith(prefix)).map((address) => address.slice(prefix.length));
|
|
5163
|
-
}
|
|
5164
|
-
/**
|
|
5165
|
-
* A portal property is in scope when the object files define it or `include` names it, whatever `custom` and
|
|
5166
|
-
* `exclude` say, or when it is custom, `custom` is on and `exclude` does not name it. validate refuses a name both
|
|
5167
|
-
* `include` and `exclude` hold.
|
|
5168
|
-
*/
|
|
5169
|
-
function inScope(scope, property) {
|
|
5170
|
-
const { name } = property;
|
|
5171
|
-
return scope.defined.has(name) || scope.include.has(name) || scope.custom && !property.hubspotDefined && !scope.exclude(name);
|
|
5172
|
-
}
|
|
5173
|
-
/** The `exclude` patterns of one object as a predicate over internal names. */
|
|
5174
|
-
function excluder(patterns = []) {
|
|
5175
|
-
const matchers = patterns.map(addressMatcher);
|
|
5176
|
-
return (name) => matchers.some((matches) => matches(name));
|
|
5177
|
-
}
|
|
5178
|
-
/** The --only glob as a predicate over addresses. `*` matches any run of characters, `/` included. */
|
|
5179
|
-
function addressMatcher(glob) {
|
|
5180
|
-
if (glob === void 0) return () => true;
|
|
5181
|
-
const literals = glob.split("*").map((part) => part.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
|
|
5182
|
-
const pattern = new RegExp(`^${literals.join(".*")}$`);
|
|
5183
|
-
return (address) => pattern.test(address);
|
|
5184
|
-
}
|
|
5185
5238
|
const CONFIG$1$1 = "kalup.config.ts";
|
|
5186
5239
|
/** HubSpot lists only non-sensitive properties unless asked, and takes one sensitivity per request. */
|
|
5187
5240
|
const SENSITIVITIES$1 = [
|
|
@@ -5497,6 +5550,7 @@ function pinWarnings(rows, now = Date.now()) {
|
|
|
5497
5550
|
const out = [];
|
|
5498
5551
|
const seen = /* @__PURE__ */ new Set();
|
|
5499
5552
|
for (const row of rows) {
|
|
5553
|
+
if (row.expires === void 0) continue;
|
|
5500
5554
|
const pin = `${row.family} ${row.version}`;
|
|
5501
5555
|
const expires = /* @__PURE__ */ new Date(`${row.expires}-01T00:00:00Z`);
|
|
5502
5556
|
if (seen.has(pin) || expires.getTime() - now > pinWarningDays * 864e5) continue;
|
|
@@ -5617,23 +5671,23 @@ function fieldOf(unit) {
|
|
|
5617
5671
|
/**
|
|
5618
5672
|
* Why HubSpot cannot take an adopt or update of `kind`, or undefined when it can. A difference in `type` or
|
|
5619
5673
|
* `hasUniqueValue` blocks whatever else the step holds, since the property has to be migrated. Written units must be
|
|
5620
|
-
* writable, a read-only definition blocks writing its fields, and read-only options block writing an option.
|
|
5621
|
-
*
|
|
5622
|
-
*
|
|
5674
|
+
* writable, a read-only definition blocks writing its fields, and read-only options block writing an option. HubSpot
|
|
5675
|
+
* answers 400 to turning showCurrencySymbol off once the property ever had a currencyPropertyName, `''` included, and
|
|
5676
|
+
* nothing clears it (observed 2026-10-01); `meta` carries the value as HubSpot returned it, which config may leave out.
|
|
5623
5677
|
*/
|
|
5624
|
-
function writeBlock(kind, units, written, meta
|
|
5678
|
+
function writeBlock(kind, units, written, meta) {
|
|
5625
5679
|
const fixed = kind === "property" ? units.filter((u) => FIXED.has(u.unit) && u.class !== "converged") : [];
|
|
5626
5680
|
if (fixed.length > 0) return {
|
|
5627
5681
|
short: `${fixed.map((u) => u.unit).join(" and ")} ${fixed.length > 1 ? "differ" : "differs"}`,
|
|
5628
5682
|
detail: fixed.map((u) => `config has ${u.unit} ${JSON.stringify(u.desired)} and the portal ${JSON.stringify(u.observed)}`).join("; "),
|
|
5629
5683
|
fix: `change the builder to match the portal, or migrate: create a new property, copy the values over, point what uses this one at the new one, then run ${bin} rm on this one`
|
|
5630
5684
|
};
|
|
5631
|
-
const currency =
|
|
5685
|
+
const currency = meta?.currencyPropertyName;
|
|
5632
5686
|
const symbolOff = units.some((u) => u.unit === "showCurrencySymbol" && u.desired !== true);
|
|
5633
|
-
if (written.includes("showCurrencySymbol") && symbolOff &&
|
|
5687
|
+
if (written.includes("showCurrencySymbol") && symbolOff && currency !== void 0) return {
|
|
5634
5688
|
short: "currency property set",
|
|
5635
|
-
detail: `HubSpot
|
|
5636
|
-
fix:
|
|
5689
|
+
detail: `HubSpot never turns showCurrencySymbol off once the property had a currencyPropertyName; this one holds ${JSON.stringify(currency)}, and clearing it does not lift the refusal`,
|
|
5690
|
+
fix: `keep showCurrencySymbol: true, or migrate: create a new property under another name, copy the values over, point what uses this one at the new one, then run ${bin} rm on this one`
|
|
5637
5691
|
};
|
|
5638
5692
|
const unwritable = written.filter((unit) => !WRITABLE[kind].has(fieldOf(unit)));
|
|
5639
5693
|
if (unwritable.length > 0) return {
|
|
@@ -5657,9 +5711,10 @@ function writeBlock(kind, units, written, meta, observed) {
|
|
|
5657
5711
|
}
|
|
5658
5712
|
/**
|
|
5659
5713
|
* Why a delete cannot run, or undefined when it can: HubSpot marks the property not archivable, or, for a group,
|
|
5660
|
-
* properties still name it,
|
|
5661
|
-
*
|
|
5662
|
-
*
|
|
5714
|
+
* active properties still name it, apart from those the same plan deletes first. HubSpot refuses to archive a group
|
|
5715
|
+
* that holds an active property and archives one whose properties are all archived (observed 2026-09-29 and
|
|
5716
|
+
* 2026-10-01); an archived group never comes back, and its archived properties can only be restored into another
|
|
5717
|
+
* group, so archived members do not block.
|
|
5663
5718
|
*/
|
|
5664
5719
|
function deleteBlock(meta, members) {
|
|
5665
5720
|
if (meta?.modificationMetadata?.archivable === false) return {
|
|
@@ -5669,12 +5724,11 @@ function deleteBlock(meta, members) {
|
|
|
5669
5724
|
};
|
|
5670
5725
|
if (members === void 0) return;
|
|
5671
5726
|
const active = members.active.filter((name) => !members.deleted.has(name));
|
|
5672
|
-
|
|
5673
|
-
if (active.length === 0 && archived.length === 0) return;
|
|
5727
|
+
if (active.length === 0) return;
|
|
5674
5728
|
return {
|
|
5675
5729
|
short: "group still holds properties",
|
|
5676
|
-
detail: `properties in HubSpot still name this group: ${
|
|
5677
|
-
fix: "move them to another group or delete them first; HubSpot
|
|
5730
|
+
detail: `properties in HubSpot still name this group: ${active.join(", ")}`,
|
|
5731
|
+
fix: "move them to another group or delete them first; HubSpot archives a group only once every property in it is archived"
|
|
5678
5732
|
};
|
|
5679
5733
|
}
|
|
5680
5734
|
function classOf(change, context) {
|
|
@@ -6678,8 +6732,8 @@ function plan$1(input) {
|
|
|
6678
6732
|
}
|
|
6679
6733
|
/**
|
|
6680
6734
|
* What the command reads between the observation and plan: the Limits Tracking readings for the creates it plans, and
|
|
6681
|
-
* the archived properties of each object that exists and gets a property create
|
|
6682
|
-
* HubSpot no longer has
|
|
6735
|
+
* the archived properties of each object that exists and gets a property create or holds a property state owns that
|
|
6736
|
+
* HubSpot no longer has.
|
|
6683
6737
|
*/
|
|
6684
6738
|
function planReads(input) {
|
|
6685
6739
|
const { observation } = input;
|
|
@@ -6691,14 +6745,9 @@ function planReads(input) {
|
|
|
6691
6745
|
}, coverage);
|
|
6692
6746
|
const creates = decided.steps.filter((s) => s.action === "create" && s.risk !== "blocked" && kindOf$3(s.address) === "property").map((s) => s.address);
|
|
6693
6747
|
const gone = decided.gone.filter((g) => kindOf$3(g.address) === "property");
|
|
6694
|
-
const groupDeletes = decided.steps.filter((s) => s.action === "delete" && s.blocked?.reason !== "not-owned" && kindOf$3(s.address) === "group");
|
|
6695
6748
|
const ids = objectTypeIds(observation);
|
|
6696
6749
|
const archived = {};
|
|
6697
|
-
for (const key of [
|
|
6698
|
-
...creates,
|
|
6699
|
-
...gone.map((g) => g.address),
|
|
6700
|
-
...groupDeletes.map((s) => s.address)
|
|
6701
|
-
].map(objectOf)) {
|
|
6750
|
+
for (const key of [...creates, ...gone.map((g) => g.address)].map(objectOf)) {
|
|
6702
6751
|
const objectType = own$2(ids, key) ?? (STANDARD_OBJECTS.has(key) ? key : void 0);
|
|
6703
6752
|
if (objectType !== void 0) archived[key] = objectType;
|
|
6704
6753
|
}
|
|
@@ -7053,7 +7102,7 @@ function present(context, address, resource, owner) {
|
|
|
7053
7102
|
function referenced(context, address) {
|
|
7054
7103
|
const { target } = context.input;
|
|
7055
7104
|
const short = "HubSpot-defined or calculated";
|
|
7056
|
-
return blocked(address, "adopt", "unsupported", short, `${short} in this portal; run ${bin} pull to make it a reference`, `run ${pullCommand(target, address)}`);
|
|
7105
|
+
return blocked(address, "adopt", "unsupported", short, `${short} in this portal, or named with a prefix HubSpot reserves; run ${bin} pull to make it a reference`, `run ${pullCommand(target, address)}`);
|
|
7057
7106
|
}
|
|
7058
7107
|
function settle$1(context, r) {
|
|
7059
7108
|
const { action, address, kind, observed, owner, units } = r;
|
|
@@ -7069,7 +7118,7 @@ function settle$1(context, r) {
|
|
|
7069
7118
|
if (bins.refused) return blocked(address, action, "unsupported", "schema writes not supported", `${NO_SCHEMA_WRITES}, so --take config cannot write its units`, "leave it out of --take");
|
|
7070
7119
|
const meta = context.input.observation.meta?.[address];
|
|
7071
7120
|
reorder(r, changes, meta);
|
|
7072
|
-
const block = writeBlock(kind, units, changes.map((c) => c.unit), meta
|
|
7121
|
+
const block = writeBlock(kind, units, changes.map((c) => c.unit), meta);
|
|
7073
7122
|
if (block) return blocked(address, action, "unsupported", block.short, block.detail, block.fix);
|
|
7074
7123
|
const takeover = takeoverUnits(context, r, changes);
|
|
7075
7124
|
const refused = takeover.size > 0 ? takeoverBlock(context, address, action, [...takeover]) : void 0;
|
|
@@ -7295,16 +7344,10 @@ function takeoverDelete(context, address, deleted) {
|
|
|
7295
7344
|
...notes
|
|
7296
7345
|
});
|
|
7297
7346
|
if (!context.coverage.complete) return noted(blocked(address, "delete", "scope", "read incomplete", `takeover would archive ${name}, and the read of target ${target} was incomplete, so takeover removes nothing there`, INCOMPLETE_FIX));
|
|
7298
|
-
|
|
7299
|
-
|
|
7300
|
-
|
|
7301
|
-
|
|
7302
|
-
members = {
|
|
7303
|
-
active: own$2(observation.members?.[key] ?? {}, name) ?? [],
|
|
7304
|
-
archived: archived.filter((p) => p.groupName === name).map((p) => p.name),
|
|
7305
|
-
deleted: deleted.get(key) ?? /* @__PURE__ */ new Set()
|
|
7306
|
-
};
|
|
7307
|
-
}
|
|
7347
|
+
const members = kindOf$3(address) === "group" ? {
|
|
7348
|
+
active: own$2(observation.members?.[key] ?? {}, name) ?? [],
|
|
7349
|
+
deleted: deleted.get(key) ?? /* @__PURE__ */ new Set()
|
|
7350
|
+
} : void 0;
|
|
7308
7351
|
const block = deleteBlock(observation.meta?.[address], members);
|
|
7309
7352
|
if (block) return noted(blocked(address, "delete", "unsupported", block.short, block.detail, block.fix));
|
|
7310
7353
|
if (!policy.allowDestroy) return noted(blocked(address, "delete", "policy", "deletes not allowed", `takeover archives ${name}, and target ${target} does not allow deletes`, `keep it in config: run ${pullCommand(target, address)}; or leave it unmanaged: add '${name}' to objects.${key}.exclude; or archive it: set allowDestroy: true under targets.${target} in kalup.config.ts`));
|
|
@@ -7391,16 +7434,10 @@ function destroy(context, address, entry, deleted) {
|
|
|
7391
7434
|
const key = objectOf(address);
|
|
7392
7435
|
if (!observed.managed) return blocked(address, "delete", "unsupported", "HubSpot-defined or calculated", "HubSpot-defined or calculated in this portal, so Kalup does not delete it");
|
|
7393
7436
|
const name = portalName$1(context, address);
|
|
7394
|
-
|
|
7395
|
-
|
|
7396
|
-
|
|
7397
|
-
|
|
7398
|
-
members = {
|
|
7399
|
-
active: own$2(input.observation.members?.[key] ?? {}, name) ?? [],
|
|
7400
|
-
archived: archived.filter((p) => p.groupName === name).map((p) => p.name),
|
|
7401
|
-
deleted: deleted.get(key) ?? /* @__PURE__ */ new Set()
|
|
7402
|
-
};
|
|
7403
|
-
}
|
|
7437
|
+
const members = kindOf$3(address) === "group" ? {
|
|
7438
|
+
active: own$2(input.observation.members?.[key] ?? {}, name) ?? [],
|
|
7439
|
+
deleted: deleted.get(key) ?? /* @__PURE__ */ new Set()
|
|
7440
|
+
} : void 0;
|
|
7404
7441
|
const block = deleteBlock(input.observation.meta?.[address], members);
|
|
7405
7442
|
if (block) return blocked(address, "delete", "unsupported", block.short, block.detail, block.fix);
|
|
7406
7443
|
if (!policy.allowDestroy) return blocked(address, "delete", "policy", "deletes not allowed", `target ${input.target} does not allow deletes`, `set allowDestroy: true under targets.${input.target} in kalup.config.ts, or change the tombstone's action to release`);
|
|
@@ -7887,7 +7924,7 @@ function recordObject(out, address, resource) {
|
|
|
7887
7924
|
else out.unsupported.push(address);
|
|
7888
7925
|
}
|
|
7889
7926
|
function needsArchived(step) {
|
|
7890
|
-
return step.action === "create" && kindOf$2(step.address) === "property"
|
|
7927
|
+
return step.action === "create" && kindOf$2(step.address) === "property";
|
|
7891
7928
|
}
|
|
7892
7929
|
async function checkBindings(plan, read, overrides) {
|
|
7893
7930
|
const effects = plan.steps.filter(hasEffect);
|
|
@@ -8466,7 +8503,7 @@ function writeRefusal(step, trusted, observation) {
|
|
|
8466
8503
|
if (unsupported || observed?.managed === false) return unsupported ? "Kalup does not write this kind of property" : "it is HubSpot-defined or calculated";
|
|
8467
8504
|
const units = unitsOf$1(step, trusted, observed);
|
|
8468
8505
|
const written = (step.changes ?? []).map((c) => c.unit);
|
|
8469
|
-
return writeBlock(kindOf$1(step.address), units, written, observation.meta[step.address]
|
|
8506
|
+
return writeBlock(kindOf$1(step.address), units, written, observation.meta[step.address])?.detail;
|
|
8470
8507
|
}
|
|
8471
8508
|
function deleteRefusal(plan, step, { owner, overrides }, observation) {
|
|
8472
8509
|
const takeover = step.labels?.includes("takeover") === true;
|
|
@@ -8483,14 +8520,11 @@ function deleteRefusal(plan, step, { owner, overrides }, observation) {
|
|
|
8483
8520
|
const kept = takeover ? keptByRead(overrides, plan.target.name, step.address, observed, observation.schemaNamed[key] ?? []) : void 0;
|
|
8484
8521
|
return kept === void 0 ? deleteBlock(observation.meta[step.address])?.detail : `takeover never archives it: ${kept}`;
|
|
8485
8522
|
}
|
|
8486
|
-
const archived = observation.archived[key];
|
|
8487
|
-
if (archived === void 0) return `the archived properties of ${key} were not read`;
|
|
8488
8523
|
const name = names.portalName(step.address);
|
|
8489
8524
|
if (takeover && (observation.members[key]?.[name] ?? []).length === 0) return "it held no property when apply read it, and HubSpot marks no group as its own, so takeover never archives an empty group";
|
|
8490
8525
|
const deleted = new Set(runOrder(plan).filter((s) => s.action === "delete" && kindOf$1(s.address) === "property" && objectOf(s.address) === key).map((s) => names.portalName(s.address)));
|
|
8491
8526
|
return deleteBlock(void 0, {
|
|
8492
8527
|
active: observation.members[key]?.[name] ?? [],
|
|
8493
|
-
archived: archived.filter((p) => p.groupName === name).map((p) => p.name),
|
|
8494
8528
|
deleted
|
|
8495
8529
|
})?.detail;
|
|
8496
8530
|
}
|
|
@@ -8673,6 +8707,8 @@ const LINE_MAX$1 = 1e3;
|
|
|
8673
8707
|
const SCOPE = /the scope (\S+)\./;
|
|
8674
8708
|
const TEXT_MAX = 400;
|
|
8675
8709
|
const USES = /used in (\d+) places?/;
|
|
8710
|
+
const USES_SHOWN = 5;
|
|
8711
|
+
const USE_MAX = 60;
|
|
8676
8712
|
/** Thrown by a request made after the signal aborted. */
|
|
8677
8713
|
var Stopped = class extends Error {};
|
|
8678
8714
|
/**
|
|
@@ -9078,13 +9114,32 @@ function rejected(run, step, sent) {
|
|
|
9078
9114
|
};
|
|
9079
9115
|
}
|
|
9080
9116
|
function refusal(run, step, sent, plan, otherwise) {
|
|
9117
|
+
if (sent.status === 403 && sent.message.includes("sensitive-data-property-create")) {
|
|
9118
|
+
const key = objectOf(step.address);
|
|
9119
|
+
const object = STANDARD_OBJECTS.has(key) ? key : "custom";
|
|
9120
|
+
const level = step.desired?.dataSensitivity === "highly_sensitive" ? "highly_sensitive" : "sensitive";
|
|
9121
|
+
return {
|
|
9122
|
+
why: `HubSpot refuses the create because the key lacks the sensitive data scope for ${key}`,
|
|
9123
|
+
fix: `add the scope crm.objects.${object}.${level}.write to the write key, then run ${plan}`
|
|
9124
|
+
};
|
|
9125
|
+
}
|
|
9081
9126
|
const reason = reasonOf(sent);
|
|
9127
|
+
if (reason === "PORTAL_NOT_ENABLED_FOR_SENSITIVE_DATA") return {
|
|
9128
|
+
why: "HubSpot has sensitive data turned off for this portal",
|
|
9129
|
+
fix: `turn it on under Settings > Privacy & Consent > Sensitive data, then run ${plan}`
|
|
9130
|
+
};
|
|
9131
|
+
if (reason === "ONLY_CURRENCY_PROPERTIES_CAN_SPECIFY_CURRENCY") return {
|
|
9132
|
+
why: `HubSpot never turns showCurrencySymbol off once ${portalName(run, step)} had a currencyPropertyName`,
|
|
9133
|
+
fix: `keep showCurrencySymbol: true, or migrate to a new property under another name, then run ${plan}`
|
|
9134
|
+
};
|
|
9082
9135
|
if (reason === "CANNOT_DELETE_PROPERTY_IN_USE") {
|
|
9083
|
-
const count = USES.exec(sent.message)?.[1];
|
|
9136
|
+
const count = sent.context?.usageCount?.[0] ?? USES.exec(sent.message)?.[1];
|
|
9084
9137
|
const counted = count === void 0 ? "" : ` (HubSpot counts ${count} use${count === "1" ? "" : "s"})`;
|
|
9138
|
+
const uses = usesOf(sent);
|
|
9139
|
+
const where = uses.length === 0 ? "" : ` (${uses.join(", ")})`;
|
|
9085
9140
|
return {
|
|
9086
9141
|
why: `HubSpot refuses to archive ${portalName(run, step)} because it is in use${counted}`,
|
|
9087
|
-
fix: `remove those uses in HubSpot first, then run ${plan}`
|
|
9142
|
+
fix: `remove those uses in HubSpot first${where}, then run ${plan}`
|
|
9088
9143
|
};
|
|
9089
9144
|
}
|
|
9090
9145
|
if (reason === "GROUP_WITH_ACTIVE_PROPERTIES") return {
|
|
@@ -9097,6 +9152,13 @@ function refusal(run, step, sent, plan, otherwise) {
|
|
|
9097
9152
|
};
|
|
9098
9153
|
return otherwise;
|
|
9099
9154
|
}
|
|
9155
|
+
function usesOf(sent) {
|
|
9156
|
+
const uses = (sent.errors ?? []).filter((e) => e.subCategory?.endsWith("PROPERTY_USAGE")).map((e) => {
|
|
9157
|
+
return sanitize(`${(e.context?.parentDisplayType?.[0] ?? e.context?.parentType?.[0] ?? "use").toLowerCase().replaceAll("_", " ")} ${e.context?.parentName?.[0] ?? ""}`.trim(), USE_MAX);
|
|
9158
|
+
});
|
|
9159
|
+
const rest = uses.length - USES_SHOWN;
|
|
9160
|
+
return rest > 0 ? [...uses.slice(0, USES_SHOWN), `and ${rest} more`] : uses;
|
|
9161
|
+
}
|
|
9100
9162
|
function reasonOf(sent) {
|
|
9101
9163
|
return sent.subCategory?.split(".").at(-1);
|
|
9102
9164
|
}
|
|
@@ -12507,6 +12569,28 @@ function pick$4(d, fields) {
|
|
|
12507
12569
|
function own(record, key) {
|
|
12508
12570
|
return Object.hasOwn(record, key) ? record[key] : void 0;
|
|
12509
12571
|
}
|
|
12572
|
+
/** The key's scopes, or undefined when HubSpot refused the request, gave no answer, or answered without a scope list. */
|
|
12573
|
+
async function readTokenInfo(http, key) {
|
|
12574
|
+
let answer;
|
|
12575
|
+
try {
|
|
12576
|
+
answer = await http.request({
|
|
12577
|
+
type: "tokenInfo",
|
|
12578
|
+
path: "read",
|
|
12579
|
+
body: { tokenKey: key }
|
|
12580
|
+
});
|
|
12581
|
+
} catch (error) {
|
|
12582
|
+
if (error instanceof KalupError) return;
|
|
12583
|
+
throw error;
|
|
12584
|
+
}
|
|
12585
|
+
const scopes = typeof answer === "object" && answer !== null ? answer.scopes : void 0;
|
|
12586
|
+
if (!(Array.isArray(scopes) && scopes.every((scope) => typeof scope === "string"))) return;
|
|
12587
|
+
return { scopes: scopes.map((scope) => sanitize(scope)) };
|
|
12588
|
+
}
|
|
12589
|
+
/** Whether `held` names `scope`, as is or with a version suffix HubSpot adds (`.sensitive.write.v2`). */
|
|
12590
|
+
function holdsScope(held, scope) {
|
|
12591
|
+
return held.some((name) => name === scope || name.replace(VERSION_SUFFIX, "") === scope);
|
|
12592
|
+
}
|
|
12593
|
+
const VERSION_SUFFIX = /\.v\d+$/;
|
|
12510
12594
|
/**
|
|
12511
12595
|
* The target for `requested`, the name a command was given, or the reason there is none. An unknown requested name
|
|
12512
12596
|
* never falls back to the default, and the first of several targets is never chosen. Choices are in declaration order.
|
|
@@ -12643,7 +12727,7 @@ function projectLayout(root) {
|
|
|
12643
12727
|
code: "E_DIR_AMBIGUOUS",
|
|
12644
12728
|
message: `both ${DEFAULT_DIR}/ and ${LEGACY_DIR}/ hold .ts files, and ${configFile} does not say which one holds the object files`,
|
|
12645
12729
|
file: configFile,
|
|
12646
|
-
fix: `add dir: '${LEGACY_DIR}' to ${configFile} to keep the
|
|
12730
|
+
fix: `add dir: '${LEGACY_DIR}' to ${configFile} to keep the old folder, or dir: '${DEFAULT_DIR}' when the object files are there`
|
|
12647
12731
|
}, exitCodes.invalid);
|
|
12648
12732
|
}
|
|
12649
12733
|
function holdsTs(dir) {
|
|
@@ -16361,19 +16445,23 @@ async function status(ctx) {
|
|
|
16361
16445
|
});
|
|
16362
16446
|
};
|
|
16363
16447
|
const names = ctx.flags.target === void 0 ? Object.keys(loaded.config.targets) : [ctx.flags.target];
|
|
16448
|
+
const recommended = {
|
|
16449
|
+
scope: limitScope(Object.keys(loaded.config.objects)),
|
|
16450
|
+
neededFor: ["the property limit check in plan"]
|
|
16451
|
+
};
|
|
16452
|
+
const writeScopes = scopeLines(Object.keys(loaded.config.objects), "write");
|
|
16453
|
+
const wanted = {
|
|
16454
|
+
recommended,
|
|
16455
|
+
writeScopes
|
|
16456
|
+
};
|
|
16364
16457
|
const targets = [];
|
|
16365
|
-
for (const name of names) targets.push(await checkTarget(root, name, loaded, found, warn));
|
|
16458
|
+
for (const name of names) targets.push(await checkTarget(root, name, loaded, wanted, found, warn));
|
|
16366
16459
|
found.push(...pinWarnings(Object.values(registry)));
|
|
16367
16460
|
const counts = {
|
|
16368
16461
|
objects: Object.keys(loaded.config.objects).length,
|
|
16369
16462
|
properties: count(loaded.ir, "property"),
|
|
16370
16463
|
groups: count(loaded.ir, "group")
|
|
16371
16464
|
};
|
|
16372
|
-
const recommended = {
|
|
16373
|
-
scope: limitScope(Object.keys(loaded.config.objects)),
|
|
16374
|
-
neededFor: ["the property limit check in plan"]
|
|
16375
|
-
};
|
|
16376
|
-
const writeScopes = scopeLines(Object.keys(loaded.config.objects), "write");
|
|
16377
16465
|
const exitCode = exitCodeOf(targets);
|
|
16378
16466
|
const lines = [
|
|
16379
16467
|
`${bin} ${version}`,
|
|
@@ -16396,7 +16484,7 @@ async function status(ctx) {
|
|
|
16396
16484
|
text: `${lines.join("\n")}\n`
|
|
16397
16485
|
};
|
|
16398
16486
|
}
|
|
16399
|
-
async function checkTarget(root, name, loaded, issues, warn) {
|
|
16487
|
+
async function checkTarget(root, name, loaded, wanted, issues, warn) {
|
|
16400
16488
|
const target = loaded.config.targets[name] ?? {};
|
|
16401
16489
|
const keyVariable = target.credentials?.read.env ?? "HUBSPOT_SERVICE_KEY";
|
|
16402
16490
|
const writeVariable = target.credentials?.write?.env ?? keyVariable;
|
|
@@ -16425,9 +16513,11 @@ async function checkTarget(root, name, loaded, issues, warn) {
|
|
|
16425
16513
|
write
|
|
16426
16514
|
};
|
|
16427
16515
|
let http;
|
|
16516
|
+
let key;
|
|
16428
16517
|
try {
|
|
16518
|
+
({key} = resolveReadKey(target, root));
|
|
16429
16519
|
http = createHttp({
|
|
16430
|
-
key
|
|
16520
|
+
key,
|
|
16431
16521
|
warn
|
|
16432
16522
|
});
|
|
16433
16523
|
} catch (error) {
|
|
@@ -16445,7 +16535,35 @@ async function checkTarget(root, name, loaded, issues, warn) {
|
|
|
16445
16535
|
}
|
|
16446
16536
|
out.protected = policyOf(target, out.account.accountType).protected;
|
|
16447
16537
|
out.protectedBy = target.protected === void 0 ? "default" : "config";
|
|
16538
|
+
let info;
|
|
16539
|
+
try {
|
|
16540
|
+
info = await readTokenInfo(http, key);
|
|
16541
|
+
} catch (error) {
|
|
16542
|
+
return fail(out, "unreachable", error, issues);
|
|
16543
|
+
}
|
|
16448
16544
|
for (const probe of probes(loaded)) out.scopes.push(await checkScope(http, probe, issues));
|
|
16545
|
+
if (info !== void 0) out.keyScopes = keyScopesOf(info.scopes, out, wanted, issues);
|
|
16546
|
+
return out;
|
|
16547
|
+
}
|
|
16548
|
+
function keyScopesOf(held, t, wanted, issues) {
|
|
16549
|
+
const { recommended, writeScopes } = wanted;
|
|
16550
|
+
const out = {
|
|
16551
|
+
held,
|
|
16552
|
+
recommended: {
|
|
16553
|
+
...recommended,
|
|
16554
|
+
ok: holdsScope(held, recommended.scope)
|
|
16555
|
+
}
|
|
16556
|
+
};
|
|
16557
|
+
if (t.write.separate) return out;
|
|
16558
|
+
out.write = writeScopes.map((line) => ({
|
|
16559
|
+
...line,
|
|
16560
|
+
ok: holdsScope(held, line.scope)
|
|
16561
|
+
}));
|
|
16562
|
+
for (const missing of out.write.filter((s) => !s.ok)) issues.push({
|
|
16563
|
+
code: "W_WRITE_SCOPE",
|
|
16564
|
+
message: `the key in ${t.keyVariable} does not hold ${missing.scope}, which apply needs for ${missing.neededFor.join(", ")}`,
|
|
16565
|
+
fix: `add the scope ${missing.scope} to the key`
|
|
16566
|
+
});
|
|
16449
16567
|
return out;
|
|
16450
16568
|
}
|
|
16451
16569
|
async function checkScope(http, probe, issues) {
|
|
@@ -16570,18 +16688,21 @@ function describe(root, t, recommended, writeScopes) {
|
|
|
16570
16688
|
const a = t.account;
|
|
16571
16689
|
const scopes = t.scopes.map(scopeText);
|
|
16572
16690
|
const why = t.protectedBy === "default" ? ` (${a?.accountType} account, default)` : "";
|
|
16691
|
+
const limit = t.keyScopes === void 0 ? ", not checked" : checkedText(t.keyScopes.recommended.ok);
|
|
16573
16692
|
return [
|
|
16574
16693
|
`${head}: portal ${t.portalId} matches, ${a?.accountType}, ${a?.uiDomain}, ${a?.timeZone}, protected: ${t.protected ? "yes" : "no"}${why}`,
|
|
16575
16694
|
` Scopes: ${scopes.length > 0 ? scopes.join(", ") : "none needed"}`,
|
|
16576
|
-
` Also recommended: ${recommended}
|
|
16695
|
+
` Also recommended: ${recommended}${limit} (the property limit check in plan)`,
|
|
16577
16696
|
writeLine(t, writeScopes),
|
|
16578
16697
|
stateLine(root, t.state)
|
|
16579
16698
|
];
|
|
16580
16699
|
}
|
|
16581
16700
|
function writeLine(t, writeScopes) {
|
|
16582
|
-
const
|
|
16701
|
+
const checked = t.keyScopes?.write;
|
|
16702
|
+
const scopes = checked === void 0 ? writeScopes.map((s) => s.scope).join(", ") : checked.map((s) => `${s.scope} ${s.ok ? "ok" : "missing"}`).join(", ");
|
|
16583
16703
|
const needs = t.write.separate ? `, which needs the read scopes and ${scopes}` : `, which also needs ${scopes}`;
|
|
16584
|
-
|
|
16704
|
+
const tail = checked === void 0 ? ", not checked" : "";
|
|
16705
|
+
return ` Write: apply uses ${t.write.keyVariable}${scopes === "" ? "" : `${needs}${tail}`}`;
|
|
16585
16706
|
}
|
|
16586
16707
|
function stateLine(root, state) {
|
|
16587
16708
|
const rel = relative(root, state.path);
|
|
@@ -16594,6 +16715,9 @@ function stateLine(root, state) {
|
|
|
16594
16715
|
else if (last) applied = `plan ${sanitize(last.planId)} at ${sanitize(last.at)}, ${last.outcome}`;
|
|
16595
16716
|
return ` State: ${path}, lineage ${state.lineage}, serial ${state.serial}. Last apply: ${applied}`;
|
|
16596
16717
|
}
|
|
16718
|
+
function checkedText(ok) {
|
|
16719
|
+
return ok ? " ok" : " missing";
|
|
16720
|
+
}
|
|
16597
16721
|
function scopeText(s) {
|
|
16598
16722
|
if (s.ok) return `${s.scope} ok`;
|
|
16599
16723
|
return s.error ? `${s.scope} failed (${s.error})` : `${s.scope} missing (needed for ${s.neededFor.join(", ")})`;
|
|
@@ -16868,7 +16992,7 @@ var FmtCommand = class extends KalupCommand {
|
|
|
16868
16992
|
static flags = {
|
|
16869
16993
|
check: Flags.boolean({ summary: "Report what would change, write nothing, and exit 2 when a file would change." }),
|
|
16870
16994
|
"exit-code": Flags.boolean({
|
|
16871
|
-
summary: "Accepted for
|
|
16995
|
+
summary: "Accepted for older scripts: --check exits 2 on changes already.",
|
|
16872
16996
|
hidden: true
|
|
16873
16997
|
})
|
|
16874
16998
|
};
|
package/dist/commands.d.mts
CHANGED
package/dist/commands.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { _ as TargetRebindCommand, a as CompareCommand, c as InitCommand, d as PlanCommand, f as PullCommand, g as StatusCommand, h as StateRebuildCommand, i as COMMANDS, l as IrCommand, m as SnapshotCommand, n as ApplyCommand, o as DocsCommand, p as RmCommand, r as BlueprintUpgradeCommand, s as FmtCommand, t as AddCommand, u as KalupCommand, v as ValidateCommand } from "./commands-
|
|
1
|
+
import { _ as TargetRebindCommand, a as CompareCommand, c as InitCommand, d as PlanCommand, f as PullCommand, g as StatusCommand, h as StateRebuildCommand, i as COMMANDS, l as IrCommand, m as SnapshotCommand, n as ApplyCommand, o as DocsCommand, p as RmCommand, r as BlueprintUpgradeCommand, s as FmtCommand, t as AddCommand, u as KalupCommand, v as ValidateCommand } from "./commands-T4j_GVBD.mjs";
|
|
2
2
|
export { AddCommand, ApplyCommand, BlueprintUpgradeCommand, COMMANDS, CompareCommand, DocsCommand, FmtCommand, InitCommand, IrCommand, KalupCommand, PlanCommand, PullCommand, RmCommand, SnapshotCommand, StateRebuildCommand, StatusCommand, TargetRebindCommand, ValidateCommand };
|
|
@@ -1237,6 +1237,16 @@ declare const issues: {
|
|
|
1237
1237
|
output: string[];
|
|
1238
1238
|
};
|
|
1239
1239
|
};
|
|
1240
|
+
W_WRITE_SCOPE: {
|
|
1241
|
+
exit: string;
|
|
1242
|
+
title: string;
|
|
1243
|
+
summary: string;
|
|
1244
|
+
when: string[];
|
|
1245
|
+
fix: string[];
|
|
1246
|
+
example: {
|
|
1247
|
+
output: string[];
|
|
1248
|
+
};
|
|
1249
|
+
};
|
|
1240
1250
|
};
|
|
1241
1251
|
type IssueCode = keyof typeof issues;
|
|
1242
1252
|
/** One entry of a command's issues[], as the envelope contract defines it. */
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { A as sanitize, C as usageError, D as disclaimer, E as bin, O as escapeJson, S as versionText, T as KalupError, b as formats, k as exitCodes, u as KalupCommand, w as IssueError, x as version, y as PATH_MAX } from "./commands-
|
|
1
|
+
import { A as sanitize, C as usageError, D as disclaimer, E as bin, O as escapeJson, S as versionText, T as KalupError, b as formats, k as exitCodes, u as KalupCommand, w as IssueError, x as version, y as PATH_MAX } from "./commands-T4j_GVBD.mjs";
|
|
2
2
|
import { existsSync, readFileSync } from "node:fs";
|
|
3
3
|
import { dirname, join } from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
package/dist/host.d.mts
CHANGED
package/dist/host.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { n as isInteractive, r as run, t as execute } from "./host-
|
|
1
|
+
import { n as isInteractive, r as run, t as execute } from "./host-CDDeLV5T.mjs";
|
|
2
2
|
export { execute, isInteractive, run };
|
package/dist/index.mjs
CHANGED
package/docs/apply.md
CHANGED
|
@@ -61,4 +61,4 @@ There is no resume and no rollback. After a run that did not finish, run `kalup
|
|
|
61
61
|
|
|
62
62
|
## Limits
|
|
63
63
|
|
|
64
|
-
A read and the write after it are not atomic: an edit in HubSpot between the two is overwritten for that field. The lock keeps apart one user's commands on one machine only; in CI, one workflow per portal applies, in a concurrency group. A delete checks no use first; HubSpot
|
|
64
|
+
A read and the write after it are not atomic: an edit in HubSpot between the two is overwritten for that field. The lock keeps apart one user's commands on one machine only; in CI, one workflow per portal applies, in a concurrency group. A delete checks no use first; HubSpot refuses to archive a property a workflow, list, form or calculation uses, and apply names each use.
|
package/docs/config.md
CHANGED
|
@@ -56,7 +56,7 @@ A definition with `label`, `group` and `fieldType` is managed: the fields presen
|
|
|
56
56
|
- On `p.string`, `p.stringArray`, `p.json` and `p.phoneNumber`: `textDisplayHint` (`unformatted_single_line`, `multi_line`, `email`, `phone_number`, `domain_name`, `ip_address`, `physical_address`, `postal_code`). HubSpot takes no value that removes a hint.
|
|
57
57
|
- `calculationFormula` with `fieldType: 'calculation_equation'`, in HubSpot's formula syntax. HubSpot stores its own spelling (`a+1` as `a + 1`); write it as pull does, or plan notes the difference. A formula change is risky.
|
|
58
58
|
|
|
59
|
-
A field another builder or field rules out is `E_DEFINITION_FIELD`. HubSpot ignores `dateDisplayHint`, so it is no field. `group` must name a group declared under `groups` for the same object, in any export or file (`E_UNKNOWN_GROUP`). A managed internal name starting with `hs_` is `E_HS_PREFIX
|
|
59
|
+
A field another builder or field rules out is `E_DEFINITION_FIELD`. HubSpot ignores `dateDisplayHint`, so it is no field. `group` must name a group declared under `groups` for the same object, in any export or file (`E_UNKNOWN_GROUP`). A managed internal name starting with `hs_` or `a<digits>_` is `E_HS_PREFIX`: HubSpot reserves `hs_` for its own properties and `a<appId>_` for an integration's, and refuses a create with either. A reference may carry the prefix, and pull writes such properties as references.
|
|
60
60
|
|
|
61
61
|
No definition makes a reference: never created, changed or removed. `p.enum` or `p.multiEnum` with `options` and nothing else is a reference with typed options, which pull refreshes.
|
|
62
62
|
|
|
@@ -4,7 +4,7 @@ A blueprint is not a valid `blueprint/1` document. Exit 1. Nothing was written.
|
|
|
4
4
|
|
|
5
5
|
## When
|
|
6
6
|
|
|
7
|
-
`kalup add` and `kalup blueprint upgrade` parse the source as JSON, never as code. Another `blueprintVersion` is refused first. Then they check `blueprint-1.schema.json` and the rules the schema cannot state: addresses of the form `group:<object>/<name>` or `property:<object>/<name>` that match their type, plain names that never start with `hs_` (a group `$ref` names a plain group too), unique option values, aliases that name an option, and a codec that fits the HubSpot type and field type. Text that is not JSON or UTF-8 is refused, and so are names a prefix makes invalid. Each issue names its path; quoted text is sanitized.
|
|
7
|
+
`kalup add` and `kalup blueprint upgrade` parse the source as JSON, never as code. Another `blueprintVersion` is refused first. Then they check `blueprint-1.schema.json` and the rules the schema cannot state: addresses of the form `group:<object>/<name>` or `property:<object>/<name>` that match their type, plain names that never start with a prefix HubSpot reserves, `hs_` or `a<digits>_` (a group `$ref` names a plain group too), unique option values, aliases that name an option, and a codec that fits the HubSpot type and field type. Text that is not JSON or UTF-8 is refused, and so are names a prefix makes invalid. Each issue names its path; quoted text is sanitized.
|
|
8
8
|
|
|
9
9
|
## Fix
|
|
10
10
|
|
|
@@ -13,5 +13,5 @@ A blueprint is third-party data: ask its author for a version that passes, `blue
|
|
|
13
13
|
## Example
|
|
14
14
|
|
|
15
15
|
```
|
|
16
|
-
E_BLUEPRINT_SCHEMA: property name 'hs_renewal_flag' starts with hs_,
|
|
16
|
+
E_BLUEPRINT_SCHEMA: property name 'hs_renewal_flag' starts with hs_, a prefix HubSpot reserves (fix: a blueprint is third-party data: ask its author for a version that passes, or fix your own copy of the file) (docs: errors/E_BLUEPRINT_SCHEMA.md)
|
|
17
17
|
```
|
|
@@ -8,7 +8,7 @@ A property definition states a field HubSpot would refuse or misread for this pr
|
|
|
8
8
|
|
|
9
9
|
- `numberDisplayHint`, `showCurrencySymbol` and `currencyPropertyName` belong to `p.number`, and `textDisplayHint` to `p.string`, `p.stringArray`, `p.json` and `p.phoneNumber`. HubSpot stores them on any property but shows them only on those.
|
|
10
10
|
- `calculationFormula` needs `fieldType: 'calculation_equation'`. Sent with another field type, HubSpot turns the property into a calculation.
|
|
11
|
-
- `currencyPropertyName` needs `showCurrencySymbol: true`. HubSpot refuses it otherwise (`ONLY_CURRENCY_PROPERTIES_CAN_SPECIFY_CURRENCY`).
|
|
11
|
+
- `currencyPropertyName` needs `showCurrencySymbol: true`. HubSpot refuses it otherwise (`ONLY_CURRENCY_PROPERTIES_CAN_SPECIFY_CURRENCY`), and an empty `''` is refused by Kalup: HubSpot stores it as a value and then never turns the symbol off again (live runs, 2026-10-01).
|
|
12
12
|
- `displayOrder` is an integer from -1 up.
|
|
13
13
|
- `p.owner` takes no `options`: HubSpot fills them with the account's users and refuses a create that sends any.
|
|
14
14
|
|
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
# E_DIR_AMBIGUOUS
|
|
2
2
|
|
|
3
|
-
Both `hubspot/` and the
|
|
3
|
+
Both `hubspot/` and the old default folder `kalup/` hold .ts files, and `kalup.config.ts` does not say which one holds the object files. Exit 3. Nothing was read or written.
|
|
4
4
|
|
|
5
5
|
## When
|
|
6
6
|
|
|
7
|
-
Without `dir` in `kalup.config.ts`, Kalup reads `hubspot/`, or
|
|
7
|
+
Without `dir` in `kalup.config.ts`, Kalup reads `hubspot/`, or an older project's `kalup/` while `hubspot/` holds no .ts file (`W_LEGACY_DIR`). When both hold .ts files, such as a half-done move or a HubSpot developer project in `hubspot/`, Kalup does not guess: reading the wrong folder would make everything in the other look removed from config.
|
|
8
8
|
|
|
9
9
|
## Fix
|
|
10
10
|
|
|
11
|
-
Add `dir: 'kalup'` to `kalup.config.ts` to keep the
|
|
11
|
+
Add `dir: 'kalup'` to `kalup.config.ts` to keep the old folder, or `dir: 'hubspot'` when the object files are there. Then move or remove the other folder's copy of the object files.
|
|
12
12
|
|
|
13
13
|
## Example
|
|
14
14
|
|
|
15
15
|
```
|
|
16
|
-
kalup.config.ts: E_DIR_AMBIGUOUS: both hubspot/ and kalup/ hold .ts files, and kalup.config.ts does not say which one holds the object files (fix: add dir: 'kalup' to kalup.config.ts to keep the
|
|
16
|
+
kalup.config.ts: E_DIR_AMBIGUOUS: both hubspot/ and kalup/ hold .ts files, and kalup.config.ts does not say which one holds the object files (fix: add dir: 'kalup' to kalup.config.ts to keep the old folder, or dir: 'hubspot' when the object files are there) (docs: errors/E_DIR_AMBIGUOUS.md)
|
|
17
17
|
```
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
# E_HS_PREFIX
|
|
2
2
|
|
|
3
|
-
A managed property's internal name starts with `hs_`. Exit 3.
|
|
3
|
+
A managed property's internal name starts with `hs_` or `a<digits>_`. Exit 3.
|
|
4
4
|
|
|
5
5
|
## When
|
|
6
6
|
|
|
7
|
-
HubSpot
|
|
7
|
+
HubSpot reserves `hs_` for its own properties and `a<appId>_` for an integration's, and refuses a create with either (400, live runs 2026-10-01). Kalup never claims those prefixes for a property it would own; `pull` writes such a property as a reference. A reference (no definition) may carry the prefix.
|
|
8
8
|
|
|
9
9
|
## Fix
|
|
10
10
|
|
|
@@ -17,5 +17,5 @@ plotCount: p.number('hs_plot_count', { label: 'Plot count', group: 'orchard', fi
|
|
|
17
17
|
```
|
|
18
18
|
|
|
19
19
|
```
|
|
20
|
-
hubspot/objects/companies.ts:9: E_HS_PREFIX: 'hs_plot_count' starts with hs_,
|
|
20
|
+
hubspot/objects/companies.ts:9: E_HS_PREFIX: 'hs_plot_count' starts with hs_, a prefix HubSpot reserves (hs_ for its own properties, a<digits>_ for an integration's) (fix: rename the property, or drop label, group and fieldType to reference it) (docs: errors/E_HS_PREFIX.md)
|
|
21
21
|
```
|
package/docs/errors/E_HTTP.md
CHANGED
|
@@ -4,7 +4,7 @@ HubSpot returned an error Kalup has no other code for. Exit 1.
|
|
|
4
4
|
|
|
5
5
|
## When
|
|
6
6
|
|
|
7
|
-
A 400, a 404, a 5xx that three retries did not clear, or a success whose body is not JSON (often a proxy's HTML page). The issue holds the status, the method, the path and HubSpot's message when it sent one. In `apply`, a refusal whose reason HubSpot names and Kalup knows says it in plain words: a property in use, a group that still holds properties,
|
|
7
|
+
A 400, a 404, a 5xx that three retries did not clear, or a success whose body is not JSON (often a proxy's HTML page). The issue holds the status, the method, the path and HubSpot's message when it sent one. In `apply`, a refusal whose reason HubSpot names and Kalup knows says it in plain words: a property in use (each workflow, list, form or calculation named), a group that still holds active properties, a property name that exists, a currency symbol HubSpot never turns off again, or a sensitive property on a portal with sensitive data turned off.
|
|
8
8
|
|
|
9
9
|
## Fix
|
|
10
10
|
|
|
@@ -6,7 +6,7 @@ The files `pull` merged would not load or validate, so it wrote nothing. Exit 3,
|
|
|
6
6
|
|
|
7
7
|
Pull merges the portal into the object files, then loads and validates the whole project as it would write it, before saving anything. The issues after this one are what `validate` would report, with the file and line in the merged text, not the file on disk.
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
An example: a new property whose internal name another key of the same object already uses (`E_DUPLICATE_KEY`).
|
|
10
10
|
|
|
11
11
|
## Fix
|
|
12
12
|
|
|
@@ -16,5 +16,5 @@ Change the portal or the file so the two agree, then pull again. To pull everyth
|
|
|
16
16
|
|
|
17
17
|
```
|
|
18
18
|
E_PULL_INVALID: the pulled project would not validate; nothing was written (fix: the issues that follow point at the files as pull would write them: change the portal or the file so they agree, or leave the resource out with --only) (docs: errors/E_PULL_INVALID.md)
|
|
19
|
-
hubspot/objects/companies.ts:20:
|
|
19
|
+
hubspot/objects/companies.ts:20: E_DUPLICATE_KEY: internal name 'plot_count' is used by two keys of Company: 'plotCount' and 'plotTotal' (fix: remove or rename one of the two entries) (docs: errors/E_DUPLICATE_KEY.md)
|
|
20
20
|
```
|
|
@@ -4,7 +4,7 @@ A warning from every command that reads the project: the object files are in `ka
|
|
|
4
4
|
|
|
5
5
|
## When
|
|
6
6
|
|
|
7
|
-
Kalup
|
|
7
|
+
Kalup keeps the object files in the folder `dir` in `kalup.config.ts` names, `hubspot/` by default. When `dir` is not set, `kalup/` holds .ts files and `hubspot/` holds none, Kalup keeps reading and writing `kalup/` and warns once per command. When both hold .ts files it stops with `E_DIR_AMBIGUOUS` instead.
|
|
8
8
|
|
|
9
9
|
## Fix
|
|
10
10
|
|
|
@@ -13,5 +13,5 @@ Add `dir: 'kalup'` to `kalup.config.ts` to keep the folder, or move `kalup/` to
|
|
|
13
13
|
## Example
|
|
14
14
|
|
|
15
15
|
```
|
|
16
|
-
kalup.config.ts: W_LEGACY_DIR: the object files are in kalup/, the
|
|
16
|
+
kalup.config.ts: W_LEGACY_DIR: the object files are in kalup/, the old default folder; the default is now hubspot/ (fix: add dir: 'kalup' to kalup.config.ts, or move kalup/ to hubspot/) (docs: errors/W_LEGACY_DIR.md)
|
|
17
17
|
```
|
|
@@ -4,11 +4,11 @@ A warning from `plan`, and from `apply` without a plan file: the plan creates pr
|
|
|
4
4
|
|
|
5
5
|
## When
|
|
6
6
|
|
|
7
|
-
Before it plans a property create, `plan` reads HubSpot's Limits Tracking API for the custom property limit (W_LIMIT_HEADROOM).
|
|
7
|
+
Before it plans a property create, `plan` reads HubSpot's Limits Tracking API for the custom property limit (W_LIMIT_HEADROOM). That read answers 403 to a key with `crm.schemas.*` scopes only, and 200 once the key holds one `crm.objects.<object>.read` scope of any object (live runs, 2026-09-29 and 2026-10-01). The message gives HubSpot's status, or the issue code for another error or a 200 without a limit and a usage (`E_HTTP`). A create past the limit then fails in `apply` instead of being blocked in the plan.
|
|
8
8
|
|
|
9
9
|
## Fix
|
|
10
10
|
|
|
11
|
-
Add a `crm.objects.<object>.read` scope to the key, such as `crm.objects.companies.read` (Development > Keys > Service keys); it also lets the key read that object's records, which Kalup never requests.
|
|
11
|
+
Add a `crm.objects.<object>.read` scope to the key, such as `crm.objects.companies.read` (Development > Keys > Service keys); it also lets the key read that object's records, which Kalup never requests. One such scope, of any object, is enough for every Limits Tracking reading.
|
|
12
12
|
|
|
13
13
|
## Example
|
|
14
14
|
|
|
@@ -8,7 +8,7 @@ From `status`: HubSpot sent no rate-limit headers, so Kalup sends at most 8 requ
|
|
|
8
8
|
|
|
9
9
|
From `plan`: HubSpot sent no daily figure, or one that is not a whole number of requests (empty, fractional, negative), so `budget.dailyRemaining` is `null` and the plan cannot weigh its calls against the daily limit. From `apply`: the same, so it cannot refuse a run that would use more than half of what is left (`E_BUDGET`).
|
|
10
10
|
|
|
11
|
-
A service key's answers
|
|
11
|
+
A service key's answers carry the daily headers (live runs, 2026-09-29 and 2026-10-01), so this warning is not expected with one; the fallback stays for an answer without them.
|
|
12
12
|
|
|
13
13
|
## Fix
|
|
14
14
|
|
|
@@ -4,7 +4,7 @@ A warning from `pull`, `plan`, `snapshot` or `compare`: HubSpot sent no rate-lim
|
|
|
4
4
|
|
|
5
5
|
## When
|
|
6
6
|
|
|
7
|
-
Kalup paces requests from HubSpot's rate-limit headers. Without them it sends at most 8 requests per second. A service key's answers
|
|
7
|
+
Kalup paces requests from HubSpot's rate-limit headers. Without them it sends at most 8 requests per second. A service key's answers carry them (live runs, 2026-09-29 and 2026-10-01), so this warning is not expected with one. `status` reports the same thing as W_RATE_HEADERS.
|
|
8
8
|
|
|
9
9
|
## Fix
|
|
10
10
|
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# W_WRITE_SCOPE
|
|
2
|
+
|
|
3
|
+
A warning from `status`: HubSpot's token introspection lists the read key's scopes, and a write scope apply needs is not among them. Exit stays 0.
|
|
4
|
+
|
|
5
|
+
## When
|
|
6
|
+
|
|
7
|
+
`status` reads the scopes a service key holds through HubSpot's token introspection (the key goes in the request body, as HubSpot requires) and checks the write scopes `init` lists against them by name, when apply writes with the same key. A separate write key is never resolved or sent, so its scopes stay unchecked and the line says so. When introspection answers nothing, status checks no write scope.
|
|
8
|
+
|
|
9
|
+
## Fix
|
|
10
|
+
|
|
11
|
+
Add the scope the message names to the key (Development > Keys > Service keys), then run `kalup status` again.
|
|
12
|
+
|
|
13
|
+
## Example
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
W_WRITE_SCOPE: the key in HUBSPOT_SANDBOX_KEY does not hold crm.schemas.companies.write, which apply needs for companies (fix: add the scope crm.schemas.companies.write to the key) (docs: errors/W_WRITE_SCOPE.md)
|
|
17
|
+
```
|
package/docs/plan.md
CHANGED
|
@@ -10,7 +10,7 @@ This page is the reference. For the walk-through with examples, see [kalup plan]
|
|
|
10
10
|
2. The read key, then the portal guard (`E_TARGET_PORTAL_MISMATCH`, exit 4).
|
|
11
11
|
3. State for that portal, `.kalup/state/portal-<portalId>.json`; an unusable file is `E_STATE_INVALID`.
|
|
12
12
|
4. Pull's read and scope, plus tombstoned properties. A 403 leaves that object unread (`E_SCOPE`).
|
|
13
|
-
5. Limits Tracking (403 without a `crm.objects.*` scope
|
|
13
|
+
5. Limits Tracking (403 without a `crm.objects.*` scope; `W_LIMIT_UNREADABLE` for property creates), then the three `archived=true` lists of each object with a property create or an owned property HubSpot no longer holds. A group delete reads no archived list: only active properties block it. A 403 there is exit 1.
|
|
14
14
|
6. The plan, checked against `plan-1.schema.json` (`E_PLAN_SCHEMA` is a bug).
|
|
15
15
|
|
|
16
16
|
## State and the base
|
|
@@ -46,10 +46,10 @@ The first rule that matches: a `skip` override (no step, `coverage.excluded`); a
|
|
|
46
46
|
`hubspot/removed.ts` tombstones name properties and groups:
|
|
47
47
|
|
|
48
48
|
- `release`: a `release` step drops the entry, even one naming another portal name; nothing is sent.
|
|
49
|
-
- `destroy`, present: a `delete`, risk `destructive`, labelled `existed-before-kalup` for an adopted resource, expecting every base unit's live value. Blocked with `policy` without `allowDestroy: true`, `unsupported` when it is not archivable or a group still holds
|
|
49
|
+
- `destroy`, present: a `delete`, risk `destructive`, labelled `existed-before-kalup` for an adopted resource, expecting every base unit's live value. Blocked with `policy` without `allowDestroy: true`, `unsupported` when it is not archivable or a group still holds active properties the plan does not delete (archived ones do not block: HubSpot archives a group once every property in it is archived), `not-owned` without an owning entry.
|
|
50
50
|
- `destroy`, absent by a complete read: a release expecting `exists: false`.
|
|
51
51
|
|
|
52
|
-
Under takeover (config.md), a `delete` labelled `takeover` archives each custom property and group in the pull scope that config lacks, with a `mode` note naming the statement that asked for it; an option removal takeover asks for carries the note too. One `Takeover on <objects>` heading precedes the first such step and says whether each is confirmed at a terminal or all are blocked. Blocked with `policy` without `allowDestroy`, `scope` after an incomplete read, `unsupported` when not archivable or a group keeps
|
|
52
|
+
Under takeover (config.md), a `delete` labelled `takeover` archives each custom property and group in the pull scope that config lacks, with a `mode` note naming the statement that asked for it; an option removal takeover asks for carries the note too. One `Takeover on <objects>` heading precedes the first such step and says whether each is confirmed at a terminal or all are blocked. Blocked with `policy` without `allowDestroy`, `scope` after an incomplete read, `unsupported` when not archivable or a group keeps an active property. The `policy` fix leads with `kalup pull --target <t> --only <address>`, which keeps it in config, then `exclude` or `lifecycle: { options: 'additive' }` to leave it unmanaged, then `allowDestroy`. A delete expects every captured field's live value. Apply checks the same rules against its own read (a skipped group, a schema's properties, an empty group).
|
|
53
53
|
|
|
54
54
|
Releases follow the config steps, then deletes, the tombstones' and then takeover's, properties before groups.
|
|
55
55
|
|
package/docs/targets.md
CHANGED
|
@@ -38,7 +38,7 @@ The first of several targets is never chosen, and no choice is remembered. Text
|
|
|
38
38
|
|
|
39
39
|
`credentials.read.env` names the variable that holds the read key. Without `credentials` it is `HUBSPOT_SERVICE_KEY`, the variable the target `init` writes names. `init` itself needs no key. The key comes from the environment, else from the project's `.env`. A missing key is `E_MISSING_KEY`. No output carries a key.
|
|
40
40
|
|
|
41
|
-
The key goes out as `Authorization: Bearer`. `init` prints the read scopes the pull scope needs; `status` checks each. Both recommend one `crm.objects.<object>.read` scope too: Limits Tracking
|
|
41
|
+
The key goes out as `Authorization: Bearer`. `init` prints the read scopes the pull scope needs; `status` checks each. Both recommend one `crm.objects.<object>.read` scope too: Limits Tracking answers 403 to `crm.schemas.*` scopes alone and 200 once one `crm.objects.<object>.read` scope of any object is added; without it `plan` cannot check the property limit (`W_LIMIT_UNREADABLE`). `credentials.write` names the key apply, `state rebuild --write` and `target rebind` use (apply.md); without it they use the read key. That key needs the read scopes and `crm.schemas.<object>.write` per object (`crm.schemas.custom.write` for custom objects), which `init` and `status` list. `status` reads the scopes a key holds through HubSpot's token introspection and checks the recommended scope and, when the write key is the read key, each write scope by name; a separate write key is never resolved by a read command, so its scopes stay unchecked.
|
|
42
42
|
|
|
43
43
|
## Overrides
|
|
44
44
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kalup",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Configuration as code for HubSpot: pull, compare, plan and apply properties and property groups",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"hubspot",
|
|
@@ -62,8 +62,8 @@
|
|
|
62
62
|
}
|
|
63
63
|
},
|
|
64
64
|
"dependencies": {
|
|
65
|
-
"@oclif/core": "^5.
|
|
66
|
-
"@kalup/core": "0.
|
|
65
|
+
"@oclif/core": "^5.1.2",
|
|
66
|
+
"@kalup/core": "0.3.0"
|
|
67
67
|
},
|
|
68
68
|
"engines": {
|
|
69
69
|
"node": ">=22.13.1"
|