@protobase/server 0.21.12 → 0.22.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.
Files changed (28) hide show
  1. package/dist/better-auth/auth-mail/invitation-email.js +23 -27
  2. package/dist/better-auth/create-auth.d.ts +462 -24
  3. package/dist/better-auth/create-auth.js +12 -1
  4. package/dist/better-auth/invitations/account-from-invitation.js +50 -0
  5. package/dist/better-auth/invitations/invitation-endpoints.js +102 -0
  6. package/dist/better-auth/invitations/invitation-link-hook.js +34 -0
  7. package/dist/better-auth/invitations/invitation-of-token.js +24 -0
  8. package/dist/better-auth/{organizations → invitations}/invitation-token.js +7 -2
  9. package/dist/better-auth/last-admin-guard.js +62 -0
  10. package/dist/better-auth/last-admin.js +18 -0
  11. package/dist/better-auth/organizations/accept-invitation.js +4 -8
  12. package/dist/better-auth/organizations/invitation-endpoints.js +17 -114
  13. package/dist/better-auth/organizations/member-endpoints.js +36 -19
  14. package/dist/better-auth/organizations/options.js +2 -1
  15. package/dist/better-auth/organizations/organization-plugin.js +1 -1
  16. package/dist/better-auth/organizations/protobase-plugin.js +7 -9
  17. package/dist/better-auth/organizations/role-definitions.js +5 -4
  18. package/dist/better-auth/passkey-plugin.js +1 -1
  19. package/dist/better-auth/roles/role-labels.js +14 -0
  20. package/dist/better-auth/sign-up/sign-up-plugin.js +1 -1
  21. package/dist/better-auth/status-route.js +3 -8
  22. package/dist/better-auth/users/app-invitation-store.js +62 -0
  23. package/dist/better-auth/users/user-endpoints.js +79 -0
  24. package/dist/better-auth/users/users-plugin.js +22 -0
  25. package/dist/better-auth/users.js +1 -2
  26. package/dist/check-views.js +5 -3
  27. package/dist/index.js +1 -1
  28. package/package.json +4 -4
@@ -1,7 +1,7 @@
1
- import { membershipRoles } from "./organizations/role-definitions.js";
1
+ import { signInPasskeyDomains } from "./passkey-domain.js";
2
2
  import { effectiveSignInPolicy } from "./sign-in-policy.js";
3
3
  import { readSignInPolicy } from "./policy-store.js";
4
- import { signInPasskeyDomains } from "./passkey-domain.js";
4
+ import { roleLabels } from "./roles/role-labels.js";
5
5
  import { hasUsers } from "./users.js";
6
6
  import { Hono } from "hono";
7
7
  //#region src/better-auth/status-route.ts
@@ -46,12 +46,7 @@ const statusRoute = (auth, { anonymous = false, plugins = [] } = {}) => {
46
46
  ...plugins.length > 0 && { plugins },
47
47
  ...auth.organizations && { organizations: {
48
48
  create: auth.organizations.create,
49
- roles: auth.roleDefinitions.names.map((name) => ({
50
- name,
51
- label: auth.roleDefinitions.labels[name] ?? name,
52
- membership: membershipRoles(auth.roleDefinitions).includes(name),
53
- grants: auth.roleDefinitions.grants[name] ?? []
54
- }))
49
+ roles: roleLabels(auth.roleDefinitions)
55
50
  } }
56
51
  });
57
52
  });
