@kitledger/core 0.0.1 → 0.0.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/dist/schema.js ADDED
@@ -0,0 +1,159 @@
1
+ import { relations } from "drizzle-orm";
2
+ import { boolean, index, jsonb, pgTable, text, timestamp, uuid, varchar } from "drizzle-orm/pg-core";
3
+ import { timestamps } from "./db.js";
4
+ /**
5
+ * 1. Tables, Indexes, and Constraints
6
+ * This allows us to defined and view the database schema in a structured way.
7
+ */
8
+ /**
9
+ * 1.1 Users and Roles
10
+ */
11
+ export const api_tokens = pgTable("api_tokens", {
12
+ id: uuid("id").primaryKey(),
13
+ user_id: uuid("user_id")
14
+ .notNull()
15
+ .references(() => users.id),
16
+ name: varchar("name", { length: 64 }).notNull(),
17
+ revoked_at: timestamp("revoked_at"),
18
+ }, (table) => [index("api_token_user_idx").on(table.user_id)]);
19
+ export const permission_assignments = pgTable("permission_assignments", {
20
+ id: uuid("id").primaryKey(),
21
+ permission_id: uuid("permission_id")
22
+ .notNull()
23
+ .references(() => permissions.id),
24
+ user_id: uuid("user_id"),
25
+ role_id: uuid("role_id"),
26
+ ...timestamps,
27
+ }, (table) => [
28
+ index("permission_assignment_user_idx").on(table.user_id),
29
+ index("permission_assignment_role_idx").on(table.role_id),
30
+ index("permission_assignment_permission_idx").on(table.permission_id),
31
+ ]);
32
+ export const permissions = pgTable("permissions", {
33
+ id: uuid("id").primaryKey(),
34
+ name: varchar("name", { length: 64 }).notNull().unique(),
35
+ description: varchar("description", { length: 255 }),
36
+ ...timestamps,
37
+ });
38
+ export const roles = pgTable("roles", {
39
+ id: uuid("id").primaryKey(),
40
+ name: varchar("name", { length: 64 }).notNull().unique(),
41
+ description: varchar("description", { length: 255 }),
42
+ ...timestamps,
43
+ });
44
+ export const sessions = pgTable("sessions", {
45
+ id: uuid("id").primaryKey(),
46
+ user_id: uuid("user_id")
47
+ .notNull()
48
+ .references(() => users.id),
49
+ expires_at: timestamp("expires_at").notNull(),
50
+ ...timestamps,
51
+ });
52
+ export const system_permissions = pgTable("system_permissions", {
53
+ id: uuid("id").primaryKey(),
54
+ permission: varchar("permission", { length: 64 }).notNull(),
55
+ user_id: uuid("user_id")
56
+ .notNull()
57
+ .references(() => users.id),
58
+ ...timestamps,
59
+ }, (table) => [
60
+ index("system_permission_user_idx").on(table.user_id),
61
+ index("system_permission_permission_idx").on(table.permission),
62
+ ]);
63
+ export const users = pgTable("users", {
64
+ id: uuid("id").primaryKey(),
65
+ first_name: varchar("first_name", { length: 64 }).notNull(),
66
+ last_name: varchar("last_name", { length: 64 }).notNull(),
67
+ email: varchar("email", { length: 64 }).notNull().unique(),
68
+ password_hash: text("password_hash").notNull(),
69
+ ...timestamps,
70
+ }, (table) => [index("user_email_idx").on(table.email)]);
71
+ export const user_roles = pgTable("user_roles", {
72
+ id: uuid("id").primaryKey(),
73
+ user_id: uuid("user_id")
74
+ .notNull()
75
+ .references(() => users.id),
76
+ role_id: uuid("role_id")
77
+ .notNull()
78
+ .references(() => roles.id),
79
+ ...timestamps,
80
+ });
81
+ /**
82
+ * 1.5 Ledgers
83
+ */
84
+ export const accounts = pgTable("accounts", {
85
+ id: uuid("id").primaryKey(),
86
+ ref_id: varchar("ref_id", { length: 64 }).notNull().unique(),
87
+ alt_id: varchar("alt_id", { length: 64 }).unique(),
88
+ balance_type: varchar("balance_type", { length: 10 }).notNull().$type(),
89
+ ledger_id: uuid("ledger_id").notNull(),
90
+ parent_id: uuid("parent_id"),
91
+ name: varchar("name", { length: 64 }).notNull(),
92
+ meta: jsonb("meta").$type(),
93
+ active: boolean("active").default(true).notNull(),
94
+ ...timestamps,
95
+ });
96
+ export const ledgers = pgTable("ledgers", {
97
+ id: uuid("id").primaryKey(),
98
+ ref_id: varchar("ref_id", { length: 64 }).notNull().unique(),
99
+ alt_id: varchar("alt_id", { length: 64 }).unique(),
100
+ name: varchar("name", { length: 64 }).notNull(),
101
+ description: varchar("description", { length: 255 }),
102
+ active: boolean("active").default(true).notNull(),
103
+ ...timestamps,
104
+ });
105
+ /**
106
+ * 2. Relations
107
+ * These are for Drizzle query building features and have no direct effect on the database schema.
108
+ */
109
+ export const apiTokenRelations = relations(api_tokens, ({ one }) => ({
110
+ user: one(users, {
111
+ fields: [api_tokens.user_id],
112
+ references: [users.id],
113
+ }),
114
+ }));
115
+ export const permissionRelations = relations(permissions, ({ many }) => ({
116
+ permissionAssignments: many(permission_assignments),
117
+ roles: many(roles),
118
+ users: many(users),
119
+ }));
120
+ export const permissionAssignmentRelations = relations(permission_assignments, ({ one }) => ({
121
+ permission: one(permissions, {
122
+ fields: [permission_assignments.permission_id],
123
+ references: [permissions.id],
124
+ }),
125
+ user: one(users, {
126
+ fields: [permission_assignments.user_id],
127
+ references: [users.id],
128
+ }),
129
+ role: one(roles, {
130
+ fields: [permission_assignments.role_id],
131
+ references: [roles.id],
132
+ }),
133
+ }));
134
+ export const roleRelations = relations(roles, ({ many }) => ({
135
+ users: many(user_roles),
136
+ permissions: many(permission_assignments),
137
+ }));
138
+ export const systemPermissionRelations = relations(system_permissions, ({ one }) => ({
139
+ user: one(users, {
140
+ fields: [system_permissions.user_id],
141
+ references: [users.id],
142
+ }),
143
+ }));
144
+ export const userRelations = relations(users, ({ many }) => ({
145
+ roles: many(user_roles),
146
+ apiTokens: many(api_tokens),
147
+ permissions: many(permission_assignments),
148
+ system_permissions: many(system_permissions),
149
+ }));
150
+ export const userRoleRelations = relations(user_roles, ({ one }) => ({
151
+ user: one(users, {
152
+ fields: [user_roles.user_id],
153
+ references: [users.id],
154
+ }),
155
+ role: one(roles, {
156
+ fields: [user_roles.role_id],
157
+ references: [roles.id],
158
+ }),
159
+ }));
@@ -3,11 +3,31 @@ export declare enum TransactionModelStatus {
3
3
  INACTIVE = 1,
4
4
  FROZEN = 2
5
5
  }
