diff --git a/app/__test__/api/DataApi.spec.ts b/app/__test__/api/DataApi.spec.ts index f574974f..c0eeb639 100644 --- a/app/__test__/api/DataApi.spec.ts +++ b/app/__test__/api/DataApi.spec.ts @@ -202,7 +202,7 @@ describe("DataApi", () => { { // create many const res = await api.createMany("posts", payload); - expect(res.data.length).toEqual(4); + expect(res.data?.length).toEqual(4); expect(res.ok).toBeTrue(); } diff --git a/app/__test__/app/AppServer.spec.ts b/app/__test__/app/AppServer.spec.ts index 87354d00..40ea4144 100644 --- a/app/__test__/app/AppServer.spec.ts +++ b/app/__test__/app/AppServer.spec.ts @@ -8,6 +8,7 @@ describe("AppServer", () => { expect(server).toBeDefined(); expect(server.config).toEqual({ cors: { + allow_credentials: true, origin: "*", allow_methods: ["GET", "POST", "PATCH", "PUT", "DELETE"], allow_headers: ["Content-Type", "Content-Length", "Authorization", "Accept"], @@ -25,6 +26,7 @@ describe("AppServer", () => { expect(server).toBeDefined(); expect(server.config).toEqual({ cors: { + allow_credentials: true, origin: "https", allow_methods: ["GET", "POST"], allow_headers: ["Content-Type", "Content-Length", "Authorization", "Accept"], diff --git a/app/package.json b/app/package.json index 42b3074c..2db29e1e 100644 --- a/app/package.json +++ b/app/package.json @@ -99,7 +99,7 @@ "dotenv": "^16.4.7", "jotai": "^2.12.2", "jsdom": "^26.0.0", - "jsonv-ts": "^0.2.4", + "jsonv-ts": "^0.3.1", "kysely-d1": "^0.3.0", "kysely-generic-sqlite": "^1.2.1", "libsql-stateless-easy": "^1.8.0", diff --git a/app/src/auth/authenticate/Authenticator.ts b/app/src/auth/authenticate/Authenticator.ts index 09ff1d54..5230a528 100644 --- a/app/src/auth/authenticate/Authenticator.ts +++ b/app/src/auth/authenticate/Authenticator.ts @@ -4,7 +4,7 @@ import { runtimeSupports, truncate, $console } from "core/utils"; import type { Context, Hono } from "hono"; import { deleteCookie, getSignedCookie, setSignedCookie } from "hono/cookie"; import { sign, verify } from "hono/jwt"; -import type { CookieOptions } from "hono/utils/cookie"; +import { type CookieOptions, serializeSigned } from "hono/utils/cookie"; import type { ServerEnv } from "modules/Controller"; import { pick } from "lodash-es"; import { InvalidConditionsException } from "auth/errors"; @@ -58,6 +58,7 @@ export const cookieConfig = s secure: s.boolean({ default: true }), httpOnly: s.boolean({ default: true }), expires: s.number({ default: defaultCookieExpires }), // seconds + partitioned: s.boolean({ default: false }), renew: s.boolean({ default: true }), pathSuccess: s.string({ default: "/" }), pathLoggedOut: s.string({ default: "/" }), @@ -334,6 +335,11 @@ export class Authenticator = Record< await setSignedCookie(c, "auth", token, secret, this.cookieOptions); } + async unsafeGetAuthCookie(token: string): Promise { + // this works for as long as cookieOptions.prefix is not set + return serializeSigned("auth", token, this.config.jwt.secret, this.cookieOptions); + } + private deleteAuthCookie(c: Context) { $console.debug("deleting auth cookie"); deleteCookie(c, "auth", this.cookieOptions); diff --git a/app/src/core/object/schema/index.ts b/app/src/core/object/schema/index.ts index 72f36e09..0f497449 100644 --- a/app/src/core/object/schema/index.ts +++ b/app/src/core/object/schema/index.ts @@ -42,6 +42,7 @@ export type ParseOptions = { withDefaults?: boolean; withExtendedDefaults?: boolean; coerce?: boolean; + coerceDropUnknown?: boolean; clone?: boolean; skipMark?: boolean; // @todo: do something with this forceParse?: boolean; // @todo: do something with this @@ -59,7 +60,10 @@ export function parse : s.Static { const schema = (opts?.clone ? cloneSchema(_schema as any) : _schema) as s.Schema; - let value = opts?.coerce !== false ? schema.coerce(v) : v; + let value = + opts?.coerce !== false + ? schema.coerce(v, { dropUnknown: opts?.coerceDropUnknown ?? false }) + : v; if (opts?.withDefaults !== false) { value = schema.template(value, { withOptional: true, diff --git a/app/src/data/api/DataController.ts b/app/src/data/api/DataController.ts index 5b63d8bc..ddce7efa 100644 --- a/app/src/data/api/DataController.ts +++ b/app/src/data/api/DataController.ts @@ -206,7 +206,7 @@ export class DataController extends Controller { const entitiesEnum = this.getEntitiesEnum(this.em); // @todo: make dynamic based on entity - const idType = s.anyOf([s.number(), s.string()], { coerce: (v) => v as any }); + const idType = s.anyOf([s.number(), s.string()], { coerce: (v) => v as number | string }); /** * Function endpoints @@ -387,7 +387,7 @@ export class DataController extends Controller { if (!this.entityExists(entity)) { return this.notFound(c); } - const options = (await c.req.json()) as RepoQuery; + const options = c.req.valid("json") as RepoQuery; const result = await this.em.repository(entity).findMany(options); return c.json(result, { status: result.data ? 200 : 404 }); @@ -397,7 +397,7 @@ export class DataController extends Controller { /** * Mutation endpoints */ - // insert one + // insert one or many hono.post( "/:entity", describeRoute({ diff --git a/app/src/modules/Controller.ts b/app/src/modules/Controller.ts index 8b424e94..b83f3898 100644 --- a/app/src/modules/Controller.ts +++ b/app/src/modules/Controller.ts @@ -64,7 +64,7 @@ export class Controller { return c.notFound(); } - protected getEntitiesEnum(em: EntityManager) { + protected getEntitiesEnum(em: EntityManager): s.StringSchema { const entities = em.entities.map((e) => e.name); // @todo: current workaround to allow strings (sometimes building is not fast enough to get the entities) return entities.length > 0 ? s.anyOf([s.string({ enum: entities }), s.string()]) : s.string(); diff --git a/app/src/modules/server/AppServer.ts b/app/src/modules/server/AppServer.ts index 2e0401c1..a17dcf07 100644 --- a/app/src/modules/server/AppServer.ts +++ b/app/src/modules/server/AppServer.ts @@ -17,6 +17,7 @@ export const serverConfigSchema = s.strictObject({ allow_headers: s.array(s.string(), { default: ["Content-Type", "Content-Length", "Authorization", "Accept"], }), + allow_credentials: s.boolean({ default: true }), }), }); @@ -36,12 +37,14 @@ export class AppServer extends Module { } override async build() { + const origin = this.config.cors.origin ?? ""; this.client.use( "*", cors({ - origin: this.config.cors.origin, + origin: origin.includes(",") ? origin.split(",").map((o) => o.trim()) : origin, allowMethods: this.config.cors.allow_methods, allowHeaders: this.config.cors.allow_headers, + credentials: this.config.cors.allow_credentials, }), ); diff --git a/app/src/modules/server/SystemController.ts b/app/src/modules/server/SystemController.ts index f9fdcab9..7b0f9ca9 100644 --- a/app/src/modules/server/SystemController.ts +++ b/app/src/modules/server/SystemController.ts @@ -323,6 +323,7 @@ export class SystemController extends Controller { local: datetimeStringLocal(), utc: datetimeStringUTC(), }, + origin: new URL(c.req.raw.url).origin, plugins: Array.from(this.app.plugins.keys()), walk: { auth: [ diff --git a/bun.lock b/bun.lock index f21fbfa9..4712d827 100644 --- a/bun.lock +++ b/bun.lock @@ -71,7 +71,7 @@ "dotenv": "^16.4.7", "jotai": "^2.12.2", "jsdom": "^26.0.0", - "jsonv-ts": "^0.2.4", + "jsonv-ts": "^0.3.1", "kysely-d1": "^0.3.0", "kysely-generic-sqlite": "^1.2.1", "libsql-stateless-easy": "^1.8.0", @@ -1216,7 +1216,7 @@ "@types/babel__traverse": ["@types/babel__traverse@7.20.6", "", { "dependencies": { "@babel/types": "^7.20.7" } }, "sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg=="], - "@types/bun": ["@types/bun@1.2.18", "", { "dependencies": { "bun-types": "1.2.18" } }, "sha512-Xf6RaWVheyemaThV0kUfaAUvCNokFr+bH8Jxp+tTZfx7dAPA8z9ePnP9S9+Vspzuxxx9JRAXhnyccRj3GyCMdQ=="], + "@types/bun": ["@types/bun@1.2.19", "", { "dependencies": { "bun-types": "1.2.19" } }, "sha512-d9ZCmrH3CJ2uYKXQIUuZ/pUnTqIvLDS0SK7pFmbx8ma+ziH/FRMoAq5bYpRG7y+w1gl+HgyNZbtqgMq4W4e2Lg=="], "@types/cookie": ["@types/cookie@0.6.0", "", {}, "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA=="], @@ -2496,7 +2496,7 @@ "jsonpointer": ["jsonpointer@5.0.1", "", {}, "sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ=="], - "jsonv-ts": ["jsonv-ts@0.2.4", "", { "optionalDependencies": { "hono": "*" }, "peerDependencies": { "typescript": "^5.0.0" } }, "sha512-GCbLh0udtsIvDeiLR09PGZo/roaTxxxxEYBqCjoMzfj8xvq5kV9x7uvMI9Qs6Ff9rboL5anOP4xKfAmZHAwJqA=="], + "jsonv-ts": ["jsonv-ts@0.3.1", "", { "optionalDependencies": { "hono": "*" }, "peerDependencies": { "typescript": "^5.0.0" } }, "sha512-MayDgM0+WprZsf0BaJjMPlp4Pk7WWlhzid0dMFSFsZ4p9Ax1I4m0wL+H5FdRiaVuSz5TfMlCQm/TzwiLx4DWBQ=="], "jsonwebtoken": ["jsonwebtoken@9.0.2", "", { "dependencies": { "jws": "^3.2.2", "lodash.includes": "^4.3.0", "lodash.isboolean": "^3.0.3", "lodash.isinteger": "^4.0.4", "lodash.isnumber": "^3.0.3", "lodash.isplainobject": "^4.0.6", "lodash.isstring": "^4.0.1", "lodash.once": "^4.0.0", "ms": "^2.1.1", "semver": "^7.5.4" } }, "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ=="], @@ -4020,7 +4020,7 @@ "@testing-library/jest-dom/chalk": ["chalk@3.0.0", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg=="], - "@types/bun/bun-types": ["bun-types@1.2.18", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-04+Eha5NP7Z0A9YgDAzMk5PHR16ZuLVa83b26kH5+cp1qZW4F6FmAURngE7INf4tKOvCE69vYvDEwoNl1tGiWw=="], + "@types/bun/bun-types": ["bun-types@1.2.19", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-uAOTaZSPuYsWIXRpj7o56Let0g/wjihKCkeRqUBhlLVM/Bt+Fj9xTo+LhC1OV1XDaGkz4hNC80et5xgy+9KTHQ=="], "@types/pg/pg-types": ["pg-types@4.0.2", "", { "dependencies": { "pg-int8": "1.0.1", "pg-numeric": "1.0.2", "postgres-array": "~3.0.1", "postgres-bytea": "~3.0.0", "postgres-date": "~2.1.0", "postgres-interval": "^3.0.0", "postgres-range": "^1.1.1" } }, "sha512-cRL3JpS3lKMGsKaWndugWQoLOCoP+Cic8oseVcbr0qhPzYD5DWXK+RZ9LY9wxRf7RQia4SCwQlXk0q6FCPrVng=="],