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.js CHANGED
@@ -8253,7 +8253,31 @@ function unitToMs2(value, unit) {
8253
8253
  }
8254
8254
 
8255
8255
  // src/server/api/agent.ts
8256
- var VERSION = "0.16.2" ;
8256
+ var VERSION = "0.16.5" ;
8257
+ var publishTemplateInputSchema = z.object({
8258
+ name: z.string().min(1).max(200),
8259
+ description: z.string().max(2e3).default(""),
8260
+ kind: z.enum(["marketing", "transactional"]),
8261
+ fromName: z.string().min(1).max(200),
8262
+ fromEmail: z.string().email(),
8263
+ replyTo: z.string().email().nullable().default(null),
8264
+ providerOverride: z.string().min(1).nullable().default(null),
8265
+ subject: z.string().min(1).max(998),
8266
+ preheader: z.string().max(998).default(""),
8267
+ body: z.object({
8268
+ mjml: z.string().default(""),
8269
+ editorJson: z.record(z.string(), z.unknown()).nullable().default(null),
8270
+ html: z.string().default(""),
8271
+ plainText: z.string().default("")
8272
+ }),
8273
+ variablesSchema: z.record(z.string(), z.unknown()).default({}),
8274
+ tags: z.array(z.string().min(1).max(100)).max(50).default([]),
8275
+ bodyFormat: z.enum(["multipart", "text_only"]).default("multipart"),
8276
+ trackOpens: z.boolean().default(true),
8277
+ trackClicks: z.boolean().default(true),
8278
+ /** Recorded as the publisher; defaults to the token's actor. */
8279
+ publishedBy: z.string().min(1).max(200).optional()
8280
+ });
8257
8281
  var MIN_AGENT_TOKEN_LENGTH = 24;
8258
8282
  function createAgentRouter(mailer, opts) {
8259
8283
  if (!opts || !Array.isArray(opts.tokens) || opts.tokens.length === 0) {
@@ -8436,6 +8460,119 @@ function createAgentRouter(mailer, opts) {
8436
8460
  res.status(201).json({ sendId, dedupeKey, dispatched: dispatchNow, send: send ? sendSummary(send) : null });
8437
8461
  })
8438
8462
  );
