From 246f0b9c3432fc4254252728e56943850a01f57e Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 25 Jun 2026 01:38:04 +0000 Subject: [PATCH 01/11] feat: add extensions/v1 API service backed by Cloudflare D1 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Migrates the 4 extension registry endpoints from fossbilling/extension-directory into this Hono Worker, making api.fossbilling.net the canonical host for all FOSSBilling APIs. New endpoints: GET /extensions/v1/list[?type=...] → Extension[] GET /extensions/v1/:id → Extension GET /extensions/v1/:id/version → plain-text semver tag GET /extensions/v1/:id/badges/:type → image/svg+xml badge Data is stored in a new D1 database (DB_EXTENSIONS). Nested fields (author, releases, license, source) are serialised as JSON TEXT. A one-shot seed script at src/services/extensions/v1/scripts/seed-db.ts populates the database from the existing TypeScript data. Co-Authored-By: Claude Sonnet 4.6 Claude-Session: https://claude.ai/code/session_01AHT7incwQNb5yWbCNwi2a2 --- package-lock.json | 147 +- package.json | 1 + src/app/index.ts | 2 + src/lib/adapters/cloudflare/index.ts | 3 +- src/services/extensions/v1/database.ts | 111 ++ src/services/extensions/v1/db/schema.sql | 17 + src/services/extensions/v1/index.ts | 116 ++ src/services/extensions/v1/interfaces.ts | 80 + .../extensions/v1/scripts/authors-data.ts | 80 + .../extensions/v1/scripts/extensions-data.ts | 1252 ++++++++++++++++ src/services/extensions/v1/scripts/seed-db.ts | 97 ++ test/services/extensions/v1/index.test.ts | 348 +++++ worker-configuration.d.ts | 1324 ++++++++++++++--- wrangler.jsonc | 4 + 14 files changed, 3311 insertions(+), 271 deletions(-) create mode 100644 src/services/extensions/v1/database.ts create mode 100644 src/services/extensions/v1/db/schema.sql create mode 100644 src/services/extensions/v1/index.ts create mode 100644 src/services/extensions/v1/interfaces.ts create mode 100644 src/services/extensions/v1/scripts/authors-data.ts create mode 100644 src/services/extensions/v1/scripts/extensions-data.ts create mode 100644 src/services/extensions/v1/scripts/seed-db.ts create mode 100644 test/services/extensions/v1/index.test.ts diff --git a/package-lock.json b/package-lock.json index 33e15c9..901c992 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "dependencies": { "@octokit/graphql": "^9.0.3", "@octokit/request": "^10.0.2", + "badge-maker": "^6.0.0", "hono": "^4.11.4", "semver": "^7.7.2" }, @@ -1201,9 +1202,6 @@ "arm" ], "dev": true, - "libc": [ - "glibc" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1221,9 +1219,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1241,9 +1236,6 @@ "ppc64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1261,9 +1253,6 @@ "riscv64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1281,9 +1270,6 @@ "s390x" ], "dev": true, - "libc": [ - "glibc" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1301,9 +1287,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1321,9 +1304,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1341,9 +1321,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1361,9 +1338,6 @@ "arm" ], "dev": true, - "libc": [ - "glibc" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -1387,9 +1361,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -1413,9 +1384,6 @@ "ppc64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -1439,9 +1407,6 @@ "riscv64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -1465,9 +1430,6 @@ "s390x" ], "dev": true, - "libc": [ - "glibc" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -1491,9 +1453,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -1517,9 +1476,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -1543,9 +1499,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -1936,9 +1889,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1956,9 +1906,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -1976,9 +1923,6 @@ "ppc64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1996,9 +1940,6 @@ "s390x" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2016,9 +1957,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2036,9 +1974,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -2630,6 +2565,15 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/anafanafo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/anafanafo/-/anafanafo-2.0.0.tgz", + "integrity": "sha512-Nlfq7NC4AOkTJerWRIZcOAiMNtIDVIGWGvQ98O7Jl6Kr2Dk0dX5u4MqN778kSRTy5KRqchpLdF2RtLFEz9FVkQ==", + "license": "MIT", + "dependencies": { + "char-width-table-consumer": "^1.0.0" + } + }, "node_modules/assertion-error": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", @@ -2640,6 +2584,22 @@ "node": ">=12" } }, + "node_modules/badge-maker": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/badge-maker/-/badge-maker-6.0.0.tgz", + "integrity": "sha512-+P5EsfmFWAR2cA9M0q20pa4OVOgnOXZkfRYUkQYHanECXCIg10ZnSBzelfBtEjt03b4Vac/yC1h55/wJPLmaOg==", + "license": "(MIT OR Apache-2.0)", + "dependencies": { + "anafanafo": "2.0.0", + "css-color-converter": "^2.0.0" + }, + "bin": { + "badge": "lib/badge-cli.js" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/balanced-match": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", @@ -2663,6 +2623,12 @@ "node": ">=6.0.0" } }, + "node_modules/binary-search": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/binary-search/-/binary-search-1.3.6.tgz", + "integrity": "sha512-nbE1WxOTTrUWIfsfZ4aHGYu5DOuNkbxGokjV6Z2kxfJK3uaAb8zNK1muzOeipoLHZjInT4Br88BHpzevc681xA==", + "license": "CC0-1.0" + }, "node_modules/blake3-wasm": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/blake3-wasm/-/blake3-wasm-2.1.5.tgz", @@ -2748,6 +2714,15 @@ "node": ">=18" } }, + "node_modules/char-width-table-consumer": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/char-width-table-consumer/-/char-width-table-consumer-1.0.0.tgz", + "integrity": "sha512-Fz4UD0LBpxPgL9i29CJ5O4KANwaMnX/OhhbxzvNa332h+9+nRKyeuLw4wA51lt/ex67+/AdsoBQJF3kgX2feYQ==", + "license": "MIT", + "dependencies": { + "binary-search": "^1.3.5" + } + }, "node_modules/cjs-module-lexer": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.2.3.tgz", @@ -2755,6 +2730,17 @@ "dev": true, "license": "MIT" }, + "node_modules/color-convert": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-0.5.3.tgz", + "integrity": "sha512-RwBeO/B/vZR3dfKL1ye/vx8MHZ40ugzpyfeVG5GsiuGnrlMWe2o8wxBbLCpw9CsxV+wHuzYlCiWnybrIA0ling==" + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, "node_modules/content-type": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz", @@ -2804,6 +2790,23 @@ "node": ">= 8" } }, + "node_modules/css-color-converter": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/css-color-converter/-/css-color-converter-2.0.0.tgz", + "integrity": "sha512-oLIG2soZz3wcC3aAl/7Us5RS8Hvvc6I8G8LniF/qfMmrm7fIKQ8RIDDRZeKyGL2SrWfNqYspuLShbnjBMVWm8g==", + "license": "MIT", + "dependencies": { + "color-convert": "^0.5.2", + "color-name": "^1.1.4", + "css-unit-converter": "^1.1.2" + } + }, + "node_modules/css-unit-converter": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/css-unit-converter/-/css-unit-converter-1.1.2.tgz", + "integrity": "sha512-IiJwMC8rdZE0+xiEZHeru6YoONC4rfPMqGm2W85jMIbkFvv5nFTwJVFHam2eFrN6txmoUYFAFXiv8ICVeTO0MA==", + "license": "MIT" + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -3675,9 +3678,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -3699,9 +3699,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -3723,9 +3720,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -3747,9 +3741,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ diff --git a/package.json b/package.json index ba6add7..c4250a5 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "dependencies": { "@octokit/graphql": "^9.0.3", "@octokit/request": "^10.0.2", + "badge-maker": "^6.0.0", "hono": "^4.11.4", "semver": "^7.7.2" }, diff --git a/src/app/index.ts b/src/app/index.ts index 61848f6..589fb6b 100644 --- a/src/app/index.ts +++ b/src/app/index.ts @@ -2,6 +2,7 @@ import { Hono } from "hono"; import { contextStorage } from "hono/context-storage"; import { HTTPException } from "hono/http-exception"; import centralAlertsV1 from "../services/central-alerts/v1"; +import extensionsV1 from "../services/extensions/v1"; import versionsV1 from "../services/versions/v1"; import statsV1 from "../services/stats/v1"; import { platformMiddleware } from "../lib/middleware"; @@ -19,6 +20,7 @@ app.use("*", async (c, next) => { }); app.route("/central-alerts/v1", centralAlertsV1); +app.route("/extensions/v1", extensionsV1); app.route("/versions/v1", versionsV1); app.route("/stats/v1", statsV1); diff --git a/src/lib/adapters/cloudflare/index.ts b/src/lib/adapters/cloudflare/index.ts index 079ee57..92113ab 100644 --- a/src/lib/adapters/cloudflare/index.ts +++ b/src/lib/adapters/cloudflare/index.ts @@ -8,7 +8,8 @@ export function createCloudflareBindings( ): IPlatformBindings { return { databases: { - DB_CENTRAL_ALERTS: new CloudflareD1Adapter(env.DB_CENTRAL_ALERTS) + DB_CENTRAL_ALERTS: new CloudflareD1Adapter(env.DB_CENTRAL_ALERTS), + DB_EXTENSIONS: new CloudflareD1Adapter(env.DB_EXTENSIONS) }, caches: { CACHE_KV: new CloudflareKVAdapter(env.CACHE_KV), diff --git a/src/services/extensions/v1/database.ts b/src/services/extensions/v1/database.ts new file mode 100644 index 0000000..414055f --- /dev/null +++ b/src/services/extensions/v1/database.ts @@ -0,0 +1,111 @@ +import { DatabaseResult, IDatabase } from "../../../lib/interfaces"; +import { Extension, Release, Author, Repository, sortReleasesDescending } from "./interfaces"; + +export class ExtensionsDatabase { + private db: IDatabase; + + constructor(db: IDatabase) { + this.db = db; + } + + async getAllExtensions(type?: string): Promise> { + const query = type + ? `SELECT * FROM extensions WHERE type = ?` + : `SELECT * FROM extensions`; + + let result; + try { + const stmt = this.db.prepare(query); + result = type + ? await stmt.bind(type).all>() + : await stmt.all>(); + } catch (error) { + return { + data: null, + error: { + message: error instanceof Error ? error.message : String(error), + code: "DATABASE_ERROR" + } + }; + } + + if (!result.success) { + return { + data: null, + error: { + message: result.error || "Database query failed", + code: "DATABASE_ERROR" + } + }; + } + + const extensions = (result.results ?? []).map(parseExtensionRow); + return { data: extensions, error: null }; + } + + async getExtensionById(id: string): Promise> { + const query = `SELECT * FROM extensions WHERE LOWER(id) = LOWER(?)`; + + let result; + try { + result = await this.db + .prepare(query) + .bind(id) + .first>(); + } catch (error) { + return { + data: null, + error: { + message: error instanceof Error ? error.message : String(error), + code: "DATABASE_ERROR" + } + }; + } + + if (!result) { + return { + data: null, + error: { + message: `Cannot find extension by id: ${id}`, + code: "NOT_FOUND" + } + }; + } + + return { data: parseExtensionRow(result), error: null }; + } +} + +function parseJSON(value: unknown, fallback: T): T { + if (typeof value === "string") { + try { + return JSON.parse(value) as T; + } catch { + return fallback; + } + } + return value !== undefined && value !== null ? (value as T) : fallback; +} + +function parseExtensionRow(row: Record): Extension { + const releases = parseJSON(row.releases, []); + return { + id: row.id as string, + type: row.type as Extension["type"], + name: row.name as string, + description: row.description as string, + author: parseJSON(row.author, { + type: "user", + name: "", + id: "" as Lowercase + }), + releases: sortReleasesDescending(releases), + website: row.website as string, + license: parseJSON(row.license, { name: "" }), + icon_url: row.icon_url as string | undefined, + readme: row.readme as string, + source: parseJSON(row.source, { type: "custom", repo: "" }), + version: row.version as string, + download_url: row.download_url as string + }; +} diff --git a/src/services/extensions/v1/db/schema.sql b/src/services/extensions/v1/db/schema.sql new file mode 100644 index 0000000..14f0a6d --- /dev/null +++ b/src/services/extensions/v1/db/schema.sql @@ -0,0 +1,17 @@ +CREATE TABLE IF NOT EXISTS extensions ( + id TEXT PRIMARY KEY NOT NULL, + type TEXT NOT NULL, + name TEXT NOT NULL, + description TEXT NOT NULL, + author TEXT NOT NULL, + releases TEXT NOT NULL, + website TEXT NOT NULL, + license TEXT NOT NULL, + icon_url TEXT, + readme TEXT NOT NULL, + source TEXT NOT NULL, + version TEXT NOT NULL, + download_url TEXT NOT NULL +); + +CREATE INDEX IF NOT EXISTS idx_extensions_type ON extensions(type); diff --git a/src/services/extensions/v1/index.ts b/src/services/extensions/v1/index.ts new file mode 100644 index 0000000..6d1a803 --- /dev/null +++ b/src/services/extensions/v1/index.ts @@ -0,0 +1,116 @@ +import { Hono } from "hono"; +import { cors } from "hono/cors"; +import { trimTrailingSlash } from "hono/trailing-slash"; +import { makeBadge } from "badge-maker"; +import { getPlatform } from "../../../lib/middleware"; +import { ExtensionsDatabase } from "./database"; +import { getLatestRelease, sortReleasesDescending } from "./interfaces"; + +const extensionsV1 = new Hono<{ Bindings: CloudflareBindings }>(); + +extensionsV1.use("/*", cors({ origin: "*" })); +extensionsV1.use("/*", trimTrailingSlash()); + +extensionsV1.get("/list", async (c) => { + const platform = getPlatform(c); + const db = new ExtensionsDatabase(platform.getDatabase("DB_EXTENSIONS")); + const type = c.req.query("type"); + + const { data, error } = await db.getAllExtensions(type); + if (error) { + return c.json({ error: { message: "Unable to load extensions" } }, 500); + } + + return c.json({ result: data }); +}); + +extensionsV1.get("/:id/badges/:type", async (c) => { + const id = c.req.param("id"); + const badgeType = c.req.param("type"); + + const platform = getPlatform(c); + const db = new ExtensionsDatabase(platform.getDatabase("DB_EXTENSIONS")); + + const { data: extension, error } = await db.getExtensionById(id); + if (error || !extension) { + return c.json( + { error: { message: error?.message ?? "Extension not found" } }, + 500 + ); + } + + const sorted = sortReleasesDescending(extension.releases); + const latest = sorted[0]; + + const knownTypes: Record = { + version: { + label: "Latest version", + message: latest ? `v${latest.tag}` : "unknown" + }, + min_fossbilling_version: { + label: "Minimum FOSSBilling version", + message: latest ? `v${latest.min_fossbilling_version}` : "unknown" + }, + license: { + label: "License", + message: extension.license.name + } + }; + + const matched = knownTypes[badgeType.toLowerCase()]; + const format = { + label: matched ? matched.label : "Unknown type", + message: matched ? matched.message : badgeType, + color: matched ? "blue" : "red" + }; + + const colorParam = c.req.query("color"); + if (colorParam) { + format.color = colorParam; + } + + const svg = makeBadge(format); + c.header("Content-Type", "image/svg+xml"); + return c.body(svg); +}); + +extensionsV1.get("/:id/version", async (c) => { + const id = c.req.param("id"); + + const platform = getPlatform(c); + const db = new ExtensionsDatabase(platform.getDatabase("DB_EXTENSIONS")); + + const { data: extension, error } = await db.getExtensionById(id); + if (error || !extension) { + return c.json( + { error: { message: error?.message ?? "Extension not found" } }, + 500 + ); + } + + const latest = getLatestRelease(extension); + if (!latest) { + return c.json({ error: { message: "No releases found" } }, 500); + } + + return c.text(latest.tag); +}); + +extensionsV1.get("/:id", async (c) => { + const id = c.req.param("id"); + + const platform = getPlatform(c); + const db = new ExtensionsDatabase(platform.getDatabase("DB_EXTENSIONS")); + + const { data: extension, error } = await db.getExtensionById(id); + if (error || !extension) { + return c.json( + { error: { message: error?.message ?? "Extension not found" } }, + 500 + ); + } + + return c.json({ result: extension }); +}); + +export default extensionsV1; diff --git a/src/services/extensions/v1/interfaces.ts b/src/services/extensions/v1/interfaces.ts new file mode 100644 index 0000000..31d5611 --- /dev/null +++ b/src/services/extensions/v1/interfaces.ts @@ -0,0 +1,80 @@ +import { gt, lt } from "semver"; + +export type Extension = { + id: string; + type: + | "mod" + | "theme" + | "payment-gateway" + | "server-manager" + | "domain-registrar" + | "hook" + | "translation"; + name: string; + description: string; + author: Author; + releases: Release[]; + website: string; + license: { + name: string; + URL?: string; + }; + icon_url?: string; + readme: string; + source: Repository; + version: string; + download_url: string; +}; + +export type Repository = { + type: "github" | "gitlab" | "custom"; + repo: string; +}; + +export type Author = Organization | User; + +export type Organization = { + type: "organization"; + name: string; + id: Lowercase; + URL?: string; +}; + +export type User = { + type: "user"; + name: string; + id: Lowercase; + URL?: string; +}; + +export type Release = { + tag: string; + date: string; + download_url: string; + changelog_url?: string; + min_fossbilling_version: string; +}; + +export function getLatestRelease(extension: Extension): Release | undefined { + if (extension.releases.length === 0) { + return undefined; + } + + let latestRelease = extension.releases[0]; + for (let i = 1; i < extension.releases.length; i++) { + const release = extension.releases[i]; + if (gt(release.tag, latestRelease.tag)) { + latestRelease = release; + } + } + + return latestRelease; +} + +export function sortReleasesDescending(releases: Release[]): Release[] { + return [...releases].sort((a, b) => { + if (gt(a.tag, b.tag)) return -1; + if (lt(a.tag, b.tag)) return 1; + return 0; + }); +} diff --git a/src/services/extensions/v1/scripts/authors-data.ts b/src/services/extensions/v1/scripts/authors-data.ts new file mode 100644 index 0000000..8a00f62 --- /dev/null +++ b/src/services/extensions/v1/scripts/authors-data.ts @@ -0,0 +1,80 @@ +import type { Author } from "../interfaces"; + +export const authorData: Author[] = [ + { + type: "organization", + name: "fossbilling", + id: "fossbilling", + URL: "https://fossbilling.org", + }, + { + type: "organization", + name: "bitcart", + id: "bitcart", + URL: "https://bitcart.ai", + }, + { + type: "organization", + name: "uddoktapay", + id: "uddoktapay", + URL: "https://uddoktapay.com", + }, + { + type: "organization", + name: "albinvar", + id: "albinvar", + URL: "https://github.com/albinvar", + }, + { + type: "organization", + name: "christiangabs", + id: "christiangabs", + URL: "https://github.com/christiangabs", + }, + { + type: "organization", + name: "neto737", + id: "neto737", + URL: "https://github.com/neto737", + }, + { + type: "organization", + name: "fzfr", + id: "fzfr", + URL: "https://github.com/FZFR", + }, + { + type: "organization", + name: "netim", + id: "netim", + URL: "https://netim.com", + }, + { + type: "organization", + name: "devife", + id: "devife", + URL: "https://devife.com", + }, + { + type: "organization", + name: "demassimo", + id: "demassimo", + URL: "https://github.com/demassimo", + }, + { + type: "organization", + name: "coinpayportal", + id: "coinpayportal", + URL: "https://coinpayportal.com", + }, + { + type: "organization", + name: "ServMe IT Limited", + id: "servmeit", + URL: "https://www.servmeit.co.nz", + }, +]; + +export function findAuthorByID(id: string): Author | undefined { + return authorData.find((author) => author.id === id); +} diff --git a/src/services/extensions/v1/scripts/extensions-data.ts b/src/services/extensions/v1/scripts/extensions-data.ts new file mode 100644 index 0000000..7d9dc47 --- /dev/null +++ b/src/services/extensions/v1/scripts/extensions-data.ts @@ -0,0 +1,1252 @@ +import type { Extension, Author } from "../interfaces"; +import { findAuthorByID } from "./authors-data"; + +function getAuthor(id: string): Author { + const author = findAuthorByID(id); + if (!author) throw new Error(`Author not found: ${id}`); + return author; +} + +export const extensionData: Extension[] = [ + { + id: "Example", + type: "mod", + name: "Example", + description: "An example module for developers to get started.", + version: "0.0.5", + download_url: "https://github.com/FOSSBilling/example-module/releases/download/0.0.5/Example.zip", + releases: [ + { + tag: "0.0.5", + date: "2024-02-12T06:36:38+00:00", + download_url: + "https://github.com/FOSSBilling/example-module/releases/download/0.0.5/Example.zip", + changelog_url: + "https://github.com/FOSSBilling/example-module/releases/tag/0.0.5", + min_fossbilling_version: "0.6", + }, + { + tag: "0.0.4", + date: "2023-09-25T07:36:29Z", + download_url: + "https://github.com/FOSSBilling/example-module/releases/download/0.0.4/Example.zip", + changelog_url: + "https://github.com/FOSSBilling/example-module/releases/tag/0.0.4", + min_fossbilling_version: "0.5", + }, + { + tag: "0.0.3", + date: "2023-06-13T14:11:11Z", + download_url: + "https://github.com/FOSSBilling/example-module/releases/download/0.0.3/Example.zip", + changelog_url: + "https://github.com/FOSSBilling/example-module/releases/tag/0.0.3", + min_fossbilling_version: "0.5", + }, + { + tag: "0.0.2", + date: "2023-05-01T08:05:02Z", + download_url: + "https://github.com/FOSSBilling/example-module/releases/download/0.0.2/Example.zip", + changelog_url: + "https://github.com/FOSSBilling/example-module/releases/tag/0.0.2", + min_fossbilling_version: "0.1", + }, + { + tag: "0.0.1", + date: "2023-03-31T08:21:28Z", + download_url: + "https://github.com/FOSSBilling/example-module/releases/download/0.0.1/Example.zip", + changelog_url: + "https://github.com/FOSSBilling/example-module/releases/tag/0.0.1", + min_fossbilling_version: "0.1", + }, + ], + author: getAuthor("fossbilling"), + license: { + name: "Apache 2.0", + URL: "https://www.apache.org/licenses/LICENSE-2.0", + }, + source: { + type: "github", + repo: "FOSSBilling/example-module", + }, + icon_url: "https://raw.githubusercontent.com/FOSSBilling/example-module/main/src/icon.svg", + website: "https://fossbilling.org", + readme: + `# Example module README file + +This module provides a starting point for the developers on creating their FOSSBilling module. + +Explore the files and comments in the code to understand the structure of the module better. See the social links on [our website](https://fossbilling.org) if you need further information. This module has its own [GitHub repository](https://github.com/FOSSBilling/example-module) where you can submit issues and pull requests. + +In general, we use modules to extend the functionality of FOSSBilling. + +All modules can communicate with the other modules using their API endpoints. + +## Technical requirements about modules + +## Required + + - Module folder has to contain a **manifest.json** file to describe itself. The module engine will look for this file to find information about your extension. + +## Optional + + - **README.md** - A file which generally is used to hold a getting started guide or installation instructions for your module. + - **html_admin** - A folder holding front-end templates (\`*.html.twig files\`) for the administrator panel. + - **html_client** - A folder holding front-end templates (\`*.html.twig files\`) for the client / guest area. + +### Controller folder + + - **Admin.php** - Defines the module's routes and navigation items for the administrator panel. + - **Client.php** - Used to define the module's routes for the client / guest area. + +### Api folder + + - **Admin.php** - Administrator API, only authorized administrators will be able to call these endpoints. + - **Client.php** - Client API, only logged in clients will be able to call these endpoints. + - **Guest.php** - Guest API, no authorization is needed for these endpoints. Don't provide confidential data over these endpoints. Anybody over the internet will be able to access these information, including bots. + +## Tips + +We recommend hosting your extensions on a public [GitHub](https://github.com) repository. + +### Automated compatibility checking + +As FOSSBilling evolves and matures, its internal functionality changes, which can create compatibility issues between your module and FOSSBilling. +To help developers catch these issues early on, we've designed a workflow that enables you to perform a PHPStan analysis of your module with both the latest FOSSBilling release and its preview builds. +While PHPStan cannot perform live tests, it's a useful tool to verify that your module doesn't reference missing functions, use incorrect types, or have other common low-level issues. + +#### Setup + +More in-depth instructions are planned. For now, check out the required files: + + - [php-ci.yml](https://github.com/FOSSBilling/example-module/blob/main/.github/workflows/php-ci.yml) + - [phpstan.neon](https://github.com/FOSSBilling/example-module/blob/main/phpstan.neon) + +## Licensing + +This extension is open source software and is released under the Apache v2.0 license. See [LICENSE](LICENSE) for the full license terms. + +This product includes the following third party work: + + - Open Source Iconography by [Pictogrammers](https://pictogrammers.com/) licensed under the [Pictogrammers Free License](https://pictogrammers.com/docs/general/license/).`, + }, + { + id: "Mollie", + type: "payment-gateway", + name: "Mollie", + description: "Mollie extension for FOSSBilling", + author: getAuthor("fossbilling"), + license: { + name: "Apache 2.0", + URL: "https://www.apache.org/licenses/LICENSE-2.0", + }, + source: { + type: "github", + repo: "FOSSBilling/Mollie", + }, + version: "0.0.5", + download_url: + "https://github.com/FOSSBilling/Mollie/releases/download/0.0.5/Mollie.zip", + releases: [ + { + tag: "0.0.5", + date: "2025-09-09T15:18:04Z", + download_url: + "https://github.com/FOSSBilling/Mollie/releases/download/0.0.5/Mollie.zip", + changelog_url: + "https://github.com/FOSSBilling/Mollie/releases/tag/0.0.5", + min_fossbilling_version: "0.5", + }, + { + tag: "0.0.4", + date: "2025-02-14T14:17:49Z", + download_url: + "https://github.com/FOSSBilling/Mollie/releases/download/0.0.4/Mollie.zip", + changelog_url: + "https://github.com/FOSSBilling/Mollie/releases/tag/0.0.4", + min_fossbilling_version: "0.5", + }, + { + tag: "0.0.3", + date: "2023-06-13T14:17:49Z", + download_url: + "https://github.com/FOSSBilling/Mollie/releases/download/0.0.3/Mollie.zip", + changelog_url: + "https://github.com/FOSSBilling/Mollie/releases/tag/0.0.3", + min_fossbilling_version: "0.5", + }, + { + tag: "0.0.2", + date: "2023-05-08T20:14:07Z", + download_url: + "https://github.com/FOSSBilling/Mollie/releases/download/0.0.2/Mollie.zip", + changelog_url: + "https://github.com/FOSSBilling/Mollie/releases/tag/0.0.2", + min_fossbilling_version: "0.1", + }, + { + tag: "0.0.1", + date: "2023-05-08T17:16:01Z", + download_url: + "https://github.com/FOSSBilling/Mollie/releases/download/0.0.1/Mollie.zip", + changelog_url: + "https://github.com/FOSSBilling/Mollie/releases/tag/0.0.1", + min_fossbilling_version: "0.1", + }, + ], + icon_url: + "https://raw.githubusercontent.com/FOSSBilling/Mollie/main/src/Mollie.png", + website: "https://fossbilling.org", + readme: `![Mollie for FOSSBilling](https://user-images.githubusercontent.com/35808275/236844335-8085c37f-ea5f-4e6a-9712-8c5bea7ebcaf.png) + # Mollie for FOSSBilling + + + Quickly and easily integrate [Mollie](https://mollie.com) into your [FOSSBilling](https://fossbilling.org) instance using this extension. + + > **Warning** + > This extension, like FOSSBilling itself is under active development but is currently very much beta software. This means that there may be stability or security issues and it is not yet recommended for use in active production environments! + + ## Installation + ### Extension directory + The easiest way to install this extension is by using the [FOSSBilling extension directory](https://extensions.fossbilling.org/extension/Mollie). + + ### Manual installation + 1. Download the latest release from the [extension directory](https://extensions.fossbilling.org/extension/Mollie) + 2. Create a new folder named \`Mollie\` in the \`/library/Payment/Adapter\` directory of your FOSSBilling installation + 3. Extract the archive you've downloaded in the first step into the new directory + 4. Go to the "Payment gateways" page in your admin panel (under the "System" menu in the navigation bar) and find Mollie in the "New payment gateway" tab + 5. Click the cog icon next to Mollie to install and configure Mollie + + ## Contributing + We love our contributors! Feel free to create a pull request if you want to help out. + + Not a developer? No problem! You can also help us by [reporting bugs, creating feature requests](https://github.com/FOSSBilling/mollie/issues/new/choose) or by donating to the project over [GitHub sponsors](https://github.com/sponsors/FOSSBilling) or [Open Collective](https://opencollective.com/fossbilling). + + ## Licensing + This extension is licensed under the Apache 2.0 license. See the [LICENSE](LICENSE) file for more information. + + ## Disclaimer + This extension is not affiliated with Mollie B.V. in any way. Mollie is a registered trademark of Mollie B.V. The "official" word refers exclusively to the fact that this extension is the only one that is officially supported by the FOSSBilling team. It does not imply any endorsement by Mollie B.V.`, + }, + { + id: "Bitcart", + type: "payment-gateway", + name: "Bitcart", + description: "Bitcart extension for FOSSBilling", + author: getAuthor("bitcart"), + license: { + name: "MIT", + URL: "https://github.com/bitcart/bitcart-fossbilling/blob/master/LICENSE", + }, + source: { + type: "github", + repo: "bitcart/bitcart-fossbilling", + }, + version: "1.1.0", + download_url: + "https://github.com/bitcart/bitcart-fossbilling/releases/download/1.1.0/Bitcart.zip", + releases: [ + { + tag: "1.1.0", + date: "2023-06-15T19:22:52Z", + download_url: + "https://github.com/bitcart/bitcart-fossbilling/releases/download/1.1.0/Bitcart.zip", + changelog_url: + "https://github.com/bitcart/bitcart-fossbilling/releases/tag/1.1.0", + min_fossbilling_version: "0.5", + }, + { + tag: "1.0.0", + date: "2023-05-15T19:22:52Z", + download_url: + "https://github.com/bitcart/bitcart-fossbilling/releases/download/1.0.0/BitcartCC.zip", + changelog_url: + "https://github.com/bitcart/bitcart-fossbilling/releases/tag/1.0.0", + min_fossbilling_version: "0.1", + }, + ], + icon_url: + "https://raw.githubusercontent.com/bitcart/bitcart-fossbilling/master/Bitcart/Bitcart.png", + website: "https://bitcart.ai", + readme: `# Bitcart plugin for FOSSBilling + + For BoxBilling or FOSSBilling versions less than 0.5.0, you will need to use an [older version](https://github.com/bitcart/bitcart-fossbilling/tree/9aeb99cd3a59545113c2f5416d7ed63f00b149eb) of this payment gateway. + Please keep in mind BoxBilling is unmaintained and both BoxBilling and outdated version FOSSBilling may suffer from security vulnerabilities. Additionally, no support will be provided for either. + + ## Integration Requirements + + This version requires the following: + + * A working and up-to-date FOSSBilling instance + * Running Bitcart instance: [deployment guide](https://docs.bitcart.ai/deployment) + + ## Installing the Plugin + + ### Extension directory + + The easiest way to install this extension is by using the [FOSSBilling extension directory](https://extensions.fossbilling.org/extension/Bitcart). + + ### Manual installation + + 1. Download the latest release from the [extension directory](https://extensions.fossbilling.org/extension/Bitcart) + 2. Create a new folder named \`Bitcart\` in the \`/library/Payment/Adapter\` directory of your FOSSBilling installation + 3. Extract the archive you've downloaded in the first step into the new directory + 4. Go to the "Payment gateways" page in your admin panel (under the "System" menu in the navigation bar) and find Bitcart in the "New payment gateway" tab + 5. Click the cog icon next to Bitcart to install and configure Bitcart + + ## Plugin Configuration + + After you have enabled the Bitcart plugin, the configuration steps are: + + 1. Enter your admin panel URL (for example, https://admin.bitcart.ai) without slashes. If deployed via configurator, you should use https://bitcart.yourdomain.com/admin + 2. Enter your merchants API URL (for example, https://api.bitcart.ai) without slashes. If deployed via configurator, you should use https://bitcart.yourdomain.com/api + 3. Enter your store ID (click on id field in Bitcart's admin panel to copy id) + + Enjoy!`, + }, + { + id: "UddoktaPay", + type: "payment-gateway", + name: "UddoktaPay", + description: "UddoktaPay extension for FOSSBilling", + author: getAuthor("uddoktapay"), + license: { + name: "MIT", + URL: "https://github.com/UddoktaPay/FOSSBilling/blob/master/LICENSE", + }, + source: { + type: "github", + repo: "UddoktaPay/FOSSBilling", + }, + version: "1.0.1", + download_url: + "https://github.com/UddoktaPay/FOSSBilling/releases/download/1.0.1/UddoktaPay.zip", + releases: [ + { + tag: "1.0.1", + date: "2024-12-12T15:01:14Z", + download_url: + "https://github.com/UddoktaPay/FOSSBilling/releases/download/1.0.1/UddoktaPay.zip", + changelog_url: + "https://github.com/UddoktaPay/FOSSBilling/releases/tag/1.0.1", + min_fossbilling_version: "0.5", + }, + { + tag: "1.0.0", + date: "2023-07-29T11:44:14Z", + download_url: + "https://github.com/UddoktaPay/FOSSBilling/releases/download/1.0.0/UddoktaPay.zip", + changelog_url: + "https://github.com/UddoktaPay/FOSSBilling/releases/tag/1.0.0", + min_fossbilling_version: "0.5", + }, + ], + icon_url: + "https://raw.githubusercontent.com/UddoktaPay/FOSSBilling/master/UddoktaPay/UddoktaPay.png", + website: "https://uddoktapay.com", + readme: `# UddoktaPay plugin for FOSSBilling + + ## Installing the Plugin + + ### Extension directory + + The easiest way to install this extension is by using the [FOSSBilling extension directory](https://extensions.fossbilling.org/extension/UddoktaPay). + + ### Manual installation + + 1. Download the latest release from the [extension directory](https://extensions.fossbilling.org/extension/UddoktaPay) + 2. Create a new folder named \`UddoktaPay\` in the \`/library/Payment/Adapter\` directory of your FOSSBilling installation + 3. Extract the archive you've downloaded in the first step into the new directory + 4. Go to the "Payment gateways" page in your admin panel (under the "System" menu in the navigation bar) and find UddoktaPay in the "New payment gateway" tab + 5. Click the cog icon next to UddoktaPay to install and configure UddoktaPay + + Enjoy!`, + }, + { + id: "Razorpay", + type: "payment-gateway", + name: "Razorpay", + description: "Razorpay extension for FOSSBilling", + author: getAuthor("albinvar"), + license: { + name: "Apache 2.0", + URL: "https://github.com/albinvar/Razorpay-FOSSBilling/blob/1.x-prod/LICENSE", + }, + source: { + type: "github", + repo: "albinvar/Razorpay-FOSSBilling", + }, + version: "0.1.0", + download_url: + "https://github.com/albinvar/Razorpay-FOSSBilling/releases/download/v0.1.0/Razorpay.zip", + releases: [ + { + tag: "0.1.0", + date: "2023-09-24T21:03:30Z", + download_url: + "https://github.com/albinvar/Razorpay-FOSSBilling/releases/download/v0.1.0/Razorpay.zip", + changelog_url: + "https://github.com/albinvar/Razorpay-FOSSBilling/releases/tag/v0.1.0", + min_fossbilling_version: "0.5", + }, + ], + icon_url: + "https://raw.githubusercontent.com/albinvar/assets/main/fossbilling/razorpay-glyph-cropped.svg", + website: "https://razorpay.com", + readme: `![Molllie for FOSSBilling](https://raw.githubusercontent.com/albinvar/assets/main/fossbilling/rzp-foss-banner.png) + + # Razorpay Integration for FOSSBilling + + Provide your [FOSSBilling](https://fossbilling.org) customers with a variety of payment options, including Credit/Debit cards, Netbanking, UPI, Wallets, and more through [Razorpay](https://razorpay.com). + + > **Disclaimer**: This module is not officially affiliated with [FOSSBilling](https://fossbilling.org) or [Razorpay](https://razorpay.com). Please refer to their respective documentation for detailed information on FOSSBilling and Razorpay. + + ## Installation + +### 1). Extension directory + +The easiest way to install this extension is by using the [FOSSBilling extension directory](https://extensions.fossbilling.org/extension/Razorpay). + +### 2). Manual installation +1. Download the latest release from the [extension directory](https://extensions.fossbilling.org/extension/Razorpay) +2. Create a new folder named **Razorpay** in the **/library/Payment/Adapter** directory of your FOSSBilling installation +3. Extract the archive you've downloaded in the first step into the new directory +4. Go to the "**Payment gateways**" page in your admin panel (under the "System" menu in the navigation bar) and find Razorpay in the "**New payment gateway**" tab +5. Click the *cog icon* next to Razorpay to install and configure Razorpay + + +## Configuration +1. **Access Razorpay Settings:** In your FOSSBilling admin panel, find "**Razorpay**" under "**Payment gateways.**" +2. **Enter API Credentials:** Input your Razorpay \`API Key\` and \`API Secret\`. You can obtain these from your Razorpay panel. +3. **Configure Preferences:** Customize settings like currency and payment methods as needed. +4. **Save Changes:** Remember to update your configuration. +5. **Test Transactions (Optional):** Test your gateway integration through a payment process. +6. **Go Live:** Switch to live mode to start accepting real payments. + +## Usage +Once you've installed and configured the module, you can start using Razorpay as a payment gateway in your FOSSBilling setup. Customers will now see Razorpay as an option during the payment process based on the configuration you have set. + +## Contributing +We welcome contributions to enhance and improve this integration module. If you'd like to contribute, please follow these steps: + +### Fork the repository. +Create a new branch for your feature or bugfix: \`git checkout -b feature-name\`. +Make your changes and commit them with a clear and concise commit message. +Push your branch to your fork: \`git push origin feature-name\` and create a [pull request](https://github.com/albinvar/Razorpay-FOSSBilling/pulls). + +## License +This FOSSBilling Razorpay Payment Gateway Integration module is open-source software licensed under the [Apache License 2.0](https://github.com/albinvar/Razorpay-FOSSBilling/blob/1.x-prod/LICENSE). + +For support or questions, feel free to contact me at albinvar@pm.me + `, + }, + { + id: "BTCPay", + type: "payment-gateway", + name: "BTCPay", + description: "BTCPay extension for FOSSBilling", + author: getAuthor("christiangabs"), + license: { + name: "Apache License 2.0", + URL: "https://github.com/ChristianGabs/btcpay-fossbilling/blob/main/LICENSE", + }, + source: { + type: "github", + repo: "ChristianGabs/btcpay-fossbilling", + }, + version: "0.1.5", + download_url: + "https://github.com/ChristianGabs/btcpay-fossbilling/releases/download/0.1.5/BTCPay-FOSSBilling-0.1.5.zip", + releases: [ + { + tag: "0.1.5", + date: "2024-06-25T18:56:00Z", + download_url: + "https://github.com/ChristianGabs/btcpay-fossbilling/releases/download/0.1.5/BTCPay-FOSSBilling-0.1.5.zip", + changelog_url: + "https://github.com/ChristianGabs/btcpay-fossbilling/releases/download/0.1.5", + min_fossbilling_version: "0.6", + }, + { + tag: "0.1.4", + date: "2024-06-14T16:13:00Z", + download_url: + "https://github.com/ChristianGabs/btcpay-fossbilling/releases/download/0.1.4/BTCPay-FOSSBilling-0.1.4.zip", + changelog_url: + "https://github.com/ChristianGabs/btcpay-fossbilling/releases/download/0.1.4", + min_fossbilling_version: "0.6", + }, + { + tag: "0.1.3", + date: "2024-06-12T20:54:00Z", + download_url: + "https://github.com/ChristianGabs/btcpay-fossbilling/releases/download/0.1.3/BTCPay-FOSSBilling-0.1.3.zip", + changelog_url: + "https://github.com/ChristianGabs/btcpay-fossbilling/releases/download/0.1.3", + min_fossbilling_version: "0.6", + }, + ], + icon_url: + "https://raw.githubusercontent.com/ChristianGabs/btcpay-fossbilling/main/src/btcpaylogo.png", + website: "https://github.com/ChristianGabs/btcpay-fossbilling", + readme: ` # BTCPay plugin for FOSSBilling + For FOSSBilling versions > 0.6.0 + + ## Integration Requirements + + This version requires the following: + + * A working and up-to-date FOSSBilling instance + * Running BTCPay instance: [deployment guide](https://docs.btcpayserver.org/Deployment/) + + ## Installing the Plugin + ### 1). Extension directory + + The easiest way to install this extension is by using the [FOSSBilling extension directory](https://extensions.fossbilling.org/extension/BTCPay). + + ### 2). Manual installation + + 1. Download the latest release from the [releases](https://github.com/ChristianGabs/btcpay-fossbilling/releases/latest) + 2. Create a new folder named \`BTCPay\` in the \`/library/Payment/Adapter\` directory of your FOSSBilling installation + 3. Extract the archive you've downloaded in the first step into the new directory + 4. Go to the "Payment gateways" page in your admin panel (under the "System" menu in the navigation bar) and find BTCPay in the "New payment gateway" tab + 5. Click the cog icon next to BTCPay to install and configure BTCPay + + ## Plugin Configuration + + After you have enabled the BTCPay plugin, the configuration steps are: + + 1. Enter your Host URL (for example, https://pay.example.com) without slashes. + 2. Enter your API Key [Account > Manager Account > Api Keys] Permissions : [btcpay.store.canviewinvoices, btcpay.store.cancreateinvoice] + 3. Enter your Store id (Settings > General > Store Id) + 4. Enter your IPN Webhook Secret Key (Settings > Webhook > Create Webhook) [Events : A payment has been settled, An invoice has expired, An invoice has been settled, An invoice became invalid] + 5. Tax Included + 6. Speed Policy Options [High,Medium,Low, Low Medium : speed] + + ## Debugging Webhook Callbacks + + ***Enable Debugging:*** + + - Open ***config.php*** and set \`\`\`debug => true\`\`\` + + - *Remember to revert this setting to false once you have finished debugging.* + + ***View Callback Responses*** + + - You can find the responses from callbacks in the log files located at FossBilling **/data/log/event/event-date.log**. + + ***Example of debugging callbacks*** + + - \`\`\`[BTCPay] validation has failed. HTTP_BTCPAY_SIG : "" IPN Secret : "********" \`\`\` + - *In this scenario, your IPN Secret is incorrect.* + + ## License + This FOSSBilling BTCPay Payment Gateway Integration module is open-source software licensed under the [Apache License 2.0](LICENSE).`, + }, + { + id: "PAYEER", + type: "payment-gateway", + name: "PAYEER", + description: "PAYEER extension for FOSSBilling", + author: getAuthor("neto737"), + license: { + name: "MIT", + URL: "https://github.com/neto737/PAYEER-FOSSBilling/blob/main/LICENSE", + }, + source: { + type: "github", + repo: "neto737/PAYEER-FOSSBilling", + }, + version: "0.1.1", + download_url: + "https://github.com/neto737/PAYEER-FOSSBilling/releases/download/0.1.1/PAYEER-v0.1.1.zip", + releases: [ + { + tag: "0.1.1", + date: "2024-06-22T22:07:28Z", + download_url: + "https://github.com/neto737/PAYEER-FOSSBilling/releases/download/0.1.1/PAYEER-v0.1.1.zip", + changelog_url: + "https://github.com/neto737/PAYEER-FOSSBilling/releases/tag/0.1.1", + min_fossbilling_version: "0.6", + }, + { + tag: "0.1.0", + date: "2024-06-21T22:41:54Z", + download_url: + "https://github.com/neto737/PAYEER-FOSSBilling/releases/download/0.1.0/PAYEER.zip", + changelog_url: + "https://github.com/neto737/PAYEER-FOSSBilling/releases/tag/0.1.0", + min_fossbilling_version: "0.6", + }, + ], + icon_url: + "https://raw.githubusercontent.com/neto737/PAYEER-FOSSBilling/main/payeer-logo.png", + website: "https://payeer.com/", + readme: ` # PAYEER for FOSSBilling + Quickly and easily integrate [PAYEER](https://payeer.com) into your [FOSSBilling](https://fossbilling.org) instance using this extension. + + ## Installing the Plugin + ### 1). Extension directory + + The easiest way to install this extension is by using the [FOSSBilling extension directory](https://extensions.fossbilling.org/extension/PAYEER). + + ### 2). Manual installation + + 1. Download the latest release from the [releases](https://github.com/neto737/PAYEER-FOSSBilling/releases/latest) + 2. Create a new folder named \`PAYEER\` in the \`/library/Payment/Adapter\` directory of your FOSSBilling installation + 3. Extract the archive you've downloaded in the first step into the new directory + 4. Go to the "Payment gateways" page in your admin panel (under the "System" menu in the navigation bar) and find PAYEER in the "New payment gateway" tab + 5. Click the cog icon next to PAYEER to install and configure PAYEER + + ## Licensing + This extension is licensed under the MIT license. See the [LICENSE](LICENSE) file for more information. + + ## Disclaimer + This extension is not affiliated with PAYEER Limited. in any way. PAYEER is a registered trademark of PAYEER Limited. + `, + }, + { + id: "Xendit", + type: "payment-gateway", + name: "Xendit", + description: "Xendit extension for FOSSBilling", + author: getAuthor("fzfr"), + license: { + name: "Apache 2.0", + URL: "https://github.com/FZFR/Xendit-FOSSBilling/blob/main/LICENSE", + }, + source: { + type: "github", + repo: "FZFR/Xendit-FOSSBilling", + }, + version: "1.0.0", + download_url: + "https://github.com/FZFR/Xendit-FOSSBilling/releases/download/v1.0.0/Xendit.zip", + releases: [ + { + tag: "1.0.0", + date: "2024-08-01T04:57:00Z", + download_url: + "https://github.com/FZFR/Xendit-FOSSBilling/releases/download/v1.0.0/Xendit.zip", + changelog_url: + "https://github.com/FZFR/Xendit-FOSSBilling/releases/tag/v1.0.0", + min_fossbilling_version: "0.6", + }, + ], + icon_url: + "https://raw.githubusercontent.com/FZFR/Xendit-FOSSBilling/main/src/Xendit.png", + website: "https://github.com/FZFR/Xendit-FOSSBilling", + readme: `![Xendit for FOSSBilling](https://cdn.fazza.fr/REDACTED/img/xendit-foss-banner.jpg) + # Xendit for FOSSBilling + + ## Overview + Provide your [FOSSBilling](https://fossbilling.org) customers with a variety of payment options, including Credit/Debit cards, Bank Transfer, E-Wallets, and more through [Xendit](https://www.xendit.co). + + > **Note** + > Warning This extension, like FOSSBilling itself is under active development but is currently very much beta software. This means that there may be stability or security issues and it is not yet recommended for use in active production environments! + + ## Installation + + ### 1). Extension directory + The easiest way to install this extension is by using the [FOSSBilling extension directory](https://extensions.fossbilling.org/extension/Xendit). + ### 2). Manual installation + 1. Download the latest release from the [GitHub repository](https://github.com/FZFR/Xendit-FOSSBilling/releases) + 2. Create a new folder named **Xendit** in the **/library/Payment/Adapter** directory of your FOSSBilling installation + 3. Extract the archive you've downloaded in the first step into the new directory + 4. Go to the "**Payment gateways**" page in your admin panel (under the "System" menu in the navigation bar) and find Xendit in the "**New payment gateway**" tab + 5. Click the *cog icon* next to Xendit to install and configure Xendit + + ## Configuration + 1. Access Xendit Settings: In your FOSSBilling admin panel, find "**Xendit**" under "**Payment gateways.**" + 2. Enter API Credentials: Input your Xendit \`API Key\` and \`Webhook Verification Token\`. You can obtain these from your Xendit dashboard. + 3. Configure Preferences: Customize settings like sandbox mode and logging as needed. + 4. Save Changes: Remember to update your configuration. + 5. Test Transactions: It's recommended to test your gateway integration through a payment process in sandbox mode before going live. + 6. Go Live: Switch to live mode to start accepting real payments once testing is complete. + + + ### Webhook Configuration + + To set up webhooks: + + 1. Log in to your Xendit dashboard. + 2. Navigate to Settings > Webhooks. + 3. Add a new webhook with the following URL: + \`https://your-fossbilling-domain.com/ipn.php?gateway_id=payment_gateway_id\` + (Replace \`your-fossbilling-domain.com\` with your actual domain and \`payment_gateway_id\` with the ID assigned by FOSSBilling) + 4. Ensure the Webhook Verification Token in your Xendit settings matches the one in your FOSSBilling configuration. + + + + ## Usage + Once installed and configured, Xendit will appear as a payment option during the checkout process. The module handles various payment statuses including successful payments, pending transactions, and failed attempts. + + ## Troubleshooting + + - Check the logs at \`library/Payment/Adapter/Xendit/logs/xendit.log\` for detailed information on transactions and errors. + - Ensure your server's IP is whitelisted in Xendit's settings if you're experiencing connection issues. + - Verify that the API keys and Webhook Verification Tokens are correctly entered in the FOSSBilling configuration. + - If you encounter timezone-related issues, check your php.ini configuration or server settings. + + ## Features + + - [x] Using Xendit Payment Link (https://docs.xendit.co/payment-link) + - [x] Automatic invoice status update to 'paid' upon successful payment + - [x] Activate service automatically after payment confirmation + - [x] Comprehensive handling of various payment statuses (PAID, EXPIRED, PENDING, FAILED) + - [x] Detailed transaction logging for easy tracking and debugging + + + ## Contributing + We welcome contributions to enhance and improve this integration module. If you'd like to contribute, please follow these steps: + + 1. Fork the repository. + 2. Create a new branch for your feature or bugfix: \`git checkout -b feature-name\`. + 3. Make your changes and commit them with a clear and concise commit message. + 4. Push your branch to your fork: \`git push origin feature-name\` and create a [pull request](https://github.com/FZFR/Xendit-FOSSBilling/pulls). + + ## License + This FOSSBilling Xendit Payment Gateway Integration module is open-source software licensed under the [Apache License 2.0](LICENSE). + + > *Note*: This module is not officially affiliated with [FOSSBilling](https://fossbilling.org) or [Xendit](https://www.xendit.co). Please refer to their respective documentation for detailed information on FOSSBilling and Xendit. + + + ## Support + + For issues related to this adapter, please open an issue. + + For Xendit-specific issues, please contact Xendit support.`, + }, + { + id: "Netim", + type: "domain-registrar", + name: "Netim", + description: "Netim registrar extension for FOSSBilling", + author: getAuthor("netim"), + license: { + name: "GPL-3.0 license", + URL: "https://github.com/netim-com/fossbilling-registrar-module/blob/main/LICENSE", + }, + source: { + type: "github", + repo: "netim-com/fossbilling-registrar-module", + }, + version: "1.0.0", + download_url: + "https://github.com/netim-com/fossbilling-registrar-module", + releases: [ + { + tag: "1.0.0", + date: "2023-07-12T04:57:00Z", + download_url: + "https://github.com/netim-com/fossbilling-registrar-module", + changelog_url: + "https://github.com/netim-com/fossbilling-registrar-module/commits/main/", + min_fossbilling_version: "0.6", + }, + ], + icon_url: + "https://avatars.githubusercontent.com/u/117817448?v=4", + website: "https://netim.com", + readme: `# Netim for FOSSBilling + + ## About the registrar module + This module is the implementation of the domain name registrar extension for Netim in FOSSBilling application. + To learn more about FOSSBilling, see https://fossbilling.org/ + + This module is licensed under the GNU General Public License v3.0. See the LICENSE file for more information. + + Online documentation: https://support.netim.com/en/docs/fossbilling + + Technical Support: modules-support@netim.com + + ## Installation + + ### 1). Extension directory + Unfortunately, as FossBilling is still under development, it is not possible to install a registrar from the directory extension. Only manual installation is available. + ### 2). Manual installation + 1. Download the latest release from [our official website](https://support.netim.com/en/docs/fossbilling/download-and-installation) + 2. Extract the archive in your FOSSBilling installation directory + 3. Ensure that the module is present in the following directory: /library/Registrar/Adapter/Netim/ + + ## Features + + __Domain Management__ + - [X] Domain Registration + - [X] Domain Transfer + - [X] Domain Renewal + - [X] Domain Registrar Lock + - [X] Domain Whois privacy + - [X] Support for AuthInfo Code (Domain Authorization Code) + - [X] Faster availability check by Netim + + __Contact Management__ + - [X] Update of contact information + - [X] Full support of Latin character set (LATIN1 / LATIN-EXTA / LATIN-EXTB character sets) + + __DNS Nameserver Management__ + - [X] Nameserver changes + + ## Testing + Our reseller service provides an OT&E platform so that all tests can be done before going live in production + + ## About NETIM + NETIM is an ICANN accredited registrar created in 2004. + + It is a worldwide registrar with more than 1000 tlds available and a large network of accreditation. + In a highly competitive and fast-growing market, the NETIM values are transparency, honesty in ethics with a clear pricing policy and a quality based-service rewarded on TrustPilot. + + Website: https://www.netim.com + + Technical Support:support@netim.com + + Sales Support:sales@netim.com`, +}, +{ + id: "FaucetPay", + type: "payment-gateway", + name: "FaucetPay", + description: "FaucetPay extension for FOSSBilling", + author: getAuthor("neto737"), + license: { + name: "MIT", + URL: "https://github.com/neto737/FaucetPay-FOSSBilling/blob/main/LICENSE", + }, + source: { + type: "github", + repo: "neto737/FaucetPay-FOSSBilling", + }, + version: "0.1.0", + download_url: + "https://github.com/neto737/FaucetPay-FOSSBilling/releases/download/0.1.0/FaucetPay-v0.1.0.zip", + releases: [ + { + tag: "0.1.0", + date: "2024-11-23T01:37:45Z", + download_url: + "https://github.com/neto737/FaucetPay-FOSSBilling/releases/download/0.1.0/FaucetPay-v0.1.0.zip", + changelog_url: + "https://github.com/neto737/FaucetPay-FOSSBilling/releases/tag/0.1.0", + min_fossbilling_version: "0.6", + }, + ], + icon_url: + "https://raw.githubusercontent.com/neto737/FaucetPay-FOSSBilling/main/faucetpay-logo.png", + website: "https://faucetpay.io/", + readme: ` # FaucetPay for FOSSBilling + Quickly and easily integrate [FaucetPay](https://faucetpay.io) into your [FOSSBilling](https://fossbilling.org) instance using this extension. + + ## Installing the Plugin + ### 1). Extension directory + + The easiest way to install this extension is by using the [FOSSBilling extension directory](https://extensions.fossbilling.org/extension/FaucetPay). + + ### 2). Manual installation + + 1. Download the latest release from the [releases](https://github.com/neto737/FaucetPay-FOSSBilling/releases/latest) + 2. Create a new folder named \`FaucetPay\` in the \`/library/Payment/Adapter\` directory of your FOSSBilling installation + 3. Extract the archive you've downloaded in the first step into the new directory + 4. Go to the "Payment gateways" page in your admin panel (under the "System" menu in the navigation bar) and find FaucetPay in the "New payment gateway" tab + 5. Click the cog icon next to FaucetPay to install and configure FaucetPay + + ## Licensing + This extension is licensed under the MIT license. See the [LICENSE](LICENSE) file for more information. + + ## Disclaimer + This extension is not affiliated with Basilisk Entertainment S.R.L in any way. FaucetPay is a registered trademark of Basilisk Entertainment S.R.L. + `, + }, + { + id: "OpenProvider", + type: "domain-registrar", + name: "OpenProvider", + description: "OpenProvider registrar extension for FOSSBilling", + author: getAuthor("devife"), + license: { + name: "Apache 2.0", + URL: "https://github.com/Devife/fossbilling-registrar-openprovider/blob/main/LICENSE", + }, + source: { + type: "github", + repo: "Devife/fossbilling-registrar-openprovider", + }, + version: "0.0.1", + download_url: + "https://github.com/Devife/fossbilling-registrar-openprovider/archive/refs/tags/0.0.1.zip", + releases: [ + { + tag: "0.0.1", + date: "2024-12-05T10:00:00Z", + download_url: + "https://github.com/Devife/fossbilling-registrar-openprovider/archive/refs/tags/0.0.1.zip", + changelog_url: + "https://github.com/Devife/fossbilling-registrar-openprovider/commits/main/", + min_fossbilling_version: "0.6", + }, + ], + icon_url: + "https://avatars.githubusercontent.com/u/8396328?v=4", + website: "https://devife.com", + readme: `# OpenProvider Integration for FOSSBilling + +This module integrates the OpenProvider domain registrar with FOSSBilling, enabling users to manage domain registration, transfer, and renewal directly from their FOSSBilling platform. + +--- + +## Features + +- **Domain Registration**: Register new domains using OpenProvider's API. +- **Domain Transfer**: Transfer existing domains to OpenProvider from FOSSBilling. +- **Domain Management**: Update DNS, WHOIS, and other settings directly. +- **Renewals**: Automate domain renewals through OpenProvider. + +--- + +## Requirements + +- **FOSSBilling**: Make sure you have FOSSBilling installed and properly configured. +- **OpenProvider Account**: An active account with OpenProvider is required to use their API. + +--- + +## Installation + +1. Clone this repository and copy the files to the root of your FOSSBilling installation: + \`\`\`bash + git clone https://github.com/Devife/fossbilling-registrar-openprovider.git + \`\`\` +1. Navigate to the FOSSBilling admin panel. + +1. Go to System > Domain registration > New domain registrar and enable the OpenProvider module. + +1. Refresh the page, go to the Registrars tab and edit the OpenProvider settings + +1. Enter your OpenProvider API credentials: + - API URL: Live https://api.openprovider.eu (Sandbox http://api.sandbox.openprovider.nl:8480) + - Username + - Password +1. Save your configuration. + +## Usage + +1. Add OpenProvider as your registrar for specific TLDs in FOSSBilling. + +1. Clients can register, transfer, or renew domains through your billing system, and the integration will communicate with OpenProvider's API to process requests. + +1. Monitor and manage domain actions directly from your FOSSBilling admin panel. + +## Troubleshooting + +- Connection Issues: Ensure your server can connect to the OpenProvider API endpoint. +- API Errors: Double-check your credentials and ensure your OpenProvider account has sufficient privileges. +- PHP Errors: Verify if your PHP version is supported by FOSSBilling. + +## Contributing + +Contributions are welcome! Please follow these steps: + +1. Fork the repository. +1. Create a new branch (feature/your-feature). +1. Commit your changes. +1. Open a pull request with a detailed description. + +## License + +This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details. + +## Acknowledgments + +1. OpenProvider for their robust API. +1. FOSSBilling for their open-source billing platform.`, +}, +{ + id: "Yoco", + type: "payment-gateway", + name: "Yoco", + description: "Yoco payment gateway integration for FOSSBilling", + author: getAuthor("demassimo"), + license: { + name: "Apache 2.0", + URL: "https://github.com/demassimo/Yoco_Payment_Gateway_Fossbilling/blob/main/LICENSE", + }, + source: { + type: "github", + repo: "demassimo/Yoco_Payment_Gateway_Fossbilling", + }, + version: "0.5.1", + download_url: + "https://github.com/demassimo/Yoco_Payment_Gateway_Fossbilling/releases/download/v0.5.1/Yoco.zip", + releases: [ + { + tag: "0.5.1", + date: "2026-05-10T09:22:26+02:00", + download_url: + "https://github.com/demassimo/Yoco_Payment_Gateway_Fossbilling/releases/download/v0.5.1/Yoco.zip", + changelog_url: + "https://github.com/demassimo/Yoco_Payment_Gateway_Fossbilling/releases/tag/v0.5.1", + min_fossbilling_version: "0.7", + }, + ], + icon_url: + "https://raw.githubusercontent.com/demassimo/Yoco_Payment_Gateway_Fossbilling/main/assets/yoco-icon.svg", + website: "https://www.yoco.com/za/", + readme: `# FOSSBilling Yoco Gateway + +Yoco payment gateway integration for FOSSBilling. + +This extension lets FOSSBilling accept one-time Yoco Checkout payments in South African Rand (ZAR). It also supports optional USD invoice conversion into ZAR before checkout. + +## Features + +- One-time Yoco Checkout payments +- ZAR invoice support +- Optional USD-to-ZAR invoice conversion +- Yoco webhook verification for payment confirmation +- Test and live API key fields + +## Installation + +1. Download the latest release ZIP. +2. Extract the archive into your FOSSBilling root so the paths merge into: + - \`library/Payment/Adapter/Yoco.php\` + - \`data/assets/gateways/yoco.png\` +3. Clear the FOSSBilling cache. +4. In the FOSSBilling admin panel, go to **System > Payment gateways**. +5. Install or enable the **Yoco** payment gateway. +6. Add your Yoco live or test keys. +7. Configure your Yoco webhook and add the webhook secret in the gateway settings. + +## Currency notes + +Yoco checkout is processed in ZAR. ZAR invoices are charged directly. USD invoices can be converted to ZAR using the configured gateway conversion rate or the FOSSBilling currency rates. + +## Webhook notes + +The gateway verifies Yoco webhook signatures when a webhook secret is configured. Payment confirmation is handled through the \`payment.succeeded\` event. + +## License + +Apache License 2.0. See [LICENSE](LICENSE). + +## Disclaimer + +This extension is not affiliated with FOSSBilling or Yoco.`, +}, +{ + id: "CoinPayPortal", + type: "payment-gateway", + name: "CoinPayPortal Crypto Payments", + description: "Accept crypto payments through CoinPayPortal. Customers are redirected to a secure checkout and invoices are automatically marked paid after verified payment confirmation.", + author: getAuthor("coinpayportal"), + license: { + name: "MIT", + URL: "https://github.com/profullstack/coinpayportal/blob/master/plugins/fossbilling/LICENSE", + }, + source: { + type: "github", + repo: "profullstack/coinpayportal", + }, + version: "1.0.0", + download_url: + "https://github.com/profullstack/coinpayportal/releases/download/1.0.0/CoinPayPortal.zip", + icon_url: "https://coinpayportal.com/icons/icon-512x512.png", + releases: [ + { + tag: "1.0.0", + date: "2026-04-30T09:27:49Z", + download_url: + "https://github.com/profullstack/coinpayportal/releases/download/1.0.0/CoinPayPortal.zip", + changelog_url: + "https://github.com/profullstack/coinpayportal/releases/tag/1.0.0", + min_fossbilling_version: "0.6", + }, + ], + website: "https://coinpayportal.com", + readme: `# CoinPayPortal Crypto Payments for FOSSBilling + +Accept cryptocurrency payments through [CoinPayPortal](https://coinpayportal.com). Customers are redirected to a secure CoinPayPortal checkout page and invoices are automatically marked paid after verified payment confirmation. + +## Features + +- Accept crypto payments (Bitcoin, Ethereum, USDC, and more) +- Secure webhook-based payment confirmation with signature verification +- Sandbox/test mode for development +- Configurable underpayment tolerance +- Debug logging for troubleshooting + +## Requirements + +- FOSSBilling >= 0.6.0 +- PHP >= 8.0 +- A [CoinPayPortal](https://coinpayportal.com) merchant account + +## Installation + +### Extension directory (recommended) + +The easiest way to install this extension is by using the [FOSSBilling extension directory](https://extensions.fossbilling.org/extension/CoinPayPortal). + +### Manual installation + +1. Download the latest release from the [extension directory](https://extensions.fossbilling.org/extension/CoinPayPortal) +2. Extract the archive into the \`/library/Payment/Adapter/\` directory of your FOSSBilling installation +3. Go to the "Payment gateways" page in your admin panel and find CoinPayPortal in the "New payment gateway" tab +4. Click the cog icon next to CoinPayPortal to install and configure it + +## Configuration + +After enabling the gateway, configure the following settings in your FOSSBilling admin panel: + +- **API Key**: Your CoinPayPortal API key (found in Settings → API in your merchant dashboard) +- **Merchant ID**: Your CoinPayPortal merchant or account ID +- **Webhook Secret**: Secret used to verify incoming webhook signatures + +For full configuration and webhook setup instructions, see the [documentation](https://github.com/profullstack/coinpayportal/tree/master/plugins/fossbilling/docs). + +## License + +This extension is licensed under the MIT License. See the [LICENSE](https://github.com/profullstack/coinpayportal/blob/master/plugins/fossbilling/LICENSE) file for details.`, +}, +{ + id: "TPPWholesale", + type: "domain-registrar", + name: "TPP Wholesale", + description: "TPP Wholesale domain registrar adapter for FOSSBilling. Supports .co.nz, .nz, .com.au, .au and .com domains. Designed for New Zealand and Australian hosting businesses.", + author: getAuthor("servmeit"), + license: { + name: "Apache 2.0", + URL: "https://www.apache.org/licenses/LICENSE-2.0", + }, + source: { + type: "github", + repo: "grant436/fossbilling-tpp-wholesale", + }, + version: "1.0.0", + download_url: + "https://github.com/grant436/fossbilling-tpp-wholesale/archive/refs/tags/v1.0.0.zip", + releases: [ + { + tag: "1.0.0", + date: "2026-06-07T00:00:00Z", + download_url: + "https://github.com/grant436/fossbilling-tpp-wholesale/archive/refs/tags/v1.0.0.zip", + changelog_url: + "https://github.com/grant436/fossbilling-tpp-wholesale/releases/tag/v1.0.0", + min_fossbilling_version: "0.8.2", + }, + ], + icon_url: + "https://www.tppwholesale.com.au/wp-content/uploads/2022/12/TPP-logo-basic.png", + website: "https://github.com/grant436/fossbilling-tpp-wholesale", + readme: `# TPP Wholesale Registrar for FOSSBilling + +A domain registrar adapter for [FOSSBilling](https://fossbilling.org) that integrates with [TPP Wholesale](https://www.tppwholesale.com.au), the leading ANZ domain registrar. + +## Features + +- Domain availability checking +- Domain registration (.co.nz, .nz, .com.au, .au, .com and more) +- Domain renewal +- Domain transfers +- Nameserver management +- Contact management +- Domain locking/unlocking +- EPP/auth code retrieval +- Test mode for safe testing without registering real domains +- Auto-derives TPP console account reference from API credentials + +## Requirements + +- FOSSBilling 0.8.2 or later (PHP 8.3+) +- TPP Wholesale reseller account with API access enabled +- TPP Legacy API credentials (Account No, Login, Password) + +## Installation + +1. Download \`TPPWholesale.php\` from the release archive +2. Copy it to \`/library/Registrar/Adapter/TPPWholesale.php\` in your FOSSBilling installation +3. In FOSSBilling admin go to **Domain Management → Registrars** +4. Click **New Domain Registrar** and select **TPPWholesale** +5. Click the cog icon and enter your TPP API credentials + +## Configuration + +| Field | Description | Required | +|---|---|---| +| Account Number | Your TPP account number | Yes | +| User ID | Your TPP API login (e.g. SER-993-API) | Yes | +| Password | Your TPP API password | Yes | +| Console Account Reference | Your TPP account reference (e.g. SER-993). Leave blank to auto-derive from User ID | No | + +## License + +Apache 2.0`, +}, +{ + id: "ISPmanager", + type: "server-manager", + name: "ISPmanager 6", + description: "ISPmanager 6 server manager adapter for FOSSBilling. Automatically provisions hosting accounts, websites and SSL certificates when customers purchase hosting plans.", + author: getAuthor("servmeit"), + license: { + name: "Apache 2.0", + URL: "https://www.apache.org/licenses/LICENSE-2.0", + }, + source: { + type: "github", + repo: "grant436/fossbilling-ispmanager", + }, + version: "1.0.0", + download_url: + "https://github.com/grant436/fossbilling-ispmanager/archive/refs/tags/v1.0.0.zip", + releases: [ + { + tag: "1.0.0", + date: "2026-06-10T00:00:00Z", + download_url: + "https://github.com/grant436/fossbilling-ispmanager/archive/refs/tags/v1.0.0.zip", + changelog_url: + "https://github.com/grant436/fossbilling-ispmanager/releases/tag/v1.0.0", + min_fossbilling_version: "0.8.2", + }, + ], + icon_url: + "https://raw.githubusercontent.com/grant436/fossbilling-ispmanager/main/logo-ispmgr.svg", + website: "https://github.com/grant436/fossbilling-ispmanager", + readme: `# ISPmanager 6 Server Manager for FOSSBilling + +Automatically provisions hosting accounts in [ISPmanager 6](https://www.ispmanager.com) when customers purchase hosting plans through [FOSSBilling](https://fossbilling.org). + +## Features + +- Automatic hosting account creation on purchase +- Account suspension and unsuspension +- Account cancellation and deletion +- Password changes +- Package upgrades and downgrades +- Primary domain creation with PHP and SSL +- Comprehensive API call logging + +## Requirements + +- FOSSBilling 0.8.2 or later (PHP 8.3+) +- ISPmanager 6 (Lite, Pro or Host) +- A dedicated API administrator user in ISPmanager (no 2FA) + +## Installation + +1. Download \`ispmanager.php\` from the release archive +2. Copy it to \`/library/Server/Manager/ISPmanager.php\` in your FOSSBilling installation +3. Create a dedicated API user in ISPmanager with administrator privileges and no 2FA +4. Allow FOSSBilling to reach ISPmanager on port 1500 +5. In FOSSBilling go to **Products & Services → Hosting Plans → Servers** and add your server +6. Set Server Manager to **ISPmanager**, enter credentials and click **Test Connection** + +## License + +Apache 2.0`, +}, +]; diff --git a/src/services/extensions/v1/scripts/seed-db.ts b/src/services/extensions/v1/scripts/seed-db.ts new file mode 100644 index 0000000..fa3a4dc --- /dev/null +++ b/src/services/extensions/v1/scripts/seed-db.ts @@ -0,0 +1,97 @@ +#!/usr/bin/env node + +import { spawnSync } from "child_process"; +import { writeFileSync } from "fs"; +import { join, dirname } from "path"; +import { fileURLToPath } from "url"; +import { extensionData } from "./extensions-data"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); +const DB_NAME = "api_extensions"; + +function sqlStr(value: string | null | undefined): string { + if (value === null || value === undefined) return "NULL"; + return `'${value.replace(/'/g, "''")}'`; +} + +function sqlJson(value: unknown): string { + return sqlStr(JSON.stringify(value)); +} + +function generateSeedSQL(): string { + const lines: string[] = [ + "-- Auto-generated seed file for extensions table", + "-- Generated by: npx tsx src/services/extensions/v1/scripts/seed-db.ts", + "" + ]; + + for (const ext of extensionData) { + const iconUrl = ext.icon_url !== undefined ? sqlStr(ext.icon_url) : "NULL"; + lines.push( + `INSERT OR REPLACE INTO extensions ` + + `(id, type, name, description, author, releases, website, license, icon_url, readme, source, version, download_url) VALUES (` + + `${sqlStr(ext.id)}, ${sqlStr(ext.type)}, ${sqlStr(ext.name)}, ${sqlStr(ext.description)}, ` + + `${sqlJson(ext.author)}, ${sqlJson(ext.releases)}, ${sqlStr(ext.website)}, ` + + `${sqlJson(ext.license)}, ${iconUrl}, ${sqlStr(ext.readme)}, ` + + `${sqlJson(ext.source)}, ${sqlStr(ext.version)}, ${sqlStr(ext.download_url)});` + ); + } + + return lines.join("\n"); +} + +function runWrangler(args: string[]): void { + const command = ["wrangler", "d1", "execute", DB_NAME, ...args]; + const result = spawnSync("npx", command, { + encoding: "utf8", + stdio: "pipe" + }); + + if (result.error) throw result.error; + + if (result.status !== 0) { + throw new Error( + [ + `Wrangler command failed: npx ${command.join(" ")}`, + result.stderr.trim() ? `stderr: ${result.stderr.trim()}` : "", + result.stdout.trim() ? `stdout: ${result.stdout.trim()}` : "" + ] + .filter(Boolean) + .join("\n") + ); + } +} + +function seedDatabase(local: boolean): void { + const schemaPath = join(__dirname, "..", "db", "schema.sql"); + const seedPath = join(__dirname, "..", "db", "seed.sql"); + const localFlag = local ? ["--local"] : []; + + console.log("Generating seed SQL..."); + const seedSQL = generateSeedSQL(); + writeFileSync(seedPath, seedSQL, "utf8"); + console.log(`Seed SQL written to ${seedPath} (${extensionData.length} extensions)`); + + console.log("Applying schema..."); + runWrangler([...localFlag, "--file", schemaPath]); + + console.log("Seeding extensions..."); + runWrangler([...localFlag, "--file", seedPath]); + + console.log("Done."); +} + +if (import.meta.url === `file://${process.argv[1]}`) { + const local = !process.argv.includes("--remote"); + try { + seedDatabase(local); + } catch (error) { + console.error( + `Seed failed: ${error instanceof Error ? error.message : String(error)}` + ); + process.exit(1); + } +} + +export { seedDatabase }; diff --git a/test/services/extensions/v1/index.test.ts b/test/services/extensions/v1/index.test.ts new file mode 100644 index 0000000..9cf5f42 --- /dev/null +++ b/test/services/extensions/v1/index.test.ts @@ -0,0 +1,348 @@ +import { describe, it, expect, beforeEach } from "vitest"; +import { + createExecutionContext, + waitOnExecutionContext +} from "cloudflare:test"; +import { env } from "cloudflare:workers"; +import app from "../../../../src/app"; + +type ExtensionRow = { + id: string; + type: string; + name: string; + description: string; + author: string; + releases: string; + website: string; + license: string; + icon_url?: string; + readme: string; + source: string; + version: string; + download_url: string; +}; + +const testExtensions: ExtensionRow[] = [ + { + id: "Example", + type: "mod", + name: "Example Module", + description: "An example module for developers.", + author: JSON.stringify({ + type: "organization", + name: "fossbilling", + id: "fossbilling", + URL: "https://fossbilling.org" + }), + releases: JSON.stringify([ + { + tag: "0.0.5", + date: "2024-02-12T06:36:38+00:00", + download_url: + "https://github.com/FOSSBilling/example-module/releases/download/0.0.5/Example.zip", + changelog_url: + "https://github.com/FOSSBilling/example-module/releases/tag/0.0.5", + min_fossbilling_version: "0.6" + }, + { + tag: "0.0.4", + date: "2023-09-25T07:36:29Z", + download_url: + "https://github.com/FOSSBilling/example-module/releases/download/0.0.4/Example.zip", + changelog_url: + "https://github.com/FOSSBilling/example-module/releases/tag/0.0.4", + min_fossbilling_version: "0.5" + } + ]), + website: "https://fossbilling.org", + license: JSON.stringify({ name: "Apache 2.0", URL: "https://www.apache.org/licenses/LICENSE-2.0" }), + icon_url: "https://raw.githubusercontent.com/FOSSBilling/example-module/main/src/icon.svg", + readme: "# Example module\n\nThis is an example module.", + source: JSON.stringify({ type: "github", repo: "FOSSBilling/example-module" }), + version: "0.0.5", + download_url: + "https://github.com/FOSSBilling/example-module/releases/download/0.0.5/Example.zip" + }, + { + id: "TestTheme", + type: "theme", + name: "Test Theme", + description: "A test theme.", + author: JSON.stringify({ + type: "organization", + name: "fossbilling", + id: "fossbilling", + URL: "https://fossbilling.org" + }), + releases: JSON.stringify([ + { + tag: "1.0.0", + date: "2024-01-01T00:00:00Z", + download_url: "https://example.com/TestTheme.zip", + min_fossbilling_version: "0.6" + } + ]), + website: "https://fossbilling.org", + license: JSON.stringify({ name: "MIT" }), + readme: "# Test Theme", + source: JSON.stringify({ type: "github", repo: "FOSSBilling/test-theme" }), + version: "1.0.0", + download_url: "https://example.com/TestTheme.zip" + } +]; + +function makeD1Mock(): D1Database { + return { + prepare(query: string): D1PreparedStatement { + let boundParams: unknown[] = []; + + const stmt: D1PreparedStatement = { + bind(...params: unknown[]) { + boundParams = params; + return stmt; + }, + + async all(): Promise> { + let rows = [...testExtensions]; + + if (query.includes("WHERE type = ?") && boundParams[0]) { + rows = rows.filter((r) => r.type === boundParams[0]); + } + + return { + success: true, + results: rows as unknown as T[], + meta: { + duration: 0, + last_row_id: 0, + changes: 0, + served_by: "mock", + size_after: 0, + rows_read: rows.length, + rows_written: 0, + changed_db: false + } + }; + }, + + async first(): Promise { + if (query.includes("LOWER(id) = LOWER(?)") && boundParams[0]) { + const id = String(boundParams[0]).toLowerCase(); + const found = testExtensions.find( + (r) => r.id.toLowerCase() === id + ); + return (found as unknown as T) ?? null; + } + return null; + }, + + raw: (() => { throw new Error("not implemented"); }) as D1PreparedStatement["raw"], + + async run>(): Promise> { + return { + success: true, + results: [], + meta: { + duration: 0, + last_row_id: 0, + changes: 0, + served_by: "mock", + size_after: 0, + rows_read: 0, + rows_written: 0, + changed_db: false + } + }; + } + }; + + return stmt; + }, + + dump(): Promise { + throw new Error("not implemented"); + }, + batch(_statements: D1PreparedStatement[]): Promise[]> { + throw new Error("not implemented"); + }, + exec(_query: string): Promise { + throw new Error("not implemented"); + }, + withSession(_constraintOrBookmark?: string): D1DatabaseSession { + throw new Error("not implemented"); + } + }; +} + +describe("Extensions API v1", () => { + beforeEach(() => { + env.DB_EXTENSIONS = makeD1Mock(); + }); + + describe("GET /list", () => { + it("should return all extensions", async () => { + const ctx = createExecutionContext(); + const res = await app.request("/extensions/v1/list", {}, env, ctx); + await waitOnExecutionContext(ctx); + + expect(res.status).toBe(200); + const data = await res.json() as { result: unknown[] }; + expect(Array.isArray(data.result)).toBe(true); + expect(data.result.length).toBe(2); + }); + + it("should filter by type", async () => { + const ctx = createExecutionContext(); + const res = await app.request("/extensions/v1/list?type=mod", {}, env, ctx); + await waitOnExecutionContext(ctx); + + expect(res.status).toBe(200); + const data = await res.json() as { result: Array<{ type: string }> }; + expect(data.result.every((e) => e.type === "mod")).toBe(true); + expect(data.result.length).toBe(1); + }); + + it("should redirect trailing slash", async () => { + const ctx = createExecutionContext(); + const res = await app.request("/extensions/v1/list/", {}, env, ctx); + await waitOnExecutionContext(ctx); + + expect(res.status).toBe(301); + }); + + it("should parse releases in descending order", async () => { + const ctx = createExecutionContext(); + const res = await app.request("/extensions/v1/list", {}, env, ctx); + await waitOnExecutionContext(ctx); + + const data = await res.json() as { result: Array<{ id: string; releases: Array<{ tag: string }> }> }; + const example = data.result.find((e) => e.id === "Example"); + expect(example).toBeTruthy(); + expect(example!.releases[0].tag).toBe("0.0.5"); + }); + }); + + describe("GET /:id", () => { + it("should return a single extension", async () => { + const ctx = createExecutionContext(); + const res = await app.request("/extensions/v1/Example", {}, env, ctx); + await waitOnExecutionContext(ctx); + + expect(res.status).toBe(200); + const data = await res.json() as { result: { id: string; name: string } }; + expect(data.result.id).toBe("Example"); + expect(data.result.name).toBe("Example Module"); + }); + + it("should do case-insensitive lookup", async () => { + const ctx = createExecutionContext(); + const res = await app.request("/extensions/v1/example", {}, env, ctx); + await waitOnExecutionContext(ctx); + + expect(res.status).toBe(200); + const data = await res.json() as { result: { id: string } }; + expect(data.result.id).toBe("Example"); + }); + + it("should return 500 for unknown extension", async () => { + const ctx = createExecutionContext(); + const res = await app.request("/extensions/v1/nonexistent", {}, env, ctx); + await waitOnExecutionContext(ctx); + + expect(res.status).toBe(500); + const data = await res.json() as { error: { message: string } }; + expect(data.error.message).toContain("nonexistent"); + }); + + it("should include parsed author object", async () => { + const ctx = createExecutionContext(); + const res = await app.request("/extensions/v1/Example", {}, env, ctx); + await waitOnExecutionContext(ctx); + + const data = await res.json() as { result: { author: { name: string } } }; + expect(data.result.author.name).toBe("fossbilling"); + }); + }); + + describe("GET /:id/version", () => { + it("should return plain text version", async () => { + const ctx = createExecutionContext(); + const res = await app.request("/extensions/v1/Example/version", {}, env, ctx); + await waitOnExecutionContext(ctx); + + expect(res.status).toBe(200); + expect(res.headers.get("Content-Type")).toContain("text/plain"); + const text = await res.text(); + expect(text).toBe("0.0.5"); + }); + + it("should return 500 for unknown extension", async () => { + const ctx = createExecutionContext(); + const res = await app.request("/extensions/v1/nonexistent/version", {}, env, ctx); + await waitOnExecutionContext(ctx); + + expect(res.status).toBe(500); + }); + }); + + describe("GET /:id/badges/:type", () => { + it("should return SVG for version badge", async () => { + const ctx = createExecutionContext(); + const res = await app.request("/extensions/v1/Example/badges/version", {}, env, ctx); + await waitOnExecutionContext(ctx); + + expect(res.status).toBe(200); + expect(res.headers.get("Content-Type")).toContain("image/svg+xml"); + const svg = await res.text(); + expect(svg).toContain(" { + const ctx = createExecutionContext(); + const res = await app.request("/extensions/v1/Example/badges/license", {}, env, ctx); + await waitOnExecutionContext(ctx); + + expect(res.status).toBe(200); + const svg = await res.text(); + expect(svg).toContain("Apache 2.0"); + }); + + it("should return red SVG for unknown badge type", async () => { + const ctx = createExecutionContext(); + const res = await app.request("/extensions/v1/Example/badges/unknown_type", {}, env, ctx); + await waitOnExecutionContext(ctx); + + expect(res.status).toBe(200); + expect(res.headers.get("Content-Type")).toContain("image/svg+xml"); + }); + + it("should accept custom color param", async () => { + const ctx = createExecutionContext(); + const res = await app.request( + "/extensions/v1/Example/badges/version?color=green", + {}, + env, + ctx + ); + await waitOnExecutionContext(ctx); + + expect(res.status).toBe(200); + const svg = await res.text(); + expect(svg).toContain(" { + const ctx = createExecutionContext(); + const res = await app.request( + "/extensions/v1/nonexistent/badges/version", + {}, + env, + ctx + ); + await waitOnExecutionContext(ctx); + + expect(res.status).toBe(500); + }); + }); +}); diff --git a/worker-configuration.d.ts b/worker-configuration.d.ts index 1d69efd..4b8f0c4 100644 --- a/worker-configuration.d.ts +++ b/worker-configuration.d.ts @@ -1,24 +1,19 @@ /* eslint-disable */ -// Generated by Wrangler by running `wrangler types --env-interface CloudflareBindings` (hash: 5932a849fb69e2a34cb7464988b02b49) -// Runtime types generated with workerd@1.20260430.1 2026-04-21 nodejs_compat +// Generated by Wrangler by running `wrangler types --env-interface=CloudflareBindings` (hash: 0005c28ca80b7c3953acb0d6707876e2) +// Runtime types generated with workerd@1.20260617.1 2026-04-21 nodejs_compat +interface __BaseEnv_CloudflareBindings { + AUTH_KV: KVNamespace; + CACHE_KV: KVNamespace; + DB_CENTRAL_ALERTS: D1Database; + DB_EXTENSIONS: D1Database; +} declare namespace Cloudflare { interface GlobalProps { mainModule: typeof import("./src/app/index"); } - interface Env { - AUTH_KV: KVNamespace; - CACHE_KV: KVNamespace; - DB_CENTRAL_ALERTS: D1Database; - GITHUB_TOKEN: string; - } -} -interface CloudflareBindings extends Cloudflare.Env {} -type StringifyValues> = { - [Binding in keyof EnvType]: EnvType[Binding] extends string ? EnvType[Binding] : string; -}; -declare namespace NodeJS { - interface ProcessEnv extends StringifyValues> {} + interface Env extends __BaseEnv_CloudflareBindings {} } +interface CloudflareBindings extends __BaseEnv_CloudflareBindings {} // Begin runtime types /*! ***************************************************************************** @@ -925,7 +920,7 @@ interface CustomEventCustomEventInit { * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Blob) */ declare class Blob { - constructor(type?: ((ArrayBuffer | ArrayBufferView) | string | Blob)[], options?: BlobOptions); + constructor(bits?: ((ArrayBuffer | ArrayBufferView) | string | Blob)[], options?: BlobOptions); /** * The **`size`** read-only property of the Blob interface returns the size of the Blob or File in bytes. * @@ -3492,6 +3487,229 @@ declare abstract class Span { get isTraced(): boolean; setAttribute(key: string, value?: (boolean | number | string)): void; } +// ============================================================================ +// Agent Memory +// +// Public type surface for user Workers binding to an Agent Memory namespace. +// ============================================================================ +/** Memory type — every memory is classified into exactly one. */ +type AgentMemoryMemoryType = "fact" | "event" | "instruction" | "task"; +/** Search intensity for recall. */ +type AgentMemoryThinkingLevel = "low" | "medium" | "high"; +/** Response verbosity for recall. */ +type AgentMemoryResponseLength = "short" | "medium" | "long"; +/** A conversation message passed to ingest(). */ +interface AgentMemoryMessage { + role: "system" | "user" | "assistant"; + content: string; + /** Optional message timestamp. */ + timestamp?: Date; +} +/** Raw memory content passed to remember(). */ +interface AgentMemoryIncomingMemory { + /** Raw memory content. The service classifies and summarizes automatically. */ + content: string; + /** Optional session identifier to associate with this memory. */ + sessionId?: string | null | undefined; +} +/** A stored memory returned from remember(), get(), and delete(). */ +interface AgentMemoryMemory { + /** Memory ID. */ + id: string; + /** Memory type. */ + type: AgentMemoryMemoryType; + /** Text summary. */ + summary: string; + /** Memory text. */ + content: string; + /** Session that created this memory. */ + sessionId: string | null; + /** Memory creation time. */ + createdAt: Date; + /** Memory last-update time. */ + updatedAt: Date; +} +/** Single entry in a list() response. Same shape as Memory minus full content. */ +type AgentMemoryMemoryListEntry = Omit; +/** A scored memory candidate in a recall result. */ +interface AgentMemoryScoredCandidate { + /** Candidate ID. */ + id: string; + /** Text summary. */ + summary: string; + /** Session that created this candidate, when known. */ + sessionId: string | null; + /** Relevance score (higher is better). Comparable only within a single query. */ + score: number; +} +/** Options for the ingest() method. */ +interface AgentMemoryIngestOptions { + /** Session identifier to associate with memories created during ingestion. */ + sessionId?: string | null | undefined; +} +/** Options for the getSummary() method. */ +interface AgentMemoryGetSummaryOptions { + /** Session identifier to retrieve session summary for. */ + sessionId?: string | null | undefined; +} +/** Response from the getSummary() method. */ +interface AgentMemoryGetSummaryResponse { + /** Markdown summary. */ + summary: string; +} +/** + * Options for the recall() method. + * + * `referenceDate` accepts a Date object, an ISO-8601 date string + * (YYYY-MM-DD), or a full ISO-8601 datetime string. When provided, this + * date is used as "today" for resolving relative time references + * ("how many days ago", "last week") instead of the server's wall-clock time. + */ +interface AgentMemoryRecallOptions { + /** Recall intensity: "low" (default), "medium", or "high". */ + thinkingLevel?: AgentMemoryThinkingLevel; + /** Response verbosity: "short", "medium" (default), or "long". */ + responseLength?: AgentMemoryResponseLength; + /** Temporal anchor for date arithmetic. */ + referenceDate?: Date | string; +} +/** Response from the recall() method. */ +interface AgentMemoryRecallResult { + /** Number of memories retrieved. */ + count: number; + /** LLM-generated answer synthesizing the matching memories. */ + answer: string; + /** Matching memories ranked by relevance. */ + candidates: AgentMemoryScoredCandidate[]; +} +/** + * Options for the list() method. + * + * `cursor` is the opaque continuation token returned by the previous page; + * pass it back unchanged to fetch the next page. `sessionId` and `type` + * are exact-match filters; combining them is allowed. + */ +interface AgentMemoryListMemoriesOptions { + /** Maximum number of memories to return. Default 20, max 500. */ + limit?: number; + /** Opaque cursor from a previous page. */ + cursor?: string; + /** Exact-match session filter. */ + sessionId?: string; + /** Exact-match memory-type filter. */ + type?: AgentMemoryMemoryType; +} +/** Response from the list() method. */ +interface AgentMemoryListMemoriesResult { + memories: AgentMemoryMemoryListEntry[]; + /** Continuation cursor; absent when this page exhausted the result set. */ + cursor?: string; +} +/** + * A single Agent Memory profile, scoped to a profile name. + * + * Returned by {@link AgentMemoryNamespace.getProfile}. + */ +declare abstract class AgentMemoryProfile { + /** + * Retrieve a memory by ID. + * + * @param memoryId - ULID of the memory to retrieve. + * @throws if the memory does not exist. + */ + get(memoryId: string): Promise; + /** + * Delete a memory by ID. + * + * Removes the memory and any source messages linked by the memory's + * source message IDs. + * + * @param memoryId - ULID of the memory to delete. + * @throws if the memory does not exist. + */ + delete(memoryId: string): Promise; + /** + * Store a memory in this profile. The content is automatically classified, + * summarized, and indexed. + * + * @param memory - Raw memory content to persist. + */ + remember(memory: AgentMemoryIncomingMemory): Promise; + /** + * Extract memories from a conversation. + * + * @param messages - Conversation messages to extract memories from. + * @param options - Optional ingest options. + */ + ingest(messages: Iterable, options?: AgentMemoryIngestOptions): Promise; + /** + * Get a profile summary. + * + * @param options - Optional getSummary options. + */ + getSummary(options?: AgentMemoryGetSummaryOptions): Promise; + /** + * Recall memories in this profile. + * + * @param query - Recall query matched against memory content and keywords. + * @param options - Optional recall parameters. + * @returns Matching memories with relevance scores and a synthesized answer. + */ + recall(query: string, options?: AgentMemoryRecallOptions): Promise; + /** + * List active memories in this profile. + * + * Returns a paginated, filterable view of stored memories. Superseded + * versions are excluded. Use the returned `cursor` (when present) to + * fetch the next page. + * + * @param options - Optional pagination and filter options. + */ + list(options?: AgentMemoryListMemoriesOptions): Promise; + /** + * Soft-delete every memory and message in this profile that is tagged + * with `sessionId`. + * + * Idempotent: deleting a sessionId that has no rows is a no-op. + * + * @param sessionId - Session to delete. + */ + deleteSession(sessionId: string): Promise; +} +/** + * Namespace-level Agent Memory binding. + * + * Used as the type of an `env.MEMORY`-style binding backed by the Agent + * Memory product. + * + * @example + * ```ts + * export default { + * async fetch(_request: Request, env: Env): Promise { + * const profile = await env.MEMORY.getProfile("wrangler-e2e"); + * const summary = await profile.getSummary(); + * return Response.json(summary); + * }, + * }; + * ``` + */ +declare abstract class AgentMemoryNamespace { + /** + * Get a memory profile by name. Profiles are isolated by namespace and + * addressed by a compound key (namespaceId:profileName). + * + * @param profileName - Profile name (validated against naming rules). + * @returns RPC target for interacting with the profile. + */ + getProfile(profileName: string): Promise; + /** + * Soft-delete a profile and schedule deferred purge. Marks all + * memories and messages as deleted. + * + * @param profileName - Name of the profile to delete. + */ + deleteProfile(profileName: string): Promise; +} // ============ AI Search Error Interfaces ============ interface AiSearchInternalError extends Error { } @@ -4837,9 +5055,6 @@ type ChatCompletionChoice = { finish_reason: "stop" | "length" | "tool_calls" | "content_filter" | "function_call"; logprobs: ChatCompletionLogprobs | null; }; -type ChatCompletionsPromptInput = { - prompt: string; -} & ChatCompletionsCommonOptions; type ChatCompletionsMessagesInput = { messages: Array; } & ChatCompletionsCommonOptions; @@ -8769,11 +8984,11 @@ declare abstract class Base_Ai_Cf_Pipecat_Ai_Smart_Turn_V2 { postProcessedOutputs: Ai_Cf_Pipecat_Ai_Smart_Turn_V2_Output; } declare abstract class Base_Ai_Cf_Openai_Gpt_Oss_120B { - inputs: XOR; + inputs: XOR; postProcessedOutputs: XOR; } declare abstract class Base_Ai_Cf_Openai_Gpt_Oss_20B { - inputs: XOR; + inputs: XOR; postProcessedOutputs: XOR; } interface Ai_Cf_Leonardo_Phoenix_1_0_Input { @@ -9749,6 +9964,10 @@ declare abstract class Base_Ai_Cf_Moonshotai_Kimi_K2_5 { inputs: ChatCompletionsInput; postProcessedOutputs: ChatCompletionsOutput; } +declare abstract class Base_Ai_Cf_Moonshotai_Kimi_K2_6 { + inputs: ChatCompletionsInput; + postProcessedOutputs: ChatCompletionsOutput; +} declare abstract class Base_Ai_Cf_Nvidia_Nemotron_3_120B_A12B { inputs: ChatCompletionsInput; postProcessedOutputs: ChatCompletionsOutput; @@ -9846,7 +10065,9 @@ interface AiModels { "@cf/black-forest-labs/flux-2-klein-9b": Base_Ai_Cf_Black_Forest_Labs_Flux_2_Klein_9B; "@cf/zai-org/glm-4.7-flash": Base_Ai_Cf_Zai_Org_Glm_4_7_Flash; "@cf/moonshotai/kimi-k2.5": Base_Ai_Cf_Moonshotai_Kimi_K2_5; + "@cf/moonshotai/kimi-k2.6": Base_Ai_Cf_Moonshotai_Kimi_K2_6; "@cf/nvidia/nemotron-3-120b-a12b": Base_Ai_Cf_Nvidia_Nemotron_3_120B_A12B; + "@cf/google/gemma-4-26b-a4b-it": Base_Ai_Cf_Google_Gemma_4_26B_A4B_IT; } type AiOptions = { /** @@ -9899,10 +10120,8 @@ type AiModelsSearchObject = { value: string; }[]; }; -type ChatCompletionsBase = XOR; -type ChatCompletionsInput = XOR; +type ChatCompletionsBase = ChatCompletionsMessagesInput; +type ChatCompletionsInput = ChatCompletionsMessagesInput; interface InferenceUpstreamError extends Error { } interface AiInternalError extends Error { @@ -9947,8 +10166,15 @@ declare abstract class Ai { }, options?: AiOptions): Promise; // Normal (default) - known model run(model: Name, inputs: AiModelList[Name]['inputs'], options?: AiOptions): Promise; - // Unknown model (gateway fallback) - run(model: string & {}, inputs: Record, options?: AiOptions): Promise>; + // Unknown model (fallback). + // + // The `Exclude<..., keyof AiModelList>` constraint forces TypeScript to + // route any model name that is a literal key of `AiModelList` to one of + // the known-model overloads above (so input/output mismatches surface as + // type errors rather than silently falling back to `Record`). + // Names that aren't in `AiModelList` — e.g. third-party gateway models + // like `"google/nano-banana"` — still hit this overload. + run(model: Model extends keyof AiModelList ? never : Model, inputs: Record, options?: AiOptions): Promise>; models(params?: AiModelsSearchParams): Promise; toMarkdown(): ToMarkdownService; toMarkdown(files: MarkdownDocument[], options?: ConversionRequestOptions): Promise; @@ -10139,12 +10365,17 @@ interface ArtifactsTokenListResult { /** Total number of tokens for the repository. */ total: number; } -/** Handle for a single repository. Returned by Artifacts.get(). */ +/** + * Handle for a single repository. Returned by Artifacts.get(). + * + * Methods may throw `ArtifactsError` with code `INTERNAL_ERROR` if an unexpected service error occurs. + */ interface ArtifactsRepo extends ArtifactsRepoInfo { /** * Create an access token for this repo. * @param scope Token scope: "write" (default) or "read". * @param ttl Time-to-live in seconds (default 86400, min 60, max 31536000). + * @throws {ArtifactsError} with code `INVALID_TTL` if ttl is out of range. */ createToken(scope?: 'write' | 'read', ttl?: number): Promise; /** List tokens for this repo (metadata only, no plaintext). */ @@ -10153,6 +10384,7 @@ interface ArtifactsRepo extends ArtifactsRepoInfo { * Revoke a token by plaintext or ID. * @param tokenOrId Plaintext token or token ID. * @returns true if revoked, false if not found. + * @throws {ArtifactsError} with code `INVALID_INPUT` if tokenOrId is empty. */ revokeToken(tokenOrId: string): Promise; // ── Fork ── @@ -10160,6 +10392,9 @@ interface ArtifactsRepo extends ArtifactsRepoInfo { * Fork this repo to a new repo. * @param name Target repository name. * @param opts Optional: description, readOnly flag, defaultBranchOnly (default true). + * @throws {ArtifactsError} with code `INVALID_REPO_NAME` if name is invalid. + * @throws {ArtifactsError} with code `ALREADY_EXISTS` if the target repo already exists. + * @throws {ArtifactsError} with code `FORK_IN_PROGRESS` if a fork is already running. */ fork(name: string, opts?: { description?: string; @@ -10167,13 +10402,41 @@ interface ArtifactsRepo extends ArtifactsRepoInfo { defaultBranchOnly?: boolean; }): Promise; } -/** Artifacts binding — namespace-level operations. */ +// ── Error types ────────────────────────────────────────────────────────────── +/** + * Error codes returned by Artifacts binding operations. + * + * Each code maps to a numeric code available on `ArtifactsError.numericCode`. + */ +type ArtifactsErrorCode = 'ALREADY_EXISTS' | 'NOT_FOUND' | 'IMPORT_IN_PROGRESS' | 'FORK_IN_PROGRESS' | 'INVALID_INPUT' | 'INVALID_REPO_NAME' | 'INVALID_TTL' | 'INVALID_URL' | 'REMOTE_AUTH_REQUIRED' | 'UPSTREAM_UNAVAILABLE' | 'MEMORY_LIMIT' | 'INTERNAL_ERROR'; +/** + * Error thrown by Artifacts binding operations. + * + * Uses a string `.code` discriminator following the Cloudflare platform + * convention (StreamError, ImagesError, etc.). The `.numericCode` matches + * the REST API `errors[].code` values. + */ +interface ArtifactsError extends Error { + readonly name: 'ArtifactsError'; + /** String error code for programmatic matching. */ + readonly code: ArtifactsErrorCode; + /** Numeric error code matching the REST API. */ + readonly numericCode: number; +} +// ── Binding ────────────────────────────────────────────────────────────────── +/** + * Artifacts binding — namespace-level operations. + * + * Methods may throw `ArtifactsError` with code `INTERNAL_ERROR` if an unexpected service error occurs. + */ interface Artifacts { /** * Create a new repository with an initial access token. * @param name Repository name (alphanumeric, dots, hyphens, underscores). * @param opts Optional: readOnly flag, description, default branch name. * @returns Repo metadata with initial token. + * @throws {ArtifactsError} with code `INVALID_REPO_NAME` if name is invalid. + * @throws {ArtifactsError} with code `ALREADY_EXISTS` if the repo already exists. */ create(name: string, opts?: { readOnly?: boolean; @@ -10184,12 +10447,23 @@ interface Artifacts { * Get a handle to an existing repository. * @param name Repository name. * @returns Repo handle. + * @throws {ArtifactsError} with code `NOT_FOUND` if the repo does not exist. + * @throws {ArtifactsError} with code `IMPORT_IN_PROGRESS` if the repo is still importing. + * @throws {ArtifactsError} with code `FORK_IN_PROGRESS` if the repo is still forking. */ get(name: string): Promise; /** * Import a repository from an external git remote. * @param params Source URL and optional branch/depth, plus target name and options. * @returns Repo metadata with initial token. + * @throws {ArtifactsError} with code `INVALID_REPO_NAME` if the target name is invalid. + * @throws {ArtifactsError} with code `INVALID_INPUT` if the source URL is not valid HTTPS. + * @throws {ArtifactsError} with code `INVALID_URL` if the source URL does not point to a git repository. + * @throws {ArtifactsError} with code `REMOTE_AUTH_REQUIRED` if the remote requires authentication. + * @throws {ArtifactsError} with code `NOT_FOUND` if the remote repository does not exist. + * @throws {ArtifactsError} with code `UPSTREAM_UNAVAILABLE` if the remote cannot be reached. + * @throws {ArtifactsError} with code `MEMORY_LIMIT` if the import exceeds service memory limits. + * @throws {ArtifactsError} with code `ALREADY_EXISTS` if the target repo already exists. */ import(params: { source: { @@ -10217,6 +10491,7 @@ interface Artifacts { * Delete a repository and all associated tokens. * @param name Repository name. * @returns true if deleted, false if not found. + * @throws {ArtifactsError} with code `INVALID_REPO_NAME` if name is invalid. */ delete(name: string): Promise; } @@ -10357,77 +10632,451 @@ declare abstract class AutoRAG { */ aiSearch(params: AutoRagAiSearchRequest): Promise; } -interface BasicImageTransformations { +type BrowserRunLifecycleEvent = 'load' | 'domcontentloaded' | 'networkidle0' | 'networkidle2'; +type BrowserRunResourceType = 'document' | 'stylesheet' | 'image' | 'media' | 'font' | 'script' | 'texttrack' | 'xhr' | 'fetch' | 'prefetch' | 'eventsource' | 'websocket' | 'manifest' | 'signedexchange' | 'ping' | 'cspviolationreport' | 'preflight' | 'other'; +/** Options fields shared by all quick actions. */ +interface BrowserRunBaseOptions { + /** Adds `