Skip to content

Feat/ai jobs twitter drafts flags apikey audit - #576

Merged
Akatenvictor merged 4 commits into
AudioBitsStellar:mainfrom
DooseWayo:feat/ai-jobs-twitter-drafts-flags-apikey-audit
Aug 30, 2026
Merged

Feat/ai jobs twitter drafts flags apikey audit#576
Akatenvictor merged 4 commits into
AudioBitsStellar:mainfrom
DooseWayo:feat/ai-jobs-twitter-drafts-flags-apikey-audit

Conversation

@DooseWayo

Copy link
Copy Markdown

Description

Please include a summary of the change and which issue is fixed.

Fixes # (issue)

Type of change

  • Bug fix
  • New feature
  • Breaking change
  • Documentation update

Checklist:

  • My code follows the coding conventions
  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes

Closes #281
Closes #286
Closes #297
Closes #320

DooseWayo and others added 4 commits August 30, 2026 18:37
npm ci failed with "Missing: zod@3.25.76 from lock file" — the lock
file had drifted from package.json after recent merges.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uqn6VeNfzGEJsCDDxEJR7P
Auditing the API key surface (creation, revocation, scoping) surfaced
several bugs from a bad merge between competing ApiKey designs:

- ApiKey was never registered in AppDataSource's entities array, so
  every ApiKeyService call throws "No metadata for ApiKey was found"
  outside tests (which mask it via synchronize:true).
- The entity had isRevoked (never migrated, never read) while the
  service read/wrote revokedAt (never on the entity). Replaced
  isRevoked with revokedAt and added the missing keyPrefix column.
- scopes and rateLimitTier existed on the entity but were never
  migrated, so production (migrations-only, no synchronize) would
  fail to persist them. Added a migration for both.
- ApiKeyController passed the request's `permissions` into the
  `scopes` parameter slot, silently dropping requested scopes.
- keyHasScope treated an empty scope list as unrestricted access
  (fail-open); a key with no scopes now holds none (fail-closed).
- keyHasPermission never re-checked the owning user's role, so a
  permission survived a role downgrade as long as the key still
  listed it. It now requires both the key to list the permission and
  the current role to still hold it (matches the documented "never
  exceed the owner's role" invariant).
- createApiKey didn't enforce that invariant at issue time either;
  it now rejects unknown or role-exceeding permissions up front.

Rewrote the stale apiKeyScopes.test.ts (asserted behavior no test
subject implements), fixed apiKeyRateLimitTier.test.ts's call
signature, and added ApiKeyService.test.ts covering revoked-key
rejection, ownership checks, and scope/permission enforcement.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uqn6VeNfzGEJsCDDxEJR7P
Adds the provider abstraction ADR-007 calls for ("all third-party AI
calls go through a thin internal service... a null/no-op provider is
always available") and routes the two AI operations flagged as
too-slow-for-sync — cover art and description generation — through
JobQueueService instead of the request/response cycle:

- src/services/ai/AiProvider.ts: vendor-neutral interface
  (generateCoverArt/generateDescription/draftTweet).
- NoopAiProvider: the always-available default. No network call is
  made; outputs are deterministic templates, same spirit as the other
  "AI-adjacent" features in docs/AI_FEATURES.md. AI_PROVIDER selects
  the vendor; anything but "noop" logs a warning and falls back to it,
  since no real vendor is implemented yet.
- AiGenerationService.requestGeneration enqueues a job via
  JobQueueService and returns a pending AiGenerationRecord immediately;
  AiJobHandlers.ts processes it on the existing job worker and only
  marks the record failed (and fires a webhook) once JobQueueService
  has exhausted retries, not on every transient attempt.
- Completion is announced via the existing webhook system as
  ai.generation.completed (WebhookService — already implements Phase 3
  of docs/WEBHOOK_IMPLEMENTATION_PLAN.md, whose status header was
  stale; corrected it).
- POST /api/ai/songs/:songId/{cover-art,description} to request a
  generation, GET /api/ai/generations/:id to poll it.

Replaces the single-global-AI_ENABLED-flag anti-pattern with
per-feature flags (src/config/aiFeatureFlags.ts) checked at each call
site — tags, descriptions, coverArt, moderationTriage, search,
playlists, tweetDrafts — each independently toggleable and
default-off. Only coverArt/descriptions have a wired call site today;
the rest are reserved for when those features are built.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uqn6VeNfzGEJsCDDxEJR7P
twitterRoutes.ts supported connect/disconnect but had no drafting
assistance for announcing a release. Adds:

- TweetDraftService.createDraft: uses the AiProvider abstraction
  (draftTweet) to draft announcement text for a song (or a generic
  release), gated by the AI_FEATURE_TWEET_DRAFTS_ENABLED flag.
- POST /api/auth/twitter/draft, GET .../drafts,
  POST .../draft/:id/approve, DELETE .../draft/:id.

Approving a draft only marks it reviewed — it does not post to
Twitter. twitterRoutes.ts's OAuth callback deliberately never persists
an access/refresh token (documented there: Twitter is used for
profile enrichment only), so there is no credential to post with.
Wiring actual posting would mean persisting tokens and implementing
the OAuth2 refresh flow, which is a separate, larger security-relevant
change than "drafting assistance" calls for.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uqn6VeNfzGEJsCDDxEJR7P
@drips-wave

drips-wave Bot commented Aug 30, 2026

Copy link
Copy Markdown

@DooseWayo Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits.

You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀

Learn more about application limits

@Akatenvictor
Akatenvictor merged commit 1598592 into AudioBitsStellar:main Aug 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants