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.
Files changed (166) hide show
  1. package/bin/arc1-cli.js +3 -7
  2. package/bin/arc1.js +4 -9
  3. package/dist/adt/abapgit.d.ts +10 -5
  4. package/dist/adt/abapgit.d.ts.map +1 -1
  5. package/dist/adt/abapgit.js +357 -76
  6. package/dist/adt/abapgit.js.map +1 -1
  7. package/dist/adt/atc.d.ts +49 -0
  8. package/dist/adt/atc.d.ts.map +1 -0
  9. package/dist/adt/atc.js +294 -0
  10. package/dist/adt/atc.js.map +1 -0
  11. package/dist/adt/aunit-package.d.ts +22 -0
  12. package/dist/adt/aunit-package.d.ts.map +1 -0
  13. package/dist/adt/aunit-package.js +78 -0
  14. package/dist/adt/aunit-package.js.map +1 -0
  15. package/dist/adt/aunit.d.ts +143 -0
  16. package/dist/adt/aunit.d.ts.map +1 -0
  17. package/dist/adt/aunit.js +987 -0
  18. package/dist/adt/aunit.js.map +1 -0
  19. package/dist/adt/client.d.ts +9 -7
  20. package/dist/adt/client.d.ts.map +1 -1
  21. package/dist/adt/client.js +55 -64
  22. package/dist/adt/client.js.map +1 -1
  23. package/dist/adt/config.d.ts +2 -0
  24. package/dist/adt/config.d.ts.map +1 -1
  25. package/dist/adt/config.js +1 -0
  26. package/dist/adt/config.js.map +1 -1
  27. package/dist/adt/devtools.d.ts +8 -30
  28. package/dist/adt/devtools.d.ts.map +1 -1
  29. package/dist/adt/devtools.js +41 -218
  30. package/dist/adt/devtools.js.map +1 -1
  31. package/dist/adt/diagnostics.d.ts +1 -1
  32. package/dist/adt/diagnostics.d.ts.map +1 -1
  33. package/dist/adt/diagnostics.js +12 -12
  34. package/dist/adt/diagnostics.js.map +1 -1
  35. package/dist/adt/errors.d.ts +7 -0
  36. package/dist/adt/errors.d.ts.map +1 -1
  37. package/dist/adt/errors.js +7 -0
  38. package/dist/adt/errors.js.map +1 -1
  39. package/dist/adt/features.d.ts +9 -3
  40. package/dist/adt/features.d.ts.map +1 -1
  41. package/dist/adt/features.js +40 -8
  42. package/dist/adt/features.js.map +1 -1
  43. package/dist/adt/gcts.d.ts +12 -45
  44. package/dist/adt/gcts.d.ts.map +1 -1
  45. package/dist/adt/gcts.js +299 -171
  46. package/dist/adt/gcts.js.map +1 -1
  47. package/dist/adt/http-deadline.d.ts +23 -0
  48. package/dist/adt/http-deadline.d.ts.map +1 -0
  49. package/dist/adt/http-deadline.js +106 -0
  50. package/dist/adt/http-deadline.js.map +1 -0
  51. package/dist/adt/http-wire-body.d.ts +2 -0
  52. package/dist/adt/http-wire-body.d.ts.map +1 -0
  53. package/dist/adt/http-wire-body.js +14 -0
  54. package/dist/adt/http-wire-body.js.map +1 -0
  55. package/dist/adt/http.d.ts +10 -24
  56. package/dist/adt/http.d.ts.map +1 -1
  57. package/dist/adt/http.js +89 -53
  58. package/dist/adt/http.js.map +1 -1
  59. package/dist/adt/path-safety.d.ts +16 -0
  60. package/dist/adt/path-safety.d.ts.map +1 -0
  61. package/dist/adt/path-safety.js +101 -0
  62. package/dist/adt/path-safety.js.map +1 -0
  63. package/dist/adt/semaphore.d.ts +2 -2
  64. package/dist/adt/semaphore.d.ts.map +1 -1
  65. package/dist/adt/semaphore.js +26 -9
  66. package/dist/adt/semaphore.js.map +1 -1
  67. package/dist/adt/source-diff.d.ts +10 -1
  68. package/dist/adt/source-diff.d.ts.map +1 -1
  69. package/dist/adt/source-diff.js +33 -7
  70. package/dist/adt/source-diff.js.map +1 -1
  71. package/dist/adt/text-search.d.ts +25 -0
  72. package/dist/adt/text-search.d.ts.map +1 -0
  73. package/dist/adt/text-search.js +152 -0
  74. package/dist/adt/text-search.js.map +1 -0
  75. package/dist/adt/transport-diff.d.ts +128 -0
  76. package/dist/adt/transport-diff.d.ts.map +1 -0
  77. package/dist/adt/transport-diff.js +524 -0
  78. package/dist/adt/transport-diff.js.map +1 -0
  79. package/dist/adt/transport.d.ts +78 -13
  80. package/dist/adt/transport.d.ts.map +1 -1
  81. package/dist/adt/transport.js +443 -41
  82. package/dist/adt/transport.js.map +1 -1
  83. package/dist/adt/types.d.ts +23 -12
  84. package/dist/adt/types.d.ts.map +1 -1
  85. package/dist/adt/xml-parser.d.ts +1 -8
  86. package/dist/adt/xml-parser.d.ts.map +1 -1
  87. package/dist/adt/xml-parser.js +54 -60
  88. package/dist/adt/xml-parser.js.map +1 -1
  89. package/dist/authz/policy.d.ts.map +1 -1
  90. package/dist/authz/policy.js +5 -2
  91. package/dist/authz/policy.js.map +1 -1
  92. package/dist/cli-args.d.ts +2 -2
  93. package/dist/cli-args.js +2 -2
  94. package/dist/cli-checks.d.ts +56 -0
  95. package/dist/cli-checks.d.ts.map +1 -0
  96. package/dist/cli-checks.js +368 -0
  97. package/dist/cli-checks.js.map +1 -0
  98. package/dist/cli.d.ts +68 -15
  99. package/dist/cli.d.ts.map +1 -1
  100. package/dist/cli.js +677 -297
  101. package/dist/cli.js.map +1 -1
  102. package/dist/handlers/cds-hints.d.ts.map +1 -1
  103. package/dist/handlers/cds-hints.js +1 -10
  104. package/dist/handlers/cds-hints.js.map +1 -1
  105. package/dist/handlers/diagnose.d.ts +4 -0
  106. package/dist/handlers/diagnose.d.ts.map +1 -1
  107. package/dist/handlers/diagnose.js +616 -9
  108. package/dist/handlers/diagnose.js.map +1 -1
  109. package/dist/handlers/dispatch.d.ts.map +1 -1
  110. package/dist/handlers/dispatch.js +40 -1
  111. package/dist/handlers/dispatch.js.map +1 -1
  112. package/dist/handlers/git.d.ts.map +1 -1
  113. package/dist/handlers/git.js +165 -84
  114. package/dist/handlers/git.js.map +1 -1
  115. package/dist/handlers/read.d.ts.map +1 -1
  116. package/dist/handlers/read.js +15 -0
  117. package/dist/handlers/read.js.map +1 -1
  118. package/dist/handlers/schemas.d.ts +15 -2
  119. package/dist/handlers/schemas.d.ts.map +1 -1
  120. package/dist/handlers/schemas.js +75 -6
  121. package/dist/handlers/schemas.js.map +1 -1
  122. package/dist/handlers/search.js +1 -1
  123. package/dist/handlers/search.js.map +1 -1
  124. package/dist/handlers/tools.d.ts.map +1 -1
  125. package/dist/handlers/tools.js +51 -42
  126. package/dist/handlers/tools.js.map +1 -1
  127. package/dist/handlers/transport.d.ts +2 -1
  128. package/dist/handlers/transport.d.ts.map +1 -1
  129. package/dist/handlers/transport.js +128 -27
  130. package/dist/handlers/transport.js.map +1 -1
  131. package/dist/handlers/write/create.d.ts.map +1 -1
  132. package/dist/handlers/write/create.js +11 -0
  133. package/dist/handlers/write/create.js.map +1 -1
  134. package/dist/handlers/write/update-delete.d.ts.map +1 -1
  135. package/dist/handlers/write/update-delete.js +33 -10
  136. package/dist/handlers/write/update-delete.js.map +1 -1
  137. package/dist/handlers/write-helpers.d.ts.map +1 -1
  138. package/dist/handlers/write-helpers.js +3 -2
  139. package/dist/handlers/write-helpers.js.map +1 -1
  140. package/dist/server/audit.d.ts +1 -1
  141. package/dist/server/audit.d.ts.map +1 -1
  142. package/dist/server/audit.js +228 -21
  143. package/dist/server/audit.js.map +1 -1
  144. package/dist/server/config.d.ts +16 -0
  145. package/dist/server/config.d.ts.map +1 -1
  146. package/dist/server/config.js +145 -17
  147. package/dist/server/config.js.map +1 -1
  148. package/dist/server/effective-policy-log.d.ts +2 -1
  149. package/dist/server/effective-policy-log.d.ts.map +1 -1
  150. package/dist/server/effective-policy-log.js +8 -2
  151. package/dist/server/effective-policy-log.js.map +1 -1
  152. package/dist/server/multi-target-runtime.d.ts.map +1 -1
  153. package/dist/server/multi-target-runtime.js +3 -0
  154. package/dist/server/multi-target-runtime.js.map +1 -1
  155. package/dist/server/multi-target-tools.d.ts.map +1 -1
  156. package/dist/server/multi-target-tools.js +5 -0
  157. package/dist/server/multi-target-tools.js.map +1 -1
  158. package/dist/server/server.d.ts +15 -13
  159. package/dist/server/server.d.ts.map +1 -1
  160. package/dist/server/server.js +36 -26
  161. package/dist/server/server.js.map +1 -1
  162. package/dist/server/types.d.ts +2 -0
  163. package/dist/server/types.d.ts.map +1 -1
  164. package/dist/server/types.js +1 -0
  165. package/dist/server/types.js.map +1 -1
  166. package/package.json +13 -13
