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,12 @@
# Golden local integration
`oauth2-typescript/` was created with:
```bash
source ../scripts/dev-env.sh
zapier-platform init oauth2-typescript --template oauth2 --language typescript
cd oauth2-typescript && npm install
zapier-platform build && zapier-platform validate --without-style
```
Structurally valid. `register` / `push` need [LOGIN.md](../LOGIN.md).

View file

@ -0,0 +1,66 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.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/
dist/
# 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
.pnpm-store/

View file

@ -0,0 +1,34 @@
# oauth2-typescript
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 --ignore-scripts # or you can use pnpm or 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
```
Then, to add more features, you can use the `zapier-platform scaffold` command, for example:
```bash
# Add a trigger
zapier-platform scaffold trigger contact
# Add an action
zapier-platform scaffold create contact
```
Find out more on the latest docs: https://docs.zapier.com/platform

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,23 @@
{
"name": "oauth2-typescript",
"version": "1.0.0",
"description": "",
"scripts": {
"test": "npm run build && vitest --run",
"clean": "rimraf ./dist ./build",
"build": "npm run clean && tsc",
"dev": "npm run build -- --watch",
"_zapier-build": "npm run build"
},
"dependencies": {
"zapier-platform-core": "19.1.0"
},
"devDependencies": {
"rimraf": "^5.0.10",
"typescript": "5.6.2",
"vitest": "^2.1.2"
},
"private": true,
"exports": "./dist/index.js",
"type": "module"
}

View file

@ -0,0 +1,100 @@
import type { ZObject, Bundle, Authentication } from 'zapier-platform-core';
const getAccessToken = async (z: ZObject, bundle: Bundle) => {
const response = await z.request({
url: 'https://auth-json-server.zapier-staging.com/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' },
});
// 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,
refresh_token: response.data.refresh_token,
};
};
const refreshAccessToken = async (z: ZObject, bundle: Bundle) => {
const response = await z.request({
url: 'https://auth-json-server.zapier-staging.com/oauth/refresh-token',
method: 'POST',
body: {
client_id: process.env.CLIENT_ID,
client_secret: process.env.CLIENT_SECRET,
grant_type: 'refresh_token',
refresh_token: bundle.authData.refresh_token,
},
headers: { 'content-type': 'application/x-www-form-urlencoded' },
});
// 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 the refresh token stays constant, no need to return it.
// If the refresh token does change, return it here to update the stored value in
// Zapier
return {
access_token: response.data.access_token,
refresh_token: response.data.refresh_token,
};
};
// 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 = (z: ZObject, bundle: Bundle) =>
z.request({ url: 'https://auth-json-server.zapier-staging.com/me' });
export default {
// 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://auth-json-server.zapier-staging.com/oauth/authorize',
params: {
client_id: '{{process.env.CLIENT_ID}}',
state: '{{bundle.inputData.state}}',
redirect_uri: '{{bundle.inputData.redirect_uri}}',
response_type: 'code',
},
},
getAccessToken,
refreshAccessToken,
autoRefresh: true,
},
// 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: ZObject, bundle: Bundle)` and data returned from the
// test can be accessed in `bundle.inputData.X`.
connectionLabel: '{{json.username}}',
} satisfies Authentication;

View file

@ -0,0 +1,21 @@
import zapier, { defineApp } from 'zapier-platform-core';
import packageJson from '../package.json' with { type: 'json' };
import authentication from './authentication.js';
import { befores, afters } from './middleware.js';
export default defineApp({
version: packageJson.version,
platformVersion: zapier.version,
authentication,
beforeRequest: [...befores],
afterResponse: [...afters],
// Add your triggers here for them to show up!
triggers: {},
// Add your creates here for them to show up!
creates: {},
});

View file

@ -0,0 +1,15 @@
import type { ZObject, Bundle, Authentication } from 'zapier-platform-core';
// 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: ZObject, bundle: Bundle) => {
if (bundle.authData.access_token) {
request.headers.Authorization = `Bearer ${bundle.authData.access_token}`;
}
return request;
};
export const befores = [includeBearerToken];
export const afters = [];

View file

