New:Socket for Asana Is Now Available.Learn more β†’
Get Started

x-openapi-flow

Package Overview
Dependencies
Maintainers
1
Versions
32
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

x-openapi-flow - npm Package Compare versions

Comparing version
1.7.1
to
1.7.2
+123
lib/runtime-guard/nestjs.js
"use strict";
const { createRuntimeFlowGuard, toErrorPayload } = require("./core");
function resolveNestOperationId(context) {
const req = context && context.req;
if (req && req.openapi && req.openapi.operationId) {
return req.openapi.operationId;
}
if (req && req.operation && req.operation.operationId) {
return req.operation.operationId;
}
return null;
}
function defaultNestPath(req) {
if (!req) {
return "/";
}
if (req.route && typeof req.route.path === "string") {
return req.route.path;
}
if (typeof req.path === "string") {
return req.path;
}
if (typeof req.originalUrl === "string") {
return req.originalUrl.split("?")[0];
}
if (typeof req.url === "string") {
return req.url.split("?")[0];
}
return "/";
}
function createNestFlowMiddleware(options = {}) {
const guard = createRuntimeFlowGuard({
...options,
resolveOperationId: options.resolveOperationId || resolveNestOperationId,
});
return async function xOpenApiFlowNestMiddleware(req, res, next) {
try {
await guard.enforce({
req,
res,
method: req && req.method,
path: defaultNestPath(req),
params: (req && req.params) || {},
});
return next();
} catch (error) {
const payload = {
error: toErrorPayload(error),
};
const statusCode = (error && error.statusCode) || 500;
if (res && typeof res.status === "function" && typeof res.json === "function") {
return res.status(statusCode).json(payload);
}
return next(error);
}
};
}
function createNestFlowCanActivate(options = {}) {
const guard = createRuntimeFlowGuard({
...options,
resolveOperationId: options.resolveOperationId || resolveNestOperationId,
});
return async function xOpenApiFlowNestCanActivate(executionContext) {
const http = executionContext
&& typeof executionContext.switchToHttp === "function"
? executionContext.switchToHttp()
: null;
const req = http && typeof http.getRequest === "function"
? http.getRequest()
: null;
const res = http && typeof http.getResponse === "function"
? http.getResponse()
: null;
try {
await guard.enforce({
req,
res,
executionContext,
method: req && req.method,
path: defaultNestPath(req),
params: (req && req.params) || {},
});
return true;
} catch (error) {
const payload = {
error: toErrorPayload(error),
};
const statusCode = (error && error.statusCode) || 500;
if (res && typeof res.status === "function" && typeof res.json === "function") {
res.status(statusCode).json(payload);
return false;
}
throw error;
}
};
}
module.exports = {
createNestFlowMiddleware,
createNestFlowCanActivate,
};
+39
-0

@@ -41,4 +41,29 @@ // Type definitions for x-openapi-flow

