@protobase/server 0.6.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (105) hide show
  1. package/dist/assistant/conversations.d.ts +1 -1
  2. package/dist/assistant/tools.d.ts +1 -1
  3. package/dist/assistant/turn.d.ts +2 -2
  4. package/dist/audit/origin.js +12 -0
  5. package/dist/auth/jwt.d.ts +2 -0
  6. package/dist/auth/jwt.js +5 -3
  7. package/dist/batch/reorder.js +1 -1
  8. package/dist/better-auth/auth-schema.d.ts +4 -2
  9. package/dist/better-auth/auth-schema.js +18 -8
  10. package/dist/better-auth/authenticator.d.ts +7 -3
  11. package/dist/better-auth/authenticator.js +24 -3
  12. package/dist/better-auth/create-auth.d.ts +683 -10
  13. package/dist/better-auth/create-auth.js +72 -27
  14. package/dist/better-auth/organizations/accept-invitation.js +80 -0
  15. package/dist/better-auth/organizations/active-organization.js +12 -0
  16. package/dist/better-auth/organizations/admin-routes.js +83 -0
  17. package/dist/better-auth/organizations/app-roles.js +43 -0
  18. package/dist/better-auth/organizations/host.d.ts +53 -0
  19. package/dist/better-auth/organizations/host.js +251 -0
  20. package/dist/better-auth/organizations/invitation-endpoints.js +133 -0
  21. package/dist/better-auth/organizations/invitation-mail.js +17 -0
  22. package/dist/better-auth/organizations/invitation-token.js +27 -0
  23. package/dist/better-auth/organizations/member-endpoints.js +182 -0
  24. package/dist/better-auth/organizations/options.d.ts +48 -0
  25. package/dist/better-auth/organizations/options.js +23 -0
  26. package/dist/better-auth/organizations/organization-plugin.js +128 -0
  27. package/dist/better-auth/organizations/protobase-plugin.js +21 -0
  28. package/dist/better-auth/organizations/role-definitions.d.ts +15 -0
  29. package/dist/better-auth/organizations/role-definitions.js +56 -0
  30. package/dist/better-auth/organizations/session-organization.js +68 -0
  31. package/dist/better-auth/organizations/store.d.ts +19 -0
  32. package/dist/better-auth/organizations/store.js +73 -0
  33. package/dist/better-auth/policy-store.js +44 -35
  34. package/dist/better-auth/snake-case-migration.js +122 -0
  35. package/dist/better-auth/snake-case-names.js +72 -0
  36. package/dist/better-auth/staff-checks.js +5 -1
  37. package/dist/better-auth/staff-log.js +56 -45
  38. package/dist/better-auth/staff-sign-in-endpoints.js +1 -1
  39. package/dist/better-auth/status-route.js +14 -3
  40. package/dist/better-auth/users.d.ts +2 -1
  41. package/dist/better-auth/users.js +4 -1
  42. package/dist/create-admin.d.ts +8 -2
  43. package/dist/create-admin.js +31 -2
  44. package/dist/deps.d.ts +1 -0
  45. package/dist/files/attach.js +80 -0
  46. package/dist/files/cleanup-runner.d.ts +8 -0
  47. package/dist/files/cleanup-runner.js +58 -0
  48. package/dist/files/cleanup-schedule.d.ts +28 -0
  49. package/dist/files/cleanup-schedule.js +48 -0
  50. package/dist/files/detect.js +51 -0
  51. package/dist/files/download-url.js +43 -0
  52. package/dist/files/download.js +90 -0
  53. package/dist/files/duration.js +35 -0
  54. package/dist/files/file-cleanup.d.ts +18 -0
  55. package/dist/files/file-cleanup.js +17 -0
  56. package/dist/files/local-files.d.ts +17 -0
  57. package/dist/files/local-files.js +101 -0
  58. package/dist/files/media-types.js +99 -0
  59. package/dist/files/options.d.ts +15 -0
  60. package/dist/files/present.d.ts +15 -0
  61. package/dist/files/present.js +59 -0
  62. package/dist/files/processors.d.ts +15 -0
  63. package/dist/files/processors.js +39 -0
  64. package/dist/files/providers.js +70 -0
  65. package/dist/files/routes.js +26 -0
  66. package/dist/files/runtime.d.ts +3 -0
  67. package/dist/files/runtime.js +37 -0
  68. package/dist/files/signer.js +32 -0
  69. package/dist/files/store.d.ts +37 -0
  70. package/dist/files/ticket.js +87 -0
  71. package/dist/files/upload-body.js +85 -0
  72. package/dist/files/upload.js +124 -0
  73. package/dist/files/uri.js +47 -0
  74. package/dist/functions/define-function.d.ts +71 -0
  75. package/dist/functions/define-function.js +36 -0
  76. package/dist/functions/function-app.d.ts +17 -0
  77. package/dist/functions/function-app.js +19 -0
  78. package/dist/functions/read-json.d.ts +35 -0
  79. package/dist/functions/read-json.js +24 -0
  80. package/dist/functions/records.d.ts +43 -0
  81. package/dist/functions/records.js +31 -0
  82. package/dist/functions/require-env.d.ts +7 -0
  83. package/dist/functions/require-env.js +12 -0
  84. package/dist/functions/routes.js +60 -0
  85. package/dist/functions/verify-signature.d.ts +32 -0
  86. package/dist/functions/verify-signature.js +49 -0
  87. package/dist/index.d.ts +21 -3
  88. package/dist/index.js +13 -3
  89. package/dist/list-service.js +4 -2
  90. package/dist/openapi/resource-paths.js +78 -1
  91. package/dist/openapi/schemas.js +28 -3
  92. package/dist/project-config.d.ts +7 -1
  93. package/dist/project-config.js +37 -1
  94. package/dist/read-service.js +5 -1
  95. package/dist/request-access.d.ts +5 -0
  96. package/dist/request-access.js +9 -4
  97. package/dist/resource-source.d.ts +4 -1
  98. package/dist/routes/batch.js +1 -1
  99. package/dist/routes/collection.js +3 -1
  100. package/dist/routes/record.js +2 -5
  101. package/dist/types.d.ts +25 -5
  102. package/dist/write-pipeline.js +2 -1
  103. package/dist/write-shared.js +6 -3
  104. package/dist/write-update.js +2 -1
  105. package/package.json +6 -4
