Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions extensions/api.d.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@

import type APIError from '@heyputer/backend/src/api/APIError.js';
import type query from '@heyputer/backend/src/om/query/query';
import type { Actor } from '@heyputer/backend/src/services/auth/Actor.js';
Expand Down Expand Up @@ -32,7 +33,7 @@ declare global {

export type { Cluster } from 'ioredis';

interface EndpointOptions {
export interface EndpointOptions {
allowedMethods?: string[];
subdomain?: string;
noauth?: boolean;
Expand All @@ -57,7 +58,16 @@ interface DriverInterface {
methods: Record<string, MethodDefinition>;
}

type HttpMethod = 'get' | 'post' | 'put' | 'delete' | 'patch';
export type HttpMethod = 'get' | 'post' | 'put' | 'delete' | 'patch';

export type ExtensionRequestHandler = RequestHandler<
Record<string, string | undefined>,
unknown,
unknown
>;
export type ExtensionRequest = Parameters<ExtensionRequestHandler>[0];
export type ExtensionResponse = Parameters<ExtensionRequestHandler>[1];
export type ExtensionNextFunction = Parameters<ExtensionRequestHandler>[2];

export type AddRouteFunction = (
path: string,
Expand Down
79 changes: 42 additions & 37 deletions extensions/extensionController/src/ExtensionController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,43 +131,48 @@ export class ExtensionController {
logger.log(`Registering route: [${route.method.toUpperCase()}] ${fullPath}`);

(extension[route.method] as RouterMethods[HttpMethod])(
fullPath,
route.options || {},
async (req, res, next) => {
try {
if ( adminsForRoute || allowedAppIds ) {
if ( ! req.actor ) {
throw new HttpError(StatusCodes.UNAUTHORIZED, 'Unauthenticated');
}
}
if ( adminsForRoute ) {
if ( ! adminsForRoute.includes(req.actor!.type.user.username) ) {
throw new HttpError(StatusCodes.FORBIDDEN,
'Only admins may request this resource.');
}
}
if ( allowedAppIds ) {
if ( ( req.actor!.type?.app?.uid && !allowedAppIds.includes(req.actor!.type.app.uid) ) ) {
throw new HttpError(StatusCodes.FORBIDDEN,
'This app may not request this resource.');
}
}
await route.handler.bind(this)(req, res, next);
} catch ( error ) {
if ( error instanceof HttpError ) {
res.status(error.statusCode).send({ error: error.message });
logger.warn('httpError:', error);
return;
}
if ( error instanceof Error ) {
res.status(StatusCodes.INTERNAL_SERVER_ERROR).send({ error: error.message });
logger.error('Non-http error:', error);
return;
}
res.status(StatusCodes.INTERNAL_SERVER_ERROR).send({ error: 'An unknown error occurred' });
logger.error('An unknown error occurred:', error);
}
});
fullPath,
route.options || {},
async (req, res, next) => {
try {
if ( adminsForRoute || allowedAppIds ) {
if ( ! req.actor ) {
throw new HttpError(StatusCodes.UNAUTHORIZED, 'Unauthenticated');
}
}
if ( adminsForRoute ) {
if ( ! adminsForRoute.includes(req.actor!.type.user.username) ) {
throw new HttpError(
StatusCodes.FORBIDDEN,
'Only admins may request this resource.',
);
}
}
if ( allowedAppIds ) {
if ( ( req.actor!.type?.app?.uid && !allowedAppIds.includes(req.actor!.type.app.uid) ) ) {
throw new HttpError(
StatusCodes.FORBIDDEN,
'This app may not request this resource.',
);
}
}
await route.handler.bind(this)(req, res, next);
} catch ( error ) {
if ( error instanceof HttpError ) {
res.status(error.statusCode).send({ error: error.message });
logger.warn('httpError:', error);
return;
}
if ( error instanceof Error ) {
res.status(StatusCodes.INTERNAL_SERVER_ERROR).send({ error: error.message });
logger.error('Non-http error:', error);
return;
}
res.status(StatusCodes.INTERNAL_SERVER_ERROR).send({ error: 'An unknown error occurred' });
logger.error('An unknown error occurred:', error);
}
},
);
}
}
}
Expand Down
27 changes: 20 additions & 7 deletions extensions/metering/controllers/UsageController.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
/* global extension */
import type { BaseDatabaseAccessService } from '@heyputer/backend/src/services/database/BaseDatabaseAccessService.js';
import type { MeteringService } from '@heyputer/backend/src/services/MeteringService/MeteringService.js';
import type {
ExtensionRequest,
ExtensionResponse,
} from '../../api.d.ts';

const { Controller, Get, ExtensionController } = extension.import('extensionController');

Expand All @@ -18,7 +23,7 @@ export class UsageController extends ExtensionController {
}

@Get('usage', { subdomain: 'api' })
async getUsage (req, res) {
async getUsage (req: ExtensionRequest, res: ExtensionResponse) {
const actor = req.actor;
if ( ! actor ) {
throw Error('actor not found in context');
Expand All @@ -35,7 +40,7 @@ export class UsageController extends ExtensionController {
}

@Get('usage/:appIdOrName', { subdomain: 'api' })
async getUsageByApp (req, res) {
async getUsageByApp (req: ExtensionRequest, res: ExtensionResponse) {
const actor = req.actor;
if ( ! actor ) {
throw Error('actor not found in context');
Expand All @@ -45,12 +50,18 @@ export class UsageController extends ExtensionController {
res.status(400).json({ error: 'appId parameter is required' });
return;
}
if ( typeof appIdOrName !== 'string' ) {
res.status(400).json({ error: 'appId parameter must be a string' });
return;
}

let appId = appIdOrName;
if ( !appIdOrName.startsWith('app-') || !/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(appIdOrName.split('app-')[1]) ) {
// Check if the part after 'app-' is a valid UUID (v4)
const appRows = await this.#sqlClient.read('SELECT `uid` FROM `apps` WHERE `name` = ? LIMIT 1',
[appIdOrName]);
const appRows = await this.#sqlClient.read(
'SELECT `uid` FROM `apps` WHERE `name` = ? LIMIT 1',
[appIdOrName],
);
if ( appRows.length > 0 ) {
appId = appRows[0].uid;
} else {
Expand All @@ -62,15 +73,17 @@ export class UsageController extends ExtensionController {
}

const appUsage =
await this.#meteringService.getActorCurrentMonthAppUsageDetails(actor,
appId);
await this.#meteringService.getActorCurrentMonthAppUsageDetails(
actor,
appId,
);

res.status(200).json(appUsage);
return;
}

@Get('globalUsage', { subdomain: 'api' }, extension.config.allowedGlobalUsageUsers || [])
async getGlobalUsage (req, res) {
async getGlobalUsage (req: ExtensionRequest, res: ExtensionResponse) {
const actor = req.actor;
if ( ! actor ) {
throw Error('actor not found in context');
Expand Down
9 changes: 7 additions & 2 deletions extensions/serverInfo/index.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
/* global config, extension */
import fs from 'fs/promises';
import os from 'os';
import type {
ExtensionRequest,
ExtensionResponse,
} from '../api.d.ts';
const { Controller, Get, ExtensionController } = extension.import('extensionController');

@Controller('/serverInfo', [...config.allowedUsernames])
class ServerInfoController extends ExtensionController {
@Get('', { subdomain: 'api' })
async getServerInfo (req, res) {
async getServerInfo (_req: ExtensionRequest, res: ExtensionResponse) {
const osData = {
platform: os.platform(),
type: os.type(),
Expand Down Expand Up @@ -60,4 +65,4 @@ class ServerInfoController extends ExtensionController {
}
}

(new ServerInfoController()).registerRoutes();
(new ServerInfoController()).registerRoutes();
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions src/puter-js/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion src/puter-js/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@heyputer/puter.js",
"version": "2.2.10",
"version": "2.2.11",
"description": "Puter.js - A JavaScript library for interacting with Puter services.",
"homepage": "https://developer.puter.com",
"main": "src/index.js",
Expand Down
1 change: 0 additions & 1 deletion src/puter-js/src/init.cjs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
const { readFileSync } = require('node:fs');
const vm = require('node:vm');
const { resolve } = require('node:path');
const { IncomingMessage } = require('node:http');
const open = require('open');
/**
* Method for loading puter.js in Node.js environment with auth token
Expand Down
4 changes: 4 additions & 0 deletions src/puter-js/src/init.d.cts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import type { Puter } from '../types/puter.d.ts';

export declare function init(authToken?: string): Puter;
export declare function getAuthToken(guiOrigin?: string): Promise<string>;
Loading