@unson/brainbase-mcp 0.21.2 → 0.22.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.
Files changed (32) hide show
  1. package/dist/local-web-fetch-bridge.js +1 -1
  2. package/dist/local-web-host.d.ts +59 -0
  3. package/dist/local-web-host.js +171 -4
  4. package/dist/meeting-minutes-http.d.ts +22 -0
  5. package/dist/meeting-minutes-http.js +252 -0
  6. package/dist/meeting-minutes-lineage-adapters.d.ts +157 -0
  7. package/dist/meeting-minutes-lineage-adapters.js +410 -0
  8. package/dist/meeting-minutes-lineage-http.d.ts +20 -0
  9. package/dist/meeting-minutes-lineage-http.js +221 -0
  10. package/dist/meeting-minutes-lineage-lifecycle.d.ts +58 -0
  11. package/dist/meeting-minutes-lineage-lifecycle.js +146 -0
  12. package/dist/meeting-minutes-lineage.d.ts +307 -0
  13. package/dist/meeting-minutes-lineage.js +1030 -0
  14. package/dist/meeting-minutes-storage-controller.d.ts +182 -0
  15. package/dist/meeting-minutes-storage-controller.js +366 -0
  16. package/dist/meeting-minutes-storage-http.d.ts +25 -0
  17. package/dist/meeting-minutes-storage-http.js +312 -0
  18. package/dist/meeting-minutes-storage.d.ts +169 -0
  19. package/dist/meeting-minutes-storage.js +402 -0
  20. package/dist/meeting-minutes.d.ts +223 -0
  21. package/dist/meeting-minutes.js +1172 -0
  22. package/package.json +46 -3
  23. package/ui/brainbase-tokens.css +35 -35
  24. package/ui/local-web-shell.css +20 -17
  25. package/ui/local-web-shell.js +34 -1
  26. package/ui/meeting-minutes-lineage-http.js +247 -0
  27. package/ui/meeting-minutes-lineage.css +177 -0
  28. package/ui/meeting-minutes-lineage.js +485 -0
  29. package/ui/meeting-minutes-storage.css +170 -0
  30. package/ui/meeting-minutes-storage.js +388 -0
  31. package/ui/meeting-minutes.css +215 -0
  32. package/ui/meeting-minutes.js +622 -0
