@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/.turbo/turbo-build.log +2 -0
- package/CHANGELOG.md +7 -0
- package/dist/crypto.d.ts +10 -0
- package/dist/crypto.js +41 -0
- package/dist/db.d.ts +6 -0
- package/dist/db.js +74 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +35 -0
- package/dist/schema.d.ts +817 -0
- package/dist/schema.js +80 -0
- package/dist/workspace.d.ts +197 -0
- package/dist/workspace.js +300 -0
- package/package.json +32 -0
- package/src/__tests__/crypto.test.ts +29 -0
- package/src/crypto.ts +50 -0
- package/src/db.ts +45 -0
- package/src/index.ts +36 -0
- package/src/schema.ts +82 -0
- package/src/workspace.ts +405 -0
- package/tsconfig.json +13 -0
- package/vitest.config.ts +8 -0
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
|
+
});
|