@infinitetoken/lumber-kit 0.5.2 → 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.
- package/README.md +26 -5
- package/contract/README.md +21 -0
- package/contract/schema.json +2273 -0
- package/contract/vectors.json +180 -0
- package/dist/index.d.mts +114 -5
- package/dist/index.d.ts +114 -5
- package/dist/index.js +58 -9
- package/dist/index.mjs +52 -9
- package/package.json +8 -3
|
@@ -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,
|
|
@@ -222,7 +239,24 @@ interface AppsQuery {
|
|
|
222
239
|
cursor?: string;
|
|
223
240
|
}
|
|
224
241
|
|
|
225
|
-
|
|
242
|
+
/**
|
|
243
|
+
* A device link request: a new device asking for the account key from a device that has it. `failed` is a code that
|
|
244
|
+
* didn't match, which ends the request.
|
|
245
|
+
*/
|
|
246
|
+
type DeviceLinkStatus = 'pending' | 'approved' | 'denied' | 'failed';
|
|
247
|
+
/** What a request comes from, so the device answering it can say so and offer a QR code only to a phone's camera. */
|
|
248
|
+
type DeviceLinkPlatform = 'ios' | 'android' | 'web' | 'desktop' | 'cli';
|
|
249
|
+
/**
|
|
250
|
+
* Body of POST /device-links.
|
|
251
|
+
*/
|
|
252
|
+
interface DeviceLinkCreateBody {
|
|
253
|
+
deviceName: string;
|
|
254
|
+
platform: DeviceLinkPlatform;
|
|
255
|
+
/** The public half of the new device's one-off X25519 keypair, which the account key is wrapped to. */
|
|
256
|
+
publicKey: string;
|
|
257
|
+
/** The new device's push device id, so the push asking the account's devices to link it skips it. */
|
|
258
|
+
deviceId?: string;
|
|
259
|
+
}
|
|
226
260
|
/**
|
|
227
261
|
* Response from POST /device-links.
|
|
228
262
|
*/
|
|
@@ -231,14 +265,48 @@ interface DeviceLinkCreatedRecord {
|
|
|
231
265
|
expiresAt: string;
|
|
232
266
|
}
|
|
233
267
|
/**
|
|
234
|
-
*
|
|
235
|
-
*
|
|
268
|
+
* Body of POST /device-links/:id/approve: the account key wrapped to the new device, and a tag over it made from the
|
|
269
|
+
* code's secret, which the new device checks before storing the key, so the server can't hand it a key of its own.
|
|
270
|
+
*/
|
|
271
|
+
interface DeviceLinkApproveBody {
|
|
272
|
+
encryptedKey: string;
|
|
273
|
+
deliveryTag: string;
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* Response from GET /device-links/:id, as the new device reads it.
|
|
277
|
+
* encryptedKey and deliveryTag are present only when status is 'approved'. sid and holderMessage are set once a device
|
|
278
|
+
* holding the account key shows the request's code, and answeredAt once the new device has answered it.
|
|
236
279
|
*/
|
|
237
280
|
interface DeviceLinkRecord {
|
|
238
281
|
id: string;
|
|
239
282
|
status: DeviceLinkStatus;
|
|
240
283
|
encryptedKey?: string;
|
|
284
|
+
deliveryTag?: string;
|
|
241
285
|
expiresAt: string;
|
|
286
|
+
sid?: string | null;
|
|
287
|
+
holderMessage?: string | null;
|
|
288
|
+
answeredAt?: string | null;
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* A request waiting on the caller's account key (GET /device-links/pending), as a device holding it reads it: enough
|
|
292
|
+
* to show its code, and to finish it once answered. The messages are the same password-authenticated exchange as a key
|
|
293
|
+
* check's, so the server can't read or test codes against them. holderState is the code's side of the exchange, sealed
|
|
294
|
+
* under the account key, so any of the account's devices can show the same code or finish the request.
|
|
295
|
+
*/
|
|
296
|
+
interface PendingDeviceLink {
|
|
297
|
+
id: string;
|
|
298
|
+
deviceName: string;
|
|
299
|
+
platform: DeviceLinkPlatform;
|
|
300
|
+
/** The new device's one-off public key, which an answer's tag has to cover before anything is wrapped to it. */
|
|
301
|
+
publicKey: string;
|
|
302
|
+
sid: string | null;
|
|
303
|
+
holderMessage: string | null;
|
|
304
|
+
holderState: string | null;
|
|
305
|
+
claimantMessage: string | null;
|
|
306
|
+
tag: string | null;
|
|
307
|
+
answeredAt: string | null;
|
|
308
|
+
expiresAt: string;
|
|
309
|
+
createdAt: string;
|
|
242
310
|
}
|
|
243
311
|
|
|
244
312
|
/**
|
|
@@ -372,7 +440,7 @@ interface LogEncrypted {
|
|
|
372
440
|
keyVersion: number;
|
|
373
441
|
}
|
|
374
442
|
/**
|
|
375
|
-
* Plaintext shape of log content
|
|
443
|
+
* Plaintext shape of log content: a plugin encrypts it into LogEncrypted, and the server never sees it.
|
|
376
444
|
*/
|
|
377
445
|
interface LogPayload {
|
|
378
446
|
level: LogLevel;
|
|
@@ -382,6 +450,25 @@ interface LogPayload {
|
|
|
382
450
|
context?: LogContext;
|
|
383
451
|
metadata?: Record<string, unknown>;
|
|
384
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';
|
|
385
472
|
/**
|
|
386
473
|
* API response shape for a stored log.
|
|
387
474
|
* level is plaintext for server-side notification routing; all content is inside encrypted.
|
|
@@ -471,6 +558,12 @@ type PushData =
|
|
|
471
558
|
deviceLinkId: string;
|
|
472
559
|
deviceName: string;
|
|
473
560
|
}
|
|
561
|
+
/** The new device entered its link code, which a device holding the account key now checks (Lumber-API). */
|
|
562
|
+
| {
|
|
563
|
+
type: 'device_link_answered';
|
|
564
|
+
deviceLinkId: string;
|
|
565
|
+
deviceName: string;
|
|
566
|
+
}
|
|
474
567
|
/** The person the reader gave a code entered it (Lumber-API). */
|
|
475
568
|
| {
|
|
476
569
|
type: 'key_check';
|
|
@@ -617,6 +710,22 @@ interface UserRecord {
|
|
|
617
710
|
notesReadAt?: Record<string, string>;
|
|
618
711
|
}
|
|
619
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
|
+
|
|
620
729
|
declare const isValidAppearance: (value: unknown) => value is Appearance;
|
|
621
730
|
/** Accepts 3- and 6-digit hex color strings (#rgb, #rrggbb). */
|
|
622
731
|
declare const isValidColor: (value: string) => boolean;
|
|
@@ -657,4 +766,4 @@ declare const apiErrorCodeOf: (body: unknown) => ApiErrorCode | null;
|
|
|
657
766
|
/** A push notification's data as PushData, or null for data no Lumber push sends. */
|
|
658
767
|
declare const parsePushData: (data: unknown) => PushData | null;
|
|
659
768
|
|
|
660
|
-
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 DeviceLinkCreatedRecord, 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 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,
|
|
@@ -222,7 +239,24 @@ interface AppsQuery {
|
|
|
222
239
|
cursor?: string;
|
|
223
240
|
}
|
|
224
241
|
|
|
225
|
-
|
|
242
|
+
/**
|
|
243
|
+
* A device link request: a new device asking for the account key from a device that has it. `failed` is a code that
|
|
244
|
+
* didn't match, which ends the request.
|
|
245
|
+
*/
|
|
246
|
+
type DeviceLinkStatus = 'pending' | 'approved' | 'denied' | 'failed';
|
|
247
|
+
/** What a request comes from, so the device answering it can say so and offer a QR code only to a phone's camera. */
|
|
248
|
+
type DeviceLinkPlatform = 'ios' | 'android' | 'web' | 'desktop' | 'cli';
|
|
249
|
+
/**
|
|
250
|
+
* Body of POST /device-links.
|
|
251
|
+
*/
|
|
252
|
+
interface DeviceLinkCreateBody {
|
|
253
|
+
deviceName: string;
|
|
254
|
+
platform: DeviceLinkPlatform;
|
|
255
|
+
/** The public half of the new device's one-off X25519 keypair, which the account key is wrapped to. */
|
|
256
|
+
publicKey: string;
|
|
257
|
+
/** The new device's push device id, so the push asking the account's devices to link it skips it. */
|
|
258
|
+
deviceId?: string;
|
|
259
|
+
}
|
|
226
260
|
/**
|
|
227
261
|
* Response from POST /device-links.
|
|
228
262
|
*/
|
|
@@ -231,14 +265,48 @@ interface DeviceLinkCreatedRecord {
|
|
|
231
265
|
expiresAt: string;
|
|
232
266
|
}
|
|
233
267
|
/**
|
|
234
|
-
*
|
|
235
|
-
*
|
|
268
|
+
* Body of POST /device-links/:id/approve: the account key wrapped to the new device, and a tag over it made from the
|
|
269
|
+
* code's secret, which the new device checks before storing the key, so the server can't hand it a key of its own.
|
|
270
|
+
*/
|
|
271
|
+
interface DeviceLinkApproveBody {
|
|
272
|
+
encryptedKey: string;
|
|
273
|
+
deliveryTag: string;
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* Response from GET /device-links/:id, as the new device reads it.
|
|
277
|
+
* encryptedKey and deliveryTag are present only when status is 'approved'. sid and holderMessage are set once a device
|
|
278
|
+
* holding the account key shows the request's code, and answeredAt once the new device has answered it.
|
|
236
279
|
*/
|
|
237
280
|
interface DeviceLinkRecord {
|
|
238
281
|
id: string;
|
|
239
282
|
status: DeviceLinkStatus;
|
|
240
283
|
encryptedKey?: string;
|
|
284
|
+
deliveryTag?: string;
|
|
241
285
|
expiresAt: string;
|
|
286
|
+
sid?: string | null;
|
|
287
|
+
holderMessage?: string | null;
|
|
288
|
+
answeredAt?: string | null;
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* A request waiting on the caller's account key (GET /device-links/pending), as a device holding it reads it: enough
|
|
292
|
+
* to show its code, and to finish it once answered. The messages are the same password-authenticated exchange as a key
|
|
293
|
+
* check's, so the server can't read or test codes against them. holderState is the code's side of the exchange, sealed
|
|
294
|
+
* under the account key, so any of the account's devices can show the same code or finish the request.
|
|
295
|
+
*/
|
|
296
|
+
interface PendingDeviceLink {
|
|
297
|
+
id: string;
|
|
298
|
+
deviceName: string;
|
|
299
|
+
platform: DeviceLinkPlatform;
|
|
300
|
+
/** The new device's one-off public key, which an answer's tag has to cover before anything is wrapped to it. */
|
|
301
|
+
publicKey: string;
|
|
302
|
+
sid: string | null;
|
|
303
|
+
holderMessage: string | null;
|
|
304
|
+
holderState: string | null;
|
|
305
|
+
claimantMessage: string | null;
|
|
306
|
+
tag: string | null;
|
|
307
|
+
answeredAt: string | null;
|
|
308
|
+
expiresAt: string;
|
|
309
|
+
createdAt: string;
|
|
242
310
|
}
|
|
243
311
|
|
|
244
312
|
/**
|
|
@@ -372,7 +440,7 @@ interface LogEncrypted {
|
|
|
372
440
|
keyVersion: number;
|
|
373
441
|
}
|
|
374
442
|
/**
|
|
375
|
-
* Plaintext shape of log content
|
|
443
|
+
* Plaintext shape of log content: a plugin encrypts it into LogEncrypted, and the server never sees it.
|
|
376
444
|
*/
|
|
377
445
|
interface LogPayload {
|
|
378
446
|
level: LogLevel;
|
|
@@ -382,6 +450,25 @@ interface LogPayload {
|
|
|
382
450
|
context?: LogContext;
|
|
383
451
|
metadata?: Record<string, unknown>;
|
|
384
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';
|
|
385
472
|
/**
|
|
386
473
|
* API response shape for a stored log.
|
|
387
474
|
* level is plaintext for server-side notification routing; all content is inside encrypted.
|
|
@@ -471,6 +558,12 @@ type PushData =
|
|
|
471
558
|
deviceLinkId: string;
|
|
472
559
|
deviceName: string;
|
|
473
560
|
}
|
|
561
|
+
/** The new device entered its link code, which a device holding the account key now checks (Lumber-API). */
|
|
562
|
+
| {
|
|
563
|
+
type: 'device_link_answered';
|
|
564
|
+
deviceLinkId: string;
|
|
565
|
+
deviceName: string;
|
|
566
|
+
}
|
|
474
567
|
/** The person the reader gave a code entered it (Lumber-API). */
|
|
475
568
|
| {
|
|
476
569
|
type: 'key_check';
|
|
@@ -617,6 +710,22 @@ interface UserRecord {
|
|
|
617
710
|
notesReadAt?: Record<string, string>;
|
|
618
711
|
}
|
|
619
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
|
+
|
|
620
729
|
declare const isValidAppearance: (value: unknown) => value is Appearance;
|
|
621
730
|
/** Accepts 3- and 6-digit hex color strings (#rgb, #rrggbb). */
|
|
622
731
|
declare const isValidColor: (value: string) => boolean;
|
|
@@ -657,4 +766,4 @@ declare const apiErrorCodeOf: (body: unknown) => ApiErrorCode | null;
|
|
|
657
766
|
/** A push notification's data as PushData, or null for data no Lumber push sends. */
|
|
658
767
|
declare const parsePushData: (data: unknown) => PushData | null;
|
|
659
768
|
|
|
660
|
-
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 DeviceLinkCreatedRecord, 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 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 };
|