Compare commits

..

1 Commits

Author SHA1 Message Date
renovate[bot] c314d721ce chore(deps): update dependency typescript to v6 2026-06-02 16:23:09 +00:00
18 changed files with 463 additions and 696 deletions
+4 -4
View File
@@ -1,8 +1,8 @@
ARG DEVICE=cpu
FROM python:3.11-bookworm@sha256:121d86b6d08752968a7dddbc708849e5f3a839bbff47f32212b46d2a1d842bab AS builder-cpu
FROM python:3.11-bookworm@sha256:970c99f886b839fc8829289040c1845dadaf2cae46b37acc7710333158ec29b4 AS builder-cpu
FROM python:3.13-slim-trixie@sha256:b04b5d7233d2ad9c379e22ea8927cd1378cd15c60d4ef876c065b25ea8fb3bf3 AS builder-openvino
FROM python:3.13-slim-trixie@sha256:d168b8d9eb761f4d3fe305ebd04aeb7e7f2de0297cec5fb2f8f6403244621664 AS builder-openvino
FROM builder-cpu AS builder-cuda
@@ -39,12 +39,12 @@ RUN --mount=type=cache,target=/root/.cache/uv \
--mount=type=bind,source=pyproject.toml,target=pyproject.toml \
uv sync --frozen --extra ${DEVICE} --no-dev --no-editable --no-install-project --compile-bytecode --no-progress --active --link-mode copy
FROM python:3.11-slim-bookworm@sha256:8dca233de9f3d9bb410665f00a4da6dd06f331083137e0e98ccf227236fcc438 AS prod-cpu
FROM python:3.11-slim-bookworm@sha256:9c6f90801e6b68e772b7c0ca74260cbf7af9f320acec894e26fccdaccfbe3b47 AS prod-cpu
ENV LD_PRELOAD=/usr/lib/libmimalloc.so.2 \
MACHINE_LEARNING_MODEL_ARENA=false
FROM python:3.13-slim-trixie@sha256:b04b5d7233d2ad9c379e22ea8927cd1378cd15c60d4ef876c065b25ea8fb3bf3 AS prod-openvino
FROM python:3.13-slim-trixie@sha256:d168b8d9eb761f4d3fe305ebd04aeb7e7f2de0297cec5fb2f8f6403244621664 AS prod-openvino
RUN apt-get update && \
apt-get install --no-install-recommends -yqq ocl-icd-libopencl1 wget && \
-1
View File
@@ -49,7 +49,6 @@ try:
str(settings.http_keepalive_timeout_s),
"--graceful-timeout",
"10",
"--no-control-socket",
],
) as cmd:
cmd.wait()
+1 -2
View File
@@ -12,7 +12,7 @@ from zipfile import BadZipFile
import orjson
from fastapi import Depends, FastAPI, File, Form, HTTPException
from fastapi.responses import PlainTextResponse
from fastapi.responses import ORJSONResponse, PlainTextResponse
from onnxruntime.capi.onnxruntime_pybind11_state import InvalidProtobuf, NoSuchFile
from PIL.Image import Image
from pydantic import ValidationError
@@ -32,7 +32,6 @@ from .schemas import (
ModelIdentity,
ModelTask,
ModelType,
ORJSONResponse,
PipelineRequest,
T,
)
@@ -89,9 +89,7 @@ class OpenClipTextualEncoder(BaseCLIPTextualEncoder):
tokenizer: Tokenizer = Tokenizer.from_file(self.tokenizer_file_path.as_posix())
pad_id = tokenizer.token_to_id(pad_token)
if pad_id is None:
raise ValueError(f"Pad token '{pad_token}' not found in tokenizer vocab")
pad_id: int = tokenizer.token_to_id(pad_token)
tokenizer.enable_padding(length=context_length, pad_token=pad_token, pad_id=pad_id)
tokenizer.enable_truncation(max_length=context_length)
-7
View File
@@ -3,16 +3,9 @@ from typing import Any, Literal, Protocol, TypeGuard, TypeVar
import numpy as np
import numpy.typing as npt
import orjson
from fastapi.responses import JSONResponse
from typing_extensions import TypedDict
class ORJSONResponse(JSONResponse):
def render(self, content: Any) -> bytes:
return orjson.dumps(content, option=orjson.OPT_SERIALIZE_NUMPY)
class StrEnum(str, Enum):
value: str
+450 -498
View File
File diff suppressed because it is too large Load Diff
-46
View File
@@ -55,22 +55,6 @@
}
],
"uiHints": ["SmartAlbum"]
},
{
"name": "asset-webhook",
"title": "Send a webhook",
"description": "Send the information of newly uploaded assets to an external endpoint",
"trigger": "AssetCreate",
"steps": [
{
"method": "immich-plugin-core#webhook",
"config": {
"url": "",
"method": "POST"
}
}
],
"uiHints": ["Webhook"]
}
],
"methods": [
@@ -254,36 +238,6 @@
"required": ["albumIds"]
}
},
{
"name": "webhook",
"title": "Send a webhook",
"description": "Send the asset information to an external endpoint",
"types": ["AssetV1"],
"schema": {
"type": "object",
"properties": {
"url": {
"type": "string",
"title": "Webhook URL",
"description": "The endpoint that will receive the asset information"
},
"method": {
"type": "string",
"title": "HTTP method",
"enum": ["POST", "PUT", "PATCH"],
"default": "POST",
"description": "HTTP method used to send the request"
},
"secret": {
"type": "string",
"title": "Secret",
"description": "Optional value sent as the X-Immich-Webhook-Secret header so the receiver can verify the request"
}
},
"required": ["url"]
},
"uiHints": ["Webhook"]
},
{
"name": "noop1",
"title": "DEV: Nested properties",
-3
View File
@@ -22,7 +22,4 @@ declare module 'main' {
export function assetTimeline(): I32;
export function assetTrash(): I32;
export function assetAddToAlbums(): I32;
// integrations
export function webhook(): I32;
}
-38
View File
@@ -102,44 +102,6 @@ export const assetTrash = () => {
return wrapper<WorkflowType.AssetV1, { inverse?: boolean }>(() => ({}));
};
type WebhookConfig = {
url: string;
method?: 'PATCH' | 'POST' | 'PUT';
secret?: string;
};
export const webhook = () => {
return wrapper<WorkflowType.AssetV1, WebhookConfig>(({ config, data, trigger, workflow }) => {
const { url, method = 'POST', secret } = config;
if (!url) {
console.warn('Webhook step skipped: no URL configured');
return {};
}
const headers: Record<string, string> = {
'Content-Type': 'application/json',
'User-Agent': 'Immich',
};
if (secret) {
headers['X-Immich-Webhook-Secret'] = secret;
}
const body = JSON.stringify({
trigger,
workflowId: workflow.id,
asset: data.asset,
});
const response = Http.request({ url, method, headers }, body);
if (response.status >= 400) {
console.error(`Webhook request to ${url} failed with status ${response.status}`);
} else {
console.debug(`Webhook request to ${url} returned status ${response.status}`);
}
return {};
});
};
export const assetAddToAlbums = () => {
return wrapper<WorkflowType.AssetV1, { albumIds: string[]; albumName?: string }>(({ config, data, functions }) => {
const assetId = data.asset.id;
+1 -1
View File
@@ -31,7 +31,7 @@
"@types/node": "^24.12.4",
"esbuild": "^0.28.0",
"tsc-alias": "^1.8.16",
"typescript": "^5.9.3"
"typescript": "^6.0.0"
},
"peerDependencies": {
"@extism/js-pdk": "^1.1.1"
+2 -2
View File
@@ -348,8 +348,8 @@ importers:
specifier: ^1.8.16
version: 1.8.17
typescript:
specifier: ^5.9.3
version: 5.9.3
specifier: ^6.0.0
version: 6.0.3
packages/sdk:
dependencies:
+2 -2
View File
@@ -1,4 +1,4 @@
FROM ghcr.io/immich-app/base-server-dev:202606021219@sha256:63fa91aa011f6f2921dd32fe6d1be8d637e9bd7f3e3dd0c8e446afb31b282af4 AS builder
FROM ghcr.io/immich-app/base-server-dev:202605121138@sha256:127cc323590e1d64d765016492e1de1e9355da8f658f078aef7be6bede0fdd0f AS builder
ENV COREPACK_ENABLE_DOWNLOAD_PROMPT=0 \
CI=1 \
COREPACK_HOME=/tmp \
@@ -80,7 +80,7 @@ RUN --mount=type=cache,id=pnpm-packages,target=/buildcache/pnpm-store \
--mount=type=cache,id=mise-tools-${TARGETPLATFORM},target=/buildcache/mise \
mise //:plugins
FROM ghcr.io/immich-app/base-server-prod:202606021219@sha256:6ef9ef5859492149af770a6c884b5e2ddbaeef99f8885ea5f2d9f73625a3d9ec
FROM ghcr.io/immich-app/base-server-prod:202605121138@sha256:b346d42d86f42799fede6b6c9f6feed0018c5ee46e4ac31d1165f50737cee842
WORKDIR /usr/src/app
ENV NODE_ENV=production \
+1 -1
View File
@@ -1,5 +1,5 @@
# dev build
FROM ghcr.io/immich-app/base-server-dev:202606021219@sha256:63fa91aa011f6f2921dd32fe6d1be8d637e9bd7f3e3dd0c8e446afb31b282af4 AS dev
FROM ghcr.io/immich-app/base-server-dev:202605121138@sha256:127cc323590e1d64d765016492e1de1e9355da8f658f078aef7be6bede0fdd0f AS dev
COPY --from=ghcr.io/jdx/mise:2026.5.18@sha256:5bb3311994fa78cef307ca3077cdb18f9551da0886371fc26ea91ab56220ffc5 /usr/local/bin/mise /usr/local/bin/mise
@@ -213,8 +213,6 @@ export class PluginRepository {
{
useWasi: true,
runInWorker,
// allow plugins (e.g. the webhook workflow step) to make outbound HTTP requests
allowedHosts: ['*'],
functions: {
'extism:host/user': functions ?? {},
},
@@ -1,16 +0,0 @@
import { Kysely, sql } from 'kysely';
export async function up(db: Kysely<any>): Promise<void> {
// Delete unauthorized cross-owner asset faces
await sql`
DELETE FROM asset_face
USING person, asset
WHERE asset_face."personId" = person.id
AND asset_face."assetId" = asset.id
AND person."ownerId" != asset."ownerId"
`.execute(db);
}
export async function down(): Promise<void> {
// Not implemented: the deleted rows were unauthorized cross-owner entries
}
@@ -452,30 +452,6 @@ describe(PersonService.name, () => {
expect(mocks.person.update).not.toHaveBeenCalled();
expect(mocks.job.queueAll).not.toHaveBeenCalled();
});
it('should reject creating a face on an asset the user does not own', async () => {
const auth = AuthFactory.create();
const asset = AssetFactory.create();
const person = PersonFactory.create({ faceAssetId: null });
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set());
mocks.access.person.checkOwnerAccess.mockResolvedValue(new Set([person.id]));
await expect(
sut.createFace(auth, {
assetId: asset.id,
personId: person.id,
imageHeight: 500,
imageWidth: 400,
x: 10,
y: 20,
width: 100,
height: 110,
}),
).rejects.toBeInstanceOf(BadRequestException);
expect(mocks.person.createAssetFace).not.toHaveBeenCalled();
});
});
describe('createNewFeaturePhoto', () => {
+1 -1
View File
@@ -625,7 +625,7 @@ export class PersonService extends BaseService {
// TODO return a asset face response
async createFace(auth: AuthDto, dto: AssetFaceCreateDto): Promise<void> {
await Promise.all([
this.requireAccess({ auth, permission: Permission.AssetUpdate, ids: [dto.assetId] }),
this.requireAccess({ auth, permission: Permission.AssetRead, ids: [dto.assetId] }),
this.requireAccess({ auth, permission: Permission.PersonRead, ids: [dto.personId] }),
]);
@@ -1,8 +1,6 @@
import { WorkflowStepConfig, WorkflowTrigger } from '@immich/plugin-sdk';
import { Kysely } from 'kysely';
import { readFileSync } from 'node:fs';
import { createServer } from 'node:http';
import { AddressInfo } from 'node:net';
import { PluginManifestDto } from 'src/dtos/plugin-manifest.dto';
import { AssetVisibility, LogLevel } from 'src/enum';
import { AccessRepository } from 'src/repositories/access.repository';
@@ -334,47 +332,4 @@ describe('core plugin', () => {
await expect(ctx.get(AlbumRepository).getAssetIds(album.id, [asset.id])).resolves.not.toContain(asset.id);
});
});
describe('webhook', () => {
it('should send the asset information to the configured endpoint', async () => {
const { user } = await ctx.newUser();
const { asset } = await ctx.newAsset({ ownerId: user.id });
const received = new Promise<{ headers: NodeJS.Dict<string | string[]>; body: any }>((resolve, reject) => {
const server = createServer((req, res) => {
const chunks: Buffer[] = [];
req.on('data', (chunk) => chunks.push(chunk));
req.on('end', () => {
res.writeHead(200);
res.end();
server.close();
resolve({ headers: req.headers, body: JSON.parse(Buffer.concat(chunks).toString()) });
});
});
server.on('error', reject);
server.listen(0, '127.0.0.1', () => {
const { port } = server.address() as AddressInfo;
createWorkflow({
ownerId: user.id,
trigger: WorkflowTrigger.AssetCreate,
steps: [
{
method: 'immich-plugin-core#webhook',
config: { url: `http://127.0.0.1:${port}/hook`, secret: 'super-secret' },
},
],
})
.then((workflow) => ctx.sut.handleAssetTrigger({ workflowId: workflow.id, assetId: asset.id }))
.catch(reject);
});
});
const { headers, body } = await received;
expect(headers['x-immich-webhook-secret']).toBe('super-secret');
expect(body).toMatchObject({
trigger: WorkflowTrigger.AssetCreate,
asset: { id: asset.id, ownerId: user.id },
});
});
});
});