arc-1 0.6.9 → 0.7.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 (157) hide show
  1. package/README.md +12 -9
  2. package/bin/arc1-cli.js +10 -0
  3. package/bin/arc1.js +1 -1
  4. package/dist/adt/abapgit.d.ts +39 -0
  5. package/dist/adt/abapgit.d.ts.map +1 -0
  6. package/dist/adt/abapgit.js +333 -0
  7. package/dist/adt/abapgit.js.map +1 -0
  8. package/dist/adt/cds-impact.d.ts +35 -0
  9. package/dist/adt/cds-impact.d.ts.map +1 -1
  10. package/dist/adt/cds-impact.js +71 -0
  11. package/dist/adt/cds-impact.js.map +1 -1
  12. package/dist/adt/client.d.ts +4 -1
  13. package/dist/adt/client.d.ts.map +1 -1
  14. package/dist/adt/client.js +18 -5
  15. package/dist/adt/client.js.map +1 -1
  16. package/dist/adt/config.d.ts +1 -0
  17. package/dist/adt/config.d.ts.map +1 -1
  18. package/dist/adt/config.js +1 -0
  19. package/dist/adt/config.js.map +1 -1
  20. package/dist/adt/crud.d.ts.map +1 -1
  21. package/dist/adt/crud.js +74 -8
  22. package/dist/adt/crud.js.map +1 -1
  23. package/dist/adt/devtools.d.ts +39 -3
  24. package/dist/adt/devtools.d.ts.map +1 -1
  25. package/dist/adt/devtools.js +237 -25
  26. package/dist/adt/devtools.js.map +1 -1
  27. package/dist/adt/diagnostics.d.ts +69 -7
  28. package/dist/adt/diagnostics.d.ts.map +1 -1
  29. package/dist/adt/diagnostics.js +694 -36
  30. package/dist/adt/diagnostics.js.map +1 -1
  31. package/dist/adt/errors.d.ts +36 -2
  32. package/dist/adt/errors.d.ts.map +1 -1
  33. package/dist/adt/errors.js +111 -11
  34. package/dist/adt/errors.js.map +1 -1
  35. package/dist/adt/features.d.ts.map +1 -1
  36. package/dist/adt/features.js +3 -0
  37. package/dist/adt/features.js.map +1 -1
  38. package/dist/adt/gcts.d.ts +68 -0
  39. package/dist/adt/gcts.d.ts.map +1 -0
  40. package/dist/adt/gcts.js +239 -0
  41. package/dist/adt/gcts.js.map +1 -0
  42. package/dist/adt/http.d.ts.map +1 -1
  43. package/dist/adt/http.js +86 -1
  44. package/dist/adt/http.js.map +1 -1
  45. package/dist/adt/rap-handlers.d.ts +165 -0
  46. package/dist/adt/rap-handlers.d.ts.map +1 -0
  47. package/dist/adt/rap-handlers.js +835 -0
  48. package/dist/adt/rap-handlers.js.map +1 -0
  49. package/dist/adt/rap-preflight.d.ts +43 -0
  50. package/dist/adt/rap-preflight.d.ts.map +1 -0
  51. package/dist/adt/rap-preflight.js +405 -0
  52. package/dist/adt/rap-preflight.js.map +1 -0
  53. package/dist/adt/safety.d.ts +60 -33
  54. package/dist/adt/safety.d.ts.map +1 -1
  55. package/dist/adt/safety.js +204 -113
  56. package/dist/adt/safety.js.map +1 -1
  57. package/dist/adt/transport.d.ts +1 -1
  58. package/dist/adt/transport.d.ts.map +1 -1
  59. package/dist/adt/transport.js +6 -3
  60. package/dist/adt/transport.js.map +1 -1
  61. package/dist/adt/types.d.ts +225 -0
  62. package/dist/adt/types.d.ts.map +1 -1
  63. package/dist/adt/xml-parser.d.ts +15 -1
  64. package/dist/adt/xml-parser.d.ts.map +1 -1
  65. package/dist/adt/xml-parser.js +28 -15
  66. package/dist/adt/xml-parser.js.map +1 -1
  67. package/dist/authz/policy.d.ts +53 -0
  68. package/dist/authz/policy.d.ts.map +1 -0
  69. package/dist/authz/policy.js +199 -0
  70. package/dist/authz/policy.js.map +1 -0
  71. package/dist/cli-args.d.ts +14 -0
  72. package/dist/cli-args.d.ts.map +1 -0
  73. package/dist/cli-args.js +62 -0
  74. package/dist/cli-args.js.map +1 -0
  75. package/dist/cli.d.ts +13 -7
  76. package/dist/cli.d.ts.map +1 -1
  77. package/dist/cli.js +252 -55
  78. package/dist/cli.js.map +1 -1
  79. package/dist/extract-sap-cookies.d.ts +24 -0
  80. package/dist/extract-sap-cookies.d.ts.map +1 -0
  81. package/dist/extract-sap-cookies.js +317 -0
  82. package/dist/extract-sap-cookies.js.map +1 -0
  83. package/dist/handlers/hyperfocused.d.ts +4 -3
  84. package/dist/handlers/hyperfocused.d.ts.map +1 -1
  85. package/dist/handlers/hyperfocused.js +25 -16
  86. package/dist/handlers/hyperfocused.js.map +1 -1
  87. package/dist/handlers/intent.d.ts +4 -12
  88. package/dist/handlers/intent.d.ts.map +1 -1
  89. package/dist/handlers/intent.js +1448 -89
  90. package/dist/handlers/intent.js.map +1 -1
  91. package/dist/handlers/schemas.d.ts +83 -11
  92. package/dist/handlers/schemas.d.ts.map +1 -1
  93. package/dist/handlers/schemas.js +115 -4
  94. package/dist/handlers/schemas.js.map +1 -1
  95. package/dist/handlers/tools.d.ts +4 -3
  96. package/dist/handlers/tools.d.ts.map +1 -1
  97. package/dist/handlers/tools.js +342 -143
  98. package/dist/handlers/tools.js.map +1 -1
  99. package/dist/index.d.ts +1 -1
  100. package/dist/index.js +7 -6
  101. package/dist/index.js.map +1 -1
  102. package/dist/probe/catalog.d.ts +30 -0
  103. package/dist/probe/catalog.d.ts.map +1 -0
  104. package/dist/probe/catalog.js +196 -0
  105. package/dist/probe/catalog.js.map +1 -0
  106. package/dist/probe/fixtures.d.ts +54 -0
  107. package/dist/probe/fixtures.d.ts.map +1 -0
  108. package/dist/probe/fixtures.js +94 -0
  109. package/dist/probe/fixtures.js.map +1 -0
  110. package/dist/probe/format.d.ts +10 -0
  111. package/dist/probe/format.d.ts.map +1 -0
  112. package/dist/probe/format.js +114 -0
  113. package/dist/probe/format.js.map +1 -0
  114. package/dist/probe/quality.d.ts +13 -0
  115. package/dist/probe/quality.d.ts.map +1 -0
  116. package/dist/probe/quality.js +50 -0
  117. package/dist/probe/quality.js.map +1 -0
  118. package/dist/probe/runner.d.ts +48 -0
  119. package/dist/probe/runner.d.ts.map +1 -0
  120. package/dist/probe/runner.js +211 -0
  121. package/dist/probe/runner.js.map +1 -0
  122. package/dist/probe/types.d.ts +159 -0
  123. package/dist/probe/types.d.ts.map +1 -0
  124. package/dist/probe/types.js +11 -0
  125. package/dist/probe/types.js.map +1 -0
  126. package/dist/server/audit.d.ts +26 -3
  127. package/dist/server/audit.d.ts.map +1 -1
  128. package/dist/server/audit.js +12 -1
  129. package/dist/server/audit.js.map +1 -1
  130. package/dist/server/config.d.ts +34 -19
  131. package/dist/server/config.d.ts.map +1 -1
  132. package/dist/server/config.js +327 -187
  133. package/dist/server/config.js.map +1 -1
  134. package/dist/server/deny-actions.d.ts +31 -0
  135. package/dist/server/deny-actions.d.ts.map +1 -0
  136. package/dist/server/deny-actions.js +156 -0
  137. package/dist/server/deny-actions.js.map +1 -0
  138. package/dist/server/effective-policy-log.d.ts +27 -0
  139. package/dist/server/effective-policy-log.d.ts.map +1 -0
  140. package/dist/server/effective-policy-log.js +103 -0
  141. package/dist/server/effective-policy-log.js.map +1 -0
  142. package/dist/server/http.d.ts.map +1 -1
  143. package/dist/server/http.js +15 -16
  144. package/dist/server/http.js.map +1 -1
  145. package/dist/server/server.d.ts +38 -4
  146. package/dist/server/server.d.ts.map +1 -1
  147. package/dist/server/server.js +234 -31
  148. package/dist/server/server.js.map +1 -1
  149. package/dist/server/types.d.ts +31 -13
  150. package/dist/server/types.d.ts.map +1 -1
  151. package/dist/server/types.js +11 -10
  152. package/dist/server/types.js.map +1 -1
  153. package/dist/server/xsuaa.d.ts +1 -2
  154. package/dist/server/xsuaa.d.ts.map +1 -1
  155. package/dist/server/xsuaa.js +13 -14
  156. package/dist/server/xsuaa.js.map +1 -1
  157. package/package.json +9 -3