6
+ export type Transaction<T = Record<string, any>> = {
7
+ id: string;
8
+ model_ref_id: string;
9
+ created_at: Date;
10
+ updated_at: Date;
11
+ data: T;
12
+ };
13
+ export type TransactionPipe<T> = (transaction: Transaction<T>) => Promise<Transaction<T> | void>;
14
+ export type TransactionListener<T> = (transaction: Readonly<Transaction<T>>) => Promise<void>;
15
+ export type TransactionModel<T = Record<string, any>> = {
16
+ ref_id: string;
17
+ alt_id?: string;
18
+ name: string;
19
+ status: TransactionModelStatus;
20
+ hooks?: {
21
+ creating?: TransactionPipe<T>[];
22
+ updating?: TransactionPipe<T>[];
23
+ created?: TransactionListener<T>[];
24
+ updated?: TransactionListener<T>[];
25
+ };
26
+ };
6
27
  export type TransactionModelOptions = {
7
28
  ref_id: string;
8
29
  alt_id?: string;
9
30
  name: string;
10
31
  status?: TransactionModelStatus;
11
32
  };
12
- export type TransactionModel = TransactionModelOptions;
13
- export declare function defineTransactionModel(options: TransactionModelOptions): TransactionModelOptions;
33
+ export declare function defineTransactionModel(options: TransactionModelOptions): TransactionModel;
@@ -5,5 +5,9 @@ export var TransactionModelStatus;
5
5
  TransactionModelStatus[TransactionModelStatus["FROZEN"] = 2] = "FROZEN";
