@tandem-language-exchange/content-store 1.3.1 → 1.3.2

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.
@@ -10,11 +10,11 @@ import {
10
10
  parseTranslationResourceRaw,
11
11
  toFlatStringMap,
12
12
  translationJsonOutputPath
13
- } from "./chunk-VBJ6LVMY.js";
13
+ } from "./chunk-LZHYKLAU.js";
14
14
  import {
15
15
  allProjects,
16
16
  defaultLocales
17
- } from "./chunk-RZLDRXNQ.js";
17
+ } from "./chunk-SF7FCBR2.js";
18
18
 
19
19
  // src/shared/bundles.ts
20
20
  import fs from "fs/promises";
@@ -286,4 +286,4 @@ export {
286
286
  fetchMergedTranslationBundles,
287
287
  queryCmsBundle
288
288
  };
289
- //# sourceMappingURL=chunk-D723FMZ2.js.map
289
+ //# sourceMappingURL=chunk-D2F7FQEM.js.map
@@ -15,7 +15,9 @@ var ContentStore = class {
15
15
  credentials: {
16
16
  accessKeyId: cfg.accessKeyId,
17
17
  secretAccessKey: cfg.secretAccessKey
18
- }
18
+ },
19
+ requestChecksumCalculation: "WHEN_REQUIRED",
20
+ responseChecksumValidation: "WHEN_REQUIRED"
19
21
  });
20
22
  this.bucket = cfg.bucket;
21
23
  }
@@ -179,4 +181,4 @@ export {
179
181
  toFlatStringMap,
180
182
  translationJsonOutputPath
181
183
  };