@ -0,0 +1,115 @@
import { describe, expect, it, beforeAll } from 'vitest';
import zapier from 'zapier-platform-core';
import App from '../index.js';
const appTester = zapier.createAppTester(App);
// Only defining the env vars 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('authentication', () => {
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.`,
);
}
});
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://auth-json-server.zapier-staging.com/oauth/authorize?client_id=1234&state=4444&redirect_uri=https%3A%2F%2Fzapier.com%2F&response_type=code',
);
});
it('can fetch an access token', async () => {
const bundle = {
inputData: {
// In production, Zapier passes along whatever code your API set in the query params when it redirects
// the user's browser to the `redirect_uri`
code: 'one_time_code',
},
environment: {
CLIENT_ID: process.env.CLIENT_ID,
CLIENT_SECRET: process.env.CLIENT_SECRET,
},
cleanedRequest: {
querystring: {
accountDomain: 'test-account',
code: 'one_time_code',
},
},
rawRequest: {
querystring: '?accountDomain=test-account&code=one_time_code',
},
};
const result = await appTester(
App.authentication.oauth2Config.getAccessToken,
bundle,
);
expect(result.access_token).toBe('a_token');
expect(result.refresh_token).toBe('a_refresh_token');
});
it('can refresh the access token', async () => {
const bundle = {
// In production, Zapier provides these. For testing, we have hard-coded them.
// When writing tests for your own app, you should consider exporting them and doing process.env.MY_ACCESS_TOKEN
authData: {
access_token: 'a_token',
refresh_token: 'a_refresh_token',
},
environment: {
CLIENT_ID: process.env.CLIENT_ID,
CLIENT_SECRET: process.env.CLIENT_SECRET,
},
};
const result = await appTester(
App.authentication.oauth2Config.refreshAccessToken,
bundle,
);
expect(result.access_token).toBe('a_token');
});
it('includes the access token in future requests', async () => {
const bundle = {
authData: {
access_token: 'a_token',
refresh_token: 'a_refresh_token',
},
};
const response = await appTester(App.authentication.test, bundle);
expect(response.data).toHaveProperty('username');
expect(response.data.username).toBe('Bret');
});
});

View file

@ -0,0 +1,18 @@
{
"compilerOptions": {
"target": "ESNext",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"resolveJsonModule": true,
"esModuleInterop": true,
"noUncheckedIndexedAccess": true,
"isolatedModules": true,
"noImplicitAny": false,
"skipLibCheck": true,
"outDir": "./dist",
"rootDir": "./src",
"strict": true
},
"include": ["./src/**/*.ts"],
"exclude": ["./**/*.test.ts"]
}

View file

@ -0,0 +1,53 @@
# Verae Time API (for the Zapier connector)
Live docs:
- Swagger UI: https://api.veraetime.net/docs/swagger/index.html
- OpenAPI 3.0: https://api.veraetime.net/docs/swagger/openapi.yaml
- Local copy: [openapi.yaml](openapi.yaml)
Title in the spec is still “Timestamping Service API”. Host is **https://api.veraetime.net** (`servers.url` is `/`).
## Auth
`POST /auth/login` with `{ username, password }` → JWT in `token`.
All other API/admin routes: `Authorization: Bearer <token>`.
Zapier mapping: **session auth** (`scratch/veraetime`).
## Two hops
Zapier should call **verae-zapier-middleware**, not this host directly.
| Layer | Where | Role |
|-------|--------|------|
| Zapier connector | `scratch/veraetime` | Session auth + Zapier operations |
| Middleware | `verae-zapier-api/verae-zapier-middleware` (also at `/Users/marchon/datacubes/verae-zapier-api/…`) | Tenants, API keys, rate limits, REST Hooks, wait-for-job, NATS workers |
| Verae Time | `https://api.veraetime.net` | Blockchain timestamping (this OpenAPI) |
Middleware routes (all under `/zapier/v1`):
| Method | Path | Connector |
|--------|------|-----------|
| POST | `/auth/login` | session `perform` (`username`/`password` or `api_key`) |
| GET | `/auth/me` | auth test |
| POST | `/timestamp` | Create Timestamp |
| POST | `/timestamp/wait` | Create Timestamp and Wait |
| POST | `/timestamp/batch` | Create Batch Timestamps |
| POST | `/verify` | Verify Certificate |
| GET | `/status/{jobId}` | Find Job Status |
| GET | `/status/{jobId}/verification` | Find Job Verification |
| POST | `/webhooks/subscribe` | Timestamp Completed (hook subscribe) |
| DELETE | `/webhooks/unsubscribe` | hook unsubscribe |
## Direct Verae mapping (what the middleware wraps)
| Zapier | Verb | Path | Notes |
|--------|------|------|--------|
| Create Timestamp | POST | `/api/timestamp` | `{ data, hashAlg? }``{ jobId }` (HTTP 202) |
| Create Batch Timestamps | POST | `/api/batch/timestamp` | `{ items: [{ data, hashAlg? }] }` |
| Verify Certificate | POST | `/api/verify` | `{ certificate }``{ valid, timestamp, blockIndex }` |
| Find Job Status | GET | `/api/status/{jobId}` | Search; empty if 404 |
| Find Job Verification | GET | `/api/verify/{jobId}` | Search |
| New Blockchain Timestamp | GET | `/admin/timestamps` | Polling trigger (admin; last ~10 blocks) |
Not in v1 (admin HTML / user admin): dashboard HTML, metrics, queue, list/create/update/delete users, batch verify/status, get-block-by-hash. Easy to add later.

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,66 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.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/
dist/
# 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
.pnpm-store/

