Compare commits

..

27 Commits

Author SHA1 Message Date
Tom Moor a7c61580f5 Require state parameter 2025-05-03 19:21:49 -04:00
Tom Moor 9c8dda0e0c final polish, use cryptographically secure random
Prevent redirect urls with http or wildcards
2025-05-03 16:00:41 -04:00
Tom Moor 40b7bea982 test: Add tests for oauthAuthentications endpoints 2025-05-03 15:13:29 -04:00
Tom Moor 1c184936f9 Cancel button should redirect to redirectURI when valid 2025-05-03 14:11:38 -04:00
Tom Moor f654b7e136 Merge branch 'main' into oauth-server 2025-05-03 12:30:46 -04:00
Tom Moor fd3c21d28b Remove withCollectionPermissions scope (#9124)
* Remove withCollectionPermissions scope

* defaultScopeWithUser -> withUserScope

* fix: Include withDrafts in groupMemberships.list

* rename
2025-05-03 12:00:54 -04:00
Tom Moor c0c36bacbb fix: Error loading collection (#9123) 2025-05-03 02:18:56 +00:00
Tom Moor 7bd1ea7c40 chore/attachments-sw-cache (#9122) 2025-05-02 22:15:39 -04:00
Tom Moor 5ebb1e8a61 feat: Add input rule to create new tables (#9118) 2025-05-02 08:19:57 -04:00
Tom Moor 96d6987858 fix: Mobile toolbar overlaps with home indicator (#9119) 2025-05-02 08:19:48 -04:00
Tom Moor 3602198cd8 fix: Subtle collection loading bug (#9120) 2025-05-02 08:19:41 -04:00
Tom Moor fb9021e9e1 fix: Allow saving OAuth app without description 2025-04-30 22:36:10 -04:00
dependabot[bot] 00bab31cff chore(deps): bump vite from 6.3.3 to 6.3.4 (#9112)
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 6.3.3 to 6.3.4.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v6.3.4/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-version: 6.3.4
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-30 18:16:38 -04:00
Tom Moor 866eea1dcf tsc 2025-04-30 07:55:23 -04:00
Tom Moor eade6eb392 Merge branch 'main' into oauth-server 2025-04-29 23:04:01 -04:00
Tom Moor a4ee81fa19 OAuth server cloud compatibility (#9091)
* Revert "Add recency boost to search results (#9038)" (#9065)

This reverts commit 2bc47cfcef.

* fix: Double fetch on refactored paginated list (#9068)

* fix: Store Linear workspace logo only when it's available (#9072)

* Extract subdomain auth redirect (#9070)

* Extract subdomain auth redirect

* docs

* Switch Linear to actor=app method (#9074)

* Basic /authorize framework for cloud auth

* Handle logged out OAuth apex

---------

Co-authored-by: Hemachandar <132386067+hmacr@users.noreply.github.com>
2025-04-29 22:43:21 -04:00
Tom Moor 3ef2b7cf42 fix: Backlinks should be ordered alphabetically (#9106) 2025-04-30 02:17:03 +00:00
Tom Moor 18743da2fc fix: bold inline code marks cause formatting to split (#9105)
* fix: Inline code mark split around bold

* Show inline formatting options + code in toolbar
2025-04-30 01:50:52 +00:00
dependabot[bot] fe1307d7e7 chore(deps): bump the aws group with 5 updates (#9086)
Bumps the aws group with 5 updates:

| Package | From | To |
| --- | --- | --- |
| [@aws-sdk/client-s3](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/clients/client-s3) | `3.787.0` | `3.797.0` |
| [@aws-sdk/lib-storage](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/lib/lib-storage) | `3.787.0` | `3.797.0` |
| [@aws-sdk/s3-presigned-post](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/packages/s3-presigned-post) | `3.787.0` | `3.797.0` |
| [@aws-sdk/s3-request-presigner](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/packages/s3-request-presigner) | `3.787.0` | `3.797.0` |
| [@aws-sdk/signature-v4-crt](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/packages/signature-v4-crt) | `3.787.0` | `3.796.0` |


Updates `@aws-sdk/client-s3` from 3.787.0 to 3.797.0
- [Release notes](https://github.com/aws/aws-sdk-js-v3/releases)
- [Changelog](https://github.com/aws/aws-sdk-js-v3/blob/main/clients/client-s3/CHANGELOG.md)
- [Commits](https://github.com/aws/aws-sdk-js-v3/commits/v3.797.0/clients/client-s3)

Updates `@aws-sdk/lib-storage` from 3.787.0 to 3.797.0
- [Release notes](https://github.com/aws/aws-sdk-js-v3/releases)
- [Changelog](https://github.com/aws/aws-sdk-js-v3/blob/main/lib/lib-storage/CHANGELOG.md)
- [Commits](https://github.com/aws/aws-sdk-js-v3/commits/v3.797.0/lib/lib-storage)

Updates `@aws-sdk/s3-presigned-post` from 3.787.0 to 3.797.0
- [Release notes](https://github.com/aws/aws-sdk-js-v3/releases)
- [Changelog](https://github.com/aws/aws-sdk-js-v3/blob/main/packages/s3-presigned-post/CHANGELOG.md)
- [Commits](https://github.com/aws/aws-sdk-js-v3/commits/v3.797.0/packages/s3-presigned-post)

Updates `@aws-sdk/s3-request-presigner` from 3.787.0 to 3.797.0
- [Release notes](https://github.com/aws/aws-sdk-js-v3/releases)
- [Changelog](https://github.com/aws/aws-sdk-js-v3/blob/main/packages/s3-request-presigner/CHANGELOG.md)
- [Commits](https://github.com/aws/aws-sdk-js-v3/commits/v3.797.0/packages/s3-request-presigner)

Updates `@aws-sdk/signature-v4-crt` from 3.787.0 to 3.796.0
- [Release notes](https://github.com/aws/aws-sdk-js-v3/releases)
- [Changelog](https://github.com/aws/aws-sdk-js-v3/blob/main/packages/signature-v4-crt/CHANGELOG.md)
- [Commits](https://github.com/aws/aws-sdk-js-v3/commits/v3.796.0/packages/signature-v4-crt)

---
updated-dependencies:
- dependency-name: "@aws-sdk/client-s3"
  dependency-version: 3.797.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: aws
- dependency-name: "@aws-sdk/lib-storage"
  dependency-version: 3.797.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: aws
- dependency-name: "@aws-sdk/s3-presigned-post"
  dependency-version: 3.797.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: aws
- dependency-name: "@aws-sdk/s3-request-presigner"
  dependency-version: 3.797.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: aws
- dependency-name: "@aws-sdk/signature-v4-crt"
  dependency-version: 3.796.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: aws
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-29 06:48:04 -04:00
codegen-sh[bot] a226889143 Update task scheduling to use instance method (#9092)
* Update task scheduling to use instance method

* Delete update_task_schedule.sh

* Applied automatic fixes

* tsc

---------

Co-authored-by: codegen-sh[bot] <131295404+codegen-sh[bot]@users.noreply.github.com>
Co-authored-by: Tom Moor <tom@getoutline.com>
2025-04-29 06:47:51 -04:00
Tom Moor 347f033802 fix: Notifications received for draft with access but no subscription (#9099) 2025-04-29 06:45:15 -04:00
Tom Moor f5c659f902 fix: Prevent cross-domain websocket connections to on-premise instances (#9064) 2025-04-28 17:27:40 -04:00
Hemachandar 722d10e7de Implement type-safe schedule method for tasks (#9079)
* Implement type-safe task scheduler

* introduce 'schedule' instance method

* typo
2025-04-28 17:27:24 -04:00
Hemachandar ce001547b5 fix: Check pasted text is url before creating an URL object (#9082) 2025-04-28 17:27:12 -04:00
dependabot[bot] 8d05e2b095 chore(deps): bump pg from 8.14.1 to 8.15.6 (#9084)
Bumps [pg](https://github.com/brianc/node-postgres/tree/HEAD/packages/pg) from 8.14.1 to 8.15.6.
- [Changelog](https://github.com/brianc/node-postgres/blob/master/CHANGELOG.md)
- [Commits](https://github.com/brianc/node-postgres/commits/pg@8.15.6/packages/pg)

---
updated-dependencies:
- dependency-name: pg
  dependency-version: 8.15.6
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-28 17:26:54 -04:00
dependabot[bot] 19e40cf814 chore(deps-dev): bump nodemon from 3.1.9 to 3.1.10 (#9085)
Bumps [nodemon](https://github.com/remy/nodemon) from 3.1.9 to 3.1.10.
- [Release notes](https://github.com/remy/nodemon/releases)
- [Commits](https://github.com/remy/nodemon/compare/v3.1.9...v3.1.10)

---
updated-dependencies:
- dependency-name: nodemon
  dependency-version: 3.1.10
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-28 17:26:28 -04:00
dependabot[bot] 2bb9b50637 chore(deps): bump react-portal from 4.2.2 to 4.3.0 (#9087)
Bumps [react-portal](https://github.com/tajo/react-portal) from 4.2.2 to 4.3.0.
- [Release notes](https://github.com/tajo/react-portal/releases)
- [Commits](https://github.com/tajo/react-portal/commits)

---
updated-dependencies:
- dependency-name: react-portal
  dependency-version: 4.3.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-28 17:26:18 -04:00
74 changed files with 948 additions and 459 deletions
+1
View File
@@ -321,6 +321,7 @@ const Container = styled(Flex)<ContainerProps>`
z-index: ${depths.mobileSidebar};
max-width: 80%;
min-width: 280px;
padding-left: var(--sal);
${fadeOnDesktopBackgrounded()}
@media print {
@@ -38,10 +38,10 @@ function StarredLink({ star }: Props) {
const { ui, collections, documents } = useStores();
const [menuOpen, handleMenuOpen, handleMenuClose] = useBoolean();
const { documentId, collectionId } = star;
const collection = collections.get(collectionId);
const collection = collectionId ? collections.get(collectionId) : undefined;
const locationSidebarContext = useLocationSidebarContext();
const sidebarContext = starredSidebarContext(
star.documentId ?? star.collectionId
star.documentId ?? star.collectionId ?? ""
);
const [expanded, setExpanded] = useState(
(star.documentId
+6 -1
View File
@@ -7,6 +7,7 @@ import { isCode } from "@shared/editor/lib/isCode";
import { findParentNode } from "@shared/editor/queries/findParentNode";
import { EditorStyleHelper } from "@shared/editor/styles/EditorStyleHelper";
import { depths, s } from "@shared/styles";
import { getSafeAreaInsets } from "@shared/utils/browser";
import { HEADER_HEIGHT } from "~/components/Header";
import { Portal } from "~/components/Portal";
import useEventListener from "~/hooks/useEventListener";
@@ -241,12 +242,16 @@ const FloatingToolbar = React.forwardRef(function FloatingToolbar_(
if (props.active) {
const rect = document.body.getBoundingClientRect();
const safeAreaInsets = getSafeAreaInsets();
return (
<ReactPortal>
<MobileWrapper
ref={menuRef}
style={{
bottom: `calc(100% - ${height - rect.y}px)`,
bottom: `calc(100% - ${
height - rect.y - safeAreaInsets.bottom
}px)`,
}}
>
{props.children}
+3 -2
View File
@@ -6,6 +6,7 @@ import { v4 } from "uuid";
import { EmbedDescriptor } from "@shared/editor/embeds";
import { MenuItem } from "@shared/editor/types";
import { MentionType } from "@shared/types";
import { isUrl } from "@shared/utils/urls";
import Integration from "~/models/Integration";
import useCurrentUser from "~/hooks/useCurrentUser";
import useStores from "~/hooks/useStores";
@@ -29,9 +30,9 @@ export const PasteMenu = observer(({ pastedText, embeds, ...props }: Props) => {
const user = useCurrentUser({ rejectOnEmpty: false });
let mentionType: MentionType | undefined;
const url = pastedText ? new URL(pastedText) : undefined;
if (url) {
if (pastedText && isUrl(pastedText)) {
const url = new URL(pastedText);
const integration = integrations.find((intg: Integration) =>
isURLMentionable({ url, integration: intg })
);
+2 -1
View File
@@ -2,7 +2,8 @@ import Extension from "@shared/editor/lib/Extension";
import { InputRule } from "@shared/editor/lib/InputRule";
const rightArrow = new InputRule(/->$/, "→");
const emdash = new InputRule(/--$/, "—");
// Note that the suppression of pipe here prevents conflict with table creation rule.
const emdash = new InputRule(/(?:^|[^\|])(--)$/, "—");
const oneHalf = new InputRule(/(?:^|\s)(1\/2)$/, "½");
const threeQuarters = new InputRule(/(?:^|\s)(3\/4)$/, "¾");
const copyright = new InputRule(/\(c\)$/, "©️");
+3 -3
View File
@@ -67,7 +67,7 @@ export default function formattingMenuItems(
shortcut: `${metaDisplay}+B`,
icon: <BoldIcon />,
active: isMarkActive(schema.marks.strong),
visible: !isCode && (!isMobile || !isEmpty),
visible: !isCodeBlock && (!isMobile || !isEmpty),
},
{
name: "em",
@@ -75,7 +75,7 @@ export default function formattingMenuItems(
shortcut: `${metaDisplay}+I`,
icon: <ItalicIcon />,
active: isMarkActive(schema.marks.em),
visible: !isCode && (!isMobile || !isEmpty),
visible: !isCodeBlock && (!isMobile || !isEmpty),
},
{
name: "strikethrough",
@@ -83,7 +83,7 @@ export default function formattingMenuItems(
shortcut: `${metaDisplay}+D`,
icon: <StrikethroughIcon />,
active: isMarkActive(schema.marks.strikethrough),
visible: !isCode && (!isMobile || !isEmpty),
visible: !isCodeBlock && (!isMobile || !isEmpty),
},
{
tooltip: dictionary.mark,
+10
View File
@@ -331,6 +331,16 @@ export default class Document extends ArchivableModel implements Searchable {
);
}
/**
* Returns the documents that link to this document.
*
* @returns documents that link to this document
*/
@computed
get backlinks(): Document[] {
return this.store.getBacklinkedDocuments(this.id);
}
/**
* Returns users that have been individually given access to the document.
*
+1 -1
View File
@@ -22,7 +22,7 @@ class Star extends Model {
document?: Document;
/** The collection ID that is starred. */
collectionId: string;
collectionId?: string;
/** The collection that is starred. */
@Relation(() => Collection, { onDelete: "cascade" })
+6 -12
View File
@@ -66,7 +66,6 @@ const CollectionScene = observer(function _CollectionScene() {
const location = useLocation();
const { t } = useTranslation();
const { documents, collections, ui } = useStores();
const [isFetching, setFetching] = React.useState(false);
const [error, setError] = React.useState<Error | undefined>();
const currentPath = location.pathname;
const [, setLastVisitedPath] = useLastVisitedPath();
@@ -120,21 +119,16 @@ const CollectionScene = observer(function _CollectionScene() {
React.useEffect(() => {
async function fetchData() {
if ((!can || !collection) && !error && !isFetching) {
try {
setError(undefined);
setFetching(true);
await collections.fetch(id);
} catch (err) {
setError(err);
} finally {
setFetching(false);
}
try {
setError(undefined);
await collections.fetch(id);
} catch (err) {
setError(err);
}
}
void fetchData();
}, [collections, isFetching, collection, error, id, can]);
}, []);
useCommandBarActions([editCollection], [ui.activeCollectionId ?? "none"]);
@@ -18,7 +18,7 @@ type Props = {
};
function References({ document }: Props) {
const { collections, documents } = useStores();
const { documents } = useStores();
const user = useCurrentUser();
const location = useLocation();
const locationSidebarContext = useLocationSidebarContext();
@@ -27,10 +27,8 @@ function References({ document }: Props) {
void documents.fetchBacklinks(document.id);
}, [documents, document.id]);
const backlinks = documents.getBacklinkedDocuments(document.id);
const collection = document.collectionId
? collections.get(document.collectionId)
: undefined;
const backlinks = document.backlinks;
const collection = document.collection;
const children = collection
? collection.getChildrenForDocument(document.id)
: [];
+20 -5
View File
@@ -4,6 +4,7 @@ import styled from "styled-components";
import Flex from "@shared/components/Flex";
import { s } from "@shared/styles";
import { parseDomain } from "@shared/utils/domains";
import type OAuthClient from "~/models/oauth/OAuthClient";
import ButtonLarge from "~/components/ButtonLarge";
import ChangeLanguage from "~/components/ChangeLanguage";
import Heading from "~/components/Heading";
@@ -16,6 +17,7 @@ import { useLoggedInSessions } from "~/hooks/useLoggedInSessions";
import useQuery from "~/hooks/useQuery";
import useRequest from "~/hooks/useRequest";
import { client } from "~/utils/ApiClient";
import { BadRequestError, NotFoundError } from "~/utils/errors";
import isCloudHosted from "~/utils/isCloudHosted";
import { detectLanguage } from "~/utils/language";
import Login from "./Login";
@@ -66,12 +68,17 @@ function Authorize() {
scope,
} = Object.fromEntries(params);
const [scopes] = React.useState(() => scope?.split(" ") ?? []);
const { error: clientError, data: response } = useRequest(
() => client.post("/oauthClients.info", { clientId }),
true
);
const { error: clientError, data: response } = useRequest<{
data: OAuthClient;
}>(() => client.post("/oauthClients.info", { clientId, redirectUri }), true);
const handleCancel = () => {
if (redirectUri && !clientError) {
const url = new URL(redirectUri);
url.searchParams.set("error", "access_denied");
window.location.href = url.toString();
return;
}
if (window.history.length) {
window.history.back();
} else {
@@ -96,6 +103,7 @@ function Authorize() {
!redirectUri && "redirect_uri",
!responseType && "response_type",
!scope && "scope",
!state && "state",
].filter(Boolean);
if (missingParams.length || clientError) {
@@ -103,13 +111,20 @@ function Authorize() {
<Background>
<Centered>
<StyledHeading>{t("An error occurred")}</StyledHeading>
{clientError ? (
{clientError instanceof NotFoundError ? (
<Text as="p" type="secondary">
{t(
"The OAuth client could not be found, please check the provided client ID"
)}
<Pre>{clientId}</Pre>
</Text>
) : clientError instanceof BadRequestError ? (
<Text as="p" type="secondary">
{t(
"The OAuth client could not be loaded, please check the redirect URI is valid"
)}
<Pre>{redirectUri}</Pre>
</Text>
) : (
<Text as="p" type="secondary">
{t("Required OAuth parameters are missing")}
+6 -2
View File
@@ -80,11 +80,16 @@ const Application = observer(function Application({ oauthClient }: Props) {
async (data: FormData) => {
try {
await oauthClient.save(data);
toast.success(
oauthClient.published
? t("Application published")
: t("Application updated")
);
} catch (error) {
toast.error(error.message);
}
},
[oauthClient]
[oauthClient, t]
);
const handleRotateSecret = React.useCallback(async () => {
@@ -173,7 +178,6 @@ const Application = observer(function Application({ oauthClient }: Props) {
<Input
type="text"
{...register("description", {
required: true,
maxLength: OAuthClientValidation.maxDescriptionLength,
})}
flex
+7
View File
@@ -186,6 +186,13 @@ export default class CollectionsStore extends Store<Collection> {
statusFilter: [CollectionStatusFilter.Archived],
});
get(id: string): Collection | undefined {
return (
this.data.get(id) ??
this.orderedData.find((collection) => id.endsWith(collection.urlId))
);
}
@computed
get archived(): Collection[] {
return orderBy(this.orderedData, "archivedAt", "desc").filter(
+2 -2
View File
@@ -300,8 +300,8 @@ export default class DocumentsStore extends Store<Document> {
const documentIds = this.backlinks.get(documentId) || [];
return orderBy(
compact(documentIds.map((id) => this.data.get(id))),
"updatedAt",
"desc"
"title",
"asc"
);
}
+9 -9
View File
@@ -48,11 +48,11 @@
"> 0.25%, not dead"
],
"dependencies": {
"@aws-sdk/client-s3": "3.787.0",
"@aws-sdk/lib-storage": "3.787.0",
"@aws-sdk/s3-presigned-post": "3.787.0",
"@aws-sdk/s3-request-presigner": "3.787.0",
"@aws-sdk/signature-v4-crt": "^3.787.0",
"@aws-sdk/client-s3": "3.797.0",
"@aws-sdk/lib-storage": "3.797.0",
"@aws-sdk/s3-presigned-post": "3.797.0",
"@aws-sdk/s3-request-presigner": "3.797.0",
"@aws-sdk/signature-v4-crt": "^3.796.0",
"@babel/core": "^7.26.10",
"@babel/plugin-proposal-decorators": "^7.25.9",
"@babel/plugin-transform-class-properties": "^7.25.9",
@@ -175,7 +175,7 @@
"passport-oauth2": "^1.8.0",
"passport-slack-oauth2": "^1.2.0",
"patch-package": "^7.0.2",
"pg": "^8.14.1",
"pg": "^8.15.6",
"pg-tsquery": "^8.4.2",
"pluralize": "^8.0.0",
"png-chunks-extract": "^1.0.0",
@@ -210,7 +210,7 @@
"react-i18next": "^12.3.1",
"react-medium-image-zoom": "5.2.13",
"react-merge-refs": "^2.1.1",
"react-portal": "^4.2.2",
"react-portal": "^4.3.0",
"react-router-dom": "^5.3.4",
"react-virtualized-auto-sizer": "^1.0.26",
"react-waypoint": "^10.3.0",
@@ -250,7 +250,7 @@
"uuid": "^8.3.2",
"validator": "13.12.0",
"vaul": "^1.1.2",
"vite": "^6.3.3",
"vite": "^6.3.4",
"vite-plugin-pwa": "^0.21.2",
"winston": "^3.17.0",
"ws": "^7.5.10",
@@ -357,7 +357,7 @@
"jest-environment-jsdom": "^29.7.0",
"jest-fetch-mock": "^3.0.3",
"lint-staged": "^13.3.0",
"nodemon": "^3.1.9",
"nodemon": "^3.1.10",
"postinstall-postinstall": "^2.1.0",
"prettier": "^2.8.8",
"react-refresh": "^0.14.2",
+5 -5
View File
@@ -77,14 +77,14 @@ router.get(
{ transaction }
);
if (workspace.logoUrl) {
transaction.afterCommit(async () => {
await UploadLinearWorkspaceLogoTask.schedule({
transaction.afterCommit(async () => {
if (workspace.logoUrl) {
await new UploadLinearWorkspaceLogoTask().schedule({
integrationId: integration.id,
logoUrl: workspace.logoUrl,
});
});
}
}
});
ctx.redirect(LinearUtils.successUrl());
}
@@ -66,6 +66,6 @@ export class NotionImportsProcessor extends ImportsProcessor<IntegrationService.
protected async scheduleTask(
importTask: ImportTask<IntegrationService.Notion>
): Promise<void> {
await NotionAPIImportTask.schedule({ importTaskId: importTask.id });
await new NotionAPIImportTask().schedule({ importTaskId: importTask.id });
}
}
@@ -77,7 +77,7 @@ export default class NotionAPIImportTask extends APIImportTask<IntegrationServic
protected async scheduleNextTask(
importTask: ImportTask<IntegrationService.Notion>
) {
await NotionAPIImportTask.schedule({ importTaskId: importTask.id });
await new NotionAPIImportTask().schedule({ importTaskId: importTask.id });
return;
}
@@ -29,8 +29,12 @@ describe("WebhookProcessor", () => {
await processor.perform(event);
expect(DeliverWebhookTask.schedule).toHaveBeenCalled();
expect(DeliverWebhookTask.schedule).toHaveBeenCalledWith({
expect(
jest.mocked(DeliverWebhookTask.prototype.schedule)
).toHaveBeenCalled();
expect(
jest.mocked(DeliverWebhookTask.prototype.schedule)
).toHaveBeenCalledWith({
event,
subscriptionId: subscription.id,
});
@@ -53,7 +57,9 @@ describe("WebhookProcessor", () => {
await processor.perform(event);
expect(DeliverWebhookTask.schedule).toHaveBeenCalledTimes(0);
expect(
jest.mocked(DeliverWebhookTask.prototype.schedule)
).toHaveBeenCalledTimes(0);
});
it("it schedules a delivery for the event for each subscription", async () => {
@@ -79,13 +85,21 @@ describe("WebhookProcessor", () => {
await processor.perform(event);
expect(DeliverWebhookTask.schedule).toHaveBeenCalled();
expect(DeliverWebhookTask.schedule).toHaveBeenCalledTimes(2);
expect(DeliverWebhookTask.schedule).toHaveBeenCalledWith({
expect(
jest.mocked(DeliverWebhookTask.prototype.schedule)
).toHaveBeenCalled();
expect(
jest.mocked(DeliverWebhookTask.prototype.schedule)
).toHaveBeenCalledTimes(2);
expect(
jest.mocked(DeliverWebhookTask.prototype.schedule)
).toHaveBeenCalledWith({
event,
subscriptionId: subscription.id,
});
expect(DeliverWebhookTask.schedule).toHaveBeenCalledWith({
expect(
jest.mocked(DeliverWebhookTask.prototype.schedule)
).toHaveBeenCalledWith({
event,
subscriptionId: subscriptionTwo.id,
});
@@ -24,7 +24,10 @@ export default class WebhookProcessor extends BaseProcessor {
await Promise.all(
applicableSubscriptions.map((subscription) =>
DeliverWebhookTask.schedule({ event, subscriptionId: subscription.id })
new DeliverWebhookTask().schedule({
event,
subscriptionId: subscription.id,
})
)
);
}
+1 -1
View File
@@ -24,7 +24,7 @@ type Props = {
/** Position of moved document within document structure */
index?: number;
/** The IP address of the user moving the document */
ip: string;
ip: string | null;
/** The database transaction to run within */
transaction?: Transaction;
};
@@ -4,9 +4,11 @@ import DeleteAttachmentTask from "@server/queues/tasks/DeleteAttachmentTask";
import { buildAttachment, buildDocument } from "@server/test/factories";
import documentPermanentDeleter from "./documentPermanentDeleter";
jest.mock("@server/queues/tasks/DeleteAttachmentTask", () => ({
schedule: jest.fn(),
}));
jest.mock("@server/queues/tasks/DeleteAttachmentTask");
beforeEach(() => {
jest.resetAllMocks();
});
describe("documentPermanentDeleter", () => {
it("should destroy documents", async () => {
@@ -60,7 +62,9 @@ describe("documentPermanentDeleter", () => {
await document.save();
const countDeletedDoc = await documentPermanentDeleter([document]);
expect(countDeletedDoc).toEqual(1);
expect(DeleteAttachmentTask.schedule).toHaveBeenCalledTimes(2);
expect(
jest.mocked(DeleteAttachmentTask.prototype.schedule)
).toHaveBeenCalledTimes(2);
expect(
await Document.unscoped().count({
where: {
+1 -1
View File
@@ -67,7 +67,7 @@ export default async function documentPermanentDeleter(documents: Document[]) {
"commands",
`Attachment ${attachmentId} scheduled for deletion`
);
await DeleteAttachmentTask.schedule({
await new DeleteAttachmentTask().schedule({
attachmentId,
teamId: document.teamId,
});
+1 -1
View File
@@ -56,5 +56,5 @@ export default async function userSuspender({
}
);
await CleanupDemotedUserTask.schedule({ userId: user.id });
await new CleanupDemotedUserTask().schedule({ userId: user.id });
}
+20 -36
View File
@@ -13,7 +13,6 @@ import {
Transaction,
Op,
FindOptions,
ScopeOptions,
WhereOptions,
EmptyResultError,
} from "sequelize";
@@ -106,20 +105,6 @@ type AdditionalFindOptions = {
},
}))
@Scopes(() => ({
withCollectionPermissions: (userId: string, paranoid = true) => ({
include: [
{
attributes: ["id", "permission", "sharing", "teamId", "deletedAt"],
model: userId
? Collection.scope({
method: ["withMembership", userId],
})
: Collection,
as: "collection",
paranoid,
},
],
}),
withoutState: {
attributes: {
exclude: ["state"],
@@ -169,13 +154,23 @@ type AdditionalFindOptions = {
],
};
},
withMembership: (userId: string) => {
withMembership: (userId: string, paranoid = true) => {
if (!userId) {
return {};
}
return {
include: [
{
attributes: ["id", "permission", "sharing", "teamId", "deletedAt"],
model: userId
? Collection.scope({
method: ["withMembership", userId],
})
: Collection,
as: "collection",
paranoid,
},
{
association: "memberships",
where: {
@@ -637,21 +632,15 @@ class Document extends ArchivableModel<
return uniq(membershipUserIds);
}
static defaultScopeWithUser(userId: string) {
const collectionScope: Readonly<ScopeOptions> = {
method: ["withCollectionPermissions", userId],
};
const viewScope: Readonly<ScopeOptions> = {
method: ["withViews", userId],
};
const membershipScope: Readonly<ScopeOptions> = {
method: ["withMembership", userId],
};
static withMembershipScope(userId: string, options?: FindOptions<Document>) {
return this.scope([
"defaultScope",
collectionScope,
viewScope,
membershipScope,
{
method: ["withViews", userId],
},
{
method: ["withMembership", userId, options?.paranoid],
},
]);
}
@@ -685,14 +674,12 @@ class Document extends ArchivableModel<
// almost every endpoint needs the collection membership to determine policy permissions.
const scope = this.scope([
"withDrafts",
{
method: ["withCollectionPermissions", userId, rest.paranoid],
},
options.includeState ? "withState" : "withoutState",
{
method: ["withViews", userId],
},
{
method: ["withMembership", userId],
method: ["withMembership", userId, rest.paranoid],
},
]);
@@ -750,9 +737,6 @@ class Document extends ArchivableModel<
const user = userId ? await User.findByPk(userId) : null;
const documents = await this.scope([
"withDrafts",
{
method: ["withCollectionPermissions", userId, rest.paranoid],
},
{
method: ["withViews", userId],
},
+1 -1
View File
@@ -408,7 +408,7 @@ class Team extends ParanoidModel<
});
if (attachment) {
await DeleteAttachmentTask.schedule({
await new DeleteAttachmentTask().schedule({
attachmentId: attachment.id,
teamId: model.id,
});
+1 -1
View File
@@ -717,7 +717,7 @@ class User extends ParanoidModel<
});
if (attachment) {
await DeleteAttachmentTask.schedule({
await new DeleteAttachmentTask().schedule({
attachmentId: attachment.id,
teamId: model.teamId,
});
@@ -1,7 +1,9 @@
import { NotificationEventType } from "@shared/types";
import { DocumentPermission, NotificationEventType } from "@shared/types";
import { UserMembership } from "@server/models";
import {
buildComment,
buildDocument,
buildDraftDocument,
buildSubscription,
buildUser,
} from "@server/test/factories";
@@ -54,6 +56,78 @@ describe("NotificationHelper", () => {
expect(recipients[0].id).toEqual(notificationEnabledUser.id);
});
it("should only return users who have notification enabled for comment creation and are subscribed to the document in case of new thread in draft", async () => {
const documentAuthor = await buildUser();
// create a draft
const document = await buildDraftDocument({
userId: documentAuthor.id,
teamId: documentAuthor.teamId,
collectionId: null,
});
// add a bunch of users as direct members
const user = await buildUser({
teamId: document.teamId,
notificationSettings: { [NotificationEventType.CreateComment]: true },
});
const user2 = await buildUser({
teamId: document.teamId,
notificationSettings: { [NotificationEventType.CreateComment]: true },
});
const user3 = await buildUser({
teamId: document.teamId,
notificationSettings: { [NotificationEventType.CreateComment]: true },
});
await UserMembership.create({
documentId: document.id,
userId: user.id,
permission: DocumentPermission.Read,
createdById: user.id,
});
await UserMembership.create({
documentId: document.id,
userId: user2.id,
permission: DocumentPermission.Read,
createdById: user.id,
});
await UserMembership.create({
documentId: document.id,
userId: user3.id,
permission: DocumentPermission.Read,
createdById: user.id,
});
// Add a subscription for only one of those users
await Promise.all([
buildSubscription({
userId: user.id,
}),
buildSubscription({
userId: user2.id,
}),
buildSubscription({
userId: user3.id,
documentId: document.id,
}),
]);
const comment = await buildComment({
documentId: document.id,
userId: documentAuthor.id,
});
const recipients =
await NotificationHelper.getCommentNotificationRecipients(
document,
comment,
comment.createdById
);
expect(recipients.length).toEqual(1);
expect(recipients[0].id).toEqual(user3.id);
});
it("should only return users who have notification enabled for comment creation and are in the thread in case of child comment", async () => {
const documentAuthor = await buildUser();
const document = await buildDocument({
+10 -4
View File
@@ -193,10 +193,16 @@ export default class NotificationHelper {
[Op.ne]: actorId,
},
event: SubscriptionType.Document,
[Op.or]: [
{ collectionId: document.collectionId },
{ documentId: document.id },
],
...(document.collectionId
? {
[Op.or]: [
{ collectionId: document.collectionId },
{ documentId: document.id },
],
}
: {
documentId: document.id,
}),
},
include: [
{
+18 -36
View File
@@ -182,25 +182,16 @@ export default class SearchHelper {
},
];
return Document.scope([
"withDrafts",
{
method: ["withViews", user.id],
},
{
method: ["withCollectionPermissions", user.id],
},
{
method: ["withMembership", user.id],
},
]).findAll({
where,
subQuery: false,
order: [["updatedAt", "DESC"]],
include,
offset,
limit,
});
return Document.withMembershipScope(user.id)
.scope("withDrafts")
.findAll({
where,
subQuery: false,
order: [["updatedAt", "DESC"]],
include,
offset,
limit,
});
}
public static async searchCollectionsForUser(
@@ -273,23 +264,14 @@ export default class SearchHelper {
// Final query to get associated document data
const [documents, count] = await Promise.all([
Document.scope([
"withDrafts",
{
method: ["withViews", user.id],
},
{
method: ["withCollectionPermissions", user.id],
},
{
method: ["withMembership", user.id],
},
]).findAll({
where: {
teamId: user.teamId,
id: map(results, "id"),
},
}),
Document.withMembershipScope(user.id)
.scope("withDrafts")
.findAll({
where: {
teamId: user.teamId,
id: map(results, "id"),
},
}),
results.length < limit && offset === 0
? Promise.resolve(results.length)
: countQuery,
+2 -2
View File
@@ -17,7 +17,7 @@ export default class AvatarProcessor extends BaseProcessor {
});
if (user.avatarUrl) {
await UploadUserAvatarTask.schedule({
await new UploadUserAvatarTask().schedule({
userId: event.userId,
avatarUrl: user.avatarUrl,
});
@@ -30,7 +30,7 @@ export default class AvatarProcessor extends BaseProcessor {
});
if (team.avatarUrl) {
await UploadTeamAvatarTask.schedule({
await new UploadTeamAvatarTask().schedule({
teamId: event.teamId,
avatarUrl: team.avatarUrl,
});
@@ -12,7 +12,7 @@ export default class CollectionsProcessor extends BaseProcessor {
];
async perform(event: CollectionEvent) {
await DetachDraftsFromCollectionTask.schedule({
await new DetachDraftsFromCollectionTask().schedule({
collectionId: event.collectionId,
actorId: event.actorId,
ip: event.ip,
@@ -27,7 +27,7 @@ export default class DocumentSubscriptionProcessor extends BaseProcessor {
async perform(event: ReceivedEvent) {
switch (event.name) {
case "collections.remove_user": {
await CollectionSubscriptionRemoveUserTask.schedule(event);
await new CollectionSubscriptionRemoveUserTask().schedule(event);
return;
}
@@ -35,7 +35,7 @@ export default class DocumentSubscriptionProcessor extends BaseProcessor {
return this.handleRemoveGroupFromCollection(event);
case "documents.remove_user": {
await DocumentSubscriptionRemoveUserTask.schedule(event);
await new DocumentSubscriptionRemoveUserTask().schedule(event);
return;
}
@@ -57,11 +57,11 @@ export default class DocumentSubscriptionProcessor extends BaseProcessor {
async (groupUsers) => {
await Promise.all(
groupUsers.map((groupUser) =>
CollectionSubscriptionRemoveUserTask.schedule({
new CollectionSubscriptionRemoveUserTask().schedule({
...event,
name: "collections.remove_user",
userId: groupUser.userId,
})
} as CollectionUserEvent)
)
);
}
@@ -79,11 +79,11 @@ export default class DocumentSubscriptionProcessor extends BaseProcessor {
async (groupUsers) => {
await Promise.all(
groupUsers.map((groupUser) =>
DocumentSubscriptionRemoveUserTask.schedule({
new DocumentSubscriptionRemoveUserTask().schedule({
...event,
name: "documents.remove_user",
userId: groupUser.userId,
})
} as DocumentUserEvent)
)
);
}
@@ -20,12 +20,12 @@ export default class FileOperationCreatedProcessor extends BaseProcessor {
if (fileOperation.type === FileOperationType.Import) {
switch (fileOperation.format) {
case FileOperationFormat.MarkdownZip:
await ImportMarkdownZipTask.schedule({
await new ImportMarkdownZipTask().schedule({
fileOperationId: event.modelId,
});
break;
case FileOperationFormat.JSON:
await ImportJSONTask.schedule({
await new ImportJSONTask().schedule({
fileOperationId: event.modelId,
});
break;
@@ -36,17 +36,17 @@ export default class FileOperationCreatedProcessor extends BaseProcessor {
if (fileOperation.type === FileOperationType.Export) {
switch (fileOperation.format) {
case FileOperationFormat.HTMLZip:
await ExportHTMLZipTask.schedule({
await new ExportHTMLZipTask().schedule({
fileOperationId: event.modelId,
});
break;
case FileOperationFormat.MarkdownZip:
await ExportMarkdownZipTask.schedule({
await new ExportMarkdownZipTask().schedule({
fileOperationId: event.modelId,
});
break;
case FileOperationFormat.JSON:
await ExportJSONTask.schedule({
await new ExportJSONTask().schedule({
fileOperationId: event.modelId,
});
break;
@@ -20,7 +20,7 @@ export default class IntegrationCreatedProcessor extends BaseProcessor {
}
// Store the available issue sources in the integration record.
await CacheIssueSourcesTask.schedule({
await new CacheIssueSourcesTask().schedule({
integrationId: integration.id,
});
@@ -62,25 +62,25 @@ export default class NotificationsProcessor extends BaseProcessor {
return;
}
await DocumentPublishedNotificationsTask.schedule(event);
await new DocumentPublishedNotificationsTask().schedule(event);
}
async documentAddUser(event: DocumentUserEvent) {
if (!event.data.isNew || event.userId === event.actorId) {
return;
}
await DocumentAddUserNotificationsTask.schedule(event);
await new DocumentAddUserNotificationsTask().schedule(event);
}
async documentAddGroup(event: DocumentGroupEvent) {
if (!event.data.isNew) {
return;
}
await DocumentAddGroupNotificationsTask.schedule(event);
await new DocumentAddGroupNotificationsTask().schedule(event);
}
async revisionCreated(event: RevisionEvent) {
await RevisionCreatedNotificationsTask.schedule(event);
await new RevisionCreatedNotificationsTask().schedule(event);
}
async collectionCreated(event: CollectionEvent) {
@@ -93,7 +93,7 @@ export default class NotificationsProcessor extends BaseProcessor {
return;
}
await CollectionCreatedNotificationsTask.schedule(event);
await new CollectionCreatedNotificationsTask().schedule(event);
}
async collectionAddUser(event: CollectionUserEvent) {
@@ -101,14 +101,14 @@ export default class NotificationsProcessor extends BaseProcessor {
return;
}
await CollectionAddUserNotificationsTask.schedule(event);
await new CollectionAddUserNotificationsTask().schedule(event);
}
async commentCreated(event: CommentEvent) {
await CommentCreatedNotificationsTask.schedule(event);
await new CommentCreatedNotificationsTask().schedule(event);
}
async commentUpdated(event: CommentEvent) {
await CommentUpdatedNotificationsTask.schedule(event);
await new CommentUpdatedNotificationsTask().schedule(event);
}
}
@@ -37,7 +37,7 @@ export default class RevisionsProcessor extends BaseProcessor {
return;
}
await DocumentUpdateTextTask.schedule(event);
await new DocumentUpdateTextTask().schedule(event);
const user = await User.findByPk(event.actorId, {
paranoid: false,
@@ -6,6 +6,6 @@ export default class UserDemotedProcessor extends BaseProcessor {
static applicableEvents: TEvent["name"][] = ["users.demote"];
async perform(event: UserEvent) {
await CleanupDemotedUserTask.schedule({ userId: event.userId });
await new CleanupDemotedUserTask().schedule({ userId: event.userId });
}
}
+3 -1
View File
@@ -325,7 +325,9 @@ export default abstract class APIImportTask<
([url, attachment]) => ({ attachmentId: attachment.id, url })
);
// publish task after attachments are persisted in DB.
const job = await UploadAttachmentsForImportTask.schedule(uploadItems);
const job = await new UploadAttachmentsForImportTask().schedule(
uploadItems
);
await job.finished();
} catch (err) {
// upload attachments failure is not critical enough to fail the whole import.
+18 -1
View File
@@ -21,7 +21,7 @@ export default abstract class BaseTask<T extends Record<string, any>> {
static cron: TaskSchedule | undefined;
/**
* Schedule this task type to be processed asyncronously by a worker.
* Schedule this task type to be processed asynchronously by a worker.
*
* @param props Properties to be used by the task
* @returns A promise that resolves once the job is placed on the task queue
@@ -39,6 +39,23 @@ export default abstract class BaseTask<T extends Record<string, any>> {
);
}
/**
* Schedule this task type to be processed asynchronously by a worker.
*
* @param props Properties to be used by the task
* @param options Job options such as priority and retry strategy, as defined by Bull.
* @returns A promise that resolves once the job is placed on the task queue
*/
public schedule(props: T, options?: JobOptions): Promise<Job> {
return taskQueue.add(
{
name: this.constructor.name,
props,
},
{ ...options, ...this.options }
);
}
/**
* Execute the task.
*
@@ -29,7 +29,7 @@ export default class CleanupDeletedTeamsTask extends BaseTask<Props> {
});
for (const team of teams) {
await CleanupDeletedTeamTask.schedule({
await new CleanupDeletedTeamTask().schedule({
teamId: team.id,
});
}
@@ -7,7 +7,7 @@ import BaseTask from "./BaseTask";
type Props = {
collectionId: string;
actorId: string;
ip: string;
ip: string | null;
};
export default class DetachDraftsFromCollectionTask extends BaseTask<Props> {
@@ -1,6 +1,6 @@
import { Op } from "sequelize";
import { GroupUser } from "@server/models";
import { DocumentGroupEvent } from "@server/types";
import { DocumentGroupEvent, DocumentUserEvent } from "@server/types";
import BaseTask, { TaskPriority } from "./BaseTask";
import DocumentAddUserNotificationsTask from "./DocumentAddUserNotificationsTask";
@@ -19,11 +19,12 @@ export default class DocumentAddGroupNotificationsTask extends BaseTask<Document
async (groupUsers) => {
await Promise.all(
groupUsers.map(async (groupUser) => {
await DocumentAddUserNotificationsTask.schedule({
await new DocumentAddUserNotificationsTask().schedule({
...event,
name: "documents.add_user",
modelId: event.data.membershipId,
userId: groupUser.userId,
});
} as DocumentUserEvent);
})
);
}
+1 -1
View File
@@ -12,7 +12,7 @@ type Props = {
sourceMetadata: Pick<Required<SourceMetadata>, "fileName" | "mimeType">;
publish?: boolean;
collectionId?: string;
parentDocumentId?: string;
parentDocumentId?: string | null;
ip: string;
key: string;
};
@@ -38,7 +38,7 @@ export default class UpdateTeamsAttachmentsSizeTask extends BaseTask<Props> {
const teamIds = rows.map((row) => row.teamId);
for (const teamId of teamIds) {
await UpdateTeamAttachmentsSizeTask.schedule({ teamId });
await new UpdateTeamAttachmentsSizeTask().schedule({ teamId });
}
}
);
+1 -1
View File
@@ -166,7 +166,7 @@ router.post(
)
);
const job = await UploadAttachmentFromUrlTask.schedule({
const job = await new UploadAttachmentFromUrlTask().schedule({
attachmentId: attachment.id,
url,
});
+1 -1
View File
@@ -135,7 +135,7 @@ router.post("auth.info", auth(), async (ctx: APIContext<T.AuthInfoReq>) => {
// If the user did not _just_ sign in then we need to check if they continue
// to have access to the workspace they are signed into.
if (user.lastSignedInAt && user.lastSignedInAt < subHours(new Date(), 1)) {
await ValidateSSOAccessTask.schedule({ userId: user.id });
await new ValidateSSOAccessTask().schedule({ userId: user.id });
}
ctx.body = {
+6 -3
View File
@@ -29,7 +29,8 @@ const cronHandler = async (ctx: APIContext<T.CronSchemaReq>) => {
for (const name in tasks) {
const TaskClass = tasks[name];
if (TaskClass.cron === period) {
await TaskClass.schedule({ limit });
// @ts-expect-error We won't instantiate an abstract class
await new TaskClass().schedule({ limit });
// Backwards compatibility for installations that have not set up
// cron jobs periods other than daily.
@@ -38,13 +39,15 @@ const cronHandler = async (ctx: APIContext<T.CronSchemaReq>) => {
!receivedPeriods.has(TaskSchedule.Minute) &&
(period === TaskSchedule.Hour || period === TaskSchedule.Day)
) {
await TaskClass.schedule({ limit });
// @ts-expect-error We won't instantiate an abstract class
await new TaskClass().schedule({ limit });
} else if (
TaskClass.cron === TaskSchedule.Hour &&
!receivedPeriods.has(TaskSchedule.Hour) &&
period === TaskSchedule.Day
) {
await TaskClass.schedule({ limit });
// @ts-expect-error We won't instantiate an abstract class
await new TaskClass().schedule({ limit });
}
}
+14 -21
View File
@@ -268,7 +268,7 @@ router.post(
}
const [documents, total] = await Promise.all([
Document.defaultScopeWithUser(user.id).findAll({
Document.withMembershipScope(user.id).findAll({
where,
order: [
[
@@ -348,7 +348,7 @@ router.post(
};
}
const documents = await Document.defaultScopeWithUser(user.id).findAll({
const documents = await Document.withMembershipScope(user.id).findAll({
where,
order: [
[
@@ -397,15 +397,11 @@ router.post(
const membershipScope: Readonly<ScopeOptions> = {
method: ["withMembership", user.id],
};
const collectionScope: Readonly<ScopeOptions> = {
method: ["withCollectionPermissions", user.id],
};
const viewScope: Readonly<ScopeOptions> = {
method: ["withViews", user.id],
};
const documents = await Document.scope([
membershipScope,
collectionScope,
viewScope,
"withDrafts",
]).findAll({
@@ -539,12 +535,14 @@ router.post(
delete where.updatedAt;
}
const documents = await Document.defaultScopeWithUser(user.id).findAll({
where,
order: [[sort, direction]],
offset: ctx.state.pagination.offset,
limit: ctx.state.pagination.limit,
});
const documents = await Document.withMembershipScope(user.id)
.scope("withDrafts")
.findAll({
where,
order: [[sort, direction]],
offset: ctx.state.pagination.offset,
limit: ctx.state.pagination.limit,
});
const data = await Promise.all(
documents.map((document) => presentDocument(ctx, document))
);
@@ -1539,7 +1537,7 @@ router.post(
acl,
});
const job = await DocumentImportTask.schedule({
const job = await new DocumentImportTask().schedule({
key,
sourceMetadata: {
fileName,
@@ -1549,6 +1547,7 @@ router.post(
collectionId,
parentDocumentId,
publish,
ip: ctx.request.ip,
});
const response: DocumentImportTaskResponse = await job.finished();
if ("error" in response) {
@@ -2032,13 +2031,7 @@ router.post(
const collectionIds = await user.collectionIds({
paranoid: false,
});
const collectionScope: Readonly<ScopeOptions> = {
method: ["withCollectionPermissions", user.id],
};
const documents = await Document.scope([
collectionScope,
"withDrafts",
]).findAll({
const documents = await Document.scope("withDrafts").findAll({
attributes: ["id"],
where: {
deletedAt: {
@@ -2062,7 +2055,7 @@ router.post(
});
if (documents.length) {
await EmptyTrashTask.schedule({
await new EmptyTrashTask().schedule({
documentIds: documents.map((doc) => doc.id),
});
}
@@ -24,6 +24,7 @@ router.post(
async (ctx: APIContext<T.GroupMembershipsListReq>) => {
const { groupId } = ctx.input.body;
const { user } = ctx.state.auth;
const userId = user.id;
const memberships = await GroupMembership.findAll({
where: {
@@ -44,7 +45,7 @@ router.post(
association: "groupUsers",
required: true,
where: {
userId: user.id,
userId,
},
},
],
@@ -57,15 +58,13 @@ router.post(
const documentIds = memberships
.map((p) => p.documentId)
.filter(Boolean) as string[];
const documents = await Document.scope([
"withDrafts",
{ method: ["withMembership", user.id] },
{ method: ["withCollectionPermissions", user.id] },
]).findAll({
where: {
id: documentIds,
},
});
const documents = await Document.withMembershipScope(userId)
.scope("withDrafts")
.findAll({
where: {
id: documentIds,
},
});
const groups = uniqBy(
memberships.map((membership) => membership.group),
@@ -0,0 +1,19 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`oauthAuthentications.delete should require authentication 1`] = `
{
"error": "authentication_required",
"message": "Authentication required",
"ok": false,
"status": 401,
}
`;
exports[`oauthAuthentications.list should require authentication 1`] = `
{
"error": "authentication_required",
"message": "Authentication required",
"ok": false,
"status": 401,
}
`;
@@ -0,0 +1,194 @@
import { OAuthClient, OAuthAuthentication } from "@server/models";
import {
buildOAuthAuthentication,
buildTeam,
buildUser,
} from "@server/test/factories";
import { getTestServer } from "@server/test/support";
const server = getTestServer();
describe("oauthAuthentications.list", () => {
it("should require authentication", async () => {
const res = await server.post("/api/oauthAuthentications.list");
const body = await res.json();
expect(res.status).toEqual(401);
expect(body).toMatchSnapshot();
});
it("should return list of oauth authentications for user", async () => {
const team = await buildTeam();
const user = await buildUser({ teamId: team.id });
const oauthClient = await OAuthClient.create({
teamId: team.id,
createdById: user.id,
name: "Test Client",
redirectUris: ["https://example.com/callback"],
});
await buildOAuthAuthentication({
oauthClientId: oauthClient.id,
user,
scope: ["read"],
});
const res = await server.post("/api/oauthAuthentications.list", {
body: {
token: user.getJwtToken(),
},
});
const body = await res.json();
expect(res.status).toEqual(200);
expect(body.data.length).toEqual(1);
expect(body.data[0].id).toBeDefined();
expect(body.data[0].oauthClient.name).toEqual("Test Client");
expect(body.policies).toBeDefined();
});
it("should only return authentications for requesting user", async () => {
const team = await buildTeam();
const user = await buildUser({ teamId: team.id });
const anotherUser = await buildUser({ teamId: team.id });
const oauthClient = await OAuthClient.create({
teamId: team.id,
createdById: user.id,
name: "Test Client",
redirectUris: ["https://example.com/callback"],
});
await buildOAuthAuthentication({
oauthClientId: oauthClient.id,
user: anotherUser,
scope: ["read"],
});
const res = await server.post("/api/oauthAuthentications.list", {
body: {
token: user.getJwtToken(),
},
});
const body = await res.json();
expect(res.status).toEqual(200);
expect(body.data.length).toEqual(0);
});
});
describe("oauthAuthentications.delete", () => {
it("should require authentication", async () => {
const res = await server.post("/api/oauthAuthentications.delete");
const body = await res.json();
expect(res.status).toEqual(401);
expect(body).toMatchSnapshot();
});
it("should delete all authentications for a client without scope", async () => {
const team = await buildTeam();
const user = await buildUser({ teamId: team.id });
const oauthClient = await OAuthClient.create({
teamId: team.id,
createdById: user.id,
name: "Test Client",
redirectUris: ["https://example.com/callback"],
});
await buildOAuthAuthentication({
oauthClientId: oauthClient.id,
user,
scope: ["read"],
});
const res = await server.post("/api/oauthAuthentications.delete", {
body: {
token: user.getJwtToken(),
oauthClientId: oauthClient.id,
},
});
const body = await res.json();
expect(res.status).toEqual(200);
expect(body.success).toBe(true);
const auths = await OAuthAuthentication.findAll({
where: {
userId: user.id,
oauthClientId: oauthClient.id,
},
});
expect(auths.length).toEqual(0);
});
it("should delete matching authentications for a client with scope", async () => {
const team = await buildTeam();
const user = await buildUser({ teamId: team.id });
const oauthClient = await OAuthClient.create({
teamId: team.id,
createdById: user.id,
name: "Test Client",
redirectUris: ["https://example.com/callback"],
});
await buildOAuthAuthentication({
oauthClientId: oauthClient.id,
user,
scope: ["read"],
});
await buildOAuthAuthentication({
oauthClientId: oauthClient.id,
user,
scope: ["write"],
});
const res = await server.post("/api/oauthAuthentications.delete", {
body: {
token: user.getJwtToken(),
oauthClientId: oauthClient.id,
scope: ["read"],
},
});
const body = await res.json();
expect(res.status).toEqual(200);
expect(body.success).toBe(true);
const auths = await OAuthAuthentication.findAll({
where: {
userId: user.id,
oauthClientId: oauthClient.id,
},
});
expect(auths.length).toEqual(1);
expect(auths[0].scope[0]).toEqual("write");
});
it("should only delete authentications for requesting user", async () => {
const team = await buildTeam();
const user = await buildUser({ teamId: team.id });
const anotherUser = await buildUser({ teamId: team.id });
const oauthClient = await OAuthClient.create({
teamId: team.id,
createdById: user.id,
name: "Test Client",
redirectUris: ["https://example.com/callback"],
});
const otherAuth = await buildOAuthAuthentication({
oauthClientId: oauthClient.id,
user: anotherUser,
scope: ["read"],
});
await server.post("/api/oauthAuthentications.delete", {
body: {
token: user.getJwtToken(),
oauthClientId: oauthClient.id,
scope: "read",
},
});
// Verify other user's auth still exists
const auth = await OAuthAuthentication.findByPk(otherAuth.id);
expect(auth).not.toBeNull();
});
});
@@ -66,12 +66,12 @@ router.post(
transaction(),
async (ctx: APIContext<T.OAuthAuthenticationsDeleteReq>) => {
const { user } = ctx.state.auth;
const { oauthClientId, scope } = ctx.request.body;
const { oauthClientId, scope } = ctx.input.body;
const oauthAuthentications = await OAuthAuthentication.findAll({
where: {
userId: user.id,
oauthClientId,
scope,
...(scope ? { scope } : {}),
},
transaction: ctx.state.transaction,
});
@@ -148,6 +148,46 @@ describe("oauthClients.info", () => {
expect(body.data.id).toBeUndefined();
expect(body.data.redirectUris).toBeUndefined();
});
it("should validate redirectUri parameter", async () => {
const team = await buildTeam();
const admin = await buildAdmin({ teamId: team.id });
const user = await buildUser();
const client = await OAuthClient.create({
teamId: team.id,
createdById: admin.id,
name: "Test Client",
redirectUris: [
"https://example.com/callback",
"https://another.com/callback",
],
published: true,
});
// Test with valid redirectUri
const validRes = await server.post("/api/oauthClients.info", {
body: {
token: user.getJwtToken(),
clientId: client.clientId,
redirectUri: "https://example.com/callback",
},
});
const validBody = await validRes.json();
expect(validRes.status).toEqual(200);
expect(validBody.data.name).toEqual("Test Client");
// Test with invalid redirectUri
const invalidRes = await server.post("/api/oauthClients.info", {
body: {
token: user.getJwtToken(),
clientId: client.clientId,
redirectUri: "https://malicious.com/callback",
},
});
expect(invalidRes.status).toEqual(400);
});
});
describe("oauthClients.create", () => {
@@ -1,5 +1,6 @@
import Router from "koa-router";
import { UserRole } from "@shared/types";
import { ValidationError } from "@server/errors";
import auth from "@server/middlewares/authentication";
import { rateLimiter } from "@server/middlewares/rateLimiter";
import { transaction } from "@server/middlewares/transaction";
@@ -49,7 +50,7 @@ router.post(
auth(),
validate(T.OAuthClientsInfoSchema),
async (ctx: APIContext<T.OAuthClientsInfoReq>) => {
const { id, clientId } = ctx.input.body;
const { id, clientId, redirectUri } = ctx.input.body;
const { user } = ctx.state.auth;
const oauthClient = await OAuthClient.findOne({
@@ -58,6 +59,10 @@ router.post(
});
authorize(user, "read", oauthClient);
if (redirectUri && !oauthClient.redirectUris.includes(redirectUri)) {
throw ValidationError("redirect_uri is invalid");
}
const isInternalApp = oauthClient.teamId === user.teamId;
ctx.body = {
+2
View File
@@ -10,6 +10,8 @@ export const OAuthClientsInfoSchema = BaseSchema.extend({
/** OAuth clientId */
clientId: z.string().optional(),
redirectUri: z.string().optional(),
})
.refine((data) => data.id || data.clientId, {
message: "Either id or clientId is required",
+1 -1
View File
@@ -113,7 +113,7 @@ router.post(
user.collectionIds(),
]);
const documents = await Document.defaultScopeWithUser(user.id).findAll({
const documents = await Document.withMembershipScope(user.id).findAll({
where: {
id: pins.map((pin) => pin.documentId),
collectionId: collectionIds,
+1 -1
View File
@@ -94,7 +94,7 @@ router.post(
.map((star) => star.documentId)
.filter(Boolean) as string[];
const documents = documentIds.length
? await Document.defaultScopeWithUser(user.id).findAll({
? await Document.withMembershipScope(user.id).findAll({
where: {
id: documentIds,
collectionId: collectionIds,
@@ -1,5 +1,4 @@
import Router from "koa-router";
import { Op, Sequelize } from "sequelize";
import auth from "@server/middlewares/authentication";
import { transaction } from "@server/middlewares/transaction";
@@ -46,14 +45,8 @@ router.post(
const documentIds = memberships
.map((p) => p.documentId)
.filter(Boolean) as string[];
const documents = await Document.scope([
"withDrafts",
{ method: ["withMembership", user.id] },
{ method: ["withCollectionPermissions", user.id] },
]).findAll({
where: {
id: documentIds,
},
const documents = await Document.findByIds(documentIds, {
userId: user.id,
});
const policies = presentPolicies(user, [...documents, ...memberships]);
+31 -26
View File
@@ -25,7 +25,7 @@ const oauth = new OAuth2Server({
router.post(
"/authorize",
rateLimiter(RateLimiterStrategy.FiftyPerHour),
rateLimiter(RateLimiterStrategy.OneHundredPerHour),
auth(),
async (ctx) => {
const { user } = ctx.state.auth;
@@ -42,7 +42,8 @@ router.post(
const response = new OAuth2Server.Response(ctx.response);
const authorizationCode = await oauth.authorize(request, response, {
allowEmptyState: true,
// Require state to prevent CSRF attacks
allowEmptyState: false,
authorizationCodeLifetime:
OAuthAuthorizationCode.authorizationCodeLifetime,
authenticateHandler: {
@@ -66,35 +67,39 @@ router.post(
}
);
router.post("/token", async (ctx) => {
// Note: These objects are mutated by the OAuth2Server library
const request = new OAuth2Server.Request(ctx.request);
const response = new OAuth2Server.Response(ctx.response);
const token = await oauth.token(request, response, {
accessTokenLifetime: OAuthAuthentication.accessTokenLifetime,
refreshTokenLifetime: OAuthAuthentication.refreshTokenLifetime,
});
router.post(
"/token",
rateLimiter(RateLimiterStrategy.OneHundredPerHour),
async (ctx) => {
// Note: These objects are mutated by the OAuth2Server library
const request = new OAuth2Server.Request(ctx.request);
const response = new OAuth2Server.Response(ctx.response);
const token = await oauth.token(request, response, {
accessTokenLifetime: OAuthAuthentication.accessTokenLifetime,
refreshTokenLifetime: OAuthAuthentication.refreshTokenLifetime,
});
if (response.headers) {
ctx.set(response.headers);
if (response.headers) {
ctx.set(response.headers);
}
ctx.body = {
access_token: token.accessToken,
refresh_token: token.refreshToken,
// OAuth2 spec says that the expires_in should be in seconds.
expires_in: token.accessTokenExpiresAt
? Math.round((token.accessTokenExpiresAt.getTime() - Date.now()) / 1000)
: undefined,
token_type: "Bearer",
// OAuth2 spec says that the scope should be a space-separated list.
scope: token.scope?.join(" "),
};
}
ctx.body = {
access_token: token.accessToken,
refresh_token: token.refreshToken,
// OAuth2 spec says that the expires_in should be in seconds.
expires_in: token.accessTokenExpiresAt
? Math.round((token.accessTokenExpiresAt.getTime() - Date.now()) / 1000)
: undefined,
token_type: "Bearer",
// OAuth2 spec says that the scope should be a space-separated list.
scope: token.scope?.join(" "),
};
});
);
router.post(
"/revoke",
rateLimiter(RateLimiterStrategy.FiftyPerHour),
rateLimiter(RateLimiterStrategy.OneHundredPerHour),
validate(T.TokenRevokeSchema),
transaction(),
async (ctx: APIContext<T.TokenRevokeReq>) => {
+2 -1
View File
@@ -7,7 +7,8 @@ export default function init() {
for (const name in tasks) {
const TaskClass = tasks[name];
if (TaskClass.cron === schedule) {
await TaskClass.schedule({ limit: 10000 });
// @ts-expect-error We won't instantiate an abstract class
await new TaskClass().schedule({ limit: 10000 });
}
}
}
+13 -1
View File
@@ -4,6 +4,7 @@ import cookie from "cookie";
import Koa from "koa";
import IO from "socket.io";
import { createAdapter } from "socket.io-redis";
import env from "@server/env";
import { AuthenticationError } from "@server/errors";
import Logger from "@server/logging/Logger";
import Metrics from "@server/logging/Metrics";
@@ -38,7 +39,8 @@ export default function init(
pingInterval: 15000,
pingTimeout: 30000,
cors: {
origin: "*",
// Included for completeness, though CORS does not apply to websocket transport.
origin: env.isCloudHosted ? "*" : env.URL,
methods: ["GET", "POST"],
},
});
@@ -60,6 +62,16 @@ export default function init(
"upgrade",
function (req: IncomingMessage, socket: Duplex, head: Buffer) {
if (req.url?.startsWith(path) && ioHandleUpgrade) {
// For on-premise deployments, ensure the websocket origin matches the deployed URL.
// In cloud-hosted we support any origin for custom domains.
if (
!env.isCloudHosted &&
(!req.headers.origin || !env.URL.startsWith(req.headers.origin))
) {
socket.end(`HTTP/1.1 400 Bad Request\r\n`);
return;
}
ioHandleUpgrade(req, socket, head);
return;
}
+7 -3
View File
@@ -756,15 +756,19 @@ export async function buildOAuthAuthorizationCode(
}
export async function buildOAuthAuthentication({
oauthClientId,
user,
scope,
}: {
oauthClientId?: string;
user: User;
scope: string[];
}) {
const oauthClient = await buildOAuthClient({
teamId: user.teamId,
});
const oauthClient = oauthClientId
? await OAuthClient.findByPk(oauthClientId, { rejectOnEmpty: true })
: await buildOAuthClient({
teamId: user.teamId,
});
const oauthInterfaceClient = {
id: oauthClient.clientId,
grants: ["authorization_code"],
+18 -12
View File
@@ -1,8 +1,8 @@
import crypto from "crypto";
import {
RefreshTokenModel,
AuthorizationCodeModel,
} from "@node-oauth/oauth2-server";
import rs from "randomstring";
import { Required } from "utility-types";
import { Scope } from "@shared/types";
import { isUrl } from "@shared/utils/urls";
@@ -41,17 +41,21 @@ export const OAuthInterface: RefreshTokenModel &
grants: ["authorization_code", "refresh_token"],
async generateAccessToken() {
return `${OAuthAuthentication.accessTokenPrefix}${rs.generate(32)}`;
return `${OAuthAuthentication.accessTokenPrefix}${crypto
.randomBytes(32)
.toString("hex")}`;
},
async generateRefreshToken() {
return `${OAuthAuthentication.refreshTokenPrefix}${rs.generate(32)}`;
return `${OAuthAuthentication.refreshTokenPrefix}${crypto
.randomBytes(32)
.toString("hex")}`;
},
async generateAuthorizationCode() {
return `${OAuthAuthorizationCode.authorizationCodePrefix}${rs.generate(
32
)}`;
return `${OAuthAuthorizationCode.authorizationCodePrefix}${crypto
.randomBytes(32)
.toString("hex")}`;
},
async getAccessToken(accessToken: string) {
@@ -139,10 +143,12 @@ export const OAuthInterface: RefreshTokenModel &
},
async saveToken(token, client, user) {
const accessToken = token.accessToken;
const refreshToken = token.refreshToken;
const accessTokenExpiresAt = token.accessTokenExpiresAt;
const refreshTokenExpiresAt = token.refreshTokenExpiresAt;
const {
accessToken,
refreshToken,
accessTokenExpiresAt,
refreshTokenExpiresAt,
} = token;
const accessTokenHash = hash(accessToken);
const refreshTokenHash = refreshToken ? hash(refreshToken) : undefined;
@@ -233,13 +239,13 @@ export const OAuthInterface: RefreshTokenModel &
* @returns True if the URI is valid, false otherwise.
*/
async validateRedirectUri(uri, client) {
if (uri.includes("#")) {
if (uri.includes("#") || uri.includes("*")) {
return false;
}
if (!client.redirectUris?.includes(uri)) {
return false;
}
if (!isUrl(uri)) {
if (!isUrl(uri, { requireHttps: true })) {
return false;
}
+1 -4
View File
@@ -99,10 +99,7 @@ export const getDocumentPermission = async ({
documentId: string;
skipMembershipId?: string;
}): Promise<DocumentPermission | undefined> => {
const document = await Document.scope({
method: ["withCollectionPermissions", userId],
}).findOne({ where: { id: documentId } });
const document = await Document.findByPk(documentId, { userId });
const permissions: DocumentPermission[] = [];
const collection = document?.collection;
+2 -2
View File
@@ -44,11 +44,11 @@ export function createTable({
};
}
function createTableInner(
export function createTableInner(
state: EditorState,
rowsCount: number,
colsCount: number,
colWidth: number,
colWidth?: number,
withHeaderRow = true,
cellContent?: Node
) {
+15
View File
@@ -1,5 +1,7 @@
import { chainCommands } from "prosemirror-commands";
import { InputRule } from "prosemirror-inputrules";
import { NodeSpec, Node as ProsemirrorNode } from "prosemirror-model";
import { TextSelection } from "prosemirror-state";
import {
addColumnAfter,
addRowAfter,
@@ -23,6 +25,7 @@ import {
deleteColSelection,
deleteRowSelection,
moveOutOfTable,
createTableInner,
} from "../commands/table";
import { MarkdownSerializerState } from "../lib/markdown/serializer";
import { FixTablesPlugin } from "../plugins/FixTables";
@@ -101,6 +104,18 @@ export default class Table extends Node {
};
}
inputRules() {
return [
new InputRule(/^(\|--)$/, (state, _, start, end) => {
const nodes = createTableInner(state, 2, 2);
const tr = state.tr.replaceWith(start - 1, end, nodes).scrollIntoView();
const resolvedPos = tr.doc.resolve(start + 1);
tr.setSelection(TextSelection.near(resolvedPos));
return tr;
}),
];
}
toMarkdown(state: MarkdownSerializerState, node: ProsemirrorNode) {
state.renderTable(node);
state.closeBlock(node);
+1 -1
View File
@@ -56,8 +56,8 @@ export const basicExtensions: Nodes = [
Emoji,
Text,
SimpleImage,
Bold,
Code,
Bold,
Italic,
Underline,
Link,
@@ -840,6 +840,7 @@
"Already have an account? Go to <1>login</1>.": "Already have an account? Go to <1>login</1>.",
"An error occurred": "An error occurred",
"The OAuth client could not be found, please check the provided client ID": "The OAuth client could not be found, please check the provided client ID",
"The OAuth client could not be loaded, please check the redirect URI is valid": "The OAuth client could not be loaded, please check the redirect URI is valid",
"Required OAuth parameters are missing": "Required OAuth parameters are missing",
"Authorize": "Authorize",
"{{ appName }} wants to access {{ teamName }}": "{{ appName }} wants to access {{ teamName }}",
@@ -887,6 +888,8 @@
"Manage which third-party and internal applications have been granted access to your {{ appName }} account.": "Manage which third-party and internal applications have been granted access to your {{ appName }} account.",
"API": "API",
"API keys can be used to authenticate with the API and programatically control\n your workspace's data. For more details see the <em>developer documentation</em>.": "API keys can be used to authenticate with the API and programatically control\n your workspace's data. For more details see the <em>developer documentation</em>.",
"Application published": "Application published",
"Application updated": "Application updated",
"Client secret rotated": "Client secret rotated",
"Rotate secret": "Rotate secret",
"Rotating the client secret will invalidate the current secret. Make sure to update any applications using these credentials.": "Rotating the client secret will invalidate the current secret. Make sure to update any applications using these credentials.",
+7
View File
@@ -113,4 +113,11 @@ export default createGlobalStyle<Props>`
outline-offset: -1px;
outline-width: initial;
}
:root {
--sat: env(safe-area-inset-top);
--sar: env(safe-area-inset-right);
--sab: env(safe-area-inset-bottom);
--sal: env(safe-area-inset-left);
}
`;
+26
View File
@@ -13,6 +13,32 @@ export function isTouchDevice(): boolean {
return window.matchMedia?.("(hover: none) and (pointer: coarse)")?.matches;
}
/**
* Returns the safe area insets for the current device.
*/
export function getSafeAreaInsets(): {
top: number;
right: number;
bottom: number;
left: number;
} {
// Check if CSS environment variables are supported
const style = getComputedStyle(document.documentElement);
const supportsEnv = window.CSS?.supports?.("top", "env(safe-area-inset-top)");
if (supportsEnv) {
return {
top: parseFloat(style.getPropertyValue("--sat") || "0"),
right: parseFloat(style.getPropertyValue("--sar") || "0"),
bottom: parseFloat(style.getPropertyValue("--sab") || "0"),
left: parseFloat(style.getPropertyValue("--sal") || "0"),
};
}
// Fallback to zero if not supported
return { top: 0, right: 0, bottom: 0, left: 0 };
}
/**
* Returns true if the client is running on a Mac.
*/
+12 -1
View File
@@ -98,7 +98,15 @@ export function isCollectionUrl(url: string) {
* @param options Parsing options.
* @returns True if a url, false otherwise.
*/
export function isUrl(text: string, options?: { requireHostname: boolean }) {
export function isUrl(
text: string,
options?: {
/** Require the url to have a hostname. */
requireHostname?: boolean;
/** Require the url not to use HTTP, custom protocols are ok. */
requireHttps?: boolean;
}
) {
if (text.match(/\n/)) {
return false;
}
@@ -113,6 +121,9 @@ export function isUrl(text: string, options?: { requireHostname: boolean }) {
if (url.hostname) {
return true;
}
if (options?.requireHttps && url.protocol === "http:") {
return false;
}
return (
url.protocol !== "" &&
+31
View File
@@ -94,6 +94,9 @@ export default () =>
modifyURLPrefix: {
"": `${environment.CDN_URL ?? ""}/static/`,
},
skipWaiting: true,
clientsClaim: true,
cleanupOutdatedCaches: true,
runtimeCaching: [
{
urlPattern: /api\/urls\.unfurl$/,
@@ -109,6 +112,34 @@ export default () =>
},
},
},
{
urlPattern: /api\/attachments\.redirect/,
handler: "CacheFirst",
options: {
cacheName: "attachments-redirect-cache",
expiration: {
maxEntries: 100,
maxAgeSeconds: 120, // 120 seconds
},
cacheableResponse: {
statuses: [0, 200, 302], // Include redirects
},
},
},
{
urlPattern: /api\/files\.get/,
handler: "CacheFirst",
options: {
cacheName: "files-cache",
expiration: {
maxEntries: 50,
maxAgeSeconds: 604800, // 7 days
},
cacheableResponse: {
statuses: [0, 200, 206], // Include partial content for range requests
},
},
},
],
},
manifest: {
+178 -178
View File
@@ -105,32 +105,32 @@
"@smithy/util-utf8" "^2.0.0"
tslib "^2.6.2"
"@aws-sdk/client-s3@3.787.0":
version "3.787.0"
resolved "https://registry.yarnpkg.com/@aws-sdk/client-s3/-/client-s3-3.787.0.tgz#ebb55ec36cd8f0b7e5e89e48c4d1b6ed1f6156dc"
integrity sha512-eGLCWkN0NlntJ9yPU6OKUggVS4cFvuZJog+cFg1KD5hniLqz7Y0YRtB4uBxW212fK3XCfddgyscEOEeHaTQQTw==
"@aws-sdk/client-s3@3.797.0":
version "3.797.0"
resolved "https://registry.yarnpkg.com/@aws-sdk/client-s3/-/client-s3-3.797.0.tgz#27d809ddc08b4f026bdf3b01c1c43c5cb5d06e43"
integrity sha512-N7pB94mXi4fCt+rYmR9TzfbbwZsWs6Mnk/jDNX9sAZyWkZQnS3AZ/nRtnUmdCimdnOPOMNVjmAoZ4mW3Ff8LDw==
dependencies:
"@aws-crypto/sha1-browser" "5.2.0"
"@aws-crypto/sha256-browser" "5.2.0"
"@aws-crypto/sha256-js" "5.2.0"
"@aws-sdk/core" "3.775.0"
"@aws-sdk/credential-provider-node" "3.787.0"
"@aws-sdk/core" "3.796.0"
"@aws-sdk/credential-provider-node" "3.797.0"
"@aws-sdk/middleware-bucket-endpoint" "3.775.0"
"@aws-sdk/middleware-expect-continue" "3.775.0"
"@aws-sdk/middleware-flexible-checksums" "3.787.0"
"@aws-sdk/middleware-flexible-checksums" "3.796.0"
"@aws-sdk/middleware-host-header" "3.775.0"
"@aws-sdk/middleware-location-constraint" "3.775.0"
"@aws-sdk/middleware-logger" "3.775.0"
"@aws-sdk/middleware-recursion-detection" "3.775.0"
"@aws-sdk/middleware-sdk-s3" "3.775.0"
"@aws-sdk/middleware-sdk-s3" "3.796.0"
"@aws-sdk/middleware-ssec" "3.775.0"
"@aws-sdk/middleware-user-agent" "3.787.0"
"@aws-sdk/middleware-user-agent" "3.796.0"
"@aws-sdk/region-config-resolver" "3.775.0"
"@aws-sdk/signature-v4-multi-region" "3.775.0"
"@aws-sdk/signature-v4-multi-region" "3.796.0"
"@aws-sdk/types" "3.775.0"
"@aws-sdk/util-endpoints" "3.787.0"
"@aws-sdk/util-user-agent-browser" "3.775.0"
"@aws-sdk/util-user-agent-node" "3.787.0"
"@aws-sdk/util-user-agent-node" "3.796.0"
"@aws-sdk/xml-builder" "3.775.0"
"@smithy/config-resolver" "^4.1.0"
"@smithy/core" "^3.2.0"
@@ -167,23 +167,23 @@
"@smithy/util-waiter" "^4.0.3"
tslib "^2.6.2"
"@aws-sdk/client-sso@3.787.0":
version "3.787.0"
resolved "https://registry.yarnpkg.com/@aws-sdk/client-sso/-/client-sso-3.787.0.tgz#39f1182296b586cb957b449b5f0dabd8f378cf1a"
integrity sha512-L8R+Mh258G0DC73ktpSVrG4TT9i2vmDLecARTDR/4q5sRivdDQSL5bUp3LKcK80Bx+FRw3UETIlX6mYMLL9PJQ==
"@aws-sdk/client-sso@3.797.0":
version "3.797.0"
resolved "https://registry.yarnpkg.com/@aws-sdk/client-sso/-/client-sso-3.797.0.tgz#94d123568830788980cc8712a94d3b20de735e4c"
integrity sha512-9xuR918p7tShR67ZL+AOSbydpJxSHAOdXcQswxxWR/hKCF7tULX7tyL3gNo3l/ETp0CDcStvorOdH/nCbzEOjw==
dependencies:
"@aws-crypto/sha256-browser" "5.2.0"
"@aws-crypto/sha256-js" "5.2.0"
"@aws-sdk/core" "3.775.0"
"@aws-sdk/core" "3.796.0"
"@aws-sdk/middleware-host-header" "3.775.0"
"@aws-sdk/middleware-logger" "3.775.0"
"@aws-sdk/middleware-recursion-detection" "3.775.0"
"@aws-sdk/middleware-user-agent" "3.787.0"
"@aws-sdk/middleware-user-agent" "3.796.0"
"@aws-sdk/region-config-resolver" "3.775.0"
"@aws-sdk/types" "3.775.0"
"@aws-sdk/util-endpoints" "3.787.0"
"@aws-sdk/util-user-agent-browser" "3.775.0"
"@aws-sdk/util-user-agent-node" "3.787.0"
"@aws-sdk/util-user-agent-node" "3.796.0"
"@smithy/config-resolver" "^4.1.0"
"@smithy/core" "^3.2.0"
"@smithy/fetch-http-handler" "^5.0.2"
@@ -211,40 +211,40 @@
"@smithy/util-utf8" "^4.0.0"
tslib "^2.6.2"
"@aws-sdk/core@3.775.0":
version "3.775.0"
resolved "https://registry.yarnpkg.com/@aws-sdk/core/-/core-3.775.0.tgz#5d22ba78f07c07b48fb4d5b18172b9a896c0cbd0"
integrity sha512-8vpW4WihVfz0DX+7WnnLGm3GuQER++b0IwQG35JlQMlgqnc44M//KbJPsIHA0aJUJVwJAEShgfr5dUbY8WUzaA==
"@aws-sdk/core@3.796.0":
version "3.796.0"
resolved "https://registry.yarnpkg.com/@aws-sdk/core/-/core-3.796.0.tgz#6076b78772c1eb97ec6ea9064c85ce500e0aa889"
integrity sha512-tH8Sp7lCxISVoLnkyv4AouuXs2CDlMhTuesWa0lq2NX1f+DXsMwSBtN37ttZdpFMw3F8mWdsJt27X9h2Oq868A==
dependencies:
"@aws-sdk/types" "3.775.0"
"@smithy/core" "^3.2.0"
"@smithy/node-config-provider" "^4.0.2"
"@smithy/property-provider" "^4.0.2"
"@smithy/protocol-http" "^5.1.0"
"@smithy/signature-v4" "^5.0.2"
"@smithy/signature-v4" "^5.1.0"
"@smithy/smithy-client" "^4.2.0"
"@smithy/types" "^4.2.0"
"@smithy/util-middleware" "^4.0.2"
fast-xml-parser "4.4.1"
tslib "^2.6.2"
"@aws-sdk/credential-provider-env@3.775.0":
version "3.775.0"
resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-env/-/credential-provider-env-3.775.0.tgz#b8c81818f4c62d89b5f04dc410ab9b48e954f22c"
integrity sha512-6ESVxwCbGm7WZ17kY1fjmxQud43vzJFoLd4bmlR+idQSWdqlzGDYdcfzpjDKTcivdtNrVYmFvcH1JBUwCRAZhw==
"@aws-sdk/credential-provider-env@3.796.0":
version "3.796.0"
resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-env/-/credential-provider-env-3.796.0.tgz#4ed6903814868b0f9daa8c8db449b1f1adcda041"
integrity sha512-kQzGKm4IOYYO6vUrai2JocNwhJm4Aml2BsAV+tBhFhhkutE7khf9PUucoVjB78b0J48nF+kdSacqzY+gB81/Uw==
dependencies:
"@aws-sdk/core" "3.775.0"
"@aws-sdk/core" "3.796.0"
"@aws-sdk/types" "3.775.0"
"@smithy/property-provider" "^4.0.2"
"@smithy/types" "^4.2.0"
tslib "^2.6.2"
"@aws-sdk/credential-provider-http@3.775.0":
version "3.775.0"
resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-http/-/credential-provider-http-3.775.0.tgz#0fbc7f4e6cada37fc9b647de0d7c12a42a44bcc6"
integrity sha512-PjDQeDH/J1S0yWV32wCj2k5liRo0ssXMseCBEkCsD3SqsU8o5cU82b0hMX4sAib/RkglCSZqGO0xMiN0/7ndww==
"@aws-sdk/credential-provider-http@3.796.0":
version "3.796.0"
resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-http/-/credential-provider-http-3.796.0.tgz#7f36074021b2605dba4b758b4b0ca98fb5b965ad"
integrity sha512-wWOT6VAHIKOuHdKFGm1iyKvx7f6+Kc/YTzFWJPuT+l+CPlXR6ylP1UMIDsHHLKpMzsrh3CH77QDsjkhQrnKkfg==
dependencies:
"@aws-sdk/core" "3.775.0"
"@aws-sdk/core" "3.796.0"
"@aws-sdk/types" "3.775.0"
"@smithy/fetch-http-handler" "^5.0.2"
"@smithy/node-http-handler" "^4.0.4"
@@ -255,18 +255,18 @@
"@smithy/util-stream" "^4.2.0"
tslib "^2.6.2"
"@aws-sdk/credential-provider-ini@3.787.0":
version "3.787.0"
resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.787.0.tgz#906ece004141462ae695504b6c07d1200688fd6c"
integrity sha512-hc2taRoDlXn2uuNuHWDJljVWYrp3r9JF1a/8XmOAZhVUNY+ImeeStylHXhXXKEA4JOjW+5PdJj0f1UDkVCHJiQ==
"@aws-sdk/credential-provider-ini@3.797.0":
version "3.797.0"
resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.797.0.tgz#e617edac6dbd761e13826d49092b85a337bf29a2"
integrity sha512-Zpj6pJ2hnebrhLDr+x61ArMUkjHG6mfJRfamHxeVTgZkhLcwHjC5aM4u9pWTVugIaPY+VBtgkKPbi3TRbHlt2g==
dependencies:
"@aws-sdk/core" "3.775.0"
"@aws-sdk/credential-provider-env" "3.775.0"
"@aws-sdk/credential-provider-http" "3.775.0"
"@aws-sdk/credential-provider-process" "3.775.0"
"@aws-sdk/credential-provider-sso" "3.787.0"
"@aws-sdk/credential-provider-web-identity" "3.787.0"
"@aws-sdk/nested-clients" "3.787.0"
"@aws-sdk/core" "3.796.0"
"@aws-sdk/credential-provider-env" "3.796.0"
"@aws-sdk/credential-provider-http" "3.796.0"
"@aws-sdk/credential-provider-process" "3.796.0"
"@aws-sdk/credential-provider-sso" "3.797.0"
"@aws-sdk/credential-provider-web-identity" "3.797.0"
"@aws-sdk/nested-clients" "3.797.0"
"@aws-sdk/types" "3.775.0"
"@smithy/credential-provider-imds" "^4.0.2"
"@smithy/property-provider" "^4.0.2"
@@ -274,17 +274,17 @@
"@smithy/types" "^4.2.0"
tslib "^2.6.2"
"@aws-sdk/credential-provider-node@3.787.0":
version "3.787.0"
resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-node/-/credential-provider-node-3.787.0.tgz#3e5cdafb0fecca25b7430f848cbca85000b25c33"
integrity sha512-JioVi44B1vDMaK2CdzqimwvJD3uzvzbQhaEWXsGMBcMcNHajXAXf08EF50JG3ZhLrhhUsT1ObXpbTaPINOhh+g==
"@aws-sdk/credential-provider-node@3.797.0":
version "3.797.0"
resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-node/-/credential-provider-node-3.797.0.tgz#bf82e239b14c8fcc7da660c2bc1ec042123638d9"
integrity sha512-xJSWvvnmzEfHbqbpN4F3E3mI9+zJ/VWLGiKOjzX1Inbspa5WqNn2GoMamolZR2TvvZS4F3Hp73TD1WoBzkIjuw==
dependencies:
"@aws-sdk/credential-provider-env" "3.775.0"
"@aws-sdk/credential-provider-http" "3.775.0"
"@aws-sdk/credential-provider-ini" "3.787.0"
"@aws-sdk/credential-provider-process" "3.775.0"
"@aws-sdk/credential-provider-sso" "3.787.0"
"@aws-sdk/credential-provider-web-identity" "3.787.0"
"@aws-sdk/credential-provider-env" "3.796.0"
"@aws-sdk/credential-provider-http" "3.796.0"
"@aws-sdk/credential-provider-ini" "3.797.0"
"@aws-sdk/credential-provider-process" "3.796.0"
"@aws-sdk/credential-provider-sso" "3.797.0"
"@aws-sdk/credential-provider-web-identity" "3.797.0"
"@aws-sdk/types" "3.775.0"
"@smithy/credential-provider-imds" "^4.0.2"
"@smithy/property-provider" "^4.0.2"
@@ -292,57 +292,57 @@
"@smithy/types" "^4.2.0"
tslib "^2.6.2"
"@aws-sdk/credential-provider-process@3.775.0":
version "3.775.0"
resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-process/-/credential-provider-process-3.775.0.tgz#7ab90383f12461c5d20546e933924e654660542b"
integrity sha512-A6k68H9rQp+2+7P7SGO90Csw6nrUEm0Qfjpn9Etc4EboZhhCLs9b66umUsTsSBHus4FDIe5JQxfCUyt1wgNogg==
"@aws-sdk/credential-provider-process@3.796.0":
version "3.796.0"
resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-process/-/credential-provider-process-3.796.0.tgz#d22d8adf73985fb218ff74365cd86b71bbd64513"
integrity sha512-r4e8/4AdKn/qQbRVocW7oXkpoiuXdTv0qty8AASNLnbQnT1vjD1bvmP6kp4fbHPWgwY8I9h0Dqjp49uy9Bqyuw==
dependencies:
"@aws-sdk/core" "3.775.0"
"@aws-sdk/core" "3.796.0"
"@aws-sdk/types" "3.775.0"
"@smithy/property-provider" "^4.0.2"
"@smithy/shared-ini-file-loader" "^4.0.2"
"@smithy/types" "^4.2.0"
tslib "^2.6.2"
"@aws-sdk/credential-provider-sso@3.787.0":
version "3.787.0"
resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.787.0.tgz#77ab6c01e4497d7ff2e6c7f081f3d8695744884b"
integrity sha512-fHc08bsvwm4+dEMEQKnQ7c1irEQmmxbgS+Fq41y09pPvPh31nAhoMcjBSTWAaPHvvsRbTYvmP4Mf12ZGr8/nfg==
"@aws-sdk/credential-provider-sso@3.797.0":
version "3.797.0"
resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.797.0.tgz#ea22714498157c937acd3170660b10ae0348366a"
integrity sha512-VlyWnjTsTnBXqXcEW0nw3S7nj00n9fYwF6uU6HPO9t860yIySG01lNPAWTvAt3DfVL5SRS0GANriCZF6ohcMcQ==
dependencies:
"@aws-sdk/client-sso" "3.787.0"
"@aws-sdk/core" "3.775.0"
"@aws-sdk/token-providers" "3.787.0"
"@aws-sdk/client-sso" "3.797.0"
"@aws-sdk/core" "3.796.0"
"@aws-sdk/token-providers" "3.797.0"
"@aws-sdk/types" "3.775.0"
"@smithy/property-provider" "^4.0.2"
"@smithy/shared-ini-file-loader" "^4.0.2"
"@smithy/types" "^4.2.0"
tslib "^2.6.2"
"@aws-sdk/credential-provider-web-identity@3.787.0":
version "3.787.0"
resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.787.0.tgz#d492d1f4a90b70f3a71a65f11b8d3ef79fb2759e"
integrity sha512-SobmCwNbk6TfEsF283mZPQEI5vV2j6eY5tOCj8Er4Lzraxu9fBPADV+Bib2A8F6jlB1lMPJzOuDCbEasSt/RIw==
"@aws-sdk/credential-provider-web-identity@3.797.0":
version "3.797.0"
resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.797.0.tgz#e52b5a054a71f83ffafe3461b41851d5008d84de"
integrity sha512-DIb05FEmdOX7bNsqSVEAB3UkaDgrYHonQ2+gcBLqZ7LoDNnovHIlvC5jii93usgEStxITZstnzw+49keNEgVWw==
dependencies:
"@aws-sdk/core" "3.775.0"
"@aws-sdk/nested-clients" "3.787.0"
"@aws-sdk/core" "3.796.0"
"@aws-sdk/nested-clients" "3.797.0"
"@aws-sdk/types" "3.775.0"
"@smithy/property-provider" "^4.0.2"
"@smithy/types" "^4.2.0"
tslib "^2.6.2"
"@aws-sdk/crt-loader@3.787.0":
version "3.787.0"
resolved "https://registry.yarnpkg.com/@aws-sdk/crt-loader/-/crt-loader-3.787.0.tgz#570c7e3cc79d20b2391beb1dafe2215343a9ac29"
integrity sha512-NQWFDkYF/lzz2m3icdVr+a0Ua/fN4dij3GPwU+Hr/nzrFR6z7txG3U4m2zkSELJ0PDT4k/1NsgmnQlpyxg0NDg==
"@aws-sdk/crt-loader@3.796.0":
version "3.796.0"
resolved "https://registry.yarnpkg.com/@aws-sdk/crt-loader/-/crt-loader-3.796.0.tgz#1cba5b2ea9f50b7a904cb5902a7751b90b65b1b3"
integrity sha512-dfTl4hCDKJSJTq4mPSQsv65p9XlYof+SVWKxQRu6OQBCOuhGPEhr7vN0byONBdwjhiY0WECjCANDi84lDVysdQ==
dependencies:
"@aws-sdk/util-user-agent-node" "3.787.0"
"@aws-sdk/util-user-agent-node" "3.796.0"
aws-crt "^1.24.0"
tslib "^2.6.2"
"@aws-sdk/lib-storage@3.787.0":
version "3.787.0"
resolved "https://registry.yarnpkg.com/@aws-sdk/lib-storage/-/lib-storage-3.787.0.tgz#fb7af2a869c31948073e7480ed1003c9755d35c4"
integrity sha512-iIMbmd9uASD3KFfGeH/OeVo4oxAeqbuXDmhoVEJb4M0hZ4yJ2o9v1V0TEaHwIXcrdlc0H8rCpd9opmV74MBxrA==
"@aws-sdk/lib-storage@3.797.0":
version "3.797.0"
resolved "https://registry.yarnpkg.com/@aws-sdk/lib-storage/-/lib-storage-3.797.0.tgz#8947c3f27fcd7b77ce171929b2ee96feed9ba3b7"
integrity sha512-hGacJXiFBnhkDhDuirptViR0ZfpvE6ENt+xJFV76F5OX8RvO7UhEkq9wdq/GzlSwrPB2XBfoYQgdtHJpjHs2zA==
dependencies:
"@smithy/abort-controller" "^4.0.2"
"@smithy/middleware-endpoint" "^4.1.0"
@@ -375,15 +375,15 @@
"@smithy/types" "^4.2.0"
tslib "^2.6.2"
"@aws-sdk/middleware-flexible-checksums@3.787.0":
version "3.787.0"
resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-flexible-checksums/-/middleware-flexible-checksums-3.787.0.tgz#bc887dcfd0193a41eef6b58c18c682478a108b07"
integrity sha512-X71qEwWoixFmwowWzlPoZUR3u1CWJ7iAzU0EzIxqmPhQpQJLFmdL1+SRjqATynDPZQzLs1a5HBtPT++EnZ+Quw==
"@aws-sdk/middleware-flexible-checksums@3.796.0":
version "3.796.0"
resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-flexible-checksums/-/middleware-flexible-checksums-3.796.0.tgz#c5a13c5dc7fd217e3d3514d3a75ae71c4b0ce5c6"
integrity sha512-JTqnyzGlbvXDcEnBtd5LFNrCFKUHnGyp/V9+BkvzNP02WXABLWzYvj1TCaf5pQySwK/b4kVn5lvbpTi0rXqjZw==
dependencies:
"@aws-crypto/crc32" "5.2.0"
"@aws-crypto/crc32c" "5.2.0"
"@aws-crypto/util" "5.2.0"
"@aws-sdk/core" "3.775.0"
"@aws-sdk/core" "3.796.0"
"@aws-sdk/types" "3.775.0"
"@smithy/is-array-buffer" "^4.0.0"
"@smithy/node-config-provider" "^4.0.2"
@@ -432,18 +432,18 @@
"@smithy/types" "^4.2.0"
tslib "^2.6.2"
"@aws-sdk/middleware-sdk-s3@3.775.0":
version "3.775.0"
resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.775.0.tgz#7b65832ec5a9ccccc8c7337780f722fa59f09d41"
integrity sha512-zsvcu7cWB28JJ60gVvjxPCI7ZU7jWGcpNACPiZGyVtjYXwcxyhXbYEVDSWKsSA6ERpz9XrpLYod8INQWfW3ECg==
"@aws-sdk/middleware-sdk-s3@3.796.0":
version "3.796.0"
resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.796.0.tgz#f20d77f02f27fd47178500581ae340f012652004"
integrity sha512-5o78oE79sGOtYkL7Up02h2nmr9UhGQZJgxE29EBdTw4dZ1EaA46L+C8oA+fBCmAB5xPQsjQqvhRrsr4Lcp+jZQ==
dependencies:
"@aws-sdk/core" "3.775.0"
"@aws-sdk/core" "3.796.0"
"@aws-sdk/types" "3.775.0"
"@aws-sdk/util-arn-parser" "3.723.0"
"@smithy/core" "^3.2.0"
"@smithy/node-config-provider" "^4.0.2"
"@smithy/protocol-http" "^5.1.0"
"@smithy/signature-v4" "^5.0.2"
"@smithy/signature-v4" "^5.1.0"
"@smithy/smithy-client" "^4.2.0"
"@smithy/types" "^4.2.0"
"@smithy/util-config-provider" "^4.0.0"
@@ -461,12 +461,12 @@
"@smithy/types" "^4.2.0"
tslib "^2.6.2"
"@aws-sdk/middleware-user-agent@3.787.0":
version "3.787.0"
resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.787.0.tgz#3d657c0ba1aec72bca079f4691ba20f25569fcfc"
integrity sha512-Lnfj8SmPLYtrDFthNIaNj66zZsBCam+E4XiUDr55DIHTGstH6qZ/q6vg0GfbukxwSmUcGMwSR4Qbn8rb8yd77g==
"@aws-sdk/middleware-user-agent@3.796.0":
version "3.796.0"
resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.796.0.tgz#80149d7f9034d41d35de88d96a6b73c1cb06413b"
integrity sha512-IeNg+3jNWT37J45opi5Jx89hGF0lOnZjiNwlMp3rKq7PlOqy8kWq5J1Gxk0W3tIkPpuf68CtBs/QFrRXWOjsZw==
dependencies:
"@aws-sdk/core" "3.775.0"
"@aws-sdk/core" "3.796.0"
"@aws-sdk/types" "3.775.0"
"@aws-sdk/util-endpoints" "3.787.0"
"@smithy/core" "^3.2.0"
@@ -474,23 +474,23 @@
"@smithy/types" "^4.2.0"
tslib "^2.6.2"
"@aws-sdk/nested-clients@3.787.0":
version "3.787.0"
resolved "https://registry.yarnpkg.com/@aws-sdk/nested-clients/-/nested-clients-3.787.0.tgz#e8a5a6e7d0b599a7f9f15b900d3223ad080b0a81"
integrity sha512-xk03q1xpKNHgbuo+trEf1dFrI239kuMmjKKsqLEsHlAZbuFq4yRGMlHBrVMnKYOPBhVFDS/VineM991XI52fKg==
"@aws-sdk/nested-clients@3.797.0":
version "3.797.0"
resolved "https://registry.yarnpkg.com/@aws-sdk/nested-clients/-/nested-clients-3.797.0.tgz#59699196a9b3fa43b021bdf1715e531d74885f75"
integrity sha512-xCsRKdsv0GAg9E28fvYBdC3JR2xdtZ2o41MVknOs+pSFtMsZm3SsgxObN35p1OTMk/o/V0LORGVLnFQMlc5QiA==
dependencies:
"@aws-crypto/sha256-browser" "5.2.0"
"@aws-crypto/sha256-js" "5.2.0"
"@aws-sdk/core" "3.775.0"
"@aws-sdk/core" "3.796.0"
"@aws-sdk/middleware-host-header" "3.775.0"
"@aws-sdk/middleware-logger" "3.775.0"
"@aws-sdk/middleware-recursion-detection" "3.775.0"
"@aws-sdk/middleware-user-agent" "3.787.0"
"@aws-sdk/middleware-user-agent" "3.796.0"
"@aws-sdk/region-config-resolver" "3.775.0"
"@aws-sdk/types" "3.775.0"
"@aws-sdk/util-endpoints" "3.787.0"
"@aws-sdk/util-user-agent-browser" "3.775.0"
"@aws-sdk/util-user-agent-node" "3.787.0"
"@aws-sdk/util-user-agent-node" "3.796.0"
"@smithy/config-resolver" "^4.1.0"
"@smithy/core" "^3.2.0"
"@smithy/fetch-http-handler" "^5.0.2"
@@ -530,27 +530,27 @@
"@smithy/util-middleware" "^4.0.2"
tslib "^2.6.2"
"@aws-sdk/s3-presigned-post@3.787.0":
version "3.787.0"
resolved "https://registry.yarnpkg.com/@aws-sdk/s3-presigned-post/-/s3-presigned-post-3.787.0.tgz#06b73ff35afc165a3208fc9a858b12a1e53d5148"
integrity sha512-/mM9VvdjC5fBa1WqlygmjTZ4R2fNsMjSc03JfdtcLWDShFZ61gungGZVSvsBbwh/YcVe1sKOnJz4qBQxl/ey8g==
"@aws-sdk/s3-presigned-post@3.797.0":
version "3.797.0"
resolved "https://registry.yarnpkg.com/@aws-sdk/s3-presigned-post/-/s3-presigned-post-3.797.0.tgz#37f87d205bbb649c82ecd0ce1831755e4ff25d7b"
integrity sha512-imfnmmH4P6hVh7NE51wP5FoBzyzph2dYqzAC/bF2bXjCv+/FaiPEt598K0A1mt4BUB5Y+0J3GcigOwzWHFU/SA==
dependencies:
"@aws-sdk/client-s3" "3.787.0"
"@aws-sdk/client-s3" "3.797.0"
"@aws-sdk/types" "3.775.0"
"@aws-sdk/util-format-url" "3.775.0"
"@smithy/middleware-endpoint" "^4.1.0"
"@smithy/signature-v4" "^5.0.2"
"@smithy/signature-v4" "^5.1.0"
"@smithy/types" "^4.2.0"
"@smithy/util-hex-encoding" "^4.0.0"
"@smithy/util-utf8" "^4.0.0"
tslib "^2.6.2"
"@aws-sdk/s3-request-presigner@3.787.0":
version "3.787.0"
resolved "https://registry.yarnpkg.com/@aws-sdk/s3-request-presigner/-/s3-request-presigner-3.787.0.tgz#1a8eadf330446e53cdca11fa0c08ae3d45a00f9c"
integrity sha512-WBm0AS3RRURNN0yjYXHaiI692boVwWXGt3RLdI7tYBX58E1Zb5nzC8rlk81O9Xe7ZTgTC1KCr8y4+jcBD+zwJg==
"@aws-sdk/s3-request-presigner@3.797.0":
version "3.797.0"
resolved "https://registry.yarnpkg.com/@aws-sdk/s3-request-presigner/-/s3-request-presigner-3.797.0.tgz#f375beb8a892f2b8452d0d6da38bd60f46afc4ab"
integrity sha512-ncEsGwmQIkXi3IREbGLC6X5sY38WM1WUAdaU5uQmBPOGEVQHtDyrIObBsIzK99j83SGWi8RqO7/n0jMGZPmeQw==
dependencies:
"@aws-sdk/signature-v4-multi-region" "3.775.0"
"@aws-sdk/signature-v4-multi-region" "3.796.0"
"@aws-sdk/types" "3.775.0"
"@aws-sdk/util-format-url" "3.775.0"
"@smithy/middleware-endpoint" "^4.1.0"
@@ -559,38 +559,38 @@
"@smithy/types" "^4.2.0"
tslib "^2.6.2"
"@aws-sdk/signature-v4-crt@^3.787.0":
version "3.787.0"
resolved "https://registry.yarnpkg.com/@aws-sdk/signature-v4-crt/-/signature-v4-crt-3.787.0.tgz#6932f0f437536fc528d4cc45a79daf08819d64a3"
integrity sha512-TATbx7B/54UIyLawAM0eTkQfnfn9KlEXV1jymniEHQtsfL68VND9/uFdOp51Ob9eTo5Q3qghH0RMHZaOpRVuGA==
"@aws-sdk/signature-v4-crt@^3.796.0":
version "3.796.0"
resolved "https://registry.yarnpkg.com/@aws-sdk/signature-v4-crt/-/signature-v4-crt-3.796.0.tgz#28df0cf702aa7e2c5e50e359776bf0f36b648e0b"
integrity sha512-RCYhLuRE8mhzpE5DzU3Mu11M1KdC480OQ+coB/h0c39VlYkcE8MGTB+eOjGOoYQx3RtFBs/y0RTSR4kj8Oo21Q==
dependencies:
"@aws-sdk/crt-loader" "3.787.0"
"@aws-sdk/signature-v4-multi-region" "3.775.0"
"@aws-sdk/crt-loader" "3.796.0"
"@aws-sdk/signature-v4-multi-region" "3.796.0"
"@aws-sdk/types" "3.775.0"
"@smithy/querystring-parser" "^4.0.2"
"@smithy/signature-v4" "^5.0.2"
"@smithy/signature-v4" "^5.1.0"
"@smithy/types" "^4.2.0"
"@smithy/util-middleware" "^4.0.2"
tslib "^2.6.2"
"@aws-sdk/signature-v4-multi-region@3.775.0":
version "3.775.0"
resolved "https://registry.yarnpkg.com/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.775.0.tgz#80cf60f3c9a9ea00f86529f2c4497a8ce936960a"
integrity sha512-cnGk8GDfTMJ8p7+qSk92QlIk2bmTmFJqhYxcXZ9PysjZtx0xmfCMxnG3Hjy1oU2mt5boPCVSOptqtWixayM17g==
"@aws-sdk/signature-v4-multi-region@3.796.0":
version "3.796.0"
resolved "https://registry.yarnpkg.com/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.796.0.tgz#b32b0ee252c87a7c18c215407b1f189927ef850c"
integrity sha512-JAOLdvazTc9HlTFslSrIOrKRMuOruuM3FeGw0hyfLP/RIbjd9bqe/xLIzDSJr3wpCpJs0sXoofwJgXtgTipvjA==
dependencies:
"@aws-sdk/middleware-sdk-s3" "3.775.0"
"@aws-sdk/middleware-sdk-s3" "3.796.0"
"@aws-sdk/types" "3.775.0"
"@smithy/protocol-http" "^5.1.0"
"@smithy/signature-v4" "^5.0.2"
"@smithy/signature-v4" "^5.1.0"
"@smithy/types" "^4.2.0"
tslib "^2.6.2"
"@aws-sdk/token-providers@3.787.0":
version "3.787.0"
resolved "https://registry.yarnpkg.com/@aws-sdk/token-providers/-/token-providers-3.787.0.tgz#18c761fb21ee25c8c3a35703876f0c733b4ae743"
integrity sha512-d7/NIqxq308Zg0RPMNrmn0QvzniL4Hx8Qdwzr6YZWLYAbUSvZYS2ppLR3BFWSkV6SsTJUx8BuDaj3P8vttkrog==
"@aws-sdk/token-providers@3.797.0":
version "3.797.0"
resolved "https://registry.yarnpkg.com/@aws-sdk/token-providers/-/token-providers-3.797.0.tgz#9f8c316f1adfda592c8dc6e69082879896720696"
integrity sha512-TLFkP4BBdkH2zCXhG3JjaYrRft25MMZ+6/YDz1C/ikq2Zk8krUbVoSmhtYMVz10JtxAPiQ++w0vI/qbz2JSDXg==
dependencies:
"@aws-sdk/nested-clients" "3.787.0"
"@aws-sdk/nested-clients" "3.797.0"
"@aws-sdk/types" "3.775.0"
"@smithy/property-provider" "^4.0.2"
"@smithy/shared-ini-file-loader" "^4.0.2"
@@ -649,12 +649,12 @@
bowser "^2.11.0"
tslib "^2.6.2"
"@aws-sdk/util-user-agent-node@3.787.0":
version "3.787.0"
resolved "https://registry.yarnpkg.com/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.787.0.tgz#58e63e99586cde1c1314f74b94596780321442f5"
integrity sha512-mG7Lz8ydfG4SF9e8WSXiPQ/Lsn3n8A5B5jtPROidafi06I3ckV2WxyMLdwG14m919NoS6IOfWHyRGSqWIwbVKA==
"@aws-sdk/util-user-agent-node@3.796.0":
version "3.796.0"
resolved "https://registry.yarnpkg.com/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.796.0.tgz#72c2ea1f32d2eb1200e111f48fd8a3c005f5202c"
integrity sha512-9fQpNcHgVFitf1tbTT8V1xGRoRHSmOAWjrhevo6Tc0WoINMAKz+4JNqfVGWRE5Tmtpq0oHKo1RmvxXQQtJYciA==
dependencies:
"@aws-sdk/middleware-user-agent" "3.787.0"
"@aws-sdk/middleware-user-agent" "3.796.0"
"@aws-sdk/types" "3.775.0"
"@smithy/node-config-provider" "^4.0.2"
"@smithy/types" "^4.2.0"
@@ -4386,10 +4386,10 @@
"@smithy/types" "^4.2.0"
tslib "^2.6.2"
"@smithy/signature-v4@^5.0.2":
version "5.0.2"
resolved "https://registry.yarnpkg.com/@smithy/signature-v4/-/signature-v4-5.0.2.tgz#363854e946fbc5bc206ff82e79ada5d5c14be640"
integrity sha512-Mz+mc7okA73Lyz8zQKJNyr7lIcHLiPYp0+oiqiMNc/t7/Kf2BENs5d63pEj7oPqdjaum6g0Fc8wC78dY1TgtXw==
"@smithy/signature-v4@^5.1.0":
version "5.1.0"
resolved "https://registry.yarnpkg.com/@smithy/signature-v4/-/signature-v4-5.1.0.tgz#2c56e5b278482b04383d84ea2c07b7f0a8eb8f63"
integrity sha512-4t5WX60sL3zGJF/CtZsUQTs3UrZEDO2P7pEaElrekbLqkWPYkgqNW1oeiNYC6xXifBnT9dVBOnNQRvOE9riU9w==
dependencies:
"@smithy/is-array-buffer" "^4.0.0"
"@smithy/protocol-http" "^5.1.0"
@@ -12267,10 +12267,10 @@ nodemailer@^6.10.0:
resolved "https://registry.yarnpkg.com/nodemailer/-/nodemailer-6.10.0.tgz#1f24c9de94ad79c6206f66d132776b6503003912"
integrity sha512-SQ3wZCExjeSatLE/HBaXS5vqUOQk6GtBdIIKxiFdmm01mOQZX/POJkO3SUX1wDiYcwUOJwT23scFSC9fY2H8IA==
nodemon@^3.1.9:
version "3.1.9"
resolved "https://registry.yarnpkg.com/nodemon/-/nodemon-3.1.9.tgz#df502cdc3b120e1c3c0c6e4152349019efa7387b"
integrity sha512-hdr1oIb2p6ZSxu3PB2JWWYS7ZQ0qvaZsc3hK8DR8f02kRzc8rjYmxAIvdz+aYC+8F2IjNaB7HMcSDg8nQpJxyg==
nodemon@^3.1.10:
version "3.1.10"
resolved "https://registry.yarnpkg.com/nodemon/-/nodemon-3.1.10.tgz#5015c5eb4fffcb24d98cf9454df14f4fecec9bc1"
integrity sha512-WDjw3pJ0/0jMFmyNDp3gvY2YizjLmmOUQo6DEBY+JgdvW/yQ9mEeSw6H5ythl5Ny2ytb7f9C2nIbjSxMNzbJXw==
dependencies:
chokidar "^3.5.2"
debug "^4"
@@ -12851,30 +12851,30 @@ performance-now@^2.1.0:
resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b"
integrity "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns= sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow=="
pg-cloudflare@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/pg-cloudflare/-/pg-cloudflare-1.1.1.tgz#e6d5833015b170e23ae819e8c5d7eaedb472ca98"
integrity "sha1-5tWDMBWxcOI66Bnoxdfq7bRyypg= sha512-xWPagP/4B6BgFO+EKz3JONXv3YDgvkbVrGw2mTo3D6tVDQRh1e7cqVGvyR3BE+eQgAvx1XhW/iEASj4/jCWl3Q=="
pg-cloudflare@^1.2.5:
version "1.2.5"
resolved "https://registry.yarnpkg.com/pg-cloudflare/-/pg-cloudflare-1.2.5.tgz#2e3649c38a7a9c74a7e5327c8098a2fd9af595bd"
integrity sha512-OOX22Vt0vOSRrdoUPKJ8Wi2OpE/o/h9T8X1s4qSkCedbNah9ei2W2765be8iMVxQUsvgT7zIAT2eIa9fs5+vtg==
pg-connection-string@^2.6.1, pg-connection-string@^2.7.0:
version "2.7.0"
resolved "https://registry.yarnpkg.com/pg-connection-string/-/pg-connection-string-2.7.0.tgz#f1d3489e427c62ece022dba98d5262efcb168b37"
integrity sha512-PI2W9mv53rXJQEOb8xNR8lH7Hr+EKa6oJa38zsK0S/ky2er16ios1wLKhZyxzD7jUReiWokc9WK5nxSnC7W1TA==
pg-connection-string@^2.6.1, pg-connection-string@^2.8.5:
version "2.8.5"
resolved "https://registry.yarnpkg.com/pg-connection-string/-/pg-connection-string-2.8.5.tgz#82cefd0269cb64a09603342d9b69e8392e6eb6cd"
integrity sha512-Ni8FuZ8yAF+sWZzojvtLE2b03cqjO5jNULcHFfM9ZZ0/JXrgom5pBREbtnAw7oxsxJqHw9Nz/XWORUEL3/IFow==
pg-int8@1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/pg-int8/-/pg-int8-1.0.1.tgz#943bd463bf5b71b4170115f80f8efc9a0c0eb78c"
integrity "sha1-lDvUY79bcbQXARX4D478mgwOt4w= sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw=="
pg-pool@^3.8.0:
version "3.8.0"
resolved "https://registry.yarnpkg.com/pg-pool/-/pg-pool-3.8.0.tgz#e6bce7fc4506a8d6106551363fc5283e5445b776"
integrity sha512-VBw3jiVm6ZOdLBTIcXLNdSotb6Iy3uOCwDGFAksZCXmi10nyRvnP2v3jl4d+IsLYRyXf6o9hIm/ZtUzlByNUdw==
pg-pool@^3.9.6:
version "3.9.6"
resolved "https://registry.yarnpkg.com/pg-pool/-/pg-pool-3.9.6.tgz#c6fde89dee615d6c262724e68a3a37e9593157fc"
integrity sha512-rFen0G7adh1YmgvrmE5IPIqbb+IgEzENUm+tzm6MLLDSlPRoZVhzU1WdML9PV2W5GOdRA9qBKURlbt1OsXOsPw==
pg-protocol@^1.8.0:
version "1.8.0"
resolved "https://registry.yarnpkg.com/pg-protocol/-/pg-protocol-1.8.0.tgz#c707101dd07813868035a44571488e4b98639d48"
integrity sha512-jvuYlEkL03NRvOoyoRktBK7+qU5kOvlAwvmrH8sr3wbLrOdVWsRxQfz8mMy9sZFsqJ1hEWNfdWKI4SAmoL+j7g==
pg-protocol@^1.9.5:
version "1.9.5"
resolved "https://registry.yarnpkg.com/pg-protocol/-/pg-protocol-1.9.5.tgz#e544eff37d6ab79c26281d7c0b59ac9be4862686"
integrity sha512-DYTWtWpfd5FOro3UnAfwvhD8jh59r2ig8bPtc9H8Ds7MscE/9NYruUQWFAOuraRl29jwcT2kyMFQ3MxeaVjUhg==
pg-tsquery@^8.4.2:
version "8.4.2"
@@ -12892,18 +12892,18 @@ pg-types@^2.1.0:
postgres-date "~1.0.4"
postgres-interval "^1.1.0"
pg@^8.14.1:
version "8.14.1"
resolved "https://registry.yarnpkg.com/pg/-/pg-8.14.1.tgz#2e3d1f287b64797cdfc8d1ba000f61a7ff8d66ed"
integrity sha512-0TdbqfjwIun9Fm/r89oB7RFQ0bLgduAhiIqIXOsyKoiC/L54DbuAAzIEN/9Op0f1Po9X7iCPXGoa/Ah+2aI8Xw==
pg@^8.15.6:
version "8.15.6"
resolved "https://registry.yarnpkg.com/pg/-/pg-8.15.6.tgz#2a28e98fb6cab18b886ce58b2c184d712a94880a"
integrity sha512-yvao7YI3GdmmrslNVsZgx9PfntfWrnXwtR+K/DjI0I/sTKif4Z623um+sjVZ1hk5670B+ODjvHDAckKdjmPTsg==
dependencies:
pg-connection-string "^2.7.0"
pg-pool "^3.8.0"
pg-protocol "^1.8.0"
pg-connection-string "^2.8.5"
pg-pool "^3.9.6"
pg-protocol "^1.9.5"
pg-types "^2.1.0"
pgpass "1.x"
optionalDependencies:
pg-cloudflare "^1.1.1"
pg-cloudflare "^1.2.5"
pgpass@1.x:
version "1.0.4"
@@ -13542,10 +13542,10 @@ react-merge-refs@^2.1.1:
resolved "https://registry.yarnpkg.com/react-merge-refs/-/react-merge-refs-2.1.1.tgz#e46763f8f1b881c0226ee54a1a2a10ffefba0233"
integrity sha512-jLQXJ/URln51zskhgppGJ2ub7b2WFKGq3cl3NYKtlHoTG+dN2q7EzWrn3hN3EgPsTMvpR9tpq5ijdp7YwFZkag==
react-portal@^4.2.2:
version "4.2.2"
resolved "https://registry.yarnpkg.com/react-portal/-/react-portal-4.2.2.tgz#bff1e024147d6041ba8c530ffc99d4c8248f49fa"
integrity "sha1-v/HgJBR9YEG6jFMP/JnUyCSPSfo= sha512-vS18idTmevQxyQpnde0Td6ZcUlv+pD8GTyR42n3CHUQq9OHi1C4jDE4ZWEbEsrbrLRhSECYiao58cvocwMtP7Q=="
react-portal@^4.3.0:
version "4.3.0"
resolved "https://registry.yarnpkg.com/react-portal/-/react-portal-4.3.0.tgz#92ca3492b1309883134f317a6aa88004534c860f"
integrity sha512-qs/2uKq1ifB3J1+K8ExfgUvCDZqlqCkfOEhqTELEDTfosloKiuzOzc7hl7IQ/7nohiFZD41BUYU0boAsIsGYHw==
dependencies:
prop-types "^15.5.8"
@@ -15766,10 +15766,10 @@ vite-plugin-static-copy@^0.17.0:
fs-extra "^11.1.0"
picocolors "^1.0.0"
vite@^6.3.3:
version "6.3.3"
resolved "https://registry.yarnpkg.com/vite/-/vite-6.3.3.tgz#497392c3f2243194e4dbf09ea83e9a3dddf49b88"
integrity sha512-5nXH+QsELbFKhsEfWLkHrvgRpTdGJzqOZ+utSdmPTvwHmvU6ITTm3xx+mRusihkcI8GeC7lCDyn3kDtiki9scw==
vite@^6.3.4:
version "6.3.4"
resolved "https://registry.yarnpkg.com/vite/-/vite-6.3.4.tgz#d441a72c7cd9a93b719bb851250a4e6c119c9cff"
integrity sha512-BiReIiMS2fyFqbqNT/Qqt4CVITDU9M9vE+DKcVAsB+ZV0wvTKd+3hMbkpxz1b+NmEDMegpVbisKiAZOnvO92Sw==
dependencies:
esbuild "^0.25.0"
fdir "^6.4.4"