@remit/backend 0.0.89 → 0.0.90
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@remit/backend",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.90",
|
|
4
4
|
"description": "Remit Mail Inspector API backend",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "",
|
|
@@ -19,6 +19,10 @@
|
|
|
19
19
|
"types": "./src/service/create-remit-client.ts",
|
|
20
20
|
"default": "./src/service/create-remit-client.ts"
|
|
21
21
|
},
|
|
22
|
+
"./folder-role-appointments": {
|
|
23
|
+
"types": "./src/handlers/folder-role-appointments.ts",
|
|
24
|
+
"default": "./src/handlers/folder-role-appointments.ts"
|
|
25
|
+
},
|
|
22
26
|
"./organize": {
|
|
23
27
|
"types": "./src/service/organize.ts",
|
|
24
28
|
"default": "./src/service/organize.ts"
|
package/src/handlers/config.ts
CHANGED
|
@@ -2,16 +2,23 @@ import type { SQSClient } from "@aws-sdk/client-sqs";
|
|
|
2
2
|
import type {
|
|
3
3
|
AccountConfigResponse,
|
|
4
4
|
ConfigDescriptionResponse,
|
|
5
|
+
ConfigImportReport,
|
|
5
6
|
} from "@remit/api-openapi-types";
|
|
6
7
|
import type { ReaderConfigDocument } from "@remit/config-format";
|
|
7
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
importConfig,
|
|
10
|
+
pendingImportOf,
|
|
11
|
+
readConfigForExport,
|
|
12
|
+
} from "@remit/config-transfer";
|
|
8
13
|
import type { AccountConfigItem, MailboxItem } from "@remit/data-ports";
|
|
9
|
-
import { NotFoundError } from "@remit/data-ports/errors";
|
|
14
|
+
import { ConfigNotEmptyError, NotFoundError } from "@remit/data-ports/errors";
|
|
15
|
+
import type { CanonicalMailboxRoleValue } from "@remit/data-ports/folder-role";
|
|
10
16
|
import { logger } from "@remit/logger-lambda";
|
|
11
17
|
import type { APIGatewayProxyEvent } from "aws-lambda";
|
|
12
18
|
import { env } from "expect-env";
|
|
13
19
|
import type { Context } from "openapi-backend";
|
|
14
20
|
import { getAccountConfigIdFromEvent, getSubFromEvent } from "../auth.js";
|
|
21
|
+
import { embedAnchorText } from "../service/config-import.js";
|
|
15
22
|
import { getClient } from "../service/data-client.js";
|
|
16
23
|
import { exportIdentity } from "../service/export-identity.js";
|
|
17
24
|
import { fireAndForget } from "../service/fire-and-forget.js";
|
|
@@ -30,6 +37,7 @@ import {
|
|
|
30
37
|
import {
|
|
31
38
|
groupFolderAppointmentsByAccount,
|
|
32
39
|
resolveFolderAppointments,
|
|
40
|
+
writeFolderRoleAppointment,
|
|
33
41
|
} from "./folder-role-appointments.js";
|
|
34
42
|
|
|
35
43
|
type StructuredLog = (fields: Record<string, unknown>, message: string) => void;
|
|
@@ -204,8 +212,15 @@ export const ConfigOperations: Record<
|
|
|
204
212
|
activeAccounts.map((acc) => acc.accountId),
|
|
205
213
|
);
|
|
206
214
|
|
|
215
|
+
// An import that named folders IMAP had not produced yet rides the config
|
|
216
|
+
// read rather than a route of its own, so nothing has to poll for it.
|
|
217
|
+
const pendingImport = pendingImportOf(
|
|
218
|
+
await client.configImport.listByAccountConfig(accountConfigId),
|
|
219
|
+
);
|
|
220
|
+
|
|
207
221
|
return {
|
|
208
222
|
accountConfig: toAccountConfigResponse(accountConfig),
|
|
223
|
+
...(pendingImport ? { pendingImport } : {}),
|
|
209
224
|
accounts: activeAccounts.map((acc) =>
|
|
210
225
|
toAccountResponse(
|
|
211
226
|
acc,
|
|
@@ -234,4 +249,59 @@ export const ConfigOperations: Record<
|
|
|
234
249
|
);
|
|
235
250
|
return { schemaVersion: document.schemaVersion, document };
|
|
236
251
|
},
|
|
252
|
+
|
|
253
|
+
ConfigOperations_importConfig: async (
|
|
254
|
+
context: Context,
|
|
255
|
+
...args: unknown[]
|
|
256
|
+
): Promise<ConfigImportReport> => {
|
|
257
|
+
const event = args[0] as APIGatewayProxyEvent;
|
|
258
|
+
const accountConfigId = getAccountConfigIdFromEvent(event);
|
|
259
|
+
const body = (context.request.requestBody ?? {}) as {
|
|
260
|
+
mode?: "validate" | "apply";
|
|
261
|
+
onExisting?: "abort" | "merge";
|
|
262
|
+
document?: unknown;
|
|
263
|
+
};
|
|
264
|
+
const client = await getClient();
|
|
265
|
+
|
|
266
|
+
const outcome = await importConfig(
|
|
267
|
+
{
|
|
268
|
+
repositories: client,
|
|
269
|
+
// Passed through as-is, undefined included: a backend with no
|
|
270
|
+
// cross-entity transaction writes without one, and the report words
|
|
271
|
+
// what survived a failure from whether this is here.
|
|
272
|
+
transaction: client.writeSet,
|
|
273
|
+
appointFolderRole: (
|
|
274
|
+
configId,
|
|
275
|
+
accountId,
|
|
276
|
+
role,
|
|
277
|
+
mailboxId,
|
|
278
|
+
lastKnownPath,
|
|
279
|
+
) =>
|
|
280
|
+
writeFolderRoleAppointment(
|
|
281
|
+
client.accountSetting,
|
|
282
|
+
configId,
|
|
283
|
+
accountId,
|
|
284
|
+
role as CanonicalMailboxRoleValue,
|
|
285
|
+
mailboxId,
|
|
286
|
+
lastKnownPath,
|
|
287
|
+
),
|
|
288
|
+
embedAnchor: embedAnchorText,
|
|
289
|
+
},
|
|
290
|
+
{
|
|
291
|
+
accountConfigId,
|
|
292
|
+
userId: getSubFromEvent(event) ?? accountConfigId,
|
|
293
|
+
document: body.document,
|
|
294
|
+
mode: body.mode ?? "validate",
|
|
295
|
+
onExisting: body.onExisting ?? "abort",
|
|
296
|
+
},
|
|
297
|
+
);
|
|
298
|
+
|
|
299
|
+
if (outcome.outcome === "conflict") {
|
|
300
|
+
throw new ConfigNotEmptyError(
|
|
301
|
+
outcome.conflict.message,
|
|
302
|
+
outcome.conflict.details,
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
return outcome.report;
|
|
306
|
+
},
|
|
237
307
|
};
|
package/src/handlers/filter.ts
CHANGED
|
@@ -3,10 +3,11 @@ import type {
|
|
|
3
3
|
FilterResponse,
|
|
4
4
|
UpdateFilterInput as UpdateFilterRequestBody,
|
|
5
5
|
} from "@remit/api-openapi-types";
|
|
6
|
-
import
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
6
|
+
import {
|
|
7
|
+
deriveFilterTtl,
|
|
8
|
+
type FilterItem,
|
|
9
|
+
type IFilterAnchorTransaction,
|
|
10
|
+
type UpdateFilterInput,
|
|
10
11
|
} from "@remit/data-ports";
|
|
11
12
|
import { BadRequestError } from "@remit/data-ports/errors";
|
|
12
13
|
import { FilterScope, FilterState } from "@remit/domain-enums";
|
|
@@ -41,24 +42,6 @@ export interface FilterCrudDeps {
|
|
|
41
42
|
): Promise<AnchorPayload | null>;
|
|
42
43
|
}
|
|
43
44
|
|
|
44
|
-
/**
|
|
45
|
-
* Epoch-seconds `ttl` derived from `expiresAt`, set only for a `Temporary`
|
|
46
|
-
* filter (RFC 034 Decision 1.3). A `Standing` filter never carries `ttl` — the
|
|
47
|
-
* reserved table-wide TTL attribute must stay absent, or the row would be swept
|
|
48
|
-
* (Decision 1.4).
|
|
49
|
-
*/
|
|
50
|
-
export const deriveFilterTtl = (
|
|
51
|
-
scope: string,
|
|
52
|
-
expiresAt: string | undefined,
|
|
53
|
-
): number | undefined => {
|
|
54
|
-
if (scope !== FilterScope.Temporary || !expiresAt) return undefined;
|
|
55
|
-
const ms = new Date(expiresAt).getTime();
|
|
56
|
-
if (Number.isNaN(ms)) {
|
|
57
|
-
throw new BadRequestError(`Invalid expiresAt: ${expiresAt}`);
|
|
58
|
-
}
|
|
59
|
-
return Math.floor(ms / 1000);
|
|
60
|
-
};
|
|
61
|
-
|
|
62
45
|
/**
|
|
63
46
|
* Reduce a PATCH body to the fields a filter update may set (RFC 034, reader
|
|
64
47
|
* #266). Preserves absence: a key not present in the body is not present in
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
AccountRepo,
|
|
5
5
|
AccountSettingRepo,
|
|
6
6
|
AddressRepo,
|
|
7
|
+
ConfigImportRepo,
|
|
7
8
|
createSqliteDatabase,
|
|
8
9
|
DrizzleEnvelopeRepository,
|
|
9
10
|
DrizzleFilterAnchorTransaction,
|
|
@@ -25,6 +26,7 @@ import {
|
|
|
25
26
|
OutboxAttachmentRepo,
|
|
26
27
|
OutboxMessageRepo,
|
|
27
28
|
QuarantineRepo,
|
|
29
|
+
runInTransaction,
|
|
28
30
|
SenderSignerStandingRepo,
|
|
29
31
|
} from "@remit/drizzle-service";
|
|
30
32
|
import { env } from "expect-env";
|
|
@@ -65,6 +67,7 @@ export const buildSqliteClient = async (): Promise<RemitClient> => {
|
|
|
65
67
|
threadMessage: new DrizzleThreadMessageRepository(genericDb),
|
|
66
68
|
envelope: new DrizzleEnvelopeRepository(messageDataDb),
|
|
67
69
|
accountExportRequest: new AccountExportRequestRepo(genericDb),
|
|
70
|
+
configImport: new ConfigImportRepo(genericDb),
|
|
68
71
|
quarantine: new QuarantineRepo(genericDb),
|
|
69
72
|
organizeJobRequest: new OrganizeJobRequestRepo(genericDb),
|
|
70
73
|
placementMove: new MessagePlacementMoveRepo(genericDb),
|
|
@@ -76,6 +79,7 @@ export const buildSqliteClient = async (): Promise<RemitClient> => {
|
|
|
76
79
|
messageLabel: new MessageLabelRepo(genericDb),
|
|
77
80
|
senderSignerStanding: new SenderSignerStandingRepo(genericDb),
|
|
78
81
|
unitOfWork: new DrizzleUnitOfWork(messageDataDb),
|
|
82
|
+
writeSet: (run) => runInTransaction(genericDb, () => run()),
|
|
79
83
|
};
|
|
80
84
|
|
|
81
85
|
return createRemitClient({ repositories, ...buildSharedDeps() });
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { EmbedAnchor } from "@remit/config-transfer";
|
|
2
|
+
import { logger } from "@remit/logger-lambda";
|
|
3
|
+
import { buildEmbeddingServiceFromEnv } from "@remit/search-service/from-env";
|
|
4
|
+
import { isSemanticCapabilityAbsence } from "./semantic-capability.js";
|
|
5
|
+
|
|
6
|
+
type Embedder = {
|
|
7
|
+
embed: (texts: string[]) => Promise<number[][]>;
|
|
8
|
+
readonly embeddingId: string;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
let cached: Embedder | null = null;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Re-embed a filter's anchor from the source text its file carries. The vector
|
|
15
|
+
* never travels — it is a function of the model that produced it — so this is
|
|
16
|
+
* the one thing an import computes rather than copies.
|
|
17
|
+
*
|
|
18
|
+
* A deployment that ships no vector pipeline answers `undefined` rather than
|
|
19
|
+
* failing the import: the anchor then lands stamped as un-embedded, and the
|
|
20
|
+
* same lazy repair that handles a model migration builds its vector the first
|
|
21
|
+
* time the filter is matched. A configuration file has to be importable on the
|
|
22
|
+
* instance a person is recovering onto, whatever that instance can run.
|
|
23
|
+
*/
|
|
24
|
+
export const embedAnchorText: EmbedAnchor = async (sourceText) => {
|
|
25
|
+
try {
|
|
26
|
+
if (!cached) cached = buildEmbeddingServiceFromEnv();
|
|
27
|
+
const [embedding] = await cached.embed([sourceText]);
|
|
28
|
+
return embedding
|
|
29
|
+
? { embedding, embeddingId: cached.embeddingId }
|
|
30
|
+
: undefined;
|
|
31
|
+
} catch (error) {
|
|
32
|
+
if (!isSemanticCapabilityAbsence(error)) throw error;
|
|
33
|
+
logger.warn(
|
|
34
|
+
{ error: error instanceof Error ? error.message : String(error) },
|
|
35
|
+
"No embedding pipeline in this deployment; an imported filter anchor is stored as text and embedded on first use",
|
|
36
|
+
);
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
};
|
|
@@ -4,6 +4,7 @@ import type {
|
|
|
4
4
|
IAccountRepository,
|
|
5
5
|
IAccountSettingRepository,
|
|
6
6
|
IAddressRepository,
|
|
7
|
+
IConfigImportRepository,
|
|
7
8
|
IEnvelopeRepository,
|
|
8
9
|
IFilterAnchorRepository,
|
|
9
10
|
IFilterAnchorTransaction,
|
|
@@ -78,6 +79,11 @@ export interface RemitClient {
|
|
|
78
79
|
envelope: IEnvelopeRepository;
|
|
79
80
|
accountExportRequest: IAccountExportRequestRepository;
|
|
80
81
|
|
|
82
|
+
// Applied configuration imports (#1021), and the folder references each is
|
|
83
|
+
// still waiting for. Read by GET /config to surface what a file named but
|
|
84
|
+
// IMAP has not produced yet, and written by the binder when it does.
|
|
85
|
+
configImport: IConfigImportRepository;
|
|
86
|
+
|
|
81
87
|
// Messages the sync path could not read (issue #72). Read-only from the API
|
|
82
88
|
// process: the sync worker writes the rows, settings lists them.
|
|
83
89
|
quarantine: IQuarantineRepository;
|
|
@@ -163,6 +169,13 @@ export interface RemitClient {
|
|
|
163
169
|
|
|
164
170
|
// Helper to create IMAP connection scope from accountId
|
|
165
171
|
createConnectionScope: (accountId: string) => Promise<ConnectionScope>;
|
|
172
|
+
|
|
173
|
+
// Runs an arbitrary set of repository writes as one transaction. Absent on a
|
|
174
|
+
// backend with no cross-entity transaction, where the caller's contract is
|
|
175
|
+
// instead: validate before the first write, fail fast, and report what
|
|
176
|
+
// landed. The message-save write set has its own bound repositories and uses
|
|
177
|
+
// `unitOfWork`; this one takes the repos as they are.
|
|
178
|
+
writeSet?: <T>(run: () => Promise<T>) => Promise<T>;
|
|
166
179
|
}
|
|
167
180
|
|
|
168
181
|
export interface RemitClientRepositories {
|
|
@@ -180,6 +193,7 @@ export interface RemitClientRepositories {
|
|
|
180
193
|
threadMessage: IThreadMessageRepository;
|
|
181
194
|
envelope: IEnvelopeRepository;
|
|
182
195
|
accountExportRequest: IAccountExportRequestRepository;
|
|
196
|
+
configImport: IConfigImportRepository;
|
|
183
197
|
quarantine: IQuarantineRepository;
|
|
184
198
|
organizeJobRequest: IOrganizeJobRequestRepository;
|
|
185
199
|
placementMove: IMessagePlacementMoveRepository;
|
|
@@ -191,6 +205,7 @@ export interface RemitClientRepositories {
|
|
|
191
205
|
messageLabel: IMessageLabelRepository;
|
|
192
206
|
senderSignerStanding: ISenderSignerStandingRepository;
|
|
193
207
|
unitOfWork?: IUnitOfWork;
|
|
208
|
+
writeSet?: <T>(run: () => Promise<T>) => Promise<T>;
|
|
194
209
|
}
|
|
195
210
|
|
|
196
211
|
export interface RemitClientSharedDeps {
|
|
@@ -407,6 +422,7 @@ export const createRemitClient = (deps: RemitClientDeps): RemitClient => {
|
|
|
407
422
|
threadMessage: repositories.threadMessage,
|
|
408
423
|
envelope: repositories.envelope,
|
|
409
424
|
accountExportRequest: repositories.accountExportRequest,
|
|
425
|
+
configImport: repositories.configImport,
|
|
410
426
|
quarantine: repositories.quarantine,
|
|
411
427
|
organizeJobRequest: repositories.organizeJobRequest,
|
|
412
428
|
filter: repositories.filter,
|
|
@@ -416,6 +432,7 @@ export const createRemitClient = (deps: RemitClientDeps): RemitClient => {
|
|
|
416
432
|
messageLabel: repositories.messageLabel,
|
|
417
433
|
senderSignerStanding: repositories.senderSignerStanding,
|
|
418
434
|
unitOfWork: repositories.unitOfWork,
|
|
435
|
+
writeSet: repositories.writeSet,
|
|
419
436
|
|
|
420
437
|
storage,
|
|
421
438
|
search,
|
package/src/types.ts
CHANGED
|
@@ -13,6 +13,7 @@ export type OperationIds =
|
|
|
13
13
|
| "MeOperations_listQuarantine"
|
|
14
14
|
| "ConfigOperations_getConfig"
|
|
15
15
|
| "ConfigOperations_exportConfig"
|
|
16
|
+
| "ConfigOperations_importConfig"
|
|
16
17
|
| "SystemOperations_getSystemUpdate"
|
|
17
18
|
| "SystemOperations_applySystemUpdate"
|
|
18
19
|
| "AccountOperations_createAccount"
|