@rebasepro/server-core 0.7.0 → 0.9.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/README.md +2 -2
- package/dist/api/ast-schema-editor.d.ts +21 -0
- package/dist/api/collections_for_test/callbacks_test_collection.d.ts +2 -0
- package/dist/api/errors.d.ts +53 -0
- package/dist/api/index.d.ts +7 -0
- package/dist/api/logs-routes.d.ts +37 -0
- package/dist/api/openapi-generator.d.ts +16 -0
- package/dist/api/rest/api-generator.d.ts +68 -0
- package/{src/api/rest/index.ts → dist/api/rest/index.d.ts} +0 -1
- package/dist/api/rest/query-parser.d.ts +6 -0
- package/dist/api/schema-editor-routes.d.ts +3 -0
- package/dist/api/types.d.ts +56 -0
- package/dist/auth/adapter-middleware.d.ts +43 -0
- package/dist/auth/admin-roles-route.d.ts +18 -0
- package/dist/auth/admin-user-ops.d.ts +79 -0
- package/dist/auth/admin-users-route.d.ts +28 -0
- package/dist/auth/api-keys/api-key-middleware.d.ts +39 -0
- package/{src/auth/api-keys/api-key-permission-guard.ts → dist/auth/api-keys/api-key-permission-guard.d.ts} +2 -34
- package/dist/auth/api-keys/api-key-routes.d.ts +20 -0
- package/dist/auth/api-keys/api-key-store.d.ts +35 -0
- package/{src/auth/api-keys/api-key-types.ts → dist/auth/api-keys/api-key-types.d.ts} +0 -6
- package/{src/auth/api-keys/index.ts → dist/auth/api-keys/index.d.ts} +2 -22
- package/dist/auth/apple-oauth.d.ts +30 -0
- package/{src/auth/auth-hooks.ts → dist/auth/auth-hooks.d.ts} +15 -73
- package/dist/auth/bitbucket-oauth.d.ts +11 -0
- package/dist/auth/builtin-auth-adapter.d.ts +55 -0
- package/dist/auth/cookie-utils.d.ts +32 -0
- package/{src/auth/crypto-utils.ts → dist/auth/crypto-utils.d.ts} +1 -16
- package/dist/auth/custom-auth-adapter.d.ts +39 -0
- package/dist/auth/discord-oauth.d.ts +14 -0
- package/dist/auth/facebook-oauth.d.ts +14 -0
- package/dist/auth/github-oauth.d.ts +15 -0
- package/dist/auth/gitlab-oauth.d.ts +13 -0
- package/dist/auth/google-oauth.d.ts +47 -0
- package/{src/auth/index.ts → dist/auth/index.d.ts} +4 -22
- package/{src/auth/interfaces.ts → dist/auth/interfaces.d.ts} +20 -89
- package/dist/auth/jwt.d.ts +67 -0
- package/dist/auth/linkedin-oauth.d.ts +18 -0
- package/dist/auth/magic-link-routes.d.ts +32 -0
- package/dist/auth/mfa-crypto.d.ts +23 -0
- package/dist/auth/mfa-routes.d.ts +7 -0
- package/dist/auth/mfa.d.ts +49 -0
- package/dist/auth/microsoft-oauth.d.ts +16 -0
- package/dist/auth/middleware.d.ts +158 -0
- package/dist/auth/password.d.ts +22 -0
- package/dist/auth/rate-limiter.d.ts +50 -0
- package/dist/auth/reset-password-admin.d.ts +29 -0
- package/dist/auth/rls-scope.d.ts +31 -0
- package/dist/auth/routes.d.ts +66 -0
- package/dist/auth/session-routes.d.ts +29 -0
- package/dist/auth/slack-oauth.d.ts +12 -0
- package/dist/auth/spotify-oauth.d.ts +12 -0
- package/dist/auth/twitter-oauth.d.ts +18 -0
- package/dist/collections/BackendCollectionRegistry.d.ts +13 -0
- package/dist/collections/loader.d.ts +5 -0
- package/dist/cron/cron-loader.d.ts +18 -0
- package/dist/cron/cron-routes.d.ts +14 -0
- package/dist/cron/cron-scheduler.d.ts +106 -0
- package/dist/cron/cron-store.d.ts +32 -0
- package/dist/cron/define-cron.d.ts +28 -0
- package/{src/cron/index.ts → dist/cron/index.d.ts} +1 -0
- package/dist/db/interfaces.d.ts +18 -0
- package/dist/dynamic-import-Dvh-K5fl.js +22 -0
- package/dist/dynamic-import-Dvh-K5fl.js.map +1 -0
- package/dist/email/index.d.ts +6 -0
- package/dist/email/smtp-email-service.d.ts +30 -0
- package/dist/email/templates.d.ts +50 -0
- package/{src/email/types.ts → dist/email/types.d.ts} +40 -36
- package/dist/env.d.ts +102 -0
- package/dist/functions/define-function.d.ts +55 -0
- package/dist/functions/function-loader.d.ts +18 -0
- package/dist/functions/function-routes.d.ts +10 -0
- package/{src/functions/index.ts → dist/functions/index.d.ts} +2 -0
- package/dist/history/history-routes.d.ts +23 -0
- package/{src/history/index.ts → dist/history/index.d.ts} +0 -1
- package/dist/index.d.ts +35 -0
- package/dist/index.es.js +18184 -0
- package/dist/index.es.js.map +1 -0
- package/dist/index.umd.js +48629 -0
- package/dist/index.umd.js.map +1 -0
- package/dist/init/docs.d.ts +4 -0
- package/dist/init/health.d.ts +2 -0
- package/dist/init/middlewares.d.ts +10 -0
- package/dist/init/shutdown.d.ts +65 -0
- package/dist/init/storage.d.ts +5 -0
- package/dist/init.d.ts +344 -0
- package/dist/jwt-BwIn8xmk.js +4200 -0
- package/dist/jwt-BwIn8xmk.js.map +1 -0
- package/dist/logger-BYU66ENZ.js +94 -0
- package/dist/logger-BYU66ENZ.js.map +1 -0
- package/dist/ms-DnYXB-Wd.js +162 -0
- package/dist/ms-DnYXB-Wd.js.map +1 -0
- package/dist/openapi-generator-Z9oYWLf_.js +586 -0
- package/dist/openapi-generator-Z9oYWLf_.js.map +1 -0
- package/dist/schema-editor-routes-D3ef8zu1.js +219 -0
- package/dist/schema-editor-routes-D3ef8zu1.js.map +1 -0
- package/dist/serve-spa.d.ts +36 -0
- package/dist/services/driver-registry.d.ts +78 -0
- package/dist/services/routed-realtime-service.d.ts +43 -0
- package/dist/services/webhook-service.d.ts +29 -0
- package/dist/singleton.d.ts +51 -0
- package/dist/src-B4OLmNVa.js +437 -0
- package/dist/src-B4OLmNVa.js.map +1 -0
- package/dist/src-B5WkP1Rv.js +24778 -0
- package/dist/src-B5WkP1Rv.js.map +1 -0
- package/dist/storage/GCSStorageController.d.ts +43 -0
- package/dist/storage/LocalStorageController.d.ts +46 -0
- package/dist/storage/S3StorageController.d.ts +40 -0
- package/dist/storage/image-transform.d.ts +50 -0
- package/{src/storage/index.ts → dist/storage/index.d.ts} +6 -21
- package/dist/storage/routes.d.ts +70 -0
- package/dist/storage/storage-registry.d.ts +78 -0
- package/dist/storage/tus-handler.d.ts +53 -0
- package/{src/storage/types.ts → dist/storage/types.d.ts} +32 -38
- package/dist/types/index.d.ts +11 -0
- package/dist/utils/dev-port.d.ts +45 -0
- package/dist/utils/dynamic-import.d.ts +25 -0
- package/dist/utils/logger.d.ts +31 -0
- package/dist/utils/logging.d.ts +9 -0
- package/dist/utils/request-id.d.ts +4 -0
- package/dist/utils/request-logger.d.ts +19 -0
- package/{src/utils/sql.ts → dist/utils/sql.d.ts} +5 -16
- package/package.json +49 -22
- package/coverage/clover.xml +0 -3739
- package/coverage/coverage-final.json +0 -31
- package/coverage/lcov-report/base.css +0 -224
- package/coverage/lcov-report/block-navigation.js +0 -87
- package/coverage/lcov-report/favicon.png +0 -0
- package/coverage/lcov-report/index.html +0 -266
- package/coverage/lcov-report/prettify.css +0 -1
- package/coverage/lcov-report/prettify.js +0 -2
- package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
- package/coverage/lcov-report/sorter.js +0 -210
- package/coverage/lcov-report/src/api/ast-schema-editor.ts.html +0 -952
- package/coverage/lcov-report/src/api/errors.ts.html +0 -472
- package/coverage/lcov-report/src/api/graphql/graphql-schema-generator.ts.html +0 -1069
- package/coverage/lcov-report/src/api/graphql/index.html +0 -116
- package/coverage/lcov-report/src/api/index.html +0 -176
- package/coverage/lcov-report/src/api/openapi-generator.ts.html +0 -565
- package/coverage/lcov-report/src/api/rest/api-generator.ts.html +0 -994
- package/coverage/lcov-report/src/api/rest/index.html +0 -131
- package/coverage/lcov-report/src/api/rest/query-parser.ts.html +0 -550
- package/coverage/lcov-report/src/api/schema-editor-routes.ts.html +0 -202
- package/coverage/lcov-report/src/api/server.ts.html +0 -823
- package/coverage/lcov-report/src/auth/admin-routes.ts.html +0 -973
- package/coverage/lcov-report/src/auth/index.html +0 -176
- package/coverage/lcov-report/src/auth/jwt.ts.html +0 -574
- package/coverage/lcov-report/src/auth/middleware.ts.html +0 -745
- package/coverage/lcov-report/src/auth/password.ts.html +0 -310
- package/coverage/lcov-report/src/auth/services.ts.html +0 -2074
- package/coverage/lcov-report/src/collections/index.html +0 -116
- package/coverage/lcov-report/src/collections/loader.ts.html +0 -232
- package/coverage/lcov-report/src/db/auth-schema.ts.html +0 -523
- package/coverage/lcov-report/src/db/data-transformer.ts.html +0 -1753
- package/coverage/lcov-report/src/db/entityService.ts.html +0 -700
- package/coverage/lcov-report/src/db/index.html +0 -146
- package/coverage/lcov-report/src/db/services/EntityFetchService.ts.html +0 -4048
- package/coverage/lcov-report/src/db/services/EntityPersistService.ts.html +0 -883
- package/coverage/lcov-report/src/db/services/RelationService.ts.html +0 -3121
- package/coverage/lcov-report/src/db/services/entity-helpers.ts.html +0 -442
- package/coverage/lcov-report/src/db/services/index.html +0 -176
- package/coverage/lcov-report/src/db/services/index.ts.html +0 -124
- package/coverage/lcov-report/src/generate-drizzle-schema-logic.ts.html +0 -1960
- package/coverage/lcov-report/src/index.html +0 -116
- package/coverage/lcov-report/src/services/driver-registry.ts.html +0 -631
- package/coverage/lcov-report/src/services/index.html +0 -131
- package/coverage/lcov-report/src/services/postgresDataDriver.ts.html +0 -3025
- package/coverage/lcov-report/src/storage/LocalStorageController.ts.html +0 -1189
- package/coverage/lcov-report/src/storage/S3StorageController.ts.html +0 -970
- package/coverage/lcov-report/src/storage/index.html +0 -161
- package/coverage/lcov-report/src/storage/storage-registry.ts.html +0 -646
- package/coverage/lcov-report/src/storage/types.ts.html +0 -451
- package/coverage/lcov-report/src/utils/drizzle-conditions.ts.html +0 -3082
- package/coverage/lcov-report/src/utils/index.html +0 -116
- package/coverage/lcov.info +0 -7179
- package/jest.config.cjs +0 -19
- package/src/api/ast-schema-editor.ts +0 -289
- package/src/api/collections_for_test/callbacks_test_collection.ts +0 -61
- package/src/api/errors.ts +0 -278
- package/src/api/graphql/graphql-schema-generator.ts +0 -383
- package/src/api/graphql/index.ts +0 -2
- package/src/api/index.ts +0 -11
- package/src/api/logs-routes.ts +0 -132
- package/src/api/openapi-generator.ts +0 -727
- package/src/api/rest/api-generator-count.test.ts +0 -126
- package/src/api/rest/api-generator.ts +0 -747
- package/src/api/rest/query-parser.ts +0 -300
- package/src/api/schema-editor-routes.ts +0 -41
- package/src/api/server.ts +0 -271
- package/src/api/types.ts +0 -104
- package/src/auth/adapter-middleware.ts +0 -114
- package/src/auth/admin-roles-route.ts +0 -36
- package/src/auth/admin-user-ops.ts +0 -236
- package/src/auth/admin-users-route.ts +0 -302
- package/src/auth/api-keys/api-key-middleware.ts +0 -133
- package/src/auth/api-keys/api-key-routes.ts +0 -193
- package/src/auth/api-keys/api-key-store.ts +0 -340
- package/src/auth/apple-oauth.ts +0 -122
- package/src/auth/bitbucket-oauth.ts +0 -84
- package/src/auth/builtin-auth-adapter.ts +0 -380
- package/src/auth/custom-auth-adapter.ts +0 -86
- package/src/auth/discord-oauth.ts +0 -85
- package/src/auth/facebook-oauth.ts +0 -74
- package/src/auth/github-oauth.ts +0 -112
- package/src/auth/gitlab-oauth.ts +0 -72
- package/src/auth/google-oauth.ts +0 -223
- package/src/auth/jwt.ts +0 -208
- package/src/auth/linkedin-oauth.ts +0 -83
- package/src/auth/magic-link-routes.ts +0 -167
- package/src/auth/mfa-crypto.ts +0 -91
- package/src/auth/mfa-routes.ts +0 -323
- package/src/auth/mfa.ts +0 -161
- package/src/auth/microsoft-oauth.ts +0 -90
- package/src/auth/middleware.ts +0 -369
- package/src/auth/password.ts +0 -77
- package/src/auth/rate-limiter.ts +0 -225
- package/src/auth/reset-password-admin.ts +0 -160
- package/src/auth/rls-scope.ts +0 -58
- package/src/auth/routes.ts +0 -802
- package/src/auth/session-routes.ts +0 -353
- package/src/auth/slack-oauth.ts +0 -73
- package/src/auth/spotify-oauth.ts +0 -69
- package/src/auth/twitter-oauth.ts +0 -128
- package/src/collections/BackendCollectionRegistry.ts +0 -20
- package/src/collections/loader.ts +0 -50
- package/src/cron/cron-loader.ts +0 -84
- package/src/cron/cron-routes.test.ts +0 -265
- package/src/cron/cron-routes.ts +0 -85
- package/src/cron/cron-scheduler.test.ts +0 -559
- package/src/cron/cron-scheduler.ts +0 -581
- package/src/cron/cron-store.ts +0 -166
- package/src/db/interfaces.ts +0 -60
- package/src/email/index.ts +0 -19
- package/src/email/smtp-email-service.ts +0 -123
- package/src/email/templates.ts +0 -470
- package/src/env.ts +0 -227
- package/src/functions/function-loader.ts +0 -116
- package/src/functions/function-routes.ts +0 -31
- package/src/history/history-routes.ts +0 -129
- package/src/index.ts +0 -70
- package/src/init/docs.ts +0 -47
- package/src/init/health.ts +0 -37
- package/src/init/middlewares.ts +0 -61
- package/src/init/shutdown.ts +0 -56
- package/src/init/storage.ts +0 -57
- package/src/init.ts +0 -1041
- package/src/serve-spa.ts +0 -89
- package/src/services/driver-registry.ts +0 -183
- package/src/services/routed-realtime-service.ts +0 -113
- package/src/services/webhook-service.ts +0 -155
- package/src/singleton.ts +0 -70
- package/src/storage/LocalStorageController.ts +0 -389
- package/src/storage/S3StorageController.ts +0 -300
- package/src/storage/image-transform.ts +0 -218
- package/src/storage/routes.ts +0 -368
- package/src/storage/storage-registry.ts +0 -188
- package/src/storage/tus-handler.ts +0 -315
- package/src/types/index.ts +0 -27
- package/src/utils/dev-port.ts +0 -196
- package/src/utils/logger.ts +0 -143
- package/src/utils/logging.ts +0 -40
- package/src/utils/request-id.ts +0 -40
- package/src/utils/request-logger.ts +0 -72
- package/test/api-generator.test.ts +0 -588
- package/test/api-key-permission-guard.test.ts +0 -144
- package/test/ast-schema-editor.test.ts +0 -89
- package/test/auth-middleware-hono.test.ts +0 -556
- package/test/auth-routes.test.ts +0 -1098
- package/test/backend-hooks-data.test.ts +0 -477
- package/test/custom-auth-adapter.test.ts +0 -189
- package/test/driver-registry.test.ts +0 -282
- package/test/email-templates.test.ts +0 -174
- package/test/env.test.ts +0 -128
- package/test/error-propagation.test.ts +0 -226
- package/test/errors-hono.test.ts +0 -133
- package/test/errors.test.ts +0 -155
- package/test/function-loader.test.ts +0 -127
- package/test/graphql-schema-generator.test.ts +0 -554
- package/test/jwt-security.test.ts +0 -182
- package/test/jwt.test.ts +0 -326
- package/test/mfa.test.ts +0 -197
- package/test/middleware.test.ts +0 -305
- package/test/multi-datasource-routing.test.ts +0 -113
- package/test/password.test.ts +0 -165
- package/test/query-parser.test.ts +0 -233
- package/test/rate-limiter.test.ts +0 -102
- package/test/reset-password-admin.test.ts +0 -113
- package/test/routed-realtime-service.test.ts +0 -86
- package/test/safe-compare.test.ts +0 -66
- package/test/singleton.test.ts +0 -59
- package/test/smtp-email-service.test.ts +0 -169
- package/test/storage-local.test.ts +0 -271
- package/test/storage-registry.test.ts +0 -282
- package/test/storage-routes.test.ts +0 -222
- package/test/storage-s3.test.ts +0 -304
- package/test/transform-auth-response.test.ts +0 -305
- package/test/webhook-service.test.ts +0 -260
- package/tsconfig.json +0 -54
- package/tsconfig.prod.json +0 -23
- package/vite.config.ts +0 -86
package/src/auth/mfa.ts
DELETED
|
@@ -1,161 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* TOTP (Time-based One-Time Password) implementation.
|
|
3
|
-
*
|
|
4
|
-
* Pure Node.js crypto implementation — no external dependencies required.
|
|
5
|
-
* Implements RFC 6238 (TOTP) and RFC 4226 (HOTP).
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import { createHmac, randomBytes, createHash } from "crypto";
|
|
9
|
-
|
|
10
|
-
// ─── Base32 Encoding/Decoding ────────────────────────────────────────────────
|
|
11
|
-
|
|
12
|
-
const BASE32_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Encode a Buffer to Base32 string (RFC 4648)
|
|
16
|
-
*/
|
|
17
|
-
export function base32Encode(buffer: Buffer): string {
|
|
18
|
-
let bits = 0;
|
|
19
|
-
let value = 0;
|
|
20
|
-
let output = "";
|
|
21
|
-
|
|
22
|
-
for (let i = 0; i < buffer.length; i++) {
|
|
23
|
-
value = (value << 8) | buffer[i];
|
|
24
|
-
bits += 8;
|
|
25
|
-
|
|
26
|
-
while (bits >= 5) {
|
|
27
|
-
output += BASE32_ALPHABET[(value >>> (bits - 5)) & 31];
|
|
28
|
-
bits -= 5;
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
if (bits > 0) {
|
|
33
|
-
output += BASE32_ALPHABET[(value << (5 - bits)) & 31];
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
return output;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* Decode a Base32 string to Buffer
|
|
41
|
-
*/
|
|
42
|
-
export function base32Decode(encoded: string): Buffer {
|
|
43
|
-
const cleanInput = encoded.replace(/=+$/, "").toUpperCase();
|
|
44
|
-
const bytes: number[] = [];
|
|
45
|
-
let bits = 0;
|
|
46
|
-
let value = 0;
|
|
47
|
-
|
|
48
|
-
for (let i = 0; i < cleanInput.length; i++) {
|
|
49
|
-
const index = BASE32_ALPHABET.indexOf(cleanInput[i]);
|
|
50
|
-
if (index === -1) continue;
|
|
51
|
-
|
|
52
|
-
value = (value << 5) | index;
|
|
53
|
-
bits += 5;
|
|
54
|
-
|
|
55
|
-
if (bits >= 8) {
|
|
56
|
-
bytes.push((value >>> (bits - 8)) & 255);
|
|
57
|
-
bits -= 8;
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
return Buffer.from(bytes);
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
// ─── HOTP/TOTP ───────────────────────────────────────────────────────────────
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* Generate an HOTP value per RFC 4226
|
|
68
|
-
*/
|
|
69
|
-
function generateHotp(secret: Buffer, counter: bigint): string {
|
|
70
|
-
const hmac = createHmac("sha1", secret);
|
|
71
|
-
const counterBuffer = Buffer.alloc(8);
|
|
72
|
-
counterBuffer.writeBigInt64BE(counter);
|
|
73
|
-
hmac.update(counterBuffer);
|
|
74
|
-
const hash = hmac.digest();
|
|
75
|
-
|
|
76
|
-
const offset = hash[hash.length - 1] & 0x0f;
|
|
77
|
-
const code =
|
|
78
|
-
(((hash[offset] & 0x7f) << 24) |
|
|
79
|
-
(hash[offset + 1] << 16) |
|
|
80
|
-
(hash[offset + 2] << 8) |
|
|
81
|
-
hash[offset + 3]) %
|
|
82
|
-
1000000;
|
|
83
|
-
|
|
84
|
-
return code.toString().padStart(6, "0");
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
/**
|
|
88
|
-
* Generate a TOTP value for the current time step
|
|
89
|
-
*/
|
|
90
|
-
export function generateTotp(secret: Buffer, timeStep = 30): string {
|
|
91
|
-
const counter = BigInt(Math.floor(Date.now() / 1000 / timeStep));
|
|
92
|
-
return generateHotp(secret, counter);
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/**
|
|
96
|
-
* Verify a TOTP token with a configurable time window
|
|
97
|
-
*
|
|
98
|
-
* @param secret - The shared secret as a Buffer
|
|
99
|
-
* @param token - The 6-digit TOTP code to verify
|
|
100
|
-
* @param window - Number of time steps to check on each side (default: 1)
|
|
101
|
-
* @returns true if the token is valid within the window
|
|
102
|
-
*/
|
|
103
|
-
export function verifyTotp(secret: Buffer, token: string, window = 1): boolean {
|
|
104
|
-
const timeStep = 30;
|
|
105
|
-
const counter = BigInt(Math.floor(Date.now() / 1000 / timeStep));
|
|
106
|
-
for (let i = -window; i <= window; i++) {
|
|
107
|
-
if (generateHotp(secret, counter + BigInt(i)) === token) {
|
|
108
|
-
return true;
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
return false;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
// ─── Secret Generation ───────────────────────────────────────────────────────
|
|
115
|
-
|
|
116
|
-
/**
|
|
117
|
-
* Generate a new TOTP secret and return the setup information
|
|
118
|
-
*
|
|
119
|
-
* @param issuer - The issuer name (app name) for the QR code
|
|
120
|
-
* @param accountName - The account identifier (usually email)
|
|
121
|
-
* @returns Object with base32 secret and otpauth URI
|
|
122
|
-
*/
|
|
123
|
-
export function generateTotpSecret(issuer: string, accountName: string): {
|
|
124
|
-
secret: string;
|
|
125
|
-
uri: string;
|
|
126
|
-
} {
|
|
127
|
-
// Generate a 20-byte random secret (160 bits, recommended by RFC 4226)
|
|
128
|
-
const secretBuffer = randomBytes(20);
|
|
129
|
-
const secret = base32Encode(secretBuffer);
|
|
130
|
-
|
|
131
|
-
// Build otpauth:// URI for QR code generation
|
|
132
|
-
const encodedIssuer = encodeURIComponent(issuer);
|
|
133
|
-
const encodedAccount = encodeURIComponent(accountName);
|
|
134
|
-
const uri = `otpauth://totp/${encodedIssuer}:${encodedAccount}?secret=${secret}&issuer=${encodedIssuer}&algorithm=SHA1&digits=6&period=30`;
|
|
135
|
-
|
|
136
|
-
return { secret,
|
|
137
|
-
uri };
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
// ─── Recovery Codes ──────────────────────────────────────────────────────────
|
|
141
|
-
|
|
142
|
-
/**
|
|
143
|
-
* Generate a set of one-time recovery codes
|
|
144
|
-
*
|
|
145
|
-
* @param count - Number of recovery codes to generate (default: 10)
|
|
146
|
-
* @returns Array of formatted recovery code strings (e.g. "A1B2C-D3E4F")
|
|
147
|
-
*/
|
|
148
|
-
export function generateRecoveryCodes(count = 10): string[] {
|
|
149
|
-
return Array.from({ length: count }, () => {
|
|
150
|
-
const raw = randomBytes(5).toString("hex").toUpperCase();
|
|
151
|
-
const parts = raw.match(/.{1,5}/g);
|
|
152
|
-
return parts ? parts.join("-") : raw;
|
|
153
|
-
});
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
/**
|
|
157
|
-
* Hash a recovery code for storage
|
|
158
|
-
*/
|
|
159
|
-
export function hashRecoveryCode(code: string): string {
|
|
160
|
-
return createHash("sha256").update(code.replace(/-/g, "").toUpperCase()).digest("hex");
|
|
161
|
-
}
|
|
@@ -1,90 +0,0 @@
|
|
|
1
|
-
import type { OAuthProvider, OAuthProviderProfile } from "./interfaces";
|
|
2
|
-
import { z } from "zod";
|
|
3
|
-
import { logger } from "../utils/logger";
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Creates a Microsoft / Entra ID (Azure AD) OAuth Provider integration.
|
|
7
|
-
*
|
|
8
|
-
* Supports both personal Microsoft accounts and work/school (Azure AD) accounts
|
|
9
|
-
* via the "common" tenant endpoint. Uses the authorization code flow.
|
|
10
|
-
*/
|
|
11
|
-
export function createMicrosoftProvider(config: {
|
|
12
|
-
clientId: string;
|
|
13
|
-
clientSecret: string;
|
|
14
|
-
/** Tenant ID. Defaults to "common" which allows both personal and organizational accounts. */
|
|
15
|
-
tenantId?: string;
|
|
16
|
-
}): OAuthProvider<{ code: string; redirectUri: string }> {
|
|
17
|
-
const tenantId = config.tenantId || "common";
|
|
18
|
-
|
|
19
|
-
return {
|
|
20
|
-
id: "microsoft",
|
|
21
|
-
schema: z.object({
|
|
22
|
-
code: z.string().min(1, "Auth code is required"),
|
|
23
|
-
redirectUri: z.string().url("Valid redirect URI is required")
|
|
24
|
-
}),
|
|
25
|
-
verify: async (payload: { code: string; redirectUri: string }): Promise<OAuthProviderProfile | null> => {
|
|
26
|
-
try {
|
|
27
|
-
// Exchange code for access token
|
|
28
|
-
const tokenResponse = await fetch(
|
|
29
|
-
`https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/token`,
|
|
30
|
-
{
|
|
31
|
-
method: "POST",
|
|
32
|
-
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
33
|
-
body: new URLSearchParams({
|
|
34
|
-
client_id: config.clientId,
|
|
35
|
-
client_secret: config.clientSecret,
|
|
36
|
-
code: payload.code,
|
|
37
|
-
redirect_uri: payload.redirectUri,
|
|
38
|
-
grant_type: "authorization_code",
|
|
39
|
-
scope: "openid profile email User.Read"
|
|
40
|
-
})
|
|
41
|
-
}
|
|
42
|
-
);
|
|
43
|
-
|
|
44
|
-
if (!tokenResponse.ok) {
|
|
45
|
-
logger.error("Failed to get Microsoft access token", { detail: await tokenResponse.text() });
|
|
46
|
-
return null;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
const tokenData = await tokenResponse.json() as { access_token: string };
|
|
50
|
-
const accessToken = tokenData.access_token;
|
|
51
|
-
|
|
52
|
-
// Fetch user profile from Microsoft Graph
|
|
53
|
-
const profileResponse = await fetch("https://graph.microsoft.com/v1.0/me", {
|
|
54
|
-
headers: { "Authorization": `Bearer ${accessToken}` }
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
if (!profileResponse.ok) {
|
|
58
|
-
logger.error("Failed to get Microsoft user info", { detail: await profileResponse.text() });
|
|
59
|
-
return null;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
const profileData = await profileResponse.json() as {
|
|
63
|
-
id: string;
|
|
64
|
-
displayName?: string | null;
|
|
65
|
-
mail?: string | null;
|
|
66
|
-
userPrincipalName?: string | null;
|
|
67
|
-
};
|
|
68
|
-
|
|
69
|
-
const email = profileData.mail || profileData.userPrincipalName;
|
|
70
|
-
if (!email) {
|
|
71
|
-
logger.error("Microsoft user has no email");
|
|
72
|
-
return null;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
// Attempt to fetch profile photo URL (Graph returns binary, not a URL).
|
|
76
|
-
// We skip this and let the frontend use the Microsoft Graph photo endpoint.
|
|
77
|
-
return {
|
|
78
|
-
providerId: profileData.id,
|
|
79
|
-
email,
|
|
80
|
-
displayName: profileData.displayName || null,
|
|
81
|
-
photoUrl: null,
|
|
82
|
-
emailVerified: true
|
|
83
|
-
};
|
|
84
|
-
} catch (error) {
|
|
85
|
-
logger.error("Microsoft OAuth error", { error: error });
|
|
86
|
-
return null;
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
};
|
|
90
|
-
}
|
package/src/auth/middleware.ts
DELETED
|
@@ -1,369 +0,0 @@
|
|
|
1
|
-
import { MiddlewareHandler, Context } from "hono";
|
|
2
|
-
import { DataDriver } from "@rebasepro/types";
|
|
3
|
-
import { verifyAccessToken, AccessTokenPayload } from "./jwt";
|
|
4
|
-
import { HonoEnv } from "../api/types";
|
|
5
|
-
import { scopeDataDriver } from "./rls-scope";
|
|
6
|
-
import { safeCompare } from "./crypto-utils";
|
|
7
|
-
import { isApiKeyToken, validateApiKey } from "./api-keys/api-key-middleware";
|
|
8
|
-
import type { ApiKeyStore } from "./api-keys/api-key-store";
|
|
9
|
-
import { logger } from "../utils/logger";
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Result from a custom auth validator.
|
|
13
|
-
* - `false`/`null`/`undefined` = not authenticated
|
|
14
|
-
* - `true` = authenticated as default user
|
|
15
|
-
* - object with `userId` or `uid` = authenticated with user info
|
|
16
|
-
*/
|
|
17
|
-
export type AuthResult = boolean | null | undefined | { userId?: string; uid?: string; roles?: string[]; [key: string]: unknown };
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Options for creating an auth middleware via createAuthMiddleware()
|
|
21
|
-
*/
|
|
22
|
-
export interface AuthMiddlewareOptions {
|
|
23
|
-
/** DataDriver to scope via withAuth() for RLS */
|
|
24
|
-
driver: DataDriver;
|
|
25
|
-
/**
|
|
26
|
-
* Optional per-request driver resolver for multi-data-source backends.
|
|
27
|
-
* Given the request context, returns the unscoped delegate to use (e.g.
|
|
28
|
-
* Postgres vs Mongo, picked by the request's collection data source).
|
|
29
|
-
* When omitted, `driver` is used for every request.
|
|
30
|
-
*/
|
|
31
|
-
resolveDriver?: (c: Context<HonoEnv>) => DataDriver;
|
|
32
|
-
/**
|
|
33
|
-
* If true, return 401 when no valid token is present.
|
|
34
|
-
*
|
|
35
|
-
* **Defaults to `true` (secure by default).** Set to `false` only for
|
|
36
|
-
* intentionally public endpoints where access control is fully delegated
|
|
37
|
-
* to Postgres Row-Level Security policies.
|
|
38
|
-
*/
|
|
39
|
-
requireAuth?: boolean;
|
|
40
|
-
/** Optional custom validator (for non-JWT auth, e.g. Firebase Auth) */
|
|
41
|
-
validator?: (c: Context<HonoEnv>) => Promise<AuthResult>;
|
|
42
|
-
/**
|
|
43
|
-
* A static secret key for server-to-server / script authentication.
|
|
44
|
-
*
|
|
45
|
-
* When a request sends `Authorization: Bearer <key>` and the key matches
|
|
46
|
-
* this value, the request is granted admin-level access (uid: `service`,
|
|
47
|
-
* roles: `["admin"]`) **without** JWT verification. The driver is scoped
|
|
48
|
-
* via `withAuth()` with the service identity.
|
|
49
|
-
*
|
|
50
|
-
* This is the Rebase equivalent of a Firebase Service Account key.
|
|
51
|
-
* Set via `REBASE_SERVICE_KEY` in `.env` and pass through the backend config.
|
|
52
|
-
*
|
|
53
|
-
* **Security:** The comparison uses constant-time equality to prevent
|
|
54
|
-
* timing attacks. The key must be at least 32 characters.
|
|
55
|
-
*/
|
|
56
|
-
serviceKey?: string;
|
|
57
|
-
/**
|
|
58
|
-
* API key store for authenticating `rk_` prefixed tokens.
|
|
59
|
-
* When set, tokens starting with `rk_` are validated against the
|
|
60
|
-
* database instead of being treated as JWTs.
|
|
61
|
-
*/
|
|
62
|
-
apiKeyStore?: ApiKeyStore;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/**
|
|
66
|
-
* Hono middleware that requires a valid JWT token
|
|
67
|
-
* Returns 401 if token is missing or invalid
|
|
68
|
-
*/
|
|
69
|
-
export const requireAuth: MiddlewareHandler<HonoEnv> = async (
|
|
70
|
-
c,
|
|
71
|
-
next
|
|
72
|
-
) => {
|
|
73
|
-
const authHeader = c.req.header("authorization");
|
|
74
|
-
const queryToken = c.req.query("token");
|
|
75
|
-
const hasBearer = authHeader && authHeader.startsWith("Bearer ");
|
|
76
|
-
|
|
77
|
-
if (!hasBearer && !queryToken) {
|
|
78
|
-
return c.json({
|
|
79
|
-
error: {
|
|
80
|
-
message: "Authorization header or token query parameter missing or invalid",
|
|
81
|
-
code: "UNAUTHORIZED"
|
|
82
|
-
}
|
|
83
|
-
}, 401);
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
const token = hasBearer ? authHeader!.substring(7) : queryToken!;
|
|
87
|
-
const payload = verifyAccessToken(token);
|
|
88
|
-
|
|
89
|
-
if (!payload) {
|
|
90
|
-
return c.json({
|
|
91
|
-
error: {
|
|
92
|
-
message: "Invalid or expired token",
|
|
93
|
-
code: "UNAUTHORIZED"
|
|
94
|
-
}
|
|
95
|
-
}, 401);
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
c.set("user", payload);
|
|
99
|
-
return next();
|
|
100
|
-
};
|
|
101
|
-
|
|
102
|
-
/**
|
|
103
|
-
* Factory that creates a requireAuth middleware with optional service key support.
|
|
104
|
-
*
|
|
105
|
-
* When `serviceKey` is provided, the middleware will check if the Bearer token
|
|
106
|
-
* matches the service key using constant-time comparison. If it matches, the
|
|
107
|
-
* request is authenticated as a service user with admin privileges.
|
|
108
|
-
*
|
|
109
|
-
* This allows admin routes (which use standalone requireAuth + requireAdmin)
|
|
110
|
-
* to be accessed via service keys for scripts and server-to-server calls.
|
|
111
|
-
*/
|
|
112
|
-
export function createRequireAuth(options?: { serviceKey?: string }): MiddlewareHandler<HonoEnv> {
|
|
113
|
-
if (!options?.serviceKey) return requireAuth;
|
|
114
|
-
|
|
115
|
-
const key = options.serviceKey;
|
|
116
|
-
return async (c, next) => {
|
|
117
|
-
const authHeader = c.req.header("authorization");
|
|
118
|
-
const queryToken = c.req.query("token");
|
|
119
|
-
const hasBearer = authHeader && authHeader.startsWith("Bearer ");
|
|
120
|
-
|
|
121
|
-
if (!hasBearer && !queryToken) {
|
|
122
|
-
return c.json({
|
|
123
|
-
error: {
|
|
124
|
-
message: "Authorization header or token query parameter missing or invalid",
|
|
125
|
-
code: "UNAUTHORIZED"
|
|
126
|
-
}
|
|
127
|
-
}, 401);
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
const token = hasBearer ? authHeader!.substring(7) : queryToken!;
|
|
131
|
-
|
|
132
|
-
// Check service key first (constant-time comparison)
|
|
133
|
-
if (safeCompare(token, key)) {
|
|
134
|
-
c.set("user", { userId: "service",
|
|
135
|
-
roles: ["admin"] } as AccessTokenPayload);
|
|
136
|
-
return next();
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
// Fall back to JWT verification
|
|
140
|
-
const payload = verifyAccessToken(token);
|
|
141
|
-
|
|
142
|
-
if (!payload) {
|
|
143
|
-
return c.json({
|
|
144
|
-
error: {
|
|
145
|
-
message: "Invalid or expired token",
|
|
146
|
-
code: "UNAUTHORIZED"
|
|
147
|
-
}
|
|
148
|
-
}, 401);
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
c.set("user", payload);
|
|
152
|
-
return next();
|
|
153
|
-
};
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
/**
|
|
157
|
-
* Middleware that requires the user to have an admin or schema-admin role.
|
|
158
|
-
* Must be used AFTER requireAuth or on a route where user is guaranteed.
|
|
159
|
-
*/
|
|
160
|
-
export const requireAdmin: MiddlewareHandler<HonoEnv> = async (
|
|
161
|
-
c,
|
|
162
|
-
next
|
|
163
|
-
) => {
|
|
164
|
-
const user = c.get("user");
|
|
165
|
-
if (!user) {
|
|
166
|
-
return c.json({
|
|
167
|
-
error: {
|
|
168
|
-
message: "User not authenticated. requireAuth middleware is missing?",
|
|
169
|
-
code: "UNAUTHORIZED"
|
|
170
|
-
}
|
|
171
|
-
}, 401);
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
const roles = (typeof user === "object" && user !== null && "roles" in user) ? (user.roles || []) : [];
|
|
175
|
-
const isAdmin = roles.some((role: string) => {
|
|
176
|
-
return role === "admin" || role === "schema-admin";
|
|
177
|
-
});
|
|
178
|
-
|
|
179
|
-
if (!isAdmin) {
|
|
180
|
-
return c.json({
|
|
181
|
-
error: {
|
|
182
|
-
message: "Admin privileges required for this operation",
|
|
183
|
-
code: "FORBIDDEN"
|
|
184
|
-
}
|
|
185
|
-
}, 403);
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
return next();
|
|
189
|
-
};
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
/**
|
|
193
|
-
* Middleware that optionally extracts user from JWT
|
|
194
|
-
* Does not return 401 if token is missing - allows anonymous access
|
|
195
|
-
*/
|
|
196
|
-
export const optionalAuth: MiddlewareHandler<HonoEnv> = async (
|
|
197
|
-
c,
|
|
198
|
-
next
|
|
199
|
-
) => {
|
|
200
|
-
const authHeader = c.req.header("authorization");
|
|
201
|
-
const queryToken = c.req.query("token");
|
|
202
|
-
const hasBearer = authHeader && authHeader.startsWith("Bearer ");
|
|
203
|
-
|
|
204
|
-
if (hasBearer || queryToken) {
|
|
205
|
-
const token = hasBearer ? authHeader!.substring(7) : queryToken!;
|
|
206
|
-
const payload = verifyAccessToken(token);
|
|
207
|
-
if (payload) {
|
|
208
|
-
c.set("user", payload);
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
return next();
|
|
213
|
-
};
|
|
214
|
-
|
|
215
|
-
/**
|
|
216
|
-
* Extract user from token - for WebSocket authentication
|
|
217
|
-
*/
|
|
218
|
-
export function extractUserFromToken(token: string): AccessTokenPayload | null {
|
|
219
|
-
return verifyAccessToken(token);
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
/**
|
|
223
|
-
* Create a configurable auth middleware that handles:
|
|
224
|
-
* 1. Token extraction (via custom validator or JWT Bearer token)
|
|
225
|
-
* 2. RLS-scoped DataDriver via withAuth()
|
|
226
|
-
* 3. Enforcement (401 when requireAuth is true and no user)
|
|
227
|
-
*
|
|
228
|
-
* **Secure by default:** `requireAuth` defaults to `true`. Anonymous
|
|
229
|
-
* access is only allowed when the developer explicitly opts out by
|
|
230
|
-
* setting `requireAuth: false`, indicating that Postgres RLS policies
|
|
231
|
-
* fully control access.
|
|
232
|
-
*
|
|
233
|
-
* **Fail-closed:** The raw unscoped driver is never placed in the
|
|
234
|
-
* request context. Every code path either scopes via `withAuth()` or
|
|
235
|
-
* rejects the request. This prevents silent RLS bypass.
|
|
236
|
-
*
|
|
237
|
-
* This is the single source of truth for HTTP auth in Rebase.
|
|
238
|
-
* Use this instead of manually parsing tokens in route handlers.
|
|
239
|
-
*/
|
|
240
|
-
|
|
241
|
-
export function createAuthMiddleware(options: AuthMiddlewareOptions): MiddlewareHandler<HonoEnv> {
|
|
242
|
-
const { driver: baseDriver, resolveDriver, requireAuth: enforceAuth = true, validator, serviceKey, apiKeyStore } = options;
|
|
243
|
-
|
|
244
|
-
return async (c, next) => {
|
|
245
|
-
// Pick the per-request delegate (multi-data-source) before scoping.
|
|
246
|
-
const driver = resolveDriver ? resolveDriver(c) : baseDriver;
|
|
247
|
-
if (validator) {
|
|
248
|
-
// Custom validator path (e.g., Firebase Auth, API keys)
|
|
249
|
-
try {
|
|
250
|
-
const authResult = await validator(c);
|
|
251
|
-
if (authResult && typeof authResult === "object") {
|
|
252
|
-
const id = ("userId" in authResult ? authResult.userId : undefined)
|
|
253
|
-
|| ("uid" in authResult ? authResult.uid : undefined);
|
|
254
|
-
if (id) {
|
|
255
|
-
const roles = authResult.roles || [];
|
|
256
|
-
c.set("user", { userId: id,
|
|
257
|
-
roles });
|
|
258
|
-
const user = { uid: id,
|
|
259
|
-
roles,
|
|
260
|
-
...authResult };
|
|
261
|
-
c.set("driver", await scopeDataDriver(driver, user));
|
|
262
|
-
} else {
|
|
263
|
-
// Validator returned an object but without an ID — scope as anon
|
|
264
|
-
c.set("driver", await scopeDataDriver(driver, { uid: "anon",
|
|
265
|
-
roles: ["anon"] }));
|
|
266
|
-
}
|
|
267
|
-
} else if (authResult === true) {
|
|
268
|
-
c.set("user", { userId: "default",
|
|
269
|
-
roles: [] });
|
|
270
|
-
c.set("driver", await scopeDataDriver(driver, { uid: "default",
|
|
271
|
-
roles: [] }));
|
|
272
|
-
} else {
|
|
273
|
-
// Not authenticated — scope as anon so RLS can evaluate.
|
|
274
|
-
// Fail closed: if anon scoping fails, reject instead of
|
|
275
|
-
// falling back to the raw driver.
|
|
276
|
-
c.set("driver", await scopeDataDriver(driver, { uid: "anon",
|
|
277
|
-
roles: ["anon"] }));
|
|
278
|
-
}
|
|
279
|
-
} catch (error) {
|
|
280
|
-
return c.json({ error: { message: "Unauthorized",
|
|
281
|
-
code: "UNAUTHORIZED" } }, 401);
|
|
282
|
-
}
|
|
283
|
-
} else {
|
|
284
|
-
// Default JWT path (with optional service key support)
|
|
285
|
-
const authHeader = c.req.header("authorization");
|
|
286
|
-
const queryToken = c.req.query("token");
|
|
287
|
-
const hasBearer = authHeader && authHeader.startsWith("Bearer ");
|
|
288
|
-
|
|
289
|
-
if (hasBearer || queryToken) {
|
|
290
|
-
const token = hasBearer ? authHeader!.substring(7) : queryToken!;
|
|
291
|
-
|
|
292
|
-
// ── Service Key check ──────────────────────────────────
|
|
293
|
-
// Check BEFORE JWT verification. Service keys are static
|
|
294
|
-
// secrets (like Firebase SA keys) that grant admin access
|
|
295
|
-
// for scripts, cron jobs, and server-to-server calls.
|
|
296
|
-
if (serviceKey && safeCompare(token, serviceKey)) {
|
|
297
|
-
const serviceUser: AccessTokenPayload = {
|
|
298
|
-
userId: "service",
|
|
299
|
-
roles: ["admin"]
|
|
300
|
-
};
|
|
301
|
-
c.set("user", serviceUser);
|
|
302
|
-
try {
|
|
303
|
-
c.set("driver", await scopeDataDriver(driver, {
|
|
304
|
-
uid: "service",
|
|
305
|
-
roles: ["admin"]
|
|
306
|
-
}));
|
|
307
|
-
} catch (error) {
|
|
308
|
-
logger.error("[AUTH] RLS scoping failed for service key", { error: error });
|
|
309
|
-
return c.json({ error: { message: "Internal authentication error",
|
|
310
|
-
code: "INTERNAL_ERROR" } }, 500);
|
|
311
|
-
}
|
|
312
|
-
} else if (apiKeyStore && isApiKeyToken(token)) {
|
|
313
|
-
// ── API Key verification ──────────────────────────────
|
|
314
|
-
// Tokens starting with `rk_` are validated against the
|
|
315
|
-
// api_keys table instead of JWT verification.
|
|
316
|
-
const result = await validateApiKey(c, token, {
|
|
317
|
-
store: apiKeyStore,
|
|
318
|
-
driver
|
|
319
|
-
});
|
|
320
|
-
if (result !== true) {
|
|
321
|
-
return result;
|
|
322
|
-
}
|
|
323
|
-
} else {
|
|
324
|
-
// ── JWT verification ───────────────────────────────────
|
|
325
|
-
const payload = extractUserFromToken(token);
|
|
326
|
-
|
|
327
|
-
if (payload) {
|
|
328
|
-
c.set("user", payload);
|
|
329
|
-
try {
|
|
330
|
-
const user = { uid: payload.userId,
|
|
331
|
-
roles: payload.roles };
|
|
332
|
-
c.set("driver", await scopeDataDriver(driver, user));
|
|
333
|
-
} catch (error) {
|
|
334
|
-
// withAuth() failed for a valid token — reject (fail closed)
|
|
335
|
-
logger.error("[AUTH] RLS scoping failed for authenticated user", { error: error });
|
|
336
|
-
return c.json({ error: { message: "Internal authentication error",
|
|
337
|
-
code: "INTERNAL_ERROR" } }, 500);
|
|
338
|
-
}
|
|
339
|
-
} else {
|
|
340
|
-
// Token present but invalid — always reject.
|
|
341
|
-
// Providing a malformed token should never grant access,
|
|
342
|
-
// regardless of requireAuth setting.
|
|
343
|
-
return c.json({ error: { message: "Invalid or expired token",
|
|
344
|
-
code: "UNAUTHORIZED" } }, 401);
|
|
345
|
-
}
|
|
346
|
-
}
|
|
347
|
-
} else {
|
|
348
|
-
// No token provided — scope as anon for RLS evaluation.
|
|
349
|
-
// Fail closed: if anon scoping fails, return 500 rather
|
|
350
|
-
// than silently proceeding with an unscoped driver.
|
|
351
|
-
try {
|
|
352
|
-
c.set("driver", await scopeDataDriver(driver, { uid: "anon",
|
|
353
|
-
roles: ["anon"] }));
|
|
354
|
-
} catch (error) {
|
|
355
|
-
logger.error("[AUTH] Failed to create anon-scoped driver", { error: error });
|
|
356
|
-
return c.json({ error: { message: "Server configuration error",
|
|
357
|
-
code: "INTERNAL_ERROR" } }, 500);
|
|
358
|
-
}
|
|
359
|
-
}
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
if (enforceAuth && !c.get("user")) {
|
|
363
|
-
return c.json({ error: { message: "Unauthorized: Authentication required",
|
|
364
|
-
code: "UNAUTHORIZED" } }, 401);
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
return next();
|
|
368
|
-
};
|
|
369
|
-
}
|
package/src/auth/password.ts
DELETED
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
import { scrypt, randomBytes, timingSafeEqual } from "crypto";
|
|
2
|
-
import { promisify } from "util";
|
|
3
|
-
|
|
4
|
-
const scryptAsync = promisify(scrypt);
|
|
5
|
-
|
|
6
|
-
// Scrypt parameters (recommended values for 2024+)
|
|
7
|
-
const SALT_LENGTH = 32;
|
|
8
|
-
const KEY_LENGTH = 64;
|
|
9
|
-
const SCRYPT_PARAMS = { N: 16384,
|
|
10
|
-
r: 8,
|
|
11
|
-
p: 1 };
|
|
12
|
-
|
|
13
|
-
export interface PasswordValidationResult {
|
|
14
|
-
valid: boolean;
|
|
15
|
-
errors: string[];
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Password requirements:
|
|
20
|
-
* - Minimum 8 characters
|
|
21
|
-
* - At least 1 uppercase letter
|
|
22
|
-
* - At least 1 lowercase letter
|
|
23
|
-
* - At least 1 number
|
|
24
|
-
*/
|
|
25
|
-
export function validatePasswordStrength(password: string): PasswordValidationResult {
|
|
26
|
-
const errors: string[] = [];
|
|
27
|
-
|
|
28
|
-
if (password.length < 8) {
|
|
29
|
-
errors.push("Password must be at least 8 characters long");
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
if (!/[A-Z]/.test(password)) {
|
|
33
|
-
errors.push("Password must contain at least one uppercase letter");
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
if (!/[a-z]/.test(password)) {
|
|
37
|
-
errors.push("Password must contain at least one lowercase letter");
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
if (!/[0-9]/.test(password)) {
|
|
41
|
-
errors.push("Password must contain at least one number");
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
return {
|
|
45
|
-
valid: errors.length === 0,
|
|
46
|
-
errors
|
|
47
|
-
};
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Hash a password using Node's built-in scrypt
|
|
52
|
-
* Returns format: salt:hash (both hex encoded)
|
|
53
|
-
*/
|
|
54
|
-
export async function hashPassword(password: string): Promise<string> {
|
|
55
|
-
const salt = randomBytes(SALT_LENGTH);
|
|
56
|
-
const derivedKey = await scryptAsync(password, salt, KEY_LENGTH) as Buffer;
|
|
57
|
-
return `${salt.toString("hex")}:${derivedKey.toString("hex")}`;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* Verify a password against a scrypt hash
|
|
62
|
-
* Expects format: salt:hash (both hex encoded)
|
|
63
|
-
*/
|
|
64
|
-
export async function verifyPassword(password: string, storedHash: string): Promise<boolean> {
|
|
65
|
-
const [saltHex, hashHex] = storedHash.split(":");
|
|
66
|
-
if (!saltHex || !hashHex) {
|
|
67
|
-
return false;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
const salt = Buffer.from(saltHex, "hex");
|
|
71
|
-
const storedKey = Buffer.from(hashHex, "hex");
|
|
72
|
-
|
|
73
|
-
const derivedKey = await scryptAsync(password, salt, KEY_LENGTH) as Buffer;
|
|
74
|
-
|
|
75
|
-
// Use timing-safe comparison to prevent timing attacks
|
|
76
|
-
return timingSafeEqual(derivedKey, storedKey);
|
|
77
|
-
}
|