mailery 0.9.0 → 0.10.1

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.d.cts CHANGED
@@ -1,5 +1,5 @@
1
- import { R as RunnerContext, N as NormalizedEvent, C as ContactAdapter, a as Contact, A as AdapterFilter, M as MailProvider, S as SendArgs, b as SendResult, c as MailTesterFeedback, d as Mailer, T as TemplateDoc, e as SuppressionScope, D as DeliveryWindow, F as FlowStep, f as SegmentFilter, P as Predicate } from './null-BRSHAxvg.cjs';
2
- export { g as AuditLogDoc, B as BroadcastDoc, h as BroadcastStatus, i as CircuitBreakerThresholds, j as Collections, k as ContactTagDoc, E as EventDoc, l as FlowDoc, m as FlowGoal, n as FlowRunDoc, o as FlowRunStatus, p as FlowVersionDoc, H as HealthDoc, q as HealthStatus, L as LeadDoc, r as MailerConfig, s as NullProvider, O as OutboxDoc, t as RESERVED_VAR_KEYS, u as RedisOptions, v as SegmentDefinition, w as SendDoc, x as SendStatus, y as SenderDomainConfig, z as SenderDomainRegistry, G as SenderDomainValidation, I as SubscriptionDoc, J as SubscriptionStatus, K as SuppressionDoc, Q as SuppressionReason, U as TemplateKind, V as TemplateVersionDoc, W as VarsAdapter, X as VarsResolveInfo, Y as WebhookEventDoc, Z as defineVars, _ as ensureIndexes, $ as getCollections, a0 as validateSenderDomain, a1 as varsJsonSchema } from './null-BRSHAxvg.cjs';
1
+ import { R as RunnerContext, N as NormalizedEvent, C as ContactAdapter, a as Contact, A as AdapterFilter, M as MailProvider, S as SendArgs, b as SendResult, c as MailTesterFeedback, d as Mailer, T as TemplateDoc, e as SuppressionScope, D as DeliveryWindow, F as FlowStep, f as SegmentFilter, P as Predicate } from './null-CU_EApC6.cjs';
2
+ export { g as AuditLogDoc, B as BroadcastDoc, h as BroadcastStatus, i as CircuitBreakerThresholds, j as Collections, k as ContactTagDoc, E as EventDoc, l as FlowDoc, m as FlowGoal, n as FlowRunDoc, o as FlowRunStatus, p as FlowVersionDoc, H as HealthDoc, q as HealthStatus, L as LeadDoc, r as MailerConfig, s as NullProvider, O as OutboxDoc, t as RESERVED_VAR_KEYS, u as RedisOptions, v as SegmentDefinition, w as SendDoc, x as SendStatus, y as SenderDomainConfig, z as SenderDomainRegistry, G as SenderDomainValidation, I as SubscriptionDoc, J as SubscriptionStatus, K as SuppressionDoc, Q as SuppressionReason, U as TemplateKind, V as TemplateVersionDoc, W as VarsAdapter, X as VarsResolveInfo, Y as WebhookEventDoc, Z as defineVars, _ as ensureIndexes, $ as getCollections, a0 as validateSenderDomain, a1 as varsJsonSchema } from './null-CU_EApC6.cjs';
3
3
  import { ObjectId, Db, Filter } from 'mongodb';
4
4
  import { Request, Router } from 'express';
5
5
  import Handlebars from 'handlebars';
@@ -367,7 +367,7 @@ interface FlowStepKindOption {
367
367
  }
368
368
  declare const FLOW_STEP_KINDS: readonly FlowStepKindOption[];
369
369
  declare function defaultFlowStep(type: FlowStep['type']): FlowStep;
