@softure-ai/mailing 0.1.7 → 0.1.9

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.
Files changed (77) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/README.md +131 -23
  3. package/dist/cli/run.d.ts +6 -2
  4. package/dist/cli/run.d.ts.map +1 -1
  5. package/dist/cli/run.js +297 -40
  6. package/dist/cli/run.js.map +1 -1
  7. package/dist/contract.d.ts +35 -5
  8. package/dist/contract.d.ts.map +1 -1
  9. package/dist/index.d.ts +14 -2
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js +1 -1
  12. package/dist/index.js.map +1 -1
  13. package/dist/next/import-deliveries.d.ts +4 -0
  14. package/dist/next/import-deliveries.d.ts.map +1 -0
  15. package/dist/next/import-deliveries.js +9 -0
  16. package/dist/next/import-deliveries.js.map +1 -0
  17. package/dist/next/index.d.ts +1 -0
  18. package/dist/next/index.d.ts.map +1 -1
  19. package/dist/next/index.js +1 -0
  20. package/dist/next/index.js.map +1 -1
  21. package/dist/next/route.d.ts +3 -2
  22. package/dist/next/route.d.ts.map +1 -1
  23. package/dist/next/route.js +5 -4
  24. package/dist/next/route.js.map +1 -1
  25. package/dist/options.d.ts +19 -2
  26. package/dist/options.d.ts.map +1 -1
  27. package/dist/options.js +30 -6
  28. package/dist/options.js.map +1 -1
  29. package/dist/schema.d.ts +17 -0
  30. package/dist/schema.d.ts.map +1 -1
  31. package/dist/schema.js +2 -0
  32. package/dist/schema.js.map +1 -1
  33. package/dist/server/campaigns.d.ts +5 -0
  34. package/dist/server/campaigns.d.ts.map +1 -1
  35. package/dist/server/campaigns.js +10 -0
  36. package/dist/server/campaigns.js.map +1 -1
  37. package/dist/server/deliveries.d.ts +9 -5
  38. package/dist/server/deliveries.d.ts.map +1 -1
  39. package/dist/server/deliveries.js +9 -7
  40. package/dist/server/deliveries.js.map +1 -1
  41. package/dist/server/dns.d.ts +48 -1
  42. package/dist/server/dns.d.ts.map +1 -1
  43. package/dist/server/dns.js +124 -15
  44. package/dist/server/dns.js.map +1 -1
  45. package/dist/server/import-deliveries.d.ts +68 -0
  46. package/dist/server/import-deliveries.d.ts.map +1 -0
  47. package/dist/server/import-deliveries.js +105 -0
  48. package/dist/server/import-deliveries.js.map +1 -0
  49. package/dist/server/index.d.ts +4 -3
  50. package/dist/server/index.d.ts.map +1 -1
  51. package/dist/server/index.js +4 -3
  52. package/dist/server/index.js.map +1 -1
  53. package/dist/server/suppressions.d.ts.map +1 -1
  54. package/dist/server/suppressions.js +9 -6
  55. package/dist/server/suppressions.js.map +1 -1
  56. package/dist/server/unsubscribe-link.d.ts +3 -2
  57. package/dist/server/unsubscribe-link.d.ts.map +1 -1
  58. package/dist/server/unsubscribe-link.js +12 -3
  59. package/dist/server/unsubscribe-link.js.map +1 -1
  60. package/migrations/0004_allow_imported_deliveries.sql +20 -0
  61. package/module.json +1 -1
  62. package/package.json +1 -1
  63. package/src/cli/run.ts +311 -39
  64. package/src/contract.ts +30 -5
  65. package/src/index.ts +4 -1
  66. package/src/next/import-deliveries.ts +9 -0
  67. package/src/next/index.ts +1 -0
  68. package/src/next/route.ts +5 -4
  69. package/src/options.ts +36 -7
  70. package/src/schema.ts +2 -0
  71. package/src/server/campaigns.ts +13 -0
  72. package/src/server/deliveries.ts +15 -8
  73. package/src/server/dns.ts +174 -16
  74. package/src/server/import-deliveries.ts +149 -0
  75. package/src/server/index.ts +21 -0
  76. package/src/server/suppressions.ts +15 -8
  77. package/src/server/unsubscribe-link.ts +10 -3
@@ -6,7 +6,7 @@
6
6
  import { err, ok, type ModuleContext, type Result } from "@softure-ai/core";
7
7
  import type { Queryable } from "@softure-ai/db";
8
8
  import { and, eq, inArray } from "drizzle-orm";
9
- import type { SuppressionSource, UnsubscribeErrorCode } from "../contract.js";
9
+ import type { SuppressionSource, UnsubscribeErrorCode, VerifiedUnsubscribeLink } from "../contract.js";
10
10
  import { suppressions } from "../schema.js";
11
11
  import { getMailingOptions } from "./options.js";
