@krizaka/mail 0.0.0-stage → 0.1.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.
- package/LICENSE +201 -0
- package/README.md +37 -2
- package/dist/chunk-YDSARDFP.js +80 -0
- package/dist/chunk-YDSARDFP.js.map +1 -0
- package/dist/index.d.ts +186 -0
- package/dist/index.js +357 -0
- package/dist/index.js.map +1 -0
- package/dist/port-Pk4TboMq.d.ts +110 -0
- package/dist/settings.d.ts +18 -0
- package/dist/settings.js +31 -0
- package/dist/settings.js.map +1 -0
- package/dist/testkit.d.ts +7 -0
- package/dist/testkit.js +67 -0
- package/dist/testkit.js.map +1 -0
- package/package.json +84 -3
package/dist/testkit.js
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import {
|
|
2
|
+
SUPPRESSION_REASON,
|
|
3
|
+
hashAddress
|
|
4
|
+
} from "./chunk-YDSARDFP.js";
|
|
5
|
+
|
|
6
|
+
// src/testkit.ts
|
|
7
|
+
import { expect, it } from "vitest";
|
|
8
|
+
var T0 = /* @__PURE__ */ new Date("2026-10-26T12:00:00.000Z");
|
|
9
|
+
var day = (n) => new Date(T0.valueOf() + n * 24 * 3600 * 1e3);
|
|
10
|
+
var entry = (over = {}) => ({ at: T0, template: "reset", category: "transactional", locale: "fr", to: hashAddress("a@b.co"), status: "sent", ...over });
|
|
11
|
+
function mailStoreContract(name, make) {
|
|
12
|
+
it(`${name}: suppresses an address once, keeps the first reason, and lifts it`, async () => {
|
|
13
|
+
const store = await make();
|
|
14
|
+
const h = hashAddress("A@B.co");
|
|
15
|
+
expect(await store.isSuppressed(h)).toBe(false);
|
|
16
|
+
expect(await store.suppress(h, SUPPRESSION_REASON.bounce, T0)).toBe(true);
|
|
17
|
+
expect(await store.suppress(h, SUPPRESSION_REASON.complaint, T0)).toBe(false);
|
|
18
|
+
expect(await store.isSuppressed(hashAddress(" a@b.co "))).toBe(true);
|
|
19
|
+
expect(await store.unsuppress(h)).toBe(true);
|
|
20
|
+
expect(await store.unsuppress(h)).toBe(false);
|
|
21
|
+
expect(await store.isSuppressed(h)).toBe(false);
|
|
22
|
+
});
|
|
23
|
+
it(`${name}: claims an idempotency key once, and gives it back`, async () => {
|
|
24
|
+
const store = await make();
|
|
25
|
+
expect(await store.hasKey("k1")).toBe(false);
|
|
26
|
+
expect(await store.claimKey("k1", T0)).toBe(true);
|
|
27
|
+
expect(await store.claimKey("k1", T0)).toBe(false);
|
|
28
|
+
expect(await store.hasKey("k1")).toBe(true);
|
|
29
|
+
await store.releaseKey("k1");
|
|
30
|
+
expect(await store.hasKey("k1")).toBe(false);
|
|
31
|
+
expect(await store.claimKey("k1", T0)).toBe(true);
|
|
32
|
+
});
|
|
33
|
+
it(`${name}: claims a key once under concurrency`, async () => {
|
|
34
|
+
const store = await make();
|
|
35
|
+
const results = await Promise.all(Array.from({ length: 20 }, () => store.claimKey("race", T0)));
|
|
36
|
+
expect(results.filter(Boolean)).toHaveLength(1);
|
|
37
|
+
});
|
|
38
|
+
it(`${name}: logs newest first, with a hash and never an address`, async () => {
|
|
39
|
+
const store = await make();
|
|
40
|
+
await store.record(entry({ status: "failed", detail: "mailgun answered 500" }));
|
|
41
|
+
await store.record(entry({ at: day(1), status: "sent", messageId: "<m1>", idempotencyKey: "k" }));
|
|
42
|
+
const [first, second] = await store.recent(10);
|
|
43
|
+
expect(first).toMatchObject({ status: "sent", messageId: "<m1>", idempotencyKey: "k", to: hashAddress("a@b.co"), at: day(1).toISOString() });
|
|
44
|
+
expect(second).toMatchObject({ status: "failed", detail: "mailgun answered 500" });
|
|
45
|
+
expect(JSON.stringify(await store.recent(10))).not.toContain("a@b.co");
|
|
46
|
+
expect(await store.recent(1)).toHaveLength(1);
|
|
47
|
+
});
|
|
48
|
+
it(`${name}: purges the log and the keys before a date, and keeps the suppressions`, async () => {
|
|
49
|
+
const store = await make();
|
|
50
|
+
const h = hashAddress("x@y.co");
|
|
51
|
+
await store.record(entry());
|
|
52
|
+
await store.record(entry({ at: day(60) }));
|
|
53
|
+
await store.claimKey("old", T0);
|
|
54
|
+
await store.claimKey("new", day(60));
|
|
55
|
+
await store.suppress(h, SUPPRESSION_REASON.unsubscribed, T0);
|
|
56
|
+
expect(await store.purgeBefore(day(30))).toBe(2);
|
|
57
|
+
expect(await store.recent(10)).toHaveLength(1);
|
|
58
|
+
expect(await store.hasKey("old")).toBe(false);
|
|
59
|
+
expect(await store.hasKey("new")).toBe(true);
|
|
60
|
+
expect(await store.isSuppressed(h)).toBe(true);
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
export {
|
|
64
|
+
T0,
|
|
65
|
+
mailStoreContract
|
|
66
|
+
};
|
|
67
|
+
//# sourceMappingURL=testkit.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/testkit.ts"],"sourcesContent":["import { expect, it } from \"vitest\";\n\nimport { type DeliveryInput, type MailStore, SUPPRESSION_REASON } from \"./port\";\nimport { hashAddress } from \"./rules\";\n\nexport const T0 = new Date(\"2026-10-26T12:00:00.000Z\");\nconst day = (n: number) => new Date(T0.valueOf() + n * 24 * 3600 * 1000);\nconst entry = (over: Partial<DeliveryInput> = {}) => ({ at: T0, template: \"reset\", category: \"transactional\", locale: \"fr\", to: hashAddress(\"a@b.co\"), status: \"sent\" as const, ...over });\n\n/** What every `MailStore` adapter must do: the same behaviour behind the same port. `make` gives an empty store. */\nexport function mailStoreContract(name: string, make: () => MailStore | Promise<MailStore>) {\n it(`${name}: suppresses an address once, keeps the first reason, and lifts it`, async () => {\n const store = await make();\n const h = hashAddress(\"A@B.co\");\n expect(await store.isSuppressed(h)).toBe(false);\n expect(await store.suppress(h, SUPPRESSION_REASON.bounce, T0)).toBe(true);\n expect(await store.suppress(h, SUPPRESSION_REASON.complaint, T0)).toBe(false);\n expect(await store.isSuppressed(hashAddress(\" a@b.co \"))).toBe(true);\n expect(await store.unsuppress(h)).toBe(true);\n expect(await store.unsuppress(h)).toBe(false);\n expect(await store.isSuppressed(h)).toBe(false);\n });\n\n it(`${name}: claims an idempotency key once, and gives it back`, async () => {\n const store = await make();\n expect(await store.hasKey(\"k1\")).toBe(false);\n expect(await store.claimKey(\"k1\", T0)).toBe(true);\n expect(await store.claimKey(\"k1\", T0)).toBe(false);\n expect(await store.hasKey(\"k1\")).toBe(true);\n await store.releaseKey(\"k1\");\n expect(await store.hasKey(\"k1\")).toBe(false);\n expect(await store.claimKey(\"k1\", T0)).toBe(true);\n });\n\n it(`${name}: claims a key once under concurrency`, async () => {\n const store = await make();\n const results = await Promise.all(Array.from({ length: 20 }, () => store.claimKey(\"race\", T0)));\n expect(results.filter(Boolean)).toHaveLength(1);\n });\n\n it(`${name}: logs newest first, with a hash and never an address`, async () => {\n const store = await make();\n await store.record(entry({ status: \"failed\", detail: \"mailgun answered 500\" }));\n await store.record(entry({ at: day(1), status: \"sent\", messageId: \"<m1>\", idempotencyKey: \"k\" }));\n const [first, second] = await store.recent(10);\n expect(first).toMatchObject({ status: \"sent\", messageId: \"<m1>\", idempotencyKey: \"k\", to: hashAddress(\"a@b.co\"), at: day(1).toISOString() });\n expect(second).toMatchObject({ status: \"failed\", detail: \"mailgun answered 500\" });\n expect(JSON.stringify(await store.recent(10))).not.toContain(\"a@b.co\");\n expect(await store.recent(1)).toHaveLength(1);\n });\n\n it(`${name}: purges the log and the keys before a date, and keeps the suppressions`, async () => {\n const store = await make();\n const h = hashAddress(\"x@y.co\");\n await store.record(entry());\n await store.record(entry({ at: day(60) }));\n await store.claimKey(\"old\", T0);\n await store.claimKey(\"new\", day(60));\n await store.suppress(h, SUPPRESSION_REASON.unsubscribed, T0);\n expect(await store.purgeBefore(day(30))).toBe(2);\n expect(await store.recent(10)).toHaveLength(1);\n expect(await store.hasKey(\"old\")).toBe(false);\n expect(await store.hasKey(\"new\")).toBe(true);\n expect(await store.isSuppressed(h)).toBe(true);\n });\n}\n"],"mappings":"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","names":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,87 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@krizaka/mail",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Mail for every Krizaka product: queued in the caller's transaction through the outbox, delivered once per idempotency key, never to a suppressed address (stored as a hash); providers (Mailgun, Resend, log) over an injected fetch; bounces and complaints from signed webhooks. No vendor SDK, no environment variable, no wall clock.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"krizaka",
|
|
7
|
+
"mail",
|
|
8
|
+
"email",
|
|
9
|
+
"outbox",
|
|
10
|
+
"suppression",
|
|
11
|
+
"mailgun",
|
|
12
|
+
"resend"
|
|
13
|
+
],
|
|
14
|
+
"homepage": "https://github.com/krizaka/krizaka-ts/tree/main/packages/mail",
|
|
15
|
+
"bugs": "https://github.com/krizaka/krizaka-ts/issues",
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "git+https://github.com/krizaka/krizaka-ts.git",
|
|
19
|
+
"directory": "packages/mail"
|
|
20
|
+
},
|
|
21
|
+
"license": "Apache-2.0",
|
|
22
|
+
"author": "Krizaka (https://www.krizaka.com)",
|
|
23
|
+
"type": "module",
|
|
24
|
+
"main": "./dist/index.js",
|
|
25
|
+
"module": "./dist/index.js",
|
|
26
|
+
"types": "./dist/index.d.ts",
|
|
27
|
+
"exports": {
|
|
28
|
+
".": {
|
|
29
|
+
"types": "./dist/index.d.ts",
|
|
30
|
+
"import": "./dist/index.js"
|
|
31
|
+
},
|
|
32
|
+
"./settings": {
|
|
33
|
+
"types": "./dist/settings.d.ts",
|
|
34
|
+
"import": "./dist/settings.js"
|
|
35
|
+
},
|
|
36
|
+
"./testkit": {
|
|
37
|
+
"types": "./dist/testkit.d.ts",
|
|
38
|
+
"import": "./dist/testkit.js"
|
|
39
|
+
},
|
|
40
|
+
"./package.json": "./package.json"
|
|
41
|
+
},
|
|
42
|
+
"files": [
|
|
43
|
+
"dist",
|
|
44
|
+
"README.md",
|
|
45
|
+
"LICENSE"
|
|
46
|
+
],
|
|
47
|
+
"sideEffects": false,
|
|
48
|
+
"engines": {
|
|
49
|
+
"node": ">=22"
|
|
50
|
+
},
|
|
51
|
+
"devDependencies": {
|
|
52
|
+
"@types/node": "^22.10.0",
|
|
53
|
+
"@types/pg": "^8.23.1",
|
|
54
|
+
"eslint": "^9.39.0",
|
|
55
|
+
"pg": "^8.23.1",
|
|
56
|
+
"tsup": "^8.5.0",
|
|
57
|
+
"typescript": "^5.9.3",
|
|
58
|
+
"vitest": "^4.1.11",
|
|
59
|
+
"zod": "^4.6.7",
|
|
60
|
+
"@krizaka/config": "^0.14.1",
|
|
61
|
+
"@krizaka/outbox": "^0.1.2"
|
|
62
|
+
},
|
|
63
|
+
"dependencies": {
|
|
64
|
+
"@krizaka/time": "^0.1.0",
|
|
65
|
+
"@krizaka/db": "^0.3.0",
|
|
66
|
+
"@krizaka/settings": "^0.5.1"
|
|
67
|
+
},
|
|
68
|
+
"peerDependencies": {
|
|
69
|
+
"vitest": ">=4",
|
|
70
|
+
"zod": ">=3.23"
|
|
71
|
+
},
|
|
72
|
+
"peerDependenciesMeta": {
|
|
73
|
+
"vitest": {
|
|
74
|
+
"optional": true
|
|
75
|
+
},
|
|
76
|
+
"zod": {
|
|
77
|
+
"optional": true
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
"scripts": {
|
|
81
|
+
"build": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\" && tsup",
|
|
82
|
+
"lint": "eslint . --max-warnings 0 && tsc --noEmit",
|
|
83
|
+
"typecheck": "tsc --noEmit",
|
|
84
|
+
"test": "vitest run --coverage --coverage.provider=v8 --coverage.reporter=lcov --coverage.reporter=text-summary \"--coverage.include=src/**/*.ts\"",
|
|
85
|
+
"check": "pnpm run lint && pnpm run test && pnpm run build"
|
|
86
|
+
}
|
|
6
87
|
}
|