clivly 0.3.0-next.2 → 0.3.0-next.3

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.
@@ -64,6 +64,12 @@ interface ClivlySDKConfig {
64
64
  * `contactEntity`.
65
65
  */
66
66
  entities?: ClivlyEntitiesConfig;
67
+ /**
68
+ * Environment to read platform variables from when discovering the public
69
+ * trigger URL. Defaults to `process.env`. Injectable so tests and non-Node
70
+ * runtimes can supply their own.
71
+ */
72
+ env?: Record<string, string | undefined>;
67
73
  /**
68
74
  * 🚧 Not implemented in 0.1.x. Reserved options for tunnel mode and CRM
69
75
  * write-back — they have NO effect yet and land with the Cloudflare
@@ -140,8 +146,19 @@ interface ClivlySDKConfig {
140
146
  * Public URL of the route where `createClivlyHandler()` is mounted, e.g.
141
147
  * `https://app.example.com/api/clivly/tick`. Reported to Clivly on each
142
148
  * heartbeat; a URL set in the dashboard overrides it.
149
+ *
150
+ * Optional — leave it unset and the SDK discovers the URL itself, from the
151
+ * hosting platform's environment or from the first request the route
152
+ * serves. Set it only to pin an address the SDK can't infer.
143
153
  */
144
154
  url?: string;
155
+ /**
156
+ * Path where `createClivlyHandler()` is mounted, e.g. `/api/clivly/tick`.
157
+ * Written by `clivly init`. Combined with the origin detected from the
158
+ * hosting platform so the SDK can report a trigger URL on its very first
159
+ * heartbeat, before any request has arrived to observe.
160
+ */
161
+ path?: string;
145
162
  /**
146
163
  * Signing secret (`whsec_…`) issued by Clivly under Integrations → Remote
147
164
  * sync. When set, `createClivlyHandler()` rejects any request that isn't
@@ -64,6 +64,12 @@ interface ClivlySDKConfig {
64
64
  * `contactEntity`.
65
65
  */
66
66
  entities?: ClivlyEntitiesConfig;
67
+ /**
68
+ * Environment to read platform variables from when discovering the public
69
+ * trigger URL. Defaults to `process.env`. Injectable so tests and non-Node
70
+ * runtimes can supply their own.
71
+ */
72
+ env?: Record<string, string | undefined>;
67
73
  /**
68
74
  * 🚧 Not implemented in 0.1.x. Reserved options for tunnel mode and CRM
69
75
  * write-back — they have NO effect yet and land with the Cloudflare
@@ -140,8 +146,19 @@ interface ClivlySDKConfig {
140
146
  * Public URL of the route where `createClivlyHandler()` is mounted, e.g.
141
147
  * `https://app.example.com/api/clivly/tick`. Reported to Clivly on each
142
148
  * heartbeat; a URL set in the dashboard overrides it.
149
+ *
150
+ * Optional — leave it unset and the SDK discovers the URL itself, from the
151
+ * hosting platform's environment or from the first request the route
152
+ * serves. Set it only to pin an address the SDK can't infer.
143
153
  */
144
154
  url?: string;
155
+ /**
156
+ * Path where `createClivlyHandler()` is mounted, e.g. `/api/clivly/tick`.
157
+ * Written by `clivly init`. Combined with the origin detected from the
158
+ * hosting platform so the SDK can report a trigger URL on its very first
159
+ * heartbeat, before any request has arrived to observe.
160
+ */
161
+ path?: string;
145
162
  /**
146
163
  * Signing secret (`whsec_…`) issued by Clivly under Integrations → Remote
147
164
  * sync. When set, `createClivlyHandler()` rejects any request that isn't
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clivly",
3
- "version": "0.3.0-next.2",
3
+ "version": "0.3.0-next.3",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Clivly CLI — scaffold a CRM into your app (clivly init).",
@@ -1,4 +1,41 @@
1
- import { t as DRIZZLE_META } from "./drizzle-meta-1z6Apnt-.mjs";
1
+ import { t as DRIZZLE_META } from "./drizzle-meta-DpMKataU.mjs";
2
+ //#region ../sdk/src/custom-slug.ts
3
+ const NON_ALNUM = /[^a-z0-9]+/g;
4
+ const EDGE_HYPHENS = /(^-|-$)/g;
5
+ function slugify(value) {
6
+ return value.toLowerCase().trim().replace(NON_ALNUM, "-").replace(EDGE_HYPHENS, "");
7
+ }
8
+ function checkCustomObjectSlugs(config) {
9
+ const custom = config.source?.custom ?? [];
10
+ if (custom.length === 0) return {
11
+ name: "Custom object slugs",
12
+ ok: false,
13
+ skipped: true,
14
+ detail: "No custom objects configured."
15
+ };
16
+ const problems = [];
17
+ const seen = /* @__PURE__ */ new Map();
18
+ for (const source of custom) {
19
+ const declared = source.objectType;
20
+ const canonical = slugify(declared);
21
+ if (canonical === "") problems.push(`"${source.entity}" has an empty objectType.`);
22
+ else if (canonical !== declared) problems.push(`"${declared}" is not a slug — the dashboard would store it as "${canonical}".`);
23
+ const prior = seen.get(canonical);
24
+ if (prior && canonical !== "") problems.push(`duplicate slug "${canonical}" declared by "${prior}" and "${source.entity}".`);
25
+ else seen.set(canonical, source.entity);
26
+ }
27
+ if (problems.length > 0) return {
28
+ name: "Custom object slugs",
29
+ ok: false,
30
+ detail: problems.join(" ")
31
+ };
32
+ return {
33
+ name: "Custom object slugs",
34
+ ok: true,
35
+ detail: `Ensure these object types exist in Clivly: ${custom.map((source) => source.objectType).join(", ")}.`
36
+ };
37
+ }
38
+ //#endregion
2
39
  //#region ../sdk/src/namespace-probe.ts
