@cavuno/board 4.32.0 → 4.34.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/dist/{_spec-BdqHycDm.d.mts → _spec-Cz-yijWv.d.mts} +20 -4
  3. package/dist/{_spec-BdqHycDm.d.ts → _spec-Cz-yijWv.d.ts} +20 -4
  4. package/dist/analytics.d.mts +44 -2
  5. package/dist/analytics.d.ts +44 -2
  6. package/dist/analytics.js +57 -1
  7. package/dist/analytics.mjs +57 -1
  8. package/dist/{blog-D85cD8W0.d.ts → blog-BtZXryRt.d.ts} +1 -1
  9. package/dist/{blog-CE0g5csE.d.mts → blog-CA6EEkdC.d.mts} +1 -1
  10. package/dist/{board-CdG7z-Vq.d.mts → board-BKUGfCIT.d.mts} +2 -2
  11. package/dist/{board-CEsr8xMh.d.ts → board-Dlv-nGl3.d.ts} +2 -2
  12. package/dist/{breadcrumbs-DmVbf4_6.d.ts → breadcrumbs-D9p_vb0h.d.ts} +2 -2
  13. package/dist/{breadcrumbs-B3n9k95M.d.mts → breadcrumbs-w0szfoX0.d.mts} +2 -2
  14. package/dist/browser/cavuno-board.global.min.js +1 -1
  15. package/dist/filters.d.mts +2 -2
  16. package/dist/filters.d.ts +2 -2
  17. package/dist/format.d.mts +4 -4
  18. package/dist/format.d.ts +4 -4
  19. package/dist/index.d.mts +27 -18
  20. package/dist/index.d.ts +27 -18
  21. package/dist/index.js +1 -1
  22. package/dist/index.mjs +1 -1
  23. package/dist/{jobs-DbZuagy-.d.ts → jobs-D0OqM6IS.d.ts} +1 -1
  24. package/dist/{jobs-COOBWXWV.d.mts → jobs-DCeOWaPk.d.mts} +1 -1
  25. package/dist/{search-BapnDodc.d.mts → search-DCRz5E-6.d.mts} +1 -1
  26. package/dist/{search-BV2B73ty.d.ts → search-pH_zLJ23.d.ts} +1 -1
  27. package/dist/{seo-BcC8Tv0d.d.ts → seo-CO1235tn.d.ts} +1 -1
  28. package/dist/{seo-C_yEotTb.d.mts → seo-CRJDxQMf.d.mts} +1 -1
  29. package/dist/seo-files.d.mts +2 -2
  30. package/dist/seo-files.d.ts +2 -2
  31. package/dist/seo.d.mts +6 -6
  32. package/dist/seo.d.ts +6 -6
  33. package/dist/server.d.mts +6 -6
  34. package/dist/server.d.ts +6 -6
  35. package/dist/sitemap.d.mts +6 -6
  36. package/dist/sitemap.d.ts +6 -6
  37. package/dist/suggest.d.mts +2 -2
  38. package/dist/suggest.d.ts +2 -2
  39. package/package.json +1 -1
  40. package/skills/manifest.json +1 -1
package/CHANGELOG.md CHANGED
@@ -3,6 +3,37 @@
3
3
  This changelog records changes that affect Board API compatibility, exported
4
4
  types, runtime behavior, or supported integration patterns.
5
5
 
6
+ ## 4.34.0 — 2026-10-05
7
+
8
+ - Added `analytics.recordConsent` to record a visitor's cookie-banner choice
9
+ (accepted, denied, or withdrawn) as proof of consent. It works without
10
+ `install` and sets no cookies.
11
+
12
+ - The default analytics script URL points at a new tracker version. On boards
13
+ that require cookie consent, load the tracker with `analytics.install()`
14
+ only after the visitor accepts.
15
+
16
+ - Jobs can have the status `pending_approval`: the job is waiting for an
17
+ operator to approve it and is not public, like a draft. Approve it with
18
+ `POST /v1/jobs/:id/publish` and reject it with `DELETE /v1/jobs/:id`.
19
+ Code that switches on job status should handle the new value.
20
+
21
+ - Public company responses include `customFieldMedia`, which resolves files
22
+ in public image gallery and file custom fields to
23
+ `{ id, name, contentType, sizeBytes, url }`, keyed by field key. Use `url`
24
+ to render them.
25
+
26
+ ## 4.33.0 — 2026-10-04
27
+
28
+ - The default analytics script URL now includes a version query, so browsers
29
+ load tracker updates instead of reusing a cached copy for up to a week.
30
+ Boards that pass their own `scriptUrl` are unaffected.
31
+
32
+ - The analytics tracker reports the visitor's raw browser timezone, a small
33
+ set of automation hints, and one `engagement` event per page view on the
34
+ first click, key press, touch, or scroll. These feed bot filtering in board
35
+ analytics; no new configuration is needed.
36
+
6
37
  ## 4.32.0 — 2026-10-02
