@atlassian-dc-mcp/common 0.27.0 → 0.28.0
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 +12 -0
- package/build/__tests__/attachment-download.test.d.ts +2 -0
- package/build/__tests__/attachment-download.test.d.ts.map +1 -0
- package/build/__tests__/attachment-download.test.js +119 -0
- package/build/__tests__/attachment-download.test.js.map +1 -0
- package/build/__tests__/attachment-gateway.test.d.ts +2 -0
- package/build/__tests__/attachment-gateway.test.d.ts.map +1 -0
- package/build/__tests__/attachment-gateway.test.js +123 -0
- package/build/__tests__/attachment-gateway.test.js.map +1 -0
- package/build/attachment-download.d.ts +54 -0
- package/build/attachment-download.d.ts.map +1 -0
- package/build/attachment-download.js +94 -0
- package/build/attachment-download.js.map +1 -0
- package/build/attachment-gateway.d.ts +57 -0
- package/build/attachment-gateway.d.ts.map +1 -0
- package/build/attachment-gateway.js +201 -0
- package/build/attachment-gateway.js.map +1 -0
- package/build/index.d.ts +2 -0
- package/build/index.d.ts.map +1 -1
- package/build/index.js +2 -0
- package/build/index.js.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/attachment-download.test.ts +162 -0
- package/src/__tests__/attachment-gateway.test.ts +156 -0
- package/src/attachment-download.ts +154 -0
- package/src/attachment-gateway.ts +255 -0
- package/src/index.ts +2 -0
- package/tsconfig.tsbuildinfo +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,18 @@
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
5
|
|
|
6
|
+
# [0.28.0](https://github.com/b1ff/atlassian-dc-mcp/compare/v0.27.0...v0.28.0) (2026-07-22)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
### Features
|
|
10
|
+
|
|
11
|
+
* **attachments:** sandbox local filesystem access behind an opt-in gateway ([2a22547](https://github.com/b1ff/atlassian-dc-mcp/commit/2a22547292a390a032642ad09bd09ede0808e9d7))
|
|
12
|
+
* **jira:** add jira_downloadAttachment tool ([31cbeef](https://github.com/b1ff/atlassian-dc-mcp/commit/31cbeefd00fde37b1245efbe975ea4d63db46758))
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
|
|
6
18
|
# [0.27.0](https://github.com/b1ff/atlassian-dc-mcp/compare/v0.26.0...v0.27.0) (2026-07-18)
|
|
7
19
|
|
|
8
20
|
**Note:** Version bump only for package @atlassian-dc-mcp/common
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"attachment-download.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/attachment-download.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { downloadAttachment } from '../attachment-download.js';
|
|
5
|
+
function mockFetchOnce(body, init) {
|
|
6
|
+
const ok = init?.ok ?? true;
|
|
7
|
+
const headers = new Map();
|
|
8
|
+
if (init?.contentType)
|
|
9
|
+
headers.set('content-type', init.contentType);
|
|
10
|
+
if (init?.contentLength)
|
|
11
|
+
headers.set('content-length', init.contentLength);
|
|
12
|
+
const response = {
|
|
13
|
+
ok,
|
|
14
|
+
status: init?.status ?? (ok ? 200 : 500),
|
|
15
|
+
statusText: init?.statusText ?? (ok ? 'OK' : 'Error'),
|
|
16
|
+
headers: { get: (name) => headers.get(name.toLowerCase()) ?? null },
|
|
17
|
+
arrayBuffer: async () => body.buffer.slice(body.byteOffset, body.byteOffset + body.byteLength),
|
|
18
|
+
};
|
|
19
|
+
if (init?.stream) {
|
|
20
|
+
let sent = false;
|
|
21
|
+
response.body = {
|
|
22
|
+
getReader: () => ({
|
|
23
|
+
read: async () => (sent ? { done: true, value: undefined } : ((sent = true), { done: false, value: new Uint8Array(body) })),
|
|
24
|
+
cancel: async () => undefined,
|
|
25
|
+
}),
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
global.fetch = jest.fn().mockResolvedValue(response);
|
|
29
|
+
}
|
|
30
|
+
describe('downloadAttachment', () => {
|
|
31
|
+
let tmpDir;
|
|
32
|
+
beforeEach(() => {
|
|
33
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'dc-mcp-dl-'));
|
|
34
|
+
});
|
|
35
|
+
afterEach(() => {
|
|
36
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
37
|
+
jest.restoreAllMocks();
|
|
38
|
+
});
|
|
39
|
+
it('writes the file to the provided destination and reports metadata', async () => {
|
|
40
|
+
mockFetchOnce(Buffer.from('hello world'), { contentType: 'text/plain' });
|
|
41
|
+
const destination = path.join(tmpDir, 'note.txt');
|
|
42
|
+
const result = await downloadAttachment({
|
|
43
|
+
url: 'https://host/download/x',
|
|
44
|
+
token: 'tok',
|
|
45
|
+
filename: 'note.txt',
|
|
46
|
+
options: { destination },
|
|
47
|
+
});
|
|
48
|
+
expect(global.fetch).toHaveBeenCalledWith('https://host/download/x', {
|
|
49
|
+
headers: { Authorization: 'Bearer tok', 'X-Atlassian-Token': 'nocheck' },
|
|
50
|
+
});
|
|
51
|
+
expect(result.savedPath).toBe(destination);
|
|
52
|
+
expect(result.size).toBe(11);
|
|
53
|
+
expect(fs.readFileSync(destination, 'utf-8')).toBe('hello world');
|
|
54
|
+
expect(result.content).toBeUndefined();
|
|
55
|
+
});
|
|
56
|
+
it('refuses to overwrite an existing destination file', async () => {
|
|
57
|
+
mockFetchOnce(Buffer.from('new content'));
|
|
58
|
+
const destination = path.join(tmpDir, 'exists.txt');
|
|
59
|
+
fs.writeFileSync(destination, 'original');
|
|
60
|
+
await expect(downloadAttachment({ url: 'https://host/x', token: 'tok', filename: 'exists.txt', options: { destination } })).rejects.toThrow();
|
|
61
|
+
expect(fs.readFileSync(destination, 'utf-8')).toBe('original');
|
|
62
|
+
});
|
|
63
|
+
it('returns text content inline when requested', async () => {
|
|
64
|
+
mockFetchOnce(Buffer.from('inline text'));
|
|
65
|
+
const result = await downloadAttachment({
|
|
66
|
+
url: 'https://host/download/x',
|
|
67
|
+
token: () => 'tok',
|
|
68
|
+
filename: 'note.txt',
|
|
69
|
+
options: { returnContent: 'text' },
|
|
70
|
+
});
|
|
71
|
+
expect(result.content).toBe('inline text');
|
|
72
|
+
expect(result.encoding).toBe('text');
|
|
73
|
+
expect(result.savedPath).toBeUndefined();
|
|
74
|
+
});
|
|
75
|
+
it('returns base64 content inline when requested', async () => {
|
|
76
|
+
const bytes = Buffer.from([0x00, 0x01, 0x02, 0xff]);
|
|
77
|
+
mockFetchOnce(bytes);
|
|
78
|
+
const result = await downloadAttachment({
|
|
79
|
+
url: 'https://host/download/x',
|
|
80
|
+
token: 'tok',
|
|
81
|
+
filename: 'blob.bin',
|
|
82
|
+
options: { returnContent: 'base64' },
|
|
83
|
+
});
|
|
84
|
+
expect(result.content).toBe(bytes.toString('base64'));
|
|
85
|
+
expect(result.encoding).toBe('base64');
|
|
86
|
+
});
|
|
87
|
+
it('omits inline content when the file exceeds maxInlineBytes', async () => {
|
|
88
|
+
mockFetchOnce(Buffer.from('123456'));
|
|
89
|
+
const result = await downloadAttachment({
|
|
90
|
+
url: 'https://host/download/x',
|
|
91
|
+
token: 'tok',
|
|
92
|
+
filename: 'big.txt',
|
|
93
|
+
options: { returnContent: 'text', maxInlineBytes: 3 },
|
|
94
|
+
});
|
|
95
|
+
expect(result.content).toBeUndefined();
|
|
96
|
+
expect(result.contentOmittedReason).toContain('exceeds inline cap');
|
|
97
|
+
});
|
|
98
|
+
it('rejects early when content-length exceeds the download cap', async () => {
|
|
99
|
+
mockFetchOnce(Buffer.from('1234567890'), { contentLength: '10' });
|
|
100
|
+
await expect(downloadAttachment({ url: 'https://host/x', token: 'tok', filename: 'big.bin', options: { maxDownloadBytes: 5 } })).rejects.toThrow('exceeds the configured download limit');
|
|
101
|
+
});
|
|
102
|
+
it('aborts a streamed body that exceeds the download cap', async () => {
|
|
103
|
+
mockFetchOnce(Buffer.from('1234567890'), { stream: true });
|
|
104
|
+
await expect(downloadAttachment({ url: 'https://host/x', token: 'tok', filename: 'big.bin', options: { maxDownloadBytes: 5 } })).rejects.toThrow('exceeds the configured download limit');
|
|
105
|
+
});
|
|
106
|
+
it('enforces the cap on a buffered body without content-length', async () => {
|
|
107
|
+
mockFetchOnce(Buffer.from('1234567890'));
|
|
108
|
+
await expect(downloadAttachment({ url: 'https://host/x', token: 'tok', filename: 'big.bin', options: { maxDownloadBytes: 5 } })).rejects.toThrow('exceeds the configured download limit');
|
|
109
|
+
});
|
|
110
|
+
it('throws when the response is not ok', async () => {
|
|
111
|
+
mockFetchOnce(Buffer.from(''), { ok: false, status: 404, statusText: 'Not Found' });
|
|
112
|
+
await expect(downloadAttachment({ url: 'https://host/x', token: 'tok', filename: 'f.txt' })).rejects.toThrow('404 Not Found');
|
|
113
|
+
});
|
|
114
|
+
it('throws when no token is available', async () => {
|
|
115
|
+
mockFetchOnce(Buffer.from(''));
|
|
116
|
+
await expect(downloadAttachment({ url: 'https://host/x', token: () => undefined, filename: 'f.txt' })).rejects.toThrow('Missing API token');
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
//# sourceMappingURL=attachment-download.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"attachment-download.test.js","sourceRoot":"","sources":["../../src/__tests__/attachment-download.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"attachment-gateway.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/attachment-gateway.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { DEFAULT_MAX_ATTACHMENT_BYTES, resolveAttachmentGateway, resolveDownloadDestination, resolveUploadSource, } from '../attachment-gateway.js';
|
|
5
|
+
const PRODUCT = {
|
|
6
|
+
id: 'jira',
|
|
7
|
+
envVars: { host: 'JIRA_HOST', apiBasePath: 'JIRA_API_BASE_PATH', token: 'JIRA_API_TOKEN', defaultPageSize: 'JIRA_DEFAULT_PAGE_SIZE' },
|
|
8
|
+
};
|
|
9
|
+
const silentWarn = () => undefined;
|
|
10
|
+
describe('resolveAttachmentGateway', () => {
|
|
11
|
+
let root;
|
|
12
|
+
let realRoot;
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
root = fs.mkdtempSync(path.join(os.tmpdir(), 'dc-mcp-gw-'));
|
|
15
|
+
realRoot = fs.realpathSync(root);
|
|
16
|
+
});
|
|
17
|
+
afterEach(() => {
|
|
18
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
19
|
+
});
|
|
20
|
+
it('is disabled by default', () => {
|
|
21
|
+
const gw = resolveAttachmentGateway(PRODUCT, { env: {}, warn: silentWarn });
|
|
22
|
+
expect(gw.upload.enabled).toBe(false);
|
|
23
|
+
expect(gw.download.enabled).toBe(false);
|
|
24
|
+
});
|
|
25
|
+
it('stays disabled when enabled but no valid root is configured', () => {
|
|
26
|
+
const gw = resolveAttachmentGateway(PRODUCT, {
|
|
27
|
+
env: { JIRA_ATTACHMENTS_UPLOAD_ENABLED: 'true' },
|
|
28
|
+
warn: silentWarn,
|
|
29
|
+
});
|
|
30
|
+
expect(gw.upload.enabled).toBe(false);
|
|
31
|
+
});
|
|
32
|
+
it('enables upload with a canonical root and default size limit', () => {
|
|
33
|
+
const gw = resolveAttachmentGateway(PRODUCT, {
|
|
34
|
+
env: { JIRA_ATTACHMENTS_UPLOAD_ENABLED: 'true', JIRA_ATTACHMENTS_UPLOAD_ROOTS: root },
|
|
35
|
+
warn: silentWarn,
|
|
36
|
+
});
|
|
37
|
+
expect(gw.upload.enabled).toBe(true);
|
|
38
|
+
expect(gw.upload.roots).toEqual([realRoot]);
|
|
39
|
+
expect(gw.upload.maxBytes).toBe(DEFAULT_MAX_ATTACHMENT_BYTES);
|
|
40
|
+
});
|
|
41
|
+
it('uses JIRA_ATTACHMENTS_DIR as a shared exchange root and honours size overrides', () => {
|
|
42
|
+
const gw = resolveAttachmentGateway(PRODUCT, {
|
|
43
|
+
env: {
|
|
44
|
+
JIRA_ATTACHMENTS_UPLOAD_ENABLED: 'true',
|
|
45
|
+
JIRA_ATTACHMENTS_DOWNLOAD_ENABLED: '1',
|
|
46
|
+
JIRA_ATTACHMENTS_DIR: root,
|
|
47
|
+
JIRA_ATTACHMENTS_MAX_UPLOAD_BYTES: '1024',
|
|
48
|
+
},
|
|
49
|
+
warn: silentWarn,
|
|
50
|
+
});
|
|
51
|
+
expect(gw.upload.roots).toEqual([realRoot]);
|
|
52
|
+
expect(gw.download.roots).toEqual([realRoot]);
|
|
53
|
+
expect(gw.upload.maxBytes).toBe(1024);
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
describe('resolveUploadSource', () => {
|
|
57
|
+
let root;
|
|
58
|
+
const side = () => ({ enabled: true, roots: [fs.realpathSync(root)], maxBytes: 100 });
|
|
59
|
+
beforeEach(() => {
|
|
60
|
+
root = fs.mkdtempSync(path.join(os.tmpdir(), 'dc-mcp-up-'));
|
|
61
|
+
});
|
|
62
|
+
afterEach(() => {
|
|
63
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
64
|
+
});
|
|
65
|
+
it('resolves a regular file inside the root', async () => {
|
|
66
|
+
fs.writeFileSync(path.join(root, 'a.txt'), 'hi');
|
|
67
|
+
const { absolutePath, size } = await resolveUploadSource({ requestedPath: 'a.txt', side: side() });
|
|
68
|
+
expect(absolutePath).toBe(path.join(fs.realpathSync(root), 'a.txt'));
|
|
69
|
+
expect(size).toBe(2);
|
|
70
|
+
});
|
|
71
|
+
it('rejects absolute paths', async () => {
|
|
72
|
+
await expect(resolveUploadSource({ requestedPath: '/etc/passwd', side: side() })).rejects.toThrow('not absolute');
|
|
73
|
+
});
|
|
74
|
+
it('rejects traversal outside the root', async () => {
|
|
75
|
+
await expect(resolveUploadSource({ requestedPath: '../secret', side: side() })).rejects.toThrow('within the configured');
|
|
76
|
+
});
|
|
77
|
+
it('rejects a symlink leaf', async () => {
|
|
78
|
+
const target = path.join(root, 'real.txt');
|
|
79
|
+
fs.writeFileSync(target, 'secret');
|
|
80
|
+
fs.symlinkSync(target, path.join(root, 'link.txt'));
|
|
81
|
+
await expect(resolveUploadSource({ requestedPath: 'link.txt', side: side() })).rejects.toThrow('symlink');
|
|
82
|
+
});
|
|
83
|
+
it('rejects a path that escapes via a symlinked directory', async () => {
|
|
84
|
+
const outside = fs.mkdtempSync(path.join(os.tmpdir(), 'dc-mcp-out-'));
|
|
85
|
+
fs.writeFileSync(path.join(outside, 'secret.txt'), 'x');
|
|
86
|
+
fs.symlinkSync(outside, path.join(root, 'escape'));
|
|
87
|
+
await expect(resolveUploadSource({ requestedPath: 'escape/secret.txt', side: side() })).rejects.toThrow('outside');
|
|
88
|
+
fs.rmSync(outside, { recursive: true, force: true });
|
|
89
|
+
});
|
|
90
|
+
it('rejects a non-regular file (directory)', async () => {
|
|
91
|
+
fs.mkdirSync(path.join(root, 'sub'));
|
|
92
|
+
await expect(resolveUploadSource({ requestedPath: 'sub', side: side() })).rejects.toThrow('non-regular');
|
|
93
|
+
});
|
|
94
|
+
it('rejects a file over the size limit', async () => {
|
|
95
|
+
fs.writeFileSync(path.join(root, 'big.txt'), 'x'.repeat(200));
|
|
96
|
+
await expect(resolveUploadSource({ requestedPath: 'big.txt', side: side() })).rejects.toThrow('exceeds the configured upload limit');
|
|
97
|
+
});
|
|
98
|
+
it('throws when upload is disabled', async () => {
|
|
99
|
+
await expect(resolveUploadSource({ requestedPath: 'a.txt', side: { enabled: false, roots: [], maxBytes: 100 } })).rejects.toThrow('disabled');
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
describe('resolveDownloadDestination', () => {
|
|
103
|
+
let root;
|
|
104
|
+
const side = () => ({ enabled: true, roots: [fs.realpathSync(root)], maxBytes: 100 });
|
|
105
|
+
beforeEach(() => {
|
|
106
|
+
root = fs.mkdtempSync(path.join(os.tmpdir(), 'dc-mcp-dd-'));
|
|
107
|
+
});
|
|
108
|
+
afterEach(() => {
|
|
109
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
110
|
+
});
|
|
111
|
+
it('resolves a basename into the download root', async () => {
|
|
112
|
+
const dest = await resolveDownloadDestination({ requestedName: 'out.bin', side: side() });
|
|
113
|
+
expect(dest).toBe(path.join(fs.realpathSync(root), 'out.bin'));
|
|
114
|
+
});
|
|
115
|
+
it('strips directory components from the requested name', async () => {
|
|
116
|
+
const dest = await resolveDownloadDestination({ requestedName: '../../etc/passwd', side: side() });
|
|
117
|
+
expect(dest).toBe(path.join(fs.realpathSync(root), 'passwd'));
|
|
118
|
+
});
|
|
119
|
+
it('throws when saving is disabled', async () => {
|
|
120
|
+
await expect(resolveDownloadDestination({ requestedName: 'out.bin', side: { enabled: false, roots: [], maxBytes: 100 } })).rejects.toThrow('disabled');
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
//# sourceMappingURL=attachment-gateway.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"attachment-gateway.test.js","sourceRoot":"","sources":["../../src/__tests__/attachment-gateway.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How the downloaded bytes should be returned to the caller inline, in addition
|
|
3
|
+
* to (or instead of) being written to disk.
|
|
4
|
+
* - `none`: do not embed the bytes in the response (default)
|
|
5
|
+
* - `base64`: embed base64-encoded bytes (suitable for binary files)
|
|
6
|
+
* - `text`: embed the bytes decoded as UTF-8 text (suitable for text files)
|
|
7
|
+
*/
|
|
8
|
+
export type AttachmentContentEncoding = 'none' | 'base64' | 'text';
|
|
9
|
+
/** Default cap for inline content so responses do not balloon. 1 MiB. */
|
|
10
|
+
export declare const DEFAULT_MAX_INLINE_BYTES = 1048576;
|
|
11
|
+
export interface AttachmentDownloadOptions {
|
|
12
|
+
/**
|
|
13
|
+
* Absolute, already-validated destination path to write the bytes to. Resolving
|
|
14
|
+
* and sandboxing this path is the caller's responsibility (see the attachment
|
|
15
|
+
* gateway); the file is written with an exclusive flag and never overwrites an
|
|
16
|
+
* existing file. When omitted, nothing is written to disk.
|
|
17
|
+
*/
|
|
18
|
+
destination?: string;
|
|
19
|
+
/** Whether and how to embed the bytes in the response. Defaults to `none`. */
|
|
20
|
+
returnContent?: AttachmentContentEncoding;
|
|
21
|
+
/** Maximum bytes to embed inline when returnContent is not `none`. */
|
|
22
|
+
maxInlineBytes?: number;
|
|
23
|
+
/** Hard cap on the number of downloaded bytes. Exceeding it aborts the download. */
|
|
24
|
+
maxDownloadBytes?: number;
|
|
25
|
+
}
|
|
26
|
+
export interface AttachmentDownloadResult {
|
|
27
|
+
filename: string;
|
|
28
|
+
mediaType?: string;
|
|
29
|
+
/** Number of bytes downloaded. */
|
|
30
|
+
size: number;
|
|
31
|
+
/** Absolute path the file was written to, when saving was requested. */
|
|
32
|
+
savedPath?: string;
|
|
33
|
+
/** Inline content, when returnContent is `base64` or `text`. */
|
|
34
|
+
content?: string;
|
|
35
|
+
encoding?: 'base64' | 'text';
|
|
36
|
+
/** Set when inline content was requested but omitted (e.g. over the size cap). */
|
|
37
|
+
contentOmittedReason?: string;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Downloads a file from an authenticated Atlassian Data Center URL using a
|
|
41
|
+
* Bearer token, optionally writing it to a pre-validated destination and/or
|
|
42
|
+
* returning its bytes inline.
|
|
43
|
+
*
|
|
44
|
+
* Writing uses an exclusive flag, so an existing file (including a symlink) is
|
|
45
|
+
* never overwritten.
|
|
46
|
+
*/
|
|
47
|
+
export declare function downloadAttachment(params: {
|
|
48
|
+
url: string;
|
|
49
|
+
token: string | (() => string | undefined);
|
|
50
|
+
filename: string;
|
|
51
|
+
mediaType?: string;
|
|
52
|
+
options?: AttachmentDownloadOptions;
|
|
53
|
+
}): Promise<AttachmentDownloadResult>;
|
|
54
|
+
//# sourceMappingURL=attachment-download.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"attachment-download.d.ts","sourceRoot":"","sources":["../src/attachment-download.ts"],"names":[],"mappings":"AAGA;;;;;;GAMG;AACH,MAAM,MAAM,yBAAyB,GAAG,MAAM,GAAG,QAAQ,GAAG,MAAM,CAAC;AAEnE,yEAAyE;AACzE,eAAO,MAAM,wBAAwB,UAAY,CAAC;AAElD,MAAM,WAAW,yBAAyB;IACxC;;;;;OAKG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,8EAA8E;IAC9E,aAAa,CAAC,EAAE,yBAAyB,CAAC;IAC1C,sEAAsE;IACtE,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,oFAAoF;IACpF,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED,MAAM,WAAW,wBAAwB;IACvC,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,kCAAkC;IAClC,IAAI,EAAE,MAAM,CAAC;IACb,wEAAwE;IACxE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,gEAAgE;IAChE,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,QAAQ,GAAG,MAAM,CAAC;IAC7B,kFAAkF;IAClF,oBAAoB,CAAC,EAAE,MAAM,CAAC;CAC/B;AA6CD;;;;;;;GAOG;AACH,wBAAsB,kBAAkB,CAAC,MAAM,EAAE;IAC/C,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,GAAG,CAAC,MAAM,MAAM,GAAG,SAAS,CAAC,CAAC;IAC3C,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,yBAAyB,CAAC;CACrC,GAAG,OAAO,CAAC,wBAAwB,CAAC,CAmDpC"}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { writeFile } from 'node:fs/promises';
|
|
2
|
+
import { basename } from 'node:path';
|
|
3
|
+
/** Default cap for inline content so responses do not balloon. 1 MiB. */
|
|
4
|
+
export const DEFAULT_MAX_INLINE_BYTES = 1_048_576;
|
|
5
|
+
async function resolveToken(token) {
|
|
6
|
+
const resolved = typeof token === 'function' ? token() : token;
|
|
7
|
+
if (!resolved) {
|
|
8
|
+
throw new Error('Missing API token for attachment download');
|
|
9
|
+
}
|
|
10
|
+
return resolved;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Reads the response body, enforcing `cap` (when set) without buffering more than
|
|
14
|
+
* the limit. Streams chunk-by-chunk when a web ReadableStream is available and
|
|
15
|
+
* falls back to a buffered read with a post-check otherwise.
|
|
16
|
+
*/
|
|
17
|
+
async function readBodyWithCap(response, cap, filename) {
|
|
18
|
+
const body = response.body;
|
|
19
|
+
if (cap !== undefined && body && typeof body.getReader === 'function') {
|
|
20
|
+
const reader = body.getReader();
|
|
21
|
+
const chunks = [];
|
|
22
|
+
let total = 0;
|
|
23
|
+
for (;;) {
|
|
24
|
+
const { done, value } = await reader.read();
|
|
25
|
+
if (done) {
|
|
26
|
+
break;
|
|
27
|
+
}
|
|
28
|
+
total += value.byteLength;
|
|
29
|
+
if (total > cap) {
|
|
30
|
+
await reader.cancel();
|
|
31
|
+
throw new Error(`Attachment "${filename}" exceeds the configured download limit of ${cap} bytes`);
|
|
32
|
+
}
|
|
33
|
+
chunks.push(Buffer.from(value));
|
|
34
|
+
}
|
|
35
|
+
return Buffer.concat(chunks);
|
|
36
|
+
}
|
|
37
|
+
const buffer = Buffer.from(await response.arrayBuffer());
|
|
38
|
+
if (cap !== undefined && buffer.length > cap) {
|
|
39
|
+
throw new Error(`Attachment "${filename}" size ${buffer.length} bytes exceeds the configured download limit of ${cap} bytes`);
|
|
40
|
+
}
|
|
41
|
+
return buffer;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Downloads a file from an authenticated Atlassian Data Center URL using a
|
|
45
|
+
* Bearer token, optionally writing it to a pre-validated destination and/or
|
|
46
|
+
* returning its bytes inline.
|
|
47
|
+
*
|
|
48
|
+
* Writing uses an exclusive flag, so an existing file (including a symlink) is
|
|
49
|
+
* never overwritten.
|
|
50
|
+
*/
|
|
51
|
+
export async function downloadAttachment(params) {
|
|
52
|
+
const { url, filename, mediaType } = params;
|
|
53
|
+
const options = params.options ?? {};
|
|
54
|
+
const token = await resolveToken(params.token);
|
|
55
|
+
const response = await fetch(url, {
|
|
56
|
+
headers: {
|
|
57
|
+
Authorization: `Bearer ${token}`,
|
|
58
|
+
// Attachment download endpoints are XSRF-protected; nocheck bypasses it.
|
|
59
|
+
'X-Atlassian-Token': 'nocheck',
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
if (!response.ok) {
|
|
63
|
+
throw new Error(`Failed to download attachment "${filename}": ${response.status} ${response.statusText}`);
|
|
64
|
+
}
|
|
65
|
+
const cap = options.maxDownloadBytes;
|
|
66
|
+
const declaredLength = Number(response.headers.get('content-length'));
|
|
67
|
+
if (cap !== undefined && Number.isFinite(declaredLength) && declaredLength > cap) {
|
|
68
|
+
throw new Error(`Attachment "${filename}" size ${declaredLength} bytes exceeds the configured download limit of ${cap} bytes`);
|
|
69
|
+
}
|
|
70
|
+
const buffer = await readBodyWithCap(response, cap, filename);
|
|
71
|
+
const resolvedMediaType = mediaType ?? response.headers.get('content-type') ?? undefined;
|
|
72
|
+
const result = {
|
|
73
|
+
filename: basename(filename),
|
|
74
|
+
mediaType: resolvedMediaType,
|
|
75
|
+
size: buffer.length,
|
|
76
|
+
};
|
|
77
|
+
if (options.destination) {
|
|
78
|
+
await writeFile(options.destination, buffer, { flag: 'wx' });
|
|
79
|
+
result.savedPath = options.destination;
|
|
80
|
+
}
|
|
81
|
+
const returnContent = options.returnContent ?? 'none';
|
|
82
|
+
if (returnContent !== 'none') {
|
|
83
|
+
const inlineCap = options.maxInlineBytes ?? DEFAULT_MAX_INLINE_BYTES;
|
|
84
|
+
if (buffer.length > inlineCap) {
|
|
85
|
+
result.contentOmittedReason = `File size ${buffer.length} bytes exceeds inline cap of ${inlineCap} bytes`;
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
result.content = returnContent === 'base64' ? buffer.toString('base64') : buffer.toString('utf-8');
|
|
89
|
+
result.encoding = returnContent;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return result;
|
|
93
|
+
}
|
|
94
|
+
//# sourceMappingURL=attachment-download.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"attachment-download.js","sourceRoot":"","sources":["../src/attachment-download.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { ProductDefinition } from './config/source.js';
|
|
2
|
+
/**
|
|
3
|
+
* Operator-controlled gateway that lets the attachment tools read from / write to
|
|
4
|
+
* the local filesystem. It is disabled by default: filesystem access must be
|
|
5
|
+
* explicitly enabled and confined to canonical root directories that the model
|
|
6
|
+
* cannot choose. Configured entirely through environment variables named after
|
|
7
|
+
* the product (e.g. JIRA_ATTACHMENTS_*).
|
|
8
|
+
*/
|
|
9
|
+
export declare const DEFAULT_MAX_ATTACHMENT_BYTES: number;
|
|
10
|
+
export interface AttachmentGatewaySide {
|
|
11
|
+
/** Whether this direction (upload/download) is enabled with a valid root. */
|
|
12
|
+
enabled: boolean;
|
|
13
|
+
/** Canonical (realpath-resolved) absolute root directories. */
|
|
14
|
+
roots: string[];
|
|
15
|
+
/** Hard byte limit for a single file in this direction. */
|
|
16
|
+
maxBytes: number;
|
|
17
|
+
}
|
|
18
|
+
export interface AttachmentGateway {
|
|
19
|
+
upload: AttachmentGatewaySide;
|
|
20
|
+
download: AttachmentGatewaySide;
|
|
21
|
+
}
|
|
22
|
+
type Env = Record<string, string | undefined>;
|
|
23
|
+
type Warn = (message: string) => void;
|
|
24
|
+
/**
|
|
25
|
+
* Reads the attachment gateway configuration for a product from the environment.
|
|
26
|
+
* Roots are canonicalized once here; a direction is only reported as enabled when
|
|
27
|
+
* its flag is set and at least one valid root resolves.
|
|
28
|
+
*/
|
|
29
|
+
export declare function resolveAttachmentGateway(product: ProductDefinition, options?: {
|
|
30
|
+
env?: Env;
|
|
31
|
+
warn?: Warn;
|
|
32
|
+
}): AttachmentGateway;
|
|
33
|
+
/**
|
|
34
|
+
* Resolves and validates a file to upload: it must live inside an allowed upload
|
|
35
|
+
* root, be a regular file (never a symlink or special file), and be within the
|
|
36
|
+
* size limit. Returns the absolute path to read.
|
|
37
|
+
*/
|
|
38
|
+
export declare function resolveUploadSource(params: {
|
|
39
|
+
requestedPath: string;
|
|
40
|
+
side: AttachmentGatewaySide;
|
|
41
|
+
}): Promise<{
|
|
42
|
+
absolutePath: string;
|
|
43
|
+
size: number;
|
|
44
|
+
}>;
|
|
45
|
+
/**
|
|
46
|
+
* Resolves a destination path to save a downloaded attachment into. The file is
|
|
47
|
+
* placed in the first configured download root; the caller writes with an
|
|
48
|
+
* exclusive flag so existing files (including symlinks) are never overwritten.
|
|
49
|
+
*/
|
|
50
|
+
export declare function resolveDownloadDestination(params: {
|
|
51
|
+
requestedName: string;
|
|
52
|
+
side: AttachmentGatewaySide;
|
|
53
|
+
}): Promise<string>;
|
|
54
|
+
/** Resolves a path within roots, throwing if it escapes. Exported for verification/tests. */
|
|
55
|
+
export declare function assertWithinRoots(requested: string, roots: string[]): Promise<string>;
|
|
56
|
+
export {};
|
|
57
|
+
//# sourceMappingURL=attachment-gateway.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"attachment-gateway.d.ts","sourceRoot":"","sources":["../src/attachment-gateway.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAE5D;;;;;;GAMG;AACH,eAAO,MAAM,4BAA4B,QAAmB,CAAC;AAE7D,MAAM,WAAW,qBAAqB;IACpC,6EAA6E;IAC7E,OAAO,EAAE,OAAO,CAAC;IACjB,+DAA+D;IAC/D,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,2DAA2D;IAC3D,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,iBAAiB;IAChC,MAAM,EAAE,qBAAqB,CAAC;IAC9B,QAAQ,EAAE,qBAAqB,CAAC;CACjC;AAED,KAAK,GAAG,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC;AAC9C,KAAK,IAAI,GAAG,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;AA8EtC;;;;GAIG;AACH,wBAAgB,wBAAwB,CACtC,OAAO,EAAE,iBAAiB,EAC1B,OAAO,CAAC,EAAE;IAAE,GAAG,CAAC,EAAE,GAAG,CAAC;IAAC,IAAI,CAAC,EAAE,IAAI,CAAA;CAAE,GACnC,iBAAiB,CA2BnB;AA8DD;;;;GAIG;AACH,wBAAsB,mBAAmB,CAAC,MAAM,EAAE;IAChD,aAAa,EAAE,MAAM,CAAC;IACtB,IAAI,EAAE,qBAAqB,CAAC;CAC7B,GAAG,OAAO,CAAC;IAAE,YAAY,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC,CAkBlD;AAED;;;;GAIG;AACH,wBAAsB,0BAA0B,CAAC,MAAM,EAAE;IACvD,aAAa,EAAE,MAAM,CAAC;IACtB,IAAI,EAAE,qBAAqB,CAAC;CAC7B,GAAG,OAAO,CAAC,MAAM,CAAC,CASlB;AAED,6FAA6F;AAC7F,wBAAsB,iBAAiB,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAE3F"}
|