spectrum-ts 0.3.0 → 0.5.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.
@@ -1,10 +1,9 @@
1
1
  import {
2
- asAttachment
3
- } from "../../chunk-ZRSCHSLZ.js";
4
- import {
2
+ asAttachment,
3
+ asContact,
5
4
  asCustom,
6
5
  stream
7
- } from "../../chunk-V2PK557T.js";
6
+ } from "../../chunk-UZWRB3FZ.js";
8
7
  import {
9
8
  asText,
10
9
  definePlatform
@@ -16,17 +15,153 @@ import {
16
15
  } from "@photon-ai/whatsapp-business";
17
16
 
18
17
  // src/providers/whatsapp-business/messages.ts
19
- var toMessage = async (client, msg) => {
20
- const content = await mapContent(client, msg.content);
21
- return {
22
- id: msg.id,
23
- content,
18
+ import { extension as mimeExtension } from "mime-types";
19
+ var mapWaPhoneType = (type) => {
20
+ if (!type) {
21
+ return void 0;
22
+ }
23
+ const upper = type.toUpperCase();
24
+ if (upper === "CELL" || upper === "MOBILE" || upper === "IPHONE") {
25
+ return "mobile";
26
+ }
27
+ if (upper === "HOME") {
28
+ return "home";
29
+ }
30
+ if (upper === "WORK" || upper === "BUSINESS") {
31
+ return "work";
32
+ }
33
+ return "other";
34
+ };
35
+ var mapWaSimpleType = (type) => {
36
+ if (!type) {
37
+ return void 0;
38
+ }
39
+ const upper = type.toUpperCase();
40
+ if (upper === "HOME") {
41
+ return "home";
42
+ }
43
+ if (upper === "WORK" || upper === "BUSINESS") {
44
+ return "work";
45
+ }
46
+ return "other";
47
+ };
48
+ var waNameToSpectrum = (name) => {
49
+ const result = { formatted: name.formattedName };
50
+ if (name.firstName) {
51
+ result.first = name.firstName;
52
+ }
53
+ if (name.lastName) {
54
+ result.last = name.lastName;
55
+ }
56
+ if (name.middleName) {
57
+ result.middle = name.middleName;
58
+ }
59
+ if (name.prefix) {
60
+ result.prefix = name.prefix;
61
+ }
62
+ if (name.suffix) {
63
+ result.suffix = name.suffix;
64
+ }
65
+ return result;
66
+ };
67
+ var waPhoneToSpectrum = (phone) => {
68
+ const entry = { value: phone.phone };
69
+ const type = mapWaPhoneType(phone.type);
70
+ if (type) {
71
+ entry.type = type;
72
+ }
73
+ return entry;
74
+ };
75
+ var waEmailToSpectrum = (email) => {
76
+ const entry = { value: email.email };
77
+ const type = mapWaSimpleType(email.type);
78
+ if (type) {
79
+ entry.type = type;
80
+ }
81
+ return entry;
82
+ };
83
+ var waAddressToSpectrum = (address) => {
84
+ const entry = {};
85
+ if (address.street) {
86
+ entry.street = address.street;
87
+ }
88
+ if (address.city) {
89
+ entry.city = address.city;
90
+ }
91
+ if (address.state) {
92
+ entry.region = address.state;
93
+ }
94
+ if (address.zip) {
95
+ entry.postalCode = address.zip;
96
+ }
97
+ if (address.country) {
98
+ entry.country = address.country;
99
+ }
100
+ const type = mapWaSimpleType(address.type);
101
+ if (type) {
102
+ entry.type = type;
103
+ }
104
+ return entry;
105
+ };
106
+ var waOrgToSpectrum = (org) => {
107
+ const entry = {};
108
+ if (org.company) {
109
+ entry.name = org.company;
110
+ }
111
+ if (org.title) {
112
+ entry.title = org.title;
113
+ }
114
+ if (org.department) {
115
+ entry.department = org.department;
116
+ }
117
+ return entry;
118
+ };
119
+ var waContactToSpectrum = (card) => {
120
+ const input = { raw: card };
121
+ input.name = waNameToSpectrum(card.name);
122
+ if (card.phones.length > 0) {
123
+ input.phones = card.phones.map(waPhoneToSpectrum);
124
+ }
125
+ if (card.emails.length > 0) {
126
+ input.emails = card.emails.map(waEmailToSpectrum);
127
+ }
128
+ if (card.addresses.length > 0) {
129
+ input.addresses = card.addresses.map(waAddressToSpectrum);
130
+ }
131
+ if (card.org) {
132
+ input.org = waOrgToSpectrum(card.org);
133
+ }
134
+ if (card.urls.length > 0) {
135
+ input.urls = card.urls.map((u) => u.url);
136
+ }
137
+ if (card.birthday) {
138
+ input.birthday = card.birthday;
139
+ }
140
+ return asContact(input);
141
+ };
142
+ var toMessages = (client, msg) => {
143
+ const base = {
24
144
  sender: { id: msg.from },
25
145
  space: { id: msg.from },
26
146
  timestamp: msg.timestamp
27
147
  };
148
+ if (msg.content.type === "contacts") {
149
+ const multi = msg.content.contacts.length > 1;
150
+ return msg.content.contacts.map((card, index) => ({
151
+ ...base,
152
+ id: multi ? `${msg.id}:${index}` : msg.id,
153
+ content: waContactToSpectrum(card)
154
+ }));
155
+ }
156
+ return [
157
+ {
158
+ ...base,
159
+ id: msg.id,
160
+ content: mapContent(client, msg.content)
161
+ }
162
+ ];
28
163
  };
29
- var mapContent = async (client, content) => {
164
+ var mapContent = (client, content) => {
30
165
  switch (content.type) {
31
166
  case "text":
32
167
  return asText(content.body);
@@ -34,16 +169,11 @@ var mapContent = async (client, content) => {
34
169
  case "video":
35
170
  case "audio":
36
171
  case "document":
37
- return downloadMedia(client, content.media);
172
+ return lazyMedia(client, content.media);
38
173
  case "sticker":
39
174
  return asCustom({ whatsapp_type: "sticker", ...content.sticker });
40
175
  case "location":
41
176
  return asCustom({ whatsapp_type: "location", ...content.location });
42
- case "contacts":
43
- return asCustom({
44
- whatsapp_type: "contacts",
45
- contacts: content.contacts
46
- });
47
177
  case "reaction":
48
178
  return asCustom({ whatsapp_type: "reaction", ...content.reaction });
49
179
  case "interactive":
@@ -58,27 +188,26 @@ var mapContent = async (client, content) => {
58
188
  return asCustom({ whatsapp_type: "unknown" });
59
189
  }
60
190
  };
61
- var downloadMedia = async (client, media) => {
62
- try {
63
- const { url } = await client.media.getUrl(media.id);
64
- const response = await fetch(url);
65
- if (!response.ok) {
66
- throw new Error(`Media download failed: ${response.status}`);
67
- }
68
- const data = Buffer.from(await response.arrayBuffer());
69
- return asAttachment({
70
- data,
71
- mimeType: media.mimeType,
72
- name: media.filename ?? `media-${media.id}`
73
- });
74
- } catch {
75
- return asCustom({
76
- whatsapp_type: "media_error",
77
- mediaId: media.id,
78
- mimeType: media.mimeType
79
- });
191
+ var fetchMedia = async (client, mediaId) => {
192
+ const { url } = await client.media.getUrl(mediaId);
193
+ const response = await fetch(url);
194
+ if (!response.ok) {
195
+ throw new Error(`Media download failed: ${response.status}`);
80
196
  }
197
+ return response;
81
198
  };
199
+ var lazyMedia = (client, media) => asAttachment({
200
+ name: media.filename ?? `media-${media.id}`,
201
+ mimeType: media.mimeType,
202
+ read: async () => Buffer.from(await (await fetchMedia(client, media.id)).arrayBuffer()),
203
+ stream: async () => {
204
+ const response = await fetchMedia(client, media.id);
205
+ if (!response.body) {
206
+ throw new Error("Media response missing body");
207
+ }
208
+ return response.body;
209
+ }
210
+ });
82
211
  var mimeToMediaType = (mimeType) => {
83
212
  if (mimeType.startsWith("image/")) {
84
213
  return "image";
@@ -91,6 +220,74 @@ var mimeToMediaType = (mimeType) => {
91
220
  }
92
221
  return "document";
93
222
  };
223
+ var voiceFilename = (content) => {
224
+ if (content.name) {
225
+ return content.name;
226
+ }
227
+ const ext = mimeExtension(content.mimeType);
228
+ return ext ? `voice.${ext}` : "voice";
229
+ };
230
+ var spectrumPhoneTypeToWa = (type) => {
231
+ if (type === "mobile") {
232
+ return "CELL";
233
+ }
234
+ if (type === "home" || type === "work" || type === "other") {
235
+ return type.toUpperCase();
236
+ }
237
+ return void 0;
238
+ };
239
+ var spectrumSimpleTypeToWa = (type) => type ? type.toUpperCase() : void 0;
240
+ var spectrumNameToWa = (name) => ({
241
+ formattedName: name?.formatted ?? ([name?.first, name?.middle, name?.last].filter((p) => Boolean(p)).join(" ") || "Unknown"),
242
+ firstName: name?.first,
243
+ lastName: name?.last,
244
+ middleName: name?.middle,
245
+ prefix: name?.prefix,
246
+ suffix: name?.suffix
247
+ });
248
+ var isWhatsAppContactCard = (value) => {
249
+ if (!value || typeof value !== "object") {
250
+ return false;
251
+ }
252
+ const raw = value;
253
+ const name = raw.name;
254
+ if (!name || typeof name !== "object" || typeof name.formattedName !== "string") {
255
+ return false;
256
+ }
257
+ return Array.isArray(raw.phones) && Array.isArray(raw.emails) && Array.isArray(raw.addresses) && Array.isArray(raw.urls);
258
+ };
259
+ var contactToWa = (contact) => {
260
+ if (isWhatsAppContactCard(contact.raw)) {
261
+ return contact.raw;
262
+ }
263
+ const card = {
264
+ name: spectrumNameToWa(contact.name),
265
+ phones: (contact.phones ?? []).map((p) => ({
266
+ phone: p.value,
267
+ type: spectrumPhoneTypeToWa(p.type)
268
+ })),
269
+ emails: (contact.emails ?? []).map((e) => ({
270
+ email: e.value,
271
+ type: spectrumSimpleTypeToWa(e.type)
272
+ })),
273
+ addresses: (contact.addresses ?? []).map((a) => ({
274
+ street: a.street,
275
+ city: a.city,
276
+ state: a.region,
277
+ zip: a.postalCode,
278
+ country: a.country,
279
+ type: spectrumSimpleTypeToWa(a.type)
280
+ })),
281
+ urls: (contact.urls ?? []).map((url) => ({ url })),
282
+ org: contact.org?.name || contact.org?.department || contact.org?.title ? {
283
+ company: contact.org.name,
284
+ department: contact.org.department,
285
+ title: contact.org.title
286
+ } : void 0,
287
+ birthday: contact.birthday
288
+ };
289
+ return card;
290
+ };
94
291
  var messages = (client) => {
95
292
  const eventStream = client.events.subscribe().filter(
96
293
  (e) => e.type === "message"
@@ -99,8 +296,9 @@ var messages = (client) => {
99
296
  (async () => {
100
297
  try {
101
298
  for await (const event of eventStream) {
102
- const msg = await toMessage(client, event.message);
103
- emit(msg);
299
+ for (const m of toMessages(client, event.message)) {
300
+ emit(m);
301
+ }
104
302
  }
105
303
  end();
106
304
  } catch (e) {
@@ -117,7 +315,7 @@ var send = async (client, spaceId, content) => {
117
315
  break;
118
316
  case "attachment": {
119
317
  const { mediaId } = await client.media.upload({
120
- file: content.data,
318
+ file: await content.read(),
121
319
  mimeType: content.mimeType,
122
320
  filename: content.name
123
321
  });
@@ -129,6 +327,24 @@ var send = async (client, spaceId, content) => {
129
327
  });
130
328
  break;
131
329
  }
330
+ case "contact":
331
+ await client.messages.send({
332
+ to: spaceId,
333
+ contacts: [contactToWa(content)]
334
+ });
335
+ break;
336
+ case "voice": {
337
+ const { mediaId } = await client.media.upload({
338
+ file: await content.read(),
339
+ mimeType: content.mimeType,
340
+ filename: voiceFilename(content)
341
+ });
342
+ await client.messages.send({
343
+ to: spaceId,
344
+ audio: { id: mediaId }
345
+ });
346
+ break;
347
+ }
132
348
  default:
133
349
  break;
134
350
  }
@@ -150,7 +366,7 @@ var replyToMessage = async (client, spaceId, messageId, content) => {
150
366
  break;
151
367
  case "attachment": {
152
368
  const { mediaId } = await client.media.upload({
153
- file: content.data,
369
+ file: await content.read(),
154
370
  mimeType: content.mimeType,
155
371
  filename: content.name
156
372
  });
@@ -163,6 +379,26 @@ var replyToMessage = async (client, spaceId, messageId, content) => {
163
379
  });
164
380
  break;
165
381
  }
382
+ case "contact":
383
+ await client.messages.send({
384
+ to: spaceId,
385
+ replyTo: messageId,
386
+ contacts: [contactToWa(content)]
387
+ });
388
+ break;
389
+ case "voice": {
390
+ const { mediaId } = await client.media.upload({
391
+ file: await content.read(),
392
+ mimeType: content.mimeType,
393
+ filename: voiceFilename(content)
394
+ });
395
+ await client.messages.send({
396
+ to: spaceId,
397
+ replyTo: messageId,
398
+ audio: { id: mediaId }
399
+ });
400
+ break;
401
+ }
166
402
  default:
167
403
  break;
168
404
  }
@@ -9,9 +9,75 @@ declare const contentSchema: z__default.ZodDiscriminatedUnion<[z__default.ZodObj
9
9
  raw: z__default.ZodUnknown;
10
10
  }, z__default.core.$strip>, z__default.ZodObject<{
11
11
  type: z__default.ZodLiteral<"attachment">;
12
- data: z__default.ZodCustom<Buffer<ArrayBufferLike>, Buffer<ArrayBufferLike>>;
13
- mimeType: z__default.ZodString;
14
12
  name: z__default.ZodString;
13
+ mimeType: z__default.ZodString;
14
+ size: z__default.ZodOptional<z__default.ZodNumber>;
15
+ read: z__default.ZodFunction<z__default.ZodTuple<readonly [], null>, z__default.ZodPromise<z__default.ZodCustom<Buffer<ArrayBufferLike>, Buffer<ArrayBufferLike>>>>;
16
+ stream: z__default.ZodFunction<z__default.ZodTuple<readonly [], null>, z__default.ZodPromise<z__default.ZodCustom<ReadableStream<unknown>, ReadableStream<unknown>>>>;
17
+ }, z__default.core.$strip>, z__default.ZodObject<{
18
+ type: z__default.ZodLiteral<"contact">;
19
+ user: z__default.ZodOptional<z__default.ZodObject<{
20
+ __platform: z__default.ZodString;
21
+ id: z__default.ZodString;
22
+ }, z__default.core.$strip>>;
23
+ name: z__default.ZodOptional<z__default.ZodObject<{
24
+ formatted: z__default.ZodOptional<z__default.ZodString>;
25
+ first: z__default.ZodOptional<z__default.ZodString>;
26
+ last: z__default.ZodOptional<z__default.ZodString>;
27
+ middle: z__default.ZodOptional<z__default.ZodString>;
28
+ prefix: z__default.ZodOptional<z__default.ZodString>;
29
+ suffix: z__default.ZodOptional<z__default.ZodString>;
30
+ }, z__default.core.$strip>>;
31
+ phones: z__default.ZodOptional<z__default.ZodArray<z__default.ZodObject<{
32
+ value: z__default.ZodString;
33
+ type: z__default.ZodOptional<z__default.ZodEnum<{
34
+ mobile: "mobile";
35
+ home: "home";
36
+ work: "work";
37
+ other: "other";
38
+ }>>;
39
+ }, z__default.core.$strip>>>;
40
+ emails: z__default.ZodOptional<z__default.ZodArray<z__default.ZodObject<{
41
+ value: z__default.ZodString;
42
+ type: z__default.ZodOptional<z__default.ZodEnum<{
43
+ home: "home";
44
+ work: "work";
45
+ other: "other";
46
+ }>>;
47
+ }, z__default.core.$strip>>>;
48
+ addresses: z__default.ZodOptional<z__default.ZodArray<z__default.ZodObject<{
49
+ street: z__default.ZodOptional<z__default.ZodString>;
50
+ city: z__default.ZodOptional<z__default.ZodString>;
51
+ region: z__default.ZodOptional<z__default.ZodString>;
52
+ postalCode: z__default.ZodOptional<z__default.ZodString>;
53
+ country: z__default.ZodOptional<z__default.ZodString>;
54
+ type: z__default.ZodOptional<z__default.ZodEnum<{
55
+ home: "home";
56
+ work: "work";
57
+ other: "other";
58
+ }>>;
59
+ }, z__default.core.$strip>>>;
60
+ org: z__default.ZodOptional<z__default.ZodObject<{
61
+ name: z__default.ZodOptional<z__default.ZodString>;
62
+ title: z__default.ZodOptional<z__default.ZodString>;
63
+ department: z__default.ZodOptional<z__default.ZodString>;
64
+ }, z__default.core.$strip>>;
65
+ urls: z__default.ZodOptional<z__default.ZodArray<z__default.ZodString>>;
66
+ birthday: z__default.ZodOptional<z__default.ZodString>;
67
+ note: z__default.ZodOptional<z__default.ZodString>;
68
+ photo: z__default.ZodOptional<z__default.ZodObject<{
69
+ mimeType: z__default.ZodString;
70
+ read: z__default.ZodFunction<z__default.ZodTuple<readonly [], null>, z__default.ZodPromise<z__default.ZodCustom<Buffer<ArrayBufferLike>, Buffer<ArrayBufferLike>>>>;
71
+ }, z__default.core.$strip>>;
72
+ raw: z__default.ZodOptional<z__default.ZodUnknown>;
73
+ }, z__default.core.$strip>, z__default.ZodObject<{
74
+ type: z__default.ZodLiteral<"voice">;
75
+ name: z__default.ZodOptional<z__default.ZodString>;
76
+ mimeType: z__default.ZodString;
77
+ duration: z__default.ZodOptional<z__default.ZodNumber>;
78
+ size: z__default.ZodOptional<z__default.ZodNumber>;
79
+ read: z__default.ZodFunction<z__default.ZodTuple<readonly [], null>, z__default.ZodPromise<z__default.ZodCustom<Buffer<ArrayBufferLike>, Buffer<ArrayBufferLike>>>>;
80
+ stream: z__default.ZodFunction<z__default.ZodTuple<readonly [], null>, z__default.ZodPromise<z__default.ZodCustom<ReadableStream<unknown>, ReadableStream<unknown>>>>;
15
81
  }, z__default.core.$strip>], "type">;
16
82
  type Content = z__default.infer<typeof contentSchema>;
17
83
  interface ContentBuilder {
@@ -19,6 +85,11 @@ interface ContentBuilder {
19
85
  }
20
86
  type ContentInput = string | ContentBuilder;
21
87
 
88
+ interface User {
89
+ readonly __platform: string;
90
+ readonly id: string;
91
+ }
92
+
22
93
  interface Space<_Def = unknown> {
23
94
  readonly __platform: string;
24
95
  readonly id: string;
@@ -28,11 +99,6 @@ interface Space<_Def = unknown> {
28
99
  stopTyping(): Promise<void>;
29
100
  }
30
101
 
31
- interface User {
32
- readonly __platform: string;
33
- readonly id: string;
34
- }
35
-
36
102
  interface Message<TPlatform extends string = string, TSender extends User = User, TSpace extends Space = Space> {
37
103
  content: Content;
38
104
  readonly id: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spectrum-ts",
3
- "version": "0.3.0",
3
+ "version": "0.5.0",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.js",
@@ -19,17 +19,24 @@
19
19
  }
20
20
  },
21
21
  "dependencies": {
22
- "@photon-ai/advanced-imessage": "^0.4.2",
22
+ "@photon-ai/advanced-imessage": "^0.4.3",
23
+ "@photon-ai/imessage-kit": "^3.0.0-rc.2",
23
24
  "@photon-ai/whatsapp-business": "^0.1.1",
24
- "@photon-ai/imessage-kit": "^3.0.0-rc.1",
25
25
  "@repeaterjs/repeater": "^3.0.6",
26
26
  "better-grpc": "^0.3.2",
27
27
  "mime-types": "^3.0.1",
28
28
  "type-fest": "^5.4.1",
29
+ "vcf": "^2.1.2",
29
30
  "zod": "^4.2.1"
30
31
  },
31
32
  "peerDependencies": {
33
+ "ffmpeg-static": "^5",
32
34
  "typescript": "^5"
33
35
  },
36
+ "peerDependenciesMeta": {
37
+ "ffmpeg-static": {
38
+ "optional": true
39
+ }
40
+ },
34
41
  "license": "MIT"
35
42
  }
@@ -1,73 +0,0 @@
1
- // src/content/custom.ts
2
- import z from "zod";
3
- var customSchema = z.object({
4
- type: z.literal("custom"),
5
- raw: z.unknown()
6
- });
7
- var asCustom = (raw) => customSchema.parse({ type: "custom", raw });
8
- function custom(raw) {
9
- return {
10
- build: async () => asCustom(raw)
11
- };
12
- }
13
-
14
- // src/utils/stream.ts
15
- import { Repeater } from "@repeaterjs/repeater";
16
- function stream(setup) {
17
- const repeater = new Repeater(async (push, stop) => {
18
- const emit = (value) => {
19
- Promise.resolve(push(value)).catch((error) => {
20
- stop(error);
21
- return void 0;
22
- });
23
- };
24
- const end = (error) => {
25
- stop(error);
26
- };
27
- const cleanup = await setup(emit, end);
28
- try {
29
- await stop;
30
- } finally {
31
- await cleanup?.();
32
- }
33
- });
34
- return Object.assign(repeater, {
35
- close: async () => {
36
- await repeater.return(void 0);
37
- }
38
- });
39
- }
40
- function mergeStreams(streams) {
41
- return stream((emit, end) => {
42
- if (streams.length === 0) {
43
- end();
44
- return;
45
- }
46
- let openStreams = streams.length;
47
- const workers = streams.map(async (source) => {
48
- try {
49
- for await (const value of source) {
50
- emit(value);
51
- }
52
- } catch (error) {
53
- end(error);
54
- } finally {
55
- openStreams -= 1;
56
- if (openStreams === 0) {
57
- end();
58
- }
59
- }
60
- });
61
- return async () => {
62
- await Promise.allSettled(streams.map((source) => source.close()));
63
- await Promise.allSettled(workers);
64
- };
65
- });
66
- }
67
-
68
- export {
69
- asCustom,
70
- custom,
71
- stream,
72
- mergeStreams
73
- };
@@ -1,44 +0,0 @@
1
- // src/content/attachment.ts
2
- import { readFile } from "fs/promises";
3
- import { basename } from "path";
4
- import { lookup as lookupMimeType } from "mime-types";
5
- import z from "zod";
6
- var DEFAULT_ATTACHMENT_NAME = "attachment";
7
- var attachmentSchema = z.object({
8
- type: z.literal("attachment"),
9
- data: z.instanceof(Buffer),
10
- mimeType: z.string().nonempty(),
11
- name: z.string().nonempty()
12
- });
13
- var resolveAttachmentName = (input, name) => name || (typeof input === "string" ? basename(input) : DEFAULT_ATTACHMENT_NAME);
14
- var resolveAttachmentMimeType = (name, mimeType) => {
15
- if (mimeType) {
16
- return mimeType;
17
- }
18
- const resolvedMimeType = lookupMimeType(name);
19
- if (!resolvedMimeType) {
20
- throw new Error(
21
- `Unable to resolve MIME type for attachment "${name}". Pass options.mimeType explicitly.`
22
- );
23
- }
24
- return resolvedMimeType;
25
- };
26
- var asAttachment = (input) => attachmentSchema.parse({ type: "attachment", ...input });
27
- function attachment(input, options) {
28
- return {
29
- build: async () => {
30
- const data = typeof input === "string" ? await readFile(input) : input;
31
- const name = resolveAttachmentName(input, options?.name);
32
- return asAttachment({
33
- data,
34
- mimeType: resolveAttachmentMimeType(name, options?.mimeType),
35
- name
36
- });
37
- }
38
- };
39
- }
40
-
41
- export {
42
- asAttachment,
43
- attachment
44
- };