@cavuno/board 4.15.0 → 4.18.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 (45) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/dist/{_spec-DdWB8z1Z.d.mts → _spec-Dk0TIRUc.d.mts} +19 -7
  3. package/dist/{_spec-DdWB8z1Z.d.ts → _spec-Dk0TIRUc.d.ts} +19 -7
  4. package/dist/analytics.d.mts +58 -0
  5. package/dist/analytics.d.ts +58 -0
  6. package/dist/analytics.js +222 -0
  7. package/dist/analytics.mjs +199 -0
  8. package/dist/bin.mjs +99 -26
  9. package/dist/{blog-B7xorPBw.d.ts → blog-1THfucKR.d.ts} +1 -1
  10. package/dist/{blog-DopIQsKV.d.mts → blog-C04NWTh_.d.mts} +1 -1
  11. package/dist/{board-Bpnqn4Rg.d.ts → board-CqeoQAa1.d.ts} +2 -2
  12. package/dist/{board-CkgTW4cC.d.mts → board-DIO9aFXx.d.mts} +2 -2
  13. package/dist/{breadcrumbs-DelEqbSz.d.ts → breadcrumbs-CCT1oC2-.d.ts} +2 -2
  14. package/dist/{breadcrumbs-CAppxMoS.d.mts → breadcrumbs-mE9Zsxlq.d.mts} +2 -2
  15. package/dist/browser/cavuno-board.global.min.js +1 -1
  16. package/dist/{chunk-W73BOGOQ.mjs → chunk-2WBQVVOB.mjs} +1 -0
  17. package/dist/doctor.js +93 -21
  18. package/dist/doctor.mjs +93 -21
  19. package/dist/filters.d.mts +2 -2
  20. package/dist/filters.d.ts +2 -2
  21. package/dist/format.d.mts +4 -4
  22. package/dist/format.d.ts +4 -4
  23. package/dist/index.d.mts +12 -12
  24. package/dist/index.d.ts +12 -12
  25. package/dist/index.js +6 -2
  26. package/dist/index.mjs +6 -3
  27. package/dist/{jobs-Dq0TTcRf.d.mts → jobs-CVmAWGtJ.d.mts} +1 -1
  28. package/dist/{jobs-CExc4QdI.d.ts → jobs-Dvv0vX8p.d.ts} +1 -1
  29. package/dist/{search-D6hau3d4.d.mts → search-BJ5myYaf.d.mts} +1 -1
  30. package/dist/{search-CZOec95V.d.ts → search-CcstbHOe.d.ts} +1 -1
  31. package/dist/{seo-C1hFa0-v.d.ts → seo-CWO6N-iE.d.ts} +1 -1
  32. package/dist/{seo-CJSwd_ss.d.mts → seo-Ds6tLMqU.d.mts} +1 -1
  33. package/dist/seo-files.d.mts +2 -2
  34. package/dist/seo-files.d.ts +2 -2
  35. package/dist/seo.d.mts +6 -6
  36. package/dist/seo.d.ts +6 -6
  37. package/dist/server.d.mts +6 -6
  38. package/dist/server.d.ts +6 -6
  39. package/dist/server.mjs +1 -1
  40. package/dist/sitemap.d.mts +6 -6
  41. package/dist/sitemap.d.ts +6 -6
  42. package/dist/suggest.d.mts +2 -2
  43. package/dist/suggest.d.ts +2 -2
  44. package/package.json +11 -1
  45. package/skills/manifest.json +1 -1
