@basicbit/vrchat-mcp 0.1.10 → 0.1.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +15 -0
- package/README.md +3 -0
- package/dist/package.json +2 -1
- package/dist/src/config/defaults.json +3 -0
- package/dist/src/config/index.js +32 -0
- package/dist/src/config/index.js.map +1 -1
- package/dist/src/core/client.js +78 -5
- package/dist/src/core/client.js.map +1 -1
- package/dist/src/core/generatedToolOverrides.js +2 -0
- package/dist/src/core/generatedToolOverrides.js.map +1 -1
- package/dist/src/core/generatedToolSkips.js +1 -0
- package/dist/src/core/generatedToolSkips.js.map +1 -1
- package/dist/src/core/operationPolicy.js +12 -0
- package/dist/src/core/operationPolicy.js.map +1 -1
- package/dist/src/models/instances.js +16 -0
- package/dist/src/models/instances.js.map +1 -1
- package/dist/src/models/uploads.js +33 -0
- package/dist/src/models/uploads.js.map +1 -0
- package/dist/src/services/api/client.js +1 -0
- package/dist/src/services/api/client.js.map +1 -1
- package/dist/src/services/events/curated.js +16 -10
- package/dist/src/services/events/curated.js.map +1 -1
- package/dist/src/services/instances/curated.js +76 -1
- package/dist/src/services/instances/curated.js.map +1 -1
- package/dist/src/services/uploads/curated.js +42 -0
- package/dist/src/services/uploads/curated.js.map +1 -0
- package/dist/src/services/uploads/index.js +3 -0
- package/dist/src/services/uploads/index.js.map +1 -0
- package/dist/src/services/uploads/localPng.js +390 -0
- package/dist/src/services/uploads/localPng.js.map +1 -0
- package/dist/src/tools/curated/instances.js +29 -2
- package/dist/src/tools/curated/instances.js.map +1 -1
- package/dist/src/tools/curated/uploads.js +57 -0
- package/dist/src/tools/curated/uploads.js.map +1 -0
- package/dist/src/tools/registerAllTools.js +2 -0
- package/dist/src/tools/registerAllTools.js.map +1 -1
- package/docs/curated-tools.md +32 -0
- package/docs/spec-drift.md +33 -0
- package/docs/tools-guide.md +1 -0
- package/docs/tools.md +228 -6
- package/package.json +2 -1
- package/server.json +9 -2
|
@@ -9,10 +9,19 @@ function invalidateGroupEventCaches(groupId) {
|
|
|
9
9
|
cacheManager.invalidateByTag(`groups:${groupId}`);
|
|
10
10
|
}
|
|
11
11
|
// occurrenceKind is not in the generated CalendarEvent type, but the API returns it
|
|
12
|
-
// and generated schemas currently preserve unknown fields via .passthrough().
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
12
|
+
// and generated schemas currently preserve unknown fields via .passthrough(). Only a
|
|
13
|
+
// missing property represents the legacy single-event response shape. Every present
|
|
14
|
+
// unrecognized value remains a mismatch so deletion fails closed.
|
|
15
|
+
function getDeleteTargetKind(event) {
|
|
16
|
+
const record = event;
|
|
17
|
+
if (!Object.prototype.hasOwnProperty.call(record, 'occurrenceKind'))
|
|
18
|
+
return 'single_event';
|
|
19
|
+
const kind = record.occurrenceKind;
|
|
20
|
+
if (kind === 'single')
|
|
21
|
+
return 'single_event';
|
|
22
|
+
if (typeof kind === 'string')
|
|
23
|
+
return kind;
|
|
24
|
+
return JSON.stringify(kind) ?? String(kind);
|
|
16
25
|
}
|
|
17
26
|
async function getGroupCalendarEventOrThrow(groupId, calendarId) {
|
|
18
27
|
const eventResult = await callReadOperationParsed('getGroupCalendarEvent', { groupId, calendarId }, {});
|
|
@@ -23,13 +32,10 @@ async function getGroupCalendarEventOrThrow(groupId, calendarId) {
|
|
|
23
32
|
return event;
|
|
24
33
|
}
|
|
25
34
|
function assertDeleteTargetKind(event, calendarId, targetKind) {
|
|
26
|
-
const
|
|
27
|
-
if (
|
|
35
|
+
const eventTargetKind = getDeleteTargetKind(event);
|
|
36
|
+
if (eventTargetKind === targetKind)
|
|
28
37
|
return;
|
|
29
|
-
|
|
30
|
-
return;
|
|
31
|
-
const found = occurrenceKind ?? 'single_event';
|
|
32
|
-
throw new Error(`Refusing to delete calendar event ${calendarId}: expected targetKind "${targetKind}" but found "${found}".`);
|
|
38
|
+
throw new Error(`Refusing to delete calendar event ${calendarId}: expected targetKind "${targetKind}" but found "${eventTargetKind}".`);
|
|
33
39
|
}
|
|
34
40
|
function parseNumber(value, fallback) {
|
|
35
41
|
if (typeof value === 'number' && Number.isFinite(value))
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"curated.js","sourceRoot":"","sources":["../../../../src/services/events/curated.ts"],"names":[],"mappings":"AACA,OAAO,EACL,yBAAyB,GAQ1B,MAAM,wBAAwB,CAAC;AAEhC,OAAO,EACL,uBAAuB,EACvB,wBAAwB,GAEzB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC3C,OAAO,EACL,YAAY,EACZ,kBAAkB,EAClB,cAAc,EACd,YAAY,GAEb,MAAM,YAAY,CAAC;AAUpB,MAAM,iBAAiB,GAAG,EAAE,CAAC;AAC7B,MAAM,iBAAiB,GAAG,CAAC,CAAC;AAC5B,MAAM,oBAAoB,GAAG,GAAG,CAAC;AAUjC,SAAS,0BAA0B,CAAC,OAAe;IACjD,YAAY,CAAC,eAAe,CAAC,UAAU,OAAO,EAAE,CAAC,CAAC;AACpD,CAAC;AAED,oFAAoF;AACpF,
|
|
1
|
+
{"version":3,"file":"curated.js","sourceRoot":"","sources":["../../../../src/services/events/curated.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
import { checkGroupAllowed } from '../groups/allowlist.js';
|
|
2
|
-
import { callWriteOperationParsed } from '../api/client.js';
|
|
2
|
+
import { callReadOperationParsed, callWriteOperationParsed, } from '../api/client.js';
|
|
3
|
+
import { cacheManager } from '../cache.js';
|
|
4
|
+
const instanceLinkLocks = new Map();
|
|
5
|
+
function withInstanceLinkLock(location, run) {
|
|
6
|
+
const prior = instanceLinkLocks.get(location) ?? Promise.resolve();
|
|
7
|
+
const result = prior.then(run, run);
|
|
8
|
+
const settled = result.then(() => undefined, () => undefined);
|
|
9
|
+
instanceLinkLocks.set(location, settled);
|
|
10
|
+
void settled.then(() => {
|
|
11
|
+
if (instanceLinkLocks.get(location) === settled)
|
|
12
|
+
instanceLinkLocks.delete(location);
|
|
13
|
+
});
|
|
14
|
+
return result;
|
|
15
|
+
}
|
|
16
|
+
function validateCanRequestInvite(input) {
|
|
17
|
+
if (input.canRequestInvite !== true || input.type === 'private')
|
|
18
|
+
return null;
|
|
19
|
+
return {
|
|
20
|
+
ok: false,
|
|
21
|
+
reason: `canRequestInvite cannot be true when type="${input.type}". Use type="private", or omit canRequestInvite or set it to false.`,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
3
24
|
function resolveOwnerId(input) {
|
|
4
25
|
if (input.type === 'group') {
|
|
5
26
|
const groupId = input.groupId ?? input.ownerId ?? null;
|
|
@@ -57,6 +78,9 @@ function applyGroupFields(request, input) {
|
|
|
57
78
|
return { ok: true, request };
|
|
58
79
|
}
|
|
59
80
|
export function prepareInstanceCreate(input) {
|
|
81
|
+
const canRequestInviteResult = validateCanRequestInvite(input);
|
|
82
|
+
if (canRequestInviteResult)
|
|
83
|
+
return canRequestInviteResult;
|
|
60
84
|
const ownerResult = resolveOwnerId(input);
|
|
61
85
|
if (!ownerResult.ok)
|
|
62
86
|
return ownerResult;
|
|
@@ -77,4 +101,55 @@ export async function createInstance(request) {
|
|
|
77
101
|
const result = await callWriteOperationParsed('createInstance', undefined, request);
|
|
78
102
|
return result.data ?? null;
|
|
79
103
|
}
|
|
104
|
+
function invalidateInstanceLinkCaches(input) {
|
|
105
|
+
cacheManager.invalidateByTag(`instances:${input.worldId}`);
|
|
106
|
+
cacheManager.invalidateByTag(`groups:${input.groupId}`);
|
|
107
|
+
}
|
|
108
|
+
async function applyInstanceCalendarLink(input) {
|
|
109
|
+
const eventResult = await callReadOperationParsed('getGroupCalendarEvent', {
|
|
110
|
+
groupId: input.groupId,
|
|
111
|
+
calendarId: input.calendarId,
|
|
112
|
+
});
|
|
113
|
+
const event = eventResult.data;
|
|
114
|
+
if (!event)
|
|
115
|
+
throw new Error(`Calendar event ${input.calendarId} not found.`);
|
|
116
|
+
if (event.ownerId !== input.groupId) {
|
|
117
|
+
throw new Error(`Refusing to link calendar event ${input.calendarId}: it is not owned by group ${input.groupId}.`);
|
|
118
|
+
}
|
|
119
|
+
const instanceResult = await callReadOperationParsed('getInstance', {
|
|
120
|
+
worldId: input.worldId,
|
|
121
|
+
instanceId: input.instanceId,
|
|
122
|
+
});
|
|
123
|
+
const instance = instanceResult.data;
|
|
124
|
+
if (!instance)
|
|
125
|
+
throw new Error(`Instance ${input.worldId}:${input.instanceId} not found.`);
|
|
126
|
+
if (instance.type !== 'group' || instance.ownerId !== input.groupId) {
|
|
127
|
+
throw new Error(`Refusing to link instance ${input.worldId}:${input.instanceId}: it is not owned by group ${input.groupId}.`);
|
|
128
|
+
}
|
|
129
|
+
const resultDetails = {
|
|
130
|
+
eventTitle: event.title,
|
|
131
|
+
instanceName: instance.displayName ?? instance.name,
|
|
132
|
+
};
|
|
133
|
+
if (instance.calendarEntryId === input.calendarId) {
|
|
134
|
+
invalidateInstanceLinkCaches(input);
|
|
135
|
+
return { status: 'already_linked', ...resultDetails };
|
|
136
|
+
}
|
|
137
|
+
if (instance.calendarEntryId) {
|
|
138
|
+
invalidateInstanceLinkCaches(input);
|
|
139
|
+
throw new Error(`Refusing to replace existing calendar link ${instance.calendarEntryId} on instance ${input.worldId}:${input.instanceId}.`);
|
|
140
|
+
}
|
|
141
|
+
try {
|
|
142
|
+
await callWriteOperationParsed('updateInstance', { worldId: input.worldId, instanceId: input.instanceId }, { calendarEntryId: input.calendarId });
|
|
143
|
+
}
|
|
144
|
+
finally {
|
|
145
|
+
invalidateInstanceLinkCaches(input);
|
|
146
|
+
}
|
|
147
|
+
return { status: 'linked', ...resultDetails };
|
|
148
|
+
}
|
|
149
|
+
export async function linkInstanceToCalendarEvent(input) {
|
|
150
|
+
const allowed = checkGroupAllowed(input.groupId);
|
|
151
|
+
if (!allowed.ok)
|
|
152
|
+
throw new Error(allowed.reason);
|
|
153
|
+
return withInstanceLinkLock(`${input.worldId}:${input.instanceId}`, () => applyInstanceCalendarLink(input));
|
|
154
|
+
}
|
|
80
155
|
//# sourceMappingURL=curated.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"curated.js","sourceRoot":"","sources":["../../../../src/services/instances/curated.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"curated.js","sourceRoot":"","sources":["../../../../src/services/instances/curated.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { CallError, uploadGalleryImageMultipart } from '../../core/client.js';
|
|
2
|
+
import { readValidatedStaticPng } from './localPng.js';
|
|
3
|
+
function optionalString(record, key) {
|
|
4
|
+
return typeof record[key] === 'string' ? record[key] : undefined;
|
|
5
|
+
}
|
|
6
|
+
function latestVersion(record) {
|
|
7
|
+
if (typeof record.version === 'number' && Number.isInteger(record.version)) {
|
|
8
|
+
return record.version;
|
|
9
|
+
}
|
|
10
|
+
if (!Array.isArray(record.versions))
|
|
11
|
+
return undefined;
|
|
12
|
+
const versions = record.versions
|
|
13
|
+
.map((entry) => entry && typeof entry === 'object' && typeof entry.version === 'number'
|
|
14
|
+
? entry.version
|
|
15
|
+
: undefined)
|
|
16
|
+
.filter((value) => value !== undefined && Number.isInteger(value));
|
|
17
|
+
return versions.length > 0 ? Math.max(...versions) : undefined;
|
|
18
|
+
}
|
|
19
|
+
function summarizeUploadedFile(data) {
|
|
20
|
+
if (!data || typeof data !== 'object' || Array.isArray(data)) {
|
|
21
|
+
throw new CallError('VRChat accepted the image upload but returned no file object. The upload may have succeeded; do not retry automatically.');
|
|
22
|
+
}
|
|
23
|
+
const record = data;
|
|
24
|
+
const id = optionalString(record, 'id');
|
|
25
|
+
if (!id) {
|
|
26
|
+
throw new CallError('VRChat accepted the image upload but returned no fileId. The upload may have succeeded; do not retry automatically.');
|
|
27
|
+
}
|
|
28
|
+
return {
|
|
29
|
+
id,
|
|
30
|
+
name: optionalString(record, 'name'),
|
|
31
|
+
mimeType: optionalString(record, 'mimeType'),
|
|
32
|
+
extension: optionalString(record, 'extension'),
|
|
33
|
+
ownerId: optionalString(record, 'ownerId'),
|
|
34
|
+
version: latestVersion(record),
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
export async function uploadGroupImage(input, allowedRoots) {
|
|
38
|
+
const image = await readValidatedStaticPng(input.imagePath, allowedRoots);
|
|
39
|
+
const result = await uploadGalleryImageMultipart(image.fileName, image.bytes);
|
|
40
|
+
return { image, file: summarizeUploadedFile(result.data) };
|
|
41
|
+
}
|
|
42
|
+
//# sourceMappingURL=curated.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"curated.js","sourceRoot":"","sources":["../../../../src/services/uploads/curated.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,2BAA2B,EAAE,MAAM,sBAAsB,CAAC;AAE9E,OAAO,EAAE,sBAAsB,EAAqB,MAAM,eAAe,CAAC;AAW1E,SAAS,cAAc,CAAC,MAA+B,EAAE,GAAW;IAClE,OAAO,OAAO,MAAM,CAAC,GAAG,CAAC,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;AACnE,CAAC;AAED,SAAS,aAAa,CAAC,MAA+B;IACpD,IAAI,OAAO,MAAM,CAAC,OAAO,KAAK,QAAQ,IAAI,MAAM,CAAC,SAAS,CAAC,MAAM,CAAC,OAAO,CAAC,EAAE,CAAC;QAC3E,OAAO,MAAM,CAAC,OAAO,CAAC;IACxB,CAAC;IACD,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC;QAAE,OAAO,SAAS,CAAC;IACtD,MAAM,QAAQ,GAAG,MAAM,CAAC,QAAQ;SAC7B,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CACb,KAAK,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,OAAQ,KAAiC,CAAC,OAAO,KAAK,QAAQ;QAClG,CAAC,CAAE,KAAgC,CAAC,OAAO;QAC3C,CAAC,CAAC,SAAS,CACd;SACA,MAAM,CAAC,CAAC,KAAK,EAAmB,EAAE,CAAC,KAAK,KAAK,SAAS,IAAI,MAAM,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,CAAC;IACtF,OAAO,QAAQ,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,QAAQ,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;AACjE,CAAC;AAED,SAAS,qBAAqB,CAAC,IAAa;IAC1C,IAAI,CAAC,IAAI,IAAI,OAAO,IAAI,KAAK,QAAQ,IAAI,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC;QAC7D,MAAM,IAAI,SAAS,CACjB,0HAA0H,CAC3H,CAAC;IACJ,CAAC;IACD,MAAM,MAAM,GAAG,IAA+B,CAAC;IAC/C,MAAM,EAAE,GAAG,cAAc,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;IACxC,IAAI,CAAC,EAAE,EAAE,CAAC;QACR,MAAM,IAAI,SAAS,CACjB,qHAAqH,CACtH,CAAC;IACJ,CAAC;IACD,OAAO;QACL,EAAE;QACF,IAAI,EAAE,cAAc,CAAC,MAAM,EAAE,MAAM,CAAC;QACpC,QAAQ,EAAE,cAAc,CAAC,MAAM,EAAE,UAAU,CAAC;QAC5C,SAAS,EAAE,cAAc,CAAC,MAAM,EAAE,WAAW,CAAC;QAC9C,OAAO,EAAE,cAAc,CAAC,MAAM,EAAE,SAAS,CAAC;QAC1C,OAAO,EAAE,aAAa,CAAC,MAAM,CAAC;KAC/B,CAAC;AACJ,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,gBAAgB,CACpC,KAA+C,EAC/C,YAAsB;IAEtB,MAAM,KAAK,GAAG,MAAM,sBAAsB,CAAC,KAAK,CAAC,SAAS,EAAE,YAAY,CAAC,CAAC;IAC1E,MAAM,MAAM,GAAG,MAAM,2BAA2B,CAAC,KAAK,CAAC,QAAQ,EAAE,KAAK,CAAC,KAAK,CAAC,CAAC;IAC9E,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,qBAAqB,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC;AAC7D,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../src/services/uploads/index.ts"],"names":[],"mappings":"AAAA,cAAc,cAAc,CAAC;AAC7B,cAAc,eAAe,CAAC"}
|
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
import { constants } from 'node:fs';
|
|
2
|
+
import fs from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { inflateSync } from 'node:zlib';
|
|
5
|
+
import { decode } from 'fast-png';
|
|
6
|
+
export const MAX_PNG_BYTES = 10 * 1024 * 1024;
|
|
7
|
+
export const MIN_PNG_DIMENSION = 65;
|
|
8
|
+
export const MAX_PNG_DIMENSION = 2048;
|
|
9
|
+
export const MAX_PNG_PIXELS = MAX_PNG_DIMENSION * MAX_PNG_DIMENSION;
|
|
10
|
+
const MAX_PNG_CHUNKS = 4096;
|
|
11
|
+
const PNG_SIGNATURE = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
|
|
12
|
+
const APNG_CHUNKS = new Set(['acTL', 'fcTL', 'fdAT']);
|
|
13
|
+
const KNOWN_CRITICAL_CHUNKS = new Set(['IHDR', 'PLTE', 'IDAT', 'IEND']);
|
|
14
|
+
const CHANNELS_BY_COLOR_TYPE = new Map([
|
|
15
|
+
[0, 1],
|
|
16
|
+
[2, 3],
|
|
17
|
+
[3, 1],
|
|
18
|
+
[4, 2],
|
|
19
|
+
[6, 4],
|
|
20
|
+
]);
|
|
21
|
+
const BIT_DEPTHS_BY_COLOR_TYPE = new Map([
|
|
22
|
+
[0, new Set([1, 2, 4, 8, 16])],
|
|
23
|
+
[2, new Set([8, 16])],
|
|
24
|
+
[3, new Set([1, 2, 4, 8])],
|
|
25
|
+
[4, new Set([8, 16])],
|
|
26
|
+
[6, new Set([8, 16])],
|
|
27
|
+
]);
|
|
28
|
+
const nativeFileSystem = {
|
|
29
|
+
lstat: (filePath) => fs.lstat(filePath, { bigint: true }),
|
|
30
|
+
stat: (filePath) => fs.stat(filePath, { bigint: true }),
|
|
31
|
+
realpath: (filePath) => fs.realpath(filePath),
|
|
32
|
+
open: (filePath, flags) => fs.open(filePath, flags),
|
|
33
|
+
};
|
|
34
|
+
function isWithin(root, candidate) {
|
|
35
|
+
const relative = path.relative(root, candidate);
|
|
36
|
+
return (relative === '' ||
|
|
37
|
+
(relative !== '..' && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative)));
|
|
38
|
+
}
|
|
39
|
+
function fileSystemMessage(err) {
|
|
40
|
+
if (err && typeof err === 'object' && 'code' in err && typeof err.code === 'string') {
|
|
41
|
+
return err.code;
|
|
42
|
+
}
|
|
43
|
+
return err instanceof Error ? err.message : 'unknown filesystem error';
|
|
44
|
+
}
|
|
45
|
+
async function canonicalizeRoots(roots, fileSystem) {
|
|
46
|
+
if (roots.length === 0) {
|
|
47
|
+
throw new Error('Local image upload is disabled because uploads.allowedRoots is empty. Configure an absolute allowed root or VRCHAT_MCP_UPLOAD_ROOTS.');
|
|
48
|
+
}
|
|
49
|
+
const resolved = [];
|
|
50
|
+
for (const configuredRoot of roots) {
|
|
51
|
+
if (!path.isAbsolute(configuredRoot)) {
|
|
52
|
+
throw new Error(`Upload root must be absolute: ${configuredRoot}`);
|
|
53
|
+
}
|
|
54
|
+
const lexical = path.normalize(configuredRoot);
|
|
55
|
+
let rootInfo;
|
|
56
|
+
try {
|
|
57
|
+
rootInfo = await fileSystem.lstat(lexical);
|
|
58
|
+
}
|
|
59
|
+
catch (err) {
|
|
60
|
+
throw new Error(`Upload root is unavailable (${fileSystemMessage(err)}): ${lexical}`);
|
|
61
|
+
}
|
|
62
|
+
if (rootInfo.isSymbolicLink()) {
|
|
63
|
+
throw new Error(`Upload root must not be a symbolic link or junction: ${lexical}`);
|
|
64
|
+
}
|
|
65
|
+
if (!rootInfo.isDirectory()) {
|
|
66
|
+
throw new Error(`Upload root is not a directory: ${lexical}`);
|
|
67
|
+
}
|
|
68
|
+
const canonical = path.normalize(await fileSystem.realpath(lexical));
|
|
69
|
+
const canonicalInfo = await fileSystem.stat(canonical);
|
|
70
|
+
if (!canonicalInfo.isDirectory()) {
|
|
71
|
+
throw new Error(`Canonical upload root is not a directory: ${lexical}`);
|
|
72
|
+
}
|
|
73
|
+
if (rootInfo.dev === 0n ||
|
|
74
|
+
rootInfo.ino === 0n ||
|
|
75
|
+
canonicalInfo.dev === 0n ||
|
|
76
|
+
canonicalInfo.ino === 0n) {
|
|
77
|
+
throw new Error(`Upload root has no stable filesystem identity: ${lexical}`);
|
|
78
|
+
}
|
|
79
|
+
if (rootInfo.dev !== canonicalInfo.dev || rootInfo.ino !== canonicalInfo.ino) {
|
|
80
|
+
throw new Error(`Upload root changed during canonical resolution: ${lexical}`);
|
|
81
|
+
}
|
|
82
|
+
resolved.push({ lexical, canonical, identity: canonicalInfo });
|
|
83
|
+
}
|
|
84
|
+
return resolved;
|
|
85
|
+
}
|
|
86
|
+
export function assertSameFileIdentity(opened, resolved) {
|
|
87
|
+
if (opened.dev === 0n || opened.ino === 0n || resolved.dev === 0n || resolved.ino === 0n) {
|
|
88
|
+
throw new Error('The platform did not provide a stable file identity; upload fails closed.');
|
|
89
|
+
}
|
|
90
|
+
if (opened.dev !== resolved.dev || opened.ino !== resolved.ino) {
|
|
91
|
+
throw new Error('The image path changed after it was opened; upload was refused.');
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
function hasSameIdentity(left, right) {
|
|
95
|
+
return (left.dev !== 0n &&
|
|
96
|
+
left.ino !== 0n &&
|
|
97
|
+
right.dev !== 0n &&
|
|
98
|
+
right.ino !== 0n &&
|
|
99
|
+
left.dev === right.dev &&
|
|
100
|
+
left.ino === right.ino);
|
|
101
|
+
}
|
|
102
|
+
function assertFileUnchanged(before, after) {
|
|
103
|
+
assertSameFileIdentity(before, after);
|
|
104
|
+
if (before.size !== after.size ||
|
|
105
|
+
before.mtimeNs !== after.mtimeNs ||
|
|
106
|
+
before.ctimeNs !== after.ctimeNs) {
|
|
107
|
+
throw new Error('The image file changed while it was being read; upload was refused.');
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
async function assertStableMatchedRoot(canonicalPath, lexicalMatches, fileSystem) {
|
|
111
|
+
const canonicalMatches = lexicalMatches.filter((root) => isWithin(root.canonical, canonicalPath));
|
|
112
|
+
if (canonicalMatches.length === 0) {
|
|
113
|
+
throw new Error('Image resolves outside uploads.allowedRoots.');
|
|
114
|
+
}
|
|
115
|
+
for (const root of canonicalMatches) {
|
|
116
|
+
try {
|
|
117
|
+
const currentRootInfo = await fileSystem.stat(root.canonical);
|
|
118
|
+
if (currentRootInfo.isDirectory() && hasSameIdentity(root.identity, currentRootInfo))
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
// A missing or unreadable root cannot authorize the opened file.
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
throw new Error('The matched upload root changed before the image was opened; upload was refused.');
|
|
126
|
+
}
|
|
127
|
+
async function readBoundedFile(handle, expectedSize) {
|
|
128
|
+
if (expectedSize > BigInt(MAX_PNG_BYTES)) {
|
|
129
|
+
throw new Error(`PNG exceeds the ${MAX_PNG_BYTES}-byte upload limit.`);
|
|
130
|
+
}
|
|
131
|
+
const expected = Number(expectedSize);
|
|
132
|
+
const bytes = Buffer.alloc(expected + 1);
|
|
133
|
+
let offset = 0;
|
|
134
|
+
while (offset < bytes.length) {
|
|
135
|
+
const result = await handle.read(bytes, offset, bytes.length - offset, null);
|
|
136
|
+
if (result.bytesRead === 0)
|
|
137
|
+
break;
|
|
138
|
+
offset += result.bytesRead;
|
|
139
|
+
}
|
|
140
|
+
if (offset !== expected) {
|
|
141
|
+
throw new Error('The image file size changed while it was being read; upload was refused.');
|
|
142
|
+
}
|
|
143
|
+
return bytes.subarray(0, expected);
|
|
144
|
+
}
|
|
145
|
+
function chunkType(bytes, offset) {
|
|
146
|
+
const type = bytes.toString('ascii', offset, offset + 4);
|
|
147
|
+
if (!/^[A-Za-z]{4}$/.test(type)) {
|
|
148
|
+
throw new Error('PNG contains an invalid chunk type.');
|
|
149
|
+
}
|
|
150
|
+
return type;
|
|
151
|
+
}
|
|
152
|
+
function readAndValidateHeader(bytes, offset) {
|
|
153
|
+
const width = bytes.readUInt32BE(offset + 8);
|
|
154
|
+
const height = bytes.readUInt32BE(offset + 12);
|
|
155
|
+
const bitDepth = bytes[offset + 16];
|
|
156
|
+
const colorType = bytes[offset + 17];
|
|
157
|
+
const compressionMethod = bytes[offset + 18];
|
|
158
|
+
const filterMethod = bytes[offset + 19];
|
|
159
|
+
const interlaceMethod = bytes[offset + 20];
|
|
160
|
+
const outsideDimensionRange = width < MIN_PNG_DIMENSION ||
|
|
161
|
+
height < MIN_PNG_DIMENSION ||
|
|
162
|
+
width > MAX_PNG_DIMENSION ||
|
|
163
|
+
height > MAX_PNG_DIMENSION;
|
|
164
|
+
if (outsideDimensionRange || width * height > MAX_PNG_PIXELS) {
|
|
165
|
+
throw new Error(`PNG dimensions must be between ${MIN_PNG_DIMENSION}x${MIN_PNG_DIMENSION} and ${MAX_PNG_DIMENSION}x${MAX_PNG_DIMENSION} pixels.`);
|
|
166
|
+
}
|
|
167
|
+
if (!CHANNELS_BY_COLOR_TYPE.has(colorType)) {
|
|
168
|
+
throw new Error(`PNG contains an unsupported color type (${colorType}).`);
|
|
169
|
+
}
|
|
170
|
+
if (!BIT_DEPTHS_BY_COLOR_TYPE.get(colorType)?.has(bitDepth)) {
|
|
171
|
+
throw new Error(`PNG bit depth ${bitDepth} is invalid for color type ${colorType}.`);
|
|
172
|
+
}
|
|
173
|
+
if (compressionMethod !== 0 || filterMethod !== 0) {
|
|
174
|
+
throw new Error('PNG uses an unsupported compression or filter method.');
|
|
175
|
+
}
|
|
176
|
+
if (interlaceMethod !== 0 && interlaceMethod !== 1) {
|
|
177
|
+
throw new Error(`PNG contains an unsupported interlace method (${interlaceMethod}).`);
|
|
178
|
+
}
|
|
179
|
+
return { width, height, bitDepth, colorType, interlaceMethod };
|
|
180
|
+
}
|
|
181
|
+
function scanlineBytes(width, bitsPerPixel) {
|
|
182
|
+
return Math.ceil((width * bitsPerPixel) / 8) + 1;
|
|
183
|
+
}
|
|
184
|
+
function adam7PassSize(state, xStart, yStart, xStep, yStep, bitsPerPixel) {
|
|
185
|
+
const passWidth = state.width <= xStart ? 0 : Math.ceil((state.width - xStart) / xStep);
|
|
186
|
+
const passHeight = state.height <= yStart ? 0 : Math.ceil((state.height - yStart) / yStep);
|
|
187
|
+
return passWidth === 0 || passHeight === 0
|
|
188
|
+
? 0
|
|
189
|
+
: scanlineBytes(passWidth, bitsPerPixel) * passHeight;
|
|
190
|
+
}
|
|
191
|
+
function expectedInflatedBytes(state) {
|
|
192
|
+
const channels = CHANNELS_BY_COLOR_TYPE.get(state.colorType);
|
|
193
|
+
if (!channels)
|
|
194
|
+
throw new Error('PNG color type was not validated.');
|
|
195
|
+
const bitsPerPixel = channels * state.bitDepth;
|
|
196
|
+
if (state.interlaceMethod === 0) {
|
|
197
|
+
return scanlineBytes(state.width, bitsPerPixel) * state.height;
|
|
198
|
+
}
|
|
199
|
+
const passes = [
|
|
200
|
+
[0, 0, 8, 8],
|
|
201
|
+
[4, 0, 8, 8],
|
|
202
|
+
[0, 4, 4, 8],
|
|
203
|
+
[2, 0, 4, 4],
|
|
204
|
+
[0, 2, 2, 4],
|
|
205
|
+
[1, 0, 2, 2],
|
|
206
|
+
[0, 1, 1, 2],
|
|
207
|
+
];
|
|
208
|
+
return passes.reduce((total, [xStart, yStart, xStep, yStep]) => total + adam7PassSize(state, xStart, yStart, xStep, yStep, bitsPerPixel), 0);
|
|
209
|
+
}
|
|
210
|
+
function assertBoundedImageData(state) {
|
|
211
|
+
const expectedBytes = expectedInflatedBytes(state);
|
|
212
|
+
let inflated;
|
|
213
|
+
try {
|
|
214
|
+
inflated = inflateSync(Buffer.concat(state.idatChunks), {
|
|
215
|
+
maxOutputLength: expectedBytes,
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
catch (err) {
|
|
219
|
+
if (err.code === 'ERR_BUFFER_TOO_LARGE') {
|
|
220
|
+
throw new Error('PNG decompressed image data exceeds its dimension-derived limit.');
|
|
221
|
+
}
|
|
222
|
+
const message = err instanceof Error ? err.message : 'invalid compressed data';
|
|
223
|
+
throw new Error(`PNG image data could not be decompressed safely: ${message}`);
|
|
224
|
+
}
|
|
225
|
+
if (inflated.length !== expectedBytes) {
|
|
226
|
+
throw new Error(`PNG decompressed image data has an unexpected length (${inflated.length} instead of ${expectedBytes}).`);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
function inspectChunk(bytes, offset, length, type, state) {
|
|
230
|
+
if (!state.seenHeader && type !== 'IHDR') {
|
|
231
|
+
throw new Error('PNG must begin with an IHDR chunk.');
|
|
232
|
+
}
|
|
233
|
+
if (APNG_CHUNKS.has(type)) {
|
|
234
|
+
throw new Error(`Animated PNG content is not supported (${type}).`);
|
|
235
|
+
}
|
|
236
|
+
if (type === 'iCCP') {
|
|
237
|
+
throw new Error('Compressed PNG color profiles are not supported.');
|
|
238
|
+
}
|
|
239
|
+
if (/^[A-Z]/.test(type) && !KNOWN_CRITICAL_CHUNKS.has(type)) {
|
|
240
|
+
throw new Error(`PNG contains an unsupported critical chunk (${type}).`);
|
|
241
|
+
}
|
|
242
|
+
if (type === 'IHDR') {
|
|
243
|
+
if (state.seenHeader || length !== 13) {
|
|
244
|
+
throw new Error('PNG contains an invalid IHDR chunk.');
|
|
245
|
+
}
|
|
246
|
+
state.seenHeader = true;
|
|
247
|
+
Object.assign(state, readAndValidateHeader(bytes, offset));
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
if (type === 'IDAT') {
|
|
251
|
+
if (state.imageDataEnded)
|
|
252
|
+
throw new Error('PNG IDAT chunks must be consecutive.');
|
|
253
|
+
state.seenImageData = true;
|
|
254
|
+
state.idatChunks.push(bytes.subarray(offset + 8, offset + 8 + length));
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
if (state.seenImageData)
|
|
258
|
+
state.imageDataEnded = true;
|
|
259
|
+
if (type === 'IEND') {
|
|
260
|
+
if (length !== 0 || !state.seenImageData) {
|
|
261
|
+
throw new Error('PNG contains an invalid IEND chunk.');
|
|
262
|
+
}
|
|
263
|
+
state.seenEnd = true;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
export function validateStaticPngBuffer(bytes) {
|
|
267
|
+
if (bytes.length < PNG_SIGNATURE.length || !bytes.subarray(0, 8).equals(PNG_SIGNATURE)) {
|
|
268
|
+
throw new Error('Image is not a PNG file.');
|
|
269
|
+
}
|
|
270
|
+
let offset = PNG_SIGNATURE.length;
|
|
271
|
+
let chunkCount = 0;
|
|
272
|
+
const state = {
|
|
273
|
+
width: 0,
|
|
274
|
+
height: 0,
|
|
275
|
+
bitDepth: 0,
|
|
276
|
+
colorType: -1,
|
|
277
|
+
interlaceMethod: -1,
|
|
278
|
+
seenHeader: false,
|
|
279
|
+
seenImageData: false,
|
|
280
|
+
imageDataEnded: false,
|
|
281
|
+
seenEnd: false,
|
|
282
|
+
idatChunks: [],
|
|
283
|
+
};
|
|
284
|
+
while (offset < bytes.length) {
|
|
285
|
+
chunkCount += 1;
|
|
286
|
+
if (chunkCount > MAX_PNG_CHUNKS) {
|
|
287
|
+
throw new Error(`PNG contains too many chunks (maximum ${MAX_PNG_CHUNKS}).`);
|
|
288
|
+
}
|
|
289
|
+
if (offset + 12 > bytes.length) {
|
|
290
|
+
throw new Error('PNG ends in a truncated chunk.');
|
|
291
|
+
}
|
|
292
|
+
const length = bytes.readUInt32BE(offset);
|
|
293
|
+
const type = chunkType(bytes, offset + 4);
|
|
294
|
+
const chunkEnd = offset + 12 + length;
|
|
295
|
+
if (chunkEnd > bytes.length) {
|
|
296
|
+
throw new Error(`PNG ${type} chunk is truncated.`);
|
|
297
|
+
}
|
|
298
|
+
inspectChunk(bytes, offset, length, type, state);
|
|
299
|
+
if (type === 'IEND') {
|
|
300
|
+
offset = chunkEnd;
|
|
301
|
+
if (offset !== bytes.length) {
|
|
302
|
+
throw new Error('PNG contains trailing data after IEND.');
|
|
303
|
+
}
|
|
304
|
+
break;
|
|
305
|
+
}
|
|
306
|
+
offset = chunkEnd;
|
|
307
|
+
}
|
|
308
|
+
if (!state.seenEnd) {
|
|
309
|
+
throw new Error('PNG is missing its terminal IEND chunk.');
|
|
310
|
+
}
|
|
311
|
+
assertBoundedImageData(state);
|
|
312
|
+
try {
|
|
313
|
+
const decoded = decode(bytes, { checkCrc: true });
|
|
314
|
+
if (decoded.width !== state.width || decoded.height !== state.height) {
|
|
315
|
+
throw new Error('decoded dimensions do not match IHDR');
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
catch (err) {
|
|
319
|
+
const message = err instanceof Error ? err.message : 'decoder rejected the image';
|
|
320
|
+
throw new Error(`PNG decoder rejected the image: ${message}`);
|
|
321
|
+
}
|
|
322
|
+
return { width: state.width, height: state.height };
|
|
323
|
+
}
|
|
324
|
+
export async function readValidatedStaticPng(imagePath, allowedRoots, fileSystem = nativeFileSystem) {
|
|
325
|
+
if (!path.isAbsolute(imagePath)) {
|
|
326
|
+
throw new Error('imagePath must be absolute.');
|
|
327
|
+
}
|
|
328
|
+
const candidate = path.normalize(imagePath);
|
|
329
|
+
const roots = await canonicalizeRoots(allowedRoots, fileSystem);
|
|
330
|
+
const lexicalMatches = roots.filter((root) => isWithin(root.lexical, candidate));
|
|
331
|
+
if (lexicalMatches.length === 0) {
|
|
332
|
+
throw new Error('imagePath is outside uploads.allowedRoots.');
|
|
333
|
+
}
|
|
334
|
+
let linkInfo;
|
|
335
|
+
try {
|
|
336
|
+
linkInfo = await fileSystem.lstat(candidate);
|
|
337
|
+
}
|
|
338
|
+
catch (err) {
|
|
339
|
+
throw new Error(`Image file is missing or unreadable (${fileSystemMessage(err)}).`);
|
|
340
|
+
}
|
|
341
|
+
if (linkInfo.isSymbolicLink()) {
|
|
342
|
+
throw new Error('Image file must not be a symbolic link or junction.');
|
|
343
|
+
}
|
|
344
|
+
if (!linkInfo.isFile()) {
|
|
345
|
+
throw new Error('Image path must refer to a regular file.');
|
|
346
|
+
}
|
|
347
|
+
const noFollow = process.platform === 'win32' ? 0 : (constants.O_NOFOLLOW ?? 0);
|
|
348
|
+
const flags = constants.O_RDONLY | (constants.O_NONBLOCK ?? 0) | noFollow;
|
|
349
|
+
let handle;
|
|
350
|
+
try {
|
|
351
|
+
handle = await fileSystem.open(candidate, flags);
|
|
352
|
+
}
|
|
353
|
+
catch (err) {
|
|
354
|
+
throw new Error(`Image file could not be opened (${fileSystemMessage(err)}).`);
|
|
355
|
+
}
|
|
356
|
+
let bytes;
|
|
357
|
+
let canonicalPath;
|
|
358
|
+
try {
|
|
359
|
+
const openedInfo = await handle.stat({ bigint: true });
|
|
360
|
+
if (!openedInfo.isFile()) {
|
|
361
|
+
throw new Error('Opened image object is not a regular file.');
|
|
362
|
+
}
|
|
363
|
+
assertSameFileIdentity(linkInfo, openedInfo);
|
|
364
|
+
if (openedInfo.size > BigInt(MAX_PNG_BYTES)) {
|
|
365
|
+
throw new Error(`PNG exceeds the ${MAX_PNG_BYTES}-byte upload limit.`);
|
|
366
|
+
}
|
|
367
|
+
canonicalPath = path.normalize(await fileSystem.realpath(candidate));
|
|
368
|
+
await assertStableMatchedRoot(canonicalPath, lexicalMatches, fileSystem);
|
|
369
|
+
const resolvedInfo = await fileSystem.stat(canonicalPath);
|
|
370
|
+
if (!resolvedInfo.isFile()) {
|
|
371
|
+
throw new Error('Resolved image object is not a regular file.');
|
|
372
|
+
}
|
|
373
|
+
assertSameFileIdentity(openedInfo, resolvedInfo);
|
|
374
|
+
bytes = await readBoundedFile(handle, openedInfo.size);
|
|
375
|
+
const afterReadInfo = await handle.stat({ bigint: true });
|
|
376
|
+
assertFileUnchanged(openedInfo, afterReadInfo);
|
|
377
|
+
}
|
|
378
|
+
finally {
|
|
379
|
+
await handle.close();
|
|
380
|
+
}
|
|
381
|
+
const dimensions = validateStaticPngBuffer(bytes);
|
|
382
|
+
return {
|
|
383
|
+
bytes,
|
|
384
|
+
byteSize: bytes.length,
|
|
385
|
+
fileName: path.basename(canonicalPath),
|
|
386
|
+
width: dimensions.width,
|
|
387
|
+
height: dimensions.height,
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
//# sourceMappingURL=localPng.js.map
|