@open-xchange/soap-client 0.1.6 → 0.2.1
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/CHANGELOG.md +41 -1
- package/README.md +23 -0
- package/client.js +72 -0
- package/package.json +18 -7
- package/services/common/context.js +156 -130
- package/services/common/user.js +207 -98
- package/services/common/util.js +23 -9
- package/services/reseller/oxaas.js +23 -11
- package/services/reseller/resellerContext.js +45 -29
- package/services/reseller/resellerUser.js +81 -63
- package/services/secondaryAccount.js +61 -46
- package/services/sharedAccount.js +120 -104
- package/soap.js +206 -45
- package/test/soap.test.js +0 -268
- package/vitest.config.js +0 -8
|
@@ -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
|
-
*
|
|
11
|
-
* @
|
|
12
|
-
* @
|
|
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
|
|
20
|
-
|
|
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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
*
|
|
31
|
-
*
|
|
32
|
-
* @
|
|
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
|
|
38
|
-
|
|
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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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
|
-
|
|
44
|
-
|
|
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
|
-
//
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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
|
-
*
|
|
118
|
-
*
|
|
119
|
-
*
|
|
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
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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
|
-
|
|
127
|
+
obs.observe({ entryTypes: ['measure'] })
|
|
128
|
+
}
|
|
132
129
|
|
|
133
130
|
async function logSoapError (e) {
|
|
134
131
|
console.error(describeError(e))
|
|
@@ -243,15 +240,169 @@ function handleFailedAttempt (context, label) {
|
|
|
243
240
|
}
|
|
244
241
|
|
|
245
242
|
/**
|
|
246
|
-
*
|
|
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`. This
|
|
248
|
+
* one is a `Credentials` element and nothing else, so it is `login` only —
|
|
249
|
+
* see {@link AdminCredentials} for the per-call shape, which is wider.
|
|
250
|
+
*/
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* @typedef {object} AdminCredentials
|
|
254
|
+
* @property {string} [login] The account name as the `auth` element calls it.
|
|
255
|
+
* @property {string} [name] The same account as the SOAP `admin_user` object
|
|
256
|
+
* calls it. Used when `login` is absent.
|
|
257
|
+
* @property {string} password
|
|
258
|
+
*/
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Read the account name out of an admin object.
|
|
262
|
+
*
|
|
263
|
+
* The SOAP `admin_user` object names the account `name`; only the `auth`
|
|
264
|
+
* element calls it `login`. Consumers hold the former and the transport needs
|
|
265
|
+
* the latter, and every one of them used to bridge that itself: the CLI
|
|
266
|
+
* (`bin/provision.js`), `resellerUserService` and appsuite-codeceptjs's
|
|
267
|
+
* `Context` constructor each added a `login` in their own way, and anyone who
|
|
268
|
+
* forgot authenticated with `login: undefined` and got a terminal
|
|
269
|
+
* "Authentication failed" from the middleware. Reading both shapes in one
|
|
270
|
+
* place removes the trap; `login` wins when both are present, so the bridges
|
|
271
|
+
* that remain outside this package keep working unchanged.
|
|
272
|
+
*
|
|
273
|
+
* @param {AdminCredentials} [admin]
|
|
274
|
+
* @returns {string|undefined}
|
|
275
|
+
*/
|
|
276
|
+
function adminLogin (admin) {
|
|
277
|
+
return admin?.login || admin?.name
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* The capability reads answer with a display string, not a data structure, so
|
|
282
|
+
* parsing belongs here rather than in every caller.
|
|
283
|
+
*
|
|
284
|
+
* `OXUserServicePortTypeImpl.getUserCapabilities` and its context twin sort the
|
|
285
|
+
* set into a `TreeSet` and join it with `", "`, comma and space, so a plain
|
|
286
|
+
* `split(',')` leaves a blank in front of every entry but the first and nothing
|
|
287
|
+
* ever compares equal to the desired state. Worse, an empty set is reported as
|
|
288
|
+
* a sentence:
|
|
289
|
+
*
|
|
290
|
+
* There are no capabilities set for user 3 in context 42
|
|
291
|
+
* There are no capabilities set for context 42
|
|
292
|
+
*
|
|
293
|
+
* Returned verbatim that becomes one capability named after the sentence, which
|
|
294
|
+
* a reconciler then tries to remove. A context that has just been created is
|
|
295
|
+
* exactly the case that hits it, so the empty answer is the common one.
|
|
296
|
+
*
|
|
297
|
+
* @param {string} [capabilities] The raw `return` of a capabilities call.
|
|
298
|
+
* @returns {string[]} The capability names, or `[]` when there are none.
|
|
299
|
+
*/
|
|
300
|
+
function parseCapabilities (capabilities) {
|
|
301
|
+
if (typeof capabilities !== 'string') return []
|
|
302
|
+
if (/^There are no capabilities set for /.test(capabilities)) return []
|
|
303
|
+
return capabilities.split(',').map(c => c.trim()).filter(Boolean)
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Create a per-endpoint SOAP client constructor. Multiple factories coexist in
|
|
308
|
+
* one process, each bound to its own endpoint and credentials — nothing is
|
|
309
|
+
* read from the environment and no process-global state is touched.
|
|
310
|
+
* @param {TransportOptions} options
|
|
311
|
+
* @returns {(type: string) => Promise<Object>} createClientAsync bound to the endpoint.
|
|
312
|
+
*/
|
|
313
|
+
function createClientFactory ({ url, auth }) {
|
|
314
|
+
if (!url) throw new TypeError('createClientFactory: url is required')
|
|
315
|
+
// Deliberately narrower than the per-call `auth` below: this is the master
|
|
316
|
+
// admin, which has no `admin_user` counterpart, so no SOAP object names it
|
|
317
|
+
// `name`. Accepting one would only let a wrong object through construction
|
|
318
|
+
// and turn it into a terminal "Authentication failed" on the first call.
|
|
319
|
+
if (!auth?.login || !auth?.password) throw new TypeError('createClientFactory: auth {login, password} is required')
|
|
320
|
+
const baseUrl = String(url).replace(/\/$/, '')
|
|
321
|
+
const defaultAuth = { auth: { login: auth.login, password: auth.password } }
|
|
322
|
+
return (type) => createTypedClientAsync(type, baseUrl, defaultAuth)
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Memoize one SOAP client per service type, but only once it resolves.
|
|
327
|
+
*
|
|
328
|
+
* A plain `promise ??= createClient(type)` caches a rejection for the lifetime
|
|
329
|
+
* of the process: if the WSDL fetch exhausts its retry schedule or hits a
|
|
330
|
+
* non-retryable fault, every later call re-throws that stale error without ever
|
|
331
|
+
* touching the network again. That is fatal for a long-running embedder, where
|
|
332
|
+
* one bad startup would poison the service until the process restarts. Clearing
|
|
333
|
+
* the slot on failure keeps the happy path a single shared client while letting
|
|
334
|
+
* the next call re-attempt construction.
|
|
335
|
+
*
|
|
336
|
+
* @param {(type: string) => Promise<Object>} createClient
|
|
337
|
+
* @param {string} type The name of the service type.
|
|
338
|
+
* @returns {() => Promise<Object>} Getter returning the memoized client.
|
|
339
|
+
*/
|
|
340
|
+
function memoizeClient (createClient, type) {
|
|
341
|
+
let clientPromise
|
|
342
|
+
return () => (clientPromise ??= createClient(type).catch(e => {
|
|
343
|
+
clientPromise = undefined
|
|
344
|
+
throw e
|
|
345
|
+
}))
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// The env-configured default transport behind the classic module exports.
|
|
349
|
+
// Built lazily on first use: loads .env files, installs the timeout-rejection
|
|
350
|
+
// filter (CLI/test-runner compatibility), and binds PROVISIONING_URL +
|
|
351
|
+
// E2E_ADMIN_USER/PW. Factory transports never take this path.
|
|
352
|
+
let defaultCreateClient
|
|
353
|
+
function getDefaultCreateClient () {
|
|
354
|
+
if (!defaultCreateClient) {
|
|
355
|
+
for (const envFile of ['.env', '.env.defaults']) {
|
|
356
|
+
try { process.loadEnvFile(envFile) } catch {}
|
|
357
|
+
}
|
|
358
|
+
installTimeoutRejectionFilter()
|
|
359
|
+
// Report the missing *environment variables*, not createClientFactory's
|
|
360
|
+
// parameter names: a caller of the classic module exports never touched
|
|
361
|
+
// that function and would otherwise get an error naming an API it has
|
|
362
|
+
// never heard of. Note this is stricter than before — calls that carry
|
|
363
|
+
// their own `auth` (every user/account/reseller method takes
|
|
364
|
+
// `context.admin`) used to work with the master credentials unset, and
|
|
365
|
+
// now fail here. See the CHANGELOG entry for 0.2.0.
|
|
366
|
+
const missing = ['PROVISIONING_URL', 'E2E_ADMIN_USER', 'E2E_ADMIN_PW'].filter(key => !process.env[key])
|
|
367
|
+
if (missing.length) {
|
|
368
|
+
throw new TypeError(
|
|
369
|
+
`@open-xchange/soap-client: missing ${missing.join(', ')}. The module exports bind to the ` +
|
|
370
|
+
'environment; set these in .env, or use createProvisioningClient({ url, auth }) from ' +
|
|
371
|
+
"'@open-xchange/soap-client/client' to pass an endpoint and credentials explicitly."
|
|
372
|
+
)
|
|
373
|
+
}
|
|
374
|
+
defaultCreateClient = createClientFactory({
|
|
375
|
+
url: process.env.PROVISIONING_URL,
|
|
376
|
+
auth: { login: process.env.E2E_ADMIN_USER, password: process.env.E2E_ADMIN_PW }
|
|
377
|
+
})
|
|
378
|
+
}
|
|
379
|
+
return defaultCreateClient
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* This function creates a SOAP client for the specified service type, bound to
|
|
384
|
+
* the env-configured default endpoint (PROVISIONING_URL, E2E_ADMIN_USER/PW).
|
|
247
385
|
* @param {string} type The name of the service type.
|
|
248
386
|
* @returns {Promise<Object>} The SOAP client.
|
|
249
387
|
*/
|
|
250
388
|
async function createClientAsync (type) {
|
|
389
|
+
return getDefaultCreateClient()(type)
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* Create a SOAP client for the specified service type against a specific
|
|
394
|
+
* endpoint with specific credentials.
|
|
395
|
+
* @param {string} type The name of the service type.
|
|
396
|
+
* @param {string} baseUrl Base URL without trailing slash.
|
|
397
|
+
* @param {{auth: {login: string, password: string}}} defaultAuth Auth option injected per call.
|
|
398
|
+
* @returns {Promise<Object>} The SOAP client.
|
|
399
|
+
*/
|
|
400
|
+
async function createTypedClientAsync (type, baseUrl, defaultAuth) {
|
|
401
|
+
ensureDebugObserver()
|
|
251
402
|
const startMark = `${type}-start`
|
|
252
403
|
const endMark = `${type}-end`
|
|
253
404
|
performance.mark(startMark)
|
|
254
|
-
const endpoint = `${
|
|
405
|
+
const endpoint = `${baseUrl}/webservices/${type}`
|
|
255
406
|
const url = `${endpoint}/?wsdl`
|
|
256
407
|
// The WSDL fetch itself can hit transient network errors (ECONNRESET,
|
|
257
408
|
// ETIMEDOUT, ...). Without retry here, a single TLS hiccup during
|
|
@@ -284,11 +435,21 @@ async function createClientAsync (type) {
|
|
|
284
435
|
const startMark = `${prop}-start`
|
|
285
436
|
const endMark = `${prop}-end`
|
|
286
437
|
performance.mark(startMark)
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
438
|
+
// Pull `auth` out before merging. Spreading `options` wholesale made a
|
|
439
|
+
// present-but-undefined `auth` (every user/account/reseller method
|
|
440
|
+
// sends `auth: context.admin`, and a context without a resolved admin
|
|
441
|
+
// makes that undefined) overwrite the endpoint credentials, so the
|
|
442
|
+
// call went out unauthenticated even though valid ones were
|
|
443
|
+
// configured. An absent or undefined `auth` now falls back to the
|
|
444
|
+
// endpoint default; a real one still wins.
|
|
445
|
+
const { auth, ...rest } = options ?? {}
|
|
446
|
+
const soapOptions = { ...defaultAuth, ...rest }
|
|
447
|
+
if (auth) {
|
|
448
|
+
// Send login and password only, never the whole admin object:
|
|
449
|
+
// the extra fields can fail on ambiguous namespacing. `adminLogin`
|
|
450
|
+
// reads either name the account goes by, so callers may pass an
|
|
451
|
+
// `admin_user` object straight through (see AdminCredentials).
|
|
452
|
+
soapOptions.auth = { login: adminLogin(auth), password: auth.password }
|
|
292
453
|
}
|
|
293
454
|
|
|
294
455
|
try {
|
|
@@ -313,4 +474,4 @@ async function createClientAsync (type) {
|
|
|
313
474
|
})
|
|
314
475
|
}
|
|
315
476
|
|
|
316
|
-
export { createClientAsync, logSoapError, describeError, shouldAbortRetry, wrapSoapFault, unwrapError, handleFailedAttempt }
|
|
477
|
+
export { createClientAsync, createClientFactory, adminLogin, parseCapabilities, memoizeClient, installTimeoutRejectionFilter, logSoapError, describeError, shouldAbortRetry, wrapSoapFault, unwrapError, handleFailedAttempt }
|