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,6 @@
build
docs
node_modules
*.log
.environment
.env

View file

@ -0,0 +1,7 @@
language: node_js
node_js:
- 8.10.0
before_script: 'npm install -g zapier-platform-cli'
script: 'zapier test'
notifications:
email: false

View file

@ -0,0 +1,5 @@
# zapier-platform-example-app-github
An example app that helps kickstart your journey as a Zapier [developer](https://developer.zapier.com/). Once logged in, you can see the tutorial itself [here](https://developer.zapier.com/cli-guide/introduction).
You can learn more about the CLI [here](https://github.com/zapier/zapier-platform/blob/main/packages/cli/README.md).

View file

@ -0,0 +1,89 @@
const getAccessToken = async (z, bundle) => {
const response = await z.request({
url: 'https://github.com/login/oauth/access_token',
method: 'POST',
body: {
client_id: process.env.CLIENT_ID,
client_secret: process.env.CLIENT_SECRET,
grant_type: 'authorization_code',
code: bundle.inputData.code,
// Extra data can be pulled from the querystring. For instance:
// 'accountDomain': bundle.cleanedRequest.querystring.accountDomain
},
headers: {
'content-type': 'application/x-www-form-urlencoded',
Accept: 'application/json',
},
});
// If you're using core v9.x or older, you should call response.throwForStatus()
// or verify response.status === 200 before you continue.
// This function should return `access_token`.
// If your app does an app refresh, then `refresh_token` should be returned here
// as well
return {
access_token: response.data.access_token,
};
};
// This function runs before every outbound request. You can have as many as you
// need. They'll need to each be registered in your index.js file.
const includeBearerToken = (request, z, bundle) => {
if (bundle.authData.access_token) {
request.headers.Authorization = `Bearer ${bundle.authData.access_token}`;
}
return request;
};
// You want to make a request to an endpoint that is either specifically designed
// to test auth, or one that every user will have access to. eg: `/me`.
// By returning the entire request object, you have access to the request and
// response data for testing purposes. Your connection label can access any data
// from the returned response using the `json.` prefix. eg: `{{json.username}}`.
const test = async (z, bundle) => {
const response = await z.request({ url: 'https://api.github.com/user' });
return response;
};
module.exports = {
config: {
// OAuth2 is a web authentication standard. There are a lot of configuration
// options that will fit most any situation.
type: 'oauth2',
oauth2Config: {
authorizeUrl: {
url: 'https://github.com/login/oauth/authorize',
params: {
client_id: '{{process.env.CLIENT_ID}}',
state: '{{bundle.inputData.state}}',
redirect_uri: '{{bundle.inputData.redirect_uri}}',
response_type: 'code',
},
},
getAccessToken,
},
// Define any input app's auth requires here. The user will be prompted to enter
// this info when they connect their account.
fields: [],
// The test method allows Zapier to verify that the credentials a user provides
// are valid. We'll execute this method whenever a user connects their account for
// the first time.
test,
// This template string can access all the data returned from the auth test. If
// you return the test object, you'll access the returned data with a label like
// `{{json.X}}`. If you return `response.data` from your test, then your label can
// be `{{X}}`. This can also be a function that returns a label. That function has
// the standard args `(z, bundle)` and data returned from the test can be accessed
// in `bundle.inputData.X`.
connectionLabel: '{{json.login}}',
},
befores: [includeBearerToken],
afters: [],
};

View file

@ -0,0 +1,38 @@
const sample = require('../samples/sample_issue');
const createIssue = (z, bundle) => {
const responsePromise = z.request({
method: 'POST',
url: `https://api.github.com/repos/${bundle.inputData.repo}/issues`,
body: {
title: bundle.inputData.title,
body: bundle.inputData.body,
},
});
return responsePromise.then((response) => response.data);
};
module.exports = {
key: 'issue',
noun: 'Issue',
display: {
label: 'Create Issue',
description: 'Creates an issue.',
},
operation: {
inputFields: [
{
key: 'repo',
label: 'Repo',
required: true,
dynamic: 'repo.full_name.full_name',
},
{ key: 'title', label: 'Title', required: true },
{ key: 'body', label: 'Body', required: false },
],
perform: createIssue,
sample: sample,
},
};

View file

@ -0,0 +1,41 @@
const repoTrigger = require('./triggers/repo');
const issueCreate = require('./creates/issue');
const issueTrigger = require('./triggers/issue');
const {
config: authentication,
befores = [],
afters = [],
} = require('./authentication');
const App = {
// 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,
authentication,
// beforeRequest & afterResponse are optional hooks into the provided HTTP client
beforeRequest: [...befores],
afterResponse: [...afters],
// If you want to define optional resources to simplify creation of triggers, searches, creates - do that here!
resources: {},
// If you want your trigger to show up, you better include it here!
triggers: {
[repoTrigger.key]: repoTrigger,
[issueTrigger.key]: issueTrigger,
},
// If you want your searches to show up, you better include it here!
searches: {},
// If you want your creates to show up, you better include it here!
creates: {
[issueCreate.key]: issueCreate,
},
};
// Finally, export the app.
module.exports = App;

View file

@ -0,0 +1,24 @@
{
"name": "zapier-platform-example-app-github",
"version": "1.0.0",
"description": "An example app for the Zapier platform.",
"repository": "zapier/zapier-platform-app-github-example",
"homepage": "https://zapier.com/developer",
"author": "Zane Lyon <zane.lyon@zapier.com>",
"license": "BSD-3-Clause",
"main": "index.js",
"scripts": {
"test": "jest --testTimeout 10000"
},
"engines": {
"node": "8.10.0",
"npm": ">=5.6.0"
},
"dependencies": {
"zapier-platform-core": "19.1.0"
},
"devDependencies": {
"jest": "^26.6.3",
"nock": "^13.1.3"
}
}

View file

@ -0,0 +1,159 @@
module.exports = {
id: 1,
url: 'https://api.github.com/repos/octocat/Hello-World/issues/1347',
repository_url: 'https://api.github.com/repos/octocat/Hello-World',
labels_url:
'https://api.github.com/repos/octocat/Hello-World/issues/1347/labels{/name}',
comments_url:
'https://api.github.com/repos/octocat/Hello-World/issues/1347/comments',
events_url:
'https://api.github.com/repos/octocat/Hello-World/issues/1347/events',
html_url: 'https://github.com/octocat/Hello-World/issues/1347',
number: 1347,
state: 'open',
title: 'Found a bug',
body: "I'm having a problem with this.",
user: {
login: 'octocat',
id: 1,
avatar_url: 'https://github.com/images/error/octocat_happy.gif',
gravatar_id: '',
url: 'https://api.github.com/users/octocat',
html_url: 'https://github.com/octocat',
followers_url: 'https://api.github.com/users/octocat/followers',
following_url:
'https://api.github.com/users/octocat/following{/other_user}',
gists_url: 'https://api.github.com/users/octocat/gists{/gist_id}',
starred_url: 'https://api.github.com/users/octocat/starred{/owner}{/repo}',
subscriptions_url: 'https://api.github.com/users/octocat/subscriptions',
organizations_url: 'https://api.github.com/users/octocat/orgs',
repos_url: 'https://api.github.com/users/octocat/repos',
events_url: 'https://api.github.com/users/octocat/events{/privacy}',
received_events_url: 'https://api.github.com/users/octocat/received_events',
type: 'User',
site_admin: false,
},
labels: [
{
id: 208045946,
url: 'https://api.github.com/repos/octocat/Hello-World/labels/bug',
name: 'bug',
color: 'f29513',
default: true,
},
],
assignee: {
login: 'octocat',
id: 1,
avatar_url: 'https://github.com/images/error/octocat_happy.gif',
gravatar_id: '',
url: 'https://api.github.com/users/octocat',
html_url: 'https://github.com/octocat',
followers_url: 'https://api.github.com/users/octocat/followers',
following_url:
'https://api.github.com/users/octocat/following{/other_user}',
gists_url: 'https://api.github.com/users/octocat/gists{/gist_id}',
starred_url: 'https://api.github.com/users/octocat/starred{/owner}{/repo}',
subscriptions_url: 'https://api.github.com/users/octocat/subscriptions',
organizations_url: 'https://api.github.com/users/octocat/orgs',
repos_url: 'https://api.github.com/users/octocat/repos',
events_url: 'https://api.github.com/users/octocat/events{/privacy}',
received_events_url: 'https://api.github.com/users/octocat/received_events',
type: 'User',
site_admin: false,
},
assignees: [
{
login: 'octocat',
id: 1,
avatar_url: 'https://github.com/images/error/octocat_happy.gif',
gravatar_id: '',
url: 'https://api.github.com/users/octocat',
html_url: 'https://github.com/octocat',
followers_url: 'https://api.github.com/users/octocat/followers',
following_url:
'https://api.github.com/users/octocat/following{/other_user}',
gists_url: 'https://api.github.com/users/octocat/gists{/gist_id}',
starred_url:
'https://api.github.com/users/octocat/starred{/owner}{/repo}',
subscriptions_url: 'https://api.github.com/users/octocat/subscriptions',
organizations_url: 'https://api.github.com/users/octocat/orgs',
repos_url: 'https://api.github.com/users/octocat/repos',
events_url: 'https://api.github.com/users/octocat/events{/privacy}',
received_events_url:
'https://api.github.com/users/octocat/received_events',
type: 'User',
site_admin: false,
},
],
milestone: {
url: 'https://api.github.com/repos/octocat/Hello-World/milestones/1',
html_url: 'https://github.com/octocat/Hello-World/milestones/v1.0',
labels_url:
'https://api.github.com/repos/octocat/Hello-World/milestones/1/labels',
id: 1002604,
number: 1,
state: 'open',
title: 'v1.0',
description: 'Tracking milestone for version 1.0',
creator: {
login: 'octocat',
id: 1,
avatar_url: 'https://github.com/images/error/octocat_happy.gif',
gravatar_id: '',
url: 'https://api.github.com/users/octocat',
html_url: 'https://github.com/octocat',
followers_url: 'https://api.github.com/users/octocat/followers',
following_url:
'https://api.github.com/users/octocat/following{/other_user}',
gists_url: 'https://api.github.com/users/octocat/gists{/gist_id}',
starred_url:
'https://api.github.com/users/octocat/starred{/owner}{/repo}',
subscriptions_url: 'https://api.github.com/users/octocat/subscriptions',
organizations_url: 'https://api.github.com/users/octocat/orgs',
repos_url: 'https://api.github.com/users/octocat/repos',
events_url: 'https://api.github.com/users/octocat/events{/privacy}',
received_events_url:
'https://api.github.com/users/octocat/received_events',
type: 'User',
site_admin: false,
},
open_issues: 4,
closed_issues: 8,
created_at: '2011-04-10T20:09:31Z',
updated_at: '2014-03-03T18:58:10Z',
closed_at: '2013-02-12T13:22:01Z',
due_on: '2012-10-09T23:39:01Z',
},
locked: false,
comments: 0,
pull_request: {
url: 'https://api.github.com/repos/octocat/Hello-World/pulls/1347',
html_url: 'https://github.com/octocat/Hello-World/pull/1347',
diff_url: 'https://github.com/octocat/Hello-World/pull/1347.diff',
patch_url: 'https://github.com/octocat/Hello-World/pull/1347.patch',
},
closed_at: null,
created_at: '2011-04-22T13:33:48Z',
updated_at: '2011-04-22T13:33:48Z',
closed_by: {
login: 'octocat',
id: 1,
avatar_url: 'https://github.com/images/error/octocat_happy.gif',
gravatar_id: '',
url: 'https://api.github.com/users/octocat',
html_url: 'https://github.com/octocat',
followers_url: 'https://api.github.com/users/octocat/followers',
following_url:
'https://api.github.com/users/octocat/following{/other_user}',
gists_url: 'https://api.github.com/users/octocat/gists{/gist_id}',
starred_url: 'https://api.github.com/users/octocat/starred{/owner}{/repo}',
subscriptions_url: 'https://api.github.com/users/octocat/subscriptions',
organizations_url: 'https://api.github.com/users/octocat/orgs',
repos_url: 'https://api.github.com/users/octocat/repos',
events_url: 'https://api.github.com/users/octocat/events{/privacy}',
received_events_url: 'https://api.github.com/users/octocat/received_events',
type: 'User',
site_admin: false,
},
};

View file

@ -0,0 +1,126 @@
module.exports = {
id: 1296269,
owner: {
login: 'octocat',
id: 1,
avatar_url: 'https://github.com/images/error/octocat_happy.gif',
gravatar_id: '',
url: 'https://api.github.com/users/octocat',
html_url: 'https://github.com/octocat',
followers_url: 'https://api.github.com/users/octocat/followers',
following_url:
'https://api.github.com/users/octocat/following{/other_user}',
gists_url: 'https://api.github.com/users/octocat/gists{/gist_id}',
starred_url: 'https://api.github.com/users/octocat/starred{/owner}{/repo}',
subscriptions_url: 'https://api.github.com/users/octocat/subscriptions',
organizations_url: 'https://api.github.com/users/octocat/orgs',
repos_url: 'https://api.github.com/users/octocat/repos',
events_url: 'https://api.github.com/users/octocat/events{/privacy}',
received_events_url: 'https://api.github.com/users/octocat/received_events',
type: 'User',
site_admin: false,
},
name: 'Hello-World',
full_name: 'octocat/Hello-World',
description: 'This your first repo!',
private: false,
fork: false,
url: 'https://api.github.com/repos/octocat/Hello-World',
html_url: 'https://github.com/octocat/Hello-World',
archive_url:
'https://api.github.com/repos/octocat/Hello-World/{archive_format}{/ref}',
assignees_url:
'https://api.github.com/repos/octocat/Hello-World/assignees{/user}',
blobs_url: 'https://api.github.com/repos/octocat/Hello-World/git/blobs{/sha}',
branches_url:
'https://api.github.com/repos/octocat/Hello-World/branches{/branch}',
clone_url: 'https://github.com/octocat/Hello-World.git',
collaborators_url:
'https://api.github.com/repos/octocat/Hello-World/collaborators{/collaborator}',
comments_url:
'https://api.github.com/repos/octocat/Hello-World/comments{/number}',
commits_url: 'https://api.github.com/repos/octocat/Hello-World/commits{/sha}',
compare_url:
'https://api.github.com/repos/octocat/Hello-World/compare/{base}...{head}',
contents_url:
'https://api.github.com/repos/octocat/Hello-World/contents/{+path}',
contributors_url:
'https://api.github.com/repos/octocat/Hello-World/contributors',
deployments_url:
'https://api.github.com/repos/octocat/Hello-World/deployments',
downloads_url: 'https://api.github.com/repos/octocat/Hello-World/downloads',
events_url: 'https://api.github.com/repos/octocat/Hello-World/events',
forks_url: 'https://api.github.com/repos/octocat/Hello-World/forks',
git_commits_url:
'https://api.github.com/repos/octocat/Hello-World/git/commits{/sha}',
git_refs_url:
'https://api.github.com/repos/octocat/Hello-World/git/refs{/sha}',
git_tags_url:
'https://api.github.com/repos/octocat/Hello-World/git/tags{/sha}',
git_url: 'git:github.com/octocat/Hello-World.git',
hooks_url: 'https://api.github.com/repos/octocat/Hello-World/hooks',
issue_comment_url:
'https://api.github.com/repos/octocat/Hello-World/issues/comments{/number}',
issue_events_url:
'https://api.github.com/repos/octocat/Hello-World/issues/events{/number}',
issues_url:
'https://api.github.com/repos/octocat/Hello-World/issues{/number}',
keys_url: 'https://api.github.com/repos/octocat/Hello-World/keys{/key_id}',
labels_url: 'https://api.github.com/repos/octocat/Hello-World/labels{/name}',
languages_url: 'https://api.github.com/repos/octocat/Hello-World/languages',
merges_url: 'https://api.github.com/repos/octocat/Hello-World/merges',
milestones_url:
'https://api.github.com/repos/octocat/Hello-World/milestones{/number}',
mirror_url: 'git:git.example.com/octocat/Hello-World',
notifications_url:
'https://api.github.com/repos/octocat/Hello-World/notifications{?since,all,participating}',
pulls_url: 'https://api.github.com/repos/octocat/Hello-World/pulls{/number}',
releases_url:
'https://api.github.com/repos/octocat/Hello-World/releases{/id}',
ssh_url: 'git@github.com:octocat/Hello-World.git',
stargazers_url: 'https://api.github.com/repos/octocat/Hello-World/stargazers',
statuses_url:
'https://api.github.com/repos/octocat/Hello-World/statuses/{sha}',
subscribers_url:
'https://api.github.com/repos/octocat/Hello-World/subscribers',
subscription_url:
'https://api.github.com/repos/octocat/Hello-World/subscription',
svn_url: 'https://svn.github.com/octocat/Hello-World',
tags_url: 'https://api.github.com/repos/octocat/Hello-World/tags',
teams_url: 'https://api.github.com/repos/octocat/Hello-World/teams',
trees_url: 'https://api.github.com/repos/octocat/Hello-World/git/trees{/sha}',
homepage: 'https://github.com',
language: null,
forks_count: 9,
stargazers_count: 80,
watchers_count: 80,
size: 108,
default_branch: 'master',
open_issues_count: 0,
topics: ['octocat', 'atom', 'electron', 'API'],
has_issues: true,
has_wiki: true,
has_pages: false,
has_downloads: true,
archived: false,
pushed_at: '2011-01-26T19:06:43Z',
created_at: '2011-01-26T19:01:12Z',
updated_at: '2011-01-26T19:14:43Z',
permissions: {
admin: false,
push: false,
pull: true,
},
allow_rebase_merge: true,
allow_squash_merge: true,
allow_merge_commit: true,
subscribers_count: 42,
network_count: 0,
license: {
key: 'mit',
name: 'MIT License',
spdx_id: 'MIT',
url: 'https://api.github.com/licenses/mit',
html_url: 'https://choosealicense.com/licenses/mit/',
},
};

View file

@ -0,0 +1,110 @@
/* globals describe, it, expect, beforeAll, beforeEach, afterEach */
const zapier = require('zapier-platform-core');
const nock = require('nock');
zapier.tools.env.inject(); // read from the .env file
const App = require('../index');
const appTester = zapier.createAppTester(App);
// Only here so the tests out of the box.
// You should create a `.env` file and populate it with the necessarily configuration
// it should look like:
/*
CLIENT_ID=1234
CLIENT_SECRET=asdf
*/
// then you can delete the following 2 lines
process.env.CLIENT_ID = process.env.CLIENT_ID || '1234';
process.env.CLIENT_SECRET = process.env.CLIENT_SECRET || 'asdf';
describe('oauth2 app', () => {
beforeAll(() => {
// It's a good idea to store your Client ID and Secret in the environment rather than in code.
if (!(process.env.CLIENT_ID && process.env.CLIENT_SECRET)) {
throw new Error(
`Before running the tests, make sure CLIENT_ID and CLIENT_SECRET are available in the environment.`,
);
}
});
afterEach(() => {
nock.cleanAll();
});
it('generates an authorize URL', async () => {
const bundle = {
// In production, these will be generated by Zapier and set automatically
inputData: {
state: '4444',
redirect_uri: 'https://zapier.com/',
},
environment: {
CLIENT_ID: process.env.CLIENT_ID,
CLIENT_SECRET: process.env.CLIENT_SECRET,
},
};
const authorizeUrl = await appTester(
App.authentication.oauth2Config.authorizeUrl,
bundle,
);
expect(authorizeUrl).toBe(
'https://github.com/login/oauth/authorize?client_id=1234&state=4444&redirect_uri=https%3A%2F%2Fzapier.com%2F&response_type=code',
);
});
});
describe('getAccessToken', () => {
beforeEach(async () => {
nock('https://github.com/login/oauth')
.post('/access_token')
.reply(200, { access_token: 'someAccessToken' });
});
afterEach(() => {
nock.cleanAll();
});
it('returns the expected tokens', async () => {
const result = await appTester(
App.authentication.oauth2Config.getAccessToken,
);
expect(result.access_token).toBe('someAccessToken');
});
});
describe('testAuth', () => {
const bundle = {
// In production, these will be generated by Zapier and set automatically
inputData: {
state: '4444',
redirect_uri: 'https://zapier.com/',
},
environment: {
CLIENT_ID: process.env.CLIENT_ID,
CLIENT_SECRET: process.env.CLIENT_SECRET,
},
};
beforeEach(async () => {
nock('https://api.github.com')
.get('/user')
.reply(200, {
json: {
login: 'myLogin',
},
});
});
afterEach(() => {
nock.cleanAll();
});
it('returns the expected info', async () => {
const result = await appTester(App.authentication.test, bundle);
expect(result.data.json.login).toBe('myLogin');
});
});

View file

@ -0,0 +1,59 @@
const sample = require('../samples/sample_issue');
const triggerIssue = (z, bundle) => {
const responsePromise = z.request({
method: 'GET',
url: `https://api.github.com/repos/${bundle.inputData.repo}/issues`,
params: {
filter: bundle.inputData.filter,
state: bundle.inputData.state,
sort: 'updated',
direction: 'desc',
},
});
return responsePromise.then((response) => response.data);
};
module.exports = {
key: 'issue',
noun: 'Issue',
display: {
label: 'New Issue',
description: 'Triggers on a new issue.',
},
operation: {
inputFields: [
{
key: 'repo',
label: 'Repo',
required: true,
dynamic: 'repo.full_name.full_name',
},
{
key: 'filter',
required: false,
label: 'Filter',
choices: {
assigned: 'assigned',
created: 'created',
mentioned: 'mentioned',
subscribed: 'subscribed',
all: 'all',
},
helpText: 'Default is "assigned"',
},
{
key: 'state',
required: false,
label: 'State',
choices: { open: 'open', closed: 'closed', all: 'all' },
helpText: 'Default is "open"',
},
],
perform: triggerIssue,
sample: sample,
},
};

View file

@ -0,0 +1,26 @@
const sample = require('../samples/sample_repo_list');
const triggerRepo = (z, bundle) => {
const responsePromise = z.request({
url: 'https://api.github.com/user/repos?per_page=100',
});
return responsePromise.then((response) => response.data);
};
module.exports = {
key: 'repo',
noun: 'Repo',
display: {
label: 'Get Repo',
hidden: true,
description:
'The only purpose of this trigger is to populate the dropdown list of repos in the UI, thus, it is hidden.',
},
operation: {
inputFields: [],
perform: triggerRepo,
sample: sample,
},
};