layoutbridge-spm 0.0.1 → 1.0.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/CHANGELOG.md +171 -0
- package/LICENSE +26 -0
- package/README.de.md +318 -0
- package/README.md +304 -4
- package/THIRD-PARTY-NOTICES.md +2878 -0
- package/dist/src/mcp/config.js +125 -0
- package/dist/src/mcp/dispatch.js +123 -0
- package/dist/src/mcp/license/free-tools.js +16 -0
- package/dist/src/mcp/license/gate.js +209 -0
- package/dist/src/mcp/license/index.js +96 -0
- package/dist/src/mcp/license/instance-name.js +56 -0
- package/dist/src/mcp/license/key-source.js +58 -0
- package/dist/src/mcp/license/lemonsqueezy-verifier.js +207 -0
- package/dist/src/mcp/license/placeholder-verifier.js +10 -0
- package/dist/src/mcp/license/polar-ids.js +63 -0
- package/dist/src/mcp/license/polar-verifier.js +265 -0
- package/dist/src/mcp/license/product-ids.js +72 -0
- package/dist/src/mcp/license/state-store.js +91 -0
- package/dist/src/mcp/license/types.js +11 -0
- package/dist/src/mcp/logging.js +270 -0
- package/dist/src/mcp/product-info.js +48 -0
- package/dist/src/mcp/server.js +250 -0
- package/dist/src/mcp/setup.js +608 -0
- package/dist/src/mcp/start.cjs +95 -0
- package/dist/src/mcp/tool-definition.js +2 -0
- package/dist/src/mcp/tools.js +2004 -0
- package/dist/src/mcp/typography-rules.js +70 -0
- package/dist/src/transport/com.js +250 -0
- package/dist/src/transport/osascript.js +171 -0
- package/dist/src/transport/queue.js +19 -0
- package/dist/src/transport/script-bundle.js +92 -0
- package/dist/src/transport/transport.js +30 -0
- package/indesign/commands/10-app.js +6 -0
- package/indesign/commands/20-documents.js +513 -0
- package/indesign/commands/22-document-setup.js +118 -0
- package/indesign/commands/23-save-package.js +255 -0
- package/indesign/commands/25-export.js +379 -0
- package/indesign/commands/26-export-image.js +197 -0
- package/indesign/commands/27-textdefaults.js +151 -0
- package/indesign/commands/28-selection.js +174 -0
- package/indesign/commands/30-pages.js +1032 -0
- package/indesign/commands/31-view.js +210 -0
- package/indesign/commands/32-sections.js +203 -0
- package/indesign/commands/35-parents.js +428 -0
- package/indesign/commands/37-layout.js +760 -0
- package/indesign/commands/40-frames.js +836 -0
- package/indesign/commands/41-shapes.js +505 -0
- package/indesign/commands/42-groups.js +117 -0
- package/indesign/commands/43-layers.js +291 -0
- package/indesign/commands/44-effects.js +274 -0
- package/indesign/commands/45-frame-options.js +247 -0
- package/indesign/commands/47-anchored.js +287 -0
- package/indesign/commands/50-modify.js +781 -0
- package/indesign/commands/52-align.js +139 -0
- package/indesign/commands/55-fit.js +106 -0
- package/indesign/commands/60-images.js +771 -0
- package/indesign/commands/62-embed.js +120 -0
- package/indesign/commands/69-placeholders.js +127 -0
- package/indesign/commands/70-text.js +567 -0
- package/indesign/commands/71-outlines.js +92 -0
- package/indesign/commands/72-place-text.js +366 -0
- package/indesign/commands/74-stories.js +116 -0
- package/indesign/commands/75-tables.js +676 -0
- package/indesign/commands/76-text-variables.js +260 -0
- package/indesign/commands/77-story-options.js +86 -0
- package/indesign/commands/78-frame-lines.js +304 -0
- package/indesign/commands/79-check-typography.js +303 -0
- package/indesign/commands/80-styles.js +1620 -0
- package/indesign/commands/81-composition.js +355 -0
- package/indesign/commands/82-object-styles.js +263 -0
- package/indesign/commands/83-indents-tabs.js +237 -0
- package/indesign/commands/84-import-styles.js +142 -0
- package/indesign/commands/85-swatches.js +112 -0
- package/indesign/commands/86-gradients.js +185 -0
- package/indesign/commands/87-replace-swatch.js +180 -0
- package/indesign/commands/88-replace-font.js +217 -0
- package/indesign/interpreter.js +758 -0
- package/package.json +55 -6
- package/rules/typography-rules.json +31 -0
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Serverkonfiguration (E-54): Hauswerte, die nicht in die Werkzeug-
|
|
3
|
+
* beschreibungen gehoeren – Toleranzen, Preset-Namen, Schwellen. Eine
|
|
4
|
+
* JSON-Datei, einmal beim Start gelesen. Ein Parameter im Aufruf geht
|
|
5
|
+
* immer vor einem konfigurierten Wert.
|
|
6
|
+
*
|
|
7
|
+
* Ort der Datei: Umgebungsvariable LAYOUTBRIDGE_SPM_CONFIG (voller Pfad),
|
|
8
|
+
* sonst macOS ~/Library/Application Support/layoutbridge-spm/config.json,
|
|
9
|
+
* sonst ~/.layoutbridge-spm/config.json. Fehlt die Datei, gilt keine
|
|
10
|
+
* Konfiguration – der Server laeuft mit den neutralen Vorgaben.
|
|
11
|
+
*/
|
|
12
|
+
import { readFileSync } from 'node:fs';
|
|
13
|
+
import { homedir, platform } from 'node:os';
|
|
14
|
+
import { join } from 'node:path';
|
|
15
|
+
const KNOWN_KEYS = ['tracking_warn_beyond', 'min_ppi', 'export_pdf'];
|
|
16
|
+
const KNOWN_PDF_KEYS = ['preset', 'fallback_preset', 'fallback_over_bytes'];
|
|
17
|
+
export function defaultConfigPath(env = process.env, os = platform(), home = homedir()) {
|
|
18
|
+
const fromEnv = env['LAYOUTBRIDGE_SPM_CONFIG'];
|
|
19
|
+
if (fromEnv !== undefined && fromEnv.length > 0)
|
|
20
|
+
return fromEnv;
|
|
21
|
+
if (os === 'darwin') {
|
|
22
|
+
return join(home, 'Library', 'Application Support', 'layoutbridge-spm', 'config.json');
|
|
23
|
+
}
|
|
24
|
+
return join(home, '.layoutbridge-spm', 'config.json');
|
|
25
|
+
}
|
|
26
|
+
function isRecord(value) {
|
|
27
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
28
|
+
}
|
|
29
|
+
function takeNumber(source, key, min, problems, prefix) {
|
|
30
|
+
const value = source[key];
|
|
31
|
+
if (value === undefined)
|
|
32
|
+
return undefined;
|
|
33
|
+
if (typeof value !== 'number' || !Number.isFinite(value) || value < min) {
|
|
34
|
+
problems.push(`${prefix}${key}: expected a number of ${min} or more, got ${JSON.stringify(value)}`);
|
|
35
|
+
return undefined;
|
|
36
|
+
}
|
|
37
|
+
return value;
|
|
38
|
+
}
|
|
39
|
+
function takeString(source, key, problems, prefix) {
|
|
40
|
+
const value = source[key];
|
|
41
|
+
if (value === undefined)
|
|
42
|
+
return undefined;
|
|
43
|
+
if (typeof value !== 'string') {
|
|
44
|
+
problems.push(`${prefix}${key}: expected a string, got ${JSON.stringify(value)}`);
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
return value;
|
|
48
|
+
}
|
|
49
|
+
/** Text einer Konfigurationsdatei pruefen. Unbekannte Schluessel werden gemeldet und ignoriert. */
|
|
50
|
+
export function parseServerConfig(text) {
|
|
51
|
+
const problems = [];
|
|
52
|
+
let raw;
|
|
53
|
+
try {
|
|
54
|
+
raw = JSON.parse(text);
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
return { config: {}, problems: ['not valid JSON: ' + String(error)] };
|
|
58
|
+
}
|
|
59
|
+
if (!isRecord(raw)) {
|
|
60
|
+
return { config: {}, problems: ['the top level must be a JSON object'] };
|
|
61
|
+
}
|
|
62
|
+
const config = {};
|
|
63
|
+
for (const key of Object.keys(raw)) {
|
|
64
|
+
if (!KNOWN_KEYS.includes(key))
|
|
65
|
+
problems.push(`unknown key "${key}" ignored`);
|
|
66
|
+
}
|
|
67
|
+
const tracking = takeNumber(raw, 'tracking_warn_beyond', 0, problems, '');
|
|
68
|
+
if (tracking !== undefined)
|
|
69
|
+
config.tracking_warn_beyond = tracking;
|
|
70
|
+
const minPpi = takeNumber(raw, 'min_ppi', 1, problems, '');
|
|
71
|
+
if (minPpi !== undefined)
|
|
72
|
+
config.min_ppi = minPpi;
|
|
73
|
+
if (raw['export_pdf'] !== undefined) {
|
|
74
|
+
const pdf = raw['export_pdf'];
|
|
75
|
+
if (!isRecord(pdf)) {
|
|
76
|
+
problems.push('export_pdf: expected an object');
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
for (const key of Object.keys(pdf)) {
|
|
80
|
+
if (!KNOWN_PDF_KEYS.includes(key))
|
|
81
|
+
problems.push(`unknown key "export_pdf.${key}" ignored`);
|
|
82
|
+
}
|
|
83
|
+
const out = {};
|
|
84
|
+
const preset = takeString(pdf, 'preset', problems, 'export_pdf.');
|
|
85
|
+
if (preset !== undefined)
|
|
86
|
+
out.preset = preset;
|
|
87
|
+
const fallback = takeString(pdf, 'fallback_preset', problems, 'export_pdf.');
|
|
88
|
+
if (fallback !== undefined)
|
|
89
|
+
out.fallback_preset = fallback;
|
|
90
|
+
const bytes = takeNumber(pdf, 'fallback_over_bytes', 0, problems, 'export_pdf.');
|
|
91
|
+
if (bytes !== undefined)
|
|
92
|
+
out.fallback_over_bytes = bytes;
|
|
93
|
+
config.export_pdf = out;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return { config, problems };
|
|
97
|
+
}
|
|
98
|
+
/** Konfigurationsdatei lesen. Keine Datei = leere Konfiguration, kein Fehler. */
|
|
99
|
+
export function loadServerConfig(path = defaultConfigPath()) {
|
|
100
|
+
let text;
|
|
101
|
+
try {
|
|
102
|
+
text = readFileSync(path, 'utf8');
|
|
103
|
+
}
|
|
104
|
+
catch (error) {
|
|
105
|
+
const code = error.code;
|
|
106
|
+
if (code === 'ENOENT')
|
|
107
|
+
return { config: {}, path: null, problems: [] };
|
|
108
|
+
return { config: {}, path, problems: ['could not read ' + path + ': ' + String(error)] };
|
|
109
|
+
}
|
|
110
|
+
const parsed = parseServerConfig(text);
|
|
111
|
+
return { config: parsed.config, path, problems: parsed.problems };
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Konfigurierte Vorgaben in die Befehlsargumente legen – nur dort, wo der
|
|
115
|
+
* Aufruf selbst nichts gesagt hat. Felder ohne Wert werden nicht gesetzt.
|
|
116
|
+
*/
|
|
117
|
+
export function applyConfigDefaults(args, defaults) {
|
|
118
|
+
const out = { ...args };
|
|
119
|
+
for (const key of Object.keys(defaults)) {
|
|
120
|
+
if (out[key] === undefined && defaults[key] !== undefined)
|
|
121
|
+
out[key] = defaults[key];
|
|
122
|
+
}
|
|
123
|
+
return out;
|
|
124
|
+
}
|
|
125
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bindeglied: geprueften Parametersatz in einen Befehl uebersetzen, ueber den
|
|
3
|
+
* Transport schicken, protokollieren, Ergebnisumschlag zurueckgeben.
|
|
4
|
+
* Kennt InDesign nicht, kennt den Transport nur als Schnittstelle.
|
|
5
|
+
*/
|
|
6
|
+
import { homedir } from 'node:os';
|
|
7
|
+
import { z } from 'zod';
|
|
8
|
+
import { applyConfigDefaults } from './config.js';
|
|
9
|
+
import { maskHome } from './logging.js';
|
|
10
|
+
import { transportError, } from '../transport/transport.js';
|
|
11
|
+
/** Entfernt Felder ohne Wert, damit der Befehl keine leeren Schluessel traegt. */
|
|
12
|
+
export function dropUndefined(input) {
|
|
13
|
+
const out = {};
|
|
14
|
+
for (const key of Object.keys(input)) {
|
|
15
|
+
if (input[key] !== undefined)
|
|
16
|
+
out[key] = input[key];
|
|
17
|
+
}
|
|
18
|
+
return out;
|
|
19
|
+
}
|
|
20
|
+
/** Parameter pruefen und daraus den Befehl bauen – ohne InDesign. */
|
|
21
|
+
export function buildCommand(definition, rawInput, config = {}) {
|
|
22
|
+
/*
|
|
23
|
+
* strictObject: unbekannte Parameter sind ein Fehler, kein stilles Weglassen.
|
|
24
|
+
* Sonst wuerde z. B. add_page mit "position" statt "at" die Seite still ans
|
|
25
|
+
* Ende haengen (Spezifikation nennt die verwechselbaren Namen ausdruecklich).
|
|
26
|
+
*/
|
|
27
|
+
const schema = z.strictObject(definition.inputShape);
|
|
28
|
+
const parsed = schema.safeParse(rawInput ?? {});
|
|
29
|
+
if (!parsed.success) {
|
|
30
|
+
const issues = parsed.error.issues.map((issue) => (issue.path.length > 0 ? issue.path.join('.') + ': ' : '') + issue.message);
|
|
31
|
+
return {
|
|
32
|
+
error: transportError('invalid_argument', issues.join('; '), { tool: definition.name, issues }),
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
const values = dropUndefined(parsed.data);
|
|
36
|
+
let args = definition.buildArgs ? definition.buildArgs(values) : values;
|
|
37
|
+
/* Konfigurierte Vorgaben erst nach der Pruefung: der Aufruf geht vor (E-54). */
|
|
38
|
+
if (definition.configArgs)
|
|
39
|
+
args = applyConfigDefaults(args, definition.configArgs(config));
|
|
40
|
+
return { command: { cmd: definition.command, args: dropUndefined(args) } };
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Ersetzt das Benutzerverzeichnis in der Fehlerantwort durch "~" – in
|
|
44
|
+
* `error.message` und in `error.details` (Auftrag A-8, Punkt 4; Entscheidung
|
|
45
|
+
* des Vermarktungsprojekts vom 14.09.2026, Vermerk E-68). Der Pfad bleibt
|
|
46
|
+
* lesbar, allein der Benutzername faellt weg. Aufbau und Felder der
|
|
47
|
+
* Fehlerantwort bleiben unveraendert (A-5).
|
|
48
|
+
*/
|
|
49
|
+
export function maskErrorPaths(result, home = homedir()) {
|
|
50
|
+
if (result.error === undefined)
|
|
51
|
+
return result;
|
|
52
|
+
return {
|
|
53
|
+
...result,
|
|
54
|
+
error: {
|
|
55
|
+
kind: result.error.kind,
|
|
56
|
+
message: maskHome(result.error.message, home),
|
|
57
|
+
details: maskDeep(result.error.details, home, 0),
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
/** Wie maskErrorPaths, aber fuer beliebig verschachtelte Details. */
|
|
62
|
+
function maskDeep(value, home, depth) {
|
|
63
|
+
if (typeof value === 'string')
|
|
64
|
+
return maskHome(value, home);
|
|
65
|
+
if (depth >= 6)
|
|
66
|
+
return value;
|
|
67
|
+
if (Array.isArray(value))
|
|
68
|
+
return value.map((item) => maskDeep(item, home, depth + 1));
|
|
69
|
+
if (value !== null && typeof value === 'object') {
|
|
70
|
+
const out = {};
|
|
71
|
+
for (const key of Object.keys(value)) {
|
|
72
|
+
out[key] = maskDeep(value[key], home, depth + 1);
|
|
73
|
+
}
|
|
74
|
+
return out;
|
|
75
|
+
}
|
|
76
|
+
return value;
|
|
77
|
+
}
|
|
78
|
+
/** Ein Werkzeugaufruf von Anfang bis Ende. */
|
|
79
|
+
export async function runTool(definition, rawInput, transport, logger, options = {}) {
|
|
80
|
+
const built = buildCommand(definition, rawInput, options.config ?? {});
|
|
81
|
+
if ('error' in built) {
|
|
82
|
+
const masked = maskErrorPaths(built.error);
|
|
83
|
+
logger.log({
|
|
84
|
+
tool: definition.name,
|
|
85
|
+
command: definition.command,
|
|
86
|
+
args: {},
|
|
87
|
+
duration_ms: 0,
|
|
88
|
+
ok: false,
|
|
89
|
+
error_kind: masked.error?.kind ?? 'invalid_argument',
|
|
90
|
+
error_message: masked.error?.message ?? '',
|
|
91
|
+
warnings: [],
|
|
92
|
+
result_keys: [],
|
|
93
|
+
});
|
|
94
|
+
return masked;
|
|
95
|
+
}
|
|
96
|
+
const startedAt = Date.now();
|
|
97
|
+
let result;
|
|
98
|
+
try {
|
|
99
|
+
const sendOptions = options.timeoutMs === undefined ? undefined : { timeoutMs: options.timeoutMs };
|
|
100
|
+
result = await transport.send(built.command, sendOptions);
|
|
101
|
+
}
|
|
102
|
+
catch (error) {
|
|
103
|
+
result = transportError('script_error', 'Transport failed: ' + String(error), null);
|
|
104
|
+
}
|
|
105
|
+
const duration = Date.now() - startedAt;
|
|
106
|
+
result = maskErrorPaths(result);
|
|
107
|
+
if (result.ok && definition.mapResult) {
|
|
108
|
+
result = { ...result, ...definition.mapResult(result) };
|
|
109
|
+
}
|
|
110
|
+
logger.log({
|
|
111
|
+
tool: definition.name,
|
|
112
|
+
command: built.command.cmd,
|
|
113
|
+
args: built.command.args,
|
|
114
|
+
duration_ms: duration,
|
|
115
|
+
ok: result.ok === true,
|
|
116
|
+
error_kind: result.error?.kind ?? null,
|
|
117
|
+
error_message: result.error?.message ?? null,
|
|
118
|
+
warnings: Array.isArray(result.warnings) ? result.warnings : [],
|
|
119
|
+
result_keys: Object.keys(result).filter((key) => key !== 'ok' && key !== 'warnings'),
|
|
120
|
+
});
|
|
121
|
+
return result;
|
|
122
|
+
}
|
|
123
|
+
//# sourceMappingURL=dispatch.js.map
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Freemium-Schalter (A-6 Nr. 4, E-60). Vorgabe: alles gesperrt.
|
|
3
|
+
*
|
|
4
|
+
* Absichtlich Konstanten im Code und kein Konfigurationsschluessel – was in
|
|
5
|
+
* config.json steht, koennte jeder Kunde aendern. Freigabe einzelner
|
|
6
|
+
* Werkzeuge: Namen in FREE_TOOLS eintragen; alle lesend gemeldeten Werkzeuge
|
|
7
|
+
* (readOnly) auf einmal: FREE_READ_ONLY_TOOLS auf true.
|
|
8
|
+
*/
|
|
9
|
+
export const FREE_READ_ONLY_TOOLS = false;
|
|
10
|
+
export const FREE_TOOLS = new Set([]);
|
|
11
|
+
export function isFreeTool(name, readOnly) {
|
|
12
|
+
if (FREE_TOOLS.has(name))
|
|
13
|
+
return true;
|
|
14
|
+
return FREE_READ_ONLY_TOOLS && readOnly;
|
|
15
|
+
}
|
|
16
|
+
//# sourceMappingURL=free-tools.js.map
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Das Gate (E-60): entscheidet je Aufruf, ob ein Werkzeug laufen darf.
|
|
3
|
+
*
|
|
4
|
+
* Reihenfolge: Schranke aus -> frei; Werkzeug auf der Freiliste -> frei;
|
|
5
|
+
* kein Schluessel -> gesperrt; gespeichertes gueltiges Ergebnis zum selben
|
|
6
|
+
* Schluessel, juenger als das Nachpruefintervall und nicht abgelaufen ->
|
|
7
|
+
* frei; sonst Pruefung ueber den Verifier: gueltig -> speichern, frei;
|
|
8
|
+
* ungueltig/abgelaufen/falsches Produkt -> speichern, gesperrt; nicht
|
|
9
|
+
* erreichbar -> frei mit Warnung, solange die Kulanzfrist ab der letzten
|
|
10
|
+
* gueltigen Pruefung laeuft, sonst gesperrt. Gleichzeitige Aufrufe teilen
|
|
11
|
+
* sich eine laufende Pruefung.
|
|
12
|
+
*/
|
|
13
|
+
import { transportError } from '../../transport/transport.js';
|
|
14
|
+
import { LICENSE_TERMS_URL, LICENSE_TERMS_URL_DE, SUPPORT_EMAIL, withSupportHint } from '../product-info.js';
|
|
15
|
+
import { fingerprint } from './key-source.js';
|
|
16
|
+
const HOUR_MS = 60 * 60 * 1000;
|
|
17
|
+
const DAY_MS = 24 * HOUR_MS;
|
|
18
|
+
/** Nach so langer Zeit wird ein gueltiges Ergebnis erneut geprueft. */
|
|
19
|
+
export const RECHECK_INTERVAL_MS = 24 * HOUR_MS;
|
|
20
|
+
/** So lange gilt ein gueltiges Ergebnis weiter, wenn der Dienst nicht erreichbar ist. */
|
|
21
|
+
export const GRACE_PERIOD_MS = 14 * DAY_MS;
|
|
22
|
+
/** Nach einem "nicht erreichbar" wird fruehestens nach dieser Zeit erneut angefragt. */
|
|
23
|
+
export const RETRY_INTERVAL_MS = 1 * HOUR_MS;
|
|
24
|
+
function isoOrNull(ms) {
|
|
25
|
+
return ms === null ? null : new Date(ms).toISOString();
|
|
26
|
+
}
|
|
27
|
+
function parseMs(value) {
|
|
28
|
+
if (value === null || value === undefined)
|
|
29
|
+
return null;
|
|
30
|
+
const ms = Date.parse(value);
|
|
31
|
+
return Number.isNaN(ms) ? null : ms;
|
|
32
|
+
}
|
|
33
|
+
export class LicenseGate {
|
|
34
|
+
enforce;
|
|
35
|
+
verifier;
|
|
36
|
+
store;
|
|
37
|
+
lookupKey;
|
|
38
|
+
isFree;
|
|
39
|
+
context;
|
|
40
|
+
now;
|
|
41
|
+
recheckIntervalMs;
|
|
42
|
+
gracePeriodMs;
|
|
43
|
+
retryIntervalMs;
|
|
44
|
+
pending = null;
|
|
45
|
+
constructor(options) {
|
|
46
|
+
this.enforce = options.enforce;
|
|
47
|
+
this.verifier = options.verifier;
|
|
48
|
+
this.store = options.store;
|
|
49
|
+
this.lookupKey = options.lookupKey;
|
|
50
|
+
this.isFree = options.isFree;
|
|
51
|
+
this.context = options.context;
|
|
52
|
+
this.now = options.now ?? (() => new Date());
|
|
53
|
+
this.recheckIntervalMs = options.recheckIntervalMs ?? RECHECK_INTERVAL_MS;
|
|
54
|
+
this.gracePeriodMs = options.gracePeriodMs ?? GRACE_PERIOD_MS;
|
|
55
|
+
this.retryIntervalMs = options.retryIntervalMs ?? RETRY_INTERVAL_MS;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Eine Zeile fuer die Startmeldung des Servers. "off" kommt seit A-9
|
|
59
|
+
* (E-86) im laufenden Server nicht mehr vor – nur bei einem im Code mit
|
|
60
|
+
* `enforce: false` gebauten Gate.
|
|
61
|
+
*/
|
|
62
|
+
describe() {
|
|
63
|
+
if (!this.enforce) {
|
|
64
|
+
return `license: off, verifier: ${this.verifierLabel()}`;
|
|
65
|
+
}
|
|
66
|
+
const lookup = this.lookupKey();
|
|
67
|
+
let key = 'none (' + lookup.path + ')';
|
|
68
|
+
if (lookup.source === 'env')
|
|
69
|
+
key = 'env';
|
|
70
|
+
if (lookup.source === 'file')
|
|
71
|
+
key = 'file ' + lookup.path;
|
|
72
|
+
return `license: enforced, verifier: ${this.verifierLabel()}, key: ${key}, state: ${this.store.location}`;
|
|
73
|
+
}
|
|
74
|
+
/** Verifier mit seinen Einstellungen, wenn er sie selbst beschreibt. */
|
|
75
|
+
verifierLabel() {
|
|
76
|
+
const describe = this.verifier.describe;
|
|
77
|
+
return typeof describe === 'function' ? describe.call(this.verifier) : this.verifier.name;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Fehlerumschlag fuer eine Sperre. Jede Meldung der Fehlerart `license`
|
|
81
|
+
* nennt die Support-Adresse im Klartext (A-10 Nr. 1); `details` traegt
|
|
82
|
+
* zusaetzlich `support` und die Adresse der Lizenzbedingungen (A-11
|
|
83
|
+
* Nr. 3) – additiv, der Aufbau der Fehlerantwort bleibt (A-5).
|
|
84
|
+
*/
|
|
85
|
+
toError(decision) {
|
|
86
|
+
return transportError('license', withSupportHint(decision.message ?? 'Licence check failed.'), {
|
|
87
|
+
...decision.details,
|
|
88
|
+
support: SUPPORT_EMAIL,
|
|
89
|
+
license_terms: LICENSE_TERMS_URL,
|
|
90
|
+
license_terms_de: LICENSE_TERMS_URL_DE,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
async decide(toolName, readOnly) {
|
|
94
|
+
if (!this.enforce)
|
|
95
|
+
return { allowed: true, status: 'off', message: null, details: {} };
|
|
96
|
+
if (this.isFree(toolName, readOnly))
|
|
97
|
+
return { allowed: true, status: 'free', message: null, details: {} };
|
|
98
|
+
const lookup = this.lookupKey();
|
|
99
|
+
if (lookup.key === null) {
|
|
100
|
+
return this.locked('missing', 'No licence key found. Set LAYOUTBRIDGE_SPM_LICENSE_KEY or put the key into ' + lookup.path + '.', lookup, null);
|
|
101
|
+
}
|
|
102
|
+
const fp = fingerprint(lookup.key);
|
|
103
|
+
const nowMs = this.now().getTime();
|
|
104
|
+
const stored = this.store.load();
|
|
105
|
+
const same = stored !== null && stored.key_fingerprint === fp ? stored : null;
|
|
106
|
+
if (same !== null) {
|
|
107
|
+
const checkedMs = parseMs(same.checked_at) ?? 0;
|
|
108
|
+
const expiresMs = parseMs(same.expires_at);
|
|
109
|
+
const notExpired = expiresMs === null || expiresMs > nowMs;
|
|
110
|
+
if (same.status === 'valid' && notExpired && nowMs - checkedMs < this.recheckIntervalMs) {
|
|
111
|
+
return this.allowed('valid', null, lookup, same);
|
|
112
|
+
}
|
|
113
|
+
if (same.status === 'unreachable' && nowMs - checkedMs < this.retryIntervalMs) {
|
|
114
|
+
return this.afterUnreachable(same, lookup, nowMs, same.message);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
if (this.pending !== null && this.pending.fingerprint === fp)
|
|
118
|
+
return this.pending.promise;
|
|
119
|
+
const promise = this.verify(lookup, fp, same).finally(() => {
|
|
120
|
+
this.pending = null;
|
|
121
|
+
});
|
|
122
|
+
this.pending = { fingerprint: fp, promise };
|
|
123
|
+
return promise;
|
|
124
|
+
}
|
|
125
|
+
async verify(lookup, fp, same) {
|
|
126
|
+
const key = lookup.key;
|
|
127
|
+
const context = { ...this.context, instance_id: same?.instance_id ?? null };
|
|
128
|
+
let outcome;
|
|
129
|
+
try {
|
|
130
|
+
outcome = await this.verifier.verify(key, context);
|
|
131
|
+
}
|
|
132
|
+
catch (error) {
|
|
133
|
+
outcome = { status: 'unreachable', message: 'Licence check failed: ' + String(error) };
|
|
134
|
+
}
|
|
135
|
+
const nowMs = this.now().getTime();
|
|
136
|
+
const checkedAt = new Date(nowMs).toISOString();
|
|
137
|
+
if (outcome.status === 'valid') {
|
|
138
|
+
const state = {
|
|
139
|
+
key_fingerprint: fp,
|
|
140
|
+
status: 'valid',
|
|
141
|
+
checked_at: checkedAt,
|
|
142
|
+
last_valid_at: checkedAt,
|
|
143
|
+
expires_at: outcome.expires_at ?? null,
|
|
144
|
+
instance_id: outcome.instance_id ?? same?.instance_id ?? null,
|
|
145
|
+
verifier: this.verifier.name,
|
|
146
|
+
message: outcome.message,
|
|
147
|
+
};
|
|
148
|
+
this.store.save(state);
|
|
149
|
+
return this.allowed('valid', null, lookup, state);
|
|
150
|
+
}
|
|
151
|
+
const state = {
|
|
152
|
+
key_fingerprint: fp,
|
|
153
|
+
status: outcome.status,
|
|
154
|
+
checked_at: checkedAt,
|
|
155
|
+
last_valid_at: same?.last_valid_at ?? null,
|
|
156
|
+
expires_at: outcome.expires_at ?? same?.expires_at ?? null,
|
|
157
|
+
instance_id: same?.instance_id ?? null,
|
|
158
|
+
verifier: this.verifier.name,
|
|
159
|
+
message: outcome.message,
|
|
160
|
+
};
|
|
161
|
+
this.store.save(state);
|
|
162
|
+
if (outcome.status === 'unreachable')
|
|
163
|
+
return this.afterUnreachable(state, lookup, nowMs, outcome.message);
|
|
164
|
+
return this.locked(outcome.status, this.describeRefusal(outcome.status, outcome.message, state), lookup, state);
|
|
165
|
+
}
|
|
166
|
+
/** Dienst nicht erreichbar: Kulanzfrist ab der letzten gueltigen Pruefung. */
|
|
167
|
+
afterUnreachable(state, lookup, nowMs, reason) {
|
|
168
|
+
const anchorMs = parseMs(state.last_valid_at);
|
|
169
|
+
const expiresMs = parseMs(state.expires_at);
|
|
170
|
+
if (expiresMs !== null && expiresMs <= nowMs) {
|
|
171
|
+
return this.locked('expired', 'The licence expired on ' + state.expires_at + ' and could not be re-verified: ' + reason, lookup, state);
|
|
172
|
+
}
|
|
173
|
+
if (anchorMs === null) {
|
|
174
|
+
return this.locked('unverified', 'The licence key could not be verified and no earlier valid check is stored: ' + reason, lookup, state);
|
|
175
|
+
}
|
|
176
|
+
const graceUntilMs = anchorMs + this.gracePeriodMs;
|
|
177
|
+
if (nowMs <= graceUntilMs) {
|
|
178
|
+
return this.allowed('grace', 'Licence not verified online since ' + state.last_valid_at + '; the grace period ends ' + isoOrNull(graceUntilMs) + '.', lookup, state, graceUntilMs);
|
|
179
|
+
}
|
|
180
|
+
return this.locked('unverified', 'The licence key could not be verified and the grace period ended ' + isoOrNull(graceUntilMs) + ': ' + reason, lookup, state);
|
|
181
|
+
}
|
|
182
|
+
describeRefusal(status, message, state) {
|
|
183
|
+
if (status === 'expired')
|
|
184
|
+
return 'The licence expired' + (state.expires_at ? ' on ' + state.expires_at : '') + '. ' + message;
|
|
185
|
+
if (status === 'invalid')
|
|
186
|
+
return 'The licence key is not valid. ' + message;
|
|
187
|
+
if (status === 'product_mismatch')
|
|
188
|
+
return 'The licence key belongs to another product or major version. ' + message;
|
|
189
|
+
return message;
|
|
190
|
+
}
|
|
191
|
+
details(lookup, state, graceUntilMs) {
|
|
192
|
+
return {
|
|
193
|
+
key_source: lookup.source,
|
|
194
|
+
key_path: lookup.path,
|
|
195
|
+
checked_at: state?.checked_at ?? null,
|
|
196
|
+
last_valid_at: state?.last_valid_at ?? null,
|
|
197
|
+
expires_at: state?.expires_at ?? null,
|
|
198
|
+
grace_until: isoOrNull(graceUntilMs),
|
|
199
|
+
verifier: this.verifier.name,
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
allowed(status, message, lookup, state, graceUntilMs = null) {
|
|
203
|
+
return { allowed: true, status, message, details: { status, ...this.details(lookup, state, graceUntilMs) } };
|
|
204
|
+
}
|
|
205
|
+
locked(status, message, lookup, state) {
|
|
206
|
+
return { allowed: false, status, message: withSupportHint(message), details: { status, ...this.details(lookup, state, null) } };
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
//# sourceMappingURL=gate.js.map
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Zusammenbau der Lizenzschranke fuer den laufenden Server (E-60, E-62).
|
|
3
|
+
*
|
|
4
|
+
* Seit der Ergaenzung des Auftrags A-6 (06.09.2026) prueft der Verifier des
|
|
5
|
+
* Zahlungsdienstes; die Attrappe bleibt fuer Tests. Scharfschalten in zwei
|
|
6
|
+
* Stufen (Ergaenzung Nr. 10): Stufe 1 hielt die Schranke im laufenden
|
|
7
|
+
* Server aus, scharf nur mit LAYOUTBRIDGE_SPM_LICENSE_ENFORCE=1. Seit
|
|
8
|
+
* Stufe 2 (A-9 fuer Polar, E-86, 23.09.2026) ist sie immer scharf; die
|
|
9
|
+
* Umgebungsvariable ist entfallen und wird nicht mehr gelesen. Aus ist die
|
|
10
|
+
* Schranke nur noch, wenn ein LicenseGate im Code mit `enforce: false`
|
|
11
|
+
* gebaut wird (Einheitstests) – kein Schalter von aussen.
|
|
12
|
+
*/
|
|
13
|
+
import { isFreeTool } from './free-tools.js';
|
|
14
|
+
import { LicenseGate } from './gate.js';
|
|
15
|
+
import { resolveInstanceName } from './instance-name.js';
|
|
16
|
+
import { defaultLicenseStatePath, readLicenseKey } from './key-source.js';
|
|
17
|
+
import { LemonSqueezyVerifier } from './lemonsqueezy-verifier.js';
|
|
18
|
+
import { resolvePolarIds } from './polar-ids.js';
|
|
19
|
+
import { PolarVerifier, resolvePolarBaseUrl } from './polar-verifier.js';
|
|
20
|
+
import { resolveProductIds } from './product-ids.js';
|
|
21
|
+
import { FileLicenseStateStore } from './state-store.js';
|
|
22
|
+
import { paymentPrivacyPolicyUrlFor, withSupportHint } from '../product-info.js';
|
|
23
|
+
export { LicenseGate } from './gate.js';
|
|
24
|
+
/**
|
|
25
|
+
* Welcher Zahlungsdienst geprueft wird (A-15 Nr. 3, 23.09.2026; A-15a Nr. 1).
|
|
26
|
+
*
|
|
27
|
+
* Vorgabe ist seit A-15a polar (Entscheidung Sascha, 23.09.2026: Polar zum
|
|
28
|
+
* Verkaufsstart). Die Klasse lemonsqueezy bleibt als Rueckfall; umgeschaltet
|
|
29
|
+
* wird allein in der Bauphase ueber die Umgebungsvariable, in beide
|
|
30
|
+
* Richtungen – kein Kundenschalter, nichts aus config.json.
|
|
31
|
+
*/
|
|
32
|
+
export const DEFAULT_LICENSE_PROVIDER = 'polar';
|
|
33
|
+
export const LICENSE_PROVIDER_ENV = 'LAYOUTBRIDGE_SPM_LICENSE_PROVIDER';
|
|
34
|
+
export function resolveLicenseProvider(env = process.env) {
|
|
35
|
+
const value = env[LICENSE_PROVIDER_ENV];
|
|
36
|
+
return value === 'polar' || value === 'lemonsqueezy' ? value : DEFAULT_LICENSE_PROVIDER;
|
|
37
|
+
}
|
|
38
|
+
export function createVerifier(env = process.env) {
|
|
39
|
+
if (resolveLicenseProvider(env) === 'polar') {
|
|
40
|
+
return new PolarVerifier({ ids: resolvePolarIds(env), baseUrl: resolvePolarBaseUrl(env) });
|
|
41
|
+
}
|
|
42
|
+
return new LemonSqueezyVerifier({ productIds: resolveProductIds(env) });
|
|
43
|
+
}
|
|
44
|
+
/** Datenschutzerklaerung des gewaehlten Zahlungsdienstes (A-15 Nr. 5). */
|
|
45
|
+
export function paymentPrivacyPolicyUrl(env = process.env) {
|
|
46
|
+
return paymentPrivacyPolicyUrlFor(resolveLicenseProvider(env));
|
|
47
|
+
}
|
|
48
|
+
export function createLicenseGate(options) {
|
|
49
|
+
const env = options.env ?? process.env;
|
|
50
|
+
return new LicenseGate({
|
|
51
|
+
enforce: true,
|
|
52
|
+
verifier: createVerifier(env),
|
|
53
|
+
store: new FileLicenseStateStore(defaultLicenseStatePath(env)),
|
|
54
|
+
lookupKey: () => readLicenseKey(env),
|
|
55
|
+
isFree: isFreeTool,
|
|
56
|
+
context: {
|
|
57
|
+
product: options.product,
|
|
58
|
+
product_version: options.version.split('.')[0] ?? options.version,
|
|
59
|
+
instance_name: resolveInstanceName(env),
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Rechnerwechsel (Ergaenzung Nr. 7): die Aktivierung dieses Arbeitsplatzes
|
|
65
|
+
* beim Dienst freigeben und den lokalen Speicher loeschen. Laeuft ohne
|
|
66
|
+
* InDesign und ohne Client – der Server wird dafuer mit --deactivate
|
|
67
|
+
* aufgerufen. Bewusst kein Werkzeug im Katalog: die Schnittstelle bleibt
|
|
68
|
+
* unveraendert (A-5), und eine KI kann den Vorgang nicht beilaeufig
|
|
69
|
+
* ausloesen.
|
|
70
|
+
*/
|
|
71
|
+
export async function runDeactivation(env = process.env, verifier = createVerifier(env), store = new FileLicenseStateStore(defaultLicenseStatePath(env))) {
|
|
72
|
+
const lookup = readLicenseKey(env);
|
|
73
|
+
if (lookup.key === null) {
|
|
74
|
+
return { ok: false, message: 'No licence key found. Set LAYOUTBRIDGE_SPM_LICENSE_KEY or put the key into ' + lookup.path + '.' };
|
|
75
|
+
}
|
|
76
|
+
const state = store.load();
|
|
77
|
+
if (state === null || state.instance_id === null) {
|
|
78
|
+
store.clear();
|
|
79
|
+
return {
|
|
80
|
+
ok: true,
|
|
81
|
+
message: 'No activation was stored for this machine; the local licence state has been removed. Nothing had to be released at the service.',
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
if (verifier.deactivate === undefined) {
|
|
85
|
+
return { ok: false, message: withSupportHint('This build cannot release an activation.') };
|
|
86
|
+
}
|
|
87
|
+
try {
|
|
88
|
+
await verifier.deactivate(lookup.key, state.instance_id);
|
|
89
|
+
}
|
|
90
|
+
catch (error) {
|
|
91
|
+
return { ok: false, message: withSupportHint('The activation was not released: ' + String(error instanceof Error ? error.message : error)) };
|
|
92
|
+
}
|
|
93
|
+
store.clear();
|
|
94
|
+
return { ok: true, message: 'The activation was released and the local licence state removed. The key can now be activated on another machine.' };
|
|
95
|
+
}
|
|
96
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Instanzname fuer die Aktivierung beim Zahlungsdienst (Auftrag A-8 des
|
|
3
|
+
* Vermarktungsprojekts, 13.09.2026, Vermerk E-64).
|
|
4
|
+
*
|
|
5
|
+
* Frueher war das der Rechnername (`hostname()`). Der kann einen
|
|
6
|
+
* Personennamen enthalten und verliess damit bei jeder Aktivierung den
|
|
7
|
+
* Rechner. Stattdessen jetzt eine zufaellige Kennung `layoutbridge-<8
|
|
8
|
+
* Hexzeichen>`, die beim ersten Mal erzeugt und neben der Lizenzdatei
|
|
9
|
+
* abgelegt wird; danach ist sie ueber Neustarts hinweg dieselbe.
|
|
10
|
+
* LAYOUTBRIDGE_SPM_INSTANCE_NAME ueberschreibt sie, fuer alle, die im
|
|
11
|
+
* Haendler-Dashboard lesbare Namen wollen.
|
|
12
|
+
*/
|
|
13
|
+
import { randomBytes } from 'node:crypto';
|
|
14
|
+
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
15
|
+
import { homedir, platform } from 'node:os';
|
|
16
|
+
import { dirname } from 'node:path';
|
|
17
|
+
import { defaultLicenseDirectory, licensePathApi } from './key-source.js';
|
|
18
|
+
/** Form der erzeugten Kennung. */
|
|
19
|
+
export const INSTANCE_NAME_PATTERN = /^layoutbridge-[0-9a-f]{8}$/;
|
|
20
|
+
export function defaultInstanceNamePath(os = platform(), home = homedir()) {
|
|
21
|
+
return licensePathApi(os).join(defaultLicenseDirectory(os, home), 'instance-name');
|
|
22
|
+
}
|
|
23
|
+
/** Eine neue Zufallskennung bilden. */
|
|
24
|
+
export function newInstanceName(random = randomBytes) {
|
|
25
|
+
return 'layoutbridge-' + random(4).toString('hex');
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Die Kennung dieses Arbeitsplatzes: Umgebungsvariable, sonst die
|
|
29
|
+
* gespeicherte, sonst eine neue. Laesst sich die neue nicht speichern,
|
|
30
|
+
* wird sie trotzdem verwendet – die Aktivierung soll an einer nicht
|
|
31
|
+
* schreibbaren Datei nicht scheitern; der Hinweis geht auf die
|
|
32
|
+
* Standardfehlerausgabe.
|
|
33
|
+
*/
|
|
34
|
+
export function resolveInstanceName(env = process.env, path = defaultInstanceNamePath()) {
|
|
35
|
+
const fromEnv = env['LAYOUTBRIDGE_SPM_INSTANCE_NAME'];
|
|
36
|
+
if (fromEnv !== undefined && fromEnv.trim().length > 0)
|
|
37
|
+
return fromEnv.trim();
|
|
38
|
+
try {
|
|
39
|
+
const stored = readFileSync(path, 'utf8').trim();
|
|
40
|
+
if (INSTANCE_NAME_PATTERN.test(stored))
|
|
41
|
+
return stored;
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
/* keine Datei oder nicht lesbar – dann eine neue Kennung */
|
|
45
|
+
}
|
|
46
|
+
const created = newInstanceName();
|
|
47
|
+
try {
|
|
48
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
49
|
+
writeFileSync(path, created + '\n', { encoding: 'utf8', mode: 0o600 });
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
process.stderr.write(`[layoutbridge-spm] cannot store the instance name: ${String(error)}\n`);
|
|
53
|
+
}
|
|
54
|
+
return created;
|
|
55
|
+
}
|
|
56
|
+
//# sourceMappingURL=instance-name.js.map
|