aws-classify-server 0.2.0 → 0.2.2
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/lib/cjs/Authentication.d.ts +0 -1
- package/lib/cjs/ClassDef.d.ts +1 -0
- package/lib/cjs/ClassifyServerless.d.ts +3 -1
- package/lib/cjs/ClassifyServerless.js +11 -5
- package/lib/cjs/dispatch.js +5 -2
- package/lib/esm/Authentication.d.ts +0 -1
- package/lib/esm/ClassDef.d.ts +1 -0
- package/lib/esm/ClassifyServerless.d.ts +3 -1
- package/lib/esm/ClassifyServerless.js +12 -6
- package/lib/esm/dispatch.js +5 -2
- package/package.json +2 -2
- package/yml/authentication-config.js +1 -3
- package/yml/functions-authenticated.yml +0 -4
- package/yml/provider-iam.yml +3 -1
- package/yml/resources-custom-website.yml +4 -1
- package/yml/resources-website-authenticated.yml +4 -1
- package/yml/resources-website.yml +4 -1
|
@@ -2,7 +2,6 @@ import type { APIGatewayProxyEvent, APIGatewayProxyEventV2 } from 'aws-lambda';
|
|
|
2
2
|
import type { AuthenticatedIdentity, RequestContext } from 'aws-classify-common';
|
|
3
3
|
export type HttpEvent = APIGatewayProxyEvent | APIGatewayProxyEventV2;
|
|
4
4
|
export interface ServerAuthenticationOptions {
|
|
5
|
-
publicSuffix: string;
|
|
6
5
|
connectionCredentialSeconds?: number;
|
|
7
6
|
/** Adapt ONLY the trusted requestContext.authorizer emitted by a configured Gateway authorizer. */
|
|
8
7
|
identityAdapter?: (authorizer: unknown) => AuthenticatedIdentity | undefined;
|
package/lib/cjs/ClassDef.d.ts
CHANGED
|
@@ -3,5 +3,6 @@ import { ClassifyResponse } from "./ClassifyResponse";
|
|
|
3
3
|
export interface ClassDef<ServerClass extends ClassifyResponse, ClientClass> {
|
|
4
4
|
serverClass: new () => ServerClass;
|
|
5
5
|
clientClass: new () => ClientClass;
|
|
6
|
+
publicMethods: ReadonlySet<string>;
|
|
6
7
|
authorizer: ((endPoint: ServerClass, method: string, args: IArguments, context?: RequestContext) => Promise<boolean>) | undefined;
|
|
7
8
|
}
|
|
@@ -19,7 +19,9 @@ export declare class ClassifyServerless {
|
|
|
19
19
|
setExpirationMinutes(minutes: number): void;
|
|
20
20
|
setLogger(log: (_message: string) => void): void;
|
|
21
21
|
setLogLevel(logLevel: Partial<typeof EndPointsLogging>): void;
|
|
22
|
-
registerResponse<T>(responseClass: new () => T, authorizer?: (_endPoint: T, _method: string, _args: IArguments, _context?: RequestContext) => Promise<boolean
|
|
22
|
+
registerResponse<T>(responseClass: new () => T, authorizer?: (_endPoint: T, _method: string, _args: IArguments, _context?: RequestContext) => Promise<boolean>, options?: {
|
|
23
|
+
publicMethods?: readonly string[];
|
|
24
|
+
}): void;
|
|
23
25
|
dispatch(ev: HttpEvent, context: Context, classes?: {}, entry?: 'protected' | 'public'): Promise<string>;
|
|
24
26
|
private dispatchRequest;
|
|
25
27
|
registerRequest<T>(requestClass: new () => T, classes?: any): new () => T;
|
|
@@ -29,11 +29,10 @@ class ClassifyServerless {
|
|
|
29
29
|
this.expirationMinutes = 60 * 24;
|
|
30
30
|
this.log = msg => console.log(msg);
|
|
31
31
|
if (process.env.AWS_CLASSIFY_AUTHENTICATION === 'true')
|
|
32
|
-
this.configureAuthentication({
|
|
32
|
+
this.configureAuthentication({});
|
|
33
33
|
}
|
|
34
34
|
configureAuthentication(options) {
|
|
35
35
|
var _a;
|
|
36
|
-
(0, aws_classify_common_1.validatePublicSuffix)(options.publicSuffix);
|
|
37
36
|
const seconds = (_a = options.connectionCredentialSeconds) !== null && _a !== void 0 ? _a : 60;
|
|
38
37
|
if (!Number.isInteger(seconds) || seconds < 1 || seconds > 300)
|
|
39
38
|
throw new Error('connectionCredentialSeconds must be 1–300');
|
|
@@ -55,7 +54,7 @@ class ClassifyServerless {
|
|
|
55
54
|
setLogLevel(logLevel) {
|
|
56
55
|
this.logLevel = logLevel;
|
|
57
56
|
}
|
|
58
|
-
registerResponse(responseClass, authorizer) {
|
|
57
|
+
registerResponse(responseClass, authorizer, options) {
|
|
59
58
|
// Find ultimate base class
|
|
60
59
|
let clientClass = responseClass;
|
|
61
60
|
while (Object.getPrototypeOf(clientClass).prototype)
|
|
@@ -66,7 +65,14 @@ class ClassifyServerless {
|
|
|
66
65
|
const interfaceName = clientClass['interfaceName'];
|
|
67
66
|
if (!interfaceName)
|
|
68
67
|
throw ('ClassifyServerless.registerResponse: Request must have interfaceName as a static property');
|
|
69
|
-
|
|
68
|
+
const methods = Object.getOwnPropertyNames(clientClass.prototype).filter(method => method !== 'constructor' && typeof clientClass.prototype[method] === 'function');
|
|
69
|
+
const decorated = methods.filter(method => (0, aws_classify_common_1.isPublicEndpoint)(clientClass.prototype[method]));
|
|
70
|
+
if ((options === null || options === void 0 ? void 0 : options.publicMethods) !== undefined && decorated.length)
|
|
71
|
+
throw new Error('Use either @Public() or publicMethods, not both');
|
|
72
|
+
const publicMethods = (options === null || options === void 0 ? void 0 : options.publicMethods) === undefined ? decorated : [...options.publicMethods];
|
|
73
|
+
if (!publicMethods.every(method => typeof method === 'string' && methods.includes(method)) || new Set(publicMethods).size !== publicMethods.length)
|
|
74
|
+
throw new Error('publicMethods must be unique request-class method names');
|
|
75
|
+
this.classDefs.set(interfaceName, { serverClass: responseClass, clientClass, authorizer, publicMethods: new Set(publicMethods) });
|
|
70
76
|
responseClass.__interfaceName__ = interfaceName;
|
|
71
77
|
}
|
|
72
78
|
dispatch(ev, context, classes = {}, entry = 'protected') {
|
|
@@ -122,7 +128,7 @@ class ClassifyServerless {
|
|
|
122
128
|
throw new Error(`No Response defined for ${request.interfaceName}`);
|
|
123
129
|
if (request.methodName === 'constructor' || !Object.prototype.hasOwnProperty.call(classDef.clientClass.prototype, request.methodName) || typeof classDef.clientClass.prototype[request.methodName] !== 'function')
|
|
124
130
|
throw new Error(`${request.methodName} not found in ${request.interfaceName} client request class`);
|
|
125
|
-
if (this.authentication &&
|
|
131
|
+
if (this.authentication && classDef.publicMethods.has(request.methodName) !== (trusted.dispatch === 'public'))
|
|
126
132
|
throw new Error('Member is not permitted on this dispatch endpoint');
|
|
127
133
|
// If authorizer passed in make sure request is authorized
|
|
128
134
|
if (!classDef.authorizer || (yield (this.authentication ? classDef.authorizer(request.interfaceName, request.methodName, request.args, trusted) : classDef.authorizer(request.interfaceName, request.methodName, request.args)))) {
|
package/lib/cjs/dispatch.js
CHANGED
|
@@ -26,13 +26,16 @@ const httpHandler = (event, context, entry) => __awaiter(void 0, void 0, void 0,
|
|
|
26
26
|
}
|
|
27
27
|
catch (err) {
|
|
28
28
|
//console.log(`request.body = ${event.body}`);
|
|
29
|
-
|
|
29
|
+
// Do not reflect parse, database, or framework errors to an untrusted caller.
|
|
30
|
+
// Operators still receive the original error in Lambda's logs.
|
|
31
|
+
console.error('Dispatch failed', err);
|
|
30
32
|
return {
|
|
31
33
|
statusCode: 200,
|
|
32
34
|
headers: { 'Cache-Control': 'no-store' },
|
|
33
35
|
body: (0, js_freeze_dry_1.serialize)({
|
|
34
36
|
data: undefined,
|
|
35
|
-
|
|
37
|
+
// Keep the existing serialized-error / HTTP-200 contract for clients.
|
|
38
|
+
exception: 'Internal Server Error',
|
|
36
39
|
cargo: undefined,
|
|
37
40
|
sessionId: undefined
|
|
38
41
|
})
|
|
@@ -2,7 +2,6 @@ import type { APIGatewayProxyEvent, APIGatewayProxyEventV2 } from 'aws-lambda';
|
|
|
2
2
|
import type { AuthenticatedIdentity, RequestContext } from 'aws-classify-common';
|
|
3
3
|
export type HttpEvent = APIGatewayProxyEvent | APIGatewayProxyEventV2;
|
|
4
4
|
export interface ServerAuthenticationOptions {
|
|
5
|
-
publicSuffix: string;
|
|
6
5
|
connectionCredentialSeconds?: number;
|
|
7
6
|
/** Adapt ONLY the trusted requestContext.authorizer emitted by a configured Gateway authorizer. */
|
|
8
7
|
identityAdapter?: (authorizer: unknown) => AuthenticatedIdentity | undefined;
|
package/lib/esm/ClassDef.d.ts
CHANGED
|
@@ -3,5 +3,6 @@ import { ClassifyResponse } from "./ClassifyResponse";
|
|
|
3
3
|
export interface ClassDef<ServerClass extends ClassifyResponse, ClientClass> {
|
|
4
4
|
serverClass: new () => ServerClass;
|
|
5
5
|
clientClass: new () => ClientClass;
|
|
6
|
+
publicMethods: ReadonlySet<string>;
|
|
6
7
|
authorizer: ((endPoint: ServerClass, method: string, args: IArguments, context?: RequestContext) => Promise<boolean>) | undefined;
|
|
7
8
|
}
|
|
@@ -19,7 +19,9 @@ export declare class ClassifyServerless {
|
|
|
19
19
|
setExpirationMinutes(minutes: number): void;
|
|
20
20
|
setLogger(log: (_message: string) => void): void;
|
|
21
21
|
setLogLevel(logLevel: Partial<typeof EndPointsLogging>): void;
|
|
22
|
-
registerResponse<T>(responseClass: new () => T, authorizer?: (_endPoint: T, _method: string, _args: IArguments, _context?: RequestContext) => Promise<boolean
|
|
22
|
+
registerResponse<T>(responseClass: new () => T, authorizer?: (_endPoint: T, _method: string, _args: IArguments, _context?: RequestContext) => Promise<boolean>, options?: {
|
|
23
|
+
publicMethods?: readonly string[];
|
|
24
|
+
}): void;
|
|
23
25
|
dispatch(ev: HttpEvent, context: Context, classes?: {}, entry?: 'protected' | 'public'): Promise<string>;
|
|
24
26
|
private dispatchRequest;
|
|
25
27
|
registerRequest<T>(requestClass: new () => T, classes?: any): new () => T;
|
|
@@ -10,7 +10,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
10
10
|
import { deserialize, serialize } from "js-freeze-dry";
|
|
11
11
|
import { sessionStore as ddbDocClient } from "./SessionStore";
|
|
12
12
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
13
|
-
import {
|
|
13
|
+
import { isPublicEndpoint } from "aws-classify-common";
|
|
14
14
|
import { requestContext } from "./Authentication";
|
|
15
15
|
import { ownedSession, sessionOwner, issueConnectionCredential } from "./AuthenticatedSessions";
|
|
16
16
|
import { ApiGatewayManagementApiClient, PostToConnectionCommand } from "@aws-sdk/client-apigatewaymanagementapi";
|
|
@@ -26,11 +26,10 @@ export class ClassifyServerless {
|
|
|
26
26
|
this.expirationMinutes = 60 * 24;
|
|
27
27
|
this.log = msg => console.log(msg);
|
|
28
28
|
if (process.env.AWS_CLASSIFY_AUTHENTICATION === 'true')
|
|
29
|
-
this.configureAuthentication({
|
|
29
|
+
this.configureAuthentication({});
|
|
30
30
|
}
|
|
31
31
|
configureAuthentication(options) {
|
|
32
32
|
var _a;
|
|
33
|
-
validatePublicSuffix(options.publicSuffix);
|
|
34
33
|
const seconds = (_a = options.connectionCredentialSeconds) !== null && _a !== void 0 ? _a : 60;
|
|
35
34
|
if (!Number.isInteger(seconds) || seconds < 1 || seconds > 300)
|
|
36
35
|
throw new Error('connectionCredentialSeconds must be 1–300');
|
|
@@ -52,7 +51,7 @@ export class ClassifyServerless {
|
|
|
52
51
|
setLogLevel(logLevel) {
|
|
53
52
|
this.logLevel = logLevel;
|
|
54
53
|
}
|
|
55
|
-
registerResponse(responseClass, authorizer) {
|
|
54
|
+
registerResponse(responseClass, authorizer, options) {
|
|
56
55
|
// Find ultimate base class
|
|
57
56
|
let clientClass = responseClass;
|
|
58
57
|
while (Object.getPrototypeOf(clientClass).prototype)
|
|
@@ -63,7 +62,14 @@ export class ClassifyServerless {
|
|
|
63
62
|
const interfaceName = clientClass['interfaceName'];
|
|
64
63
|
if (!interfaceName)
|
|
65
64
|
throw ('ClassifyServerless.registerResponse: Request must have interfaceName as a static property');
|
|
66
|
-
|
|
65
|
+
const methods = Object.getOwnPropertyNames(clientClass.prototype).filter(method => method !== 'constructor' && typeof clientClass.prototype[method] === 'function');
|
|
66
|
+
const decorated = methods.filter(method => isPublicEndpoint(clientClass.prototype[method]));
|
|
67
|
+
if ((options === null || options === void 0 ? void 0 : options.publicMethods) !== undefined && decorated.length)
|
|
68
|
+
throw new Error('Use either @Public() or publicMethods, not both');
|
|
69
|
+
const publicMethods = (options === null || options === void 0 ? void 0 : options.publicMethods) === undefined ? decorated : [...options.publicMethods];
|
|
70
|
+
if (!publicMethods.every(method => typeof method === 'string' && methods.includes(method)) || new Set(publicMethods).size !== publicMethods.length)
|
|
71
|
+
throw new Error('publicMethods must be unique request-class method names');
|
|
72
|
+
this.classDefs.set(interfaceName, { serverClass: responseClass, clientClass, authorizer, publicMethods: new Set(publicMethods) });
|
|
67
73
|
responseClass.__interfaceName__ = interfaceName;
|
|
68
74
|
}
|
|
69
75
|
dispatch(ev, context, classes = {}, entry = 'protected') {
|
|
@@ -119,7 +125,7 @@ export class ClassifyServerless {
|
|
|
119
125
|
throw new Error(`No Response defined for ${request.interfaceName}`);
|
|
120
126
|
if (request.methodName === 'constructor' || !Object.prototype.hasOwnProperty.call(classDef.clientClass.prototype, request.methodName) || typeof classDef.clientClass.prototype[request.methodName] !== 'function')
|
|
121
127
|
throw new Error(`${request.methodName} not found in ${request.interfaceName} client request class`);
|
|
122
|
-
if (this.authentication &&
|
|
128
|
+
if (this.authentication && classDef.publicMethods.has(request.methodName) !== (trusted.dispatch === 'public'))
|
|
123
129
|
throw new Error('Member is not permitted on this dispatch endpoint');
|
|
124
130
|
// If authorizer passed in make sure request is authorized
|
|
125
131
|
if (!classDef.authorizer || (yield (this.authentication ? classDef.authorizer(request.interfaceName, request.methodName, request.args, trusted) : classDef.authorizer(request.interfaceName, request.methodName, request.args)))) {
|
package/lib/esm/dispatch.js
CHANGED
|
@@ -23,13 +23,16 @@ const httpHandler = (event, context, entry) => __awaiter(void 0, void 0, void 0,
|
|
|
23
23
|
}
|
|
24
24
|
catch (err) {
|
|
25
25
|
//console.log(`request.body = ${event.body}`);
|
|
26
|
-
|
|
26
|
+
// Do not reflect parse, database, or framework errors to an untrusted caller.
|
|
27
|
+
// Operators still receive the original error in Lambda's logs.
|
|
28
|
+
console.error('Dispatch failed', err);
|
|
27
29
|
return {
|
|
28
30
|
statusCode: 200,
|
|
29
31
|
headers: { 'Cache-Control': 'no-store' },
|
|
30
32
|
body: serialize({
|
|
31
33
|
data: undefined,
|
|
32
|
-
|
|
34
|
+
// Keep the existing serialized-error / HTTP-200 contract for clients.
|
|
35
|
+
exception: 'Internal Server Error',
|
|
33
36
|
cargo: undefined,
|
|
34
37
|
sessionId: undefined
|
|
35
38
|
})
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "aws-classify-server",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"main": "./lib/cjs/index.js",
|
|
5
5
|
"module": "./lib/esm/index.js",
|
|
6
6
|
"types": "./lib/esm/index.d.ts",
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
"@aws-sdk/client-apigatewaymanagementapi": "^3.1146.0",
|
|
35
35
|
"@aws-sdk/client-dynamodb": "^3.1146.0",
|
|
36
36
|
"@aws-sdk/lib-dynamodb": "^3.1146.0",
|
|
37
|
-
"aws-classify-common": "0.2.
|
|
37
|
+
"aws-classify-common": "0.2.2",
|
|
38
38
|
"js-freeze-dry": "^1.1.4"
|
|
39
39
|
},
|
|
40
40
|
"engines": {
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
// Serverless v3/v4 file resolver. Loaded only by the authenticated functions include.
|
|
2
|
-
const {validatePublicSuffix} = require('aws-classify-common');
|
|
3
2
|
module.exports = async ({resolveVariable}) => {
|
|
4
3
|
const config = await resolveVariable('self:custom.awsClassify, null');
|
|
5
4
|
if (!config || !config.authorizer || typeof config.authorizer.name !== 'string' || !config.authorizer.name.trim())
|
|
@@ -10,9 +9,8 @@ module.exports = async ({resolveVariable}) => {
|
|
|
10
9
|
const scopes = config.authorizer.scopes;
|
|
11
10
|
if (scopes !== undefined && (!Array.isArray(scopes) || !scopes.every(scope => typeof scope === 'string' && scope.length && !/\s/.test(scope))))
|
|
12
11
|
throw new Error('awsClassify authorizer scopes must be an array of nonempty scope names');
|
|
13
|
-
const publicSuffix = validatePublicSuffix(config.publicSuffix);
|
|
14
12
|
const logging = await resolveVariable('self:provider.logs.websocket, null');
|
|
15
13
|
if (logging && (logging === true || logging.fullExecutionData !== false))
|
|
16
14
|
throw new Error('Authenticated WebSocket logging requires fullExecutionData: false to protect connection credentials');
|
|
17
|
-
return {authorizer: config.authorizer
|
|
15
|
+
return {authorizer: config.authorizer};
|
|
18
16
|
};
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
responseHandler:
|
|
2
2
|
environment:
|
|
3
3
|
AWS_CLASSIFY_AUTHENTICATION: 'true'
|
|
4
|
-
AWS_CLASSIFY_PUBLIC_SUFFIX: ${file(${self:custom.yml}/authentication-config.js):publicSuffix}
|
|
5
4
|
handler: ${self:custom.directories.responseHandlers}.responseHandler
|
|
6
5
|
events:
|
|
7
6
|
- httpApi:
|
|
@@ -11,14 +10,12 @@ responseHandler:
|
|
|
11
10
|
connectHandler:
|
|
12
11
|
environment:
|
|
13
12
|
AWS_CLASSIFY_AUTHENTICATION: 'true'
|
|
14
|
-
AWS_CLASSIFY_PUBLIC_SUFFIX: ${file(${self:custom.yml}/authentication-config.js):publicSuffix}
|
|
15
13
|
handler: ${self:custom.directories.responseHandlers}.webSocketConnect
|
|
16
14
|
events:
|
|
17
15
|
- websocket: $connect
|
|
18
16
|
disconnectHandler:
|
|
19
17
|
environment:
|
|
20
18
|
AWS_CLASSIFY_AUTHENTICATION: 'true'
|
|
21
|
-
AWS_CLASSIFY_PUBLIC_SUFFIX: ${file(${self:custom.yml}/authentication-config.js):publicSuffix}
|
|
22
19
|
handler: ${self:custom.directories.responseHandlers}.webSocketDisconnect
|
|
23
20
|
events:
|
|
24
21
|
- websocket: $disconnect
|
|
@@ -26,7 +23,6 @@ publicResponseHandler:
|
|
|
26
23
|
name: ${self:service}-${sls:stage}-public
|
|
27
24
|
environment:
|
|
28
25
|
AWS_CLASSIFY_AUTHENTICATION: 'true'
|
|
29
|
-
AWS_CLASSIFY_PUBLIC_SUFFIX: ${file(${self:custom.yml}/authentication-config.js):publicSuffix}
|
|
30
26
|
handler: ${self:custom.directories.responseHandlers}.publicResponseHandler
|
|
31
27
|
events:
|
|
32
28
|
- httpApi:
|
package/yml/provider-iam.yml
CHANGED
|
@@ -84,9 +84,12 @@ Resources:
|
|
|
84
84
|
- CloudFrontDistribution
|
|
85
85
|
- DomainName
|
|
86
86
|
Outputs:
|
|
87
|
+
ContentBucket:
|
|
88
|
+
Description: Content bucket
|
|
89
|
+
Value: !Ref S3Bucket
|
|
87
90
|
CloudFrontId:
|
|
88
91
|
Description: CloudFront Id
|
|
89
92
|
Value: !Ref CloudFrontDistribution
|
|
90
93
|
WebsiteUrl:
|
|
91
94
|
Description: Cloudfront URL
|
|
92
|
-
Value: !GetAtt CloudFrontDistribution.DomainName
|
|
95
|
+
Value: !GetAtt CloudFrontDistribution.DomainName
|
|
@@ -61,9 +61,12 @@ Resources:
|
|
|
61
61
|
Principal:
|
|
62
62
|
CanonicalUser: !GetAtt CloudFrontOriginAccessIdentity.S3CanonicalUserId
|
|
63
63
|
Outputs:
|
|
64
|
+
ContentBucket:
|
|
65
|
+
Description: Content bucket
|
|
66
|
+
Value: !Ref S3Bucket
|
|
64
67
|
CloudFrontId:
|
|
65
68
|
Description: CloudFront Id
|
|
66
69
|
Value: !Ref CloudFrontDistribution
|
|
67
70
|
WebsiteUrl:
|
|
68
71
|
Description: Cloudfront URL
|
|
69
|
-
Value: !GetAtt CloudFrontDistribution.DomainName
|
|
72
|
+
Value: !GetAtt CloudFrontDistribution.DomainName
|
|
@@ -67,9 +67,12 @@ Resources:
|
|
|
67
67
|
Principal:
|
|
68
68
|
CanonicalUser: !GetAtt CloudFrontOriginAccessIdentity.S3CanonicalUserId
|
|
69
69
|
Outputs:
|
|
70
|
+
ContentBucket:
|
|
71
|
+
Description: Content bucket
|
|
72
|
+
Value: !Ref S3Bucket
|
|
70
73
|
CloudFrontId:
|
|
71
74
|
Description: CloudFront Id
|
|
72
75
|
Value: !Ref CloudFrontDistribution
|
|
73
76
|
WebsiteUrl:
|
|
74
77
|
Description: Cloudfront URL
|
|
75
|
-
Value: !GetAtt CloudFrontDistribution.DomainName
|
|
78
|
+
Value: !GetAtt CloudFrontDistribution.DomainName
|