@haneinfo/attachments 0.0.0-stage → 0.2.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 +92 -2
- package/agent-guide.md +93 -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 +53 -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 +76 -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 +132 -0
- package/dist/service.d.ts.map +1 -0
- package/dist/service.js +399 -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,93 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @haneinfo/attachments
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Private immutable files attached to StateMachine instances (decision 202610-5f3b).
|
|
4
|
+
The attachment panel additions implement draft 202610-a7c1; this decision remains draft.
|
|
5
|
+
Package version stays 0.1.0 with a minor changeset for the added quota/authorization contract.
|
|
6
|
+
Use `attachments()` from `@haneinfo/attachments/plugin` in the existing plugin inventory. See
|
|
7
|
+
[agent-guide.md](agent-guide.md) for adoption and operations.
|
|
8
|
+
|
|
9
|
+
Apply `prisma/attachments.prisma` and `prisma/attachments.sql` in an App migration. The App must
|
|
10
|
+
add `attachments Attachment[]` to its StateMachineInstance model. This relation has the same
|
|
11
|
+
Cascade policy as instance readers. No core fragment depends on installing this optional package.
|
|
12
|
+
Configure `runtime.attachments` explicitly; missing drivers fail startup:
|
|
13
|
+
|
|
14
|
+
```ts
|
|
15
|
+
attachments: {
|
|
16
|
+
driver: 'local', localRoot: '/var/lib/app/attachments',
|
|
17
|
+
maxFileBytes: 26214400, maxAttachmentsPerInstance: 20,
|
|
18
|
+
deletedRetentionDays: 7, cleanupWorkerEnabled: true,
|
|
19
|
+
}
|
|
20
|
+
// Or: { driver: 's3', s3: { bucket: 'private-files', region: 'us-east-1',
|
|
21
|
+
// endpoint: 'https://s3.example.test', forcePathStyle: true } }
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
The module exports FILE_STORAGE, AttachmentsService and AttachmentsMcpTools. Local storage validates
|
|
25
|
+
opaque keys and refuses link components. S3 dynamically loads optional peers `@aws-sdk/client-s3`
|
|
26
|
+
and `@aws-sdk/lib-storage` (^3.1154.0). Install both to select S3; a missing SDK fails startup with an
|
|
27
|
+
explicit error. Set ATTACHMENTS_S3_ACCESS_KEY_ID, ATTACHMENTS_S3_SECRET_ACCESS_KEY and optionally
|
|
28
|
+
ATTACHMENTS_S3_SESSION_TOKEN in the environment only. The manifest marks all three secret. Config
|
|
29
|
+
rejects credential fields. The S3 client uses only these credentials, never a default credential chain.
|
|
30
|
+
|
|
31
|
+
Uploads use Busboy 1.6.0 (already present through Nest's Express adapter) as a direct dependency to
|
|
32
|
+
avoid depending on transitive hoisting. Incoming bytes stream through a size limiter, SHA-256 and
|
|
33
|
+
UTF-8/prefix validator into private temporary files; bounded ZIP metadata/content-types reads verify
|
|
34
|
+
OOXML without extracting the document. A second stream sends the validated file to private storage.
|
|
35
|
+
No production path reads a whole file into a Buffer. Files are not publicly addressable.
|
|
36
|
+
|
|
37
|
+
The default allowlist is pdf, png, jpeg (jpg accepted), webp, gif, docx, xlsx, pptx, csv, txt. App config
|
|
38
|
+
can only narrow/reorder these types; active HTML/SVG/script/executable types cannot be enabled.
|
|
39
|
+
Names lose path components and controls, normalize to NFC and are limited to 180 Unicode characters.
|
|
40
|
+
Known dangerous or supported preceding extensions are refused. CSV/txt require valid UTF-8 with no
|
|
41
|
+
NUL, active HTML/SVG/script markup or executable signature. Downloads are always attachments with
|
|
42
|
+
RFC 5987 filename encoding, nosniff, no-store, detected MIME and exact Content-Length.
|
|
43
|
+
|
|
44
|
+
All five HTTP routes require InteractiveAuthGuard under `/state-machine-instances/:id/attachments`:
|
|
45
|
+
|
|
46
|
+
| Method/path | Behavior |
|
|
47
|
+
| --- | --- |
|
|
48
|
+
| GET / | List undeleted metadata (including REJECTED), uploadedByName, canDelete, canUpload and limits |
|
|
49
|
+
| POST / | Exactly one multipart field named file, no other fields |
|
|
50
|
+
| GET /:attachmentId | One visible metadata item |
|
|
51
|
+
| GET /:attachmentId/content | Reauthorize and stream READY content |
|
|
52
|
+
| DELETE /:attachmentId | Idempotent soft deletion |
|
|
53
|
+
|
|
54
|
+
Readable instances use the shared state-machine access helper: submitter, effective instance:read,
|
|
55
|
+
or matching USER/ROLE READ/WRITE. Check readership first. Upload additionally requires submitter,
|
|
56
|
+
effective instance:write or matching WRITE. Delete requires uploader or effective instance:admin
|
|
57
|
+
after readership. Frozen/final instances accept files. No new permissions are introduced. History
|
|
58
|
+
records ATTACHMENT with decision.attachment (action, id, plain-text name); no storage key or notice.
|
|
59
|
+
|
|
60
|
+
Register `AttachmentsMcpTools.registerInto(registry, { scopeNamespace })`. The sole tool is
|
|
61
|
+
`list_instance_attachments`, requiring `<namespace>:instance:read` and a bound user. It returns only
|
|
62
|
+
id, name, size, detected type, status, uploader and time. Names use `$userContent`: data, never
|
|
63
|
+
instructions. No content, upload, delete or public URL tool exists.
|
|
64
|
+
|
|
65
|
+
## Attachment panel contract (draft 202610-a7c1)
|
|
66
|
+
|
|
67
|
+
HTTP list and read-only MCP both add `canUpload` and
|
|
68
|
+
`limits: { maxFileBytes, maxAttachmentsPerInstance, allowedTypes, count, remaining }`.
|
|
69
|
+
`canUpload` uses the same authorization function as upload preflight and commit; it remains true
|
|
70
|
+
for an authorized user even at capacity. Allowed types are the effective extension codes (`jpeg`
|
|
71
|
+
also accepts `jpg`). Counts and both upload quota checks include only undeleted READY/PENDING_SCAN;
|
|
72
|
+
REJECTED content is removed and does not occupy a slot. Remaining is clamped to zero.
|
|
73
|
+
The list retains all undeleted rejections, including when they outnumber the active-file limit;
|
|
74
|
+
the quota is not a list-length limit. Deleted records stay hidden.
|
|
75
|
+
|
|
76
|
+
Render PENDING_SCAN as scanning and REJECTED as scan failed/content removed. Neither can download.
|
|
77
|
+
Rejected metadata keeps the existing uploader/readable-admin deletion policy and retention cleanup.
|
|
78
|
+
Use `@haneinfo/frontend-instances` for the human panel; its App upload route must stream multipart
|
|
79
|
+
and validate login and Origin/Host. Never buffer it in a Server Action or a cloning Next Proxy.
|
|
80
|
+
|
|
81
|
+
| Error | HTTP |
|
|
82
|
+
| --- | ---: |
|
|
83
|
+
| INSTANCE_NOT_FOUND | 404 (missing or unreadable instance, identical message) |
|
|
84
|
+
| ATTACHMENT_NOT_FOUND | 404 (missing, deleted or non-READY download) |
|
|
85
|
+
| FORBIDDEN | 403 (readable but cannot upload/delete) |
|
|
86
|
+
| ATTACHMENT_TOO_LARGE | 413 |
|
|
87
|
+
| ATTACHMENT_LIMIT_EXCEEDED | 409 |
|
|
88
|
+
| ATTACHMENT_TYPE_NOT_ALLOWED / ATTACHMENT_TYPE_MISMATCH | 400 |
|
|
89
|
+
| ATTACHMENT_NAME_INVALID / ATTACHMENT_MULTIPART_INVALID / ATTACHMENT_UPLOAD_ABORTED | 400 |
|
|
90
|
+
|
|
91
|
+
Filename errors never echo the user's filename. HTTP metadata is plain text and must be rendered
|
|
92
|
+
with text nodes. Declared MIME is retained for audit, never used for downloads. See the guide for
|
|
93
|
+
scan recovery, cleanup limits and coordinated database/storage backups.
|
package/agent-guide.md
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# Attachments agent guide
|
|
2
|
+
|
|
3
|
+
Use this package for private immutable instance attachments (decision 202610-5f3b).
|
|
4
|
+
The panel contract additions implement draft 202610-a7c1; that decision remains draft.
|
|
5
|
+
Do not use it for exports, inline previews, versions, public links, folders or frontend panels.
|
|
6
|
+
It has no definition kind or adapters; configure the runtime rather than create a definition.
|
|
7
|
+
The HTTP, MCP, errors, storage configuration and authorization contract are in [README.md](README.md).
|
|
8
|
+
|
|
9
|
+
Draft 202610-a7c1 adds canUpload and effective limits to both HTTP/MCP lists. canUpload is authority
|
|
10
|
+
only, using the upload authorization path. Quota counts READY/PENDING_SCAN only; undeleted REJECTED
|
|
11
|
+
records remain visible, occupy no slots, cannot download, and retain existing deletion permissions.
|
|
12
|
+
Always use limits.remaining rather than items.length to precheck capacity. A rejected row may be
|
|
13
|
+
deleted by its uploader or a readable instance administrator; the soft-delete/cleanup flow is unchanged.
|
|
14
|
+
|
|
15
|
+
## Adoption
|
|
16
|
+
|
|
17
|
+
Upgrade plugin-api (new contract 1.15.0), state-machine and collaboration together. Include the new
|
|
18
|
+
fragment, add the reverse `attachments Attachment[]` relation in the App, and deploy the supplied SQL
|
|
19
|
+
as an ordinary migration before starting the plugin. No instance/history columns change. Keep the
|
|
20
|
+
App's existing interactive authentication, RBAC and background-jobs composition. The plugin owns
|
|
21
|
+
Attachment and AttachmentGarbage only and uses the shared instanceAccessAllowed helper.
|
|
22
|
+
|
|
23
|
+
## Configuration
|
|
24
|
+
|
|
25
|
+
All options are under `runtime.attachments`, validated identically by direct dynamic-module registration.
|
|
26
|
+
|
|
27
|
+
| Option | Default / meaning |
|
|
28
|
+
| --- | --- |
|
|
29
|
+
| driver | Required: local or s3 |
|
|
30
|
+
| localRoot | Required for local; trusted private persistent directory |
|
|
31
|
+
| s3.bucket / s3.region | Required for s3 |
|
|
32
|
+
| s3.endpoint / s3.forcePathStyle | Optional URL / false |
|
|
33
|
+
| maxFileBytes | 26214400; maximum Number.MAX_SAFE_INTEGER, persisted as BigInt |
|
|
34
|
+
| maxAttachmentsPerInstance | 20; maximum 1000 |
|
|
35
|
+
| allowedTypes | pdf, png, jpeg, webp, gif, docx, xlsx, pptx, csv, txt |
|
|
36
|
+
| deletedRetentionDays | 7; nonnegative integer |
|
|
37
|
+
| pendingScanTimeoutMs | 900000; expired pending files fail closed as REJECTED |
|
|
38
|
+
| maxUploadMs | 300000; maximum 1800000, covering envelope, staging and storage |
|
|
39
|
+
| cleanupBatchSize | 100; maximum 500 per metadata/garbage/staging batch |
|
|
40
|
+
| cleanupPollIntervalMs | 60000 |
|
|
41
|
+
| cleanupWorkerEnabled | true; startup check and periodic cleanup enqueueing |
|
|
42
|
+
|
|
43
|
+
To drive jobs manually in tests, disable cleanupWorkerEnabled and the existing background-jobs
|
|
44
|
+
worker (`BACKGROUND_JOBS_WORKER_ENABLED=false` in Apps that map that environment setting). The
|
|
45
|
+
producer never reclaims RUNNING jobs automatically. Existing background-jobs retry and recovery
|
|
46
|
+
rules apply. Each replica may enqueue cleanup; object deletion and record purging are idempotent.
|
|
47
|
+
|
|
48
|
+
## Optional scanning
|
|
49
|
+
|
|
50
|
+
Provide ATTACHMENT_SCANNER / haneinfo.attachment-scanner using AttachmentScannerPort. There is no
|
|
51
|
+
built-in scanner. New records are PENDING_SCAN until the registered `attachments.scan` job consumes
|
|
52
|
+
a storage stream and returns `{ status: 'CLEAN' }` or `{ status: 'REJECTED', reason }`. CLEAN sets
|
|
53
|
+
READY; rejection deletes content and persists REJECTED. Reasons are not exposed or copied to history.
|
|
54
|
+
The supplied stream is destroyed after each attempt, including failures/cancellation. Exceptions
|
|
55
|
+
leave PENDING_SCAN and use background-jobs retry. Missing content after a failed rejection commit
|
|
56
|
+
is rejected on retry. Non-READY downloads and deleted downloads uniformly return 404.
|
|
57
|
+
|
|
58
|
+
Cleanup recovers missing/failed scan enqueueing, skips active scan jobs, and rejects pending files
|
|
59
|
+
older than pendingScanTimeoutMs, deleting their content. Removing a scanner never silently approves
|
|
60
|
+
existing pending files. Rejected metadata is retained until its createdAt exceeds the deleted
|
|
61
|
+
retention period; its content is removed immediately. No scan timeout makes a file downloadable.
|
|
62
|
+
|
|
63
|
+
## Operations and consistency
|
|
64
|
+
|
|
65
|
+
Back up and restore both PostgreSQL and the configured storage backend as one coordinated set.
|
|
66
|
+
Storage keys are random `attachments/<UTC year>/<month>/<uuid>`; never derive them from names or
|
|
67
|
+
accept user-specified keys. Preserve local directory ownership/private permissions, or a private
|
|
68
|
+
S3 bucket. Configure S3 lifecycle abortion of incomplete multipart uploads for process crashes.
|
|
69
|
+
The SDK aborts failed live uploads, uses queueSize=1 and a 5 MiB part buffer.
|
|
70
|
+
|
|
71
|
+
Authorization/count preflight happens before consuming multipart bytes. Counts are checked again
|
|
72
|
+
under the existing instance lock at commit, so concurrent uploads cannot exceed the cap. A durable
|
|
73
|
+
AttachmentGarbage marker precedes every storage write. Metadata, history and marker removal commit
|
|
74
|
+
together. A failed DB write deletes the object; failed deletion leaves its marker for cleanup after
|
|
75
|
+
one hour. Ambiguous commits check for a referenced object before deletion and leave recovery to
|
|
76
|
+
cleanup when the DB is unavailable. Garbage cleanup also refuses to delete referenced objects.
|
|
77
|
+
|
|
78
|
+
Temporary staging files live under the OS temp directory's haneinfo-attachments-staging folder,
|
|
79
|
+
with random UUID names and private modes. Normal failures and client aborts remove partial files.
|
|
80
|
+
Startup/periodic cleanup reclaims stale staging files after one hour, exceeding the maximum upload
|
|
81
|
+
deadline. Run replicas with separate local temp directories; remote/shared staging is unsupported.
|
|
82
|
+
Cleanup removes content first and then DB metadata, making retries safe if either step fails.
|
|
83
|
+
Soft deletion hides records immediately; retained files must never be served outside this API.
|
|
84
|
+
|
|
85
|
+
OOXML checks use bounded central-directory metadata (128 KiB, 2000 entries), local headers, required
|
|
86
|
+
main-part names and at most 64 KiB of decompressed content-types XML. ZIP64, encrypted/macro-enabled
|
|
87
|
+
archives, oversized metadata and malformed containers are refused conservatively. Magic detection
|
|
88
|
+
is a type gate, not malware detection or complete format validation; install a scanner when needed.
|
|
89
|
+
|
|
90
|
+
For common errors, use the README table: adjust limits/allowlists, fix file extensions/content, or
|
|
91
|
+
ask the instance owner for access. Never ask an agent to upload/delete through MCP; a person uses
|
|
92
|
+
the authenticated App. Related recipe: the equipment-loan attachments validation overlay in the
|
|
93
|
+
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,53 @@
|
|
|
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
|
+
canUpload: boolean;
|
|
25
|
+
limits: {
|
|
26
|
+
maxFileBytes: number;
|
|
27
|
+
maxAttachmentsPerInstance: number;
|
|
28
|
+
allowedTypes: ("pdf" | "png" | "jpeg" | "webp" | "gif" | "docx" | "xlsx" | "pptx" | "csv" | "txt")[];
|
|
29
|
+
count: number;
|
|
30
|
+
remaining: number;
|
|
31
|
+
};
|
|
32
|
+
}>;
|
|
33
|
+
get(user: InteractivePrincipal, id: string, attachmentId: string): Promise<{
|
|
34
|
+
id: string;
|
|
35
|
+
instanceId: string;
|
|
36
|
+
fileName: string;
|
|
37
|
+
declaredContentType: string;
|
|
38
|
+
detectedContentType: string;
|
|
39
|
+
size: number;
|
|
40
|
+
sha256: string;
|
|
41
|
+
status: "READY" | "PENDING_SCAN" | "REJECTED";
|
|
42
|
+
uploadedBy: string;
|
|
43
|
+
uploadedByName: string | null;
|
|
44
|
+
createdAt: Date;
|
|
45
|
+
canDelete: boolean;
|
|
46
|
+
}>;
|
|
47
|
+
upload(user: InteractivePrincipal, id: string, request: IncomingMessage, response: ServerResponse): Promise<unknown>;
|
|
48
|
+
content(user: InteractivePrincipal, id: string, attachmentId: string, response: ServerResponse): Promise<void>;
|
|
49
|
+
delete(user: InteractivePrincipal, id: string, attachmentId: string): Promise<{
|
|
50
|
+
deleted: boolean;
|
|
51
|
+
}>;
|
|
52
|
+
}
|
|
53
|
+
//# 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"}
|