mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-11 03:06:54 +00:00
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.
This commit is contained in:
Vendored
+34
@@ -0,0 +1,34 @@
|
||||
/**
|
||||
* Ambient declaration for the private `@studio-saelix/sencho-pro`
|
||||
* package so the public Sencho core's TypeScript build passes whether
|
||||
* or not the package is installed locally. The runtime shape is
|
||||
* defined by the package itself; this stub mirrors the surface the
|
||||
* loader uses and nothing more.
|
||||
*
|
||||
* In production CI the Dockerfile installs the real package; the stub
|
||||
* is shadowed by the package's own types and any drift fails the
|
||||
* build. Local development without GitHub Packages auth falls back to
|
||||
* the stub plus the loader's in-tree LicenseService binding, which
|
||||
* keeps `npm install` and `tsc` working for everyone.
|
||||
*
|
||||
* The `implements EntitlementProvider` clause carries the full method
|
||||
* surface; we do not redeclare individual methods here. If the real
|
||||
* package ever ships a narrower return type than the interface
|
||||
* permits, the local stub would over-widen and lose call-site type
|
||||
* info — but call sites only consume the interface (via the registry),
|
||||
* so that risk does not materialise.
|
||||
*/
|
||||
declare module '@studio-saelix/sencho-pro' {
|
||||
import type { EntitlementProvider } from '../entitlements/types';
|
||||
|
||||
/** Minimal database surface the provider needs. The public core's
|
||||
* `DatabaseService` satisfies this structurally. */
|
||||
export interface DatabaseAdapter {
|
||||
getSystemState(key: string): string | null;
|
||||
setSystemState(key: string, value: string): void;
|
||||
}
|
||||
|
||||
export class LemonSqueezyEntitlementProvider implements EntitlementProvider {
|
||||
constructor(db: DatabaseAdapter);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user