@edge-base/server 0.1.1
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/admin-build/.gitkeep +0 -0
- package/admin-build/_app/env.js +1 -0
- package/admin-build/_app/immutable/assets/0.Bm6cF078.css +1 -0
- package/admin-build/_app/immutable/assets/1.BfW3pUNa.css +1 -0
- package/admin-build/_app/immutable/assets/11.CVmQOewb.css +1 -0
- package/admin-build/_app/immutable/assets/12.B1EhbRZT.css +1 -0
- package/admin-build/_app/immutable/assets/13.BvwYeuwE.css +1 -0
- package/admin-build/_app/immutable/assets/14.CdVfcO0R.css +1 -0
- package/admin-build/_app/immutable/assets/15.2yeZ66b-.css +1 -0
- package/admin-build/_app/immutable/assets/17.BVg0JEVu.css +1 -0
- package/admin-build/_app/immutable/assets/18.Rwnl3x_i.css +1 -0
- package/admin-build/_app/immutable/assets/20.DsPWA9AV.css +1 -0
- package/admin-build/_app/immutable/assets/21.Dz2RJ56c.css +1 -0
- package/admin-build/_app/immutable/assets/22.DwNLk5Ai.css +1 -0
- package/admin-build/_app/immutable/assets/23.CFpu0gOO.css +1 -0
- package/admin-build/_app/immutable/assets/24.Cy5LBeoJ.css +1 -0
- package/admin-build/_app/immutable/assets/25.pUyLVf-h.css +1 -0
- package/admin-build/_app/immutable/assets/26.DBcGrlXa.css +1 -0
- package/admin-build/_app/immutable/assets/27.BswYyAJD.css +1 -0
- package/admin-build/_app/immutable/assets/28.B4ueB1Kf.css +1 -0
- package/admin-build/_app/immutable/assets/29.B-qU6PdF.css +1 -0
- package/admin-build/_app/immutable/assets/3.Dg81Pgmd.css +1 -0
- package/admin-build/_app/immutable/assets/30.CsdWum94.css +1 -0
- package/admin-build/_app/immutable/assets/31.U6OwIp50.css +1 -0
- package/admin-build/_app/immutable/assets/4.CyawCCux.css +1 -0
- package/admin-build/_app/immutable/assets/5.C0YO2HTk.css +1 -0
- package/admin-build/_app/immutable/assets/8.Br5jd6kD.css +1 -0
- package/admin-build/_app/immutable/assets/Badge.EMYLHBxE.css +1 -0
- package/admin-build/_app/immutable/assets/Button.DpzMRTjK.css +1 -0
- package/admin-build/_app/immutable/assets/ConfirmDialog.DAnaWRRk.css +1 -0
- package/admin-build/_app/immutable/assets/EmptyState.CwKsu57Y.css +1 -0
- package/admin-build/_app/immutable/assets/Input.BDUSenmU.css +1 -0
- package/admin-build/_app/immutable/assets/Modal.Dm5B0Xie.css +1 -0
- package/admin-build/_app/immutable/assets/PageShell.CmU-Xh-b.css +1 -0
- package/admin-build/_app/immutable/assets/SchemaFieldEditor.g4NsCdno.css +1 -0
- package/admin-build/_app/immutable/assets/Select.BW4Keufm.css +1 -0
- package/admin-build/_app/immutable/assets/Skeleton.KWUulTKJ.css +1 -0
- package/admin-build/_app/immutable/assets/Tabs.CniGYb67.css +1 -0
- package/admin-build/_app/immutable/assets/TimeChart.BTCDAvmT.css +1 -0
- package/admin-build/_app/immutable/assets/Toggle.Cy_K12OM.css +1 -0
- package/admin-build/_app/immutable/assets/TopList.ClFzmPlA.css +1 -0
- package/admin-build/_app/immutable/chunks/7B47DvSx.js +1 -0
- package/admin-build/_app/immutable/chunks/7f08Id8e.js +1 -0
- package/admin-build/_app/immutable/chunks/8wJeQ7LN.js +1 -0
- package/admin-build/_app/immutable/chunks/B-h2afW5.js +1 -0
- package/admin-build/_app/immutable/chunks/B8vJP3wz.js +1 -0
- package/admin-build/_app/immutable/chunks/BR_fL5Yv.js +1 -0
- package/admin-build/_app/immutable/chunks/BY92tFS2.js +1 -0
- package/admin-build/_app/immutable/chunks/BcR-Rdj9.js +1 -0
- package/admin-build/_app/immutable/chunks/BdrwyZv8.js +1 -0
- package/admin-build/_app/immutable/chunks/Bh56EfQ_.js +1 -0
- package/admin-build/_app/immutable/chunks/BkrCkgYp.js +1 -0
- package/admin-build/_app/immutable/chunks/BmRjiP5k.js +1 -0
- package/admin-build/_app/immutable/chunks/BsokvhWC.js +1 -0
- package/admin-build/_app/immutable/chunks/C4D51vTW.js +1 -0
- package/admin-build/_app/immutable/chunks/C6puvcoR.js +2 -0
- package/admin-build/_app/immutable/chunks/CCKNu7m7.js +1 -0
- package/admin-build/_app/immutable/chunks/CWj6FrbW.js +1 -0
- package/admin-build/_app/immutable/chunks/Ce-ngf4p.js +5 -0
- package/admin-build/_app/immutable/chunks/Cs0GwzJA.js +1 -0
- package/admin-build/_app/immutable/chunks/CwROoZK0.js +1 -0
- package/admin-build/_app/immutable/chunks/CxCPv_Ut.js +1 -0
- package/admin-build/_app/immutable/chunks/CxbRue-5.js +1 -0
- package/admin-build/_app/immutable/chunks/CyqB6g-D.js +1 -0
- package/admin-build/_app/immutable/chunks/D5h5A1cc.js +2 -0
- package/admin-build/_app/immutable/chunks/DnyL7Zq-.js +1 -0
- package/admin-build/_app/immutable/chunks/DoPXzH7F.js +1 -0
- package/admin-build/_app/immutable/chunks/DrQSgw-f.js +1 -0
- package/admin-build/_app/immutable/chunks/DttM2zNO.js +1 -0
- package/admin-build/_app/immutable/chunks/DuXuUBWN.js +1 -0
- package/admin-build/_app/immutable/chunks/MdeqaOQx.js +10 -0
- package/admin-build/_app/immutable/chunks/NuUjtcO2.js +1 -0
- package/admin-build/_app/immutable/chunks/Q2nPFxS6.js +1 -0
- package/admin-build/_app/immutable/chunks/R6arueIl.js +1 -0
- package/admin-build/_app/immutable/chunks/UUazaC_N.js +1 -0
- package/admin-build/_app/immutable/chunks/cOYbrQxx.js +1 -0
- package/admin-build/_app/immutable/chunks/eFQHTGwA.js +1 -0
- package/admin-build/_app/immutable/chunks/ehbppgYb.js +1 -0
- package/admin-build/_app/immutable/chunks/glwixJlP.js +1 -0
- package/admin-build/_app/immutable/chunks/vApWTCBs.js +1 -0
- package/admin-build/_app/immutable/chunks/w89G9Xpi.js +1 -0
- package/admin-build/_app/immutable/chunks/wJsUhbfZ.js +1 -0
- package/admin-build/_app/immutable/chunks/zfauFM8P.js +1 -0
- package/admin-build/_app/immutable/entry/app.CcO-Uos3.js +2 -0
- package/admin-build/_app/immutable/entry/start.COebYq3I.js +1 -0
- package/admin-build/_app/immutable/nodes/0.CjtHKU-6.js +1 -0
- package/admin-build/_app/immutable/nodes/1.DEisjlM0.js +1 -0
- package/admin-build/_app/immutable/nodes/10.CvhdyWVB.js +1 -0
- package/admin-build/_app/immutable/nodes/11.DjHqcOvy.js +1 -0
- package/admin-build/_app/immutable/nodes/12.mQLz4Mj_.js +1 -0
- package/admin-build/_app/immutable/nodes/13.CBonZZyP.js +110 -0
- package/admin-build/_app/immutable/nodes/14.d-oiZL0j.js +3 -0
- package/admin-build/_app/immutable/nodes/15.CKPQsUYF.js +1 -0
- package/admin-build/_app/immutable/nodes/16.wPzAPQGx.js +1 -0
- package/admin-build/_app/immutable/nodes/17.DayhKyEZ.js +1 -0
- package/admin-build/_app/immutable/nodes/18.DKwS0Ir0.js +1 -0
- package/admin-build/_app/immutable/nodes/19.wPzAPQGx.js +1 -0
- package/admin-build/_app/immutable/nodes/2.BKoKrw1i.js +1 -0
- package/admin-build/_app/immutable/nodes/20.BvIkkkrW.js +1 -0
- package/admin-build/_app/immutable/nodes/21.DMaFhdHk.js +128 -0
- package/admin-build/_app/immutable/nodes/22.3xdgwuK1.js +1 -0
- package/admin-build/_app/immutable/nodes/23.8Bvgjbsl.js +112 -0
- package/admin-build/_app/immutable/nodes/24.DzSSzRhG.js +2 -0
- package/admin-build/_app/immutable/nodes/25.9KKYBnAE.js +2 -0
- package/admin-build/_app/immutable/nodes/26.Bhn9dfhY.js +1 -0
- package/admin-build/_app/immutable/nodes/27.kRLiC24G.js +1 -0
- package/admin-build/_app/immutable/nodes/28.BVIN1-7N.js +1 -0
- package/admin-build/_app/immutable/nodes/29.3yabZWj4.js +1 -0
- package/admin-build/_app/immutable/nodes/3.BFtSOkX7.js +2 -0
- package/admin-build/_app/immutable/nodes/30.CyCQlwaP.js +1 -0
- package/admin-build/_app/immutable/nodes/31.C4LDXjES.js +1 -0
- package/admin-build/_app/immutable/nodes/4.CvbiMlCa.js +1 -0
- package/admin-build/_app/immutable/nodes/5.C6BLv2eM.js +1 -0
- package/admin-build/_app/immutable/nodes/6.BcXvfl2P.js +1 -0
- package/admin-build/_app/immutable/nodes/7.CIuqhPiK.js +1 -0
- package/admin-build/_app/immutable/nodes/8.BQOR_JfO.js +1 -0
- package/admin-build/_app/immutable/nodes/9.NZqXQxPy.js +1 -0
- package/admin-build/_app/version.json +1 -0
- package/admin-build/favicon.svg +26 -0
- package/admin-build/index.html +45 -0
- package/openapi.json +19543 -0
- package/package.json +66 -0
- package/src/__tests__/admin-assets.test.ts +55 -0
- package/src/__tests__/admin-data-routes.test.ts +488 -0
- package/src/__tests__/admin-db-target.test.ts +103 -0
- package/src/__tests__/admin-routing.test.ts +31 -0
- package/src/__tests__/admin-user-management.test.ts +311 -0
- package/src/__tests__/analytics-query.test.ts +75 -0
- package/src/__tests__/auth-d1.test.ts +749 -0
- package/src/__tests__/auth-db-adapter.test.ts +73 -0
- package/src/__tests__/auth-jwt.test.ts +440 -0
- package/src/__tests__/auth-oauth.test.ts +389 -0
- package/src/__tests__/auth-password.test.ts +367 -0
- package/src/__tests__/auth-redirect.test.ts +87 -0
- package/src/__tests__/backup-restore.test.ts +711 -0
- package/src/__tests__/broadcast.test.ts +128 -0
- package/src/__tests__/cli.test.ts +178 -0
- package/src/__tests__/cloudflare-realtime.test.ts +113 -0
- package/src/__tests__/config.test.ts +469 -0
- package/src/__tests__/cors.test.ts +154 -0
- package/src/__tests__/cron.test.ts +302 -0
- package/src/__tests__/d1-handler.test.ts +402 -0
- package/src/__tests__/d1-sql.test.ts +120 -0
- package/src/__tests__/database-live-config.test.ts +42 -0
- package/src/__tests__/database-live-emitter.test.ts +56 -0
- package/src/__tests__/database-live-filters.test.ts +63 -0
- package/src/__tests__/database-live-route.test.ts +113 -0
- package/src/__tests__/db-sql.test.ts +163 -0
- package/src/__tests__/do-lifecycle.test.ts +263 -0
- package/src/__tests__/do-router.test.ts +729 -0
- package/src/__tests__/email-provider.test.ts +128 -0
- package/src/__tests__/email-templates.test.ts +528 -0
- package/src/__tests__/error-format.test.ts +250 -0
- package/src/__tests__/field-ops.test.ts +242 -0
- package/src/__tests__/functions-context.test.ts +334 -0
- package/src/__tests__/functions-d1-proxy.test.ts +229 -0
- package/src/__tests__/functions-registry-runtime-config.test.ts +17 -0
- package/src/__tests__/functions-route.test.ts +139 -0
- package/src/__tests__/internal-request.test.ts +77 -0
- package/src/__tests__/log-writer.test.ts +44 -0
- package/src/__tests__/logger.test.ts +58 -0
- package/src/__tests__/meta-admin-proxy.test.ts +48 -0
- package/src/__tests__/meta-export-coverage.test.ts +191 -0
- package/src/__tests__/meta-route-registration.test.ts +47 -0
- package/src/__tests__/namespace-dump.test.ts +28 -0
- package/src/__tests__/oauth-providers.test.ts +337 -0
- package/src/__tests__/openapi-coverage.test.ts +144 -0
- package/src/__tests__/pagination.test.ts +59 -0
- package/src/__tests__/password-policy.test.ts +191 -0
- package/src/__tests__/plugin-migrations.test.ts +379 -0
- package/src/__tests__/postgres-batch-compat.test.ts +133 -0
- package/src/__tests__/postgres-dialect.test.ts +328 -0
- package/src/__tests__/postgres-executor.test.ts +79 -0
- package/src/__tests__/postgres-field-ops-compat.test.ts +222 -0
- package/src/__tests__/postgres-schema-init.test.ts +105 -0
- package/src/__tests__/postgres-table-utils.test.ts +107 -0
- package/src/__tests__/presence.test.ts +199 -0
- package/src/__tests__/provider.test.ts +550 -0
- package/src/__tests__/public-user-profile.test.ts +339 -0
- package/src/__tests__/push-handlers.test.ts +179 -0
- package/src/__tests__/push-provider.test.ts +80 -0
- package/src/__tests__/push-token.test.ts +418 -0
- package/src/__tests__/query.test.ts +771 -0
- package/src/__tests__/rate-limit.test.ts +260 -0
- package/src/__tests__/room-access-policy.test.ts +101 -0
- package/src/__tests__/room-handler-context.test.ts +130 -0
- package/src/__tests__/room-monitoring.test.ts +138 -0
- package/src/__tests__/room-runtime-routing.test.ts +222 -0
- package/src/__tests__/room.test.ts +254 -0
- package/src/__tests__/route-parser.test.ts +490 -0
- package/src/__tests__/rules.test.ts +234 -0
- package/src/__tests__/runtime-surface-accounting.test.ts +120 -0
- package/src/__tests__/scheduled.test.ts +80 -0
- package/src/__tests__/schema.test.ts +1273 -0
- package/src/__tests__/security-hardening.test.ts +312 -0
- package/src/__tests__/server.unit.test.ts +333 -0
- package/src/__tests__/service-key-db-proxy.test.ts +650 -0
- package/src/__tests__/service-key-provider-bypass.test.ts +138 -0
- package/src/__tests__/service-key.test.ts +757 -0
- package/src/__tests__/smoke-skip-report.test.ts +72 -0
- package/src/__tests__/sms-provider.test.ts +39 -0
- package/src/__tests__/sql-route.test.ts +218 -0
- package/src/__tests__/storage-hook-context.test.ts +115 -0
- package/src/__tests__/totp.test.ts +200 -0
- package/src/__tests__/uuid.test.ts +144 -0
- package/src/__tests__/validation.test.ts +773 -0
- package/src/__tests__/websocket-pending.test.ts +163 -0
- package/src/_functions-registry.ts +51 -0
- package/src/bench-entry.ts +9 -0
- package/src/cloudflare-test.d.ts +1 -0
- package/src/durable-objects/auth-do.ts +49 -0
- package/src/durable-objects/database-do.ts +2240 -0
- package/src/durable-objects/database-live-do.ts +949 -0
- package/src/durable-objects/logs-do.ts +1200 -0
- package/src/durable-objects/room-runtime-base.ts +1604 -0
- package/src/durable-objects/rooms-do.ts +2191 -0
- package/src/generated-config.ts +6 -0
- package/src/index.ts +382 -0
- package/src/lib/admin-assets.ts +54 -0
- package/src/lib/admin-db-target.ts +301 -0
- package/src/lib/admin-routing.ts +35 -0
- package/src/lib/admin-user-management.ts +464 -0
- package/src/lib/analytics-adapter.ts +103 -0
- package/src/lib/analytics-query.ts +579 -0
- package/src/lib/auth-d1-service.ts +1193 -0
- package/src/lib/auth-d1.ts +1056 -0
- package/src/lib/auth-db-adapter.ts +289 -0
- package/src/lib/auth-redirect.ts +116 -0
- package/src/lib/cidr.ts +115 -0
- package/src/lib/client-ip.ts +51 -0
- package/src/lib/cloudflare-realtime.ts +251 -0
- package/src/lib/control-db.ts +36 -0
- package/src/lib/cron.ts +163 -0
- package/src/lib/d1-handler.ts +1425 -0
- package/src/lib/d1-schema-init.ts +255 -0
- package/src/lib/d1-sql.ts +33 -0
- package/src/lib/database-live-config.ts +24 -0
- package/src/lib/database-live-emitter.ts +111 -0
- package/src/lib/db-sql.ts +66 -0
- package/src/lib/do-retry.ts +36 -0
- package/src/lib/do-router.ts +270 -0
- package/src/lib/do-sql.ts +73 -0
- package/src/lib/email-provider.ts +379 -0
- package/src/lib/email-templates.ts +285 -0
- package/src/lib/email-translations.ts +422 -0
- package/src/lib/errors.ts +151 -0
- package/src/lib/functions.ts +2091 -0
- package/src/lib/hono.ts +56 -0
- package/src/lib/internal-request.ts +56 -0
- package/src/lib/jwt.ts +354 -0
- package/src/lib/log-writer.ts +272 -0
- package/src/lib/namespace-dump.ts +125 -0
- package/src/lib/oauth-providers.ts +1225 -0
- package/src/lib/op-parser.ts +99 -0
- package/src/lib/openapi.ts +146 -0
- package/src/lib/pagination.ts +19 -0
- package/src/lib/password-policy.ts +102 -0
- package/src/lib/password.ts +145 -0
- package/src/lib/plugin-migrations.ts +612 -0
- package/src/lib/postgres-executor.ts +203 -0
- package/src/lib/postgres-handler.ts +1102 -0
- package/src/lib/postgres-schema-init.ts +341 -0
- package/src/lib/postgres-table-utils.ts +87 -0
- package/src/lib/public-user-profile.ts +187 -0
- package/src/lib/push-provider.ts +409 -0
- package/src/lib/push-token.ts +294 -0
- package/src/lib/query-engine.ts +768 -0
- package/src/lib/room-monitoring.ts +97 -0
- package/src/lib/room-runtime.ts +14 -0
- package/src/lib/route-parser.ts +434 -0
- package/src/lib/schema.ts +538 -0
- package/src/lib/schemas.ts +152 -0
- package/src/lib/service-key.ts +419 -0
- package/src/lib/sms-provider.ts +230 -0
- package/src/lib/startup-config.ts +99 -0
- package/src/lib/totp.ts +242 -0
- package/src/lib/uuid.ts +87 -0
- package/src/lib/validation.ts +205 -0
- package/src/lib/version.ts +2 -0
- package/src/lib/websocket-pending.ts +40 -0
- package/src/middleware/auth.ts +169 -0
- package/src/middleware/captcha-verify.ts +217 -0
- package/src/middleware/cors.ts +159 -0
- package/src/middleware/error-handler.ts +54 -0
- package/src/middleware/internal-guard.ts +26 -0
- package/src/middleware/logger.ts +126 -0
- package/src/middleware/rate-limit.ts +283 -0
- package/src/middleware/rules.ts +475 -0
- package/src/routes/admin-auth.ts +447 -0
- package/src/routes/admin.ts +3501 -0
- package/src/routes/analytics-api.ts +290 -0
- package/src/routes/auth.ts +4222 -0
- package/src/routes/backup.ts +1466 -0
- package/src/routes/config.ts +53 -0
- package/src/routes/d1.ts +109 -0
- package/src/routes/database-live.ts +281 -0
- package/src/routes/functions.ts +155 -0
- package/src/routes/health.ts +32 -0
- package/src/routes/kv.ts +167 -0
- package/src/routes/oauth.ts +1055 -0
- package/src/routes/push.ts +1465 -0
- package/src/routes/room.ts +639 -0
- package/src/routes/schema-endpoint.ts +76 -0
- package/src/routes/sql.ts +176 -0
- package/src/routes/storage.ts +1674 -0
- package/src/routes/tables.ts +699 -0
- package/src/routes/users.ts +21 -0
- package/src/routes/vectorize.ts +372 -0
- package/src/types.ts +99 -0
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { materializeConfig, type EdgeBaseConfig } from '@edge-base/shared';
|
|
2
|
+
|
|
3
|
+
function hasConfigContent(config: unknown): config is EdgeBaseConfig {
|
|
4
|
+
return Boolean(
|
|
5
|
+
config &&
|
|
6
|
+
typeof config === 'object' &&
|
|
7
|
+
!Array.isArray(config) &&
|
|
8
|
+
Object.keys(config as Record<string, unknown>).length > 0,
|
|
9
|
+
);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function shouldPreferTestConfig(
|
|
13
|
+
processEnv: Record<string, string | undefined> | undefined,
|
|
14
|
+
): boolean {
|
|
15
|
+
if (!processEnv) {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (processEnv.EDGEBASE_USE_TEST_CONFIG === '1' || processEnv.EDGEBASE_USE_TEST_CONFIG === 'true') {
|
|
20
|
+
return true;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (processEnv.VITEST === '1' || processEnv.VITEST === 'true') {
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if ((processEnv.VITEST_WORKER_ID ?? '').trim().length > 0) {
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if ((processEnv.VITEST_POOL_ID ?? '').trim().length > 0) {
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return processEnv.NODE_ENV === 'test';
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function loadMaterializedTestConfig(
|
|
39
|
+
loadTestConfig: () => Promise<unknown>,
|
|
40
|
+
): Promise<EdgeBaseConfig | null> {
|
|
41
|
+
let resolvedConfig: unknown = null;
|
|
42
|
+
try {
|
|
43
|
+
const mod = await loadTestConfig();
|
|
44
|
+
resolvedConfig = (mod as { default?: unknown })?.default ?? mod;
|
|
45
|
+
} catch {
|
|
46
|
+
// Test-only config is optional in packaged/runtime environments.
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (!hasConfigContent(resolvedConfig)) {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return materializeConfig(resolvedConfig as EdgeBaseConfig);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function parseProcessEnvConfig(
|
|
57
|
+
processEnv: Record<string, string | undefined> | undefined,
|
|
58
|
+
): EdgeBaseConfig | null {
|
|
59
|
+
const rawConfig = processEnv?.EDGEBASE_CONFIG;
|
|
60
|
+
if (!rawConfig || rawConfig.trim().length === 0) {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
try {
|
|
65
|
+
const parsed = JSON.parse(rawConfig) as EdgeBaseConfig;
|
|
66
|
+
if (!hasConfigContent(parsed)) {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return materializeConfig(parsed);
|
|
71
|
+
} catch {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export async function resolveStartupConfig(
|
|
77
|
+
generatedConfig: unknown,
|
|
78
|
+
loadTestConfig: () => Promise<unknown>,
|
|
79
|
+
processEnv: Record<string, string | undefined> | undefined,
|
|
80
|
+
options?: { preferTestConfig?: boolean },
|
|
81
|
+
): Promise<EdgeBaseConfig | null> {
|
|
82
|
+
const processEnvConfig = parseProcessEnvConfig(processEnv);
|
|
83
|
+
if (processEnvConfig) {
|
|
84
|
+
return processEnvConfig;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (options?.preferTestConfig || shouldPreferTestConfig(processEnv)) {
|
|
88
|
+
const preferredTestConfig = await loadMaterializedTestConfig(loadTestConfig);
|
|
89
|
+
if (preferredTestConfig) {
|
|
90
|
+
return preferredTestConfig;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (hasConfigContent(generatedConfig)) {
|
|
95
|
+
return materializeConfig(generatedConfig as EdgeBaseConfig);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return loadMaterializedTestConfig(loadTestConfig);
|
|
99
|
+
}
|
package/src/lib/totp.ts
ADDED
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TOTP — RFC 6238 Time-Based One-Time Password implementation.
|
|
3
|
+
*
|
|
4
|
+
* Uses Web Crypto API (no Node.js dependencies) for Cloudflare Workers compatibility.
|
|
5
|
+
* Standard: 6-digit codes, 30-second step, SHA-1 HMAC.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// ─── Constants ───
|
|
9
|
+
|
|
10
|
+
const DIGITS = 6;
|
|
11
|
+
const STEP = 30; // seconds
|
|
12
|
+
const WINDOW = 1; // accept ±1 step (covers clock skew)
|
|
13
|
+
|
|
14
|
+
// ─── Secret Generation ───
|
|
15
|
+
|
|
16
|
+
/** Generate a random TOTP secret (20 bytes = 160 bits, standard for SHA-1). */
|
|
17
|
+
export function generateTOTPSecret(): string {
|
|
18
|
+
const bytes = new Uint8Array(20);
|
|
19
|
+
crypto.getRandomValues(bytes);
|
|
20
|
+
return base32Encode(bytes);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Generate an otpauth:// URI for QR code scanning. */
|
|
24
|
+
export function generateTOTPUri(
|
|
25
|
+
secret: string,
|
|
26
|
+
email: string,
|
|
27
|
+
issuer: string,
|
|
28
|
+
): string {
|
|
29
|
+
const encodedIssuer = encodeURIComponent(issuer);
|
|
30
|
+
const encodedEmail = encodeURIComponent(email);
|
|
31
|
+
return `otpauth://totp/${encodedIssuer}:${encodedEmail}?secret=${secret}&issuer=${encodedIssuer}&algorithm=SHA1&digits=${DIGITS}&period=${STEP}`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// ─── TOTP Verification ───
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Verify a TOTP code against the secret.
|
|
38
|
+
* Accepts codes within ±window steps (default: ±1 = 90 second window).
|
|
39
|
+
*/
|
|
40
|
+
export async function verifyTOTP(
|
|
41
|
+
secret: string,
|
|
42
|
+
code: string,
|
|
43
|
+
window: number = WINDOW,
|
|
44
|
+
): Promise<boolean> {
|
|
45
|
+
if (!code || code.length !== DIGITS) return false;
|
|
46
|
+
|
|
47
|
+
const now = Math.floor(Date.now() / 1000);
|
|
48
|
+
const counter = Math.floor(now / STEP);
|
|
49
|
+
|
|
50
|
+
for (let i = -window; i <= window; i++) {
|
|
51
|
+
const expected = await generateTOTPCode(secret, counter + i);
|
|
52
|
+
if (timingSafeEqual(code, expected)) {
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Generate a TOTP code for a given counter value. */
|
|
60
|
+
async function generateTOTPCode(secret: string, counter: number): Promise<string> {
|
|
61
|
+
const key = base32Decode(secret);
|
|
62
|
+
const counterBytes = new Uint8Array(8);
|
|
63
|
+
const view = new DataView(counterBytes.buffer);
|
|
64
|
+
view.setBigUint64(0, BigInt(counter));
|
|
65
|
+
|
|
66
|
+
const cryptoKey = await crypto.subtle.importKey(
|
|
67
|
+
'raw',
|
|
68
|
+
key as unknown as BufferSource,
|
|
69
|
+
{ name: 'HMAC', hash: 'SHA-1' },
|
|
70
|
+
false,
|
|
71
|
+
['sign'],
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
const hmac = new Uint8Array(
|
|
75
|
+
await crypto.subtle.sign('HMAC', cryptoKey, counterBytes),
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
// Dynamic truncation (RFC 4226 §5.4)
|
|
79
|
+
const offset = hmac[hmac.length - 1] & 0x0f;
|
|
80
|
+
const binary =
|
|
81
|
+
((hmac[offset] & 0x7f) << 24) |
|
|
82
|
+
((hmac[offset + 1] & 0xff) << 16) |
|
|
83
|
+
((hmac[offset + 2] & 0xff) << 8) |
|
|
84
|
+
(hmac[offset + 3] & 0xff);
|
|
85
|
+
|
|
86
|
+
const otp = binary % 10 ** DIGITS;
|
|
87
|
+
return otp.toString().padStart(DIGITS, '0');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ─── Recovery Codes ───
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Generate recovery codes (8 codes, 8 characters each).
|
|
94
|
+
* Returns plaintext codes. Caller must hash them before storage.
|
|
95
|
+
*/
|
|
96
|
+
export function generateRecoveryCodes(count: number = 8): string[] {
|
|
97
|
+
const codes: string[] = [];
|
|
98
|
+
const charset = 'abcdefghjkmnpqrstuvwxyz23456789'; // no ambiguous chars (0/o, 1/l/i)
|
|
99
|
+
for (let i = 0; i < count; i++) {
|
|
100
|
+
const bytes = new Uint8Array(8);
|
|
101
|
+
crypto.getRandomValues(bytes);
|
|
102
|
+
let code = '';
|
|
103
|
+
for (const b of bytes) {
|
|
104
|
+
code += charset[b % charset.length];
|
|
105
|
+
}
|
|
106
|
+
codes.push(code);
|
|
107
|
+
}
|
|
108
|
+
return codes;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// ─── Encryption (AES-GCM for secret storage) ───
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Encrypt the TOTP secret for storage using AES-256-GCM.
|
|
115
|
+
* @param plaintext The TOTP secret to encrypt
|
|
116
|
+
* @param keyMaterial The encryption key (e.g., JWT_USER_SECRET)
|
|
117
|
+
*/
|
|
118
|
+
export async function encryptSecret(plaintext: string, keyMaterial: string): Promise<string> {
|
|
119
|
+
const key = await deriveEncryptionKey(keyMaterial);
|
|
120
|
+
const iv = new Uint8Array(12);
|
|
121
|
+
crypto.getRandomValues(iv);
|
|
122
|
+
const encoded = new TextEncoder().encode(plaintext);
|
|
123
|
+
|
|
124
|
+
const ciphertext = new Uint8Array(
|
|
125
|
+
await crypto.subtle.encrypt({ name: 'AES-GCM', iv }, key, encoded),
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
// Format: base64(iv + ciphertext)
|
|
129
|
+
const combined = new Uint8Array(iv.length + ciphertext.length);
|
|
130
|
+
combined.set(iv);
|
|
131
|
+
combined.set(ciphertext, iv.length);
|
|
132
|
+
|
|
133
|
+
return uint8ArrayToBase64(combined);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Decrypt a stored TOTP secret.
|
|
138
|
+
* @param encrypted Base64-encoded (iv + ciphertext)
|
|
139
|
+
* @param keyMaterial The encryption key (e.g., JWT_USER_SECRET)
|
|
140
|
+
*/
|
|
141
|
+
export async function decryptSecret(encrypted: string, keyMaterial: string): Promise<string> {
|
|
142
|
+
const key = await deriveEncryptionKey(keyMaterial);
|
|
143
|
+
const combined = base64ToUint8Array(encrypted);
|
|
144
|
+
const iv = combined.slice(0, 12);
|
|
145
|
+
const ciphertext = combined.slice(12);
|
|
146
|
+
|
|
147
|
+
const decrypted = await crypto.subtle.decrypt(
|
|
148
|
+
{ name: 'AES-GCM', iv },
|
|
149
|
+
key,
|
|
150
|
+
ciphertext,
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
return new TextDecoder().decode(decrypted);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
async function deriveEncryptionKey(keyMaterial: string): Promise<CryptoKey> {
|
|
157
|
+
const raw = new TextEncoder().encode(keyMaterial);
|
|
158
|
+
const hash = await crypto.subtle.digest('SHA-256', raw);
|
|
159
|
+
return crypto.subtle.importKey(
|
|
160
|
+
'raw',
|
|
161
|
+
hash,
|
|
162
|
+
{ name: 'AES-GCM' },
|
|
163
|
+
false,
|
|
164
|
+
['encrypt', 'decrypt'],
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// ─── Utilities ───
|
|
169
|
+
|
|
170
|
+
/** Timing-safe string comparison to prevent timing attacks. */
|
|
171
|
+
function timingSafeEqual(a: string, b: string): boolean {
|
|
172
|
+
if (a.length !== b.length) return false;
|
|
173
|
+
let result = 0;
|
|
174
|
+
for (let i = 0; i < a.length; i++) {
|
|
175
|
+
result |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
|
176
|
+
}
|
|
177
|
+
return result === 0;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// ─── Base32 ───
|
|
181
|
+
|
|
182
|
+
const BASE32_CHARS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
|
|
183
|
+
|
|
184
|
+
function base32Encode(data: Uint8Array): string {
|
|
185
|
+
let result = '';
|
|
186
|
+
let bits = 0;
|
|
187
|
+
let buffer = 0;
|
|
188
|
+
|
|
189
|
+
for (const byte of data) {
|
|
190
|
+
buffer = (buffer << 8) | byte;
|
|
191
|
+
bits += 8;
|
|
192
|
+
while (bits >= 5) {
|
|
193
|
+
bits -= 5;
|
|
194
|
+
result += BASE32_CHARS[(buffer >> bits) & 0x1f];
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (bits > 0) {
|
|
199
|
+
result += BASE32_CHARS[(buffer << (5 - bits)) & 0x1f];
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return result;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function base32Decode(encoded: string): Uint8Array {
|
|
206
|
+
const cleaned = encoded.toUpperCase().replace(/[^A-Z2-7]/g, '');
|
|
207
|
+
const bytes: number[] = [];
|
|
208
|
+
let bits = 0;
|
|
209
|
+
let buffer = 0;
|
|
210
|
+
|
|
211
|
+
for (const char of cleaned) {
|
|
212
|
+
const val = BASE32_CHARS.indexOf(char);
|
|
213
|
+
if (val === -1) continue;
|
|
214
|
+
buffer = (buffer << 5) | val;
|
|
215
|
+
bits += 5;
|
|
216
|
+
if (bits >= 8) {
|
|
217
|
+
bits -= 8;
|
|
218
|
+
bytes.push((buffer >> bits) & 0xff);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return new Uint8Array(bytes);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// ─── Base64 ───
|
|
226
|
+
|
|
227
|
+
function uint8ArrayToBase64(data: Uint8Array): string {
|
|
228
|
+
let binary = '';
|
|
229
|
+
for (const byte of data) {
|
|
230
|
+
binary += String.fromCharCode(byte);
|
|
231
|
+
}
|
|
232
|
+
return btoa(binary);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function base64ToUint8Array(str: string): Uint8Array {
|
|
236
|
+
const binary = atob(str);
|
|
237
|
+
const bytes = new Uint8Array(binary.length);
|
|
238
|
+
for (let i = 0; i < binary.length; i++) {
|
|
239
|
+
bytes[i] = binary.charCodeAt(i);
|
|
240
|
+
}
|
|
241
|
+
return bytes;
|
|
242
|
+
}
|
package/src/lib/uuid.ts
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* UUID v7 Monotonic generation utility.
|
|
3
|
+
* Manual implementation using crypto.getRandomValues() — no external dependency.
|
|
4
|
+
* RFC 9562 compliant: 48-bit unix_ts_ms + 4-bit version(7) + 12-bit rand_a + 2-bit var(10) + 62-bit rand_b.
|
|
5
|
+
* Time-ordered for natural cursor pagination on PK.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const HEX = '0123456789abcdef';
|
|
9
|
+
let lastTimestamp = -1;
|
|
10
|
+
let lastBytes: Uint8Array | null = null;
|
|
11
|
+
|
|
12
|
+
function bytesToHex(bytes: Uint8Array): string {
|
|
13
|
+
let hex = '';
|
|
14
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
15
|
+
hex += HEX[bytes[i] >> 4] + HEX[bytes[i] & 0x0f];
|
|
16
|
+
}
|
|
17
|
+
return hex;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function writeTimestamp(bytes: Uint8Array, timestamp: number): void {
|
|
21
|
+
bytes[0] = (timestamp / 2 ** 40) & 0xff;
|
|
22
|
+
bytes[1] = (timestamp / 2 ** 32) & 0xff;
|
|
23
|
+
bytes[2] = (timestamp / 2 ** 24) & 0xff;
|
|
24
|
+
bytes[3] = (timestamp / 2 ** 16) & 0xff;
|
|
25
|
+
bytes[4] = (timestamp / 2 ** 8) & 0xff;
|
|
26
|
+
bytes[5] = timestamp & 0xff;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function createRandomUuidBytes(timestamp: number): Uint8Array {
|
|
30
|
+
const bytes = new Uint8Array(16);
|
|
31
|
+
crypto.getRandomValues(bytes);
|
|
32
|
+
writeTimestamp(bytes, timestamp);
|
|
33
|
+
|
|
34
|
+
// version: 4 bits = 0111 (7)
|
|
35
|
+
bytes[6] = (bytes[6] & 0x0f) | 0x70;
|
|
36
|
+
|
|
37
|
+
// variant: 2 bits = 10
|
|
38
|
+
bytes[8] = (bytes[8] & 0x3f) | 0x80;
|
|
39
|
+
|
|
40
|
+
return bytes;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function incrementMonotonicTail(bytes: Uint8Array): boolean {
|
|
44
|
+
for (let i = 15; i >= 6; i--) {
|
|
45
|
+
const mask = i === 6 ? 0x0f : i === 8 ? 0x3f : 0xff;
|
|
46
|
+
const fixedBits = i === 6 ? 0x70 : i === 8 ? 0x80 : 0x00;
|
|
47
|
+
const value = bytes[i] & mask;
|
|
48
|
+
|
|
49
|
+
if (value < mask) {
|
|
50
|
+
bytes[i] = fixedBits | (value + 1);
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
bytes[i] = fixedBits;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Generate a UUID v7 string.
|
|
62
|
+
* Format: xxxxxxxx-xxxx-7xxx-yxxx-xxxxxxxxxxxx
|
|
63
|
+
* where y is one of [8, 9, a, b]
|
|
64
|
+
*/
|
|
65
|
+
export function generateId(): string {
|
|
66
|
+
let timestamp = Date.now();
|
|
67
|
+
if (timestamp < lastTimestamp) {
|
|
68
|
+
timestamp = lastTimestamp;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
let bytes: Uint8Array;
|
|
72
|
+
if (lastBytes !== null && timestamp === lastTimestamp) {
|
|
73
|
+
bytes = lastBytes.slice();
|
|
74
|
+
if (!incrementMonotonicTail(bytes)) {
|
|
75
|
+
timestamp = lastTimestamp + 1;
|
|
76
|
+
bytes = createRandomUuidBytes(timestamp);
|
|
77
|
+
}
|
|
78
|
+
} else {
|
|
79
|
+
bytes = createRandomUuidBytes(timestamp);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
lastTimestamp = timestamp;
|
|
83
|
+
lastBytes = bytes;
|
|
84
|
+
|
|
85
|
+
const hex = bytesToHex(bytes);
|
|
86
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
87
|
+
}
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Field validation engine for table schemas.
|
|
3
|
+
* Validates request body against schema field definitions.
|
|
4
|
+
*/
|
|
5
|
+
import type { SchemaField } from '@edge-base/shared';
|
|
6
|
+
import { buildEffectiveSchema } from './schema.js';
|
|
7
|
+
|
|
8
|
+
export interface ValidationResult {
|
|
9
|
+
valid: boolean;
|
|
10
|
+
errors: Record<string, string>;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const CUSTOM_RECORD_ID_PATTERN = /^[A-Za-z0-9_-]+$/;
|
|
14
|
+
export const CUSTOM_RECORD_ID_MESSAGE =
|
|
15
|
+
'Record ID must use English letters, numbers, hyphen (-), or underscore (_).';
|
|
16
|
+
|
|
17
|
+
export function validateCustomRecordId(value: unknown): string | null {
|
|
18
|
+
if (value === undefined || value === null || value === '') return null;
|
|
19
|
+
if (typeof value !== 'string') return 'Record ID must be a string.';
|
|
20
|
+
return CUSTOM_RECORD_ID_PATTERN.test(value) ? null : CUSTOM_RECORD_ID_MESSAGE;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function summarizeValidationErrors(errors: Record<string, string>): string {
|
|
24
|
+
const entries = Object.entries(errors);
|
|
25
|
+
if (entries.length === 0) return 'Validation failed.';
|
|
26
|
+
|
|
27
|
+
const [field, message] = entries[0];
|
|
28
|
+
const label = field === 'id' ? 'record ID' : `'${field}'`;
|
|
29
|
+
|
|
30
|
+
if (entries.length === 1) {
|
|
31
|
+
return `Invalid ${label}. ${message}`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return `Request body failed validation. First issue: invalid ${label}. ${message}`;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Validate a record against a table schema for insert operations.
|
|
39
|
+
* Checks required fields, types, constraints (min/max/pattern/enum).
|
|
40
|
+
* When schema is undefined (schemaless,), all fields are accepted.
|
|
41
|
+
*/
|
|
42
|
+
export function validateInsert(
|
|
43
|
+
data: Record<string, unknown>,
|
|
44
|
+
schema?: Record<string, SchemaField | false>,
|
|
45
|
+
): ValidationResult {
|
|
46
|
+
// Schemaless: accept everything
|
|
47
|
+
if (!schema) return { valid: true, errors: {} };
|
|
48
|
+
|
|
49
|
+
const effective = buildEffectiveSchema(schema);
|
|
50
|
+
const errors: Record<string, string> = {};
|
|
51
|
+
const idError = validateCustomRecordId(data.id);
|
|
52
|
+
if (idError) {
|
|
53
|
+
errors.id = idError;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
for (const [name, field] of Object.entries(effective)) {
|
|
57
|
+
// Skip auto-managed fields
|
|
58
|
+
if (name === 'id' || name === 'createdAt' || name === 'updatedAt') continue;
|
|
59
|
+
|
|
60
|
+
const value = data[name];
|
|
61
|
+
|
|
62
|
+
// Check required
|
|
63
|
+
if (field.required && (value === undefined || value === null)) {
|
|
64
|
+
if (field.default === undefined) {
|
|
65
|
+
errors[name] = 'Field is required.';
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Skip validation if value is absent (optional field)
|
|
71
|
+
if (value === undefined || value === null) continue;
|
|
72
|
+
|
|
73
|
+
// Validate type and constraints
|
|
74
|
+
const typeErr = validateType(value, field);
|
|
75
|
+
if (typeErr) {
|
|
76
|
+
errors[name] = typeErr;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Unknown fields are silently ignored — the SQL layer filters them out
|
|
81
|
+
// (schema-defined: columns = Object.keys(record).filter(k => k in effective)).
|
|
82
|
+
// Rejecting unknown fields here would break SDK payloads that send extra metadata.
|
|
83
|
+
|
|
84
|
+
return { valid: Object.keys(errors).length === 0, errors };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Validate a record against a table schema for update operations.
|
|
89
|
+
* Partial validation — only checks provided fields.
|
|
90
|
+
* When schema is undefined (schemaless,), all fields are accepted.
|
|
91
|
+
*/
|
|
92
|
+
export function validateUpdate(
|
|
93
|
+
data: Record<string, unknown>,
|
|
94
|
+
schema?: Record<string, SchemaField | false>,
|
|
95
|
+
): ValidationResult {
|
|
96
|
+
// Schemaless: accept everything
|
|
97
|
+
if (!schema) return { valid: true, errors: {} };
|
|
98
|
+
|
|
99
|
+
const effective = buildEffectiveSchema(schema);
|
|
100
|
+
const errors: Record<string, string> = {};
|
|
101
|
+
|
|
102
|
+
for (const [name, value] of Object.entries(data)) {
|
|
103
|
+
// Skip auto-managed fields
|
|
104
|
+
if (name === 'id' || name === 'createdAt' || name === 'updatedAt') continue;
|
|
105
|
+
|
|
106
|
+
const field = effective[name];
|
|
107
|
+
if (!field) continue;
|
|
108
|
+
|
|
109
|
+
if (isFieldOperator(value)) {
|
|
110
|
+
if (value.$op === 'deleteField' && field.required) {
|
|
111
|
+
errors[name] = 'Field is required and cannot be deleted.';
|
|
112
|
+
}
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Check required (can't set to null if required)
|
|
117
|
+
if (field.required && (value === null || value === undefined)) {
|
|
118
|
+
errors[name] = 'Field is required and cannot be null.';
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (value === null || value === undefined) continue;
|
|
123
|
+
|
|
124
|
+
const typeErr = validateType(value, field);
|
|
125
|
+
if (typeErr) {
|
|
126
|
+
errors[name] = typeErr;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return { valid: Object.keys(errors).length === 0, errors };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// ─── Type & Constraint Validation ───
|
|
134
|
+
|
|
135
|
+
function validateType(value: unknown, field: SchemaField): string | null {
|
|
136
|
+
switch (field.type) {
|
|
137
|
+
case 'string':
|
|
138
|
+
case 'text':
|
|
139
|
+
if (typeof value !== 'string') return 'Must be a string.';
|
|
140
|
+
return validateStringConstraints(value, field);
|
|
141
|
+
|
|
142
|
+
case 'number':
|
|
143
|
+
if (typeof value !== 'number' || Number.isNaN(value)) return 'Must be a number.';
|
|
144
|
+
return validateNumberConstraints(value, field);
|
|
145
|
+
|
|
146
|
+
case 'boolean':
|
|
147
|
+
if (typeof value !== 'boolean') return 'Must be a boolean.';
|
|
148
|
+
return null;
|
|
149
|
+
|
|
150
|
+
case 'datetime':
|
|
151
|
+
if (typeof value !== 'string') return 'Must be a datetime string.';
|
|
152
|
+
if (Number.isNaN(Date.parse(value))) return 'Invalid datetime format.';
|
|
153
|
+
return null;
|
|
154
|
+
|
|
155
|
+
case 'json':
|
|
156
|
+
// JSON fields accept any serializable value
|
|
157
|
+
return null;
|
|
158
|
+
|
|
159
|
+
default:
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function validateStringConstraints(value: string, field: SchemaField): string | null {
|
|
165
|
+
if (field.min !== undefined && value.length < field.min) {
|
|
166
|
+
return `Must be at least ${field.min} characters.`;
|
|
167
|
+
}
|
|
168
|
+
if (field.max !== undefined && value.length > field.max) {
|
|
169
|
+
return `Must be at most ${field.max} characters.`;
|
|
170
|
+
}
|
|
171
|
+
if (field.pattern !== undefined) {
|
|
172
|
+
const regex = new RegExp(field.pattern);
|
|
173
|
+
if (!regex.test(value)) return `Must match pattern: ${field.pattern}`;
|
|
174
|
+
}
|
|
175
|
+
if (field.enum !== undefined && !field.enum.includes(value)) {
|
|
176
|
+
return `Must be one of: ${field.enum.join(', ')}`;
|
|
177
|
+
}
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function validateNumberConstraints(value: number, field: SchemaField): string | null {
|
|
182
|
+
if (field.min !== undefined && value < field.min) {
|
|
183
|
+
return `Must be at least ${field.min}.`;
|
|
184
|
+
}
|
|
185
|
+
if (field.max !== undefined && value > field.max) {
|
|
186
|
+
return `Must be at most ${field.max}.`;
|
|
187
|
+
}
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// ─── Field Operator Detection ───
|
|
192
|
+
|
|
193
|
+
export interface FieldOperator {
|
|
194
|
+
$op: 'increment' | 'deleteField';
|
|
195
|
+
value?: number;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export function isFieldOperator(value: unknown): value is FieldOperator {
|
|
199
|
+
return (
|
|
200
|
+
typeof value === 'object' &&
|
|
201
|
+
value !== null &&
|
|
202
|
+
'$op' in value &&
|
|
203
|
+
typeof (value as FieldOperator).$op === 'string'
|
|
204
|
+
);
|
|
205
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
function parsePendingCount(value: string | null): number {
|
|
2
|
+
const parsed = Number.parseInt(value ?? '', 10);
|
|
3
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : 0;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export async function getPendingWebSocketCount(
|
|
7
|
+
kv: KVNamespace,
|
|
8
|
+
key: string,
|
|
9
|
+
): Promise<number> {
|
|
10
|
+
return parsePendingCount(await kv.get(key));
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export async function acquirePendingWebSocketSlot(
|
|
14
|
+
kv: KVNamespace,
|
|
15
|
+
key: string,
|
|
16
|
+
maxPending: number,
|
|
17
|
+
ttlSeconds: number,
|
|
18
|
+
): Promise<boolean> {
|
|
19
|
+
const current = parsePendingCount(await kv.get(key));
|
|
20
|
+
if (current >= maxPending) {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
await kv.put(key, String(current + 1), { expirationTtl: ttlSeconds });
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function releasePendingWebSocketSlot(
|
|
29
|
+
kv: KVNamespace,
|
|
30
|
+
key: string,
|
|
31
|
+
ttlSeconds: number,
|
|
32
|
+
): Promise<void> {
|
|
33
|
+
const current = parsePendingCount(await kv.get(key));
|
|
34
|
+
if (current <= 1) {
|
|
35
|
+
await kv.delete(key);
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
await kv.put(key, String(current - 1), { expirationTtl: ttlSeconds });
|
|
40
|
+
}
|