@everystack/server 0.2.14 → 0.2.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/plugin.ts +46 -52
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@everystack/server",
3
- "version": "0.2.14",
3
+ "version": "0.2.15",
4
4
  "description": "Server runtime primitives for Lambda — event adapters, routing, SSR, image processing",
5
5
  "license": "AGPL-3.0-only",
6
6
  "publishConfig": {
package/src/plugin.ts CHANGED
@@ -111,6 +111,14 @@ export interface PluginLambdaHandlerOptions {
111
111
  actions?: Record<string, ActionHandler>;
112
112
  /** Override default Cache-Control headers */
113
113
  cache?: ServerCacheConfig;
114
+ /**
115
+ * Serve HTTP. Default true. Set false for an operator Lambda (server/ops.ts):
116
+ * routes are not built and HTTP-shaped events are rejected with 403 — the
117
+ * function answers `_action` invokes only. IAM (lambda:InvokeFunction) is the
118
+ * auth layer. Use with createAdminDb() so the privileged credential lives
119
+ * here, not in the HTTP-facing API function.
120
+ */
121
+ http?: boolean;
114
122
  }
115
123
 
116
124
  // --- Plugin Lambda Handler Implementation ---
@@ -118,9 +126,10 @@ export interface PluginLambdaHandlerOptions {
118
126
  export function createPluginLambdaHandler(
119
127
  options: PluginLambdaHandlerOptions
120
128
  ): (event: APIGatewayProxyEventV2 | Record<string, unknown>) => Promise<APIGatewayProxyStructuredResultV2 | unknown> {
129
+ const httpEnabled = options.http !== false;
121
130
  let cached: {
122
131
  ctx: PluginContext;
123
- router: (path: string, method: string) => Handler;
132
+ router?: (path: string, method: string) => Handler;
124
133
  actions: Record<string, ActionHandler>;
125
134
  logSink?: LogSink;
126
135
  } | null = null;
@@ -136,11 +145,6 @@ export function createPluginLambdaHandler(
136
145
  contributions.push(await plugin(ctx));
137
146
  }
138
147
 
139
- // Collect routes (plugin order = priority)
140
- const pluginRoutes = contributions.flatMap(c => c.routes ?? []);
141
- const appRoutes = options.routes?.(ctx) ?? [];
142
- const allRoutes = [...pluginRoutes, ...appRoutes];
143
-
144
148
  // Collect actions (app-level takes precedence on collision)
145
149
  const pluginActions: Record<string, ActionHandler> = {};
146
150
  for (const contribution of contributions) {
@@ -150,6 +154,20 @@ export function createPluginLambdaHandler(
150
154
  }
151
155
  const allActions = { ...pluginActions, ...(options.actions ?? {}) };
152
156
 
157
+ // Operator mode (http: false) — actions only. Skip routing entirely;
158
+ // HTTP-shaped events are rejected below. Routes contributed by plugins
159
+ // are intentionally ignored.
160
+ if (!httpEnabled) {
161
+ cached = { ctx, actions: allActions };
162
+ log('info', 'Plugin handlers initialized (actions only)', { plugins: options.plugins.length });
163
+ return cached;
164
+ }
165
+
166
+ // Collect routes (plugin order = priority)
167
+ const pluginRoutes = contributions.flatMap(c => c.routes ?? []);
168
+ const appRoutes = options.routes?.(ctx) ?? [];
169
+ const allRoutes = [...pluginRoutes, ...appRoutes];
170
+
153
171
  // Collect log sink (first one wins)
154
172
  const logSink = contributions.find(c => c.logSink)?.logSink;
155
173
 
@@ -182,6 +200,20 @@ export function createPluginLambdaHandler(
182
200
  }
183
201
  }
184
202
 
203
+ // Operator mode — reject anything that isn't an `_action` invoke. A caller
204
+ // with IAM invoke rights can hand the function any event shape; refuse to
205
+ // route HTTP on the privileged connection.
206
+ if (!httpEnabled) {
207
+ if ('requestContext' in event || 'rawPath' in event) {
208
+ return {
209
+ statusCode: 403,
210
+ headers: { 'Content-Type': 'application/json', 'cache-control': 'no-store' },
211
+ body: JSON.stringify({ error: 'Forbidden' }),
212
+ };
213
+ }
214
+ return { error: 'Missing _action' };
215
+ }
216
+
185
217
  // HTTP event — Function URL or API Gateway
186
218
  const httpEvent = event as APIGatewayProxyEventV2;
187
219
  const requestId =
@@ -194,7 +226,7 @@ export function createPluginLambdaHandler(
194
226
  const request = eventToRequest(httpEvent);
195
227
  const path = httpEvent.rawPath;
196
228
  const method = httpEvent.requestContext.http.method;
197
- const handlerFn = router(path, method);
229
+ const handlerFn = router!(path, method);
198
230
  const response = await handlerFn(request);
199
231
  const result = await responseToResult(response);
200
232
 
@@ -285,57 +317,19 @@ export interface OpsLambdaHandlerOptions {
285
317
  /**
286
318
  * Dedicated operator entrypoint — handles `_action` invokes only.
287
319
  *
320
+ * Thin alias for `createPluginLambdaHandler({ ...options, http: false })`.
288
321
  * Deploy as a separate Lambda (server/ops.ts) and link ADMIN_DATABASE_URL to
289
322
  * it instead of the API function. IAM (lambda:InvokeFunction) is the auth
290
- * layer, same as the API _action path. HTTP events are rejected: this
291
- * function must never be wired to a Function URL or the CDN.
323
+ * layer; HTTP-shaped events are rejected.
324
+ *
325
+ * @deprecated Prefer `createPluginLambdaHandler({ http: false })` directly.
292
326
  */
293
327
  export function createOpsLambdaHandler(
294
328
  options: OpsLambdaHandlerOptions
295
329
  ): (event: Record<string, unknown>) => Promise<unknown> {
296
- let cached: { ctx: PluginContext; actions: Record<string, ActionHandler> } | null = null;
297
-
298
- async function ensureInitialized() {
299
- if (cached) return cached;
300
- const ctx = await options.context();
301
- const actions: Record<string, ActionHandler> = {};
302
- for (const plugin of options.plugins) {
303
- const contribution = await plugin(ctx);
304
- if (contribution.actions) Object.assign(actions, contribution.actions);
305
- }
306
- Object.assign(actions, options.actions ?? {});
307
- cached = { ctx, actions };
308
- log('info', 'Ops handlers initialized', { actions: Object.keys(actions).length });
309
- return cached;
310
- }
311
-
312
- return async (event: Record<string, unknown>): Promise<unknown> => {
313
- if (!('_action' in event) || typeof event._action !== 'string') {
314
- // Not an operator invoke. Reject HTTP-shaped events explicitly.
315
- if ('requestContext' in event || 'rawPath' in event) {
316
- return {
317
- statusCode: 403,
318
- headers: { 'Content-Type': 'application/json', 'cache-control': 'no-store' },
319
- body: JSON.stringify({ error: 'Forbidden' }),
320
- };
321
- }
322
- return { error: 'Missing _action' };
323
- }
324
-
325
- const { ctx, actions } = await ensureInitialized();
326
- const actionName = event._action as string;
327
- const actionHandler = actions[actionName];
328
- if (!actionHandler) {
329
- return { error: `Unknown action: ${actionName}` };
330
- }
331
- log('info', 'Ops action invoked', { action: actionName });
332
- try {
333
- return await actionHandler(event._payload, ctx);
334
- } catch (error) {
335
- log('error', 'Ops action error', { action: actionName, error: String(error) });
336
- return { error: String(error) };
337
- }
338
- };
330
+ return createPluginLambdaHandler({ ...options, http: false }) as (
331
+ event: Record<string, unknown>
332
+ ) => Promise<unknown>;
339
333
  }
340
334
 
341
335
  // --- SSR Fallback Plugin ---