@infinitetoken/lumber-kit 0.4.22 → 0.5.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 +58 -1
- package/dist/index.d.mts +310 -30
- package/dist/index.d.ts +310 -30
- package/dist/index.js +97 -9
- package/dist/index.mjs +86 -8
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -36,6 +36,40 @@ import { LogLevel, LogRecord, LogEncrypted, LogPayload, LogsQuery } from '@infin
|
|
|
36
36
|
| `subscriptionLimits` | object | Per-tier limits (apps, seats, apiKeys, logRetentionDays, etc.) |
|
|
37
37
|
| `isValidSubscriptionLevel` | function | Type guard for `SubscriptionLevel` values |
|
|
38
38
|
|
|
39
|
+
### Plans
|
|
40
|
+
|
|
41
|
+
| Export | Kind | Purpose |
|
|
42
|
+
|--------|------|---------|
|
|
43
|
+
| `subscriptionPlans` | object | Per-tier display name and monthly price in US dollars (0 free, `null` priced per customer) |
|
|
44
|
+
| `SubscriptionPlanRecord` | interface | A plan GET /subscriptions offers, with its Stripe price id |
|
|
45
|
+
|
|
46
|
+
### Notifications
|
|
47
|
+
|
|
48
|
+
| Export | Kind | Purpose |
|
|
49
|
+
|--------|------|---------|
|
|
50
|
+
| `PUSH_OFF` | constant | A push threshold one past Fatal (5): log pushes off |
|
|
51
|
+
| `PushThreshold` | type | A `LogLevel` or `PUSH_OFF`, stored as a preference's `minimumLevel` |
|
|
52
|
+
| `normalizePushThreshold` | function | A threshold from any stored or sent value: past Fatal is off, not a number is Warn |
|
|
53
|
+
| `PushData` | type | The `data` each push type carries (`log`, `note`, `device_link`, `key_check`, `key_waiting`, `keys_shared`) |
|
|
54
|
+
| `parsePushData` | function | A push's data as `PushData`, or `null` for data no Lumber push sends |
|
|
55
|
+
|
|
56
|
+
### Socket events
|
|
57
|
+
|
|
58
|
+
| Export | Kind | Purpose |
|
|
59
|
+
|--------|------|---------|
|
|
60
|
+
| `ServerToClientEvents` | interface | Every event Lumber-Socket sends, for socket.io's typed `Server` and `Socket` |
|
|
61
|
+
| `ClientToServerEvents` | interface | Every event a client sends Lumber-Socket |
|
|
62
|
+
| `SocketAppPayload` | type | An app as the socket sends it (no wrapped keys) |
|
|
63
|
+
|
|
64
|
+
### Errors
|
|
65
|
+
|
|
66
|
+
| Export | Kind | Purpose |
|
|
67
|
+
|--------|------|---------|
|
|
68
|
+
| `ApiErrorCode` | type | Why the API refused, where the status alone doesn't say (`SEAT_LIMIT`, `ALREADY_MEMBER`, ...) |
|
|
69
|
+
| `apiErrorCodes` | array | Every `ApiErrorCode` |
|
|
70
|
+
| `ApiErrorBody` | interface | An error response's body: `{ message, code? }` |
|
|
71
|
+
| `apiErrorCodeOf` | function | The code in an error body, or `null` |
|
|
72
|
+
|
|
39
73
|
### Flag types
|
|
40
74
|
|
|
41
75
|
| Export | Kind | Purpose |
|
|
@@ -51,7 +85,30 @@ import { LogLevel, LogRecord, LogEncrypted, LogPayload, LogsQuery } from '@infin
|
|
|
51
85
|
| `AppCreatedRecord` | interface | POST /apps 201 response (includes `encryptedKey` once) |
|
|
52
86
|
| `UserRecord` | interface | API response shape for a user |
|
|
53
87
|
| `TeamRecord` | interface | API response shape for a team |
|
|
54
|
-
| `TeamMemberRecord` | interface | Team membership record |
|
|
88
|
+
| `TeamMemberRecord` | interface | Team membership record (listed members carry name, handle, color and picture) |
|
|
89
|
+
| `AppTeamAccess` | type | Who on an app's team reads it: `'everyone'` or `'owner'` |
|
|
90
|
+
| `AppPermission` | type | `'read' \| 'write' \| 'manage'` on an app |
|
|
91
|
+
| `AppPersonRecord` | interface | Someone who can read an app, as GET /apps/:id/people shows them |
|
|
92
|
+
| `InvitationRecord` | interface | An invitation to an address with no account, to a team or an app |
|
|
93
|
+
|
|
94
|
+
### Key sharing
|
|
95
|
+
|
|
96
|
+
| Export | Kind | Purpose |
|
|
97
|
+
|--------|------|---------|
|
|
98
|
+
| `PendingAppKey` | interface | Someone with access to an app and no wrapped copy of its key |
|
|
99
|
+
| `KeyHolder` | interface | Someone who already holds a wrapped copy |
|
|
100
|
+
| `AppKeyUpsertBody` | interface | Body of PUT /apps/:id/keys/:userId |
|
|
101
|
+
| `KeyPin` | interface | An account key a user accepted for someone, with its MAC |
|
|
102
|
+
| `HolderKeyCheck`, `ClaimantKeyCheck`, `KeyCheckResult` | interfaces | A key check as the holder and the person being confirmed read it |
|
|
103
|
+
|
|
104
|
+
### API keys
|
|
105
|
+
|
|
106
|
+
| Export | Kind | Purpose |
|
|
107
|
+
|--------|------|---------|
|
|
108
|
+
| `joinApiKey` | function | Joins an app's API token and its encryption key as `<token>.<encryptionKey>`, the one secret a plugin is configured with |
|
|
109
|
+
| `splitApiKey` | function | Splits an API key into `{ token, encryptionKey? }`; a value with no dot is all token |
|
|
110
|
+
|
|
111
|
+
A plugin sends only the token to the API (`Authorization: Bearer <token>`) and encrypts with the rest, so the encryption key never leaves it.
|
|
55
112
|
|
|
56
113
|
## Wire format
|
|
57
114
|
|
package/dist/index.d.mts
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Machine-readable reasons the API refuses a request, sent as `code` beside `message` where one status means more than
|
|
3
|
+
* one thing. Messages are for people and may change; clients decide by the code.
|
|
4
|
+
*/
|
|
5
|
+
declare const apiErrorCodes: readonly ["APP_LIMIT", "SEAT_LIMIT", "GUEST_LIMIT", "ALREADY_MEMBER", "ALREADY_READS", "ALREADY_INVITED", "HAS_ACCOUNT"];
|
|
6
|
+
type ApiErrorCode = (typeof apiErrorCodes)[number];
|
|
7
|
+
|
|
1
8
|
/** Log severity level. Numeric values match the existing Lumber-API level field. */
|
|
2
9
|
declare const enum LogLevel {
|
|
3
10
|
Debug = 0,
|
|
@@ -15,6 +22,13 @@ declare const logLevelNameToLevel: Record<LogLevelName, LogLevel>;
|
|
|
15
22
|
/** All valid log levels, ordered from least to most severe. */
|
|
16
23
|
declare const logLevels: readonly LogLevel[];
|
|
17
24
|
|
|
25
|
+
/** A push threshold one past Fatal, which no log reaches: log pushes are off (the account's, or one app's). */
|
|
26
|
+
declare const PUSH_OFF = 5;
|
|
27
|
+
/** The lowest log level that pushes, or PUSH_OFF. Stored as a notification preference's `minimumLevel`. */
|
|
28
|
+
type PushThreshold = LogLevel | typeof PUSH_OFF;
|
|
29
|
+
/** The threshold a user gets when none is set. */
|
|
30
|
+
declare const DEFAULT_PUSH_THRESHOLD: PushThreshold;
|
|
31
|
+
|
|
18
32
|
/** User subscription level. Mirrors User.subscriptionLevel in Lumber-API. */
|
|
19
33
|
declare const enum SubscriptionLevel {
|
|
20
34
|
Hobby = 0,
|
|
@@ -41,6 +55,70 @@ type SubscriptionLimits = {
|
|
|
41
55
|
};
|
|
42
56
|
/** Per-level resource limits. Use Infinity to mean unlimited or disabled. */
|
|
43
57
|
declare const subscriptionLimits: Record<SubscriptionLevel, SubscriptionLimits>;
|
|
58
|
+
type SubscriptionPlan = {
|
|
59
|
+
/** Display name, as every surface shows it. */
|
|
60
|
+
name: string;
|
|
61
|
+
/** Monthly price in US dollars: 0 for free, null when priced per customer. */
|
|
62
|
+
monthlyPrice: number | null;
|
|
63
|
+
};
|
|
64
|
+
/** Each level's name and price. Stripe holds the prices themselves; these must match what it charges. */
|
|
65
|
+
declare const subscriptionPlans: Record<SubscriptionLevel, SubscriptionPlan>;
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* An organization that owns apps and pays for member usage.
|
|
69
|
+
* Team subscriptionLevel takes precedence over individual member levels.
|
|
70
|
+
*/
|
|
71
|
+
interface TeamRecord {
|
|
72
|
+
id: string;
|
|
73
|
+
createdAt: string;
|
|
74
|
+
updatedAt: string;
|
|
75
|
+
name: string;
|
|
76
|
+
color?: string;
|
|
77
|
+
imageUrl?: string;
|
|
78
|
+
subscriptionLevel: SubscriptionLevel;
|
|
79
|
+
/** When a canceled plan stops; null while it renews. */
|
|
80
|
+
subscriptionEnd?: string | null;
|
|
81
|
+
/** GET /teams only: the caller's role on the team. */
|
|
82
|
+
role?: TeamMemberRecord['role'];
|
|
83
|
+
/** GET /teams only: the caller's membership id, which leaving the team deletes. */
|
|
84
|
+
memberId?: string;
|
|
85
|
+
/** GET /teams only: the team's apps, as creating one counts them. Null unless the caller is its owner or an admin. */
|
|
86
|
+
appCount?: number | null;
|
|
87
|
+
/** GET /teams only: logs its apps sent this UTC month, the monthly cap's count. Null unless owner or admin. */
|
|
88
|
+
logsThisMonth?: number | null;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Membership of a user in a team.
|
|
92
|
+
*/
|
|
93
|
+
interface TeamMemberRecord {
|
|
94
|
+
id: string;
|
|
95
|
+
createdAt: string;
|
|
96
|
+
updatedAt: string;
|
|
97
|
+
teamId: string;
|
|
98
|
+
userId: string;
|
|
99
|
+
role: 'owner' | 'admin' | 'member';
|
|
100
|
+
/** Listed members only: their display name. Never their email address. */
|
|
101
|
+
name?: string | null;
|
|
102
|
+
/** Listed members only: the part of their email before the @. */
|
|
103
|
+
handle?: string | null;
|
|
104
|
+
color?: string | null;
|
|
105
|
+
imageUrl?: string | null;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* A plan GET /subscriptions offers, with its Stripe price (none for Hobby).
|
|
109
|
+
*/
|
|
110
|
+
interface SubscriptionPlanRecord {
|
|
111
|
+
name: string;
|
|
112
|
+
priceId: string | null;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Filter params for GET /teams.
|
|
116
|
+
*/
|
|
117
|
+
interface TeamsQuery {
|
|
118
|
+
sort?: string;
|
|
119
|
+
limit?: number;
|
|
120
|
+
cursor?: string;
|
|
121
|
+
}
|
|
44
122
|
|
|
45
123
|
/**
|
|
46
124
|
* Wrapped application encryption key for a specific user.
|
|
@@ -50,9 +128,14 @@ interface AppEncryptedKey {
|
|
|
50
128
|
encryptedKey: string;
|
|
51
129
|
keyVersion: number;
|
|
52
130
|
}
|
|
131
|
+
/**
|
|
132
|
+
* Who on an app's team can read it: everyone on the team, or only its owner. Its guests read it either way.
|
|
133
|
+
*/
|
|
134
|
+
type AppTeamAccess = 'everyone' | 'owner';
|
|
53
135
|
/**
|
|
54
136
|
* API response shape for a listed or fetched app.
|
|
55
137
|
* encryptedKey is the requesting user's wrapped copy of the app key, or null if none exists.
|
|
138
|
+
* The optional fields are missing from an API deployed before them, which clients read as the default.
|
|
56
139
|
*/
|
|
57
140
|
interface AppRecord {
|
|
58
141
|
id: string;
|
|
@@ -63,6 +146,12 @@ interface AppRecord {
|
|
|
63
146
|
imageUrl?: string;
|
|
64
147
|
teamId?: string;
|
|
65
148
|
encryptedKey: string | null;
|
|
149
|
+
/** Missing means 'everyone'. */
|
|
150
|
+
teamAccess?: AppTeamAccess;
|
|
151
|
+
/** How many days of its logs the app's team's plan keeps, null for no limit (and when missing). */
|
|
152
|
+
retentionDays?: number | null;
|
|
153
|
+
/** While the team has sent its plan's monthly logs, when new ones are taken again (the next UTC month); else null. */
|
|
154
|
+
logsCappedUntil?: string | null;
|
|
66
155
|
}
|
|
67
156
|
/**
|
|
68
157
|
* App creation response.
|
|
@@ -92,6 +181,25 @@ interface GuestRecord {
|
|
|
92
181
|
userId: string;
|
|
93
182
|
role: 'owner' | 'admin' | 'read' | 'write';
|
|
94
183
|
}
|
|
184
|
+
/**
|
|
185
|
+
* What someone may do on an app. manage: the team's owner or admins, or a guest invited as owner or admin. write: a
|
|
186
|
+
* team member or a write guest. read: any other access.
|
|
187
|
+
*/
|
|
188
|
+
type AppPermission = 'read' | 'write' | 'manage';
|
|
189
|
+
/**
|
|
190
|
+
* Someone who can read an app, as the others see them (GET /apps/:id/people): never their email address.
|
|
191
|
+
*/
|
|
192
|
+
interface AppPersonRecord {
|
|
193
|
+
userId: string;
|
|
194
|
+
name: string | null;
|
|
195
|
+
/** The part of their email before the @, for anyone who hasn't set a name. */
|
|
196
|
+
handle?: string | null;
|
|
197
|
+
color: string | null;
|
|
198
|
+
/** Their picture URL, or null for their initial. */
|
|
199
|
+
imageUrl?: string | null;
|
|
200
|
+
role: TeamMemberRecord['role'] | GuestRecord['role'];
|
|
201
|
+
kind: 'team' | 'guest';
|
|
202
|
+
}
|
|
95
203
|
/**
|
|
96
204
|
* Filter params for GET /apps.
|
|
97
205
|
*/
|
|
@@ -123,6 +231,14 @@ interface DeviceLinkRecord {
|
|
|
123
231
|
expiresAt: string;
|
|
124
232
|
}
|
|
125
233
|
|
|
234
|
+
/**
|
|
235
|
+
* The body of an API error response. `code` is there where the status alone doesn't say what went wrong.
|
|
236
|
+
*/
|
|
237
|
+
interface ApiErrorBody {
|
|
238
|
+
message: string;
|
|
239
|
+
code?: ApiErrorCode;
|
|
240
|
+
}
|
|
241
|
+
|
|
126
242
|
interface FlagRecord {
|
|
127
243
|
id: string;
|
|
128
244
|
createdAt: string;
|
|
@@ -137,6 +253,92 @@ interface FlagsQuery {
|
|
|
137
253
|
cursor?: string;
|
|
138
254
|
}
|
|
139
255
|
|
|
256
|
+
/**
|
|
257
|
+
* An invitation to an address with no account yet: to a team (teamId, as a member or admin) or to one app as its guest
|
|
258
|
+
* (appId). The address joins when it signs up and confirms itself. A live one (unexpired) holds its seat.
|
|
259
|
+
*/
|
|
260
|
+
interface InvitationRecord {
|
|
261
|
+
id: string;
|
|
262
|
+
teamId: string | null;
|
|
263
|
+
appId: string | null;
|
|
264
|
+
email: string;
|
|
265
|
+
role: string;
|
|
266
|
+
/** Null when its email couldn't be sent. */
|
|
267
|
+
sentAt: string | null;
|
|
268
|
+
expiresAt: string;
|
|
269
|
+
createdAt: string;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Someone with access to an app who has no wrapped copy of its key yet (GET /apps/keys/pending), with when they last
|
|
274
|
+
* replaced their account key: null if never, missing from an API before key pins.
|
|
275
|
+
*/
|
|
276
|
+
interface PendingAppKey {
|
|
277
|
+
appId: string;
|
|
278
|
+
userId: string;
|
|
279
|
+
publicKey: string;
|
|
280
|
+
keyChangedAt?: string | null;
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* Someone who reads one of the caller's apps and already has a wrapped copy of its key (GET /apps/keys/holders).
|
|
284
|
+
*/
|
|
285
|
+
interface KeyHolder {
|
|
286
|
+
userId: string;
|
|
287
|
+
publicKey: string;
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
290
|
+
* Body of PUT /apps/:id/keys/:userId: the app key wrapped to that user's account key.
|
|
291
|
+
*/
|
|
292
|
+
interface AppKeyUpsertBody {
|
|
293
|
+
encryptedKey: string;
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
296
|
+
* An account key a user has accepted for someone they share apps with (GET /key-pins). `mac` is an HMAC by the user's
|
|
297
|
+
* own devices, under a key derived from their account key, over the user, the person and the key: the server can drop
|
|
298
|
+
* a pin but not forge or move one.
|
|
299
|
+
*/
|
|
300
|
+
interface KeyPin {
|
|
301
|
+
userId: string;
|
|
302
|
+
publicKey: string;
|
|
303
|
+
mac: string;
|
|
304
|
+
}
|
|
305
|
+
type KeyCheckResult = 'confirmed' | 'failed' | 'cancelled';
|
|
306
|
+
/**
|
|
307
|
+
* A key check the caller started, as their devices read it (GET /key-checks/mine): enough to show the code again or
|
|
308
|
+
* finish the check. The messages are a password-authenticated exchange the server can't read or test codes against.
|
|
309
|
+
*/
|
|
310
|
+
interface HolderKeyCheck {
|
|
311
|
+
id: string;
|
|
312
|
+
userId: string;
|
|
313
|
+
sid: string;
|
|
314
|
+
holderMessage: string;
|
|
315
|
+
/** The holder's half of the exchange, sealed under their account key. */
|
|
316
|
+
holderState: string;
|
|
317
|
+
claimantMessage: string | null;
|
|
318
|
+
publicKey: string | null;
|
|
319
|
+
tag: string | null;
|
|
320
|
+
answeredAt: string | null;
|
|
321
|
+
result: KeyCheckResult | null;
|
|
322
|
+
finishedAt: string | null;
|
|
323
|
+
expiresAt: string;
|
|
324
|
+
createdAt: string;
|
|
325
|
+
}
|
|
326
|
+
/**
|
|
327
|
+
* A key check waiting on the caller's answer, or how a recent answer went (GET /key-checks/for-me).
|
|
328
|
+
*/
|
|
329
|
+
interface ClaimantKeyCheck {
|
|
330
|
+
id: string;
|
|
331
|
+
holderId: string;
|
|
332
|
+
holderName: string;
|
|
333
|
+
sid: string;
|
|
334
|
+
holderMessage: string;
|
|
335
|
+
answeredAt: string | null;
|
|
336
|
+
result: KeyCheckResult | null;
|
|
337
|
+
finishedAt: string | null;
|
|
338
|
+
expiresAt: string;
|
|
339
|
+
createdAt: string;
|
|
340
|
+
}
|
|
341
|
+
|
|
140
342
|
/**
|
|
141
343
|
* Structured log context content.
|
|
142
344
|
*/
|
|
@@ -235,6 +437,47 @@ interface NotesQuery {
|
|
|
235
437
|
cursor?: string;
|
|
236
438
|
}
|
|
237
439
|
|
|
440
|
+
/**
|
|
441
|
+
* The `data` a push notification carries, which the app reads to open the screen it's about. Expo delivers data as
|
|
442
|
+
* JSON, but some platforms want strings, so every value is one.
|
|
443
|
+
*/
|
|
444
|
+
type PushData =
|
|
445
|
+
/** A log at or above the reader's threshold (Lumber-Worker). `level` is the LogLevel as a string. */
|
|
446
|
+
{
|
|
447
|
+
type: 'log';
|
|
448
|
+
appId: string;
|
|
449
|
+
logId: string;
|
|
450
|
+
level: string;
|
|
451
|
+
}
|
|
452
|
+
/** A note from someone else on an app the reader reads (Lumber-API). */
|
|
453
|
+
| {
|
|
454
|
+
type: 'note';
|
|
455
|
+
appId: string;
|
|
456
|
+
noteId: string;
|
|
457
|
+
}
|
|
458
|
+
/** A new device asking to link to the account (Lumber-Worker). */
|
|
459
|
+
| {
|
|
460
|
+
type: 'device_link';
|
|
461
|
+
deviceLinkId: string;
|
|
462
|
+
deviceName: string;
|
|
463
|
+
}
|
|
464
|
+
/** The person the reader gave a code entered it (Lumber-API). */
|
|
465
|
+
| {
|
|
466
|
+
type: 'key_check';
|
|
467
|
+
checkId: string;
|
|
468
|
+
}
|
|
469
|
+
/** Someone is waiting for the reader to confirm their key, as new or changed (Lumber-API). */
|
|
470
|
+
| {
|
|
471
|
+
type: 'key_waiting';
|
|
472
|
+
userId: string;
|
|
473
|
+
}
|
|
474
|
+
/** Someone wrapped app keys for the reader. `appId` when it was one app (Lumber-API). */
|
|
475
|
+
| {
|
|
476
|
+
type: 'keys_shared';
|
|
477
|
+
appId?: string;
|
|
478
|
+
};
|
|
479
|
+
type PushType = PushData['type'];
|
|
480
|
+
|
|
238
481
|
interface ResponseMetadata {
|
|
239
482
|
requestId: string;
|
|
240
483
|
durationMs: number;
|
|
@@ -249,37 +492,45 @@ interface ApiResponse<T> {
|
|
|
249
492
|
metadata: ResponseMetadata;
|
|
250
493
|
}
|
|
251
494
|
|
|
495
|
+
/** An app as the socket sends it: no wrapped keys, so a client refetches through the API for its own copy. */
|
|
496
|
+
type SocketAppPayload = Pick<AppRecord, 'id' | 'name' | 'teamId' | 'color' | 'imageUrl' | 'createdAt' | 'updatedAt'>;
|
|
252
497
|
/**
|
|
253
|
-
*
|
|
254
|
-
*
|
|
498
|
+
* What Lumber-Socket sends a client, for socket.io's typed `Server` and `Socket`. Each goes to the users who can read
|
|
499
|
+
* the app it's about, live updates permitting (the app's team's plan); flags go to the user who set them.
|
|
255
500
|
*/
|
|
256
|
-
interface
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
*/
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
501
|
+
interface ServerToClientEvents {
|
|
502
|
+
'log:created': (log: Omit<LogRecord, 'updatedAt'> & {
|
|
503
|
+
updatedAt?: string;
|
|
504
|
+
}) => void;
|
|
505
|
+
'flag:created': (flag: Omit<FlagRecord, 'updatedAt'> & {
|
|
506
|
+
updatedAt?: string;
|
|
507
|
+
}) => void;
|
|
508
|
+
'flag:deleted': (flag: Pick<FlagRecord, 'id'>) => void;
|
|
509
|
+
'note:created': (note: NoteRecord) => void;
|
|
510
|
+
'note:updated': (note: NoteRecord) => void;
|
|
511
|
+
'note:deleted': (note: Pick<NoteRecord, 'id' | 'appId'>) => void;
|
|
512
|
+
'app:created': (app: SocketAppPayload) => void;
|
|
513
|
+
'app:updated': (app: SocketAppPayload) => void;
|
|
514
|
+
/** Deleted, or kept to its team's owner and so gone for the rest of the team. */
|
|
515
|
+
'app:deleted': (app: Pick<AppRecord, 'id'>) => void;
|
|
516
|
+
/** Someone joined or left a team or app the user is on. Carries nothing; sent on every plan. */
|
|
517
|
+
'access:changed': (payload: Record<string, never>) => void;
|
|
518
|
+
/**
|
|
519
|
+
* The socket's database connection came back after a gap and has replayed what it could. Deletes, and anything older
|
|
520
|
+
* or past the replay's cap, need the client's own resync.
|
|
521
|
+
*/
|
|
522
|
+
'socket:reconnected': () => void;
|
|
523
|
+
/** The answer to `listMyApps`: the apps the user gets live updates for. */
|
|
524
|
+
myApps: (payload: {
|
|
525
|
+
apps: Pick<AppRecord, 'id' | 'name'>[];
|
|
526
|
+
}) => void;
|
|
527
|
+
error: (payload: {
|
|
528
|
+
message: string;
|
|
529
|
+
}) => void;
|
|
530
|
+
}
|
|
531
|
+
/** What a client sends Lumber-Socket. */
|
|
532
|
+
interface ClientToServerEvents {
|
|
533
|
+
listMyApps: () => void;
|
|
283
534
|
}
|
|
284
535
|
|
|
285
536
|
/**
|
|
@@ -317,6 +568,8 @@ interface PushToken {
|
|
|
317
568
|
token: string;
|
|
318
569
|
deviceId?: string;
|
|
319
570
|
platform?: 'ios' | 'android' | 'web';
|
|
571
|
+
/** The device's IANA time zone, so a push can say a log's time as the device would. */
|
|
572
|
+
timeZone?: string;
|
|
320
573
|
}
|
|
321
574
|
interface NotificationPreference {
|
|
322
575
|
enabled?: boolean;
|
|
@@ -326,6 +579,8 @@ interface NotificationPreference {
|
|
|
326
579
|
interface NotificationPreferences {
|
|
327
580
|
enabled?: boolean;
|
|
328
581
|
minimumLevel?: number;
|
|
582
|
+
/** Whether new notes on the user's apps push; on unless false. */
|
|
583
|
+
notes?: boolean;
|
|
329
584
|
perApp?: Record<string, NotificationPreference>;
|
|
330
585
|
}
|
|
331
586
|
/**
|
|
@@ -344,6 +599,10 @@ interface UserRecord {
|
|
|
344
599
|
agreedToTermsVersion?: string;
|
|
345
600
|
notificationPreferences: NotificationPreferences;
|
|
346
601
|
pushTokens: PushToken[];
|
|
602
|
+
/** When the address was confirmed, null until it is. Missing from an API before email confirmation. */
|
|
603
|
+
emailVerifiedAt?: string | null;
|
|
604
|
+
/** Per app, when the user last had its notes open, from any device. Missing from an API before it was kept. */
|
|
605
|
+
notesReadAt?: Record<string, string>;
|
|
347
606
|
}
|
|
348
607
|
|
|
349
608
|
declare const isValidAppearance: (value: unknown) => value is Appearance;
|
|
@@ -355,5 +614,26 @@ declare const isValidDeviceLinkStatus: (value: unknown) => value is DeviceLinkSt
|
|
|
355
614
|
declare const isValidTeamMemberRole: (value: unknown) => value is TeamMemberRecord["role"];
|
|
356
615
|
/** Accepts any valid absolute URL (https://..., http://..., etc.). */
|
|
357
616
|
declare const isValidImageUrl: (value: unknown) => value is string;
|
|
617
|
+
/**
|
|
618
|
+
* The API key the Lumber app hands out: an app's API token and its encryption key, joined as `<token>.<encryptionKey>`,
|
|
619
|
+
* so a plugin is configured with one secret. A plugin sends only the token to the API (`Authorization: Bearer <token>`)
|
|
620
|
+
* and encrypts with the rest, so the encryption key never leaves it. Neither half can contain a `.` (hex and base64).
|
|
621
|
+
*/
|
|
622
|
+
declare const joinApiKey: (token: string, encryptionKey: string) => string;
|
|
623
|
+
/** Splits an API key into its token and encryption key. A value with no dot is all token. */
|
|
624
|
+
declare const splitApiKey: (apiKey: string) => {
|
|
625
|
+
token: string;
|
|
626
|
+
encryptionKey?: string;
|
|
627
|
+
};
|
|
628
|
+
/**
|
|
629
|
+
* A push threshold from a stored or sent preference: a log level, or PUSH_OFF for anything past Fatal. Below Debug is
|
|
630
|
+
* Debug, a fraction rounds, and anything that isn't a finite number is the default (Warn).
|
|
631
|
+
*/
|
|
632
|
+
declare const normalizePushThreshold: (value: unknown) => PushThreshold;
|
|
633
|
+
declare const isApiErrorCode: (value: unknown) => value is ApiErrorCode;
|
|
634
|
+
/** The code in an API error response's body, or null when it has none. */
|
|
635
|
+
declare const apiErrorCodeOf: (body: unknown) => ApiErrorCode | null;
|
|
636
|
+
/** A push notification's data as PushData, or null for data no Lumber push sends. */
|
|
637
|
+
declare const parsePushData: (data: unknown) => PushData | null;
|
|
358
638
|
|
|
359
|
-
export { type ApiKeyRecord, type ApiResponse, type AppCreatedRecord, type AppEncryptedKey, type AppRecord, type Appearance, type AppsQuery, type DeviceLinkCreatedRecord, type DeviceLinkRecord, type DeviceLinkStatus, type FlagRecord, type FlagsQuery, type GuestRecord, 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, type PushToken, type ResponseMetadata, SubscriptionLevel, type SubscriptionLevelName, type SubscriptionLimits, type TeamMemberRecord, type TeamRecord, type TeamsQuery, type TimeseriesBucket, type TimeseriesResponse, type UserRecord, isValidAppearance, isValidColor, isValidDeviceLinkStatus, isValidImageUrl, isValidLogLevel, isValidSubscriptionLevel, isValidTeamMemberRole, logLevelNameToLevel, logLevelToName, logLevels, subscriptionLevelToName, subscriptionLevels, subscriptionLimits };
|
|
639
|
+
export { 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_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, logLevelNameToLevel, logLevelToName, logLevels, normalizePushThreshold, parsePushData, splitApiKey, subscriptionLevelToName, subscriptionLevels, subscriptionLimits, subscriptionPlans };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Machine-readable reasons the API refuses a request, sent as `code` beside `message` where one status means more than
|
|
3
|
+
* one thing. Messages are for people and may change; clients decide by the code.
|
|
4
|
+
*/
|
|
5
|
+
declare const apiErrorCodes: readonly ["APP_LIMIT", "SEAT_LIMIT", "GUEST_LIMIT", "ALREADY_MEMBER", "ALREADY_READS", "ALREADY_INVITED", "HAS_ACCOUNT"];
|
|
6
|
+
type ApiErrorCode = (typeof apiErrorCodes)[number];
|
|
7
|
+
|
|
1
8
|
/** Log severity level. Numeric values match the existing Lumber-API level field. */
|
|
2
9
|
declare const enum LogLevel {
|
|
3
10
|
Debug = 0,
|
|
@@ -15,6 +22,13 @@ declare const logLevelNameToLevel: Record<LogLevelName, LogLevel>;
|
|
|
15
22
|
/** All valid log levels, ordered from least to most severe. */
|
|
16
23
|
declare const logLevels: readonly LogLevel[];
|
|
17
24
|
|
|
25
|
+
/** A push threshold one past Fatal, which no log reaches: log pushes are off (the account's, or one app's). */
|
|
26
|
+
declare const PUSH_OFF = 5;
|
|
27
|
+
/** The lowest log level that pushes, or PUSH_OFF. Stored as a notification preference's `minimumLevel`. */
|
|
28
|
+
type PushThreshold = LogLevel | typeof PUSH_OFF;
|
|
29
|
+
/** The threshold a user gets when none is set. */
|
|
30
|
+
declare const DEFAULT_PUSH_THRESHOLD: PushThreshold;
|
|
31
|
+
|
|
18
32
|
/** User subscription level. Mirrors User.subscriptionLevel in Lumber-API. */
|
|
19
33
|
declare const enum SubscriptionLevel {
|
|
20
34
|
Hobby = 0,
|
|
@@ -41,6 +55,70 @@ type SubscriptionLimits = {
|
|
|
41
55
|
};
|
|
42
56
|
/** Per-level resource limits. Use Infinity to mean unlimited or disabled. */
|
|
43
57
|
declare const subscriptionLimits: Record<SubscriptionLevel, SubscriptionLimits>;
|
|
58
|
+
type SubscriptionPlan = {
|
|
59
|
+
/** Display name, as every surface shows it. */
|
|
60
|
+
name: string;
|
|
61
|
+
/** Monthly price in US dollars: 0 for free, null when priced per customer. */
|
|
62
|
+
monthlyPrice: number | null;
|
|
63
|
+
};
|
|
64
|
+
/** Each level's name and price. Stripe holds the prices themselves; these must match what it charges. */
|
|
65
|
+
declare const subscriptionPlans: Record<SubscriptionLevel, SubscriptionPlan>;
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* An organization that owns apps and pays for member usage.
|
|
69
|
+
* Team subscriptionLevel takes precedence over individual member levels.
|
|
70
|
+
*/
|
|
71
|
+
interface TeamRecord {
|
|
72
|
+
id: string;
|
|
73
|
+
createdAt: string;
|
|
74
|
+
updatedAt: string;
|
|
75
|
+
name: string;
|
|
76
|
+
color?: string;
|
|
77
|
+
imageUrl?: string;
|
|
78
|
+
subscriptionLevel: SubscriptionLevel;
|
|
79
|
+
/** When a canceled plan stops; null while it renews. */
|
|
80
|
+
subscriptionEnd?: string | null;
|
|
81
|
+
/** GET /teams only: the caller's role on the team. */
|
|
82
|
+
role?: TeamMemberRecord['role'];
|
|
83
|
+
/** GET /teams only: the caller's membership id, which leaving the team deletes. */
|
|
84
|
+
memberId?: string;
|
|
85
|
+
/** GET /teams only: the team's apps, as creating one counts them. Null unless the caller is its owner or an admin. */
|
|
86
|
+
appCount?: number | null;
|
|
87
|
+
/** GET /teams only: logs its apps sent this UTC month, the monthly cap's count. Null unless owner or admin. */
|
|
88
|
+
logsThisMonth?: number | null;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Membership of a user in a team.
|
|
92
|
+
*/
|
|
93
|
+
interface TeamMemberRecord {
|
|
94
|
+
id: string;
|
|
95
|
+
createdAt: string;
|
|
96
|
+
updatedAt: string;
|
|
97
|
+
teamId: string;
|
|
98
|
+
userId: string;
|
|
99
|
+
role: 'owner' | 'admin' | 'member';
|
|
100
|
+
/** Listed members only: their display name. Never their email address. */
|
|
101
|
+
name?: string | null;
|
|
102
|
+
/** Listed members only: the part of their email before the @. */
|
|
103
|
+
handle?: string | null;
|
|
104
|
+
color?: string | null;
|
|
105
|
+
imageUrl?: string | null;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* A plan GET /subscriptions offers, with its Stripe price (none for Hobby).
|
|
109
|
+
*/
|
|
110
|
+
interface SubscriptionPlanRecord {
|
|
111
|
+
name: string;
|
|
112
|
+
priceId: string | null;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Filter params for GET /teams.
|
|
116
|
+
*/
|
|
117
|
+
interface TeamsQuery {
|
|
118
|
+
sort?: string;
|
|
119
|
+
limit?: number;
|
|
120
|
+
cursor?: string;
|
|
121
|
+
}
|
|
44
122
|
|
|
45
123
|
/**
|
|
46
124
|
* Wrapped application encryption key for a specific user.
|
|
@@ -50,9 +128,14 @@ interface AppEncryptedKey {
|
|
|
50
128
|
encryptedKey: string;
|
|
51
129
|
keyVersion: number;
|
|
52
130
|
}
|
|
131
|
+
/**
|
|
132
|
+
* Who on an app's team can read it: everyone on the team, or only its owner. Its guests read it either way.
|
|
133
|
+
*/
|
|
134
|
+
type AppTeamAccess = 'everyone' | 'owner';
|
|
53
135
|
/**
|
|
54
136
|
* API response shape for a listed or fetched app.
|
|
55
137
|
* encryptedKey is the requesting user's wrapped copy of the app key, or null if none exists.
|
|
138
|
+
* The optional fields are missing from an API deployed before them, which clients read as the default.
|
|
56
139
|
*/
|
|
57
140
|
interface AppRecord {
|
|
58
141
|
id: string;
|
|
@@ -63,6 +146,12 @@ interface AppRecord {
|
|
|
63
146
|
imageUrl?: string;
|
|
64
147
|
teamId?: string;
|
|
65
148
|
encryptedKey: string | null;
|
|
149
|
+
/** Missing means 'everyone'. */
|
|
150
|
+
teamAccess?: AppTeamAccess;
|
|
151
|
+
/** How many days of its logs the app's team's plan keeps, null for no limit (and when missing). */
|
|
152
|
+
retentionDays?: number | null;
|
|
153
|
+
/** While the team has sent its plan's monthly logs, when new ones are taken again (the next UTC month); else null. */
|
|
154
|
+
logsCappedUntil?: string | null;
|
|
66
155
|
}
|
|
67
156
|
/**
|
|
68
157
|
* App creation response.
|
|
@@ -92,6 +181,25 @@ interface GuestRecord {
|
|
|
92
181
|
userId: string;
|
|
93
182
|
role: 'owner' | 'admin' | 'read' | 'write';
|
|
94
183
|
}
|
|
184
|
+
/**
|
|
185
|
+
* What someone may do on an app. manage: the team's owner or admins, or a guest invited as owner or admin. write: a
|
|
186
|
+
* team member or a write guest. read: any other access.
|
|
187
|
+
*/
|
|
188
|
+
type AppPermission = 'read' | 'write' | 'manage';
|
|
189
|
+
/**
|
|
190
|
+
* Someone who can read an app, as the others see them (GET /apps/:id/people): never their email address.
|
|
191
|
+
*/
|
|
192
|
+
interface AppPersonRecord {
|
|
193
|
+
userId: string;
|
|
194
|
+
name: string | null;
|
|
195
|
+
/** The part of their email before the @, for anyone who hasn't set a name. */
|
|
196
|
+
handle?: string | null;
|
|
197
|
+
color: string | null;
|
|
198
|
+
/** Their picture URL, or null for their initial. */
|
|
199
|
+
imageUrl?: string | null;
|
|
200
|
+
role: TeamMemberRecord['role'] | GuestRecord['role'];
|
|
201
|
+
kind: 'team' | 'guest';
|
|
202
|
+
}
|
|
95
203
|
/**
|
|
96
204
|
* Filter params for GET /apps.
|
|
97
205
|
*/
|
|
@@ -123,6 +231,14 @@ interface DeviceLinkRecord {
|
|
|
123
231
|
expiresAt: string;
|
|
124
232
|
}
|
|
125
233
|
|
|
234
|
+
/**
|
|
235
|
+
* The body of an API error response. `code` is there where the status alone doesn't say what went wrong.
|
|
236
|
+
*/
|
|
237
|
+
interface ApiErrorBody {
|
|
238
|
+
message: string;
|
|
239
|
+
code?: ApiErrorCode;
|
|
240
|
+
}
|
|
241
|
+
|
|
126
242
|
interface FlagRecord {
|
|
127
243
|
id: string;
|
|
128
244
|
createdAt: string;
|
|
@@ -137,6 +253,92 @@ interface FlagsQuery {
|
|
|
137
253
|
cursor?: string;
|
|
138
254
|
}
|
|
139
255
|
|
|
256
|
+
/**
|
|
257
|
+
* An invitation to an address with no account yet: to a team (teamId, as a member or admin) or to one app as its guest
|
|
258
|
+
* (appId). The address joins when it signs up and confirms itself. A live one (unexpired) holds its seat.
|
|
259
|
+
*/
|
|
260
|
+
interface InvitationRecord {
|
|
261
|
+
id: string;
|
|
262
|
+
teamId: string | null;
|
|
263
|
+
appId: string | null;
|
|
264
|
+
email: string;
|
|
265
|
+
role: string;
|
|
266
|
+
/** Null when its email couldn't be sent. */
|
|
267
|
+
sentAt: string | null;
|
|
268
|
+
expiresAt: string;
|
|
269
|
+
createdAt: string;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Someone with access to an app who has no wrapped copy of its key yet (GET /apps/keys/pending), with when they last
|
|
274
|
+
* replaced their account key: null if never, missing from an API before key pins.
|
|
275
|
+
*/
|
|
276
|
+
interface PendingAppKey {
|
|
277
|
+
appId: string;
|
|
278
|
+
userId: string;
|
|
279
|
+
publicKey: string;
|
|
280
|
+
keyChangedAt?: string | null;
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* Someone who reads one of the caller's apps and already has a wrapped copy of its key (GET /apps/keys/holders).
|
|
284
|
+
*/
|
|
285
|
+
interface KeyHolder {
|
|
286
|
+
userId: string;
|
|
287
|
+
publicKey: string;
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
290
|
+
* Body of PUT /apps/:id/keys/:userId: the app key wrapped to that user's account key.
|
|
291
|
+
*/
|
|
292
|
+
interface AppKeyUpsertBody {
|
|
293
|
+
encryptedKey: string;
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
296
|
+
* An account key a user has accepted for someone they share apps with (GET /key-pins). `mac` is an HMAC by the user's
|
|
297
|
+
* own devices, under a key derived from their account key, over the user, the person and the key: the server can drop
|
|
298
|
+
* a pin but not forge or move one.
|
|
299
|
+
*/
|
|
300
|
+
interface KeyPin {
|
|
301
|
+
userId: string;
|
|
302
|
+
publicKey: string;
|
|
303
|
+
mac: string;
|
|
304
|
+
}
|
|
305
|
+
type KeyCheckResult = 'confirmed' | 'failed' | 'cancelled';
|
|
306
|
+
/**
|
|
307
|
+
* A key check the caller started, as their devices read it (GET /key-checks/mine): enough to show the code again or
|
|
308
|
+
* finish the check. The messages are a password-authenticated exchange the server can't read or test codes against.
|
|
309
|
+
*/
|
|
310
|
+
interface HolderKeyCheck {
|
|
311
|
+
id: string;
|
|
312
|
+
userId: string;
|
|
313
|
+
sid: string;
|
|
314
|
+
holderMessage: string;
|
|
315
|
+
/** The holder's half of the exchange, sealed under their account key. */
|
|
316
|
+
holderState: string;
|
|
317
|
+
claimantMessage: string | null;
|
|
318
|
+
publicKey: string | null;
|
|
319
|
+
tag: string | null;
|
|
320
|
+
answeredAt: string | null;
|
|
321
|
+
result: KeyCheckResult | null;
|
|
322
|
+
finishedAt: string | null;
|
|
323
|
+
expiresAt: string;
|
|
324
|
+
createdAt: string;
|
|
325
|
+
}
|
|
326
|
+
/**
|
|
327
|
+
* A key check waiting on the caller's answer, or how a recent answer went (GET /key-checks/for-me).
|
|
328
|
+
*/
|
|
329
|
+
interface ClaimantKeyCheck {
|
|
330
|
+
id: string;
|
|
331
|
+
holderId: string;
|
|
332
|
+
holderName: string;
|
|
333
|
+
sid: string;
|
|
334
|
+
holderMessage: string;
|
|
335
|
+
answeredAt: string | null;
|
|
336
|
+
result: KeyCheckResult | null;
|
|
337
|
+
finishedAt: string | null;
|
|
338
|
+
expiresAt: string;
|
|
339
|
+
createdAt: string;
|
|
340
|
+
}
|
|
341
|
+
|
|
140
342
|
/**
|
|
141
343
|
* Structured log context content.
|
|
142
344
|
*/
|
|
@@ -235,6 +437,47 @@ interface NotesQuery {
|
|
|
235
437
|
cursor?: string;
|
|
236
438
|
}
|
|
237
439
|
|
|
440
|
+
/**
|
|
441
|
+
* The `data` a push notification carries, which the app reads to open the screen it's about. Expo delivers data as
|
|
442
|
+
* JSON, but some platforms want strings, so every value is one.
|
|
443
|
+
*/
|
|
444
|
+
type PushData =
|
|
445
|
+
/** A log at or above the reader's threshold (Lumber-Worker). `level` is the LogLevel as a string. */
|
|
446
|
+
{
|
|
447
|
+
type: 'log';
|
|
448
|
+
appId: string;
|
|
449
|
+
logId: string;
|
|
450
|
+
level: string;
|
|
451
|
+
}
|
|
452
|
+
/** A note from someone else on an app the reader reads (Lumber-API). */
|
|
453
|
+
| {
|
|
454
|
+
type: 'note';
|
|
455
|
+
appId: string;
|
|
456
|
+
noteId: string;
|
|
457
|
+
}
|
|
458
|
+
/** A new device asking to link to the account (Lumber-Worker). */
|
|
459
|
+
| {
|
|
460
|
+
type: 'device_link';
|
|
461
|
+
deviceLinkId: string;
|
|
462
|
+
deviceName: string;
|
|
463
|
+
}
|
|
464
|
+
/** The person the reader gave a code entered it (Lumber-API). */
|
|
465
|
+
| {
|
|
466
|
+
type: 'key_check';
|
|
467
|
+
checkId: string;
|
|
468
|
+
}
|
|
469
|
+
/** Someone is waiting for the reader to confirm their key, as new or changed (Lumber-API). */
|
|
470
|
+
| {
|
|
471
|
+
type: 'key_waiting';
|
|
472
|
+
userId: string;
|
|
473
|
+
}
|
|
474
|
+
/** Someone wrapped app keys for the reader. `appId` when it was one app (Lumber-API). */
|
|
475
|
+
| {
|
|
476
|
+
type: 'keys_shared';
|
|
477
|
+
appId?: string;
|
|
478
|
+
};
|
|
479
|
+
type PushType = PushData['type'];
|
|
480
|
+
|
|
238
481
|
interface ResponseMetadata {
|
|
239
482
|
requestId: string;
|
|
240
483
|
durationMs: number;
|
|
@@ -249,37 +492,45 @@ interface ApiResponse<T> {
|
|
|
249
492
|
metadata: ResponseMetadata;
|
|
250
493
|
}
|
|
251
494
|
|
|
495
|
+
/** An app as the socket sends it: no wrapped keys, so a client refetches through the API for its own copy. */
|
|
496
|
+
type SocketAppPayload = Pick<AppRecord, 'id' | 'name' | 'teamId' | 'color' | 'imageUrl' | 'createdAt' | 'updatedAt'>;
|
|
252
497
|
/**
|
|
253
|
-
*
|
|
254
|
-
*
|
|
498
|
+
* What Lumber-Socket sends a client, for socket.io's typed `Server` and `Socket`. Each goes to the users who can read
|
|
499
|
+
* the app it's about, live updates permitting (the app's team's plan); flags go to the user who set them.
|
|
255
500
|
*/
|
|
256
|
-
interface
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
*/
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
501
|
+
interface ServerToClientEvents {
|
|
502
|
+
'log:created': (log: Omit<LogRecord, 'updatedAt'> & {
|
|
503
|
+
updatedAt?: string;
|
|
504
|
+
}) => void;
|
|
505
|
+
'flag:created': (flag: Omit<FlagRecord, 'updatedAt'> & {
|
|
506
|
+
updatedAt?: string;
|
|
507
|
+
}) => void;
|
|
508
|
+
'flag:deleted': (flag: Pick<FlagRecord, 'id'>) => void;
|
|
509
|
+
'note:created': (note: NoteRecord) => void;
|
|
510
|
+
'note:updated': (note: NoteRecord) => void;
|
|
511
|
+
'note:deleted': (note: Pick<NoteRecord, 'id' | 'appId'>) => void;
|
|
512
|
+
'app:created': (app: SocketAppPayload) => void;
|
|
513
|
+
'app:updated': (app: SocketAppPayload) => void;
|
|
514
|
+
/** Deleted, or kept to its team's owner and so gone for the rest of the team. */
|
|
515
|
+
'app:deleted': (app: Pick<AppRecord, 'id'>) => void;
|
|
516
|
+
/** Someone joined or left a team or app the user is on. Carries nothing; sent on every plan. */
|
|
517
|
+
'access:changed': (payload: Record<string, never>) => void;
|
|
518
|
+
/**
|
|
519
|
+
* The socket's database connection came back after a gap and has replayed what it could. Deletes, and anything older
|
|
520
|
+
* or past the replay's cap, need the client's own resync.
|
|
521
|
+
*/
|
|
522
|
+
'socket:reconnected': () => void;
|
|
523
|
+
/** The answer to `listMyApps`: the apps the user gets live updates for. */
|
|
524
|
+
myApps: (payload: {
|
|
525
|
+
apps: Pick<AppRecord, 'id' | 'name'>[];
|
|
526
|
+
}) => void;
|
|
527
|
+
error: (payload: {
|
|
528
|
+
message: string;
|
|
529
|
+
}) => void;
|
|
530
|
+
}
|
|
531
|
+
/** What a client sends Lumber-Socket. */
|
|
532
|
+
interface ClientToServerEvents {
|
|
533
|
+
listMyApps: () => void;
|
|
283
534
|
}
|
|
284
535
|
|
|
285
536
|
/**
|
|
@@ -317,6 +568,8 @@ interface PushToken {
|
|
|
317
568
|
token: string;
|
|
318
569
|
deviceId?: string;
|
|
319
570
|
platform?: 'ios' | 'android' | 'web';
|
|
571
|
+
/** The device's IANA time zone, so a push can say a log's time as the device would. */
|
|
572
|
+
timeZone?: string;
|
|
320
573
|
}
|
|
321
574
|
interface NotificationPreference {
|
|
322
575
|
enabled?: boolean;
|
|
@@ -326,6 +579,8 @@ interface NotificationPreference {
|
|
|
326
579
|
interface NotificationPreferences {
|
|
327
580
|
enabled?: boolean;
|
|
328
581
|
minimumLevel?: number;
|
|
582
|
+
/** Whether new notes on the user's apps push; on unless false. */
|
|
583
|
+
notes?: boolean;
|
|
329
584
|
perApp?: Record<string, NotificationPreference>;
|
|
330
585
|
}
|
|
331
586
|
/**
|
|
@@ -344,6 +599,10 @@ interface UserRecord {
|
|
|
344
599
|
agreedToTermsVersion?: string;
|
|
345
600
|
notificationPreferences: NotificationPreferences;
|
|
346
601
|
pushTokens: PushToken[];
|
|
602
|
+
/** When the address was confirmed, null until it is. Missing from an API before email confirmation. */
|
|
603
|
+
emailVerifiedAt?: string | null;
|
|
604
|
+
/** Per app, when the user last had its notes open, from any device. Missing from an API before it was kept. */
|
|
605
|
+
notesReadAt?: Record<string, string>;
|
|
347
606
|
}
|
|
348
607
|
|
|
349
608
|
declare const isValidAppearance: (value: unknown) => value is Appearance;
|
|
@@ -355,5 +614,26 @@ declare const isValidDeviceLinkStatus: (value: unknown) => value is DeviceLinkSt
|
|
|
355
614
|
declare const isValidTeamMemberRole: (value: unknown) => value is TeamMemberRecord["role"];
|
|
356
615
|
/** Accepts any valid absolute URL (https://..., http://..., etc.). */
|
|
357
616
|
declare const isValidImageUrl: (value: unknown) => value is string;
|
|
617
|
+
/**
|
|
618
|
+
* The API key the Lumber app hands out: an app's API token and its encryption key, joined as `<token>.<encryptionKey>`,
|
|
619
|
+
* so a plugin is configured with one secret. A plugin sends only the token to the API (`Authorization: Bearer <token>`)
|
|
620
|
+
* and encrypts with the rest, so the encryption key never leaves it. Neither half can contain a `.` (hex and base64).
|
|
621
|
+
*/
|
|
622
|
+
declare const joinApiKey: (token: string, encryptionKey: string) => string;
|
|
623
|
+
/** Splits an API key into its token and encryption key. A value with no dot is all token. */
|
|
624
|
+
declare const splitApiKey: (apiKey: string) => {
|
|
625
|
+
token: string;
|
|
626
|
+
encryptionKey?: string;
|
|
627
|
+
};
|
|
628
|
+
/**
|
|
629
|
+
* A push threshold from a stored or sent preference: a log level, or PUSH_OFF for anything past Fatal. Below Debug is
|
|
630
|
+
* Debug, a fraction rounds, and anything that isn't a finite number is the default (Warn).
|
|
631
|
+
*/
|
|
632
|
+
declare const normalizePushThreshold: (value: unknown) => PushThreshold;
|
|
633
|
+
declare const isApiErrorCode: (value: unknown) => value is ApiErrorCode;
|
|
634
|
+
/** The code in an API error response's body, or null when it has none. */
|
|
635
|
+
declare const apiErrorCodeOf: (body: unknown) => ApiErrorCode | null;
|
|
636
|
+
/** A push notification's data as PushData, or null for data no Lumber push sends. */
|
|
637
|
+
declare const parsePushData: (data: unknown) => PushData | null;
|
|
358
638
|
|
|
359
|
-
export { type ApiKeyRecord, type ApiResponse, type AppCreatedRecord, type AppEncryptedKey, type AppRecord, type Appearance, type AppsQuery, type DeviceLinkCreatedRecord, type DeviceLinkRecord, type DeviceLinkStatus, type FlagRecord, type FlagsQuery, type GuestRecord, 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, type PushToken, type ResponseMetadata, SubscriptionLevel, type SubscriptionLevelName, type SubscriptionLimits, type TeamMemberRecord, type TeamRecord, type TeamsQuery, type TimeseriesBucket, type TimeseriesResponse, type UserRecord, isValidAppearance, isValidColor, isValidDeviceLinkStatus, isValidImageUrl, isValidLogLevel, isValidSubscriptionLevel, isValidTeamMemberRole, logLevelNameToLevel, logLevelToName, logLevels, subscriptionLevelToName, subscriptionLevels, subscriptionLimits };
|
|
639
|
+
export { 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_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, logLevelNameToLevel, logLevelToName, logLevels, normalizePushThreshold, parsePushData, splitApiKey, subscriptionLevelToName, subscriptionLevels, subscriptionLimits, subscriptionPlans };
|
package/dist/index.js
CHANGED
|
@@ -20,8 +20,13 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
20
20
|
// src/index.ts
|
|
21
21
|
var index_exports = {};
|
|
22
22
|
__export(index_exports, {
|
|
23
|
+
DEFAULT_PUSH_THRESHOLD: () => DEFAULT_PUSH_THRESHOLD,
|
|
23
24
|
LogLevel: () => LogLevel,
|
|
25
|
+
PUSH_OFF: () => PUSH_OFF,
|
|
24
26
|
SubscriptionLevel: () => SubscriptionLevel,
|
|
27
|
+
apiErrorCodeOf: () => apiErrorCodeOf,
|
|
28
|
+
apiErrorCodes: () => apiErrorCodes,
|
|
29
|
+
isApiErrorCode: () => isApiErrorCode,
|
|
25
30
|
isValidAppearance: () => isValidAppearance,
|
|
26
31
|
isValidColor: () => isValidColor,
|
|
27
32
|
isValidDeviceLinkStatus: () => isValidDeviceLinkStatus,
|
|
@@ -29,23 +34,46 @@ __export(index_exports, {
|
|
|
29
34
|
isValidLogLevel: () => isValidLogLevel,
|
|
30
35
|
isValidSubscriptionLevel: () => isValidSubscriptionLevel,
|
|
31
36
|
isValidTeamMemberRole: () => isValidTeamMemberRole,
|
|
37
|
+
joinApiKey: () => joinApiKey,
|
|
32
38
|
logLevelNameToLevel: () => logLevelNameToLevel,
|
|
33
39
|
logLevelToName: () => logLevelToName,
|
|
34
40
|
logLevels: () => logLevels,
|
|
41
|
+
normalizePushThreshold: () => normalizePushThreshold,
|
|
42
|
+
parsePushData: () => parsePushData,
|
|
43
|
+
splitApiKey: () => splitApiKey,
|
|
35
44
|
subscriptionLevelToName: () => subscriptionLevelToName,
|
|
36
45
|
subscriptionLevels: () => subscriptionLevels,
|
|
37
|
-
subscriptionLimits: () => subscriptionLimits
|
|
46
|
+
subscriptionLimits: () => subscriptionLimits,
|
|
47
|
+
subscriptionPlans: () => subscriptionPlans
|
|
38
48
|
});
|
|
39
49
|
module.exports = __toCommonJS(index_exports);
|
|
40
50
|
|
|
51
|
+
// src/constants/errors.ts
|
|
52
|
+
var apiErrorCodes = [
|
|
53
|
+
/** 403: the team's plan has no room for another app. */
|
|
54
|
+
"APP_LIMIT",
|
|
55
|
+
/** 403: the team's plan has no room for another member (members and live invitations). */
|
|
56
|
+
"SEAT_LIMIT",
|
|
57
|
+
/** 403: the app's team's plan has no room for another guest on it (guests and live invitations). */
|
|
58
|
+
"GUEST_LIMIT",
|
|
59
|
+
/** 409: the person invited is already on the team. */
|
|
60
|
+
"ALREADY_MEMBER",
|
|
61
|
+
/** 409: the person invited can already read the app. */
|
|
62
|
+
"ALREADY_READS",
|
|
63
|
+
/** 409: the address invited already has an open invitation. */
|
|
64
|
+
"ALREADY_INVITED",
|
|
65
|
+
/** 409: the address invited already has an account, so invite the person instead. */
|
|
66
|
+
"HAS_ACCOUNT"
|
|
67
|
+
];
|
|
68
|
+
|
|
41
69
|
// src/constants/logLevel.ts
|
|
42
|
-
var LogLevel = /* @__PURE__ */ ((
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
return
|
|
70
|
+
var LogLevel = /* @__PURE__ */ ((LogLevel2) => {
|
|
71
|
+
LogLevel2[LogLevel2["Debug"] = 0] = "Debug";
|
|
72
|
+
LogLevel2[LogLevel2["Info"] = 1] = "Info";
|
|
73
|
+
LogLevel2[LogLevel2["Warn"] = 2] = "Warn";
|
|
74
|
+
LogLevel2[LogLevel2["Error"] = 3] = "Error";
|
|
75
|
+
LogLevel2[LogLevel2["Fatal"] = 4] = "Fatal";
|
|
76
|
+
return LogLevel2;
|
|
49
77
|
})(LogLevel || {});
|
|
50
78
|
var logLevelToName = {
|
|
51
79
|
[0 /* Debug */]: "debug",
|
|
@@ -63,6 +91,10 @@ var logLevelNameToLevel = {
|
|
|
63
91
|
};
|
|
64
92
|
var logLevels = [0 /* Debug */, 1 /* Info */, 2 /* Warn */, 3 /* Error */, 4 /* Fatal */];
|
|
65
93
|
|
|
94
|
+
// src/constants/notifications.ts
|
|
95
|
+
var PUSH_OFF = 5;
|
|
96
|
+
var DEFAULT_PUSH_THRESHOLD = 2 /* Warn */;
|
|
97
|
+
|
|
66
98
|
// src/constants/subscription.ts
|
|
67
99
|
var SubscriptionLevel = /* @__PURE__ */ ((SubscriptionLevel3) => {
|
|
68
100
|
SubscriptionLevel3[SubscriptionLevel3["Hobby"] = 0] = "Hobby";
|
|
@@ -84,6 +116,12 @@ var subscriptionLimits = {
|
|
|
84
116
|
[2 /* Pro */]: { apps: 20, seats: 20, apiKeys: 15, logRetentionDays: 90, monthlyLogs: 2e6, realtimeLogs: true, minPushLevel: 2 /* Warn */ },
|
|
85
117
|
[3 /* Enterprise */]: { apps: Infinity, seats: Infinity, apiKeys: Infinity, logRetentionDays: Infinity, monthlyLogs: Infinity, realtimeLogs: true, minPushLevel: 0 /* Debug */ }
|
|
86
118
|
};
|
|
119
|
+
var subscriptionPlans = {
|
|
120
|
+
[0 /* Hobby */]: { name: "Hobby", monthlyPrice: 0 },
|
|
121
|
+
[1 /* Dev */]: { name: "Dev", monthlyPrice: 9 },
|
|
122
|
+
[2 /* Pro */]: { name: "Pro", monthlyPrice: 39 },
|
|
123
|
+
[3 /* Enterprise */]: { name: "Enterprise", monthlyPrice: null }
|
|
124
|
+
};
|
|
87
125
|
|
|
88
126
|
// src/utils/index.ts
|
|
89
127
|
var appearanceValues = ["system", "dark", "light"];
|
|
@@ -104,10 +142,55 @@ var isValidImageUrl = (value) => {
|
|
|
104
142
|
return false;
|
|
105
143
|
}
|
|
106
144
|
};
|
|
145
|
+
var joinApiKey = (token, encryptionKey) => `${token}.${encryptionKey}`;
|
|
146
|
+
var splitApiKey = (apiKey) => {
|
|
147
|
+
const value = apiKey.trim();
|
|
148
|
+
const dot = value.indexOf(".");
|
|
149
|
+
if (dot === -1) return { token: value };
|
|
150
|
+
return { token: value.slice(0, dot), encryptionKey: value.slice(dot + 1) };
|
|
151
|
+
};
|
|
152
|
+
var normalizePushThreshold = (value) => {
|
|
153
|
+
const level = typeof value === "number" ? value : typeof value === "string" && value.trim() !== "" ? Number(value) : NaN;
|
|
154
|
+
if (!Number.isFinite(level)) return DEFAULT_PUSH_THRESHOLD;
|
|
155
|
+
if (level > 4 /* Fatal */) return PUSH_OFF;
|
|
156
|
+
if (level <= 0 /* Debug */) return 0 /* Debug */;
|
|
157
|
+
return Math.round(level);
|
|
158
|
+
};
|
|
159
|
+
var isApiErrorCode = (value) => apiErrorCodes.includes(value);
|
|
160
|
+
var apiErrorCodeOf = (body) => {
|
|
161
|
+
const code = body && typeof body === "object" ? body.code : void 0;
|
|
162
|
+
return isApiErrorCode(code) ? code : null;
|
|
163
|
+
};
|
|
164
|
+
var isId = (value) => typeof value === "string" && value.length > 0;
|
|
165
|
+
var parsePushData = (data) => {
|
|
166
|
+
if (!data || typeof data !== "object") return null;
|
|
167
|
+
const d = data;
|
|
168
|
+
switch (d.type) {
|
|
169
|
+
case "log":
|
|
170
|
+
return isId(d.appId) && isId(d.logId) ? { type: "log", appId: d.appId, logId: d.logId, level: String(d.level ?? "") } : null;
|
|
171
|
+
case "note":
|
|
172
|
+
return isId(d.appId) && isId(d.noteId) ? { type: "note", appId: d.appId, noteId: d.noteId } : null;
|
|
173
|
+
case "device_link":
|
|
174
|
+
return isId(d.deviceLinkId) ? { type: "device_link", deviceLinkId: d.deviceLinkId, deviceName: typeof d.deviceName === "string" ? d.deviceName : "" } : null;
|
|
175
|
+
case "key_check":
|
|
176
|
+
return isId(d.checkId) ? { type: "key_check", checkId: d.checkId } : null;
|
|
177
|
+
case "key_waiting":
|
|
178
|
+
return isId(d.userId) ? { type: "key_waiting", userId: d.userId } : null;
|
|
179
|
+
case "keys_shared":
|
|
180
|
+
return isId(d.appId) ? { type: "keys_shared", appId: d.appId } : { type: "keys_shared" };
|
|
181
|
+
default:
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
184
|
+
};
|
|
107
185
|
// Annotate the CommonJS export names for ESM import in node:
|
|
108
186
|
0 && (module.exports = {
|
|
187
|
+
DEFAULT_PUSH_THRESHOLD,
|
|
109
188
|
LogLevel,
|
|
189
|
+
PUSH_OFF,
|
|
110
190
|
SubscriptionLevel,
|
|
191
|
+
apiErrorCodeOf,
|
|
192
|
+
apiErrorCodes,
|
|
193
|
+
isApiErrorCode,
|
|
111
194
|
isValidAppearance,
|
|
112
195
|
isValidColor,
|
|
113
196
|
isValidDeviceLinkStatus,
|
|
@@ -115,10 +198,15 @@ var isValidImageUrl = (value) => {
|
|
|
115
198
|
isValidLogLevel,
|
|
116
199
|
isValidSubscriptionLevel,
|
|
117
200
|
isValidTeamMemberRole,
|
|
201
|
+
joinApiKey,
|
|
118
202
|
logLevelNameToLevel,
|
|
119
203
|
logLevelToName,
|
|
120
204
|
logLevels,
|
|
205
|
+
normalizePushThreshold,
|
|
206
|
+
parsePushData,
|
|
207
|
+
splitApiKey,
|
|
121
208
|
subscriptionLevelToName,
|
|
122
209
|
subscriptionLevels,
|
|
123
|
-
subscriptionLimits
|
|
210
|
+
subscriptionLimits,
|
|
211
|
+
subscriptionPlans
|
|
124
212
|
});
|
package/dist/index.mjs
CHANGED
|
@@ -1,11 +1,29 @@
|
|
|
1
|
+
// src/constants/errors.ts
|
|
2
|
+
var apiErrorCodes = [
|
|
3
|
+
/** 403: the team's plan has no room for another app. */
|
|
4
|
+
"APP_LIMIT",
|
|
5
|
+
/** 403: the team's plan has no room for another member (members and live invitations). */
|
|
6
|
+
"SEAT_LIMIT",
|
|
7
|
+
/** 403: the app's team's plan has no room for another guest on it (guests and live invitations). */
|
|
8
|
+
"GUEST_LIMIT",
|
|
9
|
+
/** 409: the person invited is already on the team. */
|
|
10
|
+
"ALREADY_MEMBER",
|
|
11
|
+
/** 409: the person invited can already read the app. */
|
|
12
|
+
"ALREADY_READS",
|
|
13
|
+
/** 409: the address invited already has an open invitation. */
|
|
14
|
+
"ALREADY_INVITED",
|
|
15
|
+
/** 409: the address invited already has an account, so invite the person instead. */
|
|
16
|
+
"HAS_ACCOUNT"
|
|
17
|
+
];
|
|
18
|
+
|
|
1
19
|
// src/constants/logLevel.ts
|
|
2
|
-
var LogLevel = /* @__PURE__ */ ((
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
return
|
|
20
|
+
var LogLevel = /* @__PURE__ */ ((LogLevel2) => {
|
|
21
|
+
LogLevel2[LogLevel2["Debug"] = 0] = "Debug";
|
|
22
|
+
LogLevel2[LogLevel2["Info"] = 1] = "Info";
|
|
23
|
+
LogLevel2[LogLevel2["Warn"] = 2] = "Warn";
|
|
24
|
+
LogLevel2[LogLevel2["Error"] = 3] = "Error";
|
|
25
|
+
LogLevel2[LogLevel2["Fatal"] = 4] = "Fatal";
|
|
26
|
+
return LogLevel2;
|
|
9
27
|
})(LogLevel || {});
|
|
10
28
|
var logLevelToName = {
|
|
11
29
|
[0 /* Debug */]: "debug",
|
|
@@ -23,6 +41,10 @@ var logLevelNameToLevel = {
|
|
|
23
41
|
};
|
|
24
42
|
var logLevels = [0 /* Debug */, 1 /* Info */, 2 /* Warn */, 3 /* Error */, 4 /* Fatal */];
|
|
25
43
|
|
|
44
|
+
// src/constants/notifications.ts
|
|
45
|
+
var PUSH_OFF = 5;
|
|
46
|
+
var DEFAULT_PUSH_THRESHOLD = 2 /* Warn */;
|
|
47
|
+
|
|
26
48
|
// src/constants/subscription.ts
|
|
27
49
|
var SubscriptionLevel = /* @__PURE__ */ ((SubscriptionLevel3) => {
|
|
28
50
|
SubscriptionLevel3[SubscriptionLevel3["Hobby"] = 0] = "Hobby";
|
|
@@ -44,6 +66,12 @@ var subscriptionLimits = {
|
|
|
44
66
|
[2 /* Pro */]: { apps: 20, seats: 20, apiKeys: 15, logRetentionDays: 90, monthlyLogs: 2e6, realtimeLogs: true, minPushLevel: 2 /* Warn */ },
|
|
45
67
|
[3 /* Enterprise */]: { apps: Infinity, seats: Infinity, apiKeys: Infinity, logRetentionDays: Infinity, monthlyLogs: Infinity, realtimeLogs: true, minPushLevel: 0 /* Debug */ }
|
|
46
68
|
};
|
|
69
|
+
var subscriptionPlans = {
|
|
70
|
+
[0 /* Hobby */]: { name: "Hobby", monthlyPrice: 0 },
|
|
71
|
+
[1 /* Dev */]: { name: "Dev", monthlyPrice: 9 },
|
|
72
|
+
[2 /* Pro */]: { name: "Pro", monthlyPrice: 39 },
|
|
73
|
+
[3 /* Enterprise */]: { name: "Enterprise", monthlyPrice: null }
|
|
74
|
+
};
|
|
47
75
|
|
|
48
76
|
// src/utils/index.ts
|
|
49
77
|
var appearanceValues = ["system", "dark", "light"];
|
|
@@ -64,9 +92,54 @@ var isValidImageUrl = (value) => {
|
|
|
64
92
|
return false;
|
|
65
93
|
}
|
|
66
94
|
};
|
|
95
|
+
var joinApiKey = (token, encryptionKey) => `${token}.${encryptionKey}`;
|
|
96
|
+
var splitApiKey = (apiKey) => {
|
|
97
|
+
const value = apiKey.trim();
|
|
98
|
+
const dot = value.indexOf(".");
|
|
99
|
+
if (dot === -1) return { token: value };
|
|
100
|
+
return { token: value.slice(0, dot), encryptionKey: value.slice(dot + 1) };
|
|
101
|
+
};
|
|
102
|
+
var normalizePushThreshold = (value) => {
|
|
103
|
+
const level = typeof value === "number" ? value : typeof value === "string" && value.trim() !== "" ? Number(value) : NaN;
|
|
104
|
+
if (!Number.isFinite(level)) return DEFAULT_PUSH_THRESHOLD;
|
|
105
|
+
if (level > 4 /* Fatal */) return PUSH_OFF;
|
|
106
|
+
if (level <= 0 /* Debug */) return 0 /* Debug */;
|
|
107
|
+
return Math.round(level);
|
|
108
|
+
};
|
|
109
|
+
var isApiErrorCode = (value) => apiErrorCodes.includes(value);
|
|
110
|
+
var apiErrorCodeOf = (body) => {
|
|
111
|
+
const code = body && typeof body === "object" ? body.code : void 0;
|
|
112
|
+
return isApiErrorCode(code) ? code : null;
|
|
113
|
+
};
|
|
114
|
+
var isId = (value) => typeof value === "string" && value.length > 0;
|
|
115
|
+
var parsePushData = (data) => {
|
|
116
|
+
if (!data || typeof data !== "object") return null;
|
|
117
|
+
const d = data;
|
|
118
|
+
switch (d.type) {
|
|
119
|
+
case "log":
|
|
120
|
+
return isId(d.appId) && isId(d.logId) ? { type: "log", appId: d.appId, logId: d.logId, level: String(d.level ?? "") } : null;
|
|
121
|
+
case "note":
|
|
122
|
+
return isId(d.appId) && isId(d.noteId) ? { type: "note", appId: d.appId, noteId: d.noteId } : null;
|
|
123
|
+
case "device_link":
|
|
124
|
+
return isId(d.deviceLinkId) ? { type: "device_link", deviceLinkId: d.deviceLinkId, deviceName: typeof d.deviceName === "string" ? d.deviceName : "" } : null;
|
|
125
|
+
case "key_check":
|
|
126
|
+
return isId(d.checkId) ? { type: "key_check", checkId: d.checkId } : null;
|
|
127
|
+
case "key_waiting":
|
|
128
|
+
return isId(d.userId) ? { type: "key_waiting", userId: d.userId } : null;
|
|
129
|
+
case "keys_shared":
|
|
130
|
+
return isId(d.appId) ? { type: "keys_shared", appId: d.appId } : { type: "keys_shared" };
|
|
131
|
+
default:
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
};
|
|
67
135
|
export {
|
|
136
|
+
DEFAULT_PUSH_THRESHOLD,
|
|
68
137
|
LogLevel,
|
|
138
|
+
PUSH_OFF,
|
|
69
139
|
SubscriptionLevel,
|
|
140
|
+
apiErrorCodeOf,
|
|
141
|
+
apiErrorCodes,
|
|
142
|
+
isApiErrorCode,
|
|
70
143
|
isValidAppearance,
|
|
71
144
|
isValidColor,
|
|
72
145
|
isValidDeviceLinkStatus,
|
|
@@ -74,10 +147,15 @@ export {
|
|
|
74
147
|
isValidLogLevel,
|
|
75
148
|
isValidSubscriptionLevel,
|
|
76
149
|
isValidTeamMemberRole,
|
|
150
|
+
joinApiKey,
|
|
77
151
|
logLevelNameToLevel,
|
|
78
152
|
logLevelToName,
|
|
79
153
|
logLevels,
|
|
154
|
+
normalizePushThreshold,
|
|
155
|
+
parsePushData,
|
|
156
|
+
splitApiKey,
|
|
80
157
|
subscriptionLevelToName,
|
|
81
158
|
subscriptionLevels,
|
|
82
|
-
subscriptionLimits
|
|
159
|
+
subscriptionLimits,
|
|
160
|
+
subscriptionPlans
|
|
83
161
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@infinitetoken/lumber-kit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Shared TypeScript types and wire format contracts for the Lumber cloud logging suite",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"lumber",
|
|
@@ -56,9 +56,9 @@
|
|
|
56
56
|
},
|
|
57
57
|
"prettier": "@infinitetoken/eslint-config/prettier",
|
|
58
58
|
"devDependencies": {
|
|
59
|
-
"@infinitetoken/eslint-config": "^0.1
|
|
60
|
-
"@infinitetoken/jest-config": "^0.
|
|
61
|
-
"@infinitetoken/tsconfig": "^0.
|
|
59
|
+
"@infinitetoken/eslint-config": "^0.2.1",
|
|
60
|
+
"@infinitetoken/jest-config": "^0.5.2",
|
|
61
|
+
"@infinitetoken/tsconfig": "^0.6.0",
|
|
62
62
|
"@types/jest": "^30.0.0",
|
|
63
63
|
"@types/node": "^25.0.0",
|
|
64
64
|
"eslint": "^10.2.1",
|