@storyshelf/auth 0.5.4 → 0.5.5

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.
@@ -1,7 +1,7 @@
1
1
  import "node:path";
2
2
  import "node:url";
3
3
  import.meta.url;
4
- import { boolean, integer, pgTable, text, timestamp } from "drizzle-orm/pg-core";
4
+ import { boolean, integer, pgTable, text, timestamp } from "@storyshelf/core/orm/pg-core";
5
5
  //#region src/auth-tables-pg.ts
6
6
  /**
7
7
  * Auth table definitions for Postgres (pg-core mirrors of `auth-tables.ts`).
@@ -1 +1 @@
1
- {"version":3,"file":"auth-tables-pg.mjs","names":[],"sources":["../src/auth-tables-pg.ts"],"sourcesContent":["/**\n * Auth table definitions for Postgres (pg-core mirrors of `auth-tables.ts`).\n *\n * Same model names, same camelCase keys, same snake_case columns; timestamps\n * ride `timestamp(..., { mode: \"string\" })` so the bridge keeps exchanging\n * ISO strings, booleans ride native `boolean`. The bridge consumes either\n * dialect through the shared {@link AuthBridgeSchema} registry.\n */\nimport { boolean, integer, pgTable, text, timestamp } from \"drizzle-orm/pg-core\";\n\n/** Narrow `user` table definition (Better Auth base model). */\nexport const authUserPg = pgTable(\"user\", {\n id: text(\"id\").primaryKey(),\n name: text(\"name\").notNull(),\n email: text(\"email\").notNull().unique(),\n emailVerified: boolean(\"email_verified\").notNull().default(false),\n image: text(\"image\"),\n createdAt: timestamp(\"created_at\", { withTimezone: true, mode: \"string\" }).notNull(),\n updatedAt: timestamp(\"updated_at\", { withTimezone: true, mode: \"string\" }).notNull(),\n});\n\n/** Narrow `session` table definition (Better Auth base model). */\nexport const authSessionPg = pgTable(\"session\", {\n id: text(\"id\").primaryKey(),\n userId: text(\"user_id\")\n .notNull()\n .references(() => authUserPg.id, { onDelete: \"cascade\" }),\n token: text(\"token\").notNull().unique(),\n expiresAt: timestamp(\"expires_at\", { withTimezone: true, mode: \"string\" }).notNull(),\n ipAddress: text(\"ip_address\"),\n userAgent: text(\"user_agent\"),\n createdAt: timestamp(\"created_at\", { withTimezone: true, mode: \"string\" }).notNull(),\n updatedAt: timestamp(\"updated_at\", { withTimezone: true, mode: \"string\" }).notNull(),\n});\n\n/** Narrow `account` table definition (Better Auth base model). */\nexport const authAccountPg = pgTable(\"account\", {\n id: text(\"id\").primaryKey(),\n userId: text(\"user_id\")\n .notNull()\n .references(() => authUserPg.id, { onDelete: \"cascade\" }),\n accountId: text(\"account_id\").notNull(),\n providerId: text(\"provider_id\").notNull(),\n accessToken: text(\"access_token\"),\n refreshToken: text(\"refresh_token\"),\n idToken: text(\"id_token\"),\n accessTokenExpiresAt: timestamp(\"access_token_expires_at\", {\n withTimezone: true,\n mode: \"string\",\n }),\n refreshTokenExpiresAt: timestamp(\"refresh_token_expires_at\", {\n withTimezone: true,\n mode: \"string\",\n }),\n scope: text(\"scope\"),\n expiresAt: timestamp(\"expires_at\", { withTimezone: true, mode: \"string\" }),\n password: text(\"password\"),\n createdAt: timestamp(\"created_at\", { withTimezone: true, mode: \"string\" }).notNull(),\n updatedAt: timestamp(\"updated_at\", { withTimezone: true, mode: \"string\" }).notNull(),\n});\n\n/** Narrow `verification` table definition (Better Auth base model). */\nexport const authVerificationPg = pgTable(\"verification\", {\n id: text(\"id\").primaryKey(),\n identifier: text(\"identifier\").notNull(),\n value: text(\"value\").notNull(),\n expiresAt: timestamp(\"expires_at\", { withTimezone: true, mode: \"string\" }).notNull(),\n createdAt: timestamp(\"created_at\", { withTimezone: true, mode: \"string\" }).notNull(),\n updatedAt: timestamp(\"updated_at\", { withTimezone: true, mode: \"string\" }).notNull(),\n});\n\n/** Narrow `passkey` table definition (passkey plugin model). */\nexport const authPasskeyPg = pgTable(\"passkey\", {\n id: text(\"id\").primaryKey(),\n name: text(\"name\"),\n publicKey: text(\"public_key\").notNull(),\n userId: text(\"user_id\")\n .notNull()\n .references(() => authUserPg.id, { onDelete: \"cascade\" }),\n credentialID: text(\"credential_id\").notNull(),\n counter: integer(\"counter\").notNull(),\n deviceType: text(\"device_type\").notNull(),\n backedUp: boolean(\"backed_up\").notNull().default(false),\n transports: text(\"transports\"),\n aaguid: text(\"aaguid\"),\n createdAt: timestamp(\"created_at\", { withTimezone: true, mode: \"string\" }).notNull(),\n // Nullable like sqlite: the plugin's registration create omits updatedAt.\n updatedAt: timestamp(\"updated_at\", { withTimezone: true, mode: \"string\" }),\n});\n\n/** Narrow `ssoProvider` table definition (SSO plugin model). */\nexport const authSsoProviderPg = pgTable(\"ssoProvider\", {\n id: text(\"id\").primaryKey(),\n issuer: text(\"issuer\").notNull(),\n oidcConfig: text(\"oidc_config\"),\n samlConfig: text(\"saml_config\"),\n userId: text(\"user_id\")\n .notNull()\n .references(() => authUserPg.id, { onDelete: \"cascade\" }),\n providerId: text(\"provider_id\").notNull().unique(),\n organizationId: text(\"organization_id\"),\n domain: text(\"domain\").notNull(),\n});\n\n/** Table handles for the Better Auth base models, keyed by model name. */\nexport const baseAuthTablesPg = {\n user: authUserPg,\n session: authSessionPg,\n account: authAccountPg,\n verification: authVerificationPg,\n passkey: authPasskeyPg,\n ssoProvider: authSsoProviderPg,\n};\n\n/** Date-valued fields per base model (ISO strings at rest, Dates in flight). */\nexport const baseAuthDateFieldsPg: Record<string, string[]> = {\n user: [\"createdAt\", \"updatedAt\"],\n session: [\"expiresAt\", \"createdAt\", \"updatedAt\"],\n account: [\"accessTokenExpiresAt\", \"refreshTokenExpiresAt\", \"expiresAt\", \"createdAt\", \"updatedAt\"],\n verification: [\"expiresAt\", \"createdAt\", \"updatedAt\"],\n passkey: [\"createdAt\", \"updatedAt\"],\n ssoProvider: [],\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"auth-tables-pg.mjs","names":[],"sources":["../src/auth-tables-pg.ts"],"sourcesContent":["/**\n * Auth table definitions for Postgres (pg-core mirrors of `auth-tables.ts`).\n *\n * Same model names, same camelCase keys, same snake_case columns; timestamps\n * ride `timestamp(..., { mode: \"string\" })` so the bridge keeps exchanging\n * ISO strings, booleans ride native `boolean`. The bridge consumes either\n * dialect through the shared {@link AuthBridgeSchema} registry.\n */\nimport { boolean, integer, pgTable, text, timestamp } from \"@storyshelf/core/orm/pg-core\";\n\n/** Narrow `user` table definition (Better Auth base model). */\nexport const authUserPg = pgTable(\"user\", {\n id: text(\"id\").primaryKey(),\n name: text(\"name\").notNull(),\n email: text(\"email\").notNull().unique(),\n emailVerified: boolean(\"email_verified\").notNull().default(false),\n image: text(\"image\"),\n createdAt: timestamp(\"created_at\", { withTimezone: true, mode: \"string\" }).notNull(),\n updatedAt: timestamp(\"updated_at\", { withTimezone: true, mode: \"string\" }).notNull(),\n});\n\n/** Narrow `session` table definition (Better Auth base model). */\nexport const authSessionPg = pgTable(\"session\", {\n id: text(\"id\").primaryKey(),\n userId: text(\"user_id\")\n .notNull()\n .references(() => authUserPg.id, { onDelete: \"cascade\" }),\n token: text(\"token\").notNull().unique(),\n expiresAt: timestamp(\"expires_at\", { withTimezone: true, mode: \"string\" }).notNull(),\n ipAddress: text(\"ip_address\"),\n userAgent: text(\"user_agent\"),\n createdAt: timestamp(\"created_at\", { withTimezone: true, mode: \"string\" }).notNull(),\n updatedAt: timestamp(\"updated_at\", { withTimezone: true, mode: \"string\" }).notNull(),\n});\n\n/** Narrow `account` table definition (Better Auth base model). */\nexport const authAccountPg = pgTable(\"account\", {\n id: text(\"id\").primaryKey(),\n userId: text(\"user_id\")\n .notNull()\n .references(() => authUserPg.id, { onDelete: \"cascade\" }),\n accountId: text(\"account_id\").notNull(),\n providerId: text(\"provider_id\").notNull(),\n accessToken: text(\"access_token\"),\n refreshToken: text(\"refresh_token\"),\n idToken: text(\"id_token\"),\n accessTokenExpiresAt: timestamp(\"access_token_expires_at\", {\n withTimezone: true,\n mode: \"string\",\n }),\n refreshTokenExpiresAt: timestamp(\"refresh_token_expires_at\", {\n withTimezone: true,\n mode: \"string\",\n }),\n scope: text(\"scope\"),\n expiresAt: timestamp(\"expires_at\", { withTimezone: true, mode: \"string\" }),\n password: text(\"password\"),\n createdAt: timestamp(\"created_at\", { withTimezone: true, mode: \"string\" }).notNull(),\n updatedAt: timestamp(\"updated_at\", { withTimezone: true, mode: \"string\" }).notNull(),\n});\n\n/** Narrow `verification` table definition (Better Auth base model). */\nexport const authVerificationPg = pgTable(\"verification\", {\n id: text(\"id\").primaryKey(),\n identifier: text(\"identifier\").notNull(),\n value: text(\"value\").notNull(),\n expiresAt: timestamp(\"expires_at\", { withTimezone: true, mode: \"string\" }).notNull(),\n createdAt: timestamp(\"created_at\", { withTimezone: true, mode: \"string\" }).notNull(),\n updatedAt: timestamp(\"updated_at\", { withTimezone: true, mode: \"string\" }).notNull(),\n});\n\n/** Narrow `passkey` table definition (passkey plugin model). */\nexport const authPasskeyPg = pgTable(\"passkey\", {\n id: text(\"id\").primaryKey(),\n name: text(\"name\"),\n publicKey: text(\"public_key\").notNull(),\n userId: text(\"user_id\")\n .notNull()\n .references(() => authUserPg.id, { onDelete: \"cascade\" }),\n credentialID: text(\"credential_id\").notNull(),\n counter: integer(\"counter\").notNull(),\n deviceType: text(\"device_type\").notNull(),\n backedUp: boolean(\"backed_up\").notNull().default(false),\n transports: text(\"transports\"),\n aaguid: text(\"aaguid\"),\n createdAt: timestamp(\"created_at\", { withTimezone: true, mode: \"string\" }).notNull(),\n // Nullable like sqlite: the plugin's registration create omits updatedAt.\n updatedAt: timestamp(\"updated_at\", { withTimezone: true, mode: \"string\" }),\n});\n\n/** Narrow `ssoProvider` table definition (SSO plugin model). */\nexport const authSsoProviderPg = pgTable(\"ssoProvider\", {\n id: text(\"id\").primaryKey(),\n issuer: text(\"issuer\").notNull(),\n oidcConfig: text(\"oidc_config\"),\n samlConfig: text(\"saml_config\"),\n userId: text(\"user_id\")\n .notNull()\n .references(() => authUserPg.id, { onDelete: \"cascade\" }),\n providerId: text(\"provider_id\").notNull().unique(),\n organizationId: text(\"organization_id\"),\n domain: text(\"domain\").notNull(),\n});\n\n/** Table handles for the Better Auth base models, keyed by model name. */\nexport const baseAuthTablesPg = {\n user: authUserPg,\n session: authSessionPg,\n account: authAccountPg,\n verification: authVerificationPg,\n passkey: authPasskeyPg,\n ssoProvider: authSsoProviderPg,\n};\n\n/** Date-valued fields per base model (ISO strings at rest, Dates in flight). */\nexport const baseAuthDateFieldsPg: Record<string, string[]> = {\n user: [\"createdAt\", \"updatedAt\"],\n session: [\"expiresAt\", \"createdAt\", \"updatedAt\"],\n account: [\"accessTokenExpiresAt\", \"refreshTokenExpiresAt\", \"expiresAt\", \"createdAt\", \"updatedAt\"],\n verification: [\"expiresAt\", \"createdAt\", \"updatedAt\"],\n passkey: [\"createdAt\", \"updatedAt\"],\n ssoProvider: [],\n};\n"],"mappings":"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"}
@@ -1,7 +1,7 @@
1
1
  import "node:path";
2
2
  import "node:url";
3
3
  import.meta.url;
4
- import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core";
4
+ import { integer, sqliteTable, text } from "@storyshelf/core/orm/sqlite-core";
5
5
  //#region src/auth-tables.ts
