@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.js
CHANGED
|
@@ -7945,6 +7945,13 @@ import path from "path";
|
|
|
7945
7945
|
|
|
7946
7946
|
// src/plugins/social-media/linkedin-client.ts
|
|
7947
7947
|
var RESTLI = "2.0.0";
|
|
7948
|
+
function linkedInHeaders(accessToken, json = false) {
|
|
7949
|
+
return {
|
|
7950
|
+
Authorization: `Bearer ${accessToken}`,
|
|
7951
|
+
"X-Restli-Protocol-Version": RESTLI,
|
|
7952
|
+
...json ? { "Content-Type": "application/json" } : {}
|
|
7953
|
+
};
|
|
7954
|
+
}
|
|
7948
7955
|
async function linkedInGetUserinfo(accessToken) {
|
|
7949
7956
|
const r = await fetch("https://api.linkedin.com/v2/userinfo", {
|
|
7950
7957
|
headers: { Authorization: `Bearer ${accessToken}` }
|
|
@@ -7959,20 +7966,116 @@ async function linkedInGetUserinfo(accessToken) {
|
|
|
7959
7966
|
throw new Error("LinkedIn userinfo: invalid JSON");
|
|
7960
7967
|
}
|
|
7961
7968
|
}
|
|
7969
|
+
async function linkedInFetchOrganizationAcls(accessToken) {
|
|
7970
|
+
const r = await fetch("https://api.linkedin.com/v2/organizationAcls?q=roleAssignee", {
|
|
7971
|
+
headers: linkedInHeaders(accessToken)
|
|
7972
|
+
});
|
|
7973
|
+
const text = await r.text();
|
|
7974
|
+
if (!r.ok) {
|
|
7975
|
+
throw new Error(`LinkedIn organizationAcls failed (${r.status}): ${text.slice(0, 800)}`);
|
|
7976
|
+
}
|
|
7977
|
+
let data;
|
|
7978
|
+
try {
|
|
7979
|
+
data = JSON.parse(text);
|
|
7980
|
+
} catch {
|
|
7981
|
+
throw new Error("LinkedIn organizationAcls: invalid JSON");
|
|
7982
|
+
}
|
|
7983
|
+
return Array.isArray(data.elements) ? data.elements : [];
|
|
7984
|
+
}
|
|
7985
|
+
function organizationDisplayName(org) {
|
|
7986
|
+
if (org.localizedName?.trim()) return org.localizedName.trim();
|
|
7987
|
+
const localized = org.name?.localized;
|
|
7988
|
+
if (localized) {
|
|
7989
|
+
for (const v of Object.values(localized)) {
|
|
7990
|
+
if (v?.trim()) return v.trim();
|
|
7991
|
+
}
|
|
7992
|
+
}
|
|
7993
|
+
if (org.vanityName?.trim()) return org.vanityName.trim();
|
|
7994
|
+
if (org.id != null) return `Organization ${org.id}`;
|
|
7995
|
+
return "Organization";
|
|
7996
|
+
}
|
|
7997
|
+
function organizationWebsite(org) {
|
|
7998
|
+
if (org.localizedWebsite?.trim()) return org.localizedWebsite.trim();
|
|
7999
|
+
const localized = org.website?.localized;
|
|
8000
|
+
if (localized) {
|
|
8001
|
+
for (const v of Object.values(localized)) {
|
|
8002
|
+
if (v?.trim()) return v.trim();
|
|
8003
|
+
}
|
|
8004
|
+
}
|
|
8005
|
+
return void 0;
|
|
8006
|
+
}
|
|
8007
|
+
function linkedInOrganizationUrn(orgIdOrUrn) {
|
|
8008
|
+
const s = String(orgIdOrUrn ?? "").trim();
|
|
8009
|
+
if (s.startsWith("urn:li:organization:")) return s;
|
|
8010
|
+
return `urn:li:organization:${s}`;
|
|
8011
|
+
}
|
|
8012
|
+
function linkedInOrganizationIdFromUrn(organizationUrn) {
|
|
8013
|
+
const m = String(organizationUrn).trim().match(/urn:li:organization:(\d+)/);
|
|
8014
|
+
if (!m?.[1]) throw new Error(`Invalid LinkedIn organization URN: ${organizationUrn}`);
|
|
8015
|
+
return m[1];
|
|
8016
|
+
}
|
|
8017
|
+
async function linkedInGetOrganization(accessToken, orgIdOrUrn) {
|
|
8018
|
+
const orgId = String(orgIdOrUrn).startsWith("urn:") ? linkedInOrganizationIdFromUrn(String(orgIdOrUrn)) : String(orgIdOrUrn).trim();
|
|
8019
|
+
const r = await fetch(`https://api.linkedin.com/v2/organizations/${encodeURIComponent(orgId)}`, {
|
|
8020
|
+
headers: linkedInHeaders(accessToken)
|
|
8021
|
+
});
|
|
8022
|
+
const text = await r.text();
|
|
8023
|
+
if (!r.ok) {
|
|
8024
|
+
throw new Error(`LinkedIn organization ${orgId} failed (${r.status}): ${text.slice(0, 800)}`);
|
|
8025
|
+
}
|
|
8026
|
+
try {
|
|
8027
|
+
return JSON.parse(text);
|
|
8028
|
+
} catch {
|
|
8029
|
+
throw new Error(`LinkedIn organization ${orgId}: invalid JSON`);
|
|
8030
|
+
}
|
|
8031
|
+
}
|
|
8032
|
+
async function linkedInListManagedOrganizations(accessToken) {
|
|
8033
|
+
const acls = await linkedInFetchOrganizationAcls(accessToken);
|
|
8034
|
+
const approved = acls.filter((a) => a.state === "APPROVED" && a.organization?.trim());
|
|
8035
|
+
const byUrn = /* @__PURE__ */ new Map();
|
|
8036
|
+
for (const acl of approved) {
|
|
8037
|
+
const urn = linkedInOrganizationUrn(acl.organization);
|
|
8038
|
+
if (!byUrn.has(urn)) byUrn.set(urn, acl);
|
|
8039
|
+
}
|
|
8040
|
+
const out = [];
|
|
8041
|
+
for (const [urn, acl] of byUrn) {
|
|
8042
|
+
try {
|
|
8043
|
+
const org = await linkedInGetOrganization(accessToken, urn);
|
|
8044
|
+
out.push({
|
|
8045
|
+
urn,
|
|
8046
|
+
id: org.id ?? Number(linkedInOrganizationIdFromUrn(urn)),
|
|
8047
|
+
name: organizationDisplayName(org),
|
|
8048
|
+
vanityName: org.vanityName,
|
|
8049
|
+
website: organizationWebsite(org),
|
|
8050
|
+
role: acl.role
|
|
8051
|
+
});
|
|
8052
|
+
} catch {
|
|
8053
|
+
out.push({
|
|
8054
|
+
urn,
|
|
8055
|
+
id: Number(linkedInOrganizationIdFromUrn(urn)),
|
|
8056
|
+
name: linkedInOrganizationIdFromUrn(urn),
|
|
8057
|
+
role: acl.role
|
|
8058
|
+
});
|
|
8059
|
+
}
|
|
8060
|
+
}
|
|
8061
|
+
out.sort((a, b) => a.name.localeCompare(b.name));
|
|
8062
|
+
return out;
|
|
8063
|
+
}
|
|
7962
8064
|
function personUrn(personSub) {
|
|
7963
8065
|
const s = String(personSub || "").trim();
|
|
7964
8066
|
if (s.startsWith("urn:li:person:")) return s;
|
|
7965
8067
|
return `urn:li:person:${s}`;
|
|
7966
8068
|
}
|
|
7967
|
-
|
|
7968
|
-
const
|
|
8069
|
+
function authorUrnFromParams(authorUrn, personSub) {
|
|
8070
|
+
const direct = String(authorUrn ?? "").trim();
|
|
8071
|
+
if (direct.startsWith("urn:li:")) return direct;
|
|
8072
|
+
return personUrn(String(personSub ?? "").trim());
|
|
8073
|
+
}
|
|
8074
|
+
async function linkedInRegisterImageUpload(accessToken, author) {
|
|
8075
|
+
const owner = authorUrnFromParams(author);
|
|
7969
8076
|
const r = await fetch("https://api.linkedin.com/v2/assets?action=registerUpload", {
|
|
7970
8077
|
method: "POST",
|
|
7971
|
-
headers:
|
|
7972
|
-
Authorization: `Bearer ${accessToken}`,
|
|
7973
|
-
"X-Restli-Protocol-Version": RESTLI,
|
|
7974
|
-
"Content-Type": "application/json"
|
|
7975
|
-
},
|
|
8078
|
+
headers: linkedInHeaders(accessToken, true),
|
|
7976
8079
|
body: JSON.stringify({
|
|
7977
8080
|
registerUploadRequest: {
|
|
7978
8081
|
recipes: ["urn:li:digitalmediaRecipe:feedshare-image"],
|
|
@@ -8020,7 +8123,7 @@ async function linkedInUploadBinary(accessToken, uploadUrl, body, contentType) {
|
|
|
8020
8123
|
}
|
|
8021
8124
|
}
|
|
8022
8125
|
async function linkedInCreateImageShare(params) {
|
|
8023
|
-
const author =
|
|
8126
|
+
const author = authorUrnFromParams(params.authorUrn);
|
|
8024
8127
|
const commentary = params.commentary.slice(0, 2900);
|
|
8025
8128
|
const title = params.title.slice(0, 200);
|
|
8026
8129
|
const description = (params.description ?? title).slice(0, 200);
|
|
@@ -8066,7 +8169,7 @@ async function linkedInCreateImageShare(params) {
|
|
|
8066
8169
|
return { status: r.status, id: data.id };
|
|
8067
8170
|
}
|
|
8068
8171
|
async function linkedInCreateTextShare(params) {
|
|
8069
|
-
const author =
|
|
8172
|
+
const author = authorUrnFromParams(params.authorUrn);
|
|
8070
8173
|
const commentary = params.commentary.slice(0, 2900);
|
|
8071
8174
|
const r = await fetch("https://api.linkedin.com/v2/ugcPosts", {
|
|
8072
8175
|
method: "POST",
|
|
@@ -8400,18 +8503,44 @@ function createSocialMediaHandlers(config) {
|
|
|
8400
8503
|
const enabled = map.enabled !== "false";
|
|
8401
8504
|
const token = (map.linkedin_access_token ?? "").trim();
|
|
8402
8505
|
const sub = (map.linkedin_person_sub ?? "").trim();
|
|
8506
|
+
const orgUrn = (map.linkedin_organization_urn ?? "").trim();
|
|
8403
8507
|
return json({
|
|
8404
8508
|
ok: true,
|
|
8405
|
-
linkedInReady: enabled && Boolean(token) && Boolean(
|
|
8509
|
+
linkedInReady: enabled && Boolean(token) && Boolean(orgUrn),
|
|
8406
8510
|
enabled,
|
|
8407
8511
|
hasToken: Boolean(token),
|
|
8408
|
-
hasPersonSub: Boolean(sub)
|
|
8512
|
+
hasPersonSub: Boolean(sub),
|
|
8513
|
+
hasOrganization: Boolean(orgUrn),
|
|
8514
|
+
organizationName: map.linkedin_organization_name?.trim() || null
|
|
8409
8515
|
});
|
|
8410
8516
|
} catch (e) {
|
|
8411
8517
|
const msg = e instanceof Error ? e.message : "Failed to load status";
|
|
8412
8518
|
return json({ error: msg }, { status: 500 });
|
|
8413
8519
|
}
|
|
8414
8520
|
},
|
|
8521
|
+
async fetchLinkedInOrganizations(req) {
|
|
8522
|
+
const a = await requireAuth(req);
|
|
8523
|
+
if (a) return a;
|
|
8524
|
+
const pe = await requireEntityPermission(req, "settings", "read");
|
|
8525
|
+
if (pe) return pe;
|
|
8526
|
+
let body = {};
|
|
8527
|
+
try {
|
|
8528
|
+
body = await req.json();
|
|
8529
|
+
} catch {
|
|
8530
|
+
}
|
|
8531
|
+
try {
|
|
8532
|
+
const map = await loadGroupMap(dataSource, entityMap, encryptionKey);
|
|
8533
|
+
const token = String(body?.accessToken ?? map.linkedin_access_token ?? "").trim();
|
|
8534
|
+
if (!token) {
|
|
8535
|
+
return json({ error: "LinkedIn access token is required." }, { status: 400 });
|
|
8536
|
+
}
|
|
8537
|
+
const organizations = await linkedInListManagedOrganizations(token);
|
|
8538
|
+
return json({ ok: true, organizations });
|
|
8539
|
+
} catch (e) {
|
|
8540
|
+
const msg = e instanceof Error ? e.message : "LinkedIn organizations request failed";
|
|
8541
|
+
return json({ error: msg }, { status: 502 });
|
|
8542
|
+
}
|
|
8543
|
+
},
|
|
8415
8544
|
async syncLinkedInProfile(req) {
|
|
8416
8545
|
const a = await requireAuth(req);
|
|
8417
8546
|
if (a) return a;
|
|
@@ -8460,10 +8589,12 @@ function createSocialMediaHandlers(config) {
|
|
|
8460
8589
|
return json({ error: "Social media plugin is disabled." }, { status: 400 });
|
|
8461
8590
|
}
|
|
8462
8591
|
const token = (map.linkedin_access_token ?? "").trim();
|
|
8463
|
-
const
|
|
8464
|
-
if (!token || !
|
|
8592
|
+
const authorUrn = (map.linkedin_organization_urn ?? "").trim();
|
|
8593
|
+
if (!token || !authorUrn) {
|
|
8465
8594
|
return json(
|
|
8466
|
-
{
|
|
8595
|
+
{
|
|
8596
|
+
error: "Configure LinkedIn access token and target organization (Plugins \u2192 Social media \u2192 LinkedIn)."
|
|
8597
|
+
},
|
|
8467
8598
|
{ status: 400 }
|
|
8468
8599
|
);
|
|
8469
8600
|
}
|
|
@@ -8504,7 +8635,7 @@ ${excerpt}`).trim().slice(0, 2900);
|
|
|
8504
8635
|
contentType: imagePayload.contentType
|
|
8505
8636
|
});
|
|
8506
8637
|
try {
|
|
8507
|
-
const { uploadUrl, asset } = await linkedInRegisterImageUpload(token,
|
|
8638
|
+
const { uploadUrl, asset } = await linkedInRegisterImageUpload(token, authorUrn);
|
|
8508
8639
|
logLinkedInPublish(publishLog, "image_registered", {
|
|
8509
8640
|
blogId,
|
|
8510
8641
|
asset: truncateForLog(asset, 120),
|
|
@@ -8520,7 +8651,7 @@ ${excerpt}`).trim().slice(0, 2900);
|
|
|
8520
8651
|
logLinkedInPublish(publishLog, "image_uploaded", { blogId, bytes: imagePayload.buffer.length });
|
|
8521
8652
|
const out2 = await linkedInCreateImageShare({
|
|
8522
8653
|
accessToken: token,
|
|
8523
|
-
|
|
8654
|
+
authorUrn,
|
|
8524
8655
|
asset,
|
|
8525
8656
|
commentary,
|
|
8526
8657
|
title,
|
|
@@ -8558,7 +8689,7 @@ ${excerpt}`).trim().slice(0, 2900);
|
|
|
8558
8689
|
});
|
|
8559
8690
|
const out2 = await linkedInCreateTextShare({
|
|
8560
8691
|
accessToken: token,
|
|
8561
|
-
|
|
8692
|
+
authorUrn,
|
|
8562
8693
|
commentary
|
|
8563
8694
|
});
|
|
8564
8695
|
logLinkedInPublish(publishLog, "success", {
|
|
@@ -8597,7 +8728,7 @@ ${excerpt}`).trim().slice(0, 2900);
|
|
|
8597
8728
|
});
|
|
8598
8729
|
const out = await linkedInCreateTextShare({
|
|
8599
8730
|
accessToken: token,
|
|
8600
|
-
|
|
8731
|
+
authorUrn,
|
|
8601
8732
|
commentary
|
|
8602
8733
|
});
|
|
8603
8734
|
logLinkedInPublish(publishLog, "success", {
|
|
@@ -8894,6 +9025,24 @@ init_pg_boss_service();
|
|
|
8894
9025
|
// src/plugins/jobs/blog-generate-job.ts
|
|
8895
9026
|
import { In as In3 } from "typeorm";
|
|
8896
9027
|
|
|
9028
|
+
// src/plugins/jobs/schedule-next-run.ts
|
|
9029
|
+
import { parseExpression } from "cron-parser";
|
|
9030
|
+
function computeNextRunAt(schedule, from = /* @__PURE__ */ new Date()) {
|
|
9031
|
+
if (!schedule.enabled) return null;
|
|
9032
|
+
try {
|
|
9033
|
+
const cron = buildCronFromSchedule(schedule);
|
|
9034
|
+
const tz = schedule.timezone?.trim() || "UTC";
|
|
9035
|
+
const interval = parseExpression(cron, {
|
|
9036
|
+
currentDate: from,
|
|
9037
|
+
tz
|
|
9038
|
+
});
|
|
9039
|
+
return interval.next().toDate();
|
|
9040
|
+
} catch (err) {
|
|
9041
|
+
console.warn("[job-schedules] could not compute next run:", err);
|
|
9042
|
+
return null;
|
|
9043
|
+
}
|
|
9044
|
+
}
|
|
9045
|
+
|
|
8897
9046
|
// src/plugins/jobs/job-runner.ts
|
|
8898
9047
|
var executeJobRef = null;
|
|
8899
9048
|
async function ensureScheduleQueueWorker(cms, queueName) {
|
|
@@ -8922,7 +9071,7 @@ function serializeSchedule(s) {
|
|
|
8922
9071
|
lastRunAt: s.lastRunAt?.toISOString() ?? null,
|
|
8923
9072
|
lastRunStatus: s.lastRunStatus,
|
|
8924
9073
|
lastRunError: s.lastRunError,
|
|
8925
|
-
nextRunAt: s.nextRunAt?.toISOString() ?? null,
|
|
9074
|
+
nextRunAt: (computeNextRunAt(s) ?? s.nextRunAt)?.toISOString() ?? null,
|
|
8926
9075
|
createdAt: s.createdAt.toISOString(),
|
|
8927
9076
|
updatedAt: s.updatedAt.toISOString()
|
|
8928
9077
|
};
|
|
@@ -8969,6 +9118,9 @@ function createJobScheduleHandlers(apiConfig) {
|
|
|
8969
9118
|
await syncJobScheduleToPgBoss(cms, schedule, async (queueName) => {
|
|
8970
9119
|
await ensureScheduleQueueWorker(cms, queueName);
|
|
8971
9120
|
});
|
|
9121
|
+
const repo = dataSource.getRepository(ScheduleEntity);
|
|
9122
|
+
schedule.nextRunAt = computeNextRunAt(schedule);
|
|
9123
|
+
await repo.save(schedule);
|
|
8972
9124
|
}
|
|
8973
9125
|
return {
|
|
8974
9126
|
async list(req) {
|
|
@@ -9849,6 +10001,9 @@ function createCmsApiHandler(config) {
|
|
|
9849
10001
|
if (tail === "sync-profile" && m === "POST") {
|
|
9850
10002
|
return socialMediaHandlers.syncLinkedInProfile(req);
|
|
9851
10003
|
}
|
|
10004
|
+
if (tail === "fetch-organizations" && m === "POST") {
|
|
10005
|
+
return socialMediaHandlers.fetchLinkedInOrganizations(req);
|
|
10006
|
+
}
|
|
9852
10007
|
if (tail === "publish-blog" && m === "POST") {
|
|
9853
10008
|
let body;
|
|
9854
10009
|
try {
|