@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.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
- async function linkedInRegisterImageUpload(accessToken, personSub) {
7986
- const owner = personUrn(personSub);
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 = personUrn(params.personSub);
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 = personUrn(params.personSub);
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(sub),
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 personSub = (map.linkedin_person_sub ?? "").trim();
8482
- if (!token || !personSub) {
8610
+ const authorUrn = (map.linkedin_organization_urn ?? "").trim();
8611
+ if (!token || !authorUrn) {
8483
8612
  return json(
8484
- { error: "Configure LinkedIn access token and sync profile (Plugins \u2192 Social media)." },
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, personSub);
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
- personSub,
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
- personSub,
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
- personSub,
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 {