6
6
  /**
7
7
  * Auth table definitions: drizzle handles for the Better Auth base models.
@@ -1 +1 @@
1
- {"version":3,"file":"auth-tables.mjs","names":[],"sources":["../src/auth-tables.ts"],"sourcesContent":["/**\n * Auth table definitions: drizzle handles for the Better Auth base models.\n *\n * Keys are camelCase (repo convention); columns snake_case. Field names match\n * Better Auth defaults exactly, so the bridge needs no name translation.\n * Custom modelName/fieldName options are unsupported in v1 (fail fast).\n * DDL for drivers is vendored separately (see E3); these defs are the source\n * of truth the DDL must match (the strict schema checker enforces it).\n */\nimport { integer, sqliteTable, text } from \"drizzle-orm/sqlite-core\";\n\n/** Narrow `user` table definition (Better Auth base model). */\nexport const authUser = sqliteTable(\"user\", {\n id: text(\"id\").primaryKey(),\n name: text(\"name\").notNull(),\n email: text(\"email\").notNull().unique(),\n emailVerified: integer(\"email_verified\", { mode: \"boolean\" }).notNull().default(false),\n image: text(\"image\"),\n createdAt: text(\"created_at\").notNull(),\n updatedAt: text(\"updated_at\").notNull(),\n});\n\n/** Narrow `session` table definition (Better Auth base model). */\nexport const authSession = sqliteTable(\"session\", {\n id: text(\"id\").primaryKey(),\n userId: text(\"user_id\")\n .notNull()\n .references(() => authUser.id, { onDelete: \"cascade\" }),\n token: text(\"token\").notNull().unique(),\n expiresAt: text(\"expires_at\").notNull(),\n ipAddress: text(\"ip_address\"),\n userAgent: text(\"user_agent\"),\n createdAt: text(\"created_at\").notNull(),\n updatedAt: text(\"updated_at\").notNull(),\n});\n\n/** Narrow `account` table definition (Better Auth base model). */\nexport const authAccount = sqliteTable(\"account\", {\n id: text(\"id\").primaryKey(),\n userId: text(\"user_id\")\n .notNull()\n .references(() => authUser.id, { onDelete: \"cascade\" }),\n accountId: text(\"account_id\").notNull(),\n providerId: text(\"provider_id\").notNull(),\n accessToken: text(\"access_token\"),\n refreshToken: text(\"refresh_token\"),\n idToken: text(\"id_token\"),\n accessTokenExpiresAt: text(\"access_token_expires_at\"),\n refreshTokenExpiresAt: text(\"refresh_token_expires_at\"),\n scope: text(\"scope\"),\n expiresAt: text(\"expires_at\"),\n password: text(\"password\"),\n createdAt: text(\"created_at\").notNull(),\n updatedAt: text(\"updated_at\").notNull(),\n});\n\n/** Narrow `verification` table definition (Better Auth base model). */\nexport const authVerification = sqliteTable(\"verification\", {\n id: text(\"id\").primaryKey(),\n identifier: text(\"identifier\").notNull(),\n value: text(\"value\").notNull(),\n expiresAt: text(\"expires_at\").notNull(),\n createdAt: text(\"created_at\").notNull(),\n updatedAt: text(\"updated_at\").notNull(),\n});\n\n/** Narrow `passkey` table definition (passkey plugin model). */ export const authPasskey =\n sqliteTable(\"passkey\", {\n id: text(\"id\").primaryKey(),\n name: text(\"name\"),\n publicKey: text(\"public_key\").notNull(),\n userId: text(\"user_id\")\n .notNull()\n .references(() => authUser.id, { onDelete: \"cascade\" }),\n credentialID: text(\"credential_id\").notNull(),\n counter: integer(\"counter\").notNull(),\n deviceType: text(\"device_type\").notNull(),\n backedUp: integer(\"backed_up\", { mode: \"boolean\" }).notNull().default(false),\n transports: text(\"transports\"),\n aaguid: text(\"aaguid\"),\n createdAt: text(\"created_at\").notNull(),\n // Nullable by design: the plugin's verify-registration create omits\n // `updatedAt` (its model schema has no such field), so the bridge writes\n // null. Counter updates never touch it either.\n updatedAt: text(\"updated_at\"),\n });\n\n/** Narrow `ssoProvider` table definition (SSO plugin model). */\nexport const authSsoProvider = sqliteTable(\"ssoProvider\", {\n id: text(\"id\").primaryKey(),\n issuer: text(\"issuer\").notNull(),\n oidcConfig: text(\"oidc_config\"),\n samlConfig: text(\"saml_config\"),\n userId: text(\"user_id\")\n .notNull()\n .references(() => authUser.id, { onDelete: \"cascade\" }),\n providerId: text(\"provider_id\").notNull().unique(),\n organizationId: text(\"organization_id\"),\n domain: text(\"domain\").notNull(),\n});\n\n/** Table handles for the Better Auth base models, keyed by model name. */\nexport const baseAuthTables = {\n user: authUser,\n session: authSession,\n account: authAccount,\n verification: authVerification,\n passkey: authPasskey,\n ssoProvider: authSsoProvider,\n};\n\n/** Date-valued fields per base model (ISO strings at rest, Dates in flight). */\nexport const baseAuthDateFields: Record<string, string[]> = {\n user: [\"createdAt\", \"updatedAt\"],\n session: [\"expiresAt\", \"createdAt\", \"updatedAt\"],\n account: [\"accessTokenExpiresAt\", \"refreshTokenExpiresAt\", \"expiresAt\", \"createdAt\", \"updatedAt\"],\n verification: [\"expiresAt\", \"createdAt\", \"updatedAt\"],\n passkey: [\"createdAt\", \"updatedAt\"],\n // No timestamps: the SSO plugin never writes them (register create sends\n // no createdAt/updatedAt; defaultSSO entries write no rows at all).\n ssoProvider: [],\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"auth-tables.mjs","names":[],"sources":["../src/auth-tables.ts"],"sourcesContent":["/**\n * Auth table definitions: drizzle handles for the Better Auth base models.\n *\n * Keys are camelCase (repo convention); columns snake_case. Field names match\n * Better Auth defaults exactly, so the bridge needs no name translation.\n * Custom modelName/fieldName options are unsupported in v1 (fail fast).\n * DDL for drivers is vendored separately (see E3); these defs are the source\n * of truth the DDL must match (the strict schema checker enforces it).\n */\nimport { integer, sqliteTable, text } from \"@storyshelf/core/orm/sqlite-core\";\n\n/** Narrow `user` table definition (Better Auth base model). */\nexport const authUser = sqliteTable(\"user\", {\n id: text(\"id\").primaryKey(),\n name: text(\"name\").notNull(),\n email: text(\"email\").notNull().unique(),\n emailVerified: integer(\"email_verified\", { mode: \"boolean\" }).notNull().default(false),\n image: text(\"image\"),\n createdAt: text(\"created_at\").notNull(),\n updatedAt: text(\"updated_at\").notNull(),\n});\n\n/** Narrow `session` table definition (Better Auth base model). */\nexport const authSession = sqliteTable(\"session\", {\n id: text(\"id\").primaryKey(),\n userId: text(\"user_id\")\n .notNull()\n .references(() => authUser.id, { onDelete: \"cascade\" }),\n token: text(\"token\").notNull().unique(),\n expiresAt: text(\"expires_at\").notNull(),\n ipAddress: text(\"ip_address\"),\n userAgent: text(\"user_agent\"),\n createdAt: text(\"created_at\").notNull(),\n updatedAt: text(\"updated_at\").notNull(),\n});\n\n/** Narrow `account` table definition (Better Auth base model). */\nexport const authAccount = sqliteTable(\"account\", {\n id: text(\"id\").primaryKey(),\n userId: text(\"user_id\")\n .notNull()\n .references(() => authUser.id, { onDelete: \"cascade\" }),\n accountId: text(\"account_id\").notNull(),\n providerId: text(\"provider_id\").notNull(),\n accessToken: text(\"access_token\"),\n refreshToken: text(\"refresh_token\"),\n idToken: text(\"id_token\"),\n accessTokenExpiresAt: text(\"access_token_expires_at\"),\n refreshTokenExpiresAt: text(\"refresh_token_expires_at\"),\n scope: text(\"scope\"),\n expiresAt: text(\"expires_at\"),\n password: text(\"password\"),\n createdAt: text(\"created_at\").notNull(),\n updatedAt: text(\"updated_at\").notNull(),\n});\n\n/** Narrow `verification` table definition (Better Auth base model). */\nexport const authVerification = sqliteTable(\"verification\", {\n id: text(\"id\").primaryKey(),\n identifier: text(\"identifier\").notNull(),\n value: text(\"value\").notNull(),\n expiresAt: text(\"expires_at\").notNull(),\n createdAt: text(\"created_at\").notNull(),\n updatedAt: text(\"updated_at\").notNull(),\n});\n\n/** Narrow `passkey` table definition (passkey plugin model). */ export const authPasskey =\n sqliteTable(\"passkey\", {\n id: text(\"id\").primaryKey(),\n name: text(\"name\"),\n publicKey: text(\"public_key\").notNull(),\n userId: text(\"user_id\")\n .notNull()\n .references(() => authUser.id, { onDelete: \"cascade\" }),\n credentialID: text(\"credential_id\").notNull(),\n counter: integer(\"counter\").notNull(),\n deviceType: text(\"device_type\").notNull(),\n backedUp: integer(\"backed_up\", { mode: \"boolean\" }).notNull().default(false),\n transports: text(\"transports\"),\n aaguid: text(\"aaguid\"),\n createdAt: text(\"created_at\").notNull(),\n // Nullable by design: the plugin's verify-registration create omits\n // `updatedAt` (its model schema has no such field), so the bridge writes\n // null. Counter updates never touch it either.\n updatedAt: text(\"updated_at\"),\n });\n\n/** Narrow `ssoProvider` table definition (SSO plugin model). */\nexport const authSsoProvider = sqliteTable(\"ssoProvider\", {\n id: text(\"id\").primaryKey(),\n issuer: text(\"issuer\").notNull(),\n oidcConfig: text(\"oidc_config\"),\n samlConfig: text(\"saml_config\"),\n userId: text(\"user_id\")\n .notNull()\n .references(() => authUser.id, { onDelete: \"cascade\" }),\n providerId: text(\"provider_id\").notNull().unique(),\n organizationId: text(\"organization_id\"),\n domain: text(\"domain\").notNull(),\n});\n\n/** Table handles for the Better Auth base models, keyed by model name. */\nexport const baseAuthTables = {\n user: authUser,\n session: authSession,\n account: authAccount,\n verification: authVerification,\n passkey: authPasskey,\n ssoProvider: authSsoProvider,\n};\n\n/** Date-valued fields per base model (ISO strings at rest, Dates in flight). */\nexport const baseAuthDateFields: Record<string, string[]> = {\n user: [\"createdAt\", \"updatedAt\"],\n session: [\"expiresAt\", \"createdAt\", \"updatedAt\"],\n account: [\"accessTokenExpiresAt\", \"refreshTokenExpiresAt\", \"expiresAt\", \"createdAt\", \"updatedAt\"],\n verification: [\"expiresAt\", \"createdAt\", \"updatedAt\"],\n passkey: [\"createdAt\", \"updatedAt\"],\n // No timestamps: the SSO plugin never writes them (register create sends\n // no createdAt/updatedAt; defaultSSO entries write no rows at all).\n ssoProvider: [],\n};\n"],"mappings":"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"}
@@ -2,8 +2,8 @@ import "node:path";
2
2
  import "node:url";
3
3
  import.meta.url;
4
4
  import { ensureEngineIdentity, tableColumn } from "./invites.mjs";
5
+ import { eq } from "@storyshelf/core/orm";
5
6
  import { ulid } from "@storyshelf/core/utils";
6
- import { eq } from "drizzle-orm";
7
7
  //#region src/bootstrap.ts
8
8
  /** Find the env admin row, or stage it (admin role, no credential yet). */
