@koda-sl/baker-cli 0.316.1 → 0.319.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/dist/cli.js CHANGED
@@ -13271,6 +13271,172 @@ var STEP_SUBMIT_TRIGGERS = Object.fromEntries(
13271
13271
  );
13272
13272
  var MAX_CONVERSION_NAME = 80;
13273
13273
 
13274
+ // ../api/src/event-ingest/matching.ts
13275
+ var MATCH_FIELDS = [
13276
+ "first_name",
13277
+ "last_name",
13278
+ "date_of_birth",
13279
+ "gender",
13280
+ "city",
13281
+ "region",
13282
+ "postal_code",
13283
+ "country"
13284
+ ];
13285
+ var MATCH_COOKIES = ["fbp", "fbc"];
13286
+ function tightened(value) {
13287
+ const stripped = value.toLowerCase().replace(/[^\p{L}\p{N}]/gu, "");
13288
+ return stripped === "" ? null : stripped;
13289
+ }
13290
+ function loosened(value) {
13291
+ const collapsed = value.trim().toLowerCase().replace(/\s+/g, " ");
13292
+ return collapsed === "" ? null : collapsed;
13293
+ }
13294
+ function birthDate(value) {
13295
+ const digits = value.replace(/\D/g, "");
13296
+ if (!/^\d{8}$/.test(digits)) return null;
13297
+ const month = Number(digits.slice(4, 6));
13298
+ const day = Number(digits.slice(6, 8));
13299
+ if (month < 1 || month > 12 || day < 1 || day > 31) return null;
13300
+ return digits;
13301
+ }
13302
+ function genderLetter(value) {
13303
+ const first = value.trim().toLowerCase().charAt(0);
13304
+ return first === "f" || first === "m" ? first : null;
13305
+ }
13306
+ function countryCode(value) {
13307
+ const code = value.trim().toLowerCase();
13308
+ return /^[a-z]{2}$/.test(code) ? code : null;
13309
+ }
13310
+ var MATCH_SPECS = {
13311
+ first_name: { keep: "hashed", normalize: tightened, meta: "fn", google: "hashed_first_name" },
13312
+ last_name: { keep: "hashed", normalize: tightened, meta: "ln", google: "hashed_last_name" },
13313
+ // Meta only. Google's enhanced conversions have no date of birth, so there is
13314
+ // no destination wanting it in the clear and every reason not to keep one.
13315
+ date_of_birth: { keep: "hashed", normalize: birthDate, meta: "db", google: null },
13316
+ gender: { keep: "plain", normalize: genderLetter, meta: "ge", google: null },
13317
+ city: { keep: "plain", normalize: loosened, meta: "ct", google: "city" },
13318
+ region: { keep: "plain", normalize: loosened, meta: "st", google: "state" },
13319
+ postal_code: { keep: "plain", normalize: loosened, meta: "zp", google: "postal_code" },
13320
+ country: { keep: "plain", normalize: countryCode, meta: "country", google: "country_code" }
13321
+ };
13322
+ var MATCH_FIELD_FORWARDING = [
13323
+ { field: "email", meta: "em", google: "hashed_email", hashed: true },
13324
+ { field: "phone", meta: "ph", google: "hashed_phone_number", hashed: true },
13325
+ // Kept in the clear — see `resolveOwnId`. The digest is still computed and
13326
+ // still stored beside it, so a Meta forward has one; what changed is that the
13327
+ // sender's own key is now readable, which is the entire reason they sent it.
13328
+ { field: "external_id", meta: "external_id", google: null, hashed: false },
13329
+ ...MATCH_FIELDS.map((field) => ({
13330
+ field,
13331
+ meta: MATCH_SPECS[field].meta,
13332
+ google: MATCH_SPECS[field].google,
13333
+ hashed: MATCH_SPECS[field].keep === "hashed"
13334
+ })),
13335
+ { field: "fbp", meta: "fbp", google: null, hashed: false },
13336
+ { field: "fbc", meta: "fbc", google: null, hashed: false }
13337
+ ];
13338
+
13339
+ // ../api/src/event-ingest/vocabulary.ts
13340
+ var INGEST_CLICK_ID_PARAMS = [
13341
+ "gclid",
13342
+ "wbraid",
13343
+ "gbraid",
13344
+ "dclid",
13345
+ "fbclid",
13346
+ "ttclid",
13347
+ "twclid",
13348
+ "li_fat_id",
13349
+ "msclkid",
13350
+ "rdt_cid",
13351
+ "epik",
13352
+ "sccid"
13353
+ ];
13354
+
13355
+ // ../api/src/event-sources/fields.ts
13356
+ var MATCH_LABELS = {
13357
+ first_name: "First name",
13358
+ last_name: "Last name",
13359
+ date_of_birth: "Date of birth",
13360
+ gender: "Gender",
13361
+ city: "City",
13362
+ region: "Region",
13363
+ postal_code: "Postcode",
13364
+ country: "Country"
13365
+ };
13366
+ var CLICK_LABELS = {
13367
+ gclid: "Google click id",
13368
+ wbraid: "Google click id (iOS, web to app)",
13369
+ gbraid: "Google click id (iOS, app)",
13370
+ dclid: "Google click id (Display & Video)",
13371
+ fbclid: "Meta click id",
13372
+ ttclid: "TikTok click id",
13373
+ twclid: "X click id",
13374
+ li_fat_id: "LinkedIn click id",
13375
+ msclkid: "Microsoft click id",
13376
+ rdt_cid: "Reddit click id",
13377
+ epik: "Pinterest click id",
13378
+ sccid: "Snapchat click id"
13379
+ };
13380
+ var SOURCE_FIELDS = [
13381
+ { field: "event_name", label: "What happened", group: "event" },
13382
+ { field: "event_time", label: "When it happened", group: "event" },
13383
+ { field: "event_id", label: "Its id", group: "event" },
13384
+ { field: "email", label: "Email", group: "person" },
13385
+ { field: "phone", label: "Phone", group: "person" },
13386
+ { field: "external_id", label: "Your own id for the person", group: "person" },
13387
+ ...MATCH_FIELDS.map((field) => ({ field, label: MATCH_LABELS[field], group: "person" })),
13388
+ { field: "value", label: "Amount", group: "money" },
13389
+ { field: "currency", label: "Currency", group: "money" },
13390
+ ...INGEST_CLICK_ID_PARAMS.map((field) => ({ field, label: CLICK_LABELS[field], group: "click" }))
13391
+ ];
13392
+ var SOURCE_FIELD_NAMES = SOURCE_FIELDS.map((spec) => spec.field);
13393
+ var FIELD_SET = new Set(SOURCE_FIELD_NAMES);
13394
+ function isSourceField(value) {
13395
+ return FIELD_SET.has(value);
13396
+ }
13397
+ var PROPERTY_ROW_PREFIX = "properties.";
13398
+ var PROPERTY_KEY_MAX = 64;
13399
+ var PROPERTY_KEY = /^[A-Za-z0-9_][A-Za-z0-9_.-]*$/;
13400
+ var RESERVED_PROPERTY_KEYS = ["baker_source", "hubspot_property", "hubspot_value"];
13401
+ function isPropertyRow(row) {
13402
+ return row.startsWith(PROPERTY_ROW_PREFIX);
13403
+ }
13404
+ function propertyKeyOf(row) {
13405
+ return isPropertyRow(row) ? row.slice(PROPERTY_ROW_PREFIX.length) : null;
13406
+ }
13407
+ function propertyKeyProblem(key) {
13408
+ const trimmed = key.trim();
13409
+ if (trimmed === "") return "A property needs a name.";
13410
+ if (trimmed.length > PROPERTY_KEY_MAX) return `A property name is at most ${PROPERTY_KEY_MAX} characters.`;
13411
+ if (!PROPERTY_KEY.test(trimmed)) {
13412
+ return `"${trimmed}" is not a property name: letters, digits, underscore, dash and dot only, starting with a letter or digit.`;
13413
+ }
13414
+ if (RESERVED_PROPERTY_KEYS.includes(trimmed)) return `"${trimmed}" is written by Baker itself; pick another name.`;
13415
+ return null;
13416
+ }
13417
+ function mappingRowProblem(row) {
13418
+ if (isSourceField(row)) return null;
13419
+ const key = propertyKeyOf(row);
13420
+ if (key === null) {
13421
+ return `"${row}" is not a field an event can carry. A field is one of ${SOURCE_FIELD_NAMES.join(", ")}; an extra property is spelled properties.<name>.`;
13422
+ }
13423
+ return propertyKeyProblem(key);
13424
+ }
13425
+
13426
+ // ../api/src/event-sources/template.ts
13427
+ var MENTION = /\{\{\s*([^{}]+?)\s*\}\}/g;
13428
+ function mentionOf(path48) {
13429
+ return `{{${path48}}}`;
13430
+ }
13431
+ function templateMentions(template) {
13432
+ const seen = [];
13433
+ for (const match of template.matchAll(MENTION)) {
13434
+ const path48 = (match[1] ?? "").trim();
13435
+ if (path48 !== "" && !seen.includes(path48)) seen.push(path48);
13436
+ }
13437
+ return seen;
13438
+ }
13439
+
13274
13440
  // ../api/src/analytics/eventKey.ts
