Emdash source with visual editor image upload fix

Fixes:
1. media.ts: wrap placeholder generation in try-catch
2. toolbar.ts: check r.ok, display error message in popover
This commit is contained in:
2026-05-03 10:44:54 +07:00
parent 78f81bebb6
commit 2d1be52177
2352 changed files with 662964 additions and 0 deletions

View File

@@ -0,0 +1,234 @@
/**
* x402 Payment Enforcer
*
* Creates the x402 enforcement interface. Uses the @x402/core SDK
* to handle the payment protocol negotiation.
*/
import {
decodePaymentSignatureHeader,
encodePaymentRequiredHeader,
encodePaymentResponseHeader,
} from "@x402/core/http";
import { HTTPFacilitatorClient, x402ResourceServer, type ResourceConfig } from "@x402/core/server";
import type { EnforceOptions, EnforceResult, X402Config, X402Enforcer } from "./types.js";
const PAYMENT_SIGNATURE_HEADER = "payment-signature";
const PAYMENT_REQUIRED_HEADER = "PAYMENT-REQUIRED";
const PAYMENT_RESPONSE_HEADER = "PAYMENT-RESPONSE";
const DEFAULT_FACILITATOR_URL = "https://x402.org/facilitator";
const DEFAULT_SCHEME = "exact";
const DEFAULT_MAX_TIMEOUT_SECONDS = 60;
const DEFAULT_BOT_SCORE_THRESHOLD = 30;
/**
* Cached resource server instance.
* Initialized once per process, reused across requests.
*/
let _resourceServer: x402ResourceServer | null = null;
let _initPromise: Promise<void> | null = null;
/**
* Get or create the x402ResourceServer singleton.
*/
async function getResourceServer(config: X402Config): Promise<x402ResourceServer> {
if (!_resourceServer) {
const facilitatorUrl = config.facilitatorUrl ?? DEFAULT_FACILITATOR_URL;
const facilitator = new HTTPFacilitatorClient({ url: facilitatorUrl });
const server = new x402ResourceServer(facilitator);
// Register EVM scheme (default)
if (config.evm !== false) {
try {
const evmMod = await import("@x402/evm/exact/server");
const evmScheme = new evmMod.ExactEvmScheme();
server.register("eip155:*" as `${string}:${string}`, evmScheme);
} catch {
// @x402/evm not installed -- skip EVM support
}
}
// Register SVM scheme (opt-in)
if (config.svm) {
try {
const svmMod = await import("@x402/svm/exact/server");
const svmScheme = new svmMod.ExactSvmScheme();
server.register("solana:*" as `${string}:${string}`, svmScheme);
} catch {
// @x402/svm not installed -- skip Solana support
}
}
_resourceServer = server;
_initPromise = server.initialize();
}
if (_initPromise) {
await _initPromise;
_initPromise = null;
}
return _resourceServer;
}
/**
* Check if a request is from a bot using Cloudflare Bot Management.
* Returns true if the request is likely from a bot, false otherwise.
* When bot management data is unavailable (local dev, non-CF deployment),
* returns false (treat as human).
*/
function isBot(request: Request, threshold: number): boolean {
// Cloudflare Workers expose cf properties on the request
const cf: unknown = Reflect.get(request, "cf");
if (cf == null || typeof cf !== "object") return false;
const bm: unknown = Reflect.get(cf, "botManagement");
if (bm == null || typeof bm !== "object") return false;
const score: unknown = Reflect.get(bm, "score");
if (typeof score !== "number") return false;
return score < threshold;
}
/**
* Create an X402Enforcer for the given configuration.
* Called once by the middleware, reused across requests.
*/
export function createEnforcer(config: X402Config): X402Enforcer {
const botScoreThreshold = config.botScoreThreshold ?? DEFAULT_BOT_SCORE_THRESHOLD;
return {
async enforce(request: Request, options?: EnforceOptions): Promise<Response | EnforceResult> {
// In botOnly mode, skip enforcement for humans
if (config.botOnly && !isBot(request, botScoreThreshold)) {
return { paid: false, skipped: true, responseHeaders: {} };
}
const server = await getResourceServer(config);
const price = options?.price ?? config.defaultPrice;
if (price == null) {
throw new Error(
"x402: No price specified. Pass a price in enforce() options or set defaultPrice in the config.",
);
}
const payTo = options?.payTo ?? config.payTo;
const network = options?.network ?? config.network;
const scheme = options?.scheme ?? config.scheme ?? DEFAULT_SCHEME;
const maxTimeoutSeconds = config.maxTimeoutSeconds ?? DEFAULT_MAX_TIMEOUT_SECONDS;
const resourceConfig: ResourceConfig = {
scheme,
payTo,
price: normalizePrice(price),
network,
maxTimeoutSeconds,
};
const url = new URL(request.url);
const resourceInfo = {
url: url.pathname,
description: options?.description,
mimeType: options?.mimeType,
};
// Check for payment signature header
const paymentHeader =
request.headers.get(PAYMENT_SIGNATURE_HEADER) || request.headers.get("PAYMENT-SIGNATURE");
if (!paymentHeader) {
return make402(server, resourceConfig, resourceInfo, "Payment required");
}
// Payment present -- decode and verify
const paymentPayload = decodePaymentSignatureHeader(paymentHeader);
const requirements = await server.buildPaymentRequirements(resourceConfig);
const matchingReqs = server.findMatchingRequirements(requirements, paymentPayload);
if (!matchingReqs) {
return make402(
server,
resourceConfig,
resourceInfo,
"Payment does not match accepted requirements",
);
}
// Verify with facilitator
const verifyResult = await server.verifyPayment(paymentPayload, matchingReqs);
if (!verifyResult.isValid) {
return make402(
server,
resourceConfig,
resourceInfo,
verifyResult.invalidReason ?? "Payment verification failed",
);
}
// Settle
const settleResult = await server.settlePayment(paymentPayload, matchingReqs);
const responseHeaders: Record<string, string> = {};
if (settleResult) {
responseHeaders[PAYMENT_RESPONSE_HEADER] = encodePaymentResponseHeader(settleResult);
}
return {
paid: true,
skipped: false,
payer: verifyResult.payer,
settlement: settleResult,
responseHeaders,
};
},
applyHeaders(result: EnforceResult, response: { headers: Headers }): void {
for (const [key, value] of Object.entries(result.responseHeaders)) {
response.headers.set(key, value);
}
},
hasPayment(request: Request): boolean {
return !!(
request.headers.get(PAYMENT_SIGNATURE_HEADER) || request.headers.get("PAYMENT-SIGNATURE")
);
},
};
}
/** Build and return a 402 Response */
async function make402(
server: x402ResourceServer,
resourceConfig: ResourceConfig,
resourceInfo: { url: string; description?: string; mimeType?: string },
error: string,
): Promise<Response> {
const requirements = await server.buildPaymentRequirements(resourceConfig);
const paymentRequired = await server.createPaymentRequiredResponse(
requirements,
resourceInfo,
error,
);
return new Response(JSON.stringify(paymentRequired), {
status: 402,
headers: {
"Content-Type": "application/json",
[PAYMENT_REQUIRED_HEADER]: encodePaymentRequiredHeader(paymentRequired),
},
});
}
/**
* Normalize a user-friendly price into the format expected by x402 SDK.
*/
function normalizePrice(
price: string | number | { amount: string; asset: string; extra?: Record<string, unknown> },
): string | number | { amount: string; asset: string; extra?: Record<string, unknown> } {
if (typeof price === "string" && price.startsWith("$")) {
return price.slice(1);
}
return price;
}