3
40
  /**
4
41
  * Verify the tables the integration depends on are genuinely reachable:
@@ -159,6 +196,7 @@ async function runDoctor(args) {
159
196
  ok: result.ok,
160
197
  detail: result.ok ? `Reached Clivly — org "${result.org?.name ?? "(unnamed)"}".` : `Could not connect (${result.reason}).`
161
198
  });
199
+ checks.push(checkCustomObjectSlugs(config));
162
200
  checks.push(await namespaceCheck(config));
163
201
  return {
164
202
  ok: checks.every((check) => check.skipped || check.ok),
@@ -257,6 +295,85 @@ async function fullSyncSource(args) {
257
295
  };
258
296
  }
259
297
  //#endregion
298
+ //#region ../sdk/src/trigger-url.ts
299
+ const SCHEME_REGEX = /^https?:\/\//;
300
+ const INVALID_HOST_CHARS_REGEX = /[/\\@?#\s]/;
301
+ const PLATFORM_ORIGIN_SOURCES = [
302
+ {
303
+ key: "VERCEL_URL",
304
+ toOrigin: (value) => withScheme(value)
305
+ },
306
+ {
307
+ key: "CF_PAGES_URL",
308
+ toOrigin: (value) => withScheme(value)
309
+ },
310
+ {
311
+ key: "RAILWAY_PUBLIC_DOMAIN",
312
+ toOrigin: (value) => withScheme(value)
313
+ },
314
+ {
315
+ key: "RENDER_EXTERNAL_URL",
316
+ toOrigin: (value) => withScheme(value)
317
+ },
318
+ {
319
+ key: "FLY_APP_NAME",
320
+ toOrigin: (value) => `https://${value}.fly.dev`
321
+ }
322
+ ];
323
+ function withScheme(value) {
324
+ return SCHEME_REGEX.test(value) ? value : `https://${value}`;
325
+ }
326
+ function stripTrailingSlash(value) {
327
+ return value.endsWith("/") ? value.slice(0, -1) : value;
328
+ }
329
+ /**
330
+ * Derive the app's public origin from known platform environment variables.
331
+ * Returns an origin with no trailing slash, or undefined when nothing matches.
332
+ */
333
+ function originFromPlatformEnv(env) {
334
+ for (const source of PLATFORM_ORIGIN_SOURCES) {
335
+ const raw = env[source.key]?.trim();
336
+ if (raw) return stripTrailingSlash(source.toOrigin(raw));
337
+ }
338
+ }
339
+ function observedTriggerUrl(req) {
340
+ const host = (req.headers.get("x-forwarded-host") ?? req.headers.get("host"))?.trim();
341
+ if (!host) return;
342
+ if (INVALID_HOST_CHARS_REGEX.test(host)) return;
343
+ const forwardedProto = req.headers.get("x-forwarded-proto")?.split(",")[0]?.trim();
344
+ let requestUrl;
345
+ try {
346
+ requestUrl = new URL(req.url);
347
+ } catch {
348
+ return;
349
+ }
350
+ const proto = forwardedProto || requestUrl.protocol.replace(":", "");
351
+ try {
352
+ return new URL(`${proto}://${host}${requestUrl.pathname}`).toString();
353
+ } catch {
354
+ return;
355
+ }
356
+ }
357
+ /**
358
+ * Decide which trigger URL to report, in precedence order:
359
+ *
360
+ * 1. `configuredUrl` — an explicit `syncTrigger.url`. Someone typed this on
361
+ * purpose, so it beats anything inferred.
362
+ * 2. platform env origin + `configuredPath` — available at boot, before any
363
+ * request has arrived. Needs the path because platform variables only give
364
+ * an origin.
365
+ * 3. `observedUrl` — accurate but only after the route has served a request.
366
+ *
367
+ * A URL set in the Clivly dashboard overrides all of these server-side; the SDK
368
+ * doesn't need to know about it.
369
+ */
370
+ function resolveTriggerUrl(args) {
371
+ if (args.configuredUrl) return args.configuredUrl;
372
+ const origin = originFromPlatformEnv(args.env);
373
+ if (origin && args.configuredPath) return `${origin}${args.configuredPath.startsWith("/") ? args.configuredPath : `/${args.configuredPath}`}`;
374
+ return args.observedUrl;
375
+ }
376
+ //#endregion
260
377
  //#region ../sdk/src/errors.ts
