@cowliss/sdk 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Bogdan Radu
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,60 @@
1
+ # @cowliss/sdk
2
+
3
+ The TypeScript client for the Cowliss ingestion API. Send `identify` and
4
+ `track` from your app, and Cowliss keeps the profiles, the events, the
5
+ segments, and the journeys that follow from them.
6
+
7
+ Runs anywhere `fetch` does: Node 22 and up, Bun, Deno, edge runtimes and
8
+ browsers. Zero dependencies, ESM only, fully typed.
9
+
10
+ ## Install
11
+
12
+ ```sh
13
+ npm install @cowliss/sdk
14
+ ```
15
+
16
+ ## Use it
17
+
18
+ ```ts
19
+ import { Cow } from "@cowliss/sdk";
20
+
21
+ const cow = new Cow({
22
+ apiKey: process.env.COW_API_KEY!,
23
+ sourceId: process.env.COW_SOURCE_ID!,
24
+ baseUrl: "https://api.cowliss.com",
25
+ });
26
+
27
+ await cow.identify({
28
+ identifiers: { userId: "u_1", email: "ada@example.com" },
29
+ traits: { plan: "pro" },
30
+ });
31
+
32
+ await cow.track({
33
+ identifiers: { userId: "u_1" },
34
+ event: "checkout.completed",
35
+ properties: { amount: 4900, currency: "usd" },
36
+ });
37
+ ```
38
+
39
+ Every call carries `identifiers`, a map from kind to value. Cowliss generates
40
+ the profile id and resolves the map to it, so a call that carries two
41
+ identifiers is what links two profiles. There is no alias call.
42
+
43
+ `identify` returns the profile, `track` returns the stored event, and
44
+ `batch` imports many of both in one request for backfills. Each call names
45
+ the source it arrives through: set `sourceId` once on the client, or pass one
46
+ per call when a single process writes into more than one app.
47
+
48
+ Retries are handled for you. A call that fails to reach Cowliss, or that
49
+ Cowliss could not complete, backs off and tries twice more; a generated
50
+ `messageId` travels with every track, so a retry after a lost response never
51
+ writes the event twice. A call Cowliss rejected is not retried: it throws a
52
+ `CowError` carrying the reason.
53
+
54
+ ## Docs
55
+
56
+ [docs.cowliss.com](https://docs.cowliss.com)
57
+
58
+ ## License
59
+
60
+ MIT
@@ -0,0 +1,69 @@
1
+ import type { BatchBody, BatchResultDto, ErrorCode, EventDto, IdentifyInput, ProfileDto, TrackInput } from "./types.js";
2
+ export type * from "./types.js";
3
+ /**
4
+ * The Cowliss TS client SDK. Small typed surface over the ingestion API:
5
+ * cow.identify / .track / .batch. Every call carries `identifiers` (a map
6
+ * from kind to value; Cowliss generates the profile id and resolves them), so
7
+ * there is no alias call: linking happens whenever one call carries two
8
+ * identifiers. Every call also names the source it arrives through: give the
9
+ * constructor a `sourceId` and the calls inherit it, or name one per call
10
+ * when one process writes into more than one app. messageIds are generated
11
+ * automatically (and double as the Idempotency-Key, so a retry after a lost
12
+ * response dedupes instead of double-writing); network errors and 5xx retry with exponential backoff;
13
+ * 4xx surface as typed CowErrors with the API's error-code enum, never
14
+ * retried.
15
+ */
16
+ export declare class CowError extends Error {
17
+ readonly code: ErrorCode | "network_error";
18
+ readonly status: number;
19
+ constructor(code: ErrorCode | "network_error", message: string, status: number, options?: {
20
+ cause?: unknown;
21
+ });
22
+ }
23
+ export type CowOptions = {
24
+ /** Org ingestion API key ("Bearer" credential on every request). */
25
+ apiKey: string;
26
+ /**
27
+ * The `src_` source every call arrives through, when the caller does not
28
+ * name one per call. An app's source is a deployment fact, not a per-call
29
+ * one, so the common case is one client per app configured once here; a
30
+ * process that writes into two apps passes `sourceId` per call (or holds
31
+ * two clients) and this default stays out of the way.
32
+ */
33
+ sourceId?: string;
34
+ /** API origin, no trailing slash. Defaults to local dev. */
35
+ baseUrl?: string;
36
+ /** Retries after the initial attempt on network errors / 5xx. Default 2. */
37
+ maxRetries?: number;
38
+ /** Injectable for tests and non-fetch runtimes. */
39
+ fetch?: typeof globalThis.fetch;
40
+ };
41
+ /**
42
+ * The per-call shape: `sourceId` is optional here and required on the wire,
43
+ * so a client constructed with one takes calls without it. `#sourceFor`
44
+ * puts it back before the request, or fails naming both ways to supply it.
45
+ */
46
+ type WithDefaultSource<T extends {
47
+ sourceId: string;
48
+ }> = Omit<T, "sourceId"> & {
49
+ sourceId?: string;
50
+ };
51
+ export declare class Cow {
52
+ #private;
53
+ constructor(options: CowOptions);
54
+ identify(input: WithDefaultSource<IdentifyInput>): Promise<ProfileDto>;
55
+ /**
56
+ * Tracks one event. A messageId is generated when the caller does not
57
+ * supply one, and sent both in the body and as the Idempotency-Key, so a
58
+ * retried request (network error, 5xx, or lost response) dedupes
59
+ * server-side. Historical timestamps pass through for backfill scripts.
60
+ */
61
+ track(input: WithDefaultSource<TrackInput>): Promise<EventDto>;
62
+ /**
63
+ * Batch import: one source's identify and track calls in one request.
64
+ * Track items must carry an explicit messageId: the API requires it and
65
+ * the SDK does not generate one here, so re-running the same backfill
66
+ * (same messageIds) dedupes instead of double-writing.
67
+ */
68
+ batch(input: WithDefaultSource<BatchBody["data"]>): Promise<BatchResultDto>;
69
+ }
package/dist/index.js ADDED
@@ -0,0 +1,113 @@
1
+ import { CLIENT_HEADER, SDK_VERSION } from "./types.js";
2
+ /** What this client calls itself in {@link CLIENT_HEADER}. */
3
+ const CLIENT_ID = `@cowliss/sdk/${SDK_VERSION}`;
4
+ /**
5
+ * The Cowliss TS client SDK. Small typed surface over the ingestion API:
6
+ * cow.identify / .track / .batch. Every call carries `identifiers` (a map
7
+ * from kind to value; Cowliss generates the profile id and resolves them), so
8
+ * there is no alias call: linking happens whenever one call carries two
9
+ * identifiers. Every call also names the source it arrives through: give the
10
+ * constructor a `sourceId` and the calls inherit it, or name one per call
11
+ * when one process writes into more than one app. messageIds are generated
12
+ * automatically (and double as the Idempotency-Key, so a retry after a lost
13
+ * response dedupes instead of double-writing); network errors and 5xx retry with exponential backoff;
14
+ * 4xx surface as typed CowErrors with the API's error-code enum, never
15
+ * retried.
16
+ */
17
+ export class CowError extends Error {
18
+ code;
19
+ status;
20
+ constructor(code, message, status, options) {
21
+ super(message, options);
22
+ this.name = "CowError";
23
+ this.code = code;
24
+ this.status = status;
25
+ }
26
+ }
27
+ const RETRY_BASE_DELAY_MS = 100;
28
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
29
+ export class Cow {
30
+ #apiKey;
31
+ #sourceId;
32
+ #baseUrl;
33
+ #maxRetries;
34
+ #fetch;
35
+ constructor(options) {
36
+ this.#apiKey = options.apiKey;
37
+ this.#sourceId = options.sourceId;
38
+ this.#baseUrl = options.baseUrl ?? "http://localhost:3400";
39
+ this.#maxRetries = options.maxRetries ?? 2;
40
+ this.#fetch = options.fetch ?? globalThis.fetch;
41
+ }
42
+ /**
43
+ * The call's own `sourceId`, else the client's. Throws when neither, from
44
+ * inside an async method so the caller sees a rejected promise like every
45
+ * other failure rather than a synchronous throw a `.catch()` would miss.
46
+ */
47
+ #sourceFor(input) {
48
+ const sourceId = input.sourceId ?? this.#sourceId;
49
+ if (!sourceId) {
50
+ throw new CowError("validation_failed", "No sourceId: pass one to the Cow constructor, or name one on the call.", 0);
51
+ }
52
+ return sourceId;
53
+ }
54
+ async identify(input) {
55
+ return this.#post("/v1/identify", {
56
+ data: { ...input, sourceId: this.#sourceFor(input) },
57
+ });
58
+ }
59
+ /**
60
+ * Tracks one event. A messageId is generated when the caller does not
61
+ * supply one, and sent both in the body and as the Idempotency-Key, so a
62
+ * retried request (network error, 5xx, or lost response) dedupes
63
+ * server-side. Historical timestamps pass through for backfill scripts.
64
+ */
65
+ async track(input) {
66
+ const messageId = input.messageId ?? crypto.randomUUID();
67
+ return this.#post("/v1/track", { data: { ...input, sourceId: this.#sourceFor(input), messageId } }, messageId);
68
+ }
69
+ /**
70
+ * Batch import: one source's identify and track calls in one request.
71
+ * Track items must carry an explicit messageId: the API requires it and
72
+ * the SDK does not generate one here, so re-running the same backfill
73
+ * (same messageIds) dedupes instead of double-writing.
74
+ */
75
+ async batch(input) {
76
+ return this.#post("/v1/batch", {
77
+ data: { ...input, sourceId: this.#sourceFor(input) },
78
+ });
79
+ }
80
+ async #post(path, body, idempotencyKey) {
81
+ for (let attempt = 0;; attempt++) {
82
+ let res;
83
+ try {
84
+ res = await this.#fetch(`${this.#baseUrl}${path}`, {
85
+ method: "POST",
86
+ headers: {
87
+ Authorization: `Bearer ${this.#apiKey}`,
88
+ "Content-Type": "application/json",
89
+ [CLIENT_HEADER]: CLIENT_ID,
90
+ ...(idempotencyKey ? { "Idempotency-Key": idempotencyKey } : {}),
91
+ },
92
+ body: JSON.stringify(body),
93
+ });
94
+ }
95
+ catch (cause) {
96
+ if (attempt < this.#maxRetries) {
97
+ await sleep(RETRY_BASE_DELAY_MS * 2 ** attempt);
98
+ continue;
99
+ }
100
+ throw new CowError("network_error", "Network error", 0, { cause });
101
+ }
102
+ if (res.ok) {
103
+ return (await res.json()).data;
104
+ }
105
+ if (res.status >= 500 && attempt < this.#maxRetries) {
106
+ await sleep(RETRY_BASE_DELAY_MS * 2 ** attempt);
107
+ continue;
108
+ }
109
+ const parsed = (await res.json().catch(() => null));
110
+ throw new CowError(parsed?.error?.code ?? "internal", parsed?.error?.message ?? `Request failed with status ${res.status}`, res.status);
111
+ }
112
+ }
113
+ }
@@ -0,0 +1,131 @@
1
+ /**
2
+ * The SDK's public types, hand-declared.
3
+ *
4
+ * They mirror `@cowliss/shared`, and they do not import it. The shared ones
5
+ * are `z.infer` over drizzle-zod schemas, so a declaration that referenced
6
+ * them would make drizzle-orm a type dependency of an ingestion client and
7
+ * would name a package a customer has never installed (which `skipLibCheck`
8
+ * would degrade to `any` in silence rather than report). Plain object types
9
+ * keep the SDK's `dist` free of both.
10
+ *
11
+ * `types.check.ts` asserts each of these is mutually assignable with the
12
+ * shared type it mirrors, so a wire-shape change fails the monorepo
13
+ * typecheck here rather than at a customer's call site.
14
+ */
15
+ /** The API's error-code enum, as the SDK surfaces it on `CowError.code`. */
16
+ export type ErrorCode = "malformed_request" | "invalid_key" | "forbidden" | "not_found" | "conflict" | "payload_too_large" | "validation_failed" | "trait_validation_failed" | "event_denied" | "trait_denied" | "over_quota" | "rate_limited" | "dependency_unavailable" | "internal" | "journey_fuel_exceeded" | "journey_timeout" | "journey_memory_exceeded" | "journey_error" | "journey_output_invalid" | "journey_nondeterministic" | "release_invalid" | "release_not_ready" | "project_exists" | "project_missing" | "artifact_too_large" | "deploy_key_scope";
17
+ export type Environment = "development" | "production";
18
+ export type IdentifierKind = "anonymousId" | "userId" | "clerkId" | "email";
19
+ /**
20
+ * The named identifiers a call carries, at least one. Cowliss generates the
21
+ * profile id and resolves the map to it; a call carrying two identifiers is
22
+ * what links two profiles, so there is no alias call.
23
+ */
24
+ export type Identifiers = Partial<Record<IdentifierKind, string>>;
25
+ export type IdentifierDto = {
26
+ kind: IdentifierKind;
27
+ value: string;
28
+ createdAt: string;
29
+ };
30
+ /** A person, as identify returns them. */
31
+ export type ProfileDto = {
32
+ id: string;
33
+ orgId: string;
34
+ environment: Environment;
35
+ /** The app that first created the profile. */
36
+ appId: string;
37
+ /** The source the creating write arrived through. */
38
+ sourceId: string;
39
+ traits: Record<string, unknown>;
40
+ consent: Record<string, boolean>;
41
+ /** Set on a profile that was merged away, naming the survivor. */
42
+ mergedInto: string | null;
43
+ createdAt: string;
44
+ updatedAt: string;
45
+ identifiers: IdentifierDto[];
46
+ /** True when every identifier is an anonymousId. */
47
+ anonymous: boolean;
48
+ };
49
+ /** One stored event, as track returns it. */
50
+ export type EventDto = {
51
+ id: string;
52
+ orgId: string;
53
+ environment: Environment;
54
+ appId: string;
55
+ sourceId: string;
56
+ profileId: string;
57
+ event: string;
58
+ properties: Record<string, unknown>;
59
+ timestamp: string;
60
+ receivedAt: string;
61
+ messageId: string | null;
62
+ };
63
+ export type IdentifyInput = {
64
+ sourceId: string;
65
+ identifiers: Identifiers;
66
+ traits?: Record<string, unknown>;
67
+ /** ISO 8601 UTC. A past timestamp backdates a first-write profile. */
68
+ timestamp?: string;
69
+ messageId?: string;
70
+ };
71
+ export type TrackInput = {
72
+ sourceId: string;
73
+ identifiers: Identifiers;
74
+ event: string;
75
+ properties?: Record<string, unknown>;
76
+ /** ISO 8601 UTC. A past timestamp is the event time, for backfill. */
77
+ timestamp?: string;
78
+ /** Generated by the SDK when omitted; also sent as the Idempotency-Key. */
79
+ messageId?: string;
80
+ };
81
+ /** One batch item: an identify or a track, minus the batch's own source. */
82
+ export type BatchItem = ({
83
+ type: "identify";
84
+ } & Omit<IdentifyInput, "sourceId">) | ({
85
+ type: "track";
86
+ } & Omit<TrackInput, "sourceId"> & {
87
+ messageId: string;
88
+ });
89
+ export type BatchBody = {
90
+ data: {
91
+ sourceId: string;
92
+ /**
93
+ * Validated per item server-side, so one bad row never fails the import:
94
+ * the wire schema accepts anything here and reports item problems in the
95
+ * response. `BatchItem` is what a caller should send.
96
+ */
97
+ items: unknown[];
98
+ };
99
+ };
100
+ export type BatchItemResult = {
101
+ ok: true;
102
+ type: "identify";
103
+ id: string;
104
+ } | {
105
+ ok: true;
106
+ type: "track";
107
+ id: string;
108
+ deduped: boolean;
109
+ } | {
110
+ ok: false;
111
+ error: {
112
+ code: ErrorCode;
113
+ message: string;
114
+ };
115
+ };
116
+ export type BatchResultDto = {
117
+ results: BatchItemResult[];
118
+ };
119
+ /**
120
+ * The two runtime constants in this file, and the reason they live beside
121
+ * the types rather than in `@cowliss/shared`: this module is the SDK's one
122
+ * mirror of the shared vocabulary (`docs/standards/code.md`, "Central
123
+ * constants"), because a published package may not name an unpublished one.
124
+ *
125
+ * `CLIENT_HEADER` is pinned against the shared constant by `types.check.ts`.
126
+ * `SDK_VERSION` mirrors this package's own `version` instead, which no
127
+ * compiler can read from here: `apps/api/src/sdk.test.ts` is what fails when
128
+ * a release bumps one and not the other.
129
+ */
130
+ export declare const CLIENT_HEADER = "X-Cow-Client";
131
+ export declare const SDK_VERSION = "0.1.0";
package/dist/types.js ADDED
@@ -0,0 +1,27 @@
1
+ /**
2
+ * The SDK's public types, hand-declared.
3
+ *
4
+ * They mirror `@cowliss/shared`, and they do not import it. The shared ones
5
+ * are `z.infer` over drizzle-zod schemas, so a declaration that referenced
6
+ * them would make drizzle-orm a type dependency of an ingestion client and
7
+ * would name a package a customer has never installed (which `skipLibCheck`
8
+ * would degrade to `any` in silence rather than report). Plain object types
9
+ * keep the SDK's `dist` free of both.
10
+ *
11
+ * `types.check.ts` asserts each of these is mutually assignable with the
12
+ * shared type it mirrors, so a wire-shape change fails the monorepo
13
+ * typecheck here rather than at a customer's call site.
14
+ */
15
+ /**
16
+ * The two runtime constants in this file, and the reason they live beside
17
+ * the types rather than in `@cowliss/shared`: this module is the SDK's one
18
+ * mirror of the shared vocabulary (`docs/standards/code.md`, "Central
19
+ * constants"), because a published package may not name an unpublished one.
20
+ *
21
+ * `CLIENT_HEADER` is pinned against the shared constant by `types.check.ts`.
22
+ * `SDK_VERSION` mirrors this package's own `version` instead, which no
23
+ * compiler can read from here: `apps/api/src/sdk.test.ts` is what fails when
24
+ * a release bumps one and not the other.
25
+ */
26
+ export const CLIENT_HEADER = "X-Cow-Client";
27
+ export const SDK_VERSION = "0.1.0";
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "@cowliss/sdk",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "exports": {
6
+ ".": {
7
+ "types": "./dist/index.d.ts",
8
+ "default": "./dist/index.js"
9
+ }
10
+ },
11
+ "devDependencies": {
12
+ "@cowliss/shared": "0.0.0",
13
+ "@types/node": "^26.2.0"
14
+ },
15
+ "description": "TypeScript client for the Cowliss ingestion API: identify, track and batch.",
16
+ "license": "MIT",
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "git+https://github.com/bogdanrn/cowliss.git",
20
+ "directory": "packages/sdk"
21
+ },
22
+ "homepage": "https://docs.cowliss.com",
23
+ "publishConfig": {
24
+ "access": "public"
25
+ },
26
+ "engines": {
27
+ "node": ">=22"
28
+ },
29
+ "files": [
30
+ "dist",
31
+ "LICENSE",
32
+ "README.md"
33
+ ],
34
+ "scripts": {
35
+ "build": "tsc -b tsconfig.json --force",
36
+ "typecheck": "tsc -b tsconfig.json tsconfig.check.json"
37
+ }
38
+ }