182
- //# sourceMappingURL=chunk-VBJ6LVMY.js.map
184
+ //# sourceMappingURL=chunk-LZHYKLAU.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/shared/s3.ts","../src/shared/translationResource.ts","../src/shared/utils.ts"],"sourcesContent":["import {\n S3Client,\n PutObjectCommand,\n GetObjectCommand,\n} from '@aws-sdk/client-s3';\nimport type { S3Config } from './types';\n\nexport class ContentStore {\n private client: S3Client;\n private bucket: string;\n\n constructor(cfg: S3Config) {\n this.client = new S3Client({\n region: cfg.region,\n credentials: {\n accessKeyId: cfg.accessKeyId,\n secretAccessKey: cfg.secretAccessKey,\n },\n requestChecksumCalculation: 'WHEN_REQUIRED',\n responseChecksumValidation: 'WHEN_REQUIRED',\n });\n this.bucket = cfg.bucket;\n }\n\n async upload(key: string, data: unknown): Promise<string> {\n await this.client.send(\n new PutObjectCommand({\n Bucket: this.bucket,\n Key: key,\n Body: JSON.stringify(data, null, 2),\n ContentType: 'application/json',\n }),\n );\n return key;\n }\n\n /** Raw UTF-8 body (e.g. Lingohub file bytes as text). */\n async uploadRaw(\n key: string,\n body: string,\n contentType: string,\n ): Promise<string> {\n await this.client.send(\n new PutObjectCommand({\n Bucket: this.bucket,\n Key: key,\n Body: body,\n ContentType: contentType,\n }),\n );\n return key;\n }\n\n async download(key: string): Promise<unknown> {\n const response = await this.client.send(\n new GetObjectCommand({ Bucket: this.bucket, Key: key }),\n );\n const body = await response.Body?.transformToString();\n if (!body) throw new Error(`Empty response for key: ${key}`);\n return JSON.parse(body);\n }\n\n /** Raw UTF-8 body from S3 (no JSON.parse). */\n async downloadRaw(key: string): Promise<string> {\n const response = await this.client.send(\n new GetObjectCommand({ Bucket: this.bucket, Key: key }),\n );\n const body = await response.Body?.transformToString();\n if (!body) throw new Error(`Empty response for key: ${key}`);\n return body;\n }\n}\n\n/** {cms}-{contentType}.json (always points at the latest version) */\nexport const buildCmsObjectKey = (cms: string, contentType: string): string => {\n return `${cms}-${contentType}.json`;\n}\n\nexport const buildTranslationObjectKey = (project:string, fileName: string, locale: string): string => {\n return `lingohub-${project}.${fileName.replaceAll('[locale]', locale)}`;\n}\n","import path from 'node:path';\nimport { convertXMLToJS, parseIOSStrings, transformObjectToFlat } from './utils';\nimport type { LingohubResource } from './lingohub';\n\n/** Content-Type for raw Lingohub bodies stored in S3. */\nexport function contentTypeForTranslationKey(objectKey: string): string {\n if (objectKey.endsWith('.json')) return 'application/json; charset=utf-8';\n if (objectKey.endsWith('.xml')) return 'application/xml; charset=utf-8';\n if (objectKey.endsWith('.strings')) return 'text/plain; charset=utf-8';\n return 'application/octet-stream';\n}\n\n/**\n * Parses a raw Lingohub file body from S3 into structured data.\n */\nexport function parseTranslationResourceRaw(\n raw: string,\n resource: LingohubResource,\n): unknown {\n if (resource.type === 'json') {\n return JSON.parse(raw) as unknown;\n }\n\n if (resource.type === 'strings') {\n return parseIOSStrings(raw);\n }\n\n if (resource.type === 'xml') {\n return convertXMLToJS(raw);\n }\n\n throw new Error(`Unsupported resource type: ${resource.type}`);\n}\n\n/**\n * Normalizes parsed translation data to a flat string map for merging (duplicate keys: last wins).\n */\nexport function toFlatStringMap(parsed: unknown): Record<string, string> {\n if (parsed === null || parsed === undefined) return {};\n if (typeof parsed === 'string') return { value: parsed };\n if (typeof parsed !== 'object') return { value: String(parsed) };\n if (Array.isArray(parsed)) {\n const out: Record<string, string> = {};\n parsed.forEach((v, i) => {\n out[String(i)] =\n typeof v === 'object' && v !== null ? JSON.stringify(v) : String(v);\n });\n return out;\n }\n\n const flat = transformObjectToFlat(parsed as Record<string, unknown>);\n const out: Record<string, string> = {};\n for (const [k, v] of Object.entries(flat)) {\n if (v === null || v === undefined) {\n out[k] = '';\n } else if (typeof v === 'object') {\n out[k] = JSON.stringify(v);\n } else {\n out[k] = String(v);\n }\n }\n return out;\n}\n\n/** Where to write normalized JSON for one translation object key (avoids `.json.json`). */\nexport function translationJsonOutputPath(\n outputDir: string,\n objectKey: string,\n): string {\n if (objectKey.endsWith('.json')) {\n return path.resolve(outputDir, objectKey);\n }\n return path.resolve(outputDir, `${objectKey}.json`);\n}\n","import convert from 'xml-js';\nimport set from 'lodash.set';\nimport merge from 'lodash.merge';\n\nexport const transformObjectToNested = (data: Record<string, unknown>): Record<string, unknown> => {\n const result: Record<string, unknown> = {};\n\n Object.entries(data).forEach(([key, value]) => {\n const tempObject = {};\n set(tempObject, key, value);\n merge(result, tempObject);\n });\n\n return result;\n};\n\nexport const transformObjectToFlat = (data: Record<string, any>): Record<string, any> => { // eslint-disable-line @typescript-eslint/no-explicit-any\n const result: Record<string, unknown> = {};\n\n const flatten = (obj: Record<string, any>, path: string[] = []) => { // eslint-disable-line @typescript-eslint/no-explicit-any\n Object.entries(obj).forEach(([key, value]) => {\n if (value !== null && typeof value === 'object' && !Array.isArray(value)) {\n flatten(value, path.concat(key));\n } else {\n result[path.concat(key).join('.')] = value;\n }\n });\n };\n\n flatten(data);\n\n return result;\n}\n\nexport const convertXMLToJS = (xml: string): Record<string, string> => {\n const converted = convert.xml2js(xml, {\n ignoreComment: true,\n ignoreDeclaration: true,\n ignoreInstruction: true,\n compact: true,\n }) as {\n resources: {\n string: {\n _attributes: { name: string },\n _text: 'User does not exist'\n }[];\n }\n };\n\n let mapped = {};\n const strings = converted.resources.string;\n const items = Array.isArray(strings) ? strings : [strings];\n items.forEach((item) => {\n if (!item?._attributes?.name) return;\n mapped = {\n ...mapped,\n [item._attributes.name]: item._text,\n };\n });\n\n return mapped;\n};\n\nexport const parseIOSStrings = (strings: string): Record<string, string> => {\n const parsedObj: Record<string, string> = {};\n strings\n .split('\\n')\n .filter((line) => line.startsWith('\"') && line.endsWith(';'))\n .map((line) => line.trim().slice(0, -1))\n .forEach((line) => {\n const eqIdx = line.indexOf(' = ');\n if (eqIdx === -1) return;\n const key = line.slice(1, eqIdx - 1);\n const value = line.slice(eqIdx + 3 + 1, -1);\n if (!key) return;\n parsedObj[key] = value;\n });\n\n return parsedObj;\n};"],"mappings":"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","names":["path","out"]}
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  config
4
- } from "./chunk-OCAIIQZW.js";
4
+ } from "./chunk-Y6HC4NYU.js";
5
5
 
