arc-1 0.6.10 → 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 (115) hide show
  1. package/README.md +8 -7
  2. package/bin/arc1-cli.js +10 -0
  3. package/bin/arc1.js +1 -1
  4. package/dist/adt/cds-impact.d.ts +35 -0
  5. package/dist/adt/cds-impact.d.ts.map +1 -1
  6. package/dist/adt/cds-impact.js +71 -0
  7. package/dist/adt/cds-impact.js.map +1 -1
  8. package/dist/adt/client.d.ts +4 -1
  9. package/dist/adt/client.d.ts.map +1 -1
  10. package/dist/adt/client.js +18 -5
  11. package/dist/adt/client.js.map +1 -1
  12. package/dist/adt/crud.d.ts.map +1 -1
  13. package/dist/adt/crud.js +32 -5
  14. package/dist/adt/crud.js.map +1 -1
  15. package/dist/adt/devtools.d.ts +39 -3
  16. package/dist/adt/devtools.d.ts.map +1 -1
  17. package/dist/adt/devtools.js +237 -25
  18. package/dist/adt/devtools.js.map +1 -1
  19. package/dist/adt/diagnostics.d.ts +69 -7
  20. package/dist/adt/diagnostics.d.ts.map +1 -1
  21. package/dist/adt/diagnostics.js +694 -36
  22. package/dist/adt/diagnostics.js.map +1 -1
  23. package/dist/adt/errors.d.ts +14 -1
  24. package/dist/adt/errors.d.ts.map +1 -1
  25. package/dist/adt/errors.js +40 -9
  26. package/dist/adt/errors.js.map +1 -1
  27. package/dist/adt/http.d.ts.map +1 -1
  28. package/dist/adt/http.js +86 -1
  29. package/dist/adt/http.js.map +1 -1
  30. package/dist/adt/rap-handlers.d.ts +165 -0
  31. package/dist/adt/rap-handlers.d.ts.map +1 -0
  32. package/dist/adt/rap-handlers.js +835 -0
  33. package/dist/adt/rap-handlers.js.map +1 -0
  34. package/dist/adt/rap-preflight.d.ts +43 -0
  35. package/dist/adt/rap-preflight.d.ts.map +1 -0
  36. package/dist/adt/rap-preflight.js +405 -0
  37. package/dist/adt/rap-preflight.js.map +1 -0
  38. package/dist/adt/safety.d.ts +60 -36
  39. package/dist/adt/safety.d.ts.map +1 -1
  40. package/dist/adt/safety.js +202 -120
  41. package/dist/adt/safety.js.map +1 -1
  42. package/dist/adt/transport.d.ts +1 -1
  43. package/dist/adt/transport.js +2 -2
  44. package/dist/adt/transport.js.map +1 -1
  45. package/dist/adt/types.d.ts +88 -0
  46. package/dist/adt/types.d.ts.map +1 -1
  47. package/dist/adt/xml-parser.d.ts +13 -1
  48. package/dist/adt/xml-parser.d.ts.map +1 -1
  49. package/dist/adt/xml-parser.js +26 -15
  50. package/dist/adt/xml-parser.js.map +1 -1
  51. package/dist/authz/policy.d.ts +53 -0
  52. package/dist/authz/policy.d.ts.map +1 -0
  53. package/dist/authz/policy.js +199 -0
  54. package/dist/authz/policy.js.map +1 -0
  55. package/dist/cli-args.d.ts +14 -0
  56. package/dist/cli-args.d.ts.map +1 -0
  57. package/dist/cli-args.js +62 -0
  58. package/dist/cli-args.js.map +1 -0
  59. package/dist/cli.d.ts +13 -7
  60. package/dist/cli.d.ts.map +1 -1
  61. package/dist/cli.js +252 -55
  62. package/dist/cli.js.map +1 -1
  63. package/dist/extract-sap-cookies.d.ts +24 -0
  64. package/dist/extract-sap-cookies.d.ts.map +1 -0
  65. package/dist/extract-sap-cookies.js +317 -0
  66. package/dist/extract-sap-cookies.js.map +1 -0
  67. package/dist/handlers/hyperfocused.d.ts +4 -3
  68. package/dist/handlers/hyperfocused.d.ts.map +1 -1
  69. package/dist/handlers/hyperfocused.js +25 -16
  70. package/dist/handlers/hyperfocused.js.map +1 -1
  71. package/dist/handlers/intent.d.ts +4 -12
  72. package/dist/handlers/intent.d.ts.map +1 -1
  73. package/dist/handlers/intent.js +1238 -114
  74. package/dist/handlers/intent.js.map +1 -1
  75. package/dist/handlers/schemas.d.ts +38 -10
  76. package/dist/handlers/schemas.d.ts.map +1 -1
  77. package/dist/handlers/schemas.js +69 -4
  78. package/dist/handlers/schemas.js.map +1 -1
  79. package/dist/handlers/tools.d.ts.map +1 -1
  80. package/dist/handlers/tools.js +251 -164
  81. package/dist/handlers/tools.js.map +1 -1
  82. package/dist/index.d.ts +1 -1
  83. package/dist/index.js +7 -6
  84. package/dist/index.js.map +1 -1
  85. package/dist/server/audit.d.ts +26 -3
  86. package/dist/server/audit.d.ts.map +1 -1
  87. package/dist/server/audit.js.map +1 -1
  88. package/dist/server/config.d.ts +34 -19
  89. package/dist/server/config.d.ts.map +1 -1
  90. package/dist/server/config.js +320 -193
  91. package/dist/server/config.js.map +1 -1
  92. package/dist/server/deny-actions.d.ts +31 -0
  93. package/dist/server/deny-actions.d.ts.map +1 -0
  94. package/dist/server/deny-actions.js +156 -0
  95. package/dist/server/deny-actions.js.map +1 -0
  96. package/dist/server/effective-policy-log.d.ts +27 -0
  97. package/dist/server/effective-policy-log.d.ts.map +1 -0
  98. package/dist/server/effective-policy-log.js +103 -0
  99. package/dist/server/effective-policy-log.js.map +1 -0
  100. package/dist/server/http.d.ts.map +1 -1
  101. package/dist/server/http.js +15 -16
  102. package/dist/server/http.js.map +1 -1
  103. package/dist/server/server.d.ts +37 -3
  104. package/dist/server/server.d.ts.map +1 -1
  105. package/dist/server/server.js +231 -30
  106. package/dist/server/server.js.map +1 -1
  107. package/dist/server/types.d.ts +29 -13
  108. package/dist/server/types.d.ts.map +1 -1
  109. package/dist/server/types.js +10 -11
  110. package/dist/server/types.js.map +1 -1
  111. package/dist/server/xsuaa.d.ts +1 -2
  112. package/dist/server/xsuaa.d.ts.map +1 -1
  113. package/dist/server/xsuaa.js +13 -14
  114. package/dist/server/xsuaa.js.map +1 -1
  115. package/package.json +6 -3
