@voltro/plugin-mail 0.85.0 → 0.87.0

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/outbox.js ADDED
@@ -0,0 +1,15 @@
1
+ //#region src/outbox.ts
2
+ var e = (e = 200) => {
3
+ let t = [];
4
+ return {
5
+ record: (n) => {
6
+ t.push(n), t.length > e && t.splice(0, t.length - e);
7
+ },
8
+ list: () => [...t].reverse(),
9
+ clear: () => {
10
+ t.length = 0;
11
+ }
12
+ };
13
+ };
14
+ //#endregion
15
+ export { e as createOutbox };
@@ -0,0 +1,377 @@
1
+ import { MailError as e, MailWebhookError as t, toList as n } from "./types.js";
2
+ import { Effect as r } from "effect";
3
+ import { FetchHttpClient as i, HttpClient as a, HttpClientRequest as o } from "effect/http";
4
+ //#region src/providers.ts
5
+ var s = 0, c = (e) => `${e}-${s += 1}`, l = (e) => e === void 0 || e === 408 || e === 429 || e >= 500, u = (t, n) => {
6
+ let r = n.status;
7
+ return new e({
8
+ provider: t,
9
+ message: (n.message ?? "send failed").slice(0, 500),
10
+ transient: t === "smtp" ? r === void 0 || r >= 400 && r < 500 : l(r),
11
+ ...r === void 0 ? {} : { status: r }
12
+ });
13
+ }, d = (e, t, n, i, a) => r.tryPromise({
14
+ try: async () => {
15
+ let e = await fetch(t, {
16
+ method: "POST",
17
+ headers: {
18
+ "content-type": "application/json",
19
+ ...n
20
+ },
21
+ body: JSON.stringify(i)
22
+ });
23
+ if (!e.ok) {
24
+ let t = await e.text().catch(() => "");
25
+ throw {
26
+ status: e.status,
27
+ message: t || `HTTP ${e.status}`
28
+ };
29
+ }
30
+ return await a(e);
31
+ },
32
+ catch: (t) => {
33
+ let n = t;
34
+ return u(e, {
35
+ ...typeof n.status == "number" ? { status: n.status } : {},
36
+ message: n.message ?? t?.message ?? String(t)
37
+ });
38
+ }
39
+ }), f = 500, p = (e, t) => {
40
+ let r = e.stream === "broadcast" ? t?.broadcast ?? "broadcast" : t?.transactional;
41
+ return {
42
+ ...r === void 0 ? {} : { route: r },
43
+ from: e.from,
44
+ to: n(e.to),
45
+ ...e.cc ? { cc: n(e.cc) } : {},
46
+ ...e.bcc ? { bcc: n(e.bcc) } : {},
47
+ ...e.replyTo ? { reply_to: [e.replyTo] } : {},
48
+ subject: e.subject,
49
+ ...e.html === void 0 ? {} : { html: e.html },
50
+ ...e.text === void 0 ? {} : { text: e.text },
51
+ ...e.headers ? { headers: e.headers } : {},
52
+ ...e.metadata ? { metadata: e.metadata } : {},
53
+ ...e.tags && e.tags.length > 0 ? { tags: e.tags.map((e) => ({
54
+ name: e,
55
+ value: "1"
56
+ })) } : {},
57
+ ...e.scheduledAt ? { scheduled_at: e.scheduledAt.toISOString() } : {},
58
+ ...e.attachments ? { attachments: e.attachments.map((e) => ({
59
+ filename: e.filename,
60
+ content: e.content,
61
+ ...e.contentType ? { content_type: e.contentType } : {}
62
+ })) } : {}
63
+ };
64
+ }, m = (e, n, i, a = Math.floor(Date.now() / 1e3), o = 300) => r.tryPromise({
65
+ try: async () => {
66
+ let r = (e) => {
67
+ throw new t({
68
+ provider: "lettermint",
69
+ message: e
70
+ });
71
+ }, s = (t) => Object.entries(e).find(([e]) => e.toLowerCase() === t)?.[1];
72
+ i === "" && r("no webhook secret configured"), n === "" && r("empty body");
73
+ let c = s("x-lettermint-signature") ?? r("missing x-lettermint-signature"), l = s("x-lettermint-delivery") ?? r("missing x-lettermint-delivery"), u, d = [];
74
+ for (let e of c.split(",")) {
75
+ let [t, n] = e.trim().split("=", 2);
76
+ t === "t" && n !== void 0 && /^\d+$/.test(n) ? u = n : t === "v1" && n !== void 0 && /^[a-fA-F0-9]{64}$/.test(n) && d.push(n.toLowerCase());
77
+ }
78
+ (u === void 0 || d.length === 0) && r("malformed signature header"), l !== u && r("signature and delivery timestamps differ"), Math.abs(a - Number(u)) > o && r("signature timestamp outside the tolerance");
79
+ let { createHmac: f, timingSafeEqual: p } = await import("node:crypto"), m = f("sha256", i).update(`${u}.`).update(n).digest();
80
+ d.some((e) => p(Buffer.from(e, "hex"), m)) || r("signature does not match");
81
+ try {
82
+ return JSON.parse(n);
83
+ } catch {
84
+ return r("body is not JSON");
85
+ }
86
+ },
87
+ catch: (e) => e instanceof t ? e : new t({
88
+ provider: "lettermint",
89
+ message: e?.message ?? String(e)
90
+ })
91
+ }), h = (e) => {
92
+ let t = (e.baseUrl ?? "https://api.lettermint.co/v1").replace(/\/+$/, ""), n = {
93
+ "x-lettermint-token": e.projectToken,
94
+ accept: "application/json"
95
+ };
96
+ return {
97
+ name: "lettermint",
98
+ supportsScheduling: !0,
99
+ managesUnsubscribe: !0,
100
+ send: (r) => d("lettermint", `${t}/send`, {
101
+ ...n,
102
+ ...r.idempotencyKey === void 0 ? {} : { "idempotency-key": r.idempotencyKey }
103
+ }, p(r, e.routes), async (e) => ({
104
+ id: (await e.json()).message_id ?? "lettermint",
105
+ provider: "lettermint"
106
+ })),
107
+ sendBatch: (i) => r.forEach(Array.from({ length: Math.ceil(i.length / f) }, (e, t) => i.slice(t * f, (t + 1) * f)), (r) => d("lettermint", `${t}/send/batch`, n, r.map((t) => p(t, e.routes)), async (e) => {
108
+ let t = await e.json();
109
+ return r.map((e, n) => ({
110
+ id: t[n]?.message_id ?? "lettermint",
111
+ provider: "lettermint"
112
+ }));
113
+ })).pipe(r.map((e) => e.flat())),
114
+ webhook: { verify: m }
115
+ };
116
+ }, g = (e) => ({
117
+ from: e.from,
118
+ to: n(e.to),
119
+ subject: e.subject,
120
+ html: e.html,
121
+ text: e.text,
122
+ reply_to: e.replyTo,
123
+ cc: e.cc ? n(e.cc) : void 0,
124
+ bcc: e.bcc ? n(e.bcc) : void 0,
125
+ headers: e.headers,
126
+ tags: e.tags?.map((e) => ({
127
+ name: e,
128
+ value: "1"
129
+ })),
130
+ ...e.scheduledAt ? { scheduled_at: e.scheduledAt.toISOString() } : {},
131
+ attachments: e.attachments?.map((e) => ({
132
+ filename: e.filename,
133
+ content: e.content,
134
+ ...e.contentType ? { content_type: e.contentType } : {}
135
+ }))
136
+ }), _ = (e) => ({
137
+ name: "resend",
138
+ supportsScheduling: !0,
139
+ send: (t) => d("resend", "https://api.resend.com/emails", { authorization: `Bearer ${e.apiKey}` }, g(t), async (e) => ({
140
+ id: (await e.json()).id ?? "resend",
141
+ provider: "resend"
142
+ })),
143
+ sendBatch: (t) => d("resend", "https://api.resend.com/emails/batch", { authorization: `Bearer ${e.apiKey}` }, t.map(g), async (e) => {
144
+ let n = (await e.json()).data ?? [];
145
+ return t.map((e, t) => ({
146
+ id: n[t]?.id ?? "resend",
147
+ provider: "resend"
148
+ }));
149
+ })
150
+ }), v = (e) => ({
151
+ name: "postmark",
152
+ send: (t) => d("postmark", "https://api.postmarkapp.com/email", {
153
+ "x-postmark-server-token": e.serverToken,
154
+ accept: "application/json"
155
+ }, {
156
+ From: t.from,
157
+ To: n(t.to).join(","),
158
+ Subject: t.subject,
159
+ HtmlBody: t.html,
160
+ TextBody: t.text,
161
+ ReplyTo: t.replyTo,
162
+ Cc: t.cc ? n(t.cc).join(",") : void 0,
163
+ Bcc: t.bcc ? n(t.bcc).join(",") : void 0,
164
+ MessageStream: t.stream === "broadcast" ? e.streams?.broadcast ?? "broadcast" : e.streams?.transactional ?? "outbound",
165
+ Headers: t.headers ? Object.entries(t.headers).map(([e, t]) => ({
166
+ Name: e,
167
+ Value: t
168
+ })) : void 0,
169
+ Metadata: t.metadata,
170
+ Attachments: t.attachments?.map((e) => ({
171
+ Name: e.filename,
172
+ Content: e.content,
173
+ ContentType: e.contentType ?? "application/octet-stream"
174
+ }))
175
+ }, async (e) => ({
176
+ id: (await e.json()).MessageID ?? "postmark",
177
+ provider: "postmark"
178
+ }))
179
+ }), y = (e) => [...e.text ? [{
180
+ type: "text/plain",
181
+ value: e.text
182
+ }] : [], ...e.html ? [{
183
+ type: "text/html",
184
+ value: e.html
185
+ }] : []], b = (e) => ({
186
+ to: n(e.to).map((e) => ({ email: e })),
187
+ cc: e.cc ? n(e.cc).map((e) => ({ email: e })) : void 0,
188
+ bcc: e.bcc ? n(e.bcc).map((e) => ({ email: e })) : void 0,
189
+ subject: e.subject,
190
+ ...e.scheduledAt ? { send_at: Math.floor(e.scheduledAt.getTime() / 1e3) } : {}
191
+ }), x = (e) => e.attachments?.map((e) => ({
192
+ filename: e.filename,
193
+ content: e.content,
194
+ type: e.contentType ?? "application/octet-stream",
195
+ disposition: "attachment"
196
+ })), S = (e) => {
197
+ let t = (e) => ({
198
+ personalizations: [b(e)],
199
+ from: { email: e.from },
200
+ reply_to: e.replyTo ? { email: e.replyTo } : void 0,
201
+ subject: e.subject,
202
+ content: y(e),
203
+ attachments: x(e)
204
+ });
205
+ return {
206
+ name: "sendgrid",
207
+ supportsScheduling: !0,
208
+ send: (n) => d("sendgrid", "https://api.sendgrid.com/v3/mail/send", { authorization: `Bearer ${e.apiKey}` }, t(n), async (e) => ({
209
+ id: e.headers.get("x-message-id") ?? "sendgrid",
210
+ provider: "sendgrid"
211
+ })),
212
+ sendBatch: (t) => d("sendgrid", "https://api.sendgrid.com/v3/mail/send", { authorization: `Bearer ${e.apiKey}` }, {
213
+ personalizations: t.map(b),
214
+ from: { email: t[0]?.from },
215
+ subject: t[0]?.subject,
216
+ content: y(t[0] ?? {})
217
+ }, async (e) => {
218
+ let n = e.headers.get("x-message-id") ?? "sendgrid";
219
+ return t.map((e, t) => ({
220
+ id: `${n}-${t}`,
221
+ provider: "sendgrid"
222
+ }));
223
+ })
224
+ };
225
+ }, C = (e, t) => {
226
+ let n = t, r = n.response?.status;
227
+ return u(e, {
228
+ ...typeof r == "number" ? { status: r } : {},
229
+ message: n.message ?? String(t)
230
+ });
231
+ }, w = (t, n, o) => r.gen(function* () {
232
+ let e = yield* (yield* a.HttpClient).execute(n);
233
+ if (e.status < 200 || e.status >= 300) {
234
+ let n = yield* e.text.pipe(r.catch(() => r.succeed("")));
235
+ return yield* r.fail(u(t, {
236
+ status: e.status,
237
+ message: n || `HTTP ${e.status}`
238
+ }));
239
+ }
240
+ return yield* o(e);
241
+ }).pipe(r.catch((n) => n instanceof e ? r.fail(n) : r.fail(C(t, n))), r.provide(i.layer), r.provideService(i.Fetch, (e, t) => globalThis.fetch(e, t))), T = (e) => ({ sign: async (t, n, r) => {
242
+ let { createHash: i, createHmac: a } = await import("node:crypto"), o = (e) => i("sha256").update(e).digest("hex"), s = (e, t) => a("sha256", e).update(t).digest(), c = (/* @__PURE__ */ new Date()).toISOString().replace(/[:-]|\.\d{3}/g, ""), l = c.slice(0, 8), u = `host:${new URL(t).host}\nx-amz-date:${c}\n` + (e.sessionToken ? `x-amz-security-token:${e.sessionToken}\n` : ""), d = e.sessionToken ? "host;x-amz-date;x-amz-security-token" : "host;x-amz-date", f = `POST\n${n}\n\n${u}\n${d}\n${o(r)}`, p = `${l}/${e.region}/ses/aws4_request`, m = `AWS4-HMAC-SHA256\n${c}\n${p}\n${o(f)}`, h = a("sha256", s(s(s(s(`AWS4${e.secretAccessKey}`, l), e.region), "ses"), "aws4_request")).update(m).digest("hex");
243
+ return {
244
+ authorization: `AWS4-HMAC-SHA256 Credential=${e.accessKeyId}/${p}, SignedHeaders=${d}, Signature=${h}`,
245
+ "x-amz-date": c,
246
+ ...e.sessionToken ? { "x-amz-security-token": e.sessionToken } : {}
247
+ };
248
+ } }), E = (e) => JSON.stringify({
249
+ FromEmailAddress: e.from,
250
+ Destination: {
251
+ ToAddresses: n(e.to),
252
+ ...e.cc ? { CcAddresses: n(e.cc) } : {},
253
+ ...e.bcc ? { BccAddresses: n(e.bcc) } : {}
254
+ },
255
+ ...e.replyTo ? { ReplyToAddresses: [e.replyTo] } : {},
256
+ Content: { Simple: {
257
+ Subject: {
258
+ Data: e.subject,
259
+ Charset: "UTF-8"
260
+ },
261
+ Body: {
262
+ ...e.html ? { Html: {
263
+ Data: e.html,
264
+ Charset: "UTF-8"
265
+ } } : {},
266
+ ...e.text ? { Text: {
267
+ Data: e.text,
268
+ Charset: "UTF-8"
269
+ } } : {}
270
+ }
271
+ } }
272
+ }), D = (e) => {
273
+ let t = T(e), n = `https://email.${e.region}.amazonaws.com`, i = "/v2/email/outbound-emails";
274
+ return {
275
+ name: "ses",
276
+ send: (e) => r.gen(function* () {
277
+ let a = E(e), s = yield* r.tryPromise({
278
+ try: () => t.sign(n, i, a),
279
+ catch: (e) => u("ses", { message: e?.message ?? String(e) })
280
+ });
281
+ return yield* w("ses", o.post(`${n}${i}`).pipe(o.setHeaders({
282
+ "content-type": "application/json",
283
+ ...s
284
+ }), o.bodyJsonUnsafe(JSON.parse(a))), (e) => e.json.pipe(r.map((e) => ({
285
+ id: e.MessageId ?? "ses",
286
+ provider: "ses"
287
+ }))));
288
+ })
289
+ };
290
+ }, O = (e) => {
291
+ let t = [["from", e.from], ["subject", e.subject]];
292
+ for (let r of n(e.to)) t.push(["to", r]);
293
+ if (e.cc) for (let r of n(e.cc)) t.push(["cc", r]);
294
+ if (e.bcc) for (let r of n(e.bcc)) t.push(["bcc", r]);
295
+ if (e.replyTo && t.push(["h:Reply-To", e.replyTo]), e.html && t.push(["html", e.html]), e.text && t.push(["text", e.text]), e.headers) for (let [n, r] of Object.entries(e.headers)) t.push([`h:${n}`, r]);
296
+ if (e.tags) for (let n of e.tags) t.push(["o:tag", n]);
297
+ return e.scheduledAt && t.push(["o:deliverytime", e.scheduledAt.toUTCString()]), t;
298
+ }, k = (e) => {
299
+ let t = `${e.baseUrl ?? "https://api.mailgun.net"}/v3/${e.domain}/messages`;
300
+ return {
301
+ name: "mailgun",
302
+ supportsScheduling: !0,
303
+ send: (n) => {
304
+ let i = new URLSearchParams();
305
+ for (let [e, t] of O(n)) i.append(e, t);
306
+ return w("mailgun", o.post(t).pipe(o.setHeader("authorization", `Basic ${Buffer.from(`api:${e.apiKey}`).toString("base64")}`), o.bodyText(i.toString(), "application/x-www-form-urlencoded")), (e) => e.json.pipe(r.map((e) => ({
307
+ id: e.id ?? "mailgun",
308
+ provider: "mailgun"
309
+ }))));
310
+ }
311
+ };
312
+ }, A = (e) => {
313
+ let t = null;
314
+ return {
315
+ name: "smtp",
316
+ send: (i) => r.tryPromise({
317
+ try: async () => {
318
+ if (!t) {
319
+ let n = await import("nodemailer"), r = n.createTransport ?? n.default?.createTransport;
320
+ if (!r) throw Error("nodemailer: createTransport export not found");
321
+ t = r(e.url ?? {
322
+ host: e.host,
323
+ port: e.port,
324
+ secure: e.secure,
325
+ ...e.auth ? { auth: e.auth } : {}
326
+ });
327
+ }
328
+ return {
329
+ id: (await t.sendMail({
330
+ from: i.from,
331
+ to: n(i.to),
332
+ cc: i.cc ? n(i.cc) : void 0,
333
+ bcc: i.bcc ? n(i.bcc) : void 0,
334
+ replyTo: i.replyTo,
335
+ subject: i.subject,
336
+ html: i.html,
337
+ text: i.text,
338
+ headers: i.headers,
339
+ attachments: i.attachments?.map((e) => ({
340
+ filename: e.filename,
341
+ content: e.content,
342
+ encoding: "base64",
343
+ ...e.contentType ? { contentType: e.contentType } : {}
344
+ }))
345
+ }))?.messageId ?? "smtp",
346
+ provider: "smtp"
347
+ };
348
+ },
349
+ catch: (e) => {
350
+ let t = e;
351
+ return u("smtp", {
352
+ ...typeof t.responseCode == "number" ? { status: t.responseCode } : {},
353
+ message: t.message ?? String(e)
354
+ });
355
+ }
356
+ })
357
+ };
358
+ }, j = (e) => ({
359
+ name: "console",
360
+ send: (t) => r.sync(() => {
361
+ let r = `✉ to=${n(t.to).join(",")} subject=${JSON.stringify(t.subject)}`;
362
+ return e ? e(r, { from: t.from }) : process.stdout.write(`[mail:console] ${r}\n`), {
363
+ id: c("console"),
364
+ provider: "console"
365
+ };
366
+ })
367
+ }), M = [], N = () => [...M], P = () => {
368
+ M.length = 0;
369
+ }, F = () => ({
370
+ name: "memory",
371
+ send: (e) => r.sync(() => (M.push(e), {
372
+ id: c("mem"),
373
+ provider: "memory"
374
+ }))
375
+ });
376
+ //#endregion
377
+ export { P as clearMailBuffer, j as consoleProvider, h as lettermintProvider, k as mailgunProvider, F as memoryProvider, v as postmarkProvider, N as readMailBuffer, _ as resendProvider, S as sendgridProvider, D as sesProvider, A as smtpProvider, m as verifyLettermintSignature };
@@ -0,0 +1,13 @@
1
+ //#region src/registry.ts
2
+ var e = /* @__PURE__ */ new Map(), t = (e) => e.locale ? `${e.name}:${e.locale}` : e.name, n = (n) => {
3
+ for (let r of n) e.set(t(r), r);
4
+ }, r = (t, n) => (n ? e.get(`${t}:${n}`) : void 0) ?? e.get(t), i = () => e.size, a = () => [...e.values()].map((e) => e.locale ? {
5
+ name: e.name,
6
+ locale: e.locale
7
+ } : { name: e.name }), o = () => e.clear(), s = (e) => {
8
+ if (typeof e != "object" || !e) return !1;
9
+ let t = e;
10
+ return typeof t.name == "string" && typeof t.subject == "function" && typeof t.render == "function" && t.props !== void 0;
11
+ };
12
+ //#endregion
13
+ export { o as clearGlobalTemplates, i as globalTemplateCount, s as isEmailTemplate, a as listGlobalTemplates, r as lookupGlobalTemplate, n as registerEmailTemplates };
package/dist/render.js ADDED
@@ -0,0 +1,44 @@
1
+ import { MailError as e } from "./types.js";
2
+ import { Effect as t, Schema as n, SchemaIssue as r } from "effect";
3
+ //#region src/render.ts
4
+ var i = (e) => {
5
+ try {
6
+ let t = r.makeFormatterStandardSchemaV1()(e.issue).issues;
7
+ return t.length === 0 ? String(e) : t.map((e) => `${e.path !== void 0 && e.path.length > 0 ? e.path.map(String).join(".") : "(root)"}: ${e.message}`).join("; ");
8
+ } catch {
9
+ return String(e);
10
+ }
11
+ }, a = null, o = async () => {
12
+ if (!a) {
13
+ let e = await import("@react-email/render");
14
+ if (a = e.render ?? e.default?.render ?? null, !a) throw Error("@react-email/render: `render` export not found (install @react-email/render + react + react-dom)");
15
+ }
16
+ return a;
17
+ }, s = (r, a) => t.gen(function* () {
18
+ let s = r, c = yield* n.decodeUnknownEffect(s.props)(a).pipe(t.mapError((t) => new e({
19
+ provider: "template",
20
+ message: `invalid props for '${r.name}': ${i(t)}`.slice(0, 500),
21
+ transient: !1
22
+ }))), l = s.subject(c), u = s.render(c);
23
+ return {
24
+ subject: l,
25
+ html: yield* t.tryPromise({
26
+ try: async () => (await o())(u, { plainText: !1 }),
27
+ catch: (t) => new e({
28
+ provider: "template",
29
+ message: `render failed for '${r.name}': ${String(t)}`.slice(0, 500),
30
+ transient: !1
31
+ })
32
+ }),
33
+ text: yield* t.tryPromise({
34
+ try: async () => (await o())(u, { plainText: !0 }),
35
+ catch: (t) => new e({
36
+ provider: "template",
37
+ message: `render(text) failed for '${r.name}': ${String(t)}`.slice(0, 500),
38
+ transient: !1
39
+ })
40
+ })
41
+ };
42
+ });
43
+ //#endregion
44
+ export { s as renderTemplate };
@@ -0,0 +1,36 @@
1
+ import { Effect as e } from "effect";
2
+ import { createLogger as t } from "@voltro/logger";
3
+ //#region src/scheduler.ts
4
+ var n = t({ scope: "@voltro/plugin-mail" }), r = () => {
5
+ let t = /* @__PURE__ */ new Map();
6
+ return {
7
+ hold: (n) => e.sync(() => void t.set(n.id, n)),
8
+ claimDue: (n) => e.sync(() => {
9
+ let e = [];
10
+ for (let [r, i] of t) i.dueAt <= n && (e.push(i), t.delete(r));
11
+ return e;
12
+ }),
13
+ size: () => e.sync(() => t.size)
14
+ };
15
+ }, i = 0, a = (e) => e.idempotencyKey ?? `held-${i += 1}`, o = (t) => ({
16
+ shouldHold: (e) => !t.providerSchedules && e !== void 0 && e.getTime() > Date.now(),
17
+ hold: (e) => {
18
+ let { scheduledAt: n, ...r } = e;
19
+ return t.store.hold({
20
+ id: a(e),
21
+ dueAt: e.scheduledAt?.getTime() ?? Date.now(),
22
+ message: r
23
+ });
24
+ },
25
+ tick: (r = Date.now()) => e.gen(function* () {
26
+ let i = yield* t.store.claimDue(r);
27
+ for (let r of i) yield* t.flush(r.message).pipe(e.catch((t) => e.sync(() => n.warn("scheduled flush failed", {
28
+ id: r.id,
29
+ message: String(t)
30
+ }))));
31
+ return i.length;
32
+ }),
33
+ pending: () => t.store.size()
34
+ });
35
+ //#endregion
36
+ export { a as heldId, o as makeScheduler, r as memoryScheduleStore };