@electric-ax/agents-server 0.4.11 → 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.cjs
CHANGED
|
@@ -1989,7 +1989,8 @@ var StreamClient = class {
|
|
|
1989
1989
|
url: this.streamUrl(path$2),
|
|
1990
1990
|
headers: this.streamHeaders(),
|
|
1991
1991
|
contentType: opts.contentType,
|
|
1992
|
-
body: opts.body
|
|
1992
|
+
body: opts.body,
|
|
1993
|
+
closed: opts.closed
|
|
1993
1994
|
});
|
|
1994
1995
|
});
|
|
1995
1996
|
}
|
|
@@ -2089,30 +2090,11 @@ var StreamClient = class {
|
|
|
2089
2090
|
offset: fromOffset ?? `-1`,
|
|
2090
2091
|
live: false
|
|
2091
2092
|
});
|
|
2092
|
-
const
|
|
2093
|
-
return
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
if (settled) return;
|
|
2098
|
-
settled = true;
|
|
2099
|
-
unsub();
|
|
2100
|
-
resolve$1(r);
|
|
2101
|
-
};
|
|
2102
|
-
unsub = response.subscribeBytes((chunk) => {
|
|
2103
|
-
messages.push({
|
|
2104
|
-
data: chunk.data,
|
|
2105
|
-
offset: chunk.offset
|
|
2106
|
-
});
|
|
2107
|
-
if (chunk.upToDate || chunk.streamClosed) finish({ messages });
|
|
2108
|
-
});
|
|
2109
|
-
response.closed.then(() => finish({ messages })).catch((err) => {
|
|
2110
|
-
if (settled) return;
|
|
2111
|
-
settled = true;
|
|
2112
|
-
unsub();
|
|
2113
|
-
reject(err);
|
|
2114
|
-
});
|
|
2115
|
-
});
|
|
2093
|
+
const body = await response.body();
|
|
2094
|
+
return { messages: body.length === 0 ? [] : [{
|
|
2095
|
+
data: body,
|
|
2096
|
+
offset: response.offset
|
|
2097
|
+
}] };
|
|
2116
2098
|
});
|
|
2117
2099
|
}
|
|
2118
2100
|
async readJson(path$2, fromOffset) {
|
|
@@ -2719,9 +2701,45 @@ function createInitialQueuePosition(date) {
|
|
|
2719
2701
|
const DEFAULT_FORK_WAIT_TIMEOUT_MS = 12e4;
|
|
2720
2702
|
const DEFAULT_FORK_WAIT_POLL_MS = 250;
|
|
2721
2703
|
const SERVER_SIGNAL_SENDER = `/_electric/server`;
|
|
2704
|
+
const DEFAULT_MAX_ATTACHMENT_BYTES = 25 * 1024 * 1024;
|
|
2722
2705
|
function sleep(ms) {
|
|
2723
2706
|
return new Promise((resolve$1) => setTimeout(resolve$1, ms));
|
|
2724
2707
|
}
|
|
2708
|
+
function maxAttachmentBytes() {
|
|
2709
|
+
const configured = Number(process.env.ELECTRIC_AGENTS_MAX_ATTACHMENT_BYTES);
|
|
2710
|
+
return Number.isFinite(configured) && configured > 0 ? Math.floor(configured) : DEFAULT_MAX_ATTACHMENT_BYTES;
|
|
2711
|
+
}
|
|
2712
|
+
function manifestAttachmentKey(id) {
|
|
2713
|
+
return `attachment:${id}`;
|
|
2714
|
+
}
|
|
2715
|
+
function getEntityAttachmentStreamPath(entityUrl, attachmentId) {
|
|
2716
|
+
return `${entityUrl.replace(/\/+$/, ``)}/attachments/${attachmentId}`;
|
|
2717
|
+
}
|
|
2718
|
+
function isStreamCreateConflict(error) {
|
|
2719
|
+
return !!error && typeof error === `object` && (`status` in error && error.status === 409 || `code` in error && error.code === `CONFLICT_SEQ`);
|
|
2720
|
+
}
|
|
2721
|
+
function assertCanonicalAttachmentStreamPath(entityUrl, attachment) {
|
|
2722
|
+
const expected = getEntityAttachmentStreamPath(entityUrl, attachment.id);
|
|
2723
|
+
if (attachment.streamPath === expected) return;
|
|
2724
|
+
throw new ElectricAgentsError(ErrCodeInvalidRequest, `Attachment stream path does not match its entity and id`, 409);
|
|
2725
|
+
}
|
|
2726
|
+
function validateAttachmentId(id) {
|
|
2727
|
+
if (!id || id.includes(`/`) || id.startsWith(`.`)) throw new ElectricAgentsError(ErrCodeInvalidRequest, `attachment id must not be empty, start with ".", or contain forward slashes`, 400);
|
|
2728
|
+
}
|
|
2729
|
+
function validateAttachmentSubject(subject) {
|
|
2730
|
+
if (!subject.key) throw new ElectricAgentsError(ErrCodeInvalidRequest, `attachment subject key is required`, 400);
|
|
2731
|
+
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);
|
|
2732
|
+
}
|
|
2733
|
+
function concatByteMessages(messages) {
|
|
2734
|
+
const total = messages.reduce((sum, message) => sum + message.data.length, 0);
|
|
2735
|
+
const bytes = new Uint8Array(total);
|
|
2736
|
+
let offset = 0;
|
|
2737
|
+
for (const message of messages) {
|
|
2738
|
+
bytes.set(message.data, offset);
|
|
2739
|
+
offset += message.data.length;
|
|
2740
|
+
}
|
|
2741
|
+
return bytes;
|
|
2742
|
+
}
|
|
2725
2743
|
function omitUndefined$1(value) {
|
|
2726
2744
|
return Object.fromEntries(Object.entries(value).filter(([, entry]) => entry !== void 0));
|
|
2727
2745
|
}
|
|
@@ -3087,6 +3105,15 @@ var EntityManager = class {
|
|
|
3087
3105
|
await this.streamClient.fork(forkPath, sourcePath);
|
|
3088
3106
|
createdStreams.push(forkPath);
|
|
3089
3107
|
}
|
|
3108
|
+
for (const plan of entityPlans) {
|
|
3109
|
+
const manifests = snapshot.manifestsByEntity.get(plan.source.url) ?? new Map();
|
|
3110
|
+
for (const manifest of manifests.values()) {
|
|
3111
|
+
if (manifest.kind !== `attachment` || typeof manifest.streamPath !== `string` || typeof manifest.id !== `string`) continue;
|
|
3112
|
+
const forkPath = getEntityAttachmentStreamPath(plan.fork.url, manifest.id);
|
|
3113
|
+
await this.streamClient.fork(forkPath, manifest.streamPath);
|
|
3114
|
+
createdStreams.push(forkPath);
|
|
3115
|
+
}
|
|
3116
|
+
}
|
|
3090
3117
|
for (const plan of entityPlans) {
|
|
3091
3118
|
const reconciliation = this.buildForkReconciliation(plan, snapshot, entityUrlMap, sharedStateIdMap, stringMap);
|
|
3092
3119
|
activeManifestsByEntity.set(plan.fork.url, reconciliation.manifests);
|
|
@@ -3496,6 +3523,16 @@ var EntityManager = class {
|
|
|
3496
3523
|
changed: true
|
|
3497
3524
|
};
|
|
3498
3525
|
}
|
|
3526
|
+
if (next.kind === `attachment` && typeof next.streamPath === `string` && typeof next.id === `string`) for (const [sourceUrl, forkUrl] of entityUrlMap) {
|
|
3527
|
+
const prefix = `${sourceUrl}/attachments/`;
|
|
3528
|
+
if (!next.streamPath.startsWith(prefix)) continue;
|
|
3529
|
+
next.streamPath = getEntityAttachmentStreamPath(forkUrl, next.id);
|
|
3530
|
+
return {
|
|
3531
|
+
key,
|
|
3532
|
+
value: next,
|
|
3533
|
+
changed: true
|
|
3534
|
+
};
|
|
3535
|
+
}
|
|
3499
3536
|
if (next.kind === `schedule` && next.scheduleType === `future_send`) {
|
|
3500
3537
|
let changed = false;
|
|
3501
3538
|
if (typeof next.targetUrl === `string`) {
|
|
@@ -3693,6 +3730,93 @@ var EntityManager = class {
|
|
|
3693
3730
|
const envelope = __electric_ax_agents_runtime.entityStateSchema.inbox.delete({ key });
|
|
3694
3731
|
await this.streamClient.append(entity.streams.main, this.encodeChangeEvent(envelope));
|
|
3695
3732
|
}
|
|
3733
|
+
isAttachmentStreamPath(path$2) {
|
|
3734
|
+
return /^\/[^/]+\/[^/]+\/attachments\/[^/]+$/.test(path$2);
|
|
3735
|
+
}
|
|
3736
|
+
async createAttachment(entityUrl, req) {
|
|
3737
|
+
const entity = await this.registry.getEntity(entityUrl);
|
|
3738
|
+
if (!entity) throw new ElectricAgentsError(ErrCodeNotFound, `Entity not found`, 404);
|
|
3739
|
+
if (rejectsNormalWrites(entity.status)) throw new ElectricAgentsError(ErrCodeNotRunning, `Entity is not accepting writes`, 409);
|
|
3740
|
+
if (this.isForkWorkLockedEntity(entityUrl)) this.assertEntityNotForkWorkLocked(entityUrl);
|
|
3741
|
+
const id = req.id ?? (0, node_crypto.randomUUID)();
|
|
3742
|
+
validateAttachmentId(id);
|
|
3743
|
+
validateAttachmentSubject(req.subject);
|
|
3744
|
+
const limit = maxAttachmentBytes();
|
|
3745
|
+
if (req.bytes.length > limit) throw new ElectricAgentsError(ErrCodeInvalidRequest, `Attachment exceeds maximum size of ${limit} bytes`, 413);
|
|
3746
|
+
const mimeType = req.mimeType.trim() || `application/octet-stream`;
|
|
3747
|
+
const streamPath = getEntityAttachmentStreamPath(entityUrl, id);
|
|
3748
|
+
const manifestKey = manifestAttachmentKey(id);
|
|
3749
|
+
const txid = (0, node_crypto.randomUUID)();
|
|
3750
|
+
const now = new Date().toISOString();
|
|
3751
|
+
const sha256 = (0, node_crypto.createHash)(`sha256`).update(req.bytes).digest(`hex`);
|
|
3752
|
+
const attachment = {
|
|
3753
|
+
key: manifestKey,
|
|
3754
|
+
kind: `attachment`,
|
|
3755
|
+
id,
|
|
3756
|
+
streamPath,
|
|
3757
|
+
status: `complete`,
|
|
3758
|
+
subject: req.subject,
|
|
3759
|
+
role: req.role ?? `input`,
|
|
3760
|
+
mimeType,
|
|
3761
|
+
...req.filename ? { filename: req.filename } : {},
|
|
3762
|
+
byteLength: req.bytes.length,
|
|
3763
|
+
sha256,
|
|
3764
|
+
createdAt: now,
|
|
3765
|
+
...req.createdBy ? { createdBy: req.createdBy } : {},
|
|
3766
|
+
...req.meta ? { meta: req.meta } : {}
|
|
3767
|
+
};
|
|
3768
|
+
let streamCreated = false;
|
|
3769
|
+
try {
|
|
3770
|
+
await this.streamClient.create(streamPath, {
|
|
3771
|
+
contentType: mimeType,
|
|
3772
|
+
body: req.bytes,
|
|
3773
|
+
closed: true
|
|
3774
|
+
});
|
|
3775
|
+
streamCreated = true;
|
|
3776
|
+
await this.writeManifestEntry(entityUrl, manifestKey, `upsert`, attachment, { txid });
|
|
3777
|
+
} catch (error) {
|
|
3778
|
+
if (streamCreated) await this.streamClient.delete(streamPath).catch(() => void 0);
|
|
3779
|
+
if (!streamCreated && isStreamCreateConflict(error)) throw new ElectricAgentsError(ErrCodeInvalidRequest, `Attachment already exists at id "${id}"`, 409);
|
|
3780
|
+
throw error;
|
|
3781
|
+
}
|
|
3782
|
+
return {
|
|
3783
|
+
txid,
|
|
3784
|
+
attachment
|
|
3785
|
+
};
|
|
3786
|
+
}
|
|
3787
|
+
async getAttachment(entityUrl, id) {
|
|
3788
|
+
validateAttachmentId(id);
|
|
3789
|
+
const entity = await this.registry.getEntity(entityUrl);
|
|
3790
|
+
if (!entity) throw new ElectricAgentsError(ErrCodeNotFound, `Entity not found`, 404);
|
|
3791
|
+
const events = await this.streamClient.readJson(entity.streams.main);
|
|
3792
|
+
const manifest = this.reduceStateRows(events, `manifest`).get(manifestAttachmentKey(id));
|
|
3793
|
+
if (!manifest || manifest.kind !== `attachment`) return null;
|
|
3794
|
+
return manifest;
|
|
3795
|
+
}
|
|
3796
|
+
async readAttachment(entityUrl, id) {
|
|
3797
|
+
const attachment = await this.getAttachment(entityUrl, id);
|
|
3798
|
+
if (!attachment) throw new ElectricAgentsError(ErrCodeNotFound, `Attachment not found`, 404);
|
|
3799
|
+
if (attachment.status !== `complete`) throw new ElectricAgentsError(ErrCodeInvalidRequest, `Attachment is not complete`, 409);
|
|
3800
|
+
assertCanonicalAttachmentStreamPath(entityUrl, attachment);
|
|
3801
|
+
const result = await this.streamClient.read(attachment.streamPath);
|
|
3802
|
+
return {
|
|
3803
|
+
attachment,
|
|
3804
|
+
bytes: concatByteMessages(result.messages)
|
|
3805
|
+
};
|
|
3806
|
+
}
|
|
3807
|
+
async deleteAttachment(entityUrl, id) {
|
|
3808
|
+
const entity = await this.registry.getEntity(entityUrl);
|
|
3809
|
+
if (!entity) throw new ElectricAgentsError(ErrCodeNotFound, `Entity not found`, 404);
|
|
3810
|
+
if (rejectsNormalWrites(entity.status)) throw new ElectricAgentsError(ErrCodeNotRunning, `Entity is not accepting writes`, 409);
|
|
3811
|
+
if (this.isForkWorkLockedEntity(entityUrl)) this.assertEntityNotForkWorkLocked(entityUrl);
|
|
3812
|
+
const attachment = await this.getAttachment(entityUrl, id);
|
|
3813
|
+
if (!attachment) throw new ElectricAgentsError(ErrCodeNotFound, `Attachment not found`, 404);
|
|
3814
|
+
assertCanonicalAttachmentStreamPath(entityUrl, attachment);
|
|
3815
|
+
const txid = (0, node_crypto.randomUUID)();
|
|
3816
|
+
await this.writeManifestEntry(entityUrl, manifestAttachmentKey(id), `delete`, void 0, { txid });
|
|
3817
|
+
await this.streamClient.delete(attachment.streamPath).catch(() => void 0);
|
|
3818
|
+
return { txid };
|
|
3819
|
+
}
|
|
3696
3820
|
async setTag(entityUrl, key, req, token) {
|
|
3697
3821
|
const entity = await this.registry.getEntity(entityUrl);
|
|
3698
3822
|
if (!entity) throw new ElectricAgentsError(ErrCodeNotFound, `Entity not found`, 404);
|
|
@@ -6094,6 +6218,7 @@ async function handleStreamAppend(request, runtime, forward) {
|
|
|
6094
6218
|
const { manager } = runtime;
|
|
6095
6219
|
const entity = await manager.registry.getEntityByStream(path$2);
|
|
6096
6220
|
const isSharedState = path$2.startsWith(`/_electric/shared-state/`);
|
|
6221
|
+
if (!entity && manager.isAttachmentStreamPath(path$2)) return apiError(401, ErrCodeUnauthorized, `Invalid write token`);
|
|
6097
6222
|
if (!entity && !isSharedState) return void 0;
|
|
6098
6223
|
const body = await request.readBody();
|
|
6099
6224
|
const event = decodeStreamAppendEvent(body);
|
|
@@ -6662,8 +6787,9 @@ async function streamAppend(request, ctx) {
|
|
|
6662
6787
|
}));
|
|
6663
6788
|
}
|
|
6664
6789
|
async function proxyPassThrough(request, ctx) {
|
|
6665
|
-
const upstream = await forwardToDurableStreams(ctx, request);
|
|
6666
6790
|
const streamPath = new URL(request.url).pathname;
|
|
6791
|
+
if (ctx.entityManager?.isAttachmentStreamPath(streamPath)) return new Response(null, { status: 404 });
|
|
6792
|
+
const upstream = await forwardToDurableStreams(ctx, request);
|
|
6667
6793
|
const method = request.method.toUpperCase();
|
|
6668
6794
|
const endTrackedRead = method === `GET` ? await ctx.entityBridgeManager.beginClientRead(streamPath) : null;
|
|
6669
6795
|
try {
|
|
@@ -6816,6 +6942,13 @@ const eventSourceSubscriptionBodySchema = __sinclair_typebox.Type.Object({
|
|
|
6816
6942
|
lifetime: __sinclair_typebox.Type.Optional(subscriptionLifetimeSchema),
|
|
6817
6943
|
reason: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String())
|
|
6818
6944
|
});
|
|
6945
|
+
const attachmentSubjectTypes = new Set([
|
|
6946
|
+
`inbox`,
|
|
6947
|
+
`run`,
|
|
6948
|
+
`text`,
|
|
6949
|
+
`tool_call`,
|
|
6950
|
+
`context`
|
|
6951
|
+
]);
|
|
6819
6952
|
const entitiesRouter = (0, itty_router.Router)({ base: `/_electric/entities` });
|
|
6820
6953
|
entitiesRouter.get(`/`, listEntities);
|
|
6821
6954
|
entitiesRouter.put(`/:type/:instanceId`, withSpawnableEntityType, withSchema(spawnBodySchema), spawnEntity);
|
|
@@ -6824,6 +6957,9 @@ entitiesRouter.head(`/:type/:instanceId`, withExistingEntity, headEntity);
|
|
|
6824
6957
|
entitiesRouter.delete(`/:type/:instanceId`, withExistingEntity, killEntity);
|
|
6825
6958
|
entitiesRouter.post(`/:type/:instanceId/signal`, withExistingEntity, withSchema(signalBodySchema), signalEntity);
|
|
6826
6959
|
entitiesRouter.post(`/:type/:instanceId/send`, withExistingEntity, withSchema(sendBodySchema), sendEntity);
|
|
6960
|
+
entitiesRouter.post(`/:type/:instanceId/attachments`, withExistingEntity, createAttachment);
|
|
6961
|
+
entitiesRouter.get(`/:type/:instanceId/attachments/:attachmentId`, withExistingEntity, readAttachment);
|
|
6962
|
+
entitiesRouter.delete(`/:type/:instanceId/attachments/:attachmentId`, withExistingEntity, deleteAttachment);
|
|
6827
6963
|
entitiesRouter.patch(`/:type/:instanceId/inbox/:messageKey`, withExistingEntity, withSchema(inboxMessageBodySchema), updateInboxMessage);
|
|
6828
6964
|
entitiesRouter.delete(`/:type/:instanceId/inbox/:messageKey`, withExistingEntity, deleteInboxMessage);
|
|
6829
6965
|
entitiesRouter.post(`/:type/:instanceId/fork`, withExistingEntity, withSchema(forkBodySchema), forkEntity);
|
|
@@ -6850,6 +6986,82 @@ function requireExistingEntityRoute(request) {
|
|
|
6850
6986
|
if (!request.entityRoute) throw new Error(`existing entity middleware did not run`);
|
|
6851
6987
|
return request.entityRoute;
|
|
6852
6988
|
}
|
|
6989
|
+
function invalidAttachmentRequest(message) {
|
|
6990
|
+
throw new ElectricAgentsError(ErrCodeInvalidRequest, message, 400);
|
|
6991
|
+
}
|
|
6992
|
+
function formString(form, key) {
|
|
6993
|
+
const value = form.get(key);
|
|
6994
|
+
if (typeof value !== `string`) return void 0;
|
|
6995
|
+
const trimmed = value.trim();
|
|
6996
|
+
return trimmed || void 0;
|
|
6997
|
+
}
|
|
6998
|
+
function parseJsonFormField(form, key) {
|
|
6999
|
+
const raw = formString(form, key);
|
|
7000
|
+
if (!raw) return void 0;
|
|
7001
|
+
try {
|
|
7002
|
+
return JSON.parse(raw);
|
|
7003
|
+
} catch {
|
|
7004
|
+
invalidAttachmentRequest(`Invalid JSON field: ${key}`);
|
|
7005
|
+
}
|
|
7006
|
+
}
|
|
7007
|
+
function parseAttachmentSubject(form) {
|
|
7008
|
+
const explicit = parseJsonFormField(form, `subject`);
|
|
7009
|
+
if (explicit !== void 0) {
|
|
7010
|
+
if (!explicit || typeof explicit !== `object` || Array.isArray(explicit)) invalidAttachmentRequest(`attachment subject must be an object`);
|
|
7011
|
+
const subject = explicit;
|
|
7012
|
+
const type$1 = subject.type;
|
|
7013
|
+
const key$1 = subject.key;
|
|
7014
|
+
if (typeof type$1 !== `string` || typeof key$1 !== `string`) invalidAttachmentRequest(`attachment subject requires type and key`);
|
|
7015
|
+
if (!attachmentSubjectTypes.has(type$1)) invalidAttachmentRequest(`invalid attachment subject type`);
|
|
7016
|
+
return {
|
|
7017
|
+
type: type$1,
|
|
7018
|
+
key: key$1
|
|
7019
|
+
};
|
|
7020
|
+
}
|
|
7021
|
+
const type = formString(form, `subjectType`);
|
|
7022
|
+
const key = formString(form, `subjectKey`);
|
|
7023
|
+
if (!type || !key) invalidAttachmentRequest(`attachment subject is required`);
|
|
7024
|
+
if (!attachmentSubjectTypes.has(type)) invalidAttachmentRequest(`invalid attachment subject type`);
|
|
7025
|
+
return {
|
|
7026
|
+
type,
|
|
7027
|
+
key
|
|
7028
|
+
};
|
|
7029
|
+
}
|
|
7030
|
+
function getUploadedFormFile(value) {
|
|
7031
|
+
if (value !== null && typeof value === `object` && `arrayBuffer` in value && typeof value.arrayBuffer === `function`) return value;
|
|
7032
|
+
return null;
|
|
7033
|
+
}
|
|
7034
|
+
async function parseAttachmentForm(request) {
|
|
7035
|
+
const contentType = request.headers.get(`content-type`)?.toLowerCase() ?? ``;
|
|
7036
|
+
if (!contentType.includes(`multipart/form-data`)) invalidAttachmentRequest(`Attachment uploads must use multipart/form-data`);
|
|
7037
|
+
let form;
|
|
7038
|
+
try {
|
|
7039
|
+
form = await request.formData();
|
|
7040
|
+
} catch {
|
|
7041
|
+
invalidAttachmentRequest(`Invalid multipart form data`);
|
|
7042
|
+
}
|
|
7043
|
+
const file = getUploadedFormFile(form.get(`file`));
|
|
7044
|
+
if (!file) invalidAttachmentRequest(`Missing file field`);
|
|
7045
|
+
const role = formString(form, `role`);
|
|
7046
|
+
if (role !== void 0 && role !== `input` && role !== `output`) invalidAttachmentRequest(`invalid attachment role`);
|
|
7047
|
+
const fileName = formString(form, `filename`) ?? (typeof file.name === `string` ? file.name : void 0);
|
|
7048
|
+
const mimeType = formString(form, `mimeType`) || (typeof file.type === `string` ? file.type : void 0) || `application/octet-stream`;
|
|
7049
|
+
const meta = parseJsonFormField(form, `meta`);
|
|
7050
|
+
if (meta !== void 0 && (typeof meta !== `object` || Array.isArray(meta))) invalidAttachmentRequest(`attachment meta must be an object`);
|
|
7051
|
+
return {
|
|
7052
|
+
id: formString(form, `id`),
|
|
7053
|
+
bytes: new Uint8Array(await file.arrayBuffer()),
|
|
7054
|
+
mimeType,
|
|
7055
|
+
filename: fileName,
|
|
7056
|
+
subject: parseAttachmentSubject(form),
|
|
7057
|
+
role,
|
|
7058
|
+
meta
|
|
7059
|
+
};
|
|
7060
|
+
}
|
|
7061
|
+
function contentDisposition(filename) {
|
|
7062
|
+
const fallback = filename.replace(/["\\\r\n]/g, `_`);
|
|
7063
|
+
return `attachment; filename="${fallback}"; filename*=UTF-8''${encodeURIComponent(filename)}`;
|
|
7064
|
+
}
|
|
6853
7065
|
function rejectPrincipalEntityMutation(request, action) {
|
|
6854
7066
|
const { entity } = requireExistingEntityRoute(request);
|
|
6855
7067
|
if (entity.type !== `principal`) return void 0;
|
|
@@ -7034,6 +7246,44 @@ async function sendEntity(request, ctx) {
|
|
|
7034
7246
|
});
|
|
7035
7247
|
return (0, itty_router.status)(204);
|
|
7036
7248
|
}
|
|
7249
|
+
async function createAttachment(request, ctx) {
|
|
7250
|
+
const principalMutationError = rejectPrincipalEntityMutation(request, `given attachments`);
|
|
7251
|
+
if (principalMutationError) return principalMutationError;
|
|
7252
|
+
const { entityUrl } = requireExistingEntityRoute(request);
|
|
7253
|
+
const form = await parseAttachmentForm(request);
|
|
7254
|
+
const result = await ctx.entityManager.createAttachment(entityUrl, {
|
|
7255
|
+
id: form.id,
|
|
7256
|
+
bytes: form.bytes,
|
|
7257
|
+
mimeType: form.mimeType,
|
|
7258
|
+
filename: form.filename,
|
|
7259
|
+
subject: form.subject,
|
|
7260
|
+
role: form.role,
|
|
7261
|
+
createdBy: ctx.principal.url,
|
|
7262
|
+
meta: form.meta
|
|
7263
|
+
});
|
|
7264
|
+
return (0, itty_router.json)(result, { status: 201 });
|
|
7265
|
+
}
|
|
7266
|
+
async function readAttachment(request, ctx) {
|
|
7267
|
+
const { entityUrl } = requireExistingEntityRoute(request);
|
|
7268
|
+
const result = await ctx.entityManager.readAttachment(entityUrl, decodeURIComponent(request.params.attachmentId));
|
|
7269
|
+
const headers = new Headers({
|
|
7270
|
+
"content-type": result.attachment.mimeType,
|
|
7271
|
+
"content-length": String(result.bytes.length),
|
|
7272
|
+
"cache-control": `private, max-age=31536000, immutable`
|
|
7273
|
+
});
|
|
7274
|
+
if (result.attachment.filename) headers.set(`content-disposition`, contentDisposition(result.attachment.filename));
|
|
7275
|
+
return new Response(result.bytes, {
|
|
7276
|
+
status: 200,
|
|
7277
|
+
headers
|
|
7278
|
+
});
|
|
7279
|
+
}
|
|
7280
|
+
async function deleteAttachment(request, ctx) {
|
|
7281
|
+
const principalMutationError = rejectPrincipalEntityMutation(request, `stripped of attachments`);
|
|
7282
|
+
if (principalMutationError) return principalMutationError;
|
|
7283
|
+
const { entityUrl } = requireExistingEntityRoute(request);
|
|
7284
|
+
const result = await ctx.entityManager.deleteAttachment(entityUrl, decodeURIComponent(request.params.attachmentId));
|
|
7285
|
+
return (0, itty_router.json)(result);
|
|
7286
|
+
}
|
|
7037
7287
|
async function updateInboxMessage(request, ctx) {
|
|
7038
7288
|
const parsed = routeBody(request);
|
|
7039
7289
|
const { entityUrl } = requireExistingEntityRoute(request);
|
package/dist/index.d.cts
CHANGED
|
@@ -3723,6 +3723,7 @@ declare class StreamClient {
|
|
|
3723
3723
|
create(path: string, opts: {
|
|
3724
3724
|
contentType: string;
|
|
3725
3725
|
body?: Uint8Array | string;
|
|
3726
|
+
closed?: boolean;
|
|
3726
3727
|
}): Promise<void>;
|
|
3727
3728
|
fork(path: string, sourcePath: string): Promise<void>;
|
|
3728
3729
|
append(path: string, data: Uint8Array | string, opts?: {
|
|
@@ -4075,6 +4076,45 @@ declare class SchemaValidator {
|
|
|
4075
4076
|
//#endregion
|
|
4076
4077
|
//#region src/entity-manager.d.ts
|
|
4077
4078
|
type WriteTokenValidator = (entity: ElectricAgentsEntity, token: string) => boolean;
|
|
4079
|
+
type AttachmentSubjectType = `inbox` | `run` | `text` | `tool_call` | `context`;
|
|
4080
|
+
type AttachmentRole = `input` | `output`;
|
|
4081
|
+
interface CreateAttachmentRequest {
|
|
4082
|
+
id?: string;
|
|
4083
|
+
bytes: Uint8Array;
|
|
4084
|
+
mimeType: string;
|
|
4085
|
+
filename?: string;
|
|
4086
|
+
subject: {
|
|
4087
|
+
type: AttachmentSubjectType;
|
|
4088
|
+
key: string;
|
|
4089
|
+
};
|
|
4090
|
+
role?: AttachmentRole;
|
|
4091
|
+
createdBy?: string;
|
|
4092
|
+
meta?: Record<string, unknown>;
|
|
4093
|
+
}
|
|
4094
|
+
interface ReadAttachmentResult {
|
|
4095
|
+
attachment: ManifestAttachmentEntry;
|
|
4096
|
+
bytes: Uint8Array;
|
|
4097
|
+
}
|
|
4098
|
+
type ManifestAttachmentEntry = {
|
|
4099
|
+
key: string;
|
|
4100
|
+
kind: `attachment`;
|
|
4101
|
+
id: string;
|
|
4102
|
+
streamPath: string;
|
|
4103
|
+
status: `pending` | `complete` | `failed`;
|
|
4104
|
+
subject: {
|
|
4105
|
+
type: AttachmentSubjectType;
|
|
4106
|
+
key: string;
|
|
4107
|
+
};
|
|
4108
|
+
role: AttachmentRole;
|
|
4109
|
+
mimeType: string;
|
|
4110
|
+
filename?: string;
|
|
4111
|
+
byteLength?: number;
|
|
4112
|
+
sha256?: string;
|
|
4113
|
+
createdAt: string;
|
|
4114
|
+
createdBy?: string;
|
|
4115
|
+
error?: string;
|
|
4116
|
+
meta?: Record<string, unknown>;
|
|
4117
|
+
};
|
|
4078
4118
|
type ForkSubtreeOptions = {
|
|
4079
4119
|
rootInstanceId?: string;
|
|
4080
4120
|
waitTimeoutMs?: number;
|
|
@@ -4171,6 +4211,16 @@ declare class EntityManager {
|
|
|
4171
4211
|
status?: `pending` | `processed` | `cancelled`;
|
|
4172
4212
|
}): Promise<void>;
|
|
4173
4213
|
deleteInboxMessage(entityUrl: string, key: string): Promise<void>;
|
|
4214
|
+
isAttachmentStreamPath(path: string): boolean;
|
|
4215
|
+
createAttachment(entityUrl: string, req: CreateAttachmentRequest): Promise<{
|
|
4216
|
+
txid: string;
|
|
4217
|
+
attachment: ManifestAttachmentEntry;
|
|
4218
|
+
}>;
|
|
4219
|
+
getAttachment(entityUrl: string, id: string): Promise<ManifestAttachmentEntry | null>;
|
|
4220
|
+
readAttachment(entityUrl: string, id: string): Promise<ReadAttachmentResult>;
|
|
4221
|
+
deleteAttachment(entityUrl: string, id: string): Promise<{
|
|
4222
|
+
txid: string;
|
|
4223
|
+
}>;
|
|
4174
4224
|
setTag(entityUrl: string, key: string, req: SetTagRequest, token: string): Promise<ElectricAgentsEntity>;
|
|
4175
4225
|
deleteTag(entityUrl: string, key: string, token: string): Promise<ElectricAgentsEntity>;
|
|
4176
4226
|
ensureEntitiesMembershipStream(tags: Record<string, string>): Promise<{
|
package/dist/index.d.ts
CHANGED
|
@@ -3724,6 +3724,7 @@ declare class StreamClient {
|
|
|
3724
3724
|
create(path: string, opts: {
|
|
3725
3725
|
contentType: string;
|
|
3726
3726
|
body?: Uint8Array | string;
|
|
3727
|
+
closed?: boolean;
|
|
3727
3728
|
}): Promise<void>;
|
|
3728
3729
|
fork(path: string, sourcePath: string): Promise<void>;
|
|
3729
3730
|
append(path: string, data: Uint8Array | string, opts?: {
|
|
@@ -4076,6 +4077,45 @@ declare class SchemaValidator {
|
|
|
4076
4077
|
//#endregion
|
|
4077
4078
|
//#region src/entity-manager.d.ts
|
|
4078
4079
|
type WriteTokenValidator = (entity: ElectricAgentsEntity, token: string) => boolean;
|
|
4080
|
+
type AttachmentSubjectType = `inbox` | `run` | `text` | `tool_call` | `context`;
|
|
4081
|
+
type AttachmentRole = `input` | `output`;
|
|
4082
|
+
interface CreateAttachmentRequest {
|
|
4083
|
+
id?: string;
|
|
4084
|
+
bytes: Uint8Array;
|
|
4085
|
+
mimeType: string;
|
|
4086
|
+
filename?: string;
|
|
4087
|
+
subject: {
|
|
4088
|
+
type: AttachmentSubjectType;
|
|
4089
|
+
key: string;
|
|
4090
|
+
};
|
|
4091
|
+
role?: AttachmentRole;
|
|
4092
|
+
createdBy?: string;
|
|
4093
|
+
meta?: Record<string, unknown>;
|
|
4094
|
+
}
|
|
4095
|
+
interface ReadAttachmentResult {
|
|
4096
|
+
attachment: ManifestAttachmentEntry;
|
|
4097
|
+
bytes: Uint8Array;
|
|
4098
|
+
}
|
|
4099
|
+
type ManifestAttachmentEntry = {
|
|
4100
|
+
key: string;
|
|
4101
|
+
kind: `attachment`;
|
|
4102
|
+
id: string;
|
|
4103
|
+
streamPath: string;
|
|
4104
|
+
status: `pending` | `complete` | `failed`;
|
|
4105
|
+
subject: {
|
|
4106
|
+
type: AttachmentSubjectType;
|
|
4107
|
+
key: string;
|
|
4108
|
+
};
|
|
4109
|
+
role: AttachmentRole;
|
|
4110
|
+
mimeType: string;
|
|
4111
|
+
filename?: string;
|
|
4112
|
+
byteLength?: number;
|
|
4113
|
+
sha256?: string;
|
|
4114
|
+
createdAt: string;
|
|
4115
|
+
createdBy?: string;
|
|
4116
|
+
error?: string;
|
|
4117
|
+
meta?: Record<string, unknown>;
|
|
4118
|
+
};
|
|
4079
4119
|
type ForkSubtreeOptions = {
|
|
4080
4120
|
rootInstanceId?: string;
|
|
4081
4121
|
waitTimeoutMs?: number;
|
|
@@ -4172,6 +4212,16 @@ declare class EntityManager {
|
|
|
4172
4212
|
status?: `pending` | `processed` | `cancelled`;
|
|
4173
4213
|
}): Promise<void>;
|
|
4174
4214
|
deleteInboxMessage(entityUrl: string, key: string): Promise<void>;
|
|
4215
|
+
isAttachmentStreamPath(path: string): boolean;
|
|
4216
|
+
createAttachment(entityUrl: string, req: CreateAttachmentRequest): Promise<{
|
|
4217
|
+
txid: string;
|
|
4218
|
+
attachment: ManifestAttachmentEntry;
|
|
4219
|
+
}>;
|
|
4220
|
+
getAttachment(entityUrl: string, id: string): Promise<ManifestAttachmentEntry | null>;
|
|
4221
|
+
readAttachment(entityUrl: string, id: string): Promise<ReadAttachmentResult>;
|
|
4222
|
+
deleteAttachment(entityUrl: string, id: string): Promise<{
|
|
4223
|
+
txid: string;
|
|
4224
|
+
}>;
|
|
4175
4225
|
setTag(entityUrl: string, key: string, req: SetTagRequest, token: string): Promise<ElectricAgentsEntity>;
|
|
4176
4226
|
deleteTag(entityUrl: string, key: string, token: string): Promise<ElectricAgentsEntity>;
|
|
4177
4227
|
ensureEntitiesMembershipStream(tags: Record<string, string>): Promise<{
|