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:
George Lambert 2026-09-09 02:37:36 -04:00
commit b4150c8250
1364 changed files with 6814366 additions and 0 deletions

View file

@ -0,0 +1,62 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/
# Dependency directories
node_modules/
jspm_packages/
# Typescript v1 declaration files
typings/
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# environment variables file
.env
.environment
# next.js build output
.next

View file

@ -0,0 +1,30 @@
# files
This Zapier integration project is generated by the `zapier-platform init` CLI command.
These are what you normally do next:
```bash
# Install dependencies
npm install # or you can use yarn
# Run tests
zapier-platform test
# Register the integration on Zapier if you haven't
zapier-platform register "App Title"
# Or you can link to an existing integration on Zapier
zapier-platform link
# Push it to Zapier
zapier-platform push
```
Find out more on the latest docs: https://github.com/zapier/zapier-platform/blob/main/packages/cli/README.md.
# The "files" Template
This example has a trigger and a create showcasing file handling.
Find out more in the docs: https://github.com/zapier/zapier-platform/blob/main/packages/cli/README.md#stashing-files.

View file

@ -0,0 +1,64 @@
const http = require('https'); // require('http') if your URL is not https
const FormData = require('form-data');
// Getting a stream directly from http. This only works on core 10+. For core
// 9.x compatible code, see uploadFile_v9.js.
const makeDownloadStream = (url) =>
new Promise((resolve, reject) => {
http
.request(url, (res) => {
// We can risk missing the first n bytes if we don't pause!
res.pause();
resolve(res);
})
.on('error', reject)
.end();
});
const perform = async (z, bundle) => {
// bundle.inputData.file will in fact be an URL where the file data can be
// downloaded from which we do via a stream
const stream = await makeDownloadStream(bundle.inputData.file, z);
const form = new FormData();
form.append('filename', bundle.inputData.filename);
form.append('file', stream);
// All set! Resume the stream
stream.resume();
const response = await z.request({
url: 'https://auth-json-server.zapier-staging.com/upload',
method: 'POST',
body: form,
headers: {
// DO NOT do auth like this! We do this here because this is a file
// uploading example so the auth is not the point.
'x-api-key': 'secret',
},
});
return response.data;
};
module.exports = {
key: 'uploadFile_v10',
noun: 'File',
display: {
label: 'Upload File v10',
description: 'Uploads a file. Only works on zapier-platform-core v10+.',
},
operation: {
inputFields: [
{ key: 'filename', required: true, type: 'string', label: 'Filename' },
{ key: 'file', required: true, type: 'file', label: 'File' },
],
perform,
sample: {
id: 1,
filename: 'example.pdf',
file: 'SAMPLE FILE',
},
},
};

View file

@ -0,0 +1,80 @@
const { randomBytes } = require('crypto');
const fs = require('fs');
const os = require('os');
const path = require('path');
const fetch = require('node-fetch');
const FormData = require('form-data');
// Download the HTTP URL to a local temporary file, and make a readable stream
// from it. This should work compatibly for all core versions. But if you're
// using core v10+, we recommend to use the implementation of uploadFile_v10.js.
const makeDownloadStream = async (url) => {
// Create a temp file to store the downloaded file
const filename = randomBytes(16).toString('hex');
const tmpFilePath = path.join(os.tmpdir(), filename);
const dest = fs.createWriteStream(tmpFilePath);
const response = await fetch(url);
// Download the file to the temp file. When finished, open a readable stream
// from that temp file.
return new Promise((resolve, reject) => {
response.body
.pipe(dest)
.on('close', () => {
const stream = fs.createReadStream(tmpFilePath).on('close', () => {
// Delete the file once the stream is read
fs.unlinkSync(tmpFilePath);
});
resolve(stream);
})
.on('error', reject);
});
};
const perform = async (z, bundle) => {
const form = new FormData();
form.append('filename', bundle.inputData.filename);
// bundle.inputData.file will in fact be an URL where the file data can be
// downloaded from which we do via a stream
const stream = await makeDownloadStream(bundle.inputData.file, z);
form.append('file', stream);
const response = await z.request({
url: 'https://auth-json-server.zapier-staging.com/upload',
method: 'POST',
body: form,
headers: {
// DO NOT do auth like this! We do this here because this is a file
// uploading example so the auth is not the point.
'x-api-key': 'secret',
},
});
return response.json;
};
module.exports = {
key: 'uploadFile_v9',
noun: 'File',
display: {
label: 'Upload File v9',
description:
'Uploads a file. Compatible with all versions of zapier-platform-core.',
},
operation: {
inputFields: [
{ key: 'filename', required: true, type: 'string', label: 'Filename' },
{ key: 'file', required: true, type: 'file', label: 'File' },
],
perform,
sample: {
id: 1,
filename: 'example.pdf',
file: 'SAMPLE FILE',
},
},
};

View file

@ -0,0 +1,16 @@
module.exports = {
downloadFile: async (z, bundle) => {
// Use standard auth to request the file
const filePromise = z.request({
url: bundle.inputData.url,
raw: true,
});
// When `raw` is true, the result of z.request() can be passed to
// z.stashFile(). z.stashFile() will upload the file to a Zapier-owned S3
// bucket and return a promise of an S3 URL that allows Zapier to get the
// file without auth. If your file URL is permanently publicly available,
// you may skip z.stashFile() and return that URL directly here.
return z.stashFile(filePromise);
},
};

