@unson/brainbase-mcp 0.6.0 → 0.8.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/contracts/foundation/graph-history.sql +634 -0
- package/contracts/judgment-value-proof/schema.json +43 -1
- package/dist/canonical-task-contract.d.ts +9 -0
- package/dist/canonical-task-contract.js +39 -0
- package/dist/canonical-task-principal.d.ts +16 -0
- package/dist/canonical-task-principal.js +36 -0
- package/dist/canonical-task-service.d.ts +251 -0
- package/dist/canonical-task-service.js +820 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +107 -0
- package/dist/company-os-evaluation.d.ts +193 -0
- package/dist/company-os-evaluation.js +1150 -0
- package/dist/company-os-impact-review.d.ts +248 -0
- package/dist/company-os-impact-review.js +663 -0
- package/dist/company-os-learning-adoption.d.ts +288 -0
- package/dist/company-os-learning-adoption.js +944 -0
- package/dist/company-os-objectives.d.ts +63 -0
- package/dist/company-os-objectives.js +234 -0
- package/dist/company-os-receipt-adapter.d.ts +109 -0
- package/dist/company-os-receipt-adapter.js +636 -0
- package/dist/constraint-exception-store.d.ts +23 -0
- package/dist/constraint-exception-store.js +108 -0
- package/dist/constraint-resolution.d.ts +175 -0
- package/dist/constraint-resolution.js +922 -0
- package/dist/decision-adapter.d.ts +145 -0
- package/dist/decision-adapter.js +586 -0
- package/dist/durable-wait-http.d.ts +51 -0
- package/dist/durable-wait-http.js +497 -0
- package/dist/durable-waits.d.ts +234 -0
- package/dist/durable-waits.js +944 -0
- package/dist/execution-authority-http.d.ts +28 -0
- package/dist/execution-authority-http.js +359 -0
- package/dist/execution-authority.d.ts +201 -0
- package/dist/execution-authority.js +634 -0
- package/dist/foundation-authenticated-tools.d.ts +56 -0
- package/dist/foundation-authenticated-tools.js +347 -0
- package/dist/foundation-catalog.d.ts +26 -0
- package/dist/foundation-catalog.js +206 -0
- package/dist/foundation-constraint-store.d.ts +50 -0
- package/dist/foundation-constraint-store.js +215 -0
- package/dist/foundation-graph-http.d.ts +43 -0
- package/dist/foundation-graph-http.js +243 -0
- package/dist/foundation-graph-write.d.ts +90 -0
- package/dist/foundation-graph-write.js +447 -0
- package/dist/foundation-http.d.ts +90 -0
- package/dist/foundation-http.js +439 -0
- package/dist/foundation-public-provider.d.ts +140 -0
- package/dist/foundation-public-provider.js +192 -0
- package/dist/foundation-store.d.ts +132 -0
- package/dist/foundation-store.js +654 -0
- package/dist/graph-corrections.d.ts +157 -0
- package/dist/graph-corrections.js +620 -0
- package/dist/graph-foundation-reader.d.ts +60 -0
- package/dist/graph-foundation-reader.js +494 -0
- package/dist/graph-retrieval.d.ts +6 -1
- package/dist/graph-revision-reader.d.ts +80 -0
- package/dist/graph-revision-reader.js +246 -0
- package/dist/graph-web-http.d.ts +26 -0
- package/dist/graph-web-http.js +215 -0
- package/dist/graph-web.d.ts +364 -0
- package/dist/graph-web.js +639 -0
- package/dist/judgment-dag-composition.d.ts +224 -0
- package/dist/judgment-dag-composition.js +731 -0
- package/dist/judgment-dag.d.ts +2 -0
- package/dist/judgment-dag.js +1 -0
- package/dist/judgment-problem-snapshot.d.ts +213 -0
- package/dist/judgment-problem-snapshot.js +1073 -0
- package/dist/judgment-value-proof-review.d.ts +166 -0
- package/dist/judgment-value-proof-review.js +437 -0
- package/dist/judgment-value-proof.d.ts +28 -1
- package/dist/judgment-value-proof.js +53 -8
- package/dist/judgment-view-http.d.ts +22 -0
- package/dist/judgment-view-http.js +128 -0
- package/dist/judgment-view.d.ts +222 -0
- package/dist/judgment-view.js +627 -0
- package/dist/knowledge-adapter.d.ts +112 -0
- package/dist/knowledge-adapter.js +691 -0
- package/dist/knowledge-continuation.d.ts +127 -0
- package/dist/knowledge-continuation.js +446 -0
- package/dist/knowledge-lookup.d.ts +178 -0
- package/dist/knowledge-lookup.js +758 -0
- package/dist/local-web-fetch-bridge.d.ts +5 -0
- package/dist/local-web-fetch-bridge.js +37 -0
- package/dist/local-web-host.d.ts +109 -0
- package/dist/local-web-host.js +688 -0
- package/dist/local-web-security.d.ts +25 -0
- package/dist/local-web-security.js +95 -0
- package/dist/ontology-foundation.d.ts +209 -0
- package/dist/ontology-foundation.js +1000 -0
- package/dist/ontology.d.ts +13 -0
- package/dist/ontology.js +14 -0
- package/dist/organization-connection.d.ts +164 -0
- package/dist/organization-connection.js +428 -0
- package/dist/organization-graph.d.ts +15 -0
- package/dist/organization-graph.js +24 -4
- package/dist/personal-knowledge.d.ts +132 -0
- package/dist/personal-knowledge.js +779 -0
- package/dist/personal-memory-handover.d.ts +108 -0
- package/dist/personal-memory-handover.js +369 -0
- package/dist/philosophy-revision-reader.d.ts +85 -0
- package/dist/philosophy-revision-reader.js +241 -0
- package/dist/portable-graph.d.ts +24 -0
- package/dist/portable-graph.js +77 -0
- package/dist/problem-candidates.d.ts +149 -0
- package/dist/problem-candidates.js +898 -0
- package/dist/problem-selection.d.ts +238 -0
- package/dist/problem-selection.js +995 -0
- package/dist/resource-reservation-http.d.ts +31 -0
- package/dist/resource-reservation-http.js +371 -0
- package/dist/resource-reservations.d.ts +221 -0
- package/dist/resource-reservations.js +577 -0
- package/dist/server.d.ts +104 -2
- package/dist/server.js +186 -4
- package/dist/ssot.d.ts +6 -1
- package/dist/ssot.js +63 -5
- package/dist/streamable-http.d.ts +42 -0
- package/dist/streamable-http.js +288 -0
- package/dist/types.d.ts +20 -0
- package/dist/value-proof-review-http.d.ts +25 -0
- package/dist/value-proof-review-http.js +185 -0
- package/dist/world-model.d.ts +199 -0
- package/dist/world-model.js +955 -0
- package/package.json +230 -3
- package/ui/brainbase-tokens.css +56 -0
- package/ui/graph-own-share.js +367 -0
- package/ui/graph-projects-view.css +23 -0
- package/ui/graph-projects-view.js +730 -0
- package/ui/graph-registry-view.css +101 -0
- package/ui/graph-registry-view.js +804 -0
- package/ui/graph-view-shared.css +522 -0
- package/ui/graph-view-shared.js +1227 -0
- package/ui/judgment-view.css +312 -0
- package/ui/judgment-view.js +508 -0
- package/ui/local-web-shell.css +396 -0
- package/ui/local-web-shell.js +378 -0
- package/ui/objective-editor-http-port.js +154 -0
- package/ui/objective-editor.css +286 -0
- package/ui/objective-editor.js +1586 -0
- package/ui/outcome-knowledge.css +50 -14
- package/ui/outcome-knowledge.js +15 -1
- package/ui/outcome-mana.css +91 -14
- package/ui/outcome-mana.js +958 -61
- package/ui/value-proof-review.css +504 -0
- package/ui/value-proof-review.js +1104 -0
- package/ui/workspace-kit.css +485 -0
- package/ui/workspace-kit.js +229 -0
- package/ui/world-model-view.css +188 -0
- package/ui/world-model-view.js +486 -0
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import { FoundationStoreError } from './foundation-store.js';
|
|
2
|
+
import { ConstraintError, validateConstraintExceptionRecord, } from './constraint-resolution.js';
|
|
3
|
+
/**
|
|
4
|
+
* Adapts the Objective Story's GraphFoundationRevisionStore to the resolver's
|
|
5
|
+
* ConstraintStore port. Definitions are persisted in the existing PersonalOs
|
|
6
|
+
* aggregate through FoundationRevisionStore; no second Graph or file is
|
|
7
|
+
* created here.
|
|
8
|
+
*/
|
|
9
|
+
export class FoundationConstraintStore {
|
|
10
|
+
foundationStore;
|
|
11
|
+
exceptionStore;
|
|
12
|
+
constructor(options) {
|
|
13
|
+
this.foundationStore = options.foundationStore;
|
|
14
|
+
this.exceptionStore = options.exceptionStore;
|
|
15
|
+
}
|
|
16
|
+
async append(input, context) {
|
|
17
|
+
const storeContext = toFoundationContext(context);
|
|
18
|
+
let reference;
|
|
19
|
+
try {
|
|
20
|
+
if (input.expectedPreviousRevision === null) {
|
|
21
|
+
reference = await this.foundationStore.create(input.record, storeContext);
|
|
22
|
+
}
|
|
23
|
+
else if (input.expectedPreviousRevision !== undefined) {
|
|
24
|
+
reference = await this.foundationStore.update({
|
|
25
|
+
reference: {
|
|
26
|
+
id: input.record.id,
|
|
27
|
+
type: 'constraint',
|
|
28
|
+
revision: input.expectedPreviousRevision,
|
|
29
|
+
},
|
|
30
|
+
next: input.record,
|
|
31
|
+
expectedRevision: input.expectedPreviousRevision,
|
|
32
|
+
}, storeContext);
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
throw new ConstraintError('revision_conflict', 'Foundation constraint append requires an explicit previous revision or null for create', 409);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
throw normalizeFoundationError(error, input.record.id);
|
|
40
|
+
}
|
|
41
|
+
if (reference.id !== input.record.id || reference.type !== 'constraint' || reference.revision !== input.record.revision) {
|
|
42
|
+
throw new ConstraintError('constraint_store_corrupt', 'Foundation store returned a different constraint revision than requested', 500, {
|
|
43
|
+
id: input.record.id,
|
|
44
|
+
requestedRevision: input.record.revision,
|
|
45
|
+
storedRevision: reference.revision,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
const readback = await this.get({ id: reference.id, revision: reference.revision }, context);
|
|
49
|
+
if (!readback) {
|
|
50
|
+
throw new ConstraintError('constraint_store_corrupt', 'Foundation constraint readback was empty', 500, {
|
|
51
|
+
id: reference.id,
|
|
52
|
+
revision: reference.revision,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
return readback;
|
|
56
|
+
}
|
|
57
|
+
async get(ref, context) {
|
|
58
|
+
const storeContext = toFoundationContext(context);
|
|
59
|
+
try {
|
|
60
|
+
const record = await this.foundationStore.read({ id: ref.id, type: 'constraint', revision: ref.revision }, storeContext);
|
|
61
|
+
return record?.definition.type === 'constraint' ? record.definition : null;
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
throw normalizeFoundationError(error, ref.id);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
async getLatest(id, context) {
|
|
68
|
+
const storeContext = toFoundationContext(context);
|
|
69
|
+
try {
|
|
70
|
+
const record = await this.foundationStore.readLatest('constraint', id, storeContext);
|
|
71
|
+
return record?.definition.type === 'constraint' ? record.definition : null;
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
throw normalizeFoundationError(error, id);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
async list(query, context) {
|
|
78
|
+
const storeContext = toFoundationContext(context);
|
|
79
|
+
try {
|
|
80
|
+
if (query.revision !== undefined && query.id !== undefined) {
|
|
81
|
+
const exact = await this.get({ id: query.id, revision: query.revision }, context);
|
|
82
|
+
return exact && (query.ownerId === undefined || exact.acl.ownerId === query.ownerId) ? [exact] : [];
|
|
83
|
+
}
|
|
84
|
+
const records = await this.foundationStore.list('constraint', storeContext);
|
|
85
|
+
return records
|
|
86
|
+
.map((record) => record.definition)
|
|
87
|
+
.filter((definition) => definition.type === 'constraint')
|
|
88
|
+
.filter((definition) => query.id === undefined || definition.id === query.id)
|
|
89
|
+
.filter((definition) => query.ownerId === undefined || definition.acl.ownerId === query.ownerId)
|
|
90
|
+
.map((definition) => definition);
|
|
91
|
+
}
|
|
92
|
+
catch (error) {
|
|
93
|
+
throw normalizeFoundationError(error);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
async appendException(exception, context) {
|
|
97
|
+
const resolvedContext = context ?? missingContext();
|
|
98
|
+
try {
|
|
99
|
+
validateConstraintExceptionRecord(exception);
|
|
100
|
+
await this.authorizeExceptionTarget(exception.constraintRef.id, exception.constraintRef.revision, resolvedContext, 'write');
|
|
101
|
+
return await this.exceptionStore.append(exception, resolvedContext);
|
|
102
|
+
}
|
|
103
|
+
catch (error) {
|
|
104
|
+
throw normalizeFoundationError(error, exceptionConstraintId(exception));
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
async listExceptions(query, context) {
|
|
108
|
+
const resolvedContext = context ?? missingContext();
|
|
109
|
+
try {
|
|
110
|
+
if (query.constraintId !== undefined) {
|
|
111
|
+
await this.authorizeExceptionTarget(query.constraintId, query.constraintRevision, resolvedContext, 'read');
|
|
112
|
+
}
|
|
113
|
+
const exceptions = await this.exceptionStore.list(query, resolvedContext);
|
|
114
|
+
for (const exception of exceptions) {
|
|
115
|
+
validateConstraintExceptionRecord(exception);
|
|
116
|
+
await this.authorizeExceptionTarget(exception.constraintRef.id, exception.constraintRef.revision, resolvedContext, 'read');
|
|
117
|
+
}
|
|
118
|
+
return exceptions;
|
|
119
|
+
}
|
|
120
|
+
catch (error) {
|
|
121
|
+
throw normalizeFoundationError(error, query.constraintId);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* The injected exception boundary is intentionally persistence-only and may
|
|
126
|
+
* not enforce Foundation ACLs itself. Resolve the referenced Constraint at
|
|
127
|
+
* this adapter boundary before allowing exception evidence to cross it.
|
|
128
|
+
* Current ACL controls access even when an exception points at an older
|
|
129
|
+
* revision; the exact revision is still read to reject dangling references.
|
|
130
|
+
*/
|
|
131
|
+
async authorizeExceptionTarget(constraintId, constraintRevision, context, access) {
|
|
132
|
+
const storeContext = toFoundationContext(context);
|
|
133
|
+
const current = await this.foundationStore.readLatest('constraint', constraintId, storeContext);
|
|
134
|
+
if (!current || current.definition.type !== 'constraint') {
|
|
135
|
+
throw new ConstraintError('constraint_not_found', 'exception target constraint was not found', 404, { id: constraintId });
|
|
136
|
+
}
|
|
137
|
+
if (constraintRevision !== undefined) {
|
|
138
|
+
const referenced = await this.foundationStore.read({
|
|
139
|
+
id: constraintId,
|
|
140
|
+
type: 'constraint',
|
|
141
|
+
revision: constraintRevision,
|
|
142
|
+
}, storeContext);
|
|
143
|
+
if (!referenced || referenced.definition.type !== 'constraint') {
|
|
144
|
+
throw new ConstraintError('constraint_not_found', 'exception target constraint revision was not found', 404, { id: constraintId, revision: constraintRevision });
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
const target = current.definition;
|
|
148
|
+
if (target.acl.ownerId !== context.ownerId) {
|
|
149
|
+
throw new ConstraintError('scope_violation', 'exception target constraint is outside the authorization context', 403, { id: target.id, ownerId: target.acl.ownerId });
|
|
150
|
+
}
|
|
151
|
+
const canRead = target.acl.visibility === 'public'
|
|
152
|
+
|| target.acl.ownerId === context.actorId
|
|
153
|
+
|| target.acl.readerIds.includes(context.actorId)
|
|
154
|
+
|| target.acl.writerIds.includes(context.actorId);
|
|
155
|
+
if (!canRead) {
|
|
156
|
+
throw new ConstraintError('authorization_denied', 'actor cannot read the exception target constraint', 403, { id: target.id });
|
|
157
|
+
}
|
|
158
|
+
if (access === 'write') {
|
|
159
|
+
const canWrite = target.acl.ownerId === context.actorId || target.acl.writerIds.includes(context.actorId);
|
|
160
|
+
if (!canWrite) {
|
|
161
|
+
throw new ConstraintError('authorization_denied', 'actor cannot write exceptions for the target constraint', 403, { id: target.id });
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
function exceptionConstraintId(exception) {
|
|
167
|
+
if (!exception || typeof exception !== 'object')
|
|
168
|
+
return undefined;
|
|
169
|
+
const reference = exception.constraintRef;
|
|
170
|
+
if (!reference || typeof reference !== 'object')
|
|
171
|
+
return undefined;
|
|
172
|
+
const id = reference.id;
|
|
173
|
+
return typeof id === 'string' ? id : undefined;
|
|
174
|
+
}
|
|
175
|
+
export function createFoundationConstraintStore(options) {
|
|
176
|
+
return new FoundationConstraintStore(options);
|
|
177
|
+
}
|
|
178
|
+
function toFoundationContext(context) {
|
|
179
|
+
// `ownerId` identifies the target scope. It is caller supplied request
|
|
180
|
+
// data and must never be promoted to an authenticated principal. The
|
|
181
|
+
// principal is the actor identity established by the trusted auth boundary.
|
|
182
|
+
if (!context?.actorId)
|
|
183
|
+
return missingContext();
|
|
184
|
+
return { principal: context.actorId };
|
|
185
|
+
}
|
|
186
|
+
function missingContext() {
|
|
187
|
+
throw new ConstraintError('authorization_denied', 'Foundation constraint persistence requires an authenticated actor context', 403);
|
|
188
|
+
}
|
|
189
|
+
function normalizeFoundationError(error, id) {
|
|
190
|
+
if (error instanceof ConstraintError)
|
|
191
|
+
return error;
|
|
192
|
+
if (error instanceof FoundationStoreError) {
|
|
193
|
+
switch (error.code) {
|
|
194
|
+
case 'invalid_input':
|
|
195
|
+
return new ConstraintError('invalid_constraint', error.message, 400, { id });
|
|
196
|
+
case 'not_found':
|
|
197
|
+
return new ConstraintError('constraint_not_found', error.message, 404, { id });
|
|
198
|
+
case 'revision_conflict':
|
|
199
|
+
return new ConstraintError('revision_conflict', error.message, 409, {
|
|
200
|
+
id,
|
|
201
|
+
currentRevision: error.currentRevision,
|
|
202
|
+
});
|
|
203
|
+
case 'authorization_denied':
|
|
204
|
+
return new ConstraintError('authorization_denied', error.message, 403, { id });
|
|
205
|
+
case 'scope_violation':
|
|
206
|
+
return new ConstraintError('scope_violation', error.message, 403, { id });
|
|
207
|
+
case 'corrupt_catalog':
|
|
208
|
+
case 'readback_mismatch':
|
|
209
|
+
return new ConstraintError('constraint_store_corrupt', error.message, 500, { id });
|
|
210
|
+
case 'unsupported_graph':
|
|
211
|
+
return new ConstraintError('constraint_store_unavailable', error.message, 503, { id });
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
return new ConstraintError('constraint_store_unavailable', 'Foundation constraint store failed', 503, { id });
|
|
215
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { type FoundationHttpCsrfVerifier, type FoundationHttpRouter } from './foundation-http.js';
|
|
2
|
+
/**
|
|
3
|
+
* Readiness probe for the Graph history contract used by the public adapter.
|
|
4
|
+
*
|
|
5
|
+
* The query is deliberately kept in the reusable adapter. Hosts provide the
|
|
6
|
+
* transaction-bound query port, while the migration remains the authority for
|
|
7
|
+
* RLS and immutable-history trigger state.
|
|
8
|
+
*/
|
|
9
|
+
export declare const FOUNDATION_GRAPH_READY_SQL = "SELECT\n EXISTS (SELECT 1 FROM pg_class WHERE oid = to_regclass('public.graph_foundation_revisions') AND relrowsecurity)\n AND EXISTS (SELECT 1 FROM pg_trigger WHERE tgrelid = to_regclass('public.graph_entities')\n AND tgname = 'graph_entities_foundation_history_capture' AND tgenabled = 'O' AND tgtype = 21\n AND tgfoid = to_regprocedure('public.capture_graph_foundation_revision()'))\n AND EXISTS (SELECT 1 FROM pg_trigger WHERE tgrelid = to_regclass('public.graph_foundation_revisions')\n AND tgname = 'graph_foundation_revisions_guard' AND tgenabled = 'O' AND tgtype = 31\n AND tgfoid = to_regprocedure('public.guard_graph_foundation_revision_mutation()'))\n AND EXISTS (SELECT 1 FROM pg_trigger WHERE tgrelid = to_regclass('public.graph_foundation_revisions')\n AND tgname = 'graph_foundation_revisions_truncate_guard' AND tgenabled = 'O' AND tgtype = 34\n AND tgfoid = to_regprocedure('public.guard_graph_foundation_revision_mutation()')) AS ready";
|
|
10
|
+
export declare const FOUNDATION_GRAPH_CONTRACT_VERSION: "foundation-graph-http.v1";
|
|
11
|
+
type MaybePromise<T> = T | Promise<T>;
|
|
12
|
+
export interface FoundationGraphTrustedIdentity {
|
|
13
|
+
/** Canonical principal supplied by the host authentication boundary. */
|
|
14
|
+
readonly principal: string;
|
|
15
|
+
/** Canonical organization scope supplied by the host authentication boundary. */
|
|
16
|
+
readonly organizationId: string;
|
|
17
|
+
/** Project scopes the authenticated principal may select. `*` is supported by hosts. */
|
|
18
|
+
readonly projectScopeIds: readonly string[];
|
|
19
|
+
}
|
|
20
|
+
export interface FoundationGraphQueryResult {
|
|
21
|
+
readonly rows: readonly Record<string, unknown>[];
|
|
22
|
+
}
|
|
23
|
+
export interface FoundationGraphQueryClient {
|
|
24
|
+
query(text: string, values?: readonly unknown[]): FoundationGraphQueryResult | Promise<FoundationGraphQueryResult>;
|
|
25
|
+
}
|
|
26
|
+
export interface FoundationGraphHttpOptions {
|
|
27
|
+
/** Resolve only trusted identity; request body and query values are never used for identity. */
|
|
28
|
+
readonly resolveTrustedIdentity: (request: Request) => MaybePromise<FoundationGraphTrustedIdentity | null>;
|
|
29
|
+
/** Run the shared provider inside the host's authenticated transaction/context. */
|
|
30
|
+
readonly withAccessContext: <T>(identity: FoundationGraphTrustedIdentity, callback: (client: FoundationGraphQueryClient) => Promise<T>) => Promise<T>;
|
|
31
|
+
/** Host-owned CSRF verification port for mutation requests. */
|
|
32
|
+
readonly csrf?: FoundationHttpCsrfVerifier;
|
|
33
|
+
readonly bodyLimitBytes?: number;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Create the reusable Graph-backed public Foundation handler.
|
|
37
|
+
*
|
|
38
|
+
* This is a native Fetch-style adapter. It has no Express, authentication, or
|
|
39
|
+
* organization dependency: a host injects a trusted identity resolver, an
|
|
40
|
+
* already-authenticated transaction port, and its CSRF verifier.
|
|
41
|
+
*/
|
|
42
|
+
export declare function createFoundationGraphHttpHandler(options: FoundationGraphHttpOptions): FoundationHttpRouter;
|
|
43
|
+
export {};
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import { FOUNDATION_HTTP_CONTRACT_VERSION, createFoundationHttpRouter } from './foundation-http.js';
|
|
2
|
+
import { createFoundationPublicProvider, createFoundationPublicRoute } from './foundation-public-provider.js';
|
|
3
|
+
import { createGraphFoundationReaders } from './graph-foundation-reader.js';
|
|
4
|
+
/**
|
|
5
|
+
* Readiness probe for the Graph history contract used by the public adapter.
|
|
6
|
+
*
|
|
7
|
+
* The query is deliberately kept in the reusable adapter. Hosts provide the
|
|
8
|
+
* transaction-bound query port, while the migration remains the authority for
|
|
9
|
+
* RLS and immutable-history trigger state.
|
|
10
|
+
*/
|
|
11
|
+
export const FOUNDATION_GRAPH_READY_SQL = `SELECT
|
|
12
|
+
EXISTS (SELECT 1 FROM pg_class WHERE oid = to_regclass('public.graph_foundation_revisions') AND relrowsecurity)
|
|
13
|
+
AND EXISTS (SELECT 1 FROM pg_trigger WHERE tgrelid = to_regclass('public.graph_entities')
|
|
14
|
+
AND tgname = 'graph_entities_foundation_history_capture' AND tgenabled = 'O' AND tgtype = 21
|
|
15
|
+
AND tgfoid = to_regprocedure('public.capture_graph_foundation_revision()'))
|
|
16
|
+
AND EXISTS (SELECT 1 FROM pg_trigger WHERE tgrelid = to_regclass('public.graph_foundation_revisions')
|
|
17
|
+
AND tgname = 'graph_foundation_revisions_guard' AND tgenabled = 'O' AND tgtype = 31
|
|
18
|
+
AND tgfoid = to_regprocedure('public.guard_graph_foundation_revision_mutation()'))
|
|
19
|
+
AND EXISTS (SELECT 1 FROM pg_trigger WHERE tgrelid = to_regclass('public.graph_foundation_revisions')
|
|
20
|
+
AND tgname = 'graph_foundation_revisions_truncate_guard' AND tgenabled = 'O' AND tgtype = 34
|
|
21
|
+
AND tgfoid = to_regprocedure('public.guard_graph_foundation_revision_mutation()')) AS ready`;
|
|
22
|
+
export const FOUNDATION_GRAPH_CONTRACT_VERSION = 'foundation-graph-http.v1';
|
|
23
|
+
const DEFAULT_BODY_LIMIT_BYTES = 65_536;
|
|
24
|
+
const DIRECT_FOUNDATION_REFERENCE_KINDS = new Set(['objective', 'variable', 'model', 'constraint', 'philosophy']);
|
|
25
|
+
const SNAPSHOT_REFERENCE_KINDS = new Set([
|
|
26
|
+
'entity', 'edge', 'objective', 'criterion', 'variable', 'observation', 'model', 'constraint',
|
|
27
|
+
'philosophy', 'authority', 'resource', 'deadline', 'dag', 'evidence'
|
|
28
|
+
]);
|
|
29
|
+
const FOUNDATION_SCOPE_TYPES = new Set(['personal', 'project', 'organization']);
|
|
30
|
+
function jsonResponse(status, value) {
|
|
31
|
+
return new Response(JSON.stringify(value), {
|
|
32
|
+
status,
|
|
33
|
+
headers: { 'content-type': 'application/json' }
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
function isRecord(value) {
|
|
37
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Return true only for the canonical scope shape. Incomplete or extended
|
|
41
|
+
* shapes stay on the shared validator path so it remains the source of
|
|
42
|
+
* malformed-input errors.
|
|
43
|
+
*/
|
|
44
|
+
function isCanonicalFoundationScope(value) {
|
|
45
|
+
return isRecord(value)
|
|
46
|
+
&& Object.keys(value).length === 2
|
|
47
|
+
&& typeof value.type === 'string'
|
|
48
|
+
&& typeof value.id === 'string'
|
|
49
|
+
&& value.type.length > 0
|
|
50
|
+
&& value.id.length > 0
|
|
51
|
+
&& value.type === value.type.trim()
|
|
52
|
+
&& value.id === value.id.trim();
|
|
53
|
+
}
|
|
54
|
+
function isSelectedProjectScope(value, scopeId) {
|
|
55
|
+
return isCanonicalFoundationScope(value) && value.type === 'project' && value.id === scopeId;
|
|
56
|
+
}
|
|
57
|
+
function isAllowedFoundationReference(reference, scopeId, organizationId, knownKinds) {
|
|
58
|
+
if (!isRecord(reference) || !isCanonicalFoundationScope(reference.scope))
|
|
59
|
+
return true;
|
|
60
|
+
// Let the common validator report a missing/non-string kind as malformed;
|
|
61
|
+
// every present non-philosophy kind remains project-scoped.
|
|
62
|
+
if (typeof reference.kind !== 'string' || reference.kind.length === 0 || reference.kind !== reference.kind.trim())
|
|
63
|
+
return true;
|
|
64
|
+
// Unknown kinds and scope types remain the shared validator's responsibility.
|
|
65
|
+
// This boundary only narrows otherwise valid references.
|
|
66
|
+
if (!knownKinds.has(reference.kind) || !FOUNDATION_SCOPE_TYPES.has(reference.scope.type))
|
|
67
|
+
return true;
|
|
68
|
+
return isSelectedProjectScope(reference.scope, scopeId)
|
|
69
|
+
|| (reference.kind === 'philosophy'
|
|
70
|
+
&& reference.scope.type === 'organization'
|
|
71
|
+
&& reference.scope.id === organizationId);
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Reject scope widening before the common provider sees a request. The
|
|
75
|
+
* provider receives both the selected project and trusted organization so an
|
|
76
|
+
* organization philosophy can resolve, but all other foundation references
|
|
77
|
+
* remain project-scoped and measurements remain project-only.
|
|
78
|
+
*/
|
|
79
|
+
function hasFoundationScopeOverride(request, body, scopeId, organizationId) {
|
|
80
|
+
const method = (request.method || 'GET').toUpperCase();
|
|
81
|
+
if (!['POST', 'PUT', 'PATCH'].includes(method) || !isRecord(body))
|
|
82
|
+
return false;
|
|
83
|
+
const pathname = new URL(request.url).pathname;
|
|
84
|
+
if (pathname.endsWith('/judgment-references/validate')) {
|
|
85
|
+
return isRecord(body.reference)
|
|
86
|
+
&& !isAllowedFoundationReference(body.reference, scopeId, organizationId, DIRECT_FOUNDATION_REFERENCE_KINDS);
|
|
87
|
+
}
|
|
88
|
+
if (!pathname.endsWith('/judgment-problems/validate'))
|
|
89
|
+
return false;
|
|
90
|
+
const snapshot = body.snapshot;
|
|
91
|
+
if (!isRecord(snapshot))
|
|
92
|
+
return false;
|
|
93
|
+
if (Object.hasOwn(snapshot, 'owner_scope')
|
|
94
|
+
&& isCanonicalFoundationScope(snapshot.owner_scope)
|
|
95
|
+
&& FOUNDATION_SCOPE_TYPES.has(snapshot.owner_scope.type)
|
|
96
|
+
&& !isSelectedProjectScope(snapshot.owner_scope, scopeId)) {
|
|
97
|
+
return true;
|
|
98
|
+
}
|
|
99
|
+
if (!Array.isArray(snapshot.references))
|
|
100
|
+
return false;
|
|
101
|
+
return snapshot.references.some((reference) => {
|
|
102
|
+
if (!isRecord(reference))
|
|
103
|
+
return false;
|
|
104
|
+
if (Object.hasOwn(reference, 'scope')
|
|
105
|
+
&& !isAllowedFoundationReference(reference, scopeId, organizationId, SNAPSHOT_REFERENCE_KINDS)) {
|
|
106
|
+
return true;
|
|
107
|
+
}
|
|
108
|
+
const measurement = reference.measurement;
|
|
109
|
+
const measurementScope = isRecord(measurement) ? measurement.scope : undefined;
|
|
110
|
+
const subjectIds = isRecord(measurementScope) ? measurementScope.subjectIds : undefined;
|
|
111
|
+
// Incomplete subjectIds remain the shared validator's responsibility.
|
|
112
|
+
return Array.isArray(subjectIds)
|
|
113
|
+
&& subjectIds.length > 0
|
|
114
|
+
&& subjectIds.every((subjectId) => typeof subjectId === 'string'
|
|
115
|
+
&& subjectId === subjectId.trim()
|
|
116
|
+
&& subjectId.length > 0)
|
|
117
|
+
&& subjectIds.some((subjectId) => subjectId !== scopeId);
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
async function readJsonForScopeCheck(request, bodyLimitBytes) {
|
|
121
|
+
const method = (request.method || 'GET').toUpperCase();
|
|
122
|
+
const pathname = new URL(request.url).pathname;
|
|
123
|
+
if (!['POST', 'PUT', 'PATCH'].includes(method)
|
|
124
|
+
|| (!pathname.endsWith('/judgment-references/validate') && !pathname.endsWith('/judgment-problems/validate'))) {
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
try {
|
|
128
|
+
const body = await request.clone().text();
|
|
129
|
+
if (new TextEncoder().encode(body).byteLength > bodyLimitBytes)
|
|
130
|
+
return null;
|
|
131
|
+
return JSON.parse(body);
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
// The shared route owns malformed JSON errors. A failed preflight must not
|
|
135
|
+
// turn malformed input into an authorization decision.
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
function identityIsValid(identity) {
|
|
140
|
+
if (!identity)
|
|
141
|
+
return false;
|
|
142
|
+
return typeof identity.principal === 'string'
|
|
143
|
+
&& identity.principal.trim().length > 0
|
|
144
|
+
&& typeof identity.organizationId === 'string'
|
|
145
|
+
&& identity.organizationId.trim().length > 0
|
|
146
|
+
&& Array.isArray(identity.projectScopeIds);
|
|
147
|
+
}
|
|
148
|
+
function selectedScopeIsAllowed(identity, scopeId) {
|
|
149
|
+
return identity.projectScopeIds.includes('*') || identity.projectScopeIds.includes(scopeId);
|
|
150
|
+
}
|
|
151
|
+
function readyResult(result) {
|
|
152
|
+
return result.rows[0]?.ready === true;
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Create the reusable Graph-backed public Foundation handler.
|
|
156
|
+
*
|
|
157
|
+
* This is a native Fetch-style adapter. It has no Express, authentication, or
|
|
158
|
+
* organization dependency: a host injects a trusted identity resolver, an
|
|
159
|
+
* already-authenticated transaction port, and its CSRF verifier.
|
|
160
|
+
*/
|
|
161
|
+
export function createFoundationGraphHttpHandler(options) {
|
|
162
|
+
if (!options || typeof options.resolveTrustedIdentity !== 'function') {
|
|
163
|
+
throw new TypeError('resolveTrustedIdentity is required');
|
|
164
|
+
}
|
|
165
|
+
if (typeof options.withAccessContext !== 'function') {
|
|
166
|
+
throw new TypeError('withAccessContext is required');
|
|
167
|
+
}
|
|
168
|
+
const bodyLimitBytes = options.bodyLimitBytes ?? DEFAULT_BODY_LIMIT_BYTES;
|
|
169
|
+
if (!Number.isInteger(bodyLimitBytes) || bodyLimitBytes <= 0) {
|
|
170
|
+
throw new TypeError('bodyLimitBytes must be a positive integer');
|
|
171
|
+
}
|
|
172
|
+
return {
|
|
173
|
+
contractVersion: FOUNDATION_HTTP_CONTRACT_VERSION,
|
|
174
|
+
routes: [],
|
|
175
|
+
async handle(request) {
|
|
176
|
+
let identity;
|
|
177
|
+
try {
|
|
178
|
+
identity = await options.resolveTrustedIdentity(request);
|
|
179
|
+
}
|
|
180
|
+
catch {
|
|
181
|
+
identity = null;
|
|
182
|
+
}
|
|
183
|
+
if (!identityIsValid(identity)) {
|
|
184
|
+
return jsonResponse(403, { error: { code: 'foundation_trusted_context_required' } });
|
|
185
|
+
}
|
|
186
|
+
const url = new URL(request.url);
|
|
187
|
+
const scopeIds = url.searchParams.getAll('scope_id');
|
|
188
|
+
if (scopeIds.length !== 1 || !scopeIds[0]?.trim() || scopeIds[0] !== scopeIds[0].trim()) {
|
|
189
|
+
return jsonResponse(400, { error: { code: 'scope_id_required' } });
|
|
190
|
+
}
|
|
191
|
+
const scopeId = scopeIds[0];
|
|
192
|
+
if (!selectedScopeIsAllowed(identity, scopeId)) {
|
|
193
|
+
return jsonResponse(403, { error: { code: 'scope_not_allowed' } });
|
|
194
|
+
}
|
|
195
|
+
const body = await readJsonForScopeCheck(request, bodyLimitBytes);
|
|
196
|
+
if (hasFoundationScopeOverride(request, body, scopeId, identity.organizationId)) {
|
|
197
|
+
return jsonResponse(403, { error: { code: 'scope_not_allowed' } });
|
|
198
|
+
}
|
|
199
|
+
const context = {
|
|
200
|
+
principal: identity.principal,
|
|
201
|
+
scope: {
|
|
202
|
+
subjectIds: [scopeId, identity.organizationId],
|
|
203
|
+
validFrom: '1970-01-01T00:00:00Z'
|
|
204
|
+
}
|
|
205
|
+
};
|
|
206
|
+
try {
|
|
207
|
+
const response = await options.withAccessContext(identity, async (client) => {
|
|
208
|
+
let ready;
|
|
209
|
+
try {
|
|
210
|
+
ready = await client.query(FOUNDATION_GRAPH_READY_SQL);
|
|
211
|
+
}
|
|
212
|
+
catch {
|
|
213
|
+
return jsonResponse(503, { error: { code: 'foundation_provider_unavailable' } });
|
|
214
|
+
}
|
|
215
|
+
if (!readyResult(ready)) {
|
|
216
|
+
return jsonResponse(503, { error: { code: 'foundation_migration_required' } });
|
|
217
|
+
}
|
|
218
|
+
const readers = createGraphFoundationReaders({
|
|
219
|
+
context,
|
|
220
|
+
query: async (text, values) => ({
|
|
221
|
+
rows: (await client.query(text, values ?? [])).rows
|
|
222
|
+
})
|
|
223
|
+
});
|
|
224
|
+
const provider = createFoundationPublicProvider(readers);
|
|
225
|
+
const route = createFoundationPublicRoute(provider);
|
|
226
|
+
const handler = createFoundationHttpRouter({
|
|
227
|
+
resolveContext: () => context,
|
|
228
|
+
csrf: options.csrf,
|
|
229
|
+
routes: [route],
|
|
230
|
+
bodyLimitBytes
|
|
231
|
+
});
|
|
232
|
+
return handler.handle(request);
|
|
233
|
+
});
|
|
234
|
+
return response instanceof Response
|
|
235
|
+
? response
|
|
236
|
+
: jsonResponse(503, { error: { code: 'foundation_provider_unavailable' } });
|
|
237
|
+
}
|
|
238
|
+
catch {
|
|
239
|
+
return jsonResponse(503, { error: { code: 'foundation_provider_unavailable' } });
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
};
|
|
243
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import type { FoundationDefinition, FoundationScope, FoundationType } from './ontology-foundation.js';
|
|
2
|
+
/**
|
|
3
|
+
* Contract for preparing a Foundation draft for the canonical Graph writer.
|
|
4
|
+
*
|
|
5
|
+
* This module deliberately stops at a pure, fail-closed validation boundary.
|
|
6
|
+
* It does not issue Graph queries, mutate a catalog, or infer a tenant from
|
|
7
|
+
* any value in the payload. The host must provide the already-authenticated
|
|
8
|
+
* principal, selected project, and current revision.
|
|
9
|
+
*/
|
|
10
|
+
export declare const FOUNDATION_GRAPH_WRITE_CONTRACT_VERSION: "foundation-graph-draft-write.v1";
|
|
11
|
+
export type FoundationGraphWriteOperation = 'create' | 'update';
|
|
12
|
+
export type FoundationGraphWriteIssueCode = 'INVALID_INPUT' | 'INVALID_IDENTITY' | 'ROW_DEFINITION_MISMATCH' | 'REVISION_CONFLICT' | 'CURRENT_DEFINITION_REQUIRED' | 'DEFINITION_INVALID' | 'DRAFT_ADOPTION_REQUIRED' | 'CANDIDATE_STORAGE_REQUIRED' | 'DRAFT_AUTHORIZATION_REQUIRED' | 'PROVENANCE_REQUIRED' | 'OWNER_MISMATCH' | 'ACL_CHANGE_FORBIDDEN' | 'WRITER_NOT_AUTHORIZED' | 'SCOPE_VIOLATION' | 'PROJECT_MISMATCH';
|
|
13
|
+
export interface FoundationGraphWriteIssue {
|
|
14
|
+
readonly code: FoundationGraphWriteIssueCode;
|
|
15
|
+
readonly path: string;
|
|
16
|
+
readonly message: string;
|
|
17
|
+
}
|
|
18
|
+
/** Trusted context resolved before this pure contract is called. */
|
|
19
|
+
export interface FoundationGraphWriteContext {
|
|
20
|
+
/** Host auth boundary value; callers should pass `access.personId`. */
|
|
21
|
+
readonly principal: string;
|
|
22
|
+
/** Selected canonical project code, never taken from the candidate payload. */
|
|
23
|
+
readonly projectCode: string;
|
|
24
|
+
/** Optional trusted subject/time scope for the selected project. */
|
|
25
|
+
readonly scope?: FoundationScope;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* The host's Graph row shape. `type` is the canonical API name; `entityType`
|
|
29
|
+
* is accepted as a compatibility alias for adapters that mirror SQL rows.
|
|
30
|
+
*/
|
|
31
|
+
export interface FoundationGraphWriteRow {
|
|
32
|
+
readonly id: string;
|
|
33
|
+
readonly type?: string;
|
|
34
|
+
readonly entityType?: string;
|
|
35
|
+
readonly revision: string;
|
|
36
|
+
readonly projectCode: string;
|
|
37
|
+
readonly payload: {
|
|
38
|
+
readonly foundation: unknown;
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
export interface FoundationGraphWriteInput {
|
|
42
|
+
readonly context: FoundationGraphWriteContext;
|
|
43
|
+
readonly row: FoundationGraphWriteRow;
|
|
44
|
+
readonly expectedNextRevision: string;
|
|
45
|
+
readonly currentDefinition?: unknown;
|
|
46
|
+
}
|
|
47
|
+
export interface FoundationGraphWriteResult {
|
|
48
|
+
readonly contractVersion: typeof FOUNDATION_GRAPH_WRITE_CONTRACT_VERSION;
|
|
49
|
+
readonly operation: FoundationGraphWriteOperation;
|
|
50
|
+
readonly projectCode: string;
|
|
51
|
+
readonly row: {
|
|
52
|
+
readonly id: string;
|
|
53
|
+
readonly type: FoundationType;
|
|
54
|
+
readonly revision: string;
|
|
55
|
+
readonly projectCode: string;
|
|
56
|
+
readonly payload: {
|
|
57
|
+
readonly foundation: FoundationDefinition;
|
|
58
|
+
};
|
|
59
|
+
};
|
|
60
|
+
readonly definition: FoundationDefinition;
|
|
61
|
+
}
|
|
62
|
+
export interface FoundationGraphWriteValidationResult {
|
|
63
|
+
readonly valid: boolean;
|
|
64
|
+
readonly status: 'valid' | 'invalid';
|
|
65
|
+
readonly operation?: FoundationGraphWriteOperation;
|
|
66
|
+
readonly issues: readonly FoundationGraphWriteIssue[];
|
|
67
|
+
readonly normalized?: FoundationGraphWriteResult;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Error thrown by the throwing normalizer. The non-throwing validator should
|
|
71
|
+
* be used at HTTP/MCP boundaries when callers need to render all issues.
|
|
72
|
+
*/
|
|
73
|
+
export declare class FoundationGraphWriteError extends Error {
|
|
74
|
+
readonly code: FoundationGraphWriteIssueCode;
|
|
75
|
+
readonly issues: readonly FoundationGraphWriteIssue[];
|
|
76
|
+
constructor(issues: readonly FoundationGraphWriteIssue[]);
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Validate and normalize an untrusted Graph upsert candidate.
|
|
80
|
+
*
|
|
81
|
+
* A successful result is safe to hand to a canonical Graph writer only after
|
|
82
|
+
* that writer also enforces its transaction/RLS boundary. This function proves
|
|
83
|
+
* the payload contract, identity, current ACL authorization, scope, and
|
|
84
|
+
* revision identity without performing any I/O.
|
|
85
|
+
*/
|
|
86
|
+
export declare function validateFoundationGraphWrite(input: unknown): FoundationGraphWriteValidationResult;
|
|
87
|
+
/** Normalize a valid candidate or throw with the complete validation issues. */
|
|
88
|
+
export declare function normalizeFoundationGraphWrite(input: unknown): FoundationGraphWriteResult;
|
|
89
|
+
/** Descriptive alias for callers that use the validation/normalization pair. */
|
|
90
|
+
export declare const validateAndNormalizeFoundationGraphWrite: typeof normalizeFoundationGraphWrite;
|