@intelligo-dev/auth 1.0.0-beta.1
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/LICENSE +201 -0
- package/dist/client.js +25 -0
- package/dist/client.js.map +1 -0
- package/dist/edge.js +39 -0
- package/dist/edge.js.map +1 -0
- package/dist/helpers.js +248 -0
- package/dist/helpers.js.map +1 -0
- package/dist/impersonation.js +42 -0
- package/dist/impersonation.js.map +1 -0
- package/dist/index.js +30 -0
- package/dist/index.js.map +1 -0
- package/dist/onboarding/errors.js +31 -0
- package/dist/onboarding/errors.js.map +1 -0
- package/dist/onboarding/schemas.js +24 -0
- package/dist/onboarding/schemas.js.map +1 -0
- package/dist/onboarding/service.js +147 -0
- package/dist/onboarding/service.js.map +1 -0
- package/dist/org-api.js +86 -0
- package/dist/org-api.js.map +1 -0
- package/dist/profile/errors.js +32 -0
- package/dist/profile/errors.js.map +1 -0
- package/dist/profile/schemas.js +20 -0
- package/dist/profile/schemas.js.map +1 -0
- package/dist/profile/service.js +157 -0
- package/dist/profile/service.js.map +1 -0
- package/dist/roles.js +17 -0
- package/dist/roles.js.map +1 -0
- package/dist/server.js +251 -0
- package/dist/server.js.map +1 -0
- package/dist/team/errors.js +31 -0
- package/dist/team/errors.js.map +1 -0
- package/dist/team/schemas.js +29 -0
- package/dist/team/schemas.js.map +1 -0
- package/dist/team/service.js +438 -0
- package/dist/team/service.js.map +1 -0
- package/dist/workspace/errors.js +32 -0
- package/dist/workspace/errors.js.map +1 -0
- package/dist/workspace/schemas.js +45 -0
- package/dist/workspace/schemas.js.map +1 -0
- package/dist/workspace/service.js +268 -0
- package/dist/workspace/service.js.map +1 -0
- package/dist/workspace-init.js +121 -0
- package/dist/workspace-init.js.map +1 -0
- package/package.json +58 -0
- package/src/client.ts +27 -0
- package/src/edge.ts +43 -0
- package/src/helpers.ts +317 -0
- package/src/impersonation.ts +57 -0
- package/src/index.ts +109 -0
- package/src/onboarding/errors.ts +52 -0
- package/src/onboarding/schemas.ts +27 -0
- package/src/onboarding/service.ts +174 -0
- package/src/org-api.ts +198 -0
- package/src/profile/errors.ts +58 -0
- package/src/profile/schemas.ts +23 -0
- package/src/profile/service.ts +208 -0
- package/src/roles.ts +17 -0
- package/src/server.ts +305 -0
- package/src/team/errors.ts +71 -0
- package/src/team/schemas.ts +35 -0
- package/src/team/service.ts +611 -0
- package/src/workspace/errors.ts +69 -0
- package/src/workspace/schemas.ts +57 -0
- package/src/workspace/service.ts +381 -0
- package/src/workspace-init.ts +140 -0
package/src/helpers.ts
ADDED
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server-Side Auth Helpers
|
|
3
|
+
*
|
|
4
|
+
* Utilities for server components and server actions to check authentication.
|
|
5
|
+
* These helpers use Better-Auth's session API with Next.js headers.
|
|
6
|
+
*
|
|
7
|
+
* Usage in server actions (TECH-10 pattern):
|
|
8
|
+
* ```typescript
|
|
9
|
+
* export async function myServerAction() {
|
|
10
|
+
* const { user } = await requireAuth();
|
|
11
|
+
* // ... proceed with authenticated user
|
|
12
|
+
* }
|
|
13
|
+
* ```
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { headers } from "next/headers";
|
|
17
|
+
import { auth } from "./server";
|
|
18
|
+
import { createLogger } from "@intelligo-dev/core/logger";
|
|
19
|
+
import { db } from "@intelligo-dev/core/db";
|
|
20
|
+
import { users } from "@intelligo-dev/core/db/schema";
|
|
21
|
+
import { eq } from "drizzle-orm";
|
|
22
|
+
import type { Session, User } from "better-auth/types";
|
|
23
|
+
import { PLATFORM_ADMIN_ROLE } from "./roles";
|
|
24
|
+
|
|
25
|
+
/** Valid Better-Auth workspace roles. Used to type-check allowedRoles in requireRole(). */
|
|
26
|
+
export type WorkspaceRole = "owner" | "admin" | "member";
|
|
27
|
+
|
|
28
|
+
const log = createLogger("Auth");
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Get current auth session from request headers.
|
|
32
|
+
*
|
|
33
|
+
* Returns session and user if authenticated, null otherwise.
|
|
34
|
+
* Use this in server components that handle both authenticated and unauthenticated states.
|
|
35
|
+
*/
|
|
36
|
+
export async function getAuthSession(): Promise<{
|
|
37
|
+
session: Session;
|
|
38
|
+
user: User;
|
|
39
|
+
} | null> {
|
|
40
|
+
const session = await auth.api.getSession({
|
|
41
|
+
headers: await headers(),
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
if (!session?.user) {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
session: session.session,
|
|
50
|
+
user: session.user,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Require authentication.
|
|
56
|
+
*
|
|
57
|
+
* Throws "Unauthorized" error if no valid session.
|
|
58
|
+
* Use this in server actions to enforce authentication (TECH-10 pattern).
|
|
59
|
+
*
|
|
60
|
+
* @throws Error with "Unauthorized" message if not authenticated
|
|
61
|
+
*/
|
|
62
|
+
export async function requireAuth(): Promise<{
|
|
63
|
+
session: Session;
|
|
64
|
+
user: User;
|
|
65
|
+
}> {
|
|
66
|
+
const result = await getAuthSession();
|
|
67
|
+
|
|
68
|
+
if (!result) {
|
|
69
|
+
throw new Error("Unauthorized");
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return result;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Get workspace context by organization ID.
|
|
77
|
+
* Use this when you know the org ID (e.g., just after creating/activating a workspace).
|
|
78
|
+
*
|
|
79
|
+
* @param organizationId - The organization ID to get context for
|
|
80
|
+
*/
|
|
81
|
+
export async function getWorkspaceContextById(organizationId: string): Promise<{
|
|
82
|
+
session: Session;
|
|
83
|
+
user: User;
|
|
84
|
+
workspace: { id: string; name: string; slug: string; logo?: string | null };
|
|
85
|
+
membership: { id: string; role: WorkspaceRole };
|
|
86
|
+
} | null> {
|
|
87
|
+
const authResult = await getAuthSession();
|
|
88
|
+
if (!authResult) {
|
|
89
|
+
log.debug("getWorkspaceContextById: no auth session");
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
log.debug("getWorkspaceContextById: fetching org");
|
|
94
|
+
|
|
95
|
+
// Get organization by ID directly (not from session)
|
|
96
|
+
const org = await auth.api.getFullOrganization({
|
|
97
|
+
headers: await headers(),
|
|
98
|
+
query: { organizationId },
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
if (!org) {
|
|
102
|
+
log.debug("getWorkspaceContextById: org not found");
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Find user's membership
|
|
107
|
+
const membership = org.members?.find(
|
|
108
|
+
(m: any) => m.userId === authResult.user.id
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
if (!membership) {
|
|
112
|
+
log.debug("getWorkspaceContextById: user not a member");
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
session: authResult.session,
|
|
118
|
+
user: authResult.user,
|
|
119
|
+
workspace: {
|
|
120
|
+
id: org.id,
|
|
121
|
+
name: org.name,
|
|
122
|
+
slug: org.slug,
|
|
123
|
+
logo: org.logo,
|
|
124
|
+
},
|
|
125
|
+
membership: {
|
|
126
|
+
id: membership.id,
|
|
127
|
+
role: membership.role,
|
|
128
|
+
},
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Get workspace context from the active organization in session.
|
|
134
|
+
* Returns null if no active organization set.
|
|
135
|
+
*
|
|
136
|
+
* Use this in server components that need workspace context but can handle
|
|
137
|
+
* the absence of an active workspace (e.g., workspace switcher UI).
|
|
138
|
+
*/
|
|
139
|
+
export async function getWorkspaceContext(): Promise<{
|
|
140
|
+
session: Session;
|
|
141
|
+
user: User;
|
|
142
|
+
workspace: { id: string; name: string; slug: string; logo?: string | null };
|
|
143
|
+
membership: { id: string; role: WorkspaceRole };
|
|
144
|
+
} | null> {
|
|
145
|
+
const authResult = await getAuthSession();
|
|
146
|
+
if (!authResult) {
|
|
147
|
+
log.debug("getWorkspaceContext: no auth session");
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
log.debug("getWorkspaceContext: session found");
|
|
152
|
+
|
|
153
|
+
// Get active organization from session
|
|
154
|
+
let activeOrg = await auth.api.getFullOrganization({
|
|
155
|
+
headers: await headers(),
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
log.debug("getWorkspaceContext: active org", { hasOrg: !!activeOrg });
|
|
159
|
+
|
|
160
|
+
// Fallback: If no active org in session, auto-select first available workspace
|
|
161
|
+
if (!activeOrg) {
|
|
162
|
+
log.debug("getWorkspaceContext: no active org, checking workspaces");
|
|
163
|
+
const orgs: any = await auth.api.listOrganizations({
|
|
164
|
+
headers: await headers(),
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
if (orgs && orgs.length > 0) {
|
|
168
|
+
log.debug("getWorkspaceContext: found workspaces", {
|
|
169
|
+
count: String(orgs.length),
|
|
170
|
+
});
|
|
171
|
+
// Fetch full organization details for the first workspace
|
|
172
|
+
activeOrg = await auth.api.getFullOrganization({
|
|
173
|
+
headers: await headers(),
|
|
174
|
+
query: { organizationId: orgs[0].id },
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (!activeOrg) {
|
|
180
|
+
log.debug("getWorkspaceContext: no workspaces found");
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Find user's membership in the active org
|
|
185
|
+
const activeMember = activeOrg.members.find(
|
|
186
|
+
(m: any) => m.userId === authResult.user.id
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
log.debug("getWorkspaceContext: membership check", {
|
|
190
|
+
hasMembership: !!activeMember,
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
if (!activeMember) {
|
|
194
|
+
log.debug("getWorkspaceContext: user not a member of active org");
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return {
|
|
199
|
+
session: authResult.session,
|
|
200
|
+
user: authResult.user,
|
|
201
|
+
workspace: {
|
|
202
|
+
id: activeOrg.id,
|
|
203
|
+
name: activeOrg.name,
|
|
204
|
+
slug: activeOrg.slug,
|
|
205
|
+
logo: activeOrg.logo,
|
|
206
|
+
},
|
|
207
|
+
membership: {
|
|
208
|
+
id: activeMember.id,
|
|
209
|
+
role: activeMember.role,
|
|
210
|
+
},
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Require workspace context. Use in server actions that need workspace scope (TECH-10).
|
|
216
|
+
* Throws if not authenticated OR no active workspace selected.
|
|
217
|
+
*
|
|
218
|
+
* Use this in server actions that operate on workspace-scoped resources
|
|
219
|
+
* (conversations, knowledge bases, usage logs, etc.).
|
|
220
|
+
*
|
|
221
|
+
* @throws Error with "No active workspace" message if no workspace selected
|
|
222
|
+
*/
|
|
223
|
+
export async function requireWorkspace(): Promise<{
|
|
224
|
+
session: Session;
|
|
225
|
+
user: User;
|
|
226
|
+
workspace: { id: string; name: string; slug: string; logo?: string | null };
|
|
227
|
+
membership: { id: string; role: WorkspaceRole };
|
|
228
|
+
}> {
|
|
229
|
+
const context = await getWorkspaceContext();
|
|
230
|
+
if (!context) {
|
|
231
|
+
throw new Error("No active workspace");
|
|
232
|
+
}
|
|
233
|
+
return context;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Require specific role in active workspace.
|
|
238
|
+
* Use for admin/owner-only server actions (TEAM-08).
|
|
239
|
+
*
|
|
240
|
+
* Example: requireRole(["owner", "admin"]) for workspace settings actions.
|
|
241
|
+
*
|
|
242
|
+
* @param allowedRoles - Array of role names that are permitted
|
|
243
|
+
* @throws Error with "Insufficient permissions" message if user lacks required role
|
|
244
|
+
*/
|
|
245
|
+
export async function requireRole(allowedRoles: WorkspaceRole[]): Promise<{
|
|
246
|
+
session: Session;
|
|
247
|
+
user: User;
|
|
248
|
+
workspace: { id: string; name: string; slug: string; logo?: string | null };
|
|
249
|
+
membership: { id: string; role: WorkspaceRole };
|
|
250
|
+
}> {
|
|
251
|
+
const context = await requireWorkspace();
|
|
252
|
+
if (!allowedRoles.includes(context.membership.role)) {
|
|
253
|
+
throw new Error("Insufficient permissions");
|
|
254
|
+
}
|
|
255
|
+
return context;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Require PLATFORM admin — distinct from workspace roles.
|
|
260
|
+
*
|
|
261
|
+
* Workspace `owner` is a per-tenant role: any user who creates a
|
|
262
|
+
* workspace owns it. Platform-level surfaces (cross-workspace
|
|
263
|
+
* analytics, the operational console, impersonation) must never be
|
|
264
|
+
* gated on it.
|
|
265
|
+
*
|
|
266
|
+
* The authority is `users.role`. PLATFORM_ADMIN_EMAILS is the
|
|
267
|
+
* bootstrap: an allowlisted user is promoted into the column the first
|
|
268
|
+
* time they pass through here, so a fresh deployment has a way in and
|
|
269
|
+
* every later check — including Better-Auth's admin plugin, which can
|
|
270
|
+
* only read the row — agrees with this one. Two gates that can
|
|
271
|
+
* disagree is how the cross-tenant analytics leak happened in the
|
|
272
|
+
* first place.
|
|
273
|
+
*
|
|
274
|
+
* Closed by default: no allowlist and no role means no admin.
|
|
275
|
+
*
|
|
276
|
+
* @throws Error("Insufficient permissions") when the user is not a
|
|
277
|
+
* platform admin.
|
|
278
|
+
*/
|
|
279
|
+
export async function requirePlatformAdmin(): Promise<{
|
|
280
|
+
session: Session;
|
|
281
|
+
user: User;
|
|
282
|
+
}> {
|
|
283
|
+
const { session, user } = await requireAuth();
|
|
284
|
+
|
|
285
|
+
const allowlist = (process.env.PLATFORM_ADMIN_EMAILS ?? "")
|
|
286
|
+
.split(",")
|
|
287
|
+
.map((e) => e.trim().toLowerCase())
|
|
288
|
+
.filter(Boolean);
|
|
289
|
+
|
|
290
|
+
const email = user.email?.toLowerCase();
|
|
291
|
+
const allowlisted = !!email && allowlist.includes(email);
|
|
292
|
+
const roles = ((user as { role?: string | null }).role ?? "")
|
|
293
|
+
.split(",")
|
|
294
|
+
.map((r) => r.trim());
|
|
295
|
+
const hasRole = roles.includes(PLATFORM_ADMIN_ROLE);
|
|
296
|
+
|
|
297
|
+
if (!allowlisted && !hasRole) {
|
|
298
|
+
throw new Error("Insufficient permissions");
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// Promote on first use so the row, not the environment, is what
|
|
302
|
+
// every other check reads. A failure here must not lock an admin out
|
|
303
|
+
// mid-incident — they are already authorized by the allowlist.
|
|
304
|
+
if (allowlisted && !hasRole) {
|
|
305
|
+
const next = [...roles.filter(Boolean), PLATFORM_ADMIN_ROLE].join(",");
|
|
306
|
+
try {
|
|
307
|
+
await db.update(users).set({ role: next }).where(eq(users.id, user.id));
|
|
308
|
+
} catch (error) {
|
|
309
|
+
log.error("Failed to promote allowlisted platform admin", {
|
|
310
|
+
userId: user.id,
|
|
311
|
+
error: error instanceof Error ? error.message : String(error),
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
return { session, user };
|
|
317
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import "server-only";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Session-level impersonation.
|
|
5
|
+
*
|
|
6
|
+
* This module owns only the session mechanics, because that is what
|
|
7
|
+
* @intelligo-dev/auth owns: it holds the Better-Auth instance and the
|
|
8
|
+
* request headers. The *policy* — who may do it, that it is audited or
|
|
9
|
+
* refused, that a reason is mandatory — lives in @intelligo-dev/admin, and
|
|
10
|
+
* these functions must not be called without going through it.
|
|
11
|
+
*
|
|
12
|
+
* Nothing here re-checks authorization. Splitting the check from the
|
|
13
|
+
* act would give two places that can disagree about who is an admin,
|
|
14
|
+
* which is the mistake that put cross-tenant analytics behind a
|
|
15
|
+
* workspace role.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { headers } from "next/headers";
|
|
19
|
+
|
|
20
|
+
import { auth } from "./server";
|
|
21
|
+
|
|
22
|
+
export type ImpersonatedSession = {
|
|
23
|
+
targetUserId: string;
|
|
24
|
+
/** When Better-Auth will expire it — capped in the auth config. */
|
|
25
|
+
expiresAt: Date;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/** Default matching `impersonationSessionDuration` in server.ts. */
|
|
29
|
+
const FALLBACK_DURATION_MS = 30 * 60_000;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Swap the caller's session cookie for one belonging to `targetUserId`.
|
|
33
|
+
*
|
|
34
|
+
* Better-Auth refuses if the target is themselves a platform admin.
|
|
35
|
+
*/
|
|
36
|
+
export async function impersonateUser(
|
|
37
|
+
targetUserId: string
|
|
38
|
+
): Promise<ImpersonatedSession> {
|
|
39
|
+
const result = (await auth.api.impersonateUser({
|
|
40
|
+
body: { userId: targetUserId },
|
|
41
|
+
headers: await headers(),
|
|
42
|
+
})) as { session?: { expiresAt?: string | Date } };
|
|
43
|
+
|
|
44
|
+
const expiresAt = result.session?.expiresAt;
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
targetUserId,
|
|
48
|
+
expiresAt: expiresAt
|
|
49
|
+
? new Date(expiresAt)
|
|
50
|
+
: new Date(Date.now() + FALLBACK_DURATION_MS),
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Restore the admin's own session. */
|
|
55
|
+
export async function stopImpersonating(): Promise<void> {
|
|
56
|
+
await auth.api.stopImpersonating({ headers: await headers() });
|
|
57
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Better-Auth Server Exports
|
|
3
|
+
*
|
|
4
|
+
* Server-side authentication utilities.
|
|
5
|
+
* For client-side auth, use @intelligo-dev/auth/client
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export { auth } from "./server";
|
|
9
|
+
export {
|
|
10
|
+
getAuthSession,
|
|
11
|
+
requireAuth,
|
|
12
|
+
getWorkspaceContext,
|
|
13
|
+
getWorkspaceContextById,
|
|
14
|
+
requireWorkspace,
|
|
15
|
+
requireRole,
|
|
16
|
+
requirePlatformAdmin,
|
|
17
|
+
} from "./helpers";
|
|
18
|
+
export { ensureUserWorkspace } from "./workspace-init";
|
|
19
|
+
export type { WorkspaceRole } from "./helpers";
|
|
20
|
+
export { PLATFORM_ADMIN_ROLE } from "./roles";
|
|
21
|
+
export {
|
|
22
|
+
impersonateUser,
|
|
23
|
+
stopImpersonating,
|
|
24
|
+
type ImpersonatedSession,
|
|
25
|
+
} from "./impersonation";
|
|
26
|
+
|
|
27
|
+
// Re-export common types from Better-Auth
|
|
28
|
+
export type { Session, User } from "better-auth/types";
|
|
29
|
+
|
|
30
|
+
// Typed Better-Auth organization plugin wrapper
|
|
31
|
+
export {
|
|
32
|
+
orgApi,
|
|
33
|
+
type OrgApi,
|
|
34
|
+
type OrgEndpointOptions,
|
|
35
|
+
type OrgRole,
|
|
36
|
+
type OrgListItem,
|
|
37
|
+
type OrgInvitation,
|
|
38
|
+
type OrgMember,
|
|
39
|
+
} from "./org-api";
|
|
40
|
+
|
|
41
|
+
// Team management service (page/registry migration, section C1)
|
|
42
|
+
export {
|
|
43
|
+
createTeamService,
|
|
44
|
+
type TeamService,
|
|
45
|
+
type TeamServicePorts,
|
|
46
|
+
} from "./team/service";
|
|
47
|
+
export {
|
|
48
|
+
TeamServiceError,
|
|
49
|
+
isTeamServiceError,
|
|
50
|
+
type TeamServiceErrorCode,
|
|
51
|
+
type TeamServiceErrorMeta,
|
|
52
|
+
} from "./team/errors";
|
|
53
|
+
export {
|
|
54
|
+
inviteMemberSchema,
|
|
55
|
+
updateRoleSchema,
|
|
56
|
+
type InviteMemberInput,
|
|
57
|
+
type UpdateRoleInput,
|
|
58
|
+
} from "./team/schemas";
|
|
59
|
+
|
|
60
|
+
// Workspace management service (page/registry migration, workspace-settings family)
|
|
61
|
+
export {
|
|
62
|
+
createWorkspaceService,
|
|
63
|
+
type WorkspaceService,
|
|
64
|
+
type WorkspaceServicePorts,
|
|
65
|
+
type WorkspaceRecord,
|
|
66
|
+
} from "./workspace/service";
|
|
67
|
+
export {
|
|
68
|
+
WorkspaceServiceError,
|
|
69
|
+
isWorkspaceServiceError,
|
|
70
|
+
type WorkspaceServiceErrorCode,
|
|
71
|
+
type WorkspaceServiceErrorMeta,
|
|
72
|
+
} from "./workspace/errors";
|
|
73
|
+
export {
|
|
74
|
+
createWorkspaceSchema,
|
|
75
|
+
updateWorkspaceSchema,
|
|
76
|
+
type CreateWorkspaceInput,
|
|
77
|
+
type UpdateWorkspaceInput,
|
|
78
|
+
} from "./workspace/schemas";
|
|
79
|
+
|
|
80
|
+
// Profile management service (page/registry migration, profile-settings family)
|
|
81
|
+
export {
|
|
82
|
+
createProfileService,
|
|
83
|
+
type ProfileService,
|
|
84
|
+
type ProfileServicePorts,
|
|
85
|
+
type ProfileRecord,
|
|
86
|
+
} from "./profile/service";
|
|
87
|
+
export {
|
|
88
|
+
ProfileServiceError,
|
|
89
|
+
isProfileServiceError,
|
|
90
|
+
type ProfileServiceErrorCode,
|
|
91
|
+
type ProfileServiceErrorMeta,
|
|
92
|
+
} from "./profile/errors";
|
|
93
|
+
export {
|
|
94
|
+
updateProfileSchema,
|
|
95
|
+
type UpdateProfileInput,
|
|
96
|
+
} from "./profile/schemas";
|
|
97
|
+
|
|
98
|
+
// Onboarding service (page/registry migration, `onboarding` family)
|
|
99
|
+
export {
|
|
100
|
+
createOnboardingService,
|
|
101
|
+
type OnboardingService,
|
|
102
|
+
type OnboardingState,
|
|
103
|
+
} from "./onboarding/service";
|
|
104
|
+
export {
|
|
105
|
+
OnboardingServiceError,
|
|
106
|
+
isOnboardingServiceError,
|
|
107
|
+
type OnboardingServiceErrorCode,
|
|
108
|
+
} from "./onboarding/errors";
|
|
109
|
+
export { setStepSchema, type SetStepInput } from "./onboarding/schemas";
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Onboarding service error type.
|
|
3
|
+
*
|
|
4
|
+
* Mirrors `../team/errors.ts` and `../workspace/errors.ts`: the
|
|
5
|
+
* onboarding service (./service.ts) throws this for every failure it
|
|
6
|
+
* recognizes rather than returning an ad-hoc `{ success, error }`
|
|
7
|
+
* envelope — that shaping is a transport concern (a Server Action, a
|
|
8
|
+
* route handler) and belongs one layer up, alongside
|
|
9
|
+
* revalidatePath/Sentry/toast/i18n, none of which this package may
|
|
10
|
+
* depend on.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* - `forbidden` — the caller is unauthenticated.
|
|
15
|
+
* - `invalid_input` — `setStep`'s step id failed validation (empty or
|
|
16
|
+
* over 64 characters — see `./schemas.ts`).
|
|
17
|
+
* - `not_found` — the caller's `users` row could not be found.
|
|
18
|
+
*/
|
|
19
|
+
export type OnboardingServiceErrorCode =
|
|
20
|
+
| "forbidden"
|
|
21
|
+
| "invalid_input"
|
|
22
|
+
| "not_found";
|
|
23
|
+
|
|
24
|
+
export class OnboardingServiceError extends Error {
|
|
25
|
+
readonly code: OnboardingServiceErrorCode;
|
|
26
|
+
|
|
27
|
+
constructor(
|
|
28
|
+
code: OnboardingServiceErrorCode,
|
|
29
|
+
message: string,
|
|
30
|
+
options?: { cause?: unknown }
|
|
31
|
+
) {
|
|
32
|
+
super(message);
|
|
33
|
+
this.name = "OnboardingServiceError";
|
|
34
|
+
this.code = code;
|
|
35
|
+
if (options?.cause !== undefined) {
|
|
36
|
+
// ES2020 target predates the standard `cause` constructor option;
|
|
37
|
+
// assign it directly so `instanceof Error` consumers (and Node's
|
|
38
|
+
// own error inspection) still see it.
|
|
39
|
+
(this as { cause?: unknown }).cause = options.cause;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Restore prototype chain (extending built-ins across some
|
|
43
|
+
// transpilation targets loses `instanceof`).
|
|
44
|
+
Object.setPrototypeOf(this, OnboardingServiceError.prototype);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function isOnboardingServiceError(
|
|
49
|
+
error: unknown
|
|
50
|
+
): error is OnboardingServiceError {
|
|
51
|
+
return error instanceof OnboardingServiceError;
|
|
52
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Onboarding validation schema.
|
|
3
|
+
*
|
|
4
|
+
* A single generic step-id validator — see the module doc comment on
|
|
5
|
+
* `./service.ts` for why this is a bare, bounded string rather than a
|
|
6
|
+
* product-specific enum. Ignite's original
|
|
7
|
+
* `lib/validations/onboarding.ts` pinned this to
|
|
8
|
+
* `z.enum(["role", "profile", "complete"])`, Career's own 3-step wizard
|
|
9
|
+
* shape. The `users.onboarding_step` column itself is untyped `text`,
|
|
10
|
+
* so that enum was a product-level constraint bolted onto a generic
|
|
11
|
+
* column, not something this shared contract should require. A
|
|
12
|
+
* consumer defines its own step ids (typically the `id` field of a
|
|
13
|
+
* steps-config array it owns) and passes them straight through;
|
|
14
|
+
* Ignite's cut-over copy keeps its own enum-shaped schema at the
|
|
15
|
+
* transport layer if it wants stricter validation than "non-empty,
|
|
16
|
+
* bounded string".
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { z } from "zod";
|
|
20
|
+
|
|
21
|
+
export const setStepSchema = z
|
|
22
|
+
.string()
|
|
23
|
+
.trim()
|
|
24
|
+
.min(1, "Step id is required")
|
|
25
|
+
.max(64, "Step id must be at most 64 characters");
|
|
26
|
+
|
|
27
|
+
export type SetStepInput = z.infer<typeof setStepSchema>;
|