From 33a4e82f8d3db55702c0a2d0de10c74a1dedf161 Mon Sep 17 00:00:00 2001 From: Ginola Date: Thu, 5 Feb 2026 00:14:31 +0700 Subject: [PATCH 01/13] feat: dockerfile --- Dockerfile | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 Dockerfile diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..1479076 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,9 @@ +FROM node:18-alpine AS deps +WORKDIR /app +COPY package.json package-lock.json ./ +RUN npm ci + +COPY . . + +EXPOSE 3000 +CMD ["npm", "start"] From a5c648464773e82df2779abfdee70cf6ec41764e Mon Sep 17 00:00:00 2001 From: Ginola Date: Fri, 6 Feb 2026 23:41:09 +0700 Subject: [PATCH 02/13] feat: marketplace api --- scripts/schema.sql | 81 ++++---- src/routes/index.js | 36 ++-- src/routes/marketplace.js | 124 ++++++++++++ src/services/MarketplaceService.js | 309 +++++++++++++++++++++++++++++ 4 files changed, 492 insertions(+), 58 deletions(-) create mode 100644 src/routes/marketplace.js create mode 100644 src/services/MarketplaceService.js diff --git a/scripts/schema.sql b/scripts/schema.sql index 876d570..d4936df 100644 --- a/scripts/schema.sql +++ b/scripts/schema.sql @@ -1,9 +1,7 @@ -- Moltbook Database Schema -- PostgreSQL / Supabase compatible - -- Enable UUID extension CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; - -- Agents (AI agent accounts) CREATE TABLE agents ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), @@ -11,155 +9,130 @@ CREATE TABLE agents ( display_name VARCHAR(64), description TEXT, avatar_url TEXT, - -- Authentication api_key_hash VARCHAR(64) NOT NULL, claim_token VARCHAR(80), verification_code VARCHAR(16), - -- Status status VARCHAR(20) DEFAULT 'pending_claim', is_claimed BOOLEAN DEFAULT false, is_active BOOLEAN DEFAULT true, - -- Stats karma INTEGER DEFAULT 0, + credits INTEGER DEFAULT 0, follower_count INTEGER DEFAULT 0, following_count INTEGER DEFAULT 0, - -- Owner (Twitter/X verification) owner_twitter_id VARCHAR(64), owner_twitter_handle VARCHAR(64), - -- Timestamps created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), claimed_at TIMESTAMP WITH TIME ZONE, last_active TIMESTAMP WITH TIME ZONE DEFAULT NOW() ); - CREATE INDEX idx_agents_name ON agents(name); CREATE INDEX idx_agents_api_key_hash ON agents(api_key_hash); CREATE INDEX idx_agents_claim_token ON agents(claim_token); - -- Submolts (communities) CREATE TABLE submolts ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), name VARCHAR(24) UNIQUE NOT NULL, display_name VARCHAR(64), description TEXT, - -- Customization avatar_url TEXT, banner_url TEXT, banner_color VARCHAR(7), theme_color VARCHAR(7), - -- Stats subscriber_count INTEGER DEFAULT 0, post_count INTEGER DEFAULT 0, - -- Creator creator_id UUID REFERENCES agents(id), - -- Timestamps created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() ); - CREATE INDEX idx_submolts_name ON submolts(name); CREATE INDEX idx_submolts_subscriber_count ON submolts(subscriber_count DESC); - -- Submolt moderators CREATE TABLE submolt_moderators ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), submolt_id UUID NOT NULL REFERENCES submolts(id) ON DELETE CASCADE, agent_id UUID NOT NULL REFERENCES agents(id) ON DELETE CASCADE, - role VARCHAR(20) DEFAULT 'moderator', -- 'owner' or 'moderator' + role VARCHAR(20) DEFAULT 'moderator', + -- 'owner' or 'moderator' created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), UNIQUE(submolt_id, agent_id) ); - CREATE INDEX idx_submolt_moderators_submolt ON submolt_moderators(submolt_id); - -- Posts CREATE TABLE posts ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), author_id UUID NOT NULL REFERENCES agents(id) ON DELETE CASCADE, submolt_id UUID NOT NULL REFERENCES submolts(id) ON DELETE CASCADE, submolt VARCHAR(24) NOT NULL, - -- Content title VARCHAR(300) NOT NULL, content TEXT, url TEXT, - post_type VARCHAR(10) DEFAULT 'text', -- 'text' or 'link' - + post_type VARCHAR(10) DEFAULT 'text', + -- 'text' or 'link' -- Stats score INTEGER DEFAULT 0, upvotes INTEGER DEFAULT 0, downvotes INTEGER DEFAULT 0, comment_count INTEGER DEFAULT 0, - -- Moderation is_pinned BOOLEAN DEFAULT false, is_deleted BOOLEAN DEFAULT false, - -- Timestamps created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() ); - CREATE INDEX idx_posts_author ON posts(author_id); CREATE INDEX idx_posts_submolt ON posts(submolt_id); CREATE INDEX idx_posts_submolt_name ON posts(submolt); CREATE INDEX idx_posts_created ON posts(created_at DESC); CREATE INDEX idx_posts_score ON posts(score DESC); - -- Comments CREATE TABLE comments ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), post_id UUID NOT NULL REFERENCES posts(id) ON DELETE CASCADE, author_id UUID NOT NULL REFERENCES agents(id) ON DELETE CASCADE, parent_id UUID REFERENCES comments(id) ON DELETE CASCADE, - -- Content content TEXT NOT NULL, - -- Stats score INTEGER DEFAULT 0, upvotes INTEGER DEFAULT 0, downvotes INTEGER DEFAULT 0, - -- Threading depth INTEGER DEFAULT 0, - -- Moderation is_deleted BOOLEAN DEFAULT false, - -- Timestamps created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() ); - CREATE INDEX idx_comments_post ON comments(post_id); CREATE INDEX idx_comments_author ON comments(author_id); CREATE INDEX idx_comments_parent ON comments(parent_id); - -- Votes CREATE TABLE votes ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), agent_id UUID NOT NULL REFERENCES agents(id) ON DELETE CASCADE, target_id UUID NOT NULL, - target_type VARCHAR(10) NOT NULL, -- 'post' or 'comment' - value SMALLINT NOT NULL, -- 1 or -1 + target_type VARCHAR(10) NOT NULL, + -- 'post' or 'comment' + value SMALLINT NOT NULL, + -- 1 or -1 created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), UNIQUE(agent_id, target_id, target_type) ); - CREATE INDEX idx_votes_agent ON votes(agent_id); CREATE INDEX idx_votes_target ON votes(target_id, target_type); - -- Subscriptions (agent subscribes to submolt) CREATE TABLE subscriptions ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), @@ -168,10 +141,8 @@ CREATE TABLE subscriptions ( created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), UNIQUE(agent_id, submolt_id) ); - CREATE INDEX idx_subscriptions_agent ON subscriptions(agent_id); CREATE INDEX idx_subscriptions_submolt ON subscriptions(submolt_id); - -- Follows (agent follows agent) CREATE TABLE follows ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), @@ -180,10 +151,38 @@ CREATE TABLE follows ( created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), UNIQUE(follower_id, followed_id) ); - CREATE INDEX idx_follows_follower ON follows(follower_id); CREATE INDEX idx_follows_followed ON follows(followed_id); - +-- Marketplace listings (agents offer APIs/services) +CREATE TABLE marketplace_listings ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + agent_id UUID NOT NULL REFERENCES agents(id) ON DELETE CASCADE, + title VARCHAR(120) NOT NULL, + description TEXT, + price_credits INTEGER NOT NULL CHECK (price_credits >= 0), + metadata JSONB, + is_active BOOLEAN DEFAULT true, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() +); +CREATE INDEX idx_marketplace_listings_agent ON marketplace_listings(agent_id); +CREATE INDEX idx_marketplace_listings_active ON marketplace_listings(is_active); +-- Marketplace orders (agents buy listings) +CREATE TABLE marketplace_orders ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + listing_id UUID NOT NULL REFERENCES marketplace_listings(id) ON DELETE RESTRICT, + buyer_id UUID NOT NULL REFERENCES agents(id) ON DELETE CASCADE, + seller_id UUID NOT NULL REFERENCES agents(id) ON DELETE CASCADE, + price_credits INTEGER NOT NULL CHECK (price_credits >= 0), + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() +); +CREATE INDEX idx_marketplace_orders_buyer ON marketplace_orders(buyer_id); +CREATE INDEX idx_marketplace_orders_seller ON marketplace_orders(seller_id); +CREATE INDEX idx_marketplace_orders_listing ON marketplace_orders(listing_id); -- Create default submolt INSERT INTO submolts (name, display_name, description) -VALUES ('general', 'General', 'The default community for all moltys'); +VALUES ( + 'general', + 'General', + 'The default community for all moltys' + ); \ No newline at end of file diff --git a/src/routes/index.js b/src/routes/index.js index bb20467..b458f45 100644 --- a/src/routes/index.js +++ b/src/routes/index.js @@ -3,15 +3,16 @@ * Combines all API routes under /api/v1 */ -const { Router } = require('express'); -const { requestLimiter } = require('../middleware/rateLimit'); +const { Router } = require("express"); +const { requestLimiter } = require("../middleware/rateLimit"); -const agentRoutes = require('./agents'); -const postRoutes = require('./posts'); -const commentRoutes = require('./comments'); -const submoltRoutes = require('./submolts'); -const feedRoutes = require('./feed'); -const searchRoutes = require('./search'); +const agentRoutes = require("./agents"); +const postRoutes = require("./posts"); +const commentRoutes = require("./comments"); +const submoltRoutes = require("./submolts"); +const feedRoutes = require("./feed"); +const searchRoutes = require("./search"); +const marketplaceRoutes = require("./marketplace"); const router = Router(); @@ -19,19 +20,20 @@ const router = Router(); router.use(requestLimiter); // Mount routes -router.use('/agents', agentRoutes); -router.use('/posts', postRoutes); -router.use('/comments', commentRoutes); -router.use('/submolts', submoltRoutes); -router.use('/feed', feedRoutes); -router.use('/search', searchRoutes); +router.use("/agents", agentRoutes); +router.use("/posts", postRoutes); +router.use("/comments", commentRoutes); +router.use("/submolts", submoltRoutes); +router.use("/feed", feedRoutes); +router.use("/search", searchRoutes); +router.use("/marketplace", marketplaceRoutes); // Health check (no auth required) -router.get('/health', (req, res) => { +router.get("/health", (req, res) => { res.json({ success: true, - status: 'healthy', - timestamp: new Date().toISOString() + status: "healthy", + timestamp: new Date().toISOString(), }); }); diff --git a/src/routes/marketplace.js b/src/routes/marketplace.js new file mode 100644 index 0000000..cb6e4ac --- /dev/null +++ b/src/routes/marketplace.js @@ -0,0 +1,124 @@ +/** + * Marketplace Routes + * /api/v1/marketplace/* + */ + +const { Router } = require("express"); +const { asyncHandler } = require("../middleware/errorHandler"); +const { requireAuth } = require("../middleware/auth"); +const { success, created, paginated, noContent } = require("../utils/response"); +const MarketplaceService = require("../services/MarketplaceService"); +const config = require("../config"); + +const router = Router(); + +/** + * GET /marketplace/listings + * Browse marketplace listings + */ +router.get( + "/listings", + asyncHandler(async (req, res) => { + const { limit, offset, seller } = req.query; + + const parsedLimit = Math.min( + Number.parseInt(limit, 10) || config.pagination.defaultLimit, + config.pagination.maxLimit + ); + const parsedOffset = Number.parseInt(offset, 10) || 0; + + const listings = await MarketplaceService.listListings({ + limit: parsedLimit, + offset: parsedOffset, + agentId: seller || null, + activeOnly: true, + }); + + paginated(res, listings, { limit: parsedLimit, offset: parsedOffset }); + }) +); + +/** + * POST /marketplace/listings + * Create a new listing for the authenticated agent + */ +router.post( + "/listings", + requireAuth, + asyncHandler(async (req, res) => { + const { title, description, priceCredits, metadata } = req.body; + + const listing = await MarketplaceService.createListing({ + agentId: req.agent.id, + title, + description, + priceCredits, + metadata, + }); + + created(res, { listing }); + }) +); + +/** + * GET /marketplace/listings/:id + * Get a single listing + */ +router.get( + "/listings/:id", + asyncHandler(async (req, res) => { + const listing = await MarketplaceService.getListing(req.params.id); + success(res, { listing }); + }) +); + +/** + * DELETE /marketplace/listings/:id + * Archive a listing (seller only) + */ +router.delete( + "/listings/:id", + requireAuth, + asyncHandler(async (req, res) => { + await MarketplaceService.archiveListing(req.params.id, req.agent.id); + noContent(res); + }) +); + +/** + * POST /marketplace/listings/:id/buy + * Buy a listing using credits + */ +router.post( + "/listings/:id/buy", + requireAuth, + asyncHandler(async (req, res) => { + const result = await MarketplaceService.buyListing({ + listingId: req.params.id, + buyerId: req.agent.id, + }); + + success(res, result); + }) +); + +/** + * GET /marketplace/orders + * Get orders for the authenticated agent (as buyer/seller/all) + */ +router.get( + "/orders", + requireAuth, + asyncHandler(async (req, res) => { + const { role = "buyer" } = req.query; + + const orders = await MarketplaceService.getOrdersForAgent({ + agentId: req.agent.id, + role, + }); + + success(res, { orders }); + }) +); + +module.exports = router; diff --git a/src/services/MarketplaceService.js b/src/services/MarketplaceService.js new file mode 100644 index 0000000..d18a93c --- /dev/null +++ b/src/services/MarketplaceService.js @@ -0,0 +1,309 @@ +/** + * Marketplace Service + * Agents can create listings and buy/sell using credits + */ + +const { queryOne, queryAll, transaction } = require("../config/database"); +const { + BadRequestError, + NotFoundError, + ForbiddenError, +} = require("../utils/errors"); + +class MarketplaceService { + /** + * Create a new marketplace listing + * + * @param {Object} params + * @param {string} params.agentId - Listing owner (seller) agent ID + * @param {string} params.title - Listing title + * @param {string} [params.description] - Listing description + * @param {number|string} params.priceCredits - Price in credits + * @param {Object} [params.metadata] - Optional structured metadata (JSON) + */ + static async createListing({ + agentId, + title, + description = "", + priceCredits, + metadata = null, + }) { + if (!title || typeof title !== "string" || title.trim().length < 3) { + throw new BadRequestError("Title must be at least 3 characters"); + } + + const price = Number.parseInt(priceCredits, 10); + if (Number.isNaN(price) || price < 0) { + throw new BadRequestError("priceCredits must be a non-negative integer"); + } + + const listing = await queryOne( + `INSERT INTO marketplace_listings (agent_id, title, description, price_credits, metadata) + VALUES ($1, $2, $3, $4, $5) + RETURNING id, agent_id, title, description, price_credits, metadata, is_active, created_at, updated_at`, + [agentId, title.trim(), description, price, metadata] + ); + + return listing; + } + + /** + * List marketplace listings + * + * @param {Object} params + * @param {number} params.limit + * @param {number} params.offset + * @param {string|null} [params.agentId] - Filter by seller + * @param {boolean} [params.activeOnly] - Only active listings + */ + static async listListings({ + limit = 25, + offset = 0, + agentId = null, + activeOnly = true, + }) { + const conditions = []; + const values = []; + let index = 1; + + if (agentId) { + conditions.push(`l.agent_id = $${index}`); + values.push(agentId); + index += 1; + } + + if (activeOnly) { + conditions.push(`l.is_active = true`); + } + + const whereClause = + conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : ""; + + const paginationValues = [limit, offset]; + values.push(...paginationValues); + + const listings = await queryAll( + `SELECT + l.id, + l.agent_id, + a.name AS agent_name, + a.display_name AS agent_display_name, + l.title, + l.description, + l.price_credits, + l.metadata, + l.is_active, + l.created_at, + l.updated_at + FROM marketplace_listings l + JOIN agents a ON l.agent_id = a.id + ${whereClause} + ORDER BY l.created_at DESC + LIMIT $${index++} OFFSET $${index}`, + values + ); + + return listings; + } + + /** + * Get a single listing + * + * @param {string} id - Listing ID + */ + static async getListing(id) { + const listing = await queryOne( + `SELECT + l.id, + l.agent_id, + a.name AS agent_name, + a.display_name AS agent_display_name, + l.title, + l.description, + l.price_credits, + l.metadata, + l.is_active, + l.created_at, + l.updated_at + FROM marketplace_listings l + JOIN agents a ON l.agent_id = a.id + WHERE l.id = $1`, + [id] + ); + + if (!listing) { + throw new NotFoundError("Listing"); + } + + return listing; + } + + /** + * Archive (deactivate) a listing + * + * @param {string} listingId + * @param {string} agentId - Requesting agent (must be seller) + */ + static async archiveListing(listingId, agentId) { + const listing = await queryOne( + `SELECT id, agent_id, is_active FROM marketplace_listings WHERE id = $1`, + [listingId] + ); + + if (!listing) { + throw new NotFoundError("Listing"); + } + + if (listing.agent_id !== agentId) { + throw new ForbiddenError("You can only modify your own listings"); + } + + if (!listing.is_active) { + return; + } + + await queryOne( + `UPDATE marketplace_listings + SET is_active = false, + updated_at = NOW() + WHERE id = $1`, + [listingId] + ); + } + + /** + * Buy a listing + * + * Transfers credits from buyer to seller and records an order. + * + * @param {Object} params + * @param {string} params.listingId + * @param {string} params.buyerId + */ + static async buyListing({ listingId, buyerId }) { + return transaction(async (client) => { + // Lock listing + const listingResult = await client.query( + `SELECT + l.id, + l.agent_id, + l.price_credits, + l.is_active + FROM marketplace_listings l + WHERE l.id = $1 + FOR UPDATE`, + [listingId] + ); + + const listing = listingResult.rows[0]; + + if (!listing || !listing.is_active) { + throw new NotFoundError("Listing"); + } + + if (listing.agent_id === buyerId) { + throw new BadRequestError("You cannot buy your own listing"); + } + + // Lock buyer and seller rows + const buyerResult = await client.query( + `SELECT id, credits FROM agents WHERE id = $1 FOR UPDATE`, + [buyerId] + ); + const buyerRow = buyerResult.rows[0]; + + if (!buyerRow?.id) { + throw new NotFoundError("Buyer"); + } + + const buyer = buyerRow; + + const sellerResult = await client.query( + `SELECT id, credits FROM agents WHERE id = $1 FOR UPDATE`, + [listing.agent_id] + ); + const sellerRow = sellerResult.rows[0]; + + if (!sellerRow?.id) { + throw new NotFoundError("Seller"); + } + + const seller = sellerRow; + + if (buyer.credits < listing.price_credits) { + throw new ForbiddenError( + "Insufficient credits to buy this listing", + "Top up your credits before purchasing" + ); + } + + // Transfer credits + await client.query( + `UPDATE agents SET credits = credits - $2 WHERE id = $1`, + [buyer.id, listing.price_credits] + ); + + await client.query( + `UPDATE agents SET credits = credits + $2 WHERE id = $1`, + [seller.id, listing.price_credits] + ); + + // Record order + const orderResult = await client.query( + `INSERT INTO marketplace_orders (listing_id, buyer_id, seller_id, price_credits) + VALUES ($1, $2, $3, $4) + RETURNING id, listing_id, buyer_id, seller_id, price_credits, created_at`, + [listing.id, buyer.id, seller.id, listing.price_credits] + ); + + const order = orderResult.rows[0]; + + return { + order, + listingId: listing.id, + }; + }); + } + + /** + * Get orders for an agent + * + * @param {Object} params + * @param {string} params.agentId + * @param {'buyer'|'seller'|'all'} [params.role='buyer'] + */ + static async getOrdersForAgent({ agentId, role = "buyer" }) { + let where; + const values = [agentId]; + + if (role === "seller") { + where = "o.seller_id = $1"; + } else if (role === "all") { + where = "(o.buyer_id = $1 OR o.seller_id = $1)"; + } else { + where = "o.buyer_id = $1"; + } + + const orders = await queryAll( + `SELECT + o.id, + o.listing_id, + o.buyer_id, + buyer.name AS buyer_name, + o.seller_id, + seller.name AS seller_name, + o.price_credits, + o.created_at + FROM marketplace_orders o + JOIN agents buyer ON o.buyer_id = buyer.id + JOIN agents seller ON o.seller_id = seller.id + WHERE ${where} + ORDER BY o.created_at DESC`, + values + ); + + return orders; + } +} + +module.exports = MarketplaceService; From 6215611ed1bdb7774a5475cbed80eab4adec4767 Mon Sep 17 00:00:00 2001 From: Ginola Date: Sat, 7 Feb 2026 23:52:19 +0700 Subject: [PATCH 03/13] feat: agent --- .env.example | 9 + scripts/add-agent-runtime.sql | 5 + scripts/schema.sql | 4 + src/config/index.js | 69 ++++--- src/routes/agents.js | 282 ++++++++++++++++++++-------- src/services/AgentRuntimeService.js | 70 +++++++ src/services/AgentService.js | 250 +++++++++++++++--------- 7 files changed, 492 insertions(+), 197 deletions(-) create mode 100644 scripts/add-agent-runtime.sql create mode 100644 src/services/AgentRuntimeService.js diff --git a/.env.example b/.env.example index c55ed96..ab06bd3 100644 --- a/.env.example +++ b/.env.example @@ -17,3 +17,12 @@ BASE_URL=http://localhost:3000 # Twitter/X OAuth (for verification) TWITTER_CLIENT_ID= TWITTER_CLIENT_SECRET= + +# Cloud Run agent runtime (optional) +# Shared multi-tenant service URL +CLOUD_RUN_SHARED_SERVICE_URL=https://your-shared-service.run.app +# Dedicated: base URL for new-service-per-agent (or leave empty to disable) +CLOUD_RUN_DEDICATED_BASE_URL=https://moltbook-agent.run.app +# GCP for dedicated deployments (when implementing Cloud Run API) +# GCP_PROJECT_ID= +# GCP_REGION=europe-west1 diff --git a/scripts/add-agent-runtime.sql b/scripts/add-agent-runtime.sql new file mode 100644 index 0000000..8ea177b --- /dev/null +++ b/scripts/add-agent-runtime.sql @@ -0,0 +1,5 @@ +-- Add runtime columns to agents (run after schema.sql on existing DBs) +ALTER TABLE agents +ADD COLUMN IF NOT EXISTS runtime_endpoint TEXT; +ALTER TABLE agents +ADD COLUMN IF NOT EXISTS deployment_mode VARCHAR(20); \ No newline at end of file diff --git a/scripts/schema.sql b/scripts/schema.sql index d4936df..6752f6b 100644 --- a/scripts/schema.sql +++ b/scripts/schema.sql @@ -25,6 +25,10 @@ CREATE TABLE agents ( -- Owner (Twitter/X verification) owner_twitter_id VARCHAR(64), owner_twitter_handle VARCHAR(64), + -- Runtime (Cloud Run) + runtime_endpoint TEXT, + deployment_mode VARCHAR(20), + -- 'dedicated' | 'shared' -- Timestamps created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), diff --git a/src/config/index.js b/src/config/index.js index 84a5bf2..bb2e7ef 100644 --- a/src/config/index.js +++ b/src/config/index.js @@ -2,61 +2,82 @@ * Application configuration */ -require('dotenv').config(); +require("dotenv").config(); const config = { // Server port: parseInt(process.env.PORT, 10) || 3000, - nodeEnv: process.env.NODE_ENV || 'development', - isProduction: process.env.NODE_ENV === 'production', - + nodeEnv: process.env.NODE_ENV || "development", + isProduction: process.env.NODE_ENV === "production", + // Database database: { url: process.env.DATABASE_URL, - ssl: process.env.NODE_ENV === 'production' ? { rejectUnauthorized: false } : false + ssl: + process.env.NODE_ENV === "production" + ? { rejectUnauthorized: false } + : false, }, - + // Redis (optional) redis: { - url: process.env.REDIS_URL + url: process.env.REDIS_URL, }, - + // Security - jwtSecret: process.env.JWT_SECRET || 'development-secret-change-in-production', - + jwtSecret: + process.env.JWT_SECRET || "development-secret-change-in-production", + // Rate Limits rateLimits: { requests: { max: 100, window: 60 }, posts: { max: 1, window: 1800 }, - comments: { max: 50, window: 3600 } + comments: { max: 50, window: 3600 }, }, - + // Moltbook specific moltbook: { - tokenPrefix: 'moltbook_', - claimPrefix: 'moltbook_claim_', - baseUrl: process.env.BASE_URL || 'https://www.moltbook.com' + tokenPrefix: "moltbook_", + claimPrefix: "moltbook_claim_", + baseUrl: process.env.BASE_URL || "https://www.moltbook.com", }, - + // Pagination defaults pagination: { defaultLimit: 25, - maxLimit: 100 - } + maxLimit: 100, + }, + + // Cloud Run agent runtime + cloudRun: { + // Shared multi-tenant service URL (agents register here) + sharedServiceUrl: + process.env.CLOUD_RUN_SHARED_SERVICE_URL || + "https://moltbook-agents-shared.example.run.app", + // GCP project and region for dedicated (one container per agent) + projectId: process.env.GCP_PROJECT_ID || "", + region: process.env.GCP_REGION || "europe-west1", + // Base URL for dedicated services (e.g. https://agent-{id}.run.app or custom domain) + dedicatedBaseUrl: + process.env.CLOUD_RUN_DEDICATED_BASE_URL || + "https://moltbook-agent.example.run.app", + }, }; // Validate required config function validateConfig() { const required = []; - + if (config.isProduction) { - required.push('DATABASE_URL', 'JWT_SECRET'); + required.push("DATABASE_URL", "JWT_SECRET"); } - - const missing = required.filter(key => !process.env[key]); - + + const missing = required.filter((key) => !process.env[key]); + if (missing.length > 0) { - throw new Error(`Missing required environment variables: ${missing.join(', ')}`); + throw new Error( + `Missing required environment variables: ${missing.join(", ")}` + ); } } diff --git a/src/routes/agents.js b/src/routes/agents.js index 58398ef..18dde6d 100644 --- a/src/routes/agents.js +++ b/src/routes/agents.js @@ -3,123 +3,243 @@ * /api/v1/agents/* */ -const { Router } = require('express'); -const { asyncHandler } = require('../middleware/errorHandler'); -const { requireAuth } = require('../middleware/auth'); -const { success, created } = require('../utils/response'); -const AgentService = require('../services/AgentService'); -const { NotFoundError } = require('../utils/errors'); +const { Router } = require("express"); +const { asyncHandler } = require("../middleware/errorHandler"); +const { requireAuth } = require("../middleware/auth"); +const { success, created, paginated } = require("../utils/response"); +const AgentService = require("../services/AgentService"); +const AgentRuntimeService = require("../services/AgentRuntimeService"); +const { NotFoundError } = require("../utils/errors"); +const config = require("../config"); const router = Router(); +/** + * GET /agents + * List agents (paginated, optional sort) + */ +router.get( + "/", + asyncHandler(async (req, res) => { + const { limit, offset, sort } = req.query; + const parsedLimit = Math.min( + Number.parseInt(limit, 10) || config.pagination.defaultLimit, + config.pagination.maxLimit + ); + const parsedOffset = Number.parseInt(offset, 10) || 0; + const sortVal = sort === "new" ? "new" : "karma"; + + const agents = await AgentService.list({ + limit: parsedLimit, + offset: parsedOffset, + sort: sortVal, + }); + + const items = agents.map((a) => ({ + id: a.id, + name: a.name, + displayName: a.display_name, + description: a.description, + karma: a.karma, + status: a.status, + isClaimed: a.is_claimed, + followerCount: a.follower_count, + followingCount: a.following_count, + createdAt: a.created_at, + lastActive: a.last_active, + })); + + paginated(res, items, { limit: parsedLimit, offset: parsedOffset }); + }) +); + /** * POST /agents/register * Register a new agent */ -router.post('/register', asyncHandler(async (req, res) => { - const { name, description } = req.body; - const result = await AgentService.register({ name, description }); - created(res, result); -})); +router.post( + "/register", + asyncHandler(async (req, res) => { + const { name, description } = req.body; + const result = await AgentService.register({ name, description }); + created(res, result); + }) +); + +/** + * POST /agents/deploy-dedicated + * Create and deploy agent in a new Cloud Run service (dedicated container). + * Call after signup with Bearer API key. Returns agentId and runtime endpoint. + */ +router.post( + "/deploy-dedicated", + requireAuth, + asyncHandler(async (req, res) => { + const { id: agentId, name: agentName } = req.agent; + const { endpoint } = await AgentRuntimeService.deployDedicated( + agentId, + agentName + ); + const updated = await AgentService.updateRuntime(agentId, { + runtime_endpoint: endpoint, + deployment_mode: "dedicated", + }); + success(res, { + agentId: updated.id, + runtimeEndpoint: updated.runtime_endpoint, + deploymentMode: "dedicated", + }); + }) +); + +/** + * POST /agents/deploy-shared + * Create agent inside existing Cloud Run service (shared / multi-tenant). + * Call after signup with Bearer API key. Returns agentId and runtime endpoint. + */ +router.post( + "/deploy-shared", + requireAuth, + asyncHandler(async (req, res) => { + const { id: agentId, name: agentName } = req.agent; + const { endpoint } = await AgentRuntimeService.deployShared( + agentId, + agentName + ); + const updated = await AgentService.updateRuntime(agentId, { + runtime_endpoint: endpoint, + deployment_mode: "shared", + }); + success(res, { + agentId: updated.id, + runtimeEndpoint: updated.runtime_endpoint, + deploymentMode: "shared", + }); + }) +); /** * GET /agents/me * Get current agent profile */ -router.get('/me', requireAuth, asyncHandler(async (req, res) => { - success(res, { agent: req.agent }); -})); +router.get( + "/me", + requireAuth, + asyncHandler(async (req, res) => { + success(res, { agent: req.agent }); + }) +); /** * PATCH /agents/me * Update current agent profile */ -router.patch('/me', requireAuth, asyncHandler(async (req, res) => { - const { description, displayName } = req.body; - const agent = await AgentService.update(req.agent.id, { - description, - display_name: displayName - }); - success(res, { agent }); -})); +router.patch( + "/me", + requireAuth, + asyncHandler(async (req, res) => { + const { description, displayName } = req.body; + const agent = await AgentService.update(req.agent.id, { + description, + display_name: displayName, + }); + success(res, { agent }); + }) +); /** * GET /agents/status * Get agent claim status */ -router.get('/status', requireAuth, asyncHandler(async (req, res) => { - const status = await AgentService.getStatus(req.agent.id); - success(res, status); -})); +router.get( + "/status", + requireAuth, + asyncHandler(async (req, res) => { + const status = await AgentService.getStatus(req.agent.id); + success(res, status); + }) +); /** * GET /agents/profile * Get another agent's profile */ -router.get('/profile', requireAuth, asyncHandler(async (req, res) => { - const { name } = req.query; - - if (!name) { - throw new NotFoundError('Agent'); - } - - const agent = await AgentService.findByName(name); - - if (!agent) { - throw new NotFoundError('Agent'); - } - - // Check if current user is following - const isFollowing = await AgentService.isFollowing(req.agent.id, agent.id); - - // Get recent posts - const recentPosts = await AgentService.getRecentPosts(agent.id); - - success(res, { - agent: { - name: agent.name, - displayName: agent.display_name, - description: agent.description, - karma: agent.karma, - followerCount: agent.follower_count, - followingCount: agent.following_count, - isClaimed: agent.is_claimed, - createdAt: agent.created_at, - lastActive: agent.last_active - }, - isFollowing, - recentPosts - }); -})); +router.get( + "/profile", + requireAuth, + asyncHandler(async (req, res) => { + const { name } = req.query; + + if (!name) { + throw new NotFoundError("Agent"); + } + + const agent = await AgentService.findByName(name); + + if (!agent) { + throw new NotFoundError("Agent"); + } + + // Check if current user is following + const isFollowing = await AgentService.isFollowing(req.agent.id, agent.id); + + // Get recent posts + const recentPosts = await AgentService.getRecentPosts(agent.id); + + success(res, { + agent: { + name: agent.name, + displayName: agent.display_name, + description: agent.description, + karma: agent.karma, + followerCount: agent.follower_count, + followingCount: agent.following_count, + isClaimed: agent.is_claimed, + createdAt: agent.created_at, + lastActive: agent.last_active, + }, + isFollowing, + recentPosts, + }); + }) +); /** * POST /agents/:name/follow * Follow an agent */ -router.post('/:name/follow', requireAuth, asyncHandler(async (req, res) => { - const agent = await AgentService.findByName(req.params.name); - - if (!agent) { - throw new NotFoundError('Agent'); - } - - const result = await AgentService.follow(req.agent.id, agent.id); - success(res, result); -})); +router.post( + "/:name/follow", + requireAuth, + asyncHandler(async (req, res) => { + const agent = await AgentService.findByName(req.params.name); + + if (!agent) { + throw new NotFoundError("Agent"); + } + + const result = await AgentService.follow(req.agent.id, agent.id); + success(res, result); + }) +); /** * DELETE /agents/:name/follow * Unfollow an agent */ -router.delete('/:name/follow', requireAuth, asyncHandler(async (req, res) => { - const agent = await AgentService.findByName(req.params.name); - - if (!agent) { - throw new NotFoundError('Agent'); - } - - const result = await AgentService.unfollow(req.agent.id, agent.id); - success(res, result); -})); +router.delete( + "/:name/follow", + requireAuth, + asyncHandler(async (req, res) => { + const agent = await AgentService.findByName(req.params.name); + + if (!agent) { + throw new NotFoundError("Agent"); + } + + const result = await AgentService.unfollow(req.agent.id, agent.id); + success(res, result); + }) +); module.exports = router; diff --git a/src/services/AgentRuntimeService.js b/src/services/AgentRuntimeService.js new file mode 100644 index 0000000..5d536da --- /dev/null +++ b/src/services/AgentRuntimeService.js @@ -0,0 +1,70 @@ +/** + * Agent Runtime Service + * Creates and deploys agent runtimes: dedicated (new Cloud Run service) or shared (multi-tenant). + */ + +const config = require("../config"); +const { InternalError } = require("../utils/errors"); + +/** + * Create and deploy a new agent in a dedicated Cloud Run service (one container per agent). + * In production, this would call the Google Cloud Run API to create a new service. + * + * @param {string} agentId - Agent UUID + * @param {string} agentName - Agent name (e.g. for service naming) + * @returns {Promise<{ endpoint: string }>} Runtime endpoint URL + */ +async function deployDedicated(agentId, agentName) { + const { cloudRun } = config; + const baseUrl = (cloudRun.dedicatedBaseUrl || "").replace(/\/$/, ""); + + if (!baseUrl) { + throw new InternalError( + "Dedicated runtime not configured", + "Set CLOUD_RUN_DEDICATED_BASE_URL or implement Cloud Run API integration" + ); + } + + // In production: call Google Cloud Run API to create a new service + // e.g. POST https://run.googleapis.com/v2/projects/{project}/locations/{region}/services + // with image, service name from (agentName || agentId), then get status.url + const endpoint = `${baseUrl}/agent/${agentId}`; + + // Placeholder: return endpoint. Replace with real deployment: + // const run = require('@google-cloud/run'); + // const [service] = await client.createService({ parent, service: { ... } }); + // return { endpoint: service.uri }; + return { endpoint }; +} + +/** + * Create agent inside the existing shared Cloud Run service (multi-tenant). + * In production, this would call the shared service's admin API to register the agent. + * + * @param {string} agentId - Agent UUID + * @param {string} agentName - Agent name + * @returns {Promise<{ endpoint: string }>} Runtime endpoint URL for this agent + */ +async function deployShared(agentId, agentName) { + const { cloudRun } = config; + const baseUrl = (cloudRun.sharedServiceUrl || "").replace(/\/$/, ""); + + if (!baseUrl) { + throw new InternalError( + "Shared runtime not configured", + "Set CLOUD_RUN_SHARED_SERVICE_URL" + ); + } + + // In production: POST to shared service to register agent, e.g.: + // POST {baseUrl}/internal/agents with { agentId, agentName } + // Response: { endpoint: "https://shared.run.app/agents/agent-123" } + const endpoint = `${baseUrl}/agents/${agentId}`; + + return { endpoint }; +} + +module.exports = { + deployDedicated, + deployShared, +}; diff --git a/src/services/AgentService.js b/src/services/AgentService.js index 29bc501..fbb2f82 100644 --- a/src/services/AgentService.js +++ b/src/services/AgentService.js @@ -3,97 +3,113 @@ * Handles agent registration, authentication, and profile management */ -const { queryOne, queryAll, transaction } = require('../config/database'); -const { generateApiKey, generateClaimToken, generateVerificationCode, hashToken } = require('../utils/auth'); -const { BadRequestError, NotFoundError, ConflictError } = require('../utils/errors'); -const config = require('../config'); +const { queryOne, queryAll, transaction } = require("../config/database"); +const { + generateApiKey, + generateClaimToken, + generateVerificationCode, + hashToken, +} = require("../utils/auth"); +const { + BadRequestError, + NotFoundError, + ConflictError, +} = require("../utils/errors"); +const config = require("../config"); class AgentService { /** * Register a new agent - * + * * @param {Object} data - Registration data * @param {string} data.name - Agent name * @param {string} data.description - Agent description * @returns {Promise} Registration result with API key */ - static async register({ name, description = '' }) { + static async register({ name, description = "" }) { // Validate name - if (!name || typeof name !== 'string') { - throw new BadRequestError('Name is required'); + if (!name || typeof name !== "string") { + throw new BadRequestError("Name is required"); } - + const normalizedName = name.toLowerCase().trim(); - + if (normalizedName.length < 2 || normalizedName.length > 32) { - throw new BadRequestError('Name must be 2-32 characters'); + throw new BadRequestError("Name must be 2-32 characters"); } - + if (!/^[a-z0-9_]+$/i.test(normalizedName)) { throw new BadRequestError( - 'Name can only contain letters, numbers, and underscores' + "Name can only contain letters, numbers, and underscores" ); } - + // Check if name exists - const existing = await queryOne( - 'SELECT id FROM agents WHERE name = $1', - [normalizedName] - ); - + const existing = await queryOne("SELECT id FROM agents WHERE name = $1", [ + normalizedName, + ]); + if (existing) { - throw new ConflictError('Name already taken', 'Try a different name'); + throw new ConflictError("Name already taken", "Try a different name"); } - + // Generate credentials const apiKey = generateApiKey(); const claimToken = generateClaimToken(); const verificationCode = generateVerificationCode(); const apiKeyHash = hashToken(apiKey); - + // Create agent const agent = await queryOne( `INSERT INTO agents (name, display_name, description, api_key_hash, claim_token, verification_code, status) VALUES ($1, $2, $3, $4, $5, $6, 'pending_claim') RETURNING id, name, display_name, created_at`, - [normalizedName, name.trim(), description, apiKeyHash, claimToken, verificationCode] + [ + normalizedName, + name.trim(), + description, + apiKeyHash, + claimToken, + verificationCode, + ] ); - + return { agent: { + id: agent.id, api_key: apiKey, claim_url: `${config.moltbook.baseUrl}/claim/${claimToken}`, - verification_code: verificationCode + verification_code: verificationCode, }, - important: 'Save your API key! You will not see it again.' + important: "Save your API key! You will not see it again.", }; } - + /** * Find agent by API key - * + * * @param {string} apiKey - API key * @returns {Promise} Agent or null */ static async findByApiKey(apiKey) { const apiKeyHash = hashToken(apiKey); - + return queryOne( `SELECT id, name, display_name, description, karma, status, is_claimed, created_at, updated_at FROM agents WHERE api_key_hash = $1`, [apiKeyHash] ); } - + /** * Find agent by name - * + * * @param {string} name - Agent name * @returns {Promise} Agent or null */ static async findByName(name) { const normalizedName = name.toLowerCase().trim(); - + return queryOne( `SELECT id, name, display_name, description, karma, status, is_claimed, follower_count, following_count, created_at, last_active @@ -101,10 +117,10 @@ class AgentService { [normalizedName] ); } - + /** * Find agent by ID - * + * * @param {string} id - Agent ID * @returns {Promise} Agent or null */ @@ -116,20 +132,20 @@ class AgentService { [id] ); } - + /** * Update agent profile - * + * * @param {string} id - Agent ID * @param {Object} updates - Fields to update * @returns {Promise} Updated agent */ static async update(id, updates) { - const allowedFields = ['description', 'display_name', 'avatar_url']; + const allowedFields = ["description", "display_name", "avatar_url"]; const setClause = []; const values = []; let paramIndex = 1; - + for (const field of allowedFields) { if (updates[field] !== undefined) { setClause.push(`${field} = $${paramIndex}`); @@ -137,51 +153,78 @@ class AgentService { paramIndex++; } } - + if (setClause.length === 0) { - throw new BadRequestError('No valid fields to update'); + throw new BadRequestError("No valid fields to update"); } - + setClause.push(`updated_at = NOW()`); values.push(id); - + const agent = await queryOne( - `UPDATE agents SET ${setClause.join(', ')} WHERE id = $${paramIndex} + `UPDATE agents SET ${setClause.join(", ")} WHERE id = $${paramIndex} RETURNING id, name, display_name, description, karma, status, is_claimed, updated_at`, values ); - + if (!agent) { - throw new NotFoundError('Agent'); + throw new NotFoundError("Agent"); } - + return agent; } - + + /** + * Update agent runtime (endpoint and deployment mode) + * + * @param {string} agentId - Agent ID + * @param {Object} data - { runtime_endpoint, deployment_mode } + * @returns {Promise} Updated agent id and endpoint + */ + static async updateRuntime(agentId, { runtime_endpoint, deployment_mode }) { + const agent = await queryOne( + `UPDATE agents + SET runtime_endpoint = $2, deployment_mode = $3, updated_at = NOW() + WHERE id = $1 + RETURNING id, runtime_endpoint, deployment_mode`, + [agentId, runtime_endpoint, deployment_mode] + ); + + if (!agent) { + throw new NotFoundError("Agent"); + } + + return { + id: agent.id, + runtime_endpoint: agent.runtime_endpoint, + deployment_mode: agent.deployment_mode, + }; + } + /** * Get agent status - * + * * @param {string} id - Agent ID * @returns {Promise} Status info */ static async getStatus(id) { const agent = await queryOne( - 'SELECT status, is_claimed FROM agents WHERE id = $1', + "SELECT status, is_claimed FROM agents WHERE id = $1", [id] ); - + if (!agent) { - throw new NotFoundError('Agent'); + throw new NotFoundError("Agent"); } - + return { - status: agent.is_claimed ? 'claimed' : 'pending_claim' + status: agent.is_claimed ? "claimed" : "pending_claim", }; } - + /** * Claim an agent (verify ownership) - * + * * @param {string} claimToken - Claim token * @param {Object} twitterData - Twitter verification data * @returns {Promise} Claimed agent @@ -198,17 +241,17 @@ class AgentService { RETURNING id, name, display_name`, [claimToken, twitterData.id, twitterData.handle] ); - + if (!agent) { - throw new NotFoundError('Claim token'); + throw new NotFoundError("Claim token"); } - + return agent; } - + /** * Update agent karma - * + * * @param {string} id - Agent ID * @param {number} delta - Karma change * @returns {Promise} New karma value @@ -218,101 +261,101 @@ class AgentService { `UPDATE agents SET karma = karma + $2 WHERE id = $1 RETURNING karma`, [id, delta] ); - + return result?.karma || 0; } - + /** * Follow an agent - * + * * @param {string} followerId - Follower agent ID * @param {string} followedId - Agent to follow ID * @returns {Promise} Result */ static async follow(followerId, followedId) { if (followerId === followedId) { - throw new BadRequestError('Cannot follow yourself'); + throw new BadRequestError("Cannot follow yourself"); } - + // Check if already following const existing = await queryOne( - 'SELECT id FROM follows WHERE follower_id = $1 AND followed_id = $2', + "SELECT id FROM follows WHERE follower_id = $1 AND followed_id = $2", [followerId, followedId] ); - + if (existing) { - return { success: true, action: 'already_following' }; + return { success: true, action: "already_following" }; } - + await transaction(async (client) => { await client.query( - 'INSERT INTO follows (follower_id, followed_id) VALUES ($1, $2)', + "INSERT INTO follows (follower_id, followed_id) VALUES ($1, $2)", [followerId, followedId] ); - + await client.query( - 'UPDATE agents SET following_count = following_count + 1 WHERE id = $1', + "UPDATE agents SET following_count = following_count + 1 WHERE id = $1", [followerId] ); - + await client.query( - 'UPDATE agents SET follower_count = follower_count + 1 WHERE id = $1', + "UPDATE agents SET follower_count = follower_count + 1 WHERE id = $1", [followedId] ); }); - - return { success: true, action: 'followed' }; + + return { success: true, action: "followed" }; } - + /** * Unfollow an agent - * + * * @param {string} followerId - Follower agent ID * @param {string} followedId - Agent to unfollow ID * @returns {Promise} Result */ static async unfollow(followerId, followedId) { const result = await queryOne( - 'DELETE FROM follows WHERE follower_id = $1 AND followed_id = $2 RETURNING id', + "DELETE FROM follows WHERE follower_id = $1 AND followed_id = $2 RETURNING id", [followerId, followedId] ); - + if (!result) { - return { success: true, action: 'not_following' }; + return { success: true, action: "not_following" }; } - + await Promise.all([ queryOne( - 'UPDATE agents SET following_count = following_count - 1 WHERE id = $1', + "UPDATE agents SET following_count = following_count - 1 WHERE id = $1", [followerId] ), queryOne( - 'UPDATE agents SET follower_count = follower_count - 1 WHERE id = $1', + "UPDATE agents SET follower_count = follower_count - 1 WHERE id = $1", [followedId] - ) + ), ]); - - return { success: true, action: 'unfollowed' }; + + return { success: true, action: "unfollowed" }; } - + /** * Check if following - * + * * @param {string} followerId - Follower ID * @param {string} followedId - Followed ID * @returns {Promise} */ static async isFollowing(followerId, followedId) { const result = await queryOne( - 'SELECT id FROM follows WHERE follower_id = $1 AND followed_id = $2', + "SELECT id FROM follows WHERE follower_id = $1 AND followed_id = $2", [followerId, followedId] ); return !!result; } - + /** * Get recent posts by agent - * + * * @param {string} agentId - Agent ID * @param {number} limit - Max posts * @returns {Promise} Posts @@ -325,6 +368,29 @@ class AgentService { [agentId, limit] ); } + + /** + * List agents with pagination and optional sort + * + * @param {Object} options + * @param {number} options.limit - Max agents + * @param {number} options.offset - Offset for pagination + * @param {string} options.sort - 'karma' | 'new' + * @returns {Promise} Agents + */ + static async list({ limit = 25, offset = 0, sort = "karma" }) { + const orderBy = + sort === "new" ? "a.created_at DESC" : "a.karma DESC, a.created_at DESC"; + + return queryAll( + `SELECT a.id, a.name, a.display_name, a.description, a.karma, a.status, + a.is_claimed, a.follower_count, a.following_count, a.created_at, a.last_active + FROM agents a + ORDER BY ${orderBy} + LIMIT $1 OFFSET $2`, + [limit, offset] + ); + } } module.exports = AgentService; From 2e7c16f0f9a476e6b2e684fdcf8338013d3484ec Mon Sep 17 00:00:00 2001 From: Ginola Date: Sun, 8 Feb 2026 16:04:46 +0700 Subject: [PATCH 04/13] fix: allowed origin --- README.md | 15 ++++++++---- src/app.js | 60 ++++++++++++++++++++++++++------------------- src/config/index.js | 5 ++++ 3 files changed, 50 insertions(+), 30 deletions(-) diff --git a/README.md b/README.md index 489d339..5b4a803 100644 --- a/README.md +++ b/README.md @@ -72,10 +72,13 @@ Base URL: `https://www.moltbook.com/api/v1` ### Authentication All authenticated endpoints require the header: + ``` Authorization: Bearer YOUR_API_KEY ``` +**Unified Moltbook + OpenClaw auth**: The same API key works for both Moltbook web and OpenClaw CLI. Token format: `moltbook_` + 64 hex chars. In production, CORS allows OpenClaw origins (`openclaw.com`, `*.openclaw.ai`). + ### Agents #### Register a new agent @@ -91,6 +94,7 @@ Content-Type: application/json ``` Response: + ```json { "agent": { @@ -329,13 +333,14 @@ Returns matching posts, agents, and submolts. ## Rate Limits -| Resource | Limit | Window | -|----------|-------|--------| -| General requests | 100 | 1 minute | -| Posts | 1 | 30 minutes | -| Comments | 50 | 1 hour | +| Resource | Limit | Window | +| ---------------- | ----- | ---------- | +| General requests | 100 | 1 minute | +| Posts | 1 | 30 minutes | +| Comments | 50 | 1 hour | Rate limit headers are included in responses: + ``` X-RateLimit-Limit: 100 X-RateLimit-Remaining: 95 diff --git a/src/app.js b/src/app.js index 748952c..50b45bc 100644 --- a/src/app.js +++ b/src/app.js @@ -2,55 +2,65 @@ * Express Application Setup */ -const express = require('express'); -const cors = require('cors'); -const helmet = require('helmet'); -const compression = require('compression'); -const morgan = require('morgan'); +const express = require("express"); +const cors = require("cors"); +const helmet = require("helmet"); +const compression = require("compression"); +const morgan = require("morgan"); -const routes = require('./routes'); -const { notFoundHandler, errorHandler } = require('./middleware/errorHandler'); -const config = require('./config'); +const routes = require("./routes"); +const { notFoundHandler, errorHandler } = require("./middleware/errorHandler"); +const config = require("./config"); const app = express(); // Security middleware app.use(helmet()); -// CORS -app.use(cors({ - origin: config.isProduction - ? ['https://www.moltbook.com', 'https://moltbook.com'] - : '*', - methods: ['GET', 'POST', 'PATCH', 'DELETE'], - allowedHeaders: ['Content-Type', 'Authorization'] -})); +// CORS (unified auth: Moltbook + OpenClaw share same tokens) +// Origins from config.corsAllowedOrigins (env CORS_ALLOWED_ORIGINS, comma-separated) +const allowedOrigins = config.corsAllowedOrigins; +app.use( + cors({ + origin: config.isProduction + ? (origin, cb) => { + if (!origin) return cb(null, true); + if (allowedOrigins.includes(origin)) return cb(null, true); + if (/^https:\/\/[a-z0-9-]+\.openclaw\.ai$/.test(origin)) + return cb(null, true); + cb(null, false); + } + : "*", + methods: ["GET", "POST", "PATCH", "DELETE"], + allowedHeaders: ["Content-Type", "Authorization"], + }) +); // Compression app.use(compression()); // Request logging if (!config.isProduction) { - app.use(morgan('dev')); + app.use(morgan("dev")); } else { - app.use(morgan('combined')); + app.use(morgan("combined")); } // Body parsing -app.use(express.json({ limit: '1mb' })); +app.use(express.json({ limit: "1mb" })); // Trust proxy (for rate limiting behind reverse proxy) -app.set('trust proxy', 1); +app.set("trust proxy", 1); // API routes -app.use('/api/v1', routes); +app.use("/api/v1", routes); // Root endpoint -app.get('/', (req, res) => { +app.get("/", (req, res) => { res.json({ - name: 'Moltbook API', - version: '1.0.0', - documentation: 'https://www.moltbook.com/skill.md' + name: "Moltbook API", + version: "1.0.0", + documentation: "https://www.moltbook.com/skill.md", }); }); diff --git a/src/config/index.js b/src/config/index.js index bb2e7ef..7f2f384 100644 --- a/src/config/index.js +++ b/src/config/index.js @@ -35,6 +35,11 @@ const config = { comments: { max: 50, window: 3600 }, }, + // CORS allowed origins (comma-separated; env CORS_ALLOWED_ORIGINS) + corsAllowedOrigins: process.env.CORS_ALLOWED_ORIGINS?.split(",").map((o) => + o.trim() + ), + // Moltbook specific moltbook: { tokenPrefix: "moltbook_", From 2adb4293775f367c301ba089030016c444f81479 Mon Sep 17 00:00:00 2001 From: Ginola Date: Sun, 8 Feb 2026 16:04:55 +0700 Subject: [PATCH 05/13] fix: env --- .env.example | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.env.example b/.env.example index ab06bd3..4183f7a 100644 --- a/.env.example +++ b/.env.example @@ -14,6 +14,9 @@ JWT_SECRET=change-this-in-production # Base URL BASE_URL=http://localhost:3000 +# CORS allowed origins (comma-separated; production only) +# CORS_ALLOWED_ORIGINS=https://www.moltbook.com,https://moltbook.com,https://openclaw.com,https://www.openclaw.com,https://docs.openclaw.ai + # Twitter/X OAuth (for verification) TWITTER_CLIENT_ID= TWITTER_CLIENT_SECRET= From eb5a696f316ce3911cfe0ec5db6e4326a8768e0e Mon Sep 17 00:00:00 2001 From: Ginola Date: Tue, 10 Feb 2026 01:06:32 +0700 Subject: [PATCH 06/13] feat: cloud run deploy --- .env.example | 8 +- package-lock.json | 1443 ++++++++++++++++++++++++- package.json | 13 +- src/routes/cloudRun.js | 46 + src/routes/index.js | 2 + src/services/CloudRunDeployService.js | 321 ++++++ templates/cloud-run-service.yaml | 21 + 7 files changed, 1831 insertions(+), 23 deletions(-) create mode 100644 src/routes/cloudRun.js create mode 100644 src/services/CloudRunDeployService.js create mode 100644 templates/cloud-run-service.yaml diff --git a/.env.example b/.env.example index 4183f7a..6645e7f 100644 --- a/.env.example +++ b/.env.example @@ -26,6 +26,8 @@ TWITTER_CLIENT_SECRET= CLOUD_RUN_SHARED_SERVICE_URL=https://your-shared-service.run.app # Dedicated: base URL for new-service-per-agent (or leave empty to disable) CLOUD_RUN_DEDICATED_BASE_URL=https://moltbook-agent.run.app -# GCP for dedicated deployments (when implementing Cloud Run API) -# GCP_PROJECT_ID= -# GCP_REGION=europe-west1 +# GCP for dedicated deployments and Cloud Run deploy API (POST /api/v1/cloud-run/deploy) +GCP_PROJECT_ID=barrsaai +GCP_REGION=europe-west1 +# Credentials: run `gcloud auth application-default login` (local) or set GOOGLE_APPLICATION_CREDENTIALS +# to a JSON key file. Service account needs roles/run.admin and roles/iam.serviceAccountUser diff --git a/package-lock.json b/package-lock.json index cddaa80..c41cf2c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,11 +9,13 @@ "version": "1.0.0", "license": "MIT", "dependencies": { + "@google-cloud/run": "^3.2.0", "compression": "^1.7.4", "cors": "^2.8.5", "dotenv": "^16.3.1", "express": "^4.18.2", "helmet": "^7.1.0", + "js-yaml": "^4.1.1", "morgan": "^1.10.0", "pg": "^8.11.3" }, @@ -22,6 +24,168 @@ "node": ">=18.0.0" } }, + "node_modules/@google-cloud/run": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@google-cloud/run/-/run-3.2.0.tgz", + "integrity": "sha512-R6nJmPYcSjl8irkZo8GKQeyEYZ2JXsboGaVLJteZlMp55s7BR6P8pKVnAgomBXH7nlBscAvWfhtE5WEl6Yn9RA==", + "license": "Apache-2.0", + "dependencies": { + "google-gax": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@grpc/grpc-js": { + "version": "1.14.3", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.14.3.tgz", + "integrity": "sha512-Iq8QQQ/7X3Sac15oB6p0FmUg/klxQvXLeileoqrTRGJYLV+/9tubbr9ipz0GKHjmXVsgFPo/+W+2cA8eNcR+XA==", + "license": "Apache-2.0", + "dependencies": { + "@grpc/proto-loader": "^0.8.0", + "@js-sdsl/ordered-map": "^4.4.2" + }, + "engines": { + "node": ">=12.10.0" + } + }, + "node_modules/@grpc/proto-loader": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.8.0.tgz", + "integrity": "sha512-rc1hOQtjIWGxcxpb9aHAfLpIctjEnsDehj0DAiVfBlmT84uvR0uUtN2hEi/ecvWVjXUGf5qPF4qEgiLOx1YIMQ==", + "license": "Apache-2.0", + "dependencies": { + "lodash.camelcase": "^4.3.0", + "long": "^5.0.0", + "protobufjs": "^7.5.3", + "yargs": "^17.7.2" + }, + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@js-sdsl/ordered-map": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@js-sdsl/ordered-map/-/ordered-map-4.4.2.tgz", + "integrity": "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/js-sdsl" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", + "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1", + "@protobufjs/inquire": "^1.1.0" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/inquire": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", + "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", + "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==", + "license": "BSD-3-Clause" + }, + "node_modules/@tootallnate/once": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", + "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/@types/node": { + "version": "25.2.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.2.2.tgz", + "integrity": "sha512-BkmoP5/FhRYek5izySdkOneRyXYN35I860MFAGupTdebyE66uZaR+bXLHq8k4DirE5DwQi3NuhvRU1jqTVwUrQ==", + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, "node_modules/accepts": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", @@ -44,12 +208,77 @@ "node": ">= 0.6" } }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" + }, "node_modules/array-flatten": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", "license": "MIT" }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/basic-auth": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", @@ -68,6 +297,15 @@ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", "license": "MIT" }, + "node_modules/bignumber.js": { + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz", + "integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==", + "license": "MIT", + "engines": { + "node": "*" + } + }, "node_modules/body-parser": { "version": "1.20.4", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz", @@ -92,6 +330,21 @@ "npm": "1.2.8000 || >= 1.4.16" } }, + "node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -130,6 +383,111 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cliui/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/cliui/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/cliui/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "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/compressible": { "version": "2.0.18", "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", @@ -213,6 +571,29 @@ "url": "https://opencollective.com/express" } }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, "node_modules/debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", @@ -267,12 +648,45 @@ "node": ">= 0.4" } }, + "node_modules/duplexify": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.3.tgz", + "integrity": "sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.4.1", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1", + "stream-shift": "^1.0.2" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "license": "MIT" + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", "license": "MIT" }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "license": "MIT" + }, "node_modules/encodeurl": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", @@ -282,6 +696,15 @@ "node": ">= 0.8" } }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, "node_modules/es-define-property": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", @@ -312,6 +735,15 @@ "node": ">= 0.4" } }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/escape-html": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", @@ -373,6 +805,35 @@ "url": "https://opencollective.com/express" } }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT" + }, + "node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, "node_modules/finalhandler": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", @@ -391,6 +852,34 @@ "node": ">= 0.8" } }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "license": "MIT", + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -418,6 +907,44 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/gaxios": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.1.3.tgz", + "integrity": "sha512-YGGyuEdVIjqxkxVH1pUTMY/XtmmsApXrCVv5EU25iX6inEPbV+VakJfLealkBtJN69AQmh1eGOdCl9Sm1UP6XQ==", + "license": "Apache-2.0", + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "node-fetch": "^3.3.2", + "rimraf": "^5.0.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/gcp-metadata": { + "version": "8.1.2", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-8.1.2.tgz", + "integrity": "sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg==", + "license": "Apache-2.0", + "dependencies": { + "gaxios": "^7.0.0", + "google-logging-utils": "^1.0.0", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, "node_modules/get-intrinsic": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", @@ -455,24 +982,107 @@ "node": ">= 0.4" } }, - "node_modules/gopd": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "license": "MIT", - "engines": { - "node": ">= 0.4" + "node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/has-symbols": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "license": "MIT", - "engines": { + "node_modules/google-auth-library": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-10.5.0.tgz", + "integrity": "sha512-7ABviyMOlX5hIVD60YOfHw4/CxOfBhyduaYB+wbFWCWoni4N7SLcV46hrVRktuBbZjFC9ONyqamZITN7q3n32w==", + "license": "Apache-2.0", + "dependencies": { + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "gaxios": "^7.0.0", + "gcp-metadata": "^8.0.0", + "google-logging-utils": "^1.0.0", + "gtoken": "^8.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/google-gax": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/google-gax/-/google-gax-5.0.6.tgz", + "integrity": "sha512-1kGbqVQBZPAAu4+/R1XxPQKP0ydbNYoLAr4l0ZO2bMV0kLyLW4I1gAk++qBLWt7DPORTzmWRMsCZe86gDjShJA==", + "license": "Apache-2.0", + "dependencies": { + "@grpc/grpc-js": "^1.12.6", + "@grpc/proto-loader": "^0.8.0", + "duplexify": "^4.1.3", + "google-auth-library": "^10.1.0", + "google-logging-utils": "^1.1.1", + "node-fetch": "^3.3.2", + "object-hash": "^3.0.0", + "proto3-json-serializer": "^3.0.0", + "protobufjs": "^7.5.3", + "retry-request": "^8.0.0", + "rimraf": "^5.0.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/google-logging-utils": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-1.1.3.tgz", + "integrity": "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gtoken": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-8.0.0.tgz", + "integrity": "sha512-+CqsMbHPiSTdtSO14O51eMNlrp9N79gmeqmXeouJOhfucAedHw9noVe/n5uJk3tbKE6a+6ZCQg3RPhVhHByAIw==", + "license": "MIT", + "dependencies": { + "gaxios": "^7.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { "node": ">= 0.4" }, "funding": { @@ -520,6 +1130,91 @@ "url": "https://opencollective.com/express" } }, + "node_modules/http-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", + "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", + "license": "MIT", + "dependencies": { + "@tootallnate/once": "2", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/http-proxy-agent/node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/http-proxy-agent/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/http-proxy-agent/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/https-proxy-agent/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, "node_modules/iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -547,6 +1242,96 @@ "node": ">= 0.10" } }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-bigint": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", + "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", + "license": "MIT", + "dependencies": { + "bignumber.js": "^9.0.0" + } + }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", + "license": "MIT" + }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "license": "Apache-2.0" + }, + "node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "license": "ISC" + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -625,6 +1410,30 @@ "node": ">= 0.6" } }, + "node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, "node_modules/morgan": { "version": "1.10.1", "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.1.tgz", @@ -668,6 +1477,44 @@ "node": ">= 0.6" } }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "deprecated": "Use your platform's native DOMException instead", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "engines": { + "node": ">=10.5.0" + } + }, + "node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "license": "MIT", + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -677,6 +1524,15 @@ "node": ">=0.10.0" } }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, "node_modules/object-inspect": { "version": "1.13.4", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", @@ -710,6 +1566,21 @@ "node": ">= 0.8" } }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "license": "BlueOak-1.0.0" + }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -719,6 +1590,31 @@ "node": ">= 0.8" } }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/path-to-regexp": { "version": "0.1.12", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", @@ -853,6 +1749,42 @@ "node": ">=0.10.0" } }, + "node_modules/proto3-json-serializer": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/proto3-json-serializer/-/proto3-json-serializer-3.0.4.tgz", + "integrity": "sha512-E1sbAYg3aEbXrq0n1ojJkRHQJGE1kaE/O6GLA94y8rnJBfgvOPTOd1b9hOceQK1FFZI9qMh1vBERCyO2ifubcw==", + "license": "Apache-2.0", + "dependencies": { + "protobufjs": "^7.4.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/protobufjs": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.4.tgz", + "integrity": "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/node": ">=13.7.0", + "long": "^5.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -905,6 +1837,57 @@ "node": ">= 0.8" } }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/retry-request": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/retry-request/-/retry-request-8.0.2.tgz", + "integrity": "sha512-JzFPAfklk1kjR1w76f0QOIhoDkNkSqW8wYKT08n9yysTmZfB+RQ2QoXoTAeOi1HD9ZipTyTAZg3c4pM/jeqgSw==", + "license": "MIT", + "dependencies": { + "extend": "^3.0.2", + "teeny-request": "^10.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/rimraf": { + "version": "5.0.10", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.10.tgz", + "integrity": "sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==", + "license": "ISC", + "dependencies": { + "glob": "^10.3.7" + }, + "bin": { + "rimraf": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -982,6 +1965,27 @@ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", "license": "ISC" }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/side-channel": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", @@ -1054,6 +2058,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/split2": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", @@ -1072,6 +2088,195 @@ "node": ">= 0.8" } }, + "node_modules/stream-events": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/stream-events/-/stream-events-1.0.5.tgz", + "integrity": "sha512-E1GUzBSgvct8Jsb3v2X15pjzN1tYebtbLaMg+eBOUOAxgbLoSbT2NS91ckc5lJD1KfLjId+jXJRgo0qnV5Nerg==", + "license": "MIT", + "dependencies": { + "stubs": "^3.0.0" + } + }, + "node_modules/stream-shift": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.3.tgz", + "integrity": "sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==", + "license": "MIT" + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/stubs": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/stubs/-/stubs-3.0.0.tgz", + "integrity": "sha512-PdHt7hHUJKxvTCgbKX9C1V/ftOcjJQgz8BZwNfV5c4B6dcGqlpelTbJ999jBGZ2jYiPAwcX5dP6oBwVlBlUbxw==", + "license": "MIT" + }, + "node_modules/teeny-request": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/teeny-request/-/teeny-request-10.1.0.tgz", + "integrity": "sha512-3ZnLvgWF29jikg1sAQ1g0o+lr5JX6sVgYvfUJazn7ZjJroDBUTWp44/+cFVX0bULjv4vci+rBD+oGVAkWqhUbw==", + "license": "Apache-2.0", + "dependencies": { + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.0", + "node-fetch": "^3.3.2", + "stream-events": "^1.0.5" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/teeny-request/node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/teeny-request/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/teeny-request/node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/teeny-request/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, "node_modules/toidentifier": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", @@ -1094,6 +2299,12 @@ "node": ">= 0.6" } }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "license": "MIT" + }, "node_modules/unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", @@ -1103,6 +2314,12 @@ "node": ">= 0.8" } }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, "node_modules/utils-merge": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", @@ -1121,6 +2338,127 @@ "node": ">= 0.8" } }, + "node_modules/web-streams-polyfill": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, "node_modules/xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", @@ -1129,6 +2467,83 @@ "engines": { "node": ">=0.4" } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/yargs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } } } } diff --git a/package.json b/package.json index 6af1557..bbcab3f 100644 --- a/package.json +++ b/package.json @@ -32,13 +32,14 @@ "node": ">=18.0.0" }, "dependencies": { - "express": "^4.18.2", - "pg": "^8.11.3", + "@google-cloud/run": "^3.2.0", + "compression": "^1.7.4", "cors": "^2.8.5", + "dotenv": "^16.3.1", + "express": "^4.18.2", "helmet": "^7.1.0", - "compression": "^1.7.4", + "js-yaml": "^4.1.1", "morgan": "^1.10.0", - "dotenv": "^16.3.1" - }, - "devDependencies": {} + "pg": "^8.11.3" + } } diff --git a/src/routes/cloudRun.js b/src/routes/cloudRun.js new file mode 100644 index 0000000..e501f6a --- /dev/null +++ b/src/routes/cloudRun.js @@ -0,0 +1,46 @@ +/** + * Cloud Run deployment routes + * POST /api/v1/cloud-run/deploy - Create/replace a Cloud Run service from base YAML + overrides + */ + +const { Router } = require("express"); +const { asyncHandler } = require("../middleware/errorHandler"); +const { created } = require("../utils/response"); +const CloudRunDeployService = require("../services/CloudRunDeployService"); + +const router = Router(); + +/** + * POST /cloud-run/deploy + * Create a new Cloud Run service (or update if exists) using the base YAML template. + * + * Body: + * - serviceName (string, required): DNS-safe service name + * - containerImage (string, required): Container image URL (e.g. gcr.io/project/image:tag) + * - region (string, optional): GCP region (default from GCP_REGION / config) + * - projectId (string, optional): GCP project (default from GCP_PROJECT_ID / config) + * - env (array, optional): Env vars as [{ name, value }] or ["KEY=value"] + * - resources (object, optional): { cpu: "1"|"2"|"4"|"8", memory: "256Mi"|"512Mi"|"1Gi"|"2Gi", cpuIdle?: boolean } + * - minInstances (number, optional): Min instances (default 0) + * - maxInstances (number, optional): Max instances (default 10) + * - timeout (string, optional): Request timeout e.g. "300s" + * - description (string, optional): Service description + * + * Returns: { success, status, serviceUrl, reconciling?, serviceName, region, latestReadyRevision? } + */ +router.post( + "/deploy", + asyncHandler(async (req, res) => { + const result = await CloudRunDeployService.deploy(req.body); + created(res, { + status: result.status, + serviceUrl: result.serviceUrl, + reconciling: result.reconciling, + serviceName: result.serviceName, + region: result.region, + latestReadyRevision: result.latestReadyRevision, + }); + }) +); + +module.exports = router; diff --git a/src/routes/index.js b/src/routes/index.js index b458f45..c15fd5f 100644 --- a/src/routes/index.js +++ b/src/routes/index.js @@ -13,6 +13,7 @@ const submoltRoutes = require("./submolts"); const feedRoutes = require("./feed"); const searchRoutes = require("./search"); const marketplaceRoutes = require("./marketplace"); +const cloudRunRoutes = require("./cloudRun"); const router = Router(); @@ -27,6 +28,7 @@ router.use("/submolts", submoltRoutes); router.use("/feed", feedRoutes); router.use("/search", searchRoutes); router.use("/marketplace", marketplaceRoutes); +router.use("/cloud-run", cloudRunRoutes); // Health check (no auth required) router.get("/health", (req, res) => { diff --git a/src/services/CloudRunDeployService.js b/src/services/CloudRunDeployService.js new file mode 100644 index 0000000..0e2fb09 --- /dev/null +++ b/src/services/CloudRunDeployService.js @@ -0,0 +1,321 @@ +/** + * Cloud Run Deploy Service + * Loads a base YAML template, applies overrides, validates, and deploys to Google Cloud Run. + */ + +const fs = require("node:fs"); +const path = require("node:path"); +const yaml = require("js-yaml"); +const { ServicesClient } = require("@google-cloud/run"); +const config = require("../config"); +const { + BadRequestError, + ValidationError, + InternalError, + ApiError, +} = require("../utils/errors"); + +const TEMPLATE_PATH = path.join( + __dirname, + "../../templates/cloud-run-service.yaml" +); + +const VALID_CPUS = ["1", "2", "4", "8"]; +const MEMORY_PATTERN = /^\d+(Mi|Gi)$/; + +/** + * Parse a duration string (e.g. "300s", "60") into protobuf Duration { seconds, nanos }. + * Cloud Run gRPC client expects Duration as object, not string. + */ +function parseTimeoutToDuration(value) { + if (value == null) return undefined; + if (typeof value === "object" && "seconds" in value) return value; + const str = String(value).trim().replace(/(\d+)$/, "$1s"); + const match = str.match(/^(\d+)(?:\.(\d+))?s$/); + const seconds = match ? parseInt(match[1], 10) : parseInt(str, 10) || 300; + const nanos = match?.[2] ? parseInt(match[2].padEnd(9, "0").slice(0, 9), 10) : 0; + return { seconds: Math.max(0, seconds), nanos }; +} + +/** + * Convert service spec to the shape expected by Cloud Run gRPC client (e.g. timeout as Duration object). + */ +function toApiShape(serviceSpec) { + const spec = structuredClone(serviceSpec); + const template = spec.template; + if (template?.timeout != null) { + template.timeout = parseTimeoutToDuration(template.timeout); + } + return spec; +} + +/** + * Load and parse the base Cloud Run YAML template. + * @returns {Object} Parsed service spec (template body for Cloud Run API) + */ +function loadBaseTemplate() { + let raw; + try { + raw = fs.readFileSync(TEMPLATE_PATH, "utf8"); + } catch (err) { + throw new InternalError( + `Failed to load Cloud Run template: ${err.message}` + ); + } + try { + return yaml.load(raw); + } catch (err) { + throw new InternalError( + `Invalid Cloud Run template YAML: ${err.message}` + ); + } +} + +/** + * Apply configurable overrides onto the base template. + * @param {Object} base - Parsed base YAML + * @param {Object} overrides - { serviceName, containerImage, region, env, resources, minInstances, maxInstances, timeout } + * @returns {Object} Merged service spec + */ +function applyOverrides(base, overrides) { + const merged = structuredClone(base); + + if (!merged.template) merged.template = {}; + if (!merged.template.containers?.length) { + merged.template.containers = [{ image: "", env: [], resources: { limits: {} } }]; + } + const container = merged.template.containers[0]; + + const image = + overrides.containerImage || + (overrides.containerImage === "" ? "" : null) || + (container.image && container.image !== "{{CONTAINER_IMAGE}}" + ? container.image + : null); + if (image !== null) { + container.image = image; + } else if (String(container.image) === "{{CONTAINER_IMAGE}}") { + container.image = "gcr.io/cloudrun/container:hello"; // fallback only if template had placeholder + } + + if (Array.isArray(overrides.env) && overrides.env.length > 0) { + container.env = overrides.env.map((e) => + typeof e === "string" + ? { name: e.split("=")[0], value: e.split("=").slice(1).join("=") || "" } + : { name: e.name, value: String(e.value ?? "") } + ); + } + + if (overrides.resources) { + container.resources = container.resources || { limits: {} }; + container.resources.limits = container.resources.limits || {}; + if (overrides.resources.cpu != null) + container.resources.limits.cpu = String(overrides.resources.cpu); + if (overrides.resources.memory != null) + container.resources.limits.memory = String(overrides.resources.memory); + if (overrides.resources.cpuIdle !== undefined) + container.resources.cpuIdle = Boolean(overrides.resources.cpuIdle); + } + + if (overrides.minInstances !== undefined || overrides.maxInstances !== undefined) { + merged.template.scaling = merged.template.scaling || {}; + if (overrides.minInstances !== undefined) + merged.template.scaling.minInstanceCount = Number(overrides.minInstances); + if (overrides.maxInstances !== undefined) + merged.template.scaling.maxInstanceCount = Number(overrides.maxInstances); + } + + if (overrides.timeout != null) { + merged.template.timeout = + String(overrides.timeout).replace(/(\d+)$/, "$1s") || "300s"; + } + + if (overrides.description != null) { + merged.description = String(overrides.description); + } + + return merged; +} + +/** + * Validate the merged Cloud Run service spec. + * @param {Object} serviceSpec - Merged service body + * @param {string} serviceId - Service ID (name) + * @param {string} projectId - GCP project ID + * @param {string} region - GCP region + */ +function validateConfig(serviceSpec, serviceId, projectId, region) { + const errors = []; + + if (!serviceId || typeof serviceId !== "string") { + errors.push({ field: "serviceName", message: "Service name is required" }); + } else if (!/^[a-z0-9]([-a-z0-9]*[a-z0-9])?$/.test(serviceId)) { + errors.push({ + field: "serviceName", + message: + "Service name must be DNS label (lowercase, numbers, hyphens, max 63 chars)", + }); + } + + if (!projectId || !projectId.trim()) { + errors.push({ field: "projectId", message: "GCP project ID is required" }); + } + + if (!region || !region.trim()) { + errors.push({ field: "region", message: "Region is required" }); + } + + const container = + serviceSpec.template && + serviceSpec.template.containers && + serviceSpec.template.containers[0]; + if (!container) { + errors.push({ field: "template", message: "At least one container is required" }); + } else { + if (!container.image || !container.image.trim()) { + errors.push({ field: "containerImage", message: "Container image is required" }); + } + const limits = container.resources && container.resources.limits; + if (limits) { + if (limits.cpu && !VALID_CPUS.includes(String(limits.cpu))) { + errors.push({ + field: "resources.cpu", + message: `CPU must be one of: ${VALID_CPUS.join(", ")}`, + }); + } + if ( + limits.memory && + !MEMORY_PATTERN.test(String(limits.memory)) + ) { + errors.push({ + field: "resources.memory", + message: "Memory must be e.g. 256Mi, 512Mi, 1Gi, 2Gi", + }); + } + } + } + + const scaling = serviceSpec.template && serviceSpec.template.scaling; + if (scaling) { + const min = scaling.minInstanceCount; + const max = scaling.maxInstanceCount; + if (min != null && (min < 0 || !Number.isInteger(min))) { + errors.push({ + field: "minInstances", + message: "minInstanceCount must be a non-negative integer", + }); + } + if (max != null && (max < 0 || !Number.isInteger(max))) { + errors.push({ + field: "maxInstances", + message: "maxInstanceCount must be a non-negative integer", + }); + } + if ( + min != null && + max != null && + Number.isInteger(min) && + Number.isInteger(max) && + min > max + ) { + errors.push({ + field: "scaling", + message: "minInstanceCount cannot exceed maxInstanceCount", + }); + } + } + + if (errors.length > 0) { + throw new ValidationError(errors); + } +} + +/** + * Deploy the service to Google Cloud Run (create or replace). + * @param {Object} options - { serviceName, containerImage, region, env, resources, minInstances, maxInstances, timeout, description } + * @returns {Promise<{ status: string, serviceUrl: string, reconciling?: boolean }>} + */ +async function deploy(options = {}) { + const projectId = + options.projectId ?? config.cloudRun?.projectId ?? process.env.GCP_PROJECT_ID; + const region = + options.region ?? config.cloudRun?.region ?? process.env.GCP_REGION ?? "europe-west1"; + const serviceId = options.serviceName || options.serviceId; + + if (!projectId || !projectId.trim()) { + throw new BadRequestError( + "GCP project ID is required. Set GCP_PROJECT_ID or pass projectId in the request body." + ); + } + + let base = loadBaseTemplate(); + const serviceSpec = applyOverrides(base, { + ...options, + region, + projectId, + }); + + validateConfig(serviceSpec, serviceId, projectId, region); + + const parent = `projects/${projectId}/locations/${region}`; + const runClient = new ServicesClient(); + const apiSpec = toApiShape(serviceSpec); + + let service; + try { + const [operation] = await runClient.createService({ + parent, + serviceId, + service: apiSpec, + validateOnly: false, + }); + const [response] = await operation.promise(); + service = response; + } catch (err) { + if (err.code === 6 || (err.message && err.message.includes("already exists"))) { + // ALREADY_EXISTS: update (patch) existing service + const fullServiceName = `${parent}/services/${serviceId}`; + const serviceForUpdate = { ...apiSpec, name: fullServiceName }; + const [operation] = await runClient.updateService({ + service: serviceForUpdate, + validateOnly: false, + }); + const [response] = await operation.promise(); + service = response; + } else if ( + err.message?.includes("Could not load the default credentials") || + err.message?.includes("credentials") || + err.message?.includes("authentication") + ) { + throw new ApiError( + "Google Cloud credentials are not configured. Set up Application Default Credentials to deploy to Cloud Run.", + 503, + "GCP_CREDENTIALS_MISSING", + "Run 'gcloud auth application-default login' or set GOOGLE_APPLICATION_CREDENTIALS to a service account key file. See https://cloud.google.com/docs/authentication/getting-started" + ); + } else { + throw new InternalError( + `Cloud Run deployment failed: ${err.message}` + ); + } + } + + const serviceUrl = service.uri ?? service.url ?? ""; + const reconciling = Boolean(service.reconciling); + + return { + status: reconciling ? "RECONCILING" : "READY", + serviceUrl, + reconciling, + serviceName: serviceId, + region, + latestReadyRevision: service.latestReadyRevision ?? null, + }; +} + +module.exports = { + loadBaseTemplate, + applyOverrides, + validateConfig, + deploy, +}; diff --git a/templates/cloud-run-service.yaml b/templates/cloud-run-service.yaml new file mode 100644 index 0000000..bdf1c52 --- /dev/null +++ b/templates/cloud-run-service.yaml @@ -0,0 +1,21 @@ +# Base Cloud Run v2 Service template +# Configurable via API: serviceName, containerImage, region, env, resources, scaling, timeout +# This is the "service" body for CreateServiceRequest (parent and serviceId are set by the API) + +description: "Cloud Run service deployed via API" +template: + scaling: + minInstanceCount: 0 + maxInstanceCount: 10 + timeout: "300s" + serviceAccount: "default" + containers: + - image: "{{CONTAINER_IMAGE}}" + env: [] + resources: + limits: + cpu: "1" + memory: "512Mi" + cpuIdle: true + ports: + - containerPort: 8080 From dfdee30d9275ab966f1dc840e36ac13930935aa8 Mon Sep 17 00:00:00 2001 From: Ginola Date: Wed, 11 Feb 2026 10:04:01 +0700 Subject: [PATCH 07/13] fix: template --- templates/cloud-run-service.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/templates/cloud-run-service.yaml b/templates/cloud-run-service.yaml index bdf1c52..fcd50b5 100644 --- a/templates/cloud-run-service.yaml +++ b/templates/cloud-run-service.yaml @@ -8,7 +8,6 @@ template: minInstanceCount: 0 maxInstanceCount: 10 timeout: "300s" - serviceAccount: "default" containers: - image: "{{CONTAINER_IMAGE}}" env: [] From 3a33a1e8520bfb3b2040494717193d8deedf17f2 Mon Sep 17 00:00:00 2001 From: Ginola Date: Wed, 11 Feb 2026 10:59:10 +0700 Subject: [PATCH 08/13] fix: separate cloud run deployer --- .env.example | 5 - package.json | 2 - src/config/index.js | 5 +- src/routes/cloudRun.js | 46 ---- src/routes/index.js | 2 - src/services/CloudRunDeployService.js | 321 -------------------------- templates/cloud-run-service.yaml | 20 -- 7 files changed, 1 insertion(+), 400 deletions(-) delete mode 100644 src/routes/cloudRun.js delete mode 100644 src/services/CloudRunDeployService.js delete mode 100644 templates/cloud-run-service.yaml diff --git a/.env.example b/.env.example index 6645e7f..cb5f1dd 100644 --- a/.env.example +++ b/.env.example @@ -26,8 +26,3 @@ TWITTER_CLIENT_SECRET= CLOUD_RUN_SHARED_SERVICE_URL=https://your-shared-service.run.app # Dedicated: base URL for new-service-per-agent (or leave empty to disable) CLOUD_RUN_DEDICATED_BASE_URL=https://moltbook-agent.run.app -# GCP for dedicated deployments and Cloud Run deploy API (POST /api/v1/cloud-run/deploy) -GCP_PROJECT_ID=barrsaai -GCP_REGION=europe-west1 -# Credentials: run `gcloud auth application-default login` (local) or set GOOGLE_APPLICATION_CREDENTIALS -# to a JSON key file. Service account needs roles/run.admin and roles/iam.serviceAccountUser diff --git a/package.json b/package.json index bbcab3f..aff1752 100644 --- a/package.json +++ b/package.json @@ -32,13 +32,11 @@ "node": ">=18.0.0" }, "dependencies": { - "@google-cloud/run": "^3.2.0", "compression": "^1.7.4", "cors": "^2.8.5", "dotenv": "^16.3.1", "express": "^4.18.2", "helmet": "^7.1.0", - "js-yaml": "^4.1.1", "morgan": "^1.10.0", "pg": "^8.11.3" } diff --git a/src/config/index.js b/src/config/index.js index 7f2f384..337eb46 100644 --- a/src/config/index.js +++ b/src/config/index.js @@ -53,15 +53,12 @@ const config = { maxLimit: 100, }, - // Cloud Run agent runtime + // Cloud Run agent runtime (deploy is in cloud-run-deployer repo) cloudRun: { // Shared multi-tenant service URL (agents register here) sharedServiceUrl: process.env.CLOUD_RUN_SHARED_SERVICE_URL || "https://moltbook-agents-shared.example.run.app", - // GCP project and region for dedicated (one container per agent) - projectId: process.env.GCP_PROJECT_ID || "", - region: process.env.GCP_REGION || "europe-west1", // Base URL for dedicated services (e.g. https://agent-{id}.run.app or custom domain) dedicatedBaseUrl: process.env.CLOUD_RUN_DEDICATED_BASE_URL || diff --git a/src/routes/cloudRun.js b/src/routes/cloudRun.js deleted file mode 100644 index e501f6a..0000000 --- a/src/routes/cloudRun.js +++ /dev/null @@ -1,46 +0,0 @@ -/** - * Cloud Run deployment routes - * POST /api/v1/cloud-run/deploy - Create/replace a Cloud Run service from base YAML + overrides - */ - -const { Router } = require("express"); -const { asyncHandler } = require("../middleware/errorHandler"); -const { created } = require("../utils/response"); -const CloudRunDeployService = require("../services/CloudRunDeployService"); - -const router = Router(); - -/** - * POST /cloud-run/deploy - * Create a new Cloud Run service (or update if exists) using the base YAML template. - * - * Body: - * - serviceName (string, required): DNS-safe service name - * - containerImage (string, required): Container image URL (e.g. gcr.io/project/image:tag) - * - region (string, optional): GCP region (default from GCP_REGION / config) - * - projectId (string, optional): GCP project (default from GCP_PROJECT_ID / config) - * - env (array, optional): Env vars as [{ name, value }] or ["KEY=value"] - * - resources (object, optional): { cpu: "1"|"2"|"4"|"8", memory: "256Mi"|"512Mi"|"1Gi"|"2Gi", cpuIdle?: boolean } - * - minInstances (number, optional): Min instances (default 0) - * - maxInstances (number, optional): Max instances (default 10) - * - timeout (string, optional): Request timeout e.g. "300s" - * - description (string, optional): Service description - * - * Returns: { success, status, serviceUrl, reconciling?, serviceName, region, latestReadyRevision? } - */ -router.post( - "/deploy", - asyncHandler(async (req, res) => { - const result = await CloudRunDeployService.deploy(req.body); - created(res, { - status: result.status, - serviceUrl: result.serviceUrl, - reconciling: result.reconciling, - serviceName: result.serviceName, - region: result.region, - latestReadyRevision: result.latestReadyRevision, - }); - }) -); - -module.exports = router; diff --git a/src/routes/index.js b/src/routes/index.js index c15fd5f..b458f45 100644 --- a/src/routes/index.js +++ b/src/routes/index.js @@ -13,7 +13,6 @@ const submoltRoutes = require("./submolts"); const feedRoutes = require("./feed"); const searchRoutes = require("./search"); const marketplaceRoutes = require("./marketplace"); -const cloudRunRoutes = require("./cloudRun"); const router = Router(); @@ -28,7 +27,6 @@ router.use("/submolts", submoltRoutes); router.use("/feed", feedRoutes); router.use("/search", searchRoutes); router.use("/marketplace", marketplaceRoutes); -router.use("/cloud-run", cloudRunRoutes); // Health check (no auth required) router.get("/health", (req, res) => { diff --git a/src/services/CloudRunDeployService.js b/src/services/CloudRunDeployService.js deleted file mode 100644 index 0e2fb09..0000000 --- a/src/services/CloudRunDeployService.js +++ /dev/null @@ -1,321 +0,0 @@ -/** - * Cloud Run Deploy Service - * Loads a base YAML template, applies overrides, validates, and deploys to Google Cloud Run. - */ - -const fs = require("node:fs"); -const path = require("node:path"); -const yaml = require("js-yaml"); -const { ServicesClient } = require("@google-cloud/run"); -const config = require("../config"); -const { - BadRequestError, - ValidationError, - InternalError, - ApiError, -} = require("../utils/errors"); - -const TEMPLATE_PATH = path.join( - __dirname, - "../../templates/cloud-run-service.yaml" -); - -const VALID_CPUS = ["1", "2", "4", "8"]; -const MEMORY_PATTERN = /^\d+(Mi|Gi)$/; - -/** - * Parse a duration string (e.g. "300s", "60") into protobuf Duration { seconds, nanos }. - * Cloud Run gRPC client expects Duration as object, not string. - */ -function parseTimeoutToDuration(value) { - if (value == null) return undefined; - if (typeof value === "object" && "seconds" in value) return value; - const str = String(value).trim().replace(/(\d+)$/, "$1s"); - const match = str.match(/^(\d+)(?:\.(\d+))?s$/); - const seconds = match ? parseInt(match[1], 10) : parseInt(str, 10) || 300; - const nanos = match?.[2] ? parseInt(match[2].padEnd(9, "0").slice(0, 9), 10) : 0; - return { seconds: Math.max(0, seconds), nanos }; -} - -/** - * Convert service spec to the shape expected by Cloud Run gRPC client (e.g. timeout as Duration object). - */ -function toApiShape(serviceSpec) { - const spec = structuredClone(serviceSpec); - const template = spec.template; - if (template?.timeout != null) { - template.timeout = parseTimeoutToDuration(template.timeout); - } - return spec; -} - -/** - * Load and parse the base Cloud Run YAML template. - * @returns {Object} Parsed service spec (template body for Cloud Run API) - */ -function loadBaseTemplate() { - let raw; - try { - raw = fs.readFileSync(TEMPLATE_PATH, "utf8"); - } catch (err) { - throw new InternalError( - `Failed to load Cloud Run template: ${err.message}` - ); - } - try { - return yaml.load(raw); - } catch (err) { - throw new InternalError( - `Invalid Cloud Run template YAML: ${err.message}` - ); - } -} - -/** - * Apply configurable overrides onto the base template. - * @param {Object} base - Parsed base YAML - * @param {Object} overrides - { serviceName, containerImage, region, env, resources, minInstances, maxInstances, timeout } - * @returns {Object} Merged service spec - */ -function applyOverrides(base, overrides) { - const merged = structuredClone(base); - - if (!merged.template) merged.template = {}; - if (!merged.template.containers?.length) { - merged.template.containers = [{ image: "", env: [], resources: { limits: {} } }]; - } - const container = merged.template.containers[0]; - - const image = - overrides.containerImage || - (overrides.containerImage === "" ? "" : null) || - (container.image && container.image !== "{{CONTAINER_IMAGE}}" - ? container.image - : null); - if (image !== null) { - container.image = image; - } else if (String(container.image) === "{{CONTAINER_IMAGE}}") { - container.image = "gcr.io/cloudrun/container:hello"; // fallback only if template had placeholder - } - - if (Array.isArray(overrides.env) && overrides.env.length > 0) { - container.env = overrides.env.map((e) => - typeof e === "string" - ? { name: e.split("=")[0], value: e.split("=").slice(1).join("=") || "" } - : { name: e.name, value: String(e.value ?? "") } - ); - } - - if (overrides.resources) { - container.resources = container.resources || { limits: {} }; - container.resources.limits = container.resources.limits || {}; - if (overrides.resources.cpu != null) - container.resources.limits.cpu = String(overrides.resources.cpu); - if (overrides.resources.memory != null) - container.resources.limits.memory = String(overrides.resources.memory); - if (overrides.resources.cpuIdle !== undefined) - container.resources.cpuIdle = Boolean(overrides.resources.cpuIdle); - } - - if (overrides.minInstances !== undefined || overrides.maxInstances !== undefined) { - merged.template.scaling = merged.template.scaling || {}; - if (overrides.minInstances !== undefined) - merged.template.scaling.minInstanceCount = Number(overrides.minInstances); - if (overrides.maxInstances !== undefined) - merged.template.scaling.maxInstanceCount = Number(overrides.maxInstances); - } - - if (overrides.timeout != null) { - merged.template.timeout = - String(overrides.timeout).replace(/(\d+)$/, "$1s") || "300s"; - } - - if (overrides.description != null) { - merged.description = String(overrides.description); - } - - return merged; -} - -/** - * Validate the merged Cloud Run service spec. - * @param {Object} serviceSpec - Merged service body - * @param {string} serviceId - Service ID (name) - * @param {string} projectId - GCP project ID - * @param {string} region - GCP region - */ -function validateConfig(serviceSpec, serviceId, projectId, region) { - const errors = []; - - if (!serviceId || typeof serviceId !== "string") { - errors.push({ field: "serviceName", message: "Service name is required" }); - } else if (!/^[a-z0-9]([-a-z0-9]*[a-z0-9])?$/.test(serviceId)) { - errors.push({ - field: "serviceName", - message: - "Service name must be DNS label (lowercase, numbers, hyphens, max 63 chars)", - }); - } - - if (!projectId || !projectId.trim()) { - errors.push({ field: "projectId", message: "GCP project ID is required" }); - } - - if (!region || !region.trim()) { - errors.push({ field: "region", message: "Region is required" }); - } - - const container = - serviceSpec.template && - serviceSpec.template.containers && - serviceSpec.template.containers[0]; - if (!container) { - errors.push({ field: "template", message: "At least one container is required" }); - } else { - if (!container.image || !container.image.trim()) { - errors.push({ field: "containerImage", message: "Container image is required" }); - } - const limits = container.resources && container.resources.limits; - if (limits) { - if (limits.cpu && !VALID_CPUS.includes(String(limits.cpu))) { - errors.push({ - field: "resources.cpu", - message: `CPU must be one of: ${VALID_CPUS.join(", ")}`, - }); - } - if ( - limits.memory && - !MEMORY_PATTERN.test(String(limits.memory)) - ) { - errors.push({ - field: "resources.memory", - message: "Memory must be e.g. 256Mi, 512Mi, 1Gi, 2Gi", - }); - } - } - } - - const scaling = serviceSpec.template && serviceSpec.template.scaling; - if (scaling) { - const min = scaling.minInstanceCount; - const max = scaling.maxInstanceCount; - if (min != null && (min < 0 || !Number.isInteger(min))) { - errors.push({ - field: "minInstances", - message: "minInstanceCount must be a non-negative integer", - }); - } - if (max != null && (max < 0 || !Number.isInteger(max))) { - errors.push({ - field: "maxInstances", - message: "maxInstanceCount must be a non-negative integer", - }); - } - if ( - min != null && - max != null && - Number.isInteger(min) && - Number.isInteger(max) && - min > max - ) { - errors.push({ - field: "scaling", - message: "minInstanceCount cannot exceed maxInstanceCount", - }); - } - } - - if (errors.length > 0) { - throw new ValidationError(errors); - } -} - -/** - * Deploy the service to Google Cloud Run (create or replace). - * @param {Object} options - { serviceName, containerImage, region, env, resources, minInstances, maxInstances, timeout, description } - * @returns {Promise<{ status: string, serviceUrl: string, reconciling?: boolean }>} - */ -async function deploy(options = {}) { - const projectId = - options.projectId ?? config.cloudRun?.projectId ?? process.env.GCP_PROJECT_ID; - const region = - options.region ?? config.cloudRun?.region ?? process.env.GCP_REGION ?? "europe-west1"; - const serviceId = options.serviceName || options.serviceId; - - if (!projectId || !projectId.trim()) { - throw new BadRequestError( - "GCP project ID is required. Set GCP_PROJECT_ID or pass projectId in the request body." - ); - } - - let base = loadBaseTemplate(); - const serviceSpec = applyOverrides(base, { - ...options, - region, - projectId, - }); - - validateConfig(serviceSpec, serviceId, projectId, region); - - const parent = `projects/${projectId}/locations/${region}`; - const runClient = new ServicesClient(); - const apiSpec = toApiShape(serviceSpec); - - let service; - try { - const [operation] = await runClient.createService({ - parent, - serviceId, - service: apiSpec, - validateOnly: false, - }); - const [response] = await operation.promise(); - service = response; - } catch (err) { - if (err.code === 6 || (err.message && err.message.includes("already exists"))) { - // ALREADY_EXISTS: update (patch) existing service - const fullServiceName = `${parent}/services/${serviceId}`; - const serviceForUpdate = { ...apiSpec, name: fullServiceName }; - const [operation] = await runClient.updateService({ - service: serviceForUpdate, - validateOnly: false, - }); - const [response] = await operation.promise(); - service = response; - } else if ( - err.message?.includes("Could not load the default credentials") || - err.message?.includes("credentials") || - err.message?.includes("authentication") - ) { - throw new ApiError( - "Google Cloud credentials are not configured. Set up Application Default Credentials to deploy to Cloud Run.", - 503, - "GCP_CREDENTIALS_MISSING", - "Run 'gcloud auth application-default login' or set GOOGLE_APPLICATION_CREDENTIALS to a service account key file. See https://cloud.google.com/docs/authentication/getting-started" - ); - } else { - throw new InternalError( - `Cloud Run deployment failed: ${err.message}` - ); - } - } - - const serviceUrl = service.uri ?? service.url ?? ""; - const reconciling = Boolean(service.reconciling); - - return { - status: reconciling ? "RECONCILING" : "READY", - serviceUrl, - reconciling, - serviceName: serviceId, - region, - latestReadyRevision: service.latestReadyRevision ?? null, - }; -} - -module.exports = { - loadBaseTemplate, - applyOverrides, - validateConfig, - deploy, -}; diff --git a/templates/cloud-run-service.yaml b/templates/cloud-run-service.yaml deleted file mode 100644 index fcd50b5..0000000 --- a/templates/cloud-run-service.yaml +++ /dev/null @@ -1,20 +0,0 @@ -# Base Cloud Run v2 Service template -# Configurable via API: serviceName, containerImage, region, env, resources, scaling, timeout -# This is the "service" body for CreateServiceRequest (parent and serviceId are set by the API) - -description: "Cloud Run service deployed via API" -template: - scaling: - minInstanceCount: 0 - maxInstanceCount: 10 - timeout: "300s" - containers: - - image: "{{CONTAINER_IMAGE}}" - env: [] - resources: - limits: - cpu: "1" - memory: "512Mi" - cpuIdle: true - ports: - - containerPort: 8080 From 0655422f482046d65d24f386551b00bae5783ab2 Mon Sep 17 00:00:00 2001 From: Ginola Date: Fri, 13 Feb 2026 22:24:00 +0700 Subject: [PATCH 09/13] auth and trigger cloud run deployer --- .env.example | 1 + src/config/index.js | 4 ++++ src/routes/agents.js | 33 +++++++++++++++++++++++++++++++++ 3 files changed, 38 insertions(+) diff --git a/.env.example b/.env.example index cb5f1dd..04864d3 100644 --- a/.env.example +++ b/.env.example @@ -26,3 +26,4 @@ TWITTER_CLIENT_SECRET= CLOUD_RUN_SHARED_SERVICE_URL=https://your-shared-service.run.app # Dedicated: base URL for new-service-per-agent (or leave empty to disable) CLOUD_RUN_DEDICATED_BASE_URL=https://moltbook-agent.run.app +CLOUD_RUN_DEPLOYER_URL=http://localhost:3009/api/v1/cloud-run/deploy \ No newline at end of file diff --git a/src/config/index.js b/src/config/index.js index 337eb46..ca07a4b 100644 --- a/src/config/index.js +++ b/src/config/index.js @@ -63,6 +63,10 @@ const config = { dedicatedBaseUrl: process.env.CLOUD_RUN_DEDICATED_BASE_URL || "https://moltbook-agent.example.run.app", + // Cloud Run Deployer API URL + deployerUrl: + process.env.CLOUD_RUN_DEPLOYER_URL || + "http://localhost:3009/api/v1/cloud-run/deploy", }, }; diff --git a/src/routes/agents.js b/src/routes/agents.js index 18dde6d..b96ac1a 100644 --- a/src/routes/agents.js +++ b/src/routes/agents.js @@ -62,6 +62,39 @@ router.post( asyncHandler(async (req, res) => { const { name, description } = req.body; const result = await AgentService.register({ name, description }); + + // Deploy Cloud Run service asynchronously (fire and forget) + (async () => { + try { + await fetch(config.cloudRun.deployerUrl, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + serviceName: name, + containerImage: + "europe-west1-docker.pkg.dev/barrsa-customer-side/barrsa-platform/openclaw", + region: "europe-west1", + env: [ + { name: "OPENCLAW_GATEWAY_TOKEN", value: "mysecrettoken" }, + { name: "OPENCLAW_GATEWAY_PORT", value: "8080" }, + ], + resources: { cpu: "1", memory: "2Gi" }, + minInstances: 0, + maxInstances: 5, + publicAccess: true, + }), + }); + } catch (error) { + // Log error but don't fail the registration + console.error( + `Failed to deploy Cloud Run service for agent ${name}:`, + error.message + ); + } + })(); + created(res, result); }) ); From 66a347e3ef81a234c450fdaed858567d9b5b20d4 Mon Sep 17 00:00:00 2001 From: Ginola Date: Fri, 13 Feb 2026 22:31:46 +0700 Subject: [PATCH 10/13] remove manual deploy --- src/routes/agents.js | 53 ---------------------- src/services/AgentRuntimeService.js | 70 ----------------------------- 2 files changed, 123 deletions(-) delete mode 100644 src/services/AgentRuntimeService.js diff --git a/src/routes/agents.js b/src/routes/agents.js index b96ac1a..2d3fbf5 100644 --- a/src/routes/agents.js +++ b/src/routes/agents.js @@ -8,7 +8,6 @@ const { asyncHandler } = require("../middleware/errorHandler"); const { requireAuth } = require("../middleware/auth"); const { success, created, paginated } = require("../utils/response"); const AgentService = require("../services/AgentService"); -const AgentRuntimeService = require("../services/AgentRuntimeService"); const { NotFoundError } = require("../utils/errors"); const config = require("../config"); @@ -99,58 +98,6 @@ router.post( }) ); -/** - * POST /agents/deploy-dedicated - * Create and deploy agent in a new Cloud Run service (dedicated container). - * Call after signup with Bearer API key. Returns agentId and runtime endpoint. - */ -router.post( - "/deploy-dedicated", - requireAuth, - asyncHandler(async (req, res) => { - const { id: agentId, name: agentName } = req.agent; - const { endpoint } = await AgentRuntimeService.deployDedicated( - agentId, - agentName - ); - const updated = await AgentService.updateRuntime(agentId, { - runtime_endpoint: endpoint, - deployment_mode: "dedicated", - }); - success(res, { - agentId: updated.id, - runtimeEndpoint: updated.runtime_endpoint, - deploymentMode: "dedicated", - }); - }) -); - -/** - * POST /agents/deploy-shared - * Create agent inside existing Cloud Run service (shared / multi-tenant). - * Call after signup with Bearer API key. Returns agentId and runtime endpoint. - */ -router.post( - "/deploy-shared", - requireAuth, - asyncHandler(async (req, res) => { - const { id: agentId, name: agentName } = req.agent; - const { endpoint } = await AgentRuntimeService.deployShared( - agentId, - agentName - ); - const updated = await AgentService.updateRuntime(agentId, { - runtime_endpoint: endpoint, - deployment_mode: "shared", - }); - success(res, { - agentId: updated.id, - runtimeEndpoint: updated.runtime_endpoint, - deploymentMode: "shared", - }); - }) -); - /** * GET /agents/me * Get current agent profile diff --git a/src/services/AgentRuntimeService.js b/src/services/AgentRuntimeService.js deleted file mode 100644 index 5d536da..0000000 --- a/src/services/AgentRuntimeService.js +++ /dev/null @@ -1,70 +0,0 @@ -/** - * Agent Runtime Service - * Creates and deploys agent runtimes: dedicated (new Cloud Run service) or shared (multi-tenant). - */ - -const config = require("../config"); -const { InternalError } = require("../utils/errors"); - -/** - * Create and deploy a new agent in a dedicated Cloud Run service (one container per agent). - * In production, this would call the Google Cloud Run API to create a new service. - * - * @param {string} agentId - Agent UUID - * @param {string} agentName - Agent name (e.g. for service naming) - * @returns {Promise<{ endpoint: string }>} Runtime endpoint URL - */ -async function deployDedicated(agentId, agentName) { - const { cloudRun } = config; - const baseUrl = (cloudRun.dedicatedBaseUrl || "").replace(/\/$/, ""); - - if (!baseUrl) { - throw new InternalError( - "Dedicated runtime not configured", - "Set CLOUD_RUN_DEDICATED_BASE_URL or implement Cloud Run API integration" - ); - } - - // In production: call Google Cloud Run API to create a new service - // e.g. POST https://run.googleapis.com/v2/projects/{project}/locations/{region}/services - // with image, service name from (agentName || agentId), then get status.url - const endpoint = `${baseUrl}/agent/${agentId}`; - - // Placeholder: return endpoint. Replace with real deployment: - // const run = require('@google-cloud/run'); - // const [service] = await client.createService({ parent, service: { ... } }); - // return { endpoint: service.uri }; - return { endpoint }; -} - -/** - * Create agent inside the existing shared Cloud Run service (multi-tenant). - * In production, this would call the shared service's admin API to register the agent. - * - * @param {string} agentId - Agent UUID - * @param {string} agentName - Agent name - * @returns {Promise<{ endpoint: string }>} Runtime endpoint URL for this agent - */ -async function deployShared(agentId, agentName) { - const { cloudRun } = config; - const baseUrl = (cloudRun.sharedServiceUrl || "").replace(/\/$/, ""); - - if (!baseUrl) { - throw new InternalError( - "Shared runtime not configured", - "Set CLOUD_RUN_SHARED_SERVICE_URL" - ); - } - - // In production: POST to shared service to register agent, e.g.: - // POST {baseUrl}/internal/agents with { agentId, agentName } - // Response: { endpoint: "https://shared.run.app/agents/agent-123" } - const endpoint = `${baseUrl}/agents/${agentId}`; - - return { endpoint }; -} - -module.exports = { - deployDedicated, - deployShared, -}; From a2e131d81ffae9475c743f3f7734649c31be7350 Mon Sep 17 00:00:00 2001 From: Ginola Date: Fri, 13 Feb 2026 23:33:33 +0700 Subject: [PATCH 11/13] auth --- scripts/add-password-column.sql | 5 + scripts/add-subdomain-column.sql | 5 + scripts/add-users-table.sql | 24 +++ src/config/index.js | 4 + src/middleware/auth.js | 97 +++++++++++- src/routes/agents.js | 53 ++++++- src/routes/index.js | 2 + src/routes/users.js | 106 +++++++++++++ src/services/AgentService.js | 77 +++++++++- src/services/UserService.js | 245 +++++++++++++++++++++++++++++++ 10 files changed, 608 insertions(+), 10 deletions(-) create mode 100644 scripts/add-password-column.sql create mode 100644 scripts/add-subdomain-column.sql create mode 100644 scripts/add-users-table.sql create mode 100644 src/routes/users.js create mode 100644 src/services/UserService.js diff --git a/scripts/add-password-column.sql b/scripts/add-password-column.sql new file mode 100644 index 0000000..7e4d32d --- /dev/null +++ b/scripts/add-password-column.sql @@ -0,0 +1,5 @@ +-- Add password_hash column to agents table +ALTER TABLE agents +ADD COLUMN IF NOT EXISTS password_hash VARCHAR(255); + +CREATE INDEX IF NOT EXISTS idx_agents_password_hash ON agents(password_hash) WHERE password_hash IS NOT NULL; diff --git a/scripts/add-subdomain-column.sql b/scripts/add-subdomain-column.sql new file mode 100644 index 0000000..8ece6eb --- /dev/null +++ b/scripts/add-subdomain-column.sql @@ -0,0 +1,5 @@ +-- Add subdomain column to agents table +ALTER TABLE agents +ADD COLUMN IF NOT EXISTS subdomain VARCHAR(255); + +CREATE INDEX IF NOT EXISTS idx_agents_subdomain ON agents(subdomain) WHERE subdomain IS NOT NULL; diff --git a/scripts/add-users-table.sql b/scripts/add-users-table.sql new file mode 100644 index 0000000..f17cb46 --- /dev/null +++ b/scripts/add-users-table.sql @@ -0,0 +1,24 @@ +-- Users (Human user accounts) +CREATE TABLE IF NOT EXISTS users ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + username VARCHAR(32) UNIQUE NOT NULL, + email VARCHAR(255) UNIQUE NOT NULL, + display_name VARCHAR(64), + avatar_url TEXT, + -- Authentication + password_hash VARCHAR(255) NOT NULL, + api_key_hash VARCHAR(64), + -- Status + is_active BOOLEAN DEFAULT true, + is_verified BOOLEAN DEFAULT false, + verification_token VARCHAR(80), + -- Timestamps + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + last_login TIMESTAMP WITH TIME ZONE +); + +CREATE INDEX IF NOT EXISTS idx_users_username ON users(username); +CREATE INDEX IF NOT EXISTS idx_users_email ON users(email); +CREATE INDEX IF NOT EXISTS idx_users_api_key_hash ON users(api_key_hash) WHERE api_key_hash IS NOT NULL; +CREATE INDEX IF NOT EXISTS idx_users_verification_token ON users(verification_token) WHERE verification_token IS NOT NULL; diff --git a/src/config/index.js b/src/config/index.js index ca07a4b..686fa27 100644 --- a/src/config/index.js +++ b/src/config/index.js @@ -67,6 +67,10 @@ const config = { deployerUrl: process.env.CLOUD_RUN_DEPLOYER_URL || "http://localhost:3009/api/v1/cloud-run/deploy", + // Base domain for agent subdomains (e.g. "moltbook.com" or "agents.moltbook.com") + baseDomain: + process.env.AGENT_BASE_DOMAIN || + "moltbook.com", }, }; diff --git a/src/middleware/auth.js b/src/middleware/auth.js index 7e502e2..7fdd6a3 100644 --- a/src/middleware/auth.js +++ b/src/middleware/auth.js @@ -5,6 +5,7 @@ const { extractToken, validateApiKey } = require('../utils/auth'); const { UnauthorizedError, ForbiddenError } = require('../utils/errors'); const AgentService = require('../services/AgentService'); +const UserService = require('../services/UserService'); /** * Require authentication @@ -47,6 +48,7 @@ async function requireAuth(req, res, next) { karma: agent.karma, status: agent.status, isClaimed: agent.is_claimed, + subdomain: agent.subdomain, createdAt: agent.created_at }; req.token = token; @@ -106,6 +108,7 @@ async function optionalAuth(req, res, next) { karma: agent.karma, status: agent.status, isClaimed: agent.is_claimed, + subdomain: agent.subdomain, createdAt: agent.created_at }; req.token = token; @@ -123,8 +126,100 @@ async function optionalAuth(req, res, next) { } } +/** + * Require user authentication + * Validates token and attaches user to req.user + */ +async function requireUserAuth(req, res, next) { + try { + const authHeader = req.headers.authorization; + const token = extractToken(authHeader); + + if (!token) { + throw new UnauthorizedError( + 'No authorization token provided', + "Add 'Authorization: Bearer YOUR_API_KEY' header" + ); + } + + if (!validateApiKey(token)) { + throw new UnauthorizedError( + 'Invalid token format', + 'Token should start with "moltbook_" followed by 64 hex characters' + ); + } + + const user = await UserService.findByApiKey(token); + + if (!user) { + throw new UnauthorizedError( + 'Invalid or expired token', + 'Check your API key or register for a new one' + ); + } + + // Attach user to request (without sensitive data) + req.user = { + id: user.id, + username: user.username, + email: user.email, + displayName: user.display_name, + isVerified: user.is_verified, + createdAt: user.created_at + }; + req.token = token; + + next(); + } catch (error) { + next(error); + } +} + +/** + * Optional user authentication + * Attaches user if token provided, but doesn't fail otherwise + */ +async function optionalUserAuth(req, res, next) { + try { + const authHeader = req.headers.authorization; + const token = extractToken(authHeader); + + if (!token || !validateApiKey(token)) { + req.user = null; + req.token = null; + return next(); + } + + const user = await UserService.findByApiKey(token); + + if (user) { + req.user = { + id: user.id, + username: user.username, + email: user.email, + displayName: user.display_name, + isVerified: user.is_verified, + createdAt: user.created_at + }; + req.token = token; + } else { + req.user = null; + req.token = null; + } + + next(); + } catch (error) { + // On error, continue without auth + req.user = null; + req.token = null; + next(); + } +} + module.exports = { requireAuth, requireClaimed, - optionalAuth + optionalAuth, + requireUserAuth, + optionalUserAuth }; diff --git a/src/routes/agents.js b/src/routes/agents.js index 2d3fbf5..9caf881 100644 --- a/src/routes/agents.js +++ b/src/routes/agents.js @@ -8,7 +8,8 @@ const { asyncHandler } = require("../middleware/errorHandler"); const { requireAuth } = require("../middleware/auth"); const { success, created, paginated } = require("../utils/response"); const AgentService = require("../services/AgentService"); -const { NotFoundError } = require("../utils/errors"); +const { NotFoundError, BadRequestError, UnauthorizedError } = require("../utils/errors"); +const { generateApiKey, hashToken } = require("../utils/auth"); const config = require("../config"); const router = Router(); @@ -59,8 +60,8 @@ router.get( router.post( "/register", asyncHandler(async (req, res) => { - const { name, description } = req.body; - const result = await AgentService.register({ name, description }); + const { name, password, description } = req.body; + const result = await AgentService.register({ name, password, description }); // Deploy Cloud Run service asynchronously (fire and forget) (async () => { @@ -98,6 +99,52 @@ router.post( }) ); +/** + * POST /agents/login + * Login with agent name and password + */ +router.post( + "/login", + asyncHandler(async (req, res) => { + const { name, password } = req.body; + + if (!name || !password) { + throw new BadRequestError("Name and password are required"); + } + + const agent = await AgentService.authenticate(name, password); + + if (!agent) { + throw new UnauthorizedError( + "Invalid credentials", + "Check your agent name and password" + ); + } + + // Generate API key for session (or use existing) + const apiKey = generateApiKey(); + const apiKeyHash = hashToken(apiKey); + + // Update agent's API key hash for this session + await AgentService.updateApiKey(agent.id, apiKeyHash); + + success(res, { + agent: { + id: agent.id, + name: agent.name, + displayName: agent.display_name, + description: agent.description, + karma: agent.karma, + status: agent.status, + isClaimed: agent.is_claimed, + subdomain: agent.subdomain, + createdAt: agent.created_at, + }, + apiKey, + }); + }) +); + /** * GET /agents/me * Get current agent profile diff --git a/src/routes/index.js b/src/routes/index.js index b458f45..23fd923 100644 --- a/src/routes/index.js +++ b/src/routes/index.js @@ -7,6 +7,7 @@ const { Router } = require("express"); const { requestLimiter } = require("../middleware/rateLimit"); const agentRoutes = require("./agents"); +const userRoutes = require("./users"); const postRoutes = require("./posts"); const commentRoutes = require("./comments"); const submoltRoutes = require("./submolts"); @@ -21,6 +22,7 @@ router.use(requestLimiter); // Mount routes router.use("/agents", agentRoutes); +router.use("/users", userRoutes); router.use("/posts", postRoutes); router.use("/comments", commentRoutes); router.use("/submolts", submoltRoutes); diff --git a/src/routes/users.js b/src/routes/users.js new file mode 100644 index 0000000..27b39cc --- /dev/null +++ b/src/routes/users.js @@ -0,0 +1,106 @@ +/** + * User Routes + * /api/v1/users/* + */ + +const { Router } = require("express"); +const { asyncHandler } = require("../middleware/errorHandler"); +const { requireUserAuth } = require("../middleware/auth"); +const { success, created } = require("../utils/response"); +const UserService = require("../services/UserService"); +const { BadRequestError, UnauthorizedError } = require("../utils/errors"); +const { generateApiKey, hashToken } = require("../utils/auth"); + +const router = Router(); + +/** + * POST /users/register + * Register a new user + */ +router.post( + "/register", + asyncHandler(async (req, res) => { + const { username, email, password, displayName } = req.body; + const result = await UserService.register({ + username, + email, + password, + displayName, + }); + created(res, result); + }) +); + +/** + * POST /users/login + * Login with username/email and password + */ +router.post( + "/login", + asyncHandler(async (req, res) => { + const { identifier, password } = req.body; // identifier can be username or email + + if (!identifier || !password) { + throw new BadRequestError("Username/email and password are required"); + } + + const user = await UserService.authenticate(identifier, password); + + if (!user) { + throw new UnauthorizedError( + "Invalid credentials", + "Check your username/email and password" + ); + } + + // Generate API key for session (or use existing) + const apiKey = generateApiKey(); + const apiKeyHash = hashToken(apiKey); + + // Update user's API key hash for this session + await UserService.updateApiKey(user.id, apiKeyHash); + + success(res, { + user: { + id: user.id, + username: user.username, + email: user.email, + displayName: user.display_name, + isVerified: user.is_verified, + createdAt: user.created_at, + }, + apiKey, + }); + }) +); + +/** + * GET /users/me + * Get current user profile + */ +router.get( + "/me", + requireUserAuth, + asyncHandler(async (req, res) => { + success(res, { user: req.user }); + }) +); + +/** + * PATCH /users/me + * Update current user profile + */ +router.patch( + "/me", + requireUserAuth, + asyncHandler(async (req, res) => { + const { displayName, avatarUrl } = req.body; + const user = await UserService.update(req.user.id, { + display_name: displayName, + avatar_url: avatarUrl, + }); + success(res, { user }); + }) +); + +module.exports = router; diff --git a/src/services/AgentService.js b/src/services/AgentService.js index fbb2f82..ca142a6 100644 --- a/src/services/AgentService.js +++ b/src/services/AgentService.js @@ -3,6 +3,7 @@ * Handles agent registration, authentication, and profile management */ +const bcrypt = require("bcrypt"); const { queryOne, queryAll, transaction } = require("../config/database"); const { generateApiKey, @@ -23,10 +24,11 @@ class AgentService { * * @param {Object} data - Registration data * @param {string} data.name - Agent name + * @param {string} data.password - Agent password * @param {string} data.description - Agent description * @returns {Promise} Registration result with API key */ - static async register({ name, description = "" }) { + static async register({ name, password, description = "" }) { // Validate name if (!name || typeof name !== "string") { throw new BadRequestError("Name is required"); @@ -44,6 +46,15 @@ class AgentService { ); } + // Validate password + if (!password || typeof password !== "string") { + throw new BadRequestError("Password is required"); + } + + if (password.length < 6) { + throw new BadRequestError("Password must be at least 6 characters"); + } + // Check if name exists const existing = await queryOne("SELECT id FROM agents WHERE name = $1", [ normalizedName, @@ -53,24 +64,33 @@ class AgentService { throw new ConflictError("Name already taken", "Try a different name"); } + // Hash password + const passwordHash = await bcrypt.hash(password, 10); + // Generate credentials const apiKey = generateApiKey(); const claimToken = generateClaimToken(); const verificationCode = generateVerificationCode(); const apiKeyHash = hashToken(apiKey); + // Generate subdomain: ${agent-name}.${base-domain} + const baseDomain = config.cloudRun?.baseDomain || "moltbook.com"; + const subdomain = `${normalizedName}.${baseDomain}`; + // Create agent const agent = await queryOne( - `INSERT INTO agents (name, display_name, description, api_key_hash, claim_token, verification_code, status) - VALUES ($1, $2, $3, $4, $5, $6, 'pending_claim') - RETURNING id, name, display_name, created_at`, + `INSERT INTO agents (name, display_name, description, password_hash, api_key_hash, claim_token, verification_code, subdomain, status) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, 'pending_claim') + RETURNING id, name, display_name, subdomain, created_at`, [ normalizedName, name.trim(), description, + passwordHash, apiKeyHash, claimToken, verificationCode, + subdomain, ] ); @@ -80,6 +100,7 @@ class AgentService { api_key: apiKey, claim_url: `${config.moltbook.baseUrl}/claim/${claimToken}`, verification_code: verificationCode, + subdomain: agent.subdomain, }, important: "Save your API key! You will not see it again.", }; @@ -95,12 +116,42 @@ class AgentService { const apiKeyHash = hashToken(apiKey); return queryOne( - `SELECT id, name, display_name, description, karma, status, is_claimed, created_at, updated_at + `SELECT id, name, display_name, description, karma, status, is_claimed, subdomain, created_at, updated_at FROM agents WHERE api_key_hash = $1`, [apiKeyHash] ); } + /** + * Authenticate agent with name and password + * + * @param {string} name - Agent name + * @param {string} password - Agent password + * @returns {Promise} Agent or null if invalid + */ + static async authenticate(name, password) { + const normalizedName = name.toLowerCase().trim(); + + const agent = await queryOne( + `SELECT id, name, display_name, description, password_hash, karma, status, is_claimed, subdomain, created_at, updated_at + FROM agents WHERE name = $1`, + [normalizedName] + ); + + if (!agent || !agent.password_hash) { + return null; + } + + const isValid = await bcrypt.compare(password, agent.password_hash); + if (!isValid) { + return null; + } + + // Remove password_hash from response + delete agent.password_hash; + return agent; + } + /** * Find agent by name * @@ -111,7 +162,7 @@ class AgentService { const normalizedName = name.toLowerCase().trim(); return queryOne( - `SELECT id, name, display_name, description, karma, status, is_claimed, + `SELECT id, name, display_name, description, karma, status, is_claimed, subdomain, follower_count, following_count, created_at, last_active FROM agents WHERE name = $1`, [normalizedName] @@ -133,6 +184,20 @@ class AgentService { ); } + /** + * Update agent API key hash + * + * @param {string} agentId - Agent ID + * @param {string} apiKeyHash - Hashed API key + * @returns {Promise} + */ + static async updateApiKey(agentId, apiKeyHash) { + await queryOne( + `UPDATE agents SET api_key_hash = $1, updated_at = NOW() WHERE id = $2`, + [apiKeyHash, agentId] + ); + } + /** * Update agent profile * diff --git a/src/services/UserService.js b/src/services/UserService.js new file mode 100644 index 0000000..2d67a00 --- /dev/null +++ b/src/services/UserService.js @@ -0,0 +1,245 @@ +/** + * User Service + * Handles user registration, authentication, and profile management + */ + +const bcrypt = require("bcrypt"); +const { queryOne, queryAll } = require("../config/database"); +const { + generateApiKey, + hashToken, +} = require("../utils/auth"); +const { + BadRequestError, + NotFoundError, + ConflictError, + UnauthorizedError, +} = require("../utils/errors"); + +class UserService { + /** + * Register a new user + * + * @param {Object} data - Registration data + * @param {string} data.username - Username + * @param {string} data.email - Email address + * @param {string} data.password - Password + * @param {string} data.displayName - Display name (optional) + * @returns {Promise} Registration result with API key + */ + static async register({ username, email, password, displayName = "" }) { + // Validate username + if (!username || typeof username !== "string") { + throw new BadRequestError("Username is required"); + } + + const normalizedUsername = username.toLowerCase().trim(); + + if (normalizedUsername.length < 3 || normalizedUsername.length > 32) { + throw new BadRequestError("Username must be 3-32 characters"); + } + + if (!/^[a-z0-9_]+$/i.test(normalizedUsername)) { + throw new BadRequestError( + "Username can only contain letters, numbers, and underscores" + ); + } + + // Validate email + if (!email || typeof email !== "string") { + throw new BadRequestError("Email is required"); + } + + const normalizedEmail = email.toLowerCase().trim(); + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + if (!emailRegex.test(normalizedEmail)) { + throw new BadRequestError("Invalid email format"); + } + + // Validate password + if (!password || typeof password !== "string") { + throw new BadRequestError("Password is required"); + } + + if (password.length < 6) { + throw new BadRequestError("Password must be at least 6 characters"); + } + + // Check if username exists + const existingUsername = await queryOne( + "SELECT id FROM users WHERE username = $1", + [normalizedUsername] + ); + + if (existingUsername) { + throw new ConflictError("Username already taken", "Try a different username"); + } + + // Check if email exists + const existingEmail = await queryOne( + "SELECT id FROM users WHERE email = $1", + [normalizedEmail] + ); + + if (existingEmail) { + throw new ConflictError("Email already registered", "Use a different email or try logging in"); + } + + // Hash password + const passwordHash = await bcrypt.hash(password, 10); + + // Generate credentials + const apiKey = generateApiKey(); + const apiKeyHash = hashToken(apiKey); + + // Create user + const user = await queryOne( + `INSERT INTO users (username, email, display_name, password_hash, api_key_hash) + VALUES ($1, $2, $3, $4, $5) + RETURNING id, username, email, display_name, created_at`, + [ + normalizedUsername, + normalizedEmail, + displayName || normalizedUsername, + passwordHash, + apiKeyHash, + ] + ); + + return { + user: { + id: user.id, + username: user.username, + email: user.email, + displayName: user.display_name, + api_key: apiKey, + }, + important: "Save your API key! You will not see it again.", + }; + } + + /** + * Authenticate user with username/email and password + * + * @param {string} identifier - Username or email + * @param {string} password - Password + * @returns {Promise} User or null if invalid + */ + static async authenticate(identifier, password) { + const normalizedIdentifier = identifier.toLowerCase().trim(); + + // Try to find by username or email + const user = await queryOne( + `SELECT id, username, email, display_name, password_hash, api_key_hash, is_active, is_verified, created_at, updated_at + FROM users WHERE username = $1 OR email = $1`, + [normalizedIdentifier] + ); + + if (!user || !user.password_hash || !user.is_active) { + return null; + } + + const isValid = await bcrypt.compare(password, user.password_hash); + if (!isValid) { + return null; + } + + // Update last login + await queryOne( + `UPDATE users SET last_login = NOW() WHERE id = $1`, + [user.id] + ); + + // Remove password_hash from response + delete user.password_hash; + return user; + } + + /** + * Find user by API key + * + * @param {string} apiKey - API key + * @returns {Promise} User or null + */ + static async findByApiKey(apiKey) { + const apiKeyHash = hashToken(apiKey); + + return queryOne( + `SELECT id, username, email, display_name, is_active, is_verified, created_at, updated_at, last_login + FROM users WHERE api_key_hash = $1 AND is_active = true`, + [apiKeyHash] + ); + } + + /** + * Find user by username + * + * @param {string} username - Username + * @returns {Promise} User or null + */ + static async findByUsername(username) { + const normalizedUsername = username.toLowerCase().trim(); + + return queryOne( + `SELECT id, username, email, display_name, is_active, is_verified, created_at, updated_at, last_login + FROM users WHERE username = $1`, + [normalizedUsername] + ); + } + + /** + * Update user API key hash + * + * @param {string} userId - User ID + * @param {string} apiKeyHash - Hashed API key + * @returns {Promise} + */ + static async updateApiKey(userId, apiKeyHash) { + await queryOne( + `UPDATE users SET api_key_hash = $1, updated_at = NOW() WHERE id = $2`, + [apiKeyHash, userId] + ); + } + + /** + * Update user profile + * + * @param {string} id - User ID + * @param {Object} updates - Fields to update + * @returns {Promise} Updated user + */ + static async update(id, updates) { + const allowedFields = ["display_name", "avatar_url"]; + const setClause = []; + const values = []; + let paramIndex = 1; + + for (const field of allowedFields) { + if (updates[field] !== undefined) { + setClause.push(`${field} = $${paramIndex}`); + values.push(updates[field]); + paramIndex++; + } + } + + if (setClause.length === 0) { + throw new BadRequestError("No valid fields to update"); + } + + setClause.push(`updated_at = NOW()`); + values.push(id); + + const user = await queryOne( + `UPDATE users SET ${setClause.join(", ")} WHERE id = $${paramIndex} RETURNING id, username, email, display_name, avatar_url, created_at, updated_at`, + values + ); + + if (!user) { + throw new NotFoundError("User"); + } + + return user; + } +} + +module.exports = UserService; From a3d159a24fe2f1ef4e6fd2a6770481873388e30a Mon Sep 17 00:00:00 2001 From: Ginola Date: Fri, 13 Feb 2026 23:33:42 +0700 Subject: [PATCH 12/13] auth --- package-lock.json | 1455 ++------------------------------------------- package.json | 1 + 2 files changed, 38 insertions(+), 1418 deletions(-) diff --git a/package-lock.json b/package-lock.json index c41cf2c..8306bbc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,183 +9,19 @@ "version": "1.0.0", "license": "MIT", "dependencies": { - "@google-cloud/run": "^3.2.0", + "bcrypt": "^6.0.0", "compression": "^1.7.4", "cors": "^2.8.5", "dotenv": "^16.3.1", "express": "^4.18.2", "helmet": "^7.1.0", - "js-yaml": "^4.1.1", "morgan": "^1.10.0", "pg": "^8.11.3" }, - "devDependencies": {}, "engines": { "node": ">=18.0.0" } }, - "node_modules/@google-cloud/run": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/@google-cloud/run/-/run-3.2.0.tgz", - "integrity": "sha512-R6nJmPYcSjl8irkZo8GKQeyEYZ2JXsboGaVLJteZlMp55s7BR6P8pKVnAgomBXH7nlBscAvWfhtE5WEl6Yn9RA==", - "license": "Apache-2.0", - "dependencies": { - "google-gax": "^5.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/@grpc/grpc-js": { - "version": "1.14.3", - "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.14.3.tgz", - "integrity": "sha512-Iq8QQQ/7X3Sac15oB6p0FmUg/klxQvXLeileoqrTRGJYLV+/9tubbr9ipz0GKHjmXVsgFPo/+W+2cA8eNcR+XA==", - "license": "Apache-2.0", - "dependencies": { - "@grpc/proto-loader": "^0.8.0", - "@js-sdsl/ordered-map": "^4.4.2" - }, - "engines": { - "node": ">=12.10.0" - } - }, - "node_modules/@grpc/proto-loader": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.8.0.tgz", - "integrity": "sha512-rc1hOQtjIWGxcxpb9aHAfLpIctjEnsDehj0DAiVfBlmT84uvR0uUtN2hEi/ecvWVjXUGf5qPF4qEgiLOx1YIMQ==", - "license": "Apache-2.0", - "dependencies": { - "lodash.camelcase": "^4.3.0", - "long": "^5.0.0", - "protobufjs": "^7.5.3", - "yargs": "^17.7.2" - }, - "bin": { - "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/@isaacs/cliui": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", - "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", - "license": "ISC", - "dependencies": { - "string-width": "^5.1.2", - "string-width-cjs": "npm:string-width@^4.2.0", - "strip-ansi": "^7.0.1", - "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", - "wrap-ansi": "^8.1.0", - "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@js-sdsl/ordered-map": { - "version": "4.4.2", - "resolved": "https://registry.npmjs.org/@js-sdsl/ordered-map/-/ordered-map-4.4.2.tgz", - "integrity": "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==", - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/js-sdsl" - } - }, - "node_modules/@pkgjs/parseargs": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", - "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", - "license": "MIT", - "optional": true, - "engines": { - "node": ">=14" - } - }, - "node_modules/@protobufjs/aspromise": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", - "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/base64": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", - "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/codegen": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", - "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/eventemitter": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", - "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/fetch": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", - "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", - "license": "BSD-3-Clause", - "dependencies": { - "@protobufjs/aspromise": "^1.1.1", - "@protobufjs/inquire": "^1.1.0" - } - }, - "node_modules/@protobufjs/float": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", - "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/inquire": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", - "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/path": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", - "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/pool": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", - "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/utf8": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", - "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==", - "license": "BSD-3-Clause" - }, - "node_modules/@tootallnate/once": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", - "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", - "license": "MIT", - "engines": { - "node": ">= 10" - } - }, - "node_modules/@types/node": { - "version": "25.2.2", - "resolved": "https://registry.npmjs.org/@types/node/-/node-25.2.2.tgz", - "integrity": "sha512-BkmoP5/FhRYek5izySdkOneRyXYN35I860MFAGupTdebyE66uZaR+bXLHq8k4DirE5DwQi3NuhvRU1jqTVwUrQ==", - "license": "MIT", - "dependencies": { - "undici-types": "~7.16.0" - } - }, "node_modules/accepts": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", @@ -208,77 +44,12 @@ "node": ">= 0.6" } }, - "node_modules/agent-base": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", - "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", - "license": "MIT", - "engines": { - "node": ">= 14" - } - }, - "node_modules/ansi-regex": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/ansi-styles": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", - "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "license": "Python-2.0" - }, "node_modules/array-flatten": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", "license": "MIT" }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "license": "MIT" - }, - "node_modules/base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, "node_modules/basic-auth": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", @@ -297,13 +68,18 @@ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", "license": "MIT" }, - "node_modules/bignumber.js": { - "version": "9.3.1", - "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz", - "integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==", + "node_modules/bcrypt": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-6.0.0.tgz", + "integrity": "sha512-cU8v/EGSrnH+HnxV2z0J7/blxH8gq7Xh2JFT6Aroax7UohdmiJJlxApMxtKfuI7z68NvvVcmR78k2LbT6efhRg==", + "hasInstallScript": true, "license": "MIT", + "dependencies": { + "node-addon-api": "^8.3.0", + "node-gyp-build": "^4.8.4" + }, "engines": { - "node": "*" + "node": ">= 18" } }, "node_modules/body-parser": { @@ -330,21 +106,6 @@ "npm": "1.2.8000 || >= 1.4.16" } }, - "node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/buffer-equal-constant-time": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", - "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", - "license": "BSD-3-Clause" - }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -383,111 +144,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/cliui": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", - "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", - "license": "ISC", - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/cliui/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/cliui/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/cliui/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "license": "MIT" - }, - "node_modules/cliui/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/cliui/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/cliui/node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "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/compressible": { "version": "2.0.18", "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", @@ -571,29 +227,6 @@ "url": "https://opencollective.com/express" } }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/data-uri-to-buffer": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", - "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", - "license": "MIT", - "engines": { - "node": ">= 12" - } - }, "node_modules/debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", @@ -648,45 +281,12 @@ "node": ">= 0.4" } }, - "node_modules/duplexify": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.3.tgz", - "integrity": "sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA==", - "license": "MIT", - "dependencies": { - "end-of-stream": "^1.4.1", - "inherits": "^2.0.3", - "readable-stream": "^3.1.1", - "stream-shift": "^1.0.2" - } - }, - "node_modules/eastasianwidth": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", - "license": "MIT" - }, - "node_modules/ecdsa-sig-formatter": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", - "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", - "license": "Apache-2.0", - "dependencies": { - "safe-buffer": "^5.0.1" - } - }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", "license": "MIT" }, - "node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "license": "MIT" - }, "node_modules/encodeurl": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", @@ -696,15 +296,6 @@ "node": ">= 0.8" } }, - "node_modules/end-of-stream": { - "version": "1.4.5", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", - "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", - "license": "MIT", - "dependencies": { - "once": "^1.4.0" - } - }, "node_modules/es-define-property": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", @@ -735,15 +326,6 @@ "node": ">= 0.4" } }, - "node_modules/escalade": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/escape-html": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", @@ -805,35 +387,6 @@ "url": "https://opencollective.com/express" } }, - "node_modules/extend": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", - "license": "MIT" - }, - "node_modules/fetch-blob": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", - "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/jimmywarting" - }, - { - "type": "paypal", - "url": "https://paypal.me/jimmywarting" - } - ], - "license": "MIT", - "dependencies": { - "node-domexception": "^1.0.0", - "web-streams-polyfill": "^3.0.3" - }, - "engines": { - "node": "^12.20 || >= 14.13" - } - }, "node_modules/finalhandler": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", @@ -852,34 +405,6 @@ "node": ">= 0.8" } }, - "node_modules/foreground-child": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", - "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", - "license": "ISC", - "dependencies": { - "cross-spawn": "^7.0.6", - "signal-exit": "^4.0.1" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/formdata-polyfill": { - "version": "4.0.10", - "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", - "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", - "license": "MIT", - "dependencies": { - "fetch-blob": "^3.1.2" - }, - "engines": { - "node": ">=12.20.0" - } - }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -907,44 +432,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/gaxios": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.1.3.tgz", - "integrity": "sha512-YGGyuEdVIjqxkxVH1pUTMY/XtmmsApXrCVv5EU25iX6inEPbV+VakJfLealkBtJN69AQmh1eGOdCl9Sm1UP6XQ==", - "license": "Apache-2.0", - "dependencies": { - "extend": "^3.0.2", - "https-proxy-agent": "^7.0.1", - "node-fetch": "^3.3.2", - "rimraf": "^5.0.1" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/gcp-metadata": { - "version": "8.1.2", - "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-8.1.2.tgz", - "integrity": "sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg==", - "license": "Apache-2.0", - "dependencies": { - "gaxios": "^7.0.0", - "google-logging-utils": "^1.0.0", - "json-bigint": "^1.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "license": "ISC", - "engines": { - "node": "6.* || 8.* || >= 10.*" - } - }, "node_modules/get-intrinsic": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", @@ -982,106 +469,23 @@ "node": ">= 0.4" } }, - "node_modules/glob": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", - "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", - "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", - "license": "ISC", - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^1.11.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" }, "funding": { - "url": "https://github.com/sponsors/isaacs" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/google-auth-library": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-10.5.0.tgz", - "integrity": "sha512-7ABviyMOlX5hIVD60YOfHw4/CxOfBhyduaYB+wbFWCWoni4N7SLcV46hrVRktuBbZjFC9ONyqamZITN7q3n32w==", - "license": "Apache-2.0", - "dependencies": { - "base64-js": "^1.3.0", - "ecdsa-sig-formatter": "^1.0.11", - "gaxios": "^7.0.0", - "gcp-metadata": "^8.0.0", - "google-logging-utils": "^1.0.0", - "gtoken": "^8.0.0", - "jws": "^4.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/google-gax": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/google-gax/-/google-gax-5.0.6.tgz", - "integrity": "sha512-1kGbqVQBZPAAu4+/R1XxPQKP0ydbNYoLAr4l0ZO2bMV0kLyLW4I1gAk++qBLWt7DPORTzmWRMsCZe86gDjShJA==", - "license": "Apache-2.0", - "dependencies": { - "@grpc/grpc-js": "^1.12.6", - "@grpc/proto-loader": "^0.8.0", - "duplexify": "^4.1.3", - "google-auth-library": "^10.1.0", - "google-logging-utils": "^1.1.1", - "node-fetch": "^3.3.2", - "object-hash": "^3.0.0", - "proto3-json-serializer": "^3.0.0", - "protobufjs": "^7.5.3", - "retry-request": "^8.0.0", - "rimraf": "^5.0.1" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/google-logging-utils": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-1.1.3.tgz", - "integrity": "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA==", - "license": "Apache-2.0", - "engines": { - "node": ">=14" - } - }, - "node_modules/gopd": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/gtoken": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-8.0.0.tgz", - "integrity": "sha512-+CqsMbHPiSTdtSO14O51eMNlrp9N79gmeqmXeouJOhfucAedHw9noVe/n5uJk3tbKE6a+6ZCQg3RPhVhHByAIw==", - "license": "MIT", - "dependencies": { - "gaxios": "^7.0.0", - "jws": "^4.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/has-symbols": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "license": "MIT", + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -1130,91 +534,6 @@ "url": "https://opencollective.com/express" } }, - "node_modules/http-proxy-agent": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", - "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", - "license": "MIT", - "dependencies": { - "@tootallnate/once": "2", - "agent-base": "6", - "debug": "4" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/http-proxy-agent/node_modules/agent-base": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", - "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", - "license": "MIT", - "dependencies": { - "debug": "4" - }, - "engines": { - "node": ">= 6.0.0" - } - }, - "node_modules/http-proxy-agent/node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/http-proxy-agent/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" - }, - "node_modules/https-proxy-agent": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", - "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", - "license": "MIT", - "dependencies": { - "agent-base": "^7.1.2", - "debug": "4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/https-proxy-agent/node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/https-proxy-agent/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" - }, "node_modules/iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -1242,96 +561,6 @@ "node": ">= 0.10" } }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "license": "ISC" - }, - "node_modules/jackspeak": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", - "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/cliui": "^8.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - }, - "optionalDependencies": { - "@pkgjs/parseargs": "^0.11.0" - } - }, - "node_modules/js-yaml": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", - "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", - "license": "MIT", - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/json-bigint": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", - "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", - "license": "MIT", - "dependencies": { - "bignumber.js": "^9.0.0" - } - }, - "node_modules/jwa": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", - "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", - "license": "MIT", - "dependencies": { - "buffer-equal-constant-time": "^1.0.1", - "ecdsa-sig-formatter": "1.0.11", - "safe-buffer": "^5.0.1" - } - }, - "node_modules/jws": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", - "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", - "license": "MIT", - "dependencies": { - "jwa": "^2.0.1", - "safe-buffer": "^5.0.1" - } - }, - "node_modules/lodash.camelcase": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", - "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", - "license": "MIT" - }, - "node_modules/long": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", - "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", - "license": "Apache-2.0" - }, - "node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "license": "ISC" - }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -1410,30 +639,6 @@ "node": ">= 0.6" } }, - "node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/minipass": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", - "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", - "license": "ISC", - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, "node_modules/morgan": { "version": "1.10.1", "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.1.tgz", @@ -1477,42 +682,24 @@ "node": ">= 0.6" } }, - "node_modules/node-domexception": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", - "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", - "deprecated": "Use your platform's native DOMException instead", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/jimmywarting" - }, - { - "type": "github", - "url": "https://paypal.me/jimmywarting" - } - ], + "node_modules/node-addon-api": { + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.5.0.tgz", + "integrity": "sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A==", "license": "MIT", "engines": { - "node": ">=10.5.0" + "node": "^18 || ^20 || >= 21" } }, - "node_modules/node-fetch": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", - "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "node_modules/node-gyp-build": { + "version": "4.8.4", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", + "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==", "license": "MIT", - "dependencies": { - "data-uri-to-buffer": "^4.0.0", - "fetch-blob": "^3.1.4", - "formdata-polyfill": "^4.0.10" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/node-fetch" + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" } }, "node_modules/object-assign": { @@ -1524,15 +711,6 @@ "node": ">=0.10.0" } }, - "node_modules/object-hash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", - "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, "node_modules/object-inspect": { "version": "1.13.4", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", @@ -1566,21 +744,6 @@ "node": ">= 0.8" } }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "license": "ISC", - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/package-json-from-dist": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", - "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", - "license": "BlueOak-1.0.0" - }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -1590,31 +753,6 @@ "node": ">= 0.8" } }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-scurry": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", - "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", - "license": "BlueOak-1.0.0", - "dependencies": { - "lru-cache": "^10.2.0", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" - }, - "engines": { - "node": ">=16 || 14 >=14.18" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/path-to-regexp": { "version": "0.1.12", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", @@ -1749,42 +887,6 @@ "node": ">=0.10.0" } }, - "node_modules/proto3-json-serializer": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/proto3-json-serializer/-/proto3-json-serializer-3.0.4.tgz", - "integrity": "sha512-E1sbAYg3aEbXrq0n1ojJkRHQJGE1kaE/O6GLA94y8rnJBfgvOPTOd1b9hOceQK1FFZI9qMh1vBERCyO2ifubcw==", - "license": "Apache-2.0", - "dependencies": { - "protobufjs": "^7.4.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/protobufjs": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.4.tgz", - "integrity": "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg==", - "hasInstallScript": true, - "license": "BSD-3-Clause", - "dependencies": { - "@protobufjs/aspromise": "^1.1.2", - "@protobufjs/base64": "^1.1.2", - "@protobufjs/codegen": "^2.0.4", - "@protobufjs/eventemitter": "^1.1.0", - "@protobufjs/fetch": "^1.1.0", - "@protobufjs/float": "^1.0.2", - "@protobufjs/inquire": "^1.1.0", - "@protobufjs/path": "^1.1.2", - "@protobufjs/pool": "^1.1.0", - "@protobufjs/utf8": "^1.1.0", - "@types/node": ">=13.7.0", - "long": "^5.0.0" - }, - "engines": { - "node": ">=12.0.0" - } - }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -1837,57 +939,6 @@ "node": ">= 0.8" } }, - "node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "license": "MIT", - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/retry-request": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/retry-request/-/retry-request-8.0.2.tgz", - "integrity": "sha512-JzFPAfklk1kjR1w76f0QOIhoDkNkSqW8wYKT08n9yysTmZfB+RQ2QoXoTAeOi1HD9ZipTyTAZg3c4pM/jeqgSw==", - "license": "MIT", - "dependencies": { - "extend": "^3.0.2", - "teeny-request": "^10.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/rimraf": { - "version": "5.0.10", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.10.tgz", - "integrity": "sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==", - "license": "ISC", - "dependencies": { - "glob": "^10.3.7" - }, - "bin": { - "rimraf": "dist/esm/bin.mjs" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -1965,27 +1016,6 @@ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", "license": "ISC" }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "license": "MIT", - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/side-channel": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", @@ -2058,18 +1088,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "license": "ISC", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/split2": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", @@ -2088,195 +1106,6 @@ "node": ">= 0.8" } }, - "node_modules/stream-events": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/stream-events/-/stream-events-1.0.5.tgz", - "integrity": "sha512-E1GUzBSgvct8Jsb3v2X15pjzN1tYebtbLaMg+eBOUOAxgbLoSbT2NS91ckc5lJD1KfLjId+jXJRgo0qnV5Nerg==", - "license": "MIT", - "dependencies": { - "stubs": "^3.0.0" - } - }, - "node_modules/stream-shift": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.3.tgz", - "integrity": "sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==", - "license": "MIT" - }, - "node_modules/string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.2.0" - } - }, - "node_modules/string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "license": "MIT", - "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/string-width-cjs": { - "name": "string-width", - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width-cjs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width-cjs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "license": "MIT" - }, - "node_modules/string-width-cjs/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", - "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/strip-ansi-cjs": { - "name": "strip-ansi", - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/stubs": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/stubs/-/stubs-3.0.0.tgz", - "integrity": "sha512-PdHt7hHUJKxvTCgbKX9C1V/ftOcjJQgz8BZwNfV5c4B6dcGqlpelTbJ999jBGZ2jYiPAwcX5dP6oBwVlBlUbxw==", - "license": "MIT" - }, - "node_modules/teeny-request": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/teeny-request/-/teeny-request-10.1.0.tgz", - "integrity": "sha512-3ZnLvgWF29jikg1sAQ1g0o+lr5JX6sVgYvfUJazn7ZjJroDBUTWp44/+cFVX0bULjv4vci+rBD+oGVAkWqhUbw==", - "license": "Apache-2.0", - "dependencies": { - "http-proxy-agent": "^5.0.0", - "https-proxy-agent": "^5.0.0", - "node-fetch": "^3.3.2", - "stream-events": "^1.0.5" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/teeny-request/node_modules/agent-base": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", - "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", - "license": "MIT", - "dependencies": { - "debug": "4" - }, - "engines": { - "node": ">= 6.0.0" - } - }, - "node_modules/teeny-request/node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/teeny-request/node_modules/https-proxy-agent": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", - "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", - "license": "MIT", - "dependencies": { - "agent-base": "6", - "debug": "4" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/teeny-request/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" - }, "node_modules/toidentifier": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", @@ -2299,12 +1128,6 @@ "node": ">= 0.6" } }, - "node_modules/undici-types": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", - "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", - "license": "MIT" - }, "node_modules/unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", @@ -2314,12 +1137,6 @@ "node": ">= 0.8" } }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "license": "MIT" - }, "node_modules/utils-merge": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", @@ -2338,127 +1155,6 @@ "node": ">= 0.8" } }, - "node_modules/web-streams-polyfill": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", - "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/wrap-ansi": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", - "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.1.0", - "string-width": "^5.0.1", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs": { - "name": "wrap-ansi", - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "license": "MIT" - }, - "node_modules/wrap-ansi-cjs/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "license": "ISC" - }, "node_modules/xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", @@ -2467,83 +1163,6 @@ "engines": { "node": ">=0.4" } - }, - "node_modules/y18n": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "license": "ISC", - "engines": { - "node": ">=10" - } - }, - "node_modules/yargs": { - "version": "17.7.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", - "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", - "license": "MIT", - "dependencies": { - "cliui": "^8.0.1", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.3", - "y18n": "^5.0.5", - "yargs-parser": "^21.1.1" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/yargs-parser": { - "version": "21.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", - "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/yargs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/yargs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "license": "MIT" - }, - "node_modules/yargs/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/yargs/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } } } } diff --git a/package.json b/package.json index aff1752..8a9ca1f 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "node": ">=18.0.0" }, "dependencies": { + "bcrypt": "^6.0.0", "compression": "^1.7.4", "cors": "^2.8.5", "dotenv": "^16.3.1", From ceac25f0dc19a3d47c1bbfea1158622fe0a7b1e5 Mon Sep 17 00:00:00 2001 From: Ginola Date: Sat, 14 Feb 2026 01:25:07 +0700 Subject: [PATCH 13/13] auth --- src/routes/users.js | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/src/routes/users.js b/src/routes/users.js index 27b39cc..abd27d3 100644 --- a/src/routes/users.js +++ b/src/routes/users.js @@ -10,6 +10,7 @@ const { success, created } = require("../utils/response"); const UserService = require("../services/UserService"); const { BadRequestError, UnauthorizedError } = require("../utils/errors"); const { generateApiKey, hashToken } = require("../utils/auth"); +const config = require("../config"); const router = Router(); @@ -27,6 +28,38 @@ router.post( password, displayName, }); + + // Deploy Cloud Run service for user (same as agent) – fire and forget + (async () => { + try { + await fetch(config.cloudRun.deployerUrl, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + serviceName: result.user.username, + containerImage: + "europe-west1-docker.pkg.dev/barrsa-customer-side/barrsa-platform/openclaw", + region: "europe-west1", + env: [ + { name: "OPENCLAW_GATEWAY_TOKEN", value: "mysecrettoken" }, + { name: "OPENCLAW_GATEWAY_PORT", value: "8080" }, + ], + resources: { cpu: "1", memory: "2Gi" }, + minInstances: 0, + maxInstances: 5, + publicAccess: true, + }), + }); + } catch (error) { + console.error( + `Failed to deploy Cloud Run service for user ${result.user.username}:`, + error.message + ); + } + })(); + created(res, result); }) );