@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.
- package/package.json +1 -1
- package/src/plugin.ts +46 -52
package/package.json
CHANGED
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
|
|
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
|
|
291
|
-
*
|
|
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
|
-
|
|
297
|
-
|
|
298
|
-
|
|
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 ---
|