@@ -0,0 +1,62 @@
1
+ //#region src/better-auth/users/app-invitation-store.ts
2
+ const prefix = "app-invitation:";
3
+ const invitationOf = (row) => {
4
+ const { email, roles, inviterId } = JSON.parse(row.value);
5
+ return {
6
+ id: row.identifier.slice(15),
7
+ email,
8
+ roles,
9
+ inviterId,
10
+ expiresAt: new Date(row.expiresAt)
11
+ };
12
+ };
13
+ const pending = (invitation) => invitation.expiresAt.getTime() > Date.now();
14
+ const allAppInvitations = async (context) => {
15
+ return (await context.adapter.findMany({
16
+ model: "verification",
17
+ where: [{
18
+ field: "identifier",
19
+ operator: "starts_with",
20
+ value: prefix
21
+ }],
22
+ sortBy: {
23
+ field: "createdAt",
24
+ direction: "asc"
25
+ },
26
+ limit: 1e5
27
+ })).map(invitationOf);
28
+ };
29
+ /** The pending app invitations, oldest first; expired ones are left out. */
30
+ const listAppInvitations = async (context) => (await allAppInvitations(context)).filter(pending);
31
+ /** The pending app invitation with `id`, or `undefined` when there is none or it expired. */
32
+ const findAppInvitation = async (context, id) => {
33
+ const row = await context.internalAdapter.findVerificationValue(`${prefix}${id}`);
34
+ const invitation = row ? invitationOf(row) : void 0;
35
+ return invitation && pending(invitation) ? invitation : void 0;
36
+ };
37
+ const deleteAppInvitation = (context, id) => context.internalAdapter.deleteVerificationByIdentifier(`${prefix}${id}`);
38
+ /** Invites `email` with `roles` for `lifetimeSeconds`; an earlier invitation of the same address is replaced, so its link stops working. */
39
+ const createAppInvitation = async (context, input, lifetimeSeconds) => {
40
+ const email = input.email.toLowerCase();
41
+ const earlier = (await allAppInvitations(context)).filter((invitation) => invitation.email === email);
42
+ for (const invitation of earlier) await deleteAppInvitation(context, invitation.id);
43
+ const invitation = {
44
+ id: crypto.randomUUID(),
45
+ email,
46
+ roles: input.roles,
47
+ inviterId: input.inviterId,
48
+ expiresAt: new Date(Date.now() + lifetimeSeconds * 1e3)
49
+ };
50
+ await context.internalAdapter.createVerificationValue({
51
+ identifier: `${prefix}${invitation.id}`,
52
+ value: JSON.stringify({
53
+ email,
54
+ roles: invitation.roles,
55
+ inviterId: invitation.inviterId
56
+ }),
57
+ expiresAt: invitation.expiresAt
58
+ });
59
+ return invitation;
60
+ };
61
+ //#endregion
62
+ export { createAppInvitation, deleteAppInvitation, findAppInvitation, listAppInvitations };
@@ -0,0 +1,79 @@
1
+ import { invitationLink, invitationToken } from "../invitations/invitation-token.js";
2
+ import { sendAuthEmail } from "../auth-mail/send-auth-email.js";
3
+ import { adminOnly } from "../admin-only.js";
4
+ import { createAppInvitation, deleteAppInvitation, listAppInvitations } from "./app-invitation-store.js";
5
+ import { roleLabels } from "../roles/role-labels.js";
6
+ import * as z$1 from "zod";
7
+ import { APIError, createAuthEndpoint } from "better-auth/api";
8
+ //#region src/better-auth/users/user-endpoints.ts
9
+ const admins = adminOnly("Only an admin manages users.");
10
+ const answerOf = ({ id, email, roles, expiresAt }) => ({
11
+ id,
12
+ email,
13
+ roles,
14
+ expiresAt: expiresAt.toISOString()
15
+ });
16
+ /**
17
+ * What an admin manages people with besides Better Auth's admin endpoints, which list, create, set roles and block:
18
+ * invitations to the app by email, which work with or without organizations, and the roles with their labels.
19
+ */
20
+ const userEndpoints = ({ definitions, mail, invitationUrl, invitationSeconds }) => ({
21
+ /**
22
+ * `POST /users/invite` with `{ email, roles }`: invites an address without an account to make one, with `roles`, from
23
+ * the link it is mailed. Inviting it again replaces the earlier invitation, so its link stops working.
24
+ */
25
+ inviteUser: createAuthEndpoint("/users/invite", {
26
+ method: "POST",
27
+ use: [admins],
28
+ body: z$1.object({
29
+ email: z$1.email(),
30
+ roles: z$1.array(z$1.string()).min(1)
31
+ })
32
+ }, async (ctx) => {
33
+ const unknown = ctx.body.roles.find((role) => !definitions.names.includes(role));
34
+ if (unknown !== void 0) throw new APIError("BAD_REQUEST", {
35
+ code: "UNKNOWN_ROLE",
36
+ message: `Unknown role "${unknown}"; use ${definitions.names.join(", ")}`
37
+ });
38
+ if (await ctx.context.internalAdapter.findUserByEmail(ctx.body.email.toLowerCase())) throw new APIError("BAD_REQUEST", {
39
+ code: "ACCOUNT_EXISTS",
40
+ message: "This address has an account already."
41
+ });
42
+ const inviter = ctx.context.session.user;
43
+ const invitation = await createAppInvitation(ctx.context, {
44
+ email: ctx.body.email,
45
+ roles: [...new Set(ctx.body.roles)],
46
+ inviterId: inviter.id
47
+ }, invitationSeconds);
48
+ if (mail) {
49
+ const url = invitationLink(invitationUrl, await invitationToken(ctx.context.secret, invitation));
50
+ await sendAuthEmail(mail, "invitationEmail", invitation.email, {
51
+ inviter: inviter.name || inviter.email,
52
+ url,
53
+ expiresAt: invitation.expiresAt
54
+ }, url);
55
+ }
56
+ return ctx.json(answerOf(invitation));
57
+ }),
58
+ /** `GET /users/invitations`: the pending invitations to the app, oldest first. */
59
+ listUserInvitations: createAuthEndpoint("/users/invitations", {
60
+ method: "GET",
61
+ use: [admins]
62
+ }, async (ctx) => ctx.json({ invitations: (await listAppInvitations(ctx.context)).map(answerOf) })),
63
+ /** `POST /users/invitations/cancel` with `{ id }`: revokes an invitation to the app, so its link stops working. */
64
+ cancelUserInvitation: createAuthEndpoint("/users/invitations/cancel", {
65
+ method: "POST",
66
+ use: [admins],
67
+ body: z$1.object({ id: z$1.string().min(1) })
68
+ }, async (ctx) => {
69
+ await deleteAppInvitation(ctx.context, ctx.body.id);
70
+ return ctx.json({ id: ctx.body.id });
71
+ }),
72
+ /** `GET /users/roles`: every role with its label, to choose from when inviting, creating or changing a user. */
73
+ userRoles: createAuthEndpoint("/users/roles", {
74
+ method: "GET",
75
+ use: [admins]
76
+ }, async (ctx) => ctx.json(roleLabels(definitions)))
77
+ });
78
+ //#endregion
79
+ export { userEndpoints };
@@ -0,0 +1,22 @@
1
+ import { invitationLinkHook } from "../invitations/invitation-link-hook.js";
2
+ import { invitationEndpoints } from "../invitations/invitation-endpoints.js";
3
+ import { userEndpoints } from "./user-endpoints.js";
4
+ //#region src/better-auth/users/users-plugin.ts
5
+ /**
6
+ * Managing people without the command line: invitations to the app and the roles to give, for admins, and the endpoints
7
+ * every invitation link opens, to the app or, with `organizations`, to one of them. Without a mailer, whoever invites
8
+ * gets the link in the answer.
9
+ */
10
+ const usersPlugin = (options) => ({
11
+ id: "protobase-users",
12
+ endpoints: {
13
+ ...userEndpoints(options),
14
+ ...invitationEndpoints({
15
+ mail: Boolean(options.mail),
16
+ ...options.organizations && { organizations: options.organizations }
17
+ })
18
+ },
19
+ hooks: { after: options.mail ? [] : [invitationLinkHook(["/users/invite"], options.invitationUrl)] }
20
+ });
21
+ //#endregion
22
+ export { usersPlugin };
@@ -1,4 +1,5 @@
1
1
  import { parseRoles } from "./parse-roles.js";
