@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/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
- async function linkedInRegisterImageUpload(accessToken, personSub) {
7968
- const owner = personUrn(personSub);
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 = personUrn(params.personSub);
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 = personUrn(params.personSub);
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(sub),
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 personSub = (map.linkedin_person_sub ?? "").trim();
8464
- if (!token || !personSub) {
8592
+ const authorUrn = (map.linkedin_organization_urn ?? "").trim();
8593
+ if (!token || !authorUrn) {
8465
8594
  return json(
8466
- { error: "Configure LinkedIn access token and sync profile (Plugins \u2192 Social media)." },
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, personSub);
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
- personSub,
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
- personSub,
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
- personSub,
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 {