@antelopejs/dms-mailing 0.9.0 → 0.9.2

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 (71) hide show
  1. package/README.md +2 -2
  2. package/dist/db/models/sends.model.d.ts +3 -1
  3. package/dist/db/models/sends.model.js +26 -2
  4. package/dist/db/models/sends.model.js.map +1 -1
  5. package/dist/db/tables/settings.table.d.ts +1 -0
  6. package/dist/db/tables/settings.table.js +4 -0
  7. package/dist/db/tables/settings.table.js.map +1 -1
  8. package/dist/hooks/tenant-export.d.ts +1 -1
  9. package/dist/hooks/tenant-export.js.map +1 -1
  10. package/dist/implementations/dms-mailing/index.d.ts +1 -0
  11. package/dist/implementations/dms-mailing/index.js +3 -1
  12. package/dist/implementations/dms-mailing/index.js.map +1 -1
  13. package/dist/pages/sends.js.map +1 -1
  14. package/dist/routes/metrics.js +1 -2
  15. package/dist/routes/metrics.js.map +1 -1
  16. package/dist/routes/settings.d.ts +3 -2
  17. package/dist/routes/settings.js +8 -4
  18. package/dist/routes/settings.js.map +1 -1
  19. package/dist/routes/templates.js +2 -2
  20. package/dist/routes/templates.js.map +1 -1
  21. package/dist/services/attention.d.ts +1 -1
  22. package/dist/services/attention.js +3 -3
  23. package/dist/services/attention.js.map +1 -1
  24. package/dist/services/metrics.js +5 -1
  25. package/dist/services/metrics.js.map +1 -1
  26. package/dist/services/send-import.js +4 -4
  27. package/dist/services/send-import.js.map +1 -1
  28. package/dist/services/settings.d.ts +2 -1
  29. package/dist/services/settings.js +19 -1
  30. package/dist/services/settings.js.map +1 -1
  31. package/dist/services/template-attention.d.ts +1 -1
  32. package/dist/services/template-attention.js +2 -4
  33. package/dist/services/template-attention.js.map +1 -1
  34. package/dist/services/webhook-secret-declaration.d.ts +4 -0
  35. package/dist/services/webhook-secret-declaration.js +25 -0
  36. package/dist/services/webhook-secret-declaration.js.map +1 -0
  37. package/dist/services/webhook-status.js +7 -2
  38. package/dist/services/webhook-status.js.map +1 -1
  39. package/dist/types/settings.d.ts +3 -0
  40. package/dist/types/settings.js +6 -1
  41. package/dist/types/settings.js.map +1 -1
  42. package/dist/validation/declarations.schema.d.ts +13 -0
  43. package/dist/validation/declarations.schema.js +6 -1
  44. package/dist/validation/declarations.schema.js.map +1 -1
  45. package/frontend-vue/app/components/editor/EditorCanvas.vue +8 -4
  46. package/frontend-vue/app/composables/useSortableList.ts +2 -2
  47. package/frontend-vue/package.json +37 -36
  48. package/frontend-vue/pnpm-lock.yaml +25 -5
  49. package/frontend-vue/tests/editor-canvas.test.ts +142 -0
  50. package/frontend-vue/tests/sortable-list.test-d.ts +19 -0
  51. package/frontend-vue/tsconfig.typecheck.json +11 -0
  52. package/frontend-vue/vitest.config.ts +16 -0
  53. package/package.json +5 -5
  54. package/skills/dms-mailing/SKILL.md +2 -0
  55. package/src/db/models/sends.model.ts +34 -4
  56. package/src/db/tables/settings.table.ts +4 -0
  57. package/src/hooks/tenant-export.ts +4 -1
  58. package/src/implementations/dms-mailing/index.ts +1 -0
  59. package/src/pages/sends.ts +3 -2
  60. package/src/routes/metrics.ts +1 -2
  61. package/src/routes/settings.ts +19 -7
  62. package/src/routes/templates.ts +6 -7
  63. package/src/services/attention.ts +6 -6
  64. package/src/services/metrics.ts +3 -1
  65. package/src/services/send-import.ts +10 -8
  66. package/src/services/settings.ts +40 -2
  67. package/src/services/template-attention.ts +4 -5
  68. package/src/services/webhook-secret-declaration.ts +47 -0
  69. package/src/services/webhook-status.ts +9 -5
  70. package/src/types/settings.ts +14 -0
  71. package/src/validation/declarations.schema.ts +7 -0
@@ -5,13 +5,13 @@ import {
5
5
  type TemplateAttentionFields,
6
6
  TemplateModel,
7
7
  } from "../db";
8
- import { BUSINESS_AUDIENCE, type TemplateAttentionReason } from "../types";
8
+ import type { TemplateAttentionReason } from "../types";
9
9
  import {
10
10
  knownLocalesOf,
11
11
  type TemplateAttentionContext,
12
12
  templateAttentionReasons,
13
13
  } from "./attention";
14
- import { forAudience, statsWindowStart } from "./metrics";
14
+ import { statsWindowStart } from "./metrics";
15
15
 
16
16
  /** How the stored `attention` field separates its reasons. */
17
17
  const REASON_SEPARATOR = ",";
