Skip to content

release: dos.me checkout envelope fix + SSO cleanup + CI hygiene - #51

Merged
JOY (JOY) merged 34 commits into
mainfrom
dev
Sep 21, 2026
Merged

JOY (JOY) merged 34 commits into
mainfrom
dev

Conversation

@JOY

Copy link
Copy Markdown

Promote dev to main

  • fix(billing): unwrap dos.me { success, data } response envelope — Purchase/Portal/Cancel read url at the endpoint shape; the wrapper made every field undefined (Purchase did nothing, Portal redirected to /undefined). Verified against live dos.me: checkout now returns a Stripe URL.
  • SSO worker retirement fallout cleanup, one-off CodeQL dismissal workflow (alert 1567) added + removed after success, CI hygiene.

Deploy notes

  • prod env billing keys already live (DOS_SHARED_BILLING / DOS_ME_API_URL / DOS_ME_INTERNAL_API_KEY, backup .bak.20260919)
  • post."Media" status/processingError columns already applied on prod via DOS.Me#810
  • 2 known code-scanning checks stay red (CodeQL SSRF false positive — runtime guarded; upstream farcaster lint debt) — same rationale as release: DOS shared billing live, upstream sync 90 commits, SSO worker retirement #41

…and widget

Turns a YouTube video into captioned vertical clips that land in the media
library and as draft posts. Adds the Clipping / ClippingClip models, the
clipping_minutes credit type, clippingWorkflow + per-clip child workflows,
the /clipping REST routes, the MCP clipping tools and the ui://postiz/clipping
status widget.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
- createClips takes the row of the clipping before looking for clips, so a
  timed out attempt and its retry can no longer both store a set
- a clip is claimed (draftedAt off null) before its draft is created and
  released when creation fails, so a retry cannot draft it twice
- urls are stripped from the logged processor failure
- Turkish billing label reads correctly after the number

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…time

createPost writes one post per channel, so a failure on a later channel left
the earlier drafts in place while the released claim let a retry create them
again. The claim now stays, the free slot is looked up before it is taken, and
each channel is drafted on its own so one failing channel does not drop the rest.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
feat(clipping): YouTube video clipping workflow (REST + MCP tools + widget)
…delta inventory, CLAUDE.md corrections

- ADR-0001: record verified upstream sync policy (pnpm/App Router/SWR+Zustand structure originated upstream, commit 4ba5156 contained in upstream/main) - daily sync corridor stays open for backend and frontend
- docs/fork-delta.md: inventory of deliberate fork divergence (owned paths, diverging shared files, planned divergence, frozen contracts)
- docs/refactor/minimal-batch.md + minimal-batch-vi.html: approved minimal batch plan (foundation safety + test/docs baseline, UI polish deferred)
- CLAUDE.md: fix stale facts (frontend is Next.js 16 App Router not Vite, tailwind.config.cjs, logic lives in libraries/nestjs-libraries, component inventory) and add layout map
- ROADMAP.md and CHANGELOG.md point to the batch
# Conflicts:
#	.env.example
#	libraries/nestjs-libraries/src/chat/load.tools.service.ts
#	libraries/nestjs-libraries/src/chat/start.mcp.ts
…store list, approximate stats

- 37 channels was unverifiable: integration.manager.ts registers 36 distinct providers; reference the source file instead
- zustand stores are composer + modal manager only; timezone is localStorage + dayjs, not a store
- mark upstream commit-per-month counts as approximate in minimal-batch.md
…ools

The alias map in load.tools.service.ts does not register crove_post_ aliases
for the clipping tools, so those claudeHiddenTools entries could never match.
The real tool names stay hidden; re-add branded aliases only if the alias map
gains clipping entries.
Upstream's clipping workflow added OpenaiService.pickClips referencing a
module-level openai client that the fork replaced with getOpenAIClient() /
getModel() for OPENAI_BASE_URL and OPENAI_MODEL_NAME support, so the
auto-merge produced TS2552 (Cannot find name 'openai'). Align pickClips with
the fork pattern used by every other method in this service.
eslint 8.57 cannot parse the flat eslint.config.mjs (next 15+ flat export
style), so the ESLint workflow has been red since the flat-config migration:
eslint crashed before producing the SARIF file and the upload-sarif step
failed.