@@ -0,0 +1,402 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { constants as fsConstants } from 'node:fs';
3
+ import { lstat, open, realpath } from 'node:fs/promises';
4
+ import { TextDecoder } from 'node:util';
5
+ import { isAbsolute, join, posix, relative, resolve, sep } from 'node:path';
6
+ /** The first real provider adapter. Its root is supplied by the customer. */
7
+ export const FILESYSTEM_MEETING_MINUTES_ADAPTER = 'filesystem';
8
+ const FILESYSTEM_CAPABILITIES = Object.freeze({
9
+ save: false,
10
+ read: true,
11
+ read_only: true,
12
+ history: false,
13
+ current_acl: 'injected',
14
+ retention_delete: false,
15
+ });
16
+ const DEFAULT_MAX_BYTES = 10 * 1024 * 1024;
17
+ const READ_CHUNK_BYTES = 64 * 1024;
18
+ /**
19
+ * Read-only adapter for a customer-selected filesystem root.
20
+ *
21
+ * The adapter keeps no native copy and performs no external write. A plain
22
+ * filesystem has no historical version contract, so old references fail with
23
+ * an integrity mismatch when the current file changes.
24
+ */
25
+ export class FilesystemMeetingMinutesAdapter {
26
+ capabilities = FILESYSTEM_CAPABILITIES;
27
+ root;
28
+ provider;
29
+ authorize;
30
+ maxBytes;
31
+ constructor(options) {
32
+ if (!options || typeof options.root !== 'string' || !options.root.trim()) {
33
+ throw new MeetingMinutesStorageError('invalid_configuration', 'An absolute customer filesystem root is required');
34
+ }
35
+ if (!isAbsolute(options.root)) {
36
+ throw new MeetingMinutesStorageError('invalid_configuration', 'The filesystem root must be absolute');
37
+ }
38
+ const provider = options.provider ?? FILESYSTEM_MEETING_MINUTES_ADAPTER;
39
+ if (typeof provider !== 'string' || !provider.trim()) {
40
+ throw new MeetingMinutesStorageError('invalid_configuration', 'The provider identifier must not be empty');
41
+ }
42
+ if (typeof options.authorize !== 'function') {
43
+ throw new MeetingMinutesStorageError('invalid_configuration', 'A host authorization callback is required');
44
+ }
45
+ const maxBytes = options.max_bytes ?? DEFAULT_MAX_BYTES;
46
+ if (!Number.isSafeInteger(maxBytes) || maxBytes <= 0) {
47
+ throw new MeetingMinutesStorageError('invalid_configuration', 'max_bytes must be a positive safe integer');
48
+ }
49
+ this.root = resolve(options.root);
50
+ this.provider = provider;
51
+ this.authorize = options.authorize;
52
+ this.maxBytes = maxBytes;
53
+ }
54
+ async register(input) {
55
+ const authorizationError = await this.authorizationFailure(input?.request_context, 'register', { locator: typeof input?.locator === 'string' ? input.locator : '' });
56
+ if (authorizationError)
57
+ return this.registerFailure('denied', authorizationError);
58
+ const locator = normalizeLocator(input?.locator);
59
+ if (locator.status !== 'ok')
60
+ return this.registerFailure(locator.status, locator.reason);
61
+ const file = await this.resolveFile(locator.locator);
62
+ if (file.status !== 'ok')
63
+ return this.registerFailure(file.status, file.reason);
64
+ const content = await this.readStableFile(file);
65
+ if (content.status !== 'ok')
66
+ return this.registerFailure(content.status, content.reason);
67
+ try {
68
+ return {
69
+ status: 'registered',
70
+ reference: this.reference(locator.locator, content.bytes),
71
+ capabilities: this.capabilities,
72
+ };
73
+ }
74
+ catch (error) {
75
+ return this.registerFailure(...readFailure(error));
76
+ }
77
+ }
78
+ async read(input) {
79
+ const referenceError = this.referenceFailure(input?.reference);
80
+ if (referenceError)
81
+ return this.readFailure('invalid_reference', referenceError);
82
+ const authorizationError = await this.authorizationFailure(input?.request_context, 'read', { reference: input.reference });
83
+ if (authorizationError)
84
+ return this.readFailure('denied', authorizationError);
85
+ const locator = normalizeLocator(input.reference.locator);
86
+ if (locator.status !== 'ok')
87
+ return this.readFailure('invalid_reference', locator.reason);
88
+ const file = await this.resolveFile(locator.locator);
89
+ if (file.status !== 'ok')
90
+ return this.readFailure(file.status, file.reason);
91
+ const content = await this.readStableFile(file);
92
+ if (content.status !== 'ok')
93
+ return this.readFailure(content.status, content.reason);
94
+ const observed = this.reference(locator.locator, content.bytes);
95
+ if (observed.revision !== input.reference.revision || observed.digest !== input.reference.digest) {
96
+ return {
97
+ status: 'integrity_mismatch',
98
+ reason: 'The external source changed; the requested historical revision is unavailable',
99
+ observed_reference: observed,
100
+ capabilities: this.capabilities,
101
+ };
102
+ }
103
+ return {
104
+ status: 'available',
105
+ reference: observed,
106
+ body: content.body,
107
+ capabilities: this.capabilities,
108
+ };
109
+ }
110
+ async read_source(input) {
111
+ const source = input?.source_ref;
112
+ if (!source || typeof source !== 'object') {
113
+ return this.readFailure('invalid_reference', 'A persisted external source reference is required');
114
+ }
115
+ const reference = {
116
+ provider: source.provider,
117
+ locator: source.locator,
118
+ revision: source.revision,
119
+ digest: source.digest,
120
+ provenance: {
121
+ source: 'external',
122
+ adapter: FILESYSTEM_MEETING_MINUTES_ADAPTER,
123
+ observed_at: new Date().toISOString(),
124
+ },
125
+ };
126
+ return this.read({ reference, request_context: input.request_context });
127
+ }
128
+ async read_history(input) {
129
+ const authorizationError = await this.authorizationFailure(input?.request_context, 'read_history', { reference: input?.reference });
130
+ if (authorizationError) {
131
+ return {
132
+ status: 'denied',
133
+ reason: authorizationError,
134
+ capabilities: this.capabilities,
135
+ };
136
+ }
137
+ return this.unsupported('read_history', 'history', 'This filesystem adapter has no historical version provider');
138
+ }
139
+ async save(_input) {
140
+ return this.unsupported('save', 'save', 'The external filesystem is read-only through Brainbase');
141
+ }
142
+ async delete(input) {
143
+ const authorizationError = await this.authorizationFailure(input?.request_context, 'retention_delete', { reference: input?.reference });
144
+ if (authorizationError) {
145
+ return {
146
+ status: 'denied',
147
+ reason: authorizationError,
148
+ capabilities: this.capabilities,
149
+ };
150
+ }
151
+ return this.unsupported('retention_delete', 'retention_delete', 'Retention and deletion remain owned by the external storage policy');
152
+ }
153
+ async authorizationFailure(context, operation, target) {
154
+ if (!context || typeof context !== 'object')
155
+ return 'A host request context is required';
156
+ const requestContext = context;
157
+ if (typeof requestContext.principal_id !== 'string' || !requestContext.principal_id.trim()) {
158
+ return 'A host request principal is required';
159
+ }
160
+ let decision;
161
+ try {
162
+ decision = await this.authorize({ principal_id: requestContext.principal_id }, operation, target);
163
+ }
164
+ catch {
165
+ return 'The host authorization check could not be completed';
166
+ }
167
+ if (typeof decision === 'boolean') {
168
+ return decision ? undefined : 'The current host authorization does not allow this operation';
169
+ }
170
+ if (!decision || typeof decision !== 'object' || typeof decision.allowed !== 'boolean') {
171
+ return 'The host authorization check returned an invalid decision';
172
+ }
173
+ if (!decision.allowed)
174
+ return decision.reason ?? 'The current host authorization does not allow this operation';
175
+ if (decision.checked_at !== undefined && Number.isNaN(Date.parse(decision.checked_at))) {
176
+ return 'The host authorization decision timestamp is invalid';
177
+ }
178
+ return undefined;
179
+ }
180
+ async readStableFile(file) {
181
+ let handle;
182
+ try {
183
+ const noFollow = fsConstants.O_NOFOLLOW;
184
+ if (typeof noFollow !== 'number') {
185
+ return { status: 'unavailable', reason: 'The host does not provide O_NOFOLLOW for safe external reads' };
186
+ }
187
+ handle = await open(file.path, fsConstants.O_RDONLY | noFollow);
188
+ const opened = await handle.stat();
189
+ if (!sameFileIdentity(opened, file) || !opened.isFile()) {
190
+ return { status: 'unavailable', reason: 'The external source changed before it could be read safely' };
191
+ }
192
+ const openedRealPath = await realpath(file.path);
193
+ if (openedRealPath !== file.real_path) {
194
+ return { status: 'symlink_rejected', reason: 'The external source path changed outside the configured root' };
195
+ }
196
+ if (!Number.isSafeInteger(opened.size) || opened.size > this.maxBytes) {
197
+ return { status: 'too_large', reason: `The external source exceeds the ${this.maxBytes}-byte limit` };
198
+ }
199
+ const content = await readBounded(handle, this.maxBytes);
200
+ if (content.status !== 'ok')
201
+ return content;
202
+ const closedStat = await handle.stat();
203
+ if (!sameFileIdentity(closedStat, file) || closedStat.size !== opened.size) {
204
+ return { status: 'unavailable', reason: 'The external source changed while it was being read' };
205
+ }
206
+ const closedRealPath = await realpath(file.path);
207
+ if (closedRealPath !== file.real_path) {
208
+ return { status: 'symlink_rejected', reason: 'The external source path changed outside the configured root' };
209
+ }
210
+ let body;
211
+ try {
212
+ body = decodeUtf8(content.bytes);
213
+ }
214
+ catch {
215
+ return { status: 'invalid_encoding', reason: 'The external source is not valid UTF-8' };
216
+ }
217
+ return { status: 'ok', bytes: content.bytes, body };
218
+ }
219
+ catch (error) {
220
+ const [status, reason] = readFailure(error);
221
+ return { status, reason };
222
+ }
223
+ finally {
224
+ if (handle)
225
+ await handle.close().catch(() => undefined);
226
+ }
227
+ }
228
+ registerFailure(status, reason) {
229
+ if (status === 'registered')
230
+ throw new Error('A registration failure cannot be registered');
231
+ return { status, reason, capabilities: this.capabilities };
232
+ }
233
+ readFailure(status, reason) {
234
+ return { status, reason, capabilities: this.capabilities };
235
+ }
236
+ unsupported(operation, capability, reason) {
237
+ return { status: 'unsupported', operation, capability, reason, capabilities: this.capabilities };
238
+ }
239
+ reference(locator, bytes) {
240
+ const digest = `sha256:${createHash('sha256').update(bytes).digest('hex')}`;
241
+ return Object.freeze({
242
+ provider: this.provider,
243
+ locator,
244
+ revision: digest,
245
+ digest,
246
+ provenance: Object.freeze({
247
+ source: 'external',
248
+ adapter: FILESYSTEM_MEETING_MINUTES_ADAPTER,
249
+ observed_at: new Date().toISOString(),
250
+ }),
251
+ });
252
+ }
253
+ referenceFailure(reference) {
254
+ if (!reference || typeof reference !== 'object')
255
+ return 'An external reference is required';
256
+ const candidate = reference;
257
+ if (candidate.provider !== this.provider)
258
+ return 'The reference belongs to a different provider';
259
+ if (typeof candidate.locator !== 'string')
260
+ return 'The reference locator is invalid';
261
+ if (!isSha256(candidate.revision) || !isSha256(candidate.digest))
262
+ return 'The reference must include exact SHA-256 revision and digest';
263
+ if (candidate.revision !== candidate.digest)
264
+ return 'The filesystem revision must equal the source digest';
265
+ const provenance = candidate.provenance;
266
+ if (!provenance || typeof provenance !== 'object')
267
+ return 'The external reference provenance is required';
268
+ const candidateProvenance = provenance;
269
+ if (candidateProvenance.source !== 'external' || candidateProvenance.adapter !== FILESYSTEM_MEETING_MINUTES_ADAPTER) {
270
+ return 'The external reference provenance is invalid';
271
+ }
272
+ if (typeof candidateProvenance.observed_at !== 'string' || Number.isNaN(Date.parse(candidateProvenance.observed_at))) {
273
+ return 'The external reference observation timestamp is invalid';
274
+ }
275
+ return undefined;
276
+ }
277
+ async resolveFile(locator) {
278
+ let rootStat;
279
+ try {
280
+ rootStat = await lstat(this.root);
281
+ }
282
+ catch (error) {
283
+ return fsFailure(error, `The configured filesystem root is unavailable: ${this.root}`);
284
+ }
285
+ if (rootStat.isSymbolicLink()) {
286
+ return { status: 'symlink_rejected', reason: 'The configured filesystem root must not be a symlink' };
287
+ }
288
+ if (!rootStat.isDirectory()) {
289
+ return { status: 'not_found', reason: 'The configured filesystem root is not a directory' };
290
+ }
291
+ let current = this.root;
292
+ let finalStat;
293
+ const parts = locator.split('/');
294
+ for (const [index, part] of parts.entries()) {
295
+ current = join(current, part);
296
+ let stat;
297
+ try {
298
+ stat = await lstat(current);
299
+ }
300
+ catch (error) {
301
+ return fsFailure(error, `The external source does not exist at ${locator}`);
302
+ }
303
+ if (stat.isSymbolicLink()) {
304
+ return { status: 'symlink_rejected', reason: `Symlink path components are not allowed: ${locator}` };
305
+ }
306
+ if (index === parts.length - 1 ? !stat.isFile() : !stat.isDirectory()) {
307
+ return { status: 'not_found', reason: `The external source is not a regular file: ${locator}` };
308
+ }
309
+ if (index === parts.length - 1)
310
+ finalStat = stat;
311
+ }
312
+ let realFile;
313
+ try {
314
+ const realRoot = await realpath(this.root);
315
+ realFile = await realpath(current);
316
+ const escaped = relative(realRoot, realFile);
317
+ if (escaped === '..' || escaped.startsWith(`..${sep}`) || isAbsolute(escaped)) {
318
+ return { status: 'symlink_rejected', reason: `The external locator escapes the configured root: ${locator}` };
319
+ }
320
+ }
321
+ catch (error) {
322
+ return fsFailure(error, `The external source cannot be resolved at ${locator}`);
323
+ }
324
+ if (!finalStat)
325
+ return { status: 'not_found', reason: `The external source is not a regular file: ${locator}` };
326
+ return {
327
+ status: 'ok',
328
+ path: current,
329
+ real_path: realFile,
330
+ device: finalStat.dev,
331
+ inode: finalStat.ino,
332
+ };
333
+ }
334
+ }
335
+ export class MeetingMinutesStorageError extends Error {
336
+ code;
337
+ constructor(code, message) {
338
+ super(message);
339
+ this.name = 'MeetingMinutesStorageError';
340
+ this.code = code;
341
+ }
342
+ }
343
+ export function createFilesystemMeetingMinutesAdapter(options) {
344
+ return new FilesystemMeetingMinutesAdapter(options);
345
+ }
346
+ function normalizeLocator(locator) {
347
+ if (typeof locator !== 'string' || !locator.trim()) {
348
+ return { status: 'invalid_locator', reason: 'A root-relative locator is required' };
349
+ }
350
+ if (locator.includes('\0') || locator.includes('\\') || locator.startsWith('/') || posix.isAbsolute(locator)) {
351
+ return { status: 'invalid_locator', reason: 'The locator must be a root-relative POSIX path' };
352
+ }
353
+ const segments = locator.split('/');
354
+ if (segments.some((segment) => segment === '..')) {
355
+ return { status: 'invalid_locator', reason: 'Parent path segments are not allowed in an external locator' };
356
+ }
357
+ const normalized = posix.normalize(locator);
358
+ if (normalized === '.' || normalized === '..' || normalized.startsWith('../')) {
359
+ return { status: 'invalid_locator', reason: 'The locator must resolve to a file inside the configured root' };
360
+ }
361
+ return { status: 'ok', locator: normalized };
362
+ }
363
+ async function readBounded(handle, maxBytes) {
364
+ const chunks = [];
365
+ let total = 0;
366
+ while (total <= maxBytes) {
367
+ const chunkSize = Math.min(READ_CHUNK_BYTES, maxBytes - total + 1);
368
+ const chunk = Buffer.allocUnsafe(chunkSize);
369
+ const result = await handle.read(chunk, 0, chunkSize, null);
370
+ if (result.bytesRead === 0)
371
+ break;
372
+ total += result.bytesRead;
373
+ if (total > maxBytes) {
374
+ return { status: 'too_large', reason: `The external source exceeds the ${maxBytes}-byte limit` };
375
+ }
376
+ chunks.push(chunk.subarray(0, result.bytesRead));
377
+ }
378
+ return { status: 'ok', bytes: Buffer.concat(chunks, total) };
379
+ }
380
+ function sameFileIdentity(stat, file) {
381
+ return stat.dev === file.device && stat.ino === file.inode;
382
+ }
383
+ function decodeUtf8(bytes) {
384
+ return new TextDecoder('utf-8', { fatal: true }).decode(bytes);
385
+ }
386
+ function isSha256(value) {
387
+ return typeof value === 'string' && /^sha256:[0-9a-f]{64}$/.test(value);
388
+ }
389
+ function readFailure(error) {
390
+ const failure = fsFailure(error, 'The external source could not be read');
391
+ return [failure.status, failure.reason];
392
+ }
393
+ function fsFailure(error, fallback) {
394
+ const code = error && typeof error === 'object' && 'code' in error ? String(error.code) : '';
395
+ if (code === 'ENOENT' || code === 'ENOTDIR')
396
+ return { status: 'not_found', reason: fallback };
397
+ if (code === 'ELOOP')
398
+ return { status: 'symlink_rejected', reason: fallback };
399
+ if (code === 'EACCES' || code === 'EPERM')
400
+ return { status: 'unavailable', reason: fallback };
401
+ return { status: 'unavailable', reason: fallback };
402
+ }
@@ -0,0 +1,223 @@
1
+ /** Provider-independent native meeting and minutes lifecycle. */
2
+ export declare const MEETING_MINUTES_CONTRACT_VERSION: "brainbase.meeting-minutes.v1";
3
+ export declare const MEETING_MINUTES_SNAPSHOT_VERSION: 1;
4
+ export declare const MEETING_MINUTES_SIDECAR: "meeting-minutes.json";
5
+ export declare const MEETING_MINUTES_BODY_DIGEST_ALGORITHM: "sha256";
6
+ export type MeetingId = string;
7
+ export type MinutesId = string;
8
+ export type MinutesVersionId = string;
9
+ export interface MeetingMinutesSourceRef {
10
+ /** Stable provider name supplied by an adapter, never used as the native id. */
11
+ readonly provider: string;
12
+ readonly locator: string;
13
+ /** Provider revision at which the source was observed. */
14
+ readonly revision: string;
15
+ /** Exact provider content digest for duplicate suppression and readback. */
16
+ readonly digest: string;
17
+ /** Adapter supplied provenance is retained as part of the source reference. */
18
+ readonly provenance?: Readonly<Record<string, unknown>>;
19
+ /** Provider adapters may carry additional opaque locator details. */
20
+ readonly [key: string]: unknown;
21
+ }
22
+ export interface MeetingMinutesAcl {
23
+ readonly owner_id: string;
24
+ readonly reader_ids: readonly string[];
25
+ readonly writer_ids: readonly string[];
26
+ }
27
+ export interface MeetingRecord {
28
+ readonly meeting_id: MeetingId;
29
+ readonly title: string;
30
+ readonly scheduled_at?: string;
31
+ readonly ended_at?: string;
32
+ readonly participant_ids: readonly string[];
33
+ readonly metadata: Readonly<Record<string, unknown>>;
34
+ readonly acl: MeetingMinutesAcl;
35
+ readonly created_at: string;
36
+ readonly created_actor_id: string;
37
+ readonly revision: number;
38
+ }
39
+ export interface MinutesConfirmationReceipt {
40
+ readonly version_id: MinutesVersionId;
41
+ readonly actor_id: string;
42
+ readonly confirmed_at: string;
43
+ }
44
+ export interface MinutesVersionRecord {
45
+ readonly version_id: MinutesVersionId;
46
+ readonly minutes_id: MinutesId;
47
+ readonly meeting_id: MeetingId;
48
+ readonly predecessor_version_id?: MinutesVersionId;
49
+ /** Native versions carry the writable Brainbase body. External versions omit it. */
50
+ readonly body?: string;
51
+ readonly body_digest?: string;
52
+ readonly created_at: string;
53
+ readonly created_actor_id: string;
54
+ readonly confirmation?: MinutesConfirmationReceipt;
55
+ readonly source_ref?: MeetingMinutesSourceRef;
56
+ }
57
+ export interface MinutesDocumentRecord {
58
+ readonly minutes_id: MinutesId;
59
+ readonly meeting_id: MeetingId;
60
+ readonly title: string;
61
+ readonly metadata: Readonly<Record<string, unknown>>;
62
+ readonly acl: MeetingMinutesAcl;
63
+ readonly version_ids: readonly MinutesVersionId[];
64
+ readonly current_version_id: MinutesVersionId;
65
+ readonly created_at: string;
66
+ readonly created_actor_id: string;
67
+ readonly updated_at: string;
68
+ readonly revision: number;
69
+ }
70
+ export interface MeetingMinutesSnapshot {
71
+ readonly schema_version: typeof MEETING_MINUTES_SNAPSHOT_VERSION;
72
+ /** Aggregate revision used by the storage port's compare-and-swap. */
73
+ readonly revision: number;
74
+ readonly meetings: readonly MeetingRecord[];
75
+ readonly documents: readonly MinutesDocumentRecord[];
76
+ readonly versions: readonly MinutesVersionRecord[];
77
+ readonly idempotency: readonly MeetingMinutesIdempotencyRecord[];
78
+ }
79
+ export type MeetingMinutesIdempotencyOperation = 'create_meeting' | 'create_minutes' | 'save_version' | 'confirm_version';
80
+ /** Durable write receipt. It lets a retried mutation find its exact target after restart. */
81
+ export interface MeetingMinutesIdempotencyRecord {
82
+ readonly key: string;
83
+ readonly principal_id: string;
84
+ readonly fingerprint: string;
85
+ readonly operation: MeetingMinutesIdempotencyOperation;
86
+ readonly meeting_id: MeetingId;
87
+ readonly minutes_id?: MinutesId;
88
+ readonly version_id?: MinutesVersionId;
89
+ }
90
+ export interface MeetingMinutesRequestContext {
91
+ /** Resolved by the host or organization auth boundary. Never read from input JSON. */
92
+ readonly principal_id: string;
93
+ }
94
+ export type MeetingMinutesResource = MeetingRecord | MinutesDocumentRecord | MinutesVersionRecord;
95
+ export interface MeetingMinutesAuthorizer {
96
+ can_read?(resource: MeetingMinutesResource, context: MeetingMinutesRequestContext): boolean | Promise<boolean>;
97
+ can_write?(resource: MeetingMinutesResource, context: MeetingMinutesRequestContext): boolean | Promise<boolean>;
98
+ }
99
+ /** Minimal seam for organization storage adapters and future durable stores. */
100
+ export interface MeetingMinutesStoragePort {
101
+ read(): Promise<MeetingMinutesSnapshot>;
102
+ /** Implementations must compare the on-disk/current revision before replacing the snapshot. */
103
+ write(next: MeetingMinutesSnapshot, expected_revision: number): Promise<void>;
104
+ }
105
+ export interface MeetingCreateInput {
106
+ readonly title: string;
107
+ readonly scheduled_at?: string;
108
+ readonly ended_at?: string;
109
+ readonly participant_ids?: readonly string[];
110
+ readonly metadata?: Readonly<Record<string, unknown>>;
111
+ readonly minutes_title?: string;
112
+ readonly minutes_metadata?: Readonly<Record<string, unknown>>;
113
+ readonly initial_body?: string;
114
+ readonly source_ref?: MeetingMinutesSourceRef;
115
+ readonly idempotency_key?: string;
116
+ readonly idempotency_fingerprint?: string;
117
+ }
118
+ export interface CreateMinutesInput {
119
+ readonly title?: string;
120
+ readonly metadata?: Readonly<Record<string, unknown>>;
121
+ /** Exactly one of body and source_ref is required. */
122
+ readonly body?: string;
123
+ readonly source_ref?: MeetingMinutesSourceRef;
124
+ /** Aggregate snapshot revision observed by the caller. */
125
+ readonly expected_revision: number;
126
+ readonly idempotency_key?: string;
127
+ readonly idempotency_fingerprint?: string;
128
+ }
129
+ export interface SaveMinutesVersionInput {
130
+ readonly minutes_id: MinutesId;
131
+ /** Exactly one of body and source_ref is required. */
132
+ readonly body?: string;
133
+ readonly metadata?: Readonly<Record<string, unknown>>;
134
+ readonly source_ref?: MeetingMinutesSourceRef;
135
+ readonly expected_revision: number;
136
+ readonly predecessor_version_id?: MinutesVersionId;
137
+ readonly idempotency_key?: string;
138
+ readonly idempotency_fingerprint?: string;
139
+ }
140
+ export interface ConfirmMinutesVersionInput {
141
+ readonly minutes_id: MinutesId;
142
+ readonly version_id: MinutesVersionId;
143
+ readonly expected_revision: number;
144
+ readonly idempotency_key?: string;
145
+ readonly idempotency_fingerprint?: string;
146
+ }
147
+ export interface MeetingMinutesSummary {
148
+ readonly meeting: MeetingRecord;
149
+ readonly minutes: readonly MinutesDocumentRecord[];
150
+ }
151
+ export interface MeetingMinutesDetail extends MeetingMinutesSummary {
152
+ readonly versions: readonly MinutesVersionRecord[];
153
+ readonly snapshot_revision: number;
154
+ }
155
+ export type MeetingMinutesErrorCode = 'invalid_input' | 'not_found' | 'revision_conflict' | 'authorization_denied' | 'corrupt_record' | 'storage_unavailable' | 'readback_mismatch' | 'idempotency_conflict';
156
+ export declare class MeetingMinutesError extends Error {
157
+ readonly code: MeetingMinutesErrorCode;
158
+ constructor(code: MeetingMinutesErrorCode, message: string);
159
+ }
160
+ export interface MeetingMinutesStoreOptions {
161
+ readonly data_dir?: string;
162
+ readonly storage?: MeetingMinutesStoragePort;
163
+ readonly now?: () => Date;
164
+ /** Test and adapter hook; generated IDs remain Brainbase-owned. */
165
+ readonly id_factory?: () => string;
166
+ readonly authorizer?: MeetingMinutesAuthorizer;
167
+ /** Optional external registration/deduplication seam. Native-only stores omit it. */
168
+ readonly external_source_hook?: MeetingMinutesExternalSourceHook;
169
+ }
170
+ export interface MeetingMinutesExternalSourceIdentity {
171
+ readonly meeting_id: MeetingId;
172
+ readonly minutes_id: MinutesId;
173
+ readonly version_id: MinutesVersionId;
174
+ }
175
+ export interface MeetingMinutesExternalSourceHook {
176
+ /** Return the exact previously registered identity, if any. */
177
+ find_existing(source_ref: MeetingMinutesSourceRef): Promise<MeetingMinutesExternalSourceIdentity | undefined>;
178
+ /** Register a successfully persisted external version for future deduplication. */
179
+ register(source_ref: MeetingMinutesSourceRef, identity: MeetingMinutesExternalSourceIdentity): Promise<void>;
180
+ }
181
+ export interface MeetingMinutesStore {
182
+ list_meetings(context: MeetingMinutesRequestContext): Promise<readonly MeetingMinutesSummary[]>;
183
+ create_meeting(input: MeetingCreateInput, context: MeetingMinutesRequestContext): Promise<MeetingMinutesDetail>;
184
+ create_minutes(meeting_id: MeetingId, input: CreateMinutesInput, context: MeetingMinutesRequestContext): Promise<MeetingMinutesDetail>;
185
+ get_meeting(meeting_id: MeetingId, context: MeetingMinutesRequestContext): Promise<MeetingMinutesDetail>;
186
+ get_version(meeting_id: MeetingId, minutes_id: MinutesId, version_id: MinutesVersionId, context: MeetingMinutesRequestContext): Promise<MinutesVersionRecord>;
187
+ save_version(input: SaveMinutesVersionInput, context: MeetingMinutesRequestContext): Promise<MeetingMinutesDetail>;
188
+ confirm_version(input: ConfirmMinutesVersionInput, context: MeetingMinutesRequestContext): Promise<MeetingMinutesDetail>;
189
+ }
190
+ /**
191
+ * Provider-neutral exact-version seam used by lineage and other read-only
192
+ * consumers. The shape intentionally mirrors the lineage contract without
193
+ * importing that optional module into the native lifecycle.
194
+ */
195
+ export interface MeetingMinutesVersionPortReference {
196
+ readonly meetingId: MeetingId;
197
+ readonly minutesId: MinutesId;
198
+ readonly versionId: MinutesVersionId;
199
+ readonly contentDigest: string | null;
200
+ readonly locator: unknown;
201
+ readonly provenance: Readonly<Record<string, unknown>>;
202
+ }
203
+ export interface MeetingMinutesVersionPortAccess {
204
+ readonly principal: string;
205
+ readonly [key: string]: unknown;
206
+ }
207
+ export interface MeetingMinutesVersionPort {
208
+ readExact(reference: MeetingMinutesVersionPortReference, access: MeetingMinutesVersionPortAccess): Promise<{
209
+ readonly reference: MeetingMinutesVersionPortReference;
210
+ } | null>;
211
+ }
212
+ export declare function createMeetingMinutesStore(options: MeetingMinutesStoreOptions): MeetingMinutesStore;
213
+ /** Exposes the native file adapter for consumers that need an explicit port. */
214
+ export declare function createMeetingMinutesStorage(data_dir: string): MeetingMinutesStoragePort;
215
+ /** Stable body digest used by version readback and external source references. */
216
+ export declare function meetingMinutesBodyDigest(body: string): string;
217
+ /**
218
+ * Adapt one native store to the exact-version port. Every call delegates to
219
+ * `get_version`, so current ACL is evaluated by the same store instance that
220
+ * owns the native snapshot. The caller's reference is returned only after all
221
+ * stable source identity fields match the persisted version.
222
+ */
223
+ export declare function createMeetingMinutesVersionPort(store: MeetingMinutesStore): MeetingMinutesVersionPort;