diff --git a/extensions/meteringService/main.js b/extensions/meteringService/main.js new file mode 100644 index 000000000..d0a519f7d --- /dev/null +++ b/extensions/meteringService/main.js @@ -0,0 +1 @@ +import './routes/usage.js'; diff --git a/extensions/meteringService/package.json b/extensions/meteringService/package.json new file mode 100644 index 000000000..a574e2b5d --- /dev/null +++ b/extensions/meteringService/package.json @@ -0,0 +1,5 @@ +{ + "name": "@heyputer/extension-metering-service", + "main": "main.js", + "type": "module" +} \ No newline at end of file diff --git a/extensions/meteringService/routes/usage.js b/extensions/meteringService/routes/usage.js new file mode 100644 index 000000000..cb30b9faa --- /dev/null +++ b/extensions/meteringService/routes/usage.js @@ -0,0 +1,35 @@ +/** @type {import('@heyputer/backend/src/services/MeteringService/MeteringServiceWrapper.mjs').MeteringAndBillingServiceWrapper} */ +const meteringAndBillingServiceWrapper = extension.import('service:meteringService'); + +// TODO DS: move this to its own router and just use under this path +extension.get('/v2/usage', { subdomain: 'api' }, async (req, res) => { + const meteringAndBillingService = meteringAndBillingServiceWrapper.meteringAndBillingService; + + const actor = req.actor; + if ( !actor ) { + throw Error('actor not found in context'); + } + const actorUsage = await meteringAndBillingService.getActorCurrentMonthUsageDetails(actor); + res.status(200).json(actorUsage); + return; +}); + +extension.get('/v2/usage/:appId', { subdomain: 'api' }, async (req, res) => { + const meteringAndBillingService = meteringAndBillingServiceWrapper.meteringAndBillingService; + + const actor = req.actor; + if ( !actor ) { + throw Error('actor not found in context'); + } + const appId = req.params.appId; + if ( !appId ) { + res.status(400).json({ error: 'appId parameter is required' }); + return; + } + + const appUsage = await meteringAndBillingService.getActorCurrentMonthAppUsageDetails(actor, appId); + res.status(200).json(appUsage); + return; +}); + +console.debug('Loaded /v2/usage route'); \ No newline at end of file diff --git a/src/backend/src/CoreModule.js b/src/backend/src/CoreModule.js index 2073fa487..a24298e4e 100644 --- a/src/backend/src/CoreModule.js +++ b/src/backend/src/CoreModule.js @@ -412,7 +412,7 @@ const install = async ({ context, services, app, useapi, modapi }) => { const { WorkerService } = require('./services/worker/WorkerService'); services.registerService("worker-service", WorkerService); - const { MeteringAndBillingServiceWrapper } = require("./services/abuse-prevention/MeteringService/index.mjs"); + const { MeteringAndBillingServiceWrapper } = require("./services/MeteringService/MeteringServiceWrapper.mjs"); services.registerService('meteringService', MeteringAndBillingServiceWrapper); const { PermissionShortcutService } = require('./services/auth/PermissionShortcutService'); diff --git a/src/backend/src/modules/puterai/AWSPollyService.js b/src/backend/src/modules/puterai/AWSPollyService.js index 3dc2aed29..51a5be1da 100644 --- a/src/backend/src/modules/puterai/AWSPollyService.js +++ b/src/backend/src/modules/puterai/AWSPollyService.js @@ -44,7 +44,7 @@ const VALID_ENGINES = ['standard', 'neural', 'long-form', 'generative']; * @extends BaseService */ class AWSPollyService extends BaseService { - /** @type {import('../../services/abuse-prevention/MeteringService/MeteringService').MeteringAndBillingService} */ + /** @type {import('../../services/MeteringService/MeteringService').MeteringAndBillingService} */ meteringAndBillingService; static MODULES = { diff --git a/src/backend/src/modules/puterai/AWSTextractService.js b/src/backend/src/modules/puterai/AWSTextractService.js index d2904e9e1..2c3166b45 100644 --- a/src/backend/src/modules/puterai/AWSTextractService.js +++ b/src/backend/src/modules/puterai/AWSTextractService.js @@ -31,7 +31,7 @@ const { Context } = require("../../util/context"); * Handles both S3-stored and buffer-based document processing with automatic region management. */ class AWSTextractService extends BaseService { - /** @type {import('../../services/abuse-prevention/MeteringService/MeteringService').MeteringAndBillingService} */ + /** @type {import('../../services/MeteringService/MeteringService').MeteringAndBillingService} */ meteringAndBillingService; /** * AWS Textract service for OCR functionality diff --git a/src/backend/src/modules/puterai/ClaudeService.js b/src/backend/src/modules/puterai/ClaudeService.js index 0b043cf84..701934999 100644 --- a/src/backend/src/modules/puterai/ClaudeService.js +++ b/src/backend/src/modules/puterai/ClaudeService.js @@ -51,7 +51,7 @@ class ClaudeService extends BaseService { * @returns {Promise} */ - /** @type {import('../../services/abuse-prevention/MeteringService/MeteringService').MeteringAndBillingService} */ + /** @type {import('../../services/MeteringService/MeteringService').MeteringAndBillingService} */ #meteringAndBillingService; async _init() { diff --git a/src/backend/src/modules/puterai/DeepSeekService.js b/src/backend/src/modules/puterai/DeepSeekService.js index 95ca4ea3d..cb0af051a 100644 --- a/src/backend/src/modules/puterai/DeepSeekService.js +++ b/src/backend/src/modules/puterai/DeepSeekService.js @@ -36,7 +36,7 @@ class DeepSeekService extends BaseService { }; /** - * @type {import('../../services/abuse-prevention/MeteringService/MeteringService').MeteringAndBillingService} + * @type {import('../../services/MeteringService/MeteringService').MeteringAndBillingService} */ meteringAndBillingService; /** diff --git a/src/backend/src/modules/puterai/GeminiImageGenerationService.js b/src/backend/src/modules/puterai/GeminiImageGenerationService.js index b7a503566..cb20245e6 100644 --- a/src/backend/src/modules/puterai/GeminiImageGenerationService.js +++ b/src/backend/src/modules/puterai/GeminiImageGenerationService.js @@ -30,7 +30,7 @@ const { GoogleGenAI } = require('@google/genai'); * the puter-image-generation interface. */ class GeminiImageGenerationService extends BaseService { - /** @type {import('../../services/abuse-prevention/MeteringService/MeteringService').MeteringAndBillingService} */ + /** @type {import('../../services/MeteringService/MeteringService').MeteringAndBillingService} */ meteringAndBillingService; static MODULES = { }; diff --git a/src/backend/src/modules/puterai/GeminiService.js b/src/backend/src/modules/puterai/GeminiService.js index b3779b1d8..617398202 100644 --- a/src/backend/src/modules/puterai/GeminiService.js +++ b/src/backend/src/modules/puterai/GeminiService.js @@ -7,7 +7,7 @@ const { Context } = require("../../util/context"); class GeminiService extends BaseService { /** - * @type {import('../../services/abuse-prevention/MeteringService/MeteringService').MeteringAndBillingService} + * @type {import('../../services/MeteringService/MeteringService').MeteringAndBillingService} */ meteringAndBillingService = undefined; diff --git a/src/backend/src/modules/puterai/GroqAIService.js b/src/backend/src/modules/puterai/GroqAIService.js index 2ba93c2ac..96d7667f2 100644 --- a/src/backend/src/modules/puterai/GroqAIService.js +++ b/src/backend/src/modules/puterai/GroqAIService.js @@ -22,7 +22,7 @@ const BaseService = require("../../services/BaseService"); const { Context } = require("../../util/context"); const OpenAIUtil = require("./lib/OpenAIUtil"); -/** @type {import('../../services/abuse-prevention/MeteringService/MeteringService').MeteringAndBillingService} */ +/** @type {import('../../services/MeteringService/MeteringService').MeteringAndBillingService} */ /** * Service class for integrating with Groq AI's language models. @@ -34,7 +34,7 @@ const OpenAIUtil = require("./lib/OpenAIUtil"); * @extends BaseService */ class GroqAIService extends BaseService { - /** @type {import('../../services/abuse-prevention/MeteringService/MeteringService').MeteringAndBillingService} */ + /** @type {import('../../services/MeteringService/MeteringService').MeteringAndBillingService} */ meteringAndBillingService; static MODULES = { Groq: require('groq-sdk'), diff --git a/src/backend/src/modules/puterai/MistralAIService.js b/src/backend/src/modules/puterai/MistralAIService.js index 8ed2ee465..3312bb0ef 100644 --- a/src/backend/src/modules/puterai/MistralAIService.js +++ b/src/backend/src/modules/puterai/MistralAIService.js @@ -31,7 +31,7 @@ const { Context } = require("../../util/context"); * for different models and implements the puter-chat-completion interface. */ class MistralAIService extends BaseService { - /** @type {import('../../services/abuse-prevention/MeteringService/MeteringService').MeteringAndBillingService} */ + /** @type {import('../../services/MeteringService/MeteringService').MeteringAndBillingService} */ meteringAndBillingService; static MODULES = { '@mistralai/mistralai': require('@mistralai/mistralai'), diff --git a/src/backend/src/modules/puterai/OpenAIImageGenerationService.js b/src/backend/src/modules/puterai/OpenAIImageGenerationService.js index ee94762ea..49938b1ab 100644 --- a/src/backend/src/modules/puterai/OpenAIImageGenerationService.js +++ b/src/backend/src/modules/puterai/OpenAIImageGenerationService.js @@ -31,7 +31,7 @@ const { Context } = require("../../util/context"); * validation, and spending tracking. */ class OpenAIImageGenerationService extends BaseService { - /** @type {import('../../services/abuse-prevention/MeteringService/MeteringService').MeteringAndBillingService} */ + /** @type {import('../../services/MeteringService/MeteringService').MeteringAndBillingService} */ meteringAndBillingService; static MODULES = { diff --git a/src/backend/src/modules/puterai/OpenAiCompletionService/OpenAICompletionService.mjs b/src/backend/src/modules/puterai/OpenAiCompletionService/OpenAICompletionService.mjs index a9ac0fc26..83ea7d4de 100644 --- a/src/backend/src/modules/puterai/OpenAiCompletionService/OpenAICompletionService.mjs +++ b/src/backend/src/modules/puterai/OpenAiCompletionService/OpenAICompletionService.mjs @@ -48,7 +48,7 @@ export class OpenAICompletionService { #models; - /** @type {import('../../../services/abuse-prevention/MeteringService/MeteringService').MeteringAndBillingService} */ + /** @type {import('../../../services/MeteringService/MeteringService.js').MeteringAndBillingService} */ #meteringAndBillingService; constructor({ serviceName, config, globalConfig, aiChatService, meteringAndBillingService, models = OPEN_AI_MODELS, defaultModel = 'gpt-4.1-nano' }) { diff --git a/src/backend/src/modules/puterai/OpenRouterService.js b/src/backend/src/modules/puterai/OpenRouterService.js index d3adf8d6a..17d918794 100644 --- a/src/backend/src/modules/puterai/OpenRouterService.js +++ b/src/backend/src/modules/puterai/OpenRouterService.js @@ -46,7 +46,7 @@ class OpenRouterService extends BaseService { return model; } - /** @type {import('../../services/abuse-prevention/MeteringService/MeteringService').MeteringAndBillingService} */ + /** @type {import('../../services/MeteringService/MeteringService').MeteringAndBillingService} */ meteringAndBillingService; /** diff --git a/src/backend/src/modules/puterai/TogetherAIService.js b/src/backend/src/modules/puterai/TogetherAIService.js index f108802fb..e5e080f1c 100644 --- a/src/backend/src/modules/puterai/TogetherAIService.js +++ b/src/backend/src/modules/puterai/TogetherAIService.js @@ -36,7 +36,7 @@ const { Context } = require("../../util/context"); */ class TogetherAIService extends BaseService { /** - * @type {import('../../services/abuse-prevention/MeteringService/MeteringService').MeteringAndBillingService} + * @type {import('../../services/MeteringService/MeteringService').MeteringAndBillingService} */ meteringAndBillingService; static MODULES = { diff --git a/src/backend/src/modules/puterai/XAIService.js b/src/backend/src/modules/puterai/XAIService.js index 21fc87031..3599a6ca6 100644 --- a/src/backend/src/modules/puterai/XAIService.js +++ b/src/backend/src/modules/puterai/XAIService.js @@ -33,7 +33,7 @@ class XAIService extends BaseService { static MODULES = { openai: require('openai'), }; - /** @type {import('../../services/abuse-prevention/MeteringService/MeteringService').MeteringAndBillingService} */ + /** @type {import('../../services/MeteringService/MeteringService').MeteringAndBillingService} */ meteringAndBillingService; adapt_model(model) { diff --git a/src/backend/src/services/abuse-prevention/MeteringService/.gitignore b/src/backend/src/services/MeteringService/.gitignore similarity index 100% rename from src/backend/src/services/abuse-prevention/MeteringService/.gitignore rename to src/backend/src/services/MeteringService/.gitignore diff --git a/src/backend/src/services/abuse-prevention/MeteringService/MeteringService.ts b/src/backend/src/services/MeteringService/MeteringService.ts similarity index 77% rename from src/backend/src/services/abuse-prevention/MeteringService/MeteringService.ts rename to src/backend/src/services/MeteringService/MeteringService.ts index fa6e009f7..591a8d4ee 100644 --- a/src/backend/src/services/abuse-prevention/MeteringService/MeteringService.ts +++ b/src/backend/src/services/MeteringService/MeteringService.ts @@ -1,12 +1,13 @@ // @ts-ignore -import type KVStoreInterface from "../../../modules/kvstore/KVStoreInterfaceService.js"; +import { SystemActorType, type Actor } from "../auth/Actor.js"; // @ts-ignore -import { SystemActorType, type Actor } from "../../auth/Actor.js"; +import type { AlarmService } from "../../modules/core/AlarmService.js"; // @ts-ignore -import type { AlarmService } from "../../../modules/core/AlarmService.js"; +import type { DBKVStore } from '../repositories/DBKVStore/DBKVStore.mjs'; // @ts-ignore -import type { SUService } from "../../SUService.js"; +import type { SUService } from "../SUService.js"; import { COST_MAPS } from "./costMaps/index.js"; +import { SUB_POLICIES } from "./subPolicies/index.js"; interface ActorWithType extends Actor { type: { app: { uid: string } @@ -26,14 +27,6 @@ interface UsageByType { [serviceName: string]: number } - -// NOTE: create daily and hourly entry buckets that expire at given ranges 2 days for hours, 6 months for daily -// Store consumed microcents whenever a consumption event goes through -// keep timestamp of consumption last updated to limit burst usage - -const POLICY_TYPES = { - 'free': {} // TODO DS: define what needs to go here -} const GLOBAL_APP_KEY = 'os-global'; // TODO DS: this should be loaded from config or db eventually const METRICS_PREFIX = 'metering'; const POLICY_PREFIX = 'policy'; @@ -44,16 +37,16 @@ const PERIOD_ESCAPE = '_dot_'; // to replace dots in usage types for kvstore pat */ export class MeteringAndBillingService { - #kvClientWrapper: KVStoreInterface + #kvClientWrapper: DBKVStore #superUserService: SUService #alarmService: AlarmService - constructor({ kvClientWrapper, superUserService, alarmService }: { kvClientWrapper: KVStoreInterface, superUserService: SUService, alarmService: AlarmService }) { + constructor({ kvClientWrapper, superUserService, alarmService }: { kvClientWrapper: DBKVStore, superUserService: SUService, alarmService: AlarmService }) { this.#superUserService = superUserService; this.#kvClientWrapper = kvClientWrapper; this.#alarmService = alarmService; } - utilRecordUsageObject(trackedUsageObject: Record, actor: Actor, modelPrefix: string) { + utilRecordUsageObject(trackedUsageObject: Record, actor: ActorWithType, modelPrefix: string) { Object.entries(trackedUsageObject).forEach(([usageKind, amount]) => { this.incrementUsage(actor, `${modelPrefix}:${usageKind}`, amount); }); @@ -146,7 +139,6 @@ export class MeteringAndBillingService { }); return { total: 0 } as UsageByType; } - // TODO DS: this should increment the cost for the given type of operation, and the total cost for daily, weekly and monthly usage } async getActorCurrentMonthUsageDetails(actor: ActorWithType) { @@ -159,54 +151,71 @@ export class MeteringAndBillingService { `${METRICS_PREFIX}:actor:${actor.type.user.uuid}:${currentMonth}`, `${METRICS_PREFIX}:actor:${actor.type.user.uuid}:apps:${currentMonth}` ] - return this.#superUserService.sudo(async () => { + + return await this.#superUserService.sudo(async () => { const [usage, appTotals] = await this.#kvClientWrapper.get({ key: keys }) as [UsageByType | null, Record | null]; - return { - usage: usage || { total: 0 }, - appTotals: appTotals || {}, + // only show details of app based on actor, aggregate all as others, except if app is global one or null, then show all + const appId = actor.type?.app?.uid + if (appTotals && appId) { + const filteredAppTotals: Record = {}; + let othersTotal: UsageByType | null = null; + Object.entries(appTotals).forEach(([appKey, appUsage]) => { + if (appKey === appId) { + filteredAppTotals[appKey] = appUsage; + } else { + Object.entries(appUsage).forEach(([usageKind, amount]) => { + if (!othersTotal![usageKind]) { + othersTotal![usageKind] = 0; + } + othersTotal![usageKind] += amount; + }) + } + }); + if (othersTotal) { + filteredAppTotals['others'] = othersTotal; + } + return { + usage: usage || { total: 0 }, + appTotals: filteredAppTotals, + } + } else { + return { + usage: usage || { total: 0 }, + appTotals: appTotals || {}, + } } }) } - async getActorCurrentMonthAppUsageDetails(actor: ActorWithType, appId: string) { + async getActorCurrentMonthAppUsageDetails(actor: ActorWithType, appId?: string) { if (!actor.type?.user?.uuid) { throw new Error('Actor must be a user to get usage details'); } + + appId = appId || actor.type?.app?.uid || GLOBAL_APP_KEY; // batch get actor usage, per app usage, and actor app totals for the month const currentMonth = this.#getMonthYearString(); const key = `${METRICS_PREFIX}:actor:${actor.type.user.uuid}:app:${appId}:${currentMonth}` - return this.#superUserService.sudo(async () => { + return await this.#superUserService.sudo(async () => { const usage = await this.#kvClientWrapper.get({ key }) as UsageByType | null; + // only show usage if actor app is the same or if global app ( null appId ) + const actorAppId = actor.type?.app?.uid + if (actorAppId && actorAppId !== appId && appId !== GLOBAL_APP_KEY) { + throw new Error('Actor can only get usage details for their own app or global app'); + } return usage || { total: 0 }; }) } - async getCurrentMonthsConsumedCredit(actor: ActorWithType) { - if (!actor.type?.user?.uuid) { - throw new Error('Actor must be a user to get consumed credits'); - } - const currentMonth = this.#getMonthYearString(); - // batch get actor usage for the month, and actor policy, and actor policy addons to then compute cost - const keys = [ - `${METRICS_PREFIX}:actor:${actor.type.user.uuid}:${currentMonth}`, - `${POLICY_PREFIX}:actor:${actor.type.user.uuid}:addons`, - ] - return this.#superUserService.sudo(async () => { - const [usage, addons] = await this.#kvClientWrapper.get({ key: keys }) as [UsageByType | null, PolicyAddOns | null]; - return usage?.total || 0; - }) - } - - async getActorPolicy(actor: ActorWithType) { + async getActorPolicy(actor: ActorWithType): Promise<(keyof typeof SUB_POLICIES) | null> { if (!actor.type?.user.uuid) { throw new Error('Actor must be a user to get policy'); } const key = `${POLICY_PREFIX}:actor:${actor.type.user.uuid}`; return this.#superUserService.sudo(async () => { const policy = await this.#kvClientWrapper.get({ key }); - policy - return (policy || 'free') as keyof typeof POLICY_TYPES; + return policy as (keyof typeof SUB_POLICIES) || null; }) } @@ -236,7 +245,7 @@ export class MeteringAndBillingService { }) } - handlePolicyPurchase(actor: ActorWithType, policyType: keyof typeof POLICY_TYPES) { + handlePolicyPurchase(actor: ActorWithType, policyType: keyof typeof SUB_POLICIES) { // TODO DS: this should leverage extensions to call billing implementations diff --git a/src/backend/src/services/abuse-prevention/MeteringService/index.mjs b/src/backend/src/services/MeteringService/MeteringServiceWrapper.mjs similarity index 79% rename from src/backend/src/services/abuse-prevention/MeteringService/index.mjs rename to src/backend/src/services/MeteringService/MeteringServiceWrapper.mjs index 63794a663..77a11ef68 100644 --- a/src/backend/src/services/abuse-prevention/MeteringService/index.mjs +++ b/src/backend/src/services/MeteringService/MeteringServiceWrapper.mjs @@ -1,9 +1,9 @@ -import BaseService from '../../BaseService.js'; +import BaseService from '../BaseService.js'; import { MeteringAndBillingService } from "./MeteringService.js"; export class MeteringAndBillingServiceWrapper extends BaseService { - /** @type {import('./MeteringService').MeteringAndBillingService} */ + /** @type {import('./MeteringService.js').MeteringAndBillingService} */ meteringAndBillingService = undefined; _init() { this.meteringAndBillingService = new MeteringAndBillingService({ diff --git a/src/backend/src/services/abuse-prevention/MeteringService/costMaps/awsPollyCostMap.ts b/src/backend/src/services/MeteringService/costMaps/awsPollyCostMap.ts similarity index 100% rename from src/backend/src/services/abuse-prevention/MeteringService/costMaps/awsPollyCostMap.ts rename to src/backend/src/services/MeteringService/costMaps/awsPollyCostMap.ts diff --git a/src/backend/src/services/abuse-prevention/MeteringService/costMaps/awsTextractCostMap.ts b/src/backend/src/services/MeteringService/costMaps/awsTextractCostMap.ts similarity index 100% rename from src/backend/src/services/abuse-prevention/MeteringService/costMaps/awsTextractCostMap.ts rename to src/backend/src/services/MeteringService/costMaps/awsTextractCostMap.ts diff --git a/src/backend/src/services/abuse-prevention/MeteringService/costMaps/claudeCostMap.ts b/src/backend/src/services/MeteringService/costMaps/claudeCostMap.ts similarity index 100% rename from src/backend/src/services/abuse-prevention/MeteringService/costMaps/claudeCostMap.ts rename to src/backend/src/services/MeteringService/costMaps/claudeCostMap.ts diff --git a/src/backend/src/services/abuse-prevention/MeteringService/costMaps/deepSeekCostMap.ts b/src/backend/src/services/MeteringService/costMaps/deepSeekCostMap.ts similarity index 100% rename from src/backend/src/services/abuse-prevention/MeteringService/costMaps/deepSeekCostMap.ts rename to src/backend/src/services/MeteringService/costMaps/deepSeekCostMap.ts diff --git a/src/backend/src/services/abuse-prevention/MeteringService/costMaps/geminiCostMap.ts b/src/backend/src/services/MeteringService/costMaps/geminiCostMap.ts similarity index 100% rename from src/backend/src/services/abuse-prevention/MeteringService/costMaps/geminiCostMap.ts rename to src/backend/src/services/MeteringService/costMaps/geminiCostMap.ts diff --git a/src/backend/src/services/abuse-prevention/MeteringService/costMaps/groqCostMap.ts b/src/backend/src/services/MeteringService/costMaps/groqCostMap.ts similarity index 100% rename from src/backend/src/services/abuse-prevention/MeteringService/costMaps/groqCostMap.ts rename to src/backend/src/services/MeteringService/costMaps/groqCostMap.ts diff --git a/src/backend/src/services/abuse-prevention/MeteringService/costMaps/index.ts b/src/backend/src/services/MeteringService/costMaps/index.ts similarity index 100% rename from src/backend/src/services/abuse-prevention/MeteringService/costMaps/index.ts rename to src/backend/src/services/MeteringService/costMaps/index.ts diff --git a/src/backend/src/services/abuse-prevention/MeteringService/costMaps/kvCostMap.ts b/src/backend/src/services/MeteringService/costMaps/kvCostMap.ts similarity index 100% rename from src/backend/src/services/abuse-prevention/MeteringService/costMaps/kvCostMap.ts rename to src/backend/src/services/MeteringService/costMaps/kvCostMap.ts diff --git a/src/backend/src/services/abuse-prevention/MeteringService/costMaps/mistralCostMap.ts b/src/backend/src/services/MeteringService/costMaps/mistralCostMap.ts similarity index 100% rename from src/backend/src/services/abuse-prevention/MeteringService/costMaps/mistralCostMap.ts rename to src/backend/src/services/MeteringService/costMaps/mistralCostMap.ts diff --git a/src/backend/src/services/abuse-prevention/MeteringService/costMaps/openAiCostMap.ts b/src/backend/src/services/MeteringService/costMaps/openAiCostMap.ts similarity index 100% rename from src/backend/src/services/abuse-prevention/MeteringService/costMaps/openAiCostMap.ts rename to src/backend/src/services/MeteringService/costMaps/openAiCostMap.ts diff --git a/src/backend/src/services/abuse-prevention/MeteringService/costMaps/openaiImageCostMap.ts b/src/backend/src/services/MeteringService/costMaps/openaiImageCostMap.ts similarity index 100% rename from src/backend/src/services/abuse-prevention/MeteringService/costMaps/openaiImageCostMap.ts rename to src/backend/src/services/MeteringService/costMaps/openaiImageCostMap.ts diff --git a/src/backend/src/services/abuse-prevention/MeteringService/costMaps/openrouterCostMap.ts b/src/backend/src/services/MeteringService/costMaps/openrouterCostMap.ts similarity index 100% rename from src/backend/src/services/abuse-prevention/MeteringService/costMaps/openrouterCostMap.ts rename to src/backend/src/services/MeteringService/costMaps/openrouterCostMap.ts diff --git a/src/backend/src/services/abuse-prevention/MeteringService/costMaps/togetherCostMap.ts b/src/backend/src/services/MeteringService/costMaps/togetherCostMap.ts similarity index 100% rename from src/backend/src/services/abuse-prevention/MeteringService/costMaps/togetherCostMap.ts rename to src/backend/src/services/MeteringService/costMaps/togetherCostMap.ts diff --git a/src/backend/src/services/abuse-prevention/MeteringService/costMaps/xaiCostMap.ts b/src/backend/src/services/MeteringService/costMaps/xaiCostMap.ts similarity index 100% rename from src/backend/src/services/abuse-prevention/MeteringService/costMaps/xaiCostMap.ts rename to src/backend/src/services/MeteringService/costMaps/xaiCostMap.ts diff --git a/src/backend/src/services/abuse-prevention/MeteringService/serviceCostMap.ts b/src/backend/src/services/MeteringService/serviceCostMap.ts similarity index 100% rename from src/backend/src/services/abuse-prevention/MeteringService/serviceCostMap.ts rename to src/backend/src/services/MeteringService/serviceCostMap.ts diff --git a/src/backend/src/services/MeteringService/subPolicies/index.ts b/src/backend/src/services/MeteringService/subPolicies/index.ts new file mode 100644 index 000000000..b439ee0fb --- /dev/null +++ b/src/backend/src/services/MeteringService/subPolicies/index.ts @@ -0,0 +1,7 @@ +import { REGISTERED_USER_FREE } from "./registeredUserFreePolicy"; +import { TEMP_USER_FREE } from "./tempUserFreePolicy"; + +export const SUB_POLICIES = { + TEMP_USER_FREE, + REGISTERED_USER_FREE, +} \ No newline at end of file diff --git a/src/backend/src/services/MeteringService/subPolicies/registeredUserFreePolicy.ts b/src/backend/src/services/MeteringService/subPolicies/registeredUserFreePolicy.ts new file mode 100644 index 000000000..b9c49a4a7 --- /dev/null +++ b/src/backend/src/services/MeteringService/subPolicies/registeredUserFreePolicy.ts @@ -0,0 +1,6 @@ +import { toMicroCents } from "../utils"; + +export const REGISTERED_USER_FREE = { + monthUsageAllowence: toMicroCents(0.50), + monthlyStorageAllowence: 100 * 1024 * 1024, // 100MiB +}; \ No newline at end of file diff --git a/src/backend/src/services/MeteringService/subPolicies/tempUserFreePolicy.ts b/src/backend/src/services/MeteringService/subPolicies/tempUserFreePolicy.ts new file mode 100644 index 000000000..69957c5e5 --- /dev/null +++ b/src/backend/src/services/MeteringService/subPolicies/tempUserFreePolicy.ts @@ -0,0 +1,6 @@ +import { toMicroCents } from "../utils"; + +export const TEMP_USER_FREE = { + monthUsageAllowence: toMicroCents(0.25), + monthlyStorageAllowence: 100 * 1024 * 1024, // 100MiB +}; \ No newline at end of file diff --git a/src/backend/src/services/abuse-prevention/MeteringService/utils.ts b/src/backend/src/services/MeteringService/utils.ts similarity index 100% rename from src/backend/src/services/abuse-prevention/MeteringService/utils.ts rename to src/backend/src/services/MeteringService/utils.ts diff --git a/src/backend/src/services/repositories/DBKVStore/DBKVStore.mjs b/src/backend/src/services/repositories/DBKVStore/DBKVStore.mjs index db00a00fd..43b8aabc7 100644 --- a/src/backend/src/services/repositories/DBKVStore/DBKVStore.mjs +++ b/src/backend/src/services/repositories/DBKVStore/DBKVStore.mjs @@ -5,7 +5,7 @@ import { Context } from "../../../util/context.js"; const GLOBAL_APP_KEY = 'global'; export class DBKVStore { #db; - /** @type {import('../../abuse-prevention/MeteringService/MeteringService').MeteringAndBillingService} */ + /** @type {import('../../MeteringService/MeteringService.js').MeteringAndBillingService} */ #meteringService; #global_config = {}; diff --git a/src/puter-js/src/modules/Auth.js b/src/puter-js/src/modules/Auth.js index 2f85f8df1..ec523fc29 100644 --- a/src/puter-js/src/modules/Auth.js +++ b/src/puter-js/src/modules/Auth.js @@ -1,11 +1,10 @@ -import * as utils from '../lib/utils.js' +import * as utils from '../lib/utils.js'; class Auth{ // Used to generate a unique message id for each message sent to the host environment // we start from 1 because 0 is falsy and we want to avoid that for the message id #messageID = 1; - /** * Creates a new instance with the given authentication token, API origin, and app ID, * @@ -14,7 +13,7 @@ class Auth{ * @param {string} APIOrigin - Origin of the API server. Used to build the API endpoint URLs. * @param {string} appID - ID of the app to use. */ - constructor (context) { + constructor(context) { this.authToken = context.authToken; this.APIOrigin = context.APIOrigin; this.appID = context.appID; @@ -27,22 +26,22 @@ class Auth{ * @memberof [Auth] * @returns {void} */ - setAuthToken (authToken) { + setAuthToken(authToken) { this.authToken = authToken; } /** * Sets the API origin. - * + * * @param {string} APIOrigin - The new API origin. * @memberof [Auth] * @returns {void} */ - setAPIOrigin (APIOrigin) { + setAPIOrigin(APIOrigin) { this.APIOrigin = APIOrigin; } - - signIn = (options) =>{ + + signIn = (options) => { options = options || {}; return new Promise((resolve, reject) => { @@ -50,17 +49,17 @@ class Auth{ let w = 600; let h = 600; let title = 'Puter'; - var left = (screen.width/2)-(w/2); - var top = (screen.height/2)-(h/2); - + var left = (screen.width / 2) - (w / 2); + var top = (screen.height / 2) - (h / 2); + // Store reference to the popup window - const popup = window.open(puter.defaultGUIOrigin + '/action/sign-in?embedded_in_popup=true&msg_id=' + msg_id + (window.crossOriginIsolated ? '&cross_origin_isolated=true' : '') +(options.attempt_temp_user_creation ? '&attempt_temp_user_creation=true' : ''), - title, - 'toolbar=no, location=no, directories=no, status=no, menubar=no, scrollbars=no, resizable=no, copyhistory=no, width='+w+', height='+h+', top='+top+', left='+left); + const popup = window.open(`${puter.defaultGUIOrigin}/action/sign-in?embedded_in_popup=true&msg_id=${msg_id}${window.crossOriginIsolated ? '&cross_origin_isolated=true' : ''}${options.attempt_temp_user_creation ? '&attempt_temp_user_creation=true' : ''}`, + title, + `toolbar=no, location=no, directories=no, status=no, menubar=no, scrollbars=no, resizable=no, copyhistory=no, width=${w}, height=${h}, top=${top}, left=${left}`); // Set up interval to check if popup was closed const checkClosed = setInterval(() => { - if (popup.closed) { + if ( popup.closed ) { clearInterval(checkClosed); // Remove the message listener window.removeEventListener('message', messageHandler); @@ -69,21 +68,23 @@ class Auth{ }, 100); function messageHandler(e) { - if(e.data.msg_id == msg_id){ + if ( e.data.msg_id == msg_id ){ // Clear the interval since we got a response clearInterval(checkClosed); - + // remove redundant attributes delete e.data.msg_id; delete e.data.msg; - if(e.data.success){ + if ( e.data.success ){ // set the auth token puter.setAuthToken(e.data.token); resolve(e.data); - }else + } else + { reject(e.data); + } // delete the listener window.removeEventListener('message', messageHandler); @@ -92,20 +93,24 @@ class Auth{ window.addEventListener('message', messageHandler); }); - } + }; - isSignedIn = () =>{ - if(puter.authToken) + isSignedIn = () => { + if ( puter.authToken ) + { return true; + } else + { return false; - } + } + }; getUser = function(...args){ let options; // If first argument is an object, it's the options - if (typeof args[0] === 'object' && args[0] !== null) { + if ( typeof args[0] === 'object' && args[0] !== null ) { options = args[0]; } else { // Otherwise, we assume separate arguments are provided @@ -122,45 +127,125 @@ class Auth{ utils.setupXhrEventHandlers(xhr, options.success, options.error, resolve, reject); xhr.send(); - }) - } + }); + }; - signOut = () =>{ + signOut = () => { puter.resetAuthToken(); - } + }; - async whoami () { + async whoami() { try { - const resp = await fetch(this.APIOrigin + '/whoami', { + const resp = await fetch(`${this.APIOrigin}/whoami`, { headers: { - Authorization: `Bearer ${this.authToken}` - } + Authorization: `Bearer ${this.authToken}`, + }, }); - + const result = await resp.json(); - + // Log the response - if (globalThis.puter?.apiCallLogger?.isEnabled()) { + if ( globalThis.puter?.apiCallLogger?.isEnabled() ) { globalThis.puter.apiCallLogger.logRequest({ service: 'auth', operation: 'whoami', params: {}, - result: result + result: result, }); } - + return result; - } catch (error) { + } catch( error ) { // Log the error - if (globalThis.puter?.apiCallLogger?.isEnabled()) { + if ( globalThis.puter?.apiCallLogger?.isEnabled() ) { globalThis.puter.apiCallLogger.logRequest({ service: 'auth', operation: 'whoami', params: {}, error: { message: error.message || error.toString(), - stack: error.stack - } + stack: error.stack, + }, + }); + } + throw error; + } + } + + async getMonthlyUsage() { + try { + const resp = await fetch(`${this.APIOrigin}/v2/usage`, { + headers: { + Authorization: `Bearer ${this.authToken}`, + }, + }); + + const result = await resp.json(); + + // Log the response + if ( globalThis.puter?.apiCallLogger?.isEnabled() ) { + globalThis.puter.apiCallLogger.logRequest({ + service: 'auth', + operation: 'usage', + params: {}, + result: result, + }); + } + + return result; + } catch( error ) { + // Log the error + if ( globalThis.puter?.apiCallLogger?.isEnabled() ) { + globalThis.puter.apiCallLogger.logRequest({ + service: 'auth', + operation: 'usage', + params: {}, + error: { + message: error.message || error.toString(), + stack: error.stack, + }, + }); + } + throw error; + } + } + + async getDetailedAppUsage(appId) { + if ( !appId ) { + throw new Error('appId is required'); + } + + try { + const resp = await fetch(`${this.APIOrigin}/v2/usage/${appId}`, { + headers: { + Authorization: `Bearer ${this.authToken}`, + }, + }); + + const result = await resp.json(); + + // Log the response + if ( globalThis.puter?.apiCallLogger?.isEnabled() ) { + globalThis.puter.apiCallLogger.logRequest({ + service: 'auth', + operation: 'detailed_app_usage', + params: { appId }, + result: result, + }); + } + + return result; + } catch( error ) { + // Log the error + if ( globalThis.puter?.apiCallLogger?.isEnabled() ) { + globalThis.puter.apiCallLogger.logRequest({ + service: 'auth', + operation: 'detailed_app_usage', + params: { appId }, + error: { + message: error.message || error.toString(), + stack: error.stack, + }, }); } throw error; @@ -168,4 +253,4 @@ class Auth{ } } -export default Auth \ No newline at end of file +export default Auth; \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index 10013eeac..be896fb42 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,8 +1,8 @@ { "compilerOptions": { "target": "ES2022", - "module": "CommonJS", - "moduleResolution": "node10", + "module": "node16", + "moduleResolution": "node16", "rootDir": ".", "strict": true, "esModuleInterop": true, @@ -15,6 +15,7 @@ "**/test/**", "**/tests/**", "node_modules", - "dist" + "dist", + "extensions" ] } \ No newline at end of file