@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.
- package/dist/cli.d.ts +46 -0
- package/dist/cli.js +268 -0
- package/dist/index.d.ts +91 -0
- package/dist/index.js +135 -0
- package/dist/memberships-Re0HbIz4.d.ts +117 -0
- package/dist/schema/pg/index.d.ts +360 -0
- package/dist/schema/pg/index.js +47 -0
- package/dist/schema/sqlite/index.d.ts +506 -0
- package/dist/schema/sqlite/index.js +60 -0
- package/package.json +35 -0
- package/src/__tests__/cli-me.test.ts +32 -0
- package/src/__tests__/cli-members.test.ts +94 -0
- package/src/__tests__/cli-tokens.test.ts +96 -0
- package/src/__tests__/helpers.ts +42 -0
- package/src/cli.ts +356 -0
- package/src/index.ts +2 -0
- package/src/repo/api-tokens.ts +91 -0
- package/src/repo/index.ts +6 -0
- package/src/repo/memberships.ts +60 -0
- package/src/repo/users.ts +64 -0
- package/src/schema/pg/api-tokens.ts +17 -0
- package/src/schema/pg/index.ts +4 -0
- package/src/schema/pg/memberships.ts +14 -0
- package/src/schema/pg/users.ts +12 -0
- package/src/schema/sqlite/api-tokens.ts +18 -0
- package/src/schema/sqlite/index.ts +4 -0
- package/src/schema/sqlite/memberships.ts +15 -0
- package/src/schema/sqlite/users.ts +16 -0
- package/src/types.ts +45 -0
|
@@ -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,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,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
|
+
}
|