@@ -4,15 +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';
10
19
  import { logger } from './logger.js';
11
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
+ };
12
97
  /**
13
98
  * Parse API keys string into structured array.
14
99
  * Format: "key1:profile1,key2:profile2"
15
- * Each entry maps an API key to a named profile.
16
100
  */
17
101
  export function parseApiKeys(raw) {
18
102
  const entries = [];
@@ -20,20 +104,18 @@ export function parseApiKeys(raw) {
20
104
  const trimmed = pair.trim();
21
105
  if (!trimmed)
22
106
  continue;
23
- // Use LAST colon as separator — keys may contain colons (e.g. base64)
24
- // but profile names never do
25
107
  const colonIdx = trimmed.lastIndexOf(':');
26
108
  if (colonIdx === -1) {
27
109
  throw new Error(`Invalid API key entry '${trimmed}': expected 'key:profile' format. ` +
28
- `Valid profiles: ${Object.keys(PROFILES).join(', ')}`);
110
+ `Valid profiles: ${Object.keys(API_KEY_PROFILES).join(', ')}`);
29
111
  }
30
112
  const key = trimmed.slice(0, colonIdx);
31
113
  const profile = trimmed.slice(colonIdx + 1);
32
114
  if (!key) {
33
115
  throw new Error('Invalid API key entry: key cannot be empty');
34
116
  }
35
- if (!PROFILES[profile]) {
36
- 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(', ')}`);
37
119
  }
38
120
  entries.push({ key, profile });
39
121
  }
@@ -42,118 +124,150 @@ export function parseApiKeys(raw) {
42
124
  }
43
125
  return entries;
44
126
  }
45
- /**
46
- * Maps profile names to the scopes they grant.
47
- * Used when API keys are assigned to profiles — the key inherits these scopes.
48
- * Kept in sync with PROFILES: each profile's safety flags determine its scopes.
49
- */
50
- export const PROFILE_SCOPES = {
51
- viewer: ['read'],
52
- 'viewer-data': ['read', 'data'],
53
- 'viewer-sql': ['read', 'data', 'sql'],
54
- developer: ['read', 'write'],
55
- 'developer-data': ['read', 'write', 'data'],
56
- '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).',
57
138
  };
58
- /**
59
- * Named profiles — convenience presets for common safety configurations.
60
- * Each profile sets a combination of safety flags. Individual CLI flags
61
- * applied after the profile can override any profile default.
62
- */
63
- export const PROFILES = {
64
- viewer: {
65
- readOnly: true,
66
- blockData: true,
67
- blockFreeSQL: true,
68
- enableTransports: false,
69
- },
70
- 'viewer-data': {
71
- readOnly: true,
72
- blockData: false,
73
- blockFreeSQL: true,
74
- enableTransports: false,
75
- },
76
- 'viewer-sql': {
77
- readOnly: true,
78
- blockData: false,
79
- blockFreeSQL: false,
80
- enableTransports: false,
81
- },
82
- developer: {
83
- readOnly: false,
84
- blockData: true,
85
- blockFreeSQL: true,
86
- enableTransports: true,
87
- allowedPackages: ['$TMP'],
88
- },
89
- 'developer-data': {
90
- readOnly: false,
91
- blockData: false,
92
- blockFreeSQL: true,
93
- enableTransports: true,
94
- allowedPackages: ['$TMP'],
95
- },
96
- 'developer-sql': {
97
- readOnly: false,
98
- blockData: false,
99
- blockFreeSQL: false,
100
- enableTransports: true,
101
- allowedPackages: ['$TMP'],
102
- },
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,
103
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
+ }
104
167
  /**
105
- * Parse CLI arguments and environment variables into a ServerConfig.
106
- *
107
- * We use a simple hand-rolled parser here (not commander) because
108
- * the MCP server entry point needs to be fast and lightweight.
109
- * 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.
110
171
  */
111
- export function parseArgs(args) {
172
+ export function resolveConfig(args) {
173
+ detectLegacyConfig(args);
112
174
  const config = { ...DEFAULT_CONFIG };
113
- // Helper: get a CLI flag value (--flag value or --flag=value)
175
+ const sources = {};
176
+ // ── Resolvers ──────────────────────────────────────────────────────
114
177
  const getFlag = (name) => {
115
178
  const prefix = `--${name}=`;
116
179
  for (let i = 0; i < args.length; i++) {
117
- if (args[i] === `--${name}` && i + 1 < args.length) {
180
+ if (args[i] === `--${name}` && i + 1 < args.length)
118
181
  return args[i + 1];
119
- }
120
- if (args[i]?.startsWith(prefix)) {
182
+ if (args[i]?.startsWith(prefix))
121
183
  return args[i].slice(prefix.length);
122
- }
123
184
  }
124
185
  return undefined;
125
186
  };
126
- // Helper: resolve value from CLI > env > default
127
- const resolve = (flag, envVar, defaultVal) => {
128
- 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;
129
199
  };
130
- const resolveBool = (flag, envVar, defaultVal) => {
131
- const val = getFlag(flag) ?? process.env[envVar];
132
- if (val === undefined)
133
- return defaultVal;
134
- 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;
135
212
  };
136
- const resolveFeature = (flag, envVar) => {
137
- const val = getFlag(flag) ?? process.env[envVar] ?? 'auto';
138
- if (val === 'on' || val === 'off')
139
- 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';
140
229
  return 'auto';
141
230
  };
142
- // --- SAP Connection ---
143
- config.url = resolve('url', 'SAP_URL', '');
144
- config.username = resolve('user', 'SAP_USER', '');
145
- config.password = resolve('password', 'SAP_PASSWORD', '');
146
- config.client = resolve('client', 'SAP_CLIENT', '100');
147
- config.language = resolve('language', 'SAP_LANGUAGE', 'EN');
148
- config.insecure = resolveBool('insecure', 'SAP_INSECURE', false);
149
- // --- Cookie Auth ---
150
- config.cookieFile = getFlag('cookie-file') ?? process.env.SAP_COOKIE_FILE;
151
- config.cookieString = getFlag('cookie-string') ?? process.env.SAP_COOKIE_STRING;
152
- // --- Transport ---
153
- 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');
154
256
  config.transport = (transport === 'http-streamable' ? 'http-streamable' : 'stdio');
155
- config.httpAddr = resolve('http-addr', 'SAP_HTTP_ADDR', '0.0.0.0:8080');
156
- // --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
+ }
157
271
  const portOverride = getFlag('port') ?? process.env.ARC1_PORT;
158
272
  if (portOverride) {
159
273
  const parsedPort = Number.parseInt(portOverride, 10);
@@ -162,133 +276,147 @@ export function parseArgs(args) {
162
276
  }
163
277
  const addrHost = config.httpAddr.includes(':') ? config.httpAddr.split(':')[0] : '0.0.0.0';
164
278
  config.httpAddr = `${addrHost}:${parsedPort}`;
279
+ sources.httpAddr = getFlag('port') !== undefined ? { flag: '--port' } : { env: 'ARC1_PORT' };
165
280
  }
166
- // --- Profile (apply before individual safety flags so flags can override) ---
167
- const profileName = getFlag('profile') ?? process.env.ARC1_PROFILE;
168
- if (profileName) {
169
- const profile = PROFILES[profileName];
170
- if (!profile) {
171
- throw new Error(`Unknown profile '${profileName}'. Valid profiles: ${Object.keys(PROFILES).join(', ')}`);
172
- }
173
- Object.assign(config, profile);
174
- }
175
- // --- Safety (individual flags override profile defaults) ---
176
- // Only override profile defaults when the flag/env is explicitly set
177
- const readOnlyExplicit = getFlag('read-only') ?? process.env.SAP_READ_ONLY;
178
- if (readOnlyExplicit !== undefined)
179
- config.readOnly = readOnlyExplicit === 'true' || readOnlyExplicit === '1';
180
- else if (!profileName)
181
- config.readOnly = true;
182
- const blockFreeSQLExplicit = getFlag('block-free-sql') ?? process.env.SAP_BLOCK_FREE_SQL;
183
- if (blockFreeSQLExplicit !== undefined)
184
- config.blockFreeSQL = blockFreeSQLExplicit === 'true' || blockFreeSQLExplicit === '1';
185
- else if (!profileName)
186
- config.blockFreeSQL = true;
187
- const blockDataExplicit = getFlag('block-data') ?? process.env.SAP_BLOCK_DATA;
188
- if (blockDataExplicit !== undefined)
189
- config.blockData = blockDataExplicit === 'true' || blockDataExplicit === '1';
190
- else if (!profileName)
191
- config.blockData = true;
192
- config.allowedOps = resolve('allowed-ops', 'SAP_ALLOWED_OPS', '');
193
- config.disallowedOps = resolve('disallowed-ops', 'SAP_DISALLOWED_OPS', '');
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');
194
287
  const pkgs = getFlag('allowed-packages') ?? process.env.SAP_ALLOWED_PACKAGES;
195
- if (pkgs) {
288
+ if (pkgs !== undefined) {
196
289
  const raw = pkgs.split(',').map((p) => p.trim());
197
290
  const filtered = raw.filter((p) => p.length > 0);
198
291
  if (raw.length !== filtered.length) {
199
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 });
200
293
  }
201
294
  config.allowedPackages = filtered;
295
+ sources.allowedPackages =
296
+ getFlag('allowed-packages') !== undefined ? { flag: '--allowed-packages' } : { env: 'SAP_ALLOWED_PACKAGES' };
297
+ }
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';
202
314
  }
203
- const enableGitExplicit = getFlag('enable-git') ?? process.env.SAP_ENABLE_GIT;
204
- if (enableGitExplicit !== undefined)
205
- config.enableGit = enableGitExplicit === 'true' || enableGitExplicit === '1';
206
- else if (!profileName)
207
- config.enableGit = false;
208
- const enableTransportsExplicit = getFlag('enable-transports') ?? process.env.SAP_ENABLE_TRANSPORTS;
209
- if (enableTransportsExplicit !== undefined)
210
- config.enableTransports = enableTransportsExplicit === 'true' || enableTransportsExplicit === '1';
211
- else if (!profileName)
212
- config.enableTransports = false;
213
- // --- Features ---
214
- config.featureAbapGit = resolveFeature('feature-abapgit', 'SAP_FEATURE_ABAPGIT');
215
- config.featureGcts = resolveFeature('feature-gcts', 'SAP_FEATURE_GCTS');
216
- config.featureRap = resolveFeature('feature-rap', 'SAP_FEATURE_RAP');
217
- config.featureAmdp = resolveFeature('feature-amdp', 'SAP_FEATURE_AMDP');
218
- config.featureUi5 = resolveFeature('feature-ui5', 'SAP_FEATURE_UI5');
219
- config.featureTransport = resolveFeature('feature-transport', 'SAP_FEATURE_TRANSPORT');
220
- config.featureHana = resolveFeature('feature-hana', 'SAP_FEATURE_HANA');
221
- config.featureUi5Repo = resolveFeature('feature-ui5repo', 'SAP_FEATURE_UI5REPO');
222
- config.featureFlp = resolveFeature('feature-flp', 'SAP_FEATURE_FLP');
223
- // --- System Type Detection ---
224
- const systemType = resolve('system-type', 'SAP_SYSTEM_TYPE', 'auto');
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');
225
346
  config.systemType = (['btp', 'onprem'].includes(systemType) ? systemType : 'auto');
226
- // --- Authentication (MCP client → ARC-1) ---
227
- config.apiKey = getFlag('api-key') ?? process.env.ARC1_API_KEY;
228
- // Multiple API keys with per-key profiles: "key1:viewer,key2:developer"
347
+ // ── Authentication ─────────────────────────────────────────────────
229
348
  const apiKeysRaw = getFlag('api-keys') ?? process.env.ARC1_API_KEYS;
230
349
  if (apiKeysRaw) {
231
350
  config.apiKeys = parseApiKeys(apiKeysRaw);
351
+ sources.apiKeys = getFlag('api-keys') !== undefined ? { flag: '--api-keys' } : { env: 'ARC1_API_KEYS' };
232
352
  }
233
- config.oidcIssuer = getFlag('oidc-issuer') ?? process.env.SAP_OIDC_ISSUER;
234
- 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');
235
358
  const clockTolerance = getFlag('oidc-clock-tolerance') ?? process.env.SAP_OIDC_CLOCK_TOLERANCE;
236
359
  if (clockTolerance) {
237
360
  const parsed = Number.parseInt(clockTolerance, 10);
238
361
  config.oidcClockTolerance = Number.isNaN(parsed) ? undefined : parsed;
239
362
  }
240
- config.xsuaaAuth = resolveBool('xsuaa-auth', 'SAP_XSUAA_AUTH', false);
241
- // --- BTP ABAP Environment (direct connection via service key) ---
242
- config.btpServiceKey = getFlag('btp-service-key') ?? process.env.SAP_BTP_SERVICE_KEY;
243
- config.btpServiceKeyFile = getFlag('btp-service-key-file') ?? process.env.SAP_BTP_SERVICE_KEY_FILE;
244
- 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');
245
368
  config.btpOAuthCallbackPort = Number.parseInt(cbPort, 10) || 0;
246
- // --- Principal Propagation ---
247
- config.ppEnabled = resolveBool('pp-enabled', 'SAP_PP_ENABLED', false);
248
- config.ppStrict = resolveBool('pp-strict', 'SAP_PP_STRICT', false);
249
- config.ppAllowSharedCookies = resolveBool('pp-allow-shared-cookies', 'SAP_PP_ALLOW_SHARED_COOKIES', false);
250
- // --- SAML Behavior ---
251
- config.disableSaml2 = resolveBool('disable-saml', 'SAP_DISABLE_SAML', false);
252
- // --- Tool Mode ---
253
- 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');
254
377
  config.toolMode = (toolMode === 'hyperfocused' ? 'hyperfocused' : 'standard');
255
- // --- Lint ---
256
- config.abaplintConfig = getFlag('abaplint-config') ?? process.env.SAP_ABAPLINT_CONFIG;
257
- config.lintBeforeWrite = resolveBool('lint-before-write', 'SAP_LINT_BEFORE_WRITE', true);
258
- // --- Cache ---
259
- 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');
260
384
  config.cacheMode = (['memory', 'sqlite', 'none'].includes(cacheMode) ? cacheMode : 'auto');
261
- config.cacheFile = resolve('cache-file', 'ARC1_CACHE_FILE', '.arc1-cache.db');
262
- config.cacheWarmup = resolveBool('cache-warmup', 'ARC1_CACHE_WARMUP', false);
263
- config.cacheWarmupPackages = resolve('cache-warmup-packages', 'ARC1_CACHE_WARMUP_PACKAGES', '');
264
- // --- 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 ────────────────────────────────────────────────────
265
389
  const maxConcurrent = getFlag('max-concurrent') ?? process.env.ARC1_MAX_CONCURRENT;
266
390
  if (maxConcurrent) {
267
391
  const parsed = Number.parseInt(maxConcurrent, 10);
268
392
  config.maxConcurrent = Number.isNaN(parsed) || parsed < 1 ? 1 : parsed;
269
393
  }
270
- // --- Logging ---
271
- config.logFile = getFlag('log-file') ?? process.env.ARC1_LOG_FILE;
272
- 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');
273
397
  config.logLevel = (['debug', 'info', 'warn', 'error'].includes(logLevel) ? logLevel : 'info');
274
- const logFormat = resolve('log-format', 'ARC1_LOG_FORMAT', 'text');
398
+ const logFormat = resolveStr('log-format', 'ARC1_LOG_FORMAT', 'text', 'logFormat');
275
399
  config.logFormat = (logFormat === 'json' ? 'json' : 'text');
276
- // --- Misc ---
277
- config.verbose = resolveBool('verbose', 'SAP_VERBOSE', false);
278
- // --verbose is sugar for --log-level debug
279
- if (config.verbose) {
400
+ // ── Misc ───────────────────────────────────────────────────────────
401
+ config.verbose = resolveBool('verbose', 'SAP_VERBOSE', false, 'verbose');
402
+ if (config.verbose)
280
403
  config.logLevel = 'debug';
281
- }
282
- // --- Startup Validation ---
404
+ // ── Startup Validation ─────────────────────────────────────────────
283
405
  validateConfig(config);
284
- 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;
285
414
  }
286
415
  /**
287
416
  * Validate configuration for internally consistent auth settings.
288
417
  * Fails fast at startup for invalid or dangerous config combinations.
289
418
  */
290
419
  export function validateConfig(config) {
291
- // OIDC: audience is required when issuer is set (RFC 9700 §2.3 audience restriction)
292
420
  if (config.oidcIssuer && !config.oidcAudience) {
293
421
  throw new Error('SAP_OIDC_AUDIENCE is required when SAP_OIDC_ISSUER is set — ' +
294
422
  'audience validation prevents token confusion across services (RFC 9700 §2.3)');
@@ -296,7 +424,6 @@ export function validateConfig(config) {
296
424
  if (config.oidcAudience && !config.oidcIssuer) {
297
425
  throw new Error('SAP_OIDC_ISSUER is required when SAP_OIDC_AUDIENCE is set');
298
426
  }
299
- // PP: ppStrict requires ppEnabled
300
427
  if (config.ppStrict && !config.ppEnabled) {
301
428
  throw new Error('SAP_PP_STRICT=true requires SAP_PP_ENABLED=true — strict mode has no effect without principal propagation enabled');
302
429
  }