@remit/data-ports 0.0.27 → 0.0.29
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
package/src/conformance/index.ts
CHANGED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { after, before, describe, test } from "node:test";
|
|
3
|
+
import type { ISenderSignerStandingRepository } from "../interfaces/sender-signer-standing.js";
|
|
4
|
+
import type { RepositoryConformanceHarness } from "./harness.js";
|
|
5
|
+
|
|
6
|
+
export function senderSignerStandingRepositoryConformance(
|
|
7
|
+
harness: RepositoryConformanceHarness<ISenderSignerStandingRepository>,
|
|
8
|
+
): void {
|
|
9
|
+
describe("ISenderSignerStandingRepository conformance", () => {
|
|
10
|
+
let repo: ISenderSignerStandingRepository;
|
|
11
|
+
|
|
12
|
+
before(async () => {
|
|
13
|
+
repo = await harness.createRepository();
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
after(() => harness.teardown());
|
|
17
|
+
|
|
18
|
+
test("the first observation creates the row at a count of one", async () => {
|
|
19
|
+
const accountConfigId = harness.makeId();
|
|
20
|
+
const observedAt = 1_700_000_000_000;
|
|
21
|
+
|
|
22
|
+
const standing = await repo.observe({
|
|
23
|
+
accountConfigId,
|
|
24
|
+
senderKey: "example.com",
|
|
25
|
+
signerDomain: "esp.example",
|
|
26
|
+
observedAt,
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
assert.equal(standing.messageCount, 1);
|
|
30
|
+
assert.equal(standing.firstSeenAt, observedAt);
|
|
31
|
+
assert.equal(standing.lastSeenAt, observedAt);
|
|
32
|
+
assert.equal(standing.userAffirmedAt, 0);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test("a repeat observation increments the count", async () => {
|
|
36
|
+
const accountConfigId = harness.makeId();
|
|
37
|
+
const key = {
|
|
38
|
+
accountConfigId,
|
|
39
|
+
senderKey: "example.com",
|
|
40
|
+
signerDomain: "esp.example",
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
await repo.observe({ ...key, observedAt: 1_700_000_000_000 });
|
|
44
|
+
const second = await repo.observe({
|
|
45
|
+
...key,
|
|
46
|
+
observedAt: 1_700_000_060_000,
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
assert.equal(second.messageCount, 2);
|
|
50
|
+
assert.equal(
|
|
51
|
+
(await repo.get(accountConfigId, key.senderKey, key.signerDomain))
|
|
52
|
+
.messageCount,
|
|
53
|
+
2,
|
|
54
|
+
);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test("firstSeenAt survives every later observation while lastSeenAt advances", async () => {
|
|
58
|
+
const accountConfigId = harness.makeId();
|
|
59
|
+
const key = {
|
|
60
|
+
accountConfigId,
|
|
61
|
+
senderKey: "news.example.com",
|
|
62
|
+
signerDomain: "unverified",
|
|
63
|
+
};
|
|
64
|
+
const first = 1_700_000_000_000;
|
|
65
|
+
|
|
66
|
+
await repo.observe({ ...key, observedAt: first });
|
|
67
|
+
await repo.observe({ ...key, observedAt: first + 60_000 });
|
|
68
|
+
const third = await repo.observe({ ...key, observedAt: first + 120_000 });
|
|
69
|
+
|
|
70
|
+
assert.equal(third.messageCount, 3);
|
|
71
|
+
assert.equal(third.firstSeenAt, first);
|
|
72
|
+
assert.equal(third.lastSeenAt, first + 120_000);
|
|
73
|
+
|
|
74
|
+
const reread = await repo.get(
|
|
75
|
+
accountConfigId,
|
|
76
|
+
key.senderKey,
|
|
77
|
+
key.signerDomain,
|
|
78
|
+
);
|
|
79
|
+
assert.equal(reread.firstSeenAt, first);
|
|
80
|
+
assert.equal(reread.lastSeenAt, first + 120_000);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test("a key differing only in signer domain is a separate row", async () => {
|
|
84
|
+
const accountConfigId = harness.makeId();
|
|
85
|
+
const observedAt = 1_700_000_000_000;
|
|
86
|
+
|
|
87
|
+
await repo.observe({
|
|
88
|
+
accountConfigId,
|
|
89
|
+
senderKey: "example.com",
|
|
90
|
+
signerDomain: "esp-one.example",
|
|
91
|
+
observedAt,
|
|
92
|
+
});
|
|
93
|
+
const other = await repo.observe({
|
|
94
|
+
accountConfigId,
|
|
95
|
+
senderKey: "example.com",
|
|
96
|
+
signerDomain: "esp-two.example",
|
|
97
|
+
observedAt,
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
assert.equal(other.messageCount, 1);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test("get reports not-found for a key never observed, and does not create it", async () => {
|
|
104
|
+
const accountConfigId = harness.makeId();
|
|
105
|
+
|
|
106
|
+
await assert.rejects(
|
|
107
|
+
repo.get(accountConfigId, "stranger.example", "esp.example"),
|
|
108
|
+
(error) => harness.isNotFoundError(error),
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
await assert.rejects(
|
|
112
|
+
repo.get(accountConfigId, "stranger.example", "esp.example"),
|
|
113
|
+
(error) => harness.isNotFoundError(error),
|
|
114
|
+
);
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -26,6 +26,7 @@ export type {
|
|
|
26
26
|
export { holdsRoom } from "./interfaces/outbox-attachment.js";
|
|
27
27
|
export type { IOutboxMessageRepository } from "./interfaces/outbox-message.js";
|
|
28
28
|
export type { IQuarantineRepository } from "./interfaces/quarantine.js";
|
|
29
|
+
export type { ISenderSignerStandingRepository } from "./interfaces/sender-signer-standing.js";
|
|
29
30
|
export type { IThreadMessageRepository } from "./interfaces/thread-message.js";
|
|
30
31
|
export type {
|
|
31
32
|
IUnitOfWork,
|
|
@@ -85,6 +86,7 @@ export type {
|
|
|
85
86
|
MessageLabelItem,
|
|
86
87
|
MessagePlacementMoveItem,
|
|
87
88
|
MessageReferenceItem,
|
|
89
|
+
ObserveSenderSignerStandingInput,
|
|
88
90
|
OrganizeJobRequestItem,
|
|
89
91
|
OutboxAttachmentItem,
|
|
90
92
|
OutboxMessageItem,
|
|
@@ -96,6 +98,7 @@ export type {
|
|
|
96
98
|
RawMessageStorageItem,
|
|
97
99
|
ResultList,
|
|
98
100
|
SearchOptions,
|
|
101
|
+
SenderSignerStandingItem,
|
|
99
102
|
ThreadMessageItem,
|
|
100
103
|
UpdateAccountConfigInput,
|
|
101
104
|
UpdateAccountExportRequestInput,
|
|
@@ -61,14 +61,23 @@ export interface IAddressRepository {
|
|
|
61
61
|
accountConfigId: string;
|
|
62
62
|
limit?: number;
|
|
63
63
|
}): Promise<AddressItem[]>;
|
|
64
|
+
/**
|
|
65
|
+
* The account's addresses, most worth suggesting first: where the search term
|
|
66
|
+
* hit — the start of a value before the middle of one, and the display name
|
|
67
|
+
* before the local part, the domain and the whole address — then the account's
|
|
68
|
+
* standing for the sender, how much it corresponds with it, how recently, and
|
|
69
|
+
* the stored compound and id last so the order is total. Without a term every
|
|
70
|
+
* row scores the same match, so the remaining keys decide and the listing comes
|
|
71
|
+
* back by standing, correspondence and recency rather than alphabetically.
|
|
72
|
+
*/
|
|
64
73
|
listByAccountConfig(input: {
|
|
65
74
|
accountConfigId: string;
|
|
66
75
|
/**
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
76
|
+
* Matched as a substring of the display name, the local part, the domain
|
|
77
|
+
* and the whole address, so `po`, `locatie`, `amsterdam`, `pocahondas.nl`
|
|
78
|
+
* and `amsterdam@pocahondas.nl` all resolve
|
|
79
|
+
* `Pocahondas locatie amsterdam <amsterdam@pocahondas.nl>`. A name outside
|
|
80
|
+
* ASCII (`Öz` for `Özcan Bakker`) resolves through the folded compound.
|
|
72
81
|
*/
|
|
73
82
|
search?: string;
|
|
74
83
|
cursor?: string;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ObserveSenderSignerStandingInput,
|
|
3
|
+
SenderSignerStandingItem,
|
|
4
|
+
} from "../types.js";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Per-account record of how often one sender has been seen arriving under one
|
|
8
|
+
* DKIM signing identity.
|
|
9
|
+
*
|
|
10
|
+
* The authenticity derivation asks this port two things: whether a key already
|
|
11
|
+
* had standing when a message arrived, and — afterwards — that this message be
|
|
12
|
+
* counted. `get` answers the first and must not create anything; `observe`
|
|
13
|
+
* answers the second and is the only writer.
|
|
14
|
+
*/
|
|
15
|
+
export interface ISenderSignerStandingRepository {
|
|
16
|
+
/**
|
|
17
|
+
* Record one observation of a key, creating the row at `messageCount = 1`
|
|
18
|
+
* or incrementing an existing one.
|
|
19
|
+
*
|
|
20
|
+
* `firstSeenAt` is set once, on creation, and never moves — an
|
|
21
|
+
* implementation that rewrites it on the increment path erases the age the
|
|
22
|
+
* derivation reads standing from. `lastSeenAt` advances on every call.
|
|
23
|
+
*/
|
|
24
|
+
observe(
|
|
25
|
+
input: ObserveSenderSignerStandingInput,
|
|
26
|
+
): Promise<SenderSignerStandingItem>;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* The standing of one key. Throws the adapter's not-found error when the
|
|
30
|
+
* key has never been observed, and creates nothing.
|
|
31
|
+
*/
|
|
32
|
+
get(
|
|
33
|
+
accountConfigId: string,
|
|
34
|
+
senderKey: string,
|
|
35
|
+
signerDomain: string,
|
|
36
|
+
): Promise<SenderSignerStandingItem>;
|
|
37
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -25,6 +25,7 @@ import type {
|
|
|
25
25
|
OutboxMessageSchema,
|
|
26
26
|
QuarantineSchema,
|
|
27
27
|
RawMessageStorageSchema,
|
|
28
|
+
SenderSignerStandingSchema,
|
|
28
29
|
ThreadMessageSchema,
|
|
29
30
|
} from "@remit/api-zod-schemas";
|
|
30
31
|
import type { z } from "zod";
|
|
@@ -360,6 +361,7 @@ export type CreateMessageInput = Omit<
|
|
|
360
361
|
| "status"
|
|
361
362
|
| "syncStatus"
|
|
362
363
|
| "category"
|
|
364
|
+
| "authenticityVerdict"
|
|
363
365
|
| "hasListUnsubscribe"
|
|
364
366
|
| "movedByRemit"
|
|
365
367
|
> & {
|
|
@@ -367,6 +369,7 @@ export type CreateMessageInput = Omit<
|
|
|
367
369
|
status?: MessageItem["status"];
|
|
368
370
|
syncStatus?: MessageItem["syncStatus"];
|
|
369
371
|
category?: MessageItem["category"];
|
|
372
|
+
authenticityVerdict?: MessageItem["authenticityVerdict"];
|
|
370
373
|
hasListUnsubscribe?: MessageItem["hasListUnsubscribe"];
|
|
371
374
|
movedByRemit?: MessageItem["movedByRemit"];
|
|
372
375
|
};
|
|
@@ -527,3 +530,21 @@ export type CreateFilterAnchorInput = Omit<
|
|
|
527
530
|
FilterAnchorItem,
|
|
528
531
|
"createdAt" | "updatedAt"
|
|
529
532
|
>;
|
|
533
|
+
|
|
534
|
+
export type SenderSignerStandingItem = z.infer<
|
|
535
|
+
typeof SenderSignerStandingSchema
|
|
536
|
+
>;
|
|
537
|
+
|
|
538
|
+
/**
|
|
539
|
+
* One message observed arriving for a `(senderKey, signerDomain)` pair.
|
|
540
|
+
*
|
|
541
|
+
* `observedAt` is supplied rather than taken from the clock so a caller
|
|
542
|
+
* replaying a body-sync batch records the arrival's own time, and so the
|
|
543
|
+
* `firstSeenAt`-immutability contract is assertable without sleeping.
|
|
544
|
+
*/
|
|
545
|
+
export type ObserveSenderSignerStandingInput = {
|
|
546
|
+
accountConfigId: string;
|
|
547
|
+
senderKey: string;
|
|
548
|
+
signerDomain: string;
|
|
549
|
+
observedAt: number;
|
|
550
|
+
};
|