@adobe/aio-cli-plugin-api-mesh 5.0.0-alpha → 5.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,55 @@
1
+ /**
2
+ * CF-Connecting-IP provides the client IP address connecting to Cloudflare to the origin web server. This header will only be sent on the
3
+ * traffic from Cloudflare’s edge to your origin web server. Upstream requests will the Cloudflare Worker client IP address of
4
+ * `2a06:98c0:3600::103`.
5
+ * @see https://developers.cloudflare.com/fundamentals/reference/http-request-headers/#cf-connecting-ip
6
+ */
7
+ const CF_CONNECTING_IP_HEADER = 'cf-connecting-ip';
8
+
9
+ /**
10
+ * The X-Forwarded-For (XFF) request header is a de-facto standard header for identifying the originating IP address of a client connecting
11
+ * to a web server through a proxy server.
12
+ * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-For
13
+ */
14
+ const X_FORWARDED_FOR_HEADER = 'x-forwarded-for';
15
+
16
+ /**
17
+ * Add `x-forwarded-for` header from request context to each fetch.
18
+ * @param context Request context.
19
+ * @param headers Fetch headers.
20
+ */
21
+ const addXForwardedForHeader = (context, headers) => {
22
+ // `cf-connecting-ip` header contains the original visitor's IP address
23
+ const connectingIp = context?.request.headers.get(CF_CONNECTING_IP_HEADER);
24
+ const xForwardedFor = context?.request.headers.get(X_FORWARDED_FOR_HEADER);
25
+ if (connectingIp) {
26
+ if (!xForwardedFor) {
27
+ // Construct new `x-forwarded-for` header if not present in original request
28
+ headers.set(X_FORWARDED_FOR_HEADER, connectingIp);
29
+ } else {
30
+ // Construct `x-forwarded-for` header using original header
31
+ headers.set(X_FORWARDED_FOR_HEADER, `${xForwardedFor}, ${connectingIp}`);
32
+ }
33
+ }
34
+ };
35
+
36
+ /**
37
+ * Adds compliance headers to source fetch requests.
38
+ */
39
+ function useComplianceHeaders() {
40
+ return {
41
+ onFetch({ context, options }) {
42
+ // Construct mutable headers from options passed to each fetch
43
+ const headers = new Headers(options.headers);
44
+ addXForwardedForHeader(context, headers);
45
+ options.headers = headers;
46
+ },
47
+ };
48
+ }
49
+
50
+ module.exports = {
51
+ useComplianceHeaders,
52
+ CF_CONNECTING_IP_HEADER,
53
+ X_FORWARDED_FOR_HEADER,
54
+ addXForwardedForHeader,
55
+ };
@@ -0,0 +1,2 @@
1
+ const { useComplianceHeaders } = require('./complianceHeaders');
2
+ module.exports = useComplianceHeaders;
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2020 Uri Goldshtein
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,81 @@
1
+ /**
2
+ * Fork of https://github.com/ardatan/graphql-mesh/blob/%40graphql-mesh/plugin-http-details-extensions%400.1.21/packages/plugins/http-details-extensions/src/index.ts
3
+ * Version: 0.1.21
4
+ * TODO: Extract to a separate repository/artifact after approach has been validated.
5
+ */
6
+
7
+ /* eslint-disable */
8
+
9
+ const { isAsyncIterable } = require('@envelop/core');
10
+ const { getHeadersObj } = require('@graphql-mesh/utils');
11
+
12
+ function useIncludeHttpDetailsInExtensions(opts) {
13
+ if (!opts.if) {
14
+ return {};
15
+ }
16
+
17
+ const httpDetailsByContext = new WeakMap();
18
+
19
+ function getHttpDetailsByContext(context) {
20
+ let httpDetails = httpDetailsByContext.get(context);
21
+ if (!httpDetails) {
22
+ httpDetails = [];
23
+ httpDetailsByContext.set(context, httpDetails);
24
+ }
25
+ return httpDetails;
26
+ }
27
+
28
+ return {
29
+ onFetch({ url, context, info, options }) {
30
+ if (context != null) {
31
+ const requestTimestamp = Date.now();
32
+ return ({ response }) => {
33
+ const responseTimestamp = Date.now();
34
+ const responseTime = responseTimestamp - requestTimestamp;
35
+ const httpDetailsList = getHttpDetailsByContext(context);
36
+ const httpDetails = {
37
+ sourceName: (info)?.sourceName,
38
+ path: info?.path,
39
+ request: {
40
+ timestamp: requestTimestamp,
41
+ url,
42
+ method: options.method || 'GET',
43
+ headers: getHeadersObj(options.headers),
44
+ },
45
+ response: {
46
+ timestamp: responseTimestamp,
47
+ status: response.status,
48
+ statusText: response.statusText,
49
+ headers: getHeadersObj(response.headers),
50
+ // Added to interface to account for edge fetch implementation/behavior
51
+ cookies: response.headers.getSetCookie(),
52
+ },
53
+ responseTime,
54
+ };
55
+ httpDetailsList.push(httpDetails);
56
+ };
57
+ }
58
+ return undefined;
59
+ },
60
+ onExecute({ args: { contextValue } }) {
61
+ return {
62
+ onExecuteDone({ result, setResult }) {
63
+ if (!isAsyncIterable(result)) {
64
+ const httpDetailsList = httpDetailsByContext.get(contextValue);
65
+ if (httpDetailsList != null) {
66
+ setResult({
67
+ ...result,
68
+ extensions: {
69
+ ...result.extensions,
70
+ httpDetails: httpDetailsList,
71
+ },
72
+ });
73
+ }
74
+ }
75
+ },
76
+ };
77
+ },
78
+ };
79
+ }
80
+
81
+ module.exports = useIncludeHttpDetailsInExtensions;
package/src/secrets.js ADDED
@@ -0,0 +1,34 @@
1
+ // Parse the yaml secrets string from env to json object
2
+ function loadMeshSecrets(logger, secret) {
3
+ let parsedSecrets = {};
4
+
5
+ try {
6
+ // Replace escaped backslashes with a single backslash
7
+ secret = secret.replace(/\\"/g, '"');
8
+ parsedSecrets = JSON.parse(secret);
9
+ } catch (err) {
10
+ logger.error('Error parsing secrets.');
11
+ }
12
+
13
+ return parsedSecrets;
14
+ }
15
+
16
+ // Custom get secrets handler
17
+ const getSecretsHandler = {
18
+ get: function (target, prop, receiver) {
19
+ if (prop === 'toJSON') {
20
+ // Handle the toJSON case
21
+ return () => target;
22
+ }
23
+ if (prop in target) {
24
+ return Reflect.get(target, prop, receiver);
25
+ } else {
26
+ throw new Error(`The secret ${String(prop)} is not available.`);
27
+ }
28
+ },
29
+ set: function () {
30
+ throw new Error('Setting secrets is not allowed');
31
+ },
32
+ };
33
+
34
+ module.exports = { loadMeshSecrets, getSecretsHandler };
package/src/served.js ADDED
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Header to indicate which tier served the request.
3
+ */
4
+ const SERVE_TIER_HEADER = 'x-api-mesh-served';
5
+
6
+ /**
7
+ * Tiers that served the request.
8
+ */
9
+ const ServedTier = {
10
+ WORKER_HOT: 0,
11
+ };
12
+
13
+ /**
14
+ * Add the served header to the response. Requires mutable headers on the response object.
15
+ * @param response Response.
16
+ * @param servedTier Tier that served the request.
17
+ */
18
+ const addServedHeader = (response, servedTier) => {
19
+ response.headers.set(SERVE_TIER_HEADER, servedTier.toString());
20
+ };
21
+
22
+ module.exports = { ServedTier, addServedHeader };
package/src/server.js CHANGED
@@ -1,200 +1,38 @@
1
- // Resolve the path to 'fastify' and 'graphql-yoga' within the local 'node_modules'
2
- const fastifyPath = require.resolve('fastify', { paths: [process.cwd()] });
3
- const yogaPath = require.resolve('graphql-yoga', { paths: [process.cwd()] });
4
-
5
- // Load 'fastify' and 'graphql-yoga' using the resolved paths
6
- const fastify = require(fastifyPath);
7
- const { createYoga } = require(yogaPath);
8
- const logger = require('./classes/logger');
9
-
10
- //Load the functions from serverUtils.js
11
- const {
12
- readMeshConfig,
13
- removeRequestHeaders,
14
- prepSourceResponseHeaders,
15
- processResponseHeaders,
16
- readSecretsFile,
17
- } = require('./serverUtils');
18
-
19
- let yogaServer = null;
20
- let meshConfig;
21
-
22
- // catch unhandled promise rejections
23
- process.on('unhandledRejection', reason => {
24
- logger.error('Unhandled Rejection at:', reason.stack || reason);
25
- });
26
-
27
- // catch uncaught exceptions
28
- process.on('uncaughtException', err => {
29
- logger.error('Uncaught Exception thrown');
30
- logger.error(err.stack);
31
- process.exit(1);
32
- });
33
-
34
- // get meshId from command line arguments
35
- const meshId = process.argv[2];
36
-
37
- // get PORT number from command line arguments
38
- const portNo = parseInt(process.argv[3]);
39
-
40
- const getCORSOptions = () => {
41
- try {
42
- const currentWorkingDirectory = process.cwd();
43
- const meshConfigPath = `${currentWorkingDirectory}/mesh-artifact/${meshId}/.meshrc.json`;
44
-
45
- const meshConfig = require(meshConfigPath);
46
- const { responseConfig } = meshConfig;
47
- const { CORS } = responseConfig;
48
-
49
- return CORS;
50
- } catch (e) {
51
- return {};
52
- }
53
- };
54
-
55
- // Custom get secrets handler
56
- const getSecretsHandler = {
57
- get: function (target, prop, receiver) {
58
- if (prop === 'toJSON') {
59
- // Handle the toJSON case
60
- return () => target;
61
- }
62
- if (prop in target) {
63
- return Reflect.get(target, prop, receiver);
64
- } else {
65
- throw new Error(`The secret ${String(prop)} is not available.`);
66
- }
67
- },
68
- set: function () {
69
- throw new Error('Setting secrets is not allowed');
70
- },
1
+ const { spawn } = require('child_process');
2
+ const { readSecretsFile } = require('./serverUtils');
3
+ const packageData = require('../package.json');
4
+
5
+ const runServer = portNo => {
6
+ const wranglerPackageNumber = packageData.dependencies.wrangler;
7
+ const wranglerVersion = `wrangler@${wranglerPackageNumber.replace(/^[\^~]/, '')}`;
8
+ const indexFilePath = `${__dirname}/index.js`;
9
+ const filePath = '.mesh';
10
+ const secrets = readSecretsFile(filePath);
11
+ const commandArgs = [
12
+ wranglerVersion,
13
+ 'dev',
14
+ indexFilePath,
15
+ '--show-interactive-dev-session',
16
+ 'false',
17
+ '--var',
18
+ `Secret:${JSON.stringify(secrets)}`,
19
+ '--port',
20
+ portNo,
21
+ '--inspector-port',
22
+ '9229',
23
+ ];
24
+
25
+ const wrangler = spawn('npx', commandArgs, {
26
+ stdio: 'inherit',
27
+ });
28
+
29
+ wrangler.on('close', code => {
30
+ console.log(`wrangler dev process exited with code ${code}`);
31
+ });
32
+
33
+ wrangler.on('error', error => {
34
+ console.error(`Failed to start wrangler dev: ${error.message}`);
35
+ });
71
36
  };
72
37
 
73
- const getYogaServer = async () => {
74
- if (yogaServer) {
75
- return yogaServer;
76
- } else {
77
- const currentWorkingDirectory = process.cwd();
78
- const meshArtifactsPath = `${currentWorkingDirectory}/mesh-artifact/${meshId}`;
79
-
80
- const meshArtifacts = require(meshArtifactsPath);
81
- const { getBuiltMesh } = meshArtifacts;
82
-
83
- const tenantMesh = await getBuiltMesh();
84
- const corsOptions = getCORSOptions();
85
-
86
- const secrets = readSecretsFile(meshId);
87
-
88
- const secretsProxy = new Proxy(secrets, getSecretsHandler);
89
-
90
- logger.info('Creating graphQL server');
91
-
92
- meshConfig = readMeshConfig(meshId);
93
-
94
- yogaServer = createYoga({
95
- plugins: tenantMesh.plugins,
96
- graphqlEndpoint: `/graphql`,
97
- graphiql: true,
98
- maskedErrors: false,
99
- cors: corsOptions,
100
- context: initialContext => ({
101
- ...initialContext,
102
- secrets: secretsProxy,
103
- }),
104
- });
105
-
106
- return yogaServer;
107
- }
108
- };
109
-
110
- const app = fastify();
111
-
112
- app.route({
113
- method: ['GET', 'POST'],
114
- url: '/graphql',
115
- handler: async (req, res) => {
116
- logger.info('Request received: ', req.body);
117
-
118
- let body = null;
119
- let responseBody = null;
120
- let includeMetaData = false;
121
-
122
- if (req.headers['x-include-metadata'] && req.headers['x-include-metadata'].length > 0) {
123
- if (req.headers['x-include-metadata'].toLowerCase() === 'true') {
124
- includeMetaData = true;
125
- }
126
- }
127
-
128
- const response = await yogaServer.handleNodeRequest(req, {
129
- req,
130
- reply: res,
131
- });
132
-
133
- try {
134
- try {
135
- body = await response.text();
136
- if (body) {
137
- responseBody = JSON.parse(body);
138
- }
139
- } catch (err) {
140
- logger.error(`Error parsing response body: ${err}`);
141
- logger.error(response);
142
- throw new Error(`Error parsing response body: ${err}`);
143
- }
144
- //Set the value of includeHTTPDetails flag
145
-
146
- const includeHTTPDetails = !!meshConfig?.responseConfig?.includeHTTPDetails;
147
- const meshHTTPDetails = responseBody?.extensions?.httpDetails;
148
- logger.info('Mesh HTTP Details: ', meshHTTPDetails);
149
-
150
- /* the logic for handling mesh response headers using includeMetaData */
151
- prepSourceResponseHeaders(meshHTTPDetails, req.id);
152
- const responseHeaders = processResponseHeaders(meshId, req.id, includeMetaData, req.method);
153
-
154
- /** Adding the yoga response headers to the response */
155
- response.headers?.forEach((value, key) => {
156
- res.header(key, value);
157
- });
158
-
159
- // Delete the httpDetails extensions details if mesh owner has disabled those details in the config
160
- if (includeHTTPDetails !== true) {
161
- delete responseBody?.extensions?.httpDetails;
162
- }
163
-
164
- //make sure to remove the request headers from cache after the request is complete
165
- removeRequestHeaders(req.id);
166
- const fastifyResponseBody = JSON.stringify(responseBody);
167
- res.status(response.status).headers(responseHeaders).send(fastifyResponseBody);
168
- } catch (err) {
169
- logger.error(`Error parsing response body: ${err}`);
170
- //we have this fallback catch clause if someone wants to load the graphiql engine. This returns the default headers back
171
- response.headers?.forEach((value, key) => {
172
- res.header(key, value);
173
- });
174
- res.status(response.status);
175
- res.send(response.body);
176
- }
177
-
178
- return res;
179
- },
180
- });
181
-
182
- app.listen(
183
- {
184
- //set the port no of the server based on the input value
185
- port: portNo,
186
- },
187
- async err => {
188
- try {
189
- if (err) {
190
- throw new Error(`Server setup error: ${err.message}`);
191
- }
192
- yogaServer = await getYogaServer();
193
- } catch (error) {
194
- console.error(error);
195
- process.exit(1);
196
- }
197
-
198
- console.log(`Server is running on http://localhost:${portNo}/graphql`);
199
- },
200
- );
38
+ module.exports = { runServer };
@@ -318,13 +318,13 @@ function ccDirectivesToString(directives) {
318
318
 
319
319
  /**
320
320
  * Returns secrets content from artifacts
321
- * @param meshId
321
+ * @param meshPath
322
322
  * @returns
323
323
  */
324
- function readSecretsFile(meshId) {
324
+ function readSecretsFile(meshPath) {
325
325
  let secrets = {};
326
326
  try {
327
- const filePath = path.resolve(process.cwd(), 'mesh-artifact', `${meshId}`, 'secrets.yaml');
327
+ const filePath = path.resolve(process.cwd(), `${meshPath}`, 'secrets.yaml');
328
328
  if (fs.existsSync(filePath)) {
329
329
  secrets = YAML.parse(fs.readFileSync(filePath, 'utf8'));
330
330
  }
@@ -0,0 +1,14 @@
1
+ # Tenant worker core definition. For platform workers metadata configs are used to set bindings.
2
+ name = "tenant-worker-core"
3
+ compatibility_date = "2024-06-03"
4
+ node_compat = false
5
+
6
+ find_additional_modules = true
7
+ base_dir = ".mesh"
8
+ rules = [
9
+ { type = "CommonJS", globs = ["tenantFiles/**/*.js"] }
10
+ ]
11
+
12
+
13
+ [[migrations]]
14
+ tag = "v1"
@@ -0,0 +1,42 @@
1
+ const pino = require('pino');
2
+
3
+ /**
4
+ * Get a new instance of a Pino logger with default configuration.
5
+ * @param level Log level.
6
+ */
7
+ const pinoLogger = level =>
8
+ pino({
9
+ level: level || 'info',
10
+ formatters: {
11
+ level(label) {
12
+ return {
13
+ level: label,
14
+ };
15
+ },
16
+ },
17
+ });
18
+
19
+ /**
20
+ * Get a new instance of a Pino logger with default configuration and child bindings.
21
+ * @param bindings Logger bindings.
22
+ */
23
+ const logger = bindings => {
24
+ return pinoLogger(bindings?.logLevel).child({
25
+ meshId: bindings?.meshId,
26
+ requestId: bindings?.requestId,
27
+ });
28
+ };
29
+
30
+ /**
31
+ * Create a logger from environment/request.
32
+ * @param bindings Logger bindings.
33
+ */
34
+ const bindedlogger = bindings => {
35
+ return logger({
36
+ logLevel: bindings?.logLevel,
37
+ meshId: bindings?.meshId,
38
+ requestId: bindings?.requestId,
39
+ });
40
+ };
41
+
42
+ module.exports = { logger, pinoLogger, bindedlogger };
@@ -0,0 +1,26 @@
1
+ const UUID = require('../uuid');
2
+
3
+ /**
4
+ * Default request identifier header name.
5
+ */
6
+ const DEFAULT_REQUEST_ID_HEADER_NAME = 'x-request-id';
7
+
8
+ /**
9
+ * Get request identifier from headers. Will attempt to set request identifier if not present.
10
+ * @param request Request.
11
+ * @param headerName Request identifier header name.
12
+ */
13
+ function getRequestId(request, headerName = DEFAULT_REQUEST_ID_HEADER_NAME) {
14
+ let requestId = request.headers.get(headerName);
15
+ if (!requestId) {
16
+ requestId = UUID.newUuid().toString();
17
+ try {
18
+ request.headers.set(headerName, requestId);
19
+ } catch (err) {
20
+ // Unable to set request headers
21
+ }
22
+ }
23
+ return requestId;
24
+ }
25
+
26
+ module.exports = { DEFAULT_REQUEST_ID_HEADER_NAME, getRequestId };
@@ -0,0 +1,80 @@
1
+ import { getMesh } from '@graphql-mesh/runtime';
2
+
3
+ const { getCorsOptions } = require('./cors');
4
+ const { createYoga } = require('graphql-yoga');
5
+ const { GraphQLError } = require('graphql/error');
6
+
7
+ const { loadMeshSecrets, getSecretsHandler } = require('./secrets');
8
+ const useComplianceHeaders = require('./plugins/complianceHeaders');
9
+ const UseHttpDetailsExtensions = require('./plugins/httpDetailsExtensions');
10
+ const useSourceHeaders = require('@adobe/plugin-source-headers');
11
+ const { useDisableIntrospection } = require('@envelop/disable-introspection');
12
+
13
+ let meshInstance$;
14
+
15
+ async function buildMeshInstance(meshArtifacts, meshConfig) {
16
+ const { getMeshOptions } = meshArtifacts;
17
+ const options = await getMeshOptions();
18
+
19
+ options.additionalEnvelopPlugins = (options.additionalEnvelopPlugins || []).concat(
20
+ useComplianceHeaders(),
21
+ UseHttpDetailsExtensions({
22
+ // Get the details of responseConfig.includeHTTPDetails and store in Cache
23
+ if: meshConfig.responseConfig?.includeHTTPDetails || false,
24
+ }),
25
+ useSourceHeaders(meshConfig),
26
+ );
27
+
28
+ if (meshConfig.disableIntrospection) {
29
+ options.additionalEnvelopPlugins.push(useDisableIntrospection());
30
+ }
31
+
32
+ return getMesh(options).then(mesh => {
33
+ const id = mesh.pubsub.subscribe('destroy', () => {
34
+ meshInstance$ = undefined;
35
+ mesh.pubsub.unsubscribe(id);
36
+ });
37
+ return mesh;
38
+ });
39
+ }
40
+
41
+ async function getBuiltMesh(meshArtifacts, meshConfig) {
42
+ if (meshInstance$ == null) {
43
+ meshInstance$ = buildMeshInstance(meshArtifacts, meshConfig);
44
+ }
45
+ return meshInstance$;
46
+ }
47
+
48
+ const buildServer = async (loggerInstance, env, meshArtifacts, meshConfig) => {
49
+ const { Secret: secret } = env;
50
+ const tenantMesh = await getBuiltMesh(meshArtifacts, meshConfig);
51
+ const meshSecrets = loadMeshSecrets(loggerInstance, secret);
52
+ return await buildYogaServer(env, tenantMesh, meshConfig, meshSecrets);
53
+ };
54
+
55
+ async function buildYogaServer(env, tenantMesh, meshConfig, meshSecrets) {
56
+ const secretsProxy = new Proxy(meshSecrets, getSecretsHandler);
57
+ return createYoga({
58
+ plugins: tenantMesh.plugins,
59
+ graphqlEndpoint: `/graphql`,
60
+ cors: getCorsOptions(env, meshConfig),
61
+ context: initialContext => ({
62
+ ...initialContext,
63
+ secrets: secretsProxy,
64
+ }),
65
+ maskedErrors: {
66
+ maskError: maskError,
67
+ },
68
+ logging: 'debug',
69
+ });
70
+ }
71
+
72
+ const maskError = error => {
73
+ if (error instanceof GraphQLError && error.extensions?.http?.headers) {
74
+ delete error.extensions.http.headers;
75
+ }
76
+
77
+ return error;
78
+ };
79
+
80
+ module.exports = { buildServer };