@selvajs/server 0.2.0-beta.2 → 0.2.0-beta.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (155) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +32 -0
  3. package/dist/compute/client-cache.d.ts +12 -0
  4. package/dist/compute/client-cache.d.ts.map +1 -1
  5. package/dist/compute/client-cache.js +18 -3
  6. package/dist/compute/client-cache.js.map +1 -1
  7. package/dist/compute/definition-byte-cache.d.ts +85 -0
  8. package/dist/compute/definition-byte-cache.d.ts.map +1 -0
  9. package/dist/compute/definition-byte-cache.js +110 -0
  10. package/dist/compute/definition-byte-cache.js.map +1 -0
  11. package/dist/compute/index.d.ts +7 -2
  12. package/dist/compute/index.d.ts.map +1 -1
  13. package/dist/compute/index.js +6 -1
  14. package/dist/compute/index.js.map +1 -1
  15. package/dist/compute/limits.d.ts +52 -0
  16. package/dist/compute/limits.d.ts.map +1 -1
  17. package/dist/compute/limits.js +33 -1
  18. package/dist/compute/limits.js.map +1 -1
  19. package/dist/compute/memory-solve-cache.d.ts +58 -0
  20. package/dist/compute/memory-solve-cache.d.ts.map +1 -0
  21. package/dist/compute/memory-solve-cache.js +0 -0
  22. package/dist/compute/memory-solve-cache.js.map +1 -0
  23. package/dist/compute/safe-url.d.ts.map +1 -1
  24. package/dist/compute/safe-url.js +16 -0
  25. package/dist/compute/safe-url.js.map +1 -1
  26. package/dist/compute/solve-cache-envelope.d.ts +53 -0
  27. package/dist/compute/solve-cache-envelope.d.ts.map +1 -0
  28. package/dist/compute/solve-cache-envelope.js +66 -0
  29. package/dist/compute/solve-cache-envelope.js.map +1 -0
  30. package/dist/compute/solve-cache-key.d.ts +65 -0
  31. package/dist/compute/solve-cache-key.d.ts.map +1 -0
  32. package/dist/compute/solve-cache-key.js +50 -0
  33. package/dist/compute/solve-cache-key.js.map +1 -0
  34. package/dist/compute/solve-cache-single-flight.d.ts +42 -0
  35. package/dist/compute/solve-cache-single-flight.d.ts.map +1 -0
  36. package/dist/compute/solve-cache-single-flight.js +50 -0
  37. package/dist/compute/solve-cache-single-flight.js.map +1 -0
  38. package/dist/compute/solve-pipeline.d.ts +71 -8
  39. package/dist/compute/solve-pipeline.d.ts.map +1 -1
  40. package/dist/compute/solve-pipeline.js +180 -8
  41. package/dist/compute/solve-pipeline.js.map +1 -1
  42. package/dist/definitions/definition-service.d.ts +1 -1
  43. package/dist/definitions/definition-service.d.ts.map +1 -1
  44. package/dist/definitions/definition-service.js +8 -4
  45. package/dist/definitions/definition-service.js.map +1 -1
  46. package/package.json +14 -18
  47. package/dist/access/__tests__/project-access-input.test.d.ts +0 -2
  48. package/dist/access/__tests__/project-access-input.test.d.ts.map +0 -1
  49. package/dist/access/__tests__/project-access-input.test.js +0 -146
  50. package/dist/access/__tests__/project-access-input.test.js.map +0 -1
  51. package/dist/compute/__tests__/client-cache.test.d.ts +0 -11
  52. package/dist/compute/__tests__/client-cache.test.d.ts.map +0 -1
  53. package/dist/compute/__tests__/client-cache.test.js +0 -200
  54. package/dist/compute/__tests__/client-cache.test.js.map +0 -1
  55. package/dist/compute/__tests__/rate-limit.test.d.ts +0 -2
  56. package/dist/compute/__tests__/rate-limit.test.d.ts.map +0 -1
  57. package/dist/compute/__tests__/rate-limit.test.js +0 -98
  58. package/dist/compute/__tests__/rate-limit.test.js.map +0 -1
  59. package/dist/compute/__tests__/remote-definition.test.d.ts +0 -11
  60. package/dist/compute/__tests__/remote-definition.test.d.ts.map +0 -1
  61. package/dist/compute/__tests__/remote-definition.test.js +0 -114
  62. package/dist/compute/__tests__/remote-definition.test.js.map +0 -1
  63. package/dist/compute/__tests__/safe-url.test.d.ts +0 -2
  64. package/dist/compute/__tests__/safe-url.test.d.ts.map +0 -1
  65. package/dist/compute/__tests__/safe-url.test.js +0 -71
  66. package/dist/compute/__tests__/safe-url.test.js.map +0 -1
  67. package/dist/compute/__tests__/solve-pipeline.test.d.ts +0 -11
  68. package/dist/compute/__tests__/solve-pipeline.test.d.ts.map +0 -1
  69. package/dist/compute/__tests__/solve-pipeline.test.js +0 -292
  70. package/dist/compute/__tests__/solve-pipeline.test.js.map +0 -1
  71. package/dist/compute/__tests__/transform-input.test.d.ts +0 -14
  72. package/dist/compute/__tests__/transform-input.test.d.ts.map +0 -1
  73. package/dist/compute/__tests__/transform-input.test.js +0 -120
  74. package/dist/compute/__tests__/transform-input.test.js.map +0 -1
  75. package/dist/definitions/__tests__/load-for-render.test.d.ts +0 -8
  76. package/dist/definitions/__tests__/load-for-render.test.d.ts.map +0 -1
  77. package/dist/definitions/__tests__/load-for-render.test.js +0 -196
  78. package/dist/definitions/__tests__/load-for-render.test.js.map +0 -1
  79. package/dist/definitions/__tests__/schema-extraction.test.d.ts +0 -11
  80. package/dist/definitions/__tests__/schema-extraction.test.d.ts.map +0 -1
  81. package/dist/definitions/__tests__/schema-extraction.test.js +0 -115
  82. package/dist/definitions/__tests__/schema-extraction.test.js.map +0 -1
  83. package/dist/http/__tests__/body-size.test.d.ts +0 -2
  84. package/dist/http/__tests__/body-size.test.d.ts.map +0 -1
  85. package/dist/http/__tests__/body-size.test.js +0 -24
  86. package/dist/http/__tests__/body-size.test.js.map +0 -1
  87. package/dist/http/__tests__/route-classifier.test.d.ts +0 -2
  88. package/dist/http/__tests__/route-classifier.test.d.ts.map +0 -1
  89. package/dist/http/__tests__/route-classifier.test.js +0 -54
  90. package/dist/http/__tests__/route-classifier.test.js.map +0 -1
  91. package/dist/http/__tests__/safe-redirect.test.d.ts +0 -2
  92. package/dist/http/__tests__/safe-redirect.test.d.ts.map +0 -1
  93. package/dist/http/__tests__/safe-redirect.test.js +0 -44
  94. package/dist/http/__tests__/safe-redirect.test.js.map +0 -1
  95. package/dist/http/__tests__/security-headers.test.d.ts +0 -2
  96. package/dist/http/__tests__/security-headers.test.d.ts.map +0 -1
  97. package/dist/http/__tests__/security-headers.test.js +0 -24
  98. package/dist/http/__tests__/security-headers.test.js.map +0 -1
  99. package/dist/ops/__tests__/semver.test.d.ts +0 -2
  100. package/dist/ops/__tests__/semver.test.d.ts.map +0 -1
  101. package/dist/ops/__tests__/semver.test.js +0 -38
  102. package/dist/ops/__tests__/semver.test.js.map +0 -1
  103. package/dist/providers/__tests__/create-selva-providers.test.d.ts +0 -7
  104. package/dist/providers/__tests__/create-selva-providers.test.d.ts.map +0 -1
  105. package/dist/providers/__tests__/create-selva-providers.test.js +0 -135
  106. package/dist/providers/__tests__/create-selva-providers.test.js.map +0 -1
  107. package/dist/tokens/__tests__/token-codec.test.d.ts +0 -2
  108. package/dist/tokens/__tests__/token-codec.test.d.ts.map +0 -1
  109. package/dist/tokens/__tests__/token-codec.test.js +0 -50
  110. package/dist/tokens/__tests__/token-codec.test.js.map +0 -1
  111. package/src/access/__tests__/project-access-input.test.ts +0 -175
  112. package/src/access/index.ts +0 -9
  113. package/src/access/project-access-input.ts +0 -145
  114. package/src/compute/__tests__/client-cache.test.ts +0 -224
  115. package/src/compute/__tests__/rate-limit.test.ts +0 -106
  116. package/src/compute/__tests__/remote-definition.test.ts +0 -140
  117. package/src/compute/__tests__/safe-url.test.ts +0 -72
  118. package/src/compute/__tests__/solve-pipeline.test.ts +0 -341
  119. package/src/compute/__tests__/transform-input.test.ts +0 -162
  120. package/src/compute/client-cache.ts +0 -300
  121. package/src/compute/index.ts +0 -48
  122. package/src/compute/limits.ts +0 -159
  123. package/src/compute/rate-limit.ts +0 -110
  124. package/src/compute/remote-definition.ts +0 -149
  125. package/src/compute/safe-url.ts +0 -186
  126. package/src/compute/solve-pipeline.ts +0 -339
  127. package/src/compute/transform-input.ts +0 -66
  128. package/src/definitions/__tests__/load-for-render.test.ts +0 -244
  129. package/src/definitions/__tests__/schema-extraction.test.ts +0 -137
  130. package/src/definitions/definition-service.ts +0 -243
  131. package/src/definitions/index.ts +0 -22
  132. package/src/definitions/load-for-render.ts +0 -272
  133. package/src/definitions/schema-extraction.ts +0 -117
  134. package/src/errors/SentryErrorReporter.ts +0 -88
  135. package/src/errors/index.ts +0 -3
  136. package/src/http/__tests__/body-size.test.ts +0 -27
  137. package/src/http/__tests__/route-classifier.test.ts +0 -60
  138. package/src/http/__tests__/safe-redirect.test.ts +0 -47
  139. package/src/http/__tests__/security-headers.test.ts +0 -29
  140. package/src/http/body-size.ts +0 -24
  141. package/src/http/index.ts +0 -10
  142. package/src/http/redirect.ts +0 -16
  143. package/src/http/route-classifier.ts +0 -74
  144. package/src/http/security-headers.ts +0 -42
  145. package/src/index.ts +0 -27
  146. package/src/ops/__tests__/semver.test.ts +0 -42
  147. package/src/ops/index.ts +0 -3
  148. package/src/ops/semver.ts +0 -60
  149. package/src/providers/__tests__/create-selva-providers.test.ts +0 -172
  150. package/src/providers/__tests__/fixtures/override-config.mjs +0 -8
  151. package/src/providers/create-selva-providers.ts +0 -241
  152. package/src/providers/index.ts +0 -12
  153. package/src/tokens/__tests__/token-codec.test.ts +0 -63
  154. package/src/tokens/index.ts +0 -8
  155. package/src/tokens/token-codec.ts +0 -86
