Menu
Public repository / fea289fbcbddf1bdec78475b63bb3c66202eeac4
AkurAI-Platform / payment/service.ts
Latest change 0f3d573579e8a68916a82b03769d0c4453623558 - fix: fail closed until payments are configured by Ólafur Búi Ólafsson
import { Database } from "bun:sqlite";
import { mkdirSync, readFileSync } from "node:fs";
import { dirname, resolve } from "node:path";
import { createCipheriv, createDecipheriv, createHash, createHmac, randomBytes, randomUUID, timingSafeEqual, verify as verifySignature } from "node:crypto";
import { MyPOSClient, type PaymentFormFields, type WebhookValidationResult } from "mypos-online-checkout";
import embeddedCatalog from "../backend/subscriptions.json";
import {
maskEmail,
money,
nextBillingDate,
quoteCheckout,
validateCatalog,
type BillingInterval,
type Catalog,
type CheckoutQuote,
type Product,
} from "./core";
type OrderStatus = "pending" | "paid" | "failed" | "canceled" | "rolled_back";
type SubscriptionStatus = "active" | "past_due" | "unpaid" | "canceled" | "requires_payment_method";
type MembershipRole = "admin" | "member";
type LifecycleEntity = "organization" | "workspace" | "membership" | "entitlement" | "installation" | "domain";
interface OrganizationRow {
id: string;
name: string;
crm_company_id: string | null;
idp_tenant_id: string | null;
status: "active" | "suspended" | "canceled";
created_at: string;
updated_at: string;
}
interface MembershipRow {
id: string;
organization_id: string;
user_id: string;
role: MembershipRole;
status: "active" | "revoked";
created_at: string;
updated_at: string;
}
interface WorkspaceRow {
id: string;
organization_id: string;
name: string;
kind: "managed";
status: "provisioning" | "active" | "suspended" | "deprovisioned";
idp_workspace_id: string | null;
created_at: string;
updated_at: string;
}
interface EntitlementRow {
id: string;
organization_id: string;
workspace_id: string;
app_id: string;
status: "granted" | "revoked";
granted_at: string | null;
revoked_at: string | null;
created_at: string;
updated_at: string;
}
interface InstallationRow {
id: string;
organization_id: string;
workspace_id: string;
app_id: string;
status: "pending" | "installing" | "installed" | "suspended" | "uninstalled" | "failed";
created_at: string;
updated_at: string;
}
interface DomainRow {
id: string;
organization_id: string;
workspace_id: string | null;
hostname: string;
status: "pending" | "verified" | "active" | "disabled";
created_at: string;
updated_at: string;
}
interface LifecycleRevisionRow {
id: string;
organization_id: string;
workspace_id: string | null;
entity_type: LifecycleEntity;
entity_id: string;
revision: number;
from_state: string | null;
to_state: string;
reason: string | null;
actor_user_id: string | null;
metadata_json: string;
created_at: string;
}
interface OrderRow {
id: string;
kind: "initial" | "renewal";
subscription_id: string | null;
customer_name: string;
customer_email: string;
company: string | null;
interval: BillingInterval;
products_json: string;
unit_cents: number;
amount_cents: number;
currency: "EUR";
status: OrderStatus;
provider: string;
provider_ref: string | null;
provider_payload: string | null;
manage_token_hash: string;
period_start: string | null;
error_code: string | null;
created_at: string;
updated_at: string;
paid_at: string | null;
}
interface SubscriptionRow {
id: string;
initial_order_id: string;
customer_email: string;
interval: BillingInterval;
products_json: string;
unit_cents: number;
amount_cents: number;
currency: "EUR";
status: SubscriptionStatus;
card_token_enc: string | null;
manage_token_hash: string;
next_charge_at: string;
retry_at: string | null;
renew_attempts: number;
created_at: string;
updated_at: string;
canceled_at: string | null;
}
interface BeginResult {
redirectUrl: string;
payload?: PaymentFormFields;
}
interface ChargeResult {
success: boolean;
transactionRef?: string;
errorCode?: string;
}
type RecoveryResult = ChargeResult | "pending" | null;
export interface PaymentProvider {
readonly name: "disabled" | "mock" | "mypos";
begin(order: OrderRow, products: Product[]): Promise<BeginResult>;
renew(order: OrderRow, subscription: SubscriptionRow, cardToken: string, products: Product[]): Promise<ChargeResult>;
recover(order: OrderRow): Promise<RecoveryResult>;
}
interface WebhookProvider extends PaymentProvider {
readonly checkoutUrl: string;
validateWebhook(formData: FormData): WebhookValidationResult;
}
interface IdentityConfig {
readonly trustedProxySecret: string | null;
readonly credentialSecret: string | null;
readonly idpPublicKey: string | null;
readonly issuer: string | null;
readonly audience: string | null;
}
interface ServiceOptions {
env?: Record<string, string | undefined>;
dbPath?: string;
catalog?: Catalog;
catalogPath?: string;
publicUrl?: string;
upstreamUrl?: string;
now?: () => Date;
paymentProvider?: PaymentProvider;
}
export interface PaymentService {
fetch(request: Request): Promise<Response>;
runRenewals(): Promise<void>;
close(): void;
provider: "disabled" | "mock" | "mypos";
}
class Store {
private readonly db: Database;
private readonly now: () => Date;
constructor(path: string, now: () => Date) {
mkdirSync(dirname(path), { recursive: true });
this.db = new Database(path, { create: true, strict: true });
this.now = now;
this.db.exec(`
PRAGMA journal_mode = WAL;
PRAGMA foreign_keys = ON;
CREATE TABLE IF NOT EXISTS schema_migrations (
version INTEGER PRIMARY KEY,
applied_at TEXT NOT NULL
);
`);
const current = this.db.query("SELECT COALESCE(MAX(version), 0) AS version FROM schema_migrations").get() as { version: number };
if (current.version < 1) {
const migrate = this.db.transaction(() => {
this.db.exec(`
CREATE TABLE IF NOT EXISTS payment_orders (
id TEXT PRIMARY KEY,
kind TEXT NOT NULL CHECK (kind IN ('initial', 'renewal')),
subscription_id TEXT,
customer_name TEXT NOT NULL,
customer_email TEXT NOT NULL,
company TEXT,
interval TEXT NOT NULL CHECK (interval IN ('monthly', 'annual')),
products_json TEXT NOT NULL,
unit_cents INTEGER NOT NULL CHECK (unit_cents > 0),
amount_cents INTEGER NOT NULL CHECK (amount_cents > 0),
currency TEXT NOT NULL CHECK (currency = 'EUR'),
status TEXT NOT NULL CHECK (status IN ('pending', 'paid', 'failed', 'canceled', 'rolled_back')),
provider TEXT NOT NULL,
provider_ref TEXT,
provider_payload TEXT,
manage_token_hash TEXT NOT NULL,
period_start TEXT,
error_code TEXT,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
paid_at TEXT
);
CREATE TABLE IF NOT EXISTS subscriptions (
id TEXT PRIMARY KEY,
initial_order_id TEXT NOT NULL UNIQUE REFERENCES payment_orders(id),
customer_email TEXT NOT NULL,
interval TEXT NOT NULL CHECK (interval IN ('monthly', 'annual')),
products_json TEXT NOT NULL,
unit_cents INTEGER NOT NULL CHECK (unit_cents > 0),
amount_cents INTEGER NOT NULL CHECK (amount_cents > 0),
currency TEXT NOT NULL CHECK (currency = 'EUR'),
status TEXT NOT NULL CHECK (status IN ('active', 'past_due', 'unpaid', 'canceled', 'requires_payment_method')),
card_token_enc TEXT,
manage_token_hash TEXT NOT NULL UNIQUE,
next_charge_at TEXT NOT NULL,
retry_at TEXT,
renew_attempts INTEGER NOT NULL DEFAULT 0,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
canceled_at TEXT
);
CREATE INDEX IF NOT EXISTS payment_orders_subscription_idx ON payment_orders(subscription_id, status);
CREATE INDEX IF NOT EXISTS subscriptions_due_idx ON subscriptions(status, next_charge_at, retry_at);
CREATE TABLE IF NOT EXISTS organizations (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
crm_company_id TEXT UNIQUE,
idp_tenant_id TEXT UNIQUE,
status TEXT NOT NULL CHECK (status IN ('active', 'suspended', 'canceled')),
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS organization_memberships (
id TEXT PRIMARY KEY,
organization_id TEXT NOT NULL REFERENCES organizations(id),
user_id TEXT NOT NULL,
role TEXT NOT NULL CHECK (role IN ('admin', 'member')),
status TEXT NOT NULL CHECK (status IN ('active', 'revoked')),
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
UNIQUE (organization_id, user_id)
);
CREATE INDEX IF NOT EXISTS memberships_user_idx ON organization_memberships(user_id, status);
CREATE TABLE IF NOT EXISTS workspaces (
id TEXT PRIMARY KEY,
organization_id TEXT NOT NULL REFERENCES organizations(id),
name TEXT NOT NULL,
kind TEXT NOT NULL DEFAULT 'managed' CHECK (kind = 'managed'),
status TEXT NOT NULL CHECK (status IN ('provisioning', 'active', 'suspended', 'deprovisioned')),
idp_workspace_id TEXT UNIQUE,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
);
CREATE INDEX IF NOT EXISTS workspaces_org_idx ON workspaces(organization_id, status);
CREATE TABLE IF NOT EXISTS app_entitlements (
id TEXT PRIMARY KEY,
organization_id TEXT NOT NULL REFERENCES organizations(id),
workspace_id TEXT NOT NULL REFERENCES workspaces(id),
app_id TEXT NOT NULL,
status TEXT NOT NULL CHECK (status IN ('granted', 'revoked')),
granted_at TEXT,
revoked_at TEXT,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
UNIQUE (organization_id, workspace_id, app_id)
);
CREATE INDEX IF NOT EXISTS entitlements_org_idx ON app_entitlements(organization_id, workspace_id, status);
CREATE TABLE IF NOT EXISTS installations (
id TEXT PRIMARY KEY,
organization_id TEXT NOT NULL REFERENCES organizations(id),
workspace_id TEXT NOT NULL REFERENCES workspaces(id),
app_id TEXT NOT NULL,
status TEXT NOT NULL CHECK (status IN ('pending', 'installing', 'installed', 'suspended', 'uninstalled', 'failed')),
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
UNIQUE (workspace_id, app_id)
);
CREATE INDEX IF NOT EXISTS installations_org_idx ON installations(organization_id, workspace_id, status);
CREATE TABLE IF NOT EXISTS domains (
id TEXT PRIMARY KEY,
organization_id TEXT NOT NULL REFERENCES organizations(id),
workspace_id TEXT REFERENCES workspaces(id),
hostname TEXT NOT NULL UNIQUE,
status TEXT NOT NULL CHECK (status IN ('pending', 'verified', 'active', 'disabled')),
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
);
CREATE INDEX IF NOT EXISTS domains_org_idx ON domains(organization_id, workspace_id, status);
CREATE TABLE IF NOT EXISTS lifecycle_revisions (
id TEXT PRIMARY KEY,
organization_id TEXT NOT NULL REFERENCES organizations(id),
workspace_id TEXT,
entity_type TEXT NOT NULL CHECK (entity_type IN ('organization', 'workspace', 'membership', 'entitlement', 'installation', 'domain')),
entity_id TEXT NOT NULL,
revision INTEGER NOT NULL CHECK (revision > 0),
from_state TEXT,
to_state TEXT NOT NULL,
reason TEXT,
actor_user_id TEXT,
metadata_json TEXT NOT NULL DEFAULT '{}',
created_at TEXT NOT NULL,
UNIQUE (entity_type, entity_id, revision)
);
CREATE INDEX IF NOT EXISTS lifecycle_org_idx ON lifecycle_revisions(organization_id, created_at, entity_type);
CREATE TRIGGER IF NOT EXISTS lifecycle_revisions_no_update
BEFORE UPDATE ON lifecycle_revisions
BEGIN
SELECT RAISE(ABORT, 'lifecycle revisions are append-only');
END;
CREATE TRIGGER IF NOT EXISTS lifecycle_revisions_no_delete
BEFORE DELETE ON lifecycle_revisions
BEGIN
SELECT RAISE(ABORT, 'lifecycle revisions are append-only');
END;
`);
this.db.query("INSERT INTO schema_migrations(version, applied_at) VALUES (1, ?)").run(this.now().toISOString());
});
migrate();
}
this.db.exec(`
CREATE TABLE IF NOT EXISTS renewal_fences (
subscription_id TEXT PRIMARY KEY REFERENCES subscriptions(id),
lease_token TEXT NOT NULL,
acquired_at INTEGER NOT NULL,
expires_at INTEGER NOT NULL
);
`);
}
createInitial(quote: CheckoutQuote, provider: string, manageTokenHash: string): OrderRow {
const id = `ord_${randomUUID()}`;
const timestamp = this.now().toISOString();
this.db.query(`
INSERT INTO payment_orders (
id, kind, customer_name, customer_email, company, interval, products_json,
unit_cents, amount_cents, currency, status, provider, manage_token_hash,
created_at, updated_at
) VALUES (?, 'initial', ?, ?, ?, ?, ?, ?, ?, ?, 'pending', ?, ?, ?, ?)
`).run(
id,
quote.customer.name,
quote.customer.email,
quote.customer.company ?? null,
quote.interval,
JSON.stringify(quote.productIds),
quote.unitCents,
quote.amountCents,
quote.currency,
provider,
manageTokenHash,
timestamp,
timestamp,
);
return this.order(id)!;
}
createRenewal(subscription: SubscriptionRow): OrderRow {
const attempt = subscription.renew_attempts + 1;
const periodKey = subscription.next_charge_at.slice(0, 10).replaceAll("-", "");
const id = `ren_${subscription.id.slice(4, 16)}_${periodKey}_${attempt}`;
const timestamp = this.now().toISOString();
this.db.query(`
INSERT OR IGNORE INTO payment_orders (
id, kind, subscription_id, customer_name, customer_email, interval, products_json,
unit_cents, amount_cents, currency, status, provider, manage_token_hash,
period_start, created_at, updated_at
) VALUES (?, 'renewal', ?, '', ?, ?, ?, ?, ?, ?, 'pending', ?, ?, ?, ?, ?)
`).run(
id,
subscription.id,
subscription.customer_email,
subscription.interval,
subscription.products_json,
subscription.unit_cents,
subscription.amount_cents,
subscription.currency,
providerNameFromInitial(this.db, subscription.initial_order_id),
subscription.manage_token_hash,
subscription.next_charge_at,
timestamp,
timestamp,
);
return this.order(id)!;
}
order(id: string): OrderRow | null {
return (this.db.query("SELECT * FROM payment_orders WHERE id = ?").get(id) as OrderRow | null) ?? null;
}
setPayload(id: string, payload: PaymentFormFields): void {
this.db.query("UPDATE payment_orders SET provider_payload = ?, updated_at = ? WHERE id = ? AND status = 'pending'")
.run(JSON.stringify(payload), this.now().toISOString(), id);
}
markOrder(id: string, status: Exclude<OrderStatus, "paid">, errorCode?: string): void {
const timestamp = this.now().toISOString();
this.db.query("UPDATE payment_orders SET status = ?, error_code = ?, updated_at = ? WHERE id = ? AND status = 'pending'")
.run(status, errorCode ?? null, timestamp, id);
}
rollbackOrder(id: string, transactionRef?: string): void {
const timestamp = this.now().toISOString();
const transaction = this.db.transaction(() => {
this.db.query("UPDATE payment_orders SET status = 'rolled_back', provider_ref = ?, updated_at = ? WHERE id = ?")
.run(transactionRef ?? null, timestamp, id);
this.db.query("UPDATE subscriptions SET status = 'canceled', canceled_at = ?, updated_at = ? WHERE initial_order_id = ?")
.run(timestamp, timestamp, id);
});
transaction();
}
completeInitial(order: OrderRow, encryptedCardToken: string | null, transactionRef?: string): SubscriptionRow {
const timestamp = this.now().toISOString();
const subscriptionId = `sub_${order.id.slice(4)}`;
const nextChargeAt = nextBillingDate(this.now(), order.interval).toISOString();
const status: SubscriptionStatus = encryptedCardToken ? "active" : "requires_payment_method";
const transaction = this.db.transaction(() => {
const current = this.order(order.id);
if (!current) throw new Error("Order not found");
if (current.status === "paid") return;
if (current.status !== "pending") throw new Error("Order is not payable");
this.db.query(`
INSERT INTO subscriptions (
id, initial_order_id, customer_email, interval, products_json, unit_cents,
amount_cents, currency, status, card_token_enc, manage_token_hash,
next_charge_at, created_at, updated_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`).run(
subscriptionId,
order.id,
order.customer_email,
order.interval,
order.products_json,
order.unit_cents,
order.amount_cents,
order.currency,
status,
encryptedCardToken,
order.manage_token_hash,
nextChargeAt,
timestamp,
timestamp,
);
this.db.query("UPDATE payment_orders SET status = 'paid', provider_ref = ?, paid_at = ?, updated_at = ? WHERE id = ?")
.run(transactionRef ?? null, timestamp, timestamp, order.id);
});
transaction();
return this.subscriptionByOrder(order.id)!;
}
completeRenewal(order: OrderRow, transactionRef?: string): void {
if (!order.subscription_id || !order.period_start) throw new Error("Invalid renewal order");
const timestamp = this.now().toISOString();
const nextChargeAt = nextBillingDate(new Date(order.period_start), order.interval).toISOString();
const transaction = this.db.transaction(() => {
const current = this.order(order.id);
if (!current) throw new Error("Renewal order not found");
if (current.status === "paid") return;
if (current.status !== "pending") throw new Error("Renewal order is not payable");
this.db.query("UPDATE payment_orders SET status = 'paid', provider_ref = ?, paid_at = ?, updated_at = ? WHERE id = ?")
.run(transactionRef ?? null, timestamp, timestamp, order.id);
this.db.query(`
UPDATE subscriptions
SET status = 'active', next_charge_at = ?, retry_at = NULL, renew_attempts = 0, updated_at = ?
WHERE id = ? AND status != 'canceled'
`).run(nextChargeAt, timestamp, order.subscription_id);
});
transaction();
}
failRenewal(order: OrderRow, errorCode: string): void {
if (!order.subscription_id) throw new Error("Invalid renewal order");
const subscription = this.subscription(order.subscription_id);
if (!subscription || subscription.status === "canceled") return;
const attempts = subscription.renew_attempts + 1;
const terminal = attempts >= 3;
const retryDays = attempts === 1 ? 1 : 3;
const retryAt = terminal ? null : new Date(this.now().getTime() + retryDays * 86_400_000).toISOString();
const timestamp = this.now().toISOString();
const transaction = this.db.transaction(() => {
this.db.query("UPDATE payment_orders SET status = 'failed', error_code = ?, updated_at = ? WHERE id = ? AND status = 'pending'")
.run(errorCode.slice(0, 80), timestamp, order.id);
this.db.query(`
UPDATE subscriptions
SET status = ?, renew_attempts = ?, retry_at = ?, updated_at = ?
WHERE id = ? AND status != 'canceled'
`).run(terminal ? "unpaid" : "past_due", attempts, retryAt, timestamp, order.subscription_id);
});
transaction();
}
subscription(id: string): SubscriptionRow | null {
return (this.db.query("SELECT * FROM subscriptions WHERE id = ?").get(id) as SubscriptionRow | null) ?? null;
}
subscriptionByOrder(orderId: string): SubscriptionRow | null {
return (this.db.query("SELECT * FROM subscriptions WHERE initial_order_id = ?").get(orderId) as SubscriptionRow | null) ?? null;
}
subscriptionByManageHash(hash: string): SubscriptionRow | null {
return (this.db.query("SELECT * FROM subscriptions WHERE manage_token_hash = ?").get(hash) as SubscriptionRow | null) ?? null;
}
dueSubscriptions(): SubscriptionRow[] {
const timestamp = this.now().toISOString();
return this.db.query(`
SELECT * FROM subscriptions
WHERE (status = 'active' AND next_charge_at <= ?)
OR (status = 'past_due' AND retry_at IS NOT NULL AND retry_at <= ?)
ORDER BY COALESCE(retry_at, next_charge_at)
LIMIT 50
`).all(timestamp, timestamp) as SubscriptionRow[];
}
acquireRenewalFence(subscriptionId: string): string | null {
const token = randomUUID();
const acquiredAt = this.now().getTime();
const expiresAt = acquiredAt + 2 * 60 * 60 * 1000;
const transaction = this.db.transaction(() => {
const result = this.db.query(`
INSERT INTO renewal_fences(subscription_id, lease_token, acquired_at, expires_at)
VALUES (?, ?, ?, ?)
ON CONFLICT(subscription_id) DO UPDATE SET
lease_token = excluded.lease_token,
acquired_at = excluded.acquired_at,
expires_at = excluded.expires_at
WHERE renewal_fences.expires_at <= excluded.acquired_at
`).run(subscriptionId, token, acquiredAt, expiresAt) as { changes?: number };
return result.changes === 1;
});
return transaction() ? token : null;
}
releaseRenewalFence(subscriptionId: string, leaseToken: string): void {
this.db.query("DELETE FROM renewal_fences WHERE subscription_id = ? AND lease_token = ?").run(subscriptionId, leaseToken);
}
pendingRenewal(subscriptionId: string): OrderRow | null {
return (this.db.query(`
SELECT * FROM payment_orders
WHERE subscription_id = ? AND kind = 'renewal' AND status = 'pending'
ORDER BY created_at DESC LIMIT 1
`).get(subscriptionId) as OrderRow | null) ?? null;
}
cancelSubscription(manageTokenHash: string): SubscriptionRow | null {
const timestamp = this.now().toISOString();
this.db.query(`
UPDATE subscriptions
SET status = 'canceled', canceled_at = ?, retry_at = NULL, updated_at = ?
WHERE manage_token_hash = ? AND status != 'canceled'
`).run(timestamp, timestamp, manageTokenHash);
return this.subscriptionByManageHash(manageTokenHash);
}
membershipForUser(organizationId: string, userId: string): MembershipRow | null {
return (this.db.query("SELECT * FROM organization_memberships WHERE organization_id = ? AND user_id = ? AND status = 'active'").get(organizationId, userId) as MembershipRow | null) ?? null;
}
organizationForUser(organizationId: string, userId: string): OrganizationRow | null {
return (this.db.query(`
SELECT o.* FROM organizations o
JOIN organization_memberships m ON m.organization_id = o.id
WHERE o.id = ? AND m.user_id = ? AND m.status = 'active'
`).get(organizationId, userId) as OrganizationRow | null) ?? null;
}
organizationsForUser(userId: string): OrganizationRow[] {
return this.db.query(`
SELECT o.* FROM organizations o
JOIN organization_memberships m ON m.organization_id = o.id
WHERE m.user_id = ? AND m.status = 'active'
ORDER BY o.created_at
`).all(userId) as OrganizationRow[];
}
createOrganization(name: string, userId: string, crmCompanyId: string | null, idpTenantId: string | null, workspaceName: string): {
organization: OrganizationRow;
workspace: WorkspaceRow;
membership: MembershipRow;
} {
const organizationId = `org_${randomUUID()}`;
this.assertExternalLinkConsistency(organizationId, crmCompanyId, idpTenantId);
if (crmCompanyId || idpTenantId) throw new Error("External CRM/IDP links require verified binding");
const workspaceId = `ws_${randomUUID()}`;
const membershipId = `mbr_${randomUUID()}`;
const timestamp = this.now().toISOString();
const transaction = this.db.transaction(() => {
this.db.query(`
INSERT INTO organizations(id, name, crm_company_id, idp_tenant_id, status, created_at, updated_at)
VALUES (?, ?, ?, ?, 'active', ?, ?)
`).run(organizationId, name, crmCompanyId, idpTenantId, timestamp, timestamp);
this.db.query(`
INSERT INTO organization_memberships(id, organization_id, user_id, role, status, created_at, updated_at)
VALUES (?, ?, ?, 'admin', 'active', ?, ?)
`).run(membershipId, organizationId, userId, timestamp, timestamp);
this.db.query(`
INSERT INTO workspaces(id, organization_id, name, kind, status, created_at, updated_at)
VALUES (?, ?, ?, 'managed', 'provisioning', ?, ?)
`).run(workspaceId, organizationId, workspaceName, timestamp, timestamp);
this.revision(organizationId, null, "organization", organizationId, null, "active", userId, "created");
this.revision(organizationId, workspaceId, "workspace", workspaceId, null, "provisioning", userId, "created");
this.revision(organizationId, null, "membership", membershipId, null, "active", userId, "created");
});
transaction();
return {
organization: this.organization(organizationId)!,
workspace: this.workspace(workspaceId)!,
membership: this.db.query("SELECT * FROM organization_memberships WHERE id = ?").get(membershipId) as MembershipRow,
};
}
private assertExternalLinkConsistency(organizationId: string, crmCompanyId: string | null, idpTenantId: string | null): void {
const links = [
["crm_company_id", crmCompanyId, "CRM company"],
["idp_tenant_id", idpTenantId, "IDP tenant"],
] as const;
for (const [column, externalId, label] of links) {
if (!externalId) continue;
const existing = this.db.query(`SELECT id FROM organizations WHERE ${column} = ?`).get(externalId) as { id: string } | null;
if (existing && existing.id !== organizationId) throw new Error(`${label} already belongs to another organization`);
}
}
organization(id: string): OrganizationRow | null {
return (this.db.query("SELECT * FROM organizations WHERE id = ?").get(id) as OrganizationRow | null) ?? null;
}
membershipForOrganization(id: string): MembershipRow[] {
return this.db.query("SELECT * FROM organization_memberships WHERE organization_id = ? ORDER BY created_at").all(id) as MembershipRow[];
}
addMembership(organizationId: string, userId: string, role: MembershipRole, actorUserId: string): MembershipRow {
const timestamp = this.now().toISOString();
const existing = this.db.query("SELECT * FROM organization_memberships WHERE organization_id = ? AND user_id = ?").get(organizationId, userId) as MembershipRow | null;
if (existing) {
if (existing.status === "active" && existing.role === role) return existing;
const transaction = this.db.transaction(() => {
this.db.query("UPDATE organization_memberships SET role = ?, status = 'active', updated_at = ? WHERE id = ?").run(role, timestamp, existing.id);
this.revision(organizationId, null, "membership", existing.id, existing.status, "active", actorUserId, "membership_restored");
});
transaction();
return this.db.query("SELECT * FROM organization_memberships WHERE id = ?").get(existing.id) as MembershipRow;
}
const id = `mbr_${randomUUID()}`;
const transaction = this.db.transaction(() => {
this.db.query(`
INSERT INTO organization_memberships(id, organization_id, user_id, role, status, created_at, updated_at)
VALUES (?, ?, ?, ?, 'active', ?, ?)
`).run(id, organizationId, userId, role, timestamp, timestamp);
this.revision(organizationId, null, "membership", id, null, "active", actorUserId, "membership_created");
});
transaction();
return this.db.query("SELECT * FROM organization_memberships WHERE id = ?").get(id) as MembershipRow;
}
revokeMembership(organizationId: string, memberId: string, actorUserId: string): MembershipRow | null {
const existing = this.db.query("SELECT * FROM organization_memberships WHERE id = ? AND organization_id = ?").get(memberId, organizationId) as MembershipRow | null;
if (!existing) return null;
if (existing.status === "revoked") return existing;
const timestamp = this.now().toISOString();
const transaction = this.db.transaction(() => {
this.db.query("UPDATE organization_memberships SET status = 'revoked', updated_at = ? WHERE id = ?").run(timestamp, memberId);
this.revision(organizationId, null, "membership", memberId, "active", "revoked", actorUserId, "membership_revoked");
});
transaction();
return this.db.query("SELECT * FROM organization_memberships WHERE id = ?").get(memberId) as MembershipRow;
}
workspacesForOrganization(organizationId: string): WorkspaceRow[] {
return this.db.query("SELECT * FROM workspaces WHERE organization_id = ? ORDER BY created_at").all(organizationId) as WorkspaceRow[];
}
workspace(id: string): WorkspaceRow | null {
return (this.db.query("SELECT * FROM workspaces WHERE id = ?").get(id) as WorkspaceRow | null) ?? null;
}
createWorkspace(organizationId: string, name: string, actorUserId: string): WorkspaceRow {
const id = `ws_${randomUUID()}`;
const timestamp = this.now().toISOString();
const transaction = this.db.transaction(() => {
this.db.query(`
INSERT INTO workspaces(id, organization_id, name, kind, status, created_at, updated_at)
VALUES (?, ?, ?, 'managed', 'provisioning', ?, ?)
`).run(id, organizationId, name, timestamp, timestamp);
this.revision(organizationId, id, "workspace", id, null, "provisioning", actorUserId, "created");
});
transaction();
return this.workspace(id)!;
}
setWorkspaceState(organizationId: string, workspaceId: string, state: WorkspaceRow["status"], actorUserId: string): WorkspaceRow | null {
const current = this.db.query("SELECT * FROM workspaces WHERE id = ? AND organization_id = ?").get(workspaceId, organizationId) as WorkspaceRow | null;
if (!current || current.status === state) return current;
const timestamp = this.now().toISOString();
const transaction = this.db.transaction(() => {
this.db.query("UPDATE workspaces SET status = ?, updated_at = ? WHERE id = ? AND organization_id = ?").run(state, timestamp, workspaceId, organizationId);
this.revision(organizationId, workspaceId, "workspace", workspaceId, current.status, state, actorUserId, "state_changed");
});
transaction();
return this.workspace(workspaceId);
}
entitlements(organizationId: string, workspaceId?: string): EntitlementRow[] {
if (workspaceId) this.assertWorkspaceOrganization(organizationId, workspaceId);
return (workspaceId
? this.db.query("SELECT * FROM app_entitlements WHERE organization_id = ? AND workspace_id = ? ORDER BY app_id").all(organizationId, workspaceId)
: this.db.query("SELECT * FROM app_entitlements WHERE organization_id = ? ORDER BY workspace_id, app_id").all(organizationId)) as EntitlementRow[];
}
entitlement(organizationId: string, workspaceId: string, appId: string): EntitlementRow | null {
this.assertWorkspaceOrganization(organizationId, workspaceId);
return (this.db.query("SELECT * FROM app_entitlements WHERE organization_id = ? AND workspace_id = ? AND app_id = ?").get(organizationId, workspaceId, appId) as EntitlementRow | null) ?? null;
}
setEntitlement(organizationId: string, workspaceId: string, appId: string, granted: boolean, actorUserId: string): EntitlementRow {
this.assertWorkspaceOrganization(organizationId, workspaceId);
const existing = this.entitlement(organizationId, workspaceId, appId);
const timestamp = this.now().toISOString();
const target = granted ? "granted" : "revoked";
if (existing?.status === target) return existing;
const id = existing?.id ?? `ent_${randomUUID()}`;
const transaction = this.db.transaction(() => {
if (existing) {
this.db.query(`
UPDATE app_entitlements
SET status = ?, granted_at = ?, revoked_at = ?, updated_at = ?
WHERE id = ? AND organization_id = ? AND workspace_id = ?
`).run(target, granted ? timestamp : existing.granted_at, granted ? null : timestamp, timestamp, id, organizationId, workspaceId);
} else {
this.db.query(`
INSERT INTO app_entitlements(id, organization_id, workspace_id, app_id, status, granted_at, revoked_at, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
`).run(id, organizationId, workspaceId, appId, target, granted ? timestamp : null, granted ? null : timestamp, timestamp, timestamp);
}
this.revision(organizationId, workspaceId, "entitlement", id, existing?.status ?? null, target, actorUserId, granted ? "granted" : "revoked");
});
transaction();
return this.entitlement(organizationId, workspaceId, appId)!;
}
installations(organizationId: string, workspaceId?: string): InstallationRow[] {
if (workspaceId) this.assertWorkspaceOrganization(organizationId, workspaceId);
return (workspaceId
? this.db.query("SELECT * FROM installations WHERE organization_id = ? AND workspace_id = ? ORDER BY app_id").all(organizationId, workspaceId)
: this.db.query("SELECT * FROM installations WHERE organization_id = ? ORDER BY workspace_id, app_id").all(organizationId)) as InstallationRow[];
}
installation(organizationId: string, workspaceId: string, appId: string): InstallationRow | null {
this.assertWorkspaceOrganization(organizationId, workspaceId);
return (this.db.query("SELECT * FROM installations WHERE organization_id = ? AND workspace_id = ? AND app_id = ?").get(organizationId, workspaceId, appId) as InstallationRow | null) ?? null;
}
createInstallation(organizationId: string, workspaceId: string, appId: string, actorUserId: string): InstallationRow {
this.assertWorkspaceOrganization(organizationId, workspaceId);
const existing = this.installation(organizationId, workspaceId, appId);
if (existing) return existing;
const id = `inst_${randomUUID()}`;
const timestamp = this.now().toISOString();
const transaction = this.db.transaction(() => {
this.db.query(`
INSERT INTO installations(id, organization_id, workspace_id, app_id, status, created_at, updated_at)
VALUES (?, ?, ?, ?, 'pending', ?, ?)
`).run(id, organizationId, workspaceId, appId, timestamp, timestamp);
this.revision(organizationId, workspaceId, "installation", id, null, "pending", actorUserId, "created");
});
transaction();
return this.installation(organizationId, workspaceId, appId)!;
}
setInstallationState(organizationId: string, installationId: string, state: InstallationRow["status"], actorUserId: string): InstallationRow | null {
const current = this.db.query("SELECT * FROM installations WHERE id = ? AND organization_id = ?").get(installationId, organizationId) as InstallationRow | null;
if (!current || current.status === state) return current;
const timestamp = this.now().toISOString();
const transaction = this.db.transaction(() => {
this.db.query("UPDATE installations SET status = ?, updated_at = ? WHERE id = ? AND organization_id = ?").run(state, timestamp, installationId, organizationId);
this.revision(organizationId, current.workspace_id, "installation", installationId, current.status, state, actorUserId, "state_changed");
});
transaction();
return this.db.query("SELECT * FROM installations WHERE id = ? AND organization_id = ?").get(installationId, organizationId) as InstallationRow;
}
domains(organizationId: string, workspaceId?: string): DomainRow[] {
if (workspaceId) this.assertWorkspaceOrganization(organizationId, workspaceId);
return (workspaceId
? this.db.query("SELECT * FROM domains WHERE organization_id = ? AND workspace_id = ? ORDER BY hostname").all(organizationId, workspaceId)
: this.db.query("SELECT * FROM domains WHERE organization_id = ? ORDER BY hostname").all(organizationId)) as DomainRow[];
}
domain(organizationId: string, domainId: string): DomainRow | null {
return (this.db.query("SELECT * FROM domains WHERE organization_id = ? AND id = ?").get(organizationId, domainId) as DomainRow | null) ?? null;
}
createDomain(organizationId: string, workspaceId: string | null, hostname: string, actorUserId: string): DomainRow {
if (workspaceId) this.assertWorkspaceOrganization(organizationId, workspaceId);
const existing = this.db.query("SELECT * FROM domains WHERE hostname = ?").get(hostname) as DomainRow | null;
if (existing) {
if (existing.organization_id !== organizationId) throw new Error("Domain already belongs to another organization");
if (existing.workspace_id !== workspaceId) throw new Error("Domain workspace does not match organization");
return existing;
}
const id = `dom_${randomUUID()}`;
const timestamp = this.now().toISOString();
const transaction = this.db.transaction(() => {
this.db.query(`
INSERT INTO domains(id, organization_id, workspace_id, hostname, status, created_at, updated_at)
VALUES (?, ?, ?, ?, 'pending', ?, ?)
`).run(id, organizationId, workspaceId, hostname, timestamp, timestamp);
this.revision(organizationId, workspaceId, "domain", id, null, "pending", actorUserId, "created");
});
transaction();
return this.domain(organizationId, id)!;
}
setDomainState(organizationId: string, domainId: string, state: DomainRow["status"], actorUserId: string): DomainRow | null {
const current = this.domain(organizationId, domainId);
if (!current || current.status === state) return current;
const timestamp = this.now().toISOString();
const transaction = this.db.transaction(() => {
this.db.query("UPDATE domains SET status = ?, updated_at = ? WHERE id = ? AND organization_id = ?").run(state, timestamp, domainId, organizationId);
this.revision(organizationId, current.workspace_id, "domain", domainId, current.status, state, actorUserId, "state_changed");
});
transaction();
return this.domain(organizationId, domainId);
}
private assertWorkspaceOrganization(organizationId: string, workspaceId: string): WorkspaceRow {
const workspace = this.workspace(workspaceId);
if (!workspace || workspace.organization_id !== organizationId) throw new Error("Workspace does not belong to this organization");
return workspace;
}
lifecycle(organizationId: string, entityType?: LifecycleEntity, entityId?: string): LifecycleRevisionRow[] {
if (entityType && entityId) return this.db.query("SELECT * FROM lifecycle_revisions WHERE organization_id = ? AND entity_type = ? AND entity_id = ? ORDER BY revision").all(organizationId, entityType, entityId) as LifecycleRevisionRow[];
if (entityType) return this.db.query("SELECT * FROM lifecycle_revisions WHERE organization_id = ? AND entity_type = ? ORDER BY created_at, revision").all(organizationId, entityType) as LifecycleRevisionRow[];
return this.db.query("SELECT * FROM lifecycle_revisions WHERE organization_id = ? ORDER BY created_at, revision LIMIT 500").all(organizationId) as LifecycleRevisionRow[];
}
private revision(organizationId: string, workspaceId: string | null, entityType: LifecycleEntity, entityId: string, fromState: string | null, toState: string, actorUserId: string | null, reason: string | null): void {
const row = this.db.query("SELECT COALESCE(MAX(revision), 0) AS revision FROM lifecycle_revisions WHERE entity_type = ? AND entity_id = ?").get(entityType, entityId) as { revision: number };
this.db.query(`
INSERT INTO lifecycle_revisions(id, organization_id, workspace_id, entity_type, entity_id, revision, from_state, to_state, reason, actor_user_id, metadata_json, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, '{}', ?)
`).run(`lrev_${randomUUID()}`, organizationId, workspaceId, entityType, entityId, row.revision + 1, fromState, toState, reason, actorUserId, this.now().toISOString());
}
close(): void {
this.db.close();
}
}
class DisabledProvider implements PaymentProvider {
readonly name = "disabled" as const;
async begin(): Promise<BeginResult> {
throw new Error("Payments are not configured");
}
async renew(): Promise<ChargeResult> {
throw new Error("Payments are not configured");
}
async recover(): Promise<RecoveryResult> {
return null;
}
}
class MockProvider implements PaymentProvider {
readonly name = "mock" as const;
constructor(private readonly publicUrl: string) {}
async begin(order: OrderRow): Promise<BeginResult> {
return { redirectUrl: `${this.publicUrl}/api/payments/mock/checkout/${encodeURIComponent(order.id)}` };
}
async renew(order: OrderRow): Promise<ChargeResult> {
return { success: true, transactionRef: `mock_${order.id}` };
}
async recover(): Promise<RecoveryResult> {
return null;
}
}
class MyPOSProvider implements WebhookProvider {
readonly name = "mypos" as const;
readonly checkoutUrl: string;
private readonly client: MyPOSClient;
constructor(private readonly publicUrl: string, env: Record<string, string | undefined>) {
const testMode = env.MYPOS_TEST_MODE === "true";
if (!publicUrl.startsWith("https://") || new URL(publicUrl).port) {
throw new Error("myPOS mode requires an HTTPS PAYMENTS_PUBLIC_URL without a port");
}
if (env.MYPOS_CONFIG_PACK) {
this.client = new MyPOSClient({ configPack: env.MYPOS_CONFIG_PACK, testMode, source: "AkurAI Platform" });
} else {
const storeId = required(env, "MYPOS_STORE_ID");
const walletNumber = required(env, "MYPOS_WALLET_NUMBER");
const privateKey = required(env, "MYPOS_PRIVATE_KEY").replaceAll("\\n", "\n");
const publicCertificate = required(env, "MYPOS_PUBLIC_CERT").replaceAll("\\n", "\n");
const keyIndex = Number(required(env, "MYPOS_KEY_INDEX"));
if (!Number.isInteger(keyIndex) || keyIndex < 1) throw new Error("MYPOS_KEY_INDEX must be a positive integer");
this.client = new MyPOSClient({ storeId, walletNumber, privateKey, publicCertificate, keyIndex, testMode, source: "AkurAI Platform" });
}
this.checkoutUrl = this.client.checkoutUrl;
}
async begin(order: OrderRow, products: Product[]): Promise<BeginResult> {
const unitAmount = order.unit_cents / 100;
const fields = this.client.generatePurchaseFields(
{ items: products.map((product) => ({ article: `${product.name} ${order.interval} subscription`, quantity: 1, price: unitAmount, amount: unitAmount })) },
order.id,
{
urlOk: `${this.publicUrl}/checkout/success?order=${encodeURIComponent(order.id)}`,
urlCancel: `${this.publicUrl}/checkout/cancel?order=${encodeURIComponent(order.id)}`,
urlNotify: `${this.publicUrl}/api/payments/webhook`,
currency: order.currency,
language: "EN",
cardTokenRequest: 2,
paymentParametersRequired: 2,
paymentMethod: 1,
customerEmail: order.customer_email,
note: `AkurAI ${order.interval} subscription`,
expiresIn: 900,
},
);
return {
redirectUrl: `${this.publicUrl}/api/payments/mypos/redirect/${encodeURIComponent(order.id)}`,
payload: fields,
};
}
validateWebhook(formData: FormData): WebhookValidationResult {
return this.client.validateWebhook(formData);
}
async renew(order: OrderRow, subscription: SubscriptionRow, cardToken: string, products: Product[]): Promise<ChargeResult> {
const unitAmount = subscription.unit_cents / 100;
const result = await this.client.iaPurchase({
cardToken,
orderId: order.id,
cart: { items: products.map((product) => ({ article: `${product.name} ${subscription.interval} renewal`, quantity: 1, price: unitAmount, amount: unitAmount })) },
currency: subscription.currency,
language: "EN",
note: `AkurAI ${subscription.interval} renewal`,
});
return result.success
? { success: true, transactionRef: result.ipcTrnref }
: { success: false, errorCode: `mypos_${result.status}` };
}
async recover(order: OrderRow): Promise<RecoveryResult> {
const result = await this.client.getPaymentStatus({ orderId: order.id });
if (!result.success) return null;
if (result.paymentStatus === 1) return { success: true, transactionRef: result.ipcTrnref };
if (result.paymentStatus === 2) return "pending";
if (result.paymentStatus === 3 || result.paymentStatus === 4) return { success: false, errorCode: `mypos_payment_${result.paymentStatus}` };
return null;
}
}
function configuredProvider(publicUrl: string, env: Record<string, string | undefined>): PaymentProvider {
const name = env.PAYMENTS_PROVIDER ?? "disabled";
if (name === "disabled") return new DisabledProvider();
if (name === "mock") return new MockProvider(publicUrl);
if (name === "mypos") return new MyPOSProvider(publicUrl, env);
throw new Error("PAYMENTS_PROVIDER must be disabled, mock, or mypos");
}
export function createPaymentService(options: ServiceOptions = {}): PaymentService {
const env = options.env ?? Bun.env;
const now = options.now ?? (() => new Date());
const publicUrl = trimSlash(options.publicUrl ?? env.PAYMENTS_PUBLIC_URL ?? `http://127.0.0.1:${env.PAYMENTS_PORT ?? "8198"}`);
const upstreamUrl = trimSlash(options.upstreamUrl ?? env.PLATFORM_UPSTREAM ?? "http://127.0.0.1:8098");
const catalogPath = options.catalogPath ?? env.PAYMENTS_CATALOG;
const catalog = options.catalog ?? (catalogPath ? loadCatalog(catalogPath) : validateCatalog(embeddedCatalog));
const provider = options.paymentProvider ?? configuredProvider(publicUrl, env);
const key = tokenKey(env, provider.name);
const identity = identityConfig(env);
const store = new Store(resolve(options.dbPath ?? env.PAYMENTS_DB ?? "data/payments.db"), now);
const productById = new Map(catalog.products.map((product) => [product.id, product]));
let renewalsRunning = false;
async function completeInitial(order: OrderRow, cardToken: string | undefined, transactionRef?: string): Promise<void> {
const encrypted = cardToken ? encrypt(cardToken, key) : null;
store.completeInitial(order, encrypted, transactionRef);
}
async function fetchHandler(request: Request): Promise<Response> {
const url = new URL(request.url);
try {
if (request.method === "GET" && url.pathname === "/api/payments/health") {
return json({ ok: true, provider: provider.name, paymentsEnabled: provider.name !== "disabled", products: catalog.products.filter((product) => product.available !== false).length });
}
if (request.method === "GET" && url.pathname === "/api/payments/catalog") {
return json({
currency: catalog.currency,
monthlyUnitCents: catalog.monthly_unit_cents,
annualUnitCents: Math.round(catalog.monthly_unit_cents * 12 * (100 - catalog.annual_discount_percent) / 100),
annualDiscountPercent: catalog.annual_discount_percent,
products: catalog.products,
provider: provider.name,
paymentsEnabled: provider.name !== "disabled",
});
}
const platformSegments = parsePlatformPath(url.pathname);
if (platformSegments) {
const userId = authenticatedUser(request, identity);
if (!userId) return json({ error: "Authentication required" }, 401);
let body: Record<string, unknown> = {};
if (request.method !== "GET" && request.method !== "DELETE") {
if (Number(request.headers.get("content-length") ?? 0) > 64_000) return json({ error: "Request is too large" }, 413);
if (!request.headers.get("content-type")?.startsWith("application/json")) return json({ error: "Expected application/json" }, 415);
try {
const parsed = await request.json();
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return json({ error: "JSON object required" }, 400);
body = parsed as Record<string, unknown>;
} catch {
return json({ error: "Invalid JSON" }, 400);
}
}
const [resource, resourceId, action, childId] = platformSegments;
if (resource === "workspaces" && resourceId) {
const workspace = store.workspace(resourceId);
if (!workspace || !store.organizationForUser(workspace.organization_id, userId)) return json({ error: "Not found" }, 404);
if (request.method !== "GET") return json({ error: "Method not allowed" }, 405);
return json({ workspace: publicWorkspace(workspace) });
}
if (resource === "organizations" && !resourceId) {
if (request.method === "GET") return json({ organizations: store.organizationsForUser(userId).map(publicOrganization) });
if (request.method !== "POST") return json({ error: "Method not allowed" }, 405);
const name = boundedText(body.name, 160, "Organization name");
const workspaceName = boundedText(body.workspaceName ?? "Managed Workspace", 160, "Workspace name");
const crmCompanyId = boundedOptionalText(body.crmCompanyId, 160, "CRM company ID");
const idpTenantId = boundedOptionalText(body.idpTenantId, 160, "IDP tenant ID");
if (crmCompanyId || idpTenantId) return json({ error: "External CRM/IDP links require verified binding" }, 400);
const result = store.createOrganization(name, userId, null, null, workspaceName);
return json({ organization: publicOrganization(result.organization), workspace: publicWorkspace(result.workspace), membership: publicMembership(result.membership) }, 201);
}
if (resource !== "organizations" || !resourceId) return json({ error: "Not found" }, 404);
const organization = store.organizationForUser(resourceId, userId);
if (!organization) return json({ error: "Not found" }, 404);
const membership = store.membershipForUser(resourceId, userId);
if (!membership) return json({ error: "Not found" }, 404);
if (!action) {
if (request.method !== "GET") return json({ error: "Method not allowed" }, 405);
return json({
organization: publicOrganization(organization),
role: membership.role,
workspaces: store.workspacesForOrganization(resourceId).map(publicWorkspace),
});
}
if (action === "workspaces") {
if (!childId) {
if (request.method === "GET") return json({ workspaces: store.workspacesForOrganization(resourceId).map(publicWorkspace) });
if (request.method !== "POST") return json({ error: "Method not allowed" }, 405);
if (membership.role !== "admin") return json({ error: "Administrator role required" }, 403);
const workspace = store.createWorkspace(resourceId, boundedText(body.name, 160, "Workspace name"), userId);
return json({ workspace: publicWorkspace(workspace) }, 201);
}
const workspace = store.workspace(childId);
if (!workspace || workspace.organization_id !== resourceId) return json({ error: "Not found" }, 404);
if (request.method === "GET") return json({ workspace: publicWorkspace(workspace) });
if (request.method !== "PATCH") return json({ error: "Method not allowed" }, 405);
if (membership.role !== "admin") return json({ error: "Administrator role required" }, 403);
const state = body.status;
if (!isOneOf(state, ["provisioning", "active", "suspended", "deprovisioned"])) return json({ error: "Invalid workspace status" }, 400);
return json({ workspace: publicWorkspace(store.setWorkspaceState(resourceId, childId, state, userId)!) });
}
if (action === "members") {
if (!childId && request.method === "GET") return json({ memberships: store.membershipForOrganization(resourceId).map(publicMembership) });
if (membership.role !== "admin") return json({ error: "Administrator role required" }, 403);
if (!childId && request.method === "POST") {
const memberUserId = boundedText(body.userId, 200, "User ID");
const role = body.role === "admin" ? "admin" : body.role === "member" ? "member" : null;
if (!role) return json({ error: "Role must be admin or member" }, 400);
return json({ membership: publicMembership(store.addMembership(resourceId, memberUserId, role, userId)) }, 201);
}
if (childId && request.method === "DELETE") {
const revoked = store.revokeMembership(resourceId, childId, userId);
return revoked ? json({ membership: publicMembership(revoked) }) : json({ error: "Not found" }, 404);
}
return json({ error: "Method not allowed" }, 405);
}
if (action === "entitlements") {
if (!childId && request.method === "GET") {
const workspaceId = url.searchParams.get("workspaceId") ?? undefined;
if (workspaceId) {
const workspace = store.workspace(workspaceId);
if (!workspace || workspace.organization_id !== resourceId) return json({ error: "Not found" }, 404);
}
return json({ entitlements: store.entitlements(resourceId, workspaceId).map(publicEntitlement) });
}
if (membership.role !== "admin") return json({ error: "Administrator role required" }, 403);
const workspaceId = boundedText(body.workspaceId ?? url.searchParams.get("workspaceId"), 100, "Workspace ID");
const appId = boundedText(body.appId ?? childId, 120, "App ID");
const workspace = store.workspace(workspaceId);
if (!workspace || workspace.organization_id !== resourceId) return json({ error: "Not found" }, 404);
const granted = body.action !== "revoke" && body.status !== "revoked" && request.method !== "DELETE";
if (request.method !== "POST" && request.method !== "PUT" && request.method !== "DELETE") return json({ error: "Method not allowed" }, 405);
return json({ entitlement: publicEntitlement(store.setEntitlement(resourceId, workspaceId, appId, granted, userId)) });
}
if (action === "installations") {
if (!childId && request.method === "GET") {
const workspaceId = url.searchParams.get("workspaceId") ?? undefined;
if (workspaceId) {
const workspace = store.workspace(workspaceId);
if (!workspace || workspace.organization_id !== resourceId) return json({ error: "Not found" }, 404);
}
return json({ installations: store.installations(resourceId, workspaceId).map(publicInstallation) });
}
if (membership.role !== "admin") return json({ error: "Administrator role required" }, 403);
if (!childId && request.method === "POST") {
const workspaceId = boundedText(body.workspaceId, 100, "Workspace ID");
const appId = boundedText(body.appId, 120, "App ID");
const workspace = store.workspace(workspaceId);
if (!workspace || workspace.organization_id !== resourceId) return json({ error: "Not found" }, 404);
return json({ installation: publicInstallation(store.createInstallation(resourceId, workspaceId, appId, userId)) }, 201);
}
if (childId && request.method === "PATCH") {
const state = body.status;
if (!isOneOf(state, ["pending", "installing", "installed", "suspended", "uninstalled", "failed"])) return json({ error: "Invalid installation status" }, 400);
const installation = store.setInstallationState(resourceId, childId, state, userId);
return installation ? json({ installation: publicInstallation(installation) }) : json({ error: "Not found" }, 404);
}
return json({ error: "Method not allowed" }, 405);
}
if (action === "domains") {
if (!childId && request.method === "GET") {
const workspaceId = url.searchParams.get("workspaceId") ?? undefined;
if (workspaceId) {
const workspace = store.workspace(workspaceId);
if (!workspace || workspace.organization_id !== resourceId) return json({ error: "Not found" }, 404);
}
return json({ domains: store.domains(resourceId, workspaceId).map(publicDomain) });
}
if (membership.role !== "admin") return json({ error: "Administrator role required" }, 403);
if (!childId && request.method === "POST") {
const workspaceId = body.workspaceId == null ? null : boundedOptionalText(body.workspaceId, 100);
if (workspaceId && (!store.workspace(workspaceId) || store.workspace(workspaceId)?.organization_id !== resourceId)) return json({ error: "Not found" }, 404);
const hostname = boundedText(body.hostname, 253, "Hostname").toLowerCase();
if (!/^(?=.{1,253}$)([a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)(?:\.([a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?))+$/.test(hostname)) return json({ error: "Invalid hostname" }, 400);
try {
return json({ domain: publicDomain(store.createDomain(resourceId, workspaceId, hostname, userId)) }, 201);
} catch (error) {
if (error instanceof Error && error.message.includes("another organization")) return json({ error: error.message }, 409);
throw error;
}
}
if (childId && request.method === "PATCH") {
const state = body.status;
if (!isOneOf(state, ["pending", "verified", "active", "disabled"])) return json({ error: "Invalid domain status" }, 400);
const domain = store.setDomainState(resourceId, childId, state, userId);
return domain ? json({ domain: publicDomain(domain) }) : json({ error: "Not found" }, 404);
}
return json({ error: "Method not allowed" }, 405);
}
if (action === "lifecycle") {
if (request.method !== "GET") return json({ error: "Method not allowed" }, 405);
if (membership.role !== "admin") return json({ error: "Administrator role required" }, 403);
const entityType = url.searchParams.get("entityType");
const entityId = url.searchParams.get("entityId");
if (entityType && !isOneOf(entityType, ["organization", "workspace", "membership", "entitlement", "installation", "domain"])) return json({ error: "Invalid entity type" }, 400);
const revisions = store.lifecycle(resourceId, entityType as LifecycleEntity | undefined, entityId ?? undefined);
return json({ revisions: revisions.map(publicLifecycleRevision), truncated: !entityType && revisions.length === 500 });
}
return json({ error: "Not found" }, 404);
}
if (request.method === "POST" && url.pathname === "/api/payments/checkout") {
if (provider.name === "disabled") return json({ error: "Payments are not configured" }, 503);
if (Number(request.headers.get("content-length") ?? 0) > 16_384) return json({ error: "Checkout request is too large" }, 413);
if (!request.headers.get("content-type")?.startsWith("application/json")) return json({ error: "Expected application/json" }, 415);
let body: unknown;
try {
body = await request.json();
} catch {
return json({ error: "Invalid JSON" }, 400);
}
let quote: CheckoutQuote;
try {
quote = quoteCheckout(catalog, body);
} catch (error) {
return json({ error: errorMessage(error) }, 400);
}
const manageToken = randomBytes(32).toString("base64url");
const order = store.createInitial(quote, provider.name, hashToken(manageToken));
try {
const begin = await provider.begin(order, quote.products);
if (begin.payload) store.setPayload(order.id, begin.payload);
return json({
orderId: order.id,
redirectUrl: begin.redirectUrl,
manageToken,
amountCents: order.amount_cents,
currency: order.currency,
}, 201);
} catch (error) {
store.markOrder(order.id, "failed", "checkout_initialization_failed");
console.error(JSON.stringify({ event: "payment_checkout_failed", orderId: order.id, provider: provider.name, error: safeError(error) }));
return json({ error: "Payment checkout could not be started" }, 502);
}
}
const orderStatus = matchPath(url.pathname, "/api/payments/orders/");
if (request.method === "GET" && orderStatus) {
const order = store.order(orderStatus);
if (!order) return json({ error: "Order not found" }, 404);
return json(publicOrder(order, productById));
}
if (request.method === "POST" && url.pathname === "/api/payments/webhook") {
if (provider.name !== "mypos") return new Response("Not available", { status: 404 });
const result = (provider as MyPOSProvider).validateWebhook(await request.formData());
if (!result.isValid) return new Response("Invalid signature", { status: 400 });
const order = store.order(result.orderId);
if (!order) return new Response("Order not found", { status: 404 });
if (Math.round(result.amount * 100) !== order.amount_cents || result.currency !== order.currency) {
return new Response("Payment mismatch", { status: 409 });
}
if (result.method === "IPCPurchaseNotify") {
await completeInitial(order, result.cardToken, result.transactionRef);
} else if (result.method === "IPCPurchaseRollback") {
store.rollbackOrder(order.id, result.transactionRef);
} else {
return new Response("Unsupported notification", { status: 400 });
}
return new Response("OK", { headers: { "content-type": "text/plain; charset=utf-8" } });
}
const myposRedirect = matchPath(url.pathname, "/api/payments/mypos/redirect/");
if (request.method === "GET" && myposRedirect && provider.name === "mypos") {
const order = store.order(myposRedirect);
if (!order || order.status !== "pending" || !order.provider_payload) return htmlPage("Checkout unavailable", "This checkout session is no longer available.", 404);
const fields = JSON.parse(order.provider_payload) as PaymentFormFields;
return paymentRedirectPage((provider as MyPOSProvider).checkoutUrl, fields);
}
const mockCheckout = matchPath(url.pathname, "/api/payments/mock/checkout/");
if (request.method === "GET" && mockCheckout && provider.name === "mock") {
const order = store.order(mockCheckout);
if (!order || order.status !== "pending") return htmlPage("Checkout unavailable", "This mock checkout session is no longer available.", 404);
return mockCheckoutPage(order, productById);
}
const mockOutcome = url.pathname.match(/^\/api\/payments\/mock\/orders\/([^/]+)\/(approve|decline|cancel)$/);
if (request.method === "POST" && mockOutcome && provider.name === "mock") {
const order = store.order(decodeURIComponent(mockOutcome[1]));
if (!order) return htmlPage("Order not found", "The mock order does not exist.", 404);
const outcome = mockOutcome[2];
if (outcome === "approve" && order.status === "pending") {
await completeInitial(order, `mock_card_${order.id}`, `mock_txn_${order.id}`);
return redirect(`${publicUrl}/checkout/success?order=${encodeURIComponent(order.id)}`);
}
if (outcome === "decline" && order.status === "pending") store.markOrder(order.id, "failed", "mock_declined");
if (outcome === "cancel" && order.status === "pending") store.markOrder(order.id, "canceled", "customer_canceled");
return redirect(`${publicUrl}/checkout/${outcome === "cancel" ? "cancel" : "success"}?order=${encodeURIComponent(order.id)}`);
}
const manageApi = matchPath(url.pathname, "/api/payments/subscriptions/");
if (manageApi && validManageToken(manageApi)) {
const hash = hashToken(manageApi);
if (request.method === "GET") {
const subscription = store.subscriptionByManageHash(hash);
if (!subscription) return json({ error: "Subscription not found" }, 404);
return json(publicSubscription(subscription, productById));
}
if (request.method === "POST" && url.searchParams.get("action") === "cancel") {
const subscription = store.cancelSubscription(hash);
if (!subscription) return json({ error: "Subscription not found" }, 404);
if (request.headers.get("accept")?.includes("application/json")) return json(publicSubscription(subscription, productById));
return redirect(`${publicUrl}/subscriptions/manage/${encodeURIComponent(manageApi)}`);
}
}
const managePage = matchPath(url.pathname, "/subscriptions/manage/");
if (request.method === "GET" && managePage && validManageToken(managePage)) {
const subscription = store.subscriptionByManageHash(hashToken(managePage));
if (!subscription) return htmlPage("Subscription not found", "This management link is invalid or has expired.", 404);
return subscriptionPage(subscription, managePage, productById);
}
if (request.method === "GET" && url.pathname === "/checkout/success") return checkoutResultPage("success");
if (request.method === "GET" && url.pathname === "/checkout/cancel") return checkoutResultPage("cancel");
return proxy(request, upstreamUrl);
} catch (error) {
const message = errorMessage(error);
if (/ is required$| is too long$|require verified binding$|does not belong to this organization$|does not match organization$/.test(message)) return json({ error: message }, 400);
console.error(JSON.stringify({ event: "payment_request_error", path: url.pathname, error: safeError(error) }));
return json({ error: "Payment service error" }, 500);
}
}
async function runRenewals(): Promise<void> {
if (provider.name === "disabled") return;
if (renewalsRunning) return;
renewalsRunning = true;
try {
for (const subscription of store.dueSubscriptions()) {
if (!subscription.card_token_enc) continue;
const leaseToken = store.acquireRenewalFence(subscription.id);
if (!leaseToken) continue;
try {
let order = store.pendingRenewal(subscription.id);
if (order) {
const recovered = await provider.recover(order);
if (recovered === "pending") continue;
if (recovered?.success) {
store.completeRenewal(order, recovered.transactionRef);
continue;
}
if (recovered && !recovered.success) {
store.failRenewal(order, recovered.errorCode ?? "renewal_failed");
continue;
}
store.failRenewal(order, "renewal_recovery_failed");
continue;
}
order = store.createRenewal(subscription);
const products = productsFor(JSON.parse(subscription.products_json) as string[], productById);
const cardToken = decrypt(subscription.card_token_enc, key);
const result = await provider.renew(order, subscription, cardToken, products);
if (result.success) store.completeRenewal(order, result.transactionRef);
else store.failRenewal(order, result.errorCode ?? "renewal_failed");
} catch (error) {
console.error(JSON.stringify({ event: "payment_renewal_error", subscriptionId: subscription.id, provider: provider.name, error: safeError(error) }));
} finally {
store.releaseRenewalFence(subscription.id, leaseToken);
}
}
} finally {
renewalsRunning = false;
}
}
return {
fetch: fetchHandler,
runRenewals,
close: () => store.close(),
provider: provider.name,
};
}
function parsePlatformPath(pathname: string): string[] | null {
const prefix = pathname.startsWith("/api/platform/") ? "/api/platform/" : pathname.startsWith("/api/") ? "/api/" : null;
if (!prefix) return null;
const parts = pathname.slice(prefix.length).split("/").filter(Boolean);
if (!["organizations", "workspaces"].includes(parts[0] ?? "")) return null;
try {
return parts.map((part) => decodeURIComponent(part));
} catch {
return null;
}
}
function identityConfig(env: Record<string, string | undefined>): IdentityConfig {
const value = (name: string): string | null => {
const candidate = env[name]?.trim();
return candidate ? candidate : null;
};
return {
trustedProxySecret: value("PAYMENTS_TRUSTED_PROXY_SECRET"),
credentialSecret: value("PAYMENTS_IDENTITY_SECRET"),
idpPublicKey: value("PAYMENTS_IDP_PUBLIC_KEY")?.replaceAll("\\n", "\n") ?? null,
issuer: value("PAYMENTS_IDENTITY_ISSUER"),
audience: value("PAYMENTS_IDENTITY_AUDIENCE"),
};
}
function authenticatedUser(request: Request, config: IdentityConfig): string | null {
const userIdHeader = request.headers.get("x-akurai-user-id")?.trim() ?? "";
const subjectHeader = request.headers.get("x-akurai-subject")?.trim() ?? "";
const explicit = userIdHeader || subjectHeader;
if (userIdHeader && subjectHeader && userIdHeader !== subjectHeader) return null;
if (explicit) {
if (!config.trustedProxySecret) return null;
const timestamp = request.headers.get("x-akurai-identity-timestamp")?.trim() ?? "";
const signature = request.headers.get("x-akurai-identity-signature")?.trim() ?? "";
const issuedAt = Number(timestamp);
if (!/^\d+$/.test(timestamp) || !Number.isSafeInteger(issuedAt) || Math.abs(Date.now() / 1000 - issuedAt) > 60) return null;
const expected = createHmac("sha256", config.trustedProxySecret).update(`${timestamp}.${explicit}`).digest();
const received = decodeBase64Url(signature);
if (!received || received.length !== expected.length || !timingSafeEqual(received, expected)) return null;
return boundedIdentity(explicit);
}
const authorization = request.headers.get("authorization") ?? "";
const token = authorization.match(/^Bearer\s+(.+)$/i)?.[1]?.trim();
if (!token) return null;
return verifyIdentityToken(token, config);
}
function verifyIdentityToken(token: string, config: IdentityConfig): string | null {
const parts = token.split(".");
if (parts.length !== 3) return null;
let header: Record<string, unknown>;
let claims: Record<string, unknown>;
try {
header = JSON.parse(Buffer.from(parts[0], "base64url").toString("utf8")) as Record<string, unknown>;
claims = JSON.parse(Buffer.from(parts[1], "base64url").toString("utf8")) as Record<string, unknown>;
} catch {
return null;
}
const signingInput = `${parts[0]}.${parts[1]}`;
const signature = decodeBase64Url(parts[2]);
if (!signature) return null;
let valid = false;
if (header.alg === "HS256" && config.credentialSecret) {
const expected = createHmac("sha256", config.credentialSecret).update(signingInput).digest();
valid = signature.length === expected.length && timingSafeEqual(signature, expected);
} else if (header.alg === "EdDSA" && config.idpPublicKey) {
try {
valid = verifySignature(null, Buffer.from(signingInput), config.idpPublicKey, signature);
} catch {
valid = false;
}
}
if (!valid) return null;
const now = Math.floor(Date.now() / 1000);
if (typeof claims.exp !== "number" || claims.exp < now) return null;
if (typeof claims.nbf === "number" && claims.nbf > now) return null;
if (config.issuer && claims.iss !== config.issuer) return null;
if (config.audience) {
const aud = claims.aud;
if (typeof aud !== "string" && !Array.isArray(aud)) return null;
if (typeof aud === "string" ? aud !== config.audience : !aud.includes(config.audience)) return null;
}
return typeof claims.sub === "string" ? boundedIdentity(claims.sub) : null;
}
function boundedIdentity(value: string): string | null {
const result = value.trim();
return result && result.length <= 200 ? result : null;
}
function decodeBase64Url(value: string): Buffer | null {
if (!/^[A-Za-z0-9_-]+$/.test(value)) return null;
try {
return Buffer.from(value, "base64url");
} catch {
return null;
}
}
function boundedText(value: unknown, max: number, label: string): string {
if (typeof value !== "string" || !value.trim()) throw new Error(`${label} is required`);
const result = value.trim();
if (result.length > max) throw new Error(`${label} is too long`);
return result;
}
function boundedOptionalText(value: unknown, max: number, label = "Value"): string | null {
if (value == null || value === "") return null;
return boundedText(value, max, label);
}
function isOneOf<T extends string>(value: unknown, values: readonly T[]): value is T {
return typeof value === "string" && values.includes(value as T);
}
function publicOrganization(row: OrganizationRow): Record<string, unknown> {
return { id: row.id, organizationId: row.id, name: row.name, crmCompanyId: row.crm_company_id, idpTenantId: row.idp_tenant_id, status: row.status, createdAt: row.created_at, updatedAt: row.updated_at };
}
function publicMembership(row: MembershipRow): Record<string, unknown> {
return { id: row.id, membershipId: row.id, organizationId: row.organization_id, userId: row.user_id, role: row.role, status: row.status, createdAt: row.created_at, updatedAt: row.updated_at };
}
function publicWorkspace(row: WorkspaceRow): Record<string, unknown> {
return { id: row.id, workspaceId: row.id, organizationId: row.organization_id, name: row.name, kind: row.kind, status: row.status, idpWorkspaceId: row.idp_workspace_id, createdAt: row.created_at, updatedAt: row.updated_at };
}
function publicEntitlement(row: EntitlementRow): Record<string, unknown> {
return { id: row.id, entitlementId: row.id, organizationId: row.organization_id, workspaceId: row.workspace_id, appId: row.app_id, status: row.status, grantedAt: row.granted_at, revokedAt: row.revoked_at, createdAt: row.created_at, updatedAt: row.updated_at };
}
function publicInstallation(row: InstallationRow): Record<string, unknown> {
return { id: row.id, installationId: row.id, organizationId: row.organization_id, workspaceId: row.workspace_id, appId: row.app_id, status: row.status, createdAt: row.created_at, updatedAt: row.updated_at };
}
function publicDomain(row: DomainRow): Record<string, unknown> {
return { id: row.id, domainId: row.id, organizationId: row.organization_id, workspaceId: row.workspace_id, hostname: row.hostname, status: row.status, createdAt: row.created_at, updatedAt: row.updated_at };
}
function publicLifecycleRevision(row: LifecycleRevisionRow): Record<string, unknown> {
return { id: row.id, revision: row.revision, organizationId: row.organization_id, workspaceId: row.workspace_id, entityType: row.entity_type, entityId: row.entity_id, fromState: row.from_state, toState: row.to_state, reason: row.reason, actorUserId: row.actor_user_id, metadata: JSON.parse(row.metadata_json), createdAt: row.created_at };
}
function loadCatalog(path: string): Catalog {
return validateCatalog(JSON.parse(readFileSync(path, "utf8")));
}
function providerNameFromInitial(db: Database, orderId: string): string {
const row = db.query("SELECT provider FROM payment_orders WHERE id = ?").get(orderId) as { provider: string } | null;
if (!row) throw new Error("Initial order not found");
return row.provider;
}
function productsFor(ids: string[], products: Map<string, Product>): Product[] {
const result = ids.map((id) => products.get(id));
if (result.some((product) => !product)) throw new Error("Subscription contains an unknown product");
return result as Product[];
}
function publicOrder(order: OrderRow, products: Map<string, Product>): Record<string, unknown> {
const ids = JSON.parse(order.products_json) as string[];
return {
id: order.id,
status: order.status,
interval: order.interval,
amountCents: order.amount_cents,
currency: order.currency,
products: productsFor(ids, products).map(({ id, name }) => ({ id, name })),
createdAt: order.created_at,
paidAt: order.paid_at,
};
}
function publicSubscription(subscription: SubscriptionRow, products: Map<string, Product>): Record<string, unknown> {
const ids = JSON.parse(subscription.products_json) as string[];
return {
id: subscription.id,
status: subscription.status,
interval: subscription.interval,
amountCents: subscription.amount_cents,
currency: subscription.currency,
email: maskEmail(subscription.customer_email),
products: productsFor(ids, products).map(({ id, name }) => ({ id, name })),
nextChargeAt: subscription.next_charge_at,
canceledAt: subscription.canceled_at,
};
}
function tokenKey(env: Record<string, string | undefined>, provider: "disabled" | "mock" | "mypos"): Buffer {
if (env.PAYMENTS_TOKEN_KEY) {
const key = Buffer.from(env.PAYMENTS_TOKEN_KEY, "base64");
if (key.length !== 32) throw new Error("PAYMENTS_TOKEN_KEY must decode to exactly 32 bytes");
return key;
}
if (provider === "mypos") throw new Error("PAYMENTS_TOKEN_KEY is required in myPOS mode");
return createHash("sha256").update("akurai-platform-mock-token-key").digest();
}
function encrypt(value: string, key: Buffer): string {
const iv = randomBytes(12);
const cipher = createCipheriv("aes-256-gcm", key, iv);
const encrypted = Buffer.concat([cipher.update(value, "utf8"), cipher.final()]);
return Buffer.concat([iv, cipher.getAuthTag(), encrypted]).toString("base64url");
}
function decrypt(value: string, key: Buffer): string {
const payload = Buffer.from(value, "base64url");
if (payload.length < 29) throw new Error("Invalid encrypted card token");
const decipher = createDecipheriv("aes-256-gcm", key, payload.subarray(0, 12));
decipher.setAuthTag(payload.subarray(12, 28));
return Buffer.concat([decipher.update(payload.subarray(28)), decipher.final()]).toString("utf8");
}
function hashToken(token: string): string {
return createHash("sha256").update(token).digest("hex");
}
function validManageToken(token: string): boolean {
return /^[A-Za-z0-9_-]{43}$/.test(token);
}
function required(env: Record<string, string | undefined>, key: string): string {
const value = env[key];
if (!value) throw new Error(`${key} is required in myPOS mode`);
return value;
}
function matchPath(pathname: string, prefix: string): string | null {
if (!pathname.startsWith(prefix)) return null;
const value = pathname.slice(prefix.length);
if (!value || value.includes("/")) return null;
try {
return decodeURIComponent(value);
} catch {
return null;
}
}
function json(value: unknown, status = 200): Response {
return new Response(JSON.stringify(value), {
status,
headers: {
"content-type": "application/json; charset=utf-8",
"cache-control": "no-store",
"x-content-type-options": "nosniff",
},
});
}
function redirect(location: string): Response {
return new Response(null, { status: 303, headers: { location, "cache-control": "no-store" } });
}
async function proxy(request: Request, upstream: string): Promise<Response> {
const source = new URL(request.url);
const target = `${upstream}${source.pathname}${source.search}`;
const headers = new Headers(request.headers);
headers.delete("host");
return fetch(target, {
method: request.method,
headers,
body: request.method === "GET" || request.method === "HEAD" ? undefined : request.body,
redirect: "manual",
});
}
function paymentRedirectPage(action: string, fields: PaymentFormFields): Response {
const nonce = randomBytes(18).toString("base64url");
const inputs = Object.entries(fields).map(([name, value]) => `<input type="hidden" name="${escapeHtml(name)}" value="${escapeHtml(value)}">`).join("");
const origin = new URL(action).origin;
return new Response(`<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width"><title>Opening secure checkout</title></head><body><form id="checkout" method="post" action="${escapeHtml(action)}">${inputs}<button type="submit">Continue to secure checkout</button></form><script nonce="${nonce}">document.getElementById("checkout").submit()</script></body></html>`, {
headers: htmlHeaders(`default-src 'none'; script-src 'nonce-${nonce}'; style-src 'unsafe-inline'; form-action ${origin}; base-uri 'none'`),
});
}
function mockCheckoutPage(order: OrderRow, products: Map<string, Product>): Response {
const names = productsFor(JSON.parse(order.products_json) as string[], products).map((product) => product.name);
const rows = names.map((name) => `<li>${escapeHtml(name)}</li>`).join("");
const base = `/api/payments/mock/orders/${encodeURIComponent(order.id)}`;
return themedPage("myPOS sandbox simulation", `
<span class="eyebrow">Test payment</span>
<h1>Complete your AkurAI subscription</h1>
<p class="lede">This local gateway follows the production redirect and callback flow. No money or card data is used.</p>
<div class="panel"><ul>${rows}</ul><div class="total"><span>${order.interval === "annual" ? "Annual" : "Monthly"} total</span><strong>${money(order.amount_cents, order.currency)}</strong></div></div>
<div class="test-card"><span>Sandbox test card</span><strong>•••• 4242</strong></div>
<div class="actions">
<form method="post" action="${base}/approve"><button class="primary" type="submit">Approve test payment</button></form>
<form method="post" action="${base}/decline"><button type="submit">Decline</button></form>
<form method="post" action="${base}/cancel"><button type="submit">Cancel</button></form>
</div>
`);
}
function subscriptionPage(subscription: SubscriptionRow, token: string, products: Map<string, Product>): Response {
const names = productsFor(JSON.parse(subscription.products_json) as string[], products).map((product) => product.name);
const rows = names.map((name) => `<li>${escapeHtml(name)}</li>`).join("");
const active = subscription.status !== "canceled";
return themedPage("Manage subscription", `
<span class="eyebrow">Subscription</span>
<h1>${active ? "Manage your plan" : "Subscription canceled"}</h1>
<p class="lede">${escapeHtml(maskEmail(subscription.customer_email))} · ${subscription.interval} billing</p>
<div class="panel"><ul>${rows}</ul><div class="total"><span>${active ? "Next charge" : "Final status"}</span><strong>${active ? `${money(subscription.amount_cents)} · ${escapeHtml(subscription.next_charge_at.slice(0, 10))}` : "Canceled"}</strong></div></div>
<div class="actions">
${active ? `<form method="post" action="/api/payments/subscriptions/${encodeURIComponent(token)}?action=cancel"><button class="danger" type="submit">Cancel automatic renewal</button></form>` : ""}
<a class="button" href="/">Back to AkurAI Platform</a>
</div>
`);
}
function checkoutResultPage(kind: "success" | "cancel"): Response {
const nonce = randomBytes(18).toString("base64url");
const initialTitle = kind === "cancel" ? "Checkout canceled" : "Confirming payment";
const initialText = kind === "cancel" ? "No payment was taken. You can return and try again when ready." : "Waiting for the secure payment confirmation.";
const script = kind === "success" ? `
const params=new URLSearchParams(location.search);const order=params.get("order");
const title=document.getElementById("result-title");const text=document.getElementById("result-text");const manage=document.getElementById("manage");
if(order){let checks=0;const check=async()=>{try{const response=await fetch("/api/payments/orders/"+encodeURIComponent(order),{headers:{accept:"application/json"}});const data=await response.json();if(data.status==="paid"){title.textContent="Subscription active";text.textContent="Payment confirmed. Your selected AkurAI apps are now subscribed.";const token=sessionStorage.getItem("akurai_manage_"+order);if(token){manage.href="/subscriptions/manage/"+encodeURIComponent(token);manage.hidden=false}return}if(["failed","canceled","rolled_back"].includes(data.status)){title.textContent="Payment not completed";text.textContent="The payment was not completed. No subscription renewal is active.";return}}catch{}if(++checks<15)setTimeout(check,1000);else{text.textContent="Confirmation is taking longer than expected. Keep this order reference: "+order}};check()}
` : "";
return new Response(`<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>${initialTitle}</title><style>${THEME_CSS}</style></head><body><main class="card"><span class="eyebrow">AkurAI Platform</span><h1 id="result-title">${initialTitle}</h1><p id="result-text" class="lede">${initialText}</p><div class="actions"><a id="manage" class="button primary" href="#" hidden>Manage subscription</a><a class="button" href="/">Back to platform</a></div></main>${script ? `<script nonce="${nonce}">${script}</script>` : ""}</body></html>`, {
headers: htmlHeaders(`default-src 'self'; script-src 'nonce-${nonce}'; style-src 'unsafe-inline'; connect-src 'self'; base-uri 'none'; frame-ancestors 'none'`),
});
}
function htmlPage(title: string, text: string, status: number): Response {
const response = themedPage(title, `<span class="eyebrow">AkurAI Platform</span><h1>${escapeHtml(title)}</h1><p class="lede">${escapeHtml(text)}</p><a class="button" href="/">Back to platform</a>`);
return new Response(response.body, { status, headers: response.headers });
}
function themedPage(title: string, body: string): Response {
return new Response(`<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>${escapeHtml(title)}</title><style>${THEME_CSS}</style></head><body><main class="card">${body}</main></body></html>`, {
headers: htmlHeaders("default-src 'none'; style-src 'unsafe-inline'; form-action 'self'; base-uri 'none'; frame-ancestors 'none'"),
});
}
function htmlHeaders(csp: string): Headers {
return new Headers({
"content-type": "text/html; charset=utf-8",
"cache-control": "no-store",
"content-security-policy": csp,
"referrer-policy": "no-referrer",
"x-content-type-options": "nosniff",
"x-frame-options": "DENY",
});
}
function escapeHtml(value: string): string {
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
}
function trimSlash(value: string): string {
return value.replace(/\/+$/, "");
}
function errorMessage(error: unknown): string {
return error instanceof Error ? error.message : "Invalid checkout details";
}
function safeError(error: unknown): string {
return (error instanceof Error ? error.message : String(error)).slice(0, 240).replace(/[\r\n]/g, " ");
}
const THEME_CSS = `
:root{color-scheme:dark;--bg:#080b12;--panel:#111722;--line:#273244;--text:#f7f9fc;--muted:#9aa8bc;--accent:#6ee7c2;--danger:#ff8a8a;font:16px/1.5 Inter,ui-sans-serif,system-ui,sans-serif}
*{box-sizing:border-box}body{min-height:100vh;margin:0;display:grid;place-items:center;padding:24px;background:radial-gradient(circle at 20% 0,#142536 0,transparent 38%),var(--bg);color:var(--text)}
.card{width:min(620px,100%);padding:clamp(28px,6vw,56px);border:1px solid var(--line);border-radius:24px;background:color-mix(in srgb,var(--panel) 94%,transparent);box-shadow:0 24px 80px #0008}
.eyebrow{color:var(--accent);font-size:.75rem;font-weight:800;letter-spacing:.14em;text-transform:uppercase}h1{margin:.6rem 0 1rem;font-size:clamp(2rem,6vw,3.5rem);line-height:1.02}.lede{color:var(--muted);font-size:1.08rem}
.panel,.test-card{margin:24px 0;padding:20px;border:1px solid var(--line);border-radius:16px;background:#090d15}.panel ul{margin:0 0 18px;padding-left:20px}.total,.test-card{display:flex;justify-content:space-between;gap:20px}.total{padding-top:16px;border-top:1px solid var(--line)}.total span,.test-card span{color:var(--muted)}
.actions{display:flex;flex-wrap:wrap;gap:10px;margin-top:28px}.actions form{margin:0}.button,button{appearance:none;display:inline-flex;align-items:center;justify-content:center;min-height:46px;padding:0 18px;border:1px solid var(--line);border-radius:12px;background:#171e2b;color:var(--text);font:inherit;font-weight:750;text-decoration:none;cursor:pointer}.primary{border-color:var(--accent);background:var(--accent);color:#06120f}.danger{border-color:color-mix(in srgb,var(--danger) 55%,var(--line));color:var(--danger)}button:hover,.button:hover{filter:brightness(1.12)}[hidden]{display:none!important}
`;
if (import.meta.main) {
const port = Number(Bun.env.PAYMENTS_PORT ?? 8198);
if (!Number.isInteger(port) || port < 1 || port > 65_535) throw new Error("PAYMENTS_PORT must be a valid port");
const hostname = Bun.env.PAYMENTS_HOST ?? "127.0.0.1";
const service = createPaymentService();
const server = Bun.serve({ hostname, port, fetch: service.fetch });
const renewalTimer = setInterval(() => void service.runRenewals(), 60 * 60 * 1000);
renewalTimer.unref();
const shutdown = () => {
clearInterval(renewalTimer);
service.close();
void server.stop();
};
process.once("SIGINT", shutdown);
process.once("SIGTERM", shutdown);
console.log(JSON.stringify({ service: "akurai-platform-payments", provider: service.provider, hostname, port: server.port }));
}