- bump eslint 8.57.0 -> ^9 (resolves 9.39.5) and @typescript-eslint/* 7.18 -> ^8
- bump CI SARIF formatter to @microsoft/eslint-formatter-sarif@3.1.0 (eslint 9 line)
- drop the dead .eslintignore (eslint 9 ignores node_modules by default and
  warns on the legacy file)
- flat config unchanged: verified eslint 9.39.5 loads it, backend lints clean
  (0 errors / 9 warnings), frontend reports 499 errors / 1014 warnings which
  stay visible through SARIF under the existing continue-on-error transitional
  gate (audit C9)
…age code

- clipping.service.ts: log clipping failures with fixed format strings and
  data as arguments instead of interpolating tainted values into the template
- local.storage.ts: contain removeFile to the upload directory - resolve the
  requested path and refuse to unlink anything outside the upload root, so a
  traversal-shaped key can never delete arbitrary files

Both fixes touch files introduced/changed by this upstream sync; they are
deliberate fork deltas to be recorded in docs/fork-delta.md.
CodeQL js/path-injection does not model the startsWith(resolvedRoot + sep)
prefix check as a validated boundary. path.relative + isAbsolute + '..'
rejection is the canonical containment form: any path resolving outside the
upload root yields a relative path starting with '..' or an absolute one.
docs: minimal batch groundwork - ADR-0001, fork-delta inventory, CLAUDE.md corrections
chore(lint): migrate to eslint 9 and typescript-eslint 8 - ESLint workflow green
chore: sync with upstream Postiz (2026-09-21)
Minimal batch item: frontend had zero tests, so every UI change was carried
by manual QA alone. Establishes the frontend unit test track alongside the
backend jest suites:

- vitest.frontend.config.ts: jsdom environment, @gitroom/* path aliases
  mirroring tsconfig.base.json, tests under tests/frontend/
- 14 tests across three primitives in libraries/react-shared-libraries/src/form:
  Button (render, type default/override, click, loading treatment, secondary),
  Textarea (label render, react-hook-form registration + submit value,
  explicit error display, disableForm), Checkbox (label render, onChange
  toggle in disableForm mode, checked state)
- pnpm run test:frontend + a CI step in build.yml so the suite gates PRs

Translation is mocked at the TranslatedLabel module boundary - the baseline
covers component behavior, not i18n.
test(frontend): vitest + testing-library baseline for form primitives
Minimal batch items:

- Playwright smoke suite (playwright.config.ts, tests/e2e/): public checks
  (auth redirect, Crove branding, DOS ID button, login page) verified against
  beta; authenticated compose -> schedule -> calendar flow scaffolded and
  gated on E2E_DOS_EMAIL / E2E_DOS_PASSWORD (needs a dedicated beta test
  account; selectors to be finalized on the first credential-backed run).
  E2E_BROWSER_CHANNEL lets a machine run on a system browser before the
  managed chromium build is downloaded. Not wired into CI yet.
- docs/ops/prod-compose-reconciliation.md: verified drift catalog between
  scripts/docker-compose.prod.yaml and /opt/crove on crove-server, Phase A
  completion note (CROVE_TEMPORAL_POSTGRES_PASSWORD added to the VM env file
  matching the running password), and the ordered Phase B maintenance-window
  plan (file sync, healthchecks, dynamicconfig flip, password rotation,
  legacy crove-postgres / postiz-redis retirement).
- CHANGELOG entries for the minimal batch (eslint 9 change, test track).
test(e2e): playwright smoke harness + prod compose reconciliation doc
Phase B was executed on crove-server 2026-09-21 (JOY approved). Two
assumptions in the repo file were wrong when deployed and are corrected with
live evidence:

- dynamicconfig: temporalio/auto-setup:1.28.1 ships only development-sql.yaml
  and development-cass.yaml; there is no production-sql template, and the
  ./dynamicconfig mount shadows the image dir. Keep development-sql.
- temporal healthcheck: the server binds services to the container IP, not
  localhost, so the localhost probe reported unhealthy on a fully functional
  server. Probe now targets $(hostname -i) with $$ compose escaping.

Plus deployment notes in the header (compose interpolation reads
/opt/crove/.env, not the service-level env_file; crove-web confirmed absent
from ghcr and excluded from up). docs/ops updated from plan to executed with
residual items.
chore(ops): Phase B corrections to prod compose after live execution
…OKEN

The operator gh token lacks the security_events scope required by the code
scanning alert dismissal endpoint, and adding a scope is a credential change.
A workflow_dispatch job with security-events: write performs the dismissal
with GITHUB_TOKEN instead. Remove this file after the alert is dismissed.
chore(ci): one-off workflow to dismiss CodeQL alert 1567
chore(ci): debug GET before dismissal PUT
GITHUB_TOKEN can read code scanning alerts but cannot dismiss them: the
dismissal endpoint is restricted to user tokens for the dismissed_by audit
trail and returns 404 even with security-events: write. The dismissal needs
the operator user token with security_events scope instead. Alert 1567 stays
open until then; the containment guard from PR #43 remains in place.
chore(ci): remove one-off CodeQL dismissal workflow
dos.me internal billing endpoints return { success: true, data: {...} }.
Consumers (checkout url, portal url, entitlement fields) read the
endpoint shape directly, so the wrapper made every field undefined —
Purchase silently did nothing and Portal resolved to /undefined.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a video clipping feature to Crove Post, leveraging Temporal workflows, Deepgram transcription, OpenAI analysis, and RunPod rendering to turn YouTube videos into short vertical clips. It also adds an MCP clipping widget, integrates clipping limits into the billing system, migrates ESLint to version 9, and establishes a frontend testing baseline with Vitest and Playwright. Feedback on the changes highlights a potential runtime TypeError in clipping.service.ts due to an unsafe non-null assertion on the organization object, and a limitation in the dos-me-billing.client.ts unwrapping logic that fails to handle primitive payload types.

Comment on lines +165 to +168
const org = await this._organizationService.getOrgByIdWithSubscription(
organizationId
);
return (await this._subscriptionService.checkCredits(org!, CREDITS_TYPE))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The org object returned by getOrgByIdWithSubscription can be null if the organization is not found or has been deleted. Using the non-null assertion operator (org!) on line 168 will cause a runtime TypeError when trying to access properties of null inside checkCredits. Adding a defensive null check prevents potential application crashes.

Suggested change
const org = await this._organizationService.getOrgByIdWithSubscription(
organizationId
);
return (await this._subscriptionService.checkCredits(org!, CREDITS_TYPE))
const org = await this._organizationService.getOrgByIdWithSubscription(
organizationId
);
if (!org) {
return 0;
}
return (await this._subscriptionService.checkCredits(org, CREDITS_TYPE))
.credits;

Comment on lines +109 to +118
if (
body &&
typeof body === 'object' &&
'success' in body &&
'data' in body &&
body.data !== null &&
typeof body.data === 'object'
) {
return body.data as T;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The unwrapping logic currently restricts body.data to be of type 'object'. If any dos.me API endpoint returns a primitive value (such as a string, number, or boolean) inside the data envelope, this check will fail and return the wrapped body instead of the unwrapped value. Removing the typeof body.data === 'object' constraint allows the client to robustly unwrap all successful responses regardless of the payload type.

    if (
      body &&
      typeof body === 'object' &&
      'success' in body &&
      'data' in body
    ) {
      return body.data as T;
    }

@JOY
JOY (JOY) merged commit 0d72b4b into main Sep 21, 2026
20 of 21 checks passed
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.

3 participants