@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/index.d.cts
CHANGED
|
@@ -990,6 +990,7 @@ interface SocialMediaApiConfig {
|
|
|
990
990
|
}
|
|
991
991
|
declare function createSocialMediaHandlers(config: SocialMediaApiConfig): {
|
|
992
992
|
getLinkedInStatus(req: Request): Promise<Response>;
|
|
993
|
+
fetchLinkedInOrganizations(req: Request): Promise<Response>;
|
|
993
994
|
syncLinkedInProfile(req: Request): Promise<Response>;
|
|
994
995
|
publishBlogToLinkedIn(req: Request, blogId: number): Promise<Response>;
|
|
995
996
|
getFacebookStatus(req: Request): Promise<Response>;
|
package/dist/index.d.ts
CHANGED
|
@@ -990,6 +990,7 @@ interface SocialMediaApiConfig {
|
|
|
990
990
|
}
|
|
991
991
|
declare function createSocialMediaHandlers(config: SocialMediaApiConfig): {
|
|
992
992
|
getLinkedInStatus(req: Request): Promise<Response>;
|
|
993
|
+
fetchLinkedInOrganizations(req: Request): Promise<Response>;
|
|
993
994
|
syncLinkedInProfile(req: Request): Promise<Response>;
|
|
994
995
|
publishBlogToLinkedIn(req: Request, blogId: number): Promise<Response>;
|
|
995
996
|
getFacebookStatus(req: Request): Promise<Response>;
|
package/dist/index.js
CHANGED
|
@@ -4214,6 +4214,24 @@ async function queueJobScheduleNow(cms, scheduleId) {
|
|
|
4214
4214
|
});
|
|
4215
4215
|
}
|
|
4216
4216
|
|
|
4217
|
+
// src/plugins/jobs/schedule-next-run.ts
|
|
4218
|
+
import { parseExpression } from "cron-parser";
|
|
4219
|
+
function computeNextRunAt(schedule, from = /* @__PURE__ */ new Date()) {
|
|
4220
|
+
if (!schedule.enabled) return null;
|
|
4221
|
+
try {
|
|
4222
|
+
const cron = buildCronFromSchedule(schedule);
|
|
4223
|
+
const tz = schedule.timezone?.trim() || "UTC";
|
|
4224
|
+
const interval = parseExpression(cron, {
|
|
4225
|
+
currentDate: from,
|
|
4226
|
+
tz
|
|
4227
|
+
});
|
|
4228
|
+
return interval.next().toDate();
|
|
4229
|
+
} catch (err) {
|
|
4230
|
+
console.warn("[job-schedules] could not compute next run:", err);
|
|
4231
|
+
return null;
|
|
4232
|
+
}
|
|
4233
|
+
}
|
|
4234
|
+
|
|
4217
4235
|
// src/plugins/jobs/job-runner.ts
|
|
4218
4236
|
var executeJobRef = null;
|
|
4219
4237
|
function registerJobRunnerWorker(cms, deps) {
|
|
@@ -4268,6 +4286,7 @@ function registerJobRunnerWorker(cms, deps) {
|
|
|
4268
4286
|
schedule.lastRunAt = finishedAt;
|
|
4269
4287
|
schedule.lastRunStatus = "success";
|
|
4270
4288
|
schedule.lastRunError = null;
|
|
4289
|
+
schedule.nextRunAt = computeNextRunAt(schedule, finishedAt);
|
|
4271
4290
|
await scheduleRepo.save(schedule);
|
|
4272
4291
|
} catch (e) {
|
|
4273
4292
|
const message = e instanceof Error ? e.message : String(e);
|
|
@@ -4328,7 +4347,7 @@ function serializeSchedule(s) {
|
|
|
4328
4347
|
lastRunAt: s.lastRunAt?.toISOString() ?? null,
|
|
4329
4348
|
lastRunStatus: s.lastRunStatus,
|
|
4330
4349
|
lastRunError: s.lastRunError,
|
|
4331
|
-
nextRunAt: s.nextRunAt?.toISOString() ?? null,
|
|
4350
|
+
nextRunAt: (computeNextRunAt(s) ?? s.nextRunAt)?.toISOString() ?? null,
|
|
4332
4351
|
createdAt: s.createdAt.toISOString(),
|
|
4333
4352
|
updatedAt: s.updatedAt.toISOString()
|
|
4334
4353
|
};
|
|
@@ -4375,6 +4394,9 @@ function createJobScheduleHandlers(apiConfig) {
|
|
|
4375
4394
|
await syncJobScheduleToPgBoss(cms, schedule, async (queueName) => {
|
|
4376
4395
|
await ensureScheduleQueueWorker(cms, queueName);
|
|
4377
4396
|
});
|
|
4397
|
+
const repo = dataSource.getRepository(ScheduleEntity);
|
|
4398
|
+
schedule.nextRunAt = computeNextRunAt(schedule);
|
|
4399
|
+
await repo.save(schedule);
|
|
4378
4400
|
}
|
|
4379
4401
|
return {
|
|
4380
4402
|
async list(req) {
|
|
@@ -7035,6 +7057,13 @@ import path from "path";
|
|
|
7035
7057
|
|
|
7036
7058
|
// src/plugins/social-media/linkedin-client.ts
|
|
7037
7059
|
var RESTLI = "2.0.0";
|
|
7060
|
+
function linkedInHeaders(accessToken, json = false) {
|
|
7061
|
+
return {
|
|
7062
|
+
Authorization: `Bearer ${accessToken}`,
|
|
7063
|
+
"X-Restli-Protocol-Version": RESTLI,
|
|
7064
|
+
...json ? { "Content-Type": "application/json" } : {}
|
|
7065
|
+
};
|
|
7066
|
+
}
|
|
7038
7067
|
async function linkedInGetUserinfo(accessToken) {
|
|
7039
7068
|
const r = await fetch("https://api.linkedin.com/v2/userinfo", {
|
|
7040
7069
|
headers: { Authorization: `Bearer ${accessToken}` }
|
|
@@ -7049,20 +7078,116 @@ async function linkedInGetUserinfo(accessToken) {
|
|
|
7049
7078
|
throw new Error("LinkedIn userinfo: invalid JSON");
|
|
7050
7079
|
}
|
|
7051
7080
|
}
|
|
7081
|
+
async function linkedInFetchOrganizationAcls(accessToken) {
|
|
7082
|
+
const r = await fetch("https://api.linkedin.com/v2/organizationAcls?q=roleAssignee", {
|
|
7083
|
+
headers: linkedInHeaders(accessToken)
|
|
7084
|
+
});
|
|
7085
|
+
const text = await r.text();
|
|
7086
|
+
if (!r.ok) {
|
|
7087
|
+
throw new Error(`LinkedIn organizationAcls failed (${r.status}): ${text.slice(0, 800)}`);
|
|
7088
|
+
}
|
|
7089
|
+
let data;
|
|
7090
|
+
try {
|
|
7091
|
+
data = JSON.parse(text);
|
|
7092
|
+
} catch {
|
|
7093
|
+
throw new Error("LinkedIn organizationAcls: invalid JSON");
|
|
7094
|
+
}
|
|
7095
|
+
return Array.isArray(data.elements) ? data.elements : [];
|
|
7096
|
+
}
|
|
7097
|
+
function organizationDisplayName(org) {
|
|
7098
|
+
if (org.localizedName?.trim()) return org.localizedName.trim();
|
|
7099
|
+
const localized = org.name?.localized;
|
|
7100
|
+
if (localized) {
|
|
7101
|
+
for (const v of Object.values(localized)) {
|
|
7102
|
+
if (v?.trim()) return v.trim();
|
|
7103
|
+
}
|
|
7104
|
+
}
|
|
7105
|
+
if (org.vanityName?.trim()) return org.vanityName.trim();
|
|
7106
|
+
if (org.id != null) return `Organization ${org.id}`;
|
|
7107
|
+
return "Organization";
|
|
7108
|
+
}
|
|
7109
|
+
function organizationWebsite(org) {
|
|
7110
|
+
if (org.localizedWebsite?.trim()) return org.localizedWebsite.trim();
|
|
7111
|
+
const localized = org.website?.localized;
|
|
7112
|
+
if (localized) {
|
|
7113
|
+
for (const v of Object.values(localized)) {
|
|
7114
|
+
if (v?.trim()) return v.trim();
|
|
7115
|
+
}
|
|
7116
|
+
}
|
|
7117
|
+
return void 0;
|
|
7118
|
+
}
|
|
7119
|
+
function linkedInOrganizationUrn(orgIdOrUrn) {
|
|
7120
|
+
const s = String(orgIdOrUrn ?? "").trim();
|
|
7121
|
+
if (s.startsWith("urn:li:organization:")) return s;
|
|
7122
|
+
return `urn:li:organization:${s}`;
|
|
7123
|
+
}
|
|
7124
|
+
function linkedInOrganizationIdFromUrn(organizationUrn) {
|
|
7125
|
+
const m = String(organizationUrn).trim().match(/urn:li:organization:(\d+)/);
|
|
7126
|
+
if (!m?.[1]) throw new Error(`Invalid LinkedIn organization URN: ${organizationUrn}`);
|
|
7127
|
+
return m[1];
|
|
7128
|
+
}
|
|
7129
|
+
async function linkedInGetOrganization(accessToken, orgIdOrUrn) {
|
|
7130
|
+
const orgId = String(orgIdOrUrn).startsWith("urn:") ? linkedInOrganizationIdFromUrn(String(orgIdOrUrn)) : String(orgIdOrUrn).trim();
|
|
7131
|
+
const r = await fetch(`https://api.linkedin.com/v2/organizations/${encodeURIComponent(orgId)}`, {
|
|
7132
|
+
headers: linkedInHeaders(accessToken)
|
|
7133
|
+
});
|
|
7134
|
+
const text = await r.text();
|
|
7135
|
+
if (!r.ok) {
|
|
7136
|
+
throw new Error(`LinkedIn organization ${orgId} failed (${r.status}): ${text.slice(0, 800)}`);
|
|
7137
|
+
}
|
|
7138
|
+
try {
|
|
7139
|
+
return JSON.parse(text);
|
|
7140
|
+
} catch {
|
|
7141
|
+
throw new Error(`LinkedIn organization ${orgId}: invalid JSON`);
|
|
7142
|
+
}
|
|
7143
|
+
}
|
|
7144
|
+
async function linkedInListManagedOrganizations(accessToken) {
|
|
7145
|
+
const acls = await linkedInFetchOrganizationAcls(accessToken);
|
|
7146
|
+
const approved = acls.filter((a) => a.state === "APPROVED" && a.organization?.trim());
|
|
7147
|
+
const byUrn = /* @__PURE__ */ new Map();
|
|
7148
|
+
for (const acl of approved) {
|
|
7149
|
+
const urn = linkedInOrganizationUrn(acl.organization);
|
|
7150
|
+
if (!byUrn.has(urn)) byUrn.set(urn, acl);
|
|
7151
|
+
}
|
|
7152
|
+
const out = [];
|
|
7153
|
+
for (const [urn, acl] of byUrn) {
|
|
7154
|
+
try {
|
|
7155
|
+
const org = await linkedInGetOrganization(accessToken, urn);
|
|
7156
|
+
out.push({
|
|
7157
|
+
urn,
|
|
7158
|
+
id: org.id ?? Number(linkedInOrganizationIdFromUrn(urn)),
|
|
7159
|
+
name: organizationDisplayName(org),
|
|
7160
|
+
vanityName: org.vanityName,
|
|
7161
|
+
website: organizationWebsite(org),
|
|
7162
|
+
role: acl.role
|
|
7163
|
+
});
|
|
7164
|
+
} catch {
|
|
7165
|
+
out.push({
|
|
7166
|
+
urn,
|
|
7167
|
+
id: Number(linkedInOrganizationIdFromUrn(urn)),
|
|
7168
|
+
name: linkedInOrganizationIdFromUrn(urn),
|
|
7169
|
+
role: acl.role
|
|
7170
|
+
});
|
|
7171
|
+
}
|
|
7172
|
+
}
|
|
7173
|
+
out.sort((a, b) => a.name.localeCompare(b.name));
|
|
7174
|
+
return out;
|
|
7175
|
+
}
|
|
7052
7176
|
function personUrn(personSub) {
|
|
7053
7177
|
const s = String(personSub || "").trim();
|
|
7054
7178
|
if (s.startsWith("urn:li:person:")) return s;
|
|
7055
7179
|
return `urn:li:person:${s}`;
|
|
7056
7180
|
}
|
|
7057
|
-
|
|
7058
|
-
const
|
|
7181
|
+
function authorUrnFromParams(authorUrn, personSub) {
|
|
7182
|
+
const direct = String(authorUrn ?? "").trim();
|
|
7183
|
+
if (direct.startsWith("urn:li:")) return direct;
|
|
7184
|
+
return personUrn(String(personSub ?? "").trim());
|
|
7185
|
+
}
|
|
7186
|
+
async function linkedInRegisterImageUpload(accessToken, author) {
|
|
7187
|
+
const owner = authorUrnFromParams(author);
|
|
7059
7188
|
const r = await fetch("https://api.linkedin.com/v2/assets?action=registerUpload", {
|
|
7060
7189
|
method: "POST",
|
|
7061
|
-
headers:
|
|
7062
|
-
Authorization: `Bearer ${accessToken}`,
|
|
7063
|
-
"X-Restli-Protocol-Version": RESTLI,
|
|
7064
|
-
"Content-Type": "application/json"
|
|
7065
|
-
},
|
|
7190
|
+
headers: linkedInHeaders(accessToken, true),
|
|
7066
7191
|
body: JSON.stringify({
|
|
7067
7192
|
registerUploadRequest: {
|
|
7068
7193
|
recipes: ["urn:li:digitalmediaRecipe:feedshare-image"],
|
|
@@ -7110,7 +7235,7 @@ async function linkedInUploadBinary(accessToken, uploadUrl, body, contentType) {
|
|
|
7110
7235
|
}
|
|
7111
7236
|
}
|
|
7112
7237
|
async function linkedInCreateImageShare(params) {
|
|
7113
|
-
const author =
|
|
7238
|
+
const author = authorUrnFromParams(params.authorUrn);
|
|
7114
7239
|
const commentary = params.commentary.slice(0, 2900);
|
|
7115
7240
|
const title = params.title.slice(0, 200);
|
|
7116
7241
|
const description = (params.description ?? title).slice(0, 200);
|
|
@@ -7156,7 +7281,7 @@ async function linkedInCreateImageShare(params) {
|
|
|
7156
7281
|
return { status: r.status, id: data.id };
|
|
7157
7282
|
}
|
|
7158
7283
|
async function linkedInCreateTextShare(params) {
|
|
7159
|
-
const author =
|
|
7284
|
+
const author = authorUrnFromParams(params.authorUrn);
|
|
7160
7285
|
const commentary = params.commentary.slice(0, 2900);
|
|
7161
7286
|
const r = await fetch("https://api.linkedin.com/v2/ugcPosts", {
|
|
7162
7287
|
method: "POST",
|
|
@@ -7490,18 +7615,44 @@ function createSocialMediaHandlers(config) {
|
|
|
7490
7615
|
const enabled = map.enabled !== "false";
|
|
7491
7616
|
const token = (map.linkedin_access_token ?? "").trim();
|
|
7492
7617
|
const sub = (map.linkedin_person_sub ?? "").trim();
|
|
7618
|
+
const orgUrn = (map.linkedin_organization_urn ?? "").trim();
|
|
7493
7619
|
return json({
|
|
7494
7620
|
ok: true,
|
|
7495
|
-
linkedInReady: enabled && Boolean(token) && Boolean(
|
|
7621
|
+
linkedInReady: enabled && Boolean(token) && Boolean(orgUrn),
|
|
7496
7622
|
enabled,
|
|
7497
7623
|
hasToken: Boolean(token),
|
|
7498
|
-
hasPersonSub: Boolean(sub)
|
|
7624
|
+
hasPersonSub: Boolean(sub),
|
|
7625
|
+
hasOrganization: Boolean(orgUrn),
|
|
7626
|
+
organizationName: map.linkedin_organization_name?.trim() || null
|
|
7499
7627
|
});
|
|
7500
7628
|
} catch (e) {
|
|
7501
7629
|
const msg = e instanceof Error ? e.message : "Failed to load status";
|
|
7502
7630
|
return json({ error: msg }, { status: 500 });
|
|
7503
7631
|
}
|
|
7504
7632
|
},
|
|
7633
|
+
async fetchLinkedInOrganizations(req) {
|
|
7634
|
+
const a = await requireAuth(req);
|
|
7635
|
+
if (a) return a;
|
|
7636
|
+
const pe = await requireEntityPermission(req, "settings", "read");
|
|
7637
|
+
if (pe) return pe;
|
|
7638
|
+
let body = {};
|
|
7639
|
+
try {
|
|
7640
|
+
body = await req.json();
|
|
7641
|
+
} catch {
|
|
7642
|
+
}
|
|
7643
|
+
try {
|
|
7644
|
+
const map = await loadGroupMap(dataSource, entityMap, encryptionKey);
|
|
7645
|
+
const token = String(body?.accessToken ?? map.linkedin_access_token ?? "").trim();
|
|
7646
|
+
if (!token) {
|
|
7647
|
+
return json({ error: "LinkedIn access token is required." }, { status: 400 });
|
|
7648
|
+
}
|
|
7649
|
+
const organizations = await linkedInListManagedOrganizations(token);
|
|
7650
|
+
return json({ ok: true, organizations });
|
|
7651
|
+
} catch (e) {
|
|
7652
|
+
const msg = e instanceof Error ? e.message : "LinkedIn organizations request failed";
|
|
7653
|
+
return json({ error: msg }, { status: 502 });
|
|
7654
|
+
}
|
|
7655
|
+
},
|
|
7505
7656
|
async syncLinkedInProfile(req) {
|
|
7506
7657
|
const a = await requireAuth(req);
|
|
7507
7658
|
if (a) return a;
|
|
@@ -7550,10 +7701,12 @@ function createSocialMediaHandlers(config) {
|
|
|
7550
7701
|
return json({ error: "Social media plugin is disabled." }, { status: 400 });
|
|
7551
7702
|
}
|
|
7552
7703
|
const token = (map.linkedin_access_token ?? "").trim();
|
|
7553
|
-
const
|
|
7554
|
-
if (!token || !
|
|
7704
|
+
const authorUrn = (map.linkedin_organization_urn ?? "").trim();
|
|
7705
|
+
if (!token || !authorUrn) {
|
|
7555
7706
|
return json(
|
|
7556
|
-
{
|
|
7707
|
+
{
|
|
7708
|
+
error: "Configure LinkedIn access token and target organization (Plugins \u2192 Social media \u2192 LinkedIn)."
|
|
7709
|
+
},
|
|
7557
7710
|
{ status: 400 }
|
|
7558
7711
|
);
|
|
7559
7712
|
}
|
|
@@ -7594,7 +7747,7 @@ ${excerpt}`).trim().slice(0, 2900);
|
|
|
7594
7747
|
contentType: imagePayload.contentType
|
|
7595
7748
|
});
|
|
7596
7749
|
try {
|
|
7597
|
-
const { uploadUrl, asset } = await linkedInRegisterImageUpload(token,
|
|
7750
|
+
const { uploadUrl, asset } = await linkedInRegisterImageUpload(token, authorUrn);
|
|
7598
7751
|
logLinkedInPublish(publishLog, "image_registered", {
|
|
7599
7752
|
blogId,
|
|
7600
7753
|
asset: truncateForLog(asset, 120),
|
|
@@ -7610,7 +7763,7 @@ ${excerpt}`).trim().slice(0, 2900);
|
|
|
7610
7763
|
logLinkedInPublish(publishLog, "image_uploaded", { blogId, bytes: imagePayload.buffer.length });
|
|
7611
7764
|
const out2 = await linkedInCreateImageShare({
|
|
7612
7765
|
accessToken: token,
|
|
7613
|
-
|
|
7766
|
+
authorUrn,
|
|
7614
7767
|
asset,
|
|
7615
7768
|
commentary,
|
|
7616
7769
|
title,
|
|
@@ -7648,7 +7801,7 @@ ${excerpt}`).trim().slice(0, 2900);
|
|
|
7648
7801
|
});
|
|
7649
7802
|
const out2 = await linkedInCreateTextShare({
|
|
7650
7803
|
accessToken: token,
|
|
7651
|
-
|
|
7804
|
+
authorUrn,
|
|
7652
7805
|
commentary
|
|
7653
7806
|
});
|
|
7654
7807
|
logLinkedInPublish(publishLog, "success", {
|
|
@@ -7687,7 +7840,7 @@ ${excerpt}`).trim().slice(0, 2900);
|
|
|
7687
7840
|
});
|
|
7688
7841
|
const out = await linkedInCreateTextShare({
|
|
7689
7842
|
accessToken: token,
|
|
7690
|
-
|
|
7843
|
+
authorUrn,
|
|
7691
7844
|
commentary
|
|
7692
7845
|
});
|
|
7693
7846
|
logLinkedInPublish(publishLog, "success", {
|
|
@@ -14479,6 +14632,9 @@ function createCmsApiHandler(config) {
|
|
|
14479
14632
|
if (tail === "sync-profile" && m === "POST") {
|
|
14480
14633
|
return socialMediaHandlers.syncLinkedInProfile(req);
|
|
14481
14634
|
}
|
|
14635
|
+
if (tail === "fetch-organizations" && m === "POST") {
|
|
14636
|
+
return socialMediaHandlers.fetchLinkedInOrganizations(req);
|
|
14637
|
+
}
|
|
14482
14638
|
if (tail === "publish-blog" && m === "POST") {
|
|
14483
14639
|
let body;
|
|
14484
14640
|
try {
|