@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.cjs CHANGED
@@ -4421,6 +4421,24 @@ async function queueJobScheduleNow(cms, scheduleId) {
4421
4421
  });
4422
4422
  }
4423
4423
 
4424
+ // src/plugins/jobs/schedule-next-run.ts
4425
+ var import_cron_parser = require("cron-parser");
4426
+ function computeNextRunAt(schedule, from = /* @__PURE__ */ new Date()) {
4427
+ if (!schedule.enabled) return null;
4428
+ try {
4429
+ const cron = buildCronFromSchedule(schedule);
4430
+ const tz = schedule.timezone?.trim() || "UTC";
4431
+ const interval = (0, import_cron_parser.parseExpression)(cron, {
4432
+ currentDate: from,
4433
+ tz
4434
+ });
4435
+ return interval.next().toDate();
4436
+ } catch (err) {
4437
+ console.warn("[job-schedules] could not compute next run:", err);
4438
+ return null;
4439
+ }
4440
+ }
4441
+
4424
4442
  // src/plugins/jobs/job-runner.ts
4425
4443
  var executeJobRef = null;
4426
4444
  function registerJobRunnerWorker(cms, deps) {
@@ -4475,6 +4493,7 @@ function registerJobRunnerWorker(cms, deps) {
4475
4493
  schedule.lastRunAt = finishedAt;
4476
4494
  schedule.lastRunStatus = "success";
4477
4495
  schedule.lastRunError = null;
4496
+ schedule.nextRunAt = computeNextRunAt(schedule, finishedAt);
4478
4497
  await scheduleRepo.save(schedule);
4479
4498
  } catch (e) {
4480
4499
  const message = e instanceof Error ? e.message : String(e);
@@ -4535,7 +4554,7 @@ function serializeSchedule(s) {
4535
4554
  lastRunAt: s.lastRunAt?.toISOString() ?? null,
4536
4555
  lastRunStatus: s.lastRunStatus,
4537
4556
  lastRunError: s.lastRunError,
4538
- nextRunAt: s.nextRunAt?.toISOString() ?? null,
4557
+ nextRunAt: (computeNextRunAt(s) ?? s.nextRunAt)?.toISOString() ?? null,
4539
4558
  createdAt: s.createdAt.toISOString(),
4540
4559
  updatedAt: s.updatedAt.toISOString()
4541
4560
  };
@@ -4582,6 +4601,9 @@ function createJobScheduleHandlers(apiConfig) {
4582
4601
  await syncJobScheduleToPgBoss(cms, schedule, async (queueName) => {
4583
4602
  await ensureScheduleQueueWorker(cms, queueName);
4584
4603
  });
4604
+ const repo = dataSource.getRepository(ScheduleEntity);
4605
+ schedule.nextRunAt = computeNextRunAt(schedule);
4606
+ await repo.save(schedule);
4585
4607
  }
4586
4608
  return {
4587
4609
  async list(req) {
@@ -7242,6 +7264,13 @@ var import_node_path = __toESM(require("path"), 1);
7242
7264
 
7243
7265
  // src/plugins/social-media/linkedin-client.ts
7244
7266
  var RESTLI = "2.0.0";
7267
+ function linkedInHeaders(accessToken, json = false) {
7268
+ return {
7269
+ Authorization: `Bearer ${accessToken}`,
7270
+ "X-Restli-Protocol-Version": RESTLI,
7271
+ ...json ? { "Content-Type": "application/json" } : {}
7272
+ };
7273
+ }
7245
7274
  async function linkedInGetUserinfo(accessToken) {
7246
7275
  const r = await fetch("https://api.linkedin.com/v2/userinfo", {
7247
7276
  headers: { Authorization: `Bearer ${accessToken}` }
@@ -7256,20 +7285,116 @@ async function linkedInGetUserinfo(accessToken) {
7256
7285
  throw new Error("LinkedIn userinfo: invalid JSON");
7257
7286
  }
7258
7287
  }
7288
+ async function linkedInFetchOrganizationAcls(accessToken) {
7289
+ const r = await fetch("https://api.linkedin.com/v2/organizationAcls?q=roleAssignee", {
7290
+ headers: linkedInHeaders(accessToken)
7291
+ });
7292
+ const text = await r.text();
7293
+ if (!r.ok) {
7294
+ throw new Error(`LinkedIn organizationAcls failed (${r.status}): ${text.slice(0, 800)}`);
7295
+ }
7296
+ let data;
7297
+ try {
7298
+ data = JSON.parse(text);
7299
+ } catch {
7300
+ throw new Error("LinkedIn organizationAcls: invalid JSON");
7301
+ }
7302
+ return Array.isArray(data.elements) ? data.elements : [];
7303
+ }
7304
+ function organizationDisplayName(org) {
7305
+ if (org.localizedName?.trim()) return org.localizedName.trim();
7306
+ const localized = org.name?.localized;
7307
+ if (localized) {
7308
+ for (const v of Object.values(localized)) {
7309
+ if (v?.trim()) return v.trim();
7310
+ }
7311
+ }
7312
+ if (org.vanityName?.trim()) return org.vanityName.trim();
7313
+ if (org.id != null) return `Organization ${org.id}`;
7314
+ return "Organization";
7315
+ }
7316
+ function organizationWebsite(org) {
7317
+ if (org.localizedWebsite?.trim()) return org.localizedWebsite.trim();
7318
+ const localized = org.website?.localized;
7319
+ if (localized) {
7320
+ for (const v of Object.values(localized)) {
7321
+ if (v?.trim()) return v.trim();
7322
+ }
7323
+ }
7324
+ return void 0;
7325
+ }
7326
+ function linkedInOrganizationUrn(orgIdOrUrn) {
7327
+ const s = String(orgIdOrUrn ?? "").trim();
7328
+ if (s.startsWith("urn:li:organization:")) return s;
7329
+ return `urn:li:organization:${s}`;
7330
+ }
7331
+ function linkedInOrganizationIdFromUrn(organizationUrn) {
7332
+ const m = String(organizationUrn).trim().match(/urn:li:organization:(\d+)/);
7333
+ if (!m?.[1]) throw new Error(`Invalid LinkedIn organization URN: ${organizationUrn}`);
7334
+ return m[1];
7335
+ }
7336
+ async function linkedInGetOrganization(accessToken, orgIdOrUrn) {
7337
+ const orgId = String(orgIdOrUrn).startsWith("urn:") ? linkedInOrganizationIdFromUrn(String(orgIdOrUrn)) : String(orgIdOrUrn).trim();
7338
+ const r = await fetch(`https://api.linkedin.com/v2/organizations/${encodeURIComponent(orgId)}`, {
7339
+ headers: linkedInHeaders(accessToken)
7340
+ });
7341
+ const text = await r.text();
7342
+ if (!r.ok) {
7343
+ throw new Error(`LinkedIn organization ${orgId} failed (${r.status}): ${text.slice(0, 800)}`);
7344
+ }
7345
+ try {
7346
+ return JSON.parse(text);
7347
+ } catch {
7348
+ throw new Error(`LinkedIn organization ${orgId}: invalid JSON`);
7349
+ }
7350
+ }
7351
+ async function linkedInListManagedOrganizations(accessToken) {
7352
+ const acls = await linkedInFetchOrganizationAcls(accessToken);
7353
+ const approved = acls.filter((a) => a.state === "APPROVED" && a.organization?.trim());
7354
+ const byUrn = /* @__PURE__ */ new Map();
7355
+ for (const acl of approved) {
7356
+ const urn = linkedInOrganizationUrn(acl.organization);
7357
+ if (!byUrn.has(urn)) byUrn.set(urn, acl);
7358
+ }
7359
+ const out = [];
7360
+ for (const [urn, acl] of byUrn) {
7361
+ try {
7362
+ const org = await linkedInGetOrganization(accessToken, urn);
7363
+ out.push({
7364
+ urn,
7365
+ id: org.id ?? Number(linkedInOrganizationIdFromUrn(urn)),
7366
+ name: organizationDisplayName(org),
7367
+ vanityName: org.vanityName,
7368
+ website: organizationWebsite(org),
7369
+ role: acl.role
7370
+ });
7371
+ } catch {
7372
+ out.push({
7373
+ urn,
7374
+ id: Number(linkedInOrganizationIdFromUrn(urn)),
7375
+ name: linkedInOrganizationIdFromUrn(urn),
7376
+ role: acl.role
7377
+ });
7378
+ }
7379
+ }
7380
+ out.sort((a, b) => a.name.localeCompare(b.name));
7381
+ return out;
7382
+ }
7259
7383
  function personUrn(personSub) {
7260
7384
  const s = String(personSub || "").trim();
7261
7385
  if (s.startsWith("urn:li:person:")) return s;
7262
7386
  return `urn:li:person:${s}`;
7263
7387
  }
7264
- async function linkedInRegisterImageUpload(accessToken, personSub) {
7265
- const owner = personUrn(personSub);
7388
+ function authorUrnFromParams(authorUrn, personSub) {
7389
+ const direct = String(authorUrn ?? "").trim();
7390
+ if (direct.startsWith("urn:li:")) return direct;
7391
+ return personUrn(String(personSub ?? "").trim());
7392
+ }
7393
+ async function linkedInRegisterImageUpload(accessToken, author) {
7394
+ const owner = authorUrnFromParams(author);
7266
7395
  const r = await fetch("https://api.linkedin.com/v2/assets?action=registerUpload", {
7267
7396
  method: "POST",
7268
- headers: {
7269
- Authorization: `Bearer ${accessToken}`,
7270
- "X-Restli-Protocol-Version": RESTLI,
7271
- "Content-Type": "application/json"
7272
- },
7397
+ headers: linkedInHeaders(accessToken, true),
7273
7398
  body: JSON.stringify({
7274
7399
  registerUploadRequest: {
7275
7400
  recipes: ["urn:li:digitalmediaRecipe:feedshare-image"],
@@ -7317,7 +7442,7 @@ async function linkedInUploadBinary(accessToken, uploadUrl, body, contentType) {
7317
7442
  }
7318
7443
  }
7319
7444
  async function linkedInCreateImageShare(params) {
7320
- const author = personUrn(params.personSub);
7445
+ const author = authorUrnFromParams(params.authorUrn);
7321
7446
  const commentary = params.commentary.slice(0, 2900);
7322
7447
  const title = params.title.slice(0, 200);
7323
7448
  const description = (params.description ?? title).slice(0, 200);
@@ -7363,7 +7488,7 @@ async function linkedInCreateImageShare(params) {
7363
7488
  return { status: r.status, id: data.id };
7364
7489
  }
7365
7490
  async function linkedInCreateTextShare(params) {
7366
- const author = personUrn(params.personSub);
7491
+ const author = authorUrnFromParams(params.authorUrn);
7367
7492
  const commentary = params.commentary.slice(0, 2900);
7368
7493
  const r = await fetch("https://api.linkedin.com/v2/ugcPosts", {
7369
7494
  method: "POST",
@@ -7697,18 +7822,44 @@ function createSocialMediaHandlers(config) {
7697
7822
  const enabled = map.enabled !== "false";
7698
7823
  const token = (map.linkedin_access_token ?? "").trim();
7699
7824
  const sub = (map.linkedin_person_sub ?? "").trim();
7825
+ const orgUrn = (map.linkedin_organization_urn ?? "").trim();
7700
7826
  return json({
7701
7827
  ok: true,
7702
- linkedInReady: enabled && Boolean(token) && Boolean(sub),
7828
+ linkedInReady: enabled && Boolean(token) && Boolean(orgUrn),
7703
7829
  enabled,
7704
7830
  hasToken: Boolean(token),
7705
- hasPersonSub: Boolean(sub)
7831
+ hasPersonSub: Boolean(sub),
7832
+ hasOrganization: Boolean(orgUrn),
7833
+ organizationName: map.linkedin_organization_name?.trim() || null
7706
7834
  });
7707
7835
  } catch (e) {
7708
7836
  const msg = e instanceof Error ? e.message : "Failed to load status";
7709
7837
  return json({ error: msg }, { status: 500 });
7710
7838
  }
7711
7839
  },
7840
+ async fetchLinkedInOrganizations(req) {
7841
+ const a = await requireAuth(req);
7842
+ if (a) return a;
7843
+ const pe = await requireEntityPermission(req, "settings", "read");
7844
+ if (pe) return pe;
7845
+ let body = {};
7846
+ try {
7847
+ body = await req.json();
7848
+ } catch {
7849
+ }
7850
+ try {
7851
+ const map = await loadGroupMap(dataSource, entityMap, encryptionKey);
7852
+ const token = String(body?.accessToken ?? map.linkedin_access_token ?? "").trim();
7853
+ if (!token) {
7854
+ return json({ error: "LinkedIn access token is required." }, { status: 400 });
7855
+ }
7856
+ const organizations = await linkedInListManagedOrganizations(token);
7857
+ return json({ ok: true, organizations });
7858
+ } catch (e) {
7859
+ const msg = e instanceof Error ? e.message : "LinkedIn organizations request failed";
7860
+ return json({ error: msg }, { status: 502 });
7861
+ }
7862
+ },
7712
7863
  async syncLinkedInProfile(req) {
7713
7864
  const a = await requireAuth(req);
7714
7865
  if (a) return a;
@@ -7757,10 +7908,12 @@ function createSocialMediaHandlers(config) {
7757
7908
  return json({ error: "Social media plugin is disabled." }, { status: 400 });
7758
7909
  }
7759
7910
  const token = (map.linkedin_access_token ?? "").trim();
7760
- const personSub = (map.linkedin_person_sub ?? "").trim();
7761
- if (!token || !personSub) {
7911
+ const authorUrn = (map.linkedin_organization_urn ?? "").trim();
7912
+ if (!token || !authorUrn) {
7762
7913
  return json(
7763
- { error: "Configure LinkedIn access token and sync profile (Plugins \u2192 Social media)." },
7914
+ {
7915
+ error: "Configure LinkedIn access token and target organization (Plugins \u2192 Social media \u2192 LinkedIn)."
7916
+ },
7764
7917
  { status: 400 }
7765
7918
  );
7766
7919
  }
@@ -7801,7 +7954,7 @@ ${excerpt}`).trim().slice(0, 2900);
7801
7954
  contentType: imagePayload.contentType
7802
7955
  });
7803
7956
  try {
7804
- const { uploadUrl, asset } = await linkedInRegisterImageUpload(token, personSub);
7957
+ const { uploadUrl, asset } = await linkedInRegisterImageUpload(token, authorUrn);
7805
7958
  logLinkedInPublish(publishLog, "image_registered", {
7806
7959
  blogId,
7807
7960
  asset: truncateForLog(asset, 120),
@@ -7817,7 +7970,7 @@ ${excerpt}`).trim().slice(0, 2900);
7817
7970
  logLinkedInPublish(publishLog, "image_uploaded", { blogId, bytes: imagePayload.buffer.length });
7818
7971
  const out2 = await linkedInCreateImageShare({
7819
7972
  accessToken: token,
7820
- personSub,
7973
+ authorUrn,
7821
7974
  asset,
7822
7975
  commentary,
7823
7976
  title,
@@ -7855,7 +8008,7 @@ ${excerpt}`).trim().slice(0, 2900);
7855
8008
  });
7856
8009
  const out2 = await linkedInCreateTextShare({
7857
8010
  accessToken: token,
7858
- personSub,
8011
+ authorUrn,
7859
8012
  commentary
7860
8013
  });
7861
8014
  logLinkedInPublish(publishLog, "success", {
@@ -7894,7 +8047,7 @@ ${excerpt}`).trim().slice(0, 2900);
7894
8047
  });
7895
8048
  const out = await linkedInCreateTextShare({
7896
8049
  accessToken: token,
7897
- personSub,
8050
+ authorUrn,
7898
8051
  commentary
7899
8052
  });
7900
8053
  logLinkedInPublish(publishLog, "success", {
@@ -14662,6 +14815,9 @@ function createCmsApiHandler(config) {
14662
14815
  if (tail === "sync-profile" && m === "POST") {
14663
14816
  return socialMediaHandlers.syncLinkedInProfile(req);
14664
14817
  }
14818
+ if (tail === "fetch-organizations" && m === "POST") {
14819
+ return socialMediaHandlers.fetchLinkedInOrganizations(req);
14820
+ }
14665
14821
  if (tail === "publish-blog" && m === "POST") {
14666
14822
  let body;
14667
14823
  try {