@@ -1,10 +1,16 @@
1
1
  import { readMailSettings } from "../mail/mail-settings.js";
2
2
  import { smtpMailer } from "../mail/smtp-mailer.js";
3
3
  import { readOperatorSettings, resolveOperator } from "./operator-provider.js";
4
+ import { roleDefinitions } from "./organizations/role-definitions.js";
5
+ import { lastOrganizationField, organizationClaims, sessionOrganizationHooks } from "./organizations/session-organization.js";
6
+ import { organizationPlugin } from "./organizations/organization-plugin.js";
7
+ import { protobaseOrganizationPlugin } from "./organizations/protobase-plugin.js";
8
+ import { resolveOrganizations } from "./organizations/options.js";
4
9
  import { passwordResetOptions } from "./password-reset.js";
5
10
  import { platformSignInFor, platformSignInProvider, readPlatformSignIn, resolvePlatformSignIn } from "./platform-sign-in.js";
6
11
  import { signInPolicyPlugin } from "./policy-plugin.js";
7
12
  import { emailCodeOptions, twoFactorCodeOptions, unusedEmailCodePaths } from "./sign-in-mail.js";
13
+ import { coreNames, pluginNames } from "./snake-case-names.js";
8
14
  import { staffSignInPlugin } from "./staff-plugin.js";
9
15
  import { twoFactorAfterPasswordOrCode } from "./two-factor-challenge.js";
