@boldsec/next 0.1.0

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/README.md ADDED
@@ -0,0 +1,78 @@
1
+ # @boldsec/next
2
+
3
+ Live BOLA/IDOR monitoring for **Next.js (App Router)**. Wrap an API route; BoLD watches it and
4
+ raises an alarm the moment one user reaches another user's object — in real time, from your app's
5
+ actual traffic.
6
+
7
+ - **Metadata only.** BoLD receives who made the request (an opaque, non-reversible handle — never
8
+ the token), the endpoint shape, the object id, the status, and the object's declared owner if the
9
+ response exposed one. The response body is read once to extract that single owner field and then
10
+ discarded. No body, no headers, no credentials ever leave your app.
11
+ - **Fail-safe.** Shipping the metadata is fire-and-forget. If BoLD is slow or down, your response is
12
+ returned untouched and on time. BoLD can never break or slow the app it watches.
13
+ - **Never alters your response.** Your handler runs exactly as before; its response is returned
14
+ byte-for-byte.
15
+
16
+ ## Install
17
+
18
+ ```bash
19
+ npm install @boldsec/next
20
+ ```
21
+
22
+ ## Configure (once)
23
+
24
+ Add your keys (from the BoLD **Live** page, shown once when you connect the app):
25
+
26
+ ```bash
27
+ # .env.local
28
+ BOLD_INGEST_URL=https://<your-bold>/api/live/ingest
29
+ BOLD_INGEST_KEY=blk_...
30
+ # optional — owner-field names to look for in responses (default covers common ones)
31
+ BOLD_OWNER_FIELDS=ownerId,owner_id,user_id
32
+ ```
33
+
34
+ ## Use
35
+
36
+ Wrap the route handlers you want watched:
37
+
38
+ ```ts
39
+ // app/api/invoices/[id]/route.ts
40
+ import { withBold } from "@boldsec/next";
41
+
42
+ export const GET = withBold(
43
+ async (req, ctx) => {
44
+ const invoice = await getInvoice(ctx.params.id);
45
+ return Response.json(invoice); // { id, ownerId, ... }
46
+ },
47
+ // STRONGLY RECOMMENDED — tell BoLD who the caller is, in the SAME namespace as your owner field:
48
+ { resolveCallerId: (req) => getUserIdFromSession(req) }, // e.g. "usr_101"
49
+ );
50
+ ```
51
+
52
+ Works for `GET` / `PUT` / `PATCH` / `POST` / `DELETE` handlers. Redeploy, and your app's real
53
+ traffic is watched automatically — catches appear in your BoLD **Findings** with a live timestamp.
54
+
55
+ ### App shapes it reads
56
+
57
+ - **REST object routes** — string ids (`/api/invoices/inv_8101`), numeric ids (`/api/orders/4021`),
58
+ UUIDs.
59
+ - **Nested / oddly-named owner fields** — the owner is found wherever it nests (bounded depth) as
60
+ long as the field name is in your owner-field list (e.g. add `holderId` for `data.account.holderId`).
61
+ - **GraphQL** — wrap your `app/api/graphql/route.ts` the same way. BoLD parses the operation and its
62
+ `id` (from `variables` or an inline literal) and reads the owner from `data.<field>`. Single-object
63
+ queries and mutations are watched; batched/list queries, or an id BoLD can't resolve, are skipped
64
+ (it never guesses — a shape it can't read is held for review, never reported clean).
65
+
66
+ ### Why `resolveCallerId` matters
67
+
68
+ Your app already authenticated the caller, so it knows their id (e.g. `usr_101`). Passing it lets
69
+ BoLD compare the **caller** against the **object's owner** directly: if they match, it's the owner's
70
+ own access and BoLD stays silent — no false alarm. Without it, BoLD falls back to a non-reversible
71
+ hash of the auth header, which can't be matched against your owner ids, so a user reading **their
72
+ own** object may be flagged. Always provide `resolveCallerId` in production.
73
+
74
+ ## How it confirms a violation
75
+
76
+ BoLD confirms a BOLA when a request returns an object whose **owner field** identifies a *different*
77
+ user than the caller. If a response doesn't expose an owner field, BoLD reports **needs review**
78
+ rather than guessing — it never raises a false alarm.
@@ -0,0 +1,76 @@
1
+ /**
2
+ * @boldsec/next: the Next.js (App Router) route wrapper for BoLD live monitoring (Stage 3 / S3.3).
3
+ *
4
+ * Why a route wrapper and not `proxy.ts`/middleware: Next.js proxy runs BEFORE the response and
5
+ * cannot read the response body — but confirming a BOLA needs the owner field FROM the response
6
+ * (e.g. `ownerId`). The route handler is the only place that sees both the request and the response,
7
+ * so that is where BoLD watches. The user wraps their existing handler; it runs unchanged.
8
+ *
9
+ * What it sends to BoLD: METADATA ONLY — who made the request (an opaque, non-reversible identity
10
+ * handle, never the token/cookie), the endpoint shape, the object id, the status, and the object's
11
+ * declared owner if the response JSON exposed one. The response body is read once to extract that
12
+ * single owner field and then DISCARDED. No body, no headers, no credentials ever leave the app.
13
+ *
14
+ * Failure mode: FAIL-SAFE. Shipping the metadata is fire-and-forget; if BoLD is slow or down, the
15
+ * user's response is returned untouched and on time. BoLD can never break or slow the app it watches.
16
+ *
17
+ * Usage (a few lines, no terminal):
18
+ *
19
+ * // app/api/invoices/[id]/route.ts
20
+ * import { withBold } from "@boldsec/next";
21
+ *
22
+ * export const GET = withBold(
23
+ * async (req, ctx) => {
24
+ * const invoice = await getInvoice(ctx.params.id);
25
+ * return Response.json(invoice); // { id, ownerId, ... }
26
+ * },
27
+ * { resolveCallerId: (req) => getUserIdFromSession(req) }, // RECOMMENDED — see BoldConfig
28
+ * );
29
+ *
30
+ * Config via env (set once): BOLD_INGEST_URL, BOLD_INGEST_KEY, optional BOLD_OWNER_FIELDS.
31
+ */
32
+ export interface BoldConfig {
33
+ /** Where to send events — your BoLD ingest endpoint. Defaults to env BOLD_INGEST_URL. */
34
+ ingestUrl?: string;
35
+ /** The monitor's ingest key (shown once when you connect the app). Defaults to env BOLD_INGEST_KEY. */
36
+ ingestKey?: string;
37
+ /** Owner-field names to look for in the response JSON. Defaults to env BOLD_OWNER_FIELDS or a
38
+ * common set. The FIRST one present (top level or one level of nesting) is used. */
39
+ ownerFields?: string[];
40
+ /** The authenticated caller's RESOLVED id (e.g. `usr_101`) — same namespace as the response's
41
+ * owner field. STRONGLY RECOMMENDED: your app already authenticated the user, so it knows this.
42
+ * With it, BoLD sees "caller IS the owner" and never false-CONFIRMs an owner's own access. Pass
43
+ * a `(request) => id` resolver. Without it, identity falls back to a non-reversible hash of the
44
+ * auth header — which cannot match the owner namespace, so an owner reading their own object can
45
+ * be wrongly flagged. */
46
+ resolveCallerId?: (request: Request) => string | null | Promise<string | null>;
47
+ }
48
+ /** The shape of a Next.js App Router route handler: (request, context) => Response. We keep the
49
+ * context generic so any `{ params }` shape passes through untouched. */
50
+ export type RouteHandler<C> = (request: Request, context: C) => Response | Promise<Response>;
51
+ /** Split a path into an endpoint template (".../{id}") + the object id, or null if no id segment. */
52
+ export declare function endpointAndObject(pathname: string): {
53
+ endpoint: string;
54
+ objectId: string | null;
55
+ };
56
+ /** Pull the first owner-looking field from a parsed JSON body, searching by KNOWN field name only,
57
+ * breadth-first to a bounded depth. We NEVER guess an owner from an unnamed field — if no
58
+ * configured owner-field name is present anywhere in range, we return null, and the engine then
59
+ * yields needs-review (the anthem: an unfindable owner is loud, never a false clean). Breadth-first
60
+ * + shallow bound means the SHALLOWEST match wins (the real owner), not a deep look-alike. */
61
+ export declare function declaredOwner(json: unknown, ownerFields: string[]): string | null;
62
+ export interface GraphqlOp {
63
+ field: string;
64
+ objectId: string;
65
+ isMutation: boolean;
66
+ }
67
+ /** Parse a GraphQL request body into a single object operation + its id, or null. Reads the id from
68
+ * `variables` (the standard pattern) when the query references `$var`, else from an inline literal.
69
+ * Returns null for anything ambiguous (no id, multiple object ops, batched array) so we stay loud. */
70
+ export declare function parseGraphqlOp(body: unknown): GraphqlOp | null;
71
+ /**
72
+ * Wrap a Next.js App Router route handler so BoLD watches it live. The handler runs exactly as
73
+ * before; its response is returned untouched. BoLD observes metadata out of band.
74
+ */
75
+ export declare function withBold<C>(handler: RouteHandler<C>, config?: BoldConfig): RouteHandler<C>;
76
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAEH,MAAM,WAAW,UAAU;IACzB,yFAAyF;IACzF,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,uGAAuG;IACvG,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;yFACqF;IACrF,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IACvB;;;;;8BAK0B;IAC1B,eAAe,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,MAAM,GAAG,IAAI,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;CAChF;AAED;0EAC0E;AAC1E,MAAM,MAAM,YAAY,CAAC,CAAC,IAAI,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC,KAAK,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;AAsE7F,qGAAqG;AACrG,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,GAAG;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,CAMjG;AAOD;;;;+FAI+F;AAC/F,wBAAgB,aAAa,CAAC,IAAI,EAAE,OAAO,EAAE,WAAW,EAAE,MAAM,EAAE,GAAG,MAAM,GAAG,IAAI,CAuBjF;AAeD,MAAM,WAAW,SAAS;IACxB,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,OAAO,CAAC;CACrB;AAED;;uGAEuG;AACvG,wBAAgB,cAAc,CAAC,IAAI,EAAE,OAAO,GAAG,SAAS,GAAG,IAAI,CAyB9D;AAiGD;;;GAGG;AACH,wBAAgB,QAAQ,CAAC,CAAC,EAAE,OAAO,EAAE,YAAY,CAAC,CAAC,CAAC,EAAE,MAAM,CAAC,EAAE,UAAU,GAAG,YAAY,CAAC,CAAC,CAAC,CA4B1F"}
package/dist/index.js ADDED
@@ -0,0 +1,312 @@
1
+ /**
2
+ * @boldsec/next: the Next.js (App Router) route wrapper for BoLD live monitoring (Stage 3 / S3.3).
3
+ *
4
+ * Why a route wrapper and not `proxy.ts`/middleware: Next.js proxy runs BEFORE the response and
5
+ * cannot read the response body — but confirming a BOLA needs the owner field FROM the response
6
+ * (e.g. `ownerId`). The route handler is the only place that sees both the request and the response,
7
+ * so that is where BoLD watches. The user wraps their existing handler; it runs unchanged.
8
+ *
9
+ * What it sends to BoLD: METADATA ONLY — who made the request (an opaque, non-reversible identity
10
+ * handle, never the token/cookie), the endpoint shape, the object id, the status, and the object's
11
+ * declared owner if the response JSON exposed one. The response body is read once to extract that
12
+ * single owner field and then DISCARDED. No body, no headers, no credentials ever leave the app.
13
+ *
14
+ * Failure mode: FAIL-SAFE. Shipping the metadata is fire-and-forget; if BoLD is slow or down, the
15
+ * user's response is returned untouched and on time. BoLD can never break or slow the app it watches.
16
+ *
17
+ * Usage (a few lines, no terminal):
18
+ *
19
+ * // app/api/invoices/[id]/route.ts
20
+ * import { withBold } from "@boldsec/next";
21
+ *
22
+ * export const GET = withBold(
23
+ * async (req, ctx) => {
24
+ * const invoice = await getInvoice(ctx.params.id);
25
+ * return Response.json(invoice); // { id, ownerId, ... }
26
+ * },
27
+ * { resolveCallerId: (req) => getUserIdFromSession(req) }, // RECOMMENDED — see BoldConfig
28
+ * );
29
+ *
30
+ * Config via env (set once): BOLD_INGEST_URL, BOLD_INGEST_KEY, optional BOLD_OWNER_FIELDS.
31
+ */
32
+ const DEFAULT_OWNER_FIELDS = [
33
+ "ownerId",
34
+ "owner_id",
35
+ "userId",
36
+ "user_id",
37
+ "accountId",
38
+ "account_id",
39
+ "createdBy",
40
+ "created_by",
41
+ "tenantId",
42
+ "tenant_id",
43
+ ];
44
+ const OBJECT_PATH = /^(.*?)\/([^/]+)\/?$/; // ".../{prefix}/{lastSegment}"
45
+ // An id-looking last path segment: a pure number (42), a uuid/long-hex (>=6 hex/dash chars), or a
46
+ // PREFIXED id with one OR MORE underscore segments (inv_7001, acct_northstar_001,
47
+ // note_priya_investor_followup). Requiring at least one `_segment` is what distinguishes a real id
48
+ // from a route word like `orders` / `login` (which must NOT be treated as an object id).
49
+ const ID_LIKE = /^[0-9]+$|^[0-9a-fA-F-]{6,}$|^[a-zA-Z][a-zA-Z0-9]*(?:_[a-zA-Z0-9]+)+$/;
50
+ function resolveConfig(cfg) {
51
+ const env = typeof process !== "undefined" && process.env ? process.env : {};
52
+ const ingestUrl = cfg?.ingestUrl ?? env.BOLD_INGEST_URL ?? "";
53
+ const ingestKey = cfg?.ingestKey ?? env.BOLD_INGEST_KEY ?? "";
54
+ if (!ingestUrl || !ingestKey)
55
+ return null; // not configured -> silently no-op (never throw)
56
+ const ownerFields = cfg?.ownerFields ??
57
+ (env.BOLD_OWNER_FIELDS
58
+ ? env.BOLD_OWNER_FIELDS.split(",").map((s) => s.trim())
59
+ : DEFAULT_OWNER_FIELDS);
60
+ return { ingestUrl, ingestKey, ownerFields, resolveCallerId: cfg?.resolveCallerId };
61
+ }
62
+ /** The caller's identity for the engine. Best: the app's RESOLVED owner-id (same namespace as the
63
+ * response owner field) so own-access is never false-CONFIRMED. Fallback: a NON-reversible hash of
64
+ * the auth material (never the token) — lets BoLD tell "same vs different caller" but can't match
65
+ * the owner namespace. */
66
+ async function identityFrom(request, cfg) {
67
+ if (cfg.resolveCallerId) {
68
+ try {
69
+ const resolved = await cfg.resolveCallerId(request);
70
+ if (resolved)
71
+ return String(resolved);
72
+ }
73
+ catch {
74
+ // a throwing resolver must never break the app -> fall back to the hash
75
+ }
76
+ }
77
+ const auth = request.headers.get("authorization") ?? request.headers.get("cookie") ?? "";
78
+ if (!auth)
79
+ return null;
80
+ try {
81
+ const data = new TextEncoder().encode(auth);
82
+ const digest = await crypto.subtle.digest("SHA-256", data);
83
+ const hex = Array.from(new Uint8Array(digest))
84
+ .map((b) => b.toString(16).padStart(2, "0"))
85
+ .join("");
86
+ return "id_" + hex.slice(0, 16);
87
+ }
88
+ catch {
89
+ return null;
90
+ }
91
+ }
92
+ /** Split a path into an endpoint template (".../{id}") + the object id, or null if no id segment. */
93
+ export function endpointAndObject(pathname) {
94
+ const m = pathname.match(OBJECT_PATH);
95
+ if (!m)
96
+ return { endpoint: pathname, objectId: null };
97
+ const [, prefix, last] = m;
98
+ if (!ID_LIKE.test(last))
99
+ return { endpoint: pathname, objectId: null };
100
+ return { endpoint: `${prefix}/{id}`, objectId: last };
101
+ }
102
+ /** Max nesting depth declaredOwner walks. Real owner fields live shallow (top level, or under a
103
+ * `data`/`account`/`attributes` wrapper); we bound the walk so a deeply nested LOOK-ALIKE field can
104
+ * never be mistaken for the owner, and so a huge response can't cost unbounded work. */
105
+ const OWNER_MAX_DEPTH = 4;
106
+ /** Pull the first owner-looking field from a parsed JSON body, searching by KNOWN field name only,
107
+ * breadth-first to a bounded depth. We NEVER guess an owner from an unnamed field — if no
108
+ * configured owner-field name is present anywhere in range, we return null, and the engine then
109
+ * yields needs-review (the anthem: an unfindable owner is loud, never a false clean). Breadth-first
110
+ * + shallow bound means the SHALLOWEST match wins (the real owner), not a deep look-alike. */
111
+ export function declaredOwner(json, ownerFields) {
112
+ const fields = new Set(ownerFields);
113
+ // BFS so the shallowest occurrence of an owner field wins; bounded depth + visited-guard keep it
114
+ // safe on large/cyclic objects.
115
+ let level = [json];
116
+ const seen = new Set();
117
+ for (let depth = 0; depth <= OWNER_MAX_DEPTH && level.length; depth++) {
118
+ const next = [];
119
+ for (const node of level) {
120
+ if (!node || typeof node !== "object" || Array.isArray(node) || seen.has(node))
121
+ continue;
122
+ seen.add(node);
123
+ const obj = node;
124
+ for (const f of fields) {
125
+ const v = obj[f];
126
+ if (v !== undefined && v !== null && typeof v !== "object")
127
+ return String(v);
128
+ }
129
+ for (const v of Object.values(obj)) {
130
+ if (v && typeof v === "object")
131
+ next.push(v);
132
+ }
133
+ }
134
+ level = next;
135
+ }
136
+ return null;
137
+ }
138
+ // ── GraphQL adapter (Shape 3) ──────────────────────────────────────────────────────────────────
139
+ // GraphQL puts the object id in the REQUEST BODY (not the URL path) and the data under `data.<field>`
140
+ // in the response, so the path-based extractor finds nothing. This adapter parses the operation +
141
+ // id from the request and maps the owner from the response. The anthem floor holds throughout: if we
142
+ // cannot identify EXACTLY ONE object operation + its id, we return null and ship nothing for it —
143
+ // BoLD never invents an object, so it never produces a false verdict on a query it can't read.
144
+ const GQL_PATH = /\/graphql\/?$/i; // the conventional GraphQL endpoint
145
+ // A top-level selection like `order(id: $x)` or `invoice(id: "7")` — capture the field name + the id
146
+ // arg (variable ref `$name` or an inline string/number literal). Intentionally conservative.
147
+ const GQL_FIELD_WITH_ID = /\b([A-Za-z_][A-Za-z0-9_]*)\s*\(\s*[^)]*?\bid\s*:\s*(?:\$([A-Za-z_][A-Za-z0-9_]*)|"([^"]+)"|([0-9]+))/;
148
+ /** Parse a GraphQL request body into a single object operation + its id, or null. Reads the id from
149
+ * `variables` (the standard pattern) when the query references `$var`, else from an inline literal.
150
+ * Returns null for anything ambiguous (no id, multiple object ops, batched array) so we stay loud. */
151
+ export function parseGraphqlOp(body) {
152
+ if (Array.isArray(body))
153
+ return null; // batched queries -> ambiguous, hold loud
154
+ if (!body || typeof body !== "object")
155
+ return null;
156
+ const b = body;
157
+ const query = b.query;
158
+ if (typeof query !== "string")
159
+ return null;
160
+ const variables = (b.variables && typeof b.variables === "object" ? b.variables : {});
161
+ const isMutation = /^\s*mutation\b/i.test(query) || /\bmutation\b/i.test(query.split("{")[0] ?? "");
162
+ const m = query.match(GQL_FIELD_WITH_ID);
163
+ if (!m)
164
+ return null;
165
+ const [, field, varRef, strLit, numLit] = m;
166
+ let objectId = null;
167
+ if (varRef) {
168
+ const v = variables[varRef];
169
+ if (v !== undefined && v !== null && typeof v !== "object")
170
+ objectId = String(v);
171
+ }
172
+ else if (strLit !== undefined) {
173
+ objectId = strLit;
174
+ }
175
+ else if (numLit !== undefined) {
176
+ objectId = numLit;
177
+ }
178
+ if (!objectId)
179
+ return null; // an id we can't resolve -> don't guess, hold loud
180
+ return { field, objectId, isMutation };
181
+ }
182
+ /** The owner for a GraphQL op: look under the response's `data.<field>` subtree (the GraphQL result
183
+ * shape), reusing the same name-based owner search. Null if not present -> needs-review. */
184
+ function graphqlOwner(json, field, ownerFields) {
185
+ if (!json || typeof json !== "object")
186
+ return null;
187
+ const data = json.data;
188
+ if (!data || typeof data !== "object")
189
+ return null;
190
+ const node = data[field];
191
+ if (node === undefined)
192
+ return null;
193
+ return declaredOwner(node, ownerFields);
194
+ }
195
+ function methodToAction(method) {
196
+ const m = method.toUpperCase();
197
+ if (m === "GET")
198
+ return "GET";
199
+ if (m === "PUT" || m === "PATCH" || m === "POST")
200
+ return m;
201
+ if (m === "DELETE")
202
+ return "DELETE";
203
+ return null;
204
+ }
205
+ /** Build the metadata event + ship it fire-and-forget. Never throws; never blocks the response. */
206
+ async function observe(request, response, cfg,
207
+ // The GraphQL request body, captured BEFORE the handler ran (the handler consumes the body, so a
208
+ // clone taken afterwards is empty — only the up-front capture in withBold has the query). null for
209
+ // non-GraphQL requests or when the body could not be read.
210
+ gqlBody) {
211
+ try {
212
+ const url = new URL(request.url);
213
+ const respIsJson = (response.headers.get("content-type") ?? "").includes("application/json");
214
+ // Resolve {endpoint, objectId, owner, method} for either a REST object route OR a GraphQL op.
215
+ let endpoint;
216
+ let objectId;
217
+ let owner = null;
218
+ let method;
219
+ if (request.method.toUpperCase() === "POST" && GQL_PATH.test(url.pathname)) {
220
+ // ── GraphQL path: the id is in the request body (captured up-front), owner under data.<field>.
221
+ const op = parseGraphqlOp(gqlBody);
222
+ if (!op)
223
+ return; // can't identify exactly one object op -> ship nothing, never guess
224
+ endpoint = `${url.pathname}#${op.field}`; // endpoint template names the GraphQL field
225
+ objectId = op.objectId;
226
+ method = op.isMutation ? "PATCH" : "GET"; // mutation -> write (UPDATE), query -> read
227
+ if (respIsJson) {
228
+ try {
229
+ owner = graphqlOwner(await response.clone().json(), op.field, cfg.ownerFields);
230
+ }
231
+ catch {
232
+ owner = null;
233
+ }
234
+ }
235
+ }
236
+ else {
237
+ // ── REST path: id in the URL, owner anywhere named in the response JSON. ──
238
+ const eo = endpointAndObject(url.pathname);
239
+ endpoint = eo.endpoint;
240
+ objectId = eo.objectId;
241
+ if (!objectId)
242
+ return; // not an object-by-id route -> nothing to judge
243
+ if (!methodToAction(request.method))
244
+ return;
245
+ method = request.method.toUpperCase();
246
+ if (respIsJson) {
247
+ try {
248
+ owner = declaredOwner(await response.clone().json(), cfg.ownerFields);
249
+ }
250
+ catch {
251
+ owner = null; // not the JSON shape we need -> no owner signal, never an error
252
+ }
253
+ }
254
+ }
255
+ const event = {
256
+ identity: await identityFrom(request, cfg),
257
+ method,
258
+ endpoint,
259
+ object_id: objectId,
260
+ status_code: response.status,
261
+ declared_owner: owner,
262
+ };
263
+ // On serverless (Vercel/Lambda) the function is FROZEN the instant the response is returned, so
264
+ // an un-awaited fetch is killed mid-flight and the metadata never leaves (proven on a real
265
+ // deploy — `keepalive` is a browser-only hint Node ignores). So we AWAIT the POST. Still
266
+ // fail-safe: the inner .catch + the outer try/catch mean a slow/down BoLD can never break or
267
+ // alter the response — and `observe()` itself is awaited by the wrapper only up to here, so the
268
+ // owner-bytes were already read from the clone before this.
269
+ await fetch(cfg.ingestUrl, {
270
+ method: "POST",
271
+ headers: { "content-type": "application/json", authorization: `Bearer ${cfg.ingestKey}` },
272
+ body: JSON.stringify(event),
273
+ }).catch(() => { });
274
+ }
275
+ catch {
276
+ // The wrapper must NEVER affect the app. Swallow everything.
277
+ }
278
+ }
279
+ /**
280
+ * Wrap a Next.js App Router route handler so BoLD watches it live. The handler runs exactly as
281
+ * before; its response is returned untouched. BoLD observes metadata out of band.
282
+ */
283
+ export function withBold(handler, config) {
284
+ return async (request, context) => {
285
+ const cfg = resolveConfig(config);
286
+ // For a GraphQL POST, the id lives in the request body — and the handler will CONSUME that body
287
+ // when it reads it. So we must capture it from a CLONE *before* the handler runs; a clone taken
288
+ // afterwards is empty. We read it fail-safe: any error leaves gqlBody null and the request is
289
+ // simply not judged (never breaks the handler, which gets the original body untouched).
290
+ let gqlBody = null;
291
+ if (cfg && request.method.toUpperCase() === "POST") {
292
+ try {
293
+ const url = new URL(request.url);
294
+ if (GQL_PATH.test(url.pathname))
295
+ gqlBody = await request.clone().json();
296
+ }
297
+ catch {
298
+ gqlBody = null;
299
+ }
300
+ }
301
+ const response = await handler(request, context);
302
+ if (cfg) {
303
+ // AWAIT the observe so the metadata POST actually transmits before the serverless function
304
+ // freezes at return (an un-awaited POST is silently dropped on Vercel/Lambda — proven on a
305
+ // real deploy). observe() is fully fail-safe internally (try/catch + .catch), so awaiting it
306
+ // can never throw, never alter the response, and only briefly delays returning it.
307
+ await observe(request, response, cfg, gqlBody);
308
+ }
309
+ return response;
310
+ };
311
+ }
312
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "@boldsec/next",
3
+ "version": "0.1.0",
4
+ "description": "BoLD live BOLA/IDOR monitoring for Next.js App Router: wrap a route, watch it for cross-user access. Metadata only, fail-safe.",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "module": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.js"
13
+ }
14
+ },
15
+ "files": ["dist", "README.md"],
16
+ "publishConfig": {
17
+ "access": "public"
18
+ },
19
+ "homepage": "https://boldsec.io",
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "https://github.com/Sahith59/BoLD.git",
23
+ "directory": "web/sdk/bold-next"
24
+ },
25
+ "sideEffects": false,
26
+ "scripts": {
27
+ "build": "rm -rf dist && tsc -p tsconfig.build.json",
28
+ "typecheck": "tsc -p tsconfig.json",
29
+ "test": "node --test src/index.test.ts",
30
+ "prepublishOnly": "npm run typecheck && npm test && npm run build"
31
+ },
32
+ "keywords": [
33
+ "bola",
34
+ "idor",
35
+ "security",
36
+ "nextjs",
37
+ "authorization",
38
+ "monitoring",
39
+ "appsec"
40
+ ],
41
+ "license": "UNLICENSED",
42
+ "engines": {
43
+ "node": ">=18"
44
+ },
45
+ "peerDependencies": {
46
+ "next": ">=13"
47
+ },
48
+ "devDependencies": {
49
+ "@types/node": "^20",
50
+ "typescript": "^5"
51
+ }
52
+ }