Commit Graph

5 Commits

Author SHA1 Message Date
Anso e5b1c7b22b refactor(backend): collapse entitlement provider abstraction back to LicenseService (#889)
Removes backend/src/entitlements/ (registry, loadProvider,
CommunityEntitlementProvider, types, headers, normalize) and the two
abstraction-only tests. Relocates headers/normalize/types to
services/license-*.ts. Swaps 22 consumer call sites from
getEntitlementProvider() to LicenseService.getInstance(). Drops the
Dockerfile install step plus PRO_PACKAGE_VERSION build-arg and
github_token BuildKit secret in docker-publish.yml. Removes the now
stale no-restricted-imports rule in backend/eslint.config.mjs.

Net: 37 files changed, ~700 lines removed, no behavior change. Local
dev no longer requires GitHub Packages auth to start the backend.

Rationale and revisit conditions in
docs/internal/adrs/2026-05-02-collapse-entitlement-provider.md.
2026-05-02 23:45:44 -04:00
Anso cffb481106 feat(entitlements): wire dynamic import of @studio-saelix/sencho-pro (#880)
Phase 2 of the open-core hybrid extraction documented in
docs/internal/adrs/2026-05-02-open-core-hybrid-strategy.md. The
private @studio-saelix/sencho-pro package is now published to GitHub
Packages with v0.1.0 carrying the LemonSqueezy implementation
(LemonSqueezyEntitlementProvider). This PR delivers the public-side
hookup so the loader prefers the private package when installed and
falls back to the in-tree LicenseService when not.

loadEntitlementProvider() tries `await import('@studio-saelix/sencho-pro')`
first. If the package is missing, the loader falls back to
LicenseService.getInstance() so a Community-only build (no private
package installed, e.g. local dev or the public BSL Docker image)
still runs through the existing LemonSqueezy path. If the package
loaded but threw during construction, or if a transitive dep is
missing, the loader re-raises so the failure surfaces; silently
downgrading a paid install to community on a load-time bug would be
a license-bypass surface.

The discrimination uses two checks rather than the error code alone:
the message must include the literal package name. Without that
anchor, a missing transitive dep in a paid install would surface
with the same MODULE_NOT_FOUND code as the package itself missing.
ERR_PACKAGE_PATH_NOT_EXPORTED is intentionally NOT classified as
"not installed" because that code fires when the package was
resolved but its exports map does not include the requested path,
which is a packaging bug worth surfacing.

backend/src/types/sencho-pro.d.ts is an ambient module stub so tsc
passes when the package is not installed locally. The package's own
dist/index.d.ts shadows the stub when present; drift fails the
build. The stub uses class implements EntitlementProvider so the
interface clause carries the full method surface; we do not
redeclare individual methods.

eslint.config.mjs adds a no-restricted-imports rule blocking static
imports of @studio-saelix/sencho-pro and any subpath. The loader's
await import() is a dynamic import and is not flagged. Static
imports would bundle the package into the public BSL build via
TypeScript's module resolution, defeating the privacy split, and
would break in Community-only environments.

Adds 7 unit tests for isProPackageNotInstalled covering all the
discrimination paths: non-Error inputs, the two recognized codes,
the package-name anchor, transitive-dep MODULE_NOT_FOUND, and
ERR_PACKAGE_PATH_NOT_EXPORTED.

Test results: 91/91 backend test files pass, 1665 passing tests, 5
pre-existing skips. tsc clean. eslint 0 errors.

Out of scope for this PR (Phase 2b, separate follow-up):
  - Dockerfile change to install @studio-saelix/sencho-pro from
    GitHub Packages using GITHUB_TOKEN auth.
  - docker-publish.yml building dual images: saelix/sencho
    (Community-only) and saelix/sencho-pro (with private package).

Out of scope for this PR (cleanup, separate follow-up):
  - Removing services/LicenseService.ts from the public repo.
  - Switching the loader fallback from LicenseService to
    CommunityEntitlementProvider.

The transitional state keeps the public repo runnable on its own
during the dual-image rollout window. The cleanup PR lands once
saelix/sencho-pro is verified working in production.
2026-05-02 06:07:13 -04:00
Anso 4b18109286 refactor(entitlements): migrate type-only consumers to entitlements/types (#879)
Follows the Phase 1 EntitlementProvider abstraction. Two files
imported tier types from services/LicenseService via the back-compat
re-export added in Phase 1; this PR points them at the canonical
location at entitlements/types and drops the re-export block.

Migrated:
  - backend/src/types/express.ts
  - backend/src/routes/dashboard.ts

After this PR, services/LicenseService.ts has no public type re-
exports. The remaining imports of services/LicenseService are:
  - entitlements/loadProvider.ts: runtime import of the
    LicenseService class itself, the intentional Phase 1 binding
    site.
  - __tests__/license-service-id-validation.test.ts: imports
    SENCHO_LS_* catalog constants and resolveSenchoVariantFromMeta;
    these are LemonSqueezy-implementation-specific and stay in
    services/LicenseService until Phase 2 moves the file to
    @studio-saelix/sencho-pro.

Phase 2's deletion of services/LicenseService.ts now requires zero
public-core consumer changes outside the loader and the LS-specific
test file.

Test results: 89/89 backend test files clean, 1657 passing tests, 5
pre-existing skips, plus the same pre-existing database-metrics
stress test flake under parallel load that consistently passes solo.
2026-05-02 05:21:26 -04:00
Anso 929e2fa6b1 refactor(backend): extract types, constants, and guards from index.ts (phase 0) (#730)
* refactor(backend): extract types, constants, and guards from index.ts (phase 0)

Additive, behavior-preserving first step of the modular backend refactor.
Moves purely static artifacts out of backend/src/index.ts so later phases can
extract routes and middleware without touching shared symbols.

New modules:
- types/express.ts: Express Request augmentation
- helpers/constants.ts: PORT, password policy, label colors, cookie names,
  MFA TTLs, hot-path cache TTLs
- helpers/proxyExemptPaths.ts: PROXY_EXEMPT_PREFIXES + isProxyExemptPath
- helpers/cookies.ts: isSecureRequest, getCookieOptions
- helpers/policyGate.ts: buildPolicyGateOptions, runPolicyGate,
  triggerPostDeployScan
- middleware/permissions.ts: ROLE_PERMISSIONS, checkPermission,
  requirePermission
- middleware/tierGates.ts: requirePaid, requireAdmiral, requireAdmin,
  requireNodeProxy, requireScheduledTaskTier + effectiveTier/Variant

index.ts shrinks by ~260 lines; no runtime behavior changes. All 64 vitest
files and 1,278 tests pass.

* refactor(backend): drop unused imports left after phase 0 extraction

LicenseTier, LicenseVariant, DIGEST_CACHE_TTL_MS, and isProxyExemptPath
were imported into index.ts but no longer referenced there after the
phase 0 move; CI lint flagged them as errors.

isProxyExemptPath will be re-imported in phase 1 when the JSON parser
bypass and nodeContext middleware get extracted. Silence the
no-namespace warning on the Express augmentation since the namespace
syntax is required for TypeScript module augmentation.
2026-04-23 17:58:04 -04:00
unknown 293f9cef26 Initial commit: Sencho V1 complete with Auth and Dockerization 2026-02-20 18:39:32 -05:00