7
38
 
8
39
  - Registration accepts an optional `returnTo`, a same-origin path carried
@@ -704,6 +704,10 @@ interface components {
704
704
  customFieldValues: {
705
705
  [key: string]: string | number | boolean | string[];
706
706
  };
707
+ /** @description Resolved files for public `image_gallery` and `file` company custom fields, keyed by field key like `customFieldValues`, in the stored order. Each file `id` matches a value in `customFieldValues`. Use `url` to render: published images are permanent public asset URLs (e.g. `https://assets.cavuno.com/...`); other files, and images still being published, use a signed download URL that expires after at least 24 hours. Fields with no files are omitted. */
708
+ customFieldMedia: {
709
+ [key: string]: components["schemas"]["ProfileFieldMedia"][];
710
+ };
707
711
  /** @description Resolved public object references attached to this company. Private references are omitted. */
708
712
  objectReferences: components["schemas"]["ResolvedProfileObjectReference"][];
709
713
  links: components["schemas"]["PublicCompanyLinks"];
@@ -1425,10 +1429,10 @@ interface components {
1425
1429
  /** @description URL-friendly slug used in public board URLs, or `null` if no slug is set. */
1426
1430
  slug: string | null;
1427
1431
  /**
1428
- * @description Current status of the job. One of `draft`, `published`, `expired`, or `archived`.
1432
+ * @description Current status of the job. One of `draft`, `published`, `expired`, `archived`, or `pending_approval`. A `pending_approval` job is waiting for an operator to approve it: approve with `POST /v1/jobs/:id/publish`, reject with `DELETE /v1/jobs/:id`. It is not public, like a draft.
1429
1433
  * @enum {string}
1430
1434
  */
1431
- status: "draft" | "published" | "expired" | "archived";
1435
+ status: "draft" | "published" | "expired" | "archived" | "pending_approval";
1432
1436
  /** @description Identifier of the company the job belongs to, or `null` if no company is attached. */
1433
1437
  companyId: string | null;
1434
1438
  /**
@@ -2130,6 +2134,18 @@ interface components {
2130
2134
  min?: number;
2131
2135
  max?: number;
2132
2136
  };
2137
+ ProfileFieldMedia: {
2138
+ /** @description File identifier. Matches the value stored for the field in `customFieldValues`. */
2139
+ id: string;
2140
+ /** @description Original file name. */
2141
+ name: string;
2142
+ /** @description MIME type of the file. */
2143
+ contentType: string;
2144
+ /** @description Size of the file, in bytes. */
2145
+ sizeBytes: number;
2146
+ /** @description Absolute, browser-loadable URL. Published images are permanent public asset URLs (e.g. `https://assets.cavuno.com/...`) safe to cache and hotlink. Other files, and an image saved moments ago that is still being published, use a signed download URL that expires after at least 24 hours and works on password-protected boards without a password grant, so read the URL from the response rather than storing it. */
2147
+ url: string;
2148
+ };
2133
2149
  ProfileFieldValuesBody: {
2134
2150
  values: {
2135
2151
  [key: string]: string | number | boolean | string[] | unknown;
@@ -2461,10 +2477,10 @@ interface components {
2461
2477
  /** @description URL-friendly slug used in public board URLs, or `null` if no slug is set. */
2462
2478
  slug: string | null;
2463
2479
  /**
2464
- * @description Current status of the job. One of `draft`, `published`, `expired`, or `archived`.
2480
+ * @description Current status of the job. One of `draft`, `published`, `expired`, `archived`, or `pending_approval`. A `pending_approval` job is waiting for an operator to approve it: approve with `POST /v1/jobs/:id/publish`, reject with `DELETE /v1/jobs/:id`. It is not public, like a draft.
2465
2481
  * @enum {string}
2466
2482
  */
2467
- status: "draft" | "published" | "expired" | "archived";
2483
+ status: "draft" | "published" | "expired" | "archived" | "pending_approval";
2468
2484
  /** @description Identifier of the company the job belongs to, or `null` if no company is attached. */
2469
2485
  companyId: string | null;
2470
2486
  /**
@@ -704,6 +704,10 @@ interface components {
704
704
  customFieldValues: {
705
705
  [key: string]: string | number | boolean | string[];
706
706
  };
707
+ /** @description Resolved files for public `image_gallery` and `file` company custom fields, keyed by field key like `customFieldValues`, in the stored order. Each file `id` matches a value in `customFieldValues`. Use `url` to render: published images are permanent public asset URLs (e.g. `https://assets.cavuno.com/...`); other files, and images still being published, use a signed download URL that expires after at least 24 hours. Fields with no files are omitted. */
708
+ customFieldMedia: {
709
+ [key: string]: components["schemas"]["ProfileFieldMedia"][];
710
+ };
707
711
  /** @description Resolved public object references attached to this company. Private references are omitted. */
708
712
  objectReferences: components["schemas"]["ResolvedProfileObjectReference"][];
709
713
  links: components["schemas"]["PublicCompanyLinks"];
@@ -1425,10 +1429,10 @@ interface components {
1425
1429
  /** @description URL-friendly slug used in public board URLs, or `null` if no slug is set. */
1426
1430
  slug: string | null;
1427
1431
  /**
1428
- * @description Current status of the job. One of `draft`, `published`, `expired`, or `archived`.
1432
+ * @description Current status of the job. One of `draft`, `published`, `expired`, `archived`, or `pending_approval`. A `pending_approval` job is waiting for an operator to approve it: approve with `POST /v1/jobs/:id/publish`, reject with `DELETE /v1/jobs/:id`. It is not public, like a draft.
1429
1433
  * @enum {string}
1430
1434
  */
1431
- status: "draft" | "published" | "expired" | "archived";
1435
+ status: "draft" | "published" | "expired" | "archived" | "pending_approval";
1432
1436
  /** @description Identifier of the company the job belongs to, or `null` if no company is attached. */
1433
1437
  companyId: string | null;
1434
1438
  /**
@@ -2130,6 +2134,18 @@ interface components {
2130
2134
  min?: number;
2131
2135
  max?: number;
2132
2136
  };
2137
+ ProfileFieldMedia: {
2138
+ /** @description File identifier. Matches the value stored for the field in `customFieldValues`. */
2139
+ id: string;
2140
+ /** @description Original file name. */
2141
+ name: string;
2142
+ /** @description MIME type of the file. */
2143
+ contentType: string;
2144
+ /** @description Size of the file, in bytes. */
2145
+ sizeBytes: number;
2146
+ /** @description Absolute, browser-loadable URL. Published images are permanent public asset URLs (e.g. `https://assets.cavuno.com/...`) safe to cache and hotlink. Other files, and an image saved moments ago that is still being published, use a signed download URL that expires after at least 24 hours and works on password-protected boards without a password grant, so read the URL from the response rather than storing it. */
2147
+ url: string;
2148
+ };
2133
2149
  ProfileFieldValuesBody: {
2134
2150
  values: {
2135
2151
  [key: string]: string | number | boolean | string[] | unknown;
@@ -2461,10 +2477,10 @@ interface components {
2461
2477
  /** @description URL-friendly slug used in public board URLs, or `null` if no slug is set. */
2462
2478
  slug: string | null;
2463
2479
  /**
2464
- * @description Current status of the job. One of `draft`, `published`, `expired`, or `archived`.
2480
+ * @description Current status of the job. One of `draft`, `published`, `expired`, `archived`, or `pending_approval`. A `pending_approval` job is waiting for an operator to approve it: approve with `POST /v1/jobs/:id/publish`, reject with `DELETE /v1/jobs/:id`. It is not public, like a draft.
2465
2481
  * @enum {string}
2466
2482
  */
2467
- status: "draft" | "published" | "expired" | "archived";
2483
+ status: "draft" | "published" | "expired" | "archived" | "pending_approval";
2468
2484
  /** @description Identifier of the company the job belongs to, or `null` if no company is attached. */
2469
2485
  companyId: string | null;
2470
2486
  /**
@@ -1,5 +1,10 @@
1
1
  declare const DEFAULT_COLLECT_URL = "https://cavuno.com/api/analytics/collect";
2
- declare const DEFAULT_SCRIPT_URL = "https://cavuno.com/js/metrics.js";
2
+ /**
3
+ * Cavuno-hosted metrics script. The `v` query names the script release, so a
4
+ * new release reaches returning visitors instead of waiting out their browser
5
+ * cache. The unversioned URL keeps serving the current script.
6
+ */
7
+ declare const DEFAULT_SCRIPT_URL = "https://cavuno.com/js/metrics.js?v=1.7.1-cavuno.2";
3
8
  /** Placeholder tenant; collect rewrites from the publishable key. */
4
9
  declare const PENDING_TENANT_ID = "boards_pending";
5
10
  declare const WELL_KNOWN_ANALYTICS_SCRIPT_PATH = "/.well-known/cavuno/analytics.js";
@@ -35,6 +40,12 @@ type AnalyticsInstallOptions = {
35
40
  /**
36
41
  * Load the Cavuno-hosted metrics script and remember collect credentials.
37
42
  * Safe to call once per page; subsequent calls update collect targets only.
43
+ *
44
+ * The script sets a first-party `session-id` cookie (a random identifier
45
+ * that expires after 30 minutes without activity) and sends page view,
46
+ * engagement, and web-vital events. Where your board requires cookie consent
47
+ * (`analytics.cookieConsentRequired` from `board.context()`), call `install`
48
+ * only after the visitor accepts. Until it runs, `track` sends nothing.
38
49
  */
39
50
  declare function install(options: AnalyticsInstallOptions): void;
40
51
  /**
@@ -42,9 +53,40 @@ declare function install(options: AnalyticsInstallOptions): void;
42
53
  * Prefer `install` first so pageviews and vitals are also recorded.
43
54
  */
44
55
  declare function track(action: string, payload?: Record<string, unknown>): void;
56
+ /** A visitor's answer to the cookie banner, as recorded by `recordConsent`. */
57
+ type ConsentChoice = 'accepted' | 'denied' | 'withdrawn';
58
+ type RecordConsentInput = {
59
+ /** The board's publishable key (`pk_…`), the same one `install` takes. */
60
+ publishableKey: string;
61
+ /**
62
+ * Random UUID the banner generates once and keeps in its own consent
63
+ * cookie, so every choice from the same browser shares one id.
64
+ */
65
+ consentId: string;
66
+ choice: ConsentChoice;
67
+ /** Identifies the banner text the visitor saw. 1–128 characters. */
68
+ bannerVersion: string;
69
+ /** Default: Cavuno central collect, as for `install`. */
70
+ collectUrl?: string;
71
+ };
72
+ /**
73
+ * Record a cookie-banner choice as proof of consent. Call it on every
74
+ * Accept, Decline and withdrawal.
75
+ *
76
+ * Works without `install`, so a Decline is recorded even though analytics
77
+ * never load. It reads and sets no cookie and sends no session id: the
78
+ * record holds only the consent id, the choice, the banner version, the
79
+ * board and the time it was received.
80
+ *
81
+ * Sends `POST {collectUrl}` with
82
+ * `{ publishableKey, action: 'consent', payload: { consent_id, choice, banner_version } }`.
83
+ * Throws on invalid input; network failures are swallowed.
84
+ */
85
+ declare function recordConsent(input: RecordConsentInput): void;
45
86
  declare const analytics: {
46
87
  install: typeof install;
47
88
  track: typeof track;
89
+ recordConsent: typeof recordConsent;
48
90
  matchAnalyticsWellKnown: typeof matchAnalyticsWellKnown;
49
91
  DEFAULT_COLLECT_URL: string;
50
92
  DEFAULT_SCRIPT_URL: string;
@@ -55,4 +97,4 @@ declare const analytics: {
55
97
  WELL_KNOWN_COLLECT_EVENTS_PATH: string;
56
98
  };
57
99
 
58
- export { type AnalyticsInstallOptions, DEFAULT_COLLECT_URL, DEFAULT_SCRIPT_URL, PENDING_TENANT_ID, WELL_KNOWN_ANALYTICS_SCRIPT_PATH, WELL_KNOWN_COLLECT_EVENTS_PATH, WELL_KNOWN_COLLECT_PATH, WELL_KNOWN_SCRIPT_PATH, analytics, install, matchAnalyticsWellKnown, track };
100
+ export { type AnalyticsInstallOptions, type ConsentChoice, DEFAULT_COLLECT_URL, DEFAULT_SCRIPT_URL, PENDING_TENANT_ID, type RecordConsentInput, WELL_KNOWN_ANALYTICS_SCRIPT_PATH, WELL_KNOWN_COLLECT_EVENTS_PATH, WELL_KNOWN_COLLECT_PATH, WELL_KNOWN_SCRIPT_PATH, analytics, install, matchAnalyticsWellKnown, recordConsent, track };
@@ -1,5 +1,10 @@
1
1
  declare const DEFAULT_COLLECT_URL = "https://cavuno.com/api/analytics/collect";
2
- declare const DEFAULT_SCRIPT_URL = "https://cavuno.com/js/metrics.js";
2
+ /**
3
+ * Cavuno-hosted metrics script. The `v` query names the script release, so a
4
+ * new release reaches returning visitors instead of waiting out their browser
5
+ * cache. The unversioned URL keeps serving the current script.
6
+ */
7
+ declare const DEFAULT_SCRIPT_URL = "https://cavuno.com/js/metrics.js?v=1.7.1-cavuno.2";
3
8
  /** Placeholder tenant; collect rewrites from the publishable key. */
4
9
  declare const PENDING_TENANT_ID = "boards_pending";
5
10
  declare const WELL_KNOWN_ANALYTICS_SCRIPT_PATH = "/.well-known/cavuno/analytics.js";
@@ -35,6 +40,12 @@ type AnalyticsInstallOptions = {
35
40
  /**
36
41
  * Load the Cavuno-hosted metrics script and remember collect credentials.
37
42
  * Safe to call once per page; subsequent calls update collect targets only.
43
+ *
44
+ * The script sets a first-party `session-id` cookie (a random identifier
45
+ * that expires after 30 minutes without activity) and sends page view,
46
+ * engagement, and web-vital events. Where your board requires cookie consent
47
+ * (`analytics.cookieConsentRequired` from `board.context()`), call `install`
48
+ * only after the visitor accepts. Until it runs, `track` sends nothing.
38
49
  */
39
50
  declare function install(options: AnalyticsInstallOptions): void;
40
51
  /**
@@ -42,9 +53,40 @@ declare function install(options: AnalyticsInstallOptions): void;
42
53
  * Prefer `install` first so pageviews and vitals are also recorded.
43
54
  */
44
55
  declare function track(action: string, payload?: Record<string, unknown>): void;
56
+ /** A visitor's answer to the cookie banner, as recorded by `recordConsent`. */
57
+ type ConsentChoice = 'accepted' | 'denied' | 'withdrawn';
58
+ type RecordConsentInput = {
59
+ /** The board's publishable key (`pk_…`), the same one `install` takes. */
60
+ publishableKey: string;
61
+ /**
62
+ * Random UUID the banner generates once and keeps in its own consent
63
+ * cookie, so every choice from the same browser shares one id.
64
+ */
65
+ consentId: string;
66
+ choice: ConsentChoice;
67
+ /** Identifies the banner text the visitor saw. 1–128 characters. */
68
+ bannerVersion: string;
69
+ /** Default: Cavuno central collect, as for `install`. */
70
+ collectUrl?: string;
71
+ };
72
+ /**
73
+ * Record a cookie-banner choice as proof of consent. Call it on every
74
+ * Accept, Decline and withdrawal.
75
+ *
76
+ * Works without `install`, so a Decline is recorded even though analytics
77
+ * never load. It reads and sets no cookie and sends no session id: the
78
+ * record holds only the consent id, the choice, the banner version, the
79
+ * board and the time it was received.
80
+ *
81
+ * Sends `POST {collectUrl}` with
82
+ * `{ publishableKey, action: 'consent', payload: { consent_id, choice, banner_version } }`.
83
+ * Throws on invalid input; network failures are swallowed.
84
+ */
85
+ declare function recordConsent(input: RecordConsentInput): void;
45
86
  declare const analytics: {
46
87
  install: typeof install;
47
88
  track: typeof track;
89
+ recordConsent: typeof recordConsent;
48
90
  matchAnalyticsWellKnown: typeof matchAnalyticsWellKnown;
49
91
  DEFAULT_COLLECT_URL: string;
50
92
  DEFAULT_SCRIPT_URL: string;
@@ -55,4 +97,4 @@ declare const analytics: {
55
97
  WELL_KNOWN_COLLECT_EVENTS_PATH: string;
56
98
  };
57
99
 
58
- export { type AnalyticsInstallOptions, DEFAULT_COLLECT_URL, DEFAULT_SCRIPT_URL, PENDING_TENANT_ID, WELL_KNOWN_ANALYTICS_SCRIPT_PATH, WELL_KNOWN_COLLECT_EVENTS_PATH, WELL_KNOWN_COLLECT_PATH, WELL_KNOWN_SCRIPT_PATH, analytics, install, matchAnalyticsWellKnown, track };
100
+ export { type AnalyticsInstallOptions, type ConsentChoice, DEFAULT_COLLECT_URL, DEFAULT_SCRIPT_URL, PENDING_TENANT_ID, type RecordConsentInput, WELL_KNOWN_ANALYTICS_SCRIPT_PATH, WELL_KNOWN_COLLECT_EVENTS_PATH, WELL_KNOWN_COLLECT_PATH, WELL_KNOWN_SCRIPT_PATH, analytics, install, matchAnalyticsWellKnown, recordConsent, track };
package/dist/analytics.js CHANGED
@@ -30,13 +30,14 @@ __export(analytics_exports, {
30
30
  analytics: () => analytics,
31
31
  install: () => install,
32
32
  matchAnalyticsWellKnown: () => matchAnalyticsWellKnown,
33
+ recordConsent: () => recordConsent,
33
34
  track: () => track
34
35
  });
35
36
  module.exports = __toCommonJS(analytics_exports);
36
37
 
37
38
  // src/analytics/defaults.ts
38
39
  var DEFAULT_COLLECT_URL = "https://cavuno.com/api/analytics/collect";
39
- var DEFAULT_SCRIPT_URL = "https://cavuno.com/js/metrics.js";
40
+ var DEFAULT_SCRIPT_URL = "https://cavuno.com/js/metrics.js?v=1.7.1-cavuno.2";
40
41
  var PENDING_TENANT_ID = "boards_pending";
41
42
  var WELL_KNOWN_ANALYTICS_SCRIPT_PATH = "/.well-known/cavuno/analytics.js";
42
43
  var WELL_KNOWN_SCRIPT_PATH = WELL_KNOWN_ANALYTICS_SCRIPT_PATH;
@@ -210,9 +211,64 @@ function track(action, payload) {
210
211
  }
211
212
  postCollectJson(installed, trimmed, payload);
212
213
  }
214
+ var CONSENT_CHOICES = [
215
+ "accepted",
216
+ "denied",
217
+ "withdrawn"
218
+ ];
219
+ var CONSENT_ID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
220
+ var BANNER_VERSION_MAX_LENGTH = 128;
221
+ function recordConsent(input) {
222
+ const publishableKey = input.publishableKey.trim();
223
+ if (!publishableKey.startsWith("pk_")) {
224
+ throw new Error(
225
+ "analytics.recordConsent requires a publishable key (pk_\u2026)"
226
+ );
227
+ }
228
+ const consentId = input.consentId.trim().toLowerCase();
229
+ if (!CONSENT_ID_PATTERN.test(consentId)) {
230
+ throw new Error("analytics.recordConsent requires a UUID consentId");
231
+ }
232
+ if (!CONSENT_CHOICES.includes(input.choice)) {
233
+ throw new Error(
234
+ "analytics.recordConsent choice must be 'accepted', 'denied' or 'withdrawn'"
235
+ );
236
+ }
237
+ const bannerVersion = input.bannerVersion.trim();
238
+ if (bannerVersion.length === 0 || bannerVersion.length > BANNER_VERSION_MAX_LENGTH) {
239
+ throw new Error(
240
+ "analytics.recordConsent requires a bannerVersion of 1\u2013128 characters"
241
+ );
242
+ }
243
+ const collectUrl = normalizeCollectUrl(
244
+ (input.collectUrl ?? resolveDefaultCollectUrl()).trim() || resolveDefaultCollectUrl()
245
+ );
246
+ void fetch(collectUrl, {
247
+ method: "POST",
248
+ headers: {
249
+ Authorization: `Bearer ${publishableKey}`,
250
+ "Content-Type": "application/json",
251
+ Accept: "application/json"
252
+ },
253
+ body: JSON.stringify({
254
+ publishableKey,
255
+ action: "consent",
256
+ payload: {
257
+ consent_id: consentId,
258
+ choice: input.choice,
259
+ banner_version: bannerVersion
260
+ }
261
+ }),
262
+ // A consent record must not travel with cookies, first-party or not.
263
+ credentials: "omit",
264
+ keepalive: true
265
+ }).catch(() => {
266
+ });
267
+ }
213
268
  var analytics = {
214
269
  install,
215
270
  track,
271
+ recordConsent,
216
272
  matchAnalyticsWellKnown,
217
273
  DEFAULT_COLLECT_URL,
218
274
  DEFAULT_SCRIPT_URL,
@@ -1,6 +1,6 @@
1
1
  // src/analytics/defaults.ts
2
2
  var DEFAULT_COLLECT_URL = "https://cavuno.com/api/analytics/collect";
3
- var DEFAULT_SCRIPT_URL = "https://cavuno.com/js/metrics.js";
3
+ var DEFAULT_SCRIPT_URL = "https://cavuno.com/js/metrics.js?v=1.7.1-cavuno.2";
4
4
  var PENDING_TENANT_ID = "boards_pending";
5
5
  var WELL_KNOWN_ANALYTICS_SCRIPT_PATH = "/.well-known/cavuno/analytics.js";
6
6
  var WELL_KNOWN_SCRIPT_PATH = WELL_KNOWN_ANALYTICS_SCRIPT_PATH;
@@ -174,9 +174,64 @@ function track(action, payload) {
174
174
  }
175
175
  postCollectJson(installed, trimmed, payload);
176
176
  }
177
+ var CONSENT_CHOICES = [
178
+ "accepted",
179
+ "denied",
180
+ "withdrawn"
181
+ ];
182
+ var CONSENT_ID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
183
+ var BANNER_VERSION_MAX_LENGTH = 128;
184
+ function recordConsent(input) {
185
+ const publishableKey = input.publishableKey.trim();
186
+ if (!publishableKey.startsWith("pk_")) {
187
+ throw new Error(
188
+ "analytics.recordConsent requires a publishable key (pk_\u2026)"
189
+ );
190
+ }
191
+ const consentId = input.consentId.trim().toLowerCase();
192
+ if (!CONSENT_ID_PATTERN.test(consentId)) {
193
+ throw new Error("analytics.recordConsent requires a UUID consentId");
194
+ }
195
+ if (!CONSENT_CHOICES.includes(input.choice)) {
196
+ throw new Error(
197
+ "analytics.recordConsent choice must be 'accepted', 'denied' or 'withdrawn'"
198
+ );
199
+ }
200
+ const bannerVersion = input.bannerVersion.trim();
201
+ if (bannerVersion.length === 0 || bannerVersion.length > BANNER_VERSION_MAX_LENGTH) {
202
+ throw new Error(
203
+ "analytics.recordConsent requires a bannerVersion of 1\u2013128 characters"
204
+ );
205
+ }
206
+ const collectUrl = normalizeCollectUrl(
207
+ (input.collectUrl ?? resolveDefaultCollectUrl()).trim() || resolveDefaultCollectUrl()
208
+ );
209
+ void fetch(collectUrl, {
210
+ method: "POST",
211
+ headers: {
212
+ Authorization: `Bearer ${publishableKey}`,
213
+ "Content-Type": "application/json",
214
+ Accept: "application/json"
215
+ },
216
+ body: JSON.stringify({
217
+ publishableKey,
218
+ action: "consent",
219
+ payload: {
220
+ consent_id: consentId,
221
+ choice: input.choice,
222
+ banner_version: bannerVersion
223
+ }
224
+ }),
225
+ // A consent record must not travel with cookies, first-party or not.
226
+ credentials: "omit",
227
+ keepalive: true
228
+ }).catch(() => {
229
+ });
230
+ }
177
231
  var analytics = {
178
232
  install,
179
233
  track,
234
+ recordConsent,
180
235
  matchAnalyticsWellKnown,
181
236
  DEFAULT_COLLECT_URL,
182
237
  DEFAULT_SCRIPT_URL,
@@ -197,5 +252,6 @@ export {
197
252
  analytics,
198
253
  install,
199
254
  matchAnalyticsWellKnown,
255
+ recordConsent,
200
256
  track
201
257
  };
@@ -1,4 +1,4 @@
1
- import { S as Schemas } from './_spec-BdqHycDm.js';
1
+ import { S as Schemas } from './_spec-Cz-yijWv.js';
2
2
 
3
3
  /** Author shape embedded on posts (no `object` discriminator). */
4
4
  type BlogAuthorEmbed = Schemas['PublicBlogAuthorEmbed'];
@@ -1,4 +1,4 @@
1
- import { S as Schemas } from './_spec-BdqHycDm.mjs';
1
+ import { S as Schemas } from './_spec-Cz-yijWv.mjs';
2
2
 
3
3
  /** Author shape embedded on posts (no `object` discriminator). */
4
4
  type BlogAuthorEmbed = Schemas['PublicBlogAuthorEmbed'];
@@ -1,5 +1,5 @@
1
- import { S as Schemas } from './_spec-BdqHycDm.mjs';
2
- import { L as ListEnvelope, s as RelatedSearch, a as CustomFieldFilter, O as ObjectReferenceFilter } from './jobs-COOBWXWV.mjs';
1
+ import { S as Schemas } from './_spec-Cz-yijWv.mjs';
2
+ import { L as ListEnvelope, s as RelatedSearch, a as CustomFieldFilter, O as ObjectReferenceFilter } from './jobs-DCeOWaPk.mjs';
3
3
 
4
4
  type PublicCompany = Schemas['CompanyPublic'];
5
5
  /** A market (sector) a company operates in — name + its source slug. */
@@ -1,5 +1,5 @@
1
- import { S as Schemas } from './_spec-BdqHycDm.js';
2
- import { L as ListEnvelope, s as RelatedSearch, a as CustomFieldFilter, O as ObjectReferenceFilter } from './jobs-DbZuagy-.js';
1
+ import { S as Schemas } from './_spec-Cz-yijWv.js';
2
+ import { L as ListEnvelope, s as RelatedSearch, a as CustomFieldFilter, O as ObjectReferenceFilter } from './jobs-D0OqM6IS.js';
3
3
 
4
4
  type PublicCompany = Schemas['CompanyPublic'];
5
5
  /** A market (sector) a company operates in — name + its source slug. */
@@ -1,5 +1,5 @@
1
- import { P as PublicBoard, a as CompanyCategorySalary, b as CompanySalary, L as LocationSalaryDetail, S as SkillSalaryDetail, T as TitleSalaryDetail } from './board-CEsr8xMh.js';
2
- import { P as PublicJob } from './jobs-DbZuagy-.js';
1
+ import { P as PublicBoard, a as CompanyCategorySalary, b as CompanySalary, L as LocationSalaryDetail, S as SkillSalaryDetail, T as TitleSalaryDetail } from './board-Dlv-nGl3.js';
2
+ import { P as PublicJob } from './jobs-D0OqM6IS.js';
3
3
 
4
4
  /**
5
5
  * Wire enum for `salaryTimeframe`. Defined here (not re-exported from any
@@ -1,5 +1,5 @@
1
- import { P as PublicBoard, a as CompanyCategorySalary, b as CompanySalary, L as LocationSalaryDetail, S as SkillSalaryDetail, T as TitleSalaryDetail } from './board-CdG7z-Vq.mjs';
2
- import { P as PublicJob } from './jobs-COOBWXWV.mjs';
1
+ import { P as PublicBoard, a as CompanyCategorySalary, b as CompanySalary, L as LocationSalaryDetail, S as SkillSalaryDetail, T as TitleSalaryDetail } from './board-BKUGfCIT.mjs';
2
+ import { P as PublicJob } from './jobs-DCeOWaPk.mjs';
3
3
 
4
4
  /**
5
5
  * Wire enum for `salaryTimeframe`. Defined here (not re-exported from any