mailery 0.16.4 → 0.16.6

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,35 @@ function unitToMs2(value, unit) {
8253
8253
  }
8254
8254
 
8255
8255
  // src/server/api/agent.ts
8256
- var VERSION = "0.16.4" ;
8256
+ var VERSION = "0.16.6" ;
8257
+ var agentTagsInputSchema = z.object({
8258
+ add: z.array(z.string().min(1).max(128)).max(25).default([]),
8259
+ remove: z.array(z.string().min(1).max(128)).max(25).default([])
8260
+ });
8261
+ var publishTemplateInputSchema = z.object({
8262
+ name: z.string().min(1).max(200),
8263
+ description: z.string().max(2e3).default(""),
8264
+ kind: z.enum(["marketing", "transactional"]),
8265
+ fromName: z.string().min(1).max(200),
8266
+ fromEmail: z.string().email(),
8267
+ replyTo: z.string().email().nullable().default(null),
8268
+ providerOverride: z.string().min(1).nullable().default(null),
8269
+ subject: z.string().min(1).max(998),
8270
+ preheader: z.string().max(998).default(""),
8271
+ body: z.object({
8272
+ mjml: z.string().default(""),
8273
+ editorJson: z.record(z.string(), z.unknown()).nullable().default(null),
8274
+ html: z.string().default(""),
8275
+ plainText: z.string().default("")
8276
+ }),
8277
+ variablesSchema: z.record(z.string(), z.unknown()).default({}),
8278
+ tags: z.array(z.string().min(1).max(100)).max(50).default([]),
8279
+ bodyFormat: z.enum(["multipart", "text_only"]).default("multipart"),
8280
+ trackOpens: z.boolean().default(true),
8281
+ trackClicks: z.boolean().default(true),
8282
+ /** Recorded as the publisher; defaults to the token's actor. */
8283
+ publishedBy: z.string().min(1).max(200).optional()
8284
+ });
8257
8285
  var MIN_AGENT_TOKEN_LENGTH = 24;
8258
8286
  function createAgentRouter(mailer, opts) {
8259
8287
  if (!opts || !Array.isArray(opts.tokens) || opts.tokens.length === 0) {
@@ -8436,6 +8464,119 @@ function createAgentRouter(mailer, opts) {
8436
8464
  res.status(201).json({ sendId, dedupeKey, dispatched: dispatchNow, send: send ? sendSummary(send) : null });
8437
8465
  })
8438
8466
  );
