@kognitivedev/workspace-auth 0.2.29

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,80 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.productSetupStates = exports.managedProjectApiKeys = exports.apiKeys = exports.projects = exports.organizations = void 0;
4
+ const drizzle_orm_1 = require("drizzle-orm");
5
+ const pg_core_1 = require("drizzle-orm/pg-core");
6
+ exports.organizations = (0, pg_core_1.pgTable)("organizations", {
7
+ id: (0, pg_core_1.uuid)("id").defaultRandom().primaryKey(),
8
+ clerkOrgId: (0, pg_core_1.text)("clerk_org_id").notNull(),
9
+ name: (0, pg_core_1.text)("name").notNull(),
10
+ slug: (0, pg_core_1.text)("slug").notNull(),
11
+ createdAt: (0, pg_core_1.timestamp)("created_at").defaultNow().notNull(),
12
+ updatedAt: (0, pg_core_1.timestamp)("updated_at").defaultNow().notNull(),
13
+ }, (table) => ({
14
+ clerkOrgIdIdx: (0, pg_core_1.uniqueIndex)("organizations_clerk_org_id_idx").on(table.clerkOrgId),
15
+ slugIdx: (0, pg_core_1.uniqueIndex)("organizations_slug_idx").on(table.slug),
16
+ }));
17
+ exports.projects = (0, pg_core_1.pgTable)("projects", {
18
+ id: (0, pg_core_1.uuid)("id").defaultRandom().primaryKey(),
19
+ organizationId: (0, pg_core_1.uuid)("organization_id").notNull().references(() => exports.organizations.id),
20
+ name: (0, pg_core_1.text)("name").notNull(),
21
+ slug: (0, pg_core_1.text)("slug").notNull(),
22
+ description: (0, pg_core_1.text)("description"),
23
+ isActive: (0, pg_core_1.boolean)("is_active").default(true).notNull(),
24
+ createdAt: (0, pg_core_1.timestamp)("created_at").defaultNow().notNull(),
25
+ updatedAt: (0, pg_core_1.timestamp)("updated_at").defaultNow().notNull(),
26
+ }, (table) => ({
27
+ orgSlugIdx: (0, pg_core_1.uniqueIndex)("projects_org_slug_idx").on(table.organizationId, table.slug),
28
+ orgIdx: (0, pg_core_1.index)("projects_org_idx").on(table.organizationId),
29
+ }));
30
+ exports.apiKeys = (0, pg_core_1.pgTable)("api_keys", {
31
+ id: (0, pg_core_1.uuid)("id").defaultRandom().primaryKey(),
32
+ projectId: (0, pg_core_1.uuid)("project_id").notNull().references(() => exports.projects.id),
33
+ keyHash: (0, pg_core_1.text)("key_hash").notNull(),
34
+ keyPrefix: (0, pg_core_1.text)("key_prefix").notNull(),
35
+ name: (0, pg_core_1.text)("name"),
36
+ lastUsedAt: (0, pg_core_1.timestamp)("last_used_at"),
37
+ expiresAt: (0, pg_core_1.timestamp)("expires_at"),
38
+ isActive: (0, pg_core_1.boolean)("is_active").default(true).notNull(),
39
+ createdAt: (0, pg_core_1.timestamp)("created_at").defaultNow().notNull(),
40
+ }, (table) => ({
41
+ keyHashIdx: (0, pg_core_1.uniqueIndex)("api_keys_key_hash_idx").on(table.keyHash),
42
+ projectIdx: (0, pg_core_1.index)("api_keys_project_idx").on(table.projectId),
43
+ }));
44
+ exports.managedProjectApiKeys = (0, pg_core_1.pgTable)("managed_project_api_keys", {
45
+ id: (0, pg_core_1.uuid)("id").defaultRandom().primaryKey(),
46
+ projectId: (0, pg_core_1.uuid)("project_id").notNull().references(() => exports.projects.id),
47
+ apiKeyId: (0, pg_core_1.uuid)("api_key_id").notNull().references(() => exports.apiKeys.id),
48
+ product: (0, pg_core_1.text)("product").notNull(),
49
+ encryptedKey: (0, pg_core_1.text)("encrypted_key").notNull(),
50
+ iv: (0, pg_core_1.text)("iv").notNull(),
51
+ authTag: (0, pg_core_1.text)("auth_tag").notNull(),
52
+ keyPrefix: (0, pg_core_1.text)("key_prefix").notNull(),
53
+ createdAt: (0, pg_core_1.timestamp)("created_at").defaultNow().notNull(),
54
+ updatedAt: (0, pg_core_1.timestamp)("updated_at").defaultNow().notNull(),
55
+ lastUsedAt: (0, pg_core_1.timestamp)("last_used_at"),
56
+ revokedAt: (0, pg_core_1.timestamp)("revoked_at"),
57
+ }, (table) => ({
58
+ projectProductIdx: (0, pg_core_1.index)("managed_project_api_keys_project_product_idx").on(table.projectId, table.product),
59
+ apiKeyIdx: (0, pg_core_1.uniqueIndex)("managed_project_api_keys_api_key_idx").on(table.apiKeyId),
60
+ activeProjectProductIdx: (0, pg_core_1.uniqueIndex)("managed_project_api_keys_active_project_product_idx")
61
+ .on(table.projectId, table.product)
62
+ .where((0, drizzle_orm_1.sql) `${table.revokedAt} IS NULL`),
63
+ }));
64
+ exports.productSetupStates = (0, pg_core_1.pgTable)("product_setup_states", {
65
+ id: (0, pg_core_1.uuid)("id").defaultRandom().primaryKey(),
66
+ organizationId: (0, pg_core_1.uuid)("organization_id").notNull().references(() => exports.organizations.id, { onDelete: "cascade" }),
67
+ product: (0, pg_core_1.text)("product").notNull(),
68
+ state: (0, pg_core_1.text)("state").default("not_started").notNull(),
69
+ projectId: (0, pg_core_1.uuid)("project_id").references(() => exports.projects.id, { onDelete: "set null" }),
70
+ setupSessionId: (0, pg_core_1.text)("setup_session_id").notNull(),
71
+ metadata: (0, pg_core_1.jsonb)("metadata").default({}).notNull(),
72
+ startedAt: (0, pg_core_1.timestamp)("started_at"),
73
+ completedAt: (0, pg_core_1.timestamp)("completed_at"),
74
+ createdAt: (0, pg_core_1.timestamp)("created_at").defaultNow().notNull(),
75
+ updatedAt: (0, pg_core_1.timestamp)("updated_at").defaultNow().notNull(),
76
+ }, (table) => ({
77
+ orgProductIdx: (0, pg_core_1.uniqueIndex)("product_setup_states_org_product_idx").on(table.organizationId, table.product),
78
+ projectIdx: (0, pg_core_1.index)("product_setup_states_project_idx").on(table.projectId),
79
+ sessionIdx: (0, pg_core_1.uniqueIndex)("product_setup_states_setup_session_idx").on(table.setupSessionId),
80
+ }));
@@ -0,0 +1,197 @@
1
+ import { type WorkspaceAuthDb } from "./db";
2
+ export declare const ACTIVE_PROJECT_ID_COOKIE = "active_project_id";
3
+ export declare const ACTIVE_PROJECT_SLUG_COOKIE = "active_project_slug";
4
+ export declare const KOGNITIV_VOICE_PRODUCT = "kognitiv-voice";
5
+ export declare const KOGNITIV_VOICE_MANAGED_KEY_NAME = "Managed: Kognitiv Voice";
6
+ export declare const KOGNITIV_APPOINTMENTS_PRODUCT = "kognitiv-appointments";
7
+ export declare const KOGNITIV_APPOINTMENTS_MANAGED_KEY_NAME = "Managed: Kognitiv Appointments";
8
+ type Db = WorkspaceAuthDb;
9
+ export type ProductSetupStateValue = "not_started" | "in_progress" | "completed";
10
+ export declare function slugifyWorkspaceValue(value: string, fallback?: string): string;
11
+ export declare function ensureOrganizationForClerk(input: {
12
+ clerkOrgId: string;
13
+ name?: string | null;
14
+ slug?: string | null;
15
+ db?: Db;
16
+ }): Promise<{
17
+ id: string;
18
+ name: string;
19
+ clerkOrgId: string;
20
+ slug: string;
21
+ createdAt: Date;
22
+ updatedAt: Date;
23
+ }>;
24
+ export declare function listOrganizationProjects(organizationId: string, db?: Db): Promise<{
25
+ id: string;
26
+ name: string;
27
+ slug: string;
28
+ createdAt: Date;
29
+ updatedAt: Date;
30
+ organizationId: string;
31
+ description: string | null;
32
+ isActive: boolean;
33
+ }[]>;
34
+ export declare function getOwnedProject(input: {
35
+ organizationId: string;
36
+ projectId: string;
37
+ db?: Db;
38
+ }): Promise<{
39
+ id: string;
40
+ name: string;
41
+ slug: string;
42
+ createdAt: Date;
43
+ updatedAt: Date;
44
+ organizationId: string;
45
+ description: string | null;
46
+ isActive: boolean;
47
+ }>;
48
+ export declare function resolveActiveProject(input: {
49
+ organizationId: string;
50
+ activeProjectId?: string | null;
51
+ db?: Db;
52
+ }): Promise<{
53
+ project: null;
54
+ projects: {
55
+ id: string;
56
+ name: string;
57
+ slug: string;
58
+ createdAt: Date;
59
+ updatedAt: Date;
60
+ organizationId: string;
61
+ description: string | null;
62
+ isActive: boolean;
63
+ }[];
64
+ } | {
65
+ project: {
66
+ id: string;
67
+ name: string;
68
+ slug: string;
69
+ createdAt: Date;
70
+ updatedAt: Date;
71
+ organizationId: string;
72
+ description: string | null;
73
+ isActive: boolean;
74
+ };
75
+ projects: {
76
+ id: string;
77
+ name: string;
78
+ slug: string;
79
+ createdAt: Date;
80
+ updatedAt: Date;
81
+ organizationId: string;
82
+ description: string | null;
83
+ isActive: boolean;
84
+ }[];
85
+ }>;
86
+ export declare function getProductSetupState(input: {
87
+ organizationId: string;
88
+ product: string;
89
+ db?: Db;
90
+ }): Promise<{
91
+ state: ProductSetupStateValue;
92
+ metadata: Record<string, unknown>;
93
+ id: string;
94
+ createdAt: Date;
95
+ updatedAt: Date;
96
+ organizationId: string;
97
+ projectId: string | null;
98
+ product: string;
99
+ setupSessionId: string;
100
+ startedAt: Date | null;
101
+ completedAt: Date | null;
102
+ }>;
103
+ export declare function updateProductSetupState(input: {
104
+ organizationId: string;
105
+ product: string;
106
+ state?: ProductSetupStateValue | string | null;
107
+ projectId?: string | null;
108
+ metadata?: Record<string, unknown> | null;
109
+ db?: Db;
110
+ }): Promise<{
111
+ state: ProductSetupStateValue;
112
+ metadata: Record<string, unknown>;
113
+ id: string;
114
+ createdAt: Date;
115
+ updatedAt: Date;
116
+ organizationId: string;
117
+ projectId: string | null;
118
+ product: string;
119
+ setupSessionId: string;
120
+ startedAt: Date | null;
121
+ completedAt: Date | null;
122
+ }>;
123
+ export declare function resolveProductSetupWorkspace(input: {
124
+ organizationId: string;
125
+ product: string;
126
+ db?: Db;
127
+ }): Promise<{
128
+ setup: {
129
+ state: ProductSetupStateValue;
130
+ metadata: Record<string, unknown>;
131
+ id: string;
132
+ createdAt: Date;
133
+ updatedAt: Date;
134
+ organizationId: string;
135
+ projectId: string | null;
136
+ product: string;
137
+ setupSessionId: string;
138
+ startedAt: Date | null;
139
+ completedAt: Date | null;
140
+ };
141
+ project: {
142
+ id: string;
143
+ name: string;
144
+ slug: string;
145
+ createdAt: Date;
146
+ updatedAt: Date;
147
+ organizationId: string;
148
+ description: string | null;
149
+ isActive: boolean;
150
+ } | null;
151
+ projects: {
152
+ id: string;
153
+ name: string;
154
+ slug: string;
155
+ createdAt: Date;
156
+ updatedAt: Date;
157
+ organizationId: string;
158
+ description: string | null;
159
+ isActive: boolean;
160
+ }[];
161
+ }>;
162
+ export declare function createWorkspaceProject(input: {
163
+ organizationId: string;
164
+ name: string;
165
+ slug?: string | null;
166
+ description?: string | null;
167
+ db?: Db;
168
+ }): Promise<{
169
+ id: string;
170
+ name: string;
171
+ slug: string;
172
+ createdAt: Date;
173
+ updatedAt: Date;
174
+ organizationId: string;
175
+ description: string | null;
176
+ isActive: boolean;
177
+ }>;
178
+ export declare function hasManagedProjectApiKey(input: {
179
+ projectId: string;
180
+ product: string;
181
+ db?: Db;
182
+ }): Promise<boolean>;
183
+ export declare function ensureManagedProjectApiKey(input: {
184
+ projectId: string;
185
+ product: string;
186
+ name?: string | null;
187
+ db?: Db;
188
+ }): Promise<{
189
+ key: string;
190
+ keyPrefix: string;
191
+ action: "used";
192
+ } | {
193
+ key: string;
194
+ keyPrefix: string;
195
+ action: "created";
196
+ }>;
197
+ export {};
@@ -0,0 +1,300 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.KOGNITIV_APPOINTMENTS_MANAGED_KEY_NAME = exports.KOGNITIV_APPOINTMENTS_PRODUCT = exports.KOGNITIV_VOICE_MANAGED_KEY_NAME = exports.KOGNITIV_VOICE_PRODUCT = exports.ACTIVE_PROJECT_SLUG_COOKIE = exports.ACTIVE_PROJECT_ID_COOKIE = void 0;
4
+ exports.slugifyWorkspaceValue = slugifyWorkspaceValue;
5
+ exports.ensureOrganizationForClerk = ensureOrganizationForClerk;
6
+ exports.listOrganizationProjects = listOrganizationProjects;
7
+ exports.getOwnedProject = getOwnedProject;
8
+ exports.resolveActiveProject = resolveActiveProject;
9
+ exports.getProductSetupState = getProductSetupState;
10
+ exports.updateProductSetupState = updateProductSetupState;
11
+ exports.resolveProductSetupWorkspace = resolveProductSetupWorkspace;
12
+ exports.createWorkspaceProject = createWorkspaceProject;
13
+ exports.hasManagedProjectApiKey = hasManagedProjectApiKey;
14
+ exports.ensureManagedProjectApiKey = ensureManagedProjectApiKey;
15
+ const node_crypto_1 = require("node:crypto");
16
+ const drizzle_orm_1 = require("drizzle-orm");
17
+ const crypto_1 = require("./crypto");
18
+ const db_1 = require("./db");
19
+ const schema_1 = require("./schema");
20
+ exports.ACTIVE_PROJECT_ID_COOKIE = "active_project_id";
21
+ exports.ACTIVE_PROJECT_SLUG_COOKIE = "active_project_slug";
22
+ exports.KOGNITIV_VOICE_PRODUCT = "kognitiv-voice";
23
+ exports.KOGNITIV_VOICE_MANAGED_KEY_NAME = "Managed: Kognitiv Voice";
24
+ exports.KOGNITIV_APPOINTMENTS_PRODUCT = "kognitiv-appointments";
25
+ exports.KOGNITIV_APPOINTMENTS_MANAGED_KEY_NAME = "Managed: Kognitiv Appointments";
26
+ const productSetupStateValues = new Set(["not_started", "in_progress", "completed"]);
27
+ function dbOrDefault(db) {
28
+ return db !== null && db !== void 0 ? db : (0, db_1.getWorkspaceAuthDb)();
29
+ }
30
+ function isUniqueConstraintError(error, constraintName) {
31
+ var _a;
32
+ const candidate = error;
33
+ return Boolean(candidate
34
+ && candidate.code === "23505"
35
+ && (candidate.constraint === constraintName
36
+ || candidate.constraint_name === constraintName
37
+ || ((_a = candidate.message) === null || _a === void 0 ? void 0 : _a.includes(constraintName))));
38
+ }
39
+ function isManagedKeyUsable(row, now = new Date()) {
40
+ return Boolean((row === null || row === void 0 ? void 0 : row.isActive) && (!row.expiresAt || row.expiresAt > now));
41
+ }
42
+ function normalizeProductSetupState(value, fallback = "not_started") {
43
+ return typeof value === "string" && productSetupStateValues.has(value)
44
+ ? value
45
+ : fallback;
46
+ }
47
+ function setupSessionId(product, organizationId) {
48
+ return `${slugifyWorkspaceValue(product, "product")}-setup-${organizationId}-${(0, node_crypto_1.randomUUID)()}`;
49
+ }
50
+ function setupMetadata(value) {
51
+ return value && typeof value === "object" && !Array.isArray(value) ? value : {};
52
+ }
53
+ function toProductSetupState(row) {
54
+ return Object.assign(Object.assign({}, row), { state: normalizeProductSetupState(row.state), metadata: setupMetadata(row.metadata) });
55
+ }
56
+ async function findUnrevokedManagedProjectApiKey(db, input) {
57
+ return db
58
+ .select({
59
+ managedId: schema_1.managedProjectApiKeys.id,
60
+ encryptedKey: schema_1.managedProjectApiKeys.encryptedKey,
61
+ iv: schema_1.managedProjectApiKeys.iv,
62
+ authTag: schema_1.managedProjectApiKeys.authTag,
63
+ apiKeyId: schema_1.apiKeys.id,
64
+ isActive: schema_1.apiKeys.isActive,
65
+ expiresAt: schema_1.apiKeys.expiresAt,
66
+ keyPrefix: schema_1.managedProjectApiKeys.keyPrefix,
67
+ })
68
+ .from(schema_1.managedProjectApiKeys)
69
+ .innerJoin(schema_1.apiKeys, (0, drizzle_orm_1.eq)(schema_1.managedProjectApiKeys.apiKeyId, schema_1.apiKeys.id))
70
+ .where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_1.managedProjectApiKeys.projectId, input.projectId), (0, drizzle_orm_1.eq)(schema_1.managedProjectApiKeys.product, input.product), (0, drizzle_orm_1.isNull)(schema_1.managedProjectApiKeys.revokedAt)))
71
+ .orderBy((0, drizzle_orm_1.asc)(schema_1.managedProjectApiKeys.createdAt))
72
+ .limit(1)
73
+ .then((rows) => { var _a; return (_a = rows[0]) !== null && _a !== void 0 ? _a : null; });
74
+ }
75
+ async function useExistingManagedProjectApiKey(db, existing) {
76
+ const rawKey = (0, crypto_1.decryptManagedSecret)(existing);
77
+ const now = new Date();
78
+ await Promise.all([
79
+ db.update(schema_1.apiKeys).set({ lastUsedAt: now }).where((0, drizzle_orm_1.eq)(schema_1.apiKeys.id, existing.apiKeyId)),
80
+ db.update(schema_1.managedProjectApiKeys).set({ lastUsedAt: now, updatedAt: now }).where((0, drizzle_orm_1.eq)(schema_1.managedProjectApiKeys.id, existing.managedId)),
81
+ ]);
82
+ return { key: rawKey, keyPrefix: existing.keyPrefix, action: "used" };
83
+ }
84
+ async function revokeManagedProjectApiKey(db, managedId) {
85
+ const now = new Date();
86
+ await db
87
+ .update(schema_1.managedProjectApiKeys)
88
+ .set({ revokedAt: now, updatedAt: now })
89
+ .where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_1.managedProjectApiKeys.id, managedId), (0, drizzle_orm_1.isNull)(schema_1.managedProjectApiKeys.revokedAt)));
90
+ }
91
+ function slugifyWorkspaceValue(value, fallback = "project") {
92
+ const slug = String(value || "")
93
+ .trim()
94
+ .toLowerCase()
95
+ .replace(/[^a-z0-9]+/g, "-")
96
+ .replace(/^-+|-+$/g, "");
97
+ return slug || fallback;
98
+ }
99
+ async function ensureOrganizationForClerk(input) {
100
+ var _a;
101
+ const db = dbOrDefault(input.db);
102
+ const existing = await db
103
+ .select()
104
+ .from(schema_1.organizations)
105
+ .where((0, drizzle_orm_1.eq)(schema_1.organizations.clerkOrgId, input.clerkOrgId))
106
+ .limit(1)
107
+ .then((rows) => { var _a; return (_a = rows[0]) !== null && _a !== void 0 ? _a : null; });
108
+ if (existing)
109
+ return existing;
110
+ const name = ((_a = input.name) === null || _a === void 0 ? void 0 : _a.trim()) || input.clerkOrgId;
111
+ const slug = slugifyWorkspaceValue(input.slug || name || input.clerkOrgId, input.clerkOrgId.toLowerCase());
112
+ const inserted = await db
113
+ .insert(schema_1.organizations)
114
+ .values({ clerkOrgId: input.clerkOrgId, name, slug })
115
+ .returning();
116
+ return inserted[0];
117
+ }
118
+ async function listOrganizationProjects(organizationId, db) {
119
+ return dbOrDefault(db)
120
+ .select()
121
+ .from(schema_1.projects)
122
+ .where((0, drizzle_orm_1.eq)(schema_1.projects.organizationId, organizationId))
123
+ .orderBy((0, drizzle_orm_1.asc)(schema_1.projects.createdAt), (0, drizzle_orm_1.asc)(schema_1.projects.name));
124
+ }
125
+ async function getOwnedProject(input) {
126
+ return dbOrDefault(input.db)
127
+ .select()
128
+ .from(schema_1.projects)
129
+ .where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_1.projects.id, input.projectId), (0, drizzle_orm_1.eq)(schema_1.projects.organizationId, input.organizationId)))
130
+ .limit(1)
131
+ .then((rows) => { var _a; return (_a = rows[0]) !== null && _a !== void 0 ? _a : null; });
132
+ }
133
+ async function resolveActiveProject(input) {
134
+ var _a;
135
+ const orgProjects = await listOrganizationProjects(input.organizationId, input.db);
136
+ if (orgProjects.length === 0)
137
+ return { project: null, projects: orgProjects };
138
+ const activeProject = input.activeProjectId
139
+ ? (_a = orgProjects.find((project) => project.id === input.activeProjectId)) !== null && _a !== void 0 ? _a : null
140
+ : null;
141
+ return { project: activeProject !== null && activeProject !== void 0 ? activeProject : orgProjects[0], projects: orgProjects };
142
+ }
143
+ async function getProductSetupState(input) {
144
+ const db = dbOrDefault(input.db);
145
+ const product = input.product.trim();
146
+ if (!product)
147
+ throw new Error("Product is required");
148
+ const existing = await db
149
+ .select()
150
+ .from(schema_1.productSetupStates)
151
+ .where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_1.productSetupStates.organizationId, input.organizationId), (0, drizzle_orm_1.eq)(schema_1.productSetupStates.product, product)))
152
+ .limit(1)
153
+ .then((rows) => { var _a; return (_a = rows[0]) !== null && _a !== void 0 ? _a : null; });
154
+ if (existing)
155
+ return toProductSetupState(existing);
156
+ try {
157
+ const inserted = await db
158
+ .insert(schema_1.productSetupStates)
159
+ .values({
160
+ organizationId: input.organizationId,
161
+ product,
162
+ state: "not_started",
163
+ setupSessionId: setupSessionId(product, input.organizationId),
164
+ })
165
+ .returning();
166
+ return toProductSetupState(inserted[0]);
167
+ }
168
+ catch (error) {
169
+ if (!isUniqueConstraintError(error, "product_setup_states_org_product_idx"))
170
+ throw error;
171
+ const row = await db
172
+ .select()
173
+ .from(schema_1.productSetupStates)
174
+ .where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_1.productSetupStates.organizationId, input.organizationId), (0, drizzle_orm_1.eq)(schema_1.productSetupStates.product, product)))
175
+ .limit(1)
176
+ .then((rows) => { var _a; return (_a = rows[0]) !== null && _a !== void 0 ? _a : null; });
177
+ if (!row)
178
+ throw error;
179
+ return toProductSetupState(row);
180
+ }
181
+ }
182
+ async function updateProductSetupState(input) {
183
+ const db = dbOrDefault(input.db);
184
+ const existing = await getProductSetupState(input);
185
+ const nextState = input.state !== undefined && input.state !== null
186
+ ? normalizeProductSetupState(input.state, existing.state)
187
+ : input.projectId !== undefined && existing.state === "not_started"
188
+ ? "in_progress"
189
+ : existing.state;
190
+ const now = new Date();
191
+ const nextMetadata = input.metadata
192
+ ? Object.assign(Object.assign({}, existing.metadata), input.metadata) : existing.metadata;
193
+ const updates = Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({ state: nextState }, (input.projectId !== undefined ? { projectId: input.projectId } : {})), { metadata: nextMetadata }), (nextState !== "not_started" && !existing.startedAt ? { startedAt: now } : {})), (nextState === "completed" && !existing.completedAt ? { completedAt: now } : {})), { updatedAt: now });
194
+ const updated = await db
195
+ .update(schema_1.productSetupStates)
196
+ .set(updates)
197
+ .where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_1.productSetupStates.organizationId, input.organizationId), (0, drizzle_orm_1.eq)(schema_1.productSetupStates.product, input.product)))
198
+ .returning();
199
+ return toProductSetupState(updated[0]);
200
+ }
201
+ async function resolveProductSetupWorkspace(input) {
202
+ var _a;
203
+ const db = dbOrDefault(input.db);
204
+ const [setup, orgProjects] = await Promise.all([
205
+ getProductSetupState({ organizationId: input.organizationId, product: input.product, db }),
206
+ listOrganizationProjects(input.organizationId, db),
207
+ ]);
208
+ const project = setup.projectId
209
+ ? (_a = orgProjects.find((candidate) => candidate.id === setup.projectId)) !== null && _a !== void 0 ? _a : null
210
+ : null;
211
+ return { setup, project, projects: orgProjects };
212
+ }
213
+ async function createWorkspaceProject(input) {
214
+ var _a;
215
+ const name = input.name.trim();
216
+ if (!name)
217
+ throw new Error("Project name is required");
218
+ const slug = slugifyWorkspaceValue(input.slug || name);
219
+ const inserted = await dbOrDefault(input.db)
220
+ .insert(schema_1.projects)
221
+ .values({
222
+ organizationId: input.organizationId,
223
+ name,
224
+ slug,
225
+ description: ((_a = input.description) === null || _a === void 0 ? void 0 : _a.trim()) || null,
226
+ })
227
+ .returning();
228
+ return inserted[0];
229
+ }
230
+ async function hasManagedProjectApiKey(input) {
231
+ const row = await dbOrDefault(input.db)
232
+ .select({ id: schema_1.managedProjectApiKeys.id })
233
+ .from(schema_1.managedProjectApiKeys)
234
+ .innerJoin(schema_1.apiKeys, (0, drizzle_orm_1.eq)(schema_1.managedProjectApiKeys.apiKeyId, schema_1.apiKeys.id))
235
+ .where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_1.managedProjectApiKeys.projectId, input.projectId), (0, drizzle_orm_1.eq)(schema_1.managedProjectApiKeys.product, input.product), (0, drizzle_orm_1.isNull)(schema_1.managedProjectApiKeys.revokedAt), (0, drizzle_orm_1.eq)(schema_1.apiKeys.isActive, true)))
236
+ .limit(1)
237
+ .then((rows) => { var _a; return (_a = rows[0]) !== null && _a !== void 0 ? _a : null; });
238
+ return Boolean(row);
239
+ }
240
+ async function ensureManagedProjectApiKey(input) {
241
+ const db = dbOrDefault(input.db);
242
+ for (let attempt = 0; attempt < 3; attempt += 1) {
243
+ const existing = await findUnrevokedManagedProjectApiKey(db, input);
244
+ if (existing && isManagedKeyUsable(existing)) {
245
+ return useExistingManagedProjectApiKey(db, existing);
246
+ }
247
+ if (existing) {
248
+ await revokeManagedProjectApiKey(db, existing.managedId);
249
+ continue;
250
+ }
251
+ const rawKey = (0, crypto_1.createRawApiKey)();
252
+ const keyPrefix = (0, crypto_1.getApiKeyPrefix)(rawKey);
253
+ const encrypted = (0, crypto_1.encryptManagedSecret)(rawKey);
254
+ try {
255
+ const inserted = await db.transaction(async (tx) => {
256
+ var _a;
257
+ const [apiKey] = await tx
258
+ .insert(schema_1.apiKeys)
259
+ .values({
260
+ projectId: input.projectId,
261
+ keyHash: (0, crypto_1.hashApiKey)(rawKey),
262
+ keyPrefix,
263
+ name: ((_a = input.name) === null || _a === void 0 ? void 0 : _a.trim()) || null,
264
+ })
265
+ .returning();
266
+ const [managed] = await tx
267
+ .insert(schema_1.managedProjectApiKeys)
268
+ .values({
269
+ projectId: input.projectId,
270
+ apiKeyId: apiKey.id,
271
+ product: input.product,
272
+ encryptedKey: encrypted.encryptedKey,
273
+ iv: encrypted.iv,
274
+ authTag: encrypted.authTag,
275
+ keyPrefix,
276
+ })
277
+ .returning();
278
+ return { apiKey, managed };
279
+ });
280
+ return { key: rawKey, keyPrefix: inserted.managed.keyPrefix, action: "created" };
281
+ }
282
+ catch (error) {
283
+ if (!isUniqueConstraintError(error, "managed_project_api_keys_active_project_product_idx")) {
284
+ throw error;
285
+ }
286
+ const raced = await findUnrevokedManagedProjectApiKey(db, input);
287
+ if (raced && isManagedKeyUsable(raced)) {
288
+ return useExistingManagedProjectApiKey(db, raced);
289
+ }
290
+ if (raced) {
291
+ await revokeManagedProjectApiKey(db, raced.managedId);
292
+ }
293
+ }
294
+ }
295
+ const existing = await findUnrevokedManagedProjectApiKey(db, input);
296
+ if (existing && isManagedKeyUsable(existing)) {
297
+ return useExistingManagedProjectApiKey(db, existing);
298
+ }
299
+ throw new Error("Unable to ensure managed project API key");
300
+ }
package/package.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "@kognitivedev/workspace-auth",
3
+ "version": "0.2.29",
4
+ "main": "dist/index.js",
5
+ "types": "dist/index.d.ts",
6
+ "exports": {
7
+ ".": {
8
+ "types": "./dist/index.d.ts",
9
+ "default": "./dist/index.js"
10
+ }
11
+ },
12
+ "publishConfig": {
13
+ "access": "public"
14
+ },
15
+ "scripts": {
16
+ "build": "tsc",
17
+ "dev": "tsc -w --noCheck",
18
+ "test": "vitest run",
19
+ "prepublishOnly": "npm run build"
20
+ },
21
+ "dependencies": {
22
+ "drizzle-orm": "^0.36.4",
23
+ "postgres": "^3.4.8"
24
+ },
25
+ "devDependencies": {
26
+ "@types/node": "^20.0.0",
27
+ "typescript": "^5.0.0",
28
+ "vitest": "^4.0.18"
29
+ },
30
+ "description": "Shared workspace auth, project selection, and managed credential helpers for Kognitive apps",
31
+ "license": "MIT"
32
+ }
@@ -0,0 +1,29 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import {
3
+ createRawApiKey,
4
+ decryptManagedSecret,
5
+ encryptManagedSecret,
6
+ getApiKeyPrefix,
7
+ hashApiKey,
8
+ } from "../crypto";
9
+
10
+ const secret = "12345678901234567890123456789012";
11
+
12
+ describe("workspace auth crypto helpers", () => {
13
+ it("creates backend-compatible API keys and hashes", () => {
14
+ const rawKey = createRawApiKey();
15
+ expect(rawKey).toMatch(/^kog_[a-f0-9]{32}$/);
16
+ expect(getApiKeyPrefix(rawKey)).toBe(rawKey.slice(0, 8));
17
+ expect(hashApiKey(rawKey)).toMatch(/^[a-f0-9]{64}$/);
18
+ });
19
+
20
+ it("encrypts and decrypts managed secrets", () => {
21
+ const encrypted = encryptManagedSecret("kog_secret", secret);
22
+ expect(encrypted.encryptedKey).not.toBe("kog_secret");
23
+ expect(decryptManagedSecret(encrypted, secret)).toBe("kog_secret");
24
+ });
25
+
26
+ it("requires a stable 32+ byte secret", () => {
27
+ expect(() => encryptManagedSecret("kog_secret", "short")).toThrow("KOGNITIVE_MANAGED_CREDENTIAL_SECRET");
28
+ });
29
+ });