New Research: Supply Chain Attack on Axios Pulls Malicious Dependency from npm.Details →
Socket
Book a DemoSign in
Socket

@directus/api

Package Overview
Dependencies
Maintainers
4
Versions
105
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@directus/api - npm Package Compare versions

Comparing version
33.1.1
to
33.2.0
+33
dist/deployment/drivers/netlify.d.ts
import type { Credentials, Deployment, Details, Log, Options, Project, TriggerResult } from '@directus/types';
import { DeploymentDriver } from '../deployment.js';
export interface NetlifyCredentials extends Credentials {
access_token: string;
}
export interface NetlifyOptions extends Options {
account_slug?: string;
}
export declare class NetlifyDriver extends DeploymentDriver<NetlifyCredentials, NetlifyOptions> {
private api;
constructor(credentials: NetlifyCredentials, options?: NetlifyOptions);
private handleApiError;
private mapStatus;
testConnection(): Promise<void>;
private mapSiteBase;
listProjects(): Promise<Project[]>;
getProject(projectId: string): Promise<Project>;
private mapDeployUrl;
listDeployments(projectId: string, limit?: number): Promise<Deployment[]>;
getDeployment(deploymentId: string): Promise<Details>;
triggerDeployment(projectId: string, options?: {
preview?: boolean;
clearCache?: boolean;
}): Promise<TriggerResult>;
cancelDeployment(deploymentId: string): Promise<void>;
private closeWsConnection;
private setupWsIdleTimeout;
private setupWsConnectionTimeout;
private getWsConnection;
getDeploymentLogs(deploymentId: string, options?: {
since?: Date;
}): Promise<Log[]>;
}
import { InvalidCredentialsError, ServiceUnavailableError } from '@directus/errors';
import { NetlifyAPI } from '@netlify/api';
import { isNumber } from 'lodash-es';
import { DeploymentDriver } from '../deployment.js';
const WS_CONNECTIONS = new Map();
const WS_IDLE_TIMEOUT = 60_000; // 60 seconds
const WS_CONNECTION_TIMEOUT = 10_000; // 10 seconds
// eslint-disable-next-line no-control-regex
const ANSI_REGEX = /[\x1b]\[[0-9;]*m/g;
const WS_URL = 'wss://socketeer.services.netlify.com/build/logs';
export class NetlifyDriver extends DeploymentDriver {
api;
constructor(credentials, options = {}) {
super(credentials, options);
this.api = new NetlifyAPI(this.credentials.access_token);
}
async handleApiError(cb) {
try {
return await cb(this.api);
}
catch (error) {
if (error instanceof Error && 'status' in error && isNumber(error.status) && error.status >= 400) {
if (error.status === 401 || error.status === 403) {
throw new InvalidCredentialsError();
}
throw new ServiceUnavailableError({ service: 'netlify', reason: 'Netlify API error: ' + error.message });
}
throw error;
}
}
mapStatus(netlifyState) {
const normalized = netlifyState?.toLowerCase();
switch (normalized) {
case 'ready':
return 'ready';
case 'error':
return 'error';
case 'canceled':
return 'canceled';
default:
return 'building';
}
}
async testConnection() {
await this.handleApiError((api) => api.listSites({ per_page: 1 }));
}
mapSiteBase(site) {
const result = {
id: site.id,
name: site.name,
deployable: Boolean(site.build_settings?.provider && site.build_settings?.repo_url),
};
// Use custom domain if available, otherwise ssl_url or url
if (site.custom_domain) {
result.url = `https://${site.custom_domain}`;
}
else if (site.ssl_url) {
result.url = site.ssl_url;
}
else if (site.url) {
result.url = site.url;
}
return result;
}
async listProjects() {
const params = { per_page: '100' };
const response = await this.handleApiError((api) => {
return this.options.account_slug
? api.listSitesForAccount({
account_slug: this.options.account_slug,
...params,
})
: api.listSites(params);
});
return response.map((site) => this.mapSiteBase(site));
}
async getProject(projectId) {
const site = await this.handleApiError((api) => api.getSite({ siteId: projectId }));
const result = this.mapSiteBase(site);
// Add published deploy info if available
if (site.published_deploy) {
const deploy = site.published_deploy;
if (deploy.state && deploy.created_at) {
result.latest_deployment = {
status: this.mapStatus(deploy.state),
created_at: new Date(deploy.created_at),
...(deploy.published_at && { finished_at: new Date(deploy.published_at) }),
};
}
}
if (site.created_at) {
result.created_at = new Date(site.created_at);
}
if (site.updated_at) {
result.updated_at = new Date(site.updated_at);
}
return result;
}
mapDeployUrl(deploy) {
return deploy['ssl_url'] ?? deploy['deploy_ssl_url'] ?? deploy['deploy_url'] ?? deploy['url'];
}
async listDeployments(projectId, limit = 20) {
const response = await this.handleApiError((api) => api.listSiteDeploys({ site_id: projectId, per_page: limit }));
return response.map((deploy) => {
const result = {
id: deploy.id,
project_id: deploy.site_id,
status: this.mapStatus(deploy.state),
created_at: new Date(deploy.created_at),
};
const url = this.mapDeployUrl(deploy);
if (url)
result.url = url;
if (deploy.published_at) {
result.finished_at = new Date(deploy.published_at);
}
if (deploy.error_message) {
result.error_message = deploy.error_message;
}
return result;
});
}
async getDeployment(deploymentId) {
const deploy = await this.handleApiError((api) => api.getDeploy({ deployId: deploymentId }));
const result = {
id: deploy.id,
project_id: deploy.site_id,
status: this.mapStatus(deploy.state),
created_at: new Date(deploy.created_at),
};
const url = this.mapDeployUrl(deploy);
if (url)
result.url = url;
if (deploy.published_at) {
result.finished_at = new Date(deploy.published_at);
}
if (deploy.error_message) {
result.error_message = deploy.error_message;
}
return result;
}
async triggerDeployment(projectId, options) {
// Netlify builds endpoint returns a Build object with deploy_id and deploy_state
const buildResponse = await this.handleApiError((api) => api.createSiteBuild({
site_id: projectId,
clear_cache: options?.clearCache || false,
}));
const deployState = await this.handleApiError((api) => api.getDeploy({ deployId: buildResponse.deploy_id }));
const triggerResult = {
deployment_id: buildResponse.deploy_id,
status: this.mapStatus(deployState.state),
};
return triggerResult;
}
async cancelDeployment(deploymentId) {
await this.handleApiError((api) => api.cancelSiteDeploy({ deployId: deploymentId }));
this.closeWsConnection(deploymentId);
}
closeWsConnection(deploymentId, remove = true) {
const connection = WS_CONNECTIONS.get(deploymentId);
if (!connection)
return;
connection.ws.close();
if (remove) {
WS_CONNECTIONS.delete(deploymentId);
}
}
setupWsIdleTimeout(connection) {
if (connection.idleTimeout) {
clearTimeout(connection.idleTimeout);
}
connection.idleTimeout = setTimeout(() => {
this.closeWsConnection(connection.deploymentId);
}, WS_IDLE_TIMEOUT);
}
setupWsConnectionTimeout(connection, reject) {
if (connection.connectionTimeout) {
clearTimeout(connection.connectionTimeout);
}
connection.connectionTimeout = setTimeout(() => {
this.closeWsConnection(connection.deploymentId);
reject(new ServiceUnavailableError({ service: 'netlify', reason: 'WebSocket connection timeout' }));
}, WS_CONNECTION_TIMEOUT);
}
getWsConnection(deploymentId) {
return new Promise((resolve, reject) => {
const existingConnection = WS_CONNECTIONS.get(deploymentId);
if (existingConnection) {
this.setupWsIdleTimeout(existingConnection);
return resolve(existingConnection);
}
let resolveCompleted;
const completed = new Promise((res) => {
resolveCompleted = res;
});
const connection = {
ws: new WebSocket(WS_URL),
logs: [],
deploymentId,
completed,
resolveCompleted: resolveCompleted,
};
this.setupWsConnectionTimeout(connection, reject);
connection.ws.addEventListener('open', () => {
if (connection.connectionTimeout) {
clearTimeout(connection.connectionTimeout);
connection.connectionTimeout = undefined;
}
this.setupWsIdleTimeout(connection);
const payload = JSON.stringify({
deploy_id: deploymentId,
access_token: this.credentials.access_token,
});
connection.ws.send(payload);
resolve(connection);
WS_CONNECTIONS.set(deploymentId, connection);
});
connection.ws.addEventListener('message', (event) => {
const data = JSON.parse(event.data);
const cleanMessage = data.message.replace(/\r/g, '').replace(ANSI_REGEX, '');
let logType = 'stdout';
if (data.type === 'report') {
logType = cleanMessage.includes('Failing build') ? 'stderr' : 'info';
}
connection.logs.push({
timestamp: new Date(data.ts),
type: logType,
message: cleanMessage,
});
// If we receive a "report" type message, the build is complete.
// Close the WebSocket connection but don't yet remove the logs, allowing the client to fetch them until the idle timeout expires.
if (data.type === 'report') {
connection.resolveCompleted();
this.closeWsConnection(deploymentId, false);
}
});
connection.ws.addEventListener('error', () => {
this.closeWsConnection(deploymentId);
reject(new ServiceUnavailableError({ service: 'netlify', reason: 'WebSocket connection error' }));
});
connection.ws.addEventListener('close', () => {
if (connection.connectionTimeout) {
clearTimeout(connection.connectionTimeout);
}
});
});
}
async getDeploymentLogs(deploymentId, options) {
const deploy = await this.handleApiError((api) => api.getDeploy({ deployId: deploymentId }));
const connection = await this.getWsConnection(deploymentId);
// Build already finished — WS is replaying logs, wait for all of them
if (this.mapStatus(deploy.state) !== 'building') {
await connection.completed;
}
if (options?.since) {
return connection.logs.filter((log) => log.timestamp >= options.since);
}
return connection.logs;
}
}
+2
-1

@@ -31,3 +31,4 @@ import { useEnv } from '@directus/env';

res.setHeader('Content-Type', 'application/zip');
res.setHeader('Content-Disposition', `attachment; filename="folder-${metadata['name'] ? metadata['name'] : 'unknown'}-${getDateTimeFormatted()}.zip"`);
const folderName = `folder-${metadata['name'] ? metadata['name'] : 'unknown'}-${getDateTimeFormatted()}.zip`;
res.setHeader('Content-Disposition', contentDisposition(folderName, { type: 'attachment' }));
archive.pipe(res);

@@ -34,0 +35,0 @@ await complete();

@@ -34,4 +34,5 @@ import { applyFilter } from '../../run-ast/lib/apply-query/filter/index.js';

}
return this.knex.raw('(' + countQuery.toQuery() + ')');
const { sql, bindings } = countQuery.toSQL();
return this.knex.raw(`(${sql})`, bindings);
}
}

