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
102
research/zapier/scratch/veraetime/src/authentication.ts
Normal file
102
research/zapier/scratch/veraetime/src/authentication.ts
Normal 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;
|
||||
|
|
@ -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',
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
47
research/zapier/scratch/veraetime/src/creates/timestamp.ts
Normal file
47
research/zapier/scratch/veraetime/src/creates/timestamp.ts
Normal 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 }],
|
||||
},
|
||||
};
|
||||
|
|
@ -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',
|
||||
},
|
||||
},
|
||||
};
|
||||
44
research/zapier/scratch/veraetime/src/creates/verify.ts
Normal file
44
research/zapier/scratch/veraetime/src/creates/verify.ts
Normal 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 },
|
||||
],
|
||||
},
|
||||
};
|
||||
36
research/zapier/scratch/veraetime/src/index.ts
Normal file
36
research/zapier/scratch/veraetime/src/index.ts
Normal 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,
|
||||
},
|
||||
});
|
||||
14
research/zapier/scratch/veraetime/src/lib/base.ts
Normal file
14
research/zapier/scratch/veraetime/src/lib/base.ts
Normal 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}`;
|
||||
}
|
||||
28
research/zapier/scratch/veraetime/src/middleware.ts
Normal file
28
research/zapier/scratch/veraetime/src/middleware.ts
Normal 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];
|
||||
43
research/zapier/scratch/veraetime/src/searches/job_status.ts
Normal file
43
research/zapier/scratch/veraetime/src/searches/job_status.ts
Normal 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',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
@ -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' },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
|
|
@ -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',
|
||||
},
|
||||
},
|
||||
};
|
||||
Loading…
Add table
Add a link
Reference in a new issue