@open-xchange/soap-client 0.1.5 → 0.2.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,52 +1,67 @@
1
- import { createClientAsync } from '../soap.js'
2
-
3
- let OXSecondaryAccountService
4
- function getClient () {
5
- if (!OXSecondaryAccountService) OXSecondaryAccountService = createClientAsync('OXSecondaryAccountService')
6
- return OXSecondaryAccountService
7
- }
1
+ import { createClientAsync, memoizeClient } from '../soap.js'
8
2
 
9
3
  /**
10
- * Creates a secondary account using the OX Secondary Account Service
11
- * @async
12
- * @param {Object} accountData - The data for the secondary account to be created
13
- * @param {Object} context - The context object where the account will be created
14
- * @param {Array<Object>} users - Array of users to associate with the secondary account
15
- * @param {Array<Object>} groups - Array of groups to associate with the secondary account
16
- * @returns {Promise<Object>} The created secondary account object
17
- * @throws {Error} If the account creation fails
4
+ * Build a secondary-account service bound to a SOAP client constructor.
5
+ * @param {(type: string) => Promise<Object>} createClient
6
+ * @returns {Object} The secondary account service.
18
7
  */
19
- export async function create (accountData, context, users = [], groups = []) {
20
- return (await getClient()).createAsync({
21
- accountDataOnCreate: accountData,
22
- context: { id: context.id },
23
- users: users || [],
24
- auth: context.admin
25
- })
26
- }
8
+ export function createSecondaryAccountService (createClient) {
9
+ const getClient = memoizeClient(createClient, 'OXSecondaryAccountService')
27
10
 
28
- export async function list (context) {
29
- return (await getClient()).listAsync({
30
- context: { id: context.id },
31
- auth: context.admin
32
- })
33
- }
34
- export async function remove (primaryAddress, context, users, groups) {
35
- return (await getClient()).deleteAsync({
36
- primaryAddress,
37
- context: { id: context.id },
38
- users,
39
- auth: context.admin
40
- })
41
- }
11
+ /**
12
+ * Creates a secondary account using the OX Secondary Account Service
13
+ * @async
14
+ * @param {Object} accountData - The data for the secondary account to be created
15
+ * @param {Object} context - The context object where the account will be created
16
+ * @param {Array<Object>} users - Array of users to associate with the secondary account
17
+ * @param {Array<Object>} groups - Array of groups to associate with the secondary account
18
+ * @returns {Promise<Object>} The created secondary account object
19
+ * @throws {Error} If the account creation fails
20
+ */
21
+ async function create (accountData, context, users = [], groups = []) {
22
+ return (await getClient()).createAsync({
23
+ accountDataOnCreate: accountData,
24
+ context: { id: context.id },
25
+ users: users || [],
26
+ auth: context.admin
27
+ })
28
+ }
42
29
 
43
- export async function update (primaryAddress, accountData, context, users, groups) {
44
- return (await getClient()).updateAsync({
45
- primaryAddress,
46
- accountDataUpdate: accountData,
47
- context: { id: context.id },
48
- users,
49
- groups,
50
- auth: context.admin
51
- })
30
+ async function list (context) {
31
+ return (await getClient()).listAsync({
32
+ context: { id: context.id },
33
+ auth: context.admin
34
+ })
35
+ }
36
+
37
+ async function remove (primaryAddress, context, users, groups) {
38
+ return (await getClient()).deleteAsync({
39
+ primaryAddress,
40
+ context: { id: context.id },
41
+ users,
42
+ auth: context.admin
43
+ })
44
+ }
45
+
46
+ async function update (primaryAddress, accountData, context, users, groups) {
47
+ return (await getClient()).updateAsync({
48
+ primaryAddress,
49
+ accountDataUpdate: accountData,
50
+ context: { id: context.id },
51
+ users,
52
+ groups,
53
+ auth: context.admin
54
+ })
55
+ }
56
+
57
+ return { create, list, remove, update }
52
58
  }
59
+
60
+ // Classic module exports, bound to the env-configured default endpoint.
61
+ let defaultService
62
+ const service = () => (defaultService ??= createSecondaryAccountService(createClientAsync))
63
+
64
+ export const create = (...args) => service().create(...args)
65
+ export const list = (...args) => service().list(...args)
66
+ export const remove = (...args) => service().remove(...args)
67
+ export const update = (...args) => service().update(...args)
@@ -18,116 +18,132 @@
18
18
  * Any use of the work other than as authorized under this license or copyright law is prohibited.
19
19
  */
20
20
 
21
- import { createClientAsync } from '../soap.js'
22
-
23
- let OXSharedAccountService
24
- function getClient () {
25
- if (!OXSharedAccountService) OXSharedAccountService = createClientAsync('OXSharedAccountService')
26
- return OXSharedAccountService
27
- }
21
+ import { createClientAsync, memoizeClient } from '../soap.js'
28
22
 
29
23
  /**
30
- * Creates a shared account using the OX Shared Account Service
31
- *
32
- * @param {Object} context - The context object where the account will be created
33
- * @param {Object} sharedAccountData - The data for the shared account to be created
34
- * @returns {Promise<Object>} The created shared account object
35
- * @throws {Error} If the shared account creation fails
24
+ * Build a shared-account service bound to a SOAP client constructor.
25
+ * @param {(type: string) => Promise<Object>} createClient
26
+ * @returns {Object} The shared account service.
36
27
  */
37
- export async function create (context, sharedAccountData) {
38
- return (await getClient()).createAsync({
39
- ctx: { id: context.id },
40
- sharedAccountData, // display_name, given_name, imapServer, language, mailenabled, name, sur_name, primaryEmail, email1, smtpServer, timezone, password
41
- auth: context.admin
42
- })
43
- }
28
+ export function createSharedAccountService (createClient) {
29
+ const getClient = memoizeClient(createClient, 'OXSharedAccountService')
44
30
 
45
- /**
46
- * Converts an existing user to a shared account using the OX Shared Account Service
47
- *
48
- * @param {Object} context - The context of the user to be converted
49
- * @param {number} userId - ID of the user to be converted
50
- * @param {string} password - The password for the new shared account
51
- * @returns {Promise<void>}
52
- * @throws {Error} If the conversion fails
53
- */
54
- export async function convertUserToSharedAccount (context, userId, password) {
55
- return (await getClient()).convertUserToSharedAccountAsync({
56
- ctx: { id: context.id },
57
- user: { id: userId },
58
- password,
59
- auth: context.admin
60
- })
61
- }
31
+ /**
32
+ * Creates a shared account using the OX Shared Account Service
33
+ *
34
+ * @param {Object} context - The context object where the account will be created
35
+ * @param {Object} sharedAccountData - The data for the shared account to be created
36
+ * @returns {Promise<Object>} The created shared account object
37
+ * @throws {Error} If the shared account creation fails
38
+ */
39
+ async function create (context, sharedAccountData) {
40
+ return (await getClient()).createAsync({
41
+ ctx: { id: context.id },
42
+ sharedAccountData, // display_name, given_name, imapServer, language, mailenabled, name, sur_name, primaryEmail, email1, smtpServer, timezone, password
43
+ auth: context.admin
44
+ })
45
+ }
62
46
 
63
- /**
64
- * Add users and groups with specified permissions and capabilities to an existing shared account using the OX Shared Account Service.
65
- *
66
- * @param {Object} context - The context of the users and groups
67
- * @param {Object} sharedAccountContext - The context of the shared account
68
- * @param {number} sharedAccountId - ID of the shared account
69
- * @param {Object} permissions - The users, groups, capabilities, and module-specific permissions.
70
- * @returns {Promise<Object>} The shared account object
71
- * @throws {Error} If the creation of the shared account permissions fails
72
- */
73
- export async function createSharedAccountPermissions (context, sharedAccountContext, sharedAccountId, permissions) {
74
- return (await getClient()).createSharedAccountPermissionsAsync({
75
- ctx: { id: context.id },
76
- sharedAccountCtx: { id: sharedAccountContext.id },
77
- sharedAccount: { id: sharedAccountId },
78
- groups: permissions.groups ?? [],
79
- users: permissions.users?.map(user => ({ id: user.userdata.id })) ?? [],
80
- capabilities: {
81
- grantedCapability: permissions.capabilities?.grantedCapability ?? [],
82
- deniedCapability: permissions.capabilities?.deniedCapability ?? [],
83
- },
84
- mailConfig: permissions.mailConfig,
85
- calendarConfig: permissions.calendarConfig,
86
- auth: context.admin
87
- })
88
- }
47
+ /**
48
+ * Converts an existing user to a shared account using the OX Shared Account Service
49
+ *
50
+ * @param {Object} context - The context of the user to be converted
51
+ * @param {number} userId - ID of the user to be converted
52
+ * @param {string} password - The password for the new shared account
53
+ * @returns {Promise<void>}
54
+ * @throws {Error} If the conversion fails
55
+ */
56
+ async function convertUserToSharedAccount (context, userId, password) {
57
+ return (await getClient()).convertUserToSharedAccountAsync({
58
+ ctx: { id: context.id },
59
+ user: { id: userId },
60
+ password,
61
+ auth: context.admin
62
+ })
63
+ }
89
64
 
90
- /**
91
- * List all shared accounts in the specified context
92
- *
93
- * @param {Object} context - The context object whose shared account will be listed
94
- * @param {*} pattern - An optional search pattern for the contexts
95
- * @returns {Promise<Object[]>} The list of shared account objects
96
- */
97
- export async function list (context, pattern = '*') {
98
- return (await getClient()).listAsync({
99
- ctx: { id: context.id },
100
- search_pattern: pattern,
101
- auth: context.admin
102
- })
103
- }
65
+ /**
66
+ * Add users and groups with specified permissions and capabilities to an existing shared account using the OX Shared Account Service.
67
+ *
68
+ * @param {Object} context - The context of the users and groups
69
+ * @param {Object} sharedAccountContext - The context of the shared account
70
+ * @param {number} sharedAccountId - ID of the shared account
71
+ * @param {Object} permissions - The users, groups, capabilities, and module-specific permissions.
72
+ * @returns {Promise<Object>} The shared account object
73
+ * @throws {Error} If the creation of the shared account permissions fails
74
+ */
75
+ async function createSharedAccountPermissions (context, sharedAccountContext, sharedAccountId, permissions) {
76
+ return (await getClient()).createSharedAccountPermissionsAsync({
77
+ ctx: { id: context.id },
78
+ sharedAccountCtx: { id: sharedAccountContext.id },
79
+ sharedAccount: { id: sharedAccountId },
80
+ groups: permissions.groups ?? [],
81
+ users: permissions.users?.map(user => ({ id: user.userdata.id })) ?? [],
82
+ capabilities: {
83
+ grantedCapability: permissions.capabilities?.grantedCapability ?? [],
84
+ deniedCapability: permissions.capabilities?.deniedCapability ?? [],
85
+ },
86
+ mailConfig: permissions.mailConfig,
87
+ calendarConfig: permissions.calendarConfig,
88
+ auth: context.admin
89
+ })
90
+ }
104
91
 
105
- /**
106
- * Get all data of a specified shared account in the specified context
107
- *
108
- * @param {*} context - The context where the shared account exists
109
- * @param {number} sharedAccountId - ID of the requested shared account
110
- * @returns {Promise<Object>} The shared account object
111
- */
112
- export async function getData (context, sharedAccountId) {
113
- return (await getClient()).getDataAsync({
114
- ctx: { id: context.id },
115
- sharedAccount: { id: sharedAccountId },
116
- auth: context.admin
117
- })
118
- }
92
+ /**
93
+ * List all shared accounts in the specified context
94
+ *
95
+ * @param {Object} context - The context object whose shared account will be listed
96
+ * @param {*} pattern - An optional search pattern for the contexts
97
+ * @returns {Promise<Object[]>} The list of shared account objects
98
+ */
99
+ async function list (context, pattern = '*') {
100
+ return (await getClient()).listAsync({
101
+ ctx: { id: context.id },
102
+ search_pattern: pattern,
103
+ auth: context.admin
104
+ })
105
+ }
119
106
 
120
- /**
121
- * Remove a specified shared account in the specified context
122
- *
123
- * @param {*} context - The context object where the account will be removed
124
- * @param {number} sharedAccountId - ID of the shared account to be removed
125
- * @returns {Promise<void>}
126
- */
127
- export async function remove (context, sharedAccountId) {
128
- return (await getClient()).deleteAsync({
129
- ctx: { id: context.id },
130
- sharedAccount: { id: sharedAccountId },
131
- auth: context.admin
132
- })
107
+ /**
108
+ * Get all data of a specified shared account in the specified context
109
+ *
110
+ * @param {*} context - The context where the shared account exists
111
+ * @param {number} sharedAccountId - ID of the requested shared account
112
+ * @returns {Promise<Object>} The shared account object
113
+ */
114
+ async function getData (context, sharedAccountId) {
115
+ return (await getClient()).getDataAsync({
116
+ ctx: { id: context.id },
117
+ sharedAccount: { id: sharedAccountId },
118
+ auth: context.admin
119
+ })
120
+ }
121
+
122
+ /**
123
+ * Remove a specified shared account in the specified context
124
+ *
125
+ * @param {*} context - The context object where the account will be removed
126
+ * @param {number} sharedAccountId - ID of the shared account to be removed
127
+ * @returns {Promise<void>}
128
+ */
129
+ async function remove (context, sharedAccountId) {
130
+ return (await getClient()).deleteAsync({
131
+ ctx: { id: context.id },
132
+ sharedAccount: { id: sharedAccountId },
133
+ auth: context.admin
134
+ })
135
+ }
136
+
137
+ return { create, convertUserToSharedAccount, createSharedAccountPermissions, list, getData, remove }
133
138
  }
139
+
140
+ // Classic module exports, bound to the env-configured default endpoint.
141
+ let defaultService
142
+ const service = () => (defaultService ??= createSharedAccountService(createClientAsync))
143
+
144
+ export const create = (...args) => service().create(...args)
145
+ export const convertUserToSharedAccount = (...args) => service().convertUserToSharedAccount(...args)
146
+ export const createSharedAccountPermissions = (...args) => service().createSharedAccountPermissions(...args)
147
+ export const list = (...args) => service().list(...args)
148
+ export const getData = (...args) => service().getData(...args)
149
+ export const remove = (...args) => service().remove(...args)
package/soap.js CHANGED
@@ -40,12 +40,9 @@ const RETRY_OPTIONS = {
40
40
  randomize: true // jitter to avoid thundering herd across parallel shards
41
41
  }
42
42
 
43
- for (const envFile of ['.env', '.env.defaults']) {
44
- try { process.loadEnvFile(envFile) } catch {}
45
- }
46
-
47
- // This flag enables debug output including timing information.
48
- const debug = process.env.DEBUG_SOAP === 'true'
43
+ // This flag enables debug output including timing information. Read lazily so
44
+ // importing the module has no environment-dependent behavior.
45
+ const debug = () => process.env.DEBUG_SOAP === 'true'
49
46
 
50
47
  /**
51
48
  * Walk one layer of common error-wrapper shapes:
@@ -91,44 +88,44 @@ function describeError (err) {
91
88
  'unknown'
92
89
  }
93
90
 
94
- // Handle specifically timeout-related unhandled promise rejections as fallback
95
- process.on('unhandledRejection', (reason, promise) => {
96
- const r = unwrapError(reason)
97
- if (r?.code === 'ECONNABORTED' && (r?.message?.includes('timeout') || r?.message?.includes('exceeded'))) {
98
- return
99
- }
100
-
101
- // Log other unhandled rejections as they indicate real problems
102
- console.error('Unhandled promise rejection:', describeError(reason))
103
- })
104
-
105
- // This URL is used to create the SOAP client.
106
- const provisioningUrl = String(process.env.PROVISIONING_URL).replace(/\/$/, '')
107
-
108
- // This user is used to authenticate against the provisioning API.
109
- const provisioningAuth = {
110
- auth: {
111
- login: process.env.E2E_ADMIN_USER,
112
- password: process.env.E2E_ADMIN_PW
113
- }
91
+ // Swallow specifically timeout-related unhandled promise rejections as
92
+ // fallback; log everything else, as those indicate real problems. Installed
93
+ // on demand (idempotent) — importing the module must not touch process-global
94
+ // state. The env-configured default client installs it for backwards
95
+ // compatibility with the CLI and test-runner consumers; factory clients from
96
+ // createClientFactory never do, so embedding applications keep their own
97
+ // rejection policy.
98
+ let rejectionFilterInstalled = false
99
+ function installTimeoutRejectionFilter () {
100
+ if (rejectionFilterInstalled) return
101
+ rejectionFilterInstalled = true
102
+ process.on('unhandledRejection', (reason, promise) => {
103
+ const r = unwrapError(reason)
104
+ if (r?.code === 'ECONNABORTED' && (r?.message?.includes('timeout') || r?.message?.includes('exceeded'))) {
105
+ return
106
+ }
107
+ console.error('Unhandled promise rejection:', describeError(reason))
108
+ })
114
109
  }
115
110
 
116
111
  /**
117
- * This code creates a PerformanceObserver instance which asynchronously observes
118
- * performance measurement events. It logs the name of the event (which is the name
119
- * of the SOAP method in this context) and the time it took to execute in milliseconds.
120
- *
121
- * This is used for performance tracking of the SOAP methods.
112
+ * Lazily create the PerformanceObserver that logs SOAP method timings when
113
+ * DEBUG_SOAP is enabled. One observer per process, created on first client
114
+ * construction rather than at import time.
122
115
  * @see https://nodejs.org/api/perf_hooks.html
123
- * @params {PerformanceObserverEntryList} items The list of performance entries.
124
116
  * @returns {void}
125
117
  */
126
- const obs = new PerformanceObserver((items) => {
127
- items.getEntries().forEach((entry) => {
128
- if (debug) console.log(`${entry.name} took ${Math.round(entry.duration)}ms`)
118
+ let debugObserverInstalled = false
119
+ function ensureDebugObserver () {
120
+ if (debugObserverInstalled || !debug()) return
121
+ debugObserverInstalled = true
122
+ const obs = new PerformanceObserver((items) => {
123
+ items.getEntries().forEach((entry) => {
124
+ console.log(`${entry.name} took ${Math.round(entry.duration)}ms`)
125
+ })
129
126
  })
130
- })
131
- obs.observe({ entryTypes: ['measure'] })
127
+ obs.observe({ entryTypes: ['measure'] })
128
+ }
132
129
 
133
130
  async function logSoapError (e) {
134
131
  console.error(describeError(e))
@@ -218,15 +215,134 @@ function shouldAbortRetry (error) {
218
215
  }
219
216
 
220
217
  /**
221
- * This function creates a SOAP client for the specified service type.
218
+ * p-retry v8 `onFailedAttempt` handler, shared by both retry sites below.
219
+ *
220
+ * IMPORTANT: p-retry v8 invokes `onFailedAttempt` with a *context object*
221
+ * `{ error, attemptNumber, retriesLeft, retriesConsumed, retryDelay }` — NOT
222
+ * the error itself (that was the pre-v7 signature). So when we abort, we must
223
+ * pass `context.error` (the real Error) to `AbortError`, not `context`.
224
+ *
225
+ * Why it matters: `new AbortError(nonError)` sets `AbortError.message` to the
226
+ * value verbatim, and p-retry does not unwrap an AbortError thrown *by this
227
+ * callback* — it rejects with it as-is. Passing `context` would therefore
228
+ * reject with an Error whose `.message` is an object, which later blows up
229
+ * consumers doing `error.message.includes(...)` (appsuite-codeceptjs
230
+ * contexts.js) with "error.message.includes is not a function", masking the
231
+ * real SOAP fault. Passing `context.error` keeps `.message` a string and lets
232
+ * `wrapSoapFault` recover the fault downstream.
233
+ *
234
+ * @param {object} context p-retry attempt context.
235
+ * @param {string} label Human-readable label for the retry log line.
236
+ */
237
+ function handleFailedAttempt (context, label) {
238
+ if (shouldAbortRetry(context)) throw new AbortError(context.error)
239
+ console.log(`Retrying ${label} in ${context.retriesLeft} attempts (${describeError(context)})`)
240
+ }
241
+
242
+ /**
243
+ * @typedef {object} TransportOptions
244
+ * @property {string} url Base URL of the provisioning API (e.g. the core-mw
245
+ * admin Service), without the `/webservices` suffix.
246
+ * @property {{login: string, password: string}} auth Master admin credentials
247
+ * injected into every SOAP call unless the call passes its own `auth`.
248
+ */
249
+
250
+ /**
251
+ * Create a per-endpoint SOAP client constructor. Multiple factories coexist in
252
+ * one process, each bound to its own endpoint and credentials — nothing is
253
+ * read from the environment and no process-global state is touched.
254
+ * @param {TransportOptions} options
255
+ * @returns {(type: string) => Promise<Object>} createClientAsync bound to the endpoint.
256
+ */
257
+ function createClientFactory ({ url, auth }) {
258
+ if (!url) throw new TypeError('createClientFactory: url is required')
259
+ if (!auth?.login || !auth?.password) throw new TypeError('createClientFactory: auth {login, password} is required')
260
+ const baseUrl = String(url).replace(/\/$/, '')
261
+ const defaultAuth = { auth: { login: auth.login, password: auth.password } }
262
+ return (type) => createTypedClientAsync(type, baseUrl, defaultAuth)
263
+ }
264
+
265
+ /**
266
+ * Memoize one SOAP client per service type, but only once it resolves.
267
+ *
268
+ * A plain `promise ??= createClient(type)` caches a rejection for the lifetime
269
+ * of the process: if the WSDL fetch exhausts its retry schedule or hits a
270
+ * non-retryable fault, every later call re-throws that stale error without ever
271
+ * touching the network again. That is fatal for a long-running embedder, where
272
+ * one bad startup would poison the service until the process restarts. Clearing
273
+ * the slot on failure keeps the happy path a single shared client while letting
274
+ * the next call re-attempt construction.
275
+ *
276
+ * @param {(type: string) => Promise<Object>} createClient
277
+ * @param {string} type The name of the service type.
278
+ * @returns {() => Promise<Object>} Getter returning the memoized client.
279
+ */
280
+ function memoizeClient (createClient, type) {
281
+ let clientPromise
282
+ return () => (clientPromise ??= createClient(type).catch(e => {
283
+ clientPromise = undefined
284
+ throw e
285
+ }))
286
+ }
287
+
288
+ // The env-configured default transport behind the classic module exports.
289
+ // Built lazily on first use: loads .env files, installs the timeout-rejection
290
+ // filter (CLI/test-runner compatibility), and binds PROVISIONING_URL +
291
+ // E2E_ADMIN_USER/PW. Factory transports never take this path.
292
+ let defaultCreateClient
293
+ function getDefaultCreateClient () {
294
+ if (!defaultCreateClient) {
295
+ for (const envFile of ['.env', '.env.defaults']) {
296
+ try { process.loadEnvFile(envFile) } catch {}
297
+ }
298
+ installTimeoutRejectionFilter()
299
+ // Report the missing *environment variables*, not createClientFactory's
300
+ // parameter names: a caller of the classic module exports never touched
301
+ // that function and would otherwise get an error naming an API it has
302
+ // never heard of. Note this is stricter than before — calls that carry
303
+ // their own `auth` (every user/account/reseller method takes
304
+ // `context.admin`) used to work with the master credentials unset, and
305
+ // now fail here. See the CHANGELOG entry for 0.2.0.
306
+ const missing = ['PROVISIONING_URL', 'E2E_ADMIN_USER', 'E2E_ADMIN_PW'].filter(key => !process.env[key])
307
+ if (missing.length) {
308
+ throw new TypeError(
309
+ `@open-xchange/soap-client: missing ${missing.join(', ')}. The module exports bind to the ` +
310
+ 'environment; set these in .env, or use createProvisioningClient({ url, auth }) from ' +
311
+ "'@open-xchange/soap-client/client' to pass an endpoint and credentials explicitly."
312
+ )
313
+ }
314
+ defaultCreateClient = createClientFactory({
315
+ url: process.env.PROVISIONING_URL,
316
+ auth: { login: process.env.E2E_ADMIN_USER, password: process.env.E2E_ADMIN_PW }
317
+ })
318
+ }
319
+ return defaultCreateClient
320
+ }
321
+
322
+ /**
323
+ * This function creates a SOAP client for the specified service type, bound to
324
+ * the env-configured default endpoint (PROVISIONING_URL, E2E_ADMIN_USER/PW).
222
325
  * @param {string} type The name of the service type.
223
326
  * @returns {Promise<Object>} The SOAP client.
224
327
  */
225
328
  async function createClientAsync (type) {
329
+ return getDefaultCreateClient()(type)
330
+ }
331
+
332
+ /**
333
+ * Create a SOAP client for the specified service type against a specific
334
+ * endpoint with specific credentials.
335
+ * @param {string} type The name of the service type.
336
+ * @param {string} baseUrl Base URL without trailing slash.
337
+ * @param {{auth: {login: string, password: string}}} defaultAuth Auth option injected per call.
338
+ * @returns {Promise<Object>} The SOAP client.
339
+ */
340
+ async function createTypedClientAsync (type, baseUrl, defaultAuth) {
341
+ ensureDebugObserver()
226
342
  const startMark = `${type}-start`
227
343
  const endMark = `${type}-end`
228
344
  performance.mark(startMark)
229
- const endpoint = `${provisioningUrl}/webservices/${type}`
345
+ const endpoint = `${baseUrl}/webservices/${type}`
230
346
  const url = `${endpoint}/?wsdl`
231
347
  // The WSDL fetch itself can hit transient network errors (ECONNRESET,
232
348
  // ETIMEDOUT, ...). Without retry here, a single TLS hiccup during
@@ -241,10 +357,7 @@ async function createClientAsync (type) {
241
357
  gzip: true
242
358
  }), {
243
359
  ...RETRY_OPTIONS,
244
- onFailedAttempt: async error => {
245
- if (shouldAbortRetry(error)) throw new AbortError(error)
246
- console.log(`Retrying WSDL fetch (${type}) in ${error.retriesLeft} attempts (${describeError(error)})`)
247
- }
360
+ onFailedAttempt: context => handleFailedAttempt(context, `WSDL fetch (${type})`)
248
361
  })
249
362
 
250
363
  // https://stackoverflow.com/questions/30740415/namespace-for-array-field-in-node-soap-client-node-js
@@ -262,20 +375,25 @@ async function createClientAsync (type) {
262
375
  const startMark = `${prop}-start`
263
376
  const endMark = `${prop}-end`
264
377
  performance.mark(startMark)
265
- const soapOptions = { ...provisioningAuth, ...options }
266
- if (soapOptions.auth) {
378
+ // Pull `auth` out before merging. Spreading `options` wholesale made a
379
+ // present-but-undefined `auth` (every user/account/reseller method
380
+ // sends `auth: context.admin`, and a context without a resolved admin
381
+ // makes that undefined) overwrite the endpoint credentials, so the
382
+ // call went out unauthenticated even though valid ones were
383
+ // configured. An absent or undefined `auth` now falls back to the
384
+ // endpoint default; a real one still wins.
385
+ const { auth, ...rest } = options ?? {}
386
+ const soapOptions = { ...defaultAuth, ...rest }
387
+ if (auth) {
267
388
  // only send login and password instead of complete admin object.
268
389
  // this can fail because of ambiguous namespacing
269
- soapOptions.auth = { login: soapOptions.auth.login, password: soapOptions.auth.password }
390
+ soapOptions.auth = { login: auth.login, password: auth.password }
270
391
  }
271
392
 
272
393
  try {
273
394
  const result = await pRetry(() => origMethod.apply(this, [soapOptions, { timeout: 30000, ...clientOptions }, ...args]), {
274
395
  ...RETRY_OPTIONS,
275
- onFailedAttempt: async error => {
276
- if (shouldAbortRetry(error)) throw new AbortError(error)
277
- console.log(`Retrying ${String(prop)} in ${error.retriesLeft} attempts (${describeError(error)})`)
278
- }
396
+ onFailedAttempt: context => handleFailedAttempt(context, String(prop))
279
397
  })
280
398
 
281
399
  performance.mark(endMark)
@@ -294,4 +412,4 @@ async function createClientAsync (type) {
294
412
  })
295
413
  }
296
414
 
297
- export { createClientAsync, logSoapError, describeError, shouldAbortRetry, wrapSoapFault, unwrapError }
415
+ export { createClientAsync, createClientFactory, memoizeClient, installTimeoutRejectionFilter, logSoapError, describeError, shouldAbortRetry, wrapSoapFault, unwrapError, handleFailedAttempt }