@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/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
- async function linkedInRegisterImageUpload(accessToken, personSub) {
7058
- const owner = personUrn(personSub);
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 = personUrn(params.personSub);
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 = personUrn(params.personSub);
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(sub),
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 personSub = (map.linkedin_person_sub ?? "").trim();
7554
- if (!token || !personSub) {
7704
+ const authorUrn = (map.linkedin_organization_urn ?? "").trim();
7705
+ if (!token || !authorUrn) {
7555
7706
  return json(
7556
- { error: "Configure LinkedIn access token and sync profile (Plugins \u2192 Social media)." },
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, personSub);
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
- personSub,
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
- personSub,
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
- personSub,
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 {