@cenk1cenk2/md-printer 2.20.12 → 2.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/index.mjs +44 -3
- package/dist/interfaces/signature.interface.mjs +1 -0
- package/dist/services/index.mjs +5 -1
- package/dist/services/signature/adapters/a-trust.adapter.mjs +116 -0
- package/dist/services/signature/adapters/a-trust.html +77 -0
- package/dist/services/signature/index.mjs +5 -0
- package/dist/services/signature/signature.adapter.mjs +5 -0
- package/dist/services/signature/signature.service.mjs +97 -0
- package/oclif.manifest.json +21 -1
- package/package.json +8 -6
- package/templates/invoice/i18n/de.yaml +6 -0
- package/templates/invoice/i18n/en.yaml +6 -0
- package/templates/invoice/template.html.j2 +37 -0
package/dist/commands/index.mjs
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { DEFAULT_LANGUAGE, I18N_DIRECTORY, RequiredTemplateFiles, TEMPLATE_DIRECTORY, TemplateFiles } from "../constants/template.constants.mjs";
|
|
2
2
|
import { InputFileType, OutputFileType } from "../constants/file.constants.mjs";
|
|
3
3
|
import { I18nService } from "../services/i18n.service.mjs";
|
|
4
|
+
import { ATrustAdapter } from "../services/signature/adapters/a-trust.adapter.mjs";
|
|
5
|
+
import { SignatureService } from "../services/signature/signature.service.mjs";
|
|
4
6
|
import "../services/index.mjs";
|
|
5
7
|
import tailwind from "@tailwindcss/postcss";
|
|
6
8
|
import { watch } from "chokidar";
|
|
@@ -70,7 +72,10 @@ var MDPrinter = class extends Command {
|
|
|
70
72
|
char: "d",
|
|
71
73
|
description: "Run with Chrome browser instead of publishing the file."
|
|
72
74
|
}),
|
|
73
|
-
continuous: Flags.boolean({ description: "Render PDF as a single continuous page without page breaks." })
|
|
75
|
+
continuous: Flags.boolean({ description: "Render PDF as a single continuous page without page breaks." }),
|
|
76
|
+
sign: Flags.boolean({ description: "Sign the PDF with a qualified electronic signature through ID Austria (A-Trust)." }),
|
|
77
|
+
["sign-provider"]: Flags.string({ description: "Signature provider, defaults to a-trust. Overwrites the one defined in front-matter and template settings." }),
|
|
78
|
+
["sign-certificate"]: Flags.string({ description: "Signer certificate (DER or PEM) used for the visible signature block and to verify the returned signature." })
|
|
74
79
|
};
|
|
75
80
|
static args = {
|
|
76
81
|
file: Args.string({ description: "File to be processed." }),
|
|
@@ -88,17 +93,24 @@ var MDPrinter = class extends Command {
|
|
|
88
93
|
cs;
|
|
89
94
|
fs;
|
|
90
95
|
i18n;
|
|
96
|
+
signature;
|
|
91
97
|
browser;
|
|
92
98
|
register(cli) {
|
|
93
99
|
return {
|
|
94
100
|
...cli,
|
|
95
|
-
providers: [
|
|
101
|
+
providers: [
|
|
102
|
+
...cli.providers ?? [],
|
|
103
|
+
I18nService,
|
|
104
|
+
ATrustAdapter,
|
|
105
|
+
SignatureService
|
|
106
|
+
]
|
|
96
107
|
};
|
|
97
108
|
}
|
|
98
109
|
async shouldRunBefore() {
|
|
99
110
|
this.cs = this.app.get(ConfigService);
|
|
100
111
|
this.fs = this.app.get(FileSystemService);
|
|
101
112
|
this.i18n = this.app.get(I18nService);
|
|
113
|
+
this.signature = this.app.get(SignatureService);
|
|
102
114
|
await this.app.get(ParserService).register(JsonParser, YamlParser);
|
|
103
115
|
this.nunjucks.addFilter("markdown_to_html", (markdown) => {
|
|
104
116
|
return new showdown.Converter().makeHtml(markdown);
|
|
@@ -147,6 +159,7 @@ var MDPrinter = class extends Command {
|
|
|
147
159
|
ctx.options = await this.cs.extend([paths["settings.json"], {
|
|
148
160
|
dest: this.args?.output ?? ctx.metadata?.dest ?? `${basename(this.args.file, extname(this.args.file))}.${this.flags["output-filetype"]}`,
|
|
149
161
|
...title ? { document_title: title } : {},
|
|
162
|
+
...ctx.metadata?.sign ? { sign: ctx.metadata.sign } : {},
|
|
150
163
|
launch_options: { executablePath: this.flags.browser }
|
|
151
164
|
}]);
|
|
152
165
|
ctx.renderContinuous = this.flags.continuous || ctx.metadata?.continuous === true || ctx.options.continuous === true;
|
|
@@ -176,6 +189,7 @@ var MDPrinter = class extends Command {
|
|
|
176
189
|
ctx.options.css = await postcss([tailwind({ base: ctx.templates })]).process(await this.fs.read(paths["tailwind.css"]), { from: paths["tailwind.css"] }).then((result) => result.css);
|
|
177
190
|
}
|
|
178
191
|
await this.applyTranslations(ctx);
|
|
192
|
+
await this.applySignature(ctx);
|
|
179
193
|
await this.applyDocumentTitle(ctx);
|
|
180
194
|
} }]);
|
|
181
195
|
}
|
|
@@ -202,7 +216,8 @@ var MDPrinter = class extends Command {
|
|
|
202
216
|
context(ctx) {
|
|
203
217
|
const context = {
|
|
204
218
|
...ctx.metadata ?? {},
|
|
205
|
-
content: ctx.content
|
|
219
|
+
content: ctx.content,
|
|
220
|
+
signature: ctx.signature ?? null
|
|
206
221
|
};
|
|
207
222
|
if (ctx.t) {
|
|
208
223
|
context.t = ctx.t;
|
|
@@ -216,6 +231,21 @@ var MDPrinter = class extends Command {
|
|
|
216
231
|
ctx.options.document_title = this.nunjucks.renderString(title, this.context(ctx));
|
|
217
232
|
this.logger.info("Document title will be: %s", ctx.options.document_title);
|
|
218
233
|
}
|
|
234
|
+
async applySignature(ctx) {
|
|
235
|
+
ctx.options.sign = {
|
|
236
|
+
...ctx.options.sign,
|
|
237
|
+
...this.flags.sign ? { enabled: true } : {},
|
|
238
|
+
...this.flags["sign-provider"] ? { provider: this.flags["sign-provider"] } : {},
|
|
239
|
+
...this.flags["sign-certificate"] ? { certificate: this.flags["sign-certificate"] } : {}
|
|
240
|
+
};
|
|
241
|
+
if (!ctx.options.sign.enabled) return;
|
|
242
|
+
if (ctx.options.as_html) throw new Error("Signing is only supported for PDF output.");
|
|
243
|
+
this.signature.adapter(ctx.options.sign.provider);
|
|
244
|
+
ctx.signingTime = /* @__PURE__ */ new Date();
|
|
245
|
+
if (ctx.options.sign.certificate) ctx.certificate = await this.signature.certificate(ctx.options.sign.certificate, ctx.signingTime);
|
|
246
|
+
ctx.signature = this.signature.describe(ctx.signingTime, ctx.certificate);
|
|
247
|
+
this.logger.info("Document will be signed: %o", ctx.signature);
|
|
248
|
+
}
|
|
219
249
|
async applyTranslations(ctx) {
|
|
220
250
|
ctx.language = this.flags.language ?? ctx.metadata?.language ?? ctx.options.i18n?.language ?? "en";
|
|
221
251
|
ctx.t = await this.i18n.load(join(ctx.templates, I18N_DIRECTORY), {
|
|
@@ -294,6 +324,17 @@ var MDPrinter = class extends Command {
|
|
|
294
324
|
resolve(null);
|
|
295
325
|
}));
|
|
296
326
|
if (output) {
|
|
327
|
+
if (ctx.signature) {
|
|
328
|
+
if (this.flags.watch) this.logger.warn("Skipping the signature in watch mode, run without --watch to sign the document.");
|
|
329
|
+
else {
|
|
330
|
+
output.content = await this.signature.sign(Buffer.from(output.content), {
|
|
331
|
+
...ctx.options.sign,
|
|
332
|
+
date: ctx.signingTime,
|
|
333
|
+
certificate: ctx.certificate
|
|
334
|
+
});
|
|
335
|
+
this.logger.info("Document is signed.");
|
|
336
|
+
}
|
|
337
|
+
}
|
|
297
338
|
if (this.flags.stdout) {
|
|
298
339
|
process.stdout.write(output.content);
|
|
299
340
|
return;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { };
|
package/dist/services/index.mjs
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
1
|
import { I18nService } from "./i18n.service.mjs";
|
|
2
|
+
import { SignatureAdapter } from "./signature/signature.adapter.mjs";
|
|
3
|
+
import { ATrustAdapter } from "./signature/adapters/a-trust.adapter.mjs";
|
|
4
|
+
import { SignatureService } from "./signature/signature.service.mjs";
|
|
5
|
+
import "./signature/index.mjs";
|
|
2
6
|
|
|
3
|
-
export { I18nService };
|
|
7
|
+
export { ATrustAdapter, I18nService, SignatureAdapter, SignatureService };
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import __decorateMetadata from "../../../_virtual/_@oxc-project_runtime@0.153.0/helpers/esm/decorateMetadata.mjs";
|
|
2
|
+
import __decorate from "../../../_virtual/_@oxc-project_runtime@0.153.0/helpers/esm/decorate.mjs";
|
|
3
|
+
import { SignatureAdapter } from "../signature.adapter.mjs";
|
|
4
|
+
import { LoggerService } from "@cenk1cenk2/oclif-common";
|
|
5
|
+
import { Injectable } from "@nestjs/common";
|
|
6
|
+
import { spawn } from "child_process";
|
|
7
|
+
import { readFile } from "fs/promises";
|
|
8
|
+
import { createServer } from "http";
|
|
9
|
+
|
|
10
|
+
//#region src/services/signature/adapters/a-trust.adapter.ts
|
|
11
|
+
var _ATrustAdapter;
|
|
12
|
+
let ATrustAdapter = class ATrustAdapter extends SignatureAdapter {
|
|
13
|
+
static {
|
|
14
|
+
_ATrustAdapter = this;
|
|
15
|
+
}
|
|
16
|
+
logger;
|
|
17
|
+
static ENDPOINT = "https://service.a-trust.at/mobile/https-security-layer-request/default.aspx";
|
|
18
|
+
static NAMESPACE = "http://www.buergerkarte.at/namespaces/securitylayer/1.2#";
|
|
19
|
+
static TIMEOUT = 6e5;
|
|
20
|
+
static PAGE = "./a-trust.html";
|
|
21
|
+
name = "a-trust";
|
|
22
|
+
constructor(logger) {
|
|
23
|
+
super();
|
|
24
|
+
this.logger = logger;
|
|
25
|
+
this.logger.setup(this.constructor.name);
|
|
26
|
+
}
|
|
27
|
+
async sign(request) {
|
|
28
|
+
return this.collect(await this.session(this.request(request)));
|
|
29
|
+
}
|
|
30
|
+
request({ pdf, excluded: [from, to] }) {
|
|
31
|
+
const data = Buffer.from(pdf).fill(0, from, to + 1);
|
|
32
|
+
return [
|
|
33
|
+
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>",
|
|
34
|
+
`<sl:CreateCMSSignatureRequest xmlns:sl="${_ATrustAdapter.NAMESPACE}" Structure="detached" PAdESCompatibility="true">`,
|
|
35
|
+
"<sl:KeyboxIdentifier>SecureSignatureKeypair</sl:KeyboxIdentifier>",
|
|
36
|
+
"<sl:DataObject>",
|
|
37
|
+
"<sl:MetaInfo><sl:MimeType>application/pdf</sl:MimeType></sl:MetaInfo>",
|
|
38
|
+
`<sl:Content><sl:Base64Content>${data.toString("base64")}</sl:Base64Content></sl:Content>`,
|
|
39
|
+
`<sl:ExcludedByteRange><sl:From>${from}</sl:From><sl:To>${to}</sl:To></sl:ExcludedByteRange>`,
|
|
40
|
+
"</sl:DataObject>",
|
|
41
|
+
"</sl:CreateCMSSignatureRequest>"
|
|
42
|
+
].join("");
|
|
43
|
+
}
|
|
44
|
+
async session(request) {
|
|
45
|
+
let url = _ATrustAdapter.ENDPOINT;
|
|
46
|
+
let response = await fetch(url, {
|
|
47
|
+
method: "POST",
|
|
48
|
+
body: new URLSearchParams([["XMLRequest", request]]),
|
|
49
|
+
redirect: "manual"
|
|
50
|
+
});
|
|
51
|
+
while (response.status === 301 || response.status === 302) {
|
|
52
|
+
url = new URL(response.headers.get("location"), url).href;
|
|
53
|
+
response = await fetch(url, { redirect: "manual" });
|
|
54
|
+
}
|
|
55
|
+
if (!response.ok || !url.includes("/identification.aspx")) throw new Error(`A-Trust did not start a signing session, ended at ${url} with ${response.status}: ${(await response.text()).slice(0, 500)}`);
|
|
56
|
+
this.logger.debug("A-Trust signing session started: %s", url);
|
|
57
|
+
return url;
|
|
58
|
+
}
|
|
59
|
+
async collect(login) {
|
|
60
|
+
let resolve;
|
|
61
|
+
let reject;
|
|
62
|
+
const result = new Promise((done, fail) => {
|
|
63
|
+
resolve = done;
|
|
64
|
+
reject = fail;
|
|
65
|
+
});
|
|
66
|
+
const page = await readFile(new URL(_ATrustAdapter.PAGE, import.meta.url));
|
|
67
|
+
const server = createServer(async (req, res) => {
|
|
68
|
+
if (req.method === "GET" && req.url === "/session") {
|
|
69
|
+
res.setHeader("content-type", "application/json");
|
|
70
|
+
res.writeHead(200);
|
|
71
|
+
return res.end(JSON.stringify({ login }));
|
|
72
|
+
}
|
|
73
|
+
if (req.method !== "POST" || req.url !== "/signature") {
|
|
74
|
+
res.setHeader("content-type", "text/html; charset=utf-8");
|
|
75
|
+
res.writeHead(200);
|
|
76
|
+
return res.end(page);
|
|
77
|
+
}
|
|
78
|
+
let body = "";
|
|
79
|
+
for await (const chunk of req) body += chunk;
|
|
80
|
+
const cms = (body.match(/<(?:\w+:)?CMSSignature>([^<]+)</)?.[1] ?? body).replace(/\s+/g, "");
|
|
81
|
+
if (!/^MII[A-Za-z0-9+/=]+$/.test(cms)) {
|
|
82
|
+
res.writeHead(400);
|
|
83
|
+
return res.end("This does not look like a signature, it should start with MII.");
|
|
84
|
+
}
|
|
85
|
+
res.writeHead(200);
|
|
86
|
+
res.end("Signature received, you can close this tab.");
|
|
87
|
+
resolve(Buffer.from(cms, "base64"));
|
|
88
|
+
});
|
|
89
|
+
await new Promise((done) => server.listen(0, "127.0.0.1", done));
|
|
90
|
+
const url = `http://127.0.0.1:${server.address().port}/`;
|
|
91
|
+
this.logger.info("Sign the document in the browser, then paste the signature back: %s", url);
|
|
92
|
+
this.open(url);
|
|
93
|
+
const timeout = setTimeout(() => reject(/* @__PURE__ */ new Error("Timed out waiting for the signature.")), _ATrustAdapter.TIMEOUT);
|
|
94
|
+
return result.finally(() => {
|
|
95
|
+
clearTimeout(timeout);
|
|
96
|
+
server.close();
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
open(url) {
|
|
100
|
+
const [command, ...args] = process.platform === "darwin" ? ["open", url] : process.platform === "win32" ? [
|
|
101
|
+
"cmd",
|
|
102
|
+
"/c",
|
|
103
|
+
"start",
|
|
104
|
+
"",
|
|
105
|
+
url
|
|
106
|
+
] : ["xdg-open", url];
|
|
107
|
+
spawn(command, args, {
|
|
108
|
+
stdio: "ignore",
|
|
109
|
+
detached: true
|
|
110
|
+
}).on("error", (err) => this.logger.warn("Can not open the browser, open the link manually: %s", err.message)).unref();
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
ATrustAdapter = _ATrustAdapter = __decorate([Injectable(), __decorateMetadata("design:paramtypes", [typeof LoggerService === "undefined" ? Object : LoggerService])], ATrustAdapter);
|
|
114
|
+
|
|
115
|
+
//#endregion
|
|
116
|
+
export { ATrustAdapter };
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
|
+
<title>md-printer signature</title>
|
|
7
|
+
<style>
|
|
8
|
+
body {
|
|
9
|
+
font-family: system-ui, sans-serif;
|
|
10
|
+
margin: 0;
|
|
11
|
+
padding: 16px;
|
|
12
|
+
background: #f4f4f5;
|
|
13
|
+
color: #18181b;
|
|
14
|
+
}
|
|
15
|
+
main {
|
|
16
|
+
max-width: 560px;
|
|
17
|
+
margin: 0 auto;
|
|
18
|
+
display: grid;
|
|
19
|
+
gap: 12px;
|
|
20
|
+
}
|
|
21
|
+
iframe {
|
|
22
|
+
width: 100%;
|
|
23
|
+
height: 600px;
|
|
24
|
+
border: 0;
|
|
25
|
+
border-radius: 12px;
|
|
26
|
+
background: #fff;
|
|
27
|
+
}
|
|
28
|
+
textarea {
|
|
29
|
+
width: 100%;
|
|
30
|
+
height: 96px;
|
|
31
|
+
box-sizing: border-box;
|
|
32
|
+
border-radius: 8px;
|
|
33
|
+
padding: 8px;
|
|
34
|
+
font: 12px ui-monospace, monospace;
|
|
35
|
+
}
|
|
36
|
+
button {
|
|
37
|
+
width: 100%;
|
|
38
|
+
padding: 10px;
|
|
39
|
+
border: 0;
|
|
40
|
+
border-radius: 8px;
|
|
41
|
+
background: #18181b;
|
|
42
|
+
color: #fff;
|
|
43
|
+
font-weight: 600;
|
|
44
|
+
}
|
|
45
|
+
</style>
|
|
46
|
+
</head>
|
|
47
|
+
<body>
|
|
48
|
+
<main id="main">
|
|
49
|
+
<iframe id="login"></iframe>
|
|
50
|
+
<p>Log in and approve the signature. When the signature starting with <code>MII</code> shows up above, paste it here.</p>
|
|
51
|
+
<form id="form">
|
|
52
|
+
<textarea name="signature" placeholder="MII..." required></textarea>
|
|
53
|
+
<button>Sign PDF</button>
|
|
54
|
+
</form>
|
|
55
|
+
</main>
|
|
56
|
+
<script>
|
|
57
|
+
fetch('/session')
|
|
58
|
+
.then((response) => response.json())
|
|
59
|
+
.then(({ login }) => (document.getElementById('login').src = login))
|
|
60
|
+
|
|
61
|
+
document.getElementById('form').addEventListener('submit', async (event) => {
|
|
62
|
+
event.preventDefault()
|
|
63
|
+
|
|
64
|
+
const response = await fetch('/signature', { method: 'POST', body: new FormData(event.target).get('signature') })
|
|
65
|
+
const message = document.createElement('p')
|
|
66
|
+
|
|
67
|
+
message.textContent = await response.text()
|
|
68
|
+
|
|
69
|
+
if (response.ok) {
|
|
70
|
+
document.getElementById('main').replaceChildren(message)
|
|
71
|
+
} else {
|
|
72
|
+
event.target.append(message)
|
|
73
|
+
}
|
|
74
|
+
})
|
|
75
|
+
</script>
|
|
76
|
+
</body>
|
|
77
|
+
</html>
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import __decorateMetadata from "../../_virtual/_@oxc-project_runtime@0.153.0/helpers/esm/decorateMetadata.mjs";
|
|
2
|
+
import __decorate from "../../_virtual/_@oxc-project_runtime@0.153.0/helpers/esm/decorate.mjs";
|
|
3
|
+
import { ATrustAdapter } from "./adapters/a-trust.adapter.mjs";
|
|
4
|
+
import { isAbsolute, join } from "path";
|
|
5
|
+
import { FileSystemService } from "@cenk1cenk2/oclif-common";
|
|
6
|
+
import { Injectable } from "@nestjs/common";
|
|
7
|
+
import { plainAddPlaceholder } from "@signpdf/placeholder-plain";
|
|
8
|
+
import { SUBFILTER_ETSI_CADES_DETACHED, findByteRange, removeTrailingNewLine } from "@signpdf/utils";
|
|
9
|
+
import { X509Certificate } from "crypto";
|
|
10
|
+
import { homedir } from "os";
|
|
11
|
+
|
|
12
|
+
//#region src/services/signature/signature.service.ts
|
|
13
|
+
var _SignatureService;
|
|
14
|
+
let SignatureService = class SignatureService {
|
|
15
|
+
static {
|
|
16
|
+
_SignatureService = this;
|
|
17
|
+
}
|
|
18
|
+
fs;
|
|
19
|
+
static LENGTH = 16384;
|
|
20
|
+
adapters;
|
|
21
|
+
constructor(fs, aTrust) {
|
|
22
|
+
this.fs = fs;
|
|
23
|
+
this.adapters = [aTrust];
|
|
24
|
+
}
|
|
25
|
+
get providers() {
|
|
26
|
+
return this.adapters.map((adapter) => adapter.name);
|
|
27
|
+
}
|
|
28
|
+
async certificate(file, date) {
|
|
29
|
+
const path = file.replace(/^~(?=\/|$)/, homedir());
|
|
30
|
+
const certificate = new X509Certificate(await this.fs.extra.readFile(isAbsolute(path) ? path : join(process.cwd(), path)));
|
|
31
|
+
if (date < certificate.validFromDate || date > certificate.validToDate) throw new Error(`Signing certificate is not valid at ${date.toISOString()}, it is valid from ${certificate.validFrom} to ${certificate.validTo}.`);
|
|
32
|
+
return certificate;
|
|
33
|
+
}
|
|
34
|
+
describe(date, certificate) {
|
|
35
|
+
return {
|
|
36
|
+
date: date.toISOString().replace("T", " ").replace(/\.\d+Z$/, " UTC"),
|
|
37
|
+
name: certificate && this.commonName(certificate.subject),
|
|
38
|
+
issuer: certificate && this.commonName(certificate.issuer),
|
|
39
|
+
serial: certificate?.serialNumber
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
async sign(pdf, options) {
|
|
43
|
+
const request = this.prepare(pdf, options);
|
|
44
|
+
const cms = await this.adapter(options.provider).sign(request);
|
|
45
|
+
if (options.certificate && !cms.includes(options.certificate.raw)) throw new Error("The returned signature was not created with the configured certificate, the signature block would show the wrong signer.");
|
|
46
|
+
return this.embed(request, cms);
|
|
47
|
+
}
|
|
48
|
+
adapter(provider) {
|
|
49
|
+
if (!provider) return this.adapters[0];
|
|
50
|
+
const adapter = this.adapters.find((adapter) => adapter.name === provider);
|
|
51
|
+
if (!adapter) throw new Error(`Unknown signature provider "${provider}", available: ${this.providers.join(", ")}`);
|
|
52
|
+
return adapter;
|
|
53
|
+
}
|
|
54
|
+
prepare(pdf, options) {
|
|
55
|
+
const prepared = removeTrailingNewLine(plainAddPlaceholder({
|
|
56
|
+
pdfBuffer: pdf,
|
|
57
|
+
reason: options.reason ?? "",
|
|
58
|
+
location: options.location ?? "",
|
|
59
|
+
contactInfo: options.contact ?? "",
|
|
60
|
+
name: "",
|
|
61
|
+
signingTime: options.date,
|
|
62
|
+
signatureLength: _SignatureService.LENGTH,
|
|
63
|
+
subFilter: SUBFILTER_ETSI_CADES_DETACHED,
|
|
64
|
+
appName: "md-printer"
|
|
65
|
+
}));
|
|
66
|
+
const { byteRangePlaceholder, byteRangePlaceholderPosition } = findByteRange(prepared);
|
|
67
|
+
if (!byteRangePlaceholder) throw new Error("Can not find the signature byte range placeholder in the PDF.");
|
|
68
|
+
const byteRangeEnd = byteRangePlaceholderPosition + byteRangePlaceholder.length;
|
|
69
|
+
const from = prepared.indexOf("<", prepared.indexOf("/Contents ", byteRangeEnd));
|
|
70
|
+
const to = prepared.indexOf(">", from);
|
|
71
|
+
return {
|
|
72
|
+
pdf: Buffer.concat([
|
|
73
|
+
prepared.subarray(0, byteRangePlaceholderPosition),
|
|
74
|
+
Buffer.from(`/ByteRange [0 ${from} ${to + 1} ${prepared.length - to - 1}]`.padEnd(byteRangePlaceholder.length, " ")),
|
|
75
|
+
prepared.subarray(byteRangeEnd)
|
|
76
|
+
]),
|
|
77
|
+
excluded: [from, to]
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
embed({ pdf, excluded: [from, to] }, cms) {
|
|
81
|
+
const signature = cms.toString("hex");
|
|
82
|
+
const length = to - from - 1;
|
|
83
|
+
if (signature.length > length) throw new Error(`Signature is ${signature.length} bytes but the placeholder only fits ${length}.`);
|
|
84
|
+
return Buffer.concat([
|
|
85
|
+
pdf.subarray(0, from + 1),
|
|
86
|
+
Buffer.from(signature.padEnd(length, "0")),
|
|
87
|
+
pdf.subarray(to)
|
|
88
|
+
]);
|
|
89
|
+
}
|
|
90
|
+
commonName(name) {
|
|
91
|
+
return name.split("\n").find((part) => part.startsWith("CN="))?.slice(3);
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
SignatureService = _SignatureService = __decorate([Injectable(), __decorateMetadata("design:paramtypes", [typeof FileSystemService === "undefined" ? Object : FileSystemService, typeof ATrustAdapter === "undefined" ? Object : ATrustAdapter])], SignatureService);
|
|
95
|
+
|
|
96
|
+
//#endregion
|
|
97
|
+
export { SignatureService };
|
package/oclif.manifest.json
CHANGED
|
@@ -153,6 +153,26 @@
|
|
|
153
153
|
"name": "continuous",
|
|
154
154
|
"allowNo": false,
|
|
155
155
|
"type": "boolean"
|
|
156
|
+
},
|
|
157
|
+
"sign": {
|
|
158
|
+
"description": "Sign the PDF with a qualified electronic signature through ID Austria (A-Trust).",
|
|
159
|
+
"name": "sign",
|
|
160
|
+
"allowNo": false,
|
|
161
|
+
"type": "boolean"
|
|
162
|
+
},
|
|
163
|
+
"sign-provider": {
|
|
164
|
+
"description": "Signature provider, defaults to a-trust. Overwrites the one defined in front-matter and template settings.",
|
|
165
|
+
"name": "sign-provider",
|
|
166
|
+
"hasDynamicHelp": false,
|
|
167
|
+
"multiple": false,
|
|
168
|
+
"type": "option"
|
|
169
|
+
},
|
|
170
|
+
"sign-certificate": {
|
|
171
|
+
"description": "Signer certificate (DER or PEM) used for the visible signature block and to verify the returned signature.",
|
|
172
|
+
"name": "sign-certificate",
|
|
173
|
+
"hasDynamicHelp": false,
|
|
174
|
+
"multiple": false,
|
|
175
|
+
"type": "option"
|
|
156
176
|
}
|
|
157
177
|
},
|
|
158
178
|
"hasDynamicHelp": false,
|
|
@@ -165,5 +185,5 @@
|
|
|
165
185
|
"enableJsonFlag": false
|
|
166
186
|
}
|
|
167
187
|
},
|
|
168
|
-
"version": "2.
|
|
188
|
+
"version": "2.21.0"
|
|
169
189
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cenk1cenk2/md-printer",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.21.0",
|
|
4
4
|
"description": "A markdown printer.",
|
|
5
5
|
"module": "dist/index.mjs",
|
|
6
6
|
"type": "module",
|
|
@@ -50,10 +50,12 @@
|
|
|
50
50
|
],
|
|
51
51
|
"dependencies": {
|
|
52
52
|
"@cenk1cenk2/oclif-common": "^7.1.0",
|
|
53
|
-
"@listr2/manager": "^4.3.
|
|
53
|
+
"@listr2/manager": "^4.3.1",
|
|
54
54
|
"@nestjs/common": "^12.1.2",
|
|
55
55
|
"@oclif/core": "^5.1.2",
|
|
56
56
|
"@oclif/plugin-help": "^7.0.2",
|
|
57
|
+
"@signpdf/placeholder-plain": "^3.3.0",
|
|
58
|
+
"@signpdf/utils": "^3.3.0",
|
|
57
59
|
"@tailwindcss/forms": "^0.5.11",
|
|
58
60
|
"@tailwindcss/postcss": "^4.3.3",
|
|
59
61
|
"@tailwindcss/typography": "^0.5.20",
|
|
@@ -65,7 +67,7 @@
|
|
|
65
67
|
"listr2": "^11.2.0",
|
|
66
68
|
"md-to-pdf": "^5.2.5",
|
|
67
69
|
"nunjucks": "^3.2.4",
|
|
68
|
-
"postcss": "^8.5.
|
|
70
|
+
"postcss": "^8.5.29",
|
|
69
71
|
"puppeteer": "^25.12.0",
|
|
70
72
|
"rxjs": "^7.8.2",
|
|
71
73
|
"showdown": "^2.1.0",
|
|
@@ -74,15 +76,15 @@
|
|
|
74
76
|
"yaml": "^2.9.1"
|
|
75
77
|
},
|
|
76
78
|
"devDependencies": {
|
|
77
|
-
"@cenk1cenk2/cz-cc": "^3.0.
|
|
78
|
-
"@cenk1cenk2/eslint-config": "^3.3.
|
|
79
|
+
"@cenk1cenk2/cz-cc": "^3.0.6",
|
|
80
|
+
"@cenk1cenk2/eslint-config": "^3.3.9",
|
|
79
81
|
"@tsconfig/node24": "^24.0.5",
|
|
80
82
|
"@types/fs-extra": "^11.0.4",
|
|
81
83
|
"@types/node": "^24.19.1",
|
|
82
84
|
"@types/nunjucks": "^3.2.6",
|
|
83
85
|
"@types/showdown": "^2.0.6",
|
|
84
86
|
"eslint": "^10.12.0",
|
|
85
|
-
"execa": "^10.0
|
|
87
|
+
"execa": "^10.1.0",
|
|
86
88
|
"globby": "^16.2.4",
|
|
87
89
|
"oclif": "^6.0.2",
|
|
88
90
|
"prettier": "^3.9.9",
|
|
@@ -8,6 +8,12 @@ sections:
|
|
|
8
8
|
notes: NOTIZEN
|
|
9
9
|
invoice: RECHNUNG
|
|
10
10
|
signature: UNTERSCHRIFT
|
|
11
|
+
signed:
|
|
12
|
+
title: Digital signiert
|
|
13
|
+
by: Unterzeichner
|
|
14
|
+
date: Datum
|
|
15
|
+
issuer: Zertifikat
|
|
16
|
+
verify: Dieses Dokument ist qualifiziert elektronisch signiert. Prüfung unter
|
|
11
17
|
fields:
|
|
12
18
|
date: Datum
|
|
13
19
|
id: Rechnungsnr.
|
|
@@ -8,6 +8,12 @@ sections:
|
|
|
8
8
|
notes: NOTES
|
|
9
9
|
invoice: INVOICE
|
|
10
10
|
signature: SIGNATURE
|
|
11
|
+
signed:
|
|
12
|
+
title: Digitally signed
|
|
13
|
+
by: Signed by
|
|
14
|
+
date: Date
|
|
15
|
+
issuer: Certificate
|
|
16
|
+
verify: This document carries a qualified electronic signature. Verify it at
|
|
11
17
|
fields:
|
|
12
18
|
date: Date
|
|
13
19
|
id: Invoice#
|
|
@@ -178,10 +178,47 @@
|
|
|
178
178
|
{{ content | safe }}<br />
|
|
179
179
|
</div>
|
|
180
180
|
<div class="col-span-1"></div>
|
|
181
|
+
{% if signature %}
|
|
182
|
+
<div class="col-span-5 flex flex-col justify-center m-8 mr-16">
|
|
183
|
+
<div class="w-full overflow-hidden rounded-xl border-y-2 shadow-md shadow-gray-300 border-gray-900! text-xs">
|
|
184
|
+
<div class="flex items-center gap-3 px-5 py-3 bg-gray-900 text-white">
|
|
185
|
+
<svg class="w-5 h-5 shrink-0" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
|
|
186
|
+
stroke-linecap="round" stroke-linejoin="round">
|
|
187
|
+
<path d="M12 3l7 3v6c0 4.5-3 7.5-7 9-4-1.5-7-4.5-7-9V6l7-3z" />
|
|
188
|
+
<path d="M9 12l2 2 4-4" />
|
|
189
|
+
</svg>
|
|
190
|
+
<span class="font-extrabold uppercase tracking-wider">{{ t('signed.title') }}</span>
|
|
191
|
+
</div>
|
|
192
|
+
<div class="flex flex-col px-5 py-2">
|
|
193
|
+
{% if signature.name %}
|
|
194
|
+
<div class="flex justify-between gap-4 py-2">
|
|
195
|
+
<span class="font-semibold text-gray-400">{{ t('signed.by') }}</span>
|
|
196
|
+
<span class="font-semibold text-right">{{ signature.name }}</span>
|
|
197
|
+
</div>
|
|
198
|
+
{% endif %}
|
|
199
|
+
<div class="flex justify-between gap-4 py-2 border-t border-t-gray-100/30">
|
|
200
|
+
<span class="font-semibold text-gray-400">{{ t('signed.date') }}</span>
|
|
201
|
+
<span class="text-right">{{ signature.date }}</span>
|
|
202
|
+
</div>
|
|
203
|
+
{% if signature.issuer %}
|
|
204
|
+
<div class="flex justify-between gap-4 py-2 border-t border-t-gray-100/30">
|
|
205
|
+
<span class="font-semibold text-gray-400">{{ t('signed.issuer') }}</span>
|
|
206
|
+
<span class="text-right text-[0.6rem]">{{ signature.issuer }} · {{ signature.serial }}</span>
|
|
207
|
+
</div>
|
|
208
|
+
{% endif %}
|
|
209
|
+
</div>
|
|
210
|
+
<p class="px-5 py-3 bg-gray-50 text-[0.6rem] text-gray-400 leading-snug">
|
|
211
|
+
{{ t('signed.verify') }}
|
|
212
|
+
<a href="https://www.signaturpruefung.gv.at" class="font-semibold text-gray-900 underline">signaturpruefung.gv.at</a>
|
|
213
|
+
</p>
|
|
214
|
+
</div>
|
|
215
|
+
</div>
|
|
216
|
+
{% else %}
|
|
181
217
|
<div
|
|
182
218
|
class="col-span-5 flex flex-col justify-end text-center m-8 mr-16 p-4 pb-0 border-2 border-gray-50! rounded-xl text-xs text-gray-100 h-36">
|
|
183
219
|
<p>{{ t('signature') }}</p>
|
|
184
220
|
</div>
|
|
221
|
+
{% endif %}
|
|
185
222
|
</section>
|
|
186
223
|
</div>
|
|
187
224
|
</body>
|