View File

@@ -0,0 +1,99 @@
/**
* @emdash-cms/x402 -- x402 Payment Integration for Astro
*
* An Astro integration that provides x402 payment enforcement via
* Astro.locals.x402. Supports bot-only mode using Cloudflare Bot Management.
*
* @example
* ```ts
* // astro.config.mjs
* import { x402 } from "@emdash-cms/x402";
*
* export default defineConfig({
* integrations: [
* x402({
* payTo: "0xYourWallet",
* network: "eip155:8453",
* defaultPrice: "$0.01",
* botOnly: true,
* }),
* ],
* });
* ```
*
* ```astro
* ---
* const { x402 } = Astro.locals;
*
* const result = await x402.enforce(Astro.request, { price: "$0.05" });
* if (result instanceof Response) return result;
*
* x402.applyHeaders(result, Astro.response);
* ---
* <article>Premium content here</article>
* ```
*/
import type { AstroIntegration } from "astro";
import type { X402Config } from "./types.js";
const VIRTUAL_MODULE_ID = "virtual:x402/config";
const RESOLVED_VIRTUAL_MODULE_ID = "\0" + VIRTUAL_MODULE_ID;
/**
* Create the x402 Astro integration.
*/
export function x402(config: X402Config): AstroIntegration {
return {
name: "@emdash-cms/x402",
hooks: {
"astro:config:setup": ({ addMiddleware, updateConfig }) => {
// Inject the virtual module that provides config to the middleware.
// The middleware must be excluded from Vite's SSR dependency optimizer
// because esbuild cannot resolve virtual modules — only Vite plugins can.
updateConfig({
vite: {
plugins: [
{
name: "x402-virtual-config",
resolveId(id: string) {
if (id === VIRTUAL_MODULE_ID) return RESOLVED_VIRTUAL_MODULE_ID;
},
load(id: string) {
if (id === RESOLVED_VIRTUAL_MODULE_ID) {
return `export default ${JSON.stringify(config)}`;
}
},
},
],
optimizeDeps: {
exclude: ["@emdash-cms/x402"],
},
ssr: {
optimizeDeps: {
exclude: ["@emdash-cms/x402"],
},
},
},
});
// Register the middleware that puts the enforcer on locals
addMiddleware({
entrypoint: "@emdash-cms/x402/middleware",
order: "pre",
});
},
},
};
}
// Re-export types for convenience
export type {
EnforceOptions,
EnforceResult,
Network,
Price,
X402Config,
X402Enforcer,
} from "./types.js";

View File