View file

@ -0,0 +1,25 @@
const hydrators = require('./hydrators');
const newFile = require('./triggers/newFile');
const uploadFileV10 = require('./creates/uploadFile_v10');
const uploadFileV9 = require('./creates/uploadFile_v9');
module.exports = {
// This is just shorthand to reference the installed dependencies you have.
// Zapier will need to know these before we can upload.
version: require('./package.json').version,
platformVersion: require('zapier-platform-core').version,
// Any hydrators go here
hydrators,
// If you want your triggers to show up, you better include it here!
triggers: {
[newFile.key]: newFile,
},
// If you want your creates to show up, you better include it here!
creates: {
[uploadFileV10.key]: uploadFileV10,
[uploadFileV9.key]: uploadFileV9,
},
};

View file

@ -0,0 +1,17 @@
{
"name": "files",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "jest --testTimeout 10000"
},
"dependencies": {
"zapier-platform-core": "19.1.0",
"form-data": "4.0.0"
},
"devDependencies": {
"jest": "^26.6.3"
},
"private": true
}

View file

@ -0,0 +1,60 @@
/* globals describe, expect, test */
const zapier = require('zapier-platform-core');
const App = require('../index');
const appTester = zapier.createAppTester(App);
zapier.tools.env.inject();
const CORE_VERSION = zapier.version.split('.').map((s) => parseInt(s));
const FILE_URL =
'https://cdn.zapier.com/storage/files/f6679cf77afeaf6b8426de8d7b9642fc.pdf';
// This is what you get when doing `curl <FILE_URL> | sha1sum`
const EXPECTED_SHA1 = '3cf58b42a0fb1b7cc58de8110096841ece967530';
describe('uploadFile', () => {
test('upload file v10', async () => {
if (CORE_VERSION[0] < 10) {
console.warn(
`skipped because this only works on core v10+ and you're on ${zapier.version}`,
);
return;
}
const bundle = {
inputData: {
filename: 'sample.pdf',
// in production, this will be an hydration URL to the selected file's data
file: FILE_URL,
},
};
const result = await appTester(
App.creates.uploadFile_v10.operation.perform,
bundle,
);
expect(result.filename).toBe('sample.pdf');
expect(result.file.sha1).toBe(EXPECTED_SHA1);
});
test('upload file v9', async () => {
const bundle = {
inputData: {
filename: 'sample.pdf',
// in production, this will be an hydration URL to the selected file's data
file: FILE_URL,
},
};
const result = await appTester(
App.creates.uploadFile_v9.operation.perform,
bundle,
);
expect(result.filename).toBe('sample.pdf');
expect(result.file.sha1).toBe(EXPECTED_SHA1);
});
});

View file

@ -0,0 +1,27 @@
/* globals describe, expect, test */
const zapier = require('zapier-platform-core');
const App = require('../index');
const appTester = zapier.createAppTester(App);
zapier.tools.env.inject();
describe('downloadFile', () => {
test('download file', async () => {
if (!process.env.ZAPIER_DEPLOY_KEY) {
console.warn('skipped as ZAPIER_DEPLOY_KEY is not defined');
return;
}
const bundle = {
inputData: {
url: 'https://httpbin.zapier-tooling.com/xml',
},
};
const url = await appTester(App.hydrators.downloadFile, bundle);
expect(url).toContain(
'https://zapier-dev-files.s3.amazonaws.com/cli-platform/',
);
});
});

View file

@ -0,0 +1,26 @@
/* globals describe, expect, test */
const zapier = require('zapier-platform-core');
const App = require('../index');
const appTester = zapier.createAppTester(App);
zapier.tools.env.inject();
describe('newFile', () => {
test('fetch files', async () => {
const bundle = {};
const results = await appTester(
App.triggers.newFile.operation.perform,
bundle,
);
expect(results.length).toBeGreaterThan(0);
// The 'hydrate|||' thing how Zapier represents dehydrated data
const firstFile = results[0];
expect(firstFile).toEqual({
id: expect.stringMatching(/^https:/),
file: expect.stringMatching(/^hydrate\|\|\|/),
});
});
});

View file

@ -0,0 +1,46 @@
const hydrators = require('../hydrators');
const perform = (z, bundle) => {
// In reality you're more likely to get file info from a remote server. Here
// we're hard coding some links just to demonstrate.
const fileURLs = [
'https://httpbin.zapier-tooling.com/image/png',
'https://httpbin.zapier-tooling.com/image/jpeg',
'https://httpbin.zapier-tooling.com/xml',
];
return fileURLs.map((fileURL) => {
const fileInfo = {
id: fileURL,
// Make it possible to get the actual file contents if necessary. No need
// to make the request to download files now when the trigger is run.
file: z.dehydrateFile(hydrators.downloadFile, { url: fileURL }),
};
return fileInfo;
});
};
// We recommend writing your triggers separate like this and rolling them into
// the App definition at the end.
module.exports = {
key: 'newFile',
// You'll want to provide some helpful display labels and descriptions
// for users. Zapier will put them into the UX.
noun: 'File',
display: {
label: 'New File',
description: 'Triggers when a new file is added.',
},
// `operation` is where the business logic goes.
operation: {
perform,
sample: {
id: 'https://example.com/file.txt',
file: 'content',
},
},
};