arc-1 1.0.2 → 1.1.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.
- package/bin/arc1-cli.js +3 -7
- package/bin/arc1.js +4 -9
- package/dist/adt/abapgit.d.ts +10 -5
- package/dist/adt/abapgit.d.ts.map +1 -1
- package/dist/adt/abapgit.js +357 -76
- package/dist/adt/abapgit.js.map +1 -1
- package/dist/adt/atc.d.ts +49 -0
- package/dist/adt/atc.d.ts.map +1 -0
- package/dist/adt/atc.js +294 -0
- package/dist/adt/atc.js.map +1 -0
- package/dist/adt/aunit-package.d.ts +22 -0
- package/dist/adt/aunit-package.d.ts.map +1 -0
- package/dist/adt/aunit-package.js +78 -0
- package/dist/adt/aunit-package.js.map +1 -0
- package/dist/adt/aunit.d.ts +143 -0
- package/dist/adt/aunit.d.ts.map +1 -0
- package/dist/adt/aunit.js +987 -0
- package/dist/adt/aunit.js.map +1 -0
- package/dist/adt/client.d.ts +9 -7
- package/dist/adt/client.d.ts.map +1 -1
- package/dist/adt/client.js +55 -64
- package/dist/adt/client.js.map +1 -1
- package/dist/adt/config.d.ts +2 -0
- package/dist/adt/config.d.ts.map +1 -1
- package/dist/adt/config.js +1 -0
- package/dist/adt/config.js.map +1 -1
- package/dist/adt/devtools.d.ts +8 -30
- package/dist/adt/devtools.d.ts.map +1 -1
- package/dist/adt/devtools.js +41 -218
- package/dist/adt/devtools.js.map +1 -1
- package/dist/adt/diagnostics.d.ts +1 -1
- package/dist/adt/diagnostics.d.ts.map +1 -1
- package/dist/adt/diagnostics.js +12 -12
- package/dist/adt/diagnostics.js.map +1 -1
- package/dist/adt/errors.d.ts +7 -0
- package/dist/adt/errors.d.ts.map +1 -1
- package/dist/adt/errors.js +7 -0
- package/dist/adt/errors.js.map +1 -1
- package/dist/adt/features.d.ts +9 -3
- package/dist/adt/features.d.ts.map +1 -1
- package/dist/adt/features.js +40 -8
- package/dist/adt/features.js.map +1 -1
- package/dist/adt/gcts.d.ts +12 -45
- package/dist/adt/gcts.d.ts.map +1 -1
- package/dist/adt/gcts.js +299 -171
- package/dist/adt/gcts.js.map +1 -1
- package/dist/adt/http-deadline.d.ts +23 -0
- package/dist/adt/http-deadline.d.ts.map +1 -0
- package/dist/adt/http-deadline.js +106 -0
- package/dist/adt/http-deadline.js.map +1 -0
- package/dist/adt/http-wire-body.d.ts +2 -0
- package/dist/adt/http-wire-body.d.ts.map +1 -0
- package/dist/adt/http-wire-body.js +14 -0
- package/dist/adt/http-wire-body.js.map +1 -0
- package/dist/adt/http.d.ts +10 -24
- package/dist/adt/http.d.ts.map +1 -1
- package/dist/adt/http.js +89 -53
- package/dist/adt/http.js.map +1 -1
- package/dist/adt/path-safety.d.ts +16 -0
- package/dist/adt/path-safety.d.ts.map +1 -0
- package/dist/adt/path-safety.js +101 -0
- package/dist/adt/path-safety.js.map +1 -0
- package/dist/adt/semaphore.d.ts +2 -2
- package/dist/adt/semaphore.d.ts.map +1 -1
- package/dist/adt/semaphore.js +26 -9
- package/dist/adt/semaphore.js.map +1 -1
- package/dist/adt/source-diff.d.ts +10 -1
- package/dist/adt/source-diff.d.ts.map +1 -1
- package/dist/adt/source-diff.js +33 -7
- package/dist/adt/source-diff.js.map +1 -1
- package/dist/adt/text-search.d.ts +25 -0
- package/dist/adt/text-search.d.ts.map +1 -0
- package/dist/adt/text-search.js +152 -0
- package/dist/adt/text-search.js.map +1 -0
- package/dist/adt/transport-diff.d.ts +128 -0
- package/dist/adt/transport-diff.d.ts.map +1 -0
- package/dist/adt/transport-diff.js +524 -0
- package/dist/adt/transport-diff.js.map +1 -0
- package/dist/adt/transport.d.ts +78 -13
- package/dist/adt/transport.d.ts.map +1 -1
- package/dist/adt/transport.js +443 -41
- package/dist/adt/transport.js.map +1 -1
- package/dist/adt/types.d.ts +23 -12
- package/dist/adt/types.d.ts.map +1 -1
- package/dist/adt/xml-parser.d.ts +1 -8
- package/dist/adt/xml-parser.d.ts.map +1 -1
- package/dist/adt/xml-parser.js +54 -60
- package/dist/adt/xml-parser.js.map +1 -1
- package/dist/authz/policy.d.ts.map +1 -1
- package/dist/authz/policy.js +5 -2
- package/dist/authz/policy.js.map +1 -1
- package/dist/cli-args.d.ts +2 -2
- package/dist/cli-args.js +2 -2
- package/dist/cli-checks.d.ts +56 -0
- package/dist/cli-checks.d.ts.map +1 -0
- package/dist/cli-checks.js +368 -0
- package/dist/cli-checks.js.map +1 -0
- package/dist/cli.d.ts +68 -15
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +677 -297
- package/dist/cli.js.map +1 -1
- package/dist/handlers/cds-hints.d.ts.map +1 -1
- package/dist/handlers/cds-hints.js +1 -10
- package/dist/handlers/cds-hints.js.map +1 -1
- package/dist/handlers/diagnose.d.ts +4 -0
- package/dist/handlers/diagnose.d.ts.map +1 -1
- package/dist/handlers/diagnose.js +616 -9
- package/dist/handlers/diagnose.js.map +1 -1
- package/dist/handlers/dispatch.d.ts.map +1 -1
- package/dist/handlers/dispatch.js +40 -1
- package/dist/handlers/dispatch.js.map +1 -1
- package/dist/handlers/git.d.ts.map +1 -1
- package/dist/handlers/git.js +165 -84
- package/dist/handlers/git.js.map +1 -1
- package/dist/handlers/read.d.ts.map +1 -1
- package/dist/handlers/read.js +15 -0
- package/dist/handlers/read.js.map +1 -1
- package/dist/handlers/schemas.d.ts +15 -2
- package/dist/handlers/schemas.d.ts.map +1 -1
- package/dist/handlers/schemas.js +75 -6
- package/dist/handlers/schemas.js.map +1 -1
- package/dist/handlers/search.js +1 -1
- package/dist/handlers/search.js.map +1 -1
- package/dist/handlers/tools.d.ts.map +1 -1
- package/dist/handlers/tools.js +51 -42
- package/dist/handlers/tools.js.map +1 -1
- package/dist/handlers/transport.d.ts +2 -1
- package/dist/handlers/transport.d.ts.map +1 -1
- package/dist/handlers/transport.js +128 -27
- package/dist/handlers/transport.js.map +1 -1
- package/dist/handlers/write/create.d.ts.map +1 -1
- package/dist/handlers/write/create.js +11 -0
- package/dist/handlers/write/create.js.map +1 -1
- package/dist/handlers/write/update-delete.d.ts.map +1 -1
- package/dist/handlers/write/update-delete.js +33 -10
- package/dist/handlers/write/update-delete.js.map +1 -1
- package/dist/handlers/write-helpers.d.ts.map +1 -1
- package/dist/handlers/write-helpers.js +3 -2
- package/dist/handlers/write-helpers.js.map +1 -1
- package/dist/server/audit.d.ts +1 -1
- package/dist/server/audit.d.ts.map +1 -1
- package/dist/server/audit.js +228 -21
- package/dist/server/audit.js.map +1 -1
- package/dist/server/config.d.ts +16 -0
- package/dist/server/config.d.ts.map +1 -1
- package/dist/server/config.js +145 -17
- package/dist/server/config.js.map +1 -1
- package/dist/server/effective-policy-log.d.ts +2 -1
- package/dist/server/effective-policy-log.d.ts.map +1 -1
- package/dist/server/effective-policy-log.js +8 -2
- package/dist/server/effective-policy-log.js.map +1 -1
- package/dist/server/multi-target-runtime.d.ts.map +1 -1
- package/dist/server/multi-target-runtime.js +3 -0
- package/dist/server/multi-target-runtime.js.map +1 -1
- package/dist/server/multi-target-tools.d.ts.map +1 -1
- package/dist/server/multi-target-tools.js +5 -0
- package/dist/server/multi-target-tools.js.map +1 -1
- package/dist/server/server.d.ts +15 -13
- package/dist/server/server.d.ts.map +1 -1
- package/dist/server/server.js +36 -26
- package/dist/server/server.js.map +1 -1
- package/dist/server/types.d.ts +2 -0
- package/dist/server/types.d.ts.map +1 -1
- package/dist/server/types.js +1 -0
- package/dist/server/types.js.map +1 -1
- package/package.json +13 -13
package/dist/adt/abapgit.js
CHANGED
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Bridge endpoints live under /sap/bc/adt/abapgit/* and use XML payloads.
|
|
5
5
|
*/
|
|
6
|
+
import { isIP } from 'node:net';
|
|
6
7
|
import { AdtApiError, AdtSafetyError, classifyAbapgitError } from './errors.js';
|
|
8
|
+
import { assertCanonicalHostRelativeAdtPath } from './path-safety.js';
|
|
7
9
|
import { checkGit, checkOperation, checkPackage, OperationType } from './safety.js';
|
|
8
10
|
import { escapeXmlAttr, findDeepNodes, parseXml } from './xml-parser.js';
|
|
9
11
|
const ABAPGIT_BASE = '/sap/bc/adt/abapgit';
|
|
@@ -21,6 +23,163 @@ const REPO_OBJECT_ACCEPT = `${REPO_OBJECT_V2}, ${REPO_OBJECT_V1}`;
|
|
|
21
23
|
const NS_REPO = 'http://www.sap.com/adt/abapgit/repositories';
|
|
22
24
|
const NS_STAGING = 'http://www.sap.com/adt/abapgit/staging';
|
|
23
25
|
const NS_ADTCORE = 'http://www.sap.com/adt/core';
|
|
26
|
+
const ABAPGIT_REDACTION_MAX_STRING_LENGTH = 4_096;
|
|
27
|
+
const ABAPGIT_REDACTION_STRING_PREFIX_LENGTH = 2_000;
|
|
28
|
+
const GIT_SECRET_QUERY_FRAGMENTS = [
|
|
29
|
+
'password',
|
|
30
|
+
'passwd',
|
|
31
|
+
'passphrase',
|
|
32
|
+
'pwd',
|
|
33
|
+
'token',
|
|
34
|
+
'secret',
|
|
35
|
+
'apikey',
|
|
36
|
+
'api_key',
|
|
37
|
+
'auth',
|
|
38
|
+
'credential',
|
|
39
|
+
'accesskey',
|
|
40
|
+
'access_key',
|
|
41
|
+
'privatekey',
|
|
42
|
+
'private_key',
|
|
43
|
+
'sshkey',
|
|
44
|
+
'ssh_key',
|
|
45
|
+
'signature',
|
|
46
|
+
'cookie',
|
|
47
|
+
'session',
|
|
48
|
+
'sessionid',
|
|
49
|
+
'jsessionid',
|
|
50
|
+
'sapsessionid',
|
|
51
|
+
];
|
|
52
|
+
function isCredentialQueryKey(key) {
|
|
53
|
+
const normalized = key.toLowerCase();
|
|
54
|
+
return GIT_SECRET_QUERY_FRAGMENTS.some((fragment) => normalized.includes(fragment));
|
|
55
|
+
}
|
|
56
|
+
function redactCredentialAssignments(value) {
|
|
57
|
+
return value
|
|
58
|
+
.replace(/\bauthorization\s*([:=])\s*(?:bearer|basic)\s+(?:\\{1,64}"[^"]*\\{1,64}"|\\{1,64}'[^']*\\{1,64}'|"[^"]*"|'[^']*'|[^\s,;&<>"']+)/gi, 'authorization$1[REDACTED]')
|
|
59
|
+
.replace(/\b(bearer|basic)\s+(?:\\{1,64}"[^"]*\\{1,64}"|\\{1,64}'[^']*\\{1,64}'|"[^"]*"|'[^']*'|[^\s,;&<>"']+)/gi, '$1 [REDACTED]')
|
|
60
|
+
.replace(/\b(password|passwd|passphrase|pwd|token|secret|api[_-]?key|authorization|credential|access[_-]?key|private[_-]?key|ssh[_-]?key|signature|cookie|jsessionid|sap[_-]?sessionid|sessionid|session|(?:client[_-]?vcs[_-]?)?auth[_-]?(?:pwd|user|token)|remote[_-]?(?:password|user|token))(?:(?:\\{1,64})?["'])?\s*([:=])\s*(?:(?:bearer|basic)\s+)?(?:\\{1,64}"[^"]*\\{1,64}"|\\{1,64}'[^']*\\{1,64}'|"[^"]*"|'[^']*'|[^\s,;&<>"']+)/gi, '$1$2[REDACTED]');
|
|
61
|
+
}
|
|
62
|
+
function normalizeEscapedUrlSlashes(value) {
|
|
63
|
+
return value.replace(/\\{1,8}\//g, '/').replace(/\\{1,8}u([0-9a-f]{4})/gi, (encoded, hex) => {
|
|
64
|
+
const decoded = String.fromCharCode(Number.parseInt(hex, 16));
|
|
65
|
+
return /^[a-z0-9_.-]$/i.test(decoded) ? decoded : encoded;
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
function boundAbapGitString(value, originalLength = value.length) {
|
|
69
|
+
if (value.length <= ABAPGIT_REDACTION_MAX_STRING_LENGTH && originalLength <= ABAPGIT_REDACTION_MAX_STRING_LENGTH) {
|
|
70
|
+
return value;
|
|
71
|
+
}
|
|
72
|
+
return `${value.slice(0, ABAPGIT_REDACTION_STRING_PREFIX_LENGTH)}... [truncated ${originalLength} chars]`;
|
|
73
|
+
}
|
|
74
|
+
function abapGitStringInput(value) {
|
|
75
|
+
return value.length <= ABAPGIT_REDACTION_MAX_STRING_LENGTH
|
|
76
|
+
? value
|
|
77
|
+
: value.slice(0, ABAPGIT_REDACTION_MAX_STRING_LENGTH);
|
|
78
|
+
}
|
|
79
|
+
function redactUrlAssignments(value) {
|
|
80
|
+
return redactCredentialAssignments(value).replace(/(^|[&;])([^=&;]+)(?:=([^&;]*))?/g, (part, separator, rawKey) => {
|
|
81
|
+
let decodedKey = rawKey;
|
|
82
|
+
try {
|
|
83
|
+
decodedKey = decodeURIComponent(rawKey.replace(/\+/g, ' '));
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
// Keep the raw key for the sensitivity check.
|
|
87
|
+
}
|
|
88
|
+
return isCredentialQueryKey(decodedKey) ? `${separator}[REDACTED]=[REDACTED]` : part;
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
export function redactGitUrl(value) {
|
|
92
|
+
const normalizedValue = normalizeEscapedUrlSlashes(abapGitStringInput(value));
|
|
93
|
+
try {
|
|
94
|
+
const parsed = new URL(normalizedValue);
|
|
95
|
+
parsed.username = '';
|
|
96
|
+
parsed.password = '';
|
|
97
|
+
parsed.pathname = redactCredentialAssignments(parsed.pathname);
|
|
98
|
+
if (parsed.search)
|
|
99
|
+
parsed.search = `?${redactUrlAssignments(parsed.search.slice(1))}`;
|
|
100
|
+
if (parsed.hash)
|
|
101
|
+
parsed.hash = `#${redactUrlAssignments(parsed.hash.slice(1))}`;
|
|
102
|
+
return parsed.toString();
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
return '[INVALID URL]';
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
function redactGitText(value) {
|
|
109
|
+
const withoutXmlCredentials = redactCredentialAssignments(normalizeEscapedUrlSlashes(abapGitStringInput(value)))
|
|
110
|
+
.replace(/(<(?:[\w.-]+:)?remotePassword\b[^>]*>)[\s\S]*?(<\/(?:[\w.-]+:)?remotePassword>)/gi, '$1[REDACTED]$2')
|
|
111
|
+
.replace(/(<(?:[\w.-]+:)?remoteUser\b[^>]*>)[\s\S]*?(<\/(?:[\w.-]+:)?remoteUser>)/gi, '$1[REDACTED]$2');
|
|
112
|
+
const redacted = withoutXmlCredentials.replace(/https?:\/\/[^\s<>"']+/gi, (candidate) => {
|
|
113
|
+
const trailing = candidate.match(/[),.;]+$/)?.[0] ?? '';
|
|
114
|
+
const core = trailing ? candidate.slice(0, -trailing.length) : candidate;
|
|
115
|
+
return `${redactGitUrl(core)}${trailing}`;
|
|
116
|
+
});
|
|
117
|
+
return boundAbapGitString(redacted, value.length);
|
|
118
|
+
}
|
|
119
|
+
function sanitizedAbapGitApiError(err, path) {
|
|
120
|
+
const parsed = classifyAbapgitError(err.responseBody ?? '');
|
|
121
|
+
const detail = [parsed.namespace ? `[${parsed.namespace}]` : undefined, parsed.message].filter(Boolean).join(' ');
|
|
122
|
+
return new AdtApiError(redactGitText(detail || err.message), err.statusCode, redactGitText(err.path || path), redactGitText(err.responseBody ?? ''));
|
|
123
|
+
}
|
|
124
|
+
function literalHostIsPrivate(hostname) {
|
|
125
|
+
const host = hostname
|
|
126
|
+
.replace(/^\[|\]$/g, '')
|
|
127
|
+
.replace(/\.+$/, '')
|
|
128
|
+
.toLowerCase();
|
|
129
|
+
if (host === 'localhost' || host.endsWith('.localhost'))
|
|
130
|
+
return true;
|
|
131
|
+
const ipVersion = isIP(host);
|
|
132
|
+
if (ipVersion === 4) {
|
|
133
|
+
const octets = host.split('.').map(Number);
|
|
134
|
+
const [a = 0, b = 0] = octets;
|
|
135
|
+
return (a === 0 ||
|
|
136
|
+
a === 10 ||
|
|
137
|
+
a === 127 ||
|
|
138
|
+
(a === 100 && b >= 64 && b <= 127) ||
|
|
139
|
+
(a === 169 && b === 254) ||
|
|
140
|
+
(a === 172 && b >= 16 && b <= 31) ||
|
|
141
|
+
(a === 192 && b === 168));
|
|
142
|
+
}
|
|
143
|
+
if (ipVersion === 6) {
|
|
144
|
+
if (host === '::' || host === '::1')
|
|
145
|
+
return true;
|
|
146
|
+
if (/^f[cd]/.test(host) || /^fe[89ab]/.test(host))
|
|
147
|
+
return true;
|
|
148
|
+
const mappedV4 = host.match(/^::ffff:(\d+\.\d+\.\d+\.\d+)$/)?.[1];
|
|
149
|
+
if (mappedV4)
|
|
150
|
+
return literalHostIsPrivate(mappedV4);
|
|
151
|
+
// WHATWG URL canonicalizes dotted IPv4-mapped literals to two hexadecimal
|
|
152
|
+
// hextets (for example ::ffff:127.0.0.1 -> ::ffff:7f00:1).
|
|
153
|
+
const mappedHex = host.match(/^::ffff:([\da-f]{1,4}):([\da-f]{1,4})$/);
|
|
154
|
+
if (mappedHex) {
|
|
155
|
+
const high = Number.parseInt(mappedHex[1] ?? '0', 16);
|
|
156
|
+
const low = Number.parseInt(mappedHex[2] ?? '0', 16);
|
|
157
|
+
return literalHostIsPrivate(`${high >>> 8}.${high & 0xff}.${low >>> 8}.${low & 0xff}`);
|
|
158
|
+
}
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
return false;
|
|
162
|
+
}
|
|
163
|
+
/** Validate caller-controlled Git URLs before SAP is allowed to contact them. */
|
|
164
|
+
export function validateGitRemoteUrl(value, options = {}) {
|
|
165
|
+
let parsed;
|
|
166
|
+
try {
|
|
167
|
+
parsed = new URL(value);
|
|
168
|
+
}
|
|
169
|
+
catch {
|
|
170
|
+
throw new AdtSafetyError('Git remote URL is invalid. Use an absolute HTTPS URL.');
|
|
171
|
+
}
|
|
172
|
+
if (parsed.protocol !== 'https:') {
|
|
173
|
+
throw new AdtSafetyError('Git remote URL is blocked: only HTTPS URLs are allowed.');
|
|
174
|
+
}
|
|
175
|
+
if (parsed.username || parsed.password) {
|
|
176
|
+
throw new AdtSafetyError('Git remote URL is blocked: URL userinfo is not allowed; use explicit credential fields.');
|
|
177
|
+
}
|
|
178
|
+
if (options.rejectPrivateLiteral && literalHostIsPrivate(parsed.hostname)) {
|
|
179
|
+
throw new AdtSafetyError('Git remote URL is blocked: localhost and literal private/link-local addresses are not allowed.');
|
|
180
|
+
}
|
|
181
|
+
return parsed;
|
|
182
|
+
}
|
|
24
183
|
function boolish(value) {
|
|
25
184
|
if (value === true || value === false)
|
|
26
185
|
return value;
|
|
@@ -63,13 +222,32 @@ function authHeaders(user, password) {
|
|
|
63
222
|
};
|
|
64
223
|
}
|
|
65
224
|
/** Direct children of `node` under `key`, normalised to an array (fast-xml-parser collapses singletons). */
|
|
66
|
-
function childNodes(node, key) {
|
|
225
|
+
function childNodes(node, key, allowEmptyElement = false) {
|
|
67
226
|
const val = node?.[key];
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
227
|
+
const isNode = (entry) => typeof entry === 'object' && entry !== null && !Array.isArray(entry);
|
|
228
|
+
if (Array.isArray(val)) {
|
|
229
|
+
if (val.every(isNode))
|
|
230
|
+
return val;
|
|
231
|
+
throw new Error(`abapGit bridge returned an invalid <${key}> element shape.`);
|
|
232
|
+
}
|
|
233
|
+
if (isNode(val))
|
|
71
234
|
return [val];
|
|
72
|
-
|
|
235
|
+
if (val === undefined)
|
|
236
|
+
return [];
|
|
237
|
+
if (allowEmptyElement && typeof val === 'string' && val.trim() === '')
|
|
238
|
+
return [];
|
|
239
|
+
throw new Error(`abapGit bridge returned an invalid <${key}> element shape.`);
|
|
240
|
+
}
|
|
241
|
+
function singleChildNode(node, key, allowEmptyElement = false) {
|
|
242
|
+
const nodes = childNodes(node, key, allowEmptyElement);
|
|
243
|
+
if (nodes.length > 1)
|
|
244
|
+
throw new Error(`abapGit bridge returned duplicate <${key}> elements.`);
|
|
245
|
+
return nodes[0];
|
|
246
|
+
}
|
|
247
|
+
function assertKnownChildren(node, operation, allowed) {
|
|
248
|
+
const unknown = Object.keys(node).some((key) => !key.startsWith('@_') && !allowed.includes(key));
|
|
249
|
+
if (unknown)
|
|
250
|
+
throw new Error(`abapGit bridge returned unexpected child elements in ${operation}.`);
|
|
73
251
|
}
|
|
74
252
|
function attr(name, value) {
|
|
75
253
|
return value ? ` ${name}="${escapeXmlAttr(value)}"` : '';
|
|
@@ -102,11 +280,11 @@ function findRepoLink(repo, type) {
|
|
|
102
280
|
return href.endsWith(`/${relNeedle}`) || href.includes(`/${relNeedle}/`);
|
|
103
281
|
});
|
|
104
282
|
if (!link) {
|
|
105
|
-
throw new Error(`abapGit repository ${repo.key} does not expose a ${type} HATEOAS link.`);
|
|
283
|
+
throw new Error(redactGitText(`abapGit repository ${repo.key} does not expose a ${type} HATEOAS link.`));
|
|
106
284
|
}
|
|
107
285
|
return {
|
|
108
286
|
...link,
|
|
109
|
-
href: absolutizeLink(link.href),
|
|
287
|
+
href: assertCanonicalHostRelativeAdtPath(absolutizeLink(link.href), `${ABAPGIT_BASE}/`),
|
|
110
288
|
};
|
|
111
289
|
}
|
|
112
290
|
function parseAbapGitLinks(node) {
|
|
@@ -120,17 +298,36 @@ function parseAbapGitLinks(node) {
|
|
|
120
298
|
}))
|
|
121
299
|
.filter((link) => Boolean(link.rel) && Boolean(link.href));
|
|
122
300
|
}
|
|
301
|
+
function parseAbapGitXml(xml, operation) {
|
|
302
|
+
try {
|
|
303
|
+
return parseXml(xml);
|
|
304
|
+
}
|
|
305
|
+
catch {
|
|
306
|
+
// fast-xml-parser includes response prefixes in some syntax errors. SAP may echo remote
|
|
307
|
+
// credentials in that response, so expose only a fixed diagnostic across MCP/CLI boundaries.
|
|
308
|
+
throw new Error(`abapGit bridge returned invalid XML for ${operation}. Check the installed ADT backend version.`);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
123
311
|
export function parseAbapGitRepos(xml) {
|
|
124
|
-
const parsed =
|
|
125
|
-
const
|
|
126
|
-
|
|
127
|
-
|
|
312
|
+
const parsed = parseAbapGitXml(xml, 'repository-list');
|
|
313
|
+
const root = rootNode(parsed, 'repositories');
|
|
314
|
+
if (!root)
|
|
315
|
+
throw unexpectedShape('repository-list', 'repositories');
|
|
316
|
+
assertKnownChildren(root, 'repository-list', ['repository']);
|
|
317
|
+
const repositories = childNodes(root, 'repository');
|
|
318
|
+
return repositories.map((repo) => {
|
|
128
319
|
const links = parseAbapGitLinks(repo);
|
|
129
320
|
const writeProtected = boolish(field(repo, 'writeProtected', 'write_protected'));
|
|
321
|
+
const key = field(repo, 'key', 'repoKey', 'id') ?? '';
|
|
322
|
+
const packageName = field(repo, 'package', 'packageName') ?? '';
|
|
323
|
+
const url = field(repo, 'url') ?? '';
|
|
324
|
+
if (!key || !packageName || !url) {
|
|
325
|
+
throw new Error('abapGit bridge returned an incomplete <repository> row; key, package, and url are required.');
|
|
326
|
+
}
|
|
130
327
|
return {
|
|
131
|
-
key
|
|
132
|
-
package:
|
|
133
|
-
url
|
|
328
|
+
key,
|
|
329
|
+
package: packageName,
|
|
330
|
+
url,
|
|
134
331
|
branchName: field(repo, 'branchName', 'branch_name', 'branch') ?? '',
|
|
135
332
|
selectedBranch: field(repo, 'selectedBranch', 'selected_branch'),
|
|
136
333
|
deserializedBy: field(repo, 'deserializedBy', 'deserialized_by'),
|
|
@@ -140,32 +337,51 @@ export function parseAbapGitRepos(xml) {
|
|
|
140
337
|
dotAbapGit: field(repo, 'dotAbapGit', 'dot_abapgit'),
|
|
141
338
|
links,
|
|
142
339
|
};
|
|
143
|
-
})
|
|
144
|
-
.filter((repo) => repo.key && repo.package && repo.url);
|
|
340
|
+
});
|
|
145
341
|
}
|
|
146
342
|
export function parseAbapGitExternalInfo(xml) {
|
|
147
|
-
const parsed =
|
|
148
|
-
const infoNode =
|
|
149
|
-
|
|
343
|
+
const parsed = parseAbapGitXml(xml, 'external-info');
|
|
344
|
+
const infoNode = rootNode(parsed, 'externalRepoInfo');
|
|
345
|
+
if (!infoNode)
|
|
346
|
+
throw unexpectedShape('external-info', 'externalRepoInfo');
|
|
347
|
+
assertKnownChildren(infoNode, 'external-info', [
|
|
348
|
+
'accessMode',
|
|
349
|
+
'access_mode',
|
|
350
|
+
'defaultBranch',
|
|
351
|
+
'default_branch',
|
|
352
|
+
'selectedBranch',
|
|
353
|
+
'selected_branch',
|
|
354
|
+
'branch',
|
|
355
|
+
'user',
|
|
356
|
+
]);
|
|
357
|
+
const branches = childNodes(infoNode, 'branch').map((branch) => {
|
|
150
358
|
const name = field(branch, 'name', 'branchName', 'displayName') ?? '';
|
|
359
|
+
if (!name)
|
|
360
|
+
throw new Error('abapGit bridge returned an incomplete external branch row.');
|
|
151
361
|
return {
|
|
152
362
|
name,
|
|
153
363
|
isHead: boolish(field(branch, 'head', 'isHead')),
|
|
154
364
|
sha1: field(branch, 'sha1', 'hash'),
|
|
155
365
|
};
|
|
156
366
|
});
|
|
157
|
-
const userNode =
|
|
367
|
+
const userNode = singleChildNode(infoNode, 'user');
|
|
158
368
|
const user = userNode
|
|
159
369
|
? {
|
|
160
370
|
name: field(userNode, 'name', 'user'),
|
|
161
371
|
email: field(userNode, 'email', 'mail'),
|
|
162
372
|
}
|
|
163
373
|
: undefined;
|
|
374
|
+
const accessMode = field(infoNode, 'accessMode', 'access_mode');
|
|
375
|
+
const defaultBranch = field(infoNode, 'defaultBranch', 'default_branch');
|
|
376
|
+
const selectedBranch = field(infoNode, 'selectedBranch', 'selected_branch');
|
|
377
|
+
if (!accessMode && !defaultBranch && !selectedBranch && branches.length === 0) {
|
|
378
|
+
throw new Error('abapGit bridge returned an incomplete <externalRepoInfo> response.');
|
|
379
|
+
}
|
|
164
380
|
return {
|
|
165
|
-
accessMode
|
|
166
|
-
defaultBranch
|
|
167
|
-
selectedBranch
|
|
168
|
-
branches
|
|
381
|
+
accessMode,
|
|
382
|
+
defaultBranch,
|
|
383
|
+
selectedBranch,
|
|
384
|
+
branches,
|
|
169
385
|
...(user ? { user } : {}),
|
|
170
386
|
};
|
|
171
387
|
}
|
|
@@ -178,14 +394,18 @@ function rootNode(parsed, ...keys) {
|
|
|
178
394
|
if (!(key in parsed))
|
|
179
395
|
continue;
|
|
180
396
|
const value = parsed[key];
|
|
181
|
-
|
|
397
|
+
if (value && typeof value === 'object' && !Array.isArray(value))
|
|
398
|
+
return value;
|
|
399
|
+
if (typeof value === 'string' && value.trim() === '')
|
|
400
|
+
return {};
|
|
401
|
+
return undefined;
|
|
182
402
|
}
|
|
183
403
|
return undefined;
|
|
184
404
|
}
|
|
185
405
|
/** An unrecognised 200 body must fail loudly — a silent empty parse is what hid the old wire bugs. */
|
|
186
|
-
function unexpectedShape(what, expected
|
|
406
|
+
function unexpectedShape(what, expected) {
|
|
187
407
|
return new Error(`abapGit bridge returned an unexpected ${what} response: no <${expected}> root. ` +
|
|
188
|
-
|
|
408
|
+
'Check the installed abapGit ADT backend version.');
|
|
189
409
|
}
|
|
190
410
|
/**
|
|
191
411
|
* Parse a clone/pull response (ZABAPGIT_ST_REPO_POST_RES): `abapObjects:abapObjects` → `abapObject`,
|
|
@@ -193,32 +413,64 @@ function unexpectedShape(what, expected, xml) {
|
|
|
193
413
|
* the bridge deserialized into the package, with abapGit's own status message.
|
|
194
414
|
*/
|
|
195
415
|
export function parseAbapGitObjects(xml) {
|
|
196
|
-
const parsed =
|
|
416
|
+
const parsed = parseAbapGitXml(xml, 'clone/pull');
|
|
197
417
|
const root = rootNode(parsed, 'abapObjects', 'objects');
|
|
198
418
|
if (!root)
|
|
199
|
-
throw unexpectedShape('clone/pull', 'abapObjects'
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
419
|
+
throw unexpectedShape('clone/pull', 'abapObjects');
|
|
420
|
+
assertKnownChildren(root, 'clone/pull', ['abapObject', 'object']);
|
|
421
|
+
return [...childNodes(root, 'abapObject'), ...childNodes(root, 'object')].map((node) => {
|
|
422
|
+
const type = field(node, 'type', 'obj_type');
|
|
423
|
+
const name = field(node, 'name', 'obj_name');
|
|
424
|
+
if (!type || !name) {
|
|
425
|
+
throw new Error('abapGit bridge returned an incomplete object row; type and name are required.');
|
|
426
|
+
}
|
|
427
|
+
return {
|
|
428
|
+
type,
|
|
429
|
+
name,
|
|
430
|
+
package: field(node, 'package'),
|
|
431
|
+
status: field(node, 'status', 'obj_status'),
|
|
432
|
+
msgType: field(node, 'msgType', 'msg_type'),
|
|
433
|
+
msgText: field(node, 'msgText', 'msg_text'),
|
|
434
|
+
};
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
function assertSuccessfulObjectMessages(objects, path, responseBody) {
|
|
438
|
+
const rejected = objects.filter((object) => /^[EAX]$/i.test(String(object.msgType ?? '').trim()));
|
|
439
|
+
if (rejected.length === 0)
|
|
440
|
+
return;
|
|
441
|
+
const details = rejected
|
|
442
|
+
.slice(0, 5)
|
|
443
|
+
.map((object) => {
|
|
444
|
+
const identity = [object.type, object.name].filter(Boolean).join(' ');
|
|
445
|
+
return `${object.msgType}${identity ? ` ${identity}` : ''}${object.msgText ? `: ${object.msgText}` : ''}`;
|
|
446
|
+
})
|
|
447
|
+
.join('; ');
|
|
448
|
+
throw new AdtApiError(redactGitText(`abapGit reported rejecting object messages: ${details}`), 500, path, redactGitText(responseBody));
|
|
208
449
|
}
|
|
209
450
|
function parseStagingObjects(root, wrapper) {
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
451
|
+
const wrapperNode = singleChildNode(root, wrapper, true);
|
|
452
|
+
if (wrapperNode)
|
|
453
|
+
assertKnownChildren(wrapperNode, `<${wrapper}>`, ['abapgitobject']);
|
|
454
|
+
return childNodes(wrapperNode, 'abapgitobject').map((node) => {
|
|
455
|
+
const name = field(node, 'name');
|
|
456
|
+
const type = field(node, 'type');
|
|
457
|
+
if (!name || (wrapper !== 'ignored_objects' && !type)) {
|
|
458
|
+
throw new Error('abapGit bridge returned an incomplete staging object row.');
|
|
459
|
+
}
|
|
460
|
+
const files = childNodes(node, 'abapgitfile').map((file) => {
|
|
461
|
+
const fileName = field(file, 'name');
|
|
462
|
+
const path = field(file, 'path');
|
|
463
|
+
if (!fileName || !path)
|
|
464
|
+
throw new Error('abapGit bridge returned an incomplete staging file row.');
|
|
465
|
+
return {
|
|
466
|
+
name: fileName,
|
|
467
|
+
path,
|
|
468
|
+
localState: field(file, 'localState'),
|
|
469
|
+
remoteState: field(file, 'remoteState'),
|
|
470
|
+
};
|
|
471
|
+
});
|
|
472
|
+
return { name, type, uri: field(node, 'uri'), wbkey: field(node, 'wbkey'), files };
|
|
473
|
+
});
|
|
222
474
|
}
|
|
223
475
|
/**
|
|
224
476
|
* Parse a staging response (`abapgitstaging:abapgitstaging`, ZABAPGIT_ST_REPO_STAGE).
|
|
@@ -228,13 +480,14 @@ function parseStagingObjects(root, wrapper) {
|
|
|
228
480
|
* user, so a push can round-trip them instead of asking the caller for an identity.
|
|
229
481
|
*/
|
|
230
482
|
export function parseAbapGitStaging(xml) {
|
|
231
|
-
const parsed =
|
|
483
|
+
const parsed = parseAbapGitXml(xml, 'staging');
|
|
232
484
|
const root = rootNode(parsed, 'abapgitstaging');
|
|
233
485
|
if (!root)
|
|
234
|
-
throw unexpectedShape('staging', 'abapgitstaging'
|
|
235
|
-
|
|
486
|
+
throw unexpectedShape('staging', 'abapgitstaging');
|
|
487
|
+
assertKnownChildren(root, 'staging', ['unstaged_objects', 'staged_objects', 'ignored_objects', 'abapgit_comment']);
|
|
488
|
+
const commentNode = singleChildNode(root, 'abapgit_comment', true);
|
|
236
489
|
const user = (key) => {
|
|
237
|
-
const node =
|
|
490
|
+
const node = singleChildNode(commentNode, key);
|
|
238
491
|
if (!node)
|
|
239
492
|
return undefined;
|
|
240
493
|
return { name: field(node, 'name'), email: field(node, 'email') };
|
|
@@ -253,13 +506,7 @@ async function requestAbapGit(path, run) {
|
|
|
253
506
|
}
|
|
254
507
|
catch (err) {
|
|
255
508
|
if (err instanceof AdtApiError) {
|
|
256
|
-
|
|
257
|
-
if (parsed.message || parsed.namespace) {
|
|
258
|
-
const detail = [parsed.namespace ? `[${parsed.namespace}]` : undefined, parsed.message]
|
|
259
|
-
.filter(Boolean)
|
|
260
|
-
.join(' ');
|
|
261
|
-
throw new AdtApiError(detail, err.statusCode, err.path || path, err.responseBody);
|
|
262
|
-
}
|
|
509
|
+
throw sanitizedAbapGitApiError(err, path);
|
|
263
510
|
}
|
|
264
511
|
throw err;
|
|
265
512
|
}
|
|
@@ -324,7 +571,9 @@ export async function listRepos(http, safety) {
|
|
|
324
571
|
return parseAbapGitRepos(resp.body);
|
|
325
572
|
}
|
|
326
573
|
export async function getExternalInfo(http, safety, url, user, password) {
|
|
327
|
-
checkOperation(safety, OperationType.
|
|
574
|
+
checkOperation(safety, OperationType.Update, 'AbapGitExternalInfo');
|
|
575
|
+
checkGit(safety, 'external_info');
|
|
576
|
+
validateGitRemoteUrl(url, { rejectPrivateLiteral: true });
|
|
328
577
|
const path = `${ABAPGIT_BASE}/externalrepoinfo`;
|
|
329
578
|
const body = buildExternalInfoRequestXml(url, user, password);
|
|
330
579
|
const resp = await requestAbapGit(path, () => http.post(path, body, EXTERNAL_INFO_REQUEST_V2, {
|
|
@@ -336,14 +585,17 @@ export async function getExternalInfo(http, safety, url, user, password) {
|
|
|
336
585
|
export async function createRepo(http, safety, params, resolver) {
|
|
337
586
|
checkOperation(safety, OperationType.Create, 'AbapGitCreateRepo');
|
|
338
587
|
checkGit(safety, 'clone');
|
|
339
|
-
|
|
588
|
+
validateGitRemoteUrl(params.url);
|
|
589
|
+
await enforceRepoPackageAllowed(safety, params.package, resolver, 'AbapGitCreateRepo');
|
|
340
590
|
const path = `${ABAPGIT_BASE}/repos`;
|
|
341
591
|
const body = buildRepoPayloadXml(params);
|
|
342
592
|
const resp = await requestAbapGit(path, () => http.post(path, body, REPO_V3, {
|
|
343
593
|
Accept: REPO_OBJECT_ACCEPT,
|
|
344
594
|
...authHeaders(params.user, params.password),
|
|
345
595
|
}));
|
|
346
|
-
|
|
596
|
+
const objects = parseAbapGitObjects(resp.body);
|
|
597
|
+
assertSuccessfulObjectMessages(objects, path, resp.body);
|
|
598
|
+
return objects;
|
|
347
599
|
}
|
|
348
600
|
/**
|
|
349
601
|
* Enforce the package allowlist against a repository's server-bound package (R9).
|
|
@@ -360,11 +612,22 @@ export async function enforceRepoPackageAllowed(safety, repoPackage, resolver, l
|
|
|
360
612
|
if (!repoPackage) {
|
|
361
613
|
throw new AdtSafetyError(`${label}: cannot resolve the repository's package to check it against allowedPackages (${JSON.stringify(safety.allowedPackages)}); refusing.`);
|
|
362
614
|
}
|
|
363
|
-
|
|
615
|
+
const subtreeGrants = safety.allowedPackages.filter((entry) => {
|
|
616
|
+
const normalized = entry.trim().toUpperCase();
|
|
617
|
+
return normalized === '*' || normalized.endsWith('/**');
|
|
618
|
+
});
|
|
619
|
+
if (subtreeGrants.length === 0) {
|
|
620
|
+
throw new AdtSafetyError(`${label}: abapGit may create or update subpackages below '${repoPackage}'. ` +
|
|
621
|
+
`Require a subtree grant that contains '${repoPackage}' or global '*' (configured: ${JSON.stringify(safety.allowedPackages)}); exact-package and prefix-wildcard grants are insufficient.`);
|
|
622
|
+
}
|
|
623
|
+
await checkPackage({ ...safety, allowedPackages: subtreeGrants }, repoPackage, resolver);
|
|
364
624
|
}
|
|
365
|
-
export async function pullRepo(http, safety, repoId, params = {}) {
|
|
625
|
+
export async function pullRepo(http, safety, repoId, params = {}, resolver, repoPackage) {
|
|
366
626
|
checkOperation(safety, OperationType.Update, 'AbapGitPullRepo');
|
|
367
627
|
checkGit(safety, 'pull');
|
|
628
|
+
await enforceRepoPackageAllowed(safety, repoPackage, resolver, 'AbapGitPullRepo');
|
|
629
|
+
if (params.url)
|
|
630
|
+
validateGitRemoteUrl(params.url);
|
|
368
631
|
const path = `${ABAPGIT_BASE}/repos/${encodeURIComponent(repoId)}/pull`;
|
|
369
632
|
const body = buildRepoPayloadXml({
|
|
370
633
|
package: params.package ?? '$TMP',
|
|
@@ -378,13 +641,19 @@ export async function pullRepo(http, safety, repoId, params = {}) {
|
|
|
378
641
|
Accept: REPO_OBJECT_ACCEPT,
|
|
379
642
|
...authHeaders(params.user, params.password),
|
|
380
643
|
}));
|
|
381
|
-
|
|
644
|
+
const objects = parseAbapGitObjects(resp.body);
|
|
645
|
+
assertSuccessfulObjectMessages(objects, path, resp.body);
|
|
646
|
+
return objects;
|
|
382
647
|
}
|
|
383
|
-
export async function unlinkRepo(http, safety, repoId) {
|
|
648
|
+
export async function unlinkRepo(http, safety, repoId, resolver, repoPackage) {
|
|
384
649
|
checkOperation(safety, OperationType.Delete, 'AbapGitUnlinkRepo');
|
|
385
650
|
checkGit(safety, 'unlink');
|
|
651
|
+
await enforceRepoPackageAllowed(safety, repoPackage, resolver, 'AbapGitUnlinkRepo');
|
|
386
652
|
const path = `${ABAPGIT_BASE}/repos/${encodeURIComponent(repoId)}`;
|
|
387
|
-
await requestAbapGit(path, () => http.delete(path, { Accept: REPO_V3 }));
|
|
653
|
+
const response = await requestAbapGit(path, () => http.delete(path, { Accept: REPO_V3 }));
|
|
654
|
+
if (response.body.trim() !== '') {
|
|
655
|
+
throw new Error('abapGit bridge returned an unexpected non-empty unlink response; repository removal was not accepted as verified.');
|
|
656
|
+
}
|
|
388
657
|
}
|
|
389
658
|
export async function stageRepo(http, safety, repo, user, password) {
|
|
390
659
|
checkOperation(safety, OperationType.Update, 'AbapGitStageRepo');
|
|
@@ -401,15 +670,19 @@ export async function stageRepo(http, safety, repo, user, password) {
|
|
|
401
670
|
...parseAbapGitStaging(resp.body),
|
|
402
671
|
};
|
|
403
672
|
}
|
|
404
|
-
export async function pushRepo(http, safety, repo, staging, user, password) {
|
|
673
|
+
export async function pushRepo(http, safety, repo, staging, user, password, resolver) {
|
|
405
674
|
checkOperation(safety, OperationType.Update, 'AbapGitPushRepo');
|
|
406
675
|
checkGit(safety, 'push');
|
|
676
|
+
await enforceRepoPackageAllowed(safety, repo.package, resolver, 'AbapGitPushRepo');
|
|
407
677
|
const link = findRepoLink(repo, 'push_link');
|
|
408
678
|
const body = buildStagingPayloadXml(staging);
|
|
409
|
-
await requestAbapGit(link.href, () => http.post(link.href, body, REPO_STAGE_V1, {
|
|
679
|
+
const response = await requestAbapGit(link.href, () => http.post(link.href, body, REPO_STAGE_V1, {
|
|
410
680
|
Accept: REPO_STAGE_V1,
|
|
411
681
|
...authHeaders(user, password),
|
|
412
682
|
}));
|
|
683
|
+
if (response.body.trim() !== '') {
|
|
684
|
+
throw new Error('abapGit bridge returned an unexpected non-empty push response; remote completion was not accepted as verified.');
|
|
685
|
+
}
|
|
413
686
|
}
|
|
414
687
|
export async function checkRepo(http, safety, repo, user, password) {
|
|
415
688
|
checkOperation(safety, OperationType.Read, 'AbapGitCheckRepo');
|
|
@@ -428,8 +701,12 @@ export async function checkRepo(http, safety, repo, user, password) {
|
|
|
428
701
|
if (err instanceof AdtApiError) {
|
|
429
702
|
const parsed = classifyAbapgitError(err.responseBody ?? '');
|
|
430
703
|
if (parsed.namespace === 'org.abapgit.adt') {
|
|
431
|
-
return {
|
|
704
|
+
return {
|
|
705
|
+
ok: false,
|
|
706
|
+
message: redactGitText(parsed.message ?? AdtApiError.extractCleanMessage(err.responseBody ?? '')),
|
|
707
|
+
};
|
|
432
708
|
}
|
|
709
|
+
throw sanitizedAbapGitApiError(err, link.href);
|
|
433
710
|
}
|
|
434
711
|
throw err;
|
|
435
712
|
}
|
|
@@ -439,17 +716,21 @@ export async function checkRepo(http, safety, repo, user, password) {
|
|
|
439
716
|
const parsed = classifyAbapgitError(resp.body);
|
|
440
717
|
return {
|
|
441
718
|
ok: false,
|
|
442
|
-
message: parsed.message ?? AdtApiError.extractCleanMessage(resp.body),
|
|
719
|
+
message: redactGitText(parsed.message ?? AdtApiError.extractCleanMessage(resp.body)),
|
|
443
720
|
};
|
|
444
721
|
}
|
|
445
|
-
export async function switchBranch(http, safety, repoId, branch, create = false, user, password) {
|
|
722
|
+
export async function switchBranch(http, safety, repoId, branch, create = false, user, password, resolver, repoPackage) {
|
|
446
723
|
checkOperation(safety, OperationType.Update, 'AbapGitSwitchBranch');
|
|
447
724
|
checkGit(safety, create ? 'create_branch' : 'switch_branch');
|
|
725
|
+
await enforceRepoPackageAllowed(safety, repoPackage, resolver, 'AbapGitSwitchBranch');
|
|
448
726
|
const path = `${ABAPGIT_BASE}/repos/${encodeURIComponent(repoId)}/branches/${encodeURIComponent(branch)}?create=${create ? 'true' : 'false'}`;
|
|
449
727
|
// Switching fetches from the remote, so a private repo needs the bridge credentials here too.
|
|
450
|
-
await requestAbapGit(path, () => http.post(path, '', undefined, { Accept: REPO_V3, ...authHeaders(user, password) }));
|
|
728
|
+
const response = await requestAbapGit(path, () => http.post(path, '', undefined, { Accept: REPO_V3, ...authHeaders(user, password) }));
|
|
729
|
+
if (response.body.trim() !== '') {
|
|
730
|
+
throw new Error(`abapGit bridge returned an unexpected non-empty ${create ? 'create-branch' : 'switch-branch'} response; branch completion was not accepted as verified.`);
|
|
731
|
+
}
|
|
451
732
|
}
|
|
452
|
-
export async function createBranch(http, safety, repoId, branch, user, password) {
|
|
453
|
-
await switchBranch(http, safety, repoId, branch, true, user, password);
|
|
733
|
+
export async function createBranch(http, safety, repoId, branch, user, password, resolver, repoPackage) {
|
|
734
|
+
await switchBranch(http, safety, repoId, branch, true, user, password, resolver, repoPackage);
|
|
454
735
|
}
|
|
455
736
|
//# sourceMappingURL=abapgit.js.map
|