Initial import of verae-middleware from zapier monorepo
This commit is contained in:
commit
7a31f93c5b
66 changed files with 6590 additions and 0 deletions
137
src/services/tenantService.js
Normal file
137
src/services/tenantService.js
Normal file
|
|
@ -0,0 +1,137 @@
|
|||
/**
|
||||
* @fileoverview Self-serve signup and admin tenant provisioning.
|
||||
* @module services/tenantService
|
||||
*/
|
||||
|
||||
import { randomUUID } from 'node:crypto';
|
||||
import { createTenant, getTenant, listTenants } from '../store/tenants.js';
|
||||
import { veraeClient } from '../clients/veraeClient.js';
|
||||
import { AppError } from '../errors.js';
|
||||
import { createDebugger } from '../debug/logger.js';
|
||||
|
||||
const log = createDebugger('auth');
|
||||
const ALLOWED_PLANS = new Set(['free', 'starter', 'pro', 'enterprise']);
|
||||
|
||||
function slugify(value) {
|
||||
return value
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]+/g, '-')
|
||||
.replace(/^-|-$/g, '')
|
||||
.slice(0, 48);
|
||||
}
|
||||
|
||||
async function validateVeraeCredentials(username, password) {
|
||||
try {
|
||||
await veraeClient.login({ username, password });
|
||||
return true;
|
||||
} catch (err) {
|
||||
throw new AppError('Invalid Verae credentials', {
|
||||
status: 400,
|
||||
code: 'INVALID_VERAE_CREDENTIALS',
|
||||
details: { message: err.message },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Public free-tier signup.
|
||||
* @param {{ email: string, name: string, veraeUsername: string, veraePassword: string }} params
|
||||
*/
|
||||
export async function selfServeSignup({ email, name, veraeUsername, veraePassword }) {
|
||||
if (!email || !name || !veraeUsername || !veraePassword) {
|
||||
throw new AppError('email, name, veraeUsername, and veraePassword are required', {
|
||||
status: 400,
|
||||
code: 'VALIDATION_ERROR',
|
||||
});
|
||||
}
|
||||
|
||||
await validateVeraeCredentials(veraeUsername, veraePassword);
|
||||
|
||||
const id = `tenant-${slugify(email)}-${randomUUID().slice(0, 8)}`;
|
||||
const { tenant, apiKey } = createTenant({
|
||||
id,
|
||||
name,
|
||||
plan: 'free',
|
||||
veraeUsername,
|
||||
veraePassword,
|
||||
contract: null,
|
||||
metadata: { email, audience: 'self-serve', createdVia: 'signup' },
|
||||
});
|
||||
|
||||
log.info('self-serve signup', { tenantId: tenant.id });
|
||||
|
||||
return {
|
||||
tenant: { id: tenant.id, name: tenant.name, plan: tenant.plan, email },
|
||||
apiKey,
|
||||
zapierSetup: {
|
||||
authType: 'custom',
|
||||
field: 'api_key',
|
||||
middlewareUrl: '/zapier/v1/auth/me',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Admin provision (internal / enterprise).
|
||||
*/
|
||||
export async function provisionTenant({
|
||||
id,
|
||||
name,
|
||||
plan,
|
||||
veraeUsername,
|
||||
veraePassword,
|
||||
contract = null,
|
||||
metadata = {},
|
||||
audience = 'admin',
|
||||
}) {
|
||||
if (!name || !plan || !veraeUsername || !veraePassword) {
|
||||
throw new AppError('name, plan, veraeUsername, and veraePassword are required', {
|
||||
status: 400,
|
||||
code: 'VALIDATION_ERROR',
|
||||
});
|
||||
}
|
||||
|
||||
if (!ALLOWED_PLANS.has(plan)) {
|
||||
throw new AppError(`Invalid plan: ${plan}`, { status: 400, code: 'VALIDATION_ERROR' });
|
||||
}
|
||||
|
||||
if (plan === 'enterprise' && !contract) {
|
||||
throw new AppError('enterprise tenants require a contract object', {
|
||||
status: 400,
|
||||
code: 'VALIDATION_ERROR',
|
||||
});
|
||||
}
|
||||
|
||||
await validateVeraeCredentials(veraeUsername, veraePassword);
|
||||
|
||||
const tenantId = id ?? `tenant-${slugify(name)}-${randomUUID().slice(0, 8)}`;
|
||||
if (getTenant(tenantId)) {
|
||||
throw new AppError('Tenant already exists', { status: 409, code: 'CONFLICT' });
|
||||
}
|
||||
|
||||
const { tenant, apiKey } = createTenant({
|
||||
id: tenantId,
|
||||
name,
|
||||
plan,
|
||||
veraeUsername,
|
||||
veraePassword,
|
||||
contract,
|
||||
metadata: { ...metadata, audience, createdVia: 'provision' },
|
||||
});
|
||||
|
||||
log.info('tenant provisioned', { tenantId: tenant.id, plan, audience });
|
||||
return { tenant: { id: tenant.id, name: tenant.name, plan: tenant.plan }, apiKey };
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Array<object>}
|
||||
*/
|
||||
export function listProvisionedTenants() {
|
||||
return listTenants().map((tenant) => ({
|
||||
id: tenant.id,
|
||||
name: tenant.name,
|
||||
plan: tenant.plan,
|
||||
audience: tenant.metadata?.audience ?? 'unknown',
|
||||
createdAt: tenant.createdAt,
|
||||
}));
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue