@selvajs/local-provider 0.11.0
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 +21 -0
- package/README.md +123 -0
- package/dist/auth/LocalAuthProvider.d.ts +28 -0
- package/dist/auth/LocalAuthProvider.d.ts.map +1 -0
- package/dist/auth/LocalAuthProvider.js +142 -0
- package/dist/auth/LocalAuthProvider.js.map +1 -0
- package/dist/auth/__tests__/conformance.test.d.ts +2 -0
- package/dist/auth/__tests__/conformance.test.d.ts.map +1 -0
- package/dist/auth/__tests__/conformance.test.js +36 -0
- package/dist/auth/__tests__/conformance.test.js.map +1 -0
- package/dist/auth/hmac.d.ts +18 -0
- package/dist/auth/hmac.d.ts.map +1 -0
- package/dist/auth/hmac.js +41 -0
- package/dist/auth/hmac.js.map +1 -0
- package/dist/auth/index.d.ts +6 -0
- package/dist/auth/index.d.ts.map +1 -0
- package/dist/auth/index.js +4 -0
- package/dist/auth/index.js.map +1 -0
- package/dist/auth/users.d.ts +38 -0
- package/dist/auth/users.d.ts.map +1 -0
- package/dist/auth/users.js +100 -0
- package/dist/auth/users.js.map +1 -0
- package/dist/compute/FilesystemComputeProvider.d.ts +16 -0
- package/dist/compute/FilesystemComputeProvider.d.ts.map +1 -0
- package/dist/compute/FilesystemComputeProvider.js +51 -0
- package/dist/compute/FilesystemComputeProvider.js.map +1 -0
- package/dist/compute/SingleComputeServerProvider.d.ts +15 -0
- package/dist/compute/SingleComputeServerProvider.d.ts.map +1 -0
- package/dist/compute/SingleComputeServerProvider.js +26 -0
- package/dist/compute/SingleComputeServerProvider.js.map +1 -0
- package/dist/compute/types.d.ts +30 -0
- package/dist/compute/types.d.ts.map +1 -0
- package/dist/compute/types.js +2 -0
- package/dist/compute/types.js.map +1 -0
- package/dist/computeServer/FilesystemComputeServerStore.d.ts +14 -0
- package/dist/computeServer/FilesystemComputeServerStore.d.ts.map +1 -0
- package/dist/computeServer/FilesystemComputeServerStore.js +35 -0
- package/dist/computeServer/FilesystemComputeServerStore.js.map +1 -0
- package/dist/computeServer/LocalComputeServerProvider.d.ts +18 -0
- package/dist/computeServer/LocalComputeServerProvider.d.ts.map +1 -0
- package/dist/computeServer/LocalComputeServerProvider.js +29 -0
- package/dist/computeServer/LocalComputeServerProvider.js.map +1 -0
- package/dist/computeServer/__tests__/conformance.test.d.ts +2 -0
- package/dist/computeServer/__tests__/conformance.test.d.ts.map +1 -0
- package/dist/computeServer/__tests__/conformance.test.js +20 -0
- package/dist/computeServer/__tests__/conformance.test.js.map +1 -0
- package/dist/computeServer/index.d.ts +2 -0
- package/dist/computeServer/index.d.ts.map +1 -0
- package/dist/computeServer/index.js +2 -0
- package/dist/computeServer/index.js.map +1 -0
- package/dist/data/LocalComputeServerStore.d.ts +72 -0
- package/dist/data/LocalComputeServerStore.d.ts.map +1 -0
- package/dist/data/LocalComputeServerStore.js +207 -0
- package/dist/data/LocalComputeServerStore.js.map +1 -0
- package/dist/data/LocalDataProvider.d.ts +47 -0
- package/dist/data/LocalDataProvider.d.ts.map +1 -0
- package/dist/data/LocalDataProvider.js +118 -0
- package/dist/data/LocalDataProvider.js.map +1 -0
- package/dist/data/LocalDefinitionMetaProvider.d.ts +22 -0
- package/dist/data/LocalDefinitionMetaProvider.d.ts.map +1 -0
- package/dist/data/LocalDefinitionMetaProvider.js +131 -0
- package/dist/data/LocalDefinitionMetaProvider.js.map +1 -0
- package/dist/data/LocalDefinitionStore.d.ts +33 -0
- package/dist/data/LocalDefinitionStore.d.ts.map +1 -0
- package/dist/data/LocalDefinitionStore.js +274 -0
- package/dist/data/LocalDefinitionStore.js.map +1 -0
- package/dist/data/LocalInviteStore.d.ts +23 -0
- package/dist/data/LocalInviteStore.d.ts.map +1 -0
- package/dist/data/LocalInviteStore.js +98 -0
- package/dist/data/LocalInviteStore.js.map +1 -0
- package/dist/data/LocalOrgStore.d.ts +67 -0
- package/dist/data/LocalOrgStore.d.ts.map +1 -0
- package/dist/data/LocalOrgStore.js +255 -0
- package/dist/data/LocalOrgStore.js.map +1 -0
- package/dist/data/LocalPlatformProjectGrantStore.d.ts +14 -0
- package/dist/data/LocalPlatformProjectGrantStore.d.ts.map +1 -0
- package/dist/data/LocalPlatformProjectGrantStore.js +62 -0
- package/dist/data/LocalPlatformProjectGrantStore.js.map +1 -0
- package/dist/data/LocalProjectStore.d.ts +30 -0
- package/dist/data/LocalProjectStore.d.ts.map +1 -0
- package/dist/data/LocalProjectStore.js +171 -0
- package/dist/data/LocalProjectStore.js.map +1 -0
- package/dist/data/LocalShareLinkStore.d.ts +39 -0
- package/dist/data/LocalShareLinkStore.d.ts.map +1 -0
- package/dist/data/LocalShareLinkStore.js +108 -0
- package/dist/data/LocalShareLinkStore.js.map +1 -0
- package/dist/data/__tests__/LocalDefinitionMetaProvider.test.d.ts +2 -0
- package/dist/data/__tests__/LocalDefinitionMetaProvider.test.d.ts.map +1 -0
- package/dist/data/__tests__/LocalDefinitionMetaProvider.test.js +21 -0
- package/dist/data/__tests__/LocalDefinitionMetaProvider.test.js.map +1 -0
- package/dist/data/__tests__/cascade.test.d.ts +2 -0
- package/dist/data/__tests__/cascade.test.d.ts.map +1 -0
- package/dist/data/__tests__/cascade.test.js +265 -0
- package/dist/data/__tests__/cascade.test.js.map +1 -0
- package/dist/data/__tests__/compute-server-conformance.test.d.ts +2 -0
- package/dist/data/__tests__/compute-server-conformance.test.d.ts.map +1 -0
- package/dist/data/__tests__/compute-server-conformance.test.js +21 -0
- package/dist/data/__tests__/compute-server-conformance.test.js.map +1 -0
- package/dist/data/__tests__/compute-server-encryption.test.d.ts +2 -0
- package/dist/data/__tests__/compute-server-encryption.test.d.ts.map +1 -0
- package/dist/data/__tests__/compute-server-encryption.test.js +131 -0
- package/dist/data/__tests__/compute-server-encryption.test.js.map +1 -0
- package/dist/data/__tests__/definition-conformance.test.d.ts +2 -0
- package/dist/data/__tests__/definition-conformance.test.d.ts.map +1 -0
- package/dist/data/__tests__/definition-conformance.test.js +20 -0
- package/dist/data/__tests__/definition-conformance.test.js.map +1 -0
- package/dist/data/__tests__/event-sink-conformance.test.d.ts +2 -0
- package/dist/data/__tests__/event-sink-conformance.test.d.ts.map +1 -0
- package/dist/data/__tests__/event-sink-conformance.test.js +24 -0
- package/dist/data/__tests__/event-sink-conformance.test.js.map +1 -0
- package/dist/data/__tests__/invite-conformance.test.d.ts +2 -0
- package/dist/data/__tests__/invite-conformance.test.d.ts.map +1 -0
- package/dist/data/__tests__/invite-conformance.test.js +21 -0
- package/dist/data/__tests__/invite-conformance.test.js.map +1 -0
- package/dist/data/__tests__/org-conformance.test.d.ts +2 -0
- package/dist/data/__tests__/org-conformance.test.d.ts.map +1 -0
- package/dist/data/__tests__/org-conformance.test.js +36 -0
- package/dist/data/__tests__/org-conformance.test.js.map +1 -0
- package/dist/data/__tests__/platform-project-grant-conformance.test.d.ts +2 -0
- package/dist/data/__tests__/platform-project-grant-conformance.test.d.ts.map +1 -0
- package/dist/data/__tests__/platform-project-grant-conformance.test.js +20 -0
- package/dist/data/__tests__/platform-project-grant-conformance.test.js.map +1 -0
- package/dist/data/__tests__/project-conformance.test.d.ts +2 -0
- package/dist/data/__tests__/project-conformance.test.d.ts.map +1 -0
- package/dist/data/__tests__/project-conformance.test.js +53 -0
- package/dist/data/__tests__/project-conformance.test.js.map +1 -0
- package/dist/data/__tests__/rules.test.d.ts +2 -0
- package/dist/data/__tests__/rules.test.d.ts.map +1 -0
- package/dist/data/__tests__/rules.test.js +484 -0
- package/dist/data/__tests__/rules.test.js.map +1 -0
- package/dist/data/__tests__/share-link-conformance.test.d.ts +2 -0
- package/dist/data/__tests__/share-link-conformance.test.d.ts.map +1 -0
- package/dist/data/__tests__/share-link-conformance.test.js +20 -0
- package/dist/data/__tests__/share-link-conformance.test.js.map +1 -0
- package/dist/data/fsJson.d.ts +12 -0
- package/dist/data/fsJson.d.ts.map +1 -0
- package/dist/data/fsJson.js +29 -0
- package/dist/data/fsJson.js.map +1 -0
- package/dist/data/index.d.ts +13 -0
- package/dist/data/index.d.ts.map +1 -0
- package/dist/data/index.js +9 -0
- package/dist/data/index.js.map +1 -0
- package/dist/data/pagination.d.ts +15 -0
- package/dist/data/pagination.d.ts.map +1 -0
- package/dist/data/pagination.js +36 -0
- package/dist/data/pagination.js.map +1 -0
- package/dist/data/secretCrypto.d.ts +23 -0
- package/dist/data/secretCrypto.d.ts.map +1 -0
- package/dist/data/secretCrypto.js +64 -0
- package/dist/data/secretCrypto.js.map +1 -0
- package/dist/data/userData.d.ts +40 -0
- package/dist/data/userData.d.ts.map +1 -0
- package/dist/data/userData.js +84 -0
- package/dist/data/userData.js.map +1 -0
- package/dist/definitions/LocalDefinitionMetaProvider.d.ts +27 -0
- package/dist/definitions/LocalDefinitionMetaProvider.d.ts.map +1 -0
- package/dist/definitions/LocalDefinitionMetaProvider.js +188 -0
- package/dist/definitions/LocalDefinitionMetaProvider.js.map +1 -0
- package/dist/definitions/__tests__/conformance.test.d.ts +2 -0
- package/dist/definitions/__tests__/conformance.test.d.ts.map +1 -0
- package/dist/definitions/__tests__/conformance.test.js +20 -0
- package/dist/definitions/__tests__/conformance.test.js.map +1 -0
- package/dist/definitions/index.d.ts +2 -0
- package/dist/definitions/index.d.ts.map +1 -0
- package/dist/definitions/index.js +2 -0
- package/dist/definitions/index.js.map +1 -0
- package/dist/definitions/providers/filesystem-files.d.ts +24 -0
- package/dist/definitions/providers/filesystem-files.d.ts.map +1 -0
- package/dist/definitions/providers/filesystem-files.js +170 -0
- package/dist/definitions/providers/filesystem-files.js.map +1 -0
- package/dist/definitions/providers/filesystem-meta.d.ts +17 -0
- package/dist/definitions/providers/filesystem-meta.d.ts.map +1 -0
- package/dist/definitions/providers/filesystem-meta.js +216 -0
- package/dist/definitions/providers/filesystem-meta.js.map +1 -0
- package/dist/fsJson.d.ts +12 -0
- package/dist/fsJson.d.ts.map +1 -0
- package/dist/fsJson.js +29 -0
- package/dist/fsJson.js.map +1 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +16 -0
- package/dist/index.js.map +1 -0
- package/dist/invites/LocalInviteProvider.d.ts +24 -0
- package/dist/invites/LocalInviteProvider.d.ts.map +1 -0
- package/dist/invites/LocalInviteProvider.js +89 -0
- package/dist/invites/LocalInviteProvider.js.map +1 -0
- package/dist/invites/__tests__/conformance.test.d.ts +2 -0
- package/dist/invites/__tests__/conformance.test.d.ts.map +1 -0
- package/dist/invites/__tests__/conformance.test.js +21 -0
- package/dist/invites/__tests__/conformance.test.js.map +1 -0
- package/dist/organizations/LocalOrganizationProvider.d.ts +41 -0
- package/dist/organizations/LocalOrganizationProvider.d.ts.map +1 -0
- package/dist/organizations/LocalOrganizationProvider.js +198 -0
- package/dist/organizations/LocalOrganizationProvider.js.map +1 -0
- package/dist/organizations/__tests__/conformance.test.d.ts +2 -0
- package/dist/organizations/__tests__/conformance.test.d.ts.map +1 -0
- package/dist/organizations/__tests__/conformance.test.js +20 -0
- package/dist/organizations/__tests__/conformance.test.js.map +1 -0
- package/dist/organizations/index.d.ts +2 -0
- package/dist/organizations/index.d.ts.map +1 -0
- package/dist/organizations/index.js +2 -0
- package/dist/organizations/index.js.map +1 -0
- package/dist/pagination.d.ts +15 -0
- package/dist/pagination.d.ts.map +1 -0
- package/dist/pagination.js +36 -0
- package/dist/pagination.js.map +1 -0
- package/dist/permissions/LocalPlatformPermissionStore.d.ts +39 -0
- package/dist/permissions/LocalPlatformPermissionStore.d.ts.map +1 -0
- package/dist/permissions/LocalPlatformPermissionStore.js +117 -0
- package/dist/permissions/LocalPlatformPermissionStore.js.map +1 -0
- package/dist/permissions/__tests__/conformance.test.d.ts +2 -0
- package/dist/permissions/__tests__/conformance.test.d.ts.map +1 -0
- package/dist/permissions/__tests__/conformance.test.js +37 -0
- package/dist/permissions/__tests__/conformance.test.js.map +1 -0
- package/dist/permissions/index.d.ts +2 -0
- package/dist/permissions/index.d.ts.map +1 -0
- package/dist/permissions/index.js +2 -0
- package/dist/permissions/index.js.map +1 -0
- package/dist/projects/LocalProjectProvider.d.ts +21 -0
- package/dist/projects/LocalProjectProvider.d.ts.map +1 -0
- package/dist/projects/LocalProjectProvider.js +125 -0
- package/dist/projects/LocalProjectProvider.js.map +1 -0
- package/dist/projects/__tests__/conformance.test.d.ts +2 -0
- package/dist/projects/__tests__/conformance.test.d.ts.map +1 -0
- package/dist/projects/__tests__/conformance.test.js +44 -0
- package/dist/projects/__tests__/conformance.test.js.map +1 -0
- package/dist/projects/index.d.ts +2 -0
- package/dist/projects/index.d.ts.map +1 -0
- package/dist/projects/index.js +2 -0
- package/dist/projects/index.js.map +1 -0
- package/dist/storage/LocalStorageProvider.d.ts +27 -0
- package/dist/storage/LocalStorageProvider.d.ts.map +1 -0
- package/dist/storage/LocalStorageProvider.js +74 -0
- package/dist/storage/LocalStorageProvider.js.map +1 -0
- package/dist/storage/__tests__/conformance.test.d.ts +2 -0
- package/dist/storage/__tests__/conformance.test.d.ts.map +1 -0
- package/dist/storage/__tests__/conformance.test.js +20 -0
- package/dist/storage/__tests__/conformance.test.js.map +1 -0
- package/dist/storage/index.d.ts +2 -0
- package/dist/storage/index.d.ts.map +1 -0
- package/dist/storage/index.js +2 -0
- package/dist/storage/index.js.map +1 -0
- package/dist/userProfile/LocalUserProfileProvider.d.ts +25 -0
- package/dist/userProfile/LocalUserProfileProvider.d.ts.map +1 -0
- package/dist/userProfile/LocalUserProfileProvider.js +110 -0
- package/dist/userProfile/LocalUserProfileProvider.js.map +1 -0
- package/dist/userProfile/__tests__/conformance.test.d.ts +2 -0
- package/dist/userProfile/__tests__/conformance.test.d.ts.map +1 -0
- package/dist/userProfile/__tests__/conformance.test.js +40 -0
- package/dist/userProfile/__tests__/conformance.test.js.map +1 -0
- package/dist/userProfile/index.d.ts +2 -0
- package/dist/userProfile/index.d.ts.map +1 -0
- package/dist/userProfile/index.js +2 -0
- package/dist/userProfile/index.js.map +1 -0
- package/package.json +70 -0
- package/src/README.md +37 -0
- package/src/auth/LocalAuthProvider.ts +165 -0
- package/src/auth/__tests__/conformance.test.ts +40 -0
- package/src/auth/hmac.ts +53 -0
- package/src/auth/index.ts +5 -0
- package/src/auth/users.ts +151 -0
- package/src/data/LocalComputeServerStore.ts +290 -0
- package/src/data/LocalDataProvider.ts +148 -0
- package/src/data/LocalDefinitionStore.ts +369 -0
- package/src/data/LocalInviteStore.ts +117 -0
- package/src/data/LocalOrgStore.ts +356 -0
- package/src/data/LocalPlatformProjectGrantStore.ts +85 -0
- package/src/data/LocalProjectStore.ts +274 -0
- package/src/data/LocalShareLinkStore.ts +138 -0
- package/src/data/__tests__/cascade.test.ts +300 -0
- package/src/data/__tests__/compute-server-conformance.test.ts +26 -0
- package/src/data/__tests__/compute-server-encryption.test.ts +185 -0
- package/src/data/__tests__/definition-conformance.test.ts +23 -0
- package/src/data/__tests__/event-sink-conformance.test.ts +28 -0
- package/src/data/__tests__/invite-conformance.test.ts +24 -0
- package/src/data/__tests__/org-conformance.test.ts +43 -0
- package/src/data/__tests__/platform-project-grant-conformance.test.ts +24 -0
- package/src/data/__tests__/project-conformance.test.ts +64 -0
- package/src/data/__tests__/rules.test.ts +682 -0
- package/src/data/__tests__/share-link-conformance.test.ts +23 -0
- package/src/data/fsJson.ts +28 -0
- package/src/data/index.ts +16 -0
- package/src/data/pagination.ts +48 -0
- package/src/data/secretCrypto.ts +69 -0
- package/src/data/userData.ts +134 -0
- package/src/index.ts +42 -0
- package/src/permissions/LocalPlatformPermissionStore.ts +129 -0
- package/src/permissions/__tests__/conformance.test.ts +40 -0
- package/src/permissions/index.ts +1 -0
- package/src/storage/LocalStorageProvider.ts +78 -0
- package/src/storage/__tests__/conformance.test.ts +23 -0
- package/src/storage/index.ts +1 -0
- package/src/userProfile/LocalUserProfileProvider.ts +135 -0
- package/src/userProfile/__tests__/conformance.test.ts +43 -0
- package/src/userProfile/index.ts +1 -0
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
import * as path from 'node:path';
|
|
2
|
+
import {
|
|
3
|
+
isOrgServer,
|
|
4
|
+
isPlatformServer,
|
|
5
|
+
type IComputeServerStore,
|
|
6
|
+
type ComputeConfig,
|
|
7
|
+
type ComputeServerConfig,
|
|
8
|
+
type PlatformComputeServer,
|
|
9
|
+
type RequestContext
|
|
10
|
+
} from '@selvajs/platform';
|
|
11
|
+
import { readJsonFile, writeJsonFile } from './fsJson.js';
|
|
12
|
+
import {
|
|
13
|
+
decodeSecretKey,
|
|
14
|
+
decryptSecret,
|
|
15
|
+
encryptSecret,
|
|
16
|
+
isEncryptedSecret
|
|
17
|
+
} from './secretCrypto.js';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* On-disk file shape. Single document holding *all* servers (platform +
|
|
21
|
+
* org-private), the global `defaultServerId`, and the per-org
|
|
22
|
+
* `orgDefaults` map. Spec §3.
|
|
23
|
+
*
|
|
24
|
+
* `apiKey` on disk is always an `enc:v1:<…>` envelope (AES-256-GCM); the
|
|
25
|
+
* store decrypts on read so callers see plaintext.
|
|
26
|
+
*/
|
|
27
|
+
interface OnDiskShape {
|
|
28
|
+
servers: ComputeServerConfig[];
|
|
29
|
+
defaultServerId?: string;
|
|
30
|
+
orgDefaults?: Record<string, string>;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const EMPTY: OnDiskShape = { servers: [], orgDefaults: {} };
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Result of {@link LocalComputeServerStore.verifySecrets}. One entry per
|
|
37
|
+
* server whose `apiKey` couldn't be loaded:
|
|
38
|
+
* - `plaintext_on_disk` — the field exists but isn't an `enc:v1:` envelope.
|
|
39
|
+
* Either a hand-edit or a migration regression. Security-relevant.
|
|
40
|
+
* - `key_mismatch` — envelope is valid but GCM auth tag verification
|
|
41
|
+
* fails under the current `SELVA_AT_REST_KEY`. The key was rotated or the
|
|
42
|
+
* data came from another deployment.
|
|
43
|
+
*/
|
|
44
|
+
export type SecretVerificationFailureReason = 'key_mismatch' | 'plaintext_on_disk';
|
|
45
|
+
|
|
46
|
+
export interface SecretVerificationFailure {
|
|
47
|
+
serverId: string;
|
|
48
|
+
serverLabel: string;
|
|
49
|
+
reason: SecretVerificationFailureReason;
|
|
50
|
+
/** Underlying error message for `key_mismatch`. Absent for plaintext. */
|
|
51
|
+
cause?: string;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface SecretVerificationReport {
|
|
55
|
+
ok: boolean;
|
|
56
|
+
failures: SecretVerificationFailure[];
|
|
57
|
+
/** True if at least one row holds an unencrypted apiKey on disk. */
|
|
58
|
+
plaintextFound: boolean;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Reads/writes compute.config.json. The file is re-read on every read call
|
|
63
|
+
* so changes take effect without a restart.
|
|
64
|
+
*
|
|
65
|
+
* Mutation methods are scope-targeted (`savePlatformServers`,
|
|
66
|
+
* `saveOrgServers`, `setOrgDefault`) — each preserves rows in the other
|
|
67
|
+
* scopes untouched.
|
|
68
|
+
*/
|
|
69
|
+
export class LocalComputeServerStore implements IComputeServerStore {
|
|
70
|
+
static fromEnv(env: Record<string, string | undefined>): LocalComputeServerStore {
|
|
71
|
+
if (!env.DATA_PATH) throw new Error('Missing required env var: DATA_PATH');
|
|
72
|
+
if (!env.SELVA_AT_REST_KEY) {
|
|
73
|
+
throw new Error(
|
|
74
|
+
'Missing required env var: SELVA_AT_REST_KEY (32-byte hex or base64). ' +
|
|
75
|
+
"Generate one with: node -e \"console.log(require('crypto').randomBytes(32).toString('hex'))\""
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
return new LocalComputeServerStore(
|
|
79
|
+
path.join(env.DATA_PATH, 'compute.config.json'),
|
|
80
|
+
decodeSecretKey(env.SELVA_AT_REST_KEY)
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
constructor(
|
|
85
|
+
private readonly configFilePath: string,
|
|
86
|
+
private readonly secretKey: Buffer
|
|
87
|
+
) {}
|
|
88
|
+
|
|
89
|
+
private async readAll(): Promise<OnDiskShape> {
|
|
90
|
+
const raw = await readJsonFile<OnDiskShape>(this.configFilePath, EMPTY);
|
|
91
|
+
return {
|
|
92
|
+
servers: raw.servers ?? [],
|
|
93
|
+
defaultServerId: raw.defaultServerId,
|
|
94
|
+
orgDefaults: raw.orgDefaults ?? {}
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Per-row tolerant decrypt. A row whose ciphertext can't be authenticated
|
|
100
|
+
* under the current `SELVA_AT_REST_KEY` is returned with `apiKey: undefined`
|
|
101
|
+
* and a warning logged once. The page that loaded the config keeps
|
|
102
|
+
* rendering; solves against that server will fail later when Rhino.Compute
|
|
103
|
+
* rejects the missing key.
|
|
104
|
+
*
|
|
105
|
+
* Boot-time `verifySecrets()` is the strict counterpart — call that from
|
|
106
|
+
* the app entrypoint to refuse to start when this state is detected.
|
|
107
|
+
*
|
|
108
|
+
* Plaintext-on-disk is still hard-fail. That state is never produced by
|
|
109
|
+
* the store itself (every write goes through `encryptApiKeys`), so seeing
|
|
110
|
+
* it means someone hand-edited the file with a real secret in plaintext —
|
|
111
|
+
* which is a security issue we should surface loudly, not paper over.
|
|
112
|
+
*/
|
|
113
|
+
private decryptApiKeys(servers: ComputeServerConfig[]): ComputeServerConfig[] {
|
|
114
|
+
return servers.map((s) => {
|
|
115
|
+
if (!s.apiKey) return s;
|
|
116
|
+
if (!isEncryptedSecret(s.apiKey)) {
|
|
117
|
+
throw new Error(
|
|
118
|
+
`compute.config.json contains an unencrypted apiKey for server "${s.label}" (${s.id}). ` +
|
|
119
|
+
'Re-enter the key via /admin/compute so it is stored encrypted.'
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
try {
|
|
123
|
+
return { ...s, apiKey: decryptSecret(s.apiKey, this.secretKey) };
|
|
124
|
+
} catch (cause) {
|
|
125
|
+
console.warn(
|
|
126
|
+
`[selva] Could not decrypt apiKey for compute server "${s.label}" (${s.id}). ` +
|
|
127
|
+
'The stored ciphertext does not match the current SELVA_AT_REST_KEY. ' +
|
|
128
|
+
'This server will be returned without an apiKey; solves against it will fail. ' +
|
|
129
|
+
'Re-enter the key via /admin/compute, or restore the original SELVA_AT_REST_KEY. ' +
|
|
130
|
+
'See docs/Troubleshooting.md.',
|
|
131
|
+
cause
|
|
132
|
+
);
|
|
133
|
+
return { ...s, apiKey: undefined };
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Boot-time integrity check. Reads every server row and attempts to
|
|
140
|
+
* decrypt each encrypted `apiKey`. Returns a structured report — does NOT
|
|
141
|
+
* throw. The caller decides what to do (refuse boot, log + degrade, etc.).
|
|
142
|
+
*
|
|
143
|
+
* Use this from app startup (`hooks.server.ts`) so a key mismatch fails
|
|
144
|
+
* loudly at deploy time instead of as a blank page when a user first hits
|
|
145
|
+
* a route that loads compute config.
|
|
146
|
+
*/
|
|
147
|
+
async verifySecrets(): Promise<SecretVerificationReport> {
|
|
148
|
+
const all = await this.readAll();
|
|
149
|
+
const failures: SecretVerificationFailure[] = [];
|
|
150
|
+
let plaintextFound = false;
|
|
151
|
+
|
|
152
|
+
for (const s of all.servers) {
|
|
153
|
+
if (!s.apiKey) continue;
|
|
154
|
+
if (!isEncryptedSecret(s.apiKey)) {
|
|
155
|
+
plaintextFound = true;
|
|
156
|
+
failures.push({
|
|
157
|
+
serverId: s.id,
|
|
158
|
+
serverLabel: s.label,
|
|
159
|
+
reason: 'plaintext_on_disk'
|
|
160
|
+
});
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
try {
|
|
164
|
+
decryptSecret(s.apiKey, this.secretKey);
|
|
165
|
+
} catch (cause) {
|
|
166
|
+
failures.push({
|
|
167
|
+
serverId: s.id,
|
|
168
|
+
serverLabel: s.label,
|
|
169
|
+
reason: 'key_mismatch',
|
|
170
|
+
cause: cause instanceof Error ? cause.message : String(cause)
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return { ok: failures.length === 0, failures, plaintextFound };
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
private encryptApiKeys(servers: ComputeServerConfig[]): ComputeServerConfig[] {
|
|
179
|
+
return servers.map((s) => {
|
|
180
|
+
if (!s.apiKey) return s;
|
|
181
|
+
if (isEncryptedSecret(s.apiKey)) return s;
|
|
182
|
+
return { ...s, apiKey: encryptSecret(s.apiKey, this.secretKey) };
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
async getConfig(_ctx: RequestContext): Promise<ComputeConfig> {
|
|
187
|
+
const all = await this.readAll();
|
|
188
|
+
return {
|
|
189
|
+
servers: this.decryptApiKeys(all.servers),
|
|
190
|
+
defaultServerId: all.defaultServerId,
|
|
191
|
+
orgDefaults: all.orgDefaults
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
async savePlatformServers(
|
|
196
|
+
_ctx: RequestContext,
|
|
197
|
+
servers: ComputeServerConfig[],
|
|
198
|
+
defaultServerId: string | undefined
|
|
199
|
+
): Promise<void> {
|
|
200
|
+
const all = await this.readAll();
|
|
201
|
+
const orgRows = all.servers.filter(isOrgServer);
|
|
202
|
+
const platformRows = this.encryptApiKeys(servers.filter(isPlatformServer));
|
|
203
|
+
|
|
204
|
+
await writeJsonFile<OnDiskShape>(this.configFilePath, {
|
|
205
|
+
servers: [...platformRows, ...orgRows],
|
|
206
|
+
defaultServerId,
|
|
207
|
+
orgDefaults: all.orgDefaults
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
async saveOrgServers(
|
|
212
|
+
_ctx: RequestContext,
|
|
213
|
+
orgId: string,
|
|
214
|
+
servers: ComputeServerConfig[],
|
|
215
|
+
defaultServerId?: string | null
|
|
216
|
+
): Promise<void> {
|
|
217
|
+
const all = await this.readAll();
|
|
218
|
+
const platformRows = all.servers.filter(isPlatformServer);
|
|
219
|
+
const otherOrgRows = all.servers.filter((s) => isOrgServer(s) && s.ownerOrgId !== orgId);
|
|
220
|
+
const thisOrgRows = this.encryptApiKeys(
|
|
221
|
+
servers
|
|
222
|
+
.filter((_s): _s is ComputeServerConfig => true)
|
|
223
|
+
.map((s) =>
|
|
224
|
+
isOrgServer(s)
|
|
225
|
+
? { ...s, ownerOrgId: orgId }
|
|
226
|
+
: // Coerce — caller passed something with the wrong/missing scope.
|
|
227
|
+
({ ...s, scope: 'org', ownerOrgId: orgId } as ComputeServerConfig)
|
|
228
|
+
)
|
|
229
|
+
);
|
|
230
|
+
|
|
231
|
+
const orgDefaults = { ...(all.orgDefaults ?? {}) };
|
|
232
|
+
if (defaultServerId === null) {
|
|
233
|
+
delete orgDefaults[orgId];
|
|
234
|
+
} else if (typeof defaultServerId === 'string') {
|
|
235
|
+
orgDefaults[orgId] = defaultServerId;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
await writeJsonFile<OnDiskShape>(this.configFilePath, {
|
|
239
|
+
servers: [...platformRows, ...otherOrgRows, ...thisOrgRows],
|
|
240
|
+
defaultServerId: all.defaultServerId,
|
|
241
|
+
orgDefaults
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
async setOrgDefault(_ctx: RequestContext, orgId: string, serverId: string | null): Promise<void> {
|
|
246
|
+
const all = await this.readAll();
|
|
247
|
+
const orgDefaults = { ...(all.orgDefaults ?? {}) };
|
|
248
|
+
if (serverId === null) {
|
|
249
|
+
delete orgDefaults[orgId];
|
|
250
|
+
} else {
|
|
251
|
+
orgDefaults[orgId] = serverId;
|
|
252
|
+
}
|
|
253
|
+
await writeJsonFile<OnDiskShape>(this.configFilePath, { ...all, orgDefaults });
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
async deleteByOrg(_ctx: RequestContext, orgId: string): Promise<void> {
|
|
257
|
+
const all = await this.readAll();
|
|
258
|
+
|
|
259
|
+
// Drop org-private rows owned by this org.
|
|
260
|
+
const remaining = all.servers.filter((s) => !(isOrgServer(s) && s.ownerOrgId === orgId));
|
|
261
|
+
|
|
262
|
+
// Strip this org from any platform server's `sharedWith` allowlist.
|
|
263
|
+
const cleaned: ComputeServerConfig[] = remaining.map((s) => {
|
|
264
|
+
if (!isPlatformServer(s)) return s;
|
|
265
|
+
if (s.sharedWith === 'all') return s;
|
|
266
|
+
if (!s.sharedWith.includes(orgId)) return s;
|
|
267
|
+
const next: PlatformComputeServer = {
|
|
268
|
+
...s,
|
|
269
|
+
sharedWith: s.sharedWith.filter((id) => id !== orgId)
|
|
270
|
+
};
|
|
271
|
+
return next;
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
const orgDefaults = { ...(all.orgDefaults ?? {}) };
|
|
275
|
+
const hadDefault = orgId in orgDefaults;
|
|
276
|
+
delete orgDefaults[orgId];
|
|
277
|
+
|
|
278
|
+
const changed =
|
|
279
|
+
cleaned.length !== all.servers.length ||
|
|
280
|
+
hadDefault ||
|
|
281
|
+
cleaned.some((c, i) => c !== all.servers[i]);
|
|
282
|
+
if (!changed) return;
|
|
283
|
+
|
|
284
|
+
await writeJsonFile<OnDiskShape>(this.configFilePath, {
|
|
285
|
+
servers: cleaned,
|
|
286
|
+
defaultServerId: all.defaultServerId,
|
|
287
|
+
orgDefaults
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
IDataProvider,
|
|
3
|
+
IOrgStore,
|
|
4
|
+
IProjectStore,
|
|
5
|
+
IDefinitionStore,
|
|
6
|
+
IComputeServerStore,
|
|
7
|
+
IInviteStore,
|
|
8
|
+
IShareLinkStore,
|
|
9
|
+
IUserProfileStore,
|
|
10
|
+
IPlatformPermissionStore,
|
|
11
|
+
IPlatformProjectGrantStore,
|
|
12
|
+
IEventSink,
|
|
13
|
+
RequestContext
|
|
14
|
+
} from '@selvajs/platform';
|
|
15
|
+
import { NoopEventSink } from '@selvajs/platform';
|
|
16
|
+
import * as path from 'node:path';
|
|
17
|
+
import { LocalOrgStore, LocalOrgStoreLoader } from './LocalOrgStore.js';
|
|
18
|
+
import { LocalProjectStore } from './LocalProjectStore.js';
|
|
19
|
+
import { LocalDefinitionStore } from './LocalDefinitionStore.js';
|
|
20
|
+
import { LocalComputeServerStore } from './LocalComputeServerStore.js';
|
|
21
|
+
import { LocalInviteStore } from './LocalInviteStore.js';
|
|
22
|
+
import { LocalShareLinkStore } from './LocalShareLinkStore.js';
|
|
23
|
+
import { LocalPlatformProjectGrantStore } from './LocalPlatformProjectGrantStore.js';
|
|
24
|
+
import { LocalUserProfileProvider } from '../userProfile/LocalUserProfileProvider.js';
|
|
25
|
+
import { LocalPlatformPermissionStore } from '../permissions/LocalPlatformPermissionStore.js';
|
|
26
|
+
import { createLocalUserDataStore, type LocalUserDataStore } from './userData.js';
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Composition of every local-provider data store. One `LocalOrgStoreLoader`
|
|
30
|
+
* is shared across org + project stores so they see the same cache and
|
|
31
|
+
* atomic write path.
|
|
32
|
+
*
|
|
33
|
+
* The `LocalUserDataStore` is similarly shared across the permissions and
|
|
34
|
+
* profile stores: both write to `user-data.json`, and `ensureUser` seeds
|
|
35
|
+
* exactly the same row both stores read from. This is the local equivalent
|
|
36
|
+
* of Supabase's `handle_new_auth_user` trigger.
|
|
37
|
+
*
|
|
38
|
+
* Stores are passed as a record so adding a new store doesn't ripple through
|
|
39
|
+
* test fixtures and call sites.
|
|
40
|
+
*/
|
|
41
|
+
export class LocalDataProvider implements IDataProvider {
|
|
42
|
+
readonly orgs: IOrgStore;
|
|
43
|
+
readonly projects: IProjectStore;
|
|
44
|
+
readonly definitions: IDefinitionStore;
|
|
45
|
+
readonly computeServer: IComputeServerStore;
|
|
46
|
+
readonly invites: IInviteStore;
|
|
47
|
+
readonly shareLinks: IShareLinkStore;
|
|
48
|
+
readonly userProfile: IUserProfileStore;
|
|
49
|
+
readonly permissions: IPlatformPermissionStore;
|
|
50
|
+
readonly platformProjectGrants: IPlatformProjectGrantStore;
|
|
51
|
+
|
|
52
|
+
private readonly userData: LocalUserDataStore;
|
|
53
|
+
|
|
54
|
+
private constructor(
|
|
55
|
+
stores: Omit<IDataProvider, 'ensureUser' | 'onUserDeleted'>,
|
|
56
|
+
userData: LocalUserDataStore
|
|
57
|
+
) {
|
|
58
|
+
this.orgs = stores.orgs;
|
|
59
|
+
this.projects = stores.projects;
|
|
60
|
+
this.definitions = stores.definitions;
|
|
61
|
+
this.computeServer = stores.computeServer;
|
|
62
|
+
this.invites = stores.invites;
|
|
63
|
+
this.shareLinks = stores.shareLinks;
|
|
64
|
+
this.userProfile = stores.userProfile;
|
|
65
|
+
this.permissions = stores.permissions;
|
|
66
|
+
this.platformProjectGrants = stores.platformProjectGrants;
|
|
67
|
+
this.userData = userData;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Idempotently register a user in the data layer. Called from
|
|
72
|
+
* `hooks.server.ts` on every authed request — the local equivalent of
|
|
73
|
+
* Supabase's `handle_new_auth_user` trigger. After this completes the
|
|
74
|
+
* user has an empty row in `user-data.json` that the permissions and
|
|
75
|
+
* profile stores can read and update.
|
|
76
|
+
*
|
|
77
|
+
* `ctx` is unused — registration runs as a system operation regardless of
|
|
78
|
+
* the calling user. Argument is kept for interface symmetry with adapters
|
|
79
|
+
* that need it.
|
|
80
|
+
*/
|
|
81
|
+
async ensureUser(_ctx: RequestContext, userId: string): Promise<void> {
|
|
82
|
+
await this.userData.ensure(userId);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Cascade hook called after the auth provider deletes a user. Removes the
|
|
87
|
+
* matching `user-data.json` row so the data layer doesn't accumulate
|
|
88
|
+
* orphans. Tolerates missing rows.
|
|
89
|
+
*/
|
|
90
|
+
async onUserDeleted(_ctx: RequestContext, userId: string): Promise<void> {
|
|
91
|
+
try {
|
|
92
|
+
await this.userData.deleteUser(userId);
|
|
93
|
+
} catch {
|
|
94
|
+
// Already absent — nothing to clean up.
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
static fromEnv(
|
|
99
|
+
env: Record<string, string | undefined>,
|
|
100
|
+
events: IEventSink = new NoopEventSink()
|
|
101
|
+
): LocalDataProvider {
|
|
102
|
+
if (!env.DATA_PATH) throw new Error('Missing required env var: DATA_PATH');
|
|
103
|
+
const dataPath = env.DATA_PATH;
|
|
104
|
+
const userDataFilePath = path.join(dataPath, 'user-data.json');
|
|
105
|
+
|
|
106
|
+
const loader = new LocalOrgStoreLoader(dataPath);
|
|
107
|
+
const platformProjectGrants = LocalPlatformProjectGrantStore.fromEnv(env);
|
|
108
|
+
const invites = LocalInviteStore.fromEnv(env, events);
|
|
109
|
+
const computeServer = LocalComputeServerStore.fromEnv(env);
|
|
110
|
+
const projects = new LocalProjectStore({ loader, grants: platformProjectGrants, events });
|
|
111
|
+
const definitions = new LocalDefinitionStore(dataPath, undefined, events);
|
|
112
|
+
const shareLinks = new LocalShareLinkStore({
|
|
113
|
+
filePath: path.join(dataPath, 'share-links.json'),
|
|
114
|
+
events
|
|
115
|
+
});
|
|
116
|
+
const orgs = new LocalOrgStore({
|
|
117
|
+
loader,
|
|
118
|
+
invites,
|
|
119
|
+
computeServer,
|
|
120
|
+
grants: platformProjectGrants,
|
|
121
|
+
events
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
// Wire cross-store deps that aren't constructor-injected:
|
|
125
|
+
// - canEditDefinition needs the project store for `listPublic`
|
|
126
|
+
// - share-link resolution needs the definition store to enforce the §7
|
|
127
|
+
// soft-delete cascade (Supabase does the equivalent via JOIN)
|
|
128
|
+
definitions.setProjectProvider(projects);
|
|
129
|
+
shareLinks.setDefinitionProvider(definitions);
|
|
130
|
+
|
|
131
|
+
const userData = createLocalUserDataStore(userDataFilePath);
|
|
132
|
+
|
|
133
|
+
return new LocalDataProvider(
|
|
134
|
+
{
|
|
135
|
+
orgs,
|
|
136
|
+
projects,
|
|
137
|
+
definitions,
|
|
138
|
+
computeServer,
|
|
139
|
+
invites,
|
|
140
|
+
shareLinks,
|
|
141
|
+
userProfile: new LocalUserProfileProvider(userDataFilePath),
|
|
142
|
+
permissions: new LocalPlatformPermissionStore(userDataFilePath),
|
|
143
|
+
platformProjectGrants
|
|
144
|
+
},
|
|
145
|
+
userData
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
}
|