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,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',
},
},
};