View file

@ -0,0 +1,24 @@
# Verae Time — Zapier integration
Talks to **verae-zapier-middleware** (`/zapier/v1/...`), not `api.veraetime.net` directly.
Middleware source: `/Users/marchon/datacubes/verae-zapier-api/verae-zapier-middleware`
Verae OpenAPI: `../our-api/openapi.yaml`
## Auth
Session: username/password **or** tenant API key → `POST /zapier/v1/auth/login``accessToken`.
Test: `GET /zapier/v1/auth/me`.
Set **Middleware base URL** (local default `http://127.0.0.1:3100`).
```bash
# start middleware (other repo)
cd /Users/marchon/datacubes/verae-zapier-api/verae-zapier-middleware
# MOCK_VERAE=true npm start
source ../../scripts/dev-env.sh
cd /Users/marchon/research/zapier/scratch/veraetime
npm install
zapier-platform build && zapier-platform validate
```

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,23 @@
{
"name": "veraetime",
"version": "1.0.0",
"description": "Timestamp data on the Verae Time blockchain and verify certificates.",
"scripts": {
"test": "npm run build && vitest --run",
"clean": "rimraf ./dist ./build",
"build": "npm run clean && tsc",
"dev": "npm run build -- --watch",
"_zapier-build": "npm run build"
},
"dependencies": {
"zapier-platform-core": "19.1.0"
},
"devDependencies": {
"rimraf": "^5.0.10",
"typescript": "5.6.2",
"vitest": "^2.1.2"
},
"private": true,
"exports": "./dist/index.js",
"type": "module"
}

View file

@ -0,0 +1,102 @@
import type { Authentication, Bundle, ZObject } from 'zapier-platform-core';
import { zapierV1 } from './lib/base.js';
const getSessionKey = async (z: ZObject, bundle: Bundle) => {
const body = bundle.authData.api_key
? { api_key: bundle.authData.api_key }
: {
username: bundle.authData.username,
password: bundle.authData.password,
};
if (!bundle.authData.api_key && (!body.username || !body.password)) {
throw new z.errors.Error(
'Enter a Verae Time username and password, or a middleware API key.',
'AuthenticationError',
400,
);
}
const response = await z.request({
skipThrowForStatus: true,
url: zapierV1(bundle, '/auth/login'),
method: 'POST',
headers: { 'content-type': 'application/json', accept: 'application/json' },
json: body,
});
if (response.status === 401) {
throw new z.errors.Error(
'Invalid credentials or API key.',
'AuthenticationError',
401,
);
}
if (response.status >= 400) {
const msg =
response.data?.error || response.content || `Login failed (${response.status})`;
throw new z.errors.Error(String(msg), 'AuthenticationError', response.status);
}
const accessToken = response.data?.accessToken;
if (!accessToken) {
throw new z.errors.Error(
'Login succeeded but no accessToken was returned.',
'AuthenticationError',
500,
);
}
return {
sessionKey: accessToken,
username: response.data?.user?.username || bundle.authData.username,
role: response.data?.user?.role,
plan: response.data?.tenant?.plan,
tenantId: response.data?.tenant?.id,
};
};
const test = async (z: ZObject, bundle: Bundle) => {
const response = await z.request({
url: zapierV1(bundle, '/auth/me'),
headers: { accept: 'application/json' },
});
return response.data;
};
export default {
type: 'session',
sessionConfig: { perform: getSessionKey },
fields: [
{
key: 'api_base_url',
label: 'Middleware base URL',
required: false,
default: 'http://127.0.0.1:3100',
helpText:
'Verae Zapier middleware origin (no path). Local default is http://127.0.0.1:3100. Production is the deployed middleware, not api.veraetime.net.',
},
{
key: 'username',
label: 'Username',
required: false,
helpText: 'Verae Time username. Skip if you use an API key.',
},
{
key: 'password',
label: 'Password',
required: false,
type: 'password',
},
{
key: 'api_key',
label: 'Middleware API key',
required: false,
type: 'password',
helpText: 'Tenant API key issued by the middleware. Alternative to username/password.',
},
],
test,
connectionLabel: '{{username}} · {{plan}}',
} satisfies Authentication;