10
16
  import { passkey } from "@better-auth/passkey";
@@ -27,19 +33,16 @@ const authBasePath = "/api/auth";
27
33
  * `createUser`, or by an admin through the admin plugin), roles from the admin plugin's `role` column, 15 minute JWTs
28
34
  * at `/api/auth/token` with the keys at `/api/auth/jwks`.
29
35
  */
30
- const roleName = /^[a-z][a-z0-9_-]*$/;
31
36
  /** The configured roles with `admin` first; `admin` is added when missing. */
32
- const normalizeRoles = (roles = ["admin", "user"]) => {
33
- const bad = roles.find((role) => !roleName.test(role));
34
- if (bad !== void 0) throw new Error(`Invalid role "${bad}": use lowercase letters, digits, "_" or "-"`);
35
- return ["admin", ...new Set(roles.filter((role) => role !== "admin"))];
36
- };
37
+ const normalizeRoles = (roles) => roleDefinitions(roles).names;
37
38
  const adminImpersonationPaths = ["/admin/impersonate-user", "/admin/stop-impersonating"];
39
+ const noContext = () => Promise.reject(/* @__PURE__ */ new Error("The auth context is read only once createAuth has made the instance"));
40
+ const organizationPaths = ["/organization/set-active", "/organization/accept-invitation"];
38
41
  /**
39
42
  * The options `createAuth` gives Better Auth, with the roles, default role, mailer, operator provider and platform
40
43
  * sign-in provider already resolved.
41
44
  */
