UNPKG

lemon-core

Version:
666 lines 37.9 kB
"use strict"; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.MyHttpHeaderTool = exports.LambdaWEBHandler = exports.mxNextFailure = exports.mxNextHandler = exports.promised = exports.redirect = exports.failure = exports.notfound = exports.success = exports.buildResponse = void 0; /** * `lambda-web-handler.ts` * - lambda handler to process WEB(API) event. * - replace the legacy web-builder `WEB.ts` * * * ```js * const a = ''; * ``` * * @author Steve Jung <steve@lemoncloud.io> * @date 2019-11-20 initial version via backbone * @date 2022-04-07 use env, and opt header-parser * @date 2025-05-09 improved to support `referer` and `origin` header. * * @copyright (C) 2019 LemonCloud Co Ltd. - All Rights Reserved. */ // eslint-disable-next-line @typescript-eslint/no-unused-vars const engine_1 = require("../../engine/"); const lambda_handler_1 = require("./lambda-handler"); const tools_1 = require("../../tools/"); const test_helper_1 = require("../../common/test-helper"); const protocol_service_1 = require("../protocol/protocol-service"); const aws_kms_service_1 = require("../aws/aws-kms-service"); const protocol_1 = __importDefault(require("../protocol/")); const NS = engine_1.$U.NS('HWEB', 'yellow'); // NAMESPACE TO BE PRINTED. //* header definitions by environment. const HEADER_LEMON_LANGUAGE = engine_1.$U.env('HEADER_LEMON_LANGUAGE', 'x-lemon-language'); const HEADER_LEMON_IDENTITY = engine_1.$U.env('HEADER_LEMON_IDENTITY', 'x-lemon-identity'); const HEADER_COOKIE = engine_1.$U.env('HEADER_COOKIE', 'cookie'); const DEFAULT_FAVICON_ICO = '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'; const FAVICON_ICO = engine_1.$U.env('FAVICON_ICO', DEFAULT_FAVICON_ICO); /** * build http response body * - if body is string type, then content-type would be text/<some>. * - or default type is json * * @param statusCode http response code like 200 * @param body string or object * @param contentType (optional) the target content-type * @param origin (optional) the allow origin (default *) * @returns http response body */ const buildResponse = (statusCode, body, options) => { const contentType = options === null || options === void 0 ? void 0 : options.contentType; const origin = (options === null || options === void 0 ? void 0 : options.origin) === undefined ? '*' : options === null || options === void 0 ? void 0 : options.origin; const credentials = (options === null || options === void 0 ? void 0 : options.credentials) === undefined ? true : options === null || options === void 0 ? void 0 : options.credentials; const isBase64Encoded = contentType && !contentType.startsWith('text/') ? true : false; const _isHtml = (body) => body.startsWith('<!DOCTYPE html>') || (body.startsWith('<') && body.endsWith('>')); const _type = () => { if (contentType) return contentType; return typeof body === 'string' ? _isHtml(body) ? 'text/html; charset=utf-8' : 'text/plain; charset=utf-8' : 'application/json; charset=utf-8'; }; const headers = ['origin', HEADER_LEMON_LANGUAGE, HEADER_LEMON_IDENTITY].filter(s => !!s).join(', '); return { statusCode, headers: (0, tools_1.onlyDefined)({ 'Content-Type': _type(), // Required for CORS support to work 'Access-Control-Allow-Origin': origin === null ? undefined : `${origin || '*'}`, // Required for cookies, authorization headers with HTTPS 'Access-Control-Allow-Credentials': credentials === null ? undefined : credentials, // Required for CORS support as allowed headers. 'Access-Control-Allow-Headers': origin === null ? undefined : headers, }), body: typeof body === 'string' ? body : JSON.stringify(body), isBase64Encoded, }; }; exports.buildResponse = buildResponse; const success = (body, contentType, origin) => { return (0, exports.buildResponse)(200, body, { contentType, origin }); }; exports.success = success; const notfound = (body) => { return (0, exports.buildResponse)(404, body); }; exports.notfound = notfound; const failure = (body, status) => { return (0, exports.buildResponse)(status || 503, body); }; exports.failure = failure; const redirect = (location, status) => { const res = (0, exports.buildResponse)(status || 302, ''); res.headers['Location'] = location; // set location. return res; }; exports.redirect = redirect; /** * start proxy-chain by event & context. * @param event event * @param $ctx context */ const promised = (event, $ctx) => __awaiter(void 0, void 0, void 0, function* () { // TO SERVE BINARY. `$ npm i -S serverless-apigw-binary serverless-apigwy-binary`. refer 'https://read.acloud.guru/serverless-image-optimization-and-delivery-510b6c311fe5' if (event && event.httpMethod == 'GET' && event.path == '/favicon.ico') { return () => (0, exports.success)(FAVICON_ICO, 'image/x-icon'); } //* transform to protocol-context. const param = protocol_1.default.service.asTransformer('web').transformToParam(event, $ctx); (0, engine_1._log)(NS, '! protocol-param =', engine_1.$U.json(Object.assign(Object.assign({}, param), { body: undefined }))); // hide `.body` in log. //* returns object.. return { event, param, $ctx }; }); exports.promised = promised; /** * builder for default handler */ const mxNextHandler = (thiz) => (params) => __awaiter(void 0, void 0, void 0, function* () { //* determine if param or func. const fx = typeof params == 'function' ? params : null; const $param = params && typeof params == 'object' ? params : null; const { param, event } = $param || {}; //* call the main handler() const R = $param ? yield thiz.handleProtocol(param, event) : fx; //* - if like to override the full response, then return function. if (R && typeof R == 'function') return R(); //* - override `Access-Control-Allow-Origin` to the current origin due to ajax credentials. const { httpMethod: method, headers } = event || {}; if (method && method != 'GET') { const origin = `${(headers && headers['origin']) || ''}`; return (0, exports.success)(R, null, origin); } //* returns response.. return (0, exports.success)(R); }); exports.mxNextHandler = mxNextHandler; /** * builder for failure promised. */ const mxNextFailure = (event, $ctx) => (e) => { (0, engine_1._err)(NS, `! err =`, e instanceof Error ? e : engine_1.$U.json(e)); const message = `${e.message || e.reason || engine_1.$U.json(e)}`; if (message.startsWith('404 NOT FOUND')) return (0, exports.notfound)(message); (0, engine_1._err)(NS, `! err.msg =`, message); //* common format of error. if (typeof message == 'string' && /^[1-9][0-9]{2} [A-Z ]+/.test(message)) { const status = engine_1.$U.N(message.substring(0, 3), 0); //* handle for 302/301 redirect. format: 303 REDIRECT - http://~~~ if ((status == 301 || status == 302) && message.indexOf(' - ') > 0) { const loc = message.substring(message.indexOf(' - ') + 3).trim(); if (loc) return (0, exports.redirect)(loc, status); } //* handle for `400 SIGNATURE - fail to verify!`. ignore report-error. if (status == 400 && message.startsWith('400 SIGNATURE')) { return (0, exports.failure)(message, status); } //* report error and returns if (lambda_handler_1.LambdaHandler.REPORT_ERROR) return (0, engine_1.doReportError)(e, $ctx, event) .catch(test_helper_1.GETERR) .then(() => (0, exports.failure)(message, status)); return (0, exports.failure)(message, status); } else if (typeof message == 'string' && /^\.[a-zA-Z0-9_\-]+/.test(message)) { //* handle for message `.name () is required!` if (lambda_handler_1.LambdaHandler.REPORT_ERROR) return (0, engine_1.doReportError)(e, $ctx, event) .catch(test_helper_1.GETERR) .then(() => (0, exports.failure)(message, 400)); return (0, exports.failure)(message, 400); } else if (typeof message == 'string' && /^\@[a-zA-Z0-9_\-]+/.test(message)) { //* handle for message `@name () is required!` if (lambda_handler_1.LambdaHandler.REPORT_ERROR) return (0, engine_1.doReportError)(e, $ctx, event) .catch(test_helper_1.GETERR) .then(() => (0, exports.failure)(message, 400)); return (0, exports.failure)(message, 400); } //* report error and returns if (lambda_handler_1.LambdaHandler.REPORT_ERROR) return (0, engine_1.doReportError)(e, $ctx, event) .catch(test_helper_1.GETERR) .then(() => (0, exports.failure)(e instanceof Error ? message : e)); return (0, exports.failure)(e instanceof Error ? message : e); }; exports.mxNextFailure = mxNextFailure; /** * class: LambdaWEBHandler * - default WEB Handler w/ event-listeners. */ class LambdaWEBHandler extends lambda_handler_1.LambdaSubHandler { /** * default constructor w/ registering self. */ constructor(lambda, register) { super(lambda, register ? 'web' : undefined); //* handlers map. this._handlers = {}; /** * Default WEB Handler. */ this.handle = (event, $ctx) => __awaiter(this, void 0, void 0, function* () { //* inspect API parameters. (0, engine_1._log)(NS, `handle()....`); const $path = event.pathParameters || {}; const $param = event.queryStringParameters || {}; (0, engine_1._log)(NS, '! path =', event.path); (0, engine_1._log)(NS, '! $path =', engine_1.$U.json($path)); (0, engine_1._log)(NS, '! $param =', engine_1.$U.json($param)); //* start promised.. return (0, exports.promised)(event, $ctx).then((0, exports.mxNextHandler)(this)).catch((0, exports.mxNextFailure)(event, $ctx)); }); /** * builder of tools for http-headers * - extracting header content, and parse. */ this.tools = (headers) => new MyHttpHeaderTool(headers); // _log(NS, `LambdaWEBHandler()..`); } /** * add web-handlers by `NextDecoder`. * * @param type type of WEB(API) * @param decoder next decorder */ setHandler(type, decoder) { if (typeof type !== 'string') throw new Error(`@type (string) is required!`); this._handlers[type] = decoder; } /** * check if there is handler for type. * @param type type of WEB(API) */ hasHandler(type) { return typeof this._handlers[type] != 'undefined'; } /** * registr web-controller. * @param controller the web-controller. */ addController(controller) { if (typeof controller !== 'object') throw new Error(`@controller (object) is required!`); const type = controller.type(); (0, engine_1._log)(NS, `> web-controller[${type}] =`, controller.hello()); this._handlers[type] = controller; } /** * get all decoders. */ getHandlerDecoders() { //* copy return Object.entries(this._handlers).reduce((M, [key, val]) => { if (typeof val == 'function') M[key] = val; else M[key] = (m, i, c) => val.decode(m, i, c); return M; }, {}); } /** * handle param via protocol-service. * * @param param protocol parameters * @param event (optional) origin event object. */ handleProtocol(param, event) { return __awaiter(this, void 0, void 0, function* () { if (!param) throw new Error(`@param (protocol-param) is required!`); const TYPE = `${param.type || ''}`; const MODE = `${param.mode || 'GET'}`; const ID = `${param.id || ''}`; const CMD = `${param.cmd || ''}`; const PATH = `${(event && event.path) || ''}`; const $param = param.param; const $body = param.body; const context = param.context; //* debug print body. if (!$body) { (0, engine_1._log)(NS, `#${MODE}:${CMD} (${TYPE}/${ID})....`); } else { (0, engine_1._log)(NS, `#${MODE}:${CMD} (${TYPE}/${ID}).... body.len=`, $body ? engine_1.$U.json($body).length : -1); } //* find target next function // const decoder: NextDecoder | CoreWEBController = this._handlers[TYPE]; const next = ((decoder) => { //* as default handler '/', say the current version. if (MODE === 'LIST' && TYPE === '' && ID === '' && CMD === '') { return () => __awaiter(this, void 0, void 0, function* () { const $pack = (0, tools_1.loadJsonSync)('package.json'); const name = ($pack && $pack.name) || 'LEMON API'; const version = ($pack && $pack.version) || '0.0.0'; const modules = [`${name}/${version}`]; //* shows version of `lemon-core` via `dependencies`. const coreVer = $pack && $pack.dependencies && $pack.dependencies['lemon-core']; if (coreVer) modules.push(`lemon-core/${coreVer.startsWith('^') ? coreVer.substring(1) : coreVer}`); return modules.join('\n'); }); } //* error if no decoder. if (!decoder) return null; //* use decoder() to find target. if (typeof decoder == 'function') return decoder(MODE, ID, CMD, PATH); else if (typeof decoder == 'object') { const func = decoder.decode(MODE, ID, CMD, PATH); if (!func) return null; // avoid 'null' error. const next = (i, p, b, c) => func.call(decoder, i, p, b, c); return next; } return null; })(this._handlers[TYPE]); //* if no next, then report error. if (!next || typeof next != 'function') { (0, engine_1._err)(NS, `! WARN ! MISSING NEXT-HANDLER. event=`, engine_1.$U.json(event)); throw new Error(`404 NOT FOUND - ${MODE} /${TYPE}/${ID}${CMD ? `/${CMD}` : ''}`); } //* call next.. (it will return result or promised) return (() => { try { const R = next(ID, $param, $body, context); return R instanceof Promise ? R : Promise.resolve(R); } catch (e) { return Promise.reject(e); } })(); }); } /** * pack the request context for Http request. * * @param event origin Event. * @param orgContext (optional) original lambda.Context */ packContext(event, orgContext) { return __awaiter(this, void 0, void 0, function* () { (0, engine_1._log)(NS, `packContext(${event ? '' : 'null'})..`); if (!event) return null; //* prepare chain object. const reqContext = event === null || event === void 0 ? void 0 : event.requestContext; orgContext && (0, engine_1._log)(NS, `> orgContext =`, engine_1.$U.S(orgContext, 256, 32)); reqContext && (0, engine_1._log)(NS, `> reqContext =`, engine_1.$U.S(reqContext, 256, 32)); // STEP.1 support lambda call JWT Token authentication. const headers = event.headers; if (headers && headers[protocol_service_1.HEADER_PROTOCOL_CONTEXT]) { //* if it is protocol request via lambda, then returns valid context. const $param = protocol_1.default.service.asTransformer('web').transformToParam(event); return $param === null || $param === void 0 ? void 0 : $param.context; } // STEP.2 use internal identity json data via python lambda call. const $tool = this.tools(headers); const identity = yield $tool.parseIdentityHeader(); const _prepare = () => { const cookie = $tool.parseCookiesHeader(); const domain = $tool.getHeader('host'); const referer = $tool.getHeader('referer'); const origin = $tool.getHeader('origin'); const userAgent = $tool.getHeader('user-agent'); const authorization = $tool.getHeader('authorization'); return { identity, cookie, domain, referer, origin, userAgent, authorization }; }; // STEP.3. prepare the final `next-context`. const $ctx = yield $tool.prepareContext(_prepare(), reqContext); $ctx.source = protocol_1.default.service.myProtocolURI($ctx); // self service-uri as source // FINIAL. returns return $ctx; }); } } exports.LambdaWEBHandler = LambdaWEBHandler; //* shared config. LambdaWEBHandler.REPORT_ERROR = lambda_handler_1.LambdaHandler.REPORT_ERROR; /** * class: `MyHttpHeaderTool` * - basic implementation of HttpHeaderTool */ class MyHttpHeaderTool { /** * default constructor. * @param headers */ constructor(headers, options) { var _a; /** expose `onlyDefined` */ this.onlyDefined = tools_1.onlyDefined; /** * check if this request is from externals (like API-GW) * @returns true if in external */ this.isExternal = () => { const host = this.getHeader('host'); const isExternal = host ? true : false; return !!isExternal; }; const isClone = (_a = options === null || options === void 0 ? void 0 : options.isClone) !== null && _a !== void 0 ? _a : true; this.headers = isClone ? Object.assign({}, headers) : headers; } hello() { return 'header-tool-by-default'; } /** * get values by name * @param name case-insentive name of field */ getHeaders(name) { return Object.entries(this.headers || {}).reduce((L, [key, val]) => { if (name === key || key.toLowerCase() === name) { if (Array.isArray(val)) { val.forEach(val => { if (typeof val === 'string') { L.push(val.trim()); } else { (0, engine_1._err)(NS, `! invalid type @header[${name}] =`, typeof val, val); } }); } else if (typeof val === 'string') { L.push(val.trim()); } else { (0, engine_1._err)(NS, `! invalid type @header[${name}] =`, typeof val, val); } } return L; }, []); } /** * get the last value in header by name */ getHeader(name) { const vals = this.getHeaders(name); return vals.length < 1 ? undefined : vals[vals.length - 1]; } /** * parse of header[`x-lemon-identity`] to get the instance of `NextIdentity` * - lambda 호출의 2가지 방법이 있음 (interval vs external) * - internal는 AWS 같은 계정내 호출로 labmda 직접 호출이 가능함. * - external는 API-GW를 통한 호출로 JWT 지원 (since 3.1.2). * * **[FOR INTERNAL CALL BY LAMBDA]** * - `x-lemon-identity` 정보로부터, 계정 정보를 얻음 (for direct call via lambda) * - 외부 호출과 구분하기 위해서 headr[host]가 비어 있어야함 (API-GW에서는 무조건 있으므로) * * **[FOR EXTERNAL CALL BY API-GW]** * - support ONLY JWT Token authentication (verification). * - iat */ parseIdentityHeader(name = HEADER_LEMON_IDENTITY) { var _a; return __awaiter(this, void 0, void 0, function* () { //* internal means `request from internal services` const isInternal = !this.isExternal(); const val = this.getHeader(name); let result = val ? { meta: val } : {}; try { if (!val) { //NOP } else if (isInternal && val.startsWith('{') && val.endsWith('}')) { result = yield this.parseIdentityJson(val); } else if (typeof val === 'string' && val.split('.').length === 3) { result = yield this.parseIdentityJWT(val); } } catch (e) { (0, engine_1._err)(NS, '!WARN! parse.err =', e); (0, engine_1._err)(NS, '!WARN! identity =', val); result.error = (0, test_helper_1.GETERR)(e); } //* overwrite finally language selection. const lang = (_a = this.parseLanguageHeader()) !== null && _a !== void 0 ? _a : result === null || result === void 0 ? void 0 : result.lang; return Object.assign(Object.assign({}, result), { lang }); }); } /** * parse as identity from json encoded text. */ parseIdentityJson(val) { return __awaiter(this, void 0, void 0, function* () { if (typeof val !== 'string') throw new Error(`@val[${val}] is invalid - not string!`); //* (ONLY for internal) parse payload as `json` const data = JSON.parse(val); // if (typeof data?.ns !== 'string') throw new Error(`.ns[${data?.ns}] is required - IdentityHeader`); if (typeof (data === null || data === void 0 ? void 0 : data.sid) !== 'string' || !(data === null || data === void 0 ? void 0 : data.sid)) throw new Error(`.sid[${data === null || data === void 0 ? void 0 : data.sid}] is required - IdentityHeader`); return data; }); } /** * find(or make) the proper KMSService per key * @param keyId key of KMS * @returns service */ findKMSService(keyId) { // const aws = $engine.module('aws') as AWSModule; // return aws?.kms; const kms = new aws_kms_service_1.AWSKMSService(keyId); return kms; } /** * encode as JWT string. */ encodeIdentityJWT(identity, params) { var _a; return __awaiter(this, void 0, void 0, function* () { // STEP.0 validate paramters. if (!identity || typeof identity !== 'object') throw new Error(`@identity (object) is required - but ${typeof identity}`); // STEP.1 prepare payload data const current = (_a = params === null || params === void 0 ? void 0 : params.current) !== null && _a !== void 0 ? _a : engine_1.$U.current_time_ms(); const alias = params === null || params === void 0 ? void 0 : params.alias; const payload = Object.assign(Object.assign({}, identity), { iss: alias ? `kms/${alias}` : null, iat: Math.floor(current / 1000), exp: Math.floor(current / 1000) + 24 * 60 * 60 }); const base64url = (t) => (0, aws_kms_service_1.fromBase64)(Buffer.from(t).toString('base64')); const data = { header: base64url(JSON.stringify({ alg: 'RS256', typ: 'JWT', })), payload: base64url(JSON.stringify(payload)), }; // STEP.2 encode and calc signature. const message = [data.header, data.payload].join('.'); const $kms = alias ? this.findKMSService(`alias/${alias}`) : null; const signature = $kms ? yield $kms.sign(message, true) : ''; const token = [message, signature].join('.'); return { signature, message, token }; }); } /** * parse as jwt-token, and validate the signature. */ parseIdentityJWT(token, params) { var _a, _b; return __awaiter(this, void 0, void 0, function* () { const isVerify = (_a = params === null || params === void 0 ? void 0 : params.verify) !== null && _a !== void 0 ? _a : true; //* it must be JWT Token. verify signature, and load. if (typeof token !== 'string' || !token) throw new Error(`@token (string) is required - but ${typeof token}`); // STEP.1 decode jwt, and extract { iss, iat, exp } const current = (_b = params === null || params === void 0 ? void 0 : params.current) !== null && _b !== void 0 ? _b : engine_1.$U.current_time_ms(); const sections = token.split('.'); if (sections.length !== 3) throw new Error(`@token[${token}] is invalid format!`); const [header, payload, signature] = sections; const $jwt = engine_1.$U.jwt(); const data = $jwt.decode(token, { complete: false, json: true }); if (!data) throw new Error(`@token[${token}] is invalid - failed to decode!`); const { iss, iat, exp } = data; // STEP.1-1 validate parameters. if (typeof iss !== 'string' && iss !== null) throw new Error(`.iss (string) is required!`); if (typeof iat !== 'number' && iat !== null) throw new Error(`.iat (number) is required!`); if (typeof exp !== 'number' && exp !== null) throw new Error(`.exp (number) is required!`); // STEP.2 validate signature by KMS(iss).verify() //TODO - iss 에 인증제공자의 api 넣기 (ex: api/lemon-backend-dev?) const _alias = (iss, prefix = 'kms/') => iss.includes(',') ? iss.substring(prefix.length, iss.indexOf(',')) : iss.substring(prefix.length); if (!isVerify) { return data; } else if (typeof iss === 'string' && iss.startsWith('kms/')) { const alias = _alias(iss); const $kms = alias ? this.findKMSService(`alias/${alias}`) : null; const verified = $kms ? yield $kms.verify([header, payload].join('.'), signature) : false; if (!verified) throw new Error(`@signature[] is invalid - not be verified by iss:${iss}!`); if (!exp || exp * 1000 < current) throw new Error(`.exp[${engine_1.$U.ts(exp * 1000)}] is invalid - expired!`); return data; } //* or throw throw new Error(`@iss[${iss}] is invalid - unsupportable issuer!`); }); } /** * parse of header[HEADER_LEMON_LANGUAGE] to get language-type. */ parseLanguageHeader(name = HEADER_LEMON_LANGUAGE) { const val = this.getHeader(name); return typeof val === 'string' ? val.trim().toLowerCase() : undefined; } /** * parse of header[HEADER_LEMON_LANGUAGE] to get cookie-set. */ parseCookiesHeader(name = HEADER_COOKIE) { const cookie = this.getHeader(name); if (!cookie) return undefined; const parseCookies = (str) => { const rx = /([^;=\s]*)=([^;]*)/g; const obj = {}; for (let m; (m = rx.exec(str));) obj[m[1]] = decodeURIComponent(m[2]); return obj; }; return parseCookies(cookie); } /** * override with AWS request-context */ prepareContext($org, reqContext) { var _a, _b, _c, _d; return __awaiter(this, void 0, void 0, function* () { const errScope = `web.prepareContext(${(_a = $org === null || $org === void 0 ? void 0 : $org.requestId) !== null && _a !== void 0 ? _a : ''})`; // STEP.4 override w/ cognito authentication to NextIdentity. if (((_b = reqContext === null || reqContext === void 0 ? void 0 : reqContext.identity) === null || _b === void 0 ? void 0 : _b.cognitoIdentityPoolId) !== undefined) { const $idt = $org === null || $org === void 0 ? void 0 : $org.identity; if (!$idt) throw new Error(`.identity (NextIdentity) is required - ${errScope}`); const $req = reqContext.identity; (0, engine_1._inf)(NS, '! identity(req) :=', engine_1.$U.json($req)); $idt.identityProvider = $req.cognitoAuthenticationProvider; // provider string. $idt.identityPoolId = $req.cognitoIdentityPoolId; // identity-pool-id like 'ap-northeast-2:618ce9d2-3ad6-49df-b3b3-e248ea51425e' $idt.identityId = $req.cognitoIdentityId; // identity-id like 'ap-northeast-2:dbd95fb4-7423-48b8-8a04-56e5bc95e444' $idt.accountId = $req.accountId; // account-id should be same as context.accountId $idt.userAgent = $req.userAgent; // user-agent string. if (typeof $req.caller == 'string') $idt.caller = $req.caller; if (typeof $req.accessKey == 'string') $idt.accessKey = $req.accessKey; if (typeof $req.apiKey == 'string') $idt.apiKey = $req.apiKey; (0, engine_1._inf)(NS, '! identity(new) :=', engine_1.$U.json(Object.assign({}, $idt))); } // STEP.5 extract additional request infor from req-context. const clientIp = `${((_c = reqContext === null || reqContext === void 0 ? void 0 : reqContext.identity) === null || _c === void 0 ? void 0 : _c.sourceIp) || ($org === null || $org === void 0 ? void 0 : $org.clientIp) || ''}`; const userAgent = `${((_d = reqContext === null || reqContext === void 0 ? void 0 : reqContext.identity) === null || _d === void 0 ? void 0 : _d.userAgent) || ($org === null || $org === void 0 ? void 0 : $org.userAgent) || ''}`; const requestId = `${(reqContext === null || reqContext === void 0 ? void 0 : reqContext.requestId) || ($org === null || $org === void 0 ? void 0 : $org.requestId) || ''}`; const accountId = `${(reqContext === null || reqContext === void 0 ? void 0 : reqContext.accountId) || ($org === null || $org === void 0 ? void 0 : $org.accountId) || ''}`; const domain = `${(reqContext === null || reqContext === void 0 ? void 0 : reqContext.domainName) || ($org === null || $org === void 0 ? void 0 : $org.domain) || ''}`; //* chore avoid null of headers //* save into headers and returns. const context = Object.assign(Object.assign({}, $org), { userAgent, clientIp, requestId, accountId, domain }); return context; }); } } exports.MyHttpHeaderTool = MyHttpHeaderTool; //# sourceMappingURL=lambda-web-handler.js.map