@@ -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
- if (Array.isArray(val))
69
- return val;
70
- if (val && typeof val === 'object')
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
- return [];
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 = parseXml(xml);
125
- const repositories = findDeepNodes(parsed, 'repository');
126
- return repositories
127
- .map((repo) => {
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: field(repo, 'key', 'repoKey', 'id') ?? '',
132
- package: field(repo, 'package', 'packageName') ?? '',
133
- url: field(repo, '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 = parseXml(xml);
148
- const infoNode = findDeepNodes(parsed, 'externalRepoInfo')[0] ?? parsed;
149
- const branches = findDeepNodes(infoNode, 'branch').map((branch) => {
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 = findDeepNodes(infoNode, 'user')[0];
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: field(infoNode, 'accessMode', 'access_mode'),
166
- defaultBranch: field(infoNode, 'defaultBranch', 'default_branch'),
167
- selectedBranch: field(infoNode, 'selectedBranch', 'selected_branch'),
168
- branches: branches.filter((branch) => branch.name),
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
- return value && typeof value === 'object' ? value : {};
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, xml) {
406
+ function unexpectedShape(what, expected) {
187
407
  return new Error(`abapGit bridge returned an unexpected ${what} response: no <${expected}> root. ` +
188
- `Check the installed abapGit ADT backend version. First 200 chars: ${xml.trim().slice(0, 200)}`);
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 = parseXml(xml);
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', xml);
200
- return [...childNodes(root, 'abapObject'), ...childNodes(root, 'object')].map((node) => ({
201
- type: field(node, 'type', 'obj_type'),
202
- name: field(node, 'name', 'obj_name'),
203
- package: field(node, 'package'),
204
- status: field(node, 'status', 'obj_status'),
205
- msgType: field(node, 'msgType', 'msg_type'),
206
- msgText: field(node, 'msgText', 'msg_text'),
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
- return childNodes(childNodes(root, wrapper)[0], 'abapgitobject').map((node) => ({
211
- name: field(node, 'name'),
212
- type: field(node, 'type'),
213
- uri: field(node, 'uri'),
214
- wbkey: field(node, 'wbkey'),
215
- files: childNodes(node, 'abapgitfile').map((file) => ({
216
- name: field(file, 'name') ?? '',
217
- path: field(file, 'path'),
218
- localState: field(file, 'localState'),
219
- remoteState: field(file, 'remoteState'),
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 = parseXml(xml);
483
+ const parsed = parseAbapGitXml(xml, 'staging');
232
484
  const root = rootNode(parsed, 'abapgitstaging');
233
485
  if (!root)
234
- throw unexpectedShape('staging', 'abapgitstaging', xml);
235
- const commentNode = childNodes(root, 'abapgit_comment')[0];
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 = childNodes(commentNode, key)[0];
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
- const parsed = classifyAbapgitError(err.responseBody ?? '');
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.Read, 'AbapGitExternalInfo');
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
- await checkPackage(safety, params.package, resolver);
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
- return parseAbapGitObjects(resp.body);
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
- await checkPackage(safety, repoPackage, resolver);
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
- return parseAbapGitObjects(resp.body);
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 { ok: false, message: parsed.message ?? AdtApiError.extractCleanMessage(err.responseBody ?? '') };
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