@infinitetoken/lumber-kit 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,180 @@
1
+ {
2
+ "$comment": "Known answers every Lumber plugin is tested against, whatever its language. Lumber-Kit's own tests check each one against its TypeScript (buildLogPayload, splitApiKey, logDeliveryOutcome, logRetryDelayMs, the constants) and Node's crypto, so these can't drift from the kit. An absent key means absent: JSON has no undefined, and null data values are left out like missing ones.",
3
+ "constants": {
4
+ "defaultEndpoint": "https://api.lumber.infinitetoken.com",
5
+ "ingestPath": "/logs",
6
+ "keyVersion": 1,
7
+ "keyBytes": 32,
8
+ "nonceBytes": 12,
9
+ "tagBytes": 16,
10
+ "levels": { "debug": 0, "info": 1, "warn": 2, "error": 3, "fatal": 4 },
11
+ "delivery": { "maxAttempts": 5, "baseDelayMs": 1000, "maxDelayMs": 30000, "pauseMs": 3600000 }
12
+ },
13
+ "apiKeys": [
14
+ {
15
+ "name": "token and encryption key",
16
+ "apiKey": "3f9c0e7a1b2d4c6e8f0a1b2c3d4e5f60.AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=",
17
+ "token": "3f9c0e7a1b2d4c6e8f0a1b2c3d4e5f60",
18
+ "encryptionKey": "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8="
19
+ },
20
+ {
21
+ "name": "surrounding whitespace, as pasted from a terminal",
22
+ "apiKey": " 3f9c0e7a1b2d4c6e8f0a1b2c3d4e5f60.AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=\n",
23
+ "token": "3f9c0e7a1b2d4c6e8f0a1b2c3d4e5f60",
24
+ "encryptionKey": "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8="
25
+ },
26
+ {
27
+ "name": "a bare token carries no encryption key",
28
+ "apiKey": "3f9c0e7a1b2d4c6e8f0a1b2c3d4e5f60",
29
+ "token": "3f9c0e7a1b2d4c6e8f0a1b2c3d4e5f60",
30
+ "encryptionKey": null
31
+ },
32
+ {
33
+ "name": "empty",
34
+ "apiKey": "",
35
+ "token": "",
36
+ "encryptionKey": null
37
+ }
38
+ ],
39
+ "encryption": [
40
+ {
41
+ "name": "ascii",
42
+ "key": "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=",
43
+ "nonce": "oKGio6Slpqeoqaqr",
44
+ "plaintext": "{\"level\":1,\"message\":\"Server started\",\"metadata\":{\"port\":3000}}",
45
+ "payload": "GJU0Lu4J1IGhob9mA32TqJ06EEgzrm6dWFSr8Wofs60RyzwyqJURCe54Q/Rf2AFgoAIim40OcVA66GWsaA7i231gZDgNom5ce207bpQN+A=="
46
+ },
47
+ {
48
+ "name": "non-ascii, encoded as utf-8",
49
+ "key": "QkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkI=",
50
+ "nonce": "BwcHBwcHBwcHBwcH",
51
+ "plaintext": "{\"level\":3,\"message\":\"Paiement échoué ☕ 🚨\",\"context\":{\"pid\":4242,\"hostname\":\"web-1\"},\"metadata\":{\"user\":\"Zoë\"}}",
52
+ "payload": "bG1l/6IGl9vfE0wA1rhuEAPAMcnDlB7ZGWfAErA+VQ+UPWofx/cYJAkRUjZ0WTi3hotyvwFw3G2lbtCoKgD6/aRDnUpDt4kk+HF3FGk6VaEEt5fqiu28C5u9EifuYjd4Qh8KcSMdO9eeVWLqsg2eIpbV9qQQOtD1fMXktGfQmxTjmMtMZuKm"
53
+ },
54
+ {
55
+ "name": "empty message",
56
+ "key": "7qi7Ixae6epI48zxshkBw43xktBTaE/pwLxUoUhZ6e0=",
57
+ "nonce": "eDd7UldXtJRCf4kB",
58
+ "plaintext": "{\"level\":0,\"message\":\"\"}",
59
+ "payload": "M2E8vR0ITv6RfAwC3fDiMVIfWt3N9U6DEveRBpqxc/K49Py/HVQpMw=="
60
+ }
61
+ ],
62
+ "payloads": [
63
+ {
64
+ "name": "message alone",
65
+ "level": 1,
66
+ "message": "Server started",
67
+ "payload": { "level": 1, "message": "Server started" }
68
+ },
69
+ {
70
+ "name": "request and trace ids",
71
+ "level": 3,
72
+ "message": "Payment failed",
73
+ "data": { "requestId": "req-123", "traceId": "trace-abc" },
74
+ "payload": { "level": 3, "message": "Payment failed", "requestId": "req-123", "traceId": "trace-abc" }
75
+ },
76
+ {
77
+ "name": "other keys become metadata",
78
+ "level": 1,
79
+ "message": "User action",
80
+ "data": { "userId": "123", "action": "login", "order": { "id": 7, "total": 12.5, "items": ["a", "b"] } },
81
+ "payload": { "level": 1, "message": "User action", "metadata": { "userId": "123", "action": "login", "order": { "id": 7, "total": 12.5, "items": ["a", "b"] } } }
82
+ },
83
+ {
84
+ "name": "explicit metadata wins over other keys",
85
+ "level": 2,
86
+ "message": "Rate limit approaching",
87
+ "data": { "version": 1, "region": "us", "metadata": { "version": 2, "build": "b7" } },
88
+ "payload": { "level": 2, "message": "Rate limit approaching", "metadata": { "version": 2, "region": "us", "build": "b7" } }
89
+ },
90
+ {
91
+ "name": "metadata alone",
92
+ "level": 1,
93
+ "message": "Cache warmed",
94
+ "data": { "metadata": { "entries": 512 } },
95
+ "payload": { "level": 1, "message": "Cache warmed", "metadata": { "entries": 512 } }
96
+ },
97
+ {
98
+ "name": "automatic context alone",
99
+ "level": 0,
100
+ "message": "Cache miss",
101
+ "autoContext": { "pid": 4242, "hostname": "web-1" },
102
+ "payload": { "level": 0, "message": "Cache miss", "context": { "pid": 4242, "hostname": "web-1" } }
103
+ },
104
+ {
105
+ "name": "the call's context over the automatic context",
106
+ "level": 1,
107
+ "message": "Job done",
108
+ "autoContext": { "pid": 4242, "hostname": "web-1", "env": "production" },
109
+ "data": { "context": { "hostname": "worker-3", "code": "jobs/billing.py", "line": 42 } },
110
+ "payload": { "level": 1, "message": "Job done", "context": { "pid": 4242, "hostname": "worker-3", "env": "production", "code": "jobs/billing.py", "line": 42 } }
111
+ },
112
+ {
113
+ "name": "context with no automatic context",
114
+ "level": 1,
115
+ "message": "Job done",
116
+ "autoContext": {},
117
+ "data": { "context": { "code": "app.ts" } },
118
+ "payload": { "level": 1, "message": "Job done", "context": { "code": "app.ts" } }
119
+ },
120
+ {
121
+ "name": "a context that isn't an object is a metadata key",
122
+ "level": 1,
123
+ "message": "Invoice sent",
124
+ "data": { "context": "billing" },
125
+ "payload": { "level": 1, "message": "Invoice sent", "metadata": { "context": "billing" } }
126
+ },
127
+ {
128
+ "name": "metadata that isn't an object is a metadata key",
129
+ "level": 1,
130
+ "message": "Batch",
131
+ "data": { "metadata": [1, 2, 3] },
132
+ "payload": { "level": 1, "message": "Batch", "metadata": { "metadata": [1, 2, 3] } }
133
+ },
134
+ {
135
+ "name": "a null context and metadata are left out",
136
+ "level": 1,
137
+ "message": "Nothing extra",
138
+ "data": { "context": null, "metadata": null },
139
+ "payload": { "level": 1, "message": "Nothing extra" }
140
+ },
141
+ {
142
+ "name": "a level key in the data is metadata, never the log's level",
143
+ "level": 3,
144
+ "message": "Disk nearly full",
145
+ "data": { "level": "high", "message": "ignored" },
146
+ "payload": { "level": 3, "message": "Disk nearly full", "metadata": { "level": "high", "message": "ignored" } }
147
+ },
148
+ {
149
+ "name": "everything",
150
+ "level": 4,
151
+ "message": "DB connection lost ☠",
152
+ "autoContext": { "pid": 1, "hostname": "db-proxy" },
153
+ "data": { "requestId": "r1", "traceId": "t1", "context": { "method": "connect" }, "metadata": { "retries": 3 }, "host": "10.0.0.5" },
154
+ "payload": { "level": 4, "message": "DB connection lost ☠", "requestId": "r1", "traceId": "t1", "context": { "pid": 1, "hostname": "db-proxy", "method": "connect" }, "metadata": { "host": "10.0.0.5", "retries": 3 } }
155
+ }
156
+ ],
157
+ "deliveryOutcomes": [
158
+ { "status": 200, "outcome": "sent" },
159
+ { "status": 201, "outcome": "sent" },
160
+ { "status": 301, "outcome": "retry" },
161
+ { "status": 400, "outcome": "drop" },
162
+ { "status": 401, "outcome": "drop" },
163
+ { "status": 403, "outcome": "drop" },
164
+ { "status": 404, "outcome": "drop" },
165
+ { "status": 413, "outcome": "drop" },
166
+ { "status": 429, "outcome": "pause" },
167
+ { "status": 500, "outcome": "retry" },
168
+ { "status": 502, "outcome": "retry" },
169
+ { "status": 503, "outcome": "retry" },
170
+ { "status": null, "outcome": "retry" }
171
+ ],
172
+ "retryDelaysMs": [
173
+ { "failedAttempts": 1, "delayMs": 1000 },
174
+ { "failedAttempts": 2, "delayMs": 2000 },
175
+ { "failedAttempts": 3, "delayMs": 4000 },
176
+ { "failedAttempts": 4, "delayMs": 8000 },
177
+ { "failedAttempts": 5, "delayMs": 16000 },
178
+ { "failedAttempts": 6, "delayMs": 30000 }
179
+ ]
180
+ }
package/dist/index.d.mts CHANGED
@@ -15,6 +15,23 @@ declare const APP_COLORS: readonly ["#EF4444", "#F97316", "#F59E0B", "#84CC16",
15
15
  declare const apiErrorCodes: readonly ["APP_LIMIT", "SEAT_LIMIT", "GUEST_LIMIT", "ALREADY_MEMBER", "ALREADY_READS", "ALREADY_INVITED", "HAS_ACCOUNT"];
16
16
  type ApiErrorCode = (typeof apiErrorCodes)[number];
17
17
 
18
+ /** The Lumber API a plugin sends logs to unless it's given another endpoint. */
19
+ declare const DEFAULT_API_ENDPOINT = "https://api.lumber.infinitetoken.com";
20
+ /** LogEncrypted's keyVersion. Every app has one encryption key, so it's 1 until keys can be rotated. */
21
+ declare const LOG_KEY_VERSION = 1;
22
+ /**
23
+ * How a plugin delivers logs: one POST /logs per log, in the order they were logged. A network error or a 5xx is a
24
+ * failed attempt, tried again after `baseDelayMs`, doubling up to `maxDelayMs`, and dropped after `maxAttempts`. Any
25
+ * other 4xx is dropped at once, since sending it again won't help. A 429 (the team's logs for the month are used up)
26
+ * drops what's queued and every new log until its Retry-After, or for `pauseMs` when it has none.
27
+ */
28
+ declare const logDelivery: {
29
+ readonly maxAttempts: 5;
30
+ readonly baseDelayMs: 1000;
31
+ readonly maxDelayMs: 30000;
32
+ readonly pauseMs: number;
33
+ };
34
+
18
35
  /** Log severity level. Numeric values match the existing Lumber-API level field. */
19
36
  declare const enum LogLevel {
20
37
  Debug = 0,
@@ -423,7 +440,7 @@ interface LogEncrypted {
423
440
  keyVersion: number;
424
441
  }
425
442
  /**
426
- * Plaintext shape of log content — encrypted before sending, never stored plaintext on server.
443
+ * Plaintext shape of log content: a plugin encrypts it into LogEncrypted, and the server never sees it.
427
444
  */
428
445
  interface LogPayload {
429
446
  level: LogLevel;
@@ -433,6 +450,25 @@ interface LogPayload {
433
450
  context?: LogContext;
434
451
  metadata?: Record<string, unknown>;
435
452
  }
453
+ /**
454
+ * What a plugin's log call takes beside its message (Lumber-Node's second argument). requestId and traceId become the
455
+ * payload's own fields, context is merged over the plugin's automatic context, and any other key goes into metadata,
456
+ * where an explicit metadata key wins. A context or metadata that isn't an object is kept as an ordinary metadata key.
457
+ */
458
+ interface LogData {
459
+ requestId?: string;
460
+ traceId?: string;
461
+ context?: LogContext;
462
+ metadata?: Record<string, unknown>;
463
+ [key: string]: unknown;
464
+ }
465
+ /** POST /logs' body. The level stays plaintext for filtering and pushes; everything else is in `encrypted`. */
466
+ interface LogIngestBody {
467
+ level: LogLevel;
468
+ encrypted: LogEncrypted;
469
+ }
470
+ /** What a plugin does with a log after POST /logs answers (see logDelivery). */
471
+ type LogDeliveryOutcome = 'sent' | 'retry' | 'drop' | 'pause';
436
472
  /**
437
473
  * API response shape for a stored log.
438
474
  * level is plaintext for server-side notification routing; all content is inside encrypted.
@@ -674,6 +710,22 @@ interface UserRecord {
674
710
  notesReadAt?: Record<string, string>;
675
711
  }
676
712
 
713
+ /**
714
+ * The plaintext a plugin encrypts for one log call: its level and message, requestId and traceId when given, the
715
+ * plugin's automatic context (pid, hostname and the like) with the call's context over it, and the call's other keys
716
+ * as metadata with its explicit metadata over them. A context or metadata that isn't an object (a string, an array) is
717
+ * kept as an ordinary metadata key, since spreading it would turn `{ context: 'billing' }` into `{ 0: 'b', ... }`, and
718
+ * a null one is left out. Every plugin builds the same payload: contract/vectors.json pins it for the other languages.
719
+ */
720
+ declare const buildLogPayload: (level: LogLevel, message: string, data?: LogData, autoContext?: Record<string, unknown>) => LogPayload;
721
+ /**
722
+ * What to do with a log once POST /logs answers with `status`, or null for no answer (a network error): sent on a 2xx,
723
+ * pause on a 429, drop on any other 4xx, and retry on anything else.
724
+ */
725
+ declare const logDeliveryOutcome: (status: number | null) => LogDeliveryOutcome;
726
+ /** How long to wait before trying a log again after its `failedAttempts`th failure: 1s, 2s, 4s and so on, up to 30s. */
727
+ declare const logRetryDelayMs: (failedAttempts: number) => number;
728
+
677
729
  declare const isValidAppearance: (value: unknown) => value is Appearance;
678
730
  /** Accepts 3- and 6-digit hex color strings (#rgb, #rrggbb). */
679
731
  declare const isValidColor: (value: string) => boolean;
@@ -714,4 +766,4 @@ declare const apiErrorCodeOf: (body: unknown) => ApiErrorCode | null;
714
766
  /** A push notification's data as PushData, or null for data no Lumber push sends. */
715
767
  declare const parsePushData: (data: unknown) => PushData | null;
716
768
 
717
- export { APP_COLORS, type ApiErrorBody, type ApiErrorCode, type ApiKeyRecord, type ApiResponse, type AppCreatedRecord, type AppEncryptedKey, type AppKeyUpsertBody, type AppPermission, type AppPersonRecord, type AppRecord, type AppTeamAccess, type Appearance, type AppsQuery, type ClaimantKeyCheck, type ClientToServerEvents, DEFAULT_ACCENT, DEFAULT_PUSH_THRESHOLD, type DeviceLinkApproveBody, type DeviceLinkCreateBody, type DeviceLinkCreatedRecord, type DeviceLinkPlatform, type DeviceLinkRecord, type DeviceLinkStatus, type FlagRecord, type FlagsQuery, type GuestRecord, type HolderKeyCheck, type InvitationRecord, type KeyCheckResult, type KeyHolder, type KeyPin, type LogContext, type LogEncrypted, LogLevel, type LogLevelName, type LogPayload, type LogRecord, type LogTimeseriesQuery, type LogsQuery, type NoteEncrypted, type NoteRecord, type NotesQuery, type NotificationPreference, type NotificationPreferences, PUSH_OFF, type PendingAppKey, type PendingDeviceLink, type PushData, type PushThreshold, type PushToken, type PushType, type ResponseMetadata, type ServerToClientEvents, type SocketAppPayload, SubscriptionLevel, type SubscriptionLevelName, type SubscriptionLimits, type SubscriptionPlan, type SubscriptionPlanRecord, type TeamMemberRecord, type TeamRecord, type TeamsQuery, type TimeseriesBucket, type TimeseriesResponse, type UserRecord, apiErrorCodeOf, apiErrorCodes, isApiErrorCode, isValidAppearance, isValidColor, isValidDeviceLinkStatus, isValidImageUrl, isValidLogLevel, isValidSubscriptionLevel, isValidTeamMemberRole, joinApiKey, logLevelColors, logLevelNameToLevel, logLevelToName, logLevels, nameColor, normalizePushThreshold, parsePushData, personColor, resolveAppColor, splitApiKey, subscriptionLevelToName, subscriptionLevels, subscriptionLimits, subscriptionPlans };
769
+ export { APP_COLORS, type ApiErrorBody, type ApiErrorCode, type ApiKeyRecord, type ApiResponse, type AppCreatedRecord, type AppEncryptedKey, type AppKeyUpsertBody, type AppPermission, type AppPersonRecord, type AppRecord, type AppTeamAccess, type Appearance, type AppsQuery, type ClaimantKeyCheck, type ClientToServerEvents, DEFAULT_ACCENT, DEFAULT_API_ENDPOINT, DEFAULT_PUSH_THRESHOLD, type DeviceLinkApproveBody, type DeviceLinkCreateBody, type DeviceLinkCreatedRecord, type DeviceLinkPlatform, type DeviceLinkRecord, type DeviceLinkStatus, type FlagRecord, type FlagsQuery, type GuestRecord, type HolderKeyCheck, type InvitationRecord, type KeyCheckResult, type KeyHolder, type KeyPin, LOG_KEY_VERSION, type LogContext, type LogData, type LogDeliveryOutcome, type LogEncrypted, type LogIngestBody, LogLevel, type LogLevelName, type LogPayload, type LogRecord, type LogTimeseriesQuery, type LogsQuery, type NoteEncrypted, type NoteRecord, type NotesQuery, type NotificationPreference, type NotificationPreferences, PUSH_OFF, type PendingAppKey, type PendingDeviceLink, type PushData, type PushThreshold, type PushToken, type PushType, type ResponseMetadata, type ServerToClientEvents, type SocketAppPayload, SubscriptionLevel, type SubscriptionLevelName, type SubscriptionLimits, type SubscriptionPlan, type SubscriptionPlanRecord, type TeamMemberRecord, type TeamRecord, type TeamsQuery, type TimeseriesBucket, type TimeseriesResponse, type UserRecord, apiErrorCodeOf, apiErrorCodes, buildLogPayload, isApiErrorCode, isValidAppearance, isValidColor, isValidDeviceLinkStatus, isValidImageUrl, isValidLogLevel, isValidSubscriptionLevel, isValidTeamMemberRole, joinApiKey, logDelivery, logDeliveryOutcome, logLevelColors, logLevelNameToLevel, logLevelToName, logLevels, logRetryDelayMs, nameColor, normalizePushThreshold, parsePushData, personColor, resolveAppColor, splitApiKey, subscriptionLevelToName, subscriptionLevels, subscriptionLimits, subscriptionPlans };
package/dist/index.d.ts CHANGED
@@ -15,6 +15,23 @@ declare const APP_COLORS: readonly ["#EF4444", "#F97316", "#F59E0B", "#84CC16",
15
15
  declare const apiErrorCodes: readonly ["APP_LIMIT", "SEAT_LIMIT", "GUEST_LIMIT", "ALREADY_MEMBER", "ALREADY_READS", "ALREADY_INVITED", "HAS_ACCOUNT"];
16
16
  type ApiErrorCode = (typeof apiErrorCodes)[number];
17
17
 
18
+ /** The Lumber API a plugin sends logs to unless it's given another endpoint. */
19
+ declare const DEFAULT_API_ENDPOINT = "https://api.lumber.infinitetoken.com";
20
+ /** LogEncrypted's keyVersion. Every app has one encryption key, so it's 1 until keys can be rotated. */
21
+ declare const LOG_KEY_VERSION = 1;
22
+ /**
23
+ * How a plugin delivers logs: one POST /logs per log, in the order they were logged. A network error or a 5xx is a
24
+ * failed attempt, tried again after `baseDelayMs`, doubling up to `maxDelayMs`, and dropped after `maxAttempts`. Any
25
+ * other 4xx is dropped at once, since sending it again won't help. A 429 (the team's logs for the month are used up)
26
+ * drops what's queued and every new log until its Retry-After, or for `pauseMs` when it has none.
27
+ */
28
+ declare const logDelivery: {
29
+ readonly maxAttempts: 5;
30
+ readonly baseDelayMs: 1000;
31
+ readonly maxDelayMs: 30000;
32
+ readonly pauseMs: number;
33
+ };
34
+
18
35
  /** Log severity level. Numeric values match the existing Lumber-API level field. */
19
36
  declare const enum LogLevel {
20
37
  Debug = 0,
@@ -423,7 +440,7 @@ interface LogEncrypted {
423
440
  keyVersion: number;
424
441
  }
425
442
  /**
426
- * Plaintext shape of log content — encrypted before sending, never stored plaintext on server.
443
+ * Plaintext shape of log content: a plugin encrypts it into LogEncrypted, and the server never sees it.
427
444
  */
428
445
  interface LogPayload {
429
446
  level: LogLevel;
@@ -433,6 +450,25 @@ interface LogPayload {
433
450
  context?: LogContext;
434
451
  metadata?: Record<string, unknown>;
435
452
  }
453
+ /**
454
+ * What a plugin's log call takes beside its message (Lumber-Node's second argument). requestId and traceId become the
455
+ * payload's own fields, context is merged over the plugin's automatic context, and any other key goes into metadata,
456
+ * where an explicit metadata key wins. A context or metadata that isn't an object is kept as an ordinary metadata key.
457
+ */
458
+ interface LogData {
459
+ requestId?: string;
460
+ traceId?: string;
461
+ context?: LogContext;
462
+ metadata?: Record<string, unknown>;
463
+ [key: string]: unknown;
464
+ }
465
+ /** POST /logs' body. The level stays plaintext for filtering and pushes; everything else is in `encrypted`. */
466
+ interface LogIngestBody {
467
+ level: LogLevel;
468
+ encrypted: LogEncrypted;
469
+ }
470
+ /** What a plugin does with a log after POST /logs answers (see logDelivery). */
471
+ type LogDeliveryOutcome = 'sent' | 'retry' | 'drop' | 'pause';
436
472
  /**
437
473
  * API response shape for a stored log.
438
474
  * level is plaintext for server-side notification routing; all content is inside encrypted.
@@ -674,6 +710,22 @@ interface UserRecord {
674
710
  notesReadAt?: Record<string, string>;
675
711
  }
676
712
 
713
+ /**
714
+ * The plaintext a plugin encrypts for one log call: its level and message, requestId and traceId when given, the
715
+ * plugin's automatic context (pid, hostname and the like) with the call's context over it, and the call's other keys
716
+ * as metadata with its explicit metadata over them. A context or metadata that isn't an object (a string, an array) is
717
+ * kept as an ordinary metadata key, since spreading it would turn `{ context: 'billing' }` into `{ 0: 'b', ... }`, and
718
+ * a null one is left out. Every plugin builds the same payload: contract/vectors.json pins it for the other languages.
719
+ */
720
+ declare const buildLogPayload: (level: LogLevel, message: string, data?: LogData, autoContext?: Record<string, unknown>) => LogPayload;
721
+ /**
722
+ * What to do with a log once POST /logs answers with `status`, or null for no answer (a network error): sent on a 2xx,
723
+ * pause on a 429, drop on any other 4xx, and retry on anything else.
724
+ */
725
+ declare const logDeliveryOutcome: (status: number | null) => LogDeliveryOutcome;
726
+ /** How long to wait before trying a log again after its `failedAttempts`th failure: 1s, 2s, 4s and so on, up to 30s. */
727
+ declare const logRetryDelayMs: (failedAttempts: number) => number;
728
+
677
729
  declare const isValidAppearance: (value: unknown) => value is Appearance;
678
730
  /** Accepts 3- and 6-digit hex color strings (#rgb, #rrggbb). */
679
731
  declare const isValidColor: (value: string) => boolean;
@@ -714,4 +766,4 @@ declare const apiErrorCodeOf: (body: unknown) => ApiErrorCode | null;
714
766
  /** A push notification's data as PushData, or null for data no Lumber push sends. */
715
767
  declare const parsePushData: (data: unknown) => PushData | null;
716
768
 
717
- export { APP_COLORS, type ApiErrorBody, type ApiErrorCode, type ApiKeyRecord, type ApiResponse, type AppCreatedRecord, type AppEncryptedKey, type AppKeyUpsertBody, type AppPermission, type AppPersonRecord, type AppRecord, type AppTeamAccess, type Appearance, type AppsQuery, type ClaimantKeyCheck, type ClientToServerEvents, DEFAULT_ACCENT, DEFAULT_PUSH_THRESHOLD, type DeviceLinkApproveBody, type DeviceLinkCreateBody, type DeviceLinkCreatedRecord, type DeviceLinkPlatform, type DeviceLinkRecord, type DeviceLinkStatus, type FlagRecord, type FlagsQuery, type GuestRecord, type HolderKeyCheck, type InvitationRecord, type KeyCheckResult, type KeyHolder, type KeyPin, type LogContext, type LogEncrypted, LogLevel, type LogLevelName, type LogPayload, type LogRecord, type LogTimeseriesQuery, type LogsQuery, type NoteEncrypted, type NoteRecord, type NotesQuery, type NotificationPreference, type NotificationPreferences, PUSH_OFF, type PendingAppKey, type PendingDeviceLink, type PushData, type PushThreshold, type PushToken, type PushType, type ResponseMetadata, type ServerToClientEvents, type SocketAppPayload, SubscriptionLevel, type SubscriptionLevelName, type SubscriptionLimits, type SubscriptionPlan, type SubscriptionPlanRecord, type TeamMemberRecord, type TeamRecord, type TeamsQuery, type TimeseriesBucket, type TimeseriesResponse, type UserRecord, apiErrorCodeOf, apiErrorCodes, isApiErrorCode, isValidAppearance, isValidColor, isValidDeviceLinkStatus, isValidImageUrl, isValidLogLevel, isValidSubscriptionLevel, isValidTeamMemberRole, joinApiKey, logLevelColors, logLevelNameToLevel, logLevelToName, logLevels, nameColor, normalizePushThreshold, parsePushData, personColor, resolveAppColor, splitApiKey, subscriptionLevelToName, subscriptionLevels, subscriptionLimits, subscriptionPlans };
769
+ export { APP_COLORS, type ApiErrorBody, type ApiErrorCode, type ApiKeyRecord, type ApiResponse, type AppCreatedRecord, type AppEncryptedKey, type AppKeyUpsertBody, type AppPermission, type AppPersonRecord, type AppRecord, type AppTeamAccess, type Appearance, type AppsQuery, type ClaimantKeyCheck, type ClientToServerEvents, DEFAULT_ACCENT, DEFAULT_API_ENDPOINT, DEFAULT_PUSH_THRESHOLD, type DeviceLinkApproveBody, type DeviceLinkCreateBody, type DeviceLinkCreatedRecord, type DeviceLinkPlatform, type DeviceLinkRecord, type DeviceLinkStatus, type FlagRecord, type FlagsQuery, type GuestRecord, type HolderKeyCheck, type InvitationRecord, type KeyCheckResult, type KeyHolder, type KeyPin, LOG_KEY_VERSION, type LogContext, type LogData, type LogDeliveryOutcome, type LogEncrypted, type LogIngestBody, LogLevel, type LogLevelName, type LogPayload, type LogRecord, type LogTimeseriesQuery, type LogsQuery, type NoteEncrypted, type NoteRecord, type NotesQuery, type NotificationPreference, type NotificationPreferences, PUSH_OFF, type PendingAppKey, type PendingDeviceLink, type PushData, type PushThreshold, type PushToken, type PushType, type ResponseMetadata, type ServerToClientEvents, type SocketAppPayload, SubscriptionLevel, type SubscriptionLevelName, type SubscriptionLimits, type SubscriptionPlan, type SubscriptionPlanRecord, type TeamMemberRecord, type TeamRecord, type TeamsQuery, type TimeseriesBucket, type TimeseriesResponse, type UserRecord, apiErrorCodeOf, apiErrorCodes, buildLogPayload, isApiErrorCode, isValidAppearance, isValidColor, isValidDeviceLinkStatus, isValidImageUrl, isValidLogLevel, isValidSubscriptionLevel, isValidTeamMemberRole, joinApiKey, logDelivery, logDeliveryOutcome, logLevelColors, logLevelNameToLevel, logLevelToName, logLevels, logRetryDelayMs, nameColor, normalizePushThreshold, parsePushData, personColor, resolveAppColor, splitApiKey, subscriptionLevelToName, subscriptionLevels, subscriptionLimits, subscriptionPlans };
package/dist/index.js CHANGED
@@ -22,12 +22,15 @@ var index_exports = {};
22
22
  __export(index_exports, {
23
23
  APP_COLORS: () => APP_COLORS,
24
24
  DEFAULT_ACCENT: () => DEFAULT_ACCENT,
25
+ DEFAULT_API_ENDPOINT: () => DEFAULT_API_ENDPOINT,
25
26
  DEFAULT_PUSH_THRESHOLD: () => DEFAULT_PUSH_THRESHOLD,
27
+ LOG_KEY_VERSION: () => LOG_KEY_VERSION,
26
28
  LogLevel: () => LogLevel,
27
29
  PUSH_OFF: () => PUSH_OFF,
28
30
  SubscriptionLevel: () => SubscriptionLevel,
29
31
  apiErrorCodeOf: () => apiErrorCodeOf,
30
32
  apiErrorCodes: () => apiErrorCodes,
33
+ buildLogPayload: () => buildLogPayload,
31
34
  isApiErrorCode: () => isApiErrorCode,
32
35
  isValidAppearance: () => isValidAppearance,
33
36
  isValidColor: () => isValidColor,
@@ -37,10 +40,13 @@ __export(index_exports, {
37
40
  isValidSubscriptionLevel: () => isValidSubscriptionLevel,
38
41
  isValidTeamMemberRole: () => isValidTeamMemberRole,
39
42
  joinApiKey: () => joinApiKey,
43
+ logDelivery: () => logDelivery,
44
+ logDeliveryOutcome: () => logDeliveryOutcome,
40
45
  logLevelColors: () => logLevelColors,
41
46
  logLevelNameToLevel: () => logLevelNameToLevel,
42
47
  logLevelToName: () => logLevelToName,
43
48
  logLevels: () => logLevels,
49
+ logRetryDelayMs: () => logRetryDelayMs,
44
50
  nameColor: () => nameColor,
45
51
  normalizePushThreshold: () => normalizePushThreshold,
46
52
  parsePushData: () => parsePushData,
@@ -55,13 +61,13 @@ __export(index_exports, {
55
61
  module.exports = __toCommonJS(index_exports);
56
62
 
57
63
  // src/constants/logLevel.ts
58
- var LogLevel = /* @__PURE__ */ ((LogLevel2) => {
59
- LogLevel2[LogLevel2["Debug"] = 0] = "Debug";
60
- LogLevel2[LogLevel2["Info"] = 1] = "Info";
61
- LogLevel2[LogLevel2["Warn"] = 2] = "Warn";
62
- LogLevel2[LogLevel2["Error"] = 3] = "Error";
63
- LogLevel2[LogLevel2["Fatal"] = 4] = "Fatal";
64
- return LogLevel2;
64
+ var LogLevel = /* @__PURE__ */ ((LogLevel3) => {
65
+ LogLevel3[LogLevel3["Debug"] = 0] = "Debug";
66
+ LogLevel3[LogLevel3["Info"] = 1] = "Info";
67
+ LogLevel3[LogLevel3["Warn"] = 2] = "Warn";
68
+ LogLevel3[LogLevel3["Error"] = 3] = "Error";
69
+ LogLevel3[LogLevel3["Fatal"] = 4] = "Fatal";
70
+ return LogLevel3;
65
71
  })(LogLevel || {});
66
72
  var logLevelToName = {
67
73
  [0 /* Debug */]: "debug",
@@ -108,6 +114,16 @@ var apiErrorCodes = [
108
114
  "HAS_ACCOUNT"
109
115
  ];
110
116
 
117
+ // src/constants/ingest.ts
118
+ var DEFAULT_API_ENDPOINT = "https://api.lumber.infinitetoken.com";
119
+ var LOG_KEY_VERSION = 1;
120
+ var logDelivery = {
121
+ maxAttempts: 5,
122
+ baseDelayMs: 1e3,
123
+ maxDelayMs: 3e4,
124
+ pauseMs: 60 * 60 * 1e3
125
+ };
126
+
111
127
  // src/constants/notifications.ts
112
128
  var PUSH_OFF = 5;
113
129
  var DEFAULT_PUSH_THRESHOLD = 2 /* Warn */;
@@ -140,6 +156,32 @@ var subscriptionPlans = {
140
156
  [3 /* Enterprise */]: { name: "Enterprise", monthlyPrice: null }
141
157
  };
142
158
 
159
+ // src/utils/ingest.ts
160
+ var isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
161
+ var buildLogPayload = (level, message, data, autoContext) => {
162
+ const { requestId, traceId, context: givenContext, metadata: givenMetadata, ...rest } = data ?? {};
163
+ const context = isRecord(givenContext) ? givenContext : void 0;
164
+ const metadata = isRecord(givenMetadata) ? givenMetadata : void 0;
165
+ if (givenContext != null && !context) rest.context = givenContext;
166
+ if (givenMetadata != null && !metadata) rest.metadata = givenMetadata;
167
+ const mergedMetadata = Object.keys(rest).length > 0 ? { ...rest, ...metadata ?? {} } : metadata;
168
+ const mergedContext = autoContext && Object.keys(autoContext).length > 0 ? { ...autoContext, ...context ?? {} } : context;
169
+ const payload = { level, message };
170
+ if (requestId !== void 0) payload.requestId = requestId;
171
+ if (traceId !== void 0) payload.traceId = traceId;
172
+ if (mergedContext !== void 0) payload.context = mergedContext;
173
+ if (mergedMetadata !== void 0) payload.metadata = mergedMetadata;
174
+ return payload;
175
+ };
176
+ var logDeliveryOutcome = (status) => {
177
+ if (status === null) return "retry";
178
+ if (status >= 200 && status < 300) return "sent";
179
+ if (status === 429) return "pause";
180
+ if (status >= 400 && status < 500) return "drop";
181
+ return "retry";
182
+ };
183
+ var logRetryDelayMs = (failedAttempts) => Math.min(logDelivery.baseDelayMs * 2 ** Math.max(0, failedAttempts - 1), logDelivery.maxDelayMs);
184
+
143
185
  // src/utils/index.ts
144
186
  var appearanceValues = ["system", "dark", "light"];
145
187
  var isValidAppearance = (value) => appearanceValues.includes(value);
@@ -215,12 +257,15 @@ var parsePushData = (data) => {
215
257
  0 && (module.exports = {
216
258
  APP_COLORS,
217
259
  DEFAULT_ACCENT,
260
+ DEFAULT_API_ENDPOINT,
218
261
  DEFAULT_PUSH_THRESHOLD,
262
+ LOG_KEY_VERSION,
219
263
  LogLevel,
220
264
  PUSH_OFF,
221
265
  SubscriptionLevel,
222
266
  apiErrorCodeOf,
223
267
  apiErrorCodes,
268
+ buildLogPayload,
224
269
  isApiErrorCode,
225
270
  isValidAppearance,
226
271
  isValidColor,
@@ -230,10 +275,13 @@ var parsePushData = (data) => {
230
275
  isValidSubscriptionLevel,
231
276
  isValidTeamMemberRole,
232
277
  joinApiKey,
278
+ logDelivery,
279
+ logDeliveryOutcome,
233
280
  logLevelColors,
234
281
  logLevelNameToLevel,
235
282
  logLevelToName,
236
283
  logLevels,
284
+ logRetryDelayMs,
237
285
  nameColor,
238
286
  normalizePushThreshold,
239
287
  parsePushData,
package/dist/index.mjs CHANGED
@@ -1,11 +1,11 @@
1
1
  // src/constants/logLevel.ts
2
- var LogLevel = /* @__PURE__ */ ((LogLevel2) => {
3
- LogLevel2[LogLevel2["Debug"] = 0] = "Debug";
4
- LogLevel2[LogLevel2["Info"] = 1] = "Info";
5
- LogLevel2[LogLevel2["Warn"] = 2] = "Warn";
6
- LogLevel2[LogLevel2["Error"] = 3] = "Error";
7
- LogLevel2[LogLevel2["Fatal"] = 4] = "Fatal";
8
- return LogLevel2;
2
+ var LogLevel = /* @__PURE__ */ ((LogLevel3) => {
3
+ LogLevel3[LogLevel3["Debug"] = 0] = "Debug";
4
+ LogLevel3[LogLevel3["Info"] = 1] = "Info";
5
+ LogLevel3[LogLevel3["Warn"] = 2] = "Warn";
6
+ LogLevel3[LogLevel3["Error"] = 3] = "Error";
7
+ LogLevel3[LogLevel3["Fatal"] = 4] = "Fatal";
8
+ return LogLevel3;
9
9
  })(LogLevel || {});
10
10
  var logLevelToName = {
11
11
  [0 /* Debug */]: "debug",
@@ -52,6 +52,16 @@ var apiErrorCodes = [
52
52
  "HAS_ACCOUNT"
53
53
  ];
54
54
 
55
+ // src/constants/ingest.ts
56
+ var DEFAULT_API_ENDPOINT = "https://api.lumber.infinitetoken.com";
57
+ var LOG_KEY_VERSION = 1;
58
+ var logDelivery = {
59
+ maxAttempts: 5,
60
+ baseDelayMs: 1e3,
61
+ maxDelayMs: 3e4,
62
+ pauseMs: 60 * 60 * 1e3
63
+ };
64
+
55
65
  // src/constants/notifications.ts
56
66
  var PUSH_OFF = 5;
57
67
  var DEFAULT_PUSH_THRESHOLD = 2 /* Warn */;
@@ -84,6 +94,32 @@ var subscriptionPlans = {
84
94
  [3 /* Enterprise */]: { name: "Enterprise", monthlyPrice: null }
85
95
  };
86
96
 
97
+ // src/utils/ingest.ts
98
+ var isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
99
+ var buildLogPayload = (level, message, data, autoContext) => {
100
+ const { requestId, traceId, context: givenContext, metadata: givenMetadata, ...rest } = data ?? {};
101
+ const context = isRecord(givenContext) ? givenContext : void 0;
102
+ const metadata = isRecord(givenMetadata) ? givenMetadata : void 0;
103
+ if (givenContext != null && !context) rest.context = givenContext;
104
+ if (givenMetadata != null && !metadata) rest.metadata = givenMetadata;
105
+ const mergedMetadata = Object.keys(rest).length > 0 ? { ...rest, ...metadata ?? {} } : metadata;
106
+ const mergedContext = autoContext && Object.keys(autoContext).length > 0 ? { ...autoContext, ...context ?? {} } : context;
107
+ const payload = { level, message };
108
+ if (requestId !== void 0) payload.requestId = requestId;
109
+ if (traceId !== void 0) payload.traceId = traceId;
110
+ if (mergedContext !== void 0) payload.context = mergedContext;
111
+ if (mergedMetadata !== void 0) payload.metadata = mergedMetadata;
112
+ return payload;
113
+ };
114
+ var logDeliveryOutcome = (status) => {
115
+ if (status === null) return "retry";
116
+ if (status >= 200 && status < 300) return "sent";
117
+ if (status === 429) return "pause";
118
+ if (status >= 400 && status < 500) return "drop";
119
+ return "retry";
120
+ };
121
+ var logRetryDelayMs = (failedAttempts) => Math.min(logDelivery.baseDelayMs * 2 ** Math.max(0, failedAttempts - 1), logDelivery.maxDelayMs);
122
+
87
123
  // src/utils/index.ts
88
124
  var appearanceValues = ["system", "dark", "light"];
89
125
  var isValidAppearance = (value) => appearanceValues.includes(value);
@@ -158,12 +194,15 @@ var parsePushData = (data) => {
158
194
  export {
159
195
  APP_COLORS,
160
196
  DEFAULT_ACCENT,
197
+ DEFAULT_API_ENDPOINT,
161
198
  DEFAULT_PUSH_THRESHOLD,
199
+ LOG_KEY_VERSION,
162
200
  LogLevel,
163
201
  PUSH_OFF,
164
202
  SubscriptionLevel,
165
203
  apiErrorCodeOf,
166
204
  apiErrorCodes,
205
+ buildLogPayload,
167
206
  isApiErrorCode,
168
207
  isValidAppearance,
169
208
  isValidColor,
@@ -173,10 +212,13 @@ export {
173
212
  isValidSubscriptionLevel,
174
213
  isValidTeamMemberRole,
175
214
  joinApiKey,
215
+ logDelivery,
216
+ logDeliveryOutcome,
176
217
  logLevelColors,
177
218
  logLevelNameToLevel,
178
219
  logLevelToName,
179
220
  logLevels,
221
+ logRetryDelayMs,
180
222
  nameColor,
181
223
  normalizePushThreshold,
182
224
  parsePushData,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@infinitetoken/lumber-kit",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "description": "Shared TypeScript types and wire format contracts for the Lumber cloud logging suite",
5
5
  "keywords": [
6
6
  "lumber",
@@ -31,17 +31,20 @@
31
31
  "types": "./dist/index.d.ts",
32
32
  "default": "./dist/index.js"
33
33
  }
34
- }
34
+ },
35
+ "./contract/*": "./contract/*"
35
36
  },
36
37
  "main": "./dist/index.js",
37
38
  "module": "./dist/index.mjs",
38
39
  "types": "./dist/index.d.ts",
39
40
  "files": [
40
- "dist"
41
+ "dist",
42
+ "contract"
41
43
  ],
42
44
  "scripts": {
43
45
  "build": "tsup",
44
46
  "clean": "rm -rf dist coverage",
47
+ "contract": "node scripts/contract.cjs",
45
48
  "fix": "eslint . --fix",
46
49
  "lint": "eslint .",
47
50
  "prepublishOnly": "npm run build",
@@ -61,9 +64,11 @@
61
64
  "@infinitetoken/tsconfig": "^0.6.0",
62
65
  "@types/jest": "^30.0.0",
63
66
  "@types/node": "^25.0.0",
67
+ "ajv": "^8.20.0",
64
68
  "eslint": "^10.2.1",
65
69
  "jest": "^30.4.2",
66
70
  "prettier": "^3.8.3",
71
+ "ts-json-schema-generator": "^2.9.0",
67
72
  "tsup": "^8.5.1",
68
73
  "typescript": "^6.0.3"
69
74
  },