@infuro/cms-core 1.0.28 → 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
@@ -192,6 +192,81 @@ var init_email_queue = __esm({
192
192
  }
193
193
  });
194
194
 
195
+ // src/plugins/pg-boss/pg-boss-service.ts
196
+ var pg_boss_service_exports = {};
197
+ __export(pg_boss_service_exports, {
198
+ JOB_RUNNER_QUEUE: () => JOB_RUNNER_QUEUE,
199
+ PgBossService: () => PgBossService
200
+ });
201
+ import PgBoss from "pg-boss";
202
+ var JOB_RUNNER_QUEUE, PgBossService;
203
+ var init_pg_boss_service = __esm({
204
+ "src/plugins/pg-boss/pg-boss-service.ts"() {
205
+ "use strict";
206
+ JOB_RUNNER_QUEUE = "job-runner";
207
+ PgBossService = class {
208
+ boss;
209
+ started = false;
210
+ workRegistered = /* @__PURE__ */ new Set();
211
+ constructor(connectionString, schema) {
212
+ this.boss = new PgBoss({
213
+ connectionString,
214
+ ...schema?.trim() ? { schema: schema.trim() } : {},
215
+ schedule: true
216
+ });
217
+ this.boss.on("error", (err) => {
218
+ console.error("[pg-boss]", err);
219
+ });
220
+ }
221
+ async start() {
222
+ if (this.started) return;
223
+ await this.boss.start();
224
+ await this.ensureQueue(JOB_RUNNER_QUEUE);
225
+ this.started = true;
226
+ }
227
+ async stop() {
228
+ if (!this.started) return;
229
+ await this.boss.stop({ graceful: true, timeout: 1e4 });
230
+ this.started = false;
231
+ this.workRegistered.clear();
232
+ }
233
+ get raw() {
234
+ return this.boss;
235
+ }
236
+ async ensureQueue(name) {
237
+ const existing = await this.boss.getQueue(name);
238
+ if (!existing) {
239
+ await this.boss.createQueue(name);
240
+ }
241
+ }
242
+ async send(queue, data) {
243
+ await this.ensureQueue(queue);
244
+ return this.boss.send(queue, data, { retryLimit: 2 });
245
+ }
246
+ async schedule(name, cron, data, options) {
247
+ await this.ensureQueue(name);
248
+ await this.boss.schedule(name, cron, data, options);
249
+ }
250
+ async unschedule(name) {
251
+ try {
252
+ await this.boss.unschedule(name);
253
+ } catch {
254
+ }
255
+ }
256
+ async registerWork(queue, handler) {
257
+ if (this.workRegistered.has(queue)) return;
258
+ await this.ensureQueue(queue);
259
+ await this.boss.work(queue, async (jobs) => {
260
+ for (const job of jobs) {
261
+ await handler(job.data);
262
+ }
263
+ });
264
+ this.workRegistered.add(queue);
265
+ }
266
+ };
267
+ }
268
+ });
269
+
195
270
  // src/plugins/erp/erp-response-map.ts
