Files
puter/src/backend/services/index.ts
Daniel Salazar 45f6e9d8ee refactor(drivers-wiring): AppService views and launch gates, batched app lookups, shared driver policy and guards (#4186)
* perf(workers): skip hot-reload lookups for files no worker is bound to

The fs.write.file / fs.remove.node / fs.move.node listeners listed every
workers.puter.* row of the file's owner on every event, for every user.
The event's FSEntry already carries the worker rows bound to it, so a file
with none returns before any I/O; a bound file reads just its own rows,
batched, from the primary.

* perf(apps): read the uid alias only after a direct lookup misses

Every AppDriver uid lookup fired an uncached system-KV GetItem for the
merge alias in parallel with the direct read, even when the direct read
hit. The alias is now read only on a miss.

* refactor(apps): one app view with the launch gates, built by AppService

AppDriver's serializer and its private-app / hosted-backing gates move to
AppService.views, built on a pure toAppView that is the one place
index_url is withheld. AppDriver, the subdomains driver's embedded apps and
the GUI shell view all use it. Visibility of protected apps
(filterReadable) moves with it.

Subdomain embeds (associated_app, app_owner) used to return index_url with
no gate; they now withhold it from a viewer who may not open a private app
or whose hosted backing is gone, and carry privateAccess, godmode and
feedback_enabled like a direct app read.

* refactor(apps): one toAppSummary; batch the recommended and taskbar lookups

SuggestedApps, RecommendedApps, /get-launch-apps and the taskbar each built
their own launch summary; they now share toAppSummary (the extra
feedback_enabled / external flags appear on the lists that lacked them).

getRecommendedApps awaited getByName once per recommended app (29) and
getTaskbarItems did the same per taskbar entry on every whoami; both now
make one batched call per key through the new AppStore.getByNames, which
keeps getByName's case-insensitive and old-name matching. The taskbar's
raw UPDATE of the user row goes through UserStore.update, and
/get-launch-apps checks hosted backings in one batch and reaches the
stores without casts.

* refactor(apps): app routes read through AppService; /delete-site calls the subdomains driver

AppController used the global driversContainers through a getter whose
boot-order comment no longer held; it uses the injected drivers and
AppService. GET /apps/:name looked each name up, read it again through the
driver and resolved private access a second time, emitting
app.privateAccess.resolveLaunch twice per private app; it now batches the
names and resolves once. POST /query/app did a lookup, a driver read and a
raw app_filetype_association SELECT per selector (up to 200, serially); it
now makes one batched lookup per key, one grant check and one filetype read.

The driver's read shape is a pure toAppReadView both use.
HostingController's /delete-site re-implemented the subdomains driver's
delete (protected check, delete, subdomain.delete event); it keeps its
owner-scoped 404 and calls the driver. Hosting and Desktop controllers drop
constructors that discarded the drivers argument, and trivial getters.

* refactor(drivers): one per-method policy validator and resolver; drop @Driver

Four policy blocks (rateLimit, concurrent, requireSubscription,
requireReputation) share the { default, methods } shape but each had its
own validator and resolver. They become validatePerMethod(value, label,
policy) and resolvePerMethod(cfg, method); error messages are unchanged.

The @Driver class decorator and the prototype keys it wrote had no users
outside its own tests; drivers declare their fields, and resolveDriverMeta
reads only those. DriverController takes the driver name from the meta it
already resolved instead of probing the prototype.
resolveDriverMethodRateLimit/Concurrent stay as deprecated aliases for the
email extension.

* refactor(drivers): shared actor guards and crud-q concurrency policy

The apps, subdomains, workers and notifications drivers each carried a
private #requireActor, two carried the same #requireVerified, and apps and
subdomains the same owner/app_owner write predicate. These become
requireContextActor, requireContextUserActor, assertActorEmailVerified and
actorOwnsRow in core/actor.ts; each driver keeps its own error for the
cases that differed (system actors still read apps; notifications still
answer 403 for an actor without a user). The identical crud-q concurrency
block is one CRUD_CONCURRENT constant.

The metering, installedApps and appTelemetry extensions re-checked an actor
their routes already require; those checks and the tests for them go.

* refactor(extensions): store methods instead of raw SQL in OSS extensions

installedApps and appTelemetry queried user_to_app_permissions directly
and metering read apps by name with its own SELECT. PermissionStore gains
listAppsGrantedByUser, listAppAuthenticatedUsers (with the email-share
check folded in) and countAppUsers; metering uses AppStore.getByName.

* refactor(server): one layer builder and one lifecycle hook runner

PuterServer built each of its five layers with its own copy of the same
loop (built-ins, then extension additions, into the layer and its export
container), and ran each lifecycle hook with five near-identical loops per
hook. Both are now one helper each: #instantiateLayer and
#runHook(order, hook). Construction order, the args each layer receives,
and hook order (start and prepare bottom-up, shutdown top-down) are
unchanged.

* fix(workers): tear down a deleted source's worker after its FK nulls root_dir_id
2026-10-11 22:04:23 -07:00

163 lines
7.5 KiB
TypeScript

/*
* Copyright (C) 2024-present Puter Technologies Inc.
*
* This file is part of Puter.
*
* Puter is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { AppOriginBlocklistService } from './abuse/AppOriginBlocklistService';
import { ACLService } from './acl/ACLService';
import { AppIconService } from './appIcon/AppIconService';
import { AppPermissionService } from './apps/AppPermissionService';
import { AppService } from './apps/AppService';
import { RecommendedAppsService } from './apps/RecommendedAppsService';
import { SuggestedAppsService } from './apps/SuggestedAppsService';
import { AuthService } from './auth/AuthService';
import { OIDCService } from './auth/OIDCService';
import { TokenService } from './auth/TokenService';
import { BroadcastService } from './broadcast/BroadcastService';
import { CacheReplicationService } from './cache/CacheReplicationService';
import { EventForwardService } from './events/EventForwardService';
import { EventsService } from './events/EventsService';
import { AppFeedbackService } from './feedback/AppFeedbackService';
import { FSService } from './fs/FSService';
import { ServerHealthService } from './health/ServerHealthService';
import { PuterHomepageService } from './homepage/PuterHomepageService';
import { LocalWorkerService } from './localworker/LocalWorkerService';
import { MeteringService } from './metering/MeteringService';
import { NotificationService } from './notification/NotificationService';
import { PermissionService } from './permission/PermissionService';
import { ProfileService } from './profile/ProfileService';
import { DefaultUserService } from './selfhosted/DefaultUserService';
import { ShareNotificationService } from './share/ShareNotificationService';
import { ShareService } from './share/ShareService';
import { TeamService } from './team/TeamService';
import { SocketService } from './socket/SocketService';
import { SubdomainPermissionService } from './subdomain/SubdomainPermissionService';
import type { IPuterServiceRegistry } from './types';
import { SignupService } from './user/SignupService';
import { UserAccountService } from './user/UserAccountService';
/**
* Populate `IPuterServiceInstances` (declared in `./types`) with the concrete
* types of built-in services. Done via declaration merging instead of
* `LayerInstances<typeof puterServices>` because every concrete service extends
* `PuterService`, whose `protected services` field references this type — a
* direct `typeof puterServices` lookup would self-cycle.
*/
declare module './types' {
interface IPuterServiceInstances {
metering: MeteringService;
appOriginBlocklist: AppOriginBlocklistService;
permission: PermissionService;
acl: ACLService;
share: ShareService;
shareNotification: ShareNotificationService;
profile: ProfileService;
token: TokenService;
auth: AuthService;
fs: FSService;
appPermission: AppPermissionService;
subdomainPermission: SubdomainPermissionService;
recommendedApps: RecommendedAppsService;
suggestedApps: SuggestedAppsService;
app: AppService;
socket: SocketService;
events: EventsService;
eventForward: EventForwardService;
notification: NotificationService;
appFeedback: AppFeedbackService;
broadcast: BroadcastService;
cacheReplication: CacheReplicationService;
oidc: OIDCService;
appIcon: AppIconService;
defaultUser: DefaultUserService;
homepage: PuterHomepageService;
health: ServerHealthService;
userAccount: UserAccountService;
team: TeamService;
signup: SignupService;
}
}
// Ordering matters: services declared later see earlier ones as peers.
// ACLService depends on PermissionService (for scan + grant/revoke), so
// PermissionService must be constructed first.
// AuthService depends on TokenService (JWT verify).
// FSService constructs its own internal repo + S3 provider in onServerStart.
// SocketService depends on AuthService (for handshake auth).
// NotificationService depends on notification store (for DB) + event client (for socket push).
// BroadcastService is independent — only needs the event client.
export const puterServices = {
metering: MeteringService,
// Declared before `auth` so AuthService sees it as a prior peer — it
// queries the blocklist on app-token acquisition and per-request app
// token validation.
appOriginBlocklist: AppOriginBlocklistService,
permission: PermissionService,
acl: ACLService,
token: TokenService,
auth: AuthService,
fs: FSService,
// Needs acl (setUserUser), permission (canManagePermission) and fs
// (ancestor chains), so it follows all three.
share: ShareService,
// Delivery only; it reaches `notification` at call time, so its position
// relative to that service does not matter.
shareNotification: ShareNotificationService,
// Reads and writes profile files through `fs` and asks `metering` about
// the owner's plan, so it follows both.
profile: ProfileService,
// Declared after `fs` — account teardown tears the user's filesystem down
// first.
userAccount: UserAccountService,
// Leaf: team + user stores only.
team: TeamService,
// Reaches `team` and `socket` at call time only.
signup: SignupService,
// AppPermissionService + SubdomainPermissionService register permission
// rewriters/implicators only; no runtime state. Placed after fsEntry so
// the FS rewriter runs first for `fs:/path` → `fs:<uuid>` before any
// downstream check that might chain app-root-dir → fs.
appPermission: AppPermissionService,
subdomainPermission: SubdomainPermissionService,
recommendedApps: RecommendedAppsService,
suggestedApps: SuggestedAppsService,
// Reaches `auth` and `permission` at call time only.
app: AppService,
socket: SocketService,
// Delivers through `socket` and resolves paths through `fs`, so it follows
// both; `fs` reaches back for dispatch at call time only.
events: EventsService,
notification: NotificationService,
// Declared after `auth` (origin → app uid resolution happens through
// AuthService.appUidFromOrigin).
appFeedback: AppFeedbackService,
broadcast: BroadcastService,
// Forwards through `broadcast` and puts deliveries down through `socket`,
// so it follows both; `events` reaches it at call time only.
eventForward: EventForwardService,
// Independent — only needs the event client and redis.
cacheReplication: CacheReplicationService,
oidc: OIDCService,
appIcon: AppIconService,
defaultUser: DefaultUserService,
homepage: PuterHomepageService,
// Health comes after socket so its default `socket-initialized`
// check can reference the peer.
health: ServerHealthService,
localworkerservice: LocalWorkerService,
} satisfies IPuterServiceRegistry;