sounding 0.0.0 → 0.0.2
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/README.md +36 -4
- package/RESEARCH.md +743 -231
- package/index.js +74 -3
- package/lib/create-app-manager.js +329 -0
- package/lib/create-auth-helpers.js +279 -0
- package/lib/create-browser-manager.js +132 -0
- package/lib/create-expect.js +155 -0
- package/lib/create-helper-runner.js +55 -0
- package/lib/create-mail-capture.js +391 -0
- package/lib/create-mailbox.js +28 -0
- package/lib/create-request-client.js +552 -0
- package/lib/create-runtime.js +170 -0
- package/lib/create-test-api.js +228 -0
- package/lib/create-visit-client.js +114 -0
- package/lib/create-world-engine.js +300 -0
- package/lib/create-world-loader.js +128 -0
- package/lib/default-config.js +76 -0
- package/lib/define-world.js +37 -0
- package/lib/merge-config.js +25 -0
- package/lib/normalize-config.js +54 -0
- package/lib/resolve-auth-config.js +93 -0
- package/lib/resolve-datastore.js +97 -0
- package/package.json +17 -1
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
const path = require('node:path')
|
|
2
|
+
const url = require('node:url')
|
|
3
|
+
|
|
4
|
+
function normalizeList(value) {
|
|
5
|
+
if (value === undefined || value === null || value === '') {
|
|
6
|
+
return []
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
if (Array.isArray(value)) {
|
|
10
|
+
return value.flatMap((entry) => normalizeList(entry))
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
if (typeof value === 'string') {
|
|
14
|
+
return value
|
|
15
|
+
.split(',')
|
|
16
|
+
.map((entry) => entry.trim())
|
|
17
|
+
.filter(Boolean)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return [value]
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function extractLinks(html) {
|
|
24
|
+
if (!html) {
|
|
25
|
+
return []
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const matches = html.matchAll(/href=["']([^"']+)["']/gi)
|
|
29
|
+
return [...matches].map((match) => match[1])
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function resolvePrimaryLink(inputs = {}, links = []) {
|
|
33
|
+
const templateData = inputs.templateData || {}
|
|
34
|
+
const preferredKeys = [
|
|
35
|
+
'magicLinkUrl',
|
|
36
|
+
'verificationUrl',
|
|
37
|
+
'resetPasswordUrl',
|
|
38
|
+
'inviteUrl',
|
|
39
|
+
'checkoutUrl',
|
|
40
|
+
'actionUrl',
|
|
41
|
+
'url',
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
for (const key of preferredKeys) {
|
|
45
|
+
if (templateData[key]) {
|
|
46
|
+
return templateData[key]
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return links.find((link) => !/\/unsubscribe\b/i.test(link)) || links[0]
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function createRenderViewPromise(view) {
|
|
54
|
+
if (!view) {
|
|
55
|
+
return Promise.resolve(undefined)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (typeof view.intercept === 'function') {
|
|
59
|
+
return view.intercept((error) => error)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return Promise.resolve(view)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function renderTemplatePreview(sails, {
|
|
66
|
+
template,
|
|
67
|
+
templateData = {},
|
|
68
|
+
layout = 'layout-email',
|
|
69
|
+
}) {
|
|
70
|
+
if (!template || typeof sails?.renderView !== 'function') {
|
|
71
|
+
return undefined
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const emailTemplatePath = path.join('emails/', template)
|
|
75
|
+
let emailTemplateLayout = false
|
|
76
|
+
|
|
77
|
+
if (layout) {
|
|
78
|
+
emailTemplateLayout = path.relative(
|
|
79
|
+
path.dirname(emailTemplatePath),
|
|
80
|
+
path.resolve('layouts/', layout)
|
|
81
|
+
)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return createRenderViewPromise(
|
|
85
|
+
sails.renderView(emailTemplatePath, {
|
|
86
|
+
layout: emailTemplateLayout,
|
|
87
|
+
url,
|
|
88
|
+
...templateData,
|
|
89
|
+
})
|
|
90
|
+
)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function resolveMailConfig(sails) {
|
|
94
|
+
return sails?.config?.mail || {}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function resolveMailerName(sails, inputs = {}) {
|
|
98
|
+
return inputs.mailer || process.env.MAIL_MAILER || resolveMailConfig(sails).default
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function resolveTransportName(sails, mailer) {
|
|
102
|
+
return resolveMailConfig(sails).mailers?.[mailer]?.transport
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function resolveFromAddress(sails, inputs = {}) {
|
|
106
|
+
return inputs.from || resolveMailConfig(sails).from?.address || process.env.MAIL_FROM_ADDRESS
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function resolveFromName(sails, inputs = {}) {
|
|
110
|
+
return inputs.fromName || resolveMailConfig(sails).from?.name || process.env.MAIL_FROM_NAME
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function resolveReplyTo(sails, inputs = {}) {
|
|
114
|
+
return inputs.replyTo || resolveMailConfig(sails).replyTo || process.env.MAIL_REPLY_TO
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function toErrorShape(error) {
|
|
118
|
+
if (!error) {
|
|
119
|
+
return undefined
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
name: error.name,
|
|
124
|
+
message: error.message,
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async function buildCapturedMail(sails, inputs = {}) {
|
|
129
|
+
const mailer = resolveMailerName(sails, inputs)
|
|
130
|
+
const transport = resolveTransportName(sails, mailer)
|
|
131
|
+
const html = await renderTemplatePreview(sails, inputs)
|
|
132
|
+
const links = extractLinks(html)
|
|
133
|
+
|
|
134
|
+
return {
|
|
135
|
+
mailer,
|
|
136
|
+
transport,
|
|
137
|
+
template: inputs.template,
|
|
138
|
+
templateData: inputs.templateData || {},
|
|
139
|
+
to: normalizeList(inputs.to),
|
|
140
|
+
cc: normalizeList(inputs.cc),
|
|
141
|
+
bcc: normalizeList(inputs.bcc),
|
|
142
|
+
toName: inputs.toName,
|
|
143
|
+
subject: inputs.subject || '',
|
|
144
|
+
from: resolveFromAddress(sails, inputs),
|
|
145
|
+
fromName: resolveFromName(sails, inputs),
|
|
146
|
+
replyTo: resolveReplyTo(sails, inputs),
|
|
147
|
+
text: inputs.text,
|
|
148
|
+
html,
|
|
149
|
+
links,
|
|
150
|
+
ctaUrl: resolvePrimaryLink(inputs, links),
|
|
151
|
+
attachments: inputs.attachments || [],
|
|
152
|
+
headers: inputs.headers || {},
|
|
153
|
+
layout: inputs.layout === undefined ? 'layout-email' : inputs.layout,
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function createMailCapture({ sails, mailbox, getConfig }) {
|
|
158
|
+
let originalSend = null
|
|
159
|
+
|
|
160
|
+
function resolveMailSettings() {
|
|
161
|
+
const soundingConfig =
|
|
162
|
+
typeof getConfig === 'function' ? getConfig() : sails?.config?.sounding || {}
|
|
163
|
+
|
|
164
|
+
return soundingConfig.mail || {}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function isEnabled() {
|
|
168
|
+
return resolveMailSettings().capture !== false
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function shouldPassthrough() {
|
|
172
|
+
const settings = resolveMailSettings()
|
|
173
|
+
return settings.deliver === true || settings.mode === 'passthrough'
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function wrapDeferred(executor, onRejected) {
|
|
177
|
+
let promise = Promise.resolve().then(executor)
|
|
178
|
+
|
|
179
|
+
if (typeof onRejected === 'function') {
|
|
180
|
+
promise = promise.catch(async (error) => {
|
|
181
|
+
await onRejected(error)
|
|
182
|
+
throw error
|
|
183
|
+
})
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const deferred = {
|
|
187
|
+
intercept(handler) {
|
|
188
|
+
promise = promise.catch((error) => handler(error))
|
|
189
|
+
return deferred
|
|
190
|
+
},
|
|
191
|
+
|
|
192
|
+
then(onFulfilled, onRejected) {
|
|
193
|
+
return promise.then(onFulfilled, onRejected)
|
|
194
|
+
},
|
|
195
|
+
|
|
196
|
+
catch(onRejected) {
|
|
197
|
+
return promise.catch(onRejected)
|
|
198
|
+
},
|
|
199
|
+
|
|
200
|
+
finally(onFinally) {
|
|
201
|
+
return promise.finally(onFinally)
|
|
202
|
+
},
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return deferred
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function directSuccess(inputs = {}) {
|
|
209
|
+
return captureSuccessfulSend(inputs).then(() => ({}))
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function directPassthrough(sendHelper, inputs = {}) {
|
|
213
|
+
return Promise.resolve(resolveDirectInvoker(sendHelper)(inputs))
|
|
214
|
+
.then(async (result) => {
|
|
215
|
+
await captureSuccessfulSend(inputs)
|
|
216
|
+
return result
|
|
217
|
+
})
|
|
218
|
+
.catch(async (error) => {
|
|
219
|
+
await captureFailedSend(inputs, error)
|
|
220
|
+
throw error
|
|
221
|
+
})
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function deferredSuccess(inputs = {}) {
|
|
225
|
+
return wrapDeferred(async () => {
|
|
226
|
+
await captureSuccessfulSend(inputs)
|
|
227
|
+
return {}
|
|
228
|
+
})
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function deferredPassthrough(sendHelper, inputs = {}) {
|
|
232
|
+
return wrapDeferred(
|
|
233
|
+
async () => {
|
|
234
|
+
const result = await resolveWithInvoker(sendHelper)(inputs)
|
|
235
|
+
await captureSuccessfulSend(inputs)
|
|
236
|
+
return result
|
|
237
|
+
},
|
|
238
|
+
async (error) => {
|
|
239
|
+
await captureFailedSend(inputs, error)
|
|
240
|
+
}
|
|
241
|
+
)
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function resolveDirectInvoker(sendHelper) {
|
|
245
|
+
if (typeof sendHelper === 'function') {
|
|
246
|
+
return sendHelper.bind(sendHelper)
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (typeof sendHelper?.with === 'function') {
|
|
250
|
+
return sendHelper.with.bind(sendHelper)
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
throw new Error('Sounding could not invoke `sails.helpers.mail.send`.')
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function resolveWithInvoker(sendHelper) {
|
|
257
|
+
if (typeof sendHelper?.with === 'function') {
|
|
258
|
+
return sendHelper.with.bind(sendHelper)
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (typeof sendHelper === 'function') {
|
|
262
|
+
return sendHelper.bind(sendHelper)
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
throw new Error('Sounding could not invoke `sails.helpers.mail.send.with()`.')
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
async function captureSuccessfulSend(inputs = {}) {
|
|
269
|
+
try {
|
|
270
|
+
const message = await buildCapturedMail(sails, inputs)
|
|
271
|
+
mailbox.capture({
|
|
272
|
+
...message,
|
|
273
|
+
status: 'sent',
|
|
274
|
+
})
|
|
275
|
+
} catch (error) {
|
|
276
|
+
mailbox.capture({
|
|
277
|
+
mailer: resolveMailerName(sails, inputs),
|
|
278
|
+
transport: resolveTransportName(sails, resolveMailerName(sails, inputs)),
|
|
279
|
+
to: normalizeList(inputs.to),
|
|
280
|
+
subject: inputs.subject || '',
|
|
281
|
+
template: inputs.template,
|
|
282
|
+
status: 'sent',
|
|
283
|
+
captureError: toErrorShape(error),
|
|
284
|
+
})
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
async function captureFailedSend(inputs = {}, error) {
|
|
289
|
+
let message
|
|
290
|
+
|
|
291
|
+
try {
|
|
292
|
+
message = await buildCapturedMail(sails, inputs)
|
|
293
|
+
} catch (captureError) {
|
|
294
|
+
message = {
|
|
295
|
+
mailer: resolveMailerName(sails, inputs),
|
|
296
|
+
transport: resolveTransportName(sails, resolveMailerName(sails, inputs)),
|
|
297
|
+
to: normalizeList(inputs.to),
|
|
298
|
+
subject: inputs.subject || '',
|
|
299
|
+
template: inputs.template,
|
|
300
|
+
captureError: toErrorShape(captureError),
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
mailbox.capture({
|
|
305
|
+
...message,
|
|
306
|
+
status: 'failed',
|
|
307
|
+
error: toErrorShape(error),
|
|
308
|
+
})
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function wrapSendHelper(sendHelper) {
|
|
312
|
+
const wrapped = async (inputs = {}) => {
|
|
313
|
+
return shouldPassthrough()
|
|
314
|
+
? directPassthrough(sendHelper, inputs)
|
|
315
|
+
: directSuccess(inputs)
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const descriptors = Object.getOwnPropertyDescriptors(sendHelper)
|
|
319
|
+
delete descriptors.with
|
|
320
|
+
Object.defineProperties(wrapped, descriptors)
|
|
321
|
+
|
|
322
|
+
Object.defineProperty(wrapped, 'with', {
|
|
323
|
+
value(inputs = {}) {
|
|
324
|
+
return shouldPassthrough()
|
|
325
|
+
? deferredPassthrough(sendHelper, inputs)
|
|
326
|
+
: deferredSuccess(inputs)
|
|
327
|
+
},
|
|
328
|
+
writable: true,
|
|
329
|
+
configurable: true,
|
|
330
|
+
})
|
|
331
|
+
|
|
332
|
+
Object.defineProperty(wrapped, '__soundingWrapped', {
|
|
333
|
+
value: true,
|
|
334
|
+
configurable: true,
|
|
335
|
+
})
|
|
336
|
+
|
|
337
|
+
Object.defineProperty(wrapped, '__soundingOriginal', {
|
|
338
|
+
value: sendHelper,
|
|
339
|
+
configurable: true,
|
|
340
|
+
})
|
|
341
|
+
|
|
342
|
+
return wrapped
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function install() {
|
|
346
|
+
if (!isEnabled()) {
|
|
347
|
+
return false
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
const sendHelper = sails?.helpers?.mail?.send
|
|
351
|
+
if (!sendHelper) {
|
|
352
|
+
return false
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
if (sendHelper.__soundingWrapped) {
|
|
356
|
+
originalSend = sendHelper.__soundingOriginal || originalSend
|
|
357
|
+
return true
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
originalSend = sendHelper
|
|
361
|
+
sails.helpers.mail.send = wrapSendHelper(sendHelper)
|
|
362
|
+
return true
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function uninstall() {
|
|
366
|
+
if (!originalSend || !sails?.helpers?.mail) {
|
|
367
|
+
return false
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
sails.helpers.mail.send = originalSend
|
|
371
|
+
originalSend = null
|
|
372
|
+
return true
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
return {
|
|
376
|
+
install,
|
|
377
|
+
uninstall,
|
|
378
|
+
get installed() {
|
|
379
|
+
return Boolean(originalSend) && Boolean(sails?.helpers?.mail?.send?.__soundingWrapped)
|
|
380
|
+
},
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
module.exports = {
|
|
385
|
+
createMailCapture,
|
|
386
|
+
buildCapturedMail,
|
|
387
|
+
extractLinks,
|
|
388
|
+
normalizeList,
|
|
389
|
+
resolvePrimaryLink,
|
|
390
|
+
renderTemplatePreview,
|
|
391
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
function createMailbox() {
|
|
2
|
+
const messages = []
|
|
3
|
+
|
|
4
|
+
return {
|
|
5
|
+
capture(message) {
|
|
6
|
+
const normalized = {
|
|
7
|
+
capturedAt: new Date().toISOString(),
|
|
8
|
+
...message,
|
|
9
|
+
}
|
|
10
|
+
messages.push(normalized)
|
|
11
|
+
return normalized
|
|
12
|
+
},
|
|
13
|
+
|
|
14
|
+
all() {
|
|
15
|
+
return [...messages]
|
|
16
|
+
},
|
|
17
|
+
|
|
18
|
+
latest() {
|
|
19
|
+
return messages.at(-1)
|
|
20
|
+
},
|
|
21
|
+
|
|
22
|
+
clear() {
|
|
23
|
+
messages.length = 0
|
|
24
|
+
},
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
module.exports = { createMailbox }
|