@@ -1,2 +0,2 @@

import { VercelDriver } from './deployment/drivers/index.js';
import { NetlifyDriver, VercelDriver } from './deployment/drivers/index.js';
/**

@@ -11,2 +11,3 @@ * Registry of deployment driver constructors

drivers.set('vercel', VercelDriver);
drivers.set('netlify', NetlifyDriver);
}

@@ -13,0 +14,0 @@ /**

@@ -0,1 +1,2 @@

export * from './netlify.js';
export * from './vercel.js';

@@ -0,1 +1,2 @@

export * from './netlify.js';
export * from './vercel.js';

@@ -14,4 +14,15 @@ import { clamp } from 'lodash-es';

if ((transformationParams.width || transformationParams.height) && file.width && file.height) {
const toWidth = transformationParams.width ? Number(transformationParams.width) : undefined;
const toHeight = transformationParams.height ? Number(transformationParams.height) : undefined;
let toWidth = transformationParams.width ? Number(transformationParams.width) : undefined;
let toHeight = transformationParams.height ? Number(transformationParams.height) : undefined;
/*
* When withoutEnlargement is true, clamp target dimensions to original dimensions to prevent "bad extract area" errors when using focal points.
*/
if (transformationParams.withoutEnlargement) {
if (toWidth !== undefined) {
toWidth = Math.min(toWidth, file.width);
}
if (toHeight !== undefined) {
toHeight = Math.min(toHeight, file.height);
}
}
const toFocalPointX = transformationParams.focal_point_x