13275
13441
  var SEP = ":";
13276
13442
  function formEventKey(flowSlug, nodeId, triggerId) {
@@ -13305,6 +13471,106 @@ function parseEventKey(key) {
13305
13471
  }
13306
13472
  }
13307
13473
 
13474
+ // ../api/src/analytics/sends.ts
13475
+ var SEND_PLATFORMS = ["meta", "googleAds", "linkedinInsightTag", "twitterAds", "googleAnalytics"];
13476
+ function isSendPlatform(value) {
13477
+ return SEND_PLATFORMS.includes(value);
13478
+ }
13479
+ var SEND_PLATFORM_SPECS = {
13480
+ meta: {
13481
+ title: "Meta",
13482
+ eventMeans: "A Meta event name \u2014 Lead, Purchase, Schedule, or one of your own",
13483
+ eventKind: "name",
13484
+ credentialFields: ["accessToken", "datasetId"],
13485
+ setupHint: "Add the Conversions API access token and dataset id to the Meta pixel in Tracking"
13486
+ },
13487
+ googleAds: {
13488
+ title: "Google Ads",
13489
+ eventMeans: "One of the account's conversion actions, picked from its list",
13490
+ eventKind: "id",
13491
+ credentialFields: ["oauthProviderId"],
13492
+ setupHint: "Connect the Google Ads conversion tag to a Google Ads account in Tracking"
13493
+ },
13494
+ linkedinInsightTag: {
13495
+ title: "LinkedIn",
13496
+ eventMeans: "The id of a conversion rule in LinkedIn Campaign Manager",
13497
+ eventKind: "id",
13498
+ credentialFields: ["oauthProviderId"],
13499
+ setupHint: "Connect the LinkedIn Insight Tag to a LinkedIn account in Tracking"
13500
+ },
13501
+ twitterAds: {
13502
+ title: "X",
13503
+ eventMeans: "The event tag id from X Events Manager, like tw-oka17-abc12",
13504
+ eventKind: "id",
13505
+ credentialFields: ["oauthProviderId"],
13506
+ setupHint: "Connect the X pixel to an X Ads account in Tracking"
13507
+ },
13508
+ googleAnalytics: {
13509
+ title: "Google Analytics",
13510
+ eventMeans: "A GA4 event name \u2014 generate_lead, purchase, or one of your own",
13511
+ eventKind: "name",
13512
+ credentialFields: ["apiSecret"],
13513
+ setupHint: "Add the Measurement Protocol API secret to the Google Analytics tag in Tracking"
13514
+ }
13515
+ };
13516
+ var SEND_ORIGINS = ["server", "website", "any"];
13517
+ var MAX_SEND_ID = 64;
13518
+ var MAX_SEND_EVENT_KEY = 320;
13519
+ function sendEventIsTemplated(event) {
13520
+ return templateMentions(event).length > 0;
13521
+ }
13522
+ function sendEventProblem(platform, event) {
13523
+ if (!isSendPlatform(platform)) return null;
13524
+ if (!sendEventIsTemplated(event)) return null;
13525
+ const spec = SEND_PLATFORM_SPECS[platform];
13526
+ if (spec.eventKind === "name") return null;
13527
+ return `${spec.title} identifies a conversion by an id it issued, so it cannot be written from the event. Paste the id; pass what varies as an extra parameter instead.`;
13528
+ }
13529
+
13530
+ // ../api/src/analytics/destinations.ts
13531
+ var CONVERSION_SEND_PREFIX = "conversionSend:";
13532
+ function isConversionSend(type) {
13533
+ return type.startsWith(CONVERSION_SEND_PREFIX);
13534
+ }
13535
+ var SERVER_SIDE_SENDER_TAG = {
13536
+ metaCapi: "meta",
13537
+ googleAdsCapi: "googleAds",
13538
+ linkedinCapi: "linkedinInsightTag",
13539
+ twitterAdsCapi: "twitterAds",
13540
+ ga4Mp: "googleAnalytics"
13541
+ };
13542
+ var DESTINATION_NAMES = {
13543
+ email: "Email",
13544
+ httpWebhook: "HTTP Webhook",
13545
+ zapier: "Zapier",
13546
+ tiktok: "TikTok Pixel",
13547
+ pinterest: "Pinterest Tag",
13548
+ googleTagManager: "Google Tag Manager",
13549
+ amplitude: "Amplitude",
13550
+ hotjar: "Hotjar",
13551
+ clarity: "Microsoft Clarity",
13552
+ hubspot: "HubSpot Events",
13553
+ vwo: "VWO",
13554
+ posthog: "PostHog",
13555
+ datafast: "DataFast",
13556
+ code: "Custom Code",
13557
+ pipedrive: "Pipedrive",
13558
+ googleSpreadsheet: "Google Sheets",
13559
+ hubspotForm: "HubSpot Form",
13560
+ goHighlevelContact: "GoHighLevel",
13561
+ crmble: "Crmble"
13562
+ };
13563
+ function destinationKey(type) {
13564
+ const platform = isConversionSend(type) ? type.slice(CONVERSION_SEND_PREFIX.length) : type;
13565
+ return SERVER_SIDE_SENDER_TAG[platform] ?? platform;
13566
+ }
13567
+ function destinationName(type) {
13568
+ if (type === "") return "";
13569
+ const key = destinationKey(type);
13570
+ if (isSendPlatform(key)) return SEND_PLATFORM_SPECS[key].title;
13571
+ return DESTINATION_NAMES[key] ?? type;
13572
+ }
13573
+
13308
13574
  // ../api/src/analytics/events.ts
