@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.
- package/LICENSE +1 -1
- package/README.md +32 -0
- package/dist/compute/client-cache.d.ts +12 -0
- package/dist/compute/client-cache.d.ts.map +1 -1
- package/dist/compute/client-cache.js +18 -3
- 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 +52 -0
- package/dist/compute/limits.d.ts.map +1 -1
- package/dist/compute/limits.js +33 -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/safe-url.d.ts.map +1 -1
- package/dist/compute/safe-url.js +16 -0
- package/dist/compute/safe-url.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 +42 -0
- package/dist/compute/solve-cache-single-flight.d.ts.map +1 -0
- package/dist/compute/solve-cache-single-flight.js +50 -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 +180 -8
- package/dist/compute/solve-pipeline.js.map +1 -1
- 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/package.json +14 -18
- package/dist/access/__tests__/project-access-input.test.d.ts +0 -2
- package/dist/access/__tests__/project-access-input.test.d.ts.map +0 -1
- package/dist/access/__tests__/project-access-input.test.js +0 -146
- package/dist/access/__tests__/project-access-input.test.js.map +0 -1
- package/dist/compute/__tests__/client-cache.test.d.ts +0 -11
- package/dist/compute/__tests__/client-cache.test.d.ts.map +0 -1
- package/dist/compute/__tests__/client-cache.test.js +0 -200
- package/dist/compute/__tests__/client-cache.test.js.map +0 -1
- package/dist/compute/__tests__/rate-limit.test.d.ts +0 -2
- package/dist/compute/__tests__/rate-limit.test.d.ts.map +0 -1
- package/dist/compute/__tests__/rate-limit.test.js +0 -98
- package/dist/compute/__tests__/rate-limit.test.js.map +0 -1
- package/dist/compute/__tests__/remote-definition.test.d.ts +0 -11
- package/dist/compute/__tests__/remote-definition.test.d.ts.map +0 -1
- package/dist/compute/__tests__/remote-definition.test.js +0 -114
- package/dist/compute/__tests__/remote-definition.test.js.map +0 -1
- package/dist/compute/__tests__/safe-url.test.d.ts +0 -2
- package/dist/compute/__tests__/safe-url.test.d.ts.map +0 -1
- package/dist/compute/__tests__/safe-url.test.js +0 -71
- package/dist/compute/__tests__/safe-url.test.js.map +0 -1
- package/dist/compute/__tests__/solve-pipeline.test.d.ts +0 -11
- package/dist/compute/__tests__/solve-pipeline.test.d.ts.map +0 -1
- package/dist/compute/__tests__/solve-pipeline.test.js +0 -292
- package/dist/compute/__tests__/solve-pipeline.test.js.map +0 -1
- package/dist/compute/__tests__/transform-input.test.d.ts +0 -14
- package/dist/compute/__tests__/transform-input.test.d.ts.map +0 -1
- package/dist/compute/__tests__/transform-input.test.js +0 -120
- package/dist/compute/__tests__/transform-input.test.js.map +0 -1
- package/dist/definitions/__tests__/load-for-render.test.d.ts +0 -8
- package/dist/definitions/__tests__/load-for-render.test.d.ts.map +0 -1
- package/dist/definitions/__tests__/load-for-render.test.js +0 -196
- package/dist/definitions/__tests__/load-for-render.test.js.map +0 -1
- package/dist/definitions/__tests__/schema-extraction.test.d.ts +0 -11
- package/dist/definitions/__tests__/schema-extraction.test.d.ts.map +0 -1
- package/dist/definitions/__tests__/schema-extraction.test.js +0 -115
- package/dist/definitions/__tests__/schema-extraction.test.js.map +0 -1
- package/dist/http/__tests__/body-size.test.d.ts +0 -2
- package/dist/http/__tests__/body-size.test.d.ts.map +0 -1
- package/dist/http/__tests__/body-size.test.js +0 -24
- package/dist/http/__tests__/body-size.test.js.map +0 -1
- package/dist/http/__tests__/route-classifier.test.d.ts +0 -2
- package/dist/http/__tests__/route-classifier.test.d.ts.map +0 -1
- package/dist/http/__tests__/route-classifier.test.js +0 -54
- package/dist/http/__tests__/route-classifier.test.js.map +0 -1
- package/dist/http/__tests__/safe-redirect.test.d.ts +0 -2
- package/dist/http/__tests__/safe-redirect.test.d.ts.map +0 -1
- package/dist/http/__tests__/safe-redirect.test.js +0 -44
- package/dist/http/__tests__/safe-redirect.test.js.map +0 -1
- package/dist/http/__tests__/security-headers.test.d.ts +0 -2
- package/dist/http/__tests__/security-headers.test.d.ts.map +0 -1
- package/dist/http/__tests__/security-headers.test.js +0 -24
- package/dist/http/__tests__/security-headers.test.js.map +0 -1
- package/dist/ops/__tests__/semver.test.d.ts +0 -2
- package/dist/ops/__tests__/semver.test.d.ts.map +0 -1
- package/dist/ops/__tests__/semver.test.js +0 -38
- package/dist/ops/__tests__/semver.test.js.map +0 -1
- package/dist/providers/__tests__/create-selva-providers.test.d.ts +0 -7
- package/dist/providers/__tests__/create-selva-providers.test.d.ts.map +0 -1
- package/dist/providers/__tests__/create-selva-providers.test.js +0 -135
- package/dist/providers/__tests__/create-selva-providers.test.js.map +0 -1
- package/dist/tokens/__tests__/token-codec.test.d.ts +0 -2
- package/dist/tokens/__tests__/token-codec.test.d.ts.map +0 -1
- package/dist/tokens/__tests__/token-codec.test.js +0 -50
- package/dist/tokens/__tests__/token-codec.test.js.map +0 -1
- package/src/access/__tests__/project-access-input.test.ts +0 -175
- package/src/access/index.ts +0 -9
- package/src/access/project-access-input.ts +0 -145
- package/src/compute/__tests__/client-cache.test.ts +0 -224
- package/src/compute/__tests__/rate-limit.test.ts +0 -106
- package/src/compute/__tests__/remote-definition.test.ts +0 -140
- package/src/compute/__tests__/safe-url.test.ts +0 -72
- package/src/compute/__tests__/solve-pipeline.test.ts +0 -341
- package/src/compute/__tests__/transform-input.test.ts +0 -162
- package/src/compute/client-cache.ts +0 -300
- package/src/compute/index.ts +0 -48
- package/src/compute/limits.ts +0 -159
- package/src/compute/rate-limit.ts +0 -110
- package/src/compute/remote-definition.ts +0 -149
- package/src/compute/safe-url.ts +0 -186
- package/src/compute/solve-pipeline.ts +0 -339
- package/src/compute/transform-input.ts +0 -66
- package/src/definitions/__tests__/load-for-render.test.ts +0 -244
- package/src/definitions/__tests__/schema-extraction.test.ts +0 -137
- package/src/definitions/definition-service.ts +0 -243
- package/src/definitions/index.ts +0 -22
- package/src/definitions/load-for-render.ts +0 -272
- package/src/definitions/schema-extraction.ts +0 -117
- package/src/errors/SentryErrorReporter.ts +0 -88
- package/src/errors/index.ts +0 -3
- package/src/http/__tests__/body-size.test.ts +0 -27
- package/src/http/__tests__/route-classifier.test.ts +0 -60
- package/src/http/__tests__/safe-redirect.test.ts +0 -47
- package/src/http/__tests__/security-headers.test.ts +0 -29
- package/src/http/body-size.ts +0 -24
- package/src/http/index.ts +0 -10
- package/src/http/redirect.ts +0 -16
- package/src/http/route-classifier.ts +0 -74
- package/src/http/security-headers.ts +0 -42
- package/src/index.ts +0 -27
- package/src/ops/__tests__/semver.test.ts +0 -42
- package/src/ops/index.ts +0 -3
- package/src/ops/semver.ts +0 -60
- package/src/providers/__tests__/create-selva-providers.test.ts +0 -172
- package/src/providers/__tests__/fixtures/override-config.mjs +0 -8
- package/src/providers/create-selva-providers.ts +0 -241
- package/src/providers/index.ts +0 -12
- package/src/tokens/__tests__/token-codec.test.ts +0 -63
- package/src/tokens/index.ts +0 -8
- package/src/tokens/token-codec.ts +0 -86
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"create-selva-providers.test.d.ts","sourceRoot":"","sources":["../../../src/providers/__tests__/create-selva-providers.test.ts"],"names":[],"mappings":"AAAA;;;;GAIG"}
|
|
@@ -1,135 +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
|
-
import { describe, it, expect, vi } from 'vitest';
|
|
7
|
-
import { fileURLToPath } from 'node:url';
|
|
8
|
-
import { createSelvaProviders } from '../create-selva-providers.js';
|
|
9
|
-
class FakeData {
|
|
10
|
-
}
|
|
11
|
-
class FakeStorage {
|
|
12
|
-
}
|
|
13
|
-
function makeRegistry() {
|
|
14
|
-
const authLocal = vi.fn(() => ({ name: 'local-auth' }));
|
|
15
|
-
const authSupabase = vi.fn(() => ({ name: 'supabase-auth' }));
|
|
16
|
-
const dataLocal = vi.fn(() => new FakeData());
|
|
17
|
-
const dataSupabase = vi.fn(() => new FakeData());
|
|
18
|
-
const storageLocal = vi.fn(() => new FakeStorage());
|
|
19
|
-
const registry = {
|
|
20
|
-
auth: { local: authLocal, supabase: authSupabase },
|
|
21
|
-
data: { local: dataLocal, supabase: dataSupabase },
|
|
22
|
-
storage: { local: storageLocal }
|
|
23
|
-
};
|
|
24
|
-
return { registry, authLocal, authSupabase, dataLocal, dataSupabase, storageLocal };
|
|
25
|
-
}
|
|
26
|
-
const noBoot = { onBoot: () => { } };
|
|
27
|
-
describe('registry selection', () => {
|
|
28
|
-
it('falls back to "local" for every slot when env is empty', async () => {
|
|
29
|
-
const { registry, authLocal, dataLocal, storageLocal } = makeRegistry();
|
|
30
|
-
const runtime = await createSelvaProviders({}, { registry, ...noBoot });
|
|
31
|
-
runtime.resolve();
|
|
32
|
-
expect(authLocal).toHaveBeenCalledTimes(1);
|
|
33
|
-
expect(dataLocal).toHaveBeenCalledTimes(1);
|
|
34
|
-
expect(storageLocal).toHaveBeenCalledTimes(1);
|
|
35
|
-
});
|
|
36
|
-
it('honors SELVA_*_PROVIDER env choices, case-insensitively', async () => {
|
|
37
|
-
const { registry, authSupabase, dataSupabase } = makeRegistry();
|
|
38
|
-
const runtime = await createSelvaProviders({ SELVA_AUTH_PROVIDER: 'Supabase', SELVA_DATA_PROVIDER: 'SUPABASE' }, { registry, ...noBoot });
|
|
39
|
-
runtime.resolve();
|
|
40
|
-
expect(authSupabase).toHaveBeenCalledTimes(1);
|
|
41
|
-
expect(dataSupabase).toHaveBeenCalledTimes(1);
|
|
42
|
-
});
|
|
43
|
-
it('honors the defaults option over the built-in "local"', async () => {
|
|
44
|
-
const { registry, authSupabase } = makeRegistry();
|
|
45
|
-
const runtime = await createSelvaProviders({}, { registry, defaults: { auth: 'supabase' }, ...noBoot });
|
|
46
|
-
runtime.resolve();
|
|
47
|
-
expect(authSupabase).toHaveBeenCalledTimes(1);
|
|
48
|
-
});
|
|
49
|
-
it('rejects an unknown choice, listing the registered names', async () => {
|
|
50
|
-
const { registry } = makeRegistry();
|
|
51
|
-
const runtime = await createSelvaProviders({ SELVA_AUTH_PROVIDER: 'okta' }, { registry, ...noBoot });
|
|
52
|
-
expect(() => runtime.resolve()).toThrow(/SELVA_AUTH_PROVIDER="okta".*local \| supabase/);
|
|
53
|
-
});
|
|
54
|
-
});
|
|
55
|
-
describe('laziness and memoization', () => {
|
|
56
|
-
it('does not instantiate providers until resolve() is called', async () => {
|
|
57
|
-
const { registry, authLocal } = makeRegistry();
|
|
58
|
-
await createSelvaProviders({}, { registry, ...noBoot });
|
|
59
|
-
expect(authLocal).not.toHaveBeenCalled();
|
|
60
|
-
});
|
|
61
|
-
it('instantiates once and logs the boot summary once across repeated resolves', async () => {
|
|
62
|
-
const { registry, authLocal } = makeRegistry();
|
|
63
|
-
const onBoot = vi.fn();
|
|
64
|
-
const runtime = await createSelvaProviders({}, { registry, onBoot });
|
|
65
|
-
runtime.resolve();
|
|
66
|
-
runtime.resolve();
|
|
67
|
-
runtime.flag('ENABLE_SHARING');
|
|
68
|
-
expect(authLocal).toHaveBeenCalledTimes(1);
|
|
69
|
-
expect(onBoot).toHaveBeenCalledTimes(1);
|
|
70
|
-
expect(onBoot.mock.calls[0][0]).toContain('auth=local-auth');
|
|
71
|
-
expect(onBoot.mock.calls[0][0]).toContain('data=FakeData');
|
|
72
|
-
expect(onBoot.mock.calls[0][0]).toContain('tenancy=single');
|
|
73
|
-
});
|
|
74
|
-
});
|
|
75
|
-
describe('config parsing', () => {
|
|
76
|
-
it('parses flags from SELVA_FLAG_* (omitted → false)', async () => {
|
|
77
|
-
const { registry } = makeRegistry();
|
|
78
|
-
const runtime = await createSelvaProviders({ SELVA_FLAG_ENABLE_SHARING: 'true', SELVA_FLAG_ALLOW_ORG_CREATION: 'no' }, { registry, ...noBoot });
|
|
79
|
-
expect(runtime.flag('ENABLE_SHARING')).toBe(true);
|
|
80
|
-
expect(runtime.flag('ALLOW_ORG_CREATION')).toBe(false);
|
|
81
|
-
expect(runtime.flag('ALLOW_CROSS_ORG_PUBLIC')).toBe(false);
|
|
82
|
-
});
|
|
83
|
-
it('parses tenancy and rejects unknown values', async () => {
|
|
84
|
-
const { registry } = makeRegistry();
|
|
85
|
-
const multi = await createSelvaProviders({ SELVA_TENANCY: 'multi' }, { registry, ...noBoot });
|
|
86
|
-
expect(multi.tenancy()).toBe('multi');
|
|
87
|
-
const bad = await createSelvaProviders({ SELVA_TENANCY: 'galactic' }, { registry, ...noBoot });
|
|
88
|
-
expect(() => bad.tenancy()).toThrow(/SELVA_TENANCY="galactic"/);
|
|
89
|
-
});
|
|
90
|
-
it('brands with defaults; copyright falls back to the overridden name', async () => {
|
|
91
|
-
const { registry } = makeRegistry();
|
|
92
|
-
const plain = await createSelvaProviders({}, { registry, ...noBoot });
|
|
93
|
-
expect(plain.branding().name).toBe('Selva');
|
|
94
|
-
expect(plain.branding().copyrightName).toBe('Selva');
|
|
95
|
-
expect(plain.branding().tagline).toBeTruthy();
|
|
96
|
-
const branded = await createSelvaProviders({ SELVA_BRAND_NAME: ' Acme ' }, { registry, ...noBoot });
|
|
97
|
-
expect(branded.branding().name).toBe('Acme');
|
|
98
|
-
expect(branded.branding().copyrightName).toBe('Acme');
|
|
99
|
-
expect(branded.branding().description).toContain('Acme');
|
|
100
|
-
});
|
|
101
|
-
});
|
|
102
|
-
describe('solve metric sink', () => {
|
|
103
|
-
it('falls back to a noop sink when the data provider has no solveMetrics', async () => {
|
|
104
|
-
const { registry } = makeRegistry();
|
|
105
|
-
const runtime = await createSelvaProviders({}, { registry, ...noBoot });
|
|
106
|
-
const sink = runtime.solveMetricSink();
|
|
107
|
-
expect(typeof sink.record).toBe('function');
|
|
108
|
-
expect(runtime.solveMetricSink()).toBe(sink); // memoized
|
|
109
|
-
});
|
|
110
|
-
it('reuses a duck-typed solveMetrics sink carried by the data provider', async () => {
|
|
111
|
-
const { registry } = makeRegistry();
|
|
112
|
-
const carried = { record: vi.fn() };
|
|
113
|
-
registry.data.local = vi.fn(() => ({ solveMetrics: carried }));
|
|
114
|
-
const runtime = await createSelvaProviders({}, { registry, ...noBoot });
|
|
115
|
-
expect(runtime.solveMetricSink()).toBe(carried);
|
|
116
|
-
});
|
|
117
|
-
});
|
|
118
|
-
describe('config-path override', () => {
|
|
119
|
-
it('loads the module default export and hands it the env', async () => {
|
|
120
|
-
const { registry, authLocal } = makeRegistry();
|
|
121
|
-
const configPath = fileURLToPath(new URL('./fixtures/override-config.mjs', import.meta.url));
|
|
122
|
-
const runtime = await createSelvaProviders({ OVERRIDE_MARKER: 'hello' }, { registry, configPath, ...noBoot });
|
|
123
|
-
const config = runtime.resolve();
|
|
124
|
-
expect(config.auth.name).toBe('override-auth');
|
|
125
|
-
expect(config.data.marker).toBe('hello');
|
|
126
|
-
expect(runtime.tenancy()).toBe('multi');
|
|
127
|
-
// The registry is bypassed entirely.
|
|
128
|
-
expect(authLocal).not.toHaveBeenCalled();
|
|
129
|
-
});
|
|
130
|
-
it('rejects at load time when the path does not exist', async () => {
|
|
131
|
-
const { registry } = makeRegistry();
|
|
132
|
-
await expect(createSelvaProviders({}, { registry, configPath: './does-not-exist.js', ...noBoot })).rejects.toThrow(/does-not-exist\.js/);
|
|
133
|
-
});
|
|
134
|
-
});
|
|
135
|
-
//# sourceMappingURL=create-selva-providers.test.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"create-selva-providers.test.js","sourceRoot":"","sources":["../../../src/providers/__tests__/create-selva-providers.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"token-codec.test.d.ts","sourceRoot":"","sources":["../../../src/tokens/__tests__/token-codec.test.ts"],"names":[],"mappings":""}
|
|
@@ -1,50 +0,0 @@
|
|
|
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
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"token-codec.test.js","sourceRoot":"","sources":["../../../src/tokens/__tests__/token-codec.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,175 +0,0 @@
|
|
|
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
|
-
});
|
package/src/access/index.ts
DELETED
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
// Access-rule input assembly — marshals providers + flags into what
|
|
2
|
-
// platform's pure canView/canEdit/… rules consume.
|
|
3
|
-
|
|
4
|
-
export {
|
|
5
|
-
createProjectAccessInputBuilder,
|
|
6
|
-
type ProjectAccessFlags,
|
|
7
|
-
type ProjectAccessInputBuilder,
|
|
8
|
-
type ProjectAccessInputDeps
|
|
9
|
-
} from './project-access-input.js';
|
|
@@ -1,145 +0,0 @@
|
|
|
1
|
-
import type { Project, ProjectAccessInput, RequestContext } from '@selvajs/platform';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Assembly of `ProjectAccessInput` — the marshalling layer between an app's
|
|
5
|
-
* providers and platform's pure access rules (`canView`/`canEdit`/`canSolve`/
|
|
6
|
-
* `canManage`/…). The rules are pure functions over one input shape; the
|
|
7
|
-
* knowledge captured here is **which rows each project visibility needs**, so
|
|
8
|
-
* a consuming app doesn't reimplement (and mis-fetch) it:
|
|
9
|
-
*
|
|
10
|
-
* - `platform` → grants (skipped entirely while the feature flag is off)
|
|
11
|
-
* - `private` → caller's project member row
|
|
12
|
-
* - `org` / `public` → caller's project member row (for canEdit/canManage) and
|
|
13
|
-
* org member row (for canView). Cross-org public skips the org row.
|
|
14
|
-
*
|
|
15
|
-
* Wiring is injected: row lookups as functions (so lazily-initialized
|
|
16
|
-
* providers work unchanged) and flags read per call (so runtime flag flips
|
|
17
|
-
* behave like the app's own `flag()` reads).
|
|
18
|
-
*/
|
|
19
|
-
|
|
20
|
-
export interface ProjectAccessFlags {
|
|
21
|
-
/** `ALLOW_CROSS_ORG_PUBLIC` — public visibility spans orgs when true. */
|
|
22
|
-
allowCrossOrgPublic: boolean;
|
|
23
|
-
/** `ENABLE_PLATFORM_PROJECTS` — platform visibility is live when true. */
|
|
24
|
-
enablePlatformProjects: boolean;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export interface ProjectAccessInputDeps {
|
|
28
|
-
getProjectMember(
|
|
29
|
-
ctx: RequestContext,
|
|
30
|
-
projectId: string,
|
|
31
|
-
userId: string
|
|
32
|
-
): Promise<ProjectAccessInput['member']>;
|
|
33
|
-
getOrgMember(
|
|
34
|
-
ctx: RequestContext,
|
|
35
|
-
orgId: string,
|
|
36
|
-
userId: string
|
|
37
|
-
): Promise<ProjectAccessInput['orgMember']>;
|
|
38
|
-
listPlatformGrants(
|
|
39
|
-
ctx: RequestContext,
|
|
40
|
-
projectId: string
|
|
41
|
-
): Promise<ProjectAccessInput['platformGrants']>;
|
|
42
|
-
/** Read the two access-relevant feature flags. Called once per assembly. */
|
|
43
|
-
flags(): ProjectAccessFlags;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
export interface ProjectAccessInputBuilder {
|
|
47
|
-
/**
|
|
48
|
-
* Build the rule input for any project-scope rule, fetching exactly the
|
|
49
|
-
* rows the rule will consult based on `project.visibility`. Other fields
|
|
50
|
-
* default to safe values; pass `overrides` for the rare callers that
|
|
51
|
-
* already loaded a row (e.g. tests, batched listing pages).
|
|
52
|
-
*/
|
|
53
|
-
buildProjectAccessInput(
|
|
54
|
-
ctx: RequestContext,
|
|
55
|
-
project: Project,
|
|
56
|
-
overrides?: Partial<ProjectAccessInput>
|
|
57
|
-
): Promise<ProjectAccessInput>;
|
|
58
|
-
/**
|
|
59
|
-
* Build a `ProjectAccessInput` from caller-provided rows without any I/O.
|
|
60
|
-
* Used by listing pages that have already batch-loaded membership for many
|
|
61
|
-
* projects; the per-row predicate calls this instead of
|
|
62
|
-
* `buildProjectAccessInput` to avoid an N+1 fetch.
|
|
63
|
-
*/
|
|
64
|
-
projectAccessInputFromRows(
|
|
65
|
-
ctx: RequestContext,
|
|
66
|
-
project: Project,
|
|
67
|
-
rows: {
|
|
68
|
-
member?: ProjectAccessInput['member'];
|
|
69
|
-
orgMember?: ProjectAccessInput['orgMember'];
|
|
70
|
-
platformGrants?: ProjectAccessInput['platformGrants'];
|
|
71
|
-
}
|
|
72
|
-
): ProjectAccessInput;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
export function createProjectAccessInputBuilder(
|
|
76
|
-
deps: ProjectAccessInputDeps
|
|
77
|
-
): ProjectAccessInputBuilder {
|
|
78
|
-
return {
|
|
79
|
-
async buildProjectAccessInput(
|
|
80
|
-
ctx: RequestContext,
|
|
81
|
-
project: Project,
|
|
82
|
-
overrides: Partial<ProjectAccessInput> = {}
|
|
83
|
-
): Promise<ProjectAccessInput> {
|
|
84
|
-
const { allowCrossOrgPublic, enablePlatformProjects } = deps.flags();
|
|
85
|
-
|
|
86
|
-
let member: ProjectAccessInput['member'] = null;
|
|
87
|
-
let orgMember: ProjectAccessInput['orgMember'] = null;
|
|
88
|
-
let platformGrants: ProjectAccessInput['platformGrants'] = [];
|
|
89
|
-
|
|
90
|
-
if (project.visibility === 'platform') {
|
|
91
|
-
// When the flag is off the rule short-circuits before reading grants —
|
|
92
|
-
// skip the lookup to keep "feature disabled" cheap.
|
|
93
|
-
if (enablePlatformProjects) {
|
|
94
|
-
platformGrants = await deps.listPlatformGrants(ctx, project.id);
|
|
95
|
-
}
|
|
96
|
-
} else if (project.visibility === 'private') {
|
|
97
|
-
member = await deps.getProjectMember(ctx, project.id, ctx.userId);
|
|
98
|
-
} else {
|
|
99
|
-
const skipOrgMember = project.visibility === 'public' && allowCrossOrgPublic;
|
|
100
|
-
[member, orgMember] = await Promise.all([
|
|
101
|
-
deps.getProjectMember(ctx, project.id, ctx.userId),
|
|
102
|
-
skipOrgMember ? Promise.resolve(null) : deps.getOrgMember(ctx, project.orgId, ctx.userId)
|
|
103
|
-
]);
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
return {
|
|
107
|
-
orgPermissions: ctx.orgPermissions,
|
|
108
|
-
platformPermissions: ctx.platformPermissions,
|
|
109
|
-
project,
|
|
110
|
-
member,
|
|
111
|
-
orgMember,
|
|
112
|
-
allowCrossOrgPublic,
|
|
113
|
-
enablePlatformProjects,
|
|
114
|
-
platformGrants,
|
|
115
|
-
actingOrgId: ctx.actingOrgId ?? null,
|
|
116
|
-
userId: ctx.userId,
|
|
117
|
-
...overrides
|
|
118
|
-
};
|
|
119
|
-
},
|
|
120
|
-
|
|
121
|
-
projectAccessInputFromRows(
|
|
122
|
-
ctx: RequestContext,
|
|
123
|
-
project: Project,
|
|
124
|
-
rows: {
|
|
125
|
-
member?: ProjectAccessInput['member'];
|
|
126
|
-
orgMember?: ProjectAccessInput['orgMember'];
|
|
127
|
-
platformGrants?: ProjectAccessInput['platformGrants'];
|
|
128
|
-
}
|
|
129
|
-
): ProjectAccessInput {
|
|
130
|
-
const { allowCrossOrgPublic, enablePlatformProjects } = deps.flags();
|
|
131
|
-
return {
|
|
132
|
-
orgPermissions: ctx.orgPermissions,
|
|
133
|
-
platformPermissions: ctx.platformPermissions,
|
|
134
|
-
project,
|
|
135
|
-
member: rows.member ?? null,
|
|
136
|
-
orgMember: rows.orgMember ?? null,
|
|
137
|
-
allowCrossOrgPublic,
|
|
138
|
-
enablePlatformProjects,
|
|
139
|
-
platformGrants: rows.platformGrants ?? [],
|
|
140
|
-
actingOrgId: ctx.actingOrgId ?? null,
|
|
141
|
-
userId: ctx.userId
|
|
142
|
-
};
|
|
143
|
-
}
|
|
144
|
-
};
|
|
145
|
-
}
|