@@ -4,14 +4,99 @@
4
4
  * Resolves configuration from CLI flags, environment variables, and defaults.
5
5
  * Priority: CLI > env > .env > defaults
6
6
  *
7
- * Environment variable names match the Go version exactly (SAP_URL, SAP_USER, etc.)
8
- * for drop-in compatibility with existing deployments and documentation.
7
+ * Post-authz-refactor-v2 (v0.7):
8
+ * - Profile layer (`ARC1_PROFILE`) was removed. Use explicit `SAP_ALLOW_*` env vars.
9
+ * - Op-code allowlist/blocklist env vars (`SAP_ALLOWED_OPS` / `SAP_DISALLOWED_OPS`)
10
+ * were removed. Use `SAP_DENY_ACTIONS` for fine-grained per-action denials.
11
+ * - Single `ARC1_API_KEY` was removed. Use `ARC1_API_KEYS="key:profile"` instead.
12
+ * - Negated safety flags (`SAP_READ_ONLY`, `SAP_BLOCK_DATA`, `SAP_BLOCK_FREE_SQL`,
13
+ * `SAP_ENABLE_TRANSPORTS`, `SAP_ENABLE_GIT`) were replaced with positive opt-ins
14
+ * (`SAP_ALLOW_WRITES`, `SAP_ALLOW_DATA_PREVIEW`, `SAP_ALLOW_FREE_SQL`,
15
+ * `SAP_ALLOW_TRANSPORT_WRITES`, `SAP_ALLOW_GIT_WRITES`).
16
+ * - See docs_page/updating.md for the full migration table.
9
17
  */
18
+ import { parseDenyActions, validateDenyActions } from './deny-actions.js';
19
+ import { logger } from './logger.js';
10
20
  import { DEFAULT_CONFIG } from './types.js';