package/CHANGELOG.md CHANGED
@@ -3,6 +3,33 @@
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.18.0 — 2026-09-02
7
+
8
+ - **`board.auth.requestMagicLink({ intent: 'sign_in' })`**: new optional
9
+ `intent` on `POST /auth/magic-link`. Pass `'sign_in'` from a sign-in form so
10
+ an unknown email is refused with 404 `board_auth_account_not_found` instead
11
+ of silently minting a candidate sign-up token. Omit it to keep the previous
12
+ sign-in-or-sign-up behaviour. Any other value is a 400.
13
+ - **`BOARD_API_ERROR_CODES`**: adds `board_auth_account_not_found`.
14
+
15
+ ## 4.17.0 — 2026-08-31
16
+
17
+ - **`EmployerPipelineStage.systemStage`**: a closed set of pipeline literals —
18
+ `shortlisted`, `contacted`, `replied`, `review`, `offer`, `hired`,
19
+ `rejected`, or `null` — instead of an open string. `applied` remains a
20
+ list-query alias for `review`, not a `systemStage` value.
21
+
22
+ ## 4.16.0 — 2026-08-31
23
+
24
+ - **`@cavuno/board/analytics`**: new subpath with `install({ publishableKey })`
25
+ and `track(action, payload?)`. Uses the board publishable key (`pk_…`) only.
26
+ Default collect URL is Cavuno central collect; pass a first-party
27
+ `/.well-known/cavuno/collect` URL when the board host serves those handlers.
28
+ - **Stable analytics tenant**: events are keyed by the immutable public board
29
+ id (`boards_<accountId>`), not the mutable board slug.
30
+ - **Doctor**: `npx @cavuno/board doctor` includes an analytics-surface scan that
31
+ flags legacy third-party tracker credentials in app source.
32
+
6
33
  ## 4.15.0 — 2026-08-30
7
34
 
8
35
  - **LinkedIn conversion IDs on `board.context().analytics`**: five nullable
@@ -249,6 +249,11 @@ interface components {
249
249
  email: string;
250
250
  /** @description Optional same-origin path to carry through the email link. */
251
251
  returnTo?: string;
252
+ /**
253
+ * @description Pass `sign_in` from a sign-in form: an unknown email returns 404 `board_auth_account_not_found` and nothing is minted or sent; existing users are unchanged. Omit it for today’s behaviour (existing users get a sign-in token; unknown emails get a candidate sign-up token when candidate registration is enabled).
254
+ * @enum {string}
255
+ */
256
+ intent?: "sign_in";
252
257
  };