261
378
  var ClivlyError = class ClivlyError extends Error {
262
379
  /** HTTP status, or null when the request never completed (network error). */
@@ -547,6 +664,7 @@ function createClivlySDK(config) {
547
664
  const cursorStore = config.cursorStore ?? createMemoryCursorStore();
548
665
  const fetcher = createFetcher(config.retry);
549
666
  let warnedUnsignedTrigger = false;
667
+ let observedUrl;
550
668
  let lastPushFailure = null;
551
669
  const createPush = (runId) => async (path, body) => {
552
670
  const response = await fetcher(`${apiUrl}${path}`, {
@@ -631,7 +749,15 @@ function createClivlySDK(config) {
631
749
  contactEntity: resolveContactEntity(config),
632
750
  namespaceReady: config.namespaceReady ?? false,
633
751
  schema: buildSchema(config),
634
- ...config.syncTrigger?.url ? { syncTrigger: { url: config.syncTrigger.url } } : {}
752
+ ...(() => {
753
+ const url = resolveTriggerUrl({
754
+ configuredUrl: config.syncTrigger?.url,
755
+ configuredPath: config.syncTrigger?.path,
756
+ env: config.env ?? process.env,
757
+ observedUrl
758
+ });
759
+ return url ? { syncTrigger: { url } } : {};
760
+ })()
635
761
  })
636
762
  });
637
763
  const heartbeat = async () => {
@@ -685,6 +811,7 @@ function createClivlySDK(config) {
685
811
  }
686
812
  };
687
813
  const createClivlyHandler = () => async (req) => {
814
+ observedUrl = observedTriggerUrl(req) ?? observedUrl;
688
815
  const rawBody = await req.text().catch(() => "");
689
816
  if (!await triggerRequestAllowed(req, rawBody)) return jsonResponse({
690
817
  ok: false,
@@ -1,4 +1,41 @@
1
- const require_drizzle_meta = require("./drizzle-meta-CM5M73Z4.cjs");
1
+ const require_drizzle_meta = require("./drizzle-meta-BVR1qiyu.cjs");
2
+ //#region ../sdk/src/custom-slug.ts
3
+ const NON_ALNUM = /[^a-z0-9]+/g;
4
+ const EDGE_HYPHENS = /(^-|-$)/g;
5
+ function slugify(value) {
6
+ return value.toLowerCase().trim().replace(NON_ALNUM, "-").replace(EDGE_HYPHENS, "");
7
+ }
8
+ function checkCustomObjectSlugs(config) {
9
+ const custom = config.source?.custom ?? [];
10
+ if (custom.length === 0) return {
11
+ name: "Custom object slugs",
12
+ ok: false,
13
+ skipped: true,
14
+ detail: "No custom objects configured."
15
+ };
16
+ const problems = [];
17
+ const seen = /* @__PURE__ */ new Map();
18
+ for (const source of custom) {
19
+ const declared = source.objectType;
20
+ const canonical = slugify(declared);
21
+ if (canonical === "") problems.push(`"${source.entity}" has an empty objectType.`);
22
+ else if (canonical !== declared) problems.push(`"${declared}" is not a slug — the dashboard would store it as "${canonical}".`);
23
+ const prior = seen.get(canonical);
24
+ if (prior && canonical !== "") problems.push(`duplicate slug "${canonical}" declared by "${prior}" and "${source.entity}".`);
25
+ else seen.set(canonical, source.entity);
26
+ }
27
+ if (problems.length > 0) return {
28
+ name: "Custom object slugs",
29
+ ok: false,
30
+ detail: problems.join(" ")
31
+ };
32
+ return {
33
+ name: "Custom object slugs",
34
+ ok: true,
35
+ detail: `Ensure these object types exist in Clivly: ${custom.map((source) => source.objectType).join(", ")}.`
36
+ };
37
+ }
38
+ //#endregion
2
39
  //#region ../sdk/src/namespace-probe.ts
3
40
  /**
4
41
  * Verify the tables the integration depends on are genuinely reachable:
@@ -159,6 +196,7 @@ async function runDoctor(args) {
159
196
  ok: result.ok,
160
197
  detail: result.ok ? `Reached Clivly — org "${result.org?.name ?? "(unnamed)"}".` : `Could not connect (${result.reason}).`
161
198
  });
199
+ checks.push(checkCustomObjectSlugs(config));
162
200
  checks.push(await namespaceCheck(config));
163
201
  return {
164
202
  ok: checks.every((check) => check.skipped || check.ok),
@@ -257,6 +295,85 @@ async function fullSyncSource(args) {
257
295
  };
258
296
  }
259
297
  //#endregion
298
+ //#region ../sdk/src/trigger-url.ts
299
+ const SCHEME_REGEX = /^https?:\/\//;
300
+ const INVALID_HOST_CHARS_REGEX = /[/\\@?#\s]/;
301
+ const PLATFORM_ORIGIN_SOURCES = [
302
+ {
303
+ key: "VERCEL_URL",
304
+ toOrigin: (value) => withScheme(value)
305
+ },
306
+ {
307
+ key: "CF_PAGES_URL",
308
+ toOrigin: (value) => withScheme(value)
309
+ },
310
+ {
311
+ key: "RAILWAY_PUBLIC_DOMAIN",
312
+ toOrigin: (value) => withScheme(value)
313
+ },
314
+ {
315
+ key: "RENDER_EXTERNAL_URL",
316
+ toOrigin: (value) => withScheme(value)
317
+ },
318
+ {
319
+ key: "FLY_APP_NAME",
320
+ toOrigin: (value) => `https://${value}.fly.dev`
321
+ }
322
+ ];
323
+ function withScheme(value) {
324
+ return SCHEME_REGEX.test(value) ? value : `https://${value}`;
325
+ }
326
+ function stripTrailingSlash(value) {
327
+ return value.endsWith("/") ? value.slice(0, -1) : value;
328
+ }
329
+ /**
330
+ * Derive the app's public origin from known platform environment variables.
331
+ * Returns an origin with no trailing slash, or undefined when nothing matches.
332
+ */
333
+ function originFromPlatformEnv(env) {
334
+ for (const source of PLATFORM_ORIGIN_SOURCES) {
335
+ const raw = env[source.key]?.trim();
336
+ if (raw) return stripTrailingSlash(source.toOrigin(raw));
337
+ }
338
+ }
339
+ function observedTriggerUrl(req) {
340
+ const host = (req.headers.get("x-forwarded-host") ?? req.headers.get("host"))?.trim();
341
+ if (!host) return;
342
+ if (INVALID_HOST_CHARS_REGEX.test(host)) return;
343
+ const forwardedProto = req.headers.get("x-forwarded-proto")?.split(",")[0]?.trim();
344
+ let requestUrl;
345
+ try {
346
+ requestUrl = new URL(req.url);
347
+ } catch {
348
+ return;
349
+ }
350
+ const proto = forwardedProto || requestUrl.protocol.replace(":", "");
351
+ try {
352
+ return new URL(`${proto}://${host}${requestUrl.pathname}`).toString();
353
+ } catch {
354
+ return;
355
+ }
356
+ }
357
+ /**
358
+ * Decide which trigger URL to report, in precedence order:
359
+ *
360
+ * 1. `configuredUrl` — an explicit `syncTrigger.url`. Someone typed this on
361
+ * purpose, so it beats anything inferred.
362
+ * 2. platform env origin + `configuredPath` — available at boot, before any
363
+ * request has arrived. Needs the path because platform variables only give
364
+ * an origin.
365
+ * 3. `observedUrl` — accurate but only after the route has served a request.
366
+ *
367
+ * A URL set in the Clivly dashboard overrides all of these server-side; the SDK
368
+ * doesn't need to know about it.
369
+ */
370
+ function resolveTriggerUrl(args) {
371
+ if (args.configuredUrl) return args.configuredUrl;
372
+ const origin = originFromPlatformEnv(args.env);
373
+ if (origin && args.configuredPath) return `${origin}${args.configuredPath.startsWith("/") ? args.configuredPath : `/${args.configuredPath}`}`;
374
+ return args.observedUrl;
375
+ }
376
+ //#endregion
260
377
  //#region ../sdk/src/errors.ts
261
378
  var ClivlyError = class ClivlyError extends Error {
262
379
  /** HTTP status, or null when the request never completed (network error). */
@@ -547,6 +664,7 @@ function createClivlySDK(config) {
547
664
  const cursorStore = config.cursorStore ?? createMemoryCursorStore();
548
665
  const fetcher = createFetcher(config.retry);
549
666
  let warnedUnsignedTrigger = false;
667
+ let observedUrl;
550
668
  let lastPushFailure = null;
551
669
  const createPush = (runId) => async (path, body) => {
552
670
  const response = await fetcher(`${apiUrl}${path}`, {
@@ -631,7 +749,15 @@ function createClivlySDK(config) {
631
749
  contactEntity: resolveContactEntity(config),
632
750
  namespaceReady: config.namespaceReady ?? false,
633
751
  schema: buildSchema(config),
634
- ...config.syncTrigger?.url ? { syncTrigger: { url: config.syncTrigger.url } } : {}
752
+ ...(() => {
753
+ const url = resolveTriggerUrl({
754
+ configuredUrl: config.syncTrigger?.url,
755
+ configuredPath: config.syncTrigger?.path,
756
+ env: config.env ?? process.env,
757
+ observedUrl
758
+ });
759
+ return url ? { syncTrigger: { url } } : {};
760
+ })()
635
761
  })
636
762
  });
637
763
  const heartbeat = async () => {
@@ -685,6 +811,7 @@ function createClivlySDK(config) {
685
811
  }
686
812
  };
687
813
  const createClivlyHandler = () => async (req) => {
814
+ observedUrl = observedTriggerUrl(req) ?? observedUrl;
688
815
  const rawBody = await req.text().catch(() => "");
689
816
  if (!await triggerRequestAllowed(req, rawBody)) return jsonResponse({
690
817
  ok: false,
package/sdk.cjs CHANGED
@@ -1,5 +1,5 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_sdk = require("./sdk-BNdK3KgW.cjs");
2
+ const require_sdk = require("./sdk-r4khXzoH.cjs");
3
3
  exports.ClivlyAuthError = require_sdk.ClivlyAuthError;
4
4
  exports.ClivlyError = require_sdk.ClivlyError;
5
5
  exports.ClivlyNetworkError = require_sdk.ClivlyNetworkError;
package/sdk.d.cts CHANGED
@@ -1,4 +1,4 @@
1
- import { a as ClivlySDKConfig, c as DiscoveredTable, i as ClivlySDK, l as DoctorCheck, n as ClivlyConnectReason, o as ClivlyUser, r as ClivlyConnectResult, s as CursorStore, u as DoctorReport } from "./namespace-probe-BtS0nXcr.cjs";
1
+ import { a as ClivlySDKConfig, c as DiscoveredTable, i as ClivlySDK, l as DoctorCheck, n as ClivlyConnectReason, o as ClivlyUser, r as ClivlyConnectResult, s as CursorStore, u as DoctorReport } from "./namespace-probe-CcYne5Je.cjs";
2
2
 
3
3
  //#region ../sdk/src/errors.d.ts
4
4
  declare class ClivlyError extends Error {
package/sdk.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { a as ClivlySDKConfig, c as DiscoveredTable, i as ClivlySDK, l as DoctorCheck, n as ClivlyConnectReason, o as ClivlyUser, r as ClivlyConnectResult, s as CursorStore, u as DoctorReport } from "./namespace-probe-CYMie-BI.mjs";
1
+ import { a as ClivlySDKConfig, c as DiscoveredTable, i as ClivlySDK, l as DoctorCheck, n as ClivlyConnectReason, o as ClivlyUser, r as ClivlyConnectResult, s as CursorStore, u as DoctorReport } from "./namespace-probe-DjWzU9gG.mjs";
2
2
 
3
3
  //#region ../sdk/src/errors.d.ts
4
4
  declare class ClivlyError extends Error {
package/sdk.mjs CHANGED
@@ -1,2 +1,2 @@
1
- import { a as ClivlyRateLimitError, i as ClivlyNetworkError, n as ClivlyAuthError, r as ClivlyError, t as createClivlySDK } from "./sdk-vu_uej2q.mjs";
1
+ import { a as ClivlyRateLimitError, i as ClivlyNetworkError, n as ClivlyAuthError, r as ClivlyError, t as createClivlySDK } from "./sdk-DtFmeuwl.mjs";
2
2
  export { ClivlyAuthError, ClivlyError, ClivlyNetworkError, ClivlyRateLimitError, createClivlySDK };
package/vite.d.cts CHANGED
@@ -1,4 +1,4 @@
1
- import { i as ClivlySDK } from "./namespace-probe-BtS0nXcr.cjs";
1
+ import { i as ClivlySDK } from "./namespace-probe-CcYne5Je.cjs";
2
2
  import { Plugin } from "vite";
3
3
 
4
4
  //#region ../sdk/src/vite.d.ts
package/vite.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { i as ClivlySDK } from "./namespace-probe-CYMie-BI.mjs";
1
+ import { i as ClivlySDK } from "./namespace-probe-DjWzU9gG.mjs";
2
2
  import { Plugin } from "vite";
3
3
 
4
4
  //#region ../sdk/src/vite.d.ts