@baseworks/account 0.2.0

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.
@@ -0,0 +1,6 @@
1
+ export { createUserRepo } from './users.js'
2
+ export type { UserRepo } from './users.js'
3
+ export { createOrgMembershipRepo } from './memberships.js'
4
+ export type { OrgMembershipRepo } from './memberships.js'
5
+ export { createApiTokenRepo } from './api-tokens.js'
6
+ export type { ApiTokenRepo } from './api-tokens.js'
@@ -0,0 +1,60 @@
1
+ import { and, eq } from 'drizzle-orm'
2
+ import { generateId } from '@baseworks/core'
3
+ import type { OrgMembership, OrgRole } from '../types.js'
4
+
5
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
6
+ type AnyDB = any
7
+
8
+ export function createOrgMembershipRepo(db: AnyDB, schema: { orgMemberships: any }) {
9
+ const { orgMemberships } = schema
10
+
11
+ return {
12
+ async add(userId: string, organizationId: string, role: OrgRole): Promise<OrgMembership> {
13
+ const now = Date.now()
14
+ const row: OrgMembership = { id: generateId(), userId, organizationId, role, createdAt: now, updatedAt: now }
15
+ await db.insert(orgMemberships).values(row).onConflictDoNothing()
16
+ return row
17
+ },
18
+
19
+ async updateRole(userId: string, organizationId: string, role: OrgRole): Promise<void> {
20
+ await db
21
+ .update(orgMemberships)
22
+ .set({ role, updatedAt: Date.now() })
23
+ .where(and(eq(orgMemberships.userId, userId), eq(orgMemberships.organizationId, organizationId)))
24
+ },
25
+
26
+ async remove(userId: string, organizationId: string): Promise<void> {
27
+ await db
28
+ .delete(orgMemberships)
29
+ .where(and(eq(orgMemberships.userId, userId), eq(orgMemberships.organizationId, organizationId)))
30
+ },
31
+
32
+ async find(userId: string, organizationId: string): Promise<OrgMembership | undefined> {
33
+ const rows: OrgMembership[] = await db
34
+ .select()
35
+ .from(orgMemberships)
36
+ .where(and(eq(orgMemberships.userId, userId), eq(orgMemberships.organizationId, organizationId)))
37
+ .limit(1)
38
+ return rows[0]
39
+ },
40
+
41
+ async listByUser(userId: string): Promise<OrgMembership[]> {
42
+ return db.select().from(orgMemberships).where(eq(orgMemberships.userId, userId))
43
+ },
44
+
45
+ async listByOrg(organizationId: string): Promise<OrgMembership[]> {
46
+ return db.select().from(orgMemberships).where(eq(orgMemberships.organizationId, organizationId))
47
+ },
48
+
49
+ async firstForUser(userId: string): Promise<OrgMembership | undefined> {
50
+ const rows: OrgMembership[] = await db
51
+ .select()
52
+ .from(orgMemberships)
53
+ .where(eq(orgMemberships.userId, userId))
54
+ .limit(1)
55
+ return rows[0]
56
+ },
57
+ }
58
+ }
59
+
60
+ export type OrgMembershipRepo = ReturnType<typeof createOrgMembershipRepo>
@@ -0,0 +1,64 @@
1
+ import { and, eq } from 'drizzle-orm'
2
+ import { generateId } from '@baseworks/core'
3
+ import type { User, OidcIdentity } from '../types.js'
4
+
5
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
6
+ type AnyDB = any
7
+
8
+ export function createUserRepo(db: AnyDB, schema: { users: any }) {
9
+ const { users } = schema
10
+
11
+ return {
12
+ async upsert(identity: OidcIdentity): Promise<User> {
13
+ const now = Date.now()
14
+ const existing = await db
15
+ .select()
16
+ .from(users)
17
+ .where(and(eq(users.subject, identity.subject), eq(users.issuer, identity.issuer)))
18
+ .limit(1)
19
+ .then((r: User[]) => r[0])
20
+
21
+ if (existing) {
22
+ await db
23
+ .update(users)
24
+ .set({ email: identity.email, name: identity.name ?? null, picture: identity.picture ?? null, updatedAt: now })
25
+ .where(eq(users.id, existing.id))
26
+ return { ...existing, email: identity.email, name: identity.name ?? null, picture: identity.picture ?? null, updatedAt: now }
27
+ }
28
+
29
+ const row: User = {
30
+ id: generateId(),
31
+ subject: identity.subject,
32
+ issuer: identity.issuer,
33
+ email: identity.email,
34
+ name: identity.name ?? null,
35
+ picture: identity.picture ?? null,
36
+ createdAt: now,
37
+ updatedAt: now,
38
+ }
39
+ await db.insert(users).values(row)
40
+ return row
41
+ },
42
+
43
+ async findById(id: string): Promise<User | undefined> {
44
+ const rows: User[] = await db.select().from(users).where(eq(users.id, id)).limit(1)
45
+ return rows[0]
46
+ },
47
+
48
+ async findBySubject(issuer: string, subject: string): Promise<User | undefined> {
49
+ const rows: User[] = await db
50
+ .select()
51
+ .from(users)
52
+ .where(and(eq(users.issuer, issuer), eq(users.subject, subject)))
53
+ .limit(1)
54
+ return rows[0]
55
+ },
56
+
57
+ async findByEmail(email: string): Promise<User | undefined> {
58
+ const rows: User[] = await db.select().from(users).where(eq(users.email, email)).limit(1)
59
+ return rows[0]
60
+ },
61
+ }
62
+ }
63
+
64
+ export type UserRepo = ReturnType<typeof createUserRepo>
@@ -0,0 +1,17 @@
1
+ import { bigint, pgTable, text } from 'drizzle-orm/pg-core'
2
+ import { users } from './users.js'
3
+
4
+ export const apiTokens = pgTable('api_tokens', {
5
+ id: text('id').primaryKey(),
6
+ organizationId: text('organization_id').notNull(),
7
+ userId: text('user_id').references(() => users.id, { onDelete: 'cascade' }),
8
+ name: text('name').notNull(),
9
+ tokenHash: text('token_hash').notNull().unique(),
10
+ keyPrefix: text('key_prefix').notNull(),
11
+ scopes: text('scopes').notNull().default('[]'),
12
+ lastUsedAt: bigint('last_used_at', { mode: 'number' }),
13
+ expiresAt: bigint('expires_at', { mode: 'number' }),
14
+ revokedAt: bigint('revoked_at', { mode: 'number' }),
15
+ createdAt: bigint('created_at', { mode: 'number' }).notNull(),
16
+ updatedAt: bigint('updated_at', { mode: 'number' }).notNull(),
17
+ })
@@ -0,0 +1,4 @@
1
+ export { users } from './users.js'
2
+ export { orgMemberships, OrgRoles } from './memberships.js'
3
+ export type { OrgRole } from './memberships.js'
4
+ export { apiTokens } from './api-tokens.js'
@@ -0,0 +1,14 @@
1
+ import { bigint, pgTable, text } from 'drizzle-orm/pg-core'
2
+ import { users } from './users.js'
3
+
4
+ export const OrgRoles = ['admin', 'member', 'viewer'] as const
5
+ export type OrgRole = typeof OrgRoles[number]
6
+
7
+ export const orgMemberships = pgTable('org_memberships', {
8
+ id: text('id').primaryKey(),
9
+ userId: text('user_id').notNull().references(() => users.id, { onDelete: 'cascade' }),
10
+ organizationId: text('organization_id').notNull(),
11
+ role: text('role').notNull().$type<OrgRole>(),
12
+ createdAt: bigint('created_at', { mode: 'number' }).notNull(),
13
+ updatedAt: bigint('updated_at', { mode: 'number' }).notNull(),
14
+ })
@@ -0,0 +1,12 @@
1
+ import { bigint, pgTable, text } from 'drizzle-orm/pg-core'
2
+
3
+ export const users = pgTable('users', {
4
+ id: text('id').primaryKey(),
5
+ subject: text('subject').notNull(),
6
+ issuer: text('issuer').notNull(),
7
+ email: text('email').notNull(),
8
+ name: text('name'),
9
+ picture: text('picture'),
10
+ createdAt: bigint('created_at', { mode: 'number' }).notNull(),
11
+ updatedAt: bigint('updated_at', { mode: 'number' }).notNull(),
12
+ })
@@ -0,0 +1,18 @@
1
+ import { integer, sqliteTable, text } from 'drizzle-orm/sqlite-core';
2
+ import { users } from './users';
3
+ import { organizations } from '@baseworks/organization';
4
+
5
+ export const apiTokens = sqliteTable('api_tokens', {
6
+ id: text('id').primaryKey(),
7
+ organizationId: text('organization_id').notNull().references(() => organizations.id, { onDelete: 'cascade' }),
8
+ userId: text('user_id').references(() => users.id, { onDelete: 'cascade' }), // null = org-level key
9
+ name: text('name').notNull(),
10
+ tokenHash: text('token_hash').notNull().unique(),
11
+ keyPrefix: text('key_prefix').notNull(), // e.g. "orb_", "tally_" — service identifier
12
+ scopes: text('scopes').notNull().default('[]'), // JSON string[]
13
+ lastUsedAt: integer('last_used_at'), // Unix ms
14
+ expiresAt: integer('expires_at'), // Unix ms, null = never
15
+ revokedAt: integer('revoked_at'), // Unix ms
16
+ createdAt: integer('created_at').notNull(),
17
+ updatedAt: integer('updated_at').notNull(),
18
+ });
@@ -0,0 +1,4 @@
1
+ export { users } from './users.js'
2
+ export { orgMemberships, OrgRoles } from './memberships.js'
3
+ export type { OrgRole } from './memberships.js'
4
+ export { apiTokens } from './api-tokens.js'
@@ -0,0 +1,15 @@
1
+ import { integer, sqliteTable, text } from 'drizzle-orm/sqlite-core';
2
+ import { users } from './users';
3
+ import { organizations } from '@baseworks/organization';
4
+
5
+ export const OrgRoles = ['owner', 'admin', 'member', 'viewer'] as const;
6
+ export type OrgRole = typeof OrgRoles[number];
7
+
8
+ export const orgMemberships = sqliteTable('org_memberships', {
9
+ id: text('id').primaryKey(),
10
+ userId: text('user_id').notNull().references(() => users.id, { onDelete: 'cascade' }),
11
+ organizationId: text('organization_id').notNull().references(() => organizations.id, { onDelete: 'cascade' }),
12
+ role: text('role').notNull().$type<OrgRole>(),
13
+ createdAt: integer('created_at').notNull(),
14
+ updatedAt: integer('updated_at').notNull(),
15
+ });
@@ -0,0 +1,16 @@
1
+ import { integer, sqliteTable, text } from 'drizzle-orm/sqlite-core';
2
+
3
+ // OIDC identity mirror.
4
+ // id = our internal stable key (UUIDv7).
5
+ // subject + issuer = the OIDC identity, globally unique together.
6
+ // This lets us support multiple OIDC providers without a schema migration.
7
+ export const users = sqliteTable('users', {
8
+ id: text('id').primaryKey(), // UUIDv7 — our internal key
9
+ subject: text('subject').notNull(), // OIDC `sub` claim
10
+ issuer: text('issuer').notNull(), // OIDC `iss` — e.g. https://auth.example.com
11
+ email: text('email').notNull(),
12
+ name: text('name'),
13
+ picture: text('picture'), // avatar URL from OIDC `picture` claim
14
+ createdAt: integer('created_at').notNull(), // Unix ms
15
+ updatedAt: integer('updated_at').notNull(),
16
+ });
package/src/types.ts ADDED
@@ -0,0 +1,45 @@
1
+ export type { OrgRole } from './schema/pg/index.js'
2
+
3
+ export interface User {
4
+ id: string
5
+ subject: string
6
+ issuer: string
7
+ email: string
8
+ name: string | null
9
+ picture: string | null
10
+ createdAt: number
11
+ updatedAt: number
12
+ }
13
+
14
+ export interface OrgMembership {
15
+ id: string
16
+ userId: string
17
+ organizationId: string
18
+ role: string
19
+ createdAt: number
20
+ updatedAt: number
21
+ }
22
+
23
+ export interface ApiToken {
24
+ id: string
25
+ organizationId: string
26
+ userId: string | null
27
+ name: string
28
+ tokenHash: string
29
+ keyPrefix: string
30
+ scopes: string // JSON string[]
31
+ lastUsedAt: number | null
32
+ expiresAt: number | null
33
+ revokedAt: number | null
34
+ createdAt: number
35
+ updatedAt: number
36
+ }
37
+
38
+ // Provider-agnostic identity from OIDC token verification
39
+ export interface OidcIdentity {
40
+ subject: string
41
+ issuer: string
42
+ email: string
43
+ name?: string
44
+ picture?: string
45
+ }