2
+ import { isActiveAdmin } from "./last-admin.js";
2
3
  import { ownOrganizationsWithoutMembers } from "./organizations/host.js";
3
4
  //#region src/better-auth/users.ts
4
5
  /** The roles this auth instance allows, `admin` first. */
@@ -61,8 +62,6 @@ const allUsers = async (auth) => {
61
62
  });
62
63
  };
63
64
  const rolesOf = (user) => parseRoles(user.role);
64
- const isAdmin = (user) => rolesOf(user).includes("admin");
65
- const isActiveAdmin = (user) => isAdmin(user) && !user.banned;
66
65
  const findUser = async (auth, email) => {
67
66
  const user = (await allUsers(auth)).find((candidate) => candidate.email.toLowerCase() === email.toLowerCase());
68
67
  if (!user) throw new Error(`No user with email ${email}`);
@@ -7,7 +7,7 @@ import { mapDisplayErrors, mapErrors } from "./view-map.js";
7
7
  import { relatedColumnErrors } from "./view-related-column.js";
8
8
  import { editableErrors } from "./view-editable.js";
9
9
  import { conditionProblems } from "@protobase/layout";
10
- import { actionInputErrors, addressErrors, addressPlaceholders, boardErrors, calendarErrors, chartFields, displayFields, fieldRefsIn, hrefField, hrefPlaceholders, layoutItems, linkActionError, promptError, promptFields } from "@protobase/schema/internal";
10
+ import { actionInputErrors, addressErrors, addressPlaceholders, boardErrors, calendarErrors, chartFields, displayFields, fieldRefsIn, hrefField, hrefPlaceholders, layoutItems, linkActionError, promptError, promptFields, updateSetErrors } from "@protobase/schema/internal";
11
11
  import { isRecordCondition } from "@protobase/schema";
12
12
  //#region src/check-views.ts
13
13
  const linkFields = (run) => run?.kind !== "link" ? [] : "to" in run ? addressPlaceholders(run.to) : hrefPlaceholders(run.href);
@@ -85,9 +85,11 @@ const aiActionErrors = (view, model) => view.actions.flatMap(({ name, run }) =>
85
85
  ].map((error) => `action "${name}": ${error}`);
86
86
  });
