@coffer-org/server 7.3.0 → 7.5.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.js CHANGED
@@ -2,8 +2,7 @@ import { field } from '@coffer-org/sdk/fields';
2
2
  import { materializeTree } from '@coffer-org/sdk/materialize/pipeline';
3
3
  import { countUsers, createUser, findUserByLogin, findUserById, getPasswordHash, listUsers, updateUser, deleteUser, countAdmins, createSession, resolveSession, deleteSession, createApiToken, resolveApiToken, listApiTokens, revokeApiToken, } from "./auth-store.js";
4
4
  import { resolveAccessToken } from "./oauth-store.js";
5
- import { listLinks, unlink, mintLinkCode } from "./identity-link.js";
6
- import { listLinkableConnectors, connectorLinkUrl } from "./orchestrator/index.js";
5
+ import { listIdentityProviders, getIdentityProvider } from "./identity-providers.js";
7
6
  import { mcpResource } from "./public-url.js";
8
7
  import { verifyPassword } from "./auth-crypto.js";
9
8
  import { maskSecrets, preserveSecrets } from "./field-masking.js";
@@ -13,7 +12,6 @@ import { USERS_SHELF } from '@coffer-org/sdk/users-shelf';
13
12
  const PASSWORD_FIELDS = materializeTree({ password: field.password({}) });
14
13
  const SESSION_TTL_MS = 30 * 24 * 60 * 60 * 1000;
15
14
  const COOKIE_NAME = 'sid';
16
- const LINK_CODE_TTL_MS = 10 * 60 * 1000;
17
15
  export const PUBLIC_API_PATHS = ['/api/auth/status', '/api/auth/login', '/api/auth/setup'];
