@outray/runtime-shared 0.1.0 → 0.1.1

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,102 @@
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/buffered-logger.ts
21
+ var buffered_logger_exports = {};
22
+ __export(buffered_logger_exports, {
23
+ BufferedAnalyticsLogger: () => BufferedAnalyticsLogger
24
+ });
25
+ module.exports = __toCommonJS(buffered_logger_exports);
26
+ var BufferedAnalyticsLogger = class {
27
+ constructor(queue, name, batchSize, flushIntervalMs, maxBufferBytes = 16 * 1024 * 1024, maxRecordBytes = 0) {
28
+ this.queue = queue;
29
+ this.name = name;
30
+ this.batchSize = batchSize;
31
+ this.flushIntervalMs = flushIntervalMs;
32
+ this.maxBufferBytes = maxBufferBytes;
33
+ this.maxRecordBytes = maxRecordBytes;
34
+ }
35
+ queue;
36
+ name;
37
+ batchSize;
38
+ flushIntervalMs;
39
+ maxBufferBytes;
40
+ maxRecordBytes;
41
+ buffer = [];
42
+ bufferBytes = 0;
43
+ flushing = null;
44
+ interval = null;
45
+ start(deliver = true) {
46
+ if (this.interval) return;
47
+ if (deliver) this.queue.start();
48
+ this.interval = setInterval(() => {
49
+ void this.flush().catch(() => {
50
+ console.error(`${this.name} analytics could not reach Redis; buffered records will retry`);
51
+ });
52
+ }, this.flushIntervalMs);
53
+ this.interval.unref();
54
+ }
55
+ log(record) {
56
+ const bytes = Buffer.byteLength(JSON.stringify(record));
57
+ if (this.maxRecordBytes && bytes > this.maxRecordBytes) {
58
+ console.error(`${this.name} analytics record exceeds the capture size limit; only this record was rejected`);
59
+ return;
60
+ }
61
+ if (this.bufferBytes + bytes > this.maxBufferBytes) {
62
+ console.error(`${this.name} analytics buffer is full; one record was rejected`);
63
+ return;
64
+ }
65
+ this.buffer.push({ record, bytes });
66
+ this.bufferBytes += bytes;
67
+ if (this.buffer.length >= this.batchSize || this.bufferBytes >= 4 * 1024 * 1024) {
68
+ void this.flush().catch(() => {
69
+ console.error(`${this.name} analytics could not reach Redis; buffered records will retry`);
70
+ });
71
+ }
72
+ }
73
+ flush() {
74
+ if (this.flushing) return this.flushing;
75
+ this.flushing = this.flushPending().finally(() => {
76
+ this.flushing = null;
77
+ });
78
+ return this.flushing;
79
+ }
80
+ async flushPending() {
81
+ while (this.buffer.length) {
82
+ const batch = this.buffer.slice(0, this.batchSize);
83
+ await this.queue.enqueue(batch.map(({ record }) => record));
84
+ this.buffer.splice(0, batch.length);
85
+ this.bufferBytes -= batch.reduce((total, item) => total + item.bytes, 0);
86
+ }
87
+ }
88
+ async shutdown() {
89
+ if (this.interval) clearInterval(this.interval);
90
+ this.interval = null;
91
+ try {
92
+ await this.flush();
93
+ } finally {
94
+ await this.queue.close();
95
+ }
96
+ }
97
+ };
98
+ // Annotate the CommonJS export names for ESM import in node:
99
+ 0 && (module.exports = {
100
+ BufferedAnalyticsLogger
101
+ });
102
+ //# sourceMappingURL=buffered-logger.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/buffered-logger.ts"],"sourcesContent":["export interface AnalyticsQueue<T> {\n enqueue(records: T[]): Promise<void>;\n start(): void;\n close(): Promise<void>;\n}\n\n/** Retain failed Redis writes, bound memory, and serialize flushes. */\nexport class BufferedAnalyticsLogger<T> {\n private readonly buffer: { record: T; bytes: number }[] = [];\n private bufferBytes = 0;\n private flushing: Promise<void> | null = null;\n private interval: NodeJS.Timeout | null = null;\n\n constructor(\n private readonly queue: AnalyticsQueue<T>,\n private readonly name: string,\n private readonly batchSize: number,\n private readonly flushIntervalMs: number,\n private readonly maxBufferBytes = 16 * 1024 * 1024,\n private readonly maxRecordBytes = 0,\n ) {}\n\n start(deliver = true): void {\n if (this.interval) return;\n if (deliver) this.queue.start();\n this.interval = setInterval(() => {\n void this.flush().catch(() => {\n console.error(`${this.name} analytics could not reach Redis; buffered records will retry`);\n });\n }, this.flushIntervalMs);\n this.interval.unref();\n }\n\n log(record: T): void {\n const bytes = Buffer.byteLength(JSON.stringify(record));\n if (this.maxRecordBytes && bytes > this.maxRecordBytes) {\n console.error(`${this.name} analytics record exceeds the capture size limit; only this record was rejected`);\n return;\n }\n if (this.bufferBytes + bytes > this.maxBufferBytes) {\n // A bounded in-memory fallback cannot safely absorb an unlimited outage.\n // Make overload explicit without exposing request data in the error.\n console.error(`${this.name} analytics buffer is full; one record was rejected`);\n return;\n }\n this.buffer.push({ record, bytes });\n this.bufferBytes += bytes;\n // Captures can contain two large base64 bodies. Flush on bytes as well as\n // rows so a normal low-count burst does not fill the fallback buffer.\n if (this.buffer.length >= this.batchSize || this.bufferBytes >= 4 * 1024 * 1024) {\n void this.flush().catch(() => {\n console.error(`${this.name} analytics could not reach Redis; buffered records will retry`);\n });\n }\n }\n\n flush(): Promise<void> {\n if (this.flushing) return this.flushing;\n this.flushing = this.flushPending().finally(() => { this.flushing = null; });\n return this.flushing;\n }\n\n private async flushPending(): Promise<void> {\n while (this.buffer.length) {\n const batch = this.buffer.slice(0, this.batchSize);\n await this.queue.enqueue(batch.map(({ record }) => record));\n this.buffer.splice(0, batch.length);\n this.bufferBytes -= batch.reduce((total, item) => total + item.bytes, 0);\n }\n }\n\n async shutdown(): Promise<void> {\n if (this.interval) clearInterval(this.interval);\n this.interval = null;\n try {\n await this.flush();\n } finally {\n await this.queue.close();\n }\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAOO,IAAM,0BAAN,MAAiC;AAAA,EAMtC,YACmB,OACA,MACA,WACA,iBACA,iBAAiB,KAAK,OAAO,MAC7B,iBAAiB,GAClC;AANiB;AACA;AACA;AACA;AACA;AACA;AAAA,EAChB;AAAA,EANgB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EAXF,SAAyC,CAAC;AAAA,EACnD,cAAc;AAAA,EACd,WAAiC;AAAA,EACjC,WAAkC;AAAA,EAW1C,MAAM,UAAU,MAAY;AAC1B,QAAI,KAAK,SAAU;AACnB,QAAI,QAAS,MAAK,MAAM,MAAM;AAC9B,SAAK,WAAW,YAAY,MAAM;AAChC,WAAK,KAAK,MAAM,EAAE,MAAM,MAAM;AAC5B,gBAAQ,MAAM,GAAG,KAAK,IAAI,+DAA+D;AAAA,MAC3F,CAAC;AAAA,IACH,GAAG,KAAK,eAAe;AACvB,SAAK,SAAS,MAAM;AAAA,EACtB;AAAA,EAEA,IAAI,QAAiB;AACnB,UAAM,QAAQ,OAAO,WAAW,KAAK,UAAU,MAAM,CAAC;AACtD,QAAI,KAAK,kBAAkB,QAAQ,KAAK,gBAAgB;AACtD,cAAQ,MAAM,GAAG,KAAK,IAAI,iFAAiF;AAC3G;AAAA,IACF;AACA,QAAI,KAAK,cAAc,QAAQ,KAAK,gBAAgB;AAGlD,cAAQ,MAAM,GAAG,KAAK,IAAI,oDAAoD;AAC9E;AAAA,IACF;AACA,SAAK,OAAO,KAAK,EAAE,QAAQ,MAAM,CAAC;AAClC,SAAK,eAAe;AAGpB,QAAI,KAAK,OAAO,UAAU,KAAK,aAAa,KAAK,eAAe,IAAI,OAAO,MAAM;AAC/E,WAAK,KAAK,MAAM,EAAE,MAAM,MAAM;AAC5B,gBAAQ,MAAM,GAAG,KAAK,IAAI,+DAA+D;AAAA,MAC3F,CAAC;AAAA,IACH;AAAA,EACF;AAAA,EAEA,QAAuB;AACrB,QAAI,KAAK,SAAU,QAAO,KAAK;AAC/B,SAAK,WAAW,KAAK,aAAa,EAAE,QAAQ,MAAM;AAAE,WAAK,WAAW;AAAA,IAAM,CAAC;AAC3E,WAAO,KAAK;AAAA,EACd;AAAA,EAEA,MAAc,eAA8B;AAC1C,WAAO,KAAK,OAAO,QAAQ;AACzB,YAAM,QAAQ,KAAK,OAAO,MAAM,GAAG,KAAK,SAAS;AACjD,YAAM,KAAK,MAAM,QAAQ,MAAM,IAAI,CAAC,EAAE,OAAO,MAAM,MAAM,CAAC;AAC1D,WAAK,OAAO,OAAO,GAAG,MAAM,MAAM;AAClC,WAAK,eAAe,MAAM,OAAO,CAAC,OAAO,SAAS,QAAQ,KAAK,OAAO,CAAC;AAAA,IACzE;AAAA,EACF;AAAA,EAEA,MAAM,WAA0B;AAC9B,QAAI,KAAK,SAAU,eAAc,KAAK,QAAQ;AAC9C,SAAK,WAAW;AAChB,QAAI;AACF,YAAM,KAAK,MAAM;AAAA,IACnB,UAAE;AACA,YAAM,KAAK,MAAM,MAAM;AAAA,IACzB;AAAA,EACF;AACF;","names":[]}
@@ -0,0 +1,26 @@
1
+ interface AnalyticsQueue<T> {
2
+ enqueue(records: T[]): Promise<void>;
3
+ start(): void;
4
+ close(): Promise<void>;
5
+ }
6
+ /** Retain failed Redis writes, bound memory, and serialize flushes. */
7
+ declare class BufferedAnalyticsLogger<T> {
8
+ private readonly queue;
9
+ private readonly name;
10
+ private readonly batchSize;
11
+ private readonly flushIntervalMs;
12
+ private readonly maxBufferBytes;
13
+ private readonly maxRecordBytes;
14
+ private readonly buffer;
15
+ private bufferBytes;
16
+ private flushing;
17
+ private interval;
18
+ constructor(queue: AnalyticsQueue<T>, name: string, batchSize: number, flushIntervalMs: number, maxBufferBytes?: number, maxRecordBytes?: number);
19
+ start(deliver?: boolean): void;
20
+ log(record: T): void;
21
+ flush(): Promise<void>;
22
+ private flushPending;
23
+ shutdown(): Promise<void>;
24
+ }
25
+
26
+ export { type AnalyticsQueue, BufferedAnalyticsLogger };
@@ -0,0 +1,26 @@
1
+ interface AnalyticsQueue<T> {
2
+ enqueue(records: T[]): Promise<void>;
3
+ start(): void;
4
+ close(): Promise<void>;
5
+ }
6
+ /** Retain failed Redis writes, bound memory, and serialize flushes. */
7
+ declare class BufferedAnalyticsLogger<T> {
8
+ private readonly queue;
9
+ private readonly name;
10
+ private readonly batchSize;
11
+ private readonly flushIntervalMs;
12
+ private readonly maxBufferBytes;
13
+ private readonly maxRecordBytes;
14
+ private readonly buffer;
15
+ private bufferBytes;
16
+ private flushing;
17
+ private interval;
18
+ constructor(queue: AnalyticsQueue<T>, name: string, batchSize: number, flushIntervalMs: number, maxBufferBytes?: number, maxRecordBytes?: number);
19
+ start(deliver?: boolean): void;
20
+ log(record: T): void;
21
+ flush(): Promise<void>;
22
+ private flushPending;
23
+ shutdown(): Promise<void>;
24
+ }
25
+
26
+ export { type AnalyticsQueue, BufferedAnalyticsLogger };
@@ -0,0 +1,77 @@
1
+ // src/buffered-logger.ts
2
+ var BufferedAnalyticsLogger = class {
3
+ constructor(queue, name, batchSize, flushIntervalMs, maxBufferBytes = 16 * 1024 * 1024, maxRecordBytes = 0) {
4
+ this.queue = queue;
5
+ this.name = name;
6
+ this.batchSize = batchSize;
7
+ this.flushIntervalMs = flushIntervalMs;
8
+ this.maxBufferBytes = maxBufferBytes;
9
+ this.maxRecordBytes = maxRecordBytes;
10
+ }
11
+ queue;
12
+ name;
13
+ batchSize;
14
+ flushIntervalMs;
15
+ maxBufferBytes;
16
+ maxRecordBytes;
17
+ buffer = [];
18
+ bufferBytes = 0;
19
+ flushing = null;
20
+ interval = null;
21
+ start(deliver = true) {
22
+ if (this.interval) return;
23
+ if (deliver) this.queue.start();
24
+ this.interval = setInterval(() => {
25
+ void this.flush().catch(() => {
26
+ console.error(`${this.name} analytics could not reach Redis; buffered records will retry`);
27
+ });
28
+ }, this.flushIntervalMs);
29
+ this.interval.unref();
30
+ }
31
+ log(record) {
32
+ const bytes = Buffer.byteLength(JSON.stringify(record));
33
+ if (this.maxRecordBytes && bytes > this.maxRecordBytes) {
34
+ console.error(`${this.name} analytics record exceeds the capture size limit; only this record was rejected`);
35
+ return;
36
+ }
37
+ if (this.bufferBytes + bytes > this.maxBufferBytes) {
38
+ console.error(`${this.name} analytics buffer is full; one record was rejected`);
39
+ return;
40
+ }
41
+ this.buffer.push({ record, bytes });
42
+ this.bufferBytes += bytes;
43
+ if (this.buffer.length >= this.batchSize || this.bufferBytes >= 4 * 1024 * 1024) {
44
+ void this.flush().catch(() => {
45
+ console.error(`${this.name} analytics could not reach Redis; buffered records will retry`);
46
+ });
47
+ }
48
+ }
49
+ flush() {
50
+ if (this.flushing) return this.flushing;
51
+ this.flushing = this.flushPending().finally(() => {
52
+ this.flushing = null;
53
+ });
54
+ return this.flushing;
55
+ }
56
+ async flushPending() {
57
+ while (this.buffer.length) {
58
+ const batch = this.buffer.slice(0, this.batchSize);
59
+ await this.queue.enqueue(batch.map(({ record }) => record));
60
+ this.buffer.splice(0, batch.length);
61
+ this.bufferBytes -= batch.reduce((total, item) => total + item.bytes, 0);
62
+ }
63
+ }
64
+ async shutdown() {
65
+ if (this.interval) clearInterval(this.interval);
66
+ this.interval = null;
67
+ try {
68
+ await this.flush();
69
+ } finally {
70
+ await this.queue.close();
71
+ }
72
+ }
73
+ };
74
+ export {
75
+ BufferedAnalyticsLogger
76
+ };
77
+ //# sourceMappingURL=buffered-logger.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/buffered-logger.ts"],"sourcesContent":["export interface AnalyticsQueue<T> {\n enqueue(records: T[]): Promise<void>;\n start(): void;\n close(): Promise<void>;\n}\n\n/** Retain failed Redis writes, bound memory, and serialize flushes. */\nexport class BufferedAnalyticsLogger<T> {\n private readonly buffer: { record: T; bytes: number }[] = [];\n private bufferBytes = 0;\n private flushing: Promise<void> | null = null;\n private interval: NodeJS.Timeout | null = null;\n\n constructor(\n private readonly queue: AnalyticsQueue<T>,\n private readonly name: string,\n private readonly batchSize: number,\n private readonly flushIntervalMs: number,\n private readonly maxBufferBytes = 16 * 1024 * 1024,\n private readonly maxRecordBytes = 0,\n ) {}\n\n start(deliver = true): void {\n if (this.interval) return;\n if (deliver) this.queue.start();\n this.interval = setInterval(() => {\n void this.flush().catch(() => {\n console.error(`${this.name} analytics could not reach Redis; buffered records will retry`);\n });\n }, this.flushIntervalMs);\n this.interval.unref();\n }\n\n log(record: T): void {\n const bytes = Buffer.byteLength(JSON.stringify(record));\n if (this.maxRecordBytes && bytes > this.maxRecordBytes) {\n console.error(`${this.name} analytics record exceeds the capture size limit; only this record was rejected`);\n return;\n }\n if (this.bufferBytes + bytes > this.maxBufferBytes) {\n // A bounded in-memory fallback cannot safely absorb an unlimited outage.\n // Make overload explicit without exposing request data in the error.\n console.error(`${this.name} analytics buffer is full; one record was rejected`);\n return;\n }\n this.buffer.push({ record, bytes });\n this.bufferBytes += bytes;\n // Captures can contain two large base64 bodies. Flush on bytes as well as\n // rows so a normal low-count burst does not fill the fallback buffer.\n if (this.buffer.length >= this.batchSize || this.bufferBytes >= 4 * 1024 * 1024) {\n void this.flush().catch(() => {\n console.error(`${this.name} analytics could not reach Redis; buffered records will retry`);\n });\n }\n }\n\n flush(): Promise<void> {\n if (this.flushing) return this.flushing;\n this.flushing = this.flushPending().finally(() => { this.flushing = null; });\n return this.flushing;\n }\n\n private async flushPending(): Promise<void> {\n while (this.buffer.length) {\n const batch = this.buffer.slice(0, this.batchSize);\n await this.queue.enqueue(batch.map(({ record }) => record));\n this.buffer.splice(0, batch.length);\n this.bufferBytes -= batch.reduce((total, item) => total + item.bytes, 0);\n }\n }\n\n async shutdown(): Promise<void> {\n if (this.interval) clearInterval(this.interval);\n this.interval = null;\n try {\n await this.flush();\n } finally {\n await this.queue.close();\n }\n }\n}\n"],"mappings":";AAOO,IAAM,0BAAN,MAAiC;AAAA,EAMtC,YACmB,OACA,MACA,WACA,iBACA,iBAAiB,KAAK,OAAO,MAC7B,iBAAiB,GAClC;AANiB;AACA;AACA;AACA;AACA;AACA;AAAA,EAChB;AAAA,EANgB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EAXF,SAAyC,CAAC;AAAA,EACnD,cAAc;AAAA,EACd,WAAiC;AAAA,EACjC,WAAkC;AAAA,EAW1C,MAAM,UAAU,MAAY;AAC1B,QAAI,KAAK,SAAU;AACnB,QAAI,QAAS,MAAK,MAAM,MAAM;AAC9B,SAAK,WAAW,YAAY,MAAM;AAChC,WAAK,KAAK,MAAM,EAAE,MAAM,MAAM;AAC5B,gBAAQ,MAAM,GAAG,KAAK,IAAI,+DAA+D;AAAA,MAC3F,CAAC;AAAA,IACH,GAAG,KAAK,eAAe;AACvB,SAAK,SAAS,MAAM;AAAA,EACtB;AAAA,EAEA,IAAI,QAAiB;AACnB,UAAM,QAAQ,OAAO,WAAW,KAAK,UAAU,MAAM,CAAC;AACtD,QAAI,KAAK,kBAAkB,QAAQ,KAAK,gBAAgB;AACtD,cAAQ,MAAM,GAAG,KAAK,IAAI,iFAAiF;AAC3G;AAAA,IACF;AACA,QAAI,KAAK,cAAc,QAAQ,KAAK,gBAAgB;AAGlD,cAAQ,MAAM,GAAG,KAAK,IAAI,oDAAoD;AAC9E;AAAA,IACF;AACA,SAAK,OAAO,KAAK,EAAE,QAAQ,MAAM,CAAC;AAClC,SAAK,eAAe;AAGpB,QAAI,KAAK,OAAO,UAAU,KAAK,aAAa,KAAK,eAAe,IAAI,OAAO,MAAM;AAC/E,WAAK,KAAK,MAAM,EAAE,MAAM,MAAM;AAC5B,gBAAQ,MAAM,GAAG,KAAK,IAAI,+DAA+D;AAAA,MAC3F,CAAC;AAAA,IACH;AAAA,EACF;AAAA,EAEA,QAAuB;AACrB,QAAI,KAAK,SAAU,QAAO,KAAK;AAC/B,SAAK,WAAW,KAAK,aAAa,EAAE,QAAQ,MAAM;AAAE,WAAK,WAAW;AAAA,IAAM,CAAC;AAC3E,WAAO,KAAK;AAAA,EACd;AAAA,EAEA,MAAc,eAA8B;AAC1C,WAAO,KAAK,OAAO,QAAQ;AACzB,YAAM,QAAQ,KAAK,OAAO,MAAM,GAAG,KAAK,SAAS;AACjD,YAAM,KAAK,MAAM,QAAQ,MAAM,IAAI,CAAC,EAAE,OAAO,MAAM,MAAM,CAAC;AAC1D,WAAK,OAAO,OAAO,GAAG,MAAM,MAAM;AAClC,WAAK,eAAe,MAAM,OAAO,CAAC,OAAO,SAAS,QAAQ,KAAK,OAAO,CAAC;AAAA,IACzE;AAAA,EACF;AAAA,EAEA,MAAM,WAA0B;AAC9B,QAAI,KAAK,SAAU,eAAc,KAAK,QAAQ;AAC9C,SAAK,WAAW;AAChB,QAAI;AACF,YAAM,KAAK,MAAM;AAAA,IACnB,UAAE;AACA,YAAM,KAAK,MAAM,MAAM;AAAA,IACzB;AAAA,EACF;AACF;","names":[]}
@@ -0,0 +1,160 @@
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/tinybird-records.ts
21
+ var tinybird_records_exports = {};
22
+ __export(tinybird_records_exports, {
23
+ TunnelTinybirdClient: () => TunnelTinybirdClient,
24
+ captureRecordIsCurrent: () => captureRecordIsCurrent,
25
+ normalizeRetentionDays: () => normalizeRetentionDays,
26
+ protocolEventRecord: () => protocolEventRecord,
27
+ requestCaptureRecord: () => requestCaptureRecord,
28
+ tinybirdTimestamp: () => tinybirdTimestamp,
29
+ tunnelEventRecord: () => tunnelEventRecord
30
+ });
31
+ module.exports = __toCommonJS(tinybird_records_exports);
32
+ var import_node_crypto = require("crypto");
33
+ function normalizeRetentionDays(retentionDays) {
34
+ if (!Number.isFinite(retentionDays)) return 3;
35
+ return Math.min(90, Math.max(1, Math.trunc(retentionDays)));
36
+ }
37
+ function tinybirdTimestamp(timestamp) {
38
+ return new Date(timestamp).toISOString().replace("T", " ").replace("Z", "");
39
+ }
40
+ function tunnelEventRecord(event) {
41
+ return {
42
+ ...event,
43
+ request_id: event.request_id || "",
44
+ event_id: event.request_id || (0, import_node_crypto.randomUUID)(),
45
+ timestamp: tinybirdTimestamp(event.timestamp),
46
+ ingested_at: tinybirdTimestamp(Date.now()),
47
+ retention_days: normalizeRetentionDays(event.retention_days)
48
+ };
49
+ }
50
+ function requestCaptureRecord(capture) {
51
+ return {
52
+ ...capture,
53
+ timestamp: tinybirdTimestamp(capture.timestamp),
54
+ ingested_at: tinybirdTimestamp(Date.now()),
55
+ retention_days: normalizeRetentionDays(capture.retention_days),
56
+ request_headers: JSON.stringify(capture.request_headers),
57
+ response_headers: JSON.stringify(capture.response_headers)
58
+ };
59
+ }
60
+ function protocolEventRecord(event) {
61
+ return {
62
+ ...event,
63
+ event_id: (0, import_node_crypto.randomUUID)(),
64
+ timestamp: tinybirdTimestamp(event.timestamp),
65
+ ingested_at: tinybirdTimestamp(Date.now()),
66
+ retention_days: normalizeRetentionDays(event.retention_days)
67
+ };
68
+ }
69
+ var TunnelTinybirdClient = class {
70
+ constructor(host, token) {
71
+ this.host = host;
72
+ this.token = token;
73
+ }
74
+ host;
75
+ token;
76
+ get configured() {
77
+ if (!this.host || !this.token) return false;
78
+ try {
79
+ const url = new URL(this.host);
80
+ return (url.protocol === "https:" || url.protocol === "http:" && ["localhost", "127.0.0.1", "[::1]"].includes(url.hostname)) && !url.username && !url.password && !url.search && !url.hash && url.pathname === "/";
81
+ } catch {
82
+ return false;
83
+ }
84
+ }
85
+ async append(datasource, records) {
86
+ if (datasource === "tunnel_request_captures") {
87
+ const oversized = records.filter((record) => Buffer.byteLength(JSON.stringify(record)) > 3 * 1024 * 1024).length;
88
+ if (oversized) console.error(`Tinybird capture delivery rejected ${oversized} oversized records; other captures will continue`);
89
+ records = records.filter((record) => captureRecordIsCurrent(record) && Buffer.byteLength(JSON.stringify(record)) <= 3 * 1024 * 1024);
90
+ }
91
+ if (!records.length) return;
92
+ if (!this.configured) throw new Error("Tinybird tunnel ingestion is not configured");
93
+ const maxBytes = 4 * 1024 * 1024;
94
+ let lines = [];
95
+ let bytes = 0;
96
+ for (const record of records) {
97
+ const line = JSON.stringify(record);
98
+ const lineBytes = Buffer.byteLength(line) + 1;
99
+ if (lineBytes > maxBytes) {
100
+ throw new Error(`Tinybird ${datasource} record exceeds the ingestion size limit`);
101
+ }
102
+ if (bytes + lineBytes > maxBytes && lines.length) {
103
+ await this.appendBatch(datasource, lines);
104
+ lines = [];
105
+ bytes = 0;
106
+ }
107
+ lines.push(line);
108
+ bytes += lineBytes;
109
+ }
110
+ if (lines.length) await this.appendBatch(datasource, lines);
111
+ }
112
+ async appendBatch(datasource, lines) {
113
+ const response = await fetch(
114
+ `${this.host.replace(/\/$/, "")}/v0/events?name=${encodeURIComponent(datasource)}&wait=true`,
115
+ {
116
+ method: "POST",
117
+ headers: {
118
+ Authorization: `Bearer ${this.token}`,
119
+ "Content-Type": "application/x-ndjson"
120
+ },
121
+ body: lines.join("\n"),
122
+ signal: AbortSignal.timeout(15e3)
123
+ }
124
+ );
125
+ if (!response.ok) {
126
+ await response.body?.cancel();
127
+ throw new Error(`Tinybird ${datasource} ingestion failed (${response.status})`);
128
+ }
129
+ let result;
130
+ try {
131
+ const candidate = await response.json();
132
+ if (!candidate || typeof candidate !== "object" || Array.isArray(candidate)) {
133
+ throw new Error("invalid acknowledgement");
134
+ }
135
+ result = candidate;
136
+ } catch {
137
+ throw new Error(`Tinybird ${datasource} ingestion returned an invalid acknowledgement`);
138
+ }
139
+ if (Number(result.quarantined_rows) > 0 || Number(result.successful_rows) !== lines.length) {
140
+ throw new Error(`Tinybird ${datasource} ingestion did not acknowledge the complete batch`);
141
+ }
142
+ }
143
+ };
144
+ function captureRecordIsCurrent(record, now = Date.now()) {
145
+ if (typeof record.timestamp !== "string") return false;
146
+ const timestamp = Date.parse(`${record.timestamp.replace(" ", "T").replace(/Z$/, "")}Z`);
147
+ const days = normalizeRetentionDays(Number(record.retention_days));
148
+ return Number.isFinite(timestamp) && timestamp + days * 24 * 60 * 60 * 1e3 > now;
149
+ }
150
+ // Annotate the CommonJS export names for ESM import in node:
151
+ 0 && (module.exports = {
152
+ TunnelTinybirdClient,
153
+ captureRecordIsCurrent,
154
+ normalizeRetentionDays,
155
+ protocolEventRecord,
156
+ requestCaptureRecord,
157
+ tinybirdTimestamp,
158
+ tunnelEventRecord
159
+ });
160
+ //# sourceMappingURL=tinybird-records.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/tinybird-records.ts"],"sourcesContent":["import { randomUUID } from \"node:crypto\";\n\nexport interface TunnelEvent {\n request_id?: string;\n timestamp: number;\n tunnel_id: string;\n organization_id: string;\n retention_days: number;\n host: string;\n method: string;\n path: string;\n status_code: number;\n request_duration_ms: number;\n bytes_in: number;\n bytes_out: number;\n client_ip: string;\n user_agent: string;\n}\n\nexport interface RequestCapture {\n id: string;\n timestamp: number;\n tunnel_id: string;\n organization_id: string;\n retention_days: number;\n request_headers: Record<string, string | string[]>;\n request_body: string | null;\n request_body_size: number;\n response_headers: Record<string, string | string[]>;\n response_body: string | null;\n response_body_size: number;\n}\n\nexport interface ProtocolEvent {\n direction?: \"in\" | \"out\";\n timestamp: number;\n tunnel_id: string;\n organization_id: string;\n retention_days: number;\n protocol: \"tcp\" | \"udp\";\n event_type: \"connection\" | \"data\" | \"close\" | \"packet\";\n connection_id: string;\n client_ip: string;\n client_port: number;\n bytes_in: number;\n bytes_out: number;\n duration_ms: number;\n}\n\nexport type TinybirdTunnelRecord = Record<string, string | number | null>;\n\nexport function normalizeRetentionDays(retentionDays: number): number {\n if (!Number.isFinite(retentionDays)) return 3;\n return Math.min(90, Math.max(1, Math.trunc(retentionDays)));\n}\n\nexport function tinybirdTimestamp(timestamp: number | Date): string {\n return new Date(timestamp).toISOString().replace(\"T\", \" \").replace(\"Z\", \"\");\n}\n\nexport function tunnelEventRecord(event: TunnelEvent): TinybirdTunnelRecord {\n return {\n ...event,\n request_id: event.request_id || \"\",\n event_id: event.request_id || randomUUID(),\n timestamp: tinybirdTimestamp(event.timestamp),\n ingested_at: tinybirdTimestamp(Date.now()),\n retention_days: normalizeRetentionDays(event.retention_days),\n };\n}\n\nexport function requestCaptureRecord(capture: RequestCapture): TinybirdTunnelRecord {\n return {\n ...capture,\n timestamp: tinybirdTimestamp(capture.timestamp),\n ingested_at: tinybirdTimestamp(Date.now()),\n retention_days: normalizeRetentionDays(capture.retention_days),\n request_headers: JSON.stringify(capture.request_headers),\n response_headers: JSON.stringify(capture.response_headers),\n };\n}\n\nexport function protocolEventRecord(event: ProtocolEvent): TinybirdTunnelRecord {\n return {\n ...event,\n event_id: randomUUID(),\n timestamp: tinybirdTimestamp(event.timestamp),\n ingested_at: tinybirdTimestamp(Date.now()),\n retention_days: normalizeRetentionDays(event.retention_days),\n };\n}\n\nexport class TunnelTinybirdClient {\n constructor(private readonly host: string, private readonly token: string) {}\n\n get configured(): boolean {\n if (!this.host || !this.token) return false;\n try {\n const url = new URL(this.host);\n return (url.protocol === \"https:\" ||\n (url.protocol === \"http:\" && [\"localhost\", \"127.0.0.1\", \"[::1]\"].includes(url.hostname))) &&\n !url.username && !url.password && !url.search && !url.hash && url.pathname === \"/\";\n } catch {\n return false;\n }\n }\n\n async append(datasource: string, records: TinybirdTunnelRecord[]): Promise<void> {\n if (datasource === \"tunnel_request_captures\") {\n const oversized = records.filter((record) => Buffer.byteLength(JSON.stringify(record)) > 3 * 1024 * 1024).length;\n if (oversized) console.error(`Tinybird capture delivery rejected ${oversized} oversized records; other captures will continue`);\n records = records.filter((record) => captureRecordIsCurrent(record) && Buffer.byteLength(JSON.stringify(record)) <= 3 * 1024 * 1024);\n }\n if (!records.length) return;\n if (!this.configured) throw new Error(\"Tinybird tunnel ingestion is not configured\");\n // Keep requests below the Events API payload ceiling, even when captures\n // contain large base64 request/response bodies. Stable ids make retrying an\n // already accepted prefix safe when a later request fails.\n const maxBytes = 4 * 1024 * 1024;\n let lines: string[] = [];\n let bytes = 0;\n for (const record of records) {\n const line = JSON.stringify(record);\n const lineBytes = Buffer.byteLength(line) + 1;\n if (lineBytes > maxBytes) {\n throw new Error(`Tinybird ${datasource} record exceeds the ingestion size limit`);\n }\n if (bytes + lineBytes > maxBytes && lines.length) {\n await this.appendBatch(datasource, lines);\n lines = [];\n bytes = 0;\n }\n lines.push(line);\n bytes += lineBytes;\n }\n if (lines.length) await this.appendBatch(datasource, lines);\n }\n\n private async appendBatch(datasource: string, lines: string[]): Promise<void> {\n const response = await fetch(\n `${this.host.replace(/\\/$/, \"\")}/v0/events?name=${encodeURIComponent(datasource)}&wait=true`,\n {\n method: \"POST\",\n headers: {\n Authorization: `Bearer ${this.token}`,\n \"Content-Type\": \"application/x-ndjson\",\n },\n body: lines.join(\"\\n\"),\n signal: AbortSignal.timeout(15_000),\n },\n );\n // Error bodies can echo capture values. Never include them in logs/errors.\n if (!response.ok) {\n await response.body?.cancel();\n throw new Error(`Tinybird ${datasource} ingestion failed (${response.status})`);\n }\n let result: { successful_rows?: number; quarantined_rows?: number };\n try {\n const candidate: unknown = await response.json();\n if (!candidate || typeof candidate !== \"object\" || Array.isArray(candidate)) {\n throw new Error(\"invalid acknowledgement\");\n }\n result = candidate as typeof result;\n } catch {\n throw new Error(`Tinybird ${datasource} ingestion returned an invalid acknowledgement`);\n }\n if (Number(result.quarantined_rows) > 0 || Number(result.successful_rows) !== lines.length) {\n throw new Error(`Tinybird ${datasource} ingestion did not acknowledge the complete batch`);\n }\n }\n}\n\n/** Backlogs must never republish captures past their individual plan expiry. */\nexport function captureRecordIsCurrent(record: TinybirdTunnelRecord, now = Date.now()): boolean {\n if (typeof record.timestamp !== \"string\") return false;\n const timestamp = Date.parse(`${record.timestamp.replace(\" \", \"T\").replace(/Z$/, \"\")}Z`);\n const days = normalizeRetentionDays(Number(record.retention_days));\n return Number.isFinite(timestamp) && timestamp + days * 24 * 60 * 60 * 1000 > now;\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,62 @@
1
+ interface TunnelEvent {
2
+ request_id?: string;
3
+ timestamp: number;
4
+ tunnel_id: string;
5
+ organization_id: string;
6
+ retention_days: number;
7
+ host: string;
8
+ method: string;
9
+ path: string;
10
+ status_code: number;
11
+ request_duration_ms: number;
12
+ bytes_in: number;
13
+ bytes_out: number;
14
+ client_ip: string;
15
+ user_agent: string;
16
+ }
17
+ interface RequestCapture {
18
+ id: string;
19
+ timestamp: number;
20
+ tunnel_id: string;
21
+ organization_id: string;
22
+ retention_days: number;
23
+ request_headers: Record<string, string | string[]>;
24
+ request_body: string | null;
25
+ request_body_size: number;
26
+ response_headers: Record<string, string | string[]>;
27
+ response_body: string | null;
28
+ response_body_size: number;
29
+ }
30
+ interface ProtocolEvent {
31
+ direction?: "in" | "out";
32
+ timestamp: number;
33
+ tunnel_id: string;
34
+ organization_id: string;
35
+ retention_days: number;
36
+ protocol: "tcp" | "udp";
37
+ event_type: "connection" | "data" | "close" | "packet";
38
+ connection_id: string;
39
+ client_ip: string;
40
+ client_port: number;
41
+ bytes_in: number;
42
+ bytes_out: number;
43
+ duration_ms: number;
44
+ }
45
+ type TinybirdTunnelRecord = Record<string, string | number | null>;
46
+ declare function normalizeRetentionDays(retentionDays: number): number;
47
+ declare function tinybirdTimestamp(timestamp: number | Date): string;
48
+ declare function tunnelEventRecord(event: TunnelEvent): TinybirdTunnelRecord;
49
+ declare function requestCaptureRecord(capture: RequestCapture): TinybirdTunnelRecord;
50
+ declare function protocolEventRecord(event: ProtocolEvent): TinybirdTunnelRecord;
51
+ declare class TunnelTinybirdClient {
52
+ private readonly host;
53
+ private readonly token;
54
+ constructor(host: string, token: string);
55
+ get configured(): boolean;
56
+ append(datasource: string, records: TinybirdTunnelRecord[]): Promise<void>;
57
+ private appendBatch;
58
+ }
59
+ /** Backlogs must never republish captures past their individual plan expiry. */
60
+ declare function captureRecordIsCurrent(record: TinybirdTunnelRecord, now?: number): boolean;
61
+
62
+ export { type ProtocolEvent, type RequestCapture, type TinybirdTunnelRecord, type TunnelEvent, TunnelTinybirdClient, captureRecordIsCurrent, normalizeRetentionDays, protocolEventRecord, requestCaptureRecord, tinybirdTimestamp, tunnelEventRecord };
@@ -0,0 +1,62 @@
1
+ interface TunnelEvent {
2
+ request_id?: string;
3
+ timestamp: number;
4
+ tunnel_id: string;
5
+ organization_id: string;
6
+ retention_days: number;
7
+ host: string;
8
+ method: string;
9
+ path: string;
10
+ status_code: number;
11
+ request_duration_ms: number;
12
+ bytes_in: number;
13
+ bytes_out: number;
14
+ client_ip: string;
15
+ user_agent: string;
16
+ }
17
+ interface RequestCapture {
18
+ id: string;
19
+ timestamp: number;
20
+ tunnel_id: string;
21
+ organization_id: string;
22
+ retention_days: number;
23
+ request_headers: Record<string, string | string[]>;
24
+ request_body: string | null;
25
+ request_body_size: number;
26
+ response_headers: Record<string, string | string[]>;
27
+ response_body: string | null;
28
+ response_body_size: number;
29
+ }
30
+ interface ProtocolEvent {
31
+ direction?: "in" | "out";
32
+ timestamp: number;
33
+ tunnel_id: string;
34
+ organization_id: string;
35
+ retention_days: number;
36
+ protocol: "tcp" | "udp";
37
+ event_type: "connection" | "data" | "close" | "packet";
38
+ connection_id: string;
39
+ client_ip: string;
40
+ client_port: number;
41
+ bytes_in: number;
42
+ bytes_out: number;
43
+ duration_ms: number;
44
+ }
45
+ type TinybirdTunnelRecord = Record<string, string | number | null>;
46
+ declare function normalizeRetentionDays(retentionDays: number): number;
47
+ declare function tinybirdTimestamp(timestamp: number | Date): string;
48
+ declare function tunnelEventRecord(event: TunnelEvent): TinybirdTunnelRecord;
49
+ declare function requestCaptureRecord(capture: RequestCapture): TinybirdTunnelRecord;
50
+ declare function protocolEventRecord(event: ProtocolEvent): TinybirdTunnelRecord;
51
+ declare class TunnelTinybirdClient {
52
+ private readonly host;
53
+ private readonly token;
54
+ constructor(host: string, token: string);
55
+ get configured(): boolean;
56
+ append(datasource: string, records: TinybirdTunnelRecord[]): Promise<void>;
57
+ private appendBatch;
58
+ }
59
+ /** Backlogs must never republish captures past their individual plan expiry. */
60
+ declare function captureRecordIsCurrent(record: TinybirdTunnelRecord, now?: number): boolean;
61
+
62
+ export { type ProtocolEvent, type RequestCapture, type TinybirdTunnelRecord, type TunnelEvent, TunnelTinybirdClient, captureRecordIsCurrent, normalizeRetentionDays, protocolEventRecord, requestCaptureRecord, tinybirdTimestamp, tunnelEventRecord };
@@ -0,0 +1,129 @@
1
+ // src/tinybird-records.ts
2
+ import { randomUUID } from "crypto";
3
+ function normalizeRetentionDays(retentionDays) {
4
+ if (!Number.isFinite(retentionDays)) return 3;
5
+ return Math.min(90, Math.max(1, Math.trunc(retentionDays)));
6
+ }
7
+ function tinybirdTimestamp(timestamp) {
8
+ return new Date(timestamp).toISOString().replace("T", " ").replace("Z", "");
9
+ }
10
+ function tunnelEventRecord(event) {
11
+ return {
12
+ ...event,
13
+ request_id: event.request_id || "",
14
+ event_id: event.request_id || randomUUID(),
15
+ timestamp: tinybirdTimestamp(event.timestamp),
16
+ ingested_at: tinybirdTimestamp(Date.now()),
17
+ retention_days: normalizeRetentionDays(event.retention_days)
18
+ };
19
+ }
20
+ function requestCaptureRecord(capture) {
21
+ return {
22
+ ...capture,
23
+ timestamp: tinybirdTimestamp(capture.timestamp),
24
+ ingested_at: tinybirdTimestamp(Date.now()),
25
+ retention_days: normalizeRetentionDays(capture.retention_days),
26
+ request_headers: JSON.stringify(capture.request_headers),
27
+ response_headers: JSON.stringify(capture.response_headers)
28
+ };
29
+ }
30
+ function protocolEventRecord(event) {
31
+ return {
32
+ ...event,
33
+ event_id: randomUUID(),
34
+ timestamp: tinybirdTimestamp(event.timestamp),
35
+ ingested_at: tinybirdTimestamp(Date.now()),
36
+ retention_days: normalizeRetentionDays(event.retention_days)
37
+ };
38
+ }
39
+ var TunnelTinybirdClient = class {
40
+ constructor(host, token) {
41
+ this.host = host;
42
+ this.token = token;
43
+ }
44
+ host;
45
+ token;
46
+ get configured() {
47
+ if (!this.host || !this.token) return false;
48
+ try {
49
+ const url = new URL(this.host);
50
+ return (url.protocol === "https:" || url.protocol === "http:" && ["localhost", "127.0.0.1", "[::1]"].includes(url.hostname)) && !url.username && !url.password && !url.search && !url.hash && url.pathname === "/";
51
+ } catch {
52
+ return false;
53
+ }
54
+ }
55
+ async append(datasource, records) {
56
+ if (datasource === "tunnel_request_captures") {
57
+ const oversized = records.filter((record) => Buffer.byteLength(JSON.stringify(record)) > 3 * 1024 * 1024).length;
58
+ if (oversized) console.error(`Tinybird capture delivery rejected ${oversized} oversized records; other captures will continue`);
59
+ records = records.filter((record) => captureRecordIsCurrent(record) && Buffer.byteLength(JSON.stringify(record)) <= 3 * 1024 * 1024);
60
+ }
61
+ if (!records.length) return;
62
+ if (!this.configured) throw new Error("Tinybird tunnel ingestion is not configured");
63
+ const maxBytes = 4 * 1024 * 1024;
64
+ let lines = [];
65
+ let bytes = 0;
66
+ for (const record of records) {
67
+ const line = JSON.stringify(record);
68
+ const lineBytes = Buffer.byteLength(line) + 1;
69
+ if (lineBytes > maxBytes) {
70
+ throw new Error(`Tinybird ${datasource} record exceeds the ingestion size limit`);
71
+ }
72
+ if (bytes + lineBytes > maxBytes && lines.length) {
73
+ await this.appendBatch(datasource, lines);
74
+ lines = [];
75
+ bytes = 0;
76
+ }
77
+ lines.push(line);
78
+ bytes += lineBytes;
79
+ }
80
+ if (lines.length) await this.appendBatch(datasource, lines);
81
+ }
82
+ async appendBatch(datasource, lines) {
83
+ const response = await fetch(
84
+ `${this.host.replace(/\/$/, "")}/v0/events?name=${encodeURIComponent(datasource)}&wait=true`,
85
+ {
86
+ method: "POST",
87
+ headers: {
88
+ Authorization: `Bearer ${this.token}`,
89
+ "Content-Type": "application/x-ndjson"
90
+ },
91
+ body: lines.join("\n"),
92
+ signal: AbortSignal.timeout(15e3)
93
+ }
94
+ );
95
+ if (!response.ok) {
96
+ await response.body?.cancel();
97
+ throw new Error(`Tinybird ${datasource} ingestion failed (${response.status})`);
98
+ }
99
+ let result;
100
+ try {
101
+ const candidate = await response.json();
102
+ if (!candidate || typeof candidate !== "object" || Array.isArray(candidate)) {
103
+ throw new Error("invalid acknowledgement");
104
+ }
105
+ result = candidate;
106
+ } catch {
107
+ throw new Error(`Tinybird ${datasource} ingestion returned an invalid acknowledgement`);
108
+ }
109
+ if (Number(result.quarantined_rows) > 0 || Number(result.successful_rows) !== lines.length) {
110
+ throw new Error(`Tinybird ${datasource} ingestion did not acknowledge the complete batch`);
111
+ }
112
+ }
113
+ };
114
+ function captureRecordIsCurrent(record, now = Date.now()) {
115
+ if (typeof record.timestamp !== "string") return false;
116
+ const timestamp = Date.parse(`${record.timestamp.replace(" ", "T").replace(/Z$/, "")}Z`);
117
+ const days = normalizeRetentionDays(Number(record.retention_days));
118
+ return Number.isFinite(timestamp) && timestamp + days * 24 * 60 * 60 * 1e3 > now;
119
+ }
120
+ export {
121
+ TunnelTinybirdClient,
122
+ captureRecordIsCurrent,
123
+ normalizeRetentionDays,
124
+ protocolEventRecord,
125
+ requestCaptureRecord,
126
+ tinybirdTimestamp,
127
+ tunnelEventRecord
128
+ };
129
+ //# sourceMappingURL=tinybird-records.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/tinybird-records.ts"],"sourcesContent":["import { randomUUID } from \"node:crypto\";\n\nexport interface TunnelEvent {\n request_id?: string;\n timestamp: number;\n tunnel_id: string;\n organization_id: string;\n retention_days: number;\n host: string;\n method: string;\n path: string;\n status_code: number;\n request_duration_ms: number;\n bytes_in: number;\n bytes_out: number;\n client_ip: string;\n user_agent: string;\n}\n\nexport interface RequestCapture {\n id: string;\n timestamp: number;\n tunnel_id: string;\n organization_id: string;\n retention_days: number;\n request_headers: Record<string, string | string[]>;\n request_body: string | null;\n request_body_size: number;\n response_headers: Record<string, string | string[]>;\n response_body: string | null;\n response_body_size: number;\n}\n\nexport interface ProtocolEvent {\n direction?: \"in\" | \"out\";\n timestamp: number;\n tunnel_id: string;\n organization_id: string;\n retention_days: number;\n protocol: \"tcp\" | \"udp\";\n event_type: \"connection\" | \"data\" | \"close\" | \"packet\";\n connection_id: string;\n client_ip: string;\n client_port: number;\n bytes_in: number;\n bytes_out: number;\n duration_ms: number;\n}\n\nexport type TinybirdTunnelRecord = Record<string, string | number | null>;\n\nexport function normalizeRetentionDays(retentionDays: number): number {\n if (!Number.isFinite(retentionDays)) return 3;\n return Math.min(90, Math.max(1, Math.trunc(retentionDays)));\n}\n\nexport function tinybirdTimestamp(timestamp: number | Date): string {\n return new Date(timestamp).toISOString().replace(\"T\", \" \").replace(\"Z\", \"\");\n}\n\nexport function tunnelEventRecord(event: TunnelEvent): TinybirdTunnelRecord {\n return {\n ...event,\n request_id: event.request_id || \"\",\n event_id: event.request_id || randomUUID(),\n timestamp: tinybirdTimestamp(event.timestamp),\n ingested_at: tinybirdTimestamp(Date.now()),\n retention_days: normalizeRetentionDays(event.retention_days),\n };\n}\n\nexport function requestCaptureRecord(capture: RequestCapture): TinybirdTunnelRecord {\n return {\n ...capture,\n timestamp: tinybirdTimestamp(capture.timestamp),\n ingested_at: tinybirdTimestamp(Date.now()),\n retention_days: normalizeRetentionDays(capture.retention_days),\n request_headers: JSON.stringify(capture.request_headers),\n response_headers: JSON.stringify(capture.response_headers),\n };\n}\n\nexport function protocolEventRecord(event: ProtocolEvent): TinybirdTunnelRecord {\n return {\n ...event,\n event_id: randomUUID(),\n timestamp: tinybirdTimestamp(event.timestamp),\n ingested_at: tinybirdTimestamp(Date.now()),\n retention_days: normalizeRetentionDays(event.retention_days),\n };\n}\n\nexport class TunnelTinybirdClient {\n constructor(private readonly host: string, private readonly token: string) {}\n\n get configured(): boolean {\n if (!this.host || !this.token) return false;\n try {\n const url = new URL(this.host);\n return (url.protocol === \"https:\" ||\n (url.protocol === \"http:\" && [\"localhost\", \"127.0.0.1\", \"[::1]\"].includes(url.hostname))) &&\n !url.username && !url.password && !url.search && !url.hash && url.pathname === \"/\";\n } catch {\n return false;\n }\n }\n\n async append(datasource: string, records: TinybirdTunnelRecord[]): Promise<void> {\n if (datasource === \"tunnel_request_captures\") {\n const oversized = records.filter((record) => Buffer.byteLength(JSON.stringify(record)) > 3 * 1024 * 1024).length;\n if (oversized) console.error(`Tinybird capture delivery rejected ${oversized} oversized records; other captures will continue`);\n records = records.filter((record) => captureRecordIsCurrent(record) && Buffer.byteLength(JSON.stringify(record)) <= 3 * 1024 * 1024);\n }\n if (!records.length) return;\n if (!this.configured) throw new Error(\"Tinybird tunnel ingestion is not configured\");\n // Keep requests below the Events API payload ceiling, even when captures\n // contain large base64 request/response bodies. Stable ids make retrying an\n // already accepted prefix safe when a later request fails.\n const maxBytes = 4 * 1024 * 1024;\n let lines: string[] = [];\n let bytes = 0;\n for (const record of records) {\n const line = JSON.stringify(record);\n const lineBytes = Buffer.byteLength(line) + 1;\n if (lineBytes > maxBytes) {\n throw new Error(`Tinybird ${datasource} record exceeds the ingestion size limit`);\n }\n if (bytes + lineBytes > maxBytes && lines.length) {\n await this.appendBatch(datasource, lines);\n lines = [];\n bytes = 0;\n }\n lines.push(line);\n bytes += lineBytes;\n }\n if (lines.length) await this.appendBatch(datasource, lines);\n }\n\n private async appendBatch(datasource: string, lines: string[]): Promise<void> {\n const response = await fetch(\n `${this.host.replace(/\\/$/, \"\")}/v0/events?name=${encodeURIComponent(datasource)}&wait=true`,\n {\n method: \"POST\",\n headers: {\n Authorization: `Bearer ${this.token}`,\n \"Content-Type\": \"application/x-ndjson\",\n },\n body: lines.join(\"\\n\"),\n signal: AbortSignal.timeout(15_000),\n },\n );\n // Error bodies can echo capture values. Never include them in logs/errors.\n if (!response.ok) {\n await response.body?.cancel();\n throw new Error(`Tinybird ${datasource} ingestion failed (${response.status})`);\n }\n let result: { successful_rows?: number; quarantined_rows?: number };\n try {\n const candidate: unknown = await response.json();\n if (!candidate || typeof candidate !== \"object\" || Array.isArray(candidate)) {\n throw new Error(\"invalid acknowledgement\");\n }\n result = candidate as typeof result;\n } catch {\n throw new Error(`Tinybird ${datasource} ingestion returned an invalid acknowledgement`);\n }\n if (Number(result.quarantined_rows) > 0 || Number(result.successful_rows) !== lines.length) {\n throw new Error(`Tinybird ${datasource} ingestion did not acknowledge the complete batch`);\n }\n }\n}\n\n/** Backlogs must never republish captures past their individual plan expiry. */\nexport function captureRecordIsCurrent(record: TinybirdTunnelRecord, now = Date.now()): boolean {\n if (typeof record.timestamp !== \"string\") return false;\n const timestamp = Date.parse(`${record.timestamp.replace(\" \", \"T\").replace(/Z$/, \"\")}Z`);\n const days = normalizeRetentionDays(Number(record.retention_days));\n return Number.isFinite(timestamp) && timestamp + days * 24 * 60 * 60 * 1000 > now;\n}\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@outray/runtime-shared",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Server primitives and tunnel contracts shared by OutRay services",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -64,6 +64,16 @@
64
64
  "types": "./dist/tunnel-accounting.d.ts",
65
65
  "import": "./dist/tunnel-accounting.js",
66
66
  "require": "./dist/tunnel-accounting.cjs"
67
+ },
68
+ "./buffered-logger": {
69
+ "types": "./dist/buffered-logger.d.ts",
70
+ "import": "./dist/buffered-logger.js",
71
+ "require": "./dist/buffered-logger.cjs"
72
+ },
73
+ "./tinybird-records": {
74
+ "types": "./dist/tinybird-records.d.ts",
75
+ "import": "./dist/tinybird-records.js",
76
+ "require": "./dist/tinybird-records.cjs"
67
77
  }
68
78
  },
69
79
  "engines": {