87
87
  const inputErrors = (view, models) => view.actions.flatMap(({ name, run, inputs }) => {
88
- if (!inputs) return [];
88
+ const update = run?.kind === "update" ? updateSetErrors(run.values, inputs) : [];
89
+ if (!inputs) return update.map((error) => `action "${name}": ${error}`);
89
90
  return [
90
- ...run ? [`inputs need a handler, and a${run.kind === "update" || run.kind === "ai" ? "n" : ""} ${run.kind} action has none`] : [],
91
+ ...update,
92
+ ...run && run.kind !== "update" ? [`inputs need a handler, and a${run.kind === "ai" ? "n" : ""} ${run.kind} action has none`] : [],
91
93
  ...actionInputErrors(inputs),
92
94
  ...inputs.flatMap((input) => input.resource !== void 0 && !models[input.resource] ? [`input "${input.name}" picks from resource "${input.resource}", which the app does not have`] : [])
93
95
  ].map((error) => `action "${name}": ${error}`);
package/dist/index.js CHANGED
@@ -19,8 +19,8 @@ import { consoleAuditQueue } from "./audit/console-queue.js";
19
19
  import { webauthnPath } from "./better-auth/origins.js";
20
20
  import { readOperatorSettings } from "./better-auth/operator-provider.js";
21
21
  import { parseRoles } from "./better-auth/parse-roles.js";
22
- import { defaultSignInPolicy, signInPolicyMethods } from "./better-auth/sign-in-policy.js";
23
22
  import { readPlatformSignIn } from "./better-auth/platform-sign-in.js";
23
+ import { defaultSignInPolicy, signInPolicyMethods } from "./better-auth/sign-in-policy.js";
24
24
  import { createAuth, normalizeRoles } from "./better-auth/create-auth.js";
25
25
  import { addAppRoleToOwners, addMember, createOrganization, listOrganizations, removeMember, setMemberRoles } from "./better-auth/organizations/host.js";
26
26
  import { createUser, deleteUser, hasUsers, listUsers, roleChoices, setUserBanned, setUserRole } from "./better-auth/users.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@protobase/server",
3
- "version": "0.21.12",
3
+ "version": "0.22.1",
4
4
  "description": "The Protobase REST API as a Hono app, with Better Auth sign-in.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -31,9 +31,9 @@
31
31
  "dependencies": {
32
32
  "@better-auth/passkey": "^1.7.7",
33
33
  "@better-auth/sso": "^1.7.7",
34
- "@protobase/layout": "0.21.12",
35
- "@protobase/query": "0.21.12",
36
- "@protobase/schema": "0.21.12",
34
+ "@protobase/layout": "0.22.1",
35
+ "@protobase/query": "0.22.1",
36
+ "@protobase/schema": "0.22.1",
37
37
  "@react-email/render": "^2.1.0",
38
38
  "@scalar/hono-api-reference": "^0.12.10",
39
39
  "@sentry/core": "^11.7.0",