Skip to content

Commit 0dc2461

Browse files
committed
Update plugin
1 parent 6583ef4 commit 0dc2461

17 files changed

Lines changed: 5635 additions & 545 deletions

.github/workflows/build.yml

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
name: Publish image with plugin
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
workflow_dispatch:
8+
9+
jobs:
10+
push-plugin:
11+
runs-on: ubuntu-latest
12+
steps:
13+
- uses: neoforged/actions/checkout@main
14+
15+
- name: Use Node.js
16+
uses: actions/setup-node@v4
17+
with:
18+
node-version: 22
19+
cache: 'npm'
20+
21+
- name: Install NPM Dependencies
22+
run: npm ci
23+
24+
- name: Build Plugin
25+
run: npm run build
26+
27+
- name: Setup Docker Buildx
28+
uses: docker/setup-buildx-action@v3
29+
30+
- name: Login to GitHub Container Registry
31+
uses: docker/login-action@v3
32+
with:
33+
registry: ghcr.io
34+
username: ${{ github.actor }}
35+
password: ${{ github.token }}
36+
37+
- name: Create Docker Metadata
38+
id: docker_metadata
39+
uses: docker/metadata-action@v5
40+
with:
41+
images: ghcr.io/${{ github.repository }}
42+
# custom latest tag, the rest are default values
43+
tags: |
44+
type=raw,value=latest,enable={{is_default_branch}}
45+
type=schedule
46+
type=ref,event=branch
47+
type=ref,event=tag
48+
type=ref,event=pr
49+
50+
- name: Build Docker Image
51+
uses: docker/build-push-action@v6
52+
with:
53+
context: .
54+
push: true
55+
cache-from: type=gha
56+
cache-to: type=gha,mode=max
57+
tags: ${{ steps.docker_metadata.outputs.tags }}
58+
labels: ${{ steps.docker_metadata.outputs.labels }}

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,4 @@ dist
66
npm-debug.log*
77
yarn.lock
88
.vscode/launch.json
9+
.vscode

Dockerfile

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
FROM n8nio/n8n:1.98.0
2+
USER root
3+
COPY ./ /home/node/.n8n/custom/node_modules/neoforged-n8n-plugin
4+
USER node
5+
WORKDIR /home/node

credentials/ExampleCredentialsApi.credentials.ts

Lines changed: 0 additions & 59 deletions
This file was deleted.
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
import {
2+
ICredentialDataDecryptedObject,
3+
ICredentialType,
4+
IHttpRequestOptions,
5+
INodeProperties,
6+
} from 'n8n-workflow';
7+
import jwt from 'jsonwebtoken'
8+
import { Octokit } from '@octokit/rest';
9+
10+
class AsyncCache<K, V> {
11+
private cache: Map<K, {
12+
value: V,
13+
expiry: number
14+
}>;
15+
16+
constructor() {
17+
this.cache = new Map();
18+
}
19+
20+
async get(key: K, creator: () => Promise<V>, timeout = 5000) {
21+
const now = Date.now();
22+
23+
if (this.cache.has(key)) {
24+
const { value, expiry } = this.cache.get(key)!
25+
if (expiry > now) {
26+
return value;
27+
}
28+
this.cache.delete(key)
29+
}
30+
31+
const value = await creator()
32+
this.cache.set(key, { value, expiry: now + timeout });
33+
return value;
34+
}
35+
}
36+
37+
const tokenCache = new AsyncCache<string, string>()
38+
39+
export async function genToken(creds: ICredentialDataDecryptedObject, forceGenerate: boolean = false, ovInstallation: string | undefined = undefined): Promise<string> {
40+
const key = creds['key'] as string
41+
const appid = creds['appid'] as number
42+
const installation = ovInstallation ?? creds['installation'] as string
43+
const generator = async () => {
44+
console.log('Generating GH application token for ' + appid)
45+
46+
const now = Math.floor(Date.now() / 1000)
47+
48+
const octo = new Octokit({
49+
auth: jwt.sign({
50+
iss: appid.toString(),
51+
iat: now,
52+
exp: now + 599
53+
}, formatPrivateKey(key), {
54+
algorithm: 'RS256'
55+
})
56+
})
57+
58+
let install: number
59+
60+
if (isNaN(Number(installation))) {
61+
if (installation.includes('/')) {
62+
const split = installation.split('/')
63+
install = (await octo.apps.getRepoInstallation({owner: split[0], repo: split[1]})).data.id
64+
} else {
65+
install = (await octo.apps.getOrgInstallation({org: installation})).data.id
66+
}
67+
} else {
68+
install = parseInt(installation)
69+
}
70+
return (await octo.apps.createInstallationAccessToken({
71+
installation_id: install
72+
})).data.token
73+
}
74+
if (forceGenerate) return await generator()
75+
return await tokenCache.get(JSON.stringify([key, appid, installation]), generator, 45 * 60 * 1000) // 45 minutes
76+
}
77+
78+
export class GitHubAppApi implements ICredentialType {
79+
name = 'githubappApi';
80+
displayName = 'GitHub Application API';
81+
documentationUrl = 'https://docs.github.com/en/apps';
82+
icon = { light: 'file:ghapp.svg', dark: 'file:ghapp.svg'} as const
83+
properties: INodeProperties[] = [
84+
{
85+
displayName: 'Private key',
86+
description: 'The application private key',
87+
name: 'key',
88+
type: 'string',
89+
default: '',
90+
required: true,
91+
typeOptions: {
92+
password: true,
93+
}
94+
},
95+
{
96+
displayName: 'Application ID',
97+
description: 'The ID of the GitHub application',
98+
name: 'appid',
99+
type: 'number',
100+
default: 0,
101+
required: true
102+
},
103+
{
104+
displayName: 'Installation',
105+
description: 'The installation (organisation or repository) to generate application tokens for by default',
106+
name: 'installation',
107+
type: 'string',
108+
default: ''
109+
}
110+
];
111+
112+
113+
authenticate = async (creds: ICredentialDataDecryptedObject, req: IHttpRequestOptions): Promise<IHttpRequestOptions> => {
114+
return {
115+
...req,
116+
headers: {
117+
...req.headers,
118+
'Authorization': `Bearer ${await genToken(creds)}`
119+
}
120+
}
121+
}
122+
}
123+
124+
export function formatPrivateKey(privateKey: string): string {
125+
let regex = /(PRIVATE KEY|CERTIFICATE)/;
126+
if (!privateKey || /\n/.test(privateKey)) {
127+
return privateKey;
128+
}
129+
let formattedPrivateKey = '';
130+
const parts = privateKey.split('-----').filter((item) => item !== '');
131+
parts.forEach((part) => {
132+
if (regex.test(part)) {
133+
formattedPrivateKey += `-----${part}-----`;
134+
} else {
135+
const passRegex = /Proc-Type|DEK-Info/;
136+
if (passRegex.test(part)) {
137+
part = part.replace(/:\s+/g, ':');
138+
formattedPrivateKey += part.replace(/\\n/g, '\n').replace(/\s+/g, '\n');
139+
} else {
140+
formattedPrivateKey += part.replace(/\\n/g, '\n').replace(/\s+/g, '\n');
141+
}
142+
}
143+
});
144+
return formattedPrivateKey;
145+
}

credentials/HttpBinApi.credentials.ts

Lines changed: 0 additions & 50 deletions
This file was deleted.

credentials/ghapp.svg

Lines changed: 31 additions & 0 deletions
Loading

0 commit comments

Comments
 (0)