layoutbridge-light 0.0.0-stage → 1.0.1

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.
Files changed (92) hide show
  1. package/CHANGELOG.md +105 -0
  2. package/LICENSE +26 -0
  3. package/README.de.md +367 -0
  4. package/README.md +353 -2
  5. package/THIRD-PARTY-NOTICES.md +2878 -0
  6. package/dist/src/mcp/catalog.js +131 -0
  7. package/dist/src/mcp/config.js +211 -0
  8. package/dist/src/mcp/content-credentials.js +65 -0
  9. package/dist/src/mcp/dispatch.js +171 -0
  10. package/dist/src/mcp/epub-check.js +331 -0
  11. package/dist/src/mcp/epub-orientation.js +132 -0
  12. package/dist/src/mcp/license/free-tools.js +16 -0
  13. package/dist/src/mcp/license/gate.js +210 -0
  14. package/dist/src/mcp/license/index.js +107 -0
  15. package/dist/src/mcp/license/instance-name.js +57 -0
  16. package/dist/src/mcp/license/key-source.js +59 -0
  17. package/dist/src/mcp/license/lemonsqueezy-verifier.js +207 -0
  18. package/dist/src/mcp/license/placeholder-verifier.js +10 -0
  19. package/dist/src/mcp/license/polar-ids.js +100 -0
  20. package/dist/src/mcp/license/polar-verifier.js +285 -0
  21. package/dist/src/mcp/license/product-ids.js +73 -0
  22. package/dist/src/mcp/license/state-store.js +91 -0
  23. package/dist/src/mcp/license/types.js +11 -0
  24. package/dist/src/mcp/logging.js +271 -0
  25. package/dist/src/mcp/pdf-postprocess.js +798 -0
  26. package/dist/src/mcp/product-info.js +54 -0
  27. package/dist/src/mcp/product.js +56 -0
  28. package/dist/src/mcp/server.js +253 -0
  29. package/dist/src/mcp/setup.js +680 -0
  30. package/dist/src/mcp/start.cjs +117 -0
  31. package/dist/src/mcp/target-folder.js +25 -0
  32. package/dist/src/mcp/tool-definition.js +2 -0
  33. package/dist/src/mcp/tools.js +2680 -0
  34. package/dist/src/mcp/typography-rules.js +70 -0
  35. package/dist/src/transport/com.js +436 -0
  36. package/dist/src/transport/osascript.js +171 -0
  37. package/dist/src/transport/queue.js +19 -0
  38. package/dist/src/transport/script-bundle.js +92 -0
  39. package/dist/src/transport/transport.js +52 -0
  40. package/indesign/commands/10-app.js +6 -0
  41. package/indesign/commands/20-documents.js +517 -0
  42. package/indesign/commands/22-document-setup.js +204 -0
  43. package/indesign/commands/23-save-package.js +258 -0
  44. package/indesign/commands/25-export.js +695 -0
  45. package/indesign/commands/26-export-image.js +202 -0
  46. package/indesign/commands/27-textdefaults.js +151 -0
  47. package/indesign/commands/28-selection.js +174 -0
  48. package/indesign/commands/30-pages.js +1032 -0
  49. package/indesign/commands/31-view.js +310 -0
  50. package/indesign/commands/32-sections.js +203 -0
  51. package/indesign/commands/35-parents.js +448 -0
  52. package/indesign/commands/37-layout.js +837 -0
  53. package/indesign/commands/40-frames.js +886 -0
  54. package/indesign/commands/41-shapes.js +505 -0
  55. package/indesign/commands/42-groups.js +117 -0
  56. package/indesign/commands/43-layers.js +310 -0
  57. package/indesign/commands/44-effects.js +274 -0
  58. package/indesign/commands/45-frame-options.js +247 -0
  59. package/indesign/commands/47-anchored.js +473 -0
  60. package/indesign/commands/50-modify.js +803 -0
  61. package/indesign/commands/52-align.js +139 -0
  62. package/indesign/commands/55-fit.js +106 -0
  63. package/indesign/commands/60-images.js +1154 -0
  64. package/indesign/commands/62-embed.js +120 -0
  65. package/indesign/commands/69-placeholders.js +132 -0
  66. package/indesign/commands/70-text.js +710 -0
  67. package/indesign/commands/71-outlines.js +92 -0
  68. package/indesign/commands/72-place-text.js +476 -0
  69. package/indesign/commands/73-story-lock.js +157 -0
  70. package/indesign/commands/74-stories.js +121 -0
  71. package/indesign/commands/75-tables.js +702 -0
  72. package/indesign/commands/76-text-variables.js +260 -0
  73. package/indesign/commands/77-story-options.js +86 -0
  74. package/indesign/commands/78-frame-lines.js +511 -0
  75. package/indesign/commands/79-check-typography.js +480 -0
  76. package/indesign/commands/80-styles.js +1960 -0
  77. package/indesign/commands/81-composition.js +355 -0
  78. package/indesign/commands/82-object-styles.js +263 -0
  79. package/indesign/commands/83-drop-caps.js +87 -0
  80. package/indesign/commands/83-indents-tabs.js +264 -0
  81. package/indesign/commands/84-import-styles.js +142 -0
  82. package/indesign/commands/85-swatches.js +112 -0
  83. package/indesign/commands/86-gradients.js +185 -0
  84. package/indesign/commands/87-replace-swatch.js +180 -0
  85. package/indesign/commands/88-replace-font.js +212 -0
  86. package/indesign/commands/89-toc-index.js +1002 -0
  87. package/indesign/commands/90-print-on-demand.js +495 -0
  88. package/indesign/commands/91-epub.js +629 -0
  89. package/indesign/commands/92-accessibility.js +1131 -0
  90. package/indesign/interpreter.js +829 -0
  91. package/package.json +41 -4
  92. package/rules/typography-rules.json +32 -0
