@dudousxd/nestjs-agent 1.11.0 → 1.12.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/README.md +21 -14
- package/dist/{agent-deps.factory-BRTBu0lV.d.cts → agent-deps.factory-pqv_qTmJ.d.cts} +22 -50
- package/dist/{agent-deps.factory-BRTBu0lV.d.ts → agent-deps.factory-pqv_qTmJ.d.ts} +22 -50
- package/dist/durable/index.cjs +669 -654
- package/dist/durable/index.cjs.map +1 -1
- package/dist/durable/index.d.cts +1 -1
- package/dist/durable/index.d.ts +1 -1
- package/dist/durable/index.js +569 -554
- package/dist/durable/index.js.map +1 -1
- package/dist/index.cjs +682 -663
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +40 -47
- package/dist/index.d.ts +40 -47
- package/dist/index.js +570 -553
- package/dist/index.js.map +1 -1
- package/dist/media/index.cjs +11 -2
- package/dist/media/index.cjs.map +1 -1
- package/dist/media/index.d.cts +5 -1
- package/dist/media/index.d.ts +5 -1
- package/dist/media/index.js +11 -2
- package/dist/media/index.js.map +1 -1
- package/package.json +2 -2
package/dist/media/index.cjs
CHANGED
|
@@ -79,6 +79,14 @@ var MediaAttachmentStaging2 = class _MediaAttachmentStaging {
|
|
|
79
79
|
this.now = options.clock ?? (() => /* @__PURE__ */ new Date());
|
|
80
80
|
this.newId = options.idGenerator ?? (() => (0, import_node_crypto.randomUUID)());
|
|
81
81
|
}
|
|
82
|
+
/** The limits this store enforces, and how a client uploads to it (see `GET <base>/config`). */
|
|
83
|
+
describe() {
|
|
84
|
+
return {
|
|
85
|
+
maxBytes: this.maxBytes,
|
|
86
|
+
allowedContentTypes: this.allowed,
|
|
87
|
+
upload: this.deps.resumableRoutes === false ? "multipart" : "resumable"
|
|
88
|
+
};
|
|
89
|
+
}
|
|
82
90
|
get diskName() {
|
|
83
91
|
return this.options.disk ?? this.deps.storage.defaultDisk;
|
|
84
92
|
}
|
|
@@ -531,7 +539,7 @@ function _ts_decorate2(decorators, target, key, desc) {
|
|
|
531
539
|
}
|
|
532
540
|
__name(_ts_decorate2, "_ts_decorate");
|
|
533
541
|
var GUARDS_METADATA = "__guards__";
|
|
534
|
-
function stagingProvider() {
|
|
542
|
+
function stagingProvider(routes) {
|
|
535
543
|
return {
|
|
536
544
|
provide: AGENT_MEDIA_ATTACHMENTS,
|
|
537
545
|
inject: [
|
|
@@ -552,6 +560,7 @@ function stagingProvider() {
|
|
|
552
560
|
storage,
|
|
553
561
|
store,
|
|
554
562
|
uploads,
|
|
563
|
+
resumableRoutes: routes,
|
|
555
564
|
...agentStore !== void 0 ? {
|
|
556
565
|
agentStore
|
|
557
566
|
} : {}
|
|
@@ -585,7 +594,7 @@ function build(mount, optionsProvider, imports = []) {
|
|
|
585
594
|
] : [],
|
|
586
595
|
providers: [
|
|
587
596
|
optionsProvider,
|
|
588
|
-
stagingProvider(),
|
|
597
|
+
stagingProvider(routes),
|
|
589
598
|
{
|
|
590
599
|
provide: import_nestjs_agent_core2.AGENT_ATTACHMENT_STAGING,
|
|
591
600
|
useExisting: AGENT_MEDIA_ATTACHMENTS
|
package/dist/media/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/media/index.ts","../../src/media/media-attachment-staging.ts","../../src/media/media-attachments.options.ts","../../src/media/media-attachments.module.ts","../../src/media/media-uploads.controller.ts","../../src/media/tokens.ts"],"sourcesContent":["export {\n type BeginMediaUploadInput,\n type BeginMediaUploadResult,\n MediaAttachmentStaging,\n type MediaAttachmentStagingDeps,\n} from './media-attachment-staging.js';\nexport { AgentMediaAttachmentsModule } from './media-attachments.module.js';\nexport {\n type AgentMediaAttachmentsAsyncOptions,\n type AgentMediaAttachmentsModuleOptions,\n type AgentMediaAttachmentsMountOptions,\n type AgentMediaAttachmentsOptions,\n DEFAULT_AGENT_MEDIA_COLLECTION,\n DEFAULT_AGENT_MEDIA_OWNER_TYPE,\n DEFAULT_AGENT_MEDIA_TUS_BASE_PATH,\n DEFAULT_AGENT_MEDIA_URL_TTL_SECONDS,\n type MediaAttachmentAccessInput,\n type ResolveMediaUrlContext,\n} from './media-attachments.options.js';\nexport { AgentMediaUploadsController } from './media-uploads.controller.js';\nexport { AGENT_MEDIA_ATTACHMENTS, AGENT_MEDIA_ATTACHMENTS_OPTIONS } from './tokens.js';\n","import { randomUUID } from 'node:crypto';\nimport type {\n Actor,\n AgentStore,\n AttachmentStagingStore,\n ListStagedAttachmentsInput,\n MessageAttachment,\n ResolveAttachmentInput,\n StageAttachmentInput,\n StagedAttachment,\n} from '@dudousxd/nestjs-agent-core';\nimport {\n type MediaRecord,\n type MediaStore,\n type ResumableUploadManager,\n type StorageManager,\n publishMedia,\n} from '@dudousxd/nestjs-media';\nimport {\n BadRequestException,\n ConflictException,\n Logger,\n NotImplementedException,\n PayloadTooLargeException,\n UnprocessableEntityException,\n UnsupportedMediaTypeException,\n} from '@nestjs/common';\nimport {\n type AgentMediaAttachmentsOptions,\n DEFAULT_AGENT_MEDIA_COLLECTION,\n DEFAULT_AGENT_MEDIA_OWNER_TYPE,\n DEFAULT_AGENT_MEDIA_TUS_BASE_PATH,\n DEFAULT_AGENT_MEDIA_URL_TTL_SECONDS,\n} from './media-attachments.options.js';\n\n/**\n * Mirrors `DEFAULT_MAX_ATTACHMENT_BYTES` / `DEFAULT_ALLOWED_ATTACHMENT_CONTENT_TYPES` from the\n * `POST /agent/attachments` controller — restated rather than imported so this subpath never pulls\n * the root entry's classes into its own bundle.\n */\nconst DEFAULT_MAX_BYTES = 20 * 1024 * 1024;\nconst DEFAULT_ALLOWED_CONTENT_TYPES: readonly string[] = [\n 'image/png',\n 'image/jpeg',\n 'image/gif',\n 'image/webp',\n 'application/pdf',\n 'text/plain',\n 'text/csv',\n];\n\n/** Where the record's bytes stand. Kept on `customProperties.agentAttachment`. */\ninterface AgentAttachmentState {\n status: 'pending' | 'ready';\n /** The tus session the bytes are streaming through, while `pending`. */\n uploadId?: string;\n}\n\n/** What {@link MediaAttachmentStaging} needs from `@dudousxd/nestjs-media`. */\nexport interface MediaAttachmentStagingDeps {\n storage: StorageManager;\n store: MediaStore;\n /** `null` when `MediaModule` has no `uploadSessions` — resumable uploads are then unavailable. */\n uploads: ResumableUploadManager | null;\n /** Lets media referenced from the actor's own thread resolve (see {@link MediaAttachmentStaging.resolve}). */\n agentStore?: AgentStore;\n}\n\nexport interface BeginMediaUploadInput {\n actor: Actor;\n filename: string;\n contentType: string;\n /** Exact byte length of the file — tus needs it up front, and completion is checked against it. */\n size: number;\n}\n\n/** A tus session opened for one attachment. Stream the bytes to `location`, then complete. */\nexport interface BeginMediaUploadResult {\n /** The attachment's id — what the chat turn will send as `{ mediaId }`. */\n mediaId: string;\n /** The tus upload id. */\n uploadId: string;\n /** The tus resource to `PATCH` the bytes to (relative to the API origin). */\n location: string;\n}\n\n/**\n * {@link AttachmentStagingStore} backed by `@dudousxd/nestjs-media`: every chat attachment is a\n * media-library record (`ownerType` + the actor's id as owner, one collection), its bytes on a\n * media disk.\n *\n * Two ways in, one store:\n * - `beginUpload` → the client streams the bytes to nestjs-media's own tus endpoint → `completeUpload`\n * (resumable, what `@dudousxd/nestjs-agent-react/media` drives);\n * - `stage` — what `POST /agent/attachments` calls with a buffered multipart file, when a host also\n * mounts that route.\n *\n * The record exists from `beginUpload` on, so an upload abandoned half-way is in the actor's\n * inventory ({@link list}) and ages into `AgentService.collectableAttachments` like any unsent file.\n */\nexport class MediaAttachmentStaging implements AttachmentStagingStore {\n private readonly logger = new Logger(MediaAttachmentStaging.name);\n private readonly collection: string;\n private readonly ownerType: string;\n private readonly maxBytes: number;\n private readonly allowed: readonly string[];\n private readonly now: () => Date;\n private readonly newId: () => string;\n private warnedInline = false;\n\n constructor(\n private readonly deps: MediaAttachmentStagingDeps,\n private readonly options: AgentMediaAttachmentsOptions,\n ) {\n this.collection = options.collection ?? DEFAULT_AGENT_MEDIA_COLLECTION;\n this.ownerType = options.ownerType ?? DEFAULT_AGENT_MEDIA_OWNER_TYPE;\n this.maxBytes = options.maxBytes ?? DEFAULT_MAX_BYTES;\n this.allowed = options.allowedContentTypes ?? DEFAULT_ALLOWED_CONTENT_TYPES;\n this.now = options.clock ?? (() => new Date());\n this.newId = options.idGenerator ?? (() => randomUUID());\n }\n\n private get diskName(): string {\n return this.options.disk ?? this.deps.storage.defaultDisk;\n }\n\n /** Open a tus session for one file, owned by `actor`. Validation happens here, before any byte moves. */\n async beginUpload(input: BeginMediaUploadInput): Promise<BeginMediaUploadResult> {\n const uploads = this.deps.uploads;\n if (uploads === null) {\n throw new NotImplementedException(\n 'Resumable attachment uploads need MediaModule configured with `uploadSessions` and `tus`.',\n );\n }\n this.validate(input.filename, input.contentType, input.size);\n const id = this.newId();\n const fileName = safeFileName(input.filename);\n const path = this.pathFor(input.actor, id, fileName);\n const session = await uploads.createUpload({\n disk: this.diskName,\n key: path,\n size: input.size,\n contentType: input.contentType,\n metadata: { agentMediaId: id },\n });\n await this.saveRecord({\n id,\n actor: input.actor,\n fileName,\n mimeType: input.contentType,\n size: input.size,\n path,\n state: { status: 'pending', uploadId: session.id },\n });\n const base = (this.options.tusBasePath ?? DEFAULT_AGENT_MEDIA_TUS_BASE_PATH).replace(\n /\\/+$/,\n '',\n );\n return { mediaId: id, uploadId: session.id, location: `${base}/${session.id}` };\n }\n\n /**\n * Confirm the bytes of a `beginUpload` landed and whole. `null` for an unknown or foreign id;\n * `409` while the bytes are still missing; `422` (and the record dropped) when what arrived is\n * not the size that was declared. Idempotent on a ready record.\n */\n async completeUpload(input: {\n actor: Actor;\n mediaId: string;\n }): Promise<MessageAttachment | null> {\n const record = await this.ownRecord(input.mediaId, input.actor);\n if (record === null) return null;\n const ready = await this.ensureReady(record);\n if (ready === 'missing') {\n throw new ConflictException(`upload ${input.mediaId} has not finished`);\n }\n if (ready === 'mismatch') {\n await this.remove(record.id);\n throw new UnprocessableEntityException(\n `upload ${input.mediaId} does not match its declared size`,\n );\n }\n return this.toAttachment(ready, { forModel: false });\n }\n\n /** The actor drops one of their own attachments — aborting its upload if still in flight. */\n async discard(input: { actor: Actor; mediaId: string }): Promise<boolean> {\n const record = await this.ownRecord(input.mediaId, input.actor);\n if (record === null) return false;\n await this.remove(record.id);\n return true;\n }\n\n /**\n * Delete an attachment's bytes, its in-flight tus session and its record. NO ownership check —\n * the host's sweep calls this for what `AgentService.collectableAttachments` returned.\n */\n async remove(mediaId: string): Promise<void> {\n const record = await this.deps.store.find(mediaId);\n if (record === null || !this.isOurs(record)) return;\n const state = stateOf(record);\n if (state.status === 'pending' && state.uploadId !== undefined) {\n await this.deps.uploads?.abort(state.uploadId).catch(() => undefined);\n }\n await this.deps.storage\n .disk(record.disk)\n .delete(record.path)\n .catch(() => undefined);\n await this.deps.store.delete(record.id);\n if (this.options.indexForRag === true && state.status === 'ready') {\n publishMedia('delete', {\n id: record.id,\n ownerType: record.ownerType,\n ownerId: record.ownerId,\n });\n }\n }\n\n async stage(input: StageAttachmentInput): Promise<MessageAttachment> {\n const id = this.newId();\n const fileName = safeFileName(input.filename);\n const path = this.pathFor(input.actor, id, fileName);\n await this.deps.storage\n .disk(this.diskName)\n .put(path, input.data, { contentType: input.contentType });\n const record = await this.saveRecord({\n id,\n actor: input.actor,\n fileName,\n mimeType: input.contentType,\n size: input.sizeBytes,\n path,\n state: { status: 'ready' },\n });\n this.announce(record);\n return this.toAttachment(record, { forModel: false });\n }\n\n /**\n * The attachment for `mediaId`, or `null` when it is unknown, not this actor's, or its bytes have\n * not (fully) arrived. The actor may use media they own, and media a message in one of THEIR\n * threads already carries (a fork, a regenerate) — never anything else.\n */\n async resolve(input: ResolveAttachmentInput): Promise<MessageAttachment | null> {\n const record = await this.deps.store.find(input.mediaId);\n if (record === null || !this.isOurs(record)) return null;\n if (!(await this.canUse(record, input.actor))) return null;\n const ready = await this.ensureReady(record);\n if (typeof ready === 'string') return null;\n return this.toAttachment(ready, { forModel: true });\n }\n\n async list(input: ListStagedAttachmentsInput): Promise<StagedAttachment[]> {\n const records = await this.deps.store.listByOwner(\n this.ownerType,\n input.actor.id,\n this.collection,\n );\n const entries = records\n .map((record) => ({\n mediaId: record.id,\n name: record.fileName,\n contentType: record.mimeType,\n sizeBytes: record.size,\n createdAt: new Date(record.createdAt).toISOString(),\n }))\n .filter((entry) => input.stagedBefore === undefined || entry.createdAt < input.stagedBefore)\n .sort((a, b) => b.createdAt.localeCompare(a.createdAt));\n return input.limit === undefined ? entries : entries.slice(0, input.limit);\n }\n\n private validate(filename: string, contentType: string, size: number): void {\n if (typeof filename !== 'string' || filename.trim().length === 0) {\n throw new BadRequestException('filename is required');\n }\n if (typeof contentType !== 'string' || !this.allowed.includes(contentType)) {\n throw new UnsupportedMediaTypeException(\n `content type \"${String(contentType)}\" is not allowed (allowed: ${this.allowed.join(', ')})`,\n );\n }\n if (typeof size !== 'number' || !Number.isInteger(size) || size <= 0) {\n throw new BadRequestException('size must be a positive integer byte count');\n }\n if (size > this.maxBytes) {\n throw new PayloadTooLargeException(`file exceeds the ${this.maxBytes}-byte limit`);\n }\n }\n\n private pathFor(actor: Actor, id: string, fileName: string): string {\n return `${this.ownerType}/${encodeURIComponent(actor.id)}/${this.collection}/${id}/${fileName}`;\n }\n\n private async saveRecord(input: {\n id: string;\n actor: Actor;\n fileName: string;\n mimeType: string;\n size: number;\n path: string;\n state: AgentAttachmentState;\n }): Promise<MediaRecord> {\n const timestamp = this.now();\n return this.deps.store.save({\n id: input.id,\n ownerType: this.ownerType,\n ownerId: input.actor.id,\n collection: this.collection,\n name: stripExtension(input.fileName),\n fileName: input.fileName,\n mimeType: input.mimeType,\n size: input.size,\n disk: this.diskName,\n path: input.path,\n order: await this.deps.store.nextOrder(this.ownerType, input.actor.id, this.collection),\n customProperties: { agentAttachment: input.state },\n conversions: {},\n createdAt: timestamp,\n updatedAt: timestamp,\n });\n }\n\n private isOurs(record: MediaRecord): boolean {\n return record.ownerType === this.ownerType && record.collection === this.collection;\n }\n\n private async ownRecord(mediaId: string, actor: Actor): Promise<MediaRecord | null> {\n const record = await this.deps.store.find(mediaId);\n if (record === null || !this.isOurs(record) || record.ownerId !== actor.id) return null;\n return record;\n }\n\n private async canUse(record: MediaRecord, actor: Actor): Promise<boolean> {\n const allowed = await this.defaultAccess(record, actor);\n const custom = this.options.canAccess;\n return custom === undefined ? allowed : (await custom({ record, actor, allowed })) === true;\n }\n\n private async defaultAccess(record: MediaRecord, actor: Actor): Promise<boolean> {\n if (record.ownerId === actor.id) return true;\n const referenced = this.deps.agentStore?.referencedMediaIds?.bind(this.deps.agentStore);\n if (referenced === undefined) return false;\n return (await referenced(actor.id, [record.id])).includes(record.id);\n }\n\n /**\n * A ready record as-is; a pending one promoted to ready once its bytes are on the disk at exactly\n * the declared size. `'missing'` while they are not; `'mismatch'` when they arrived at another size.\n */\n private async ensureReady(record: MediaRecord): Promise<MediaRecord | 'missing' | 'mismatch'> {\n if (stateOf(record).status === 'ready') return record;\n const disk = this.deps.storage.disk(record.disk);\n if (!(await disk.exists(record.path))) return 'missing';\n const size = await disk.size(record.path);\n if (size !== record.size || size > this.maxBytes) return 'mismatch';\n const ready = await this.deps.store.save({\n ...record,\n customProperties: { ...record.customProperties, agentAttachment: { status: 'ready' } },\n updatedAt: this.now(),\n });\n this.announce(ready);\n return ready;\n }\n\n private announce(record: MediaRecord): void {\n if (this.options.indexForRag !== true) return;\n publishMedia('attach', {\n id: record.id,\n ownerType: record.ownerType,\n ownerId: record.ownerId,\n collection: record.collection,\n disk: record.disk,\n path: record.path,\n size: record.size,\n mimeType: record.mimeType,\n });\n }\n\n /**\n * `forModel: false` is what goes back to the uploader: never inline bytes (they already have the\n * file), so an inline-only disk answers with an empty url there.\n */\n private async toAttachment(\n record: MediaRecord,\n { forModel }: { forModel: boolean },\n ): Promise<MessageAttachment> {\n const base = { mediaId: record.id, contentType: record.mimeType, name: record.fileName };\n const disk = this.deps.storage.disk(record.disk);\n if (this.options.resolveUrl !== undefined) {\n return { ...base, url: await this.options.resolveUrl(record, { disk }) };\n }\n if (this.options.visibility === 'public') {\n return { ...base, url: await disk.url(record.path) };\n }\n if (disk.capabilities.presign) {\n const ttl = this.options.urlExpiresInSeconds ?? DEFAULT_AGENT_MEDIA_URL_TTL_SECONDS;\n return {\n ...base,\n url: await disk.temporaryUrl(record.path, ttl, { responseContentType: record.mimeType }),\n };\n }\n if (!forModel) return { ...base, url: '' };\n if (!this.warnedInline) {\n this.warnedInline = true;\n this.logger.warn(\n `disk \"${record.disk}\" cannot presign urls: attachments reach the model inline (data: urls, persisted with each message). Use a presign-capable disk or resolveUrl in production.`,\n );\n }\n const bytes = await disk.get(record.path);\n return { ...base, url: `data:${record.mimeType};base64,${bytes.toString('base64')}` };\n }\n}\n\nfunction stateOf(record: MediaRecord): AgentAttachmentState {\n const state = record.customProperties.agentAttachment as AgentAttachmentState | undefined;\n return state ?? { status: 'ready' };\n}\n\n/** The last path segment, stripped of anything a disk would read as a directory or control. */\nfunction safeFileName(filename: string): string {\n const last = filename.split(/[\\\\/]/).pop() ?? '';\n // biome-ignore lint/suspicious/noControlCharactersInRegex: stripping control chars is the point.\n const cleaned = last.replace(/[\\u0000-\\u001f\\u007f]/g, '').trim();\n return cleaned === '' || cleaned === '.' || cleaned === '..' ? 'file' : cleaned;\n}\n\nfunction stripExtension(fileName: string): string {\n const dot = fileName.lastIndexOf('.');\n return dot > 0 ? fileName.slice(0, dot) : fileName;\n}\n","import type { Actor } from '@dudousxd/nestjs-agent-core';\nimport type { MediaRecord, StorageDriver } from '@dudousxd/nestjs-media';\nimport type {\n CanActivate,\n DynamicModule,\n InjectionToken,\n OptionalFactoryDependency,\n Type,\n} from '@nestjs/common';\n\n/** Default media-library collection chat attachments are filed under. */\nexport const DEFAULT_AGENT_MEDIA_COLLECTION = 'agent-attachments';\n\n/** Default `ownerType` of the media records — the owner id is the actor's id. */\nexport const DEFAULT_AGENT_MEDIA_OWNER_TYPE = 'agent-actor';\n\n/** Default lifetime of a presigned url handed to the model provider (7 days — S3's ceiling). */\nexport const DEFAULT_AGENT_MEDIA_URL_TTL_SECONDS = 7 * 24 * 60 * 60;\n\n/** Default tus base path — `MediaModule`'s own default for `tus.basePath`. */\nexport const DEFAULT_AGENT_MEDIA_TUS_BASE_PATH = '/media/uploads';\n\n/** What {@link AgentMediaAttachmentsOptions.resolveUrl} is handed besides the record. */\nexport interface ResolveMediaUrlContext {\n /** The storage driver the record's bytes live on. */\n disk: StorageDriver;\n}\n\n/** What {@link AgentMediaAttachmentsOptions.canAccess} decides on. */\nexport interface MediaAttachmentAccessInput {\n record: MediaRecord;\n actor: Actor;\n /** The default verdict: the actor owns the record, or a message in their own thread carries it. */\n allowed: boolean;\n}\n\nexport interface AgentMediaAttachmentsOptions {\n /** Media-library collection the records are filed under. Default `'agent-attachments'`. */\n collection?: string;\n /** `ownerType` of the records; `ownerId` is always the actor's id. Default `'agent-actor'`. */\n ownerType?: string;\n /** Storage disk the bytes land on. Default: the `MediaModule`'s default disk. */\n disk?: string;\n /** Per-file size cap. Default 20 MiB — the same as `POST /agent/attachments`. */\n maxBytes?: number;\n /**\n * Content types a chat may attach. Default: the same allowlist as `POST /agent/attachments`\n * (png/jpeg/gif/webp images, pdf, plain text, csv).\n */\n allowedContentTypes?: readonly string[];\n /**\n * How the model provider gets at the bytes.\n * - `'private'` (default): a presigned url (`temporaryUrl`) when the disk can mint one (S3), else\n * the bytes inline as a `data:` url. Inline works everywhere but is persisted with the message —\n * fine for local development, use a presign-capable disk (or `resolveUrl`) in production.\n * - `'public'`: the disk's stable public `url()` — only for a bucket the provider may read.\n */\n visibility?: 'private' | 'public';\n /** Lifetime of a presigned url, in seconds. Default 7 days. */\n urlExpiresInSeconds?: number;\n /**\n * Take over url minting entirely (e.g. a signed route on your own API). Wins over `visibility`.\n * SECURITY: the url is fetched server-side by the model provider — build it from the record only.\n */\n resolveUrl?: (record: MediaRecord, context: ResolveMediaUrlContext) => string | Promise<string>;\n /**\n * Who may send (resolve) a record in a chat turn. Default: `allowed` — the owner, or anyone whose\n * own thread already carries it. Return `allowed || …` to widen that (a shared team space),\n * `allowed && …` to narrow it. Only `resolve` consults this; completing and discarding an upload\n * stay the uploader's.\n */\n canAccess?: (input: MediaAttachmentAccessInput) => boolean | Promise<boolean>;\n /**\n * The public path of `MediaModule`'s tus endpoint (its `tus.basePath`, global prefix included) —\n * used to build the `location` a client streams the bytes to. Default `'/media/uploads'`.\n */\n tusBasePath?: string;\n /**\n * Announce each ready attachment on `aviary:media:attach` (and its removal on\n * `aviary:media:delete`), so `@dudousxd/nestjs-agent-rag-media` indexes it for retrieval. Off by\n * default: a chat attachment is private to the person who sent it, and indexing it is only safe\n * where retrieval is scoped by owner (`FilteredRetriever` on `ownerType`/`ownerId`).\n */\n indexForRag?: boolean;\n /** Injectable clock for tests. */\n clock?: () => Date;\n /** Injectable id generator for tests. Default `crypto.randomUUID`. */\n idGenerator?: () => string;\n}\n\n/** Build-time wiring shared by `forRoot` / `forRootAsync` — Nest mounts controllers before any factory runs. */\nexport interface AgentMediaAttachmentsMountOptions {\n /**\n * Route prefix — must match `AgentModule`'s `path`. The upload routes mount under\n * `<path>/attachments/uploads`. Default `'agent'`.\n */\n path?: string;\n /** Guards for the upload routes (mirror `AgentModule`'s `guards`). */\n guards?: Type<CanActivate>[];\n /**\n * Mount the resumable-upload routes. Default `true`. `false` keeps only the staging store — for a\n * host that opens tus sessions its own way and only wants resolve/list backed by media.\n */\n routes?: boolean;\n}\n\nexport interface AgentMediaAttachmentsModuleOptions\n extends AgentMediaAttachmentsOptions,\n AgentMediaAttachmentsMountOptions {}\n\nexport interface AgentMediaAttachmentsAsyncOptions extends AgentMediaAttachmentsMountOptions {\n imports?: DynamicModule['imports'];\n inject?: Array<InjectionToken | OptionalFactoryDependency>;\n useFactory: (\n ...args: never[]\n ) => AgentMediaAttachmentsOptions | Promise<AgentMediaAttachmentsOptions>;\n}\n","import {\n AGENT_ATTACHMENT_STAGING,\n AGENT_STORE,\n type AgentStore,\n} from '@dudousxd/nestjs-agent-core';\nimport {\n MEDIA_STORAGE_SHARED,\n MEDIA_STORE,\n MEDIA_UPLOADS,\n type MediaStore,\n type ResumableUploadManager,\n type StorageManager,\n} from '@dudousxd/nestjs-media';\nimport {\n type CanActivate,\n type DynamicModule,\n Global,\n Module,\n type Provider,\n type Type,\n} from '@nestjs/common';\nimport { RouterModule } from '@nestjs/core';\nimport { MediaAttachmentStaging } from './media-attachment-staging.js';\nimport type {\n AgentMediaAttachmentsAsyncOptions,\n AgentMediaAttachmentsModuleOptions,\n AgentMediaAttachmentsMountOptions,\n AgentMediaAttachmentsOptions,\n} from './media-attachments.options.js';\nimport { AgentMediaUploadsController } from './media-uploads.controller.js';\nimport { AGENT_MEDIA_ATTACHMENTS, AGENT_MEDIA_ATTACHMENTS_OPTIONS } from './tokens.js';\n\n/** See `agent.module.ts` — Nest's `GUARDS_METADATA` key, inlined for the same ESM reason. */\nconst GUARDS_METADATA = '__guards__';\n\nfunction stagingProvider(): Provider {\n return {\n provide: AGENT_MEDIA_ATTACHMENTS,\n inject: [\n AGENT_MEDIA_ATTACHMENTS_OPTIONS,\n MEDIA_STORAGE_SHARED,\n MEDIA_STORE,\n MEDIA_UPLOADS,\n { token: AGENT_STORE, optional: true },\n ],\n useFactory: (\n options: AgentMediaAttachmentsOptions,\n storage: StorageManager,\n store: MediaStore | null,\n uploads: ResumableUploadManager | null,\n agentStore: AgentStore | undefined,\n ) => {\n if (store === null) {\n throw new Error(\n 'AgentMediaAttachmentsModule: MediaModule has no `store`. Chat attachments are media ' +\n 'records, so configure MediaModule with a MediaStore (e.g. a database adapter).',\n );\n }\n return new MediaAttachmentStaging(\n { storage, store, uploads, ...(agentStore !== undefined ? { agentStore } : {}) },\n options,\n );\n },\n };\n}\n\nfunction build(\n mount: AgentMediaAttachmentsMountOptions,\n optionsProvider: Provider,\n imports: DynamicModule['imports'] = [],\n): DynamicModule {\n const routes = mount.routes !== false;\n Reflect.defineMetadata(GUARDS_METADATA, mount.guards ?? [], AgentMediaUploadsController);\n const guards: Type<CanActivate>[] = [...new Set(mount.guards ?? [])];\n return {\n module: AgentMediaAttachmentsModule,\n global: true,\n imports: [\n ...imports,\n ...(routes\n ? [\n RouterModule.register([\n { path: mount.path ?? 'agent', module: AgentMediaAttachmentsModule },\n ]),\n ]\n : []),\n ],\n controllers: routes ? [AgentMediaUploadsController] : [],\n providers: [\n optionsProvider,\n stagingProvider(),\n { provide: AGENT_ATTACHMENT_STAGING, useExisting: AGENT_MEDIA_ATTACHMENTS },\n { provide: MediaAttachmentStaging, useExisting: AGENT_MEDIA_ATTACHMENTS },\n ...guards,\n ],\n exports: [AGENT_ATTACHMENT_STAGING, AGENT_MEDIA_ATTACHMENTS, MediaAttachmentStaging],\n };\n}\n\n/**\n * Chat attachments on `@dudousxd/nestjs-media`: binds `AGENT_ATTACHMENT_STAGING` to a\n * media-backed store and mounts the resumable-upload routes (`<path>/attachments/uploads`).\n * Import it next to `AgentModule` and a `MediaModule` configured with `store`, `uploadSessions`\n * and `tus`.\n */\n@Global()\n@Module({})\nexport class AgentMediaAttachmentsModule {\n static forRoot(options: AgentMediaAttachmentsModuleOptions = {}): DynamicModule {\n return build(options, { provide: AGENT_MEDIA_ATTACHMENTS_OPTIONS, useValue: options });\n }\n\n static forRootAsync(options: AgentMediaAttachmentsAsyncOptions): DynamicModule {\n return build(\n options,\n {\n provide: AGENT_MEDIA_ATTACHMENTS_OPTIONS,\n inject: options.inject ?? [],\n useFactory: options.useFactory,\n },\n options.imports,\n );\n }\n}\n","import {\n AGENT_ACTOR_RESOLVER,\n type ActorResolver,\n type MessageAttachment,\n} from '@dudousxd/nestjs-agent-core';\nimport {\n BadRequestException,\n Body,\n Controller,\n Delete,\n HttpCode,\n Inject,\n NotFoundException,\n Param,\n Post,\n Req,\n} from '@nestjs/common';\nimport type { Request } from 'express';\nimport type { BeginMediaUploadResult, MediaAttachmentStaging } from './media-attachment-staging.js';\nimport { AGENT_MEDIA_ATTACHMENTS } from './tokens.js';\n\n/**\n * The agent's half of a resumable chat upload. It never carries a byte: it opens an owned,\n * validated tus session on `MediaModule` (`POST`), confirms the bytes arrived (`POST :id/complete`)\n * and drops an attachment the user removed (`DELETE :id`). The bytes themselves go to\n * nestjs-media's own tus endpoint (`PATCH <tusBasePath>/:uploadId`), which the host guards through\n * `MediaModule`'s `guards`.\n *\n * Mounted at `<AgentModule path>/attachments/uploads`.\n */\n@Controller('attachments/uploads')\nexport class AgentMediaUploadsController {\n constructor(\n @Inject(AGENT_ACTOR_RESOLVER) private readonly actorResolver: ActorResolver,\n @Inject(AGENT_MEDIA_ATTACHMENTS) private readonly staging: MediaAttachmentStaging,\n ) {}\n\n /** `{ filename, contentType, size }` → `{ mediaId, uploadId, location }`. */\n @Post()\n async begin(@Req() req: Request, @Body() body: unknown): Promise<BeginMediaUploadResult> {\n if (typeof body !== 'object' || body === null) {\n throw new BadRequestException('body must be { filename, contentType, size }');\n }\n const { filename, contentType, size } = body as Record<string, unknown>;\n const actor = await this.actorResolver.resolve(req);\n return this.staging.beginUpload({\n actor,\n filename: filename as string,\n contentType: contentType as string,\n size: size as number,\n });\n }\n\n @Post(':mediaId/complete')\n @HttpCode(200)\n async complete(\n @Req() req: Request,\n @Param('mediaId') mediaId: string,\n ): Promise<MessageAttachment> {\n const actor = await this.actorResolver.resolve(req);\n const attachment = await this.staging.completeUpload({ actor, mediaId });\n if (attachment === null) throw new NotFoundException(`attachment ${mediaId} not found`);\n return attachment;\n }\n\n @Delete(':mediaId')\n @HttpCode(204)\n async discard(@Req() req: Request, @Param('mediaId') mediaId: string): Promise<void> {\n const actor = await this.actorResolver.resolve(req);\n if (!(await this.staging.discard({ actor, mediaId }))) {\n throw new NotFoundException(`attachment ${mediaId} not found`);\n }\n }\n}\n","/** The {@link import('./media-attachment-staging.js').MediaAttachmentStaging} instance — inject it for `remove()` in a sweep. */\nexport const AGENT_MEDIA_ATTACHMENTS = Symbol.for('@dudousxd/nestjs-agent:media-attachments');\n\n/** The resolved {@link import('./media-attachments.options.js').AgentMediaAttachmentsOptions}. */\nexport const AGENT_MEDIA_ATTACHMENTS_OPTIONS = Symbol.for(\n '@dudousxd/nestjs-agent:media-attachments-options',\n);\n"],"mappings":"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","names":["MediaAttachmentStaging","DEFAULT_AGENT_MEDIA_COLLECTION","DEFAULT_AGENT_MEDIA_OWNER_TYPE","DEFAULT_AGENT_MEDIA_URL_TTL_SECONDS","DEFAULT_AGENT_MEDIA_TUS_BASE_PATH","DEFAULT_MAX_BYTES","DEFAULT_ALLOWED_CONTENT_TYPES","MediaAttachmentStaging","logger","Logger","name","collection","ownerType","maxBytes","allowed","now","newId","warnedInline","deps","options","DEFAULT_AGENT_MEDIA_COLLECTION","DEFAULT_AGENT_MEDIA_OWNER_TYPE","allowedContentTypes","clock","Date","idGenerator","randomUUID","diskName","disk","storage","defaultDisk","beginUpload","input","uploads","NotImplementedException","validate","filename","contentType","size","id","fileName","safeFileName","path","pathFor","actor","session","createUpload","key","metadata","agentMediaId","saveRecord","mimeType","state","status","uploadId","base","tusBasePath","DEFAULT_AGENT_MEDIA_TUS_BASE_PATH","replace","mediaId","location","completeUpload","record","ownRecord","ready","ensureReady","ConflictException","remove","UnprocessableEntityException","toAttachment","forModel","discard","store","find","isOurs","stateOf","undefined","abort","catch","delete","indexForRag","publishMedia","ownerId","stage","put","data","sizeBytes","announce","resolve","canUse","list","records","listByOwner","entries","map","createdAt","toISOString","filter","entry","stagedBefore","sort","a","b","localeCompare","limit","slice","trim","length","BadRequestException","includes","UnsupportedMediaTypeException","String","join","Number","isInteger","PayloadTooLargeException","encodeURIComponent","timestamp","save","stripExtension","order","nextOrder","customProperties","agentAttachment","conversions","updatedAt","defaultAccess","custom","canAccess","referenced","agentStore","referencedMediaIds","bind","exists","resolveUrl","url","visibility","capabilities","presign","ttl","urlExpiresInSeconds","DEFAULT_AGENT_MEDIA_URL_TTL_SECONDS","temporaryUrl","responseContentType","warn","bytes","get","toString","last","split","pop","cleaned","dot","lastIndexOf","import_nestjs_agent_core","import_nestjs_media","import_common","import_common","AGENT_MEDIA_ATTACHMENTS","Symbol","for","AGENT_MEDIA_ATTACHMENTS_OPTIONS","AgentMediaUploadsController","actorResolver","staging","begin","req","body","BadRequestException","filename","contentType","size","actor","resolve","beginUpload","complete","mediaId","attachment","completeUpload","NotFoundException","discard","GUARDS_METADATA","stagingProvider","provide","AGENT_MEDIA_ATTACHMENTS","inject","AGENT_MEDIA_ATTACHMENTS_OPTIONS","MEDIA_STORAGE_SHARED","MEDIA_STORE","MEDIA_UPLOADS","token","AGENT_STORE","optional","useFactory","options","storage","store","uploads","agentStore","Error","MediaAttachmentStaging","undefined","build","mount","optionsProvider","imports","routes","Reflect","defineMetadata","guards","AgentMediaUploadsController","Set","module","AgentMediaAttachmentsModule","global","RouterModule","register","path","controllers","providers","AGENT_ATTACHMENT_STAGING","useExisting","exports","forRoot","useValue","forRootAsync"]}
|
|
1
|
+
{"version":3,"sources":["../../src/media/index.ts","../../src/media/media-attachment-staging.ts","../../src/media/media-attachments.options.ts","../../src/media/media-attachments.module.ts","../../src/media/media-uploads.controller.ts","../../src/media/tokens.ts"],"sourcesContent":["export {\n type BeginMediaUploadInput,\n type BeginMediaUploadResult,\n MediaAttachmentStaging,\n type MediaAttachmentStagingDeps,\n} from './media-attachment-staging.js';\nexport { AgentMediaAttachmentsModule } from './media-attachments.module.js';\nexport {\n type AgentMediaAttachmentsAsyncOptions,\n type AgentMediaAttachmentsModuleOptions,\n type AgentMediaAttachmentsMountOptions,\n type AgentMediaAttachmentsOptions,\n DEFAULT_AGENT_MEDIA_COLLECTION,\n DEFAULT_AGENT_MEDIA_OWNER_TYPE,\n DEFAULT_AGENT_MEDIA_TUS_BASE_PATH,\n DEFAULT_AGENT_MEDIA_URL_TTL_SECONDS,\n type MediaAttachmentAccessInput,\n type ResolveMediaUrlContext,\n} from './media-attachments.options.js';\nexport { AgentMediaUploadsController } from './media-uploads.controller.js';\nexport { AGENT_MEDIA_ATTACHMENTS, AGENT_MEDIA_ATTACHMENTS_OPTIONS } from './tokens.js';\n","import { randomUUID } from 'node:crypto';\nimport type {\n Actor,\n AgentStore,\n AttachmentStagingDescription,\n AttachmentStagingStore,\n ListStagedAttachmentsInput,\n MessageAttachment,\n ResolveAttachmentInput,\n StageAttachmentInput,\n StagedAttachment,\n} from '@dudousxd/nestjs-agent-core';\nimport {\n type MediaRecord,\n type MediaStore,\n type ResumableUploadManager,\n type StorageManager,\n publishMedia,\n} from '@dudousxd/nestjs-media';\nimport {\n BadRequestException,\n ConflictException,\n Logger,\n NotImplementedException,\n PayloadTooLargeException,\n UnprocessableEntityException,\n UnsupportedMediaTypeException,\n} from '@nestjs/common';\nimport {\n type AgentMediaAttachmentsOptions,\n DEFAULT_AGENT_MEDIA_COLLECTION,\n DEFAULT_AGENT_MEDIA_OWNER_TYPE,\n DEFAULT_AGENT_MEDIA_TUS_BASE_PATH,\n DEFAULT_AGENT_MEDIA_URL_TTL_SECONDS,\n} from './media-attachments.options.js';\n\n/**\n * Mirrors `DEFAULT_MAX_ATTACHMENT_BYTES` / `DEFAULT_ALLOWED_ATTACHMENT_CONTENT_TYPES` from the\n * `POST /agent/attachments` controller — restated rather than imported so this subpath never pulls\n * the root entry's classes into its own bundle.\n */\nconst DEFAULT_MAX_BYTES = 20 * 1024 * 1024;\nconst DEFAULT_ALLOWED_CONTENT_TYPES: readonly string[] = [\n 'image/png',\n 'image/jpeg',\n 'image/gif',\n 'image/webp',\n 'application/pdf',\n 'text/plain',\n 'text/csv',\n];\n\n/** Where the record's bytes stand. Kept on `customProperties.agentAttachment`. */\ninterface AgentAttachmentState {\n status: 'pending' | 'ready';\n /** The tus session the bytes are streaming through, while `pending`. */\n uploadId?: string;\n}\n\n/** What {@link MediaAttachmentStaging} needs from `@dudousxd/nestjs-media`. */\nexport interface MediaAttachmentStagingDeps {\n storage: StorageManager;\n store: MediaStore;\n /** `null` when `MediaModule` has no `uploadSessions` — resumable uploads are then unavailable. */\n uploads: ResumableUploadManager | null;\n /** Lets media referenced from the actor's own thread resolve (see {@link MediaAttachmentStaging.resolve}). */\n agentStore?: AgentStore;\n /** The resumable-upload routes are mounted — what {@link MediaAttachmentStaging.describe} tells clients. */\n resumableRoutes?: boolean;\n}\n\nexport interface BeginMediaUploadInput {\n actor: Actor;\n filename: string;\n contentType: string;\n /** Exact byte length of the file — tus needs it up front, and completion is checked against it. */\n size: number;\n}\n\n/** A tus session opened for one attachment. Stream the bytes to `location`, then complete. */\nexport interface BeginMediaUploadResult {\n /** The attachment's id — what the chat turn will send as `{ mediaId }`. */\n mediaId: string;\n /** The tus upload id. */\n uploadId: string;\n /** The tus resource to `PATCH` the bytes to (relative to the API origin). */\n location: string;\n}\n\n/**\n * {@link AttachmentStagingStore} backed by `@dudousxd/nestjs-media`: every chat attachment is a\n * media-library record (`ownerType` + the actor's id as owner, one collection), its bytes on a\n * media disk.\n *\n * Two ways in, one store:\n * - `beginUpload` → the client streams the bytes to nestjs-media's own tus endpoint → `completeUpload`\n * (resumable, what `@dudousxd/nestjs-agent-react/media` drives);\n * - `stage` — what `POST /agent/attachments` calls with a buffered multipart file, when a host also\n * mounts that route.\n *\n * The record exists from `beginUpload` on, so an upload abandoned half-way is in the actor's\n * inventory ({@link list}) and ages into `AgentService.collectableAttachments` like any unsent file.\n */\nexport class MediaAttachmentStaging implements AttachmentStagingStore {\n private readonly logger = new Logger(MediaAttachmentStaging.name);\n private readonly collection: string;\n private readonly ownerType: string;\n private readonly maxBytes: number;\n private readonly allowed: readonly string[];\n private readonly now: () => Date;\n private readonly newId: () => string;\n private warnedInline = false;\n\n constructor(\n private readonly deps: MediaAttachmentStagingDeps,\n private readonly options: AgentMediaAttachmentsOptions,\n ) {\n this.collection = options.collection ?? DEFAULT_AGENT_MEDIA_COLLECTION;\n this.ownerType = options.ownerType ?? DEFAULT_AGENT_MEDIA_OWNER_TYPE;\n this.maxBytes = options.maxBytes ?? DEFAULT_MAX_BYTES;\n this.allowed = options.allowedContentTypes ?? DEFAULT_ALLOWED_CONTENT_TYPES;\n this.now = options.clock ?? (() => new Date());\n this.newId = options.idGenerator ?? (() => randomUUID());\n }\n\n /** The limits this store enforces, and how a client uploads to it (see `GET <base>/config`). */\n describe(): AttachmentStagingDescription {\n return {\n maxBytes: this.maxBytes,\n allowedContentTypes: this.allowed,\n upload: this.deps.resumableRoutes === false ? 'multipart' : 'resumable',\n };\n }\n\n private get diskName(): string {\n return this.options.disk ?? this.deps.storage.defaultDisk;\n }\n\n /** Open a tus session for one file, owned by `actor`. Validation happens here, before any byte moves. */\n async beginUpload(input: BeginMediaUploadInput): Promise<BeginMediaUploadResult> {\n const uploads = this.deps.uploads;\n if (uploads === null) {\n throw new NotImplementedException(\n 'Resumable attachment uploads need MediaModule configured with `uploadSessions` and `tus`.',\n );\n }\n this.validate(input.filename, input.contentType, input.size);\n const id = this.newId();\n const fileName = safeFileName(input.filename);\n const path = this.pathFor(input.actor, id, fileName);\n const session = await uploads.createUpload({\n disk: this.diskName,\n key: path,\n size: input.size,\n contentType: input.contentType,\n metadata: { agentMediaId: id },\n });\n await this.saveRecord({\n id,\n actor: input.actor,\n fileName,\n mimeType: input.contentType,\n size: input.size,\n path,\n state: { status: 'pending', uploadId: session.id },\n });\n const base = (this.options.tusBasePath ?? DEFAULT_AGENT_MEDIA_TUS_BASE_PATH).replace(\n /\\/+$/,\n '',\n );\n return { mediaId: id, uploadId: session.id, location: `${base}/${session.id}` };\n }\n\n /**\n * Confirm the bytes of a `beginUpload` landed and whole. `null` for an unknown or foreign id;\n * `409` while the bytes are still missing; `422` (and the record dropped) when what arrived is\n * not the size that was declared. Idempotent on a ready record.\n */\n async completeUpload(input: {\n actor: Actor;\n mediaId: string;\n }): Promise<MessageAttachment | null> {\n const record = await this.ownRecord(input.mediaId, input.actor);\n if (record === null) return null;\n const ready = await this.ensureReady(record);\n if (ready === 'missing') {\n throw new ConflictException(`upload ${input.mediaId} has not finished`);\n }\n if (ready === 'mismatch') {\n await this.remove(record.id);\n throw new UnprocessableEntityException(\n `upload ${input.mediaId} does not match its declared size`,\n );\n }\n return this.toAttachment(ready, { forModel: false });\n }\n\n /** The actor drops one of their own attachments — aborting its upload if still in flight. */\n async discard(input: { actor: Actor; mediaId: string }): Promise<boolean> {\n const record = await this.ownRecord(input.mediaId, input.actor);\n if (record === null) return false;\n await this.remove(record.id);\n return true;\n }\n\n /**\n * Delete an attachment's bytes, its in-flight tus session and its record. NO ownership check —\n * the host's sweep calls this for what `AgentService.collectableAttachments` returned.\n */\n async remove(mediaId: string): Promise<void> {\n const record = await this.deps.store.find(mediaId);\n if (record === null || !this.isOurs(record)) return;\n const state = stateOf(record);\n if (state.status === 'pending' && state.uploadId !== undefined) {\n await this.deps.uploads?.abort(state.uploadId).catch(() => undefined);\n }\n await this.deps.storage\n .disk(record.disk)\n .delete(record.path)\n .catch(() => undefined);\n await this.deps.store.delete(record.id);\n if (this.options.indexForRag === true && state.status === 'ready') {\n publishMedia('delete', {\n id: record.id,\n ownerType: record.ownerType,\n ownerId: record.ownerId,\n });\n }\n }\n\n async stage(input: StageAttachmentInput): Promise<MessageAttachment> {\n const id = this.newId();\n const fileName = safeFileName(input.filename);\n const path = this.pathFor(input.actor, id, fileName);\n await this.deps.storage\n .disk(this.diskName)\n .put(path, input.data, { contentType: input.contentType });\n const record = await this.saveRecord({\n id,\n actor: input.actor,\n fileName,\n mimeType: input.contentType,\n size: input.sizeBytes,\n path,\n state: { status: 'ready' },\n });\n this.announce(record);\n return this.toAttachment(record, { forModel: false });\n }\n\n /**\n * The attachment for `mediaId`, or `null` when it is unknown, not this actor's, or its bytes have\n * not (fully) arrived. The actor may use media they own, and media a message in one of THEIR\n * threads already carries (a fork, a regenerate) — never anything else.\n */\n async resolve(input: ResolveAttachmentInput): Promise<MessageAttachment | null> {\n const record = await this.deps.store.find(input.mediaId);\n if (record === null || !this.isOurs(record)) return null;\n if (!(await this.canUse(record, input.actor))) return null;\n const ready = await this.ensureReady(record);\n if (typeof ready === 'string') return null;\n return this.toAttachment(ready, { forModel: true });\n }\n\n async list(input: ListStagedAttachmentsInput): Promise<StagedAttachment[]> {\n const records = await this.deps.store.listByOwner(\n this.ownerType,\n input.actor.id,\n this.collection,\n );\n const entries = records\n .map((record) => ({\n mediaId: record.id,\n name: record.fileName,\n contentType: record.mimeType,\n sizeBytes: record.size,\n createdAt: new Date(record.createdAt).toISOString(),\n }))\n .filter((entry) => input.stagedBefore === undefined || entry.createdAt < input.stagedBefore)\n .sort((a, b) => b.createdAt.localeCompare(a.createdAt));\n return input.limit === undefined ? entries : entries.slice(0, input.limit);\n }\n\n private validate(filename: string, contentType: string, size: number): void {\n if (typeof filename !== 'string' || filename.trim().length === 0) {\n throw new BadRequestException('filename is required');\n }\n if (typeof contentType !== 'string' || !this.allowed.includes(contentType)) {\n throw new UnsupportedMediaTypeException(\n `content type \"${String(contentType)}\" is not allowed (allowed: ${this.allowed.join(', ')})`,\n );\n }\n if (typeof size !== 'number' || !Number.isInteger(size) || size <= 0) {\n throw new BadRequestException('size must be a positive integer byte count');\n }\n if (size > this.maxBytes) {\n throw new PayloadTooLargeException(`file exceeds the ${this.maxBytes}-byte limit`);\n }\n }\n\n private pathFor(actor: Actor, id: string, fileName: string): string {\n return `${this.ownerType}/${encodeURIComponent(actor.id)}/${this.collection}/${id}/${fileName}`;\n }\n\n private async saveRecord(input: {\n id: string;\n actor: Actor;\n fileName: string;\n mimeType: string;\n size: number;\n path: string;\n state: AgentAttachmentState;\n }): Promise<MediaRecord> {\n const timestamp = this.now();\n return this.deps.store.save({\n id: input.id,\n ownerType: this.ownerType,\n ownerId: input.actor.id,\n collection: this.collection,\n name: stripExtension(input.fileName),\n fileName: input.fileName,\n mimeType: input.mimeType,\n size: input.size,\n disk: this.diskName,\n path: input.path,\n order: await this.deps.store.nextOrder(this.ownerType, input.actor.id, this.collection),\n customProperties: { agentAttachment: input.state },\n conversions: {},\n createdAt: timestamp,\n updatedAt: timestamp,\n });\n }\n\n private isOurs(record: MediaRecord): boolean {\n return record.ownerType === this.ownerType && record.collection === this.collection;\n }\n\n private async ownRecord(mediaId: string, actor: Actor): Promise<MediaRecord | null> {\n const record = await this.deps.store.find(mediaId);\n if (record === null || !this.isOurs(record) || record.ownerId !== actor.id) return null;\n return record;\n }\n\n private async canUse(record: MediaRecord, actor: Actor): Promise<boolean> {\n const allowed = await this.defaultAccess(record, actor);\n const custom = this.options.canAccess;\n return custom === undefined ? allowed : (await custom({ record, actor, allowed })) === true;\n }\n\n private async defaultAccess(record: MediaRecord, actor: Actor): Promise<boolean> {\n if (record.ownerId === actor.id) return true;\n const referenced = this.deps.agentStore?.referencedMediaIds?.bind(this.deps.agentStore);\n if (referenced === undefined) return false;\n return (await referenced(actor.id, [record.id])).includes(record.id);\n }\n\n /**\n * A ready record as-is; a pending one promoted to ready once its bytes are on the disk at exactly\n * the declared size. `'missing'` while they are not; `'mismatch'` when they arrived at another size.\n */\n private async ensureReady(record: MediaRecord): Promise<MediaRecord | 'missing' | 'mismatch'> {\n if (stateOf(record).status === 'ready') return record;\n const disk = this.deps.storage.disk(record.disk);\n if (!(await disk.exists(record.path))) return 'missing';\n const size = await disk.size(record.path);\n if (size !== record.size || size > this.maxBytes) return 'mismatch';\n const ready = await this.deps.store.save({\n ...record,\n customProperties: { ...record.customProperties, agentAttachment: { status: 'ready' } },\n updatedAt: this.now(),\n });\n this.announce(ready);\n return ready;\n }\n\n private announce(record: MediaRecord): void {\n if (this.options.indexForRag !== true) return;\n publishMedia('attach', {\n id: record.id,\n ownerType: record.ownerType,\n ownerId: record.ownerId,\n collection: record.collection,\n disk: record.disk,\n path: record.path,\n size: record.size,\n mimeType: record.mimeType,\n });\n }\n\n /**\n * `forModel: false` is what goes back to the uploader: never inline bytes (they already have the\n * file), so an inline-only disk answers with an empty url there.\n */\n private async toAttachment(\n record: MediaRecord,\n { forModel }: { forModel: boolean },\n ): Promise<MessageAttachment> {\n const base = { mediaId: record.id, contentType: record.mimeType, name: record.fileName };\n const disk = this.deps.storage.disk(record.disk);\n if (this.options.resolveUrl !== undefined) {\n return { ...base, url: await this.options.resolveUrl(record, { disk }) };\n }\n if (this.options.visibility === 'public') {\n return { ...base, url: await disk.url(record.path) };\n }\n if (disk.capabilities.presign) {\n const ttl = this.options.urlExpiresInSeconds ?? DEFAULT_AGENT_MEDIA_URL_TTL_SECONDS;\n return {\n ...base,\n url: await disk.temporaryUrl(record.path, ttl, { responseContentType: record.mimeType }),\n };\n }\n if (!forModel) return { ...base, url: '' };\n if (!this.warnedInline) {\n this.warnedInline = true;\n this.logger.warn(\n `disk \"${record.disk}\" cannot presign urls: attachments reach the model inline (data: urls, persisted with each message). Use a presign-capable disk or resolveUrl in production.`,\n );\n }\n const bytes = await disk.get(record.path);\n return { ...base, url: `data:${record.mimeType};base64,${bytes.toString('base64')}` };\n }\n}\n\nfunction stateOf(record: MediaRecord): AgentAttachmentState {\n const state = record.customProperties.agentAttachment as AgentAttachmentState | undefined;\n return state ?? { status: 'ready' };\n}\n\n/** The last path segment, stripped of anything a disk would read as a directory or control. */\nfunction safeFileName(filename: string): string {\n const last = filename.split(/[\\\\/]/).pop() ?? '';\n // biome-ignore lint/suspicious/noControlCharactersInRegex: stripping control chars is the point.\n const cleaned = last.replace(/[\\u0000-\\u001f\\u007f]/g, '').trim();\n return cleaned === '' || cleaned === '.' || cleaned === '..' ? 'file' : cleaned;\n}\n\nfunction stripExtension(fileName: string): string {\n const dot = fileName.lastIndexOf('.');\n return dot > 0 ? fileName.slice(0, dot) : fileName;\n}\n","import type { Actor } from '@dudousxd/nestjs-agent-core';\nimport type { MediaRecord, StorageDriver } from '@dudousxd/nestjs-media';\nimport type {\n CanActivate,\n DynamicModule,\n InjectionToken,\n OptionalFactoryDependency,\n Type,\n} from '@nestjs/common';\n\n/** Default media-library collection chat attachments are filed under. */\nexport const DEFAULT_AGENT_MEDIA_COLLECTION = 'agent-attachments';\n\n/** Default `ownerType` of the media records — the owner id is the actor's id. */\nexport const DEFAULT_AGENT_MEDIA_OWNER_TYPE = 'agent-actor';\n\n/** Default lifetime of a presigned url handed to the model provider (7 days — S3's ceiling). */\nexport const DEFAULT_AGENT_MEDIA_URL_TTL_SECONDS = 7 * 24 * 60 * 60;\n\n/** Default tus base path — `MediaModule`'s own default for `tus.basePath`. */\nexport const DEFAULT_AGENT_MEDIA_TUS_BASE_PATH = '/media/uploads';\n\n/** What {@link AgentMediaAttachmentsOptions.resolveUrl} is handed besides the record. */\nexport interface ResolveMediaUrlContext {\n /** The storage driver the record's bytes live on. */\n disk: StorageDriver;\n}\n\n/** What {@link AgentMediaAttachmentsOptions.canAccess} decides on. */\nexport interface MediaAttachmentAccessInput {\n record: MediaRecord;\n actor: Actor;\n /** The default verdict: the actor owns the record, or a message in their own thread carries it. */\n allowed: boolean;\n}\n\nexport interface AgentMediaAttachmentsOptions {\n /** Media-library collection the records are filed under. Default `'agent-attachments'`. */\n collection?: string;\n /** `ownerType` of the records; `ownerId` is always the actor's id. Default `'agent-actor'`. */\n ownerType?: string;\n /** Storage disk the bytes land on. Default: the `MediaModule`'s default disk. */\n disk?: string;\n /** Per-file size cap. Default 20 MiB — the same as `POST /agent/attachments`. */\n maxBytes?: number;\n /**\n * Content types a chat may attach. Default: the same allowlist as `POST /agent/attachments`\n * (png/jpeg/gif/webp images, pdf, plain text, csv).\n */\n allowedContentTypes?: readonly string[];\n /**\n * How the model provider gets at the bytes.\n * - `'private'` (default): a presigned url (`temporaryUrl`) when the disk can mint one (S3), else\n * the bytes inline as a `data:` url. Inline works everywhere but is persisted with the message —\n * fine for local development, use a presign-capable disk (or `resolveUrl`) in production.\n * - `'public'`: the disk's stable public `url()` — only for a bucket the provider may read.\n */\n visibility?: 'private' | 'public';\n /** Lifetime of a presigned url, in seconds. Default 7 days. */\n urlExpiresInSeconds?: number;\n /**\n * Take over url minting entirely (e.g. a signed route on your own API). Wins over `visibility`.\n * SECURITY: the url is fetched server-side by the model provider — build it from the record only.\n */\n resolveUrl?: (record: MediaRecord, context: ResolveMediaUrlContext) => string | Promise<string>;\n /**\n * Who may send (resolve) a record in a chat turn. Default: `allowed` — the owner, or anyone whose\n * own thread already carries it. Return `allowed || …` to widen that (a shared team space),\n * `allowed && …` to narrow it. Only `resolve` consults this; completing and discarding an upload\n * stay the uploader's.\n */\n canAccess?: (input: MediaAttachmentAccessInput) => boolean | Promise<boolean>;\n /**\n * The public path of `MediaModule`'s tus endpoint (its `tus.basePath`, global prefix included) —\n * used to build the `location` a client streams the bytes to. Default `'/media/uploads'`.\n */\n tusBasePath?: string;\n /**\n * Announce each ready attachment on `aviary:media:attach` (and its removal on\n * `aviary:media:delete`), so `@dudousxd/nestjs-agent-rag-media` indexes it for retrieval. Off by\n * default: a chat attachment is private to the person who sent it, and indexing it is only safe\n * where retrieval is scoped by owner (`FilteredRetriever` on `ownerType`/`ownerId`).\n */\n indexForRag?: boolean;\n /** Injectable clock for tests. */\n clock?: () => Date;\n /** Injectable id generator for tests. Default `crypto.randomUUID`. */\n idGenerator?: () => string;\n}\n\n/** Build-time wiring shared by `forRoot` / `forRootAsync` — Nest mounts controllers before any factory runs. */\nexport interface AgentMediaAttachmentsMountOptions {\n /**\n * Route prefix — must match `AgentModule`'s `path`. The upload routes mount under\n * `<path>/attachments/uploads`. Default `'agent'`.\n */\n path?: string;\n /** Guards for the upload routes (mirror `AgentModule`'s `guards`). */\n guards?: Type<CanActivate>[];\n /**\n * Mount the resumable-upload routes. Default `true`. `false` keeps only the staging store — for a\n * host that opens tus sessions its own way and only wants resolve/list backed by media.\n */\n routes?: boolean;\n}\n\nexport interface AgentMediaAttachmentsModuleOptions\n extends AgentMediaAttachmentsOptions,\n AgentMediaAttachmentsMountOptions {}\n\nexport interface AgentMediaAttachmentsAsyncOptions extends AgentMediaAttachmentsMountOptions {\n imports?: DynamicModule['imports'];\n inject?: Array<InjectionToken | OptionalFactoryDependency>;\n useFactory: (\n ...args: never[]\n ) => AgentMediaAttachmentsOptions | Promise<AgentMediaAttachmentsOptions>;\n}\n","import {\n AGENT_ATTACHMENT_STAGING,\n AGENT_STORE,\n type AgentStore,\n} from '@dudousxd/nestjs-agent-core';\nimport {\n MEDIA_STORAGE_SHARED,\n MEDIA_STORE,\n MEDIA_UPLOADS,\n type MediaStore,\n type ResumableUploadManager,\n type StorageManager,\n} from '@dudousxd/nestjs-media';\nimport {\n type CanActivate,\n type DynamicModule,\n Global,\n Module,\n type Provider,\n type Type,\n} from '@nestjs/common';\nimport { RouterModule } from '@nestjs/core';\nimport { MediaAttachmentStaging } from './media-attachment-staging.js';\nimport type {\n AgentMediaAttachmentsAsyncOptions,\n AgentMediaAttachmentsModuleOptions,\n AgentMediaAttachmentsMountOptions,\n AgentMediaAttachmentsOptions,\n} from './media-attachments.options.js';\nimport { AgentMediaUploadsController } from './media-uploads.controller.js';\nimport { AGENT_MEDIA_ATTACHMENTS, AGENT_MEDIA_ATTACHMENTS_OPTIONS } from './tokens.js';\n\n/** See `agent.module.ts` — Nest's `GUARDS_METADATA` key, inlined for the same ESM reason. */\nconst GUARDS_METADATA = '__guards__';\n\nfunction stagingProvider(routes: boolean): Provider {\n return {\n provide: AGENT_MEDIA_ATTACHMENTS,\n inject: [\n AGENT_MEDIA_ATTACHMENTS_OPTIONS,\n MEDIA_STORAGE_SHARED,\n MEDIA_STORE,\n MEDIA_UPLOADS,\n { token: AGENT_STORE, optional: true },\n ],\n useFactory: (\n options: AgentMediaAttachmentsOptions,\n storage: StorageManager,\n store: MediaStore | null,\n uploads: ResumableUploadManager | null,\n agentStore: AgentStore | undefined,\n ) => {\n if (store === null) {\n throw new Error(\n 'AgentMediaAttachmentsModule: MediaModule has no `store`. Chat attachments are media ' +\n 'records, so configure MediaModule with a MediaStore (e.g. a database adapter).',\n );\n }\n return new MediaAttachmentStaging(\n {\n storage,\n store,\n uploads,\n resumableRoutes: routes,\n ...(agentStore !== undefined ? { agentStore } : {}),\n },\n options,\n );\n },\n };\n}\n\nfunction build(\n mount: AgentMediaAttachmentsMountOptions,\n optionsProvider: Provider,\n imports: DynamicModule['imports'] = [],\n): DynamicModule {\n const routes = mount.routes !== false;\n Reflect.defineMetadata(GUARDS_METADATA, mount.guards ?? [], AgentMediaUploadsController);\n const guards: Type<CanActivate>[] = [...new Set(mount.guards ?? [])];\n return {\n module: AgentMediaAttachmentsModule,\n global: true,\n imports: [\n ...imports,\n ...(routes\n ? [\n RouterModule.register([\n { path: mount.path ?? 'agent', module: AgentMediaAttachmentsModule },\n ]),\n ]\n : []),\n ],\n controllers: routes ? [AgentMediaUploadsController] : [],\n providers: [\n optionsProvider,\n stagingProvider(routes),\n { provide: AGENT_ATTACHMENT_STAGING, useExisting: AGENT_MEDIA_ATTACHMENTS },\n { provide: MediaAttachmentStaging, useExisting: AGENT_MEDIA_ATTACHMENTS },\n ...guards,\n ],\n exports: [AGENT_ATTACHMENT_STAGING, AGENT_MEDIA_ATTACHMENTS, MediaAttachmentStaging],\n };\n}\n\n/**\n * Chat attachments on `@dudousxd/nestjs-media`: binds `AGENT_ATTACHMENT_STAGING` to a\n * media-backed store and mounts the resumable-upload routes (`<path>/attachments/uploads`).\n * Import it next to `AgentModule` and a `MediaModule` configured with `store`, `uploadSessions`\n * and `tus`.\n */\n@Global()\n@Module({})\nexport class AgentMediaAttachmentsModule {\n static forRoot(options: AgentMediaAttachmentsModuleOptions = {}): DynamicModule {\n return build(options, { provide: AGENT_MEDIA_ATTACHMENTS_OPTIONS, useValue: options });\n }\n\n static forRootAsync(options: AgentMediaAttachmentsAsyncOptions): DynamicModule {\n return build(\n options,\n {\n provide: AGENT_MEDIA_ATTACHMENTS_OPTIONS,\n inject: options.inject ?? [],\n useFactory: options.useFactory,\n },\n options.imports,\n );\n }\n}\n","import {\n AGENT_ACTOR_RESOLVER,\n type ActorResolver,\n type MessageAttachment,\n} from '@dudousxd/nestjs-agent-core';\nimport {\n BadRequestException,\n Body,\n Controller,\n Delete,\n HttpCode,\n Inject,\n NotFoundException,\n Param,\n Post,\n Req,\n} from '@nestjs/common';\nimport type { Request } from 'express';\nimport type { BeginMediaUploadResult, MediaAttachmentStaging } from './media-attachment-staging.js';\nimport { AGENT_MEDIA_ATTACHMENTS } from './tokens.js';\n\n/**\n * The agent's half of a resumable chat upload. It never carries a byte: it opens an owned,\n * validated tus session on `MediaModule` (`POST`), confirms the bytes arrived (`POST :id/complete`)\n * and drops an attachment the user removed (`DELETE :id`). The bytes themselves go to\n * nestjs-media's own tus endpoint (`PATCH <tusBasePath>/:uploadId`), which the host guards through\n * `MediaModule`'s `guards`.\n *\n * Mounted at `<AgentModule path>/attachments/uploads`.\n */\n@Controller('attachments/uploads')\nexport class AgentMediaUploadsController {\n constructor(\n @Inject(AGENT_ACTOR_RESOLVER) private readonly actorResolver: ActorResolver,\n @Inject(AGENT_MEDIA_ATTACHMENTS) private readonly staging: MediaAttachmentStaging,\n ) {}\n\n /** `{ filename, contentType, size }` → `{ mediaId, uploadId, location }`. */\n @Post()\n async begin(@Req() req: Request, @Body() body: unknown): Promise<BeginMediaUploadResult> {\n if (typeof body !== 'object' || body === null) {\n throw new BadRequestException('body must be { filename, contentType, size }');\n }\n const { filename, contentType, size } = body as Record<string, unknown>;\n const actor = await this.actorResolver.resolve(req);\n return this.staging.beginUpload({\n actor,\n filename: filename as string,\n contentType: contentType as string,\n size: size as number,\n });\n }\n\n @Post(':mediaId/complete')\n @HttpCode(200)\n async complete(\n @Req() req: Request,\n @Param('mediaId') mediaId: string,\n ): Promise<MessageAttachment> {\n const actor = await this.actorResolver.resolve(req);\n const attachment = await this.staging.completeUpload({ actor, mediaId });\n if (attachment === null) throw new NotFoundException(`attachment ${mediaId} not found`);\n return attachment;\n }\n\n @Delete(':mediaId')\n @HttpCode(204)\n async discard(@Req() req: Request, @Param('mediaId') mediaId: string): Promise<void> {\n const actor = await this.actorResolver.resolve(req);\n if (!(await this.staging.discard({ actor, mediaId }))) {\n throw new NotFoundException(`attachment ${mediaId} not found`);\n }\n }\n}\n","/** The {@link import('./media-attachment-staging.js').MediaAttachmentStaging} instance — inject it for `remove()` in a sweep. */\nexport const AGENT_MEDIA_ATTACHMENTS = Symbol.for('@dudousxd/nestjs-agent:media-attachments');\n\n/** The resolved {@link import('./media-attachments.options.js').AgentMediaAttachmentsOptions}. */\nexport const AGENT_MEDIA_ATTACHMENTS_OPTIONS = Symbol.for(\n '@dudousxd/nestjs-agent:media-attachments-options',\n);\n"],"mappings":"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","names":["MediaAttachmentStaging","DEFAULT_AGENT_MEDIA_COLLECTION","DEFAULT_AGENT_MEDIA_OWNER_TYPE","DEFAULT_AGENT_MEDIA_URL_TTL_SECONDS","DEFAULT_AGENT_MEDIA_TUS_BASE_PATH","DEFAULT_MAX_BYTES","DEFAULT_ALLOWED_CONTENT_TYPES","MediaAttachmentStaging","logger","Logger","name","collection","ownerType","maxBytes","allowed","now","newId","warnedInline","deps","options","DEFAULT_AGENT_MEDIA_COLLECTION","DEFAULT_AGENT_MEDIA_OWNER_TYPE","allowedContentTypes","clock","Date","idGenerator","randomUUID","describe","upload","resumableRoutes","diskName","disk","storage","defaultDisk","beginUpload","input","uploads","NotImplementedException","validate","filename","contentType","size","id","fileName","safeFileName","path","pathFor","actor","session","createUpload","key","metadata","agentMediaId","saveRecord","mimeType","state","status","uploadId","base","tusBasePath","DEFAULT_AGENT_MEDIA_TUS_BASE_PATH","replace","mediaId","location","completeUpload","record","ownRecord","ready","ensureReady","ConflictException","remove","UnprocessableEntityException","toAttachment","forModel","discard","store","find","isOurs","stateOf","undefined","abort","catch","delete","indexForRag","publishMedia","ownerId","stage","put","data","sizeBytes","announce","resolve","canUse","list","records","listByOwner","entries","map","createdAt","toISOString","filter","entry","stagedBefore","sort","a","b","localeCompare","limit","slice","trim","length","BadRequestException","includes","UnsupportedMediaTypeException","String","join","Number","isInteger","PayloadTooLargeException","encodeURIComponent","timestamp","save","stripExtension","order","nextOrder","customProperties","agentAttachment","conversions","updatedAt","defaultAccess","custom","canAccess","referenced","agentStore","referencedMediaIds","bind","exists","resolveUrl","url","visibility","capabilities","presign","ttl","urlExpiresInSeconds","DEFAULT_AGENT_MEDIA_URL_TTL_SECONDS","temporaryUrl","responseContentType","warn","bytes","get","toString","last","split","pop","cleaned","dot","lastIndexOf","import_nestjs_agent_core","import_nestjs_media","import_common","import_common","AGENT_MEDIA_ATTACHMENTS","Symbol","for","AGENT_MEDIA_ATTACHMENTS_OPTIONS","AgentMediaUploadsController","actorResolver","staging","begin","req","body","BadRequestException","filename","contentType","size","actor","resolve","beginUpload","complete","mediaId","attachment","completeUpload","NotFoundException","discard","GUARDS_METADATA","stagingProvider","routes","provide","AGENT_MEDIA_ATTACHMENTS","inject","AGENT_MEDIA_ATTACHMENTS_OPTIONS","MEDIA_STORAGE_SHARED","MEDIA_STORE","MEDIA_UPLOADS","token","AGENT_STORE","optional","useFactory","options","storage","store","uploads","agentStore","Error","MediaAttachmentStaging","resumableRoutes","undefined","build","mount","optionsProvider","imports","Reflect","defineMetadata","guards","AgentMediaUploadsController","Set","module","AgentMediaAttachmentsModule","global","RouterModule","register","path","controllers","providers","AGENT_ATTACHMENT_STAGING","useExisting","exports","forRoot","useValue","forRootAsync"]}
|
package/dist/media/index.d.cts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Actor, AttachmentStagingStore, AgentStore, MessageAttachment, StageAttachmentInput, ResolveAttachmentInput, ListStagedAttachmentsInput, StagedAttachment, ActorResolver } from '@dudousxd/nestjs-agent-core';
|
|
1
|
+
import { Actor, AttachmentStagingStore, AgentStore, AttachmentStagingDescription, MessageAttachment, StageAttachmentInput, ResolveAttachmentInput, ListStagedAttachmentsInput, StagedAttachment, ActorResolver } from '@dudousxd/nestjs-agent-core';
|
|
2
2
|
import { MediaRecord, StorageDriver, StorageManager, MediaStore, ResumableUploadManager } from '@dudousxd/nestjs-media';
|
|
3
3
|
import { Type, CanActivate, DynamicModule, InjectionToken, OptionalFactoryDependency } from '@nestjs/common';
|
|
4
4
|
import { Request } from 'express';
|
|
@@ -107,6 +107,8 @@ interface MediaAttachmentStagingDeps {
|
|
|
107
107
|
uploads: ResumableUploadManager | null;
|
|
108
108
|
/** Lets media referenced from the actor's own thread resolve (see {@link MediaAttachmentStaging.resolve}). */
|
|
109
109
|
agentStore?: AgentStore;
|
|
110
|
+
/** The resumable-upload routes are mounted — what {@link MediaAttachmentStaging.describe} tells clients. */
|
|
111
|
+
resumableRoutes?: boolean;
|
|
110
112
|
}
|
|
111
113
|
interface BeginMediaUploadInput {
|
|
112
114
|
actor: Actor;
|
|
@@ -150,6 +152,8 @@ declare class MediaAttachmentStaging implements AttachmentStagingStore {
|
|
|
150
152
|
private readonly newId;
|
|
151
153
|
private warnedInline;
|
|
152
154
|
constructor(deps: MediaAttachmentStagingDeps, options: AgentMediaAttachmentsOptions);
|
|
155
|
+
/** The limits this store enforces, and how a client uploads to it (see `GET <base>/config`). */
|
|
156
|
+
describe(): AttachmentStagingDescription;
|
|
153
157
|
private get diskName();
|
|
154
158
|
/** Open a tus session for one file, owned by `actor`. Validation happens here, before any byte moves. */
|
|
155
159
|
beginUpload(input: BeginMediaUploadInput): Promise<BeginMediaUploadResult>;
|
package/dist/media/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Actor, AttachmentStagingStore, AgentStore, MessageAttachment, StageAttachmentInput, ResolveAttachmentInput, ListStagedAttachmentsInput, StagedAttachment, ActorResolver } from '@dudousxd/nestjs-agent-core';
|
|
1
|
+
import { Actor, AttachmentStagingStore, AgentStore, AttachmentStagingDescription, MessageAttachment, StageAttachmentInput, ResolveAttachmentInput, ListStagedAttachmentsInput, StagedAttachment, ActorResolver } from '@dudousxd/nestjs-agent-core';
|
|
2
2
|
import { MediaRecord, StorageDriver, StorageManager, MediaStore, ResumableUploadManager } from '@dudousxd/nestjs-media';
|
|
3
3
|
import { Type, CanActivate, DynamicModule, InjectionToken, OptionalFactoryDependency } from '@nestjs/common';
|
|
4
4
|
import { Request } from 'express';
|
|
@@ -107,6 +107,8 @@ interface MediaAttachmentStagingDeps {
|
|
|
107
107
|
uploads: ResumableUploadManager | null;
|
|
108
108
|
/** Lets media referenced from the actor's own thread resolve (see {@link MediaAttachmentStaging.resolve}). */
|
|
109
109
|
agentStore?: AgentStore;
|
|
110
|
+
/** The resumable-upload routes are mounted — what {@link MediaAttachmentStaging.describe} tells clients. */
|
|
111
|
+
resumableRoutes?: boolean;
|
|
110
112
|
}
|
|
111
113
|
interface BeginMediaUploadInput {
|
|
112
114
|
actor: Actor;
|
|
@@ -150,6 +152,8 @@ declare class MediaAttachmentStaging implements AttachmentStagingStore {
|
|
|
150
152
|
private readonly newId;
|
|
151
153
|
private warnedInline;
|
|
152
154
|
constructor(deps: MediaAttachmentStagingDeps, options: AgentMediaAttachmentsOptions);
|
|
155
|
+
/** The limits this store enforces, and how a client uploads to it (see `GET <base>/config`). */
|
|
156
|
+
describe(): AttachmentStagingDescription;
|
|
153
157
|
private get diskName();
|
|
154
158
|
/** Open a tus session for one file, owned by `actor`. Validation happens here, before any byte moves. */
|
|
155
159
|
beginUpload(input: BeginMediaUploadInput): Promise<BeginMediaUploadResult>;
|
package/dist/media/index.js
CHANGED
|
@@ -47,6 +47,14 @@ var MediaAttachmentStaging2 = class _MediaAttachmentStaging {
|
|
|
47
47
|
this.now = options.clock ?? (() => /* @__PURE__ */ new Date());
|
|
48
48
|
this.newId = options.idGenerator ?? (() => randomUUID());
|
|
49
49
|
}
|
|
50
|
+
/** The limits this store enforces, and how a client uploads to it (see `GET <base>/config`). */
|
|
51
|
+
describe() {
|
|
52
|
+
return {
|
|
53
|
+
maxBytes: this.maxBytes,
|
|
54
|
+
allowedContentTypes: this.allowed,
|
|
55
|
+
upload: this.deps.resumableRoutes === false ? "multipart" : "resumable"
|
|
56
|
+
};
|
|
57
|
+
}
|
|
50
58
|
get diskName() {
|
|
51
59
|
return this.options.disk ?? this.deps.storage.defaultDisk;
|
|
52
60
|
}
|
|
@@ -499,7 +507,7 @@ function _ts_decorate2(decorators, target, key, desc) {
|
|
|
499
507
|
}
|
|
500
508
|
__name(_ts_decorate2, "_ts_decorate");
|
|
501
509
|
var GUARDS_METADATA = "__guards__";
|
|
502
|
-
function stagingProvider() {
|
|
510
|
+
function stagingProvider(routes) {
|
|
503
511
|
return {
|
|
504
512
|
provide: AGENT_MEDIA_ATTACHMENTS,
|
|
505
513
|
inject: [
|
|
@@ -520,6 +528,7 @@ function stagingProvider() {
|
|
|
520
528
|
storage,
|
|
521
529
|
store,
|
|
522
530
|
uploads,
|
|
531
|
+
resumableRoutes: routes,
|
|
523
532
|
...agentStore !== void 0 ? {
|
|
524
533
|
agentStore
|
|
525
534
|
} : {}
|
|
@@ -553,7 +562,7 @@ function build(mount, optionsProvider, imports = []) {
|
|
|
553
562
|
] : [],
|
|
554
563
|
providers: [
|
|
555
564
|
optionsProvider,
|
|
556
|
-
stagingProvider(),
|
|
565
|
+
stagingProvider(routes),
|
|
557
566
|
{
|
|
558
567
|
provide: AGENT_ATTACHMENT_STAGING,
|
|
559
568
|
useExisting: AGENT_MEDIA_ATTACHMENTS
|
package/dist/media/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/media/media-attachment-staging.ts","../../src/media/media-attachments.options.ts","../../src/media/media-attachments.module.ts","../../src/media/media-uploads.controller.ts","../../src/media/tokens.ts"],"sourcesContent":["import { randomUUID } from 'node:crypto';\nimport type {\n Actor,\n AgentStore,\n AttachmentStagingStore,\n ListStagedAttachmentsInput,\n MessageAttachment,\n ResolveAttachmentInput,\n StageAttachmentInput,\n StagedAttachment,\n} from '@dudousxd/nestjs-agent-core';\nimport {\n type MediaRecord,\n type MediaStore,\n type ResumableUploadManager,\n type StorageManager,\n publishMedia,\n} from '@dudousxd/nestjs-media';\nimport {\n BadRequestException,\n ConflictException,\n Logger,\n NotImplementedException,\n PayloadTooLargeException,\n UnprocessableEntityException,\n UnsupportedMediaTypeException,\n} from '@nestjs/common';\nimport {\n type AgentMediaAttachmentsOptions,\n DEFAULT_AGENT_MEDIA_COLLECTION,\n DEFAULT_AGENT_MEDIA_OWNER_TYPE,\n DEFAULT_AGENT_MEDIA_TUS_BASE_PATH,\n DEFAULT_AGENT_MEDIA_URL_TTL_SECONDS,\n} from './media-attachments.options.js';\n\n/**\n * Mirrors `DEFAULT_MAX_ATTACHMENT_BYTES` / `DEFAULT_ALLOWED_ATTACHMENT_CONTENT_TYPES` from the\n * `POST /agent/attachments` controller — restated rather than imported so this subpath never pulls\n * the root entry's classes into its own bundle.\n */\nconst DEFAULT_MAX_BYTES = 20 * 1024 * 1024;\nconst DEFAULT_ALLOWED_CONTENT_TYPES: readonly string[] = [\n 'image/png',\n 'image/jpeg',\n 'image/gif',\n 'image/webp',\n 'application/pdf',\n 'text/plain',\n 'text/csv',\n];\n\n/** Where the record's bytes stand. Kept on `customProperties.agentAttachment`. */\ninterface AgentAttachmentState {\n status: 'pending' | 'ready';\n /** The tus session the bytes are streaming through, while `pending`. */\n uploadId?: string;\n}\n\n/** What {@link MediaAttachmentStaging} needs from `@dudousxd/nestjs-media`. */\nexport interface MediaAttachmentStagingDeps {\n storage: StorageManager;\n store: MediaStore;\n /** `null` when `MediaModule` has no `uploadSessions` — resumable uploads are then unavailable. */\n uploads: ResumableUploadManager | null;\n /** Lets media referenced from the actor's own thread resolve (see {@link MediaAttachmentStaging.resolve}). */\n agentStore?: AgentStore;\n}\n\nexport interface BeginMediaUploadInput {\n actor: Actor;\n filename: string;\n contentType: string;\n /** Exact byte length of the file — tus needs it up front, and completion is checked against it. */\n size: number;\n}\n\n/** A tus session opened for one attachment. Stream the bytes to `location`, then complete. */\nexport interface BeginMediaUploadResult {\n /** The attachment's id — what the chat turn will send as `{ mediaId }`. */\n mediaId: string;\n /** The tus upload id. */\n uploadId: string;\n /** The tus resource to `PATCH` the bytes to (relative to the API origin). */\n location: string;\n}\n\n/**\n * {@link AttachmentStagingStore} backed by `@dudousxd/nestjs-media`: every chat attachment is a\n * media-library record (`ownerType` + the actor's id as owner, one collection), its bytes on a\n * media disk.\n *\n * Two ways in, one store:\n * - `beginUpload` → the client streams the bytes to nestjs-media's own tus endpoint → `completeUpload`\n * (resumable, what `@dudousxd/nestjs-agent-react/media` drives);\n * - `stage` — what `POST /agent/attachments` calls with a buffered multipart file, when a host also\n * mounts that route.\n *\n * The record exists from `beginUpload` on, so an upload abandoned half-way is in the actor's\n * inventory ({@link list}) and ages into `AgentService.collectableAttachments` like any unsent file.\n */\nexport class MediaAttachmentStaging implements AttachmentStagingStore {\n private readonly logger = new Logger(MediaAttachmentStaging.name);\n private readonly collection: string;\n private readonly ownerType: string;\n private readonly maxBytes: number;\n private readonly allowed: readonly string[];\n private readonly now: () => Date;\n private readonly newId: () => string;\n private warnedInline = false;\n\n constructor(\n private readonly deps: MediaAttachmentStagingDeps,\n private readonly options: AgentMediaAttachmentsOptions,\n ) {\n this.collection = options.collection ?? DEFAULT_AGENT_MEDIA_COLLECTION;\n this.ownerType = options.ownerType ?? DEFAULT_AGENT_MEDIA_OWNER_TYPE;\n this.maxBytes = options.maxBytes ?? DEFAULT_MAX_BYTES;\n this.allowed = options.allowedContentTypes ?? DEFAULT_ALLOWED_CONTENT_TYPES;\n this.now = options.clock ?? (() => new Date());\n this.newId = options.idGenerator ?? (() => randomUUID());\n }\n\n private get diskName(): string {\n return this.options.disk ?? this.deps.storage.defaultDisk;\n }\n\n /** Open a tus session for one file, owned by `actor`. Validation happens here, before any byte moves. */\n async beginUpload(input: BeginMediaUploadInput): Promise<BeginMediaUploadResult> {\n const uploads = this.deps.uploads;\n if (uploads === null) {\n throw new NotImplementedException(\n 'Resumable attachment uploads need MediaModule configured with `uploadSessions` and `tus`.',\n );\n }\n this.validate(input.filename, input.contentType, input.size);\n const id = this.newId();\n const fileName = safeFileName(input.filename);\n const path = this.pathFor(input.actor, id, fileName);\n const session = await uploads.createUpload({\n disk: this.diskName,\n key: path,\n size: input.size,\n contentType: input.contentType,\n metadata: { agentMediaId: id },\n });\n await this.saveRecord({\n id,\n actor: input.actor,\n fileName,\n mimeType: input.contentType,\n size: input.size,\n path,\n state: { status: 'pending', uploadId: session.id },\n });\n const base = (this.options.tusBasePath ?? DEFAULT_AGENT_MEDIA_TUS_BASE_PATH).replace(\n /\\/+$/,\n '',\n );\n return { mediaId: id, uploadId: session.id, location: `${base}/${session.id}` };\n }\n\n /**\n * Confirm the bytes of a `beginUpload` landed and whole. `null` for an unknown or foreign id;\n * `409` while the bytes are still missing; `422` (and the record dropped) when what arrived is\n * not the size that was declared. Idempotent on a ready record.\n */\n async completeUpload(input: {\n actor: Actor;\n mediaId: string;\n }): Promise<MessageAttachment | null> {\n const record = await this.ownRecord(input.mediaId, input.actor);\n if (record === null) return null;\n const ready = await this.ensureReady(record);\n if (ready === 'missing') {\n throw new ConflictException(`upload ${input.mediaId} has not finished`);\n }\n if (ready === 'mismatch') {\n await this.remove(record.id);\n throw new UnprocessableEntityException(\n `upload ${input.mediaId} does not match its declared size`,\n );\n }\n return this.toAttachment(ready, { forModel: false });\n }\n\n /** The actor drops one of their own attachments — aborting its upload if still in flight. */\n async discard(input: { actor: Actor; mediaId: string }): Promise<boolean> {\n const record = await this.ownRecord(input.mediaId, input.actor);\n if (record === null) return false;\n await this.remove(record.id);\n return true;\n }\n\n /**\n * Delete an attachment's bytes, its in-flight tus session and its record. NO ownership check —\n * the host's sweep calls this for what `AgentService.collectableAttachments` returned.\n */\n async remove(mediaId: string): Promise<void> {\n const record = await this.deps.store.find(mediaId);\n if (record === null || !this.isOurs(record)) return;\n const state = stateOf(record);\n if (state.status === 'pending' && state.uploadId !== undefined) {\n await this.deps.uploads?.abort(state.uploadId).catch(() => undefined);\n }\n await this.deps.storage\n .disk(record.disk)\n .delete(record.path)\n .catch(() => undefined);\n await this.deps.store.delete(record.id);\n if (this.options.indexForRag === true && state.status === 'ready') {\n publishMedia('delete', {\n id: record.id,\n ownerType: record.ownerType,\n ownerId: record.ownerId,\n });\n }\n }\n\n async stage(input: StageAttachmentInput): Promise<MessageAttachment> {\n const id = this.newId();\n const fileName = safeFileName(input.filename);\n const path = this.pathFor(input.actor, id, fileName);\n await this.deps.storage\n .disk(this.diskName)\n .put(path, input.data, { contentType: input.contentType });\n const record = await this.saveRecord({\n id,\n actor: input.actor,\n fileName,\n mimeType: input.contentType,\n size: input.sizeBytes,\n path,\n state: { status: 'ready' },\n });\n this.announce(record);\n return this.toAttachment(record, { forModel: false });\n }\n\n /**\n * The attachment for `mediaId`, or `null` when it is unknown, not this actor's, or its bytes have\n * not (fully) arrived. The actor may use media they own, and media a message in one of THEIR\n * threads already carries (a fork, a regenerate) — never anything else.\n */\n async resolve(input: ResolveAttachmentInput): Promise<MessageAttachment | null> {\n const record = await this.deps.store.find(input.mediaId);\n if (record === null || !this.isOurs(record)) return null;\n if (!(await this.canUse(record, input.actor))) return null;\n const ready = await this.ensureReady(record);\n if (typeof ready === 'string') return null;\n return this.toAttachment(ready, { forModel: true });\n }\n\n async list(input: ListStagedAttachmentsInput): Promise<StagedAttachment[]> {\n const records = await this.deps.store.listByOwner(\n this.ownerType,\n input.actor.id,\n this.collection,\n );\n const entries = records\n .map((record) => ({\n mediaId: record.id,\n name: record.fileName,\n contentType: record.mimeType,\n sizeBytes: record.size,\n createdAt: new Date(record.createdAt).toISOString(),\n }))\n .filter((entry) => input.stagedBefore === undefined || entry.createdAt < input.stagedBefore)\n .sort((a, b) => b.createdAt.localeCompare(a.createdAt));\n return input.limit === undefined ? entries : entries.slice(0, input.limit);\n }\n\n private validate(filename: string, contentType: string, size: number): void {\n if (typeof filename !== 'string' || filename.trim().length === 0) {\n throw new BadRequestException('filename is required');\n }\n if (typeof contentType !== 'string' || !this.allowed.includes(contentType)) {\n throw new UnsupportedMediaTypeException(\n `content type \"${String(contentType)}\" is not allowed (allowed: ${this.allowed.join(', ')})`,\n );\n }\n if (typeof size !== 'number' || !Number.isInteger(size) || size <= 0) {\n throw new BadRequestException('size must be a positive integer byte count');\n }\n if (size > this.maxBytes) {\n throw new PayloadTooLargeException(`file exceeds the ${this.maxBytes}-byte limit`);\n }\n }\n\n private pathFor(actor: Actor, id: string, fileName: string): string {\n return `${this.ownerType}/${encodeURIComponent(actor.id)}/${this.collection}/${id}/${fileName}`;\n }\n\n private async saveRecord(input: {\n id: string;\n actor: Actor;\n fileName: string;\n mimeType: string;\n size: number;\n path: string;\n state: AgentAttachmentState;\n }): Promise<MediaRecord> {\n const timestamp = this.now();\n return this.deps.store.save({\n id: input.id,\n ownerType: this.ownerType,\n ownerId: input.actor.id,\n collection: this.collection,\n name: stripExtension(input.fileName),\n fileName: input.fileName,\n mimeType: input.mimeType,\n size: input.size,\n disk: this.diskName,\n path: input.path,\n order: await this.deps.store.nextOrder(this.ownerType, input.actor.id, this.collection),\n customProperties: { agentAttachment: input.state },\n conversions: {},\n createdAt: timestamp,\n updatedAt: timestamp,\n });\n }\n\n private isOurs(record: MediaRecord): boolean {\n return record.ownerType === this.ownerType && record.collection === this.collection;\n }\n\n private async ownRecord(mediaId: string, actor: Actor): Promise<MediaRecord | null> {\n const record = await this.deps.store.find(mediaId);\n if (record === null || !this.isOurs(record) || record.ownerId !== actor.id) return null;\n return record;\n }\n\n private async canUse(record: MediaRecord, actor: Actor): Promise<boolean> {\n const allowed = await this.defaultAccess(record, actor);\n const custom = this.options.canAccess;\n return custom === undefined ? allowed : (await custom({ record, actor, allowed })) === true;\n }\n\n private async defaultAccess(record: MediaRecord, actor: Actor): Promise<boolean> {\n if (record.ownerId === actor.id) return true;\n const referenced = this.deps.agentStore?.referencedMediaIds?.bind(this.deps.agentStore);\n if (referenced === undefined) return false;\n return (await referenced(actor.id, [record.id])).includes(record.id);\n }\n\n /**\n * A ready record as-is; a pending one promoted to ready once its bytes are on the disk at exactly\n * the declared size. `'missing'` while they are not; `'mismatch'` when they arrived at another size.\n */\n private async ensureReady(record: MediaRecord): Promise<MediaRecord | 'missing' | 'mismatch'> {\n if (stateOf(record).status === 'ready') return record;\n const disk = this.deps.storage.disk(record.disk);\n if (!(await disk.exists(record.path))) return 'missing';\n const size = await disk.size(record.path);\n if (size !== record.size || size > this.maxBytes) return 'mismatch';\n const ready = await this.deps.store.save({\n ...record,\n customProperties: { ...record.customProperties, agentAttachment: { status: 'ready' } },\n updatedAt: this.now(),\n });\n this.announce(ready);\n return ready;\n }\n\n private announce(record: MediaRecord): void {\n if (this.options.indexForRag !== true) return;\n publishMedia('attach', {\n id: record.id,\n ownerType: record.ownerType,\n ownerId: record.ownerId,\n collection: record.collection,\n disk: record.disk,\n path: record.path,\n size: record.size,\n mimeType: record.mimeType,\n });\n }\n\n /**\n * `forModel: false` is what goes back to the uploader: never inline bytes (they already have the\n * file), so an inline-only disk answers with an empty url there.\n */\n private async toAttachment(\n record: MediaRecord,\n { forModel }: { forModel: boolean },\n ): Promise<MessageAttachment> {\n const base = { mediaId: record.id, contentType: record.mimeType, name: record.fileName };\n const disk = this.deps.storage.disk(record.disk);\n if (this.options.resolveUrl !== undefined) {\n return { ...base, url: await this.options.resolveUrl(record, { disk }) };\n }\n if (this.options.visibility === 'public') {\n return { ...base, url: await disk.url(record.path) };\n }\n if (disk.capabilities.presign) {\n const ttl = this.options.urlExpiresInSeconds ?? DEFAULT_AGENT_MEDIA_URL_TTL_SECONDS;\n return {\n ...base,\n url: await disk.temporaryUrl(record.path, ttl, { responseContentType: record.mimeType }),\n };\n }\n if (!forModel) return { ...base, url: '' };\n if (!this.warnedInline) {\n this.warnedInline = true;\n this.logger.warn(\n `disk \"${record.disk}\" cannot presign urls: attachments reach the model inline (data: urls, persisted with each message). Use a presign-capable disk or resolveUrl in production.`,\n );\n }\n const bytes = await disk.get(record.path);\n return { ...base, url: `data:${record.mimeType};base64,${bytes.toString('base64')}` };\n }\n}\n\nfunction stateOf(record: MediaRecord): AgentAttachmentState {\n const state = record.customProperties.agentAttachment as AgentAttachmentState | undefined;\n return state ?? { status: 'ready' };\n}\n\n/** The last path segment, stripped of anything a disk would read as a directory or control. */\nfunction safeFileName(filename: string): string {\n const last = filename.split(/[\\\\/]/).pop() ?? '';\n // biome-ignore lint/suspicious/noControlCharactersInRegex: stripping control chars is the point.\n const cleaned = last.replace(/[\\u0000-\\u001f\\u007f]/g, '').trim();\n return cleaned === '' || cleaned === '.' || cleaned === '..' ? 'file' : cleaned;\n}\n\nfunction stripExtension(fileName: string): string {\n const dot = fileName.lastIndexOf('.');\n return dot > 0 ? fileName.slice(0, dot) : fileName;\n}\n","import type { Actor } from '@dudousxd/nestjs-agent-core';\nimport type { MediaRecord, StorageDriver } from '@dudousxd/nestjs-media';\nimport type {\n CanActivate,\n DynamicModule,\n InjectionToken,\n OptionalFactoryDependency,\n Type,\n} from '@nestjs/common';\n\n/** Default media-library collection chat attachments are filed under. */\nexport const DEFAULT_AGENT_MEDIA_COLLECTION = 'agent-attachments';\n\n/** Default `ownerType` of the media records — the owner id is the actor's id. */\nexport const DEFAULT_AGENT_MEDIA_OWNER_TYPE = 'agent-actor';\n\n/** Default lifetime of a presigned url handed to the model provider (7 days — S3's ceiling). */\nexport const DEFAULT_AGENT_MEDIA_URL_TTL_SECONDS = 7 * 24 * 60 * 60;\n\n/** Default tus base path — `MediaModule`'s own default for `tus.basePath`. */\nexport const DEFAULT_AGENT_MEDIA_TUS_BASE_PATH = '/media/uploads';\n\n/** What {@link AgentMediaAttachmentsOptions.resolveUrl} is handed besides the record. */\nexport interface ResolveMediaUrlContext {\n /** The storage driver the record's bytes live on. */\n disk: StorageDriver;\n}\n\n/** What {@link AgentMediaAttachmentsOptions.canAccess} decides on. */\nexport interface MediaAttachmentAccessInput {\n record: MediaRecord;\n actor: Actor;\n /** The default verdict: the actor owns the record, or a message in their own thread carries it. */\n allowed: boolean;\n}\n\nexport interface AgentMediaAttachmentsOptions {\n /** Media-library collection the records are filed under. Default `'agent-attachments'`. */\n collection?: string;\n /** `ownerType` of the records; `ownerId` is always the actor's id. Default `'agent-actor'`. */\n ownerType?: string;\n /** Storage disk the bytes land on. Default: the `MediaModule`'s default disk. */\n disk?: string;\n /** Per-file size cap. Default 20 MiB — the same as `POST /agent/attachments`. */\n maxBytes?: number;\n /**\n * Content types a chat may attach. Default: the same allowlist as `POST /agent/attachments`\n * (png/jpeg/gif/webp images, pdf, plain text, csv).\n */\n allowedContentTypes?: readonly string[];\n /**\n * How the model provider gets at the bytes.\n * - `'private'` (default): a presigned url (`temporaryUrl`) when the disk can mint one (S3), else\n * the bytes inline as a `data:` url. Inline works everywhere but is persisted with the message —\n * fine for local development, use a presign-capable disk (or `resolveUrl`) in production.\n * - `'public'`: the disk's stable public `url()` — only for a bucket the provider may read.\n */\n visibility?: 'private' | 'public';\n /** Lifetime of a presigned url, in seconds. Default 7 days. */\n urlExpiresInSeconds?: number;\n /**\n * Take over url minting entirely (e.g. a signed route on your own API). Wins over `visibility`.\n * SECURITY: the url is fetched server-side by the model provider — build it from the record only.\n */\n resolveUrl?: (record: MediaRecord, context: ResolveMediaUrlContext) => string | Promise<string>;\n /**\n * Who may send (resolve) a record in a chat turn. Default: `allowed` — the owner, or anyone whose\n * own thread already carries it. Return `allowed || …` to widen that (a shared team space),\n * `allowed && …` to narrow it. Only `resolve` consults this; completing and discarding an upload\n * stay the uploader's.\n */\n canAccess?: (input: MediaAttachmentAccessInput) => boolean | Promise<boolean>;\n /**\n * The public path of `MediaModule`'s tus endpoint (its `tus.basePath`, global prefix included) —\n * used to build the `location` a client streams the bytes to. Default `'/media/uploads'`.\n */\n tusBasePath?: string;\n /**\n * Announce each ready attachment on `aviary:media:attach` (and its removal on\n * `aviary:media:delete`), so `@dudousxd/nestjs-agent-rag-media` indexes it for retrieval. Off by\n * default: a chat attachment is private to the person who sent it, and indexing it is only safe\n * where retrieval is scoped by owner (`FilteredRetriever` on `ownerType`/`ownerId`).\n */\n indexForRag?: boolean;\n /** Injectable clock for tests. */\n clock?: () => Date;\n /** Injectable id generator for tests. Default `crypto.randomUUID`. */\n idGenerator?: () => string;\n}\n\n/** Build-time wiring shared by `forRoot` / `forRootAsync` — Nest mounts controllers before any factory runs. */\nexport interface AgentMediaAttachmentsMountOptions {\n /**\n * Route prefix — must match `AgentModule`'s `path`. The upload routes mount under\n * `<path>/attachments/uploads`. Default `'agent'`.\n */\n path?: string;\n /** Guards for the upload routes (mirror `AgentModule`'s `guards`). */\n guards?: Type<CanActivate>[];\n /**\n * Mount the resumable-upload routes. Default `true`. `false` keeps only the staging store — for a\n * host that opens tus sessions its own way and only wants resolve/list backed by media.\n */\n routes?: boolean;\n}\n\nexport interface AgentMediaAttachmentsModuleOptions\n extends AgentMediaAttachmentsOptions,\n AgentMediaAttachmentsMountOptions {}\n\nexport interface AgentMediaAttachmentsAsyncOptions extends AgentMediaAttachmentsMountOptions {\n imports?: DynamicModule['imports'];\n inject?: Array<InjectionToken | OptionalFactoryDependency>;\n useFactory: (\n ...args: never[]\n ) => AgentMediaAttachmentsOptions | Promise<AgentMediaAttachmentsOptions>;\n}\n","import {\n AGENT_ATTACHMENT_STAGING,\n AGENT_STORE,\n type AgentStore,\n} from '@dudousxd/nestjs-agent-core';\nimport {\n MEDIA_STORAGE_SHARED,\n MEDIA_STORE,\n MEDIA_UPLOADS,\n type MediaStore,\n type ResumableUploadManager,\n type StorageManager,\n} from '@dudousxd/nestjs-media';\nimport {\n type CanActivate,\n type DynamicModule,\n Global,\n Module,\n type Provider,\n type Type,\n} from '@nestjs/common';\nimport { RouterModule } from '@nestjs/core';\nimport { MediaAttachmentStaging } from './media-attachment-staging.js';\nimport type {\n AgentMediaAttachmentsAsyncOptions,\n AgentMediaAttachmentsModuleOptions,\n AgentMediaAttachmentsMountOptions,\n AgentMediaAttachmentsOptions,\n} from './media-attachments.options.js';\nimport { AgentMediaUploadsController } from './media-uploads.controller.js';\nimport { AGENT_MEDIA_ATTACHMENTS, AGENT_MEDIA_ATTACHMENTS_OPTIONS } from './tokens.js';\n\n/** See `agent.module.ts` — Nest's `GUARDS_METADATA` key, inlined for the same ESM reason. */\nconst GUARDS_METADATA = '__guards__';\n\nfunction stagingProvider(): Provider {\n return {\n provide: AGENT_MEDIA_ATTACHMENTS,\n inject: [\n AGENT_MEDIA_ATTACHMENTS_OPTIONS,\n MEDIA_STORAGE_SHARED,\n MEDIA_STORE,\n MEDIA_UPLOADS,\n { token: AGENT_STORE, optional: true },\n ],\n useFactory: (\n options: AgentMediaAttachmentsOptions,\n storage: StorageManager,\n store: MediaStore | null,\n uploads: ResumableUploadManager | null,\n agentStore: AgentStore | undefined,\n ) => {\n if (store === null) {\n throw new Error(\n 'AgentMediaAttachmentsModule: MediaModule has no `store`. Chat attachments are media ' +\n 'records, so configure MediaModule with a MediaStore (e.g. a database adapter).',\n );\n }\n return new MediaAttachmentStaging(\n { storage, store, uploads, ...(agentStore !== undefined ? { agentStore } : {}) },\n options,\n );\n },\n };\n}\n\nfunction build(\n mount: AgentMediaAttachmentsMountOptions,\n optionsProvider: Provider,\n imports: DynamicModule['imports'] = [],\n): DynamicModule {\n const routes = mount.routes !== false;\n Reflect.defineMetadata(GUARDS_METADATA, mount.guards ?? [], AgentMediaUploadsController);\n const guards: Type<CanActivate>[] = [...new Set(mount.guards ?? [])];\n return {\n module: AgentMediaAttachmentsModule,\n global: true,\n imports: [\n ...imports,\n ...(routes\n ? [\n RouterModule.register([\n { path: mount.path ?? 'agent', module: AgentMediaAttachmentsModule },\n ]),\n ]\n : []),\n ],\n controllers: routes ? [AgentMediaUploadsController] : [],\n providers: [\n optionsProvider,\n stagingProvider(),\n { provide: AGENT_ATTACHMENT_STAGING, useExisting: AGENT_MEDIA_ATTACHMENTS },\n { provide: MediaAttachmentStaging, useExisting: AGENT_MEDIA_ATTACHMENTS },\n ...guards,\n ],\n exports: [AGENT_ATTACHMENT_STAGING, AGENT_MEDIA_ATTACHMENTS, MediaAttachmentStaging],\n };\n}\n\n/**\n * Chat attachments on `@dudousxd/nestjs-media`: binds `AGENT_ATTACHMENT_STAGING` to a\n * media-backed store and mounts the resumable-upload routes (`<path>/attachments/uploads`).\n * Import it next to `AgentModule` and a `MediaModule` configured with `store`, `uploadSessions`\n * and `tus`.\n */\n@Global()\n@Module({})\nexport class AgentMediaAttachmentsModule {\n static forRoot(options: AgentMediaAttachmentsModuleOptions = {}): DynamicModule {\n return build(options, { provide: AGENT_MEDIA_ATTACHMENTS_OPTIONS, useValue: options });\n }\n\n static forRootAsync(options: AgentMediaAttachmentsAsyncOptions): DynamicModule {\n return build(\n options,\n {\n provide: AGENT_MEDIA_ATTACHMENTS_OPTIONS,\n inject: options.inject ?? [],\n useFactory: options.useFactory,\n },\n options.imports,\n );\n }\n}\n","import {\n AGENT_ACTOR_RESOLVER,\n type ActorResolver,\n type MessageAttachment,\n} from '@dudousxd/nestjs-agent-core';\nimport {\n BadRequestException,\n Body,\n Controller,\n Delete,\n HttpCode,\n Inject,\n NotFoundException,\n Param,\n Post,\n Req,\n} from '@nestjs/common';\nimport type { Request } from 'express';\nimport type { BeginMediaUploadResult, MediaAttachmentStaging } from './media-attachment-staging.js';\nimport { AGENT_MEDIA_ATTACHMENTS } from './tokens.js';\n\n/**\n * The agent's half of a resumable chat upload. It never carries a byte: it opens an owned,\n * validated tus session on `MediaModule` (`POST`), confirms the bytes arrived (`POST :id/complete`)\n * and drops an attachment the user removed (`DELETE :id`). The bytes themselves go to\n * nestjs-media's own tus endpoint (`PATCH <tusBasePath>/:uploadId`), which the host guards through\n * `MediaModule`'s `guards`.\n *\n * Mounted at `<AgentModule path>/attachments/uploads`.\n */\n@Controller('attachments/uploads')\nexport class AgentMediaUploadsController {\n constructor(\n @Inject(AGENT_ACTOR_RESOLVER) private readonly actorResolver: ActorResolver,\n @Inject(AGENT_MEDIA_ATTACHMENTS) private readonly staging: MediaAttachmentStaging,\n ) {}\n\n /** `{ filename, contentType, size }` → `{ mediaId, uploadId, location }`. */\n @Post()\n async begin(@Req() req: Request, @Body() body: unknown): Promise<BeginMediaUploadResult> {\n if (typeof body !== 'object' || body === null) {\n throw new BadRequestException('body must be { filename, contentType, size }');\n }\n const { filename, contentType, size } = body as Record<string, unknown>;\n const actor = await this.actorResolver.resolve(req);\n return this.staging.beginUpload({\n actor,\n filename: filename as string,\n contentType: contentType as string,\n size: size as number,\n });\n }\n\n @Post(':mediaId/complete')\n @HttpCode(200)\n async complete(\n @Req() req: Request,\n @Param('mediaId') mediaId: string,\n ): Promise<MessageAttachment> {\n const actor = await this.actorResolver.resolve(req);\n const attachment = await this.staging.completeUpload({ actor, mediaId });\n if (attachment === null) throw new NotFoundException(`attachment ${mediaId} not found`);\n return attachment;\n }\n\n @Delete(':mediaId')\n @HttpCode(204)\n async discard(@Req() req: Request, @Param('mediaId') mediaId: string): Promise<void> {\n const actor = await this.actorResolver.resolve(req);\n if (!(await this.staging.discard({ actor, mediaId }))) {\n throw new NotFoundException(`attachment ${mediaId} not found`);\n }\n }\n}\n","/** The {@link import('./media-attachment-staging.js').MediaAttachmentStaging} instance — inject it for `remove()` in a sweep. */\nexport const AGENT_MEDIA_ATTACHMENTS = Symbol.for('@dudousxd/nestjs-agent:media-attachments');\n\n/** The resolved {@link import('./media-attachments.options.js').AgentMediaAttachmentsOptions}. */\nexport const AGENT_MEDIA_ATTACHMENTS_OPTIONS = Symbol.for(\n '@dudousxd/nestjs-agent:media-attachments-options',\n);\n"],"mappings":"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","names":["randomUUID","publishMedia","BadRequestException","ConflictException","Logger","NotImplementedException","PayloadTooLargeException","UnprocessableEntityException","UnsupportedMediaTypeException","DEFAULT_AGENT_MEDIA_COLLECTION","DEFAULT_AGENT_MEDIA_OWNER_TYPE","DEFAULT_AGENT_MEDIA_URL_TTL_SECONDS","DEFAULT_AGENT_MEDIA_TUS_BASE_PATH","DEFAULT_MAX_BYTES","DEFAULT_ALLOWED_CONTENT_TYPES","MediaAttachmentStaging","logger","Logger","name","collection","ownerType","maxBytes","allowed","now","newId","warnedInline","deps","options","DEFAULT_AGENT_MEDIA_COLLECTION","DEFAULT_AGENT_MEDIA_OWNER_TYPE","allowedContentTypes","clock","Date","idGenerator","randomUUID","diskName","disk","storage","defaultDisk","beginUpload","input","uploads","NotImplementedException","validate","filename","contentType","size","id","fileName","safeFileName","path","pathFor","actor","session","createUpload","key","metadata","agentMediaId","saveRecord","mimeType","state","status","uploadId","base","tusBasePath","DEFAULT_AGENT_MEDIA_TUS_BASE_PATH","replace","mediaId","location","completeUpload","record","ownRecord","ready","ensureReady","ConflictException","remove","UnprocessableEntityException","toAttachment","forModel","discard","store","find","isOurs","stateOf","undefined","abort","catch","delete","indexForRag","publishMedia","ownerId","stage","put","data","sizeBytes","announce","resolve","canUse","list","records","listByOwner","entries","map","createdAt","toISOString","filter","entry","stagedBefore","sort","a","b","localeCompare","limit","slice","trim","length","BadRequestException","includes","UnsupportedMediaTypeException","String","join","Number","isInteger","PayloadTooLargeException","encodeURIComponent","timestamp","save","stripExtension","order","nextOrder","customProperties","agentAttachment","conversions","updatedAt","defaultAccess","custom","canAccess","referenced","agentStore","referencedMediaIds","bind","exists","resolveUrl","url","visibility","capabilities","presign","ttl","urlExpiresInSeconds","DEFAULT_AGENT_MEDIA_URL_TTL_SECONDS","temporaryUrl","responseContentType","warn","bytes","get","toString","last","split","pop","cleaned","dot","lastIndexOf","AGENT_ATTACHMENT_STAGING","AGENT_STORE","MEDIA_STORAGE_SHARED","MEDIA_STORE","MEDIA_UPLOADS","Global","Module","RouterModule","AGENT_ACTOR_RESOLVER","BadRequestException","Body","Controller","Delete","HttpCode","Inject","NotFoundException","Param","Post","Req","AGENT_MEDIA_ATTACHMENTS","Symbol","for","AGENT_MEDIA_ATTACHMENTS_OPTIONS","AgentMediaUploadsController","actorResolver","staging","begin","req","body","BadRequestException","filename","contentType","size","actor","resolve","beginUpload","complete","mediaId","attachment","completeUpload","NotFoundException","discard","GUARDS_METADATA","stagingProvider","provide","AGENT_MEDIA_ATTACHMENTS","inject","AGENT_MEDIA_ATTACHMENTS_OPTIONS","MEDIA_STORAGE_SHARED","MEDIA_STORE","MEDIA_UPLOADS","token","AGENT_STORE","optional","useFactory","options","storage","store","uploads","agentStore","Error","MediaAttachmentStaging","undefined","build","mount","optionsProvider","imports","routes","Reflect","defineMetadata","guards","AgentMediaUploadsController","Set","module","AgentMediaAttachmentsModule","global","RouterModule","register","path","controllers","providers","AGENT_ATTACHMENT_STAGING","useExisting","exports","forRoot","useValue","forRootAsync"]}
|
|
1
|
+
{"version":3,"sources":["../../src/media/media-attachment-staging.ts","../../src/media/media-attachments.options.ts","../../src/media/media-attachments.module.ts","../../src/media/media-uploads.controller.ts","../../src/media/tokens.ts"],"sourcesContent":["import { randomUUID } from 'node:crypto';\nimport type {\n Actor,\n AgentStore,\n AttachmentStagingDescription,\n AttachmentStagingStore,\n ListStagedAttachmentsInput,\n MessageAttachment,\n ResolveAttachmentInput,\n StageAttachmentInput,\n StagedAttachment,\n} from '@dudousxd/nestjs-agent-core';\nimport {\n type MediaRecord,\n type MediaStore,\n type ResumableUploadManager,\n type StorageManager,\n publishMedia,\n} from '@dudousxd/nestjs-media';\nimport {\n BadRequestException,\n ConflictException,\n Logger,\n NotImplementedException,\n PayloadTooLargeException,\n UnprocessableEntityException,\n UnsupportedMediaTypeException,\n} from '@nestjs/common';\nimport {\n type AgentMediaAttachmentsOptions,\n DEFAULT_AGENT_MEDIA_COLLECTION,\n DEFAULT_AGENT_MEDIA_OWNER_TYPE,\n DEFAULT_AGENT_MEDIA_TUS_BASE_PATH,\n DEFAULT_AGENT_MEDIA_URL_TTL_SECONDS,\n} from './media-attachments.options.js';\n\n/**\n * Mirrors `DEFAULT_MAX_ATTACHMENT_BYTES` / `DEFAULT_ALLOWED_ATTACHMENT_CONTENT_TYPES` from the\n * `POST /agent/attachments` controller — restated rather than imported so this subpath never pulls\n * the root entry's classes into its own bundle.\n */\nconst DEFAULT_MAX_BYTES = 20 * 1024 * 1024;\nconst DEFAULT_ALLOWED_CONTENT_TYPES: readonly string[] = [\n 'image/png',\n 'image/jpeg',\n 'image/gif',\n 'image/webp',\n 'application/pdf',\n 'text/plain',\n 'text/csv',\n];\n\n/** Where the record's bytes stand. Kept on `customProperties.agentAttachment`. */\ninterface AgentAttachmentState {\n status: 'pending' | 'ready';\n /** The tus session the bytes are streaming through, while `pending`. */\n uploadId?: string;\n}\n\n/** What {@link MediaAttachmentStaging} needs from `@dudousxd/nestjs-media`. */\nexport interface MediaAttachmentStagingDeps {\n storage: StorageManager;\n store: MediaStore;\n /** `null` when `MediaModule` has no `uploadSessions` — resumable uploads are then unavailable. */\n uploads: ResumableUploadManager | null;\n /** Lets media referenced from the actor's own thread resolve (see {@link MediaAttachmentStaging.resolve}). */\n agentStore?: AgentStore;\n /** The resumable-upload routes are mounted — what {@link MediaAttachmentStaging.describe} tells clients. */\n resumableRoutes?: boolean;\n}\n\nexport interface BeginMediaUploadInput {\n actor: Actor;\n filename: string;\n contentType: string;\n /** Exact byte length of the file — tus needs it up front, and completion is checked against it. */\n size: number;\n}\n\n/** A tus session opened for one attachment. Stream the bytes to `location`, then complete. */\nexport interface BeginMediaUploadResult {\n /** The attachment's id — what the chat turn will send as `{ mediaId }`. */\n mediaId: string;\n /** The tus upload id. */\n uploadId: string;\n /** The tus resource to `PATCH` the bytes to (relative to the API origin). */\n location: string;\n}\n\n/**\n * {@link AttachmentStagingStore} backed by `@dudousxd/nestjs-media`: every chat attachment is a\n * media-library record (`ownerType` + the actor's id as owner, one collection), its bytes on a\n * media disk.\n *\n * Two ways in, one store:\n * - `beginUpload` → the client streams the bytes to nestjs-media's own tus endpoint → `completeUpload`\n * (resumable, what `@dudousxd/nestjs-agent-react/media` drives);\n * - `stage` — what `POST /agent/attachments` calls with a buffered multipart file, when a host also\n * mounts that route.\n *\n * The record exists from `beginUpload` on, so an upload abandoned half-way is in the actor's\n * inventory ({@link list}) and ages into `AgentService.collectableAttachments` like any unsent file.\n */\nexport class MediaAttachmentStaging implements AttachmentStagingStore {\n private readonly logger = new Logger(MediaAttachmentStaging.name);\n private readonly collection: string;\n private readonly ownerType: string;\n private readonly maxBytes: number;\n private readonly allowed: readonly string[];\n private readonly now: () => Date;\n private readonly newId: () => string;\n private warnedInline = false;\n\n constructor(\n private readonly deps: MediaAttachmentStagingDeps,\n private readonly options: AgentMediaAttachmentsOptions,\n ) {\n this.collection = options.collection ?? DEFAULT_AGENT_MEDIA_COLLECTION;\n this.ownerType = options.ownerType ?? DEFAULT_AGENT_MEDIA_OWNER_TYPE;\n this.maxBytes = options.maxBytes ?? DEFAULT_MAX_BYTES;\n this.allowed = options.allowedContentTypes ?? DEFAULT_ALLOWED_CONTENT_TYPES;\n this.now = options.clock ?? (() => new Date());\n this.newId = options.idGenerator ?? (() => randomUUID());\n }\n\n /** The limits this store enforces, and how a client uploads to it (see `GET <base>/config`). */\n describe(): AttachmentStagingDescription {\n return {\n maxBytes: this.maxBytes,\n allowedContentTypes: this.allowed,\n upload: this.deps.resumableRoutes === false ? 'multipart' : 'resumable',\n };\n }\n\n private get diskName(): string {\n return this.options.disk ?? this.deps.storage.defaultDisk;\n }\n\n /** Open a tus session for one file, owned by `actor`. Validation happens here, before any byte moves. */\n async beginUpload(input: BeginMediaUploadInput): Promise<BeginMediaUploadResult> {\n const uploads = this.deps.uploads;\n if (uploads === null) {\n throw new NotImplementedException(\n 'Resumable attachment uploads need MediaModule configured with `uploadSessions` and `tus`.',\n );\n }\n this.validate(input.filename, input.contentType, input.size);\n const id = this.newId();\n const fileName = safeFileName(input.filename);\n const path = this.pathFor(input.actor, id, fileName);\n const session = await uploads.createUpload({\n disk: this.diskName,\n key: path,\n size: input.size,\n contentType: input.contentType,\n metadata: { agentMediaId: id },\n });\n await this.saveRecord({\n id,\n actor: input.actor,\n fileName,\n mimeType: input.contentType,\n size: input.size,\n path,\n state: { status: 'pending', uploadId: session.id },\n });\n const base = (this.options.tusBasePath ?? DEFAULT_AGENT_MEDIA_TUS_BASE_PATH).replace(\n /\\/+$/,\n '',\n );\n return { mediaId: id, uploadId: session.id, location: `${base}/${session.id}` };\n }\n\n /**\n * Confirm the bytes of a `beginUpload` landed and whole. `null` for an unknown or foreign id;\n * `409` while the bytes are still missing; `422` (and the record dropped) when what arrived is\n * not the size that was declared. Idempotent on a ready record.\n */\n async completeUpload(input: {\n actor: Actor;\n mediaId: string;\n }): Promise<MessageAttachment | null> {\n const record = await this.ownRecord(input.mediaId, input.actor);\n if (record === null) return null;\n const ready = await this.ensureReady(record);\n if (ready === 'missing') {\n throw new ConflictException(`upload ${input.mediaId} has not finished`);\n }\n if (ready === 'mismatch') {\n await this.remove(record.id);\n throw new UnprocessableEntityException(\n `upload ${input.mediaId} does not match its declared size`,\n );\n }\n return this.toAttachment(ready, { forModel: false });\n }\n\n /** The actor drops one of their own attachments — aborting its upload if still in flight. */\n async discard(input: { actor: Actor; mediaId: string }): Promise<boolean> {\n const record = await this.ownRecord(input.mediaId, input.actor);\n if (record === null) return false;\n await this.remove(record.id);\n return true;\n }\n\n /**\n * Delete an attachment's bytes, its in-flight tus session and its record. NO ownership check —\n * the host's sweep calls this for what `AgentService.collectableAttachments` returned.\n */\n async remove(mediaId: string): Promise<void> {\n const record = await this.deps.store.find(mediaId);\n if (record === null || !this.isOurs(record)) return;\n const state = stateOf(record);\n if (state.status === 'pending' && state.uploadId !== undefined) {\n await this.deps.uploads?.abort(state.uploadId).catch(() => undefined);\n }\n await this.deps.storage\n .disk(record.disk)\n .delete(record.path)\n .catch(() => undefined);\n await this.deps.store.delete(record.id);\n if (this.options.indexForRag === true && state.status === 'ready') {\n publishMedia('delete', {\n id: record.id,\n ownerType: record.ownerType,\n ownerId: record.ownerId,\n });\n }\n }\n\n async stage(input: StageAttachmentInput): Promise<MessageAttachment> {\n const id = this.newId();\n const fileName = safeFileName(input.filename);\n const path = this.pathFor(input.actor, id, fileName);\n await this.deps.storage\n .disk(this.diskName)\n .put(path, input.data, { contentType: input.contentType });\n const record = await this.saveRecord({\n id,\n actor: input.actor,\n fileName,\n mimeType: input.contentType,\n size: input.sizeBytes,\n path,\n state: { status: 'ready' },\n });\n this.announce(record);\n return this.toAttachment(record, { forModel: false });\n }\n\n /**\n * The attachment for `mediaId`, or `null` when it is unknown, not this actor's, or its bytes have\n * not (fully) arrived. The actor may use media they own, and media a message in one of THEIR\n * threads already carries (a fork, a regenerate) — never anything else.\n */\n async resolve(input: ResolveAttachmentInput): Promise<MessageAttachment | null> {\n const record = await this.deps.store.find(input.mediaId);\n if (record === null || !this.isOurs(record)) return null;\n if (!(await this.canUse(record, input.actor))) return null;\n const ready = await this.ensureReady(record);\n if (typeof ready === 'string') return null;\n return this.toAttachment(ready, { forModel: true });\n }\n\n async list(input: ListStagedAttachmentsInput): Promise<StagedAttachment[]> {\n const records = await this.deps.store.listByOwner(\n this.ownerType,\n input.actor.id,\n this.collection,\n );\n const entries = records\n .map((record) => ({\n mediaId: record.id,\n name: record.fileName,\n contentType: record.mimeType,\n sizeBytes: record.size,\n createdAt: new Date(record.createdAt).toISOString(),\n }))\n .filter((entry) => input.stagedBefore === undefined || entry.createdAt < input.stagedBefore)\n .sort((a, b) => b.createdAt.localeCompare(a.createdAt));\n return input.limit === undefined ? entries : entries.slice(0, input.limit);\n }\n\n private validate(filename: string, contentType: string, size: number): void {\n if (typeof filename !== 'string' || filename.trim().length === 0) {\n throw new BadRequestException('filename is required');\n }\n if (typeof contentType !== 'string' || !this.allowed.includes(contentType)) {\n throw new UnsupportedMediaTypeException(\n `content type \"${String(contentType)}\" is not allowed (allowed: ${this.allowed.join(', ')})`,\n );\n }\n if (typeof size !== 'number' || !Number.isInteger(size) || size <= 0) {\n throw new BadRequestException('size must be a positive integer byte count');\n }\n if (size > this.maxBytes) {\n throw new PayloadTooLargeException(`file exceeds the ${this.maxBytes}-byte limit`);\n }\n }\n\n private pathFor(actor: Actor, id: string, fileName: string): string {\n return `${this.ownerType}/${encodeURIComponent(actor.id)}/${this.collection}/${id}/${fileName}`;\n }\n\n private async saveRecord(input: {\n id: string;\n actor: Actor;\n fileName: string;\n mimeType: string;\n size: number;\n path: string;\n state: AgentAttachmentState;\n }): Promise<MediaRecord> {\n const timestamp = this.now();\n return this.deps.store.save({\n id: input.id,\n ownerType: this.ownerType,\n ownerId: input.actor.id,\n collection: this.collection,\n name: stripExtension(input.fileName),\n fileName: input.fileName,\n mimeType: input.mimeType,\n size: input.size,\n disk: this.diskName,\n path: input.path,\n order: await this.deps.store.nextOrder(this.ownerType, input.actor.id, this.collection),\n customProperties: { agentAttachment: input.state },\n conversions: {},\n createdAt: timestamp,\n updatedAt: timestamp,\n });\n }\n\n private isOurs(record: MediaRecord): boolean {\n return record.ownerType === this.ownerType && record.collection === this.collection;\n }\n\n private async ownRecord(mediaId: string, actor: Actor): Promise<MediaRecord | null> {\n const record = await this.deps.store.find(mediaId);\n if (record === null || !this.isOurs(record) || record.ownerId !== actor.id) return null;\n return record;\n }\n\n private async canUse(record: MediaRecord, actor: Actor): Promise<boolean> {\n const allowed = await this.defaultAccess(record, actor);\n const custom = this.options.canAccess;\n return custom === undefined ? allowed : (await custom({ record, actor, allowed })) === true;\n }\n\n private async defaultAccess(record: MediaRecord, actor: Actor): Promise<boolean> {\n if (record.ownerId === actor.id) return true;\n const referenced = this.deps.agentStore?.referencedMediaIds?.bind(this.deps.agentStore);\n if (referenced === undefined) return false;\n return (await referenced(actor.id, [record.id])).includes(record.id);\n }\n\n /**\n * A ready record as-is; a pending one promoted to ready once its bytes are on the disk at exactly\n * the declared size. `'missing'` while they are not; `'mismatch'` when they arrived at another size.\n */\n private async ensureReady(record: MediaRecord): Promise<MediaRecord | 'missing' | 'mismatch'> {\n if (stateOf(record).status === 'ready') return record;\n const disk = this.deps.storage.disk(record.disk);\n if (!(await disk.exists(record.path))) return 'missing';\n const size = await disk.size(record.path);\n if (size !== record.size || size > this.maxBytes) return 'mismatch';\n const ready = await this.deps.store.save({\n ...record,\n customProperties: { ...record.customProperties, agentAttachment: { status: 'ready' } },\n updatedAt: this.now(),\n });\n this.announce(ready);\n return ready;\n }\n\n private announce(record: MediaRecord): void {\n if (this.options.indexForRag !== true) return;\n publishMedia('attach', {\n id: record.id,\n ownerType: record.ownerType,\n ownerId: record.ownerId,\n collection: record.collection,\n disk: record.disk,\n path: record.path,\n size: record.size,\n mimeType: record.mimeType,\n });\n }\n\n /**\n * `forModel: false` is what goes back to the uploader: never inline bytes (they already have the\n * file), so an inline-only disk answers with an empty url there.\n */\n private async toAttachment(\n record: MediaRecord,\n { forModel }: { forModel: boolean },\n ): Promise<MessageAttachment> {\n const base = { mediaId: record.id, contentType: record.mimeType, name: record.fileName };\n const disk = this.deps.storage.disk(record.disk);\n if (this.options.resolveUrl !== undefined) {\n return { ...base, url: await this.options.resolveUrl(record, { disk }) };\n }\n if (this.options.visibility === 'public') {\n return { ...base, url: await disk.url(record.path) };\n }\n if (disk.capabilities.presign) {\n const ttl = this.options.urlExpiresInSeconds ?? DEFAULT_AGENT_MEDIA_URL_TTL_SECONDS;\n return {\n ...base,\n url: await disk.temporaryUrl(record.path, ttl, { responseContentType: record.mimeType }),\n };\n }\n if (!forModel) return { ...base, url: '' };\n if (!this.warnedInline) {\n this.warnedInline = true;\n this.logger.warn(\n `disk \"${record.disk}\" cannot presign urls: attachments reach the model inline (data: urls, persisted with each message). Use a presign-capable disk or resolveUrl in production.`,\n );\n }\n const bytes = await disk.get(record.path);\n return { ...base, url: `data:${record.mimeType};base64,${bytes.toString('base64')}` };\n }\n}\n\nfunction stateOf(record: MediaRecord): AgentAttachmentState {\n const state = record.customProperties.agentAttachment as AgentAttachmentState | undefined;\n return state ?? { status: 'ready' };\n}\n\n/** The last path segment, stripped of anything a disk would read as a directory or control. */\nfunction safeFileName(filename: string): string {\n const last = filename.split(/[\\\\/]/).pop() ?? '';\n // biome-ignore lint/suspicious/noControlCharactersInRegex: stripping control chars is the point.\n const cleaned = last.replace(/[\\u0000-\\u001f\\u007f]/g, '').trim();\n return cleaned === '' || cleaned === '.' || cleaned === '..' ? 'file' : cleaned;\n}\n\nfunction stripExtension(fileName: string): string {\n const dot = fileName.lastIndexOf('.');\n return dot > 0 ? fileName.slice(0, dot) : fileName;\n}\n","import type { Actor } from '@dudousxd/nestjs-agent-core';\nimport type { MediaRecord, StorageDriver } from '@dudousxd/nestjs-media';\nimport type {\n CanActivate,\n DynamicModule,\n InjectionToken,\n OptionalFactoryDependency,\n Type,\n} from '@nestjs/common';\n\n/** Default media-library collection chat attachments are filed under. */\nexport const DEFAULT_AGENT_MEDIA_COLLECTION = 'agent-attachments';\n\n/** Default `ownerType` of the media records — the owner id is the actor's id. */\nexport const DEFAULT_AGENT_MEDIA_OWNER_TYPE = 'agent-actor';\n\n/** Default lifetime of a presigned url handed to the model provider (7 days — S3's ceiling). */\nexport const DEFAULT_AGENT_MEDIA_URL_TTL_SECONDS = 7 * 24 * 60 * 60;\n\n/** Default tus base path — `MediaModule`'s own default for `tus.basePath`. */\nexport const DEFAULT_AGENT_MEDIA_TUS_BASE_PATH = '/media/uploads';\n\n/** What {@link AgentMediaAttachmentsOptions.resolveUrl} is handed besides the record. */\nexport interface ResolveMediaUrlContext {\n /** The storage driver the record's bytes live on. */\n disk: StorageDriver;\n}\n\n/** What {@link AgentMediaAttachmentsOptions.canAccess} decides on. */\nexport interface MediaAttachmentAccessInput {\n record: MediaRecord;\n actor: Actor;\n /** The default verdict: the actor owns the record, or a message in their own thread carries it. */\n allowed: boolean;\n}\n\nexport interface AgentMediaAttachmentsOptions {\n /** Media-library collection the records are filed under. Default `'agent-attachments'`. */\n collection?: string;\n /** `ownerType` of the records; `ownerId` is always the actor's id. Default `'agent-actor'`. */\n ownerType?: string;\n /** Storage disk the bytes land on. Default: the `MediaModule`'s default disk. */\n disk?: string;\n /** Per-file size cap. Default 20 MiB — the same as `POST /agent/attachments`. */\n maxBytes?: number;\n /**\n * Content types a chat may attach. Default: the same allowlist as `POST /agent/attachments`\n * (png/jpeg/gif/webp images, pdf, plain text, csv).\n */\n allowedContentTypes?: readonly string[];\n /**\n * How the model provider gets at the bytes.\n * - `'private'` (default): a presigned url (`temporaryUrl`) when the disk can mint one (S3), else\n * the bytes inline as a `data:` url. Inline works everywhere but is persisted with the message —\n * fine for local development, use a presign-capable disk (or `resolveUrl`) in production.\n * - `'public'`: the disk's stable public `url()` — only for a bucket the provider may read.\n */\n visibility?: 'private' | 'public';\n /** Lifetime of a presigned url, in seconds. Default 7 days. */\n urlExpiresInSeconds?: number;\n /**\n * Take over url minting entirely (e.g. a signed route on your own API). Wins over `visibility`.\n * SECURITY: the url is fetched server-side by the model provider — build it from the record only.\n */\n resolveUrl?: (record: MediaRecord, context: ResolveMediaUrlContext) => string | Promise<string>;\n /**\n * Who may send (resolve) a record in a chat turn. Default: `allowed` — the owner, or anyone whose\n * own thread already carries it. Return `allowed || …` to widen that (a shared team space),\n * `allowed && …` to narrow it. Only `resolve` consults this; completing and discarding an upload\n * stay the uploader's.\n */\n canAccess?: (input: MediaAttachmentAccessInput) => boolean | Promise<boolean>;\n /**\n * The public path of `MediaModule`'s tus endpoint (its `tus.basePath`, global prefix included) —\n * used to build the `location` a client streams the bytes to. Default `'/media/uploads'`.\n */\n tusBasePath?: string;\n /**\n * Announce each ready attachment on `aviary:media:attach` (and its removal on\n * `aviary:media:delete`), so `@dudousxd/nestjs-agent-rag-media` indexes it for retrieval. Off by\n * default: a chat attachment is private to the person who sent it, and indexing it is only safe\n * where retrieval is scoped by owner (`FilteredRetriever` on `ownerType`/`ownerId`).\n */\n indexForRag?: boolean;\n /** Injectable clock for tests. */\n clock?: () => Date;\n /** Injectable id generator for tests. Default `crypto.randomUUID`. */\n idGenerator?: () => string;\n}\n\n/** Build-time wiring shared by `forRoot` / `forRootAsync` — Nest mounts controllers before any factory runs. */\nexport interface AgentMediaAttachmentsMountOptions {\n /**\n * Route prefix — must match `AgentModule`'s `path`. The upload routes mount under\n * `<path>/attachments/uploads`. Default `'agent'`.\n */\n path?: string;\n /** Guards for the upload routes (mirror `AgentModule`'s `guards`). */\n guards?: Type<CanActivate>[];\n /**\n * Mount the resumable-upload routes. Default `true`. `false` keeps only the staging store — for a\n * host that opens tus sessions its own way and only wants resolve/list backed by media.\n */\n routes?: boolean;\n}\n\nexport interface AgentMediaAttachmentsModuleOptions\n extends AgentMediaAttachmentsOptions,\n AgentMediaAttachmentsMountOptions {}\n\nexport interface AgentMediaAttachmentsAsyncOptions extends AgentMediaAttachmentsMountOptions {\n imports?: DynamicModule['imports'];\n inject?: Array<InjectionToken | OptionalFactoryDependency>;\n useFactory: (\n ...args: never[]\n ) => AgentMediaAttachmentsOptions | Promise<AgentMediaAttachmentsOptions>;\n}\n","import {\n AGENT_ATTACHMENT_STAGING,\n AGENT_STORE,\n type AgentStore,\n} from '@dudousxd/nestjs-agent-core';\nimport {\n MEDIA_STORAGE_SHARED,\n MEDIA_STORE,\n MEDIA_UPLOADS,\n type MediaStore,\n type ResumableUploadManager,\n type StorageManager,\n} from '@dudousxd/nestjs-media';\nimport {\n type CanActivate,\n type DynamicModule,\n Global,\n Module,\n type Provider,\n type Type,\n} from '@nestjs/common';\nimport { RouterModule } from '@nestjs/core';\nimport { MediaAttachmentStaging } from './media-attachment-staging.js';\nimport type {\n AgentMediaAttachmentsAsyncOptions,\n AgentMediaAttachmentsModuleOptions,\n AgentMediaAttachmentsMountOptions,\n AgentMediaAttachmentsOptions,\n} from './media-attachments.options.js';\nimport { AgentMediaUploadsController } from './media-uploads.controller.js';\nimport { AGENT_MEDIA_ATTACHMENTS, AGENT_MEDIA_ATTACHMENTS_OPTIONS } from './tokens.js';\n\n/** See `agent.module.ts` — Nest's `GUARDS_METADATA` key, inlined for the same ESM reason. */\nconst GUARDS_METADATA = '__guards__';\n\nfunction stagingProvider(routes: boolean): Provider {\n return {\n provide: AGENT_MEDIA_ATTACHMENTS,\n inject: [\n AGENT_MEDIA_ATTACHMENTS_OPTIONS,\n MEDIA_STORAGE_SHARED,\n MEDIA_STORE,\n MEDIA_UPLOADS,\n { token: AGENT_STORE, optional: true },\n ],\n useFactory: (\n options: AgentMediaAttachmentsOptions,\n storage: StorageManager,\n store: MediaStore | null,\n uploads: ResumableUploadManager | null,\n agentStore: AgentStore | undefined,\n ) => {\n if (store === null) {\n throw new Error(\n 'AgentMediaAttachmentsModule: MediaModule has no `store`. Chat attachments are media ' +\n 'records, so configure MediaModule with a MediaStore (e.g. a database adapter).',\n );\n }\n return new MediaAttachmentStaging(\n {\n storage,\n store,\n uploads,\n resumableRoutes: routes,\n ...(agentStore !== undefined ? { agentStore } : {}),\n },\n options,\n );\n },\n };\n}\n\nfunction build(\n mount: AgentMediaAttachmentsMountOptions,\n optionsProvider: Provider,\n imports: DynamicModule['imports'] = [],\n): DynamicModule {\n const routes = mount.routes !== false;\n Reflect.defineMetadata(GUARDS_METADATA, mount.guards ?? [], AgentMediaUploadsController);\n const guards: Type<CanActivate>[] = [...new Set(mount.guards ?? [])];\n return {\n module: AgentMediaAttachmentsModule,\n global: true,\n imports: [\n ...imports,\n ...(routes\n ? [\n RouterModule.register([\n { path: mount.path ?? 'agent', module: AgentMediaAttachmentsModule },\n ]),\n ]\n : []),\n ],\n controllers: routes ? [AgentMediaUploadsController] : [],\n providers: [\n optionsProvider,\n stagingProvider(routes),\n { provide: AGENT_ATTACHMENT_STAGING, useExisting: AGENT_MEDIA_ATTACHMENTS },\n { provide: MediaAttachmentStaging, useExisting: AGENT_MEDIA_ATTACHMENTS },\n ...guards,\n ],\n exports: [AGENT_ATTACHMENT_STAGING, AGENT_MEDIA_ATTACHMENTS, MediaAttachmentStaging],\n };\n}\n\n/**\n * Chat attachments on `@dudousxd/nestjs-media`: binds `AGENT_ATTACHMENT_STAGING` to a\n * media-backed store and mounts the resumable-upload routes (`<path>/attachments/uploads`).\n * Import it next to `AgentModule` and a `MediaModule` configured with `store`, `uploadSessions`\n * and `tus`.\n */\n@Global()\n@Module({})\nexport class AgentMediaAttachmentsModule {\n static forRoot(options: AgentMediaAttachmentsModuleOptions = {}): DynamicModule {\n return build(options, { provide: AGENT_MEDIA_ATTACHMENTS_OPTIONS, useValue: options });\n }\n\n static forRootAsync(options: AgentMediaAttachmentsAsyncOptions): DynamicModule {\n return build(\n options,\n {\n provide: AGENT_MEDIA_ATTACHMENTS_OPTIONS,\n inject: options.inject ?? [],\n useFactory: options.useFactory,\n },\n options.imports,\n );\n }\n}\n","import {\n AGENT_ACTOR_RESOLVER,\n type ActorResolver,\n type MessageAttachment,\n} from '@dudousxd/nestjs-agent-core';\nimport {\n BadRequestException,\n Body,\n Controller,\n Delete,\n HttpCode,\n Inject,\n NotFoundException,\n Param,\n Post,\n Req,\n} from '@nestjs/common';\nimport type { Request } from 'express';\nimport type { BeginMediaUploadResult, MediaAttachmentStaging } from './media-attachment-staging.js';\nimport { AGENT_MEDIA_ATTACHMENTS } from './tokens.js';\n\n/**\n * The agent's half of a resumable chat upload. It never carries a byte: it opens an owned,\n * validated tus session on `MediaModule` (`POST`), confirms the bytes arrived (`POST :id/complete`)\n * and drops an attachment the user removed (`DELETE :id`). The bytes themselves go to\n * nestjs-media's own tus endpoint (`PATCH <tusBasePath>/:uploadId`), which the host guards through\n * `MediaModule`'s `guards`.\n *\n * Mounted at `<AgentModule path>/attachments/uploads`.\n */\n@Controller('attachments/uploads')\nexport class AgentMediaUploadsController {\n constructor(\n @Inject(AGENT_ACTOR_RESOLVER) private readonly actorResolver: ActorResolver,\n @Inject(AGENT_MEDIA_ATTACHMENTS) private readonly staging: MediaAttachmentStaging,\n ) {}\n\n /** `{ filename, contentType, size }` → `{ mediaId, uploadId, location }`. */\n @Post()\n async begin(@Req() req: Request, @Body() body: unknown): Promise<BeginMediaUploadResult> {\n if (typeof body !== 'object' || body === null) {\n throw new BadRequestException('body must be { filename, contentType, size }');\n }\n const { filename, contentType, size } = body as Record<string, unknown>;\n const actor = await this.actorResolver.resolve(req);\n return this.staging.beginUpload({\n actor,\n filename: filename as string,\n contentType: contentType as string,\n size: size as number,\n });\n }\n\n @Post(':mediaId/complete')\n @HttpCode(200)\n async complete(\n @Req() req: Request,\n @Param('mediaId') mediaId: string,\n ): Promise<MessageAttachment> {\n const actor = await this.actorResolver.resolve(req);\n const attachment = await this.staging.completeUpload({ actor, mediaId });\n if (attachment === null) throw new NotFoundException(`attachment ${mediaId} not found`);\n return attachment;\n }\n\n @Delete(':mediaId')\n @HttpCode(204)\n async discard(@Req() req: Request, @Param('mediaId') mediaId: string): Promise<void> {\n const actor = await this.actorResolver.resolve(req);\n if (!(await this.staging.discard({ actor, mediaId }))) {\n throw new NotFoundException(`attachment ${mediaId} not found`);\n }\n }\n}\n","/** The {@link import('./media-attachment-staging.js').MediaAttachmentStaging} instance — inject it for `remove()` in a sweep. */\nexport const AGENT_MEDIA_ATTACHMENTS = Symbol.for('@dudousxd/nestjs-agent:media-attachments');\n\n/** The resolved {@link import('./media-attachments.options.js').AgentMediaAttachmentsOptions}. */\nexport const AGENT_MEDIA_ATTACHMENTS_OPTIONS = Symbol.for(\n '@dudousxd/nestjs-agent:media-attachments-options',\n);\n"],"mappings":"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","names":["randomUUID","publishMedia","BadRequestException","ConflictException","Logger","NotImplementedException","PayloadTooLargeException","UnprocessableEntityException","UnsupportedMediaTypeException","DEFAULT_AGENT_MEDIA_COLLECTION","DEFAULT_AGENT_MEDIA_OWNER_TYPE","DEFAULT_AGENT_MEDIA_URL_TTL_SECONDS","DEFAULT_AGENT_MEDIA_TUS_BASE_PATH","DEFAULT_MAX_BYTES","DEFAULT_ALLOWED_CONTENT_TYPES","MediaAttachmentStaging","logger","Logger","name","collection","ownerType","maxBytes","allowed","now","newId","warnedInline","deps","options","DEFAULT_AGENT_MEDIA_COLLECTION","DEFAULT_AGENT_MEDIA_OWNER_TYPE","allowedContentTypes","clock","Date","idGenerator","randomUUID","describe","upload","resumableRoutes","diskName","disk","storage","defaultDisk","beginUpload","input","uploads","NotImplementedException","validate","filename","contentType","size","id","fileName","safeFileName","path","pathFor","actor","session","createUpload","key","metadata","agentMediaId","saveRecord","mimeType","state","status","uploadId","base","tusBasePath","DEFAULT_AGENT_MEDIA_TUS_BASE_PATH","replace","mediaId","location","completeUpload","record","ownRecord","ready","ensureReady","ConflictException","remove","UnprocessableEntityException","toAttachment","forModel","discard","store","find","isOurs","stateOf","undefined","abort","catch","delete","indexForRag","publishMedia","ownerId","stage","put","data","sizeBytes","announce","resolve","canUse","list","records","listByOwner","entries","map","createdAt","toISOString","filter","entry","stagedBefore","sort","a","b","localeCompare","limit","slice","trim","length","BadRequestException","includes","UnsupportedMediaTypeException","String","join","Number","isInteger","PayloadTooLargeException","encodeURIComponent","timestamp","save","stripExtension","order","nextOrder","customProperties","agentAttachment","conversions","updatedAt","defaultAccess","custom","canAccess","referenced","agentStore","referencedMediaIds","bind","exists","resolveUrl","url","visibility","capabilities","presign","ttl","urlExpiresInSeconds","DEFAULT_AGENT_MEDIA_URL_TTL_SECONDS","temporaryUrl","responseContentType","warn","bytes","get","toString","last","split","pop","cleaned","dot","lastIndexOf","AGENT_ATTACHMENT_STAGING","AGENT_STORE","MEDIA_STORAGE_SHARED","MEDIA_STORE","MEDIA_UPLOADS","Global","Module","RouterModule","AGENT_ACTOR_RESOLVER","BadRequestException","Body","Controller","Delete","HttpCode","Inject","NotFoundException","Param","Post","Req","AGENT_MEDIA_ATTACHMENTS","Symbol","for","AGENT_MEDIA_ATTACHMENTS_OPTIONS","AgentMediaUploadsController","actorResolver","staging","begin","req","body","BadRequestException","filename","contentType","size","actor","resolve","beginUpload","complete","mediaId","attachment","completeUpload","NotFoundException","discard","GUARDS_METADATA","stagingProvider","routes","provide","AGENT_MEDIA_ATTACHMENTS","inject","AGENT_MEDIA_ATTACHMENTS_OPTIONS","MEDIA_STORAGE_SHARED","MEDIA_STORE","MEDIA_UPLOADS","token","AGENT_STORE","optional","useFactory","options","storage","store","uploads","agentStore","Error","MediaAttachmentStaging","resumableRoutes","undefined","build","mount","optionsProvider","imports","Reflect","defineMetadata","guards","AgentMediaUploadsController","Set","module","AgentMediaAttachmentsModule","global","RouterModule","register","path","controllers","providers","AGENT_ATTACHMENT_STAGING","useExisting","exports","forRoot","useValue","forRootAsync"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dudousxd/nestjs-agent",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.12.0",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "git+https://github.com/DavideCarvalho/nestjs-agent.git",
|
|
@@ -89,7 +89,7 @@
|
|
|
89
89
|
"dependencies": {
|
|
90
90
|
"@standard-schema/spec": "1.1.0",
|
|
91
91
|
"zod": "3.25.76",
|
|
92
|
-
"@dudousxd/nestjs-agent-core": "0.
|
|
92
|
+
"@dudousxd/nestjs-agent-core": "0.28.0"
|
|
93
93
|
},
|
|
94
94
|
"peerDependencies": {
|
|
95
95
|
"@dudousxd/nestjs-durable": ">=0.34.0",
|