@webiny/api-core 6.5.0-beta.1 → 6.5.0-beta.3

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 (46) hide show
  1. package/ApiCoreFeature.js +2 -0
  2. package/ApiCoreFeature.js.map +1 -1
  3. package/createApiCore.d.ts +1 -1
  4. package/createApiCore.js +3 -1
  5. package/createApiCore.js.map +1 -1
  6. package/features/debugger/Debugger.d.ts +23 -0
  7. package/features/debugger/Debugger.js +134 -0
  8. package/features/debugger/Debugger.js.map +1 -0
  9. package/features/debugger/GraphQLDebuggerTransport.d.ts +26 -0
  10. package/features/debugger/GraphQLDebuggerTransport.js +45 -0
  11. package/features/debugger/GraphQLDebuggerTransport.js.map +1 -0
  12. package/features/debugger/__tests__/matchNamespace.test.d.ts +1 -0
  13. package/features/debugger/__tests__/matchNamespace.test.js +67 -0
  14. package/features/debugger/__tests__/matchNamespace.test.js.map +1 -0
  15. package/features/debugger/__tests__/serialize.test.d.ts +1 -0
  16. package/features/debugger/__tests__/serialize.test.js +220 -0
  17. package/features/debugger/__tests__/serialize.test.js.map +1 -0
  18. package/features/debugger/abstractions.d.ts +100 -0
  19. package/features/debugger/abstractions.js +6 -0
  20. package/features/debugger/abstractions.js.map +1 -0
  21. package/features/debugger/feature.d.ts +5 -0
  22. package/features/debugger/feature.js +15 -0
  23. package/features/debugger/feature.js.map +1 -0
  24. package/features/debugger/index.d.ts +7 -0
  25. package/features/debugger/index.js +7 -0
  26. package/features/debugger/limits.d.ts +18 -0
  27. package/features/debugger/limits.js +11 -0
  28. package/features/debugger/limits.js.map +1 -0
  29. package/features/debugger/matchNamespace.d.ts +21 -0
  30. package/features/debugger/matchNamespace.js +48 -0
  31. package/features/debugger/matchNamespace.js.map +1 -0
  32. package/features/debugger/permissions.d.ts +33 -0
  33. package/features/debugger/permissions.js +12 -0
  34. package/features/debugger/permissions.js.map +1 -0
  35. package/features/debugger/permissionsSchema.d.ts +17 -0
  36. package/features/debugger/permissionsSchema.js +18 -0
  37. package/features/debugger/permissionsSchema.js.map +1 -0
  38. package/features/debugger/plugins.d.ts +4 -0
  39. package/features/debugger/plugins.js +82 -0
  40. package/features/debugger/plugins.js.map +1 -0
  41. package/features/debugger/serialize.d.ts +11 -0
  42. package/features/debugger/serialize.js +121 -0
  43. package/features/debugger/serialize.js.map +1 -0
  44. package/legacy/security/plugins/secureHeaders.js +1 -0
  45. package/legacy/security/plugins/secureHeaders.js.map +1 -1
  46. package/package.json +14 -14