View file

@ -0,0 +1,50 @@
import type { Bundle, ZObject } from 'zapier-platform-core';
import { zapierV1 } from '../lib/base.js';
const perform = async (z: ZObject, bundle: Bundle) => {
const items = (bundle.inputData.items || []) as { data?: string; hashAlg?: string }[];
if (!items.length) {
throw new z.errors.Error('Add at least one item.', 'InvalidInput', 400);
}
const response = await z.request({
url: zapierV1(bundle, '/timestamp/batch'),
method: 'POST',
headers: { 'content-type': 'application/json', accept: 'application/json' },
json: {
items: items.map((item) => ({
data: item.data,
...(item.hashAlg ? { hashAlg: item.hashAlg } : {}),
})),
},
});
return response.data;
};
export default {
key: 'batch_timestamp',
noun: 'Batch Timestamp',
display: {
label: 'Create Batch Timestamps',
description: 'Submit multiple data items to be timestamped in one request.',
},
operation: {
perform,
inputFields: [
{
key: 'items',
label: 'Items',
children: [
{ key: 'data', label: 'Data', type: 'text' as const, required: true },
{ key: 'hashAlg', label: 'Hash algorithm', type: 'string' as const, required: false },
],
},
],
sample: {
jobIds: [
'550e8400-e29b-41d4-a716-446655440000',
'650e8400-e29b-41d4-a716-446655440001',
],
},
},
};

View file

@ -0,0 +1,47 @@
import type { Bundle, ZObject } from 'zapier-platform-core';
import { zapierV1 } from '../lib/base.js';
const perform = async (z: ZObject, bundle: Bundle) => {
const response = await z.request({
url: zapierV1(bundle, '/timestamp'),
method: 'POST',
headers: { 'content-type': 'application/json', accept: 'application/json' },
json: {
data: bundle.inputData.data,
...(bundle.inputData.hashAlg ? { hashAlg: bundle.inputData.hashAlg } : {}),
},
});
return response.data;
};
export default {
key: 'timestamp',
noun: 'Timestamp',
display: {
label: 'Create Timestamp',
description: 'Submit data to be timestamped on the Verae Time blockchain.',
},
operation: {
perform,
inputFields: [
{
key: 'data',
label: 'Data',
type: 'text' as const,
required: true,
helpText: 'Payload to hash and record on the chain.',
},
{
key: 'hashAlg',
label: 'Hash algorithm',
type: 'string' as const,
required: false,
default: 'SHA256',
helpText: 'Defaults to SHA256 if omitted.',
},
],
sample: { jobId: '550e8400-e29b-41d4-a716-446655440000' },
outputFields: [{ key: 'jobId', label: 'Job ID', type: 'string' as const }],
},
};

View file

@ -0,0 +1,50 @@
import type { Bundle, ZObject } from 'zapier-platform-core';
import { zapierV1 } from '../lib/base.js';
const perform = async (z: ZObject, bundle: Bundle) => {
const response = await z.request({
url: zapierV1(bundle, '/timestamp/wait'),
method: 'POST',
headers: { 'content-type': 'application/json', accept: 'application/json' },
json: {
data: bundle.inputData.data,
...(bundle.inputData.hashAlg ? { hashAlg: bundle.inputData.hashAlg } : {}),
},
});
return response.data;
};
export default {
key: 'timestamp_wait',
noun: 'Timestamp',
display: {
label: 'Create Timestamp and Wait',
description:
'Submit data and wait until the middleware reports the job completed or failed.',
},
operation: {
perform,
inputFields: [
{
key: 'data',
label: 'Data',
type: 'text' as const,
required: true,
},
{
key: 'hashAlg',
label: 'Hash algorithm',
type: 'string' as const,
required: false,
default: 'SHA256',
},
],
sample: {
id: '550e8400-e29b-41d4-a716-446655440000',
status: 'completed',
result: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...',
completedAt: '2023-01-01T12:05:00Z',
},
},
};

View file