requireResourceIdForTransitions?: boolean;
/**
* Optional observability callback invoked on each runtime guard decision.
* Exceptions inside this callback are swallowed by the guard.
*/
onDecision?: (decision: RuntimeFlowDecision) => void;
}
export interface RuntimeFlowDecision {
decision:
| "allowed_transition"
| "allowed_idempotent_state"
| "allowed_initial_state"
| "skipped_unknown_operation"
| "denied_unknown_operation"
| "denied_missing_resource_id"
| "denied_missing_current_state"
| "denied_invalid_transition"
| "denied_missing_state_resolver";
operationId?: string | null;
resourceId?: string | null;
currentState?: string | null;
nextState?: string | null;
method?: string;
path?: string;
durationMs: number;
}
export interface FlowOperation {

@@ -93,5 +118,19 @@ operationId: string;

export type FastifyPreHandler = (request: object, reply: object) => Promise<void>;
export type NestMiddlewareFunction = (req: object, res: object, next: (err?: unknown) => void) => void;
export interface NestHttpContextLike {
getRequest: () => object;
getResponse: () => object;
}
export interface NestExecutionContextLike {
switchToHttp: () => NestHttpContextLike;
}
export type NestCanActivateFunction = (executionContext: NestExecutionContextLike) => Promise<boolean>;
export declare function createExpressFlowGuard(options: RuntimeFlowGuardOptions): ExpressMiddleware;
export declare function createFastifyFlowGuard(options: RuntimeFlowGuardOptions): FastifyPreHandler;
export declare function createNestFlowMiddleware(options: RuntimeFlowGuardOptions): NestMiddlewareFunction;
export declare function createNestFlowCanActivate(options: RuntimeFlowGuardOptions): NestCanActivateFunction;

@@ -98,0 +137,0 @@ // ---------------------------------------------------------------------------

+69
-1

@@ -29,2 +29,14 @@ "use strict";

function safeEmitDecision(hook, payload) {
if (typeof hook !== "function") {
return;
}
try {
hook(payload);
} catch (_err) {
// Observability hook must never break request handling.
}
}
class RuntimeFlowGuard {

@@ -42,2 +54,3 @@ constructor(options = {}) {

this.resolveOperationId = options.resolveOperationId || null;
this.onDecision = typeof options.onDecision === "function" ? options.onDecision : null;

@@ -76,3 +89,16 @@ this.allowUnknownOperations = options.allowUnknownOperations === true;

async enforce(context = {}) {
const startedAt = Date.now();
const emitDecision = (decision) => {
safeEmitDecision(this.onDecision, {
method: context.method,
path: context.path,
durationMs: Date.now() - startedAt,
...decision,
});
};
if (typeof this.getCurrentState !== "function") {
emitDecision({
decision: "denied_missing_state_resolver",
});
throw missingStateResolverError();

@@ -93,5 +119,14 @@ }

if (this.allowUnknownOperations) {
emitDecision({
decision: "skipped_unknown_operation",
operationId: context.operationId || operationFromResolver || null,
});
return { ok: true, skipped: true, reason: "unknown_operation" };
}
emitDecision({
decision: "denied_unknown_operation",
operationId: context.operationId || operationFromResolver || null,
});
throw unknownOperationError({

@@ -106,2 +141,6 @@ operationId: context.operationId || operationFromResolver,

if (!resourceId && operation.incomingFromStates.size > 0 && this.requireResourceIdForTransitions) {
emitDecision({
decision: "denied_missing_resource_id",
operationId: operation.operationId,
});
throw missingResourceIdError({

@@ -123,2 +162,9 @@ operationId: operation.operationId,

if (operation.incomingFromStates.size === 0 && this.allowMissingStateForInitial) {
emitDecision({
decision: "allowed_initial_state",
operationId: operation.operationId,
resourceId,
currentState: null,
nextState: operation.currentState || null,
});
return {

@@ -132,2 +178,9 @@ ok: true,

emitDecision({
decision: "denied_missing_current_state",
operationId: operation.operationId,
resourceId,
currentState: null,
});
throw invalidTransitionError({

@@ -144,4 +197,11 @@ operationId: operation.operationId,

const isSameState = this.allowIdempotentState && normalizedState === String(operation.currentState);
const nextState = this.stateMachine.getNextState(normalizedState, operation.operationId);
if (!isAllowedFrom && !isSameState) {
emitDecision({
decision: "denied_invalid_transition",
operationId: operation.operationId,
resourceId,
currentState: normalizedState,
});
throw invalidTransitionError({

@@ -155,2 +215,10 @@ operationId: operation.operationId,

emitDecision({
decision: isAllowedFrom ? "allowed_transition" : "allowed_idempotent_state",
operationId: operation.operationId,
resourceId,
currentState: normalizedState,
nextState,
});
return {

@@ -161,3 +229,3 @@ ok: true,

currentState: normalizedState,
nextState: this.stateMachine.getNextState(normalizedState, operation.operationId),
nextState,
};

@@ -164,0 +232,0 @@ }

@@ -6,2 +6,3 @@ "use strict";

const { createFastifyFlowGuard } = require("./fastify");
const { createNestFlowMiddleware, createNestFlowCanActivate } = require("./nestjs");
const { FlowGuardError } = require("./errors");

@@ -15,2 +16,4 @@ const { MemoryAdapter, FileAdapter, RedisAdapter, GenericSQLAdapter } = require("./adapters");

createFastifyFlowGuard,
createNestFlowMiddleware,
createNestFlowCanActivate,
FlowGuardError,

@@ -17,0 +20,0 @@ toErrorPayload,

+1
-1
{
"name": "x-openapi-flow",
"version": "1.7.1",
"version": "1.7.2",
"description": "Enforce, visualize and generate code from OpenAPI resource lifecycles β€” runtime guard, SDK gen, Postman/Insomnia/Redoc adapters, MCP sidecar for AI agents",

@@ -5,0 +5,0 @@ "main": "lib/validator.js",

+103
-5

@@ -125,2 +125,25 @@ <!-- Auto-generated from /README.md via scripts/sync-package-readme.js. Do not edit directly. -->

## Start in 2 Minutes (Online Playground)
Prefer no local setup? Open the minimal runtime-guard demo directly in your browser:
- StackBlitz: https://stackblitz.com/github/tiago-marques/x-openapi-flow/tree/main/example/runtime-guard/minimal-order
- Codespaces (repo): https://github.com/tiago-marques/x-openapi-flow
Once open, run:
```bash
npm install
npm run start
```
Then in another terminal:
```bash
curl -s -X POST http://localhost:3110/orders
curl -i -X POST http://localhost:3110/orders/<id>/ship
```
Expected: `409 INVALID_STATE_TRANSITION`.
Fastest way to see value (guided scaffold):

@@ -192,3 +215,3 @@

- name: Validate OpenAPI flow rules
uses: tiago-marques/x-openapi-flow/.github/actions/validate@main
uses: tiago-marques/x-openapi-flow@v1
with:

@@ -371,3 +394,40 @@ openapi-file: openapi.flow.yaml

### Observability Hooks (Metrics/Audit)
You can instrument runtime decisions with `onDecision` to feed Prometheus, logs, or tracing.
```js
const { Counter } = require("prom-client");
const { createExpressFlowGuard } = require("x-openapi-flow/lib/runtime-guard");
const flowDecisions = new Counter({
name: "xflow_runtime_guard_decisions_total",
help: "Runtime guard decisions by type and operation",
labelNames: ["decision", "operation_id"],
});
app.use(
createExpressFlowGuard({
openapi,
...store.forGuard(),
onDecision(event) {
flowDecisions.inc({
decision: event.decision,
operation_id: event.operationId || "unknown",
});
},
})
);
```
Common decision values include:
- `allowed_transition`
- `allowed_idempotent_state`
- `allowed_initial_state`
- `denied_invalid_transition`
- `denied_missing_resource_id`
- `denied_unknown_operation`
- `skipped_unknown_operation`
Want to see the value immediately? Use the official minimal demo:

@@ -613,12 +673,25 @@

Use x-openapi-flow runtime guard inside NestJS via a custom guard or interceptor. No dedicated package required:
Dedicated package: **x-openapi-flow-nestjs**
```bash
npm install x-openapi-flow x-openapi-flow-nestjs
```
Then import from `x-openapi-flow-nestjs`.
This package wraps the official runtime-guard helpers and exposes a NestJS-first API.
Release automation for this package uses dedicated tags in the format `nestjs-v<version>`
(example: `nestjs-v0.1.0`) so it does not conflict with `x-openapi-flow` tags.
### Option A: Middleware (drop-in)
```ts
// flow-guard.middleware.ts
import { Injectable, NestMiddleware } from "@nestjs/common";
import { createExpressFlowGuard, MemoryAdapter } from "x-openapi-flow/lib/runtime-guard";
import { createFlowMiddleware, MemoryAdapter } from "x-openapi-flow-nestjs";
import openapi from "./openapi.flow.json";
const store = new MemoryAdapter(); // swap for RedisAdapter or GenericSQLAdapter in production
const guard = createExpressFlowGuard({ openapi, ...store.forGuard() });
const guard = createFlowMiddleware({ openapi, ...store.forGuard() });

@@ -628,3 +701,3 @@ @Injectable()

use(req: any, res: any, next: () => void) {
guard(req, res, next);
guard.use(req, res, next);
}

@@ -641,2 +714,21 @@ }

### Option B: CanActivate Guard
```ts
// flow.guard.ts
import { CanActivate, ExecutionContext, Injectable } from "@nestjs/common";
import { createFlowGuard, MemoryAdapter } from "x-openapi-flow-nestjs";
import openapi from "./openapi.flow.json";
const store = new MemoryAdapter();
const flowGuard = createFlowGuard({ openapi, ...store.forGuard() });
@Injectable()
export class FlowGuard implements CanActivate {
canActivate(context: ExecutionContext) {
return flowGuard.canActivate(context);
}
}
```
After each successful state-changing request, call `store.setState` to advance the resource:

@@ -678,2 +770,8 @@

## Community and Proof
- **Share your case study** – [docs/wiki/community/Case-Study-Template.md](https://github.com/tiago-marques/x-openapi-flow/blob/main/docs/wiki/community/Case-Study-Template.md)
- **Open discussions and ideas** – use GitHub Issues to share adoption blockers, metrics, and integration stories
- **Prove impact with metrics** – track `denied_invalid_transition`, onboarding lead time, and CI catch rate before/after adoption
## Roadmap

@@ -680,0 +778,0 @@