UNPKG

lemon-core

Version:
661 lines 38.3 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.FunctionWEBHandler = 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 Ian Kim <ian@lemoncloud.io> * @date 2023-10-30 initial version * * @copyright (C) lemoncloud.io 2023 - All Rights Reserved. */ // eslint-disable-next-line @typescript-eslint/no-unused-vars const engine_1 = require("../../engine"); const functions_handler_1 = require("./functions-handler"); const shared_1 = require("../../tools/shared"); const test_helper_1 = require("../../common/test-helper"); const protocol_service_1 = require("../protocol/protocol-service"); const azure_keyvault_service_1 = require("../azure/azure-keyvault-service"); const protocol_1 = __importDefault(require("../protocol/")); const NS = engine_1.$U.NS('FWEB', '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, contentType, origin) => { const isBase64Encoded = contentType && !contentType.startsWith('text/') ? true : false; contentType = contentType || (typeof body === 'string' ? body.startsWith('<') && body.endsWith('>') ? 'text/html; charset=utf-8' : 'text/plain; charset=utf-8' : 'application/json; charset=utf-8'); return { statusCode, headers: { 'Content-Type': contentType, 'Access-Control-Allow-Origin': `${origin || '*'}`, 'Access-Control-Allow-Credentials': true, // eslint-disable-next-line prettier/prettier 'Access-Control-Allow-Headers': ['origin', HEADER_LEMON_LANGUAGE, HEADER_LEMON_IDENTITY].filter(s => !!s).join(', '), // custom 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 = (ctx, req) => __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 (ctx && ctx.req && ctx.req.method == 'GET') { // return () => success(FAVICON_ICO, 'image/x-icon'); // } const param = req.params; // const param: any = $protocol.service.asTransformer('web').transformToParam(ctx); // _log(NS, '! protocol-param =', $U.json({ ...param, body: undefined })); // hide `.body` in log. //! returns object.. return { ctx, param }; }); 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; //! Serverless if ($param.ctx.invocationId) { const param = $param.param; const ctx = $param.ctx; //! call the main handler() const R = $param ? yield thiz.handleProtocol(param, ctx) : 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 { method, headers } = $param.ctx.req || {}; if (method && method != 'GET') { const origin = `${(headers && headers['origin']) || ''}`; return (0, exports.success)(R, null, origin); } //! returns response.. return (0, exports.success)(R); } //! Local else { const param = $param.ctx.pathParameters; const ctx = $param.ctx; //! call the main handler() const R = $param ? yield thiz.handleProtocol(param, ctx) : 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 } = ctx || {}; 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 = (ctx, req) => (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 (functions_handler_1.FunctionHandler.REPORT_ERROR) return (0, engine_1.doReportError)(e, ctx, req) .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 (functions_handler_1.FunctionHandler.REPORT_ERROR) return (0, engine_1.doReportError)(e, ctx, req) .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 (functions_handler_1.FunctionHandler.REPORT_ERROR) return (0, engine_1.doReportError)(e, ctx, req) .catch(test_helper_1.GETERR) .then(() => (0, exports.failure)(message, 400)); return (0, exports.failure)(message, 400); } //! report error and returns if (functions_handler_1.FunctionHandler.REPORT_ERROR) return (0, engine_1.doReportError)(e, ctx, req) .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 FunctionWEBHandler extends functions_handler_1.FunctionSubHandler { /** * default constructor w/ registering self. */ constructor(functions, register) { super(functions, register ? 'web' : undefined); //! handlers map. this._handlers = {}; /** * Default WEB Handler. */ this.handle = (ctx, req) => __awaiter(this, void 0, void 0, function* () { //! inspect API parameters. (0, engine_1._log)(NS, `handle()....`); const isInvocationId = ctx.invocationId ? true : false; const url = isInvocationId ? req.url : ctx.path; const parsedUrl = isInvocationId ? new URL(url) : null; const path = isInvocationId ? parsedUrl.pathname : ctx.path; const $path = isInvocationId ? req.params || {} : ctx.pathParameters || {}; const $param = isInvocationId ? req.query || {} : ctx.queryStringParameters || {}; if (!($path === null || $path === void 0 ? void 0 : $path.id)) { ctx.httpMethod = 'LIST'; } (0, engine_1._log)(NS, '! path =', path); (0, engine_1._log)(NS, '! $path =', engine_1.$U.json($path)); (0, engine_1._log)(NS, '! $param =', engine_1.$U.json($param)); return (0, exports.promised)(ctx, req).then((0, exports.mxNextHandler)(this)).catch((0, exports.mxNextFailure)(ctx, req)); }); /** * builder of tools for http-headers * - extracting header content, and parse. */ this.tools = (headers) => new MyHttpHeaderTool(this, headers); // _log(NS, `FunctionWEBHandler()..`); } /** * 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, ctx) { var _a, _b; return __awaiter(this, void 0, void 0, function* () { if (!param) throw new Error(`@param (protocol-param) is required!`); if (!ctx) throw new Error(`@ctx (protocol-ctx) is required!`); const TYPE = (ctx === null || ctx === void 0 ? void 0 : ctx.invocationId) ? new URL(ctx.req.url).pathname.split('/')[2] || '' : (param === null || param === void 0 ? void 0 : param.type) || ''; const MODE = (ctx === null || ctx === void 0 ? void 0 : ctx.invocationId) ? ctx.httpMethod === 'LIST' && ctx.req.method === 'GET' ? 'LIST' : ctx.req.method : ctx.httpMethod; const ID = (ctx === null || ctx === void 0 ? void 0 : ctx.invocationId) ? ((_a = ctx.req.params) === null || _a === void 0 ? void 0 : _a.id) || '' : (param === null || param === void 0 ? void 0 : param.id) || ''; const CMD = (ctx === null || ctx === void 0 ? void 0 : ctx.invocationId) ? ((_b = ctx.req.params) === null || _b === void 0 ? void 0 : _b.cmd) || '' : (param === null || param === void 0 ? void 0 : param.cmd) || ''; const PATH = (ctx === null || ctx === void 0 ? void 0 : ctx.invocationId) ? new URL(ctx.req.url).pathname || '' : ctx.path || ''; const $param = (ctx === null || ctx === void 0 ? void 0 : ctx.invocationId) ? ctx.req.query || {} : ctx.queryStringParameters || {}; const $body = (ctx === null || ctx === void 0 ? void 0 : ctx.invocationId) ? ctx.req.body || {} : ctx.body || {}; //! 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, shared_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(ctx)); 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, ctx); 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 cookie = $tool.parseCookiesHeader(); // STEP.3. prepare the final `next-context`. const $ctx = yield $tool.prepareContext({ identity, cookie }, reqContext); $ctx.source = protocol_1.default.service.myProtocolURI($ctx); // self service-uri as source // FINIAL. returns return $ctx; }); } } exports.FunctionWEBHandler = FunctionWEBHandler; //! shared config. FunctionWEBHandler.REPORT_ERROR = functions_handler_1.FunctionHandler.REPORT_ERROR; /** * class: `MyHttpHeaderTool` * - basic implementation of HttpHeaderTool */ class MyHttpHeaderTool { /** * default constructor. * @param headers */ constructor(handler, headers) { /** * get values by name * @param name case-insentive name of field */ this.getHeaders = (name) => 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 */ this.getHeader = (name) => { const vals = this.getHeaders(name); return vals.length < 1 ? undefined : vals[vals.length - 1]; }; /** * 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; }; /** * 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 */ this.parseIdentityHeader = (name = HEADER_LEMON_IDENTITY) => __awaiter(this, void 0, void 0, function* () { var _a; //! 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. */ this.parseIdentityJson = (val) => __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; }); /** * encode as JWT string. */ this.encodeIdentityJWT = (identity, params) => __awaiter(this, void 0, void 0, function* () { var _b; // 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 = (_b = params === null || params === void 0 ? void 0 : params.current) !== null && _b !== void 0 ? _b : 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, azure_keyvault_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. */ this.parseIdentityJWT = (token, params) => __awaiter(this, void 0, void 0, function* () { var _c, _d; const isVerify = (_c = params === null || params === void 0 ? void 0 : params.verify) !== null && _c !== void 0 ? _c : 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 = (_d = params === null || params === void 0 ? void 0 : params.current) !== null && _d !== void 0 ? _d : 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. */ this.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. */ this.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 */ this.prepareContext = ($org, reqContext) => __awaiter(this, void 0, void 0, function* () { var _e, _f, _g, _h; // STEP.4 override w/ cognito authentication to NextIdentity. if (((_e = reqContext === null || reqContext === void 0 ? void 0 : reqContext.identity) === null || _e === void 0 ? void 0 : _e.cognitoIdentityPoolId) !== undefined) { const identity = $org.identity; if (!identity) throw new Error(`.identity is required - prepareContext()`); const $id = reqContext.identity; (0, engine_1._inf)(NS, '! identity(req) :=', engine_1.$U.json(Object.assign({}, $id))); identity.identityProvider = $id.cognitoAuthenticationProvider; // provider string. identity.identityPoolId = $id.cognitoIdentityPoolId; // identity-pool-id like 'ap-northeast-2:618ce9d2-3ad6-49df-b3b3-e248ea51425e' identity.identityId = $id.cognitoIdentityId; // identity-id like 'ap-northeast-2:dbd95fb4-7423-48b8-8a04-56e5bc95e444' identity.accountId = $id.accountId; // account-id should be same as context.accountId identity.userAgent = $id.userAgent; // user-agent string. identity.caller = (_f = $id.caller) !== null && _f !== void 0 ? _f : undefined; // caller string. (0, engine_1._inf)(NS, '! identity(new) :=', engine_1.$U.json(Object.assign({}, identity))); } //TODO - transform to access identity via `lemon-accounts-api` service @200106 // STEP.5 extract additional request infor from req-context. const clientIp = `${((_g = reqContext === null || reqContext === void 0 ? void 0 : reqContext.identity) === null || _g === void 0 ? void 0 : _g.sourceIp) || ''}`; const userAgent = `${((_h = reqContext === null || reqContext === void 0 ? void 0 : reqContext.identity) === null || _h === void 0 ? void 0 : _h.userAgent) || ''}`; const requestId = `${(reqContext === null || reqContext === void 0 ? void 0 : reqContext.requestId) || ''}`; const accountId = `${(reqContext === null || reqContext === void 0 ? void 0 : reqContext.accountId) || ''}`; const domain = `${(reqContext === null || reqContext === void 0 ? void 0 : reqContext.domainName) || this.getHeader('host') || ''}`; //! chore avoid null of headers //! save into headers and returns. const context = Object.assign(Object.assign({}, $org), { userAgent, clientIp, requestId, accountId, domain }); return context; }); this.handler = handler; this.headers = Object.assign({}, headers); } hello() { return 'header-tool-by-default'; } /** * 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 azure_keyvault_service_1.KeyVaultService(keyId); return kms; } } exports.MyHttpHeaderTool = MyHttpHeaderTool; //# sourceMappingURL=functions-web-handler.js.map