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,58 @@
|
|
|
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
|
+
/**
|
|
12
|
+
* Pfadregeln der Zielplattform, nicht des Wirtsrechners (Sitzung 25): so
|
|
13
|
+
* liefert ein Aufruf fuer 'darwin' auch unter Windows Schraegstriche und
|
|
14
|
+
* einer fuer 'win32' auch am Mac Backslashes – wie setupLicenseKeyPath in
|
|
15
|
+
* setup.ts. Im Betrieb ist die Plattform der Wirt; das Ergebnis bleibt gleich.
|
|
16
|
+
*/
|
|
17
|
+
export function licensePathApi(os) {
|
|
18
|
+
return os === 'win32' ? win32 : posix;
|
|
19
|
+
}
|
|
20
|
+
export function defaultLicenseDirectory(os = platform(), home = homedir()) {
|
|
21
|
+
const p = licensePathApi(os);
|
|
22
|
+
if (os === 'darwin')
|
|
23
|
+
return p.join(home, 'Library', 'Application Support', 'layoutbridge-spm');
|
|
24
|
+
return p.join(home, '.layoutbridge-spm');
|
|
25
|
+
}
|
|
26
|
+
export function defaultLicenseKeyPath(env = process.env, os = platform(), home = homedir()) {
|
|
27
|
+
const fromEnv = env['LAYOUTBRIDGE_SPM_LICENSE_FILE'];
|
|
28
|
+
if (fromEnv !== undefined && fromEnv.length > 0)
|
|
29
|
+
return fromEnv;
|
|
30
|
+
return licensePathApi(os).join(defaultLicenseDirectory(os, home), 'license.key');
|
|
31
|
+
}
|
|
32
|
+
export function defaultLicenseStatePath(env = process.env, os = platform(), home = homedir()) {
|
|
33
|
+
const fromEnv = env['LAYOUTBRIDGE_SPM_LICENSE_STATE'];
|
|
34
|
+
if (fromEnv !== undefined && fromEnv.length > 0)
|
|
35
|
+
return fromEnv;
|
|
36
|
+
return licensePathApi(os).join(defaultLicenseDirectory(os, home), 'license-state.json');
|
|
37
|
+
}
|
|
38
|
+
/** Kurzer Fingerabdruck fuer Speicher und Protokoll – nie der Schluessel selbst. */
|
|
39
|
+
export function fingerprint(key) {
|
|
40
|
+
return createHash('sha256').update(key, 'utf8').digest('hex').slice(0, 16);
|
|
41
|
+
}
|
|
42
|
+
/** Schluessel suchen. Leere Werte zaehlen als nicht vorhanden. */
|
|
43
|
+
export function readLicenseKey(env = process.env, path = defaultLicenseKeyPath(env)) {
|
|
44
|
+
const fromEnv = env['LAYOUTBRIDGE_SPM_LICENSE_KEY'];
|
|
45
|
+
if (fromEnv !== undefined && fromEnv.trim().length > 0) {
|
|
46
|
+
return { key: fromEnv.trim(), source: 'env', path };
|
|
47
|
+
}
|
|
48
|
+
try {
|
|
49
|
+
const text = readFileSync(path, 'utf8').trim();
|
|
50
|
+
if (text.length > 0)
|
|
51
|
+
return { key: text, source: 'file', path };
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
/* keine Datei oder nicht lesbar = kein Schluessel */
|
|
55
|
+
}
|
|
56
|
+
return { key: null, source: null, path };
|
|
57
|
+
}
|
|
58
|
+
//# 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
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hinterlegte Kennungen des Zahlungsdienstes Polar (Auftrag A-15 Nr. 2,
|
|
3
|
+
* 23.09.2026).
|
|
4
|
+
*
|
|
5
|
+
* Polar arbeitet mit UUIDs statt Zahlen: die Organisation
|
|
6
|
+
* (organization_id) gehoert in jeden Aufruf, der Benefit (benefit_id)
|
|
7
|
+
* benennt den Lizenzschluessel-Vorteil des Produkts. Store, Produkt und
|
|
8
|
+
* Variante wie bei lemonsqueezy gibt es hier nicht; die Kennungen von
|
|
9
|
+
* lemonsqueezy in product-ids.ts bleiben davon unberuehrt.
|
|
10
|
+
*
|
|
11
|
+
* Hinterlegt sind die Kennungen des Live-Kontos (Organisation "MCP
|
|
12
|
+
* Interfaces"), wie sie das Auftragsdokument unter A-15a Teil 2
|
|
13
|
+
* (23.09.2026, 07:14 Uhr) nennt: organization_id aus Settings >
|
|
14
|
+
* Preferences (Feld "Identifier"), benefit_id aus der Adresse der
|
|
15
|
+
* Benefit-Seite (Vermerk E-85). Bestaetigt durch die erste validate-Antwort
|
|
16
|
+
* mit dem Live-Schluessel fuer Saschas Arbeitsplatz (Nachtrag 23.09.2026,
|
|
17
|
+
* 08:29 Uhr) und die Live-Probe von A-9 (Vermerk E-86): beide Werte
|
|
18
|
+
* stimmen mit der Antwort ueberein.
|
|
19
|
+
*
|
|
20
|
+
* Fuer die Sandbox-Probe lassen sie sich von aussen ueberschreiben – ueber
|
|
21
|
+
* Umgebungsvariablen und ausschliesslich in der Bauphase, nie ueber
|
|
22
|
+
* config.json: config.json ist Kundenkonfiguration und koennte damit den
|
|
23
|
+
* Produktabgleich aushebeln.
|
|
24
|
+
*/
|
|
25
|
+
/** Kennungen des Live-Kontos (A-15a Teil 2, 23.09.2026). */
|
|
26
|
+
export const POLAR_IDS = {
|
|
27
|
+
organization_id: 'e8d80621-fb90-45e2-b0b6-73a94271fe18',
|
|
28
|
+
benefit_id: 'e9b61756-779b-4d65-83b9-f398346f6184',
|
|
29
|
+
};
|
|
30
|
+
/** Name der Umgebungsvariablen der Bauphase. */
|
|
31
|
+
export const POLAR_ORG_ID_ENV = 'LAYOUTBRIDGE_SPM_LICENSE_POLAR_ORG_ID';
|
|
32
|
+
export const POLAR_BENEFIT_ID_ENV = 'LAYOUTBRIDGE_SPM_LICENSE_POLAR_BENEFIT_ID';
|
|
33
|
+
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
34
|
+
/** Nur eine UUID zaehlt als Kennung; alles andere gilt als nicht gesetzt. */
|
|
35
|
+
function uuidFromEnv(value) {
|
|
36
|
+
if (value === undefined)
|
|
37
|
+
return null;
|
|
38
|
+
const trimmed = value.trim();
|
|
39
|
+
if (!UUID.test(trimmed))
|
|
40
|
+
return null;
|
|
41
|
+
return trimmed.toLowerCase();
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Kennungen fuer diesen Lauf: die hinterlegten Konstanten, einzeln
|
|
45
|
+
* ueberschreibbar durch Umgebungsvariablen der Bauphase.
|
|
46
|
+
*/
|
|
47
|
+
export function resolvePolarIds(env = process.env, base = POLAR_IDS) {
|
|
48
|
+
return {
|
|
49
|
+
organization_id: uuidFromEnv(env[POLAR_ORG_ID_ENV]) ?? base.organization_id,
|
|
50
|
+
benefit_id: uuidFromEnv(env[POLAR_BENEFIT_ID_ENV]) ?? base.benefit_id,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
/** Ist ueberhaupt eine Kennung hinterlegt? */
|
|
54
|
+
export function polarIdsConfigured(ids) {
|
|
55
|
+
return ids.organization_id !== null || ids.benefit_id !== null;
|
|
56
|
+
}
|
|
57
|
+
/** Kurzfassung fuer Startzeile und Protokoll. */
|
|
58
|
+
export function describePolarIds(ids) {
|
|
59
|
+
if (!polarIdsConfigured(ids))
|
|
60
|
+
return 'not set';
|
|
61
|
+
return `organization ${ids.organization_id ?? '-'}, benefit ${ids.benefit_id ?? '-'}`;
|
|
62
|
+
}
|
|
63
|
+
//# sourceMappingURL=polar-ids.js.map
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prueffunktion des Zahlungsdienstes Polar (Auftrag A-15, 23.09.2026;
|
|
3
|
+
* Beobachtungen aus der Sandbox in Polar-Pruefung_2026-09-23.md, 3b).
|
|
4
|
+
*
|
|
5
|
+
* Gebaut wie lemonsqueezy-verifier.ts und **neben** dieser Klasse: drei
|
|
6
|
+
* Endpunkte des Kundenportals – activate, validate, deactivate –, alle ohne
|
|
7
|
+
* Anmeldung und damit ohne Geheimnis im Connector, Rumpf JSON, kein
|
|
8
|
+
* Authorization-Header. Kein SDK, keine neue Laufzeitabhaengigkeit; der
|
|
9
|
+
* Aufruf geht ueber fetch aus Node ab Version 20, Zeitueberschreitung 10 s.
|
|
10
|
+
*
|
|
11
|
+
* Unterschiede zu lemonsqueezy, die den Bau bestimmen:
|
|
12
|
+
* - Kennungen sind UUIDs (organization_id, benefit_id) statt Zahlen.
|
|
13
|
+
* - Zustaende heissen granted / revoked / disabled.
|
|
14
|
+
* - Die Aktivierung kommt bei activate als `id` (nicht `instance.id`);
|
|
15
|
+
* validate traegt die Felder des Schluessels unmittelbar im Rumpf und die
|
|
16
|
+
* Aktivierung in `activation`.
|
|
17
|
+
* - Gesperrt oder gekuendigt kommt als HTTP 404 mit eigenem Wortlaut, nicht
|
|
18
|
+
* als Zustand; deactivate antwortet mit HTTP 204 ohne Rumpf.
|
|
19
|
+
* - Ohne Anmeldung gelten 3 Aufrufe je Sekunde; HTTP 429 zaehlt als
|
|
20
|
+
* "nicht erreichbar", und zwischen zwei eigenen Aufrufen liegt eine Pause.
|
|
21
|
+
*
|
|
22
|
+
* Aufrufmuster (A-15 Nr. 4): ohne gespeicherte Aktivierung aktivieren, sonst
|
|
23
|
+
* mit activation_id pruefen. Kommt 404 "Not found" (die Aktivierung wurde
|
|
24
|
+
* beim Haendler oder vom Kunden im Portal entfernt), genau einmal neu
|
|
25
|
+
* aktivieren; scheitert das, bleibt es beim Ergebnis dieses Versuchs.
|
|
26
|
+
*
|
|
27
|
+
* Die Antworten enthalten Kundendaten (`customer`, `user`). Sie werden
|
|
28
|
+
* weder uebernommen noch protokolliert (A-8); nach aussen gehen nur
|
|
29
|
+
* Zustand, Ablauf und die Aktivierungskennung. Der Lizenzschluessel
|
|
30
|
+
* erscheint in keiner Meldung.
|
|
31
|
+
*/
|
|
32
|
+
import { describePolarIds, polarIdsConfigured, } from './polar-ids.js';
|
|
33
|
+
import { SUPPORT_EMAIL } from '../product-info.js';
|
|
34
|
+
export const DEFAULT_BASE_URL = 'https://api.polar.sh';
|
|
35
|
+
/** Sandbox; nur in der Bauphase, ueber die Umgebungsvariable unten. */
|
|
36
|
+
export const SANDBOX_BASE_URL = 'https://sandbox-api.polar.sh';
|
|
37
|
+
export const POLAR_SANDBOX_ENV = 'LAYOUTBRIDGE_SPM_LICENSE_POLAR_SANDBOX';
|
|
38
|
+
export const DEFAULT_TIMEOUT_MS = 10_000;
|
|
39
|
+
/** Abstand vor einem zweiten eigenen Aufruf (Grenze des Dienstes: 3/s). */
|
|
40
|
+
export const DEFAULT_RETRY_DELAY_MS = 1_000;
|
|
41
|
+
function record(value) {
|
|
42
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value))
|
|
43
|
+
return null;
|
|
44
|
+
return value;
|
|
45
|
+
}
|
|
46
|
+
function text(value) {
|
|
47
|
+
return typeof value === 'string' && value.length > 0 ? value : null;
|
|
48
|
+
}
|
|
49
|
+
/** Einen Satz sauber abschliessen, damit zwei Saetze nicht zusammenlaufen. */
|
|
50
|
+
function sentence(value) {
|
|
51
|
+
const trimmed = value.trimEnd();
|
|
52
|
+
return /[.!?]$/.test(trimmed) ? trimmed : trimmed + '.';
|
|
53
|
+
}
|
|
54
|
+
/** Den Schluessel aus jedem Text entfernen, der nach aussen geht. */
|
|
55
|
+
export function redactKey(message, key) {
|
|
56
|
+
if (key.length === 0)
|
|
57
|
+
return message;
|
|
58
|
+
return message.split(key).join('<key>');
|
|
59
|
+
}
|
|
60
|
+
/** Basisadresse fuer diesen Lauf: live, in der Bauphase auf Wunsch Sandbox. */
|
|
61
|
+
export function resolvePolarBaseUrl(env = process.env) {
|
|
62
|
+
return env[POLAR_SANDBOX_ENV] === '1' ? SANDBOX_BASE_URL : DEFAULT_BASE_URL;
|
|
63
|
+
}
|
|
64
|
+
export class PolarVerifier {
|
|
65
|
+
name = 'polar';
|
|
66
|
+
ids;
|
|
67
|
+
baseUrl;
|
|
68
|
+
timeoutMs;
|
|
69
|
+
fetchImpl;
|
|
70
|
+
now;
|
|
71
|
+
retryDelayMs;
|
|
72
|
+
constructor(options) {
|
|
73
|
+
this.ids = options.ids;
|
|
74
|
+
this.baseUrl = options.baseUrl ?? DEFAULT_BASE_URL;
|
|
75
|
+
this.timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
76
|
+
this.fetchImpl = options.fetchImpl ?? ((input, init) => fetch(input, init));
|
|
77
|
+
this.now = options.now ?? (() => new Date());
|
|
78
|
+
this.retryDelayMs = options.retryDelayMs ?? DEFAULT_RETRY_DELAY_MS;
|
|
79
|
+
}
|
|
80
|
+
/** Eine Zeile fuer die Startmeldung des Servers. */
|
|
81
|
+
describe() {
|
|
82
|
+
return `${this.name} (${this.baseUrl}, ids: ${describePolarIds(this.ids)})`;
|
|
83
|
+
}
|
|
84
|
+
async verify(key, context) {
|
|
85
|
+
if (context.instance_id === null) {
|
|
86
|
+
return this.interpret(await this.activate(key, context.instance_name), key);
|
|
87
|
+
}
|
|
88
|
+
const validated = await this.call('validate', { key, activation_id: context.instance_id });
|
|
89
|
+
const outcome = this.interpret(validated, key, context.instance_id);
|
|
90
|
+
if (outcome.status !== 'invalid' || !this.activationLikelyGone(validated))
|
|
91
|
+
return outcome;
|
|
92
|
+
/* Aktivierung entfernt: genau ein Aktivierungsversuch, mit Abstand. */
|
|
93
|
+
await this.pause();
|
|
94
|
+
return this.interpret(await this.activate(key, context.instance_name), key);
|
|
95
|
+
}
|
|
96
|
+
async deactivate(key, instanceId) {
|
|
97
|
+
const answer = await this.call('deactivate', { key, activation_id: instanceId });
|
|
98
|
+
if (answer.httpStatus === 204)
|
|
99
|
+
return;
|
|
100
|
+
if (answer.body === null) {
|
|
101
|
+
throw new Error('The licence service could not be reached: ' + answer.reason);
|
|
102
|
+
}
|
|
103
|
+
const reason = text(answer.body['detail']) ??
|
|
104
|
+
text(answer.body['error']) ??
|
|
105
|
+
'the service refused the deactivation (HTTP ' + String(answer.httpStatus) + ')';
|
|
106
|
+
throw new Error(redactKey(reason, key));
|
|
107
|
+
}
|
|
108
|
+
activate(key, label) {
|
|
109
|
+
return this.call('activate', { key, label });
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Der Schluessel selbst ist noch brauchbar, nur die Aktivierung nicht
|
|
113
|
+
* mehr: HTTP 404 mit dem Wortlaut "Not found" (Sandbox 23.09.2026). Der
|
|
114
|
+
* eigene Wortlaut fuer einen erloschenen Schluessel ("no longer active")
|
|
115
|
+
* gehoert nicht dazu – dort waere ein zweiter Aufruf sinnlos.
|
|
116
|
+
*/
|
|
117
|
+
activationLikelyGone(answer) {
|
|
118
|
+
if (answer.httpStatus !== 404 || answer.body === null)
|
|
119
|
+
return false;
|
|
120
|
+
if (this.keyGone(answer.body))
|
|
121
|
+
return false;
|
|
122
|
+
return true;
|
|
123
|
+
}
|
|
124
|
+
/** 404 mit dem Hinweis, dass der Schluessel selbst erloschen ist. */
|
|
125
|
+
keyGone(body) {
|
|
126
|
+
const detail = text(body['detail']);
|
|
127
|
+
return detail !== null && /no longer active/i.test(detail);
|
|
128
|
+
}
|
|
129
|
+
pause() {
|
|
130
|
+
if (this.retryDelayMs <= 0)
|
|
131
|
+
return Promise.resolve();
|
|
132
|
+
return new Promise((resolve) => {
|
|
133
|
+
setTimeout(resolve, this.retryDelayMs);
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
async call(endpoint, fields) {
|
|
137
|
+
/* organization_id gehoert in jeden Aufruf, sobald sie hinterlegt ist. */
|
|
138
|
+
const payload = { ...fields };
|
|
139
|
+
if (this.ids.organization_id !== null)
|
|
140
|
+
payload['organization_id'] = this.ids.organization_id;
|
|
141
|
+
let response;
|
|
142
|
+
try {
|
|
143
|
+
response = await this.fetchImpl(`${this.baseUrl}/v1/customer-portal/license-keys/${endpoint}`, {
|
|
144
|
+
method: 'POST',
|
|
145
|
+
headers: {
|
|
146
|
+
Accept: 'application/json',
|
|
147
|
+
'Content-Type': 'application/json',
|
|
148
|
+
},
|
|
149
|
+
body: JSON.stringify(payload),
|
|
150
|
+
signal: AbortSignal.timeout(this.timeoutMs),
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
catch (error) {
|
|
154
|
+
return { body: null, httpStatus: null, endpoint, reason: 'request failed (' + String(error) + ')' };
|
|
155
|
+
}
|
|
156
|
+
const httpStatus = response.status;
|
|
157
|
+
/* Erfolg ohne Rumpf: deactivate antwortet mit 204. */
|
|
158
|
+
if (httpStatus === 204) {
|
|
159
|
+
return { body: {}, httpStatus, endpoint, reason: '' };
|
|
160
|
+
}
|
|
161
|
+
if (httpStatus >= 500 || httpStatus === 429) {
|
|
162
|
+
return { body: null, httpStatus, endpoint, reason: 'the service answered HTTP ' + httpStatus };
|
|
163
|
+
}
|
|
164
|
+
let parsed;
|
|
165
|
+
try {
|
|
166
|
+
parsed = await response.json();
|
|
167
|
+
}
|
|
168
|
+
catch {
|
|
169
|
+
return { body: null, httpStatus, endpoint, reason: 'the answer was not readable JSON (HTTP ' + httpStatus + ')' };
|
|
170
|
+
}
|
|
171
|
+
const asRecord = record(parsed);
|
|
172
|
+
if (asRecord === null) {
|
|
173
|
+
return { body: null, httpStatus, endpoint, reason: 'the answer was not a JSON object (HTTP ' + httpStatus + ')' };
|
|
174
|
+
}
|
|
175
|
+
return { body: asRecord, httpStatus, endpoint, reason: '' };
|
|
176
|
+
}
|
|
177
|
+
/** Antwort des Dienstes auf die Ergebnisse aus S11 abbilden (A-15 Nr. 4). */
|
|
178
|
+
interpret(answer, key, storedActivationId = null) {
|
|
179
|
+
if (answer.body === null) {
|
|
180
|
+
return { status: 'unreachable', message: 'The licence service could not be reached: ' + answer.reason };
|
|
181
|
+
}
|
|
182
|
+
const body = answer.body;
|
|
183
|
+
/* activate antwortet mit einem license_key-Block, validate mit den
|
|
184
|
+
Feldern des Schluessels unmittelbar im Rumpf. */
|
|
185
|
+
const licenseKey = record(body['license_key']) ?? body;
|
|
186
|
+
const detail = text(body['detail']);
|
|
187
|
+
const expiresAt = text(licenseKey['expires_at']);
|
|
188
|
+
const activationId = this.activationId(answer, storedActivationId);
|
|
189
|
+
/* 1. Produkt vor allem anderen – auch wenn der Dienst "gueltig" sagt. */
|
|
190
|
+
const mismatch = this.productMismatch(licenseKey);
|
|
191
|
+
if (mismatch !== null) {
|
|
192
|
+
return this.outcome('product_mismatch', mismatch, key, expiresAt, activationId);
|
|
193
|
+
}
|
|
194
|
+
/* 2. Abgelaufen, gesperrt oder erloschen. */
|
|
195
|
+
const status = text(licenseKey['status']);
|
|
196
|
+
if (status === 'revoked' || this.expiryPassed(expiresAt) || this.keyGone(body)) {
|
|
197
|
+
const reason = detail ?? 'The licence has expired.';
|
|
198
|
+
return this.outcome('expired', reason, key, expiresAt, activationId);
|
|
199
|
+
}
|
|
200
|
+
/* 3. Erfolg. */
|
|
201
|
+
if (answer.httpStatus === 200 && status === 'granted') {
|
|
202
|
+
return this.outcome('valid', 'The licence key is valid.', key, expiresAt, activationId);
|
|
203
|
+
}
|
|
204
|
+
/* 4. Alles Uebrige. */
|
|
205
|
+
let reason = detail ?? text(body['error']) ?? 'The service did not confirm the licence key (HTTP ' + String(answer.httpStatus) + ').';
|
|
206
|
+
if (answer.httpStatus === 403 || /activation limit/i.test(reason)) {
|
|
207
|
+
/* Der Wortlaut des Dienstes endet ohne Punkt (Sandbox 23.09.2026). */
|
|
208
|
+
reason =
|
|
209
|
+
sentence(reason) +
|
|
210
|
+
' 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.';
|
|
211
|
+
}
|
|
212
|
+
else if (answer.httpStatus === 200 && status !== null && status !== 'granted') {
|
|
213
|
+
reason = 'The service confirmed the key but reports the state "' + status + '".';
|
|
214
|
+
}
|
|
215
|
+
return this.outcome('invalid', reason, key, expiresAt, activationId);
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* activate liefert die Aktivierung als `id` im Rumpf, validate als
|
|
219
|
+
* `activation.id`; sonst bleibt die gespeicherte Kennung stehen.
|
|
220
|
+
*/
|
|
221
|
+
activationId(answer, storedActivationId) {
|
|
222
|
+
const body = answer.body;
|
|
223
|
+
if (body === null)
|
|
224
|
+
return storedActivationId;
|
|
225
|
+
if (answer.endpoint === 'activate')
|
|
226
|
+
return text(body['id']) ?? storedActivationId;
|
|
227
|
+
const activation = record(body['activation']);
|
|
228
|
+
const fromActivation = activation === null ? null : text(activation['id']);
|
|
229
|
+
return fromActivation ?? storedActivationId;
|
|
230
|
+
}
|
|
231
|
+
outcome(status, message, key, expiresAt, activationId) {
|
|
232
|
+
return {
|
|
233
|
+
status,
|
|
234
|
+
message: redactKey(message, key),
|
|
235
|
+
expires_at: expiresAt,
|
|
236
|
+
instance_id: activationId,
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
expiryPassed(expiresAt) {
|
|
240
|
+
if (expiresAt === null)
|
|
241
|
+
return false;
|
|
242
|
+
const ms = Date.parse(expiresAt);
|
|
243
|
+
if (Number.isNaN(ms))
|
|
244
|
+
return false;
|
|
245
|
+
return ms <= this.now().getTime();
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* null = in Ordnung; sonst der Klartext der Abweichung. Verglichen wird
|
|
249
|
+
* nur, was hinterlegt ist und was die Antwort auch nennt.
|
|
250
|
+
*/
|
|
251
|
+
productMismatch(licenseKey) {
|
|
252
|
+
if (!polarIdsConfigured(this.ids))
|
|
253
|
+
return null;
|
|
254
|
+
const organizationId = text(licenseKey['organization_id']);
|
|
255
|
+
const benefitId = text(licenseKey['benefit_id']);
|
|
256
|
+
if (this.ids.organization_id !== null && organizationId !== null && organizationId.toLowerCase() !== this.ids.organization_id) {
|
|
257
|
+
return 'The licence key belongs to another organisation.';
|
|
258
|
+
}
|
|
259
|
+
if (this.ids.benefit_id !== null && benefitId !== null && benefitId.toLowerCase() !== this.ids.benefit_id) {
|
|
260
|
+
return 'The licence key belongs to another product.';
|
|
261
|
+
}
|
|
262
|
+
return null;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
//# sourceMappingURL=polar-verifier.js.map
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hinterlegte Produktkennungen des Zahlungsdienstes (A-6 Ergaenzung
|
|
3
|
+
* 06.09.2026, Nr. 4).
|
|
4
|
+
*
|
|
5
|
+
* Der Dienst kennt keine Versionsangabe. Es gibt genau ein Produkt mit den
|
|
6
|
+
* Varianten Monatsabo und Jahresabo; der Connector haelt store_id,
|
|
7
|
+
* product_id und die zulaessigen variant_id-Werte fest und vergleicht sie
|
|
8
|
+
* mit dem meta-Block der Antwort. product_version aus dem Kontext wird
|
|
9
|
+
* nicht gegen die Antwort geprueft, weil der Dienst dazu nichts liefert.
|
|
10
|
+
*
|
|
11
|
+
* Solange kein Store besteht, sind die Konstanten leer (null bzw. leere
|
|
12
|
+
* Liste). Fuer die Live-Probe lassen sie sich von aussen setzen – ueber
|
|
13
|
+
* Umgebungsvariablen und ausschliesslich in der Bauphase, nie ueber
|
|
14
|
+
* config.json: config.json ist Kundenkonfiguration und koennte damit den
|
|
15
|
+
* Produktabgleich aushebeln.
|
|
16
|
+
*/
|
|
17
|
+
/** Kennungen des Live-Stores. Traegt das Vermarktungsprojekt nach. */
|
|
18
|
+
export const PRODUCT_IDS = {
|
|
19
|
+
store_id: null,
|
|
20
|
+
product_id: null,
|
|
21
|
+
variant_ids: [],
|
|
22
|
+
};
|
|
23
|
+
function numberFromEnv(value) {
|
|
24
|
+
if (value === undefined)
|
|
25
|
+
return null;
|
|
26
|
+
const trimmed = value.trim();
|
|
27
|
+
if (trimmed.length === 0)
|
|
28
|
+
return null;
|
|
29
|
+
const parsed = Number(trimmed);
|
|
30
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
31
|
+
}
|
|
32
|
+
function numberListFromEnv(value) {
|
|
33
|
+
if (value === undefined)
|
|
34
|
+
return null;
|
|
35
|
+
const parts = value
|
|
36
|
+
.split(',')
|
|
37
|
+
.map((part) => part.trim())
|
|
38
|
+
.filter((part) => part.length > 0);
|
|
39
|
+
if (parts.length === 0)
|
|
40
|
+
return null;
|
|
41
|
+
const numbers = [];
|
|
42
|
+
for (const part of parts) {
|
|
43
|
+
const parsed = Number(part);
|
|
44
|
+
if (!Number.isFinite(parsed))
|
|
45
|
+
return null;
|
|
46
|
+
numbers.push(parsed);
|
|
47
|
+
}
|
|
48
|
+
return numbers;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Kennungen fuer diesen Lauf: die hinterlegten Konstanten, einzeln
|
|
52
|
+
* ueberschreibbar durch Umgebungsvariablen der Bauphase.
|
|
53
|
+
*/
|
|
54
|
+
export function resolveProductIds(env = process.env, base = PRODUCT_IDS) {
|
|
55
|
+
return {
|
|
56
|
+
store_id: numberFromEnv(env['LAYOUTBRIDGE_SPM_LICENSE_STORE_ID']) ?? base.store_id,
|
|
57
|
+
product_id: numberFromEnv(env['LAYOUTBRIDGE_SPM_LICENSE_PRODUCT_ID']) ?? base.product_id,
|
|
58
|
+
variant_ids: numberListFromEnv(env['LAYOUTBRIDGE_SPM_LICENSE_VARIANT_IDS']) ?? base.variant_ids,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
/** Ist ueberhaupt eine Kennung hinterlegt? */
|
|
62
|
+
export function productIdsConfigured(ids) {
|
|
63
|
+
return ids.store_id !== null || ids.product_id !== null || ids.variant_ids.length > 0;
|
|
64
|
+
}
|
|
65
|
+
/** Kurzfassung fuer Startzeile und Protokoll. */
|
|
66
|
+
export function describeProductIds(ids) {
|
|
67
|
+
if (!productIdsConfigured(ids))
|
|
68
|
+
return 'not set';
|
|
69
|
+
const variants = ids.variant_ids.length > 0 ? ids.variant_ids.join('/') : '-';
|
|
70
|
+
return `store ${ids.store_id ?? '-'}, product ${ids.product_id ?? '-'}, variant ${variants}`;
|
|
71
|
+
}
|
|
72
|
+
//# sourceMappingURL=product-ids.js.map
|