[core] Convert GOWS Lottie stickers (application/was) to animated WebP - fix #2039 closes devlikeapro/waha-plus#43
Lottie stickers arrive from WhatsApp as a ZIP archive (mimetype
application/was) containing animation/animation.json. This change
intercepts those stickers in the GOWS Plus session, renders each
animation frame off-screen, and delivers an animated WebP to the
webhook instead of the raw ZIP.
Implementation:
- src/plus/utils/lottie-converter.ts (new):
- Extracts animation.json from the ZIP via adm-zip
- Renders frames with @lottiefiles/dotlottie-web + @napi-rs/canvas
(HTMLCanvasElement polyfill required; ImageData must NOT be polyfilled
to avoid per-frame pixel caching in WASM memory)
- Drives frames manually via dotLottie.setFrame(i) after 'load' for
reliable synchronous capture
- Encodes each RGBA frame to single-frame WebP with sharp (quality=80)
- Assembles animated WebP via node-webpmux with full alpha transparency
- Frame delay derived from animation fps; clamped to >=30ms
- Fallback delay configurable via WAHA_LOTTIE_DEFAULT_DELAY_MS env var
- sharp/adm-zip/node-webpmux loaded via require() — their module.exports
is the callable itself with no .default, so ESM default import resolves
to undefined at runtime
- src/plus/engines/gows/session.gows.plus.ts:
- Overrides downloadMedia with LottieAwareGOWSEngineMediaProcessor
- normalizeStickerUrl: fixes GOWS URL field case mismatch (URL vs url)
that caused an infinite network loop on Lottie downloads
- Reports mimetype image/webp and extension .webp for Lottie stickers
- package.json: add @lottiefiles/dotlottie-web, @napi-rs/canvas,
node-webpmux to dependencies
This commit is contained in:
1 parent
dfe0a3c8c4
commit
4ff1c01e38
8 files changed
+522
-4
No files matched your search
@@ -39,7 +39,9 @@
|
||||
"@casl/ability": "^6.8.0",
|
||||
"@figuro/chatwoot-sdk": "^1.1.17",
|
||||
"@liaoliaots/nestjs-redis": "^9",
|
||||
"@lottiefiles/dotlottie-web": "^0.72.1",
|
||||
"@modelcontextprotocol/sdk": "^1.29.0",
|
||||
"@napi-rs/canvas": "^1.0.0",
|
||||
"@nestjs/axios": "^3.0.2",
|
||||
"@nestjs/bullmq": "^11.0.2",
|
||||
"@nestjs/common": "^11.0.0",
|
||||
@@ -94,6 +96,7 @@
|
||||
"mustache": "^4.2.0",
|
||||
"nestjs-pino": "^4.1.0",
|
||||
"node-cache": "5.1.2",
|
||||
"node-webpmux": "^3.2.1",
|
||||
"passport": "^0.7.0",
|
||||
"passport-headerapikey": "^1.2.2",
|
||||
"pg": "^8.13.1",
|
||||
|
||||
@@ -39,6 +39,7 @@ import {
|
||||
NotImplementedByEngineError,
|
||||
} from '@waha/core/exceptions';
|
||||
import { IMediaEngineProcessor } from '@waha/core/media/IMediaEngineProcessor';
|
||||
import { LottieMediaProcessorWrapper } from '@waha/core/media/LottieMediaProcessorWrapper';
|
||||
import { QR } from '@waha/core/QR';
|
||||
import { ExtractMessageKeysForRead } from '@waha/core/utils/convertors';
|
||||
import { parseMessageIdSerialized } from '@waha/core/utils/ids';
|
||||
@@ -2224,7 +2225,10 @@ export class WhatsappSessionGoWSCore extends WhatsappSession {
|
||||
}
|
||||
|
||||
protected async downloadMedia(message) {
|
||||
const processor = new GOWSEngineMediaProcessor(this);
|
||||
let processor: IMediaEngineProcessor<any> = new GOWSEngineMediaProcessor(
|
||||
this,
|
||||
);
|
||||
processor = new LottieMediaProcessorWrapper(processor, this.logger);
|
||||
const media = await this.mediaManager.processMedia(
|
||||
processor,
|
||||
message,
|
||||
@@ -2574,6 +2578,15 @@ export class GOWSEngineMediaProcessor implements IMediaEngineProcessor<any> {
|
||||
async getMediaBuffer(message: any): Promise<Buffer | null> {
|
||||
const mediaDownloadTimeoutMs = 600_000; // 10 minutes
|
||||
|
||||
// Go serializes stickerMessage.URL as uppercase (Go struct field name), but
|
||||
// whatsmeow's download unmarshals it expecting lowercase proto JSON. Add the
|
||||
// lowercase alias so the download URL is found correctly.
|
||||
const sticker = message?.Message?.stickerMessage;
|
||||
if (sticker?.URL && !sticker?.url) {
|
||||
message = JSON.parse(JSON.stringify(message));
|
||||
message.Message.stickerMessage.url = message.Message.stickerMessage.URL;
|
||||
}
|
||||
|
||||
const data = JSON.stringify(message.Message);
|
||||
const tmpdir = new TmpDir(
|
||||
this.session.logger,
|
||||
|
||||
@@ -66,6 +66,7 @@ import { toVcardV3 } from '@waha/core/vcard';
|
||||
import { createAgentProxy } from '@waha/core/helpers.proxy';
|
||||
import type { Agent } from 'https';
|
||||
import { IMediaEngineProcessor } from '@waha/core/media/IMediaEngineProcessor';
|
||||
import { LottieMediaProcessorWrapper } from '@waha/core/media/LottieMediaProcessorWrapper';
|
||||
import { QR } from '@waha/core/QR';
|
||||
import { AckToStatus, StatusToAck } from '@waha/core/utils/acks';
|
||||
import { pairs } from '@waha/utils/pairs';
|
||||
@@ -2770,7 +2771,11 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
|
||||
}
|
||||
|
||||
protected async downloadMedia(message): Promise<WAMedia | null> {
|
||||
const processor = new NOWEBEngineMediaProcessor(this, this.loggerBuilder);
|
||||
let processor: IMediaEngineProcessor<any> = new NOWEBEngineMediaProcessor(
|
||||
this,
|
||||
this.loggerBuilder,
|
||||
);
|
||||
processor = new LottieMediaProcessorWrapper(processor, this.logger);
|
||||
return this.mediaManager.processMedia(processor, message, this.name);
|
||||
}
|
||||
|
||||
|
||||
@@ -33,6 +33,7 @@ import {
|
||||
NotImplementedByEngineError,
|
||||
} from '@waha/core/exceptions';
|
||||
import { IMediaEngineProcessor } from '@waha/core/media/IMediaEngineProcessor';
|
||||
import { LottieMediaProcessorWrapper } from '@waha/core/media/LottieMediaProcessorWrapper';
|
||||
import { QR } from '@waha/core/QR';
|
||||
import { StatusToAck } from '@waha/core/utils/acks';
|
||||
import {
|
||||
@@ -2224,7 +2225,8 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
|
||||
}
|
||||
|
||||
protected async downloadMedia(message: Message) {
|
||||
const processor = new WEBJSEngineMediaProcessor();
|
||||
let processor = new WEBJSEngineMediaProcessor();
|
||||
processor = new LottieMediaProcessorWrapper(processor, this.logger);
|
||||
const media = await this.mediaManager.processMedia(
|
||||
processor,
|
||||
message,
|
||||
|
||||
@@ -126,6 +126,7 @@ import {
|
||||
NotImplementedByEngineError,
|
||||
} from '@waha/core/exceptions';
|
||||
import { IMediaEngineProcessor } from '@waha/core/media/IMediaEngineProcessor';
|
||||
import { LottieMediaProcessorWrapper } from '@waha/core/media/LottieMediaProcessorWrapper';
|
||||
import { IWPPAuthManager } from '@waha/core/engines/wpp/IWPPAuthManager';
|
||||
import { QR } from '@waha/core/QR';
|
||||
import { removeSingletonFiles } from '@waha/core/utils/chrome';
|
||||
@@ -2118,7 +2119,8 @@ export class WhatsappSessionWPPCore extends WhatsappSession {
|
||||
}
|
||||
|
||||
protected async downloadMedia(message: any): Promise<WAMedia | null> {
|
||||
const processor = new WPPEngineMediaProcessor(this.wpp);
|
||||
let processor = new WPPEngineMediaProcessor(this.wpp);
|
||||
processor = new LottieMediaProcessorWrapper(processor, this.logger);
|
||||
return this.mediaManager.processMedia(processor, message, this.name);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
import { IMediaEngineProcessor } from '@waha/core/media/IMediaEngineProcessor';
|
||||
import { convertLottieZipToWebp } from '@waha/core/utils/lottie-converter';
|
||||
|
||||
const LOTTIE_MIMETYPE = 'application/was';
|
||||
|
||||
/**
|
||||
* Wraps any engine media processor to transparently convert Lottie stickers
|
||||
* (mimetype `application/was`, delivered as a ZIP archive) into animated WebP.
|
||||
* The inner processor downloads the raw ZIP; this wrapper converts it before
|
||||
* the result reaches the media storage layer, so callers see `image/webp`.
|
||||
*/
|
||||
export class LottieMediaProcessorWrapper implements IMediaEngineProcessor<any> {
|
||||
constructor(
|
||||
private inner: IMediaEngineProcessor<any>,
|
||||
private logger: any,
|
||||
) {}
|
||||
|
||||
hasMedia(msg: any): boolean {
|
||||
return this.inner.hasMedia(msg);
|
||||
}
|
||||
|
||||
getChatId(msg: any): string {
|
||||
return this.inner.getChatId(msg);
|
||||
}
|
||||
|
||||
getMessageId(msg: any): string {
|
||||
return this.inner.getMessageId(msg);
|
||||
}
|
||||
|
||||
getMimetype(msg: any): string {
|
||||
const mime = this.inner.getMimetype(msg);
|
||||
return mime === LOTTIE_MIMETYPE ? 'image/webp' : mime;
|
||||
}
|
||||
|
||||
getFilename(msg: any): string | null {
|
||||
if (this.inner.getMimetype(msg) !== LOTTIE_MIMETYPE) {
|
||||
return this.inner.getFilename(msg);
|
||||
}
|
||||
return `${this.inner.getMessageId(msg)}.webp`;
|
||||
}
|
||||
|
||||
async getMediaBuffer(msg: any): Promise<Buffer | null> {
|
||||
const buffer = await this.inner.getMediaBuffer(msg);
|
||||
if (!buffer || !buffer.length) {
|
||||
return null;
|
||||
}
|
||||
if (this.inner.getMimetype(msg) !== LOTTIE_MIMETYPE) {
|
||||
return buffer;
|
||||
}
|
||||
|
||||
const id = this.inner.getMessageId(msg);
|
||||
this.logger.info(
|
||||
`Converting Lottie sticker '${id}' to WebP (input ${buffer.length} bytes)...`,
|
||||
);
|
||||
try {
|
||||
const webp = await convertLottieZipToWebp(buffer);
|
||||
this.logger.info(
|
||||
`Lottie sticker '${id}' converted to WebP (${webp.byteLength} bytes)`,
|
||||
);
|
||||
return webp;
|
||||
} catch (err) {
|
||||
this.logger.warn(
|
||||
{ err: err },
|
||||
`Lottie conversion failed for '${id}', returning original buffer`,
|
||||
);
|
||||
return buffer;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,284 @@
|
||||
import { createCanvas, Canvas } from '@napi-rs/canvas';
|
||||
import { DotLottie } from '@lottiefiles/dotlottie-web';
|
||||
// sharp, adm-zip and node-webpmux are CJS modules whose module.exports is the
|
||||
// callable/constructor itself — static default imports resolve to undefined at
|
||||
// runtime because there is no .default property. require() is the correct interop.
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
const sharp = require('sharp');
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
const AdmZip = require('adm-zip');
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
const { Image: WebpImage } = require('node-webpmux');
|
||||
|
||||
// DotLottie._draw() guards canvas rendering behind:
|
||||
// canvas instanceof HTMLCanvasElement || canvas instanceof OffscreenCanvas
|
||||
// Both are undefined in Node.js, so the guard fails and _draw() returns without
|
||||
// calling putImageData – producing a fully black canvas. Registering the
|
||||
// @napi-rs/canvas Canvas constructor as HTMLCanvasElement makes the instanceof
|
||||
// check pass so DotLottie acquires the 2D context and paints frames normally.
|
||||
//
|
||||
// ImageData must NOT be registered globally. When it is, DotLottie uses:
|
||||
// new ImageData(Uint8ClampedArray_view_into_WASM_memory, w, h)
|
||||
// @napi-rs/canvas copies the bytes once at construction and never re-reads the
|
||||
// WASM buffer, so every frame produces the same cached pixels. Without a global
|
||||
// ImageData, DotLottie falls back to:
|
||||
// ctx.createImageData(w, h) + imageData.data.set(currentPixelBuffer)
|
||||
// which copies fresh pixel data from the WASM buffer on every frame.
|
||||
const globalAny = global as Record<string, unknown>;
|
||||
if (!globalAny['HTMLCanvasElement']) {
|
||||
globalAny['HTMLCanvasElement'] = Canvas;
|
||||
}
|
||||
|
||||
export class LottieConvertError extends Error {
|
||||
constructor(message: string, options?: { cause?: unknown }) {
|
||||
super(message);
|
||||
this.name = 'LottieConvertError';
|
||||
if (options?.cause !== undefined) {
|
||||
(this as any).cause = options.cause;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const STICKER_SIZE = 256;
|
||||
const envDelayMs = parseInt(
|
||||
process.env['WAHA_LOTTIE_DEFAULT_DELAY_MS'] ?? '',
|
||||
10,
|
||||
);
|
||||
const DEFAULT_DELAY_MS = envDelayMs > 0 ? envDelayMs : 100;
|
||||
|
||||
function isZipBuffer(buffer: Buffer): boolean {
|
||||
return (
|
||||
buffer.length >= 4 &&
|
||||
buffer[0] === 0x50 &&
|
||||
buffer[1] === 0x4b &&
|
||||
buffer[2] === 0x03 &&
|
||||
buffer[3] === 0x04
|
||||
);
|
||||
}
|
||||
|
||||
function extractAnimationJson(zipBuffer: Buffer): string | null {
|
||||
const zip = new AdmZip(zipBuffer);
|
||||
const entry =
|
||||
zip.getEntry('animation/animation.json') ??
|
||||
zip.getEntries().find((e) => e.entryName.endsWith('.json'));
|
||||
if (!entry) {
|
||||
return null;
|
||||
}
|
||||
return zip.readAsText(entry);
|
||||
}
|
||||
|
||||
export async function convertLottieZipToWebp(
|
||||
zipBuffer: Buffer,
|
||||
): Promise<Buffer> {
|
||||
if (!isZipBuffer(zipBuffer)) {
|
||||
throw new LottieConvertError('Lottie buffer is not a valid ZIP');
|
||||
}
|
||||
|
||||
let animationJson: string | null;
|
||||
try {
|
||||
animationJson = extractAnimationJson(zipBuffer);
|
||||
} catch (err) {
|
||||
throw new LottieConvertError(`Failed to unzip Lottie file: ${err}`, {
|
||||
cause: err,
|
||||
});
|
||||
}
|
||||
|
||||
if (!animationJson) {
|
||||
throw new LottieConvertError('Lottie ZIP has no animation.json entry');
|
||||
}
|
||||
|
||||
let animationData: Record<string, unknown>;
|
||||
try {
|
||||
animationData = JSON.parse(animationJson);
|
||||
} catch (err) {
|
||||
throw new LottieConvertError(
|
||||
`Lottie animation.json is not valid JSON: ${err}`,
|
||||
{
|
||||
cause: err,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return renderLottieToWebp(animationData);
|
||||
}
|
||||
|
||||
function renderLottieToWebp(
|
||||
animationData: Record<string, unknown>,
|
||||
): Promise<Buffer> {
|
||||
return new Promise<Buffer>((resolve, reject) => {
|
||||
let settled = false;
|
||||
|
||||
function finish(result: Buffer) {
|
||||
if (settled) return;
|
||||
settled = true;
|
||||
resolve(result);
|
||||
}
|
||||
|
||||
function fail(reason: unknown) {
|
||||
if (settled) return;
|
||||
settled = true;
|
||||
reject(
|
||||
reason instanceof LottieConvertError
|
||||
? reason
|
||||
: new LottieConvertError(String(reason), { cause: reason }),
|
||||
);
|
||||
}
|
||||
|
||||
let canvas: Canvas;
|
||||
let dotLottie: DotLottie;
|
||||
|
||||
try {
|
||||
canvas = createCanvas(STICKER_SIZE, STICKER_SIZE);
|
||||
} catch (err) {
|
||||
return fail(err);
|
||||
}
|
||||
|
||||
// DotLottie dispatches 'load' via setTimeout. Instead of relying on the
|
||||
// setImmediate-based animation loop (timing-dependent, unreliable for
|
||||
// frame capture), we drive frames manually with setFrame(i) after load.
|
||||
// setFrame() renders synchronously: set_frame → render → putImageData.
|
||||
try {
|
||||
dotLottie = new DotLottie({
|
||||
loop: false,
|
||||
useFrameInterpolation: false,
|
||||
autoplay: false,
|
||||
canvas: canvas as unknown as HTMLCanvasElement,
|
||||
data: animationData,
|
||||
renderConfig: {
|
||||
autoResize: false,
|
||||
devicePixelRatio: 1,
|
||||
freezeOnOffscreen: false,
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
return fail(err);
|
||||
}
|
||||
|
||||
function onLoad() {
|
||||
if (settled) return;
|
||||
|
||||
const totalFrames = dotLottie.totalFrames;
|
||||
const duration = dotLottie.duration;
|
||||
let delayMs: number;
|
||||
let framePixels: Buffer[];
|
||||
|
||||
try {
|
||||
if (totalFrames <= 0) {
|
||||
throw new Error(
|
||||
`Animation has no frames (totalFrames=${totalFrames})`,
|
||||
);
|
||||
}
|
||||
|
||||
let rawDelayMs = DEFAULT_DELAY_MS;
|
||||
if (duration > 0 && isFinite(duration)) {
|
||||
const fps = totalFrames / duration;
|
||||
const computed = Math.round(1000 / fps);
|
||||
if (computed > 0 && isFinite(computed)) {
|
||||
rawDelayMs = computed;
|
||||
}
|
||||
}
|
||||
// WebP delay is in milliseconds. Clamp to at least 30ms to stay above
|
||||
// the ~20ms threshold where some players impose their own minimum.
|
||||
// When clamping, subsample frames proportionally so total duration is
|
||||
// preserved — e.g. a 60fps animation must not play 1.8× too slowly
|
||||
// because all 180 frames are kept at 30ms each.
|
||||
delayMs = Math.max(30, rawDelayMs);
|
||||
const frameStep = delayMs / rawDelayMs;
|
||||
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (!ctx) {
|
||||
throw new Error('canvas.getContext("2d") returned null');
|
||||
}
|
||||
|
||||
// Collect raw RGBA pixels. When frameStep > 1 we subsample frames to
|
||||
// keep playback speed correct despite the minimum-delay clamp.
|
||||
// Pass the float position to setFrame so the renderer interpolates
|
||||
// smoothly along the animation curve rather than snapping to the
|
||||
// nearest integer frame (which causes uneven motion).
|
||||
framePixels = [];
|
||||
for (let t = 0; t < totalFrames; t += frameStep) {
|
||||
// setFrame(0) is a no-op (already rendered after load), so skip it.
|
||||
if (t > 0) {
|
||||
dotLottie.setFrame(t);
|
||||
}
|
||||
framePixels.push(
|
||||
Buffer.from(
|
||||
ctx.getImageData(0, 0, STICKER_SIZE, STICKER_SIZE).data,
|
||||
),
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
dotLottie.destroy();
|
||||
fail(err);
|
||||
return;
|
||||
}
|
||||
|
||||
dotLottie.destroy();
|
||||
|
||||
// Encode each RGBA frame as a single-frame WebP, then assemble into an
|
||||
// animated WebP via node-webpmux. This preserves full alpha transparency
|
||||
// and avoids GIF's 256-colour palette limitation.
|
||||
if (framePixels.length === 1) {
|
||||
sharp(framePixels[0], {
|
||||
raw: { width: STICKER_SIZE, height: STICKER_SIZE, channels: 4 },
|
||||
})
|
||||
.webp({ quality: 80, effort: 3 })
|
||||
.toBuffer()
|
||||
.then(finish)
|
||||
.catch(fail);
|
||||
return;
|
||||
}
|
||||
|
||||
Promise.all(
|
||||
framePixels.map((px) =>
|
||||
sharp(px, {
|
||||
raw: { width: STICKER_SIZE, height: STICKER_SIZE, channels: 4 },
|
||||
})
|
||||
.webp({ quality: 80, effort: 3 })
|
||||
.toBuffer(),
|
||||
),
|
||||
)
|
||||
.then((webpFrames) =>
|
||||
Promise.all(
|
||||
webpFrames.map((buf) =>
|
||||
WebpImage.generateFrame({
|
||||
buffer: buf,
|
||||
delay: delayMs,
|
||||
x: 0,
|
||||
y: 0,
|
||||
blend: false,
|
||||
dispose: true,
|
||||
}),
|
||||
),
|
||||
),
|
||||
)
|
||||
.then((frames) =>
|
||||
WebpImage.save(null, {
|
||||
width: STICKER_SIZE,
|
||||
height: STICKER_SIZE,
|
||||
bgColor: [0, 0, 0, 0],
|
||||
loops: 0,
|
||||
frames: frames,
|
||||
}),
|
||||
)
|
||||
.then(finish)
|
||||
.catch(fail);
|
||||
}
|
||||
|
||||
function onLoadError(payload: any) {
|
||||
if (settled) return;
|
||||
dotLottie.destroy();
|
||||
fail(
|
||||
new LottieConvertError(
|
||||
`DotLottie failed to load animation: ${
|
||||
payload?.error ?? 'unknown error'
|
||||
}`,
|
||||
{ cause: payload?.error },
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
dotLottie.addEventListener('load', onLoad);
|
||||
dotLottie.addEventListener('loadError', onLoadError);
|
||||
});
|
||||
}
|
||||
@@ -2250,6 +2250,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@lottiefiles/dotlottie-web@npm:^0.72.1":
|
||||
version: 0.72.1
|
||||
resolution: "@lottiefiles/dotlottie-web@npm:0.72.1"
|
||||
checksum: 10/79bec5037fb07f38a676d32a11597277c0b15c8ad9716dbde008fe79b2e4e0374aaefcffd9bb68672a8bb35d02b57af4ed01a857238166f8ed85786ad29756b1
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@lukeed/csprng@npm:^1.0.0":
|
||||
version: 1.1.0
|
||||
resolution: "@lukeed/csprng@npm:1.1.0"
|
||||
@@ -2367,6 +2374,129 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@napi-rs/canvas-android-arm64@npm:1.0.0":
|
||||
version: 1.0.0
|
||||
resolution: "@napi-rs/canvas-android-arm64@npm:1.0.0"
|
||||
conditions: os=android & cpu=arm64
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@napi-rs/canvas-darwin-arm64@npm:1.0.0":
|
||||
version: 1.0.0
|
||||
resolution: "@napi-rs/canvas-darwin-arm64@npm:1.0.0"
|
||||
conditions: os=darwin & cpu=arm64
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@napi-rs/canvas-darwin-x64@npm:1.0.0":
|
||||
version: 1.0.0
|
||||
resolution: "@napi-rs/canvas-darwin-x64@npm:1.0.0"
|
||||
conditions: os=darwin & cpu=x64
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@napi-rs/canvas-linux-arm-gnueabihf@npm:1.0.0":
|
||||
version: 1.0.0
|
||||
resolution: "@napi-rs/canvas-linux-arm-gnueabihf@npm:1.0.0"
|
||||
conditions: os=linux & cpu=arm
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@napi-rs/canvas-linux-arm64-gnu@npm:1.0.0":
|
||||
version: 1.0.0
|
||||
resolution: "@napi-rs/canvas-linux-arm64-gnu@npm:1.0.0"
|
||||
conditions: os=linux & cpu=arm64 & libc=glibc
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@napi-rs/canvas-linux-arm64-musl@npm:1.0.0":
|
||||
version: 1.0.0
|
||||
resolution: "@napi-rs/canvas-linux-arm64-musl@npm:1.0.0"
|
||||
conditions: os=linux & cpu=arm64 & libc=musl
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@napi-rs/canvas-linux-riscv64-gnu@npm:1.0.0":
|
||||
version: 1.0.0
|
||||
resolution: "@napi-rs/canvas-linux-riscv64-gnu@npm:1.0.0"
|
||||
conditions: os=linux & cpu=riscv64 & libc=glibc
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@napi-rs/canvas-linux-x64-gnu@npm:1.0.0":
|
||||
version: 1.0.0
|
||||
resolution: "@napi-rs/canvas-linux-x64-gnu@npm:1.0.0"
|
||||
conditions: os=linux & cpu=x64 & libc=glibc
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@napi-rs/canvas-linux-x64-musl@npm:1.0.0":
|
||||
version: 1.0.0
|
||||
resolution: "@napi-rs/canvas-linux-x64-musl@npm:1.0.0"
|
||||
conditions: os=linux & cpu=x64 & libc=musl
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@napi-rs/canvas-win32-arm64-msvc@npm:1.0.0":
|
||||
version: 1.0.0
|
||||
resolution: "@napi-rs/canvas-win32-arm64-msvc@npm:1.0.0"
|
||||
conditions: os=win32 & cpu=arm64
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@napi-rs/canvas-win32-x64-msvc@npm:1.0.0":
|
||||
version: 1.0.0
|
||||
resolution: "@napi-rs/canvas-win32-x64-msvc@npm:1.0.0"
|
||||
conditions: os=win32 & cpu=x64
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@napi-rs/canvas@npm:^1.0.0":
|
||||
version: 1.0.0
|
||||
resolution: "@napi-rs/canvas@npm:1.0.0"
|
||||
dependencies:
|
||||
"@napi-rs/canvas-android-arm64": "npm:1.0.0"
|
||||
"@napi-rs/canvas-darwin-arm64": "npm:1.0.0"
|
||||
"@napi-rs/canvas-darwin-x64": "npm:1.0.0"
|
||||
"@napi-rs/canvas-linux-arm-gnueabihf": "npm:1.0.0"
|
||||
"@napi-rs/canvas-linux-arm64-gnu": "npm:1.0.0"
|
||||
"@napi-rs/canvas-linux-arm64-musl": "npm:1.0.0"
|
||||
"@napi-rs/canvas-linux-riscv64-gnu": "npm:1.0.0"
|
||||
"@napi-rs/canvas-linux-x64-gnu": "npm:1.0.0"
|
||||
"@napi-rs/canvas-linux-x64-musl": "npm:1.0.0"
|
||||
"@napi-rs/canvas-win32-arm64-msvc": "npm:1.0.0"
|
||||
"@napi-rs/canvas-win32-x64-msvc": "npm:1.0.0"
|
||||
dependenciesMeta:
|
||||
"@napi-rs/canvas-android-arm64":
|
||||
optional: true
|
||||
"@napi-rs/canvas-darwin-arm64":
|
||||
optional: true
|
||||
"@napi-rs/canvas-darwin-x64":
|
||||
optional: true
|
||||
"@napi-rs/canvas-linux-arm-gnueabihf":
|
||||
optional: true
|
||||
"@napi-rs/canvas-linux-arm64-gnu":
|
||||
optional: true
|
||||
"@napi-rs/canvas-linux-arm64-musl":
|
||||
optional: true
|
||||
"@napi-rs/canvas-linux-riscv64-gnu":
|
||||
optional: true
|
||||
"@napi-rs/canvas-linux-x64-gnu":
|
||||
optional: true
|
||||
"@napi-rs/canvas-linux-x64-musl":
|
||||
optional: true
|
||||
"@napi-rs/canvas-win32-arm64-msvc":
|
||||
optional: true
|
||||
"@napi-rs/canvas-win32-x64-msvc":
|
||||
optional: true
|
||||
canvas:
|
||||
built: true
|
||||
skia-canvas:
|
||||
built: true
|
||||
checksum: 10/1b2838833988c98d86105980ccd52fb53c4059e30732c7bebdf21774d83a351bf389337c8e6eb4d55fc31359accb90e27d8d80c94cc3bb08d415ab0586227464
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@nestjs/axios@npm:^3.0.2":
|
||||
version: 3.1.3
|
||||
resolution: "@nestjs/axios@npm:3.1.3"
|
||||
@@ -10498,6 +10628,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"node-webpmux@npm:^3.2.1":
|
||||
version: 3.2.1
|
||||
resolution: "node-webpmux@npm:3.2.1"
|
||||
checksum: 10/8797ebe4846707443629800c70d95c592ab1c18f313b360dd26832b8443efa9b89966269ad6b1e3aa59a18e790406b29a55e19c10212fc3068dc476e48cee6e9
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"nopt@npm:^5.0.0":
|
||||
version: 5.0.0
|
||||
resolution: "nopt@npm:5.0.0"
|
||||
@@ -13795,7 +13932,9 @@ __metadata:
|
||||
"@grpc/grpc-js": "npm:^1.14.1"
|
||||
"@grpc/proto-loader": "npm:^0.8.0"
|
||||
"@liaoliaots/nestjs-redis": "npm:^9"
|
||||
"@lottiefiles/dotlottie-web": "npm:^0.72.1"
|
||||
"@modelcontextprotocol/sdk": "npm:^1.29.0"
|
||||
"@napi-rs/canvas": "npm:^1.0.0"
|
||||
"@nestjs/axios": "npm:^3.0.2"
|
||||
"@nestjs/bullmq": "npm:^11.0.2"
|
||||
"@nestjs/cli": "npm:^11.0.0"
|
||||
@@ -13864,6 +14003,7 @@ __metadata:
|
||||
mustache: "npm:^4.2.0"
|
||||
nestjs-pino: "npm:^4.1.0"
|
||||
node-cache: "npm:5.1.2"
|
||||
node-webpmux: "npm:^3.2.1"
|
||||
oxlint: "npm:^1.13.0"
|
||||
passport: "npm:^0.7.0"
|
||||
passport-headerapikey: "npm:^1.2.2"
|
||||
|
||||
Reference in new issue
Block a user