@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.
- package/ApiCoreFeature.js +2 -0
- package/ApiCoreFeature.js.map +1 -1
- package/createApiCore.d.ts +1 -1
- package/createApiCore.js +3 -1
- package/createApiCore.js.map +1 -1
- package/features/debugger/Debugger.d.ts +23 -0
- package/features/debugger/Debugger.js +134 -0
- package/features/debugger/Debugger.js.map +1 -0
- package/features/debugger/GraphQLDebuggerTransport.d.ts +26 -0
- package/features/debugger/GraphQLDebuggerTransport.js +45 -0
- package/features/debugger/GraphQLDebuggerTransport.js.map +1 -0
- package/features/debugger/__tests__/matchNamespace.test.d.ts +1 -0
- package/features/debugger/__tests__/matchNamespace.test.js +67 -0
- package/features/debugger/__tests__/matchNamespace.test.js.map +1 -0
- package/features/debugger/__tests__/serialize.test.d.ts +1 -0
- package/features/debugger/__tests__/serialize.test.js +220 -0
- package/features/debugger/__tests__/serialize.test.js.map +1 -0
- package/features/debugger/abstractions.d.ts +100 -0
- package/features/debugger/abstractions.js +6 -0
- package/features/debugger/abstractions.js.map +1 -0
- package/features/debugger/feature.d.ts +5 -0
- package/features/debugger/feature.js +15 -0
- package/features/debugger/feature.js.map +1 -0
- package/features/debugger/index.d.ts +7 -0
- package/features/debugger/index.js +7 -0
- package/features/debugger/limits.d.ts +18 -0
- package/features/debugger/limits.js +11 -0
- package/features/debugger/limits.js.map +1 -0
- package/features/debugger/matchNamespace.d.ts +21 -0
- package/features/debugger/matchNamespace.js +48 -0
- package/features/debugger/matchNamespace.js.map +1 -0
- package/features/debugger/permissions.d.ts +33 -0
- package/features/debugger/permissions.js +12 -0
- package/features/debugger/permissions.js.map +1 -0
- package/features/debugger/permissionsSchema.d.ts +17 -0
- package/features/debugger/permissionsSchema.js +18 -0
- package/features/debugger/permissionsSchema.js.map +1 -0
- package/features/debugger/plugins.d.ts +4 -0
- package/features/debugger/plugins.js +82 -0
- package/features/debugger/plugins.js.map +1 -0
- package/features/debugger/serialize.d.ts +11 -0
- package/features/debugger/serialize.js +121 -0
- package/features/debugger/serialize.js.map +1 -0
- package/legacy/security/plugins/secureHeaders.js +1 -0
- package/legacy/security/plugins/secureHeaders.js.map +1 -1
- package/package.json +14 -14
package/ApiCoreFeature.js
CHANGED
|
@@ -8,6 +8,7 @@ import { AdminUsersFeature } from "./features/users/AdminUsersFeature.js";
|
|
|
8
8
|
import { IdpAuthenticatorFeature } from "./idp/feature.js";
|
|
9
9
|
import { KeyValueStoreFeature } from "./features/keyValueStore/feature.js";
|
|
10
10
|
import { BuildParamsFeature } from "./features/buildParams/feature.js";
|
|
11
|
+
import { DebuggerFeature } from "./features/debugger/feature.js";
|
|
11
12
|
import { EncryptionFeature } from "./features/encryption/feature.js";
|
|
12
13
|
import { FeatureFlagsFeature } from "./features/featureFlags/feature.js";
|
|
13
14
|
import { MaskerFeature } from "./features/masker/feature.js";
|
|
@@ -22,6 +23,7 @@ const ApiCoreFeature = createFeature({
|
|
|
22
23
|
LoggerFeature.register(container);
|
|
23
24
|
EventPublisherFeature.register(container);
|
|
24
25
|
BuildParamsFeature.register(container);
|
|
26
|
+
DebuggerFeature.register(container);
|
|
25
27
|
EncryptionFeature.register(container);
|
|
26
28
|
FeatureFlagsFeature.register(container);
|
|
27
29
|
TenancyFeature.register(container, config.tenancyStorageOperations);
|
package/ApiCoreFeature.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ApiCoreFeature.js","sources":["../src/ApiCoreFeature.ts"],"sourcesContent":["import { createFeature } from \"@webiny/feature/api\";\nimport { Container } from \"@webiny/di\";\nimport { EventPublisherFeature } from \"~/features/eventPublisher/feature.js\";\nimport { LoggerFeature } from \"~/features/logger/feature.js\";\nimport { SecurityFeature } from \"~/features/security/SecurityFeature.js\";\nimport { SystemFeature } from \"~/features/system/SystemFeature.js\";\nimport { TenancyFeature } from \"./features/tenancy/TenancyFeature.js\";\nimport { AdminUsersFeature } from \"~/features/users/AdminUsersFeature.js\";\nimport type { ApiCoreStorageOperations } from \"~/types/core.js\";\nimport { IdpAuthenticatorFeature } from \"~/idp/feature.js\";\nimport { KeyValueStoreFeature } from \"~/features/keyValueStore/feature.js\";\nimport { BuildParamsFeature } from \"~/features/buildParams/feature.js\";\nimport { EncryptionFeature } from \"~/features/encryption/feature.js\";\nimport { FeatureFlagsFeature } from \"~/features/featureFlags/feature.js\";\nimport { MaskerFeature } from \"~/features/masker/feature.js\";\nimport { AiFeature } from \"~/features/ai/feature.js\";\nimport { NullWebhookDispatcher } from \"./features/webhooks/WebhookDispatcher/NullWebhookDispatcher.js\";\nimport { WebhookProviderFeature } from \"~/features/webhooks/index.js\";\n\nexport const ApiCoreFeature = createFeature({\n name: \"ApiCore\",\n register(container: Container, config: ApiCoreStorageOperations) {\n // Register features\n MaskerFeature.register(container);\n AiFeature.register(container);\n LoggerFeature.register(container);\n EventPublisherFeature.register(container);\n BuildParamsFeature.register(container);\n EncryptionFeature.register(container);\n FeatureFlagsFeature.register(container);\n TenancyFeature.register(container, config.tenancyStorageOperations);\n SecurityFeature.register(container, config.securityStorageOperations);\n AdminUsersFeature.register(container, config.usersStorageOperations);\n KeyValueStoreFeature.register(container, config.keyValueStorageOperations);\n SystemFeature.register(container);\n IdpAuthenticatorFeature.register(container);\n container.register(NullWebhookDispatcher).inSingletonScope();\n WebhookProviderFeature.register(container);\n }\n});\n"],"names":["ApiCoreFeature","createFeature","container","config","MaskerFeature","AiFeature","LoggerFeature","EventPublisherFeature","BuildParamsFeature","EncryptionFeature","FeatureFlagsFeature","TenancyFeature","SecurityFeature","AdminUsersFeature","KeyValueStoreFeature","SystemFeature","IdpAuthenticatorFeature","NullWebhookDispatcher","WebhookProviderFeature"],"mappings":"
|
|
1
|
+
{"version":3,"file":"ApiCoreFeature.js","sources":["../src/ApiCoreFeature.ts"],"sourcesContent":["import { createFeature } from \"@webiny/feature/api\";\nimport { Container } from \"@webiny/di\";\nimport { EventPublisherFeature } from \"~/features/eventPublisher/feature.js\";\nimport { LoggerFeature } from \"~/features/logger/feature.js\";\nimport { SecurityFeature } from \"~/features/security/SecurityFeature.js\";\nimport { SystemFeature } from \"~/features/system/SystemFeature.js\";\nimport { TenancyFeature } from \"./features/tenancy/TenancyFeature.js\";\nimport { AdminUsersFeature } from \"~/features/users/AdminUsersFeature.js\";\nimport type { ApiCoreStorageOperations } from \"~/types/core.js\";\nimport { IdpAuthenticatorFeature } from \"~/idp/feature.js\";\nimport { KeyValueStoreFeature } from \"~/features/keyValueStore/feature.js\";\nimport { BuildParamsFeature } from \"~/features/buildParams/feature.js\";\nimport { DebuggerFeature } from \"~/features/debugger/feature.js\";\nimport { EncryptionFeature } from \"~/features/encryption/feature.js\";\nimport { FeatureFlagsFeature } from \"~/features/featureFlags/feature.js\";\nimport { MaskerFeature } from \"~/features/masker/feature.js\";\nimport { AiFeature } from \"~/features/ai/feature.js\";\nimport { NullWebhookDispatcher } from \"./features/webhooks/WebhookDispatcher/NullWebhookDispatcher.js\";\nimport { WebhookProviderFeature } from \"~/features/webhooks/index.js\";\n\nexport const ApiCoreFeature = createFeature({\n name: \"ApiCore\",\n register(container: Container, config: ApiCoreStorageOperations) {\n // Register features\n MaskerFeature.register(container);\n AiFeature.register(container);\n LoggerFeature.register(container);\n EventPublisherFeature.register(container);\n BuildParamsFeature.register(container);\n DebuggerFeature.register(container);\n EncryptionFeature.register(container);\n FeatureFlagsFeature.register(container);\n TenancyFeature.register(container, config.tenancyStorageOperations);\n SecurityFeature.register(container, config.securityStorageOperations);\n AdminUsersFeature.register(container, config.usersStorageOperations);\n KeyValueStoreFeature.register(container, config.keyValueStorageOperations);\n SystemFeature.register(container);\n IdpAuthenticatorFeature.register(container);\n container.register(NullWebhookDispatcher).inSingletonScope();\n WebhookProviderFeature.register(container);\n }\n});\n"],"names":["ApiCoreFeature","createFeature","container","config","MaskerFeature","AiFeature","LoggerFeature","EventPublisherFeature","BuildParamsFeature","DebuggerFeature","EncryptionFeature","FeatureFlagsFeature","TenancyFeature","SecurityFeature","AdminUsersFeature","KeyValueStoreFeature","SystemFeature","IdpAuthenticatorFeature","NullWebhookDispatcher","WebhookProviderFeature"],"mappings":";;;;;;;;;;;;;;;;;AAoBO,MAAMA,iBAAiBC,cAAc;IACxC,MAAM;IACN,UAASC,SAAoB,EAAEC,MAAgC;QAE3DC,cAAc,QAAQ,CAACF;QACvBG,UAAU,QAAQ,CAACH;QACnBI,cAAc,QAAQ,CAACJ;QACvBK,sBAAsB,QAAQ,CAACL;QAC/BM,mBAAmB,QAAQ,CAACN;QAC5BO,gBAAgB,QAAQ,CAACP;QACzBQ,kBAAkB,QAAQ,CAACR;QAC3BS,oBAAoB,QAAQ,CAACT;QAC7BU,eAAe,QAAQ,CAACV,WAAWC,OAAO,wBAAwB;QAClEU,gBAAgB,QAAQ,CAACX,WAAWC,OAAO,yBAAyB;QACpEW,kBAAkB,QAAQ,CAACZ,WAAWC,OAAO,sBAAsB;QACnEY,qBAAqB,QAAQ,CAACb,WAAWC,OAAO,yBAAyB;QACzEa,cAAc,QAAQ,CAACd;QACvBe,wBAAwB,QAAQ,CAACf;QACjCA,UAAU,QAAQ,CAACgB,uBAAuB,gBAAgB;QAC1DC,uBAAuB,QAAQ,CAACjB;IACpC;AACJ"}
|
package/createApiCore.d.ts
CHANGED
|
@@ -4,4 +4,4 @@ export interface ApiCoreConfig {
|
|
|
4
4
|
storageOperations: ApiCoreStorageOperations;
|
|
5
5
|
testProjectLicense?: DecryptedWcpProjectLicense;
|
|
6
6
|
}
|
|
7
|
-
export declare const createApiCore: (config: ApiCoreConfig) => (import("@webiny/api/index.js").ContextPlugin<import("~/types/core.js").ApiCoreContext>[] | import("@webiny/api/index.js").ContextPlugin<import("~/types/core.js").ApiCoreContext> | import("@webiny/handler-graphql/index.js").GraphQLSchemaPlugin<import("@webiny/api/types.js").Context> | import("@webiny/handler/index.js").RegisterExtensionPlugin<import("@webiny/handler/types.js").Context>)[];
|
|
7
|
+
export declare const createApiCore: (config: ApiCoreConfig) => (import("@webiny/api/index.js").ContextPlugin<import("~/types/core.js").ApiCoreContext>[] | import("@webiny/api/index.js").ContextPlugin<import("~/types/core.js").ApiCoreContext> | import("@webiny/handler-graphql/index.js").GraphQLSchemaPlugin<import("@webiny/api/types.js").Context> | import("@webiny/plugins/types.js").PluginCollection | import("@webiny/plugins/types.js").PluginFactory | import("@webiny/handler/index.js").RegisterExtensionPlugin<import("@webiny/handler/types.js").Context> | import("@webiny/plugins/types.js").Plugin<Record<string, any>>)[];
|
package/createApiCore.js
CHANGED
|
@@ -5,6 +5,7 @@ import { createSecurityContext } from "./legacy/security/createSecurityContext.j
|
|
|
5
5
|
import { createAdminUsersContext } from "./legacy/users/createAdminUsersContext.js";
|
|
6
6
|
import { createTenancyContext } from "./legacy/tenancy/createTenancyContext.js";
|
|
7
7
|
import { createSystemGraphQL } from "./graphql/system/createSystemGraphQL.js";
|
|
8
|
+
import { createDebuggerPlugins } from "./features/debugger/index.js";
|
|
8
9
|
const createApiCore = (config)=>{
|
|
9
10
|
const plugin = createRegisterExtensionPlugin((context)=>{
|
|
10
11
|
ApiCoreFeature.register(context.container, config.storageOperations);
|
|
@@ -18,7 +19,8 @@ const createApiCore = (config)=>{
|
|
|
18
19
|
createTenancyContext(),
|
|
19
20
|
createSecurityContext(),
|
|
20
21
|
createAdminUsersContext(),
|
|
21
|
-
createSystemGraphQL()
|
|
22
|
+
createSystemGraphQL(),
|
|
23
|
+
...createDebuggerPlugins()
|
|
22
24
|
];
|
|
23
25
|
};
|
|
24
26
|
export { createApiCore };
|
package/createApiCore.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"createApiCore.js","sources":["../src/createApiCore.ts"],"sourcesContent":["import { createRegisterExtensionPlugin } from \"@webiny/handler/plugins/RegisterExtensionPlugin.js\";\nimport { ApiCoreFeature } from \"./ApiCoreFeature.js\";\nimport type { DecryptedWcpProjectLicense } from \"@webiny/wcp/types.js\";\nimport { createWcpContext } from \"~/legacy/wcp/context.js\";\nimport { createSecurityContext } from \"~/legacy/security/createSecurityContext.js\";\nimport { createAdminUsersContext } from \"~/legacy/users/createAdminUsersContext.js\";\nimport { createTenancyContext } from \"~/legacy/tenancy/createTenancyContext.js\";\nimport { createSystemGraphQL } from \"~/graphql/system/createSystemGraphQL.js\";\nimport type { ApiCoreStorageOperations } from \"~/types/core.js\";\n\nexport interface ApiCoreConfig {\n storageOperations: ApiCoreStorageOperations;\n testProjectLicense?: DecryptedWcpProjectLicense;\n}\n\nexport const createApiCore = (config: ApiCoreConfig) => {\n const plugin = createRegisterExtensionPlugin(context => {\n ApiCoreFeature.register(context.container, config.storageOperations);\n });\n plugin.name = \"apiCore.extension\";\n\n return [\n plugin,\n createWcpContext({ testProjectLicense: config.testProjectLicense }),\n createTenancyContext(),\n createSecurityContext(),\n createAdminUsersContext(),\n createSystemGraphQL()\n ];\n};\n"],"names":["createApiCore","config","plugin","createRegisterExtensionPlugin","context","ApiCoreFeature","createWcpContext","createTenancyContext","createSecurityContext","createAdminUsersContext","createSystemGraphQL"],"mappings":"
|
|
1
|
+
{"version":3,"file":"createApiCore.js","sources":["../src/createApiCore.ts"],"sourcesContent":["import { createRegisterExtensionPlugin } from \"@webiny/handler/plugins/RegisterExtensionPlugin.js\";\nimport { ApiCoreFeature } from \"./ApiCoreFeature.js\";\nimport type { DecryptedWcpProjectLicense } from \"@webiny/wcp/types.js\";\nimport { createWcpContext } from \"~/legacy/wcp/context.js\";\nimport { createSecurityContext } from \"~/legacy/security/createSecurityContext.js\";\nimport { createAdminUsersContext } from \"~/legacy/users/createAdminUsersContext.js\";\nimport { createTenancyContext } from \"~/legacy/tenancy/createTenancyContext.js\";\nimport { createSystemGraphQL } from \"~/graphql/system/createSystemGraphQL.js\";\nimport type { ApiCoreStorageOperations } from \"~/types/core.js\";\nimport { createDebuggerPlugins } from \"~/features/debugger/index.js\";\n\nexport interface ApiCoreConfig {\n storageOperations: ApiCoreStorageOperations;\n testProjectLicense?: DecryptedWcpProjectLicense;\n}\n\nexport const createApiCore = (config: ApiCoreConfig) => {\n const plugin = createRegisterExtensionPlugin(context => {\n ApiCoreFeature.register(context.container, config.storageOperations);\n });\n plugin.name = \"apiCore.extension\";\n\n return [\n plugin,\n createWcpContext({ testProjectLicense: config.testProjectLicense }),\n createTenancyContext(),\n createSecurityContext(),\n createAdminUsersContext(),\n createSystemGraphQL(),\n ...createDebuggerPlugins()\n ];\n};\n"],"names":["createApiCore","config","plugin","createRegisterExtensionPlugin","context","ApiCoreFeature","createWcpContext","createTenancyContext","createSecurityContext","createAdminUsersContext","createSystemGraphQL","createDebuggerPlugins"],"mappings":";;;;;;;;AAgBO,MAAMA,gBAAgB,CAACC;IAC1B,MAAMC,SAASC,8BAA8BC,CAAAA;QACzCC,eAAe,QAAQ,CAACD,QAAQ,SAAS,EAAEH,OAAO,iBAAiB;IACvE;IACAC,OAAO,IAAI,GAAG;IAEd,OAAO;QACHA;QACAI,iBAAiB;YAAE,oBAAoBL,OAAO,kBAAkB;QAAC;QACjEM;QACAC;QACAC;QACAC;WACGC;KACN;AACL"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { Debugger as DebuggerAbstraction, DebuggerTransport, type DebugNamespace, type IDebugDeliveryTarget, type IDebuggerStartParams, type IDebugPayloadFactory } from "./abstractions.js";
|
|
2
|
+
import { IdentityContext } from "../../features/security/IdentityContext/index.js";
|
|
3
|
+
import { BuildParams } from "../../features/buildParams/index.js";
|
|
4
|
+
export declare class DebuggerImpl implements DebuggerAbstraction.Interface {
|
|
5
|
+
private readonly identityContext;
|
|
6
|
+
private readonly buildParams;
|
|
7
|
+
private readonly transports;
|
|
8
|
+
private session;
|
|
9
|
+
private readonly limits;
|
|
10
|
+
constructor(identityContext: IdentityContext.Interface, buildParams: BuildParams.Interface, transports: DebuggerTransport.Interface[]);
|
|
11
|
+
start(params: IDebuggerStartParams): void;
|
|
12
|
+
isEnabled(namespace: DebugNamespace): boolean;
|
|
13
|
+
log(namespace: DebugNamespace, payload: IDebugPayloadFactory): void;
|
|
14
|
+
flush(target: IDebugDeliveryTarget): Promise<void>;
|
|
15
|
+
discard(): void;
|
|
16
|
+
private isDisabled;
|
|
17
|
+
/**
|
|
18
|
+
* Drops entries from the front until the session fits the byte budget. The client reproduces the
|
|
19
|
+
* bug, so the failure is at the end of the session and the tail is what matters.
|
|
20
|
+
*/
|
|
21
|
+
private enforceBudget;
|
|
22
|
+
}
|
|
23
|
+
export declare const Debugger: import("@webiny/di").Implementation<typeof DebuggerImpl>;
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { createImplementation } from "@webiny/feature/api";
|
|
2
|
+
import { Debugger, DebuggerTransport } from "./abstractions.js";
|
|
3
|
+
import { IdentityContext } from "../security/IdentityContext/index.js";
|
|
4
|
+
import { BuildParams } from "../buildParams/index.js";
|
|
5
|
+
import { createNamespaceFilter } from "./matchNamespace.js";
|
|
6
|
+
import { DEFAULT_LIMITS } from "./limits.js";
|
|
7
|
+
import { serialize } from "./serialize.js";
|
|
8
|
+
const FLUSH_TIMEOUT_MS = 2000;
|
|
9
|
+
const describeError = (error)=>{
|
|
10
|
+
if (error instanceof Error) return `${error.name}: ${error.message}`;
|
|
11
|
+
return "Unknown error.";
|
|
12
|
+
};
|
|
13
|
+
class DebuggerImpl {
|
|
14
|
+
constructor(identityContext, buildParams, transports){
|
|
15
|
+
this.identityContext = identityContext;
|
|
16
|
+
this.buildParams = buildParams;
|
|
17
|
+
this.transports = transports;
|
|
18
|
+
this.session = void 0;
|
|
19
|
+
this.limits = DEFAULT_LIMITS;
|
|
20
|
+
}
|
|
21
|
+
start(params) {
|
|
22
|
+
if (this.isDisabled()) return;
|
|
23
|
+
if (0 === params.namespaces.length) return;
|
|
24
|
+
this.session = {
|
|
25
|
+
namespaces: params.namespaces,
|
|
26
|
+
filter: createNamespaceFilter(params.namespaces),
|
|
27
|
+
startedAt: Date.now(),
|
|
28
|
+
entries: [],
|
|
29
|
+
bytes: 0,
|
|
30
|
+
dropped: 0,
|
|
31
|
+
truncated: false,
|
|
32
|
+
seq: 0
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
isEnabled(namespace) {
|
|
36
|
+
const session = this.session;
|
|
37
|
+
if (!session) return false;
|
|
38
|
+
if (this.identityContext.getIdentity().isAnonymous()) return false;
|
|
39
|
+
return session.filter.matches(namespace);
|
|
40
|
+
}
|
|
41
|
+
log(namespace, payload) {
|
|
42
|
+
try {
|
|
43
|
+
const session = this.session;
|
|
44
|
+
if (!session || !this.isEnabled(namespace)) return;
|
|
45
|
+
if (session.entries.length >= this.limits.entryCount) return;
|
|
46
|
+
const { json, bytes, truncated } = serialize(payload(), this.limits);
|
|
47
|
+
const entry = {
|
|
48
|
+
seq: ++session.seq,
|
|
49
|
+
namespace,
|
|
50
|
+
timestamp: Date.now(),
|
|
51
|
+
elapsed: Date.now() - session.startedAt,
|
|
52
|
+
json,
|
|
53
|
+
bytes
|
|
54
|
+
};
|
|
55
|
+
session.entries.push(entry);
|
|
56
|
+
session.bytes += bytes;
|
|
57
|
+
session.truncated = session.truncated || truncated;
|
|
58
|
+
this.enforceBudget(session);
|
|
59
|
+
} catch (ex) {
|
|
60
|
+
console.error(`Debugger failed to capture "${namespace}".`, describeError(ex));
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
async flush(target) {
|
|
64
|
+
const session = this.session;
|
|
65
|
+
this.session = void 0;
|
|
66
|
+
if (!session) return;
|
|
67
|
+
const transports = this.transports.filter((transport)=>{
|
|
68
|
+
try {
|
|
69
|
+
return transport.canDeliver(target);
|
|
70
|
+
} catch {
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
if (0 === transports.length) return;
|
|
75
|
+
const frozen = Object.freeze({
|
|
76
|
+
namespaces: session.namespaces,
|
|
77
|
+
startedAt: session.startedAt,
|
|
78
|
+
entries: session.entries,
|
|
79
|
+
bytes: session.bytes,
|
|
80
|
+
dropped: session.dropped,
|
|
81
|
+
truncated: session.truncated
|
|
82
|
+
});
|
|
83
|
+
const delivery = Promise.allSettled(transports.map(async (transport)=>await transport.deliver({
|
|
84
|
+
target,
|
|
85
|
+
session: frozen
|
|
86
|
+
})));
|
|
87
|
+
let timer;
|
|
88
|
+
const timeout = new Promise((resolve)=>{
|
|
89
|
+
timer = setTimeout(resolve, FLUSH_TIMEOUT_MS);
|
|
90
|
+
});
|
|
91
|
+
try {
|
|
92
|
+
const results = await Promise.race([
|
|
93
|
+
delivery,
|
|
94
|
+
timeout
|
|
95
|
+
]);
|
|
96
|
+
if (Array.isArray(results)) {
|
|
97
|
+
for (const result of results)if ("rejected" === result.status) console.error("Debugger transport failed to deliver.", describeError(result.reason));
|
|
98
|
+
}
|
|
99
|
+
} finally{
|
|
100
|
+
if (timer) clearTimeout(timer);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
discard() {
|
|
104
|
+
this.session = void 0;
|
|
105
|
+
}
|
|
106
|
+
isDisabled() {
|
|
107
|
+
return true === this.buildParams.get("DebuggerDisabled");
|
|
108
|
+
}
|
|
109
|
+
enforceBudget(session) {
|
|
110
|
+
while(session.bytes > this.limits.totalBytes && session.entries.length > 0){
|
|
111
|
+
const removed = session.entries.shift();
|
|
112
|
+
if (!removed) break;
|
|
113
|
+
session.bytes -= removed.bytes;
|
|
114
|
+
session.dropped++;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
const Debugger_Debugger = createImplementation({
|
|
119
|
+
abstraction: Debugger,
|
|
120
|
+
implementation: DebuggerImpl,
|
|
121
|
+
dependencies: [
|
|
122
|
+
IdentityContext,
|
|
123
|
+
BuildParams,
|
|
124
|
+
[
|
|
125
|
+
DebuggerTransport,
|
|
126
|
+
{
|
|
127
|
+
multiple: true
|
|
128
|
+
}
|
|
129
|
+
]
|
|
130
|
+
]
|
|
131
|
+
});
|
|
132
|
+
export { DebuggerImpl, Debugger_Debugger as Debugger };
|
|
133
|
+
|
|
134
|
+
//# sourceMappingURL=Debugger.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"features/debugger/Debugger.js","sources":["../../../src/features/debugger/Debugger.ts"],"sourcesContent":["import { createImplementation } from \"@webiny/feature/api\";\nimport {\n Debugger as DebuggerAbstraction,\n DebuggerTransport,\n type DebugNamespace,\n type IDebugDeliveryTarget,\n type IDebugEntry,\n type IDebuggerStartParams,\n type IDebugPayloadFactory,\n type IDebugSession\n} from \"./abstractions.js\";\nimport { IdentityContext } from \"~/features/security/IdentityContext/index.js\";\nimport { BuildParams } from \"~/features/buildParams/index.js\";\nimport { createNamespaceFilter, type INamespaceFilter } from \"./matchNamespace.js\";\nimport { DEFAULT_LIMITS, type IDebuggerLimits } from \"./limits.js\";\nimport { serialize } from \"./serialize.js\";\n\nconst FLUSH_TIMEOUT_MS = 2000;\n\n/**\n * Renders an error for logging without touching anything that could hold a captured payload.\n *\n * Deliberately not `stringifyError` from `@webiny/handler`: that spreads the whole error object, so\n * an error carrying a `data` property would put debug payloads into CloudWatch - the one sink they\n * must never reach.\n */\nconst describeError = (error: unknown): string => {\n if (error instanceof Error) {\n return `${error.name}: ${error.message}`;\n }\n return \"Unknown error.\";\n};\n\ninterface IActiveSession extends IDebugSession {\n filter: INamespaceFilter;\n seq: number;\n}\n\nexport class DebuggerImpl implements DebuggerAbstraction.Interface {\n private session: IActiveSession | undefined = undefined;\n private readonly limits: IDebuggerLimits = DEFAULT_LIMITS;\n\n public constructor(\n private readonly identityContext: IdentityContext.Interface,\n private readonly buildParams: BuildParams.Interface,\n private readonly transports: DebuggerTransport.Interface[]\n ) {}\n\n public start(params: IDebuggerStartParams): void {\n if (this.isDisabled()) {\n return;\n }\n\n if (params.namespaces.length === 0) {\n return;\n }\n\n /**\n * Always replace. A session surviving into the next request would mean one identity\n * receiving another's entries.\n */\n this.session = {\n namespaces: params.namespaces,\n filter: createNamespaceFilter(params.namespaces),\n startedAt: Date.now(),\n entries: [],\n bytes: 0,\n dropped: 0,\n truncated: false,\n seq: 0\n };\n }\n\n public isEnabled(namespace: DebugNamespace): boolean {\n const session = this.session;\n if (!session) {\n return false;\n }\n\n /**\n * An anonymous identity must never accumulate payloads. The flush-time permission check is\n * authoritative, but this earlier gate means there is nothing to leak if a future code path\n * delivers a session without passing through it.\n */\n if (this.identityContext.getIdentity().isAnonymous()) {\n return false;\n }\n\n return session.filter.matches(namespace);\n }\n\n public log(namespace: DebugNamespace, payload: IDebugPayloadFactory): void {\n /**\n * The debugger must never be able to break the request it is observing, and payload\n * factories are written in a hurry during an incident.\n */\n try {\n const session = this.session;\n if (!session || !this.isEnabled(namespace)) {\n return;\n }\n\n if (session.entries.length >= this.limits.entryCount) {\n return;\n }\n\n /**\n * Invoke and serialize immediately: this releases the closure and the payload graph, and\n * it is what makes the entry's size known, which is what makes the budget enforceable.\n */\n const { json, bytes, truncated } = serialize(payload(), this.limits);\n\n const entry: IDebugEntry = {\n seq: ++session.seq,\n namespace,\n timestamp: Date.now(),\n elapsed: Date.now() - session.startedAt,\n json,\n bytes\n };\n\n session.entries.push(entry);\n session.bytes += bytes;\n session.truncated = session.truncated || truncated;\n\n this.enforceBudget(session);\n } catch (ex) {\n console.error(`Debugger failed to capture \"${namespace}\".`, describeError(ex));\n }\n }\n\n public async flush(target: IDebugDeliveryTarget): Promise<void> {\n const session = this.session;\n this.session = undefined;\n\n if (!session) {\n return;\n }\n\n const transports = this.transports.filter(transport => {\n try {\n return transport.canDeliver(target);\n } catch {\n return false;\n }\n });\n\n if (transports.length === 0) {\n return;\n }\n\n const frozen = Object.freeze({\n namespaces: session.namespaces,\n startedAt: session.startedAt,\n entries: session.entries,\n bytes: session.bytes,\n dropped: session.dropped,\n truncated: session.truncated\n }) as IDebugSession;\n\n const delivery = Promise.allSettled(\n transports.map(async transport => {\n return await transport.deliver({ target, session: frozen });\n })\n );\n\n /**\n * In-memory delivery is instant, but a future network sink is not, and debug data is never\n * worth making the app feel broken. The timer is cleared so it cannot hold the event loop\n * open between the response and the Lambda freeze.\n */\n let timer: NodeJS.Timeout | undefined;\n const timeout = new Promise<void>(resolve => {\n timer = setTimeout(resolve, FLUSH_TIMEOUT_MS);\n });\n\n try {\n const results = await Promise.race([delivery, timeout]);\n if (Array.isArray(results)) {\n for (const result of results) {\n if (result.status === \"rejected\") {\n /**\n * Only the error - never the session or any entry. `console.error` goes to\n * CloudWatch, which is the one place debug payloads must never reach.\n */\n console.error(\n \"Debugger transport failed to deliver.\",\n describeError(result.reason)\n );\n }\n }\n }\n } finally {\n if (timer) {\n clearTimeout(timer);\n }\n }\n }\n\n public discard(): void {\n this.session = undefined;\n }\n\n private isDisabled(): boolean {\n return this.buildParams.get<boolean>(\"DebuggerDisabled\") === true;\n }\n\n /**\n * Drops entries from the front until the session fits the byte budget. The client reproduces the\n * bug, so the failure is at the end of the session and the tail is what matters.\n */\n private enforceBudget(session: IActiveSession): void {\n while (session.bytes > this.limits.totalBytes && session.entries.length > 0) {\n const removed = session.entries.shift();\n if (!removed) {\n break;\n }\n session.bytes -= removed.bytes;\n session.dropped++;\n }\n }\n}\n\nexport const Debugger = createImplementation({\n abstraction: DebuggerAbstraction,\n implementation: DebuggerImpl,\n dependencies: [IdentityContext, BuildParams, [DebuggerTransport, { multiple: true }]]\n});\n"],"names":["FLUSH_TIMEOUT_MS","describeError","error","Error","DebuggerImpl","identityContext","buildParams","transports","undefined","DEFAULT_LIMITS","params","createNamespaceFilter","Date","namespace","session","payload","json","bytes","truncated","serialize","entry","ex","console","target","transport","frozen","Object","delivery","Promise","timer","timeout","resolve","setTimeout","results","Array","result","clearTimeout","removed","Debugger","createImplementation","DebuggerAbstraction","IdentityContext","BuildParams","DebuggerTransport"],"mappings":"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"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { DebuggerTransport as DebuggerTransportAbstraction, type IDebugDeliveryTarget, type IDebuggerTransportDeliverParams } from "./abstractions.js";
|
|
2
|
+
export declare const GRAPHQL_TARGET = "graphql";
|
|
3
|
+
export interface IGraphQLDeliveryTarget extends IDebugDeliveryTarget {
|
|
4
|
+
type: typeof GRAPHQL_TARGET;
|
|
5
|
+
/**
|
|
6
|
+
* Already resolved to a single result object - never the array. Batched requests are reduced to
|
|
7
|
+
* their last element by the flush plugin, so this transport never has to know about batching.
|
|
8
|
+
*/
|
|
9
|
+
result: Record<string, any>;
|
|
10
|
+
requestId: string;
|
|
11
|
+
operations: string[];
|
|
12
|
+
url: string;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Writes a finished session into the GraphQL response's `extensions` property.
|
|
16
|
+
*/
|
|
17
|
+
export declare class GraphQLDebuggerTransportImpl implements DebuggerTransportAbstraction.Interface {
|
|
18
|
+
canDeliver(target: IDebugDeliveryTarget): boolean;
|
|
19
|
+
deliver({ target, session }: IDebuggerTransportDeliverParams): void;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Merges a value under `extensions.debug`, never assigning `extensions` wholesale - other plugins
|
|
23
|
+
* write to it too.
|
|
24
|
+
*/
|
|
25
|
+
export declare const writeExtension: (result: Record<string, any>, debug: Record<string, any>) => void;
|
|
26
|
+
export declare const GraphQLDebuggerTransport: import("@webiny/di").Implementation<typeof GraphQLDebuggerTransportImpl>;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { createImplementation } from "@webiny/feature/api";
|
|
2
|
+
import { DebuggerTransport } from "./abstractions.js";
|
|
3
|
+
const GRAPHQL_TARGET = "graphql";
|
|
4
|
+
const isGraphQLTarget = (target)=>target.type === GRAPHQL_TARGET;
|
|
5
|
+
class GraphQLDebuggerTransportImpl {
|
|
6
|
+
canDeliver(target) {
|
|
7
|
+
return isGraphQLTarget(target);
|
|
8
|
+
}
|
|
9
|
+
deliver({ target, session }) {
|
|
10
|
+
if (!isGraphQLTarget(target)) return;
|
|
11
|
+
if (0 === session.entries.length) return;
|
|
12
|
+
const entries = session.entries.map((entry)=>({
|
|
13
|
+
seq: entry.seq,
|
|
14
|
+
namespace: entry.namespace,
|
|
15
|
+
timestamp: entry.timestamp,
|
|
16
|
+
elapsed: entry.elapsed,
|
|
17
|
+
data: JSON.parse(entry.json)
|
|
18
|
+
}));
|
|
19
|
+
writeExtension(target.result, {
|
|
20
|
+
enabled: true,
|
|
21
|
+
requestId: target.requestId,
|
|
22
|
+
url: target.url,
|
|
23
|
+
namespaces: session.namespaces,
|
|
24
|
+
operations: target.operations,
|
|
25
|
+
entries,
|
|
26
|
+
bytes: session.bytes,
|
|
27
|
+
dropped: session.dropped,
|
|
28
|
+
truncated: session.truncated
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
const writeExtension = (result, debug)=>{
|
|
33
|
+
result.extensions = {
|
|
34
|
+
...result.extensions,
|
|
35
|
+
debug
|
|
36
|
+
};
|
|
37
|
+
};
|
|
38
|
+
const GraphQLDebuggerTransport = createImplementation({
|
|
39
|
+
abstraction: DebuggerTransport,
|
|
40
|
+
implementation: GraphQLDebuggerTransportImpl,
|
|
41
|
+
dependencies: []
|
|
42
|
+
});
|
|
43
|
+
export { GRAPHQL_TARGET, GraphQLDebuggerTransport, GraphQLDebuggerTransportImpl, writeExtension };
|
|
44
|
+
|
|
45
|
+
//# sourceMappingURL=GraphQLDebuggerTransport.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"features/debugger/GraphQLDebuggerTransport.js","sources":["../../../src/features/debugger/GraphQLDebuggerTransport.ts"],"sourcesContent":["import { createImplementation } from \"@webiny/feature/api\";\nimport {\n DebuggerTransport as DebuggerTransportAbstraction,\n type IDebugDeliveryTarget,\n type IDebuggerTransportDeliverParams\n} from \"./abstractions.js\";\n\nexport const GRAPHQL_TARGET = \"graphql\";\n\nexport interface IGraphQLDeliveryTarget extends IDebugDeliveryTarget {\n type: typeof GRAPHQL_TARGET;\n /**\n * Already resolved to a single result object - never the array. Batched requests are reduced to\n * their last element by the flush plugin, so this transport never has to know about batching.\n */\n result: Record<string, any>;\n requestId: string;\n operations: string[];\n url: string;\n}\n\nconst isGraphQLTarget = (target: IDebugDeliveryTarget): target is IGraphQLDeliveryTarget => {\n return target.type === GRAPHQL_TARGET;\n};\n\n/**\n * Writes a finished session into the GraphQL response's `extensions` property.\n */\nexport class GraphQLDebuggerTransportImpl implements DebuggerTransportAbstraction.Interface {\n public canDeliver(target: IDebugDeliveryTarget): boolean {\n return isGraphQLTarget(target);\n }\n\n public deliver({ target, session }: IDebuggerTransportDeliverParams): void {\n if (!isGraphQLTarget(target)) {\n return;\n }\n\n /**\n * Nothing matched the filter on this request. Writing an envelope with an empty `entries`\n * array would put a payload on every response for no benefit, and give whoever reads the\n * report a pile of blanks to scroll past.\n */\n if (session.entries.length === 0) {\n return;\n }\n\n const entries = session.entries.map(entry => {\n return {\n seq: entry.seq,\n namespace: entry.namespace,\n timestamp: entry.timestamp,\n elapsed: entry.elapsed,\n /**\n * Entries are buffered as strings so the payload graph can be released at capture\n * time. Parsing here is what puts structured data in the response rather than JSON\n * inside JSON.\n */\n data: JSON.parse(entry.json)\n };\n });\n\n writeExtension(target.result, {\n enabled: true,\n requestId: target.requestId,\n url: target.url,\n namespaces: session.namespaces,\n operations: target.operations,\n entries,\n bytes: session.bytes,\n dropped: session.dropped,\n truncated: session.truncated\n });\n }\n}\n\n/**\n * Merges a value under `extensions.debug`, never assigning `extensions` wholesale - other plugins\n * write to it too.\n */\nexport const writeExtension = (result: Record<string, any>, debug: Record<string, any>): void => {\n result.extensions = { ...result.extensions, debug };\n};\n\nexport const GraphQLDebuggerTransport = createImplementation({\n abstraction: DebuggerTransportAbstraction,\n implementation: GraphQLDebuggerTransportImpl,\n dependencies: []\n});\n"],"names":["GRAPHQL_TARGET","isGraphQLTarget","target","GraphQLDebuggerTransportImpl","session","entries","entry","JSON","writeExtension","result","debug","GraphQLDebuggerTransport","createImplementation","DebuggerTransportAbstraction"],"mappings":";;AAOO,MAAMA,iBAAiB;AAc9B,MAAMC,kBAAkB,CAACC,SACdA,OAAO,IAAI,KAAKF;AAMpB,MAAMG;IACF,WAAWD,MAA4B,EAAW;QACrD,OAAOD,gBAAgBC;IAC3B;IAEO,QAAQ,EAAEA,MAAM,EAAEE,OAAO,EAAmC,EAAQ;QACvE,IAAI,CAACH,gBAAgBC,SACjB;QAQJ,IAAIE,AAA2B,MAA3BA,QAAQ,OAAO,CAAC,MAAM,EACtB;QAGJ,MAAMC,UAAUD,QAAQ,OAAO,CAAC,GAAG,CAACE,CAAAA,QACzB;gBACH,KAAKA,MAAM,GAAG;gBACd,WAAWA,MAAM,SAAS;gBAC1B,WAAWA,MAAM,SAAS;gBAC1B,SAASA,MAAM,OAAO;gBAMtB,MAAMC,KAAK,KAAK,CAACD,MAAM,IAAI;YAC/B;QAGJE,eAAeN,OAAO,MAAM,EAAE;YAC1B,SAAS;YACT,WAAWA,OAAO,SAAS;YAC3B,KAAKA,OAAO,GAAG;YACf,YAAYE,QAAQ,UAAU;YAC9B,YAAYF,OAAO,UAAU;YAC7BG;YACA,OAAOD,QAAQ,KAAK;YACpB,SAASA,QAAQ,OAAO;YACxB,WAAWA,QAAQ,SAAS;QAChC;IACJ;AACJ;AAMO,MAAMI,iBAAiB,CAACC,QAA6BC;IACxDD,OAAO,UAAU,GAAG;QAAE,GAAGA,OAAO,UAAU;QAAEC;IAAM;AACtD;AAEO,MAAMC,2BAA2BC,qBAAqB;IACzD,aAAaC;IACb,gBAAgBV;IAChB,cAAc,EAAE;AACpB"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { createNamespaceFilter, parseNamespaceHeader } from "../matchNamespace.js";
|
|
3
|
+
describe("parseNamespaceHeader", ()=>{
|
|
4
|
+
it("should not start a session when the header is absent or empty", ()=>{
|
|
5
|
+
expect(parseNamespaceHeader(void 0)).toEqual([]);
|
|
6
|
+
expect(parseNamespaceHeader("")).toEqual([]);
|
|
7
|
+
expect(parseNamespaceHeader(" ")).toEqual([]);
|
|
8
|
+
});
|
|
9
|
+
it("should treat the truthy shorthands as capture-all", ()=>{
|
|
10
|
+
expect(parseNamespaceHeader("1")).toEqual([
|
|
11
|
+
"*"
|
|
12
|
+
]);
|
|
13
|
+
expect(parseNamespaceHeader("true")).toEqual([
|
|
14
|
+
"*"
|
|
15
|
+
]);
|
|
16
|
+
expect(parseNamespaceHeader("*")).toEqual([
|
|
17
|
+
"*"
|
|
18
|
+
]);
|
|
19
|
+
});
|
|
20
|
+
it("should trim tokens, since duplicate headers arrive comma-joined with spaces", ()=>{
|
|
21
|
+
expect(parseNamespaceHeader("cms.os.*, fm.*")).toEqual([
|
|
22
|
+
"cms.os.*",
|
|
23
|
+
"fm.*"
|
|
24
|
+
]);
|
|
25
|
+
});
|
|
26
|
+
it("should accept the array form produced by app.inject", ()=>{
|
|
27
|
+
expect(parseNamespaceHeader([
|
|
28
|
+
"cms.os.*",
|
|
29
|
+
"fm.*"
|
|
30
|
+
])).toEqual([
|
|
31
|
+
"cms.os.*",
|
|
32
|
+
"fm.*"
|
|
33
|
+
]);
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
describe("createNamespaceFilter", ()=>{
|
|
37
|
+
it("should match everything for the wildcard", ()=>{
|
|
38
|
+
const filter = createNamespaceFilter([
|
|
39
|
+
"*"
|
|
40
|
+
]);
|
|
41
|
+
expect(filter.matches("cms.os.list")).toBe(true);
|
|
42
|
+
expect(filter.matches("core.anything")).toBe(true);
|
|
43
|
+
});
|
|
44
|
+
it("should match a prefix glob across dots", ()=>{
|
|
45
|
+
const filter = createNamespaceFilter([
|
|
46
|
+
"cms.*"
|
|
47
|
+
]);
|
|
48
|
+
expect(filter.matches("cms.os.list")).toBe(true);
|
|
49
|
+
expect(filter.matches("fm.file.get")).toBe(false);
|
|
50
|
+
});
|
|
51
|
+
it("should exclude rather than invert on negation", ()=>{
|
|
52
|
+
const filter = createNamespaceFilter([
|
|
53
|
+
"*",
|
|
54
|
+
"!cms.os.*"
|
|
55
|
+
]);
|
|
56
|
+
expect(filter.matches("cms.os.list")).toBe(false);
|
|
57
|
+
expect(filter.matches("fm.file.get")).toBe(true);
|
|
58
|
+
});
|
|
59
|
+
it("should require at least one positive match", ()=>{
|
|
60
|
+
const filter = createNamespaceFilter([
|
|
61
|
+
"!cms.os.*"
|
|
62
|
+
]);
|
|
63
|
+
expect(filter.matches("fm.file.get")).toBe(false);
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
//# sourceMappingURL=matchNamespace.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"features/debugger/__tests__/matchNamespace.test.js","sources":["../../../../src/features/debugger/__tests__/matchNamespace.test.ts"],"sourcesContent":["import { describe, expect, it } from \"vitest\";\nimport { createNamespaceFilter, parseNamespaceHeader } from \"../matchNamespace.js\";\n\ndescribe(\"parseNamespaceHeader\", () => {\n it(\"should not start a session when the header is absent or empty\", () => {\n expect(parseNamespaceHeader(undefined)).toEqual([]);\n expect(parseNamespaceHeader(\"\")).toEqual([]);\n expect(parseNamespaceHeader(\" \")).toEqual([]);\n });\n\n it(\"should treat the truthy shorthands as capture-all\", () => {\n expect(parseNamespaceHeader(\"1\")).toEqual([\"*\"]);\n expect(parseNamespaceHeader(\"true\")).toEqual([\"*\"]);\n expect(parseNamespaceHeader(\"*\")).toEqual([\"*\"]);\n });\n\n it(\"should trim tokens, since duplicate headers arrive comma-joined with spaces\", () => {\n expect(parseNamespaceHeader(\"cms.os.*, fm.*\")).toEqual([\"cms.os.*\", \"fm.*\"]);\n });\n\n it(\"should accept the array form produced by app.inject\", () => {\n expect(parseNamespaceHeader([\"cms.os.*\", \"fm.*\"])).toEqual([\"cms.os.*\", \"fm.*\"]);\n });\n});\n\ndescribe(\"createNamespaceFilter\", () => {\n it(\"should match everything for the wildcard\", () => {\n const filter = createNamespaceFilter([\"*\"]);\n\n expect(filter.matches(\"cms.os.list\")).toBe(true);\n expect(filter.matches(\"core.anything\")).toBe(true);\n });\n\n it(\"should match a prefix glob across dots\", () => {\n const filter = createNamespaceFilter([\"cms.*\"]);\n\n expect(filter.matches(\"cms.os.list\")).toBe(true);\n expect(filter.matches(\"fm.file.get\")).toBe(false);\n });\n\n it(\"should exclude rather than invert on negation\", () => {\n const filter = createNamespaceFilter([\"*\", \"!cms.os.*\"]);\n\n expect(filter.matches(\"cms.os.list\")).toBe(false);\n expect(filter.matches(\"fm.file.get\")).toBe(true);\n });\n\n it(\"should require at least one positive match\", () => {\n const filter = createNamespaceFilter([\"!cms.os.*\"]);\n\n expect(filter.matches(\"fm.file.get\")).toBe(false);\n });\n});\n"],"names":["describe","it","expect","parseNamespaceHeader","undefined","filter","createNamespaceFilter"],"mappings":";;AAGAA,SAAS,wBAAwB;IAC7BC,GAAG,iEAAiE;QAChEC,OAAOC,qBAAqBC,SAAY,OAAO,CAAC,EAAE;QAClDF,OAAOC,qBAAqB,KAAK,OAAO,CAAC,EAAE;QAC3CD,OAAOC,qBAAqB,QAAQ,OAAO,CAAC,EAAE;IAClD;IAEAF,GAAG,qDAAqD;QACpDC,OAAOC,qBAAqB,MAAM,OAAO,CAAC;YAAC;SAAI;QAC/CD,OAAOC,qBAAqB,SAAS,OAAO,CAAC;YAAC;SAAI;QAClDD,OAAOC,qBAAqB,MAAM,OAAO,CAAC;YAAC;SAAI;IACnD;IAEAF,GAAG,+EAA+E;QAC9EC,OAAOC,qBAAqB,mBAAmB,OAAO,CAAC;YAAC;YAAY;SAAO;IAC/E;IAEAF,GAAG,uDAAuD;QACtDC,OAAOC,qBAAqB;YAAC;YAAY;SAAO,GAAG,OAAO,CAAC;YAAC;YAAY;SAAO;IACnF;AACJ;AAEAH,SAAS,yBAAyB;IAC9BC,GAAG,4CAA4C;QAC3C,MAAMI,SAASC,sBAAsB;YAAC;SAAI;QAE1CJ,OAAOG,OAAO,OAAO,CAAC,gBAAgB,IAAI,CAAC;QAC3CH,OAAOG,OAAO,OAAO,CAAC,kBAAkB,IAAI,CAAC;IACjD;IAEAJ,GAAG,0CAA0C;QACzC,MAAMI,SAASC,sBAAsB;YAAC;SAAQ;QAE9CJ,OAAOG,OAAO,OAAO,CAAC,gBAAgB,IAAI,CAAC;QAC3CH,OAAOG,OAAO,OAAO,CAAC,gBAAgB,IAAI,CAAC;IAC/C;IAEAJ,GAAG,iDAAiD;QAChD,MAAMI,SAASC,sBAAsB;YAAC;YAAK;SAAY;QAEvDJ,OAAOG,OAAO,OAAO,CAAC,gBAAgB,IAAI,CAAC;QAC3CH,OAAOG,OAAO,OAAO,CAAC,gBAAgB,IAAI,CAAC;IAC/C;IAEAJ,GAAG,8CAA8C;QAC7C,MAAMI,SAASC,sBAAsB;YAAC;SAAY;QAElDJ,OAAOG,OAAO,OAAO,CAAC,gBAAgB,IAAI,CAAC;IAC/C;AACJ"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { serialize } from "../serialize.js";
|
|
3
|
+
import { DEFAULT_LIMITS } from "../limits.js";
|
|
4
|
+
const limits = (overrides = {})=>({
|
|
5
|
+
...DEFAULT_LIMITS,
|
|
6
|
+
...overrides
|
|
7
|
+
});
|
|
8
|
+
const parse = (value, overrides = {})=>{
|
|
9
|
+
const result = serialize(value, limits(overrides));
|
|
10
|
+
return {
|
|
11
|
+
...result,
|
|
12
|
+
parsed: JSON.parse(result.json)
|
|
13
|
+
};
|
|
14
|
+
};
|
|
15
|
+
describe("serialize", ()=>{
|
|
16
|
+
it("should always produce parseable JSON", ()=>{
|
|
17
|
+
const circular = {
|
|
18
|
+
name: "root"
|
|
19
|
+
};
|
|
20
|
+
circular.self = circular;
|
|
21
|
+
const { parsed } = parse(circular);
|
|
22
|
+
expect(parsed).toEqual({
|
|
23
|
+
name: "root",
|
|
24
|
+
self: "#[Circular]"
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
it("should not report siblings as circular", ()=>{
|
|
28
|
+
const shared = {
|
|
29
|
+
id: 1
|
|
30
|
+
};
|
|
31
|
+
const { parsed } = parse({
|
|
32
|
+
a: shared,
|
|
33
|
+
b: shared
|
|
34
|
+
});
|
|
35
|
+
expect(parsed).toEqual({
|
|
36
|
+
a: {
|
|
37
|
+
id: 1
|
|
38
|
+
},
|
|
39
|
+
b: {
|
|
40
|
+
id: 1
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
it("should serialize an Error, which JSON.stringify renders as an empty object", ()=>{
|
|
45
|
+
const error = new Error("Boom");
|
|
46
|
+
const { parsed } = parse({
|
|
47
|
+
error
|
|
48
|
+
});
|
|
49
|
+
expect(parsed.error.name).toBe("Error");
|
|
50
|
+
expect(parsed.error.message).toBe("Boom");
|
|
51
|
+
expect(typeof parsed.error.stack).toBe("string");
|
|
52
|
+
});
|
|
53
|
+
it("should serialize a BigInt, which JSON.stringify throws on", ()=>{
|
|
54
|
+
const { parsed } = parse({
|
|
55
|
+
value: BigInt(123)
|
|
56
|
+
});
|
|
57
|
+
expect(parsed).toEqual({
|
|
58
|
+
value: "123n"
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
it("should describe binary data without including its contents", ()=>{
|
|
62
|
+
const { parsed } = parse({
|
|
63
|
+
buffer: Buffer.from("hello world")
|
|
64
|
+
});
|
|
65
|
+
expect(parsed).toEqual({
|
|
66
|
+
buffer: "#[Buffer 11 bytes]"
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
it("should serialize Map and Set", ()=>{
|
|
70
|
+
const { parsed } = parse({
|
|
71
|
+
map: new Map([
|
|
72
|
+
[
|
|
73
|
+
"a",
|
|
74
|
+
1
|
|
75
|
+
]
|
|
76
|
+
]),
|
|
77
|
+
set: new Set([
|
|
78
|
+
1,
|
|
79
|
+
2
|
|
80
|
+
])
|
|
81
|
+
});
|
|
82
|
+
expect(parsed.map).toEqual({
|
|
83
|
+
__type: "Map",
|
|
84
|
+
entries: [
|
|
85
|
+
[
|
|
86
|
+
"a",
|
|
87
|
+
1
|
|
88
|
+
]
|
|
89
|
+
]
|
|
90
|
+
});
|
|
91
|
+
expect(parsed.set).toEqual({
|
|
92
|
+
__type: "Set",
|
|
93
|
+
items: [
|
|
94
|
+
1,
|
|
95
|
+
2
|
|
96
|
+
]
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
it("should drop an undefined property, so a logged payload matches what was sent", ()=>{
|
|
100
|
+
const { parsed } = parse({
|
|
101
|
+
kept: 1,
|
|
102
|
+
dropped: void 0
|
|
103
|
+
});
|
|
104
|
+
expect(parsed).toEqual({
|
|
105
|
+
kept: 1
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
it("should keep undefined array elements, where dropping would shift every later index", ()=>{
|
|
109
|
+
const { parsed } = parse({
|
|
110
|
+
items: [
|
|
111
|
+
1,
|
|
112
|
+
void 0,
|
|
113
|
+
3
|
|
114
|
+
]
|
|
115
|
+
});
|
|
116
|
+
expect(parsed).toEqual({
|
|
117
|
+
items: [
|
|
118
|
+
1,
|
|
119
|
+
"#[Undefined value]",
|
|
120
|
+
3
|
|
121
|
+
]
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
it("should not throw when a getter throws", ()=>{
|
|
125
|
+
const value = {
|
|
126
|
+
get exploding () {
|
|
127
|
+
throw new Error("Nope");
|
|
128
|
+
},
|
|
129
|
+
safe: 1
|
|
130
|
+
};
|
|
131
|
+
const { parsed } = parse(value);
|
|
132
|
+
expect(parsed.exploding).toBe("#[Throws: Nope]");
|
|
133
|
+
expect(parsed.safe).toBe(1);
|
|
134
|
+
});
|
|
135
|
+
it("should cap depth", ()=>{
|
|
136
|
+
const { parsed } = parse({
|
|
137
|
+
a: {
|
|
138
|
+
b: {
|
|
139
|
+
c: {
|
|
140
|
+
d: "deep"
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}, {
|
|
145
|
+
depth: 2
|
|
146
|
+
});
|
|
147
|
+
expect(parsed).toEqual({
|
|
148
|
+
a: {
|
|
149
|
+
b: "#[Depth limit]"
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
it("should cap string length and say how much was cut", ()=>{
|
|
154
|
+
const { parsed } = parse({
|
|
155
|
+
text: "x".repeat(20)
|
|
156
|
+
}, {
|
|
157
|
+
stringLength: 5
|
|
158
|
+
});
|
|
159
|
+
expect(parsed).toEqual({
|
|
160
|
+
text: "xxxxx…(15 more)"
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
it("should cap array length and say how much was cut", ()=>{
|
|
164
|
+
const { parsed } = parse({
|
|
165
|
+
items: [
|
|
166
|
+
1,
|
|
167
|
+
2,
|
|
168
|
+
3,
|
|
169
|
+
4,
|
|
170
|
+
5
|
|
171
|
+
]
|
|
172
|
+
}, {
|
|
173
|
+
arrayLength: 2
|
|
174
|
+
});
|
|
175
|
+
expect(parsed).toEqual({
|
|
176
|
+
items: [
|
|
177
|
+
1,
|
|
178
|
+
2,
|
|
179
|
+
"…(3 more items)"
|
|
180
|
+
]
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
it("should replace the payload wholesale when it exceeds the per-entry cap", ()=>{
|
|
184
|
+
const result = parse({
|
|
185
|
+
text: "x".repeat(5000)
|
|
186
|
+
}, {
|
|
187
|
+
entryBytes: 100,
|
|
188
|
+
stringLength: 100000
|
|
189
|
+
});
|
|
190
|
+
expect(result.truncated).toBe(true);
|
|
191
|
+
expect(result.parsed.truncated).toBe(true);
|
|
192
|
+
expect(result.parsed.originalBytes).toBeGreaterThan(100);
|
|
193
|
+
expect(result.bytes).toBeLessThanOrEqual(100);
|
|
194
|
+
});
|
|
195
|
+
it("should never invoke a custom toJSON, so it cannot be broken by one", ()=>{
|
|
196
|
+
const value = {
|
|
197
|
+
id: 1,
|
|
198
|
+
toJSON () {
|
|
199
|
+
throw new Error("Nope");
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
const { parsed } = parse({
|
|
203
|
+
value
|
|
204
|
+
});
|
|
205
|
+
expect(parsed.value.id).toBe(1);
|
|
206
|
+
expect(parsed.value.toJSON).toBe("#[Function: toJSON]");
|
|
207
|
+
});
|
|
208
|
+
it("should keep non-finite numbers readable instead of turning them into null", ()=>{
|
|
209
|
+
const { parsed } = parse({
|
|
210
|
+
a: 1 / 0,
|
|
211
|
+
b: NaN
|
|
212
|
+
});
|
|
213
|
+
expect(parsed).toEqual({
|
|
214
|
+
a: "Infinity",
|
|
215
|
+
b: "NaN"
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
//# sourceMappingURL=serialize.test.js.map
|