@coffer-org/server 7.2.0 → 7.4.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/dist/auth-api.d.ts +4 -0
- package/dist/auth-api.js +52 -0
- package/dist/auth-store.d.ts +2 -0
- package/dist/auth-store.js +1 -0
- package/dist/compute-unit.js +0 -3
- package/dist/conversation-store.d.ts +61 -0
- package/dist/conversation-store.js +223 -0
- package/dist/entity-schema.d.ts +5 -2
- package/dist/entity-schema.js +35 -18
- package/dist/identity-link.d.ts +15 -0
- package/dist/identity-link.js +76 -0
- package/dist/identity-providers.d.ts +17 -0
- package/dist/identity-providers.js +14 -0
- package/dist/index.js +8 -1
- package/dist/mcp-contract/schema.d.ts +0 -1
- package/dist/mcp-contract/schema.js +0 -2
- package/dist/mcp-http.js +7 -3
- package/dist/mcp-tools.d.ts +4 -3
- package/dist/mcp-tools.js +84 -84
- package/dist/media/image.d.ts +23 -0
- package/dist/media/image.js +103 -0
- package/dist/media/index.d.ts +1 -0
- package/dist/media/index.js +1 -0
- package/dist/migrations.js +1 -1
- package/dist/mutate.js +0 -15
- package/dist/orchestrator/agent-capabilities.d.ts +2 -2
- package/dist/orchestrator/agent-capabilities.js +3 -3
- package/dist/orchestrator/allow.d.ts +1 -16
- package/dist/orchestrator/allow.js +3 -53
- package/dist/orchestrator/config.js +0 -1
- package/dist/orchestrator/context-facts.d.ts +27 -0
- package/dist/orchestrator/context-facts.js +89 -0
- package/dist/orchestrator/conversation-access.d.ts +9 -0
- package/dist/orchestrator/conversation-access.js +12 -0
- package/dist/orchestrator/draft-message.d.ts +18 -0
- package/dist/orchestrator/draft-message.js +85 -0
- package/dist/orchestrator/environment.d.ts +1 -0
- package/dist/orchestrator/environment.js +10 -0
- package/dist/orchestrator/file-inspection.d.ts +2 -2
- package/dist/orchestrator/file-inspection.js +41 -19
- package/dist/orchestrator/index.d.ts +14 -9
- package/dist/orchestrator/index.js +6 -7
- package/dist/orchestrator/live-message.d.ts +7 -4
- package/dist/orchestrator/live-message.js +52 -32
- package/dist/orchestrator/pipeline.d.ts +22 -4
- package/dist/orchestrator/pipeline.js +316 -115
- package/dist/orchestrator/registry.d.ts +2 -2
- package/dist/orchestrator/registry.js +1 -1
- package/dist/orchestrator/system-areas.d.ts +12 -0
- package/dist/orchestrator/system-areas.js +63 -0
- package/dist/orchestrator/system-capabilities.js +1 -1
- package/dist/orchestrator/turn-context.d.ts +18 -0
- package/dist/orchestrator/turn-context.js +39 -0
- package/dist/orchestrator/types.d.ts +100 -49
- package/dist/plugin-hooks.d.ts +38 -1
- package/dist/plugin-hooks.js +4 -0
- package/dist/plugin-http-mounts.d.ts +18 -0
- package/dist/plugin-http-mounts.js +94 -0
- package/dist/plugin-runtime.d.ts +4 -0
- package/dist/plugin-runtime.js +9 -5
- package/dist/plugins-api.d.ts +8 -2
- package/dist/plugins-api.js +4 -2
- package/dist/records-api.js +15 -3
- package/dist/settings-write.d.ts +1 -2
- package/dist/settings-write.js +1 -2
- package/dist/system-settings.js +0 -1
- package/dist/temporal.js +8 -3
- package/dist/thread-state.d.ts +14 -0
- package/dist/thread-state.js +71 -11
- package/dist/thread-store.d.ts +5 -3
- package/dist/thread-store.js +12 -9
- package/dist/turn-gate.d.ts +8 -0
- package/dist/turn-gate.js +39 -0
- package/package.json +7 -2
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { currentMoment } from "./environment.js";
|
|
2
|
+
export const PAUSE_THRESHOLD_MS = 10 * 60_000;
|
|
3
|
+
export const MAX_SPEAKER_NAME = 80;
|
|
4
|
+
const FORGERY_CHARS = /[\p{Cc}\p{Zl}\p{Zp}\p{Bidi_Control}]+/gu;
|
|
5
|
+
export function oneLine(value, maxLen) {
|
|
6
|
+
const flat = value.replace(FORGERY_CHARS, ' ').replace(/\s+/g, ' ').trim();
|
|
7
|
+
return maxLen !== undefined && flat.length > maxLen ? `${flat.slice(0, maxLen - 1).trimEnd()}…` : flat;
|
|
8
|
+
}
|
|
9
|
+
export function humanizePause(ms) {
|
|
10
|
+
const minutes = Math.round(ms / 60_000);
|
|
11
|
+
if (minutes < 90)
|
|
12
|
+
return `about ${minutes} minutes`;
|
|
13
|
+
const hours = Math.round(ms / 3_600_000);
|
|
14
|
+
if (hours < 48)
|
|
15
|
+
return `about ${hours} hours`;
|
|
16
|
+
return `about ${Math.round(ms / 86_400_000)} days`;
|
|
17
|
+
}
|
|
18
|
+
export function buildTurnContext(input) {
|
|
19
|
+
const name = oneLine(input.speaker.name, MAX_SPEAKER_NAME) || 'unknown';
|
|
20
|
+
const previousName = input.previousSpeakerName
|
|
21
|
+
? oneLine(input.previousSpeakerName, MAX_SPEAKER_NAME) || 'unknown'
|
|
22
|
+
: null;
|
|
23
|
+
const who = `${name} (${input.speaker.role})`;
|
|
24
|
+
const lines = [
|
|
25
|
+
previousName ? `Speaking now: ${who} — the previous message was from ${previousName}.` : `Speaking now: ${who}.`,
|
|
26
|
+
`Current time: ${currentMoment(input.now, input.timeZone)} (${input.timeZone}).`,
|
|
27
|
+
];
|
|
28
|
+
if (input.previousMessageTs !== null) {
|
|
29
|
+
const gap = input.now.getTime() - input.previousMessageTs * 1000;
|
|
30
|
+
if (gap >= PAUSE_THRESHOLD_MS)
|
|
31
|
+
lines.push(`Time since the previous message: ${humanizePause(gap)}.`);
|
|
32
|
+
}
|
|
33
|
+
for (const raw of input.connectorLines) {
|
|
34
|
+
const line = oneLine(raw);
|
|
35
|
+
if (line)
|
|
36
|
+
lines.push(line);
|
|
37
|
+
}
|
|
38
|
+
return lines.join('\n');
|
|
39
|
+
}
|
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
import type { ImageTarget } from '../media/index.ts';
|
|
2
|
+
import type { AuthRole } from '../plugin-hooks.ts';
|
|
3
|
+
import type { ContextFact } from './context-facts.ts';
|
|
4
|
+
import type { SystemAreas } from './system-areas.ts';
|
|
5
|
+
import type { StoredAttachment } from '../conversation-store.ts';
|
|
6
|
+
export type { ImageTarget };
|
|
7
|
+
export type { ContextFact };
|
|
8
|
+
export type { SystemAreas };
|
|
9
|
+
export type { StoredAttachment };
|
|
1
10
|
export interface AttachmentRef {
|
|
2
11
|
name: string;
|
|
3
12
|
mime?: string;
|
|
@@ -9,7 +18,6 @@ export interface AgentToolDefinition {
|
|
|
9
18
|
description: string;
|
|
10
19
|
inputSchema: Record<string, unknown>;
|
|
11
20
|
handler: (args: Record<string, unknown>, signal?: AbortSignal) => Promise<unknown>;
|
|
12
|
-
forcedAfterAnswer?: boolean;
|
|
13
21
|
}
|
|
14
22
|
export interface AgentToolProvider {
|
|
15
23
|
tools: AgentToolDefinition[];
|
|
@@ -34,18 +42,25 @@ export interface AgentToolContentResult {
|
|
|
34
42
|
export interface ConvMessage {
|
|
35
43
|
role: 'user' | 'assistant';
|
|
36
44
|
content: string;
|
|
37
|
-
context?:
|
|
45
|
+
context?: ContextFact[];
|
|
38
46
|
attachments?: AttachmentRef[];
|
|
39
47
|
sender?: string | null;
|
|
40
48
|
msgId: string;
|
|
41
49
|
ts: number;
|
|
42
50
|
}
|
|
43
51
|
export interface AgentCapabilities {
|
|
44
|
-
vision?: boolean;
|
|
45
|
-
documents?: boolean;
|
|
46
52
|
tools?: boolean;
|
|
47
53
|
reasoning?: boolean;
|
|
48
54
|
}
|
|
55
|
+
export interface AgentMediaKind {
|
|
56
|
+
maxBytes: number;
|
|
57
|
+
accepts: readonly string[];
|
|
58
|
+
}
|
|
59
|
+
export interface AgentMediaLimits {
|
|
60
|
+
image: AgentMediaKind & ImageTarget;
|
|
61
|
+
document: AgentMediaKind;
|
|
62
|
+
text: AgentMediaKind;
|
|
63
|
+
}
|
|
49
64
|
export interface AgentPreset {
|
|
50
65
|
id: string;
|
|
51
66
|
title: string;
|
|
@@ -60,35 +75,42 @@ export interface AgentDescriptor {
|
|
|
60
75
|
title: string;
|
|
61
76
|
presets: AgentPreset[];
|
|
62
77
|
}
|
|
78
|
+
export interface AgentCatalogEntry extends AgentDescriptor {
|
|
79
|
+
media: AgentMediaLimits;
|
|
80
|
+
}
|
|
63
81
|
export interface AgentBudget {
|
|
64
82
|
responseTimeout?: number;
|
|
65
83
|
toolRounds?: number;
|
|
66
84
|
}
|
|
67
|
-
export interface
|
|
68
|
-
|
|
69
|
-
|
|
85
|
+
export interface AgentTurn {
|
|
86
|
+
envelope: TurnEnvelope;
|
|
87
|
+
body: {
|
|
88
|
+
system: string[];
|
|
89
|
+
messages: ConvMessage[];
|
|
90
|
+
userMessageCount: number;
|
|
91
|
+
};
|
|
70
92
|
toolProvider?: AgentToolProvider;
|
|
71
93
|
presetId?: string;
|
|
72
94
|
budget?: AgentBudget;
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
onSegment?: () => void;
|
|
76
|
-
}
|
|
77
|
-
export interface AgentResult {
|
|
78
|
-
text: string | null;
|
|
79
|
-
reasoning: string | null;
|
|
80
|
-
tokensIn: number | null;
|
|
81
|
-
tokensOut: number | null;
|
|
82
|
-
stopReason: string | null;
|
|
83
|
-
presetId: string | null;
|
|
84
|
-
suggestions?: string[] | null;
|
|
95
|
+
senderRole?: AuthRole;
|
|
96
|
+
signal?: AbortSignal;
|
|
85
97
|
}
|
|
86
98
|
export interface AgentRuntime {
|
|
87
99
|
id: string;
|
|
88
|
-
|
|
100
|
+
media: AgentMediaLimits;
|
|
101
|
+
run(turn: AgentTurn, emit: (e: TurnEvent) => void): Promise<void>;
|
|
89
102
|
systemBase(): Promise<string>;
|
|
90
103
|
describe(): Promise<AgentDescriptor>;
|
|
91
104
|
starters?(hint: string): Promise<string[]>;
|
|
105
|
+
afterword?(turn: AgentTurn, answer: string, want: {
|
|
106
|
+
suggestions: boolean;
|
|
107
|
+
title: boolean;
|
|
108
|
+
}): Promise<{
|
|
109
|
+
suggestions?: string[];
|
|
110
|
+
title?: string;
|
|
111
|
+
tokensIn?: number;
|
|
112
|
+
tokensOut?: number;
|
|
113
|
+
}>;
|
|
92
114
|
}
|
|
93
115
|
export interface ConnectorRegistration {
|
|
94
116
|
id: string;
|
|
@@ -99,48 +121,77 @@ export interface AttachmentMaterializer {
|
|
|
99
121
|
mime?: string;
|
|
100
122
|
}): Promise<AttachmentRef>;
|
|
101
123
|
}
|
|
102
|
-
export interface
|
|
124
|
+
export interface TurnEnvelope {
|
|
103
125
|
connectorId: string;
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
126
|
+
conversationId: string;
|
|
127
|
+
turnId: string;
|
|
128
|
+
}
|
|
129
|
+
export interface ConnectorCapabilities {
|
|
130
|
+
events: readonly ('delta' | 'reasoning' | 'segment' | 'suggestions' | 'title')[];
|
|
131
|
+
privateChats: boolean;
|
|
132
|
+
}
|
|
133
|
+
export interface TurnBody {
|
|
134
|
+
systemPrompt: SystemAreas;
|
|
135
|
+
capabilities: ConnectorCapabilities;
|
|
136
|
+
}
|
|
137
|
+
export interface TurnRequest {
|
|
138
|
+
envelope: TurnEnvelope;
|
|
109
139
|
sender: {
|
|
110
|
-
|
|
140
|
+
userId: number;
|
|
111
141
|
displayName?: string;
|
|
112
142
|
};
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
143
|
+
message?: {
|
|
144
|
+
text: string;
|
|
145
|
+
attachments?: StoredAttachment[];
|
|
146
|
+
replyToMsgId?: string | null;
|
|
147
|
+
};
|
|
148
|
+
capabilities?: ConnectorCapabilities;
|
|
149
|
+
systemPrompt?: SystemAreas;
|
|
150
|
+
turnContext?: ContextFact[];
|
|
151
|
+
agentId?: string;
|
|
152
|
+
presetId?: string;
|
|
153
|
+
signal?: AbortSignal;
|
|
154
|
+
prepareAttachments?(m: AttachmentMaterializer): Promise<StoredAttachment[] | ConvMessage[]>;
|
|
119
155
|
}
|
|
120
|
-
export
|
|
156
|
+
export type TurnEvent = {
|
|
157
|
+
kind: 'delta';
|
|
158
|
+
text: string;
|
|
159
|
+
} | {
|
|
160
|
+
kind: 'reasoning';
|
|
161
|
+
text: string;
|
|
162
|
+
} | {
|
|
163
|
+
kind: 'segment';
|
|
164
|
+
} | {
|
|
165
|
+
kind: 'answer';
|
|
121
166
|
text: string | null;
|
|
122
167
|
reasoning: string | null;
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
168
|
+
} | {
|
|
169
|
+
kind: 'usage';
|
|
170
|
+
tokensIn: number | null;
|
|
171
|
+
tokensOut: number | null;
|
|
172
|
+
presetId: string | null;
|
|
173
|
+
stopReason: string | null;
|
|
174
|
+
} | {
|
|
175
|
+
kind: 'error';
|
|
176
|
+
message: string;
|
|
177
|
+
} | {
|
|
178
|
+
kind: 'suggestions';
|
|
179
|
+
items: string[];
|
|
180
|
+
} | {
|
|
181
|
+
kind: 'title';
|
|
182
|
+
text: string;
|
|
183
|
+
};
|
|
184
|
+
export interface TurnSink {
|
|
185
|
+
emit(event: TurnEvent): void;
|
|
186
|
+
done(): Promise<void>;
|
|
130
187
|
}
|
|
131
188
|
export interface Connector {
|
|
132
189
|
id: string;
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
parentMsgId: string | null;
|
|
136
|
-
botMsgId: string | null;
|
|
137
|
-
text: string;
|
|
138
|
-
reasoning: string | null;
|
|
139
|
-
}): Promise<void>;
|
|
190
|
+
open(envelope: TurnEnvelope, msgId: string): TurnSink;
|
|
191
|
+
bindMessage?(envelope: TurnEnvelope, msgId: string, externalId: string): Promise<void>;
|
|
140
192
|
}
|
|
141
193
|
export interface GatePolicy {
|
|
142
194
|
agentId?: string;
|
|
143
|
-
accessPassword: string;
|
|
144
195
|
triggerPrefix: string;
|
|
145
196
|
replyWindow: number;
|
|
146
197
|
}
|
package/dist/plugin-hooks.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { EntityManager } from '@mikro-orm/core';
|
|
2
2
|
import type { z } from 'zod';
|
|
3
|
-
import type { ColumnType, ColumnConversion } from '@coffer-org/sdk/fields';
|
|
3
|
+
import type { ColumnType, ColumnConversion, LayoutEl } from '@coffer-org/sdk/fields';
|
|
4
|
+
import type { LayoutInput } from '@coffer-org/sdk/materialize/decl';
|
|
4
5
|
import { type Logger } from '@coffer-org/sdk/logger';
|
|
5
6
|
export type { BackgroundTask } from './background-scheduler.ts';
|
|
6
7
|
export interface TableOps {
|
|
@@ -60,6 +61,15 @@ export type PluginStreamAction = (body: Record<string, unknown>, ctx: {
|
|
|
60
61
|
emit: (event: string, data: unknown) => void;
|
|
61
62
|
signal: AbortSignal;
|
|
62
63
|
}) => Promise<void>;
|
|
64
|
+
export interface PrivateTableDef {
|
|
65
|
+
name: string;
|
|
66
|
+
fields: LayoutEl[];
|
|
67
|
+
unique?: string[][];
|
|
68
|
+
}
|
|
69
|
+
export type PrivateTableDefInput = Omit<PrivateTableDef, 'fields'> & {
|
|
70
|
+
fields: LayoutInput;
|
|
71
|
+
};
|
|
72
|
+
export declare function definePrivateTable(t: PrivateTableDefInput): PrivateTableDef;
|
|
63
73
|
export interface PluginHooks {
|
|
64
74
|
migrations?: Migration[];
|
|
65
75
|
seed?: Seed[];
|
|
@@ -70,5 +80,32 @@ export interface PluginHooks {
|
|
|
70
80
|
actions?: Record<string, PluginAction>;
|
|
71
81
|
userActions?: Record<string, PluginUserAction>;
|
|
72
82
|
streamActions?: Record<string, PluginStreamAction>;
|
|
83
|
+
http?: PluginHttpMount[];
|
|
84
|
+
tables?: PrivateTableDef[];
|
|
73
85
|
}
|
|
74
86
|
export declare const pluginHooks: Record<string, PluginHooks>;
|
|
87
|
+
export interface PluginHttpRequest {
|
|
88
|
+
method: string;
|
|
89
|
+
path: string;
|
|
90
|
+
url: string;
|
|
91
|
+
headers: Record<string, string>;
|
|
92
|
+
body: string;
|
|
93
|
+
user: {
|
|
94
|
+
id: number;
|
|
95
|
+
login: string;
|
|
96
|
+
role: AuthRole;
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
export interface PluginHttpResponse {
|
|
100
|
+
status: number;
|
|
101
|
+
headers?: Record<string, string>;
|
|
102
|
+
body?: string;
|
|
103
|
+
}
|
|
104
|
+
export interface PluginHttpMount {
|
|
105
|
+
prefix: string;
|
|
106
|
+
methods: string[];
|
|
107
|
+
auth: 'member' | 'admin' | 'token';
|
|
108
|
+
body?: 'text' | 'none';
|
|
109
|
+
wellKnown?: string;
|
|
110
|
+
handle(req: PluginHttpRequest): Promise<PluginHttpResponse>;
|
|
111
|
+
}
|
package/dist/plugin-hooks.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { materializeDef } from '@coffer-org/sdk/materialize/pipeline';
|
|
1
2
|
import { getLogger } from '@coffer-org/sdk/logger';
|
|
2
3
|
export function pluginCtx(id, em, signal) {
|
|
3
4
|
return { em, log: getLogger(id), ...(signal ? { signal } : {}) };
|
|
@@ -10,4 +11,7 @@ export class HttpError extends Error {
|
|
|
10
11
|
this.name = 'HttpError';
|
|
11
12
|
}
|
|
12
13
|
}
|
|
14
|
+
export function definePrivateTable(t) {
|
|
15
|
+
return materializeDef(t);
|
|
16
|
+
}
|
|
13
17
|
export const pluginHooks = {};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { FastifyInstance, FastifyRequest } from 'fastify';
|
|
2
|
+
import type { AuthRole, PluginHooks, PluginHttpMount } from './plugin-hooks.ts';
|
|
3
|
+
export interface MountEntry {
|
|
4
|
+
pluginId: string;
|
|
5
|
+
mount: PluginHttpMount;
|
|
6
|
+
}
|
|
7
|
+
export declare function collectHttpMounts(hooks: Record<string, PluginHooks>): MountEntry[];
|
|
8
|
+
export declare function customMethodsOf(entries: MountEntry[]): string[];
|
|
9
|
+
export declare function registerTextBodyParsers(app: FastifyInstance): void;
|
|
10
|
+
export interface MountDeps {
|
|
11
|
+
disabledSet: () => Promise<Set<string>>;
|
|
12
|
+
resolveUser: (req: FastifyRequest) => Promise<{
|
|
13
|
+
id: number;
|
|
14
|
+
login: string;
|
|
15
|
+
role: AuthRole;
|
|
16
|
+
} | null>;
|
|
17
|
+
}
|
|
18
|
+
export declare function registerPluginHttpMounts(app: FastifyInstance, entries: MountEntry[], deps: MountDeps): void;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
const RESERVED = ['/api', '/mcp', '/health', '/uploads', '/.well-known', '/assets'];
|
|
2
|
+
const DEFAULT_METHODS = new Set(['GET', 'HEAD', 'POST', 'PUT', 'DELETE', 'OPTIONS', 'PATCH']);
|
|
3
|
+
export function collectHttpMounts(hooks) {
|
|
4
|
+
const out = [];
|
|
5
|
+
const owner = new Map();
|
|
6
|
+
for (const [pluginId, h] of Object.entries(hooks)) {
|
|
7
|
+
for (const mount of h.http ?? []) {
|
|
8
|
+
const prefix = mount.prefix.replace(/\/+$/, '');
|
|
9
|
+
if (!prefix.startsWith('/') || prefix === '') {
|
|
10
|
+
throw new Error(`[plugin-http] ${pluginId}: prefix must be absolute, got '${mount.prefix}'`);
|
|
11
|
+
}
|
|
12
|
+
if (RESERVED.some((r) => prefix === r || prefix.startsWith(r + '/'))) {
|
|
13
|
+
throw new Error(`[plugin-http] ${pluginId}: prefix '${prefix}' is reserved for the core`);
|
|
14
|
+
}
|
|
15
|
+
const other = owner.get(prefix);
|
|
16
|
+
if (other)
|
|
17
|
+
throw new Error(`[plugin-http] prefix '${prefix}' is declared by both ${other} and ${pluginId}`);
|
|
18
|
+
owner.set(prefix, pluginId);
|
|
19
|
+
out.push({ pluginId, mount: { ...mount, prefix, methods: mount.methods.map((m) => m.toUpperCase()) } });
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return out;
|
|
23
|
+
}
|
|
24
|
+
export function customMethodsOf(entries) {
|
|
25
|
+
const seen = new Set();
|
|
26
|
+
for (const e of entries)
|
|
27
|
+
for (const m of e.mount.methods)
|
|
28
|
+
if (!DEFAULT_METHODS.has(m))
|
|
29
|
+
seen.add(m);
|
|
30
|
+
return [...seen];
|
|
31
|
+
}
|
|
32
|
+
export function registerTextBodyParsers(app) {
|
|
33
|
+
const asText = (_req, body, done) => done(null, body);
|
|
34
|
+
for (const type of ['text/calendar', 'text/xml', 'application/xml', 'text/plain', 'application/octet-stream']) {
|
|
35
|
+
if (!app.hasContentTypeParser(type))
|
|
36
|
+
app.addContentTypeParser(type, { parseAs: 'string' }, asText);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
function flatHeaders(raw) {
|
|
40
|
+
const out = {};
|
|
41
|
+
for (const [k, v] of Object.entries(raw)) {
|
|
42
|
+
if (v === undefined)
|
|
43
|
+
continue;
|
|
44
|
+
out[k.toLowerCase()] = Array.isArray(v) ? v.join(', ') : String(v);
|
|
45
|
+
}
|
|
46
|
+
return out;
|
|
47
|
+
}
|
|
48
|
+
export function registerPluginHttpMounts(app, entries, deps) {
|
|
49
|
+
for (const method of customMethodsOf(entries))
|
|
50
|
+
app.addHttpMethod(method, { hasBody: true });
|
|
51
|
+
for (const { pluginId, mount } of entries) {
|
|
52
|
+
const allow = mount.methods.join(', ');
|
|
53
|
+
const handler = async (req, reply) => {
|
|
54
|
+
if ((await deps.disabledSet()).has(pluginId))
|
|
55
|
+
return reply.code(404).send({ error: 'not_found' });
|
|
56
|
+
if (!mount.methods.includes(req.method.toUpperCase())) {
|
|
57
|
+
return reply.code(405).header('allow', allow).send();
|
|
58
|
+
}
|
|
59
|
+
const user = req.user ?? (await deps.resolveUser(req));
|
|
60
|
+
if (!user) {
|
|
61
|
+
if (mount.auth === 'token')
|
|
62
|
+
reply.header('www-authenticate', 'Basic realm="Coffer"');
|
|
63
|
+
return reply.code(401).send({ error: 'unauthorized' });
|
|
64
|
+
}
|
|
65
|
+
if (mount.auth === 'admin' && user.role !== 'admin')
|
|
66
|
+
return reply.code(403).send({ error: 'forbidden' });
|
|
67
|
+
const full = req.url.split('?')[0];
|
|
68
|
+
const below = full.slice(mount.prefix.length);
|
|
69
|
+
const body = mount.body === 'none' ? '' : typeof req.body === 'string' ? req.body : req.body ? JSON.stringify(req.body) : '';
|
|
70
|
+
const res = await mount.handle({
|
|
71
|
+
method: req.method.toUpperCase(),
|
|
72
|
+
path: below === '' ? '/' : below,
|
|
73
|
+
url: req.url,
|
|
74
|
+
headers: flatHeaders(req.headers),
|
|
75
|
+
body,
|
|
76
|
+
user: { id: user.id, login: user.login, role: user.role },
|
|
77
|
+
});
|
|
78
|
+
reply.code(res.status);
|
|
79
|
+
for (const [k, v] of Object.entries(res.headers ?? {}))
|
|
80
|
+
reply.header(k, v);
|
|
81
|
+
return reply.send(res.body ?? '');
|
|
82
|
+
};
|
|
83
|
+
const verbs = [...new Set([...mount.methods, ...DEFAULT_METHODS])];
|
|
84
|
+
app.route({ method: verbs, url: mount.prefix, handler });
|
|
85
|
+
app.route({ method: verbs, url: `${mount.prefix}/*`, handler });
|
|
86
|
+
if (mount.wellKnown) {
|
|
87
|
+
app.route({
|
|
88
|
+
method: verbs,
|
|
89
|
+
url: `/.well-known/${mount.wellKnown}`,
|
|
90
|
+
handler: async (_req, reply) => reply.code(301).header('location', `${mount.prefix}/`).send(),
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
package/dist/plugin-runtime.d.ts
CHANGED
|
@@ -1,14 +1,18 @@
|
|
|
1
|
+
import type { EntitySchema } from '@mikro-orm/core';
|
|
1
2
|
import { type Registry } from '@coffer-org/sdk/compose';
|
|
2
3
|
import type { PluginManifest } from '@coffer-org/sdk/plugin';
|
|
4
|
+
import { type PluginHooks } from './plugin-hooks.ts';
|
|
3
5
|
export declare function getPlugins(): Promise<PluginManifest[]>;
|
|
4
6
|
export declare function readDisabled(): Promise<Set<string>>;
|
|
5
7
|
export declare function getDisabled(): Promise<Set<string>>;
|
|
6
8
|
export declare function getPluginSettings(groupId: string): Promise<Record<string, unknown>>;
|
|
7
9
|
export declare function requireSettings<K extends string>(pluginId: string, keys: readonly K[]): Promise<Record<K, string>>;
|
|
10
|
+
export declare function privateTableEntities(plugins: PluginManifest[], hooks: Record<string, PluginHooks>): EntitySchema[];
|
|
8
11
|
export declare function initStorage(): Promise<Set<string>>;
|
|
9
12
|
export declare function initPlugins(): Promise<Registry>;
|
|
10
13
|
export declare function teardownPlugins(): Promise<void>;
|
|
11
14
|
export declare function teardownPlugin(id: string): Promise<void>;
|
|
15
|
+
export declare function pluginTables(p: PluginManifest, hooks?: Record<string, PluginHooks>): string[];
|
|
12
16
|
export declare function purgePluginData(p: PluginManifest, actor: string): Promise<void>;
|
|
13
17
|
export declare function filterEnabled<T extends {
|
|
14
18
|
id: string;
|
package/dist/plugin-runtime.js
CHANGED
|
@@ -2,7 +2,7 @@ import { composeRegistry } from '@coffer-org/sdk/compose';
|
|
|
2
2
|
import { getLogger } from '@coffer-org/sdk/logger';
|
|
3
3
|
import { initDb, getOrm, getEm } from "./db.js";
|
|
4
4
|
import { syncSchema } from "./schema-sync.js";
|
|
5
|
-
import { systemEntities, buildPluginEntities, shelfTableName } from "./entity-schema.js";
|
|
5
|
+
import { systemEntities, buildPluginEntities, buildPrivateTableEntity, shelfTableName, privateTableName, } from "./entity-schema.js";
|
|
6
6
|
import { pluginHooks, pluginCtx, HttpError } from "./plugin-hooks.js";
|
|
7
7
|
import { discoverPlugins, loadServerHooks } from "./plugin-discovery.js";
|
|
8
8
|
import { selectRows } from "./read-rows.js";
|
|
@@ -78,6 +78,9 @@ async function seedPluginRows() {
|
|
|
78
78
|
}
|
|
79
79
|
await fork.flush();
|
|
80
80
|
}
|
|
81
|
+
export function privateTableEntities(plugins, hooks) {
|
|
82
|
+
return plugins.flatMap((p) => (hooks[p.id]?.tables ?? []).map((t) => buildPrivateTableEntity(p.id, t)));
|
|
83
|
+
}
|
|
81
84
|
export async function initStorage() {
|
|
82
85
|
await initDb(systemEntities);
|
|
83
86
|
await runSystemMigrations(getEm().fork());
|
|
@@ -92,10 +95,10 @@ export async function initStorage() {
|
|
|
92
95
|
const hooks = await loadServerHooks();
|
|
93
96
|
await runMigrations({
|
|
94
97
|
em: getEm().fork(),
|
|
95
|
-
plugins: active.map((p) => ({ id: p.id, tables: pluginTables(p) })),
|
|
98
|
+
plugins: active.map((p) => ({ id: p.id, tables: pluginTables(p, hooks) })),
|
|
96
99
|
hooks,
|
|
97
100
|
});
|
|
98
|
-
const pluginEntities = buildPluginEntities(active);
|
|
101
|
+
const pluginEntities = [...buildPluginEntities(active), ...privateTableEntities(active, hooks)];
|
|
99
102
|
await assertSafeRequired(getEm().fork(), pluginEntities);
|
|
100
103
|
if (pluginEntities.length)
|
|
101
104
|
getOrm().discoverEntity(pluginEntities);
|
|
@@ -175,16 +178,17 @@ function pluginShelves(p) {
|
|
|
175
178
|
...(p.libraryShelves ?? []).map((m) => ({ library: m.library, shelf: m.shelf })),
|
|
176
179
|
];
|
|
177
180
|
}
|
|
178
|
-
function pluginTables(p) {
|
|
181
|
+
export function pluginTables(p, hooks) {
|
|
179
182
|
return [
|
|
180
183
|
...pluginShelves(p).map(({ library, shelf }) => shelfTableName(library, shelf)),
|
|
181
184
|
...(p.extends_ ?? []).map((e) => `extend__${e.id}`),
|
|
182
185
|
...(p.settings && p.settings.fields.length > 0 ? [`_settings__${p.id}`] : []),
|
|
186
|
+
...(hooks?.[p.id]?.tables ?? []).map((t) => privateTableName(p.id, t.name)),
|
|
183
187
|
];
|
|
184
188
|
}
|
|
185
189
|
export async function purgePluginData(p, actor) {
|
|
186
190
|
await teardownPlugin(p.id);
|
|
187
|
-
const tables = pluginTables(p);
|
|
191
|
+
const tables = pluginTables(p, pluginHooks);
|
|
188
192
|
const conn = getEm().fork().getConnection();
|
|
189
193
|
for (const t of tables) {
|
|
190
194
|
await conn.execute(`DROP TABLE IF EXISTS "${t}"`);
|
package/dist/plugins-api.d.ts
CHANGED
|
@@ -1,15 +1,21 @@
|
|
|
1
1
|
import type { FastifyInstance } from 'fastify';
|
|
2
2
|
import type { PluginManifest } from '@coffer-org/sdk/plugin';
|
|
3
3
|
import type { PluginAssetRecord } from './plugin-discovery.ts';
|
|
4
|
+
export interface PluginLabelEntry {
|
|
5
|
+
id: string;
|
|
6
|
+
label: string;
|
|
7
|
+
}
|
|
4
8
|
export interface PluginListEntry {
|
|
5
9
|
id: string;
|
|
10
|
+
label: string;
|
|
11
|
+
description: string;
|
|
6
12
|
installedVersion: string;
|
|
7
13
|
latestVersion: string | null;
|
|
8
14
|
packageName: string | null;
|
|
9
15
|
dependsOn: string[];
|
|
10
16
|
enabled: boolean;
|
|
11
|
-
libraries:
|
|
12
|
-
extends:
|
|
17
|
+
libraries: PluginLabelEntry[];
|
|
18
|
+
extends: PluginLabelEntry[];
|
|
13
19
|
hasSettings: boolean;
|
|
14
20
|
schema?: string;
|
|
15
21
|
web?: string;
|
package/dist/plugins-api.js
CHANGED
|
@@ -11,13 +11,15 @@ export async function buildPluginListResponse(plugins, assets, disabled, withUpd
|
|
|
11
11
|
const a = assetById.get(p.id);
|
|
12
12
|
return {
|
|
13
13
|
id: p.id,
|
|
14
|
+
label: p.label,
|
|
15
|
+
description: p.description,
|
|
14
16
|
installedVersion: a?.version ?? p.version,
|
|
15
17
|
latestVersion: withUpdates && a && !a.local ? await checkLatestVersion(a.packageName, { force: forceUpdates }) : null,
|
|
16
18
|
packageName: a?.packageName ?? null,
|
|
17
19
|
dependsOn: p.dependsOn,
|
|
18
20
|
enabled: !disabled.has(p.id),
|
|
19
|
-
libraries: (p.libraries ?? []).map((v) => v.meta.id),
|
|
20
|
-
extends: (p.extends_ ?? []).map((e) => e.id),
|
|
21
|
+
libraries: (p.libraries ?? []).map((v) => ({ id: v.meta.id, label: v.meta.label })),
|
|
22
|
+
extends: (p.extends_ ?? []).map((e) => ({ id: e.id, label: e.label })),
|
|
21
23
|
hasSettings: Boolean(p.settings && p.settings.fields.length > 0),
|
|
22
24
|
schema: a?.schema,
|
|
23
25
|
web: a?.web,
|
package/dist/records-api.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { isJsonArrayStored } from '@coffer-org/sdk/fields';
|
|
2
|
+
import { raw } from '@mikro-orm/core';
|
|
1
3
|
import { fieldMap, textSearchKeys, titleKey, recordTitle, listKeys, storageColumnsFor, magnitudeSub, resolveColumnKey, } from '@coffer-org/sdk/shelf';
|
|
2
4
|
import { tokenize, matchScoreFolded, foldText } from '@coffer-org/sdk/search';
|
|
3
5
|
import { getActiveRegistry, getShelf, getExtendsFor } from "./registry-context.js";
|
|
@@ -97,7 +99,7 @@ function resolveFilterKey(m, fm, k) {
|
|
|
97
99
|
const direct = fm[k];
|
|
98
100
|
if (direct) {
|
|
99
101
|
if (!direct.columns)
|
|
100
|
-
return { column: k, type: direct.column };
|
|
102
|
+
return { column: k, type: direct.column, json: isJsonArrayStored(direct) };
|
|
101
103
|
const sub = magnitudeSub(direct);
|
|
102
104
|
if (sub === undefined)
|
|
103
105
|
throw new FilterError(`${m.library}/${m.shelf}: '${k}' is a composite with no stored part — it owns no column to filter on`);
|
|
@@ -110,14 +112,24 @@ function resolveFilterKey(m, fm, k) {
|
|
|
110
112
|
throw new FilterError(`${m.library}/${m.shelf}: '${k}' names no part of composite '${owner.key}' (parts: ${Object.keys(owner.field.columns).join(', ')})`);
|
|
111
113
|
return { column: k, type: owner.field.columns[owner.sub] };
|
|
112
114
|
}
|
|
115
|
+
function jsonMemberClause(column, value) {
|
|
116
|
+
const col = `"${column.replace(/"/g, '""')}"`;
|
|
117
|
+
return {
|
|
118
|
+
[raw(`(json_valid(${col}) AND EXISTS (SELECT 1 FROM json_each(${col}) WHERE CAST(value AS TEXT) = ?))`, [value])]: 1,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
113
121
|
function buildWhere(m, filterParams) {
|
|
114
|
-
|
|
122
|
+
let where = {};
|
|
115
123
|
const fm = fieldMap(m.fields);
|
|
116
124
|
for (const [k, v] of Object.entries(filterParams)) {
|
|
117
125
|
if (k === 'id' || v === undefined)
|
|
118
126
|
continue;
|
|
119
127
|
const target = resolveFilterKey(m, fm, k);
|
|
120
|
-
if (target)
|
|
128
|
+
if (!target)
|
|
129
|
+
continue;
|
|
130
|
+
if (target.json)
|
|
131
|
+
where = { ...where, ...jsonMemberClause(target.column, String(v)) };
|
|
132
|
+
else
|
|
121
133
|
where[target.column] = coerceFilter(String(v), target.type);
|
|
122
134
|
}
|
|
123
135
|
if (filterParams['id']) {
|
package/dist/settings-write.d.ts
CHANGED
|
@@ -5,11 +5,10 @@ export interface SettingsFieldInfo {
|
|
|
5
5
|
key: string;
|
|
6
6
|
kind: string;
|
|
7
7
|
label?: string;
|
|
8
|
-
required: boolean;
|
|
9
8
|
options?: string[];
|
|
10
9
|
}
|
|
11
10
|
export declare function describeSettingsFields(fields: LayoutEl[]): SettingsFieldInfo[];
|
|
12
|
-
export declare function buildSettingsBody(groupId: string, fields: LayoutEl[], incoming: Record<string, unknown>, existing: Record<string, unknown>, label
|
|
11
|
+
export declare function buildSettingsBody(groupId: string, fields: LayoutEl[], incoming: Record<string, unknown>, existing: Record<string, unknown>, label: string): Record<string, unknown>;
|
|
13
12
|
export declare function writePluginSettings(em: EntityManager, groupId: string, incoming: Record<string, unknown>, actor: string, pluginsOverride?: PluginManifest[]): Promise<Record<string, unknown>>;
|
|
14
13
|
export declare function listSettings(pluginsOverride?: PluginManifest[]): Promise<{
|
|
15
14
|
group: string;
|
package/dist/settings-write.js
CHANGED
|
@@ -9,11 +9,10 @@ export function describeSettingsFields(fields) {
|
|
|
9
9
|
key,
|
|
10
10
|
kind: f.kind,
|
|
11
11
|
label: f.label,
|
|
12
|
-
required: f.required === true,
|
|
13
12
|
...(f.options ? { options: f.options.map((o) => o.value) } : {}),
|
|
14
13
|
}));
|
|
15
14
|
}
|
|
16
|
-
function settingsShelf(groupId, fields, label
|
|
15
|
+
function settingsShelf(groupId, fields, label) {
|
|
17
16
|
return { library: '_settings', shelf: groupId, label, fields };
|
|
18
17
|
}
|
|
19
18
|
export function buildSettingsBody(groupId, fields, incoming, existing, label) {
|
package/dist/system-settings.js
CHANGED
|
@@ -20,7 +20,6 @@ export const SYSTEM_SETTINGS = defineSettings({
|
|
|
20
20
|
],
|
|
21
21
|
}),
|
|
22
22
|
agent_id: field.string({ label: 'core.settings.agent_id', strict: true, noSearch: true }),
|
|
23
|
-
access_password: field.password({ label: 'core.settings.access_password' }),
|
|
24
23
|
trigger_prefix: field.string({ label: 'core.settings.trigger_prefix' }),
|
|
25
24
|
reply_window: field.int({ label: 'core.settings.reply_window', default: 1800 }),
|
|
26
25
|
},
|
package/dist/temporal.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { fieldEntries, storageColumns } from '@coffer-org/sdk/shelf';
|
|
2
2
|
export function dtStringToDate(s) {
|
|
3
3
|
return new Date(`${s}:00Z`);
|
|
4
4
|
}
|
|
@@ -8,8 +8,13 @@ export function dateToDtString(d) {
|
|
|
8
8
|
`T${p(d.getUTCHours())}:${p(d.getUTCMinutes())}`);
|
|
9
9
|
}
|
|
10
10
|
function datetimeKeysAt(fields) {
|
|
11
|
-
const
|
|
12
|
-
|
|
11
|
+
const out = [];
|
|
12
|
+
for (const [key, fm] of fieldEntries(fields)) {
|
|
13
|
+
for (const [col, type] of storageColumns(key, fm))
|
|
14
|
+
if (type === 'datetime')
|
|
15
|
+
out.push(col);
|
|
16
|
+
}
|
|
17
|
+
return out;
|
|
13
18
|
}
|
|
14
19
|
export function encodeTemporalAt(fields, row) {
|
|
15
20
|
const out = { ...row };
|