13309
13575
  import { z as z28 } from "zod";
13310
13576
 
@@ -13464,11 +13730,15 @@ var analyticsBeaconClickIdsSchema = z28.object({
13464
13730
  gclid: z28.string().max(255).optional(),
13465
13731
  wbraid: z28.string().max(255).optional(),
13466
13732
  gbraid: z28.string().max(255).optional(),
13733
+ dclid: z28.string().max(255).optional(),
13467
13734
  fbclid: z28.string().max(255).optional(),
13468
13735
  ttclid: z28.string().max(255).optional(),
13469
13736
  twclid: z28.string().max(255).optional(),
13470
13737
  liFatId: z28.string().max(255).optional(),
13471
- msclkid: z28.string().max(255).optional()
13738
+ msclkid: z28.string().max(255).optional(),
13739
+ rdtCid: z28.string().max(255).optional(),
13740
+ epik: z28.string().max(255).optional(),
13741
+ sccid: z28.string().max(255).optional()
13472
13742
  });
13473
13743
  var analyticsBeaconIdentitySchema = z28.object({
13474
13744
  /** Lowercase hex SHA-256. Anything else is dropped rather than stored. */
@@ -13624,218 +13894,169 @@ var analyticsBeaconPayloadSchema = z28.object({
13624
13894
  consent: z28.object({ analytics: z28.boolean() }).optional()
13625
13895
  });
13626
13896
 
13627
- // ../api/src/event-ingest/matching.ts
13628
- var MATCH_FIELDS = [
13629
- "first_name",
13630
- "last_name",
13631
- "date_of_birth",
13632
- "gender",
13633
- "city",
13634
- "region",
13635
- "postal_code",
13636
- "country"
13637
- ];
13638
- var MATCH_COOKIES = ["fbp", "fbc"];
13639
- function tightened(value) {
13640
- const stripped = value.toLowerCase().replace(/[^\p{L}\p{N}]/gu, "");
13641
- return stripped === "" ? null : stripped;
13642
- }
13643
- function loosened(value) {
13644
- const collapsed = value.trim().toLowerCase().replace(/\s+/g, " ");
13645
- return collapsed === "" ? null : collapsed;
13646
- }
13647
- function birthDate(value) {
13648
- const digits = value.replace(/\D/g, "");
13649
- if (!/^\d{8}$/.test(digits)) return null;
13650
- const month = Number(digits.slice(4, 6));
13651
- const day = Number(digits.slice(6, 8));
13652
- if (month < 1 || month > 12 || day < 1 || day > 31) return null;
13653
- return digits;
13897
+ // ../api/src/analytics/triggers.ts
13898
+ var TRIGGERS = {
13899
+ "calendlyEvent.eventScheduled": {
13900
+ what: "Call booked",
13901
+ meaning: "Somebody picked a slot and confirmed it. This is the booking."
13902
+ },
13903
+ "calendlyEvent.dateAndTimeSelected": {
13904
+ what: "Time picked",
13905
+ meaning: "Somebody chose a slot but has not confirmed the booking yet."
13906
+ },
13907
+ "calendlyEvent.eventTypeViewed": {
13908
+ what: "Calendar opened",
13909
+ meaning: "The calendar appeared. Nobody has picked a time yet.",
13910
+ lifecycle: true
13911
+ },
13912
+ "calendlyEvent.profilePageViewed": {
13913
+ what: "Booking page shown",
13914
+ meaning: "Calendly's own page appeared. Nobody has picked a time yet.",
13915
+ lifecycle: true
13916
+ },
13917
+ "customForm.onFormSubmitted": {
13918
+ what: "Form submitted",
13919
+ meaning: "Somebody filled this step in and sent it."
13920
+ },
13921
+ "highlevelEvent.appointmentScheduled": {
13922
+ what: "Appointment booked",
13923
+ meaning: "Somebody picked a slot and confirmed it. This is the booking."
13924
+ },
13925
+ "highlevelEvent.calendarLoaded": {
13926
+ what: "Calendar finished loading",
13927
+ meaning: "The calendar appeared. Nobody has picked a time yet.",
13928
+ lifecycle: true
13929
+ },
13930
+ "highlevelFormEvent.formSubmitted": {
13931
+ what: "Form submitted",
13932
+ meaning: "Somebody filled the form in and sent it."
13933
+ },
13934
+ "highlevelFormEvent.formLoaded": {
13935
+ what: "Form finished loading",
13936
+ meaning: "The form appeared. Nobody has typed anything yet.",
13937
+ lifecycle: true
13938
+ },
13939
+ "hubspotForm.onFormSubmitted": {
13940
+ what: "Form submitted",
13941
+ meaning: "Somebody filled the form in and sent it. This is the one that means a lead."
13942
+ },
13943
+ "hubspotForm.onBeforeFormSubmit": {
13944
+ what: "Form about to be sent",
13945
+ meaning: "Fires an instant before the form is sent \u2014 not the same as it arriving.",
13946
+ lifecycle: true
13947
+ },
13948
+ "hubspotForm.onFormReady": {
13949
+ what: "Form finished loading",
13950
+ meaning: "The form appeared. Nobody has typed anything yet.",
13951
+ lifecycle: true
13952
+ },
13953
+ "hubspotForm.onBeforeFormInit": {
13954
+ what: "Form starting to load",
13955
+ meaning: "Fires before the form has even drawn. It only means the page was opened.",
13956
+ lifecycle: true
13957
+ },
13958
+ "hubspotMeeting.onMeetingScheduled": {
13959
+ what: "Meeting booked",
13960
+ meaning: "Somebody picked a slot and confirmed it. This is the booking."
13961
+ },
13962
+ "link.onClick": {
13963
+ what: "Link clicked",
13964
+ meaning: "Somebody clicked the link on this step and left for wherever it points."
13965
+ },
13966
+ "savvycalEvent.scheduled": {
13967
+ what: "Call booked",
13968
+ meaning: "Somebody picked a slot and confirmed it. This is the booking."
13969
+ },
13970
+ "text.onTextDisplayed": {
13971
+ what: "Screen shown",
13972
+ meaning: "This screen appeared. On a thank-you screen that is the outcome; elsewhere it is not."
13973
+ },
13974
+ "text.onButtonClicked": {
13975
+ what: "Button pressed",
13976
+ meaning: "Somebody pressed the button on this screen."
13977
+ }
13978
+ };
13979
+ function triggerDescription(triggerId) {
13980
+ return TRIGGERS[triggerId];
13654
13981
  }
13655
- function genderLetter(value) {
13656
- const first = value.trim().toLowerCase().charAt(0);
13657
- return first === "f" || first === "m" ? first : null;
13982
+
13983
+ // ../api/src/analytics/feedNoise.ts
13984
+ var BOOKKEEPING_TYPES = /* @__PURE__ */ new Set([
13985
+ "visitor_new",
13986
+ "session_start",
13987
+ "identify",
13988
+ "form_step_view",
13989
+ "side_effect_settled"
13990
+ ]);
13991
+ var SUBMIT_TRIGGERS = new Set(Object.values(STEP_SUBMIT_TRIGGERS));
13992
+ function isEmbedLifecycle(row) {
13993
+ return row.eventType === "form_trigger" && triggerDescription(row.triggerId)?.lifecycle === true;
13658
13994
  }
13659
- function countryCode(value) {
13660
- const code = value.trim().toLowerCase();
13661
- return /^[a-z]{2}$/.test(code) ? code : null;
13995
+ function isBookkeepingRow(row) {
13996
+ if (BOOKKEEPING_TYPES.has(row.eventType)) return true;
13997
+ if (isEmbedLifecycle(row)) return true;
13998
+ return row.eventType === "form_trigger" && SUBMIT_TRIGGERS.has(row.triggerId);
13662
13999
  }
13663
- var MATCH_SPECS = {
13664
- first_name: { keep: "hashed", normalize: tightened, meta: "fn", google: "hashed_first_name" },
13665
- last_name: { keep: "hashed", normalize: tightened, meta: "ln", google: "hashed_last_name" },
13666
- // Meta only. Google's enhanced conversions have no date of birth, so there is
13667
- // no destination wanting it in the clear and every reason not to keep one.
13668
- date_of_birth: { keep: "hashed", normalize: birthDate, meta: "db", google: null },
13669
- gender: { keep: "plain", normalize: genderLetter, meta: "ge", google: null },
13670
- city: { keep: "plain", normalize: loosened, meta: "ct", google: "city" },
13671
- region: { keep: "plain", normalize: loosened, meta: "st", google: "state" },
13672
- postal_code: { keep: "plain", normalize: loosened, meta: "zp", google: "postal_code" },
13673
- country: { keep: "plain", normalize: countryCode, meta: "country", google: "country_code" }
13674
- };
13675
- var MATCH_FIELD_FORWARDING = [
13676
- { field: "email", meta: "em", google: "hashed_email", hashed: true },
13677
- { field: "phone", meta: "ph", google: "hashed_phone_number", hashed: true },
13678
- // Kept in the clear — see `resolveOwnId`. The digest is still computed and
13679
- // still stored beside it, so a Meta forward has one; what changed is that the
13680
- // sender's own key is now readable, which is the entire reason they sent it.
13681
- { field: "external_id", meta: "external_id", google: null, hashed: false },
13682
- ...MATCH_FIELDS.map((field) => ({
13683
- field,
13684
- meta: MATCH_SPECS[field].meta,
13685
- google: MATCH_SPECS[field].google,
13686
- hashed: MATCH_SPECS[field].keep === "hashed"
13687
- })),
13688
- { field: "fbp", meta: "fbp", google: null, hashed: false },
13689
- { field: "fbc", meta: "fbc", google: null, hashed: false }
13690
- ];
13691
14000
 
13692
- // ../api/src/event-ingest/vocabulary.ts
13693
- var INGEST_CLICK_ID_PARAMS = [
14001
+ // ../api/src/analytics/identityLinks.ts
14002
+ var IDENTITY_KEY_KINDS = [
14003
+ "email",
14004
+ "phone",
14005
+ "external",
14006
+ "browser",
13694
14007
  "gclid",
13695
14008
  "wbraid",
13696
14009
  "gbraid",
14010
+ "dclid",
13697
14011
  "fbclid",
13698
14012
  "ttclid",
13699
14013
  "twclid",
13700
14014
  "li_fat_id",
13701
- "msclkid"
14015
+ "msclkid",
14016
+ "rdt_cid",
14017
+ "epik",
14018
+ "sccid",
14019
+ "fbp",
14020
+ "fbc"
13702
14021
  ];
14022
+ var ADDRESS_BOOK_FILL_KINDS = IDENTITY_KEY_KINDS.filter((kind) => kind !== "browser");
13703
14023
 
13704
- // ../api/src/event-sources/fields.ts
13705
- var MATCH_LABELS = {
13706
- first_name: "First name",
13707
- last_name: "Last name",
13708
- date_of_birth: "Date of birth",
13709
- gender: "Gender",
13710
- city: "City",
13711
- region: "Region",
13712
- postal_code: "Postcode",
13713
- country: "Country"
13714
- };
13715
- var CLICK_LABELS = {
13716
- gclid: "Google click id",
13717
- wbraid: "Google click id (iOS, web to app)",
13718
- gbraid: "Google click id (iOS, app)",
13719
- fbclid: "Meta click id",
13720
- ttclid: "TikTok click id",
13721
- twclid: "X click id",
13722
- li_fat_id: "LinkedIn click id",
13723
- msclkid: "Microsoft click id"
14024
+ // ../api/src/analytics/matchKeys.ts
14025
+ var CONTACT_KEYS = ["email", "phone", "external_id", "baker_id"];
14026
+ var MATCH_KEY_FIELDS = [...CONTACT_KEYS, ...INGEST_CLICK_ID_PARAMS, ...MATCH_COOKIES];
14027
+ var MATCH_KEY_LABELS = {
14028
+ email: "email",
14029
+ phone: "phone",
14030
+ external_id: "external ID",
14031
+ baker_id: "Baker ID",
14032
+ gclid: "Google click",
14033
+ wbraid: "Google click (wbraid)",
14034
+ gbraid: "Google click (gbraid)",
14035
+ dclid: "Google Display click",
14036
+ fbclid: "Meta click",
14037
+ ttclid: "TikTok click",
14038
+ twclid: "X click",
14039
+ li_fat_id: "LinkedIn click",
14040
+ msclkid: "Microsoft click",
14041
+ rdt_cid: "Reddit click",
14042
+ epik: "Pinterest click",
14043
+ sccid: "Snapchat click",
14044
+ fbp: "Meta browser cookie",
14045
+ fbc: "Meta click cookie"
13724
14046
  };
13725
- var SOURCE_FIELDS = [
13726
- { field: "event_name", label: "What happened", group: "event" },
13727
- { field: "event_time", label: "When it happened", group: "event" },
13728
- { field: "event_id", label: "Its id", group: "event" },
13729
- { field: "email", label: "Email", group: "person" },
13730
- { field: "phone", label: "Phone", group: "person" },
13731
- { field: "external_id", label: "Your own id for the person", group: "person" },
13732
- ...MATCH_FIELDS.map((field) => ({ field, label: MATCH_LABELS[field], group: "person" })),
13733
- { field: "value", label: "Amount", group: "money" },
13734
- { field: "currency", label: "Currency", group: "money" },
13735
- ...INGEST_CLICK_ID_PARAMS.map((field) => ({ field, label: CLICK_LABELS[field], group: "click" }))
13736
- ];
13737
- var SOURCE_FIELD_NAMES = SOURCE_FIELDS.map((spec) => spec.field);
13738
- var FIELD_SET = new Set(SOURCE_FIELD_NAMES);
13739
- function isSourceField(value) {
13740
- return FIELD_SET.has(value);
13741
- }
13742
- var PROPERTY_ROW_PREFIX = "properties.";
13743
- var PROPERTY_KEY_MAX = 64;
13744
- var PROPERTY_KEY = /^[A-Za-z0-9_][A-Za-z0-9_.-]*$/;
13745
- var RESERVED_PROPERTY_KEYS = ["baker_source", "hubspot_property", "hubspot_value"];
13746
- function isPropertyRow(row) {
13747
- return row.startsWith(PROPERTY_ROW_PREFIX);
13748
- }
13749
- function propertyKeyOf(row) {
13750
- return isPropertyRow(row) ? row.slice(PROPERTY_ROW_PREFIX.length) : null;
13751
- }
13752
- function propertyKeyProblem(key) {
13753
- const trimmed = key.trim();
13754
- if (trimmed === "") return "A property needs a name.";
13755
- if (trimmed.length > PROPERTY_KEY_MAX) return `A property name is at most ${PROPERTY_KEY_MAX} characters.`;
13756
- if (!PROPERTY_KEY.test(trimmed)) {
13757
- return `"${trimmed}" is not a property name: letters, digits, underscore, dash and dot only, starting with a letter or digit.`;
13758
- }
13759
- if (RESERVED_PROPERTY_KEYS.includes(trimmed)) return `"${trimmed}" is written by Baker itself; pick another name.`;
13760
- return null;
13761
- }
13762
- function mappingRowProblem(row) {
13763
- if (isSourceField(row)) return null;
13764
- const key = propertyKeyOf(row);
13765
- if (key === null) {
13766
- return `"${row}" is not a field an event can carry. A field is one of ${SOURCE_FIELD_NAMES.join(", ")}; an extra property is spelled properties.<name>.`;
13767
- }
13768
- return propertyKeyProblem(key);
13769
- }
13770
-
13771
- // ../api/src/event-sources/template.ts
13772
- var MENTION = /\{\{\s*([^{}]+?)\s*\}\}/g;
13773
- function mentionOf(path48) {
13774
- return `{{${path48}}}`;
13775
- }
13776
- function templateMentions(template) {
13777
- const seen = [];
13778
- for (const match of template.matchAll(MENTION)) {
13779
- const path48 = (match[1] ?? "").trim();
13780
- if (path48 !== "" && !seen.includes(path48)) seen.push(path48);
13781
- }
13782
- return seen;
13783
- }
13784
-
13785
- // ../api/src/analytics/sends.ts
13786
- var SEND_PLATFORMS = ["meta", "googleAds", "linkedinInsightTag", "twitterAds", "googleAnalytics"];
13787
- function isSendPlatform(value) {
13788
- return SEND_PLATFORMS.includes(value);
13789
- }
13790
- var SEND_PLATFORM_SPECS = {
13791
- meta: {
13792
- title: "Meta",
13793
- eventMeans: "A Meta event name \u2014 Lead, Purchase, Schedule, or one of your own",
13794
- eventKind: "name",
13795
- credentialFields: ["accessToken", "datasetId"],
13796
- setupHint: "Add the Conversions API access token and dataset id to the Meta pixel in Tracking"
13797
- },
13798
- googleAds: {
13799
- title: "Google Ads",
13800
- eventMeans: "One of the account's conversion actions, picked from its list",
13801
- eventKind: "id",
13802
- credentialFields: ["oauthProviderId"],
13803
- setupHint: "Connect the Google Ads conversion tag to a Google Ads account in Tracking"
13804
- },
13805
- linkedinInsightTag: {
13806
- title: "LinkedIn",
13807
- eventMeans: "The id of a conversion rule in LinkedIn Campaign Manager",
13808
- eventKind: "id",
13809
- credentialFields: ["oauthProviderId"],
13810
- setupHint: "Connect the LinkedIn Insight Tag to a LinkedIn account in Tracking"
13811
- },
13812
- twitterAds: {
13813
- title: "X",
13814
- eventMeans: "The event tag id from X Events Manager, like tw-oka17-abc12",
13815
- eventKind: "id",
13816
- credentialFields: ["oauthProviderId"],
13817
- setupHint: "Connect the X pixel to an X Ads account in Tracking"
13818
- },
13819
- googleAnalytics: {
13820
- title: "Google Analytics",
13821
- eventMeans: "A GA4 event name \u2014 generate_lead, purchase, or one of your own",
13822
- eventKind: "name",
13823
- credentialFields: ["apiSecret"],
13824
- setupHint: "Add the Measurement Protocol API secret to the Google Analytics tag in Tracking"
14047
+ function carries(userData, field) {
14048
+ for (const key of [field, `${field}_sha256`, `${field}_preview`]) {
14049
+ const value = userData[key];
14050
+ if (typeof value === "string" && value !== "") return true;
13825
14051
  }
13826
- };
13827
- var SEND_ORIGINS = ["server", "website", "any"];
13828
- var MAX_SEND_ID = 64;
13829
- var MAX_SEND_EVENT_KEY = 320;
13830
- function sendEventIsTemplated(event) {
13831
- return templateMentions(event).length > 0;
14052
+ return false;
13832
14053
  }
13833
- function sendEventProblem(platform, event) {
13834
- if (!isSendPlatform(platform)) return null;
13835
- if (!sendEventIsTemplated(event)) return null;
13836
- const spec = SEND_PLATFORM_SPECS[platform];
13837
- if (spec.eventKind === "name") return null;
13838
- return `${spec.title} identifies a conversion by an id it issued, so it cannot be written from the event. Paste the id; pass what varies as an extra parameter instead.`;
14054
+ function rowMatchKeys(userData) {
14055
+ if (userData === void 0) return [];
14056
+ return MATCH_KEY_FIELDS.filter((field) => carries(userData, field)).map((field) => ({
14057
+ field,
14058
+ label: MATCH_KEY_LABELS[field] ?? field
14059
+ }));
13839
14060
  }
13840
14061
 
13841
14062
  // ../api/src/analytics/sendPayload.ts
@@ -13858,11 +14079,15 @@ var CLICK_LABELS2 = {
13858
14079
  gclid: "Google click id",
13859
14080
  wbraid: "Google click id (web-to-app)",
13860
14081
  gbraid: "Google click id (app-to-web)",
14082
+ dclid: "Google click id (Display & Video)",
13861
14083
  fbclid: "Meta click id",
13862
14084
  ttclid: "TikTok click id",
13863
14085
  twclid: "X click id",
13864
14086
  li_fat_id: "LinkedIn click id",
13865
- msclkid: "Microsoft click id"
14087
+ msclkid: "Microsoft click id",
14088
+ rdt_cid: "Reddit click id",
14089
+ epik: "Pinterest click id",
14090
+ sccid: "Snapchat click id"
13866
14091
  };
13867
14092
  var PERSON_FIELDS = MATCH_FIELD_FORWARDING.map((row) => ({
13868
14093
  name: row.field,
@@ -14572,9 +14797,15 @@ var analyticsQueryRequestSchema = z31.object({
14572
14797
  /**
14573
14798
  * One browser, by its cookie id.
14574
14799
  *
14575
- * Narrows `people` to a single person, and replays that person's whole
14576
- * history on `session` — every visit they made, not just the one. Given
14577
- * with `sessionId`, the session wins and this is ignored.
14800
+ * Narrows `people` to a single person, narrows `stream` to everything
14801
+ * that person did, and replays that person's whole history on `session` —
14802
+ * every visit they made, not just the one. Given with `sessionId`, the
14803
+ * session wins and this is ignored.
14804
+ *
14805
+ * On `stream` it is a seed rather than a cookie filter: the browser plus
14806
+ * every address, phone, click id and platform cookie ever seen beside it.
14807
+ * Filtering on the cookie alone would drop exactly the rows the reader
14808
+ * came for — a deal a CRM posted names the person and carries no cookie.
14578
14809
  */
14579
14810
  anonId: z31.string().max(120).optional(),
14580
14811
  /**
@@ -15633,7 +15864,8 @@ var analyticsVisitorRowSchema = z31.object({
15633
15864
  campaignParams: z31.record(z31.string(), z31.string()),
15634
15865
  /**
15635
15866
  * The click ids this person arrived on, by their own names — `gclid`,
15636
- * `fbclid`, `msclkid`, `li_fat_id`, `ttclid`, `twclid`, `wbraid`, `gbraid`.
15867
+ * `fbclid`, `msclkid`, `li_fat_id`, `ttclid`, `twclid`, `wbraid`, `gbraid`,
15868
+ * `dclid`, `rdt_cid`, `epik`, `sccid`.
15637
15869
  *
15638
15870
  * Only the ones that were present. The endpoint has always returned all eight
15639
15871
  * columns and nothing above it carried them, so the one screen whose subject
@@ -29229,9 +29461,15 @@ var EVENT_INGEST_BODY_MAX_BYTES = 8 * 1024 * 1024;
29229
29461
  var EVENT_INGEST_REQUESTS_PER_MINUTE = 60;
29230
29462
  var FUTURE_TOLERANCE_MS = 5 * 6e4;
29231
29463
  var MAX_EVENT_NAME = 80;
29464
+ var EVENT_NAME = /^[\p{L}\p{N}][\p{L}\p{M}\p{N} ._-]*$/u;
29465
+ function validateEventName(name) {
29466
+ const value = typeof name === "string" ? name.trim() : "";
29467
+ if (value === "" || value.length > MAX_EVENT_NAME || !EVENT_NAME.test(value)) return "invalid_name";
29468
+ return null;
29469
+ }
29232
29470
  var REJECTION_MESSAGES = {
29233
29471
  invalid_event_id: "event_id, when sent, must be 1-128 characters of A-Z a-z 0-9 . _ : -",
29234
- invalid_name: "event_name is required: up to 80 characters of letters, digits, spaces, . _ and -",
29472
+ invalid_name: "event_name is required: up to 80 characters of letters (any language), digits, spaces, . _ and -, starting with a letter or a digit",
29235
29473
  missing_identity: "user_data named nobody, and no url named a page",
29236
29474
  invalid_timestamp: "event_time must be an ISO-8601 instant with an offset or Unix seconds, and not in the future",
29237
29475
  invalid_value: "value and currency go together: a non-negative decimal and an ISO-4217 code",
@@ -29259,8 +29497,8 @@ var EVENT_FIELDS = [
29259
29497
  {
29260
29498
  name: "event_name",
29261
29499
  need: "required",
29262
- takes: `What happened. Up to ${MAX_EVENT_NAME} characters of letters, digits, spaces, . _ and -`,
29263
- note: "The only required field. Use one of the standard names below wherever one fits \u2014 anything else is yours to invent."
29500
+ takes: `What happened. Up to ${MAX_EVENT_NAME} characters of letters, digits, spaces, . _ and -, starting with a letter or a digit`,
29501
+ note: 'The only required field. Use one of the standard names below wherever one fits \u2014 anything else is yours to invent, in whatever language you name things in: a letter is a letter in any script, and "Matr\xEDcula Realizada" is a name.'
29264
29502
  },
29265
29503
  {
29266
29504
  name: "event_time",
@@ -29311,7 +29549,7 @@ var EVENT_FIELDS = [
29311
29549
  name: "the ad click ids",
29312
29550
  need: "optional",
29313
29551
  under: true,
29314
- takes: "The click id that brought them, if you kept it, under the name the platform stamps on the link: gclid, wbraid, gbraid, fbclid, ttclid, twclid, li_fat_id, msclkid.",
29552
+ takes: "The click id that brought them, if you kept it, under the name the platform stamps on the link: gclid, wbraid, gbraid, dclid, fbclid, ttclid, twclid, li_fat_id, msclkid, rdt_cid, epik, sccid (Snapchat's ScCid, in any case).",
29315
29553
  note: "A click id names a person too \u2014 it is enough on its own, and it attributes the event to the campaign that earned it."
29316
29554
  },
29317
29555
  {
@@ -30121,6 +30359,40 @@ function mergeStoredWithCounts(stored, reported) {
30121
30359
  }).sort((a, b) => a.name.localeCompare(b.name));
30122
30360
  }
30123
30361
 
30362
+ // src/commands/analytics/feedRows.ts
30363
+ function isStreamRow(value) {
30364
+ return typeof value === "object" && value !== null && !Array.isArray(value);
30365
+ }
30366
+ function enrich(row) {
30367
+ const out = { ...row };
30368
+ const destination = typeof row.sideEffectType === "string" ? row.sideEffectType : "";
30369
+ if (destination !== "") out.destination = destinationName(destination);
30370
+ const keys = rowMatchKeys(row.userData);
30371
+ if (keys.length > 0) out.matchedBy = keys.map((key) => key.field);
30372
+ return out;
30373
+ }
30374
+ function shapeFeed(stream, everything) {
30375
+ if (!Array.isArray(stream)) return { rows: [], heldBack: 0 };
30376
+ const rows = stream.filter(isStreamRow);
30377
+ const kept = everything ? rows : rows.filter((row) => !isBookkeepingRow({ eventType: row.eventType ?? "", triggerId: row.triggerId ?? "" }));
30378
+ return { rows: kept.map(enrich), heldBack: rows.length - kept.length };
30379
+ }
30380
+ function feedHints(shape) {
30381
+ const hints2 = [];
30382
+ if (shape.heldBack > 0) {
30383
+ hints2.push(
30384
+ `${shape.heldBack} ${shape.heldBack === 1 ? "row is" : "rows are"} not listed: rows recorded to work the reports out \u2014 identifications, step views, session starts, a destination's answer already folded onto its dispatch \u2014 and embedded forms and calendars announcing that they drew. Add --everything to see them.`
30385
+ );
30386
+ }
30387
+ const upload = shape.rows.find((row) => row.sideEffectType !== void 0 && row.eventId !== void 0);
30388
+ if (upload !== void 0) {
30389
+ hints2.push(
30390
+ `A delivery row carries the event it was made for in \`eventId\`. \`baker analytics feed --uid ${String(upload.eventId)}\` returns that event and every upload made for it, each with the platform's verdict.`
30391
+ );
30392
+ }
30393
+ return hints2;
30394
+ }
30395
+
30124
30396
  // src/commands/analytics/flowConversionKeys.ts
30125
30397
  function formKeyRefusal(eventKey, tree, slug) {
30126
30398
  const parsed = parseEventKey(eventKey);
@@ -30752,10 +31024,10 @@ function sendsWrite(args4) {
30752
31024
  return parsed.data;
30753
31025
  }
30754
31026
  function sendsView(response, full) {
30755
- const { carries, templateFields, ...rest } = response.data;
31027
+ const { carries: carries2, templateFields, ...rest } = response.data;
30756
31028
  return {
30757
31029
  ok: true,
30758
- data: { ...rest, ...full ? { carries, templateFields } : {} },
31030
+ data: { ...rest, ...full ? { carries: carries2, templateFields } : {} },
30759
31031
  ...response.hints && response.hints.length > 0 ? { hints: response.hints } : {}
30760
31032
  };
30761
31033
  }
@@ -31009,6 +31281,7 @@ import { z as z36 } from "zod";
31009
31281
  var SOURCE_KINDS = ["webhook", "hubspot"];
31010
31282
  var SOURCE_STATUSES = ["learning", "counting", "paused"];
31011
31283
  var SOURCE_NAME_MAX = 80;
31284
+ var sourceNameSchema = z36.string().trim().min(1).max(SOURCE_NAME_MAX).refine((name) => validateEventName(name) === null, REJECTION_MESSAGES.invalid_name);
31012
31285
  var sourceIdSchema = z36.string().min(1);
31013
31286
  var sourceMappingSchema = z36.record(z36.string(), z36.string().min(1)).superRefine((mapping, ctx) => {
31014
31287
  for (const row of Object.keys(mapping)) {
@@ -31084,10 +31357,10 @@ var hubspotPreviewSchema = z36.strictObject({
31084
31357
  var sourcesRequestSchema = z36.strictObject({
31085
31358
  /** The company's own server door: made once, `counting` from the start, the same address for ever after. */
31086
31359
  server: z36.strictObject({}).optional(),
31087
- create: z36.strictObject({ name: z36.string().trim().min(1).max(SOURCE_NAME_MAX) }).optional(),
31360
+ create: z36.strictObject({ name: sourceNameSchema }).optional(),
31088
31361
  /** A HubSpot event: read every five minutes, counting from now. The labels are looked up server-side. */
31089
31362
  createHubspot: z36.strictObject({
31090
- name: z36.string().trim().min(1).max(SOURCE_NAME_MAX).optional(),
31363
+ name: sourceNameSchema.optional(),
31091
31364
  /** A contact, or a deal. A deal's event is about its contact, read through `contact.<property>` rows. */
31092
31365
  object: hubspotObjectSchema.default("contact"),
31093
31366
  trigger: hubspotTriggerInputSchema,
@@ -31113,11 +31386,46 @@ var sourcesRequestSchema = z36.strictObject({
31113
31386
  amountProperty: z36.string().trim().min(1).optional(),
31114
31387
  currency: currencyCodeSchema.optional()
31115
31388
  }).optional(),
31389
+ /**
31390
+ * Count a HubSpot event's history: every moment its trigger came to hold
31391
+ * since `since`, read out of HubSpot's own property history.
31392
+ *
31393
+ * Not the thing a source deliberately refuses to do. A source never
31394
+ * backfills the **present state** — somebody who already satisfies a
31395
+ * condition is not an event today — and this reports only moments HubSpot
31396
+ * has a timestamp for, so nothing is invented. Dry unless `write`.
31397
+ */
31398
+ backfill: z36.strictObject({
31399
+ sourceId: sourceIdSchema,
31400
+ /** ISO instant, or Unix milliseconds. Bounded by the 400 days the store keeps. */
31401
+ since: z36.union([z36.string().trim().min(1), z36.number()]),
31402
+ /**
31403
+ * Where a capped run carries on from: only records last changed at or
31404
+ * before this instant are read.
31405
+ *
31406
+ * The **read** is what it bounds, never the history. Records are chosen
31407
+ * most-recently-changed first, so a run that reaches its cap has covered
31408
+ * the newest of them and the rest are the ones nearest `since` — which
31409
+ * no shorter window can reach, because shortening one only drops
31410
+ * candidates from the old end and hands back the same newest records
31411
+ * again. `reachedBack` is the value to pass here, and the moments each
31412
+ * record is then judged on are still the whole of `since` to now.
31413
+ */
31414
+ before: z36.union([z36.string().trim().min(1), z36.number()]).optional(),
31415
+ /** Without it, nothing is counted and the answer says what would be. */
31416
+ write: z36.boolean().optional()
31417
+ }).optional(),
31116
31418
  map: z36.strictObject({ sourceId: sourceIdSchema, mapping: sourceMappingSchema }).optional(),
31117
31419
  pause: z36.strictObject({ sourceId: sourceIdSchema }).optional(),
31118
31420
  resume: z36.strictObject({ sourceId: sourceIdSchema }).optional(),
31119
31421
  remove: z36.strictObject({ sourceId: sourceIdSchema }).optional()
31120
31422
  });
31423
+ var sourceRefusalSchema = z36.strictObject({
31424
+ at: z36.number(),
31425
+ count: z36.number(),
31426
+ code: z36.string(),
31427
+ reason: z36.string()
31428
+ });
31121
31429
  var detectedFieldSchema = z36.strictObject({
31122
31430
  path: z36.string(),
31123
31431
  confidence: z36.enum(["key", "value"])
@@ -31145,6 +31453,14 @@ var sourceViewSchema = z36.strictObject({
31145
31453
  /** Why the last delivery could not be counted, in product words; absent when the last one was. */
31146
31454
  lastError: z36.string().optional(),
31147
31455
  lastArrivalAt: z36.number().optional(),
31456
+ /**
31457
+ * What its last delivery **refused, event by event** — which `lastError` is
31458
+ * not and `lastArrivalAt` cannot be. A source refusing everything it
31459
+ * receives has no arrival and no read failure, so without this it reads as
31460
+ * one nobody has sent anything to. `refusalWords` turns it into the
31461
+ * sentence a person is shown. Cleared by the next delivery that counts.
31462
+ */
31463
+ lastRefusal: sourceRefusalSchema.optional(),
31148
31464
  /** The first send, masked, while learning. Kept once confirmed. */
31149
31465
  sample: z36.strictObject({ receivedAt: z36.number(), body: z36.string() }).optional(),
31150
31466
  /** Field → template; empty means the guess (an address) or the defaults (a HubSpot read). */
@@ -31166,12 +31482,37 @@ var stagedSourceOpSchema = z36.strictObject({
31166
31482
  mapping: z36.record(z36.string(), z36.string()).optional(),
31167
31483
  status: z36.enum(["paused", "counting"]).optional()
31168
31484
  });
31485
+ var backfillResultSchema = z36.strictObject({
31486
+ sourceId: z36.string(),
31487
+ name: z36.string(),
31488
+ since: z36.number(),
31489
+ /** Records whose history was read. */
31490
+ examined: z36.number(),
31491
+ /** Moments the trigger came to hold, across them. */
31492
+ entries: z36.number(),
31493
+ /** Of those, already counted — the journal got there first, or a previous run did. */
31494
+ alreadyCounted: z36.number(),
31495
+ /** What a `write` run would hand over, or did. */
31496
+ pending: z36.number(),
31497
+ /** Whether the read stopped at its cap; {@link BackfillResult.reachedBack} is where to carry on from. */
31498
+ capped: z36.boolean(),
31499
+ /**
31500
+ * How far back the read got: the instant the oldest record it examined was
31501
+ * last changed. Only on a capped run, and it is the next run's `before`.
31502
+ */
31503
+ reachedBack: z36.number().optional(),
31504
+ counted: z36.number().optional(),
31505
+ refused: z36.number().optional(),
31506
+ refusedReason: z36.string().optional()
31507
+ });
31169
31508
  var sourcesResponseSchema = z36.strictObject({
31170
31509
  ok: z36.literal(true),
31171
31510
  data: z36.strictObject({
31172
31511
  sources: z36.array(sourceViewSchema),
31173
31512
  /** Only when the request carried `preview`. */
31174
31513
  preview: hubspotPreviewSchema.optional(),
31514
+ /** Only when the request carried `backfill`. */
31515
+ backfill: backfillResultSchema.optional(),
31175
31516
  /** What this chat has staged, when one is attached: applied at Publish, dropped with a discard. */
31176
31517
  staged: z36.array(stagedSourceOpSchema).optional()
31177
31518
  }),
@@ -31282,6 +31623,26 @@ var args2 = {
31282
31623
  description: "Change where a HubSpot-read source takes its amount from. Needs --source; with --amount-property and --currency it reads them, with neither it stops reading an amount",
31283
31624
  required: false
31284
31625
  },
31626
+ backfill: {
31627
+ type: "boolean",
31628
+ description: "Count a HubSpot event's history: every moment its trigger came to hold since --since, at the instant HubSpot says it did. Needs --source and --since. Nothing is invented \u2014 a record that merely satisfies the trigger today is not an event today, and only moments HubSpot has a timestamp for are counted. Dry without --write. Reach for it when an event was made after the deals it should have counted, or after fixing one that was refusing everything it received",
31629
+ required: false
31630
+ },
31631
+ since: {
31632
+ type: "string",
31633
+ description: "With --backfill: the date to count this event's history from, `2026-09-19` or an ISO instant. At most 400 days back \u2014 the store keeps no more",
31634
+ required: false
31635
+ },
31636
+ before: {
31637
+ type: "string",
31638
+ description: "With --backfill: carry on from where a capped run stopped, by passing back the `reachedBack` it returned. One run reads at most 500 records, most recently changed first, so a capped run has covered the newest and the rest are the ones nearest --since \u2014 narrowing --since cannot reach them, this can. Repeat until `capped` is false; nothing is counted twice. It bounds which records are read, never which of their moments count: those are still the whole of --since to now",
31639
+ required: false
31640
+ },
31641
+ write: {
31642
+ type: "boolean",
31643
+ description: "With --backfill: actually count what it found. Without it nothing is counted and the answer says what would be. Events inside the last 72 hours are uploaded to this event's ad platforms within minutes of counting, which spends real budget",
31644
+ required: false
31645
+ },
31285
31646
  name: {
31286
31647
  type: "string",
31287
31648
  description: "The tool the source is for \u2014 HubSpot, Zapier, Stripe. It is the event's name when the payload does not say one, and every event it sends carries it as `baker_source`",
@@ -31445,14 +31806,37 @@ var VERBS = [
31445
31806
  "create-hubspot",
31446
31807
  "set-trigger",
31447
31808
  "preview",
31809
+ "backfill",
31448
31810
  "set-amount",
31449
31811
  "map",
31450
31812
  "pause",
31451
31813
  "resume",
31452
31814
  "remove"
31453
31815
  ];
31454
- function sourcesRequest(raw, argv) {
31816
+ function checkedName(raw, argv) {
31455
31817
  const name = onlyOnce(raw.name, "--name", argv);
31818
+ if (name === void 0 || validateEventName(name) === null) return name;
31819
+ throw new ConversionArgsError(
31820
+ `"${name}" cannot be an event name, and the name is what every event this source raises will carry. ${REJECTION_MESSAGES.invalid_name}`
31821
+ );
31822
+ }
31823
+ function backfillArgs(raw, argv, sourceId) {
31824
+ const since = onlyOnce(raw.since, "--since", argv);
31825
+ if (since === void 0) {
31826
+ throw new ConversionArgsError(
31827
+ "--backfill needs --since <date>, e.g. --since 2026-09-19. Nothing before it is read."
31828
+ );
31829
+ }
31830
+ const before = onlyOnce(raw.before, "--before", argv);
31831
+ return {
31832
+ sourceId,
31833
+ since,
31834
+ ...before === void 0 ? {} : { before },
31835
+ ...raw.write === true ? { write: true } : {}
31836
+ };
31837
+ }
31838
+ function sourcesRequest(raw, argv) {
31839
+ const name = checkedName(raw, argv);
31456
31840
  const source = onlyOnce(raw.source, "--source", argv);
31457
31841
  const verbs = VERBS.filter((verb2) => raw[verb2] === true);
31458
31842
  if (verbs.length > 1) throw new ConversionArgsError(`One of --${verbs.join(", --")} per call.`);
@@ -31466,6 +31850,7 @@ function sourcesRequest(raw, argv) {
31466
31850
  if (verb === "create-hubspot") return { createHubspot: createHubspotArgs(raw, argv, name) };
31467
31851
  if (verb === "preview") return { preview: previewArgs(raw, argv) };
31468
31852
  if (source === void 0) throw new ConversionArgsError(`--${verb} needs --source <id>, from the list.`);
31853
+ if (verb === "backfill") return { backfill: backfillArgs(raw, argv, source) };
31469
31854
  if (verb === "set-trigger") {
31470
31855
  const contacts = contactsArg(raw, argv);
31471
31856
  return {
@@ -31754,7 +32139,10 @@ async function runPreset(args4, options) {
31754
32139
  process.exit(1);
31755
32140
  }
31756
32141
  const data = pruneForAgent(response.data, args4.full === true);
32142
+ const feed = "stream" in data ? shapeFeed(data.stream, args4.everything === true) : void 0;
32143
+ if (feed !== void 0) data.stream = feed.rows;
31757
32144
  const hints2 = [
32145
+ ...feed === void 0 ? [] : feedHints(feed),
31758
32146
  // The platform the ad blocks were scoped to, so the tagging hint can
31759
32147
  // only speak about the inventory it was actually given. `adParams` is one
31760
32148
  // platform's list; a hint drawn from it without knowing which platform
@@ -31984,7 +32372,8 @@ function feedNarrowing(args4) {
31984
32372
  ...args4.kind ? { eventKey: String(args4.kind) } : {},
31985
32373
  ...args4.send ? { sendId: String(args4.send) } : {},
31986
32374
  ...args4["sent-to"] ? { sentTo: String(args4["sent-to"]) } : {},
31987
- ...args4.uid ? { eventUid: String(args4.uid) } : {}
32375
+ ...args4.uid ? { eventUid: String(args4.uid) } : {},
32376
+ ...args4.person ? { anonId: String(args4.person) } : {}
31988
32377
  };
31989
32378
  }
31990
32379
  var feedCommand = presetCommand({
@@ -32026,6 +32415,16 @@ var feedCommand = presetCommand({
32026
32415
  type: "string",
32027
32416
  description: "One kind of event, by the key `baker analytics conversions` lists (page:Deal won, form:contact:\u2026)",
32028
32417
  required: false
32418
+ },
32419
+ person: {
32420
+ type: "string",
32421
+ description: "One person and everything they did, seeded by a browser id from the `anonId` on any row. A seed rather than a cookie filter: the store resolves it to the whole person \u2014 across devices, and across every address, phone, customer id, ad click and platform cookie ever seen beside that browser \u2014 so a deal their CRM posted, which carries no cookie at all, is on the list beside the page view that named them",
32422
+ required: false
32423
+ },
32424
+ everything: {
32425
+ type: "boolean",
32426
+ description: "Include the rows held back by default: the ones recorded so the reports can be worked out (identifications, step views, session starts, a destination's answer already folded onto its dispatch) and embedded forms and calendars announcing that they drew. Reach for it when debugging the collector rather than an integration \u2014 an embed that draws and never fires again is only visible here",
32427
+ required: false
32029
32428
  }
32030
32429
  },
32031
32430
  resolve: (args4) => feedRequest(args4)