8463
+ router.put(
8464
+ "/templates/:slug",
8465
+ wrap2(async (req, res) => {
8466
+ const slugParse = slugSchema.safeParse(String(req.params.slug));
8467
+ if (!slugParse.success) {
8468
+ return res.status(400).json({ error: "validation_failed", message: "slug must be lowercase letters, digits and hyphens" });
8469
+ }
8470
+ const slug = slugParse.data;
8471
+ const parsed = publishTemplateInputSchema.safeParse(req.body ?? {});
8472
+ if (!parsed.success) {
8473
+ return res.status(400).json({
8474
+ error: "validation_failed",
8475
+ message: parsed.error.issues.map((i) => `${i.path.join(".") || "body"}: ${i.message}`).join("; ")
8476
+ });
8477
+ }
8478
+ const input = parsed.data;
8479
+ if (!input.body.html.trim()) {
8480
+ return res.status(400).json({
8481
+ error: "empty_body",
8482
+ message: "body.html is required \u2014 this route publishes compiled HTML; a draft goes through POST /api/templates/:slug/publish"
8483
+ });
8484
+ }
8485
+ const senderCheck = validateSenderDomain(input.fromEmail, input.kind, mailer.config.senderDomains);
8486
+ if (!senderCheck.ok) {
8487
+ return res.status(400).json({ error: "sender_domain_invalid", code: senderCheck.code, message: senderCheck.reason });
8488
+ }
8489
+ const plainText = input.body.plainText.trim() ? input.body.plainText : derivePlaintext(input.body.html);
8490
+ const lint = lintTemplate(
8491
+ {
8492
+ subject: input.subject,
8493
+ preheader: input.preheader,
8494
+ mjml: input.body.mjml,
8495
+ editorJson: input.body.editorJson ?? void 0,
8496
+ html: input.body.html,
8497
+ plainText,
8498
+ kind: input.kind,
8499
+ fromEmail: input.fromEmail
8500
+ },
8501
+ { senderDomains: mailer.config.senderDomains, varsJsonSchema: varsSchema }
8502
+ );
8503
+ if (lint.errors.length > 0) {
8504
+ return res.status(422).json({
8505
+ error: "lint_failed",
8506
+ message: `Template publish blocked by ${lint.errors.length} content issue(s).`,
8507
+ lint
8508
+ });
8509
+ }
8510
+ const now = /* @__PURE__ */ new Date();
8511
+ const publishedBy = input.publishedBy ?? actorOf(req);
8512
+ const set = {
8513
+ slug,
8514
+ name: input.name,
8515
+ description: input.description,
8516
+ kind: input.kind,
8517
+ fromName: input.fromName,
8518
+ fromEmail: input.fromEmail,
8519
+ replyTo: input.replyTo,
8520
+ providerOverride: input.providerOverride,
8521
+ subject: input.subject,
8522
+ preheader: input.preheader,
8523
+ body: {
8524
+ mjml: input.body.mjml,
8525
+ editorJson: input.body.editorJson,
8526
+ html: input.body.html,
8527
+ plainText,
8528
+ compiledAt: now
8529
+ },
8530
+ variablesSchema: input.variablesSchema,
8531
+ draft: null,
8532
+ tags: input.tags,
8533
+ bodyFormat: input.bodyFormat,
8534
+ trackOpens: input.trackOpens,
8535
+ trackClicks: input.trackClicks,
8536
+ publishedAt: now,
8537
+ publishedBy,
8538
+ updatedAt: now
8539
+ };
8540
+ const result = await c.templates.updateOne(
8541
+ { slug },
8542
+ {
8543
+ $set: set,
8544
+ $setOnInsert: {
8545
+ createdAt: now,
8546
+ stats: { sent: 0, delivered: 0, opened: 0, clicked: 0, bounced: 0, complained: 0, unsubscribed: 0, lastSentAt: null }
8547
+ }
8548
+ },
8549
+ { upsert: true }
8550
+ );
8551
+ const created = result.upsertedCount > 0;
8552
+ const stored = await c.templates.findOne({ slug });
8553
+ await mailer.audit({
8554
+ actor: actorOf(req),
8555
+ action: "agent.template.publish",
8556
+ resource: { collection: "mailer_templates", id: stored?._id, slug },
8557
+ diffSummary: `${created ? "created" : "updated"} kind=${input.kind} html=${Buffer.byteLength(input.body.html, "utf8")}B trackOpens=${input.trackOpens} trackClicks=${input.trackClicks}`
8558
+ });
8559
+ res.status(created ? 201 : 200).json({
8560
+ slug,
8561
+ created,
8562
+ lint: { warnings: lint.warnings, infos: lint.infos },
8563
+ template: {
8564
+ slug,
8565
+ kind: input.kind,
8566
+ subject: input.subject,
8567
+ fromEmail: input.fromEmail,
8568
+ trackOpens: input.trackOpens,
8569
+ trackClicks: input.trackClicks,
8570
+ publishedAt: now,
8571
+ publishedBy
8572
+ }
8573
+ });
8574
+ })
8575
+ );
8439
8576
  router.get(
8440
8577
  "/sends/:id/wait",
8441
8578
  wrap2(async (req, res) => {
@@ -9220,6 +9357,7 @@ var ENDPOINTS = [
9220
9357
  { method: "POST", path: "/templates/verify-all", summary: "Verify every template (or {slugs}) for each of {contactIds}; a matrix of pass/fail." },
9221
9358
  { 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." },
9222
9359
  { 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 },
9360
+ { 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." },
9223
9361
  { 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." },
9224
9362
  { method: "POST", path: "/sends/:id/dispatch", summary: "Dispatch a queued send now (test contacts only).", testContactsOnly: true },
9225
9363
  { 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." },
@@ -9502,7 +9640,7 @@ function createPublicRouter(mailer, opts = {}) {
9502
9640
  logger.error?.({ err, sendId: id }, "mailery: open pixel update failed");
9503
9641
  }
9504
9642
  }));
9505
- router.get("/click/:sendId/:linkId{/:sig}", wrap(logger, async (req, res) => {
9643
+ router.get(["/click/:sendId/:linkId/:sig", "/click/:sendId/:linkId"], wrap(logger, async (req, res) => {
9506
9644
  const { sendId: sendIdStr, linkId, sig } = req.params;
9507
9645
  if (!ObjectId.isValid(sendIdStr)) return res.status(400).end();
9508
9646
  const sendId = new ObjectId(sendIdStr);
@@ -9953,7 +10091,7 @@ var DEDUPE_POLICIES = [
9953
10091
  ];
9954
10092
 
9955
10093
  // src/server/index.ts
9956
- var VERSION2 = "0.16.2" ;
10094
+ var VERSION2 = "0.16.5" ;
9957
10095
 
9958
10096
  export { DEDUPE_POLICIES, DEFAULT_BOT_UA_RE, FLOW_STEP_KINDS, FlowOperationError, MIN_AGENT_TOKEN_LENGTH, Mailer, MongoContactAdapter, NullProvider, PREDICATE_KINDS, RESERVED_VAR_KEYS, SEGMENT_FILTER_KINDS, SendGridProvider, TRACKING_SIG_LENGTH, VERSION2 as VERSION, applyTracking, applyWebhookEvent, armFlow, compileMailyTemplate, compileTemplate, computeDeliveryTime, createAdminApiRouter, createAdminRouter, createAgentRouter, createPublicRouter, defaultFlowStep, defaultPredicate, defaultSegmentFilter, defineVars, derivePlaintext, disarmFlow, dispatchSend, drainPendingUnsubscribes, ensureIndexes, gateFlow, getCollections, isCanaryGate, predicateKind, processNewlyFiredEventTriggers, processOneRunStep, referencedPaths, renderForContact, renderTemplate, runTick, sendgridInboundParser, sha256Hex, signTrackingToken, signUnsubscribeToken, simulateFlow, stampWatermarkIfNull, sweepStrandedFlowRuns, ungateFlow, validateSenderDomain, varsJsonSchema, verifyTemplate, verifyTrackingToken, verifyUnsubscribeToken };
9959
10097
  //# sourceMappingURL=index.js.map