@remit/backend 2.0.0 → 2.1.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.
@@ -1,7 +1,10 @@
1
1
  import type { MsOAuthConfig } from "./msoauth.js";
2
+ export type Switch = "Enabled" | "Disabled";
2
3
  export type AuthConfig = {
3
4
  mode: "BetterAuthJwt";
4
5
  contentSigningSecret: string;
6
+ signIn: Switch;
7
+ selfSignUp: Switch;
5
8
  } | {
6
9
  mode: "GatewayAuthorizer";
7
10
  };
@@ -1,3 +1,4 @@
1
+ import { resolveSelfSignUpEnabled, resolveSignInEnabled, } from "@remit/auth-service/verifier";
1
2
  import { msOAuthCredentialsFromSecret } from "./msoauth-secrets-manager.js";
2
3
  const DEFAULT_UPDATE_CONTROL_DIR = "/data/control";
3
4
  const optional = (env, key) => env[key] || undefined;
@@ -11,10 +12,13 @@ const commaList = (raw) => (raw ?? "")
11
12
  .split(",")
12
13
  .map((origin) => origin.trim())
13
14
  .filter((origin) => origin.length > 0);
15
+ const switchOf = (on) => (on ? "Enabled" : "Disabled");
14
16
  const authFromEnv = (env) => env.DATA_BACKEND === "sqlite"
15
17
  ? {
16
18
  mode: "BetterAuthJwt",
17
19
  contentSigningSecret: required(env, "BETTER_AUTH_SECRET", "The self-host SQL backend signs content URLs with it."),
20
+ signIn: switchOf(resolveSignInEnabled(env.SIGN_IN_ENABLED)),
21
+ selfSignUp: switchOf(resolveSelfSignUpEnabled(env.SELF_SIGN_UP_ENABLED)),
18
22
  }
19
23
  : { mode: "GatewayAuthorizer" };
20
24
  const contentDeliveryDomainFromEnv = (env, auth, publicOrigin) => {
@@ -174,6 +174,12 @@ export const getCalendarFeedIcal = async (context, ctx) => {
174
174
  const feedToken = readCalendarFeedToken(context.request.path);
175
175
  if (feedToken === null)
176
176
  return feedNotFound();
177
+ // The kill switch stops every feed with the answer an unknown token gets;
178
+ // the rows stay, so feeds resume when sign-in is turned back on.
179
+ if (ctx.config.auth.mode === "BetterAuthJwt" &&
180
+ ctx.config.auth.signIn === "Disabled") {
181
+ return feedNotFound();
182
+ }
177
183
  const ifNoneMatch = context.request.headers["if-none-match"];
178
184
  const ifModifiedSince = context.request.headers["if-modified-since"];
179
185
  const deps = calendarDepsOf(ctx.client);
@@ -175,6 +175,11 @@ export declare const handlers: {
175
175
  AccountDetailOperations_deleteAccount: (context: import("openapi-backend").Context, ctx: import("../types.js").HandlerContext<"accountFanout">) => Promise<import("@remit/api-openapi-types").DeleteAccountResponse>;
176
176
  AccountOperations_createAccount: (context: import("openapi-backend").Context, ctx: import("../types.js").HandlerContext<"sync">) => Promise<import("@remit/api-openapi-types").AccountResponse>;
177
177
  AccountOperations_testConnection: (context: import("openapi-backend").Context, ctx: import("../types.js").HandlerContext) => Promise<import("@remit/api-openapi-types").TestConnectionResponse>;
178
+ PeopleOperations_listPeople: OperationHandler<import("../types.js").PeopleOperationIds>;
179
+ PeopleOperations_createStepUp: OperationHandler<import("../types.js").PeopleOperationIds>;
180
+ PeopleOperations_updatePerson: OperationHandler<import("../types.js").PeopleOperationIds>;
181
+ PeopleOperations_createPasswordReset: OperationHandler<import("../types.js").PeopleOperationIds>;
182
+ PeopleOperations_signOutPerson: OperationHandler<import("../types.js").PeopleOperationIds>;
178
183
  SystemOperations_getSystemUpdate: OperationHandler<import("../types.js").SystemOperationIds>;
179
184
  SystemOperations_applySystemUpdate: OperationHandler<import("../types.js").SystemOperationIds>;
180
185
  ConfigOperations_getConfig: (_context: import("openapi-backend").Context, ctx: import("../types.js").HandlerContext<"sync">) => Promise<import("@remit/api-openapi-types").ConfigDescriptionResponse>;
@@ -15,6 +15,7 @@ import { MeOperations } from "./me.js";
15
15
  import { MessageBulkOperations, MessageOperations } from "./message.js";
16
16
  import { OrganizeJobDetailOperations, OrganizeOperations } from "./organize.js";
17
17
  import { OutboxAttachmentOperations, OutboxDetailOperations, OutboxOperations, } from "./outbox.js";
18
+ import { PeopleOperations } from "./people.js";
18
19
  import { SyncOperations } from "./sync.js";
19
20
  import { SystemOperations } from "./system-update.js";
20
21
  import { ThreadDetailOperations, ThreadOperations } from "./thread.js";
@@ -24,6 +25,7 @@ export const handlers = {
24
25
  ...MeOperations,
25
26
  ...ConfigOperations,
26
27
  ...SystemOperations,
28
+ ...PeopleOperations,
27
29
  ...AccountOperations,
28
30
  ...AccountDetailOperations,
29
31
  ...MicrosoftOAuthOperations,
@@ -0,0 +1,2 @@
1
+ import type { OperationHandler, PeopleOperationIds } from "../types.js";
2
+ export declare const PeopleOperations: Record<PeopleOperationIds, OperationHandler<PeopleOperationIds>>;
@@ -0,0 +1,119 @@
1
+ import { getPeopleDirectory } from "../service/people-directory.js";
2
+ const CLIENT_IP_HEADER = "x-remit-client-ip";
3
+ const refuse = (statusCode, code, message) => ({ statusCode, body: { code, message } });
4
+ const REFUSALS = {
5
+ caller_disabled: [403, "Your account is disabled."],
6
+ not_found: [404, "Not found"],
7
+ step_up_required: [403, "Confirm your password again."],
8
+ wrong_password: [403, "That password is not right."],
9
+ step_up_locked: [429, "Too many wrong passwords. Try again in 15 minutes."],
10
+ cannot_disable_self: [409, "You cannot disable your own account."],
11
+ };
12
+ const refusalOf = (refusal) => {
13
+ const [statusCode, message] = REFUSALS[refusal];
14
+ return refuse(statusCode, refusal, message);
15
+ };
16
+ const headerOf = (context, name) => {
17
+ const headers = context.request.headers;
18
+ const key = Object.keys(headers).find((each) => each.toLowerCase() === name);
19
+ const value = key === undefined ? undefined : headers[key];
20
+ return typeof value === "string" ? value : undefined;
21
+ };
22
+ /**
23
+ * The guards every People operation runs before any read or write. The
24
+ * surface does not exist (404) while anyone can sign up, since anyone could
25
+ * then make themselves an operator; the kill switch refuses it; an unsafe
26
+ * request from an origin this instance does not serve is refused. The caller's
27
+ * own row is checked inside the directory, in the transaction that acts.
28
+ */
29
+ const guard = (context, ctx, targetId, unsafe) => {
30
+ const directory = getPeopleDirectory();
31
+ const { auth } = ctx.config;
32
+ if (auth.mode !== "BetterAuthJwt" ||
33
+ auth.selfSignUp === "Enabled" ||
34
+ !directory) {
35
+ return refusalOf("not_found");
36
+ }
37
+ const request = {
38
+ directory,
39
+ callerId: ctx.identity.sub,
40
+ clientIp: headerOf(context, CLIENT_IP_HEADER) ?? "unknown",
41
+ };
42
+ const refuseAudited = (outcome, code, message) => {
43
+ directory.recordRefusal({ ...request, targetId, outcome });
44
+ return refuse(403, code, message);
45
+ };
46
+ if (auth.signIn === "Disabled") {
47
+ return refuseAudited("RefusedSignInOff", "sign_in_disabled", "Sign-in is turned off on this server.");
48
+ }
49
+ const origin = headerOf(context, "origin");
50
+ if (unsafe &&
51
+ origin !== undefined &&
52
+ !ctx.config.corsAllowedOrigins.includes(origin)) {
53
+ return refuseAudited("RefusedForeignOrigin", "foreign_origin", "This request came from a page this server does not serve.");
54
+ }
55
+ return request;
56
+ };
57
+ const isGuarded = (value) => "directory" in value;
58
+ const answer = (outcome) => outcome.ok ? outcome.value : refusalOf(outcome.refusal);
59
+ const targetOf = (context) => context.request.params.userId;
60
+ export const PeopleOperations = {
61
+ PeopleOperations_listPeople: async (context, ctx) => {
62
+ const guarded = guard(context, ctx, "all", false);
63
+ if (!isGuarded(guarded))
64
+ return guarded;
65
+ const listed = answer(await guarded.directory.listPeople(guarded));
66
+ return Array.isArray(listed) ? { people: listed } : listed;
67
+ },
68
+ PeopleOperations_createStepUp: async (context, ctx) => {
69
+ const body = context.request.requestBody;
70
+ const guarded = guard(context, ctx, body.userId, true);
71
+ if (!isGuarded(guarded))
72
+ return guarded;
73
+ return answer(await guarded.directory.createStepUp({
74
+ ...guarded,
75
+ targetId: body.userId,
76
+ action: body.action,
77
+ password: body.password,
78
+ }));
79
+ },
80
+ PeopleOperations_signOutPerson: async (context, ctx) => {
81
+ const targetId = targetOf(context);
82
+ const guarded = guard(context, ctx, targetId, true);
83
+ if (!isGuarded(guarded))
84
+ return guarded;
85
+ const { stepUpToken } = context.request.requestBody;
86
+ return answer(await guarded.directory.signOutPerson({
87
+ ...guarded,
88
+ targetId,
89
+ stepUpToken,
90
+ }));
91
+ },
92
+ PeopleOperations_updatePerson: async (context, ctx) => {
93
+ const targetId = targetOf(context);
94
+ const guarded = guard(context, ctx, targetId, true);
95
+ if (!isGuarded(guarded))
96
+ return guarded;
97
+ const { status, stepUpToken } = context.request
98
+ .requestBody;
99
+ return answer(await guarded.directory.updatePerson({
100
+ ...guarded,
101
+ targetId,
102
+ stepUpToken,
103
+ status,
104
+ }));
105
+ },
106
+ PeopleOperations_createPasswordReset: async (context, ctx) => {
107
+ const targetId = targetOf(context);
108
+ const guarded = guard(context, ctx, targetId, true);
109
+ if (!isGuarded(guarded))
110
+ return guarded;
111
+ const { stepUpToken } = context.request
112
+ .requestBody;
113
+ return answer(await guarded.directory.createPasswordReset({
114
+ ...guarded,
115
+ targetId,
116
+ stepUpToken,
117
+ }));
118
+ },
119
+ };
@@ -0,0 +1,8 @@
1
+ import type { PeopleDirectory } from "@remit/auth-service/people";
2
+ /**
3
+ * Register the self-host people directory from the composition root. A host
4
+ * that registers none answers every People operation with 404, which is what
5
+ * keeps better-auth out of any bundle that only serves the mail API.
6
+ */
7
+ export declare const setPeopleDirectory: (directory: PeopleDirectory | null) => void;
8
+ export declare const getPeopleDirectory: () => PeopleDirectory | null;
@@ -0,0 +1,10 @@
1
+ let registered = null;
2
+ /**
3
+ * Register the self-host people directory from the composition root. A host
4
+ * that registers none answers every People operation with 404, which is what
5
+ * keeps better-auth out of any bundle that only serves the mail API.
6
+ */
7
+ export const setPeopleDirectory = (directory) => {
8
+ registered = directory;
9
+ };
10
+ export const getPeopleDirectory = () => registered;
@@ -3,10 +3,11 @@ import type { ApiConfig } from "./config/api-config.js";
3
3
  import type { RemitClient } from "./service/create-remit-client.js";
4
4
  import type { QueueName } from "./service/queues.js";
5
5
  type MatchPrefix<Prefix extends string, S extends string> = S extends `${Prefix}${infer _}` ? S : never;
6
- export type OperationIds = "MeOperations_deleteMe" | "MeOperations_listVipSuggestions" | "MeOperations_createExport" | "MeOperations_getExport" | "MeOperations_listQuarantine" | "ConfigOperations_getConfig" | "ConfigOperations_exportConfig" | "ConfigOperations_importConfig" | "SystemOperations_getSystemUpdate" | "SystemOperations_applySystemUpdate" | "AccountOperations_createAccount" | "AccountOperations_testConnection" | "AccountDetailOperations_updateAccount" | "AccountDetailOperations_deleteAccount" | "MicrosoftOAuthOperations_microsoftOAuthStart" | "MicrosoftOAuthOperations_microsoftOAuthCallback" | "MailboxOperations_listMailboxes" | "MailboxOperations_createMailbox" | "MailboxDetailOperations_getMailbox" | "MailboxDetailOperations_renameMailbox" | "MailboxDetailOperations_deleteMailbox" | "FolderRoleOperations_appointFolderRole" | "FilterOperations_listFilters" | "FilterOperations_createFilter" | "FilterDetailOperations_getFilter" | "FilterDetailOperations_updateFilter" | "FilterDetailOperations_deleteFilter" | "LabelOperations_listLabels" | "LabelOperations_createLabel" | "LabelDetailOperations_getLabel" | "LabelDetailOperations_updateLabel" | "LabelDetailOperations_deleteLabel" | "OrganizeOperations_createOrganizeJob" | "OrganizeOperations_previewOrganize" | "OrganizeJobDetailOperations_getOrganizeJob" | "SyncOperations_triggerSync" | "SyncOperations_getSyncStatus" | "ThreadDetailOperations_listThreadMessages" | "UnifiedThreadOperations_listAllThreads" | "ThreadOperations_listThreads" | "ThreadOperations_searchThreads" | "MessageOperations_describeMessage" | "MessageOperations_getRawMessage" | "MessageOperations_updateMessageFlags" | "MessageBulkOperations_deleteMessages" | "MessageBulkOperations_moveMessages" | "MessageBulkOperations_updateFlags" | "MessageBulkOperations_copyMessages" | "MessageBulkOperations_updateMessageLabels" | "MessageBulkOperations_reportSpam" | "MessageBulkOperations_notSpam" | "CalendarOperations_listCalendars" | "CalendarOperations_createCalendar" | "CalendarDetailOperations_getCalendar" | "CalendarDetailOperations_updateCalendar" | "CalendarDetailOperations_deleteCalendar" | "CalendarDetailOperations_getCalendarFeed" | "CalendarDetailOperations_putCalendarFeed" | "CalendarDetailOperations_revokeCalendarFeed" | "CalendarFeedOperations_getCalendarFeedIcal" | "CalendarEventOperations_listCalendarEvents" | "CalendarEventOperations_createCalendarEvent" | "CalendarEventDetailOperations_getCalendarEvent" | "CalendarEventDetailOperations_updateCalendarEvent" | "CalendarEventDetailOperations_deleteCalendarEvent" | "CalendarFreeBusyOperations_listCalendarFreeBusy" | "UserSettingsOperations_getUserSettings" | "UserSettingsOperations_updateUserSettings" | "UserSettingsOperations_clearDefaultInvitationSender" | "CalendarTaskOperations_listCalendarTasks" | "CalendarTaskOperations_createCalendarTask" | "CalendarTaskDetailOperations_getCalendarTask" | "CalendarTaskDetailOperations_updateCalendarTask" | "CalendarTaskDetailOperations_deleteCalendarTask" | "TrashOperations_emptyTrash" | "OutboxOperations_createOutboxMessage" | "OutboxOperations_listOutboxMessages" | "OutboxDetailOperations_getOutboxMessage" | "OutboxDetailOperations_updateOutboxMessage" | "OutboxDetailOperations_deleteOutboxMessage" | "OutboxDetailOperations_sendOutboxMessage" | "OutboxDetailOperations_mintOutboxAttachment" | "OutboxAttachmentOperations_completeOutboxAttachment" | "AddressOperations_searchAddresses" | "AddressDetailOperations_updateAddress" | "CalendarSuggestionOperations_listCalendarSuggestions" | "MessageCalendarSuggestionOperations_listMessageCalendarSuggestions" | "CalendarSuggestionActionOperations_acceptCalendarSuggestion" | "CalendarSuggestionActionOperations_declineCalendarSuggestion" | "CalendarSuggestionActionOperations_dismissCalendarSuggestion" | "CalendarSuggestionActionOperations_reopenCalendarSuggestion";
6
+ export type OperationIds = "MeOperations_deleteMe" | "MeOperations_listVipSuggestions" | "MeOperations_createExport" | "MeOperations_getExport" | "MeOperations_listQuarantine" | "ConfigOperations_getConfig" | "ConfigOperations_exportConfig" | "ConfigOperations_importConfig" | "SystemOperations_getSystemUpdate" | "SystemOperations_applySystemUpdate" | "PeopleOperations_listPeople" | "PeopleOperations_createStepUp" | "PeopleOperations_signOutPerson" | "PeopleOperations_updatePerson" | "PeopleOperations_createPasswordReset" | "AccountOperations_createAccount" | "AccountOperations_testConnection" | "AccountDetailOperations_updateAccount" | "AccountDetailOperations_deleteAccount" | "MicrosoftOAuthOperations_microsoftOAuthStart" | "MicrosoftOAuthOperations_microsoftOAuthCallback" | "MailboxOperations_listMailboxes" | "MailboxOperations_createMailbox" | "MailboxDetailOperations_getMailbox" | "MailboxDetailOperations_renameMailbox" | "MailboxDetailOperations_deleteMailbox" | "FolderRoleOperations_appointFolderRole" | "FilterOperations_listFilters" | "FilterOperations_createFilter" | "FilterDetailOperations_getFilter" | "FilterDetailOperations_updateFilter" | "FilterDetailOperations_deleteFilter" | "LabelOperations_listLabels" | "LabelOperations_createLabel" | "LabelDetailOperations_getLabel" | "LabelDetailOperations_updateLabel" | "LabelDetailOperations_deleteLabel" | "OrganizeOperations_createOrganizeJob" | "OrganizeOperations_previewOrganize" | "OrganizeJobDetailOperations_getOrganizeJob" | "SyncOperations_triggerSync" | "SyncOperations_getSyncStatus" | "ThreadDetailOperations_listThreadMessages" | "UnifiedThreadOperations_listAllThreads" | "ThreadOperations_listThreads" | "ThreadOperations_searchThreads" | "MessageOperations_describeMessage" | "MessageOperations_getRawMessage" | "MessageOperations_updateMessageFlags" | "MessageBulkOperations_deleteMessages" | "MessageBulkOperations_moveMessages" | "MessageBulkOperations_updateFlags" | "MessageBulkOperations_copyMessages" | "MessageBulkOperations_updateMessageLabels" | "MessageBulkOperations_reportSpam" | "MessageBulkOperations_notSpam" | "CalendarOperations_listCalendars" | "CalendarOperations_createCalendar" | "CalendarDetailOperations_getCalendar" | "CalendarDetailOperations_updateCalendar" | "CalendarDetailOperations_deleteCalendar" | "CalendarDetailOperations_getCalendarFeed" | "CalendarDetailOperations_putCalendarFeed" | "CalendarDetailOperations_revokeCalendarFeed" | "CalendarFeedOperations_getCalendarFeedIcal" | "CalendarEventOperations_listCalendarEvents" | "CalendarEventOperations_createCalendarEvent" | "CalendarEventDetailOperations_getCalendarEvent" | "CalendarEventDetailOperations_updateCalendarEvent" | "CalendarEventDetailOperations_deleteCalendarEvent" | "CalendarFreeBusyOperations_listCalendarFreeBusy" | "UserSettingsOperations_getUserSettings" | "UserSettingsOperations_updateUserSettings" | "UserSettingsOperations_clearDefaultInvitationSender" | "CalendarTaskOperations_listCalendarTasks" | "CalendarTaskOperations_createCalendarTask" | "CalendarTaskDetailOperations_getCalendarTask" | "CalendarTaskDetailOperations_updateCalendarTask" | "CalendarTaskDetailOperations_deleteCalendarTask" | "TrashOperations_emptyTrash" | "OutboxOperations_createOutboxMessage" | "OutboxOperations_listOutboxMessages" | "OutboxDetailOperations_getOutboxMessage" | "OutboxDetailOperations_updateOutboxMessage" | "OutboxDetailOperations_deleteOutboxMessage" | "OutboxDetailOperations_sendOutboxMessage" | "OutboxDetailOperations_mintOutboxAttachment" | "OutboxAttachmentOperations_completeOutboxAttachment" | "AddressOperations_searchAddresses" | "AddressDetailOperations_updateAddress" | "CalendarSuggestionOperations_listCalendarSuggestions" | "MessageCalendarSuggestionOperations_listMessageCalendarSuggestions" | "CalendarSuggestionActionOperations_acceptCalendarSuggestion" | "CalendarSuggestionActionOperations_declineCalendarSuggestion" | "CalendarSuggestionActionOperations_dismissCalendarSuggestion" | "CalendarSuggestionActionOperations_reopenCalendarSuggestion";
7
7
  export type MeOperationIds = MatchPrefix<"MeOperations_", OperationIds>;
8
8
  export type ConfigOperationIds = MatchPrefix<"ConfigOperations_", OperationIds>;
9
9
  export type SystemOperationIds = MatchPrefix<"SystemOperations_", OperationIds>;
10
+ export type PeopleOperationIds = MatchPrefix<"PeopleOperations_", OperationIds>;
10
11
  export type AccountOperationIds = MatchPrefix<"AccountOperations_", OperationIds>;
11
12
  export type AccountDetailOperationIds = MatchPrefix<"AccountDetailOperations_", OperationIds>;
12
13
  export type MailboxOperationIds = MatchPrefix<"MailboxOperations_", OperationIds>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remit/backend",
3
- "version": "2.0.0",
3
+ "version": "2.1.0",
4
4
  "description": "Remit Mail Inspector API backend",
5
5
  "license": "MIT",
6
6
  "author": "",