@benclmnt/postmock 0.0.3 → 0.0.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.
@@ -1,7 +1,7 @@
1
1
  import { z } from "zod";
2
2
  import type { PostmockConfig } from "./server.ts";
3
3
  /** Every setting is one of these env keys. Plugins read their own keys when they start. */
4
- export declare const ENV_KEYS: readonly ["POSTMOCK_HOST", "POSTMOCK_API_PORT", "POSTMOCK_CONTROL_PORT", "POSTMOCK_SEED", "POSTMOCK_HTTPS_PORT", "POSTMOCK_HTTPS_TLS_KEY", "POSTMOCK_HTTPS_TLS_CERT", "POSTMOCK_SMTP_PORTS", "POSTMOCK_SMTP_TLS_KEY", "POSTMOCK_SMTP_TLS_CERT", "POSTMOCK_WEBHOOKS_ALLOW_HOSTS"];
4
+ export declare const ENV_KEYS: readonly ["POSTMOCK_HOST", "POSTMOCK_API_PORT", "POSTMOCK_CONTROL_PORT", "POSTMOCK_SEED", "POSTMOCK_CLOCK", "POSTMOCK_HTTPS_PORT", "POSTMOCK_HTTPS_TLS_KEY", "POSTMOCK_HTTPS_TLS_CERT", "POSTMOCK_SMTP_PORTS", "POSTMOCK_SMTP_TLS_KEY", "POSTMOCK_SMTP_TLS_CERT", "POSTMOCK_WEBHOOKS_ALLOW_HOSTS"];
5
5
  export declare const flagOf: (key: string) => string;
6
6
  export declare const USAGE: string;