6
6
  // src/client/config.ts
7
7
  import dotenv from "dotenv";
@@ -14,4 +14,4 @@ var config2 = {
14
14
  export {
15
15
  config2 as config
16
16
  };
17
- //# sourceMappingURL=chunk-SDEERVPV.js.map
17
+ //# sourceMappingURL=chunk-MOGVAQ2N.js.map
@@ -1,471 +1,54 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  config
4
- } from "./chunk-OCAIIQZW.js";
4
+ } from "./chunk-Y6HC4NYU.js";
5
5
  import {
6
6
  ContentStore,
7
7
  buildCmsObjectKey,
8
8
  buildTranslationObjectKey,
9
9
  contentTypeForTranslationKey
10
- } from "./chunk-VBJ6LVMY.js";
10
+ } from "./chunk-LZHYKLAU.js";
11
11
  import {
12
12
  allProjects,
13
13
  defaultLocales
14
- } from "./chunk-RZLDRXNQ.js";
15
-
16
- // src/server/adapters/azure/types.ts
17
- var AZURE_DEVOPS_PROJECT_KEYS = [
18
- "web-site",
19
- "web-app",
20
- "web-invites"
21
- ];
22
-
23
- // src/server/adapters/azure/client.ts
24
- var AzureDevOpsClient = class {
25
- constructor(config3) {
26
- this.config = config3;
27
- const org = config3.organization.replace(/^\/+|\/+$/g, "");
28
- const project = encodeURIComponent(config3.project);
29
- this.baseUrl = `https://dev.azure.com/${org}/${project}`;
30
- }
31
- baseUrl;
32
- /**
33
- * Call any Azure DevOps REST endpoint under the configured organization and project.
34
- */
35
- async request(options) {
36
- const {
37
- method = "GET",
38
- path,
39
- query = {},
40
- body,
41
- apiVersion = this.config.apiVersion,
42
- headers: extraHeaders = {}
43
- } = options;
44
- if (!this.config.pat) {
45
- throw new Error(
46
- "Azure DevOps is not configured (set AZURE_DEVOPS_ACCESS_TOKEN)"
47
- );
48
- }
49
- const url = this.buildUrl(path, { ...query, "api-version": apiVersion });
50
- const headers = {
51
- Authorization: this.basicAuthHeader(),
52
- Accept: "application/json",
53
- ...extraHeaders
54
- };
55
- const init = { method, headers };
56
- if (body !== void 0) {
57
- headers["Content-Type"] = "application/json";
58
- init.body = JSON.stringify(body);
59
- }
60
- const response = await fetch(url, init);
61
- const text = await response.text();
62
- let parsed;
63
- if (text) {
64
- try {
65
- parsed = JSON.parse(text);
66
- } catch {
67
- parsed = text;
68
- }
69
- }
70
- if (!response.ok) {
71
- const errBody = parsed;
72
- const detail = errBody?.message ?? (typeof parsed === "string" ? parsed : JSON.stringify(parsed));
73
- throw new Error(
74
- `Azure DevOps ${method} ${path} failed (${response.status}): ${detail}`
75
- );
76
- }
77
- return parsed;
78
- }
79
- buildUrl(path, query) {
80
- const base = /^https?:\/\//i.test(path) ? path : `${this.baseUrl}${path}`;
81
- const url = new URL(base);
82
- for (const [key, value] of Object.entries(query)) {
83
- if (value !== void 0) {
84
- url.searchParams.set(key, String(value));
85
- }
86
- }
87
- return url.toString();
88
- }
89
- basicAuthHeader() {
90
- const encoded = Buffer.from(`:${this.config.pat}`).toString("base64");
91
- return `Basic ${encoded}`;
92
- }
93
- };
94
- function createAzureDevOpsClient(config3) {
95
- return new AzureDevOpsClient(config3);
96
- }
97
-
98
- // src/server/adapters/azure/pipelines.ts
99
- function isAzureDevOpsProjectKey(value) {
100
- return AZURE_DEVOPS_PROJECT_KEYS.includes(value);
101
- }
102
- function resolveProjectConfig(azureConfig, project) {
103
- const projectConfig = azureConfig.projects[project];
104
- if (!projectConfig) {
105
- throw new Error(`Unknown Azure DevOps project "${project}"`);
106
- }
107
- return projectConfig;
108
- }
109
- async function triggerPipelineBuild(azureConfig, project, variables = {}) {
110
- const { pipeline } = resolveProjectConfig(azureConfig, project);
111
- if (!pipeline?.id) {
112
- throw new Error(
113
- `No pipeline id configured for project "${project}" (instance ENVIRONMENT=${azureConfig.instanceEnvironment}, Azure=${azureConfig.pipelineEnvironment})`
114
- );
115
- }
116
- const client = createAzureDevOpsClient({
117
- organization: azureConfig.organization,
118
- project,
119
- pat: azureConfig.pat,
120
- apiVersion: azureConfig.apiVersion
121
- });
122
- const run = await client.request({
123
- method: "POST",
124
- path: `/_apis/pipelines/${pipeline.id}/runs`,
125
- body: {
126
- resources: {
127
- repositories: {
128
- self: {
129
- refName: pipeline.refName
130
- }
131
- }
132
- },
133
- variables
134
- }
135
- });
136
- return {
137
- project,
138
- instanceEnvironment: azureConfig.instanceEnvironment,
139
- pipelineEnvironment: azureConfig.pipelineEnvironment,
140
- pipelineId: pipeline.id,
141
- refName: pipeline.refName,
142
- run
143
- };
144
- }
145
- function formatPipelineRunSummary(result) {
146
- const {
147
- run,
148
- project,
149
- instanceEnvironment,
150
- pipelineEnvironment,
151
- pipelineId,
152
- refName
153
- } = result;
154
- const webUrl = run._links?.web?.href ?? run.url;
155
- const envLabel = instanceEnvironment === pipelineEnvironment ? `\`${pipelineEnvironment}\`` : `\`${instanceEnvironment}\` \u2192 Azure \`${pipelineEnvironment}\``;
156
- const lines = [
157
- `Environment ${envLabel} \u2014 project \`${project}\`, pipeline id \`${pipelineId}\`, ref \`${refName}\``,
158
- `Run #${run.id}${run.state ? ` \u2014 state: \`${run.state}\`` : ""}`
159
- ];
160
- if (webUrl) {
161
- lines.push(`<${webUrl}|Open run in Azure DevOps>`);
162
- }
163
- return lines.join("\n");
164
- }
165
-
166
- // src/server/adapters/azure/environment.ts
167
- var PRODUCTION_ALIASES = /* @__PURE__ */ new Set(["production", "live", "prod"]);
168
- var STAGING_ALIASES = /* @__PURE__ */ new Set(["staging", "beta", "development", "dev", "local"]);
169
- function normalizeToAzureEnvironment(instanceEnvironment) {
170
- const key = instanceEnvironment.trim().toLowerCase();
171
- if (!key) {
172
- console.warn(
173
- "[azure] Empty ENVIRONMENT; defaulting Azure pipeline environment to staging"
174
- );
175
- return "staging";
176
- }
177
- if (PRODUCTION_ALIASES.has(key)) {
178
- return "production";
179
- }
180
- if (STAGING_ALIASES.has(key)) {
181
- return "staging";
182
- }
183
- console.warn(
184
- `[azure] Unrecognized ENVIRONMENT="${instanceEnvironment}"; defaulting Azure pipeline environment to staging`
185
- );
186
- return "staging";
187
- }
188
- function pipelineRefForAzure(pipelineEnvironment) {
189
- return `refs/heads/${pipelineEnvironment}`;
190
- }
191
-
192
- // src/shared/content-refresh.ts
193
- import { timingSafeEqual } from "crypto";
194
- async function postContentRefresh(target, url, basicAuth, request) {
195
- try {
196
- const response = await fetch(url, {
197
- method: "POST",
198
- headers: {
199
- Authorization: `Basic ${basicAuth}`,
200
- "Content-Type": "application/json",
201
- Accept: "application/json"
202
- },
203
- body: JSON.stringify(request)
204
- });
205
- const text = await response.text();
206
- let body;
207
- if (text) {
208
- try {
209
- body = JSON.parse(text);
210
- } catch {
211
- body = text;
212
- }
213
- }
214
- return {
215
- target,
216
- ok: response.ok,
217
- status: response.status,
218
- body,
219
- error: response.ok ? void 0 : typeof body === "object" && body !== null && "error" in body ? String(body.error) : `HTTP ${response.status}`
220
- };
221
- } catch (err) {
222
- return {
223
- target,
224
- ok: false,
225
- status: 0,
226
- error: err instanceof Error ? err.message : String(err)
227
- };
228
- }
229
- }
230
-
231
- // src/server/content-refresh-notify.ts
232
- function isContentRefreshEnabledForInstance(instanceEnvironment) {
233
- return normalizeToAzureEnvironment(instanceEnvironment) === "staging";
234
- }
235
- async function notifyContentRefreshTargets(notifyConfig, request, projects) {
236
- if (!notifyConfig.enabled || !notifyConfig.basicAuth) {
237
- return [];
238
- }
239
- const keys = projects ?? AZURE_DEVOPS_PROJECT_KEYS.filter(
240
- (p) => notifyConfig.targets[p]?.url
241
- );
242
- const results = [];
243
- for (const project of keys) {
244
- const url = notifyConfig.targets[project]?.url;
245
- if (!url) {
246
- continue;
247
- }
248
- const result = await postContentRefresh(
249
- project,
250
- url,
251
- notifyConfig.basicAuth,
252
- request
253
- );
254
- results.push(result);
255
- if (result.ok) {
256
- console.log(
257
- `[content-refresh] Notified ${project} (${result.status})`
258
- );
259
- } else {
260
- console.error(
261
- `[content-refresh] Failed to notify ${project}: ${result.error ?? result.status}`
262
- );
263
- }
264
- }
265
- return results;
266
- }
14
+ } from "./chunk-SF7FCBR2.js";
267
15
 
268
16
  // src/server/config.ts
269
17
  import dotenv from "dotenv";
270
-
271
- // src/server/restrictedCron.ts
272
- var MAX_SEARCH_MINUTES = 366 * 24 * 60;
273
- function tokenize(expr) {
274
- return expr.trim().split(/\s+/).map((s) => s.trim()).filter(Boolean);
275
- }
276
- function validateScheduleCronExpression(expr) {
277
- const parts = tokenize(expr);
278
- if (parts.length !== 2) {
279
- throw new Error(
280
- `Schedule cron must be exactly two fields (minute hour), whitespace-separated; got ${parts.length} field(s): "${expr}"`
281
- );
282
- }
283
- const minuteSpec = parts[0];
284
- const hourSpec = parts[1];
285
- parseField(minuteSpec, 0, 59, "minute");
286
- parseField(hourSpec, 0, 23, "hour");
287
- return `${minuteSpec} ${hourSpec}`;
288
- }
289
- function parseField(spec, lo, hi, fieldName) {
290
- const subs = spec.split(",").map((s) => s.trim()).filter(Boolean);
291
- if (subs.length === 0) {
292
- throw new Error(`Empty ${fieldName} field in cron`);
293
- }
294
- const preds = subs.map((sub) => parseSubfield(sub, lo, hi, fieldName));
295
- return (n) => preds.some((p) => p(n));
296
- }
297
- function parseSubfield(sub, lo, hi, fieldName) {
298
- if (sub === "*") {
299
- return () => true;
300
- }
301
- if (sub.startsWith("*/")) {
302
- const step = parseInt(sub.slice(2), 10);
303
- if (!Number.isFinite(step) || step < 1) {
304
- throw new Error(`Invalid step in ${fieldName} field: "${sub}"`);
305
- }
306
- return (n) => n >= lo && n <= hi && n % step === 0;
307
- }
308
- if (sub.includes("-")) {
309
- const [a, b] = sub.split("-").map((x) => parseInt(x.trim(), 10));
310
- if (!Number.isFinite(a) || !Number.isFinite(b)) {
311
- throw new Error(`Invalid range in ${fieldName} field: "${sub}"`);
312
- }
313
- if (a < lo || b > hi || a > b) {
314
- throw new Error(`Range out of bounds in ${fieldName} field: "${sub}"`);
315
- }
316
- return (n) => n >= a && n <= b;
317
- }
318
- const v = parseInt(sub, 10);
319
- if (!Number.isFinite(v) || v < lo || v > hi) {
320
- throw new Error(`Invalid value in ${fieldName} field: "${sub}"`);
321
- }
322
- return (n) => n === v;
323
- }
324
- function floorToMinuteStart(d) {
325
- const t = new Date(d.getTime());
326
- t.setSeconds(0, 0);
327
- return t;
328
- }
329
- function addOneMinute(d) {
330
- const t = new Date(d.getTime());
331
- t.setMinutes(t.getMinutes() + 1, 0, 0);
332
- return t;
333
- }
334
- function matchesAtMinuteStart(minutePred, hourPred, d) {
335
- return minutePred(d.getMinutes()) && hourPred(d.getHours());
336
- }
337
- function nextCronFireAfter(expr, after) {
338
- const parts = tokenize(expr);
339
- if (parts.length !== 2) {
340
- throw new Error(
341
- `nextCronFireAfter expects exactly two fields (minute hour); got ${parts.length}: "${expr}"`
342
- );
343
- }
344
- const minutePred = parseField(parts[0], 0, 59, "minute");
345
- const hourPred = parseField(parts[1], 0, 23, "hour");
346
- let d = floorToMinuteStart(after);
347
- if (d.getTime() <= after.getTime()) {
348
- d = addOneMinute(d);
349
- }
350
- for (let i = 0; i < MAX_SEARCH_MINUTES; i++) {
351
- if (matchesAtMinuteStart(minutePred, hourPred, d)) {
352
- return d;
353
- }
354
- d = addOneMinute(d);
355
- }
356
- throw new Error(`No cron match within ${MAX_SEARCH_MINUTES} minutes for "${expr}"`);
357
- }
358
-
359
- // src/server/config.ts
360
18
  dotenv.config({ path: ".env.local" });
361
19
  dotenv.config();
362
- function readScheduleCronEnv() {
363
- return {
364
- scheduleCron: (process.env.SCHEDULE_CRON ?? "").trim(),
365
- cmsCronRaw: (process.env.SCHEDULE_CMS_CRON ?? "").trim(),
366
- translationCronRaw: (process.env.SCHEDULE_TRANSLATION_CRON ?? "").trim()
367
- };
368
- }
369
- function resolveScheduleCron(jobSpecific, globalCron, jobLabel) {
370
- const raw = jobSpecific || globalCron;
371
- if (!raw) return null;
372
- try {
373
- return validateScheduleCronExpression(raw);
374
- } catch (e) {
375
- const msg = e instanceof Error ? e.message : String(e);
376
- console.error(`[config] Invalid ${jobLabel} schedule cron: ${msg}`);
377
- return null;
378
- }
379
- }
380
20
  function parseScheduledCmsJobConfig() {
381
- const { scheduleCron, cmsCronRaw } = readScheduleCronEnv();
21
+ const intervalMinutes = parseInt(process.env.SCHEDULE_INTERVAL_MINUTES ?? "0", 10);
22
+ const enabled = Number.isFinite(intervalMinutes) && intervalMinutes > 0;
382
23
  const runOnStart = (process.env.SCHEDULE_RUN_ON_START ?? "true").toLowerCase() !== "false";
24
+ const task = (process.env.SCHEDULE_TASK ?? "sync").trim();
383
25
  const syncCms = (process.env.SCHEDULE_SYNC_CMS ?? "").trim();
384
- const rawTypes = process.env.SCHEDULE_SYNC_CONTENT_TYPES?.trim();
26
+ const rawTypes = process.env.SCHEDULE_SYNC_TYPES?.trim();
385
27
  const syncTypes = rawTypes ? rawTypes.split(",").map((s) => s.trim()).filter(Boolean) : void 0;
386
- const resolved = resolveScheduleCron(cmsCronRaw, scheduleCron, "CMS");
387
- const cmsOk = !!syncCms && (syncCms === "contentful" || syncCms === "sanity");
388
- if (resolved !== null && !cmsOk) {
389
- console.warn(
390
- '[config] CMS schedule cron is set but SCHEDULE_SYNC_CMS is missing or not "contentful"|"sanity"; scheduled CMS sync is disabled.'
391
- );
392
- }
393
- const enabled = resolved !== null && cmsOk;
394
28
  return {
395
29
  enabled,
396
- cronExpression: resolved ?? "",
30
+ intervalMinutes,
397
31
  runOnStart,
32
+ task,
398
33
  syncCms: syncCms || void 0,
399
34
  syncTypes
400
35
  };
401
36
  }
402
37
  function parseScheduledTranslationJobConfig() {
403
- const { scheduleCron, translationCronRaw } = readScheduleCronEnv();
38
+ const intervalMinutes = parseInt(process.env.SCHEDULE_INTERVAL_MINUTES ?? "0", 10);
39
+ const enabled = Number.isFinite(intervalMinutes) && intervalMinutes > 0;
404
40
  const runOnStart = (process.env.SCHEDULE_RUN_ON_START ?? "true").toLowerCase() !== "false";
41
+ const task = (process.env.SCHEDULE_TASK ?? "sync").trim();
405
42
  const rawProjects = process.env.SCHEDULE_SYNC_TRANSLATION_PROJECTS?.trim();
406
43
  const syncProjects = rawProjects ? rawProjects.split(",").map((s) => s.trim()).filter(Boolean) : void 0;
407
- const resolved = resolveScheduleCron(
408
- translationCronRaw,
409
- scheduleCron,
410
- "translation"
411
- );
412
- const enabled = resolved !== null;
413
44
  return {
414
45
  enabled,
415
- cronExpression: resolved ?? "",
46
+ intervalMinutes,
416
47
  runOnStart,
48
+ task,
417
49
  syncProjects
418
50
  };
419
51
  }
420
- function projectEnvSlug(project) {
421
- return project.toUpperCase().replace(/-/g, "_");
422
- }
423
- function readProjectPipeline(project, pipelineEnvironment) {
424
- const slug = projectEnvSlug(project);
425
- const id = parseInt(process.env[`AZURE_${slug}_PIPELINE_ID`]?.trim() ?? "0", 10);
426
- return { id, refName: pipelineRefForAzure(pipelineEnvironment) };
427
- }
428
- function parseAzureDevOpsConfig(instanceEnvironment) {
429
- const pipelineEnvironment = normalizeToAzureEnvironment(instanceEnvironment);
430
- const pat = process.env.AZURE_DEVOPS_ACCESS_TOKEN?.trim() ?? "";
431
- const defaultProjectRaw = (process.env.AZURE_DEVOPS_DEFAULT_PROJECT ?? "web-site").trim();
432
- const defaultProject = AZURE_DEVOPS_PROJECT_KEYS.includes(
433
- defaultProjectRaw
434
- ) ? defaultProjectRaw : "web-site";
435
- const projects = Object.fromEntries(
436
- AZURE_DEVOPS_PROJECT_KEYS.map((project) => [
437
- project,
438
- { pipeline: readProjectPipeline(project, pipelineEnvironment) }
439
- ])
440
- );
441
- return {
442
- enabled: pat.length > 0,
443
- organization: process.env.AZURE_DEVOPS_ORGANIZATION?.trim() || "tripod-technology",
444
- pat,
445
- apiVersion: process.env.AZURE_DEVOPS_API_VERSION?.trim() || "7.1",
446
- defaultProject,
447
- instanceEnvironment,
448
- pipelineEnvironment,
449
- projects
450
- };
451
- }
452
- function readContentRefreshUrl(project) {
453
- const slug = projectEnvSlug(project);
454
- return process.env[`CONTENT_REFRESH_${slug}_URL`]?.trim() || void 0;
455
- }
456
- function parseContentRefreshConfig(instanceEnvironment) {
457
- const targets = Object.fromEntries(
458
- AZURE_DEVOPS_PROJECT_KEYS.map((project) => [
459
- project,
460
- { url: readContentRefreshUrl(project) }
461
- ])
462
- );
463
- return {
464
- enabled: isContentRefreshEnabledForInstance(instanceEnvironment),
465
- basicAuth: process.env.CONTENT_REFRESH_BASIC_AUTH?.trim() ?? "",
466
- targets
467
- };
468
- }
469
52
  var config2 = {
470
53
  ...config,
471
54
  contentful: {
@@ -506,8 +89,6 @@ var config2 = {
506
89
  },
507
90
  scheduledCmsJob: parseScheduledCmsJobConfig(),
508
91
  scheduledTranslationJob: parseScheduledTranslationJobConfig(),
509
- azure: parseAzureDevOpsConfig(config.environment),
510
- contentRefresh: parseContentRefreshConfig(config.environment),
511
92
  slack: {
512
93
  enabled: (process.env.SLACK_BOT_TOKEN ?? "").length > 0,
513
94
  botToken: process.env.SLACK_BOT_TOKEN ?? "",
@@ -515,8 +96,7 @@ var config2 = {
515
96
  appToken: process.env.SLACK_APP_TOKEN ?? "",
516
97
  notifyChannel: process.env.SLACK_NOTIFY_CHANNEL ?? "",
517
98
  cmdSyncContent: process.env.SLACK_CMD_SYNC_CONTENT ?? "/sync-content",
518
- cmdSyncTranslations: process.env.SLACK_CMD_SYNC_TRANSLATIONS ?? "/sync-translations",
519
- cmdTriggerBuild: process.env.SLACK_CMD_TRIGGER_BUILD ?? "/trigger-build"
99
+ cmdSyncTranslations: process.env.SLACK_CMD_SYNC_TRANSLATIONS ?? "/sync-translations"
520
100
  }
521
101
  };
522
102
 
@@ -872,8 +452,8 @@ async function syncTranslations(projects, locales) {
872
452
  );
873
453
  } catch (err) {
874
454
  const message = err instanceof Error ? err.message : String(err);
875
- errors.push({ project, locale, error: message });
876
- console.error(` x ${project} - ${locale}: ${message}`);
455
+ errors.push({ project, locale, resource: resource.resource, error: message });
456
+ console.error(` x ${project} / ${resource.resource} - ${locale}: ${message}`);
877
457
  }
878
458
  }
879
459
  }
@@ -882,12 +462,6 @@ async function syncTranslations(projects, locales) {
882
462
  }
883
463
 
884
464
  export {
885
- AZURE_DEVOPS_PROJECT_KEYS,
886
- isAzureDevOpsProjectKey,
887
- triggerPipelineBuild,
888
- formatPipelineRunSummary,
889
- notifyContentRefreshTargets,
890
- nextCronFireAfter,
891
465
  config2 as config,
892
466
  summariseCmsEntries,
893
467
  summariseCmsErrors,
@@ -895,4 +469,4 @@ export {
895
469
  syncCmsContent,
896
470
  syncTranslations
897
471
  };
898
- //# sourceMappingURL=chunk-U73PO7OV.js.map
472
+ //# sourceMappingURL=chunk-NQHWG4XM.js.map