@forge/api 2.18.3-next.0 → 2.18.3-next.1

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/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # @forge/api
2
2
 
3
+ ## 2.18.3-next.1
4
+
5
+ ### Patch Changes
6
+
7
+ - 88be9538: Use HTTP polyfill to call proxy
8
+
3
9
  ## 2.18.3-next.0
4
10
 
5
11
  ### Patch Changes
@@ -1,17 +1,13 @@
1
- import { RequestInfo, RequestInit, Response, Headers } from 'node-fetch';
2
- import { FetchAPI } from '..';
3
- type FetchFunction = (url: RequestInfo, options?: RequestInit) => Promise<Response>;
4
- type ProxyUrlProvider = 'app' | 'user' | 'none';
5
- type ProxyUrlRemote = 'jira' | 'confluence' | 'stargate' | 'bitbucket';
6
- type ProxyFetchArgs = {
7
- type: 'fpp';
8
- provider: ProxyUrlProvider;
9
- remote: ProxyUrlRemote;
10
- } | {
11
- type: 'egress';
1
+ import { RequestInit } from 'node-fetch';
2
+ import { FetchAPI, FetchMethod } from '..';
3
+ type AuthProvider = 'app' | 'user' | 'none';
4
+ type RemoteAPI = 'jira' | 'confluence' | 'stargate' | 'bitbucket';
5
+ type FetchArgs = {
6
+ provider: AuthProvider;
7
+ remote: RemoteAPI;
12
8
  };
13
- export declare const getRedirectUrl: (responseHeaders: Headers, original: string, isFromEgress: boolean) => string;
14
- export declare const createProxyFetch: (args: ProxyFetchArgs) => FetchFunction;
9
+ export declare function fetchProduct(args: FetchArgs): FetchMethod;
10
+ export declare const addMagicAgent: (init?: RequestInit) => RequestInit;
15
11
  export declare function getNodeRuntimeAPI(): FetchAPI;
16
12
  export declare function getSandboxRuntimeAPI(api: any): FetchAPI;
17
13
  export {};
@@ -1 +1 @@
1
- {"version":3,"file":"fetch.d.ts","sourceRoot":"","sources":["../../src/api/fetch.ts"],"names":[],"mappings":"AAAA,OAAc,EAAE,WAAW,EAAE,WAAW,EAAE,QAAQ,EAAW,OAAO,EAAE,MAAM,YAAY,CAAC;AAEzF,OAAO,EAAE,QAAQ,EAAE,MAAM,IAAI,CAAC;AAS9B,KAAK,aAAa,GAAG,CAAC,GAAG,EAAE,WAAW,EAAE,OAAO,CAAC,EAAE,WAAW,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAC;AACpF,KAAK,gBAAgB,GAAG,KAAK,GAAG,MAAM,GAAG,MAAM,CAAC;AAChD,KAAK,cAAc,GAAG,MAAM,GAAG,YAAY,GAAG,UAAU,GAAG,WAAW,CAAC;AA4BvE,KAAK,cAAc,GAAG;IAAE,IAAI,EAAE,KAAK,CAAC;IAAC,QAAQ,EAAE,gBAAgB,CAAC;IAAC,MAAM,EAAE,cAAc,CAAA;CAAE,GAAG;IAAE,IAAI,EAAE,QAAQ,CAAA;CAAE,CAAC;AAsE/G,eAAO,MAAM,cAAc,oBAAqB,OAAO,YAAY,MAAM,gBAAgB,OAAO,KAAG,MAQlG,CAAC;AAyEF,eAAO,MAAM,gBAAgB,SAAU,cAAc,KAAG,aAerD,CAAC;AAOJ,wBAAgB,iBAAiB,IAAI,QAAQ,CAoB5C;AAED,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,GAAG,GAAG,QAAQ,CAEvD"}
1
+ {"version":3,"file":"fetch.d.ts","sourceRoot":"","sources":["../../src/api/fetch.ts"],"names":[],"mappings":"AAEA,OAAc,EAAE,WAAW,EAAY,MAAM,YAAY,CAAC;AAG1D,OAAO,EAAE,QAAQ,EAAE,WAAW,EAAE,MAAM,IAAI,CAAC;AAI3C,KAAK,YAAY,GAAG,KAAK,GAAG,MAAM,GAAG,MAAM,CAAC;AAC5C,KAAK,SAAS,GAAG,MAAM,GAAG,YAAY,GAAG,UAAU,GAAG,WAAW,CAAC;AAElE,KAAK,SAAS,GAAG;IAAE,QAAQ,EAAE,YAAY,CAAC;IAAC,MAAM,EAAE,SAAS,CAAA;CAAE,CAAC;AAS/D,wBAAgB,YAAY,CAAC,IAAI,EAAE,SAAS,GAAG,WAAW,CAkBzD;AAgCD,eAAO,MAAM,aAAa,EAAE,CAAC,IAAI,CAAC,EAAE,WAAW,KAAK,WAGlD,CAAC;AAOH,wBAAgB,iBAAiB,IAAI,QAAQ,CAoB5C;AAED,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,GAAG,GAAG,QAAQ,CAEvD"}
package/out/api/fetch.js CHANGED
@@ -1,171 +1,61 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.getSandboxRuntimeAPI = exports.getNodeRuntimeAPI = exports.createProxyFetch = exports.getRedirectUrl = void 0;
3
+ exports.getSandboxRuntimeAPI = exports.getNodeRuntimeAPI = exports.addMagicAgent = exports.fetchProduct = void 0;
4
4
  const tslib_1 = require("tslib");
5
- const node_fetch_1 = tslib_1.__importStar(require("node-fetch"));
5
+ const node_fetch_1 = tslib_1.__importDefault(require("node-fetch"));
6
6
  const _1 = require(".");
7
- const runtime_1 = require("./runtime");
8
7
  const polyfill_response_1 = require("./polyfill-response");
9
8
  const errors_1 = require("./errors");
10
- const perf_hooks_1 = require("perf_hooks");
11
- const FORGE_PROXY_UPSTREAM_LATENCY_HEADER = 'forge-proxy-upstream-latency';
9
+ function fetchProduct(args) {
10
+ return async (path, init) => {
11
+ const url = productURL(args.remote, path);
12
+ init = (0, exports.addMagicAgent)(init);
13
+ init.headers = Object.assign(Object.assign({}, init.headers), { authorization: `Forge ${args.provider}` });
14
+ const response = await (0, node_fetch_1.default)(url, init);
15
+ handleProxyResponseErrors(response);
16
+ return response;
17
+ };
18
+ }
19
+ exports.fetchProduct = fetchProduct;
12
20
  const ATLASSIAN_TOKEN_SERVICE_KEY = 'atlassian-token-service-key';
13
- const providerToMetric = {
14
- app: 'asApp',
15
- user: 'asUser'
16
- };
17
- const remoteToMetric = {
18
- jira: 'requestJira',
19
- confluence: 'requestConfluence',
20
- bitbucket: 'requestBitbucket',
21
- stargate: 'requestAtlassian'
22
- };
23
- const metricName = (args) => {
24
- switch (args.type) {
25
- case 'egress':
26
- return 'api.fetch';
27
- case 'fpp':
28
- if (args.provider === 'none') {
29
- return `api.${remoteToMetric[args.remote]}`;
30
- }
31
- return `api.${providerToMetric[args.provider]}.${remoteToMetric[args.remote]}`;
32
- }
33
- };
34
- const createProxyRequest = ({ url: proxyUrl, token, host }, args, originalRequest) => {
35
- let proxyEndpoint;
36
- switch (args.type) {
37
- case 'egress':
38
- proxyEndpoint = `${proxyUrl}/egress`;
39
- break;
40
- case 'fpp':
41
- proxyEndpoint = `${proxyUrl}/fpp/provider/${args.provider}/remote/${args.remote}`;
42
- break;
43
- }
44
- const tempRequest = new node_fetch_1.Request(proxyEndpoint, originalRequest);
45
- const proxyRequest = new node_fetch_1.Request(tempRequest, { redirect: 'manual' });
46
- proxyRequest.headers.set('Forge-Proxy-Target', originalRequest.url);
47
- proxyRequest.headers.set('Forge-Proxy-Authorization', `Bearer ${token}`);
48
- if (host) {
49
- proxyRequest.headers.set('Host', host);
50
- }
51
- return proxyRequest;
52
- };
53
- const wrapWithOverheadMetric = async (fetch, metrics) => {
54
- const requestStart = perf_hooks_1.performance.now();
55
- const response = await fetch;
56
- const requestEnd = perf_hooks_1.performance.now();
57
- const proxyUpstreamLatency = parseInt(response.headers.get(FORGE_PROXY_UPSTREAM_LATENCY_HEADER) || '');
58
- if (proxyUpstreamLatency) {
59
- metrics.timing('proxy-success-overhead').set(requestEnd - requestStart - proxyUpstreamLatency);
60
- }
61
- return response;
62
- };
63
- const getRedirectArgs = (args, response) => {
64
- const proxyRelativeLocation = response.headers.get('forge-proxy-relative-location');
65
- if (!proxyRelativeLocation) {
66
- return { type: 'egress' };
67
- }
68
- return args;
69
- };
70
- const buildGetRequest = (url, originalRequest) => {
71
- const request = new node_fetch_1.Request(url, { method: 'GET' });
72
- for (const [name, values] of Array.from(originalRequest.headers.entries())) {
73
- if (!name.toLowerCase().startsWith('content-')) {
74
- request.headers.set(name, values);
75
- }
76
- }
77
- return request;
78
- };
79
- const getLocation = (responseHeaders) => {
80
- const location = responseHeaders.get('location');
81
- if (!location) {
82
- throw new errors_1.FetchError('Redirect location is empty');
83
- }
84
- return location;
85
- };
86
- const getRedirectUrl = (responseHeaders, original, isFromEgress) => {
87
- if (!isFromEgress) {
88
- return responseHeaders.get('forge-proxy-relative-location') || getLocation(responseHeaders);
89
- }
90
- return new URL(getLocation(responseHeaders), original).toString();
91
- };
92
- exports.getRedirectUrl = getRedirectUrl;
93
- const buildRedirectedRequest = (response, originalRequest, isFromEgress) => {
94
- const url = (0, exports.getRedirectUrl)(response.headers, originalRequest.url, isFromEgress);
95
- if (response.status === 303) {
96
- return buildGetRequest(url, originalRequest);
97
- }
98
- return new node_fetch_1.Request(url, originalRequest);
99
- };
100
- const fetchViaProxy = async ({ proxy, proxyFetchArgs, request, metrics, count = 0 }) => {
101
- const REDIRECT_STATUS = [300, 301, 302, 303, 307, 308];
102
- const MAX_REDIRECTS = 20;
103
- if (count >= MAX_REDIRECTS) {
104
- throw new errors_1.FetchError('Max redirects exceeded');
105
- }
106
- const proxyRequest = createProxyRequest(proxy, proxyFetchArgs, request);
107
- if (request.redirect === 'manual' && count === 0) {
108
- return wrapWithOverheadMetric((0, node_fetch_1.default)(proxyRequest), metrics);
109
- }
110
- const response = await wrapWithOverheadMetric((0, node_fetch_1.default)(proxyRequest), metrics);
111
- if (REDIRECT_STATUS.includes(response.status)) {
112
- const redirectedRequest = buildRedirectedRequest(response, request, proxyFetchArgs.type === 'egress');
113
- const redirectedArgs = getRedirectArgs(proxyFetchArgs, response);
114
- return fetchViaProxy({
115
- proxy,
116
- proxyFetchArgs: redirectedArgs,
117
- request: redirectedRequest,
118
- metrics,
119
- count: count + 1
120
- });
121
- }
122
- return response;
123
- };
124
- const handleProxyResponseErrors = (response, requestUrl) => {
21
+ const handleProxyResponseErrors = (response) => {
125
22
  if (response.headers.has('forge-proxy-error')) {
126
23
  const errorReason = response.headers.get('forge-proxy-error');
127
24
  if (errorReason === 'NEEDS_AUTHENTICATION_ERR') {
128
25
  throw new errors_1.NeedsAuthenticationError('Authentication Required', ATLASSIAN_TOKEN_SERVICE_KEY);
129
26
  }
130
- if (errorReason === 'BLOCKED_EGRESS') {
131
- throw new errors_1.ExternalEndpointNotAllowedError(requestUrl);
132
- }
133
27
  throw new errors_1.ProxyRequestError(response.status, response.headers.get('forge-proxy-error'));
134
28
  }
135
29
  };
136
- const createProxyFetch = (args) => (0, runtime_1.wrapInMetrics)(metricName(args), async (url, options) => {
137
- const { proxy, metrics } = (0, runtime_1.getRuntime)();
138
- const response = await fetchViaProxy({
139
- proxy,
140
- proxyFetchArgs: args,
141
- request: new node_fetch_1.Request(url, options),
142
- metrics
143
- });
144
- handleProxyResponseErrors(response, url.toString());
145
- return response;
146
- }, { tags: { proxy: 'true' } });
147
- exports.createProxyFetch = createProxyFetch;
30
+ function productURL(remote, path) {
31
+ if (!path.startsWith('/')) {
32
+ path = '/' + path;
33
+ }
34
+ return `https://${remote}${path}`;
35
+ }
36
+ const addMagicAgent = (init) => (Object.assign(Object.assign({}, init), { agent: 'FORGE_PRODUCT_REQUEST' }));
37
+ exports.addMagicAgent = addMagicAgent;
148
38
  const throwNotImplementedError = () => {
149
39
  throw new Error('not implemented');
150
40
  };
151
41
  function getNodeRuntimeAPI() {
152
42
  return {
153
- fetch: (0, _1.wrapWithRouteUnwrapper)((0, exports.createProxyFetch)({ type: 'egress' })),
154
- requestJira: (0, _1.wrapRequestProduct)((0, exports.createProxyFetch)({ type: 'fpp', provider: 'none', remote: 'jira' })),
155
- requestConfluence: (0, _1.wrapRequestProduct)((0, exports.createProxyFetch)({ type: 'fpp', provider: 'none', remote: 'confluence' })),
156
- requestBitbucket: (0, _1.wrapRequestProduct)((0, exports.createProxyFetch)({ type: 'fpp', provider: 'none', remote: 'bitbucket' })),
43
+ fetch: (0, _1.wrapWithRouteUnwrapper)(node_fetch_1.default),
44
+ requestJira: (0, _1.wrapRequestProduct)(fetchProduct({ provider: 'none', remote: 'jira' })),
45
+ requestConfluence: (0, _1.wrapRequestProduct)(fetchProduct({ provider: 'none', remote: 'confluence' })),
46
+ requestBitbucket: (0, _1.wrapRequestProduct)(fetchProduct({ provider: 'none', remote: 'bitbucket' })),
157
47
  asUser: () => ({
158
- requestJira: (0, _1.wrapRequestProduct)((0, exports.createProxyFetch)({ type: 'fpp', provider: 'user', remote: 'jira' })),
159
- requestConfluence: (0, _1.wrapRequestProduct)((0, exports.createProxyFetch)({ type: 'fpp', provider: 'user', remote: 'confluence' })),
160
- requestBitbucket: (0, _1.wrapRequestProduct)((0, exports.createProxyFetch)({ type: 'fpp', provider: 'user', remote: 'bitbucket' })),
161
- requestGraph: (0, _1.wrapRequestGraph)((0, exports.createProxyFetch)({ type: 'fpp', provider: 'user', remote: 'stargate' })),
48
+ requestJira: (0, _1.wrapRequestProduct)(fetchProduct({ provider: 'user', remote: 'jira' })),
49
+ requestConfluence: (0, _1.wrapRequestProduct)(fetchProduct({ provider: 'user', remote: 'confluence' })),
50
+ requestBitbucket: (0, _1.wrapRequestProduct)(fetchProduct({ provider: 'user', remote: 'bitbucket' })),
51
+ requestGraph: (0, _1.wrapRequestGraph)(fetchProduct({ provider: 'user', remote: 'stargate' })),
162
52
  withProvider: throwNotImplementedError
163
53
  }),
164
54
  asApp: () => ({
165
- requestJira: (0, _1.wrapRequestProduct)((0, exports.createProxyFetch)({ type: 'fpp', provider: 'app', remote: 'jira' })),
166
- requestConfluence: (0, _1.wrapRequestProduct)((0, exports.createProxyFetch)({ type: 'fpp', provider: 'app', remote: 'confluence' })),
167
- requestBitbucket: (0, _1.wrapRequestProduct)((0, exports.createProxyFetch)({ type: 'fpp', provider: 'app', remote: 'bitbucket' })),
168
- requestGraph: (0, _1.wrapRequestGraph)((0, exports.createProxyFetch)({ type: 'fpp', provider: 'app', remote: 'stargate' }))
55
+ requestJira: (0, _1.wrapRequestProduct)(fetchProduct({ provider: 'app', remote: 'jira' })),
56
+ requestConfluence: (0, _1.wrapRequestProduct)(fetchProduct({ provider: 'app', remote: 'confluence' })),
57
+ requestBitbucket: (0, _1.wrapRequestProduct)(fetchProduct({ provider: 'app', remote: 'bitbucket' })),
58
+ requestGraph: (0, _1.wrapRequestGraph)(fetchProduct({ provider: 'app', remote: 'stargate' }))
169
59
  })
170
60
  };
171
61
  }
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/api/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AACtD,OAAO,EACL,QAAQ,EAER,WAAW,EAEX,oBAAoB,EACpB,wBAAwB,EAGxB,WAAW,EACZ,MAAM,IAAI,CAAC;AAIZ,MAAM,MAAM,YAAY,GAAG,CAAC,QAAQ,EAAE,QAAQ,KAAK,CAAC,GAAG,EAAE,WAAW,EAAE,IAAI,CAAC,EAAE,WAAW,KAAK,OAAO,CAAC,WAAW,CAAC,CAAC;AAElH,eAAO,MAAM,gBAAgB,oBAAqB,WAAW,aAAa,MAAM,cAAc,GAAG,uCAQ7F,CAAC;AAEL,eAAO,MAAM,kBAAkB,EAAE,CAAC,CAAC,EAAE,WAAW,KAAK,oBAGpD,CAAC;AAEF,eAAO,MAAM,sBAAsB,EAAE,CAAC,CAAC,EAAE,WAAW,KAAK,wBAGxD,CAAC;AAEF,eAAO,MAAM,mBAAmB,QAAS,GAAG,aAAa,YAAY,KAAG,QAkCvE,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/api/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AACtD,OAAO,EACL,QAAQ,EAER,WAAW,EAEX,oBAAoB,EACpB,wBAAwB,EAGxB,WAAW,EACZ,MAAM,IAAI,CAAC;AAIZ,MAAM,MAAM,YAAY,GAAG,CAAC,QAAQ,EAAE,QAAQ,KAAK,CAAC,GAAG,EAAE,WAAW,EAAE,IAAI,CAAC,EAAE,WAAW,KAAK,OAAO,CAAC,WAAW,CAAC,CAAC;AAElH,eAAO,MAAM,gBAAgB,oBACT,WAAW,aACrB,MAAM,cAAc,GAAG,uCAQ3B,CAAC;AAEP,eAAO,MAAM,kBAAkB,EAAE,CAAC,CAAC,EAAE,WAAW,KAAK,oBAGpD,CAAC;AAEF,eAAO,MAAM,sBAAsB,EAAE,CAAC,CAAC,EAAE,WAAW,KAAK,wBAGxD,CAAC;AAEF,eAAO,MAAM,mBAAmB,QAAS,GAAG,aAAa,YAAY,KAAG,QAkCvE,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"polyfill-response.d.ts","sourceRoot":"","sources":["../../src/api/polyfill-response.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,YAAY,EAAE,MAAM,GAAG,CAAC;AAGjC,eAAO,MAAM,iBAAiB,EAAE,YAM/B,CAAC"}
1
+ {"version":3,"file":"polyfill-response.d.ts","sourceRoot":"","sources":["../../src/api/polyfill-response.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,YAAY,EAAE,MAAM,GAAG,CAAC;AAGjC,eAAO,MAAM,iBAAiB,EAAE,YAQ7B,CAAC"}
@@ -1,5 +1,5 @@
1
- import { createProxyFetch } from '../api/fetch';
2
- export declare const createRequestStargateAsApp: () => ReturnType<typeof createProxyFetch>;
1
+ import { FetchMethod } from '..';
2
+ export declare const createRequestStargateAsApp: () => FetchMethod;
3
3
  export declare const getContextAri: () => any;
4
4
  export declare const getFetchAPI: () => import("..").FetchAPI;
5
5
  //# sourceMappingURL=fetch-and-storage.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"fetch-and-storage.d.ts","sourceRoot":"","sources":["../../src/runtime/fetch-and-storage.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAA2C,MAAM,cAAc,CAAC;AAGzF,eAAO,MAAM,0BAA0B,EAAE,MAAM,UAAU,CAAC,OAAO,gBAAgB,CAET,CAAC;AAEzE,eAAO,MAAM,aAAa,WAQzB,CAAC;AAEF,eAAO,MAAM,WAAW,6BAOvB,CAAC"}
1
+ {"version":3,"file":"fetch-and-storage.d.ts","sourceRoot":"","sources":["../../src/runtime/fetch-and-storage.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,IAAI,CAAC;AAIjC,eAAO,MAAM,0BAA0B,EAAE,MAAM,WAC2D,CAAC;AAE3G,eAAO,MAAM,aAAa,WAQzB,CAAC;AAEF,eAAO,MAAM,WAAW,6BAOvB,CAAC"}
@@ -3,10 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.getFetchAPI = exports.getContextAri = exports.createRequestStargateAsApp = void 0;
4
4
  const fetch_1 = require("../api/fetch");
5
5
  const runtime_1 = require("../api/runtime");
6
- const createRequestStargateAsApp = () => {
7
- var _a, _b;
8
- return (_b = (_a = global.api) === null || _a === void 0 ? void 0 : _a.asApp().__requestAtlassian) !== null && _b !== void 0 ? _b : (0, fetch_1.createProxyFetch)({ type: 'fpp', provider: 'app', remote: 'stargate' });
9
- };
6
+ const createRequestStargateAsApp = () => { var _a, _b; return (_b = (_a = global.api) === null || _a === void 0 ? void 0 : _a.asApp().__requestAtlassian) !== null && _b !== void 0 ? _b : (0, fetch_1.fetchProduct)({ provider: 'app', remote: 'stargate' }); };
10
7
  exports.createRequestStargateAsApp = createRequestStargateAsApp;
11
8
  const getContextAri = () => {
12
9
  var _a;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@forge/api",
3
- "version": "2.18.3-next.0",
3
+ "version": "2.18.3-next.1",
4
4
  "description": "Forge API methods",
5
5
  "author": "Atlassian",
6
6
  "license": "UNLICENSED",
@@ -12,7 +12,7 @@
12
12
  "compile": "tsc -b -v"
13
13
  },
14
14
  "devDependencies": {
15
- "@forge/runtime": "4.4.5-next.0",
15
+ "@forge/runtime": "4.4.5-next.1",
16
16
  "@types/node": "14.18.53",
17
17
  "jest-matcher-specific-error": "^1.0.0",
18
18
  "nock": "^10.0.6"