@coffer-org/plugin-http 1.3.1 → 2.0.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.
@@ -0,0 +1,20 @@
1
+ import type { ConvMessage } from '@coffer-org/plugin-orchestrator/runtime';
2
+ export declare function recordUser(m: {
3
+ chatId: string;
4
+ msgId: string;
5
+ sender?: string | null;
6
+ text: string;
7
+ ts: number;
8
+ replyToId: string | null;
9
+ }): Promise<void>;
10
+ export declare function recordAssistant(m: {
11
+ chatId: string;
12
+ parentMsgId: string | null;
13
+ botMsgId: string | null;
14
+ text: string;
15
+ now: number;
16
+ }): Promise<boolean>;
17
+ export declare function buildChain(headMsgId: string, opts: {
18
+ chatId: string;
19
+ maxDepth?: number;
20
+ }): Promise<ConvMessage[]>;
@@ -0,0 +1,35 @@
1
+ import { getThreadMessage, putThreadMessage } from '@coffer-org/server/thread-store';
2
+ import { getLogger } from '@coffer-org/sdk/logger';
3
+ const log = getLogger('http-connector');
4
+ const CONNECTOR = 'http';
5
+ const DEFAULT_MAX_DEPTH = 30;
6
+ export async function recordUser(m) {
7
+ await putThreadMessage({ connector: CONNECTOR, chatId: m.chatId, msgId: m.msgId, role: 'user', sender: m.sender ?? null, text: m.text, ts: m.ts, replyToId: m.replyToId });
8
+ }
9
+ export async function recordAssistant(m) {
10
+ if (m.botMsgId == null || !m.text.trim())
11
+ return false;
12
+ await putThreadMessage({ connector: CONNECTOR, chatId: m.chatId, msgId: m.botMsgId, role: 'assistant', text: m.text, ts: m.now, replyToId: m.parentMsgId });
13
+ return true;
14
+ }
15
+ export async function buildChain(headMsgId, opts) {
16
+ const max = opts.maxDepth ?? DEFAULT_MAX_DEPTH;
17
+ const acc = [];
18
+ let cur = headMsgId;
19
+ const seen = new Set();
20
+ while (cur && acc.length < max && !seen.has(cur)) {
21
+ seen.add(cur);
22
+ const stored = await getThreadMessage(CONNECTOR, opts.chatId, cur);
23
+ if (!stored)
24
+ break;
25
+ if (stored.role !== 'reasoning') {
26
+ acc.push({ role: stored.role, content: stored.text, sender: stored.sender, msgId: stored.msgId, ts: stored.ts });
27
+ }
28
+ cur = stored.replyToId;
29
+ }
30
+ if (acc.length >= max)
31
+ log.info(`chain capped at ${max} for ${headMsgId}`);
32
+ while (acc.length && acc.at(-1)?.role !== 'user')
33
+ acc.pop();
34
+ return acc.reverse();
35
+ }
@@ -1,6 +1,13 @@
1
1
  import type { Connector } from '@coffer-org/plugin-orchestrator/runtime';
2
+ import { recordAssistant as storeAssistant } from './chain-store.ts';
2
3
  export interface BufferedConnector {
3
4
  connector: Connector;
4
5
  drain(): string[];
6
+ recorded(): string | null;
5
7
  }
