@selvajs/platform 0.14.3-beta.0 → 0.15.0-beta.2

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 (154) hide show
  1. package/LICENSE +1 -1
  2. package/dist/computeServer/index.d.ts +1 -1
  3. package/dist/computeServer/index.d.ts.map +1 -1
  4. package/dist/computeServer/interface.d.ts +29 -1
  5. package/dist/computeServer/interface.d.ts.map +1 -1
  6. package/dist/computeServer/types.d.ts +12 -1
  7. package/dist/computeServer/types.d.ts.map +1 -1
  8. package/dist/computeServer/types.js.map +1 -1
  9. package/dist/data/index.d.ts +2 -1
  10. package/dist/data/index.d.ts.map +1 -1
  11. package/dist/data/index.js +1 -1
  12. package/dist/data/index.js.map +1 -1
  13. package/dist/data/interface.d.ts +69 -8
  14. package/dist/data/interface.d.ts.map +1 -1
  15. package/dist/data/interface.js +7 -1
  16. package/dist/data/interface.js.map +1 -1
  17. package/dist/definitions/interface.d.ts +16 -1
  18. package/dist/definitions/interface.d.ts.map +1 -1
  19. package/dist/definitions/schemas.d.ts +1 -0
  20. package/dist/definitions/schemas.d.ts.map +1 -1
  21. package/dist/definitions/schemas.js +8 -0
  22. package/dist/definitions/schemas.js.map +1 -1
  23. package/dist/definitions/types.d.ts +24 -1
  24. package/dist/definitions/types.d.ts.map +1 -1
  25. package/dist/events/index.d.ts +1 -1
  26. package/dist/events/index.d.ts.map +1 -1
  27. package/dist/events/index.js +1 -1
  28. package/dist/events/index.js.map +1 -1
  29. package/dist/events/interface.d.ts +30 -0
  30. package/dist/events/interface.d.ts.map +1 -1
  31. package/dist/events/interface.js +7 -0
  32. package/dist/events/interface.js.map +1 -1
  33. package/dist/index.d.ts +9 -4
  34. package/dist/index.d.ts.map +1 -1
  35. package/dist/index.js +5 -2
  36. package/dist/index.js.map +1 -1
  37. package/dist/logging/interface.d.ts +82 -0
  38. package/dist/logging/interface.d.ts.map +1 -0
  39. package/dist/logging/interface.js +33 -0
  40. package/dist/logging/interface.js.map +1 -0
  41. package/dist/metrics/interface.d.ts +6 -0
  42. package/dist/metrics/interface.d.ts.map +1 -1
  43. package/dist/metrics/interface.js.map +1 -1
  44. package/dist/organizations/__tests__/slug-schema.test.d.ts +2 -0
  45. package/dist/organizations/__tests__/slug-schema.test.d.ts.map +1 -0
  46. package/dist/organizations/__tests__/slug-schema.test.js +38 -0
  47. package/dist/organizations/__tests__/slug-schema.test.js.map +1 -0
  48. package/dist/organizations/index.d.ts +1 -1
  49. package/dist/organizations/index.d.ts.map +1 -1
  50. package/dist/organizations/index.js +1 -1
  51. package/dist/organizations/index.js.map +1 -1
  52. package/dist/organizations/schemas.d.ts +16 -2
  53. package/dist/organizations/schemas.d.ts.map +1 -1
  54. package/dist/organizations/schemas.js +32 -2
  55. package/dist/organizations/schemas.js.map +1 -1
  56. package/dist/solveCache/interface.d.ts +86 -0
  57. package/dist/solveCache/interface.d.ts.map +1 -0
  58. package/dist/solveCache/interface.js +12 -0
  59. package/dist/solveCache/interface.js.map +1 -0
  60. package/dist/testing/suites/definitionStoreSuite.d.ts.map +1 -1
  61. package/dist/testing/suites/definitionStoreSuite.js +57 -0
  62. package/dist/testing/suites/definitionStoreSuite.js.map +1 -1
  63. package/dist/testing/suites/eventSinkSuite.js +1 -0
  64. package/dist/testing/suites/eventSinkSuite.js.map +1 -1
  65. package/dist/testing/suites/platformPermissionStoreSuite.d.ts +9 -0
  66. package/dist/testing/suites/platformPermissionStoreSuite.d.ts.map +1 -1
  67. package/dist/testing/suites/platformPermissionStoreSuite.js +34 -1
  68. package/dist/testing/suites/platformPermissionStoreSuite.js.map +1 -1
  69. package/dist/testing/suites/solveMetricSinkSuite.d.ts +5 -0
  70. package/dist/testing/suites/solveMetricSinkSuite.d.ts.map +1 -1
  71. package/dist/testing/suites/solveMetricSinkSuite.js +17 -8
  72. package/dist/testing/suites/solveMetricSinkSuite.js.map +1 -1
  73. package/dist/testing/suites/storageProviderSuite.d.ts.map +1 -1
  74. package/dist/testing/suites/storageProviderSuite.js +23 -2
  75. package/dist/testing/suites/storageProviderSuite.js.map +1 -1
  76. package/package.json +9 -24
  77. package/src/access/index.ts +0 -23
  78. package/src/access/rules.ts +0 -276
  79. package/src/auth/index.ts +0 -8
  80. package/src/auth/interface.ts +0 -279
  81. package/src/auth/types.ts +0 -32
  82. package/src/bindings/index.ts +0 -2
  83. package/src/bindings/interface.ts +0 -61
  84. package/src/computeServer/index.ts +0 -28
  85. package/src/computeServer/interface.ts +0 -73
  86. package/src/computeServer/secrets.ts +0 -102
  87. package/src/computeServer/types.ts +0 -74
  88. package/src/computeServer/utils.ts +0 -112
  89. package/src/config.ts +0 -99
  90. package/src/context.ts +0 -80
  91. package/src/data/index.ts +0 -1
  92. package/src/data/interface.ts +0 -70
  93. package/src/definitions/index.ts +0 -26
  94. package/src/definitions/interface.ts +0 -87
  95. package/src/definitions/paths.ts +0 -31
  96. package/src/definitions/schemas.ts +0 -64
  97. package/src/definitions/types.ts +0 -111
  98. package/src/errors/interface.ts +0 -45
  99. package/src/errors.ts +0 -16
  100. package/src/events/audit.ts +0 -67
  101. package/src/events/index.ts +0 -9
  102. package/src/events/interface.ts +0 -102
  103. package/src/index.ts +0 -247
  104. package/src/invites/index.ts +0 -2
  105. package/src/invites/interface.ts +0 -38
  106. package/src/invites/types.ts +0 -39
  107. package/src/metrics/interface.ts +0 -86
  108. package/src/organizations/index.ts +0 -23
  109. package/src/organizations/interface.ts +0 -68
  110. package/src/organizations/paths.ts +0 -45
  111. package/src/organizations/schemas.ts +0 -61
  112. package/src/organizations/types.ts +0 -41
  113. package/src/pagination.ts +0 -45
  114. package/src/permissions/index.ts +0 -3
  115. package/src/permissions/interface.ts +0 -60
  116. package/src/permissions/types.ts +0 -21
  117. package/src/platformProjects/index.ts +0 -3
  118. package/src/platformProjects/interface.ts +0 -27
  119. package/src/platformProjects/schemas.ts +0 -13
  120. package/src/platformProjects/types.ts +0 -22
  121. package/src/projects/index.ts +0 -4
  122. package/src/projects/interface.ts +0 -58
  123. package/src/projects/schemas.ts +0 -28
  124. package/src/projects/types.ts +0 -36
  125. package/src/shareLinks/index.ts +0 -5
  126. package/src/shareLinks/interface.ts +0 -35
  127. package/src/shareLinks/schemas.ts +0 -21
  128. package/src/shareLinks/types.ts +0 -34
  129. package/src/storage/assetClasses.ts +0 -134
  130. package/src/storage/cacheBust.ts +0 -26
  131. package/src/storage/image.ts +0 -82
  132. package/src/storage/index.ts +0 -12
  133. package/src/storage/interface.ts +0 -27
  134. package/src/testing/index.ts +0 -47
  135. package/src/testing/suites/authProviderSuite.ts +0 -207
  136. package/src/testing/suites/computeServerStoreSuite.ts +0 -195
  137. package/src/testing/suites/definitionStoreSuite.ts +0 -630
  138. package/src/testing/suites/emailLinkAuthSuite.ts +0 -83
  139. package/src/testing/suites/eventSinkSuite.ts +0 -379
  140. package/src/testing/suites/helpers.ts +0 -87
  141. package/src/testing/suites/inviteStoreSuite.ts +0 -143
  142. package/src/testing/suites/orgStoreSuite.ts +0 -429
  143. package/src/testing/suites/platformPermissionStoreSuite.ts +0 -196
  144. package/src/testing/suites/platformProjectGrantStoreSuite.ts +0 -166
  145. package/src/testing/suites/projectStoreSuite.ts +0 -381
  146. package/src/testing/suites/shareLinkStoreSuite.ts +0 -203
  147. package/src/testing/suites/solveMetricSinkSuite.ts +0 -135
  148. package/src/testing/suites/storageProviderSuite.ts +0 -196
  149. package/src/testing/suites/userProfileStoreSuite.ts +0 -134
  150. package/src/userProfile/index.ts +0 -3
  151. package/src/userProfile/interface.ts +0 -48
  152. package/src/userProfile/types.ts +0 -24
  153. package/src/utils/audit.ts +0 -46
  154. package/src/utils/index.ts +0 -2