@@ -1,24 +0,0 @@
1
- /** Minimal read-side view of a request's headers — `Headers` satisfies it. */
2
- export interface HeadersLike {
3
- get(name: string): string | null;
4
- }
5
-
6
- /**
7
- * True when the request's declared `Content-Length` exceeds `maxBytes`.
8
- * Check BEFORE the body is read so a malicious client can't burn memory by
9
- * sending a huge payload to a small-body endpoint; the route maps a `true`
10
- * result to its transport's 413.
11
- *
12
- * Background: a global body-size limit (e.g. adapter-node's `BODY_SIZE_LIMIT`)
13
- * has to be set high enough for the largest legitimate upload, which means
14
- * smaller JSON endpoints inherit that ceiling by default. This check is the
15
- * per-route lower bound.
16
- *
17
- * Caveat: requests without `Content-Length` (chunked transfer encoding) bypass
18
- * this check. Most browsers and HTTP clients send Content-Length on POST/PUT.
19
- * The global limit is the backstop for those.
20
- */
21
- export function declaredBodySizeExceeds(headers: HeadersLike, maxBytes: number): boolean {
22
- const declared = Number(headers.get('content-length'));
23
- return Number.isFinite(declared) && declared > maxBytes;
24
- }
package/src/http/index.ts DELETED
@@ -1,10 +0,0 @@
1
- // HTTP hardening helpers — transport-agnostic request/response guards.
2
-
3
- export { safeRedirectTarget } from './redirect.js';
4
- export { declaredBodySizeExceeds, type HeadersLike } from './body-size.js';
5
- export { applySecurityHeaders, type SecurityHeaderOptions } from './security-headers.js';
6
- export {
7
- createRouteClassifier,
8
- type RouteClassifier,
9
- type RouteClassifierConfig
10
- } from './route-classifier.js';
@@ -1,16 +0,0 @@
1
- /**
2
- * Validate a user-supplied post-login redirect target. Accepts only same-origin
3
- * relative paths starting with `/` followed by a non-`/` character, so
4
- * `//evil.com/path` (protocol-relative URL — browser treats as cross-origin)
5
- * and `/\evil.com` (back-slash variants some browsers normalize) are rejected.
6
- *
7
- * Always returns a safe path: the validated target on success, the fallback
8
- * otherwise. Routes call this with `redirectTo` from form data or query string.
9
- */
10
- export function safeRedirectTarget(raw: string | null | undefined, fallback: string): string {
11
- if (typeof raw !== 'string' || raw.length < 2) return fallback;
12
- if (raw[0] !== '/') return fallback;
13
- // Reject protocol-relative (`//host`) and back-slash bypass (`/\host`).
14
- if (raw[1] === '/' || raw[1] === '\\') return fallback;
15
- return raw;
16
- }
@@ -1,74 +0,0 @@
1
- /**
2
- * Deny-by-default route classification. Every request is gated unless its
3
- * path matches one of the explicit allowlists supplied here — so adding a new
4
- * top-level route is safe: forgetting to update the lists makes it gated (a
5
- * loud 401/redirect), not silently public.
6
- *
7
- * The *pattern* is reusable; the *values* are app policy — a consuming app
8
- * supplies its own route sets. All matching is plain exact/prefix string
9
- * comparison: cheap enough for the hooks hot path, and no regex to get wrong.
10
- */
11
-
12
- export interface RouteClassifierConfig {
13
- /** Exact-match public pages — no session needed (e.g. `/`, `/login`). */
14
- publicPages?: Iterable<string>;
15
- /**
16
- * Public path prefixes (e.g. `/auth/` for OAuth start + callback — the
17
- * user has no session yet by definition).
18
- */
19
- publicPrefixes?: readonly string[];
20
- /**
21
- * Exact-match API endpoints that must answer without a session (e.g. a
22
- * load-balancer health probe). Anything added here must be safe to expose
23
- * to anonymous callers.
24
- */
25
- publicApis?: Iterable<string>;
26
- /**
27
- * A single prefix whose routes apply their own per-request authorization
28
- * (e.g. a blob proxy classifying each asset path). The hook must NOT deny
29
- * these up front; they classify as public (best-effort session attach) and
30
- * the route makes the real decision. Keep this to ONE prefix — every other
31
- * API route stays deny-by-default.
32
- */
33
- selfGatingPrefix?: string;
34
- /** Static-asset path prefixes the adapter serves directly (e.g. `/_app/`). */
35
- staticPrefixes?: readonly string[];
36
- /** Exact static-asset paths (e.g. `/favicon.svg`, `/robots.txt`). */
37
- staticPaths?: Iterable<string>;
38
- }
39
-
40
- export interface RouteClassifier {
41
- /** Static assets bypass auth gating; callers may add cache headers. */
42
- isStaticAsset(pathname: string): boolean;
43
- /** True for paths under `selfGatingPrefix` — the route gates itself. */
44
- isSelfGatingApiRoute(pathname: string): boolean;
45
- /** True when the path needs no session: pages, APIs, prefixes, self-gating. */
46
- isPublicRoute(pathname: string): boolean;
47
- }
48
-
49
- export function createRouteClassifier(config: RouteClassifierConfig): RouteClassifier {
50
- const publicPages = new Set(config.publicPages ?? []);
51
- const publicPrefixes = config.publicPrefixes ?? [];
52
- const publicApis = new Set(config.publicApis ?? []);
53
- const selfGatingPrefix = config.selfGatingPrefix;
54
- const staticPrefixes = config.staticPrefixes ?? [];
55
- const staticPaths = new Set(config.staticPaths ?? []);
56
-
57
- function isStaticAsset(pathname: string): boolean {
58
- if (staticPaths.has(pathname)) return true;
59
- return staticPrefixes.some((p) => pathname.startsWith(p));
60
- }
61
-
62
- function isSelfGatingApiRoute(pathname: string): boolean {
63
- return selfGatingPrefix !== undefined && pathname.startsWith(selfGatingPrefix);
64
- }
65
-
66
- function isPublicRoute(pathname: string): boolean {
67
- if (publicPages.has(pathname)) return true;
68
- if (publicApis.has(pathname)) return true;
69
- if (isSelfGatingApiRoute(pathname)) return true;
70
- return publicPrefixes.some((p) => pathname.startsWith(p));
71
- }
72
-
73
- return { isStaticAsset, isSelfGatingApiRoute, isPublicRoute };
74
- }
@@ -1,42 +0,0 @@
1
- export interface SecurityHeaderOptions {
2
- /**
3
- * Set `Strict-Transport-Security` — production only. Locking dev
4
- * (http://localhost) into HTTPS would brick local development, so the
5
- * caller decides what "production" means (e.g. `NODE_ENV`).
6
- */
7
- hsts: boolean;
8
- }
9
-
10
- /**
11
- * Apply baseline browser-hardening headers to a response. Cheap wins that
12
- * don't require UI verification:
13
- *
14
- * - X-Content-Type-Options: nosniff — disables MIME sniffing.
15
- * - Referrer-Policy: strict-origin-when-cross-origin — strips the path
16
- * and query from cross-origin Referer headers; matches modern browser
17
- * defaults but pins the behavior in case the default ever drifts.
18
- * - Permissions-Policy: opt out of browser APIs Selva-engine apps don't
19
- * use, so XSS can't enable them.
20
- * - HSTS when `opts.hsts` (production).
21
- *
22
- * **Intentionally NOT set here:**
23
- * - Content-Security-Policy + frame-ancestors. Selva-engine apps are built
24
- * for iframe embedding, so a strict CSP needs UI-phase validation against
25
- * real consumer sites before it can ship.
26
- * - X-Frame-Options. Same iframe-embedding constraint.
27
- * - Cache-Control. Asset-path layout is app policy; set it in the caller.
28
- *
29
- * Returns the same response for chaining.
30
- */
31
- export function applySecurityHeaders(response: Response, opts: SecurityHeaderOptions): Response {
32
- response.headers.set('X-Content-Type-Options', 'nosniff');
33
- response.headers.set('Referrer-Policy', 'strict-origin-when-cross-origin');
34
- response.headers.set(
35
- 'Permissions-Policy',
36
- 'accelerometer=(), camera=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), payment=(), usb=()'
37
- );
38
- if (opts.hsts) {
39
- response.headers.set('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
40
- }
41
- return response;
42
- }
package/src/index.ts DELETED
@@ -1,27 +0,0 @@
1
- /**
2
- * `@selvajs/server` — transport-agnostic compute/solve server building blocks
3
- * for apps built on the Selva engine.
4
- *
5
- * The compute stack (limits, per-key rate limiting, SSRF guard, input
6
- * transform) that used to live inside the `@selvajs/selva` app, extracted so a
7
- * consuming app can reuse it instead of copying it. Everything here is
8
- * env-injected (no ambient `$env` / `process.env`) and framework-agnostic.
9
- *
10
- * The full compute subsurface is also available at `@selvajs/server/compute`,
11
- * the definitions slice (DefinitionService, schema extraction/validation,
12
- * render loader) at `@selvajs/server/definitions`, the provider wiring
13
- * (registry-driven `createSelvaProviders`) at `@selvajs/server/providers`,
14
- * the HMAC token codec at `@selvajs/server/tokens`, error reporting at
15
- * `@selvajs/server/errors`, HTTP hardening helpers at `@selvajs/server/http`,
16
- * access-rule input assembly at `@selvajs/server/access`, and deployment/ops
17
- * helpers at `@selvajs/server/ops`.
18
- */
19
-
20
- export * from './compute/index.js';
21
- export * from './definitions/index.js';
22
- export * from './providers/index.js';
23
- export * from './tokens/index.js';
24
- export * from './errors/index.js';
25
- export * from './http/index.js';
26
- export * from './access/index.js';
27
- export * from './ops/index.js';
@@ -1,42 +0,0 @@
1
- import { describe, it, expect } from 'vitest';
2
- import { isNewer } from '../semver.js';
3
-
4
- describe('isNewer', () => {
5
- it('detects a newer patch / minor / major', () => {
6
- expect(isNewer('4.3.2', '4.3.1')).toBe(true);
7
- expect(isNewer('4.4.0', '4.3.9')).toBe(true);
8
- expect(isNewer('5.0.0', '4.9.9')).toBe(true);
9
- });
10
-
11
- it('returns false for same or older versions', () => {
12
- expect(isNewer('4.3.1', '4.3.1')).toBe(false);
13
- expect(isNewer('4.3.0', '4.3.1')).toBe(false);
14
- expect(isNewer('3.9.9', '4.0.0')).toBe(false);
15
- });
16
-
17
- it('ignores pre-release suffixes on the stable channel (only stable bumps surface)', () => {
18
- expect(isNewer('4.3.2-beta.1', '4.3.1')).toBe(true);
19
- expect(isNewer('4.3.1-beta.1', '4.3.1')).toBe(false);
20
- // Explicit stable channel matches the default.
21
- expect(isNewer('4.3.1-beta.1', '4.3.1', 'stable')).toBe(false);
22
- });
23
-
24
- it('orders pre-releases on the beta channel', () => {
25
- // Successive betas of the same core.
26
- expect(isNewer('4.6.0-beta.2', '4.6.0-beta.1', 'beta')).toBe(true);
27
- expect(isNewer('4.6.0-beta.1', '4.6.0-beta.2', 'beta')).toBe(false);
28
- // A newer core beta beats an older stable.
29
- expect(isNewer('4.6.0-beta.1', '4.5.1', 'beta')).toBe(true);
30
- // Stable of a core outranks any beta of the same core (promotion).
31
- expect(isNewer('4.6.0', '4.6.0-beta.9', 'beta')).toBe(true);
32
- expect(isNewer('4.6.0-beta.9', '4.6.0', 'beta')).toBe(false);
33
- // Reverting to an OLDER stable is NOT "newer" — the channel switch, not
34
- // isNewer, is what makes it actionable.
35
- expect(isNewer('4.5.1', '4.6.0-beta.2', 'beta')).toBe(false);
36
- });
37
-
38
- it('falls back to a string compare when unparseable', () => {
39
- expect(isNewer('weird', 'weird')).toBe(false);
40
- expect(isNewer('a', 'b')).toBe(true);
41
- });
42
- });
package/src/ops/index.ts DELETED
@@ -1,3 +0,0 @@
1
- // Deployment/ops helpers — channel-aware semver comparison.
2
-
3
- export { parseSemver, isNewer, type ReleaseChannel } from './semver.js';
package/src/ops/semver.ts DELETED
@@ -1,60 +0,0 @@
1
- /**
2
- * Channel-aware semver comparison for update checks. Understands exactly the
3
- * two-channel model Selva-engine deployments publish under: a stable line
4
- * (`x.y.z`, npm `latest`) and a beta line (`x.y.z-beta.N`, npm `beta`).
5
- * Deliberately not a general semver implementation — no build metadata, no
6
- * arbitrary pre-release identifiers.
7
- */
8
-
9
- /** Which published line a deployment tracks. */
10
- export type ReleaseChannel = 'stable' | 'beta';
11
-
12
- /**
13
- * Parse `x.y.z` plus an optional `-beta.N` pre-release counter. The
14
- * pre-release number is returned separately so beta-channel comparisons can
15
- * order successive betas of the same x.y.z core. Returns null for anything
16
- * that doesn't lead with a numeric core.
17
- */
18
- export function parseSemver(
19
- v: string
20
- ): { core: [number, number, number]; beta: number | null } | null {
21
- const m = /^(\d+)\.(\d+)\.(\d+)(?:-beta\.(\d+))?/.exec(v.trim());
22
- if (!m) return null;
23
- return {
24
- core: [Number(m[1]), Number(m[2]), Number(m[3])],
25
- beta: m[4] != null ? Number(m[4]) : null
26
- };
27
- }
28
-
29
- /**
30
- * True when `latest` is strictly newer than `current` by semver.
31
- *
32
- * Stable channel (default): pre-release suffixes are ignored — only stable
33
- * core bumps surface.
34
- *
35
- * Beta channel: pre-releases participate. A higher core wins; on an equal
36
- * core, a higher `-beta.N` wins, and a stable (no suffix) outranks any -beta
37
- * of the same core (4.6.0 > 4.6.0-beta.9). This lets an update UI show
38
- * "beta.1 → beta.2" and "beta → stable promotion of the same line".
39
- *
40
- * Unparseable versions fall back to inequality: different strings count as
41
- * "newer" so a weird published version still surfaces, identical ones don't.
42
- */
43
- export function isNewer(
44
- latest: string,
45
- current: string,
46
- channel: ReleaseChannel = 'stable'
47
- ): boolean {
48
- const a = parseSemver(latest);
49
- const b = parseSemver(current);
50
- if (!a || !b) return latest !== current;
51
- for (let i = 0; i < 3; i++) {
52
- if (a.core[i] > b.core[i]) return true;
53
- if (a.core[i] < b.core[i]) return false;
54
- }
55
- // Equal core. On the stable channel we don't compare pre-release tails.
56
- if (channel !== 'beta') return false;
57
- // Stable (beta == null) ranks above any pre-release of the same core.
58
- const rank = (beta: number | null) => (beta == null ? Infinity : beta);
59
- return rank(a.beta) > rank(b.beta);
60
- }
@@ -1,172 +0,0 @@
1
- /**
2
- * Tests for `createSelvaProviders` (K5) — registry-driven provider selection,
3
- * lazy memoized instantiation, config parsing (flags/branding/tenancy), the
4
- * solve-metrics duck-type pick, and the external config-path override.
5
- */
6
-
7
- import { describe, it, expect, vi } from 'vitest';
8
- import { fileURLToPath } from 'node:url';
9
- import type { IAuthProvider, IDataProvider, IStorageProvider } from '@selvajs/platform';
10
- import { createSelvaProviders, type ProviderRegistry } from '../create-selva-providers.js';
11
-
12
- class FakeData {}
13
- class FakeStorage {}
14
-
15
- function makeRegistry() {
16
- const authLocal = vi.fn(() => ({ name: 'local-auth' }) as unknown as IAuthProvider);
17
- const authSupabase = vi.fn(() => ({ name: 'supabase-auth' }) as unknown as IAuthProvider);
18
- const dataLocal = vi.fn(() => new FakeData() as unknown as IDataProvider);
19
- const dataSupabase = vi.fn(() => new FakeData() as unknown as IDataProvider);
20
- const storageLocal = vi.fn(() => new FakeStorage() as unknown as IStorageProvider);
21
- const registry: ProviderRegistry = {
22
- auth: { local: authLocal, supabase: authSupabase },
23
- data: { local: dataLocal, supabase: dataSupabase },
24
- storage: { local: storageLocal }
25
- };
26
- return { registry, authLocal, authSupabase, dataLocal, dataSupabase, storageLocal };
27
- }
28
-
29
- const noBoot = { onBoot: () => {} };
30
-
31
- describe('registry selection', () => {
32
- it('falls back to "local" for every slot when env is empty', async () => {
33
- const { registry, authLocal, dataLocal, storageLocal } = makeRegistry();
34
- const runtime = await createSelvaProviders({}, { registry, ...noBoot });
35
- runtime.resolve();
36
- expect(authLocal).toHaveBeenCalledTimes(1);
37
- expect(dataLocal).toHaveBeenCalledTimes(1);
38
- expect(storageLocal).toHaveBeenCalledTimes(1);
39
- });
40
-
41
- it('honors SELVA_*_PROVIDER env choices, case-insensitively', async () => {
42
- const { registry, authSupabase, dataSupabase } = makeRegistry();
43
- const runtime = await createSelvaProviders(
44
- { SELVA_AUTH_PROVIDER: 'Supabase', SELVA_DATA_PROVIDER: 'SUPABASE' },
45
- { registry, ...noBoot }
46
- );
47
- runtime.resolve();
48
- expect(authSupabase).toHaveBeenCalledTimes(1);
49
- expect(dataSupabase).toHaveBeenCalledTimes(1);
50
- });
51
-
52
- it('honors the defaults option over the built-in "local"', async () => {
53
- const { registry, authSupabase } = makeRegistry();
54
- const runtime = await createSelvaProviders(
55
- {},
56
- { registry, defaults: { auth: 'supabase' }, ...noBoot }
57
- );
58
- runtime.resolve();
59
- expect(authSupabase).toHaveBeenCalledTimes(1);
60
- });
61
-
62
- it('rejects an unknown choice, listing the registered names', async () => {
63
- const { registry } = makeRegistry();
64
- const runtime = await createSelvaProviders(
65
- { SELVA_AUTH_PROVIDER: 'okta' },
66
- { registry, ...noBoot }
67
- );
68
- expect(() => runtime.resolve()).toThrow(/SELVA_AUTH_PROVIDER="okta".*local \| supabase/);
69
- });
70
- });
71
-
72
- describe('laziness and memoization', () => {
73
- it('does not instantiate providers until resolve() is called', async () => {
74
- const { registry, authLocal } = makeRegistry();
75
- await createSelvaProviders({}, { registry, ...noBoot });
76
- expect(authLocal).not.toHaveBeenCalled();
77
- });
78
-
79
- it('instantiates once and logs the boot summary once across repeated resolves', async () => {
80
- const { registry, authLocal } = makeRegistry();
81
- const onBoot = vi.fn();
82
- const runtime = await createSelvaProviders({}, { registry, onBoot });
83
- runtime.resolve();
84
- runtime.resolve();
85
- runtime.flag('ENABLE_SHARING');
86
- expect(authLocal).toHaveBeenCalledTimes(1);
87
- expect(onBoot).toHaveBeenCalledTimes(1);
88
- expect(onBoot.mock.calls[0][0]).toContain('auth=local-auth');
89
- expect(onBoot.mock.calls[0][0]).toContain('data=FakeData');
90
- expect(onBoot.mock.calls[0][0]).toContain('tenancy=single');
91
- });
92
- });
93
-
94
- describe('config parsing', () => {
95
- it('parses flags from SELVA_FLAG_* (omitted → false)', async () => {
96
- const { registry } = makeRegistry();
97
- const runtime = await createSelvaProviders(
98
- { SELVA_FLAG_ENABLE_SHARING: 'true', SELVA_FLAG_ALLOW_ORG_CREATION: 'no' },
99
- { registry, ...noBoot }
100
- );
101
- expect(runtime.flag('ENABLE_SHARING')).toBe(true);
102
- expect(runtime.flag('ALLOW_ORG_CREATION')).toBe(false);
103
- expect(runtime.flag('ALLOW_CROSS_ORG_PUBLIC')).toBe(false);
104
- });
105
-
106
- it('parses tenancy and rejects unknown values', async () => {
107
- const { registry } = makeRegistry();
108
- const multi = await createSelvaProviders({ SELVA_TENANCY: 'multi' }, { registry, ...noBoot });
109
- expect(multi.tenancy()).toBe('multi');
110
-
111
- const bad = await createSelvaProviders({ SELVA_TENANCY: 'galactic' }, { registry, ...noBoot });
112
- expect(() => bad.tenancy()).toThrow(/SELVA_TENANCY="galactic"/);
113
- });
114
-
115
- it('brands with defaults; copyright falls back to the overridden name', async () => {
116
- const { registry } = makeRegistry();
117
- const plain = await createSelvaProviders({}, { registry, ...noBoot });
118
- expect(plain.branding().name).toBe('Selva');
119
- expect(plain.branding().copyrightName).toBe('Selva');
120
- expect(plain.branding().tagline).toBeTruthy();
121
-
122
- const branded = await createSelvaProviders(
123
- { SELVA_BRAND_NAME: ' Acme ' },
124
- { registry, ...noBoot }
125
- );
126
- expect(branded.branding().name).toBe('Acme');
127
- expect(branded.branding().copyrightName).toBe('Acme');
128
- expect(branded.branding().description).toContain('Acme');
129
- });
130
- });
131
-
132
- describe('solve metric sink', () => {
133
- it('falls back to a noop sink when the data provider has no solveMetrics', async () => {
134
- const { registry } = makeRegistry();
135
- const runtime = await createSelvaProviders({}, { registry, ...noBoot });
136
- const sink = runtime.solveMetricSink();
137
- expect(typeof sink.record).toBe('function');
138
- expect(runtime.solveMetricSink()).toBe(sink); // memoized
139
- });
140
-
141
- it('reuses a duck-typed solveMetrics sink carried by the data provider', async () => {
142
- const { registry } = makeRegistry();
143
- const carried = { record: vi.fn() };
144
- registry.data.local = vi.fn(() => ({ solveMetrics: carried }) as unknown as IDataProvider);
145
- const runtime = await createSelvaProviders({}, { registry, ...noBoot });
146
- expect(runtime.solveMetricSink()).toBe(carried);
147
- });
148
- });
149
-
150
- describe('config-path override', () => {
151
- it('loads the module default export and hands it the env', async () => {
152
- const { registry, authLocal } = makeRegistry();
153
- const configPath = fileURLToPath(new URL('./fixtures/override-config.mjs', import.meta.url));
154
- const runtime = await createSelvaProviders(
155
- { OVERRIDE_MARKER: 'hello' },
156
- { registry, configPath, ...noBoot }
157
- );
158
- const config = runtime.resolve();
159
- expect(config.auth.name).toBe('override-auth');
160
- expect((config.data as unknown as { marker: string }).marker).toBe('hello');
161
- expect(runtime.tenancy()).toBe('multi');
162
- // The registry is bypassed entirely.
163
- expect(authLocal).not.toHaveBeenCalled();
164
- });
165
-
166
- it('rejects at load time when the path does not exist', async () => {
167
- const { registry } = makeRegistry();
168
- await expect(
169
- createSelvaProviders({}, { registry, configPath: './does-not-exist.js', ...noBoot })
170
- ).rejects.toThrow(/does-not-exist\.js/);
171
- });
172
- });
@@ -1,8 +0,0 @@
1
- // Fixture for the SELVA_CONFIG_PATH override test — a minimal selva.config.js
2
- // whose default export is a config factory, exactly as an operator would write.
3
- export default (env) => ({
4
- tenancy: 'multi',
5
- auth: { name: 'override-auth' },
6
- data: { marker: env.OVERRIDE_MARKER },
7
- storage: {}
8
- });