@@ -0,0 +1 @@
1
+ {"version":3,"file":"features/debugger/__tests__/serialize.test.js","sources":["../../../../src/features/debugger/__tests__/serialize.test.ts"],"sourcesContent":["import { describe, expect, it } from \"vitest\";\nimport { serialize } from \"../serialize.js\";\nimport { DEFAULT_LIMITS, type IDebuggerLimits } from \"../limits.js\";\n\nconst limits = (overrides: Partial<IDebuggerLimits> = {}): IDebuggerLimits => {\n return { ...DEFAULT_LIMITS, ...overrides };\n};\n\nconst parse = (value: unknown, overrides: Partial<IDebuggerLimits> = {}) => {\n const result = serialize(value, limits(overrides));\n return { ...result, parsed: JSON.parse(result.json) };\n};\n\ndescribe(\"serialize\", () => {\n it(\"should always produce parseable JSON\", () => {\n const circular: Record<string, unknown> = { name: \"root\" };\n circular.self = circular;\n\n const { parsed } = parse(circular);\n\n expect(parsed).toEqual({ name: \"root\", self: \"#[Circular]\" });\n });\n\n it(\"should not report siblings as circular\", () => {\n const shared = { id: 1 };\n\n const { parsed } = parse({ a: shared, b: shared });\n\n expect(parsed).toEqual({ a: { id: 1 }, b: { id: 1 } });\n });\n\n it(\"should serialize an Error, which JSON.stringify renders as an empty object\", () => {\n const error = new Error(\"Boom\");\n\n const { parsed } = parse({ error });\n\n expect(parsed.error.name).toBe(\"Error\");\n expect(parsed.error.message).toBe(\"Boom\");\n expect(typeof parsed.error.stack).toBe(\"string\");\n });\n\n it(\"should serialize a BigInt, which JSON.stringify throws on\", () => {\n const { parsed } = parse({ value: BigInt(123) });\n\n expect(parsed).toEqual({ value: \"123n\" });\n });\n\n it(\"should describe binary data without including its contents\", () => {\n const { parsed } = parse({ buffer: Buffer.from(\"hello world\") });\n\n expect(parsed).toEqual({ buffer: \"#[Buffer 11 bytes]\" });\n });\n\n it(\"should serialize Map and Set\", () => {\n const { parsed } = parse({\n map: new Map([[\"a\", 1]]),\n set: new Set([1, 2])\n });\n\n expect(parsed.map).toEqual({ __type: \"Map\", entries: [[\"a\", 1]] });\n expect(parsed.set).toEqual({ __type: \"Set\", items: [1, 2] });\n });\n\n it(\"should drop an undefined property, so a logged payload matches what was sent\", () => {\n const { parsed } = parse({ kept: 1, dropped: undefined });\n\n expect(parsed).toEqual({ kept: 1 });\n });\n\n it(\"should keep undefined array elements, where dropping would shift every later index\", () => {\n const { parsed } = parse({ items: [1, undefined, 3] });\n\n expect(parsed).toEqual({ items: [1, \"#[Undefined value]\", 3] });\n });\n\n it(\"should not throw when a getter throws\", () => {\n const value = {\n get exploding() {\n throw new Error(\"Nope\");\n },\n safe: 1\n };\n\n const { parsed } = parse(value);\n\n expect(parsed.exploding).toBe(\"#[Throws: Nope]\");\n expect(parsed.safe).toBe(1);\n });\n\n it(\"should cap depth\", () => {\n const { parsed } = parse({ a: { b: { c: { d: \"deep\" } } } }, { depth: 2 });\n\n expect(parsed).toEqual({ a: { b: \"#[Depth limit]\" } });\n });\n\n it(\"should cap string length and say how much was cut\", () => {\n const { parsed } = parse({ text: \"x\".repeat(20) }, { stringLength: 5 });\n\n expect(parsed).toEqual({ text: \"xxxxx…(15 more)\" });\n });\n\n it(\"should cap array length and say how much was cut\", () => {\n const { parsed } = parse({ items: [1, 2, 3, 4, 5] }, { arrayLength: 2 });\n\n expect(parsed).toEqual({ items: [1, 2, \"…(3 more items)\"] });\n });\n\n it(\"should replace the payload wholesale when it exceeds the per-entry cap\", () => {\n const result = parse({ text: \"x\".repeat(5000) }, { entryBytes: 100, stringLength: 100000 });\n\n expect(result.truncated).toBe(true);\n expect(result.parsed.truncated).toBe(true);\n expect(result.parsed.originalBytes).toBeGreaterThan(100);\n expect(result.bytes).toBeLessThanOrEqual(100);\n });\n\n it(\"should never invoke a custom toJSON, so it cannot be broken by one\", () => {\n const value = {\n id: 1,\n toJSON() {\n throw new Error(\"Nope\");\n }\n };\n\n const { parsed } = parse({ value });\n\n expect(parsed.value.id).toBe(1);\n expect(parsed.value.toJSON).toBe(\"#[Function: toJSON]\");\n });\n\n it(\"should keep non-finite numbers readable instead of turning them into null\", () => {\n const { parsed } = parse({ a: Infinity, b: NaN });\n\n expect(parsed).toEqual({ a: \"Infinity\", b: \"NaN\" });\n });\n});\n"],"names":["limits","overrides","DEFAULT_LIMITS","parse","value","result","serialize","JSON","describe","it","circular","parsed","expect","shared","error","Error","BigInt","Buffer","Map","Set","undefined","Infinity","NaN"],"mappings":"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"}
@@ -0,0 +1,100 @@
1
+ /**
2
+ * Registry of debug namespace prefixes.
3
+ *
4
+ * Packages augment this interface to claim their own prefix:
5
+ *
6
+ * ```ts
7
+ * declare module "@webiny/api-core/features/debugger/abstractions.js" {
8
+ * interface DebugNamespaces {
9
+ * cms: `cms.${string}`;
10
+ * }
11
+ * }
12
+ * ```
13
+ *
14
+ * The module specifier must be exactly the one above - a relative path augments nothing.
15
+ */
16
+ export interface DebugNamespaces {
17
+ core: `core.${string}`;
18
+ }
19
+ export type DebugNamespace = DebugNamespaces[keyof DebugNamespaces];
20
+ /**
21
+ * Produces the payload to capture. Invoked only when capture is active, so building an expensive
22
+ * payload costs nothing when the debugger is off.
23
+ */
24
+ export interface IDebugPayloadFactory {
25
+ (): unknown;
26
+ }
27
+ export interface IDebugEntry {
28
+ seq: number;
29
+ namespace: string;
30
+ timestamp: number;
31
+ /** Milliseconds since the session started. */
32
+ elapsed: number;
33
+ /** Already serialized and capped. Valid JSON. */
34
+ json: string;
35
+ bytes: number;
36
+ }
37
+ export interface IDebugSession {
38
+ namespaces: string[];
39
+ startedAt: number;
40
+ entries: IDebugEntry[];
41
+ bytes: number;
42
+ /** Entries discarded from the front of the buffer because the total budget was exceeded. */
43
+ dropped: number;
44
+ /** True once any single entry's payload was replaced by the per-entry cap marker. */
45
+ truncated: boolean;
46
+ }
47
+ export interface IDebuggerStartParams {
48
+ namespaces: string[];
49
+ }
50
+ export interface IDebugDeliveryTarget {
51
+ type: string;
52
+ }
53
+ export interface IDebuggerTransportDeliverParams {
54
+ target: IDebugDeliveryTarget;
55
+ session: IDebugSession;
56
+ }
57
+ export interface IDebugger {
58
+ /**
59
+ * Opens a capture session, replacing any existing one.
60
+ */
61
+ start(params: IDebuggerStartParams): void;
62
+ /**
63
+ * Captures a payload, if a session is open and the namespace matches its filter.
64
+ * Never throws.
65
+ */
66
+ log(namespace: DebugNamespace, payload: IDebugPayloadFactory): void;
67
+ /**
68
+ * Whether a namespace would currently be captured. For guarding expensive work *around* a log
69
+ * call - `log` already performs this check itself.
70
+ */
71
+ isEnabled(namespace: DebugNamespace): boolean;
72
+ /**
73
+ * Hands the session to every transport that can deliver it, then closes it.
74
+ */
75
+ flush(target: IDebugDeliveryTarget): Promise<void>;
76
+ /**
77
+ * Closes the session without delivering it.
78
+ */
79
+ discard(): void;
80
+ }
81
+ export interface IDebuggerTransport {
82
+ canDeliver(target: IDebugDeliveryTarget): boolean;
83
+ deliver(params: IDebuggerTransportDeliverParams): Promise<void> | void;
84
+ }
85
+ /** Captures debug payloads for the current request, for delivery to a permitted identity. */
86
+ export declare const Debugger: import("@webiny/di").Abstraction<IDebugger>;
87
+ export declare namespace Debugger {
88
+ type Interface = IDebugger;
89
+ type Session = IDebugSession;
90
+ type Entry = IDebugEntry;
91
+ type StartParams = IDebuggerStartParams;
92
+ type PayloadFactory = IDebugPayloadFactory;
93
+ }
94
+ /** Delivers a finished debug session to a sink. */
95
+ export declare const DebuggerTransport: import("@webiny/di").Abstraction<IDebuggerTransport>;
96
+ export declare namespace DebuggerTransport {
97
+ type Interface = IDebuggerTransport;
98
+ type Target = IDebugDeliveryTarget;
99
+ type DeliverParams = IDebuggerTransportDeliverParams;
100
+ }
@@ -0,0 +1,6 @@
1
+ import { createAbstraction } from "@webiny/feature/api";
2
+ const Debugger = createAbstraction("Debugger");
3
+ const DebuggerTransport = createAbstraction("DebuggerTransport");
4
+ export { Debugger, DebuggerTransport };
5
+
6
+ //# sourceMappingURL=abstractions.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"features/debugger/abstractions.js","sources":["../../../src/features/debugger/abstractions.ts"],"sourcesContent":["import { createAbstraction } from \"@webiny/feature/api\";\n\n/**\n * Registry of debug namespace prefixes.\n *\n * Packages augment this interface to claim their own prefix:\n *\n * ```ts\n * declare module \"@webiny/api-core/features/debugger/abstractions.js\" {\n * interface DebugNamespaces {\n * cms: `cms.${string}`;\n * }\n * }\n * ```\n *\n * The module specifier must be exactly the one above - a relative path augments nothing.\n */\nexport interface DebugNamespaces {\n core: `core.${string}`;\n}\n\nexport type DebugNamespace = DebugNamespaces[keyof DebugNamespaces];\n\n/**\n * Produces the payload to capture. Invoked only when capture is active, so building an expensive\n * payload costs nothing when the debugger is off.\n */\nexport interface IDebugPayloadFactory {\n (): unknown;\n}\n\nexport interface IDebugEntry {\n seq: number;\n namespace: string;\n timestamp: number;\n /** Milliseconds since the session started. */\n elapsed: number;\n /** Already serialized and capped. Valid JSON. */\n json: string;\n bytes: number;\n}\n\nexport interface IDebugSession {\n namespaces: string[];\n startedAt: number;\n entries: IDebugEntry[];\n bytes: number;\n /** Entries discarded from the front of the buffer because the total budget was exceeded. */\n dropped: number;\n /** True once any single entry's payload was replaced by the per-entry cap marker. */\n truncated: boolean;\n}\n\nexport interface IDebuggerStartParams {\n namespaces: string[];\n}\n\nexport interface IDebugDeliveryTarget {\n type: string;\n}\n\nexport interface IDebuggerTransportDeliverParams {\n target: IDebugDeliveryTarget;\n session: IDebugSession;\n}\n\nexport interface IDebugger {\n /**\n * Opens a capture session, replacing any existing one.\n */\n start(params: IDebuggerStartParams): void;\n /**\n * Captures a payload, if a session is open and the namespace matches its filter.\n * Never throws.\n */\n log(namespace: DebugNamespace, payload: IDebugPayloadFactory): void;\n /**\n * Whether a namespace would currently be captured. For guarding expensive work *around* a log\n * call - `log` already performs this check itself.\n */\n isEnabled(namespace: DebugNamespace): boolean;\n /**\n * Hands the session to every transport that can deliver it, then closes it.\n */\n flush(target: IDebugDeliveryTarget): Promise<void>;\n /**\n * Closes the session without delivering it.\n */\n discard(): void;\n}\n\nexport interface IDebuggerTransport {\n canDeliver(target: IDebugDeliveryTarget): boolean;\n deliver(params: IDebuggerTransportDeliverParams): Promise<void> | void;\n}\n\n/** Captures debug payloads for the current request, for delivery to a permitted identity. */\nexport const Debugger = createAbstraction<IDebugger>(\"Debugger\");\n\nexport namespace Debugger {\n export type Interface = IDebugger;\n export type Session = IDebugSession;\n export type Entry = IDebugEntry;\n export type StartParams = IDebuggerStartParams;\n export type PayloadFactory = IDebugPayloadFactory;\n}\n\n/** Delivers a finished debug session to a sink. */\nexport const DebuggerTransport = createAbstraction<IDebuggerTransport>(\"DebuggerTransport\");\n\nexport namespace DebuggerTransport {\n export type Interface = IDebuggerTransport;\n export type Target = IDebugDeliveryTarget;\n export type DeliverParams = IDebuggerTransportDeliverParams;\n}\n"],"names":["Debugger","createAbstraction","DebuggerTransport"],"mappings":";AAiGO,MAAMA,WAAWC,kBAA6B;AAW9C,MAAMC,oBAAoBD,kBAAsC"}
@@ -0,0 +1,5 @@
1
+ import { Container } from "@webiny/di";
2
+ export declare const DebuggerFeature: {
3
+ name: string;
4
+ register(container: Container): void;
5
+ };
@@ -0,0 +1,15 @@
1
+ import { createFeature } from "@webiny/feature/api";
2
+ import { Debugger } from "./Debugger.js";
3
+ import { GraphQLDebuggerTransport } from "./GraphQLDebuggerTransport.js";
4
+ import { DebuggerPermissionsFeature } from "./permissions.js";
5
+ const DebuggerFeature = createFeature({
6
+ name: "DebuggerFeature",
7
+ register (container) {
8
+ container.register(Debugger).inSingletonScope();
9
+ container.register(GraphQLDebuggerTransport);
10
+ DebuggerPermissionsFeature.register(container);
11
+ }
12
+ });
13
+ export { DebuggerFeature };
14
+
15
+ //# sourceMappingURL=feature.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"features/debugger/feature.js","sources":["../../../src/features/debugger/feature.ts"],"sourcesContent":["import { createFeature } from \"@webiny/feature/api\";\nimport { Container } from \"@webiny/di\";\nimport { Debugger } from \"./Debugger.js\";\nimport { GraphQLDebuggerTransport } from \"./GraphQLDebuggerTransport.js\";\nimport { DebuggerPermissionsFeature } from \"./permissions.js\";\n\nexport const DebuggerFeature = createFeature({\n name: \"DebuggerFeature\",\n register(container: Container) {\n /**\n * Must be a singleton. The session is instance state, so a transient registration would give\n * the start plugin, every call site and the flush plugin three different instances - and the\n * failure would be silent: nothing throws, nothing is reported.\n */\n container.register(Debugger).inSingletonScope();\n container.register(GraphQLDebuggerTransport);\n DebuggerPermissionsFeature.register(container);\n }\n});\n"],"names":["DebuggerFeature","createFeature","container","Debugger","GraphQLDebuggerTransport","DebuggerPermissionsFeature"],"mappings":";;;;AAMO,MAAMA,kBAAkBC,cAAc;IACzC,MAAM;IACN,UAASC,SAAoB;QAMzBA,UAAU,QAAQ,CAACC,UAAU,gBAAgB;QAC7CD,UAAU,QAAQ,CAACE;QACnBC,2BAA2B,QAAQ,CAACH;IACxC;AACJ"}
@@ -0,0 +1,7 @@
1
+ export * from "./abstractions.js";
2
+ export { DebuggerFeature } from "./feature.js";
3
+ export { createDebuggerPlugins, DEBUG_HEADER, DEBUG_PERMISSION } from "./plugins.js";
4
+ export { GRAPHQL_TARGET, type IGraphQLDeliveryTarget } from "./GraphQLDebuggerTransport.js";
5
+ export { DEFAULT_LIMITS, type IDebuggerLimits } from "./limits.js";
6
+ export { DEBUGGER_PERMISSIONS_SCHEMA } from "./permissionsSchema.js";
7
+ export { DebuggerPermissions, DebuggerPermissionsFeature } from "./permissions.js";
@@ -0,0 +1,7 @@
1
+ export * from "./abstractions.js";
2
+ export { DebuggerFeature } from "./feature.js";
3
+ export { DEBUG_HEADER, DEBUG_PERMISSION, createDebuggerPlugins } from "./plugins.js";
4
+ export { GRAPHQL_TARGET } from "./GraphQLDebuggerTransport.js";
5
+ export { DEFAULT_LIMITS } from "./limits.js";
6
+ export { DEBUGGER_PERMISSIONS_SCHEMA } from "./permissionsSchema.js";
7
+ export { DebuggerPermissions, DebuggerPermissionsFeature } from "./permissions.js";
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Capture limits. None of these are settable by the client.
3
+ */
4
+ export interface IDebuggerLimits {
5
+ /** Total serialized bytes a session may hold. Overflow drops entries from the front. */
6
+ totalBytes: number;
7
+ /** Maximum serialized size of a single entry, after the walk caps below are applied. */
8
+ entryBytes: number;
9
+ /** Runaway-loop guard. The byte budget is the real control. */
10
+ entryCount: number;
11
+ /** Maximum nesting depth walked before emitting a marker. */
12
+ depth: number;
13
+ /** Maximum characters kept from a single string value. */
14
+ stringLength: number;
15
+ /** Maximum items kept from a single array. */
16
+ arrayLength: number;
17
+ }
18
+ export declare const DEFAULT_LIMITS: IDebuggerLimits;
@@ -0,0 +1,11 @@
1
+ const DEFAULT_LIMITS = {
2
+ totalBytes: 4194304,
3
+ entryBytes: 524288,
4
+ entryCount: 10000,
5
+ depth: 10,
6
+ stringLength: 10000,
7
+ arrayLength: 100
8
+ };
9
+ export { DEFAULT_LIMITS };
10
+
11
+ //# sourceMappingURL=limits.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"features/debugger/limits.js","sources":["../../../src/features/debugger/limits.ts"],"sourcesContent":["/**\n * Capture limits. None of these are settable by the client.\n */\nexport interface IDebuggerLimits {\n /** Total serialized bytes a session may hold. Overflow drops entries from the front. */\n totalBytes: number;\n /** Maximum serialized size of a single entry, after the walk caps below are applied. */\n entryBytes: number;\n /** Runaway-loop guard. The byte budget is the real control. */\n entryCount: number;\n /** Maximum nesting depth walked before emitting a marker. */\n depth: number;\n /** Maximum characters kept from a single string value. */\n stringLength: number;\n /** Maximum items kept from a single array. */\n arrayLength: number;\n}\n\nexport const DEFAULT_LIMITS: IDebuggerLimits = {\n totalBytes: 4 * 1024 * 1024,\n entryBytes: 512 * 1024,\n entryCount: 10000,\n depth: 10,\n stringLength: 10000,\n arrayLength: 100\n};\n"],"names":["DEFAULT_LIMITS"],"mappings":"AAkBO,MAAMA,iBAAkC;IAC3C,YAAY;IACZ,YAAY;IACZ,YAAY;IACZ,OAAO;IACP,cAAc;IACd,aAAa;AACjB"}
@@ -0,0 +1,21 @@
1
+ export interface INamespaceFilter {
2
+ matches(namespace: string): boolean;
3
+ }
4
+ /**
5
+ * Builds a namespace matcher from the patterns requested by the client.
6
+ *
7
+ * A namespace matches when at least one positive pattern matches it and no negative pattern does.
8
+ *
9
+ * `minimatch` has its own native `!` negation which *inverts* the result rather than reporting a
10
+ * match, so `!cms.os.*` would return `true` for every namespace that does not match it. Patterns are
11
+ * therefore split into positive and negative lists here and matched positively, with `nonegate` set
12
+ * as a second guard.
13
+ */
14
+ export declare const createNamespaceFilter: (patterns: string[]) => INamespaceFilter;
15
+ /**
16
+ * Parses the `x-webiny-debug` header into a namespace pattern list.
17
+ *
18
+ * Returns an empty array when capture was not requested. Never throws - a malformed header must not
19
+ * be able to fail the request it arrived on.
20
+ */
21
+ export declare const parseNamespaceHeader: (value: string | string[] | undefined) => string[];
@@ -0,0 +1,48 @@
1
+ import { minimatch } from "minimatch";
2
+ const createNamespaceFilter = (patterns)=>{
3
+ const positive = [];
4
+ const negative = [];
5
+ for (const pattern of patterns){
6
+ if (pattern.startsWith("!")) {
7
+ const stripped = pattern.slice(1);
8
+ if (stripped) negative.push(stripped);
9
+ continue;
10
+ }
11
+ if (pattern) positive.push(pattern);
12
+ }
13
+ const cache = new Map();
14
+ const test = (namespace)=>{
15
+ const isIncluded = positive.some((pattern)=>minimatch(namespace, pattern, {
16
+ nonegate: true
17
+ }));
18
+ if (!isIncluded) return false;
19
+ return !negative.some((pattern)=>minimatch(namespace, pattern, {
20
+ nonegate: true
21
+ }));
22
+ };
23
+ return {
24
+ matches (namespace) {
25
+ const cached = cache.get(namespace);
26
+ if (void 0 !== cached) return cached;
27
+ const result = test(namespace);
28
+ cache.set(namespace, result);
29
+ return result;
30
+ }
31
+ };
32
+ };
33
+ const parseNamespaceHeader = (value)=>{
34
+ if (!value) return [];
35
+ const raw = Array.isArray(value) ? value.join(",") : value;
36
+ const patterns = raw.split(",").map((item)=>item.trim()).filter(Boolean);
37
+ if (0 === patterns.length) return [];
38
+ if (1 === patterns.length) {
39
+ const only = patterns[0].toLowerCase();
40
+ if ("1" === only || "true" === only) return [
41
+ "*"
42
+ ];
43
+ }
44
+ return patterns;
45
+ };
46
+ export { createNamespaceFilter, parseNamespaceHeader };
47
+
48
+ //# sourceMappingURL=matchNamespace.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"features/debugger/matchNamespace.js","sources":["../../../src/features/debugger/matchNamespace.ts"],"sourcesContent":["import { minimatch } from \"minimatch\";\n\nexport interface INamespaceFilter {\n matches(namespace: string): boolean;\n}\n\n/**\n * Builds a namespace matcher from the patterns requested by the client.\n *\n * A namespace matches when at least one positive pattern matches it and no negative pattern does.\n *\n * `minimatch` has its own native `!` negation which *inverts* the result rather than reporting a\n * match, so `!cms.os.*` would return `true` for every namespace that does not match it. Patterns are\n * therefore split into positive and negative lists here and matched positively, with `nonegate` set\n * as a second guard.\n */\nexport const createNamespaceFilter = (patterns: string[]): INamespaceFilter => {\n const positive: string[] = [];\n const negative: string[] = [];\n\n for (const pattern of patterns) {\n if (pattern.startsWith(\"!\")) {\n const stripped = pattern.slice(1);\n if (stripped) {\n negative.push(stripped);\n }\n continue;\n }\n if (pattern) {\n positive.push(pattern);\n }\n }\n\n /**\n * Matching is stable for the life of a session, so each distinct namespace is matched once.\n */\n const cache = new Map<string, boolean>();\n\n const test = (namespace: string): boolean => {\n const isIncluded = positive.some(pattern => {\n return minimatch(namespace, pattern, { nonegate: true });\n });\n\n if (!isIncluded) {\n return false;\n }\n\n return !negative.some(pattern => {\n return minimatch(namespace, pattern, { nonegate: true });\n });\n };\n\n return {\n matches(namespace) {\n const cached = cache.get(namespace);\n if (cached !== undefined) {\n return cached;\n }\n const result = test(namespace);\n cache.set(namespace, result);\n return result;\n }\n };\n};\n\n/**\n * Parses the `x-webiny-debug` header into a namespace pattern list.\n *\n * Returns an empty array when capture was not requested. Never throws - a malformed header must not\n * be able to fail the request it arrived on.\n */\nexport const parseNamespaceHeader = (value: string | string[] | undefined): string[] => {\n if (!value) {\n return [];\n }\n\n /**\n * Duplicate headers arrive comma-joined from both Node and API Gateway; the array form only\n * shows up under `app.inject`.\n */\n const raw = Array.isArray(value) ? value.join(\",\") : value;\n\n const patterns = raw\n .split(\",\")\n .map(item => item.trim())\n .filter(Boolean);\n\n if (patterns.length === 0) {\n return [];\n }\n\n /**\n * `1` and `true` are shorthand for \"everything\". Anything else is treated as a pattern list.\n */\n if (patterns.length === 1) {\n const only = patterns[0].toLowerCase();\n if (only === \"1\" || only === \"true\") {\n return [\"*\"];\n }\n }\n\n return patterns;\n};\n"],"names":["createNamespaceFilter","patterns","positive","negative","pattern","stripped","cache","Map","test","namespace","isIncluded","minimatch","cached","undefined","result","parseNamespaceHeader","value","raw","Array","item","Boolean","only"],"mappings":";AAgBO,MAAMA,wBAAwB,CAACC;IAClC,MAAMC,WAAqB,EAAE;IAC7B,MAAMC,WAAqB,EAAE;IAE7B,KAAK,MAAMC,WAAWH,SAAU;QAC5B,IAAIG,QAAQ,UAAU,CAAC,MAAM;YACzB,MAAMC,WAAWD,QAAQ,KAAK,CAAC;YAC/B,IAAIC,UACAF,SAAS,IAAI,CAACE;YAElB;QACJ;QACA,IAAID,SACAF,SAAS,IAAI,CAACE;IAEtB;IAKA,MAAME,QAAQ,IAAIC;IAElB,MAAMC,OAAO,CAACC;QACV,MAAMC,aAAaR,SAAS,IAAI,CAACE,CAAAA,UACtBO,UAAUF,WAAWL,SAAS;gBAAE,UAAU;YAAK;QAG1D,IAAI,CAACM,YACD,OAAO;QAGX,OAAO,CAACP,SAAS,IAAI,CAACC,CAAAA,UACXO,UAAUF,WAAWL,SAAS;gBAAE,UAAU;YAAK;IAE9D;IAEA,OAAO;QACH,SAAQK,SAAS;YACb,MAAMG,SAASN,MAAM,GAAG,CAACG;YACzB,IAAIG,AAAWC,WAAXD,QACA,OAAOA;YAEX,MAAME,SAASN,KAAKC;YACpBH,MAAM,GAAG,CAACG,WAAWK;YACrB,OAAOA;QACX;IACJ;AACJ;AAQO,MAAMC,uBAAuB,CAACC;IACjC,IAAI,CAACA,OACD,OAAO,EAAE;IAOb,MAAMC,MAAMC,MAAM,OAAO,CAACF,SAASA,MAAM,IAAI,CAAC,OAAOA;IAErD,MAAMf,WAAWgB,IACZ,KAAK,CAAC,KACN,GAAG,CAACE,CAAAA,OAAQA,KAAK,IAAI,IACrB,MAAM,CAACC;IAEZ,IAAInB,AAAoB,MAApBA,SAAS,MAAM,EACf,OAAO,EAAE;IAMb,IAAIA,AAAoB,MAApBA,SAAS,MAAM,EAAQ;QACvB,MAAMoB,OAAOpB,QAAQ,CAAC,EAAE,CAAC,WAAW;QACpC,IAAIoB,AAAS,QAATA,QAAgBA,AAAS,WAATA,MAChB,OAAO;YAAC;SAAI;IAEpB;IAEA,OAAOpB;AACX"}
@@ -0,0 +1,33 @@
1
+ import { type Permissions } from "../../features/security/permissions/index.js";
2
+ import type { IIdentityContext } from "../../features/security/IdentityContext/abstractions.js";
3
+ import { DEBUGGER_PERMISSIONS_SCHEMA } from "./permissionsSchema.js";
4
+ export declare const DEBUG_PERMISSION_NAME = "dev-tools.debug";
5
+ export declare const DebuggerPermissions: import("@webiny/di").Abstraction<import("../security/permissions/types.js").PermissionsTyped<{
6
+ readonly prefix: "dev-tools";
7
+ readonly fullAccess: true;
8
+ readonly entities: readonly [{
9
+ readonly id: "debug";
10
+ readonly title: "Debugger";
11
+ readonly permission: "dev-tools.debug";
12
+ readonly scopes: readonly ["full"];
13
+ }];
14
+ }>>;
15
+ export declare namespace DebuggerPermissions {
16
+ type Interface = Permissions<typeof DEBUGGER_PERMISSIONS_SCHEMA>;
17
+ }
18
+ export declare const DebuggerPermissionsFeature: {
19
+ name: string;
20
+ register(container: import("@webiny/di").Container): void;
21
+ };
22
+ /**
23
+ * Whether an identity may capture debug data.
24
+ *
25
+ * Deliberately an exact match rather than `canAccess`, which grants on `*` and on `dev-tools.*`.
26
+ * Capture runs automatically for whoever holds this, so a wildcard would leave it permanently on for
27
+ * every full-access administrator - collecting record content on every request, indefinitely, for
28
+ * people who never asked for it.
29
+ *
30
+ * The consequence, which is intended: full access is not enough. The entity has to be granted
31
+ * deliberately, and that act is the opt-in.
32
+ */
33
+ export declare const canCaptureDebugData: (identityContext: IIdentityContext) => Promise<boolean>;
@@ -0,0 +1,12 @@
1
+ import { createPermissionsAbstraction, createPermissionsFeature } from "../security/permissions/index.js";
2
+ import { DEBUGGER_PERMISSIONS_SCHEMA } from "./permissionsSchema.js";
3
+ const DEBUG_PERMISSION_NAME = "dev-tools.debug";
4
+ const DebuggerPermissions = createPermissionsAbstraction(DEBUGGER_PERMISSIONS_SCHEMA);
5
+ const DebuggerPermissionsFeature = createPermissionsFeature(DEBUGGER_PERMISSIONS_SCHEMA, DebuggerPermissions);
6
+ const canCaptureDebugData = async (identityContext)=>{
7
+ const permissions = await identityContext.listPermissions();
8
+ return permissions.some((permission)=>permission.name === DEBUG_PERMISSION_NAME);
9
+ };
10
+ export { DEBUG_PERMISSION_NAME, DebuggerPermissions, DebuggerPermissionsFeature, canCaptureDebugData };
11
+
12
+ //# sourceMappingURL=permissions.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"features/debugger/permissions.js","sources":["../../../src/features/debugger/permissions.ts"],"sourcesContent":["import {\n createPermissionsAbstraction,\n createPermissionsFeature,\n type Permissions\n} from \"~/features/security/permissions/index.js\";\nimport type { IIdentityContext } from \"~/features/security/IdentityContext/abstractions.js\";\nimport type { SecurityPermission } from \"~/types/security.js\";\nimport { DEBUGGER_PERMISSIONS_SCHEMA } from \"./permissionsSchema.js\";\n\nexport const DEBUG_PERMISSION_NAME = \"dev-tools.debug\";\n\nexport const DebuggerPermissions = createPermissionsAbstraction(DEBUGGER_PERMISSIONS_SCHEMA);\n\nexport namespace DebuggerPermissions {\n export type Interface = Permissions<typeof DEBUGGER_PERMISSIONS_SCHEMA>;\n}\n\nexport const DebuggerPermissionsFeature = createPermissionsFeature(\n DEBUGGER_PERMISSIONS_SCHEMA,\n DebuggerPermissions\n);\n\n/**\n * Whether an identity may capture debug data.\n *\n * Deliberately an exact match rather than `canAccess`, which grants on `*` and on `dev-tools.*`.\n * Capture runs automatically for whoever holds this, so a wildcard would leave it permanently on for\n * every full-access administrator - collecting record content on every request, indefinitely, for\n * people who never asked for it.\n *\n * The consequence, which is intended: full access is not enough. The entity has to be granted\n * deliberately, and that act is the opt-in.\n */\nexport const canCaptureDebugData = async (identityContext: IIdentityContext): Promise<boolean> => {\n const permissions = await identityContext.listPermissions();\n return permissions.some(\n (permission: SecurityPermission) => permission.name === DEBUG_PERMISSION_NAME\n );\n};\n"],"names":["DEBUG_PERMISSION_NAME","DebuggerPermissions","createPermissionsAbstraction","DEBUGGER_PERMISSIONS_SCHEMA","DebuggerPermissionsFeature","createPermissionsFeature","canCaptureDebugData","identityContext","permissions","permission"],"mappings":";;AASO,MAAMA,wBAAwB;AAE9B,MAAMC,sBAAsBC,6BAA6BC;AAMzD,MAAMC,6BAA6BC,yBACtCF,6BACAF;AAcG,MAAMK,sBAAsB,OAAOC;IACtC,MAAMC,cAAc,MAAMD,gBAAgB,eAAe;IACzD,OAAOC,YAAY,IAAI,CACnB,CAACC,aAAmCA,WAAW,IAAI,KAAKT;AAEhE"}
@@ -0,0 +1,17 @@
1
+ /**
2
+ * The debugger lives under the `dev-tools` prefix, alongside the playgrounds.
3
+ *
4
+ * Note the consequence: `fullAccess: true` emits `dev-tools.*`, and permission matching is glob
5
+ * based, so any role holding Dev Tools full access can capture debug data without `dev-tools.debug`
6
+ * being granted explicitly.
7
+ */
8
+ export declare const DEBUGGER_PERMISSIONS_SCHEMA: {
9
+ readonly prefix: "dev-tools";
10
+ readonly fullAccess: true;
11
+ readonly entities: readonly [{
12
+ readonly id: "debug";
13
+ readonly title: "Debugger";
14
+ readonly permission: "dev-tools.debug";
15
+ readonly scopes: readonly ["full"];
16
+ }];
17
+ };
@@ -0,0 +1,18 @@
1
+ import { createPermissionSchema } from "../security/permissions/index.js";
2
+ const DEBUGGER_PERMISSIONS_SCHEMA = createPermissionSchema({
3
+ prefix: "dev-tools",
4
+ fullAccess: true,
5
+ entities: [
6
+ {
7
+ id: "debug",
8
+ title: "Debugger",
9
+ permission: "dev-tools.debug",
10
+ scopes: [
11
+ "full"
12
+ ]
13
+ }
14
+ ]
15
+ });
16
+ export { DEBUGGER_PERMISSIONS_SCHEMA };
17
+
18
+ //# sourceMappingURL=permissionsSchema.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"features/debugger/permissionsSchema.js","sources":["../../../src/features/debugger/permissionsSchema.ts"],"sourcesContent":["import { createPermissionSchema } from \"~/features/security/permissions/index.js\";\n\n/**\n * The debugger lives under the `dev-tools` prefix, alongside the playgrounds.\n *\n * Note the consequence: `fullAccess: true` emits `dev-tools.*`, and permission matching is glob\n * based, so any role holding Dev Tools full access can capture debug data without `dev-tools.debug`\n * being granted explicitly.\n */\nexport const DEBUGGER_PERMISSIONS_SCHEMA = createPermissionSchema({\n prefix: \"dev-tools\",\n fullAccess: true,\n entities: [\n {\n id: \"debug\",\n title: \"Debugger\",\n permission: \"dev-tools.debug\",\n scopes: [\"full\"]\n }\n ]\n});\n"],"names":["DEBUGGER_PERMISSIONS_SCHEMA","createPermissionSchema"],"mappings":";AASO,MAAMA,8BAA8BC,uBAAuB;IAC9D,QAAQ;IACR,YAAY;IACZ,UAAU;QACN;YACI,IAAI;YACJ,OAAO;YACP,YAAY;YACZ,QAAQ;gBAAC;aAAO;QACpB;KACH;AACL"}
@@ -0,0 +1,4 @@
1
+ import type { PluginCollection } from "@webiny/plugins/types.js";
2
+ export declare const DEBUG_HEADER = "x-webiny-debug";
3
+ export declare const DEBUG_PERMISSION = "dev-tools.debug";
4
+ export declare const createDebuggerPlugins: () => PluginCollection;
@@ -0,0 +1,82 @@
1
+ import { Request, RequestId, createBeforeHandlerPlugin, createHandlerResultPlugin } from "@webiny/handler";
2
+ import { Debugger } from "./abstractions.js";
3
+ import { IdentityContext } from "../security/IdentityContext/index.js";
4
+ import { parseNamespaceHeader } from "./matchNamespace.js";
5
+ import { canCaptureDebugData } from "./permissions.js";
6
+ import { GRAPHQL_TARGET } from "./GraphQLDebuggerTransport.js";
7
+ const DEBUG_HEADER = "x-webiny-debug";
8
+ const DEBUG_PERMISSION = "dev-tools.debug";
9
+ const isPlainObject = (value)=>"object" == typeof value && null !== value && !Array.isArray(value);
10
+ const isGraphQLResult = (value)=>isPlainObject(value) && ("data" in value || "errors" in value);
11
+ const resolveTarget = (payload)=>{
12
+ const candidate = Array.isArray(payload) ? payload.at(-1) : payload;
13
+ return isGraphQLResult(candidate) ? candidate : void 0;
14
+ };
15
+ const readOperationNames = (body)=>{
16
+ const operations = Array.isArray(body) ? body : [
17
+ body
18
+ ];
19
+ return operations.map((operation)=>isPlainObject(operation) && "string" == typeof operation.operationName ? operation.operationName : void 0).filter((name)=>Boolean(name));
20
+ };
21
+ const createStartPlugin = ()=>createBeforeHandlerPlugin(async (context)=>{
22
+ try {
23
+ const request = context.container.resolve(Request);
24
+ const requested = parseNamespaceHeader(request.headers[DEBUG_HEADER]);
25
+ const namespaces = requested.length > 0 ? requested : [
26
+ "*"
27
+ ];
28
+ context.container.resolve(Debugger).start({
29
+ namespaces
30
+ });
31
+ } catch {}
32
+ });
33
+ const createFlushPlugin = ()=>createHandlerResultPlugin(async (context, payload)=>{
34
+ try {
35
+ const debuggerService = context.container.resolve(Debugger);
36
+ const identityContext = context.container.resolve(IdentityContext);
37
+ const result = resolveTarget(payload);
38
+ if (!result) {
39
+ debuggerService.discard();
40
+ return payload;
41
+ }
42
+ if (identityContext.getIdentity().isAnonymous()) {
43
+ debuggerService.discard();
44
+ return payload;
45
+ }
46
+ const isAuthorized = identityContext.isAuthorizationEnabled() && await canCaptureDebugData(identityContext);
47
+ if (!isAuthorized) {
48
+ debuggerService.discard();
49
+ return payload;
50
+ }
51
+ let operations = [];
52
+ let requestId = "";
53
+ let url = "";
54
+ try {
55
+ const request = context.container.resolve(Request);
56
+ operations = readOperationNames(request.body);
57
+ requestId = context.container.resolve(RequestId).value;
58
+ url = request.url || "";
59
+ } catch {}
60
+ const target = {
61
+ type: GRAPHQL_TARGET,
62
+ result,
63
+ requestId,
64
+ operations,
65
+ url
66
+ };
67
+ await debuggerService.flush(target);
68
+ } catch (ex) {
69
+ console.error("Debugger failed to flush.", ex instanceof Error ? `${ex.name}: ${ex.message}` : "Unknown error.");
70
+ try {
71
+ context.container.resolve(Debugger).discard();
72
+ } catch {}
73
+ }
74
+ return payload;
75
+ });
76
+ const createDebuggerPlugins = ()=>[
77
+ createStartPlugin(),
78
+ createFlushPlugin()
79
+ ];
80
+ export { DEBUG_HEADER, DEBUG_PERMISSION, createDebuggerPlugins };
81
+
82
+ //# sourceMappingURL=plugins.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"features/debugger/plugins.js","sources":["../../../src/features/debugger/plugins.ts"],"sourcesContent":["import {\n createBeforeHandlerPlugin,\n createHandlerResultPlugin,\n Request,\n RequestId\n} from \"@webiny/handler\";\nimport type { PluginCollection } from \"@webiny/plugins/types.js\";\nimport type { Context } from \"@webiny/handler/types.js\";\nimport { Debugger } from \"./abstractions.js\";\nimport { IdentityContext } from \"~/features/security/IdentityContext/index.js\";\nimport { parseNamespaceHeader } from \"./matchNamespace.js\";\nimport { canCaptureDebugData } from \"./permissions.js\";\nimport { GRAPHQL_TARGET, type IGraphQLDeliveryTarget } from \"./GraphQLDebuggerTransport.js\";\n\nexport const DEBUG_HEADER = \"x-webiny-debug\";\nexport const DEBUG_PERMISSION = \"dev-tools.debug\";\n\nconst isPlainObject = (value: unknown): value is Record<string, any> => {\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\n};\n\n/**\n * A GraphQL result carries `data` or `errors`. Anything else - asset delivery, a plain string body -\n * is not ours to write to.\n */\nconst isGraphQLResult = (value: unknown): value is Record<string, any> => {\n return isPlainObject(value) && (\"data\" in value || \"errors\" in value);\n};\n\n/**\n * Batched requests produce an array of results. The session spans the whole HTTP request, so it is\n * attached once, to the last element - duplicating it per operation would multiply it against the\n * byte budget.\n */\nconst resolveTarget = (payload: unknown): Record<string, any> | undefined => {\n const candidate = Array.isArray(payload) ? payload.at(-1) : payload;\n return isGraphQLResult(candidate) ? candidate : undefined;\n};\n\nconst readOperationNames = (body: unknown): string[] => {\n const operations = Array.isArray(body) ? body : [body];\n return operations\n .map(operation => {\n return isPlainObject(operation) && typeof operation.operationName === \"string\"\n ? operation.operationName\n : undefined;\n })\n .filter((name): name is string => Boolean(name));\n};\n\n/**\n * Opens the capture session.\n *\n * Runs as a `BeforeHandlerPlugin`, so it is in place before the route handler and therefore before\n * any resolver. Note that `ContextPlugin`s run earlier still, so anything logged from one of those\n * is not captured.\n */\nconst createStartPlugin = () => {\n return createBeforeHandlerPlugin<Context>(async context => {\n /**\n * Must never throw: `ProcessBeforeHandlerPlugins` rethrows, which would turn a malformed\n * header into a 500 for the whole request.\n */\n try {\n const request = context.container.resolve(Request);\n\n /**\n * A session starts on every request, and the permission check at flush decides whether\n * anything is delivered or the whole thing is discarded.\n *\n * Nothing is required from the client. Asking for a request header would mean putting it\n * on the CORS allow-list, which is cached in preflight responses at CloudFront and in\n * the browser for a day - so rolling one out breaks requests intermittently until every\n * cache entry expires.\n *\n * The header is still honoured when present, so a direct API client can narrow what it\n * captures.\n */\n const requested = parseNamespaceHeader(request.headers[DEBUG_HEADER]);\n const namespaces = requested.length > 0 ? requested : [\"*\"];\n\n context.container.resolve(Debugger).start({ namespaces });\n } catch {\n // A debugger that cannot start is not a reason to fail the request.\n }\n });\n};\n\n/**\n * Performs the permission gate and hands the session to the transports.\n *\n * This runs once per HTTP response, in the `preSerialization` hook - deliberately not in\n * `graphql-after-query`, which also fires for the nested executions that CMS resolvers perform\n * internally and would attach the session to a result that is then discarded.\n */\nconst createFlushPlugin = () => {\n return createHandlerResultPlugin<Context>(async (context, payload) => {\n /**\n * The whole body is guarded: `preSerialization` rethrows whatever a `HandlerResultPlugin`\n * throws, so an error here would turn a working response into a 500 - and only for requests\n * that asked for debugging.\n */\n try {\n const debuggerService = context.container.resolve(Debugger);\n const identityContext = context.container.resolve(IdentityContext);\n\n const result = resolveTarget(payload);\n if (!result) {\n debuggerService.discard();\n return payload;\n }\n\n /**\n * Anonymous callers are rejected before permissions are resolved: `getPermission` runs\n * every registered Authorizer, some of which hit the database.\n */\n if (identityContext.getIdentity().isAnonymous()) {\n debuggerService.discard();\n return payload;\n }\n\n /**\n * `getPermission` returns a wildcard permission whenever authorization is disabled, so a\n * flush occurring inside a `withoutAuthorization` scope would pass for anyone.\n */\n const isAuthorized =\n identityContext.isAuthorizationEnabled() &&\n (await canCaptureDebugData(identityContext));\n\n if (!isAuthorized) {\n /**\n * Nothing is written. Reporting the denial would tell every unauthorized caller that\n * the feature exists, and the Admin app already knows from the identity's own\n * permissions whether capture is available to it.\n */\n debuggerService.discard();\n return payload;\n }\n\n let operations: string[] = [];\n let requestId = \"\";\n let url = \"\";\n try {\n const request = context.container.resolve(Request);\n operations = readOperationNames(request.body);\n requestId = context.container.resolve(RequestId).value;\n /**\n * Which endpoint this was. An Admin session talks to several - the main API and the\n * CMS manage, read and preview endpoints - and knowing which one produced an entry\n * is most of the context when reading a report.\n */\n url = request.url || \"\";\n } catch {\n // All three are conveniences; their absence must not stop the flush.\n }\n\n const target: IGraphQLDeliveryTarget = {\n type: GRAPHQL_TARGET,\n result,\n requestId,\n operations,\n url\n };\n\n await debuggerService.flush(target);\n } catch (ex) {\n /**\n * Only the message - never the session or the payload.\n */\n console.error(\n \"Debugger failed to flush.\",\n ex instanceof Error ? `${ex.name}: ${ex.message}` : \"Unknown error.\"\n );\n try {\n context.container.resolve(Debugger).discard();\n } catch {\n // Nothing further to do.\n }\n }\n\n /**\n * `preSerialization` ignores this return value and serializes the original payload object,\n * so every write above mutates `result` in place.\n */\n return payload;\n });\n};\n\nexport const createDebuggerPlugins = (): PluginCollection => {\n return [createStartPlugin(), createFlushPlugin()];\n};\n"],"names":["DEBUG_HEADER","DEBUG_PERMISSION","isPlainObject","value","Array","isGraphQLResult","resolveTarget","payload","candidate","undefined","readOperationNames","body","operations","operation","name","Boolean","createStartPlugin","createBeforeHandlerPlugin","context","request","Request","requested","parseNamespaceHeader","namespaces","Debugger","createFlushPlugin","createHandlerResultPlugin","debuggerService","identityContext","IdentityContext","result","isAuthorized","canCaptureDebugData","requestId","url","RequestId","target","GRAPHQL_TARGET","ex","console","Error","createDebuggerPlugins"],"mappings":";;;;;;AAcO,MAAMA,eAAe;AACrB,MAAMC,mBAAmB;AAEhC,MAAMC,gBAAgB,CAACC,QACZ,AAAiB,YAAjB,OAAOA,SAAsBA,AAAU,SAAVA,SAAkB,CAACC,MAAM,OAAO,CAACD;AAOzE,MAAME,kBAAkB,CAACF,QACdD,cAAcC,UAAW,WAAUA,SAAS,YAAYA,KAAI;AAQvE,MAAMG,gBAAgB,CAACC;IACnB,MAAMC,YAAYJ,MAAM,OAAO,CAACG,WAAWA,QAAQ,EAAE,CAAC,MAAMA;IAC5D,OAAOF,gBAAgBG,aAAaA,YAAYC;AACpD;AAEA,MAAMC,qBAAqB,CAACC;IACxB,MAAMC,aAAaR,MAAM,OAAO,CAACO,QAAQA,OAAO;QAACA;KAAK;IACtD,OAAOC,WACF,GAAG,CAACC,CAAAA,YACMX,cAAcW,cAAc,AAAmC,YAAnC,OAAOA,UAAU,aAAa,GAC3DA,UAAU,aAAa,GACvBJ,QAET,MAAM,CAAC,CAACK,OAAyBC,QAAQD;AAClD;AASA,MAAME,oBAAoB,IACfC,0BAAmC,OAAMC;QAK5C,IAAI;YACA,MAAMC,UAAUD,QAAQ,SAAS,CAAC,OAAO,CAACE;YAc1C,MAAMC,YAAYC,qBAAqBH,QAAQ,OAAO,CAACnB,aAAa;YACpE,MAAMuB,aAAaF,UAAU,MAAM,GAAG,IAAIA,YAAY;gBAAC;aAAI;YAE3DH,QAAQ,SAAS,CAAC,OAAO,CAACM,UAAU,KAAK,CAAC;gBAAED;YAAW;QAC3D,EAAE,OAAM,CAER;IACJ;AAUJ,MAAME,oBAAoB,IACfC,0BAAmC,OAAOR,SAASX;QAMtD,IAAI;YACA,MAAMoB,kBAAkBT,QAAQ,SAAS,CAAC,OAAO,CAACM;YAClD,MAAMI,kBAAkBV,QAAQ,SAAS,CAAC,OAAO,CAACW;YAElD,MAAMC,SAASxB,cAAcC;YAC7B,IAAI,CAACuB,QAAQ;gBACTH,gBAAgB,OAAO;gBACvB,OAAOpB;YACX;YAMA,IAAIqB,gBAAgB,WAAW,GAAG,WAAW,IAAI;gBAC7CD,gBAAgB,OAAO;gBACvB,OAAOpB;YACX;YAMA,MAAMwB,eACFH,gBAAgB,sBAAsB,MACrC,MAAMI,oBAAoBJ;YAE/B,IAAI,CAACG,cAAc;gBAMfJ,gBAAgB,OAAO;gBACvB,OAAOpB;YACX;YAEA,IAAIK,aAAuB,EAAE;YAC7B,IAAIqB,YAAY;YAChB,IAAIC,MAAM;YACV,IAAI;gBACA,MAAMf,UAAUD,QAAQ,SAAS,CAAC,OAAO,CAACE;gBAC1CR,aAAaF,mBAAmBS,QAAQ,IAAI;gBAC5Cc,YAAYf,QAAQ,SAAS,CAAC,OAAO,CAACiB,WAAW,KAAK;gBAMtDD,MAAMf,QAAQ,GAAG,IAAI;YACzB,EAAE,OAAM,CAER;YAEA,MAAMiB,SAAiC;gBACnC,MAAMC;gBACNP;gBACAG;gBACArB;gBACAsB;YACJ;YAEA,MAAMP,gBAAgB,KAAK,CAACS;QAChC,EAAE,OAAOE,IAAI;YAITC,QAAQ,KAAK,CACT,6BACAD,cAAcE,QAAQ,GAAGF,GAAG,IAAI,CAAC,EAAE,EAAEA,GAAG,OAAO,EAAE,GAAG;YAExD,IAAI;gBACApB,QAAQ,SAAS,CAAC,OAAO,CAACM,UAAU,OAAO;YAC/C,EAAE,OAAM,CAER;QACJ;QAMA,OAAOjB;IACX;AAGG,MAAMkC,wBAAwB,IAC1B;QAACzB;QAAqBS;KAAoB"}
@@ -0,0 +1,11 @@
1
+ import type { IDebuggerLimits } from "./limits.js";
2
+ export interface ISerializeResult {
3
+ json: string;
4
+ bytes: number;
5
+ /** True if the value was replaced by the per-entry cap marker. */
6
+ truncated: boolean;
7
+ }
8
+ /**
9
+ * Serializes a captured payload to JSON, never throwing and always producing valid JSON.
10
+ */
11
+ export declare const serialize: (value: unknown, limits: IDebuggerLimits) => ISerializeResult;