From 2dbfb02c52210400685d56be842c50cd3f53bfd9 Mon Sep 17 00:00:00 2001 From: Wanjohi Date: Mon, 27 Oct 2025 08:25:31 +0300 Subject: [PATCH] fix: Use a better ID format --- packages/core/src/examples.ts | 6 +- packages/core/src/id/index.ts | 89 +++++++++++++++++++++ packages/core/src/organisation/index.ts | 3 +- packages/core/src/steam/index.ts | 9 ++- packages/core/src/user/index.ts | 6 +- packages/core/src/utils/id.ts | 25 ------ packages/function/src/api/routes/session.ts | 24 ++++++ 7 files changed, 126 insertions(+), 36 deletions(-) create mode 100644 packages/core/src/id/index.ts delete mode 100644 packages/core/src/utils/id.ts create mode 100644 packages/function/src/api/routes/session.ts diff --git a/packages/core/src/examples.ts b/packages/core/src/examples.ts index 7557793a..6ae2b860 100644 --- a/packages/core/src/examples.ts +++ b/packages/core/src/examples.ts @@ -1,8 +1,8 @@ -import { prefixes } from "./utils/id.js"; +import { Identifier } from "./id/index.js"; export namespace Examples { - export const Id = (prefix: keyof typeof prefixes) => - `${prefixes[prefix]}_XXXXXXXXXXXXXXXXXXXXXXXXX`; + export const Id = (prefix: keyof typeof Identifier.prefixes) => + `${Identifier.prefixes[prefix]}_XXXXXXXXXXXXXXXXXXXXXXXXX`; export const User = { id: Id("user"), diff --git a/packages/core/src/id/index.ts b/packages/core/src/id/index.ts new file mode 100644 index 00000000..0be0f193 --- /dev/null +++ b/packages/core/src/id/index.ts @@ -0,0 +1,89 @@ +import { z } from "zod/v4"; +import { randomBytes } from "crypto"; + +export namespace Identifier { + export const prefixes = { + app: "app", + user: "usr", + steam: "stm", + member: "mbr", + session: "ses", + apiSecret: "sec", + apiPersonal: "pat", + organisation: "org", + } as const; + + export function schema(prefix: keyof typeof prefixes) { + return z.string().startsWith(prefixes[prefix]); + } + + const LENGTH = 26; + + // State for monotonic ID generation + let lastTimestamp = 0; + let counter = 0; + + export function ascending(prefix: keyof typeof prefixes, given?: string) { + return generateID(prefix, false, given); + } + + export function descending(prefix: keyof typeof prefixes, given?: string) { + return generateID(prefix, true, given); + } + + function generateID( + prefix: keyof typeof prefixes, + descending: boolean, + given?: string, + ): string { + if (!given) { + return create(prefix, descending); + } + + if (!given.startsWith(prefixes[prefix])) { + throw new Error(`ID ${given} does not start with ${prefixes[prefix]}`); + } + return given; + } + + function randomBase62(length: number): string { + const chars = + "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; + let result = ""; + const bytes = randomBytes(length); + for (let i = 0; i < length; i++) { + result += chars[bytes[i]! % 62]; + } + return result; + } + + export function create( + prefix: keyof typeof prefixes, + descending: boolean, + timestamp?: number, + ): string { + const currentTimestamp = timestamp ?? Date.now(); + + if (currentTimestamp !== lastTimestamp) { + lastTimestamp = currentTimestamp; + counter = 0; + } + counter++; + + let now = BigInt(currentTimestamp) * BigInt(0x1000) + BigInt(counter); + + now = descending ? ~now : now; + + const timeBytes = Buffer.alloc(6); + for (let i = 0; i < 6; i++) { + timeBytes[i] = Number((now >> BigInt(40 - 8 * i)) & BigInt(0xff)); + } + + return ( + prefixes[prefix] + + "_" + + timeBytes.toString("hex") + + randomBase62(LENGTH - 12) + ); + } +} diff --git a/packages/core/src/organisation/index.ts b/packages/core/src/organisation/index.ts index bcf5573e..c7dc7579 100644 --- a/packages/core/src/organisation/index.ts +++ b/packages/core/src/organisation/index.ts @@ -1,10 +1,11 @@ import { z } from "zod/v4"; import { Examples } from "../examples.js"; +import { Identifier } from "../id/index.js"; export namespace Organisation { export const Info = z .object({ - id: z.string().meta({ + id: Identifier.schema("organisation").meta({ description: "The unique identifier of the organisation", example: Examples.Organisation.id, }), diff --git a/packages/core/src/steam/index.ts b/packages/core/src/steam/index.ts index 55b3f63f..6f989fd1 100644 --- a/packages/core/src/steam/index.ts +++ b/packages/core/src/steam/index.ts @@ -1,15 +1,16 @@ import { z } from "zod/v4"; -import { Examples } from "../examples.js"; -import { Limitations, steamTable } from "./steam.sql.js"; import { fn } from "../utils/fn.js"; import { Actor } from "../actor.js"; -import { Database } from "../drizzle/index.js"; +import { Examples } from "../examples.js"; +import { Identifier } from "../id/index.js"; import { eq, isNull, and } from "drizzle-orm"; +import { Database } from "../drizzle/index.js"; +import { Limitations, steamTable } from "./steam.sql.js"; export namespace Steam { export const Info = z .object({ - id: z.string().meta({ + id: Identifier.schema("steam").meta({ description: "The unique Steam ID of the account", example: Examples.SteamAccount.id, }), diff --git a/packages/core/src/user/index.ts b/packages/core/src/user/index.ts index ceaa01d5..dc495b00 100644 --- a/packages/core/src/user/index.ts +++ b/packages/core/src/user/index.ts @@ -1,16 +1,16 @@ import { z } from "zod/v4"; import { fn } from "../utils/fn.js"; import { userTable } from "./user.sql.js"; -import { createID } from "../utils/id.js"; import { Examples } from "../examples.js"; import { Polar } from "../polar/index.js"; +import { Identifier } from "../id/index.js"; import { Database } from "../drizzle/index.js"; import { eq, and, isNotNull } from "drizzle-orm"; export namespace User { export const Info = z .object({ - id: z.string().meta({ + id: Identifier.schema("user").meta({ description: "The unique identifier of the user", example: Examples.User.id, }), @@ -38,7 +38,7 @@ export namespace User { export const create = fn( Info.omit({ customerID: true }).partial({ id: true }), async (input) => { - const id = input.id ?? createID("user"); + const id = Identifier.ascending("user", input.id); const customerID = await Polar.fromUserEmail(input.email).then( (z) => z.id, diff --git a/packages/core/src/utils/id.ts b/packages/core/src/utils/id.ts deleted file mode 100644 index 4e8a1ab9..00000000 --- a/packages/core/src/utils/id.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { ulid } from "ulid"; - -export const prefixes = { - user: "usr", - steam: "stm", - app: "app", - member: "mbr", - apiSecret: "sec", - apiPersonal: "pat", - organisation: "org", -} as const; - -/** - * Generates a unique identifier by concatenating a predefined prefix with a ULID. - * - * Given a key from the predefined prefixes mapping (e.g.,"user", "member", "steam"), - * this function retrieves the corresponding prefix and combines it with a ULID using an underscore - * as a separator. The resulting identifier is formatted as "prefix_ulid". - * - * @param prefix - A key from the prefixes mapping. - * @returns A unique identifier string. - */ -export function createID(prefix: keyof typeof prefixes): string { - return [prefixes[prefix], ulid()].join("_"); -} diff --git a/packages/function/src/api/routes/session.ts b/packages/function/src/api/routes/session.ts new file mode 100644 index 00000000..38db2020 --- /dev/null +++ b/packages/function/src/api/routes/session.ts @@ -0,0 +1,24 @@ +import { Hono } from "hono"; +import { describeRoute, resolver } from "hono-openapi"; + +export namespace SessionRoute { + export const route = new Hono().get( + "/", + describeRoute({ + tags: ["Session"], + operationId: "session.list", + summary: "List all active sessions", + description: "List all the current user's active sessions", + responses: { + 200:{ + description:"List of sessions", + content:{ + "application/json":{ + schema: resolver(Session.Info.array()) + } + } + } + }, + }), + ); +}