@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 +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,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=token-codec.test.d.ts.map
@@ -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
- });
@@ -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
- }