@clivly/sdk 0.1.0 → 0.2.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.
@@ -0,0 +1,224 @@
1
+ import { ClivlyEntitiesConfig } from "@clivly/core";
2
+
3
+ //#region src/drizzle-meta.d.ts
4
+ declare const DRIZZLE_META: unique symbol;
5
+ interface DrizzleSourceMeta {
6
+ /** Drizzle property keys of every column on the source table. */
7
+ columnKeys: string[];
8
+ cursorField: string;
9
+ idField: string;
10
+ }
11
+ //#endregion
12
+ //#region src/types.d.ts
13
+ interface DiscoveredTable {
14
+ columns: string[];
15
+ name: string;
16
+ }
17
+ interface ClivlySDKConfig {
18
+ /** Org-scoped API token (sk_live_…) from Settings → API keys. */
19
+ apiKey: string;
20
+ /** Clivly API base URL. Defaults to https://api.clivly.com. */
21
+ apiUrl?: string;
22
+ /**
23
+ * The host table Clivly treats as the contact entity.
24
+ * @deprecated Prefer `entities` — the same mapping-derived shape
25
+ * `crm_entity_mappings` (and `@clivly/core`'s `mappingsToEntitiesConfig`)
26
+ * produce, so the SDK, the mappings table, and the sync engine share one
27
+ * model. A bare `contactEntity` is treated as a single contact entity.
28
+ */
29
+ contactEntity?: string;
30
+ /** Which columns of the contact entity Clivly may read. */
31
+ contactFields?: string[];
32
+ /**
33
+ * Where per-entity delta cursors are persisted between syncs. Defaults to an
34
+ * in-memory store (fine for a long-lived process). On serverless/edge, pass a
35
+ * store backed by your own KV/DB so a cold start resumes delta sync instead of
36
+ * re-paging the whole entity. See `CursorStore`.
37
+ */
38
+ cursorStore?: CursorStore;
39
+ /**
40
+ * Declarative entity config — the single source of truth shared with
41
+ * `crm_entity_mappings`. When set, discovery and heartbeat are derived from
42
+ * it (each entity's `source` table + mapped columns), superseding the flat
43
+ * `contactEntity`.
44
+ */
45
+ entities?: ClivlyEntitiesConfig;
46
+ /**
47
+ * 🚧 Not implemented in 0.1.x. Reserved options for tunnel mode and CRM
48
+ * write-back — they have NO effect yet and land with the Cloudflare
49
+ * infrastructure. Grouped under `experimental` so the stable config surface
50
+ * above is unambiguous; nothing here is part of the supported API.
51
+ */
52
+ experimental?: {
53
+ /** Cloudflare Tunnel token — reserved for tunnel mode. Not wired up yet. */tunnelToken?: string; /** CRM write-back target — reserved. Not wired up yet. */
54
+ writeBack?: {
55
+ enabled?: boolean; /** Only crm_* tables are ever accepted; this narrows further. */
56
+ tables?: string[];
57
+ };
58
+ };
59
+ /** Reported to the portal so it can show the detected stack. */
60
+ framework?: string;
61
+ /** Heartbeat cadence in ms. Defaults to 60s. */
62
+ heartbeatIntervalMs?: number;
63
+ /** Whether the crm_* namespace exists in the host DB (self-reported). */
64
+ namespaceReady?: boolean;
65
+ orm?: string;
66
+ /**
67
+ * Network retry policy for outbound calls (heartbeat + sync push). Transient
68
+ * failures (network errors, HTTP 429, HTTP 5xx) are retried with exponential
69
+ * backoff + jitter; a `Retry-After` header on a 429 is honoured. Non-transient
70
+ * responses (e.g. 4xx other than 429) are returned immediately, not retried.
71
+ */
72
+ retry?: {
73
+ /** Total attempts including the first. Defaults to 3. Min 1. */maxAttempts?: number; /** Base backoff in ms (doubled each attempt). Defaults to 500. */
74
+ baseDelayMs?: number;
75
+ };
76
+ /** Tables/columns to report for entity discovery + mapping. */
77
+ schema?: DiscoveredTable[];
78
+ sdkVersion?: string;
79
+ /** Host entities to mirror into Clivly Cloud. */
80
+ source?: {
81
+ contacts: SyncSource;
82
+ companies?: SyncSource;
83
+ };
84
+ sync?: {
85
+ onBoot?: boolean;
86
+ /**
87
+ * Sync cadence in ms for the background keep-alive loop. Defaults to
88
+ * `heartbeatIntervalMs` (60s) so sync and heartbeat share a tick unless you
89
+ * decouple them — e.g. a slow full-mirror alongside frequent heartbeats.
90
+ */
91
+ intervalMs?: number;
92
+ batchSize?: number;
93
+ };
94
+ /**
95
+ * Shared secret Clivly Cloud presents (as `Authorization: Bearer <token>`) when
96
+ * calling the `/clivly/auth/verify` endpoint. Required for the verify handler to
97
+ * authenticate the caller; without it the handler fails closed (HTTP 503).
98
+ */
99
+ verifyToken?: string;
100
+ }
101
+ interface ClivlyUser {
102
+ email: string;
103
+ id: string;
104
+ name?: string;
105
+ }
106
+ interface DoctorCheck {
107
+ /** Human-readable outcome — what passed, or why it failed. */
108
+ detail: string;
109
+ /** Short label, e.g. "API key present". */
110
+ name: string;
111
+ /** True when the check passed. Ignored when `skipped` is true. */
112
+ ok: boolean;
113
+ /** True when the check could not run (e.g. a hand-written source). */
114
+ skipped?: boolean;
115
+ }
116
+ interface DoctorReport {
117
+ checks: DoctorCheck[];
118
+ /** AND of every non-skipped check's `ok`. */
119
+ ok: boolean;
120
+ }
121
+ type ClivlyConnectReason = "ok" | "invalid_api_key" | "forbidden" | "rate_limited" | "server_error" | "client_error" | "network_error";
122
+ interface ClivlyConnectResult {
123
+ ok: boolean;
124
+ /** Org resolved from a successful heartbeat, when the server returns it. */
125
+ org?: {
126
+ id?: string;
127
+ name?: string;
128
+ };
129
+ reason: ClivlyConnectReason;
130
+ /** Whether a retry could plausibly succeed (network, 429, 5xx). */
131
+ retryable: boolean;
132
+ /** HTTP status, or null when the request never completed (network error). */
133
+ status: number | null;
134
+ }
135
+ interface SyncCursor {
136
+ /** The row's id, breaking ties when timestamps collide. */
137
+ id: string;
138
+ /** The row's cursor-field value (typically `updated_at`). */
139
+ time: Date;
140
+ }
141
+ interface SyncSource {
142
+ cursorField: string;
143
+ entity: string;
144
+ fetchPage(args: {
145
+ since: SyncCursor | null;
146
+ limit: number;
147
+ }): Promise<Record<string, unknown>[]>;
148
+ /** Stable, unique tie-breaker column for keyset paging. Defaults to "id". */
149
+ idField?: string;
150
+ /** Set by `fromDrizzle`; lets `doctor` introspect the backing table. */
151
+ [DRIZZLE_META]?: DrizzleSourceMeta;
152
+ }
153
+ type SyncMode = "delta" | "full";
154
+ interface CursorStore {
155
+ get(entity: string): Promise<SyncCursor | null> | SyncCursor | null;
156
+ set(entity: string, cursor: SyncCursor | null): Promise<void> | void;
157
+ }
158
+ interface ClivlySDK {
159
+ /**
160
+ * Send a single heartbeat and report the outcome richly: `ok`, the HTTP
161
+ * `status` (null on network failure), a coarse `reason`, whether it's
162
+ * `retryable`, and the resolved `org` when the server returns it. Never throws
163
+ * — inspect the result, or turn a failure into an exception with
164
+ * `ClivlyError.from(result)`. Prefer this over `heartbeat()` for onboarding,
165
+ * health checks, and one-shot serverless calls.
166
+ */
167
+ connect(): Promise<ClivlyConnectResult>;
168
+ /**
169
+ * Build the `/clivly/auth/verify` handler the developer mounts in their app.
170
+ * Clivly's cloud calls it to validate a CRM user's identity against the host
171
+ * app's session. Pass a resolver that reads the host session from the request.
172
+ *
173
+ * The handler authenticates the caller against `config.verifyToken` (presented
174
+ * as `Authorization: Bearer <token>`) before invoking the resolver, so the
175
+ * endpoint can't be used by third parties to probe host sessions. It fails
176
+ * closed with HTTP 503 if `verifyToken` is unset (a missing config value, not a
177
+ * server fault — so it won't trip the host's 5xx error alerting).
178
+ */
179
+ createAuthVerifyHandler(resolveUser: (req: Request) => Promise<ClivlyUser | null> | ClivlyUser | null): (req: Request) => Promise<Response>;
180
+ /**
181
+ * A fetch handler to mount at a route and drive from an external scheduler
182
+ * (Cloudflare Workers cron, an uptime ping, etc.). Each request runs one
183
+ * `runScheduled()` and returns JSON: 200 when the sync succeeded, 500 otherwise.
184
+ * The ergonomic serverless entry point alongside `runScheduled()`.
185
+ */
186
+ createClivlyHandler(): (req: Request) => Promise<Response>;
187
+ /** Validate this setup end-to-end (read-only) before the first sync. */
188
+ doctor(): Promise<DoctorReport>;
189
+ /**
190
+ * Send a single heartbeat now. Returns true on success. Kept for the keep-alive
191
+ * loop and back-compat; use `connect()` when you need the failure reason.
192
+ */
193
+ heartbeat(): Promise<boolean>;
194
+ /**
195
+ * One-shot heartbeat + a single sync, with no keep-alive timers. The correct
196
+ * entry point on serverless/edge (Workers, Vercel, Lambda), where `start()`'s
197
+ * `setInterval` loop can't persist between invocations — call this from a
198
+ * scheduled/cron trigger. Throws if a sync push fails (so the run is marked
199
+ * failed); heartbeat is best-effort.
200
+ */
201
+ runScheduled(options?: {
202
+ mode?: SyncMode;
203
+ }): Promise<void>;
204
+ /**
205
+ * Send the initial heartbeat and start the keep-alive loop. For long-lived
206
+ * hosts only — on serverless/edge the interval can't persist, so prefer
207
+ * `runScheduled()` / `createClivlyHandler()`. Emits a warning if a serverless
208
+ * runtime is detected.
209
+ */
210
+ start(): Promise<void>;
211
+ /** Stop the keep-alive loop. */
212
+ stop(): void;
213
+ /**
214
+ * Sync all configured sources now. `delta` (default) pages changed rows since
215
+ * the last cursor; `full` pushes each complete entity so the server can
216
+ * reconcile leavers (archive-on-leave). Throws if a push fails, so callers see
217
+ * the failure — the background keep-alive loop swallows and retries instead.
218
+ */
219
+ sync(options?: {
220
+ mode?: SyncMode;
221
+ }): Promise<void>;
222
+ }
223
+ //#endregion
224
+ export { ClivlyUser as a, DoctorCheck as c, ClivlySDKConfig as i, DoctorReport as l, ClivlyConnectResult as n, CursorStore as o, ClivlySDK as r, DiscoveredTable as s, ClivlyConnectReason as t, SyncSource as u };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clivly/sdk",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Clivly SDK — connect your app to Clivly Cloud (heartbeat, discovery, auth bridge).",
@@ -8,8 +8,24 @@
8
8
  "sideEffects": false,
