,
/**
* Extension-declared routes. Shape matches the controller-layer
* `RouteDescriptor`, so both flow through the same materializer
* (`PuterServer#materializeRoute`) and inherit the same options →
* middleware translation (subdomain, auth, body parsers, ...).
*/
routeHandlers: [] as RouteDescriptor[],
};
/**
* Internal: normalize `(path, handler)` or `(path, options, handler)` into
* a single `RouteDescriptor` the server can materialize.
*/
const pushRoute = (
method: RouteMethod,
path: RoutePath,
optionsOrHandler: RouteOptions | RequestHandler,
maybeHandler?: RequestHandler,
): void => {
const handler =
typeof optionsOrHandler === 'function'
? optionsOrHandler
: maybeHandler;
const options =
typeof optionsOrHandler === 'function' ? {} : optionsOrHandler;
if (!handler) {
throw new Error(
`extension.${method}('${String(path)}', ...) missing handler`,
);
}
extensionStore.routeHandlers.push({ method, path, options, handler });
};
interface ExtensionRouteFn {
(path: RoutePath, handler: RequestHandler): void;
(path: RoutePath, options: RouteOptions, handler: RequestHandler): void;
}
const makeRouteFn = (method: RouteMethod): ExtensionRouteFn => {
return ((
path: RoutePath,
optionsOrHandler: RouteOptions | RequestHandler,
maybeHandler?: RequestHandler,
) => {
pushRoute(method, path, optionsOrHandler, maybeHandler);
}) as ExtensionRouteFn;
};
/**
* `extension.use` mirrors `app.use` and supports three shapes:
* use(handler)
* use(options, handler)
* use(path, handler)
* use(path, options, handler)
* Pathless calls register global middleware — the server materializer
* drops the path when calling `app.use` (see `RouteDescriptor.path?`).
*/
interface ExtensionUseFn {
(handler: RequestHandler): void;
(options: RouteOptions, handler: RequestHandler): void;
(path: RoutePath, handler: RequestHandler): void;
(path: RoutePath, options: RouteOptions, handler: RequestHandler): void;
}
const isRequestHandler = (v: unknown): v is RequestHandler =>
typeof v === 'function';
const isRoutePath = (v: unknown): v is RoutePath =>
typeof v === 'string' || v instanceof RegExp || Array.isArray(v);
const makeUseFn = (): ExtensionUseFn => {
return ((
a: RoutePath | RouteOptions | RequestHandler,
b?: RouteOptions | RequestHandler,
c?: RequestHandler,
): void => {
let path: RoutePath | undefined;
let options: RouteOptions = {};
let handler: RequestHandler | undefined;
if (isRoutePath(a)) {
path = a;
if (isRequestHandler(b)) {
handler = b;
} else {
options = (b as RouteOptions) ?? {};
handler = c;
}
} else if (isRequestHandler(a)) {
handler = a;
} else {
options = (a as RouteOptions) ?? {};
handler = isRequestHandler(b) ? b : undefined;
}
if (!handler) {
throw new Error('extension.use(...) missing handler');
}
extensionStore.routeHandlers.push({
method: 'use',
...(path !== undefined ? { path } : {}),
options,
handler,
});
}) as ExtensionUseFn;
};
/**
* Global `extension` API available inside every dynamically-loaded extension
* module. Exposes:
*
* - Registry writers: `registerClient`, `registerStore`, `registerService`,
* `registerController`, `registerDriver`.
* - Event subscription: `on(event, handler)`.
* - Imperative route registration: `get`, `post`, `put`, `delete`, `patch`,
* `head`, `options`, `all`, `use`. Each accepts the same `RouteOptions`
* vocabulary used by controllers (subdomain, requireAuth, bodyJson, …)
* so extension routes get identical gate + parser treatment.
* - Back-reference lookup: `import('service:foo')` / `'client:bar'` /
* `'store:baz'` / `'controller:qux'` / `'driver:fred'` — returns a lazy
* proxy to the registered instance (thrown on use-before-init).
*/
export const extension = {
// ── Config access ───────────────────────────────────────────────
//
// Lazy proxy to the server config. Populated by PuterServer during
// boot, so extensions can read it at request time (not import time).
get config(): IConfig {
return configContainer;
},
// ── Event subscription ───────────────────────────────────────────
on: (
key: P,
callback: (
key: MatchingEvents
,
data: EventMap[MatchingEvents
],
meta: EventMetadata,
) => Promise | void,
) => {
if (!extensionStore.events[key]) {
extensionStore.events[key] = [];
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
extensionStore.events[key].push(callback as any);
},
// ── Registry writers ─────────────────────────────────────────────
registerClient: (
name: string,
client: IPuterClientRegistry[keyof IPuterClientRegistry],
) => {
extensionStore.clients[name] = client;
},
registerStore: (
name: string,
store: IPuterStoreRegistry[keyof IPuterStoreRegistry],
) => {
extensionStore.stores[name] = store;
},
registerService: (
name: string,
service: IPuterServiceRegistry[keyof IPuterServiceRegistry],
) => {
extensionStore.services[name] = service;
},
registerController: (
name: string,
controller: IPuterControllerRegistry[keyof IPuterControllerRegistry],
) => {
extensionStore.controllers[name] = controller;
},
registerDriver: (
name: string,
driver: IPuterDriverRegistry[keyof IPuterDriverRegistry],
) => {
extensionStore.drivers[name] = driver;
},
registerGlobalMiddleware: (middleware: RequestHandler) => {
extensionStore.globalMiddlewares.push(middleware);
},
// ── Route registration ───────────────────────────────────────────
//
// Supports two call shapes per verb:
// extension.get('/path', handler)
// extension.get('/path', options, handler)
//
// The `options` object is the same `RouteOptions` shape controllers use,
// so everything that works on a controller route (subdomain, requireAuth,
// requireUserActor, adminOnly, allowedAppIds, middleware, bodyJson,
// bodyRaw, bodyText, bodyUrlencoded) works here identically.
get: makeRouteFn('get'),
post: makeRouteFn('post'),
put: makeRouteFn('put'),
delete: makeRouteFn('delete'),
patch: makeRouteFn('patch'),
head: makeRouteFn('head'),
options: makeRouteFn('options'),
all: makeRouteFn('all'),
use: makeUseFn(),
// ── Import proxy ─────────────────────────────────────────────────
import: (
name: S,
): S extends 'client' | 'clients'
? LayerInstances & IExtensionClientInstances
: S extends 'store' | 'stores'
? LayerInstances & IExtensionStoreInstances
: S extends 'service' | 'services'
? LayerInstances & IExtensionServiceInstances
: S extends 'controller' | 'controllers'
? LayerInstances &
IExtensionControllerInstances
: S extends 'driver' | 'drivers'
? LayerInstances &
IExtensionDriverInstances
: never => {
switch (name) {
case 'clients':
case 'client': {
const proxyHandler = {
get: (_target: object, prop: string) => {
const proxiedObj = clientsContainers[prop];
if (!proxiedObj) {
const proxyProxyHandler = {
get: (_target2: object, prop2: string) => {
const proxiedObj2 =
// @ts-expect-error any type needed
clientsContainers[prop][prop2];
if (!proxiedObj2) {
throw new Error(
`extension.import('client:${prop}') missing property '${prop2}'`,
);
}
return proxiedObj2;
},
};
return new Proxy({}, proxyProxyHandler) as object;
}
return proxiedObj;
},
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return new Proxy({}, proxyHandler) as any;
}
case 'stores':
case 'store': {
const proxyHandler = {
get: (_target: object, prop: string) => {
const proxiedObj = storesContainers[prop];
if (!proxiedObj) {
const proxyProxyHandler = {
get: (_target2: object, prop2: string) => {
const proxiedObj2 =
// @ts-expect-error any type needed
clientsContainers[prop][prop2];
if (!proxiedObj2) {
throw new Error(
`extension.import('client:${prop}') missing property '${prop2}'`,
);
}
return proxiedObj2;
},
};
return new Proxy({}, proxyProxyHandler) as object;
}
return proxiedObj;
},
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return new Proxy({}, proxyHandler) as any;
}
case 'services':
case 'service': {
const proxyHandler = {
get: (_target: object, prop: string) => {
const proxiedObj = servicesContainers[prop];
if (!proxiedObj) {
const proxyProxyHandler = {
get: (_target2: object, prop2: string) => {
const proxiedObj2 =
// @ts-expect-error any type needed
clientsContainers[prop][prop2];
if (!proxiedObj2) {
throw new Error(
`extension.import('client:${prop}') missing property '${prop2}'`,
);
}
return proxiedObj2;
},
};
return new Proxy({}, proxyProxyHandler) as object;
}
return proxiedObj;
},
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return new Proxy({}, proxyHandler) as any;
}
case 'controllers':
case 'controller': {
const proxyHandler = {
get: (_target: object, prop: string) => {
const proxiedObj = controllersContainers[prop];
if (!proxiedObj) {
const proxyProxyHandler = {
get: (_target2: object, prop2: string) => {
const proxiedObj2 =
// @ts-expect-error any type needed
clientsContainers[prop][prop2];
if (!proxiedObj2) {
throw new Error(
`extension.import('client:${prop}') missing property '${prop2}'`,
);
}
return proxiedObj2;
},
};
return new Proxy({}, proxyProxyHandler) as object;
}
return proxiedObj;
},
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return new Proxy({}, proxyHandler) as any;
}
case 'drivers':
case 'driver': {
const proxyHandler = {
get: (_target: object, prop: string) => {
const proxiedObj = driversContainers[prop];
if (!proxiedObj) {
const proxyProxyHandler = {
get: (_target2: object, prop2: string) => {
const proxiedObj2 =
// @ts-expect-error any type needed
clientsContainers[prop][prop2];
if (!proxiedObj2) {
throw new Error(
`extension.import('client:${prop}') missing property '${prop2}'`,
);
}
return proxiedObj2;
},
};
return new Proxy({}, proxyProxyHandler) as object;
}
return proxiedObj;
},
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return new Proxy({}, proxyHandler) as any;
}
default:
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return undefined as any;
}
},
};