@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
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workspace management service — the durable business rules behind
|
|
3
|
+
* workspace listing, creation, switching, editing and deletion,
|
|
4
|
+
* extracted from the product application's workspace actions (page/
|
|
5
|
+
* registry migration, `workspace-settings` family, roadmap item 8).
|
|
6
|
+
*
|
|
7
|
+
* Mirrors `createTeamService(ports)` (./../team/service.ts) one
|
|
8
|
+
* directory over: a factory over optional ports, so this package's
|
|
9
|
+
* allowlisted dependency (`@intelligo-dev/core` only — see
|
|
10
|
+
* tests/architecture/dependency-direction.test.ts) never grows to
|
|
11
|
+
* include billing. A consumer binds that in at its composition root:
|
|
12
|
+
*
|
|
13
|
+
* const workspaceService = createWorkspaceService({
|
|
14
|
+
* checkWorkspaceLimit: ..., // adapts @intelligo-dev/billing's checkPlanLimit
|
|
15
|
+
* });
|
|
16
|
+
*
|
|
17
|
+
* Authorization (`requireAuth`/`requireWorkspace`/`requireRole`) lives
|
|
18
|
+
* INSIDE each method, not at the transport. Every recognized failure
|
|
19
|
+
* throws `WorkspaceServiceError` with a stable `code` — no
|
|
20
|
+
* revalidatePath/Sentry/next-intl/toast here; that shaping is the
|
|
21
|
+
* transport's job (a Server Action, a route handler).
|
|
22
|
+
*
|
|
23
|
+
* ---------------------------------------------------------------------
|
|
24
|
+
* Why `checkWorkspaceLimit` takes a `userId`, not a `workspaceId`
|
|
25
|
+
* ---------------------------------------------------------------------
|
|
26
|
+
* `createWorkspace` has no workspace to check the plan of yet — it is
|
|
27
|
+
* the thing being created. Ignite's original action worked around this
|
|
28
|
+
* by reading the caller's *existing* workspaces and using the first
|
|
29
|
+
* one's id to look up a plan via `@intelligo-dev/billing`'s
|
|
30
|
+
* `checkPlanLimit(workspaceId, "workspaces", currentCount)`, i.e. it
|
|
31
|
+
* borrowed an arbitrary existing workspace's subscription as a stand-in
|
|
32
|
+
* for "the caller's plan". That borrowing is a binding-layer concern,
|
|
33
|
+
* not a service-layer one: this service only knows the caller's
|
|
34
|
+
* `userId` and how many workspaces they already have. The composition
|
|
35
|
+
* root's binding (`checkWorkspaceLimit`) is where a consumer decides
|
|
36
|
+
* how to resolve "this user's plan" — by reading their first workspace
|
|
37
|
+
* the same way ignite did, or by a real per-user plan lookup if one
|
|
38
|
+
* exists.
|
|
39
|
+
*
|
|
40
|
+
* ---------------------------------------------------------------------
|
|
41
|
+
* The two Better-Auth pitfalls (already solved in ../team/service.ts —
|
|
42
|
+
* copied here rather than re-derived)
|
|
43
|
+
* ---------------------------------------------------------------------
|
|
44
|
+
* 1. Method-name mismatch: the typed `orgApi` wrapper (../org-api.ts)
|
|
45
|
+
* exists precisely because `auth.api`'s organization-plugin methods
|
|
46
|
+
* are keyed by their own camelCase *server* id, which does not
|
|
47
|
+
* always match the HTTP path or the client SDK name. This service
|
|
48
|
+
* uses `orgApi["/organization/list"]` and
|
|
49
|
+
* `orgApi["/organization/set-active"]` for the two calls the
|
|
50
|
+
* wrapper covers, and calls `auth.api.{createOrganization,
|
|
51
|
+
* updateOrganization, deleteOrganization, getFullOrganization}`
|
|
52
|
+
* directly for the base organization CRUD surface, which is not
|
|
53
|
+
* part of `orgApi`'s typed table (the same approach
|
|
54
|
+
* `../team/service.ts` takes for `getFullOrganization`). Ignite's
|
|
55
|
+
* original `actions/workspace.ts` already called these four by
|
|
56
|
+
* their correct `auth.api` names directly (it never went through a
|
|
57
|
+
* path-keyed cast), so there is no method-name bug to fix here.
|
|
58
|
+
* 2. `sessions.activeOrganizationId` does not exist on this repo's
|
|
59
|
+
* Drizzle schema. Ignite's original `getActiveWorkspace()` called
|
|
60
|
+
* `auth.api.getFullOrganization({ headers })` with no
|
|
61
|
+
* `organizationId` — the exact bug documented in
|
|
62
|
+
* `../team/service.ts`'s module comment: that resolves to *no*
|
|
63
|
+
* organization regardless of what the caller most recently
|
|
64
|
+
* activated. This service's `getActiveWorkspace` instead resolves
|
|
65
|
+
* the caller's workspace via `requireWorkspace()` first (which has
|
|
66
|
+
* its own explicit-id fallback) and passes that id explicitly to
|
|
67
|
+
* `getFullOrganization`.
|
|
68
|
+
*/
|
|
69
|
+
import { headers } from "next/headers";
|
|
70
|
+
import { createLogger } from "@intelligo-dev/core/logger";
|
|
71
|
+
import { auth } from "../server";
|
|
72
|
+
import { requireAuth, requireRole, requireWorkspace } from "../helpers";
|
|
73
|
+
import { orgApi } from "../org-api";
|
|
74
|
+
import { createWorkspaceSchema, updateWorkspaceSchema, } from "./schemas";
|
|
75
|
+
import { WorkspaceServiceError, isWorkspaceServiceError } from "./errors";
|
|
76
|
+
const log = createLogger("WorkspaceService");
|
|
77
|
+
function errorMessage(error) {
|
|
78
|
+
return error instanceof Error ? error.message : String(error);
|
|
79
|
+
}
|
|
80
|
+
/** Maps requireAuth/requireWorkspace/requireRole failures to `forbidden`. */
|
|
81
|
+
function toForbidden(error) {
|
|
82
|
+
if (isWorkspaceServiceError(error))
|
|
83
|
+
return error;
|
|
84
|
+
return new WorkspaceServiceError("forbidden", errorMessage(error), {
|
|
85
|
+
cause: error,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
function parseInput(schema, input) {
|
|
89
|
+
const result = schema.safeParse(input);
|
|
90
|
+
if (!result.success) {
|
|
91
|
+
throw new WorkspaceServiceError("invalid_input", result.error.issues.map((issue) => issue.message).join("; ") ||
|
|
92
|
+
"Invalid input", { cause: result.error });
|
|
93
|
+
}
|
|
94
|
+
return result.data;
|
|
95
|
+
}
|
|
96
|
+
/** Derives a URL-safe slug base from a workspace name. */
|
|
97
|
+
function slugify(name) {
|
|
98
|
+
return name
|
|
99
|
+
.toLowerCase()
|
|
100
|
+
.replace(/[^a-z0-9-]/g, "-")
|
|
101
|
+
.replace(/-+/g, "-")
|
|
102
|
+
.slice(0, 50);
|
|
103
|
+
}
|
|
104
|
+
export function createWorkspaceService(ports = {}) {
|
|
105
|
+
async function callRequireAuth() {
|
|
106
|
+
try {
|
|
107
|
+
return await requireAuth();
|
|
108
|
+
}
|
|
109
|
+
catch (error) {
|
|
110
|
+
throw toForbidden(error);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
async function callRequireWorkspace() {
|
|
114
|
+
try {
|
|
115
|
+
return await requireWorkspace();
|
|
116
|
+
}
|
|
117
|
+
catch (error) {
|
|
118
|
+
throw toForbidden(error);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
async function callRequireRole(allowedRoles) {
|
|
122
|
+
try {
|
|
123
|
+
return await requireRole(allowedRoles);
|
|
124
|
+
}
|
|
125
|
+
catch (error) {
|
|
126
|
+
throw toForbidden(error);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
/** Wraps a Better-Auth org-plugin call; unrecognized failures become `provider_error`. */
|
|
130
|
+
async function callOrgApi(context, fn) {
|
|
131
|
+
try {
|
|
132
|
+
return await fn();
|
|
133
|
+
}
|
|
134
|
+
catch (error) {
|
|
135
|
+
if (isWorkspaceServiceError(error))
|
|
136
|
+
throw error;
|
|
137
|
+
log.error("Org API call failed", { context, error: errorMessage(error) });
|
|
138
|
+
throw new WorkspaceServiceError("provider_error", `Better-Auth organization API call failed (${context})`, { cause: error });
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* List all workspaces for the current user.
|
|
143
|
+
*/
|
|
144
|
+
async function listWorkspaces() {
|
|
145
|
+
await callRequireAuth();
|
|
146
|
+
const hdrs = await headers();
|
|
147
|
+
const orgs = await callOrgApi("list", () => orgApi["/organization/list"]({ headers: hdrs }));
|
|
148
|
+
return orgs ?? [];
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Create a new workspace. Auto-generates a slug from the name if not
|
|
152
|
+
* provided, and appends a uniqueness suffix (Better-Auth requires
|
|
153
|
+
* unique slugs). The caller's first workspace is always allowed; the
|
|
154
|
+
* limit port, when bound, gates every workspace after that. The new
|
|
155
|
+
* workspace is auto-activated on success.
|
|
156
|
+
*/
|
|
157
|
+
async function createWorkspace(input) {
|
|
158
|
+
const { user } = await callRequireAuth();
|
|
159
|
+
const validated = parseInput(createWorkspaceSchema, input);
|
|
160
|
+
const hdrs = await headers();
|
|
161
|
+
const existing = await callOrgApi("list", () => orgApi["/organization/list"]({ headers: hdrs }));
|
|
162
|
+
const existingCount = existing?.length ?? 0;
|
|
163
|
+
if (ports.checkWorkspaceLimit && existingCount > 0) {
|
|
164
|
+
const limitCheck = await ports.checkWorkspaceLimit(user.id, existingCount);
|
|
165
|
+
if (!limitCheck.allowed) {
|
|
166
|
+
throw new WorkspaceServiceError("workspace_limit_reached", `Your plan allows up to ${limitCheck.limit} workspace${limitCheck.limit === 1 ? "" : "s"}. Upgrade to create more workspaces.`, { meta: { limit: limitCheck.limit } });
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
const slugBase = validated.slug || slugify(validated.name);
|
|
170
|
+
// Append timestamp for uniqueness (Better-Auth requires unique slugs).
|
|
171
|
+
const slug = `${slugBase}-${Date.now().toString(36)}`;
|
|
172
|
+
const org = (await callOrgApi("create", () => auth.api.createOrganization({
|
|
173
|
+
headers: hdrs,
|
|
174
|
+
body: { name: validated.name, slug },
|
|
175
|
+
})));
|
|
176
|
+
if (!org) {
|
|
177
|
+
throw new WorkspaceServiceError("provider_error", "Failed to create workspace");
|
|
178
|
+
}
|
|
179
|
+
await callOrgApi("set-active", () => orgApi["/organization/set-active"]({
|
|
180
|
+
headers: hdrs,
|
|
181
|
+
body: { organizationId: org.id },
|
|
182
|
+
}));
|
|
183
|
+
return org;
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Switch the caller's active workspace.
|
|
187
|
+
*/
|
|
188
|
+
async function switchWorkspace(organizationId) {
|
|
189
|
+
await callRequireAuth();
|
|
190
|
+
if (typeof organizationId !== "string" || organizationId.length === 0) {
|
|
191
|
+
throw new WorkspaceServiceError("invalid_input", "Invalid workspace id");
|
|
192
|
+
}
|
|
193
|
+
const hdrs = await headers();
|
|
194
|
+
await callOrgApi("set-active", () => orgApi["/organization/set-active"]({
|
|
195
|
+
headers: hdrs,
|
|
196
|
+
body: { organizationId },
|
|
197
|
+
}));
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Update workspace settings (name/slug/logo). Owner/admin only.
|
|
201
|
+
*/
|
|
202
|
+
async function updateWorkspace(input) {
|
|
203
|
+
const { workspace } = await callRequireRole(["owner", "admin"]);
|
|
204
|
+
const validated = parseInput(updateWorkspaceSchema, input);
|
|
205
|
+
const hdrs = await headers();
|
|
206
|
+
const updateData = {};
|
|
207
|
+
if (validated.name)
|
|
208
|
+
updateData.name = validated.name;
|
|
209
|
+
if (validated.slug)
|
|
210
|
+
updateData.slug = validated.slug;
|
|
211
|
+
if (validated.logo)
|
|
212
|
+
updateData.logo = validated.logo;
|
|
213
|
+
const result = (await callOrgApi("update", () => auth.api.updateOrganization({
|
|
214
|
+
headers: hdrs,
|
|
215
|
+
body: { data: updateData, organizationId: workspace.id },
|
|
216
|
+
})));
|
|
217
|
+
if (!result) {
|
|
218
|
+
throw new WorkspaceServiceError("provider_error", "Failed to update workspace");
|
|
219
|
+
}
|
|
220
|
+
return result;
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Delete the caller's active workspace. Owner only. Switches the
|
|
224
|
+
* caller to another workspace afterward, if one remains.
|
|
225
|
+
*/
|
|
226
|
+
async function deleteWorkspace() {
|
|
227
|
+
const { workspace } = await callRequireRole(["owner"]);
|
|
228
|
+
const hdrs = await headers();
|
|
229
|
+
await callOrgApi("delete", () => auth.api.deleteOrganization({
|
|
230
|
+
headers: hdrs,
|
|
231
|
+
body: { organizationId: workspace.id },
|
|
232
|
+
}));
|
|
233
|
+
const orgs = await callOrgApi("list", () => orgApi["/organization/list"]({ headers: hdrs }));
|
|
234
|
+
if (orgs && orgs.length > 0) {
|
|
235
|
+
await callOrgApi("set-active", () => orgApi["/organization/set-active"]({
|
|
236
|
+
headers: hdrs,
|
|
237
|
+
body: { organizationId: orgs[0].id },
|
|
238
|
+
}));
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* Get the caller's active workspace, fully resolved. Unlike
|
|
243
|
+
* ignite's original (see the module doc comment), this resolves the
|
|
244
|
+
* workspace id explicitly via `requireWorkspace()` rather than
|
|
245
|
+
* relying on a non-existent `sessions.activeOrganizationId` fallback.
|
|
246
|
+
*/
|
|
247
|
+
async function getActiveWorkspace() {
|
|
248
|
+
const { workspace } = await callRequireWorkspace();
|
|
249
|
+
const hdrs = await headers();
|
|
250
|
+
const org = (await callOrgApi("getFullOrganization", () => auth.api.getFullOrganization({
|
|
251
|
+
headers: hdrs,
|
|
252
|
+
query: { organizationId: workspace.id },
|
|
253
|
+
})));
|
|
254
|
+
if (!org) {
|
|
255
|
+
throw new WorkspaceServiceError("not_found", "Workspace not found");
|
|
256
|
+
}
|
|
257
|
+
return org;
|
|
258
|
+
}
|
|
259
|
+
return {
|
|
260
|
+
listWorkspaces,
|
|
261
|
+
createWorkspace,
|
|
262
|
+
switchWorkspace,
|
|
263
|
+
updateWorkspace,
|
|
264
|
+
deleteWorkspace,
|
|
265
|
+
getActiveWorkspace,
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
//# sourceMappingURL=service.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"service.js","sourceRoot":"","sources":["../../src/workspace/service.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workspace Initialization
|
|
3
|
+
*
|
|
4
|
+
* Ensures every authenticated user has at least one workspace.
|
|
5
|
+
* Called from the (app) layout to guarantee workspace exists.
|
|
6
|
+
* Idempotent - safe to call on every page load.
|
|
7
|
+
*
|
|
8
|
+
* Pattern: "Belt-and-suspenders" approach consistent with Phase 9.
|
|
9
|
+
* Better-Auth doesn't provide a reliable hook for post-signup workspace creation,
|
|
10
|
+
* so we check + create on every authenticated page load.
|
|
11
|
+
*
|
|
12
|
+
* Dependency Inversion (Phase 42, PKG-01 preparation):
|
|
13
|
+
* Post-creation logic (e.g., trial provisioning) is injected via callback,
|
|
14
|
+
* so this module has ZERO billing imports. The actual billing call lives
|
|
15
|
+
* in the consumer application's authenticated layout, which already
|
|
16
|
+
* imports billing.
|
|
17
|
+
* This breaks the auth → billing circular dependency before Phase 43 extraction.
|
|
18
|
+
*/
|
|
19
|
+
import { auth } from "./server";
|
|
20
|
+
import { createLogger } from "@intelligo-dev/core/logger";
|
|
21
|
+
const log = createLogger("WorkspaceInit");
|
|
22
|
+
/**
|
|
23
|
+
* Ensure user has at least one workspace (WORK-01).
|
|
24
|
+
*
|
|
25
|
+
* If user has organizations, sets the first as active. If none exist,
|
|
26
|
+
* creates a personal workspace — this serves as a fallback in case the
|
|
27
|
+
* user.create hook (server.ts databaseHooks) hasn't completed yet, and
|
|
28
|
+
* also handles users created via non-Better-Auth flows (e.g. admin panel).
|
|
29
|
+
*
|
|
30
|
+
* Idempotent - safe to call repeatedly. Returns quickly if workspace exists.
|
|
31
|
+
*
|
|
32
|
+
* @param user - Authenticated user from session
|
|
33
|
+
* @param headers - Request headers (required for Better-Auth API)
|
|
34
|
+
* @param options.onWorkspaceCreated - Optional callback invoked after a new workspace is created.
|
|
35
|
+
* Receives workspaceId and email. Failure does NOT block workspace creation (same fire-and-forget
|
|
36
|
+
* behavior as the previous inline trial provisioning call).
|
|
37
|
+
* @returns The organization ID that was set as active (useful for immediate access before session updates)
|
|
38
|
+
*/
|
|
39
|
+
export async function ensureUserWorkspace(user, headers, options) {
|
|
40
|
+
log.info("Starting workspace check", { email: user.email });
|
|
41
|
+
// Check if user has any organizations
|
|
42
|
+
const orgs = await auth.api.listOrganizations({
|
|
43
|
+
headers,
|
|
44
|
+
});
|
|
45
|
+
log.info("Found organizations", { count: orgs?.length ?? 0 });
|
|
46
|
+
if (orgs && orgs.length > 0 && orgs[0]) {
|
|
47
|
+
log.debug("User has workspaces, setting first as active");
|
|
48
|
+
// User has workspaces - ensure one is active
|
|
49
|
+
try {
|
|
50
|
+
await auth.api.setActiveOrganization({
|
|
51
|
+
headers,
|
|
52
|
+
body: { organizationId: orgs[0].id },
|
|
53
|
+
});
|
|
54
|
+
log.info("Set active organization", { orgId: orgs[0].id });
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
log.error("Failed to set active organization", {
|
|
58
|
+
error: error instanceof Error ? error.message : String(error),
|
|
59
|
+
});
|
|
60
|
+
throw error;
|
|
61
|
+
}
|
|
62
|
+
return orgs[0].id;
|
|
63
|
+
}
|
|
64
|
+
// TR-DB03 fix: No organizations found — the user.create hook may not have
|
|
65
|
+
// completed yet, or user was created via a non-Better-Auth flow.
|
|
66
|
+
// Create the workspace here as a fallback rather than throwing.
|
|
67
|
+
// Idempotent: the unique constraint on organization.slug prevents duplicates
|
|
68
|
+
// if both this fallback and the hook fire for the same signup.
|
|
69
|
+
log.warn("No workspace found, creating one (hook may still fire)", {
|
|
70
|
+
email: user.email,
|
|
71
|
+
});
|
|
72
|
+
const slug = ((user.email || "user").split("@")[0] || "user")
|
|
73
|
+
.toLowerCase()
|
|
74
|
+
.replace(/[^a-z0-9-]/g, "-")
|
|
75
|
+
.slice(0, 30);
|
|
76
|
+
let workspaceId;
|
|
77
|
+
try {
|
|
78
|
+
const result = await auth.api.createOrganization({
|
|
79
|
+
headers,
|
|
80
|
+
body: {
|
|
81
|
+
name: `${user.name || "User"}'s Workspace`,
|
|
82
|
+
slug: `${slug}-${Date.now().toString(36)}`,
|
|
83
|
+
userId: user.id,
|
|
84
|
+
},
|
|
85
|
+
});
|
|
86
|
+
if (!result) {
|
|
87
|
+
throw new Error("createOrganization returned no result");
|
|
88
|
+
}
|
|
89
|
+
workspaceId = result.id;
|
|
90
|
+
log.info("Created fallback workspace", { workspaceId });
|
|
91
|
+
}
|
|
92
|
+
catch (error) {
|
|
93
|
+
// If creation failed because the unique constraint was hit (hook already
|
|
94
|
+
// created), the error message contains the duplicate key info.
|
|
95
|
+
// List orgs again — if one now exists, use it.
|
|
96
|
+
const orgsAfter = await auth.api.listOrganizations({ headers });
|
|
97
|
+
if (orgsAfter && orgsAfter.length > 0 && orgsAfter[0]) {
|
|
98
|
+
log.info("Workspace created concurrently by hook, using it");
|
|
99
|
+
await auth.api.setActiveOrganization({
|
|
100
|
+
headers,
|
|
101
|
+
body: { organizationId: orgsAfter[0].id },
|
|
102
|
+
});
|
|
103
|
+
return orgsAfter[0].id;
|
|
104
|
+
}
|
|
105
|
+
// Genuine failure
|
|
106
|
+
log.error("Failed to create fallback workspace", {
|
|
107
|
+
error: error instanceof Error ? error.message : String(error),
|
|
108
|
+
});
|
|
109
|
+
throw error;
|
|
110
|
+
}
|
|
111
|
+
// Fire the optional callback (trial credits, referral tracking, etc.)
|
|
112
|
+
if (options?.onWorkspaceCreated) {
|
|
113
|
+
options
|
|
114
|
+
.onWorkspaceCreated({ workspaceId, email: user.email })
|
|
115
|
+
.catch((err) => log.error("onWorkspaceCreated callback failed", {
|
|
116
|
+
error: err instanceof Error ? err.message : String(err),
|
|
117
|
+
}));
|
|
118
|
+
}
|
|
119
|
+
return workspaceId;
|
|
120
|
+
}
|
|
121
|
+
//# sourceMappingURL=workspace-init.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"workspace-init.js","sourceRoot":"","sources":["../src/workspace-init.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@intelligo-dev/auth",
|
|
3
|
+
"version": "1.0.0-beta.1",
|
|
4
|
+
"license": "Apache-2.0",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "git+https://github.com/intelligo-mn/framework.git",
|
|
8
|
+
"directory": "packages/auth"
|
|
9
|
+
},
|
|
10
|
+
"homepage": "https://github.com/intelligo-mn/framework#readme",
|
|
11
|
+
"bugs": {
|
|
12
|
+
"url": "https://github.com/intelligo-mn/framework/issues"
|
|
13
|
+
},
|
|
14
|
+
"type": "module",
|
|
15
|
+
"exports": {
|
|
16
|
+
".": {
|
|
17
|
+
"types": "./src/index.ts",
|
|
18
|
+
"default": "./dist/index.js"
|
|
19
|
+
},
|
|
20
|
+
"./client": {
|
|
21
|
+
"types": "./src/client.ts",
|
|
22
|
+
"default": "./dist/client.js"
|
|
23
|
+
},
|
|
24
|
+
"./edge": {
|
|
25
|
+
"types": "./src/edge.ts",
|
|
26
|
+
"default": "./dist/edge.js"
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"better-auth": "~1.6.30",
|
|
31
|
+
"drizzle-orm": "^0.45.1",
|
|
32
|
+
"server-only": "^0.0.1",
|
|
33
|
+
"zod": "^3.24.1",
|
|
34
|
+
"@intelligo-dev/core": "1.0.0-beta.1"
|
|
35
|
+
},
|
|
36
|
+
"peerDependencies": {
|
|
37
|
+
"next": "^15.0.0 || ^16.0.0"
|
|
38
|
+
},
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"@types/react": "^19.0.10",
|
|
41
|
+
"next": "^16.0.7",
|
|
42
|
+
"typescript": "^5.7.2"
|
|
43
|
+
},
|
|
44
|
+
"files": [
|
|
45
|
+
"dist",
|
|
46
|
+
"src",
|
|
47
|
+
"!src/**/*.test.ts",
|
|
48
|
+
"!src/**/*.test.tsx"
|
|
49
|
+
],
|
|
50
|
+
"publishConfig": {
|
|
51
|
+
"access": "public"
|
|
52
|
+
},
|
|
53
|
+
"scripts": {
|
|
54
|
+
"type-check": "tsc --noEmit",
|
|
55
|
+
"lint": "eslint .",
|
|
56
|
+
"build": "tsc -p tsconfig.build.json"
|
|
57
|
+
}
|
|
58
|
+
}
|
package/src/client.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Better-Auth Client
|
|
3
|
+
*
|
|
4
|
+
* Client-side authentication utilities for React components.
|
|
5
|
+
* This module uses "better-auth/react" which is a client-side module.
|
|
6
|
+
*
|
|
7
|
+
* IMPORTANT: Only import this in "use client" components.
|
|
8
|
+
* For server-side auth, use @intelligo-dev/auth (server.ts)
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { createAuthClient } from "better-auth/react";
|
|
12
|
+
import { organizationClient } from "better-auth/client/plugins";
|
|
13
|
+
|
|
14
|
+
// No `baseURL`: the client and the Better-Auth server always live on the
|
|
15
|
+
// same origin (this app's own `/api/auth/*` routes), so Better-Auth's
|
|
16
|
+
// client defaults to `window.location.origin` and every request is
|
|
17
|
+
// same-origin. Do not reintroduce an absolute `NEXT_PUBLIC_APP_URL`
|
|
18
|
+
// baseURL here — the dev port has drifted before (3000 -> 3001 -> 4000)
|
|
19
|
+
// and an absolute cross-origin URL is blocked by the app's CSP
|
|
20
|
+
// (`connect-src 'self'`) whenever it doesn't match the port the app is
|
|
21
|
+
// actually served on.
|
|
22
|
+
export const authClient = createAuthClient({
|
|
23
|
+
plugins: [organizationClient()],
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
// Export commonly used hooks and methods for convenience
|
|
27
|
+
export const { useSession, signIn, signUp, signOut } = authClient;
|
package/src/edge.ts
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Edge-safe session presence check.
|
|
3
|
+
*
|
|
4
|
+
* Middleware runs in the Edge Runtime, where no TCP Postgres driver is
|
|
5
|
+
* available. The previous version of this module solved that by
|
|
6
|
+
* instantiating Better-Auth against the Neon HTTP driver — which made
|
|
7
|
+
* middleware silently Neon-only. Against any other Postgres the session
|
|
8
|
+
* query failed with `TypeError: fetch failed`, Better-Auth turned that
|
|
9
|
+
* into `APIError: Failed to get session`, and the middleware threw: every
|
|
10
|
+
* request to the app returned HTTP 500. The old code hid this behind a
|
|
11
|
+
* `NODE_ENV === "development"` bypass that skipped auth entirely, so the
|
|
12
|
+
* failure only appeared in a production build on a non-Neon database.
|
|
13
|
+
*
|
|
14
|
+
* The fix is to stop querying the database from the edge at all.
|
|
15
|
+
* Middleware performs an OPTIMISTIC check: it reads the session cookie
|
|
16
|
+
* and decides where to send the request. It never asserts that the
|
|
17
|
+
* session is valid.
|
|
18
|
+
*
|
|
19
|
+
* The authoritative check stays server-side, where it always was —
|
|
20
|
+
* `requireAuth`/`requireWorkspace`/`requireRole` and the authenticated
|
|
21
|
+
* layout's own `getAuthSession()` redirect. A forged or expired cookie
|
|
22
|
+
* gets past middleware and is then rejected by the page or action that
|
|
23
|
+
* actually reads data. This is the pattern Better-Auth documents for
|
|
24
|
+
* Next.js middleware, and it is what makes the guard work on any
|
|
25
|
+
* Postgres, in any runtime, with one code path in every environment.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import { getSessionCookie } from "better-auth/cookies";
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* True when the request carries a Better-Auth session cookie.
|
|
32
|
+
*
|
|
33
|
+
* Presence only — the cookie's signature is NOT verified and no
|
|
34
|
+
* database read happens. Never use this to authorize access to data;
|
|
35
|
+
* use it to choose a redirect. Authorization belongs to `requireAuth`
|
|
36
|
+
* and friends, which run in the Node runtime against the real session.
|
|
37
|
+
*/
|
|
38
|
+
export function hasSessionCookie(request: Request): boolean {
|
|
39
|
+
return getSessionCookie(request) !== null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Re-export for edge routes that reason about workspace roles.
|
|
43
|
+
export { type WorkspaceRole } from "./helpers";
|