9
9
  "exports": {
10
10
  ".": {
11
- "types": "./dist/index.d.mts",
12
- "import": "./dist/index.mjs"
11
+ "import": {
12
+ "types": "./dist/index.d.mts",
13
+ "default": "./dist/index.mjs"
14
+ },
15
+ "require": {
16
+ "types": "./dist/index.d.cts",
17
+ "default": "./dist/index.cjs"
18
+ }
19
+ },
20
+ "./drizzle": {
21
+ "import": {
22
+ "types": "./dist/drizzle.d.mts",
23
+ "default": "./dist/drizzle.mjs"
24
+ },
25
+ "require": {
26
+ "types": "./dist/drizzle.d.cts",
27
+ "default": "./dist/drizzle.cjs"
28
+ }
13
29
  }
14
30
  },
15
31
  "files": [
@@ -19,19 +35,29 @@
19
35
  "access": "public"
20
36
  },
21
37
  "dependencies": {
22
- "@clivly/core": "0.1.0"
38
+ "@clivly/core": "0.2.0"
39
+ },
40
+ "peerDependencies": {
41
+ "drizzle-orm": ">=0.30"
42
+ },
43
+ "peerDependenciesMeta": {
44
+ "drizzle-orm": {
45
+ "optional": true
46
+ }
23
47
  },
24
48
  "devDependencies": {
49
+ "@electric-sql/pglite": "^0.5.4",
50
+ "drizzle-orm": "^0.45.2",
25
51
  "tsdown": "^0.21.9",
26
52
  "typescript": "^6",
27
- "vitest": "^3.2.0",
28
- "@clivly.com/config": "0.0.0"
53
+ "vitest": "^3.2.0"
29
54
  },
30
55
  "scripts": {
31
56
  "build": "tsdown",
32
57
  "check-types": "tsc --noEmit",
33
58
  "test": "vitest run"
34
59
  },
35
- "main": "./dist/index.mjs",
36
- "types": "./dist/index.d.mts"
60
+ "main": "./dist/index.cjs",
61
+ "module": "./dist/index.mjs",
62
+ "types": "./dist/index.d.cts"
37
63
  }