@@ -18,0 +29,0 @@ ? Number(transformationParams.focal_point_x)

{
"name": "@directus/api",
"version": "33.1.1",
"version": "33.2.0",
"description": "Directus is a real-time API and App dashboard for managing SQL database content",

@@ -69,2 +69,3 @@ "keywords": [

"@modelcontextprotocol/sdk": "1.26.0",
"@netlify/api": "14.0.14",
"@rollup/plugin-alias": "5.1.1",

@@ -165,26 +166,26 @@ "@rollup/plugin-node-resolve": "16.0.3",

"@directus/ai": "1.1.0",
"@directus/app": "15.1.1",
"@directus/constants": "14.0.0",
"@directus/env": "5.5.1",
"@directus/extensions": "3.0.17",
"@directus/env": "5.5.2",
"@directus/app": "15.2.0",
"@directus/errors": "2.2.0",
"@directus/extensions-sdk": "17.0.7",
"@directus/extensions-registry": "3.0.17",
"@directus/memory": "3.1.0",
"@directus/extensions": "3.0.18",
"@directus/extensions-registry": "3.0.18",
"@directus/extensions-sdk": "17.0.8",
"@directus/format-title": "12.1.1",
"@directus/pressure": "3.0.15",
"@directus/memory": "3.1.1",
"@directus/pressure": "3.0.16",
"@directus/schema": "13.0.5",
"@directus/schema-builder": "0.0.12",
"@directus/schema-builder": "0.0.13",
"@directus/specs": "12.0.0",
"@directus/storage": "12.0.3",
"@directus/storage-driver-azure": "12.0.15",
"@directus/storage-driver-cloudinary": "12.0.15",
"@directus/storage-driver-azure": "12.0.16",
"@directus/storage-driver-cloudinary": "12.0.16",
"@directus/storage-driver-local": "12.0.3",
"@directus/storage-driver-s3": "12.1.1",
"@directus/storage-driver-supabase": "3.0.15",
"@directus/storage-driver-gcs": "12.0.15",
"@directus/utils": "13.2.0",
"@directus/constants": "14.0.0",
"@directus/storage-driver-gcs": "12.0.16",
"@directus/storage-driver-supabase": "3.0.16",
"@directus/system-data": "4.1.0",
"@directus/validation": "2.0.15",
"directus": "11.15.1"
"@directus/utils": "13.2.1",
"@directus/storage-driver-s3": "12.1.2",
"@directus/validation": "2.0.16",
"directus": "11.15.2"
},

@@ -232,4 +233,4 @@ "devDependencies": {

"vitest": "3.2.4",
"@directus/schema-builder": "0.0.12",
"@directus/types": "14.1.0"
"@directus/schema-builder": "0.0.13",
"@directus/types": "14.2.0"
},

@@ -236,0 +237,0 @@ "optionalDependencies": {