@tandem-language-exchange/content-store 1.3.4 → 1.3.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-LXA6HPF5.js → chunk-FMP35RL2.js} +7 -6
- package/dist/chunk-FMP35RL2.js.map +1 -0
- package/dist/{chunk-VBJ6LVMY.js → chunk-K4RASQFK.js} +1 -2
- package/dist/chunk-K4RASQFK.js.map +1 -0
- package/dist/{chunk-Q3TFZNBV.js → chunk-WJDMWZOX.js} +3 -3
- package/dist/{chunk-Q3TFZNBV.js.map → chunk-WJDMWZOX.js.map} +1 -1
- package/dist/{chunk-OWGYS6EG.js → chunk-ZXSGKKXJ.js} +10 -6
- package/dist/chunk-ZXSGKKXJ.js.map +1 -0
- package/dist/client/fetch-content-bundles.js +2 -2
- package/dist/client/fetch-merged-translation-bundles.js +2 -2
- package/dist/client/fetch-translation-bundles.js +2 -2
- package/dist/client/query-cms.js +2 -2
- package/dist/node.js +8 -5
- package/dist/node.js.map +1 -1
- package/package.json +1 -4
- package/dist/chunk-LXA6HPF5.js.map +0 -1
- package/dist/chunk-OWGYS6EG.js.map +0 -1
- package/dist/chunk-VBJ6LVMY.js.map +0 -1
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
syncCmsContent,
|
|
13
13
|
syncTranslations,
|
|
14
14
|
triggerPipelineBuild
|
|
15
|
-
} from "./chunk-
|
|
15
|
+
} from "./chunk-WJDMWZOX.js";
|
|
16
16
|
import {
|
|
17
17
|
allProjects
|
|
18
18
|
} from "./chunk-TMR6VJBQ.js";
|
|
@@ -106,18 +106,18 @@ async function startSlackBot() {
|
|
|
106
106
|
if (!raw) {
|
|
107
107
|
await respond({
|
|
108
108
|
response_type: "ephemeral",
|
|
109
|
-
text: "Usage: `/sync-translations <
|
|
109
|
+
text: "Usage: `/sync-translations <projects|all> [<comma-separated locales>]`\nExamples:\n\u2022 `/sync-translations all` \u2014 all projects, default locales\n\u2022 `/sync-translations all en,de` \u2014 all projects, English and German\n\u2022 `/sync-translations tandem,tandem-(website) en,de,fr` \u2014 specific projects\nOmit locales to use the default locale list."
|
|
110
110
|
});
|
|
111
111
|
return;
|
|
112
112
|
}
|
|
113
113
|
const spaceIdx = raw.indexOf(" ");
|
|
114
114
|
const projectsPart = spaceIdx === -1 ? raw : raw.slice(0, spaceIdx).trim();
|
|
115
115
|
const localesPart = spaceIdx === -1 ? "" : raw.slice(spaceIdx + 1).trim();
|
|
116
|
-
const projects = projectsPart.split(",").map((s) => s.trim()).filter(Boolean);
|
|
116
|
+
const projects = projectsPart.toLowerCase() === "all" ? Object.keys(allProjects) : projectsPart.split(",").map((s) => s.trim()).filter(Boolean);
|
|
117
117
|
if (projects.length === 0) {
|
|
118
118
|
await respond({
|
|
119
119
|
response_type: "ephemeral",
|
|
120
|
-
text: "No valid projects. Pass a comma-separated list as the first argument, e.g. `tandem,tandem-(website)`."
|
|
120
|
+
text: "No valid projects. Pass `all` or a comma-separated list as the first argument, e.g. `tandem,tandem-(website)`."
|
|
121
121
|
});
|
|
122
122
|
return;
|
|
123
123
|
}
|
|
@@ -125,10 +125,11 @@ async function startSlackBot() {
|
|
|
125
125
|
if (locales && locales.length === 0) {
|
|
126
126
|
locales = void 0;
|
|
127
127
|
}
|
|
128
|
+
const projectsHint = projects.length === Object.keys(allProjects).length ? "all projects" : `projects: *${projects.join(", ")}*`;
|
|
128
129
|
const localesHint = locales?.length ? ` \u2014 locales: ${locales?.join(", ")}` : " \u2014 default locales";
|
|
129
130
|
await respond({
|
|
130
131
|
response_type: "in_channel",
|
|
131
|
-
text: `:hourglass_flowing_sand: Translation sync started for
|
|
132
|
+
text: `:hourglass_flowing_sand: Translation sync started for ${projectsHint}${localesHint}\u2026`
|
|
132
133
|
});
|
|
133
134
|
try {
|
|
134
135
|
const result = await syncTranslations(projects, locales);
|
|
@@ -250,4 +251,4 @@ export {
|
|
|
250
251
|
notifySlack,
|
|
251
252
|
startSlackBot
|
|
252
253
|
};
|
|
253
|
-
//# sourceMappingURL=chunk-
|
|
254
|
+
//# sourceMappingURL=chunk-FMP35RL2.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/server/slack.ts"],"sourcesContent":["import { App } from '@slack/bolt';\nimport {\n AZURE_DEVOPS_PROJECT_KEYS,\n formatPipelineRunSummary,\n isAzureDevOpsProjectKey,\n triggerPipelineBuild,\n} from './adapters/azure';\nimport { config, type CMSProvider } from './config';\nimport { notifyClientsAfterCmsSync } from './content-refresh-notify';\nimport {\n syncCmsContent,\n syncTranslations,\n summariseTranslationEntries,\n summariseCmsEntries,\n summariseCmsErrors,\n summariseCmsContentfulApiUsage,\n} from './sync/engine';\nimport { allProjects } from '../shared/lingohub';\n\nconst VALID_CMS: CMSProvider[] = ['contentful', 'sanity'];\n\nlet app: App | null = null;\n\n/**\n * Post a message to the configured Slack notify channel.\n * Silently logs and returns if Slack is not configured or no channel is set.\n */\nexport async function notifySlack(text: string): Promise<void> {\n const { notifyChannel } = config.slack;\n if (!app || !notifyChannel) {\n console.log('[slack] Notification skipped (no app or channel configured)');\n return;\n }\n try {\n await app.client.chat.postMessage({ channel: notifyChannel, text: `[${config.environment}] ${text}` });\n } catch (err) {\n console.error('[slack] Failed to send notification:', err);\n }\n}\n\nexport async function startSlackBot(): Promise<void> {\n const { slack } = config;\n if (!slack.enabled) {\n console.log('[slack] Disabled (SLACK_BOT_TOKEN not set)');\n return;\n }\n\n app = new App({\n token: slack.botToken,\n signingSecret: slack.signingSecret,\n appToken: slack.appToken,\n socketMode: true,\n });\n\n app.command(slack.cmdSyncContent, async ({ command, ack, respond }) => {\n await ack();\n\n const args = command.text.trim().split(/\\s+/).filter(Boolean);\n const cms = (args[0] ?? 'contentful') as CMSProvider;\n const contentTypes = args.length > 1 ? args.slice(1) : undefined;\n\n if (!VALID_CMS.includes(cms)) {\n await respond({\n response_type: 'ephemeral',\n text: `Invalid CMS provider \\`${cms}\\`. Use \\`contentful\\` or \\`sanity\\`.`,\n });\n return;\n }\n\n const typesHint = contentTypes?.length ? ` (types: ${contentTypes.join(', ')})` : ' (all types)';\n await respond({\n response_type: 'in_channel',\n text: `:hourglass_flowing_sand: Sync started for *${cms}*${typesHint}…`,\n });\n\n try {\n const result = await syncCmsContent(cms, contentTypes);\n\n const blocks: string[] = [\n result.errors.length === 0\n ? `:white_check_mark: *Sync complete* — ${result.entries.length} content types synced`\n : `:warning: *Sync complete with errors* — ${result.entries.length} succeeded, ${result.errors.length} failed`,\n '',\n ...summariseCmsEntries(result),\n ];\n\n if (result.errors.length > 0) {\n blocks.push('', '*Errors:*', ...summariseCmsErrors(result));\n }\n\n const apiUsageLines = summariseCmsContentfulApiUsage(result);\n if (apiUsageLines.length > 0) {\n blocks.push('', ...apiUsageLines);\n }\n\n const refreshResults = await notifyClientsAfterCmsSync(\n result,\n contentTypes,\n );\n if (refreshResults.length > 0) {\n const ok = refreshResults.filter((r) => r.ok).map((r) => r.target);\n const failed = refreshResults.filter((r) => !r.ok).map((r) => r.target);\n if (ok.length > 0) {\n blocks.push('', `*Content refresh:* notified \\`${ok.join('`, `')}\\``);\n }\n if (failed.length > 0) {\n blocks.push(`*Content refresh failed:* \\`${failed.join('`, `')}\\``);\n }\n }\n\n await respond({ response_type: 'in_channel', text: blocks.join('\\n') });\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n console.error('[slack] Sync failed:', message);\n await respond({\n response_type: 'ephemeral',\n text: `:x: Sync failed: ${message}`,\n });\n }\n });\n\n app.command(slack.cmdSyncTranslations, async ({ command, ack, respond }) => {\n await ack();\n\n const raw = command.text.trim();\n if (!raw) {\n await respond({\n response_type: 'ephemeral',\n text:\n 'Usage: `/sync-translations <projects|all> [<comma-separated locales>]`\\n' +\n 'Examples:\\n' +\n '• `/sync-translations all` — all projects, default locales\\n' +\n '• `/sync-translations all en,de` — all projects, English and German\\n' +\n '• `/sync-translations tandem,tandem-(website) en,de,fr` — specific projects\\n' +\n 'Omit locales to use the default locale list.',\n });\n return;\n }\n\n const spaceIdx = raw.indexOf(' ');\n const projectsPart = spaceIdx === -1 ? raw : raw.slice(0, spaceIdx).trim();\n const localesPart = spaceIdx === -1 ? '' : raw.slice(spaceIdx + 1).trim();\n\n const projects =\n projectsPart.toLowerCase() === 'all'\n ? Object.keys(allProjects)\n : projectsPart\n .split(',')\n .map((s) => s.trim())\n .filter(Boolean);\n if (projects.length === 0) {\n await respond({\n response_type: 'ephemeral',\n text:\n 'No valid projects. Pass `all` or a comma-separated list as the first argument, e.g. `tandem,tandem-(website)`.',\n });\n return;\n }\n\n let locales: string[] | undefined = localesPart.length\n ? localesPart\n .split(',')\n .map((s) => s.trim())\n .filter(Boolean)\n : undefined;\n if (locales && locales.length === 0) {\n locales = undefined;\n }\n\n const projectsHint =\n projects.length === Object.keys(allProjects).length ? 'all projects' : `projects: *${projects.join(', ')}*`;\n const localesHint =\n locales?.length ? ` — locales: ${locales?.join(', ')}` : ' — default locales';\n await respond({\n response_type: 'in_channel',\n text: `:hourglass_flowing_sand: Translation sync started for ${projectsHint}${localesHint}…`,\n });\n\n try {\n const result = await syncTranslations(projects, locales);\n\n const lines = summariseTranslationEntries(result.entries);\n const errorLines = result.errors.map(\n (e) =>\n `• \\`${e.project}\\` / \\`${e.locale}\\` — ${e.error}`,\n );\n\n const blocks: string[] = [\n result.errors.length === 0\n ? `:white_check_mark: *Translation sync complete* — ${result.entries.length} resource × locale uploads`\n : `:warning: *Translation sync complete with errors* — ${result.entries.length} succeeded, ${result.errors.length} failed`,\n '',\n ...lines,\n ];\n\n if (errorLines.length > 0) {\n blocks.push('', '*Errors:*', ...errorLines);\n }\n\n await respond({ response_type: 'in_channel', text: blocks.join('\\n') });\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n console.error('[slack] Sync failed:', message);\n await respond({\n response_type: 'ephemeral',\n text: `:x: Sync failed: ${message}`,\n });\n }\n });\n\n app.command(slack.cmdTriggerBuild, async ({ command, ack, respond }) => {\n await ack();\n\n const args = command.text.trim().split(/\\s+/).filter(Boolean);\n const projectArg = (args[0] ?? config.azure.defaultProject).toLowerCase();\n\n if (args.length > 1) {\n const projects = AZURE_DEVOPS_PROJECT_KEYS.join('`, `');\n await respond({\n response_type: 'ephemeral',\n text:\n 'Usage: `/trigger-build [project]`\\n' +\n 'Queues the pipeline configured for this instance (`ENVIRONMENT=' +\n config.azure.instanceEnvironment +\n '`, Azure `' +\n config.azure.pipelineEnvironment +\n '`).\\n' +\n 'Default project: `' +\n config.azure.defaultProject +\n '`. Projects: `' +\n projects +\n '`',\n });\n return;\n }\n\n if (!isAzureDevOpsProjectKey(projectArg)) {\n await respond({\n response_type: 'ephemeral',\n text:\n `Unknown project \\`${projectArg}\\`. Use one of: \\`${AZURE_DEVOPS_PROJECT_KEYS.join('`, `')}\\`.`,\n });\n return;\n }\n\n if (!config.azure.enabled) {\n await respond({\n response_type: 'ephemeral',\n text: 'Azure DevOps is not configured on this server (set `AZURE_DEVOPS_ACCESS_TOKEN`).',\n });\n return;\n }\n\n await respond({\n response_type: 'in_channel',\n text:\n `:hourglass_flowing_sand: Queuing pipeline for \\`${projectArg}\\` ` +\n `(\\`${config.azure.instanceEnvironment}\\` / Azure \\`${config.azure.pipelineEnvironment}\\`)…`,\n });\n\n try {\n const result = await triggerPipelineBuild(config.azure, projectArg);\n await respond({\n response_type: 'in_channel',\n text: `:rocket: Pipeline run queued\\n${formatPipelineRunSummary(result)}`,\n });\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n console.error('[slack] Pipeline trigger failed:', message);\n await respond({\n response_type: 'ephemeral',\n text: `:x: Failed to trigger build: ${message}`,\n });\n }\n });\n\n app.command('/list-projects', async ({ ack }) => {\n const projectNames = Object.keys(allProjects);\n const lines = projectNames.map((name, i) => `${i + 1}. ${name}`);\n await ack({\n response_type: 'ephemeral',\n text: `*Available Lingohub projects:*\\n${lines.join('\\n')}`,\n });\n });\n\n app.command('/list-resources', async ({ command, ack }) => {\n const project = command.text.trim();\n if (!project) {\n const projectNames = Object.keys(allProjects);\n const lines = projectNames.map((name, i) => `${i + 1}. ${name}`);\n await ack({\n response_type: 'ephemeral',\n text: `Usage: \\`/list-resources <project-name>\\`\\n\\n*Available projects:*\\n${lines.join('\\n')}`,\n });\n return;\n }\n\n const resources = allProjects[project];\n if (!resources) {\n const available = Object.keys(allProjects).join(', ');\n await ack({\n response_type: 'ephemeral',\n text: `Unknown project \\`${project}\\`.\\nAvailable projects: ${available}`,\n });\n return;\n }\n\n const lines = resources.map(\n (r, i) => `${i + 1}. \\`${r.resource}\\` (${r.fileName})`,\n );\n await ack({\n response_type: 'ephemeral',\n text: `*Resources for \"${project}\":*\\n${lines.join('\\n')}`,\n });\n });\n\n await app.start();\n console.log('[slack] Bot connected via Socket Mode');\n}\n"],"mappings":"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","names":["lines"]}
|
|
@@ -72,7 +72,6 @@ import path from "path";
|
|
|
72
72
|
|
|
73
73
|
// src/shared/utils.ts
|
|
74
74
|
import convert from "xml-js";
|
|
75
|
-
import set from "lodash.set";
|
|
76
75
|
import merge from "lodash.merge";
|
|
77
76
|
var transformObjectToFlat = (data) => {
|
|
78
77
|
const result = {};
|
|
@@ -179,4 +178,4 @@ export {
|
|
|
179
178
|
toFlatStringMap,
|
|
180
179
|
translationJsonOutputPath
|
|
181
180
|
};
|
|
182
|
-
//# sourceMappingURL=chunk-
|
|
181
|
+
//# sourceMappingURL=chunk-K4RASQFK.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 });\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 merge from 'lodash.merge';\n\nconst set = (obj: Record<string, unknown>, path: string, value: unknown): void => {\n const parts = path.split('.');\n let current: Record<string, unknown> = obj;\n for (let i = 0; i < parts.length - 1; i++) {\n const part = parts[i];\n if (part === '__proto__' || part === 'constructor' || part === 'prototype') return;\n if (current[part] === undefined || typeof current[part] !== 'object') {\n current[part] = {};\n }\n current = current[part] as Record<string, unknown>;\n }\n const last = parts[parts.length - 1];\n if (last !== '__proto__' && last !== 'constructor' && last !== 'prototype') {\n current[last] = value;\n }\n};\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: Record<string, unknown> = {};\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"]}
|
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
buildCmsObjectKey,
|
|
8
8
|
buildTranslationObjectKey,
|
|
9
9
|
contentTypeForTranslationKey
|
|
10
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-K4RASQFK.js";
|
|
11
11
|
import {
|
|
12
12
|
allProjects,
|
|
13
13
|
defaultLocales
|
|
@@ -957,7 +957,7 @@ async function syncTranslations(projects, locales) {
|
|
|
957
957
|
continue;
|
|
958
958
|
}
|
|
959
959
|
for (const resource of resources) {
|
|
960
|
-
const resourceLocales = resource.locales
|
|
960
|
+
const resourceLocales = resource.locales ? locales.filter((l) => resource.locales.includes(l)) : locales;
|
|
961
961
|
for (const loc of resourceLocales) {
|
|
962
962
|
const locale = resource.localeMapping && resource.localeMapping[loc] ? resource.localeMapping[loc] : loc;
|
|
963
963
|
try {
|
|
@@ -1013,4 +1013,4 @@ export {
|
|
|
1013
1013
|
syncCmsContent,
|
|
1014
1014
|
syncTranslations
|
|
1015
1015
|
};
|
|
1016
|
-
//# sourceMappingURL=chunk-
|
|
1016
|
+
//# sourceMappingURL=chunk-WJDMWZOX.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/server/adapters/azure/types.ts","../src/server/adapters/azure/client.ts","../src/server/adapters/azure/pipelines.ts","../src/server/adapters/azure/environment.ts","../src/server/config.ts","../src/server/restrictedCron.ts","../src/shared/content-refresh.ts","../src/server/content-refresh-notify.ts","../src/server/adapters/contentful-api-usage.ts","../src/server/adapters/contentful.ts","../src/server/sync/retry.ts","../src/server/adapters/sanity.ts","../src/server/adapters/index.ts","../src/server/adapters/lingohub.ts","../src/server/sync/engine.ts"],"sourcesContent":["import type { AzurePipelineEnvironment } from './environment';\n\nexport type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';\n\n/** Azure DevOps project name (e.g. `web-site`, `web-app`). */\nexport type AzureDevOpsProjectKey = 'web-site' | 'web-app' | 'web-invites';\n\nexport const AZURE_DEVOPS_PROJECT_KEYS: AzureDevOpsProjectKey[] = [\n 'web-site',\n 'web-app',\n 'web-invites',\n];\n\nexport interface AzurePipelineDefinition {\n id: number;\n refName: string;\n}\n\n/** Pipeline for this content-store instance (one id/ref per ADO project). */\nexport interface AzureProjectConfig {\n pipeline: AzurePipelineDefinition;\n}\n\n/** Credentials and per-project pipeline map for this deployment. */\nexport interface AzureDevOpsConfig {\n organization: string;\n /** Personal access token with pipeline run permissions. */\n pat: string;\n apiVersion: string;\n /** Default when Slack command omits the project argument. */\n defaultProject: AzureDevOpsProjectKey;\n /** Raw `ENVIRONMENT` on this instance (e.g. beta, live). */\n instanceEnvironment: string;\n /** Normalized for Azure (staging | production). */\n pipelineEnvironment: AzurePipelineEnvironment;\n projects: Record<AzureDevOpsProjectKey, AzureProjectConfig>;\n}\n\n/** Connection settings for a single ADO project (used by the HTTP client). */\nexport interface AzureDevOpsClientConfig {\n organization: string;\n project: string;\n pat: string;\n apiVersion: string;\n}\n\nexport interface AzureRequestOptions {\n method?: HttpMethod;\n /**\n * Path under the project, e.g. `/_apis/pipelines/281/runs`.\n * If it starts with `http://` or `https://`, it is used as the full URL.\n */\n path: string;\n query?: Record<string, string | number | boolean | undefined>;\n body?: unknown;\n /** Overrides the default api-version query parameter. */\n apiVersion?: string;\n headers?: Record<string, string>;\n}\n\nexport interface AzureApiErrorBody {\n message?: string;\n typeKey?: string;\n}\n\n/** Subset of the Azure Pipelines run object returned by the Runs API. */\nexport interface PipelineRunResponse {\n id: number;\n name?: string;\n state?: string;\n url?: string;\n createdDate?: string;\n _links?: {\n web?: { href?: string };\n pipeline?: { href?: string };\n };\n}\n\nexport interface TriggerPipelineRunResult {\n project: AzureDevOpsProjectKey;\n instanceEnvironment: string;\n pipelineEnvironment: AzurePipelineEnvironment;\n pipelineId: number;\n refName: string;\n run: PipelineRunResponse;\n}\n","import type {\n AzureApiErrorBody,\n AzureDevOpsClientConfig,\n AzureRequestOptions,\n HttpMethod,\n} from './types';\n\nexport class AzureDevOpsClient {\n private readonly baseUrl: string;\n\n constructor(private readonly config: AzureDevOpsClientConfig) {\n const org = config.organization.replace(/^\\/+|\\/+$/g, '');\n const project = encodeURIComponent(config.project);\n this.baseUrl = `https://dev.azure.com/${org}/${project}`;\n }\n\n /**\n * Call any Azure DevOps REST endpoint under the configured organization and project.\n */\n async request<T>(options: AzureRequestOptions): Promise<T> {\n const {\n method = 'GET',\n path,\n query = {},\n body,\n apiVersion = this.config.apiVersion,\n headers: extraHeaders = {},\n } = options;\n\n if (!this.config.pat) {\n throw new Error(\n 'Azure DevOps is not configured (set AZURE_DEVOPS_ACCESS_TOKEN)',\n );\n }\n\n const url = this.buildUrl(path, { ...query, 'api-version': apiVersion });\n const headers: Record<string, string> = {\n Authorization: this.basicAuthHeader(),\n Accept: 'application/json',\n ...extraHeaders,\n };\n\n const init: RequestInit = { method, headers };\n if (body !== undefined) {\n headers['Content-Type'] = 'application/json';\n init.body = JSON.stringify(body);\n }\n\n const response = await fetch(url, init);\n const text = await response.text();\n let parsed: unknown;\n if (text) {\n try {\n parsed = JSON.parse(text) as unknown;\n } catch {\n parsed = text;\n }\n }\n\n if (!response.ok) {\n const errBody = parsed as AzureApiErrorBody | undefined;\n const detail =\n errBody?.message ??\n (typeof parsed === 'string' ? parsed : JSON.stringify(parsed));\n throw new Error(\n `Azure DevOps ${method} ${path} failed (${response.status}): ${detail}`,\n );\n }\n\n return parsed as T;\n }\n\n private buildUrl(\n path: string,\n query: Record<string, string | number | boolean | undefined>,\n ): string {\n const base = /^https?:\\/\\//i.test(path) ? path : `${this.baseUrl}${path}`;\n const url = new URL(base);\n for (const [key, value] of Object.entries(query)) {\n if (value !== undefined) {\n url.searchParams.set(key, String(value));\n }\n }\n return url.toString();\n }\n\n private basicAuthHeader(): string {\n const encoded = Buffer.from(`:${this.config.pat}`).toString('base64');\n return `Basic ${encoded}`;\n }\n}\n\nexport function createAzureDevOpsClient(\n config: AzureDevOpsClientConfig,\n): AzureDevOpsClient {\n return new AzureDevOpsClient(config);\n}\n\nexport type { HttpMethod };\n","import { createAzureDevOpsClient } from './client';\nimport type {\n AzureDevOpsConfig,\n AzureDevOpsProjectKey,\n PipelineRunResponse,\n TriggerPipelineRunResult,\n} from './types';\nimport { AZURE_DEVOPS_PROJECT_KEYS } from './types';\n\nexport function isAzureDevOpsProjectKey(value: string): value is AzureDevOpsProjectKey {\n return (AZURE_DEVOPS_PROJECT_KEYS as string[]).includes(value);\n}\n\nfunction resolveProjectConfig(\n azureConfig: AzureDevOpsConfig,\n project: AzureDevOpsProjectKey,\n) {\n const projectConfig = azureConfig.projects[project];\n if (!projectConfig) {\n throw new Error(`Unknown Azure DevOps project \"${project}\"`);\n }\n return projectConfig;\n}\n\n/**\n * Queue a pipeline run for the given ADO project using this instance's configured pipeline id/ref.\n */\nexport async function triggerPipelineBuild(\n azureConfig: AzureDevOpsConfig,\n project: AzureDevOpsProjectKey,\n variables: Record<string, string> = {},\n): Promise<TriggerPipelineRunResult> {\n const { pipeline } = resolveProjectConfig(azureConfig, project);\n if (!pipeline?.id) {\n throw new Error(\n `No pipeline id configured for project \"${project}\" ` +\n `(instance ENVIRONMENT=${azureConfig.instanceEnvironment}, ` +\n `Azure=${azureConfig.pipelineEnvironment})`,\n );\n }\n\n const client = createAzureDevOpsClient({\n organization: azureConfig.organization,\n project,\n pat: azureConfig.pat,\n apiVersion: azureConfig.apiVersion,\n });\n\n const run = await client.request<PipelineRunResponse>({\n method: 'POST',\n path: `/_apis/pipelines/${pipeline.id}/runs`,\n body: {\n resources: {\n repositories: {\n self: {\n refName: pipeline.refName,\n },\n },\n },\n variables,\n },\n });\n\n return {\n project,\n instanceEnvironment: azureConfig.instanceEnvironment,\n pipelineEnvironment: azureConfig.pipelineEnvironment,\n pipelineId: pipeline.id,\n refName: pipeline.refName,\n run,\n };\n}\n\nexport function formatPipelineRunSummary(result: TriggerPipelineRunResult): string {\n const {\n run,\n project,\n instanceEnvironment,\n pipelineEnvironment,\n pipelineId,\n refName,\n } = result;\n const webUrl = run._links?.web?.href ?? run.url;\n const envLabel =\n instanceEnvironment === pipelineEnvironment\n ? `\\`${pipelineEnvironment}\\``\n : `\\`${instanceEnvironment}\\` → Azure \\`${pipelineEnvironment}\\``;\n const lines = [\n `Environment ${envLabel} — project \\`${project}\\`, pipeline id \\`${pipelineId}\\`, ref \\`${refName}\\``,\n `Run #${run.id}${run.state ? ` — state: \\`${run.state}\\`` : ''}`,\n ];\n if (webUrl) {\n lines.push(`<${webUrl}|Open run in Azure DevOps>`);\n }\n return lines.join('\\n');\n}\n","/**\n * Azure Pipelines only use staging / production naming.\n * Map AWS / shared `ENVIRONMENT` values (beta, live, …) at the adapter boundary.\n */\nexport type AzurePipelineEnvironment = 'staging' | 'production';\n\nconst PRODUCTION_ALIASES = new Set(['production', 'live', 'prod']);\nconst STAGING_ALIASES = new Set(['staging', 'beta', 'development', 'dev', 'local']);\n\n/**\n * Normalize any instance `ENVIRONMENT` value to Azure's staging | production.\n * Unknown values default to staging and log a warning (avoids queuing production by mistake).\n */\nexport function normalizeToAzureEnvironment(\n instanceEnvironment: string,\n): AzurePipelineEnvironment {\n const key = instanceEnvironment.trim().toLowerCase();\n if (!key) {\n console.warn(\n '[azure] Empty ENVIRONMENT; defaulting Azure pipeline environment to staging',\n );\n return 'staging';\n }\n if (PRODUCTION_ALIASES.has(key)) {\n return 'production';\n }\n if (STAGING_ALIASES.has(key)) {\n return 'staging';\n }\n console.warn(\n `[azure] Unrecognized ENVIRONMENT=\"${instanceEnvironment}\"; defaulting Azure pipeline environment to staging`,\n );\n return 'staging';\n}\n\n/** Git ref queued for the pipeline run (always derived from the Azure environment). */\nexport function pipelineRefForAzure(\n pipelineEnvironment: AzurePipelineEnvironment,\n): string {\n return `refs/heads/${pipelineEnvironment}`;\n}\n","import dotenv from 'dotenv';\nimport type { S3Config, CMSProvider } from '../shared/types';\nimport { SharedConfig, config as sharedConfig } from '../shared/config';\nimport type { AzureDevOpsConfig, AzureDevOpsProjectKey } from './adapters/azure';\nimport {\n AZURE_DEVOPS_PROJECT_KEYS,\n normalizeToAzureEnvironment,\n pipelineRefForAzure,\n} from './adapters/azure';\nimport type { AzurePipelineEnvironment } from './adapters/azure';\nimport type { ContentRefreshNotifyConfig } from './content-refresh-notify';\nimport { isContentRefreshEnabledForInstance } from './content-refresh-notify';\nimport { validateScheduleCronExpression } from './restrictedCron';\n\ndotenv.config({ path: '.env.local' });\ndotenv.config();\n\nexport type { CMSProvider, S3Config };\n\nexport interface ContentfulConfig {\n spaceId: string;\n accessToken: string;\n host: string;\n batchSize: number;\n /** Content types to sync. When empty, all content types in the space are synced. */\n contentTypes: string[];\n /** Max nesting depth when unwrapping resolved entries. Deeper references are dropped. */\n maxDepth: number;\n}\n\nexport interface SanityConfig {\n projectId: string;\n dataset: string;\n token: string;\n apiVersion: string;\n}\n\nexport interface LingohubConfig {\n authToken: string;\n workspace: string;\n}\n\nexport interface RetryConfig {\n maxRetries: number;\n baseDelayMs: number;\n maxDelayMs: number;\n}\n\nexport interface RestApiConfig {\n port: number;\n apiToken: string;\n}\n\n/** Background schedule (same behaviour as `content-store sync` in the CLI). */\nexport interface ScheduledCmsJobConfig {\n enabled: boolean;\n /** Two-field cron: minute then hour only. Example: `30 3` or a minute step with star in the hour field. Empty when disabled. */\n cronExpression: string;\n /** When true, run once when the server starts, then on the cron wall clock. */\n runOnStart: boolean;\n syncCms?: CMSProvider;\n syncTypes?: string[];\n}\n\nexport interface ScheduledTranslationJobConfig {\n enabled: boolean;\n cronExpression: string;\n runOnStart: boolean;\n syncProjects?: string[];\n}\n\nfunction readScheduleCronEnv(): {\n scheduleCron: string;\n cmsCronRaw: string;\n translationCronRaw: string;\n} {\n return {\n scheduleCron: (process.env.SCHEDULE_CRON ?? '').trim(),\n cmsCronRaw: (process.env.SCHEDULE_CMS_CRON ?? '').trim(),\n translationCronRaw: (process.env.SCHEDULE_TRANSLATION_CRON ?? '').trim(),\n };\n}\n\nfunction resolveScheduleCron(\n jobSpecific: string,\n globalCron: string,\n jobLabel: string,\n): string | null {\n const raw = jobSpecific || globalCron;\n if (!raw) return null;\n try {\n return validateScheduleCronExpression(raw);\n } catch (e) {\n const msg = e instanceof Error ? e.message : String(e);\n console.error(`[config] Invalid ${jobLabel} schedule cron: ${msg}`);\n return null;\n }\n}\n\nfunction parseScheduledCmsJobConfig(): ScheduledCmsJobConfig {\n const { scheduleCron, cmsCronRaw } = readScheduleCronEnv();\n const runOnStart =\n (process.env.SCHEDULE_RUN_ON_START ?? 'true').toLowerCase() !== 'false';\n const syncCms = (process.env.SCHEDULE_SYNC_CMS ?? '').trim() as CMSProvider;\n const rawTypes = process.env.SCHEDULE_SYNC_CONTENT_TYPES?.trim();\n const syncTypes = rawTypes\n ? rawTypes.split(',').map((s) => s.trim()).filter(Boolean)\n : undefined;\n\n const resolved = resolveScheduleCron(cmsCronRaw, scheduleCron, 'CMS');\n const cmsOk =\n !!syncCms && (syncCms === 'contentful' || syncCms === 'sanity');\n if (resolved !== null && !cmsOk) {\n console.warn(\n '[config] CMS schedule cron is set but SCHEDULE_SYNC_CMS is missing or not \"contentful\"|\"sanity\"; scheduled CMS sync is disabled.',\n );\n }\n const enabled = resolved !== null && cmsOk;\n\n return {\n enabled,\n cronExpression: resolved ?? '',\n runOnStart,\n syncCms: syncCms || undefined,\n syncTypes,\n };\n}\n\nfunction parseScheduledTranslationJobConfig(): ScheduledTranslationJobConfig {\n const { scheduleCron, translationCronRaw } = readScheduleCronEnv();\n const runOnStart =\n (process.env.SCHEDULE_RUN_ON_START ?? 'true').toLowerCase() !== 'false';\n const rawProjects = process.env.SCHEDULE_SYNC_TRANSLATION_PROJECTS?.trim();\n const syncProjects = rawProjects\n ? rawProjects.split(',').map((s) => s.trim()).filter(Boolean)\n : undefined;\n\n const resolved = resolveScheduleCron(\n translationCronRaw,\n scheduleCron,\n 'translation',\n );\n const enabled = resolved !== null;\n\n return {\n enabled,\n cronExpression: resolved ?? '',\n runOnStart,\n syncProjects,\n };\n}\n\nexport interface SlackConfig {\n enabled: boolean;\n botToken: string;\n signingSecret: string;\n /** Socket Mode app-level token (xapp-…). Required when enabled. */\n appToken: string;\n /** Channel ID or name for async notifications (e.g. sync results). */\n notifyChannel: string;\n /** Slash command name for CMS sync (default `/sync-content`). */\n cmdSyncContent: string;\n /** Slash command name for translation sync (default `/sync-translations`). */\n cmdSyncTranslations: string;\n /** Slash command to queue an Azure Pipelines run (default `/trigger-pipeline`). */\n cmdTriggerBuild: string;\n}\n\nfunction projectEnvSlug(project: AzureDevOpsProjectKey): string {\n return project.toUpperCase().replace(/-/g, '_');\n}\n\nfunction readProjectPipeline(\n project: AzureDevOpsProjectKey,\n pipelineEnvironment: AzurePipelineEnvironment,\n): { id: number; refName: string } {\n const slug = projectEnvSlug(project);\n const id = parseInt(process.env[`AZURE_${slug}_PIPELINE_ID`]?.trim() ?? '0', 10);\n return { id, refName: pipelineRefForAzure(pipelineEnvironment) };\n}\n\nfunction parseAzureDevOpsConfig(\n instanceEnvironment: string,\n): AzureDevOpsConfig & { enabled: boolean } {\n const pipelineEnvironment = normalizeToAzureEnvironment(instanceEnvironment);\n const pat = process.env.AZURE_DEVOPS_ACCESS_TOKEN?.trim() ?? '';\n\n const defaultProjectRaw = (\n process.env.AZURE_DEVOPS_DEFAULT_PROJECT ?? 'web-site'\n ).trim() as AzureDevOpsProjectKey;\n\n const defaultProject = (AZURE_DEVOPS_PROJECT_KEYS as string[]).includes(\n defaultProjectRaw,\n )\n ? defaultProjectRaw\n : 'web-site';\n\n const projects = Object.fromEntries(\n AZURE_DEVOPS_PROJECT_KEYS.map((project) => [\n project,\n { pipeline: readProjectPipeline(project, pipelineEnvironment) },\n ]),\n ) as AzureDevOpsConfig['projects'];\n\n return {\n enabled: pat.length > 0,\n organization: process.env.AZURE_DEVOPS_ORGANIZATION?.trim() || 'tripod-technology',\n pat,\n apiVersion: process.env.AZURE_DEVOPS_API_VERSION?.trim() || '7.1',\n defaultProject,\n instanceEnvironment,\n pipelineEnvironment,\n projects,\n };\n}\n\nfunction readContentRefreshUrl(project: AzureDevOpsProjectKey): string | undefined {\n const slug = projectEnvSlug(project);\n return process.env[`CONTENT_REFRESH_${slug}_URL`]?.trim() || undefined;\n}\n\nfunction parseContentRefreshConfig(\n instanceEnvironment: string,\n): ContentRefreshNotifyConfig {\n const targets = Object.fromEntries(\n AZURE_DEVOPS_PROJECT_KEYS.map((project) => [\n project,\n { url: readContentRefreshUrl(project) },\n ]),\n ) as ContentRefreshNotifyConfig['targets'];\n\n return {\n enabled: isContentRefreshEnabledForInstance(instanceEnvironment),\n basicAuth: process.env.CONTENT_REFRESH_BASIC_AUTH?.trim() ?? '',\n targets,\n };\n}\n\nexport interface ServerConfig {\n contentful: ContentfulConfig;\n sanity: SanityConfig;\n lingohub: LingohubConfig;\n retry: RetryConfig;\n api: RestApiConfig;\n scheduledCmsJob: ScheduledCmsJobConfig;\n scheduledTranslationJob: ScheduledTranslationJobConfig;\n azure: AzureDevOpsConfig & { enabled: boolean };\n contentRefresh: ContentRefreshNotifyConfig;\n slack: SlackConfig;\n}\n\nexport const config: ServerConfig & SharedConfig = {\n ...sharedConfig,\n contentful: {\n spaceId: process.env.CONTENTFUL_SPACE_ID ?? '',\n accessToken: process.env.CONTENTFUL_WEBSITE_TOKEN ?? '',\n host: process.env.CONTENTFUL_HOST ?? 'preview.contentful.com',\n batchSize: 1000,\n maxDepth: 4,\n contentTypes: [\n 'asset','page','longtailPage','customJson','banner','cookieBanner','downloadPage'\n // Add Contentful content type IDs here to limit sync scope.\n ],\n },\n\n sanity: {\n projectId: process.env.SANITY_PROJECT_ID ?? '',\n dataset: process.env.SANITY_DATASET ?? 'main',\n token: process.env.SANITY_API_TOKEN ?? '',\n apiVersion: '2024-01-01',\n },\n\n lingohub: {\n authToken : process.env.LINGOHUB_AUTH_TOKEN ?? '',\n workspace: process.env.LINGOHUB_WORKSPACE ?? '',\n },\n\n retry: {\n maxRetries: parseInt(process.env.RETRY_MAX_RETRIES ?? '5', 10),\n baseDelayMs: parseInt(process.env.RETRY_BASE_DELAY_MS ?? '1000', 10),\n maxDelayMs: parseInt(process.env.RETRY_MAX_DELAY_MS ?? '60000', 10),\n },\n\n api: {\n port: parseInt(process.env.PORT ?? '3030', 10),\n apiToken: process.env.CONTENT_STORE_API_TOKEN ?? '',\n },\n\n scheduledCmsJob: parseScheduledCmsJobConfig(),\n scheduledTranslationJob: parseScheduledTranslationJobConfig(),\n\n azure: parseAzureDevOpsConfig(sharedConfig.environment),\n\n contentRefresh: parseContentRefreshConfig(sharedConfig.environment),\n\n slack: {\n enabled: (process.env.SLACK_BOT_TOKEN ?? '').length > 0,\n botToken: process.env.SLACK_BOT_TOKEN ?? '',\n signingSecret: process.env.SLACK_SIGNING_SECRET ?? '',\n appToken: process.env.SLACK_APP_TOKEN ?? '',\n notifyChannel: process.env.SLACK_NOTIFY_CHANNEL ?? '',\n cmdSyncContent: process.env.SLACK_CMD_SYNC_CONTENT ?? '/sync-content',\n cmdSyncTranslations: process.env.SLACK_CMD_SYNC_TRANSLATIONS ?? '/sync-translations',\n cmdTriggerBuild:\n process.env.SLACK_CMD_TRIGGER_BUILD ?? '/trigger-build',\n },\n};\n","/**\n * Schedule expressions: exactly **two** whitespace-separated fields: minute, then hour\n * (same meaning as the first two fields of standard cron). No day-of-month, month, or\n * day-of-week — those are not accepted.\n *\n * Supported per field: `*`, `n`, `a-b`, steps (asterisk + slash + step), and comma lists.\n * Step must be >= 1.\n */\n\nconst MAX_SEARCH_MINUTES = 366 * 24 * 60;\n\nfunction tokenize(expr: string): string[] {\n return expr\n .trim()\n .split(/\\s+/)\n .map((s) => s.trim())\n .filter(Boolean);\n}\n\n/**\n * Validates that `expr` is exactly two cron fields (minute hour) and returns them\n * as a single normalized string `\"<minute> <hour>\"` for storage and logging.\n */\nexport function validateScheduleCronExpression(expr: string): string {\n const parts = tokenize(expr);\n if (parts.length !== 2) {\n throw new Error(\n `Schedule cron must be exactly two fields (minute hour), whitespace-separated; got ${parts.length} field(s): \"${expr}\"`,\n );\n }\n const minuteSpec = parts[0]!;\n const hourSpec = parts[1]!;\n parseField(minuteSpec, 0, 59, 'minute');\n parseField(hourSpec, 0, 23, 'hour');\n return `${minuteSpec} ${hourSpec}`;\n}\n\nfunction parseField(\n spec: string,\n lo: number,\n hi: number,\n fieldName: string,\n): (n: number) => boolean {\n const subs = spec.split(',').map((s) => s.trim()).filter(Boolean);\n if (subs.length === 0) {\n throw new Error(`Empty ${fieldName} field in cron`);\n }\n const preds = subs.map((sub) => parseSubfield(sub, lo, hi, fieldName));\n return (n: number) => preds.some((p) => p(n));\n}\n\nfunction parseSubfield(\n sub: string,\n lo: number,\n hi: number,\n fieldName: string,\n): (n: number) => boolean {\n if (sub === '*') {\n return () => true;\n }\n if (sub.startsWith('*/')) {\n const step = parseInt(sub.slice(2), 10);\n if (!Number.isFinite(step) || step < 1) {\n throw new Error(`Invalid step in ${fieldName} field: \"${sub}\"`);\n }\n return (n: number) => n >= lo && n <= hi && n % step === 0;\n }\n if (sub.includes('-')) {\n const [a, b] = sub.split('-').map((x) => parseInt(x.trim(), 10));\n if (!Number.isFinite(a) || !Number.isFinite(b)) {\n throw new Error(`Invalid range in ${fieldName} field: \"${sub}\"`);\n }\n if (a < lo || b > hi || a > b) {\n throw new Error(`Range out of bounds in ${fieldName} field: \"${sub}\"`);\n }\n return (n: number) => n >= a && n <= b;\n }\n const v = parseInt(sub, 10);\n if (!Number.isFinite(v) || v < lo || v > hi) {\n throw new Error(`Invalid value in ${fieldName} field: \"${sub}\"`);\n }\n return (n: number) => n === v;\n}\n\nfunction floorToMinuteStart(d: Date): Date {\n const t = new Date(d.getTime());\n t.setSeconds(0, 0);\n return t;\n}\n\nfunction addOneMinute(d: Date): Date {\n const t = new Date(d.getTime());\n t.setMinutes(t.getMinutes() + 1, 0, 0);\n return t;\n}\n\nfunction matchesAtMinuteStart(\n minutePred: (m: number) => boolean,\n hourPred: (h: number) => boolean,\n d: Date,\n): boolean {\n return minutePred(d.getMinutes()) && hourPred(d.getHours());\n}\n\n/**\n * Earliest minute boundary strictly after `after` that satisfies the two-field expression.\n */\nexport function nextCronFireAfter(expr: string, after: Date): Date {\n const parts = tokenize(expr);\n if (parts.length !== 2) {\n throw new Error(\n `nextCronFireAfter expects exactly two fields (minute hour); got ${parts.length}: \"${expr}\"`,\n );\n }\n const minutePred = parseField(parts[0]!, 0, 59, 'minute');\n const hourPred = parseField(parts[1]!, 0, 23, 'hour');\n\n let d = floorToMinuteStart(after);\n if (d.getTime() <= after.getTime()) {\n d = addOneMinute(d);\n }\n\n for (let i = 0; i < MAX_SEARCH_MINUTES; i++) {\n if (matchesAtMinuteStart(minutePred, hourPred, d)) {\n return d;\n }\n d = addOneMinute(d);\n }\n\n throw new Error(`No cron match within ${MAX_SEARCH_MINUTES} minutes for \"${expr}\"`);\n}\n","import { timingSafeEqual } from 'node:crypto';\nimport type { CMSProvider } from './types';\nimport type {\n FetchCmsBundlesOptions,\n FetchTranslationBundlesOptions,\n TranslationBundleInfo,\n} from './bundles';\n\nexport type ContentRefreshScope = 'cms' | 'translations' | 'all';\n\nexport interface ContentRefreshRequest {\n scope?: ContentRefreshScope;\n cms?: CMSProvider;\n content_types?: string[];\n projects?: string[];\n locales?: string[];\n}\n\n/** Defaults applied when the request omits fields (set by the host application). */\nexport interface ContentRefreshDefaults {\n scope?: ContentRefreshScope;\n cms?: CMSProvider;\n contentTypes?: string[];\n translationProjects?: string[];\n locales?: string[];\n}\n\nexport interface ContentRefreshError {\n step: string;\n message: string;\n}\n\nexport interface ContentRefreshResult {\n ok: boolean;\n scope: ContentRefreshScope;\n cmsFiles?: Record<string, string>;\n translationFiles?: TranslationBundleInfo;\n errors: ContentRefreshError[];\n durationMs: number;\n}\n\nexport interface ContentRefreshFetchers {\n fetchCmsBundles: (\n options: FetchCmsBundlesOptions,\n ) => Promise<Record<string, string>>;\n fetchTranslationBundles: (\n options: FetchTranslationBundlesOptions,\n ) => Promise<TranslationBundleInfo>;\n}\n\nexport function resolveContentRefreshScope(\n request: ContentRefreshRequest,\n defaults: ContentRefreshDefaults,\n): ContentRefreshScope {\n return request.scope ?? defaults.scope ?? 'cms';\n}\n\n/** Base64 of `username:password` (value only — prefix with `Basic ` in the header). */\nexport function encodeBasicAuthCredentials(\n username: string,\n password: string,\n): string {\n return Buffer.from(`${username}:${password}`, 'utf8').toString('base64');\n}\n\n/**\n * Validates `Authorization: Basic <base64>` against the expected credentials\n * (base64 of `user:pass`, same as staging site basic auth).\n */\nexport function assertContentRefreshBasicAuth(\n authorizationHeader: string | undefined,\n expectedCredentialsBase64: string,\n): void {\n if (!expectedCredentialsBase64) {\n throw new ContentRefreshAuthError(\n 'Content refresh basic auth is not configured (set CONTENT_REFRESH_BASIC_AUTH)',\n 500,\n );\n }\n if (!authorizationHeader?.startsWith('Basic ')) {\n throw new ContentRefreshAuthError(\n 'Missing or malformed Authorization header (expected Basic)',\n 401,\n );\n }\n const provided = authorizationHeader.slice(6).trim();\n const bufA = new TextEncoder().encode(provided);\n const bufB = new TextEncoder().encode(expectedCredentialsBase64);\n if (bufA.byteLength !== bufB.byteLength || !timingSafeEqual(bufA, bufB)) {\n throw new ContentRefreshAuthError('Invalid basic auth credentials', 403);\n }\n}\n\nexport class ContentRefreshAuthError extends Error {\n constructor(\n message: string,\n readonly statusCode: number,\n ) {\n super(message);\n this.name = 'ContentRefreshAuthError';\n }\n}\n\n/**\n * Pull the latest bundles from S3 into the host app's configured output directory.\n */\nexport async function executeContentRefresh(\n fetchers: ContentRefreshFetchers,\n request: ContentRefreshRequest,\n defaults: ContentRefreshDefaults = {},\n): Promise<ContentRefreshResult> {\n const started = Date.now();\n const scope = resolveContentRefreshScope(request, defaults);\n const errors: ContentRefreshError[] = [];\n let cmsFiles: Record<string, string> | undefined;\n let translationFiles: TranslationBundleInfo | undefined;\n\n if (scope === 'cms' || scope === 'all') {\n const cms = request.cms ?? defaults.cms ?? 'contentful';\n const contentTypes = request.content_types ?? defaults.contentTypes;\n if (!contentTypes?.length) {\n errors.push({\n step: 'cms',\n message: 'content_types (or handler defaults.contentTypes) is required for CMS refresh',\n });\n } else {\n try {\n cmsFiles = await fetchers.fetchCmsBundles({ cms, contentTypes });\n } catch (err) {\n errors.push({\n step: 'cms',\n message: err instanceof Error ? err.message : String(err),\n });\n }\n }\n }\n\n if (scope === 'translations' || scope === 'all') {\n const projects = request.projects ?? defaults.translationProjects;\n if (!projects?.length) {\n errors.push({\n step: 'translations',\n message:\n 'projects (or handler defaults.translationProjects) is required for translation refresh',\n });\n } else {\n const locales = request.locales ?? defaults.locales;\n try {\n const projectMap = Object.fromEntries(\n projects.map((p) => [p, [] as string[]]),\n );\n translationFiles = await fetchers.fetchTranslationBundles({\n projects: projectMap,\n locales,\n });\n } catch (err) {\n errors.push({\n step: 'translations',\n message: err instanceof Error ? err.message : String(err),\n });\n }\n }\n }\n\n return {\n ok: errors.length === 0,\n scope,\n cmsFiles,\n translationFiles,\n errors,\n durationMs: Date.now() - started,\n };\n}\n\nexport interface PostContentRefreshResponse {\n target: string;\n ok: boolean;\n status: number;\n body?: unknown;\n error?: string;\n}\n\n/**\n * Ask a remote application (web-site, web-app, …) to refresh its local content cache.\n */\nexport async function postContentRefresh(\n target: string,\n url: string,\n basicAuth: string,\n request: ContentRefreshRequest,\n): Promise<PostContentRefreshResponse> {\n try {\n const response = await fetch(url, {\n method: 'POST',\n headers: {\n Authorization: `Basic ${basicAuth}`,\n 'Content-Type': 'application/json',\n Accept: 'application/json',\n },\n body: JSON.stringify(request),\n });\n\n const text = await response.text();\n let body: unknown;\n if (text) {\n try {\n body = JSON.parse(text) as unknown;\n } catch {\n body = text;\n }\n }\n\n return {\n target,\n ok: response.ok,\n status: response.status,\n body,\n error: response.ok\n ? undefined\n : typeof body === 'object' && body !== null && 'error' in body\n ? String((body as { error: unknown }).error)\n : `HTTP ${response.status}`,\n };\n } catch (err) {\n return {\n target,\n ok: false,\n status: 0,\n error: err instanceof Error ? err.message : String(err),\n };\n }\n}\n","import type { AzureDevOpsProjectKey } from './adapters/azure';\nimport { AZURE_DEVOPS_PROJECT_KEYS } from './adapters/azure';\nimport { normalizeToAzureEnvironment } from './adapters/azure';\nimport { config, type CMSProvider } from './config';\nimport type { CmsSyncResult } from './sync/engine';\nimport {\n postContentRefresh,\n type ContentRefreshRequest,\n type PostContentRefreshResponse,\n} from '../shared/content-refresh';\n\nexport interface ContentRefreshTargetConfig {\n url?: string;\n}\n\nexport interface ContentRefreshNotifyConfig {\n /** When false, never call client refresh URLs (e.g. production uses pipeline builds). */\n enabled: boolean;\n /** Base64 of `username:password` (no `Basic ` prefix). */\n basicAuth: string;\n targets: Record<AzureDevOpsProjectKey, ContentRefreshTargetConfig>;\n}\n\nexport function isContentRefreshEnabledForInstance(\n instanceEnvironment: string,\n): boolean {\n return normalizeToAzureEnvironment(instanceEnvironment) === 'staging';\n}\n\nexport async function notifyContentRefreshTargets(\n notifyConfig: ContentRefreshNotifyConfig,\n request: ContentRefreshRequest,\n projects?: AzureDevOpsProjectKey[],\n): Promise<PostContentRefreshResponse[]> {\n if (!notifyConfig.enabled || !notifyConfig.basicAuth) {\n return [];\n }\n\n const keys =\n projects ??\n (AZURE_DEVOPS_PROJECT_KEYS.filter(\n (p) => notifyConfig.targets[p]?.url,\n ) as AzureDevOpsProjectKey[]);\n\n const results: PostContentRefreshResponse[] = [];\n\n for (const project of keys) {\n const url = notifyConfig.targets[project]?.url;\n if (!url) {\n continue;\n }\n const result = await postContentRefresh(\n project,\n url,\n notifyConfig.basicAuth,\n request,\n );\n results.push(result);\n if (result.ok) {\n console.log(\n `[content-refresh] Notified ${project} (${result.status})`,\n );\n } else {\n console.error(\n `[content-refresh] Failed to notify ${project}: ${result.error ?? result.status}`,\n );\n }\n }\n\n return results;\n}\n\n/**\n * After a successful CMS sync (Slack, HTTP API, or cron), POST to configured client refresh URLs.\n */\nexport async function notifyClientsAfterCmsSync(\n result: CmsSyncResult,\n requestedContentTypes?: string[],\n projects?: AzureDevOpsProjectKey[],\n): Promise<PostContentRefreshResponse[]> {\n const { contentRefresh } = config;\n\n if (!contentRefresh.enabled) {\n console.log(\n '[content-refresh] Skipped after CMS sync (disabled on this instance; staging/beta only)',\n );\n return [];\n }\n if (!contentRefresh.basicAuth) {\n console.warn(\n '[content-refresh] Skipped after CMS sync (set CONTENT_REFRESH_BASIC_AUTH)',\n );\n return [];\n }\n if (result.errors.length > 0) {\n console.log('[content-refresh] Skipped after CMS sync (sync had errors)');\n return [];\n }\n\n const types = requestedContentTypes?.length\n ? requestedContentTypes\n : result.entries.map((e) => e.contentType);\n if (types.length === 0) {\n console.warn('[content-refresh] Skipped after CMS sync (no content types)');\n return [];\n }\n\n const targets = (\n projects ??\n (AZURE_DEVOPS_PROJECT_KEYS.filter(\n (p) => contentRefresh.targets[p]?.url,\n ) as AzureDevOpsProjectKey[])\n );\n\n if (targets.length === 0) {\n console.warn(\n '[content-refresh] Skipped after CMS sync (no CONTENT_REFRESH_*_URL configured)',\n );\n return [];\n }\n\n console.log(\n `[content-refresh] Notifying after CMS sync (${result.cms}): ${targets.join(', ')}`,\n );\n\n return notifyContentRefreshTargets(\n contentRefresh,\n {\n scope: 'cms',\n cms: result.cms as CMSProvider,\n content_types: types,\n },\n projects,\n );\n}\n","/** Per-API request counts for a single Contentful sync run. */\nexport interface ContentfulApiUsage {\n cda: number;\n cma: number;\n cpa: number;\n}\n\nexport type ContentfulApiKind = keyof ContentfulApiUsage;\n\nexport function emptyContentfulApiUsage(): ContentfulApiUsage {\n return { cda: 0, cma: 0, cpa: 0 };\n}\n\n/** Map Contentful client `host` to CDA / CPA / CMA (one kind per adapter instance). */\nexport function contentfulApiKindFromHost(host: string): ContentfulApiKind {\n const h = host.toLowerCase();\n if (h.includes('preview')) {\n return 'cpa';\n }\n if (h.includes('api.contentful') || h === 'api.contentful.com') {\n return 'cma';\n }\n return 'cda';\n}\n\nexport function totalContentfulApiCalls(usage: ContentfulApiUsage): number {\n return usage.cda + usage.cma + usage.cpa;\n}\n\n/** Slack / log lines for Contentful API usage (omits zero counts except total). */\nexport function summariseContentfulApiUsage(usage: ContentfulApiUsage): string[] {\n const lines: string[] = [];\n if (usage.cda > 0) {\n lines.push(`CDA requests: ${usage.cda}`);\n }\n if (usage.cpa > 0) {\n lines.push(`CPA requests: ${usage.cpa}`);\n }\n if (usage.cma > 0) {\n lines.push(`CMA requests: ${usage.cma}`);\n }\n lines.push(`Total contentful API calls: ${totalContentfulApiCalls(usage)}`);\n return lines;\n}\n\nexport class ContentfulApiUsageTracker {\n private readonly kind: ContentfulApiKind;\n private readonly counts = emptyContentfulApiUsage();\n\n constructor(host: string) {\n this.kind = contentfulApiKindFromHost(host);\n }\n\n recordCall(): void {\n this.counts[this.kind] += 1;\n }\n\n snapshot(): ContentfulApiUsage {\n return { ...this.counts };\n }\n}\n","import {\n createClient,\n type ContentfulClientApi,\n type ContentTypeCollection,\n type AssetCollection,\n} from 'contentful';\nimport type { ContentfulConfig, RetryConfig } from '../config';\nimport type { CMSAdapter, FetchResult } from './types';\nimport {\n type ContentfulApiUsage,\n ContentfulApiUsageTracker,\n} from './contentful-api-usage';\nimport { withRetry } from '../sync/retry';\n\ntype CfCollection = {\n items: CfItem[],\n total: number\n}\n\ntype CfItem = {\n metadata:{\n tags: string[],\n concepts: string[]\n }\n sys:{\n type: string,\n id: string\n space: {\n sys: {\n type: string\n linkType: string\n id: string\n }\n },\n environment: {\n sys: {\n id: string\n type: 'Link',\n linkType: 'Environment'\n }\n },\n contentType: {\n sys: {\n type: 'Link',\n linkType: 'ContentType',\n id: string\n }\n },\n createdBy: {\n sys: {\n type: 'Link',\n linkType: 'User',\n id: string,\n }\n },\n updatedBy: {\n sys: {\n type: 'Link',\n linkType: 'User',\n id: string\n }\n },\n 'revision': number,\n 'createdAt': string,\n 'updatedAt': string,\n 'publishedVersion': string\n },\n fields:{\n [key:string]: unknown\n }\n}\n\n\n/**\n * Recursively unwraps Contentful's { metadata, sys, fields } envelope.\n * `depth` tracks how many entry/asset envelopes deep we are — anything\n * beyond `maxDepth` is dropped to avoid blowing the call stack on\n * circular or extremely deep reference chains.\n *\n * `path` holds objects on the current recursion branch only. That way a\n * shared reference (e.g. the same asset on `image` and `mobileImage`) is\n * unwrapped for each sibling; only true cycles (an object recurring as a\n * descendant of itself) yield `undefined`.\n */\nfunction stripEnvelope(\n value: unknown,\n maxDepth: number,\n depth = 0,\n path = new WeakSet<object>(),\n): unknown {\n if (value === null || typeof value !== 'object') return value;\n\n const obj = value as CfItem;\n\n if (path.has(obj)) return undefined;\n path.add(obj);\n\n try {\n if (Array.isArray(value)) {\n return value.map((item) => stripEnvelope(item, maxDepth, depth, path));\n }\n\n const isEnvelope = 'sys' in obj && 'fields' in obj && typeof obj.fields === 'object';\n\n if (isEnvelope) {\n if (depth >= maxDepth) return undefined;\n\n const fields = obj.fields as Record<string, unknown>;\n const result: Record<string, unknown> = {};\n for (const [k, v] of Object.entries(fields)) {\n result[k] = stripEnvelope(v, maxDepth, depth + 1, path);\n }\n\n const sys = obj.sys;\n if (sys) {\n const existingMeta = result.meta;\n const metaBase =\n typeof existingMeta === 'object' &&\n existingMeta !== null &&\n !Array.isArray(existingMeta)\n ? (existingMeta as Record<string, unknown>)\n : {};\n const _contentType = sys.contentType ? sys.contentType.sys.id : 'Asset'\n result.meta = { ...metaBase, _id: sys.id, _contentType, _updatedAt: sys.updatedAt };\n }\n\n return result;\n }\n\n const result: Record<string, unknown> = {};\n for (const [k, v] of Object.entries(obj)) {\n result[k] = stripEnvelope(v, maxDepth, depth, path);\n }\n return result;\n } finally {\n path.delete(obj);\n }\n}\n\nexport class ContentfulAdapter implements CMSAdapter {\n readonly name = 'contentful';\n private client: ContentfulClientApi<undefined>;\n private batchSize: number;\n private maxDepth: number;\n private allowedTypes: string[];\n private retryConfig: RetryConfig;\n private readonly apiUsage: ContentfulApiUsageTracker;\n\n constructor(cfg: ContentfulConfig, retryConfig: RetryConfig) {\n this.client = createClient({\n space: cfg.spaceId,\n accessToken: cfg.accessToken,\n host: cfg.host,\n });\n this.batchSize = cfg.batchSize;\n this.maxDepth = cfg.maxDepth;\n this.allowedTypes = cfg.contentTypes;\n this.retryConfig = retryConfig;\n this.apiUsage = new ContentfulApiUsageTracker(cfg.host);\n }\n\n getContentfulApiUsage(): ContentfulApiUsage {\n return this.apiUsage.snapshot();\n }\n\n async getContentTypes(): Promise<string[]> {\n const response = await withRetry<ContentTypeCollection>(\n () => {\n this.apiUsage.recordCall();\n return this.client.getContentTypes();\n },\n this.retryConfig,\n );\n\n const allTypes = response.items.map((ct) => ct.sys.id);\n\n if (this.allowedTypes.length > 0) {\n return allTypes.filter((t) => this.allowedTypes.includes(t));\n }\n return allTypes;\n }\n\n /**\n * Fetches every entry for a content type using batched pagination.\n * Contentful caps `getEntries` at 1 000 items per call, so we page through\n * with `skip` until all items are collected.\n *\n * The reserved content type `\"asset\"` fetches from `getAssets()` instead.\n */\n async fetchAll(contentType: string, includeLevels: 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 = 4): Promise<FetchResult> {\n if (contentType === 'asset') {\n return this.fetchAllAssets();\n }\n\n const allItems: unknown[] = [];\n let skip = 0;\n let total = 0;\n\n do {\n const payload = {\n content_type: contentType,\n limit: this.batchSize,\n skip,\n include: includeLevels,\n };\n this.apiUsage.recordCall();\n const response = await this.client.getEntries(payload) as unknown as CfCollection;\n total = response.total;\n allItems.push(...response.items);\n skip += response.items.length;\n\n if (total > this.batchSize) {\n console.log(\n ` [contentful] ${contentType}: fetched ${allItems.length}/${total}`,\n );\n }\n } while (skip < total);\n\n return {\n contentType,\n items: allItems.map((item) => stripEnvelope(item, this.maxDepth)),\n total,\n };\n }\n\n private async fetchAllAssets(): Promise<FetchResult> {\n const allItems: unknown[] = [];\n let skip = 0;\n let total = 0;\n\n do {\n const response = await withRetry<AssetCollection>(\n () => {\n this.apiUsage.recordCall();\n return this.client.getAssets({ limit: this.batchSize, skip });\n },\n this.retryConfig,\n );\n total = response.total;\n allItems.push(...response.items);\n skip += response.items.length;\n\n if (total > this.batchSize) {\n console.log(\n ` [contentful] asset: fetched ${allItems.length}/${total}`,\n );\n }\n } while (skip < total);\n\n return {\n contentType: 'asset',\n items: allItems.map((item) => stripEnvelope(item, this.maxDepth)),\n total,\n };\n }\n}\n","import type { RetryConfig } from '../config';\n\n/**\n * Inspects an error to determine if it represents an API rate-limit (HTTP 429).\n * Returns the suggested wait time in ms when available, otherwise `0` to signal\n * that the caller should fall back to computed backoff. Returns `null` when the\n * error is *not* a rate-limit error.\n */\nfunction rateLimitDelayMs(err: unknown): number | null {\n const e = err as Record<string, unknown>;\n\n if (e?.status === 429 || e?.statusCode === 429) {\n const reset = (e?.headers as Record<string, string> | undefined)?.[\n 'x-contentful-ratelimit-reset'\n ];\n return reset ? parseFloat(reset) * 1000 : 0;\n }\n\n const resp = e?.response as Record<string, unknown> | undefined;\n if (resp?.status === 429) {\n const headers = resp?.headers as Record<string, string> | undefined;\n const reset = headers?.['x-contentful-ratelimit-reset'];\n return reset ? parseFloat(reset) * 1000 : 0;\n }\n\n return null;\n}\n\nfunction computeDelay(\n attempt: number,\n baseDelayMs: number,\n maxDelayMs: number,\n): number {\n const exponential = baseDelayMs * Math.pow(2, attempt);\n const jitter = Math.random() * baseDelayMs;\n return Math.min(exponential + jitter, maxDelayMs);\n}\n\n/**\n * Executes `fn` with automatic retry + exponential backoff.\n * Rate-limit (429) responses are handled specially: if the API provides a\n * Retry-After / reset header, that value is respected instead of computed backoff.\n */\nexport async function withRetry<T>(\n fn: () => Promise<T>,\n { maxRetries, baseDelayMs, maxDelayMs }: RetryConfig,\n): Promise<T> {\n for (let attempt = 0; attempt <= maxRetries; attempt++) {\n try {\n return await fn();\n } catch (err) {\n if (attempt === maxRetries) throw err;\n\n const rlDelay = rateLimitDelayMs(err);\n let delay: number;\n\n if (rlDelay !== null) {\n delay =\n rlDelay > 0 ? rlDelay : computeDelay(attempt, baseDelayMs, maxDelayMs);\n console.warn(\n ` Rate limited (attempt ${attempt + 1}/${maxRetries}). ` +\n `Waiting ${Math.round(delay)}ms…`,\n );\n } else {\n delay = computeDelay(attempt, baseDelayMs, maxDelayMs);\n console.warn(\n ` Request failed (attempt ${attempt + 1}/${maxRetries}): ` +\n `${(err as Error).message}. Retrying in ${Math.round(delay)}ms…`,\n );\n }\n\n await new Promise((resolve) => setTimeout(resolve, delay));\n }\n }\n\n throw new Error('withRetry: unreachable');\n}\n","import { createClient, type SanityClient } from '@sanity/client';\nimport type { SanityConfig, RetryConfig } from '../config';\nimport type { CMSAdapter, FetchResult } from './types';\nimport { withRetry } from '../sync/retry';\n\nexport class SanityAdapter implements CMSAdapter {\n readonly name = 'sanity';\n private client: SanityClient;\n private retryConfig: RetryConfig;\n\n constructor(cfg: SanityConfig, retryConfig: RetryConfig) {\n this.client = createClient({\n projectId: cfg.projectId,\n dataset: cfg.dataset,\n token: cfg.token,\n apiVersion: cfg.apiVersion,\n useCdn: false,\n });\n this.retryConfig = retryConfig;\n }\n\n async getContentTypes(): Promise<string[]> {\n const types: string[] = await withRetry(\n () => this.client.fetch('array::unique(*[]._type)'),\n this.retryConfig,\n );\n return types.filter(\n (t) => !t.startsWith('system.') && !t.startsWith('sanity.'),\n );\n }\n\n async fetchAll(contentType: string): Promise<FetchResult> {\n const items: unknown[] = await withRetry(\n () => this.client.fetch('*[_type == $type]', { type: contentType }),\n this.retryConfig,\n );\n\n console.log(` [sanity] ${contentType}: fetched ${items.length} items`);\n return { contentType, items, total: items.length };\n }\n}\n","import { config, type CMSProvider } from '../config';\nimport type { CMSAdapter } from './types';\nimport { ContentfulAdapter } from './contentful';\nimport { SanityAdapter } from './sanity';\n\nexport function createAdapter(cms: CMSProvider): CMSAdapter {\n switch (cms) {\n case 'contentful':\n return new ContentfulAdapter(config.contentful, config.retry);\n case 'sanity':\n return new SanityAdapter(config.sanity, config.retry);\n default:\n throw new Error(`Unknown CMS provider: ${cms as string}`);\n }\n}\n\nexport type { CMSAdapter, FetchResult } from './types';\n","import { config } from '../config';\nimport type { LingohubResource } from '../../shared/lingohub';\n\nconst cfg = config.lingohub;\nconst apiUrl = 'https://api.lingohub.com/v1/' + cfg.workspace + '/projects/';\n\n/**\n * Downloads the raw Lingohub resource body (exact bytes as UTF-8 text).\n * Sync uploads this unmodified to S3; conversion happens at fetch time.\n */\nexport async function fetchLingohubResourceRaw(\n project: string,\n resource: LingohubResource,\n locale: string,\n): Promise<string> {\n const urlForResourceLocalised =\n `${apiUrl}${project}/resources/${resource.fileName}?auth_token=${cfg.authToken}`.replace(\n '[locale]',\n locale,\n );\n const res = await fetch(urlForResourceLocalised, { method: 'GET' });\n if (!res.ok) {\n throw new Error(`Failed to fetch resource \\`${resource.fileName}\\` (${locale}): ${res.status} - ${res.statusText}`);\n }\n return await res.text();\n}\n","import type { ContentfulApiUsage } from '../adapters/contentful-api-usage';\nimport { summariseContentfulApiUsage } from '../adapters/contentful-api-usage';\nimport type { CMSProvider } from '../config';\nimport { config } from '../config';\nimport { createAdapter } from '../adapters';\nimport {buildCmsObjectKey, buildTranslationObjectKey, ContentStore} from '../../shared/s3';\nimport { allProjects, defaultLocales } from '../../shared/lingohub';\nimport { fetchLingohubResourceRaw } from '../adapters/lingohub';\nimport { contentTypeForTranslationKey } from '../../shared/translationResource';\nimport { withRetry } from './retry';\n\nexport interface CmsSyncResultEntry {\n contentType: string;\n itemCount: number;\n objectKey: string;\n}\n\nexport interface CmsSyncResult {\n cms: CMSProvider;\n timestamp: number;\n entries: CmsSyncResultEntry[];\n errors: Array<{ contentType: string; error: string }>;\n /** Present when `cms` is `contentful`; counts each Contentful SDK HTTP request in this run. */\n contentfulApiUsage?: ContentfulApiUsage;\n}\n\nexport interface TranslationSyncResultEntry {\n project: string;\n resource: string;\n locale: string;\n /** UTF-8 byte size of the raw Lingohub file uploaded to S3. */\n itemCount: number;\n objectKey: string;\n}\n\nexport interface TranslationSyncResult {\n timestamp: number;\n entries: TranslationSyncResultEntry[];\n errors: Array<{ locale: string; project: string, error: string }>;\n}\n\nexport function summariseCmsEntries(result: CmsSyncResult): string[] {\n return result.entries.map(\n (e) => `• \\`${e.contentType}\\` — ${e.itemCount} items`,\n );\n}\n\nexport function summariseCmsErrors(result: CmsSyncResult): string[] {\n return result.errors.map(\n (e) => `• \\`${e.contentType}\\` — ${e.error}`,\n );\n}\n\n/** Slack-style lines for Contentful CDA/CPA/CMA request totals (empty when not Contentful). */\nexport function summariseCmsContentfulApiUsage(result: CmsSyncResult): string[] {\n if (!result.contentfulApiUsage) {\n return [];\n }\n return summariseContentfulApiUsage(result.contentfulApiUsage);\n}\n\n/** Group translation sync entries into one summary line per project/resource. */\nexport function summariseTranslationEntries(entries: TranslationSyncResultEntry[]): string[] {\n const grouped = new Map<string, { locales: number; bytes: number }>();\n for (const e of entries) {\n const key = `${e.project} / ${e.resource}`;\n const existing = grouped.get(key);\n if (existing) {\n existing.locales += 1;\n existing.bytes += e.itemCount;\n } else {\n grouped.set(key, { locales: 1, bytes: e.itemCount });\n }\n }\n return [...grouped.entries()].map(\n ([key, { locales, bytes }]) => `• \\`${key}\\` / [${locales} locale${locales === 1 ? '' : 's'}] — ${bytes} bytes`,\n );\n}\n\nexport async function runSync(cms: CMSProvider, contentTypes?: string[], includeLevels?: number){\n await syncCmsContent(cms, contentTypes, includeLevels )\n}\n\nexport async function syncCmsContent(\n cms: CMSProvider,\n contentTypes?: string[],\n includeLevels?: number\n): Promise<CmsSyncResult> {\n const adapter = createAdapter(cms);\n const store = new ContentStore(config.s3);\n const timestamp = Math.floor(Date.now() / 1000);\n\n console.log(`\\nStarting sync from ${cms} at ${new Date(timestamp * 1000).toISOString()}`);\n\n const typesToSync =\n contentTypes && contentTypes.length > 0\n ? contentTypes\n : await adapter.getContentTypes();\n\n console.log(`Content types to sync: ${typesToSync.join(', ')}\\n`);\n\n const entries: CmsSyncResultEntry[] = [];\n const errors: Array<{ contentType: string; error: string }> = [];\n\n for (const contentType of typesToSync) {\n try {\n const result = await adapter.fetchAll(contentType, includeLevels);\n const objectKey = buildCmsObjectKey(cms, contentType);\n await store.upload(objectKey, result.items);\n\n entries.push({\n contentType,\n itemCount: result.total,\n objectKey,\n });\n\n console.log(\n ` + ${contentType}: ${result.total} items -> ${objectKey}`,\n );\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n errors.push({ contentType, error: message });\n console.error(` x ${contentType}: ${message}`);\n }\n }\n\n const contentfulApiUsage = adapter.getContentfulApiUsage?.();\n\n if (contentfulApiUsage) {\n console.log(\n `\\nContentful API calls: ${summariseContentfulApiUsage(contentfulApiUsage).join(', ')}`,\n );\n }\n\n console.log(\n `\\nSync complete: ${entries.length} succeeded, ${errors.length} failed\\n`,\n );\n\n return { cms, timestamp, entries, errors, contentfulApiUsage };\n}\n\nexport async function syncTranslations(projects?: string[], locales?:string[]):Promise<TranslationSyncResult> {\n\n const store = new ContentStore(config.s3);\n const entries: TranslationSyncResultEntry[] = [];\n const errors: Array<{ project: string; locale: string, error: string }> = [];\n const timestamp = Math.floor(Date.now() / 1000);\n if(!locales){\n locales = defaultLocales;\n }\n if(!projects){\n projects = Object.keys(allProjects);\n }\n\n for(const project of projects) {\n const resources = allProjects[project];\n if(!resources){\n console.error(`No resources found for ${project}`);\n continue;\n }\n for(const resource of resources) {\n const resourceLocales = resource.locales ?? locales;\n for(const loc of resourceLocales){\n const locale = (resource.localeMapping && resource.localeMapping[loc]) ? resource.localeMapping[loc] : loc;\n try {\n const raw = await withRetry(\n () => fetchLingohubResourceRaw(project, resource, locale),\n config.retry,\n );\n const objectKey = buildTranslationObjectKey(\n project,\n resource.fileName,\n locale,\n );\n await store.uploadRaw(\n objectKey,\n raw,\n contentTypeForTranslationKey(objectKey),\n );\n const byteLength = Buffer.byteLength(raw, 'utf8');\n entries.push({\n project,\n resource: resource.resource,\n locale,\n itemCount: byteLength,\n objectKey,\n });\n\n console.log(\n ` + ${project} - ${locale}: ${byteLength} bytes -> ${objectKey}`,\n );\n }catch(err){\n const message = err instanceof Error ? err.message : String(err);\n errors.push({ project,locale, error: message });\n console.error(` x ${project} - ${locale}: ${message}`);\n }\n }\n }\n }\n\n return { timestamp, entries, errors };\n}\n"],"mappings":"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","names":["config","config","config","result","cfg","createClient","cfg","createClient","config","config","config"]}
|
|
1
|
+
{"version":3,"sources":["../src/server/adapters/azure/types.ts","../src/server/adapters/azure/client.ts","../src/server/adapters/azure/pipelines.ts","../src/server/adapters/azure/environment.ts","../src/server/config.ts","../src/server/restrictedCron.ts","../src/shared/content-refresh.ts","../src/server/content-refresh-notify.ts","../src/server/adapters/contentful-api-usage.ts","../src/server/adapters/contentful.ts","../src/server/sync/retry.ts","../src/server/adapters/sanity.ts","../src/server/adapters/index.ts","../src/server/adapters/lingohub.ts","../src/server/sync/engine.ts"],"sourcesContent":["import type { AzurePipelineEnvironment } from './environment';\n\nexport type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';\n\n/** Azure DevOps project name (e.g. `web-site`, `web-app`). */\nexport type AzureDevOpsProjectKey = 'web-site' | 'web-app' | 'web-invites';\n\nexport const AZURE_DEVOPS_PROJECT_KEYS: AzureDevOpsProjectKey[] = [\n 'web-site',\n 'web-app',\n 'web-invites',\n];\n\nexport interface AzurePipelineDefinition {\n id: number;\n refName: string;\n}\n\n/** Pipeline for this content-store instance (one id/ref per ADO project). */\nexport interface AzureProjectConfig {\n pipeline: AzurePipelineDefinition;\n}\n\n/** Credentials and per-project pipeline map for this deployment. */\nexport interface AzureDevOpsConfig {\n organization: string;\n /** Personal access token with pipeline run permissions. */\n pat: string;\n apiVersion: string;\n /** Default when Slack command omits the project argument. */\n defaultProject: AzureDevOpsProjectKey;\n /** Raw `ENVIRONMENT` on this instance (e.g. beta, live). */\n instanceEnvironment: string;\n /** Normalized for Azure (staging | production). */\n pipelineEnvironment: AzurePipelineEnvironment;\n projects: Record<AzureDevOpsProjectKey, AzureProjectConfig>;\n}\n\n/** Connection settings for a single ADO project (used by the HTTP client). */\nexport interface AzureDevOpsClientConfig {\n organization: string;\n project: string;\n pat: string;\n apiVersion: string;\n}\n\nexport interface AzureRequestOptions {\n method?: HttpMethod;\n /**\n * Path under the project, e.g. `/_apis/pipelines/281/runs`.\n * If it starts with `http://` or `https://`, it is used as the full URL.\n */\n path: string;\n query?: Record<string, string | number | boolean | undefined>;\n body?: unknown;\n /** Overrides the default api-version query parameter. */\n apiVersion?: string;\n headers?: Record<string, string>;\n}\n\nexport interface AzureApiErrorBody {\n message?: string;\n typeKey?: string;\n}\n\n/** Subset of the Azure Pipelines run object returned by the Runs API. */\nexport interface PipelineRunResponse {\n id: number;\n name?: string;\n state?: string;\n url?: string;\n createdDate?: string;\n _links?: {\n web?: { href?: string };\n pipeline?: { href?: string };\n };\n}\n\nexport interface TriggerPipelineRunResult {\n project: AzureDevOpsProjectKey;\n instanceEnvironment: string;\n pipelineEnvironment: AzurePipelineEnvironment;\n pipelineId: number;\n refName: string;\n run: PipelineRunResponse;\n}\n","import type {\n AzureApiErrorBody,\n AzureDevOpsClientConfig,\n AzureRequestOptions,\n HttpMethod,\n} from './types';\n\nexport class AzureDevOpsClient {\n private readonly baseUrl: string;\n\n constructor(private readonly config: AzureDevOpsClientConfig) {\n const org = config.organization.replace(/^\\/+|\\/+$/g, '');\n const project = encodeURIComponent(config.project);\n this.baseUrl = `https://dev.azure.com/${org}/${project}`;\n }\n\n /**\n * Call any Azure DevOps REST endpoint under the configured organization and project.\n */\n async request<T>(options: AzureRequestOptions): Promise<T> {\n const {\n method = 'GET',\n path,\n query = {},\n body,\n apiVersion = this.config.apiVersion,\n headers: extraHeaders = {},\n } = options;\n\n if (!this.config.pat) {\n throw new Error(\n 'Azure DevOps is not configured (set AZURE_DEVOPS_ACCESS_TOKEN)',\n );\n }\n\n const url = this.buildUrl(path, { ...query, 'api-version': apiVersion });\n const headers: Record<string, string> = {\n Authorization: this.basicAuthHeader(),\n Accept: 'application/json',\n ...extraHeaders,\n };\n\n const init: RequestInit = { method, headers };\n if (body !== undefined) {\n headers['Content-Type'] = 'application/json';\n init.body = JSON.stringify(body);\n }\n\n const response = await fetch(url, init);\n const text = await response.text();\n let parsed: unknown;\n if (text) {\n try {\n parsed = JSON.parse(text) as unknown;\n } catch {\n parsed = text;\n }\n }\n\n if (!response.ok) {\n const errBody = parsed as AzureApiErrorBody | undefined;\n const detail =\n errBody?.message ??\n (typeof parsed === 'string' ? parsed : JSON.stringify(parsed));\n throw new Error(\n `Azure DevOps ${method} ${path} failed (${response.status}): ${detail}`,\n );\n }\n\n return parsed as T;\n }\n\n private buildUrl(\n path: string,\n query: Record<string, string | number | boolean | undefined>,\n ): string {\n const base = /^https?:\\/\\//i.test(path) ? path : `${this.baseUrl}${path}`;\n const url = new URL(base);\n for (const [key, value] of Object.entries(query)) {\n if (value !== undefined) {\n url.searchParams.set(key, String(value));\n }\n }\n return url.toString();\n }\n\n private basicAuthHeader(): string {\n const encoded = Buffer.from(`:${this.config.pat}`).toString('base64');\n return `Basic ${encoded}`;\n }\n}\n\nexport function createAzureDevOpsClient(\n config: AzureDevOpsClientConfig,\n): AzureDevOpsClient {\n return new AzureDevOpsClient(config);\n}\n\nexport type { HttpMethod };\n","import { createAzureDevOpsClient } from './client';\nimport type {\n AzureDevOpsConfig,\n AzureDevOpsProjectKey,\n PipelineRunResponse,\n TriggerPipelineRunResult,\n} from './types';\nimport { AZURE_DEVOPS_PROJECT_KEYS } from './types';\n\nexport function isAzureDevOpsProjectKey(value: string): value is AzureDevOpsProjectKey {\n return (AZURE_DEVOPS_PROJECT_KEYS as string[]).includes(value);\n}\n\nfunction resolveProjectConfig(\n azureConfig: AzureDevOpsConfig,\n project: AzureDevOpsProjectKey,\n) {\n const projectConfig = azureConfig.projects[project];\n if (!projectConfig) {\n throw new Error(`Unknown Azure DevOps project \"${project}\"`);\n }\n return projectConfig;\n}\n\n/**\n * Queue a pipeline run for the given ADO project using this instance's configured pipeline id/ref.\n */\nexport async function triggerPipelineBuild(\n azureConfig: AzureDevOpsConfig,\n project: AzureDevOpsProjectKey,\n variables: Record<string, string> = {},\n): Promise<TriggerPipelineRunResult> {\n const { pipeline } = resolveProjectConfig(azureConfig, project);\n if (!pipeline?.id) {\n throw new Error(\n `No pipeline id configured for project \"${project}\" ` +\n `(instance ENVIRONMENT=${azureConfig.instanceEnvironment}, ` +\n `Azure=${azureConfig.pipelineEnvironment})`,\n );\n }\n\n const client = createAzureDevOpsClient({\n organization: azureConfig.organization,\n project,\n pat: azureConfig.pat,\n apiVersion: azureConfig.apiVersion,\n });\n\n const run = await client.request<PipelineRunResponse>({\n method: 'POST',\n path: `/_apis/pipelines/${pipeline.id}/runs`,\n body: {\n resources: {\n repositories: {\n self: {\n refName: pipeline.refName,\n },\n },\n },\n variables,\n },\n });\n\n return {\n project,\n instanceEnvironment: azureConfig.instanceEnvironment,\n pipelineEnvironment: azureConfig.pipelineEnvironment,\n pipelineId: pipeline.id,\n refName: pipeline.refName,\n run,\n };\n}\n\nexport function formatPipelineRunSummary(result: TriggerPipelineRunResult): string {\n const {\n run,\n project,\n instanceEnvironment,\n pipelineEnvironment,\n pipelineId,\n refName,\n } = result;\n const webUrl = run._links?.web?.href ?? run.url;\n const envLabel =\n instanceEnvironment === pipelineEnvironment\n ? `\\`${pipelineEnvironment}\\``\n : `\\`${instanceEnvironment}\\` → Azure \\`${pipelineEnvironment}\\``;\n const lines = [\n `Environment ${envLabel} — project \\`${project}\\`, pipeline id \\`${pipelineId}\\`, ref \\`${refName}\\``,\n `Run #${run.id}${run.state ? ` — state: \\`${run.state}\\`` : ''}`,\n ];\n if (webUrl) {\n lines.push(`<${webUrl}|Open run in Azure DevOps>`);\n }\n return lines.join('\\n');\n}\n","/**\n * Azure Pipelines only use staging / production naming.\n * Map AWS / shared `ENVIRONMENT` values (beta, live, …) at the adapter boundary.\n */\nexport type AzurePipelineEnvironment = 'staging' | 'production';\n\nconst PRODUCTION_ALIASES = new Set(['production', 'live', 'prod']);\nconst STAGING_ALIASES = new Set(['staging', 'beta', 'development', 'dev', 'local']);\n\n/**\n * Normalize any instance `ENVIRONMENT` value to Azure's staging | production.\n * Unknown values default to staging and log a warning (avoids queuing production by mistake).\n */\nexport function normalizeToAzureEnvironment(\n instanceEnvironment: string,\n): AzurePipelineEnvironment {\n const key = instanceEnvironment.trim().toLowerCase();\n if (!key) {\n console.warn(\n '[azure] Empty ENVIRONMENT; defaulting Azure pipeline environment to staging',\n );\n return 'staging';\n }\n if (PRODUCTION_ALIASES.has(key)) {\n return 'production';\n }\n if (STAGING_ALIASES.has(key)) {\n return 'staging';\n }\n console.warn(\n `[azure] Unrecognized ENVIRONMENT=\"${instanceEnvironment}\"; defaulting Azure pipeline environment to staging`,\n );\n return 'staging';\n}\n\n/** Git ref queued for the pipeline run (always derived from the Azure environment). */\nexport function pipelineRefForAzure(\n pipelineEnvironment: AzurePipelineEnvironment,\n): string {\n return `refs/heads/${pipelineEnvironment}`;\n}\n","import dotenv from 'dotenv';\nimport type { S3Config, CMSProvider } from '../shared/types';\nimport { SharedConfig, config as sharedConfig } from '../shared/config';\nimport type { AzureDevOpsConfig, AzureDevOpsProjectKey } from './adapters/azure';\nimport {\n AZURE_DEVOPS_PROJECT_KEYS,\n normalizeToAzureEnvironment,\n pipelineRefForAzure,\n} from './adapters/azure';\nimport type { AzurePipelineEnvironment } from './adapters/azure';\nimport type { ContentRefreshNotifyConfig } from './content-refresh-notify';\nimport { isContentRefreshEnabledForInstance } from './content-refresh-notify';\nimport { validateScheduleCronExpression } from './restrictedCron';\n\ndotenv.config({ path: '.env.local' });\ndotenv.config();\n\nexport type { CMSProvider, S3Config };\n\nexport interface ContentfulConfig {\n spaceId: string;\n accessToken: string;\n host: string;\n batchSize: number;\n /** Content types to sync. When empty, all content types in the space are synced. */\n contentTypes: string[];\n /** Max nesting depth when unwrapping resolved entries. Deeper references are dropped. */\n maxDepth: number;\n}\n\nexport interface SanityConfig {\n projectId: string;\n dataset: string;\n token: string;\n apiVersion: string;\n}\n\nexport interface LingohubConfig {\n authToken: string;\n workspace: string;\n}\n\nexport interface RetryConfig {\n maxRetries: number;\n baseDelayMs: number;\n maxDelayMs: number;\n}\n\nexport interface RestApiConfig {\n port: number;\n apiToken: string;\n}\n\n/** Background schedule (same behaviour as `content-store sync` in the CLI). */\nexport interface ScheduledCmsJobConfig {\n enabled: boolean;\n /** Two-field cron: minute then hour only. Example: `30 3` or a minute step with star in the hour field. Empty when disabled. */\n cronExpression: string;\n /** When true, run once when the server starts, then on the cron wall clock. */\n runOnStart: boolean;\n syncCms?: CMSProvider;\n syncTypes?: string[];\n}\n\nexport interface ScheduledTranslationJobConfig {\n enabled: boolean;\n cronExpression: string;\n runOnStart: boolean;\n syncProjects?: string[];\n}\n\nfunction readScheduleCronEnv(): {\n scheduleCron: string;\n cmsCronRaw: string;\n translationCronRaw: string;\n} {\n return {\n scheduleCron: (process.env.SCHEDULE_CRON ?? '').trim(),\n cmsCronRaw: (process.env.SCHEDULE_CMS_CRON ?? '').trim(),\n translationCronRaw: (process.env.SCHEDULE_TRANSLATION_CRON ?? '').trim(),\n };\n}\n\nfunction resolveScheduleCron(\n jobSpecific: string,\n globalCron: string,\n jobLabel: string,\n): string | null {\n const raw = jobSpecific || globalCron;\n if (!raw) return null;\n try {\n return validateScheduleCronExpression(raw);\n } catch (e) {\n const msg = e instanceof Error ? e.message : String(e);\n console.error(`[config] Invalid ${jobLabel} schedule cron: ${msg}`);\n return null;\n }\n}\n\nfunction parseScheduledCmsJobConfig(): ScheduledCmsJobConfig {\n const { scheduleCron, cmsCronRaw } = readScheduleCronEnv();\n const runOnStart =\n (process.env.SCHEDULE_RUN_ON_START ?? 'true').toLowerCase() !== 'false';\n const syncCms = (process.env.SCHEDULE_SYNC_CMS ?? '').trim() as CMSProvider;\n const rawTypes = process.env.SCHEDULE_SYNC_CONTENT_TYPES?.trim();\n const syncTypes = rawTypes\n ? rawTypes.split(',').map((s) => s.trim()).filter(Boolean)\n : undefined;\n\n const resolved = resolveScheduleCron(cmsCronRaw, scheduleCron, 'CMS');\n const cmsOk =\n !!syncCms && (syncCms === 'contentful' || syncCms === 'sanity');\n if (resolved !== null && !cmsOk) {\n console.warn(\n '[config] CMS schedule cron is set but SCHEDULE_SYNC_CMS is missing or not \"contentful\"|\"sanity\"; scheduled CMS sync is disabled.',\n );\n }\n const enabled = resolved !== null && cmsOk;\n\n return {\n enabled,\n cronExpression: resolved ?? '',\n runOnStart,\n syncCms: syncCms || undefined,\n syncTypes,\n };\n}\n\nfunction parseScheduledTranslationJobConfig(): ScheduledTranslationJobConfig {\n const { scheduleCron, translationCronRaw } = readScheduleCronEnv();\n const runOnStart =\n (process.env.SCHEDULE_RUN_ON_START ?? 'true').toLowerCase() !== 'false';\n const rawProjects = process.env.SCHEDULE_SYNC_TRANSLATION_PROJECTS?.trim();\n const syncProjects = rawProjects\n ? rawProjects.split(',').map((s) => s.trim()).filter(Boolean)\n : undefined;\n\n const resolved = resolveScheduleCron(\n translationCronRaw,\n scheduleCron,\n 'translation',\n );\n const enabled = resolved !== null;\n\n return {\n enabled,\n cronExpression: resolved ?? '',\n runOnStart,\n syncProjects,\n };\n}\n\nexport interface SlackConfig {\n enabled: boolean;\n botToken: string;\n signingSecret: string;\n /** Socket Mode app-level token (xapp-…). Required when enabled. */\n appToken: string;\n /** Channel ID or name for async notifications (e.g. sync results). */\n notifyChannel: string;\n /** Slash command name for CMS sync (default `/sync-content`). */\n cmdSyncContent: string;\n /** Slash command name for translation sync (default `/sync-translations`). */\n cmdSyncTranslations: string;\n /** Slash command to queue an Azure Pipelines run (default `/trigger-pipeline`). */\n cmdTriggerBuild: string;\n}\n\nfunction projectEnvSlug(project: AzureDevOpsProjectKey): string {\n return project.toUpperCase().replace(/-/g, '_');\n}\n\nfunction readProjectPipeline(\n project: AzureDevOpsProjectKey,\n pipelineEnvironment: AzurePipelineEnvironment,\n): { id: number; refName: string } {\n const slug = projectEnvSlug(project);\n const id = parseInt(process.env[`AZURE_${slug}_PIPELINE_ID`]?.trim() ?? '0', 10);\n return { id, refName: pipelineRefForAzure(pipelineEnvironment) };\n}\n\nfunction parseAzureDevOpsConfig(\n instanceEnvironment: string,\n): AzureDevOpsConfig & { enabled: boolean } {\n const pipelineEnvironment = normalizeToAzureEnvironment(instanceEnvironment);\n const pat = process.env.AZURE_DEVOPS_ACCESS_TOKEN?.trim() ?? '';\n\n const defaultProjectRaw = (\n process.env.AZURE_DEVOPS_DEFAULT_PROJECT ?? 'web-site'\n ).trim() as AzureDevOpsProjectKey;\n\n const defaultProject = (AZURE_DEVOPS_PROJECT_KEYS as string[]).includes(\n defaultProjectRaw,\n )\n ? defaultProjectRaw\n : 'web-site';\n\n const projects = Object.fromEntries(\n AZURE_DEVOPS_PROJECT_KEYS.map((project) => [\n project,\n { pipeline: readProjectPipeline(project, pipelineEnvironment) },\n ]),\n ) as AzureDevOpsConfig['projects'];\n\n return {\n enabled: pat.length > 0,\n organization: process.env.AZURE_DEVOPS_ORGANIZATION?.trim() || 'tripod-technology',\n pat,\n apiVersion: process.env.AZURE_DEVOPS_API_VERSION?.trim() || '7.1',\n defaultProject,\n instanceEnvironment,\n pipelineEnvironment,\n projects,\n };\n}\n\nfunction readContentRefreshUrl(project: AzureDevOpsProjectKey): string | undefined {\n const slug = projectEnvSlug(project);\n return process.env[`CONTENT_REFRESH_${slug}_URL`]?.trim() || undefined;\n}\n\nfunction parseContentRefreshConfig(\n instanceEnvironment: string,\n): ContentRefreshNotifyConfig {\n const targets = Object.fromEntries(\n AZURE_DEVOPS_PROJECT_KEYS.map((project) => [\n project,\n { url: readContentRefreshUrl(project) },\n ]),\n ) as ContentRefreshNotifyConfig['targets'];\n\n return {\n enabled: isContentRefreshEnabledForInstance(instanceEnvironment),\n basicAuth: process.env.CONTENT_REFRESH_BASIC_AUTH?.trim() ?? '',\n targets,\n };\n}\n\nexport interface ServerConfig {\n contentful: ContentfulConfig;\n sanity: SanityConfig;\n lingohub: LingohubConfig;\n retry: RetryConfig;\n api: RestApiConfig;\n scheduledCmsJob: ScheduledCmsJobConfig;\n scheduledTranslationJob: ScheduledTranslationJobConfig;\n azure: AzureDevOpsConfig & { enabled: boolean };\n contentRefresh: ContentRefreshNotifyConfig;\n slack: SlackConfig;\n}\n\nexport const config: ServerConfig & SharedConfig = {\n ...sharedConfig,\n contentful: {\n spaceId: process.env.CONTENTFUL_SPACE_ID ?? '',\n accessToken: process.env.CONTENTFUL_WEBSITE_TOKEN ?? '',\n host: process.env.CONTENTFUL_HOST ?? 'preview.contentful.com',\n batchSize: 1000,\n maxDepth: 4,\n contentTypes: [\n 'asset','page','longtailPage','customJson','banner','cookieBanner','downloadPage'\n // Add Contentful content type IDs here to limit sync scope.\n ],\n },\n\n sanity: {\n projectId: process.env.SANITY_PROJECT_ID ?? '',\n dataset: process.env.SANITY_DATASET ?? 'main',\n token: process.env.SANITY_API_TOKEN ?? '',\n apiVersion: '2024-01-01',\n },\n\n lingohub: {\n authToken : process.env.LINGOHUB_AUTH_TOKEN ?? '',\n workspace: process.env.LINGOHUB_WORKSPACE ?? '',\n },\n\n retry: {\n maxRetries: parseInt(process.env.RETRY_MAX_RETRIES ?? '5', 10),\n baseDelayMs: parseInt(process.env.RETRY_BASE_DELAY_MS ?? '1000', 10),\n maxDelayMs: parseInt(process.env.RETRY_MAX_DELAY_MS ?? '60000', 10),\n },\n\n api: {\n port: parseInt(process.env.PORT ?? '3030', 10),\n apiToken: process.env.CONTENT_STORE_API_TOKEN ?? '',\n },\n\n scheduledCmsJob: parseScheduledCmsJobConfig(),\n scheduledTranslationJob: parseScheduledTranslationJobConfig(),\n\n azure: parseAzureDevOpsConfig(sharedConfig.environment),\n\n contentRefresh: parseContentRefreshConfig(sharedConfig.environment),\n\n slack: {\n enabled: (process.env.SLACK_BOT_TOKEN ?? '').length > 0,\n botToken: process.env.SLACK_BOT_TOKEN ?? '',\n signingSecret: process.env.SLACK_SIGNING_SECRET ?? '',\n appToken: process.env.SLACK_APP_TOKEN ?? '',\n notifyChannel: process.env.SLACK_NOTIFY_CHANNEL ?? '',\n cmdSyncContent: process.env.SLACK_CMD_SYNC_CONTENT ?? '/sync-content',\n cmdSyncTranslations: process.env.SLACK_CMD_SYNC_TRANSLATIONS ?? '/sync-translations',\n cmdTriggerBuild:\n process.env.SLACK_CMD_TRIGGER_BUILD ?? '/trigger-build',\n },\n};\n","/**\n * Schedule expressions: exactly **two** whitespace-separated fields: minute, then hour\n * (same meaning as the first two fields of standard cron). No day-of-month, month, or\n * day-of-week — those are not accepted.\n *\n * Supported per field: `*`, `n`, `a-b`, steps (asterisk + slash + step), and comma lists.\n * Step must be >= 1.\n */\n\nconst MAX_SEARCH_MINUTES = 366 * 24 * 60;\n\nfunction tokenize(expr: string): string[] {\n return expr\n .trim()\n .split(/\\s+/)\n .map((s) => s.trim())\n .filter(Boolean);\n}\n\n/**\n * Validates that `expr` is exactly two cron fields (minute hour) and returns them\n * as a single normalized string `\"<minute> <hour>\"` for storage and logging.\n */\nexport function validateScheduleCronExpression(expr: string): string {\n const parts = tokenize(expr);\n if (parts.length !== 2) {\n throw new Error(\n `Schedule cron must be exactly two fields (minute hour), whitespace-separated; got ${parts.length} field(s): \"${expr}\"`,\n );\n }\n const minuteSpec = parts[0]!;\n const hourSpec = parts[1]!;\n parseField(minuteSpec, 0, 59, 'minute');\n parseField(hourSpec, 0, 23, 'hour');\n return `${minuteSpec} ${hourSpec}`;\n}\n\nfunction parseField(\n spec: string,\n lo: number,\n hi: number,\n fieldName: string,\n): (n: number) => boolean {\n const subs = spec.split(',').map((s) => s.trim()).filter(Boolean);\n if (subs.length === 0) {\n throw new Error(`Empty ${fieldName} field in cron`);\n }\n const preds = subs.map((sub) => parseSubfield(sub, lo, hi, fieldName));\n return (n: number) => preds.some((p) => p(n));\n}\n\nfunction parseSubfield(\n sub: string,\n lo: number,\n hi: number,\n fieldName: string,\n): (n: number) => boolean {\n if (sub === '*') {\n return () => true;\n }\n if (sub.startsWith('*/')) {\n const step = parseInt(sub.slice(2), 10);\n if (!Number.isFinite(step) || step < 1) {\n throw new Error(`Invalid step in ${fieldName} field: \"${sub}\"`);\n }\n return (n: number) => n >= lo && n <= hi && n % step === 0;\n }\n if (sub.includes('-')) {\n const [a, b] = sub.split('-').map((x) => parseInt(x.trim(), 10));\n if (!Number.isFinite(a) || !Number.isFinite(b)) {\n throw new Error(`Invalid range in ${fieldName} field: \"${sub}\"`);\n }\n if (a < lo || b > hi || a > b) {\n throw new Error(`Range out of bounds in ${fieldName} field: \"${sub}\"`);\n }\n return (n: number) => n >= a && n <= b;\n }\n const v = parseInt(sub, 10);\n if (!Number.isFinite(v) || v < lo || v > hi) {\n throw new Error(`Invalid value in ${fieldName} field: \"${sub}\"`);\n }\n return (n: number) => n === v;\n}\n\nfunction floorToMinuteStart(d: Date): Date {\n const t = new Date(d.getTime());\n t.setSeconds(0, 0);\n return t;\n}\n\nfunction addOneMinute(d: Date): Date {\n const t = new Date(d.getTime());\n t.setMinutes(t.getMinutes() + 1, 0, 0);\n return t;\n}\n\nfunction matchesAtMinuteStart(\n minutePred: (m: number) => boolean,\n hourPred: (h: number) => boolean,\n d: Date,\n): boolean {\n return minutePred(d.getMinutes()) && hourPred(d.getHours());\n}\n\n/**\n * Earliest minute boundary strictly after `after` that satisfies the two-field expression.\n */\nexport function nextCronFireAfter(expr: string, after: Date): Date {\n const parts = tokenize(expr);\n if (parts.length !== 2) {\n throw new Error(\n `nextCronFireAfter expects exactly two fields (minute hour); got ${parts.length}: \"${expr}\"`,\n );\n }\n const minutePred = parseField(parts[0]!, 0, 59, 'minute');\n const hourPred = parseField(parts[1]!, 0, 23, 'hour');\n\n let d = floorToMinuteStart(after);\n if (d.getTime() <= after.getTime()) {\n d = addOneMinute(d);\n }\n\n for (let i = 0; i < MAX_SEARCH_MINUTES; i++) {\n if (matchesAtMinuteStart(minutePred, hourPred, d)) {\n return d;\n }\n d = addOneMinute(d);\n }\n\n throw new Error(`No cron match within ${MAX_SEARCH_MINUTES} minutes for \"${expr}\"`);\n}\n","import { timingSafeEqual } from 'node:crypto';\nimport type { CMSProvider } from './types';\nimport type {\n FetchCmsBundlesOptions,\n FetchTranslationBundlesOptions,\n TranslationBundleInfo,\n} from './bundles';\n\nexport type ContentRefreshScope = 'cms' | 'translations' | 'all';\n\nexport interface ContentRefreshRequest {\n scope?: ContentRefreshScope;\n cms?: CMSProvider;\n content_types?: string[];\n projects?: string[];\n locales?: string[];\n}\n\n/** Defaults applied when the request omits fields (set by the host application). */\nexport interface ContentRefreshDefaults {\n scope?: ContentRefreshScope;\n cms?: CMSProvider;\n contentTypes?: string[];\n translationProjects?: string[];\n locales?: string[];\n}\n\nexport interface ContentRefreshError {\n step: string;\n message: string;\n}\n\nexport interface ContentRefreshResult {\n ok: boolean;\n scope: ContentRefreshScope;\n cmsFiles?: Record<string, string>;\n translationFiles?: TranslationBundleInfo;\n errors: ContentRefreshError[];\n durationMs: number;\n}\n\nexport interface ContentRefreshFetchers {\n fetchCmsBundles: (\n options: FetchCmsBundlesOptions,\n ) => Promise<Record<string, string>>;\n fetchTranslationBundles: (\n options: FetchTranslationBundlesOptions,\n ) => Promise<TranslationBundleInfo>;\n}\n\nexport function resolveContentRefreshScope(\n request: ContentRefreshRequest,\n defaults: ContentRefreshDefaults,\n): ContentRefreshScope {\n return request.scope ?? defaults.scope ?? 'cms';\n}\n\n/** Base64 of `username:password` (value only — prefix with `Basic ` in the header). */\nexport function encodeBasicAuthCredentials(\n username: string,\n password: string,\n): string {\n return Buffer.from(`${username}:${password}`, 'utf8').toString('base64');\n}\n\n/**\n * Validates `Authorization: Basic <base64>` against the expected credentials\n * (base64 of `user:pass`, same as staging site basic auth).\n */\nexport function assertContentRefreshBasicAuth(\n authorizationHeader: string | undefined,\n expectedCredentialsBase64: string,\n): void {\n if (!expectedCredentialsBase64) {\n throw new ContentRefreshAuthError(\n 'Content refresh basic auth is not configured (set CONTENT_REFRESH_BASIC_AUTH)',\n 500,\n );\n }\n if (!authorizationHeader?.startsWith('Basic ')) {\n throw new ContentRefreshAuthError(\n 'Missing or malformed Authorization header (expected Basic)',\n 401,\n );\n }\n const provided = authorizationHeader.slice(6).trim();\n const bufA = new TextEncoder().encode(provided);\n const bufB = new TextEncoder().encode(expectedCredentialsBase64);\n if (bufA.byteLength !== bufB.byteLength || !timingSafeEqual(bufA, bufB)) {\n throw new ContentRefreshAuthError('Invalid basic auth credentials', 403);\n }\n}\n\nexport class ContentRefreshAuthError extends Error {\n constructor(\n message: string,\n readonly statusCode: number,\n ) {\n super(message);\n this.name = 'ContentRefreshAuthError';\n }\n}\n\n/**\n * Pull the latest bundles from S3 into the host app's configured output directory.\n */\nexport async function executeContentRefresh(\n fetchers: ContentRefreshFetchers,\n request: ContentRefreshRequest,\n defaults: ContentRefreshDefaults = {},\n): Promise<ContentRefreshResult> {\n const started = Date.now();\n const scope = resolveContentRefreshScope(request, defaults);\n const errors: ContentRefreshError[] = [];\n let cmsFiles: Record<string, string> | undefined;\n let translationFiles: TranslationBundleInfo | undefined;\n\n if (scope === 'cms' || scope === 'all') {\n const cms = request.cms ?? defaults.cms ?? 'contentful';\n const contentTypes = request.content_types ?? defaults.contentTypes;\n if (!contentTypes?.length) {\n errors.push({\n step: 'cms',\n message: 'content_types (or handler defaults.contentTypes) is required for CMS refresh',\n });\n } else {\n try {\n cmsFiles = await fetchers.fetchCmsBundles({ cms, contentTypes });\n } catch (err) {\n errors.push({\n step: 'cms',\n message: err instanceof Error ? err.message : String(err),\n });\n }\n }\n }\n\n if (scope === 'translations' || scope === 'all') {\n const projects = request.projects ?? defaults.translationProjects;\n if (!projects?.length) {\n errors.push({\n step: 'translations',\n message:\n 'projects (or handler defaults.translationProjects) is required for translation refresh',\n });\n } else {\n const locales = request.locales ?? defaults.locales;\n try {\n const projectMap = Object.fromEntries(\n projects.map((p) => [p, [] as string[]]),\n );\n translationFiles = await fetchers.fetchTranslationBundles({\n projects: projectMap,\n locales,\n });\n } catch (err) {\n errors.push({\n step: 'translations',\n message: err instanceof Error ? err.message : String(err),\n });\n }\n }\n }\n\n return {\n ok: errors.length === 0,\n scope,\n cmsFiles,\n translationFiles,\n errors,\n durationMs: Date.now() - started,\n };\n}\n\nexport interface PostContentRefreshResponse {\n target: string;\n ok: boolean;\n status: number;\n body?: unknown;\n error?: string;\n}\n\n/**\n * Ask a remote application (web-site, web-app, …) to refresh its local content cache.\n */\nexport async function postContentRefresh(\n target: string,\n url: string,\n basicAuth: string,\n request: ContentRefreshRequest,\n): Promise<PostContentRefreshResponse> {\n try {\n const response = await fetch(url, {\n method: 'POST',\n headers: {\n Authorization: `Basic ${basicAuth}`,\n 'Content-Type': 'application/json',\n Accept: 'application/json',\n },\n body: JSON.stringify(request),\n });\n\n const text = await response.text();\n let body: unknown;\n if (text) {\n try {\n body = JSON.parse(text) as unknown;\n } catch {\n body = text;\n }\n }\n\n return {\n target,\n ok: response.ok,\n status: response.status,\n body,\n error: response.ok\n ? undefined\n : typeof body === 'object' && body !== null && 'error' in body\n ? String((body as { error: unknown }).error)\n : `HTTP ${response.status}`,\n };\n } catch (err) {\n return {\n target,\n ok: false,\n status: 0,\n error: err instanceof Error ? err.message : String(err),\n };\n }\n}\n","import type { AzureDevOpsProjectKey } from './adapters/azure';\nimport { AZURE_DEVOPS_PROJECT_KEYS } from './adapters/azure';\nimport { normalizeToAzureEnvironment } from './adapters/azure';\nimport { config, type CMSProvider } from './config';\nimport type { CmsSyncResult } from './sync/engine';\nimport {\n postContentRefresh,\n type ContentRefreshRequest,\n type PostContentRefreshResponse,\n} from '../shared/content-refresh';\n\nexport interface ContentRefreshTargetConfig {\n url?: string;\n}\n\nexport interface ContentRefreshNotifyConfig {\n /** When false, never call client refresh URLs (e.g. production uses pipeline builds). */\n enabled: boolean;\n /** Base64 of `username:password` (no `Basic ` prefix). */\n basicAuth: string;\n targets: Record<AzureDevOpsProjectKey, ContentRefreshTargetConfig>;\n}\n\nexport function isContentRefreshEnabledForInstance(\n instanceEnvironment: string,\n): boolean {\n return normalizeToAzureEnvironment(instanceEnvironment) === 'staging';\n}\n\nexport async function notifyContentRefreshTargets(\n notifyConfig: ContentRefreshNotifyConfig,\n request: ContentRefreshRequest,\n projects?: AzureDevOpsProjectKey[],\n): Promise<PostContentRefreshResponse[]> {\n if (!notifyConfig.enabled || !notifyConfig.basicAuth) {\n return [];\n }\n\n const keys =\n projects ??\n (AZURE_DEVOPS_PROJECT_KEYS.filter(\n (p) => notifyConfig.targets[p]?.url,\n ) as AzureDevOpsProjectKey[]);\n\n const results: PostContentRefreshResponse[] = [];\n\n for (const project of keys) {\n const url = notifyConfig.targets[project]?.url;\n if (!url) {\n continue;\n }\n const result = await postContentRefresh(\n project,\n url,\n notifyConfig.basicAuth,\n request,\n );\n results.push(result);\n if (result.ok) {\n console.log(\n `[content-refresh] Notified ${project} (${result.status})`,\n );\n } else {\n console.error(\n `[content-refresh] Failed to notify ${project}: ${result.error ?? result.status}`,\n );\n }\n }\n\n return results;\n}\n\n/**\n * After a successful CMS sync (Slack, HTTP API, or cron), POST to configured client refresh URLs.\n */\nexport async function notifyClientsAfterCmsSync(\n result: CmsSyncResult,\n requestedContentTypes?: string[],\n projects?: AzureDevOpsProjectKey[],\n): Promise<PostContentRefreshResponse[]> {\n const { contentRefresh } = config;\n\n if (!contentRefresh.enabled) {\n console.log(\n '[content-refresh] Skipped after CMS sync (disabled on this instance; staging/beta only)',\n );\n return [];\n }\n if (!contentRefresh.basicAuth) {\n console.warn(\n '[content-refresh] Skipped after CMS sync (set CONTENT_REFRESH_BASIC_AUTH)',\n );\n return [];\n }\n if (result.errors.length > 0) {\n console.log('[content-refresh] Skipped after CMS sync (sync had errors)');\n return [];\n }\n\n const types = requestedContentTypes?.length\n ? requestedContentTypes\n : result.entries.map((e) => e.contentType);\n if (types.length === 0) {\n console.warn('[content-refresh] Skipped after CMS sync (no content types)');\n return [];\n }\n\n const targets = (\n projects ??\n (AZURE_DEVOPS_PROJECT_KEYS.filter(\n (p) => contentRefresh.targets[p]?.url,\n ) as AzureDevOpsProjectKey[])\n );\n\n if (targets.length === 0) {\n console.warn(\n '[content-refresh] Skipped after CMS sync (no CONTENT_REFRESH_*_URL configured)',\n );\n return [];\n }\n\n console.log(\n `[content-refresh] Notifying after CMS sync (${result.cms}): ${targets.join(', ')}`,\n );\n\n return notifyContentRefreshTargets(\n contentRefresh,\n {\n scope: 'cms',\n cms: result.cms as CMSProvider,\n content_types: types,\n },\n projects,\n );\n}\n","/** Per-API request counts for a single Contentful sync run. */\nexport interface ContentfulApiUsage {\n cda: number;\n cma: number;\n cpa: number;\n}\n\nexport type ContentfulApiKind = keyof ContentfulApiUsage;\n\nexport function emptyContentfulApiUsage(): ContentfulApiUsage {\n return { cda: 0, cma: 0, cpa: 0 };\n}\n\n/** Map Contentful client `host` to CDA / CPA / CMA (one kind per adapter instance). */\nexport function contentfulApiKindFromHost(host: string): ContentfulApiKind {\n const h = host.toLowerCase();\n if (h.includes('preview')) {\n return 'cpa';\n }\n if (h.includes('api.contentful') || h === 'api.contentful.com') {\n return 'cma';\n }\n return 'cda';\n}\n\nexport function totalContentfulApiCalls(usage: ContentfulApiUsage): number {\n return usage.cda + usage.cma + usage.cpa;\n}\n\n/** Slack / log lines for Contentful API usage (omits zero counts except total). */\nexport function summariseContentfulApiUsage(usage: ContentfulApiUsage): string[] {\n const lines: string[] = [];\n if (usage.cda > 0) {\n lines.push(`CDA requests: ${usage.cda}`);\n }\n if (usage.cpa > 0) {\n lines.push(`CPA requests: ${usage.cpa}`);\n }\n if (usage.cma > 0) {\n lines.push(`CMA requests: ${usage.cma}`);\n }\n lines.push(`Total contentful API calls: ${totalContentfulApiCalls(usage)}`);\n return lines;\n}\n\nexport class ContentfulApiUsageTracker {\n private readonly kind: ContentfulApiKind;\n private readonly counts = emptyContentfulApiUsage();\n\n constructor(host: string) {\n this.kind = contentfulApiKindFromHost(host);\n }\n\n recordCall(): void {\n this.counts[this.kind] += 1;\n }\n\n snapshot(): ContentfulApiUsage {\n return { ...this.counts };\n }\n}\n","import {\n createClient,\n type ContentfulClientApi,\n type ContentTypeCollection,\n type AssetCollection,\n} from 'contentful';\nimport type { ContentfulConfig, RetryConfig } from '../config';\nimport type { CMSAdapter, FetchResult } from './types';\nimport {\n type ContentfulApiUsage,\n ContentfulApiUsageTracker,\n} from './contentful-api-usage';\nimport { withRetry } from '../sync/retry';\n\ntype CfCollection = {\n items: CfItem[],\n total: number\n}\n\ntype CfItem = {\n metadata:{\n tags: string[],\n concepts: string[]\n }\n sys:{\n type: string,\n id: string\n space: {\n sys: {\n type: string\n linkType: string\n id: string\n }\n },\n environment: {\n sys: {\n id: string\n type: 'Link',\n linkType: 'Environment'\n }\n },\n contentType: {\n sys: {\n type: 'Link',\n linkType: 'ContentType',\n id: string\n }\n },\n createdBy: {\n sys: {\n type: 'Link',\n linkType: 'User',\n id: string,\n }\n },\n updatedBy: {\n sys: {\n type: 'Link',\n linkType: 'User',\n id: string\n }\n },\n 'revision': number,\n 'createdAt': string,\n 'updatedAt': string,\n 'publishedVersion': string\n },\n fields:{\n [key:string]: unknown\n }\n}\n\n\n/**\n * Recursively unwraps Contentful's { metadata, sys, fields } envelope.\n * `depth` tracks how many entry/asset envelopes deep we are — anything\n * beyond `maxDepth` is dropped to avoid blowing the call stack on\n * circular or extremely deep reference chains.\n *\n * `path` holds objects on the current recursion branch only. That way a\n * shared reference (e.g. the same asset on `image` and `mobileImage`) is\n * unwrapped for each sibling; only true cycles (an object recurring as a\n * descendant of itself) yield `undefined`.\n */\nfunction stripEnvelope(\n value: unknown,\n maxDepth: number,\n depth = 0,\n path = new WeakSet<object>(),\n): unknown {\n if (value === null || typeof value !== 'object') return value;\n\n const obj = value as CfItem;\n\n if (path.has(obj)) return undefined;\n path.add(obj);\n\n try {\n if (Array.isArray(value)) {\n return value.map((item) => stripEnvelope(item, maxDepth, depth, path));\n }\n\n const isEnvelope = 'sys' in obj && 'fields' in obj && typeof obj.fields === 'object';\n\n if (isEnvelope) {\n if (depth >= maxDepth) return undefined;\n\n const fields = obj.fields as Record<string, unknown>;\n const result: Record<string, unknown> = {};\n for (const [k, v] of Object.entries(fields)) {\n result[k] = stripEnvelope(v, maxDepth, depth + 1, path);\n }\n\n const sys = obj.sys;\n if (sys) {\n const existingMeta = result.meta;\n const metaBase =\n typeof existingMeta === 'object' &&\n existingMeta !== null &&\n !Array.isArray(existingMeta)\n ? (existingMeta as Record<string, unknown>)\n : {};\n const _contentType = sys.contentType ? sys.contentType.sys.id : 'Asset'\n result.meta = { ...metaBase, _id: sys.id, _contentType, _updatedAt: sys.updatedAt };\n }\n\n return result;\n }\n\n const result: Record<string, unknown> = {};\n for (const [k, v] of Object.entries(obj)) {\n result[k] = stripEnvelope(v, maxDepth, depth, path);\n }\n return result;\n } finally {\n path.delete(obj);\n }\n}\n\nexport class ContentfulAdapter implements CMSAdapter {\n readonly name = 'contentful';\n private client: ContentfulClientApi<undefined>;\n private batchSize: number;\n private maxDepth: number;\n private allowedTypes: string[];\n private retryConfig: RetryConfig;\n private readonly apiUsage: ContentfulApiUsageTracker;\n\n constructor(cfg: ContentfulConfig, retryConfig: RetryConfig) {\n this.client = createClient({\n space: cfg.spaceId,\n accessToken: cfg.accessToken,\n host: cfg.host,\n });\n this.batchSize = cfg.batchSize;\n this.maxDepth = cfg.maxDepth;\n this.allowedTypes = cfg.contentTypes;\n this.retryConfig = retryConfig;\n this.apiUsage = new ContentfulApiUsageTracker(cfg.host);\n }\n\n getContentfulApiUsage(): ContentfulApiUsage {\n return this.apiUsage.snapshot();\n }\n\n async getContentTypes(): Promise<string[]> {\n const response = await withRetry<ContentTypeCollection>(\n () => {\n this.apiUsage.recordCall();\n return this.client.getContentTypes();\n },\n this.retryConfig,\n );\n\n const allTypes = response.items.map((ct) => ct.sys.id);\n\n if (this.allowedTypes.length > 0) {\n return allTypes.filter((t) => this.allowedTypes.includes(t));\n }\n return allTypes;\n }\n\n /**\n * Fetches every entry for a content type using batched pagination.\n * Contentful caps `getEntries` at 1 000 items per call, so we page through\n * with `skip` until all items are collected.\n *\n * The reserved content type `\"asset\"` fetches from `getAssets()` instead.\n */\n async fetchAll(contentType: string, includeLevels: 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 = 4): Promise<FetchResult> {\n if (contentType === 'asset') {\n return this.fetchAllAssets();\n }\n\n const allItems: unknown[] = [];\n let skip = 0;\n let total = 0;\n\n do {\n const payload = {\n content_type: contentType,\n limit: this.batchSize,\n skip,\n include: includeLevels,\n };\n this.apiUsage.recordCall();\n const response = await this.client.getEntries(payload) as unknown as CfCollection;\n total = response.total;\n allItems.push(...response.items);\n skip += response.items.length;\n\n if (total > this.batchSize) {\n console.log(\n ` [contentful] ${contentType}: fetched ${allItems.length}/${total}`,\n );\n }\n } while (skip < total);\n\n return {\n contentType,\n items: allItems.map((item) => stripEnvelope(item, this.maxDepth)),\n total,\n };\n }\n\n private async fetchAllAssets(): Promise<FetchResult> {\n const allItems: unknown[] = [];\n let skip = 0;\n let total = 0;\n\n do {\n const response = await withRetry<AssetCollection>(\n () => {\n this.apiUsage.recordCall();\n return this.client.getAssets({ limit: this.batchSize, skip });\n },\n this.retryConfig,\n );\n total = response.total;\n allItems.push(...response.items);\n skip += response.items.length;\n\n if (total > this.batchSize) {\n console.log(\n ` [contentful] asset: fetched ${allItems.length}/${total}`,\n );\n }\n } while (skip < total);\n\n return {\n contentType: 'asset',\n items: allItems.map((item) => stripEnvelope(item, this.maxDepth)),\n total,\n };\n }\n}\n","import type { RetryConfig } from '../config';\n\n/**\n * Inspects an error to determine if it represents an API rate-limit (HTTP 429).\n * Returns the suggested wait time in ms when available, otherwise `0` to signal\n * that the caller should fall back to computed backoff. Returns `null` when the\n * error is *not* a rate-limit error.\n */\nfunction rateLimitDelayMs(err: unknown): number | null {\n const e = err as Record<string, unknown>;\n\n if (e?.status === 429 || e?.statusCode === 429) {\n const reset = (e?.headers as Record<string, string> | undefined)?.[\n 'x-contentful-ratelimit-reset'\n ];\n return reset ? parseFloat(reset) * 1000 : 0;\n }\n\n const resp = e?.response as Record<string, unknown> | undefined;\n if (resp?.status === 429) {\n const headers = resp?.headers as Record<string, string> | undefined;\n const reset = headers?.['x-contentful-ratelimit-reset'];\n return reset ? parseFloat(reset) * 1000 : 0;\n }\n\n return null;\n}\n\nfunction computeDelay(\n attempt: number,\n baseDelayMs: number,\n maxDelayMs: number,\n): number {\n const exponential = baseDelayMs * Math.pow(2, attempt);\n const jitter = Math.random() * baseDelayMs;\n return Math.min(exponential + jitter, maxDelayMs);\n}\n\n/**\n * Executes `fn` with automatic retry + exponential backoff.\n * Rate-limit (429) responses are handled specially: if the API provides a\n * Retry-After / reset header, that value is respected instead of computed backoff.\n */\nexport async function withRetry<T>(\n fn: () => Promise<T>,\n { maxRetries, baseDelayMs, maxDelayMs }: RetryConfig,\n): Promise<T> {\n for (let attempt = 0; attempt <= maxRetries; attempt++) {\n try {\n return await fn();\n } catch (err) {\n if (attempt === maxRetries) throw err;\n\n const rlDelay = rateLimitDelayMs(err);\n let delay: number;\n\n if (rlDelay !== null) {\n delay =\n rlDelay > 0 ? rlDelay : computeDelay(attempt, baseDelayMs, maxDelayMs);\n console.warn(\n ` Rate limited (attempt ${attempt + 1}/${maxRetries}). ` +\n `Waiting ${Math.round(delay)}ms…`,\n );\n } else {\n delay = computeDelay(attempt, baseDelayMs, maxDelayMs);\n console.warn(\n ` Request failed (attempt ${attempt + 1}/${maxRetries}): ` +\n `${(err as Error).message}. Retrying in ${Math.round(delay)}ms…`,\n );\n }\n\n await new Promise((resolve) => setTimeout(resolve, delay));\n }\n }\n\n throw new Error('withRetry: unreachable');\n}\n","import { createClient, type SanityClient } from '@sanity/client';\nimport type { SanityConfig, RetryConfig } from '../config';\nimport type { CMSAdapter, FetchResult } from './types';\nimport { withRetry } from '../sync/retry';\n\nexport class SanityAdapter implements CMSAdapter {\n readonly name = 'sanity';\n private client: SanityClient;\n private retryConfig: RetryConfig;\n\n constructor(cfg: SanityConfig, retryConfig: RetryConfig) {\n this.client = createClient({\n projectId: cfg.projectId,\n dataset: cfg.dataset,\n token: cfg.token,\n apiVersion: cfg.apiVersion,\n useCdn: false,\n });\n this.retryConfig = retryConfig;\n }\n\n async getContentTypes(): Promise<string[]> {\n const types: string[] = await withRetry(\n () => this.client.fetch('array::unique(*[]._type)'),\n this.retryConfig,\n );\n return types.filter(\n (t) => !t.startsWith('system.') && !t.startsWith('sanity.'),\n );\n }\n\n async fetchAll(contentType: string): Promise<FetchResult> {\n const items: unknown[] = await withRetry(\n () => this.client.fetch('*[_type == $type]', { type: contentType }),\n this.retryConfig,\n );\n\n console.log(` [sanity] ${contentType}: fetched ${items.length} items`);\n return { contentType, items, total: items.length };\n }\n}\n","import { config, type CMSProvider } from '../config';\nimport type { CMSAdapter } from './types';\nimport { ContentfulAdapter } from './contentful';\nimport { SanityAdapter } from './sanity';\n\nexport function createAdapter(cms: CMSProvider): CMSAdapter {\n switch (cms) {\n case 'contentful':\n return new ContentfulAdapter(config.contentful, config.retry);\n case 'sanity':\n return new SanityAdapter(config.sanity, config.retry);\n default:\n throw new Error(`Unknown CMS provider: ${cms as string}`);\n }\n}\n\nexport type { CMSAdapter, FetchResult } from './types';\n","import { config } from '../config';\nimport type { LingohubResource } from '../../shared/lingohub';\n\nconst cfg = config.lingohub;\nconst apiUrl = 'https://api.lingohub.com/v1/' + cfg.workspace + '/projects/';\n\n/**\n * Downloads the raw Lingohub resource body (exact bytes as UTF-8 text).\n * Sync uploads this unmodified to S3; conversion happens at fetch time.\n */\nexport async function fetchLingohubResourceRaw(\n project: string,\n resource: LingohubResource,\n locale: string,\n): Promise<string> {\n const urlForResourceLocalised =\n `${apiUrl}${project}/resources/${resource.fileName}?auth_token=${cfg.authToken}`.replace(\n '[locale]',\n locale,\n );\n const res = await fetch(urlForResourceLocalised, { method: 'GET' });\n if (!res.ok) {\n throw new Error(`Failed to fetch resource \\`${resource.fileName}\\` (${locale}): ${res.status} - ${res.statusText}`);\n }\n return await res.text();\n}\n","import type { ContentfulApiUsage } from '../adapters/contentful-api-usage';\nimport { summariseContentfulApiUsage } from '../adapters/contentful-api-usage';\nimport type { CMSProvider } from '../config';\nimport { config } from '../config';\nimport { createAdapter } from '../adapters';\nimport {buildCmsObjectKey, buildTranslationObjectKey, ContentStore} from '../../shared/s3';\nimport { allProjects, defaultLocales } from '../../shared/lingohub';\nimport { fetchLingohubResourceRaw } from '../adapters/lingohub';\nimport { contentTypeForTranslationKey } from '../../shared/translationResource';\nimport { withRetry } from './retry';\n\nexport interface CmsSyncResultEntry {\n contentType: string;\n itemCount: number;\n objectKey: string;\n}\n\nexport interface CmsSyncResult {\n cms: CMSProvider;\n timestamp: number;\n entries: CmsSyncResultEntry[];\n errors: Array<{ contentType: string; error: string }>;\n /** Present when `cms` is `contentful`; counts each Contentful SDK HTTP request in this run. */\n contentfulApiUsage?: ContentfulApiUsage;\n}\n\nexport interface TranslationSyncResultEntry {\n project: string;\n resource: string;\n locale: string;\n /** UTF-8 byte size of the raw Lingohub file uploaded to S3. */\n itemCount: number;\n objectKey: string;\n}\n\nexport interface TranslationSyncResult {\n timestamp: number;\n entries: TranslationSyncResultEntry[];\n errors: Array<{ locale: string; project: string, error: string }>;\n}\n\nexport function summariseCmsEntries(result: CmsSyncResult): string[] {\n return result.entries.map(\n (e) => `• \\`${e.contentType}\\` — ${e.itemCount} items`,\n );\n}\n\nexport function summariseCmsErrors(result: CmsSyncResult): string[] {\n return result.errors.map(\n (e) => `• \\`${e.contentType}\\` — ${e.error}`,\n );\n}\n\n/** Slack-style lines for Contentful CDA/CPA/CMA request totals (empty when not Contentful). */\nexport function summariseCmsContentfulApiUsage(result: CmsSyncResult): string[] {\n if (!result.contentfulApiUsage) {\n return [];\n }\n return summariseContentfulApiUsage(result.contentfulApiUsage);\n}\n\n/** Group translation sync entries into one summary line per project/resource. */\nexport function summariseTranslationEntries(entries: TranslationSyncResultEntry[]): string[] {\n const grouped = new Map<string, { locales: number; bytes: number }>();\n for (const e of entries) {\n const key = `${e.project} / ${e.resource}`;\n const existing = grouped.get(key);\n if (existing) {\n existing.locales += 1;\n existing.bytes += e.itemCount;\n } else {\n grouped.set(key, { locales: 1, bytes: e.itemCount });\n }\n }\n return [...grouped.entries()].map(\n ([key, { locales, bytes }]) => `• \\`${key}\\` / [${locales} locale${locales === 1 ? '' : 's'}] — ${bytes} bytes`,\n );\n}\n\nexport async function runSync(cms: CMSProvider, contentTypes?: string[], includeLevels?: number){\n await syncCmsContent(cms, contentTypes, includeLevels )\n}\n\nexport async function syncCmsContent(\n cms: CMSProvider,\n contentTypes?: string[],\n includeLevels?: number\n): Promise<CmsSyncResult> {\n const adapter = createAdapter(cms);\n const store = new ContentStore(config.s3);\n const timestamp = Math.floor(Date.now() / 1000);\n\n console.log(`\\nStarting sync from ${cms} at ${new Date(timestamp * 1000).toISOString()}`);\n\n const typesToSync =\n contentTypes && contentTypes.length > 0\n ? contentTypes\n : await adapter.getContentTypes();\n\n console.log(`Content types to sync: ${typesToSync.join(', ')}\\n`);\n\n const entries: CmsSyncResultEntry[] = [];\n const errors: Array<{ contentType: string; error: string }> = [];\n\n for (const contentType of typesToSync) {\n try {\n const result = await adapter.fetchAll(contentType, includeLevels);\n const objectKey = buildCmsObjectKey(cms, contentType);\n await store.upload(objectKey, result.items);\n\n entries.push({\n contentType,\n itemCount: result.total,\n objectKey,\n });\n\n console.log(\n ` + ${contentType}: ${result.total} items -> ${objectKey}`,\n );\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n errors.push({ contentType, error: message });\n console.error(` x ${contentType}: ${message}`);\n }\n }\n\n const contentfulApiUsage = adapter.getContentfulApiUsage?.();\n\n if (contentfulApiUsage) {\n console.log(\n `\\nContentful API calls: ${summariseContentfulApiUsage(contentfulApiUsage).join(', ')}`,\n );\n }\n\n console.log(\n `\\nSync complete: ${entries.length} succeeded, ${errors.length} failed\\n`,\n );\n\n return { cms, timestamp, entries, errors, contentfulApiUsage };\n}\n\nexport async function syncTranslations(projects?: string[], locales?:string[]):Promise<TranslationSyncResult> {\n\n const store = new ContentStore(config.s3);\n const entries: TranslationSyncResultEntry[] = [];\n const errors: Array<{ project: string; locale: string, error: string }> = [];\n const timestamp = Math.floor(Date.now() / 1000);\n if(!locales){\n locales = defaultLocales;\n }\n if(!projects){\n projects = Object.keys(allProjects);\n }\n\n for(const project of projects) {\n const resources = allProjects[project];\n if(!resources){\n console.error(`No resources found for ${project}`);\n continue;\n }\n for(const resource of resources) {\n const resourceLocales = resource.locales\n ? locales.filter((l) => resource.locales!.includes(l))\n : locales;\n for(const loc of resourceLocales){\n const locale = (resource.localeMapping && resource.localeMapping[loc]) ? resource.localeMapping[loc] : loc;\n try {\n const raw = await withRetry(\n () => fetchLingohubResourceRaw(project, resource, locale),\n config.retry,\n );\n const objectKey = buildTranslationObjectKey(\n project,\n resource.fileName,\n locale,\n );\n await store.uploadRaw(\n objectKey,\n raw,\n contentTypeForTranslationKey(objectKey),\n );\n const byteLength = Buffer.byteLength(raw, 'utf8');\n entries.push({\n project,\n resource: resource.resource,\n locale,\n itemCount: byteLength,\n objectKey,\n });\n\n console.log(\n ` + ${project} - ${locale}: ${byteLength} bytes -> ${objectKey}`,\n );\n }catch(err){\n const message = err instanceof Error ? err.message : String(err);\n errors.push({ project,locale, error: message });\n console.error(` x ${project} - ${locale}: ${message}`);\n }\n }\n }\n }\n\n return { timestamp, entries, errors };\n}\n"],"mappings":"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","names":["config","config","config","result","cfg","createClient","cfg","createClient","config","config","config"]}
|
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
parseTranslationResourceRaw,
|
|
11
11
|
toFlatStringMap,
|
|
12
12
|
translationJsonOutputPath
|
|
13
|
-
} from "./chunk-
|
|
13
|
+
} from "./chunk-K4RASQFK.js";
|
|
14
14
|
import {
|
|
15
15
|
allProjects,
|
|
16
16
|
defaultLocales
|
|
@@ -159,7 +159,8 @@ async function fetchTranslationBundles(store, outputDir, options) {
|
|
|
159
159
|
if (!resources.length) return;
|
|
160
160
|
const resourceTasks = [];
|
|
161
161
|
for (const resource of resources) {
|
|
162
|
-
|
|
162
|
+
const resourceLocales = resource.locales ? localesToSync.filter((l) => resource.locales.includes(l)) : localesToSync;
|
|
163
|
+
for (const loc of resourceLocales) {
|
|
163
164
|
const locale = resource.localeMapping && resource.localeMapping[loc] ? resource.localeMapping[loc] : loc;
|
|
164
165
|
const objectKey = buildTranslationObjectKey(
|
|
165
166
|
project,
|
|
@@ -198,13 +199,16 @@ async function fetchMergedTranslationBundles(store, outputDir, options) {
|
|
|
198
199
|
await fs.mkdir(outputDir, { recursive: true });
|
|
199
200
|
const localesToSync = locales && locales.length ? locales : defaultLocales;
|
|
200
201
|
const tasks = [];
|
|
202
|
+
const allLocales = /* @__PURE__ */ new Set();
|
|
201
203
|
for (const [project, resourceFilter] of Object.entries(projects)) {
|
|
202
204
|
const allResources = allProjects[project];
|
|
203
205
|
if (!allResources?.length) continue;
|
|
204
206
|
const resources = filterResources(allResources, resourceFilter);
|
|
205
207
|
if (!resources.length) continue;
|
|
206
208
|
for (const resource of resources) {
|
|
207
|
-
|
|
209
|
+
const resourceLocales = resource.locales ? localesToSync.filter((l) => resource.locales.includes(l)) : localesToSync;
|
|
210
|
+
for (const loc of resourceLocales) {
|
|
211
|
+
allLocales.add(loc);
|
|
208
212
|
const mappedLocale = resource.localeMapping && resource.localeMapping[loc] ? resource.localeMapping[loc] : loc;
|
|
209
213
|
const objectKey = buildTranslationObjectKey(
|
|
210
214
|
project,
|
|
@@ -224,14 +228,14 @@ async function fetchMergedTranslationBundles(store, outputDir, options) {
|
|
|
224
228
|
})
|
|
225
229
|
);
|
|
226
230
|
const merged = {};
|
|
227
|
-
for (const loc of
|
|
231
|
+
for (const loc of allLocales) {
|
|
228
232
|
merged[loc] = {};
|
|
229
233
|
}
|
|
230
234
|
for (const { catalogLocale, stringMap } of results) {
|
|
231
235
|
Object.assign(merged[catalogLocale], stringMap);
|
|
232
236
|
}
|
|
233
237
|
const out = {};
|
|
234
|
-
for (const loc of
|
|
238
|
+
for (const loc of allLocales) {
|
|
235
239
|
const filePath = path.resolve(outputDir, `${loc}.json`);
|
|
236
240
|
const output = nested ? nestKeys(merged[loc]) : merged[loc];
|
|
237
241
|
await fs.writeFile(
|
|
@@ -286,4 +290,4 @@ export {
|
|
|
286
290
|
fetchMergedTranslationBundles,
|
|
287
291
|
queryCmsBundle
|
|
288
292
|
};
|
|
289
|
-
//# sourceMappingURL=chunk-
|
|
293
|
+
//# sourceMappingURL=chunk-ZXSGKKXJ.js.map
|