@haneinfo/attachments 0.0.0-stage → 0.1.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/LICENSE +14 -0
- package/README.md +75 -2
- package/agent-guide.md +86 -0
- package/dist/cleanup.worker.d.ts +13 -0
- package/dist/cleanup.worker.d.ts.map +1 -0
- package/dist/cleanup.worker.js +57 -0
- package/dist/cleanup.worker.js.map +1 -0
- package/dist/config.d.ts +38 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +74 -0
- package/dist/config.js.map +1 -0
- package/dist/controller.d.ts +45 -0
- package/dist/controller.d.ts.map +1 -0
- package/dist/controller.js +214 -0
- package/dist/controller.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +24 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp-tools.d.ts +10 -0
- package/dist/mcp-tools.d.ts.map +1 -0
- package/dist/mcp-tools.js +74 -0
- package/dist/mcp-tools.js.map +1 -0
- package/dist/module.d.ts +6 -0
- package/dist/module.d.ts.map +1 -0
- package/dist/module.js +54 -0
- package/dist/module.js.map +1 -0
- package/dist/plugin.d.ts +44 -0
- package/dist/plugin.d.ts.map +1 -0
- package/dist/plugin.js +90 -0
- package/dist/plugin.js.map +1 -0
- package/dist/service.d.ts +123 -0
- package/dist/service.d.ts.map +1 -0
- package/dist/service.js +384 -0
- package/dist/service.js.map +1 -0
- package/dist/storage.d.ts +54 -0
- package/dist/storage.d.ts.map +1 -0
- package/dist/storage.js +239 -0
- package/dist/storage.js.map +1 -0
- package/dist/store.d.ts +61 -0
- package/dist/store.d.ts.map +1 -0
- package/dist/store.js +25 -0
- package/dist/store.js.map +1 -0
- package/dist/validation.d.ts +29 -0
- package/dist/validation.d.ts.map +1 -0
- package/dist/validation.js +297 -0
- package/dist/validation.js.map +1 -0
- package/haneinfo.plugin.json +72 -0
- package/package.json +80 -3
- package/plugin.d.ts +2 -0
- package/plugin.js +4 -0
- package/prisma/attachments.prisma +32 -0
- package/prisma/attachments.sql +13 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
Copyright (c) 2026 Haneinfo. All Rights Reserved.
|
|
2
|
+
|
|
3
|
+
This software and associated documentation files (the "Software") are the proprietary and
|
|
4
|
+
confidential property of Haneinfo. No part of the Software may be copied, modified, merged,
|
|
5
|
+
published, distributed, sublicensed, or sold without the prior written permission of Haneinfo.
|
|
6
|
+
|
|
7
|
+
The Software is provided for internal use by Haneinfo and its authorized personnel only, and is
|
|
8
|
+
NOT licensed for external distribution or open-source use.
|
|
9
|
+
|
|
10
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
|
|
11
|
+
NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
12
|
+
NONINFRINGEMENT. IN NO EVENT SHALL HANEINFO BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
|
13
|
+
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH
|
|
14
|
+
THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,76 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @haneinfo/attachments
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Private immutable files attached to StateMachine instances. Implements draft decision 202610-5f3b;
|
|
4
|
+
the decision remains draft. Package version stays 0.0.0 with a minor changeset for first release 0.1.0.
|
|
5
|
+
Use `attachments()` from `@haneinfo/attachments/plugin` in the existing plugin inventory. See
|
|
6
|
+
[agent-guide.md](agent-guide.md) for adoption and operations.
|
|
7
|
+
|
|
8
|
+
Apply `prisma/attachments.prisma` and `prisma/attachments.sql` in an App migration. The App must
|
|
9
|
+
add `attachments Attachment[]` to its StateMachineInstance model. This relation has the same
|
|
10
|
+
Cascade policy as instance readers. No core fragment depends on installing this optional package.
|
|
11
|
+
Configure `runtime.attachments` explicitly; missing drivers fail startup:
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
attachments: {
|
|
15
|
+
driver: 'local', localRoot: '/var/lib/app/attachments',
|
|
16
|
+
maxFileBytes: 26214400, maxAttachmentsPerInstance: 20,
|
|
17
|
+
deletedRetentionDays: 7, cleanupWorkerEnabled: true,
|
|
18
|
+
}
|
|
19
|
+
// Or: { driver: 's3', s3: { bucket: 'private-files', region: 'us-east-1',
|
|
20
|
+
// endpoint: 'https://s3.example.test', forcePathStyle: true } }
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
The module exports FILE_STORAGE, AttachmentsService and AttachmentsMcpTools. Local storage validates
|
|
24
|
+
opaque keys and refuses link components. S3 dynamically loads optional peers `@aws-sdk/client-s3`
|
|
25
|
+
and `@aws-sdk/lib-storage` (^3.1154.0). Install both to select S3; a missing SDK fails startup with an
|
|
26
|
+
explicit error. Set ATTACHMENTS_S3_ACCESS_KEY_ID, ATTACHMENTS_S3_SECRET_ACCESS_KEY and optionally
|
|
27
|
+
ATTACHMENTS_S3_SESSION_TOKEN in the environment only. The manifest marks all three secret. Config
|
|
28
|
+
rejects credential fields. The S3 client uses only these credentials, never a default credential chain.
|
|
29
|
+
|
|
30
|
+
Uploads use Busboy 1.6.0 (already present through Nest's Express adapter) as a direct dependency to
|
|
31
|
+
avoid depending on transitive hoisting. Incoming bytes stream through a size limiter, SHA-256 and
|
|
32
|
+
UTF-8/prefix validator into private temporary files; bounded ZIP metadata/content-types reads verify
|
|
33
|
+
OOXML without extracting the document. A second stream sends the validated file to private storage.
|
|
34
|
+
No production path reads a whole file into a Buffer. Files are not publicly addressable.
|
|
35
|
+
|
|
36
|
+
The default allowlist is pdf, png, jpeg (jpg accepted), webp, gif, docx, xlsx, pptx, csv, txt. App config
|
|
37
|
+
can only narrow/reorder these types; active HTML/SVG/script/executable types cannot be enabled.
|
|
38
|
+
Names lose path components and controls, normalize to NFC and are limited to 180 Unicode characters.
|
|
39
|
+
Known dangerous or supported preceding extensions are refused. CSV/txt require valid UTF-8 with no
|
|
40
|
+
NUL, active HTML/SVG/script markup or executable signature. Downloads are always attachments with
|
|
41
|
+
RFC 5987 filename encoding, nosniff, no-store, detected MIME and exact Content-Length.
|
|
42
|
+
|
|
43
|
+
All five HTTP routes require InteractiveAuthGuard under `/state-machine-instances/:id/attachments`:
|
|
44
|
+
|
|
45
|
+
| Method/path | Behavior |
|
|
46
|
+
| --- | --- |
|
|
47
|
+
| GET / | List visible metadata, uploadedByName and backend canDelete |
|
|
48
|
+
| POST / | Exactly one multipart field named file, no other fields |
|
|
49
|
+
| GET /:attachmentId | One visible metadata item |
|
|
50
|
+
| GET /:attachmentId/content | Reauthorize and stream READY content |
|
|
51
|
+
| DELETE /:attachmentId | Idempotent soft deletion |
|
|
52
|
+
|
|
53
|
+
Readable instances use the shared state-machine access helper: submitter, effective instance:read,
|
|
54
|
+
or matching USER/ROLE READ/WRITE. Check readership first. Upload additionally requires submitter,
|
|
55
|
+
effective instance:write or matching WRITE. Delete requires uploader or effective instance:admin
|
|
56
|
+
after readership. Frozen/final instances accept files. No new permissions are introduced. History
|
|
57
|
+
records ATTACHMENT with decision.attachment (action, id, plain-text name); no storage key or notice.
|
|
58
|
+
|
|
59
|
+
Register `AttachmentsMcpTools.registerInto(registry, { scopeNamespace })`. The sole tool is
|
|
60
|
+
`list_instance_attachments`, requiring `<namespace>:instance:read` and a bound user. It returns only
|
|
61
|
+
id, name, size, detected type, status, uploader and time. Names use `$userContent`: data, never
|
|
62
|
+
instructions. No content, upload, delete or public URL tool exists.
|
|
63
|
+
|
|
64
|
+
| Error | HTTP |
|
|
65
|
+
| --- | ---: |
|
|
66
|
+
| INSTANCE_NOT_FOUND | 404 (missing or unreadable instance, identical message) |
|
|
67
|
+
| ATTACHMENT_NOT_FOUND | 404 (missing, deleted or non-READY download) |
|
|
68
|
+
| FORBIDDEN | 403 (readable but cannot upload/delete) |
|
|
69
|
+
| ATTACHMENT_TOO_LARGE | 413 |
|
|
70
|
+
| ATTACHMENT_LIMIT_EXCEEDED | 409 |
|
|
71
|
+
| ATTACHMENT_TYPE_NOT_ALLOWED / ATTACHMENT_TYPE_MISMATCH | 400 |
|
|
72
|
+
| ATTACHMENT_NAME_INVALID / ATTACHMENT_MULTIPART_INVALID / ATTACHMENT_UPLOAD_ABORTED | 400 |
|
|
73
|
+
|
|
74
|
+
Filename errors never echo the user's filename. HTTP metadata is plain text and must be rendered
|
|
75
|
+
with text nodes. Declared MIME is retained for audit, never used for downloads. See the guide for
|
|
76
|
+
scan recovery, cleanup limits and coordinated database/storage backups.
|
package/agent-guide.md
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# Attachments agent guide
|
|
2
|
+
|
|
3
|
+
Use this package for private immutable instance attachments. Draft decision 202610-5f3b remains draft.
|
|
4
|
+
Do not use it for exports, inline previews, versions, public links, folders or frontend panels.
|
|
5
|
+
It has no definition kind or adapters; configure the runtime rather than create a definition.
|
|
6
|
+
The HTTP, MCP, errors, storage configuration and authorization contract are in [README.md](README.md).
|
|
7
|
+
|
|
8
|
+
## Adoption
|
|
9
|
+
|
|
10
|
+
Upgrade plugin-api (new contract 1.15.0), state-machine and collaboration together. Include the new
|
|
11
|
+
fragment, add the reverse `attachments Attachment[]` relation in the App, and deploy the supplied SQL
|
|
12
|
+
as an ordinary migration before starting the plugin. No instance/history columns change. Keep the
|
|
13
|
+
App's existing interactive authentication, RBAC and background-jobs composition. The plugin owns
|
|
14
|
+
Attachment and AttachmentGarbage only and uses the shared instanceAccessAllowed helper.
|
|
15
|
+
|
|
16
|
+
## Configuration
|
|
17
|
+
|
|
18
|
+
All options are under `runtime.attachments`, validated identically by direct dynamic-module registration.
|
|
19
|
+
|
|
20
|
+
| Option | Default / meaning |
|
|
21
|
+
| --- | --- |
|
|
22
|
+
| driver | Required: local or s3 |
|
|
23
|
+
| localRoot | Required for local; trusted private persistent directory |
|
|
24
|
+
| s3.bucket / s3.region | Required for s3 |
|
|
25
|
+
| s3.endpoint / s3.forcePathStyle | Optional URL / false |
|
|
26
|
+
| maxFileBytes | 26214400; maximum Number.MAX_SAFE_INTEGER, persisted as BigInt |
|
|
27
|
+
| maxAttachmentsPerInstance | 20; maximum 1000 |
|
|
28
|
+
| allowedTypes | pdf, png, jpeg, webp, gif, docx, xlsx, pptx, csv, txt |
|
|
29
|
+
| deletedRetentionDays | 7; nonnegative integer |
|
|
30
|
+
| pendingScanTimeoutMs | 900000; expired pending files fail closed as REJECTED |
|
|
31
|
+
| maxUploadMs | 300000; maximum 1800000, covering envelope, staging and storage |
|
|
32
|
+
| cleanupBatchSize | 100; maximum 500 per metadata/garbage/staging batch |
|
|
33
|
+
| cleanupPollIntervalMs | 60000 |
|
|
34
|
+
| cleanupWorkerEnabled | true; startup check and periodic cleanup enqueueing |
|
|
35
|
+
|
|
36
|
+
To drive jobs manually in tests, disable cleanupWorkerEnabled and the existing background-jobs
|
|
37
|
+
worker (`BACKGROUND_JOBS_WORKER_ENABLED=false` in Apps that map that environment setting). The
|
|
38
|
+
producer never reclaims RUNNING jobs automatically. Existing background-jobs retry and recovery
|
|
39
|
+
rules apply. Each replica may enqueue cleanup; object deletion and record purging are idempotent.
|
|
40
|
+
|
|
41
|
+
## Optional scanning
|
|
42
|
+
|
|
43
|
+
Provide ATTACHMENT_SCANNER / haneinfo.attachment-scanner using AttachmentScannerPort. There is no
|
|
44
|
+
built-in scanner. New records are PENDING_SCAN until the registered `attachments.scan` job consumes
|
|
45
|
+
a storage stream and returns `{ status: 'CLEAN' }` or `{ status: 'REJECTED', reason }`. CLEAN sets
|
|
46
|
+
READY; rejection deletes content and persists REJECTED. Reasons are not exposed or copied to history.
|
|
47
|
+
The supplied stream is destroyed after each attempt, including failures/cancellation. Exceptions
|
|
48
|
+
leave PENDING_SCAN and use background-jobs retry. Missing content after a failed rejection commit
|
|
49
|
+
is rejected on retry. Non-READY downloads and deleted downloads uniformly return 404.
|
|
50
|
+
|
|
51
|
+
Cleanup recovers missing/failed scan enqueueing, skips active scan jobs, and rejects pending files
|
|
52
|
+
older than pendingScanTimeoutMs, deleting their content. Removing a scanner never silently approves
|
|
53
|
+
existing pending files. Rejected metadata is retained until its createdAt exceeds the deleted
|
|
54
|
+
retention period; its content is removed immediately. No scan timeout makes a file downloadable.
|
|
55
|
+
|
|
56
|
+
## Operations and consistency
|
|
57
|
+
|
|
58
|
+
Back up and restore both PostgreSQL and the configured storage backend as one coordinated set.
|
|
59
|
+
Storage keys are random `attachments/<UTC year>/<month>/<uuid>`; never derive them from names or
|
|
60
|
+
accept user-specified keys. Preserve local directory ownership/private permissions, or a private
|
|
61
|
+
S3 bucket. Configure S3 lifecycle abortion of incomplete multipart uploads for process crashes.
|
|
62
|
+
The SDK aborts failed live uploads, uses queueSize=1 and a 5 MiB part buffer.
|
|
63
|
+
|
|
64
|
+
Authorization/count preflight happens before consuming multipart bytes. Counts are checked again
|
|
65
|
+
under the existing instance lock at commit, so concurrent uploads cannot exceed the cap. A durable
|
|
66
|
+
AttachmentGarbage marker precedes every storage write. Metadata, history and marker removal commit
|
|
67
|
+
together. A failed DB write deletes the object; failed deletion leaves its marker for cleanup after
|
|
68
|
+
one hour. Ambiguous commits check for a referenced object before deletion and leave recovery to
|
|
69
|
+
cleanup when the DB is unavailable. Garbage cleanup also refuses to delete referenced objects.
|
|
70
|
+
|
|
71
|
+
Temporary staging files live under the OS temp directory's haneinfo-attachments-staging folder,
|
|
72
|
+
with random UUID names and private modes. Normal failures and client aborts remove partial files.
|
|
73
|
+
Startup/periodic cleanup reclaims stale staging files after one hour, exceeding the maximum upload
|
|
74
|
+
deadline. Run replicas with separate local temp directories; remote/shared staging is unsupported.
|
|
75
|
+
Cleanup removes content first and then DB metadata, making retries safe if either step fails.
|
|
76
|
+
Soft deletion hides records immediately; retained files must never be served outside this API.
|
|
77
|
+
|
|
78
|
+
OOXML checks use bounded central-directory metadata (128 KiB, 2000 entries), local headers, required
|
|
79
|
+
main-part names and at most 64 KiB of decompressed content-types XML. ZIP64, encrypted/macro-enabled
|
|
80
|
+
archives, oversized metadata and malformed containers are refused conservatively. Magic detection
|
|
81
|
+
is a type gate, not malware detection or complete format validation; install a scanner when needed.
|
|
82
|
+
|
|
83
|
+
For common errors, use the README table: adjust limits/allowlists, fix file extensions/content, or
|
|
84
|
+
ask the instance owner for access. Never ask an agent to upload/delete through MCP; a person uses
|
|
85
|
+
the authenticated App. Related recipe: the equipment-loan attachments validation overlay in the
|
|
86
|
+
package repository demonstrates migration, private local storage and read-only MCP registration.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type OnApplicationBootstrap, type OnModuleDestroy } from '@nestjs/common';
|
|
2
|
+
import { AttachmentsService } from './service';
|
|
3
|
+
export declare class AttachmentsCleanupWorker implements OnApplicationBootstrap, OnModuleDestroy {
|
|
4
|
+
private readonly attachments;
|
|
5
|
+
private timer?;
|
|
6
|
+
private pending?;
|
|
7
|
+
private readonly logger;
|
|
8
|
+
constructor(attachments: AttachmentsService);
|
|
9
|
+
onApplicationBootstrap(): void;
|
|
10
|
+
tick(): Promise<void>;
|
|
11
|
+
onModuleDestroy(): Promise<void>;
|
|
12
|
+
}
|
|
13
|
+
//# sourceMappingURL=cleanup.worker.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cleanup.worker.d.ts","sourceRoot":"","sources":["../src/cleanup.worker.ts"],"names":[],"mappings":"AAAA,OAAO,EAIL,KAAK,sBAAsB,EAC3B,KAAK,eAAe,EACrB,MAAM,gBAAgB,CAAC;AACxB,OAAO,EAAE,kBAAkB,EAAE,MAAM,WAAW,CAAC;AAE/C,qBACa,wBAAyB,YAAW,sBAAsB,EAAE,eAAe;IAI9C,OAAO,CAAC,QAAQ,CAAC,WAAW;IAHpE,OAAO,CAAC,KAAK,CAAC,CAAiC;IAC/C,OAAO,CAAC,OAAO,CAAC,CAAmB;IACnC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAA0C;gBACR,WAAW,EAAE,kBAAkB;IACxF,sBAAsB;IAQhB,IAAI;IAWJ,eAAe;CAItB"}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
5
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
6
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
+
};
|
|
8
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
9
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
10
|
+
};
|
|
11
|
+
var __param = (this && this.__param) || function (paramIndex, decorator) {
|
|
12
|
+
return function (target, key) { decorator(target, key, paramIndex); }
|
|
13
|
+
};
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.AttachmentsCleanupWorker = void 0;
|
|
16
|
+
const common_1 = require("@nestjs/common");
|
|
17
|
+
const service_1 = require("./service");
|
|
18
|
+
let AttachmentsCleanupWorker = class AttachmentsCleanupWorker {
|
|
19
|
+
constructor(attachments) {
|
|
20
|
+
this.attachments = attachments;
|
|
21
|
+
this.logger = new common_1.Logger('AttachmentsCleanupWorker');
|
|
22
|
+
}
|
|
23
|
+
onApplicationBootstrap() {
|
|
24
|
+
if (!this.attachments.config.cleanupWorkerEnabled)
|
|
25
|
+
return;
|
|
26
|
+
void this.tick();
|
|
27
|
+
this.timer = setInterval(() => {
|
|
28
|
+
void this.tick();
|
|
29
|
+
}, this.attachments.config.cleanupPollIntervalMs);
|
|
30
|
+
this.timer.unref?.();
|
|
31
|
+
}
|
|
32
|
+
async tick() {
|
|
33
|
+
if (this.pending || !this.attachments.config.cleanupWorkerEnabled)
|
|
34
|
+
return;
|
|
35
|
+
this.pending = this.attachments
|
|
36
|
+
.enqueueCleanup()
|
|
37
|
+
.catch(() => this.logger.error('Could not enqueue attachment cleanup'));
|
|
38
|
+
try {
|
|
39
|
+
await this.pending;
|
|
40
|
+
}
|
|
41
|
+
finally {
|
|
42
|
+
this.pending = undefined;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
async onModuleDestroy() {
|
|
46
|
+
if (this.timer)
|
|
47
|
+
clearInterval(this.timer);
|
|
48
|
+
await this.pending;
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
exports.AttachmentsCleanupWorker = AttachmentsCleanupWorker;
|
|
52
|
+
exports.AttachmentsCleanupWorker = AttachmentsCleanupWorker = __decorate([
|
|
53
|
+
(0, common_1.Injectable)(),
|
|
54
|
+
__param(0, (0, common_1.Inject)(service_1.AttachmentsService)),
|
|
55
|
+
__metadata("design:paramtypes", [service_1.AttachmentsService])
|
|
56
|
+
], AttachmentsCleanupWorker);
|
|
57
|
+
//# sourceMappingURL=cleanup.worker.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cleanup.worker.js","sourceRoot":"","sources":["../src/cleanup.worker.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;AAAA,2CAMwB;AACxB,uCAA+C;AAGxC,IAAM,wBAAwB,GAA9B,MAAM,wBAAwB;IAInC,YAAwC,WAAgD;QAA/B,gBAAW,GAAX,WAAW,CAAoB;QADvE,WAAM,GAAG,IAAI,eAAM,CAAC,0BAA0B,CAAC,CAAC;IAC0B,CAAC;IAC5F,sBAAsB;QACpB,IAAI,CAAC,IAAI,CAAC,WAAW,CAAC,MAAM,CAAC,oBAAoB;YAAE,OAAO;QAC1D,KAAK,IAAI,CAAC,IAAI,EAAE,CAAC;QACjB,IAAI,CAAC,KAAK,GAAG,WAAW,CAAC,GAAG,EAAE;YAC5B,KAAK,IAAI,CAAC,IAAI,EAAE,CAAC;QACnB,CAAC,EAAE,IAAI,CAAC,WAAW,CAAC,MAAM,CAAC,qBAAqB,CAAC,CAAC;QAClD,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE,EAAE,CAAC;IACvB,CAAC;IACD,KAAK,CAAC,IAAI;QACR,IAAI,IAAI,CAAC,OAAO,IAAI,CAAC,IAAI,CAAC,WAAW,CAAC,MAAM,CAAC,oBAAoB;YAAE,OAAO;QAC1E,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC,WAAW;aAC5B,cAAc,EAAE;aAChB,KAAK,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,sCAAsC,CAAC,CAAC,CAAC;QAC1E,IAAI,CAAC;YACH,MAAM,IAAI,CAAC,OAAO,CAAC;QACrB,CAAC;gBAAS,CAAC;YACT,IAAI,CAAC,OAAO,GAAG,SAAS,CAAC;QAC3B,CAAC;IACH,CAAC;IACD,KAAK,CAAC,eAAe;QACnB,IAAI,IAAI,CAAC,KAAK;YAAE,aAAa,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAC1C,MAAM,IAAI,CAAC,OAAO,CAAC;IACrB,CAAC;CACF,CAAA;AA5BY,4DAAwB;mCAAxB,wBAAwB;IADpC,IAAA,mBAAU,GAAE;IAKE,WAAA,IAAA,eAAM,EAAC,4BAAkB,CAAC,CAAA;qCAA+B,4BAAkB;GAJ7E,wBAAwB,CA4BpC"}
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export declare const attachmentTypes: readonly ["pdf", "png", "jpeg", "webp", "gif", "docx", "xlsx", "pptx", "csv", "txt"];
|
|
3
|
+
export declare const attachmentsConfigSchema: z.ZodObject<{
|
|
4
|
+
driver: z.ZodEnum<{
|
|
5
|
+
local: "local";
|
|
6
|
+
s3: "s3";
|
|
7
|
+
}>;
|
|
8
|
+
localRoot: z.ZodOptional<z.ZodString>;
|
|
9
|
+
s3: z.ZodOptional<z.ZodObject<{
|
|
10
|
+
endpoint: z.ZodOptional<z.ZodURL>;
|
|
11
|
+
bucket: z.ZodString;
|
|
12
|
+
region: z.ZodString;
|
|
13
|
+
forcePathStyle: z.ZodDefault<z.ZodBoolean>;
|
|
14
|
+
}, z.core.$strict>>;
|
|
15
|
+
maxFileBytes: z.ZodDefault<z.ZodNumber>;
|
|
16
|
+
maxAttachmentsPerInstance: z.ZodDefault<z.ZodNumber>;
|
|
17
|
+
allowedTypes: z.ZodDefault<z.ZodArray<z.ZodEnum<{
|
|
18
|
+
pdf: "pdf";
|
|
19
|
+
png: "png";
|
|
20
|
+
jpeg: "jpeg";
|
|
21
|
+
webp: "webp";
|
|
22
|
+
gif: "gif";
|
|
23
|
+
docx: "docx";
|
|
24
|
+
xlsx: "xlsx";
|
|
25
|
+
pptx: "pptx";
|
|
26
|
+
csv: "csv";
|
|
27
|
+
txt: "txt";
|
|
28
|
+
}>>>;
|
|
29
|
+
deletedRetentionDays: z.ZodDefault<z.ZodNumber>;
|
|
30
|
+
pendingScanTimeoutMs: z.ZodDefault<z.ZodNumber>;
|
|
31
|
+
maxUploadMs: z.ZodDefault<z.ZodNumber>;
|
|
32
|
+
cleanupBatchSize: z.ZodDefault<z.ZodNumber>;
|
|
33
|
+
cleanupPollIntervalMs: z.ZodDefault<z.ZodNumber>;
|
|
34
|
+
cleanupWorkerEnabled: z.ZodDefault<z.ZodBoolean>;
|
|
35
|
+
}, z.core.$strict>;
|
|
36
|
+
export type AttachmentsConfig = z.input<typeof attachmentsConfigSchema>;
|
|
37
|
+
export type ResolvedAttachmentsConfig = z.output<typeof attachmentsConfigSchema>;
|
|
38
|
+
//# sourceMappingURL=config.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,eAAO,MAAM,eAAe,sFAWlB,CAAC;AACX,eAAO,MAAM,uBAAuB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;kBAwDhC,CAAC;AACL,MAAM,MAAM,iBAAiB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,uBAAuB,CAAC,CAAC;AACxE,MAAM,MAAM,yBAAyB,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,uBAAuB,CAAC,CAAC"}
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.attachmentsConfigSchema = exports.attachmentTypes = void 0;
|
|
4
|
+
const zod_1 = require("zod");
|
|
5
|
+
exports.attachmentTypes = [
|
|
6
|
+
'pdf',
|
|
7
|
+
'png',
|
|
8
|
+
'jpeg',
|
|
9
|
+
'webp',
|
|
10
|
+
'gif',
|
|
11
|
+
'docx',
|
|
12
|
+
'xlsx',
|
|
13
|
+
'pptx',
|
|
14
|
+
'csv',
|
|
15
|
+
'txt',
|
|
16
|
+
];
|
|
17
|
+
exports.attachmentsConfigSchema = zod_1.z
|
|
18
|
+
.object({
|
|
19
|
+
driver: zod_1.z.enum(['local', 's3'], {
|
|
20
|
+
error: 'Attachments require an explicit local or s3 storage driver',
|
|
21
|
+
}),
|
|
22
|
+
localRoot: zod_1.z.string().min(1).optional(),
|
|
23
|
+
s3: zod_1.z
|
|
24
|
+
.object({
|
|
25
|
+
endpoint: zod_1.z.url().optional(),
|
|
26
|
+
bucket: zod_1.z.string().min(1),
|
|
27
|
+
region: zod_1.z.string().min(1),
|
|
28
|
+
forcePathStyle: zod_1.z.boolean().default(false),
|
|
29
|
+
})
|
|
30
|
+
.strict()
|
|
31
|
+
.optional(),
|
|
32
|
+
maxFileBytes: zod_1.z
|
|
33
|
+
.number()
|
|
34
|
+
.int()
|
|
35
|
+
.positive()
|
|
36
|
+
.max(Number.MAX_SAFE_INTEGER)
|
|
37
|
+
.default(25 * 1024 * 1024),
|
|
38
|
+
maxAttachmentsPerInstance: zod_1.z.number().int().positive().max(1000).default(20),
|
|
39
|
+
allowedTypes: zod_1.z
|
|
40
|
+
.array(zod_1.z.enum(exports.attachmentTypes))
|
|
41
|
+
.min(1)
|
|
42
|
+
.default([...exports.attachmentTypes]),
|
|
43
|
+
deletedRetentionDays: zod_1.z.number().int().nonnegative().default(7),
|
|
44
|
+
pendingScanTimeoutMs: zod_1.z
|
|
45
|
+
.number()
|
|
46
|
+
.int()
|
|
47
|
+
.positive()
|
|
48
|
+
.default(15 * 60 * 1000),
|
|
49
|
+
maxUploadMs: zod_1.z
|
|
50
|
+
.number()
|
|
51
|
+
.int()
|
|
52
|
+
.positive()
|
|
53
|
+
.max(30 * 60 * 1000)
|
|
54
|
+
.default(5 * 60 * 1000),
|
|
55
|
+
cleanupBatchSize: zod_1.z.number().int().positive().max(500).default(100),
|
|
56
|
+
cleanupPollIntervalMs: zod_1.z.number().int().positive().default(60000),
|
|
57
|
+
cleanupWorkerEnabled: zod_1.z.boolean().default(true),
|
|
58
|
+
})
|
|
59
|
+
.strict()
|
|
60
|
+
.superRefine((config, ctx) => {
|
|
61
|
+
if (config.driver === 'local' && !config.localRoot)
|
|
62
|
+
ctx.addIssue({
|
|
63
|
+
code: 'custom',
|
|
64
|
+
path: ['localRoot'],
|
|
65
|
+
message: 'Local storage requires localRoot',
|
|
66
|
+
});
|
|
67
|
+
if (config.driver === 's3' && !config.s3)
|
|
68
|
+
ctx.addIssue({
|
|
69
|
+
code: 'custom',
|
|
70
|
+
path: ['s3'],
|
|
71
|
+
message: 'S3 storage requires bucket and region',
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
2
|
+
import { type InteractivePrincipal } from '@haneinfo/plugin-api';
|
|
3
|
+
import { AttachmentsService } from './service';
|
|
4
|
+
/** No body interceptor: authorization/count checks happen before the parser consumes bytes. */
|
|
5
|
+
export declare function receiveMultipart(request: IncomingMessage, response: ServerResponse, service: AttachmentsService, userId: string, instanceId: string): Promise<unknown>;
|
|
6
|
+
export declare class AttachmentsController {
|
|
7
|
+
private readonly attachments;
|
|
8
|
+
constructor(attachments: AttachmentsService);
|
|
9
|
+
list(user: InteractivePrincipal, id: string): Promise<{
|
|
10
|
+
items: {
|
|
11
|
+
id: string;
|
|
12
|
+
instanceId: string;
|
|
13
|
+
fileName: string;
|
|
14
|
+
declaredContentType: string;
|
|
15
|
+
detectedContentType: string;
|
|
16
|
+
size: number;
|
|
17
|
+
sha256: string;
|
|
18
|
+
status: "READY" | "PENDING_SCAN" | "REJECTED";
|
|
19
|
+
uploadedBy: string;
|
|
20
|
+
uploadedByName: string | null;
|
|
21
|
+
createdAt: Date;
|
|
22
|
+
canDelete: boolean;
|
|
23
|
+
}[];
|
|
24
|
+
}>;
|
|
25
|
+
get(user: InteractivePrincipal, id: string, attachmentId: string): Promise<{
|
|
26
|
+
id: string;
|
|
27
|
+
instanceId: string;
|
|
28
|
+
fileName: string;
|
|
29
|
+
declaredContentType: string;
|
|
30
|
+
detectedContentType: string;
|
|
31
|
+
size: number;
|
|
32
|
+
sha256: string;
|
|
33
|
+
status: "READY" | "PENDING_SCAN" | "REJECTED";
|
|
34
|
+
uploadedBy: string;
|
|
35
|
+
uploadedByName: string | null;
|
|
36
|
+
createdAt: Date;
|
|
37
|
+
canDelete: boolean;
|
|
38
|
+
}>;
|
|
39
|
+
upload(user: InteractivePrincipal, id: string, request: IncomingMessage, response: ServerResponse): Promise<unknown>;
|
|
40
|
+
content(user: InteractivePrincipal, id: string, attachmentId: string, response: ServerResponse): Promise<void>;
|
|
41
|
+
delete(user: InteractivePrincipal, id: string, attachmentId: string): Promise<{
|
|
42
|
+
deleted: boolean;
|
|
43
|
+
}>;
|
|
44
|
+
}
|
|
45
|
+
//# sourceMappingURL=controller.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"controller.d.ts","sourceRoot":"","sources":["../src/controller.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAEjE,OAAO,EAAE,KAAK,oBAAoB,EAAkB,MAAM,sBAAsB,CAAC;AAKjF,OAAO,EAAE,kBAAkB,EAAE,MAAM,WAAW,CAAC;AAE/C,+FAA+F;AAC/F,wBAAsB,gBAAgB,CACpC,OAAO,EAAE,eAAe,EACxB,QAAQ,EAAE,cAAc,EACxB,OAAO,EAAE,kBAAkB,EAC3B,MAAM,EAAE,MAAM,EACd,UAAU,EAAE,MAAM,oBA+GnB;AAED,qBAEa,qBAAqB;IACQ,OAAO,CAAC,QAAQ,CAAC,WAAW;gBAAX,WAAW,EAAE,kBAAkB;IAExF,IAAI,CAAgB,IAAI,EAAE,oBAAoB,EAAe,EAAE,EAAE,MAAM;;;;;;;;;;;;;;;;IAIvE,GAAG,CACc,IAAI,EAAE,oBAAoB,EAC5B,EAAE,EAAE,MAAM,EACA,YAAY,EAAE,MAAM;;;;;;;;;;;;;;IAK7C,MAAM,CACW,IAAI,EAAE,oBAAoB,EAC5B,EAAE,EAAE,MAAM,EAChB,OAAO,EAAE,eAAe,EACH,QAAQ,EAAE,cAAc;IAKtD,OAAO,CACU,IAAI,EAAE,oBAAoB,EAC5B,EAAE,EAAE,MAAM,EACA,YAAY,EAAE,MAAM,EACpC,QAAQ,EAAE,cAAc;IAkBjC,MAAM,CACW,IAAI,EAAE,oBAAoB,EAC5B,EAAE,EAAE,MAAM,EACA,YAAY,EAAE,MAAM;;;CAI9C"}
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
5
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
6
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
+
};
|
|
8
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
9
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
10
|
+
};
|
|
11
|
+
var __param = (this && this.__param) || function (paramIndex, decorator) {
|
|
12
|
+
return function (target, key) { decorator(target, key, paramIndex); }
|
|
13
|
+
};
|
|
14
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
15
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
16
|
+
};
|
|
17
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
18
|
+
exports.AttachmentsController = void 0;
|
|
19
|
+
exports.receiveMultipart = receiveMultipart;
|
|
20
|
+
const promises_1 = require("node:stream/promises");
|
|
21
|
+
const plugin_api_1 = require("@haneinfo/plugin-api");
|
|
22
|
+
const plugin_host_nest_1 = require("@haneinfo/plugin-host-nest");
|
|
23
|
+
const state_machine_1 = require("@haneinfo/state-machine");
|
|
24
|
+
const common_1 = require("@nestjs/common");
|
|
25
|
+
const busboy_1 = __importDefault(require("busboy"));
|
|
26
|
+
const service_1 = require("./service");
|
|
27
|
+
/** No body interceptor: authorization/count checks happen before the parser consumes bytes. */
|
|
28
|
+
async function receiveMultipart(request, response, service, userId, instanceId) {
|
|
29
|
+
await service.preflight(userId, instanceId);
|
|
30
|
+
const abort = new AbortController();
|
|
31
|
+
let parser;
|
|
32
|
+
try {
|
|
33
|
+
parser = (0, busboy_1.default)({
|
|
34
|
+
headers: request.headers,
|
|
35
|
+
defParamCharset: 'utf8',
|
|
36
|
+
preservePath: true,
|
|
37
|
+
limits: {
|
|
38
|
+
fileSize: service.config.maxFileBytes + 1,
|
|
39
|
+
files: 1,
|
|
40
|
+
fields: 0,
|
|
41
|
+
fieldNameSize: 100,
|
|
42
|
+
headerPairs: 100,
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
throw new plugin_api_1.MechanismError('ATTACHMENT_MULTIPART_INVALID', 'One multipart file field named file is required');
|
|
48
|
+
}
|
|
49
|
+
let count = 0;
|
|
50
|
+
let resolveComplete, rejectComplete;
|
|
51
|
+
const complete = new Promise((resolve, reject) => {
|
|
52
|
+
resolveComplete = resolve;
|
|
53
|
+
rejectComplete = reject;
|
|
54
|
+
});
|
|
55
|
+
void complete.catch(() => { });
|
|
56
|
+
let upload, failure;
|
|
57
|
+
const fail = (error) => {
|
|
58
|
+
if (failure)
|
|
59
|
+
return;
|
|
60
|
+
failure = error;
|
|
61
|
+
rejectComplete(error);
|
|
62
|
+
abort.abort(error);
|
|
63
|
+
request.unpipe(parser);
|
|
64
|
+
request.pause();
|
|
65
|
+
response.setHeader('Connection', 'close');
|
|
66
|
+
parser.destroy(error);
|
|
67
|
+
};
|
|
68
|
+
const invalid = () => fail(new plugin_api_1.MechanismError('ATTACHMENT_MULTIPART_INVALID', 'Exactly one file field and no other fields are allowed'));
|
|
69
|
+
const aborted = () => fail(new plugin_api_1.MechanismError('ATTACHMENT_UPLOAD_ABORTED', 'Upload interrupted', 400));
|
|
70
|
+
const timeout = setTimeout(aborted, service.config.maxUploadMs);
|
|
71
|
+
timeout.unref?.();
|
|
72
|
+
request.once('aborted', aborted);
|
|
73
|
+
request.once('error', aborted);
|
|
74
|
+
const closed = () => {
|
|
75
|
+
if (!response.writableFinished)
|
|
76
|
+
aborted();
|
|
77
|
+
};
|
|
78
|
+
response.once('close', closed);
|
|
79
|
+
parser.on('file', (name, file, info) => {
|
|
80
|
+
count++;
|
|
81
|
+
file.on('error', () => {
|
|
82
|
+
if (!failure)
|
|
83
|
+
fail(new plugin_api_1.MechanismError('ATTACHMENT_MULTIPART_INVALID', 'Malformed multipart upload'));
|
|
84
|
+
});
|
|
85
|
+
if (name !== 'file' || count !== 1) {
|
|
86
|
+
file.resume();
|
|
87
|
+
invalid();
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
file.once('limit', () => fail(new plugin_api_1.MechanismError('ATTACHMENT_TOO_LARGE', 'The file exceeds maxFileBytes', 413)));
|
|
91
|
+
upload = service.uploadFor(userId, instanceId, { fileName: info.filename ?? '', declaredContentType: info.mimeType, stream: file, complete }, abort.signal);
|
|
92
|
+
void upload.catch((error) => fail(error));
|
|
93
|
+
});
|
|
94
|
+
parser.on('field', invalid);
|
|
95
|
+
parser.on('filesLimit', invalid);
|
|
96
|
+
parser.on('fieldsLimit', invalid);
|
|
97
|
+
parser.on('error', (error) => fail(new plugin_api_1.MechanismError('ATTACHMENT_MULTIPART_INVALID', error instanceof plugin_api_1.MechanismError ? error.message : 'Malformed multipart upload')));
|
|
98
|
+
parser.once('close', () => {
|
|
99
|
+
if (!failure && count === 1)
|
|
100
|
+
resolveComplete();
|
|
101
|
+
else if (!failure)
|
|
102
|
+
invalid();
|
|
103
|
+
});
|
|
104
|
+
try {
|
|
105
|
+
request.pipe(parser);
|
|
106
|
+
await complete;
|
|
107
|
+
const result = await upload;
|
|
108
|
+
if (failure)
|
|
109
|
+
throw failure;
|
|
110
|
+
return result;
|
|
111
|
+
}
|
|
112
|
+
catch (error) {
|
|
113
|
+
if (upload)
|
|
114
|
+
await upload.catch(() => { });
|
|
115
|
+
throw failure ?? error;
|
|
116
|
+
}
|
|
117
|
+
finally {
|
|
118
|
+
clearTimeout(timeout);
|
|
119
|
+
request.removeListener('aborted', aborted);
|
|
120
|
+
request.removeListener('error', aborted);
|
|
121
|
+
response.removeListener('close', closed);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
let AttachmentsController = class AttachmentsController {
|
|
125
|
+
constructor(attachments) {
|
|
126
|
+
this.attachments = attachments;
|
|
127
|
+
}
|
|
128
|
+
list(user, id) {
|
|
129
|
+
return (0, state_machine_1.runHttp)(() => this.attachments.listFor(user.sub, id));
|
|
130
|
+
}
|
|
131
|
+
get(user, id, attachmentId) {
|
|
132
|
+
return (0, state_machine_1.runHttp)(() => this.attachments.getFor(user.sub, id, attachmentId));
|
|
133
|
+
}
|
|
134
|
+
upload(user, id, request, response) {
|
|
135
|
+
return (0, state_machine_1.runHttp)(() => receiveMultipart(request, response, this.attachments, user.sub, id));
|
|
136
|
+
}
|
|
137
|
+
content(user, id, attachmentId, response) {
|
|
138
|
+
return (0, state_machine_1.runHttp)(async () => {
|
|
139
|
+
const { stream, headers } = await this.attachments.downloadFor(user.sub, id, attachmentId);
|
|
140
|
+
for (const [key, value] of Object.entries(headers))
|
|
141
|
+
response.setHeader(key, value);
|
|
142
|
+
const close = () => stream.destroy();
|
|
143
|
+
response.once('close', close);
|
|
144
|
+
try {
|
|
145
|
+
await (0, promises_1.pipeline)(stream, response);
|
|
146
|
+
}
|
|
147
|
+
catch (error) {
|
|
148
|
+
if (!response.destroyed)
|
|
149
|
+
throw error;
|
|
150
|
+
}
|
|
151
|
+
finally {
|
|
152
|
+
response.removeListener('close', close);
|
|
153
|
+
stream.destroy();
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
delete(user, id, attachmentId) {
|
|
158
|
+
return (0, state_machine_1.runHttp)(() => this.attachments.deleteFor(user.sub, id, attachmentId));
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
exports.AttachmentsController = AttachmentsController;
|
|
162
|
+
__decorate([
|
|
163
|
+
(0, common_1.Get)(),
|
|
164
|
+
__param(0, (0, plugin_host_nest_1.CurrentUser)()),
|
|
165
|
+
__param(1, (0, common_1.Param)('id')),
|
|
166
|
+
__metadata("design:type", Function),
|
|
167
|
+
__metadata("design:paramtypes", [Object, String]),
|
|
168
|
+
__metadata("design:returntype", void 0)
|
|
169
|
+
], AttachmentsController.prototype, "list", null);
|
|
170
|
+
__decorate([
|
|
171
|
+
(0, common_1.Get)(':attachmentId'),
|
|
172
|
+
__param(0, (0, plugin_host_nest_1.CurrentUser)()),
|
|
173
|
+
__param(1, (0, common_1.Param)('id')),
|
|
174
|
+
__param(2, (0, common_1.Param)('attachmentId')),
|
|
175
|
+
__metadata("design:type", Function),
|
|
176
|
+
__metadata("design:paramtypes", [Object, String, String]),
|
|
177
|
+
__metadata("design:returntype", void 0)
|
|
178
|
+
], AttachmentsController.prototype, "get", null);
|
|
179
|
+
__decorate([
|
|
180
|
+
(0, common_1.Post)(),
|
|
181
|
+
__param(0, (0, plugin_host_nest_1.CurrentUser)()),
|
|
182
|
+
__param(1, (0, common_1.Param)('id')),
|
|
183
|
+
__param(2, (0, common_1.Req)()),
|
|
184
|
+
__param(3, (0, common_1.Res)({ passthrough: true })),
|
|
185
|
+
__metadata("design:type", Function),
|
|
186
|
+
__metadata("design:paramtypes", [Object, String, Function, Function]),
|
|
187
|
+
__metadata("design:returntype", void 0)
|
|
188
|
+
], AttachmentsController.prototype, "upload", null);
|
|
189
|
+
__decorate([
|
|
190
|
+
(0, common_1.Get)(':attachmentId/content'),
|
|
191
|
+
__param(0, (0, plugin_host_nest_1.CurrentUser)()),
|
|
192
|
+
__param(1, (0, common_1.Param)('id')),
|
|
193
|
+
__param(2, (0, common_1.Param)('attachmentId')),
|
|
194
|
+
__param(3, (0, common_1.Res)()),
|
|
195
|
+
__metadata("design:type", Function),
|
|
196
|
+
__metadata("design:paramtypes", [Object, String, String, Function]),
|
|
197
|
+
__metadata("design:returntype", void 0)
|
|
198
|
+
], AttachmentsController.prototype, "content", null);
|
|
199
|
+
__decorate([
|
|
200
|
+
(0, common_1.Delete)(':attachmentId'),
|
|
201
|
+
__param(0, (0, plugin_host_nest_1.CurrentUser)()),
|
|
202
|
+
__param(1, (0, common_1.Param)('id')),
|
|
203
|
+
__param(2, (0, common_1.Param)('attachmentId')),
|
|
204
|
+
__metadata("design:type", Function),
|
|
205
|
+
__metadata("design:paramtypes", [Object, String, String]),
|
|
206
|
+
__metadata("design:returntype", void 0)
|
|
207
|
+
], AttachmentsController.prototype, "delete", null);
|
|
208
|
+
exports.AttachmentsController = AttachmentsController = __decorate([
|
|
209
|
+
(0, common_1.Controller)('state-machine-instances/:id/attachments'),
|
|
210
|
+
(0, common_1.UseGuards)(plugin_host_nest_1.InteractiveAuthGuard),
|
|
211
|
+
__param(0, (0, common_1.Inject)(service_1.AttachmentsService)),
|
|
212
|
+
__metadata("design:paramtypes", [service_1.AttachmentsService])
|
|
213
|
+
], AttachmentsController);
|
|
214
|
+
//# sourceMappingURL=controller.js.map
|