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
34 changes: 32 additions & 2 deletions extensions/api.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ import type { RequestHandler } from 'express';
import type FSNodeContext from '../src/backend/src/filesystem/FSNodeContext.js';
import type helpers from '../src/backend/src/helpers.js';
import type * as ExtensionControllerExports from './ExtensionController/src/ExtensionController.ts';
import { Context } from '@heyputer/backend/src/util/context.js';
import config from '../volatile/config/config.json'
import APIError from '@heyputer/backend/src/api/APIError.js';
import query from '@heyputer/backend/src/om/query/query';

declare global {
namespace Express {
interface Request {
Expand All @@ -34,6 +39,24 @@ interface EndpointOptions {
}
}

// Driver interface types
type ParameterDefinition = {
type: 'string' | 'number' | 'boolean' | 'object' | 'array';
optional: boolean;
};
type MethodDefinition = {
description: string;
parameters: Record<string, ParameterDefinition>;
};
type DriverInterface = {
description: string;
methods: Record<string, MethodDefinition>;
};





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

export type AddRouteFunction = (path: string, options: EndpointOptions, handler: RequestHandler) => void;
Expand All @@ -49,6 +72,8 @@ interface CoreRuntimeModule {
util: {
helpers: typeof helpers,
}
Context: typeof Context,
APIError: typeof APIError
}

interface FilesystemModule {
Expand All @@ -72,10 +97,15 @@ interface Extension extends RouterMethods {
run<T>(label: string, fn: () => T): T;
run<T>(fn: () => T): T;
},
config: Record<string | number | symbol, any>,
on<T extends unknown[]>(event: string, listener: (...args: T) => void): void, // TODO DS: type events better
import(module: 'data'): { db: BaseDatabaseAccessService, kv: DBKVStore, cache: unknown }// TODO DS: type cache better
on(event: 'create.drivers', listener: (event: {createDriver: (interface: string, service: string, executors: any)=>any}) => void),
on(event: 'create.permissions', listener: (event: {grant_to_everyone: (permission: string) => void, grant_to_users: (permission: string) => void})=>void)
on(event: 'create.interfaces', listener: (event: {createInterface: (interface: string, interfaces: DriverInterface) => void}) => void)
import(module: 'data'): { db: BaseDatabaseAccessService, kv: DBKVStore & {get: (string) => void, set: (string, string) => void}, cache: unknown }// TODO DS: type cache better
import(module: 'core'): CoreRuntimeModule,
import(module: 'fs'): FilesystemModule,
import(module: 'query'): typeof query,
import(module: 'extensionController'): typeof ExtensionControllerExports
import<T extends `service:${keyof ServiceNameMap}` | (string & {})>(module: T): T extends `service:${infer R extends keyof ServiceNameMap}`
? ServiceNameMap[R]
Expand All @@ -85,6 +115,6 @@ interface Extension extends RouterMethods {
declare global {
// Declare the extension variable
const extension: Extension;
const config: Record<string | number | symbol, unknown>;
const config: Record<string | number | symbol, any>;
const global_config: Record<string | number | symbol, unknown>;
}
81 changes: 81 additions & 0 deletions extensions/app-telemetry/app-user-count.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
const { Eq } = extension.import('query')
const { kv } = extension.import('data');
const span = extension.span;
const { db } = extension.import('data');
const { Context, APIError } = extension.import('core');
const app_es: any = extension.import('service:es:app');

extension.on('create.interfaces', (event) => {
event.createInterface('app-telemetry', {
description: 'Provides methods for getting app telemetry',
methods: {
get_users: {
description: 'Returns users who have used your app',
parameters: {
app_uuid: {
type: 'string',
optional: false,
},
limit: {
type: 'number',
optional: true,
},
offset: {
type: 'number',
optional: true
}
},
},
user_count: {
description: 'Returns number of users who have used your app',
parameters: {
app_uuid: {
type: 'string',
optional: false,
}
},
}
},
});
});

extension.on('create.drivers', event => {
event.createDriver('app-telemetry', 'app-telemetry', {
async get_users({ app_uuid, limit = 100, offset = 0 }: {app_uuid: string, limit: number, offset: number}) {
// first lets make sure executor owns this app
const [result] = (await app_es.select({ predicate: new Eq({ key: 'uid', value: app_uuid }) }));
if (!result) {
throw APIError.create('permission_denied');
}

// Fetch and return users
const users: Array<{username: string, uuid: string}> = await db.read(
`SELECT user.username, user.uuid FROM user_to_app_permissions
INNER JOIN user ON user_to_app_permissions.user_id = user.id
WHERE permission = 'flag:app-is-authenticated' AND app_id=? ORDER BY (dt IS NOT NULL), dt, user_id LIMIT ? OFFSET ?`,
[result.private_meta.mysql_id, limit, offset],
);
return users.map(e=>{return {user: e.username, user_uuid: e.uuid}});
},
async user_count({ app_uuid }: {app_uuid: string}) {
// first lets make sure executor owns this app
const [result] = (await app_es.select({ predicate: new Eq({ key: 'uid', value: app_uuid }) }));
if (!result) {
throw APIError.create('permission_denied');
}

// Fetch and return authenticated user count
const [data] = await db.read(
`SELECT count(*) FROM user_to_app_permissions
WHERE permission = 'flag:app-is-authenticated' AND app_id=?;`,
[result.private_meta.mysql_id],
);
const count = data['count(*)'];
return count;
}
});
});

extension.on('create.permissions', (event) => {
event.grant_to_everyone('service:app-telemetry:ii:app-telemetry');
});
5 changes: 5 additions & 0 deletions extensions/app-telemetry/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"name": "@heyputer/app-telemetry",
"main": "app-user-count.ts",
"type": "module"
}
6 changes: 6 additions & 0 deletions src/backend/src/CoreModule.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ const { TDetachable } = require('@heyputer/putility/src/traits/traits.js');
const { MultiDetachable } = require('@heyputer/putility/src/libs/listener.js');
const { OperationFrame } = require('./services/OperationTraceService');
const opentelemetry = require('@opentelemetry/api');
const query = require('./om/query/query');

/**
* @footgun - real install method is defined above
Expand Down Expand Up @@ -86,6 +87,11 @@ const install = async ({ context, services, app, useapi, modapi }) => {
context.get('runtime-modules').register(runtimeModule);
runtimeModule.exports = useapi.use('core');
}
{
const runtimeModule = new RuntimeModule({ name: 'query' });
context.get('runtime-modules').register(runtimeModule);
runtimeModule.exports = query;
}

// Extension module: 'tel'
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,17 @@ export const OPENAI_IMAGE_COST_MAP = {
'openai:dall-e-2:512x512': toMicroCents(0.018), // $0.018
'openai:dall-e-2:256x256': toMicroCents(0.016), // $0.016

// gpt-image-1.5
'openai:gpt-image-1.5:low:1024x1024': toMicroCents(0.009),
'openai:gpt-image-1.5:low:1024x1536': toMicroCents(0.013),
'openai:gpt-image-1.5:low:1536x1024': toMicroCents(0.013),
'openai:gpt-image-1.5:medium:1024x1024': toMicroCents(0.034),
'openai:gpt-image-1.5:medium:1024x1536': toMicroCents(0.051),
'openai:gpt-image-1.5:medium:1536x1024': toMicroCents(0.05),
'openai:gpt-image-1.5:high:1024x1024': toMicroCents(0.133),
'openai:gpt-image-1.5:high:1024x1536': toMicroCents(0.20),
'openai:gpt-image-1.5:high:1536x1024': toMicroCents(0.199),

// gpt-image-1
'openai:gpt-image-1:low:1024x1024': toMicroCents(0.011),
'openai:gpt-image-1:low:1024x1536': toMicroCents(0.016),
Expand Down
2 changes: 1 addition & 1 deletion src/backend/src/services/User.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,6 @@ export interface IUser {
uuid: string,
username: string,
email?: string,
subscription?: (typeof SUB_POLICIES)[number]['id'],
subscription?: (typeof SUB_POLICIES)[number]['id'] & {active: boolean, tier: string},
metadata?: Record<string, unknown> & { hasDevAccountAccess?: boolean }
}
Original file line number Diff line number Diff line change
Expand Up @@ -139,15 +139,20 @@ export class OpenAiImageGenerationProvider implements IImageProvider {
return url;
}

#isGptImageModel (model: string) {
// Covers gpt-image-1, gpt-image-1-mini, gpt-image-1.5 and future variants.
return model.startsWith('gpt-image-1');
}

#buildPriceKey (model: string, quality: string, size: string) {
if ( model === 'gpt-image-1' || model === 'gpt-image-1-mini' ) {
// gpt-image-1 and gpt-image-1-mini use format: "quality:size" - default to low if not specified
if ( this.#isGptImageModel(model) ) {
// GPT image models use format: "quality:size" - default to low if not specified
const qualityLevel = quality || 'low';
return `${qualityLevel}:${size}`;
} else {
// dall-e models use format: "hd:size" or just "size"
return (quality === 'hd' ? 'hd:' : '') + size;
}

// DALL-E models use format: "hd:size" or just "size"
return (quality === 'hd' ? 'hd:' : '') + size;
}

#buildApiParams (model: string, baseParams: Partial<ImageGenerateParamsNonStreaming>): ImageGenerateParamsNonStreaming {
Expand All @@ -157,8 +162,8 @@ export class OpenAiImageGenerationProvider implements IImageProvider {
size: baseParams.size,
} as ImageGenerateParamsNonStreaming;

if ( model === 'gpt-image-1' || model === 'gpt-image-1-mini' ) {
// gpt-image-1 requires the model parameter and uses different quality mapping
if ( this.#isGptImageModel(model) ) {
// GPT image models require the model parameter and use quality mapping
apiParams.model = model;
// Default to low quality if not specified, consistent with _buildPriceKey
apiParams.quality = baseParams.quality || 'low';
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,25 @@
import { IImageModel } from '../types';

export const OPEN_AI_IMAGE_GENERATION_MODELS: IImageModel[] = [

{ id: 'gpt-image-1.5',
name: 'GPT Image 1.5',
version: '1.5',
costs_currency: 'usd-cents',
index_cost_key: 'low:1024x1024',
costs: {
'low:1024x1024': 0.9,
'low:1024x1536': 1.3,
'low:1536x1024': 1.3,
'medium:1024x1024': 3.4,
'medium:1024x1536': 5.1,
'medium:1536x1024': 5,
'high:1024x1024': 13.3,
'high:1024x1536': 20,
'high:1536x1024': 19.9,
},
allowedQualityLevels: ['low', 'medium', 'high'],
allowedRatios: [{ w: 1024, h: 1024 }, { w: 1024, h: 1536 }, { w: 1536, h: 1024 }],
},
{ id: 'gpt-image-1-mini',
name: 'GPT Image 1 Mini',
version: '1.0',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,12 @@ class SqliteDatabaseAccessService extends BaseDatabaseAccessService {
[37, [
'0041_add_unique_constraint_user_uuid.sql',
]],
[38, [
'0042_add_cloudflare_d1.sql',
]],
[39, [
'0043_add_dt.sql',
]],
];

// Database upgrade logic
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ALTER TABLE `subdomains` ADD COLUMN `database_id` varchar(40) DEFAULT NULL;
22 changes: 22 additions & 0 deletions src/backend/src/services/database/sqlite_setup/0043_add_dt.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
PRAGMA foreign_keys = OFF;

CREATE TABLE user_to_app_permissions_new (
user_id INTEGER NOT NULL,
app_id INTEGER NOT NULL,
permission VARCHAR(255) NOT NULL,
extra JSON DEFAULT NULL,
dt DATETIME DEFAULT CURRENT_TIMESTAMP,

FOREIGN KEY (user_id) REFERENCES user(id) ON DELETE CASCADE ON UPDATE CASCADE,
FOREIGN KEY (app_id) REFERENCES apps(id) ON DELETE CASCADE ON UPDATE CASCADE,
PRIMARY KEY (user_id, app_id, permission)
);

INSERT INTO user_to_app_permissions_new (user_id, app_id, permission, extra, dt)
SELECT user_id, app_id, permission, extra, NULL
FROM user_to_app_permissions;

DROP TABLE user_to_app_permissions;
ALTER TABLE user_to_app_permissions_new RENAME TO user_to_app_permissions;

PRAGMA foreign_keys = ON;
23 changes: 22 additions & 1 deletion src/puter-js/src/modules/Apps.js
Original file line number Diff line number Diff line change
Expand Up @@ -159,8 +159,29 @@ class Apps {
if ( typeof args[0] === 'object' && args[0] !== null ) {
options.params = args[0];
}
const app = await utils.make_driver_method(['uid'], 'puter-apps', undefined, 'read').call(this, options);
app.getUsers = async (params) => {
params = params ?? {};
return (await puter.drivers.call('app-telemetry', 'app-telemetry', 'get_users', { app_uuid: app.uid, limit: params.limit, offset: params.offset })).result;
}
app.users = async function* (pageSize = 100) {
let offset = 0;

while (true) {
const users = await app.getUsers({ limit: pageSize, offset });

if (!users || users.length === 0) return;

for (const user of users) {
yield user;
}

offset += users.length;
if (users.length < pageSize) return;
}
}
return app;

return utils.make_driver_method(['uid'], 'puter-apps', undefined, 'read').call(this, options);
};

delete = async (...args) => {
Expand Down