mailery 0.16.2 → 0.16.5

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/dist/index.cjs CHANGED
@@ -8271,7 +8271,31 @@ function unitToMs2(value, unit) {
8271
8271
  }
8272
8272
 
8273
8273
  // src/server/api/agent.ts
8274
- var VERSION = "0.16.2" ;
8274
+ var VERSION = "0.16.5" ;
8275
+ var publishTemplateInputSchema = zod.z.object({
8276
+ name: zod.z.string().min(1).max(200),
8277
+ description: zod.z.string().max(2e3).default(""),
8278
+ kind: zod.z.enum(["marketing", "transactional"]),
8279
+ fromName: zod.z.string().min(1).max(200),
8280
+ fromEmail: zod.z.string().email(),
8281
+ replyTo: zod.z.string().email().nullable().default(null),
8282
+ providerOverride: zod.z.string().min(1).nullable().default(null),
8283
+ subject: zod.z.string().min(1).max(998),
8284
+ preheader: zod.z.string().max(998).default(""),
8285
+ body: zod.z.object({
8286
+ mjml: zod.z.string().default(""),
8287
+ editorJson: zod.z.record(zod.z.string(), zod.z.unknown()).nullable().default(null),
8288
+ html: zod.z.string().default(""),
8289
+ plainText: zod.z.string().default("")
8290
+ }),
8291
+ variablesSchema: zod.z.record(zod.z.string(), zod.z.unknown()).default({}),
8292
+ tags: zod.z.array(zod.z.string().min(1).max(100)).max(50).default([]),
8293
+ bodyFormat: zod.z.enum(["multipart", "text_only"]).default("multipart"),
8294
+ trackOpens: zod.z.boolean().default(true),
8295
+ trackClicks: zod.z.boolean().default(true),
8296
+ /** Recorded as the publisher; defaults to the token's actor. */
8297
+ publishedBy: zod.z.string().min(1).max(200).optional()
8298
+ });
8275
8299
  var MIN_AGENT_TOKEN_LENGTH = 24;
8276
8300
  function createAgentRouter(mailer, opts) {
8277
8301
  if (!opts || !Array.isArray(opts.tokens) || opts.tokens.length === 0) {
@@ -8454,6 +8478,119 @@ function createAgentRouter(mailer, opts) {
8454
8478
  res.status(201).json({ sendId, dedupeKey, dispatched: dispatchNow, send: send ? sendSummary(send) : null });
8455
8479
  })
8456
8480
  );