@ -0,0 +1,44 @@
import type { Bundle, ZObject } from 'zapier-platform-core';
import { zapierV1 } from '../lib/base.js';
const perform = async (z: ZObject, bundle: Bundle) => {
const response = await z.request({
url: zapierV1(bundle, '/verify'),
method: 'POST',
headers: { 'content-type': 'application/json', accept: 'application/json' },
json: { certificate: bundle.inputData.certificate },
});
return response.data;
};
export default {
key: 'verify_certificate',
noun: 'Certificate',
display: {
label: 'Verify Certificate',
description: 'Verify a Verae Time timestamp certificate.',
},
operation: {
perform,
inputFields: [
{
key: 'certificate',
label: 'Certificate',
type: 'text' as const,
required: true,
helpText: 'The timestamp certificate string returned when a job completes.',
},
],
sample: {
valid: true,
timestamp: '2023-01-01T12:00:00Z',
blockIndex: 42,
},
outputFields: [
{ key: 'valid', label: 'Valid', type: 'boolean' as const },
{ key: 'timestamp', label: 'Timestamp', type: 'datetime' as const },
{ key: 'blockIndex', label: 'Block index', type: 'integer' as const },
],
},
};

View file

@ -0,0 +1,36 @@
import zapier, { defineApp } from 'zapier-platform-core';
import packageJson from '../package.json' with { type: 'json' };
import authentication from './authentication.js';
import { befores, afters } from './middleware.js';
import timestamp from './creates/timestamp.js';
import timestampWait from './creates/timestamp_wait.js';
import batchTimestamp from './creates/batch_timestamp.js';
import verifyCertificate from './creates/verify.js';
import jobStatus from './searches/job_status.js';
import jobVerification from './searches/job_verification.js';
import timestampCompleted from './triggers/timestamp_completed.js';
export default defineApp({
version: packageJson.version,
platformVersion: zapier.version,
authentication,
beforeRequest: [...befores],
afterResponse: [...afters],
triggers: {
[timestampCompleted.key]: timestampCompleted,
},
creates: {
[timestamp.key]: timestamp,
[timestampWait.key]: timestampWait,
[batchTimestamp.key]: batchTimestamp,
[verifyCertificate.key]: verifyCertificate,
},
searches: {
[jobStatus.key]: jobStatus,
[jobVerification.key]: jobVerification,
},
});

View file

@ -0,0 +1,14 @@
import type { Bundle } from 'zapier-platform-core';
/** Local default for vera-zapier-middleware (`PORT` 3100). */
export const DEFAULT_BASE = 'http://127.0.0.1:3100';
export function apiBase(bundle: Bundle): string {
const raw = (bundle.authData.api_base_url || DEFAULT_BASE).trim();
return raw.replace(/\/+$/, '');
}
export function zapierV1(bundle: Bundle, path: string): string {
const p = path.startsWith('/') ? path : `/${path}`;
return `${apiBase(bundle)}/zapier/v1${p}`;
}

View file

@ -0,0 +1,28 @@
import type {
AfterResponseMiddleware,
BeforeRequestMiddleware,
HttpResponse,
ZObject,
} from 'zapier-platform-core';
const includeBearer: BeforeRequestMiddleware = (request, _z, bundle) => {
const url = request.url || '';
if (url.includes('/auth/login') || url.includes('/zapier/v1/signup')) {
return request;
}
if (bundle.authData.sessionKey) {
request.headers = request.headers || {};
request.headers.Authorization = `Bearer ${bundle.authData.sessionKey}`;
}
return request;
};
const handleAuthErrors: AfterResponseMiddleware = (response: HttpResponse, z: ZObject) => {
if (response.status === 401) {
throw new z.errors.RefreshAuthError('Verae Time token expired or invalid');
}
return response;
};
export const befores = [includeBearer];
export const afters = [handleAuthErrors];

View file

@ -0,0 +1,43 @@
import type { Bundle, ZObject } from 'zapier-platform-core';
import { zapierV1 } from '../lib/base.js';
const perform = async (z: ZObject, bundle: Bundle) => {
const response = await z.request({
skipThrowForStatus: true,
url: zapierV1(bundle, `/status/${encodeURIComponent(String(bundle.inputData.jobId))}`),
headers: { accept: 'application/json' },
});
if (response.status === 404) {
return [];
}
if (response.status >= 400) {
throw new z.errors.Error(
response.data?.error || `Status lookup failed (${response.status})`,
'JobStatusError',
response.status,
);
}
return [response.data];
};
export default {
key: 'job_status',
noun: 'Job',
display: {
label: 'Find Job Status',
description: 'Look up a timestamp job by ID (pending, completed, or failed).',
},
operation: {
perform,
inputFields: [
{ key: 'jobId', label: 'Job ID', type: 'string' as const, required: true },
],
sample: {
id: '550e8400-e29b-41d4-a716-446655440000',
status: 'completed',
result: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...',
completedAt: '2023-01-01T12:05:00Z',
},
},
};

