Skip to content

feat(drs-client): generic createSchedule with fixed cadences + tenant-isolation guards#1816

Open
dzehnder wants to merge 2 commits into
mainfrom
feat/prompt-suggestion-schedules
Open

feat(drs-client): generic createSchedule with fixed cadences + tenant-isolation guards#1816
dzehnder wants to merge 2 commits into
mainfrom
feat/prompt-suggestion-schedules

Conversation

@dzehnder

Copy link
Copy Markdown
Contributor

Summary

Adds a generic DrsClient#createSchedule(...) to @adobe/spacecat-shared-drs-client for creating recurring DRS schedules on a fixed cadence — the mechanism the LLMO "prompt suggestion" pipelines (prompt_generation_semrush, prompt_generation_agentic_traffic, prompt_generation_synthetic_personas) need to run on a recurring basis and at onboarding. Sibling to the existing createExperimentSchedule; both now build the POST /schedules envelope through one shared private #buildScheduleBody, so the two paths cannot drift.

This is Phase 2 of the DRS prompt-suggestion schedules + onboarding-triggers effort. spacecat-api-service consumes createSchedule in a downstream PR (onboarding triggers); spacecat-audit-worker also depends on this client, and the change is purely additive so it is unaffected.

New API

createSchedule({
  siteId,                     // required
  providerIds,                // required, non-empty string[]
  cadence,                    // required, one of SCHEDULE_CADENCES
  description,                // optional (<= 1024 chars)
  enableBrandPresence = false,
  providerParameters,         // optional passthrough
  priority = 'HIGH',          // 'HIGH' | 'LOW'
  metadata,                   // optional
  triggerImmediately = false,
  timeout,
}) => Promise<{ scheduleId: string, alreadyExisted: boolean }>

New export: SCHEDULE_CADENCES = { TWICE_MONTHLY: 'twice_monthly', QUARTERLY: 'quarterly' }.

Design decisions

  • Constrained cadence, not free-form cron. frequency is always 'cron'; the cron is derived from the cadence enum — a raw cron string is never accepted. twice_monthly -> 0 {H} 1,15 * * where H is a deterministic per-site hour (thundering-herd guard); quarterly -> 0 8 1 1,4,7,10 *. This closes the "leaked x-api-key sets * * * * *" fleet-wide Fargate-storm risk.
  • Tenant-isolation hardening (defense in depth). A caller-supplied imsOrgId (also ims_org_id / imsorgid, any case, at any nesting depth) inside job_config is rejected. DRS derives the isolation key from site_id server-side; the client never accepts an org id. The guard is in the shared body-builder, so createExperimentSchedule gets it too.
  • Size caps. description capped at 1024 chars and the serialized job_config at 100 KB (DynamoDB oversized-item guard).
  • API key stays header-only and is never a logged argument.
  • Idempotent. A 409 dedup from DRS is treated as success (alreadyExisted: true), matching createBrandPresenceSchedule.

Tests

npm run lint and npm test for the package both pass locally: 133 passing, 100% statements / branches / functions / lines (package enforces 100% branch coverage). New coverage includes cadence-derived cron, per-site jitter determinism, 409 dedup, imsOrgId rejection (nested + in arrays), size caps, and error paths.

Versioning

Additive feat: — semantic-release will publish 1.14.0 (current 1.13.0). No breaking change.

🤖 Generated with Claude Code

…tenant-isolation guards

Add DrsClient#createSchedule, a generic recurring-schedule creator for the LLMO
prompt-suggestion pipelines (semrush / agentic-traffic / synthetic-personas),
alongside the existing createExperimentSchedule.

- Extract a shared private #buildScheduleBody so createExperimentSchedule and
  createSchedule build the POST /schedules envelope through one path and cannot
  drift.
- Constrain input: frequency is always 'cron' and the cron is DERIVED from a small
  cadence enum (SCHEDULE_CADENCES: twice_monthly -> `0 {H} 1,15 * *` with a
  deterministic per-site hour jitter; quarterly -> `0 8 1 1,4,7,10 *`). Arbitrary
  caller-supplied cron is intentionally not accepted (leaked-key Fargate-storm guard).
- Security hardening (defense in depth; DRS also enforces server-side): reject any
  caller-supplied imsOrgId (ims_org_id / imsorgid, any case, any nesting depth) inside
  job_config; cap description (1024 chars) and serialized job_config (100 KB); the
  api key remains header-only and is never a loggable argument.
- Idempotent: treat a 409 dedup as success, returning { scheduleId, alreadyExisted }.

Additive only — no breaking change. spacecat-api-service will consume createSchedule
for onboarding triggers in a downstream PR.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

This PR will trigger a minor release when merged.

Stop computing cron client-side for createSchedule. DRS derives frequency +
cron server-side from job_config.cadence for prompt-suggestion providers and
overwrites anything sent, so the client's jitter (Math.imul) differed from
DRS's sha256 % 24 and logged a fire time that never happened. The client now
sends only the cadence label; #buildScheduleBody sends frequency/cron_expression
only when a caller-supplied cron is authoritative (the experiment path).

Fix the dedup signal: DRS returns HTTP 200 { idempotent: true, schedule_id }
on a deterministic-id collision (not 409 + existing_schedule_id), and the
create is create-only. Surface that as alreadyExisted: true and document that a
changed cadence/job_config is not applied on a repeat call.

Also: use Buffer.byteLength for the job_config byte cap, extend the imsOrgId
key regex to hyphen variants, add a recursion depth guard to assertNoImsOrgId,
and validate priority against { HIGH, LOW }.

Delete cronForCadence/siteHourJitter and their now-dead cron logging.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant