@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
@@ -0,0 +1,128 @@
1
+ import "./role-definitions.js";
2
+ import { checkAppRoleChange, effectiveRoles, globalRoles, parseAppRoles } from "./app-roles.js";
3
+ import { findMember } from "./store.js";
4
+ import { invitationEmail } from "./invitation-mail.js";
5
+ import { invitationLink, invitationToken } from "./invitation-token.js";
6
+ import { APIError } from "better-auth/api";
7
+ import { organization } from "better-auth/plugins/organization";
8
+ //#region src/better-auth/organizations/organization-plugin.ts
9
+ /** The snake_case names of the organization plugin's tables and columns, with the app roles of members and invitations. */
10
+ const organizationNames = {
11
+ organization: { fields: { createdAt: "created_at" } },
12
+ member: {
13
+ fields: {
14
+ organizationId: "organization_id",
15
+ userId: "user_id",
16
+ createdAt: "created_at"
17
+ },
18
+ additionalFields: { appRoles: {
19
+ type: "string[]",
20
+ required: false,
21
+ defaultValue: [],
22
+ input: false,
23
+ fieldName: "app_roles"
24
+ } }
25
+ },
26
+ invitation: {
27
+ fields: {
28
+ organizationId: "organization_id",
29
+ inviterId: "inviter_id",
30
+ expiresAt: "expires_at",
31
+ createdAt: "created_at"
32
+ },
33
+ additionalFields: { appRoles: {
34
+ type: "string[]",
35
+ required: false,
36
+ defaultValue: [],
37
+ input: true,
38
+ fieldName: "app_roles"
39
+ } }
40
+ },
41
+ session: { fields: { activeOrganizationId: "active_organization_id" } }
42
+ };
43
+ /** Runs Protobase's hook, then the project's, with the same data; data either returns is merged, the project's last. */
44
+ const mergeHooks = (own, project = {}) => {
45
+ const keys = /* @__PURE__ */ new Set([...Object.keys(own), ...Object.keys(project)]);
46
+ return Object.fromEntries([...keys].map((key) => {
47
+ const first = own[key];
48
+ const second = project[key];
49
+ if (!first || !second) return [key, first ?? second];
50
+ const both = async (...args) => {
51
+ const data = {
52
+ ...(await first(...args))?.data,
53
+ ...(await second(...args))?.data
54
+ };
55
+ return Object.keys(data).length > 0 ? { data } : void 0;
56
+ };
57
+ return [key, both];
58
+ }));
59
+ };
60
+ /**
61
+ * Better Auth's organization plugin as Protobase uses it: organization roles `owner`, `admin` and `member` with Better
62
+ * Auth's own access control, app roles on members and invitations, ids from `generateId`, creators holding
63
+ * `creatorAppRoles`, invitations checked against what the inviter may give and mailed with a signed link, and the
64
+ * project's hooks after Protobase's.
65
+ */
66
+ const organizationPlugin = ({ resolved, secret, mailer, context }) => {
67
+ const { definitions } = resolved;
68
+ const own = {
69
+ beforeCreateOrganization: async () => ({ data: { id: await resolved.generateId() } }),
70
+ beforeAddMember: async ({ member }) => member.role === "owner" && member.appRoles === void 0 ? { data: { appRoles: resolved.creatorAppRoles } } : void 0,
71
+ afterCreateOrganization: async ({ organization, member }) => {
72
+ await resolved.options.onCreated?.({
73
+ organization,
74
+ member: {
75
+ ...member,
76
+ appRoles: parseAppRoles(member.appRoles)
77
+ }
78
+ });
79
+ },
80
+ beforeCreateInvitation: async ({ invitation, inviter }) => {
81
+ const membership = await findMember((await context()).adapter, {
82
+ organizationId: invitation.organizationId,
83
+ userId: inviter.id
84
+ });
85
+ const held = effectiveRoles(globalRoles(inviter.role), membership?.appRoles);
86
+ const refusal = checkAppRoleChange({
87
+ held,
88
+ before: [],
89
+ after: parseAppRoles(invitation.appRoles),
90
+ definitions
91
+ });
92
+ if (refusal) throw new APIError(refusal.status, {
93
+ code: refusal.code,
94
+ message: refusal.message
95
+ });
96
+ },
97
+ afterRemoveMember: async ({ member, user }) => {
98
+ if (globalRoles(user.role).length > 0) return;
99
+ const { internalAdapter } = await context();
100
+ for (const session of await internalAdapter.listSessions(member.userId)) if (session.activeOrganizationId === member.organizationId) await internalAdapter.updateSession(session.token, { activeOrganizationId: null });
101
+ },
102
+ beforeDeleteOrganization: async ({ organization }) => {
103
+ await resolved.options.beforeDelete?.(organization);
104
+ }
105
+ };
106
+ return organization({
107
+ creatorRole: "owner",
108
+ allowUserToCreateOrganization: (user) => resolved.create === "everyone" || globalRoles(user.role).includes("admin"),
109
+ invitationExpiresIn: resolved.invitationExpiresIn,
110
+ membershipLimit: resolved.membershipLimit,
111
+ requireEmailVerificationOnInvitation: true,
112
+ cancelPendingInvitationsOnReInvite: true,
113
+ schema: organizationNames,
114
+ ...mailer && { sendInvitationEmail: async ({ invitation, organization, inviter }) => {
115
+ const token = await invitationToken(secret, invitation);
116
+ await mailer.send(invitationEmail({
117
+ to: invitation.email,
118
+ inviter: inviter.user.name || inviter.user.email,
119
+ organization: organization.name,
120
+ url: invitationLink(resolved.invitationUrl, token),
121
+ expiresAt: invitation.expiresAt
122
+ }));
123
+ } },
124
+ organizationHooks: mergeHooks(own, resolved.options.hooks)
125
+ });
126
+ };
127
+ //#endregion
128
+ export { organizationNames, organizationPlugin };
@@ -0,0 +1,21 @@
1
+ import { invitationEndpoints, invitationLinkHook } from "./invitation-endpoints.js";
2
+ import { memberEndpoints } from "./member-endpoints.js";
3
+ //#region src/better-auth/organizations/protobase-plugin.ts
4
+ /**
5
+ * Protobase's endpoints beside Better Auth's organization plugin: invitation links that also make accounts, app roles,
6
+ * handing ownership over, and finding organizations for people with a global role. Without a mailer, whoever invites
7
+ * gets the link in the answer.
8
+ */
9
+ const protobaseOrganizationPlugin = ({ resolved, mail }) => ({
10
+ id: "protobase-organizations",
11
+ endpoints: {
12
+ ...invitationEndpoints({
13
+ resolved,
14
+ mail
15
+ }),
16
+ ...memberEndpoints({ resolved })
17
+ },
18
+ hooks: { after: mail ? [] : [invitationLinkHook(resolved)] }
19
+ });
20
+ //#endregion
21
+ export { protobaseOrganizationPlugin };
@@ -0,0 +1,15 @@
1
+ //#region src/better-auth/organizations/role-definitions.d.ts
2
+ /** A role: its label, or its label and the other roles its holders may also give (see `grantableRoles`). */
3
+ export type RoleDefinition = string | {
4
+ label: string;
5
+ grants?: string[];
6
+ };
7
+ /** The roles of an app: names only, or each with its definition. */
8
+ export type RolesInput = string[] | Record<string, RoleDefinition>;
9
+ export type RoleDefinitions = {
10
+ /** `admin` first, then the others in the order given. */
11
+ names: string[];
12
+ labels: Record<string, string>;
13
+ grants: Record<string, string[]>;
14
+ };
15
+ //#endregion
@@ -0,0 +1,56 @@
1
+ //#region src/better-auth/organizations/role-definitions.ts
2
+ const roleName = /^[a-z][a-z0-9_-]*$/;
3
+ /** The superuser: holds every role in every organization. */
4
+ const superuserRole = "admin";
5
+ /** The admin plugin's role for an account without a global role; in organization mode it grants nothing. */
6
+ const noGlobalRole = "user";
7
+ const organizationRoleNames = ["owner", "member"];
8
+ const builtInLabels = {
9
+ [superuserRole]: "Superuser",
10
+ [noGlobalRole]: "No global role"
11
+ };
12
+ const entriesOf = (input) => Array.isArray(input) ? input.map((name) => [name, void 0]) : Object.entries(input);
13
+ /**
14
+ * The roles with `admin` first (added when missing), their labels and grants. With `organizations`, every role needs a
15
+ * label (`admin` and `user` have one built in), `user` is always there, as the role of an account without a global
16
+ * role, and `owner` and `member` are refused, since they name organization roles.
17
+ */
18
+ const roleDefinitions = (input = ["admin", "user"], { organizations = false } = {}) => {
19
+ const entries = entriesOf(input);
20
+ const bad = entries.find(([name]) => !roleName.test(name));
21
+ if (bad) throw new Error(`Invalid role "${bad[0]}": use lowercase letters, digits, "_" or "-"`);
22
+ const given = new Map(entries);
23
+ const names = [
24
+ superuserRole,
25
+ ...new Set(entries.map(([name]) => name).filter((name) => name !== superuserRole)),
26
+ ...organizations && !given.has("user") ? [noGlobalRole] : []
27
+ ];
28
+ const labels = {};
29
+ const grants = {};
30
+ for (const name of names) {
31
+ const definition = given.get(name);
32
+ const label = typeof definition === "string" ? definition : definition?.label ?? builtInLabels[name];
33
+ if (label !== void 0) labels[name] = label;
34
+ grants[name] = typeof definition === "object" ? definition.grants ?? [] : [];
35
+ const unknown = grants[name].find((granted) => !names.includes(granted));
36
+ if (unknown !== void 0) throw new Error(`Role "${name}" grants "${unknown}", which is not one of the roles: ${names.join(", ")}`);
37
+ }
38
+ if (!organizations) return {
39
+ names,
40
+ labels,
41
+ grants
42
+ };
43
+ const reserved = names.find((name) => organizationRoleNames.includes(name));
44
+ if (reserved) throw new Error(`Role "${reserved}" names an organization role; choose another name`);
45
+ const unlabelled = names.find((name) => labels[name] === void 0);
46
+ if (unlabelled) throw new Error(`Role "${unlabelled}" needs a label: with organizations, roles are given as { name: 'Label' }`);
47
+ return {
48
+ names,
49
+ labels,
50
+ grants
51
+ };
52
+ };
53
+ /** The roles a member of an organization can hold there: every role but the superuser's and the placeholder. */
54
+ const membershipRoles = ({ names }) => names.filter((name) => name !== "admin" && name !== "user");
55
+ //#endregion
56
+ export { membershipRoles, noGlobalRole, roleDefinitions, superuserRole };
@@ -0,0 +1,68 @@
1
+ import { globalRoles } from "./app-roles.js";
2
+ import { initialOrganization } from "./active-organization.js";
3
+ import { findMember, findOrganization, membershipsOf, organizationRoleOf, summaries } from "./store.js";
4
+ //#region src/better-auth/organizations/session-organization.ts
5
+ /** The user field that keeps the organization someone last worked in, for their next sign-in. */
6
+ const lastOrganizationField = { lastOrganizationId: {
7
+ type: "string",
8
+ required: false,
9
+ input: false,
10
+ fieldName: "last_organization_id"
11
+ } };
12
+ /** Whether someone may work in an organization: a member, or holding a global role, which enters every one. */
13
+ const mayEnter = async (adapter, user, organizationId) => {
14
+ if (await findMember(adapter, {
15
+ organizationId,
16
+ userId: user.id
17
+ })) return true;
18
+ return globalRoles(user.role).length > 0 && await findOrganization(adapter, organizationId) !== null;
19
+ };
20
+ /**
21
+ * The token's organization claims for a session: `org`, the active organization, while the person may still work in
22
+ * it; for a member also `org_role`, their organization role, and `app_roles`, their app roles there. Read when the
23
+ * token is issued, so a removed member or a changed role shows within the token's lifetime.
24
+ */
25
+ const organizationClaims = async (adapter, user, activeOrganizationId) => {
26
+ if (!activeOrganizationId) return {};
27
+ const member = await findMember(adapter, {
28
+ organizationId: activeOrganizationId,
29
+ userId: user.id
30
+ });
31
+ if (member) return {
32
+ org: activeOrganizationId,
33
+ org_role: organizationRoleOf(member),
34
+ app_roles: member.appRoles
35
+ };
36
+ if (await mayEnter(adapter, user, activeOrganizationId)) return { org: activeOrganizationId };
37
+ return {};
38
+ };
39
+ /**
40
+ * Better Auth's session hooks for organizations: a new session starts in the organization the person last worked in
41
+ * (`initialOrganization`), and a session's change of organization is kept on the user as the next one's start.
42
+ */
43
+ const sessionOrganizationHooks = (context) => ({
44
+ create: { before: async (session) => {
45
+ const { adapter, internalAdapter } = await context();
46
+ const user = await internalAdapter.findUserById(session.userId);
47
+ if (!user) return;
48
+ const last = user.lastOrganizationId;
49
+ const activeOrganizationId = initialOrganization({
50
+ memberships: summaries(await membershipsOf(adapter, user.id)),
51
+ last,
52
+ entersAny: globalRoles(user.role).length > 0,
53
+ lastExists: Boolean(last) && await findOrganization(adapter, last) !== null
54
+ });
55
+ return activeOrganizationId ? { data: {
56
+ ...session,
57
+ activeOrganizationId
58
+ } } : void 0;
59
+ } },
60
+ update: { after: async (session) => {
61
+ if (!session.activeOrganizationId) return;
62
+ const { internalAdapter } = await context();
63
+ const user = await internalAdapter.findUserById(session.userId);
64
+ if (user && user.lastOrganizationId !== session.activeOrganizationId) await internalAdapter.updateUser(session.userId, { lastOrganizationId: session.activeOrganizationId });
65
+ } }
66
+ });
67
+ //#endregion
68
+ export { lastOrganizationField, mayEnter, organizationClaims, sessionOrganizationHooks };
@@ -0,0 +1,19 @@
1
+ import { createAuthMiddleware } from "better-auth/api";
2
+ //#region src/better-auth/organizations/store.d.ts
3
+ export type OrganizationRole = 'owner' | 'admin' | 'member';
4
+ export type StoredMember = {
5
+ id: string;
6
+ organizationId: string;
7
+ userId: string;
8
+ role: string;
9
+ appRoles: string[];
10
+ createdAt: Date;
11
+ };
12
+ export type StoredOrganization = {
13
+ id: string;
14
+ name: string;
15
+ slug: string;
16
+ logo?: string | null;
17
+ createdAt: Date;
18
+ };
19
+ //#endregion
@@ -0,0 +1,73 @@
1
+ import { parseAppRoles } from "./app-roles.js";
2
+ //#region src/better-auth/organizations/store.ts
3
+ /** Better Auth's `findMany` returns 100 rows unless told otherwise; these lists are read whole. */
4
+ const everyRow = 1e5;
5
+ const memberOf = (row) => ({
6
+ ...row,
7
+ appRoles: parseAppRoles(row.appRoles)
8
+ });
9
+ /** Better Auth stores several organization roles comma separated; Protobase gives each member one. */
10
+ const organizationRoleOf = (member) => {
11
+ const roles = member.role.split(",").map((name) => name.trim());
12
+ return roles.includes("owner") ? "owner" : roles.includes("admin") ? "admin" : "member";
13
+ };
14
+ const findMember = async (adapter, where) => {
15
+ const row = await adapter.findOne({
16
+ model: "member",
17
+ where: [{
18
+ field: "organizationId",
19
+ value: where.organizationId
20
+ }, {
21
+ field: "userId",
22
+ value: where.userId
23
+ }]
24
+ });
25
+ return row ? memberOf(row) : void 0;
26
+ };
27
+ const findMemberById = async (adapter, id) => {
28
+ const row = await adapter.findOne({
29
+ model: "member",
30
+ where: [{
31
+ field: "id",
32
+ value: id
33
+ }]
34
+ });
35
+ return row ? memberOf(row) : void 0;
36
+ };
37
+ const membershipsOf = async (adapter, userId) => (await adapter.findMany({
38
+ model: "member",
39
+ where: [{
40
+ field: "userId",
41
+ value: userId
42
+ }],
43
+ limit: everyRow
44
+ })).map(memberOf);
45
+ const membersOf = async (adapter, organizationId) => (await adapter.findMany({
46
+ model: "member",
47
+ where: [{
48
+ field: "organizationId",
49
+ value: organizationId
50
+ }],
51
+ limit: everyRow
52
+ })).map(memberOf);
53
+ const findOrganization = (adapter, id) => adapter.findOne({
54
+ model: "organization",
55
+ where: [{
56
+ field: "id",
57
+ value: id
58
+ }]
59
+ });
60
+ const summaries = (members) => members.map(({ organizationId, createdAt }) => ({
61
+ organizationId,
62
+ createdAt
63
+ }));
64
+ const setAppRoles = (adapter, memberId, appRoles) => adapter.update({
65
+ model: "member",
66
+ where: [{
67
+ field: "id",
68
+ value: memberId
69
+ }],
70
+ update: { appRoles: [...appRoles] }
71
+ });
72
+ //#endregion
73
+ export { everyRow, findMember, findMemberById, findOrganization, membersOf, membershipsOf, organizationRoleOf, setAppRoles, summaries };
@@ -51,7 +51,11 @@ const accountChanges = [
51
51
  "/passkey/generate-register-options",
52
52
  "/passkey/verify-registration",
53
53
  "/passkey/update-passkey",
54
- "/passkey/delete-passkey"
54
+ "/passkey/delete-passkey",
55
+ "/invitation/accept",
56
+ "/organization/leave",
57
+ "/organization/transfer-ownership",
58
+ "/organization/delete"
55
59
  ];
56
60
  const staffCannotChange = {
57
61
  code: "STAFF_CANNOT_CHANGE_SIGN_IN",
@@ -6,8 +6,8 @@ import { policyRefusal, staffSignInPath } from "./sign-in-enforcement.js";
6
6
  import { staffSignInEmail } from "./sign-in-mail.js";
7
7
  import { logStaffSignIn } from "./staff-log.js";
8
8
  import { APIError, createAuthEndpoint } from "better-auth/api";
9
- import * as z$1 from "zod";
10
9
  import { expireCookie, setSessionCookie } from "better-auth/cookies";
10
+ import * as z$1 from "zod";
11
11
  //#region src/better-auth/staff-sign-in-endpoints.ts
12
12
  const startLifetimeMs = 6e5;
13
13
  const stateCookie = "staff_state";
@@ -1,3 +1,4 @@
1
+ import { membershipRoles } from "./organizations/role-definitions.js";
1
2
  import { effectiveSignInPolicy } from "./sign-in-policy.js";
2
3
  import { readSignInPolicy } from "./policy-store.js";
3
4
  import { hasUsers } from "./users.js";
@@ -8,8 +9,9 @@ import { Hono } from "hono";
8
9
  * `signInMethods`, the ways to sign in the sign-in policy leaves on (`password`, `emailCode`, `passkey`), so it offers
9
10
  * only those; `passwordReset` when reset mail can be sent and passwords are on, so it offers "Forgot password?" only
10
11
  * then; the ids of the `socialProviders` people can sign in with, so it offers "Continue with GitHub" when `github` is
11
- * one; `platformSignIn`, the id and name of the platform's provider when it is one of them; and `staffSignIn`, the
12
- * operator provider's name, when staff can sign in as people.
12
+ * one; `platformSignIn`, the id and name of the platform's provider when it is one of them; `staffSignIn`, the
13
+ * operator provider's name, when staff can sign in as people; and with organizations, who may create them and the roles
14
+ * with their labels, `membership` for those a member can hold, and the roles each one's holders may also give.
13
15
  */
14
16
  const statusRoute = (auth) => {
15
17
  const app = new Hono();
@@ -31,7 +33,16 @@ const statusRoute = (auth) => {
31
33
  passwordReset: auth.passwordReset && policy.password === "allowed",
32
34
  socialProviders,
33
35
  ...platformSignIn && { platformSignIn },
34
- ...staffSignIn && { staffSignIn }
36
+ ...staffSignIn && { staffSignIn },
37
+ ...auth.organizations && { organizations: {
38
+ create: auth.organizations.create,
39
+ roles: auth.roleDefinitions.names.map((name) => ({
40
+ name,
41
+ label: auth.roleDefinitions.labels[name] ?? name,
42
+ membership: membershipRoles(auth.roleDefinitions).includes(name),
43
+ grants: auth.roleDefinitions.grants[name] ?? []
44
+ }))
45
+ } }
35
46
  });
36
47
  });
37
48
  return app;
@@ -28,7 +28,8 @@ export declare const hasUsers: (auth: AdminAuth) => Promise<boolean>;
28
28
  * Creates a user directly in the admin store. Host side only (no HTTP route creates accounts while the store is empty):
29
29
  * the first user is always an `admin`, later ones get `role` (default `user`). The address counts as verified, so the
30
30
  * user can also sign in with an emailed code. `accounts` link the user to sign-in providers up front, so a provider
31
- * signs them in by its own id whatever address it reports; one that signs in another user already is refused.
31
+ * signs them in by its own id whatever address it reports; one that signs in another user already is refused. With
32
+ * organizations, the first user also owns every organization without members, such as those an app's seed made.
32
33
  */
33
34
  export declare const createUser: (auth: AdminAuth, input: NewUser) => Promise<{
34
35
  id: string;
@@ -1,4 +1,5 @@
1
1
  import { parseRoles } from "./parse-roles.js";
2
+ import { ownOrganizationsWithoutMembers } from "./organizations/host.js";
2
3
  //#region src/better-auth/users.ts
3
4
  /** The roles this auth instance allows, `admin` first. */
4
5
  const roleChoices = (auth) => [...auth.roles];
@@ -21,7 +22,8 @@ const checkAccounts = async (auth, accounts) => {
21
22
  * Creates a user directly in the admin store. Host side only (no HTTP route creates accounts while the store is empty):
22
23
  * the first user is always an `admin`, later ones get `role` (default `user`). The address counts as verified, so the
23
24
  * user can also sign in with an emailed code. `accounts` link the user to sign-in providers up front, so a provider
24
- * signs them in by its own id whatever address it reports; one that signs in another user already is refused.
25
+ * signs them in by its own id whatever address it reports; one that signs in another user already is refused. With
26
+ * organizations, the first user also owns every organization without members, such as those an app's seed made.
25
27
  */
26
28
  const createUser = async (auth, input) => {
27
29
  const first = !await hasUsers(auth);
@@ -41,6 +43,7 @@ const createUser = async (auth, input) => {
41
43
  userId: user.id,
42
44
  ...account
43
45
  });
46
+ if (first && auth.organizations) await ownOrganizationsWithoutMembers(auth, user.id);
44
47
  return {
45
48
  id: user.id,
46
49
  email: user.email,
@@ -1,6 +1,8 @@
1
1
  import { AdminAuth } from "./better-auth/create-auth.js";
2
- import { ResourceSource } from "./resource-source.js";
2
+ import { FilesOptions } from "./files/options.js";
3
3
  import { AdminEnv, Authenticator, PageSource, ViewSource } from "./types.js";
4
+ import { ResourceSource } from "./resource-source.js";
5
+ import { FunctionSource } from "./functions/define-function.js";
4
6
  import { AdminOptions } from "./admin-options.js";
5
7
  import { Hono } from "hono";
6
8
  import { UserMenuSource } from "@protobase/schema";
@@ -17,8 +19,12 @@ export type CreateAdminInput = {
17
19
  authenticate: Authenticator;
18
20
  /** A Better Auth instance (see `createAuth`): its handler is mounted at `/api/auth`. */
19
21
  auth?: AdminAuth;
22
+ /** API functions of the app's own, each served at `<systemPath>/functions/<name>` (see `defineFunction`). */
23
+ functions?: Record<string, FunctionSource>;
24
+ /** Where file fields keep their bytes (see `FilesOptions`). */
25
+ files?: FilesOptions;
20
26
  options?: AdminOptions;
21
27
  };
22
28
  /** The admin REST API as a Hono app. It uses only Web APIs, so it runs on Node, Workers and anywhere else Hono does. */
23
- export declare const createAdmin: ({ resources, views, pages, userMenu, db, authenticate, auth, options }: CreateAdminInput) => Hono<AdminEnv, import("hono/types").BlankSchema, "/">;
29
+ export declare const createAdmin: ({ resources, views, pages, userMenu, db, authenticate, auth, functions, files: filesOptions, options }: CreateAdminInput) => Hono<AdminEnv, import("hono/types").BlankSchema, "/">;
24
30
  //#endregion
@@ -4,9 +4,15 @@ import { assistantRecords } from "./assistant/records.js";
4
4
  import { builtInAssistant } from "./assistant/built-in-assistant.js";
5
5
  import { consoleAuditQueue } from "./audit/console-queue.js";
6
6
  import { authBasePath } from "./better-auth/create-auth.js";
7
+ import { organizationRoutes } from "./better-auth/organizations/admin-routes.js";
7
8
  import { statusRoute } from "./better-auth/status-route.js";
8
9
  import { checkShell } from "./check-shell.js";
9
10
  import { checkViews } from "./check-views.js";
11
+ import { createFilesRuntime } from "./files/runtime.js";
12
+ import { fileRoutes } from "./files/routes.js";
13
+ import { toApiFunction } from "./functions/define-function.js";
14
+ import { callerRecords } from "./functions/records.js";
15
+ import { functionRoutes } from "./functions/routes.js";
10
16
  import { runtimeUrl } from "./runtime/runtime-settings.js";
11
17
  import { createMeta } from "./meta.js";
12
18
  import { buildRegistry } from "./registry.js";
@@ -20,9 +26,10 @@ import { Hono } from "hono";
20
26
  import { checkPages } from "@protobase/layout";
21
27
  //#region src/create-admin.ts
22
28
  /** The admin REST API as a Hono app. It uses only Web APIs, so it runs on Node, Workers and anywhere else Hono does. */
23
- const createAdmin = ({ resources, views = [], pages = [], userMenu, db, authenticate, auth, options = {} }) => {
29
+ const createAdmin = ({ resources, views = [], pages = [], userMenu, db, authenticate, auth, functions = {}, files: filesOptions, options = {} }) => {
24
30
  const basePath = options.basePath ?? "/api/v1";
25
31
  const registry = buildRegistry(resources);
32
+ const apiFunctions = Object.fromEntries(Object.entries(functions).map(([name, source]) => [name, toApiFunction(name, source)]));
26
33
  const viewModels = views.map((view) => view.toModel());
27
34
  const userMenuModel = userMenu?.toUserMenuModel();
28
35
  const pageModels = pages.map((entry) => entry.toModel());
@@ -35,6 +42,12 @@ const createAdmin = ({ resources, views = [], pages = [], userMenu, db, authenti
35
42
  const systemPath = basePath.slice(0, basePath.lastIndexOf("/"));
36
43
  const assistant = assistantSettings(options.assistant, globalThis.process?.env ?? {});
37
44
  const runtime = runtimeUrl(options.runtime, globalThis.process?.env ?? {});
45
+ const files = createFilesRuntime({
46
+ ...filesOptions && { options: filesOptions },
47
+ env: globalThis.process?.env ?? {},
48
+ models: registry.entries.map((entry) => entry.model),
49
+ systemPath
50
+ });
38
51
  const deps = {
39
52
  db,
40
53
  registry,
@@ -53,7 +66,8 @@ const createAdmin = ({ resources, views = [], pages = [], userMenu, db, authenti
53
66
  },
54
67
  rowPermissions: (resource) => typeof options.rowPermissions === "object" ? !options.rowPermissions.except.includes(resource) : options.rowPermissions !== false,
55
68
  ...assistant && { assistant: assistant.kind === "external" ? assistant.url : `${systemPath}/assistant` },
56
- ...runtime && { runtime }
69
+ ...runtime && { runtime },
70
+ ...files && { files }
57
71
  };
58
72
  const meta = createMeta(deps);
59
73
  const onError = createErrorHandler(options.onUnhandledError);
@@ -83,6 +97,12 @@ const createAdmin = ({ resources, views = [], pages = [], userMenu, db, authenti
83
97
  app.onError(onError);
84
98
  if (auth) {
85
99
  app.route(authBasePath, statusRoute(auth));
100
+ if (auth.organizations) app.route(authBasePath, organizationRoutes({
101
+ auth,
102
+ audit: deps.audit,
103
+ registry,
104
+ db
105
+ }));
86
106
  app.on(["GET", "POST"], `${authBasePath}/*`, (c) => auth.handler(c.req.raw));
87
107
  }
88
108
  if (assistant?.kind === "built-in") {
@@ -99,7 +119,16 @@ const createAdmin = ({ resources, views = [], pages = [], userMenu, db, authenti
99
119
  report: options.onUnhandledError
100
120
  }));
101
121
  }
122
+ app.route("/", functionRoutes({
123
+ basePath: `${systemPath}/functions`,
124
+ functions: apiFunctions,
125
+ authenticate,
126
+ db,
127
+ records: (session) => callerRecords(deps, session),
128
+ onError
129
+ }));
102
130
  app.route("/", batchRoute(deps, authenticate, meta));
131
+ app.route("/", fileRoutes(deps, authenticate));
103
132
  app.route(basePath, api);
104
133
  app.route(deps.systemPath || "/", system);
105
134
  return app;
package/dist/deps.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import "./types.js";
2
+ import "./files/runtime.js";
2
3
  import "./registry.js";
3
4
  import "./scan-guard.js";
4
5
  import { PageModel } from "@protobase/layout";