@gurulu/node 0.1.0 → 0.1.2

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.
@@ -1,341 +0,0 @@
1
- "use strict";
2
- /**
3
- * Canonical Event Envelope — Phase 10, W1.1
4
- *
5
- * Single source of truth for the event shape flowing through every ingest
6
- * lane (web SDK, server SDK, future app SDK). Pure module: no DB, no logging,
7
- * no side effects. Must run in Node (ingest routes) and be safe to bundle
8
- * into esbuild for the browser SDK.
9
- *
10
- * Related docs: PHASE-10-ROADMAP.md §W1.1
11
- */
12
- Object.defineProperty(exports, "__esModule", { value: true });
13
- exports.parseEnvelope = parseEnvelope;
14
- exports.normalizeLegacy = normalizeLegacy;
15
- exports.createEnvelope = createEnvelope;
16
- const EVENT_TIERS = ['raw', 'inferred', 'verified'];
17
- const EVENT_SOURCES = [
18
- 'client_sdk',
19
- 'server_sdk',
20
- 'system_inferred',
21
- 'user_confirmed',
22
- ];
23
- const CONSENT_LEVELS = [
24
- 'pending',
25
- 'accepted',
26
- 'rejected',
27
- ];
28
- const GRANULAR_CONSENT_LEVELS = [
29
- 'none',
30
- 'analytics',
31
- 'marketing',
32
- 'full',
33
- ];
34
- // ── UUID helper (dependency-free, safe in browser and Node) ─────────────────
35
- function generateUuid() {
36
- // Prefer crypto.randomUUID when available (Node 14.17+, modern browsers).
37
- const g = globalThis.crypto;
38
- if (g && typeof g.randomUUID === 'function') {
39
- try {
40
- return g.randomUUID();
41
- }
42
- catch {
43
- // fall through
44
- }
45
- }
46
- // RFC4122 v4 fallback using Math.random (non-cryptographic but deterministic-free).
47
- const hex = '0123456789abcdef';
48
- let out = '';
49
- for (let i = 0; i < 36; i++) {
50
- if (i === 8 || i === 13 || i === 18 || i === 23) {
51
- out += '-';
52
- }
53
- else if (i === 14) {
54
- out += '4';
55
- }
56
- else if (i === 19) {
57
- out += hex[(Math.random() * 4) | 0 | 8];
58
- }
59
- else {
60
- out += hex[(Math.random() * 16) | 0];
61
- }
62
- }
63
- return out;
64
- }
65
- // ── Field helpers ────────────────────────────────────────────────────────────
66
- function isObject(v) {
67
- return typeof v === 'object' && v !== null && !Array.isArray(v);
68
- }
69
- function asString(v) {
70
- if (typeof v === 'string')
71
- return v;
72
- return undefined;
73
- }
74
- function asNumber(v) {
75
- if (typeof v === 'number' && Number.isFinite(v))
76
- return v;
77
- return undefined;
78
- }
79
- function asProperties(v) {
80
- return isObject(v) ? v : {};
81
- }
82
- function requireString(obj, key, errors) {
83
- const v = obj[key];
84
- if (typeof v !== 'string' || v.length === 0) {
85
- errors.push(`missing or invalid '${key}' (expected non-empty string)`);
86
- return '';
87
- }
88
- return v;
89
- }
90
- function validTimestamp(v, errors) {
91
- if (typeof v === 'string') {
92
- if (!Number.isNaN(Date.parse(v)))
93
- return v;
94
- errors.push(`invalid 'timestamp' (unparseable string)`);
95
- return '';
96
- }
97
- if (typeof v === 'number' && Number.isFinite(v)) {
98
- return new Date(v).toISOString();
99
- }
100
- errors.push(`missing or invalid 'timestamp' (expected ISO-8601 string or epoch ms)`);
101
- return '';
102
- }
103
- function requireEnum(value, allowed, key, errors) {
104
- if (typeof value === 'string' && allowed.includes(value)) {
105
- return value;
106
- }
107
- errors.push(`invalid '${key}': expected one of [${allowed.join(', ')}], got ${JSON.stringify(value)}`);
108
- return allowed[0];
109
- }
110
- // ── parseEnvelope — strict ──────────────────────────────────────────────────
111
- /**
112
- * Strict parser. Accepts only already-canonical envelopes. Used on internal
113
- * pipeline boundaries where we know producers emit the full shape.
114
- *
115
- * Throws-in-return-value: never throws, returns ParseError on failure.
116
- */
117
- function parseEnvelope(raw) {
118
- const errors = [];
119
- if (!isObject(raw)) {
120
- return { ok: false, errors: ['envelope must be an object'] };
121
- }
122
- const event_id = requireString(raw, 'event_id', errors);
123
- const timestamp = validTimestamp(raw.timestamp, errors);
124
- const site_id = requireString(raw, 'site_id', errors);
125
- const anonymous_id = requireString(raw, 'anonymous_id', errors);
126
- const session_id = requireString(raw, 'session_id', errors);
127
- const event_type = requireString(raw, 'event_type', errors);
128
- const event_name = requireString(raw, 'event_name', errors);
129
- const page_url = requireString(raw, 'page_url', errors);
130
- const sdk_version = requireString(raw, 'sdk_version', errors);
131
- const event_tier = requireEnum(raw.event_tier, EVENT_TIERS, 'event_tier', errors);
132
- const event_source = requireEnum(raw.event_source, EVENT_SOURCES, 'event_source', errors);
133
- const consent_level = requireEnum(raw.consent_level, CONSENT_LEVELS, 'consent_level', errors);
134
- // Granular consent is optional — parse if present, skip if absent.
135
- const rawGranular = asString(raw.granular_consent_level);
136
- const granular_consent_level = rawGranular && GRANULAR_CONSENT_LEVELS.includes(rawGranular)
137
- ? rawGranular
138
- : undefined;
139
- if (errors.length > 0) {
140
- return { ok: false, errors };
141
- }
142
- const value = {
143
- event_id,
144
- timestamp,
145
- site_id,
146
- anonymous_id,
147
- canonical_id: asString(raw.canonical_id),
148
- session_id,
149
- event_type,
150
- event_name,
151
- event_tier,
152
- event_source,
153
- correlation_id: asString(raw.correlation_id),
154
- parent_event_id: asString(raw.parent_event_id),
155
- consent_level,
156
- granular_consent_level,
157
- sdk_version,
158
- config_version: asString(raw.config_version),
159
- page_url,
160
- page_title: asString(raw.page_title),
161
- referrer: asString(raw.referrer),
162
- utm_source: asString(raw.utm_source),
163
- utm_medium: asString(raw.utm_medium),
164
- utm_campaign: asString(raw.utm_campaign),
165
- utm_term: asString(raw.utm_term),
166
- utm_content: asString(raw.utm_content),
167
- device_type: asString(raw.device_type),
168
- browser: asString(raw.browser),
169
- os: asString(raw.os),
170
- screen_width: asNumber(raw.screen_width),
171
- screen_height: asNumber(raw.screen_height),
172
- device_id: asString(raw.device_id),
173
- phone: asString(raw.phone),
174
- event_source_platform: asString(raw.event_source_platform),
175
- properties: asProperties(raw.properties),
176
- };
177
- return { ok: true, value };
178
- }
179
- // ── normalizeLegacy — accept current SDK shape ──────────────────────────────
180
- /**
181
- * Maps the legacy SDK consent values ('necessary' | 'analytics' | 'marketing'
182
- * | 'rejected') onto the canonical envelope consent vocabulary. Anything
183
- * non-rejected is treated as 'accepted'; missing values become 'pending'.
184
- */
185
- function mapLegacyConsent(v) {
186
- if (typeof v !== 'string')
187
- return 'pending';
188
- if (v === 'rejected')
189
- return 'rejected';
190
- if (v === 'pending')
191
- return 'pending';
192
- if (v === 'accepted')
193
- return 'accepted';
194
- // 'necessary' | 'analytics' | 'marketing' all indicate affirmative consent.
195
- if (v === 'necessary' || v === 'analytics' || v === 'marketing') {
196
- return 'accepted';
197
- }
198
- return 'pending';
199
- }
200
- /**
201
- * Maps legacy SDK consent values to the 4-level granular consent system.
202
- * - 'rejected' / 'none' → 'none'
203
- * - 'necessary' / 'pending' → 'none' (privacy by default)
204
- * - 'analytics' → 'analytics'
205
- * - 'marketing' → 'marketing'
206
- * - 'accepted' / 'full' → 'full'
207
- * Also accepts an explicit granular_consent_level field if the SDK sends it.
208
- */
209
- function mapLegacyToGranularConsent(raw) {
210
- // If the SDK already sends granular_consent_level, prefer it
211
- const explicit = asString(raw.granular_consent_level);
212
- if (explicit && GRANULAR_CONSENT_LEVELS.includes(explicit)) {
213
- return explicit;
214
- }
215
- // Fall back to mapping the legacy consent_level
216
- const v = asString(raw.consent_level);
217
- if (!v)
218
- return 'none';
219
- switch (v) {
220
- case 'rejected':
221
- case 'none':
222
- case 'necessary':
223
- case 'pending':
224
- return 'none';
225
- case 'analytics':
226
- return 'analytics';
227
- case 'marketing':
228
- return 'marketing';
229
- case 'accepted':
230
- case 'full':
231
- return 'full';
232
- default:
233
- return 'none';
234
- }
235
- }
236
- /**
237
- * Accepts the loose JSON shape that the current `sdk/tracker.ts` sends and
238
- * promotes it to a canonical Envelope, filling defaults for fields the legacy
239
- * shape doesn't know about. Use this on ingest boundaries during the
240
- * migration window (Phase 10 Wave 1 → Wave 3).
241
- */
242
- function normalizeLegacy(raw) {
243
- const errors = [];
244
- if (!isObject(raw)) {
245
- return { ok: false, errors: ['legacy event must be an object'] };
246
- }
247
- // Required in the legacy shape
248
- const anonymous_id = requireString(raw, 'anonymous_id', errors);
249
- const session_id = requireString(raw, 'session_id', errors);
250
- const event_type = requireString(raw, 'event_type', errors);
251
- const event_name = requireString(raw, 'event_name', errors);
252
- if (errors.length > 0) {
253
- return { ok: false, errors };
254
- }
255
- const site_id = asString(raw.site_id) ?? ''; // collect route supplies site_id at the batch level
256
- const event_id = asString(raw.event_id) ?? generateUuid();
257
- const ts = raw.timestamp;
258
- const timestamp = typeof ts === 'string' && !Number.isNaN(Date.parse(ts))
259
- ? ts
260
- : typeof ts === 'number' && Number.isFinite(ts)
261
- ? new Date(ts).toISOString()
262
- : new Date().toISOString();
263
- const value = {
264
- event_id,
265
- timestamp,
266
- site_id,
267
- anonymous_id,
268
- canonical_id: asString(raw.canonical_id),
269
- session_id,
270
- event_type,
271
- event_name,
272
- event_tier: 'raw',
273
- event_source: 'client_sdk',
274
- correlation_id: asString(raw.correlation_id),
275
- parent_event_id: asString(raw.parent_event_id),
276
- consent_level: mapLegacyConsent(raw.consent_level),
277
- granular_consent_level: mapLegacyToGranularConsent(raw),
278
- sdk_version: asString(raw.sdk_version) ?? 'web@legacy',
279
- config_version: asString(raw.config_version),
280
- page_url: asString(raw.page_url) ?? '',
281
- page_title: asString(raw.page_title),
282
- referrer: asString(raw.referrer),
283
- utm_source: asString(raw.utm_source),
284
- utm_medium: asString(raw.utm_medium),
285
- utm_campaign: asString(raw.utm_campaign),
286
- utm_term: asString(raw.utm_term),
287
- utm_content: asString(raw.utm_content),
288
- device_type: asString(raw.device_type),
289
- browser: asString(raw.browser),
290
- os: asString(raw.os),
291
- screen_width: asNumber(raw.screen_width),
292
- screen_height: asNumber(raw.screen_height),
293
- device_id: asString(raw.device_id),
294
- phone: asString(raw.phone),
295
- event_source_platform: asString(raw.event_source_platform),
296
- properties: asProperties(raw.properties),
297
- };
298
- return { ok: true, value };
299
- }
300
- /**
301
- * Server-side helper for constructing envelopes from partial input. Fills
302
- * event_id, timestamp, and all enum defaults. Not used on the client SDK hot
303
- * path.
304
- */
305
- function createEnvelope(partial) {
306
- return {
307
- event_id: partial.event_id ?? generateUuid(),
308
- timestamp: partial.timestamp ?? new Date().toISOString(),
309
- site_id: partial.site_id,
310
- anonymous_id: partial.anonymous_id,
311
- canonical_id: partial.canonical_id,
312
- session_id: partial.session_id,
313
- event_type: partial.event_type ?? 'custom',
314
- event_name: partial.event_name,
315
- event_tier: partial.event_tier ?? 'raw',
316
- event_source: partial.event_source ?? 'client_sdk',
317
- correlation_id: partial.correlation_id,
318
- parent_event_id: partial.parent_event_id,
319
- consent_level: partial.consent_level ?? 'pending',
320
- granular_consent_level: partial.granular_consent_level ?? 'none',
321
- sdk_version: partial.sdk_version ?? 'server@unknown',
322
- config_version: partial.config_version,
323
- page_url: partial.page_url ?? '',
324
- page_title: partial.page_title,
325
- referrer: partial.referrer,
326
- utm_source: partial.utm_source,
327
- utm_medium: partial.utm_medium,
328
- utm_campaign: partial.utm_campaign,
329
- utm_term: partial.utm_term,
330
- utm_content: partial.utm_content,
331
- device_type: partial.device_type,
332
- browser: partial.browser,
333
- os: partial.os,
334
- screen_width: partial.screen_width,
335
- screen_height: partial.screen_height,
336
- device_id: partial.device_id,
337
- phone: partial.phone,
338
- event_source_platform: partial.event_source_platform,
339
- properties: partial.properties ?? {},
340
- };
341
- }
@@ -1,6 +0,0 @@
1
- /**
2
- * @gurulu/shared-core — canonical contracts shared by every Gurulu SDK.
3
- */
4
- export * from './envelope';
5
- export * from './server-event';
6
- export * from './canonical-events';
@@ -1,22 +0,0 @@
1
- "use strict";
2
- /**
3
- * @gurulu/shared-core — canonical contracts shared by every Gurulu SDK.
4
- */
5
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
6
- if (k2 === undefined) k2 = k;
7
- var desc = Object.getOwnPropertyDescriptor(m, k);
8
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
9
- desc = { enumerable: true, get: function() { return m[k]; } };
10
- }
11
- Object.defineProperty(o, k2, desc);
12
- }) : (function(o, m, k, k2) {
13
- if (k2 === undefined) k2 = k;
14
- o[k2] = m[k];
15
- }));
16
- var __exportStar = (this && this.__exportStar) || function(m, exports) {
17
- for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
18
- };
19
- Object.defineProperty(exports, "__esModule", { value: true });
20
- __exportStar(require("./envelope"), exports);
21
- __exportStar(require("./server-event"), exports);
22
- __exportStar(require("./canonical-events"), exports);
@@ -1,37 +0,0 @@
1
- /**
2
- * Server SDK public types — lifted from packages/node-sdk so every lane
3
- * imports them from a single source of truth.
4
- */
5
- export interface GuruluConfig {
6
- siteId: string;
7
- apiKey: string;
8
- endpoint?: string;
9
- flushInterval?: number;
10
- maxBatchSize?: number;
11
- maxRetries?: number;
12
- debug?: boolean;
13
- }
14
- export interface ServerEvent {
15
- event_name: string;
16
- user_id: string;
17
- properties?: Record<string, unknown>;
18
- timestamp?: string;
19
- idempotency_key?: string;
20
- correlation_id?: string;
21
- }
22
- export interface TrackOptions {
23
- userId: string;
24
- timestamp?: Date;
25
- idempotencyKey?: string;
26
- correlationId?: string;
27
- }
28
- export interface IngestResponse {
29
- accepted: number;
30
- rejected: number;
31
- correlations: Array<{
32
- server_event: string;
33
- matched_client_event: string;
34
- inferred_intent: string;
35
- confidence: number;
36
- }>;
37
- }
@@ -1,6 +0,0 @@
1
- "use strict";
2
- /**
3
- * Server SDK public types — lifted from packages/node-sdk so every lane
4
- * imports them from a single source of truth.
5
- */
6
- Object.defineProperty(exports, "__esModule", { value: true });
File without changes