7
7
  /**
@@ -7,6 +7,7 @@ export const ENV_KEYS = [
7
7
  "POSTMOCK_API_PORT",
8
8
  "POSTMOCK_CONTROL_PORT",
9
9
  "POSTMOCK_SEED",
10
+ "POSTMOCK_CLOCK",
10
11
  "POSTMOCK_HTTPS_PORT",
11
12
  "POSTMOCK_HTTPS_TLS_KEY",
12
13
  "POSTMOCK_HTTPS_TLS_CERT",
@@ -63,6 +64,7 @@ const envSchema = z.object({
63
64
  POSTMOCK_API_PORT: portSchema.default(8080),
64
65
  POSTMOCK_CONTROL_PORT: portSchema.default(8025),
65
66
  POSTMOCK_SEED: z.string().min(1).default("empty"),
67
+ POSTMOCK_CLOCK: z.enum(["real", "manual"]).default("real"),
66
68
  POSTMOCK_HTTPS_PORT: portSchema.optional(),
67
69
  POSTMOCK_HTTPS_TLS_KEY: z.string().min(1).optional(),
68
70
  POSTMOCK_HTTPS_TLS_CERT: z.string().min(1).optional(),
@@ -75,6 +77,7 @@ export function configFromEnv(env) {
75
77
  apiPort: e.POSTMOCK_API_PORT,
76
78
  controlPort: e.POSTMOCK_CONTROL_PORT,
77
79
  seed: e.POSTMOCK_SEED,
80
+ clock: e.POSTMOCK_CLOCK,
78
81
  };
79
82
  const https = [e.POSTMOCK_HTTPS_PORT, e.POSTMOCK_HTTPS_TLS_KEY, e.POSTMOCK_HTTPS_TLS_CERT];
80
83
  if (https.every((v) => v === undefined))
@@ -3,7 +3,7 @@ import { apiError, ERROR_FAMILIES, isSummaryRow } from "../../errors.js";
3
3
  import { formatTimestamp } from "../../time.js";
4
4
  import { ControlError, controlInput, defineControl } from "../registry.js";
5
5
  import { seedAtomically } from "../seeding.js";
6
- // docs/09 §5: reset, seed, clock, faults, messages.
6
+ // docs/09 §5: reset, seed, clock, latency, faults, messages.
7
7
  defineControl({
8
8
  method: "POST",
9
9
  path: "/control/reset",
@@ -28,7 +28,49 @@ defineControl({
28
28
  handler: async (ctx) => {
29
29
  const { ms } = controlInput(z.object({ ms: z.int().nonnegative() }), ctx.body);
30
30
  await ctx.clock.advance(ms);
31
- return { now: formatTimestamp(ctx.clock.now(), "utc") };
31
+ return clockState(ctx.clock);
32
+ },
33
+ });
34
+ defineControl({
35
+ method: "GET",
36
+ path: "/control/clock",
37
+ handler: (ctx) => clockState(ctx.clock),
38
+ });
39
+ function clockState(clock) {
40
+ return { now: formatTimestamp(clock.now(), "utc"), pending: clock.pending };
41
+ }
42
+ const requestMatch = z.object({
43
+ method: z.string(),
44
+ path: z.string().startsWith("/"),
45
+ // The domain part of a parsed address: DNS labels with a letter, so no IP, literal or trailing dot.
46
+ recipientDomain: z
47
+ .string()
48
+ .regex(/^(?=.*[a-z])[a-z0-9-]+(\.[a-z0-9-]+)*$/i)
49
+ .transform((d) => d.toLowerCase())
50
+ .optional(),
51
+ });
52
+ function requestRule(match, times) {
53
+ return {
54
+ method: match.method,
55
+ path: match.path,
56
+ recipientDomain: match.recipientDomain,
57
+ remaining: times,
58
+ };
59
+ }
60
+ defineControl({
61
+ method: "POST",
62
+ path: "/control/latency",
63
+ handler: (ctx) => {
64
+ const { match, times, ms } = controlInput(z.object({
65
+ match: requestMatch,
66
+ times: z.int().positive().default(1),
67
+ ms: z.int().positive(),
68
+ }), ctx.body);
69
+ ctx.store.state.latencies.push({
70
+ ...requestRule(match, times),
71
+ ms,
72
+ });
73
+ return { latencies: ctx.store.state.latencies.length };
32
74
  },
33
75
  });
34
76
  const errorReply = z.object({
@@ -38,7 +80,7 @@ const errorReply = z.object({
38
80
  message: z.string().optional(),
39
81
  });
40
82
  const faultSchema = z.object({
41
- match: z.object({ method: z.string(), path: z.string().startsWith("/") }),
83
+ match: requestMatch,
42
84
  times: z.int().positive().default(1),
43
85
  reply: z.unknown(),
44
86
  });
@@ -48,9 +90,7 @@ defineControl({
48
90
  handler: (ctx) => {
49
91
  const { match, times, reply } = controlInput(faultSchema, ctx.body);
50
92
  ctx.store.state.faults.push({
51
- method: match.method,
52
- path: match.path,
53
- remaining: times,
93
+ ...requestRule(match, times),
54
94
  reply: reply === "timeout" || reply === "reset"
55
95
  ? reply
56
96
  : faultError(controlInput(errorReply, reply)),
@@ -12,7 +12,8 @@ export function createApiApp(runtime) {
12
12
  app.all("*", async (c) => {
13
13
  const request = c.req.raw;
14
14
  const url = new URL(request.url);
15
- const fault = await applyFault(runtime.store.state, request.method, url.pathname, c.env);
15
+ const bytes = await request.arrayBuffer();
16
+ const fault = await applyFault(runtime, { method: request.method, pathname: url.pathname, body: bytes }, c.env);
16
17
  if (fault)
17
18
  return fault;
18
19
  const matched = apiRoutes.match(request.method, url.pathname);
@@ -22,7 +23,7 @@ export function createApiApp(runtime) {
22
23
  const { route, params } = matched;
23
24
  try {
24
25
  const auth = authenticate(route.auth, request.headers, runtime.store.state, `${route.method} ${route.path}`, runtime.clock.now());
25
- const body = decodeJsonBody(await request.arrayBuffer());
26
+ const body = decodeJsonBody(bytes);
26
27
  const result = await route.handler({
27
28
  ...runtime,
28
29
  method: route.method,
@@ -1,8 +1,14 @@
1
1
  import type { HttpBindings } from "@hono/node-server";
2
- import type { State } from "../state/store.ts";
2
+ import type { Runtime } from "../runtime.ts";
3
+ /** What a rule can match on: the route, and the send body for a recipient domain. */
4
+ export interface RuleRequest {
5
+ method: string;
6
+ pathname: string;
7
+ body: ArrayBuffer;
8
+ }
3
9
  /**
4
- * Applies the first fault that matches the request and uses one of its `times`
5
- * (docs/09 §5 `POST /control/faults`). `timeout` holds the request open until the client gives up;
6
- * `reset` destroys the socket.
10
+ * Holds the request for the first matching latency rule, then applies the first matching fault
11
+ * (docs/09 §5 `POST /control/latency`, `POST /control/faults`). Each match uses one of the rule's
12
+ * `times`. `timeout` holds the request open until the client gives up; `reset` destroys the socket.
7
13
  */
