fuli-context 0.2.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/LICENSE +201 -0
- package/README.md +178 -0
- package/compose.graphiti.yml +86 -0
- package/dist/web/assets/ConnectionsPage-CY6H_mJJ.js +1 -0
- package/dist/web/assets/KnowledgeInspector-BSpz5Sfm.js +1 -0
- package/dist/web/assets/KnowledgeInspector-CHbB_0rC.css +1 -0
- package/dist/web/assets/KnowledgeOrganizerPage-BlEvhPrv.css +1 -0
- package/dist/web/assets/KnowledgeOrganizerPage-CvjGPlUc.js +1 -0
- package/dist/web/assets/KnowledgePage-Khuo3cNs.js +1 -0
- package/dist/web/assets/KnowledgeWorkspace-DVZusvem.js +4 -0
- package/dist/web/assets/OverviewPage-DC4C4Fhn.js +1 -0
- package/dist/web/assets/PersonalProfilePage-CSV9Mtgz.js +1 -0
- package/dist/web/assets/PersonalProfilePage-CVJ3TcRi.css +1 -0
- package/dist/web/assets/PersonalProjectsPage-A50EaseQ.js +1 -0
- package/dist/web/assets/PublicProjectsPage-BAdO_bae.js +1 -0
- package/dist/web/assets/ReviewPage-Dztk2jDG.js +1 -0
- package/dist/web/assets/console-tbvTUi6p.js +1 -0
- package/dist/web/assets/fuli-logo-DGuyqchQ.png +0 -0
- package/dist/web/assets/identity-B3s5CXhv.js +1 -0
- package/dist/web/assets/index-Devf7glZ.css +1 -0
- package/dist/web/assets/index-DwUDhQKc.js +6 -0
- package/dist/web/assets/model-DVT8sjy9.js +1 -0
- package/dist/web/assets/paths-B790iniO.js +1 -0
- package/dist/web/assets/runtime-dom.esm-bundler-DttsK21F.js +1 -0
- package/dist/web/assets/vue-router-BYTAoWT3.js +1 -0
- package/dist/web/index.html +22 -0
- package/graph-provider/Dockerfile +16 -0
- package/graph-provider/fuli_graph/__init__.py +5 -0
- package/graph-provider/fuli_graph/app.py +547 -0
- package/graph-provider/fuli_graph/auth.py +17 -0
- package/graph-provider/fuli_graph/collaboration_context.py +290 -0
- package/graph-provider/fuli_graph/config.py +34 -0
- package/graph-provider/fuli_graph/graph_models.py +123 -0
- package/graph-provider/fuli_graph/graph_projection.py +405 -0
- package/graph-provider/fuli_graph/graph_query.py +561 -0
- package/graph-provider/fuli_graph/knowledge_audit.py +765 -0
- package/graph-provider/fuli_graph/knowledge_batch_confirmation.py +246 -0
- package/graph-provider/fuli_graph/knowledge_management.py +816 -0
- package/graph-provider/fuli_graph/knowledge_records.py +65 -0
- package/graph-provider/fuli_graph/knowledge_search.py +723 -0
- package/graph-provider/fuli_graph/knowledge_usage_models.py +52 -0
- package/graph-provider/fuli_graph/model_validation.py +58 -0
- package/graph-provider/fuli_graph/models.py +1179 -0
- package/graph-provider/fuli_graph/personal_project_access.py +29 -0
- package/graph-provider/fuli_graph/preference_conflicts.py +420 -0
- package/graph-provider/fuli_graph/project_action_models.py +142 -0
- package/graph-provider/fuli_graph/project_knowledge.py +707 -0
- package/graph-provider/fuli_graph/project_management.py +251 -0
- package/graph-provider/fuli_graph/provider_values.py +40 -0
- package/graph-provider/fuli_graph/runtime.py +350 -0
- package/graph-provider/fuli_graph/search_projection.py +156 -0
- package/graph-provider/fuli_graph/store.py +24 -0
- package/graph-provider/fuli_graph/store_identity.py +237 -0
- package/graph-provider/fuli_graph/store_knowledge.py +447 -0
- package/graph-provider/fuli_graph/store_projects.py +242 -0
- package/graph-provider/fuli_graph/store_publication.py +370 -0
- package/graph-provider/fuli_graph/store_records.py +134 -0
- package/graph-provider/pyproject.toml +35 -0
- package/npm-shrinkwrap.json +6045 -0
- package/package.json +99 -0
- package/skills/capturing-session-knowledge/SKILL.md +226 -0
- package/skills/capturing-session-knowledge/agents/openai.yaml +13 -0
- package/skills/grilling-project/SKILL.md +129 -0
- package/skills/grilling-project/agents/openai.yaml +4 -0
- package/skills/grilling-project/references/project-profile.md +105 -0
- package/src/agent/candidate-read-service.js +53 -0
- package/src/agent/context-pack-service.js +83 -0
- package/src/agent/public-read-policy.js +39 -0
- package/src/agent/public-read-service.js +69 -0
- package/src/agent/read-projection.js +107 -0
- package/src/agent/search-service.js +61 -0
- package/src/agent/space-boundary.js +33 -0
- package/src/agent-tools/graph-definitions.js +479 -0
- package/src/agent-tools/graph-handlers.js +96 -0
- package/src/agent-tools/schema.js +37 -0
- package/src/agent-tools.js +16 -0
- package/src/app/application-error.js +13 -0
- package/src/app/bootstrap.js +8 -0
- package/src/app/create-application.js +143 -0
- package/src/app/state-service.js +38 -0
- package/src/candidates.js +78 -0
- package/src/classifier.js +63 -0
- package/src/cli/command-arguments.js +14 -0
- package/src/cli/command-registry.js +78 -0
- package/src/cli/commands/candidates.js +17 -0
- package/src/cli/commands/ingestion.js +32 -0
- package/src/cli/commands/reads.js +50 -0
- package/src/cli/commands/spaces.js +20 -0
- package/src/cli/invocation.js +47 -0
- package/src/cli/local-runtime-command.js +94 -0
- package/src/cli/local-runtime-options.js +64 -0
- package/src/cli/migrate-command.js +71 -0
- package/src/cli/migration-destination.js +117 -0
- package/src/cli/setup-agent-selection.js +195 -0
- package/src/cli/setup-command.js +85 -0
- package/src/cli/uninstall-command.js +67 -0
- package/src/cli.js +90 -0
- package/src/context-pack.js +206 -0
- package/src/defaults.js +1 -0
- package/src/extractor.js +77 -0
- package/src/fact-history.js +39 -0
- package/src/facts/fact-service.js +89 -0
- package/src/graphiti/agent-access-policy.js +88 -0
- package/src/graphiti/capture-policy.js +84 -0
- package/src/graphiti/federated-application.js +1292 -0
- package/src/graphiti/personal-context.js +29 -0
- package/src/graphiti/project-path-context.js +159 -0
- package/src/graphiti/provider-client.js +288 -0
- package/src/graphiti/runtime-config.js +85 -0
- package/src/graphiti/setup.js +43 -0
- package/src/graphiti/source-marker.js +142 -0
- package/src/http/api-router.js +133 -0
- package/src/http/bounded-json-array.js +19 -0
- package/src/http/error-mapping.js +36 -0
- package/src/http/graph-api-router.js +257 -0
- package/src/http/lens-route.js +50 -0
- package/src/http/request-policy.js +41 -0
- package/src/http/response.js +29 -0
- package/src/http/static-handler.js +55 -0
- package/src/ingestion.js +161 -0
- package/src/lens/lens-correction-service.js +81 -0
- package/src/lens/lens-fact-writer.js +85 -0
- package/src/lens/lens-input.js +62 -0
- package/src/lens/lens-query-projection.js +87 -0
- package/src/lens/lens-query-ranking.js +62 -0
- package/src/lens/lens-query.js +143 -0
- package/src/lens/lens-resource-projection.js +93 -0
- package/src/lens/lens-resource-selection.js +45 -0
- package/src/lens/lens-resource-service.js +103 -0
- package/src/lens/lens-search-query.js +122 -0
- package/src/lens/lens-service.js +183 -0
- package/src/local-runtime/lifecycle.js +254 -0
- package/src/mcp/create-mcp-server.js +85 -0
- package/src/mcp/instructions.js +10 -0
- package/src/mcp/project-action-preview-tokens.js +72 -0
- package/src/mcp/runtime.js +71 -0
- package/src/mcp/tool-annotations.js +63 -0
- package/src/mcp/tool-result.js +167 -0
- package/src/mcp/tool-schema.js +60 -0
- package/src/mcp-server.js +76 -0
- package/src/models.js +44 -0
- package/src/observer.js +83 -0
- package/src/package-metadata.js +6 -0
- package/src/project-rules.js +33 -0
- package/src/publication/canonical-json.js +39 -0
- package/src/publication/outbox-service.js +46 -0
- package/src/publication/publication-envelope-schema.js +75 -0
- package/src/publication/publication-envelope.js +37 -0
- package/src/publication/publication-policy.js +82 -0
- package/src/publication/publication-service.js +48 -0
- package/src/router.js +58 -0
- package/src/runtime-options.js +139 -0
- package/src/security/episode-visibility.js +14 -0
- package/src/security/fact-visibility.js +15 -0
- package/src/security/sensitive-content.js +18 -0
- package/src/security/source-metadata-policy.js +8 -0
- package/src/server/application-lifecycle.js +34 -0
- package/src/server/listen.js +24 -0
- package/src/server.js +135 -0
- package/src/setup/agent-installation-status.js +97 -0
- package/src/setup/agents.js +123 -0
- package/src/setup/codex-bootstrap.js +118 -0
- package/src/setup/codex-config.js +122 -0
- package/src/setup/config-backup.js +22 -0
- package/src/setup/container-runtime.js +388 -0
- package/src/setup/cursor-config.js +61 -0
- package/src/setup/graph-runtime.js +360 -0
- package/src/setup/node-runtime.js +24 -0
- package/src/setup/options.js +91 -0
- package/src/setup/paths.js +47 -0
- package/src/setup/session-skill.js +91 -0
- package/src/setup/setup.js +85 -0
- package/src/setup/uninstall.js +90 -0
- package/src/storage/episode-evidence-preview.js +100 -0
- package/src/storage/fact-search-order.js +18 -0
- package/src/storage/file/episode-evidence.js +37 -0
- package/src/storage/file/episode-index.js +59 -0
- package/src/storage/file/fact-search.js +60 -0
- package/src/storage/file-store.js +437 -0
- package/src/storage/import-json.js +70 -0
- package/src/storage/json-file.js +41 -0
- package/src/storage/legacy-record-validation.js +152 -0
- package/src/storage/legacy-snapshot-validation.js +32 -0
- package/src/storage/migrate.js +128 -0
- package/src/storage/migrations/001-initial.sql +83 -0
- package/src/storage/migrations/002-storage-integrity.sql +113 -0
- package/src/storage/record-validation.js +46 -0
- package/src/storage/snapshot-defaults.js +54 -0
- package/src/storage/sqlite/candidates-repository.js +84 -0
- package/src/storage/sqlite/episode-evidence-repository.js +154 -0
- package/src/storage/sqlite/episode-privacy-sql.js +33 -0
- package/src/storage/sqlite/episodes-repository.js +52 -0
- package/src/storage/sqlite/fact-search-repository.js +162 -0
- package/src/storage/sqlite/facts-repository.js +146 -0
- package/src/storage/sqlite/imports-repository.js +39 -0
- package/src/storage/sqlite/mapper.js +84 -0
- package/src/storage/sqlite/open-database.js +15 -0
- package/src/storage/sqlite/outbox-repository.js +103 -0
- package/src/storage/sqlite/snapshot-repository.js +69 -0
- package/src/storage/sqlite/spaces-repository.js +52 -0
- package/src/storage/sqlite/subscriptions-repository.js +54 -0
- package/src/storage/sqlite-store.js +159 -0
- package/src/storage/stable-json.js +24 -0
- package/src/storage/store-port.js +20 -0
- package/src/store.js +2 -0
- package/src/workspace-protocol/array-validation.js +19 -0
- package/src/workspace-protocol/base64url-contract.js +27 -0
- package/src/workspace-protocol/constants.js +37 -0
- package/src/workspace-protocol/error-contract.js +24 -0
- package/src/workspace-protocol/fact-contract.js +110 -0
- package/src/workspace-protocol/governance-contract.js +47 -0
- package/src/workspace-protocol/index.js +53 -0
- package/src/workspace-protocol/json-budget.js +19 -0
- package/src/workspace-protocol/manifest-contract.js +186 -0
- package/src/workspace-protocol/publication-contract.js +212 -0
- package/src/workspace-protocol/query-contract.js +149 -0
- package/src/workspace-protocol/scalar-contract.js +192 -0
- package/src/workspace-protocol/sync-contract.js +184 -0
- package/src/workspace-protocol/text-contract.js +39 -0
- package/src/workspace-protocol/validation.js +66 -0
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
const MAX_SOURCE_URI_LENGTH = 2_048;
|
|
4
|
+
const HTTP_SOURCE_URI_SCHEMES = new Set(['https', 'http']);
|
|
5
|
+
const CUSTOM_SOURCE_URI_SCHEMES = new Set([
|
|
6
|
+
'prd',
|
|
7
|
+
'git',
|
|
8
|
+
'github',
|
|
9
|
+
'gitlab',
|
|
10
|
+
'jira',
|
|
11
|
+
'linear',
|
|
12
|
+
'notion',
|
|
13
|
+
]);
|
|
14
|
+
const ALLOWED_SOURCE_URI_SCHEMES = new Set([
|
|
15
|
+
...HTTP_SOURCE_URI_SCHEMES,
|
|
16
|
+
...CUSTOM_SOURCE_URI_SCHEMES,
|
|
17
|
+
]);
|
|
18
|
+
|
|
19
|
+
function parseRawHierarchicalUri(value) {
|
|
20
|
+
const match = /^([A-Za-z][A-Za-z0-9+.-]*):\/\/([^/]+)(\/.*)?$/u.exec(value);
|
|
21
|
+
|
|
22
|
+
if (match === null) {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return {
|
|
27
|
+
scheme: match[1].toLowerCase(),
|
|
28
|
+
authority: match[2],
|
|
29
|
+
path: match[3] ?? '',
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function isUnsafeRawSourceUri(value, { authority }) {
|
|
34
|
+
return (
|
|
35
|
+
/\p{C}/u.test(value)
|
|
36
|
+
|| /\s/u.test(value)
|
|
37
|
+
|| value.includes('\\')
|
|
38
|
+
|| value.includes('?')
|
|
39
|
+
|| value.includes('#')
|
|
40
|
+
|| authority.includes('@')
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function isSafeCustomSourceUri({ authority, path }) {
|
|
45
|
+
return (
|
|
46
|
+
/^[\x21-\x7e]+$/u.test(authority)
|
|
47
|
+
&& /^[\x21-\x7e]*$/u.test(path)
|
|
48
|
+
&& !authority.includes('%')
|
|
49
|
+
&& !path.includes('%')
|
|
50
|
+
&& !/^[A-Za-z]:$/u.test(authority)
|
|
51
|
+
&& !/^\/[A-Za-z]:(?:\/|$)/u.test(path)
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function isSafeHttpPath(url) {
|
|
56
|
+
if (/%25/iu.test(url.pathname)) {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
let decodedPathname;
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
decodedPathname = decodeURIComponent(url.pathname);
|
|
64
|
+
} catch {
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const pathWithoutLeadingSlash = decodedPathname.startsWith('/')
|
|
69
|
+
? decodedPathname.slice(1)
|
|
70
|
+
: decodedPathname;
|
|
71
|
+
|
|
72
|
+
return (
|
|
73
|
+
!/\p{C}/u.test(decodedPathname)
|
|
74
|
+
&& !decodedPathname.includes('\\')
|
|
75
|
+
&& !/^[A-Za-z]:(?:[\\/]|$)/u.test(pathWithoutLeadingSlash)
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export const idSchema = z
|
|
80
|
+
.string()
|
|
81
|
+
.min(1)
|
|
82
|
+
.max(128)
|
|
83
|
+
.refine((value) => !/(?:\s|\p{C})/u.test(value));
|
|
84
|
+
|
|
85
|
+
export const cursorSchema = z
|
|
86
|
+
.string()
|
|
87
|
+
.min(1)
|
|
88
|
+
.max(512)
|
|
89
|
+
.refine((value) => !/(?:\s|\p{C})/u.test(value));
|
|
90
|
+
|
|
91
|
+
export const revisionSchema = z
|
|
92
|
+
.string()
|
|
93
|
+
.regex(/^(?:0|[1-9][0-9]{0,19})$/);
|
|
94
|
+
|
|
95
|
+
export const timestampSchema = z
|
|
96
|
+
.string()
|
|
97
|
+
.min(20)
|
|
98
|
+
.max(35)
|
|
99
|
+
.regex(
|
|
100
|
+
/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,3})?(?:Z|[+-]\d{2}:\d{2})$/,
|
|
101
|
+
)
|
|
102
|
+
.pipe(z.iso.datetime({ offset: true }));
|
|
103
|
+
|
|
104
|
+
export const sha256Schema = z.string().regex(/^[0-9a-f]{64}$/);
|
|
105
|
+
|
|
106
|
+
export const sourceUriSchema = z
|
|
107
|
+
.string()
|
|
108
|
+
.min(1)
|
|
109
|
+
.max(MAX_SOURCE_URI_LENGTH)
|
|
110
|
+
.transform((value, context) => {
|
|
111
|
+
const rawUri = parseRawHierarchicalUri(value);
|
|
112
|
+
|
|
113
|
+
if (
|
|
114
|
+
rawUri === null
|
|
115
|
+
|| !ALLOWED_SOURCE_URI_SCHEMES.has(rawUri.scheme)
|
|
116
|
+
|| isUnsafeRawSourceUri(value, rawUri)
|
|
117
|
+
|| (
|
|
118
|
+
CUSTOM_SOURCE_URI_SCHEMES.has(rawUri.scheme)
|
|
119
|
+
&& !isSafeCustomSourceUri(rawUri)
|
|
120
|
+
)
|
|
121
|
+
|| (
|
|
122
|
+
HTTP_SOURCE_URI_SCHEMES.has(rawUri.scheme)
|
|
123
|
+
&& /%(?![0-9A-Fa-f]{2})/u.test(value)
|
|
124
|
+
)
|
|
125
|
+
) {
|
|
126
|
+
context.addIssue({
|
|
127
|
+
code: 'custom',
|
|
128
|
+
message: 'Expected an allowed absolute source URI',
|
|
129
|
+
});
|
|
130
|
+
return z.NEVER;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
let uri;
|
|
134
|
+
|
|
135
|
+
try {
|
|
136
|
+
uri = new URL(value);
|
|
137
|
+
} catch {
|
|
138
|
+
context.addIssue({
|
|
139
|
+
code: 'custom',
|
|
140
|
+
message: 'Expected a valid absolute URI',
|
|
141
|
+
});
|
|
142
|
+
return z.NEVER;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (
|
|
146
|
+
uri.username !== ''
|
|
147
|
+
|| uri.password !== ''
|
|
148
|
+
|| uri.search !== ''
|
|
149
|
+
|| uri.hash !== ''
|
|
150
|
+
|| uri.host === ''
|
|
151
|
+
|| (
|
|
152
|
+
HTTP_SOURCE_URI_SCHEMES.has(rawUri.scheme)
|
|
153
|
+
&& !isSafeHttpPath(uri)
|
|
154
|
+
)
|
|
155
|
+
) {
|
|
156
|
+
context.addIssue({
|
|
157
|
+
code: 'custom',
|
|
158
|
+
message: 'Expected a non-local URI without credentials, query, or fragment',
|
|
159
|
+
});
|
|
160
|
+
return z.NEVER;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const canonicalUri = uri.href;
|
|
164
|
+
const canonicalRawUri = parseRawHierarchicalUri(canonicalUri);
|
|
165
|
+
|
|
166
|
+
if (
|
|
167
|
+
canonicalUri.length > MAX_SOURCE_URI_LENGTH
|
|
168
|
+
|| canonicalRawUri === null
|
|
169
|
+
|| (
|
|
170
|
+
CUSTOM_SOURCE_URI_SCHEMES.has(rawUri.scheme)
|
|
171
|
+
&& !isSafeCustomSourceUri(canonicalRawUri)
|
|
172
|
+
)
|
|
173
|
+
) {
|
|
174
|
+
context.addIssue({
|
|
175
|
+
code: 'custom',
|
|
176
|
+
message: 'Canonical source URI exceeds the maximum length',
|
|
177
|
+
});
|
|
178
|
+
return z.NEVER;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
try {
|
|
182
|
+
new URL(canonicalUri);
|
|
183
|
+
} catch {
|
|
184
|
+
context.addIssue({
|
|
185
|
+
code: 'custom',
|
|
186
|
+
message: 'Canonical source URI must remain parseable',
|
|
187
|
+
});
|
|
188
|
+
return z.NEVER;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return canonicalUri;
|
|
192
|
+
});
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
import { canonicalJson } from '../publication/canonical-json.js';
|
|
4
|
+
import {
|
|
5
|
+
ProposalStatus,
|
|
6
|
+
WorkspaceRole,
|
|
7
|
+
WorkspaceVisibility,
|
|
8
|
+
} from './constants.js';
|
|
9
|
+
import { refineUtf8JsonByteBudget } from './json-budget.js';
|
|
10
|
+
import {
|
|
11
|
+
cursorSchema,
|
|
12
|
+
idSchema,
|
|
13
|
+
revisionSchema,
|
|
14
|
+
timestampSchema,
|
|
15
|
+
} from './scalar-contract.js';
|
|
16
|
+
import { parseProtocolValue } from './validation.js';
|
|
17
|
+
|
|
18
|
+
const MAX_SYNC_EVENTS = 500;
|
|
19
|
+
const MAX_SYNC_PAGE_BYTES = 1_048_576;
|
|
20
|
+
|
|
21
|
+
function createWorkspaceEventSchema(type, data) {
|
|
22
|
+
return z.strictObject({
|
|
23
|
+
eventId: idSchema,
|
|
24
|
+
workspaceRevision: revisionSchema,
|
|
25
|
+
recordedAt: timestampSchema,
|
|
26
|
+
type: z.literal(type),
|
|
27
|
+
data,
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const workspaceUpdatedEventSchema = createWorkspaceEventSchema(
|
|
32
|
+
'workspace.updated',
|
|
33
|
+
z.strictObject({
|
|
34
|
+
visibility: z.enum(Object.values(WorkspaceVisibility)),
|
|
35
|
+
}),
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
const membershipChangedEventSchema = createWorkspaceEventSchema(
|
|
39
|
+
'membership.changed',
|
|
40
|
+
z.strictObject({
|
|
41
|
+
subjectId: idSchema,
|
|
42
|
+
role: z.enum(Object.values(WorkspaceRole)).nullable(),
|
|
43
|
+
}),
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
const episodeRecordedEventSchema = createWorkspaceEventSchema(
|
|
47
|
+
'episode.recorded',
|
|
48
|
+
z.strictObject({ episodeId: idSchema }),
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
const factActivatedEventSchema = createWorkspaceEventSchema(
|
|
52
|
+
'fact.activated',
|
|
53
|
+
z.strictObject({ factId: idSchema }),
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
const factSupersededEventSchema = createWorkspaceEventSchema(
|
|
57
|
+
'fact.superseded',
|
|
58
|
+
z
|
|
59
|
+
.strictObject({
|
|
60
|
+
factId: idSchema,
|
|
61
|
+
replacedByFactId: idSchema,
|
|
62
|
+
})
|
|
63
|
+
.superRefine((data, context) => {
|
|
64
|
+
if (data.factId === data.replacedByFactId) {
|
|
65
|
+
context.addIssue({
|
|
66
|
+
code: 'custom',
|
|
67
|
+
path: ['replacedByFactId'],
|
|
68
|
+
message: 'A fact cannot supersede itself',
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
}),
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
const factRetractedEventSchema = createWorkspaceEventSchema(
|
|
75
|
+
'fact.retracted',
|
|
76
|
+
z.strictObject({ factId: idSchema }),
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
const proposalPendingEventSchema = createWorkspaceEventSchema(
|
|
80
|
+
'proposal.pending',
|
|
81
|
+
z.strictObject({ proposalId: idSchema }),
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
const proposalDecidedEventSchema = createWorkspaceEventSchema(
|
|
85
|
+
'proposal.decided',
|
|
86
|
+
z.strictObject({
|
|
87
|
+
proposalId: idSchema,
|
|
88
|
+
status: z.enum([
|
|
89
|
+
ProposalStatus.EFFECTIVE,
|
|
90
|
+
ProposalStatus.REJECTED,
|
|
91
|
+
]),
|
|
92
|
+
}),
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
const signingKeyRevokedEventSchema = createWorkspaceEventSchema(
|
|
96
|
+
'signing_key.revoked',
|
|
97
|
+
z.strictObject({ keyId: idSchema }),
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
export const workspaceEventSchema = z.discriminatedUnion('type', [
|
|
101
|
+
workspaceUpdatedEventSchema,
|
|
102
|
+
membershipChangedEventSchema,
|
|
103
|
+
episodeRecordedEventSchema,
|
|
104
|
+
factActivatedEventSchema,
|
|
105
|
+
factSupersededEventSchema,
|
|
106
|
+
factRetractedEventSchema,
|
|
107
|
+
proposalPendingEventSchema,
|
|
108
|
+
proposalDecidedEventSchema,
|
|
109
|
+
signingKeyRevokedEventSchema,
|
|
110
|
+
]);
|
|
111
|
+
|
|
112
|
+
export const syncPageSchema = z
|
|
113
|
+
.strictObject({
|
|
114
|
+
workspaceId: idSchema,
|
|
115
|
+
events: z.array(workspaceEventSchema).max(MAX_SYNC_EVENTS),
|
|
116
|
+
nextCursor: cursorSchema.nullable(),
|
|
117
|
+
hasMore: z.boolean(),
|
|
118
|
+
})
|
|
119
|
+
.superRefine((page, context) => {
|
|
120
|
+
const fingerprintsByEventId = new Map();
|
|
121
|
+
let previousUniqueRevision;
|
|
122
|
+
|
|
123
|
+
page.events.forEach((workspaceEvent, index) => {
|
|
124
|
+
const fingerprint = canonicalJson(workspaceEvent);
|
|
125
|
+
const previousFingerprint = fingerprintsByEventId.get(
|
|
126
|
+
workspaceEvent.eventId,
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
if (previousFingerprint !== undefined) {
|
|
130
|
+
if (previousFingerprint !== fingerprint) {
|
|
131
|
+
context.addIssue({
|
|
132
|
+
code: 'custom',
|
|
133
|
+
path: ['events', index, 'eventId'],
|
|
134
|
+
message: 'An event ID cannot identify conflicting events',
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
fingerprintsByEventId.set(workspaceEvent.eventId, fingerprint);
|
|
142
|
+
const currentRevision = BigInt(workspaceEvent.workspaceRevision);
|
|
143
|
+
|
|
144
|
+
if (
|
|
145
|
+
previousUniqueRevision !== undefined
|
|
146
|
+
&& currentRevision < previousUniqueRevision
|
|
147
|
+
) {
|
|
148
|
+
context.addIssue({
|
|
149
|
+
code: 'custom',
|
|
150
|
+
path: ['events', index, 'workspaceRevision'],
|
|
151
|
+
message: 'Workspace revisions must be nondecreasing',
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
previousUniqueRevision = currentRevision;
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
if (page.hasMore && page.events.length === 0) {
|
|
159
|
+
context.addIssue({
|
|
160
|
+
code: 'custom',
|
|
161
|
+
path: ['events'],
|
|
162
|
+
message: 'A continued Sync page requires at least one event',
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (page.hasMore && page.nextCursor === null) {
|
|
167
|
+
context.addIssue({
|
|
168
|
+
code: 'custom',
|
|
169
|
+
path: ['nextCursor'],
|
|
170
|
+
message: 'A continued Sync page requires a next cursor',
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
refineUtf8JsonByteBudget(
|
|
175
|
+
page,
|
|
176
|
+
context,
|
|
177
|
+
MAX_SYNC_PAGE_BYTES,
|
|
178
|
+
'Sync page',
|
|
179
|
+
);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
export function parseSyncPage(value) {
|
|
183
|
+
return parseProtocolValue(syncPageSchema, value, 'Sync page');
|
|
184
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
const unsafeWireTextPattern = /\p{C}/u;
|
|
4
|
+
|
|
5
|
+
function hasSafeWireText(value) {
|
|
6
|
+
return !unsafeWireTextPattern.test(value);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function createPreservedSemanticTextSchema(maxLength) {
|
|
10
|
+
return z
|
|
11
|
+
.string()
|
|
12
|
+
.min(1)
|
|
13
|
+
.max(maxLength)
|
|
14
|
+
.refine((value) => (
|
|
15
|
+
value === value.trim()
|
|
16
|
+
&& hasSafeWireText(value)
|
|
17
|
+
));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function createPreservedContentTextSchema(maxLength) {
|
|
21
|
+
return z
|
|
22
|
+
.string()
|
|
23
|
+
.min(0)
|
|
24
|
+
.max(maxLength)
|
|
25
|
+
.refine((value) => (
|
|
26
|
+
(value.length === 0 || value === value.trim())
|
|
27
|
+
&& hasSafeWireText(value)
|
|
28
|
+
));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function createNormalizedSemanticTextSchema(maxLength) {
|
|
32
|
+
return z
|
|
33
|
+
.string()
|
|
34
|
+
.min(1)
|
|
35
|
+
.max(maxLength)
|
|
36
|
+
.refine(hasSafeWireText)
|
|
37
|
+
.transform((value) => value.trim())
|
|
38
|
+
.pipe(z.string().min(1).max(maxLength));
|
|
39
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
const MAX_ISSUES = 20;
|
|
2
|
+
const MAX_ISSUE_CODE_LENGTH = 64;
|
|
3
|
+
const MAX_ISSUE_MESSAGE_LENGTH = 256;
|
|
4
|
+
const MAX_PATH_SEGMENTS = 16;
|
|
5
|
+
const MAX_PATH_SEGMENT_LENGTH = 128;
|
|
6
|
+
const ISSUE_MESSAGES = Object.freeze({
|
|
7
|
+
unrecognized_keys: 'Unexpected field',
|
|
8
|
+
invalid_type: 'Invalid value type',
|
|
9
|
+
too_small: 'Value is below the allowed minimum',
|
|
10
|
+
too_big: 'Value exceeds the allowed maximum',
|
|
11
|
+
invalid_format: 'Value has an invalid format',
|
|
12
|
+
invalid_value: 'Value is not allowed',
|
|
13
|
+
invalid_union: 'Value does not match any allowed shape',
|
|
14
|
+
custom: 'Value violates a protocol constraint',
|
|
15
|
+
});
|
|
16
|
+
const DEFAULT_ISSUE_MESSAGE = 'Value failed protocol validation';
|
|
17
|
+
|
|
18
|
+
function truncateSafely(value, maxLength) {
|
|
19
|
+
try {
|
|
20
|
+
return String(value).slice(0, maxLength);
|
|
21
|
+
} catch {
|
|
22
|
+
return '[unprintable]';
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function projectPath(path) {
|
|
27
|
+
const projectedPath = path.slice(0, MAX_PATH_SEGMENTS).map((segment) => {
|
|
28
|
+
if (typeof segment === 'number') {
|
|
29
|
+
return segment;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return truncateSafely(segment, MAX_PATH_SEGMENT_LENGTH);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
return Object.freeze(projectedPath);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function projectIssue({ code, path }) {
|
|
39
|
+
const message = Object.hasOwn(ISSUE_MESSAGES, code)
|
|
40
|
+
? ISSUE_MESSAGES[code]
|
|
41
|
+
: DEFAULT_ISSUE_MESSAGE;
|
|
42
|
+
|
|
43
|
+
return Object.freeze({
|
|
44
|
+
code: truncateSafely(code, MAX_ISSUE_CODE_LENGTH),
|
|
45
|
+
path: projectPath(path),
|
|
46
|
+
message: truncateSafely(message, MAX_ISSUE_MESSAGE_LENGTH),
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export class ProtocolValidationError extends TypeError {
|
|
51
|
+
constructor(label, issues) {
|
|
52
|
+
super(`${label} failed protocol validation`);
|
|
53
|
+
this.name = 'ProtocolValidationError';
|
|
54
|
+
this.issues = Object.freeze(issues.slice(0, MAX_ISSUES).map(projectIssue));
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function parseProtocolValue(schema, value, label) {
|
|
59
|
+
const result = schema.safeParse(value);
|
|
60
|
+
|
|
61
|
+
if (!result.success) {
|
|
62
|
+
throw new ProtocolValidationError(label, result.error.issues);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return result.data;
|
|
66
|
+
}
|