@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 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
- * An organization that owns apps and pays for member usage.
254
- * Team subscriptionLevel takes precedence over individual member levels.
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 TeamRecord {
257
- id: string;
258
- createdAt: string;
259
- updatedAt: string;
260
- name: string;
261
- color?: string;
262
- imageUrl?: string;
263
- subscriptionLevel: SubscriptionLevel;
264
- }
265
- /**
266
- * Membership of a user in a team.
267
- */
268
- interface TeamMemberRecord {
269
- id: string;
270
- createdAt: string;
271
- updatedAt: string;
272
- teamId: string;
273
- userId: string;
274
- role: 'owner' | 'admin' | 'member';
275
- }
276
- /**
277
- * Filter params for GET /teams.
278
- */
279
- interface TeamsQuery {
280
- sort?: string;
281
- limit?: number;
282
- cursor?: string;
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
- * An organization that owns apps and pays for member usage.
254
- * Team subscriptionLevel takes precedence over individual member levels.
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 TeamRecord {
257
- id: string;
258
- createdAt: string;
259
- updatedAt: string;
260
- name: string;
261
- color?: string;
262
- imageUrl?: string;
263
- subscriptionLevel: SubscriptionLevel;
264
- }
265
- /**
266
- * Membership of a user in a team.
267
- */
268
- interface TeamMemberRecord {
269
- id: string;
270
- createdAt: string;
271
- updatedAt: string;
272
- teamId: string;
273
- userId: string;
274
- role: 'owner' | 'admin' | 'member';
275
- }
276
- /**
277
- * Filter params for GET /teams.
278
- */
279
- interface TeamsQuery {
280
- sort?: string;
281
- limit?: number;
282
- cursor?: string;
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__ */ ((LogLevel3) => {
43
- LogLevel3[LogLevel3["Debug"] = 0] = "Debug";
44
- LogLevel3[LogLevel3["Info"] = 1] = "Info";
45
- LogLevel3[LogLevel3["Warn"] = 2] = "Warn";
46
- LogLevel3[LogLevel3["Error"] = 3] = "Error";
47
- LogLevel3[LogLevel3["Fatal"] = 4] = "Fatal";
48
- return LogLevel3;
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__ */ ((LogLevel3) => {
3
- LogLevel3[LogLevel3["Debug"] = 0] = "Debug";
4
- LogLevel3[LogLevel3["Info"] = 1] = "Info";
5
- LogLevel3[LogLevel3["Warn"] = 2] = "Warn";
6
- LogLevel3[LogLevel3["Error"] = 3] = "Error";
7
- LogLevel3[LogLevel3["Fatal"] = 4] = "Fatal";
8
- return LogLevel3;
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.4.22",
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.6",
60
- "@infinitetoken/jest-config": "^0.1.3",
61
- "@infinitetoken/tsconfig": "^0.3.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",