21
+ export const API_KEY_PROFILES = {
22
+ viewer: {
23
+ scopes: ['read'],
24
+ safety: {
25
+ allowWrites: false,
26
+ allowDataPreview: false,
27
+ allowFreeSQL: false,
28
+ allowTransportWrites: false,
29
+ allowGitWrites: false,
30
+ },
31
+ },
32
+ 'viewer-data': {
33
+ scopes: ['read', 'data'],
34
+ safety: {
35
+ allowWrites: false,
36
+ allowDataPreview: true,
37
+ allowFreeSQL: false,
38
+ allowTransportWrites: false,
39
+ allowGitWrites: false,
40
+ },
41
+ },
42
+ 'viewer-sql': {
43
+ scopes: ['read', 'data', 'sql'],
44
+ safety: {
45
+ allowWrites: false,
46
+ allowDataPreview: true,
47
+ allowFreeSQL: true,
48
+ allowTransportWrites: false,
49
+ allowGitWrites: false,
50
+ },
51
+ },
52
+ developer: {
53
+ scopes: ['read', 'write', 'transports', 'git'],
54
+ safety: {
55
+ allowWrites: true,
56
+ allowDataPreview: false,
57
+ allowFreeSQL: false,
58
+ allowTransportWrites: true,
59
+ allowGitWrites: true,
60
+ allowedPackages: ['$TMP'],
61
+ },
62
+ },
63
+ 'developer-data': {
64
+ scopes: ['read', 'write', 'data', 'transports', 'git'],
65
+ safety: {
66
+ allowWrites: true,
67
+ allowDataPreview: true,
68
+ allowFreeSQL: false,
69
+ allowTransportWrites: true,
70
+ allowGitWrites: true,
71
+ allowedPackages: ['$TMP'],
72
+ },
73
+ },
74
+ 'developer-sql': {
75
+ scopes: ['read', 'write', 'data', 'sql', 'transports', 'git'],
76
+ safety: {
77
+ allowWrites: true,
78
+ allowDataPreview: true,
79
+ allowFreeSQL: true,
80
+ allowTransportWrites: true,
81
+ allowGitWrites: true,
82
+ allowedPackages: ['$TMP'],
83
+ },
84
+ },
85
+ admin: {
86
+ scopes: ['read', 'write', 'data', 'sql', 'transports', 'git', 'admin'],
87
+ safety: {
88
+ allowWrites: true,
89
+ allowDataPreview: true,
90
+ allowFreeSQL: true,
91
+ allowTransportWrites: true,
92
+ allowGitWrites: true,
93
+ allowedPackages: [],
94
+ },
95
+ },
96
+ };
11
97
  /**
12
98
  * Parse API keys string into structured array.
13
99
  * Format: "key1:profile1,key2:profile2"
14
- * Each entry maps an API key to a named profile.
15
100
  */
16
101
  export function parseApiKeys(raw) {
17
102
  const entries = [];
@@ -19,20 +104,18 @@ export function parseApiKeys(raw) {
19
104
  const trimmed = pair.trim();
20
105
  if (!trimmed)
21
106
  continue;
22
- // Use LAST colon as separator — keys may contain colons (e.g. base64)
23
- // but profile names never do
24
107
  const colonIdx = trimmed.lastIndexOf(':');
25
108
  if (colonIdx === -1) {
26
109
  throw new Error(`Invalid API key entry '${trimmed}': expected 'key:profile' format. ` +
27
- `Valid profiles: ${Object.keys(PROFILES).join(', ')}`);
110
+ `Valid profiles: ${Object.keys(API_KEY_PROFILES).join(', ')}`);
28
111
  }
29
112
  const key = trimmed.slice(0, colonIdx);
30
113
  const profile = trimmed.slice(colonIdx + 1);
31
114
  if (!key) {
32
115
  throw new Error('Invalid API key entry: key cannot be empty');
33
116
  }
34
- if (!PROFILES[profile]) {
35
- throw new Error(`Invalid profile '${profile}' in API key entry. Valid profiles: ${Object.keys(PROFILES).join(', ')}`);
117
+ if (!API_KEY_PROFILES[profile]) {
118
+ throw new Error(`Invalid profile '${profile}' in API key entry. Valid profiles: ${Object.keys(API_KEY_PROFILES).join(', ')}`);
36
119
  }
37
120
  entries.push({ key, profile });
38
121
  }
@@ -41,118 +124,150 @@ export function parseApiKeys(raw) {
41
124
  }
42
125
  return entries;
43
126
  }
