@protobase/server 0.7.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 (99) 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/authenticator.d.ts +7 -3
  9. package/dist/better-auth/authenticator.js +24 -3
  10. package/dist/better-auth/create-auth.d.ts +619 -81
  11. package/dist/better-auth/create-auth.js +64 -27
  12. package/dist/better-auth/organizations/accept-invitation.js +80 -0
  13. package/dist/better-auth/organizations/active-organization.js +12 -0
  14. package/dist/better-auth/organizations/admin-routes.js +83 -0
  15. package/dist/better-auth/organizations/app-roles.js +43 -0
  16. package/dist/better-auth/organizations/host.d.ts +53 -0
  17. package/dist/better-auth/organizations/host.js +251 -0
  18. package/dist/better-auth/organizations/invitation-endpoints.js +133 -0
  19. package/dist/better-auth/organizations/invitation-mail.js +17 -0
  20. package/dist/better-auth/organizations/invitation-token.js +27 -0
  21. package/dist/better-auth/organizations/member-endpoints.js +182 -0
  22. package/dist/better-auth/organizations/options.d.ts +48 -0
  23. package/dist/better-auth/organizations/options.js +23 -0
  24. package/dist/better-auth/organizations/organization-plugin.js +128 -0
  25. package/dist/better-auth/organizations/protobase-plugin.js +21 -0
  26. package/dist/better-auth/organizations/role-definitions.d.ts +15 -0
  27. package/dist/better-auth/organizations/role-definitions.js +56 -0
  28. package/dist/better-auth/organizations/session-organization.js +68 -0
  29. package/dist/better-auth/organizations/store.d.ts +19 -0
  30. package/dist/better-auth/organizations/store.js +73 -0
  31. package/dist/better-auth/staff-checks.js +5 -1
  32. package/dist/better-auth/staff-sign-in-endpoints.js +1 -1
  33. package/dist/better-auth/status-route.js +14 -3
  34. package/dist/better-auth/users.d.ts +2 -1
  35. package/dist/better-auth/users.js +4 -1
  36. package/dist/create-admin.d.ts +8 -2
  37. package/dist/create-admin.js +31 -2
  38. package/dist/deps.d.ts +1 -0
  39. package/dist/files/attach.js +80 -0
  40. package/dist/files/cleanup-runner.d.ts +8 -0
  41. package/dist/files/cleanup-runner.js +58 -0
  42. package/dist/files/cleanup-schedule.d.ts +28 -0
  43. package/dist/files/cleanup-schedule.js +48 -0
  44. package/dist/files/detect.js +51 -0
  45. package/dist/files/download-url.js +43 -0
  46. package/dist/files/download.js +90 -0
  47. package/dist/files/duration.js +35 -0
  48. package/dist/files/file-cleanup.d.ts +18 -0
  49. package/dist/files/file-cleanup.js +17 -0
  50. package/dist/files/local-files.d.ts +17 -0
  51. package/dist/files/local-files.js +101 -0
  52. package/dist/files/media-types.js +99 -0
  53. package/dist/files/options.d.ts +15 -0
  54. package/dist/files/present.d.ts +15 -0
  55. package/dist/files/present.js +59 -0
  56. package/dist/files/processors.d.ts +15 -0
  57. package/dist/files/processors.js +39 -0
  58. package/dist/files/providers.js +70 -0
  59. package/dist/files/routes.js +26 -0
  60. package/dist/files/runtime.d.ts +3 -0
  61. package/dist/files/runtime.js +37 -0
  62. package/dist/files/signer.js +32 -0
  63. package/dist/files/store.d.ts +37 -0
  64. package/dist/files/ticket.js +87 -0
  65. package/dist/files/upload-body.js +85 -0
  66. package/dist/files/upload.js +124 -0
  67. package/dist/files/uri.js +47 -0
  68. package/dist/functions/define-function.d.ts +71 -0
  69. package/dist/functions/define-function.js +36 -0
  70. package/dist/functions/function-app.d.ts +17 -0
  71. package/dist/functions/function-app.js +19 -0
  72. package/dist/functions/read-json.d.ts +35 -0
  73. package/dist/functions/read-json.js +24 -0
  74. package/dist/functions/records.d.ts +43 -0
  75. package/dist/functions/records.js +31 -0
  76. package/dist/functions/require-env.d.ts +7 -0
  77. package/dist/functions/require-env.js +12 -0
  78. package/dist/functions/routes.js +60 -0
  79. package/dist/functions/verify-signature.d.ts +32 -0
  80. package/dist/functions/verify-signature.js +49 -0
  81. package/dist/index.d.ts +21 -3
  82. package/dist/index.js +13 -3
  83. package/dist/list-service.js +4 -2
  84. package/dist/openapi/resource-paths.js +78 -1
  85. package/dist/openapi/schemas.js +28 -3
  86. package/dist/project-config.d.ts +7 -1
  87. package/dist/project-config.js +37 -1
  88. package/dist/read-service.js +5 -1
  89. package/dist/request-access.d.ts +5 -0
  90. package/dist/request-access.js +9 -4
  91. package/dist/resource-source.d.ts +4 -1
  92. package/dist/routes/batch.js +1 -1
  93. package/dist/routes/collection.js +3 -1
  94. package/dist/routes/record.js +2 -5
  95. package/dist/types.d.ts +25 -5
  96. package/dist/write-pipeline.js +2 -1
  97. package/dist/write-shared.js +6 -3
  98. package/dist/write-update.js +2 -1
  99. package/package.json +6 -4
@@ -1,6 +1,11 @@
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";
@@ -28,19 +33,16 @@ const authBasePath = "/api/auth";
28
33
  * `createUser`, or by an admin through the admin plugin), roles from the admin plugin's `role` column, 15 minute JWTs
