zapier-platform-core 18.5.1 → 19.0.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.
@@ -0,0 +1,981 @@
1
+ 'use strict';
2
+
3
+ const applyMiddleware = require('../middleware');
4
+ const ensureArray = require('../tools/ensure-array');
5
+
6
+ // before middlewares
7
+ const addBasicAuthHeader = require('../http-middlewares/before/add-basic-auth-header');
8
+ const addQueryParams = require('../http-middlewares/before/add-query-params');
9
+ const createInjectInputMiddleware = require('../http-middlewares/before/inject-input');
10
+ const prepareRequest = require('../http-middlewares/before/prepare-request');
11
+ const sanitizeHeaders = require('../http-middlewares/before/sanatize-headers');
12
+
13
+ const { REPLACE_CURLIES } = require('../constants');
14
+ const { withHttpCapture } = require('./http-capture');
15
+ const { buildLegacyScripting, loadLegacyZap } = require('./legacy-scripting');
16
+
17
+ const errors = require('../errors');
18
+
19
+ // --- Helpers ---
20
+
21
+ // Opaque sentinels survive core's curly-stripping (normalizeEmptyParamFields)
22
+ // and any stringification middleware does. We embed them into placeholder
23
+ // authData / proxied process.env, then convert back to {{curlies}} on the
24
+ // way out (cleanTemplate). Lowercase-underscore markers are URL-safe in
25
+ // hostnames AND querystrings, so a sentinel survives substitution into
26
+ // any URL position without breaking `new URL(...)` parsing — which is
27
+ // what extractTemplate uses to recover params after addQueryParams.
28
+ const AUTH_SENTINEL_OPEN = '__placeholder_auth__';
29
+ const ENV_SENTINEL_OPEN = '__placeholder_env__';
30
+ const SENTINEL_CLOSE = '__end_placeholder__';
31
+ const wrapAuthSentinel = (key) =>
32
+ `${AUTH_SENTINEL_OPEN}${key}${SENTINEL_CLOSE}`;
33
+ const wrapEnvSentinel = (key) => `${ENV_SENTINEL_OPEN}${key}${SENTINEL_CLOSE}`;
34
+ const AUTH_SENTINEL_RE = /__placeholder_auth__(.+?)__end_placeholder__/g;
35
+ const ENV_SENTINEL_RE = /__placeholder_env__(.+?)__end_placeholder__/g;
36
+
37
+ // Walk a template and replace sentinels with their {{curly}} equivalents.
38
+ // Returns a new object; the input is not mutated.
39
+ const sentinelsToCurlies = (value) => {
40
+ if (typeof value === 'string') {
41
+ return value
42
+ .replace(AUTH_SENTINEL_RE, (_, k) => `{{bundle.authData.${k}}}`)
43
+ .replace(ENV_SENTINEL_RE, (_, k) => `{{process.env.${k}}}`);
44
+ }
45
+ if (Array.isArray(value)) {
46
+ return value.map(sentinelsToCurlies);
47
+ }
48
+ if (value && typeof value === 'object') {
49
+ const out = {};
50
+ for (const [k, v] of Object.entries(value)) {
51
+ out[k] = sentinelsToCurlies(v);
52
+ }
53
+ return out;
54
+ }
55
+ return value;
56
+ };
57
+
58
+ const hasAuthPlaceholders = (obj) => {
59
+ const s = JSON.stringify(obj);
60
+ return (
61
+ s.includes(AUTH_SENTINEL_OPEN) ||
62
+ s.includes(ENV_SENTINEL_OPEN) ||
63
+ /\{\{\s*bundle\.authData\./.test(s) ||
64
+ /\{\{\s*process\.env\./.test(s)
65
+ );
66
+ };
67
+
68
+ // Check if auth field placeholders were consumed by encoding (e.g.,
69
+ // base64). Returns true if the template has placeholders but none of
70
+ // them are bundle.authData, AND the app has declared auth fields that
71
+ // should have survived.
72
+ // Build a supported:true result, but check if auth fields were consumed
73
+ // by encoding (e.g., base64). If so, override to supported:false.
74
+ //
75
+ // We only flag as consumed when the developer *explicitly declared*
76
+ // auth.fields. Implicit standard fields (basic's username/password,
77
+ // oauth2's access_token, etc.) aren't a strong "I expect this in the
78
+ // request" signal — apps may use process.env exclusively and never
79
+ // reference standard fields, which would be a false positive here.
80
+ const supportedResult = (authType, source, template, auth) => {
81
+ if (template && Object.keys(template).length > 0) {
82
+ const s = JSON.stringify(template);
83
+ const hasAuthData = /\{\{\s*bundle\.authData\./.test(s);
84
+ const hasProcessEnv = /\{\{\s*process\.env\./.test(s);
85
+ const hasDeclaredFields =
86
+ auth && auth.fields && auth.fields.some((f) => f.key);
87
+ if (hasProcessEnv && !hasAuthData && hasDeclaredFields) {
88
+ return { supported: false, reason: 'auth_fields_consumed', authType };
89
+ }
90
+ }
91
+ return { supported: true, authType, source, template };
92
+ };
93
+
94
+ // Remove empty headers/params/body from a template object and convert
95
+ // any internal sentinels back to user-facing {{curly}} placeholders.
96
+ const cleanTemplate = (template) => {
97
+ const cleaned = {};
98
+ if (template.headers && Object.keys(template.headers).length > 0) {
99
+ cleaned.headers = sentinelsToCurlies(template.headers);
100
+ }
101
+ if (template.params && Object.keys(template.params).length > 0) {
102
+ cleaned.params = sentinelsToCurlies(template.params);
103
+ }
104
+ if (template.body && Object.keys(template.body).length > 0) {
105
+ cleaned.body = sentinelsToCurlies(template.body);
106
+ }
107
+ return cleaned;
108
+ };
109
+
110
+ // Token-like authData keys that session-auth integrations populate at
111
+ // runtime via `sessionConfig.perform` without declaring them in
112
+ // `authentication.fields`. We add placeholders for these so middleware
113
+ // reading `bundle.authData.<key>` produces a template entry.
114
+ const SESSION_AUTH_COMMON_KEYS = [
115
+ 'PHPSESSID',
116
+ 'accessToken',
117
+ 'access_token',
118
+ 'apiToken',
119
+ 'refresh_token',
120
+ 'sessionKey',
121
+ 'sessionToken',
122
+ 'token',
123
+ ];
124
+
125
+ // Build placeholder authData where each value is an opaque sentinel
126
+ // string. Sentinels survive core's normalize/curly-stripping and any
127
+ // stringification middleware does, so the captured request still
128
+ // contains them verbatim. cleanTemplate converts sentinels back to
129
+ // {{bundle.authData.X}} on the way out.
130
+ const buildPlaceholderAuthData = (auth) => {
131
+ const authData = {};
132
+
133
+ for (const field of auth.fields || []) {
134
+ if (field.key) {
135
+ authData[field.key] = wrapAuthSentinel(field.key);
136
+ }
137
+ }
138
+
139
+ // Standard fields for known auth types (if not already declared)
140
+ if (auth.type === 'basic') {
141
+ authData.username = authData.username || wrapAuthSentinel('username');
142
+ authData.password = authData.password || wrapAuthSentinel('password');
143
+ }
144
+ if (auth.type === 'oauth2') {
145
+ authData.access_token =
146
+ authData.access_token || wrapAuthSentinel('access_token');
147
+ if (
148
+ auth.oauth2Config &&
149
+ auth.oauth2Config.autoRefresh &&
150
+ auth.oauth2Config.refreshAccessToken
151
+ ) {
152
+ authData.refresh_token =
153
+ authData.refresh_token || wrapAuthSentinel('refresh_token');
154
+ }
155
+ }
156
+ if (auth.type === 'oauth1') {
157
+ authData.oauth_token =
158
+ authData.oauth_token || wrapAuthSentinel('oauth_token');
159
+ authData.oauth_token_secret =
160
+ authData.oauth_token_secret || wrapAuthSentinel('oauth_token_secret');
161
+ }
162
+ if (
163
+ auth.type === 'custom' &&
164
+ auth.customConfig &&
165
+ auth.customConfig.sendCode != null
166
+ ) {
167
+ authData.code = authData.code || wrapAuthSentinel('code');
168
+ }
169
+ // Session auth has no schema-defined standard fields, but some apps
170
+ // stash their token under conventional undeclared names (set at
171
+ // runtime by the session auth flow). Add placeholders for these so
172
+ // middleware that reads `bundle.authData.<key>` can still produce a
173
+ // template.
174
+ if (auth.type === 'session') {
175
+ for (const key of SESSION_AUTH_COMMON_KEYS) {
176
+ authData[key] = authData[key] || wrapAuthSentinel(key);
177
+ }
178
+ }
179
+
180
+ return authData;
181
+ };
182
+
183
+ // Check if template A is a superset of template B (all keys in B exist in A
184
+ // with the same values, but A may have extra keys).
185
+ const isSuperset = (a, b) => {
186
+ if (!b || Object.keys(b).length === 0) {
187
+ return true;
188
+ }
189
+ for (const section of ['headers', 'params', 'body']) {
190
+ if (!b[section]) {
191
+ continue;
192
+ }
193
+ if (!a[section]) {
194
+ return false;
195
+ }
196
+ for (const [k, v] of Object.entries(b[section])) {
197
+ if (a[section][k] !== v) {
198
+ return false;
199
+ }
200
+ }
201
+ }
202
+ return true;
203
+ };
204
+
205
+ // Wrap placeholderAuthData in a Proxy that returns truthy values for any
206
+ // undeclared key accessed by middleware. Used for divergence detection:
207
+ // if middleware branches on undeclared authData fields, the Proxy run will
208
+ // produce a different template than the plain run.
209
+ const buildProxyAuthData = (placeholderAuthData) =>
210
+ new Proxy(placeholderAuthData, {
211
+ get(target, prop) {
212
+ if (prop in target) {
213
+ return target[prop];
214
+ }
215
+ // Symbol properties (e.g. Symbol.toPrimitive) and internal props should pass through
216
+ if (typeof prop === 'symbol') {
217
+ return undefined;
218
+ }
219
+ return `__undeclared_${prop}__`;
220
+ },
221
+ has(target, prop) {
222
+ // Make `'key' in authData` return true for any string key
223
+ return typeof prop === 'string' || prop in target;
224
+ },
225
+ });
226
+
227
+ // Check if two templates are structurally equal (same keys and values).
228
+ const templatesEqual = (a, b) =>
229
+ JSON.stringify(cleanTemplate(a)) === JSON.stringify(cleanTemplate(b));
230
+
231
+ // Run fn with process.env proxied to return placeholders for unknown vars.
232
+ // Concurrent withProxiedEnv calls (e.g., parallel URL probe runs) must
233
+ // share the same proxy — naive "save current; restore current" would let
234
+ // the inner call save the outer's Proxy and "restore" to it, leaking the
235
+ // Proxy past the outermost finally.
236
+ let envProxyDepth = 0;
237
+ let realOrigEnv = null;
238
+ const withProxiedEnv = async (fn) => {
239
+ if (envProxyDepth === 0) {
240
+ realOrigEnv = process.env;
241
+ process.env = new Proxy(realOrigEnv, {
242
+ get(target, prop) {
243
+ if (prop in target) {
244
+ return target[prop];
245
+ }
246
+ if (typeof prop === 'symbol') {
247
+ return undefined;
248
+ }
249
+ return wrapEnvSentinel(prop);
250
+ },
251
+ });
252
+ }
253
+ envProxyDepth++;
254
+ try {
255
+ return await fn();
256
+ } finally {
257
+ envProxyDepth--;
258
+ if (envProxyDepth === 0) {
259
+ process.env = realOrigEnv;
260
+ realOrigEnv = null;
261
+ }
262
+ }
263
+ };
264
+
265
+ const buildSyntheticInput = (input, placeholderAuthData) => ({
266
+ _zapier: {
267
+ ...input._zapier,
268
+ event: {
269
+ ...input._zapier.event,
270
+ bundle: {
271
+ authData: placeholderAuthData,
272
+ inputData: {},
273
+ meta: {},
274
+ },
275
+ },
276
+ },
277
+ });
278
+
279
+ // Create a String-like object whose comparison methods always return a fixed
280
+ // truthy/falsy result. Used to detect middleware that branches on request.url.
281
+ const createUrlProbe = (baseUrl, matchAll) => {
282
+ const s = new String(baseUrl); // eslint-disable-line no-new-wrappers
283
+ s.includes = () => matchAll;
284
+ s.startsWith = () => matchAll;
285
+ s.endsWith = () => matchAll;
286
+ s.indexOf = () => (matchAll ? 0 : -1);
287
+ s.search = () => (matchAll ? 0 : -1);
288
+ s.match = () => (matchAll ? [baseUrl] : null);
289
+ return s;
290
+ };
291
+
292
+ // Extract headers/params/body from a captured request, stripping defaults.
293
+ const extractTemplate = (req) => {
294
+ const template = {};
295
+
296
+ if (req.headers) {
297
+ const headers = { ...req.headers };
298
+ // Strip transport-level headers that shouldn't be in the auth template
299
+ for (const key of Object.keys(headers)) {
300
+ const lower = key.toLowerCase();
301
+ if (lower === 'content-length') {
302
+ delete headers[key];
303
+ }
304
+ }
305
+ if (Object.keys(headers).length > 0) {
306
+ template.headers = headers;
307
+ }
308
+ }
309
+
310
+ // Check explicit params first, then extract from URL query string
311
+ // (addQueryParams middleware moves params into the URL).
312
+ const params =
313
+ req.params && Object.keys(req.params).length > 0 ? { ...req.params } : {};
314
+
315
+ if (Object.keys(params).length === 0 && req.url) {
316
+ try {
317
+ const parsed = new URL(req.url);
318
+ for (const [k, v] of parsed.searchParams.entries()) {
319
+ // Keep params that carry auth placeholders (sentinels survive
320
+ // normalize; legacy {{curlies}} may also appear in user-written
321
+ // requestTemplates that bypass substitution). Skip non-auth
322
+ // literals like trigger-specific filter params.
323
+ if (
324
+ (typeof v === 'string' && v.includes(AUTH_SENTINEL_OPEN)) ||
325
+ (typeof v === 'string' && v.includes(ENV_SENTINEL_OPEN)) ||
326
+ /\{\{bundle\.authData\./.test(v) ||
327
+ /\{\{process\.env\./.test(v)
328
+ ) {
329
+ params[k] = v;
330
+ }
331
+ }
332
+ } catch {
333
+ // URL might have unresolved placeholders
334
+ }
335
+ }
336
+
337
+ if (Object.keys(params).length > 0) {
338
+ template.params = params;
339
+ }
340
+
341
+ if (req.body) {
342
+ template.body = req.body;
343
+ }
344
+
345
+ return template;
346
+ };
347
+
348
+ // Stub z object used for pipeline capture and test function survival.
349
+ const createStubZ = (compiledApp, cachedZap) => {
350
+ const Zap = cachedZap !== undefined ? cachedZap : loadLegacyZap(compiledApp);
351
+
352
+ const stubRequest = async () => ({
353
+ status: 200,
354
+ headers: {},
355
+ data: {},
356
+ content: '{}',
357
+ });
358
+
359
+ const stubZ = {
360
+ console: { log: () => {}, error: () => {}, warn: () => {} },
361
+ errors,
362
+ JSON: { parse: JSON.parse, stringify: JSON.stringify },
363
+ legacyScripting: buildLegacyScripting(
364
+ compiledApp,
365
+ (req) => stubZ.request(req),
366
+ Zap,
367
+ ),
368
+ request: stubRequest,
369
+ };
370
+
371
+ return stubZ;
372
+ };
373
+
374
+ // --- Survival routines ---
375
+
376
+ // Run placeholder authData through the beforeRequest middleware pipeline.
377
+ // Captures the prepared request right before it would be sent over HTTP.
378
+ // Returns { template, error? }.
379
+ const runMiddlewareSurvival = async (
380
+ compiledApp,
381
+ input,
382
+ auth,
383
+ placeholderAuthData,
384
+ { url = 'https://example.com', urlProbe, reqOverrides = {}, cachedZap } = {},
385
+ ) => {
386
+ const syntheticInput = buildSyntheticInput(input, placeholderAuthData);
387
+
388
+ const httpBefores = [
389
+ createInjectInputMiddleware(syntheticInput),
390
+ prepareRequest,
391
+ ];
392
+
393
+ // When a urlProbe is provided, inject it after prepareRequest (which
394
+ // stringifies the URL) but before the app's beforeRequest middleware.
395
+ if (urlProbe) {
396
+ httpBefores.push((req) => {
397
+ req.url = urlProbe;
398
+ return req;
399
+ });
400
+ }
401
+
402
+ httpBefores.push(...ensureArray(compiledApp.beforeRequest));
403
+
404
+ // After the app's beforeRequest runs, unwrap any urlProbe back to a plain
405
+ // string. The probe's overridden comparison methods are intended to perturb
406
+ // the app's beforeRequest only — leaving them in place perturbs downstream
407
+ // core middlewares too (e.g., addQueryParams checks `url.includes('?')` to
408
+ // pick its separator), which produces false-positive URL divergence.
409
+ if (urlProbe) {
410
+ httpBefores.push((req) => {
411
+ if (req.url && typeof req.url !== 'string') {
412
+ req.url = String(req.url);
413
+ }
414
+ return req;
415
+ });
416
+ }
417
+
418
+ if (auth.type === 'basic') {
419
+ httpBefores.push(addBasicAuthHeader);
420
+ }
421
+
422
+ httpBefores.push(sanitizeHeaders);
423
+ httpBefores.push(addQueryParams);
424
+
425
+ let capturedReq = null;
426
+ const captureFunction = (preparedReq) => {
427
+ capturedReq = preparedReq;
428
+ return Promise.resolve({
429
+ status: 200,
430
+ headers: {},
431
+ getHeader: () => undefined,
432
+ content: '{}',
433
+ data: {},
434
+ request: preparedReq,
435
+ });
436
+ };
437
+
438
+ const stubZ = createStubZ(compiledApp, cachedZap);
439
+ const syntheticBundle = {
440
+ authData: placeholderAuthData,
441
+ inputData: {},
442
+ meta: {},
443
+ };
444
+
445
+ const client = applyMiddleware(httpBefores, [], captureFunction, {
446
+ skipEnvelope: true,
447
+ extraArgs: [stubZ, syntheticBundle],
448
+ });
449
+
450
+ try {
451
+ await withProxiedEnv(() =>
452
+ client({
453
+ method: 'GET',
454
+ headers: {},
455
+ params: {},
456
+ ...reqOverrides,
457
+ url,
458
+ merge: true,
459
+ [REPLACE_CURLIES]: true,
460
+ }),
461
+ );
462
+ } catch (err) {
463
+ return { template: {}, error: err.message };
464
+ }
465
+
466
+ if (!capturedReq) {
467
+ return { template: {} };
468
+ }
469
+
470
+ return { template: extractTemplate(capturedReq) };
471
+ };
472
+
473
+ // Run placeholder authData through authentication.test (when it's a function).
474
+ // Stubs z.request AND monkey-patches http/https/fetch to capture outbound requests.
475
+ // Returns { template, requestMade, error? }.
476
+ const runTestFunctionSurvival = async (
477
+ testFn,
478
+ placeholderAuthData,
479
+ compiledApp,
480
+ input,
481
+ ) => {
482
+ let capturedReq = null;
483
+
484
+ const capture = (req) => {
485
+ if (!capturedReq) {
486
+ capturedReq = req;
487
+ }
488
+ };
489
+
490
+ const auth = compiledApp.authentication || {};
491
+ const syntheticInput = buildSyntheticInput(input, placeholderAuthData);
492
+
493
+ const httpBefores = [
494
+ createInjectInputMiddleware(syntheticInput),
495
+ prepareRequest,
496
+ ...ensureArray(compiledApp.beforeRequest),
497
+ ];
498
+
499
+ if (auth.type === 'basic') {
500
+ httpBefores.push(addBasicAuthHeader);
501
+ }
502
+ httpBefores.push(sanitizeHeaders);
503
+ httpBefores.push(addQueryParams);
504
+
505
+ const captureFunction = (preparedReq) => {
506
+ capture(preparedReq);
507
+ return Promise.resolve({
508
+ status: 200,
509
+ headers: {},
510
+ getHeader: () => undefined,
511
+ content: '{}',
512
+ data: {},
513
+ request: preparedReq,
514
+ });
515
+ };
516
+
517
+ const stubZ = createStubZ(compiledApp);
518
+ const syntheticBundle = {
519
+ authData: placeholderAuthData,
520
+ inputData: {},
521
+ meta: {},
522
+ };
523
+
524
+ const client = applyMiddleware(httpBefores, [], captureFunction, {
525
+ skipEnvelope: true,
526
+ extraArgs: [stubZ, syntheticBundle],
527
+ });
528
+
529
+ stubZ.request = async (reqOrUrl) => {
530
+ const req =
531
+ typeof reqOrUrl === 'string' ? { url: reqOrUrl } : { ...reqOrUrl };
532
+ // Run through the beforeRequest middleware pipeline
533
+ const response = await client({
534
+ ...req,
535
+ method: req.method || 'GET',
536
+ headers: req.headers || {},
537
+ params: req.params || {},
538
+ merge: true,
539
+ [REPLACE_CURLIES]: true,
540
+ });
541
+ return {
542
+ ...response,
543
+ throwForStatus: () => {},
544
+ json: {},
545
+ };
546
+ };
547
+
548
+ const bundle = {
549
+ authData: placeholderAuthData,
550
+ inputData: {},
551
+ meta: {},
552
+ };
553
+
554
+ try {
555
+ await withHttpCapture(capture, () =>
556
+ withProxiedEnv(() => testFn(stubZ, bundle)),
557
+ );
558
+ } catch (err) {
559
+ // If a request was captured before the error, use its template.
560
+ // Many test functions crash parsing the stub response (e.g.,
561
+ // accessing response.data.emails[0]) — that's fine, we already
562
+ // have what we need.
563
+ if (capturedReq) {
564
+ return { template: extractTemplate(capturedReq), requestMade: true };
565
+ }
566
+ return { template: {}, requestMade: false, error: err.message };
567
+ }
568
+
569
+ if (!capturedReq) {
570
+ return { template: {}, requestMade: false };
571
+ }
572
+
573
+ return { template: extractTemplate(capturedReq), requestMade: true };
574
+ };
575
+
576
+ // --- Main command handler ---
577
+
578
+ const getAuthTemplate = async (compiledApp, input) => {
579
+ const auth = compiledApp.authentication;
580
+ const authType = auth ? auth.type : null;
581
+
582
+ // No authentication defined — nothing to inject
583
+ if (!auth) {
584
+ return { supported: true, authType: null, source: 'none', template: {} };
585
+ }
586
+
587
+ // Digest can't be expressed as a static template (per-request nonce).
588
+ // OAuth1 falls through — OAuth1 apps can implement a simplified static
589
+ // template (e.g., Trello).
590
+ if (authType === 'digest') {
591
+ return { supported: false, reason: 'digest', authType };
592
+ }
593
+
594
+ // Basic auth always runs through addBasicAuthHeader, which base64-encodes
595
+ // username:password. The encoding consumes the placeholder strings, so no
596
+ // {{bundle.authData.X}} survives in the captured request. Our template
597
+ // format has no way to express "base64-encode these fields at render
598
+ // time," so basic auth is fundamentally unsupportable here.
599
+ if (authType === 'basic') {
600
+ return { supported: false, reason: 'basic', authType };
601
+ }
602
+
603
+ const placeholderAuthData = buildPlaceholderAuthData(auth);
604
+ let beforeRequestTemplate;
605
+ let beforeRequestFailed = false;
606
+
607
+ const beforeRequest = ensureArray(compiledApp.beforeRequest);
608
+
609
+ // --- Step 1: requestTemplate (only when there's no beforeRequest) ---
610
+ // If the app declares a requestTemplate AND has no beforeRequest, that IS
611
+ // the auth template — return it directly without running middleware.
612
+ // When beforeRequest also exists, fall through to Step 2: prepareRequest
613
+ // merges requestTemplate into the captured request, so the pipeline sees
614
+ // the union of both contributions.
615
+ const requestTemplate = compiledApp.requestTemplate;
616
+ if (
617
+ beforeRequest.length === 0 &&
618
+ requestTemplate &&
619
+ Object.keys(requestTemplate).length > 0
620
+ ) {
621
+ const cleaned = cleanTemplate(requestTemplate);
622
+ // Return requestTemplate if it has auth placeholders or auth-like
623
+ // header names. Skip if it only has non-auth headers (Accept,
624
+ // Content-Type, User-Agent) — auth may come from beforeRequest or
625
+ // authentication.test.
626
+ const hasAuthContent =
627
+ hasAuthPlaceholders(cleaned) ||
628
+ (cleaned.headers &&
629
+ Object.keys(cleaned.headers).some((k) => {
630
+ const lower = k.toLowerCase();
631
+ return (
632
+ lower === 'authorization' ||
633
+ lower.includes('api-key') ||
634
+ lower.includes('apikey') ||
635
+ lower.includes('token')
636
+ );
637
+ })) ||
638
+ (cleaned.params && Object.keys(cleaned.params).length > 0);
639
+ if (Object.keys(cleaned).length > 0 && hasAuthContent) {
640
+ return supportedResult(authType, 'requestTemplate', cleaned, auth);
641
+ }
642
+ // requestTemplate has no auth content — fall through to Step 2
643
+ }
644
+
645
+ // --- Step 2: beforeRequest middleware ---
646
+ // Run placeholder authData through the beforeRequest pipeline directly.
647
+ // This captures auth injected by middleware (most common pattern).
648
+ if (beforeRequest.length > 0) {
649
+ const { template, error } = await runMiddlewareSurvival(
650
+ compiledApp,
651
+ input,
652
+ auth,
653
+ placeholderAuthData,
654
+ );
655
+
656
+ if (error) {
657
+ if (!auth.test) {
658
+ return {
659
+ supported: false,
660
+ reason: 'beforeRequest_error',
661
+ authType,
662
+ error,
663
+ };
664
+ }
665
+ // beforeRequest errored but auth.test exists — fall through
666
+ } else {
667
+ if (hasAuthPlaceholders(template)) {
668
+ // Divergence check: authData proxy
669
+ const proxyAuthData = buildProxyAuthData(placeholderAuthData);
670
+ const { template: proxyTemplate, error: proxyError } =
671
+ await runMiddlewareSurvival(compiledApp, input, auth, proxyAuthData);
672
+
673
+ if (proxyError || !templatesEqual(template, proxyTemplate)) {
674
+ if (!auth.test) {
675
+ return {
676
+ supported: false,
677
+ reason: 'beforeRequest_not_static',
678
+ authType,
679
+ };
680
+ }
681
+ // else: fall through to authentication.test
682
+ } else {
683
+ // URL divergence check
684
+ const urlProbeTrue = createUrlProbe('https://example.com', true);
685
+ const urlProbeFalse = createUrlProbe('https://example.com', false);
686
+ const [
687
+ { template: urlTrueTemplate, error: urlTrueError },
688
+ { template: urlFalseTemplate, error: urlFalseError },
689
+ ] = await Promise.all([
690
+ runMiddlewareSurvival(
691
+ compiledApp,
692
+ input,
693
+ auth,
694
+ placeholderAuthData,
695
+ {
696
+ urlProbe: urlProbeTrue,
697
+ },
698
+ ),
699
+ runMiddlewareSurvival(
700
+ compiledApp,
701
+ input,
702
+ auth,
703
+ placeholderAuthData,
704
+ {
705
+ urlProbe: urlProbeFalse,
706
+ },
707
+ ),
708
+ ]);
709
+
710
+ if (
711
+ urlTrueError ||
712
+ urlFalseError ||
713
+ !templatesEqual(urlTrueTemplate, urlFalseTemplate)
714
+ ) {
715
+ // URL-conditional middleware detected. If the app has
716
+ // authentication.test, fall through — the test function uses a
717
+ // real API URL where the middleware will behave normally.
718
+ if (!auth.test) {
719
+ return {
720
+ supported: false,
721
+ reason: 'beforeRequest_not_static',
722
+ authType,
723
+ };
724
+ }
725
+ // else: fall through to authentication.test steps
726
+ } else {
727
+ // beforeRequest succeeded. Store the template — if authentication.test
728
+ // produces a superset (e.g., adds per-operation auth headers from
729
+ // legacy scripting hooks), we'll prefer that instead.
730
+ beforeRequestTemplate = cleanTemplate(template);
731
+ }
732
+ } // end else (proxy check passed)
733
+ }
734
+
735
+ // No auth placeholders survived (BR consumed them, e.g. base64
736
+ // encoding) — or divergence was detected and we'd fall through if
737
+ // auth.test were available.
738
+ if (!auth.test) {
739
+ return {
740
+ supported: false,
741
+ reason: 'auth_fields_consumed',
742
+ authType,
743
+ };
744
+ }
745
+ } // end else (no error)
746
+
747
+ // beforeRequest couldn't produce a usable template — remember this so
748
+ // that if authentication.test also fails, we return not-supported.
749
+ beforeRequestFailed = !beforeRequestTemplate;
750
+ }
751
+
752
+ // --- Step 3: authentication.test is an object (request config) ---
753
+ // Run it through the beforeRequest pipeline just like core's
754
+ // executeRequest does, so auth headers/params are included.
755
+ if (auth.test && typeof auth.test !== 'function') {
756
+ const placeholderAuthData = buildPlaceholderAuthData(auth);
757
+ const testReq = auth.test;
758
+ const { template, error } = await runMiddlewareSurvival(
759
+ compiledApp,
760
+ input,
761
+ auth,
762
+ placeholderAuthData,
763
+ {
764
+ url: testReq.url || 'https://example.com',
765
+ reqOverrides: {
766
+ method: testReq.method || 'GET',
767
+ headers: testReq.headers || {},
768
+ params: testReq.params || {},
769
+ body: testReq.body,
770
+ },
771
+ },
772
+ );
773
+
774
+ if (error) {
775
+ return {
776
+ supported: false,
777
+ reason: 'beforeRequest_error',
778
+ authType,
779
+ error,
780
+ };
781
+ }
782
+
783
+ const testReqOverrides = {
784
+ method: testReq.method || 'GET',
785
+ headers: testReq.headers || {},
786
+ params: testReq.params || {},
787
+ body: testReq.body,
788
+ };
789
+
790
+ if (hasAuthPlaceholders(template)) {
791
+ // Divergence checks: authData proxy + URL probe
792
+ const proxyAuthData = buildProxyAuthData(placeholderAuthData);
793
+ const { template: proxyTemplate, error: proxyError } =
794
+ await runMiddlewareSurvival(compiledApp, input, auth, proxyAuthData, {
795
+ url: testReq.url || 'https://example.com',
796
+ reqOverrides: testReqOverrides,
797
+ });
798
+
799
+ if (proxyError || !templatesEqual(template, proxyTemplate)) {
800
+ return {
801
+ supported: false,
802
+ reason: 'beforeRequest_not_static',
803
+ authType,
804
+ };
805
+ }
806
+
807
+ // URL divergence check — only when there's beforeRequest middleware
808
+ // that could branch on URL. Skip if no beforeRequest (the test
809
+ // object's own URL/params are static by definition).
810
+ const hasBR = beforeRequest.length > 0;
811
+ const urlProbeTrue = createUrlProbe(
812
+ testReq.url || 'https://example.com',
813
+ true,
814
+ );
815
+ const urlProbeFalse = createUrlProbe(
816
+ testReq.url || 'https://example.com',
817
+ false,
818
+ );
819
+ const [
820
+ { template: urlTrueTemplate, error: urlTrueError },
821
+ { template: urlFalseTemplate, error: urlFalseError },
822
+ ] = await Promise.all([
823
+ runMiddlewareSurvival(compiledApp, input, auth, placeholderAuthData, {
824
+ urlProbe: urlProbeTrue,
825
+ reqOverrides: testReqOverrides,
826
+ }),
827
+ runMiddlewareSurvival(compiledApp, input, auth, placeholderAuthData, {
828
+ urlProbe: urlProbeFalse,
829
+ reqOverrides: testReqOverrides,
830
+ }),
831
+ ]);
832
+
833
+ if (
834
+ hasBR &&
835
+ (urlTrueError ||
836
+ urlFalseError ||
837
+ !templatesEqual(urlTrueTemplate, urlFalseTemplate))
838
+ ) {
839
+ return {
840
+ supported: false,
841
+ reason: 'beforeRequest_not_static',
842
+ authType,
843
+ };
844
+ }
845
+
846
+ return supportedResult(
847
+ authType,
848
+ 'authentication.test',
849
+ cleanTemplate(template),
850
+ auth,
851
+ );
852
+ }
853
+
854
+ return {
855
+ supported: false,
856
+ reason: 'auth_fields_consumed',
857
+ authType,
858
+ };
859
+ }
860
+
861
+ // --- Step 4: authentication.test is a function ---
862
+ if (typeof auth.test === 'function') {
863
+ const { template, requestMade, error } = await runTestFunctionSurvival(
864
+ auth.test,
865
+ placeholderAuthData,
866
+ compiledApp,
867
+ input,
868
+ );
869
+
870
+ if (error && !requestMade) {
871
+ // Function crashed before making a request.
872
+ if (beforeRequestFailed) {
873
+ return {
874
+ supported: false,
875
+ reason: 'test_function_error',
876
+ authType,
877
+ };
878
+ }
879
+ // beforeRequest may have a template — handled at the end
880
+ } else if (error) {
881
+ // Request was made but function crashed after. If beforeRequest
882
+ // has a template, prefer that over failing.
883
+ if (!beforeRequestTemplate) {
884
+ return {
885
+ supported: false,
886
+ reason: 'test_function_error',
887
+ authType,
888
+ error,
889
+ };
890
+ }
891
+ } else if (!requestMade) {
892
+ if (beforeRequestFailed) {
893
+ return {
894
+ supported: false,
895
+ reason: 'test_function_no_request',
896
+ authType,
897
+ };
898
+ }
899
+ // beforeRequest may have a template — handled at the end
900
+ } else {
901
+ if (hasAuthPlaceholders(template)) {
902
+ // Divergence check: run again with Proxy authData
903
+ const proxyAuthData = buildProxyAuthData(placeholderAuthData);
904
+ const { template: proxyTemplate, error: proxyError } =
905
+ await runTestFunctionSurvival(
906
+ auth.test,
907
+ proxyAuthData,
908
+ compiledApp,
909
+ input,
910
+ );
911
+
912
+ if (proxyError || !templatesEqual(template, proxyTemplate)) {
913
+ return {
914
+ supported: false,
915
+ reason: 'test_function_not_static',
916
+ authType,
917
+ };
918
+ }
919
+
920
+ // No URL divergence check here — the test function used a real API
921
+ // URL, so the captured template reflects normal request auth. URL
922
+ // divergence is only checked in the beforeRequest fallback path
923
+ // (which uses a synthetic URL).
924
+
925
+ const testTemplate = cleanTemplate(template);
926
+
927
+ // If beforeRequest also produced a template, pick the richer one.
928
+ // The test function may capture per-operation auth (e.g., legacy
929
+ // scripting hooks) that beforeRequest alone misses.
930
+ if (
931
+ beforeRequestTemplate &&
932
+ !isSuperset(testTemplate, beforeRequestTemplate)
933
+ ) {
934
+ return supportedResult(
935
+ authType,
936
+ 'beforeRequest',
937
+ beforeRequestTemplate,
938
+ auth,
939
+ );
940
+ }
941
+
942
+ return supportedResult(
943
+ authType,
944
+ 'authentication.test',
945
+ testTemplate,
946
+ auth,
947
+ );
948
+ }
949
+
950
+ if (beforeRequestTemplate) {
951
+ return supportedResult(
952
+ authType,
953
+ 'beforeRequest',
954
+ beforeRequestTemplate,
955
+ auth,
956
+ );
957
+ }
958
+
959
+ return {
960
+ supported: false,
961
+ reason: 'auth_fields_consumed',
962
+ authType,
963
+ };
964
+ } // end else (requestMade)
965
+ }
966
+
967
+ // No authentication.test captured a request. Use beforeRequestTemplate
968
+ // if available.
969
+ if (beforeRequestTemplate) {
970
+ return supportedResult(
971
+ authType,
972
+ 'beforeRequest',
973
+ beforeRequestTemplate,
974
+ auth,
975
+ );
976
+ }
977
+
978
+ return { supported: true, authType, source: 'none', template: {} };
979
+ };
980
+
981
+ module.exports = getAuthTemplate;