@@ -31,14 +31,13 @@ async function attentionContext(
31
31
  templates: MailingTemplate[],
32
32
  ): Promise<TemplateAttentionContext> {
33
33
  const now = Date.now();
34
- const sends = await GetModel(SendModel, tenantId).listBetween(
34
+ const slugs = await GetModel(SendModel, tenantId).listSentSlugs(
35
35
  statsWindowStart(now),
36
36
  new Date(now),
37
37
  );
38
- const real = forAudience(sends, BUSINESS_AUDIENCE);
39
38
  return {
40
39
  now,
41
- sentSlugs: new Set(real.map((send) => send.templateSlug)),
40
+ sentSlugs: new Set(slugs),
42
41
  knownLocales: knownLocalesOf(templates),
43
42
  };
44
43
  }
@@ -0,0 +1,47 @@
1
+ import type {
2
+ DeclareWebhookSecretResult,
3
+ WebhookSecretDeclaration,
4
+ WebhookSecretOutcome,
5
+ } from "@antelopejs/interface-dms-mailing";
6
+ import type { MailingSettingsValues } from "../types";
7
+ import { webhookSecretDeclarationSchema } from "../validation/declarations.schema";
8
+ import { parseDeclared } from "./declaration-error";
9
+ import { webhookSecretMatches } from "./events";
10
+ import { getSettings, saveWebhookSecret } from "./settings";
11
+
12
+ const SET: WebhookSecretOutcome = "set";
13
+
14
+ /**
15
+ * What declaring `secret` does to the stored one: nothing when it is already
16
+ * stored, nothing when the workspace owner set or rotated it and the caller
17
+ * did not ask to replace it, else a write.
18
+ */
19
+ export function webhookSecretOutcome(
20
+ stored: Pick<MailingSettingsValues, "webhookSecret" | "webhookSecretSource">,
21
+ secret: string,
22
+ replaceRotated: boolean,
23
+ ): WebhookSecretOutcome {
24
+ if (webhookSecretMatches(secret, stored.webhookSecret)) return "unchanged";
25
+ if (stored.webhookSecretSource === "operator" && !replaceRotated)
26
+ return "kept";
27
+ return SET;
28
+ }
29
+
30
+ /**
31
+ * Sets the workspace's webhook secret a project ships as code. The secret is
32
+ * never returned, and a refusal names the rule it broke, never the value.
33
+ */
34
+ export async function declareWebhookSecret(
35
+ input: WebhookSecretDeclaration,
36
+ ): Promise<DeclareWebhookSecretResult> {
37
+ const { tenantId, secret, replaceRotated } = parseDeclared(
38
+ webhookSecretDeclarationSchema,
39
+ input,
40
+ "invalid_webhook_secret",
41
+ "webhook secret declaration",
42
+ );
43
+ const stored = await getSettings(tenantId);
44
+ const outcome = webhookSecretOutcome(stored, secret, replaceRotated);
45
+ if (outcome === SET) await saveWebhookSecret(tenantId, secret, "declared");
46
+ return { outcome };
47
+ }
@@ -144,15 +144,19 @@ export async function readWebhookStatus(
144
144
  );
145
145
  }
146
146
 
147
+ function isWebhookRefusal(value: unknown): value is WebhookRefusal {
148
+ return isText(value) && Object.hasOwn(WEBHOOK_REFUSAL_MESSAGES, value);
149
+ }
150
+
151
+ /** The webhook's own refusal reason in a response, not one another endpoint gives. */
147
152
  async function refusalOf(
148
153
  response: Response,
149
154
  ): Promise<WebhookRefusal | undefined> {
150
155
  try {
151
- // SAFETY: Response.json() answers any, and the URL under test is this module's own webhook route, which refuses
152
- // with refusalBody(reason); another endpoint can answer any JSON, which is read the same way, and a non-object
153
- // reads as no refusal (only null throws, into the catch below).
154
- const body = (await response.json()) as Partial<WebhookRefusalBody>;
155
- return body.reason;
156
+ const body: unknown = await response.json();
157
+ return isObject(body) && isWebhookRefusal(body.reason)
158
+ ? body.reason
159
+ : undefined;
156
160
  } catch {
157
161
  return undefined;
158
162
  }
@@ -14,6 +14,19 @@ export interface CategoryInLocale {
14
14
  icon: string;
15
15
  }
16
16
 
17
+ /**
18
+ * Who set the webhook secret: the module drew it (`generated`), a project
19
+ * declared it (`declared`), or the workspace owner set or rotated it in the
20
+ * settings (`operator`). A declaration replaces only the first two.
21
+ */
22
+ export const WEBHOOK_SECRET_SOURCES = [
23
+ "generated",
24
+ "declared",
25
+ "operator",
26
+ ] as const;
27
+
28
+ export type WebhookSecretSource = (typeof WEBHOOK_SECRET_SOURCES)[number];
29
+
17
30
  export interface MailingSettingsValues {
18
31
  fallbackLocale: string;
19
32
  logRetentionDays: number;
@@ -22,6 +35,7 @@ export interface MailingSettingsValues {
22
35
  senderEmail: string;
23
36
  replyTo: string;
24
37
  webhookSecret: string;
38
+ webhookSecretSource: WebhookSecretSource;
25
39
  categories: TemplateCategory[];
26
40
  }
27
41
 
@@ -68,6 +68,13 @@ export const senderDeclarationSchema = z.object({
68
68
 
69
69
  export type ParsedSenderDeclaration = z.infer<typeof senderDeclarationSchema>;
70
70
 
71
+ /** A webhook secret declared from code: the settings form's rule. */
72
+ export const webhookSecretDeclarationSchema = z.object({
73
+ tenantId: tenantIdSchema,
74
+ secret: settingsSchema.shape.webhookSecret,
75
+ replaceRotated: z.boolean().default(false),
76
+ });
77
+
71
78
  const recipientSchema = z.union([
72
79
  z.string().trim().email(),
73
80
  z.object({