@@ -0,0 +1,210 @@
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
+ import { envName } from '../product.js';
17
+ const HOUR_MS = 60 * 60 * 1000;
18
+ const DAY_MS = 24 * HOUR_MS;
19
+ /** Nach so langer Zeit wird ein gueltiges Ergebnis erneut geprueft. */
20
+ export const RECHECK_INTERVAL_MS = 24 * HOUR_MS;
21
+ /** So lange gilt ein gueltiges Ergebnis weiter, wenn der Dienst nicht erreichbar ist. */
22
+ export const GRACE_PERIOD_MS = 14 * DAY_MS;
23
+ /** Nach einem "nicht erreichbar" wird fruehestens nach dieser Zeit erneut angefragt. */
24
+ export const RETRY_INTERVAL_MS = 1 * HOUR_MS;
25
+ function isoOrNull(ms) {
26
+ return ms === null ? null : new Date(ms).toISOString();
27
+ }
28
+ function parseMs(value) {
29
+ if (value === null || value === undefined)
30
+ return null;
31
+ const ms = Date.parse(value);
32
+ return Number.isNaN(ms) ? null : ms;
33
+ }
34
+ export class LicenseGate {
35
+ enforce;
36
+ verifier;
37
+ store;
38
+ lookupKey;
39
+ isFree;
40
+ context;
41
+ now;
42
+ recheckIntervalMs;
43
+ gracePeriodMs;
44
+ retryIntervalMs;
45
+ pending = null;
46
+ constructor(options) {
47
+ this.enforce = options.enforce;
48
+ this.verifier = options.verifier;
49
+ this.store = options.store;
50
+ this.lookupKey = options.lookupKey;
51
+ this.isFree = options.isFree;
52
+ this.context = options.context;
53
+ this.now = options.now ?? (() => new Date());
54
+ this.recheckIntervalMs = options.recheckIntervalMs ?? RECHECK_INTERVAL_MS;
55
+ this.gracePeriodMs = options.gracePeriodMs ?? GRACE_PERIOD_MS;
56
+ this.retryIntervalMs = options.retryIntervalMs ?? RETRY_INTERVAL_MS;
57
+ }
58
+ /**
59
+ * Eine Zeile fuer die Startmeldung des Servers. "off" kommt seit A-9
60
+ * (E-86) im laufenden Server nicht mehr vor – nur bei einem im Code mit
61
+ * `enforce: false` gebauten Gate.
62
+ */
63
+ describe() {
64
+ if (!this.enforce) {
65
+ return `license: off, verifier: ${this.verifierLabel()}`;
66
+ }
67
+ const lookup = this.lookupKey();
68
+ let key = 'none (' + lookup.path + ')';
69
+ if (lookup.source === 'env')
70
+ key = 'env';
71
+ if (lookup.source === 'file')
72
+ key = 'file ' + lookup.path;
73
+ return `license: enforced, verifier: ${this.verifierLabel()}, key: ${key}, state: ${this.store.location}`;
74
+ }
75
+ /** Verifier mit seinen Einstellungen, wenn er sie selbst beschreibt. */
76
+ verifierLabel() {
77
+ const describe = this.verifier.describe;
78
+ return typeof describe === 'function' ? describe.call(this.verifier) : this.verifier.name;
79
+ }
80
+ /**
81
+ * Fehlerumschlag fuer eine Sperre. Jede Meldung der Fehlerart `license`
82
+ * nennt die Support-Adresse im Klartext (A-10 Nr. 1); `details` traegt
83
+ * zusaetzlich `support` und die Adresse der Lizenzbedingungen (A-11
84
+ * Nr. 3) – additiv, der Aufbau der Fehlerantwort bleibt (A-5).
85
+ */
86
+ toError(decision) {
87
+ return transportError('license', withSupportHint(decision.message ?? 'Licence check failed.'), {
88
+ ...decision.details,
89
+ support: SUPPORT_EMAIL,
90
+ license_terms: LICENSE_TERMS_URL,
91
+ license_terms_de: LICENSE_TERMS_URL_DE,
92
+ });
93
+ }
94
+ async decide(toolName, readOnly) {
95
+ if (!this.enforce)
96
+ return { allowed: true, status: 'off', message: null, details: {} };
97
+ if (this.isFree(toolName, readOnly))
98
+ return { allowed: true, status: 'free', message: null, details: {} };
99
+ const lookup = this.lookupKey();
100
+ if (lookup.key === null) {
101
+ return this.locked('missing', 'No licence key found. Set ' + envName('LICENSE_KEY') + ' or put the key into ' + lookup.path + '.', lookup, null);
102
+ }
103
+ const fp = fingerprint(lookup.key);
104
+ const nowMs = this.now().getTime();
105
+ const stored = this.store.load();
106
+ const same = stored !== null && stored.key_fingerprint === fp ? stored : null;
107
+ if (same !== null) {
108
+ const checkedMs = parseMs(same.checked_at) ?? 0;
109
+ const expiresMs = parseMs(same.expires_at);
110
+ const notExpired = expiresMs === null || expiresMs > nowMs;
111
+ if (same.status === 'valid' && notExpired && nowMs - checkedMs < this.recheckIntervalMs) {
112
+ return this.allowed('valid', null, lookup, same);
113
+ }
114
+ if (same.status === 'unreachable' && nowMs - checkedMs < this.retryIntervalMs) {
115
+ return this.afterUnreachable(same, lookup, nowMs, same.message);
116
+ }
117
+ }
118
+ if (this.pending !== null && this.pending.fingerprint === fp)
119
+ return this.pending.promise;
120
+ const promise = this.verify(lookup, fp, same).finally(() => {
121
+ this.pending = null;
122
+ });
123
+ this.pending = { fingerprint: fp, promise };
124
+ return promise;
125
+ }
126
+ async verify(lookup, fp, same) {
127
+ const key = lookup.key;
128
+ const context = { ...this.context, instance_id: same?.instance_id ?? null };
129
+ let outcome;
130
+ try {
131
+ outcome = await this.verifier.verify(key, context);
132
+ }
133
+ catch (error) {
134
+ outcome = { status: 'unreachable', message: 'Licence check failed: ' + String(error) };
135
+ }
136
+ const nowMs = this.now().getTime();
137
+ const checkedAt = new Date(nowMs).toISOString();
138
+ if (outcome.status === 'valid') {
139
+ const state = {
140
+ key_fingerprint: fp,
141
+ status: 'valid',
142
+ checked_at: checkedAt,
143
+ last_valid_at: checkedAt,
144
+ expires_at: outcome.expires_at ?? null,
145
+ instance_id: outcome.instance_id ?? same?.instance_id ?? null,
146
+ verifier: this.verifier.name,
147
+ message: outcome.message,
148
+ };
149
+ this.store.save(state);
150
+ return this.allowed('valid', null, lookup, state);
151
+ }
152
+ const state = {
153
+ key_fingerprint: fp,
154
+ status: outcome.status,
155
+ checked_at: checkedAt,
156
+ last_valid_at: same?.last_valid_at ?? null,
157
+ expires_at: outcome.expires_at ?? same?.expires_at ?? null,
158
+ instance_id: same?.instance_id ?? null,
159
+ verifier: this.verifier.name,
160
+ message: outcome.message,
161
+ };
162
+ this.store.save(state);
163
+ if (outcome.status === 'unreachable')
164
+ return this.afterUnreachable(state, lookup, nowMs, outcome.message);
165
+ return this.locked(outcome.status, this.describeRefusal(outcome.status, outcome.message, state), lookup, state);
166
+ }
167
+ /** Dienst nicht erreichbar: Kulanzfrist ab der letzten gueltigen Pruefung. */
168
+ afterUnreachable(state, lookup, nowMs, reason) {
169
+ const anchorMs = parseMs(state.last_valid_at);
170
+ const expiresMs = parseMs(state.expires_at);
171
+ if (expiresMs !== null && expiresMs <= nowMs) {
172
+ return this.locked('expired', 'The licence expired on ' + state.expires_at + ' and could not be re-verified: ' + reason, lookup, state);
173
+ }
174
+ if (anchorMs === null) {
175
+ return this.locked('unverified', 'The licence key could not be verified and no earlier valid check is stored: ' + reason, lookup, state);
176
+ }
177
+ const graceUntilMs = anchorMs + this.gracePeriodMs;
178
+ if (nowMs <= graceUntilMs) {
179
+ return this.allowed('grace', 'Licence not verified online since ' + state.last_valid_at + '; the grace period ends ' + isoOrNull(graceUntilMs) + '.', lookup, state, graceUntilMs);
180
+ }
181
+ return this.locked('unverified', 'The licence key could not be verified and the grace period ended ' + isoOrNull(graceUntilMs) + ': ' + reason, lookup, state);
182
+ }
183
+ describeRefusal(status, message, state) {
184
+ if (status === 'expired')
185
+ return 'The licence expired' + (state.expires_at ? ' on ' + state.expires_at : '') + '. ' + message;
186
+ if (status === 'invalid')
187
+ return 'The licence key is not valid. ' + message;
188
+ if (status === 'product_mismatch')
189
+ return 'The licence key belongs to another product or major version. ' + message;
190
+ return message;
191
+ }
192
+ details(lookup, state, graceUntilMs) {
193
+ return {
194
+ key_source: lookup.source,
195
+ key_path: lookup.path,
196
+ checked_at: state?.checked_at ?? null,
197
+ last_valid_at: state?.last_valid_at ?? null,
198
+ expires_at: state?.expires_at ?? null,
199
+ grace_until: isoOrNull(graceUntilMs),
200
+ verifier: this.verifier.name,
201
+ };
202
+ }
203
+ allowed(status, message, lookup, state, graceUntilMs = null) {
204
+ return { allowed: true, status, message, details: { status, ...this.details(lookup, state, graceUntilMs) } };
205
+ }
206
+ locked(status, message, lookup, state) {
207
+ return { allowed: false, status, message: withSupportHint(message), details: { status, ...this.details(lookup, state, null) } };
208
+ }
209
+ }
210
+ //# sourceMappingURL=gate.js.map
@@ -0,0 +1,107 @@
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
+ import { envName, PRODUCT } from '../product.js';
24
+ export { LicenseGate } from './gate.js';
25
+ /**
26
+ * Welcher Zahlungsdienst geprueft wird (A-15 Nr. 3, 23.09.2026; A-15a Nr. 1).
27
+ *
28
+ * Vorgabe ist seit A-15a polar (Entscheidung des Anbieters, 23.09.2026: Polar zum
29
+ * Verkaufsstart). Die Klasse lemonsqueezy bleibt als Rueckfall; umgeschaltet
30
+ * wird allein in der Bauphase ueber die Umgebungsvariable, in beide
31
+ * Richtungen – kein Kundenschalter, nichts aus config.json.
32
+ */
33
+ export const DEFAULT_LICENSE_PROVIDER = 'polar';
34
+ export const LICENSE_PROVIDER_ENV = 'LAYOUTBRIDGE_SPM_LICENSE_PROVIDER';
35
+ /**
36
+ * LayoutBridge Light kennt nur Polar (E-109): kein Rueckfall auf
37
+ * lemonsqueezy, auch nicht ueber die Umgebungsvariable.
38
+ */
39
+ export function resolveLicenseProvider(env = process.env, product = PRODUCT) {
40
+ if (product.id === 'light')
41
+ return 'polar';
42
+ const value = env[envName('LICENSE_PROVIDER', product)];
43
+ return value === 'polar' || value === 'lemonsqueezy' ? value : DEFAULT_LICENSE_PROVIDER;
44
+ }
45
+ export function createVerifier(env = process.env, product = PRODUCT) {
46
+ if (resolveLicenseProvider(env, product) === 'polar') {
47
+ return new PolarVerifier({
48
+ ids: resolvePolarIds(env, undefined, product),
49
+ baseUrl: resolvePolarBaseUrl(env, product),
50
+ requireBenefit: product.id === 'light',
51
+ });
52
+ }
53
+ return new LemonSqueezyVerifier({ productIds: resolveProductIds(env, undefined, product) });
54
+ }
55
+ /** Datenschutzerklaerung des gewaehlten Zahlungsdienstes (A-15 Nr. 5). */
56
+ export function paymentPrivacyPolicyUrl(env = process.env, product = PRODUCT) {
57
+ return paymentPrivacyPolicyUrlFor(resolveLicenseProvider(env, product));
58
+ }
59
+ export function createLicenseGate(options) {
60
+ const env = options.env ?? process.env;
61
+ return new LicenseGate({
62
+ enforce: true,
63
+ verifier: createVerifier(env),
64
+ store: new FileLicenseStateStore(defaultLicenseStatePath(env)),
65
+ lookupKey: () => readLicenseKey(env),
66
+ isFree: isFreeTool,
67
+ context: {
68
+ product: options.product,
69
+ product_version: options.version.split('.')[0] ?? options.version,
70
+ instance_name: resolveInstanceName(env),
71
+ },
72
+ });
73
+ }
74
+ /**
75
+ * Rechnerwechsel (Ergaenzung Nr. 7): die Aktivierung dieses Arbeitsplatzes
76
+ * beim Dienst freigeben und den lokalen Speicher loeschen. Laeuft ohne
77
+ * InDesign und ohne Client – der Server wird dafuer mit --deactivate
78
+ * aufgerufen. Bewusst kein Werkzeug im Katalog: die Schnittstelle bleibt
79
+ * unveraendert (A-5), und eine KI kann den Vorgang nicht beilaeufig
80
+ * ausloesen.
81
+ */
82
+ export async function runDeactivation(env = process.env, verifier = createVerifier(env), store = new FileLicenseStateStore(defaultLicenseStatePath(env))) {
83
+ const lookup = readLicenseKey(env);
84
+ if (lookup.key === null) {
85
+ return { ok: false, message: 'No licence key found. Set ' + envName('LICENSE_KEY') + ' or put the key into ' + lookup.path + '.' };
86
+ }
87
+ const state = store.load();
88
+ if (state === null || state.instance_id === null) {
89
+ store.clear();
90
+ return {
91
+ ok: true,
92
+ message: 'No activation was stored for this machine; the local licence state has been removed. Nothing had to be released at the service.',
93
+ };
94
+ }
95
+ if (verifier.deactivate === undefined) {
96
+ return { ok: false, message: withSupportHint('This build cannot release an activation.') };
97
+ }
98
+ try {
99
+ await verifier.deactivate(lookup.key, state.instance_id);
100
+ }
101
+ catch (error) {
102
+ return { ok: false, message: withSupportHint('The activation was not released: ' + String(error instanceof Error ? error.message : error)) };
103
+ }
104
+ store.clear();
105
+ return { ok: true, message: 'The activation was released and the local licence state removed. The key can now be activated on another machine.' };
106
+ }
107
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,57 @@
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
+ import { envName, PRODUCT } from '../product.js';
19
+ /** Form der erzeugten Kennung. */
20
+ export const INSTANCE_NAME_PATTERN = /^layoutbridge-[0-9a-f]{8}$/;
21
+ export function defaultInstanceNamePath(os = platform(), home = homedir(), product = PRODUCT) {
22
+ return licensePathApi(os).join(defaultLicenseDirectory(os, home, product), 'instance-name');
23
+ }
24
+ /** Eine neue Zufallskennung bilden. */
25
+ export function newInstanceName(random = randomBytes) {
26
+ return 'layoutbridge-' + random(4).toString('hex');
27
+ }
28
+ /**
29
+ * Die Kennung dieses Arbeitsplatzes: Umgebungsvariable, sonst die
30
+ * gespeicherte, sonst eine neue. Laesst sich die neue nicht speichern,
31
+ * wird sie trotzdem verwendet – die Aktivierung soll an einer nicht
32
+ * schreibbaren Datei nicht scheitern; der Hinweis geht auf die
33
+ * Standardfehlerausgabe.
34
+ */
35
+ export function resolveInstanceName(env = process.env, path = defaultInstanceNamePath(), product = PRODUCT) {
36
+ const fromEnv = env[envName('INSTANCE_NAME', product)];
37
+ if (fromEnv !== undefined && fromEnv.trim().length > 0)
38
+ return fromEnv.trim();
39
+ try {
40
+ const stored = readFileSync(path, 'utf8').trim();
41
+ if (INSTANCE_NAME_PATTERN.test(stored))
42
+ return stored;
43
+ }
44
+ catch {
45
+ /* keine Datei oder nicht lesbar – dann eine neue Kennung */
46
+ }
47
+ const created = newInstanceName();
48
+ try {
49
+ mkdirSync(dirname(path), { recursive: true });
50
+ writeFileSync(path, created + '\n', { encoding: 'utf8', mode: 0o600 });
51
+ }
52
+ catch (error) {
53
+ process.stderr.write(`[${product.packageName}] cannot store the instance name: ${String(error)}\n`);
54
+ }
55
+ return created;
56
+ }
57
+ //# sourceMappingURL=instance-name.js.map
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Schluesselquelle (E-60): Umgebungsvariable LAYOUTBRIDGE_SPM_LICENSE_KEY,
3
+ * sonst die Datei aus LAYOUTBRIDGE_SPM_LICENSE_FILE, sonst die Vorgabedatei
4
+ * neben der Serverkonfiguration. Wird bei jedem Aufruf gelesen, der eine
5
+ * Pruefung braucht – ein nachgelegter Schluessel wirkt ohne Neustart.
6
+ */
7
+ import { createHash } from 'node:crypto';
8
+ import { readFileSync } from 'node:fs';
9
+ import { homedir, platform } from 'node:os';
10
+ import { posix, win32 } from 'node:path';
11
+ import { envName, PRODUCT } from '../product.js';
12
+ /**
13
+ * Pfadregeln der Zielplattform, nicht des Wirtsrechners (Sitzung 25): so
14
+ * liefert ein Aufruf fuer 'darwin' auch unter Windows Schraegstriche und
15
+ * einer fuer 'win32' auch am Mac Backslashes – wie setupLicenseKeyPath in
16
+ * setup.ts. Im Betrieb ist die Plattform der Wirt; das Ergebnis bleibt gleich.
17
+ */
18
+ export function licensePathApi(os) {
19
+ return os === 'win32' ? win32 : posix;
20
+ }
21
+ export function defaultLicenseDirectory(os = platform(), home = homedir(), product = PRODUCT) {
22
+ const p = licensePathApi(os);
23
+ if (os === 'darwin')
24
+ return p.join(home, 'Library', 'Application Support', product.dataDirName);
25
+ return p.join(home, '.' + product.dataDirName);
26
+ }
27
+ export function defaultLicenseKeyPath(env = process.env, os = platform(), home = homedir(), product = PRODUCT) {
28
+ const fromEnv = env[envName('LICENSE_FILE', product)];
29
+ if (fromEnv !== undefined && fromEnv.length > 0)
30
+ return fromEnv;
31
+ return licensePathApi(os).join(defaultLicenseDirectory(os, home, product), 'license.key');
32
+ }
33
+ export function defaultLicenseStatePath(env = process.env, os = platform(), home = homedir(), product = PRODUCT) {
34
+ const fromEnv = env[envName('LICENSE_STATE', product)];
35
+ if (fromEnv !== undefined && fromEnv.length > 0)
36
+ return fromEnv;
37
+ return licensePathApi(os).join(defaultLicenseDirectory(os, home, product), 'license-state.json');
38
+ }
39
+ /** Kurzer Fingerabdruck fuer Speicher und Protokoll – nie der Schluessel selbst. */
40
+ export function fingerprint(key) {
41
+ return createHash('sha256').update(key, 'utf8').digest('hex').slice(0, 16);
42
+ }
43
+ /** Schluessel suchen. Leere Werte zaehlen als nicht vorhanden. */
44
+ export function readLicenseKey(env = process.env, path = defaultLicenseKeyPath(env), product = PRODUCT) {
45
+ const fromEnv = env[envName('LICENSE_KEY', product)];
46
+ if (fromEnv !== undefined && fromEnv.trim().length > 0) {
47
+ return { key: fromEnv.trim(), source: 'env', path };
48
+ }
49
+ try {
50
+ const text = readFileSync(path, 'utf8').trim();
51
+ if (text.length > 0)
52
+ return { key: text, source: 'file', path };
53
+ }
54
+ catch {
55
+ /* keine Datei oder nicht lesbar = kein Schluessel */
56
+ }
57
+ return { key: null, source: null, path };
58
+ }
59
+ //# sourceMappingURL=key-source.js.map
@@ -0,0 +1,207 @@
1
+ /**
2
+ * Prueffunktion des Zahlungsdienstes (A-6 Ergaenzung 06.09.2026).
3
+ *
4
+ * Drei Endpunkte der License API, alle ohne API-Schluessel und damit ohne
5
+ * Geheimnis im Connector: activate, validate, deactivate. Kein SDK, keine
6
+ * neue Laufzeitabhaengigkeit – der Aufruf geht ueber fetch aus Node ab
7
+ * Version 20.
8
+ *
9
+ * Aufrufmuster (Ergaenzung Nr. 4): ohne gespeicherte Instanzkennung
10
+ * aktivieren, sonst pruefen; meldet die Pruefung fuer die gespeicherte
11
+ * Instanz keinen Erfolg und ist der Schluessel selbst weder abgelaufen noch
12
+ * gesperrt (Instanz beim Haendler entfernt), genau einmal neu aktivieren.
13
+ *
14
+ * Der Lizenzschluessel erscheint nie in einer Meldung: was der Dienst
15
+ * zurueckschreibt, wird vor der Weitergabe um den Schluessel bereinigt.
16
+ */
17
+ import { describeProductIds, productIdsConfigured, } from './product-ids.js';
18
+ import { SUPPORT_EMAIL } from '../product-info.js';
19
+ export const DEFAULT_BASE_URL = 'https://api.lemonsqueezy.com';
20
+ export const DEFAULT_TIMEOUT_MS = 10_000;
21
+ function record(value) {
22
+ if (typeof value !== 'object' || value === null || Array.isArray(value))
23
+ return null;
24
+ return value;
25
+ }
26
+ function text(value) {
27
+ return typeof value === 'string' && value.length > 0 ? value : null;
28
+ }
29
+ function numberOrNull(value) {
30
+ return typeof value === 'number' && Number.isFinite(value) ? value : null;
31
+ }
32
+ /** Den Schluessel aus jedem Text entfernen, der nach aussen geht. */
33
+ export function redactKey(message, key) {
34
+ if (key.length === 0)
35
+ return message;
36
+ return message.split(key).join('<key>');
37
+ }
38
+ export class LemonSqueezyVerifier {
39
+ name = 'lemonsqueezy';
40
+ productIds;
41
+ baseUrl;
42
+ timeoutMs;
43
+ fetchImpl;
44
+ now;
45
+ constructor(options) {
46
+ this.productIds = options.productIds;
47
+ this.baseUrl = options.baseUrl ?? DEFAULT_BASE_URL;
48
+ this.timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
49
+ this.fetchImpl = options.fetchImpl ?? ((input, init) => fetch(input, init));
50
+ this.now = options.now ?? (() => new Date());
51
+ }
52
+ /** Eine Zeile fuer die Startmeldung des Servers. */
53
+ describe() {
54
+ return `${this.name} (${this.baseUrl}, product ids: ${describeProductIds(this.productIds)})`;
55
+ }
56
+ async verify(key, context) {
57
+ if (context.instance_id === null) {
58
+ return this.interpret(await this.call('activate', { license_key: key, instance_name: context.instance_name }), key);
59
+ }
60
+ const validated = await this.call('validate', { license_key: key, instance_id: context.instance_id });
61
+ const outcome = this.interpret(validated, key, context.instance_id);
62
+ if (outcome.status !== 'invalid' || !this.instanceLikelyGone(validated))
63
+ return outcome;
64
+ /* Instanz beim Haendler entfernt: genau ein Aktivierungsversuch. */
65
+ return this.interpret(await this.call('activate', { license_key: key, instance_name: context.instance_name }), key);
66
+ }
67
+ async deactivate(key, instanceId) {
68
+ const answer = await this.call('deactivate', { license_key: key, instance_id: instanceId });
69
+ if (answer.body === null) {
70
+ throw new Error('The licence service could not be reached: ' + answer.reason);
71
+ }
72
+ if (answer.body['deactivated'] !== true) {
73
+ const reason = text(answer.body['error']) ?? 'the service refused the deactivation';
74
+ throw new Error(redactKey(reason, key));
75
+ }
76
+ }
77
+ /**
78
+ * Der Schluessel selbst ist noch brauchbar, nur die Instanz nicht mehr:
79
+ * Antwort auswertbar, ein license_key-Block vorhanden, dessen Zustand
80
+ * weder abgelaufen noch gesperrt ist.
81
+ */
82
+ instanceLikelyGone(answer) {
83
+ if (answer.body === null)
84
+ return false;
85
+ /* Live-Befund 15.09.2026 (Test-Store): bei unbekannter Instanz antwortet
86
+ der Dienst mit HTTP 404 {"valid":false,"error":"instance_id not found."}
87
+ - ohne license_key-Block. Der Wortlaut allein genuegt dann. */
88
+ const serviceError = text(answer.body['error']);
89
+ if (serviceError !== null && /\binstance(_id)?\b[^.]*not found/i.test(serviceError))
90
+ return true;
91
+ const licenseKey = record(answer.body['license_key']);
92
+ if (licenseKey === null)
93
+ return false;
94
+ const status = text(licenseKey['status']);
95
+ return status !== 'expired' && status !== 'disabled';
96
+ }
97
+ async call(endpoint, fields) {
98
+ const body = new URLSearchParams(fields).toString();
99
+ let response;
100
+ try {
101
+ response = await this.fetchImpl(`${this.baseUrl}/v1/licenses/${endpoint}`, {
102
+ method: 'POST',
103
+ headers: {
104
+ Accept: 'application/json',
105
+ 'Content-Type': 'application/x-www-form-urlencoded',
106
+ },
107
+ body,
108
+ signal: AbortSignal.timeout(this.timeoutMs),
109
+ });
110
+ }
111
+ catch (error) {
112
+ return { body: null, httpStatus: null, reason: 'request failed (' + String(error) + ')' };
113
+ }
114
+ const httpStatus = response.status;
115
+ if (httpStatus >= 500 || httpStatus === 429) {
116
+ return { body: null, httpStatus, reason: 'the service answered HTTP ' + httpStatus };
117
+ }
118
+ let parsed;
119
+ try {
120
+ parsed = await response.json();
121
+ }
122
+ catch {
123
+ return { body: null, httpStatus, reason: 'the answer was not readable JSON (HTTP ' + httpStatus + ')' };
124
+ }
125
+ const asRecord = record(parsed);
126
+ if (asRecord === null) {
127
+ return { body: null, httpStatus, reason: 'the answer was not a JSON object (HTTP ' + httpStatus + ')' };
128
+ }
129
+ return { body: asRecord, httpStatus, reason: '' };
130
+ }
131
+ /** Antwort des Dienstes auf die Ergebnisse aus S11 abbilden. */
132
+ interpret(answer, key, storedInstanceId = null) {
133
+ if (answer.body === null) {
134
+ return { status: 'unreachable', message: 'The licence service could not be reached: ' + answer.reason };
135
+ }
136
+ const body = answer.body;
137
+ const serviceError = text(body['error']);
138
+ const licenseKey = record(body['license_key']);
139
+ const meta = record(body['meta']);
140
+ const instance = record(body['instance']);
141
+ const expiresAt = licenseKey === null ? null : text(licenseKey['expires_at']);
142
+ const instanceId = (instance === null ? null : text(instance['id'])) ?? storedInstanceId;
143
+ /* 1. Produkt vor allem anderen – auch wenn der Dienst "gueltig" sagt. */
144
+ const mismatch = this.productMismatch(meta);
145
+ if (mismatch !== null) {
146
+ return this.outcome('product_mismatch', mismatch, key, expiresAt, instanceId);
147
+ }
148
+ /* 2. Abgelaufen. */
149
+ const status = licenseKey === null ? null : text(licenseKey['status']);
150
+ if (status === 'expired' || this.expiryPassed(expiresAt)) {
151
+ const reason = serviceError ?? 'The licence has expired.';
152
+ return this.outcome('expired', reason, key, expiresAt, instanceId);
153
+ }
154
+ /* 3. Erfolg. */
155
+ const succeeded = body['activated'] === true || body['valid'] === true;
156
+ if (succeeded && status === 'active') {
157
+ return this.outcome('valid', 'The licence key is valid.', key, expiresAt, instanceId);
158
+ }
159
+ /* 4. Alles Uebrige. */
160
+ let reason = serviceError ?? 'The service did not confirm the licence key (HTTP ' + String(answer.httpStatus) + ').';
161
+ if (serviceError !== null && serviceError.toLowerCase().includes('activation limit')) {
162
+ reason +=
163
+ ' Release the licence on the machine that uses it (run the server with --deactivate there) or ask LayoutBridge support (' + SUPPORT_EMAIL + ') to free the activation.';
164
+ }
165
+ else if (succeeded && status !== 'active') {
166
+ reason = 'The service confirmed the key but reports the state "' + String(status) + '".';
167
+ }
168
+ return this.outcome('invalid', reason, key, expiresAt, instanceId);
169
+ }
170
+ outcome(status, message, key, expiresAt, instanceId) {
171
+ return {
172
+ status,
173
+ message: redactKey(message, key),
174
+ expires_at: expiresAt,
175
+ instance_id: instanceId,
176
+ };
177
+ }
178
+ expiryPassed(expiresAt) {
179
+ if (expiresAt === null)
180
+ return false;
181
+ const ms = Date.parse(expiresAt);
182
+ if (Number.isNaN(ms))
183
+ return false;
184
+ return ms <= this.now().getTime();
185
+ }
186
+ /** null = in Ordnung; sonst der Klartext der Abweichung. */
187
+ productMismatch(meta) {
188
+ if (!productIdsConfigured(this.productIds))
189
+ return null;
190
+ if (meta === null)
191
+ return null;
192
+ const storeId = numberOrNull(meta['store_id']);
193
+ const productId = numberOrNull(meta['product_id']);
194
+ const variantId = numberOrNull(meta['variant_id']);
195
+ if (this.productIds.store_id !== null && storeId !== this.productIds.store_id) {
196
+ return 'The licence key belongs to another store.';
197
+ }
198
+ if (this.productIds.product_id !== null && productId !== this.productIds.product_id) {
199
+ return 'The licence key belongs to another product.';
200
+ }
201
+ if (this.productIds.variant_ids.length > 0 && (variantId === null || !this.productIds.variant_ids.includes(variantId))) {
202
+ return 'The licence key belongs to another product variant.';
203
+ }
204
+ return null;
205
+ }
206
+ }
207
+ //# sourceMappingURL=lemonsqueezy-verifier.js.map
@@ -0,0 +1,10 @@
1
+ export class PlaceholderVerifier {
2
+ name = 'placeholder';
3
+ async verify(_key, _context) {
4
+ return {
5
+ status: 'unreachable',
6
+ message: 'No licence service is wired into this build; the key cannot be verified online.',
7
+ };
8
+ }
9
+ }
10
+ //# sourceMappingURL=placeholder-verifier.js.map