@xray-analytics/analytics-server 0.0.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.
@@ -0,0 +1,177 @@
1
+ import { ZodType } from 'zod';
2
+
3
+ type AnalyticsEventProps = Record<string, unknown>;
4
+ type AnalyticsEventInput = {
5
+ name: string;
6
+ ts?: number;
7
+ appId: string;
8
+ sessionId?: string;
9
+ url?: string;
10
+ path?: string;
11
+ ref?: string;
12
+ environment?: string;
13
+ props?: AnalyticsEventProps;
14
+ tags?: string[];
15
+ clientMeta?: {
16
+ ip?: string;
17
+ userAgent?: string;
18
+ isMobile?: boolean;
19
+ os?: string;
20
+ platform?: string;
21
+ language?: string;
22
+ screen?: {
23
+ width: number;
24
+ height: number;
25
+ };
26
+ };
27
+ writeKey?: string;
28
+ };
29
+ type IngestContext = {
30
+ ip?: string;
31
+ userAgent?: string;
32
+ headers?: Record<string, string | undefined>;
33
+ requestId?: string;
34
+ };
35
+ type StoredAnalyticsEvent = AnalyticsEventInput & {
36
+ ts: number;
37
+ receivedAt: number;
38
+ props: AnalyticsEventProps;
39
+ meta?: {
40
+ ip?: string;
41
+ userAgent?: string;
42
+ requestId?: string;
43
+ };
44
+ };
45
+ interface AnalyticsStorageAdapter {
46
+ save: (event: StoredAnalyticsEvent) => Promise<void>;
47
+ getAll: (dateInit?: number, dateEnd?: number) => Promise<StoredAnalyticsEvent[]>;
48
+ clear: (dateInit?: number, dateEnd?: number) => Promise<number>;
49
+ }
50
+ type MaskFieldArgs = {
51
+ path: string;
52
+ key: string;
53
+ value: unknown;
54
+ event: StoredAnalyticsEvent;
55
+ };
56
+ type MaskConfig = {
57
+ paths?: string[];
58
+ keyPatterns?: RegExp[];
59
+ maskValue?: string;
60
+ maskField?: (args: MaskFieldArgs) => unknown;
61
+ };
62
+ type AcceptedTrack = {
63
+ trackName: string;
64
+ schema: ZodType;
65
+ validateOn?: 'props' | 'event';
66
+ version?: number;
67
+ description?: string;
68
+ tags?: string[];
69
+ deprecated?: boolean;
70
+ catalogSchema?: Record<string, unknown>;
71
+ };
72
+ type AnalyticsTrackCatalogItem = {
73
+ trackName: string;
74
+ validateOn: 'props' | 'event';
75
+ version: number;
76
+ description?: string;
77
+ tags?: string[];
78
+ deprecated?: boolean;
79
+ schema?: Record<string, unknown>;
80
+ };
81
+ type AnalyticsTrackCatalog = {
82
+ generatedAt: number;
83
+ tracks: AnalyticsTrackCatalogItem[];
84
+ };
85
+ type AnalyticsServerConfig = {
86
+ storage: AnalyticsStorageAdapter;
87
+ acceptedTracks?: AcceptedTrack[];
88
+ rejectUnknownTracks?: boolean;
89
+ masking?: MaskConfig;
90
+ };
91
+ type IngestErrorCode = 'invalid_payload' | 'track_not_allowed' | 'schema_mismatch' | 'storage_error';
92
+ type IngestResult = {
93
+ ok: true;
94
+ event: StoredAnalyticsEvent;
95
+ } | {
96
+ ok: false;
97
+ error: {
98
+ code: IngestErrorCode;
99
+ message: string;
100
+ details?: unknown;
101
+ };
102
+ };
103
+
104
+ declare function createAnalyticsServer(config: AnalyticsServerConfig): {
105
+ ingest: (input: unknown, context?: IngestContext) => Promise<IngestResult>;
106
+ getCatalog: () => AnalyticsTrackCatalog;
107
+ };
108
+
109
+ declare function createMemoryAdapter(initialEvents?: StoredAnalyticsEvent[]): AnalyticsStorageAdapter;
110
+
111
+ type PostgresQueryable = {
112
+ query: (text: string, values?: unknown[]) => Promise<unknown>;
113
+ };
114
+ type PostgresAdapterOptions = {
115
+ db: PostgresQueryable;
116
+ tableName?: string;
117
+ schemaName?: string;
118
+ };
119
+ declare function createPostgresAdapter({ db, tableName, schemaName, }: PostgresAdapterOptions): AnalyticsStorageAdapter;
120
+
121
+ type Server$5 = ReturnType<typeof createAnalyticsServer>;
122
+ type HeaderValue = string | string[] | undefined;
123
+ type ExpressLikeRequest = {
124
+ method?: string;
125
+ body?: unknown;
126
+ headers?: Record<string, HeaderValue>;
127
+ ip?: string;
128
+ get?: (name: string) => string | undefined;
129
+ header?: (name: string) => string | undefined;
130
+ };
131
+ type ExpressLikeResponse = {
132
+ status: (code: number) => ExpressLikeResponse;
133
+ json: (body: unknown) => unknown;
134
+ };
135
+ type ExpressIngestHandlerOptions = {
136
+ getContext?: (req: ExpressLikeRequest) => IngestContext;
137
+ };
138
+ declare function createExpressIngestHandler(server: Server$5, options?: ExpressIngestHandlerOptions): (req: ExpressLikeRequest, res: ExpressLikeResponse) => Promise<unknown>;
139
+
140
+ type Server$4 = ReturnType<typeof createAnalyticsServer>;
141
+ declare function createExpressCatalogHandler(server: Server$4): (req: ExpressLikeRequest, res: ExpressLikeResponse) => Promise<unknown>;
142
+
143
+ type Server$3 = ReturnType<typeof createAnalyticsServer>;
144
+ declare function createFetchCatalogHandler(server: Server$3): (request: Request) => Promise<Response>;
145
+
146
+ type Server$2 = ReturnType<typeof createAnalyticsServer>;
147
+ declare function createFetchIngestHandler(server: Server$2): (request: Request) => Promise<Response>;
148
+
149
+ type Server$1 = ReturnType<typeof createAnalyticsServer>;
150
+ type CreateCatalogHandlerConfig = {
151
+ adapter?: 'fetch';
152
+ } | {
153
+ adapter: 'express';
154
+ };
155
+ declare function createCatalogHandler(server: Server$1, config?: {
156
+ adapter?: 'fetch';
157
+ }): (request: Request) => Promise<Response>;
158
+ declare function createCatalogHandler(server: Server$1, config: {
159
+ adapter: 'express';
160
+ }): (req: ExpressLikeRequest, res: ExpressLikeResponse) => Promise<unknown>;
161
+
162
+ type Server = ReturnType<typeof createAnalyticsServer>;
163
+ type CreateIngestHandlerConfig = {
164
+ adapter?: 'fetch';
165
+ } | {
166
+ adapter: 'express';
167
+ express?: ExpressIngestHandlerOptions;
168
+ };
169
+ declare function createIngestHandler(server: Server, config?: {
170
+ adapter?: 'fetch';
171
+ }): (request: Request) => Promise<Response>;
172
+ declare function createIngestHandler(server: Server, config: {
173
+ adapter: 'express';
174
+ express?: ExpressIngestHandlerOptions;
175
+ }): (req: ExpressLikeRequest, res: ExpressLikeResponse) => Promise<unknown>;
176
+
177
+ export { type AcceptedTrack, type AnalyticsEventInput, type AnalyticsServerConfig, type AnalyticsStorageAdapter, type AnalyticsTrackCatalog, type AnalyticsTrackCatalogItem, type CreateCatalogHandlerConfig, type CreateIngestHandlerConfig, type ExpressIngestHandlerOptions, type ExpressLikeRequest, type ExpressLikeResponse, type IngestContext, type IngestResult, type MaskConfig, type PostgresAdapterOptions, type PostgresQueryable, type StoredAnalyticsEvent, createAnalyticsServer, createCatalogHandler, createExpressCatalogHandler, createExpressIngestHandler, createFetchCatalogHandler, createFetchIngestHandler, createIngestHandler, createMemoryAdapter, createPostgresAdapter };
@@ -0,0 +1,177 @@
1
+ import { ZodType } from 'zod';
2
+
3
+ type AnalyticsEventProps = Record<string, unknown>;
4
+ type AnalyticsEventInput = {
5
+ name: string;
6
+ ts?: number;
7
+ appId: string;
8
+ sessionId?: string;
9
+ url?: string;
10
+ path?: string;
11
+ ref?: string;
12
+ environment?: string;
13
+ props?: AnalyticsEventProps;
14
+ tags?: string[];
15
+ clientMeta?: {
16
+ ip?: string;
17
+ userAgent?: string;
18
+ isMobile?: boolean;
19
+ os?: string;
20
+ platform?: string;
21
+ language?: string;
22
+ screen?: {
23
+ width: number;
24
+ height: number;
25
+ };
26
+ };
27
+ writeKey?: string;
28
+ };
29
+ type IngestContext = {
30
+ ip?: string;
31
+ userAgent?: string;
32
+ headers?: Record<string, string | undefined>;
33
+ requestId?: string;
34
+ };
35
+ type StoredAnalyticsEvent = AnalyticsEventInput & {
36
+ ts: number;
37
+ receivedAt: number;
38
+ props: AnalyticsEventProps;
39
+ meta?: {
40
+ ip?: string;
41
+ userAgent?: string;
42
+ requestId?: string;
43
+ };
44
+ };
45
+ interface AnalyticsStorageAdapter {
46
+ save: (event: StoredAnalyticsEvent) => Promise<void>;
47
+ getAll: (dateInit?: number, dateEnd?: number) => Promise<StoredAnalyticsEvent[]>;
48
+ clear: (dateInit?: number, dateEnd?: number) => Promise<number>;
49
+ }
50
+ type MaskFieldArgs = {
51
+ path: string;
52
+ key: string;
53
+ value: unknown;
54
+ event: StoredAnalyticsEvent;
55
+ };
56
+ type MaskConfig = {
57
+ paths?: string[];
58
+ keyPatterns?: RegExp[];
59
+ maskValue?: string;
60
+ maskField?: (args: MaskFieldArgs) => unknown;
61
+ };
62
+ type AcceptedTrack = {
63
+ trackName: string;
64
+ schema: ZodType;
65
+ validateOn?: 'props' | 'event';
66
+ version?: number;
67
+ description?: string;
68
+ tags?: string[];
69
+ deprecated?: boolean;
70
+ catalogSchema?: Record<string, unknown>;
71
+ };
72
+ type AnalyticsTrackCatalogItem = {
73
+ trackName: string;
74
+ validateOn: 'props' | 'event';
75
+ version: number;
76
+ description?: string;
77
+ tags?: string[];
78
+ deprecated?: boolean;
79
+ schema?: Record<string, unknown>;
80
+ };
81
+ type AnalyticsTrackCatalog = {
82
+ generatedAt: number;
83
+ tracks: AnalyticsTrackCatalogItem[];
84
+ };
85
+ type AnalyticsServerConfig = {
86
+ storage: AnalyticsStorageAdapter;
87
+ acceptedTracks?: AcceptedTrack[];
88
+ rejectUnknownTracks?: boolean;
89
+ masking?: MaskConfig;
90
+ };
91
+ type IngestErrorCode = 'invalid_payload' | 'track_not_allowed' | 'schema_mismatch' | 'storage_error';
92
+ type IngestResult = {
93
+ ok: true;
94
+ event: StoredAnalyticsEvent;
95
+ } | {
96
+ ok: false;
97
+ error: {
98
+ code: IngestErrorCode;
99
+ message: string;
100
+ details?: unknown;
101
+ };
102
+ };
103
+
104
+ declare function createAnalyticsServer(config: AnalyticsServerConfig): {
105
+ ingest: (input: unknown, context?: IngestContext) => Promise<IngestResult>;
106
+ getCatalog: () => AnalyticsTrackCatalog;
107
+ };
108
+
109
+ declare function createMemoryAdapter(initialEvents?: StoredAnalyticsEvent[]): AnalyticsStorageAdapter;
110
+
111
+ type PostgresQueryable = {
112
+ query: (text: string, values?: unknown[]) => Promise<unknown>;
113
+ };
114
+ type PostgresAdapterOptions = {
115
+ db: PostgresQueryable;
116
+ tableName?: string;
117
+ schemaName?: string;
118
+ };
119
+ declare function createPostgresAdapter({ db, tableName, schemaName, }: PostgresAdapterOptions): AnalyticsStorageAdapter;
120
+
121
+ type Server$5 = ReturnType<typeof createAnalyticsServer>;
122
+ type HeaderValue = string | string[] | undefined;
123
+ type ExpressLikeRequest = {
124
+ method?: string;
125
+ body?: unknown;
126
+ headers?: Record<string, HeaderValue>;
127
+ ip?: string;
128
+ get?: (name: string) => string | undefined;
129
+ header?: (name: string) => string | undefined;
130
+ };
131
+ type ExpressLikeResponse = {
132
+ status: (code: number) => ExpressLikeResponse;
133
+ json: (body: unknown) => unknown;
134
+ };
135
+ type ExpressIngestHandlerOptions = {
136
+ getContext?: (req: ExpressLikeRequest) => IngestContext;
137
+ };
138
+ declare function createExpressIngestHandler(server: Server$5, options?: ExpressIngestHandlerOptions): (req: ExpressLikeRequest, res: ExpressLikeResponse) => Promise<unknown>;
139
+
140
+ type Server$4 = ReturnType<typeof createAnalyticsServer>;
141
+ declare function createExpressCatalogHandler(server: Server$4): (req: ExpressLikeRequest, res: ExpressLikeResponse) => Promise<unknown>;
142
+
143
+ type Server$3 = ReturnType<typeof createAnalyticsServer>;
144
+ declare function createFetchCatalogHandler(server: Server$3): (request: Request) => Promise<Response>;
145
+
146
+ type Server$2 = ReturnType<typeof createAnalyticsServer>;
147
+ declare function createFetchIngestHandler(server: Server$2): (request: Request) => Promise<Response>;
148
+
149
+ type Server$1 = ReturnType<typeof createAnalyticsServer>;
150
+ type CreateCatalogHandlerConfig = {
151
+ adapter?: 'fetch';
152
+ } | {
153
+ adapter: 'express';
154
+ };
155
+ declare function createCatalogHandler(server: Server$1, config?: {
156
+ adapter?: 'fetch';
157
+ }): (request: Request) => Promise<Response>;
158
+ declare function createCatalogHandler(server: Server$1, config: {
159
+ adapter: 'express';
160
+ }): (req: ExpressLikeRequest, res: ExpressLikeResponse) => Promise<unknown>;
161
+
162
+ type Server = ReturnType<typeof createAnalyticsServer>;
163
+ type CreateIngestHandlerConfig = {
164
+ adapter?: 'fetch';
165
+ } | {
166
+ adapter: 'express';
167
+ express?: ExpressIngestHandlerOptions;
168
+ };
169
+ declare function createIngestHandler(server: Server, config?: {
170
+ adapter?: 'fetch';
171
+ }): (request: Request) => Promise<Response>;
172
+ declare function createIngestHandler(server: Server, config: {
173
+ adapter: 'express';
174
+ express?: ExpressIngestHandlerOptions;
175
+ }): (req: ExpressLikeRequest, res: ExpressLikeResponse) => Promise<unknown>;
176
+
177
+ export { type AcceptedTrack, type AnalyticsEventInput, type AnalyticsServerConfig, type AnalyticsStorageAdapter, type AnalyticsTrackCatalog, type AnalyticsTrackCatalogItem, type CreateCatalogHandlerConfig, type CreateIngestHandlerConfig, type ExpressIngestHandlerOptions, type ExpressLikeRequest, type ExpressLikeResponse, type IngestContext, type IngestResult, type MaskConfig, type PostgresAdapterOptions, type PostgresQueryable, type StoredAnalyticsEvent, createAnalyticsServer, createCatalogHandler, createExpressCatalogHandler, createExpressIngestHandler, createFetchCatalogHandler, createFetchIngestHandler, createIngestHandler, createMemoryAdapter, createPostgresAdapter };
package/dist/index.js ADDED
@@ -0,0 +1,475 @@
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/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ createAnalyticsServer: () => createAnalyticsServer,
24
+ createCatalogHandler: () => createCatalogHandler,
25
+ createExpressCatalogHandler: () => createExpressCatalogHandler,
26
+ createExpressIngestHandler: () => createExpressIngestHandler,
27
+ createFetchCatalogHandler: () => createFetchCatalogHandler,
28
+ createFetchIngestHandler: () => createFetchIngestHandler,
29
+ createIngestHandler: () => createIngestHandler,
30
+ createMemoryAdapter: () => createMemoryAdapter,
31
+ createPostgresAdapter: () => createPostgresAdapter
32
+ });
33
+ module.exports = __toCommonJS(index_exports);
34
+
35
+ // src/masking.ts
36
+ function isPlainObject(value) {
37
+ return Object.prototype.toString.call(value) === "[object Object]";
38
+ }
39
+ function shouldMask(path, key, config) {
40
+ if (config.paths?.includes(path)) return true;
41
+ if (config.keyPatterns?.some((pattern) => pattern.test(key))) return true;
42
+ return false;
43
+ }
44
+ function maskRecursive(input, config, event, parentPath = "") {
45
+ if (Array.isArray(input)) {
46
+ return input.map(
47
+ (item, index) => maskRecursive(item, config, event, parentPath ? `${parentPath}.${index}` : String(index))
48
+ );
49
+ }
50
+ if (!isPlainObject(input)) return input;
51
+ const out = {};
52
+ for (const [key, value] of Object.entries(input)) {
53
+ const path = parentPath ? `${parentPath}.${key}` : key;
54
+ if (shouldMask(path, key, config)) {
55
+ out[key] = config.maskField ? config.maskField({ path, key, value, event }) : config.maskValue ?? "***";
56
+ continue;
57
+ }
58
+ out[key] = maskRecursive(value, config, event, path);
59
+ }
60
+ return out;
61
+ }
62
+ function applyMasking(event, config) {
63
+ if (!config) return event;
64
+ return {
65
+ ...event,
66
+ props: maskRecursive(event.props, config, event) ?? {}
67
+ };
68
+ }
69
+
70
+ // src/validation.ts
71
+ var import_zod = require("zod");
72
+ var baseEventSchema = import_zod.z.object({
73
+ name: import_zod.z.string().min(1),
74
+ ts: import_zod.z.number().optional(),
75
+ appId: import_zod.z.string().min(1),
76
+ sessionId: import_zod.z.string().optional(),
77
+ url: import_zod.z.string().optional(),
78
+ path: import_zod.z.string().optional(),
79
+ ref: import_zod.z.string().optional(),
80
+ environment: import_zod.z.string().optional(),
81
+ props: import_zod.z.record(import_zod.z.string(), import_zod.z.unknown()).optional(),
82
+ tags: import_zod.z.array(import_zod.z.string()).optional(),
83
+ clientMeta: import_zod.z.object({
84
+ ip: import_zod.z.string().optional(),
85
+ userAgent: import_zod.z.string().optional(),
86
+ isMobile: import_zod.z.boolean().optional(),
87
+ os: import_zod.z.string().optional(),
88
+ platform: import_zod.z.string().optional(),
89
+ language: import_zod.z.string().optional(),
90
+ screen: import_zod.z.object({
91
+ width: import_zod.z.number(),
92
+ height: import_zod.z.number()
93
+ }).optional()
94
+ }).optional(),
95
+ writeKey: import_zod.z.string().optional()
96
+ });
97
+ function validateBasePayload(input) {
98
+ const parsed = baseEventSchema.safeParse(input);
99
+ if (!parsed.success) {
100
+ return {
101
+ ok: false,
102
+ error: {
103
+ code: "invalid_payload",
104
+ message: "Invalid track payload",
105
+ details: parsed.error.issues
106
+ }
107
+ };
108
+ }
109
+ return { ok: true, data: parsed.data };
110
+ }
111
+ function validateAgainstAcceptedTracks(event, acceptedTracks, rejectUnknownTracks) {
112
+ if (!acceptedTracks || acceptedTracks.length === 0) return null;
113
+ const trackConfig = acceptedTracks.find((item) => item.trackName === event.name);
114
+ if (!trackConfig) {
115
+ if (!rejectUnknownTracks) return null;
116
+ return {
117
+ ok: false,
118
+ error: {
119
+ code: "track_not_allowed",
120
+ message: `Track '${event.name}' is not in the allowed track list`
121
+ }
122
+ };
123
+ }
124
+ const target = trackConfig.validateOn === "event" ? event : event.props;
125
+ const parsed = trackConfig.schema.safeParse(target);
126
+ if (!parsed.success) {
127
+ return {
128
+ ok: false,
129
+ error: {
130
+ code: "schema_mismatch",
131
+ message: `Track '${event.name}' does not match the configured schema`,
132
+ details: parsed.error.issues
133
+ }
134
+ };
135
+ }
136
+ return null;
137
+ }
138
+
139
+ // src/create-analytics-server.ts
140
+ function buildStoredEvent(input, context) {
141
+ return {
142
+ ...input,
143
+ ts: input.ts ?? Date.now(),
144
+ receivedAt: Date.now(),
145
+ props: input.props ?? {},
146
+ meta: {
147
+ ip: context?.ip,
148
+ userAgent: context?.userAgent,
149
+ requestId: context?.requestId
150
+ }
151
+ };
152
+ }
153
+ function createAnalyticsServer(config) {
154
+ const rejectUnknownTracks = config.rejectUnknownTracks ?? true;
155
+ const generatedAt = Date.now();
156
+ const trackCatalog = (config.acceptedTracks ?? []).map((track) => ({
157
+ trackName: track.trackName,
158
+ validateOn: track.validateOn ?? "props",
159
+ version: track.version ?? 1,
160
+ description: track.description,
161
+ tags: track.tags,
162
+ deprecated: track.deprecated,
163
+ schema: track.catalogSchema
164
+ }));
165
+ async function ingest(input, context) {
166
+ const payloadValidation = validateBasePayload(input);
167
+ if (!payloadValidation.ok) return payloadValidation;
168
+ const event = buildStoredEvent(payloadValidation.data, context);
169
+ const trackValidation = validateAgainstAcceptedTracks(
170
+ event,
171
+ config.acceptedTracks,
172
+ rejectUnknownTracks
173
+ );
174
+ if (trackValidation) return trackValidation;
175
+ const maskedEvent = applyMasking(event, config.masking);
176
+ try {
177
+ await config.storage.save(maskedEvent);
178
+ return {
179
+ ok: true,
180
+ event: maskedEvent
181
+ };
182
+ } catch (error) {
183
+ return {
184
+ ok: false,
185
+ error: {
186
+ code: "storage_error",
187
+ message: "Failed to persist track",
188
+ details: error instanceof Error ? error.message : error
189
+ }
190
+ };
191
+ }
192
+ }
193
+ function getCatalog() {
194
+ return {
195
+ generatedAt,
196
+ tracks: [...trackCatalog]
197
+ };
198
+ }
199
+ return {
200
+ ingest,
201
+ getCatalog
202
+ };
203
+ }
204
+
205
+ // src/adapters/memory-adapter.ts
206
+ function createMemoryAdapter(initialEvents = []) {
207
+ const events = [...initialEvents];
208
+ const adapter = {
209
+ async save(event) {
210
+ events.push(event);
211
+ },
212
+ async getAll(dateInit, dateEnd) {
213
+ if (dateInit === void 0 && dateEnd === void 0) return [...events];
214
+ return events.filter((event) => {
215
+ if (dateInit !== void 0 && event.ts < dateInit) return false;
216
+ if (dateEnd !== void 0 && event.ts > dateEnd) return false;
217
+ return true;
218
+ });
219
+ },
220
+ async clear(dateInit, dateEnd) {
221
+ const before = events.length;
222
+ if (dateInit === void 0 && dateEnd === void 0) {
223
+ events.length = 0;
224
+ return before;
225
+ }
226
+ const kept = events.filter((event) => {
227
+ if (dateInit !== void 0 && event.ts < dateInit) return true;
228
+ if (dateEnd !== void 0 && event.ts > dateEnd) return true;
229
+ if (dateInit === void 0 && dateEnd !== void 0) return event.ts > dateEnd;
230
+ return false;
231
+ });
232
+ events.length = 0;
233
+ events.push(...kept);
234
+ return before - events.length;
235
+ }
236
+ };
237
+ return adapter;
238
+ }
239
+
240
+ // src/adapters/postgres-adapter.ts
241
+ function quoteIdentifier(identifier) {
242
+ if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(identifier)) {
243
+ throw new Error(`Invalid SQL identifier: '${identifier}'`);
244
+ }
245
+ return `"${identifier}"`;
246
+ }
247
+ function buildDateRangeWhereClause(dateInit, dateEnd) {
248
+ const conditions = [];
249
+ const values = [];
250
+ if (dateInit !== void 0) {
251
+ conditions.push(`ts >= $${values.length + 1}`);
252
+ values.push(dateInit);
253
+ }
254
+ if (dateEnd !== void 0) {
255
+ conditions.push(`ts <= $${values.length + 1}`);
256
+ values.push(dateEnd);
257
+ }
258
+ if (conditions.length === 0) {
259
+ return { whereClause: "", values };
260
+ }
261
+ return {
262
+ whereClause: ` where ${conditions.join(" and ")}`,
263
+ values
264
+ };
265
+ }
266
+ function mapRowToStoredEvent(row) {
267
+ return {
268
+ name: row.name,
269
+ ts: row.ts,
270
+ appId: row.app_id,
271
+ sessionId: row.session_id ?? void 0,
272
+ url: row.url ?? void 0,
273
+ path: row.path ?? void 0,
274
+ ref: row.ref ?? void 0,
275
+ environment: row.environment ?? void 0,
276
+ props: row.props ?? {},
277
+ tags: row.tags ?? void 0,
278
+ clientMeta: row.client_meta ?? void 0,
279
+ writeKey: row.write_key ?? void 0,
280
+ receivedAt: row.received_at,
281
+ meta: row.meta ?? void 0
282
+ };
283
+ }
284
+ function extractQueryResultData(result) {
285
+ if (typeof result === "object" && result !== null) {
286
+ return result;
287
+ }
288
+ return {};
289
+ }
290
+ function createPostgresAdapter({
291
+ db,
292
+ tableName = "analytics_events",
293
+ schemaName
294
+ }) {
295
+ const tableRef = schemaName ? `${quoteIdentifier(schemaName)}.${quoteIdentifier(tableName)}` : quoteIdentifier(tableName);
296
+ return {
297
+ async save(event) {
298
+ await db.query(
299
+ `
300
+ insert into ${tableRef}
301
+ (name, ts, app_id, session_id, url, path, ref, environment, props, tags, client_meta, write_key, received_at, meta)
302
+ values
303
+ ($1, $2, $3, $4, $5, $6, $7, $8, $9::jsonb, $10::text[], $11::jsonb, $12, $13, $14::jsonb)
304
+ `,
305
+ [
306
+ event.name,
307
+ event.ts,
308
+ event.appId,
309
+ event.sessionId ?? null,
310
+ event.url ?? null,
311
+ event.path ?? null,
312
+ event.ref ?? null,
313
+ event.environment ?? null,
314
+ event.props,
315
+ event.tags ?? null,
316
+ event.clientMeta ?? null,
317
+ event.writeKey ?? null,
318
+ event.receivedAt,
319
+ event.meta ?? null
320
+ ]
321
+ );
322
+ },
323
+ async getAll(dateInit, dateEnd) {
324
+ const { whereClause, values } = buildDateRangeWhereClause(dateInit, dateEnd);
325
+ const result = await db.query(
326
+ `
327
+ select
328
+ name,
329
+ ts,
330
+ app_id,
331
+ session_id,
332
+ url,
333
+ path,
334
+ ref,
335
+ environment,
336
+ props,
337
+ tags,
338
+ client_meta,
339
+ write_key,
340
+ received_at,
341
+ meta
342
+ from ${tableRef}${whereClause}
343
+ order by ts asc
344
+ `,
345
+ values
346
+ );
347
+ const rows = extractQueryResultData(result).rows ?? [];
348
+ return rows.map((row) => mapRowToStoredEvent(row));
349
+ },
350
+ async clear(dateInit, dateEnd) {
351
+ const { whereClause, values } = buildDateRangeWhereClause(dateInit, dateEnd);
352
+ const result = await db.query(`delete from ${tableRef}${whereClause}`, values);
353
+ return extractQueryResultData(result).rowCount ?? 0;
354
+ }
355
+ };
356
+ }
357
+
358
+ // src/http/create-express-catalog-handler.ts
359
+ function createExpressCatalogHandler(server) {
360
+ return async function handle(req, res) {
361
+ if ((req.method ?? "GET").toUpperCase() !== "GET") {
362
+ return res.status(405).json({ ok: false, error: "method_not_allowed" });
363
+ }
364
+ return res.status(200).json({ ok: true, data: server.getCatalog() });
365
+ };
366
+ }
367
+
368
+ // src/http/create-express-handler.ts
369
+ function getHeader(req, name) {
370
+ const lower = name.toLowerCase();
371
+ if (typeof req.get === "function") return req.get(lower) ?? req.get(name) ?? void 0;
372
+ if (typeof req.header === "function") return req.header(lower) ?? req.header(name) ?? void 0;
373
+ const raw = req.headers?.[lower] ?? req.headers?.[name];
374
+ if (Array.isArray(raw)) return raw[0];
375
+ return raw;
376
+ }
377
+ function extractContextFromExpress(req) {
378
+ return {
379
+ ip: req.ip ?? getHeader(req, "x-forwarded-for"),
380
+ userAgent: getHeader(req, "user-agent"),
381
+ requestId: getHeader(req, "x-request-id")
382
+ };
383
+ }
384
+ function resolveBody(body) {
385
+ if (typeof body === "string") {
386
+ return JSON.parse(body);
387
+ }
388
+ return body;
389
+ }
390
+ function createExpressIngestHandler(server, options = {}) {
391
+ return async function handle(req, res) {
392
+ if ((req.method ?? "GET").toUpperCase() !== "POST") {
393
+ return res.status(405).json({ ok: false, error: "method_not_allowed" });
394
+ }
395
+ let body;
396
+ try {
397
+ body = resolveBody(req.body);
398
+ } catch {
399
+ return res.status(400).json({ ok: false, error: "invalid_json" });
400
+ }
401
+ const context = options.getContext?.(req) ?? extractContextFromExpress(req);
402
+ const result = await server.ingest(body, context);
403
+ if (!result.ok) {
404
+ const status = result.error.code === "storage_error" ? 500 : result.error.code === "invalid_payload" || result.error.code === "schema_mismatch" ? 422 : 403;
405
+ return res.status(status).json({ ok: false, error: result.error });
406
+ }
407
+ return res.status(202).json({ ok: true });
408
+ };
409
+ }
410
+
411
+ // src/http/create-fetch-catalog-handler.ts
412
+ function createFetchCatalogHandler(server) {
413
+ return async function handle(request) {
414
+ if (request.method !== "GET") {
415
+ return Response.json({ ok: false, error: "method_not_allowed" }, { status: 405 });
416
+ }
417
+ return Response.json({ ok: true, data: server.getCatalog() }, { status: 200 });
418
+ };
419
+ }
420
+
421
+ // src/http/create-fetch-handler.ts
422
+ function extractContext(request) {
423
+ return {
424
+ ip: request.headers.get("x-forwarded-for") ?? void 0,
425
+ userAgent: request.headers.get("user-agent") ?? void 0,
426
+ requestId: request.headers.get("x-request-id") ?? void 0
427
+ };
428
+ }
429
+ function createFetchIngestHandler(server) {
430
+ return async function handle(request) {
431
+ if (request.method !== "POST") {
432
+ return Response.json({ ok: false, error: "method_not_allowed" }, { status: 405 });
433
+ }
434
+ let body;
435
+ try {
436
+ body = await request.json();
437
+ } catch {
438
+ return Response.json({ ok: false, error: "invalid_json" }, { status: 400 });
439
+ }
440
+ const result = await server.ingest(body, extractContext(request));
441
+ if (!result.ok) {
442
+ const status = result.error.code === "storage_error" ? 500 : result.error.code === "invalid_payload" || result.error.code === "schema_mismatch" ? 422 : 403;
443
+ return Response.json({ ok: false, error: result.error }, { status });
444
+ }
445
+ return Response.json({ ok: true }, { status: 202 });
446
+ };
447
+ }
448
+
449
+ // src/http/create-catalog-handler.ts
450
+ function createCatalogHandler(server, config = {}) {
451
+ if (config.adapter === "express") {
452
+ return createExpressCatalogHandler(server);
453
+ }
454
+ return createFetchCatalogHandler(server);
455
+ }
456
+
457
+ // src/http/create-ingest-handler.ts
458
+ function createIngestHandler(server, config = {}) {
459
+ if (config.adapter === "express") {
460
+ return createExpressIngestHandler(server, config.express);
461
+ }
462
+ return createFetchIngestHandler(server);
463
+ }
464
+ // Annotate the CommonJS export names for ESM import in node:
465
+ 0 && (module.exports = {
466
+ createAnalyticsServer,
467
+ createCatalogHandler,
468
+ createExpressCatalogHandler,
469
+ createExpressIngestHandler,
470
+ createFetchCatalogHandler,
471
+ createFetchIngestHandler,
472
+ createIngestHandler,
473
+ createMemoryAdapter,
474
+ createPostgresAdapter
475
+ });
package/dist/index.mjs ADDED
@@ -0,0 +1,440 @@
1
+ // src/masking.ts
2
+ function isPlainObject(value) {
3
+ return Object.prototype.toString.call(value) === "[object Object]";
4
+ }
5
+ function shouldMask(path, key, config) {
6
+ if (config.paths?.includes(path)) return true;
7
+ if (config.keyPatterns?.some((pattern) => pattern.test(key))) return true;
8
+ return false;
9
+ }
10
+ function maskRecursive(input, config, event, parentPath = "") {
11
+ if (Array.isArray(input)) {
12
+ return input.map(
13
+ (item, index) => maskRecursive(item, config, event, parentPath ? `${parentPath}.${index}` : String(index))
14
+ );
15
+ }
16
+ if (!isPlainObject(input)) return input;
17
+ const out = {};
18
+ for (const [key, value] of Object.entries(input)) {
19
+ const path = parentPath ? `${parentPath}.${key}` : key;
20
+ if (shouldMask(path, key, config)) {
21
+ out[key] = config.maskField ? config.maskField({ path, key, value, event }) : config.maskValue ?? "***";
22
+ continue;
23
+ }
24
+ out[key] = maskRecursive(value, config, event, path);
25
+ }
26
+ return out;
27
+ }
28
+ function applyMasking(event, config) {
29
+ if (!config) return event;
30
+ return {
31
+ ...event,
32
+ props: maskRecursive(event.props, config, event) ?? {}
33
+ };
34
+ }
35
+
36
+ // src/validation.ts
37
+ import { z } from "zod";
38
+ var baseEventSchema = z.object({
39
+ name: z.string().min(1),
40
+ ts: z.number().optional(),
41
+ appId: z.string().min(1),
42
+ sessionId: z.string().optional(),
43
+ url: z.string().optional(),
44
+ path: z.string().optional(),
45
+ ref: z.string().optional(),
46
+ environment: z.string().optional(),
47
+ props: z.record(z.string(), z.unknown()).optional(),
48
+ tags: z.array(z.string()).optional(),
49
+ clientMeta: z.object({
50
+ ip: z.string().optional(),
51
+ userAgent: z.string().optional(),
52
+ isMobile: z.boolean().optional(),
53
+ os: z.string().optional(),
54
+ platform: z.string().optional(),
55
+ language: z.string().optional(),
56
+ screen: z.object({
57
+ width: z.number(),
58
+ height: z.number()
59
+ }).optional()
60
+ }).optional(),
61
+ writeKey: z.string().optional()
62
+ });
63
+ function validateBasePayload(input) {
64
+ const parsed = baseEventSchema.safeParse(input);
65
+ if (!parsed.success) {
66
+ return {
67
+ ok: false,
68
+ error: {
69
+ code: "invalid_payload",
70
+ message: "Invalid track payload",
71
+ details: parsed.error.issues
72
+ }
73
+ };
74
+ }
75
+ return { ok: true, data: parsed.data };
76
+ }
77
+ function validateAgainstAcceptedTracks(event, acceptedTracks, rejectUnknownTracks) {
78
+ if (!acceptedTracks || acceptedTracks.length === 0) return null;
79
+ const trackConfig = acceptedTracks.find((item) => item.trackName === event.name);
80
+ if (!trackConfig) {
81
+ if (!rejectUnknownTracks) return null;
82
+ return {
83
+ ok: false,
84
+ error: {
85
+ code: "track_not_allowed",
86
+ message: `Track '${event.name}' is not in the allowed track list`
87
+ }
88
+ };
89
+ }
90
+ const target = trackConfig.validateOn === "event" ? event : event.props;
91
+ const parsed = trackConfig.schema.safeParse(target);
92
+ if (!parsed.success) {
93
+ return {
94
+ ok: false,
95
+ error: {
96
+ code: "schema_mismatch",
97
+ message: `Track '${event.name}' does not match the configured schema`,
98
+ details: parsed.error.issues
99
+ }
100
+ };
101
+ }
102
+ return null;
103
+ }
104
+
105
+ // src/create-analytics-server.ts
106
+ function buildStoredEvent(input, context) {
107
+ return {
108
+ ...input,
109
+ ts: input.ts ?? Date.now(),
110
+ receivedAt: Date.now(),
111
+ props: input.props ?? {},
112
+ meta: {
113
+ ip: context?.ip,
114
+ userAgent: context?.userAgent,
115
+ requestId: context?.requestId
116
+ }
117
+ };
118
+ }
119
+ function createAnalyticsServer(config) {
120
+ const rejectUnknownTracks = config.rejectUnknownTracks ?? true;
121
+ const generatedAt = Date.now();
122
+ const trackCatalog = (config.acceptedTracks ?? []).map((track) => ({
123
+ trackName: track.trackName,
124
+ validateOn: track.validateOn ?? "props",
125
+ version: track.version ?? 1,
126
+ description: track.description,
127
+ tags: track.tags,
128
+ deprecated: track.deprecated,
129
+ schema: track.catalogSchema
130
+ }));
131
+ async function ingest(input, context) {
132
+ const payloadValidation = validateBasePayload(input);
133
+ if (!payloadValidation.ok) return payloadValidation;
134
+ const event = buildStoredEvent(payloadValidation.data, context);
135
+ const trackValidation = validateAgainstAcceptedTracks(
136
+ event,
137
+ config.acceptedTracks,
138
+ rejectUnknownTracks
139
+ );
140
+ if (trackValidation) return trackValidation;
141
+ const maskedEvent = applyMasking(event, config.masking);
142
+ try {
143
+ await config.storage.save(maskedEvent);
144
+ return {
145
+ ok: true,
146
+ event: maskedEvent
147
+ };
148
+ } catch (error) {
149
+ return {
150
+ ok: false,
151
+ error: {
152
+ code: "storage_error",
153
+ message: "Failed to persist track",
154
+ details: error instanceof Error ? error.message : error
155
+ }
156
+ };
157
+ }
158
+ }
159
+ function getCatalog() {
160
+ return {
161
+ generatedAt,
162
+ tracks: [...trackCatalog]
163
+ };
164
+ }
165
+ return {
166
+ ingest,
167
+ getCatalog
168
+ };
169
+ }
170
+
171
+ // src/adapters/memory-adapter.ts
172
+ function createMemoryAdapter(initialEvents = []) {
173
+ const events = [...initialEvents];
174
+ const adapter = {
175
+ async save(event) {
176
+ events.push(event);
177
+ },
178
+ async getAll(dateInit, dateEnd) {
179
+ if (dateInit === void 0 && dateEnd === void 0) return [...events];
180
+ return events.filter((event) => {
181
+ if (dateInit !== void 0 && event.ts < dateInit) return false;
182
+ if (dateEnd !== void 0 && event.ts > dateEnd) return false;
183
+ return true;
184
+ });
185
+ },
186
+ async clear(dateInit, dateEnd) {
187
+ const before = events.length;
188
+ if (dateInit === void 0 && dateEnd === void 0) {
189
+ events.length = 0;
190
+ return before;
191
+ }
192
+ const kept = events.filter((event) => {
193
+ if (dateInit !== void 0 && event.ts < dateInit) return true;
194
+ if (dateEnd !== void 0 && event.ts > dateEnd) return true;
195
+ if (dateInit === void 0 && dateEnd !== void 0) return event.ts > dateEnd;
196
+ return false;
197
+ });
198
+ events.length = 0;
199
+ events.push(...kept);
200
+ return before - events.length;
201
+ }
202
+ };
203
+ return adapter;
204
+ }
205
+
206
+ // src/adapters/postgres-adapter.ts
207
+ function quoteIdentifier(identifier) {
208
+ if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(identifier)) {
209
+ throw new Error(`Invalid SQL identifier: '${identifier}'`);
210
+ }
211
+ return `"${identifier}"`;
212
+ }
213
+ function buildDateRangeWhereClause(dateInit, dateEnd) {
214
+ const conditions = [];
215
+ const values = [];
216
+ if (dateInit !== void 0) {
217
+ conditions.push(`ts >= $${values.length + 1}`);
218
+ values.push(dateInit);
219
+ }
220
+ if (dateEnd !== void 0) {
221
+ conditions.push(`ts <= $${values.length + 1}`);
222
+ values.push(dateEnd);
223
+ }
224
+ if (conditions.length === 0) {
225
+ return { whereClause: "", values };
226
+ }
227
+ return {
228
+ whereClause: ` where ${conditions.join(" and ")}`,
229
+ values
230
+ };
231
+ }
232
+ function mapRowToStoredEvent(row) {
233
+ return {
234
+ name: row.name,
235
+ ts: row.ts,
236
+ appId: row.app_id,
237
+ sessionId: row.session_id ?? void 0,
238
+ url: row.url ?? void 0,
239
+ path: row.path ?? void 0,
240
+ ref: row.ref ?? void 0,
241
+ environment: row.environment ?? void 0,
242
+ props: row.props ?? {},
243
+ tags: row.tags ?? void 0,
244
+ clientMeta: row.client_meta ?? void 0,
245
+ writeKey: row.write_key ?? void 0,
246
+ receivedAt: row.received_at,
247
+ meta: row.meta ?? void 0
248
+ };
249
+ }
250
+ function extractQueryResultData(result) {
251
+ if (typeof result === "object" && result !== null) {
252
+ return result;
253
+ }
254
+ return {};
255
+ }
256
+ function createPostgresAdapter({
257
+ db,
258
+ tableName = "analytics_events",
259
+ schemaName
260
+ }) {
261
+ const tableRef = schemaName ? `${quoteIdentifier(schemaName)}.${quoteIdentifier(tableName)}` : quoteIdentifier(tableName);
262
+ return {
263
+ async save(event) {
264
+ await db.query(
265
+ `
266
+ insert into ${tableRef}
267
+ (name, ts, app_id, session_id, url, path, ref, environment, props, tags, client_meta, write_key, received_at, meta)
268
+ values
269
+ ($1, $2, $3, $4, $5, $6, $7, $8, $9::jsonb, $10::text[], $11::jsonb, $12, $13, $14::jsonb)
270
+ `,
271
+ [
272
+ event.name,
273
+ event.ts,
274
+ event.appId,
275
+ event.sessionId ?? null,
276
+ event.url ?? null,
277
+ event.path ?? null,
278
+ event.ref ?? null,
279
+ event.environment ?? null,
280
+ event.props,
281
+ event.tags ?? null,
282
+ event.clientMeta ?? null,
283
+ event.writeKey ?? null,
284
+ event.receivedAt,
285
+ event.meta ?? null
286
+ ]
287
+ );
288
+ },
289
+ async getAll(dateInit, dateEnd) {
290
+ const { whereClause, values } = buildDateRangeWhereClause(dateInit, dateEnd);
291
+ const result = await db.query(
292
+ `
293
+ select
294
+ name,
295
+ ts,
296
+ app_id,
297
+ session_id,
298
+ url,
299
+ path,
300
+ ref,
301
+ environment,
302
+ props,
303
+ tags,
304
+ client_meta,
305
+ write_key,
306
+ received_at,
307
+ meta
308
+ from ${tableRef}${whereClause}
309
+ order by ts asc
310
+ `,
311
+ values
312
+ );
313
+ const rows = extractQueryResultData(result).rows ?? [];
314
+ return rows.map((row) => mapRowToStoredEvent(row));
315
+ },
316
+ async clear(dateInit, dateEnd) {
317
+ const { whereClause, values } = buildDateRangeWhereClause(dateInit, dateEnd);
318
+ const result = await db.query(`delete from ${tableRef}${whereClause}`, values);
319
+ return extractQueryResultData(result).rowCount ?? 0;
320
+ }
321
+ };
322
+ }
323
+
324
+ // src/http/create-express-catalog-handler.ts
325
+ function createExpressCatalogHandler(server) {
326
+ return async function handle(req, res) {
327
+ if ((req.method ?? "GET").toUpperCase() !== "GET") {
328
+ return res.status(405).json({ ok: false, error: "method_not_allowed" });
329
+ }
330
+ return res.status(200).json({ ok: true, data: server.getCatalog() });
331
+ };
332
+ }
333
+
334
+ // src/http/create-express-handler.ts
335
+ function getHeader(req, name) {
336
+ const lower = name.toLowerCase();
337
+ if (typeof req.get === "function") return req.get(lower) ?? req.get(name) ?? void 0;
338
+ if (typeof req.header === "function") return req.header(lower) ?? req.header(name) ?? void 0;
339
+ const raw = req.headers?.[lower] ?? req.headers?.[name];
340
+ if (Array.isArray(raw)) return raw[0];
341
+ return raw;
342
+ }
343
+ function extractContextFromExpress(req) {
344
+ return {
345
+ ip: req.ip ?? getHeader(req, "x-forwarded-for"),
346
+ userAgent: getHeader(req, "user-agent"),
347
+ requestId: getHeader(req, "x-request-id")
348
+ };
349
+ }
350
+ function resolveBody(body) {
351
+ if (typeof body === "string") {
352
+ return JSON.parse(body);
353
+ }
354
+ return body;
355
+ }
356
+ function createExpressIngestHandler(server, options = {}) {
357
+ return async function handle(req, res) {
358
+ if ((req.method ?? "GET").toUpperCase() !== "POST") {
359
+ return res.status(405).json({ ok: false, error: "method_not_allowed" });
360
+ }
361
+ let body;
362
+ try {
363
+ body = resolveBody(req.body);
364
+ } catch {
365
+ return res.status(400).json({ ok: false, error: "invalid_json" });
366
+ }
367
+ const context = options.getContext?.(req) ?? extractContextFromExpress(req);
368
+ const result = await server.ingest(body, context);
369
+ if (!result.ok) {
370
+ const status = result.error.code === "storage_error" ? 500 : result.error.code === "invalid_payload" || result.error.code === "schema_mismatch" ? 422 : 403;
371
+ return res.status(status).json({ ok: false, error: result.error });
372
+ }
373
+ return res.status(202).json({ ok: true });
374
+ };
375
+ }
376
+
377
+ // src/http/create-fetch-catalog-handler.ts
378
+ function createFetchCatalogHandler(server) {
379
+ return async function handle(request) {
380
+ if (request.method !== "GET") {
381
+ return Response.json({ ok: false, error: "method_not_allowed" }, { status: 405 });
382
+ }
383
+ return Response.json({ ok: true, data: server.getCatalog() }, { status: 200 });
384
+ };
385
+ }
386
+
387
+ // src/http/create-fetch-handler.ts
388
+ function extractContext(request) {
389
+ return {
390
+ ip: request.headers.get("x-forwarded-for") ?? void 0,
391
+ userAgent: request.headers.get("user-agent") ?? void 0,
392
+ requestId: request.headers.get("x-request-id") ?? void 0
393
+ };
394
+ }
395
+ function createFetchIngestHandler(server) {
396
+ return async function handle(request) {
397
+ if (request.method !== "POST") {
398
+ return Response.json({ ok: false, error: "method_not_allowed" }, { status: 405 });
399
+ }
400
+ let body;
401
+ try {
402
+ body = await request.json();
403
+ } catch {
404
+ return Response.json({ ok: false, error: "invalid_json" }, { status: 400 });
405
+ }
406
+ const result = await server.ingest(body, extractContext(request));
407
+ if (!result.ok) {
408
+ const status = result.error.code === "storage_error" ? 500 : result.error.code === "invalid_payload" || result.error.code === "schema_mismatch" ? 422 : 403;
409
+ return Response.json({ ok: false, error: result.error }, { status });
410
+ }
411
+ return Response.json({ ok: true }, { status: 202 });
412
+ };
413
+ }
414
+
415
+ // src/http/create-catalog-handler.ts
416
+ function createCatalogHandler(server, config = {}) {
417
+ if (config.adapter === "express") {
418
+ return createExpressCatalogHandler(server);
419
+ }
420
+ return createFetchCatalogHandler(server);
421
+ }
422
+
423
+ // src/http/create-ingest-handler.ts
424
+ function createIngestHandler(server, config = {}) {
425
+ if (config.adapter === "express") {
426
+ return createExpressIngestHandler(server, config.express);
427
+ }
428
+ return createFetchIngestHandler(server);
429
+ }
430
+ export {
431
+ createAnalyticsServer,
432
+ createCatalogHandler,
433
+ createExpressCatalogHandler,
434
+ createExpressIngestHandler,
435
+ createFetchCatalogHandler,
436
+ createFetchIngestHandler,
437
+ createIngestHandler,
438
+ createMemoryAdapter,
439
+ createPostgresAdapter
440
+ };
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "@xray-analytics/analytics-server",
3
+ "version": "0.0.2",
4
+ "private": false,
5
+ "main": "dist/index.js",
6
+ "module": "dist/index.mjs",
7
+ "types": "dist/index.d.ts",
8
+ "publishConfig": {
9
+ "access": "public",
10
+ "registry": "https://registry.npmjs.org/"
11
+ },
12
+ "exports": {
13
+ ".": {
14
+ "types": "./dist/index.d.ts",
15
+ "import": "./dist/index.mjs",
16
+ "require": "./dist/index.js"
17
+ }
18
+ },
19
+ "files": [
20
+ "dist"
21
+ ],
22
+ "sideEffects": false,
23
+ "scripts": {
24
+ "build": "tsup src/index.ts --format cjs,esm --dts",
25
+ "lint": "eslint .",
26
+ "typecheck": "tsc -p tsconfig.json --noEmit",
27
+ "test": "vitest run"
28
+ },
29
+ "dependencies": {
30
+ "zod": "^4.1.5"
31
+ },
32
+ "devDependencies": {
33
+ "@types/node": "^25.2.3"
34
+ }
35
+ }