6
- export declare function makeBufferedConnector(): BufferedConnector;
8
+ export interface BufferedConnectorOpts {
9
+ chatId: string;
10
+ botMsgId: string;
11
+ record?: typeof storeAssistant;
12
+ }
13
+ export declare function makeBufferedConnector(opts: BufferedConnectorOpts): BufferedConnector;
@@ -1,17 +1,32 @@
1
- export function makeBufferedConnector() {
1
+ import { plainRender } from '@coffer-org/plugin-orchestrator/runtime';
2
+ import { recordAssistant as storeAssistant } from "./chain-store.js";
3
+ const HTTP_MAX = 100_000;
4
+ export function makeBufferedConnector(opts) {
2
5
  const buffer = [];
3
- let counter = 0;
4
- const capabilities = {
5
- streaming: false,
6
- typing: false,
7
- maxMessageLength: 100_000,
8
- };
6
+ const record = opts.record ?? storeAssistant;
7
+ let recordedId = null;
9
8
  const connector = {
10
9
  id: 'http',
11
- capabilities,
12
- async sendMessage(_chatId, text) {
13
- buffer.push(text);
14
- return { msgId: String(counter++) };
10
+ reply(_chatId, _ctx) {
11
+ return {
12
+ update() { },
13
+ segment() { },
14
+ async finish(r) {
15
+ for (const part of plainRender(HTTP_MAX)(r))
16
+ buffer.push(part);
17
+ return opts.botMsgId;
18
+ },
19
+ };
20
+ },
21
+ async recordAssistant(m) {
22
+ const wrote = await record({
23
+ chatId: opts.chatId,
24
+ parentMsgId: m.parentMsgId,
25
+ botMsgId: m.botMsgId,
26
+ text: m.text,
27
+ now: Math.floor(Date.now() / 1000),
28
+ });
29
+ recordedId = wrote ? m.botMsgId : null;
15
30
  },
16
31
  };
17
32
  return {
@@ -19,5 +34,8 @@ export function makeBufferedConnector() {
19
34
  drain() {
20
35
  return buffer.splice(0);
21
36
  },
37
+ recorded() {
38
+ return recordedId;
39
+ },
22
40
  };
23
41
  }
@@ -0,0 +1 @@
1
+ export declare const HTTP_FORMAT: string;
@@ -0,0 +1,12 @@
1
+ export const HTTP_FORMAT = [
2
+ 'CRITICAL OUTPUT FORMAT — HTML ONLY. Format the reply strictly as HTML and NOTHING else. NEVER use Markdown — no *bold*, _italic_, **bold**, `code`, ```blocks```, # headings, - bullets, or | tables. Markdown does not render here; it shows up as literal characters. When in doubt, use an HTML tag or plain text.',
3
+ 'Inline styling (tags may nest): <b>bold</b>, <i>italic</i>, <u>underline</u>, <s>strike</s>, <code>inline code</code>. Links: <a href="https://…">text</a> — the URL is shown next to the text, so do not repeat it in prose.',
4
+ 'Blocks: <p>paragraph</p>, <h1>…</h1> through <h6>…</h6> for headings, <blockquote>quote</blockquote>, <hr> for a separator, <br> for a line break.',
5
+ 'Lists: <ul><li>item</li></ul> for bullets, <ol><li>item</li></ol> for numbered steps. Do not hand-write "• " or "1. " prefixes — the tags render them.',
6
+ 'Tables render for real — use <table><thead><tr><th>Col</th></tr></thead><tbody><tr><td>cell</td></tr></tbody></table>. Do NOT flatten a table into <pre> or ASCII art.',
7
+ 'Code blocks: <pre><code class="language-python">…</code></pre> — the language class enables syntax highlight. Plain <pre>…</pre> works for non-code preformatted text.',
8
+ 'Telegram-only tags do NOT work here — never use <spoiler>, <tg-spoiler>, <tg-emoji>, or tg:// links.',
9
+ 'Escape literal <, >, and & in prose as &lt;, &gt;, and &amp; so they are not read as tags.',
10
+ 'Beyond the tags, use Unicode symbols and emoji to structure and enrich replies: separators (─────), arrows (→), status marks (✅ ⚠️ ❌ ⏳), and topic emoji (📅 💰 🏥 🛒 🌱 …). They make replies scannable.',
11
+ 'The reply is read in a terminal — keep it short and skimmable: prefer short paragraphs and lists over long prose, and keep table columns narrow enough to fit an ~80-column width.',
12
+ ].join('\n');
@@ -2,5 +2,7 @@ import type { PluginHooks } from '@coffer-org/server/plugin-hooks';
2
2
  export type { HttpConfig, LoadOpts } from './config.ts';
3
3
  export { loadHttpConfig, policy } from './config.ts';
4
4
  export { makeBufferedConnector } from './connector.ts';
5
+ export { HTTP_FORMAT } from './format.ts';
5
6
  export { startServer, stopServer } from './server.ts';
7
+ export { recordUser, recordAssistant, buildChain } from './chain-store.ts';
6
8
  export declare const serverHooks: PluginHooks;
@@ -1,10 +1,14 @@
1
1
  import { getPluginSettings } from '@coffer-org/server/plugin-runtime';
2
+ import { pruneThreadMessages } from '@coffer-org/server/thread-store';
2
3
  import { startServer, stopServer } from "./server.js";
3
4
  import { getLogger } from '@coffer-org/sdk/logger';
4
5
  const log = getLogger('http-connector');
6
+ const THREAD_TTL_MS = Number(process.env['HTTP_THREAD_TTL_MS'] ?? 7 * 86_400_000);
5
7
  export { loadHttpConfig, policy } from "./config.js";
6
8
  export { makeBufferedConnector } from "./connector.js";
9
+ export { HTTP_FORMAT } from "./format.js";
7
10
  export { startServer, stopServer } from "./server.js";
11
+ export { recordUser, recordAssistant, buildChain } from "./chain-store.js";
8
12
  export const serverHooks = {
9
13
  init: async () => {
10
14
  const { loadHttpConfig } = await import("./config.js");
@@ -15,4 +19,13 @@ export const serverHooks = {
15
19
  teardown: async () => {
16
20
  await stopServer();
17
21
  },
22
+ backgroundTasks: [
23
+ {
24
+ name: 'http-thread-prune',
25
+ intervalMs: 86_400_000,
26
+ run: async () => {
27
+ await pruneThreadMessages('http', Math.floor((Date.now() - THREAD_TTL_MS) / 1000));
28
+ },
29
+ },
30
+ ],
18
31
  };
@@ -1,7 +1,10 @@
1
1
  import { createServer } from 'node:http';
2
+ import { randomUUID } from 'node:crypto';
2
3
  import { handleIncoming } from '@coffer-org/plugin-orchestrator/runtime';
3
4
  import { policy } from "./config.js";
4
5
  import { makeBufferedConnector } from "./connector.js";
6
+ import { recordUser, buildChain } from "./chain-store.js";
7
+ import { HTTP_FORMAT } from "./format.js";
5
8
  import { getLogger } from '@coffer-org/sdk/logger';
6
9
  const log = getLogger('http-connector');
7
10
  const BIND_HOST = '127.0.0.1';
@@ -41,19 +44,32 @@ async function handleRequest(req, res, cfg, deps) {
41
44
  sendJson(res, 400, { error: 'Body must be a JSON object' });
42
45
  return;
43
46
  }
44
- const { chatId, userId, text } = body;
47
+ const { chatId, userId, text, replyTo } = body;
45
48
  if (typeof text !== 'string' || !text.trim()) {
46
49
  sendJson(res, 400, { error: 'Missing required field: text' });
47
50
  return;
48
51
  }
49
- const { connector, drain } = makeBufferedConnector();
52
+ if (replyTo !== undefined && typeof replyTo !== 'string') {
53
+ sendJson(res, 400, { error: 'Field replyTo must be a string' });
54
+ return;
55
+ }
56
+ const chat = typeof chatId === 'string' ? chatId : 'http';
57
+ const user = typeof userId === 'string' ? userId : 'http';
58
+ const msgId = randomUUID();
59
+ const botMsgId = randomUUID();
60
+ const nowSec = Math.floor(Date.now() / 1000);
61
+ await recordUser({ chatId: chat, msgId, sender: user, text, ts: nowSec, replyToId: replyTo ?? null });
62
+ const messages = await buildChain(msgId, { chatId: chat });
63
+ const { connector, drain, recorded } = makeBufferedConnector({ chatId: chat, botMsgId });
50
64
  await doHandle(connector, {
51
- chatId: typeof chatId === 'string' ? chatId : 'http',
52
- userId: typeof userId === 'string' ? userId : 'http',
53
- text,
54
- replyToId: null,
65
+ connectorId: 'http',
66
+ channelSystem: HTTP_FORMAT,
67
+ chatId: chat,
68
+ sender: { id: user },
69
+ messages,
55
70
  }, { policy: policy(cfg) });
56
- sendJson(res, 200, { reply: drain().join('\n\n') });
71
+ const reply = drain().join('\n\n');
72
+ sendJson(res, 200, { reply, id: recorded() });
57
73
  }
58
74
  export async function startServer(cfg, deps = {}) {
59
75
  if (server)
package/dist/schema.js CHANGED
@@ -154,7 +154,10 @@ function assignProp(target, prop, value) {
154
154
  }
155
155
  function mergeDefs(...defs) {
156
156
  const mergedDescriptors = {};
157
- for (const def of defs) Object.assign(mergedDescriptors, Object.getOwnPropertyDescriptors(def));
157
+ for (const def of defs) {
158
+ const descriptors = Object.getOwnPropertyDescriptors(def);
159
+ Object.assign(mergedDescriptors, descriptors);
160
+ }
158
161
  return Object.defineProperties({}, mergedDescriptors);
159
162
  }
160
163
  function esc(str) {
@@ -476,7 +479,7 @@ var _parse = (_Err) => (schema, value, _ctx, _params) => {
476
479
  }, ctx);
477
480
  if (result instanceof Promise) throw new $ZodAsyncError();
478
481
  if (result.issues.length) {
479
- const e = new (_params?.Err ?? _Err)(result.issues.map((iss) => finalizeIssue(iss, ctx, config())));
482
+ const e = new ((_params?.Err) ?? _Err)(result.issues.map((iss) => finalizeIssue(iss, ctx, config())));
480
483
  captureStackTrace(e, _params?.callee);
481
484
  throw e;
482
485
  }
@@ -493,7 +496,7 @@ var _parseAsync = (_Err) => async (schema, value, _ctx, params) => {
493
496
  }, ctx);
494
497
  if (result instanceof Promise) result = await result;
495
498
  if (result.issues.length) {
496
- const e = new (params?.Err ?? _Err)(result.issues.map((iss) => finalizeIssue(iss, ctx, config())));
499
+ const e = new ((params?.Err) ?? _Err)(result.issues.map((iss) => finalizeIssue(iss, ctx, config())));
497
500
  captureStackTrace(e, params?.callee);
498
501
  throw e;
499
502
  }
@@ -1589,13 +1592,13 @@ var $ZodObject = /*@__PURE__*/ $constructor("$ZodObject", (inst, def) => {
1589
1592
  }
1590
1593
  return propValues;
1591
1594
  });
1592
- const isObject$1 = isObject;
1595
+ const isObject$2 = isObject;
1593
1596
  const catchall = def.catchall;
1594
1597
  let value;
1595
1598
  inst._zod.parse = (payload, ctx) => {
1596
1599
  value ?? (value = _normalized.value);
1597
1600
  const input = payload.value;
1598
- if (!isObject$1(input)) {
1601
+ if (!isObject$2(input)) {
1599
1602
  payload.issues.push({
1600
1603
  expected: "object",
1601
1604
  code: "invalid_type",
@@ -1718,7 +1721,7 @@ var $ZodObjectJIT = /*@__PURE__*/ $constructor("$ZodObjectJIT", (inst, def) => {
1718
1721
  return (payload, ctx) => fn(shape, payload, ctx);
1719
1722
  };
1720
1723
  let fastpass;
1721
- const isObject$2 = isObject;
1724
+ const isObject$1 = isObject;
1722
1725
  const jit = !globalConfig.jitless;
1723
1726
  const fastEnabled = jit && allowsEval.value;
1724
1727
  const catchall = def.catchall;
@@ -1726,7 +1729,7 @@ var $ZodObjectJIT = /*@__PURE__*/ $constructor("$ZodObjectJIT", (inst, def) => {
1726
1729
  inst._zod.parse = (payload, ctx) => {
1727
1730
  value ?? (value = _normalized.value);
1728
1731
  const input = payload.value;
1729
- if (!isObject$2(input)) {
1732
+ if (!isObject$1(input)) {
1730
1733
  payload.issues.push({
1731
1734
  expected: "object",
1732
1735
  code: "invalid_type",
@@ -1945,7 +1948,7 @@ var $ZodOptional = /*@__PURE__*/ $constructor("$ZodOptional", (inst, def) => {
1945
1948
  inst._zod.optin = "optional";
1946
1949
  inst._zod.optout = "optional";
1947
1950
  defineLazy(inst._zod, "values", () => {
1948
- return def.innerType._zod.values ? new Set([...def.innerType._zod.values, void 0]) : void 0;
1951
+ return def.innerType._zod.values ? /* @__PURE__ */ new Set([...def.innerType._zod.values, void 0]) : void 0;
1949
1952
  });
1950
1953
  defineLazy(inst._zod, "pattern", () => {
1951
1954
  const pattern = def.innerType._zod.pattern;
@@ -1979,7 +1982,7 @@ var $ZodNullable = /*@__PURE__*/ $constructor("$ZodNullable", (inst, def) => {
1979
1982
  return pattern ? new RegExp(`^(${cleanRegex(pattern.source)}|null)$`) : void 0;
1980
1983
  });
1981
1984
  defineLazy(inst._zod, "values", () => {
1982
- return def.innerType._zod.values ? new Set([...def.innerType._zod.values, null]) : void 0;
1985
+ return def.innerType._zod.values ? /* @__PURE__ */ new Set([...def.innerType._zod.values, null]) : void 0;
1983
1986
  });
1984
1987
  inst._zod.parse = (payload, ctx) => {
1985
1988
  if (payload.value === null) return payload;
@@ -4587,7 +4590,7 @@ function password(raw) {
4587
4590
  * Internal API-token list — keyValue-shaped collection (name + write-only
4588
4591
  * token + timestamps), custom renderer (`kind:'internalApiToken'`, own
4589
4592
  * create/revoke UI — same trick as url()/perWeekday()). "Internal" = not a
4590
- * general-purpose field type for plugin modules, only for the core account
4593
+ * general-purpose field type for plugin shelves, only for the core account
4591
4594
  * settings page. `token` is `kind:'password'` so maskSecrets/preserveTree
4592
4595
  * already mask/preserve it for free.
4593
4596
  */
@@ -4606,6 +4609,25 @@ function internalApiToken(o) {
4606
4609
  ]
4607
4610
  });
4608
4611
  }
4612
+ /**
4613
+ * Internal "connected apps" list — the OAuth clients (claude.ai & co) the user
4614
+ * has authorized for MCP access. Same "internal" caveat and custom-renderer
4615
+ * trick as internalApiToken(): read-only rows + a revoke action, never part of
4616
+ * the parent form's save.
4617
+ */
4618
+ function internalOauthGrants(o) {
4619
+ return group({
4620
+ key: o.key,
4621
+ label: o.label ?? o.key,
4622
+ multiple: true,
4623
+ view: { kind: "internalOauthGrants" },
4624
+ fields: [
4625
+ string({ key: "clientName" }),
4626
+ string({ key: "createdAt" }),
4627
+ string({ key: "lastUsedAt" })
4628
+ ]
4629
+ });
4630
+ }
4609
4631
  var SLUG_RE = /^[a-z0-9-]+$/;
4610
4632
  function slug(raw) {
4611
4633
  const o = normalizeOpts(raw);
@@ -4686,7 +4708,7 @@ var TEL_RE = /^\+?[\d\s()-]{4,}$/;
4686
4708
  /** Loose URL: будь-який scheme:// АБО host-з-крапкою (+опц. порт/шлях). Без пробілів. */
4687
4709
  var LINK_RE = /^([a-z][a-z0-9+.-]*:\/\/\S+|[\w-]+(\.[\w-]+)+(:\d+)?(\/\S*)?)$/i;
4688
4710
  /** CSS named colors (CSS Color Module L4) — для f.colorname. */
4689
- var CSS_COLOR_NAMES = new Set([
4711
+ var CSS_COLOR_NAMES = /* @__PURE__ */ new Set([
4690
4712
  "aliceblue",
4691
4713
  "antiquewhite",
4692
4714
  "aqua",
@@ -5035,7 +5057,8 @@ var presets = {
5035
5057
  weight,
5036
5058
  dimensions,
5037
5059
  country,
5038
- internalApiToken
5060
+ internalApiToken,
5061
+ internalOauthGrants
5039
5062
  };
5040
5063
  //#endregion
5041
5064
  //#region ../../node_modules/iso-639-1/src/data.js
@@ -5147,7 +5170,7 @@ var require_data = /* @__PURE__ */ __commonJSMin(((exports, module) => {
5147
5170
  },
5148
5171
  cs: {
5149
5172
  name: "Czech",
5150
- nativeName: "Čeština"
5173
+ nativeName: "čeština"
5151
5174
  },
5152
5175
  cu: {
5153
5176
  name: "Old Church Slavonic",
@@ -5611,7 +5634,7 @@ var require_data = /* @__PURE__ */ __commonJSMin(((exports, module) => {
5611
5634
  },
5612
5635
  sk: {
5613
5636
  name: "Slovak",
5614
- nativeName: "Slovenčina"
5637
+ nativeName: "slovenčtina"
5615
5638
  },
5616
5639
  sl: {
5617
5640
  name: "Slovenian",
@@ -5862,7 +5885,7 @@ function group(o) {
5862
5885
  required: o.required,
5863
5886
  unique: r.unique,
5864
5887
  label: o.label,
5865
- icon: v.icon,
5888
+ icon: o.icon,
5866
5889
  display: v.display ?? "wrap",
5867
5890
  kind: v.kind,
5868
5891
  fixed: r.fixed,
@@ -5873,11 +5896,9 @@ function group(o) {
5873
5896
  function row(o) {
5874
5897
  return group({
5875
5898
  label: o.label,
5899
+ icon: o.icon,
5876
5900
  fields: o.fields,
5877
- view: {
5878
- ...o.view,
5879
- display: "scroll"
5880
- }
5901
+ view: { display: "scroll" }
5881
5902
  });
5882
5903
  }
5883
5904
  /** Tabular collection (array of rows → <table>, aligned by columns). Sugar. */
@@ -5885,14 +5906,12 @@ function table(o) {
5885
5906
  return group({
5886
5907
  key: o.key,
5887
5908
  label: o.label,
5909
+ icon: o.icon,
5888
5910
  fields: o.fields,
5889
5911
  multiple: true,
5890
5912
  required: o.required,
5891
5913
  rules: o.rules,
5892
- view: {
5893
- ...o.view,
5894
- display: "table"
5895
- }
5914
+ view: { display: "table" }
5896
5915
  });
5897
5916
  }
5898
5917
  /**
@@ -5908,11 +5927,9 @@ function sheet(o) {
5908
5927
  });
5909
5928
  return group({
5910
5929
  label: o.label,
5930
+ icon: o.icon,
5911
5931
  fields: flat,
5912
- view: {
5913
- ...o.view,
5914
- display: "sheet"
5915
- }
5932
+ view: { display: "sheet" }
5916
5933
  });
5917
5934
  }
5918
5935
  /**
@@ -5944,8 +5961,8 @@ function keyed(o) {
5944
5961
  return {
5945
5962
  ...(o.container ?? group)({
5946
5963
  label: o.label,
5947
- fields: [o.by, ...o.fields],
5948
- view: o.view
5964
+ icon: o.icon,
5965
+ fields: [o.by, ...o.fields]
5949
5966
  }),
5950
5967
  key: o.key,
5951
5968
  multiple: true,
@@ -5989,6 +6006,34 @@ function wrapKey(opts, meta) {
5989
6006
  noEditControl: true
5990
6007
  }
5991
6008
  };
6009
+ if (opts.emphasis) m = {
6010
+ ...m,
6011
+ hints: {
6012
+ ...m.hints,
6013
+ emphasis: opts.emphasis
6014
+ }
6015
+ };
6016
+ if (opts.noLabel) m = {
6017
+ ...m,
6018
+ hints: {
6019
+ ...m.hints,
6020
+ noLabel: true
6021
+ }
6022
+ };
6023
+ if (opts.role) m = {
6024
+ ...m,
6025
+ hints: {
6026
+ ...m.hints,
6027
+ role: opts.role
6028
+ }
6029
+ };
6030
+ if (opts.pinned) m = {
6031
+ ...m,
6032
+ hints: {
6033
+ ...m.hints,
6034
+ pinned: true
6035
+ }
6036
+ };
5992
6037
  if (opts.default !== void 0) m = {
5993
6038
  ...m,
5994
6039
  default: opts.default
@@ -6352,8 +6397,8 @@ function relation(raw) {
6352
6397
  displayKey: o.displayKey ?? "name"
6353
6398
  },
6354
6399
  relation: {
6355
- vault: raw.options.vault,
6356
- type: raw.options.module
6400
+ library: raw.options.library,
6401
+ shelf: raw.options.shelf
6357
6402
  },
6358
6403
  ...multi && { json: true },
6359
6404
  zod: optionalize(s, required)
@@ -6733,15 +6778,44 @@ function period(raw) {
6733
6778
  zod: optionalize(s, required)
6734
6779
  });
6735
6780
  }
6781
+ /** Keys a file entry may carry. Everything else is rejected — see fileEntryIssue. */
6782
+ var FILE_KEYS = /* @__PURE__ */ new Set([
6783
+ "name",
6784
+ "mime",
6785
+ "size"
6786
+ ]);
6787
+ /** Keys that mean "the author pasted a remote address" — the one mistake worth naming. */
6788
+ var FILE_URL_KEYS = [
6789
+ "url",
6790
+ "src",
6791
+ "href",
6792
+ "link"
6793
+ ];
6794
+ /**
6795
+ * A file entry points at a file already uploaded to the server: `{ name }`, where
6796
+ * name is the bare filename returned by POST /api/upload. Anything else — a remote
6797
+ * URL, an extra key, a path — is refused here, so a record can never hold a
6798
+ * reference the server cannot serve. `mime`/`size` are accepted (legacy payloads
6799
+ * and the web uploader send them) but the server overwrites them from disk.
6800
+ * Returns a vmsg code, or null when the entry is well-formed.
6801
+ */
6802
+ function fileEntryIssue(it) {
6803
+ if (typeof it !== "object" || it === null || Array.isArray(it)) return "file_structure";
6804
+ const rec = it;
6805
+ if (FILE_URL_KEYS.some((k) => k in rec)) return "file_remote_url";
6806
+ const name = rec["name"];
6807
+ if (typeof name !== "string" || name === "") return "file_structure";
6808
+ if (name.includes("://") || name.startsWith("//")) return "file_remote_url";
6809
+ if (/[\\/]/.test(name) || name.includes("..")) return "file_name";
6810
+ for (const k of Object.keys(rec)) if (!FILE_KEYS.has(k)) return "file_unknown_key";
6811
+ if (rec["mime"] !== void 0 && typeof rec["mime"] !== "string") return "file_structure";
6812
+ if (rec["size"] !== void 0 && typeof rec["size"] !== "number") return "file_structure";
6813
+ return null;
6814
+ }
6736
6815
  function makeFile(kind, raw) {
6737
6816
  const o = normalizeOpts(raw);
6738
6817
  const required = o.required ?? false;
6739
6818
  const multiple = o.multiple ?? false;
6740
- const rowSchema = object({
6741
- name: string$1().min(1),
6742
- mime: string$1().optional(),
6743
- size: number$1().optional()
6744
- });
6745
6819
  const s = unknown().superRefine((raw, ctx) => {
6746
6820
  if (typeof raw === "string") try {
6747
6821
  JSON.parse(raw);
@@ -6754,12 +6828,15 @@ function makeFile(kind, raw) {
6754
6828
  }
6755
6829
  const parsed = jsonValue(raw);
6756
6830
  const items = Array.isArray(parsed) ? parsed : [parsed];
6757
- for (const it of items) if (!rowSchema.safeParse(it).success) {
6758
- ctx.addIssue({
6759
- code: ZodIssueCode.custom,
6760
- message: vmsg("file_structure")
6761
- });
6762
- return;
6831
+ for (const it of items) {
6832
+ const code = fileEntryIssue(it);
6833
+ if (code) {
6834
+ ctx.addIssue({
6835
+ code: ZodIssueCode.custom,
6836
+ message: vmsg(code)
6837
+ });
6838
+ return;
6839
+ }
6763
6840
  }
6764
6841
  });
6765
6842
  return wrapKey(o, {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@coffer-org/plugin-http",
3
- "version": "1.3.1",
3
+ "version": "2.0.0",
4
4
  "type": "module",
5
5
  "engines": {
6
6
  "node": ">=24"
@@ -25,9 +25,9 @@
25
25
  "test": "node --import tsx --test \"src/runtime/*.test.ts\""
26
26
  },
27
27
  "dependencies": {
28
- "@coffer-org/plugin-orchestrator": "^1.3.1",
29
- "@coffer-org/sdk": "^1.4.0",
30
- "@coffer-org/server": "^1.4.0"
28
+ "@coffer-org/plugin-orchestrator": "^2.0.0",
29
+ "@coffer-org/sdk": "^2.0.0",
30
+ "@coffer-org/server": "^2.0.1"
31
31
  },
32
32
  "devDependencies": {},
33
33
  "coffer": {