8467
+ router.put(
8468
+ "/templates/:slug",
8469
+ wrap2(async (req, res) => {
8470
+ const slugParse = slugSchema.safeParse(String(req.params.slug));
8471
+ if (!slugParse.success) {
8472
+ return res.status(400).json({ error: "validation_failed", message: "slug must be lowercase letters, digits and hyphens" });
8473
+ }
8474
+ const slug = slugParse.data;
8475
+ const parsed = publishTemplateInputSchema.safeParse(req.body ?? {});
8476
+ if (!parsed.success) {
8477
+ return res.status(400).json({
8478
+ error: "validation_failed",
8479
+ message: parsed.error.issues.map((i) => `${i.path.join(".") || "body"}: ${i.message}`).join("; ")
8480
+ });
8481
+ }
8482
+ const input = parsed.data;
8483
+ if (!input.body.html.trim()) {
8484
+ return res.status(400).json({
8485
+ error: "empty_body",
8486
+ message: "body.html is required \u2014 this route publishes compiled HTML; a draft goes through POST /api/templates/:slug/publish"
8487
+ });
8488
+ }
8489
+ const senderCheck = validateSenderDomain(input.fromEmail, input.kind, mailer.config.senderDomains);
8490
+ if (!senderCheck.ok) {
8491
+ return res.status(400).json({ error: "sender_domain_invalid", code: senderCheck.code, message: senderCheck.reason });
8492
+ }
8493
+ const plainText = input.body.plainText.trim() ? input.body.plainText : derivePlaintext(input.body.html);
8494
+ const lint = lintTemplate(
8495
+ {
8496
+ subject: input.subject,
8497
+ preheader: input.preheader,
8498
+ mjml: input.body.mjml,
8499
+ editorJson: input.body.editorJson ?? void 0,
8500
+ html: input.body.html,
8501
+ plainText,
8502
+ kind: input.kind,
8503
+ fromEmail: input.fromEmail
8504
+ },
8505
+ { senderDomains: mailer.config.senderDomains, varsJsonSchema: varsSchema }
8506
+ );
8507
+ if (lint.errors.length > 0) {
8508
+ return res.status(422).json({
8509
+ error: "lint_failed",
8510
+ message: `Template publish blocked by ${lint.errors.length} content issue(s).`,
8511
+ lint
8512
+ });
8513
+ }
8514
+ const now = /* @__PURE__ */ new Date();
8515
+ const publishedBy = input.publishedBy ?? actorOf(req);
8516
+ const set = {
8517
+ slug,
8518
+ name: input.name,
8519
+ description: input.description,
8520
+ kind: input.kind,
8521
+ fromName: input.fromName,
8522
+ fromEmail: input.fromEmail,
8523
+ replyTo: input.replyTo,
8524
+ providerOverride: input.providerOverride,
8525
+ subject: input.subject,
8526
+ preheader: input.preheader,
8527
+ body: {
8528
+ mjml: input.body.mjml,
8529
+ editorJson: input.body.editorJson,
8530
+ html: input.body.html,
8531
+ plainText,
8532
+ compiledAt: now
8533
+ },
8534
+ variablesSchema: input.variablesSchema,
8535
+ draft: null,
8536
+ tags: input.tags,
8537
+ bodyFormat: input.bodyFormat,
8538
+ trackOpens: input.trackOpens,
8539
+ trackClicks: input.trackClicks,
8540
+ publishedAt: now,
8541
+ publishedBy,
8542
+ updatedAt: now
8543
+ };
8544
+ const result = await c.templates.updateOne(
8545
+ { slug },
8546
+ {
8547
+ $set: set,
8548
+ $setOnInsert: {
8549
+ createdAt: now,
8550
+ stats: { sent: 0, delivered: 0, opened: 0, clicked: 0, bounced: 0, complained: 0, unsubscribed: 0, lastSentAt: null }
8551
+ }
8552
+ },
8553
+ { upsert: true }
8554
+ );
8555
+ const created = result.upsertedCount > 0;
8556
+ const stored = await c.templates.findOne({ slug });
8557
+ await mailer.audit({
8558
+ actor: actorOf(req),
8559
+ action: "agent.template.publish",
8560
+ resource: { collection: "mailer_templates", id: stored?._id, slug },
8561
+ diffSummary: `${created ? "created" : "updated"} kind=${input.kind} html=${Buffer.byteLength(input.body.html, "utf8")}B trackOpens=${input.trackOpens} trackClicks=${input.trackClicks}`
8562
+ });
8563
+ res.status(created ? 201 : 200).json({
8564
+ slug,
8565
+ created,
8566
+ lint: { warnings: lint.warnings, infos: lint.infos },
8567
+ template: {
8568
+ slug,
8569
+ kind: input.kind,
8570
+ subject: input.subject,
8571
+ fromEmail: input.fromEmail,
8572
+ trackOpens: input.trackOpens,
8573
+ trackClicks: input.trackClicks,
8574
+ publishedAt: now,
8575
+ publishedBy
8576
+ }
8577
+ });
8578
+ })
8579
+ );
8439
8580
  router.get(
8440
8581
  "/sends/:id/wait",
8441
8582
  wrap2(async (req, res) => {
@@ -8720,6 +8861,41 @@ function createAgentRouter(mailer, opts) {
8720
8861
  res.json({ subscription: sub });
8721
8862
  })
8722
8863
  );
8864
+ router.post(
8865
+ "/contacts/:externalId/tags",
8866
+ wrap2(async (req, res) => {
8867
+ const contact = await loadContact(res, String(req.params.externalId));
8868
+ if (!contact) return;
8869
+ if (!guardTestContact(res, contact)) return;
8870
+ const parsed = agentTagsInputSchema.safeParse(req.body ?? {});
8871
+ if (!parsed.success) {
8872
+ return res.status(400).json({ error: "validation_failed", issues: parsed.error.issues });
8873
+ }
8874
+ const { add, remove } = parsed.data;
8875
+ if (add.length === 0 && remove.length === 0) {
8876
+ return res.status(400).json({ error: "no_tags", message: "Pass {add: [...]} and/or {remove: [...]}." });
8877
+ }
8878
+ const overlap = add.filter((t) => remove.includes(t));
8879
+ if (overlap.length > 0) {
8880
+ return res.status(400).json({ error: "tag_conflict", tags: overlap });
8881
+ }
8882
+ for (const tag of add) await mailer.tag(contact.externalId, tag);
8883
+ for (const tag of remove) await mailer.untag(contact.externalId, tag);
8884
+ const after = await mailer.adapter.getById(contact.externalId);
8885
+ await mailer.audit({
8886
+ actor: actorOf(req),
8887
+ action: "agent.contact.tags",
8888
+ resource: { collection: "contacts", id: contact.externalId },
8889
+ diffSummary: `${contact.email}: ${add.length ? `+${add.join(", +")}` : ""}${add.length && remove.length ? " " : ""}${remove.length ? `-${remove.join(", -")}` : ""}`
8890
+ });
8891
+ res.json({
8892
+ contact: { externalId: contact.externalId, email: contact.email },
8893
+ added: add,
8894
+ removed: remove,
8895
+ tags: after?.tags ?? []
8896
+ });
8897
+ })
8898
+ );
8723
8899
  router.post(
8724
8900
  "/contacts/:externalId/reset",
8725
8901
  wrap2(async (req, res) => {
@@ -9220,6 +9396,7 @@ var ENDPOINTS = [
9220
9396
  { method: "POST", path: "/templates/verify-all", summary: "Verify every template (or {slugs}) for each of {contactIds}; a matrix of pass/fail." },
9221
9397
  { 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
9398
  { 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 },
9399
+ { 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
9400
  { 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
9401
  { method: "POST", path: "/sends/:id/dispatch", summary: "Dispatch a queued send now (test contacts only).", testContactsOnly: true },
9225
9402
  { 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." },
@@ -9237,6 +9414,7 @@ var ENDPOINTS = [
9237
9414
  { method: "GET", path: "/contacts/:externalId/unsubscribe-url", summary: "A signed one-click unsubscribe URL for a test contact, to exercise POST /m/unsub/:token.", testContactsOnly: true },
9238
9415
  { method: "POST", path: "/contacts/:externalId/subscribe", summary: "Subscribe a test contact.", testContactsOnly: true },
9239
9416
  { method: "POST", path: "/contacts/:externalId/unsubscribe", summary: "Unsubscribe a test contact (marketing scope).", testContactsOnly: true },
9417
+ { method: "POST", path: "/contacts/:externalId/tags", summary: "Add or remove tags on a test contact ({add: [...], remove: [...]}), so a gated flow lets it through.", testContactsOnly: true },
9240
9418
  { method: "POST", path: "/contacts/:externalId/reset", summary: "Delete a test contact's runs, sends, events ({events: [names]} to narrow) and suppressions, then resubscribe. Each part can be turned off with false.", testContactsOnly: true },
9241
9419
  { method: "POST", path: "/tick", summary: "Run the runner tick now (trigger scan, sweeps, outbox, webhook backlog)." },
9242
9420
  { method: "GET", path: "/webhooks/status", summary: "Provider webhook ingest: last event received, counts by type (24h), unprocessed backlog." },
@@ -9953,7 +10131,7 @@ var DEDUPE_POLICIES = [
9953
10131
  ];
9954
10132
 
9955
10133
  // src/server/index.ts
9956
- var VERSION2 = "0.16.4" ;
10134
+ var VERSION2 = "0.16.6" ;
9957
10135
 
9958
10136
  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
10137
  //# sourceMappingURL=index.js.map