8
- export declare function applyFault(state: State, method: string, pathname: string, env: HttpBindings): Promise<Response | undefined>;
14
+ export declare function applyFault({ store, clock }: Runtime, request: RuleRequest, env: HttpBindings): Promise<Response | undefined>;
@@ -1,23 +1,71 @@
1
1
  import { ApiError } from "../errors.js";
2
+ import { parseAddressList } from "../pipeline/addresses.js";
2
3
  import { errorResponse } from "./respond.js";
3
4
  import { RouteTable } from "./routes.js";
4
5
  /**
5
- * Applies the first fault that matches the request and uses one of its `times`
6
- * (docs/09 §5 `POST /control/faults`). `timeout` holds the request open until the client gives up;
7
- * `reset` destroys the socket.
6
+ * Holds the request for the first matching latency rule, then applies the first matching fault
7
+ * (docs/09 §5 `POST /control/latency`, `POST /control/faults`). Each match uses one of the rule's
8
+ * `times`. `timeout` holds the request open until the client gives up; `reset` destroys the socket.
8
9
  */
9
- export async function applyFault(state, method, pathname, env) {
10
- const fault = state.faults.find((f) => {
11
- if (f.remaining <= 0)
10
+ export async function applyFault({ store, clock }, request, env) {
11
+ let domains;
12
+ const mailsDomain = (domain) => {
13
+ domains ??= recipientDomains(request.body);
14
+ return domains.has(domain);
15
+ };
16
+ const latency = take(store.state.latencies, request, mailsDomain);
17
+ if (latency)
18
+ await clock.hold(latency.ms);
19
+ const fault = take(store.state.faults, request, mailsDomain);
20
+ return fault && reply(fault, env);
21
+ }
22
+ function take(rules, request, mailsDomain) {
23
+ const rule = rules.find((r) => {
24
+ if (r.remaining <= 0)
12
25
  return false;
13
26
  const table = new RouteTable();
14
- table.add({ method: f.method.toUpperCase(), path: f.path });
15
- return table.match(method, pathname) !== undefined;
27
+ table.add({ method: r.method.toUpperCase(), path: r.path });
28
+ if (table.match(request.method, request.pathname) === undefined)
29
+ return false;
30
+ return r.recipientDomain === undefined || mailsDomain(r.recipientDomain);
16
31
  });
17
- if (!fault)
32
+ if (rule)
33
+ rule.remaining -= 1;
34
+ return rule;
35
+ }
36
+ /**
37
+ * The lower-case domains of every To, Cc and Bcc address in a send body: one message, a batch
38
+ * array, or a `Messages` list. Keys match without case (docs/08 R8). A body that is not JSON, or a
39
+ * field with a malformed entry, gives none; its route answers the Postmark error.
40
+ */
41
+ function recipientDomains(bytes) {
42
+ let body;
43
+ try {
44
+ body = JSON.parse(new TextDecoder("utf-8").decode(bytes));
45
+ }
46
+ catch {
47
+ return new Set();
48
+ }
49
+ const list = field(body, "Messages");
50
+ const messages = Array.isArray(body) ? body : Array.isArray(list) ? list : [body];
51
+ const domains = new Set();
52
+ for (const message of messages) {
53
+ for (const name of ["To", "Cc", "Bcc"]) {
54
+ const raw = field(message, name);
55
+ if (typeof raw !== "string")
56
+ continue;
57
+ for (const { Email } of parseAddressList(raw) ?? []) {
58
+ domains.add(Email.slice(Email.lastIndexOf("@") + 1).toLowerCase());
59
+ }
60
+ }
61
+ }
62
+ return domains;
63
+ }
64
+ function field(value, name) {
65
+ if (typeof value !== "object" || value === null || Array.isArray(value))
18
66
  return undefined;
19
- fault.remaining -= 1;
20
- return reply(fault, env);
67
+ const key = Object.keys(value).find((k) => k.toLowerCase() === name.toLowerCase());
68
+ return key === undefined ? undefined : value[key];
21
69
  }
22
70
  async function reply(fault, env) {
23
71
  const { reply } = fault;
@@ -1,5 +1,6 @@
1
1
  import { type Plugin } from "./plugins.ts";
2
2
  import { type Runtime } from "./runtime.ts";
3
+ import { type ClockMode } from "./state/clock.ts";
3
4
  export interface PostmockConfig {
4
5
  host: string;
5
6
  /** Plain-http REST port; 0 picks a free port. */
@@ -12,6 +13,8 @@ export interface PostmockConfig {
12
13
  cert: string;
13
14
  };
14
15
  seed: string;
16
+ /** `manual`: time moves only on `POST /control/clock/advance`. */
17
+ clock: ClockMode;
15
18
  /** Defaults to every plugin in `src/plugins/`. */
16
19
  plugins?: readonly Plugin[];
17
20
  }
@@ -5,6 +5,7 @@ import { applySeed } from "./control/seed.js";
5
5
  import { createApiApp } from "./http/app.js";
6
6
  import { PLUGINS } from "./plugins.js";
7
7
  import { createRuntime } from "./runtime.js";
8
+ import { Clock } from "./state/clock.js";
8
9
  function listen(name, fetch, host, port, tls) {
9
10
  return new Promise((resolve) => {
10
11
  const onListen = (info) => resolve({
@@ -19,7 +20,7 @@ function listen(name, fetch, host, port, tls) {
19
20
  }
20
21
  /** Seeds the state, then starts the REST, control and plugin listeners. */
21
22
  export async function startPostmock(config) {
22
- const runtime = createRuntime(config.plugins ?? PLUGINS);
23
+ const runtime = createRuntime(config.plugins ?? PLUGINS, new Clock(Date.now, config.clock));
23
24
  await applySeed(runtime, config.seed);
24
25
  const api = createApiApp(runtime);
25
26
  const started = [await listen("api", api.fetch, config.host, config.apiPort)];
@@ -41,9 +42,10 @@ export async function startPostmock(config) {
41
42
  runtime,
42
43
  listeners: Object.fromEntries(started.map((l) => [l.name, l.url])),
43
44
  close: async () => {
44
- await Promise.all(started.map((l) => l.close()));
45
+ // Reset first: a listener closes only after its held requests answer.
45
46
  await runtime.clock.idle();
46
47
  runtime.clock.reset();
48
+ await Promise.all(started.map((l) => l.close()));
47
49
  },
48
50
  };
49
51
  }
@@ -1,19 +1,29 @@
1
+ /** `real`: time follows real time plus advances. `manual`: time moves only on `advance()`. */
2
+ export type ClockMode = "real" | "manual";
1
3
  type Run = () => void | Promise<void>;
2
4
  /**
3
5
  * Real time plus an offset that `advance()` grows (docs/09 §5 `clock/advance`).
4
6
  * A scheduled task runs once: when real time reaches it, or when `advance()` passes it.
7
+ * A `manual` clock starts at real time, stands still, and only `advance()` moves it and runs tasks.
5
8
  * Advances and real-timer tasks run one at a time, in the order they start.
6
9
  */
7
10
  export declare class Clock {
8
11
  private offsetMs;
12
+ /** The instant a manual clock stands at; `undefined` on a real clock. */
13
+ private manualNow;
9
14
  private seq;
10
15
  private advancing;
11
16
  private queue;
12
17
  private readonly tasks;
18
+ private readonly holds;
13
19
  private readonly realNow;
14
- constructor(realNow?: () => number);
20
+ constructor(realNow?: () => number, mode?: ClockMode);
15
21
  now(): Date;
22
+ /** The number of tasks not yet run. */
23
+ get pending(): number;
16
24
  schedule(delayMs: number, run: Run): void;
25
+ /** Resolves after `delayMs` on the clock, or at `reset()`, so a held request always answers. */
26
+ hold(delayMs: number): Promise<void>;
17
27
  /**
18
28
  * Moves time forward by `ms`, after any advance already running. Runs each task due by the end in
19
29
  * due order, with `now()` set to its due time, and awaits it. A task scheduled during the advance
@@ -22,9 +32,12 @@ export declare class Clock {
22
32
  advance(ms: number): Promise<void>;
23
33
  /** Resolves when every advance and real-timer task started so far has finished. */
24
34
  idle(): Promise<void>;
25
- /** Drops pending tasks and the offset. Throws during an advance, which would undo it. */
35
+ /**
36
+ * Drops pending tasks and the offset, and releases every hold; a manual clock stands at real time
37
+ * again. Throws during an advance, which would undo it.
38
+ */
26
39
  reset(): void;
27
- /** Captures offset and pending tasks; the returned function puts them back. */
40
+ /** Captures time and pending tasks; the returned function puts them back. */
28
41
  checkpoint(): () => void;
29
42
  private add;
30
43
  private enqueue;
@@ -3,20 +3,30 @@ const MAX_TIMER_MS = 2 ** 31 - 1;
3
3
  /**
4
4
  * Real time plus an offset that `advance()` grows (docs/09 §5 `clock/advance`).
5
5
  * A scheduled task runs once: when real time reaches it, or when `advance()` passes it.
6
+ * A `manual` clock starts at real time, stands still, and only `advance()` moves it and runs tasks.
6
7
  * Advances and real-timer tasks run one at a time, in the order they start.
7
8
  */
8
9
  export class Clock {
9
10
  offsetMs = 0;
11
+ /** The instant a manual clock stands at; `undefined` on a real clock. */
12
+ manualNow;
10
13
  seq = 0;
11
14
  advancing = false;
12
15
  queue = Promise.resolve();
13
16
  tasks = new Set();
17
+ holds = new Set();
14
18
  realNow;
15
- constructor(realNow = Date.now) {
19
+ constructor(realNow = Date.now, mode = "real") {
16
20
  this.realNow = realNow;
21
+ if (mode === "manual")
22
+ this.manualNow = realNow();
17
23
  }
18
24
  now() {
19
- return new Date(this.realNow() + this.offsetMs);
25
+ return new Date(this.manualNow ?? this.realNow() + this.offsetMs);
26
+ }
27
+ /** The number of tasks not yet run. */
28
+ get pending() {
29
+ return this.tasks.size;
20
30
  }
21
31
  schedule(delayMs, run) {
22
32
  if (!Number.isInteger(delayMs) || delayMs < 0) {
@@ -24,6 +34,16 @@ export class Clock {
24
34
  }
25
35
  this.add({ due: this.now().getTime() + delayMs, seq: this.seq++, run, timer: undefined });
26
36
  }
37
+ /** Resolves after `delayMs` on the clock, or at `reset()`, so a held request always answers. */
38
+ hold(delayMs) {
39
+ return new Promise((resolve) => {
40
+ this.holds.add(resolve);
41
+ this.schedule(delayMs, () => {
42
+ this.holds.delete(resolve);
43
+ resolve();
44
+ });
45
+ });
46
+ }
27
47
  /**
28
48
  * Moves time forward by `ms`, after any advance already running. Runs each task due by the end in
29
49
  * due order, with `now()` set to its due time, and awaits it. A task scheduled during the advance
@@ -57,27 +77,39 @@ export class Clock {
57
77
  idle() {
58
78
  return this.queue;
59
79
  }
60
- /** Drops pending tasks and the offset. Throws during an advance, which would undo it. */
80
+ /**
81
+ * Drops pending tasks and the offset, and releases every hold; a manual clock stands at real time
82
+ * again. Throws during an advance, which would undo it.
83
+ */
61
84
  reset() {
62
85
  if (this.advancing)
63
86
  throw new Error("clock.reset during an advance");
64
87
  for (const task of this.tasks)
65
88
  clearTimeout(task.timer);
66
89
  this.tasks.clear();
90
+ for (const release of this.holds)
91
+ release();
92
+ this.holds.clear();
67
93
  this.offsetMs = 0;
94
+ if (this.manualNow !== undefined)
95
+ this.manualNow = this.realNow();
68
96
  }
69
- /** Captures offset and pending tasks; the returned function puts them back. */
97
+ /** Captures time and pending tasks; the returned function puts them back. */
70
98
  checkpoint() {
71
- const offsetMs = this.offsetMs;
99
+ const { offsetMs, manualNow } = this;
72
100
  const tasks = [...this.tasks].map(({ due, seq, run }) => ({ due, seq, run }));
73
101
  return () => {
74
102
  this.reset();
75
103
  this.offsetMs = offsetMs;
104
+ this.manualNow = manualNow;
76
105
  for (const task of tasks)
77
106
  this.add({ ...task, timer: undefined });
78
107
  };
79
108
  }
80
109
  add(task) {
110
+ this.tasks.add(task);
111
+ if (this.manualNow !== undefined)
112
+ return;
81
113
  const delay = task.due - this.now().getTime();
82
114
  if (delay <= MAX_TIMER_MS) {
83
115
  // A task that throws on a real timer is a postmock bug: rethrow so the process crashes.
@@ -87,7 +119,6 @@ export class Clock {
87
119
  }));
88
120
  }, Math.max(0, delay)).unref();
89
121
  }
90
- this.tasks.add(task);
91
122
  }
92
123
  enqueue(work) {
93
124
  const result = this.queue.then(work);
@@ -96,7 +127,11 @@ export class Clock {
96
127
  }
97
128
  /** Time never moves back. */
98
129
  moveTo(instant) {
99
- this.offsetMs = Math.max(this.now().getTime(), instant) - this.realNow();
130
+ const target = Math.max(this.now().getTime(), instant);
131
+ if (this.manualNow === undefined)
132
+ this.offsetMs = target - this.realNow();
133
+ else
134
+ this.manualNow = target;
100
135
  }
101
136
  async take(task) {
102
137
  if (!this.tasks.delete(task))
@@ -1,4 +1,4 @@
1
- import type { Account, Bounce, BulkRequest, ClickEvent, DataRemoval, Domain, Fault, InboundMessage, InboundRule, MessageStream, OpenEvent, OutboundMessage, SenderSignature, Server, SmtpFault, SmtpToken, StatsFact, Suppression, Template, Webhook, WebhookAttempt } from "./types.ts";
1
+ import type { Account, Bounce, BulkRequest, ClickEvent, DataRemoval, Domain, Fault, InboundMessage, InboundRule, Latency, MessageStream, OpenEvent, OutboundMessage, SenderSignature, Server, SmtpFault, SmtpToken, StatsFact, Suppression, Template, Webhook, WebhookAttempt } from "./types.ts";
2
2
  export type IdKind = "server" | "bounce" | "template" | "webhook" | "webhookAttempt" | "inboundRule" | "domain" | "sender" | "dataRemoval";
3
3
  export interface State {
4
4
  account: Account;
@@ -32,6 +32,7 @@ export interface State {
32
32
  /** Key: access key. */
33
33
  smtpTokens: Map<string, SmtpToken>;
34
34
  faults: Fault[];
35
+ latencies: Latency[];
35
36
  smtpFaults: SmtpFault[];
36
37
  usedIds: Record<IdKind, Set<number>>;
37
38
  /** Highest used ID per kind. */
@@ -29,6 +29,7 @@ function emptyState() {
29
29
  dataRemovals: new Map(),
30
30
  smtpTokens: new Map(),
31
31
  faults: [],
32
+ latencies: [],
32
33
  smtpFaults: [],
33
34
  usedIds: {
34
35
  server: new Set(),
@@ -521,10 +521,19 @@ export type FaultReply = {
521
521
  Message: string;
522
522
  };
523
523
  } | "timeout" | "reset";
524
- export interface Fault {
524
+ /** A control-API rule for REST requests: `path` is a route pattern, matched like an API route. */
525
+ export interface RequestRule {
525
526
  method: string;
526
- /** A route pattern, matched like an API route (`/email`, `/templates/:id`). */
527
+ /** `/email`, `/templates/:id` */
527
528
  path: string;
529
+ /** Lower case. Only a request with a To, Cc or Bcc address at this domain matches. */
530
+ recipientDomain: string | undefined;
528
531
  remaining: number;
532
+ }
533
+ export interface Fault extends RequestRule {
529
534
  reply: FaultReply;
530
535
  }
536
+ /** `POST /control/latency`: the request waits `ms` on the clock, then goes on as usual. */
537
+ export interface Latency extends RequestRule {
538
+ ms: number;
539
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@benclmnt/postmock",
3
- "version": "0.0.3",
3
+ "version": "0.0.5",
4
4
  "description": "A mock Postmark server for testing code that sends email through Postmark",
5
5
  "license": "MIT",
6
6
  "repository": {