@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
|
+
/**
|
|
2
|
+
* $op field operator parser for PATCH update requests.
|
|
3
|
+
* Converts field operators to SQL SET clause fragments.
|
|
4
|
+
*
|
|
5
|
+
* Supported operators:
|
|
6
|
+
* - increment(n): field = COALESCE(field, 0) + n
|
|
7
|
+
* - deleteField(): field = NULL
|
|
8
|
+
*/
|
|
9
|
+
import type { FieldOperator } from './validation.js';
|
|
10
|
+
import { EdgeBaseError } from '@edge-base/shared';
|
|
11
|
+
|
|
12
|
+
interface SetClause {
|
|
13
|
+
/** SQL fragment for SET clause, e.g. "viewCount" = COALESCE("viewCount", 0) + ? */
|
|
14
|
+
sql: string;
|
|
15
|
+
/** Parameters for the SQL fragment */
|
|
16
|
+
params: unknown[];
|
|
17
|
+
/** Next placeholder index after consuming this clause's params */
|
|
18
|
+
nextParamIndex: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface ParseUpdateBodyOptions {
|
|
22
|
+
dialect?: 'sqlite' | 'postgres';
|
|
23
|
+
startIndex?: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Parse update body into SQL SET clause parts.
|
|
28
|
+
* Handles both regular values and $op field operators.
|
|
29
|
+
* Returns { setClauses, params } ready for UPDATE statement.
|
|
30
|
+
*/
|
|
31
|
+
export function parseUpdateBody(
|
|
32
|
+
data: Record<string, unknown>,
|
|
33
|
+
excludeFields: string[] = ['id'],
|
|
34
|
+
options: ParseUpdateBodyOptions = {},
|
|
35
|
+
): { setClauses: string[]; params: unknown[]; nextParamIndex: number } {
|
|
36
|
+
const setClauses: string[] = [];
|
|
37
|
+
const params: unknown[] = [];
|
|
38
|
+
const dialect = options.dialect ?? 'sqlite';
|
|
39
|
+
let paramIndex = options.startIndex ?? 1;
|
|
40
|
+
|
|
41
|
+
for (const [key, value] of Object.entries(data)) {
|
|
42
|
+
if (excludeFields.includes(key)) continue;
|
|
43
|
+
|
|
44
|
+
if (isOpObject(value)) {
|
|
45
|
+
const clause = buildOpClause(key, value as FieldOperator, dialect, paramIndex);
|
|
46
|
+
setClauses.push(clause.sql);
|
|
47
|
+
params.push(...clause.params);
|
|
48
|
+
paramIndex = clause.nextParamIndex;
|
|
49
|
+
} else {
|
|
50
|
+
setClauses.push(`${esc(key)} = ${placeholderFor(dialect, paramIndex)}`);
|
|
51
|
+
params.push(value);
|
|
52
|
+
paramIndex++;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return { setClauses, params, nextParamIndex: paramIndex };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function buildOpClause(
|
|
60
|
+
field: string,
|
|
61
|
+
op: FieldOperator,
|
|
62
|
+
dialect: 'sqlite' | 'postgres',
|
|
63
|
+
paramIndex: number,
|
|
64
|
+
): SetClause {
|
|
65
|
+
switch (op.$op) {
|
|
66
|
+
case 'increment':
|
|
67
|
+
return {
|
|
68
|
+
sql: `${esc(field)} = COALESCE(${esc(field)}, 0) + ${placeholderFor(dialect, paramIndex)}`,
|
|
69
|
+
params: [op.value ?? 0],
|
|
70
|
+
nextParamIndex: paramIndex + 1,
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
case 'deleteField':
|
|
74
|
+
return {
|
|
75
|
+
sql: `${esc(field)} = NULL`,
|
|
76
|
+
params: [],
|
|
77
|
+
nextParamIndex: paramIndex,
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
default:
|
|
81
|
+
throw new EdgeBaseError(400, `Unknown field operator '${(op as FieldOperator).$op}'. Supported operators: increment, deleteField.`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function placeholderFor(dialect: 'sqlite' | 'postgres', paramIndex: number): string {
|
|
86
|
+
return dialect === 'postgres' ? `$${paramIndex}` : '?';
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function isOpObject(value: unknown): boolean {
|
|
90
|
+
return (
|
|
91
|
+
typeof value === 'object' &&
|
|
92
|
+
value !== null &&
|
|
93
|
+
'$op' in value
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function esc(name: string): string {
|
|
98
|
+
return `"${name.replace(/"/g, '""')}"`;
|
|
99
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
type OpenApiSecurityRequirement = Record<string, string[]>;
|
|
2
|
+
type OpenApiOperation = Record<string, unknown> & {
|
|
3
|
+
security?: OpenApiSecurityRequirement[];
|
|
4
|
+
};
|
|
5
|
+
type OpenApiPathItem = Record<string, OpenApiOperation | unknown>;
|
|
6
|
+
export type OpenApiSpec = {
|
|
7
|
+
servers?: Array<{ url: string; description?: string }>;
|
|
8
|
+
components?: object & {
|
|
9
|
+
securitySchemes?: Record<string, Record<string, unknown>>;
|
|
10
|
+
};
|
|
11
|
+
paths?: Record<string, OpenApiPathItem>;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const HTTP_METHODS = new Set(['get', 'post', 'put', 'patch', 'delete', 'head', 'options']);
|
|
15
|
+
|
|
16
|
+
const ADMIN_PUBLIC_PATHS = new Set([
|
|
17
|
+
'/admin/api/setup',
|
|
18
|
+
'/admin/api/setup/status',
|
|
19
|
+
'/admin/api/auth/login',
|
|
20
|
+
'/admin/api/auth/refresh',
|
|
21
|
+
]);
|
|
22
|
+
|
|
23
|
+
const USER_BEARER_PATHS = new Set([
|
|
24
|
+
'/api/auth/link/phone',
|
|
25
|
+
'/api/auth/verify-link-phone',
|
|
26
|
+
'/api/auth/mfa/totp/enroll',
|
|
27
|
+
'/api/auth/mfa/totp/verify',
|
|
28
|
+
'/api/auth/mfa/totp',
|
|
29
|
+
'/api/auth/mfa/factors',
|
|
30
|
+
'/api/auth/change-password',
|
|
31
|
+
'/api/auth/change-email',
|
|
32
|
+
'/api/auth/passkeys/register-options',
|
|
33
|
+
'/api/auth/passkeys/register',
|
|
34
|
+
'/api/auth/passkeys',
|
|
35
|
+
'/api/auth/passkeys/{credentialId}',
|
|
36
|
+
'/api/auth/me',
|
|
37
|
+
'/api/auth/profile',
|
|
38
|
+
'/api/auth/sessions',
|
|
39
|
+
'/api/auth/sessions/{id}',
|
|
40
|
+
'/api/auth/identities',
|
|
41
|
+
'/api/auth/identities/{identityId}',
|
|
42
|
+
'/api/auth/link/email',
|
|
43
|
+
'/api/auth/oauth/link/{provider}',
|
|
44
|
+
'/api/push/register',
|
|
45
|
+
'/api/push/unregister',
|
|
46
|
+
'/api/push/topic/subscribe',
|
|
47
|
+
'/api/push/topic/unsubscribe',
|
|
48
|
+
]);
|
|
49
|
+
|
|
50
|
+
const USER_BEARER_PREFIXES = [
|
|
51
|
+
'/api/room/media/realtime/',
|
|
52
|
+
];
|
|
53
|
+
|
|
54
|
+
const SERVICE_KEY_ONLY_PATHS = new Set([
|
|
55
|
+
'/api/db/broadcast',
|
|
56
|
+
'/api/sql',
|
|
57
|
+
'/api/push/send',
|
|
58
|
+
'/api/push/send-many',
|
|
59
|
+
'/api/push/send-to-token',
|
|
60
|
+
'/api/push/send-to-topic',
|
|
61
|
+
'/api/push/broadcast',
|
|
62
|
+
'/api/push/logs',
|
|
63
|
+
'/api/push/tokens',
|
|
64
|
+
'/api/analytics/query',
|
|
65
|
+
'/api/analytics/events',
|
|
66
|
+
]);
|
|
67
|
+
|
|
68
|
+
const SERVICE_KEY_ONLY_PREFIXES = [
|
|
69
|
+
'/admin/api/internal/',
|
|
70
|
+
'/admin/api/backup/',
|
|
71
|
+
'/api/auth/admin/',
|
|
72
|
+
'/api/kv/',
|
|
73
|
+
'/api/d1/',
|
|
74
|
+
'/api/vectorize/',
|
|
75
|
+
];
|
|
76
|
+
|
|
77
|
+
function isOperation(value: unknown): value is OpenApiOperation {
|
|
78
|
+
return !!value && typeof value === 'object' && !Array.isArray(value);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function hasPrefix(path: string, prefixes: string[]): boolean {
|
|
82
|
+
return prefixes.some((prefix) => path.startsWith(prefix));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function getSecurityForPath(path: string): OpenApiSecurityRequirement[] | undefined {
|
|
86
|
+
if (ADMIN_PUBLIC_PATHS.has(path)) return undefined;
|
|
87
|
+
|
|
88
|
+
if (SERVICE_KEY_ONLY_PATHS.has(path) || hasPrefix(path, SERVICE_KEY_ONLY_PREFIXES)) {
|
|
89
|
+
return [{ serviceKeyAuth: [] }];
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (path.startsWith('/admin/api/')) {
|
|
93
|
+
return [{ adminBearerAuth: [] }, { serviceKeyAuth: [] }];
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (USER_BEARER_PATHS.has(path) || hasPrefix(path, USER_BEARER_PREFIXES)) {
|
|
97
|
+
return [{ userBearerAuth: [] }];
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return undefined;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function normalizeOpenApiDocument(spec: OpenApiSpec, origin?: string): OpenApiSpec {
|
|
104
|
+
if (origin) {
|
|
105
|
+
spec.servers = [
|
|
106
|
+
{
|
|
107
|
+
url: origin,
|
|
108
|
+
description: 'Current EdgeBase instance',
|
|
109
|
+
},
|
|
110
|
+
];
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const components = (spec.components ??= {});
|
|
114
|
+
const securitySchemes = (components.securitySchemes ??= {});
|
|
115
|
+
|
|
116
|
+
securitySchemes.adminBearerAuth ??= {
|
|
117
|
+
type: 'http',
|
|
118
|
+
scheme: 'bearer',
|
|
119
|
+
bearerFormat: 'JWT',
|
|
120
|
+
description: 'Admin JWT used by the Admin Dashboard.',
|
|
121
|
+
};
|
|
122
|
+
securitySchemes.userBearerAuth ??= {
|
|
123
|
+
type: 'http',
|
|
124
|
+
scheme: 'bearer',
|
|
125
|
+
bearerFormat: 'JWT',
|
|
126
|
+
description: 'User access token for authenticated client endpoints.',
|
|
127
|
+
};
|
|
128
|
+
securitySchemes.serviceKeyAuth ??= {
|
|
129
|
+
type: 'apiKey',
|
|
130
|
+
in: 'header',
|
|
131
|
+
name: 'X-EdgeBase-Service-Key',
|
|
132
|
+
description: 'Scoped service key for internal or server-side operations.',
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
for (const [path, pathItem] of Object.entries(spec.paths ?? {})) {
|
|
136
|
+
const security = getSecurityForPath(path);
|
|
137
|
+
if (!security) continue;
|
|
138
|
+
|
|
139
|
+
for (const [method, operation] of Object.entries(pathItem)) {
|
|
140
|
+
if (!HTTP_METHODS.has(method) || !isOperation(operation) || operation.security) continue;
|
|
141
|
+
operation.security = security;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return spec;
|
|
146
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pagination parameter validation.
|
|
3
|
+
*
|
|
4
|
+
* Clamps limit to 1..100 (default 20) and offset to ≥0 (default 0).
|
|
5
|
+
* Handles NaN, negative, and absurdly large values safely.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export function parsePagination(
|
|
9
|
+
limitParam: string | undefined,
|
|
10
|
+
offsetParam: string | undefined,
|
|
11
|
+
): { limit: number; offset: number } {
|
|
12
|
+
const rawLimit = parseInt(limitParam || '20', 10);
|
|
13
|
+
const limit = Number.isFinite(rawLimit) && rawLimit > 0 ? Math.min(rawLimit, 100) : 20;
|
|
14
|
+
|
|
15
|
+
const rawOffset = parseInt(offsetParam || '0', 10);
|
|
16
|
+
const offset = Number.isFinite(rawOffset) && rawOffset >= 0 ? rawOffset : 0;
|
|
17
|
+
|
|
18
|
+
return { limit, offset };
|
|
19
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Password Policy — validates passwords against configurable rules + HIBP k-anonymity check.
|
|
3
|
+
*
|
|
4
|
+
* HIBP: Uses k-anonymity (first 5 chars of SHA-1 hash) to check if password has been leaked.
|
|
5
|
+
* Fail-open: If the HIBP API is unavailable, the password is accepted (no false negatives).
|
|
6
|
+
*/
|
|
7
|
+
import type { PasswordPolicyConfig } from '@edge-base/shared';
|
|
8
|
+
|
|
9
|
+
export interface PasswordValidationResult {
|
|
10
|
+
valid: boolean;
|
|
11
|
+
errors: string[];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Validate a password against the configured policy.
|
|
16
|
+
* Returns { valid: true } if all checks pass, or { valid: false, errors: [...] } with all failures.
|
|
17
|
+
*/
|
|
18
|
+
export async function validatePassword(
|
|
19
|
+
password: string,
|
|
20
|
+
policy?: PasswordPolicyConfig,
|
|
21
|
+
): Promise<PasswordValidationResult> {
|
|
22
|
+
const errors: string[] = [];
|
|
23
|
+
|
|
24
|
+
// Default minLength is 8 regardless of policy config
|
|
25
|
+
const minLength = policy?.minLength ?? 8;
|
|
26
|
+
|
|
27
|
+
if (password.length < minLength) {
|
|
28
|
+
errors.push(`Password must be at least ${minLength} characters.`);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (policy?.requireUppercase && !/[A-Z]/.test(password)) {
|
|
32
|
+
errors.push('Password must contain at least one uppercase letter.');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (policy?.requireLowercase && !/[a-z]/.test(password)) {
|
|
36
|
+
errors.push('Password must contain at least one lowercase letter.');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (policy?.requireNumber && !/\d/.test(password)) {
|
|
40
|
+
errors.push('Password must contain at least one digit.');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (policy?.requireSpecial && !/[^A-Za-z0-9]/.test(password)) {
|
|
44
|
+
errors.push('Password must contain at least one special character.');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// HIBP check (k-anonymity, fail-open)
|
|
48
|
+
if (policy?.checkLeaked && errors.length === 0) {
|
|
49
|
+
const leaked = await checkHIBP(password);
|
|
50
|
+
if (leaked) {
|
|
51
|
+
errors.push('This password has been found in a data breach. Please choose a different password.');
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return { valid: errors.length === 0, errors };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Check if a password has been leaked using HIBP's k-anonymity API.
|
|
60
|
+
* Returns true if the password is found in the breach database.
|
|
61
|
+
* Fail-open: returns false on any error (network, timeout, etc.).
|
|
62
|
+
*/
|
|
63
|
+
async function checkHIBP(password: string): Promise<boolean> {
|
|
64
|
+
try {
|
|
65
|
+
// SHA-1 hash the password
|
|
66
|
+
const encoder = new TextEncoder();
|
|
67
|
+
const data = encoder.encode(password);
|
|
68
|
+
const hashBuffer = await crypto.subtle.digest('SHA-1', data);
|
|
69
|
+
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
|
70
|
+
const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('').toUpperCase();
|
|
71
|
+
|
|
72
|
+
const prefix = hashHex.substring(0, 5);
|
|
73
|
+
const suffix = hashHex.substring(5);
|
|
74
|
+
|
|
75
|
+
// Fetch range from HIBP API (k-anonymity: only send first 5 chars)
|
|
76
|
+
const controller = new AbortController();
|
|
77
|
+
const timeout = setTimeout(() => controller.abort(), 3000); // 3s timeout
|
|
78
|
+
|
|
79
|
+
const resp = await fetch(`https://api.pwnedpasswords.com/range/${prefix}`, {
|
|
80
|
+
signal: controller.signal,
|
|
81
|
+
headers: { 'User-Agent': 'EdgeBase-PasswordPolicy' },
|
|
82
|
+
});
|
|
83
|
+
clearTimeout(timeout);
|
|
84
|
+
|
|
85
|
+
if (!resp.ok) return false; // fail-open
|
|
86
|
+
|
|
87
|
+
const text = await resp.text();
|
|
88
|
+
// Each line is "SUFFIX:COUNT"
|
|
89
|
+
const lines = text.split('\n');
|
|
90
|
+
for (const line of lines) {
|
|
91
|
+
const [lineSuffix] = line.trim().split(':');
|
|
92
|
+
if (lineSuffix === suffix) {
|
|
93
|
+
return true; // password found in breach
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return false;
|
|
98
|
+
} catch {
|
|
99
|
+
// Fail-open: network error, timeout, etc.
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Password hashing — PBKDF2-SHA256 via Web Crypto API
|
|
3
|
+
*
|
|
4
|
+
* Workers-compatible (no native modules needed).
|
|
5
|
+
* Format: `pbkdf2:sha256:100000:{salt_base64}:{hash_base64}`
|
|
6
|
+
*
|
|
7
|
+
* Also supports verifying imported password hashes:
|
|
8
|
+
* - bcrypt ($2a$, $2b$, $2y$) via bcryptjs (pure JS, Workers-compatible)
|
|
9
|
+
* - Lazy re-hashing: needsRehash() returns true for non-PBKDF2 formats
|
|
10
|
+
*/
|
|
11
|
+
import bcrypt from 'bcryptjs';
|
|
12
|
+
|
|
13
|
+
// ─── Constants ───
|
|
14
|
+
|
|
15
|
+
const ALGORITHM = 'PBKDF2';
|
|
16
|
+
const HASH_ALGO = 'SHA-256';
|
|
17
|
+
// Cloudflare Workers' WebCrypto PBKDF2 currently rejects iteration counts above 100000.
|
|
18
|
+
const ITERATIONS = 100_000;
|
|
19
|
+
const SALT_LENGTH = 16; // 128-bit salt
|
|
20
|
+
const KEY_LENGTH = 32; // 256-bit derived key
|
|
21
|
+
|
|
22
|
+
// ─── Helpers ───
|
|
23
|
+
|
|
24
|
+
function toBase64(buffer: ArrayBuffer): string {
|
|
25
|
+
return btoa(String.fromCharCode(...new Uint8Array(buffer)));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function fromBase64(base64: string): Uint8Array {
|
|
29
|
+
return Uint8Array.from(atob(base64), (c) => c.charCodeAt(0));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// ─── Public API ───
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Hash a password using PBKDF2-SHA256.
|
|
36
|
+
* Returns a self-describing string: `pbkdf2:sha256:{iterations}:{salt_b64}:{hash_b64}`
|
|
37
|
+
*/
|
|
38
|
+
export async function hashPassword(password: string): Promise<string> {
|
|
39
|
+
const salt = crypto.getRandomValues(new Uint8Array(SALT_LENGTH));
|
|
40
|
+
|
|
41
|
+
const keyMaterial = await crypto.subtle.importKey(
|
|
42
|
+
'raw',
|
|
43
|
+
new TextEncoder().encode(password),
|
|
44
|
+
ALGORITHM,
|
|
45
|
+
false,
|
|
46
|
+
['deriveBits'],
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
const hash = await crypto.subtle.deriveBits(
|
|
50
|
+
{
|
|
51
|
+
name: ALGORITHM,
|
|
52
|
+
salt,
|
|
53
|
+
iterations: ITERATIONS,
|
|
54
|
+
hash: HASH_ALGO,
|
|
55
|
+
},
|
|
56
|
+
keyMaterial,
|
|
57
|
+
KEY_LENGTH * 8, // bits
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
return `pbkdf2:sha256:${ITERATIONS}:${toBase64(salt.buffer)}:${toBase64(hash)}`;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Verify a password against a stored hash.
|
|
65
|
+
* Supports:
|
|
66
|
+
* - pbkdf2:sha256:{iterations}:{salt_b64}:{hash_b64} (native format)
|
|
67
|
+
* - $2a$... / $2b$... / $2y$... (bcrypt, imported hashes)
|
|
68
|
+
*/
|
|
69
|
+
export async function verifyPassword(
|
|
70
|
+
password: string,
|
|
71
|
+
storedHash: string,
|
|
72
|
+
): Promise<boolean> {
|
|
73
|
+
// Native PBKDF2 format
|
|
74
|
+
if (storedHash.startsWith('pbkdf2:')) {
|
|
75
|
+
return verifyPBKDF2(password, storedHash);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// bcrypt format ($2a$, $2b$, $2y$)
|
|
79
|
+
if (storedHash.startsWith('$2a$') || storedHash.startsWith('$2b$') || storedHash.startsWith('$2y$')) {
|
|
80
|
+
return bcrypt.compareSync(password, storedHash);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Unknown format — reject
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Check if a stored hash needs to be re-hashed to the native format.
|
|
89
|
+
* Returns true for non-PBKDF2 formats (bcrypt, etc.).
|
|
90
|
+
*/
|
|
91
|
+
export function needsRehash(storedHash: string): boolean {
|
|
92
|
+
return !storedHash.startsWith('pbkdf2:');
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Check if a string is a recognized password hash format.
|
|
97
|
+
* Used during import to distinguish pre-hashed passwords from plaintext.
|
|
98
|
+
*/
|
|
99
|
+
export function isPasswordHash(value: string): boolean {
|
|
100
|
+
if (value.startsWith('pbkdf2:sha256:')) return true;
|
|
101
|
+
if (value.startsWith('$2a$') || value.startsWith('$2b$') || value.startsWith('$2y$')) return true;
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// ─── PBKDF2 Verification (internal) ───
|
|
106
|
+
|
|
107
|
+
async function verifyPBKDF2(password: string, storedHash: string): Promise<boolean> {
|
|
108
|
+
const parts = storedHash.split(':');
|
|
109
|
+
if (parts.length !== 5 || parts[0] !== 'pbkdf2' || parts[1] !== 'sha256') {
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const iterations = parseInt(parts[2], 10);
|
|
114
|
+
const salt = fromBase64(parts[3]);
|
|
115
|
+
const expectedHash = fromBase64(parts[4]);
|
|
116
|
+
|
|
117
|
+
const keyMaterial = await crypto.subtle.importKey(
|
|
118
|
+
'raw',
|
|
119
|
+
new TextEncoder().encode(password),
|
|
120
|
+
ALGORITHM,
|
|
121
|
+
false,
|
|
122
|
+
['deriveBits'],
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
const derivedBits = await crypto.subtle.deriveBits(
|
|
126
|
+
{
|
|
127
|
+
name: ALGORITHM,
|
|
128
|
+
salt: salt as unknown as BufferSource,
|
|
129
|
+
iterations,
|
|
130
|
+
hash: HASH_ALGO,
|
|
131
|
+
},
|
|
132
|
+
keyMaterial,
|
|
133
|
+
KEY_LENGTH * 8,
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
// Constant-time comparison to prevent timing attacks
|
|
137
|
+
const derived = new Uint8Array(derivedBits);
|
|
138
|
+
if (derived.length !== expectedHash.length) return false;
|
|
139
|
+
|
|
140
|
+
let diff = 0;
|
|
141
|
+
for (let i = 0; i < derived.length; i++) {
|
|
142
|
+
diff |= derived[i] ^ expectedHash[i];
|
|
143
|
+
}
|
|
144
|
+
return diff === 0;
|
|
145
|
+
}
|