8481
+ router.put(
8482
+ "/templates/:slug",
8483
+ wrap2(async (req, res) => {
8484
+ const slugParse = slugSchema.safeParse(String(req.params.slug));
8485
+ if (!slugParse.success) {
8486
+ return res.status(400).json({ error: "validation_failed", message: "slug must be lowercase letters, digits and hyphens" });
8487
+ }
8488
+ const slug = slugParse.data;
8489
+ const parsed = publishTemplateInputSchema.safeParse(req.body ?? {});
8490
+ if (!parsed.success) {
8491
+ return res.status(400).json({
8492
+ error: "validation_failed",
8493
+ message: parsed.error.issues.map((i) => `${i.path.join(".") || "body"}: ${i.message}`).join("; ")
8494
+ });
8495
+ }
8496
+ const input = parsed.data;
8497
+ if (!input.body.html.trim()) {
8498
+ return res.status(400).json({
8499
+ error: "empty_body",
8500
+ message: "body.html is required \u2014 this route publishes compiled HTML; a draft goes through POST /api/templates/:slug/publish"
8501
+ });
8502
+ }
8503
+ const senderCheck = validateSenderDomain(input.fromEmail, input.kind, mailer.config.senderDomains);
8504
+ if (!senderCheck.ok) {
8505
+ return res.status(400).json({ error: "sender_domain_invalid", code: senderCheck.code, message: senderCheck.reason });
8506
+ }
8507
+ const plainText = input.body.plainText.trim() ? input.body.plainText : derivePlaintext(input.body.html);
8508
+ const lint = lintTemplate(
8509
+ {
8510
+ subject: input.subject,
8511
+ preheader: input.preheader,
8512
+ mjml: input.body.mjml,
8513
+ editorJson: input.body.editorJson ?? void 0,
8514
+ html: input.body.html,
8515
+ plainText,
8516
+ kind: input.kind,
8517
+ fromEmail: input.fromEmail
8518
+ },
8519
+ { senderDomains: mailer.config.senderDomains, varsJsonSchema: varsSchema }
8520
+ );
8521
+ if (lint.errors.length > 0) {
8522
+ return res.status(422).json({
8523
+ error: "lint_failed",
8524
+ message: `Template publish blocked by ${lint.errors.length} content issue(s).`,
8525
+ lint
8526
+ });
8527
+ }
8528
+ const now = /* @__PURE__ */ new Date();
8529
+ const publishedBy = input.publishedBy ?? actorOf(req);
8530
+ const set = {
8531
+ slug,
8532
+ name: input.name,
8533
+ description: input.description,
8534
+ kind: input.kind,
8535
+ fromName: input.fromName,
8536
+ fromEmail: input.fromEmail,
8537
+ replyTo: input.replyTo,
8538
+ providerOverride: input.providerOverride,
8539
+ subject: input.subject,
8540
+ preheader: input.preheader,
8541
+ body: {
8542
+ mjml: input.body.mjml,
8543
+ editorJson: input.body.editorJson,
8544
+ html: input.body.html,
8545
+ plainText,
8546
+ compiledAt: now
8547
+ },
8548
+ variablesSchema: input.variablesSchema,
8549
+ draft: null,
8550
+ tags: input.tags,
8551
+ bodyFormat: input.bodyFormat,
8552
+ trackOpens: input.trackOpens,
8553
+ trackClicks: input.trackClicks,
8554
+ publishedAt: now,
8555
+ publishedBy,
8556
+ updatedAt: now
8557
+ };
8558
+ const result = await c.templates.updateOne(
8559
+ { slug },
8560
+ {
8561
+ $set: set,
8562
+ $setOnInsert: {
8563
+ createdAt: now,
8564
+ stats: { sent: 0, delivered: 0, opened: 0, clicked: 0, bounced: 0, complained: 0, unsubscribed: 0, lastSentAt: null }
8565
+ }
8566
+ },
8567
+ { upsert: true }
8568
+ );
8569
+ const created = result.upsertedCount > 0;
8570
+ const stored = await c.templates.findOne({ slug });
8571
+ await mailer.audit({
8572
+ actor: actorOf(req),
8573
+ action: "agent.template.publish",
8574
+ resource: { collection: "mailer_templates", id: stored?._id, slug },
8575
+ diffSummary: `${created ? "created" : "updated"} kind=${input.kind} html=${Buffer.byteLength(input.body.html, "utf8")}B trackOpens=${input.trackOpens} trackClicks=${input.trackClicks}`
8576
+ });
8577
+ res.status(created ? 201 : 200).json({
8578
+ slug,
8579
+ created,
8580
+ lint: { warnings: lint.warnings, infos: lint.infos },
8581
+ template: {
8582
+ slug,
8583
+ kind: input.kind,
8584
+ subject: input.subject,
8585
+ fromEmail: input.fromEmail,
8586
+ trackOpens: input.trackOpens,
8587
+ trackClicks: input.trackClicks,
8588
+ publishedAt: now,
8589
+ publishedBy
8590
+ }
8591
+ });
8592
+ })
8593
+ );
8457
8594
  router.get(
8458
8595
  "/sends/:id/wait",
8459
8596
  wrap2(async (req, res) => {
@@ -9238,6 +9375,7 @@ var ENDPOINTS = [
9238
9375
  { method: "POST", path: "/templates/verify-all", summary: "Verify every template (or {slugs}) for each of {contactIds}; a matrix of pass/fail." },
9239
9376
  { method: "POST", path: "/templates/:slug/render", summary: "Render for a contact and return subject, preheader, HTML, plain text, resolved vars and the signed unsubscribe URL." },
9240
9377
  { method: "POST", path: "/templates/:slug/send", summary: 'A real send through the pipeline to a test contact ({contactId}); dispatched inline unless {dispatch: "queue"}. Returns the sendId.', testContactsOnly: true },
9378
+ { method: "PUT", path: "/templates/:slug", summary: "Publish a compiled template document (html, plain text, kind, sender, subject, tracking flags) with the sender-domain and lint gates; upserts on slug, keeping createdAt and stats. The deploy-script path over HTTP." },
9241
9379
  { method: "GET", path: "/sends/:id/wait?status=delivered&timeoutMs=30000", summary: "Long-poll a send until it reaches sent | delivered | opened | clicked | terminal, with its webhook events." },
9242
9380
  { method: "POST", path: "/sends/:id/dispatch", summary: "Dispatch a queued send now (test contacts only).", testContactsOnly: true },
9243
9381
  { method: "POST", path: "/flows/:slug/simulate", summary: "Dry-run the flow for {contactId} from {at} with {eventProperties}: the path taken, every gate verdict, projected send times, where it ends. Writes nothing." },
@@ -9520,7 +9658,7 @@ function createPublicRouter(mailer, opts = {}) {
9520
9658
  logger.error?.({ err, sendId: id }, "mailery: open pixel update failed");
9521
9659
  }
9522
9660
  }));
9523
- router.get("/click/:sendId/:linkId{/:sig}", wrap(logger, async (req, res) => {
9661
+ router.get(["/click/:sendId/:linkId/:sig", "/click/:sendId/:linkId"], wrap(logger, async (req, res) => {
9524
9662
  const { sendId: sendIdStr, linkId, sig } = req.params;
9525
9663
  if (!mongodb.ObjectId.isValid(sendIdStr)) return res.status(400).end();
9526
9664
  const sendId = new mongodb.ObjectId(sendIdStr);
@@ -9971,7 +10109,7 @@ var DEDUPE_POLICIES = [
9971
10109
  ];
9972
10110
 
9973
10111
  // src/server/index.ts
9974
- var VERSION2 = "0.16.2" ;
10112
+ var VERSION2 = "0.16.5" ;
9975
10113
 
9976
10114
  exports.DEDUPE_POLICIES = DEDUPE_POLICIES;
9977
10115
  exports.DEFAULT_BOT_UA_RE = DEFAULT_BOT_UA_RE;