370
- type PredicateKind = 'hasTag' | 'notHasTag' | 'fieldEquals' | 'fieldExists' | 'hasFiredEvent' | 'notHasFiredEvent' | 'subscriptionStatus' | 'hasOpened' | 'hasClicked' | 'hasOpenedExcludingBots' | 'hasClickedExcludingBots' | 'openedAtLeastN' | 'clickedAtLeastN';
370
+ type PredicateKind = 'hasTag' | 'notHasTag' | 'fieldEquals' | 'fieldExists' | 'triggerPropertyEquals' | 'triggerPropertyTruthy' | 'hasFiredEvent' | 'notHasFiredEvent' | 'subscriptionStatus' | 'hasOpened' | 'hasClicked' | 'hasOpenedExcludingBots' | 'hasClickedExcludingBots' | 'openedAtLeastN' | 'clickedAtLeastN';
371
371
  interface PredicateKindOption {
372
372
  value: PredicateKind;
373
373
  label: string;
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { R as RunnerContext, N as NormalizedEvent, C as ContactAdapter, a as Contact, A as AdapterFilter, M as MailProvider, S as SendArgs, b as SendResult, c as MailTesterFeedback, d as Mailer, T as TemplateDoc, e as SuppressionScope, D as DeliveryWindow, F as FlowStep, f as SegmentFilter, P as Predicate } from './null-BRSHAxvg.js';
2
- export { g as AuditLogDoc, B as BroadcastDoc, h as BroadcastStatus, i as CircuitBreakerThresholds, j as Collections, k as ContactTagDoc, E as EventDoc, l as FlowDoc, m as FlowGoal, n as FlowRunDoc, o as FlowRunStatus, p as FlowVersionDoc, H as HealthDoc, q as HealthStatus, L as LeadDoc, r as MailerConfig, s as NullProvider, O as OutboxDoc, t as RESERVED_VAR_KEYS, u as RedisOptions, v as SegmentDefinition, w as SendDoc, x as SendStatus, y as SenderDomainConfig, z as SenderDomainRegistry, G as SenderDomainValidation, I as SubscriptionDoc, J as SubscriptionStatus, K as SuppressionDoc, Q as SuppressionReason, U as TemplateKind, V as TemplateVersionDoc, W as VarsAdapter, X as VarsResolveInfo, Y as WebhookEventDoc, Z as defineVars, _ as ensureIndexes, $ as getCollections, a0 as validateSenderDomain, a1 as varsJsonSchema } from './null-BRSHAxvg.js';
1
+ import { R as RunnerContext, N as NormalizedEvent, C as ContactAdapter, a as Contact, A as AdapterFilter, M as MailProvider, S as SendArgs, b as SendResult, c as MailTesterFeedback, d as Mailer, T as TemplateDoc, e as SuppressionScope, D as DeliveryWindow, F as FlowStep, f as SegmentFilter, P as Predicate } from './null-CU_EApC6.js';
2
+ export { g as AuditLogDoc, B as BroadcastDoc, h as BroadcastStatus, i as CircuitBreakerThresholds, j as Collections, k as ContactTagDoc, E as EventDoc, l as FlowDoc, m as FlowGoal, n as FlowRunDoc, o as FlowRunStatus, p as FlowVersionDoc, H as HealthDoc, q as HealthStatus, L as LeadDoc, r as MailerConfig, s as NullProvider, O as OutboxDoc, t as RESERVED_VAR_KEYS, u as RedisOptions, v as SegmentDefinition, w as SendDoc, x as SendStatus, y as SenderDomainConfig, z as SenderDomainRegistry, G as SenderDomainValidation, I as SubscriptionDoc, J as SubscriptionStatus, K as SuppressionDoc, Q as SuppressionReason, U as TemplateKind, V as TemplateVersionDoc, W as VarsAdapter, X as VarsResolveInfo, Y as WebhookEventDoc, Z as defineVars, _ as ensureIndexes, $ as getCollections, a0 as validateSenderDomain, a1 as varsJsonSchema } from './null-CU_EApC6.js';
3
3
  import { ObjectId, Db, Filter } from 'mongodb';
4
4
  import { Request, Router } from 'express';
5
5
  import Handlebars from 'handlebars';
@@ -367,7 +367,7 @@ interface FlowStepKindOption {
367
367
  }
368
368
  declare const FLOW_STEP_KINDS: readonly FlowStepKindOption[];
369
369
  declare function defaultFlowStep(type: FlowStep['type']): FlowStep;
370
- type PredicateKind = 'hasTag' | 'notHasTag' | 'fieldEquals' | 'fieldExists' | 'hasFiredEvent' | 'notHasFiredEvent' | 'subscriptionStatus' | 'hasOpened' | 'hasClicked' | 'hasOpenedExcludingBots' | 'hasClickedExcludingBots' | 'openedAtLeastN' | 'clickedAtLeastN';
370
+ type PredicateKind = 'hasTag' | 'notHasTag' | 'fieldEquals' | 'fieldExists' | 'triggerPropertyEquals' | 'triggerPropertyTruthy' | 'hasFiredEvent' | 'notHasFiredEvent' | 'subscriptionStatus' | 'hasOpened' | 'hasClicked' | 'hasOpenedExcludingBots' | 'hasClickedExcludingBots' | 'openedAtLeastN' | 'clickedAtLeastN';
371
371
  interface PredicateKindOption {
372
372
  value: PredicateKind;
373
373
  label: string;
package/dist/index.js CHANGED
@@ -396,7 +396,25 @@ var tagInputSchema = z.object({
396
396
  var abortFlowInputSchema = z.object({
397
397
  flowSlug: slugSchema,
398
398
  externalId: externalIdSchema,
399
- reason: z.string().min(1).max(200).optional()
399
+ reason: z.string().min(1).max(200).optional(),
400
+ /**
401
+ * Restrict the abort to runs whose trigger event carried these properties —
402
+ * e.g. `{ accountId }` to cancel one account's series while the same
403
+ * contact's other accounts keep running. Omit to abort every active run for
404
+ * the contact on this flow.
405
+ *
406
+ * Keys and values are both constrained because these go straight into a
407
+ * Mongo query. Values are primitives only: an object value like
408
+ * `{ $ne: null }` would reach the query as an OPERATOR and match every
409
+ * scoped run, turning a one-account abort into abort-everything. Hosts
410
+ * typically pass an id from a request body, so treat it as untrusted. The
411
+ * key regex likewise blocks `$`-prefixed keys and dots (a dot would silently
412
+ * extend the path and change match semantics).
413
+ */
414
+ matchTriggerProperties: z.record(
415
+ z.string().regex(/^[A-Za-z0-9_]+$/),
416
+ z.union([z.string(), z.number(), z.boolean(), z.null()])
417
+ ).optional()
400
418
  });
401
419
  var abortAllFlowsInputSchema = abortFlowInputSchema.omit({ flowSlug: true });
402
420
  var sendOneOffInputSchema = z.object({
@@ -465,6 +483,13 @@ var predicateSchema = z.lazy(
465
483
  z.object({ notHasTag: z.string() }),
466
484
  z.object({ fieldEquals: z.object({ field: z.string(), value: z.unknown() }) }),
467
485
  z.object({ fieldExists: z.string() }),
486
+ z.object({
487
+ triggerPropertyEquals: z.object({
488
+ key: z.string().min(1),
489
+ value: z.union([z.string(), z.number(), z.boolean(), z.null()])
490
+ })
491
+ }),
492
+ z.object({ triggerPropertyTruthy: z.string().min(1) }),
468
493
  z.object({
469
494
  hasFiredEvent: z.string(),
470
495
  sinceFlowStart: z.boolean().optional(),
@@ -1648,6 +1673,12 @@ async function evaluatePredicate(predicate, ctx) {
1648
1673
  if ("fieldExists" in p) {
1649
1674
  return ctx.contact.fields[p.fieldExists] !== void 0;
1650
1675
  }
1676
+ if ("triggerPropertyEquals" in p) {
1677
+ return (ctx.run.triggerEvent?.properties ?? {})[p.triggerPropertyEquals.key] === p.triggerPropertyEquals.value;
1678
+ }
1679
+ if ("triggerPropertyTruthy" in p) {
1680
+ return Boolean((ctx.run.triggerEvent?.properties ?? {})[p.triggerPropertyTruthy]);
1681
+ }
1651
1682
  if ("subscriptionStatus" in p) {
1652
1683
  const sub = await ctx.collections.subscriptions.findOne({ externalId: ctx.contact.externalId });
1653
1684
  return sub?.status === p.subscriptionStatus;
@@ -2254,9 +2285,11 @@ function sha256(s) {
2254
2285
  }
2255
2286
 
2256
2287
  // src/server/runner/step.ts
2288
+ var DUE_SKEW_MS = 1e3;
2257
2289
  async function processOneRunStep(runId, ctx) {
2258
2290
  const run = await ctx.collections.flowRuns.findOne({ _id: runId });
2259
2291
  if (!run || run.status !== "active") return;
2292
+ if (run.nextActionAt && run.nextActionAt.getTime() > Date.now() + DUE_SKEW_MS) return;
2260
2293
  const flow = await ctx.collections.flows.findOne({ _id: run.flowId });
2261
2294
  if (!flow) {
2262
2295
  await failFlowRun(run, "flow_missing", ctx);
@@ -2400,7 +2433,14 @@ async function handleFireEvent(run, step, ctx) {
2400
2433
  await ctx.collections.events.insertOne({
2401
2434
  externalId: run.externalId,
2402
2435
  name: step.eventName,
2403
- properties: step.properties ?? {},
2436
+ // Inherit the triggering event's properties so a handoff carries the
2437
+ // context that identifies what the run is ABOUT (which account, order,
2438
+ // subscription, ...). A step's `properties` are static — authored once in
2439
+ // the flow definition — so without this a fired event can only ever say
2440
+ // "this contact", losing the scope the originating event supplied, and
2441
+ // the receiving flow has nothing to resolve variables against. Explicit
2442
+ // step.properties win on conflict.
2443
+ properties: { ...run.triggerEvent?.properties ?? {}, ...step.properties ?? {} },
2404
2444
  dedupeKey,
2405
2445
  occurredAt: /* @__PURE__ */ new Date(),
2406
2446
  createdAt: /* @__PURE__ */ new Date()
@@ -4235,14 +4275,25 @@ var Mailer = class _Mailer {
4235
4275
  * same handler that processes the business event ("user upgraded").
4236
4276
  */
4237
4277
  async abortFlow(flowSlug, externalId, opts = {}) {
4238
- const parsed = abortFlowInputSchema.parse({ flowSlug, externalId, reason: opts.reason });
4278
+ const parsed = abortFlowInputSchema.parse({
4279
+ flowSlug,
4280
+ externalId,
4281
+ reason: opts.reason,
4282
+ matchTriggerProperties: opts.matchTriggerProperties
4283
+ });
4239
4284
  const flow = await this.collections.flows.findOne(
4240
4285
  { slug: parsed.flowSlug },
4241
4286
  { projection: { _id: 1 } }
4242
4287
  );
4243
4288
  if (!flow) throw new Error(`abortFlow: unknown flow slug "${parsed.flowSlug}"`);
4289
+ const triggerMatch = Object.fromEntries(
4290
+ Object.entries(parsed.matchTriggerProperties ?? {}).map(([k, v]) => [
4291
+ `triggerEvent.properties.${k}`,
4292
+ v
4293
+ ])
4294
+ );
4244
4295
  const result = await this.abortActiveRuns(
4245
- { externalId: parsed.externalId, flowId: flow._id },
4296
+ { externalId: parsed.externalId, flowId: flow._id, ...triggerMatch },
4246
4297
  parsed.reason ? `aborted_by_host:${parsed.reason}` : "aborted_by_host"
4247
4298
  );
4248
4299
  if (result.abortedRuns > 0 || result.cancelledSends > 0) {
@@ -4250,7 +4301,9 @@ var Mailer = class _Mailer {
4250
4301
  actor: "host",
4251
4302
  action: "flow.abort",
4252
4303
  resource: { collection: "mailer_flow_runs", slug: parsed.flowSlug },
4253
- diffSummary: `abortFlow slug=${parsed.flowSlug} externalId=${parsed.externalId} runs=${result.abortedRuns} sends=${result.cancelledSends}${parsed.reason ? ` reason=${parsed.reason}` : ""}`
4304
+ // Record the scope: without it a one-account abort and an abort-every-
4305
+ // run-for-this-contact are indistinguishable in the audit trail.
4306
+ diffSummary: `abortFlow slug=${parsed.flowSlug} externalId=${parsed.externalId} scope=${parsed.matchTriggerProperties ? JSON.stringify(parsed.matchTriggerProperties) : "all"} runs=${result.abortedRuns} sends=${result.cancelledSends}${parsed.reason ? ` reason=${parsed.reason}` : ""}`
4254
4307
  });
4255
4308
  }
4256
4309
  return result;
@@ -7416,6 +7469,8 @@ var PREDICATE_KINDS = [
7416
7469
  { value: "notHasTag", label: "does NOT have tag" },
7417
7470
  { value: "fieldEquals", label: "field equals" },
7418
7471
  { value: "fieldExists", label: "field exists" },
7472
+ { value: "triggerPropertyEquals", label: "trigger property equals" },
7473
+ { value: "triggerPropertyTruthy", label: "trigger property is set" },
7419
7474
  { value: "hasFiredEvent", label: "fired event" },
7420
7475
  { value: "notHasFiredEvent", label: "has NOT fired event" },
7421
7476
  { value: "subscriptionStatus", label: "subscription status" },
@@ -7443,6 +7498,10 @@ function defaultPredicate(kind) {
7443
7498
  return { fieldEquals: { field: "tier", value: "Pro" } };
7444
7499
  case "fieldExists":
7445
7500
  return { fieldExists: "tier" };
7501
+ case "triggerPropertyEquals":
7502
+ return { triggerPropertyEquals: { key: "plan", value: "pro" } };
7503
+ case "triggerPropertyTruthy":
7504
+ return { triggerPropertyTruthy: "wasReferred" };
7446
7505
  case "hasFiredEvent":
7447
7506
  return { hasFiredEvent: "Activated app" };
7448
7507
  case "notHasFiredEvent":
@@ -7533,7 +7592,7 @@ var DEDUPE_POLICIES = [
7533
7592
  ];
7534
7593
 
7535
7594
  // src/server/index.ts
7536
- var VERSION = "0.9.0" ;
7595
+ var VERSION = "0.10.1" ;
7537
7596
 
7538
7597
  export { DEDUPE_POLICIES, FLOW_STEP_KINDS, Mailer, MongoContactAdapter, NullProvider, PREDICATE_KINDS, RESERVED_VAR_KEYS, SEGMENT_FILTER_KINDS, SendGridProvider, VERSION, applyTracking, applyWebhookEvent, compileMailyTemplate, compileTemplate, computeDeliveryTime, createAdminRouter, createPublicRouter, defaultFlowStep, defaultPredicate, defaultSegmentFilter, defineVars, derivePlaintext, dispatchSend, ensureIndexes, getCollections, predicateKind, processNewlyFiredEventTriggers, processOneRunStep, renderTemplate, runTick, sha256Hex, signUnsubscribeToken, sweepStrandedFlowRuns, validateSenderDomain, varsJsonSchema, verifyUnsubscribeToken };
7539
7598
  //# sourceMappingURL=index.js.map