@selvajs/server 0.2.0-beta.1 → 0.2.0-beta.3
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.
- package/dist/access/__tests__/project-access-input.test.d.ts +2 -0
- package/dist/access/__tests__/project-access-input.test.d.ts.map +1 -0
- package/dist/access/__tests__/project-access-input.test.js +146 -0
- package/dist/access/__tests__/project-access-input.test.js.map +1 -0
- package/dist/access/index.d.ts +2 -0
- package/dist/access/index.d.ts.map +1 -0
- package/dist/access/index.js +4 -0
- package/dist/access/index.js.map +1 -0
- package/dist/access/project-access-input.d.ts +52 -0
- package/dist/access/project-access-input.d.ts.map +1 -0
- package/dist/access/project-access-input.js +56 -0
- package/dist/access/project-access-input.js.map +1 -0
- package/dist/compute/__tests__/client-cache.test.js +70 -1
- package/dist/compute/__tests__/client-cache.test.js.map +1 -1
- package/dist/compute/__tests__/definition-byte-cache.test.d.ts +2 -0
- package/dist/compute/__tests__/definition-byte-cache.test.d.ts.map +1 -0
- package/dist/compute/__tests__/definition-byte-cache.test.js +107 -0
- package/dist/compute/__tests__/definition-byte-cache.test.js.map +1 -0
- package/dist/compute/__tests__/rate-limit.test.js +36 -0
- package/dist/compute/__tests__/rate-limit.test.js.map +1 -1
- package/dist/compute/__tests__/solve-cache.test.d.ts +13 -0
- package/dist/compute/__tests__/solve-cache.test.d.ts.map +1 -0
- package/dist/compute/__tests__/solve-cache.test.js +234 -0
- package/dist/compute/__tests__/solve-cache.test.js.map +1 -0
- package/dist/compute/__tests__/solve-pipeline.test.js +327 -15
- package/dist/compute/__tests__/solve-pipeline.test.js.map +1 -1
- package/dist/compute/__tests__/transform-input.test.js +7 -1
- package/dist/compute/__tests__/transform-input.test.js.map +1 -1
- package/dist/compute/client-cache.d.ts +32 -8
- package/dist/compute/client-cache.d.ts.map +1 -1
- package/dist/compute/client-cache.js +24 -6
- package/dist/compute/client-cache.js.map +1 -1
- package/dist/compute/definition-byte-cache.d.ts +85 -0
- package/dist/compute/definition-byte-cache.d.ts.map +1 -0
- package/dist/compute/definition-byte-cache.js +110 -0
- package/dist/compute/definition-byte-cache.js.map +1 -0
- package/dist/compute/index.d.ts +7 -2
- package/dist/compute/index.d.ts.map +1 -1
- package/dist/compute/index.js +6 -1
- package/dist/compute/index.js.map +1 -1
- package/dist/compute/limits.d.ts +62 -0
- package/dist/compute/limits.d.ts.map +1 -1
- package/dist/compute/limits.js +35 -1
- package/dist/compute/limits.js.map +1 -1
- package/dist/compute/memory-solve-cache.d.ts +58 -0
- package/dist/compute/memory-solve-cache.d.ts.map +1 -0
- package/dist/compute/memory-solve-cache.js +0 -0
- package/dist/compute/memory-solve-cache.js.map +1 -0
- package/dist/compute/rate-limit.d.ts +12 -0
- package/dist/compute/rate-limit.d.ts.map +1 -1
- package/dist/compute/rate-limit.js +18 -0
- package/dist/compute/rate-limit.js.map +1 -1
- package/dist/compute/solve-cache-envelope.d.ts +53 -0
- package/dist/compute/solve-cache-envelope.d.ts.map +1 -0
- package/dist/compute/solve-cache-envelope.js +66 -0
- package/dist/compute/solve-cache-envelope.js.map +1 -0
- package/dist/compute/solve-cache-key.d.ts +65 -0
- package/dist/compute/solve-cache-key.d.ts.map +1 -0
- package/dist/compute/solve-cache-key.js +50 -0
- package/dist/compute/solve-cache-key.js.map +1 -0
- package/dist/compute/solve-cache-single-flight.d.ts +35 -0
- package/dist/compute/solve-cache-single-flight.d.ts.map +1 -0
- package/dist/compute/solve-cache-single-flight.js +48 -0
- package/dist/compute/solve-cache-single-flight.js.map +1 -0
- package/dist/compute/solve-pipeline.d.ts +71 -8
- package/dist/compute/solve-pipeline.d.ts.map +1 -1
- package/dist/compute/solve-pipeline.js +198 -15
- package/dist/compute/solve-pipeline.js.map +1 -1
- package/dist/compute/transform-input.d.ts.map +1 -1
- package/dist/compute/transform-input.js +21 -6
- package/dist/compute/transform-input.js.map +1 -1
- package/dist/definitions/__tests__/definition-service.test.d.ts +14 -0
- package/dist/definitions/__tests__/definition-service.test.d.ts.map +1 -0
- package/dist/definitions/__tests__/definition-service.test.js +95 -0
- package/dist/definitions/__tests__/definition-service.test.js.map +1 -0
- package/dist/definitions/definition-service.d.ts +1 -1
- package/dist/definitions/definition-service.d.ts.map +1 -1
- package/dist/definitions/definition-service.js +8 -4
- package/dist/definitions/definition-service.js.map +1 -1
- package/dist/errors/SentryErrorReporter.d.ts +17 -0
- package/dist/errors/SentryErrorReporter.d.ts.map +1 -0
- package/dist/errors/SentryErrorReporter.js +60 -0
- package/dist/errors/SentryErrorReporter.js.map +1 -0
- package/dist/errors/index.d.ts +2 -0
- package/dist/errors/index.d.ts.map +1 -0
- package/dist/errors/index.js +3 -0
- package/dist/errors/index.js.map +1 -0
- package/dist/http/__tests__/body-size.test.d.ts +2 -0
- package/dist/http/__tests__/body-size.test.d.ts.map +1 -0
- package/dist/http/__tests__/body-size.test.js +24 -0
- package/dist/http/__tests__/body-size.test.js.map +1 -0
- package/dist/http/__tests__/route-classifier.test.d.ts +2 -0
- package/dist/http/__tests__/route-classifier.test.d.ts.map +1 -0
- package/dist/http/__tests__/route-classifier.test.js +54 -0
- package/dist/http/__tests__/route-classifier.test.js.map +1 -0
- package/dist/http/__tests__/safe-redirect.test.d.ts +2 -0
- package/dist/http/__tests__/safe-redirect.test.d.ts.map +1 -0
- package/dist/http/__tests__/safe-redirect.test.js +44 -0
- package/dist/http/__tests__/safe-redirect.test.js.map +1 -0
- package/dist/http/__tests__/security-headers.test.d.ts +2 -0
- package/dist/http/__tests__/security-headers.test.d.ts.map +1 -0
- package/dist/http/__tests__/security-headers.test.js +24 -0
- package/dist/http/__tests__/security-headers.test.js.map +1 -0
- package/dist/http/body-size.d.ts +21 -0
- package/dist/http/body-size.d.ts.map +1 -0
- package/dist/http/body-size.js +20 -0
- package/dist/http/body-size.js.map +1 -0
- package/dist/http/index.d.ts +5 -0
- package/dist/http/index.d.ts.map +1 -0
- package/dist/http/index.js +6 -0
- package/dist/http/index.js.map +1 -0
- package/dist/http/redirect.d.ts +11 -0
- package/dist/http/redirect.d.ts.map +1 -0
- package/dist/http/redirect.js +20 -0
- package/dist/http/redirect.js.map +1 -0
- package/dist/http/route-classifier.d.ts +47 -0
- package/dist/http/route-classifier.d.ts.map +1 -0
- package/dist/http/route-classifier.js +37 -0
- package/dist/http/route-classifier.js.map +1 -0
- package/dist/http/security-headers.d.ts +31 -0
- package/dist/http/security-headers.d.ts.map +1 -0
- package/dist/http/security-headers.js +31 -0
- package/dist/http/security-headers.js.map +1 -0
- package/dist/index.d.ts +11 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11 -2
- package/dist/index.js.map +1 -1
- package/dist/ops/__tests__/semver.test.d.ts +2 -0
- package/dist/ops/__tests__/semver.test.d.ts.map +1 -0
- package/dist/ops/__tests__/semver.test.js +38 -0
- package/dist/ops/__tests__/semver.test.js.map +1 -0
- package/dist/ops/index.d.ts +2 -0
- package/dist/ops/index.d.ts.map +1 -0
- package/dist/ops/index.js +3 -0
- package/dist/ops/index.js.map +1 -0
- package/dist/ops/semver.d.ts +35 -0
- package/dist/ops/semver.d.ts.map +1 -0
- package/dist/ops/semver.js +55 -0
- package/dist/ops/semver.js.map +1 -0
- package/dist/tokens/__tests__/token-codec.test.d.ts +2 -0
- package/dist/tokens/__tests__/token-codec.test.d.ts.map +1 -0
- package/dist/tokens/__tests__/token-codec.test.js +50 -0
- package/dist/tokens/__tests__/token-codec.test.js.map +1 -0
- package/dist/tokens/index.d.ts +2 -0
- package/dist/tokens/index.d.ts.map +1 -0
- package/dist/tokens/index.js +3 -0
- package/dist/tokens/index.js.map +1 -0
- package/dist/tokens/token-codec.d.ts +54 -0
- package/dist/tokens/token-codec.d.ts.map +1 -0
- package/dist/tokens/token-codec.js +62 -0
- package/dist/tokens/token-codec.js.map +1 -0
- package/package.json +35 -2
- package/src/access/__tests__/project-access-input.test.ts +175 -0
- package/src/access/index.ts +9 -0
- package/src/access/project-access-input.ts +145 -0
- package/src/compute/__tests__/client-cache.test.ts +81 -1
- package/src/compute/__tests__/definition-byte-cache.test.ts +129 -0
- package/src/compute/__tests__/rate-limit.test.ts +37 -0
- package/src/compute/__tests__/solve-cache.test.ts +268 -0
- package/src/compute/__tests__/solve-pipeline.test.ts +380 -14
- package/src/compute/__tests__/transform-input.test.ts +8 -1
- package/src/compute/client-cache.ts +59 -16
- package/src/compute/definition-byte-cache.ts +168 -0
- package/src/compute/index.ts +34 -0
- package/src/compute/limits.ts +91 -1
- package/src/compute/memory-solve-cache.ts +0 -0
- package/src/compute/rate-limit.ts +27 -0
- package/src/compute/solve-cache-envelope.ts +98 -0
- package/src/compute/solve-cache-key.ts +85 -0
- package/src/compute/solve-cache-single-flight.ts +59 -0
- package/src/compute/solve-pipeline.ts +301 -22
- package/src/compute/transform-input.ts +20 -6
- package/src/definitions/__tests__/definition-service.test.ts +131 -0
- package/src/definitions/definition-service.ts +8 -4
- package/src/errors/SentryErrorReporter.ts +88 -0
- package/src/errors/index.ts +3 -0
- package/src/http/__tests__/body-size.test.ts +27 -0
- package/src/http/__tests__/route-classifier.test.ts +60 -0
- package/src/http/__tests__/safe-redirect.test.ts +47 -0
- package/src/http/__tests__/security-headers.test.ts +29 -0
- package/src/http/body-size.ts +24 -0
- package/src/http/index.ts +10 -0
- package/src/http/redirect.ts +16 -0
- package/src/http/route-classifier.ts +74 -0
- package/src/http/security-headers.ts +42 -0
- package/src/index.ts +11 -2
- package/src/ops/__tests__/semver.test.ts +42 -0
- package/src/ops/index.ts +3 -0
- package/src/ops/semver.ts +60 -0
- package/src/tokens/__tests__/token-codec.test.ts +63 -0
- package/src/tokens/index.ts +8 -0
- package/src/tokens/token-codec.ts +86 -0
package/dist/index.js
CHANGED
|
@@ -9,10 +9,19 @@
|
|
|
9
9
|
*
|
|
10
10
|
* The full compute subsurface is also available at `@selvajs/server/compute`,
|
|
11
11
|
* the definitions slice (DefinitionService, schema extraction/validation,
|
|
12
|
-
* render loader) at `@selvajs/server/definitions`,
|
|
13
|
-
* (registry-driven `createSelvaProviders`) at `@selvajs/server/providers
|
|
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`.
|
|
14
18
|
*/
|
|
15
19
|
export * from './compute/index.js';
|
|
16
20
|
export * from './definitions/index.js';
|
|
17
21
|
export * from './providers/index.js';
|
|
22
|
+
export * from './tokens/index.js';
|
|
23
|
+
export * from './errors/index.js';
|
|
24
|
+
export * from './http/index.js';
|
|
25
|
+
export * from './access/index.js';
|
|
26
|
+
export * from './ops/index.js';
|
|
18
27
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,cAAc,oBAAoB,CAAC;AACnC,cAAc,wBAAwB,CAAC;AACvC,cAAc,sBAAsB,CAAC;AACrC,cAAc,mBAAmB,CAAC;AAClC,cAAc,mBAAmB,CAAC;AAClC,cAAc,iBAAiB,CAAC;AAChC,cAAc,mBAAmB,CAAC;AAClC,cAAc,gBAAgB,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"semver.test.d.ts","sourceRoot":"","sources":["../../../src/ops/__tests__/semver.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { isNewer } from '../semver.js';
|
|
3
|
+
describe('isNewer', () => {
|
|
4
|
+
it('detects a newer patch / minor / major', () => {
|
|
5
|
+
expect(isNewer('4.3.2', '4.3.1')).toBe(true);
|
|
6
|
+
expect(isNewer('4.4.0', '4.3.9')).toBe(true);
|
|
7
|
+
expect(isNewer('5.0.0', '4.9.9')).toBe(true);
|
|
8
|
+
});
|
|
9
|
+
it('returns false for same or older versions', () => {
|
|
10
|
+
expect(isNewer('4.3.1', '4.3.1')).toBe(false);
|
|
11
|
+
expect(isNewer('4.3.0', '4.3.1')).toBe(false);
|
|
12
|
+
expect(isNewer('3.9.9', '4.0.0')).toBe(false);
|
|
13
|
+
});
|
|
14
|
+
it('ignores pre-release suffixes on the stable channel (only stable bumps surface)', () => {
|
|
15
|
+
expect(isNewer('4.3.2-beta.1', '4.3.1')).toBe(true);
|
|
16
|
+
expect(isNewer('4.3.1-beta.1', '4.3.1')).toBe(false);
|
|
17
|
+
// Explicit stable channel matches the default.
|
|
18
|
+
expect(isNewer('4.3.1-beta.1', '4.3.1', 'stable')).toBe(false);
|
|
19
|
+
});
|
|
20
|
+
it('orders pre-releases on the beta channel', () => {
|
|
21
|
+
// Successive betas of the same core.
|
|
22
|
+
expect(isNewer('4.6.0-beta.2', '4.6.0-beta.1', 'beta')).toBe(true);
|
|
23
|
+
expect(isNewer('4.6.0-beta.1', '4.6.0-beta.2', 'beta')).toBe(false);
|
|
24
|
+
// A newer core beta beats an older stable.
|
|
25
|
+
expect(isNewer('4.6.0-beta.1', '4.5.1', 'beta')).toBe(true);
|
|
26
|
+
// Stable of a core outranks any beta of the same core (promotion).
|
|
27
|
+
expect(isNewer('4.6.0', '4.6.0-beta.9', 'beta')).toBe(true);
|
|
28
|
+
expect(isNewer('4.6.0-beta.9', '4.6.0', 'beta')).toBe(false);
|
|
29
|
+
// Reverting to an OLDER stable is NOT "newer" — the channel switch, not
|
|
30
|
+
// isNewer, is what makes it actionable.
|
|
31
|
+
expect(isNewer('4.5.1', '4.6.0-beta.2', 'beta')).toBe(false);
|
|
32
|
+
});
|
|
33
|
+
it('falls back to a string compare when unparseable', () => {
|
|
34
|
+
expect(isNewer('weird', 'weird')).toBe(false);
|
|
35
|
+
expect(isNewer('a', 'b')).toBe(true);
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
//# sourceMappingURL=semver.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"semver.test.js","sourceRoot":"","sources":["../../../src/ops/__tests__/semver.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAC9C,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAEvC,QAAQ,CAAC,SAAS,EAAE,GAAG,EAAE;IACxB,EAAE,CAAC,uCAAuC,EAAE,GAAG,EAAE;QAChD,MAAM,CAAC,OAAO,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAC7C,MAAM,CAAC,OAAO,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAC7C,MAAM,CAAC,OAAO,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC9C,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,0CAA0C,EAAE,GAAG,EAAE;QACnD,MAAM,CAAC,OAAO,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAC9C,MAAM,CAAC,OAAO,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAC9C,MAAM,CAAC,OAAO,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IAC/C,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,gFAAgF,EAAE,GAAG,EAAE;QACzF,MAAM,CAAC,OAAO,CAAC,cAAc,EAAE,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACpD,MAAM,CAAC,OAAO,CAAC,cAAc,EAAE,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QACrD,+CAA+C;QAC/C,MAAM,CAAC,OAAO,CAAC,cAAc,EAAE,OAAO,EAAE,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IAChE,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,yCAAyC,EAAE,GAAG,EAAE;QAClD,qCAAqC;QACrC,MAAM,CAAC,OAAO,CAAC,cAAc,EAAE,cAAc,EAAE,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACnE,MAAM,CAAC,OAAO,CAAC,cAAc,EAAE,cAAc,EAAE,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QACpE,2CAA2C;QAC3C,MAAM,CAAC,OAAO,CAAC,cAAc,EAAE,OAAO,EAAE,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAC5D,mEAAmE;QACnE,MAAM,CAAC,OAAO,CAAC,OAAO,EAAE,cAAc,EAAE,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAC5D,MAAM,CAAC,OAAO,CAAC,cAAc,EAAE,OAAO,EAAE,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAC7D,wEAAwE;QACxE,wCAAwC;QACxC,MAAM,CAAC,OAAO,CAAC,OAAO,EAAE,cAAc,EAAE,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IAC9D,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,iDAAiD,EAAE,GAAG,EAAE;QAC1D,MAAM,CAAC,OAAO,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAC9C,MAAM,CAAC,OAAO,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACtC,CAAC,CAAC,CAAC;AACJ,CAAC,CAAC,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/ops/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,WAAW,EAAE,OAAO,EAAE,KAAK,cAAc,EAAE,MAAM,aAAa,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/ops/index.ts"],"names":[],"mappings":"AAAA,4DAA4D;AAE5D,OAAO,EAAE,WAAW,EAAE,OAAO,EAAuB,MAAM,aAAa,CAAC"}
|
|
@@ -0,0 +1,35 @@
|
|
|
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
|
+
/** Which published line a deployment tracks. */
|
|
9
|
+
export type ReleaseChannel = 'stable' | 'beta';
|
|
10
|
+
/**
|
|
11
|
+
* Parse `x.y.z` plus an optional `-beta.N` pre-release counter. The
|
|
12
|
+
* pre-release number is returned separately so beta-channel comparisons can
|
|
13
|
+
* order successive betas of the same x.y.z core. Returns null for anything
|
|
14
|
+
* that doesn't lead with a numeric core.
|
|
15
|
+
*/
|
|
16
|
+
export declare function parseSemver(v: string): {
|
|
17
|
+
core: [number, number, number];
|
|
18
|
+
beta: number | null;
|
|
19
|
+
} | null;
|
|
20
|
+
/**
|
|
21
|
+
* True when `latest` is strictly newer than `current` by semver.
|
|
22
|
+
*
|
|
23
|
+
* Stable channel (default): pre-release suffixes are ignored — only stable
|
|
24
|
+
* core bumps surface.
|
|
25
|
+
*
|
|
26
|
+
* Beta channel: pre-releases participate. A higher core wins; on an equal
|
|
27
|
+
* core, a higher `-beta.N` wins, and a stable (no suffix) outranks any -beta
|
|
28
|
+
* of the same core (4.6.0 > 4.6.0-beta.9). This lets an update UI show
|
|
29
|
+
* "beta.1 → beta.2" and "beta → stable promotion of the same line".
|
|
30
|
+
*
|
|
31
|
+
* Unparseable versions fall back to inequality: different strings count as
|
|
32
|
+
* "newer" so a weird published version still surfaces, identical ones don't.
|
|
33
|
+
*/
|
|
34
|
+
export declare function isNewer(latest: string, current: string, channel?: ReleaseChannel): boolean;
|
|
35
|
+
//# sourceMappingURL=semver.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"semver.d.ts","sourceRoot":"","sources":["../../src/ops/semver.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,gDAAgD;AAChD,MAAM,MAAM,cAAc,GAAG,QAAQ,GAAG,MAAM,CAAC;AAE/C;;;;;GAKG;AACH,wBAAgB,WAAW,CAC1B,CAAC,EAAE,MAAM,GACP;IAAE,IAAI,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC;IAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,GAAG,IAAI,CAOhE;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,OAAO,CACtB,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,MAAM,EACf,OAAO,GAAE,cAAyB,GAChC,OAAO,CAaT"}
|
|
@@ -0,0 +1,55 @@
|
|
|
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
|
+
* Parse `x.y.z` plus an optional `-beta.N` pre-release counter. The
|
|
10
|
+
* pre-release number is returned separately so beta-channel comparisons can
|
|
11
|
+
* order successive betas of the same x.y.z core. Returns null for anything
|
|
12
|
+
* that doesn't lead with a numeric core.
|
|
13
|
+
*/
|
|
14
|
+
export function parseSemver(v) {
|
|
15
|
+
const m = /^(\d+)\.(\d+)\.(\d+)(?:-beta\.(\d+))?/.exec(v.trim());
|
|
16
|
+
if (!m)
|
|
17
|
+
return null;
|
|
18
|
+
return {
|
|
19
|
+
core: [Number(m[1]), Number(m[2]), Number(m[3])],
|
|
20
|
+
beta: m[4] != null ? Number(m[4]) : null
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* True when `latest` is strictly newer than `current` by semver.
|
|
25
|
+
*
|
|
26
|
+
* Stable channel (default): pre-release suffixes are ignored — only stable
|
|
27
|
+
* core bumps surface.
|
|
28
|
+
*
|
|
29
|
+
* Beta channel: pre-releases participate. A higher core wins; on an equal
|
|
30
|
+
* core, a higher `-beta.N` wins, and a stable (no suffix) outranks any -beta
|
|
31
|
+
* of the same core (4.6.0 > 4.6.0-beta.9). This lets an update UI show
|
|
32
|
+
* "beta.1 → beta.2" and "beta → stable promotion of the same line".
|
|
33
|
+
*
|
|
34
|
+
* Unparseable versions fall back to inequality: different strings count as
|
|
35
|
+
* "newer" so a weird published version still surfaces, identical ones don't.
|
|
36
|
+
*/
|
|
37
|
+
export function isNewer(latest, current, channel = 'stable') {
|
|
38
|
+
const a = parseSemver(latest);
|
|
39
|
+
const b = parseSemver(current);
|
|
40
|
+
if (!a || !b)
|
|
41
|
+
return latest !== current;
|
|
42
|
+
for (let i = 0; i < 3; i++) {
|
|
43
|
+
if (a.core[i] > b.core[i])
|
|
44
|
+
return true;
|
|
45
|
+
if (a.core[i] < b.core[i])
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
// Equal core. On the stable channel we don't compare pre-release tails.
|
|
49
|
+
if (channel !== 'beta')
|
|
50
|
+
return false;
|
|
51
|
+
// Stable (beta == null) ranks above any pre-release of the same core.
|
|
52
|
+
const rank = (beta) => (beta == null ? Infinity : beta);
|
|
53
|
+
return rank(a.beta) > rank(b.beta);
|
|
54
|
+
}
|
|
55
|
+
//# sourceMappingURL=semver.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"semver.js","sourceRoot":"","sources":["../../src/ops/semver.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAKH;;;;;GAKG;AACH,MAAM,UAAU,WAAW,CAC1B,CAAS;IAET,MAAM,CAAC,GAAG,uCAAuC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC;IACjE,IAAI,CAAC,CAAC;QAAE,OAAO,IAAI,CAAC;IACpB,OAAO;QACN,IAAI,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QAChD,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI;KACxC,CAAC;AACH,CAAC;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,OAAO,CACtB,MAAc,EACd,OAAe,EACf,UAA0B,QAAQ;IAElC,MAAM,CAAC,GAAG,WAAW,CAAC,MAAM,CAAC,CAAC;IAC9B,MAAM,CAAC,GAAG,WAAW,CAAC,OAAO,CAAC,CAAC;IAC/B,IAAI,CAAC,CAAC,IAAI,CAAC,CAAC;QAAE,OAAO,MAAM,KAAK,OAAO,CAAC;IACxC,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;QAC5B,IAAI,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC;YAAE,OAAO,IAAI,CAAC;QACvC,IAAI,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC;YAAE,OAAO,KAAK,CAAC;IACzC,CAAC;IACD,wEAAwE;IACxE,IAAI,OAAO,KAAK,MAAM;QAAE,OAAO,KAAK,CAAC;IACrC,sEAAsE;IACtE,MAAM,IAAI,GAAG,CAAC,IAAmB,EAAE,EAAE,CAAC,CAAC,IAAI,IAAI,IAAI,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;IACvE,OAAO,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;AACpC,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"token-codec.test.d.ts","sourceRoot":"","sources":["../../../src/tokens/__tests__/token-codec.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { createTokenCodec, MIN_TOKEN_SECRET_LENGTH } from '../token-codec.js';
|
|
3
|
+
const SECRET = 'unit-test-secret-that-is-long-enough-to-pass';
|
|
4
|
+
describe('createTokenCodec', () => {
|
|
5
|
+
it('rejects a secret shorter than the minimum', () => {
|
|
6
|
+
expect(() => createTokenCodec({ prefix: 'share_', secret: 'short' })).toThrow(/at least 32 characters/);
|
|
7
|
+
expect(() => createTokenCodec({ prefix: 'share_', secret: 'x'.repeat(MIN_TOKEN_SECRET_LENGTH - 1) })).toThrow(/got 31/);
|
|
8
|
+
});
|
|
9
|
+
it('accepts a secret at exactly the minimum length', () => {
|
|
10
|
+
expect(() => createTokenCodec({ prefix: 'share_', secret: 'x'.repeat(MIN_TOKEN_SECRET_LENGTH) })).not.toThrow();
|
|
11
|
+
});
|
|
12
|
+
it('mints prefixed, unique, URL-safe tokens', () => {
|
|
13
|
+
const codec = createTokenCodec({ prefix: 'share_', secret: SECRET });
|
|
14
|
+
const a = codec.mintRawToken();
|
|
15
|
+
const b = codec.mintRawToken();
|
|
16
|
+
expect(a).toMatch(/^share_[A-Za-z0-9_-]{43}$/); // 32 bytes → 43 base64url chars
|
|
17
|
+
expect(a).not.toBe(b);
|
|
18
|
+
});
|
|
19
|
+
it('hashes deterministically, never echoing the raw token', () => {
|
|
20
|
+
const codec = createTokenCodec({ prefix: 'share_', secret: SECRET });
|
|
21
|
+
const raw = codec.mintRawToken();
|
|
22
|
+
const hash = codec.hashToken(raw);
|
|
23
|
+
expect(codec.hashToken(raw)).toBe(hash);
|
|
24
|
+
expect(hash).not.toContain(raw);
|
|
25
|
+
expect(hash).toMatch(/^[A-Za-z0-9_-]+$/); // base64url
|
|
26
|
+
});
|
|
27
|
+
it('produces different hashes under different secrets (rotation invalidates)', () => {
|
|
28
|
+
const a = createTokenCodec({ prefix: 'share_', secret: SECRET });
|
|
29
|
+
const b = createTokenCodec({ prefix: 'share_', secret: SECRET + '-rotated' });
|
|
30
|
+
const raw = a.mintRawToken();
|
|
31
|
+
expect(a.hashToken(raw)).not.toBe(b.hashToken(raw));
|
|
32
|
+
});
|
|
33
|
+
it('hashesEqual: equal, unequal, and length-mismatch inputs', () => {
|
|
34
|
+
const codec = createTokenCodec({ prefix: 'invite_', secret: SECRET });
|
|
35
|
+
const hash = codec.hashToken(codec.mintRawToken());
|
|
36
|
+
const other = codec.hashToken(codec.mintRawToken());
|
|
37
|
+
expect(codec.hashesEqual(hash, hash)).toBe(true);
|
|
38
|
+
expect(codec.hashesEqual(hash, other)).toBe(false);
|
|
39
|
+
expect(codec.hashesEqual(hash, hash.slice(0, -1))).toBe(false);
|
|
40
|
+
});
|
|
41
|
+
it('looksLikeToken recognizes only its own prefix', () => {
|
|
42
|
+
const share = createTokenCodec({ prefix: 'share_', secret: SECRET });
|
|
43
|
+
const invite = createTokenCodec({ prefix: 'invite_', secret: SECRET });
|
|
44
|
+
const raw = share.mintRawToken();
|
|
45
|
+
expect(share.looksLikeToken(raw)).toBe(true);
|
|
46
|
+
expect(invite.looksLikeToken(raw)).toBe(false);
|
|
47
|
+
expect(share.looksLikeToken('random-string')).toBe(false);
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
//# sourceMappingURL=token-codec.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"token-codec.test.js","sourceRoot":"","sources":["../../../src/tokens/__tests__/token-codec.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/tokens/index.ts"],"names":[],"mappings":"AAEA,OAAO,EACN,gBAAgB,EAChB,uBAAuB,EACvB,KAAK,UAAU,EACf,KAAK,gBAAgB,EACrB,MAAM,kBAAkB,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/tokens/index.ts"],"names":[],"mappings":"AAAA,wEAAwE;AAExE,OAAO,EACN,gBAAgB,EAChB,uBAAuB,EAGvB,MAAM,kBAAkB,CAAC"}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HMAC token codec — the primitive behind capability-URL tokens (share links,
|
|
3
|
+
* invites, and anything shaped like them).
|
|
4
|
+
*
|
|
5
|
+
* ## Two-part design
|
|
6
|
+
*
|
|
7
|
+
* Each minted token involves TWO secrets:
|
|
8
|
+
*
|
|
9
|
+
* 1. **Per-token secret** — generated in `mintRawToken()` from 32 random
|
|
10
|
+
* bytes, returned to the caller exactly once at mint time. Different
|
|
11
|
+
* every token. This is what end-users carry in `?token=…`.
|
|
12
|
+
*
|
|
13
|
+
* 2. **Instance-wide HMAC secret** (`config.secret`) — used to hash tokens
|
|
14
|
+
* for storage. The store sees only `HMAC-SHA256(secret, token)`. A
|
|
15
|
+
* DB-only leak therefore can't be replayed: the attacker would need the
|
|
16
|
+
* secret too, and it lives in env, not the database.
|
|
17
|
+
*
|
|
18
|
+
* ## Format
|
|
19
|
+
* raw = `<prefix><base64url(32 random bytes)>`
|
|
20
|
+
* hash = base64url( HMAC-SHA256(secret, raw) )
|
|
21
|
+
*
|
|
22
|
+
* The resolution path hashes the supplied token with the same secret and looks
|
|
23
|
+
* up by exact match against the stored hash.
|
|
24
|
+
*
|
|
25
|
+
* Rotating the secret invalidates every token minted under it — give each
|
|
26
|
+
* token family (each prefix) its own secret so they rotate independently.
|
|
27
|
+
*/
|
|
28
|
+
/**
|
|
29
|
+
* Minimum accepted secret length. Matches the "random ≥32-byte string"
|
|
30
|
+
* guidance everywhere these secrets are documented; enforced here so a
|
|
31
|
+
* short dev secret can't reach production silently (presence-only checks
|
|
32
|
+
* let a 4-char secret through).
|
|
33
|
+
*/
|
|
34
|
+
export declare const MIN_TOKEN_SECRET_LENGTH = 32;
|
|
35
|
+
export interface TokenCodecConfig {
|
|
36
|
+
/** Token-family marker prepended to every raw token, e.g. `share_`. */
|
|
37
|
+
prefix: string;
|
|
38
|
+
/** Instance-wide HMAC secret. Must be at least 32 characters. */
|
|
39
|
+
secret: string;
|
|
40
|
+
}
|
|
41
|
+
export interface TokenCodec {
|
|
42
|
+
/** Mint: generate a fresh raw token. Show to the caller exactly once. */
|
|
43
|
+
mintRawToken(): string;
|
|
44
|
+
/** Hash a raw token to its store-side representation. */
|
|
45
|
+
hashToken(raw: string): string;
|
|
46
|
+
/** Constant-time equality for two stored hashes. */
|
|
47
|
+
hashesEqual(a: string, b: string): boolean;
|
|
48
|
+
/** Recognize this codec's token format on inbound requests. */
|
|
49
|
+
looksLikeToken(value: string): boolean;
|
|
50
|
+
/** The configured prefix, for callers that build URLs or error copy. */
|
|
51
|
+
readonly prefix: string;
|
|
52
|
+
}
|
|
53
|
+
export declare function createTokenCodec(config: TokenCodecConfig): TokenCodec;
|
|
54
|
+
//# sourceMappingURL=token-codec.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"token-codec.d.ts","sourceRoot":"","sources":["../../src/tokens/token-codec.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH;;;;;GAKG;AACH,eAAO,MAAM,uBAAuB,KAAK,CAAC;AAE1C,MAAM,WAAW,gBAAgB;IAChC,uEAAuE;IACvE,MAAM,EAAE,MAAM,CAAC;IACf,iEAAiE;IACjE,MAAM,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,UAAU;IAC1B,yEAAyE;IACzE,YAAY,IAAI,MAAM,CAAC;IACvB,yDAAyD;IACzD,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAAC;IAC/B,oDAAoD;IACpD,WAAW,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;IAC3C,+DAA+D;IAC/D,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC;IACvC,wEAAwE;IACxE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACxB;AAED,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,gBAAgB,GAAG,UAAU,CA2BrE"}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { randomBytes, createHmac, timingSafeEqual } from 'node:crypto';
|
|
2
|
+
/**
|
|
3
|
+
* HMAC token codec — the primitive behind capability-URL tokens (share links,
|
|
4
|
+
* invites, and anything shaped like them).
|
|
5
|
+
*
|
|
6
|
+
* ## Two-part design
|
|
7
|
+
*
|
|
8
|
+
* Each minted token involves TWO secrets:
|
|
9
|
+
*
|
|
10
|
+
* 1. **Per-token secret** — generated in `mintRawToken()` from 32 random
|
|
11
|
+
* bytes, returned to the caller exactly once at mint time. Different
|
|
12
|
+
* every token. This is what end-users carry in `?token=…`.
|
|
13
|
+
*
|
|
14
|
+
* 2. **Instance-wide HMAC secret** (`config.secret`) — used to hash tokens
|
|
15
|
+
* for storage. The store sees only `HMAC-SHA256(secret, token)`. A
|
|
16
|
+
* DB-only leak therefore can't be replayed: the attacker would need the
|
|
17
|
+
* secret too, and it lives in env, not the database.
|
|
18
|
+
*
|
|
19
|
+
* ## Format
|
|
20
|
+
* raw = `<prefix><base64url(32 random bytes)>`
|
|
21
|
+
* hash = base64url( HMAC-SHA256(secret, raw) )
|
|
22
|
+
*
|
|
23
|
+
* The resolution path hashes the supplied token with the same secret and looks
|
|
24
|
+
* up by exact match against the stored hash.
|
|
25
|
+
*
|
|
26
|
+
* Rotating the secret invalidates every token minted under it — give each
|
|
27
|
+
* token family (each prefix) its own secret so they rotate independently.
|
|
28
|
+
*/
|
|
29
|
+
/**
|
|
30
|
+
* Minimum accepted secret length. Matches the "random ≥32-byte string"
|
|
31
|
+
* guidance everywhere these secrets are documented; enforced here so a
|
|
32
|
+
* short dev secret can't reach production silently (presence-only checks
|
|
33
|
+
* let a 4-char secret through).
|
|
34
|
+
*/
|
|
35
|
+
export const MIN_TOKEN_SECRET_LENGTH = 32;
|
|
36
|
+
export function createTokenCodec(config) {
|
|
37
|
+
const { prefix, secret } = config;
|
|
38
|
+
if (secret.length < MIN_TOKEN_SECRET_LENGTH) {
|
|
39
|
+
throw new Error(`Secret for "${prefix}" tokens must be at least ${MIN_TOKEN_SECRET_LENGTH} characters ` +
|
|
40
|
+
`(got ${secret.length}). Generate one with: openssl rand -base64 32`);
|
|
41
|
+
}
|
|
42
|
+
return {
|
|
43
|
+
prefix,
|
|
44
|
+
mintRawToken() {
|
|
45
|
+
return prefix + randomBytes(32).toString('base64url');
|
|
46
|
+
},
|
|
47
|
+
hashToken(raw) {
|
|
48
|
+
return createHmac('sha256', secret).update(raw).digest('base64url');
|
|
49
|
+
},
|
|
50
|
+
hashesEqual(a, b) {
|
|
51
|
+
const ab = Buffer.from(a);
|
|
52
|
+
const bb = Buffer.from(b);
|
|
53
|
+
if (ab.length !== bb.length)
|
|
54
|
+
return false;
|
|
55
|
+
return timingSafeEqual(ab, bb);
|
|
56
|
+
},
|
|
57
|
+
looksLikeToken(value) {
|
|
58
|
+
return value.startsWith(prefix);
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
//# sourceMappingURL=token-codec.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"token-codec.js","sourceRoot":"","sources":["../../src/tokens/token-codec.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAEvE;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH;;;;;GAKG;AACH,MAAM,CAAC,MAAM,uBAAuB,GAAG,EAAE,CAAC;AAsB1C,MAAM,UAAU,gBAAgB,CAAC,MAAwB;IACxD,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG,MAAM,CAAC;IAClC,IAAI,MAAM,CAAC,MAAM,GAAG,uBAAuB,EAAE,CAAC;QAC7C,MAAM,IAAI,KAAK,CACd,eAAe,MAAM,6BAA6B,uBAAuB,cAAc;YACtF,QAAQ,MAAM,CAAC,MAAM,+CAA+C,CACrE,CAAC;IACH,CAAC;IAED,OAAO;QACN,MAAM;QACN,YAAY;YACX,OAAO,MAAM,GAAG,WAAW,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC;QACvD,CAAC;QACD,SAAS,CAAC,GAAW;YACpB,OAAO,UAAU,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC;QACrE,CAAC;QACD,WAAW,CAAC,CAAS,EAAE,CAAS;YAC/B,MAAM,EAAE,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;YAC1B,MAAM,EAAE,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;YAC1B,IAAI,EAAE,CAAC,MAAM,KAAK,EAAE,CAAC,MAAM;gBAAE,OAAO,KAAK,CAAC;YAC1C,OAAO,eAAe,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC;QAChC,CAAC;QACD,cAAc,CAAC,KAAa;YAC3B,OAAO,KAAK,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC;QACjC,CAAC;KACD,CAAC;AACH,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@selvajs/server",
|
|
3
|
-
"version": "0.2.0-beta.
|
|
3
|
+
"version": "0.2.0-beta.3",
|
|
4
4
|
"description": "Transport-agnostic compute/solve/definitions server building blocks for apps built on the Selva engine (limits, rate limiting, SSRF guard, input transform, solve pipeline, definition service + render loader)",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"publishConfig": {
|
|
@@ -34,6 +34,31 @@
|
|
|
34
34
|
"source": "./src/providers/index.ts",
|
|
35
35
|
"types": "./dist/providers/index.d.ts",
|
|
36
36
|
"import": "./dist/providers/index.js"
|
|
37
|
+
},
|
|
38
|
+
"./tokens": {
|
|
39
|
+
"source": "./src/tokens/index.ts",
|
|
40
|
+
"types": "./dist/tokens/index.d.ts",
|
|
41
|
+
"import": "./dist/tokens/index.js"
|
|
42
|
+
},
|
|
43
|
+
"./errors": {
|
|
44
|
+
"source": "./src/errors/index.ts",
|
|
45
|
+
"types": "./dist/errors/index.d.ts",
|
|
46
|
+
"import": "./dist/errors/index.js"
|
|
47
|
+
},
|
|
48
|
+
"./http": {
|
|
49
|
+
"source": "./src/http/index.ts",
|
|
50
|
+
"types": "./dist/http/index.d.ts",
|
|
51
|
+
"import": "./dist/http/index.js"
|
|
52
|
+
},
|
|
53
|
+
"./access": {
|
|
54
|
+
"source": "./src/access/index.ts",
|
|
55
|
+
"types": "./dist/access/index.d.ts",
|
|
56
|
+
"import": "./dist/access/index.js"
|
|
57
|
+
},
|
|
58
|
+
"./ops": {
|
|
59
|
+
"source": "./src/ops/index.ts",
|
|
60
|
+
"types": "./dist/ops/index.d.ts",
|
|
61
|
+
"import": "./dist/ops/index.js"
|
|
37
62
|
}
|
|
38
63
|
},
|
|
39
64
|
"files": [
|
|
@@ -41,10 +66,18 @@
|
|
|
41
66
|
"src"
|
|
42
67
|
],
|
|
43
68
|
"dependencies": {
|
|
44
|
-
"@selvajs/compute": "^3.1.0-beta.
|
|
69
|
+
"@selvajs/compute": "^3.1.0-beta.6",
|
|
45
70
|
"@selvajs/platform": "0.14.3-beta.0",
|
|
46
71
|
"@selvajs/schemas": "4.7.0-beta.0"
|
|
47
72
|
},
|
|
73
|
+
"peerDependencies": {
|
|
74
|
+
"@sentry/node": ">=8"
|
|
75
|
+
},
|
|
76
|
+
"peerDependenciesMeta": {
|
|
77
|
+
"@sentry/node": {
|
|
78
|
+
"optional": true
|
|
79
|
+
}
|
|
80
|
+
},
|
|
48
81
|
"devDependencies": {
|
|
49
82
|
"@types/node": "^22.19.17",
|
|
50
83
|
"typescript": "^5.9.3",
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
+
import type {
|
|
3
|
+
Project,
|
|
4
|
+
ProjectAccessInput,
|
|
5
|
+
ProjectVisibility,
|
|
6
|
+
RequestContext
|
|
7
|
+
} from '@selvajs/platform';
|
|
8
|
+
import {
|
|
9
|
+
createProjectAccessInputBuilder,
|
|
10
|
+
type ProjectAccessFlags
|
|
11
|
+
} from '../project-access-input.js';
|
|
12
|
+
|
|
13
|
+
const CTX: RequestContext = {
|
|
14
|
+
userId: 'user-1',
|
|
15
|
+
actingOrgId: 'org-1',
|
|
16
|
+
platformPermissions: ['manage_compute'],
|
|
17
|
+
orgPermissions: ['manage_org_members']
|
|
18
|
+
} as RequestContext;
|
|
19
|
+
|
|
20
|
+
function project(visibility: ProjectVisibility): Project {
|
|
21
|
+
return {
|
|
22
|
+
id: 'proj-1',
|
|
23
|
+
orgId: 'org-1',
|
|
24
|
+
name: 'P',
|
|
25
|
+
slug: 'p',
|
|
26
|
+
visibility,
|
|
27
|
+
ownerId: 'user-9',
|
|
28
|
+
createdBy: 'user-9',
|
|
29
|
+
updatedBy: 'user-9',
|
|
30
|
+
autoJoinOnUpload: false,
|
|
31
|
+
createdAt: '2026-01-01T00:00:00Z',
|
|
32
|
+
updatedAt: '2026-01-01T00:00:00Z'
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Row identity is all the builder consumes — it passes rows through opaquely.
|
|
37
|
+
const MEMBER = {
|
|
38
|
+
projectId: 'proj-1',
|
|
39
|
+
userId: 'user-1',
|
|
40
|
+
role: 'editor'
|
|
41
|
+
} as unknown as NonNullable<ProjectAccessInput['member']>;
|
|
42
|
+
const ORG_MEMBER = {
|
|
43
|
+
orgId: 'org-1',
|
|
44
|
+
userId: 'user-1',
|
|
45
|
+
role: 'member'
|
|
46
|
+
} as unknown as NonNullable<ProjectAccessInput['orgMember']>;
|
|
47
|
+
const GRANT = {
|
|
48
|
+
projectId: 'proj-1',
|
|
49
|
+
orgId: 'org-2',
|
|
50
|
+
canSolve: true
|
|
51
|
+
} as unknown as ProjectAccessInput['platformGrants'][number];
|
|
52
|
+
|
|
53
|
+
function makeDeps(flags: Partial<ProjectAccessFlags> = {}) {
|
|
54
|
+
return {
|
|
55
|
+
getProjectMember: vi.fn().mockResolvedValue(MEMBER),
|
|
56
|
+
getOrgMember: vi.fn().mockResolvedValue(ORG_MEMBER),
|
|
57
|
+
listPlatformGrants: vi.fn().mockResolvedValue([GRANT]),
|
|
58
|
+
flags: vi.fn().mockReturnValue({
|
|
59
|
+
allowCrossOrgPublic: false,
|
|
60
|
+
enablePlatformProjects: false,
|
|
61
|
+
...flags
|
|
62
|
+
})
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
describe('buildProjectAccessInput — per-visibility row fetching', () => {
|
|
67
|
+
it('private: fetches only the project member row', async () => {
|
|
68
|
+
const deps = makeDeps();
|
|
69
|
+
const builder = createProjectAccessInputBuilder(deps);
|
|
70
|
+
const input = await builder.buildProjectAccessInput(CTX, project('private'));
|
|
71
|
+
|
|
72
|
+
expect(input.member).toEqual(MEMBER);
|
|
73
|
+
expect(input.orgMember).toBeNull();
|
|
74
|
+
expect(input.platformGrants).toEqual([]);
|
|
75
|
+
expect(deps.getOrgMember).not.toHaveBeenCalled();
|
|
76
|
+
expect(deps.listPlatformGrants).not.toHaveBeenCalled();
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it.each(['org', 'public'] as const)('%s: fetches project member + org member', async (vis) => {
|
|
80
|
+
const deps = makeDeps();
|
|
81
|
+
const builder = createProjectAccessInputBuilder(deps);
|
|
82
|
+
const input = await builder.buildProjectAccessInput(CTX, project(vis));
|
|
83
|
+
|
|
84
|
+
expect(input.member).toEqual(MEMBER);
|
|
85
|
+
expect(input.orgMember).toEqual(ORG_MEMBER);
|
|
86
|
+
expect(deps.getOrgMember).toHaveBeenCalledWith(CTX, 'org-1', 'user-1');
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('public + ALLOW_CROSS_ORG_PUBLIC: skips the org member row', async () => {
|
|
90
|
+
const deps = makeDeps({ allowCrossOrgPublic: true });
|
|
91
|
+
const builder = createProjectAccessInputBuilder(deps);
|
|
92
|
+
const input = await builder.buildProjectAccessInput(CTX, project('public'));
|
|
93
|
+
|
|
94
|
+
expect(input.member).toEqual(MEMBER);
|
|
95
|
+
expect(input.orgMember).toBeNull();
|
|
96
|
+
expect(deps.getOrgMember).not.toHaveBeenCalled();
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('org visibility still fetches the org row when cross-org public is on', async () => {
|
|
100
|
+
const deps = makeDeps({ allowCrossOrgPublic: true });
|
|
101
|
+
const builder = createProjectAccessInputBuilder(deps);
|
|
102
|
+
const input = await builder.buildProjectAccessInput(CTX, project('org'));
|
|
103
|
+
expect(input.orgMember).toEqual(ORG_MEMBER);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('platform + flag on: fetches grants only', async () => {
|
|
107
|
+
const deps = makeDeps({ enablePlatformProjects: true });
|
|
108
|
+
const builder = createProjectAccessInputBuilder(deps);
|
|
109
|
+
const input = await builder.buildProjectAccessInput(CTX, project('platform'));
|
|
110
|
+
|
|
111
|
+
expect(input.platformGrants).toEqual([GRANT]);
|
|
112
|
+
expect(input.member).toBeNull();
|
|
113
|
+
expect(deps.getProjectMember).not.toHaveBeenCalled();
|
|
114
|
+
expect(deps.getOrgMember).not.toHaveBeenCalled();
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('platform + flag off: skips the grant lookup entirely (feature disabled stays cheap)', async () => {
|
|
118
|
+
const deps = makeDeps({ enablePlatformProjects: false });
|
|
119
|
+
const builder = createProjectAccessInputBuilder(deps);
|
|
120
|
+
const input = await builder.buildProjectAccessInput(CTX, project('platform'));
|
|
121
|
+
|
|
122
|
+
expect(input.platformGrants).toEqual([]);
|
|
123
|
+
expect(deps.listPlatformGrants).not.toHaveBeenCalled();
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('threads ctx fields and flags into the input, with overrides winning', async () => {
|
|
127
|
+
const deps = makeDeps({ allowCrossOrgPublic: true });
|
|
128
|
+
const builder = createProjectAccessInputBuilder(deps);
|
|
129
|
+
const input = await builder.buildProjectAccessInput(CTX, project('private'), {
|
|
130
|
+
member: null
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
expect(input.userId).toBe('user-1');
|
|
134
|
+
expect(input.actingOrgId).toBe('org-1');
|
|
135
|
+
expect(input.orgPermissions).toEqual(CTX.orgPermissions);
|
|
136
|
+
expect(input.platformPermissions).toEqual(CTX.platformPermissions);
|
|
137
|
+
expect(input.allowCrossOrgPublic).toBe(true);
|
|
138
|
+
expect(input.member).toBeNull(); // override beat the fetched row
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('reads flags per call, not at builder creation', async () => {
|
|
142
|
+
const deps = makeDeps();
|
|
143
|
+
const builder = createProjectAccessInputBuilder(deps);
|
|
144
|
+
await builder.buildProjectAccessInput(CTX, project('private'));
|
|
145
|
+
deps.flags.mockReturnValue({ allowCrossOrgPublic: true, enablePlatformProjects: true });
|
|
146
|
+
const input = await builder.buildProjectAccessInput(CTX, project('private'));
|
|
147
|
+
expect(input.enablePlatformProjects).toBe(true);
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
describe('projectAccessInputFromRows', () => {
|
|
152
|
+
it('assembles from caller rows with zero I/O', () => {
|
|
153
|
+
const deps = makeDeps();
|
|
154
|
+
const builder = createProjectAccessInputBuilder(deps);
|
|
155
|
+
const input = builder.projectAccessInputFromRows(CTX, project('org'), {
|
|
156
|
+
member: MEMBER,
|
|
157
|
+
orgMember: ORG_MEMBER
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
expect(input.member).toEqual(MEMBER);
|
|
161
|
+
expect(input.orgMember).toEqual(ORG_MEMBER);
|
|
162
|
+
expect(input.platformGrants).toEqual([]);
|
|
163
|
+
expect(deps.getProjectMember).not.toHaveBeenCalled();
|
|
164
|
+
expect(deps.getOrgMember).not.toHaveBeenCalled();
|
|
165
|
+
expect(deps.listPlatformGrants).not.toHaveBeenCalled();
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it('defaults omitted rows to null/empty', () => {
|
|
169
|
+
const builder = createProjectAccessInputBuilder(makeDeps());
|
|
170
|
+
const input = builder.projectAccessInputFromRows(CTX, project('private'), {});
|
|
171
|
+
expect(input.member).toBeNull();
|
|
172
|
+
expect(input.orgMember).toBeNull();
|
|
173
|
+
expect(input.platformGrants).toEqual([]);
|
|
174
|
+
});
|
|
175
|
+
});
|