12
12
  import { getRecipientKey, readUnsubscribeSecrets, verifyUnsubscribeToken, type Env, type UnsubscribeLink, type UnsubscribeToken } from "./unsubscribe-link.js";
@@ -67,26 +67,33 @@ export async function unsubscribe(
67
67
  source: Exclude<SuppressionSource, "operator">,
68
68
  env: Env = process.env,
69
69
  ): Promise<Result<undefined, UnsubscribeErrorCode>> {
70
- const recipientKey = await verifyLink(ctx, link, env);
71
- if (recipientKey === null) return err("mailing.invalid_link");
70
+ const verified = await verifyLink(ctx, link, env);
71
+ if (verified === null) return err("mailing.invalid_link");
72
+ const { recipientKey } = verified;
72
73
  const { onUnsubscribed } = getMailingOptions(ctx.config);
73
74
  await ctx.db.transaction(async (tx) => {
74
75
  const txCtx: SuppressionContext = { ...ctx, db: tx };
75
76
  await recordSuppression(txCtx, recipientKey, source);
76
- await onUnsubscribed?.({ recipientKey, source }, txCtx);
77
+ await onUnsubscribed?.({ recipientKey, source, link: verified.link }, txCtx);
77
78
  });
78
79
  return ok();
79
80
  }
80
81
 
81
- /** The recipient key a link proves, or `null` when it proves none. */
82
- async function verifyLink(ctx: SuppressionContext, link: UnsubscribeLink | UnsubscribeToken | null, env: Env): Promise<string | null> {
82
+ interface VerifiedLink {
83
+ readonly recipientKey: string;
84
+ readonly link: VerifiedUnsubscribeLink;
85
+ }
86
+
87
+ /** The recipient key a link proves and the link it came through, or `null` when it proves none. */
88
+ async function verifyLink(ctx: SuppressionContext, link: UnsubscribeLink | UnsubscribeToken | null, env: Env): Promise<VerifiedLink | null> {
83
89
  if (link === null) return null;
84
90
  if (!("scheme" in link) || link.scheme === "signed") {
85
91
  const token = "scheme" in link ? link.token : link;
86
- return verifyUnsubscribeToken(token, readUnsubscribeSecrets(env)) ? token.recipientKey : null;
92
+ return verifyUnsubscribeToken(token, readUnsubscribeSecrets(env)) ? { recipientKey: token.recipientKey, link: { scheme: "signed" } } : null;
87
93
  }
88
94
  const legacy = getMailingOptions(ctx.config).legacyUnsubscribe;
89
95
  if (legacy === undefined) return null;
90
96
  const address: unknown = await legacy.verify(link.values, ctx);
91
- return typeof address === "string" && address.trim() !== "" ? getRecipientKey(address) : null;
97
+ if (typeof address !== "string" || address.trim() === "") return null;
98
+ return { recipientKey: getRecipientKey(address), link: { scheme: "legacy", values: link.values } };
92
99
  }
@@ -117,8 +117,9 @@ export interface LinkParams {
117
117
  /**
118
118
  * The link in a query or a form. A link with the recipient parameter (`r`) is a signed link, whatever else it
119
119
  * carries, so a crafted URL cannot route a signed link to the app's legacy check. Without it, and with
120
- * `mailing({ legacyUnsubscribe })`, a link that has every legacy parameter (non-empty, at most 512 characters) is
121
- * a legacy link. Anything else is `null`. Checks no signature: `unsubscribe` does.
120
+ * `mailing({ legacyUnsubscribe })`, a link that has every required legacy parameter (non-empty, at most 512
121
+ * characters) is a legacy link; its values also hold the optional parameters it has (an empty one is absent, one
122
+ * over 512 characters makes the link invalid). Anything else is `null`. Checks no signature: `unsubscribe` does.
122
123
  */
123
124
  export function readUnsubscribeLink(params: LinkParams, config: SoftureConfig): UnsubscribeLink | null {
124
125
  if (params.get(RECIPIENT_PARAM) !== null && params.get(RECIPIENT_PARAM) !== undefined) {
@@ -128,11 +129,17 @@ export function readUnsubscribeLink(params: LinkParams, config: SoftureConfig):
128
129
  const legacy = getMailingOptions(config).legacyUnsubscribe;
129
130
  if (legacy === undefined) return null;
130
131
  const values: Record<string, string> = {};
131
- for (const name of legacy.params) {
132
+ for (const name of legacy.params.required) {
132
133
  const value = params.get(name);
133
134
  if (typeof value !== "string" || value === "" || value.length > MAX_LEGACY_VALUE_LENGTH) return null;
134
135
  values[name] = value;
135
136
  }
137
+ for (const name of legacy.params.optional) {
138
+ const value = params.get(name);
139
+ if (typeof value !== "string" || value === "") continue;
140
+ if (value.length > MAX_LEGACY_VALUE_LENGTH) return null;
141
+ values[name] = value;
142
+ }
136
143
  return { scheme: "legacy", values };
137
144
  }
138
145