@@ -1,279 +0,0 @@
1
- import type { AuthUser, LoginResult, UserManagementResult } from './types.js';
2
- import type { ListOptions, Page } from '../pagination.js';
3
-
4
- /**
5
- * Optional password-based authentication surface. Implemented by providers
6
- * that own credentials (Local, Supabase Auth). OIDC-only providers leave
7
- * `IAuthProvider.passwordAuth` undefined.
8
- */
9
- export interface IPasswordAuth {
10
- /**
11
- * Verify credentials. Discriminated union — callers MUST `switch (result.kind)`,
12
- * never treat truthiness as authenticated.
13
- */
14
- verifyLogin(email: string, password: string): Promise<LoginResult>;
15
-
16
- /**
17
- * Admin-initiated user creation. Pure identity — callers grant platform
18
- * permissions separately via `IPlatformPermissionStore.set`.
19
- */
20
- createUserWithPassword(email: string, password: string): Promise<AuthUser>;
21
-
22
- /** Self-service registration. Return null when self-registration is disabled. */
23
- registerUser?(email: string, password: string): Promise<AuthUser | null>;
24
- }
25
-
26
- /**
27
- * Optional OAuth surface. Implemented by providers that broker OAuth flows
28
- * (Supabase Auth, future Eterna/Auth0/etc.). Local credential-only providers
29
- * leave `IAuthProvider.oauth` undefined.
30
- *
31
- * Lifecycle is the standard authorization-code flow:
32
- *
33
- * 1. Browser hits `/auth/{provider}/start?provider=google` → server calls
34
- * `getOAuthAuthorizationUrl` and 303s to the result.
35
- * 2. IdP redirects back to `/auth/{provider}/callback?code=...` →
36
- * server calls `exchangeOAuthCode` to mint the session pair.
37
- * 3. When the access token expires, the session-refresh middleware in
38
- * `hooks.server.ts` calls `refreshSession` with the stored refresh
39
- * token to swap for a fresh pair without bouncing the user.
40
- *
41
- * Each method returns `null` (rather than throwing) on irrecoverable
42
- * credential failures — the route layer translates `null` into the right
43
- * HTTP shape (401 for refresh, login redirect for callback).
44
- */
45
- export interface IOAuthAuth {
46
- /**
47
- * The OAuth providers this adapter is configured to broker. Returned as
48
- * lowercased identifiers ("google", "github", …) — the adapter reads its
49
- * own configuration source (env var, dashboard, hard-coded list) and
50
- * decides what's available. Empty array when none are wired.
51
- *
52
- * Driving adapters (HTTP routes, UI) call this instead of reading
53
- * provider-specific env vars directly — keeps the provider name out of
54
- * `selva`.
55
- */
56
- listProviders(): readonly string[];
57
-
58
- /**
59
- * Build the IdP authorization URL the browser should be redirected to.
60
- * `redirectTo` is the full callback URL — must be the route that handles
61
- * the `?code=...` exchange. Provider-name validation is the caller's job;
62
- * passing an unconfigured provider may surface as an upstream error.
63
- */
64
- getOAuthAuthorizationUrl(
65
- provider: 'google' | 'github' | 'azure' | 'gitlab',
66
- redirectTo: string
67
- ): Promise<string>;
68
-
69
- /**
70
- * Exchange an authorization code for a session. Returns the resolved
71
- * identity and an access/refresh token pair. `null` when the code is
72
- * invalid, expired, or already redeemed — callers MUST treat null as
73
- * "send the user back to /login".
74
- */
75
- exchangeOAuthCode(code: string): Promise<{
76
- user: AuthUser;
77
- sessionToken: string;
78
- refreshToken: string;
79
- } | null>;
80
-
81
- /**
82
- * Swap a refresh token for a fresh access/refresh pair. Returns null on
83
- * irrecoverable failure (revoked, expired, signed by a different secret);
84
- * the session-refresh middleware treats null as "force re-login" by
85
- * clearing the refresh cookie.
86
- */
87
- refreshSession(refreshToken: string): Promise<{
88
- sessionToken: string;
89
- refreshToken: string;
90
- } | null>;
91
- }
92
-
93
- /**
94
- * Optional passwordless email surface — "type your email, click the link in
95
- * your inbox, you're in." Implemented by providers that broker email delivery
96
- * + token verification (Supabase Auth, future Eterna ID, …). Adapters that
97
- * can't send mail or don't model this flow leave `IAuthProvider.emailLink`
98
- * undefined.
99
- *
100
- * Lifecycle is two phases mirroring OAuth's start/callback split:
101
- *
102
- * 1. User submits an email → server calls `sendMagicLink(email,
103
- * callbackUrl)`. The adapter delivers a link to that callback URL with
104
- * whatever verification token shape it uses (Supabase: `?token_hash=…
105
- * &type=magiclink`).
106
- * 2. User clicks the link, browser hits the callback route → server calls
107
- * `verifyMagicLink(rawTokenFromUrl)` to mint a session.
108
- *
109
- * The driving adapter never inspects the token itself — it forwards the raw
110
- * URL string. Adapter-specific token shapes stay inside the adapter.
111
- */
112
- export interface IEmailLinkAuth {
113
- /**
114
- * Send a magic-link email. `callbackUrl` is the absolute URL the
115
- * adapter should embed in the link — must point at the route that
116
- * handles `verifyMagicLink`.
117
- *
118
- * Returns `{ ok: true }` on success. On adapter-classified failures
119
- * (rate limit, signup disabled, invalid email) returns
120
- * `{ ok: false, reason }` so the route can render a user-friendly
121
- * message without exposing provider internals. Network/unknown errors
122
- * still throw.
123
- */
124
- sendMagicLink(
125
- email: string,
126
- callbackUrl: string
127
- ): Promise<
128
- { ok: true } | { ok: false; reason: 'rate_limited' | 'signup_disabled' | 'invalid_email' }
129
- >;
130
-
131
- /**
132
- * Verify the token the IdP sent the user. `rawCallbackUrl` is the full
133
- * URL the user landed on (or an opaque adapter-specific token string —
134
- * adapters document which); the adapter pulls whatever fields it needs.
135
- *
136
- * Returns `null` for invalid/expired/already-redeemed tokens — callers
137
- * MUST treat null as "send the user back to /login". `refreshToken` is
138
- * present only for adapters that issue refresh tokens for email-link
139
- * sessions (Supabase does); omit otherwise.
140
- */
141
- verifyMagicLink(rawCallbackUrl: string): Promise<{
142
- user: AuthUser;
143
- sessionToken: string;
144
- refreshToken?: string;
145
- } | null>;
146
- }
147
-
148
- /**
149
- * Optional forward-proxy authentication surface. Implemented by providers
150
- * that derive identity from signals injected by a trusted upstream reverse
151
- * proxy (HTTP headers, mTLS, etc.) — e.g. Caddy `forward_auth`,
152
- * oauth2-proxy, Authelia, Pomerium, Traefik forward-auth.
153
- *
154
- * ⚠ TRUST BOUNDARY — READ BEFORE IMPLEMENTING ⚠
155
- *
156
- * The signals this surface consumes are NOT verified cryptographically by
157
- * Selva. They are trusted because the deployment runs the app behind a
158
- * proxy that:
159
- *
160
- * 1. Authenticates the user against an upstream IdP, AND
161
- * 2. STRIPS any client-supplied copies of the trusted headers from
162
- * inbound requests before adding its own, AND
163
- * 3. Is the ONLY network path that can reach the app process (bind to
164
- * 127.0.0.1, firewall the app port, or a private socket).
165
- *
166
- * If any condition fails, an attacker can spoof the headers and become
167
- * anyone. **Each implementer of this interface MUST ship a README that
168
- * states, loudly, the exact proxy configuration required.** The platform
169
- * layer cannot verify this — it is the provider's contract with its
170
- * operator.
171
- *
172
- * Lifecycle: `identifyFromHeaders` is called from `hooks.server.ts` on
173
- * every authed request whose session cookie is missing or invalid. It MUST
174
- * be cheap — a header read plus at most one indexed lookup. No network
175
- * calls, no synchronous disk scans of unbounded size.
176
- */
177
- export interface IProxyAuth {
178
- /**
179
- * Identify the caller from trusted upstream-proxy signals on the
180
- * incoming request. Return `null` for "not identified" (header missing,
181
- * UPN not allowlisted, account disabled). MUST NOT throw for absent
182
- * headers — those are the normal anonymous case.
183
- *
184
- * On first identification of a previously-allowlisted user, the
185
- * implementation MAY materialize stored fields (display name, email)
186
- * from the same headers. Auto-creating arbitrary users from headers
187
- * is forbidden: the allowlist IS the security boundary.
188
- */
189
- identifyFromHeaders(headers: Headers): Promise<AuthUser | null>;
190
-
191
- /**
192
- * Diagnostic helper. Returns true iff none of the identity headers this
193
- * provider trusts arrived on the request. The hook layer uses this on
194
- * `/login` misses to distinguish "proxy never reached us" from "proxy is
195
- * here but didn't recognise the user" so operators can tell silent
196
- * forward-auth misconfigurations apart from real allowlist gaps without
197
- * leaking the trusted header names to end users.
198
- */
199
- hasNoIdentityHeaders(headers: Headers): boolean;
200
-
201
- /**
202
- * Diagnostic helper. Returns the identity-header names this provider is
203
- * configured to read. Used purely for log output — never surfaced to end
204
- * users.
205
- */
206
- readonly configuredHeaderNames: readonly string[];
207
- }
208
-
209
- /**
210
- * Authentication provider — identity verification only. Profile state lives
211
- * in `IUserProfileStore`, platform permissions in `IPlatformPermissionStore`,
212
- * password ops in optional `passwordAuth`, OAuth in optional `oauth`.
213
- *
214
- * Methods here do NOT take a `RequestContext` — the provider produces the
215
- * identity used to *build* the context.
216
- */
217
- export interface IAuthProvider {
218
- /** Display name shown in admin UI (e.g. "Local", "Microsoft Entra ID"). */
219
- readonly name: string;
220
-
221
- /** Present for credential-owning providers; undefined for OIDC-only. */
222
- readonly passwordAuth?: IPasswordAuth;
223
-
224
- /** Present for providers that broker OAuth flows; undefined otherwise. */
225
- readonly oauth?: IOAuthAuth;
226
-
227
- /**
228
- * Present for providers that send sign-in links over email (Supabase Auth,
229
- * future Eterna ID). Undefined otherwise.
230
- */
231
- readonly emailLink?: IEmailLinkAuth;
232
-
233
- /**
234
- * Present for providers that derive identity from a trusted upstream
235
- * proxy (forward-auth headers, mTLS, etc.). Undefined for credential
236
- * or OIDC-broker providers. See `IProxyAuth` for the trust contract.
237
- *
238
- * When set, the platform treats sign-out as the proxy/IdP's job and
239
- * does not render a logout button — Selva has no session to destroy.
240
- */
241
- readonly proxyAuth?: IProxyAuth;
242
-
243
- /**
244
- * Verify a token (session cookie, JWT, ID token, etc.). Returns the user
245
- * or null when invalid/expired.
246
- */
247
- verifyToken(token: string): Promise<AuthUser | null>;
248
-
249
- getUser(id: string): Promise<AuthUser | null>;
250
-
251
- /** Returns null if the provider does not support user management. */
252
- listUsers(opts?: ListOptions): Promise<Page<AuthUser> | null>;
253
-
254
- /**
255
- * Optional: allowlist a user without a password (OAuth providers). Pure
256
- * identity — callers grant permissions via `IPlatformPermissionStore.set`.
257
- */
258
- createUser?(email: string): Promise<AuthUser>;
259
-
260
- /**
261
- * Delete a user identity. The sole-`instance_admin` invariant is NOT
262
- * enforced here — callers MUST consult
263
- * `IPlatformPermissionStore.countInstanceAdminsExcluding(id)` first and
264
- * surface `'last_admin'` themselves.
265
- */
266
- deleteUser(id: string): Promise<UserManagementResult>;
267
-
268
- /**
269
- * Disable a user. Sessions become invalid; identity is preserved (preferred
270
- * over deletion). Same invariant rules as `deleteUser`.
271
- */
272
- disableUser(id: string): Promise<UserManagementResult>;
273
-
274
- /**
275
- * Stamp `lastLoginAt`. Best-effort — failure MUST NOT block auth.
276
- * Adapters MAY debounce (e.g. skip if existing timestamp < 60s old).
277
- */
278
- touchLastLogin?(id: string): Promise<void>;
279
- }
package/src/auth/types.ts DELETED
@@ -1,32 +0,0 @@
1
- /**
2
- * Identity record from an auth provider. Pure identity — no Selva-specific
3
- * authorization or profile state. Platform permissions live in
4
- * `IPlatformPermissionStore`; profile state lives in `IUserProfileStore`.
5
- */
6
- export interface AuthUser {
7
- id: string;
8
- email?: string;
9
- /** Provider-specific only — never PII, credentials, or tokens. */
10
- metadata?: Record<string, unknown>;
11
- createdAt?: string;
12
- /** Provider-updated only; never client-writable. */
13
- lastLoginAt?: string;
14
- /** When true, `verifyToken` and `verifyLogin` MUST fail. Prefer over deletion. */
15
- disabled?: boolean;
16
- }
17
-
18
- /**
19
- * - `ok` — succeeded
20
- * - `not_found` — target user doesn't exist
21
- * - `not_supported` — provider doesn't implement user management
22
- * - `last_admin` — refused; would leave zero `instance_admin` users
23
- */
24
- export type UserManagementResult = 'ok' | 'not_found' | 'not_supported' | 'last_admin';
25
-
26
- /**
27
- * - `success` — credentials valid; includes session token the provider minted.
28
- * - `failed` — credentials invalid, user disabled, or rate-limited.
29
- */
30
- export type LoginResult =
31
- | { kind: 'success'; user: AuthUser; sessionToken: string }
32
- | { kind: 'failed'; reason?: 'invalid_credentials' | 'disabled' | 'rate_limited' };
@@ -1,2 +0,0 @@
1
- export type { IBindingResolver } from './interface.js';
2
- export { NoopBindingResolver } from './interface.js';
@@ -1,61 +0,0 @@
1
- /**
2
- * Server-side resolver for inputs marked as `source: { kind: 'server', key }`
3
- * in the schema. The host (selva app or a fork) supplies an implementation;
4
- * the platform stays domain-agnostic.
5
- *
6
- * Keys are opaque strings. The host defines the namespace
7
- * (e.g. 'capture.geometry', 'parcel.boundary') and the resolver interprets it
8
- * however it likes — typically as an address into the host's own domain store.
9
- *
10
- * Status: preliminary. The interface is expected to refine once we wire a
11
- * real binding end-to-end (see docs/upstream-binding-prep PR plan). Current
12
- * shape is the minimum needed to slot a host implementation behind
13
- * `SelvaConfig.bindingResolver`.
14
- */
15
-
16
- import type { RequestContext } from '../context.js';
17
-
18
- export interface IBindingResolver {
19
- /**
20
- * Resolve a batch of bound keys in one call.
21
- *
22
- * - `keys` is what the schema author wrote (`source.key`) — opaque to the
23
- * platform, interpreted by the resolver. The author writes keys at
24
- * design time, so the key should describe WHAT attribute to read, not
25
- * WHICH entity to read it from.
26
- * - `scope` is set by the calling route at solve time — opaque to the
27
- * platform, supplies the "which entity" anchor that varies per
28
- * request (capture id, parcel id, custom struct). Optional because
29
- * not every host needs it.
30
- *
31
- * Implementations should return ONLY the keys they successfully
32
- * resolved — absent entries in the returned map signal "missing" to the
33
- * caller, who then decides what to do (e.g. error the solve, or fall back
34
- * to the input's `default`).
35
- *
36
- * Do not throw for individual missing keys. Throw only for resolver-wide
37
- * failures (DB unreachable, configuration error). Per-key "not found" is
38
- * normal and is signalled by omitting the entry.
39
- *
40
- * Batching is intentional: a single solve often binds multiple inputs
41
- * from one domain object, and the resolver should be able to do that
42
- * with a single DB round-trip.
43
- */
44
- resolve(
45
- ctx: RequestContext,
46
- keys: readonly string[],
47
- scope?: unknown
48
- ): Promise<Map<string, unknown>>;
49
- }
50
-
51
- /**
52
- * Default resolver used when the host hasn't configured one. Returns an
53
- * empty map for any request, which makes every bound input fail at solve
54
- * time — a loud signal that a resolver needs to be wired up. Drop-in replace
55
- * via `SelvaConfig.bindingResolver` once the host has a real implementation.
56
- */
57
- export class NoopBindingResolver implements IBindingResolver {
58
- async resolve(): Promise<Map<string, unknown>> {
59
- return new Map();
60
- }
61
- }
@@ -1,28 +0,0 @@
1
- export type {
2
- ComputeServerConfig,
3
- ComputeConfig,
4
- PlatformComputeServer,
5
- OrgComputeServer
6
- } from './types.js';
7
- export { isPlatformServer, isOrgServer } from './types.js';
8
- export type { IComputeServerStore } from './interface.js';
9
- // At-rest secret crypto (uses `node:crypto`). Server-only — deliberately NOT
10
- // re-exported from the root barrel, which client `.svelte` code imports and
11
- // where a browser bundle can't resolve `node:crypto`. Import these from
12
- // `@selvajs/platform/computeServer` in server code. The report TYPES are erased
13
- // at build, so they also stay re-exported from the root barrel for convenience.
14
- export { isEncryptedSecret, encryptSecret, decryptSecret, decodeSecretKey } from './secrets.js';
15
- export type {
16
- SecretVerificationReport,
17
- SecretVerificationFailure,
18
- SecretVerificationFailureReason
19
- } from './secrets.js';
20
- export type { ResolveOptions } from './utils.js';
21
- export {
22
- serversVisibleTo,
23
- defaultServerIdFor,
24
- resolveServerForOrg,
25
- findServerById,
26
- platformServers,
27
- orgServersFor
28
- } from './utils.js';
@@ -1,73 +0,0 @@
1
- import type { RequestContext } from '../context.js';
2
- import type { ComputeConfig, ComputeServerConfig } from './types.js';
3
- import type { SecretVerificationReport } from './secrets.js';
4
-
5
- /**
6
- * Compute-server configuration. Architecture spec §4.8.
7
- *
8
- * One config doc holds *all* servers (platform + org-private) plus the
9
- * global `defaultServerId` and per-org `orgDefaults` map. Authorization is
10
- * the route layer's job — `manage_compute` for platform-server mutations,
11
- * `manage_org_compute` (gated by the `ALLOW_ORG_COMPUTE_OVERRIDE` flag) for
12
- * org-private mutations and `orgDefaults[orgId]`.
13
- */
14
- export interface IComputeServerStore {
15
- /**
16
- * Read the full config. Callers (resolver, page loaders, route handlers)
17
- * apply the visibility predicate from `serversVisibleTo` themselves —
18
- * the store does not pre-filter, because the same row set is needed at
19
- * different scopes (admin manage vs. org manage vs. solve).
20
- */
21
- getConfig(ctx: RequestContext): Promise<ComputeConfig>;
22
-
23
- /**
24
- * Replace the entire platform-server set + global `defaultServerId`.
25
- * Org-private rows and `orgDefaults` are left untouched.
26
- */
27
- savePlatformServers(
28
- ctx: RequestContext,
29
- servers: ComputeServerConfig[],
30
- defaultServerId: string | undefined
31
- ): Promise<void>;
32
-
33
- /**
34
- * Replace the org-private server set for `orgId` and (optionally) update
35
- * `orgDefaults[orgId]`. Platform rows are left untouched.
36
- *
37
- * Pass `defaultServerId: null` to clear the org's override; `undefined`
38
- * leaves the existing value alone.
39
- */
40
- saveOrgServers(
41
- ctx: RequestContext,
42
- orgId: string,
43
- servers: ComputeServerConfig[],
44
- defaultServerId?: string | null
45
- ): Promise<void>;
46
-
47
- /**
48
- * Set or clear `orgDefaults[orgId]`. The route layer is responsible for
49
- * verifying the chosen server is visible to the org before calling this.
50
- *
51
- * `serverId === null` clears the override.
52
- */
53
- setOrgDefault(ctx: RequestContext, orgId: string, serverId: string | null): Promise<void>;
54
-
55
- /**
56
- * Hard-delete this org's compute rows: org-private servers, the
57
- * `orgDefaults[orgId]` entry, and any references to this org inside
58
- * platform servers' `sharedWith` allowlists. No-op when none exist.
59
- * Called from `deleteOrg` so soft-deleting an org does not leave its
60
- * compute config behind.
61
- */
62
- deleteByOrg(ctx: RequestContext, orgId: string): Promise<void>;
63
-
64
- /**
65
- * Boot-time integrity check: attempt to decrypt every stored `apiKey` and
66
- * report any that are plaintext-at-rest or fail to decrypt under the current
67
- * `SELVA_AT_REST_KEY`. Does NOT throw — returns a structured report so the
68
- * caller (boot health) decides whether to refuse boot / drive `/api/health`
69
- * to 503. Both encrypting stores (local file, Supabase) implement this;
70
- * optional so a hypothetical non-encrypting store can omit it.
71
- */
72
- verifySecrets?(): Promise<SecretVerificationReport>;
73
- }
@@ -1,102 +0,0 @@
1
- import { randomBytes, createCipheriv, createDecipheriv } from 'node:crypto';
2
-
3
- const ALGO = 'aes-256-gcm';
4
- const IV_BYTES = 12;
5
- const TAG_BYTES = 16;
6
- const PREFIX = 'enc:v1:';
7
-
8
- /**
9
- * AES-256-GCM envelope for at-rest secrets (e.g. compute API keys).
10
- *
11
- * Wire format: `enc:v1:<base64(iv|tag|ciphertext)>`. The version prefix lets
12
- * us migrate to a new algorithm later without ambiguity. Plaintext never sits
13
- * at rest — on disk (local provider) or in the DB (Supabase provider).
14
- *
15
- * Security model: defends against backup leaks, accidental file/DB sharing,
16
- * and read-only storage access. An attacker with both the stored ciphertext
17
- * *and* the master key (`SELVA_AT_REST_KEY` env var, process memory) can still
18
- * decrypt — this is encryption at rest, not a secret manager.
19
- *
20
- * Provider-agnostic: pure `node:crypto`, no fs or DB coupling, so both the
21
- * local and Supabase compute-server stores share this one implementation.
22
- */
23
-
24
- export function isEncryptedSecret(value: string): boolean {
25
- return value.startsWith(PREFIX);
26
- }
27
-
28
- export function encryptSecret(plaintext: string, key: Buffer): string {
29
- if (key.length !== 32) {
30
- throw new Error(`Secret key must be 32 bytes; got ${key.length}`);
31
- }
32
- if (isEncryptedSecret(plaintext)) {
33
- throw new Error('Refusing to encrypt a value that is already encrypted');
34
- }
35
- const iv = randomBytes(IV_BYTES);
36
- const cipher = createCipheriv(ALGO, key, iv);
37
- const ciphertext = Buffer.concat([cipher.update(plaintext, 'utf8'), cipher.final()]);
38
- const tag = cipher.getAuthTag();
39
- return PREFIX + Buffer.concat([iv, tag, ciphertext]).toString('base64');
40
- }
41
-
42
- export function decryptSecret(envelope: string, key: Buffer): string {
43
- if (key.length !== 32) {
44
- throw new Error(`Secret key must be 32 bytes; got ${key.length}`);
45
- }
46
- if (!isEncryptedSecret(envelope)) {
47
- throw new Error('Value is not an encrypted secret envelope');
48
- }
49
- const buf = Buffer.from(envelope.slice(PREFIX.length), 'base64');
50
- const iv = buf.subarray(0, IV_BYTES);
51
- const tag = buf.subarray(IV_BYTES, IV_BYTES + TAG_BYTES);
52
- const ciphertext = buf.subarray(IV_BYTES + TAG_BYTES);
53
- const decipher = createDecipheriv(ALGO, key, iv);
54
- decipher.setAuthTag(tag);
55
- return Buffer.concat([decipher.update(ciphertext), decipher.final()]).toString('utf8');
56
- }
57
-
58
- /**
59
- * Decode a `SELVA_AT_REST_KEY` env var into a 32-byte buffer. Accepts either
60
- * 64-char hex or base64 (with or without padding). Throws on anything else
61
- * so misconfiguration fails loudly at boot rather than silently producing
62
- * an undecryptable value.
63
- */
64
- export function decodeSecretKey(raw: string): Buffer {
65
- if (/^[0-9a-fA-F]{64}$/.test(raw)) return Buffer.from(raw, 'hex');
66
- const buf = Buffer.from(raw, 'base64');
67
- if (buf.length === 32) return buf;
68
- throw new Error(
69
- 'SELVA_AT_REST_KEY must be 32 bytes encoded as 64-char hex or base64. ' +
70
- "Generate one with: node -e \"console.log(require('crypto').randomBytes(32).toString('hex'))\""
71
- );
72
- }
73
-
74
- /**
75
- * Result of a compute-server store's `verifySecrets()`. One `failures` entry
76
- * per server whose `apiKey` couldn't be loaded:
77
- * - `plaintext_on_disk` — the field exists but isn't an `enc:v1:` envelope.
78
- * Either a hand-edit, a legacy row from before encryption, or a migration
79
- * regression. Security-relevant.
80
- * - `key_mismatch` — envelope is valid but GCM auth-tag verification
81
- * fails under the current `SELVA_AT_REST_KEY`. The key was rotated or the
82
- * data came from another deployment.
83
- *
84
- * Storage-agnostic (`plaintext_on_disk` reads "plaintext at rest" for both the
85
- * file- and DB-backed stores) so boot health can treat every provider alike.
86
- */
87
- export type SecretVerificationFailureReason = 'key_mismatch' | 'plaintext_on_disk';
88
-
89
- export interface SecretVerificationFailure {
90
- serverId: string;
91
- serverLabel: string;
92
- reason: SecretVerificationFailureReason;
93
- /** Underlying error message for `key_mismatch`. Absent for plaintext. */
94
- cause?: string;
95
- }
96
-
97
- export interface SecretVerificationReport {
98
- ok: boolean;
99
- failures: SecretVerificationFailure[];
100
- /** True if at least one row holds an unencrypted apiKey at rest. */
101
- plaintextFound: boolean;
102
- }
@@ -1,74 +0,0 @@
1
- /**
2
- * Compute servers belong to one of two scopes — never both.
3
- *
4
- * - **Platform server** (`scope: 'platform'`) — created by `manage_compute`.
5
- * `sharedWith` controls which orgs see it. The single global
6
- * `defaultServerId` (in {@link ComputeConfig}) must reference a platform
7
- * server and is *always* visible to every org regardless of `sharedWith`.
8
- *
9
- * - **Org-private server** (`scope: 'org'`) — created by an org owner/admin
10
- * with `manage_org_compute`. Visible only to its `ownerOrgId`. Gated by
11
- * the `ALLOW_ORG_COMPUTE_OVERRIDE` platform flag.
12
- *
13
- * Architecture spec §4.8.
14
- */
15
- export type ComputeServerConfig = PlatformComputeServer | OrgComputeServer;
16
-
17
- interface ComputeServerCommon {
18
- id: string;
19
- label: string;
20
- /** Base URL of the Rhino.Compute instance. */
21
- serverUrl: string;
22
- /** Sent as `RhinoComputeKey` header. */
23
- apiKey?: string;
24
- /** Default: 30000. */
25
- timeoutMs?: number;
26
- /** Default: 0. */
27
- retryCount?: number;
28
- }
29
-
30
- export interface PlatformComputeServer extends ComputeServerCommon {
31
- scope: 'platform';
32
- /**
33
- * Which orgs can see this server in pickers / use it for solves.
34
- * - `'all'` — every org. Default in `tenancy: 'single'`.
35
- * - `string[]` — explicit org-id allowlist. `[]` = dormant
36
- * (admin-only) unless this server is the global `defaultServerId`,
37
- * which is always usable everywhere.
38
- */
39
- sharedWith: 'all' | string[];
40
- }
41
-
42
- export interface OrgComputeServer extends ComputeServerCommon {
43
- scope: 'org';
44
- /** The org that owns this server. Visible only to members of this org. */
45
- ownerOrgId: string;
46
- }
47
-
48
- export interface ComputeConfig {
49
- servers: ComputeServerConfig[];
50
- /**
51
- * Global default. Must reference a platform server. Always usable by
52
- * every org regardless of that server's `sharedWith` — this is the
53
- * "baseline server any user uses" floor.
54
- */
55
- defaultServerId?: string;
56
- /**
57
- * Per-org default override. `orgDefaults[orgId]` must reference a server
58
- * that is visible to `orgId` (a platform server with `orgId` in
59
- * `sharedWith` or `'all'`, or an org server owned by `orgId`).
60
- */
61
- orgDefaults?: Record<string, string>;
62
- }
63
-
64
- // ============================================================================
65
- // Type guards
66
- // ============================================================================
67
-
68
- export function isPlatformServer(s: ComputeServerConfig): s is PlatformComputeServer {
69
- return s.scope === 'platform';
70
- }
71
-
72
- export function isOrgServer(s: ComputeServerConfig): s is OrgComputeServer {
73
- return s.scope === 'org';
74
- }