42
- const betterAuthOptions = (options, { roles, defaultRole, mailer, operator, platformSignIn }) => {
45
+ const betterAuthOptions = (options, { roles, defaultRole, mailer, operator, platformSignIn, organizations, context = noContext }) => {
43
46
  const secure = options.baseURL.startsWith("https://");
44
47
  const signIn = platformSignInFor(platformSignIn, Object.keys(options.socialProviders ?? {}));
45
48
  return {
@@ -60,32 +63,43 @@ const betterAuthOptions = (options, { roles, defaultRole, mailer, operator, plat
60
63
  ...mailer && passwordResetOptions(mailer)
61
64
  },
62
65
  ...options.socialProviders && { socialProviders: options.socialProviders },
66
+ user: organizations ? {
67
+ ...coreNames.user,
68
+ additionalFields: lastOrganizationField
69
+ } : coreNames.user,
70
+ session: coreNames.session,
71
+ verification: coreNames.verification,
63
72
  account: {
73
+ ...coreNames.account,
64
74
  accountLinking: { allowDifferentEmails: true },
65
75
  ...options.encryptOAuthTokens && { encryptOAuthTokens: true }
66
76
  },
67
77
  plugins: [
68
78
  admin({
79
+ schema: pluginNames.admin,
69
80
  defaultRole: defaultRole ?? "unassigned",
70
81
  roles: Object.fromEntries(roles.map((role) => [role, role === "admin" ? adminAc : userAc]))
71
82
  }),
72
83
  jwt({
84
+ schema: pluginNames.jwt,
73
85
  jwt: {
74
86
  expirationTime: tokenLifetime,
75
87
  issuer: options.baseURL,
76
88
  audience: options.baseURL,
77
- definePayload: ({ user }) => ({
89
+ definePayload: async ({ user, session }) => ({
78
90
  email: user.email,
79
- role: user.role
91
+ role: user.role,
92
+ ...organizations && await organizationClaims((await context()).adapter, user, session.activeOrganizationId)
80
93
  })
81
94
  },
82
95
  disableSettingJwtHeader: true
83
96
  }),
84
97
  twoFactorAfterPasswordOrCode({
98
+ schema: pluginNames.twoFactor,
85
99
  allowPasswordless: true,
86
100
  ...mailer && { otpOptions: twoFactorCodeOptions(mailer) }
87
101
  }),
88
- passkey(),
102
+ passkey({ schema: pluginNames.passkey }),
89
103
  ...mailer ? [emailOTP(emailCodeOptions(mailer))] : [],
90
104
  ...signIn ? [genericOAuth({ config: [platformSignInProvider(signIn)] })] : [],
91
105
  signInPolicyPlugin({
@@ -100,21 +114,37 @@ const betterAuthOptions = (options, { roles, defaultRole, mailer, operator, plat
100
114
  staffSignInPlugin({
101
115
  ...operator && { operator },
102
116
  ...mailer && { mailer }
103
- })
117
+ }),
118
+ ...organizations ? [organizationPlugin({
119
+ resolved: organizations,
120
+ secret: options.secret,
121
+ ...mailer && { mailer },
122
+ context
123
+ }), protobaseOrganizationPlugin({
124
+ resolved: organizations,
125
+ mail: Boolean(mailer)
126
+ })] : []
104
127
  ],
105
- disabledPaths: [...unusedEmailCodePaths, ...adminImpersonationPaths],
106
- databaseHooks: { user: { create: {
107
- before: async (user, context) => {
108
- const given = String(user.role ?? "").split(",").map((name) => name.trim()).filter(Boolean);
109
- const unknown = given.find((name) => !roles.includes(name));
110
- if (given.length === 0 || unknown !== void 0) throw new APIError("BAD_REQUEST", { message: `Choose a role for the new user: ${roles.join(", ")}` });
111
- return { data: context?.path === "/admin/create-user" ? {
112
- ...user,
113
- emailVerified: true
114
- } : user };
115
- },
116
- ...options.onUserCreated && { after: options.onUserCreated }
117
- } } },
128
+ disabledPaths: [
129
+ ...unusedEmailCodePaths,
130
+ ...adminImpersonationPaths,
131
+ ...organizations ? organizationPaths : []
132
+ ],
133
+ databaseHooks: {
134
+ user: { create: {
135
+ before: async (user, context) => {
136
+ const given = String(user.role ?? "").split(",").map((name) => name.trim()).filter(Boolean);
137
+ const unknown = given.find((name) => !roles.includes(name));
138
+ if (given.length === 0 || unknown !== void 0) throw new APIError("BAD_REQUEST", { message: `Choose a role for the new user: ${roles.join(", ")}` });
139
+ return { data: context?.path === "/admin/create-user" ? {
140
+ ...user,
141
+ emailVerified: true
142
+ } : user };
143
+ },
144
+ ...options.onUserCreated && { after: options.onUserCreated }
145
+ } },
146
+ ...organizations && { session: sessionOrganizationHooks(context) }
147
+ },
118
148
  rateLimit: {
119
149
  enabled: true,
120
150
  window: 60,
@@ -127,6 +157,10 @@ const betterAuthOptions = (options, { roles, defaultRole, mailer, operator, plat
127
157
  "/staff/sign-in": {
128
158
  window: 60,
129
159
  max: 5
160
+ },
161
+ "/invitation/sign-up": {
162
+ window: 60,
163
+ max: options.signInPerMinute ?? 5
130
164
  }
131
165
  }
132
166
  },
@@ -141,25 +175,36 @@ const betterAuthOptions = (options, { roles, defaultRole, mailer, operator, plat
141
175
  };
142
176
  };
143
177
  const createAuth = (options) => {
144
- const roles = normalizeRoles(options.roles);
178
+ const definitions = roleDefinitions(options.roles, { organizations: options.organizations !== void 0 });
179
+ const roles = definitions.names;
145
180
  if (options.defaultRole !== void 0 && !roles.includes(options.defaultRole)) throw new Error(`defaultRole "${options.defaultRole}" is not one of the roles: ${roles.join(", ")}`);
146
181
  const others = roles.filter((role) => role !== "admin");
147
- const defaultRole = options.defaultRole ?? (others.length === 1 ? others[0] : void 0);
182
+ const defaultRole = options.defaultRole ?? (options.organizations ? "user" : others.length === 1 ? others[0] : void 0);
148
183
  const mailer = options.mailer === void 0 ? platformMailer() : options.mailer || void 0;
149
184
  const provider = options.operator === void 0 ? platformOperator() : options.operator || void 0;
150
185
  const operator = provider && resolveOperator(provider);
151
186
  const signInSettings = options.signInProvider === void 0 ? platformSignInSettings() : options.signInProvider || void 0;
152
187
  const socialProviders = Object.keys(options.socialProviders ?? {});
153
188
  const platformSignIn = platformSignInFor(signInSettings && resolvePlatformSignIn(signInSettings), socialProviders);
189
+ const organizations = options.organizations && resolveOrganizations(options.organizations, definitions, options.baseURL);
190
+ let instance;
191
+ const context = () => instance?.$context ?? Promise.reject(/* @__PURE__ */ new Error("The auth instance is not made yet"));
154
192
  const auth = betterAuth(betterAuthOptions(options, {
155
193
  roles,
156
194
  defaultRole,
157
195
  mailer,
158
196
  operator,
159
- platformSignIn
197
+ platformSignIn,
198
+ ...organizations && {
199
+ organizations,
200
+ context
201
+ }
160
202
  }));
203
+ instance = auth;
161
204
  return Object.assign(auth, {
162
205
  roles,
206
+ roleDefinitions: definitions,
207
+ organizations,
163
208
  defaultRole,
164
209
  mail: Boolean(mailer),
165
210
  passwordReset: Boolean(mailer),
@@ -0,0 +1,80 @@
1
+ import { parseAppRoles } from "./app-roles.js";
2
+ import { findMember, membersOf } from "./store.js";
3
+ import { invitationIdOf, verifyInvitationToken } from "./invitation-token.js";
4
+ import { APIError } from "better-auth/api";
5
+ //#region src/better-auth/organizations/accept-invitation.ts
6
+ const invalid = {
7
+ code: "INVITATION_INVALID",
8
+ message: "This invitation link is not valid any more: it was used, revoked, replaced by a newer one, or it expired."
9
+ };
10
+ /** The pending invitation an emailed link's token names, or a 400 `INVITATION_INVALID`. */
11
+ const invitationOfToken = async (context, token) => {
12
+ const id = invitationIdOf(token);
13
+ const row = id && await context.adapter.findOne({
14
+ model: "invitation",
15
+ where: [{
16
+ field: "id",
17
+ value: id
18
+ }]
19
+ });
20
+ if (!row || !await verifyInvitationToken(context.secret, token, row)) throw new APIError("BAD_REQUEST", invalid);
21
+ if (row.status !== "pending" || new Date(row.expiresAt).getTime() < Date.now()) throw new APIError("BAD_REQUEST", invalid);
22
+ return {
23
+ ...row,
24
+ appRoles: parseAppRoles(row.appRoles)
25
+ };
26
+ };
27
+ /**
28
+ * Makes `user` a member as the invitation says, with its organization role and app roles, in one transaction with the
29
+ * invitation's acceptance, so a member never exists without the app roles they were invited with. Better Auth's own
30
+ * accept leaves the app roles out; it is turned off with organizations.
31
+ */
32
+ const acceptInvitation = async (context, invitation, user, membershipLimit) => {
33
+ if (invitation.email.toLowerCase() !== user.email.toLowerCase()) throw new APIError("FORBIDDEN", {
34
+ code: "NOT_THE_INVITED_ADDRESS",
35
+ message: `This invitation is for ${invitation.email}; sign in with that address to accept it.`
36
+ });
37
+ if (!user.emailVerified) throw new APIError("FORBIDDEN", {
38
+ code: "EMAIL_NOT_VERIFIED",
39
+ message: "Verify your email address before accepting an invitation."
40
+ });
41
+ if (await findMember(context.adapter, {
42
+ organizationId: invitation.organizationId,
43
+ userId: user.id
44
+ })) throw new APIError("BAD_REQUEST", {
45
+ code: "ALREADY_A_MEMBER",
46
+ message: "You are a member of this organization already."
47
+ });
48
+ if ((await membersOf(context.adapter, invitation.organizationId)).length >= membershipLimit) throw new APIError("FORBIDDEN", {
49
+ code: "MEMBERSHIP_LIMIT_REACHED",
50
+ message: "This organization has as many members as it can have."
51
+ });
52
+ return context.adapter.transaction(async (trx) => {
53
+ if (!await trx.update({
54
+ model: "invitation",
55
+ where: [{
56
+ field: "id",
57
+ value: invitation.id
58
+ }, {
59
+ field: "status",
60
+ value: "pending"
61
+ }],
62
+ update: { status: "accepted" }
63
+ })) throw new APIError("BAD_REQUEST", invalid);
64
+ return {
65
+ ...await trx.create({
66
+ model: "member",
67
+ data: {
68
+ organizationId: invitation.organizationId,
69
+ userId: user.id,
70
+ role: invitation.role,
71
+ appRoles: invitation.appRoles,
72
+ createdAt: /* @__PURE__ */ new Date()
73
+ }
74
+ }),
75
+ appRoles: invitation.appRoles
76
+ };
77
+ });
78
+ };
79
+ //#endregion
80
+ export { acceptInvitation, invitationOfToken };
@@ -0,0 +1,12 @@
1
+ //#region src/better-auth/organizations/active-organization.ts
2
+ /**
3
+ * The organization a new session starts in: the last one the person worked in while they may still enter it (a member
4
+ * there, or holding a global role, which enters any that still exists), else their oldest membership, else none.
5
+ */
6
+ const initialOrganization = (input) => {
7
+ const { memberships, last } = input;
8
+ if (last && (memberships.some((membership) => membership.organizationId === last) || input.entersAny && input.lastExists)) return last;
9
+ return [...memberships].sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime())[0]?.organizationId;
10
+ };
11
+ //#endregion
12
+ export { initialOrganization };
@@ -0,0 +1,83 @@
1
+ import { globalRoles } from "./app-roles.js";
2
+ import { findMember, findOrganization } from "./store.js";
3
+ import { auditOrigin } from "../../audit/origin.js";
4
+ import { Hono } from "hono";
5
+ import { exactCount } from "@protobase/query";
6
+ //#region src/better-auth/organizations/admin-routes.ts
7
+ /** The resources with rows in an organization, soft-deleted ones too: they would point at nothing once it is gone. */
8
+ const resourcesWithRecords = async (registry, db, organizationId) => {
9
+ const names = [];
10
+ for (const entry of registry.entries) {
11
+ if (!entry.model.tenant) continue;
12
+ const { softDelete: _, ...model } = entry.model;
13
+ if (await exactCount(db, model, void 0, { tenantValue: organizationId }) > 0) names.push(entry.name);
14
+ }
15
+ return names;
16
+ };
17
+ const refusal = (status, code, message) => ({
18
+ status,
19
+ code,
20
+ message
21
+ });
22
+ /**
23
+ * The organization routes that need the admin's own parts, mounted before Better Auth's handler: switching the
24
+ * organization one works in, which writes an audit event when someone enters one they are not a member of, and, while
25
+ * `deleteRecords` is `'refuse'`, deleting only an organization without records.
26
+ */
27
+ const organizationRoutes = ({ auth, audit, registry, db }) => {
28
+ const app = new Hono();
29
+ const contextOf = async () => await auth.$context;
30
+ const fail = (c, problem) => c.json({
31
+ code: problem.code,
32
+ message: problem.message
33
+ }, problem.status);
34
+ /**
35
+ * `POST /organization/switch` with `{ organizationId }`: the session works in that organization from its next token
36
+ * on, for a member, or for someone with a global role, who may enter any; `null` leaves it.
37
+ */
38
+ app.post("/organization/switch", async (c) => {
39
+ const session = await auth.api.getSession({ headers: c.req.raw.headers });
40
+ if (!session) return fail(c, refusal(401, "UNAUTHORIZED", "Sign in first."));
41
+ const organizationId = (await c.req.json().catch(() => void 0))?.organizationId;
42
+ if (organizationId !== null && typeof organizationId !== "string") return fail(c, refusal(400, "ORGANIZATION_REQUIRED", "Name the organization to work in."));
43
+ const { adapter, internalAdapter } = await contextOf();
44
+ if (organizationId !== null) {
45
+ const member = await findMember(adapter, {
46
+ organizationId,
47
+ userId: session.user.id
48
+ });
49
+ const global = globalRoles(session.user.role);
50
+ if (!member) {
51
+ if (global.length === 0 || !await findOrganization(adapter, organizationId)) return fail(c, refusal(403, "NOT_A_MEMBER", "You are not a member of this organization."));
52
+ await audit.publish({
53
+ type: "organization.entered",
54
+ at: (/* @__PURE__ */ new Date()).toISOString(),
55
+ actor: {
56
+ id: session.user.id,
57
+ roles: global
58
+ },
59
+ organization: organizationId,
60
+ origin: auditOrigin(c.req.raw.headers)
61
+ });
62
+ }
63
+ }
64
+ await internalAdapter.updateSession(session.session.token, { activeOrganizationId: organizationId });
65
+ return c.json({ organizationId });
66
+ });
67
+ if (auth.organizations?.deleteRecords !== "cascade") app.post("/organization/delete", async (c, next) => {
68
+ const body = await c.req.raw.clone().json().catch(() => void 0);
69
+ const session = await auth.api.getSession({ headers: c.req.raw.headers });
70
+ if (!session || typeof body?.organizationId !== "string") return next();
71
+ const member = await findMember((await contextOf()).adapter, {
72
+ organizationId: body.organizationId,
73
+ userId: session.user.id
74
+ });
75
+ if (!member || !member.role.split(",").includes("owner")) return next();
76
+ const names = await resourcesWithRecords(registry, db, body.organizationId);
77
+ if (names.length > 0) return fail(c, refusal(409, "ORGANIZATION_HAS_RECORDS", `Delete the organization's records first: ${names.join(", ")}.`));
78
+ return next();
79
+ });
80
+ return app;
81
+ };
82
+ //#endregion
83
+ export { organizationRoutes, resourcesWithRecords };
@@ -0,0 +1,43 @@
1
+ import { parseRoles } from "../parse-roles.js";
2
+ import { membershipRoles, noGlobalRole } from "./role-definitions.js";
3
+ //#region src/better-auth/organizations/app-roles.ts
4
+ /** A user's global roles, from the admin plugin's `role` column; the placeholder `user` is none. */
5
+ const globalRoles = (role) => parseRoles(role).filter((name) => name !== noGlobalRole);
6
+ /** App roles stored on a membership or an invitation: a JSON array, as the store returns it or as text. */
7
+ const parseAppRoles = (value) => {
8
+ const list = typeof value === "string" ? JSON.parse(value) : value;
9
+ return Array.isArray(list) ? list.filter((name) => typeof name === "string") : [];
10
+ };
11
+ /** The roles a request has: the global ones, then the membership's in the active organization, each once. */
12
+ const effectiveRoles = (global, membership = []) => [.../* @__PURE__ */ new Set([...global, ...membership])];
13
+ /**
14
+ * The roles someone may give or take away: those they hold, globally or in the organization, and those a held role
15
+ * lists in its `grants`. Grants do not chain. A superuser may give every role.
16
+ */
17
+ const grantableRoles = (held, definitions) => {
18
+ if (held.includes("admin")) return new Set(definitions.names);
19
+ return /* @__PURE__ */ new Set([...held, ...held.flatMap((name) => definitions.grants[name] ?? [])]);
20
+ };
21
+ /**
22
+ * Whether someone holding `held` may change a membership's app roles from `before` to `after`: every role added or
23
+ * removed must be grantable by them, and every role in `after` one a member can hold. `undefined` when allowed.
24
+ */
25
+ const checkAppRoleChange = (input) => {
26
+ const allowed = membershipRoles(input.definitions);
27
+ const unknown = input.after.filter((name) => !allowed.includes(name));
28
+ if (unknown.length > 0) return {
29
+ status: "BAD_REQUEST",
30
+ code: "UNKNOWN_APP_ROLE",
31
+ message: `A member cannot hold ${unknown.join(", ")}; choose from ${allowed.join(", ")}`
32
+ };
33
+ const grantable = grantableRoles(input.held, input.definitions);
34
+ const refused = [...input.after.filter((name) => !input.before.includes(name)), ...input.before.filter((name) => !input.after.includes(name))].filter((name) => !grantable.has(name));
35
+ if (refused.length === 0) return void 0;
36
+ return {
37
+ status: "FORBIDDEN",
38
+ code: "APP_ROLE_NOT_GRANTABLE",
39
+ message: `You cannot give or take away ${refused.map((name) => input.definitions.labels[name] ?? name).join(", ")}: hold the role, or one that grants it`
40
+ };
41
+ };
42
+ //#endregion
43
+ export { checkAppRoleChange, effectiveRoles, globalRoles, grantableRoles, parseAppRoles };
@@ -0,0 +1,53 @@
1
+ import { OrganizationRole, StoredMember, StoredOrganization } from "./store.js";
2
+ import { AdminAuth } from "../create-auth.js";
3
+ //#region src/better-auth/organizations/host.d.ts
4
+ export type NewOrganization = {
5
+ name: string;
6
+ slug: string;
7
+ owner: string;
8
+ id?: string;
9
+ };
10
+ /**
11
+ * Creates an organization with `owner` (an email) as its owner, holding the creator's app roles. `id` keeps an existing
12
+ * tenant value, for an app that had one organization before; otherwise the configured `generateId` makes one.
13
+ */
14
+ export declare const createOrganization: (auth: AdminAuth, input: NewOrganization) => Promise<{
15
+ organization: StoredOrganization;
16
+ member: {
17
+ appRoles: string[];
18
+ id: string;
19
+ organizationId: string;
20
+ userId: string;
21
+ role: string;
22
+ createdAt: Date;
23
+ };
24
+ }>;
25
+ export type MemberInput = {
26
+ organization: string;
27
+ email: string;
28
+ role?: string;
29
+ appRoles?: string[];
30
+ };
31
+ /** Adds someone with an account as a member: organization role `member` unless given, and the app roles given. */
32
+ export declare const addMember: (auth: AdminAuth, input: MemberInput) => Promise<StoredMember>;
33
+ /** Changes a member's organization role, app roles, or both. */
34
+ export declare const setMemberRoles: (auth: AdminAuth, input: MemberInput) => Promise<StoredMember | undefined>;
35
+ /** Removes a member; refuses the last owner. */
36
+ export declare const removeMember: (auth: AdminAuth, input: {
37
+ organization: string;
38
+ email: string;
39
+ }) => Promise<void>;
40
+ /** Every organization, oldest first, with its members' emails and roles. */
41
+ export declare const listOrganizations: (auth: AdminAuth) => Promise<{
42
+ id: string;
43
+ name: string;
44
+ slug: string;
45
+ members: {
46
+ email: string;
47
+ role: OrganizationRole;
48
+ appRoles: string[];
49
+ }[];
50
+ }[]>;
51
+ /** Gives an app role to every owner of every organization, for a role the project adds once organizations exist. */
52
+ export declare const addAppRoleToOwners: (auth: AdminAuth, appRole: string) => Promise<number>;
53
+ //#endregion