@storyshelf/auth 0.5.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +43 -0
- package/dist/auth-tables-pg.d.mts +1971 -0
- package/dist/auth-tables-pg.mjs +167 -0
- package/dist/auth-tables-pg.mjs.map +1 -0
- package/dist/auth-tables.d.mts +1092 -0
- package/dist/auth-tables.mjs +122 -0
- package/dist/auth-tables.mjs.map +1 -0
- package/dist/bootstrap.d.mts +15 -0
- package/dist/bootstrap.mjs +40 -0
- package/dist/bootstrap.mjs.map +1 -0
- package/dist/config.d.mts +27 -0
- package/dist/config.mjs +204 -0
- package/dist/config.mjs.map +1 -0
- package/dist/db-bridge.d.mts +20 -0
- package/dist/db-bridge.mjs +208 -0
- package/dist/db-bridge.mjs.map +1 -0
- package/dist/db-values.mjs +41 -0
- package/dist/db-values.mjs.map +1 -0
- package/dist/db-where.mjs +85 -0
- package/dist/db-where.mjs.map +1 -0
- package/dist/engine.d.mts +208 -0
- package/dist/engine.mjs +444 -0
- package/dist/engine.mjs.map +1 -0
- package/dist/groups.d.mts +54 -0
- package/dist/groups.mjs +129 -0
- package/dist/groups.mjs.map +1 -0
- package/dist/index.d.mts +18 -0
- package/dist/index.mjs +17 -0
- package/dist/invites.d.mts +41 -0
- package/dist/invites.mjs +225 -0
- package/dist/invites.mjs.map +1 -0
- package/dist/passkeys.d.mts +21 -0
- package/dist/passkeys.mjs +27 -0
- package/dist/passkeys.mjs.map +1 -0
- package/dist/presets/enterprise.d.mts +24 -0
- package/dist/presets/enterprise.mjs +44 -0
- package/dist/presets/enterprise.mjs.map +1 -0
- package/dist/presets/index.d.mts +5 -0
- package/dist/presets/social.d.mts +34 -0
- package/dist/presets/social.mjs +60 -0
- package/dist/presets/social.mjs.map +1 -0
- package/dist/presets/sso.d.mts +72 -0
- package/dist/presets/sso.mjs +136 -0
- package/dist/presets/sso.mjs.map +1 -0
- package/dist/sessions.d.mts +23 -0
- package/dist/sessions.mjs +51 -0
- package/dist/sessions.mjs.map +1 -0
- package/package.json +82 -0
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import "node:path";
|
|
2
|
+
import "node:url";
|
|
3
|
+
import.meta.url;
|
|
4
|
+
import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core";
|
|
5
|
+
//#region src/auth-tables.ts
|
|
6
|
+
/**
|
|
7
|
+
* Auth table definitions: drizzle handles for the Better Auth base models.
|
|
8
|
+
*
|
|
9
|
+
* Keys are camelCase (repo convention); columns snake_case. Field names match
|
|
10
|
+
* Better Auth defaults exactly, so the bridge needs no name translation.
|
|
11
|
+
* Custom modelName/fieldName options are unsupported in v1 (fail fast).
|
|
12
|
+
* DDL for drivers is vendored separately (see E3); these defs are the source
|
|
13
|
+
* of truth the DDL must match (the strict schema checker enforces it).
|
|
14
|
+
*/
|
|
15
|
+
/** Narrow `user` table definition (Better Auth base model). */
|
|
16
|
+
const authUser = sqliteTable("user", {
|
|
17
|
+
id: text("id").primaryKey(),
|
|
18
|
+
name: text("name").notNull(),
|
|
19
|
+
email: text("email").notNull().unique(),
|
|
20
|
+
emailVerified: integer("email_verified", { mode: "boolean" }).notNull().default(false),
|
|
21
|
+
image: text("image"),
|
|
22
|
+
createdAt: text("created_at").notNull(),
|
|
23
|
+
updatedAt: text("updated_at").notNull()
|
|
24
|
+
});
|
|
25
|
+
/** Narrow `session` table definition (Better Auth base model). */
|
|
26
|
+
const authSession = sqliteTable("session", {
|
|
27
|
+
id: text("id").primaryKey(),
|
|
28
|
+
userId: text("user_id").notNull().references(() => authUser.id, { onDelete: "cascade" }),
|
|
29
|
+
token: text("token").notNull().unique(),
|
|
30
|
+
expiresAt: text("expires_at").notNull(),
|
|
31
|
+
ipAddress: text("ip_address"),
|
|
32
|
+
userAgent: text("user_agent"),
|
|
33
|
+
createdAt: text("created_at").notNull(),
|
|
34
|
+
updatedAt: text("updated_at").notNull()
|
|
35
|
+
});
|
|
36
|
+
/** Narrow `account` table definition (Better Auth base model). */
|
|
37
|
+
const authAccount = sqliteTable("account", {
|
|
38
|
+
id: text("id").primaryKey(),
|
|
39
|
+
userId: text("user_id").notNull().references(() => authUser.id, { onDelete: "cascade" }),
|
|
40
|
+
accountId: text("account_id").notNull(),
|
|
41
|
+
providerId: text("provider_id").notNull(),
|
|
42
|
+
accessToken: text("access_token"),
|
|
43
|
+
refreshToken: text("refresh_token"),
|
|
44
|
+
idToken: text("id_token"),
|
|
45
|
+
accessTokenExpiresAt: text("access_token_expires_at"),
|
|
46
|
+
refreshTokenExpiresAt: text("refresh_token_expires_at"),
|
|
47
|
+
scope: text("scope"),
|
|
48
|
+
expiresAt: text("expires_at"),
|
|
49
|
+
password: text("password"),
|
|
50
|
+
createdAt: text("created_at").notNull(),
|
|
51
|
+
updatedAt: text("updated_at").notNull()
|
|
52
|
+
});
|
|
53
|
+
/** Narrow `verification` table definition (Better Auth base model). */
|
|
54
|
+
const authVerification = sqliteTable("verification", {
|
|
55
|
+
id: text("id").primaryKey(),
|
|
56
|
+
identifier: text("identifier").notNull(),
|
|
57
|
+
value: text("value").notNull(),
|
|
58
|
+
expiresAt: text("expires_at").notNull(),
|
|
59
|
+
createdAt: text("created_at").notNull(),
|
|
60
|
+
updatedAt: text("updated_at").notNull()
|
|
61
|
+
});
|
|
62
|
+
/** Narrow `passkey` table definition (passkey plugin model). */ const authPasskey = sqliteTable("passkey", {
|
|
63
|
+
id: text("id").primaryKey(),
|
|
64
|
+
name: text("name"),
|
|
65
|
+
publicKey: text("public_key").notNull(),
|
|
66
|
+
userId: text("user_id").notNull().references(() => authUser.id, { onDelete: "cascade" }),
|
|
67
|
+
credentialID: text("credential_id").notNull(),
|
|
68
|
+
counter: integer("counter").notNull(),
|
|
69
|
+
deviceType: text("device_type").notNull(),
|
|
70
|
+
backedUp: integer("backed_up", { mode: "boolean" }).notNull().default(false),
|
|
71
|
+
transports: text("transports"),
|
|
72
|
+
aaguid: text("aaguid"),
|
|
73
|
+
createdAt: text("created_at").notNull(),
|
|
74
|
+
updatedAt: text("updated_at")
|
|
75
|
+
});
|
|
76
|
+
/** Narrow `ssoProvider` table definition (SSO plugin model). */
|
|
77
|
+
const authSsoProvider = sqliteTable("ssoProvider", {
|
|
78
|
+
id: text("id").primaryKey(),
|
|
79
|
+
issuer: text("issuer").notNull(),
|
|
80
|
+
oidcConfig: text("oidc_config"),
|
|
81
|
+
samlConfig: text("saml_config"),
|
|
82
|
+
userId: text("user_id").notNull().references(() => authUser.id, { onDelete: "cascade" }),
|
|
83
|
+
providerId: text("provider_id").notNull().unique(),
|
|
84
|
+
organizationId: text("organization_id"),
|
|
85
|
+
domain: text("domain").notNull()
|
|
86
|
+
});
|
|
87
|
+
/** Table handles for the Better Auth base models, keyed by model name. */
|
|
88
|
+
const baseAuthTables = {
|
|
89
|
+
user: authUser,
|
|
90
|
+
session: authSession,
|
|
91
|
+
account: authAccount,
|
|
92
|
+
verification: authVerification,
|
|
93
|
+
passkey: authPasskey,
|
|
94
|
+
ssoProvider: authSsoProvider
|
|
95
|
+
};
|
|
96
|
+
/** Date-valued fields per base model (ISO strings at rest, Dates in flight). */
|
|
97
|
+
const baseAuthDateFields = {
|
|
98
|
+
user: ["createdAt", "updatedAt"],
|
|
99
|
+
session: [
|
|
100
|
+
"expiresAt",
|
|
101
|
+
"createdAt",
|
|
102
|
+
"updatedAt"
|
|
103
|
+
],
|
|
104
|
+
account: [
|
|
105
|
+
"accessTokenExpiresAt",
|
|
106
|
+
"refreshTokenExpiresAt",
|
|
107
|
+
"expiresAt",
|
|
108
|
+
"createdAt",
|
|
109
|
+
"updatedAt"
|
|
110
|
+
],
|
|
111
|
+
verification: [
|
|
112
|
+
"expiresAt",
|
|
113
|
+
"createdAt",
|
|
114
|
+
"updatedAt"
|
|
115
|
+
],
|
|
116
|
+
passkey: ["createdAt", "updatedAt"],
|
|
117
|
+
ssoProvider: []
|
|
118
|
+
};
|
|
119
|
+
//#endregion
|
|
120
|
+
export { authAccount, authPasskey, authSession, authSsoProvider, authUser, authVerification, baseAuthDateFields, baseAuthTables };
|
|
121
|
+
|
|
122
|
+
//# sourceMappingURL=auth-tables.mjs.map
|
|
@@ -0,0 +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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"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import __tsdown_shims_path from 'node:path';
|
|
2
|
+
import __tsdown_shims_url from 'node:url';
|
|
3
|
+
import { DatabaseAdapter } from "@storyshelf/core/adapter/database";
|
|
4
|
+
//#region src/bootstrap.d.ts
|
|
5
|
+
/** Input for {@link ensurePasswordAdmin}. */
|
|
6
|
+
interface PasswordAdminInput {
|
|
7
|
+
email: string;
|
|
8
|
+
name?: string;
|
|
9
|
+
password: string;
|
|
10
|
+
}
|
|
11
|
+
/** Provision (or refresh the credential of) the env-driven local admin. */
|
|
12
|
+
declare function ensurePasswordAdmin(db: DatabaseAdapter, input: PasswordAdminInput): Promise<void>;
|
|
13
|
+
//#endregion
|
|
14
|
+
export { PasswordAdminInput, ensurePasswordAdmin };
|
|
15
|
+
//# sourceMappingURL=bootstrap.d.mts.map
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import "node:path";
|
|
2
|
+
import "node:url";
|
|
3
|
+
import.meta.url;
|
|
4
|
+
import { ensureEngineIdentity, tableColumn } from "./invites.mjs";
|
|
5
|
+
import { ulid } from "@storyshelf/core/utils";
|
|
6
|
+
import { eq } from "drizzle-orm";
|
|
7
|
+
//#region src/bootstrap.ts
|
|
8
|
+
/** Find the env admin row, or stage it (admin role, no credential yet). */
|
|
9
|
+
async function stagePasswordAdmin(db, email, name, now) {
|
|
10
|
+
const existing = (await db.list(db.tables.users, {
|
|
11
|
+
where: eq(tableColumn(db.tables.users, "email"), email),
|
|
12
|
+
limit: 1
|
|
13
|
+
}))[0];
|
|
14
|
+
if (existing) return existing;
|
|
15
|
+
return await db.insert(db.tables.users, {
|
|
16
|
+
id: ulid(),
|
|
17
|
+
email,
|
|
18
|
+
name,
|
|
19
|
+
avatarUrl: null,
|
|
20
|
+
role: "admin",
|
|
21
|
+
lastLoginAt: null,
|
|
22
|
+
createdAt: now,
|
|
23
|
+
passwordHash: null,
|
|
24
|
+
displayNameOverride: null,
|
|
25
|
+
authProvider: "local",
|
|
26
|
+
disabled: false
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
/** Provision (or refresh the credential of) the env-driven local admin. */
|
|
30
|
+
async function ensurePasswordAdmin(db, input) {
|
|
31
|
+
const email = input.email.trim().toLowerCase();
|
|
32
|
+
if (input.password.length < 12) throw new Error("Password must be at least 12 characters");
|
|
33
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
34
|
+
const shelfUser = await stagePasswordAdmin(db, email, input.name ?? "Admin", now);
|
|
35
|
+
await ensureEngineIdentity(db, shelfUser, input.password);
|
|
36
|
+
}
|
|
37
|
+
//#endregion
|
|
38
|
+
export { ensurePasswordAdmin };
|
|
39
|
+
|
|
40
|
+
//# sourceMappingURL=bootstrap.mjs.map
|
|
@@ -0,0 +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"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import __tsdown_shims_path from 'node:path';
|
|
2
|
+
import __tsdown_shims_url from 'node:url';
|
|
3
|
+
import { ShelfAuthOptions } from "./engine.mjs";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
//#region src/config.d.ts
|
|
6
|
+
/** Extract the variable name from an `{env:NAME}` ref, or null. */
|
|
7
|
+
declare function parseEnvRef(value: string): string | null;
|
|
8
|
+
/**
|
|
9
|
+
* Customer-owned secret backend (Vault/AWS/GCP/...). Called once per boot
|
|
10
|
+
* with the ref names missing from `process.env`; return the values you hold.
|
|
11
|
+
*/
|
|
12
|
+
interface ResolveSecrets {
|
|
13
|
+
(names: string[]): Promise<Record<string, string | undefined>> | Record<string, string | undefined>;
|
|
14
|
+
}
|
|
15
|
+
/** Input for {@link resolveAuthOptions} (refs allowed in any string field). */
|
|
16
|
+
interface RawShelfAuthOptions extends Omit<ShelfAuthOptions, "plugins"> {
|
|
17
|
+
/** Secret backend fallback for refs missing from `process.env`. */
|
|
18
|
+
resolveSecrets?: ResolveSecrets;
|
|
19
|
+
/** Custom Better Auth plugins (never validated; passed through by reference). */
|
|
20
|
+
plugins?: ShelfAuthOptions["plugins"];
|
|
21
|
+
}
|
|
22
|
+
/** Zod schema validating shelf auth options (before secret resolution). */
|
|
23
|
+
declare const authOptionsSchema: z.ZodType<Omit<ShelfAuthOptions, "plugins">>;
|
|
24
|
+
declare function resolveAuthOptions(input: RawShelfAuthOptions): Promise<ShelfAuthOptions>;
|
|
25
|
+
//#endregion
|
|
26
|
+
export { RawShelfAuthOptions, ResolveSecrets, authOptionsSchema, parseEnvRef, resolveAuthOptions };
|
|
27
|
+
//# sourceMappingURL=config.d.mts.map
|
package/dist/config.mjs
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import "node:path";
|
|
2
|
+
import "node:url";
|
|
3
|
+
import.meta.url;
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
//#region src/config.ts
|
|
6
|
+
/**
|
|
7
|
+
* Auth config-as-code: validated, secret-safe engine options.
|
|
8
|
+
*
|
|
9
|
+
* Servers declare auth in code; secrets stay out of it via `{env:NAME}`
|
|
10
|
+
* refs resolved at boot from `process.env` first, then the customer-owned
|
|
11
|
+
* `resolveSecrets` hook (Vault/AWS/GCP/...). Precedence everywhere:
|
|
12
|
+
* explicit literal > `{env}` ref > schema default, and code (`defaultSSO`,
|
|
13
|
+
* recipes) > DB rows > plugin defaults at sign-in time.
|
|
14
|
+
*/
|
|
15
|
+
/** `{env:NAME}` reference (whole-string match; surrounding text is literal). */
|
|
16
|
+
const ENV_REF = /^\{env:(?<name>[A-Za-z_][A-Za-z0-9_]*)\}$/u;
|
|
17
|
+
/** Extract the variable name from an `{env:NAME}` ref, or null. */
|
|
18
|
+
function parseEnvRef(value) {
|
|
19
|
+
return ENV_REF.exec(value)?.groups?.["name"] ?? null;
|
|
20
|
+
}
|
|
21
|
+
const socialSchema = z.object({
|
|
22
|
+
id: z.enum([
|
|
23
|
+
"github",
|
|
24
|
+
"gitlab",
|
|
25
|
+
"google",
|
|
26
|
+
"microsoft",
|
|
27
|
+
"cognito"
|
|
28
|
+
]),
|
|
29
|
+
label: z.string().min(1),
|
|
30
|
+
clientId: z.string().min(1),
|
|
31
|
+
clientSecret: z.string().optional(),
|
|
32
|
+
extra: z.record(z.string(), z.string().optional()).optional()
|
|
33
|
+
});
|
|
34
|
+
const oauthSchema = z.object({
|
|
35
|
+
id: z.string().min(1),
|
|
36
|
+
label: z.string().min(1),
|
|
37
|
+
clientId: z.string().min(1),
|
|
38
|
+
clientSecret: z.string().optional(),
|
|
39
|
+
discoveryUrl: z.string().optional(),
|
|
40
|
+
authorizationUrl: z.string().optional(),
|
|
41
|
+
tokenUrl: z.string().optional(),
|
|
42
|
+
userInfoUrl: z.string().optional(),
|
|
43
|
+
scopes: z.array(z.string().min(1)).optional()
|
|
44
|
+
});
|
|
45
|
+
const ssoOidcSchema = z.object({
|
|
46
|
+
issuer: z.string().min(1),
|
|
47
|
+
discoveryUrl: z.string().optional(),
|
|
48
|
+
clientId: z.string().min(1),
|
|
49
|
+
clientSecret: z.string().optional(),
|
|
50
|
+
scopes: z.array(z.string().min(1)).optional(),
|
|
51
|
+
pkce: z.boolean().optional()
|
|
52
|
+
});
|
|
53
|
+
const ssoSamlSchema = z.object({
|
|
54
|
+
issuer: z.string().min(1),
|
|
55
|
+
entryPoint: z.string().min(1),
|
|
56
|
+
cert: z.union([z.string(), z.array(z.string())]).optional(),
|
|
57
|
+
metadataXml: z.string().optional(),
|
|
58
|
+
entityID: z.string().optional(),
|
|
59
|
+
audience: z.string().optional(),
|
|
60
|
+
wantAssertionsSigned: z.boolean().optional()
|
|
61
|
+
});
|
|
62
|
+
const ssoGroupsSchema = z.object({
|
|
63
|
+
claim: z.union([z.string().min(1), z.array(z.string().min(1)).min(1)]).optional(),
|
|
64
|
+
admins: z.array(z.string().min(1)).optional()
|
|
65
|
+
});
|
|
66
|
+
const ssoProviderSchema = z.object({
|
|
67
|
+
id: z.string().min(1),
|
|
68
|
+
label: z.string().min(1),
|
|
69
|
+
domain: z.string().min(1),
|
|
70
|
+
oidc: ssoOidcSchema.optional(),
|
|
71
|
+
saml: ssoSamlSchema.optional(),
|
|
72
|
+
groups: ssoGroupsSchema.optional()
|
|
73
|
+
});
|
|
74
|
+
const passkeysSchema = z.object({
|
|
75
|
+
rpID: z.string().min(1).optional(),
|
|
76
|
+
rpName: z.string().min(1).optional(),
|
|
77
|
+
origin: z.union([z.string().min(1), z.array(z.string().min(1))]).optional()
|
|
78
|
+
});
|
|
79
|
+
/** Zod schema validating shelf auth options (before secret resolution). */
|
|
80
|
+
const authOptionsSchema = z.object({
|
|
81
|
+
db: z.custom((value) => typeof value === "object" && value !== null),
|
|
82
|
+
secret: z.string().min(1),
|
|
83
|
+
baseURL: z.string().url(),
|
|
84
|
+
emailPassword: z.boolean().optional(),
|
|
85
|
+
emailSender: z.custom((value) => typeof value === "object" && value !== null).optional(),
|
|
86
|
+
fromEmail: z.string().min(1).optional(),
|
|
87
|
+
onAuthSystemEvent: z.custom((value) => typeof value === "function").optional(),
|
|
88
|
+
logger: z.custom((value) => typeof value === "object" && value !== null).optional(),
|
|
89
|
+
social: z.array(socialSchema).optional(),
|
|
90
|
+
oauth: z.array(oauthSchema).optional(),
|
|
91
|
+
sso: z.object({
|
|
92
|
+
providers: z.array(ssoProviderSchema).min(1),
|
|
93
|
+
disableImplicitSignUp: z.boolean().optional()
|
|
94
|
+
}).optional(),
|
|
95
|
+
passkeys: passkeysSchema.optional(),
|
|
96
|
+
sessionExpiresInDays: z.number().int().positive().optional()
|
|
97
|
+
}).strict();
|
|
98
|
+
function formatIssues(error) {
|
|
99
|
+
return error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ");
|
|
100
|
+
}
|
|
101
|
+
function collectFromValue(value, into) {
|
|
102
|
+
if (Array.isArray(value)) {
|
|
103
|
+
for (const entry of value) collectEnvNames(entry, into);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
if (typeof value === "object" && value !== null) for (const entry of Object.values(value)) collectEnvNames(entry, into);
|
|
107
|
+
}
|
|
108
|
+
function collectEnvNames(value, into) {
|
|
109
|
+
if (typeof value === "string") {
|
|
110
|
+
const name = parseEnvRef(value);
|
|
111
|
+
if (name) into.add(name);
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
collectFromValue(value, into);
|
|
115
|
+
}
|
|
116
|
+
function substituteEnvNames(value, secrets) {
|
|
117
|
+
if (typeof value === "string") {
|
|
118
|
+
const name = parseEnvRef(value);
|
|
119
|
+
return name ? secrets.get(name) ?? value : value;
|
|
120
|
+
}
|
|
121
|
+
if (Array.isArray(value)) return value.map((entry) => substituteEnvNames(entry, secrets));
|
|
122
|
+
if (typeof value === "object" && value !== null) return Object.fromEntries(Object.entries(value).map(([key, entry]) => [key, substituteEnvNames(entry, secrets)]));
|
|
123
|
+
return value;
|
|
124
|
+
}
|
|
125
|
+
/** Split ref names into env-held and hook-needed sets. */
|
|
126
|
+
function partitionSecretNames(names) {
|
|
127
|
+
const secrets = /* @__PURE__ */ new Map();
|
|
128
|
+
const missing = [];
|
|
129
|
+
for (const name of names) {
|
|
130
|
+
const fromEnv = process.env[name];
|
|
131
|
+
if (fromEnv) secrets.set(name, fromEnv);
|
|
132
|
+
else missing.push(name);
|
|
133
|
+
}
|
|
134
|
+
return {
|
|
135
|
+
secrets,
|
|
136
|
+
missing
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
/** Fill hook-provided values into the secret map. */
|
|
140
|
+
async function fillFromHook(secrets, missing, hook) {
|
|
141
|
+
if (missing.length === 0 || !hook) return;
|
|
142
|
+
const provided = await hook(missing);
|
|
143
|
+
for (const name of missing) if (provided[name]) secrets.set(name, provided[name]);
|
|
144
|
+
}
|
|
145
|
+
/** Resolve `{env:NAME}` refs from the environment, then the hook. */
|
|
146
|
+
async function resolveSecretValues(names, hook) {
|
|
147
|
+
const { secrets, missing } = partitionSecretNames(names);
|
|
148
|
+
await fillFromHook(secrets, missing, hook);
|
|
149
|
+
const unsettled = [...names].filter((name) => !secrets.has(name));
|
|
150
|
+
if (unsettled.length > 0) throw new Error(`Shelf auth secrets missing: ${unsettled.join(", ")} (set the env vars or provide resolveSecrets)`);
|
|
151
|
+
return secrets;
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Secret-bearing subtrees: `db` (live adapter with circular drizzle handles),
|
|
155
|
+
* `emailSender` (live transport), `logger` (pino instance), `onAuthSystemEvent`
|
|
156
|
+
* (host callback), and `plugins` (class instances) pass through by reference
|
|
157
|
+
* and are never walked or cloned — refs only make sense in plain config data.
|
|
158
|
+
*/
|
|
159
|
+
function secretSubtrees(options) {
|
|
160
|
+
const { db: _db, emailSender: _sender, logger: _logger, onAuthSystemEvent: _hook, ...rest } = options;
|
|
161
|
+
return rest;
|
|
162
|
+
}
|
|
163
|
+
function parseRawOptions(input) {
|
|
164
|
+
const { resolveSecrets, plugins, ...rest } = input;
|
|
165
|
+
const parsed = authOptionsSchema.safeParse(rest);
|
|
166
|
+
if (!parsed.success) throw new Error(`Invalid shelf auth options: ${formatIssues(parsed.error)}`);
|
|
167
|
+
return {
|
|
168
|
+
options: parsed.data,
|
|
169
|
+
plugins,
|
|
170
|
+
resolveSecrets
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Validate auth options and resolve `{env:NAME}` secret refs.
|
|
175
|
+
*
|
|
176
|
+
* @param input - Raw options (refs allowed in any string field) plus the
|
|
177
|
+
* optional customer-owned `resolveSecrets` fallback.
|
|
178
|
+
* @returns Engine-ready options for {@link createShelfAuth}.
|
|
179
|
+
*/
|
|
180
|
+
/** Collect `{env:}` refs and resolve their values (empty when none). */
|
|
181
|
+
async function collectSecrets(options, hook) {
|
|
182
|
+
const names = /* @__PURE__ */ new Set();
|
|
183
|
+
collectEnvNames(secretSubtrees(options), names);
|
|
184
|
+
if (names.size === 0) return {};
|
|
185
|
+
const values = await resolveSecretValues(names, hook);
|
|
186
|
+
return substituteEnvNames(secretSubtrees(options), values);
|
|
187
|
+
}
|
|
188
|
+
async function resolveAuthOptions(input) {
|
|
189
|
+
const { options, plugins, resolveSecrets } = parseRawOptions(input);
|
|
190
|
+
const secrets = await collectSecrets(options, resolveSecrets);
|
|
191
|
+
const resolved = {
|
|
192
|
+
...options,
|
|
193
|
+
...secrets
|
|
194
|
+
};
|
|
195
|
+
if (resolved.secret.length < 32) throw new Error("Shelf auth requires a secret of at least 32 characters");
|
|
196
|
+
return {
|
|
197
|
+
...resolved,
|
|
198
|
+
...plugins ? { plugins } : {}
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
//#endregion
|
|
202
|
+
export { authOptionsSchema, parseEnvRef, resolveAuthOptions };
|
|
203
|
+
|
|
204
|
+
//# sourceMappingURL=config.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.mjs","names":[],"sources":["../src/config.ts"],"sourcesContent":["/**\n * Auth config-as-code: validated, secret-safe engine options.\n *\n * Servers declare auth in code; secrets stay out of it via `{env:NAME}`\n * refs resolved at boot from `process.env` first, then the customer-owned\n * `resolveSecrets` hook (Vault/AWS/GCP/...). Precedence everywhere:\n * explicit literal > `{env}` ref > schema default, and code (`defaultSSO`,\n * recipes) > DB rows > plugin defaults at sign-in time.\n */\nimport { z } from \"zod\";\nimport type {\n ShelfAuthOptions,\n ShelfOAuthProvider,\n ShelfPasskeyOptions,\n ShelfSocialProvider,\n ShelfSSOGroupSync,\n ShelfSSOOidc,\n ShelfSSOProvider,\n ShelfSSOSaml,\n} from \"./engine.ts\";\n\n/** `{env:NAME}` reference (whole-string match; surrounding text is literal). */\nconst ENV_REF = /^\\{env:(?<name>[A-Za-z_][A-Za-z0-9_]*)\\}$/u;\n\n/** Extract the variable name from an `{env:NAME}` ref, or null. */\nexport function parseEnvRef(value: string): string | null {\n return ENV_REF.exec(value)?.groups?.[\"name\"] ?? null;\n}\n\n/**\n * Customer-owned secret backend (Vault/AWS/GCP/...). Called once per boot\n * with the ref names missing from `process.env`; return the values you hold.\n */\nexport interface ResolveSecrets {\n (\n names: string[],\n ): Promise<Record<string, string | undefined>> | Record<string, string | undefined>;\n}\n\n/** Input for {@link resolveAuthOptions} (refs allowed in any string field). */\nexport interface RawShelfAuthOptions extends Omit<ShelfAuthOptions, \"plugins\"> {\n /** Secret backend fallback for refs missing from `process.env`. */\n resolveSecrets?: ResolveSecrets;\n /** Custom Better Auth plugins (never validated; passed through by reference). */\n plugins?: ShelfAuthOptions[\"plugins\"];\n}\n\nconst socialSchema: z.ZodType<ShelfSocialProvider> = z.object({\n id: z.enum([\"github\", \"gitlab\", \"google\", \"microsoft\", \"cognito\"]),\n label: z.string().min(1),\n clientId: z.string().min(1),\n clientSecret: z.string().optional(),\n extra: z.record(z.string(), z.string().optional()).optional(),\n});\n\nconst oauthSchema: z.ZodType<ShelfOAuthProvider> = z.object({\n id: z.string().min(1),\n label: z.string().min(1),\n clientId: z.string().min(1),\n clientSecret: z.string().optional(),\n discoveryUrl: z.string().optional(),\n authorizationUrl: z.string().optional(),\n tokenUrl: z.string().optional(),\n userInfoUrl: z.string().optional(),\n scopes: z.array(z.string().min(1)).optional(),\n});\n\nconst ssoOidcSchema: z.ZodType<ShelfSSOOidc> = z.object({\n issuer: z.string().min(1),\n discoveryUrl: z.string().optional(),\n clientId: z.string().min(1),\n clientSecret: z.string().optional(),\n scopes: z.array(z.string().min(1)).optional(),\n pkce: z.boolean().optional(),\n});\n\nconst ssoSamlSchema: z.ZodType<ShelfSSOSaml> = z.object({\n issuer: z.string().min(1),\n entryPoint: z.string().min(1),\n cert: z.union([z.string(), z.array(z.string())]).optional(),\n metadataXml: z.string().optional(),\n entityID: z.string().optional(),\n audience: z.string().optional(),\n wantAssertionsSigned: z.boolean().optional(),\n});\n\nconst ssoGroupsSchema: z.ZodType<ShelfSSOGroupSync> = z.object({\n claim: z.union([z.string().min(1), z.array(z.string().min(1)).min(1)]).optional(),\n admins: z.array(z.string().min(1)).optional(),\n});\n\nconst ssoProviderSchema: z.ZodType<ShelfSSOProvider> = z.object({\n id: z.string().min(1),\n label: z.string().min(1),\n domain: z.string().min(1),\n oidc: ssoOidcSchema.optional(),\n saml: ssoSamlSchema.optional(),\n groups: ssoGroupsSchema.optional(),\n});\n\nconst passkeysSchema: z.ZodType<ShelfPasskeyOptions> = z.object({\n rpID: z.string().min(1).optional(),\n rpName: z.string().min(1).optional(),\n origin: z.union([z.string().min(1), z.array(z.string().min(1))]).optional(),\n});\n\n/** Zod schema validating shelf auth options (before secret resolution). */\nexport const authOptionsSchema = z\n .object({\n db: z.custom<ShelfAuthOptions[\"db\"]>((value) => typeof value === \"object\" && value !== null),\n // Any non-empty string (refs resolve later); the 32-char floor applies\n // to the resolved value (same threshold as the engine boot check).\n secret: z.string().min(1),\n // oxlint-disable-next-line typescript/no-deprecated -- z.string().url() kept for zod v3 API compat\n baseURL: z.string().url(),\n emailPassword: z.boolean().optional(),\n emailSender: z\n .custom<ShelfAuthOptions[\"emailSender\"]>(\n (value) => typeof value === \"object\" && value !== null,\n )\n .optional(),\n fromEmail: z.string().min(1).optional(),\n onAuthSystemEvent: z\n .custom<ShelfAuthOptions[\"onAuthSystemEvent\"]>((value) => typeof value === \"function\")\n .optional(),\n logger: z\n .custom<ShelfAuthOptions[\"logger\"]>((value) => typeof value === \"object\" && value !== null)\n .optional(),\n social: z.array(socialSchema).optional(),\n oauth: z.array(oauthSchema).optional(),\n sso: z\n .object({\n providers: z.array(ssoProviderSchema).min(1),\n disableImplicitSignUp: z.boolean().optional(),\n })\n .optional(),\n passkeys: passkeysSchema.optional(),\n sessionExpiresInDays: z.number().int().positive().optional(),\n })\n .strict() as z.ZodType<Omit<ShelfAuthOptions, \"plugins\">>;\n\nfunction formatIssues(error: z.ZodError): string {\n return error.issues.map((i) => `${i.path.join(\".\")}: ${i.message}`).join(\"; \");\n}\n\nfunction collectFromValue(value: unknown, into: Set<string>): void {\n if (Array.isArray(value)) {\n for (const entry of value) {\n collectEnvNames(entry, into);\n }\n return;\n }\n if (typeof value === \"object\" && value !== null) {\n for (const entry of Object.values(value)) {\n collectEnvNames(entry, into);\n }\n }\n}\n\nfunction collectEnvNames(value: unknown, into: Set<string>): void {\n if (typeof value === \"string\") {\n const name = parseEnvRef(value);\n if (name) {\n into.add(name);\n }\n return;\n }\n collectFromValue(value, into);\n}\n\nfunction substituteEnvNames(value: unknown, secrets: Map<string, string>): unknown {\n if (typeof value === \"string\") {\n const name = parseEnvRef(value);\n return name ? (secrets.get(name) ?? value) : value;\n }\n if (Array.isArray(value)) {\n return value.map((entry) => substituteEnvNames(entry, secrets));\n }\n if (typeof value === \"object\" && value !== null) {\n return Object.fromEntries(\n Object.entries(value).map(([key, entry]) => [key, substituteEnvNames(entry, secrets)]),\n );\n }\n return value;\n}\n\n/** Split ref names into env-held and hook-needed sets. */\nfunction partitionSecretNames(names: Set<string>): {\n secrets: Map<string, string>;\n missing: string[];\n} {\n const secrets = new Map<string, string>();\n const missing: string[] = [];\n for (const name of names) {\n const fromEnv = process.env[name];\n if (fromEnv) {\n secrets.set(name, fromEnv);\n } else {\n missing.push(name);\n }\n }\n return { secrets, missing };\n}\n\n/** Fill hook-provided values into the secret map. */\nasync function fillFromHook(\n secrets: Map<string, string>,\n missing: string[],\n hook: ResolveSecrets | undefined,\n): Promise<void> {\n if (missing.length === 0 || !hook) {\n return;\n }\n const provided = await hook(missing);\n for (const name of missing) {\n if (provided[name]) {\n secrets.set(name, provided[name]);\n }\n }\n}\n\n/** Resolve `{env:NAME}` refs from the environment, then the hook. */\nasync function resolveSecretValues(\n names: Set<string>,\n hook: ResolveSecrets | undefined,\n): Promise<Map<string, string>> {\n const { secrets, missing } = partitionSecretNames(names);\n await fillFromHook(secrets, missing, hook);\n const unsettled = [...names].filter((name) => !secrets.has(name));\n if (unsettled.length > 0) {\n throw new Error(\n `Shelf auth secrets missing: ${unsettled.join(\", \")} (set the env vars or provide resolveSecrets)`,\n );\n }\n return secrets;\n}\n\n/**\n * Secret-bearing subtrees: `db` (live adapter with circular drizzle handles),\n * `emailSender` (live transport), `logger` (pino instance), `onAuthSystemEvent`\n * (host callback), and `plugins` (class instances) pass through by reference\n * and are never walked or cloned — refs only make sense in plain config data.\n */\nfunction secretSubtrees(options: Omit<ShelfAuthOptions, \"plugins\">): Record<string, unknown> {\n const {\n db: _db,\n emailSender: _sender,\n logger: _logger,\n onAuthSystemEvent: _hook,\n ...rest\n } = options as Omit<ShelfAuthOptions, \"plugins\"> & {\n db: unknown;\n emailSender: unknown;\n logger: unknown;\n onAuthSystemEvent: unknown;\n };\n return rest;\n}\n\nfunction parseRawOptions(input: RawShelfAuthOptions): {\n options: Omit<ShelfAuthOptions, \"plugins\">;\n plugins: ShelfAuthOptions[\"plugins\"];\n resolveSecrets: ResolveSecrets | undefined;\n} {\n const { resolveSecrets, plugins, ...rest } = input;\n const parsed = authOptionsSchema.safeParse(rest);\n if (!parsed.success) {\n throw new Error(`Invalid shelf auth options: ${formatIssues(parsed.error)}`);\n }\n return { options: parsed.data, plugins, resolveSecrets };\n}\n\n/**\n * Validate auth options and resolve `{env:NAME}` secret refs.\n *\n * @param input - Raw options (refs allowed in any string field) plus the\n * optional customer-owned `resolveSecrets` fallback.\n * @returns Engine-ready options for {@link createShelfAuth}.\n */\n/** Collect `{env:}` refs and resolve their values (empty when none). */\nasync function collectSecrets(\n options: Omit<ShelfAuthOptions, \"plugins\">,\n hook: ResolveSecrets | undefined,\n): Promise<Record<string, unknown>> {\n const names = new Set<string>();\n collectEnvNames(secretSubtrees(options), names);\n if (names.size === 0) {\n return {};\n }\n const values = await resolveSecretValues(names, hook);\n return substituteEnvNames(secretSubtrees(options), values) as Record<string, unknown>;\n}\n\nexport async function resolveAuthOptions(input: RawShelfAuthOptions): Promise<ShelfAuthOptions> {\n const { options, plugins, resolveSecrets } = parseRawOptions(input);\n const secrets = await collectSecrets(options, resolveSecrets);\n const resolved = { ...options, ...secrets } as Omit<ShelfAuthOptions, \"plugins\">;\n if (resolved.secret.length < 32) {\n throw new Error(\"Shelf auth requires a secret of at least 32 characters\");\n }\n return { ...resolved, ...(plugins ? { plugins } : {}) };\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import __tsdown_shims_path from 'node:path';
|
|
2
|
+
import __tsdown_shims_url from 'node:url';
|
|
3
|
+
import { Table } from "drizzle-orm";
|
|
4
|
+
import { DBAdapterInstance } from "better-auth";
|
|
5
|
+
import { DatabaseAdapter } from "@storyshelf/core/adapter/database";
|
|
6
|
+
//#region src/db-bridge.d.ts
|
|
7
|
+
/** Model→table registry plus per-model date fields. */
|
|
8
|
+
interface AuthBridgeSchema {
|
|
9
|
+
/** Drizzle table handles keyed by Better Auth model name. */
|
|
10
|
+
tables: Record<string, Table>;
|
|
11
|
+
/** Date-valued fields per model (ISO at rest, Date in flight). */
|
|
12
|
+
dateFields: Record<string, readonly string[]>;
|
|
13
|
+
/** Id generator for creates missing an id (defaults to ULID). */
|
|
14
|
+
generateId?: (model: string) => string;
|
|
15
|
+
}
|
|
16
|
+
/** Build the Better Auth database adapter over a shelf database. */
|
|
17
|
+
declare function createShelfDbBridge(db: DatabaseAdapter, schema: AuthBridgeSchema): DBAdapterInstance;
|
|
18
|
+
//#endregion
|
|
19
|
+
export { AuthBridgeSchema, createShelfDbBridge };
|
|
20
|
+
//# sourceMappingURL=db-bridge.d.mts.map
|