@infuro/cms-core 1.0.29 → 1.0.30
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/admin.cjs +1711 -1133
- package/dist/admin.cjs.map +1 -1
- package/dist/admin.js +1712 -1123
- package/dist/admin.js.map +1 -1
- package/dist/api.cjs +174 -19
- package/dist/api.cjs.map +1 -1
- package/dist/api.js +174 -19
- package/dist/api.js.map +1 -1
- package/dist/cli.cjs +5 -1
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +5 -1
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +175 -19
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +175 -19
- package/dist/index.js.map +1 -1
- package/package.json +8 -1
package/dist/api.cjs
CHANGED
|
@@ -7963,6 +7963,13 @@ var import_node_path = __toESM(require("path"), 1);
|
|
|
7963
7963
|
|
|
7964
7964
|
// src/plugins/social-media/linkedin-client.ts
|
|
7965
7965
|
var RESTLI = "2.0.0";
|
|
7966
|
+
function linkedInHeaders(accessToken, json = false) {
|
|
7967
|
+
return {
|
|
7968
|
+
Authorization: `Bearer ${accessToken}`,
|
|
7969
|
+
"X-Restli-Protocol-Version": RESTLI,
|
|
7970
|
+
...json ? { "Content-Type": "application/json" } : {}
|
|
7971
|
+
};
|
|
7972
|
+
}
|
|
7966
7973
|
async function linkedInGetUserinfo(accessToken) {
|
|
7967
7974
|
const r = await fetch("https://api.linkedin.com/v2/userinfo", {
|
|
7968
7975
|
headers: { Authorization: `Bearer ${accessToken}` }
|
|
@@ -7977,20 +7984,116 @@ async function linkedInGetUserinfo(accessToken) {
|
|
|
7977
7984
|
throw new Error("LinkedIn userinfo: invalid JSON");
|
|
7978
7985
|
}
|
|
7979
7986
|
}
|
|
7987
|
+
async function linkedInFetchOrganizationAcls(accessToken) {
|
|
7988
|
+
const r = await fetch("https://api.linkedin.com/v2/organizationAcls?q=roleAssignee", {
|
|
7989
|
+
headers: linkedInHeaders(accessToken)
|
|
7990
|
+
});
|
|
7991
|
+
const text = await r.text();
|
|
7992
|
+
if (!r.ok) {
|
|
7993
|
+
throw new Error(`LinkedIn organizationAcls failed (${r.status}): ${text.slice(0, 800)}`);
|
|
7994
|
+
}
|
|
7995
|
+
let data;
|
|
7996
|
+
try {
|
|
7997
|
+
data = JSON.parse(text);
|
|
7998
|
+
} catch {
|
|
7999
|
+
throw new Error("LinkedIn organizationAcls: invalid JSON");
|
|
8000
|
+
}
|
|
8001
|
+
return Array.isArray(data.elements) ? data.elements : [];
|
|
8002
|
+
}
|
|
8003
|
+
function organizationDisplayName(org) {
|
|
8004
|
+
if (org.localizedName?.trim()) return org.localizedName.trim();
|
|
8005
|
+
const localized = org.name?.localized;
|
|
8006
|
+
if (localized) {
|
|
8007
|
+
for (const v of Object.values(localized)) {
|
|
8008
|
+
if (v?.trim()) return v.trim();
|
|
8009
|
+
}
|
|
8010
|
+
}
|
|
8011
|
+
if (org.vanityName?.trim()) return org.vanityName.trim();
|
|
8012
|
+
if (org.id != null) return `Organization ${org.id}`;
|
|
8013
|
+
return "Organization";
|
|
8014
|
+
}
|
|
8015
|
+
function organizationWebsite(org) {
|
|
8016
|
+
if (org.localizedWebsite?.trim()) return org.localizedWebsite.trim();
|
|
8017
|
+
const localized = org.website?.localized;
|
|
8018
|
+
if (localized) {
|
|
8019
|
+
for (const v of Object.values(localized)) {
|
|
8020
|
+
if (v?.trim()) return v.trim();
|
|
8021
|
+
}
|
|
8022
|
+
}
|
|
8023
|
+
return void 0;
|
|
8024
|
+
}
|
|
8025
|
+
function linkedInOrganizationUrn(orgIdOrUrn) {
|
|
8026
|
+
const s = String(orgIdOrUrn ?? "").trim();
|
|
8027
|
+
if (s.startsWith("urn:li:organization:")) return s;
|
|
8028
|
+
return `urn:li:organization:${s}`;
|
|
8029
|
+
}
|
|
8030
|
+
function linkedInOrganizationIdFromUrn(organizationUrn) {
|
|
8031
|
+
const m = String(organizationUrn).trim().match(/urn:li:organization:(\d+)/);
|
|
8032
|
+
if (!m?.[1]) throw new Error(`Invalid LinkedIn organization URN: ${organizationUrn}`);
|
|
8033
|
+
return m[1];
|
|
8034
|
+
}
|
|
8035
|
+
async function linkedInGetOrganization(accessToken, orgIdOrUrn) {
|
|
8036
|
+
const orgId = String(orgIdOrUrn).startsWith("urn:") ? linkedInOrganizationIdFromUrn(String(orgIdOrUrn)) : String(orgIdOrUrn).trim();
|
|
8037
|
+
const r = await fetch(`https://api.linkedin.com/v2/organizations/${encodeURIComponent(orgId)}`, {
|
|
8038
|
+
headers: linkedInHeaders(accessToken)
|
|
8039
|
+
});
|
|
8040
|
+
const text = await r.text();
|
|
8041
|
+
if (!r.ok) {
|
|
8042
|
+
throw new Error(`LinkedIn organization ${orgId} failed (${r.status}): ${text.slice(0, 800)}`);
|
|
8043
|
+
}
|
|
8044
|
+
try {
|
|
8045
|
+
return JSON.parse(text);
|
|
8046
|
+
} catch {
|
|
8047
|
+
throw new Error(`LinkedIn organization ${orgId}: invalid JSON`);
|
|
8048
|
+
}
|
|
8049
|
+
}
|
|
8050
|
+
async function linkedInListManagedOrganizations(accessToken) {
|
|
8051
|
+
const acls = await linkedInFetchOrganizationAcls(accessToken);
|
|
8052
|
+
const approved = acls.filter((a) => a.state === "APPROVED" && a.organization?.trim());
|
|
8053
|
+
const byUrn = /* @__PURE__ */ new Map();
|
|
8054
|
+
for (const acl of approved) {
|
|
8055
|
+
const urn = linkedInOrganizationUrn(acl.organization);
|
|
8056
|
+
if (!byUrn.has(urn)) byUrn.set(urn, acl);
|
|
8057
|
+
}
|
|
8058
|
+
const out = [];
|
|
8059
|
+
for (const [urn, acl] of byUrn) {
|
|
8060
|
+
try {
|
|
8061
|
+
const org = await linkedInGetOrganization(accessToken, urn);
|
|
8062
|
+
out.push({
|
|
8063
|
+
urn,
|
|
8064
|
+
id: org.id ?? Number(linkedInOrganizationIdFromUrn(urn)),
|
|
8065
|
+
name: organizationDisplayName(org),
|
|
8066
|
+
vanityName: org.vanityName,
|
|
8067
|
+
website: organizationWebsite(org),
|
|
8068
|
+
role: acl.role
|
|
8069
|
+
});
|
|
8070
|
+
} catch {
|
|
8071
|
+
out.push({
|
|
8072
|
+
urn,
|
|
8073
|
+
id: Number(linkedInOrganizationIdFromUrn(urn)),
|
|
8074
|
+
name: linkedInOrganizationIdFromUrn(urn),
|
|
8075
|
+
role: acl.role
|
|
8076
|
+
});
|
|
8077
|
+
}
|
|
8078
|
+
}
|
|
8079
|
+
out.sort((a, b) => a.name.localeCompare(b.name));
|
|
8080
|
+
return out;
|
|
8081
|
+
}
|
|
7980
8082
|
function personUrn(personSub) {
|
|
7981
8083
|
const s = String(personSub || "").trim();
|
|
7982
8084
|
if (s.startsWith("urn:li:person:")) return s;
|
|
7983
8085
|
return `urn:li:person:${s}`;
|
|
7984
8086
|
}
|
|
7985
|
-
|
|
7986
|
-
const
|
|
8087
|
+
function authorUrnFromParams(authorUrn, personSub) {
|
|
8088
|
+
const direct = String(authorUrn ?? "").trim();
|
|
8089
|
+
if (direct.startsWith("urn:li:")) return direct;
|
|
8090
|
+
return personUrn(String(personSub ?? "").trim());
|
|
8091
|
+
}
|
|
8092
|
+
async function linkedInRegisterImageUpload(accessToken, author) {
|
|
8093
|
+
const owner = authorUrnFromParams(author);
|
|
7987
8094
|
const r = await fetch("https://api.linkedin.com/v2/assets?action=registerUpload", {
|
|
7988
8095
|
method: "POST",
|
|
7989
|
-
headers:
|
|
7990
|
-
Authorization: `Bearer ${accessToken}`,
|
|
7991
|
-
"X-Restli-Protocol-Version": RESTLI,
|
|
7992
|
-
"Content-Type": "application/json"
|
|
7993
|
-
},
|
|
8096
|
+
headers: linkedInHeaders(accessToken, true),
|
|
7994
8097
|
body: JSON.stringify({
|
|
7995
8098
|
registerUploadRequest: {
|
|
7996
8099
|
recipes: ["urn:li:digitalmediaRecipe:feedshare-image"],
|
|
@@ -8038,7 +8141,7 @@ async function linkedInUploadBinary(accessToken, uploadUrl, body, contentType) {
|
|
|
8038
8141
|
}
|
|
8039
8142
|
}
|
|
8040
8143
|
async function linkedInCreateImageShare(params) {
|
|
8041
|
-
const author =
|
|
8144
|
+
const author = authorUrnFromParams(params.authorUrn);
|
|
8042
8145
|
const commentary = params.commentary.slice(0, 2900);
|
|
8043
8146
|
const title = params.title.slice(0, 200);
|
|
8044
8147
|
const description = (params.description ?? title).slice(0, 200);
|
|
@@ -8084,7 +8187,7 @@ async function linkedInCreateImageShare(params) {
|
|
|
8084
8187
|
return { status: r.status, id: data.id };
|
|
8085
8188
|
}
|
|
8086
8189
|
async function linkedInCreateTextShare(params) {
|
|
8087
|
-
const author =
|
|
8190
|
+
const author = authorUrnFromParams(params.authorUrn);
|
|
8088
8191
|
const commentary = params.commentary.slice(0, 2900);
|
|
8089
8192
|
const r = await fetch("https://api.linkedin.com/v2/ugcPosts", {
|
|
8090
8193
|
method: "POST",
|
|
@@ -8418,18 +8521,44 @@ function createSocialMediaHandlers(config) {
|
|
|
8418
8521
|
const enabled = map.enabled !== "false";
|
|
8419
8522
|
const token = (map.linkedin_access_token ?? "").trim();
|
|
8420
8523
|
const sub = (map.linkedin_person_sub ?? "").trim();
|
|
8524
|
+
const orgUrn = (map.linkedin_organization_urn ?? "").trim();
|
|
8421
8525
|
return json({
|
|
8422
8526
|
ok: true,
|
|
8423
|
-
linkedInReady: enabled && Boolean(token) && Boolean(
|
|
8527
|
+
linkedInReady: enabled && Boolean(token) && Boolean(orgUrn),
|
|
8424
8528
|
enabled,
|
|
8425
8529
|
hasToken: Boolean(token),
|
|
8426
|
-
hasPersonSub: Boolean(sub)
|
|
8530
|
+
hasPersonSub: Boolean(sub),
|
|
8531
|
+
hasOrganization: Boolean(orgUrn),
|
|
8532
|
+
organizationName: map.linkedin_organization_name?.trim() || null
|
|
8427
8533
|
});
|
|
8428
8534
|
} catch (e) {
|
|
8429
8535
|
const msg = e instanceof Error ? e.message : "Failed to load status";
|
|
8430
8536
|
return json({ error: msg }, { status: 500 });
|
|
8431
8537
|
}
|
|
8432
8538
|
},
|
|
8539
|
+
async fetchLinkedInOrganizations(req) {
|
|
8540
|
+
const a = await requireAuth(req);
|
|
8541
|
+
if (a) return a;
|
|
8542
|
+
const pe = await requireEntityPermission(req, "settings", "read");
|
|
8543
|
+
if (pe) return pe;
|
|
8544
|
+
let body = {};
|
|
8545
|
+
try {
|
|
8546
|
+
body = await req.json();
|
|
8547
|
+
} catch {
|
|
8548
|
+
}
|
|
8549
|
+
try {
|
|
8550
|
+
const map = await loadGroupMap(dataSource, entityMap, encryptionKey);
|
|
8551
|
+
const token = String(body?.accessToken ?? map.linkedin_access_token ?? "").trim();
|
|
8552
|
+
if (!token) {
|
|
8553
|
+
return json({ error: "LinkedIn access token is required." }, { status: 400 });
|
|
8554
|
+
}
|
|
8555
|
+
const organizations = await linkedInListManagedOrganizations(token);
|
|
8556
|
+
return json({ ok: true, organizations });
|
|
8557
|
+
} catch (e) {
|
|
8558
|
+
const msg = e instanceof Error ? e.message : "LinkedIn organizations request failed";
|
|
8559
|
+
return json({ error: msg }, { status: 502 });
|
|
8560
|
+
}
|
|
8561
|
+
},
|
|
8433
8562
|
async syncLinkedInProfile(req) {
|
|
8434
8563
|
const a = await requireAuth(req);
|
|
8435
8564
|
if (a) return a;
|
|
@@ -8478,10 +8607,12 @@ function createSocialMediaHandlers(config) {
|
|
|
8478
8607
|
return json({ error: "Social media plugin is disabled." }, { status: 400 });
|
|
8479
8608
|
}
|
|
8480
8609
|
const token = (map.linkedin_access_token ?? "").trim();
|
|
8481
|
-
const
|
|
8482
|
-
if (!token || !
|
|
8610
|
+
const authorUrn = (map.linkedin_organization_urn ?? "").trim();
|
|
8611
|
+
if (!token || !authorUrn) {
|
|
8483
8612
|
return json(
|
|
8484
|
-
{
|
|
8613
|
+
{
|
|
8614
|
+
error: "Configure LinkedIn access token and target organization (Plugins \u2192 Social media \u2192 LinkedIn)."
|
|
8615
|
+
},
|
|
8485
8616
|
{ status: 400 }
|
|
8486
8617
|
);
|
|
8487
8618
|
}
|
|
@@ -8522,7 +8653,7 @@ ${excerpt}`).trim().slice(0, 2900);
|
|
|
8522
8653
|
contentType: imagePayload.contentType
|
|
8523
8654
|
});
|
|
8524
8655
|
try {
|
|
8525
|
-
const { uploadUrl, asset } = await linkedInRegisterImageUpload(token,
|
|
8656
|
+
const { uploadUrl, asset } = await linkedInRegisterImageUpload(token, authorUrn);
|
|
8526
8657
|
logLinkedInPublish(publishLog, "image_registered", {
|
|
8527
8658
|
blogId,
|
|
8528
8659
|
asset: truncateForLog(asset, 120),
|
|
@@ -8538,7 +8669,7 @@ ${excerpt}`).trim().slice(0, 2900);
|
|
|
8538
8669
|
logLinkedInPublish(publishLog, "image_uploaded", { blogId, bytes: imagePayload.buffer.length });
|
|
8539
8670
|
const out2 = await linkedInCreateImageShare({
|
|
8540
8671
|
accessToken: token,
|
|
8541
|
-
|
|
8672
|
+
authorUrn,
|
|
8542
8673
|
asset,
|
|
8543
8674
|
commentary,
|
|
8544
8675
|
title,
|
|
@@ -8576,7 +8707,7 @@ ${excerpt}`).trim().slice(0, 2900);
|
|
|
8576
8707
|
});
|
|
8577
8708
|
const out2 = await linkedInCreateTextShare({
|
|
8578
8709
|
accessToken: token,
|
|
8579
|
-
|
|
8710
|
+
authorUrn,
|
|
8580
8711
|
commentary
|
|
8581
8712
|
});
|
|
8582
8713
|
logLinkedInPublish(publishLog, "success", {
|
|
@@ -8615,7 +8746,7 @@ ${excerpt}`).trim().slice(0, 2900);
|
|
|
8615
8746
|
});
|
|
8616
8747
|
const out = await linkedInCreateTextShare({
|
|
8617
8748
|
accessToken: token,
|
|
8618
|
-
|
|
8749
|
+
authorUrn,
|
|
8619
8750
|
commentary
|
|
8620
8751
|
});
|
|
8621
8752
|
logLinkedInPublish(publishLog, "success", {
|
|
@@ -8912,6 +9043,24 @@ init_pg_boss_service();
|
|
|
8912
9043
|
// src/plugins/jobs/blog-generate-job.ts
|
|
8913
9044
|
var import_typeorm50 = require("typeorm");
|
|
8914
9045
|
|
|
9046
|
+
// src/plugins/jobs/schedule-next-run.ts
|
|
9047
|
+
var import_cron_parser = require("cron-parser");
|
|
9048
|
+
function computeNextRunAt(schedule, from = /* @__PURE__ */ new Date()) {
|
|
9049
|
+
if (!schedule.enabled) return null;
|
|
9050
|
+
try {
|
|
9051
|
+
const cron = buildCronFromSchedule(schedule);
|
|
9052
|
+
const tz = schedule.timezone?.trim() || "UTC";
|
|
9053
|
+
const interval = (0, import_cron_parser.parseExpression)(cron, {
|
|
9054
|
+
currentDate: from,
|
|
9055
|
+
tz
|
|
9056
|
+
});
|
|
9057
|
+
return interval.next().toDate();
|
|
9058
|
+
} catch (err) {
|
|
9059
|
+
console.warn("[job-schedules] could not compute next run:", err);
|
|
9060
|
+
return null;
|
|
9061
|
+
}
|
|
9062
|
+
}
|
|
9063
|
+
|
|
8915
9064
|
// src/plugins/jobs/job-runner.ts
|
|
8916
9065
|
var executeJobRef = null;
|
|
8917
9066
|
async function ensureScheduleQueueWorker(cms, queueName) {
|
|
@@ -8940,7 +9089,7 @@ function serializeSchedule(s) {
|
|
|
8940
9089
|
lastRunAt: s.lastRunAt?.toISOString() ?? null,
|
|
8941
9090
|
lastRunStatus: s.lastRunStatus,
|
|
8942
9091
|
lastRunError: s.lastRunError,
|
|
8943
|
-
nextRunAt: s.nextRunAt?.toISOString() ?? null,
|
|
9092
|
+
nextRunAt: (computeNextRunAt(s) ?? s.nextRunAt)?.toISOString() ?? null,
|
|
8944
9093
|
createdAt: s.createdAt.toISOString(),
|
|
8945
9094
|
updatedAt: s.updatedAt.toISOString()
|
|
8946
9095
|
};
|
|
@@ -8987,6 +9136,9 @@ function createJobScheduleHandlers(apiConfig) {
|
|
|
8987
9136
|
await syncJobScheduleToPgBoss(cms, schedule, async (queueName) => {
|
|
8988
9137
|
await ensureScheduleQueueWorker(cms, queueName);
|
|
8989
9138
|
});
|
|
9139
|
+
const repo = dataSource.getRepository(ScheduleEntity);
|
|
9140
|
+
schedule.nextRunAt = computeNextRunAt(schedule);
|
|
9141
|
+
await repo.save(schedule);
|
|
8990
9142
|
}
|
|
8991
9143
|
return {
|
|
8992
9144
|
async list(req) {
|
|
@@ -9867,6 +10019,9 @@ function createCmsApiHandler(config) {
|
|
|
9867
10019
|
if (tail === "sync-profile" && m === "POST") {
|
|
9868
10020
|
return socialMediaHandlers.syncLinkedInProfile(req);
|
|
9869
10021
|
}
|
|
10022
|
+
if (tail === "fetch-organizations" && m === "POST") {
|
|
10023
|
+
return socialMediaHandlers.fetchLinkedInOrganizations(req);
|
|
10024
|
+
}
|
|
9870
10025
|
if (tail === "publish-blog" && m === "POST") {
|
|
9871
10026
|
let body;
|
|
9872
10027
|
try {
|