@gmickel/gno 1.20.0 → 1.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.
- package/README.md +29 -5
- package/assets/skill/SKILL.md +46 -15
- package/package.json +2 -1
- package/spec/cli.md +144 -0
- package/spec/db/schema.sql +170 -0
- package/spec/evals-agentic.md +48 -0
- package/spec/mcp.md +22 -0
- package/spec/output-schemas/capsule-reverified-event.schema.json +47 -0
- package/spec/output-schemas/changes.schema.json +280 -0
- package/spec/output-schemas/document-diff.schema.json +185 -0
- package/spec/output-schemas/impact.schema.json +122 -0
- package/spec/output-schemas/publish-artifact.schema.json +284 -0
- package/spec/output-schemas/saved-capsule-list.schema.json +16 -0
- package/spec/output-schemas/saved-capsule-registration.schema.json +172 -0
- package/spec/output-schemas/saved-capsule-reverification.schema.json +59 -0
- package/spec/output-schemas/saved-capsule-unwatch.schema.json +16 -0
- package/spec/output-schemas/saved-capsule-watch.schema.json +17 -0
- package/src/cli/commands/changes.ts +160 -0
- package/src/cli/commands/context-saved.ts +189 -0
- package/src/cli/options.ts +8 -0
- package/src/cli/program.ts +195 -0
- package/src/core/capsule-registry.ts +279 -0
- package/src/core/capsule-reverification-scheduler.ts +218 -0
- package/src/core/capsule-reverification.ts +289 -0
- package/src/core/change-diff.ts +182 -0
- package/src/core/change-journal.ts +228 -0
- package/src/core/knowledge-delta.ts +395 -0
- package/src/core/knowledge-impact.ts +202 -0
- package/src/ingestion/sync.ts +214 -165
- package/src/mcp/tools/changes.ts +80 -0
- package/src/mcp/tools/index.ts +29 -0
- package/src/publish/artifact-validation.ts +259 -0
- package/src/publish/artifact.ts +234 -118
- package/src/publish/export-service.ts +5 -9
- package/src/publish/metadata.ts +195 -0
- package/src/sdk/client.ts +42 -0
- package/src/sdk/index.ts +7 -0
- package/src/sdk/types.ts +22 -0
- package/src/serve/doc-events.ts +12 -1
- package/src/serve/resident-runtime.ts +22 -0
- package/src/serve/routes/api.ts +13 -0
- package/src/serve/routes/changes.ts +102 -0
- package/src/serve/server.ts +34 -0
- package/src/serve/watch-service.ts +9 -0
- package/src/store/index.ts +21 -0
- package/src/store/migrations/015-document-change-journal.ts +85 -0
- package/src/store/migrations/016-saved-capsules.ts +131 -0
- package/src/store/migrations/017-document-change-retention-counters.ts +33 -0
- package/src/store/migrations/018-saved-capsule-registration-epoch.ts +24 -0
- package/src/store/migrations/019-saved-capsule-registration-generation.ts +53 -0
- package/src/store/migrations/index.ts +10 -0
- package/src/store/sqlite/adapter.ts +291 -7
- package/src/store/sqlite/capsule-registry-store.ts +534 -0
- package/src/store/sqlite/change-journal-store.ts +473 -0
- package/src/store/types.ts +262 -0
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime validation and closed projection for publish artifact builders.
|
|
3
|
+
*
|
|
4
|
+
* @module src/publish/artifact-validation
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export const MAX_PUBLISH_SLUG_LENGTH = 80;
|
|
8
|
+
export const MAX_ENCRYPTED_CIPHERTEXT_BASE64_LENGTH = 67_108_864;
|
|
9
|
+
export const MAX_ENCRYPTED_KEY_MATERIAL_BASE64_LENGTH = 1024;
|
|
10
|
+
export const MAX_ENCRYPTED_SECRET_TOKEN_LENGTH = 512;
|
|
11
|
+
|
|
12
|
+
const PUBLISH_SLUG_PATTERN = /^[a-z0-9](?:[a-z0-9-]{0,78}[a-z0-9])?$/u;
|
|
13
|
+
const BASE64_PATTERN =
|
|
14
|
+
/^(?:[a-zA-Z0-9+/]{4})*(?:[a-zA-Z0-9+/]{2}==|[a-zA-Z0-9+/]{3}=)?$/u;
|
|
15
|
+
|
|
16
|
+
const SOURCE_TYPES = new Set(["collection", "note"]);
|
|
17
|
+
const READER_VISIBILITIES = new Set(["invite-only", "public", "secret-link"]);
|
|
18
|
+
|
|
19
|
+
export interface ValidatedPublishNote {
|
|
20
|
+
markdown: string;
|
|
21
|
+
metadata?: Record<string, string | string[]>;
|
|
22
|
+
slug: string;
|
|
23
|
+
summary: string;
|
|
24
|
+
title: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface ValidatedPublishSpaceInput {
|
|
28
|
+
homeNoteSlug?: string;
|
|
29
|
+
notes: ValidatedPublishNote[];
|
|
30
|
+
routeSlug: string;
|
|
31
|
+
sourceType: "note" | "collection";
|
|
32
|
+
summary: string;
|
|
33
|
+
title: string;
|
|
34
|
+
visibility: "invite-only" | "public" | "secret-link";
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface ValidatedEncryptedPublishInput {
|
|
38
|
+
encryptedPayload: {
|
|
39
|
+
ciphertext: string;
|
|
40
|
+
iterations: number;
|
|
41
|
+
iv: string;
|
|
42
|
+
salt: string;
|
|
43
|
+
};
|
|
44
|
+
routeSlug: string;
|
|
45
|
+
secretToken: string;
|
|
46
|
+
sourceType: "note" | "collection";
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const requireRecord = (
|
|
50
|
+
value: unknown,
|
|
51
|
+
field: string
|
|
52
|
+
): Record<string, unknown> => {
|
|
53
|
+
if (!(value && typeof value === "object" && !Array.isArray(value))) {
|
|
54
|
+
throw new Error(`${field} must be an object`);
|
|
55
|
+
}
|
|
56
|
+
return value as Record<string, unknown>;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const requireString = (value: unknown, field: string): string => {
|
|
60
|
+
if (typeof value !== "string") {
|
|
61
|
+
throw new Error(`${field} must be a string`);
|
|
62
|
+
}
|
|
63
|
+
return value;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const requireNonblankString = (value: unknown, field: string): string => {
|
|
67
|
+
const result = requireString(value, field);
|
|
68
|
+
if (result.trim().length === 0) {
|
|
69
|
+
throw new Error(`${field} must not be blank`);
|
|
70
|
+
}
|
|
71
|
+
return result;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const requireBoundedNonblankString = (
|
|
75
|
+
value: unknown,
|
|
76
|
+
field: string,
|
|
77
|
+
maxLength: number
|
|
78
|
+
): string => {
|
|
79
|
+
const result = requireNonblankString(value, field);
|
|
80
|
+
if (result.length > maxLength) {
|
|
81
|
+
throw new Error(`${field} must not exceed ${maxLength} characters`);
|
|
82
|
+
}
|
|
83
|
+
return result;
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const requireBase64 = (
|
|
87
|
+
value: unknown,
|
|
88
|
+
field: string,
|
|
89
|
+
maxLength: number
|
|
90
|
+
): string => {
|
|
91
|
+
const result = requireBoundedNonblankString(value, field, maxLength);
|
|
92
|
+
if (!BASE64_PATTERN.test(result)) {
|
|
93
|
+
throw new Error(`${field} must be valid base64`);
|
|
94
|
+
}
|
|
95
|
+
return result;
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const requireSlug = (value: unknown, field: string): string => {
|
|
99
|
+
const result = requireString(value, field);
|
|
100
|
+
if (!PUBLISH_SLUG_PATTERN.test(result)) {
|
|
101
|
+
throw new Error(`${field} must be a valid publish slug`);
|
|
102
|
+
}
|
|
103
|
+
return result;
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const projectMetadata = (
|
|
107
|
+
value: unknown,
|
|
108
|
+
field: string
|
|
109
|
+
): Record<string, string | string[]> | undefined => {
|
|
110
|
+
if (value === undefined) return undefined;
|
|
111
|
+
|
|
112
|
+
const input = requireRecord(value, field);
|
|
113
|
+
const result: Record<string, string | string[]> = {};
|
|
114
|
+
for (const [key, entry] of Object.entries(input)) {
|
|
115
|
+
if (typeof entry === "string") {
|
|
116
|
+
result[key] = entry;
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
if (
|
|
120
|
+
Array.isArray(entry) &&
|
|
121
|
+
entry.every((item): item is string => typeof item === "string")
|
|
122
|
+
) {
|
|
123
|
+
result[key] = [...entry];
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
throw new Error(`${field}.${key} must be a string or string array`);
|
|
127
|
+
}
|
|
128
|
+
return result;
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
const projectNote = (value: unknown, index: number): ValidatedPublishNote => {
|
|
132
|
+
const field = `notes[${index}]`;
|
|
133
|
+
const input = requireRecord(value, field);
|
|
134
|
+
const metadata = projectMetadata(input.metadata, `${field}.metadata`);
|
|
135
|
+
const note: ValidatedPublishNote = {
|
|
136
|
+
markdown: requireString(input.markdown, `${field}.markdown`),
|
|
137
|
+
slug: requireSlug(input.slug, `${field}.slug`),
|
|
138
|
+
summary: requireString(input.summary, `${field}.summary`),
|
|
139
|
+
title: requireNonblankString(input.title, `${field}.title`),
|
|
140
|
+
};
|
|
141
|
+
if (metadata !== undefined) note.metadata = metadata;
|
|
142
|
+
return note;
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
export const validateAndProjectPublishSpaceInput = (
|
|
146
|
+
value: unknown
|
|
147
|
+
): ValidatedPublishSpaceInput => {
|
|
148
|
+
const input = requireRecord(value, "publish input");
|
|
149
|
+
if (!Array.isArray(input.notes) || input.notes.length === 0) {
|
|
150
|
+
throw new Error("Publish input must contain at least one note");
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const notes = input.notes.map(projectNote);
|
|
154
|
+
const noteSlugs = new Set<string>();
|
|
155
|
+
for (const note of notes) {
|
|
156
|
+
if (noteSlugs.has(note.slug)) {
|
|
157
|
+
throw new Error(
|
|
158
|
+
`Publish input contains duplicate note slug "${note.slug}"`
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
noteSlugs.add(note.slug);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const routeSlug = requireSlug(input.routeSlug, "routeSlug");
|
|
165
|
+
const sourceType = requireString(input.sourceType, "sourceType");
|
|
166
|
+
if (!SOURCE_TYPES.has(sourceType)) {
|
|
167
|
+
throw new Error('sourceType must be "note" or "collection"');
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const visibility = requireString(input.visibility, "visibility");
|
|
171
|
+
if (!READER_VISIBILITIES.has(visibility)) {
|
|
172
|
+
throw new Error(
|
|
173
|
+
'visibility must be "public", "secret-link", or "invite-only"'
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const homeNoteSlug =
|
|
178
|
+
input.homeNoteSlug === undefined
|
|
179
|
+
? undefined
|
|
180
|
+
: requireSlug(input.homeNoteSlug, "homeNoteSlug");
|
|
181
|
+
if (homeNoteSlug !== undefined && !noteSlugs.has(homeNoteSlug)) {
|
|
182
|
+
throw new Error(`homeNoteSlug "${homeNoteSlug}" is not present in notes`);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const result: ValidatedPublishSpaceInput = {
|
|
186
|
+
notes,
|
|
187
|
+
routeSlug,
|
|
188
|
+
sourceType: sourceType as "note" | "collection",
|
|
189
|
+
summary: requireString(input.summary, "summary"),
|
|
190
|
+
title: requireNonblankString(input.title, "title"),
|
|
191
|
+
visibility: visibility as ValidatedPublishSpaceInput["visibility"],
|
|
192
|
+
};
|
|
193
|
+
if (homeNoteSlug !== undefined) result.homeNoteSlug = homeNoteSlug;
|
|
194
|
+
return result;
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
export const validateAndProjectEncryptedPublishInput = (
|
|
198
|
+
value: unknown
|
|
199
|
+
): ValidatedEncryptedPublishInput => {
|
|
200
|
+
const input = requireRecord(value, "encrypted publish input");
|
|
201
|
+
const payload = requireRecord(input.encryptedPayload, "encryptedPayload");
|
|
202
|
+
const iterations = payload.iterations;
|
|
203
|
+
if (
|
|
204
|
+
typeof iterations !== "number" ||
|
|
205
|
+
!Number.isSafeInteger(iterations) ||
|
|
206
|
+
iterations <= 0
|
|
207
|
+
) {
|
|
208
|
+
throw new Error(
|
|
209
|
+
"encryptedPayload.iterations must be a positive safe integer"
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const sourceType = requireString(input.sourceType, "sourceType");
|
|
214
|
+
if (!SOURCE_TYPES.has(sourceType)) {
|
|
215
|
+
throw new Error('sourceType must be "note" or "collection"');
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return {
|
|
219
|
+
encryptedPayload: {
|
|
220
|
+
ciphertext: requireBase64(
|
|
221
|
+
payload.ciphertext,
|
|
222
|
+
"encryptedPayload.ciphertext",
|
|
223
|
+
MAX_ENCRYPTED_CIPHERTEXT_BASE64_LENGTH
|
|
224
|
+
),
|
|
225
|
+
iterations,
|
|
226
|
+
iv: requireBase64(
|
|
227
|
+
payload.iv,
|
|
228
|
+
"encryptedPayload.iv",
|
|
229
|
+
MAX_ENCRYPTED_KEY_MATERIAL_BASE64_LENGTH
|
|
230
|
+
),
|
|
231
|
+
salt: requireBase64(
|
|
232
|
+
payload.salt,
|
|
233
|
+
"encryptedPayload.salt",
|
|
234
|
+
MAX_ENCRYPTED_KEY_MATERIAL_BASE64_LENGTH
|
|
235
|
+
),
|
|
236
|
+
},
|
|
237
|
+
routeSlug: requireSlug(input.routeSlug, "routeSlug"),
|
|
238
|
+
secretToken: requireBoundedNonblankString(
|
|
239
|
+
input.secretToken,
|
|
240
|
+
"secretToken",
|
|
241
|
+
MAX_ENCRYPTED_SECRET_TOKEN_LENGTH
|
|
242
|
+
),
|
|
243
|
+
sourceType: sourceType as "note" | "collection",
|
|
244
|
+
};
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
export const requirePublishDateTime = (
|
|
248
|
+
value: unknown,
|
|
249
|
+
field = "exportedAt"
|
|
250
|
+
): string => {
|
|
251
|
+
const result = requireString(value, field);
|
|
252
|
+
if (
|
|
253
|
+
!/^\d{4}-\d{2}-\d{2}T/u.test(result) ||
|
|
254
|
+
Number.isNaN(Date.parse(result))
|
|
255
|
+
) {
|
|
256
|
+
throw new Error(`${field} must be a valid date-time`);
|
|
257
|
+
}
|
|
258
|
+
return result;
|
|
259
|
+
};
|
package/src/publish/artifact.ts
CHANGED
|
@@ -4,9 +4,23 @@
|
|
|
4
4
|
* @module src/publish/artifact
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import type { DocumentRow
|
|
7
|
+
import type { DocumentRow } from "../store/types";
|
|
8
8
|
|
|
9
|
+
import { deriveDocid } from "../app/constants";
|
|
10
|
+
import {
|
|
11
|
+
contextCapsuleEvidenceIdentity,
|
|
12
|
+
sha256Text,
|
|
13
|
+
} from "../core/context-capsule-validation";
|
|
9
14
|
import { stripFrontmatter } from "../ingestion/frontmatter";
|
|
15
|
+
import {
|
|
16
|
+
MAX_PUBLISH_SLUG_LENGTH,
|
|
17
|
+
requirePublishDateTime,
|
|
18
|
+
validateAndProjectEncryptedPublishInput,
|
|
19
|
+
validateAndProjectPublishSpaceInput,
|
|
20
|
+
} from "./artifact-validation";
|
|
21
|
+
|
|
22
|
+
export { MAX_PUBLISH_SLUG_LENGTH } from "./artifact-validation";
|
|
23
|
+
export { buildExportedMetadata } from "./metadata";
|
|
10
24
|
|
|
11
25
|
export type PublishVisibility =
|
|
12
26
|
| "encrypted"
|
|
@@ -22,16 +36,70 @@ export interface PublishArtifactNote {
|
|
|
22
36
|
title: string;
|
|
23
37
|
}
|
|
24
38
|
|
|
25
|
-
export
|
|
39
|
+
export const PUBLIC_PUBLISH_MANIFEST_SCHEMA_VERSION = "1.0" as const;
|
|
40
|
+
|
|
41
|
+
export const PUBLIC_PUBLISH_CAPABILITIES = {
|
|
42
|
+
capsuleEvidence: true,
|
|
43
|
+
exactLineCitations: true,
|
|
44
|
+
llmsTxt: true,
|
|
45
|
+
markdownDocuments: true,
|
|
46
|
+
} as const;
|
|
47
|
+
|
|
48
|
+
export interface PublicPublishEvidence {
|
|
49
|
+
docid: string;
|
|
50
|
+
endLine: number;
|
|
51
|
+
evidenceId: string;
|
|
52
|
+
locator: string;
|
|
53
|
+
mirrorHash: string;
|
|
54
|
+
passageHash: string;
|
|
55
|
+
sourceHash: string;
|
|
56
|
+
startLine: number;
|
|
57
|
+
uri: string;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface PublicPublishDocument {
|
|
61
|
+
byteLength: number;
|
|
62
|
+
contentHash: string;
|
|
63
|
+
evidence: PublicPublishEvidence;
|
|
64
|
+
lineCount: number;
|
|
65
|
+
markdownPath: string;
|
|
66
|
+
slug: string;
|
|
67
|
+
summary: string;
|
|
68
|
+
title: string;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface PublicPublishManifest {
|
|
72
|
+
capabilities: typeof PUBLIC_PUBLISH_CAPABILITIES;
|
|
73
|
+
documents: PublicPublishDocument[];
|
|
74
|
+
generatedAt: string;
|
|
75
|
+
projectionRevision: string;
|
|
76
|
+
schemaVersion: typeof PUBLIC_PUBLISH_MANIFEST_SCHEMA_VERSION;
|
|
77
|
+
visibility: "public";
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
interface PublishArtifactSpaceBase {
|
|
26
81
|
homeNoteSlug?: string;
|
|
27
82
|
notes: PublishArtifactNote[];
|
|
28
83
|
routeSlug: string;
|
|
29
84
|
sourceType: "note" | "collection";
|
|
30
85
|
summary: string;
|
|
31
86
|
title: string;
|
|
32
|
-
visibility: PublishVisibility;
|
|
33
87
|
}
|
|
34
88
|
|
|
89
|
+
export interface PublicPublishArtifactSpace extends PublishArtifactSpaceBase {
|
|
90
|
+
manifest: PublicPublishManifest;
|
|
91
|
+
visibility: "public";
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export interface RestrictedPublishArtifactSpace extends PublishArtifactSpaceBase {
|
|
95
|
+
manifest?: never;
|
|
96
|
+
visibility: "invite-only" | "secret-link";
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export type PublishArtifactSpace =
|
|
100
|
+
| PublicPublishArtifactSpace
|
|
101
|
+
| RestrictedPublishArtifactSpace;
|
|
102
|
+
|
|
35
103
|
export interface EncryptedArtifactPayload {
|
|
36
104
|
ciphertext: string;
|
|
37
105
|
iterations: number;
|
|
@@ -70,29 +138,6 @@ export const PUBLISH_VISIBILITY_VALUES = [
|
|
|
70
138
|
"encrypted",
|
|
71
139
|
] as const;
|
|
72
140
|
|
|
73
|
-
export const MAX_PUBLISH_SLUG_LENGTH = 80;
|
|
74
|
-
|
|
75
|
-
const ALLOWED_FRONTMATTER_METADATA_KEYS = new Set([
|
|
76
|
-
"audience",
|
|
77
|
-
"canonical",
|
|
78
|
-
"canonicalUrl",
|
|
79
|
-
"canonicalURL",
|
|
80
|
-
"coverAlt",
|
|
81
|
-
"coverImage",
|
|
82
|
-
"icon",
|
|
83
|
-
"image",
|
|
84
|
-
"layout",
|
|
85
|
-
"publishedAt",
|
|
86
|
-
"readingTime",
|
|
87
|
-
"series",
|
|
88
|
-
"seriesOrder",
|
|
89
|
-
"status",
|
|
90
|
-
"subtitle",
|
|
91
|
-
"theme",
|
|
92
|
-
"topic",
|
|
93
|
-
"topics",
|
|
94
|
-
]);
|
|
95
|
-
|
|
96
141
|
export const isPublishVisibility = (
|
|
97
142
|
value: unknown
|
|
98
143
|
): value is PublishVisibility =>
|
|
@@ -175,119 +220,190 @@ export const deriveExportedSummary = (
|
|
|
175
220
|
return plain.slice(0, 200).trim();
|
|
176
221
|
};
|
|
177
222
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
| "collection"
|
|
184
|
-
| "contentType"
|
|
185
|
-
| "frontmatterDate"
|
|
186
|
-
| "languageHint"
|
|
187
|
-
| "relPath"
|
|
188
|
-
>,
|
|
189
|
-
parsedFrontmatter: Record<string, unknown>,
|
|
190
|
-
tags: TagRow[]
|
|
191
|
-
) => {
|
|
192
|
-
const metadata: Record<string, string | string[]> = {};
|
|
223
|
+
const compareCodeUnits = (left: string, right: string): number => {
|
|
224
|
+
if (left < right) return -1;
|
|
225
|
+
if (left > right) return 1;
|
|
226
|
+
return 0;
|
|
227
|
+
};
|
|
193
228
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
if (doc.contentType) {
|
|
198
|
-
metadata.contentType = doc.contentType;
|
|
199
|
-
}
|
|
200
|
-
if (doc.languageHint) {
|
|
201
|
-
metadata.language = doc.languageHint;
|
|
202
|
-
}
|
|
203
|
-
if (doc.frontmatterDate) {
|
|
204
|
-
metadata.date = doc.frontmatterDate;
|
|
229
|
+
const canonicalizeJsonValue = (value: unknown): unknown => {
|
|
230
|
+
if (Array.isArray(value)) {
|
|
231
|
+
return value.map(canonicalizeJsonValue);
|
|
205
232
|
}
|
|
206
|
-
if (
|
|
207
|
-
|
|
233
|
+
if (value && typeof value === "object") {
|
|
234
|
+
return Object.fromEntries(
|
|
235
|
+
Object.entries(value)
|
|
236
|
+
.sort(([left], [right]) => compareCodeUnits(left, right))
|
|
237
|
+
.map(([key, entry]) => [key, canonicalizeJsonValue(entry)])
|
|
238
|
+
);
|
|
208
239
|
}
|
|
240
|
+
return value;
|
|
241
|
+
};
|
|
209
242
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
243
|
+
const canonicalJson = (value: unknown): string =>
|
|
244
|
+
JSON.stringify(canonicalizeJsonValue(value));
|
|
245
|
+
|
|
246
|
+
const buildPublicPublishDocument = (
|
|
247
|
+
routeSlug: string,
|
|
248
|
+
note: PublishArtifactNote
|
|
249
|
+
): PublicPublishDocument => {
|
|
250
|
+
const contentHash = sha256Text(note.markdown);
|
|
251
|
+
const docid = deriveDocid(contentHash);
|
|
252
|
+
const lineCount = note.markdown.split("\n").length;
|
|
253
|
+
const markdownPath = `./${note.slug}.md`;
|
|
254
|
+
const uri = `gno://public/${routeSlug}/${note.slug}.md`;
|
|
255
|
+
const evidenceBase = {
|
|
256
|
+
uri,
|
|
257
|
+
docid,
|
|
258
|
+
startLine: 1,
|
|
259
|
+
endLine: lineCount,
|
|
260
|
+
sourceHash: contentHash,
|
|
261
|
+
mirrorHash: contentHash,
|
|
262
|
+
passageHash: contentHash,
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
return {
|
|
266
|
+
byteLength: new TextEncoder().encode(note.markdown).byteLength,
|
|
267
|
+
contentHash,
|
|
268
|
+
evidence: {
|
|
269
|
+
...evidenceBase,
|
|
270
|
+
evidenceId: contextCapsuleEvidenceIdentity(evidenceBase),
|
|
271
|
+
locator: `${markdownPath}#L1-L${lineCount}`,
|
|
272
|
+
},
|
|
273
|
+
lineCount,
|
|
274
|
+
markdownPath,
|
|
275
|
+
slug: note.slug,
|
|
276
|
+
summary: note.summary,
|
|
277
|
+
title: note.title,
|
|
278
|
+
};
|
|
279
|
+
};
|
|
214
280
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
metadata[key] = value.trim();
|
|
229
|
-
continue;
|
|
230
|
-
}
|
|
231
|
-
if (Array.isArray(value)) {
|
|
232
|
-
const cleaned = value
|
|
233
|
-
.filter((entry): entry is string => typeof entry === "string")
|
|
234
|
-
.map((entry) => entry.trim())
|
|
235
|
-
.filter(Boolean);
|
|
236
|
-
if (cleaned.length > 0) {
|
|
237
|
-
metadata[key] = cleaned;
|
|
238
|
-
}
|
|
239
|
-
}
|
|
281
|
+
export const buildPublicPublishManifest = (input: {
|
|
282
|
+
exportedAt: string;
|
|
283
|
+
homeNoteSlug?: string;
|
|
284
|
+
notes: PublishArtifactNote[];
|
|
285
|
+
routeSlug: string;
|
|
286
|
+
sourceType: "note" | "collection";
|
|
287
|
+
summary: string;
|
|
288
|
+
title: string;
|
|
289
|
+
visibility: "public";
|
|
290
|
+
}): PublicPublishManifest => {
|
|
291
|
+
const validated = validateAndProjectPublishSpaceInput(input);
|
|
292
|
+
if (validated.visibility !== "public") {
|
|
293
|
+
throw new Error("Public publish manifests require public visibility");
|
|
240
294
|
}
|
|
241
|
-
|
|
242
|
-
|
|
295
|
+
const exportedAt = requirePublishDateTime(input.exportedAt);
|
|
296
|
+
const documents = validated.notes
|
|
297
|
+
.map((note) => buildPublicPublishDocument(validated.routeSlug, note))
|
|
298
|
+
.sort(
|
|
299
|
+
(left, right) =>
|
|
300
|
+
compareCodeUnits(left.slug, right.slug) ||
|
|
301
|
+
compareCodeUnits(left.title, right.title) ||
|
|
302
|
+
compareCodeUnits(left.contentHash, right.contentHash)
|
|
303
|
+
);
|
|
304
|
+
const revisionProjection = {
|
|
305
|
+
capabilities: PUBLIC_PUBLISH_CAPABILITIES,
|
|
306
|
+
documents,
|
|
307
|
+
homeNoteSlug: validated.homeNoteSlug ?? null,
|
|
308
|
+
notes: validated.notes
|
|
309
|
+
.map((note) => ({
|
|
310
|
+
markdown: note.markdown,
|
|
311
|
+
metadata: note.metadata ?? {},
|
|
312
|
+
slug: note.slug,
|
|
313
|
+
summary: note.summary,
|
|
314
|
+
title: note.title,
|
|
315
|
+
}))
|
|
316
|
+
.sort((left, right) => compareCodeUnits(left.slug, right.slug)),
|
|
317
|
+
routeSlug: validated.routeSlug,
|
|
318
|
+
sourceType: validated.sourceType,
|
|
319
|
+
summary: validated.summary,
|
|
320
|
+
title: validated.title,
|
|
321
|
+
visibility: validated.visibility,
|
|
322
|
+
};
|
|
323
|
+
|
|
324
|
+
return {
|
|
325
|
+
capabilities: { ...PUBLIC_PUBLISH_CAPABILITIES },
|
|
326
|
+
documents,
|
|
327
|
+
generatedAt: exportedAt,
|
|
328
|
+
projectionRevision: sha256Text(canonicalJson(revisionProjection)),
|
|
329
|
+
schemaVersion: PUBLIC_PUBLISH_MANIFEST_SCHEMA_VERSION,
|
|
330
|
+
visibility: validated.visibility,
|
|
331
|
+
};
|
|
243
332
|
};
|
|
244
333
|
|
|
245
334
|
export const buildPublishArtifact = (input: {
|
|
246
335
|
homeNoteSlug?: string;
|
|
247
336
|
notes: PublishArtifactNote[];
|
|
248
337
|
routeSlug: string;
|
|
249
|
-
source: string;
|
|
250
338
|
sourceType: "note" | "collection";
|
|
251
339
|
summary: string;
|
|
252
340
|
title: string;
|
|
253
|
-
visibility: PublishVisibility
|
|
254
|
-
}) =>
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
341
|
+
visibility: Exclude<PublishVisibility, "encrypted">;
|
|
342
|
+
}): PublishArtifactV1 => {
|
|
343
|
+
const validated = validateAndProjectPublishSpaceInput(input);
|
|
344
|
+
const exportedAt = new Date().toISOString();
|
|
345
|
+
const base = {
|
|
346
|
+
...(validated.homeNoteSlug === undefined
|
|
347
|
+
? {}
|
|
348
|
+
: { homeNoteSlug: validated.homeNoteSlug }),
|
|
349
|
+
notes: validated.notes,
|
|
350
|
+
routeSlug: validated.routeSlug,
|
|
351
|
+
sourceType: validated.sourceType,
|
|
352
|
+
summary: validated.summary,
|
|
353
|
+
title: validated.title,
|
|
354
|
+
};
|
|
355
|
+
const space: PublishArtifactSpace =
|
|
356
|
+
validated.visibility === "public"
|
|
357
|
+
? {
|
|
358
|
+
...base,
|
|
359
|
+
manifest: buildPublicPublishManifest({
|
|
360
|
+
exportedAt,
|
|
361
|
+
homeNoteSlug: validated.homeNoteSlug,
|
|
362
|
+
notes: validated.notes,
|
|
363
|
+
routeSlug: validated.routeSlug,
|
|
364
|
+
sourceType: validated.sourceType,
|
|
365
|
+
summary: validated.summary,
|
|
366
|
+
title: validated.title,
|
|
367
|
+
visibility: validated.visibility,
|
|
368
|
+
}),
|
|
369
|
+
visibility: validated.visibility,
|
|
370
|
+
}
|
|
371
|
+
: {
|
|
372
|
+
...base,
|
|
373
|
+
visibility: validated.visibility,
|
|
374
|
+
};
|
|
375
|
+
|
|
376
|
+
return {
|
|
377
|
+
exportedAt,
|
|
378
|
+
source: validated.routeSlug,
|
|
379
|
+
spaces: [space],
|
|
380
|
+
version: 1,
|
|
381
|
+
};
|
|
382
|
+
};
|
|
270
383
|
|
|
271
384
|
export const buildEncryptedPublishArtifact = (input: {
|
|
272
385
|
encryptedPayload: EncryptedArtifactPayload;
|
|
273
386
|
routeSlug: string;
|
|
274
387
|
secretToken: string;
|
|
275
|
-
source: string;
|
|
276
388
|
sourceType: "note" | "collection";
|
|
277
|
-
}) =>
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
}
|
|
389
|
+
}): PublishArtifactV2 => {
|
|
390
|
+
const validated = validateAndProjectEncryptedPublishInput(input);
|
|
391
|
+
const exportedAt = requirePublishDateTime(new Date().toISOString());
|
|
392
|
+
return {
|
|
393
|
+
exportedAt,
|
|
394
|
+
source: validated.routeSlug,
|
|
395
|
+
spaces: [
|
|
396
|
+
{
|
|
397
|
+
encryptedPayload: validated.encryptedPayload,
|
|
398
|
+
routeSlug: validated.routeSlug,
|
|
399
|
+
secretToken: validated.secretToken,
|
|
400
|
+
sourceType: validated.sourceType,
|
|
401
|
+
visibility: "encrypted",
|
|
402
|
+
},
|
|
403
|
+
],
|
|
404
|
+
version: 2,
|
|
405
|
+
};
|
|
406
|
+
};
|
|
291
407
|
|
|
292
408
|
export const derivePublishArtifactFilename = (artifact: PublishArtifact) => {
|
|
293
409
|
const routeSlug =
|