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
package/index.js
CHANGED
|
@@ -1,4 +1,75 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
const { createRuntime } = require('./lib/create-runtime')
|
|
2
|
+
const { createAppManager } = require('./lib/create-app-manager')
|
|
3
|
+
const { createMailbox } = require('./lib/create-mailbox')
|
|
4
|
+
const { createMailCapture } = require('./lib/create-mail-capture')
|
|
5
|
+
const { createWorldEngine } = require('./lib/create-world-engine')
|
|
6
|
+
const { loadWorldFiles } = require('./lib/create-world-loader')
|
|
7
|
+
const { defineFactory, defineScenario } = require('./lib/define-world')
|
|
8
|
+
const { createHelperRunner } = require('./lib/create-helper-runner')
|
|
9
|
+
const { createRequestClient } = require('./lib/create-request-client')
|
|
10
|
+
const { createVisitClient } = require('./lib/create-visit-client')
|
|
11
|
+
const { createBrowserManager } = require('./lib/create-browser-manager')
|
|
12
|
+
const { createAuthHelpers } = require('./lib/create-auth-helpers')
|
|
13
|
+
const { createExpect } = require('./lib/create-expect')
|
|
14
|
+
const { createTestApi } = require('./lib/create-test-api')
|
|
15
|
+
const { getDefaultConfig } = require('./lib/default-config')
|
|
16
|
+
|
|
17
|
+
function isProductionEnvironment(sails) {
|
|
18
|
+
return (sails.config?.environment || process.env.NODE_ENV) === 'production'
|
|
4
19
|
}
|
|
20
|
+
|
|
21
|
+
function shouldEnableHook(sails) {
|
|
22
|
+
return sails.config?.sounding?.enableInProduction === true || !isProductionEnvironment(sails)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function soundingHook(sails) {
|
|
26
|
+
const runtime = createRuntime(sails)
|
|
27
|
+
|
|
28
|
+
return {
|
|
29
|
+
defaults: {
|
|
30
|
+
sounding: getDefaultConfig(),
|
|
31
|
+
},
|
|
32
|
+
|
|
33
|
+
configure() {
|
|
34
|
+
if (!shouldEnableHook(sails)) {
|
|
35
|
+
return
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
sails.hooks ||= {}
|
|
39
|
+
sails.sounding = runtime
|
|
40
|
+
sails.hooks.sounding = this
|
|
41
|
+
runtime.configure()
|
|
42
|
+
},
|
|
43
|
+
|
|
44
|
+
initialize(done) {
|
|
45
|
+
if (!shouldEnableHook(sails)) {
|
|
46
|
+
return done()
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
sails.sounding = runtime
|
|
50
|
+
Object.assign(this, runtime)
|
|
51
|
+
sails.hooks.sounding = this
|
|
52
|
+
return done()
|
|
53
|
+
},
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
module.exports = soundingHook
|
|
58
|
+
module.exports.test = createTestApi()
|
|
59
|
+
module.exports.expect = createExpect
|
|
60
|
+
module.exports.defineFactory = defineFactory
|
|
61
|
+
module.exports.defineScenario = defineScenario
|
|
62
|
+
module.exports.createRuntime = createRuntime
|
|
63
|
+
module.exports.createAppManager = createAppManager
|
|
64
|
+
module.exports.createMailbox = createMailbox
|
|
65
|
+
module.exports.createWorldEngine = createWorldEngine
|
|
66
|
+
module.exports.loadWorldFiles = loadWorldFiles
|
|
67
|
+
module.exports.createHelperRunner = createHelperRunner
|
|
68
|
+
module.exports.createRequestClient = createRequestClient
|
|
69
|
+
module.exports.createVisitClient = createVisitClient
|
|
70
|
+
module.exports.createBrowserManager = createBrowserManager
|
|
71
|
+
module.exports.createAuthHelpers = createAuthHelpers
|
|
72
|
+
module.exports.createExpect = createExpect
|
|
73
|
+
module.exports.createTestApi = createTestApi
|
|
74
|
+
module.exports.getDefaultConfig = getDefaultConfig
|
|
75
|
+
module.exports.createMailCapture = createMailCapture
|
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
const fs = require('node:fs')
|
|
2
|
+
const path = require('node:path')
|
|
3
|
+
|
|
4
|
+
const { getDefaultConfig } = require('./default-config')
|
|
5
|
+
const { mergeConfig } = require('./merge-config')
|
|
6
|
+
const { normalizeUserConfig } = require('./normalize-config')
|
|
7
|
+
const { buildManagedSqlitePath, resolveManagedRoot } = require('./resolve-datastore')
|
|
8
|
+
|
|
9
|
+
function resolveModuleFromApp(appPath, moduleId) {
|
|
10
|
+
return require(require.resolve(moduleId, { paths: [appPath, process.cwd(), __dirname] }))
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function loadAppSoundingConfig(appPath) {
|
|
14
|
+
const configPath = path.join(appPath, 'config', 'sounding.js')
|
|
15
|
+
|
|
16
|
+
if (!fs.existsSync(configPath)) {
|
|
17
|
+
return getDefaultConfig()
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
delete require.cache[require.resolve(configPath)]
|
|
21
|
+
const loaded = require(configPath)
|
|
22
|
+
return mergeConfig(getDefaultConfig(), normalizeUserConfig(loaded?.sounding || {}))
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function buildManagedDatastoreOverrides(config, appPath) {
|
|
26
|
+
if (config.datastore?.mode !== 'managed') {
|
|
27
|
+
return {}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const identity = config.datastore.identity || 'default'
|
|
31
|
+
|
|
32
|
+
return {
|
|
33
|
+
datastores: {
|
|
34
|
+
[identity]: {
|
|
35
|
+
adapter: config.datastore.adapter || 'sails-sqlite',
|
|
36
|
+
url: buildManagedSqlitePath({
|
|
37
|
+
root: resolveManagedRoot({
|
|
38
|
+
datastore: config.datastore,
|
|
39
|
+
appPath,
|
|
40
|
+
}),
|
|
41
|
+
identity,
|
|
42
|
+
isolation: config.datastore.isolation || 'worker',
|
|
43
|
+
}),
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
models: {
|
|
47
|
+
migrate: 'drop',
|
|
48
|
+
},
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function createOutputFilter(config = {}) {
|
|
53
|
+
if (config.app?.quiet === false) {
|
|
54
|
+
return {
|
|
55
|
+
install() {},
|
|
56
|
+
uninstall() {},
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const noisyPatterns = [
|
|
61
|
+
/start\s+build started/i,
|
|
62
|
+
/ready\s+built in/i,
|
|
63
|
+
/\[DEP0044\].*util\.isArray/i,
|
|
64
|
+
/node --trace-deprecation/i,
|
|
65
|
+
/^\s*info:\s/m,
|
|
66
|
+
/success:\s*true.*no new issues to notify/i,
|
|
67
|
+
]
|
|
68
|
+
|
|
69
|
+
let installed = false
|
|
70
|
+
let originalStdoutWrite = null
|
|
71
|
+
let originalStderrWrite = null
|
|
72
|
+
|
|
73
|
+
function shouldSuppress(chunk) {
|
|
74
|
+
const value = String(chunk || '')
|
|
75
|
+
const normalizedValue = value.replace(/\u001b\[[0-9;]*m/g, '')
|
|
76
|
+
return noisyPatterns.some((pattern) => pattern.test(normalizedValue))
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function wrapWrite(write) {
|
|
80
|
+
return function soundingWrite(chunk, encoding, callback) {
|
|
81
|
+
if (shouldSuppress(chunk)) {
|
|
82
|
+
if (typeof encoding === 'function') {
|
|
83
|
+
encoding()
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (typeof callback === 'function') {
|
|
87
|
+
callback()
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return true
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return write.call(this, chunk, encoding, callback)
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
install() {
|
|
99
|
+
if (installed) {
|
|
100
|
+
return
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
originalStdoutWrite = process.stdout.write
|
|
104
|
+
originalStderrWrite = process.stderr.write
|
|
105
|
+
process.stdout.write = wrapWrite(originalStdoutWrite)
|
|
106
|
+
process.stderr.write = wrapWrite(originalStderrWrite)
|
|
107
|
+
installed = true
|
|
108
|
+
},
|
|
109
|
+
|
|
110
|
+
uninstall() {
|
|
111
|
+
if (!installed) {
|
|
112
|
+
return
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
process.stdout.write = originalStdoutWrite
|
|
116
|
+
process.stderr.write = originalStderrWrite
|
|
117
|
+
originalStdoutWrite = null
|
|
118
|
+
originalStderrWrite = null
|
|
119
|
+
installed = false
|
|
120
|
+
},
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function resolveManagedDatastoreFile(config, appPath) {
|
|
125
|
+
if (config.datastore?.mode !== 'managed') {
|
|
126
|
+
return null
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const identity = config.datastore.identity || 'default'
|
|
130
|
+
|
|
131
|
+
return buildManagedSqlitePath({
|
|
132
|
+
root: resolveManagedRoot({
|
|
133
|
+
datastore: config.datastore,
|
|
134
|
+
appPath,
|
|
135
|
+
}),
|
|
136
|
+
identity,
|
|
137
|
+
isolation: config.datastore.isolation || 'worker',
|
|
138
|
+
})
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function createAppManager({
|
|
142
|
+
appPath = process.cwd(),
|
|
143
|
+
environment = 'test',
|
|
144
|
+
liftOptions = {},
|
|
145
|
+
SailsConstructor,
|
|
146
|
+
} = {}) {
|
|
147
|
+
let loadedApp = null
|
|
148
|
+
let liftedApp = null
|
|
149
|
+
let loadPromise = null
|
|
150
|
+
let liftPromise = null
|
|
151
|
+
const managedArtifacts = new Set()
|
|
152
|
+
let outputFilter = null
|
|
153
|
+
|
|
154
|
+
function resolveAppPath() {
|
|
155
|
+
return path.resolve(appPath)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function resolveConfig() {
|
|
159
|
+
return loadAppSoundingConfig(resolveAppPath())
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function resolveSailsConstructor() {
|
|
163
|
+
if (SailsConstructor) {
|
|
164
|
+
return SailsConstructor
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return resolveModuleFromApp(resolveAppPath(), 'sails').constructor
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function buildOptions(mode) {
|
|
171
|
+
const config = resolveConfig()
|
|
172
|
+
outputFilter ||= createOutputFilter(config)
|
|
173
|
+
const managedFile = resolveManagedDatastoreFile(config, resolveAppPath())
|
|
174
|
+
|
|
175
|
+
if (managedFile) {
|
|
176
|
+
managedArtifacts.add(managedFile)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const appConfig = config.app || {}
|
|
180
|
+
const baseOptions = {
|
|
181
|
+
appPath: resolveAppPath(),
|
|
182
|
+
environment: appConfig.environment || environment,
|
|
183
|
+
...buildManagedDatastoreOverrides(config, resolveAppPath()),
|
|
184
|
+
}
|
|
185
|
+
const modeOptions =
|
|
186
|
+
mode === 'load'
|
|
187
|
+
? mergeConfig(
|
|
188
|
+
{
|
|
189
|
+
hooks: {
|
|
190
|
+
shipwright: false,
|
|
191
|
+
content: false,
|
|
192
|
+
},
|
|
193
|
+
},
|
|
194
|
+
appConfig.loadOptions || {}
|
|
195
|
+
)
|
|
196
|
+
: mergeConfig(appConfig.liftOptions || {}, liftOptions)
|
|
197
|
+
const nextOptions = mergeConfig(baseOptions, modeOptions)
|
|
198
|
+
|
|
199
|
+
if (mode === 'load' && nextOptions.datastores?.content) {
|
|
200
|
+
delete nextOptions.datastores.content
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return nextOptions
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
async function cleanupManagedArtifacts() {
|
|
207
|
+
for (const filePath of managedArtifacts) {
|
|
208
|
+
const companionPaths = [
|
|
209
|
+
filePath,
|
|
210
|
+
`${filePath}-journal`,
|
|
211
|
+
`${filePath}-wal`,
|
|
212
|
+
`${filePath}-shm`,
|
|
213
|
+
]
|
|
214
|
+
|
|
215
|
+
for (const artifactPath of companionPaths) {
|
|
216
|
+
await fs.promises.rm(artifactPath, { force: true }).catch(() => {})
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
async function load() {
|
|
222
|
+
if (loadedApp) {
|
|
223
|
+
return loadedApp
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (loadPromise) {
|
|
227
|
+
return loadPromise
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const Sails = resolveSailsConstructor()
|
|
231
|
+
const sailsApp = new Sails()
|
|
232
|
+
const nextLoadOptions = buildOptions('load')
|
|
233
|
+
outputFilter?.install()
|
|
234
|
+
|
|
235
|
+
loadPromise = new Promise((resolve, reject) => {
|
|
236
|
+
sailsApp.load(nextLoadOptions, (error, loadedSails) => {
|
|
237
|
+
if (error) {
|
|
238
|
+
loadPromise = null
|
|
239
|
+
cleanupManagedArtifacts().catch(() => {})
|
|
240
|
+
outputFilter?.uninstall()
|
|
241
|
+
reject(error)
|
|
242
|
+
return
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
loadedApp = loadedSails
|
|
246
|
+
globalThis.sails = loadedSails
|
|
247
|
+
globalThis.sounding = loadedSails.sounding
|
|
248
|
+
resolve(loadedSails)
|
|
249
|
+
})
|
|
250
|
+
})
|
|
251
|
+
|
|
252
|
+
return loadPromise
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
async function lift() {
|
|
256
|
+
if (liftedApp) {
|
|
257
|
+
return liftedApp
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (liftPromise) {
|
|
261
|
+
return liftPromise
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const Sails = resolveSailsConstructor()
|
|
265
|
+
const sailsApp = new Sails()
|
|
266
|
+
const nextLiftOptions = buildOptions('lift')
|
|
267
|
+
outputFilter?.install()
|
|
268
|
+
|
|
269
|
+
liftPromise = new Promise((resolve, reject) => {
|
|
270
|
+
sailsApp.lift(nextLiftOptions, (error, liftedSails) => {
|
|
271
|
+
if (error) {
|
|
272
|
+
liftPromise = null
|
|
273
|
+
cleanupManagedArtifacts().catch(() => {})
|
|
274
|
+
outputFilter?.uninstall()
|
|
275
|
+
reject(error)
|
|
276
|
+
return
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
liftedApp = liftedSails
|
|
280
|
+
globalThis.sails = liftedSails
|
|
281
|
+
globalThis.sounding = liftedSails.sounding
|
|
282
|
+
resolve(liftedSails)
|
|
283
|
+
})
|
|
284
|
+
})
|
|
285
|
+
|
|
286
|
+
return liftPromise
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
async function runtime(options = {}) {
|
|
290
|
+
const app = options.http ? await lift() : await load()
|
|
291
|
+
return app.sounding || app.hooks?.sounding
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
async function lower() {
|
|
295
|
+
const apps = [loadedApp, liftedApp].filter(Boolean)
|
|
296
|
+
|
|
297
|
+
loadedApp = null
|
|
298
|
+
liftedApp = null
|
|
299
|
+
loadPromise = null
|
|
300
|
+
liftPromise = null
|
|
301
|
+
|
|
302
|
+
await Promise.all(
|
|
303
|
+
apps.map(
|
|
304
|
+
(app) =>
|
|
305
|
+
new Promise((resolve) => {
|
|
306
|
+
app.lower(() => resolve())
|
|
307
|
+
})
|
|
308
|
+
)
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
delete globalThis.sails
|
|
312
|
+
delete globalThis.sounding
|
|
313
|
+
outputFilter?.uninstall()
|
|
314
|
+
await cleanupManagedArtifacts()
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
return {
|
|
318
|
+
load,
|
|
319
|
+
lift,
|
|
320
|
+
runtime,
|
|
321
|
+
lower,
|
|
322
|
+
resolveConfig,
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
module.exports = {
|
|
327
|
+
createAppManager,
|
|
328
|
+
loadAppSoundingConfig,
|
|
329
|
+
}
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
const { resolveAuthConfig } = require('./resolve-auth-config')
|
|
2
|
+
|
|
3
|
+
function normalizeEmail(value) {
|
|
4
|
+
return String(value || '')
|
|
5
|
+
.trim()
|
|
6
|
+
.toLowerCase()
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function looksLikeEmail(value) {
|
|
10
|
+
return typeof value === 'string' && value.includes('@')
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function createAuthHelpers({ sails, world, mailbox, request }) {
|
|
14
|
+
function getAuthConfig() {
|
|
15
|
+
return resolveAuthConfig({ sails })
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function getAuthModel() {
|
|
19
|
+
return getAuthConfig().model
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function resolveWorldActor(alias) {
|
|
23
|
+
if (!alias || typeof alias !== 'string') {
|
|
24
|
+
return null
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const auth = getAuthConfig()
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
world.current?.[auth.worldCollection]?.[alias] ||
|
|
31
|
+
world.current?.users?.[alias] ||
|
|
32
|
+
world.current?.creators?.[alias] ||
|
|
33
|
+
null
|
|
34
|
+
)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function createUserFromEmail(email, fullName) {
|
|
38
|
+
const auth = getAuthConfig()
|
|
39
|
+
const User = getAuthModel()
|
|
40
|
+
|
|
41
|
+
if (auth.modelIdentity !== 'user') {
|
|
42
|
+
throw new Error(
|
|
43
|
+
`Sounding auth helpers could not auto-create a missing ${auth.modelIdentity} record.`
|
|
44
|
+
)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (!sails?.helpers?.user?.signupWithTeam) {
|
|
48
|
+
throw new Error(
|
|
49
|
+
'Sounding auth helpers could not find `sails.helpers.user.signupWithTeam`.'
|
|
50
|
+
)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const signupResult = await sails.helpers.user.signupWithTeam.with({
|
|
54
|
+
fullName: fullName || normalizeEmail(email).split('@')[0],
|
|
55
|
+
email: normalizeEmail(email),
|
|
56
|
+
tosAcceptedByIp: '127.0.0.1',
|
|
57
|
+
emailStatus: 'verified',
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
if (User?.updateOne) {
|
|
61
|
+
await User.updateOne({ id: signupResult.user.id }).set({
|
|
62
|
+
emailStatus: 'verified',
|
|
63
|
+
})
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (User?.findOne) {
|
|
67
|
+
return User.findOne({ id: signupResult.user.id })
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return signupResult.user
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async function resolveActor(actorOrEmail, options = {}) {
|
|
74
|
+
const auth = getAuthConfig()
|
|
75
|
+
const User = getAuthModel()
|
|
76
|
+
|
|
77
|
+
if (!actorOrEmail) {
|
|
78
|
+
throw new Error('Sounding auth helpers require an actor or email address.')
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
let candidate = actorOrEmail
|
|
82
|
+
|
|
83
|
+
if (typeof candidate === 'string' && !looksLikeEmail(candidate)) {
|
|
84
|
+
candidate = resolveWorldActor(candidate) || candidate
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (candidate?.id && User?.findOne) {
|
|
88
|
+
return User.findOne({ id: candidate.id }) || candidate
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const email = candidate?.email || (looksLikeEmail(candidate) ? candidate : null)
|
|
92
|
+
|
|
93
|
+
if (!email) {
|
|
94
|
+
throw new Error(
|
|
95
|
+
`Sounding auth helpers could not resolve an email address for actor \`${candidate}\`.`
|
|
96
|
+
)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const normalizedEmail = normalizeEmail(email)
|
|
100
|
+
let user = User?.findOne ? await User.findOne({ email: normalizedEmail }) : null
|
|
101
|
+
|
|
102
|
+
if (!user && options.createIfMissing !== false) {
|
|
103
|
+
user = await createUserFromEmail(normalizedEmail, candidate?.fullName || options.fullName)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (!user) {
|
|
107
|
+
throw new Error(
|
|
108
|
+
`Sounding auth helpers could not find a ${auth.modelIdentity} for ${normalizedEmail}.`
|
|
109
|
+
)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return user
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async function issueMagicLink(actorOrEmail, options = {}) {
|
|
116
|
+
const User = getAuthModel()
|
|
117
|
+
|
|
118
|
+
if (!User?.updateOne) {
|
|
119
|
+
throw new Error('Sounding auth helpers require an auth model with updateOne().')
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const user = await resolveActor(actorOrEmail, {
|
|
123
|
+
createIfMissing: true,
|
|
124
|
+
...options,
|
|
125
|
+
})
|
|
126
|
+
const token = await sails.helpers.magicLink.generateToken()
|
|
127
|
+
const hashedToken = await sails.helpers.magicLink.hashToken(token)
|
|
128
|
+
|
|
129
|
+
await User.updateOne({ id: user.id }).set({
|
|
130
|
+
emailStatus: 'verified',
|
|
131
|
+
magicLinkToken: hashedToken,
|
|
132
|
+
magicLinkTokenExpiresAt: Date.now() + 15 * 60 * 1000,
|
|
133
|
+
magicLinkTokenUsedAt: null,
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
const refreshedUser = User.findOne ? await User.findOne({ id: user.id }) : user
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
user: refreshedUser,
|
|
140
|
+
email: refreshedUser.email,
|
|
141
|
+
token,
|
|
142
|
+
url: `/magic-link/${token}`,
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async function requestMagicLink(actorOrEmail, options = {}) {
|
|
147
|
+
const user = await resolveActor(actorOrEmail, {
|
|
148
|
+
createIfMissing: true,
|
|
149
|
+
...options,
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
const response = await request.post(
|
|
153
|
+
'/magic-link',
|
|
154
|
+
{
|
|
155
|
+
email: user.email,
|
|
156
|
+
fullName: options.fullName || user.fullName,
|
|
157
|
+
redirectUrl: options.redirectUrl || '/login',
|
|
158
|
+
},
|
|
159
|
+
options.requestOptions || {}
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
return {
|
|
163
|
+
response,
|
|
164
|
+
email: user.email,
|
|
165
|
+
message: mailbox.latest(),
|
|
166
|
+
url: mailbox.latest()?.ctaUrl,
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
async function loginWithPassword(actorOrEmail, page, options = {}) {
|
|
171
|
+
if (!page || typeof page.goto !== 'function') {
|
|
172
|
+
throw new Error('Sounding password browser login requires a Playwright page.')
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const auth = getAuthConfig()
|
|
176
|
+
const actor = await resolveActor(actorOrEmail, {
|
|
177
|
+
createIfMissing: false,
|
|
178
|
+
...options,
|
|
179
|
+
})
|
|
180
|
+
const email = normalizeEmail(actor?.email || actorOrEmail)
|
|
181
|
+
|
|
182
|
+
if (!options.password) {
|
|
183
|
+
throw new Error('Sounding password login requires a `password` option.')
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const loginUrl = new URL(auth.password.pagePath, 'http://sounding.local')
|
|
187
|
+
|
|
188
|
+
for (const [key, value] of Object.entries(auth.password.pageQuery || {})) {
|
|
189
|
+
if (value != null) {
|
|
190
|
+
loginUrl.searchParams.set(key, String(value))
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (options.returnUrl) {
|
|
195
|
+
loginUrl.searchParams.set(auth.password.form.returnUrl, options.returnUrl)
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
await page.goto(`${loginUrl.pathname}${loginUrl.search}`)
|
|
199
|
+
await page.fill(auth.password.selectors.email, email)
|
|
200
|
+
await page.fill(auth.password.selectors.password, options.password)
|
|
201
|
+
|
|
202
|
+
if (options.rememberMe && typeof page.check === 'function') {
|
|
203
|
+
await page.check(auth.password.selectors.rememberMe)
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
await page.click(auth.password.selectors.submit)
|
|
207
|
+
|
|
208
|
+
return {
|
|
209
|
+
actor,
|
|
210
|
+
email,
|
|
211
|
+
path: `${loginUrl.pathname}${loginUrl.search}`,
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
async function requestWithPassword(actorOrEmail, options = {}) {
|
|
216
|
+
const auth = getAuthConfig()
|
|
217
|
+
const actor = await resolveActor(actorOrEmail, {
|
|
218
|
+
createIfMissing: false,
|
|
219
|
+
...options,
|
|
220
|
+
})
|
|
221
|
+
const email = normalizeEmail(actor?.email || actorOrEmail)
|
|
222
|
+
|
|
223
|
+
if (!options.password) {
|
|
224
|
+
throw new Error('Sounding password request auth requires a `password` option.')
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const payload = {
|
|
228
|
+
[auth.password.form.email]: email,
|
|
229
|
+
[auth.password.form.password]: options.password,
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (options.rememberMe !== undefined) {
|
|
233
|
+
payload[auth.password.form.rememberMe] = options.rememberMe
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (options.returnUrl !== undefined) {
|
|
237
|
+
payload[auth.password.form.returnUrl] = options.returnUrl
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const targetRequest = options.request || request
|
|
241
|
+
const response = await targetRequest.post(
|
|
242
|
+
auth.password.loginPath,
|
|
243
|
+
payload,
|
|
244
|
+
options.requestOptions || {}
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
return {
|
|
248
|
+
actor,
|
|
249
|
+
email,
|
|
250
|
+
request: targetRequest,
|
|
251
|
+
response,
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const login = {
|
|
256
|
+
async as(actorOrEmail, page, options = {}) {
|
|
257
|
+
const magicLink = await issueMagicLink(actorOrEmail, options)
|
|
258
|
+
await page.goto(magicLink.url)
|
|
259
|
+
return magicLink
|
|
260
|
+
},
|
|
261
|
+
withPassword: loginWithPassword,
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return {
|
|
265
|
+
conventions: getAuthConfig(),
|
|
266
|
+
resolveActor,
|
|
267
|
+
resolveUser: resolveActor,
|
|
268
|
+
issueMagicLink,
|
|
269
|
+
requestMagicLink,
|
|
270
|
+
request: {
|
|
271
|
+
withPassword: requestWithPassword,
|
|
272
|
+
},
|
|
273
|
+
login,
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
module.exports = {
|
|
278
|
+
createAuthHelpers,
|
|
279
|
+
}
|