44
- /**
45
- * Maps profile names to the scopes they grant.
46
- * Used when API keys are assigned to profiles — the key inherits these scopes.
47
- * Kept in sync with PROFILES: each profile's safety flags determine its scopes.
48
- */
49
- export const PROFILE_SCOPES = {
50
- viewer: ['read'],
51
- 'viewer-data': ['read', 'data'],
52
- 'viewer-sql': ['read', 'data', 'sql'],
53
- developer: ['read', 'write'],
54
- 'developer-data': ['read', 'write', 'data'],
55
- 'developer-sql': ['read', 'write', 'data', 'sql'],
127
+ /** Map of legacy env-var names → human-readable migration hint. */
128
+ const LEGACY_ENV_VARS = {
129
+ SAP_READ_ONLY: 'Replaced by SAP_ALLOW_WRITES (inverted). Set SAP_ALLOW_WRITES=true to enable writes.',
130
+ SAP_BLOCK_DATA: 'Replaced by SAP_ALLOW_DATA_PREVIEW (inverted). Set SAP_ALLOW_DATA_PREVIEW=true to enable table preview.',
131
+ SAP_BLOCK_FREE_SQL: 'Replaced by SAP_ALLOW_FREE_SQL (inverted). Set SAP_ALLOW_FREE_SQL=true to enable freestyle SQL.',
132
+ SAP_ENABLE_TRANSPORTS: 'Replaced by SAP_ALLOW_TRANSPORT_WRITES. Transport reads are always available; writes need SAP_ALLOW_TRANSPORT_WRITES=true + SAP_ALLOW_WRITES=true.',
133
+ SAP_ENABLE_GIT: 'Replaced by SAP_ALLOW_GIT_WRITES. Git reads are always available; writes need SAP_ALLOW_GIT_WRITES=true + SAP_ALLOW_WRITES=true.',
134
+ SAP_ALLOWED_OPS: 'Op-code allowlist was removed. Use SAP_DENY_ACTIONS for fine-grained per-action denials (e.g., SAP_DENY_ACTIONS="SAPWrite.delete,SAPManage.flp_*").',
135
+ SAP_DISALLOWED_OPS: 'Op-code blocklist was removed. Use SAP_DENY_ACTIONS instead.',
136
+ ARC1_PROFILE: 'Server-side profile presets were removed. Set individual SAP_ALLOW_* flags (see .env.example for recipes).',
137
+ ARC1_API_KEY: 'Single API-key mode was removed. Use ARC1_API_KEYS="key:profile" with a profile name (valid: viewer, viewer-data, viewer-sql, developer, developer-data, developer-sql, admin).',
56
138
  };
57
- /**
58
- * Named profiles — convenience presets for common safety configurations.
59
- * Each profile sets a combination of safety flags. Individual CLI flags
60
- * applied after the profile can override any profile default.
61
- */
62
- export const PROFILES = {
63
- viewer: {
64
- readOnly: true,
65
- blockData: true,
66
- blockFreeSQL: true,
67
- enableTransports: false,
68
- },
69
- 'viewer-data': {
70
- readOnly: true,
71
- blockData: false,
72
- blockFreeSQL: true,
73
- enableTransports: false,
74
- },
75
- 'viewer-sql': {
76
- readOnly: true,
77
- blockData: false,
78
- blockFreeSQL: false,
79
- enableTransports: false,
80
- },
81
- developer: {
82
- readOnly: false,
83
- blockData: true,
84
- blockFreeSQL: true,
85
- enableTransports: true,
86
- allowedPackages: ['$TMP'],
87
- },
88
- 'developer-data': {
89
- readOnly: false,
90
- blockData: false,
91
- blockFreeSQL: true,
92
- enableTransports: true,
93
- allowedPackages: ['$TMP'],
94
- },
95
- 'developer-sql': {
96
- readOnly: false,
97
- blockData: false,
98
- blockFreeSQL: false,
99
- enableTransports: true,
100
- allowedPackages: ['$TMP'],
101
- },
139
+ const LEGACY_CLI_FLAGS = {
140
+ 'read-only': LEGACY_ENV_VARS.SAP_READ_ONLY,
141
+ 'block-data': LEGACY_ENV_VARS.SAP_BLOCK_DATA,
142
+ 'block-free-sql': LEGACY_ENV_VARS.SAP_BLOCK_FREE_SQL,
143
+ 'enable-transports': LEGACY_ENV_VARS.SAP_ENABLE_TRANSPORTS,
144
+ 'enable-git': LEGACY_ENV_VARS.SAP_ENABLE_GIT,
145
+ 'allowed-ops': LEGACY_ENV_VARS.SAP_ALLOWED_OPS,
146
+ 'disallowed-ops': LEGACY_ENV_VARS.SAP_DISALLOWED_OPS,
147
+ profile: LEGACY_ENV_VARS.ARC1_PROFILE,
148
+ 'api-key': LEGACY_ENV_VARS.ARC1_API_KEY,
102
149
  };
150
+ /** Migration guard — throws a helpful error if any legacy identifier is set. */
151
+ function detectLegacyConfig(args) {
152
+ const violations = [];
153
+ for (const env of Object.keys(LEGACY_ENV_VARS)) {
154
+ if (process.env[env] !== undefined) {
155
+ violations.push(` ${env}: ${LEGACY_ENV_VARS[env]}`);
156
+ }
157
+ }
158
+ for (const flag of Object.keys(LEGACY_CLI_FLAGS)) {
159
+ if (args.some((a) => a === `--${flag}` || a.startsWith(`--${flag}=`))) {
160
+ violations.push(` --${flag}: ${LEGACY_CLI_FLAGS[flag]}`);
161
+ }
162
+ }
163
+ if (violations.length > 0) {
164
+ throw new Error(`Legacy authorization config detected (removed in v0.7):\n${violations.join('\n')}\n\nSee docs_page/updating.md#v07-authorization-refactor-breaking-change for the full migration guide.`);
165
+ }
166
+ }
103
167
  /**
104
- * Parse CLI arguments and environment variables into a ServerConfig.
105
- *
106
- * We use a simple hand-rolled parser here (not commander) because
107
- * the MCP server entry point needs to be fast and lightweight.
108
- * Commander is used for the full CLI (cli.ts), not the server startup.
168
+ * Parse CLI args + env into a `{ config, sources }` pair.
169
+ * `sources` records where each field's value came from (default / env / flag / file).
170
+ * Consumed by the startup effective-policy log and the `arc1 config show` subcommand.
109
171
  */
110
- export function parseArgs(args) {
172
+ export function resolveConfig(args) {
173
+ detectLegacyConfig(args);
111
174
  const config = { ...DEFAULT_CONFIG };
112
- // Helper: get a CLI flag value (--flag value or --flag=value)
175
+ const sources = {};
176
+ // ── Resolvers ──────────────────────────────────────────────────────
113
177
  const getFlag = (name) => {
114
178
  const prefix = `--${name}=`;
115
179
  for (let i = 0; i < args.length; i++) {
116
- if (args[i] === `--${name}` && i + 1 < args.length) {
180
+ if (args[i] === `--${name}` && i + 1 < args.length)
117
181
  return args[i + 1];
118
- }
119
- if (args[i]?.startsWith(prefix)) {
182
+ if (args[i]?.startsWith(prefix))
120
183
  return args[i].slice(prefix.length);
121
- }
122
184
  }
123
185
  return undefined;
124
186
  };
125
- // Helper: resolve value from CLI > env > default
126
- const resolve = (flag, envVar, defaultVal) => {
127
- return getFlag(flag) ?? process.env[envVar] ?? defaultVal;
187
+ const resolveStr = (flag, envVar, defaultVal, fieldName) => {
188
+ const flagVal = getFlag(flag);
189
+ if (flagVal !== undefined) {
190
+ sources[fieldName] = { flag: `--${flag}` };
191
+ return flagVal;
192
+ }
193
+ if (process.env[envVar] !== undefined) {
194
+ sources[fieldName] = { env: envVar };
195
+ return process.env[envVar];
196
+ }
197
+ sources[fieldName] = 'default';
198
+ return defaultVal;
128
199
  };
129
- const resolveBool = (flag, envVar, defaultVal) => {
130
- const val = getFlag(flag) ?? process.env[envVar];
131
- if (val === undefined)
132
- return defaultVal;
133
- return val === 'true' || val === '1';
200
+ const resolveBool = (flag, envVar, defaultVal, fieldName) => {
201
+ const flagVal = getFlag(flag);
202
+ if (flagVal !== undefined) {
203
+ sources[fieldName] = { flag: `--${flag}` };
204
+ return flagVal === 'true' || flagVal === '1';
205
+ }
206
+ if (process.env[envVar] !== undefined) {
207
+ sources[fieldName] = { env: envVar };
208
+ return process.env[envVar] === 'true' || process.env[envVar] === '1';
209
+ }
210
+ sources[fieldName] = 'default';
211
+ return defaultVal;
134
212
  };
135
- const resolveFeature = (flag, envVar) => {
136
- const val = getFlag(flag) ?? process.env[envVar] ?? 'auto';
137
- if (val === 'on' || val === 'off')
138
- return val;
213
+ const resolveFeature = (flag, envVar, fieldName) => {
214
+ const flagVal = getFlag(flag);
215
+ if (flagVal !== undefined) {
216
+ sources[fieldName] = { flag: `--${flag}` };
217
+ if (flagVal === 'on' || flagVal === 'off')
218
+ return flagVal;
219
+ return 'auto';
220
+ }
221
+ const envVal = process.env[envVar];
222
+ if (envVal !== undefined) {
223
+ sources[fieldName] = { env: envVar };
224
+ if (envVal === 'on' || envVal === 'off')
225
+ return envVal;
226
+ return 'auto';
227
+ }
228
+ sources[fieldName] = 'default';
139
229
  return 'auto';
140
230
  };
141
- // --- SAP Connection ---
142
- config.url = resolve('url', 'SAP_URL', '');
143
- config.username = resolve('user', 'SAP_USER', '');
144
- config.password = resolve('password', 'SAP_PASSWORD', '');
145
- config.client = resolve('client', 'SAP_CLIENT', '100');
146
- config.language = resolve('language', 'SAP_LANGUAGE', 'EN');
147
- config.insecure = resolveBool('insecure', 'SAP_INSECURE', false);
148
- // --- Cookie Auth ---
149
- config.cookieFile = getFlag('cookie-file') ?? process.env.SAP_COOKIE_FILE;
150
- config.cookieString = getFlag('cookie-string') ?? process.env.SAP_COOKIE_STRING;
151
- // --- Transport ---
152
- const transport = resolve('transport', 'SAP_TRANSPORT', 'stdio');
231
+ const resolveOptionalStr = (flag, envVar, fieldName) => {
232
+ const flagVal = getFlag(flag);
233
+ if (flagVal !== undefined) {
234
+ sources[fieldName] = { flag: `--${flag}` };
235
+ return flagVal;
236
+ }
237
+ if (process.env[envVar] !== undefined) {
238
+ sources[fieldName] = { env: envVar };
239
+ return process.env[envVar];
240
+ }
241
+ sources[fieldName] = 'default';
242
+ return undefined;
243
+ };
244
+ // ── SAP Connection ─────────────────────────────────────────────────
245
+ config.url = resolveStr('url', 'SAP_URL', '', 'url');
246
+ config.username = resolveStr('user', 'SAP_USER', '', 'username');
247
+ config.password = resolveStr('password', 'SAP_PASSWORD', '', 'password');
248
+ config.client = resolveStr('client', 'SAP_CLIENT', '100', 'client');
249
+ config.language = resolveStr('language', 'SAP_LANGUAGE', 'EN', 'language');
250
+ config.insecure = resolveBool('insecure', 'SAP_INSECURE', false, 'insecure');
251
+ // ── Cookie Auth ────────────────────────────────────────────────────
252
+ config.cookieFile = resolveOptionalStr('cookie-file', 'SAP_COOKIE_FILE', 'cookieFile');
253
+ config.cookieString = resolveOptionalStr('cookie-string', 'SAP_COOKIE_STRING', 'cookieString');
254
+ // ── Transport ──────────────────────────────────────────────────────
255
+ const transport = resolveStr('transport', 'SAP_TRANSPORT', 'stdio', 'transport');
153
256
  config.transport = (transport === 'http-streamable' ? 'http-streamable' : 'stdio');
154
- config.httpAddr = resolve('http-addr', 'SAP_HTTP_ADDR', '0.0.0.0:8080');
155
- // --port / ARC1_PORT overrides just the port part of httpAddr (simpler alternative to --http-addr)
257
+ const httpAddrFlag = getFlag('http-addr');
258
+ const httpAddrEnv = process.env.ARC1_HTTP_ADDR ?? process.env.SAP_HTTP_ADDR;
259
+ if (httpAddrFlag !== undefined) {
260
+ config.httpAddr = httpAddrFlag;
261
+ sources.httpAddr = { flag: '--http-addr' };
262
+ }
263
+ else if (httpAddrEnv !== undefined) {
264
+ config.httpAddr = httpAddrEnv;
265
+ sources.httpAddr = process.env.ARC1_HTTP_ADDR !== undefined ? { env: 'ARC1_HTTP_ADDR' } : { env: 'SAP_HTTP_ADDR' };
266
+ }
267
+ else {
268
+ config.httpAddr = '0.0.0.0:8080';
269
+ sources.httpAddr = 'default';
270
+ }
156
271
  const portOverride = getFlag('port') ?? process.env.ARC1_PORT;
157
272
  if (portOverride) {
158
273
  const parsedPort = Number.parseInt(portOverride, 10);
@@ -161,121 +276,147 @@ export function parseArgs(args) {
161
276
  }
162
277
  const addrHost = config.httpAddr.includes(':') ? config.httpAddr.split(':')[0] : '0.0.0.0';
163
278
  config.httpAddr = `${addrHost}:${parsedPort}`;
279
+ sources.httpAddr = getFlag('port') !== undefined ? { flag: '--port' } : { env: 'ARC1_PORT' };
164
280
  }
165
- // --- Profile (apply before individual safety flags so flags can override) ---
166
- const profileName = getFlag('profile') ?? process.env.ARC1_PROFILE;
167
- if (profileName) {
168
- const profile = PROFILES[profileName];
169
- if (!profile) {
170
- throw new Error(`Unknown profile '${profileName}'. Valid profiles: ${Object.keys(PROFILES).join(', ')}`);
281
+ // ── Safety (positive opt-ins) ──────────────────────────────────────
282
+ config.allowWrites = resolveBool('allow-writes', 'SAP_ALLOW_WRITES', false, 'allowWrites');
283
+ config.allowDataPreview = resolveBool('allow-data-preview', 'SAP_ALLOW_DATA_PREVIEW', false, 'allowDataPreview');
284
+ config.allowFreeSQL = resolveBool('allow-free-sql', 'SAP_ALLOW_FREE_SQL', false, 'allowFreeSQL');
285
+ config.allowTransportWrites = resolveBool('allow-transport-writes', 'SAP_ALLOW_TRANSPORT_WRITES', false, 'allowTransportWrites');
286
+ config.allowGitWrites = resolveBool('allow-git-writes', 'SAP_ALLOW_GIT_WRITES', false, 'allowGitWrites');
287
+ const pkgs = getFlag('allowed-packages') ?? process.env.SAP_ALLOWED_PACKAGES;
288
+ if (pkgs !== undefined) {
289
+ const raw = pkgs.split(',').map((p) => p.trim());
290
+ const filtered = raw.filter((p) => p.length > 0);
291
+ if (raw.length !== filtered.length) {
292
+ logger.warn("SAP_ALLOWED_PACKAGES contained empty entries — likely shell expansion of unset $VARs. Use single quotes: SAP_ALLOWED_PACKAGES='$TMP,Z*'", { raw: pkgs, parsed: filtered });
171
293
  }
172
- Object.assign(config, profile);
294
+ config.allowedPackages = filtered;
295
+ sources.allowedPackages =
296
+ getFlag('allowed-packages') !== undefined ? { flag: '--allowed-packages' } : { env: 'SAP_ALLOWED_PACKAGES' };
173
297
  }
174
- // --- Safety (individual flags override profile defaults) ---
175
- // Only override profile defaults when the flag/env is explicitly set
176
- const readOnlyExplicit = getFlag('read-only') ?? process.env.SAP_READ_ONLY;
177
- if (readOnlyExplicit !== undefined)
178
- config.readOnly = readOnlyExplicit === 'true' || readOnlyExplicit === '1';
179
- else if (!profileName)
180
- config.readOnly = true;
181
- const blockFreeSQLExplicit = getFlag('block-free-sql') ?? process.env.SAP_BLOCK_FREE_SQL;
182
- if (blockFreeSQLExplicit !== undefined)
183
- config.blockFreeSQL = blockFreeSQLExplicit === 'true' || blockFreeSQLExplicit === '1';
184
- else if (!profileName)
185
- config.blockFreeSQL = true;
186
- const blockDataExplicit = getFlag('block-data') ?? process.env.SAP_BLOCK_DATA;
187
- if (blockDataExplicit !== undefined)
188
- config.blockData = blockDataExplicit === 'true' || blockDataExplicit === '1';
189
- else if (!profileName)
190
- config.blockData = true;
191
- config.allowedOps = resolve('allowed-ops', 'SAP_ALLOWED_OPS', '');
192
- config.disallowedOps = resolve('disallowed-ops', 'SAP_DISALLOWED_OPS', '');
193
- const pkgs = getFlag('allowed-packages') ?? process.env.SAP_ALLOWED_PACKAGES;
194
- if (pkgs)
195
- config.allowedPackages = pkgs.split(',').map((p) => p.trim());
196
- const enableTransportsExplicit = getFlag('enable-transports') ?? process.env.SAP_ENABLE_TRANSPORTS;
197
- if (enableTransportsExplicit !== undefined)
198
- config.enableTransports = enableTransportsExplicit === 'true' || enableTransportsExplicit === '1';
199
- else if (!profileName)
200
- config.enableTransports = false;
201
- // --- Features ---
202
- config.featureAbapGit = resolveFeature('feature-abapgit', 'SAP_FEATURE_ABAPGIT');
203
- config.featureRap = resolveFeature('feature-rap', 'SAP_FEATURE_RAP');
204
- config.featureAmdp = resolveFeature('feature-amdp', 'SAP_FEATURE_AMDP');
205
- config.featureUi5 = resolveFeature('feature-ui5', 'SAP_FEATURE_UI5');
206
- config.featureTransport = resolveFeature('feature-transport', 'SAP_FEATURE_TRANSPORT');
207
- config.featureHana = resolveFeature('feature-hana', 'SAP_FEATURE_HANA');
208
- config.featureUi5Repo = resolveFeature('feature-ui5repo', 'SAP_FEATURE_UI5REPO');
209
- config.featureFlp = resolveFeature('feature-flp', 'SAP_FEATURE_FLP');
210
- // --- System Type Detection ---
211
- const systemType = resolve('system-type', 'SAP_SYSTEM_TYPE', 'auto');
298
+ else {
299
+ sources.allowedPackages = 'default';
300
+ }
301
+ const transports = getFlag('allowed-transports') ?? process.env.SAP_ALLOWED_TRANSPORTS;
302
+ if (transports !== undefined) {
303
+ config.allowedTransports = transports
304
+ .split(',')
305
+ .map((t) => t.trim())
306
+ .filter((t) => t.length > 0);
307
+ sources.allowedTransports =
308
+ getFlag('allowed-transports') !== undefined
309
+ ? { flag: '--allowed-transports' }
310
+ : { env: 'SAP_ALLOWED_TRANSPORTS' };
311
+ }
312
+ else {
313
+ sources.allowedTransports = 'default';
314
+ }
315
+ // ── Deny Actions (parsed + validated; fails fast on error) ─────────
316
+ const denyActionsRaw = getFlag('deny-actions') ?? process.env.SAP_DENY_ACTIONS;
317
+ if (denyActionsRaw) {
318
+ const fromFile = denyActionsRaw.startsWith('/') ||
319
+ denyActionsRaw.startsWith('./') ||
320
+ denyActionsRaw.startsWith('~/') ||
321
+ denyActionsRaw.startsWith('../');
322
+ const parsed = parseDenyActions(denyActionsRaw);
323
+ validateDenyActions(parsed);
324
+ config.denyActions = parsed;
325
+ sources.denyActions = fromFile
326
+ ? { file: denyActionsRaw.replace(/^~/, process.env.HOME ?? '~') }
327
+ : getFlag('deny-actions') !== undefined
328
+ ? { flag: '--deny-actions' }
329
+ : { env: 'SAP_DENY_ACTIONS' };
330
+ }
331
+ else {
332
+ sources.denyActions = 'default';
333
+ }
334
+ // ── Features ───────────────────────────────────────────────────────
335
+ config.featureAbapGit = resolveFeature('feature-abapgit', 'SAP_FEATURE_ABAPGIT', 'featureAbapGit');
336
+ config.featureGcts = resolveFeature('feature-gcts', 'SAP_FEATURE_GCTS', 'featureGcts');
337
+ config.featureRap = resolveFeature('feature-rap', 'SAP_FEATURE_RAP', 'featureRap');
338
+ config.featureAmdp = resolveFeature('feature-amdp', 'SAP_FEATURE_AMDP', 'featureAmdp');
339
+ config.featureUi5 = resolveFeature('feature-ui5', 'SAP_FEATURE_UI5', 'featureUi5');
340
+ config.featureTransport = resolveFeature('feature-transport', 'SAP_FEATURE_TRANSPORT', 'featureTransport');
341
+ config.featureHana = resolveFeature('feature-hana', 'SAP_FEATURE_HANA', 'featureHana');
342
+ config.featureUi5Repo = resolveFeature('feature-ui5repo', 'SAP_FEATURE_UI5REPO', 'featureUi5Repo');
343
+ config.featureFlp = resolveFeature('feature-flp', 'SAP_FEATURE_FLP', 'featureFlp');
344
+ // ── System Type Detection ──────────────────────────────────────────
345
+ const systemType = resolveStr('system-type', 'SAP_SYSTEM_TYPE', 'auto', 'systemType');
212
346
  config.systemType = (['btp', 'onprem'].includes(systemType) ? systemType : 'auto');
213
- // --- Authentication (MCP client → ARC-1) ---
214
- config.apiKey = getFlag('api-key') ?? process.env.ARC1_API_KEY;
215
- // Multiple API keys with per-key profiles: "key1:viewer,key2:developer"
347
+ // ── Authentication ─────────────────────────────────────────────────
216
348
  const apiKeysRaw = getFlag('api-keys') ?? process.env.ARC1_API_KEYS;
217
349
  if (apiKeysRaw) {
218
350
  config.apiKeys = parseApiKeys(apiKeysRaw);
351
+ sources.apiKeys = getFlag('api-keys') !== undefined ? { flag: '--api-keys' } : { env: 'ARC1_API_KEYS' };
219
352
  }
220
- config.oidcIssuer = getFlag('oidc-issuer') ?? process.env.SAP_OIDC_ISSUER;
221
- config.oidcAudience = getFlag('oidc-audience') ?? process.env.SAP_OIDC_AUDIENCE;
353
+ else {
354
+ sources.apiKeys = 'default';
355
+ }
356
+ config.oidcIssuer = resolveOptionalStr('oidc-issuer', 'SAP_OIDC_ISSUER', 'oidcIssuer');
357
+ config.oidcAudience = resolveOptionalStr('oidc-audience', 'SAP_OIDC_AUDIENCE', 'oidcAudience');
222
358
  const clockTolerance = getFlag('oidc-clock-tolerance') ?? process.env.SAP_OIDC_CLOCK_TOLERANCE;
223
359
  if (clockTolerance) {
224
360
  const parsed = Number.parseInt(clockTolerance, 10);
225
361
  config.oidcClockTolerance = Number.isNaN(parsed) ? undefined : parsed;
226
362
  }
227
- config.xsuaaAuth = resolveBool('xsuaa-auth', 'SAP_XSUAA_AUTH', false);
228
- // --- BTP ABAP Environment (direct connection via service key) ---
229
- config.btpServiceKey = getFlag('btp-service-key') ?? process.env.SAP_BTP_SERVICE_KEY;
230
- config.btpServiceKeyFile = getFlag('btp-service-key-file') ?? process.env.SAP_BTP_SERVICE_KEY_FILE;
231
- const cbPort = resolve('btp-oauth-callback-port', 'SAP_BTP_OAUTH_CALLBACK_PORT', '0');
363
+ config.xsuaaAuth = resolveBool('xsuaa-auth', 'SAP_XSUAA_AUTH', false, 'xsuaaAuth');
364
+ // ── BTP ABAP Environment ───────────────────────────────────────────
365
+ config.btpServiceKey = resolveOptionalStr('btp-service-key', 'SAP_BTP_SERVICE_KEY', 'btpServiceKey');
366
+ config.btpServiceKeyFile = resolveOptionalStr('btp-service-key-file', 'SAP_BTP_SERVICE_KEY_FILE', 'btpServiceKeyFile');
367
+ const cbPort = resolveStr('btp-oauth-callback-port', 'SAP_BTP_OAUTH_CALLBACK_PORT', '0', 'btpOAuthCallbackPort');
232
368
  config.btpOAuthCallbackPort = Number.parseInt(cbPort, 10) || 0;
233
- // --- Principal Propagation ---
234
- config.ppEnabled = resolveBool('pp-enabled', 'SAP_PP_ENABLED', false);
235
- config.ppStrict = resolveBool('pp-strict', 'SAP_PP_STRICT', false);
236
- config.ppAllowSharedCookies = resolveBool('pp-allow-shared-cookies', 'SAP_PP_ALLOW_SHARED_COOKIES', false);
237
- // --- SAML Behavior ---
238
- config.disableSaml2 = resolveBool('disable-saml', 'SAP_DISABLE_SAML', false);
239
- // --- Tool Mode ---
240
- const toolMode = resolve('tool-mode', 'ARC1_TOOL_MODE', 'standard');
369
+ // ── Principal Propagation ──────────────────────────────────────────
370
+ config.ppEnabled = resolveBool('pp-enabled', 'SAP_PP_ENABLED', false, 'ppEnabled');
371
+ config.ppStrict = resolveBool('pp-strict', 'SAP_PP_STRICT', false, 'ppStrict');
372
+ config.ppAllowSharedCookies = resolveBool('pp-allow-shared-cookies', 'SAP_PP_ALLOW_SHARED_COOKIES', false, 'ppAllowSharedCookies');
373
+ // ── SAML Behavior ──────────────────────────────────────────────────
374
+ config.disableSaml2 = resolveBool('disable-saml', 'SAP_DISABLE_SAML', false, 'disableSaml2');
375
+ // ── Tool Mode ──────────────────────────────────────────────────────
376
+ const toolMode = resolveStr('tool-mode', 'ARC1_TOOL_MODE', 'standard', 'toolMode');
241
377
  config.toolMode = (toolMode === 'hyperfocused' ? 'hyperfocused' : 'standard');
242
- // --- Lint ---
243
- config.abaplintConfig = getFlag('abaplint-config') ?? process.env.SAP_ABAPLINT_CONFIG;
244
- config.lintBeforeWrite = resolveBool('lint-before-write', 'SAP_LINT_BEFORE_WRITE', true);
245
- // --- Cache ---
246
- const cacheMode = resolve('cache', 'ARC1_CACHE', 'auto');
378
+ // ── Lint ───────────────────────────────────────────────────────────
379
+ config.abaplintConfig = resolveOptionalStr('abaplint-config', 'SAP_ABAPLINT_CONFIG', 'abaplintConfig');
380
+ config.lintBeforeWrite = resolveBool('lint-before-write', 'SAP_LINT_BEFORE_WRITE', true, 'lintBeforeWrite');
381
+ config.checkBeforeWrite = resolveBool('check-before-write', 'SAP_CHECK_BEFORE_WRITE', false, 'checkBeforeWrite');
382
+ // ── Cache ──────────────────────────────────────────────────────────
383
+ const cacheMode = resolveStr('cache', 'ARC1_CACHE', 'auto', 'cacheMode');
247
384
  config.cacheMode = (['memory', 'sqlite', 'none'].includes(cacheMode) ? cacheMode : 'auto');
248
- config.cacheFile = resolve('cache-file', 'ARC1_CACHE_FILE', '.arc1-cache.db');
249
- config.cacheWarmup = resolveBool('cache-warmup', 'ARC1_CACHE_WARMUP', false);
250
- config.cacheWarmupPackages = resolve('cache-warmup-packages', 'ARC1_CACHE_WARMUP_PACKAGES', '');
251
- // --- Concurrency ---
385
+ config.cacheFile = resolveStr('cache-file', 'ARC1_CACHE_FILE', '.arc1-cache.db', 'cacheFile');
386
+ config.cacheWarmup = resolveBool('cache-warmup', 'ARC1_CACHE_WARMUP', false, 'cacheWarmup');
387
+ config.cacheWarmupPackages = resolveStr('cache-warmup-packages', 'ARC1_CACHE_WARMUP_PACKAGES', '', 'cacheWarmupPackages');
388
+ // ── Concurrency ────────────────────────────────────────────────────
252
389
  const maxConcurrent = getFlag('max-concurrent') ?? process.env.ARC1_MAX_CONCURRENT;
253
390
  if (maxConcurrent) {
254
391
  const parsed = Number.parseInt(maxConcurrent, 10);
255
392
  config.maxConcurrent = Number.isNaN(parsed) || parsed < 1 ? 1 : parsed;
256
393
  }
257
- // --- Logging ---
258
- config.logFile = getFlag('log-file') ?? process.env.ARC1_LOG_FILE;
259
- const logLevel = resolve('log-level', 'ARC1_LOG_LEVEL', 'info');
394
+ // ── Logging ────────────────────────────────────────────────────────
395
+ config.logFile = resolveOptionalStr('log-file', 'ARC1_LOG_FILE', 'logFile');
396
+ const logLevel = resolveStr('log-level', 'ARC1_LOG_LEVEL', 'info', 'logLevel');
260
397
  config.logLevel = (['debug', 'info', 'warn', 'error'].includes(logLevel) ? logLevel : 'info');
261
- const logFormat = resolve('log-format', 'ARC1_LOG_FORMAT', 'text');
398
+ const logFormat = resolveStr('log-format', 'ARC1_LOG_FORMAT', 'text', 'logFormat');
262
399
  config.logFormat = (logFormat === 'json' ? 'json' : 'text');
263
- // --- Misc ---
264
- config.verbose = resolveBool('verbose', 'SAP_VERBOSE', false);
265
- // --verbose is sugar for --log-level debug
266
- if (config.verbose) {
400
+ // ── Misc ───────────────────────────────────────────────────────────
401
+ config.verbose = resolveBool('verbose', 'SAP_VERBOSE', false, 'verbose');
402
+ if (config.verbose)
267
403
  config.logLevel = 'debug';
268
- }
269
- // --- Startup Validation ---
404
+ // ── Startup Validation ─────────────────────────────────────────────
270
405
  validateConfig(config);
271
- return config;
406
+ return { config, sources };
407
+ }
408
+ /**
409
+ * Thin wrapper around `resolveConfig` that returns only the config object.
410
+ * Kept for callers that don't need per-field source attribution.
411
+ */
412
+ export function parseArgs(args) {
413
+ return resolveConfig(args).config;
272
414
  }
273
415
  /**
274
416
  * Validate configuration for internally consistent auth settings.
275
417
  * Fails fast at startup for invalid or dangerous config combinations.
276
418
  */
277
419
  export function validateConfig(config) {
278
- // OIDC: audience is required when issuer is set (RFC 9700 §2.3 audience restriction)
279
420
  if (config.oidcIssuer && !config.oidcAudience) {
280
421
  throw new Error('SAP_OIDC_AUDIENCE is required when SAP_OIDC_ISSUER is set — ' +
281
422
  'audience validation prevents token confusion across services (RFC 9700 §2.3)');
@@ -283,7 +424,6 @@ export function validateConfig(config) {
283
424
  if (config.oidcAudience && !config.oidcIssuer) {
284
425
  throw new Error('SAP_OIDC_ISSUER is required when SAP_OIDC_AUDIENCE is set');
285
426
  }
286
- // PP: ppStrict requires ppEnabled
287
427
  if (config.ppStrict && !config.ppEnabled) {
288
428
  throw new Error('SAP_PP_STRICT=true requires SAP_PP_ENABLED=true — strict mode has no effect without principal propagation enabled');
289
429
  }