@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
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Selva FelixBrunold VektorNode
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# @selvajs/local-provider
|
|
2
|
+
|
|
3
|
+
Filesystem + JSON + HMAC implementation of the `@selvajs/platform` interfaces.
|
|
4
|
+
|
|
5
|
+
The default provider for development and small single-instance deployments. All state — users, orgs, projects, definitions, compute config, uploaded `.gh` files — lives under one directory on disk. No database, no external services.
|
|
6
|
+
|
|
7
|
+
For production-scale or multi-instance deployments, use [`@selvajs/supabase-provider`](../supabase/README.md) instead.
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## Table of contents
|
|
12
|
+
|
|
13
|
+
- [When to use this provider](#when-to-use-this-provider)
|
|
14
|
+
- [Environment variables](#environment-variables)
|
|
15
|
+
- [On-disk layout](#on-disk-layout)
|
|
16
|
+
- [Wiring into `selva.config.ts`](#wiring-into-selvaconfigts)
|
|
17
|
+
- [Architecture notes](#architecture-notes)
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## When to use this provider
|
|
22
|
+
|
|
23
|
+
Pick local when:
|
|
24
|
+
|
|
25
|
+
- You're developing or evaluating Selva
|
|
26
|
+
- You're running a single-tenant, single-instance deployment (one VM, PM2)
|
|
27
|
+
- You want zero external dependencies — no DB, no S3
|
|
28
|
+
- You're OK with simple file-based backups (`tar` the data dir)
|
|
29
|
+
|
|
30
|
+
Pick [Supabase](../supabase/README.md) when:
|
|
31
|
+
|
|
32
|
+
- You need multiple selva app instances behind a load balancer
|
|
33
|
+
- You want managed auth (password reset, MFA, OAuth)
|
|
34
|
+
- You want managed Postgres + storage with backups + RLS
|
|
35
|
+
- You need atomic counters across processes
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## Environment variables
|
|
40
|
+
|
|
41
|
+
All env vars are documented in [`packages/selva/.env.example`](../../selva/.env.example) — copy that file to `.env` and edit it. The local provider reads `DATA_PATH`, `SELVA_HMAC_KEY` (signs sessions + tokens), and `SELVA_AT_REST_KEY` (encrypts the Rhino.Compute API key on disk) from there.
|
|
42
|
+
|
|
43
|
+
The first admin user is created through the in-app setup page on first boot — there is no env-var fallback login.
|
|
44
|
+
|
|
45
|
+
Rhino.Compute server URL + API key are configured in `/admin/compute` and persisted to `compute.config.json` — not env vars.
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## On-disk layout
|
|
50
|
+
|
|
51
|
+
```
|
|
52
|
+
$DATA_PATH/
|
|
53
|
+
├── users.json # users + hashed passwords + platform permissions
|
|
54
|
+
├── local-org.json # organizations, projects, and their memberships
|
|
55
|
+
├── definitions-config.json # definition metadata + version history
|
|
56
|
+
├── share-links.json # per-definition share tokens (HMAC-hashed)
|
|
57
|
+
├── invites.json # pending invite tokens
|
|
58
|
+
├── compute.config.json # registered Rhino.Compute servers
|
|
59
|
+
└── definitions/ # uploaded .gh / .ghx + cover images
|
|
60
|
+
└── <definition-guid>/
|
|
61
|
+
├── versions/v{n}.{ext}
|
|
62
|
+
└── cover.webp
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
All JSON files are written atomically (temp file + rename) so a crash mid-write leaves either the old or new file — never a partial. Image uploads are transcoded to WebP (1200px max, quality 85) via `sharp`.
|
|
66
|
+
|
|
67
|
+
**Backups:** `tar -czf backup.tar.gz $DATA_PATH`. Restore is the reverse — no schema migrations, no DB to bring up.
|
|
68
|
+
|
|
69
|
+
**Caveats:**
|
|
70
|
+
|
|
71
|
+
- Not safe across **processes** — no file locking. One selva app instance per data dir.
|
|
72
|
+
- Read-modify-write on JSON files (`incrementRunCount`, etc.) can lose updates under concurrent solves on the same definition. Acceptable for typical single-user workloads; switch to Supabase if you need exact counts under contention.
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
## Wiring into `selva.config.ts`
|
|
77
|
+
|
|
78
|
+
This is the default config in [`selva.config.ts`](../../../selva.config.ts) at the repo root:
|
|
79
|
+
|
|
80
|
+
```ts
|
|
81
|
+
import { defineConfig } from '@selvajs/platform';
|
|
82
|
+
import * as local from '@selvajs/local-provider';
|
|
83
|
+
|
|
84
|
+
export default defineConfig((env) => ({
|
|
85
|
+
tenancy: 'single' as const,
|
|
86
|
+
flags: {
|
|
87
|
+
ALLOW_CROSS_ORG_PUBLIC: false,
|
|
88
|
+
ALLOW_ORG_COMPUTE_OVERRIDE: false,
|
|
89
|
+
ALLOW_ORG_CREATION: false
|
|
90
|
+
},
|
|
91
|
+
auth: local.LocalAuthProvider.fromEnv(env),
|
|
92
|
+
data: local.LocalDataProvider.fromEnv(env),
|
|
93
|
+
storage: local.LocalStorageProvider.fromEnv(env)
|
|
94
|
+
}));
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
`LocalDataProvider` internally wires every store — orgs, projects, definitions, share-links, invites, compute server, user profile, platform permissions.
|
|
98
|
+
|
|
99
|
+
To switch to Supabase, see [`@selvajs/supabase-provider`](../supabase/README.md#wiring-into-selvaconfigts).
|
|
100
|
+
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
## Architecture notes
|
|
104
|
+
|
|
105
|
+
### Auth
|
|
106
|
+
|
|
107
|
+
`LocalAuthProvider` issues HMAC-signed session tokens (no JWT library; see [`auth/`](src/auth/)). Tokens carry `{ userId, expiresAt }` and are verified on every request.
|
|
108
|
+
|
|
109
|
+
Users live in `users.json` with `PBKDF2-SHA256` password hashes and platform permissions. The first admin is bootstrapped through the in-app setup page on a fresh install.
|
|
110
|
+
|
|
111
|
+
### Data
|
|
112
|
+
|
|
113
|
+
Each store (`LocalOrgStore`, `LocalProjectStore`, `LocalDefinitionStore`, `LocalInviteStore`, `LocalComputeServerStore`) reads its JSON file fully into memory on each call, mutates, and writes back. Fine at config-scale; not for high-churn data.
|
|
114
|
+
|
|
115
|
+
Access control is enforced **in-process** by inspecting `RequestContext.adapterContext` — there's no RLS layer to lean on. Tests for these checks live alongside each store.
|
|
116
|
+
|
|
117
|
+
### Storage
|
|
118
|
+
|
|
119
|
+
`LocalStorageProvider` writes blobs under `$DATA_PATH/<path>` (e.g. `$DATA_PATH/definitions/<guid>/versions/v1.gh`) — the caller's storage path is appended directly to the data root, with `..` rejected. `getPublicUrl` returns `/api/files/<path>`, which the selva app proxies after an auth check. Image uploads pass through the shared `transcodeImageIfNeeded` helper from `@selvajs/platform/storage` — same WebP output as Supabase.
|
|
120
|
+
|
|
121
|
+
### Shared helpers
|
|
122
|
+
|
|
123
|
+
`src/fsJson.ts` centralizes the read/atomic-write pattern every store uses. See [src/README.md](src/README.md) for details on the helper API.
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { IAuthProvider, IPasswordAuth, AuthUser, UserManagementResult, ListOptions, Page } from '@selvajs/platform';
|
|
2
|
+
export interface LocalAuthProviderConfig {
|
|
3
|
+
/** HMAC signing secret. Pass from env (SELVA_HMAC_KEY). */
|
|
4
|
+
hmacSecret: string;
|
|
5
|
+
/**
|
|
6
|
+
* Absolute path to auth-users.json — identity-only storage. Per-user app
|
|
7
|
+
* state (permissions, profile, starred defs, recent runs) lives in
|
|
8
|
+
* `user-data.json`, owned by `LocalDataProvider`. The two files key on
|
|
9
|
+
* the same user ID and are written independently.
|
|
10
|
+
*/
|
|
11
|
+
usersFilePath?: string;
|
|
12
|
+
}
|
|
13
|
+
export declare class LocalAuthProvider implements IAuthProvider {
|
|
14
|
+
private readonly hmacSecret;
|
|
15
|
+
private readonly users?;
|
|
16
|
+
readonly name = "Local";
|
|
17
|
+
readonly passwordAuth: IPasswordAuth;
|
|
18
|
+
constructor(config: LocalAuthProviderConfig);
|
|
19
|
+
static fromEnv(env: Record<string, string | undefined>): LocalAuthProvider;
|
|
20
|
+
/** Verify an HMAC session token and return the live user record. */
|
|
21
|
+
verifyToken(token: string): Promise<AuthUser | null>;
|
|
22
|
+
touchLastLogin(id: string): Promise<void>;
|
|
23
|
+
getUser(id: string): Promise<AuthUser | null>;
|
|
24
|
+
listUsers(opts?: ListOptions): Promise<Page<AuthUser> | null>;
|
|
25
|
+
deleteUser(id: string): Promise<UserManagementResult>;
|
|
26
|
+
disableUser(id: string): Promise<UserManagementResult>;
|
|
27
|
+
}
|
|
28
|
+
//# sourceMappingURL=LocalAuthProvider.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"LocalAuthProvider.d.ts","sourceRoot":"","sources":["../../src/auth/LocalAuthProvider.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACX,aAAa,EACb,aAAa,EACb,QAAQ,EAER,oBAAoB,EACpB,WAAW,EACX,IAAI,EACJ,MAAM,mBAAmB,CAAC;AAqB3B,MAAM,WAAW,uBAAuB;IACvC,2DAA2D;IAC3D,UAAU,EAAE,MAAM,CAAC;IACnB;;;;;OAKG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;CACvB;AAuCD,qBAAa,iBAAkB,YAAW,aAAa;IACtD,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;IACpC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAqB;IAE5C,QAAQ,CAAC,IAAI,WAAW;IACxB,QAAQ,CAAC,YAAY,EAAE,aAAa,CAAC;gBAEzB,MAAM,EAAE,uBAAuB;IAU3C,MAAM,CAAC,OAAO,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,GAAG,iBAAiB;IAS1E,oEAAoE;IAC9D,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC;IAUpD,cAAc,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAKzC,OAAO,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC;IAM7C,SAAS,CAAC,IAAI,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC,GAAG,IAAI,CAAC;IAM7D,UAAU,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,oBAAoB,CAAC;IAiBrD,WAAW,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,oBAAoB,CAAC;CAc5D"}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import * as path from 'node:path';
|
|
2
|
+
import { ProviderError } from '@selvajs/platform';
|
|
3
|
+
import { signHmacToken, verifyHmacToken } from './hmac.js';
|
|
4
|
+
import { verifyPasswordHash, createLocalAuthUserStore } from './users.js';
|
|
5
|
+
import { paginate } from '../data/pagination.js';
|
|
6
|
+
const SESSION_MAX_AGE_MS = 8 * 60 * 60 * 1000; // 8 hours
|
|
7
|
+
function toAuthUser(u) {
|
|
8
|
+
return {
|
|
9
|
+
id: u.id,
|
|
10
|
+
email: u.email,
|
|
11
|
+
createdAt: u.createdAt,
|
|
12
|
+
lastLoginAt: u.lastLoginAt,
|
|
13
|
+
disabled: u.disabled
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
class LocalPasswordAuth {
|
|
17
|
+
users;
|
|
18
|
+
mintToken;
|
|
19
|
+
constructor(users, mintToken) {
|
|
20
|
+
this.users = users;
|
|
21
|
+
this.mintToken = mintToken;
|
|
22
|
+
}
|
|
23
|
+
async verifyLogin(email, password) {
|
|
24
|
+
if (!this.users)
|
|
25
|
+
return { kind: 'failed', reason: 'invalid_credentials' };
|
|
26
|
+
const user = await this.users.findByEmail(email);
|
|
27
|
+
if (!user)
|
|
28
|
+
return { kind: 'failed', reason: 'invalid_credentials' };
|
|
29
|
+
if (user.disabled)
|
|
30
|
+
return { kind: 'failed', reason: 'disabled' };
|
|
31
|
+
if (!user.passwordHash || !(await verifyPasswordHash(password, user.passwordHash))) {
|
|
32
|
+
return { kind: 'failed', reason: 'invalid_credentials' };
|
|
33
|
+
}
|
|
34
|
+
await this.users.touchLastLogin(user.id).catch(() => { });
|
|
35
|
+
const auth = toAuthUser(user);
|
|
36
|
+
return { kind: 'success', user: auth, sessionToken: this.mintToken(auth) };
|
|
37
|
+
}
|
|
38
|
+
async createUserWithPassword(email, password) {
|
|
39
|
+
if (!this.users) {
|
|
40
|
+
throw new ProviderError('createUserWithPassword requires a users.json backend (DATA_PATH)', 500);
|
|
41
|
+
}
|
|
42
|
+
// Identity-only — platform permissions and the user-data row are seeded
|
|
43
|
+
// separately via `IDataProvider.ensureUser` + `IPlatformPermissionStore.set`.
|
|
44
|
+
return toAuthUser(await this.users.createUser(email, password));
|
|
45
|
+
}
|
|
46
|
+
async registerUser(email, password) {
|
|
47
|
+
if (!this.users)
|
|
48
|
+
return null;
|
|
49
|
+
return toAuthUser(await this.users.createUser(email, password));
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
export class LocalAuthProvider {
|
|
53
|
+
hmacSecret;
|
|
54
|
+
users;
|
|
55
|
+
name = 'Local';
|
|
56
|
+
passwordAuth;
|
|
57
|
+
constructor(config) {
|
|
58
|
+
this.hmacSecret = config.hmacSecret;
|
|
59
|
+
if (config.usersFilePath) {
|
|
60
|
+
this.users = createLocalAuthUserStore(config.usersFilePath);
|
|
61
|
+
}
|
|
62
|
+
this.passwordAuth = new LocalPasswordAuth(this.users, (user) => signHmacToken(this.hmacSecret, user.id, SESSION_MAX_AGE_MS));
|
|
63
|
+
}
|
|
64
|
+
static fromEnv(env) {
|
|
65
|
+
const hmacSecret = env.SELVA_HMAC_KEY;
|
|
66
|
+
if (!hmacSecret)
|
|
67
|
+
throw new Error('Missing required env var: SELVA_HMAC_KEY');
|
|
68
|
+
return new LocalAuthProvider({
|
|
69
|
+
hmacSecret,
|
|
70
|
+
usersFilePath: env.DATA_PATH ? path.join(env.DATA_PATH, 'auth-users.json') : undefined
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
/** Verify an HMAC session token and return the live user record. */
|
|
74
|
+
async verifyToken(token) {
|
|
75
|
+
const { valid, userId } = verifyHmacToken(token, this.hmacSecret);
|
|
76
|
+
if (!valid)
|
|
77
|
+
return null;
|
|
78
|
+
if (!this.users)
|
|
79
|
+
return null;
|
|
80
|
+
const u = await this.users.findById(userId);
|
|
81
|
+
if (!u || u.disabled)
|
|
82
|
+
return null;
|
|
83
|
+
await this.users.touchLastLogin(u.id).catch(() => { });
|
|
84
|
+
return toAuthUser(u);
|
|
85
|
+
}
|
|
86
|
+
async touchLastLogin(id) {
|
|
87
|
+
if (!this.users)
|
|
88
|
+
return;
|
|
89
|
+
await this.users.touchLastLogin(id);
|
|
90
|
+
}
|
|
91
|
+
async getUser(id) {
|
|
92
|
+
if (!this.users)
|
|
93
|
+
return null;
|
|
94
|
+
const u = await this.users.findById(id);
|
|
95
|
+
return u ? toAuthUser(u) : null;
|
|
96
|
+
}
|
|
97
|
+
async listUsers(opts) {
|
|
98
|
+
if (!this.users)
|
|
99
|
+
return null;
|
|
100
|
+
const users = await this.users.listUsers();
|
|
101
|
+
return paginate(users.map(toAuthUser), opts);
|
|
102
|
+
}
|
|
103
|
+
async deleteUser(id) {
|
|
104
|
+
// Identity-only delete. The §2 sole-`instance_admin` invariant is
|
|
105
|
+
// enforced by the caller via `IPlatformPermissionStore.countInstanceAdminsExcluding`
|
|
106
|
+
// before this method is called. The caller is also responsible for
|
|
107
|
+
// removing the user-data row via `LocalDataProvider`'s cascade hook.
|
|
108
|
+
if (!this.users)
|
|
109
|
+
return 'not_supported';
|
|
110
|
+
const target = await this.users.findById(id);
|
|
111
|
+
if (!target)
|
|
112
|
+
return 'not_found';
|
|
113
|
+
try {
|
|
114
|
+
await this.users.deleteUser(id);
|
|
115
|
+
return 'ok';
|
|
116
|
+
}
|
|
117
|
+
catch (err) {
|
|
118
|
+
if (err instanceof ProviderError && err.statusCode === 404)
|
|
119
|
+
return 'not_found';
|
|
120
|
+
throw err;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
async disableUser(id) {
|
|
124
|
+
// Identity-only disable. The §2 sole-`instance_admin` invariant is
|
|
125
|
+
// enforced by the caller, same as `deleteUser`.
|
|
126
|
+
if (!this.users)
|
|
127
|
+
return 'not_supported';
|
|
128
|
+
const target = await this.users.findById(id);
|
|
129
|
+
if (!target)
|
|
130
|
+
return 'not_found';
|
|
131
|
+
try {
|
|
132
|
+
await this.users.setDisabled(id, true);
|
|
133
|
+
return 'ok';
|
|
134
|
+
}
|
|
135
|
+
catch (err) {
|
|
136
|
+
if (err instanceof ProviderError && err.statusCode === 404)
|
|
137
|
+
return 'not_found';
|
|
138
|
+
throw err;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
//# sourceMappingURL=LocalAuthProvider.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"LocalAuthProvider.js","sourceRoot":"","sources":["../../src/auth/LocalAuthProvider.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,IAAI,MAAM,WAAW,CAAC;AAUlC,OAAO,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAClD,OAAO,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,WAAW,CAAC;AAC3D,OAAO,EAAE,kBAAkB,EAAE,wBAAwB,EAAE,MAAM,YAAY,CAAC;AAE1E,OAAO,EAAE,QAAQ,EAAE,MAAM,uBAAuB,CAAC;AAEjD,MAAM,kBAAkB,GAAG,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC,CAAC,UAAU;AAEzD,SAAS,UAAU,CAClB,CAAkF;IAElF,OAAO;QACN,EAAE,EAAE,CAAC,CAAC,EAAE;QACR,KAAK,EAAE,CAAC,CAAC,KAAK;QACd,SAAS,EAAE,CAAC,CAAC,SAAS;QACtB,WAAW,EAAE,CAAC,CAAC,WAAW;QAC1B,QAAQ,EAAE,CAAC,CAAC,QAAQ;KACpB,CAAC;AACH,CAAC;AAcD,MAAM,iBAAiB;IAEJ;IACA;IAFlB,YACkB,KAAqC,EACrC,SAAqC;QADrC,UAAK,GAAL,KAAK,CAAgC;QACrC,cAAS,GAAT,SAAS,CAA4B;IACpD,CAAC;IAEJ,KAAK,CAAC,WAAW,CAAC,KAAa,EAAE,QAAgB;QAChD,IAAI,CAAC,IAAI,CAAC,KAAK;YAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,qBAAqB,EAAE,CAAC;QAC1E,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,KAAK,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC;QACjD,IAAI,CAAC,IAAI;YAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,qBAAqB,EAAE,CAAC;QACpE,IAAI,IAAI,CAAC,QAAQ;YAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,UAAU,EAAE,CAAC;QACjE,IAAI,CAAC,IAAI,CAAC,YAAY,IAAI,CAAC,CAAC,MAAM,kBAAkB,CAAC,QAAQ,EAAE,IAAI,CAAC,YAAY,CAAC,CAAC,EAAE,CAAC;YACpF,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,qBAAqB,EAAE,CAAC;QAC1D,CAAC;QACD,MAAM,IAAI,CAAC,KAAK,CAAC,cAAc,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;QACzD,MAAM,IAAI,GAAG,UAAU,CAAC,IAAI,CAAC,CAAC;QAC9B,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,IAAI,EAAE,YAAY,EAAE,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,EAAE,CAAC;IAC5E,CAAC;IAED,KAAK,CAAC,sBAAsB,CAAC,KAAa,EAAE,QAAgB;QAC3D,IAAI,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC;YACjB,MAAM,IAAI,aAAa,CACtB,kEAAkE,EAClE,GAAG,CACH,CAAC;QACH,CAAC;QACD,wEAAwE;QACxE,8EAA8E;QAC9E,OAAO,UAAU,CAAC,MAAM,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,KAAK,EAAE,QAAQ,CAAC,CAAC,CAAC;IACjE,CAAC;IAED,KAAK,CAAC,YAAY,CAAC,KAAa,EAAE,QAAgB;QACjD,IAAI,CAAC,IAAI,CAAC,KAAK;YAAE,OAAO,IAAI,CAAC;QAC7B,OAAO,UAAU,CAAC,MAAM,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,KAAK,EAAE,QAAQ,CAAC,CAAC,CAAC;IACjE,CAAC;CACD;AAED,MAAM,OAAO,iBAAiB;IACZ,UAAU,CAAS;IACnB,KAAK,CAAsB;IAEnC,IAAI,GAAG,OAAO,CAAC;IACf,YAAY,CAAgB;IAErC,YAAY,MAA+B;QAC1C,IAAI,CAAC,UAAU,GAAG,MAAM,CAAC,UAAU,CAAC;QACpC,IAAI,MAAM,CAAC,aAAa,EAAE,CAAC;YAC1B,IAAI,CAAC,KAAK,GAAG,wBAAwB,CAAC,MAAM,CAAC,aAAa,CAAC,CAAC;QAC7D,CAAC;QACD,IAAI,CAAC,YAAY,GAAG,IAAI,iBAAiB,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC,IAAI,EAAE,EAAE,CAC9D,aAAa,CAAC,IAAI,CAAC,UAAU,EAAE,IAAI,CAAC,EAAE,EAAE,kBAAkB,CAAC,CAC3D,CAAC;IACH,CAAC;IAED,MAAM,CAAC,OAAO,CAAC,GAAuC;QACrD,MAAM,UAAU,GAAG,GAAG,CAAC,cAAc,CAAC;QACtC,IAAI,CAAC,UAAU;YAAE,MAAM,IAAI,KAAK,CAAC,0CAA0C,CAAC,CAAC;QAC7E,OAAO,IAAI,iBAAiB,CAAC;YAC5B,UAAU;YACV,aAAa,EAAE,GAAG,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,SAAS,EAAE,iBAAiB,CAAC,CAAC,CAAC,CAAC,SAAS;SACtF,CAAC,CAAC;IACJ,CAAC;IAED,oEAAoE;IACpE,KAAK,CAAC,WAAW,CAAC,KAAa;QAC9B,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,eAAe,CAAC,KAAK,EAAE,IAAI,CAAC,UAAU,CAAC,CAAC;QAClE,IAAI,CAAC,KAAK;YAAE,OAAO,IAAI,CAAC;QACxB,IAAI,CAAC,IAAI,CAAC,KAAK;YAAE,OAAO,IAAI,CAAC;QAC7B,MAAM,CAAC,GAAG,MAAM,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;QAC5C,IAAI,CAAC,CAAC,IAAI,CAAC,CAAC,QAAQ;YAAE,OAAO,IAAI,CAAC;QAClC,MAAM,IAAI,CAAC,KAAK,CAAC,cAAc,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;QACtD,OAAO,UAAU,CAAC,CAAC,CAAC,CAAC;IACtB,CAAC;IAED,KAAK,CAAC,cAAc,CAAC,EAAU;QAC9B,IAAI,CAAC,IAAI,CAAC,KAAK;YAAE,OAAO;QACxB,MAAM,IAAI,CAAC,KAAK,CAAC,cAAc,CAAC,EAAE,CAAC,CAAC;IACrC,CAAC;IAED,KAAK,CAAC,OAAO,CAAC,EAAU;QACvB,IAAI,CAAC,IAAI,CAAC,KAAK;YAAE,OAAO,IAAI,CAAC;QAC7B,MAAM,CAAC,GAAG,MAAM,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC;QACxC,OAAO,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;IACjC,CAAC;IAED,KAAK,CAAC,SAAS,CAAC,IAAkB;QACjC,IAAI,CAAC,IAAI,CAAC,KAAK;YAAE,OAAO,IAAI,CAAC;QAC7B,MAAM,KAAK,GAAG,MAAM,IAAI,CAAC,KAAK,CAAC,SAAS,EAAE,CAAC;QAC3C,OAAO,QAAQ,CAAC,KAAK,CAAC,GAAG,CAAC,UAAU,CAAC,EAAE,IAAI,CAAC,CAAC;IAC9C,CAAC;IAED,KAAK,CAAC,UAAU,CAAC,EAAU;QAC1B,kEAAkE;QAClE,qFAAqF;QACrF,mEAAmE;QACnE,qEAAqE;QACrE,IAAI,CAAC,IAAI,CAAC,KAAK;YAAE,OAAO,eAAe,CAAC;QACxC,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC;QAC7C,IAAI,CAAC,MAAM;YAAE,OAAO,WAAW,CAAC;QAChC,IAAI,CAAC;YACJ,MAAM,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,EAAE,CAAC,CAAC;YAChC,OAAO,IAAI,CAAC;QACb,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACd,IAAI,GAAG,YAAY,aAAa,IAAI,GAAG,CAAC,UAAU,KAAK,GAAG;gBAAE,OAAO,WAAW,CAAC;YAC/E,MAAM,GAAG,CAAC;QACX,CAAC;IACF,CAAC;IAED,KAAK,CAAC,WAAW,CAAC,EAAU;QAC3B,mEAAmE;QACnE,gDAAgD;QAChD,IAAI,CAAC,IAAI,CAAC,KAAK;YAAE,OAAO,eAAe,CAAC;QACxC,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC;QAC7C,IAAI,CAAC,MAAM;YAAE,OAAO,WAAW,CAAC;QAChC,IAAI,CAAC;YACJ,MAAM,IAAI,CAAC,KAAK,CAAC,WAAW,CAAC,EAAE,EAAE,IAAI,CAAC,CAAC;YACvC,OAAO,IAAI,CAAC;QACb,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACd,IAAI,GAAG,YAAY,aAAa,IAAI,GAAG,CAAC,UAAU,KAAK,GAAG;gBAAE,OAAO,WAAW,CAAC;YAC/E,MAAM,GAAG,CAAC;QACX,CAAC;IACF,CAAC;CACD"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"conformance.test.d.ts","sourceRoot":"","sources":["../../../src/auth/__tests__/conformance.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { describe, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import * as fs from 'node:fs/promises';
|
|
3
|
+
import * as path from 'node:path';
|
|
4
|
+
import * as os from 'node:os';
|
|
5
|
+
import { runAuthProviderConformance } from '@selvajs/platform/testing';
|
|
6
|
+
import { LocalAuthProvider } from '../LocalAuthProvider.js';
|
|
7
|
+
const TEST_SECRET = 'test-hmac-secret-for-conformance';
|
|
8
|
+
const ADMIN_EMAIL = 'conformance-admin@example.com';
|
|
9
|
+
const ADMIN_PASSWORD = 'test-admin-password';
|
|
10
|
+
describe('LocalAuthProvider — auth-users.json mode', () => {
|
|
11
|
+
let tempDir;
|
|
12
|
+
beforeEach(async () => {
|
|
13
|
+
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'selva-auth-test-'));
|
|
14
|
+
});
|
|
15
|
+
afterEach(async () => {
|
|
16
|
+
await fs.rm(tempDir, { recursive: true, force: true });
|
|
17
|
+
});
|
|
18
|
+
runAuthProviderConformance({
|
|
19
|
+
name: 'LocalAuthProvider/auth-users-json',
|
|
20
|
+
createProvider: async () => {
|
|
21
|
+
const provider = new LocalAuthProvider({
|
|
22
|
+
hmacSecret: TEST_SECRET,
|
|
23
|
+
usersFilePath: path.join(tempDir, 'auth-users.json')
|
|
24
|
+
});
|
|
25
|
+
// Idempotent seed — the conformance suite calls createProvider per test,
|
|
26
|
+
// but a single test may invoke it more than once.
|
|
27
|
+
const existing = await provider.passwordAuth.verifyLogin(ADMIN_EMAIL, ADMIN_PASSWORD);
|
|
28
|
+
if (existing.kind !== 'success') {
|
|
29
|
+
await provider.passwordAuth.createUserWithPassword(ADMIN_EMAIL, ADMIN_PASSWORD);
|
|
30
|
+
}
|
|
31
|
+
return { provider, adminEmail: ADMIN_EMAIL, adminPassword: ADMIN_PASSWORD };
|
|
32
|
+
},
|
|
33
|
+
userManagement: true
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
//# sourceMappingURL=conformance.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"conformance.test.js","sourceRoot":"","sources":["../../../src/auth/__tests__/conformance.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,QAAQ,CAAC;AACzD,OAAO,KAAK,EAAE,MAAM,kBAAkB,CAAC;AACvC,OAAO,KAAK,IAAI,MAAM,WAAW,CAAC;AAClC,OAAO,KAAK,EAAE,MAAM,SAAS,CAAC;AAC9B,OAAO,EAAE,0BAA0B,EAAE,MAAM,2BAA2B,CAAC;AACvE,OAAO,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AAE5D,MAAM,WAAW,GAAG,kCAAkC,CAAC;AACvD,MAAM,WAAW,GAAG,+BAA+B,CAAC;AACpD,MAAM,cAAc,GAAG,qBAAqB,CAAC;AAE7C,QAAQ,CAAC,0CAA0C,EAAE,GAAG,EAAE;IACzD,IAAI,OAAe,CAAC;IAEpB,UAAU,CAAC,KAAK,IAAI,EAAE;QACrB,OAAO,GAAG,MAAM,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,MAAM,EAAE,EAAE,kBAAkB,CAAC,CAAC,CAAC;IACxE,CAAC,CAAC,CAAC;IAEH,SAAS,CAAC,KAAK,IAAI,EAAE;QACpB,MAAM,EAAE,CAAC,EAAE,CAAC,OAAO,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;IACxD,CAAC,CAAC,CAAC;IAEH,0BAA0B,CAAC;QAC1B,IAAI,EAAE,mCAAmC;QACzC,cAAc,EAAE,KAAK,IAAI,EAAE;YAC1B,MAAM,QAAQ,GAAG,IAAI,iBAAiB,CAAC;gBACtC,UAAU,EAAE,WAAW;gBACvB,aAAa,EAAE,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,iBAAiB,CAAC;aACpD,CAAC,CAAC;YACH,yEAAyE;YACzE,kDAAkD;YAClD,MAAM,QAAQ,GAAG,MAAM,QAAQ,CAAC,YAAY,CAAC,WAAW,CAAC,WAAW,EAAE,cAAc,CAAC,CAAC;YACtF,IAAI,QAAQ,CAAC,IAAI,KAAK,SAAS,EAAE,CAAC;gBACjC,MAAM,QAAQ,CAAC,YAAY,CAAC,sBAAsB,CAAC,WAAW,EAAE,cAAc,CAAC,CAAC;YACjF,CAAC;YACD,OAAO,EAAE,QAAQ,EAAE,UAAU,EAAE,WAAW,EAAE,aAAa,EAAE,cAAc,EAAE,CAAC;QAC7E,CAAC;QACD,cAAc,EAAE,IAAI;KACpB,CAAC,CAAC;AACJ,CAAC,CAAC,CAAC"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Token format: base64url(userId + ':' + expiry) + '.' + hmac(payload)
|
|
3
|
+
*
|
|
4
|
+
* The userId is embedded so verifyToken can look up the live user record
|
|
5
|
+
* (and therefore always reflect the current permissions, not stale ones
|
|
6
|
+
* baked into the token at login time).
|
|
7
|
+
*/
|
|
8
|
+
export declare function signHmacToken(secret: string, userId: string, maxAgeMs?: number): string;
|
|
9
|
+
export interface HmacTokenPayload {
|
|
10
|
+
userId: string;
|
|
11
|
+
valid: boolean;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Verify an HMAC token. Returns the userId if valid and unexpired, or
|
|
15
|
+
* { valid: false } if the signature is wrong or the token has expired.
|
|
16
|
+
*/
|
|
17
|
+
export declare function verifyHmacToken(token: string, secret: string): HmacTokenPayload;
|
|
18
|
+
//# sourceMappingURL=hmac.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hmac.d.ts","sourceRoot":"","sources":["../../src/auth/hmac.ts"],"names":[],"mappings":"AAIA;;;;;;GAMG;AACH,wBAAgB,aAAa,CAC5B,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,MAAM,EACd,QAAQ,SAAqB,GAC3B,MAAM,CAKR;AAED,MAAM,WAAW,gBAAgB;IAChC,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,OAAO,CAAC;CACf;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,gBAAgB,CAqB/E"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { timingSafeEqual, createHmac } from 'node:crypto';
|
|
2
|
+
const DEFAULT_MAX_AGE_MS = 8 * 60 * 60 * 1000; // 8 hours
|
|
3
|
+
/**
|
|
4
|
+
* Token format: base64url(userId + ':' + expiry) + '.' + hmac(payload)
|
|
5
|
+
*
|
|
6
|
+
* The userId is embedded so verifyToken can look up the live user record
|
|
7
|
+
* (and therefore always reflect the current permissions, not stale ones
|
|
8
|
+
* baked into the token at login time).
|
|
9
|
+
*/
|
|
10
|
+
export function signHmacToken(secret, userId, maxAgeMs = DEFAULT_MAX_AGE_MS) {
|
|
11
|
+
const expiry = Date.now() + maxAgeMs;
|
|
12
|
+
const payload = Buffer.from(`${userId}:${expiry}`).toString('base64url');
|
|
13
|
+
const sig = createHmac('sha256', secret).update(payload).digest('base64url');
|
|
14
|
+
return `${payload}.${sig}`;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Verify an HMAC token. Returns the userId if valid and unexpired, or
|
|
18
|
+
* { valid: false } if the signature is wrong or the token has expired.
|
|
19
|
+
*/
|
|
20
|
+
export function verifyHmacToken(token, secret) {
|
|
21
|
+
const dot = token.lastIndexOf('.');
|
|
22
|
+
if (dot === -1)
|
|
23
|
+
return { userId: '', valid: false };
|
|
24
|
+
const payload = token.slice(0, dot);
|
|
25
|
+
const sig = token.slice(dot + 1);
|
|
26
|
+
const expectedSig = createHmac('sha256', secret).update(payload).digest('base64url');
|
|
27
|
+
const a = Buffer.from(sig);
|
|
28
|
+
const b = Buffer.from(expectedSig);
|
|
29
|
+
if (a.length !== b.length || !timingSafeEqual(a, b))
|
|
30
|
+
return { userId: '', valid: false };
|
|
31
|
+
const decoded = Buffer.from(payload, 'base64url').toString();
|
|
32
|
+
const colon = decoded.lastIndexOf(':');
|
|
33
|
+
if (colon === -1)
|
|
34
|
+
return { userId: '', valid: false };
|
|
35
|
+
const userId = decoded.slice(0, colon);
|
|
36
|
+
const expiry = parseInt(decoded.slice(colon + 1), 10);
|
|
37
|
+
if (!Number.isFinite(expiry) || Date.now() >= expiry)
|
|
38
|
+
return { userId: '', valid: false };
|
|
39
|
+
return { userId, valid: true };
|
|
40
|
+
}
|
|
41
|
+
//# sourceMappingURL=hmac.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hmac.js","sourceRoot":"","sources":["../../src/auth/hmac.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAE1D,MAAM,kBAAkB,GAAG,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC,CAAC,UAAU;AAEzD;;;;;;GAMG;AACH,MAAM,UAAU,aAAa,CAC5B,MAAc,EACd,MAAc,EACd,QAAQ,GAAG,kBAAkB;IAE7B,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,QAAQ,CAAC;IACrC,MAAM,OAAO,GAAG,MAAM,CAAC,IAAI,CAAC,GAAG,MAAM,IAAI,MAAM,EAAE,CAAC,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC;IACzE,MAAM,GAAG,GAAG,UAAU,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC;IAC7E,OAAO,GAAG,OAAO,IAAI,GAAG,EAAE,CAAC;AAC5B,CAAC;AAOD;;;GAGG;AACH,MAAM,UAAU,eAAe,CAAC,KAAa,EAAE,MAAc;IAC5D,MAAM,GAAG,GAAG,KAAK,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC;IACnC,IAAI,GAAG,KAAK,CAAC,CAAC;QAAE,OAAO,EAAE,MAAM,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC;IAEpD,MAAM,OAAO,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;IACpC,MAAM,GAAG,GAAG,KAAK,CAAC,KAAK,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC;IACjC,MAAM,WAAW,GAAG,UAAU,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC;IAErF,MAAM,CAAC,GAAG,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IAC3B,MAAM,CAAC,GAAG,MAAM,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC;IACnC,IAAI,CAAC,CAAC,MAAM,KAAK,CAAC,CAAC,MAAM,IAAI,CAAC,eAAe,CAAC,CAAC,EAAE,CAAC,CAAC;QAAE,OAAO,EAAE,MAAM,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC;IAEzF,MAAM,OAAO,GAAG,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,WAAW,CAAC,CAAC,QAAQ,EAAE,CAAC;IAC7D,MAAM,KAAK,GAAG,OAAO,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC;IACvC,IAAI,KAAK,KAAK,CAAC,CAAC;QAAE,OAAO,EAAE,MAAM,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC;IAEtD,MAAM,MAAM,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC;IACvC,MAAM,MAAM,GAAG,QAAQ,CAAC,OAAO,CAAC,KAAK,CAAC,KAAK,GAAG,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;IACtD,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,IAAI,IAAI,CAAC,GAAG,EAAE,IAAI,MAAM;QAAE,OAAO,EAAE,MAAM,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC;IAE1F,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC;AAChC,CAAC"}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { LocalAuthProvider } from './LocalAuthProvider.js';
|
|
2
|
+
export type { LocalAuthProviderConfig } from './LocalAuthProvider.js';
|
|
3
|
+
export { signHmacToken, verifyHmacToken } from './hmac.js';
|
|
4
|
+
export { hashPassword, verifyPasswordHash, createLocalAuthUserStore } from './users.js';
|
|
5
|
+
export type { StoredAuthUser, AuthUsersFile, LocalAuthUserStore } from './users.js';
|
|
6
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/auth/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAC3D,YAAY,EAAE,uBAAuB,EAAE,MAAM,wBAAwB,CAAC;AACtE,OAAO,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,WAAW,CAAC;AAC3D,OAAO,EAAE,YAAY,EAAE,kBAAkB,EAAE,wBAAwB,EAAE,MAAM,YAAY,CAAC;AACxF,YAAY,EAAE,cAAc,EAAE,aAAa,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/auth/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAE3D,OAAO,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,WAAW,CAAC;AAC3D,OAAO,EAAE,YAAY,EAAE,kBAAkB,EAAE,wBAAwB,EAAE,MAAM,YAAY,CAAC"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Identity-only on-disk shape. Per-user app state (permissions, profile,
|
|
3
|
+
* starred definitions, recent runs) lives in `user-data.json`, owned by
|
|
4
|
+
* `LocalDataProvider`. This split lets `LocalAuthProvider` be paired with any
|
|
5
|
+
* data provider, and any auth provider be paired with `LocalDataProvider`,
|
|
6
|
+
* without one stepping on the other.
|
|
7
|
+
*/
|
|
8
|
+
export interface StoredAuthUser {
|
|
9
|
+
id: string;
|
|
10
|
+
email: string;
|
|
11
|
+
/**
|
|
12
|
+
* "pbkdf2:sha256:<iterations>:<salt>:<hash>" — all binary values base64url encoded.
|
|
13
|
+
* Null for OAuth-only users (allowlisted email, no password stored).
|
|
14
|
+
*/
|
|
15
|
+
passwordHash: string | null;
|
|
16
|
+
createdAt: string;
|
|
17
|
+
/** ISO 8601 — most recent successful credential login or token verification. */
|
|
18
|
+
lastLoginAt?: string;
|
|
19
|
+
/** When true, the provider MUST refuse to authenticate this user. */
|
|
20
|
+
disabled?: boolean;
|
|
21
|
+
}
|
|
22
|
+
export interface AuthUsersFile {
|
|
23
|
+
users: StoredAuthUser[];
|
|
24
|
+
}
|
|
25
|
+
export declare function hashPassword(password: string): Promise<string>;
|
|
26
|
+
export declare function verifyPasswordHash(password: string, storedHash: string): Promise<boolean>;
|
|
27
|
+
export interface LocalAuthUserStore {
|
|
28
|
+
findByEmail(email: string): Promise<StoredAuthUser | null>;
|
|
29
|
+
findById(id: string): Promise<StoredAuthUser | null>;
|
|
30
|
+
listUsers(): Promise<Omit<StoredAuthUser, 'passwordHash'>[]>;
|
|
31
|
+
/** password null = OAuth allowlist entry (no password stored) */
|
|
32
|
+
createUser(email: string, password: string | null): Promise<StoredAuthUser>;
|
|
33
|
+
setDisabled(id: string, disabled: boolean): Promise<void>;
|
|
34
|
+
touchLastLogin(id: string): Promise<void>;
|
|
35
|
+
deleteUser(id: string): Promise<void>;
|
|
36
|
+
}
|
|
37
|
+
export declare function createLocalAuthUserStore(usersFilePath: string): LocalAuthUserStore;
|
|
38
|
+
//# sourceMappingURL=users.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"users.d.ts","sourceRoot":"","sources":["../../src/auth/users.ts"],"names":[],"mappings":"AAKA;;;;;;GAMG;AACH,MAAM,WAAW,cAAc;IAC9B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd;;;OAGG;IACH,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,SAAS,EAAE,MAAM,CAAC;IAClB,gFAAgF;IAChF,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,qEAAqE;IACrE,QAAQ,CAAC,EAAE,OAAO,CAAC;CACnB;AAKD,MAAM,WAAW,aAAa;IAC7B,KAAK,EAAE,cAAc,EAAE,CAAC;CACxB;AASD,wBAAsB,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAQpE;AAED,wBAAsB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAgB/F;AAUD,MAAM,WAAW,kBAAkB;IAClC,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,GAAG,IAAI,CAAC,CAAC;IAC3D,QAAQ,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,GAAG,IAAI,CAAC,CAAC;IACrD,SAAS,IAAI,OAAO,CAAC,IAAI,CAAC,cAAc,EAAE,cAAc,CAAC,EAAE,CAAC,CAAC;IAC7D,iEAAiE;IACjE,UAAU,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAAC,cAAc,CAAC,CAAC;IAC5E,WAAW,CAAC,EAAE,EAAE,MAAM,EAAE,QAAQ,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1D,cAAc,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1C,UAAU,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACtC;AAED,wBAAgB,wBAAwB,CAAC,aAAa,EAAE,MAAM,GAAG,kBAAkB,CA8DlF"}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import * as crypto from 'node:crypto';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import { ProviderError } from '@selvajs/platform';
|
|
4
|
+
import { readJsonFile, writeJsonFile } from '../data/fsJson.js';
|
|
5
|
+
/** Debounce window for lastLoginAt writes — skip if prior stamp is newer than this. */
|
|
6
|
+
const LAST_LOGIN_DEBOUNCE_MS = 60_000;
|
|
7
|
+
// ============================================================================
|
|
8
|
+
// PBKDF2 password hashing
|
|
9
|
+
// ============================================================================
|
|
10
|
+
const PBKDF2_ITERATIONS = 100_000;
|
|
11
|
+
const PBKDF2_KEYLEN = 32;
|
|
12
|
+
const PBKDF2_DIGEST = 'sha256';
|
|
13
|
+
export async function hashPassword(password) {
|
|
14
|
+
const salt = crypto.randomBytes(16).toString('base64url');
|
|
15
|
+
const hash = await new Promise((resolve, reject) => crypto.pbkdf2(password, salt, PBKDF2_ITERATIONS, PBKDF2_KEYLEN, PBKDF2_DIGEST, (err, key) => err ? reject(err) : resolve(key)));
|
|
16
|
+
return `pbkdf2:${PBKDF2_DIGEST}:${PBKDF2_ITERATIONS}:${salt}:${hash.toString('base64url')}`;
|
|
17
|
+
}
|
|
18
|
+
export async function verifyPasswordHash(password, storedHash) {
|
|
19
|
+
const parts = storedHash.split(':');
|
|
20
|
+
if (parts.length !== 5 || parts[0] !== 'pbkdf2')
|
|
21
|
+
return false;
|
|
22
|
+
const [, digest, iterStr, salt, expectedHashB64] = parts;
|
|
23
|
+
const iterations = parseInt(iterStr, 10);
|
|
24
|
+
if (!Number.isFinite(iterations) || iterations <= 0)
|
|
25
|
+
return false;
|
|
26
|
+
const expected = Buffer.from(expectedHashB64, 'base64url');
|
|
27
|
+
const actual = await new Promise((resolve, reject) => crypto.pbkdf2(password, salt, iterations, PBKDF2_KEYLEN, digest, (err, key) => err ? reject(err) : resolve(key)));
|
|
28
|
+
if (actual.length !== expected.length)
|
|
29
|
+
return false;
|
|
30
|
+
return crypto.timingSafeEqual(actual, expected);
|
|
31
|
+
}
|
|
32
|
+
// ============================================================================
|
|
33
|
+
// CRUD
|
|
34
|
+
// ============================================================================
|
|
35
|
+
// Fresh object per call — `readJsonFile` returns its fallback by reference
|
|
36
|
+
// when the file is missing, so a shared singleton would let one test (or
|
|
37
|
+
// one process write) mutate state visible to the next read.
|
|
38
|
+
const empty = () => ({ users: [] });
|
|
39
|
+
export function createLocalAuthUserStore(usersFilePath) {
|
|
40
|
+
return {
|
|
41
|
+
async findByEmail(email) {
|
|
42
|
+
const { users } = await readJsonFile(usersFilePath, empty());
|
|
43
|
+
return users.find((u) => u.email.toLowerCase() === email.toLowerCase()) ?? null;
|
|
44
|
+
},
|
|
45
|
+
async findById(id) {
|
|
46
|
+
const { users } = await readJsonFile(usersFilePath, empty());
|
|
47
|
+
return users.find((u) => u.id === id) ?? null;
|
|
48
|
+
},
|
|
49
|
+
async listUsers() {
|
|
50
|
+
const { users } = await readJsonFile(usersFilePath, empty());
|
|
51
|
+
return users.map(({ passwordHash: _ph, ...rest }) => rest);
|
|
52
|
+
},
|
|
53
|
+
async createUser(email, password) {
|
|
54
|
+
const file = await readJsonFile(usersFilePath, empty());
|
|
55
|
+
if (file.users.some((u) => u.email.toLowerCase() === email.toLowerCase())) {
|
|
56
|
+
throw new ProviderError(`User with email "${email}" already exists`, 409);
|
|
57
|
+
}
|
|
58
|
+
const user = {
|
|
59
|
+
id: randomUUID(),
|
|
60
|
+
email,
|
|
61
|
+
passwordHash: password !== null ? await hashPassword(password) : null,
|
|
62
|
+
createdAt: new Date().toISOString()
|
|
63
|
+
};
|
|
64
|
+
file.users.push(user);
|
|
65
|
+
await writeJsonFile(usersFilePath, file);
|
|
66
|
+
return user;
|
|
67
|
+
},
|
|
68
|
+
async setDisabled(id, disabled) {
|
|
69
|
+
const file = await readJsonFile(usersFilePath, empty());
|
|
70
|
+
const user = file.users.find((u) => u.id === id);
|
|
71
|
+
if (!user)
|
|
72
|
+
throw new ProviderError(`User "${id}" not found`, 404);
|
|
73
|
+
user.disabled = disabled;
|
|
74
|
+
await writeJsonFile(usersFilePath, file);
|
|
75
|
+
},
|
|
76
|
+
async touchLastLogin(id) {
|
|
77
|
+
const file = await readJsonFile(usersFilePath, empty());
|
|
78
|
+
const user = file.users.find((u) => u.id === id);
|
|
79
|
+
if (!user)
|
|
80
|
+
return;
|
|
81
|
+
const now = Date.now();
|
|
82
|
+
if (user.lastLoginAt) {
|
|
83
|
+
const prev = Date.parse(user.lastLoginAt);
|
|
84
|
+
if (Number.isFinite(prev) && now - prev < LAST_LOGIN_DEBOUNCE_MS)
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
user.lastLoginAt = new Date(now).toISOString();
|
|
88
|
+
await writeJsonFile(usersFilePath, file);
|
|
89
|
+
},
|
|
90
|
+
async deleteUser(id) {
|
|
91
|
+
const file = await readJsonFile(usersFilePath, empty());
|
|
92
|
+
const before = file.users.length;
|
|
93
|
+
file.users = file.users.filter((u) => u.id !== id);
|
|
94
|
+
if (file.users.length === before)
|
|
95
|
+
throw new ProviderError(`User "${id}" not found`, 404);
|
|
96
|
+
await writeJsonFile(usersFilePath, file);
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
//# sourceMappingURL=users.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"users.js","sourceRoot":"","sources":["../../src/auth/users.ts"],"names":[],"mappings":"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"}
|