9
9
  async function stagePasswordAdmin(db, email, name, now) {
@@ -1 +1 @@
1
- {"version":3,"file":"bootstrap.mjs","names":[],"sources":["../src/bootstrap.ts"],"sourcesContent":["/**\n * Development bootstrap: provision a local admin from environment credentials.\n *\n * Replaces the dropped shared-password tier for `dev-server`/`fly-app`:\n * with `AUTH_PASSWORD` set, the first boot creates an admin identity and\n * every boot aligns its engine credential with the env value (same\n * always-from-env semantics the shared tier had). Production deployments\n * use the invite flow instead.\n */\nimport type { DatabaseAdapter } from \"@storyshelf/core/adapter/database\";\nimport { ulid } from \"@storyshelf/core/utils\";\nimport { eq } from \"drizzle-orm\";\nimport { ensureEngineIdentity, MIN_PASSWORD_LENGTH, tableColumn } from \"./invites.ts\";\n\n/** Input for {@link ensurePasswordAdmin}. */\nexport interface PasswordAdminInput {\n email: string;\n name?: string;\n password: string;\n}\n\n/** Find the env admin row, or stage it (admin role, no credential yet). */\nasync function stagePasswordAdmin(\n db: DatabaseAdapter,\n email: string,\n name: string,\n now: string,\n): Promise<Record<string, unknown>> {\n const rows: Record<string, unknown>[] = await db.list(db.tables.users, {\n where: eq(tableColumn(db.tables.users, \"email\"), email),\n limit: 1,\n });\n const existing = rows[0];\n if (existing) {\n return existing;\n }\n return await db.insert(db.tables.users, {\n id: ulid(),\n email,\n name,\n avatarUrl: null,\n role: \"admin\",\n lastLoginAt: null,\n createdAt: now,\n passwordHash: null,\n displayNameOverride: null,\n authProvider: \"local\",\n disabled: false,\n });\n}\n\n/** Provision (or refresh the credential of) the env-driven local admin. */\nexport async function ensurePasswordAdmin(\n db: DatabaseAdapter,\n input: PasswordAdminInput,\n): Promise<void> {\n const email = input.email.trim().toLowerCase();\n if (input.password.length < MIN_PASSWORD_LENGTH) {\n throw new Error(\"Password must be at least 12 characters\");\n }\n const now = new Date().toISOString();\n const shelfUser = await stagePasswordAdmin(db, email, input.name ?? \"Admin\", now);\n await ensureEngineIdentity(db, shelfUser, input.password);\n}\n"],"mappings":";;;;;;;;AAsBA,eAAe,mBACb,IACA,OACA,MACA,KACkC;CAKlC,MAAM,YAAW,MAJ6B,GAAG,KAAK,GAAG,OAAO,OAAO;EACrE,OAAO,GAAG,YAAY,GAAG,OAAO,OAAO,OAAO,GAAG,KAAK;EACtD,OAAO;CACT,CAAC,EAAA,CACqB;CACtB,IAAI,UACF,OAAO;CAET,OAAO,MAAM,GAAG,OAAO,GAAG,OAAO,OAAO;EACtC,IAAI,KAAK;EACT;EACA;EACA,WAAW;EACX,MAAM;EACN,aAAa;EACb,WAAW;EACX,cAAc;EACd,qBAAqB;EACrB,cAAc;EACd,UAAU;CACZ,CAAC;AACH;;AAGA,eAAsB,oBACpB,IACA,OACe;CACf,MAAM,QAAQ,MAAM,MAAM,KAAK,CAAC,CAAC,YAAY;CAC7C,IAAI,MAAM,SAAS,SAAA,IACjB,MAAM,IAAI,MAAM,yCAAyC;CAE3D,MAAM,uBAAM,IAAI,KAAK,EAAA,CAAE,YAAY;CACnC,MAAM,YAAY,MAAM,mBAAmB,IAAI,OAAO,MAAM,QAAQ,SAAS,GAAG;CAChF,MAAM,qBAAqB,IAAI,WAAW,MAAM,QAAQ;AAC1D"}
1
+ {"version":3,"file":"bootstrap.mjs","names":[],"sources":["../src/bootstrap.ts"],"sourcesContent":["/**\n * Development bootstrap: provision a local admin from environment credentials.\n *\n * Replaces the dropped shared-password tier for `dev-server`/`fly-app`:\n * with `AUTH_PASSWORD` set, the first boot creates an admin identity and\n * every boot aligns its engine credential with the env value (same\n * always-from-env semantics the shared tier had). Production deployments\n * use the invite flow instead.\n */\nimport type { DatabaseAdapter } from \"@storyshelf/core/adapter/database\";\nimport { eq } from \"@storyshelf/core/orm\";\nimport { ulid } from \"@storyshelf/core/utils\";\nimport { ensureEngineIdentity, MIN_PASSWORD_LENGTH, tableColumn } from \"./invites.ts\";\n\n/** Input for {@link ensurePasswordAdmin}. */\nexport interface PasswordAdminInput {\n email: string;\n name?: string;\n password: string;\n}\n\n/** Find the env admin row, or stage it (admin role, no credential yet). */\nasync function stagePasswordAdmin(\n db: DatabaseAdapter,\n email: string,\n name: string,\n now: string,\n): Promise<Record<string, unknown>> {\n const rows: Record<string, unknown>[] = await db.list(db.tables.users, {\n where: eq(tableColumn(db.tables.users, \"email\"), email),\n limit: 1,\n });\n const existing = rows[0];\n if (existing) {\n return existing;\n }\n return await db.insert(db.tables.users, {\n id: ulid(),\n email,\n name,\n avatarUrl: null,\n role: \"admin\",\n lastLoginAt: null,\n createdAt: now,\n passwordHash: null,\n displayNameOverride: null,\n authProvider: \"local\",\n disabled: false,\n });\n}\n\n/** Provision (or refresh the credential of) the env-driven local admin. */\nexport async function ensurePasswordAdmin(\n db: DatabaseAdapter,\n input: PasswordAdminInput,\n): Promise<void> {\n const email = input.email.trim().toLowerCase();\n if (input.password.length < MIN_PASSWORD_LENGTH) {\n throw new Error(\"Password must be at least 12 characters\");\n }\n const now = new Date().toISOString();\n const shelfUser = await stagePasswordAdmin(db, email, input.name ?? \"Admin\", now);\n await ensureEngineIdentity(db, shelfUser, input.password);\n}\n"],"mappings":";;;;;;;;AAsBA,eAAe,mBACb,IACA,OACA,MACA,KACkC;CAKlC,MAAM,YAAW,MAJ6B,GAAG,KAAK,GAAG,OAAO,OAAO;EACrE,OAAO,GAAG,YAAY,GAAG,OAAO,OAAO,OAAO,GAAG,KAAK;EACtD,OAAO;CACT,CAAC,EAAA,CACqB;CACtB,IAAI,UACF,OAAO;CAET,OAAO,MAAM,GAAG,OAAO,GAAG,OAAO,OAAO;EACtC,IAAI,KAAK;EACT;EACA;EACA,WAAW;EACX,MAAM;EACN,aAAa;EACb,WAAW;EACX,cAAc;EACd,qBAAqB;EACrB,cAAc;EACd,UAAU;CACZ,CAAC;AACH;;AAGA,eAAsB,oBACpB,IACA,OACe;CACf,MAAM,QAAQ,MAAM,MAAM,KAAK,CAAC,CAAC,YAAY;CAC7C,IAAI,MAAM,SAAS,SAAA,IACjB,MAAM,IAAI,MAAM,yCAAyC;CAE3D,MAAM,uBAAM,IAAI,KAAK,EAAA,CAAE,YAAY;CACnC,MAAM,YAAY,MAAM,mBAAmB,IAAI,OAAO,MAAM,QAAQ,SAAS,GAAG;CAChF,MAAM,qBAAqB,IAAI,WAAW,MAAM,QAAQ;AAC1D"}
@@ -1,6 +1,6 @@
1
1
  import __tsdown_shims_path from 'node:path';
2
2
  import __tsdown_shims_url from 'node:url';
3
- import { Table } from "drizzle-orm";
3
+ import { Table } from "@storyshelf/core/orm";
4
4
  import { DBAdapterInstance } from "better-auth";
5
5
  import { DatabaseAdapter } from "@storyshelf/core/adapter/database";
6
6
  //#region src/db-bridge.d.ts
@@ -3,8 +3,8 @@ import "node:url";
3
3
  import.meta.url;
4
4
  import { applySelect, toDriverRow, toModelRow } from "./db-values.mjs";
5
5
  import { buildCondition, buildOrderBy } from "./db-where.mjs";
6
+ import { getTableColumns } from "@storyshelf/core/orm";
6
7
  import { ulid } from "@storyshelf/core/utils";
7
- import { getTableColumns } from "drizzle-orm";
8
8
  //#region src/db-bridge.ts
9
9
  function tableFor(schema, model) {
10
10
  const table = schema.tables[model];
@@ -1 +1 @@
1
- {"version":3,"file":"db-bridge.mjs","names":[],"sources":["../src/db-bridge.ts"],"sourcesContent":["import type { DatabaseAdapter, ListOptions, TxStore } from \"@storyshelf/core/adapter/database\";\nimport { ulid } from \"@storyshelf/core/utils\";\n/**\n * Opaque Better Auth database bridge over the shelf DatabaseAdapter.\n *\n * Better Auth talks to `DBAdapterInstance` (create/findOne/findMany/count/\n * update/updateMany/delete/deleteMany/consumeOne/incrementOne/transaction);\n * this maps each call onto {@link DatabaseAdapter} CRUD over an explicit\n * model→table registry. No dialect knowledge: sqlite, Turso, and Postgres\n * drivers all work untouched, present and future.\n */\nimport type { DBAdapterInstance, DBTransactionAdapter, Where } from \"better-auth\";\nimport type { SQL, Table } from \"drizzle-orm\";\nimport { getTableColumns } from \"drizzle-orm\";\nimport { applySelect, toDriverRow, toModelRow } from \"./db-values.ts\";\nimport { buildCondition, buildOrderBy } from \"./db-where.ts\";\n\n/** Model→table registry plus per-model date fields. */\nexport interface AuthBridgeSchema {\n /** Drizzle table handles keyed by Better Auth model name. */\n tables: Record<string, Table>;\n /** Date-valued fields per model (ISO at rest, Date in flight). */\n dateFields: Record<string, readonly string[]>;\n /** Id generator for creates missing an id (defaults to ULID). */\n generateId?: (model: string) => string;\n}\n\n/** CRUD surface the bridge is built over (adapter or transaction store). */\ntype CrudStore = Pick<TxStore, \"insert\" | \"update\" | \"get\" | \"remove\" | \"list\" | \"count\">;\n\ninterface OpInput {\n model: string;\n}\n\nfunction tableFor(schema: AuthBridgeSchema, model: string): Table {\n const table = schema.tables[model];\n if (!table) {\n throw new Error(`Unknown auth model \"${model}\"`);\n }\n return table;\n}\n\nfunction datesFor(schema: AuthBridgeSchema, model: string): readonly string[] {\n return schema.dateFields[model] ?? [];\n}\n\nfunction defaultGenerateId(): string {\n return ulid();\n}\n\nfunction whereOf(table: Table, where: readonly Where[] | undefined): SQL | undefined {\n if (!where || where.length === 0) {\n return undefined;\n }\n return buildCondition(table, where);\n}\n\nasync function findFirst(\n store: CrudStore,\n schema: AuthBridgeSchema,\n model: string,\n where: readonly Where[] | undefined,\n): Promise<Record<string, unknown> | null> {\n const table = tableFor(schema, model);\n const rows = (await store.list(table, { where: whereOf(table, where), limit: 1 })) as Record<\n string,\n unknown\n >[];\n const row = rows[0];\n return row ? toModelRow(row, datesFor(schema, model)) : null;\n}\n\nasync function updateById(\n store: CrudStore,\n schema: AuthBridgeSchema,\n model: string,\n id: string,\n update: Record<string, unknown>,\n): Promise<Record<string, unknown> | null> {\n const table = tableFor(schema, model);\n const existing = await store.get(table, id);\n if (!existing) {\n return null;\n }\n const updated = await store.update(table, id, toDriverRow(update));\n return toModelRow(updated, datesFor(schema, model));\n}\n\nasync function opCreate(\n store: CrudStore,\n schema: AuthBridgeSchema,\n input: OpInput & { data: Record<string, unknown> },\n): Promise<Record<string, unknown>> {\n const table = tableFor(schema, input.model);\n const generate = schema.generateId ?? defaultGenerateId;\n const data =\n input.data[\"id\"] === undefined || input.data[\"id\"] === null\n ? { ...input.data, id: generate(input.model) }\n : input.data;\n const inserted = await store.insert(table, toDriverRow(data));\n return toModelRow(inserted, datesFor(schema, input.model));\n}\n\nasync function opUpdate(\n store: CrudStore,\n schema: AuthBridgeSchema,\n input: OpInput & { where: Where[]; update: Record<string, unknown> },\n): Promise<Record<string, unknown> | null> {\n const row = await findFirst(store, schema, input.model, input.where);\n if (!row || typeof row[\"id\"] !== \"string\") {\n return null;\n }\n return await updateById(store, schema, input.model, row[\"id\"], input.update);\n}\n\nasync function opUpdateMany(\n store: CrudStore,\n schema: AuthBridgeSchema,\n input: OpInput & { where: Where[]; update: Record<string, unknown> },\n): Promise<number> {\n const table = tableFor(schema, input.model);\n const rows = (await store.list(table, {\n where: whereOf(table, input.where),\n })) as Record<string, unknown>[];\n const ids = rows.map((row) => row[\"id\"]).filter((id): id is string => typeof id === \"string\");\n await Promise.all(\n ids.map(async (id) => await updateById(store, schema, input.model, id, input.update)),\n );\n return ids.length;\n}\n\nasync function opDeleteMany(\n store: CrudStore,\n schema: AuthBridgeSchema,\n input: OpInput & { where: Where[] },\n): Promise<number> {\n const table = tableFor(schema, input.model);\n const rows = (await store.list(table, {\n where: whereOf(table, input.where),\n })) as Record<string, unknown>[];\n const ids = rows.map((row) => row[\"id\"]).filter((id): id is string => typeof id === \"string\");\n await Promise.all(\n ids.map(async (id) => {\n await store.remove(table, id);\n }),\n );\n return ids.length;\n}\n\nasync function opConsumeOne(\n store: CrudStore,\n schema: AuthBridgeSchema,\n input: OpInput & { where: Where[] },\n): Promise<Record<string, unknown> | null> {\n // Runs directly on the given store: the top-level binding wraps this in\n // `transact` for atomicity, while transaction-scoped bindings pass the\n // ambient `tx` so Better Auth's own transaction wrapper never nests a\n // second `begin` on single-connection drivers (sqlite).\n const row = await findFirst(store, schema, input.model, input.where);\n if (!row || typeof row[\"id\"] !== \"string\") {\n return null;\n }\n await store.remove(tableFor(schema, input.model), row[\"id\"]);\n return row;\n}\n\nasync function opIncrementOne(\n store: CrudStore,\n schema: AuthBridgeSchema,\n input: OpInput & {\n where: Where[];\n increment: Record<string, number>;\n set?: Record<string, unknown>;\n },\n): Promise<Record<string, unknown> | null> {\n const row = await findFirst(store, schema, input.model, input.where);\n if (!row || typeof row[\"id\"] !== \"string\") {\n return null;\n }\n const deltas = Object.fromEntries(\n Object.entries(input.increment).map(([field, delta]) => {\n const current = row[field];\n return [field, (typeof current === \"number\" ? current : 0) + delta];\n }),\n );\n return await updateById(store, schema, input.model, row[\"id\"], {\n ...deltas,\n ...input.set,\n });\n}\n\nfunction hasColumn(table: Table, field: string): boolean {\n return getTableColumns(table)[field] !== undefined;\n}\n\nasync function joinMany(\n store: CrudStore,\n schema: AuthBridgeSchema,\n parentModel: string,\n parentId: unknown,\n joinedModel: string,\n limit: number | undefined,\n): Promise<Record<string, unknown>[]> {\n const table = tableFor(schema, joinedModel);\n const opts: ListOptions = { limit };\n const where = whereOf(table, [{ field: `${parentModel}Id`, value: parentId as never }]);\n if (where) {\n opts.where = where;\n }\n const rows = (await store.list(table, opts)) as Record<string, unknown>[];\n return rows.map((row) => toModelRow(row, datesFor(schema, joinedModel)));\n}\n\nasync function joinRelation(\n store: CrudStore,\n schema: AuthBridgeSchema,\n parentModel: string,\n parent: Record<string, unknown>,\n joinedModel: string,\n limit: number | undefined,\n): Promise<Record<string, unknown>[] | Record<string, unknown> | null> {\n const table = tableFor(schema, joinedModel);\n if (hasColumn(table, `${parentModel}Id`)) {\n return await joinMany(store, schema, parentModel, parent[\"id\"], joinedModel, limit);\n }\n if (typeof parent[`${joinedModel}Id`] === \"string\") {\n return await findFirst(store, schema, joinedModel, [\n { field: \"id\", value: parent[`${joinedModel}Id`] as never },\n ]);\n }\n throw new Error(`Unknown auth relation \"${parentModel}.${joinedModel}\"`);\n}\n\nasync function attachJoins(\n store: CrudStore,\n schema: AuthBridgeSchema,\n parentModel: string,\n parent: Record<string, unknown>,\n join: Record<string, boolean | { limit?: number }> | undefined,\n): Promise<Record<string, unknown>> {\n if (!join) {\n return {};\n }\n const attached: Record<string, unknown> = {};\n const entries = Object.entries(join);\n await Promise.all(\n entries.map(async ([joinedModel, joinAttr]) => {\n const limit = typeof joinAttr === \"object\" ? joinAttr.limit : undefined;\n attached[joinedModel] = await joinRelation(\n store,\n schema,\n parentModel,\n parent,\n joinedModel,\n limit,\n );\n }),\n );\n return attached;\n}\n\ntype FindManyInput = OpInput & {\n where?: Where[];\n limit?: number;\n offset?: number;\n sortBy?: { field: string; direction: \"asc\" | \"desc\" };\n select?: string[];\n join?: Record<string, boolean | { limit?: number }>;\n};\n\nasync function listRows(\n store: CrudStore,\n schema: AuthBridgeSchema,\n input: FindManyInput,\n): Promise<Record<string, unknown>[]> {\n const table = tableFor(schema, input.model);\n const opts: ListOptions = { limit: input.limit, offset: input.offset };\n const where = whereOf(table, input.where);\n if (where) {\n opts.where = where;\n }\n const orderBy = buildOrderBy(table, input.sortBy);\n if (orderBy) {\n opts.orderBy = orderBy;\n }\n const listed: Record<string, unknown>[] = await store.list(table, opts);\n return listed;\n}\n\nasync function opFindMany(\n store: CrudStore,\n schema: AuthBridgeSchema,\n input: FindManyInput,\n): Promise<Record<string, unknown>[]> {\n const rows = await listRows(store, schema, input);\n return await Promise.all(\n rows.map(async (row) => {\n const withJoins = input.join\n ? { ...row, ...(await attachJoins(store, schema, input.model, row, input.join)) }\n : row;\n return applySelect(toModelRow(withJoins, datesFor(schema, input.model)), input.select);\n }),\n );\n}\n\n/** One bound operation set (adapter top-level or transaction store). */\ninterface JoinSpec {\n join?: Record<string, boolean | { limit?: number }>;\n}\n\ninterface BoundOps {\n create(input: OpInput & { data: Record<string, unknown> }): Promise<Record<string, unknown>>;\n findOne(\n input: OpInput & { where: Where[]; select?: string[] } & JoinSpec,\n ): Promise<Record<string, unknown> | null>;\n findMany(\n input: OpInput & {\n where?: Where[];\n limit?: number;\n offset?: number;\n sortBy?: { field: string; direction: \"asc\" | \"desc\" };\n select?: string[];\n } & JoinSpec,\n ): Promise<Record<string, unknown>[]>;\n count(input: OpInput & { where?: Where[] }): Promise<number>;\n update(\n input: OpInput & { where: Where[]; update: Record<string, unknown> },\n ): Promise<Record<string, unknown> | null>;\n updateMany(input: OpInput & { where: Where[]; update: Record<string, unknown> }): Promise<number>;\n delete(input: OpInput & { where: Where[] }): Promise<void>;\n deleteMany(input: OpInput & { where: Where[] }): Promise<number>;\n consumeOne(input: OpInput & { where: Where[] }): Promise<Record<string, unknown> | null>;\n incrementOne(\n input: OpInput & {\n where: Where[];\n increment: Record<string, number>;\n set?: Record<string, unknown>;\n },\n ): Promise<Record<string, unknown> | null>;\n}\n\nasync function opFindOne(\n store: CrudStore,\n schema: AuthBridgeSchema,\n input: OpInput & { where: Where[]; select?: string[] } & JoinSpec,\n): Promise<Record<string, unknown> | null> {\n const row = await findFirst(store, schema, input.model, input.where);\n if (!row) {\n return null;\n }\n const withJoins = input.join\n ? { ...row, ...(await attachJoins(store, schema, input.model, row, input.join)) }\n : row;\n return applySelect(withJoins, input.select);\n}\n\n/** Read operations bound to one store. */\nfunction bindQueryOps(\n store: CrudStore,\n schema: AuthBridgeSchema,\n): Pick<BoundOps, \"findOne\" | \"findMany\" | \"count\"> {\n return {\n findOne: async (\n input: OpInput & { where: Where[]; select?: string[] } & JoinSpec,\n ): Promise<Record<string, unknown> | null> => await opFindOne(store, schema, input),\n findMany: async (\n input: OpInput & {\n where?: Where[];\n limit?: number;\n offset?: number;\n sortBy?: { field: string; direction: \"asc\" | \"desc\" };\n select?: string[];\n } & JoinSpec,\n ): Promise<Record<string, unknown>[]> => await opFindMany(store, schema, input),\n count: async (input: OpInput & { where?: Where[] }): Promise<number> => {\n const table = tableFor(schema, input.model);\n return await store.count(table, whereOf(table, input.where));\n },\n };\n}\n\n/** Write operations bound to one store (transaction-aware where marked). */\nfunction bindMutationOps(\n store: CrudStore,\n schema: AuthBridgeSchema,\n db: DatabaseAdapter,\n): Omit<BoundOps, \"findOne\" | \"findMany\" | \"count\"> {\n return {\n create: async (\n input: OpInput & { data: Record<string, unknown> },\n ): Promise<Record<string, unknown>> => await opCreate(store, schema, input),\n update: async (\n input: OpInput & { where: Where[]; update: Record<string, unknown> },\n ): Promise<Record<string, unknown> | null> => await opUpdate(store, schema, input),\n updateMany: async (\n input: OpInput & { where: Where[]; update: Record<string, unknown> },\n ): Promise<number> => await opUpdateMany(store, schema, input),\n delete: async (input: OpInput & { where: Where[] }): Promise<void> => {\n const row = await findFirst(store, schema, input.model, input.where);\n if (row && typeof row[\"id\"] === \"string\") {\n await store.remove(tableFor(schema, input.model), row[\"id\"]);\n }\n },\n deleteMany: async (input: OpInput & { where: Where[] }): Promise<number> =>\n await opDeleteMany(store, schema, input),\n consumeOne: async (\n input: OpInput & { where: Where[] },\n ): Promise<Record<string, unknown> | null> => {\n // Atomic at the top level when the driver offers transact; inside a\n // Better Auth transaction `store` is the ambient `tx`, which must run\n // directly (a second `begin` fails on single-connection sqlite).\n if (store === db && db.transact) {\n return await db.transact(async (tx) => await opConsumeOne(tx, schema, input));\n }\n return await opConsumeOne(store, schema, input);\n },\n incrementOne: async (\n input: OpInput & {\n where: Where[];\n increment: Record<string, number>;\n set?: Record<string, unknown>;\n },\n ): Promise<Record<string, unknown> | null> => {\n if (store === db && db.transact) {\n return await db.transact(async (tx) => await opIncrementOne(tx, schema, input));\n }\n return await opIncrementOne(store, schema, input);\n },\n };\n}\n\n/** Build the Better Auth database adapter over a shelf database. */\nexport function createShelfDbBridge(\n db: DatabaseAdapter,\n schema: AuthBridgeSchema,\n): DBAdapterInstance {\n return (_options: unknown) => {\n const ops: BoundOps = { ...bindQueryOps(db, schema), ...bindMutationOps(db, schema, db) };\n return {\n ...ops,\n transaction: async <R>(callback: (trx: DBTransactionAdapter) => Promise<R>): Promise<R> => {\n if (db.transact) {\n return await db.transact(async (tx) => {\n const txOps: BoundOps = {\n ...bindQueryOps(tx, schema),\n ...bindMutationOps(tx, schema, db),\n };\n return await callback(txOps as never);\n });\n }\n return await callback(ops as never);\n },\n options: { adapterName: \"storyshelf\", supportsDates: false, supportsJSON: false },\n } as never;\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"db-bridge.mjs","names":[],"sources":["../src/db-bridge.ts"],"sourcesContent":["import type { DatabaseAdapter, ListOptions, TxStore } from \"@storyshelf/core/adapter/database\";\nimport type { SQL, Table } from \"@storyshelf/core/orm\";\nimport { getTableColumns } from \"@storyshelf/core/orm\";\nimport { ulid } from \"@storyshelf/core/utils\";\n/**\n * Opaque Better Auth database bridge over the shelf DatabaseAdapter.\n *\n * Better Auth talks to `DBAdapterInstance` (create/findOne/findMany/count/\n * update/updateMany/delete/deleteMany/consumeOne/incrementOne/transaction);\n * this maps each call onto {@link DatabaseAdapter} CRUD over an explicit\n * model→table registry. No dialect knowledge: sqlite, Turso, and Postgres\n * drivers all work untouched, present and future.\n */\nimport type { DBAdapterInstance, DBTransactionAdapter, Where } from \"better-auth\";\nimport { applySelect, toDriverRow, toModelRow } from \"./db-values.ts\";\nimport { buildCondition, buildOrderBy } from \"./db-where.ts\";\n\n/** Model→table registry plus per-model date fields. */\nexport interface AuthBridgeSchema {\n /** Drizzle table handles keyed by Better Auth model name. */\n tables: Record<string, Table>;\n /** Date-valued fields per model (ISO at rest, Date in flight). */\n dateFields: Record<string, readonly string[]>;\n /** Id generator for creates missing an id (defaults to ULID). */\n generateId?: (model: string) => string;\n}\n\n/** CRUD surface the bridge is built over (adapter or transaction store). */\ntype CrudStore = Pick<TxStore, \"insert\" | \"update\" | \"get\" | \"remove\" | \"list\" | \"count\">;\n\ninterface OpInput {\n model: string;\n}\n\nfunction tableFor(schema: AuthBridgeSchema, model: string): Table {\n const table = schema.tables[model];\n if (!table) {\n throw new Error(`Unknown auth model \"${model}\"`);\n }\n return table;\n}\n\nfunction datesFor(schema: AuthBridgeSchema, model: string): readonly string[] {\n return schema.dateFields[model] ?? [];\n}\n\nfunction defaultGenerateId(): string {\n return ulid();\n}\n\nfunction whereOf(table: Table, where: readonly Where[] | undefined): SQL | undefined {\n if (!where || where.length === 0) {\n return undefined;\n }\n return buildCondition(table, where);\n}\n\nasync function findFirst(\n store: CrudStore,\n schema: AuthBridgeSchema,\n model: string,\n where: readonly Where[] | undefined,\n): Promise<Record<string, unknown> | null> {\n const table = tableFor(schema, model);\n const rows = (await store.list(table, { where: whereOf(table, where), limit: 1 })) as Record<\n string,\n unknown\n >[];\n const row = rows[0];\n return row ? toModelRow(row, datesFor(schema, model)) : null;\n}\n\nasync function updateById(\n store: CrudStore,\n schema: AuthBridgeSchema,\n model: string,\n id: string,\n update: Record<string, unknown>,\n): Promise<Record<string, unknown> | null> {\n const table = tableFor(schema, model);\n const existing = await store.get(table, id);\n if (!existing) {\n return null;\n }\n const updated = await store.update(table, id, toDriverRow(update));\n return toModelRow(updated, datesFor(schema, model));\n}\n\nasync function opCreate(\n store: CrudStore,\n schema: AuthBridgeSchema,\n input: OpInput & { data: Record<string, unknown> },\n): Promise<Record<string, unknown>> {\n const table = tableFor(schema, input.model);\n const generate = schema.generateId ?? defaultGenerateId;\n const data =\n input.data[\"id\"] === undefined || input.data[\"id\"] === null\n ? { ...input.data, id: generate(input.model) }\n : input.data;\n const inserted = await store.insert(table, toDriverRow(data));\n return toModelRow(inserted, datesFor(schema, input.model));\n}\n\nasync function opUpdate(\n store: CrudStore,\n schema: AuthBridgeSchema,\n input: OpInput & { where: Where[]; update: Record<string, unknown> },\n): Promise<Record<string, unknown> | null> {\n const row = await findFirst(store, schema, input.model, input.where);\n if (!row || typeof row[\"id\"] !== \"string\") {\n return null;\n }\n return await updateById(store, schema, input.model, row[\"id\"], input.update);\n}\n\nasync function opUpdateMany(\n store: CrudStore,\n schema: AuthBridgeSchema,\n input: OpInput & { where: Where[]; update: Record<string, unknown> },\n): Promise<number> {\n const table = tableFor(schema, input.model);\n const rows = (await store.list(table, {\n where: whereOf(table, input.where),\n })) as Record<string, unknown>[];\n const ids = rows.map((row) => row[\"id\"]).filter((id): id is string => typeof id === \"string\");\n await Promise.all(\n ids.map(async (id) => await updateById(store, schema, input.model, id, input.update)),\n );\n return ids.length;\n}\n\nasync function opDeleteMany(\n store: CrudStore,\n schema: AuthBridgeSchema,\n input: OpInput & { where: Where[] },\n): Promise<number> {\n const table = tableFor(schema, input.model);\n const rows = (await store.list(table, {\n where: whereOf(table, input.where),\n })) as Record<string, unknown>[];\n const ids = rows.map((row) => row[\"id\"]).filter((id): id is string => typeof id === \"string\");\n await Promise.all(\n ids.map(async (id) => {\n await store.remove(table, id);\n }),\n );\n return ids.length;\n}\n\nasync function opConsumeOne(\n store: CrudStore,\n schema: AuthBridgeSchema,\n input: OpInput & { where: Where[] },\n): Promise<Record<string, unknown> | null> {\n // Runs directly on the given store: the top-level binding wraps this in\n // `transact` for atomicity, while transaction-scoped bindings pass the\n // ambient `tx` so Better Auth's own transaction wrapper never nests a\n // second `begin` on single-connection drivers (sqlite).\n const row = await findFirst(store, schema, input.model, input.where);\n if (!row || typeof row[\"id\"] !== \"string\") {\n return null;\n }\n await store.remove(tableFor(schema, input.model), row[\"id\"]);\n return row;\n}\n\nasync function opIncrementOne(\n store: CrudStore,\n schema: AuthBridgeSchema,\n input: OpInput & {\n where: Where[];\n increment: Record<string, number>;\n set?: Record<string, unknown>;\n },\n): Promise<Record<string, unknown> | null> {\n const row = await findFirst(store, schema, input.model, input.where);\n if (!row || typeof row[\"id\"] !== \"string\") {\n return null;\n }\n const deltas = Object.fromEntries(\n Object.entries(input.increment).map(([field, delta]) => {\n const current = row[field];\n return [field, (typeof current === \"number\" ? current : 0) + delta];\n }),\n );\n return await updateById(store, schema, input.model, row[\"id\"], {\n ...deltas,\n ...input.set,\n });\n}\n\nfunction hasColumn(table: Table, field: string): boolean {\n return getTableColumns(table)[field] !== undefined;\n}\n\nasync function joinMany(\n store: CrudStore,\n schema: AuthBridgeSchema,\n parentModel: string,\n parentId: unknown,\n joinedModel: string,\n limit: number | undefined,\n): Promise<Record<string, unknown>[]> {\n const table = tableFor(schema, joinedModel);\n const opts: ListOptions = { limit };\n const where = whereOf(table, [{ field: `${parentModel}Id`, value: parentId as never }]);\n if (where) {\n opts.where = where;\n }\n const rows = (await store.list(table, opts)) as Record<string, unknown>[];\n return rows.map((row) => toModelRow(row, datesFor(schema, joinedModel)));\n}\n\nasync function joinRelation(\n store: CrudStore,\n schema: AuthBridgeSchema,\n parentModel: string,\n parent: Record<string, unknown>,\n joinedModel: string,\n limit: number | undefined,\n): Promise<Record<string, unknown>[] | Record<string, unknown> | null> {\n const table = tableFor(schema, joinedModel);\n if (hasColumn(table, `${parentModel}Id`)) {\n return await joinMany(store, schema, parentModel, parent[\"id\"], joinedModel, limit);\n }\n if (typeof parent[`${joinedModel}Id`] === \"string\") {\n return await findFirst(store, schema, joinedModel, [\n { field: \"id\", value: parent[`${joinedModel}Id`] as never },\n ]);\n }\n throw new Error(`Unknown auth relation \"${parentModel}.${joinedModel}\"`);\n}\n\nasync function attachJoins(\n store: CrudStore,\n schema: AuthBridgeSchema,\n parentModel: string,\n parent: Record<string, unknown>,\n join: Record<string, boolean | { limit?: number }> | undefined,\n): Promise<Record<string, unknown>> {\n if (!join) {\n return {};\n }\n const attached: Record<string, unknown> = {};\n const entries = Object.entries(join);\n await Promise.all(\n entries.map(async ([joinedModel, joinAttr]) => {\n const limit = typeof joinAttr === \"object\" ? joinAttr.limit : undefined;\n attached[joinedModel] = await joinRelation(\n store,\n schema,\n parentModel,\n parent,\n joinedModel,\n limit,\n );\n }),\n );\n return attached;\n}\n\ntype FindManyInput = OpInput & {\n where?: Where[];\n limit?: number;\n offset?: number;\n sortBy?: { field: string; direction: \"asc\" | \"desc\" };\n select?: string[];\n join?: Record<string, boolean | { limit?: number }>;\n};\n\nasync function listRows(\n store: CrudStore,\n schema: AuthBridgeSchema,\n input: FindManyInput,\n): Promise<Record<string, unknown>[]> {\n const table = tableFor(schema, input.model);\n const opts: ListOptions = { limit: input.limit, offset: input.offset };\n const where = whereOf(table, input.where);\n if (where) {\n opts.where = where;\n }\n const orderBy = buildOrderBy(table, input.sortBy);\n if (orderBy) {\n opts.orderBy = orderBy;\n }\n const listed: Record<string, unknown>[] = await store.list(table, opts);\n return listed;\n}\n\nasync function opFindMany(\n store: CrudStore,\n schema: AuthBridgeSchema,\n input: FindManyInput,\n): Promise<Record<string, unknown>[]> {\n const rows = await listRows(store, schema, input);\n return await Promise.all(\n rows.map(async (row) => {\n const withJoins = input.join\n ? { ...row, ...(await attachJoins(store, schema, input.model, row, input.join)) }\n : row;\n return applySelect(toModelRow(withJoins, datesFor(schema, input.model)), input.select);\n }),\n );\n}\n\n/** One bound operation set (adapter top-level or transaction store). */\ninterface JoinSpec {\n join?: Record<string, boolean | { limit?: number }>;\n}\n\ninterface BoundOps {\n create(input: OpInput & { data: Record<string, unknown> }): Promise<Record<string, unknown>>;\n findOne(\n input: OpInput & { where: Where[]; select?: string[] } & JoinSpec,\n ): Promise<Record<string, unknown> | null>;\n findMany(\n input: OpInput & {\n where?: Where[];\n limit?: number;\n offset?: number;\n sortBy?: { field: string; direction: \"asc\" | \"desc\" };\n select?: string[];\n } & JoinSpec,\n ): Promise<Record<string, unknown>[]>;\n count(input: OpInput & { where?: Where[] }): Promise<number>;\n update(\n input: OpInput & { where: Where[]; update: Record<string, unknown> },\n ): Promise<Record<string, unknown> | null>;\n updateMany(input: OpInput & { where: Where[]; update: Record<string, unknown> }): Promise<number>;\n delete(input: OpInput & { where: Where[] }): Promise<void>;\n deleteMany(input: OpInput & { where: Where[] }): Promise<number>;\n consumeOne(input: OpInput & { where: Where[] }): Promise<Record<string, unknown> | null>;\n incrementOne(\n input: OpInput & {\n where: Where[];\n increment: Record<string, number>;\n set?: Record<string, unknown>;\n },\n ): Promise<Record<string, unknown> | null>;\n}\n\nasync function opFindOne(\n store: CrudStore,\n schema: AuthBridgeSchema,\n input: OpInput & { where: Where[]; select?: string[] } & JoinSpec,\n): Promise<Record<string, unknown> | null> {\n const row = await findFirst(store, schema, input.model, input.where);\n if (!row) {\n return null;\n }\n const withJoins = input.join\n ? { ...row, ...(await attachJoins(store, schema, input.model, row, input.join)) }\n : row;\n return applySelect(withJoins, input.select);\n}\n\n/** Read operations bound to one store. */\nfunction bindQueryOps(\n store: CrudStore,\n schema: AuthBridgeSchema,\n): Pick<BoundOps, \"findOne\" | \"findMany\" | \"count\"> {\n return {\n findOne: async (\n input: OpInput & { where: Where[]; select?: string[] } & JoinSpec,\n ): Promise<Record<string, unknown> | null> => await opFindOne(store, schema, input),\n findMany: async (\n input: OpInput & {\n where?: Where[];\n limit?: number;\n offset?: number;\n sortBy?: { field: string; direction: \"asc\" | \"desc\" };\n select?: string[];\n } & JoinSpec,\n ): Promise<Record<string, unknown>[]> => await opFindMany(store, schema, input),\n count: async (input: OpInput & { where?: Where[] }): Promise<number> => {\n const table = tableFor(schema, input.model);\n return await store.count(table, whereOf(table, input.where));\n },\n };\n}\n\n/** Write operations bound to one store (transaction-aware where marked). */\nfunction bindMutationOps(\n store: CrudStore,\n schema: AuthBridgeSchema,\n db: DatabaseAdapter,\n): Omit<BoundOps, \"findOne\" | \"findMany\" | \"count\"> {\n return {\n create: async (\n input: OpInput & { data: Record<string, unknown> },\n ): Promise<Record<string, unknown>> => await opCreate(store, schema, input),\n update: async (\n input: OpInput & { where: Where[]; update: Record<string, unknown> },\n ): Promise<Record<string, unknown> | null> => await opUpdate(store, schema, input),\n updateMany: async (\n input: OpInput & { where: Where[]; update: Record<string, unknown> },\n ): Promise<number> => await opUpdateMany(store, schema, input),\n delete: async (input: OpInput & { where: Where[] }): Promise<void> => {\n const row = await findFirst(store, schema, input.model, input.where);\n if (row && typeof row[\"id\"] === \"string\") {\n await store.remove(tableFor(schema, input.model), row[\"id\"]);\n }\n },\n deleteMany: async (input: OpInput & { where: Where[] }): Promise<number> =>\n await opDeleteMany(store, schema, input),\n consumeOne: async (\n input: OpInput & { where: Where[] },\n ): Promise<Record<string, unknown> | null> => {\n // Atomic at the top level when the driver offers transact; inside a\n // Better Auth transaction `store` is the ambient `tx`, which must run\n // directly (a second `begin` fails on single-connection sqlite).\n if (store === db && db.transact) {\n return await db.transact(async (tx) => await opConsumeOne(tx, schema, input));\n }\n return await opConsumeOne(store, schema, input);\n },\n incrementOne: async (\n input: OpInput & {\n where: Where[];\n increment: Record<string, number>;\n set?: Record<string, unknown>;\n },\n ): Promise<Record<string, unknown> | null> => {\n if (store === db && db.transact) {\n return await db.transact(async (tx) => await opIncrementOne(tx, schema, input));\n }\n return await opIncrementOne(store, schema, input);\n },\n };\n}\n\n/** Build the Better Auth database adapter over a shelf database. */\nexport function createShelfDbBridge(\n db: DatabaseAdapter,\n schema: AuthBridgeSchema,\n): DBAdapterInstance {\n return (_options: unknown) => {\n const ops: BoundOps = { ...bindQueryOps(db, schema), ...bindMutationOps(db, schema, db) };\n return {\n ...ops,\n transaction: async <R>(callback: (trx: DBTransactionAdapter) => Promise<R>): Promise<R> => {\n if (db.transact) {\n return await db.transact(async (tx) => {\n const txOps: BoundOps = {\n ...bindQueryOps(tx, schema),\n ...bindMutationOps(tx, schema, db),\n };\n return await callback(txOps as never);\n });\n }\n return await callback(ops as never);\n },\n options: { adapterName: \"storyshelf\", supportsDates: false, supportsJSON: false },\n } as never;\n };\n}\n"],"mappings":";;;;;;;;AAkCA,SAAS,SAAS,QAA0B,OAAsB;CAChE,MAAM,QAAQ,OAAO,OAAO;CAC5B,IAAI,CAAC,OACH,MAAM,IAAI,MAAM,uBAAuB,MAAM,EAAE;CAEjD,OAAO;AACT;AAEA,SAAS,SAAS,QAA0B,OAAkC;CAC5E,OAAO,OAAO,WAAW,UAAU,CAAC;AACtC;AAEA,SAAS,oBAA4B;CACnC,OAAO,KAAK;AACd;AAEA,SAAS,QAAQ,OAAc,OAAsD;CACnF,IAAI,CAAC,SAAS,MAAM,WAAW,GAC7B;CAEF,OAAO,eAAe,OAAO,KAAK;AACpC;AAEA,eAAe,UACb,OACA,QACA,OACA,OACyC;CACzC,MAAM,QAAQ,SAAS,QAAQ,KAAK;CAKpC,MAAM,OAAM,MAJQ,MAAM,KAAK,OAAO;EAAE,OAAO,QAAQ,OAAO,KAAK;EAAG,OAAO;CAAE,CAAC,EAAA,CAI/D;CACjB,OAAO,MAAM,WAAW,KAAK,SAAS,QAAQ,KAAK,CAAC,IAAI;AAC1D;AAEA,eAAe,WACb,OACA,QACA,OACA,IACA,QACyC;CACzC,MAAM,QAAQ,SAAS,QAAQ,KAAK;CAEpC,IAAI,CAAC,MADkB,MAAM,IAAI,OAAO,EAAE,GAExC,OAAO;CAET,MAAM,UAAU,MAAM,MAAM,OAAO,OAAO,IAAI,YAAY,MAAM,CAAC;CACjE,OAAO,WAAW,SAAS,SAAS,QAAQ,KAAK,CAAC;AACpD;AAEA,eAAe,SACb,OACA,QACA,OACkC;CAClC,MAAM,QAAQ,SAAS,QAAQ,MAAM,KAAK;CAC1C,MAAM,WAAW,OAAO,cAAc;CACtC,MAAM,OACJ,MAAM,KAAK,UAAU,KAAA,KAAa,MAAM,KAAK,UAAU,OACnD;EAAE,GAAG,MAAM;EAAM,IAAI,SAAS,MAAM,KAAK;CAAE,IAC3C,MAAM;CACZ,MAAM,WAAW,MAAM,MAAM,OAAO,OAAO,YAAY,IAAI,CAAC;CAC5D,OAAO,WAAW,UAAU,SAAS,QAAQ,MAAM,KAAK,CAAC;AAC3D;AAEA,eAAe,SACb,OACA,QACA,OACyC;CACzC,MAAM,MAAM,MAAM,UAAU,OAAO,QAAQ,MAAM,OAAO,MAAM,KAAK;CACnE,IAAI,CAAC,OAAO,OAAO,IAAI,UAAU,UAC/B,OAAO;CAET,OAAO,MAAM,WAAW,OAAO,QAAQ,MAAM,OAAO,IAAI,OAAO,MAAM,MAAM;AAC7E;AAEA,eAAe,aACb,OACA,QACA,OACiB;CACjB,MAAM,QAAQ,SAAS,QAAQ,MAAM,KAAK;CAI1C,MAAM,OAAM,MAHQ,MAAM,KAAK,OAAO,EACpC,OAAO,QAAQ,OAAO,MAAM,KAAK,EACnC,CAAC,EAAA,CACgB,KAAK,QAAQ,IAAI,KAAK,CAAC,CAAC,QAAQ,OAAqB,OAAO,OAAO,QAAQ;CAC5F,MAAM,QAAQ,IACZ,IAAI,IAAI,OAAO,OAAO,MAAM,WAAW,OAAO,QAAQ,MAAM,OAAO,IAAI,MAAM,MAAM,CAAC,CACtF;CACA,OAAO,IAAI;AACb;AAEA,eAAe,aACb,OACA,QACA,OACiB;CACjB,MAAM,QAAQ,SAAS,QAAQ,MAAM,KAAK;CAI1C,MAAM,OAAM,MAHQ,MAAM,KAAK,OAAO,EACpC,OAAO,QAAQ,OAAO,MAAM,KAAK,EACnC,CAAC,EAAA,CACgB,KAAK,QAAQ,IAAI,KAAK,CAAC,CAAC,QAAQ,OAAqB,OAAO,OAAO,QAAQ;CAC5F,MAAM,QAAQ,IACZ,IAAI,IAAI,OAAO,OAAO;EACpB,MAAM,MAAM,OAAO,OAAO,EAAE;CAC9B,CAAC,CACH;CACA,OAAO,IAAI;AACb;AAEA,eAAe,aACb,OACA,QACA,OACyC;CAKzC,MAAM,MAAM,MAAM,UAAU,OAAO,QAAQ,MAAM,OAAO,MAAM,KAAK;CACnE,IAAI,CAAC,OAAO,OAAO,IAAI,UAAU,UAC/B,OAAO;CAET,MAAM,MAAM,OAAO,SAAS,QAAQ,MAAM,KAAK,GAAG,IAAI,KAAK;CAC3D,OAAO;AACT;AAEA,eAAe,eACb,OACA,QACA,OAKyC;CACzC,MAAM,MAAM,MAAM,UAAU,OAAO,QAAQ,MAAM,OAAO,MAAM,KAAK;CACnE,IAAI,CAAC,OAAO,OAAO,IAAI,UAAU,UAC/B,OAAO;CAET,MAAM,SAAS,OAAO,YACpB,OAAO,QAAQ,MAAM,SAAS,CAAC,CAAC,KAAK,CAAC,OAAO,WAAW;EACtD,MAAM,UAAU,IAAI;EACpB,OAAO,CAAC,QAAQ,OAAO,YAAY,WAAW,UAAU,KAAK,KAAK;CACpE,CAAC,CACH;CACA,OAAO,MAAM,WAAW,OAAO,QAAQ,MAAM,OAAO,IAAI,OAAO;EAC7D,GAAG;EACH,GAAG,MAAM;CACX,CAAC;AACH;AAEA,SAAS,UAAU,OAAc,OAAwB;CACvD,OAAO,gBAAgB,KAAK,CAAC,CAAC,WAAW,KAAA;AAC3C;AAEA,eAAe,SACb,OACA,QACA,aACA,UACA,aACA,OACoC;CACpC,MAAM,QAAQ,SAAS,QAAQ,WAAW;CAC1C,MAAM,OAAoB,EAAE,MAAM;CAClC,MAAM,QAAQ,QAAQ,OAAO,CAAC;EAAE,OAAO,GAAG,YAAY;EAAK,OAAO;CAAkB,CAAC,CAAC;CACtF,IAAI,OACF,KAAK,QAAQ;CAGf,QAAO,MADa,MAAM,KAAK,OAAO,IAAI,EAAA,CAC9B,KAAK,QAAQ,WAAW,KAAK,SAAS,QAAQ,WAAW,CAAC,CAAC;AACzE;AAEA,eAAe,aACb,OACA,QACA,aACA,QACA,aACA,OACqE;CAErE,IAAI,UADU,SAAS,QAAQ,WACb,GAAG,GAAG,YAAY,GAAG,GACrC,OAAO,MAAM,SAAS,OAAO,QAAQ,aAAa,OAAO,OAAO,aAAa,KAAK;CAEpF,IAAI,OAAO,OAAO,GAAG,YAAY,SAAS,UACxC,OAAO,MAAM,UAAU,OAAO,QAAQ,aAAa,CACjD;EAAE,OAAO;EAAM,OAAO,OAAO,GAAG,YAAY;CAAc,CAC5D,CAAC;CAEH,MAAM,IAAI,MAAM,0BAA0B,YAAY,GAAG,YAAY,EAAE;AACzE;AAEA,eAAe,YACb,OACA,QACA,aACA,QACA,MACkC;CAClC,IAAI,CAAC,MACH,OAAO,CAAC;CAEV,MAAM,WAAoC,CAAC;CAC3C,MAAM,UAAU,OAAO,QAAQ,IAAI;CACnC,MAAM,QAAQ,IACZ,QAAQ,IAAI,OAAO,CAAC,aAAa,cAAc;EAC7C,MAAM,QAAQ,OAAO,aAAa,WAAW,SAAS,QAAQ,KAAA;EAC9D,SAAS,eAAe,MAAM,aAC5B,OACA,QACA,aACA,QACA,aACA,KACF;CACF,CAAC,CACH;CACA,OAAO;AACT;AAWA,eAAe,SACb,OACA,QACA,OACoC;CACpC,MAAM,QAAQ,SAAS,QAAQ,MAAM,KAAK;CAC1C,MAAM,OAAoB;EAAE,OAAO,MAAM;EAAO,QAAQ,MAAM;CAAO;CACrE,MAAM,QAAQ,QAAQ,OAAO,MAAM,KAAK;CACxC,IAAI,OACF,KAAK,QAAQ;CAEf,MAAM,UAAU,aAAa,OAAO,MAAM,MAAM;CAChD,IAAI,SACF,KAAK,UAAU;CAGjB,OAAO,MADyC,MAAM,KAAK,OAAO,IAAI;AAExE;AAEA,eAAe,WACb,OACA,QACA,OACoC;CACpC,MAAM,OAAO,MAAM,SAAS,OAAO,QAAQ,KAAK;CAChD,OAAO,MAAM,QAAQ,IACnB,KAAK,IAAI,OAAO,QAAQ;EACtB,MAAM,YAAY,MAAM,OACpB;GAAE,GAAG;GAAK,GAAI,MAAM,YAAY,OAAO,QAAQ,MAAM,OAAO,KAAK,MAAM,IAAI;EAAG,IAC9E;EACJ,OAAO,YAAY,WAAW,WAAW,SAAS,QAAQ,MAAM,KAAK,CAAC,GAAG,MAAM,MAAM;CACvF,CAAC,CACH;AACF;AAsCA,eAAe,UACb,OACA,QACA,OACyC;CACzC,MAAM,MAAM,MAAM,UAAU,OAAO,QAAQ,MAAM,OAAO,MAAM,KAAK;CACnE,IAAI,CAAC,KACH,OAAO;CAET,MAAM,YAAY,MAAM,OACpB;EAAE,GAAG;EAAK,GAAI,MAAM,YAAY,OAAO,QAAQ,MAAM,OAAO,KAAK,MAAM,IAAI;CAAG,IAC9E;CACJ,OAAO,YAAY,WAAW,MAAM,MAAM;AAC5C;;AAGA,SAAS,aACP,OACA,QACkD;CAClD,OAAO;EACL,SAAS,OACP,UAC4C,MAAM,UAAU,OAAO,QAAQ,KAAK;EAClF,UAAU,OACR,UAOuC,MAAM,WAAW,OAAO,QAAQ,KAAK;EAC9E,OAAO,OAAO,UAA0D;GACtE,MAAM,QAAQ,SAAS,QAAQ,MAAM,KAAK;GAC1C,OAAO,MAAM,MAAM,MAAM,OAAO,QAAQ,OAAO,MAAM,KAAK,CAAC;EAC7D;CACF;AACF;;AAGA,SAAS,gBACP,OACA,QACA,IACkD;CAClD,OAAO;EACL,QAAQ,OACN,UACqC,MAAM,SAAS,OAAO,QAAQ,KAAK;EAC1E,QAAQ,OACN,UAC4C,MAAM,SAAS,OAAO,QAAQ,KAAK;EACjF,YAAY,OACV,UACoB,MAAM,aAAa,OAAO,QAAQ,KAAK;EAC7D,QAAQ,OAAO,UAAuD;GACpE,MAAM,MAAM,MAAM,UAAU,OAAO,QAAQ,MAAM,OAAO,MAAM,KAAK;GACnE,IAAI,OAAO,OAAO,IAAI,UAAU,UAC9B,MAAM,MAAM,OAAO,SAAS,QAAQ,MAAM,KAAK,GAAG,IAAI,KAAK;EAE/D;EACA,YAAY,OAAO,UACjB,MAAM,aAAa,OAAO,QAAQ,KAAK;EACzC,YAAY,OACV,UAC4C;GAI5C,IAAI,UAAU,MAAM,GAAG,UACrB,OAAO,MAAM,GAAG,SAAS,OAAO,OAAO,MAAM,aAAa,IAAI,QAAQ,KAAK,CAAC;GAE9E,OAAO,MAAM,aAAa,OAAO,QAAQ,KAAK;EAChD;EACA,cAAc,OACZ,UAK4C;GAC5C,IAAI,UAAU,MAAM,GAAG,UACrB,OAAO,MAAM,GAAG,SAAS,OAAO,OAAO,MAAM,eAAe,IAAI,QAAQ,KAAK,CAAC;GAEhF,OAAO,MAAM,eAAe,OAAO,QAAQ,KAAK;EAClD;CACF;AACF;;AAGA,SAAgB,oBACd,IACA,QACmB;CACnB,QAAQ,aAAsB;EAC5B,MAAM,MAAgB;GAAE,GAAG,aAAa,IAAI,MAAM;GAAG,GAAG,gBAAgB,IAAI,QAAQ,EAAE;EAAE;EACxF,OAAO;GACL,GAAG;GACH,aAAa,OAAU,aAAoE;IACzF,IAAI,GAAG,UACL,OAAO,MAAM,GAAG,SAAS,OAAO,OAAO;KAKrC,OAAO,MAAM,SAAS;MAHpB,GAAG,aAAa,IAAI,MAAM;MAC1B,GAAG,gBAAgB,IAAI,QAAQ,EAAE;KAET,CAAU;IACtC,CAAC;IAEH,OAAO,MAAM,SAAS,GAAY;GACpC;GACA,SAAS;IAAE,aAAa;IAAc,eAAe;IAAO,cAAc;GAAM;EAClF;CACF;AACF"}
package/dist/db-where.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  import "node:path";
2
2
  import "node:url";
3
3
  import.meta.url;
4
- import { and, asc, desc, eq, getTableColumns, gt, gte, inArray, lt, lte, ne, not, or, sql } from "drizzle-orm";
4
+ import { and, asc, desc, eq, getTableColumns, gt, gte, inArray, lt, lte, ne, not, or, sql } from "@storyshelf/core/orm";
5
5
  //#region src/db-where.ts
6
6
  function columnOf(table, field) {
7
7
  const column = getTableColumns(table)[field];
@@ -1 +1 @@
1
- {"version":3,"file":"db-where.mjs","names":[],"sources":["../src/db-where.ts"],"sourcesContent":["/**\n * Better Auth Where[] to drizzle conditions.\n *\n * Operators covered: eq, ne, lt, lte, gt, gte, in, not_in, contains,\n * starts_with, ends_with. Connectors AND/OR (default AND). Case-insensitive\n * mode lowers both sides (portable across sqlite and Postgres). Unknown\n * fields fail fast — silent ignores would corrupt auth lookups.\n */\nimport type { Where } from \"better-auth\";\nimport type { SQL, Table } from \"drizzle-orm\";\nimport {\n and,\n asc,\n desc,\n eq,\n getTableColumns,\n gt,\n gte,\n inArray,\n lt,\n lte,\n ne,\n not,\n or,\n sql,\n} from \"drizzle-orm\";\n\nfunction columnOf(table: Table, field: string): SQL {\n const column = getTableColumns(table)[field] as SQL | undefined;\n if (!column) {\n throw new Error(`Unknown auth field \"${field}\"`);\n }\n return column;\n}\n\nfunction lowered(column: SQL): SQL {\n return sql`lower(${column})`;\n}\n\n/** Escape LIKE metacharacters so user input matches literally. */\nfunction escapeLike(value: string): string {\n return value.replaceAll(\"\\\\\", \"\\\\\\\\\").replaceAll(\"%\", \"\\\\%\").replaceAll(\"_\", \"\\\\_\");\n}\n\nfunction patternValue(operator: string, value: string): string {\n const literal = escapeLike(value);\n if (operator === \"contains\") {\n return `%${literal}%`;\n }\n if (operator === \"starts_with\") {\n return `${literal}%`;\n }\n return `%${literal}`;\n}\n\n/** LIKE with an explicit escape (sqlite has no default escape character). */\nfunction likeEscaped(target: SQL, pattern: string): SQL {\n return sql`${target} LIKE ${pattern} ESCAPE '\\\\'`;\n}\n\nfunction singleCondition(table: Table, item: Where): SQL {\n const column = columnOf(table, item.field);\n const operator = item.operator ?? \"eq\";\n const raw =\n typeof item.value === \"string\" && item.mode === \"insensitive\"\n ? item.value.toLowerCase()\n : item.value;\n const target =\n typeof item.value === \"string\" && item.mode === \"insensitive\" ? lowered(column) : column;\n const value = toDriverScalar(raw);\n switch (operator) {\n case \"eq\":\n return eq(target, value);\n case \"ne\":\n return ne(target, value);\n case \"lt\":\n return lt(target, value as string | number);\n case \"lte\":\n return lte(target, value as string | number);\n case \"gt\":\n return gt(target, value as string | number);\n case \"gte\":\n return gte(target, value as string | number);\n case \"in\":\n return inArray(target, toArray(value));\n case \"not_in\":\n return not(inArray(target, toArray(value)));\n case \"contains\":\n case \"starts_with\":\n case \"ends_with\":\n return likeEscaped(target, patternValue(operator, String(raw ?? \"\")));\n default:\n throw new Error(`Unsupported auth where operator \"${String(operator)}\"`);\n }\n}\n\nfunction toArray(value: unknown): (string | number)[] {\n if (!Array.isArray(value)) {\n throw new TypeError(\"Auth where `in` operator needs an array value\");\n }\n return value as (string | number)[];\n}\n\nfunction toDriverScalar(value: Where[\"value\"]): unknown {\n if (value instanceof Date) {\n return value.toISOString();\n }\n return value;\n}\n\n/** Split items into AND-groups at OR connectors (SQL precedence). */\nfunction splitOrGroups(table: Table, where: readonly Where[]): SQL[][] {\n const groups: SQL[][] = [];\n let current: SQL[] = [];\n for (const [index, item] of where.entries()) {\n if (index > 0 && item.connector === \"OR\") {\n groups.push(current);\n current = [];\n }\n current.push(singleCondition(table, item));\n }\n groups.push(current);\n return groups;\n}\n\n/** Combine Where[] items with their connectors into one condition. */\nexport function buildCondition(table: Table, where: readonly Where[]): SQL | undefined {\n if (where.length === 0) {\n return undefined;\n }\n return or(...splitOrGroups(table, where).map((group) => and(...group)));\n}\n\n/** Build an order-by clause from a sort descriptor. */\nexport function buildOrderBy(\n table: Table,\n sortBy: { field: string; direction: \"asc\" | \"desc\" } | undefined,\n): SQL | undefined {\n if (!sortBy) {\n return undefined;\n }\n const column = columnOf(table, sortBy.field);\n return sortBy.direction === \"desc\" ? desc(column) : asc(column);\n}\n"],"mappings":";;;;;AA2BA,SAAS,SAAS,OAAc,OAAoB;CAClD,MAAM,SAAS,gBAAgB,KAAK,CAAC,CAAC;CACtC,IAAI,CAAC,QACH,MAAM,IAAI,MAAM,uBAAuB,MAAM,EAAE;CAEjD,OAAO;AACT;AAEA,SAAS,QAAQ,QAAkB;CACjC,OAAO,GAAG,SAAS,OAAO;AAC5B;;AAGA,SAAS,WAAW,OAAuB;CACzC,OAAO,MAAM,WAAW,MAAM,MAAM,CAAC,CAAC,WAAW,KAAK,KAAK,CAAC,CAAC,WAAW,KAAK,KAAK;AACpF;AAEA,SAAS,aAAa,UAAkB,OAAuB;CAC7D,MAAM,UAAU,WAAW,KAAK;CAChC,IAAI,aAAa,YACf,OAAO,IAAI,QAAQ;CAErB,IAAI,aAAa,eACf,OAAO,GAAG,QAAQ;CAEpB,OAAO,IAAI;AACb;;AAGA,SAAS,YAAY,QAAa,SAAsB;CACtD,OAAO,GAAG,GAAG,OAAO,QAAQ,QAAQ;AACtC;AAEA,SAAS,gBAAgB,OAAc,MAAkB;CACvD,MAAM,SAAS,SAAS,OAAO,KAAK,KAAK;CACzC,MAAM,WAAW,KAAK,YAAY;CAClC,MAAM,MACJ,OAAO,KAAK,UAAU,YAAY,KAAK,SAAS,gBAC5C,KAAK,MAAM,YAAY,IACvB,KAAK;CACX,MAAM,SACJ,OAAO,KAAK,UAAU,YAAY,KAAK,SAAS,gBAAgB,QAAQ,MAAM,IAAI;CACpF,MAAM,QAAQ,eAAe,GAAG;CAChC,QAAQ,UAAR;EACE,KAAK,MACH,OAAO,GAAG,QAAQ,KAAK;EACzB,KAAK,MACH,OAAO,GAAG,QAAQ,KAAK;EACzB,KAAK,MACH,OAAO,GAAG,QAAQ,KAAwB;EAC5C,KAAK,OACH,OAAO,IAAI,QAAQ,KAAwB;EAC7C,KAAK,MACH,OAAO,GAAG,QAAQ,KAAwB;EAC5C,KAAK,OACH,OAAO,IAAI,QAAQ,KAAwB;EAC7C,KAAK,MACH,OAAO,QAAQ,QAAQ,QAAQ,KAAK,CAAC;EACvC,KAAK,UACH,OAAO,IAAI,QAAQ,QAAQ,QAAQ,KAAK,CAAC,CAAC;EAC5C,KAAK;EACL,KAAK;EACL,KAAK,aACH,OAAO,YAAY,QAAQ,aAAa,UAAU,OAAO,OAAO,EAAE,CAAC,CAAC;EACtE,SACE,MAAM,IAAI,MAAM,oCAAoC,OAAO,QAAQ,EAAE,EAAE;CAC3E;AACF;AAEA,SAAS,QAAQ,OAAqC;CACpD,IAAI,CAAC,MAAM,QAAQ,KAAK,GACtB,MAAM,IAAI,UAAU,+CAA+C;CAErE,OAAO;AACT;AAEA,SAAS,eAAe,OAAgC;CACtD,IAAI,iBAAiB,MACnB,OAAO,MAAM,YAAY;CAE3B,OAAO;AACT;;AAGA,SAAS,cAAc,OAAc,OAAkC;CACrE,MAAM,SAAkB,CAAC;CACzB,IAAI,UAAiB,CAAC;CACtB,KAAK,MAAM,CAAC,OAAO,SAAS,MAAM,QAAQ,GAAG;EAC3C,IAAI,QAAQ,KAAK,KAAK,cAAc,MAAM;GACxC,OAAO,KAAK,OAAO;GACnB,UAAU,CAAC;EACb;EACA,QAAQ,KAAK,gBAAgB,OAAO,IAAI,CAAC;CAC3C;CACA,OAAO,KAAK,OAAO;CACnB,OAAO;AACT;;AAGA,SAAgB,eAAe,OAAc,OAA0C;CACrF,IAAI,MAAM,WAAW,GACnB;CAEF,OAAO,GAAG,GAAG,cAAc,OAAO,KAAK,CAAC,CAAC,KAAK,UAAU,IAAI,GAAG,KAAK,CAAC,CAAC;AACxE;;AAGA,SAAgB,aACd,OACA,QACiB;CACjB,IAAI,CAAC,QACH;CAEF,MAAM,SAAS,SAAS,OAAO,OAAO,KAAK;CAC3C,OAAO,OAAO,cAAc,SAAS,KAAK,MAAM,IAAI,IAAI,MAAM;AAChE"}
1
+ {"version":3,"file":"db-where.mjs","names":[],"sources":["../src/db-where.ts"],"sourcesContent":["import type { SQL, Table } from \"@storyshelf/core/orm\";\nimport {\n and,\n asc,\n desc,\n eq,\n getTableColumns,\n gt,\n gte,\n inArray,\n lt,\n lte,\n ne,\n not,\n or,\n sql,\n} from \"@storyshelf/core/orm\";\n/**\n * Better Auth Where[] to drizzle conditions.\n *\n * Operators covered: eq, ne, lt, lte, gt, gte, in, not_in, contains,\n * starts_with, ends_with. Connectors AND/OR (default AND). Case-insensitive\n * mode lowers both sides (portable across sqlite and Postgres). Unknown\n * fields fail fast — silent ignores would corrupt auth lookups.\n */\nimport type { Where } from \"better-auth\";\n\nfunction columnOf(table: Table, field: string): SQL {\n const column = getTableColumns(table)[field] as SQL | undefined;\n if (!column) {\n throw new Error(`Unknown auth field \"${field}\"`);\n }\n return column;\n}\n\nfunction lowered(column: SQL): SQL {\n return sql`lower(${column})`;\n}\n\n/** Escape LIKE metacharacters so user input matches literally. */\nfunction escapeLike(value: string): string {\n return value.replaceAll(\"\\\\\", \"\\\\\\\\\").replaceAll(\"%\", \"\\\\%\").replaceAll(\"_\", \"\\\\_\");\n}\n\nfunction patternValue(operator: string, value: string): string {\n const literal = escapeLike(value);\n if (operator === \"contains\") {\n return `%${literal}%`;\n }\n if (operator === \"starts_with\") {\n return `${literal}%`;\n }\n return `%${literal}`;\n}\n\n/** LIKE with an explicit escape (sqlite has no default escape character). */\nfunction likeEscaped(target: SQL, pattern: string): SQL {\n return sql`${target} LIKE ${pattern} ESCAPE '\\\\'`;\n}\n\nfunction singleCondition(table: Table, item: Where): SQL {\n const column = columnOf(table, item.field);\n const operator = item.operator ?? \"eq\";\n const raw =\n typeof item.value === \"string\" && item.mode === \"insensitive\"\n ? item.value.toLowerCase()\n : item.value;\n const target =\n typeof item.value === \"string\" && item.mode === \"insensitive\" ? lowered(column) : column;\n const value = toDriverScalar(raw);\n switch (operator) {\n case \"eq\":\n return eq(target, value);\n case \"ne\":\n return ne(target, value);\n case \"lt\":\n return lt(target, value as string | number);\n case \"lte\":\n return lte(target, value as string | number);\n case \"gt\":\n return gt(target, value as string | number);\n case \"gte\":\n return gte(target, value as string | number);\n case \"in\":\n return inArray(target, toArray(value));\n case \"not_in\":\n return not(inArray(target, toArray(value)));\n case \"contains\":\n case \"starts_with\":\n case \"ends_with\":\n return likeEscaped(target, patternValue(operator, String(raw ?? \"\")));\n default:\n throw new Error(`Unsupported auth where operator \"${String(operator)}\"`);\n }\n}\n\nfunction toArray(value: unknown): (string | number)[] {\n if (!Array.isArray(value)) {\n throw new TypeError(\"Auth where `in` operator needs an array value\");\n }\n return value as (string | number)[];\n}\n\nfunction toDriverScalar(value: Where[\"value\"]): unknown {\n if (value instanceof Date) {\n return value.toISOString();\n }\n return value;\n}\n\n/** Split items into AND-groups at OR connectors (SQL precedence). */\nfunction splitOrGroups(table: Table, where: readonly Where[]): SQL[][] {\n const groups: SQL[][] = [];\n let current: SQL[] = [];\n for (const [index, item] of where.entries()) {\n if (index > 0 && item.connector === \"OR\") {\n groups.push(current);\n current = [];\n }\n current.push(singleCondition(table, item));\n }\n groups.push(current);\n return groups;\n}\n\n/** Combine Where[] items with their connectors into one condition. */\nexport function buildCondition(table: Table, where: readonly Where[]): SQL | undefined {\n if (where.length === 0) {\n return undefined;\n }\n return or(...splitOrGroups(table, where).map((group) => and(...group)));\n}\n\n/** Build an order-by clause from a sort descriptor. */\nexport function buildOrderBy(\n table: Table,\n sortBy: { field: string; direction: \"asc\" | \"desc\" } | undefined,\n): SQL | undefined {\n if (!sortBy) {\n return undefined;\n }\n const column = columnOf(table, sortBy.field);\n return sortBy.direction === \"desc\" ? desc(column) : asc(column);\n}\n"],"mappings":";;;;;AA2BA,SAAS,SAAS,OAAc,OAAoB;CAClD,MAAM,SAAS,gBAAgB,KAAK,CAAC,CAAC;CACtC,IAAI,CAAC,QACH,MAAM,IAAI,MAAM,uBAAuB,MAAM,EAAE;CAEjD,OAAO;AACT;AAEA,SAAS,QAAQ,QAAkB;CACjC,OAAO,GAAG,SAAS,OAAO;AAC5B;;AAGA,SAAS,WAAW,OAAuB;CACzC,OAAO,MAAM,WAAW,MAAM,MAAM,CAAC,CAAC,WAAW,KAAK,KAAK,CAAC,CAAC,WAAW,KAAK,KAAK;AACpF;AAEA,SAAS,aAAa,UAAkB,OAAuB;CAC7D,MAAM,UAAU,WAAW,KAAK;CAChC,IAAI,aAAa,YACf,OAAO,IAAI,QAAQ;CAErB,IAAI,aAAa,eACf,OAAO,GAAG,QAAQ;CAEpB,OAAO,IAAI;AACb;;AAGA,SAAS,YAAY,QAAa,SAAsB;CACtD,OAAO,GAAG,GAAG,OAAO,QAAQ,QAAQ;AACtC;AAEA,SAAS,gBAAgB,OAAc,MAAkB;CACvD,MAAM,SAAS,SAAS,OAAO,KAAK,KAAK;CACzC,MAAM,WAAW,KAAK,YAAY;CAClC,MAAM,MACJ,OAAO,KAAK,UAAU,YAAY,KAAK,SAAS,gBAC5C,KAAK,MAAM,YAAY,IACvB,KAAK;CACX,MAAM,SACJ,OAAO,KAAK,UAAU,YAAY,KAAK,SAAS,gBAAgB,QAAQ,MAAM,IAAI;CACpF,MAAM,QAAQ,eAAe,GAAG;CAChC,QAAQ,UAAR;EACE,KAAK,MACH,OAAO,GAAG,QAAQ,KAAK;EACzB,KAAK,MACH,OAAO,GAAG,QAAQ,KAAK;EACzB,KAAK,MACH,OAAO,GAAG,QAAQ,KAAwB;EAC5C,KAAK,OACH,OAAO,IAAI,QAAQ,KAAwB;EAC7C,KAAK,MACH,OAAO,GAAG,QAAQ,KAAwB;EAC5C,KAAK,OACH,OAAO,IAAI,QAAQ,KAAwB;EAC7C,KAAK,MACH,OAAO,QAAQ,QAAQ,QAAQ,KAAK,CAAC;EACvC,KAAK,UACH,OAAO,IAAI,QAAQ,QAAQ,QAAQ,KAAK,CAAC,CAAC;EAC5C,KAAK;EACL,KAAK;EACL,KAAK,aACH,OAAO,YAAY,QAAQ,aAAa,UAAU,OAAO,OAAO,EAAE,CAAC,CAAC;EACtE,SACE,MAAM,IAAI,MAAM,oCAAoC,OAAO,QAAQ,EAAE,EAAE;CAC3E;AACF;AAEA,SAAS,QAAQ,OAAqC;CACpD,IAAI,CAAC,MAAM,QAAQ,KAAK,GACtB,MAAM,IAAI,UAAU,+CAA+C;CAErE,OAAO;AACT;AAEA,SAAS,eAAe,OAAgC;CACtD,IAAI,iBAAiB,MACnB,OAAO,MAAM,YAAY;CAE3B,OAAO;AACT;;AAGA,SAAS,cAAc,OAAc,OAAkC;CACrE,MAAM,SAAkB,CAAC;CACzB,IAAI,UAAiB,CAAC;CACtB,KAAK,MAAM,CAAC,OAAO,SAAS,MAAM,QAAQ,GAAG;EAC3C,IAAI,QAAQ,KAAK,KAAK,cAAc,MAAM;GACxC,OAAO,KAAK,OAAO;GACnB,UAAU,CAAC;EACb;EACA,QAAQ,KAAK,gBAAgB,OAAO,IAAI,CAAC;CAC3C;CACA,OAAO,KAAK,OAAO;CACnB,OAAO;AACT;;AAGA,SAAgB,eAAe,OAAc,OAA0C;CACrF,IAAI,MAAM,WAAW,GACnB;CAEF,OAAO,GAAG,GAAG,cAAc,OAAO,KAAK,CAAC,CAAC,KAAK,UAAU,IAAI,GAAG,KAAK,CAAC,CAAC;AACxE;;AAGA,SAAgB,aACd,OACA,QACiB;CACjB,IAAI,CAAC,QACH;CAEF,MAAM,SAAS,SAAS,OAAO,OAAO,KAAK;CAC3C,OAAO,OAAO,cAAc,SAAS,KAAK,MAAM,IAAI,IAAI,MAAM;AAChE"}
@@ -1,6 +1,6 @@
1
1
  import __tsdown_shims_path from 'node:path';
2
2
  import __tsdown_shims_url from 'node:url';
3
- import { Table } from "drizzle-orm";
3
+ import { Table } from "@storyshelf/core/orm";
4
4
  import { AuthUser } from "@storyshelf/core/types";
5
5
  import { DatabaseAdapter } from "@storyshelf/core/adapter/database";
6
6
  //#region src/invites.d.ts
package/dist/invites.mjs CHANGED
@@ -2,8 +2,8 @@ import "node:path";
2
2
  import "node:url";
3
3
  import.meta.url;
4
4
  import { baseAuthTables } from "./auth-tables.mjs";
5
+ import { eq, getTableColumns, sql } from "@storyshelf/core/orm";
5
6
  import { randomToken, sha256, timingSafeEqualString, ulid } from "@storyshelf/core/utils";
6
- import { eq, getTableColumns, sql } from "drizzle-orm";
7
7
  import { hashPassword } from "better-auth/crypto";
8
8
  //#region src/invites.ts
9
9
  const DEFAULT_INVITE_MS = 6048e5;
@@ -1 +1 @@
1
- {"version":3,"file":"invites.mjs","names":[],"sources":["../src/invites.ts"],"sourcesContent":["import type { DatabaseAdapter, TxStore } from \"@storyshelf/core/adapter/database\";\n/**\n * Invite-only local accounts on the engine: our tables own the invite,\n * Better Auth owns the credential.\n *\n * `issueInvite` stages a shelf `users` row plus a single-use token;\n * `acceptInvite` verifies the token, then writes the engine `user` row and\n * `credential` account row directly through the DatabaseAdapter. Direct\n * writes (instead of the admin `createUser` endpoint) because invite accept\n * is a pre-authentication flow and admin endpoints require a session by\n * design. Passwords use Better Auth's own hasher so engine sign-in verifies\n * them without format translation.\n */\nimport type { AuthUser } from \"@storyshelf/core/types\";\nimport { randomToken, sha256, timingSafeEqualString, ulid } from \"@storyshelf/core/utils\";\nimport { hashPassword } from \"better-auth/crypto\";\nimport { eq, getTableColumns, sql } from \"drizzle-orm\";\nimport type { SQL, Table } from \"drizzle-orm\";\nimport { baseAuthTables } from \"./auth-tables.ts\";\n\nconst DEFAULT_INVITE_MS = 7 * 24 * 60 * 60 * 1000;\nconst MAX_INVITE_MS = 365 * 24 * 60 * 60 * 1000;\n\n/** Minimum password length for local accounts (matches legacy policy). */\nexport const MIN_PASSWORD_LENGTH = 12;\n\n/** Input for {@link issueInvite}. */\nexport interface IssueInviteInput {\n email: string;\n name: string;\n role: AuthUser[\"role\"];\n /** Invite lifetime override. Defaults to 7 days. */\n inviteExpiryMs?: number;\n}\n\n/** Input for {@link acceptInvite} and {@link verifyInvite}. */\nexport interface InviteTokenInput {\n inviteId: string;\n token: string;\n}\n\n/** Input for {@link acceptInvite}. */\nexport interface AcceptInviteInput extends InviteTokenInput {\n password: string;\n}\n\ntype ShelfRow = Record<string, unknown>;\n\n/**\n * CRUD surface invites run on: the top-level adapter or a transaction store\n * (TxStore carries no table handles, so callers attach them). Both\n * DatabaseAdapter and TxStore satisfy this structurally.\n */\nexport type InviteStore = Pick<TxStore, \"insert\" | \"update\" | \"get\" | \"remove\" | \"list\"> & {\n readonly tables: DatabaseAdapter[\"tables\"];\n};\n\n/**\n * Run `fn` inside a transaction. Invites are consume-once state: without\n * driver transactions concurrent accepts race, so transact is required\n * (every production driver offers it) rather than silently sequential.\n */\nasync function withInviteTx<R>(\n db: DatabaseAdapter,\n fn: (store: InviteStore) => Promise<R>,\n): Promise<R> {\n if (!db.transact) {\n throw new Error(\"Shelf invites require a database with transaction support\");\n }\n return await db.transact(async (tx) => await fn({ ...tx, tables: db.tables }));\n}\n\nfunction normalizeEmail(email: string): string {\n return email.trim().toLowerCase();\n}\n\nfunction isEmail(value: string): boolean {\n return /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/u.test(value);\n}\n\nfunction inviteError(): Error {\n return new Error(\"Invalid or expired invite\");\n}\n\n/** Resolve a shelf/engine table column, failing fast on schema drift. */\nexport function tableColumn(table: Table, name: string): SQL {\n const column = getTableColumns(table)[name] as SQL | undefined;\n if (!column) {\n throw new Error(`Shelf table is missing column \"${name}\"`);\n }\n return column;\n}\n\nfunction assertPasswordPolicy(password: string): void {\n if (password.length < MIN_PASSWORD_LENGTH) {\n throw new Error(\"Password must be at least 12 characters\");\n }\n}\n\nasync function listShelfUsersByEmail(db: InviteStore, email: string): Promise<ShelfRow[]> {\n return await db.list(db.tables.users, {\n where: eq(tableColumn(db.tables.users, \"email\"), normalizeEmail(email)),\n limit: 1,\n });\n}\n\nasync function findEngineUserByEmail(db: InviteStore, email: string): Promise<ShelfRow | null> {\n // Case-insensitive: engine rows may carry provider-given casing while the\n // shelf normalizes (and UNIQUE(email) is case-sensitive, so an exact-only\n // lookup would 500 on a case-variant insert).\n const column = tableColumn(baseAuthTables.user, \"email\");\n const rows = (await db.list(baseAuthTables.user, {\n where: eq(sql`lower(${column})`, email.toLowerCase()),\n limit: 1,\n })) as ShelfRow[];\n return rows[0] ?? null;\n}\n\nasync function findCredentialAccount(db: InviteStore, userId: string): Promise<ShelfRow | null> {\n const rows = (await db.list(baseAuthTables.account, {\n where: eq(tableColumn(baseAuthTables.account, \"userId\"), userId),\n })) as ShelfRow[];\n return rows.find((row) => row[\"providerId\"] === \"credential\") ?? null;\n}\n\n/** Load the invite row, or throw the generic invite error. */\nasync function loadInvite(db: InviteStore, input: InviteTokenInput): Promise<ShelfRow> {\n const invite: ShelfRow | null = await db.get(db.tables.userInviteTokens, input.inviteId);\n if (!invite || invite[\"usedAt\"]) {\n throw inviteError();\n }\n // Fail closed: a corrupt expiresAt parses to NaN, and NaN comparisons\n // are false — without this check the invite would never expire.\n const expires = new Date(String(invite[\"expiresAt\"])).getTime();\n if (Number.isNaN(expires) || expires <= Date.now()) {\n throw inviteError();\n }\n if (!timingSafeEqualString(sha256(input.token), String(invite[\"tokenHash\"]))) {\n throw inviteError();\n }\n return invite;\n}\n\n/** Load the invite plus its shelf user, or throw the generic invite error. */\nasync function assertInviteValid(\n db: InviteStore,\n input: InviteTokenInput,\n): Promise<{ invite: ShelfRow; shelfUser: ShelfRow }> {\n const invite = await loadInvite(db, input);\n const shelfUser: ShelfRow | null = await db.get(db.tables.users, String(invite[\"userId\"]));\n if (!shelfUser || shelfUser[\"disabled\"]) {\n throw inviteError();\n }\n return { invite, shelfUser };\n}\n\n/** Create the credential account row, or refresh the existing password. */\nasync function upsertCredentialAccount(\n db: InviteStore,\n userId: string,\n password: string,\n now: string,\n): Promise<void> {\n const hash = await hashPassword(password);\n const account = await findCredentialAccount(db, userId);\n if (account) {\n await db.update(baseAuthTables.account, String(account[\"id\"]), {\n password: hash,\n updatedAt: now,\n });\n return;\n }\n await db.insert(baseAuthTables.account, {\n id: ulid(),\n userId,\n accountId: userId,\n providerId: \"credential\",\n password: hash,\n createdAt: now,\n updatedAt: now,\n });\n}\n\n/** Create the engine identity rows (shared id), or refresh the credential. */\nexport async function ensureEngineIdentity(\n db: InviteStore,\n shelfUser: ShelfRow,\n password: string,\n): Promise<void> {\n const userId = String(shelfUser[\"id\"]);\n const email = String(shelfUser[\"email\"]);\n const now = new Date().toISOString();\n // Verified-email linking only: an existing engine row takes the credential\n // when its email is verified (same owner). An unverified row (e.g. an OIDC\n // identity with an untrusted email claim) must stay separate — but engine\n // emails are UNIQUE, so no second row can mint: reject and point at SSO.\n const engineUser = await findEngineUserByEmail(db, email);\n if (engineUser && engineUser[\"emailVerified\"] !== true) {\n throw new Error(\n \"Email is registered with an unverified SSO identity; sign in with SSO instead\",\n );\n }\n if (!engineUser) {\n await db.insert(baseAuthTables.user, {\n id: userId,\n name: String(shelfUser[\"name\"]),\n email,\n emailVerified: true,\n image: null,\n createdAt: now,\n updatedAt: now,\n });\n }\n const targetId = engineUser ? String(engineUser[\"id\"]) : userId;\n await upsertCredentialAccount(db, targetId, password, now);\n}\n\nfunction toAuthUser(row: ShelfRow): AuthUser {\n return {\n id: String(row[\"id\"]),\n email: String(row[\"email\"]),\n name: String(row[\"name\"]),\n avatarUrl: (row[\"avatarUrl\"] as string | null) ?? undefined,\n role: row[\"role\"] as AuthUser[\"role\"],\n };\n}\n\nfunction inviteLifetimeMs(expiryMs: number | undefined): number {\n const lifetime = expiryMs ?? DEFAULT_INVITE_MS;\n if (!Number.isFinite(lifetime) || lifetime <= 0 || lifetime > MAX_INVITE_MS) {\n throw new Error(\"Invite lifetime must be between 1ms and 365 days\");\n }\n return lifetime;\n}\n\n/** Refresh name/role on the existing row (re-invite is admin intent). */\nasync function refreshShelfUser(\n db: InviteStore,\n id: string,\n input: IssueInviteInput,\n): Promise<ShelfRow> {\n const updated: ShelfRow = await db.update(db.tables.users, id, {\n name: input.name.trim(),\n role: input.role,\n });\n return updated;\n}\n\nasync function stageShelfUser(db: InviteStore, input: IssueInviteInput): Promise<ShelfRow> {\n const email = normalizeEmail(input.email);\n if (!isEmail(email)) {\n throw new Error(\"Invalid email\");\n }\n if (input.name.trim() === \"\") {\n throw new Error(\"Name is required\");\n }\n const existing = await listShelfUsersByEmail(db, email);\n // Re-invite refreshes name and role: issuing is operator-only, so the\n // latest invite is the current admin intent (and the password flow).\n if (existing[0]) {\n return await refreshShelfUser(db, String(existing[0][\"id\"]), input);\n }\n const now = new Date().toISOString();\n return await db.insert(db.tables.users, {\n id: ulid(),\n email,\n name: input.name.trim(),\n avatarUrl: null,\n role: input.role,\n lastLoginAt: null,\n createdAt: now,\n passwordHash: null,\n displayNameOverride: null,\n authProvider: \"local\",\n disabled: false,\n });\n}\n\nasync function removeUnusedInvite(db: InviteStore, row: ShelfRow): Promise<void> {\n await db.remove(db.tables.userInviteTokens, String(row[\"id\"]));\n}\n\nasync function supersedeUnusedInvites(db: InviteStore, userId: string): Promise<void> {\n const existing = (await db.list(db.tables.userInviteTokens, {\n where: eq(tableColumn(db.tables.userInviteTokens, \"userId\"), userId),\n })) as ShelfRow[];\n await Promise.all(\n existing\n .filter((row) => !row[\"usedAt\"])\n .map(async (row) => {\n await removeUnusedInvite(db, row);\n }),\n );\n}\n\n/** Stage a shelf user and mint a single-use invite token (returned once). */\nexport async function issueInvite(\n db: DatabaseAdapter,\n input: IssueInviteInput,\n): Promise<{ inviteId: string; token: string; expiresAt: string }> {\n return await withInviteTx(db, async (store) => {\n const shelfUser = await stageShelfUser(store, input);\n const userId = String(shelfUser[\"id\"]);\n await supersedeUnusedInvites(store, userId);\n const now = new Date().toISOString();\n const expiresAt = new Date(Date.now() + inviteLifetimeMs(input.inviteExpiryMs)).toISOString();\n const inviteId = ulid();\n const token = randomToken(\"inv_\").value;\n await store.insert(store.tables.userInviteTokens, {\n id: inviteId,\n userId,\n tokenHash: sha256(token),\n expiresAt,\n usedAt: null,\n createdAt: now,\n });\n return { inviteId, token, expiresAt };\n });\n}\n\n/** Check an invite token without consuming it (drives the accept page). */\nexport async function verifyInvite(\n db: DatabaseAdapter,\n input: InviteTokenInput,\n): Promise<{ email: string; name: string }> {\n const { shelfUser } = await assertInviteValid(db, input);\n return { email: String(shelfUser[\"email\"]), name: String(shelfUser[\"name\"]) };\n}\n\n/** Whether an invite row is already spent (busy-downgrade probe). */\nasync function inviteSpent(db: InviteStore, inviteId: string): Promise<boolean> {\n const invite: ShelfRow | null = await db.get(db.tables.userInviteTokens, inviteId);\n return !!invite?.[\"usedAt\"];\n}\n\n/** Consume an invite: set the engine credential and return the shelf user. */\nexport async function acceptInvite(\n db: DatabaseAdapter,\n input: AcceptInviteInput,\n): Promise<AuthUser> {\n assertPasswordPolicy(input.password);\n try {\n return await withInviteTx(db, async (store) => {\n const { invite, shelfUser } = await assertInviteValid(store, input);\n await ensureEngineIdentity(store, shelfUser, input.password);\n const now = new Date().toISOString();\n await store.update(store.tables.users, String(shelfUser[\"id\"]), { lastLoginAt: now });\n await store.update(store.tables.userInviteTokens, String(invite[\"id\"]), { usedAt: now });\n return toAuthUser(shelfUser);\n });\n } catch (error) {\n // A transaction loser (e.g. sqlite's busy connection) surfaces a driver\n // error instead of the invite error; re-probe so callers see the stable\n // \"Invalid or expired invite\" whenever the winner consumed the token.\n if (await inviteSpent(db, input.inviteId)) {\n throw inviteError();\n }\n throw error;\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"invites.mjs","names":[],"sources":["../src/invites.ts"],"sourcesContent":["import type { DatabaseAdapter, TxStore } from \"@storyshelf/core/adapter/database\";\nimport { eq, getTableColumns, sql } from \"@storyshelf/core/orm\";\nimport type { SQL, Table } from \"@storyshelf/core/orm\";\n/**\n * Invite-only local accounts on the engine: our tables own the invite,\n * Better Auth owns the credential.\n *\n * `issueInvite` stages a shelf `users` row plus a single-use token;\n * `acceptInvite` verifies the token, then writes the engine `user` row and\n * `credential` account row directly through the DatabaseAdapter. Direct\n * writes (instead of the admin `createUser` endpoint) because invite accept\n * is a pre-authentication flow and admin endpoints require a session by\n * design. Passwords use Better Auth's own hasher so engine sign-in verifies\n * them without format translation.\n */\nimport type { AuthUser } from \"@storyshelf/core/types\";\nimport { randomToken, sha256, timingSafeEqualString, ulid } from \"@storyshelf/core/utils\";\nimport { hashPassword } from \"better-auth/crypto\";\nimport { baseAuthTables } from \"./auth-tables.ts\";\n\nconst DEFAULT_INVITE_MS = 7 * 24 * 60 * 60 * 1000;\nconst MAX_INVITE_MS = 365 * 24 * 60 * 60 * 1000;\n\n/** Minimum password length for local accounts (matches legacy policy). */\nexport const MIN_PASSWORD_LENGTH = 12;\n\n/** Input for {@link issueInvite}. */\nexport interface IssueInviteInput {\n email: string;\n name: string;\n role: AuthUser[\"role\"];\n /** Invite lifetime override. Defaults to 7 days. */\n inviteExpiryMs?: number;\n}\n\n/** Input for {@link acceptInvite} and {@link verifyInvite}. */\nexport interface InviteTokenInput {\n inviteId: string;\n token: string;\n}\n\n/** Input for {@link acceptInvite}. */\nexport interface AcceptInviteInput extends InviteTokenInput {\n password: string;\n}\n\ntype ShelfRow = Record<string, unknown>;\n\n/**\n * CRUD surface invites run on: the top-level adapter or a transaction store\n * (TxStore carries no table handles, so callers attach them). Both\n * DatabaseAdapter and TxStore satisfy this structurally.\n */\nexport type InviteStore = Pick<TxStore, \"insert\" | \"update\" | \"get\" | \"remove\" | \"list\"> & {\n readonly tables: DatabaseAdapter[\"tables\"];\n};\n\n/**\n * Run `fn` inside a transaction. Invites are consume-once state: without\n * driver transactions concurrent accepts race, so transact is required\n * (every production driver offers it) rather than silently sequential.\n */\nasync function withInviteTx<R>(\n db: DatabaseAdapter,\n fn: (store: InviteStore) => Promise<R>,\n): Promise<R> {\n if (!db.transact) {\n throw new Error(\"Shelf invites require a database with transaction support\");\n }\n return await db.transact(async (tx) => await fn({ ...tx, tables: db.tables }));\n}\n\nfunction normalizeEmail(email: string): string {\n return email.trim().toLowerCase();\n}\n\nfunction isEmail(value: string): boolean {\n return /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/u.test(value);\n}\n\nfunction inviteError(): Error {\n return new Error(\"Invalid or expired invite\");\n}\n\n/** Resolve a shelf/engine table column, failing fast on schema drift. */\nexport function tableColumn(table: Table, name: string): SQL {\n const column = getTableColumns(table)[name] as SQL | undefined;\n if (!column) {\n throw new Error(`Shelf table is missing column \"${name}\"`);\n }\n return column;\n}\n\nfunction assertPasswordPolicy(password: string): void {\n if (password.length < MIN_PASSWORD_LENGTH) {\n throw new Error(\"Password must be at least 12 characters\");\n }\n}\n\nasync function listShelfUsersByEmail(db: InviteStore, email: string): Promise<ShelfRow[]> {\n return await db.list(db.tables.users, {\n where: eq(tableColumn(db.tables.users, \"email\"), normalizeEmail(email)),\n limit: 1,\n });\n}\n\nasync function findEngineUserByEmail(db: InviteStore, email: string): Promise<ShelfRow | null> {\n // Case-insensitive: engine rows may carry provider-given casing while the\n // shelf normalizes (and UNIQUE(email) is case-sensitive, so an exact-only\n // lookup would 500 on a case-variant insert).\n const column = tableColumn(baseAuthTables.user, \"email\");\n const rows = (await db.list(baseAuthTables.user, {\n where: eq(sql`lower(${column})`, email.toLowerCase()),\n limit: 1,\n })) as ShelfRow[];\n return rows[0] ?? null;\n}\n\nasync function findCredentialAccount(db: InviteStore, userId: string): Promise<ShelfRow | null> {\n const rows = (await db.list(baseAuthTables.account, {\n where: eq(tableColumn(baseAuthTables.account, \"userId\"), userId),\n })) as ShelfRow[];\n return rows.find((row) => row[\"providerId\"] === \"credential\") ?? null;\n}\n\n/** Load the invite row, or throw the generic invite error. */\nasync function loadInvite(db: InviteStore, input: InviteTokenInput): Promise<ShelfRow> {\n const invite: ShelfRow | null = await db.get(db.tables.userInviteTokens, input.inviteId);\n if (!invite || invite[\"usedAt\"]) {\n throw inviteError();\n }\n // Fail closed: a corrupt expiresAt parses to NaN, and NaN comparisons\n // are false — without this check the invite would never expire.\n const expires = new Date(String(invite[\"expiresAt\"])).getTime();\n if (Number.isNaN(expires) || expires <= Date.now()) {\n throw inviteError();\n }\n if (!timingSafeEqualString(sha256(input.token), String(invite[\"tokenHash\"]))) {\n throw inviteError();\n }\n return invite;\n}\n\n/** Load the invite plus its shelf user, or throw the generic invite error. */\nasync function assertInviteValid(\n db: InviteStore,\n input: InviteTokenInput,\n): Promise<{ invite: ShelfRow; shelfUser: ShelfRow }> {\n const invite = await loadInvite(db, input);\n const shelfUser: ShelfRow | null = await db.get(db.tables.users, String(invite[\"userId\"]));\n if (!shelfUser || shelfUser[\"disabled\"]) {\n throw inviteError();\n }\n return { invite, shelfUser };\n}\n\n/** Create the credential account row, or refresh the existing password. */\nasync function upsertCredentialAccount(\n db: InviteStore,\n userId: string,\n password: string,\n now: string,\n): Promise<void> {\n const hash = await hashPassword(password);\n const account = await findCredentialAccount(db, userId);\n if (account) {\n await db.update(baseAuthTables.account, String(account[\"id\"]), {\n password: hash,\n updatedAt: now,\n });\n return;\n }\n await db.insert(baseAuthTables.account, {\n id: ulid(),\n userId,\n accountId: userId,\n providerId: \"credential\",\n password: hash,\n createdAt: now,\n updatedAt: now,\n });\n}\n\n/** Create the engine identity rows (shared id), or refresh the credential. */\nexport async function ensureEngineIdentity(\n db: InviteStore,\n shelfUser: ShelfRow,\n password: string,\n): Promise<void> {\n const userId = String(shelfUser[\"id\"]);\n const email = String(shelfUser[\"email\"]);\n const now = new Date().toISOString();\n // Verified-email linking only: an existing engine row takes the credential\n // when its email is verified (same owner). An unverified row (e.g. an OIDC\n // identity with an untrusted email claim) must stay separate — but engine\n // emails are UNIQUE, so no second row can mint: reject and point at SSO.\n const engineUser = await findEngineUserByEmail(db, email);\n if (engineUser && engineUser[\"emailVerified\"] !== true) {\n throw new Error(\n \"Email is registered with an unverified SSO identity; sign in with SSO instead\",\n );\n }\n if (!engineUser) {\n await db.insert(baseAuthTables.user, {\n id: userId,\n name: String(shelfUser[\"name\"]),\n email,\n emailVerified: true,\n image: null,\n createdAt: now,\n updatedAt: now,\n });\n }\n const targetId = engineUser ? String(engineUser[\"id\"]) : userId;\n await upsertCredentialAccount(db, targetId, password, now);\n}\n\nfunction toAuthUser(row: ShelfRow): AuthUser {\n return {\n id: String(row[\"id\"]),\n email: String(row[\"email\"]),\n name: String(row[\"name\"]),\n avatarUrl: (row[\"avatarUrl\"] as string | null) ?? undefined,\n role: row[\"role\"] as AuthUser[\"role\"],\n };\n}\n\nfunction inviteLifetimeMs(expiryMs: number | undefined): number {\n const lifetime = expiryMs ?? DEFAULT_INVITE_MS;\n if (!Number.isFinite(lifetime) || lifetime <= 0 || lifetime > MAX_INVITE_MS) {\n throw new Error(\"Invite lifetime must be between 1ms and 365 days\");\n }\n return lifetime;\n}\n\n/** Refresh name/role on the existing row (re-invite is admin intent). */\nasync function refreshShelfUser(\n db: InviteStore,\n id: string,\n input: IssueInviteInput,\n): Promise<ShelfRow> {\n const updated: ShelfRow = await db.update(db.tables.users, id, {\n name: input.name.trim(),\n role: input.role,\n });\n return updated;\n}\n\nasync function stageShelfUser(db: InviteStore, input: IssueInviteInput): Promise<ShelfRow> {\n const email = normalizeEmail(input.email);\n if (!isEmail(email)) {\n throw new Error(\"Invalid email\");\n }\n if (input.name.trim() === \"\") {\n throw new Error(\"Name is required\");\n }\n const existing = await listShelfUsersByEmail(db, email);\n // Re-invite refreshes name and role: issuing is operator-only, so the\n // latest invite is the current admin intent (and the password flow).\n if (existing[0]) {\n return await refreshShelfUser(db, String(existing[0][\"id\"]), input);\n }\n const now = new Date().toISOString();\n return await db.insert(db.tables.users, {\n id: ulid(),\n email,\n name: input.name.trim(),\n avatarUrl: null,\n role: input.role,\n lastLoginAt: null,\n createdAt: now,\n passwordHash: null,\n displayNameOverride: null,\n authProvider: \"local\",\n disabled: false,\n });\n}\n\nasync function removeUnusedInvite(db: InviteStore, row: ShelfRow): Promise<void> {\n await db.remove(db.tables.userInviteTokens, String(row[\"id\"]));\n}\n\nasync function supersedeUnusedInvites(db: InviteStore, userId: string): Promise<void> {\n const existing = (await db.list(db.tables.userInviteTokens, {\n where: eq(tableColumn(db.tables.userInviteTokens, \"userId\"), userId),\n })) as ShelfRow[];\n await Promise.all(\n existing\n .filter((row) => !row[\"usedAt\"])\n .map(async (row) => {\n await removeUnusedInvite(db, row);\n }),\n );\n}\n\n/** Stage a shelf user and mint a single-use invite token (returned once). */\nexport async function issueInvite(\n db: DatabaseAdapter,\n input: IssueInviteInput,\n): Promise<{ inviteId: string; token: string; expiresAt: string }> {\n return await withInviteTx(db, async (store) => {\n const shelfUser = await stageShelfUser(store, input);\n const userId = String(shelfUser[\"id\"]);\n await supersedeUnusedInvites(store, userId);\n const now = new Date().toISOString();\n const expiresAt = new Date(Date.now() + inviteLifetimeMs(input.inviteExpiryMs)).toISOString();\n const inviteId = ulid();\n const token = randomToken(\"inv_\").value;\n await store.insert(store.tables.userInviteTokens, {\n id: inviteId,\n userId,\n tokenHash: sha256(token),\n expiresAt,\n usedAt: null,\n createdAt: now,\n });\n return { inviteId, token, expiresAt };\n });\n}\n\n/** Check an invite token without consuming it (drives the accept page). */\nexport async function verifyInvite(\n db: DatabaseAdapter,\n input: InviteTokenInput,\n): Promise<{ email: string; name: string }> {\n const { shelfUser } = await assertInviteValid(db, input);\n return { email: String(shelfUser[\"email\"]), name: String(shelfUser[\"name\"]) };\n}\n\n/** Whether an invite row is already spent (busy-downgrade probe). */\nasync function inviteSpent(db: InviteStore, inviteId: string): Promise<boolean> {\n const invite: ShelfRow | null = await db.get(db.tables.userInviteTokens, inviteId);\n return !!invite?.[\"usedAt\"];\n}\n\n/** Consume an invite: set the engine credential and return the shelf user. */\nexport async function acceptInvite(\n db: DatabaseAdapter,\n input: AcceptInviteInput,\n): Promise<AuthUser> {\n assertPasswordPolicy(input.password);\n try {\n return await withInviteTx(db, async (store) => {\n const { invite, shelfUser } = await assertInviteValid(store, input);\n await ensureEngineIdentity(store, shelfUser, input.password);\n const now = new Date().toISOString();\n await store.update(store.tables.users, String(shelfUser[\"id\"]), { lastLoginAt: now });\n await store.update(store.tables.userInviteTokens, String(invite[\"id\"]), { usedAt: now });\n return toAuthUser(shelfUser);\n });\n } catch (error) {\n // A transaction loser (e.g. sqlite's busy connection) surfaces a driver\n // error instead of the invite error; re-probe so callers see the stable\n // \"Invalid or expired invite\" whenever the winner consumed the token.\n if (await inviteSpent(db, input.inviteId)) {\n throw inviteError();\n }\n throw error;\n }\n}\n"],"mappings":"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"}
package/dist/passkeys.mjs CHANGED
@@ -3,7 +3,7 @@ import "node:url";
3
3
  import.meta.url;
4
4
  import { baseAuthTables } from "./auth-tables.mjs";
5
5
  import { tableColumn } from "./invites.mjs";
6
- import { eq } from "drizzle-orm";
6
+ import { eq } from "@storyshelf/core/orm";
7
7
  //#region src/passkeys.ts
8
8
  /** Registered passkeys for a user, oldest first. */
9
9
  async function listUserPasskeys(db, userId) {
@@ -1 +1 @@
1
- {"version":3,"file":"passkeys.mjs","names":[],"sources":["../src/passkeys.ts"],"sourcesContent":["/**\n * Shelf-side passkey inventory plus the local-credential flag.\n *\n * The profile page needs both to decide which security sections to render:\n * the key list (with the \"register a 2nd key\" nudge) and the password form\n * (only when a `credential` account exists — social-only users have none).\n * Registration and deletion go through the passkey plugin endpoints;\n * deletion is ownership-scoped server-side.\n */\nimport type { DatabaseAdapter } from \"@storyshelf/core/adapter/database\";\nimport { eq } from \"drizzle-orm\";\nimport { baseAuthTables } from \"./auth-tables.ts\";\nimport { tableColumn } from \"./invites.ts\";\n\n/** One registered passkey for the profile page. */\nexport interface EnginePasskeyInfo {\n id: string;\n name: string | null;\n deviceType: string;\n backedUp: boolean;\n transports: string | null;\n aaguid: string | null;\n createdAt: string;\n}\n\n/** Registered passkeys for a user, oldest first. */\nexport async function listUserPasskeys(\n db: DatabaseAdapter,\n userId: string,\n): Promise<EnginePasskeyInfo[]> {\n const rows = (await db.list(baseAuthTables.passkey, {\n where: eq(tableColumn(baseAuthTables.passkey, \"userId\"), userId),\n })) as Record<string, unknown>[];\n return rows\n .map((row) => ({\n id: String(row[\"id\"]),\n name: (row[\"name\"] as string | null) ?? null,\n deviceType: typeof row[\"deviceType\"] === \"string\" ? row[\"deviceType\"] : \"unknown\",\n backedUp: row[\"backedUp\"] === true,\n transports: (row[\"transports\"] as string | null) ?? null,\n aaguid: (row[\"aaguid\"] as string | null) ?? null,\n createdAt: String(row[\"createdAt\"]),\n }))\n .toSorted((a, b) => (a.createdAt < b.createdAt ? -1 : 1));\n}\n\n/** Whether the user can change a local password (has a credential account). */\nexport async function hasPasswordCredential(db: DatabaseAdapter, userId: string): Promise<boolean> {\n const rows = (await db.list(baseAuthTables.account, {\n where: eq(tableColumn(baseAuthTables.account, \"userId\"), userId),\n })) as Record<string, unknown>[];\n return rows.some((row) => row[\"providerId\"] === \"credential\");\n}\n"],"mappings":";;;;;;;;AA0BA,eAAsB,iBACpB,IACA,QAC8B;CAI9B,QAAO,MAHa,GAAG,KAAK,eAAe,SAAS,EAClD,OAAO,GAAG,YAAY,eAAe,SAAS,QAAQ,GAAG,MAAM,EACjE,CAAC,EAAA,CAEE,KAAK,SAAS;EACb,IAAI,OAAO,IAAI,KAAK;EACpB,MAAO,IAAI,WAA6B;EACxC,YAAY,OAAO,IAAI,kBAAkB,WAAW,IAAI,gBAAgB;EACxE,UAAU,IAAI,gBAAgB;EAC9B,YAAa,IAAI,iBAAmC;EACpD,QAAS,IAAI,aAA+B;EAC5C,WAAW,OAAO,IAAI,YAAY;CACpC,EAAE,CAAC,CACF,UAAU,GAAG,MAAO,EAAE,YAAY,EAAE,YAAY,KAAK,CAAE;AAC5D;;AAGA,eAAsB,sBAAsB,IAAqB,QAAkC;CAIjG,QAAO,MAHa,GAAG,KAAK,eAAe,SAAS,EAClD,OAAO,GAAG,YAAY,eAAe,SAAS,QAAQ,GAAG,MAAM,EACjE,CAAC,EAAA,CACW,MAAM,QAAQ,IAAI,kBAAkB,YAAY;AAC9D"}
1
+ {"version":3,"file":"passkeys.mjs","names":[],"sources":["../src/passkeys.ts"],"sourcesContent":["/**\n * Shelf-side passkey inventory plus the local-credential flag.\n *\n * The profile page needs both to decide which security sections to render:\n * the key list (with the \"register a 2nd key\" nudge) and the password form\n * (only when a `credential` account exists — social-only users have none).\n * Registration and deletion go through the passkey plugin endpoints;\n * deletion is ownership-scoped server-side.\n */\nimport type { DatabaseAdapter } from \"@storyshelf/core/adapter/database\";\nimport { eq } from \"@storyshelf/core/orm\";\nimport { baseAuthTables } from \"./auth-tables.ts\";\nimport { tableColumn } from \"./invites.ts\";\n\n/** One registered passkey for the profile page. */\nexport interface EnginePasskeyInfo {\n id: string;\n name: string | null;\n deviceType: string;\n backedUp: boolean;\n transports: string | null;\n aaguid: string | null;\n createdAt: string;\n}\n\n/** Registered passkeys for a user, oldest first. */\nexport async function listUserPasskeys(\n db: DatabaseAdapter,\n userId: string,\n): Promise<EnginePasskeyInfo[]> {\n const rows = (await db.list(baseAuthTables.passkey, {\n where: eq(tableColumn(baseAuthTables.passkey, \"userId\"), userId),\n })) as Record<string, unknown>[];\n return rows\n .map((row) => ({\n id: String(row[\"id\"]),\n name: (row[\"name\"] as string | null) ?? null,\n deviceType: typeof row[\"deviceType\"] === \"string\" ? row[\"deviceType\"] : \"unknown\",\n backedUp: row[\"backedUp\"] === true,\n transports: (row[\"transports\"] as string | null) ?? null,\n aaguid: (row[\"aaguid\"] as string | null) ?? null,\n createdAt: String(row[\"createdAt\"]),\n }))\n .toSorted((a, b) => (a.createdAt < b.createdAt ? -1 : 1));\n}\n\n/** Whether the user can change a local password (has a credential account). */\nexport async function hasPasswordCredential(db: DatabaseAdapter, userId: string): Promise<boolean> {\n const rows = (await db.list(baseAuthTables.account, {\n where: eq(tableColumn(baseAuthTables.account, \"userId\"), userId),\n })) as Record<string, unknown>[];\n return rows.some((row) => row[\"providerId\"] === \"credential\");\n}\n"],"mappings":";;;;;;;;AA0BA,eAAsB,iBACpB,IACA,QAC8B;CAI9B,QAAO,MAHa,GAAG,KAAK,eAAe,SAAS,EAClD,OAAO,GAAG,YAAY,eAAe,SAAS,QAAQ,GAAG,MAAM,EACjE,CAAC,EAAA,CAEE,KAAK,SAAS;EACb,IAAI,OAAO,IAAI,KAAK;EACpB,MAAO,IAAI,WAA6B;EACxC,YAAY,OAAO,IAAI,kBAAkB,WAAW,IAAI,gBAAgB;EACxE,UAAU,IAAI,gBAAgB;EAC9B,YAAa,IAAI,iBAAmC;EACpD,QAAS,IAAI,aAA+B;EAC5C,WAAW,OAAO,IAAI,YAAY;CACpC,EAAE,CAAC,CACF,UAAU,GAAG,MAAO,EAAE,YAAY,EAAE,YAAY,KAAK,CAAE;AAC5D;;AAGA,eAAsB,sBAAsB,IAAqB,QAAkC;CAIjG,QAAO,MAHa,GAAG,KAAK,eAAe,SAAS,EAClD,OAAO,GAAG,YAAY,eAAe,SAAS,QAAQ,GAAG,MAAM,EACjE,CAAC,EAAA,CACW,MAAM,QAAQ,IAAI,kBAAkB,YAAY;AAC9D"}
package/dist/sessions.mjs CHANGED
@@ -3,7 +3,7 @@ import "node:url";
3
3
  import.meta.url;
4
4
  import { baseAuthTables } from "./auth-tables.mjs";
5
5
  import { tableColumn } from "./invites.mjs";
6
- import { eq } from "drizzle-orm";
6
+ import { eq } from "@storyshelf/core/orm";
7
7
  //#region src/sessions.ts
8
8
  /**
9
9
  * Raw session token from a possibly-signed cookie value (`token.signature`).
@@ -1 +1 @@
1
- {"version":3,"file":"sessions.mjs","names":[],"sources":["../src/sessions.ts"],"sourcesContent":["/**\n * Shelf-side session inventory for the profile device list.\n *\n * Listing reads our `session` table directly (full rows, including tokens\n * for revoke + current-device matching). Mutations go through engine\n * endpoints (`/revoke-session` is ownership-checked server-side,\n * `/revoke-other-sessions` keeps the caller), never raw deletes.\n */\nimport type { DatabaseAdapter } from \"@storyshelf/core/adapter/database\";\nimport { eq } from \"drizzle-orm\";\nimport { baseAuthTables } from \"./auth-tables.ts\";\nimport { tableColumn } from \"./invites.ts\";\n\n/** One device session for the profile page. */\nexport interface EngineSessionInfo {\n id: string;\n token: string;\n ipAddress: string | null;\n userAgent: string | null;\n createdAt: string;\n expiresAt: string;\n}\n\n/**\n * Raw session token from a possibly-signed cookie value (`token.signature`).\n * Single shared parser: cookies, CSRF binding, and table lookups must agree.\n */\nexport function unsignedToken(value: string): string {\n return value.split(\".\")[0] ?? \"\";\n}\n\n/** Delete every session row for a user (eager revocation on disable). */\nexport async function deleteUserSessions(db: DatabaseAdapter, userId: string): Promise<number> {\n const rows = (await db.list(baseAuthTables.session, {\n where: eq(tableColumn(baseAuthTables.session, \"userId\"), userId),\n })) as Record<string, unknown>[];\n await Promise.all(\n rows.map(async (row) => {\n await db.remove(baseAuthTables.session, String(row[\"id\"]));\n }),\n );\n return rows.length;\n}\n\n/** Delete one session by token (accepts the signed cookie value or raw token). */\nexport async function deleteSessionByToken(db: DatabaseAdapter, token: string): Promise<boolean> {\n const raw = token.split(\".\")[0] ?? \"\";\n if (!raw) {\n return false;\n }\n const rows = (await db.list(baseAuthTables.session, {\n where: eq(tableColumn(baseAuthTables.session, \"token\"), raw),\n limit: 1,\n })) as Record<string, unknown>[];\n const row = rows[0];\n if (!row) {\n return false;\n }\n await db.remove(baseAuthTables.session, String(row[\"id\"]));\n return true;\n}\n\n/** Active sessions for a user, newest first (expired rows are dropped). */\nexport async function listUserSessions(\n db: DatabaseAdapter,\n userId: string,\n): Promise<EngineSessionInfo[]> {\n const rows = (await db.list(baseAuthTables.session, {\n where: eq(tableColumn(baseAuthTables.session, \"userId\"), userId),\n })) as Record<string, unknown>[];\n const now = Date.now();\n return rows\n .filter((row) => new Date(String(row[\"expiresAt\"])).getTime() > now)\n .map((row) => ({\n id: String(row[\"id\"]),\n token: String(row[\"token\"]),\n ipAddress: (row[\"ipAddress\"] as string | null) ?? null,\n userAgent: (row[\"userAgent\"] as string | null) ?? null,\n createdAt: String(row[\"createdAt\"]),\n expiresAt: String(row[\"expiresAt\"]),\n }))\n .toSorted((a, b) =>\n a.createdAt === b.createdAt ? a.id.localeCompare(b.id) : a.createdAt < b.createdAt ? 1 : -1,\n );\n}\n"],"mappings":";;;;;;;;;;;AA2BA,SAAgB,cAAc,OAAuB;CACnD,OAAO,MAAM,MAAM,GAAG,CAAC,CAAC,MAAM;AAChC;;AAGA,eAAsB,mBAAmB,IAAqB,QAAiC;CAC7F,MAAM,OAAQ,MAAM,GAAG,KAAK,eAAe,SAAS,EAClD,OAAO,GAAG,YAAY,eAAe,SAAS,QAAQ,GAAG,MAAM,EACjE,CAAC;CACD,MAAM,QAAQ,IACZ,KAAK,IAAI,OAAO,QAAQ;EACtB,MAAM,GAAG,OAAO,eAAe,SAAS,OAAO,IAAI,KAAK,CAAC;CAC3D,CAAC,CACH;CACA,OAAO,KAAK;AACd;;AAGA,eAAsB,qBAAqB,IAAqB,OAAiC;CAC/F,MAAM,MAAM,MAAM,MAAM,GAAG,CAAC,CAAC,MAAM;CACnC,IAAI,CAAC,KACH,OAAO;CAMT,MAAM,OAAM,MAJQ,GAAG,KAAK,eAAe,SAAS;EAClD,OAAO,GAAG,YAAY,eAAe,SAAS,OAAO,GAAG,GAAG;EAC3D,OAAO;CACT,CAAC,EAAA,CACgB;CACjB,IAAI,CAAC,KACH,OAAO;CAET,MAAM,GAAG,OAAO,eAAe,SAAS,OAAO,IAAI,KAAK,CAAC;CACzD,OAAO;AACT;;AAGA,eAAsB,iBACpB,IACA,QAC8B;CAC9B,MAAM,OAAQ,MAAM,GAAG,KAAK,eAAe,SAAS,EAClD,OAAO,GAAG,YAAY,eAAe,SAAS,QAAQ,GAAG,MAAM,EACjE,CAAC;CACD,MAAM,MAAM,KAAK,IAAI;CACrB,OAAO,KACJ,QAAQ,QAAQ,IAAI,KAAK,OAAO,IAAI,YAAY,CAAC,CAAC,CAAC,QAAQ,IAAI,GAAG,CAAC,CACnE,KAAK,SAAS;EACb,IAAI,OAAO,IAAI,KAAK;EACpB,OAAO,OAAO,IAAI,QAAQ;EAC1B,WAAY,IAAI,gBAAkC;EAClD,WAAY,IAAI,gBAAkC;EAClD,WAAW,OAAO,IAAI,YAAY;EAClC,WAAW,OAAO,IAAI,YAAY;CACpC,EAAE,CAAC,CACF,UAAU,GAAG,MACZ,EAAE,cAAc,EAAE,YAAY,EAAE,GAAG,cAAc,EAAE,EAAE,IAAI,EAAE,YAAY,EAAE,YAAY,IAAI,EAC3F;AACJ"}
1
+ {"version":3,"file":"sessions.mjs","names":[],"sources":["../src/sessions.ts"],"sourcesContent":["/**\n * Shelf-side session inventory for the profile device list.\n *\n * Listing reads our `session` table directly (full rows, including tokens\n * for revoke + current-device matching). Mutations go through engine\n * endpoints (`/revoke-session` is ownership-checked server-side,\n * `/revoke-other-sessions` keeps the caller), never raw deletes.\n */\nimport type { DatabaseAdapter } from \"@storyshelf/core/adapter/database\";\nimport { eq } from \"@storyshelf/core/orm\";\nimport { baseAuthTables } from \"./auth-tables.ts\";\nimport { tableColumn } from \"./invites.ts\";\n\n/** One device session for the profile page. */\nexport interface EngineSessionInfo {\n id: string;\n token: string;\n ipAddress: string | null;\n userAgent: string | null;\n createdAt: string;\n expiresAt: string;\n}\n\n/**\n * Raw session token from a possibly-signed cookie value (`token.signature`).\n * Single shared parser: cookies, CSRF binding, and table lookups must agree.\n */\nexport function unsignedToken(value: string): string {\n return value.split(\".\")[0] ?? \"\";\n}\n\n/** Delete every session row for a user (eager revocation on disable). */\nexport async function deleteUserSessions(db: DatabaseAdapter, userId: string): Promise<number> {\n const rows = (await db.list(baseAuthTables.session, {\n where: eq(tableColumn(baseAuthTables.session, \"userId\"), userId),\n })) as Record<string, unknown>[];\n await Promise.all(\n rows.map(async (row) => {\n await db.remove(baseAuthTables.session, String(row[\"id\"]));\n }),\n );\n return rows.length;\n}\n\n/** Delete one session by token (accepts the signed cookie value or raw token). */\nexport async function deleteSessionByToken(db: DatabaseAdapter, token: string): Promise<boolean> {\n const raw = token.split(\".\")[0] ?? \"\";\n if (!raw) {\n return false;\n }\n const rows = (await db.list(baseAuthTables.session, {\n where: eq(tableColumn(baseAuthTables.session, \"token\"), raw),\n limit: 1,\n })) as Record<string, unknown>[];\n const row = rows[0];\n if (!row) {\n return false;\n }\n await db.remove(baseAuthTables.session, String(row[\"id\"]));\n return true;\n}\n\n/** Active sessions for a user, newest first (expired rows are dropped). */\nexport async function listUserSessions(\n db: DatabaseAdapter,\n userId: string,\n): Promise<EngineSessionInfo[]> {\n const rows = (await db.list(baseAuthTables.session, {\n where: eq(tableColumn(baseAuthTables.session, \"userId\"), userId),\n })) as Record<string, unknown>[];\n const now = Date.now();\n return rows\n .filter((row) => new Date(String(row[\"expiresAt\"])).getTime() > now)\n .map((row) => ({\n id: String(row[\"id\"]),\n token: String(row[\"token\"]),\n ipAddress: (row[\"ipAddress\"] as string | null) ?? null,\n userAgent: (row[\"userAgent\"] as string | null) ?? null,\n createdAt: String(row[\"createdAt\"]),\n expiresAt: String(row[\"expiresAt\"]),\n }))\n .toSorted((a, b) =>\n a.createdAt === b.createdAt ? a.id.localeCompare(b.id) : a.createdAt < b.createdAt ? 1 : -1,\n );\n}\n"],"mappings":";;;;;;;;;;;AA2BA,SAAgB,cAAc,OAAuB;CACnD,OAAO,MAAM,MAAM,GAAG,CAAC,CAAC,MAAM;AAChC;;AAGA,eAAsB,mBAAmB,IAAqB,QAAiC;CAC7F,MAAM,OAAQ,MAAM,GAAG,KAAK,eAAe,SAAS,EAClD,OAAO,GAAG,YAAY,eAAe,SAAS,QAAQ,GAAG,MAAM,EACjE,CAAC;CACD,MAAM,QAAQ,IACZ,KAAK,IAAI,OAAO,QAAQ;EACtB,MAAM,GAAG,OAAO,eAAe,SAAS,OAAO,IAAI,KAAK,CAAC;CAC3D,CAAC,CACH;CACA,OAAO,KAAK;AACd;;AAGA,eAAsB,qBAAqB,IAAqB,OAAiC;CAC/F,MAAM,MAAM,MAAM,MAAM,GAAG,CAAC,CAAC,MAAM;CACnC,IAAI,CAAC,KACH,OAAO;CAMT,MAAM,OAAM,MAJQ,GAAG,KAAK,eAAe,SAAS;EAClD,OAAO,GAAG,YAAY,eAAe,SAAS,OAAO,GAAG,GAAG;EAC3D,OAAO;CACT,CAAC,EAAA,CACgB;CACjB,IAAI,CAAC,KACH,OAAO;CAET,MAAM,GAAG,OAAO,eAAe,SAAS,OAAO,IAAI,KAAK,CAAC;CACzD,OAAO;AACT;;AAGA,eAAsB,iBACpB,IACA,QAC8B;CAC9B,MAAM,OAAQ,MAAM,GAAG,KAAK,eAAe,SAAS,EAClD,OAAO,GAAG,YAAY,eAAe,SAAS,QAAQ,GAAG,MAAM,EACjE,CAAC;CACD,MAAM,MAAM,KAAK,IAAI;CACrB,OAAO,KACJ,QAAQ,QAAQ,IAAI,KAAK,OAAO,IAAI,YAAY,CAAC,CAAC,CAAC,QAAQ,IAAI,GAAG,CAAC,CACnE,KAAK,SAAS;EACb,IAAI,OAAO,IAAI,KAAK;EACpB,OAAO,OAAO,IAAI,QAAQ;EAC1B,WAAY,IAAI,gBAAkC;EAClD,WAAY,IAAI,gBAAkC;EAClD,WAAW,OAAO,IAAI,YAAY;EAClC,WAAW,OAAO,IAAI,YAAY;CACpC,EAAE,CAAC,CACF,UAAU,GAAG,MACZ,EAAE,cAAc,EAAE,YAAY,EAAE,GAAG,cAAc,EAAE,EAAE,IAAI,EAAE,YAAY,EAAE,YAAY,IAAI,EAC3F;AACJ"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@storyshelf/auth",
3
- "version": "0.5.4",
3
+ "version": "0.5.5",
4
4
  "description": "Better Auth engine for StoryShelf: opaque DBAdapter bridge, shelf factory, and auth presets.",
5
5
  "keywords": [
6
6
  "auth",
@@ -53,14 +53,13 @@
53
53
  "dependencies": {
54
54
  "@better-auth/passkey": "1.7.6",
55
55
  "@better-auth/sso": "1.7.6",
56
- "@storyshelf/core": "0.5.4",
56
+ "@storyshelf/core": "0.5.5",
57
57
  "better-auth": "1.7.6",
58
- "drizzle-orm": "^0.44.0",
59
58
  "zod": "^4.2.0"
60
59
  },
61
60
  "devDependencies": {
62
- "@storyshelf/db-postgres": "0.5.4",
63
- "@storyshelf/db-sqlite": "0.5.4",
61
+ "@storyshelf/db-postgres": "0.5.5",
62
+ "@storyshelf/db-sqlite": "0.5.5",
64
63
  "@types/node": "^26.0.0",
65
64
  "@vitest/coverage-v8": "^4.0.0",
66
65
  "oxfmt": "^0.65.0",