29
34
  * at `/api/auth/token` with the keys at `/api/auth/jwks`.
30
35
  */
31
- const roleName = /^[a-z][a-z0-9_-]*$/;
32
36
  /** The configured roles with `admin` first; `admin` is added when missing. */
33
- const normalizeRoles = (roles = ["admin", "user"]) => {
34
- const bad = roles.find((role) => !roleName.test(role));
35
- if (bad !== void 0) throw new Error(`Invalid role "${bad}": use lowercase letters, digits, "_" or "-"`);
36
- return ["admin", ...new Set(roles.filter((role) => role !== "admin"))];
37
- };
37
+ const normalizeRoles = (roles) => roleDefinitions(roles).names;
38
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"];
39
41
  /**
40
42
  * The options `createAuth` gives Better Auth, with the roles, default role, mailer, operator provider and platform
41
43
  * sign-in provider already resolved.
42
44
  */
43
- const betterAuthOptions = (options, { roles, defaultRole, mailer, operator, platformSignIn }) => {
45
+ const betterAuthOptions = (options, { roles, defaultRole, mailer, operator, platformSignIn, organizations, context = noContext }) => {
44
46
  const secure = options.baseURL.startsWith("https://");
45
47
  const signIn = platformSignInFor(platformSignIn, Object.keys(options.socialProviders ?? {}));
46
48
  return {
@@ -61,7 +63,10 @@ const betterAuthOptions = (options, { roles, defaultRole, mailer, operator, plat
61
63
  ...mailer && passwordResetOptions(mailer)
62
64
  },
63
65
  ...options.socialProviders && { socialProviders: options.socialProviders },
64
- user: coreNames.user,
66
+ user: organizations ? {
67
+ ...coreNames.user,
68
+ additionalFields: lastOrganizationField
69
+ } : coreNames.user,
65
70
  session: coreNames.session,
66
71
  verification: coreNames.verification,
67
72
  account: {
@@ -81,9 +86,10 @@ const betterAuthOptions = (options, { roles, defaultRole, mailer, operator, plat
81
86
  expirationTime: tokenLifetime,
82
87
  issuer: options.baseURL,
83
88
  audience: options.baseURL,
84
- definePayload: ({ user }) => ({
89
+ definePayload: async ({ user, session }) => ({
85
90
  email: user.email,
86
- role: user.role
91
+ role: user.role,
92
+ ...organizations && await organizationClaims((await context()).adapter, user, session.activeOrganizationId)
87
93
  })
88
94
  },
89
95
  disableSettingJwtHeader: true
@@ -108,21 +114,37 @@ const betterAuthOptions = (options, { roles, defaultRole, mailer, operator, plat
108
114
  staffSignInPlugin({
109
115
  ...operator && { operator },
110
116
  ...mailer && { mailer }
111
- })
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
+ })] : []
112
127
  ],
113
- disabledPaths: [...unusedEmailCodePaths, ...adminImpersonationPaths],
114
- databaseHooks: { user: { create: {
115
- before: async (user, context) => {
116
- const given = String(user.role ?? "").split(",").map((name) => name.trim()).filter(Boolean);
117
- const unknown = given.find((name) => !roles.includes(name));
118
- if (given.length === 0 || unknown !== void 0) throw new APIError("BAD_REQUEST", { message: `Choose a role for the new user: ${roles.join(", ")}` });
119
- return { data: context?.path === "/admin/create-user" ? {
120
- ...user,
121
- emailVerified: true
122
- } : user };
123
- },
124
- ...options.onUserCreated && { after: options.onUserCreated }
125
- } } },
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
+ },
126
148
  rateLimit: {
127
149
  enabled: true,
128
150
  window: 60,
@@ -135,6 +157,10 @@ const betterAuthOptions = (options, { roles, defaultRole, mailer, operator, plat
135
157
  "/staff/sign-in": {
136
158
  window: 60,
137
159
  max: 5
160
+ },
161
+ "/invitation/sign-up": {
162
+ window: 60,
163
+ max: options.signInPerMinute ?? 5
138
164
  }
139
165
  }
140
166
  },
@@ -149,25 +175,36 @@ const betterAuthOptions = (options, { roles, defaultRole, mailer, operator, plat
149
175
  };
150
176
  };
151
177
  const createAuth = (options) => {
152
- const roles = normalizeRoles(options.roles);
178
+ const definitions = roleDefinitions(options.roles, { organizations: options.organizations !== void 0 });
179
+ const roles = definitions.names;
153
180
  if (options.defaultRole !== void 0 && !roles.includes(options.defaultRole)) throw new Error(`defaultRole "${options.defaultRole}" is not one of the roles: ${roles.join(", ")}`);
154
181
  const others = roles.filter((role) => role !== "admin");
155
- 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);
156
183
  const mailer = options.mailer === void 0 ? platformMailer() : options.mailer || void 0;
157
184
  const provider = options.operator === void 0 ? platformOperator() : options.operator || void 0;
158
185
  const operator = provider && resolveOperator(provider);
159
186
  const signInSettings = options.signInProvider === void 0 ? platformSignInSettings() : options.signInProvider || void 0;
160
187
  const socialProviders = Object.keys(options.socialProviders ?? {});
161
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"));
162
192
  const auth = betterAuth(betterAuthOptions(options, {
163
193
  roles,
164
194
  defaultRole,
165
195
  mailer,
166
196
  operator,
167
- platformSignIn
197
+ platformSignIn,
198
+ ...organizations && {
199
+ organizations,
200
+ context
201
+ }
168
202
  }));
203
+ instance = auth;
169
204
  return Object.assign(auth, {
170
205
  roles,
206
+ roleDefinitions: definitions,
207
+ organizations,
171
208
  defaultRole,
172
209
  mail: Boolean(mailer),
173
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