From 3849f7dc1880ceecf32f662a91e16c8190e0ad47 Mon Sep 17 00:00:00 2001
From: PierreDemailly
Date: Sun, 29 Oct 2023 19:35:31 +0100
Subject: [PATCH 1/6] feat(morphix): introduce new micro template package with
function support
---
.github/workflows/agent.yml | 2 +-
.github/workflows/codeql.yml | 6 +-
.github/workflows/config.yml | 2 +-
.github/workflows/discord.yml | 2 +-
.github/workflows/logql.yml | 2 +-
.github/workflows/morphix.yml | 39 ++++++++
.github/workflows/scorecard.yml | 4 +-
.github/workflows/slack.yml | 2 +-
.github/workflows/teams.yml | 2 +-
README.md | 1 +
package-lock.json | 72 ++++++--------
package.json | 3 +-
src/agent/package.json | 3 +-
src/agent/src/notifiers/notifierQueue.ts | 9 +-
src/discord/package.json | 3 +-
src/discord/src/index.ts | 14 ++-
src/morphix/README.md | 116 +++++++++++++++++++++++
src/morphix/package.json | 40 ++++++++
src/morphix/src/functions.ts | 17 ++++
src/morphix/src/index.ts | 77 +++++++++++++++
src/morphix/test/index.spec.ts | 84 ++++++++++++++++
src/morphix/tsconfig.json | 20 ++++
src/slack/package.json | 3 +-
src/slack/src/index.ts | 12 +--
src/teams/package.json | 3 +-
src/teams/src/index.ts | 10 +-
tsconfig.json | 18 ++++
27 files changed, 480 insertions(+), 86 deletions(-)
create mode 100644 .github/workflows/morphix.yml
create mode 100644 src/morphix/README.md
create mode 100644 src/morphix/package.json
create mode 100644 src/morphix/src/functions.ts
create mode 100644 src/morphix/src/index.ts
create mode 100644 src/morphix/test/index.spec.ts
create mode 100644 src/morphix/tsconfig.json
diff --git a/.github/workflows/agent.yml b/.github/workflows/agent.yml
index 6a6e210..b73397f 100644
--- a/.github/workflows/agent.yml
+++ b/.github/workflows/agent.yml
@@ -28,7 +28,7 @@ jobs:
egress-policy: audit
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- name: Use Node.js ${{ matrix.node-version }}
- uses: actions/setup-node@5e21ff4d9bc1a8cf6de233a3057d20ec6b3fb69d # v3.8.1
+ uses: actions/setup-node@8f152de45cc393bb48ce5d89d36b731f54556e65 # v4.0.0
with:
node-version: ${{ matrix.node-version }}
- name: Install dependencies
diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml
index 52ef8a3..bac409c 100644
--- a/.github/workflows/codeql.yml
+++ b/.github/workflows/codeql.yml
@@ -54,7 +54,7 @@ jobs:
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
- uses: github/codeql-action/init@49abf0ba24d0b7953cb586944e918a0b92074c80 # v2.22.4
+ uses: github/codeql-action/init@74483a38d39275f33fcff5f35b679b5ca4a26a99 # v2.22.5
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
@@ -68,7 +68,7 @@ jobs:
# Autobuild attempts to build any compiled languages (C/C++, C#, Go, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
- uses: github/codeql-action/autobuild@49abf0ba24d0b7953cb586944e918a0b92074c80 # v2.22.4
+ uses: github/codeql-action/autobuild@74483a38d39275f33fcff5f35b679b5ca4a26a99 # v2.22.5
# âšī¸ Command-line programs to run using the OS shell.
# đ See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
@@ -81,6 +81,6 @@ jobs:
# ./location_of_script_within_repo/buildscript.sh
- name: Perform CodeQL Analysis
- uses: github/codeql-action/analyze@49abf0ba24d0b7953cb586944e918a0b92074c80 # v2.22.4
+ uses: github/codeql-action/analyze@74483a38d39275f33fcff5f35b679b5ca4a26a99 # v2.22.5
with:
category: "/language:${{matrix.language}}"
diff --git a/.github/workflows/config.yml b/.github/workflows/config.yml
index 7d04e1d..9319420 100644
--- a/.github/workflows/config.yml
+++ b/.github/workflows/config.yml
@@ -28,7 +28,7 @@ jobs:
egress-policy: audit
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- name: Use Node.js ${{ matrix.node-version }}
- uses: actions/setup-node@5e21ff4d9bc1a8cf6de233a3057d20ec6b3fb69d # v3.8.1
+ uses: actions/setup-node@8f152de45cc393bb48ce5d89d36b731f54556e65 # v4.0.0
with:
node-version: ${{ matrix.node-version }}
- name: Install dependencies
diff --git a/.github/workflows/discord.yml b/.github/workflows/discord.yml
index f30abd9..77581e9 100644
--- a/.github/workflows/discord.yml
+++ b/.github/workflows/discord.yml
@@ -28,7 +28,7 @@ jobs:
egress-policy: audit
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- name: Use Node.js ${{ matrix.node-version }}
- uses: actions/setup-node@5e21ff4d9bc1a8cf6de233a3057d20ec6b3fb69d # v3.8.1
+ uses: actions/setup-node@8f152de45cc393bb48ce5d89d36b731f54556e65 # v4.0.0
with:
node-version: ${{ matrix.node-version }}
- name: Install dependencies
diff --git a/.github/workflows/logql.yml b/.github/workflows/logql.yml
index df93a93..d10ab2e 100644
--- a/.github/workflows/logql.yml
+++ b/.github/workflows/logql.yml
@@ -28,7 +28,7 @@ jobs:
egress-policy: audit
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- name: Use Node.js ${{ matrix.node-version }}
- uses: actions/setup-node@5e21ff4d9bc1a8cf6de233a3057d20ec6b3fb69d # v3.8.1
+ uses: actions/setup-node@8f152de45cc393bb48ce5d89d36b731f54556e65 # v4.0.0
with:
node-version: ${{ matrix.node-version }}
- name: Install dependencies
diff --git a/.github/workflows/morphix.yml b/.github/workflows/morphix.yml
new file mode 100644
index 0000000..144f582
--- /dev/null
+++ b/.github/workflows/morphix.yml
@@ -0,0 +1,39 @@
+name: Morphix
+
+on:
+ push:
+ branches: [main]
+ paths:
+ - src/morphix/src/**
+ - src/morphix/test/**
+ pull_request:
+ paths:
+ - src/morphix/src/**
+ - src/morphix/test/**
+
+permissions:
+ contents: read
+
+jobs:
+ test:
+ runs-on: ubuntu-latest
+ strategy:
+ matrix:
+ node-version: [18.x, 20.x]
+ fail-fast: false
+ steps:
+ - name: Harden Runner
+ uses: step-security/harden-runner@1b05615854632b887b69ae1be8cbefe72d3ae423 # v2.6.0
+ with:
+ egress-policy: audit
+ allowed-endpoints: >
+ dns.google
+ - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
+ - name: Use Node.js ${{ matrix.node-version }}
+ uses: actions/setup-node@8f152de45cc393bb48ce5d89d36b731f54556e65 # v4.0.0
+ with:
+ node-version: ${{ matrix.node-version }}
+ - name: Install dependencies
+ run: npm ci
+ - name: Run tests
+ run: npm run test --workspace=src/morphix
diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml
index c0d9e23..7221a12 100644
--- a/.github/workflows/scorecard.yml
+++ b/.github/workflows/scorecard.yml
@@ -43,7 +43,7 @@ jobs:
persist-credentials: false
- name: "Run analysis"
- uses: ossf/scorecard-action@483ef80eb98fb506c348f7d62e28055e49fe2398 # v2.3.0
+ uses: ossf/scorecard-action@0864cf19026789058feabb7e87baa5f140aac736 # v2.3.1
with:
results_file: results.sarif
results_format: sarif
@@ -73,6 +73,6 @@ jobs:
# Upload the results to GitHub's code scanning dashboard.
- name: "Upload to code-scanning"
- uses: github/codeql-action/upload-sarif@49abf0ba24d0b7953cb586944e918a0b92074c80 # v2.22.4
+ uses: github/codeql-action/upload-sarif@74483a38d39275f33fcff5f35b679b5ca4a26a99 # v2.22.5
with:
sarif_file: results.sarif
diff --git a/.github/workflows/slack.yml b/.github/workflows/slack.yml
index ab8b446..31953bd 100644
--- a/.github/workflows/slack.yml
+++ b/.github/workflows/slack.yml
@@ -28,7 +28,7 @@ jobs:
egress-policy: audit
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- name: Use Node.js ${{ matrix.node-version }}
- uses: actions/setup-node@5e21ff4d9bc1a8cf6de233a3057d20ec6b3fb69d # v3.8.1
+ uses: actions/setup-node@8f152de45cc393bb48ce5d89d36b731f54556e65 # v4.0.0
with:
node-version: ${{ matrix.node-version }}
- name: Install dependencies
diff --git a/.github/workflows/teams.yml b/.github/workflows/teams.yml
index 2662098..177e8f7 100644
--- a/.github/workflows/teams.yml
+++ b/.github/workflows/teams.yml
@@ -28,7 +28,7 @@ jobs:
egress-policy: audit
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- name: Use Node.js ${{ matrix.node-version }}
- uses: actions/setup-node@5e21ff4d9bc1a8cf6de233a3057d20ec6b3fb69d # v3.8.1
+ uses: actions/setup-node@8f152de45cc393bb48ce5d89d36b731f54556e65 # v4.0.0
with:
node-version: ${{ matrix.node-version }}
- name: Install dependencies
diff --git a/README.md b/README.md
index b95fe72..786902d 100644
--- a/README.md
+++ b/README.md
@@ -39,6 +39,7 @@ Click on one of the links to access the documentation of the package:
| name | package and link |
| --- | --- |
| logql | [@sigyn/logql](./src/logql) |
+| morphix | [@sigyn/morphix](./src/morphix) |
### Notifiers
| name | package and link |
diff --git a/package-lock.json b/package-lock.json
index 23862a1..f53528d 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -12,13 +12,14 @@
"src/config",
"src/logql",
"src/agent",
+ "src/morphix",
"src/discord",
"src/slack",
"src/teams"
],
"devDependencies": {
"@nodesecure/eslint-config": "^1.8.0",
- "@types/node": "^20.8.7",
+ "@types/node": "^20.8.9",
"c8": "^8.0.1",
"cross-env": "^7.0.3",
"glob": "^10.3.10",
@@ -1070,6 +1071,10 @@
"resolved": "src/logql",
"link": true
},
+ "node_modules/@sigyn/morphix": {
+ "resolved": "src/morphix",
+ "link": true
+ },
"node_modules/@sigyn/slack": {
"resolved": "src/slack",
"link": true
@@ -1100,12 +1105,12 @@
"dev": true
},
"node_modules/@types/node": {
- "version": "20.8.7",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-20.8.7.tgz",
- "integrity": "sha512-21TKHHh3eUHIi2MloeptJWALuCu5H7HQTdTrWIFReA8ad+aggoX+lRes3ex7/FtpC+sVUpFMQ+QTfYr74mruiQ==",
+ "version": "20.8.9",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-20.8.9.tgz",
+ "integrity": "sha512-UzykFsT3FhHb1h7yD4CA4YhBHq545JC0YnEz41xkipN88eKQtL6rSgocL5tbAP6Ola9Izm/Aw4Ora8He4x0BHg==",
"dev": true,
"dependencies": {
- "undici-types": "~5.25.1"
+ "undici-types": "~5.26.4"
}
},
"node_modules/@types/semver": {
@@ -2215,17 +2220,6 @@
"node": ">=6"
}
},
- "node_modules/escape-goat": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/escape-goat/-/escape-goat-4.0.0.tgz",
- "integrity": "sha512-2Sd4ShcWxbx6OY1IHyla/CVNwvg7XwZVoXZHcSu9w9SReNP1EzzD5T8NWKIR38fIqEns9kDWKUQTXXAmlDrdPg==",
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
"node_modules/escape-string-regexp": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
@@ -3825,9 +3819,9 @@
}
},
"node_modules/pino": {
- "version": "8.16.0",
- "resolved": "https://registry.npmjs.org/pino/-/pino-8.16.0.tgz",
- "integrity": "sha512-UUmvQ/7KTZt/vHjhRrnyS7h+J7qPBQnpG80V56xmIC+o9IqYmQOw/UIny9S9zYDfRBR0ClouCr464EkBMIT7Fw==",
+ "version": "8.16.1",
+ "resolved": "https://registry.npmjs.org/pino/-/pino-8.16.1.tgz",
+ "integrity": "sha512-3bKsVhBmgPjGV9pyn4fO/8RtoVDR8ssW1ev819FsRXlRNgW8gR/9Kx+gCK4UPWd4JjrRDLWpzd/pb1AyWm3MGA==",
"dependencies": {
"atomic-sleep": "^1.0.0",
"fast-redact": "^3.1.1",
@@ -3986,20 +3980,6 @@
"node": ">=6"
}
},
- "node_modules/pupa": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/pupa/-/pupa-3.1.0.tgz",
- "integrity": "sha512-FLpr4flz5xZTSJxSeaheeMKN/EDzMdK7b8PTOC6a5PYFKTucWbdqjgqaEyH0shFiSJrVB1+Qqi4Tk19ccU6Aug==",
- "dependencies": {
- "escape-goat": "^4.0.0"
- },
- "engines": {
- "node": ">=12.20"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
"node_modules/queue-microtask": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
@@ -4852,9 +4832,9 @@
}
},
"node_modules/undici-types": {
- "version": "5.25.3",
- "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.25.3.tgz",
- "integrity": "sha512-Ga1jfYwRn7+cP9v8auvEXN1rX3sWqlayd4HP7OKk4mZWylEmu3KzXDUGrQUN6Ol7qo1gPvB2e5gX6udnyEPgdA==",
+ "version": "5.26.5",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
+ "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
"dev": true
},
"node_modules/update-browserslist-db": {
@@ -5173,8 +5153,7 @@
"dayjs": "^1.11.10",
"dotenv": "^16.3.1",
"ms": "^2.1.3",
- "pino": "^8.16.0",
- "pupa": "^3.1.0",
+ "pino": "^8.16.1",
"toad-scheduler": "^3.0.0"
},
"devDependencies": {
@@ -5264,8 +5243,7 @@
"version": "1.3.1",
"license": "MIT",
"dependencies": {
- "@myunisoft/httpie": "^2.0.3",
- "pupa": "^3.1.0"
+ "@myunisoft/httpie": "^2.0.3"
},
"engines": {
"node": ">=18"
@@ -5279,13 +5257,20 @@
"node": ">=18"
}
},
+ "src/morphix": {
+ "name": "@sigyn/morphix",
+ "version": "1.0.0",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
"src/slack": {
"name": "@sigyn/slack",
"version": "1.6.1",
"license": "MIT",
"dependencies": {
- "@myunisoft/httpie": "^2.0.3",
- "pupa": "^3.1.0"
+ "@myunisoft/httpie": "^2.0.3"
},
"engines": {
"node": ">=18"
@@ -5296,8 +5281,7 @@
"version": "1.3.0",
"license": "MIT",
"dependencies": {
- "@myunisoft/httpie": "^2.0.3",
- "pupa": "^3.1.0"
+ "@myunisoft/httpie": "^2.0.3"
},
"engines": {
"node": ">=18"
diff --git a/package.json b/package.json
index 1d6396e..e9bad6d 100644
--- a/package.json
+++ b/package.json
@@ -13,13 +13,14 @@
"src/config",
"src/logql",
"src/agent",
+ "src/morphix",
"src/discord",
"src/slack",
"src/teams"
],
"devDependencies": {
"@nodesecure/eslint-config": "^1.8.0",
- "@types/node": "^20.8.7",
+ "@types/node": "^20.8.9",
"c8": "^8.0.1",
"cross-env": "^7.0.3",
"glob": "^10.3.10",
diff --git a/src/agent/package.json b/src/agent/package.json
index cd0af1d..d90d01b 100644
--- a/src/agent/package.json
+++ b/src/agent/package.json
@@ -47,8 +47,7 @@
"dayjs": "^1.11.10",
"dotenv": "^16.3.1",
"ms": "^2.1.3",
- "pino": "^8.16.0",
- "pupa": "^3.1.0",
+ "pino": "^8.16.1",
"toad-scheduler": "^3.0.0"
},
"devDependencies": {
diff --git a/src/agent/src/notifiers/notifierQueue.ts b/src/agent/src/notifiers/notifierQueue.ts
index aaf1fbb..e45112e 100644
--- a/src/agent/src/notifiers/notifierQueue.ts
+++ b/src/agent/src/notifiers/notifierQueue.ts
@@ -8,6 +8,9 @@ const kPrivateInstancier = Symbol("instancier");
type NotifierQueueAlert = T & {
_id: symbol;
_nonUniqueMatcher: (notification: T, newNotifications: T) => boolean;
+ notifierConfig: {
+ notifier: string;
+ }
}
export class NotifierQueue extends EventEmitter {
@@ -38,7 +41,11 @@ export class NotifierQueue extends EventEmitter {
for (const newNotification of notifications) {
const { _id } = newNotification;
const alreadyInQueue = this.#notificationAlerts
- .find((notification) => notification._id === _id && notification._nonUniqueMatcher(newNotification, notification));
+ .find(
+ (notification) => notification._id === _id &&
+ notification._nonUniqueMatcher(newNotification, notification) &&
+ notification.notifierConfig.notifier === newNotification.notifierConfig.notifier
+ );
if (alreadyInQueue === undefined) {
this.#notificationAlerts.push(newNotification);
diff --git a/src/discord/package.json b/src/discord/package.json
index b209de0..c486067 100644
--- a/src/discord/package.json
+++ b/src/discord/package.json
@@ -38,7 +38,6 @@
"author": "GENTILHOMME Thomas ",
"license": "MIT",
"dependencies": {
- "@myunisoft/httpie": "^2.0.3",
- "pupa": "^3.1.0"
+ "@myunisoft/httpie": "^2.0.3"
}
}
diff --git a/src/discord/src/index.ts b/src/discord/src/index.ts
index d09a1a3..a49167c 100644
--- a/src/discord/src/index.ts
+++ b/src/discord/src/index.ts
@@ -1,6 +1,7 @@
// Import Third-party Dependencies
import * as httpie from "@myunisoft/httpie";
import { NotifierFormattedSigynRule, SigynInitializedTemplate } from "@sigyn/config";
+import { morphix } from "@sigyn/morphix";
// CONSTANTS
const kWebhookUsername = "Sigyn Agent";
@@ -40,8 +41,6 @@ interface ExecuteWebhookData {
async function formatWebhook(options: ExecuteWebhookOptions) {
const { agentFailure, counter, ruleConfig, label, severity, lokiUrl, rules } = options.data;
- // pupa is ESM only, need a dynamic import for CommonJS.
- const { default: pupa } = await import("pupa");
const { title: templateTitle = "", content: templateContent = [] } = options.template;
if (templateTitle === "" && templateContent.length === 0) {
@@ -75,15 +74,14 @@ async function formatWebhook(options: ExecuteWebhookOptions) {
ignoreMissing: true
};
- const content: string[] = templateContent.map((content) => pupa(
- content,
- templateData,
- contentTemplateOptions
- ));
+ const content: string[] = [];
+ for (const template of templateContent) {
+ content.push(await morphix(template, templateData, contentTemplateOptions));
+ }
return {
embeds: [{
- title: pupa(`${kSeverityEmoji[severity]} ${templateTitle}`, templateData, titleTemplateOptions),
+ title: await morphix(`${kSeverityEmoji[severity]} ${templateTitle}`, templateData, titleTemplateOptions),
description: content.join("\n"),
color: kEmbedColor[severity]
}],
diff --git a/src/morphix/README.md b/src/morphix/README.md
new file mode 100644
index 0000000..8243871
--- /dev/null
+++ b/src/morphix/README.md
@@ -0,0 +1,116 @@
+
+ Morphix
+
+
+
+ Micro templating with function pipes support
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+## đ Getting Started
+
+This package is available in the Node Package Repository and can be easily installed with [npm](https://doc.npmjs.com/getting-started/what-is-npm) or [yarn](https://yarnpkg.com)
+
+```bash
+$ npm i @sigyn/morphix
+# or
+$ yarn add @sigyn/morphix
+```
+
+## đ Usage
+
+```ts
+import { morphix } from "@sigyn/morphix";
+
+await morphix("Hello {name | capitalize}", { name: "john" });
+```
+
+> [!NOTE]
+> `morphix()` is async because it supports async **functions**
+
+## đ API
+
+### `morphix`
+
+```ts
+async function morphix(
+ template: string,
+ data: Record | unknown[],
+ options: MorphixOptions = {}
+): Promise
+```
+
+**template**
+Type: `string`
+
+Text with placeholders for data properties.
+
+**data**
+Type: `object | unknown[]`
+
+Data to interpolate into template.
+
+The keys should be a valid JS identifier or number (a-z, A-Z, 0-9).
+
+**options**
+Type: `object`
+
+**ignoreMissing**
+Type: `boolean`
+Default: `false`
+
+By default, Morphix throws a MissingValueError when a placeholder resolves to undefined. With this option set to true, it simply ignores it and leaves the placeholder as is.
+
+**transform**
+Type: `(data: { value: unknown; key: string }) => unknown` (default: `({value}) => value)`)
+
+Performs arbitrary operation for each interpolation. If the returned value was undefined, it behaves differently depending on the ignoreMissing option. Otherwise, the returned value will be interpolated into a string and embedded into the template.
+
+**MissingValueError**
+Exposed for instance checking.
+
+## đĻ Functions
+
+**capitalize**
+
+Capitalize the first letter.
+
+**dnsresolve**
+
+Retrieve host of a given IP. It uses `dns.reverse`.
+
+If it fails to retrieve the host, it returns the ip instead.
+
+## đī¸ Interfaces
+
+```ts
+interface MorphixOptions {
+ transform?: (data: {
+ value: unknown;
+ key: string;
+ }) => unknown;
+ ignoreMissing?: boolean;
+}
+```
+
+## Credits
+This package is heavily inspired by [pupa](https://github.com/sindresorhus/pupa). Morphix is a fork with function support and doesn't support HTML escape.
+
+## License
+MIT
diff --git a/src/morphix/package.json b/src/morphix/package.json
new file mode 100644
index 0000000..3168e50
--- /dev/null
+++ b/src/morphix/package.json
@@ -0,0 +1,40 @@
+{
+ "name": "@sigyn/morphix",
+ "version": "1.0.0",
+ "description": "Micro templating with function pipes support",
+ "main": "./dist/index.js",
+ "module": "./dist/index.mjs",
+ "types": "./dist/index.d.ts",
+ "exports": {
+ ".": {
+ "require": "./dist/index.js",
+ "import": "./dist/index.mjs",
+ "types": "./dist/index.d.ts"
+ }
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "scripts": {
+ "build": "tsup src/index.ts --format cjs,esm --dts --clean",
+ "prepublishOnly": "npm run build",
+ "test": "glob -c \"tsx --test\" \"./test/**/*.spec.ts\"",
+ "coverage": "c8 -r html npm test",
+ "lint": "cross-env eslint src/**/*.ts"
+ },
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/MyUnisoft/sigyn.git",
+ "directory": "src/morphix"
+ },
+ "bugs": {
+ "url": "https://github.com/MyUnisoft/sigyn/issues"
+ },
+ "homepage": "https://github.com/MyUnisoft/sigyn/blob/main/src/morphix/README.md",
+ "files": [
+ "dist"
+ ],
+ "keywords": [],
+ "author": "GENTILHOMME Thomas ",
+ "license": "MIT"
+}
diff --git a/src/morphix/src/functions.ts b/src/morphix/src/functions.ts
new file mode 100644
index 0000000..f84e0be
--- /dev/null
+++ b/src/morphix/src/functions.ts
@@ -0,0 +1,17 @@
+// Import Node.js Dependencies
+import dns from "node:dns/promises";
+
+export function capitalize(value: string): string {
+ return `${value.charAt(0).toUpperCase()}${value.slice(1)}`;
+}
+
+export async function dnsresolve(ip: string): Promise {
+ try {
+ const hostNames = await dns.reverse(ip);
+
+ return hostNames.join(", ");
+ }
+ catch {
+ return ip;
+ }
+}
diff --git a/src/morphix/src/index.ts b/src/morphix/src/index.ts
new file mode 100644
index 0000000..071106c
--- /dev/null
+++ b/src/morphix/src/index.ts
@@ -0,0 +1,77 @@
+/* eslint-disable func-style */
+
+// Import Internal Dependencies
+import { capitalize, dnsresolve } from "./functions";
+
+// CONSTANTS
+const kFunctions = {
+ capitalize,
+ dnsresolve
+};
+
+export interface MorphixOptions {
+ transform?: (data: { value: unknown; key: string }) => unknown;
+ ignoreMissing?: boolean;
+}
+
+export class MissingValueError extends Error {
+ key: string;
+
+ constructor(key: string) {
+ super(`Missing a value for ${key ? `the placeholder: ${key}` : "a placeholder"}`);
+ this.name = "MissingValueError";
+ this.key = key;
+ }
+}
+
+export async function morphix(
+ template: string,
+ data: Record | unknown[],
+ options: MorphixOptions = {}
+) {
+ const {
+ transform = ({ value }) => value,
+ ignoreMissing = false
+ } = options;
+
+ if (typeof template !== "string") {
+ throw new TypeError(`Expected a \`string\` in the first argument, got \`${typeof template}\``);
+ }
+
+ if (typeof data !== "object") {
+ throw new TypeError(`Expected an \`object\` or \`Array\` in the second argument, got \`${typeof data}\``);
+ }
+
+ const replace = async(placeholder: string, key: string, func: string | undefined) => {
+ let value: string | undefined = undefined;
+ for (const property of key?.split(".")) {
+ // eslint-disable-next-line no-nested-ternary
+ value = value ? value[property] : data ? data[property] : undefined;
+ }
+
+ const transformedValue = transform({ value, key });
+ if (transformedValue === undefined) {
+ if (ignoreMissing) {
+ return placeholder;
+ }
+
+ throw new MissingValueError(key);
+ }
+
+ if (func === undefined) {
+ return String(transformedValue);
+ }
+
+ return await kFunctions[func](String(transformedValue));
+ };
+
+ const braceFnRegex = /{\s*([a-z0-9-.]*)\s*(?:\|\s*((?:(?!{)[a-z0-9-.]*)*?)\s*)?}/gi;
+ let formattedTemplate = template;
+
+ for (const [match, placeholder, func] of template.matchAll(braceFnRegex)) {
+ const replaceText = await replace(match, placeholder, func);
+ formattedTemplate = formattedTemplate.replace(match, () => replaceText);
+ }
+
+ return formattedTemplate;
+}
diff --git a/src/morphix/test/index.spec.ts b/src/morphix/test/index.spec.ts
new file mode 100644
index 0000000..9800071
--- /dev/null
+++ b/src/morphix/test/index.spec.ts
@@ -0,0 +1,84 @@
+// Import Node.js Dependencies
+import assert from "node:assert";
+import { describe, it } from "node:test";
+
+// Import Internal Dependencies
+import { morphix } from "../src/index";
+
+describe("Morphix", () => {
+ it("main", async() => {
+ // Normal placeholder
+ assert.equal(await morphix("{foo}", { foo: "!" }), "!");
+ assert.equal(await morphix("{foo}", { foo: 10 }), "10");
+ assert.equal(await morphix("{foo}", { foo: 0 }), "0");
+ assert.equal(await morphix("{fo-o}", { "fo-o": 0 }), "0");
+ assert.equal(await morphix("{foo}{foo}", { foo: "!" }), "!!");
+ assert.equal(await morphix("{foo}{bar}{foo}", { foo: "!", bar: "#" }), "!#!");
+ assert.equal(await morphix("yo {foo} lol {bar} sup", { foo: "đĻ", bar: "đ" }), "yo đĻ lol đ sup");
+
+ assert.equal(await morphix("{foo}{deeply.nested.valueFoo}", {
+ foo: "!",
+ deeply: {
+ nested: {
+ valueFoo: "#"
+ }
+ }
+ }), "!#");
+
+ assert.equal(await morphix("{0}{1}", ["!", "#"]), "!#");
+
+ assert.equal(await morphix("{0}{1}", ["!", "#"]), "!#");
+ });
+
+ it("do not match non-identifiers", async() => {
+ const fixture = "\"*.{json,md,css,graphql,html}\"";
+ assert.equal(await morphix(fixture, []), fixture);
+ });
+
+ it("ignore missing", async() => {
+ const template = "foo{{bar}}{undefined}";
+ const options = { ignoreMissing: true };
+ assert.equal(await morphix(template, {}, options), template);
+ });
+
+ it("throw on undefined by default", async() => {
+ assert.rejects(async() => {
+ await morphix("{foo}", {});
+ }, {
+ message: "Missing a value for the placeholder: foo"
+ });
+ });
+
+ it("transform and ignore missing", async() => {
+ const options = {
+ ignoreMissing: true,
+ transform: ({ value }) => (Number.isNaN(Number.parseInt(value, 10)) ? undefined : value)
+ };
+ assert.equal(await morphix("{0} {1} {2}", ["0", 42, 3.14], options), "0 42 3.14");
+ assert.equal(await morphix("{0} {1} {2}", ["0", null, 3.14], options), "0 {1} 3.14");
+ });
+
+ it("transform and throw on undefined", async() => {
+ const options = {
+ transform: ({ value }) => (Number.isNaN(Number.parseInt(value, 10)) ? undefined : value)
+ };
+
+ await assert.doesNotReject(async() => {
+ await morphix("{0} {1} {2}", ["0", 42, 3.14], options);
+ });
+
+ await assert.rejects(async() => {
+ await morphix("{0} {1} {2}", ["0", null, 3.14], options);
+ }, {
+ message: "Missing a value for the placeholder: 1"
+ });
+ });
+
+ it("should capitalize value", async() => {
+ assert.equal(await morphix("{foo | capitalize}", { foo: "foo" }), "Foo");
+ });
+
+ it("should find ip hostname", async() => {
+ assert.equal(await morphix("{foo | dnsresolve}", { foo: "8.8.8.8" }), "dns.google");
+ });
+});
diff --git a/src/morphix/tsconfig.json b/src/morphix/tsconfig.json
new file mode 100644
index 0000000..e9d6a60
--- /dev/null
+++ b/src/morphix/tsconfig.json
@@ -0,0 +1,20 @@
+{
+ "compilerOptions": {
+ "declaration": true,
+ "strictNullChecks": true,
+ "target": "ES2022",
+ "outDir": "dist",
+ "module": "ES2022",
+ "moduleResolution": "node",
+ "esModuleInterop": true,
+ "resolveJsonModule": false,
+ "skipDefaultLibCheck": true,
+ "skipLibCheck": true,
+ "forceConsistentCasingInFileNames": true,
+ "sourceMap": true,
+ "rootDir": "./src",
+ "types": ["node"]
+ },
+ "include": ["src"],
+ "exclude": ["node_modules", "dist"]
+}
diff --git a/src/slack/package.json b/src/slack/package.json
index fb20f9b..0b6af06 100644
--- a/src/slack/package.json
+++ b/src/slack/package.json
@@ -38,7 +38,6 @@
"author": "GENTILHOMME Thomas ",
"license": "MIT",
"dependencies": {
- "@myunisoft/httpie": "^2.0.3",
- "pupa": "^3.1.0"
+ "@myunisoft/httpie": "^2.0.3"
}
}
diff --git a/src/slack/src/index.ts b/src/slack/src/index.ts
index 9df7d17..3b6fd69 100644
--- a/src/slack/src/index.ts
+++ b/src/slack/src/index.ts
@@ -1,6 +1,7 @@
// Import Third-party Dependencies
import * as httpie from "@myunisoft/httpie";
import { NotifierFormattedSigynRule, SigynInitializedTemplate } from "@sigyn/config";
+import { morphix } from "@sigyn/morphix";
// CONSTANTS
const kAttachmentColor = {
@@ -38,9 +39,6 @@ interface ExecuteWebhookData {
async function formatWebhook(options: ExecuteWebhookOptions) {
const { agentFailure, counter, ruleConfig, label, severity, lokiUrl, rules } = options.data;
- // pupa is ESM only, need a dynamic import for CommonJS.
- const { default: pupa } = await import("pupa");
-
const { title: templateTitle = "", content: templateContent = [] } = options.template;
if (templateTitle === "" && templateContent.length === 0) {
throw new Error("Invalid rule template: one of the title or content is required.");
@@ -81,10 +79,10 @@ async function formatWebhook(options: ExecuteWebhookOptions) {
{
mrkdwn_in: ["text"],
color: kAttachmentColor[severity],
- title: pupa(formattedTitle, templateData, titleTemplateOptions),
+ title: morphix(formattedTitle, templateData, titleTemplateOptions),
fields: [
{
- value: templateContent.map((text) => {
+ value: (await Promise.all(templateContent.map(async(text) => {
if (text === "") {
return "";
}
@@ -97,12 +95,12 @@ async function formatWebhook(options: ExecuteWebhookOptions) {
formattedText = formattedText.replace(url, `<${link}|${label}>`);
}
- return pupa(
+ return await morphix(
formattedText,
templateData,
templateOptions
);
- }).join("\n").replaceAll(/>(?!\s|$)/g, "âē"),
+ }))).join("\n").replaceAll(/>(?!\s|$)/g, "âē"),
short: false
}
]
diff --git a/src/teams/package.json b/src/teams/package.json
index 2774a9b..510873b 100644
--- a/src/teams/package.json
+++ b/src/teams/package.json
@@ -38,7 +38,6 @@
"author": "GENTILHOMME Thomas ",
"license": "MIT",
"dependencies": {
- "@myunisoft/httpie": "^2.0.3",
- "pupa": "^3.1.0"
+ "@myunisoft/httpie": "^2.0.3"
}
}
diff --git a/src/teams/src/index.ts b/src/teams/src/index.ts
index a081848..f2142ce 100644
--- a/src/teams/src/index.ts
+++ b/src/teams/src/index.ts
@@ -1,6 +1,7 @@
// Import Third-party Dependencies
import * as httpie from "@myunisoft/httpie";
import { NotifierFormattedSigynRule, SigynInitializedTemplate } from "@sigyn/config";
+import { morphix } from "@sigyn/morphix";
// CONSTANTS
const kSeverityEmoji = {
@@ -32,9 +33,6 @@ interface ExecuteWebhookData {
async function formatWebhook(options: ExecuteWebhookOptions) {
const { agentFailure, counter, ruleConfig, label, severity, lokiUrl, rules } = options.data;
- // pupa is ESM only, need a dynamic import for CommonJS.
- const { default: pupa } = await import("pupa");
-
const { title: templateTitle = "", content: templateContent = [] } = options.template;
if (templateTitle === "" && templateContent.length === 0) {
throw new Error("Invalid rule template: one of the title or content is required.");
@@ -61,14 +59,14 @@ async function formatWebhook(options: ExecuteWebhookOptions) {
ignoreMissing: true
};
- const content: string[] = templateContent.map((content) => pupa(
+ const content: string[] = await Promise.all(templateContent.map(async(content) => await morphix(
content,
templateData,
textTemplateOptions
- ));
+ )));
return {
- title: pupa(`${kSeverityEmoji[severity]} ${templateTitle}`, templateData, titleTemplateOptions),
+ title: await morphix(`${kSeverityEmoji[severity]} ${templateTitle}`, templateData, titleTemplateOptions),
text: content.join("\n")
};
}
diff --git a/tsconfig.json b/tsconfig.json
index fc8da69..cc286e6 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -1,8 +1,26 @@
{
"files": [],
"references": [
+ {
+ "path": "./src/config"
+ },
+ {
+ "path": "./src/logql"
+ },
{
"path": "./src/agent"
+ },
+ {
+ "path": "./src/morphix"
+ },
+ {
+ "path": "./src/discord"
+ },
+ {
+ "path": "./src/slack"
+ },
+ {
+ "path": "./src/teams"
}
]
}
From ae3584d3c1891bc7eb9ce2906ac138f4a37aefd2 Mon Sep 17 00:00:00 2001
From: PierreDemailly
Date: Sun, 29 Oct 2023 19:48:00 +0100
Subject: [PATCH 2/6] fix(ci): build notifiers
---
.github/workflows/discord.yml | 2 ++
.github/workflows/slack.yml | 2 ++
.github/workflows/teams.yml | 2 ++
3 files changed, 6 insertions(+)
diff --git a/.github/workflows/discord.yml b/.github/workflows/discord.yml
index 77581e9..e9bbcbc 100644
--- a/.github/workflows/discord.yml
+++ b/.github/workflows/discord.yml
@@ -33,5 +33,7 @@ jobs:
node-version: ${{ matrix.node-version }}
- name: Install dependencies
run: npm ci
+ - name: Build packages
+ run: npm run build
- name: Run tests
run: npm run test --workspace=src/discord
diff --git a/.github/workflows/slack.yml b/.github/workflows/slack.yml
index 31953bd..174ac96 100644
--- a/.github/workflows/slack.yml
+++ b/.github/workflows/slack.yml
@@ -33,5 +33,7 @@ jobs:
node-version: ${{ matrix.node-version }}
- name: Install dependencies
run: npm ci
+ - name: Build packages
+ run: npm run build
- name: Run tests
run: npm run test --workspace=src/slack
diff --git a/.github/workflows/teams.yml b/.github/workflows/teams.yml
index 177e8f7..cf85787 100644
--- a/.github/workflows/teams.yml
+++ b/.github/workflows/teams.yml
@@ -33,5 +33,7 @@ jobs:
node-version: ${{ matrix.node-version }}
- name: Install dependencies
run: npm ci
+ - name: Build packages
+ run: npm run build
- name: Run tests
run: npm run test --workspace=src/teams
From eb7f2b17323ed3dd5d802c360dc8fb6da3bf1b62 Mon Sep 17 00:00:00 2001
From: PierreDemailly
Date: Sun, 29 Oct 2023 19:48:46 +0100
Subject: [PATCH 3/6] test without harden runner
---
.github/workflows/morphix.yml | 6 ------
1 file changed, 6 deletions(-)
diff --git a/.github/workflows/morphix.yml b/.github/workflows/morphix.yml
index 144f582..2bbcce5 100644
--- a/.github/workflows/morphix.yml
+++ b/.github/workflows/morphix.yml
@@ -22,12 +22,6 @@ jobs:
node-version: [18.x, 20.x]
fail-fast: false
steps:
- - name: Harden Runner
- uses: step-security/harden-runner@1b05615854632b887b69ae1be8cbefe72d3ae423 # v2.6.0
- with:
- egress-policy: audit
- allowed-endpoints: >
- dns.google
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@8f152de45cc393bb48ce5d89d36b731f54556e65 # v4.0.0
From a1e6edccb8e8e9ec15489768f87ff4f3cf00fb89 Mon Sep 17 00:00:00 2001
From: PierreDemailly
Date: Sun, 29 Oct 2023 20:02:22 +0100
Subject: [PATCH 4/6] chore(morphix): improve regexp
---
src/morphix/src/index.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/morphix/src/index.ts b/src/morphix/src/index.ts
index 071106c..f16a53d 100644
--- a/src/morphix/src/index.ts
+++ b/src/morphix/src/index.ts
@@ -65,7 +65,7 @@ export async function morphix(
return await kFunctions[func](String(transformedValue));
};
- const braceFnRegex = /{\s*([a-z0-9-.]*)\s*(?:\|\s*((?:(?!{)[a-z0-9-.]*)*?)\s*)?}/gi;
+ const braceFnRegex = /{\s{0,1}([a-z0-9-.]*)\s{0,1}(?:\|\s{0,1}((?:(?!{)[a-z0-9-]*)*?)\s{0,1})?}/gi;
let formattedTemplate = template;
for (const [match, placeholder, func] of template.matchAll(braceFnRegex)) {
From 6d23514629da2eae45f84692e80858a2ca83afd2 Mon Sep 17 00:00:00 2001
From: PierreDemailly
Date: Sun, 29 Oct 2023 20:28:48 +0100
Subject: [PATCH 5/6] chore: mock dns
---
.github/workflows/morphix.yml | 4 ++++
package-lock.json | 12 ++++++++++
src/morphix/package.json | 7 ++++--
.../test/{index.spec.ts => index.spec.mts} | 23 +++++++++++++++++--
4 files changed, 42 insertions(+), 4 deletions(-)
rename src/morphix/test/{index.spec.ts => index.spec.mts} (79%)
diff --git a/.github/workflows/morphix.yml b/.github/workflows/morphix.yml
index 2bbcce5..555b092 100644
--- a/.github/workflows/morphix.yml
+++ b/.github/workflows/morphix.yml
@@ -22,6 +22,10 @@ jobs:
node-version: [18.x, 20.x]
fail-fast: false
steps:
+ - name: Harden Runner
+ uses: step-security/harden-runner@1b05615854632b887b69ae1be8cbefe72d3ae423 # v2.6.0
+ with:
+ egress-policy: audit
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@8f152de45cc393bb48ce5d89d36b731f54556e65 # v4.0.0
diff --git a/package-lock.json b/package-lock.json
index f53528d..57309be 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -2439,6 +2439,15 @@
"node": ">=8"
}
},
+ "node_modules/esmock": {
+ "version": "2.5.8",
+ "resolved": "https://registry.npmjs.org/esmock/-/esmock-2.5.8.tgz",
+ "integrity": "sha512-Lvn8OhAvdM+OBgIkljnG8WNPayDN4vMyoHaKGnXnM4J7T3bE6qztUWHd7cnbnyW0ueLQRT/3bS6SoHnBN1hEsg==",
+ "dev": true,
+ "engines": {
+ "node": ">=14.16.0"
+ }
+ },
"node_modules/espree": {
"version": "9.6.1",
"resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz",
@@ -5261,6 +5270,9 @@
"name": "@sigyn/morphix",
"version": "1.0.0",
"license": "MIT",
+ "devDependencies": {
+ "esmock": "^2.5.8"
+ },
"engines": {
"node": ">=18"
}
diff --git a/src/morphix/package.json b/src/morphix/package.json
index 3168e50..309bc4b 100644
--- a/src/morphix/package.json
+++ b/src/morphix/package.json
@@ -18,7 +18,7 @@
"scripts": {
"build": "tsup src/index.ts --format cjs,esm --dts --clean",
"prepublishOnly": "npm run build",
- "test": "glob -c \"tsx --test\" \"./test/**/*.spec.ts\"",
+ "test": "npm run build && glob -c \"tsx --test --loader=esmock\" \"./test/**/*.spec.mts\"",
"coverage": "c8 -r html npm test",
"lint": "cross-env eslint src/**/*.ts"
},
@@ -36,5 +36,8 @@
],
"keywords": [],
"author": "GENTILHOMME Thomas ",
- "license": "MIT"
+ "license": "MIT",
+ "devDependencies": {
+ "esmock": "^2.5.8"
+ }
}
diff --git a/src/morphix/test/index.spec.ts b/src/morphix/test/index.spec.mts
similarity index 79%
rename from src/morphix/test/index.spec.ts
rename to src/morphix/test/index.spec.mts
index 9800071..f2f3cb6 100644
--- a/src/morphix/test/index.spec.ts
+++ b/src/morphix/test/index.spec.mts
@@ -2,8 +2,11 @@
import assert from "node:assert";
import { describe, it } from "node:test";
+// Import Third-party Dependencies
+import esmock from "esmock";
+
// Import Internal Dependencies
-import { morphix } from "../src/index";
+import { morphix } from "../dist/index.mjs";
describe("Morphix", () => {
it("main", async() => {
@@ -79,6 +82,22 @@ describe("Morphix", () => {
});
it("should find ip hostname", async() => {
- assert.equal(await morphix("{foo | dnsresolve}", { foo: "8.8.8.8" }), "dns.google");
+ const { morphix } = await esmock("../dist/index.mjs", {
+ "node:dns/promises": {
+ reverse: async() => ["dns.google"]
+ }
+ });
+ assert.equal(await morphix("host: {foo | dnsresolve}", { foo: "8.8.8.8" }), "host: dns.google");
+ });
+
+ it("should not find ip hostname", async() => {
+ const { morphix } = await esmock("../dist/index.mjs", {
+ "node:dns/promises": {
+ reverse: async() => {
+ throw new Error("Not found");
+ }
+ }
+ });
+ assert.equal(await morphix("host: {foo | dnsresolve}", { foo: "8.8.8.8" }), "host: 8.8.8.8");
});
});
From 20f866954c0a31f1cb175086fc84f77a880765bd Mon Sep 17 00:00:00 2001
From: PierreDemailly
Date: Mon, 30 Oct 2023 10:18:09 +0100
Subject: [PATCH 6/6] chore(morphix): split functions into multiple files
---
src/morphix/src/functions/capitalize.ts | 3 +++
src/morphix/src/{functions.ts => functions/dnsresolve.ts} | 4 ----
src/morphix/src/functions/index.ts | 2 ++
3 files changed, 5 insertions(+), 4 deletions(-)
create mode 100644 src/morphix/src/functions/capitalize.ts
rename src/morphix/src/{functions.ts => functions/dnsresolve.ts} (68%)
create mode 100644 src/morphix/src/functions/index.ts
diff --git a/src/morphix/src/functions/capitalize.ts b/src/morphix/src/functions/capitalize.ts
new file mode 100644
index 0000000..7efdbde
--- /dev/null
+++ b/src/morphix/src/functions/capitalize.ts
@@ -0,0 +1,3 @@
+export function capitalize(value: string): string {
+ return `${value.charAt(0).toUpperCase()}${value.slice(1)}`;
+}
diff --git a/src/morphix/src/functions.ts b/src/morphix/src/functions/dnsresolve.ts
similarity index 68%
rename from src/morphix/src/functions.ts
rename to src/morphix/src/functions/dnsresolve.ts
index f84e0be..897bbbe 100644
--- a/src/morphix/src/functions.ts
+++ b/src/morphix/src/functions/dnsresolve.ts
@@ -1,10 +1,6 @@
// Import Node.js Dependencies
import dns from "node:dns/promises";
-export function capitalize(value: string): string {
- return `${value.charAt(0).toUpperCase()}${value.slice(1)}`;
-}
-
export async function dnsresolve(ip: string): Promise {
try {
const hostNames = await dns.reverse(ip);
diff --git a/src/morphix/src/functions/index.ts b/src/morphix/src/functions/index.ts
new file mode 100644
index 0000000..d3f5ef2
--- /dev/null
+++ b/src/morphix/src/functions/index.ts
@@ -0,0 +1,2 @@
+export { capitalize } from "./capitalize";
+export { dnsresolve } from "./dnsresolve";