@electric-ax/agents-server 0.4.12 → 0.4.13
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/dist/entrypoint.js +276 -26
- package/dist/index.cjs +276 -26
- package/dist/index.d.cts +50 -0
- package/dist/index.d.ts +50 -0
- package/dist/index.js +276 -26
- package/package.json +8 -8
- package/src/entity-manager.ts +351 -1
- package/src/routing/durable-streams-router.ts +4 -1
- package/src/routing/entities-router.ts +226 -0
- package/src/routing/stream-append.ts +3 -0
- package/src/stream-client.ts +14 -33
package/dist/index.js
CHANGED
|
@@ -1960,7 +1960,8 @@ var StreamClient = class {
|
|
|
1960
1960
|
url: this.streamUrl(path$1),
|
|
1961
1961
|
headers: this.streamHeaders(),
|
|
1962
1962
|
contentType: opts.contentType,
|
|
1963
|
-
body: opts.body
|
|
1963
|
+
body: opts.body,
|
|
1964
|
+
closed: opts.closed
|
|
1964
1965
|
});
|
|
1965
1966
|
});
|
|
1966
1967
|
}
|
|
@@ -2060,30 +2061,11 @@ var StreamClient = class {
|
|
|
2060
2061
|
offset: fromOffset ?? `-1`,
|
|
2061
2062
|
live: false
|
|
2062
2063
|
});
|
|
2063
|
-
const
|
|
2064
|
-
return
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
if (settled) return;
|
|
2069
|
-
settled = true;
|
|
2070
|
-
unsub();
|
|
2071
|
-
resolve$1(r);
|
|
2072
|
-
};
|
|
2073
|
-
unsub = response.subscribeBytes((chunk) => {
|
|
2074
|
-
messages.push({
|
|
2075
|
-
data: chunk.data,
|
|
2076
|
-
offset: chunk.offset
|
|
2077
|
-
});
|
|
2078
|
-
if (chunk.upToDate || chunk.streamClosed) finish({ messages });
|
|
2079
|
-
});
|
|
2080
|
-
response.closed.then(() => finish({ messages })).catch((err) => {
|
|
2081
|
-
if (settled) return;
|
|
2082
|
-
settled = true;
|
|
2083
|
-
unsub();
|
|
2084
|
-
reject(err);
|
|
2085
|
-
});
|
|
2086
|
-
});
|
|
2064
|
+
const body = await response.body();
|
|
2065
|
+
return { messages: body.length === 0 ? [] : [{
|
|
2066
|
+
data: body,
|
|
2067
|
+
offset: response.offset
|
|
2068
|
+
}] };
|
|
2087
2069
|
});
|
|
2088
2070
|
}
|
|
2089
2071
|
async readJson(path$1, fromOffset) {
|
|
@@ -2690,9 +2672,45 @@ function createInitialQueuePosition(date) {
|
|
|
2690
2672
|
const DEFAULT_FORK_WAIT_TIMEOUT_MS = 12e4;
|
|
2691
2673
|
const DEFAULT_FORK_WAIT_POLL_MS = 250;
|
|
2692
2674
|
const SERVER_SIGNAL_SENDER = `/_electric/server`;
|
|
2675
|
+
const DEFAULT_MAX_ATTACHMENT_BYTES = 25 * 1024 * 1024;
|
|
2693
2676
|
function sleep(ms) {
|
|
2694
2677
|
return new Promise((resolve$1) => setTimeout(resolve$1, ms));
|
|
2695
2678
|
}
|
|
2679
|
+
function maxAttachmentBytes() {
|
|
2680
|
+
const configured = Number(process.env.ELECTRIC_AGENTS_MAX_ATTACHMENT_BYTES);
|
|
2681
|
+
return Number.isFinite(configured) && configured > 0 ? Math.floor(configured) : DEFAULT_MAX_ATTACHMENT_BYTES;
|
|
2682
|
+
}
|
|
2683
|
+
function manifestAttachmentKey(id) {
|
|
2684
|
+
return `attachment:${id}`;
|
|
2685
|
+
}
|
|
2686
|
+
function getEntityAttachmentStreamPath(entityUrl, attachmentId) {
|
|
2687
|
+
return `${entityUrl.replace(/\/+$/, ``)}/attachments/${attachmentId}`;
|
|
2688
|
+
}
|
|
2689
|
+
function isStreamCreateConflict(error) {
|
|
2690
|
+
return !!error && typeof error === `object` && (`status` in error && error.status === 409 || `code` in error && error.code === `CONFLICT_SEQ`);
|
|
2691
|
+
}
|
|
2692
|
+
function assertCanonicalAttachmentStreamPath(entityUrl, attachment) {
|
|
2693
|
+
const expected = getEntityAttachmentStreamPath(entityUrl, attachment.id);
|
|
2694
|
+
if (attachment.streamPath === expected) return;
|
|
2695
|
+
throw new ElectricAgentsError(ErrCodeInvalidRequest, `Attachment stream path does not match its entity and id`, 409);
|
|
2696
|
+
}
|
|
2697
|
+
function validateAttachmentId(id) {
|
|
2698
|
+
if (!id || id.includes(`/`) || id.startsWith(`.`)) throw new ElectricAgentsError(ErrCodeInvalidRequest, `attachment id must not be empty, start with ".", or contain forward slashes`, 400);
|
|
2699
|
+
}
|
|
2700
|
+
function validateAttachmentSubject(subject) {
|
|
2701
|
+
if (!subject.key) throw new ElectricAgentsError(ErrCodeInvalidRequest, `attachment subject key is required`, 400);
|
|
2702
|
+
if (subject.type !== `inbox` && subject.type !== `run` && subject.type !== `text` && subject.type !== `tool_call` && subject.type !== `context`) throw new ElectricAgentsError(ErrCodeInvalidRequest, `invalid attachment subject type`, 400);
|
|
2703
|
+
}
|
|
2704
|
+
function concatByteMessages(messages) {
|
|
2705
|
+
const total = messages.reduce((sum, message) => sum + message.data.length, 0);
|
|
2706
|
+
const bytes = new Uint8Array(total);
|
|
2707
|
+
let offset = 0;
|
|
2708
|
+
for (const message of messages) {
|
|
2709
|
+
bytes.set(message.data, offset);
|
|
2710
|
+
offset += message.data.length;
|
|
2711
|
+
}
|
|
2712
|
+
return bytes;
|
|
2713
|
+
}
|
|
2696
2714
|
function omitUndefined$1(value) {
|
|
2697
2715
|
return Object.fromEntries(Object.entries(value).filter(([, entry]) => entry !== void 0));
|
|
2698
2716
|
}
|
|
@@ -3058,6 +3076,15 @@ var EntityManager = class {
|
|
|
3058
3076
|
await this.streamClient.fork(forkPath, sourcePath);
|
|
3059
3077
|
createdStreams.push(forkPath);
|
|
3060
3078
|
}
|
|
3079
|
+
for (const plan of entityPlans) {
|
|
3080
|
+
const manifests = snapshot.manifestsByEntity.get(plan.source.url) ?? new Map();
|
|
3081
|
+
for (const manifest of manifests.values()) {
|
|
3082
|
+
if (manifest.kind !== `attachment` || typeof manifest.streamPath !== `string` || typeof manifest.id !== `string`) continue;
|
|
3083
|
+
const forkPath = getEntityAttachmentStreamPath(plan.fork.url, manifest.id);
|
|
3084
|
+
await this.streamClient.fork(forkPath, manifest.streamPath);
|
|
3085
|
+
createdStreams.push(forkPath);
|
|
3086
|
+
}
|
|
3087
|
+
}
|
|
3061
3088
|
for (const plan of entityPlans) {
|
|
3062
3089
|
const reconciliation = this.buildForkReconciliation(plan, snapshot, entityUrlMap, sharedStateIdMap, stringMap);
|
|
3063
3090
|
activeManifestsByEntity.set(plan.fork.url, reconciliation.manifests);
|
|
@@ -3467,6 +3494,16 @@ var EntityManager = class {
|
|
|
3467
3494
|
changed: true
|
|
3468
3495
|
};
|
|
3469
3496
|
}
|
|
3497
|
+
if (next.kind === `attachment` && typeof next.streamPath === `string` && typeof next.id === `string`) for (const [sourceUrl, forkUrl] of entityUrlMap) {
|
|
3498
|
+
const prefix = `${sourceUrl}/attachments/`;
|
|
3499
|
+
if (!next.streamPath.startsWith(prefix)) continue;
|
|
3500
|
+
next.streamPath = getEntityAttachmentStreamPath(forkUrl, next.id);
|
|
3501
|
+
return {
|
|
3502
|
+
key,
|
|
3503
|
+
value: next,
|
|
3504
|
+
changed: true
|
|
3505
|
+
};
|
|
3506
|
+
}
|
|
3470
3507
|
if (next.kind === `schedule` && next.scheduleType === `future_send`) {
|
|
3471
3508
|
let changed = false;
|
|
3472
3509
|
if (typeof next.targetUrl === `string`) {
|
|
@@ -3664,6 +3701,93 @@ var EntityManager = class {
|
|
|
3664
3701
|
const envelope = entityStateSchema.inbox.delete({ key });
|
|
3665
3702
|
await this.streamClient.append(entity.streams.main, this.encodeChangeEvent(envelope));
|
|
3666
3703
|
}
|
|
3704
|
+
isAttachmentStreamPath(path$1) {
|
|
3705
|
+
return /^\/[^/]+\/[^/]+\/attachments\/[^/]+$/.test(path$1);
|
|
3706
|
+
}
|
|
3707
|
+
async createAttachment(entityUrl, req) {
|
|
3708
|
+
const entity = await this.registry.getEntity(entityUrl);
|
|
3709
|
+
if (!entity) throw new ElectricAgentsError(ErrCodeNotFound, `Entity not found`, 404);
|
|
3710
|
+
if (rejectsNormalWrites(entity.status)) throw new ElectricAgentsError(ErrCodeNotRunning, `Entity is not accepting writes`, 409);
|
|
3711
|
+
if (this.isForkWorkLockedEntity(entityUrl)) this.assertEntityNotForkWorkLocked(entityUrl);
|
|
3712
|
+
const id = req.id ?? randomUUID();
|
|
3713
|
+
validateAttachmentId(id);
|
|
3714
|
+
validateAttachmentSubject(req.subject);
|
|
3715
|
+
const limit = maxAttachmentBytes();
|
|
3716
|
+
if (req.bytes.length > limit) throw new ElectricAgentsError(ErrCodeInvalidRequest, `Attachment exceeds maximum size of ${limit} bytes`, 413);
|
|
3717
|
+
const mimeType = req.mimeType.trim() || `application/octet-stream`;
|
|
3718
|
+
const streamPath = getEntityAttachmentStreamPath(entityUrl, id);
|
|
3719
|
+
const manifestKey = manifestAttachmentKey(id);
|
|
3720
|
+
const txid = randomUUID();
|
|
3721
|
+
const now = new Date().toISOString();
|
|
3722
|
+
const sha256 = createHash(`sha256`).update(req.bytes).digest(`hex`);
|
|
3723
|
+
const attachment = {
|
|
3724
|
+
key: manifestKey,
|
|
3725
|
+
kind: `attachment`,
|
|
3726
|
+
id,
|
|
3727
|
+
streamPath,
|
|
3728
|
+
status: `complete`,
|
|
3729
|
+
subject: req.subject,
|
|
3730
|
+
role: req.role ?? `input`,
|
|
3731
|
+
mimeType,
|
|
3732
|
+
...req.filename ? { filename: req.filename } : {},
|
|
3733
|
+
byteLength: req.bytes.length,
|
|
3734
|
+
sha256,
|
|
3735
|
+
createdAt: now,
|
|
3736
|
+
...req.createdBy ? { createdBy: req.createdBy } : {},
|
|
3737
|
+
...req.meta ? { meta: req.meta } : {}
|
|
3738
|
+
};
|
|
3739
|
+
let streamCreated = false;
|
|
3740
|
+
try {
|
|
3741
|
+
await this.streamClient.create(streamPath, {
|
|
3742
|
+
contentType: mimeType,
|
|
3743
|
+
body: req.bytes,
|
|
3744
|
+
closed: true
|
|
3745
|
+
});
|
|
3746
|
+
streamCreated = true;
|
|
3747
|
+
await this.writeManifestEntry(entityUrl, manifestKey, `upsert`, attachment, { txid });
|
|
3748
|
+
} catch (error) {
|
|
3749
|
+
if (streamCreated) await this.streamClient.delete(streamPath).catch(() => void 0);
|
|
3750
|
+
if (!streamCreated && isStreamCreateConflict(error)) throw new ElectricAgentsError(ErrCodeInvalidRequest, `Attachment already exists at id "${id}"`, 409);
|
|
3751
|
+
throw error;
|
|
3752
|
+
}
|
|
3753
|
+
return {
|
|
3754
|
+
txid,
|
|
3755
|
+
attachment
|
|
3756
|
+
};
|
|
3757
|
+
}
|
|
3758
|
+
async getAttachment(entityUrl, id) {
|
|
3759
|
+
validateAttachmentId(id);
|
|
3760
|
+
const entity = await this.registry.getEntity(entityUrl);
|
|
3761
|
+
if (!entity) throw new ElectricAgentsError(ErrCodeNotFound, `Entity not found`, 404);
|
|
3762
|
+
const events = await this.streamClient.readJson(entity.streams.main);
|
|
3763
|
+
const manifest = this.reduceStateRows(events, `manifest`).get(manifestAttachmentKey(id));
|
|
3764
|
+
if (!manifest || manifest.kind !== `attachment`) return null;
|
|
3765
|
+
return manifest;
|
|
3766
|
+
}
|
|
3767
|
+
async readAttachment(entityUrl, id) {
|
|
3768
|
+
const attachment = await this.getAttachment(entityUrl, id);
|
|
3769
|
+
if (!attachment) throw new ElectricAgentsError(ErrCodeNotFound, `Attachment not found`, 404);
|
|
3770
|
+
if (attachment.status !== `complete`) throw new ElectricAgentsError(ErrCodeInvalidRequest, `Attachment is not complete`, 409);
|
|
3771
|
+
assertCanonicalAttachmentStreamPath(entityUrl, attachment);
|
|
3772
|
+
const result = await this.streamClient.read(attachment.streamPath);
|
|
3773
|
+
return {
|
|
3774
|
+
attachment,
|
|
3775
|
+
bytes: concatByteMessages(result.messages)
|
|
3776
|
+
};
|
|
3777
|
+
}
|
|
3778
|
+
async deleteAttachment(entityUrl, id) {
|
|
3779
|
+
const entity = await this.registry.getEntity(entityUrl);
|
|
3780
|
+
if (!entity) throw new ElectricAgentsError(ErrCodeNotFound, `Entity not found`, 404);
|
|
3781
|
+
if (rejectsNormalWrites(entity.status)) throw new ElectricAgentsError(ErrCodeNotRunning, `Entity is not accepting writes`, 409);
|
|
3782
|
+
if (this.isForkWorkLockedEntity(entityUrl)) this.assertEntityNotForkWorkLocked(entityUrl);
|
|
3783
|
+
const attachment = await this.getAttachment(entityUrl, id);
|
|
3784
|
+
if (!attachment) throw new ElectricAgentsError(ErrCodeNotFound, `Attachment not found`, 404);
|
|
3785
|
+
assertCanonicalAttachmentStreamPath(entityUrl, attachment);
|
|
3786
|
+
const txid = randomUUID();
|
|
3787
|
+
await this.writeManifestEntry(entityUrl, manifestAttachmentKey(id), `delete`, void 0, { txid });
|
|
3788
|
+
await this.streamClient.delete(attachment.streamPath).catch(() => void 0);
|
|
3789
|
+
return { txid };
|
|
3790
|
+
}
|
|
3667
3791
|
async setTag(entityUrl, key, req, token) {
|
|
3668
3792
|
const entity = await this.registry.getEntity(entityUrl);
|
|
3669
3793
|
if (!entity) throw new ElectricAgentsError(ErrCodeNotFound, `Entity not found`, 404);
|
|
@@ -6065,6 +6189,7 @@ async function handleStreamAppend(request, runtime, forward) {
|
|
|
6065
6189
|
const { manager } = runtime;
|
|
6066
6190
|
const entity = await manager.registry.getEntityByStream(path$1);
|
|
6067
6191
|
const isSharedState = path$1.startsWith(`/_electric/shared-state/`);
|
|
6192
|
+
if (!entity && manager.isAttachmentStreamPath(path$1)) return apiError(401, ErrCodeUnauthorized, `Invalid write token`);
|
|
6068
6193
|
if (!entity && !isSharedState) return void 0;
|
|
6069
6194
|
const body = await request.readBody();
|
|
6070
6195
|
const event = decodeStreamAppendEvent(body);
|
|
@@ -6633,8 +6758,9 @@ async function streamAppend(request, ctx) {
|
|
|
6633
6758
|
}));
|
|
6634
6759
|
}
|
|
6635
6760
|
async function proxyPassThrough(request, ctx) {
|
|
6636
|
-
const upstream = await forwardToDurableStreams(ctx, request);
|
|
6637
6761
|
const streamPath = new URL(request.url).pathname;
|
|
6762
|
+
if (ctx.entityManager?.isAttachmentStreamPath(streamPath)) return new Response(null, { status: 404 });
|
|
6763
|
+
const upstream = await forwardToDurableStreams(ctx, request);
|
|
6638
6764
|
const method = request.method.toUpperCase();
|
|
6639
6765
|
const endTrackedRead = method === `GET` ? await ctx.entityBridgeManager.beginClientRead(streamPath) : null;
|
|
6640
6766
|
try {
|
|
@@ -6787,6 +6913,13 @@ const eventSourceSubscriptionBodySchema = Type.Object({
|
|
|
6787
6913
|
lifetime: Type.Optional(subscriptionLifetimeSchema),
|
|
6788
6914
|
reason: Type.Optional(Type.String())
|
|
6789
6915
|
});
|
|
6916
|
+
const attachmentSubjectTypes = new Set([
|
|
6917
|
+
`inbox`,
|
|
6918
|
+
`run`,
|
|
6919
|
+
`text`,
|
|
6920
|
+
`tool_call`,
|
|
6921
|
+
`context`
|
|
6922
|
+
]);
|
|
6790
6923
|
const entitiesRouter = Router({ base: `/_electric/entities` });
|
|
6791
6924
|
entitiesRouter.get(`/`, listEntities);
|
|
6792
6925
|
entitiesRouter.put(`/:type/:instanceId`, withSpawnableEntityType, withSchema(spawnBodySchema), spawnEntity);
|
|
@@ -6795,6 +6928,9 @@ entitiesRouter.head(`/:type/:instanceId`, withExistingEntity, headEntity);
|
|
|
6795
6928
|
entitiesRouter.delete(`/:type/:instanceId`, withExistingEntity, killEntity);
|
|
6796
6929
|
entitiesRouter.post(`/:type/:instanceId/signal`, withExistingEntity, withSchema(signalBodySchema), signalEntity);
|
|
6797
6930
|
entitiesRouter.post(`/:type/:instanceId/send`, withExistingEntity, withSchema(sendBodySchema), sendEntity);
|
|
6931
|
+
entitiesRouter.post(`/:type/:instanceId/attachments`, withExistingEntity, createAttachment);
|
|
6932
|
+
entitiesRouter.get(`/:type/:instanceId/attachments/:attachmentId`, withExistingEntity, readAttachment);
|
|
6933
|
+
entitiesRouter.delete(`/:type/:instanceId/attachments/:attachmentId`, withExistingEntity, deleteAttachment);
|
|
6798
6934
|
entitiesRouter.patch(`/:type/:instanceId/inbox/:messageKey`, withExistingEntity, withSchema(inboxMessageBodySchema), updateInboxMessage);
|
|
6799
6935
|
entitiesRouter.delete(`/:type/:instanceId/inbox/:messageKey`, withExistingEntity, deleteInboxMessage);
|
|
6800
6936
|
entitiesRouter.post(`/:type/:instanceId/fork`, withExistingEntity, withSchema(forkBodySchema), forkEntity);
|
|
@@ -6821,6 +6957,82 @@ function requireExistingEntityRoute(request) {
|
|
|
6821
6957
|
if (!request.entityRoute) throw new Error(`existing entity middleware did not run`);
|
|
6822
6958
|
return request.entityRoute;
|
|
6823
6959
|
}
|
|
6960
|
+
function invalidAttachmentRequest(message) {
|
|
6961
|
+
throw new ElectricAgentsError(ErrCodeInvalidRequest, message, 400);
|
|
6962
|
+
}
|
|
6963
|
+
function formString(form, key) {
|
|
6964
|
+
const value = form.get(key);
|
|
6965
|
+
if (typeof value !== `string`) return void 0;
|
|
6966
|
+
const trimmed = value.trim();
|
|
6967
|
+
return trimmed || void 0;
|
|
6968
|
+
}
|
|
6969
|
+
function parseJsonFormField(form, key) {
|
|
6970
|
+
const raw = formString(form, key);
|
|
6971
|
+
if (!raw) return void 0;
|
|
6972
|
+
try {
|
|
6973
|
+
return JSON.parse(raw);
|
|
6974
|
+
} catch {
|
|
6975
|
+
invalidAttachmentRequest(`Invalid JSON field: ${key}`);
|
|
6976
|
+
}
|
|
6977
|
+
}
|
|
6978
|
+
function parseAttachmentSubject(form) {
|
|
6979
|
+
const explicit = parseJsonFormField(form, `subject`);
|
|
6980
|
+
if (explicit !== void 0) {
|
|
6981
|
+
if (!explicit || typeof explicit !== `object` || Array.isArray(explicit)) invalidAttachmentRequest(`attachment subject must be an object`);
|
|
6982
|
+
const subject = explicit;
|
|
6983
|
+
const type$1 = subject.type;
|
|
6984
|
+
const key$1 = subject.key;
|
|
6985
|
+
if (typeof type$1 !== `string` || typeof key$1 !== `string`) invalidAttachmentRequest(`attachment subject requires type and key`);
|
|
6986
|
+
if (!attachmentSubjectTypes.has(type$1)) invalidAttachmentRequest(`invalid attachment subject type`);
|
|
6987
|
+
return {
|
|
6988
|
+
type: type$1,
|
|
6989
|
+
key: key$1
|
|
6990
|
+
};
|
|
6991
|
+
}
|
|
6992
|
+
const type = formString(form, `subjectType`);
|
|
6993
|
+
const key = formString(form, `subjectKey`);
|
|
6994
|
+
if (!type || !key) invalidAttachmentRequest(`attachment subject is required`);
|
|
6995
|
+
if (!attachmentSubjectTypes.has(type)) invalidAttachmentRequest(`invalid attachment subject type`);
|
|
6996
|
+
return {
|
|
6997
|
+
type,
|
|
6998
|
+
key
|
|
6999
|
+
};
|
|
7000
|
+
}
|
|
7001
|
+
function getUploadedFormFile(value) {
|
|
7002
|
+
if (value !== null && typeof value === `object` && `arrayBuffer` in value && typeof value.arrayBuffer === `function`) return value;
|
|
7003
|
+
return null;
|
|
7004
|
+
}
|
|
7005
|
+
async function parseAttachmentForm(request) {
|
|
7006
|
+
const contentType = request.headers.get(`content-type`)?.toLowerCase() ?? ``;
|
|
7007
|
+
if (!contentType.includes(`multipart/form-data`)) invalidAttachmentRequest(`Attachment uploads must use multipart/form-data`);
|
|
7008
|
+
let form;
|
|
7009
|
+
try {
|
|
7010
|
+
form = await request.formData();
|
|
7011
|
+
} catch {
|
|
7012
|
+
invalidAttachmentRequest(`Invalid multipart form data`);
|
|
7013
|
+
}
|
|
7014
|
+
const file = getUploadedFormFile(form.get(`file`));
|
|
7015
|
+
if (!file) invalidAttachmentRequest(`Missing file field`);
|
|
7016
|
+
const role = formString(form, `role`);
|
|
7017
|
+
if (role !== void 0 && role !== `input` && role !== `output`) invalidAttachmentRequest(`invalid attachment role`);
|
|
7018
|
+
const fileName = formString(form, `filename`) ?? (typeof file.name === `string` ? file.name : void 0);
|
|
7019
|
+
const mimeType = formString(form, `mimeType`) || (typeof file.type === `string` ? file.type : void 0) || `application/octet-stream`;
|
|
7020
|
+
const meta = parseJsonFormField(form, `meta`);
|
|
7021
|
+
if (meta !== void 0 && (typeof meta !== `object` || Array.isArray(meta))) invalidAttachmentRequest(`attachment meta must be an object`);
|
|
7022
|
+
return {
|
|
7023
|
+
id: formString(form, `id`),
|
|
7024
|
+
bytes: new Uint8Array(await file.arrayBuffer()),
|
|
7025
|
+
mimeType,
|
|
7026
|
+
filename: fileName,
|
|
7027
|
+
subject: parseAttachmentSubject(form),
|
|
7028
|
+
role,
|
|
7029
|
+
meta
|
|
7030
|
+
};
|
|
7031
|
+
}
|
|
7032
|
+
function contentDisposition(filename) {
|
|
7033
|
+
const fallback = filename.replace(/["\\\r\n]/g, `_`);
|
|
7034
|
+
return `attachment; filename="${fallback}"; filename*=UTF-8''${encodeURIComponent(filename)}`;
|
|
7035
|
+
}
|
|
6824
7036
|
function rejectPrincipalEntityMutation(request, action) {
|
|
6825
7037
|
const { entity } = requireExistingEntityRoute(request);
|
|
6826
7038
|
if (entity.type !== `principal`) return void 0;
|
|
@@ -7005,6 +7217,44 @@ async function sendEntity(request, ctx) {
|
|
|
7005
7217
|
});
|
|
7006
7218
|
return status(204);
|
|
7007
7219
|
}
|
|
7220
|
+
async function createAttachment(request, ctx) {
|
|
7221
|
+
const principalMutationError = rejectPrincipalEntityMutation(request, `given attachments`);
|
|
7222
|
+
if (principalMutationError) return principalMutationError;
|
|
7223
|
+
const { entityUrl } = requireExistingEntityRoute(request);
|
|
7224
|
+
const form = await parseAttachmentForm(request);
|
|
7225
|
+
const result = await ctx.entityManager.createAttachment(entityUrl, {
|
|
7226
|
+
id: form.id,
|
|
7227
|
+
bytes: form.bytes,
|
|
7228
|
+
mimeType: form.mimeType,
|
|
7229
|
+
filename: form.filename,
|
|
7230
|
+
subject: form.subject,
|
|
7231
|
+
role: form.role,
|
|
7232
|
+
createdBy: ctx.principal.url,
|
|
7233
|
+
meta: form.meta
|
|
7234
|
+
});
|
|
7235
|
+
return json(result, { status: 201 });
|
|
7236
|
+
}
|
|
7237
|
+
async function readAttachment(request, ctx) {
|
|
7238
|
+
const { entityUrl } = requireExistingEntityRoute(request);
|
|
7239
|
+
const result = await ctx.entityManager.readAttachment(entityUrl, decodeURIComponent(request.params.attachmentId));
|
|
7240
|
+
const headers = new Headers({
|
|
7241
|
+
"content-type": result.attachment.mimeType,
|
|
7242
|
+
"content-length": String(result.bytes.length),
|
|
7243
|
+
"cache-control": `private, max-age=31536000, immutable`
|
|
7244
|
+
});
|
|
7245
|
+
if (result.attachment.filename) headers.set(`content-disposition`, contentDisposition(result.attachment.filename));
|
|
7246
|
+
return new Response(result.bytes, {
|
|
7247
|
+
status: 200,
|
|
7248
|
+
headers
|
|
7249
|
+
});
|
|
7250
|
+
}
|
|
7251
|
+
async function deleteAttachment(request, ctx) {
|
|
7252
|
+
const principalMutationError = rejectPrincipalEntityMutation(request, `stripped of attachments`);
|
|
7253
|
+
if (principalMutationError) return principalMutationError;
|
|
7254
|
+
const { entityUrl } = requireExistingEntityRoute(request);
|
|
7255
|
+
const result = await ctx.entityManager.deleteAttachment(entityUrl, decodeURIComponent(request.params.attachmentId));
|
|
7256
|
+
return json(result);
|
|
7257
|
+
}
|
|
7008
7258
|
async function updateInboxMessage(request, ctx) {
|
|
7009
7259
|
const parsed = routeBody(request);
|
|
7010
7260
|
const { entityUrl } = requireExistingEntityRoute(request);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@electric-ax/agents-server",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.13",
|
|
4
4
|
"description": "Electric Agents entity runtime server",
|
|
5
5
|
"author": "Durable Stream contributors",
|
|
6
6
|
"bin": {
|
|
@@ -36,9 +36,9 @@
|
|
|
36
36
|
"sideEffects": false,
|
|
37
37
|
"dependencies": {
|
|
38
38
|
"@anthropic-ai/sdk": "^0.78.0",
|
|
39
|
-
"@durable-streams/client": "
|
|
40
|
-
"@durable-streams/server": "
|
|
41
|
-
"@durable-streams/state": "
|
|
39
|
+
"@durable-streams/client": "^0.2.6",
|
|
40
|
+
"@durable-streams/server": "^0.3.5",
|
|
41
|
+
"@durable-streams/state": "^0.2.9",
|
|
42
42
|
"@electric-sql/client": "^1.5.19",
|
|
43
43
|
"@mariozechner/pi-agent-core": "^0.70.2",
|
|
44
44
|
"@opentelemetry/api": "^1.9.1",
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"pino-pretty": "^13.0.0",
|
|
55
55
|
"postgres": "^3.4.0",
|
|
56
56
|
"undici": "^7.24.7",
|
|
57
|
-
"@electric-ax/agents-runtime": "0.3.
|
|
57
|
+
"@electric-ax/agents-runtime": "0.3.6"
|
|
58
58
|
},
|
|
59
59
|
"devDependencies": {
|
|
60
60
|
"@types/node": "^22.19.15",
|
|
@@ -65,9 +65,9 @@
|
|
|
65
65
|
"tsx": "^4.19.0",
|
|
66
66
|
"typescript": "^5.0.0",
|
|
67
67
|
"vitest": "^4.1.0",
|
|
68
|
-
"@electric-ax/agents
|
|
69
|
-
"@electric-ax/agents": "0.
|
|
70
|
-
"@electric-ax/agents-server-ui": "0.4.
|
|
68
|
+
"@electric-ax/agents": "0.4.10",
|
|
69
|
+
"@electric-ax/agents-server-conformance-tests": "0.1.9",
|
|
70
|
+
"@electric-ax/agents-server-ui": "0.4.13"
|
|
71
71
|
},
|
|
72
72
|
"files": [
|
|
73
73
|
"dist",
|