Milestone 0: import zappier billing, Verae middleware, and Zapier research
Compose-ready workspace: packages/zappier (rate card, portal, Stripe), packages/verae-zapier-middleware (timestamp + NATS), packages/verae-zapier (CLI app), vendor/zapier-platform, and research/zapier vendor corpus. Gate 0 structure checks pass. Product code and research are not yet wired.
This commit is contained in:
commit
b4150c8250
1364 changed files with 6814366 additions and 0 deletions
34
vendor/zapier-platform/packages/legacy-scripting-runner/$.js
vendored
Normal file
34
vendor/zapier-platform/packages/legacy-scripting-runner/$.js
vendored
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
const jQuery = require('jquery');
|
||||
const { DOMParser } = require('@xmldom/xmldom');
|
||||
const { jsdom } = require('jsdom');
|
||||
|
||||
const window = jsdom().defaultView;
|
||||
window.DOMParser = DOMParser;
|
||||
|
||||
const $ = jQuery(window);
|
||||
|
||||
$.parseXML = (data) => {
|
||||
if (!data || typeof data !== 'string') {
|
||||
return null;
|
||||
}
|
||||
|
||||
let xml;
|
||||
|
||||
try {
|
||||
xml = new DOMParser().parseFromString(data, 'text/xml');
|
||||
} catch (e) {
|
||||
// We can safely ignore this
|
||||
}
|
||||
|
||||
if (
|
||||
!xml ||
|
||||
!xml.documentElement ||
|
||||
xml.getElementsByTagName('parsererror').length
|
||||
) {
|
||||
throw new Error(`Invalid XML: ${data}`);
|
||||
}
|
||||
|
||||
return xml;
|
||||
};
|
||||
|
||||
module.exports = $;
|
||||
3
vendor/zapier-platform/packages/legacy-scripting-runner/.gitignore
vendored
Normal file
3
vendor/zapier-platform/packages/legacy-scripting-runner/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
node_modules
|
||||
*.log
|
||||
.DS_Store
|
||||
1
vendor/zapier-platform/packages/legacy-scripting-runner/.nvmrc
vendored
Normal file
1
vendor/zapier-platform/packages/legacy-scripting-runner/.nvmrc
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
v22
|
||||
26
vendor/zapier-platform/packages/legacy-scripting-runner/ARCHITECTURE.md
vendored
Normal file
26
vendor/zapier-platform/packages/legacy-scripting-runner/ARCHITECTURE.md
vendored
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
# `zapier-platform-legacy-scripting-runner` Architecture
|
||||
|
||||
## Purpose
|
||||
|
||||
- This package, released as `zapier-platform-legacy-scripting-runner` publicly on [npm](https://www.npmjs.com/package/zapier-platform-legacy-scripting-runner), is a compatibility shim between the legacy v2 platform and the current "v3" (aka CLI) platform.
|
||||
- Most developers won't install this directly, but we'll include it when auto-converting their legacy applications.
|
||||
- It implements everything found in the [legacy scripting environment](https://platform.zapier.com/legacy/scripting#available-libraries) ([github pinned link](https://github.com/zapier/visual-builder/blob/8da4359f0b67b7a343067fa84b74576917c85fdd/docs/_legacy/scripting.md) in case that done doesn't work).
|
||||
- While `core`, `cli`, and `schema` are released in lockstep and always match versions, `legacy-scripting-runner` is versioned separately and released on its own.
|
||||
|
||||
## Technical Organization
|
||||
|
||||
> `runner/...`, when used in a path to a file, is shorthand for `zapier-platform/packages/legacy-scripting-runner/...`
|
||||
|
||||
### Important Functions
|
||||
|
||||
- The lion's share of code lives in `runner/index.js`.
|
||||
- The primary purposes are to:
|
||||
- make available global functions that were available in the legacy environment (see the `compileLegacyScriptingSource` function)
|
||||
- handle `pre_X` and `post_X` lifecycle hooks (see `runEventCombo` function)
|
||||
- ensure inputs and outputs continue to allow what was allowed in the legacy environment (while CLI apps may have more strict requirements, such as needing to return arrays from triggers)
|
||||
- There are also individual files in `runner/*` that power functions/libraries exposed above, such as `btoa`, `$` (for `jQuery`), etc.
|
||||
|
||||
### Tests
|
||||
|
||||
- The main test file is `runner/test/integration-test.js`, which takes the basic example app from `runner/test/example-app/index.js`, tweaks a single trigger/action at a time, and makes asserts about how it performs.
|
||||
- There are also unit tests for individually implemented functions (such as `btoa`) in `runner/test/libraries.js`.
|
||||
227
vendor/zapier-platform/packages/legacy-scripting-runner/CHANGELOG.md
vendored
Normal file
227
vendor/zapier-platform/packages/legacy-scripting-runner/CHANGELOG.md
vendored
Normal file
|
|
@ -0,0 +1,227 @@
|
|||
## 4.0.8
|
||||
|
||||
- :hammer: Bump `@xmldom/xmldom` from 0.8.11 to 0.8.13 ([#1286](https://github.com/zapier/zapier-platform/pull/1286))
|
||||
|
||||
## 4.0.7
|
||||
|
||||
- :hammer: Bump `underscore` from 1.13.7 to 1.13.8 ([#1256](https://github.com/zapier/zapier-platform/pull/1256))
|
||||
- :hammer: Bump `lodash` from 4.17.23 to 4.18.1 ([#1274](https://github.com/zapier/zapier-platform/pull/1274))
|
||||
|
||||
## 4.0.6
|
||||
|
||||
- :bug: Declare optional peer dependency to fix pnpm strict module resolution ([#1255](https://github.com/zapier/zapier-platform/pull/1255))
|
||||
|
||||
## 4.0.5
|
||||
|
||||
_Released 2026-02-26_
|
||||
|
||||
- :bug: Add explicit `form-data` dependency to work with pnpm strict module isolation ([#1247](https://github.com/zapier/zapier-platform/pull/1247))
|
||||
- :hammer: Bump `lodash` from 4.17.21 to 4.17.23 ([#1235](https://github.com/zapier/zapier-platform/pull/1235))
|
||||
|
||||
## 4.0.1
|
||||
|
||||
_Release 2025-11-14_
|
||||
|
||||
- :bug: fix "Template string must be a string" when auth mapping contains non-string values ([#1195](https://github.com/zapier/zapier-platform/pull/1195))
|
||||
|
||||
## 4.0.0
|
||||
|
||||
_Released 2025-10-30_
|
||||
|
||||
We updated several dependencies of legacy-scripting-runner to address security vulnerabilities. This includes a **breaking change** :exclamation: from underscore's `_.template()` function, where the `_.template(templateString, data)` usage is no longer supported. You will have to change your legacy scripting code to use `_.template(templateString)(data)` instead.
|
||||
|
||||
That's the only breaking change. For details and more dependency updates, see the PR linked below:
|
||||
|
||||
- :exclamation: Update underscore, lodash, xmldom ([#1177](https://github.com/zapier/zapier-platform/pull/1177))
|
||||
|
||||
## 3.8.18
|
||||
|
||||
_Released 2025-07-17_
|
||||
|
||||
- :bug: Make `sanitizeHeaders` middleware an optional import ([#1050](https://github.com/zapier/zapier-platform/pull/1050))
|
||||
|
||||
## 3.8.17
|
||||
|
||||
- :bug: Ensure `z.request` renders `{{curlies}}` on different zapier-platform-core versions ([#1037](https://github.com/zapier/zapier-platform/pull/1037))
|
||||
|
||||
## 3.8.16
|
||||
|
||||
- :bug: sync `z.request` should log `request_params` ([#1017](https://github.com/zapier/zapier-platform/pull/1017))
|
||||
|
||||
## 3.8.15
|
||||
|
||||
- :hammer: Trim newline and whitespaces from request header ([#1000](https://github.com/zapier/zapier-platform/pull/1000))
|
||||
|
||||
|
||||
## 3.8.14
|
||||
|
||||
- :bug: Revert `aws-sdk v2` bundling change from 3.8.13 ([#916](https://github.com/zapier/zapier-platform/pull/916)). This release is essentially the same as 3.8.12.
|
||||
|
||||
## 3.8.13
|
||||
|
||||
- :hammer: Add `aws-sdk v2` to dependency list ([#912](https://github.com/zapier/zapier-platform/pull/912))
|
||||
|
||||
## 3.8.11
|
||||
|
||||
- :bug: sync `z.request` doesn't produce an HTTP log ([#566](https://github.com/zapier/zapier-platform/pull/566))
|
||||
|
||||
## 3.8.10
|
||||
|
||||
- :bug: Add support for `StopRequestException` in more methods ([#561](https://github.com/zapier/zapier-platform/pull/561))
|
||||
|
||||
## 3.8.9
|
||||
|
||||
- :bug: `StopRequestException` should be caught instead of thrown ([#558](https://github.com/zapier/zapier-platform/pull/558))
|
||||
|
||||
## 3.8.8
|
||||
|
||||
- :bug: Make `logResponse` import backward compatible to fix `func.apply is not a function` error ([#548](https://github.com/zapier/zapier-platform/pull/548))
|
||||
|
||||
## 3.8.7
|
||||
|
||||
- :bug: Replace deasync with synckit to fix hanging ([#509](https://github.com/zapier/zapier-platform/pull/509))
|
||||
|
||||
## 3.8.6
|
||||
|
||||
- :bug: Fix issues with file uploading ([#496](https://github.com/zapier/zapier-platform/pull/496))
|
||||
- :nail_care: Pass `legacy.skipEncodingChars` to `z.request()` ([#501](https://github.com/zapier/zapier-platform/pull/501))
|
||||
|
||||
## 3.8.5
|
||||
|
||||
- :bug: Prune nulls and undefined's from query params ([#446](https://github.com/zapier/zapier-platform/pull/446))
|
||||
- :bug: Handle string array output from a scriptless create/action ([#447](https://github.com/zapier/zapier-platform/pull/447))
|
||||
- :bug: Encode `request.data` in `form-urlencoded` for `pre` methods ([#448](https://github.com/zapier/zapier-platform/pull/448))
|
||||
|
||||
## 3.8.4
|
||||
|
||||
- :bug: Make sure `redirect_uri` is available during `oauth2.refresh` ([#444](https://github.com/zapier/zapier-platform/pull/444))
|
||||
- :bug: Don't send auth with `z.dehydrateFile(url, {})` ([#445](https://github.com/zapier/zapier-platform/pull/445))
|
||||
|
||||
## 3.8.3
|
||||
|
||||
- :bug: Fix empty auth params when merging `request.url` and `reqeust.params` ([#441](https://github.com/zapier/zapier-platform/pull/441))
|
||||
|
||||
## 3.8.2
|
||||
|
||||
- :bug: Merge `request.url` to `request.params` before making a request ([#435](https://github.com/zapier/zapier-platform/pull/435))
|
||||
- :bug: Default to auth fields if auth mapping is empty ([#438](https://github.com/zapier/zapier-platform/pull/438))
|
||||
|
||||
## 3.8.1
|
||||
|
||||
- :bug: Add support for `z.reqeust({ json: true, body: {...} })` ([#418](https://github.com/zapier/zapier-platform/pull/418))
|
||||
- :bug: Empty `request.data` should send an empty request body instead of an empty object ([#423](https://github.com/zapier/zapier-platform/pull/423))
|
||||
- :bug: Fix duplicate headers with session auth ([#424](https://github.com/zapier/zapier-platform/pull/424))
|
||||
|
||||
## 3.8.0
|
||||
|
||||
- :tada: Include `isBulkRead` to `bundle.meta` ([#414](https://github.com/zapier/zapier-platform/pull/414))
|
||||
- :bug: Fix `Cannot read property 'errors' of undefined` on auth refresh ([#410](https://github.com/zapier/zapier-platform/pull/410))
|
||||
- :bug: Fix `JSON results array could not be located` when a trigger response is "null" ([#415](https://github.com/zapier/zapier-platform/pull/415))
|
||||
|
||||
## 3.7.17
|
||||
|
||||
- :bug: Allow `pre_write` method to "cancel" multipart body by emptying `request.files` ([#394](https://github.com/zapier/zapier-platform/pull/394))
|
||||
|
||||
## 3.7.16
|
||||
|
||||
- :bug: Allow `auth` to be an object for `z.request` ([#366](https://github.com/zapier/zapier-platform/pull/366))
|
||||
|
||||
## 3.7.15
|
||||
|
||||
- :bug: Fix another edge case that can lead to duplicate API Key headers ([#365](https://github.com/zapier/zapier-platform/pull/365))
|
||||
|
||||
## 3.7.14
|
||||
|
||||
- :bug: Fix case-insesitive duplicate auth headers when using "API Key in Header" ([#364](https://github.com/zapier/zapier-platform/pull/364))
|
||||
|
||||
## 3.7.13
|
||||
|
||||
- :bug: `inputData` should take precedence over `authData` when auth fields are not saved yet ([#359](https://github.com/zapier/zapier-platform/pull/359))
|
||||
|
||||
## 3.7.12
|
||||
|
||||
- :bug: Add `bundle.trigger_data` to `pre_subscribe` and `post_subscribe` ([#342](https://github.com/zapier/zapier-platform/pull/342))
|
||||
- :bug: Fix inconsistency with optional file fields ([#344](https://github.com/zapier/zapier-platform/pull/344))
|
||||
|
||||
## 3.7.11
|
||||
|
||||
- :bug: Yet-to-save auth fields should be in `bundle.auth_fields` ([#331](https://github.com/zapier/zapier-platform/pull/331))
|
||||
|
||||
## 3.7.10
|
||||
|
||||
- :bug: Fix `pre_oauthv2_token` and `pre_oauthv2_refresh` discrepancies ([#329](https://github.com/zapier/zapier-platform/pull/329))
|
||||
|
||||
## 3.7.9
|
||||
|
||||
- :bug: Throwing `ErrorException` should appear in an error log ([#309](https://github.com/zapier/zapier-platform/pull/309))
|
||||
|
||||
## 3.7.8
|
||||
|
||||
- :bug: Better choose between `ResponseError` and script error ([#305](https://github.com/zapier/zapier-platform/pull/305))
|
||||
|
||||
## 3.7.7
|
||||
|
||||
- :bug: Shouldn't double encode when `pre` method also encodes the URL ([#304](https://github.com/zapier/zapier-platform/pull/304))
|
||||
|
||||
## 3.7.6
|
||||
|
||||
- :bug: `bundle.request.data` should always be an object in `pre_oauthv2_refresh` ([#294](https://github.com/zapier/zapier-platform/pull/294))
|
||||
|
||||
## 3.7.5
|
||||
|
||||
- :bug: Make response headers case-insensitive ([#253](https://github.com/zapier/zapier-platform/pull/253))
|
||||
- :bug: `bundle.request.data` should be undefined in `pre_custom_action_fields` ([#256](https://github.com/zapier/zapier-platform/pull/256))
|
||||
|
||||
## 3.7.4
|
||||
|
||||
- :bug: Fix env vars in curlies not resolving properly ([#237](https://github.com/zapier/zapier-platform/pull/237))
|
||||
|
||||
## 3.7.3
|
||||
|
||||
- :bug: Encode URL before sending a request ([#235](https://github.com/zapier/zapier-platform/pull/235))
|
||||
- :hammer: Refactor tests to use Zapier-hosted httpbin ([#228](https://github.com/zapier/zapier-platform/pull/228))
|
||||
- :hammer: Fix tests where inline function source isn't compiled ([#226](https://github.com/zapier/zapier-platform/pull/226))
|
||||
|
||||
## 3.7.2
|
||||
|
||||
- :bug: Prune body if `allowGetBody` and body is empty ([#224](https://github.com/zapier/zapier-platform/pull/224))
|
||||
|
||||
## 3.7.1
|
||||
|
||||
- :bug: Allow `post_read_resource` to return an array ([#219](https://github.com/zapier/zapier-platform/pull/219))
|
||||
- :hammer: Upgrade dependencies ([#218](https://github.com/zapier/zapier-platform/pull/218))
|
||||
|
||||
## 3.7.0
|
||||
|
||||
- :tada: More complete bundle logs ([#213](https://github.com/zapier/zapier-platform/pull/213))
|
||||
- :bug: Trim bloated bundles for `KEY_post` methods ([#213](https://github.com/zapier/zapier-platform/pull/213))
|
||||
|
||||
## 3.6.0
|
||||
|
||||
- :nail_care: Allow GET requests to have body (requires zapier-platform-core 9.4.0+) ([#195](https://github.com/zapier/zapier-platform/pull/195))
|
||||
- :bug: More "reliably interpolate arrays or objects to a string" ([#203](https://github.com/zapier/zapier-platform/pull/203))
|
||||
- :hammer: Upgrade `request` package ([#196](https://github.com/zapier/zapier-platform/pull/196))
|
||||
|
||||
## 3.5.0
|
||||
|
||||
- :tada: Add support for unbounded curlies ([#194](https://github.com/zapier/zapier-platform/pull/194))
|
||||
|
||||
## 3.4.0
|
||||
|
||||
- :tada: Allow to "reliably interpolate arrays or objects to a string" ([#190](https://github.com/zapier/zapier-platform/pull/190))
|
||||
|
||||
## 3.3.3
|
||||
|
||||
- :bug: Run post method first before throwing error for response status ([#183](https://github.com/zapier/zapier-platform/pull/183))
|
||||
|
||||
## 3.3.2
|
||||
|
||||
- :bug: Fix missing `console.log` ([#182](https://github.com/zapier/zapier-platform/pull/182))
|
||||
|
||||
## 3.3.1
|
||||
|
||||
- :bug: Add full jQuery support ([#181](https://github.com/zapier/zapier-platform/pull/181))
|
||||
|
||||
## 3.3.0
|
||||
|
||||
- :tada: Log converted bundles ([#177](https://github.com/zapier/zapier-platform/pull/177))
|
||||
3
vendor/zapier-platform/packages/legacy-scripting-runner/LICENSE
vendored
Normal file
3
vendor/zapier-platform/packages/legacy-scripting-runner/LICENSE
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
Copyright (c) Zapier, Inc.
|
||||
|
||||
This repository is part of Zapier Platform. By downloading, installing, accessing, or using any part of the Zapier Platform, including this repository, you agree to the Zapier Platform Agreement, which can be found at: https://zapier.com/platform/tos. If you do not agree to the Zapier Platform Agreement, you may not download, install, access, or use any part of the Zapier Platform, including this repository.
|
||||
51
vendor/zapier-platform/packages/legacy-scripting-runner/README.md
vendored
Normal file
51
vendor/zapier-platform/packages/legacy-scripting-runner/README.md
vendored
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
# Zapier Platform Legacy Runner
|
||||
|
||||
This is Zapier's Legacy Scripting Runner, used by Web Builder apps converted to [CLI](https://docs.zapier.com/platform).
|
||||
|
||||
This allows you to run methods from existing Scripting code in CLI, and handles the bundle conversion and method availability automatically for you.
|
||||
|
||||
You shouldn't have to install anything. This package will be required and installed by your app automatically, if necessary, after running `zapier-platform convert`. You only need [CLI installed](https://docs.zapier.com/platform) (i.e. `npm i -g zapier-platform-cli`).
|
||||
|
||||
## Steps
|
||||
|
||||
1. `zapier-platform convert <YOUR_APP_ID> <FOLDER_TO_PUT_APP_INTO>`
|
||||
2. `cd <FOLDER_TO_PUT_APP_INTO> && npm install`
|
||||
3. `zapier-platform test`.
|
||||
|
||||
Note `scripting.js` should look very familiar to you.
|
||||
|
||||
## Known Limitations
|
||||
|
||||
- `z.cookie_jar` is unavailable (uncommon).
|
||||
- `bundle.zap` won't be filled out in most cases (CLI doesn't receive this information except for `performSubscribe` and `performUnsubscribe` in Hooks).
|
||||
|
||||
## Development
|
||||
|
||||
Note this section is intended for Zapier engineers, not for App Developers.
|
||||
|
||||
1. Clone this repo and run `npm link` inside of it;
|
||||
2. Convert an app;
|
||||
3. Inside the app run `npm link zapier-platform-legacy-scripting-runner`.
|
||||
|
||||
## Testing
|
||||
|
||||
`npm test` runs some unit tests, `npm run ci-test` pulls the "full-test" app and runs `zapier-platform test` in it.
|
||||
|
||||
## Releasing
|
||||
|
||||
1. Run `npm version [patch|minor|major]` to update the version in `package.json` and push a version tag to GitHub.
|
||||
2. Wait for [Travis](https://travis-ci.org/zapier/zapier-platform-legacy-scripting-runner) to publish the package to npm.
|
||||
|
||||
## Test Repos
|
||||
|
||||
Some repos might be private. They might have more instructions in their own READMEs.
|
||||
|
||||
This is the "full test" for the legacy-scripting-runner:
|
||||
|
||||
- [Full Test](https://github.com/zapier/zapier-platform-app-converted-full-test)
|
||||
|
||||
A few converted sample apps (proofs of concept):
|
||||
|
||||
- [Google Maps](https://github.com/zapier/zapier-platform-app-converted-google-maps)
|
||||
- [Instapaper](https://github.com/zapier/zapier-platform-app-converted-instapaper)
|
||||
- [Remember The Milk](https://github.com/zapier/zapier-platform-app-converted-remember-the-milk)
|
||||
5
vendor/zapier-platform/packages/legacy-scripting-runner/atob.js
vendored
Normal file
5
vendor/zapier-platform/packages/legacy-scripting-runner/atob.js
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
'use strict';
|
||||
const atob = (string) => {
|
||||
return Buffer.from(string, 'base64').toString('binary');
|
||||
};
|
||||
module.exports = atob;
|
||||
5
vendor/zapier-platform/packages/legacy-scripting-runner/btoa.js
vendored
Normal file
5
vendor/zapier-platform/packages/legacy-scripting-runner/btoa.js
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
'use strict';
|
||||
const btoa = (string) => {
|
||||
return Buffer.from(string, 'binary').toString('base64');
|
||||
};
|
||||
module.exports = btoa;
|
||||
310
vendor/zapier-platform/packages/legacy-scripting-runner/bundle.js
vendored
Normal file
310
vendor/zapier-platform/packages/legacy-scripting-runner/bundle.js
vendored
Normal file
|
|
@ -0,0 +1,310 @@
|
|||
const _ = require('lodash');
|
||||
|
||||
const { isFileField, hasFileFields, LazyFile } = require('./file');
|
||||
|
||||
// Max parts a key can have for unflattening
|
||||
const MAX_KEY_PARTS = 6;
|
||||
|
||||
// Replace '{{bundle.inputData.abc}}' with '{{abc}}'
|
||||
const undoCurlyReplacement = (str) =>
|
||||
str ? str.replace(/{{\s*bundle\.[^.]+\.([^}\s]+)\s*}}/g, '{{$1}}') : str;
|
||||
|
||||
// Unflatten from {key__child: value} to {key: {child: value}}
|
||||
const unflattenObject = (data, separator = '__') => {
|
||||
if (Object(data) !== data || _.isArray(data)) {
|
||||
return data;
|
||||
}
|
||||
|
||||
const keys = Object.keys(data);
|
||||
|
||||
_.each(keys, (key) => {
|
||||
if (
|
||||
key.includes(separator) &&
|
||||
!key.startsWith(separator) &&
|
||||
!key.endsWith(separator)
|
||||
) {
|
||||
const value = data[key];
|
||||
const keyParts = key.split(separator, MAX_KEY_PARTS);
|
||||
|
||||
let i;
|
||||
let previousProp = data;
|
||||
let currentProp = data;
|
||||
|
||||
for (i = 0; i < keyParts.length; i++) {
|
||||
currentProp[keyParts[i]] = currentProp[keyParts[i]] || {};
|
||||
previousProp = currentProp;
|
||||
currentProp = currentProp[keyParts[i]];
|
||||
}
|
||||
|
||||
previousProp[keyParts[i - 1]] = value;
|
||||
|
||||
delete data[key];
|
||||
}
|
||||
});
|
||||
|
||||
return data;
|
||||
};
|
||||
|
||||
const convertToQueryString = (object) => {
|
||||
if (!object) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const objectKeys = Object.keys(object);
|
||||
const queryStringItems = _.map(objectKeys, (key) => `${key}=${object[key]}`);
|
||||
|
||||
return queryStringItems.join('&');
|
||||
};
|
||||
|
||||
//
|
||||
// Methods with logic for bundleConverter
|
||||
//
|
||||
|
||||
const addAuthData = (event, bundle, convertedBundle) => {
|
||||
// Get authData only for basic auth here, to include in the request
|
||||
const { username, password } = _.get(bundle, 'authData', {});
|
||||
|
||||
if (username && password) {
|
||||
convertedBundle.request.auth = [username, password];
|
||||
}
|
||||
|
||||
// OAuth2 specific
|
||||
if (event.name.startsWith('auth.oauth2')) {
|
||||
convertedBundle.oauth_data = {
|
||||
client_id: process.env.CLIENT_ID,
|
||||
client_secret: process.env.CLIENT_SECRET,
|
||||
};
|
||||
convertedBundle.load = _.get(bundle, 'request.body', {});
|
||||
}
|
||||
};
|
||||
|
||||
const addInputData = (event, bundle, convertedBundle) => {
|
||||
if (event.name === 'auth.connectionLabel') {
|
||||
convertedBundle.test_result = bundle.inputData;
|
||||
} else if (event.name.startsWith('trigger.')) {
|
||||
convertedBundle.trigger_fields = bundle.inputData;
|
||||
convertedBundle.trigger_fields_raw =
|
||||
bundle.inputDataRaw || bundle.inputData;
|
||||
} else if (event.name.startsWith('create.')) {
|
||||
convertedBundle.action_fields = bundle._unflatInputData;
|
||||
convertedBundle.action_fields_full = bundle.inputData;
|
||||
convertedBundle.action_fields_raw = bundle.inputDataRaw || bundle.inputData;
|
||||
} else if (event.name.startsWith('search.')) {
|
||||
convertedBundle.search_fields = bundle.inputData;
|
||||
|
||||
if (event.name.startsWith('search.resource')) {
|
||||
convertedBundle.read_fields = event.results || bundle.inputData;
|
||||
convertedBundle.read_context = bundle.inputData;
|
||||
}
|
||||
} else if (event.name === 'hydrate.method') {
|
||||
Object.assign(convertedBundle, bundle.inputData.bundle);
|
||||
} else if (event.name.startsWith('auth.oauth2.token')) {
|
||||
// Overwrite convertedBundle.auth_fields with bundle.inputData
|
||||
convertedBundle.auth_fields = Object.keys(
|
||||
convertedBundle.auth_fields,
|
||||
).reduce((result, k) => {
|
||||
result[k] = bundle.inputData[k] || convertedBundle.auth_fields[k];
|
||||
return result;
|
||||
}, {});
|
||||
}
|
||||
};
|
||||
|
||||
const addHookData = (event, bundle, convertedBundle) => {
|
||||
if (event.name === 'trigger.hook') {
|
||||
convertedBundle.request = bundle.rawRequest || convertedBundle.request;
|
||||
convertedBundle.cleaned_request = bundle.cleanedRequest;
|
||||
|
||||
if (!convertedBundle.request.querystring) {
|
||||
convertedBundle.request.querystring = convertToQueryString(
|
||||
bundle.inputData,
|
||||
);
|
||||
}
|
||||
if (!convertedBundle.request.content) {
|
||||
convertedBundle.request.content = convertedBundle.request.data || '';
|
||||
}
|
||||
} else if (event.name.startsWith('trigger.hook.subscribe')) {
|
||||
convertedBundle.target_url = bundle.targetUrl;
|
||||
convertedBundle.subscription_url = bundle.targetUrl;
|
||||
convertedBundle.event = bundle._legacyEvent;
|
||||
convertedBundle.trigger_data = bundle.inputData;
|
||||
} else if (event.name.startsWith('trigger.hook.unsubscribe')) {
|
||||
convertedBundle.target_url = bundle.targetUrl;
|
||||
convertedBundle.subscription_url = bundle.targetUrl;
|
||||
convertedBundle.subscribe_data = bundle.subscribeData;
|
||||
convertedBundle.event = bundle._legacyEvent;
|
||||
convertedBundle.trigger_data = bundle.inputData;
|
||||
}
|
||||
};
|
||||
|
||||
const addRequestData = async (event, z, bundle, convertedBundle) => {
|
||||
Object.assign(
|
||||
convertedBundle.request.headers,
|
||||
_.get(bundle, 'request.headers'),
|
||||
);
|
||||
|
||||
Object.assign(
|
||||
convertedBundle.request.params,
|
||||
_.get(bundle, 'request.params'),
|
||||
);
|
||||
|
||||
const body = _.get(bundle, 'request.body');
|
||||
if (!_.isEmpty(body)) {
|
||||
let data = body;
|
||||
let files;
|
||||
|
||||
if (typeof data !== 'string' && !event.name.startsWith('auth.oauth2')) {
|
||||
if (hasFileFields(bundle)) {
|
||||
// Exclude file fields from request.data
|
||||
data = Object.keys(body)
|
||||
.filter((k) => !isFileField(k, bundle))
|
||||
.reduce((result, k) => {
|
||||
result[k] = body[k];
|
||||
return result;
|
||||
}, {});
|
||||
|
||||
const fileFieldKeys = Object.keys(body).filter((k) =>
|
||||
isFileField(k, bundle),
|
||||
);
|
||||
const fileMetas = await Promise.all(
|
||||
fileFieldKeys.map((k) => LazyFile(body[k]).meta()),
|
||||
);
|
||||
|
||||
files = _.zip(fileFieldKeys, fileMetas)
|
||||
.map(([k, meta]) => {
|
||||
const urlOrContent = body[k];
|
||||
return [k, [meta.filename, urlOrContent, meta.contentType]];
|
||||
})
|
||||
.reduce((result, [k, file]) => {
|
||||
result[k] = file;
|
||||
return result;
|
||||
}, {});
|
||||
}
|
||||
|
||||
data = JSON.stringify(data);
|
||||
}
|
||||
|
||||
convertedBundle.request.data = data;
|
||||
|
||||
if (!_.isEmpty(files)) {
|
||||
convertedBundle.request.files = files;
|
||||
delete convertedBundle.request.headers['Content-Type'];
|
||||
}
|
||||
} else if (event.name.startsWith('auth.oauth2.refresh.pre')) {
|
||||
// Make sure bundle.request.data is an object
|
||||
convertedBundle.request.data = convertedBundle.request.data || {};
|
||||
} else if (event.name.startsWith('create')) {
|
||||
if (
|
||||
!_.isEmpty(bundle._unflatInputData) ||
|
||||
(!event.name.includes('.input') && !event.name.includes('.output'))
|
||||
) {
|
||||
// Only stringify an empty object when we aren't fetching custom fields
|
||||
convertedBundle.request.data = JSON.stringify(bundle._unflatInputData);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const addResponse = (event, bundle, convertedBundle) => {
|
||||
if (event.name.endsWith('.post')) {
|
||||
convertedBundle.response = { ...event.response };
|
||||
convertedBundle.response.status_code = convertedBundle.response.status;
|
||||
if (convertedBundle.response.request) {
|
||||
convertedBundle.response.request = {
|
||||
...convertedBundle.response.request,
|
||||
};
|
||||
// `request.input` contains the entire app definition, which is big and
|
||||
// unnecessary for legacy scripting
|
||||
delete convertedBundle.response.request.input;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const convertBundleMeta = (meta) => {
|
||||
if (_.isEmpty(meta)) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const newMeta = {
|
||||
auth_test:
|
||||
meta.auth_test === undefined ? meta.isTestingAuth : meta.auth_test,
|
||||
frontend:
|
||||
meta.frontend === undefined ? meta.isLoadingSample : meta.frontend,
|
||||
prefill:
|
||||
meta.prefill === undefined ? meta.isFillingDynamicDropdown : meta.prefill,
|
||||
hydrate: true,
|
||||
test_poll:
|
||||
meta.test_poll === undefined ? meta.isTestingAuth : meta.test_poll,
|
||||
first_poll:
|
||||
meta.first_poll === undefined ? meta.isPopulatingDedupe : meta.first_poll,
|
||||
limit: meta.limit,
|
||||
page: meta.page,
|
||||
isBulkRead: meta.isBulkRead || false,
|
||||
};
|
||||
newMeta.standard_poll = !newMeta.test_poll;
|
||||
return newMeta;
|
||||
};
|
||||
|
||||
// Convert bundle from CLI to WB based on which event to run
|
||||
const bundleConverter = async (bundle, event, z) => {
|
||||
let requestMethod = _.get(bundle, 'request.method');
|
||||
if (!requestMethod) {
|
||||
if (
|
||||
event.name.startsWith('create') ||
|
||||
event.name.startsWith('auth.oauth2') ||
|
||||
event.name.startsWith('trigger.hook.subscribe')
|
||||
) {
|
||||
requestMethod = 'POST';
|
||||
} else if (event.name === 'trigger.hook.unsubscribe.pre') {
|
||||
requestMethod = 'DELETE';
|
||||
} else {
|
||||
requestMethod = 'GET';
|
||||
}
|
||||
}
|
||||
|
||||
// From inputData, remove keys that are in fieldsExcludedFromBody
|
||||
const excludedFieldKeys = bundle._fieldsExcludedFromBody || [];
|
||||
const filteredInputData = Object.keys(bundle.inputData || {})
|
||||
.filter((key) => !excludedFieldKeys.includes(key))
|
||||
.reduce((fields, key) => {
|
||||
return { [key]: bundle.inputData[key], ...fields };
|
||||
}, {});
|
||||
|
||||
// Attach to bundle so we can reuse it
|
||||
bundle._unflatInputData = unflattenObject(filteredInputData);
|
||||
|
||||
const meta = convertBundleMeta(bundle.meta);
|
||||
const zap = _.get(bundle, 'meta.zap') || { id: 0 };
|
||||
|
||||
const convertedBundle = {
|
||||
request: {
|
||||
method: requestMethod,
|
||||
url: undoCurlyReplacement(_.get(bundle, 'request.url', '')),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
params: {},
|
||||
data: !requestMethod || requestMethod === 'GET' ? null : '',
|
||||
},
|
||||
auth_fields: _.get(bundle, 'authData', {}),
|
||||
meta,
|
||||
zap,
|
||||
};
|
||||
|
||||
if (bundle._legacyUrl) {
|
||||
convertedBundle.raw_url = convertedBundle.url_raw = undoCurlyReplacement(
|
||||
bundle._legacyUrl,
|
||||
);
|
||||
}
|
||||
|
||||
addAuthData(event, bundle, convertedBundle);
|
||||
addInputData(event, bundle, convertedBundle);
|
||||
await addRequestData(event, z, bundle, convertedBundle);
|
||||
addHookData(event, bundle, convertedBundle);
|
||||
addResponse(event, bundle, convertedBundle);
|
||||
|
||||
return convertedBundle;
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
bundleConverter,
|
||||
unflattenObject,
|
||||
};
|
||||
66
vendor/zapier-platform/packages/legacy-scripting-runner/exceptions.js
vendored
Normal file
66
vendor/zapier-platform/packages/legacy-scripting-runner/exceptions.js
vendored
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
'use strict';
|
||||
|
||||
const _ = require('lodash');
|
||||
const util = require('util');
|
||||
|
||||
class AppError extends Error {
|
||||
constructor(message, code, status) {
|
||||
super(
|
||||
JSON.stringify({
|
||||
message,
|
||||
code,
|
||||
status,
|
||||
}),
|
||||
);
|
||||
this.name = 'AppError';
|
||||
this.doNotContextify = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Make some of the errors we'll use!
|
||||
const createError = (name) => {
|
||||
const NewError = function (message) {
|
||||
this.name = name;
|
||||
this.message = message || '';
|
||||
Error.call(this);
|
||||
Error.captureStackTrace(this, this.constructor);
|
||||
};
|
||||
util.inherits(NewError, Error);
|
||||
return NewError;
|
||||
};
|
||||
|
||||
const names = [
|
||||
'HaltedError',
|
||||
'StopRequestError',
|
||||
'ExpiredAuthError',
|
||||
'RefreshAuthError',
|
||||
'DehydrateError',
|
||||
];
|
||||
|
||||
const cliErrors = _.reduce(
|
||||
names,
|
||||
(error, name) => {
|
||||
error[name] = createError(name);
|
||||
return error;
|
||||
},
|
||||
{
|
||||
Error: AppError,
|
||||
},
|
||||
);
|
||||
|
||||
const exceptions = {
|
||||
ErrorException: cliErrors.Error,
|
||||
HaltedException: cliErrors.HaltedError,
|
||||
StopRequestException: cliErrors.StopRequestError,
|
||||
ExpiredAuthException: cliErrors.ExpiredAuthError,
|
||||
RefreshTokenException: cliErrors.RefreshAuthError,
|
||||
InvalidSessionException: cliErrors.RefreshAuthError, // In CLI, RefreshAuthError works the same for both OAuth and Session
|
||||
DehydrateException: cliErrors.DehydrateError,
|
||||
};
|
||||
|
||||
const DEFINED_ERROR_NAMES = ['AppError', ...names];
|
||||
|
||||
module.exports = {
|
||||
...exceptions,
|
||||
DEFINED_ERROR_NAMES,
|
||||
};
|
||||
183
vendor/zapier-platform/packages/legacy-scripting-runner/file.js
vendored
Normal file
183
vendor/zapier-platform/packages/legacy-scripting-runner/file.js
vendored
Normal file
|
|
@ -0,0 +1,183 @@
|
|||
// A module handling file upload, file fields, and file scripting.
|
||||
|
||||
const urllib = require('url');
|
||||
|
||||
const _ = require('lodash');
|
||||
const fetch = require('node-fetch');
|
||||
|
||||
const markFileFieldsInBundle = (bundle, inputFields) => {
|
||||
const fileFieldKeys = inputFields
|
||||
.filter((field) => field.type === 'file')
|
||||
.map((field) => field.key);
|
||||
|
||||
if (fileFieldKeys.length > 0) {
|
||||
// Add it to bundle so that functions that don't have access to app
|
||||
// definition, such as bundleConverter, knows which fields are files
|
||||
bundle._fileFieldKeys = fileFieldKeys;
|
||||
}
|
||||
};
|
||||
|
||||
const hasFileFields = (bundle) => {
|
||||
return bundle._fileFieldKeys && bundle._fileFieldKeys.length > 0;
|
||||
};
|
||||
|
||||
const isFileField = (fieldKey, bundle) => {
|
||||
if (!bundle._fileFieldKeys) {
|
||||
return false;
|
||||
}
|
||||
return bundle._fileFieldKeys.indexOf(fieldKey) >= 0;
|
||||
};
|
||||
|
||||
const isAnyFileFieldSet = (bundle) => {
|
||||
const body = _.get(bundle, 'request.body');
|
||||
if (body && bundle._fileFieldKeys) {
|
||||
for (const k of bundle._fileFieldKeys) {
|
||||
if (body[k]) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const isUrl = (str) => {
|
||||
try {
|
||||
const parsed = new urllib.URL(str);
|
||||
return (
|
||||
(parsed.protocol === 'http:' || parsed.protocol === 'https:') &&
|
||||
parsed.hostname
|
||||
);
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const extractFilenameFromContent = (content) =>
|
||||
content.substr(0, 12).replace('.txt', '') + ' ... .txt';
|
||||
|
||||
const extractFilenameFromContentDisposition = (value) => {
|
||||
let filename = '';
|
||||
|
||||
// Follows RFC 6266
|
||||
const patterns = [
|
||||
// Example: "attachment; filename*= UTF-8''%e2%82%ac%20rates"
|
||||
/filename\*\s*=\s*[a-z0-9_-]+''(.*)(?:;|$)/gi,
|
||||
|
||||
// Example: 'INLINE; FILENAME= "an example.html"'
|
||||
/filename\s*=\s*"([^"]+)"/gi,
|
||||
|
||||
// Example: 'Attachment; filename=example.html'
|
||||
/filename\s*=\s*([^ ]+)/gi,
|
||||
];
|
||||
|
||||
for (const pattern of patterns) {
|
||||
const match = pattern.exec(value);
|
||||
if (match) {
|
||||
filename = match[1];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (filename) {
|
||||
filename = decodeURIComponent(filename);
|
||||
}
|
||||
|
||||
return filename;
|
||||
};
|
||||
|
||||
const extractFilenameFromUrl = (url) => {
|
||||
const pathname = new urllib.URL(url).pathname;
|
||||
if (pathname) {
|
||||
const parts = pathname.split('/');
|
||||
return parts[parts.length - 1] || '';
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
const downloadFile = async (url) => {
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Got ${response.statusText} when download file: ${url}`);
|
||||
}
|
||||
|
||||
const disposition = response.headers.get('content-disposition');
|
||||
const filename = disposition
|
||||
? extractFilenameFromContentDisposition(disposition)
|
||||
: extractFilenameFromUrl(response.url);
|
||||
const contentType =
|
||||
response.headers.get('content-type') || 'application/octet-stream';
|
||||
|
||||
const buffer = await response.buffer();
|
||||
|
||||
return {
|
||||
meta: { filename, contentType },
|
||||
content: buffer,
|
||||
};
|
||||
};
|
||||
|
||||
const ContentBackedLazyFile = (content, fileMeta) => {
|
||||
const meta = async () => {
|
||||
return {
|
||||
filename: fileMeta.filename || extractFilenameFromContent(content),
|
||||
contentType: fileMeta.contentType || 'text/plain',
|
||||
};
|
||||
};
|
||||
|
||||
// readStream is only used by FormData.append(). And FormData.append(key,
|
||||
// data, options) accepts a string for its `data` argument, so instead of
|
||||
// trying to make the string a readable stream, we can just return the
|
||||
// string here.
|
||||
const readStream = async () => content;
|
||||
|
||||
return { meta, readStream };
|
||||
};
|
||||
|
||||
const UrlBackedLazyFile = (url, fileMeta) => {
|
||||
const hasCompleteMeta = fileMeta.filename && fileMeta.contentType;
|
||||
|
||||
let cachedFile;
|
||||
const downloadFileWithCache = async (fileUrl) => {
|
||||
// Cache file so when we call LazyFile.meta or LazyFile.readStream, we
|
||||
// don't need to send an HTTP request again
|
||||
if (cachedFile) {
|
||||
return cachedFile;
|
||||
}
|
||||
|
||||
const file = await downloadFile(fileUrl);
|
||||
cachedFile = file;
|
||||
return file;
|
||||
};
|
||||
|
||||
const meta = async () => {
|
||||
if (hasCompleteMeta) {
|
||||
return fileMeta;
|
||||
}
|
||||
const file = await downloadFileWithCache(url);
|
||||
return _.extend(file.meta, fileMeta);
|
||||
};
|
||||
|
||||
const readStream = async () => {
|
||||
const file = await downloadFileWithCache(url);
|
||||
return file.content;
|
||||
};
|
||||
|
||||
return { meta, readStream };
|
||||
};
|
||||
|
||||
const LazyFile = (urlOrContent, fileMeta, options) => {
|
||||
fileMeta = fileMeta || {};
|
||||
options = options || {};
|
||||
|
||||
if (options.dontLoadUrl || !isUrl(urlOrContent)) {
|
||||
return ContentBackedLazyFile(urlOrContent, fileMeta);
|
||||
}
|
||||
return UrlBackedLazyFile(urlOrContent, fileMeta);
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
markFileFieldsInBundle,
|
||||
hasFileFields,
|
||||
isFileField,
|
||||
isAnyFileFieldSet,
|
||||
LazyFile,
|
||||
};
|
||||
1650
vendor/zapier-platform/packages/legacy-scripting-runner/index.js
vendored
Normal file
1650
vendor/zapier-platform/packages/legacy-scripting-runner/index.js
vendored
Normal file
File diff suppressed because it is too large
Load diff
331
vendor/zapier-platform/packages/legacy-scripting-runner/middleware-factory.js
vendored
Normal file
331
vendor/zapier-platform/packages/legacy-scripting-runner/middleware-factory.js
vendored
Normal file
|
|
@ -0,0 +1,331 @@
|
|||
const querystring = require('querystring');
|
||||
|
||||
const _ = require('lodash');
|
||||
|
||||
const isPrimitive = (x) => {
|
||||
const tipe = typeof x;
|
||||
return (
|
||||
tipe === 'string' || tipe === 'number' || tipe === 'boolean' || tipe == null
|
||||
);
|
||||
};
|
||||
|
||||
const renderTemplate = (templateString, context) => {
|
||||
// Security: Ensure templateString is a string and not user-controlled
|
||||
if (!isPrimitive(templateString)) {
|
||||
throw new Error('Template string must be a primitive');
|
||||
}
|
||||
|
||||
const re = /{{([^}]+)}}/g;
|
||||
|
||||
// _.template doesn't allow us to set defaults, so we need to make sure all the
|
||||
// variables in templateString are defined (as empty strings) in context
|
||||
const defaults = _.clone(context);
|
||||
let match = re.exec(templateString);
|
||||
while (match) {
|
||||
const key = match[1].trim();
|
||||
defaults[key] = '';
|
||||
match = re.exec(templateString);
|
||||
}
|
||||
const finalContext = _.defaults(_.clone(context), defaults);
|
||||
|
||||
const options = {
|
||||
interpolate: re,
|
||||
// Security: Disable code evaluation to prevent injection
|
||||
evaluate: false,
|
||||
escape: false,
|
||||
};
|
||||
|
||||
// Security: Use safe template compilation
|
||||
try {
|
||||
return _.template(templateString, options)(finalContext);
|
||||
} catch (error) {
|
||||
// Log template errors but don't expose internal details
|
||||
console.error('Template rendering error:', error.message);
|
||||
return templateString; // Return original string on error
|
||||
}
|
||||
};
|
||||
|
||||
const getLowerHeaders = (headers) =>
|
||||
Object.entries(headers).reduce((result, [k, v]) => {
|
||||
result[k.toLowerCase()] = v;
|
||||
return result;
|
||||
}, {});
|
||||
|
||||
const renderAuthMapping = (authMapping, authData) => {
|
||||
if (_.isEmpty(authMapping)) {
|
||||
return authData;
|
||||
}
|
||||
return Object.entries(authMapping).reduce((result, [k, v]) => {
|
||||
result[k] = renderTemplate(v, authData);
|
||||
return result;
|
||||
}, {});
|
||||
};
|
||||
|
||||
const applyAuthMappingInHeaders = (authMapping, req, authData) => {
|
||||
const rendered = renderAuthMapping(authMapping, authData);
|
||||
const lowerHeaders = getLowerHeaders(req.headers);
|
||||
Object.entries(rendered).forEach(([k, v]) => {
|
||||
const lowerKey = k.toLowerCase();
|
||||
if (!lowerHeaders[lowerKey]) {
|
||||
req.headers[k] = v;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const applyAuthMappingInQuerystring = (authMapping, req, authData) => {
|
||||
const rendered = renderAuthMapping(authMapping, authData);
|
||||
Object.entries(rendered).forEach(([k, v]) => {
|
||||
req.params[k] = req.params[k] || v;
|
||||
});
|
||||
};
|
||||
|
||||
const createBeforeRequest = (app) => {
|
||||
const authType = _.get(app, 'authentication.type');
|
||||
const authMapping = _.get(app, 'legacy.authentication.mapping');
|
||||
const placement = _.get(app, 'legacy.authentication.placement') || 'header';
|
||||
|
||||
const sessionAuthInHeader = (req, z, bundle) => {
|
||||
if (!_.isEmpty(bundle.authData)) {
|
||||
applyAuthMappingInHeaders(authMapping, req, bundle.authData);
|
||||
}
|
||||
return req;
|
||||
};
|
||||
|
||||
const sessionAuthInQuerystring = (req, z, bundle) => {
|
||||
if (!_.isEmpty(bundle.authData)) {
|
||||
applyAuthMappingInQuerystring(authMapping, req, bundle.authData);
|
||||
}
|
||||
return req;
|
||||
};
|
||||
|
||||
const sessionAuthInBoth = (req, z, bundle) => {
|
||||
if (!_.isEmpty(bundle.authData)) {
|
||||
applyAuthMappingInHeaders(authMapping, req, bundle.authData);
|
||||
applyAuthMappingInQuerystring(authMapping, req, bundle.authData);
|
||||
}
|
||||
return req;
|
||||
};
|
||||
|
||||
const getGrantType = (req) => {
|
||||
const grantType = _.get(req, 'params.grant_type');
|
||||
if (grantType) {
|
||||
return grantType;
|
||||
}
|
||||
|
||||
const contentType = req.headers['Content-Type'] || '';
|
||||
if (contentType.includes('application/json')) {
|
||||
try {
|
||||
return JSON.parse(req.body).grant_type;
|
||||
} catch (err) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return querystring.parse(req.body).grant_type;
|
||||
};
|
||||
|
||||
const oauth2InHeader = (req, z, bundle) => {
|
||||
if (bundle.authData.access_token && getGrantType(req) !== 'refresh_token') {
|
||||
req.headers.Authorization =
|
||||
req.headers.Authorization || `Bearer ${bundle.authData.access_token}`;
|
||||
}
|
||||
return req;
|
||||
};
|
||||
|
||||
const oauth2InQuerystring = (req, z, bundle) => {
|
||||
if (bundle.authData.access_token && getGrantType(req) !== 'refresh_token') {
|
||||
req.params.access_token =
|
||||
req.params.access_token || bundle.authData.access_token;
|
||||
}
|
||||
return req;
|
||||
};
|
||||
|
||||
const oauth2InBoth = (req, z, bundle) => {
|
||||
if (bundle.authData.access_token && getGrantType(req) !== 'refresh_token') {
|
||||
req.headers.Authorization =
|
||||
req.headers.Authorization || `Bearer ${bundle.authData.access_token}`;
|
||||
req.params.access_token =
|
||||
req.params.access_token || bundle.authData.access_token;
|
||||
}
|
||||
return req;
|
||||
};
|
||||
|
||||
const apiKeyInHeader = (req, z, bundle) => {
|
||||
if (!_.isEmpty(bundle.authData)) {
|
||||
applyAuthMappingInHeaders(authMapping, req, bundle.authData);
|
||||
}
|
||||
return req;
|
||||
};
|
||||
|
||||
const apiKeyInQuerystring = (req, z, bundle) => {
|
||||
if (!_.isEmpty(bundle.authData)) {
|
||||
applyAuthMappingInQuerystring(authMapping, req, bundle.authData);
|
||||
}
|
||||
return req;
|
||||
};
|
||||
|
||||
const basicDigestAuth = (req, z, bundle) => {
|
||||
if (!bundle._legacyBasicDigestAuthMiddlewareApplied) {
|
||||
const username = renderTemplate(
|
||||
authMapping.username || '',
|
||||
bundle.authData,
|
||||
);
|
||||
const password = renderTemplate(
|
||||
authMapping.password || '',
|
||||
bundle.authData,
|
||||
);
|
||||
bundle.authData.username = username;
|
||||
bundle.authData.password = password;
|
||||
bundle._legacyBasicDigestAuthMiddlewareApplied = true;
|
||||
}
|
||||
return req;
|
||||
};
|
||||
|
||||
const oauth1 = (req, z, bundle) => {
|
||||
if (
|
||||
bundle.authData &&
|
||||
bundle.authData.oauth_token &&
|
||||
bundle.authData.oauth_token_secret
|
||||
) {
|
||||
req.auth = req.auth || {};
|
||||
|
||||
let templateContext;
|
||||
|
||||
if (!req.auth.oauth_consumer_key) {
|
||||
templateContext = Object.assign({}, bundle.authData, bundle.inputData);
|
||||
req.auth.oauth_consumer_key = renderTemplate(
|
||||
process.env.CLIENT_ID,
|
||||
templateContext,
|
||||
);
|
||||
}
|
||||
if (!req.auth.oauth_consumer_secret) {
|
||||
if (!templateContext) {
|
||||
templateContext = Object.assign(
|
||||
{},
|
||||
bundle.authData,
|
||||
bundle.inputData,
|
||||
);
|
||||
}
|
||||
req.auth.oauth_consumer_secret = renderTemplate(
|
||||
process.env.CLIENT_SECRET,
|
||||
templateContext,
|
||||
);
|
||||
}
|
||||
|
||||
req.auth.oauth_token =
|
||||
req.auth.oauth_token || bundle.authData.oauth_token;
|
||||
req.auth.oauth_token_secret =
|
||||
req.auth.oauth_token_secret || bundle.authData.oauth_token_secret;
|
||||
}
|
||||
return req;
|
||||
};
|
||||
|
||||
let authBefore;
|
||||
|
||||
if (authType === 'session') {
|
||||
authBefore = {
|
||||
header: sessionAuthInHeader,
|
||||
querystring: sessionAuthInQuerystring,
|
||||
both: sessionAuthInBoth,
|
||||
}[placement];
|
||||
} else if (authType === 'oauth2') {
|
||||
authBefore = {
|
||||
header: oauth2InHeader,
|
||||
querystring: oauth2InQuerystring,
|
||||
both: oauth2InBoth,
|
||||
}[placement];
|
||||
} else if (authType === 'custom') {
|
||||
authBefore = {
|
||||
header: apiKeyInHeader,
|
||||
querystring: apiKeyInQuerystring,
|
||||
}[placement];
|
||||
} else if (authType === 'basic' || authType === 'digest') {
|
||||
authBefore = basicDigestAuth;
|
||||
} else if (authType === 'oauth1') {
|
||||
authBefore = oauth1;
|
||||
}
|
||||
|
||||
if (!authBefore) {
|
||||
authBefore = (req) => req;
|
||||
}
|
||||
|
||||
const pruneEmptyBodyForGET = (req, z, bundle) => {
|
||||
if (req.allowGetBody && req.method === 'GET') {
|
||||
const contentType = req.headers['Content-Type'] || '';
|
||||
try {
|
||||
const parsedBody = contentType.includes('application/json')
|
||||
? JSON.parse(req.body)
|
||||
: req.body;
|
||||
if (_.isEmpty(parsedBody)) {
|
||||
delete req.body;
|
||||
}
|
||||
} catch (err) {
|
||||
// Ignore
|
||||
}
|
||||
}
|
||||
return req;
|
||||
};
|
||||
|
||||
return (req, z, bundle) => {
|
||||
req = authBefore(req, z, bundle);
|
||||
return pruneEmptyBodyForGET(req, z, bundle);
|
||||
};
|
||||
};
|
||||
|
||||
const proxyHeaders = (headers) => {
|
||||
const proxy = {
|
||||
get: (target, prop) => {
|
||||
const original = Reflect.get(target, prop);
|
||||
if (typeof original === 'function' || typeof original === 'symbol') {
|
||||
// defaults to defined functions on the Headers class; the symbol type
|
||||
// is used for accessing an internal map
|
||||
return original;
|
||||
}
|
||||
try {
|
||||
// try to retrieve the header via the get() function
|
||||
return target.get(prop);
|
||||
} catch {
|
||||
// otherwise, default to original target[prop] value
|
||||
return original;
|
||||
}
|
||||
},
|
||||
};
|
||||
return new Proxy(headers, proxy);
|
||||
};
|
||||
|
||||
const createAfterResponse = (app) => {
|
||||
const authType = _.get(app, 'authentication.type');
|
||||
const autoRefresh = _.get(app, 'authentication.oauth2Config.autoRefresh');
|
||||
|
||||
const throwForStaleAuth = (response, z) => {
|
||||
if (response.status === 401) {
|
||||
throw new z.errors.RefreshAuthError('Authentication needs refreshing');
|
||||
}
|
||||
return response;
|
||||
};
|
||||
|
||||
const makeHeaderCaseInsensitive = (response, z) => {
|
||||
response.headers = proxyHeaders(response.headers);
|
||||
return response;
|
||||
};
|
||||
|
||||
let afterResponse;
|
||||
|
||||
if (authType === 'session' || (authType === 'oauth2' && autoRefresh)) {
|
||||
afterResponse = throwForStaleAuth;
|
||||
}
|
||||
|
||||
if (!afterResponse) {
|
||||
afterResponse = (response) => response;
|
||||
}
|
||||
|
||||
return (response, z, bundle) => {
|
||||
response = afterResponse(response, z, bundle);
|
||||
return makeHeaderCaseInsensitive(response);
|
||||
};
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
createBeforeRequest,
|
||||
createAfterResponse,
|
||||
renderTemplate,
|
||||
};
|
||||
52
vendor/zapier-platform/packages/legacy-scripting-runner/package.json
vendored
Normal file
52
vendor/zapier-platform/packages/legacy-scripting-runner/package.json
vendored
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
{
|
||||
"name": "zapier-platform-legacy-scripting-runner",
|
||||
"version": "4.0.8",
|
||||
"description": "Zapier's Legacy Scripting Runner, used by Web Builder apps converted to CLI.",
|
||||
"repository": "zapier/zapier-platform",
|
||||
"homepage": "https://platform.zapier.com/",
|
||||
"author": "Zapier Engineering <contact@zapier.com>",
|
||||
"license": "SEE LICENSE IN LICENSE",
|
||||
"main": "index.js",
|
||||
"files": [
|
||||
"/*.js"
|
||||
],
|
||||
"scripts": {
|
||||
"lint": "eslint .",
|
||||
"test": "CLIENT_ID=1234 CLIENT_SECRET=asdf AWS_ACCESS_KEY_ID=fake AWS_SECRET_ACCESS_KEY=fake mocha --recursive -t 20s --exit",
|
||||
"test:debug": "CLIENT_ID=1234 CLIENT_SECRET=asdf AWS_ACCESS_KEY_ID=fake AWS_SECRET_ACCESS_KEY=fake mocha inspect --recursive -t 20s --exit",
|
||||
"preversion": "git pull",
|
||||
"postversion": "git push && git push --tags",
|
||||
"validate": "pnpm test && pnpm lint"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.10.0",
|
||||
"npm": ">=5.6.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"async": "2.6.4",
|
||||
"flat": "5.0.2",
|
||||
"form-data": "2.3.3",
|
||||
"jquery": "3.5.0",
|
||||
"jsdom": "7.0.0",
|
||||
"lodash": "4.18.1",
|
||||
"moment-timezone": "0.5.43",
|
||||
"node-fetch": "2.6.7",
|
||||
"request": "2.88.2",
|
||||
"synckit": "0.6.0",
|
||||
"underscore": "1.13.8",
|
||||
"@xmldom/xmldom": "0.8.13"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"zapier-platform-core": ">=14"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"zapier-platform-core": {
|
||||
"optional": true
|
||||
}
|
||||
},
|
||||
"devDependencies": {
|
||||
"aws-sdk": "^2.1397.0",
|
||||
"nock": "13.3.1",
|
||||
"zapier-platform-core": ">=7.6.0"
|
||||
}
|
||||
}
|
||||
22
vendor/zapier-platform/packages/legacy-scripting-runner/request-worker.js
vendored
Normal file
22
vendor/zapier-platform/packages/legacy-scripting-runner/request-worker.js
vendored
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
const { promisify } = require('util');
|
||||
|
||||
const request = require('request');
|
||||
const { runAsWorker } = require('synckit');
|
||||
|
||||
const { isMainThread } = require('worker_threads');
|
||||
|
||||
if (!isMainThread) {
|
||||
const asyncRequest = promisify(request);
|
||||
|
||||
runAsWorker(async (options) => {
|
||||
const response = await asyncRequest(options);
|
||||
|
||||
// Remove unnecessary fields as the result will serialized by
|
||||
// https://developer.mozilla.org/docs/Web/API/Web_Workers_API/Structured_clone_algorithm
|
||||
return {
|
||||
statusCode: response.statusCode,
|
||||
headers: { ...response.headers },
|
||||
body: response.body,
|
||||
};
|
||||
});
|
||||
}
|
||||
1227
vendor/zapier-platform/packages/legacy-scripting-runner/test/bundle.js
vendored
Normal file
1227
vendor/zapier-platform/packages/legacy-scripting-runner/test/bundle.js
vendored
Normal file
File diff suppressed because it is too large
Load diff
11
vendor/zapier-platform/packages/legacy-scripting-runner/test/constants.js
vendored
Normal file
11
vendor/zapier-platform/packages/legacy-scripting-runner/test/constants.js
vendored
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
const AUTH_JSON_SERVER_URL =
|
||||
process.env.AUTH_JSON_SERVER_URL ||
|
||||
'https://auth-json-server.zapier-staging.com';
|
||||
|
||||
const HTTPBIN_URL =
|
||||
process.env.HTTPBIN_URL || 'https://httpbin.zapier-tooling.com';
|
||||
|
||||
module.exports = {
|
||||
AUTH_JSON_SERVER_URL,
|
||||
HTTPBIN_URL,
|
||||
};
|
||||
35
vendor/zapier-platform/packages/legacy-scripting-runner/test/example-app/api-key-auth.js
vendored
Normal file
35
vendor/zapier-platform/packages/legacy-scripting-runner/test/example-app/api-key-auth.js
vendored
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
const { AUTH_JSON_SERVER_URL } = require('../constants');
|
||||
|
||||
const testAuthSource = `
|
||||
const responsePromise = z.request({
|
||||
url: '${AUTH_JSON_SERVER_URL}/me'
|
||||
});
|
||||
return responsePromise.then(response => {
|
||||
if (response.status !== 200) {
|
||||
throw new Error('Auth failed');
|
||||
}
|
||||
return z.JSON.parse(response.content);
|
||||
});
|
||||
`;
|
||||
|
||||
module.exports = {
|
||||
legacy: {
|
||||
authentication: {
|
||||
mapping: { 'X-Api-Key': '{{api_key}}' },
|
||||
placement: 'header',
|
||||
},
|
||||
},
|
||||
|
||||
authentication: {
|
||||
type: 'custom',
|
||||
test: { source: testAuthSource },
|
||||
fields: [
|
||||
{
|
||||
key: 'api_key',
|
||||
label: 'API Key',
|
||||
type: 'string',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
1509
vendor/zapier-platform/packages/legacy-scripting-runner/test/example-app/index.js
vendored
Normal file
1509
vendor/zapier-platform/packages/legacy-scripting-runner/test/example-app/index.js
vendored
Normal file
File diff suppressed because it is too large
Load diff
60
vendor/zapier-platform/packages/legacy-scripting-runner/test/example-app/oauth2.js
vendored
Normal file
60
vendor/zapier-platform/packages/legacy-scripting-runner/test/example-app/oauth2.js
vendored
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
const { AUTH_JSON_SERVER_URL } = require('../constants');
|
||||
|
||||
const testAuthSource = `
|
||||
const responsePromise = z.request({
|
||||
url: '${AUTH_JSON_SERVER_URL}/me'
|
||||
});
|
||||
return responsePromise.then(response => {
|
||||
if (response.status !== 200) {
|
||||
throw new Error('Auth failed');
|
||||
}
|
||||
return z.JSON.parse(response.content);
|
||||
});
|
||||
`;
|
||||
|
||||
const getAuthorizeUrlSource = `
|
||||
return z.legacyScripting.run(bundle, 'auth.oauth2.authorize');
|
||||
`;
|
||||
|
||||
const getAccessTokenSource = `
|
||||
return z.legacyScripting.run(bundle, 'auth.oauth2.token');
|
||||
`;
|
||||
|
||||
const refreshAccessTokenSource = `
|
||||
return z.legacyScripting.run(bundle, 'auth.oauth2.refresh');
|
||||
`;
|
||||
|
||||
module.exports = {
|
||||
legacy: {
|
||||
authentication: {
|
||||
placement: 'header',
|
||||
mapping: {},
|
||||
},
|
||||
},
|
||||
authentication: {
|
||||
type: 'oauth2',
|
||||
test: { source: testAuthSource },
|
||||
fields: [
|
||||
// No need to define access_token and refresh_token here, they will be
|
||||
// added automatically by the backend
|
||||
{
|
||||
key: 'something_custom',
|
||||
type: 'string',
|
||||
required: true,
|
||||
computed: true,
|
||||
},
|
||||
],
|
||||
oauth2Config: {
|
||||
authorizeUrl: {
|
||||
source: getAuthorizeUrlSource,
|
||||
},
|
||||
getAccessToken: {
|
||||
source: getAccessTokenSource,
|
||||
},
|
||||
refreshAccessToken: {
|
||||
source: refreshAccessTokenSource,
|
||||
},
|
||||
autoRefresh: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
47
vendor/zapier-platform/packages/legacy-scripting-runner/test/example-app/session-auth.js
vendored
Normal file
47
vendor/zapier-platform/packages/legacy-scripting-runner/test/example-app/session-auth.js
vendored
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
'use strict';
|
||||
|
||||
const testAuthSource = `
|
||||
return z.legacyScripting.run(bundle, 'trigger', 'contact_full');
|
||||
`;
|
||||
|
||||
const getSessionKeySource = `
|
||||
return z.legacyScripting.run(bundle, 'auth.session');
|
||||
`;
|
||||
|
||||
const getConnectionLabelSource = `
|
||||
return z.legacyScripting.run(bundle, 'auth.connectionLabel');
|
||||
`;
|
||||
|
||||
module.exports = {
|
||||
legacy: {
|
||||
authentication: {
|
||||
mapping: {
|
||||
'X-Api-Key': '{{key1}}{{key2}}',
|
||||
},
|
||||
placement: 'header',
|
||||
},
|
||||
testTrigger: 'contact_full',
|
||||
},
|
||||
authentication: {
|
||||
type: 'session',
|
||||
test: { source: testAuthSource },
|
||||
fields: [
|
||||
{
|
||||
key: 'username',
|
||||
label: 'Username',
|
||||
type: 'string',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
key: 'password',
|
||||
label: 'Password',
|
||||
type: 'password',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
sessionConfig: {
|
||||
perform: { source: getSessionKeySource },
|
||||
},
|
||||
connectionLabel: { source: getConnectionLabelSource },
|
||||
},
|
||||
};
|
||||
4939
vendor/zapier-platform/packages/legacy-scripting-runner/test/integration-test.js
vendored
Normal file
4939
vendor/zapier-platform/packages/legacy-scripting-runner/test/integration-test.js
vendored
Normal file
File diff suppressed because it is too large
Load diff
34
vendor/zapier-platform/packages/legacy-scripting-runner/test/libraries.js
vendored
Normal file
34
vendor/zapier-platform/packages/legacy-scripting-runner/test/libraries.js
vendored
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
require('should');
|
||||
|
||||
describe('Utils/Libraries', () => {
|
||||
it('btoa', (done) => {
|
||||
const btoa = require('../btoa');
|
||||
const result = btoa('something');
|
||||
result.should.equal('c29tZXRoaW5n');
|
||||
done();
|
||||
});
|
||||
|
||||
it('atob', (done) => {
|
||||
const atob = require('../atob');
|
||||
const result = atob('c29tZXRoaW5n');
|
||||
result.should.equal('something');
|
||||
done();
|
||||
});
|
||||
|
||||
describe('$', () => {
|
||||
const $ = require('../$');
|
||||
|
||||
it('$.param()', (done) => {
|
||||
const result = $.param({ test: 'something', more: true, also: '@' });
|
||||
result.should.equal('test=something&more=true&also=%40');
|
||||
done();
|
||||
});
|
||||
|
||||
it('$.parseXML()', (done) => {
|
||||
const xml = $.parseXML('<do><something>also</something></do>');
|
||||
const result = xml.getElementsByTagName('do').length;
|
||||
result.should.equal(1);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
88
vendor/zapier-platform/packages/legacy-scripting-runner/test/runner.js
vendored
Normal file
88
vendor/zapier-platform/packages/legacy-scripting-runner/test/runner.js
vendored
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
const should = require('should');
|
||||
|
||||
const scriptingRunner = require('../index');
|
||||
|
||||
describe('scriptingRunner', () => {
|
||||
const defaultBundle = {
|
||||
_legacyUrl: 'https://zapier.com',
|
||||
inputData: {
|
||||
user: 'Zapier',
|
||||
},
|
||||
authData: {
|
||||
apiKey: 'Zapier-API-Key',
|
||||
},
|
||||
meta: {
|
||||
frontend: false,
|
||||
prefill: false,
|
||||
},
|
||||
};
|
||||
|
||||
const z = {
|
||||
request: () => {},
|
||||
};
|
||||
|
||||
it('should return nothing if there is no scripting', (done) => {
|
||||
const event = {
|
||||
name: 'trigger.poll',
|
||||
key: 'trigger',
|
||||
response: {
|
||||
status: 200,
|
||||
content: '[{"id": 1, "name": "Zapier"}]',
|
||||
},
|
||||
};
|
||||
const bundle = defaultBundle;
|
||||
const Zap = {};
|
||||
|
||||
const legacyScriptingRunner = scriptingRunner(Zap);
|
||||
|
||||
legacyScriptingRunner
|
||||
.runEvent(event, z, bundle)
|
||||
.then((result) => {
|
||||
should(result).eql(undefined);
|
||||
done();
|
||||
})
|
||||
.catch(done);
|
||||
});
|
||||
|
||||
it('should return nothing if there is no event', (done) => {
|
||||
const event = {};
|
||||
const bundle = defaultBundle;
|
||||
const Zap = {
|
||||
trigger_poll: () => true,
|
||||
};
|
||||
|
||||
const legacyScriptingRunner = scriptingRunner(Zap);
|
||||
|
||||
legacyScriptingRunner
|
||||
.runEvent(event, z, bundle)
|
||||
.then((result) => {
|
||||
should(result).eql(undefined);
|
||||
done();
|
||||
})
|
||||
.catch(done);
|
||||
});
|
||||
|
||||
it('should return nothing if there is no event.name', (done) => {
|
||||
const event = {
|
||||
key: 'trigger',
|
||||
response: {
|
||||
status: 200,
|
||||
content: '[{"id": 1, "name": "Zapier"}]',
|
||||
},
|
||||
};
|
||||
const bundle = defaultBundle;
|
||||
const Zap = {
|
||||
trigger_poll: () => true,
|
||||
};
|
||||
|
||||
const legacyScriptingRunner = scriptingRunner(Zap);
|
||||
|
||||
legacyScriptingRunner
|
||||
.runEvent(event, z, bundle)
|
||||
.then((result) => {
|
||||
should(result).eql(undefined);
|
||||
done();
|
||||
})
|
||||
.catch(done);
|
||||
});
|
||||
});
|
||||
59
vendor/zapier-platform/packages/legacy-scripting-runner/test/template-security.js
vendored
Normal file
59
vendor/zapier-platform/packages/legacy-scripting-runner/test/template-security.js
vendored
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
const { renderTemplate } = require('../middleware-factory');
|
||||
|
||||
describe('middleware renderTemplate security', () => {
|
||||
it('should handle normal template rendering', () => {
|
||||
const context = { clientId: 'test123', secret: 'mysecret' };
|
||||
const template = 'Client: {{clientId}}, Secret: {{secret}}';
|
||||
|
||||
const result = renderTemplate(template, context);
|
||||
result.should.equal('Client: test123, Secret: mysecret');
|
||||
});
|
||||
|
||||
it('should prevent code injection in middleware templates', () => {
|
||||
const context = {
|
||||
clientId: 'test123',
|
||||
malicious: 'process.exit(1)',
|
||||
};
|
||||
|
||||
// This should NOT execute the malicious code
|
||||
const result = renderTemplate(
|
||||
'ID: {{clientId}}, Value: {{malicious}}',
|
||||
context,
|
||||
);
|
||||
result.should.equal('ID: test123, Value: process.exit(1)');
|
||||
});
|
||||
|
||||
it('should handle non-string template input safely in middleware', () => {
|
||||
const context = { test: 'value' };
|
||||
|
||||
(() => {
|
||||
renderTemplate({}, context);
|
||||
}).should.throw('Template string must be a primitive');
|
||||
|
||||
(() => {
|
||||
renderTemplate([], context);
|
||||
}).should.throw('Template string must be a primitive');
|
||||
|
||||
(() => {
|
||||
renderTemplate(() => {
|
||||
console.log('do evil stuff');
|
||||
}, context);
|
||||
}).should.throw('Template string must be a primitive');
|
||||
});
|
||||
|
||||
it('should handle template errors gracefully in middleware', () => {
|
||||
const context = { name: 'John' };
|
||||
|
||||
// Malformed template should return original string, not crash
|
||||
const result = renderTemplate('{{unclosed', context);
|
||||
result.should.equal('{{unclosed');
|
||||
});
|
||||
|
||||
it('should handle undefined variables with defaults', () => {
|
||||
const context = { name: 'John' };
|
||||
|
||||
// renderTemplate sets undefined vars to empty string
|
||||
const result = renderTemplate('{{name}} {{undefined_var}}', context);
|
||||
result.should.equal('John ');
|
||||
});
|
||||
});
|
||||
122
vendor/zapier-platform/packages/legacy-scripting-runner/test/zfactory.js
vendored
Normal file
122
vendor/zapier-platform/packages/legacy-scripting-runner/test/zfactory.js
vendored
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
const should = require('should');
|
||||
|
||||
const z = require('../zfactory')();
|
||||
const { HTTPBIN_URL } = require('./constants');
|
||||
|
||||
describe('z', () => {
|
||||
it('z.hash', (done) => {
|
||||
const result = z.hash('sha256', 'my awesome string');
|
||||
result.should.equal(
|
||||
'97f13a1635524dd41daca6601e5d9fe07e10e62790851e527b039851b1f8b9a1',
|
||||
);
|
||||
done();
|
||||
});
|
||||
|
||||
it('z.hmac', (done) => {
|
||||
const result = z.hmac('sha1', 'secret', 'signme');
|
||||
result.should.equal('f67a0be1fa49a3f1dbd659726d8983b838ee6e7d');
|
||||
done();
|
||||
});
|
||||
|
||||
it('z.snipify', (done) => {
|
||||
const result = z.snipify('something');
|
||||
result.should.equal(':censored:9:720a531ca0:');
|
||||
done();
|
||||
});
|
||||
|
||||
it('z.request - sync', () => {
|
||||
const bundleRequest = {
|
||||
method: 'GET',
|
||||
url: `${HTTPBIN_URL}/get`,
|
||||
params: {
|
||||
hello: 'world',
|
||||
},
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
},
|
||||
auth: null,
|
||||
data: null,
|
||||
};
|
||||
|
||||
const response = z.request(bundleRequest);
|
||||
response.should.have.property('status_code');
|
||||
response.should.have.property('headers');
|
||||
response.should.have.property('content');
|
||||
|
||||
response.status_code.should.eql(200);
|
||||
const results = JSON.parse(response.content);
|
||||
results.args.should.deepEqual({ hello: ['world'] });
|
||||
results.headers.Accept.should.deepEqual(['application/json']);
|
||||
});
|
||||
|
||||
it('z.request - async', (done) => {
|
||||
const bundleRequest = {
|
||||
method: 'POST',
|
||||
url: `${HTTPBIN_URL}/post`,
|
||||
params: {
|
||||
hello: 'world',
|
||||
},
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
},
|
||||
auth: null,
|
||||
data: JSON.stringify({
|
||||
world: 'hello',
|
||||
}),
|
||||
};
|
||||
|
||||
z.request(bundleRequest, (error, response) => {
|
||||
should(error).eql(null);
|
||||
response.should.have.property('status_code');
|
||||
response.should.have.property('headers');
|
||||
response.should.have.property('content');
|
||||
|
||||
response.status_code.should.eql(200);
|
||||
|
||||
const results = JSON.parse(response.content);
|
||||
results.args.should.eql({ hello: ['world'] });
|
||||
|
||||
// Current version of httpbin.zapier-tooling.com encodes the input in
|
||||
// base64 and returns it, so we need to decode it here.
|
||||
const [header, encodedBody] = results.data.split(',');
|
||||
header.should.eql('data:application/octet-stream;base64');
|
||||
|
||||
const decodedBody = Buffer.from(encodedBody, 'base64').toString('utf8');
|
||||
decodedBody.should.eql(bundleRequest.data);
|
||||
results.headers.Accept.should.deepEqual(['application/json']);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('z.JSON.parse', (done) => {
|
||||
const result = z.JSON.parse('{"hello": "world"}');
|
||||
result.should.have.property('hello');
|
||||
result.hello.should.eql('world');
|
||||
|
||||
const invalidJsonString = '{invalid"hello": "world"}';
|
||||
|
||||
try {
|
||||
z.JSON.parse(invalidJsonString);
|
||||
} catch (e) {
|
||||
e.name.should.eql('Error');
|
||||
e.message.should.eql(
|
||||
`Error parsing response. We got: "${invalidJsonString}"`,
|
||||
);
|
||||
done();
|
||||
}
|
||||
});
|
||||
|
||||
it('z.JSON.stringify', (done) => {
|
||||
const result = z.JSON.stringify({
|
||||
hello: 'world',
|
||||
});
|
||||
result.should.equal('{"hello":"world"}');
|
||||
done();
|
||||
});
|
||||
|
||||
it('z.AWS', (done) => {
|
||||
const AWS = z.AWS();
|
||||
AWS.config.getCredentials(done);
|
||||
});
|
||||
});
|
||||
207
vendor/zapier-platform/packages/legacy-scripting-runner/zfactory.js
vendored
Normal file
207
vendor/zapier-platform/packages/legacy-scripting-runner/zfactory.js
vendored
Normal file
|
|
@ -0,0 +1,207 @@
|
|||
const crypto = require('crypto');
|
||||
|
||||
const _ = require('lodash');
|
||||
const request = require('request');
|
||||
const { createSyncFn } = require('synckit');
|
||||
|
||||
// So `zapier build` doesn't forget to include request-worker.js
|
||||
require('./request-worker');
|
||||
|
||||
// Converts WB `bundle.request` format to something `request` can use
|
||||
const convertBundleRequest = (bundleOrBundleRequest) => {
|
||||
bundleOrBundleRequest = _.extend({}, bundleOrBundleRequest);
|
||||
|
||||
// LEGACY: allow for the whole bundle to mistakingly be sent over
|
||||
const bundleRequest = bundleOrBundleRequest.request
|
||||
? bundleOrBundleRequest.request
|
||||
: bundleOrBundleRequest;
|
||||
|
||||
if (
|
||||
bundleRequest.auth &&
|
||||
Array.isArray(bundleRequest.auth) &&
|
||||
bundleRequest.auth.length === 2
|
||||
) {
|
||||
bundleRequest.auth = {
|
||||
user: bundleRequest.auth[0],
|
||||
password: bundleRequest.auth[1],
|
||||
};
|
||||
}
|
||||
|
||||
if (!bundleRequest.qs && bundleRequest.params) {
|
||||
bundleRequest.qs = bundleRequest.params;
|
||||
}
|
||||
if (!bundleRequest.body && bundleRequest.data) {
|
||||
bundleRequest.body = bundleRequest.data;
|
||||
}
|
||||
|
||||
delete bundleRequest.params;
|
||||
delete bundleRequest.data;
|
||||
|
||||
return bundleRequest;
|
||||
};
|
||||
|
||||
const parseBody = (body) => {
|
||||
if (body) {
|
||||
if (typeof body === 'string' || body.writeInt32BE) {
|
||||
return String(body);
|
||||
}
|
||||
|
||||
return body;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
// Converts `request`'s response into a simplified object
|
||||
const convertResponse = (response) => {
|
||||
if (response) {
|
||||
return {
|
||||
status_code: response.statusCode,
|
||||
headers: _.extend({}, response.headers),
|
||||
content: parseBody(response.body),
|
||||
};
|
||||
}
|
||||
|
||||
return {};
|
||||
};
|
||||
|
||||
const syncRequest = createSyncFn(require.resolve('./request-worker'));
|
||||
|
||||
const zfactory = (zcli, app, logger) => {
|
||||
const AWS = () => {
|
||||
// Direct require breaks the build as the module isn't found by browserify
|
||||
const moduleName = 'aws-sdk';
|
||||
return require(moduleName);
|
||||
};
|
||||
|
||||
const jsonParse = (str) => {
|
||||
try {
|
||||
return JSON.parse(str);
|
||||
} catch (err) {
|
||||
let preview = str;
|
||||
|
||||
if (str && str.length > 100) {
|
||||
preview = str.substr(0, 100);
|
||||
}
|
||||
|
||||
throw new Error(`Error parsing response. We got: "${preview}"`);
|
||||
}
|
||||
};
|
||||
|
||||
const jsonStringify = (obj) => {
|
||||
try {
|
||||
return JSON.stringify(obj);
|
||||
} catch (err) {
|
||||
throw new Error(err.message);
|
||||
}
|
||||
};
|
||||
|
||||
const sendHttpLog = (req, res) => {
|
||||
// Log fields here intend to match the ones in createHttpPatch in core
|
||||
const method = (req.method || 'GET').toUpperCase();
|
||||
const url = req.url || req.uri;
|
||||
const responseBody =
|
||||
typeof res.content === 'string'
|
||||
? res.content
|
||||
: 'Could not show response content';
|
||||
logger(`${res.status_code} ${method} ${url}`, {
|
||||
log_type: 'http',
|
||||
request_type: 'devplatform-outbound',
|
||||
request_url: url,
|
||||
request_method: method,
|
||||
request_headers: req.headers,
|
||||
request_params: req.qs,
|
||||
request_data: req.data,
|
||||
request_via_client: false,
|
||||
response_status_code: res.status_code,
|
||||
response_headers: res.headers,
|
||||
response_content: responseBody,
|
||||
});
|
||||
};
|
||||
|
||||
const requestMethod = (bundleRequest, callback) => {
|
||||
const options = convertBundleRequest(bundleRequest);
|
||||
|
||||
if (_.isFunction(callback)) {
|
||||
return request(options, (err, response) =>
|
||||
callback(err, convertResponse(response)),
|
||||
);
|
||||
}
|
||||
|
||||
const normalizedOptions = request.initParams(options);
|
||||
const response = syncRequest(normalizedOptions);
|
||||
|
||||
const convertedResponse = convertResponse(response);
|
||||
|
||||
// syncRequest() is done by a worker thread, which isn't httpPatch'ed, so we
|
||||
// need to explicit write the http log here
|
||||
if (logger) {
|
||||
sendHttpLog(normalizedOptions, convertedResponse);
|
||||
}
|
||||
|
||||
return convertedResponse;
|
||||
};
|
||||
|
||||
const hash = (
|
||||
algorithm,
|
||||
string,
|
||||
encoding = 'hex',
|
||||
inputEncoding = 'binary',
|
||||
) => {
|
||||
const hasher = crypto.createHash(algorithm);
|
||||
hasher.update(string, inputEncoding);
|
||||
|
||||
return hasher.digest(encoding);
|
||||
};
|
||||
|
||||
const hmac = (algorithm, key, string, encoding = 'hex') => {
|
||||
const hasher = crypto.createHmac(algorithm, key);
|
||||
hasher.update(string);
|
||||
|
||||
return hasher.digest(encoding);
|
||||
};
|
||||
|
||||
const snipify = (string) => {
|
||||
const SALT = process.env.SECRET_SALT || 'doesntmatterreally';
|
||||
if (!_.isString(string)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const length = string.length;
|
||||
string += SALT;
|
||||
const result = hash('sha256', string);
|
||||
|
||||
return `:censored:${length}:${result.substr(0, 10)}:`;
|
||||
};
|
||||
|
||||
const dehydrate = (method, bundle) => {
|
||||
return zcli.dehydrate(app.hydrators.legacyMethodHydrator, {
|
||||
method,
|
||||
bundle,
|
||||
});
|
||||
};
|
||||
|
||||
const dehydrateFile = (url, requestOptions, meta) => {
|
||||
return zcli.dehydrateFile(app.hydrators.legacyFileHydrator, {
|
||||
url,
|
||||
request: requestOptions,
|
||||
meta,
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
AWS,
|
||||
JSON: {
|
||||
parse: jsonParse,
|
||||
stringify: jsonStringify,
|
||||
},
|
||||
request: requestMethod,
|
||||
hash,
|
||||
hmac,
|
||||
snipify,
|
||||
dehydrate,
|
||||
dehydrateFile,
|
||||
};
|
||||
};
|
||||
|
||||
module.exports = zfactory;
|
||||
Loading…
Add table
Add a link
Reference in a new issue