diff --git a/src/database/index.ts b/src/database/index.ts index f44b5a8e..b116816d 100644 --- a/src/database/index.ts +++ b/src/database/index.ts @@ -1,7 +1,7 @@ import { DatabaseSync, type StatementSync } from "node:sqlite"; import { LruCache } from "@std/cache"; -import { monotonicUlid, ulid } from "@std/ulid"; +import { ulid } from "@std/ulid"; import { constantPathDatabaseFile } from "#/global.ts"; import { migrations } from "#db/migration.ts"; @@ -24,6 +24,8 @@ export class Database { private readonly database: DatabaseSync; private readonly store = new LruCache(200); + private savepointId = 0; + public constructor(options: Options = {}) { options.ephemeral ??= env.JSPB_DEBUG_DATABASE_EPHEMERAL; @@ -53,7 +55,7 @@ export class Database { for (const [delta, migration] of migrations.slice(query.user_version).entries()) { try { - await this.transaction(async () => { + await this.transactionAsync(async () => { await migration.preMigration?.(this); this.exec(migration.sql); await migration.postMigration?.(this); @@ -109,7 +111,7 @@ export class Database { public transaction(callback: () => T): T { if (this.database.isTransaction) { - const name = `_${monotonicUlid()}`; + const name = `_${++this.savepointId}`; this.exec(`SAVEPOINT ${name};`); try { @@ -137,6 +139,36 @@ export class Database { } } + public async transactionAsync(callback: () => Promise): Promise { + if (this.database.isTransaction) { + const name = `_${++this.savepointId}`; + + this.exec(`SAVEPOINT ${name};`); + try { + return await callback(); + } catch (error) { + this.exec(`ROLLBACK TO ${name};`); + + throw error; + } finally { + this.exec(`RELEASE ${name};`); + } + } + + this.exec("BEGIN IMMEDIATE;"); + try { + const result = await callback(); + + this.exec("COMMIT;"); + + return result; + } catch (error) { + this.exec("ROLLBACK;"); + + throw error; + } + } + public [Symbol.dispose](): void { this.database.close(); } diff --git a/src/database/migration.ts b/src/database/migration.ts index 84826bd3..6d55949c 100644 --- a/src/database/migration.ts +++ b/src/database/migration.ts @@ -1,7 +1,7 @@ import { mapNotNullish } from "@std/collections"; import { ulid } from "@std/ulid"; -import { mutable } from "#/global.ts"; +import { mutableDatabase } from "#/global.ts"; import type { Database } from "#db/index.ts"; import { Logger } from "#util/console.ts"; import { generateHash } from "#util/crypto.ts"; @@ -62,7 +62,7 @@ export const migrations: Migration[] = [ database.user.delete("id", userRootIdOld); - const userRootId = mutable.database.user.getRoot()?.id; + const userRootId = mutableDatabase.user.getRoot()?.id; if (userRootId) { database.user.update("id", userRootId, "token", userRootToken); } diff --git a/src/endpoints/document/v1/drop.ts b/src/endpoints/document/v1/drop.ts index ee892efe..edaadd1f 100644 --- a/src/endpoints/document/v1/drop.ts +++ b/src/endpoints/document/v1/drop.ts @@ -2,7 +2,7 @@ import { describeRoute, validator } from "@hono/openapi"; import { type } from "arktype"; import { Hono } from "hono/tiny"; -import { constantHttpStatusCodes, mutable } from "#/global.ts"; +import { constantHttpStatusCodes, mutableDatabase } from "#/global.ts"; import type { Env } from "#http/handler.ts"; import { authMiddleware } from "#http/middleware/authorization.ts"; import { isOwner } from "#util/document.ts"; @@ -41,7 +41,7 @@ export default new Hono().delete( // @ts-expect-error upstream } = ctx.req.valid("param") as typeof schemaParam.infer; - const document = mutable.database.document.get("name", name); + const document = mutableDatabase.document.get("name", name); if (!document?.id) { return errorThrow(ErrorCode.DocumentNotFound); } @@ -52,7 +52,7 @@ export default new Hono().delete( return errorThrow(ErrorCode.UserInvalidToken); } - mutable.database.document.delete("name", name); + mutableDatabase.document.delete("name", name); void fsDelete(document); return ctx.body(null); diff --git a/src/endpoints/document/v1/get.ts b/src/endpoints/document/v1/get.ts index bb2744e3..7eee219e 100644 --- a/src/endpoints/document/v1/get.ts +++ b/src/endpoints/document/v1/get.ts @@ -1,10 +1,9 @@ import { describeRoute, resolver, validator } from "@hono/openapi"; import { decodeTime } from "@std/ulid"; import { type } from "arktype"; -import { stream } from "hono/streaming"; import { Hono } from "hono/tiny"; -import { constantHttpStatusCodes, mutable } from "#/global.ts"; +import { constantHttpStatusCodes, mutableDatabase } from "#/global.ts"; import type { Env } from "#http/handler.ts"; import { verifyHash } from "#util/crypto.ts"; import { ErrorCode, errorThrow, genericErrorResponse } from "#util/error.ts"; @@ -62,7 +61,7 @@ Note: If you only need to query the document metadata, you should use HEAD metho validator("param", schemaParam, validatorHandler), validator("header", schemaHeader, validatorHandler), validator("query", schemaQuery, validatorHandler), - (ctx) => { + async (ctx) => { const { name // @ts-expect-error upstream @@ -76,7 +75,7 @@ Note: If you only need to query the document metadata, you should use HEAD metho // @ts-expect-error upstream } = ctx.req.valid("query") as typeof schemaQuery.infer; - const document = mutable.database.document.get("name", name); + const document = mutableDatabase.document.get("name", name); if (!document?.id) { return errorThrow(ErrorCode.DocumentNotFound); } @@ -108,6 +107,6 @@ Note: If you only need to query the document metadata, you should use HEAD metho ctx.res.headers.set("transfer-encoding", "chunked"); - return stream(ctx, async (stream) => await stream.pipe(await fsRead(ctx, document))); + return ctx.body(await fsRead(ctx, document)); } ); diff --git a/src/endpoints/document/v1/list.ts b/src/endpoints/document/v1/list.ts index f6b8e4c1..1c301159 100644 --- a/src/endpoints/document/v1/list.ts +++ b/src/endpoints/document/v1/list.ts @@ -2,7 +2,7 @@ import { describeRoute, resolver } from "@hono/openapi"; import { decodeTime } from "@std/ulid"; import { Hono } from "hono/tiny"; -import { constantHttpStatusCodes, mutable } from "#/global.ts"; +import { constantHttpStatusCodes, mutableDatabase } from "#/global.ts"; import type { Env } from "#http/handler.ts"; import { authMiddleware } from "#http/middleware/authorization.ts"; import { ErrorCode, errorThrow, genericErrorResponse } from "#util/error.ts"; @@ -45,7 +45,7 @@ export default new Hono().get( return ctx.body(null); } - const documents = mutable.database.user.getDocuments(userId).map((document) => { + const documents = mutableDatabase.user.getDocuments(userId).map((document) => { return { name: document.name, created: Temporal.Instant.fromEpochMilliseconds(decodeTime(document.id)).toString() diff --git a/src/endpoints/document/v1/patch.ts b/src/endpoints/document/v1/patch.ts index 5e7a5544..b0dc03b6 100644 --- a/src/endpoints/document/v1/patch.ts +++ b/src/endpoints/document/v1/patch.ts @@ -2,7 +2,7 @@ import { describeRoute, resolver, validator } from "@hono/openapi"; import { type } from "arktype"; import { Hono } from "hono/tiny"; -import { constantHttpStatusCodes, mutable } from "#/global.ts"; +import { constantHttpStatusCodes, mutableDatabase } from "#/global.ts"; import type { Env } from "#http/handler.ts"; import { authMiddleware } from "#http/middleware/authorization.ts"; import { bodyStream } from "#http/middleware/bodyStream.ts"; @@ -83,7 +83,7 @@ Note: To remove (nullify) a value, send the header with an empty value`, // @ts-expect-error upstream } = ctx.req.valid("header") as typeof schemaHeader.infer; - const document = mutable.database.document.get("name", actualName); + const document = mutableDatabase.document.get("name", actualName); if (!document?.id) { return errorThrow(ErrorCode.DocumentNotFound); } @@ -96,27 +96,27 @@ Note: To remove (nullify) a value, send the header with an empty value`, if (newPassword !== undefined) { if (newPassword === "") { - mutable.database.document.update("name", actualName, "password", null); + mutableDatabase.document.update("name", actualName, "password", null); } else { const hash = generateHash(newPassword); - mutable.database.document.update("name", actualName, "password", hash.combo); + mutableDatabase.document.update("name", actualName, "password", hash.combo); } } // keep newName last thing to alter in case of race conditions if (newName) { - if (mutable.database.document.get("name", newName)?.name) { + if (mutableDatabase.document.get("name", newName)?.name) { return errorThrow(ErrorCode.DocumentNameAlreadyExists); } - mutable.database.document.update("name", actualName, "name", newName); + mutableDatabase.document.update("name", actualName, "name", newName); actualName = newName; } if (ctx.get("hasBody")) { - mutable.database.document.update("name", actualName, "version", env.JSPB_DOCUMENT_COMPRESSION); + mutableDatabase.document.update("name", actualName, "version", env.JSPB_DOCUMENT_COMPRESSION); await fsWrite(ctx, document); } diff --git a/src/endpoints/document/v1/post.ts b/src/endpoints/document/v1/post.ts index 439ee979..39edb747 100644 --- a/src/endpoints/document/v1/post.ts +++ b/src/endpoints/document/v1/post.ts @@ -3,7 +3,7 @@ import { monotonicUlid } from "@std/ulid"; import { type } from "arktype"; import { Hono } from "hono/tiny"; -import { constantHttpStatusCodes, mutable } from "#/global.ts"; +import { constantHttpStatusCodes, mutableDatabase } from "#/global.ts"; import type { Env } from "#http/handler.ts"; import { authMiddleware } from "#http/middleware/authorization.ts"; import { bodyStream } from "#http/middleware/bodyStream.ts"; @@ -87,7 +87,7 @@ export default new Hono().post( let setName: string; if (name) { - if (mutable.database.document.get("name", name)?.name) { + if (mutableDatabase.document.get("name", name)?.name) { return errorThrow(ErrorCode.DocumentNameAlreadyExists); } @@ -105,7 +105,7 @@ export default new Hono().post( hashCombo = null; } - mutable.database.document.create({ + mutableDatabase.document.create({ id: setId, user_id: ctx.get("userId") ?? null, version: env.JSPB_DOCUMENT_COMPRESSION, @@ -116,7 +116,7 @@ export default new Hono().post( try { await fsWrite(ctx, { id: setId }); } catch (why) { - mutable.database.document.delete("id", setId); + mutableDatabase.document.delete("id", setId); throw why; } diff --git a/src/endpoints/legacy/v2/documents/access.route.ts b/src/endpoints/legacy/v2/documents/access.route.ts index 2f299473..57123f32 100644 --- a/src/endpoints/legacy/v2/documents/access.route.ts +++ b/src/endpoints/legacy/v2/documents/access.route.ts @@ -3,7 +3,7 @@ import { toText } from "@std/streams"; import { type } from "arktype"; import { Hono } from "hono/tiny"; -import { constantHttpStatusCodes, mutable } from "#/global.ts"; +import { constantHttpStatusCodes, mutableDatabase } from "#/global.ts"; import type { Env } from "#http/handler.ts"; import { verifyHash } from "#util/crypto.ts"; import { ErrorCode, errorThrow, genericErrorResponse } from "#util/error.ts"; @@ -74,7 +74,7 @@ export default new Hono().get( // @ts-expect-error upstream const header = ctx.req.valid("header") as typeof schemaHeader.infer; - const document = mutable.database.document.get("name", param.name); + const document = mutableDatabase.document.get("name", param.name); if (!document?.id) { return errorThrow(ErrorCode.DocumentNotFound); } diff --git a/src/endpoints/legacy/v2/documents/accessRaw.route.ts b/src/endpoints/legacy/v2/documents/accessRaw.route.ts index f0ac71c0..5d16c133 100644 --- a/src/endpoints/legacy/v2/documents/accessRaw.route.ts +++ b/src/endpoints/legacy/v2/documents/accessRaw.route.ts @@ -1,9 +1,8 @@ import { describeRoute, resolver, validator } from "@hono/openapi"; import { type } from "arktype"; -import { stream } from "hono/streaming"; import { Hono } from "hono/tiny"; -import { constantHttpStatusCodes, mutable } from "#/global.ts"; +import { constantHttpStatusCodes, mutableDatabase } from "#/global.ts"; import type { Env } from "#http/handler.ts"; import { verifyHash } from "#util/crypto.ts"; import { ErrorCode, errorThrow, genericErrorResponse } from "#util/error.ts"; @@ -50,7 +49,7 @@ export default new Hono().get( validator("param", schemaParam, validatorHandler), validator("header", schemaHeader, validatorHandler), validator("query", schemaQuery, validatorHandler), - (ctx) => { + async (ctx) => { // https://github.com/honojs/hono/issues/1130 if (ctx.req.method === "HEAD") { return ctx.body(null); @@ -66,7 +65,7 @@ export default new Hono().get( password: header.password || query.p }; - const document = mutable.database.document.get("name", param.name); + const document = mutableDatabase.document.get("name", param.name); if (!document?.id) { return errorThrow(ErrorCode.DocumentNotFound); } @@ -83,6 +82,6 @@ export default new Hono().get( ctx.res.headers.set("content-type", "text/plain"); ctx.res.headers.set("transfer-encoding", "chunked"); - return stream(ctx, async (stream) => await stream.pipe(await fsRead(ctx, document, true))); + return ctx.body(await fsRead(ctx, document, true)); } ); diff --git a/src/endpoints/legacy/v2/documents/edit.route.ts b/src/endpoints/legacy/v2/documents/edit.route.ts index 9eb2c493..93167f37 100644 --- a/src/endpoints/legacy/v2/documents/edit.route.ts +++ b/src/endpoints/legacy/v2/documents/edit.route.ts @@ -2,7 +2,7 @@ import { describeRoute, resolver, validator } from "@hono/openapi"; import { type } from "arktype"; import { Hono } from "hono/tiny"; -import { constantHttpStatusCodes, mutable } from "#/global.ts"; +import { constantHttpStatusCodes, mutableDatabase } from "#/global.ts"; import type { Env } from "#http/handler.ts"; import { bodyStream } from "#http/middleware/bodyStream.ts"; import { env } from "#util/env.ts"; @@ -63,12 +63,12 @@ export default new Hono().patch( // @ts-expect-error upstream const param = ctx.req.valid("param") as typeof schemaParam.infer; - const document = mutable.database.document.get("name", param.name); + const document = mutableDatabase.document.get("name", param.name); if (!document?.id || document.user_id) { return errorThrow(ErrorCode.DocumentNotFound); } - mutable.database.document.update("name", param.name, "version", env.JSPB_DOCUMENT_COMPRESSION); + mutableDatabase.document.update("name", param.name, "version", env.JSPB_DOCUMENT_COMPRESSION); await fsWrite(ctx, document); return ctx.json({ diff --git a/src/endpoints/legacy/v2/documents/exists.route.ts b/src/endpoints/legacy/v2/documents/exists.route.ts index e21cc91c..aa91185f 100644 --- a/src/endpoints/legacy/v2/documents/exists.route.ts +++ b/src/endpoints/legacy/v2/documents/exists.route.ts @@ -2,7 +2,7 @@ import { describeRoute, resolver, validator } from "@hono/openapi"; import { type } from "arktype"; import { Hono } from "hono/tiny"; -import { constantHttpStatusCodes, mutable } from "#/global.ts"; +import { constantHttpStatusCodes, mutableDatabase } from "#/global.ts"; import type { Env } from "#http/handler.ts"; import { genericErrorResponse } from "#util/error.ts"; import { validatorDocumentName } from "#util/validator/document.ts"; @@ -43,6 +43,6 @@ export default new Hono().get( // @ts-expect-error upstream const param = ctx.req.valid("param") as typeof schemaParam.infer; - return ctx.text(mutable.database.document.get("name", param.name)?.name ? "true" : "false"); + return ctx.text(mutableDatabase.document.get("name", param.name)?.name ? "true" : "false"); } ); diff --git a/src/endpoints/legacy/v2/documents/publish.route.ts b/src/endpoints/legacy/v2/documents/publish.route.ts index a1d2fbce..581386cc 100644 --- a/src/endpoints/legacy/v2/documents/publish.route.ts +++ b/src/endpoints/legacy/v2/documents/publish.route.ts @@ -7,7 +7,7 @@ import { constantDocumentNameLengthMax, constantDocumentNameLengthMin, constantHttpStatusCodes, - mutable + mutableDatabase } from "#/global.ts"; import type { Env } from "#http/handler.ts"; import { bodyStream } from "#http/middleware/bodyStream.ts"; @@ -81,7 +81,7 @@ export default new Hono().post( let setName: string; if (name) { - if (mutable.database.document.get("name", name)?.name) { + if (mutableDatabase.document.get("name", name)?.name) { return errorThrow(ErrorCode.DocumentNameAlreadyExists); } @@ -99,7 +99,7 @@ export default new Hono().post( hashCombo = null; } - mutable.database.document.create({ + mutableDatabase.document.create({ id: id, user_id: null, version: env.JSPB_DOCUMENT_COMPRESSION, diff --git a/src/endpoints/legacy/v2/documents/remove.route.ts b/src/endpoints/legacy/v2/documents/remove.route.ts index bcba2dbb..d45f0a71 100644 --- a/src/endpoints/legacy/v2/documents/remove.route.ts +++ b/src/endpoints/legacy/v2/documents/remove.route.ts @@ -2,7 +2,7 @@ import { describeRoute, resolver, validator } from "@hono/openapi"; import { type } from "arktype"; import { Hono } from "hono/tiny"; -import { constantHttpStatusCodes, mutable } from "#/global.ts"; +import { constantHttpStatusCodes, mutableDatabase } from "#/global.ts"; import type { Env } from "#http/handler.ts"; import { ErrorCode, errorThrow, genericErrorResponse } from "#util/error.ts"; import { fsDelete } from "#util/fs.ts"; @@ -46,12 +46,12 @@ export default new Hono().delete( // @ts-expect-error upstream const param = ctx.req.valid("param") as typeof schemaParam.infer; - const document = mutable.database.document.get("name", param.name); + const document = mutableDatabase.document.get("name", param.name); if (!document?.id || document.user_id) { return errorThrow(ErrorCode.DocumentNotFound); } - mutable.database.document.delete("name", param.name); + mutableDatabase.document.delete("name", param.name); void fsDelete(document); return ctx.json({ removed: true }); diff --git a/src/endpoints/user/v1/create.ts b/src/endpoints/user/v1/create.ts index 66b5360e..df4e3037 100644 --- a/src/endpoints/user/v1/create.ts +++ b/src/endpoints/user/v1/create.ts @@ -2,7 +2,7 @@ import { describeRoute, resolver } from "@hono/openapi"; import { type } from "arktype"; import { Hono } from "hono/tiny"; -import { constantHttpStatusCodes, mutable } from "#/global.ts"; +import { constantHttpStatusCodes, mutableDatabase, mutableRootId } from "#/global.ts"; import type { Env } from "#http/handler.ts"; import { authMiddleware } from "#http/middleware/authorization.ts"; import { env } from "#util/env.ts"; @@ -40,12 +40,12 @@ export default new Hono().post( }), authMiddleware, (ctx) => { - if (!env.JSPB_USER_REGISTER && ctx.get("userId") !== mutable.database.user.getRoot()?.id) { + if (!env.JSPB_USER_REGISTER && ctx.get("userId") !== mutableRootId) { return errorThrow(ErrorCode.UserInvalidToken); } return ctx.json({ - token: mutable.database.user.create() + token: mutableDatabase.user.create() }); } ); diff --git a/src/endpoints/user/v1/drop.ts b/src/endpoints/user/v1/drop.ts index 6f33b848..244d3ae4 100644 --- a/src/endpoints/user/v1/drop.ts +++ b/src/endpoints/user/v1/drop.ts @@ -1,7 +1,7 @@ import { describeRoute } from "@hono/openapi"; import { Hono } from "hono/tiny"; -import { constantHttpStatusCodes, mutable } from "#/global.ts"; +import { constantHttpStatusCodes, mutableDatabase } from "#/global.ts"; import type { Env } from "#http/handler.ts"; import { authMiddleware } from "#http/middleware/authorization.ts"; import { ErrorCode, errorThrow, genericErrorResponse } from "#util/error.ts"; @@ -33,7 +33,7 @@ Note: All documents owned by the user will also be deleted`, return errorThrow(ErrorCode.UserInvalidToken); } - mutable.database.user.delete("id", userId); + mutableDatabase.user.delete("id", userId); return ctx.body(null); } diff --git a/src/endpoints/user/v1/rotateToken.ts b/src/endpoints/user/v1/rotateToken.ts index bc35753a..c48bfe14 100644 --- a/src/endpoints/user/v1/rotateToken.ts +++ b/src/endpoints/user/v1/rotateToken.ts @@ -2,7 +2,7 @@ import { describeRoute, resolver } from "@hono/openapi"; import { type } from "arktype"; import { Hono } from "hono/tiny"; -import { constantHttpStatusCodes, mutable } from "#/global.ts"; +import { constantHttpStatusCodes, mutableDatabase } from "#/global.ts"; import type { Env } from "#http/handler.ts"; import { authMiddleware } from "#http/middleware/authorization.ts"; import { generateHash } from "#util/crypto.ts"; @@ -49,7 +49,7 @@ export default new Hono().post( const token = generateToken(userId); const hash = generateHash(token); - mutable.database.user.update("id", userId, "token", hash.combo); + mutableDatabase.user.update("id", userId, "token", hash.combo); return ctx.json({ token: token diff --git a/src/global.ts b/src/global.ts index 8ccab5c1..f6101898 100644 --- a/src/global.ts +++ b/src/global.ts @@ -5,9 +5,19 @@ import { customAlphabet } from "nanoid"; import type { Database } from "#db/index.ts"; -export const mutable = { - database: undefined as unknown as Database, - http: undefined as unknown as Deno.HttpServer +export let mutableDatabase: Database; +export const setMutableDatabase = (database: Database) => { + mutableDatabase = database; +}; + +export let mutableHttpServer: Deno.HttpServer; +export const setMutableHttpServer = (httpServer: Deno.HttpServer) => { + mutableHttpServer = httpServer; +}; + +export let mutableRootId: string; +export const setMutableRootId = (rootId: string) => { + mutableRootId = rootId; }; export const constantDatabaseMaxElements = 10_000; diff --git a/src/http/handler.ts b/src/http/handler.ts index 37d37559..919ea92b 100644 --- a/src/http/handler.ts +++ b/src/http/handler.ts @@ -56,7 +56,7 @@ export const handler = (): Hono => { // disable compression // https://docs.deno.com/runtime/fundamentals/http_server/#automatic-body-compression - ctx.res.headers.append("Cache-Control", "no-transform"); + ctx.res.headers.set("Cache-Control", "no-transform"); }); handler.get( diff --git a/src/http/middleware/authorization.ts b/src/http/middleware/authorization.ts index 3d1b454b..001268c2 100644 --- a/src/http/middleware/authorization.ts +++ b/src/http/middleware/authorization.ts @@ -1,7 +1,7 @@ import { type } from "arktype"; import { createMiddleware } from "hono/factory"; -import { mutable } from "#/global.ts"; +import { mutableDatabase } from "#/global.ts"; import type { Env } from "#http/handler.ts"; import { verifyHash } from "#util/crypto.ts"; import { ErrorCode, errorThrow } from "#util/error.ts"; @@ -18,11 +18,12 @@ export const authMiddleware = createMiddleware(async (ctx, next) => { return errorThrow(ErrorCode.Validation, token.summary); } - if (!token.includes(".")) { + const dotIndex = token.indexOf("."); + if (dotIndex === -1) { // unhashed token if (token.length === 32) { // @ts-expect-error unindexed select - const id = mutable.database.user.get("token", token)?.id; + const id = mutableDatabase.user.get("token", token)?.id; if (!id) { return errorThrow(ErrorCode.UserInvalidToken); } @@ -35,13 +36,13 @@ export const authMiddleware = createMiddleware(async (ctx, next) => { return errorThrow(ErrorCode.UserInvalidToken); } - const [id] = token.split("."); + const id = token.slice(0, dotIndex); if (!id) { return errorThrow(ErrorCode.UserInvalidToken); } // trying to minimize timing attacks by always calling verifyHash - const combo = mutable.database.user.get("id", id)?.token ?? "0 0"; + const combo = mutableDatabase.user.get("id", id)?.token ?? "0 0"; if (!verifyHash(token, combo)) { return errorThrow(ErrorCode.UserInvalidToken); } diff --git a/src/index.ts b/src/index.ts index 8b64543c..2c3182c0 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,7 +1,7 @@ import { abortable } from "@std/async"; import { constantStoreDispose } from "#/global.ts"; -import { initDatabase, initDirStruct, initHTTPServer, initUnhashedTokenCheck, initTask } from "#/init.ts"; +import { initDatabase, initDirStruct, initHTTPServer, initTask, initUnhashedTokenCheck } from "#/init.ts"; import { handler } from "#http/handler.ts"; import { Logger } from "#util/console.ts"; diff --git a/src/init.ts b/src/init.ts index 4b9cf160..73f7e855 100644 --- a/src/init.ts +++ b/src/init.ts @@ -7,7 +7,16 @@ import { sweeper } from "#task/list/sweeper.ts"; import { Logger } from "#util/console.ts"; import { env } from "#util/env.ts"; -import { constantPathStructStorage, constantPathStructStorageData, constantStoreDispose, mutable } from "./global.ts"; +import { + constantPathStructStorage, + constantPathStructStorageData, + constantStoreDispose, + mutableDatabase, + mutableHttpServer, + setMutableDatabase, + setMutableHttpServer, + setMutableRootId +} from "./global.ts"; const log: Logger = new Logger(); @@ -20,17 +29,19 @@ export const initHTTPServer = async (handler?: Deno.ServeHandler): Pr await constantStoreDispose.get(id)?.run(); - mutable.http = http({ - handler: handler - }); + setMutableHttpServer( + http({ + handler: handler + }) + ); constantStoreDispose.set(id, { priority: 10, run: async (): Promise => { - mutable.http.unref(); + mutableHttpServer.unref(); // Deno.serve will deadlock on shutdown under pressure - await mutable.http.shutdown(); + await mutableHttpServer.shutdown(); } }); }; @@ -40,16 +51,23 @@ export const initDatabase = async (): Promise => { await constantStoreDispose.get(id)?.run(); - mutable.database = new Database(); + setMutableDatabase(new Database()); constantStoreDispose.set(id, { priority: 0, run: (): void => { - mutable.database[Symbol.dispose](); + mutableDatabase[Symbol.dispose](); } }); - await mutable.database.migration(); + await mutableDatabase.migration(); + + const rootId = mutableDatabase.user.getRoot()?.id; + if (!rootId) { + throw new Error('"root" user not found. Database may be corrupted.'); + } + + setMutableRootId(rootId); }; export const initTask = (): void => { @@ -59,7 +77,7 @@ export const initTask = (): void => { }; export const initUnhashedTokenCheck = (): void => { - const userTokens = mutable.database.user.getAll(["token"]); + const userTokens = mutableDatabase.user.getAll(["token"]); let userUnhashedToken = false; for (const entry of userTokens) { diff --git a/src/tasks/list/sweeper.ts b/src/tasks/list/sweeper.ts index b70950a3..6b7992d8 100644 --- a/src/tasks/list/sweeper.ts +++ b/src/tasks/list/sweeper.ts @@ -1,7 +1,7 @@ import { mapNotNullish } from "@std/collections"; import { decodeTime } from "@std/ulid"; -import { constantTemporalUTC, mutable } from "#/global.ts"; +import { constantTemporalUTC, mutableRootId } from "#/global.ts"; import { Database } from "#db/index.ts"; import { Logger } from "#util/console.ts"; import { env } from "#util/env.ts"; @@ -26,7 +26,7 @@ const sweeperDatabaseUser = (): void => { const users = mapNotNullish(database.user.getAllWithoutDocuments(), ({ id }) => { if (!id) return; - if (id === mutable.database.user.getRoot()?.id) return; + if (id === mutableRootId) return; if (temporalFuture.epochMilliseconds > decodeTime(id)) { return id; @@ -45,13 +45,13 @@ const sweeperDatabaseDocument = (): void => { using database = new Database(); const temporalNow = constantTemporalUTC(); + const documentAgeAnonymous = env.JSPB_DOCUMENT_ANONYMOUS_AGE.total("milliseconds"); + const documentAge = env.JSPB_DOCUMENT_AGE.total("milliseconds"); const documents = mapNotNullish(database.document.getAll(["id", "user_id"]), ({ id, user_id }) => { if (!id) return; - const ageType = user_id - ? env.JSPB_DOCUMENT_AGE.total("milliseconds") - : env.JSPB_DOCUMENT_ANONYMOUS_AGE.total("milliseconds"); + const ageType = user_id ? documentAge : documentAgeAnonymous; if (ageType > 0 && temporalNow.epochMilliseconds - decodeTime(id) > ageType) { return id; diff --git a/src/utils/console.ts b/src/utils/console.ts index 1aae7876..2b5a4696 100644 --- a/src/utils/console.ts +++ b/src/utils/console.ts @@ -42,7 +42,12 @@ export class Logger { if (env.JSPB_LOG_TIME) { prefix += - gray(Temporal.Now.zonedDateTimeISO().toString({ timeZoneName: "never", fractionalSecondDigits: 3 })) + " "; + gray( + Temporal.Now.zonedDateTimeISO().toString({ + timeZoneName: "never", + fractionalSecondDigits: 3 + }) + ) + " "; } prefix += name; diff --git a/src/utils/crypto.ts b/src/utils/crypto.ts index 156bbc6c..c9f8627c 100644 --- a/src/utils/crypto.ts +++ b/src/utils/crypto.ts @@ -1,10 +1,13 @@ import { decodeAscii85, encodeAscii85 } from "@std/encoding"; +import type { EncodeAscii85Options } from "@std/encoding/ascii85"; import { createBLAKE3 } from "hash-wasm"; import { constantTextEncoder } from "#/global.ts"; const hasher = await createBLAKE3(); +const encoderOptions: EncodeAscii85Options = { standard: "Z85" }; + export const generateSalt = (length: number): Uint8Array => { return crypto.getRandomValues(new Uint8Array(length)); }; @@ -16,21 +19,28 @@ export const generateHash = (input: string, salt?: Uint8Array): { combo: string; hasher.update(defaultSalt); hasher.update(constantTextEncoder.encode(input)); - const encodedHash = encodeAscii85(hasher.digest("binary"), { standard: "Z85" }); + const encodedHash = encodeAscii85(hasher.digest("binary"), encoderOptions); return { - combo: `${encodedHash} ${encodeAscii85(defaultSalt, { standard: "Z85" })}`, + combo: `${encodedHash} ${encodeAscii85(defaultSalt, encoderOptions)}`, hash: encodedHash }; }; export const verifyHash = (input: string, combo: string): boolean => { - const [hash, salt] = combo.split(" "); + const comboSeparatorIndex = combo.indexOf(" "); + if (comboSeparatorIndex === -1) { + throw new Error("Invalid hash combo"); + } + + const hash = combo.slice(0, comboSeparatorIndex); + const salt = combo.slice(comboSeparatorIndex + 1); + if (!(hash && salt)) { throw new Error("Invalid hash combo"); } - const { hash: inputHash } = generateHash(input, decodeAscii85(salt, { standard: "Z85" })); + const { hash: inputHash } = generateHash(input, decodeAscii85(salt, encoderOptions)); return inputHash === hash; }; diff --git a/src/utils/document.ts b/src/utils/document.ts index 0cad01e7..2513ec1f 100644 --- a/src/utils/document.ts +++ b/src/utils/document.ts @@ -1,4 +1,4 @@ -import { constantDocumentNameLengthDefault, constantNanoid, mutable } from "#/global.ts"; +import { constantDocumentNameLengthDefault, constantNanoid, mutableDatabase, mutableRootId } from "#/global.ts"; // deflate export const documentVersionV1 = 1; @@ -11,7 +11,7 @@ export const generateName = (length = constantDocumentNameLengthDefault): string let name: string; do { name = constantNanoid(length); - } while (mutable.database.document.get("name", name)?.name); + } while (mutableDatabase.document.get("name", name)?.name); return name; }; @@ -29,7 +29,7 @@ export const isOwner = (userId?: string | null, documentUserId?: string | null): } // the root user can alter everything - if (userId === mutable.database.user.getRoot()?.id) { + if (userId === mutableRootId) { return true; } }