196
271
  function pickString(o, keys) {
197
272
  for (const k of keys) {
@@ -7324,6 +7399,159 @@ RssFeed = __decorateClass([
7324
7399
  Entity42("rss_feeds")
7325
7400
  ], RssFeed);
7326
7401
 
7402
+ // src/entities/job-schedule.entity.ts
7403
+ import {
7404
+ Entity as Entity44,
7405
+ PrimaryGeneratedColumn as PrimaryGeneratedColumn44,
7406
+ Column as Column44,
7407
+ CreateDateColumn as CreateDateColumn4,
7408
+ UpdateDateColumn as UpdateDateColumn2,
7409
+ OneToMany as OneToMany18
7410
+ } from "typeorm";
7411
+
7412
+ // src/entities/job-schedule-run.entity.ts
7413
+ import {
7414
+ Entity as Entity43,
7415
+ PrimaryGeneratedColumn as PrimaryGeneratedColumn43,
7416
+ Column as Column43,
7417
+ CreateDateColumn as CreateDateColumn3,
7418
+ ManyToOne as ManyToOne29,
7419
+ JoinColumn as JoinColumn29
7420
+ } from "typeorm";
7421
+ var JobScheduleRun = class {
7422
+ id;
7423
+ scheduleId;
7424
+ schedule;
7425
+ startedAt;
7426
+ finishedAt;
7427
+ status;
7428
+ error;
7429
+ result;
7430
+ triggeredBy;
7431
+ createdAt;
7432
+ };
7433
+ __decorateClass([
7434
+ PrimaryGeneratedColumn43("uuid")
7435
+ ], JobScheduleRun.prototype, "id", 2);
7436
+ __decorateClass([
7437
+ Column43({ type: "uuid" })
7438
+ ], JobScheduleRun.prototype, "scheduleId", 2);
7439
+ __decorateClass([
7440
+ ManyToOne29(() => JobSchedule, (s) => s.runs, { onDelete: "CASCADE" }),
7441
+ JoinColumn29({ name: "scheduleId" })
7442
+ ], JobScheduleRun.prototype, "schedule", 2);
7443
+ __decorateClass([
7444
+ Column43({ type: "timestamp" })
7445
+ ], JobScheduleRun.prototype, "startedAt", 2);
7446
+ __decorateClass([
7447
+ Column43({ type: "timestamp", nullable: true })
7448
+ ], JobScheduleRun.prototype, "finishedAt", 2);
7449
+ __decorateClass([
7450
+ Column43({ type: "varchar", length: 32 })
7451
+ ], JobScheduleRun.prototype, "status", 2);
7452
+ __decorateClass([
7453
+ Column43({ type: "text", nullable: true })
7454
+ ], JobScheduleRun.prototype, "error", 2);
7455
+ __decorateClass([
7456
+ Column43({ type: "jsonb", nullable: true })
7457
+ ], JobScheduleRun.prototype, "result", 2);
7458
+ __decorateClass([
7459
+ Column43({ type: "varchar", length: 16, default: "schedule" })
7460
+ ], JobScheduleRun.prototype, "triggeredBy", 2);
7461
+ __decorateClass([
7462
+ CreateDateColumn3()
7463
+ ], JobScheduleRun.prototype, "createdAt", 2);
7464
+ JobScheduleRun = __decorateClass([
7465
+ Entity43("job_schedule_runs")
7466
+ ], JobScheduleRun);
7467
+
7468
+ // src/entities/job-schedule.entity.ts
7469
+ var JobSchedule = class {
7470
+ id;
7471
+ name;
7472
+ jobType;
7473
+ enabled;
7474
+ scheduleMode;
7475
+ intervalMinutes;
7476
+ runAtTime;
7477
+ runOnDays;
7478
+ timezone;
7479
+ cronExpression;
7480
+ payload;
7481
+ authorId;
7482
+ pgBossScheduleName;
7483
+ lastRunAt;
7484
+ lastRunStatus;
7485
+ lastRunError;
7486
+ nextRunAt;
7487
+ runs;
7488
+ createdAt;
7489
+ updatedAt;
7490
+ };
7491
+ __decorateClass([
7492
+ PrimaryGeneratedColumn44("uuid")
7493
+ ], JobSchedule.prototype, "id", 2);
7494
+ __decorateClass([
7495
+ Column44({ type: "text" })
7496
+ ], JobSchedule.prototype, "name", 2);
7497
+ __decorateClass([
7498
+ Column44({ type: "varchar", length: 64, default: "blog_generate" })
7499
+ ], JobSchedule.prototype, "jobType", 2);
7500
+ __decorateClass([
7501
+ Column44({ type: "boolean", default: false })
7502
+ ], JobSchedule.prototype, "enabled", 2);
7503
+ __decorateClass([
7504
+ Column44({ type: "varchar", length: 32, default: "daily" })
7505
+ ], JobSchedule.prototype, "scheduleMode", 2);
7506
+ __decorateClass([
7507
+ Column44({ type: "int", nullable: true })
7508
+ ], JobSchedule.prototype, "intervalMinutes", 2);
7509
+ __decorateClass([
7510
+ Column44({ type: "varchar", length: 8, nullable: true })
7511
+ ], JobSchedule.prototype, "runAtTime", 2);
7512
+ __decorateClass([
7513
+ Column44({ type: "jsonb", nullable: true })
7514
+ ], JobSchedule.prototype, "runOnDays", 2);
7515
+ __decorateClass([
7516
+ Column44({ type: "varchar", length: 64, default: "UTC" })
7517
+ ], JobSchedule.prototype, "timezone", 2);
7518
+ __decorateClass([
7519
+ Column44({ type: "text", nullable: true })
7520
+ ], JobSchedule.prototype, "cronExpression", 2);
7521
+ __decorateClass([
7522
+ Column44({ type: "jsonb", default: {} })
7523
+ ], JobSchedule.prototype, "payload", 2);
7524
+ __decorateClass([
7525
+ Column44({ type: "int", nullable: true })
7526
+ ], JobSchedule.prototype, "authorId", 2);
7527
+ __decorateClass([
7528
+ Column44({ type: "varchar", length: 128, unique: true })
7529
+ ], JobSchedule.prototype, "pgBossScheduleName", 2);
7530
+ __decorateClass([
7531
+ Column44({ type: "timestamp", nullable: true })
7532
+ ], JobSchedule.prototype, "lastRunAt", 2);
7533
+ __decorateClass([
7534
+ Column44({ type: "varchar", length: 32, nullable: true })
7535
+ ], JobSchedule.prototype, "lastRunStatus", 2);
7536
+ __decorateClass([
7537
+ Column44({ type: "text", nullable: true })
7538
+ ], JobSchedule.prototype, "lastRunError", 2);
7539
+ __decorateClass([
7540
+ Column44({ type: "timestamp", nullable: true })
7541
+ ], JobSchedule.prototype, "nextRunAt", 2);
7542
+ __decorateClass([
7543
+ OneToMany18(() => JobScheduleRun, (r) => r.schedule)
7544
+ ], JobSchedule.prototype, "runs", 2);
7545
+ __decorateClass([
7546
+ CreateDateColumn4()
7547
+ ], JobSchedule.prototype, "createdAt", 2);
7548
+ __decorateClass([
7549
+ UpdateDateColumn2()
7550
+ ], JobSchedule.prototype, "updatedAt", 2);
7551
+ JobSchedule = __decorateClass([
7552
+ Entity44("job_schedules")
7553
+ ], JobSchedule);
7554
+
7327
7555
  // src/entities/index.ts
7328
7556
  var CMS_ENTITY_MAP = {
7329
7557
  users: User,
@@ -7367,7 +7595,9 @@ var CMS_ENTITY_MAP = {
7367
7595
  llm_agents: LlmAgent,
7368
7596
  llm_agent_knowledge_documents: LlmAgentKnowledgeDocument,
7369
7597
  rss_feeds: RssFeed,
7370
- rss_articles: RssArticle
7598
+ rss_articles: RssArticle,
7599
+ job_schedules: JobSchedule,
7600
+ job_schedule_runs: JobScheduleRun
7371
7601
  };
7372
7602
 
7373
7603
  // src/plugins/blog-generator/blog-generator-service.ts
@@ -7715,6 +7945,13 @@ import path from "path";
7715
7945
 
7716
7946
  // src/plugins/social-media/linkedin-client.ts
7717
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
+ }
7718
7955
  async function linkedInGetUserinfo(accessToken) {
7719
7956
  const r = await fetch("https://api.linkedin.com/v2/userinfo", {
7720
7957
  headers: { Authorization: `Bearer ${accessToken}` }
@@ -7729,20 +7966,116 @@ async function linkedInGetUserinfo(accessToken) {
7729
7966
  throw new Error("LinkedIn userinfo: invalid JSON");
7730
7967
  }
7731
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
+ }
7732
8064
  function personUrn(personSub) {
7733
8065
  const s = String(personSub || "").trim();
7734
8066
  if (s.startsWith("urn:li:person:")) return s;
7735
8067
  return `urn:li:person:${s}`;
7736
8068
  }
7737
- async function linkedInRegisterImageUpload(accessToken, personSub) {
7738
- 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);
7739
8076
  const r = await fetch("https://api.linkedin.com/v2/assets?action=registerUpload", {
7740
8077
  method: "POST",
7741
- headers: {
7742
- Authorization: `Bearer ${accessToken}`,
7743
- "X-Restli-Protocol-Version": RESTLI,
7744
- "Content-Type": "application/json"
7745
- },
8078
+ headers: linkedInHeaders(accessToken, true),
7746
8079
  body: JSON.stringify({
7747
8080
  registerUploadRequest: {
7748
8081
  recipes: ["urn:li:digitalmediaRecipe:feedshare-image"],
@@ -7790,7 +8123,7 @@ async function linkedInUploadBinary(accessToken, uploadUrl, body, contentType) {
7790
8123
  }
7791
8124
  }
7792
8125
  async function linkedInCreateImageShare(params) {
7793
- const author = personUrn(params.personSub);
8126
+ const author = authorUrnFromParams(params.authorUrn);
7794
8127
  const commentary = params.commentary.slice(0, 2900);
7795
8128
  const title = params.title.slice(0, 200);
7796
8129
  const description = (params.description ?? title).slice(0, 200);
@@ -7836,7 +8169,7 @@ async function linkedInCreateImageShare(params) {
7836
8169
  return { status: r.status, id: data.id };
7837
8170
  }
7838
8171
  async function linkedInCreateTextShare(params) {
7839
- const author = personUrn(params.personSub);
8172
+ const author = authorUrnFromParams(params.authorUrn);
7840
8173
  const commentary = params.commentary.slice(0, 2900);
7841
8174
  const r = await fetch("https://api.linkedin.com/v2/ugcPosts", {
7842
8175
  method: "POST",
@@ -8170,18 +8503,44 @@ function createSocialMediaHandlers(config) {
8170
8503
  const enabled = map.enabled !== "false";
8171
8504
  const token = (map.linkedin_access_token ?? "").trim();
8172
8505
  const sub = (map.linkedin_person_sub ?? "").trim();
8506
+ const orgUrn = (map.linkedin_organization_urn ?? "").trim();
8173
8507
  return json({
8174
8508
  ok: true,
8175
- linkedInReady: enabled && Boolean(token) && Boolean(sub),
8509
+ linkedInReady: enabled && Boolean(token) && Boolean(orgUrn),
8176
8510
  enabled,
8177
8511
  hasToken: Boolean(token),
8178
- hasPersonSub: Boolean(sub)
8512
+ hasPersonSub: Boolean(sub),
8513
+ hasOrganization: Boolean(orgUrn),
8514
+ organizationName: map.linkedin_organization_name?.trim() || null
8179
8515
  });
8180
8516
  } catch (e) {
8181
8517
  const msg = e instanceof Error ? e.message : "Failed to load status";
8182
8518
  return json({ error: msg }, { status: 500 });
8183
8519
  }
8184
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
+ },
8185
8544
  async syncLinkedInProfile(req) {
8186
8545
  const a = await requireAuth(req);
8187
8546
  if (a) return a;
@@ -8230,10 +8589,12 @@ function createSocialMediaHandlers(config) {
8230
8589
  return json({ error: "Social media plugin is disabled." }, { status: 400 });
8231
8590
  }
8232
8591
  const token = (map.linkedin_access_token ?? "").trim();
8233
- const personSub = (map.linkedin_person_sub ?? "").trim();
8234
- if (!token || !personSub) {
8592
+ const authorUrn = (map.linkedin_organization_urn ?? "").trim();
8593
+ if (!token || !authorUrn) {
8235
8594
  return json(
8236
- { 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
+ },
8237
8598
  { status: 400 }
8238
8599
  );
8239
8600
  }
@@ -8274,7 +8635,7 @@ ${excerpt}`).trim().slice(0, 2900);
8274
8635
  contentType: imagePayload.contentType
8275
8636
  });
8276
8637
  try {
8277
- const { uploadUrl, asset } = await linkedInRegisterImageUpload(token, personSub);
8638
+ const { uploadUrl, asset } = await linkedInRegisterImageUpload(token, authorUrn);
8278
8639
  logLinkedInPublish(publishLog, "image_registered", {
8279
8640
  blogId,
8280
8641
  asset: truncateForLog(asset, 120),
@@ -8290,7 +8651,7 @@ ${excerpt}`).trim().slice(0, 2900);
8290
8651
  logLinkedInPublish(publishLog, "image_uploaded", { blogId, bytes: imagePayload.buffer.length });
8291
8652
  const out2 = await linkedInCreateImageShare({
8292
8653
  accessToken: token,
8293
- personSub,
8654
+ authorUrn,
8294
8655
  asset,
8295
8656
  commentary,
8296
8657
  title,
@@ -8328,7 +8689,7 @@ ${excerpt}`).trim().slice(0, 2900);
8328
8689
  });
8329
8690
  const out2 = await linkedInCreateTextShare({
8330
8691
  accessToken: token,
8331
- personSub,
8692
+ authorUrn,
8332
8693
  commentary
8333
8694
  });
8334
8695
  logLinkedInPublish(publishLog, "success", {
@@ -8367,7 +8728,7 @@ ${excerpt}`).trim().slice(0, 2900);
8367
8728
  });
8368
8729
  const out = await linkedInCreateTextShare({
8369
8730
  accessToken: token,
8370
- personSub,
8731
+ authorUrn,
8371
8732
  commentary
8372
8733
  });
8373
8734
  logLinkedInPublish(publishLog, "success", {
@@ -8570,6 +8931,323 @@ ${excerpt}`).trim().slice(0, 2200);
8570
8931
  };
8571
8932
  }
8572
8933
 
8934
+ // src/api/job-schedule-handlers.ts
8935
+ import { randomUUID } from "crypto";
8936
+
8937
+ // src/plugins/jobs/schedule-cron.ts
8938
+ function pgBossScheduleNameForId(scheduleId) {
8939
+ return `schedule:${scheduleId}`;
8940
+ }
8941
+ function buildCronFromSchedule(schedule) {
8942
+ if (schedule.scheduleMode === "cron" && schedule.cronExpression?.trim()) {
8943
+ return schedule.cronExpression.trim();
8944
+ }
8945
+ const time = parseRunAtTime(schedule.runAtTime);
8946
+ const minute = time?.minute ?? 0;
8947
+ const hour = time?.hour ?? 9;
8948
+ if (schedule.scheduleMode === "interval") {
8949
+ const mins = schedule.intervalMinutes ?? 60;
8950
+ if (mins >= 60 && mins % 60 === 0) {
8951
+ const h = Math.max(1, Math.min(23, Math.floor(mins / 60)));
8952
+ if (h === 1) return `${minute} * * * *`;
8953
+ return `${minute} */${h} * * *`;
8954
+ }
8955
+ if (mins <= 59) {
8956
+ return `*/${Math.max(1, mins)} * * * *`;
8957
+ }
8958
+ return `${minute} * * * *`;
8959
+ }
8960
+ if (schedule.scheduleMode === "weekly") {
8961
+ const days = Array.isArray(schedule.runOnDays) && schedule.runOnDays.length > 0 ? schedule.runOnDays.filter((d) => d >= 0 && d <= 6).join(",") : "1";
8962
+ return `${minute} ${hour} * * ${days}`;
8963
+ }
8964
+ return `${minute} ${hour} * * *`;
8965
+ }
8966
+ function parseRunAtTime(runAtTime) {
8967
+ if (!runAtTime?.trim()) return null;
8968
+ const m = runAtTime.trim().match(/^(\d{1,2}):(\d{2})$/);
8969
+ if (!m) return null;
8970
+ const hour = parseInt(m[1], 10);
8971
+ const minute = parseInt(m[2], 10);
8972
+ if (hour < 0 || hour > 23 || minute < 0 || minute > 59) return null;
8973
+ return { hour, minute };
8974
+ }
8975
+ function validateScheduleInput(schedule) {
8976
+ if (schedule.scheduleMode === "cron") {
8977
+ if (!schedule.cronExpression?.trim()) return "cronExpression is required for cron mode";
8978
+ return null;
8979
+ }
8980
+ if (schedule.scheduleMode === "interval") {
8981
+ const mins = schedule.intervalMinutes;
8982
+ if (mins == null || !Number.isFinite(mins) || mins < 1 || mins > 10080) {
8983
+ return "intervalMinutes must be between 1 and 10080";
8984
+ }
8985
+ return null;
8986
+ }
8987
+ if (schedule.runAtTime?.trim()) {
8988
+ if (!parseRunAtTime(schedule.runAtTime)) return "runAtTime must be HH:mm (24h)";
8989
+ }
8990
+ return null;
8991
+ }
8992
+
8993
+ // src/plugins/jobs/schedule-sync.ts
8994
+ async function syncJobScheduleToPgBoss(cms, schedule, onRegisterQueue) {
8995
+ const boss = cms.getPlugin("pg_boss");
8996
+ if (!boss) return;
8997
+ const name = schedule.pgBossScheduleName;
8998
+ await boss.unschedule(name);
8999
+ if (!schedule.enabled) return;
9000
+ const cron = buildCronFromSchedule(schedule);
9001
+ const data = {
9002
+ scheduleId: schedule.id,
9003
+ triggeredBy: "schedule"
9004
+ };
9005
+ await boss.schedule(name, cron, data, { tz: schedule.timezone || "UTC" });
9006
+ if (onRegisterQueue) {
9007
+ await onRegisterQueue(name);
9008
+ }
9009
+ }
9010
+ async function queueJobScheduleNow(cms, scheduleId) {
9011
+ const boss = cms.getPlugin("pg_boss");
9012
+ if (!boss) {
9013
+ throw new Error("pg-boss is not configured (DATABASE_URL required)");
9014
+ }
9015
+ const { JOB_RUNNER_QUEUE: JOB_RUNNER_QUEUE2 } = await Promise.resolve().then(() => (init_pg_boss_service(), pg_boss_service_exports));
9016
+ await boss.send(JOB_RUNNER_QUEUE2, {
9017
+ scheduleId,
9018
+ triggeredBy: "manual"
9019
+ });
9020
+ }
9021
+
9022
+ // src/plugins/jobs/job-runner.ts
9023
+ init_pg_boss_service();
9024
+
9025
+ // src/plugins/jobs/blog-generate-job.ts
9026
+ import { In as In3 } from "typeorm";
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
+
9046
+ // src/plugins/jobs/job-runner.ts
9047
+ var executeJobRef = null;
9048
+ async function ensureScheduleQueueWorker(cms, queueName) {
9049
+ const boss = cms.getPlugin("pg_boss");
9050
+ if (!boss || !executeJobRef) return;
9051
+ await boss.registerWork(queueName, executeJobRef);
9052
+ }
9053
+
9054
+ // src/api/job-schedule-handlers.ts
9055
+ function serializeSchedule(s) {
9056
+ return {
9057
+ id: s.id,
9058
+ name: s.name,
9059
+ jobType: s.jobType,
9060
+ enabled: s.enabled,
9061
+ scheduleMode: s.scheduleMode,
9062
+ intervalMinutes: s.intervalMinutes,
9063
+ runAtTime: s.runAtTime,
9064
+ runOnDays: s.runOnDays,
9065
+ timezone: s.timezone,
9066
+ cronExpression: s.cronExpression,
9067
+ cronResolved: buildCronFromSchedule(s),
9068
+ payload: s.payload ?? {},
9069
+ authorId: s.authorId,
9070
+ pgBossScheduleName: s.pgBossScheduleName,
9071
+ lastRunAt: s.lastRunAt?.toISOString() ?? null,
9072
+ lastRunStatus: s.lastRunStatus,
9073
+ lastRunError: s.lastRunError,
9074
+ nextRunAt: (computeNextRunAt(s) ?? s.nextRunAt)?.toISOString() ?? null,
9075
+ createdAt: s.createdAt.toISOString(),
9076
+ updatedAt: s.updatedAt.toISOString()
9077
+ };
9078
+ }
9079
+ function serializeRun(r) {
9080
+ return {
9081
+ id: r.id,
9082
+ scheduleId: r.scheduleId,
9083
+ startedAt: r.startedAt.toISOString(),
9084
+ finishedAt: r.finishedAt?.toISOString() ?? null,
9085
+ status: r.status,
9086
+ error: r.error,
9087
+ result: r.result,
9088
+ triggeredBy: r.triggeredBy,
9089
+ createdAt: r.createdAt.toISOString()
9090
+ };
9091
+ }
9092
+ function parseScheduleBody(body, existing) {
9093
+ const scheduleMode = typeof body.scheduleMode === "string" ? body.scheduleMode : existing?.scheduleMode ?? "daily";
9094
+ const patch = {
9095
+ name: typeof body.name === "string" ? body.name.trim().slice(0, 500) : existing?.name,
9096
+ jobType: body.jobType === "blog_generate" ? "blog_generate" : existing?.jobType ?? "blog_generate",
9097
+ enabled: typeof body.enabled === "boolean" ? body.enabled : existing?.enabled ?? false,
9098
+ scheduleMode,
9099
+ intervalMinutes: typeof body.intervalMinutes === "number" ? body.intervalMinutes : body.intervalMinutes != null ? Number(body.intervalMinutes) : existing?.intervalMinutes ?? null,
9100
+ runAtTime: typeof body.runAtTime === "string" ? body.runAtTime.trim() || null : body.runAtTime === null ? null : existing?.runAtTime ?? null,
9101
+ runOnDays: Array.isArray(body.runOnDays) ? body.runOnDays.map((d) => Number(d)).filter((d) => Number.isFinite(d)) : existing?.runOnDays ?? null,
9102
+ timezone: typeof body.timezone === "string" && body.timezone.trim() ? body.timezone.trim().slice(0, 64) : existing?.timezone ?? "UTC",
9103
+ cronExpression: typeof body.cronExpression === "string" ? body.cronExpression.trim() || null : existing?.cronExpression ?? null,
9104
+ payload: body.payload != null && typeof body.payload === "object" && !Array.isArray(body.payload) ? body.payload : existing?.payload ?? {},
9105
+ authorId: typeof body.authorId === "number" ? body.authorId : body.authorId != null ? Number(body.authorId) : existing?.authorId ?? null
9106
+ };
9107
+ return patch;
9108
+ }
9109
+ function createJobScheduleHandlers(apiConfig) {
9110
+ const { dataSource, entityMap, json, requireAuth, requireEntityPermission, getCms } = apiConfig;
9111
+ const ScheduleEntity = entityMap.job_schedules ?? JobSchedule;
9112
+ const RunEntity = entityMap.job_schedule_runs ?? JobScheduleRun;
9113
+ async function perm(req, action) {
9114
+ if (!requireEntityPermission) return null;
9115
+ return requireEntityPermission(req, "blogs", action);
9116
+ }
9117
+ async function syncAfterSave(cms, schedule) {
9118
+ await syncJobScheduleToPgBoss(cms, schedule, async (queueName) => {
9119
+ await ensureScheduleQueueWorker(cms, queueName);
9120
+ });
9121
+ const repo = dataSource.getRepository(ScheduleEntity);
9122
+ schedule.nextRunAt = computeNextRunAt(schedule);
9123
+ await repo.save(schedule);
9124
+ }
9125
+ return {
9126
+ async list(req) {
9127
+ const a = await requireAuth(req);
9128
+ if (a) return a;
9129
+ const pe = await perm(req, "read");
9130
+ if (pe) return pe;
9131
+ const list = await dataSource.getRepository(ScheduleEntity).find({
9132
+ order: { updatedAt: "DESC" }
9133
+ });
9134
+ return json({ schedules: list.map(serializeSchedule) });
9135
+ },
9136
+ async getById(req, id) {
9137
+ const a = await requireAuth(req);
9138
+ if (a) return a;
9139
+ const pe = await perm(req, "read");
9140
+ if (pe) return pe;
9141
+ const row = await dataSource.getRepository(ScheduleEntity).findOne({ where: { id } });
9142
+ if (!row) return json({ error: "Schedule not found" }, { status: 404 });
9143
+ const runs = await dataSource.getRepository(RunEntity).find({
9144
+ where: { scheduleId: id },
9145
+ order: { startedAt: "DESC" },
9146
+ take: 20
9147
+ });
9148
+ return json({ schedule: serializeSchedule(row), runs: runs.map(serializeRun) });
9149
+ },
9150
+ async create(req) {
9151
+ const a = await requireAuth(req);
9152
+ if (a) return a;
9153
+ const pe = await perm(req, "create");
9154
+ if (pe) return pe;
9155
+ if (!getCms) return json({ error: "CMS not configured" }, { status: 503 });
9156
+ let body;
9157
+ try {
9158
+ body = await req.json();
9159
+ } catch {
9160
+ return json({ error: "Invalid JSON body" }, { status: 400 });
9161
+ }
9162
+ const patch = parseScheduleBody(body);
9163
+ if (!patch.name) return json({ error: "name is required" }, { status: 400 });
9164
+ const draft = Object.assign(new JobSchedule(), patch);
9165
+ const validationError = validateScheduleInput(draft);
9166
+ if (validationError) return json({ error: validationError }, { status: 400 });
9167
+ const repo = dataSource.getRepository(ScheduleEntity);
9168
+ let row = repo.create({
9169
+ ...patch,
9170
+ pgBossScheduleName: `pending-${randomUUID()}`
9171
+ });
9172
+ row = await repo.save(row);
9173
+ row.pgBossScheduleName = pgBossScheduleNameForId(row.id);
9174
+ row = await repo.save(row);
9175
+ const cms = await getCms();
9176
+ await syncAfterSave(cms, row);
9177
+ return json({ schedule: serializeSchedule(row) }, { status: 201 });
9178
+ },
9179
+ async update(req, id) {
9180
+ const a = await requireAuth(req);
9181
+ if (a) return a;
9182
+ const pe = await perm(req, "update");
9183
+ if (pe) return pe;
9184
+ if (!getCms) return json({ error: "CMS not configured" }, { status: 503 });
9185
+ const repo = dataSource.getRepository(ScheduleEntity);
9186
+ const existing = await repo.findOne({ where: { id } });
9187
+ if (!existing) return json({ error: "Schedule not found" }, { status: 404 });
9188
+ let body;
9189
+ try {
9190
+ body = await req.json();
9191
+ } catch {
9192
+ return json({ error: "Invalid JSON body" }, { status: 400 });
9193
+ }
9194
+ const patch = parseScheduleBody(body, existing);
9195
+ Object.assign(existing, patch);
9196
+ const validationError = validateScheduleInput(existing);
9197
+ if (validationError) return json({ error: validationError }, { status: 400 });
9198
+ const row = await repo.save(existing);
9199
+ const cms = await getCms();
9200
+ await syncAfterSave(cms, row);
9201
+ return json({ schedule: serializeSchedule(row) });
9202
+ },
9203
+ async remove(req, id) {
9204
+ const a = await requireAuth(req);
9205
+ if (a) return a;
9206
+ const pe = await perm(req, "delete");
9207
+ if (pe) return pe;
9208
+ if (!getCms) return json({ error: "CMS not configured" }, { status: 503 });
9209
+ const repo = dataSource.getRepository(ScheduleEntity);
9210
+ const existing = await repo.findOne({ where: { id } });
9211
+ if (!existing) return json({ error: "Schedule not found" }, { status: 404 });
9212
+ const cms = await getCms();
9213
+ existing.enabled = false;
9214
+ await syncJobScheduleToPgBoss(cms, existing);
9215
+ await repo.delete({ id });
9216
+ return json({ ok: true });
9217
+ },
9218
+ async runNow(req, id) {
9219
+ const a = await requireAuth(req);
9220
+ if (a) return a;
9221
+ const pe = await perm(req, "create");
9222
+ if (pe) return pe;
9223
+ if (!getCms) return json({ error: "CMS not configured" }, { status: 503 });
9224
+ const repo = dataSource.getRepository(ScheduleEntity);
9225
+ const existing = await repo.findOne({ where: { id } });
9226
+ if (!existing) return json({ error: "Schedule not found" }, { status: 404 });
9227
+ try {
9228
+ const cms = await getCms();
9229
+ await queueJobScheduleNow(cms, id);
9230
+ return json({ ok: true, message: "Job queued" });
9231
+ } catch (e) {
9232
+ const message = e instanceof Error ? e.message : "Failed to queue job";
9233
+ return json({ error: message }, { status: 503 });
9234
+ }
9235
+ },
9236
+ async listRuns(req, id) {
9237
+ const a = await requireAuth(req);
9238
+ if (a) return a;
9239
+ const pe = await perm(req, "read");
9240
+ if (pe) return pe;
9241
+ const runs = await dataSource.getRepository(RunEntity).find({
9242
+ where: { scheduleId: id },
9243
+ order: { startedAt: "DESC" },
9244
+ take: 50
9245
+ });
9246
+ return json({ runs: runs.map(serializeRun) });
9247
+ }
9248
+ };
9249
+ }
9250
+
8573
9251
  // src/api/cms-api-handler.ts
8574
9252
  var KNOWLEDGE_SUFFIX = "knowledge";
8575
9253
  var CMS_API_LOG = "[cms-api]";
@@ -8630,7 +9308,9 @@ var DEFAULT_EXCLUDE = /* @__PURE__ */ new Set([
8630
9308
  "message_templates",
8631
9309
  "llm_agent_knowledge_documents",
8632
9310
  "rss_feeds",
8633
- "rss_articles"
9311
+ "rss_articles",
9312
+ "job_schedules",
9313
+ "job_schedule_runs"
8634
9314
  ]);
8635
9315
  function createCmsApiHandler(config) {
8636
9316
  const {
@@ -8793,6 +9473,15 @@ function createCmsApiHandler(config) {
8793
9473
  ...llmAgentKnowledgeMerged,
8794
9474
  requireEntityPermission: requireEntityPermissionEffective
8795
9475
  }) : null;
9476
+ const jobScheduleHandlers = getCms ? createJobScheduleHandlers({
9477
+ dataSource,
9478
+ entityMap,
9479
+ json: config.json,
9480
+ requireAuth: config.requireAuth,
9481
+ requireEntityPermission: requireEntityPermissionEffective,
9482
+ getCms,
9483
+ config: typeof process !== "undefined" ? process.env : {}
9484
+ }) : null;
8796
9485
  function resolveResource(segment) {
8797
9486
  const model = pathToModel(segment);
8798
9487
  return crudResources.includes(model) ? model : segment;
@@ -8823,6 +9512,30 @@ function createCmsApiHandler(config) {
8823
9512
  if (g) return g;
8824
9513
  return ecommerceAnalyticsGet(req);
8825
9514
  }
9515
+ if (path2[0] === "job-schedules" && jobScheduleHandlers) {
9516
+ if (path2.length === 2 && m === "GET") {
9517
+ return jobScheduleHandlers.getById(req, path2[1]);
9518
+ }
9519
+ if (path2.length === 2 && m === "PATCH") {
9520
+ return jobScheduleHandlers.update(req, path2[1]);
9521
+ }
9522
+ if (path2.length === 2 && m === "DELETE") {
9523
+ return jobScheduleHandlers.remove(req, path2[1]);
9524
+ }
9525
+ if (path2.length === 3 && path2[2] === "run-now" && m === "POST") {
9526
+ return jobScheduleHandlers.runNow(req, path2[1]);
9527
+ }
9528
+ if (path2.length === 3 && path2[2] === "runs" && m === "GET") {
9529
+ return jobScheduleHandlers.listRuns(req, path2[1]);
9530
+ }
9531
+ if (path2.length === 1 && m === "GET") {
9532
+ return jobScheduleHandlers.list(req);
9533
+ }
9534
+ if (path2.length === 1 && m === "POST") {
9535
+ return jobScheduleHandlers.create(req);
9536
+ }
9537
+ return config.json({ error: "Not found" }, { status: 404 });
9538
+ }
8826
9539
  if (path2[0] === "blog-generator" && path2[1] === "feeds" && path2.length === 3 && m === "DELETE" && getCms) {
8827
9540
  const id = path2[2];
8828
9541
  const uuidLooksValid = typeof id === "string" && /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(id);
@@ -9288,6 +10001,9 @@ function createCmsApiHandler(config) {
9288
10001
  if (tail === "sync-profile" && m === "POST") {
9289
10002
  return socialMediaHandlers.syncLinkedInProfile(req);
9290
10003
  }
10004
+ if (tail === "fetch-organizations" && m === "POST") {
10005
+ return socialMediaHandlers.fetchLinkedInOrganizations(req);
10006
+ }
9291
10007
  if (tail === "publish-blog" && m === "POST") {
9292
10008
  let body;
9293
10009
  try {
@@ -9468,7 +10184,7 @@ function createCmsApiHandler(config) {
9468
10184
  }
9469
10185
 
9470
10186
  // src/api/storefront-handlers.ts
9471
- import { In as In3, IsNull as IsNull4 } from "typeorm";
10187
+ import { In as In4, IsNull as IsNull4 } from "typeorm";
9472
10188
 
9473
10189
  // src/lib/is-valid-signup-email.ts
9474
10190
  var MAX_EMAIL = 254;
@@ -11082,7 +11798,7 @@ function createStorefrontApiHandler(config) {
11082
11798
  const previewByOrder = {};
11083
11799
  if (orderIds.length) {
11084
11800
  const oItems = await orderItemRepo().find({
11085
- where: { orderId: In3(orderIds) },
11801
+ where: { orderId: In4(orderIds) },
11086
11802
  relations: ["product"],
11087
11803
  order: { id: "ASC" }
11088
11804
  });
@@ -11218,6 +11934,7 @@ export {
11218
11934
  createForgotPasswordHandler,
11219
11935
  createFormBySlugHandler,
11220
11936
  createInviteAcceptHandler,
11937
+ createJobScheduleHandlers,
11221
11938
  createLlmAgentKnowledgeHandlers,
11222
11939
  createMediaZipExtractHandler,
11223
11940
  createSetPasswordHandler,