@remit/data-ports 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,28 @@
1
+ import type { IAddressRepository } from "./address.js";
2
+ import type { IEnvelopeRepository } from "./envelope.js";
3
+ import type { IMessageRepository } from "./message.js";
4
+ import type { IThreadMessageRepository } from "./thread-message.js";
5
+
6
+ /**
7
+ * The repositories a `saveMessage` write set touches, bound to a single unit of
8
+ * work. On Postgres these are transaction-bound so every write — including the
9
+ * transactional-outbox rows the message write appends — commits or rolls back
10
+ * together.
11
+ */
12
+ export interface UnitOfWorkRepositories {
13
+ message: IMessageRepository;
14
+ envelope: IEnvelopeRepository;
15
+ address: IAddressRepository;
16
+ threadMessage: IThreadMessageRepository;
17
+ }
18
+
19
+ /**
20
+ * Runs a set of related writes as one atomic unit. The callback receives
21
+ * repositories bound to the unit of work; a throw rolls the whole set back.
22
+ *
23
+ * Backends without cross-entity transactions supply a pass-through
24
+ * implementation that runs the callback against the plain repositories.
25
+ */
26
+ export interface IUnitOfWork {
27
+ transaction<T>(fn: (repos: UnitOfWorkRepositories) => Promise<T>): Promise<T>;
28
+ }
package/src/types.ts ADDED
@@ -0,0 +1,494 @@
1
+ import type {
2
+ AccountConfigSchema,
3
+ AccountSchema,
4
+ AddressSchema,
5
+ BodyPartContentSchema,
6
+ BodyPartParameterSchema,
7
+ BodyPartSchema,
8
+ BodyPartStorageSchema,
9
+ EnvelopeAddressSchema,
10
+ EnvelopeSchema,
11
+ FilterAnchorSchema,
12
+ FilterSchema,
13
+ LabelSchema,
14
+ MailboxLockSchema,
15
+ MailboxSchema,
16
+ MailboxSpecialUseEntrySchema,
17
+ MailboxSpecialUseSchema,
18
+ MessageFlagPushSchema,
19
+ MessageFlagSchema,
20
+ MessageLabelSchema,
21
+ MessagePlacementMoveSchema,
22
+ MessageSchema,
23
+ OrganizeJobRequestSchema,
24
+ OutboxMessageSchema,
25
+ RawMessageStorageSchema,
26
+ ThreadMessageSchema,
27
+ } from "@remit/api-zod-schemas";
28
+ import type { z } from "zod";
29
+
30
+ export type ResultList<T> = {
31
+ items: T[];
32
+ continuationToken: string | undefined;
33
+ };
34
+
35
+ /**
36
+ * Page shape for internal, non-API-facing pagination (e.g. the scheduled-sync
37
+ * tick paging through every account). `cursor` is a raw backend-native token —
38
+ * unlike `ResultList.continuationToken` it carries no salt/tamper check, since
39
+ * it never crosses a trust boundary. `null` means no further pages.
40
+ */
41
+ export type AccountSchedulerPage = {
42
+ items: AccountItem[];
43
+ cursor: string | null;
44
+ };
45
+
46
+ export type ListOptions = {
47
+ limit?: number;
48
+ continuationToken?: string;
49
+ };
50
+
51
+ export type AccountItem = z.infer<typeof AccountSchema>;
52
+ export type AccountConfigItem = z.infer<typeof AccountConfigSchema>;
53
+ export type MailboxItem = z.infer<typeof MailboxSchema>;
54
+ export type ThreadMessageItem = z.infer<typeof ThreadMessageSchema>;
55
+ export type OutboxMessageItem = z.infer<typeof OutboxMessageSchema>;
56
+ export type MessageItem = z.infer<typeof MessageSchema>;
57
+ export type MessageFlagItem = z.infer<typeof MessageFlagSchema>;
58
+ export type BodyPartItem = z.infer<typeof BodyPartSchema>;
59
+ export type MessageReferenceItem = {
60
+ messageReferenceId: string;
61
+ messageId: string;
62
+ envelopeId: string;
63
+ messageIdValue: string;
64
+ referenceType: string;
65
+ referenceOrder: number;
66
+ createdAt: number;
67
+ updatedAt: number;
68
+ };
69
+ export type EnvelopeAddressItem = z.infer<typeof EnvelopeAddressSchema>;
70
+ export type BodyPartParameterItem = z.infer<typeof BodyPartParameterSchema>;
71
+ export type RawMessageStorageItem = z.infer<typeof RawMessageStorageSchema>;
72
+ export type BodyPartStorageItem = z.infer<typeof BodyPartStorageSchema>;
73
+ export type BodyPartContentItem = z.infer<typeof BodyPartContentSchema>;
74
+ export type EnvelopeItem = z.infer<typeof EnvelopeSchema>;
75
+ export type MailboxLockItem = z.infer<typeof MailboxLockSchema>;
76
+ export type MailboxSpecialUseItem = z.infer<
77
+ typeof MailboxSpecialUseEntrySchema
78
+ >;
79
+ export type AddressItem = z.infer<typeof AddressSchema>;
80
+ export type LabelItem = z.infer<typeof LabelSchema>;
81
+ export type MessageLabelItem = z.infer<typeof MessageLabelSchema>;
82
+ export type FilterItem = z.infer<typeof FilterSchema>;
83
+ export type FilterAnchorItem = z.infer<typeof FilterAnchorSchema>;
84
+ export type MessagePlacementMoveItem = z.infer<
85
+ typeof MessagePlacementMoveSchema
86
+ >;
87
+ export type MessageFlagPushItem = z.infer<typeof MessageFlagPushSchema>;
88
+
89
+ export type MailboxSpecialUseValue = z.infer<typeof MailboxSpecialUseSchema>;
90
+
91
+ export type AccountSettingValue =
92
+ | { kind: "Boolean"; value: boolean }
93
+ | { kind: "String"; value: string }
94
+ | { kind: "Number"; value: number }
95
+ | { kind: "StringList"; value: string[] }
96
+ | { kind: "Map"; value: Record<string, string> }
97
+ | {
98
+ kind: "MutedFlag";
99
+ value: {
100
+ value: boolean;
101
+ setAt: number;
102
+ setBy?: string;
103
+ expiresAt?: number;
104
+ reason?: string;
105
+ };
106
+ };
107
+
108
+ export type AccountSettingItem = {
109
+ accountSettingId: string;
110
+ accountConfigId: string;
111
+ name: string;
112
+ value: AccountSettingValue;
113
+ createdAt: number;
114
+ updatedAt: number;
115
+ };
116
+
117
+ export type AccountExportRequestItem = {
118
+ accountExportRequestId: string;
119
+ accountConfigId: string;
120
+ userId: string;
121
+ state: "Failed" | "Pending" | "Processing" | "Ready";
122
+ createdAt: number;
123
+ updatedAt: number;
124
+ expiresAt?: number;
125
+ objectKey?: string;
126
+ downloadUrl?: string;
127
+ errorMessage?: string;
128
+ };
129
+
130
+ export type AccountDescription = {
131
+ account: AccountItem[];
132
+ mailbox: MailboxItem[];
133
+ };
134
+
135
+ export type AccountConfigDescription = {
136
+ accountConfig: AccountConfigItem[];
137
+ account: AccountItem[];
138
+ address: AddressItem[];
139
+ };
140
+
141
+ export type MessageData = {
142
+ envelope: EnvelopeItem[];
143
+ messageReference: MessageReferenceItem[];
144
+ envelopeAddress: EnvelopeAddressItem[];
145
+ bodyPart: BodyPartItem[];
146
+ bodyPartParameter: BodyPartParameterItem[];
147
+ rawMessageStorage: RawMessageStorageItem[];
148
+ bodyPartStorage: BodyPartStorageItem[];
149
+ bodyPartContent: BodyPartContentItem[];
150
+ };
151
+
152
+ export type MessageDescription = {
153
+ message: MessageItem[];
154
+ messageFlag: MessageFlagItem[];
155
+ envelope: EnvelopeItem[];
156
+ messageReference: MessageReferenceItem[];
157
+ envelopeAddress: EnvelopeAddressItem[];
158
+ bodyPart: BodyPartItem[];
159
+ bodyPartParameter: BodyPartParameterItem[];
160
+ rawMessageStorage: RawMessageStorageItem[];
161
+ bodyPartStorage: BodyPartStorageItem[];
162
+ bodyPartContent: BodyPartContentItem[];
163
+ };
164
+
165
+ export type WithMailboxLockResult<T> = {
166
+ executed: boolean;
167
+ result?: T;
168
+ };
169
+
170
+ export type CreateAccountInput = Omit<
171
+ AccountItem,
172
+ | "accountId"
173
+ | "createdAt"
174
+ | "updatedAt"
175
+ | "authType"
176
+ | "smtpEnabled"
177
+ | "smtpHost"
178
+ | "smtpPort"
179
+ | "smtpTls"
180
+ | "smtpStartTls"
181
+ | "smtpUsername"
182
+ > & {
183
+ accountId?: string;
184
+ authType?: AccountItem["authType"];
185
+ smtpEnabled?: AccountItem["smtpEnabled"];
186
+ smtpHost?: AccountItem["smtpHost"];
187
+ smtpPort?: AccountItem["smtpPort"];
188
+ smtpTls?: AccountItem["smtpTls"];
189
+ smtpStartTls?: AccountItem["smtpStartTls"];
190
+ smtpUsername?: AccountItem["smtpUsername"];
191
+ };
192
+
193
+ export type UpdateAccountInput = Partial<
194
+ Omit<CreateAccountInput, "accountConfigId">
195
+ >;
196
+
197
+ export type CreateAccountConfigInput = Omit<
198
+ AccountConfigItem,
199
+ "accountConfigId" | "createdAt" | "updatedAt" | "state"
200
+ > & { accountConfigId?: string; state?: AccountConfigItem["state"] };
201
+
202
+ export type UpdateAccountConfigInput = Partial<
203
+ Omit<CreateAccountConfigInput, "userId">
204
+ >;
205
+
206
+ export type CreateAccountExportRequestInput = Omit<
207
+ AccountExportRequestItem,
208
+ "accountExportRequestId" | "createdAt" | "updatedAt"
209
+ >;
210
+
211
+ export type UpdateAccountExportRequestInput = Partial<
212
+ Pick<AccountExportRequestItem, "state" | "expiresAt" | "errorMessage"> & {
213
+ objectKey?: string;
214
+ }
215
+ >;
216
+
217
+ export type OrganizeJobRequestItem = z.infer<typeof OrganizeJobRequestSchema>;
218
+
219
+ export type CreateOrganizeJobRequestInput = Omit<
220
+ OrganizeJobRequestItem,
221
+ | "organizeJobId"
222
+ | "createdAt"
223
+ | "updatedAt"
224
+ | "state"
225
+ | "matchedCount"
226
+ | "appliedCount"
227
+ | "failedCount"
228
+ | "errorMessage"
229
+ > & { state?: OrganizeJobRequestItem["state"] };
230
+
231
+ export type UpdateOrganizeJobRequestInput = Partial<
232
+ Pick<
233
+ OrganizeJobRequestItem,
234
+ "state" | "matchedCount" | "appliedCount" | "failedCount" | "errorMessage"
235
+ >
236
+ >;
237
+
238
+ export type UpsertAccountSettingInput = {
239
+ accountConfigId: string;
240
+ name: string;
241
+ value: AccountSettingValue;
242
+ };
243
+
244
+ export type AddressFlags = NonNullable<AddressItem["flags"]>;
245
+ export type FlagsMergePatch = {
246
+ [K in keyof AddressFlags]?: AddressFlags[K] | null;
247
+ };
248
+
249
+ export type CreateAddressInput = Omit<
250
+ AddressItem,
251
+ | "addressId"
252
+ | "createdAt"
253
+ | "updatedAt"
254
+ | "flags"
255
+ | "inboundCount"
256
+ | "outboundCount"
257
+ | "replyCount"
258
+ | "lastInboundAt"
259
+ | "lastReplyAt"
260
+ > & {
261
+ addressId: string;
262
+ flags?: AddressFlags;
263
+ inboundCount?: AddressItem["inboundCount"];
264
+ outboundCount?: AddressItem["outboundCount"];
265
+ replyCount?: AddressItem["replyCount"];
266
+ lastInboundAt?: AddressItem["lastInboundAt"];
267
+ lastReplyAt?: AddressItem["lastReplyAt"];
268
+ };
269
+
270
+ export type UpdateAddressInput = Partial<
271
+ Omit<CreateAddressInput, "accountConfigId">
272
+ >;
273
+
274
+ export type CreateEnvelopeAddressInput = Omit<
275
+ EnvelopeAddressItem,
276
+ "envelopeAddressId" | "createdAt" | "updatedAt"
277
+ > & { envelopeAddressId: string };
278
+
279
+ export type CreateEnvelopeInput = Omit<
280
+ EnvelopeItem,
281
+ "envelopeId" | "createdAt" | "updatedAt"
282
+ > & { envelopeId: string };
283
+
284
+ export type UpdateEnvelopeInput = Partial<
285
+ Omit<CreateEnvelopeInput, "messageId">
286
+ >;
287
+
288
+ export type BodyPartParameterUpsertInput = {
289
+ parameterName: string;
290
+ parameterValue: string;
291
+ };
292
+
293
+ export type BodyPartUpsertInput = {
294
+ partPath: string;
295
+ parentPartPath: string | null;
296
+ mediaType: BodyPartItem["mediaType"];
297
+ mediaSubtype: string;
298
+ contentId?: string;
299
+ contentDescription?: string;
300
+ transferEncoding: BodyPartItem["transferEncoding"];
301
+ sizeOctets: number;
302
+ lineCount?: number;
303
+ md5Hash?: string;
304
+ disposition?: BodyPartItem["disposition"];
305
+ dispositionFilename?: string;
306
+ language?: string;
307
+ location?: string;
308
+ isMultipart: boolean;
309
+ multipartSubtype?: BodyPartItem["multipartSubtype"];
310
+ parameters: BodyPartParameterUpsertInput[];
311
+ };
312
+
313
+ export type BodyPartContentUpsertInput = {
314
+ bodyPartId: string;
315
+ content: string;
316
+ };
317
+
318
+ export type CreateMailboxInput = Omit<
319
+ MailboxItem,
320
+ | "mailboxId"
321
+ | "createdAt"
322
+ | "updatedAt"
323
+ | "namespaceType"
324
+ | "parentMailboxId"
325
+ | "cursorState"
326
+ > & {
327
+ namespaceType?: MailboxItem["namespaceType"];
328
+ parentMailboxId?: MailboxItem["parentMailboxId"];
329
+ /** Total per RFC 032 (defaults to `normal`) — optional at the input boundary, callers rarely set it explicitly. */
330
+ cursorState?: MailboxItem["cursorState"];
331
+ };
332
+
333
+ export type UpdateMailboxInput = Partial<Omit<CreateMailboxInput, "accountId">>;
334
+
335
+ export type CreateMessageInput = Omit<
336
+ MessageItem,
337
+ | "messageId"
338
+ | "createdAt"
339
+ | "updatedAt"
340
+ | "status"
341
+ | "syncStatus"
342
+ | "category"
343
+ | "hasListUnsubscribe"
344
+ | "movedByRemit"
345
+ > & {
346
+ messageId: string;
347
+ status?: MessageItem["status"];
348
+ syncStatus?: MessageItem["syncStatus"];
349
+ category?: MessageItem["category"];
350
+ hasListUnsubscribe?: MessageItem["hasListUnsubscribe"];
351
+ movedByRemit?: MessageItem["movedByRemit"];
352
+ };
353
+
354
+ export type UpdateMessageInput = Partial<
355
+ Omit<CreateMessageInput, "mailboxId" | "uid">
356
+ >;
357
+
358
+ export type UpdateMessageMoveInput = Partial<
359
+ Pick<
360
+ CreateMessageInput,
361
+ | "mailboxId"
362
+ | "uid"
363
+ | "status"
364
+ | "syncStatus"
365
+ | "originalMailboxId"
366
+ | "originalUid"
367
+ >
368
+ >;
369
+
370
+ export type MessageIdSource = {
371
+ messageId?: string;
372
+ uid: number;
373
+ mailboxId: string;
374
+ date?: string;
375
+ subject?: string;
376
+ fromMailbox?: string;
377
+ fromHost?: string;
378
+ };
379
+
380
+ export type CreateMessageFlagInput = Omit<
381
+ MessageFlagItem,
382
+ "messageFlagId" | "createdAt" | "updatedAt"
383
+ >;
384
+
385
+ export type PutMessagePlacementMoveInput = Omit<
386
+ MessagePlacementMoveItem,
387
+ "state" | "createdAt" | "updatedAt"
388
+ >;
389
+
390
+ export type PutMessageFlagPushInput = Omit<
391
+ MessageFlagPushItem,
392
+ "state" | "createdAt" | "updatedAt"
393
+ >;
394
+
395
+ export type CreateOutboxMessageInput = Omit<
396
+ OutboxMessageItem,
397
+ | "outboxMessageId"
398
+ | "createdAt"
399
+ | "updatedAt"
400
+ | "ccAddresses"
401
+ | "bccAddresses"
402
+ | "references"
403
+ > & {
404
+ ccAddresses?: string[];
405
+ bccAddresses?: string[];
406
+ references?: string[];
407
+ };
408
+
409
+ export type UpdateOutboxMessageInput = Partial<
410
+ Pick<
411
+ OutboxMessageItem,
412
+ | "status"
413
+ | "lastError"
414
+ | "lastSmtpCode"
415
+ | "sentAt"
416
+ | "smtpMessageId"
417
+ | "toAddresses"
418
+ | "ccAddresses"
419
+ | "bccAddresses"
420
+ | "subject"
421
+ | "textBody"
422
+ | "htmlBody"
423
+ | "inReplyTo"
424
+ | "references"
425
+ >
426
+ >;
427
+
428
+ export type CreateThreadMessageInput = Omit<
429
+ ThreadMessageItem,
430
+ "threadMessageId" | "createdAt" | "updatedAt" | "star" | "category"
431
+ > & {
432
+ star?: ThreadMessageItem["star"];
433
+ category?: ThreadMessageItem["category"];
434
+ };
435
+
436
+ export type UpdateThreadMessageInput = Partial<
437
+ Omit<CreateThreadMessageInput, "accountConfigId" | "threadId" | "messageId">
438
+ >;
439
+
440
+ export type SearchOptions = {
441
+ query?: string;
442
+ subject?: string;
443
+ from?: string;
444
+ unread?: boolean;
445
+ starred?: boolean;
446
+ attachments?: boolean;
447
+ };
448
+
449
+ export type CreateLabelInput = Omit<
450
+ LabelItem,
451
+ "labelId" | "normalizedName" | "createdAt" | "updatedAt" | "color"
452
+ > & { color?: LabelItem["color"] };
453
+
454
+ export type UpdateLabelInput = Partial<
455
+ Pick<LabelItem, "name" | "normalizedName" | "color">
456
+ >;
457
+
458
+ export type CreateMessageLabelInput = Omit<
459
+ MessageLabelItem,
460
+ "messageLabelId" | "createdAt" | "updatedAt" | "appliedByFilterId"
461
+ > & { appliedByFilterId?: MessageLabelItem["appliedByFilterId"] };
462
+
463
+ export type CreateFilterInput = Omit<
464
+ FilterItem,
465
+ | "filterId"
466
+ | "createdAt"
467
+ | "updatedAt"
468
+ | "state"
469
+ | "hasAnchor"
470
+ | "ruleChangedAt"
471
+ | "matchOperator"
472
+ | "literalClauses"
473
+ | "actionLabelId"
474
+ | "actionMailboxId"
475
+ > & {
476
+ state?: FilterItem["state"];
477
+ hasAnchor?: FilterItem["hasAnchor"];
478
+ matchOperator?: FilterItem["matchOperator"];
479
+ literalClauses?: FilterItem["literalClauses"];
480
+ actionLabelId?: FilterItem["actionLabelId"];
481
+ actionMailboxId?: FilterItem["actionMailboxId"];
482
+ };
483
+
484
+ // ruleChangedAt is never client-supplied: FilterService derives it, bumping
485
+ // only when a predicate/action field is present in the update (RFC 034
486
+ // Decision 3.2) — a cosmetic `name` rename must not move it.
487
+ export type UpdateFilterInput = Partial<
488
+ Omit<CreateFilterInput, "accountConfigId" | "scope">
489
+ >;
490
+
491
+ export type CreateFilterAnchorInput = Omit<
492
+ FilterAnchorItem,
493
+ "createdAt" | "updatedAt"
494
+ >;
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Wellknown promotion rule (issue #233).
3
+ *
4
+ * Pure predicate over an Address engagement snapshot. Returns `true` when
5
+ * the address should be auto-promoted to `flags.wellknown.value === true`.
6
+ *
7
+ * Three conjuncts:
8
+ * 1. `flags.wellknown.value` is not already `true` (idempotent).
9
+ * 2. Engagement threshold:
10
+ * - `replyCount >= 1` always promotes (a real two-way conversation).
11
+ * - `inboundCount >= 3` promotes ONLY for non-bulk senders. Bulk senders
12
+ * (newsletters, marketing, automated, or List-Unsubscribe present) reach
13
+ * high inbound counts by volume alone — that is not engagement. Only a
14
+ * real reply from the user (`replyCount >= 1`) promotes a bulk sender.
15
+ * 3. `lastInboundAt` exists and is within the last 90 days
16
+ * (keeps dormant senders from being promoted by stale counters).
17
+ *
18
+ * The body-sync inbound path always writes `lastInboundAt = now`, so the
19
+ * 90-day check trivially passes there. The SMTP reply path reads the
20
+ * existing `lastInboundAt` (the user is replying to an inbound, which
21
+ * implies a recent inbound, but we still gate on it explicitly).
22
+ */
23
+
24
+ export const WELLKNOWN_INBOUND_THRESHOLD = 3;
25
+ export const WELLKNOWN_REPLY_THRESHOLD = 1;
26
+ export const WELLKNOWN_INBOUND_WINDOW_MS = 90 * 24 * 60 * 60 * 1000;
27
+
28
+ /**
29
+ * Categories and signals that make a sender "bulk". Bulk senders cannot be
30
+ * promoted by inbound volume alone — only a real reply promotes them.
31
+ */
32
+ const BULK_CATEGORIES: ReadonlySet<string> = new Set([
33
+ "newsletter",
34
+ "marketing",
35
+ "automated",
36
+ ]);
37
+
38
+ export interface WellknownEngagementSnapshot {
39
+ inboundCount?: number;
40
+ replyCount?: number;
41
+ lastInboundAt?: number;
42
+ /**
43
+ * Whether this sender is a bulk/automated sender (newsletter, marketing,
44
+ * automated category, or List-Unsubscribe header present). Bulk senders
45
+ * can only be promoted via `replyCount >= 1`, not by inbound volume.
46
+ */
47
+ isBulk?: boolean;
48
+ flags?: {
49
+ wellknown?: { value: boolean } | undefined;
50
+ };
51
+ }
52
+
53
+ export const shouldPromoteWellknown = (
54
+ snapshot: WellknownEngagementSnapshot,
55
+ now: number,
56
+ ): boolean => {
57
+ if (snapshot.flags?.wellknown?.value === true) return false;
58
+
59
+ const replyCount = snapshot.replyCount ?? 0;
60
+ const inboundCount = snapshot.inboundCount ?? 0;
61
+ const replied = replyCount >= WELLKNOWN_REPLY_THRESHOLD;
62
+ // Bulk senders are only promotable via a real reply, not inbound volume.
63
+ const inboundEnough =
64
+ !snapshot.isBulk && inboundCount >= WELLKNOWN_INBOUND_THRESHOLD;
65
+ if (!replied && !inboundEnough) return false;
66
+
67
+ const lastInboundAt = snapshot.lastInboundAt;
68
+ if (lastInboundAt === undefined) return false;
69
+ if (now - lastInboundAt > WELLKNOWN_INBOUND_WINDOW_MS) return false;
70
+
71
+ return true;
72
+ };
73
+
74
+ /**
75
+ * Returns true when the category or presence of List-Unsubscribe marks the
76
+ * sender as bulk, meaning they cannot be promoted by inbound volume alone.
77
+ */
78
+ export function isBulkSender(
79
+ category: string | undefined,
80
+ hasListUnsubscribe: boolean,
81
+ ): boolean {
82
+ if (hasListUnsubscribe) return true;
83
+ if (category != null && BULK_CATEGORIES.has(category)) return true;
84
+ return false;
85
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,8 @@
1
+ {
2
+ "extends": "../../tsconfig.json",
3
+ "compilerOptions": {
4
+ "outDir": "dist",
5
+ "rootDir": "src"
6
+ },
7
+ "include": ["src/**/*.ts"]
8
+ }