253
258
  BoardAuthResetPasswordBody: {
254
259
  token: string;
@@ -1054,7 +1059,7 @@ interface components {
1054
1059
  inOfficePeriod?: "per_week" | "per_month" | "per_year";
1055
1060
  /** @description How often the candidate must be in-office over `inOfficePeriod`. */
1056
1061
  inOfficeFrequency?: number;
1057
- /** @description Physical office locations associated with the job. Each entry is forward-geocoded server-side; a country mismatch returns `400 jobs_unresolvable_location`. */
1062
+ /** @description Physical office locations associated with the job. Prefer `{query: "City, Country"}` for free-form input; `{city, country, region?, locality?}` is also accepted when you already have structured fields. Each entry is resolved server-side; a country mismatch returns `400 jobs_unresolvable_location`. */
1058
1063
  officeLocations?: components["schemas"]["JobOfficeLocationInput"][];
1059
1064
  /** @description The job title. */
1060
1065
  title: string;
@@ -1211,8 +1216,11 @@ interface components {
1211
1216
  object: "employer_pipeline_stage";
1212
1217
  jobId: string;
1213
1218
  label: string;
1214
- /** @description The stable system meaning (`review`/`offer`/`hired`/`rejected`), or `null` for a custom employer stage. */
1215
- systemStage: string | null;
1219
+ /**
1220
+ * @description The stable system meaning, or `null` for a custom employer stage.
1221
+ * @enum {string|null}
1222
+ */
1223
+ systemStage: "shortlisted" | "contacted" | "replied" | "review" | "offer" | "hired" | "rejected" | null;
1216
1224
  isProtected: boolean;
1217
1225
  hidden: boolean;
1218
1226
  position: number;
@@ -1299,7 +1307,7 @@ interface components {
1299
1307
  inOfficePeriod?: "per_week" | "per_month" | "per_year";
1300
1308
  /** @description How often the candidate must be in-office over `inOfficePeriod`. */
1301
1309
  inOfficeFrequency?: number;
1302
- /** @description Physical office locations associated with the job. Each entry is forward-geocoded server-side; a country mismatch returns `400 jobs_unresolvable_location`. */
1310
+ /** @description Physical office locations associated with the job. Prefer `{query: "City, Country"}` for free-form input; `{city, country, region?, locality?}` is also accepted when you already have structured fields. Each entry is resolved server-side; a country mismatch returns `400 jobs_unresolvable_location`. */
1303
1311
  officeLocations?: components["schemas"]["JobOfficeLocationInput"][];
1304
1312
  /** @description The job title. */
1305
1313
  title?: string;
@@ -1355,6 +1363,9 @@ interface components {
1355
1363
  displayName: string | null;
1356
1364
  };
1357
1365
  JobOfficeLocationInput: {
1366
+ /** @description Free-form location string (e.g. `"Berlin, Germany"`, `"Utrecht, Netherlands"`, `"Mountain View, California, USA"`). Resolved server-side; rejected when no high-confidence match is found. */
1367
+ query: string;
1368
+ } | {
1358
1369
  /** @description Neighborhood or sub-locality. */
1359
1370
  locality?: string;
1360
1371
  /** @description City. */
@@ -1363,9 +1374,6 @@ interface components {
1363
1374
  region?: string;
1364
1375
  /** @description ISO 3166-1 alpha-2 country code OR recognized country name/alias. Aliases are normalized to canonical alpha-2 server-side (e.g. `US`, `USA`, `United States` → `US`; `UK`, `GB`, `United Kingdom` → `GB`). */
1365
1376
  country: string;
1366
- } | {
1367
- /** @description Free-form location string (e.g. `"Berlin, Germany"`, `"Mountain View, California, USA"`). Mapbox parses + ranks candidates server-side; rejects on low confidence. */
1368
- query: string;
1369
1377
  };
1370
1378
  JobPostingBillingOptions: {
1371
1379
  /** @enum {string} */
@@ -2597,9 +2605,13 @@ interface components {
2597
2605
  StartConversationBody: {
2598
2606
  candidateBoardUserId: string;
2599
2607
  body: string;
2608
+ /** @description Optional job id. When set, this send is from that job pipeline and may auto-advance Shortlisted to Contacted. */
2609
+ job?: string;
2600
2610
  } | {
2601
2611
  candidateHandle: string;
2602
2612
  body: string;
2613
+ /** @description Optional job id. When set, this send is from that job pipeline and may auto-advance Shortlisted to Contacted. */
2614
+ job?: string;
2603
2615
  };
2604
2616
  SuggestResult: {
2605
2617
  /** @enum {string} */
@@ -249,6 +249,11 @@ interface components {
249
249
  email: string;
250
250
  /** @description Optional same-origin path to carry through the email link. */
251
251
  returnTo?: string;
252
+ /**
253
+ * @description Pass `sign_in` from a sign-in form: an unknown email returns 404 `board_auth_account_not_found` and nothing is minted or sent; existing users are unchanged. Omit it for today’s behaviour (existing users get a sign-in token; unknown emails get a candidate sign-up token when candidate registration is enabled).
254
+ * @enum {string}
255
+ */
256
+ intent?: "sign_in";
252
257
  };
253
258
  BoardAuthResetPasswordBody: {
254
259
  token: string;
@@ -1054,7 +1059,7 @@ interface components {
1054
1059
  inOfficePeriod?: "per_week" | "per_month" | "per_year";
1055
1060
  /** @description How often the candidate must be in-office over `inOfficePeriod`. */
1056
1061
  inOfficeFrequency?: number;
1057
- /** @description Physical office locations associated with the job. Each entry is forward-geocoded server-side; a country mismatch returns `400 jobs_unresolvable_location`. */
1062
+ /** @description Physical office locations associated with the job. Prefer `{query: "City, Country"}` for free-form input; `{city, country, region?, locality?}` is also accepted when you already have structured fields. Each entry is resolved server-side; a country mismatch returns `400 jobs_unresolvable_location`. */
1058
1063
  officeLocations?: components["schemas"]["JobOfficeLocationInput"][];
1059
1064
  /** @description The job title. */
1060
1065
  title: string;
@@ -1211,8 +1216,11 @@ interface components {
1211
1216
  object: "employer_pipeline_stage";
1212
1217
  jobId: string;
1213
1218
  label: string;
1214
- /** @description The stable system meaning (`review`/`offer`/`hired`/`rejected`), or `null` for a custom employer stage. */
1215
- systemStage: string | null;
1219
+ /**
1220
+ * @description The stable system meaning, or `null` for a custom employer stage.
1221
+ * @enum {string|null}
1222
+ */
1223
+ systemStage: "shortlisted" | "contacted" | "replied" | "review" | "offer" | "hired" | "rejected" | null;
1216
1224
  isProtected: boolean;
1217
1225
  hidden: boolean;
1218
1226
  position: number;
@@ -1299,7 +1307,7 @@ interface components {
1299
1307
  inOfficePeriod?: "per_week" | "per_month" | "per_year";
1300
1308
  /** @description How often the candidate must be in-office over `inOfficePeriod`. */
1301
1309
  inOfficeFrequency?: number;
1302
- /** @description Physical office locations associated with the job. Each entry is forward-geocoded server-side; a country mismatch returns `400 jobs_unresolvable_location`. */
1310
+ /** @description Physical office locations associated with the job. Prefer `{query: "City, Country"}` for free-form input; `{city, country, region?, locality?}` is also accepted when you already have structured fields. Each entry is resolved server-side; a country mismatch returns `400 jobs_unresolvable_location`. */
1303
1311
  officeLocations?: components["schemas"]["JobOfficeLocationInput"][];
1304
1312
  /** @description The job title. */
1305
1313
  title?: string;
@@ -1355,6 +1363,9 @@ interface components {
1355
1363
  displayName: string | null;
1356
1364
  };
1357
1365
  JobOfficeLocationInput: {
1366
+ /** @description Free-form location string (e.g. `"Berlin, Germany"`, `"Utrecht, Netherlands"`, `"Mountain View, California, USA"`). Resolved server-side; rejected when no high-confidence match is found. */
1367
+ query: string;
1368
+ } | {
1358
1369
  /** @description Neighborhood or sub-locality. */
1359
1370
  locality?: string;
1360
1371
  /** @description City. */
@@ -1363,9 +1374,6 @@ interface components {
1363
1374
  region?: string;
1364
1375
  /** @description ISO 3166-1 alpha-2 country code OR recognized country name/alias. Aliases are normalized to canonical alpha-2 server-side (e.g. `US`, `USA`, `United States` → `US`; `UK`, `GB`, `United Kingdom` → `GB`). */
1365
1376
  country: string;
1366
- } | {
1367
- /** @description Free-form location string (e.g. `"Berlin, Germany"`, `"Mountain View, California, USA"`). Mapbox parses + ranks candidates server-side; rejects on low confidence. */
1368
- query: string;
1369
1377
  };
1370
1378
  JobPostingBillingOptions: {
1371
1379
  /** @enum {string} */
@@ -2597,9 +2605,13 @@ interface components {
2597
2605
  StartConversationBody: {
2598
2606
  candidateBoardUserId: string;
2599
2607
  body: string;
2608
+ /** @description Optional job id. When set, this send is from that job pipeline and may auto-advance Shortlisted to Contacted. */
2609
+ job?: string;
2600
2610
  } | {
2601
2611
  candidateHandle: string;
2602
2612
  body: string;
2613
+ /** @description Optional job id. When set, this send is from that job pipeline and may auto-advance Shortlisted to Contacted. */
2614
+ job?: string;
2603
2615
  };
2604
2616
  SuggestResult: {
2605
2617
  /** @enum {string} */
@@ -0,0 +1,58 @@
1
+ declare const DEFAULT_COLLECT_URL = "https://cavuno.com/api/analytics/collect";
2
+ declare const DEFAULT_SCRIPT_URL = "https://cavuno.com/js/metrics.js";
3
+ /** Placeholder tenant; collect rewrites from the publishable key. */
4
+ declare const PENDING_TENANT_ID = "boards_pending";
5
+ declare const WELL_KNOWN_ANALYTICS_SCRIPT_PATH = "/.well-known/cavuno/analytics.js";
6
+ declare const WELL_KNOWN_SCRIPT_PATH = "/.well-known/cavuno/analytics.js";
7
+ declare const WELL_KNOWN_COLLECT_PATH = "/.well-known/cavuno/collect";
8
+
9
+ /**
10
+ * Optional first-party well-known handlers for board origin Workers.
11
+ * Central collect remains the default when these paths are not mounted.
12
+ */
13
+
14
+ declare const WELL_KNOWN_COLLECT_EVENTS_PATH = "/.well-known/cavuno/collect/v0/events";
15
+ /**
16
+ * Handle `/.well-known/cavuno/{analytics.js,collect}` when mounted on a Worker.
17
+ * Returns null for every other path so the host can fall through.
18
+ */
19
+ declare function matchAnalyticsWellKnown(request: Request): Promise<Response | null>;
20
+
21
+ /**
22
+ * `@cavuno/board/analytics` — client write surface for board analytics.
23
+ *
24
+ * Emits pageviews (via hosted script) and custom events to Cavuno collect.
25
+ * Uses the board publishable key only; no second analytics credential.
26
+ */
27
+
28
+ type AnalyticsInstallOptions = {
29
+ publishableKey: string;
30
+ /** Default: Cavuno central collect (self-host safe) */
31
+ collectUrl?: string;
32
+ /** Default: Cavuno-hosted metrics script */
33
+ scriptUrl?: string;
34
+ };
35
+ /**
36
+ * Load the Cavuno-hosted metrics script and remember collect credentials.
37
+ * Safe to call once per page; subsequent calls update collect targets only.
38
+ */
39
+ declare function install(options: AnalyticsInstallOptions): void;
40
+ /**
41
+ * Emit a custom analytics event to Cavuno collect.
42
+ * Prefer `install` first so pageviews and vitals are also recorded.
43
+ */
44
+ declare function track(action: string, payload?: Record<string, unknown>): void;
45
+ declare const analytics: {
46
+ install: typeof install;
47
+ track: typeof track;
48
+ matchAnalyticsWellKnown: typeof matchAnalyticsWellKnown;
49
+ DEFAULT_COLLECT_URL: string;
50
+ DEFAULT_SCRIPT_URL: string;
51
+ PENDING_TENANT_ID: string;
52
+ WELL_KNOWN_ANALYTICS_SCRIPT_PATH: string;
53
+ WELL_KNOWN_SCRIPT_PATH: string;
54
+ WELL_KNOWN_COLLECT_PATH: string;
55
+ WELL_KNOWN_COLLECT_EVENTS_PATH: string;
56
+ };
57
+
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 };
@@ -0,0 +1,58 @@
1
+ declare const DEFAULT_COLLECT_URL = "https://cavuno.com/api/analytics/collect";
2
+ declare const DEFAULT_SCRIPT_URL = "https://cavuno.com/js/metrics.js";
3
+ /** Placeholder tenant; collect rewrites from the publishable key. */
4
+ declare const PENDING_TENANT_ID = "boards_pending";
5
+ declare const WELL_KNOWN_ANALYTICS_SCRIPT_PATH = "/.well-known/cavuno/analytics.js";
6
+ declare const WELL_KNOWN_SCRIPT_PATH = "/.well-known/cavuno/analytics.js";
7
+ declare const WELL_KNOWN_COLLECT_PATH = "/.well-known/cavuno/collect";
8
+
9
+ /**
10
+ * Optional first-party well-known handlers for board origin Workers.
11
+ * Central collect remains the default when these paths are not mounted.
12
+ */
13
+
14
+ declare const WELL_KNOWN_COLLECT_EVENTS_PATH = "/.well-known/cavuno/collect/v0/events";
15
+ /**
16
+ * Handle `/.well-known/cavuno/{analytics.js,collect}` when mounted on a Worker.
17
+ * Returns null for every other path so the host can fall through.
18
+ */
19
+ declare function matchAnalyticsWellKnown(request: Request): Promise<Response | null>;
20
+
21
+ /**
22
+ * `@cavuno/board/analytics` — client write surface for board analytics.
23
+ *
24
+ * Emits pageviews (via hosted script) and custom events to Cavuno collect.
25
+ * Uses the board publishable key only; no second analytics credential.
26
+ */
27
+
28
+ type AnalyticsInstallOptions = {
29
+ publishableKey: string;
30
+ /** Default: Cavuno central collect (self-host safe) */
31
+ collectUrl?: string;
32
+ /** Default: Cavuno-hosted metrics script */
33
+ scriptUrl?: string;
34
+ };
35
+ /**
36
+ * Load the Cavuno-hosted metrics script and remember collect credentials.
37
+ * Safe to call once per page; subsequent calls update collect targets only.
38
+ */
39
+ declare function install(options: AnalyticsInstallOptions): void;
40
+ /**
41
+ * Emit a custom analytics event to Cavuno collect.
42
+ * Prefer `install` first so pageviews and vitals are also recorded.
43
+ */
44
+ declare function track(action: string, payload?: Record<string, unknown>): void;
45
+ declare const analytics: {
46
+ install: typeof install;
47
+ track: typeof track;
48
+ matchAnalyticsWellKnown: typeof matchAnalyticsWellKnown;
49
+ DEFAULT_COLLECT_URL: string;
50
+ DEFAULT_SCRIPT_URL: string;
51
+ PENDING_TENANT_ID: string;
52
+ WELL_KNOWN_ANALYTICS_SCRIPT_PATH: string;
53
+ WELL_KNOWN_SCRIPT_PATH: string;
54
+ WELL_KNOWN_COLLECT_PATH: string;
55
+ WELL_KNOWN_COLLECT_EVENTS_PATH: string;
56
+ };
57
+
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 };
@@ -0,0 +1,222 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/analytics/index.ts
21
+ var analytics_exports = {};
22
+ __export(analytics_exports, {
23
+ DEFAULT_COLLECT_URL: () => DEFAULT_COLLECT_URL,
24
+ DEFAULT_SCRIPT_URL: () => DEFAULT_SCRIPT_URL,
25
+ PENDING_TENANT_ID: () => PENDING_TENANT_ID,
26
+ WELL_KNOWN_ANALYTICS_SCRIPT_PATH: () => WELL_KNOWN_ANALYTICS_SCRIPT_PATH,
27
+ WELL_KNOWN_COLLECT_EVENTS_PATH: () => WELL_KNOWN_COLLECT_EVENTS_PATH,
28
+ WELL_KNOWN_COLLECT_PATH: () => WELL_KNOWN_COLLECT_PATH,
29
+ WELL_KNOWN_SCRIPT_PATH: () => WELL_KNOWN_SCRIPT_PATH,
30
+ analytics: () => analytics,
31
+ install: () => install,
32
+ matchAnalyticsWellKnown: () => matchAnalyticsWellKnown,
33
+ track: () => track
34
+ });
35
+ module.exports = __toCommonJS(analytics_exports);
36
+
37
+ // src/analytics/defaults.ts
38
+ var DEFAULT_COLLECT_URL = "https://cavuno.com/api/analytics/collect";
39
+ var DEFAULT_SCRIPT_URL = "https://cavuno.com/js/metrics.js";
40
+ var PENDING_TENANT_ID = "boards_pending";
41
+ var WELL_KNOWN_ANALYTICS_SCRIPT_PATH = "/.well-known/cavuno/analytics.js";
42
+ var WELL_KNOWN_SCRIPT_PATH = WELL_KNOWN_ANALYTICS_SCRIPT_PATH;
43
+ var WELL_KNOWN_COLLECT_PATH = "/.well-known/cavuno/collect";
44
+
45
+ // src/analytics/well-known.ts
46
+ var WELL_KNOWN_COLLECT_EVENTS_PATH = "/.well-known/cavuno/collect/v0/events";
47
+ var CORS_HEADERS = {
48
+ "Access-Control-Allow-Origin": "*",
49
+ "Access-Control-Allow-Methods": "POST, OPTIONS",
50
+ "Access-Control-Allow-Headers": "Authorization, Content-Type, Accept",
51
+ "Access-Control-Max-Age": "86400",
52
+ "Cross-Origin-Resource-Policy": "cross-origin"
53
+ };
54
+ function stripTrailingSlash(pathname) {
55
+ return pathname.length > 1 && pathname.endsWith("/") ? pathname.slice(0, -1) : pathname;
56
+ }
57
+ function withCors(response) {
58
+ const headers = new Headers(response.headers);
59
+ for (const [key, value] of Object.entries(CORS_HEADERS)) {
60
+ headers.set(key, value);
61
+ }
62
+ return new Response(response.body, {
63
+ status: response.status,
64
+ statusText: response.statusText,
65
+ headers
66
+ });
67
+ }
68
+ function corsPreflight() {
69
+ return withCors(new Response(null, { status: 204 }));
70
+ }
71
+ async function proxyCollect(request, targetUrl) {
72
+ if (request.method === "OPTIONS") {
73
+ return corsPreflight();
74
+ }
75
+ if (request.method !== "POST") {
76
+ return withCors(new Response(null, { status: 405 }));
77
+ }
78
+ const headers = new Headers();
79
+ const authorization = request.headers.get("Authorization");
80
+ const contentType = request.headers.get("Content-Type");
81
+ const accept = request.headers.get("Accept");
82
+ if (authorization) headers.set("Authorization", authorization);
83
+ if (contentType) headers.set("Content-Type", contentType);
84
+ if (accept) headers.set("Accept", accept);
85
+ const body = await request.arrayBuffer();
86
+ const upstream = await fetch(targetUrl, {
87
+ method: "POST",
88
+ headers,
89
+ body
90
+ });
91
+ return withCors(
92
+ new Response(upstream.body, {
93
+ status: upstream.status,
94
+ headers: {
95
+ "Content-Type": upstream.headers.get("Content-Type") ?? "application/json"
96
+ }
97
+ })
98
+ );
99
+ }
100
+ async function matchAnalyticsWellKnown(request) {
101
+ const path = stripTrailingSlash(new URL(request.url).pathname);
102
+ if (path === WELL_KNOWN_ANALYTICS_SCRIPT_PATH) {
103
+ if (request.method !== "GET" && request.method !== "HEAD") {
104
+ return new Response(null, { status: 405 });
105
+ }
106
+ return Response.redirect(DEFAULT_SCRIPT_URL, 302);
107
+ }
108
+ if (path === WELL_KNOWN_COLLECT_PATH) {
109
+ return proxyCollect(request, DEFAULT_COLLECT_URL);
110
+ }
111
+ if (path === WELL_KNOWN_COLLECT_EVENTS_PATH) {
112
+ return proxyCollect(request, `${DEFAULT_COLLECT_URL}/v0/events`);
113
+ }
114
+ return null;
115
+ }
116
+
117
+ // src/analytics/index.ts
118
+ var GLOBAL_NAME = "CavunoAnalytics";
119
+ var installed = null;
120
+ function getRoot() {
121
+ return globalThis;
122
+ }
123
+ function normalizeCollectUrl(url) {
124
+ return url.replace(/\/+$/, "");
125
+ }
126
+ function resolveDefaultCollectUrl() {
127
+ return DEFAULT_COLLECT_URL;
128
+ }
129
+ function resolveDefaultScriptUrl() {
130
+ return DEFAULT_SCRIPT_URL;
131
+ }
132
+ function postCollectJson(state, action, payload) {
133
+ const body = JSON.stringify({
134
+ publishableKey: state.publishableKey,
135
+ action,
136
+ payload: payload ?? {}
137
+ });
138
+ void fetch(state.collectUrl, {
139
+ method: "POST",
140
+ headers: {
141
+ Authorization: `Bearer ${state.publishableKey}`,
142
+ "Content-Type": "application/json",
143
+ Accept: "application/json"
144
+ },
145
+ body,
146
+ keepalive: true
147
+ }).catch(() => {
148
+ });
149
+ }
150
+ function exposeGlobal(state) {
151
+ const root = getRoot();
152
+ root[GLOBAL_NAME] = {
153
+ trackEvent(action, payload) {
154
+ postCollectJson(state, action, payload);
155
+ }
156
+ };
157
+ }
158
+ function injectScript(state, scriptUrl) {
159
+ const doc = getRoot().document;
160
+ if (!doc?.createElement) {
161
+ return;
162
+ }
163
+ if (doc.querySelector?.(`script[data-cavuno-analytics="1"]`)) {
164
+ return;
165
+ }
166
+ const el = doc.createElement("script");
167
+ el.defer = true;
168
+ el.src = scriptUrl;
169
+ el.setAttribute("data-cavuno-analytics", "1");
170
+ el.setAttribute("data-token", state.publishableKey);
171
+ el.setAttribute("data-host", state.collectUrl);
172
+ el.setAttribute("data-tenant-id", PENDING_TENANT_ID);
173
+ el.setAttribute("data-web-vitals", "true");
174
+ const head = doc.head;
175
+ if (head?.appendChild) {
176
+ head.appendChild(el);
177
+ return;
178
+ }
179
+ const first = doc.getElementsByTagName("script")[0];
180
+ first?.parentNode?.insertBefore(el, first);
181
+ }
182
+ function install(options) {
183
+ const publishableKey = options.publishableKey.trim();
184
+ if (!publishableKey.startsWith("pk_")) {
185
+ throw new Error("analytics.install requires a publishable key (pk_\u2026)");
186
+ }
187
+ const collectUrl = normalizeCollectUrl(
188
+ (options.collectUrl ?? resolveDefaultCollectUrl()).trim() || resolveDefaultCollectUrl()
189
+ );
190
+ const scriptUrl = (options.scriptUrl ?? resolveDefaultScriptUrl()).trim() || resolveDefaultScriptUrl();
191
+ installed = { publishableKey, collectUrl };
192
+ exposeGlobal(installed);
193
+ injectScript(installed, scriptUrl);
194
+ }
195
+ function track(action, payload) {
196
+ const trimmed = action.trim();
197
+ if (!trimmed) {
198
+ return;
199
+ }
200
+ const root = getRoot();
201
+ const globalTrack = root[GLOBAL_NAME]?.trackEvent;
202
+ if (typeof globalTrack === "function" && installed) {
203
+ globalTrack(trimmed, payload);
204
+ return;
205
+ }
206
+ if (!installed) {
207
+ return;
208
+ }
209
+ postCollectJson(installed, trimmed, payload);
210
+ }
211
+ var analytics = {
212
+ install,
213
+ track,
214
+ matchAnalyticsWellKnown,
215
+ DEFAULT_COLLECT_URL,
216
+ DEFAULT_SCRIPT_URL,
217
+ PENDING_TENANT_ID,
218
+ WELL_KNOWN_ANALYTICS_SCRIPT_PATH,
219
+ WELL_KNOWN_SCRIPT_PATH,
220
+ WELL_KNOWN_COLLECT_PATH,
221
+ WELL_KNOWN_COLLECT_EVENTS_PATH
222
+ };