@galaxy-stack/orbit-graphql-federation 0.1.7 → 0.1.9
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/dist/index.js +293 -7
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -446,6 +446,269 @@ class SchemaBuilder {
|
|
|
446
446
|
// ../graphql/src/module/graphql.module.ts
|
|
447
447
|
import"reflect-metadata";
|
|
448
448
|
import { parse, validate, execute } from "graphql";
|
|
449
|
+
|
|
450
|
+
// ../common/src/decorators/params.decorator.ts
|
|
451
|
+
import"reflect-metadata";
|
|
452
|
+
var ROUTE_PARAMS_KEY = "orbit:route:params";
|
|
453
|
+
function createParamDecorator2(type) {
|
|
454
|
+
return (data) => {
|
|
455
|
+
return (target, propertyKey, parameterIndex) => {
|
|
456
|
+
if (!propertyKey)
|
|
457
|
+
return;
|
|
458
|
+
const existingParams = Reflect.getMetadata(ROUTE_PARAMS_KEY, target, propertyKey) || [];
|
|
459
|
+
existingParams.push({
|
|
460
|
+
type,
|
|
461
|
+
data,
|
|
462
|
+
index: parameterIndex
|
|
463
|
+
});
|
|
464
|
+
Reflect.defineMetadata(ROUTE_PARAMS_KEY, existingParams, target, propertyKey);
|
|
465
|
+
};
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
var Body = createParamDecorator2("body" /* BODY */);
|
|
469
|
+
var Query = createParamDecorator2("query" /* QUERY */);
|
|
470
|
+
var Param = createParamDecorator2("param" /* PARAM */);
|
|
471
|
+
var Headers2 = createParamDecorator2("headers" /* HEADERS */);
|
|
472
|
+
var Req = createParamDecorator2("request" /* REQUEST */);
|
|
473
|
+
var Res = createParamDecorator2("response" /* RESPONSE */);
|
|
474
|
+
var Ip = createParamDecorator2("ip" /* IP */);
|
|
475
|
+
var Session = createParamDecorator2("session" /* SESSION */);
|
|
476
|
+
var UploadedFile = createParamDecorator2("file" /* FILE */);
|
|
477
|
+
var UploadedFiles = createParamDecorator2("files" /* FILES */);
|
|
478
|
+
// ../common/src/decorators/http-code.decorator.ts
|
|
479
|
+
import"reflect-metadata";
|
|
480
|
+
// ../common/src/decorators/use-guards.decorator.ts
|
|
481
|
+
import"reflect-metadata";
|
|
482
|
+
// ../common/src/decorators/use-pipes.decorator.ts
|
|
483
|
+
import"reflect-metadata";
|
|
484
|
+
// ../common/src/decorators/use-interceptors.decorator.ts
|
|
485
|
+
import"reflect-metadata";
|
|
486
|
+
// ../common/src/decorators/catch.decorator.ts
|
|
487
|
+
import"reflect-metadata";
|
|
488
|
+
// ../common/src/decorators/transform.decorator.ts
|
|
489
|
+
var TRANSFORM_METADATA = Symbol("transform:metadata");
|
|
490
|
+
// ../common/src/security-headers.ts
|
|
491
|
+
var DEFAULTS = {
|
|
492
|
+
hsts: true,
|
|
493
|
+
frameguard: "SAMEORIGIN",
|
|
494
|
+
noSniff: true,
|
|
495
|
+
referrerPolicy: "no-referrer",
|
|
496
|
+
crossOriginOpenerPolicy: "same-origin",
|
|
497
|
+
crossOriginResourcePolicy: "same-origin",
|
|
498
|
+
hidePoweredBy: true,
|
|
499
|
+
originAgentCluster: true,
|
|
500
|
+
permittedCrossDomainPolicies: "none",
|
|
501
|
+
dnsPrefetchControl: false
|
|
502
|
+
};
|
|
503
|
+
function buildSecureHeaders(options = {}) {
|
|
504
|
+
const opts = { ...DEFAULTS, ...options };
|
|
505
|
+
const headers = {};
|
|
506
|
+
if (opts.hidePoweredBy) {
|
|
507
|
+
headers["X-Powered-By"] = "";
|
|
508
|
+
}
|
|
509
|
+
if (opts.noSniff) {
|
|
510
|
+
headers["X-Content-Type-Options"] = "nosniff";
|
|
511
|
+
}
|
|
512
|
+
if (opts.frameguard) {
|
|
513
|
+
headers["X-Frame-Options"] = opts.frameguard;
|
|
514
|
+
}
|
|
515
|
+
if (opts.hsts) {
|
|
516
|
+
const hsts = typeof opts.hsts === "object" ? opts.hsts : { maxAge: 15552000, includeSubDomains: true };
|
|
517
|
+
let value = `max-age=${hsts.maxAge ?? 15552000}`;
|
|
518
|
+
if (hsts.includeSubDomains !== false)
|
|
519
|
+
value += "; includeSubDomains";
|
|
520
|
+
if ("preload" in hsts && hsts.preload)
|
|
521
|
+
value += "; preload";
|
|
522
|
+
headers["Strict-Transport-Security"] = value;
|
|
523
|
+
}
|
|
524
|
+
if (opts.referrerPolicy) {
|
|
525
|
+
headers["Referrer-Policy"] = opts.referrerPolicy;
|
|
526
|
+
}
|
|
527
|
+
if (opts.crossOriginOpenerPolicy) {
|
|
528
|
+
headers["Cross-Origin-Opener-Policy"] = opts.crossOriginOpenerPolicy;
|
|
529
|
+
}
|
|
530
|
+
if (opts.crossOriginResourcePolicy) {
|
|
531
|
+
headers["Cross-Origin-Resource-Policy"] = opts.crossOriginResourcePolicy;
|
|
532
|
+
}
|
|
533
|
+
if (opts.originAgentCluster) {
|
|
534
|
+
headers["Origin-Agent-Cluster"] = "?1";
|
|
535
|
+
}
|
|
536
|
+
if (opts.permittedCrossDomainPolicies) {
|
|
537
|
+
headers["X-Permitted-Cross-Domain-Policies"] = opts.permittedCrossDomainPolicies;
|
|
538
|
+
}
|
|
539
|
+
if (opts.dnsPrefetchControl) {
|
|
540
|
+
headers["X-DNS-Prefetch-Control"] = "on";
|
|
541
|
+
} else {
|
|
542
|
+
headers["X-DNS-Prefetch-Control"] = "off";
|
|
543
|
+
}
|
|
544
|
+
return headers;
|
|
545
|
+
}
|
|
546
|
+
function withSecureHeaders(response, options = {}) {
|
|
547
|
+
const secure = buildSecureHeaders(options);
|
|
548
|
+
const headers = new Headers(response.headers);
|
|
549
|
+
for (const [key, value] of Object.entries(secure)) {
|
|
550
|
+
if (value === "") {
|
|
551
|
+
headers.delete(key);
|
|
552
|
+
} else {
|
|
553
|
+
headers.set(key, value);
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
return new Response(response.body, {
|
|
557
|
+
status: response.status,
|
|
558
|
+
statusText: response.statusText,
|
|
559
|
+
headers
|
|
560
|
+
});
|
|
561
|
+
}
|
|
562
|
+
// ../graphql/src/security/validation-rules.ts
|
|
563
|
+
import {
|
|
564
|
+
GraphQLError,
|
|
565
|
+
Kind
|
|
566
|
+
} from "graphql";
|
|
567
|
+
function depthLimit(maxDepth) {
|
|
568
|
+
return (context) => {
|
|
569
|
+
const definedFragments = new Map;
|
|
570
|
+
for (const def of context.getDocument().definitions) {
|
|
571
|
+
if (def.kind === Kind.FRAGMENT_DEFINITION) {
|
|
572
|
+
definedFragments.set(def.name.value, def);
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
const fragmentDepths = new Map;
|
|
576
|
+
const visiting = new Set;
|
|
577
|
+
const measureSelections = (selections, depth) => {
|
|
578
|
+
let deepest = depth;
|
|
579
|
+
for (const node of selections) {
|
|
580
|
+
if (node.kind === Kind.FIELD) {
|
|
581
|
+
const field = node;
|
|
582
|
+
if (field.name.value === "__typename")
|
|
583
|
+
continue;
|
|
584
|
+
deepest = Math.max(deepest, measureSelections(field.selectionSet?.selections ?? [], depth + 1));
|
|
585
|
+
} else if (node.kind === Kind.INLINE_FRAGMENT) {
|
|
586
|
+
deepest = Math.max(deepest, measureSelections(node.selectionSet.selections, depth));
|
|
587
|
+
} else if (node.kind === Kind.FRAGMENT_SPREAD) {
|
|
588
|
+
const name = node.name.value;
|
|
589
|
+
if (visiting.has(name))
|
|
590
|
+
continue;
|
|
591
|
+
const frag = definedFragments.get(name);
|
|
592
|
+
if (!frag)
|
|
593
|
+
continue;
|
|
594
|
+
const cached = fragmentDepths.get(name);
|
|
595
|
+
const fragDepth = cached !== undefined ? cached : (() => {
|
|
596
|
+
visiting.add(name);
|
|
597
|
+
const d = measureSelections(frag.selectionSet.selections, depth);
|
|
598
|
+
visiting.delete(name);
|
|
599
|
+
fragmentDepths.set(name, d);
|
|
600
|
+
return d;
|
|
601
|
+
})();
|
|
602
|
+
deepest = Math.max(deepest, fragDepth);
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
return deepest;
|
|
606
|
+
};
|
|
607
|
+
return {
|
|
608
|
+
OperationDefinition(node) {
|
|
609
|
+
const depth = measureSelections(node.selectionSet.selections, 0);
|
|
610
|
+
if (depth > maxDepth) {
|
|
611
|
+
context.reportError(new GraphQLError(`Query exceeds maximum depth of ${maxDepth} (got ${depth}).`, { nodes: node }));
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
};
|
|
615
|
+
};
|
|
616
|
+
}
|
|
617
|
+
function complexityLimit(options) {
|
|
618
|
+
const {
|
|
619
|
+
maxComplexity,
|
|
620
|
+
defaultFieldCost = 1,
|
|
621
|
+
defaultListFactor = 10,
|
|
622
|
+
fieldCosts = {},
|
|
623
|
+
listFactors = {}
|
|
624
|
+
} = options;
|
|
625
|
+
return (context) => {
|
|
626
|
+
const definedFragments = new Map;
|
|
627
|
+
for (const def of context.getDocument().definitions) {
|
|
628
|
+
if (def.kind === Kind.FRAGMENT_DEFINITION) {
|
|
629
|
+
definedFragments.set(def.name.value, def);
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
const fragmentCosts = new Map;
|
|
633
|
+
const visiting = new Set;
|
|
634
|
+
const measureSelections = (selections, multiplier) => {
|
|
635
|
+
let total = 0;
|
|
636
|
+
for (const node of selections) {
|
|
637
|
+
if (node.kind === Kind.FIELD) {
|
|
638
|
+
const field = node;
|
|
639
|
+
if (field.name.value === "__typename")
|
|
640
|
+
continue;
|
|
641
|
+
const cost = fieldCosts[field.name.value] ?? defaultFieldCost;
|
|
642
|
+
const isList = Boolean(field.selectionSet) && (listFactors[field.name.value] ?? defaultListFactor);
|
|
643
|
+
const factor = field.selectionSet ? listFactors[field.name.value] ?? defaultListFactor : 1;
|
|
644
|
+
const childCost = field.selectionSet ? measureSelections(field.selectionSet.selections, multiplier * factor) : 0;
|
|
645
|
+
total += cost * multiplier + childCost;
|
|
646
|
+
} else if (node.kind === Kind.INLINE_FRAGMENT) {
|
|
647
|
+
total += measureSelections(node.selectionSet.selections, multiplier);
|
|
648
|
+
} else if (node.kind === Kind.FRAGMENT_SPREAD) {
|
|
649
|
+
const name = node.name.value;
|
|
650
|
+
if (visiting.has(name))
|
|
651
|
+
continue;
|
|
652
|
+
const frag = definedFragments.get(name);
|
|
653
|
+
if (!frag)
|
|
654
|
+
continue;
|
|
655
|
+
const cached = fragmentCosts.get(name);
|
|
656
|
+
const fragCost = cached !== undefined ? cached : (() => {
|
|
657
|
+
visiting.add(name);
|
|
658
|
+
const c = measureSelections(frag.selectionSet.selections, multiplier);
|
|
659
|
+
visiting.delete(name);
|
|
660
|
+
fragmentCosts.set(name, c);
|
|
661
|
+
return c;
|
|
662
|
+
})();
|
|
663
|
+
total += fragCost;
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
return total;
|
|
667
|
+
};
|
|
668
|
+
return {
|
|
669
|
+
OperationDefinition(node) {
|
|
670
|
+
const complexity = measureSelections(node.selectionSet.selections, 1);
|
|
671
|
+
if (complexity > maxComplexity) {
|
|
672
|
+
context.reportError(new GraphQLError(`Query complexity ${complexity} exceeds maximum of ${maxComplexity}.`, { nodes: node }));
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
};
|
|
676
|
+
};
|
|
677
|
+
}
|
|
678
|
+
function aliasLimit(maxAliases) {
|
|
679
|
+
return (context) => {
|
|
680
|
+
let aliasCount = 0;
|
|
681
|
+
return {
|
|
682
|
+
Field(node) {
|
|
683
|
+
if (node.alias) {
|
|
684
|
+
aliasCount++;
|
|
685
|
+
if (aliasCount > maxAliases) {
|
|
686
|
+
context.reportError(new GraphQLError(`Query exceeds maximum of ${maxAliases} aliases.`, { nodes: node }));
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
};
|
|
691
|
+
};
|
|
692
|
+
}
|
|
693
|
+
function blockIntrospection() {
|
|
694
|
+
return (context) => {
|
|
695
|
+
return {
|
|
696
|
+
Field(node) {
|
|
697
|
+
if (node.name.value === "__schema" || node.name.value === "__type") {
|
|
698
|
+
context.reportError(new GraphQLError("GraphQL introspection is disabled.", { nodes: node }));
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
};
|
|
702
|
+
};
|
|
703
|
+
}
|
|
704
|
+
// ../graphql/src/security/options.ts
|
|
705
|
+
var DEFAULT_GRAPHQL_SECURITY = {
|
|
706
|
+
maxDepth: 10,
|
|
707
|
+
maxComplexity: 1000,
|
|
708
|
+
maxAliases: 30
|
|
709
|
+
};
|
|
710
|
+
|
|
711
|
+
// ../graphql/src/module/graphql.module.ts
|
|
449
712
|
var GRAPHQL_OPTIONS = Symbol("GRAPHQL_OPTIONS");
|
|
450
713
|
var GRAPHQL_SCHEMA = Symbol("GRAPHQL_SCHEMA");
|
|
451
714
|
|
|
@@ -530,6 +793,11 @@ class GraphQLHandler {
|
|
|
530
793
|
this.schema = schema;
|
|
531
794
|
this.options = options;
|
|
532
795
|
}
|
|
796
|
+
wrap(response) {
|
|
797
|
+
if (this.options.secureHeaders === false)
|
|
798
|
+
return response;
|
|
799
|
+
return withSecureHeaders(response);
|
|
800
|
+
}
|
|
533
801
|
createLoaderContext() {
|
|
534
802
|
const ctx = new DataLoaderContext;
|
|
535
803
|
const loaders = this.options.loaders || {};
|
|
@@ -561,14 +829,14 @@ class GraphQLHandler {
|
|
|
561
829
|
...userContext
|
|
562
830
|
};
|
|
563
831
|
const document = parse(query);
|
|
564
|
-
const validationErrors = validate(this.schema, document);
|
|
832
|
+
const validationErrors = validate(this.schema, document, this.buildSecurityRules());
|
|
565
833
|
if (validationErrors.length > 0) {
|
|
566
|
-
return new Response(JSON.stringify({
|
|
834
|
+
return this.wrap(new Response(JSON.stringify({
|
|
567
835
|
errors: validationErrors.map((e) => this.formatError(e))
|
|
568
836
|
}), {
|
|
569
837
|
status: 400,
|
|
570
838
|
headers: { "Content-Type": "application/json" }
|
|
571
|
-
});
|
|
839
|
+
}));
|
|
572
840
|
}
|
|
573
841
|
const result = await execute({
|
|
574
842
|
schema: this.schema,
|
|
@@ -580,17 +848,35 @@ class GraphQLHandler {
|
|
|
580
848
|
if (result.errors) {
|
|
581
849
|
result.errors = result.errors.map((e) => this.formatError(e));
|
|
582
850
|
}
|
|
583
|
-
return new Response(JSON.stringify(result), {
|
|
851
|
+
return this.wrap(new Response(JSON.stringify(result), {
|
|
584
852
|
headers: { "Content-Type": "application/json" }
|
|
585
|
-
});
|
|
853
|
+
}));
|
|
586
854
|
} catch (error) {
|
|
587
|
-
return new Response(JSON.stringify({
|
|
855
|
+
return this.wrap(new Response(JSON.stringify({
|
|
588
856
|
errors: [this.formatError(error)]
|
|
589
857
|
}), {
|
|
590
858
|
status: 500,
|
|
591
859
|
headers: { "Content-Type": "application/json" }
|
|
592
|
-
});
|
|
860
|
+
}));
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
buildSecurityRules() {
|
|
864
|
+
const sec = this.options.security || {};
|
|
865
|
+
const introspection = this.options.introspection !== false;
|
|
866
|
+
const disableIntrospection = sec.disableIntrospection ?? !introspection;
|
|
867
|
+
const rules = [
|
|
868
|
+
depthLimit(sec.maxDepth ?? DEFAULT_GRAPHQL_SECURITY.maxDepth),
|
|
869
|
+
complexityLimit({
|
|
870
|
+
maxComplexity: sec.maxComplexity ?? DEFAULT_GRAPHQL_SECURITY.maxComplexity,
|
|
871
|
+
fieldCosts: sec.fieldCosts,
|
|
872
|
+
listFactors: sec.listFactors
|
|
873
|
+
}),
|
|
874
|
+
aliasLimit(sec.maxAliases ?? DEFAULT_GRAPHQL_SECURITY.maxAliases)
|
|
875
|
+
];
|
|
876
|
+
if (disableIntrospection) {
|
|
877
|
+
rules.push(blockIntrospection());
|
|
593
878
|
}
|
|
879
|
+
return rules;
|
|
594
880
|
}
|
|
595
881
|
formatError(error) {
|
|
596
882
|
if (this.options.formatError) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@galaxy-stack/orbit-graphql-federation",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.9",
|
|
4
4
|
"description": "GraphQL Federation support for Orbit framework",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -14,8 +14,8 @@
|
|
|
14
14
|
"graphql": "^16.0.0"
|
|
15
15
|
},
|
|
16
16
|
"dependencies": {
|
|
17
|
-
"@galaxy-stack/orbit-core": "^0.
|
|
18
|
-
"@galaxy-stack/orbit-graphql": "^0.1.
|
|
17
|
+
"@galaxy-stack/orbit-core": "^0.2.0",
|
|
18
|
+
"@galaxy-stack/orbit-graphql": "^0.1.10"
|
|
19
19
|
},
|
|
20
20
|
"devDependencies": {
|
|
21
21
|
"graphql": "^16.9.0",
|