6
6
  })(TransactionModelStatus || (TransactionModelStatus = {}));
7
7
  export function defineTransactionModel(options) {
8
- return options;
8
+ const transactionModel = {
9
+ ...options,
10
+ status: options.status ?? TransactionModelStatus.ACTIVE,
11
+ };
12
+ return transactionModel;
9
13
  }
package/dist/ui.d.ts CHANGED
@@ -1,7 +1,8 @@
1
1
  export type StaticUIOptions = {
2
- path: string;
3
- htmlContent: string;
4
2
  assetsPath: string;
3
+ basePath: string;
4
+ htmlContent: string;
5
+ serverPath: string;
5
6
  };
6
7
  export type StaticUIConfig = StaticUIOptions;
7
8
  export declare function defineStaticUI(options: StaticUIOptions): StaticUIConfig;
@@ -0,0 +1,16 @@
1
+ import type { AppUser, User, AuthConfigOptions } from "./auth.js";
2
+ import { type KitledgerDb } from "./db.js";
3
+ export declare function getSessionUserId(db: KitledgerDb, sessionId: string): Promise<string | null>;
4
+ export declare function getTokenUserId(db: KitledgerDb, tokenId: string): Promise<string | null>;
5
+ export declare function validateUserCredentials(db: KitledgerDb, email: string, password: string): Promise<{
6
+ id: string;
7
+ first_name: string;
8
+ last_name: string;
9
+ email: string;
10
+ } | null>;
11
+ export declare function getAuthUser(db: KitledgerDb, userId: string): Promise<AppUser | null>;
12
+ export type NewSuperUser = Pick<User, "id" | "first_name" | "last_name" | "email"> & {
13
+ password: string;
14
+ api_token: string;
15
+ };
16
+ export declare function createSuperUser(db: KitledgerDb, authConfig: AuthConfigOptions, firstName: string, lastName: string, email: string, overrideExisting?: boolean): Promise<NewSuperUser | null>;
package/dist/users.js ADDED
@@ -0,0 +1,198 @@
1
+ import { and, eq, gt, isNull, isNotNull } from "drizzle-orm";
2
+ import { randomBytes } from "node:crypto";
3
+ import { v7 } from "uuid";
4
+ import { SYSTEM_ADMIN_PERMISSION } from "./auth.js";
5
+ import { createToken } from "./auth.js";
6
+ import { verifyPassword, hashPassword } from "./crypto.js";
7
+ import { signToken, assembleApiTokenJwtPayload } from "./jwt.js";
8
+ import { api_tokens, sessions, users, system_permissions } from "./schema.js";
9
+ export async function getSessionUserId(db, sessionId) {
10
+ const session = await db.query.sessions.findFirst({
11
+ where: and(eq(sessions.id, sessionId), gt(sessions.expires_at, new Date())),
12
+ columns: {
13
+ user_id: true,
14
+ },
15
+ });
16
+ return session ? session.user_id : null;
17
+ }
18
+ export async function getTokenUserId(db, tokenId) {
19
+ const token = await db.query.api_tokens.findFirst({
20
+ where: and(eq(api_tokens.id, tokenId), isNull(api_tokens.revoked_at)),
21
+ columns: {
22
+ user_id: true,
23
+ },
24
+ });
25
+ if (token) {
26
+ return token.user_id;
27
+ }
28
+ else {
29
+ return null;
30
+ }
31
+ }
32
+ export async function validateUserCredentials(db, email, password) {
33
+ const user = await db.query.users.findFirst({
34
+ where: eq(users.email, email),
35
+ columns: {
36
+ id: true,
37
+ first_name: true,
38
+ last_name: true,
39
+ email: true,
40
+ password_hash: true,
41
+ },
42
+ });
43
+ if (!user || !user.password_hash) {
44
+ return null;
45
+ }
46
+ const validPassword = await verifyPassword(user.password_hash, password);
47
+ if (validPassword) {
48
+ return {
49
+ id: user.id,
50
+ first_name: user.first_name,
51
+ last_name: user.last_name,
52
+ email: user.email,
53
+ };
54
+ }
55
+ else {
56
+ return null;
57
+ }
58
+ }
59
+ export async function getAuthUser(db, userId) {
60
+ // 1. Fetch the user and all related data in one go.
61
+ // This requires the `userRelations` to be correctly defined (see below).
62
+ const userProfile = await db.query.users.findFirst({
63
+ where: eq(users.id, userId),
64
+ columns: {
65
+ password_hash: false, // Exclude the password hash
66
+ },
67
+ with: {
68
+ // Fetch system permissions (requires relation)
69
+ system_permissions: true,
70
+ // Fetch direct permission assignments
71
+ permissions: {
72
+ with: {
73
+ permission: true, // Include the actual permission details
74
+ },
75
+ },
76
+ // Fetch user_roles
77
+ roles: {
78
+ with: {
79
+ // For each user_role, fetch the role
80
+ role: {
81
+ with: {
82
+ // For each role, fetch its permission assignments
83
+ permissions: {
84
+ with: {
85
+ permission: true, // Include the permission details
86
+ },
87
+ },
88
+ },
89
+ },
90
+ },
91
+ },
92
+ },
93
+ });
94
+ if (!userProfile) {
95
+ return null;
96
+ }
97
+ // 2. Process the nested data to match the AppUser type
98
+ // Get the final list of Role objects
99
+ const userRoles = userProfile.roles.map((userRole) => userRole.role);
100
+ // Use a Map to deduplicate permissions
101
+ const permissionMap = new Map();
102
+ // Add permissions from roles
103
+ for (const userRole of userProfile.roles) {
104
+ for (const permAssignment of userRole.role.permissions) {
105
+ if (permAssignment.permission) {
106
+ permissionMap.set(permAssignment.permission.id, permAssignment.permission);
107
+ }
108
+ }
109
+ }
110
+ // Add/overwrite with direct permissions
111
+ for (const permAssignment of userProfile.permissions) {
112
+ if (permAssignment.permission) {
113
+ permissionMap.set(permAssignment.permission.id, permAssignment.permission);
114
+ }
115
+ }
116
+ // Convert the map back to an array
117
+ const allPermissions = Array.from(permissionMap.values());
118
+ // 3. Construct the final AppUser object
119
+ const appUser = {
120
+ ...userProfile,
121
+ roles: userRoles,
122
+ permissions: allPermissions,
123
+ system_permissions: userProfile.system_permissions,
124
+ };
125
+ return appUser;
126
+ }
127
+ export async function createSuperUser(db, authConfig, firstName, lastName, email, overrideExisting = false) {
128
+ const newSuperUser = await db.transaction(async (tx) => {
129
+ // Check if a super user exists, regardless of email.
130
+ const existingAdmin = await tx
131
+ .select()
132
+ .from(system_permissions)
133
+ .where(and(isNotNull(system_permissions.user_id), eq(system_permissions.permission, SYSTEM_ADMIN_PERMISSION)))
134
+ .limit(1);
135
+ if (existingAdmin.length > 0 && !overrideExisting) {
136
+ console.error("A super user already exists. Aborting creation.");
137
+ return null;
138
+ }
139
+ try {
140
+ /**
141
+ * generate a random password.
142
+ */
143
+ const password = randomBytes(20).toString("hex");
144
+ let passwordHash = null;
145
+ try {
146
+ passwordHash = await hashPassword(password);
147
+ }
148
+ catch (error) {
149
+ console.error("Error hashing password:", error);
150
+ passwordHash = null;
151
+ }
152
+ if (!passwordHash) {
153
+ throw new Error("Failed to hash password");
154
+ }
155
+ const userId = v7();
156
+ const newUser = await tx
157
+ .insert(users)
158
+ .values({
159
+ id: userId,
160
+ first_name: firstName,
161
+ last_name: lastName,
162
+ email: email,
163
+ password_hash: passwordHash,
164
+ created_at: new Date(),
165
+ })
166
+ .returning();
167
+ await tx.insert(system_permissions).values({
168
+ id: v7(),
169
+ user_id: newUser[0].id,
170
+ permission: SYSTEM_ADMIN_PERMISSION,
171
+ });
172
+ return {
173
+ id: newUser[0].id,
174
+ first_name: firstName,
175
+ last_name: lastName,
176
+ email: email,
177
+ password: password,
178
+ api_token: "",
179
+ };
180
+ }
181
+ catch (error) {
182
+ console.error("Error creating super user:", error);
183
+ tx.rollback();
184
+ return null;
185
+ }
186
+ });
187
+ // Create API token for the new super user, using the encapsulated function.
188
+ if (!newSuperUser) {
189
+ return null;
190
+ }
191
+ const tokenName = `${firstName} ${lastName} Super User Token`.slice(0, 64);
192
+ const apiToken = await createToken(db, newSuperUser.id, tokenName);
193
+ if (!apiToken) {
194
+ console.error("Failed to create API token for super user");
195
+ }
196
+ newSuperUser.api_token = await signToken(authConfig, assembleApiTokenJwtPayload(apiToken));
197
+ return newSuperUser;
198
+ }
@@ -0,0 +1,22 @@
1
+ import { type BaseIssue } from "valibot";
2
+ export type ValidationError = {
3
+ type: "structure" | "data";
4
+ path: string | null;
5
+ message: string;
6
+ };
7
+ export declare function parseValibotIssues<T>(issues: BaseIssue<T>[]): ValidationError[];
8
+ export type ValidationSuccess<T> = {
9
+ success: true;
10
+ data: T;
11
+ };
12
+ export type ValidationResult<T> = {
13
+ success: boolean;
14
+ data?: T;
15
+ errors?: ValidationError[];
16
+ };
17
+ export type ValidationFailure<T> = {
18
+ success: false;
19
+ data?: T;
20
+ errors?: ValidationError[];
21
+ };
22
+ export declare function isValidationFailure<T extends object, U>(result: T | ValidationResult<U>): result is ValidationResult<U>;
@@ -0,0 +1,10 @@
1
+ export function parseValibotIssues(issues) {
2
+ return issues.map((issue) => ({
3
+ type: "structure",
4
+ path: issue.path ? issue.path.map((p) => p.key).join(".") : "",
5
+ message: issue.message,
6
+ }));
7
+ }
8
+ export function isValidationFailure(result) {
9
+ return "errors" in result;
10
+ }
package/package.json CHANGED
@@ -1,15 +1,9 @@
1
1
  {
2
2
  "name": "@kitledger/core",
3
+ "version": "0.0.3",
4
+ "private": false,
3
5
  "license": "Apache-2.0",
4
- "version": "0.0.1",
5
6
  "type": "module",
6
- "files": [
7
- "dist"
8
- ],
9
- "private": false,
10
- "publishConfig": {
11
- "access": "public"
12
- },
13
7
  "exports": {
14
8
  ".": {
15
9
  "types": "./dist/index.d.ts",
@@ -28,9 +22,29 @@
28
22
  "default": "./dist/ui.js"
29
23
  }
30
24
  },
25
+ "files": [
26
+ "dist"
27
+ ],
28
+ "dependencies": {
29
+ "@node-rs/argon2": "^2.0.2",
30
+ "drizzle-orm": "^0.45.1",
31
+ "jose": "^6.1.3",
32
+ "knex": "^3.1.0",
33
+ "pg": "^8.16.3",
34
+ "postgres": "^3.4.7",
35
+ "uuid": "^13.0.0",
36
+ "@kitledger/query": "0.0.15"
37
+ },
38
+ "devDependencies": {
39
+ "@faker-js/faker": "^10.1.0",
40
+ "drizzle-kit": "^0.31.8"
41
+ },
31
42
  "peerDependencies": {
32
43
  "valibot": "^1.1.0"
33
44
  },
45
+ "publishConfig": {
46
+ "access": "public"
47
+ },
34
48
  "scripts": {
35
49
  "build": "tsc",
36
50
  "typecheck": "tsc --noEmit"