View file

@ -0,0 +1,45 @@
import type { Bundle, ZObject } from 'zapier-platform-core';
import { zapierV1 } from '../lib/base.js';
const perform = async (z: ZObject, bundle: Bundle) => {
const response = await z.request({
skipThrowForStatus: true,
url: zapierV1(
bundle,
`/status/${encodeURIComponent(String(bundle.inputData.jobId))}/verification`,
),
headers: { accept: 'application/json' },
});
if (response.status === 404) {
return [];
}
if (response.status >= 400) {
throw new z.errors.Error(
response.data?.error || `Verification lookup failed (${response.status})`,
'JobVerifyError',
response.status,
);
}
return [response.data];
};
export default {
key: 'job_verification',
noun: 'Job Verification',
display: {
label: 'Find Job Verification',
description: 'Get block and verification details for a completed timestamp job.',
},
operation: {
perform,
inputFields: [
{ key: 'jobId', label: 'Job ID', type: 'string' as const, required: true },
],
sample: {
id: '550e8400-e29b-41d4-a716-446655440000',
status: 'completed',
metadata: { blockIndex: 42, timestamp: '2023-01-01T12:00:00Z' },
},
},
};

View file

@ -0,0 +1,22 @@
import { describe, expect, it } from 'vitest';
import zapier from 'zapier-platform-core';
import App from '../index.js';
const appTester = zapier.createAppTester(App);
describe('app definition', () => {
it('exports session auth and core operations', () => {
expect(App.authentication?.type).toBe('session');
expect(App.creates?.timestamp).toBeTruthy();
expect(App.creates?.verify_certificate).toBeTruthy();
expect(App.searches?.job_status).toBeTruthy();
expect(App.triggers?.timestamp_completed).toBeTruthy();
expect(App.creates?.timestamp_wait).toBeTruthy();
});
it('session perform posts to /auth/login', async () => {
const perform = App.authentication?.sessionConfig?.perform;
expect(typeof perform).toBe('function');
});
});

View file

@ -0,0 +1,65 @@
import type { Bundle, ZObject } from 'zapier-platform-core';
import { zapierV1 } from '../lib/base.js';
const subscribe = async (z: ZObject, bundle: Bundle) => {
const response = await z.request({
url: zapierV1(bundle, '/webhooks/subscribe'),
method: 'POST',
headers: { 'content-type': 'application/json', accept: 'application/json' },
json: {
targetUrl: bundle.targetUrl,
event: 'timestamp.completed',
},
});
return response.data;
};
const unsubscribe = async (z: ZObject, bundle: Bundle) => {
const hookId = bundle.subscribeData?.id;
const response = await z.request({
url: zapierV1(bundle, '/webhooks/unsubscribe'),
method: 'DELETE',
headers: { 'content-type': 'application/json', accept: 'application/json' },
json: { hookId },
});
return response.data;
};
const perform = async (_z: ZObject, bundle: Bundle) => {
const payload = bundle.cleanedRequest;
if (!payload) return [];
return Array.isArray(payload) ? payload : [payload];
};
const performList = async () => [
{
id: 'sample-job',
event: 'timestamp.completed',
jobId: '550e8400-e29b-41d4-a716-446655440000',
status: 'completed',
result: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...',
},
];
export default {
key: 'timestamp_completed',
noun: 'Timestamp',
display: {
label: 'Timestamp Completed',
description: 'Triggers when the middleware finishes a timestamp job (REST Hook).',
},
operation: {
type: 'hook' as const,
performSubscribe: subscribe,
performUnsubscribe: unsubscribe,
perform,
performList,
sample: {
id: 'sample-job',
event: 'timestamp.completed',
jobId: '550e8400-e29b-41d4-a716-446655440000',
status: 'completed',
},
},
};

View file

@ -0,0 +1,18 @@
{
"compilerOptions": {
"target": "ESNext",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"resolveJsonModule": true,
"esModuleInterop": true,
"noUncheckedIndexedAccess": true,
"isolatedModules": true,
"noImplicitAny": false,
"skipLibCheck": true,
"outDir": "./dist",
"rootDir": "./src",
"strict": true
},
"include": ["./src/**/*.ts"],
"exclude": ["./**/*.test.ts"]
}