@@ -0,0 +1,25 @@
/**
* x402 Astro Middleware
*
* Injected by the x402 integration. Creates the enforcer and
* places it on Astro.locals.x402 for use in page frontmatter.
*
* The config is passed via the virtual module resolved by the integration.
*/
import { defineMiddleware } from "astro:middleware";
// The integration injects config via a virtual module.
// @ts-ignore -- virtual module, resolved at build time
import x402Config from "virtual:x402/config";
import { createEnforcer } from "./enforcer.js";
import type { X402Config } from "./types.js";
// eslint-disable-next-line typescript-eslint/no-unsafe-type-assertion -- virtual module import has no type info
const config: X402Config = x402Config as X402Config;
const enforcer = createEnforcer(config);
export const onRequest = defineMiddleware(async (context, next) => {
context.locals.x402 = enforcer;
return next();
});

141
packages/x402/src/types.ts Normal file
View File

@@ -0,0 +1,141 @@
/**
* x402 Payment Integration Types
*/
import type { SettleResponse } from "@x402/core/types";
/** CAIP-2 network identifier (e.g., "eip155:8453" for Base mainnet) */
export type Network = `${string}:${string}`;
/** Human-readable price: "$0.10", "0.50", or atomic units { amount, asset } */
export type Price =
| string
| number
| { amount: string; asset: string; extra?: Record<string, unknown> };
/**
* Configuration for the x402 Astro integration.
*
* @example
* ```ts
* import { x402 } from "@emdash-cms/x402";
*
* export default defineConfig({
* integrations: [
* x402({
* payTo: "0xYourWallet",
* network: "eip155:8453",
* defaultPrice: "$0.01",
* botOnly: true,
* }),
* ],
* });
* ```
*/
export interface X402Config {
/** Destination wallet address for payments */
payTo: string;
/** CAIP-2 network identifier */
network: Network;
/** Default price for content (can be overridden per-page) */
defaultPrice?: Price;
/** Facilitator URL (defaults to x402.org testnet facilitator) */
facilitatorUrl?: string;
/** Payment scheme (defaults to "exact") */
scheme?: string;
/** Maximum timeout for payment signatures in seconds (defaults to 60) */
maxTimeoutSeconds?: number;
/** Enable EVM chain support (defaults to true) */
evm?: boolean;
/** Enable Solana chain support (defaults to false) */
svm?: boolean;
/**
* Only enforce payment for bots/agents, not humans.
* Uses Cloudflare Bot Management score from request.cf.botManagement.score.
* Requires Cloudflare deployment with Bot Management enabled.
* When true, requests with a bot score >= botScoreThreshold are treated as
* human and enforcement is skipped.
*/
botOnly?: boolean;
/**
* Bot score threshold. Requests with a score below this are treated as bots.
* Only used when botOnly is true. Defaults to 30.
* Score range: 1 (almost certainly bot) to 99 (almost certainly human).
*/
botScoreThreshold?: number;
}
/**
* Options passed to enforce() to override defaults for a specific page.
*/
export interface EnforceOptions {
/** Override the price for this specific request */
price?: Price;
/** Override the destination wallet */
payTo?: string;
/** Override the network */
network?: Network;
/** Override the payment scheme */
scheme?: string;
/** Resource description for the payment prompt */
description?: string;
/** MIME type hint for the resource */
mimeType?: string;
}
/**
* Result of a successful payment enforcement check.
* Returned when the request should proceed (either paid or skipped).
*/
export interface EnforceResult {
/** Whether payment was required and verified */
paid: boolean;
/** Whether enforcement was skipped (e.g., human in botOnly mode) */
skipped: boolean;
/** The payer's wallet address (if paid) */
payer?: string;
/** Settlement response (if payment was settled) */
settlement?: SettleResponse;
/** Headers to add to the response (e.g., PAYMENT-RESPONSE) */
responseHeaders: Record<string, string>;
}
/**
* The x402 enforcement interface available on Astro.locals.x402.
*/
export interface X402Enforcer {
/**
* Check if the current request includes valid payment.
* If not paid, returns a 402 Response that should be returned directly.
* If paid (or skipped in botOnly mode), returns an EnforceResult.
*
* @param request - The incoming Request object
* @param options - Optional overrides for this specific enforcement
* @returns A 402 Response (return it) or an EnforceResult (proceed with page render)
*
* @example
* ```astro
* ---
* const { x402 } = Astro.locals;
*
* const result = await x402.enforce(Astro.request, { price: "$0.01" });
* if (result instanceof Response) return result;
*
* x402.applyHeaders(result, Astro.response);
* ---
* ```
*/
enforce(request: Request, options?: EnforceOptions): Promise<Response | EnforceResult>;
/**
* Apply x402 response headers (e.g., PAYMENT-RESPONSE) to the Astro response.
* Call this after a successful enforce() to include settlement proof in the response.
*/
applyHeaders(result: EnforceResult, response: { headers: Headers }): void;
/**
* Check if a request has a payment signature without verifying it.
* Useful for conditional rendering without enforcement.
*/
hasPayment(request: Request): boolean;
}