18
16
  function readCookie(req, name) {
19
17
  const header = req.headers.cookie;
@@ -234,29 +232,30 @@ export async function registerAuthApi(app) {
234
232
  app.get('/api/auth/links', async (req, reply) => {
235
233
  if (!req.user)
236
234
  return reply.code(401).send({ error: 'unauthorized' });
237
- return { links: await listLinks(req.user.id), linkable: listLinkableConnectors() };
235
+ const providers = listIdentityProviders();
236
+ const links = (await Promise.all(providers.map(async (p) => (await p.listLinks(req.user.id)).map((l) => ({ provider: p.id, ...l }))))).flat();
237
+ return { providers: providers.map(({ id, label, icon }) => ({ id, label, icon })), links };
238
238
  });
239
- app.post('/api/auth/links', async (req, reply) => {
239
+ app.post('/api/auth/links/:provider', async (req, reply) => {
240
240
  if (!req.user)
241
241
  return reply.code(401).send({ error: 'unauthorized' });
242
- const minted = await mintLinkCode(req.user.id, LINK_CODE_TTL_MS);
243
- const links = {};
244
- for (const connectorId of listLinkableConnectors()) {
245
- const url = connectorLinkUrl(connectorId, minted.raw);
246
- if (url)
247
- links[connectorId] = url;
248
- }
249
- return { ...minted, links };
242
+ const { provider } = req.params;
243
+ const p = getIdentityProvider(provider);
244
+ if (!p)
245
+ return reply.code(404).send({ error: 'not_found' });
246
+ const started = await p.beginLink(req.user.id);
247
+ if (!started)
248
+ return reply.code(503).send({ error: 'unavailable' });
249
+ return started;
250
250
  });
251
- app.delete('/api/auth/links/:connector/:externalId', async (req, reply) => {
251
+ app.delete('/api/auth/links/:provider/:externalId', async (req, reply) => {
252
252
  if (!req.user)
253
253
  return reply.code(401).send({ error: 'unauthorized' });
254
- const { connector, externalId } = req.params;
255
- const own = await listLinks(req.user.id);
256
- const owns = own.some((l) => l.connector === connector && l.externalId === externalId);
257
- if (!owns)
254
+ const { provider, externalId } = req.params;
255
+ const p = getIdentityProvider(provider);
256
+ if (!p)
258
257
  return reply.code(404).send({ error: 'not_found' });
259
- const ok = await unlink(connector, externalId);
258
+ const ok = await p.unlink(req.user.id, externalId);
260
259
  if (!ok)
261
260
  return reply.code(404).send({ error: 'not_found' });
262
261
  return { ok: true };
@@ -1,5 +1,4 @@
1
1
  import { hasChildren, isNamedField, isCollectionGroup, isEmbeddedGroup, isStorageGroup, } from '@coffer-org/sdk/fields';
2
- import { mandatedClientParts } from '@coffer-org/sdk/parts';
3
2
  import { PartContractError } from "./part-errors.js";
4
3
  import { createRefLoader } from "./part-ref.js";
5
4
  import { wholeClockColumn, partClockColumn } from "./cache-clock.js";
@@ -267,8 +266,6 @@ export function materializable(fm) {
267
266
  const parts = fm.parts ?? [];
268
267
  if (!parts.some((p) => p.mode === 'computedStored'))
269
268
  return false;
270
- if (mandatedClientParts(fm.required, parts).length > 0)
271
- return false;
272
269
  return true;
273
270
  }
274
271
  export function storedFilled(fm, value) {
@@ -0,0 +1,61 @@
1
+ export declare const HIDDEN_ROLES: readonly ["reasoning", "suggestions", "context"];
2
+ export declare const SIDECAR_ROLES: readonly ["reasoning", "suggestions"];
3
+ export declare const TITLE_MAX = 60;
4
+ export declare const DEFAULT_MAX_DEPTH = 40;
5
+ export declare const conversationEmitter: EventTarget;
6
+ export interface PutMessageInput {
7
+ conversationId: string;
8
+ msgId: string;
9
+ role: string;
10
+ sender?: string | null;
11
+ text: string;
12
+ attachments?: StoredAttachment[];
13
+ ts: number;
14
+ replyToId?: string | null;
15
+ }
16
+ export declare function onMessage(cb: (m: PutMessageInput) => void | Promise<void>): () => void;
17
+ export interface StoredMsg {
18
+ msgId: string;
19
+ role: string;
20
+ sender: string | null;
21
+ text: string;
22
+ attachments?: StoredAttachment[];
23
+ ts: number;
24
+ replyToId: string | null;
25
+ }
26
+ export interface StoredAttachment {
27
+ name: string;
28
+ mime?: string;
29
+ size?: number;
30
+ label?: string;
31
+ }
32
+ export interface Conversation {
33
+ id: string;
34
+ owner: string | null;
35
+ title: string | null;
36
+ agentId: string | null;
37
+ presetId: string | null;
38
+ visibility: 'private' | null;
39
+ updatedAt: string;
40
+ }
41
+ export interface ConversationSummary extends Conversation {
42
+ lastTs: number;
43
+ count: number;
44
+ firstUserText: string;
45
+ }
46
+ export declare function newConversationId(): string;
47
+ export declare function newMessageId(): string;
48
+ export declare function getConversation(id: string): Promise<Conversation>;
49
+ export declare function setConversation(id: string, patch: Partial<Omit<Conversation, 'id' | 'updatedAt'>>): Promise<void>;
50
+ export declare function readAndTouchConversation(id: string): Promise<Conversation>;
51
+ export declare function listConversations(viewerId: string): Promise<ConversationSummary[]>;
52
+ export declare function putMessage(m: PutMessageInput): Promise<void>;
53
+ export declare const insertMessage: typeof putMessage;
54
+ export declare function getMessage(conversationId: string, msgId: string): Promise<StoredMsg | null>;
55
+ export declare function readConversation(conversationId: string, opts?: {
56
+ limit?: number;
57
+ }): Promise<StoredMsg[]>;
58
+ export declare function readAll(conversationId: string): Promise<StoredMsg[]>;
59
+ export declare function countUserMessages(conversationId: string): Promise<number>;
60
+ export declare function lastMessageId(conversationId: string): Promise<string | null>;
61
+ export declare function pruneConversations(cutoffTs: number, cutoffIso: string): Promise<void>;
@@ -0,0 +1,223 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { getEm } from "./db.js";
3
+ import { mayRead } from "./orchestrator/conversation-access.js";
4
+ export const HIDDEN_ROLES = ['reasoning', 'suggestions', 'context'];
5
+ export const SIDECAR_ROLES = ['reasoning', 'suggestions'];
6
+ export const TITLE_MAX = 60;
7
+ export const DEFAULT_MAX_DEPTH = 40;
8
+ export const conversationEmitter = new EventTarget();
9
+ export function onMessage(cb) {
10
+ const handler = (e) => {
11
+ if (e instanceof CustomEvent) {
12
+ void cb(e.detail);
13
+ }
14
+ };
15
+ conversationEmitter.addEventListener('message', handler);
16
+ return () => {
17
+ conversationEmitter.removeEventListener('message', handler);
18
+ };
19
+ }
20
+ function capTitle(title) {
21
+ const t = title.trim();
22
+ return t.length <= TITLE_MAX ? t : `${t.slice(0, TITLE_MAX - 1)}…`;
23
+ }
24
+ function emptyConversation(id) {
25
+ return {
26
+ id,
27
+ owner: null,
28
+ title: null,
29
+ agentId: null,
30
+ presetId: null,
31
+ visibility: null,
32
+ updatedAt: '',
33
+ };
34
+ }
35
+ function toConversation(row) {
36
+ return {
37
+ id: row.id,
38
+ owner: row.owner,
39
+ title: row.title,
40
+ agentId: row.agent_id,
41
+ presetId: row.preset_id,
42
+ visibility: row.visibility === 'private' ? 'private' : null,
43
+ updatedAt: row.updated_at,
44
+ };
45
+ }
46
+ function toStored(r) {
47
+ let attachments;
48
+ if (r.attachments) {
49
+ try {
50
+ const parsed = JSON.parse(r.attachments);
51
+ if (Array.isArray(parsed)) {
52
+ const valid = parsed.filter((v) => typeof v === 'object' && v !== null && typeof v.name === 'string');
53
+ if (valid.length)
54
+ attachments = valid;
55
+ }
56
+ }
57
+ catch {
58
+ }
59
+ }
60
+ return {
61
+ msgId: r.msg_id,
62
+ role: r.role,
63
+ sender: r.sender,
64
+ text: r.text,
65
+ ...(attachments ? { attachments } : {}),
66
+ ts: r.ts,
67
+ replyToId: r.reply_to_id,
68
+ };
69
+ }
70
+ export function newConversationId() {
71
+ return randomUUID();
72
+ }
73
+ export function newMessageId() {
74
+ return randomUUID();
75
+ }
76
+ export async function getConversation(id) {
77
+ const em = getEm().fork();
78
+ const row = (await em.findOne('_Conversation', { id }));
79
+ if (!row)
80
+ return emptyConversation(id);
81
+ return toConversation(row);
82
+ }
83
+ export async function setConversation(id, patch) {
84
+ const em = getEm().fork();
85
+ const data = { updated_at: new Date().toISOString() };
86
+ if ('agentId' in patch)
87
+ data['agent_id'] = patch.agentId ?? null;
88
+ if ('presetId' in patch)
89
+ data['preset_id'] = patch.presetId ?? null;
90
+ if ('title' in patch)
91
+ data['title'] = patch.title ? capTitle(patch.title) : null;
92
+ if ('owner' in patch)
93
+ data['owner'] = patch.owner ?? null;
94
+ if ('visibility' in patch)
95
+ data['visibility'] = patch.visibility ?? null;
96
+ const existing = (await em.findOne('_Conversation', { id }));
97
+ if (existing) {
98
+ em.assign(existing, data);
99
+ }
100
+ else {
101
+ em.persist(em.create('_Conversation', {
102
+ id,
103
+ agent_id: data['agent_id'] ?? null,
104
+ preset_id: data['preset_id'] ?? null,
105
+ title: data['title'] ?? null,
106
+ owner: data['owner'] ?? null,
107
+ visibility: data['visibility'] ?? null,
108
+ updated_at: data['updated_at'],
109
+ }));
110
+ }
111
+ await em.flush();
112
+ conversationEmitter.dispatchEvent(new CustomEvent('conversation', { detail: { id, patch } }));
113
+ }
114
+ export async function readAndTouchConversation(id) {
115
+ const em = getEm().fork();
116
+ const row = (await em.findOne('_Conversation', { id }));
117
+ if (!row)
118
+ return emptyConversation(id);
119
+ const now = new Date().toISOString();
120
+ em.assign(row, { updated_at: now });
121
+ await em.flush();
122
+ return toConversation(row);
123
+ }
124
+ export async function listConversations(viewerId) {
125
+ const em = getEm().fork();
126
+ const convRows = (await em.find('_Conversation', {}));
127
+ const msgRows = (await em.find('_ConversationMessage', {}, { orderBy: { ts: 'asc', msg_id: 'asc' } }));
128
+ const msgSummaries = new Map();
129
+ for (const r of msgRows) {
130
+ if (HIDDEN_ROLES.includes(r.role))
131
+ continue;
132
+ const cur = msgSummaries.get(r.conversation_id);
133
+ if (!cur) {
134
+ msgSummaries.set(r.conversation_id, {
135
+ lastTs: r.ts,
136
+ count: 1,
137
+ firstUserText: r.role === 'user' ? r.text : '',
138
+ });
139
+ continue;
140
+ }
141
+ cur.count += 1;
142
+ if (r.ts > cur.lastTs)
143
+ cur.lastTs = r.ts;
144
+ if (!cur.firstUserText && r.role === 'user')
145
+ cur.firstUserText = r.text;
146
+ }
147
+ const convMap = new Map();
148
+ for (const r of convRows) {
149
+ convMap.set(r.id, toConversation(r));
150
+ }
151
+ const allIds = new Set([...convMap.keys(), ...msgSummaries.keys()]);
152
+ const out = [];
153
+ for (const id of allIds) {
154
+ const conv = convMap.get(id) ?? emptyConversation(id);
155
+ if (!mayRead(conv, viewerId))
156
+ continue;
157
+ const summary = msgSummaries.get(id);
158
+ out.push({
159
+ ...conv,
160
+ lastTs: summary?.lastTs ?? 0,
161
+ count: summary?.count ?? 0,
162
+ firstUserText: summary?.firstUserText ?? '',
163
+ });
164
+ }
165
+ return out.sort((a, b) => {
166
+ if (b.lastTs !== a.lastTs)
167
+ return b.lastTs - a.lastTs;
168
+ return (b.updatedAt || '').localeCompare(a.updatedAt || '') || b.id.localeCompare(a.id);
169
+ });
170
+ }
171
+ export async function putMessage(m) {
172
+ const em = getEm().fork();
173
+ await em.upsert('_ConversationMessage', {
174
+ conversation_id: m.conversationId,
175
+ msg_id: m.msgId,
176
+ role: m.role,
177
+ sender: m.sender ?? null,
178
+ attachments: m.attachments?.length ? JSON.stringify(m.attachments) : null,
179
+ text: m.text,
180
+ ts: m.ts,
181
+ reply_to_id: m.replyToId ?? null,
182
+ });
183
+ await em.flush();
184
+ conversationEmitter.dispatchEvent(new CustomEvent('message', { detail: m }));
185
+ }
186
+ export const insertMessage = putMessage;
187
+ export async function getMessage(conversationId, msgId) {
188
+ const em = getEm().fork();
189
+ const row = (await em.findOne('_ConversationMessage', { conversation_id: conversationId, msg_id: msgId }));
190
+ return row ? toStored(row) : null;
191
+ }
192
+ export async function readConversation(conversationId, opts) {
193
+ const limit = opts?.limit ?? DEFAULT_MAX_DEPTH;
194
+ const em = getEm().fork();
195
+ const visible = (await em.find('_ConversationMessage', { conversation_id: conversationId, role: { $nin: HIDDEN_ROLES } }, { orderBy: { ts: 'desc', msg_id: 'desc' }, limit }));
196
+ const oldest = visible.at(-1)?.ts;
197
+ const sidecars = oldest === undefined
198
+ ? []
199
+ : (await em.find('_ConversationMessage', { conversation_id: conversationId, role: { $in: SIDECAR_ROLES }, ts: { $gte: oldest - 1 } }, { orderBy: { ts: 'desc', msg_id: 'desc' } }));
200
+ return [...visible, ...sidecars]
201
+ .sort((a, b) => b.ts - a.ts || (a.msg_id < b.msg_id ? 1 : a.msg_id > b.msg_id ? -1 : 0))
202
+ .reverse()
203
+ .map(toStored);
204
+ }
205
+ export async function readAll(conversationId) {
206
+ const em = getEm().fork();
207
+ const rows = (await em.find('_ConversationMessage', { conversation_id: conversationId }, { orderBy: { ts: 'asc', msg_id: 'asc' } }));
208
+ return rows.map(toStored);
209
+ }
210
+ export async function countUserMessages(conversationId) {
211
+ const em = getEm().fork();
212
+ return em.count('_ConversationMessage', { conversation_id: conversationId, role: 'user' });
213
+ }
214
+ export async function lastMessageId(conversationId) {
215
+ const em = getEm().fork();
216
+ const row = (await em.findOne('_ConversationMessage', { conversation_id: conversationId, role: { $nin: HIDDEN_ROLES } }, { orderBy: { ts: 'desc', msg_id: 'desc' } }));
217
+ return row ? row.msg_id : null;
218
+ }
219
+ export async function pruneConversations(cutoffTs, cutoffIso) {
220
+ const em = getEm().fork();
221
+ await em.nativeDelete('_ConversationMessage', { ts: { $lt: cutoffTs } });
222
+ await em.nativeDelete('_Conversation', { updated_at: { $lt: cutoffIso }, owner: null, visibility: null });
223
+ }
@@ -3,10 +3,13 @@ import type { ShelfDef } from '@coffer-org/sdk/shelf';
3
3
  import type { ExtendDef } from '@coffer-org/sdk/extend';
4
4
  import type { SettingsDef } from '@coffer-org/sdk/settings';
5
5
  import type { PluginManifest } from '@coffer-org/sdk/plugin';
6
+ import type { PrivateTableDef } from './plugin-hooks.ts';
6
7
  export declare function shelfTableName(library: string, shelf: string): string;
7
8
  export declare function buildEntitySchema(m: ShelfDef): EntitySchema;
8
9
  export declare function buildExtendEntitySchema(e: ExtendDef): EntitySchema;
9
10
  export declare function buildSettingsEntitySchema(pluginId: string, settings: SettingsDef): EntitySchema;
11
+ export declare function privateTableName(pluginId: string, name: string): string;
12
+ export declare function buildPrivateTableEntity(pluginId: string, t: PrivateTableDef): EntitySchema;
10
13
  export declare function childTableName(library: string, shelf: string, key: string): string;
11
14
  export declare function buildCollectionEntities(m: ShelfDef): EntitySchema[];
12
15
  export declare function buildExtendCollectionEntities(e: ExtendDef): EntitySchema[];
@@ -18,15 +21,13 @@ export declare const SeedRowSchema: EntitySchema<any, never, import("@mikro-orm/
18
21
  export declare const EmbeddingSchema: EntitySchema<any, never, import("@mikro-orm/core").EntityCtor<any>>;
19
22
  export declare const PluginStateSchema: EntitySchema<any, never, import("@mikro-orm/core").EntityCtor<any>>;
20
23
  export declare const RecordActivitySchema: EntitySchema<any, never, import("@mikro-orm/core").EntityCtor<any>>;
21
- export declare const ThreadMessageSchema: EntitySchema<any, never, import("@mikro-orm/core").EntityCtor<any>>;
22
- export declare const ThreadStateSchema: EntitySchema<any, never, import("@mikro-orm/core").EntityCtor<any>>;
24
+ export declare const ConversationMessageSchema: EntitySchema<any, never, import("@mikro-orm/core").EntityCtor<any>>;
25
+ export declare const ConversationSchema: EntitySchema<any, never, import("@mikro-orm/core").EntityCtor<any>>;
23
26
  export declare function buildPluginEntities(plugins: PluginManifest[]): EntitySchema[];
24
27
  export declare const MsgLogSchema: EntitySchema<any, never, import("@mikro-orm/core").EntityCtor<any>>;
25
28
  export declare const UserSchema: EntitySchema<any, never, import("@mikro-orm/core").EntityCtor<any>>;
26
29
  export declare const SessionSchema: EntitySchema<any, never, import("@mikro-orm/core").EntityCtor<any>>;
27
30
  export declare const ApiTokenSchema: EntitySchema<any, never, import("@mikro-orm/core").EntityCtor<any>>;
28
- export declare const IdentityLinkSchema: EntitySchema<any, never, import("@mikro-orm/core").EntityCtor<any>>;
29
- export declare const LinkCodeSchema: EntitySchema<any, never, import("@mikro-orm/core").EntityCtor<any>>;
30
31
  export declare const OAuthClientSchema: EntitySchema<any, never, import("@mikro-orm/core").EntityCtor<any>>;
31
32
  export declare const OAuthCodeSchema: EntitySchema<any, never, import("@mikro-orm/core").EntityCtor<any>>;
32
33
  export declare const OAuthTokenSchema: EntitySchema<any, never, import("@mikro-orm/core").EntityCtor<any>>;
@@ -23,14 +23,14 @@ function columnProp(field, nullable) {
23
23
  p['default'] = field.default;
24
24
  return p;
25
25
  }
26
- function addFieldProps(properties, fields, plainNullable) {
26
+ function addFieldProps(properties, fields) {
27
27
  for (const [key, field] of fieldEntries(fields)) {
28
28
  if (field.columns) {
29
29
  for (const [col, type] of storageColumns(key, field))
30
30
  properties[col] = { type: mikroType(type), nullable: true };
31
31
  }
32
32
  else {
33
- properties[key] = columnProp(field, plainNullable(field));
33
+ properties[key] = columnProp(field, true);
34
34
  }
35
35
  }
36
36
  for (const col of cacheClockColumns(fields))
@@ -46,7 +46,7 @@ export function buildEntitySchema(m) {
46
46
  updated_at: { type: 'text' },
47
47
  deleted_at: { type: 'text', nullable: true },
48
48
  };
49
- addFieldProps(properties, m.fields, (f) => !f.required);
49
+ addFieldProps(properties, m.fields);
50
50
  const name = shelfTableName(m.library, m.shelf);
51
51
  return new EntitySchema({ name, tableName: name, properties });
52
52
  }
@@ -54,7 +54,7 @@ export function buildExtendEntitySchema(e) {
54
54
  const properties = {
55
55
  base_id: { type: 'integer', primary: true },
56
56
  };
57
- addFieldProps(properties, e.fields, () => true);
57
+ addFieldProps(properties, e.fields);
58
58
  const name = `extend__${e.id}`;
59
59
  return new EntitySchema({ name, tableName: name, properties });
60
60
  }
@@ -62,10 +62,26 @@ export function buildSettingsEntitySchema(pluginId, settings) {
62
62
  const properties = {
63
63
  plugin_id: { type: 'text', primary: true },
64
64
  };
65
- addFieldProps(properties, settings.fields, (f) => !f.required);
65
+ addFieldProps(properties, settings.fields);
66
66
  const name = `_settings__${pluginId}`;
67
67
  return new EntitySchema({ name, tableName: name, properties });
68
68
  }
69
+ export function privateTableName(pluginId, name) {
70
+ return `_${pluginId}__${name}`;
71
+ }
72
+ export function buildPrivateTableEntity(pluginId, t) {
73
+ const properties = {
74
+ id: { type: 'integer', primary: true, autoincrement: true },
75
+ };
76
+ addFieldProps(properties, t.fields);
77
+ const name = privateTableName(pluginId, t.name);
78
+ return new EntitySchema({
79
+ name,
80
+ tableName: name,
81
+ properties,
82
+ uniques: (t.unique ?? []).map((properties) => ({ properties })),
83
+ });
84
+ }
69
85
  export function childTableName(library, shelf, key) {
70
86
  return `${library}__${shelf}__${key}`;
71
87
  }
@@ -75,7 +91,7 @@ function buildChildSchemaAt(prefix, c) {
75
91
  parent_id: { type: 'integer', index: true },
76
92
  position: { type: 'integer' },
77
93
  };
78
- addFieldProps(properties, c.group.fields, (f) => !f.required);
94
+ addFieldProps(properties, c.group.fields);
79
95
  const name = `${prefix}__${c.key}`;
80
96
  return new EntitySchema({ name, tableName: name, properties });
81
97
  }
@@ -186,12 +202,11 @@ export const RecordActivitySchema = new EntitySchema({
186
202
  last_viewed_at: { type: 'text', nullable: true },
187
203
  },
188
204
  });
189
- export const ThreadMessageSchema = new EntitySchema({
190
- name: '_ThreadMessage',
191
- tableName: '_thread_message',
205
+ export const ConversationMessageSchema = new EntitySchema({
206
+ name: '_ConversationMessage',
207
+ tableName: '_conversation_message',
192
208
  properties: {
193
- connector: { type: 'text', primary: true },
194
- chat_id: { type: 'text', primary: true },
209
+ conversation_id: { type: 'text', primary: true },
195
210
  msg_id: { type: 'text', primary: true },
196
211
  role: { type: 'text' },
197
212
  sender: { type: 'text', nullable: true },
@@ -201,12 +216,11 @@ export const ThreadMessageSchema = new EntitySchema({
201
216
  reply_to_id: { type: 'text', nullable: true },
202
217
  },
203
218
  });
204
- export const ThreadStateSchema = new EntitySchema({
205
- name: '_ThreadState',
206
- tableName: '_thread_state',
219
+ export const ConversationSchema = new EntitySchema({
220
+ name: '_Conversation',
221
+ tableName: '_conversation',
207
222
  properties: {
208
- connector: { type: 'text', primary: true },
209
- chat_id: { type: 'text', primary: true },
223
+ id: { type: 'text', primary: true },
210
224
  agent_id: { type: 'text', nullable: true },
211
225
  preset_id: { type: 'text', nullable: true },
212
226
  title: { type: 'text', nullable: true },
@@ -277,27 +291,6 @@ export const ApiTokenSchema = new EntitySchema({
277
291
  revoked: { type: 'integer' },
278
292
  },
279
293
  });
280
- export const IdentityLinkSchema = new EntitySchema({
281
- name: '_IdentityLink',
282
- tableName: '_identity_links',
283
- properties: {
284
- id: { type: 'integer', primary: true, autoincrement: true },
285
- connector: { type: 'text' },
286
- external_id: { type: 'text' },
287
- user_id: { type: 'integer' },
288
- linked_at: { type: 'text' },
289
- },
290
- });
291
- export const LinkCodeSchema = new EntitySchema({
292
- name: '_LinkCode',
293
- tableName: '_link_codes',
294
- properties: {
295
- code_hash: { type: 'text', primary: true },
296
- user_id: { type: 'integer' },
297
- expires_at: { type: 'text' },
298
- created_at: { type: 'text' },
299
- },
300
- });
301
294
  export const OAuthClientSchema = new EntitySchema({
302
295
  name: '_OAuthClient',
303
296
  tableName: '_oauth_clients',
@@ -353,9 +346,7 @@ export const systemEntities = [
353
346
  OAuthClientSchema,
354
347
  OAuthCodeSchema,
355
348
  OAuthTokenSchema,
356
- ThreadMessageSchema,
357
- ThreadStateSchema,
358
- IdentityLinkSchema,
359
- LinkCodeSchema,
349
+ ConversationMessageSchema,
350
+ ConversationSchema,
360
351
  buildSettingsEntitySchema(SYSTEM_SETTINGS_ID, SYSTEM_SETTINGS),
361
352
  ];
@@ -0,0 +1,17 @@
1
+ export interface IdentityProvider {
2
+ id: string;
3
+ label: string;
4
+ icon: string;
5
+ beginLink(userId: number): Promise<{
6
+ url?: string;
7
+ code?: string;
8
+ } | null>;
9
+ listLinks(userId: number): Promise<{
10
+ externalId: string;
11
+ linkedAt: string;
12
+ }[]>;
13
+ unlink(userId: number, externalId: string): Promise<boolean>;
14
+ }
15
+ export declare function registerIdentityProvider(p: IdentityProvider): () => void;
16
+ export declare function listIdentityProviders(): IdentityProvider[];
17
+ export declare function getIdentityProvider(id: string): IdentityProvider | undefined;
@@ -0,0 +1,14 @@
1
+ const providers = new Map();
2
+ export function registerIdentityProvider(p) {
3
+ providers.set(p.id, p);
4
+ return () => {
5
+ if (providers.get(p.id) === p)
6
+ providers.delete(p.id);
7
+ };
8
+ }
9
+ export function listIdentityProviders() {
10
+ return [...providers.values()].sort((a, b) => a.id.localeCompare(b.id));
11
+ }
12
+ export function getIdentityProvider(id) {
13
+ return providers.get(id);
14
+ }
package/dist/index.js CHANGED
@@ -8,6 +8,7 @@ import Fastify from 'fastify';
8
8
  import cors from '@fastify/cors';
9
9
  import multipart from '@fastify/multipart';
10
10
  import fastifyStatic from '@fastify/static';
11
+ import { layoutToClient } from '@coffer-org/sdk/shelf';
11
12
  import { shelfTableName } from "./entity-schema.js";
12
13
  import { getEm, closeDb } from "./db.js";
13
14
  import { recordCounts } from "./counts.js";
@@ -306,7 +307,7 @@ app.post('/api/system/orchestrator-diagnostics', async (req, reply) => {
306
307
  return;
307
308
  reply.send(await orchestratorDiagnostics());
308
309
  });
309
- app.get('/api/settings', async () => (await settingsGroups()).map((g) => ({ id: g.id, label: g.label, fields: g.fields })));
310
+ app.get('/api/settings', async () => (await settingsGroups()).map((g) => ({ id: g.id, label: g.label, fields: layoutToClient(g.fields) })));
310
311
  app.get('/api/settings/:id', async (req, reply) => {
311
312
  if (!requireAdmin(req, reply))
312
313
  return;
@@ -35,7 +35,6 @@ export interface FieldInfo {
35
35
  key: string;
36
36
  kind: string;
37
37
  prim: string;
38
- required: boolean;
39
38
  agent?: string;
40
39
  relation?: {
41
40
  library: string;
@@ -52,7 +52,6 @@ export function flattenFields(items) {
52
52
  key,
53
53
  kind: t.kind,
54
54
  prim: t.prim,
55
- required: t.required,
56
55
  ...(t.agent ? { agent: t.agent } : {}),
57
56
  ...(t.relation ? { relation: t.relation } : {}),
58
57
  ...(t.options ? { options: t.options } : {}),
@@ -69,7 +68,6 @@ export function flattenFields(items) {
69
68
  key: it.key,
70
69
  kind: 'group',
71
70
  prim: 'group',
72
- required: it.required === true,
73
71
  ...(it.agent ? { agent: it.agent } : {}),
74
72
  ...(it.multiple ? { multiple: true } : {}),
75
73
  fields: flattenFields(it.fields),
package/dist/mutate.js CHANGED
@@ -1,7 +1,6 @@
1
1
  import { serialize } from '@mikro-orm/core';
2
2
  import { buildZodObject, buildZodObjectPartial, fieldEntries, collectionGroups } from '@coffer-org/sdk/shelf';
3
3
  import { isJsonStored, decodeVmsg } from '@coffer-org/sdk/fields';
4
- import { resolveFlag } from '@coffer-org/sdk/condition';
5
4
  import { getEm } from "./db.js";
6
5
  import { selectRows } from "./read-rows.js";
7
6
  import { normalizeFileFields, dropUnchangedFileFields, touchesFileFields } from "./file-fields.js";
@@ -190,20 +189,6 @@ export async function updateRecord(m, entityName, id, input, ctx, afterBase) {
190
189
  throw new ValidationError(fileIssues);
191
190
  const { base, collections } = splitCollections(m, { ...patchData });
192
191
  const merged = { ...existing, ...base };
193
- const reqIssues = [];
194
- for (const [key, f] of fieldEntries(m.fields)) {
195
- if (f.required === true)
196
- continue;
197
- if (key.includes('__'))
198
- continue;
199
- if (!resolveFlag(f.required, merged))
200
- continue;
201
- const v = merged[key];
202
- if (v == null || v === '' || (Array.isArray(v) && v.length === 0))
203
- reqIssues.push({ field: key, code: 'required', path: [key] });
204
- }
205
- if (reqIssues.length)
206
- throw new ValidationError(reqIssues);
207
192
  const storedWithMeta = hasStoredPartWork(m.fields)
208
193
  ? await readCollectionsWithMeta(tx, m, id)
209
194
  : { collections: {}, meta: {} };
@@ -0,0 +1,18 @@
1
+ import type { TurnSink } from './types.ts';
2
+ import type { RenderFn } from './live-message.ts';
3
+ export interface DraftChannelOps {
4
+ draft(text: string): Promise<void>;
5
+ send(text: string): Promise<string | null>;
6
+ }
7
+ export interface DraftSinkOpts {
8
+ ops: DraftChannelOps;
9
+ throttleMs: number;
10
+ keepAliveMs: number;
11
+ render: RenderFn;
12
+ preview: (r: {
13
+ text: string | null;
14
+ reasoning: string | null;
15
+ }) => string;
16
+ maxLength: number;
17
+ }
18
+ export declare function makeDraftSink(o: DraftSinkOpts): TurnSink;