fold-agent 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/README.md +14 -0
  2. package/bin/fold-agent.js +2 -0
  3. package/dist/cli/app.d.ts +4 -0
  4. package/dist/cli/app.js +1113 -0
  5. package/dist/cli/app.js.map +1 -0
  6. package/dist/cli/bin.d.ts +2 -0
  7. package/dist/cli/bin.js +8 -0
  8. package/dist/cli/bin.js.map +1 -0
  9. package/dist/cli/context.d.ts +5 -0
  10. package/dist/cli/context.js +2 -0
  11. package/dist/cli/context.js.map +1 -0
  12. package/dist/cli/operations.d.ts +126 -0
  13. package/dist/cli/operations.js +1159 -0
  14. package/dist/cli/operations.js.map +1 -0
  15. package/dist/cli/output.d.ts +22 -0
  16. package/dist/cli/output.js +245 -0
  17. package/dist/cli/output.js.map +1 -0
  18. package/dist/cli/package-info.d.ts +5 -0
  19. package/dist/cli/package-info.js +12 -0
  20. package/dist/cli/package-info.js.map +1 -0
  21. package/dist/cli/results.d.ts +393 -0
  22. package/dist/cli/results.js +2 -0
  23. package/dist/cli/results.js.map +1 -0
  24. package/dist/cli/skill-install.d.ts +7 -0
  25. package/dist/cli/skill-install.js +211 -0
  26. package/dist/cli/skill-install.js.map +1 -0
  27. package/dist/deploy/public-origin.d.ts +15 -0
  28. package/dist/deploy/public-origin.js +59 -0
  29. package/dist/deploy/public-origin.js.map +1 -0
  30. package/dist/rooms/append-log-api.d.ts +16 -0
  31. package/dist/rooms/append-log-api.js +72 -0
  32. package/dist/rooms/append-log-api.js.map +1 -0
  33. package/dist/rooms/append-log-validation.d.ts +2 -0
  34. package/dist/rooms/append-log-validation.js +16 -0
  35. package/dist/rooms/append-log-validation.js.map +1 -0
  36. package/dist/rooms/comments.d.ts +63 -0
  37. package/dist/rooms/comments.js +136 -0
  38. package/dist/rooms/comments.js.map +1 -0
  39. package/dist/rooms/crypto.d.ts +11 -0
  40. package/dist/rooms/crypto.js +44 -0
  41. package/dist/rooms/crypto.js.map +1 -0
  42. package/dist/rooms/encrypted-records.d.ts +5 -0
  43. package/dist/rooms/encrypted-records.js +21 -0
  44. package/dist/rooms/encrypted-records.js.map +1 -0
  45. package/dist/rooms/markdown-snapshot.d.ts +23 -0
  46. package/dist/rooms/markdown-snapshot.js +126 -0
  47. package/dist/rooms/markdown-snapshot.js.map +1 -0
  48. package/dist/rooms/metadata.d.ts +32 -0
  49. package/dist/rooms/metadata.js +118 -0
  50. package/dist/rooms/metadata.js.map +1 -0
  51. package/dist/rooms/personas.d.ts +16 -0
  52. package/dist/rooms/personas.js +78 -0
  53. package/dist/rooms/personas.js.map +1 -0
  54. package/dist/rooms/project-state.d.ts +41 -0
  55. package/dist/rooms/project-state.js +249 -0
  56. package/dist/rooms/project-state.js.map +1 -0
  57. package/dist/rooms/proposals.d.ts +63 -0
  58. package/dist/rooms/proposals.js +254 -0
  59. package/dist/rooms/proposals.js.map +1 -0
  60. package/dist/rooms/replay.d.ts +13 -0
  61. package/dist/rooms/replay.js +19 -0
  62. package/dist/rooms/replay.js.map +1 -0
  63. package/dist/rooms/room-reference.d.ts +21 -0
  64. package/dist/rooms/room-reference.js +142 -0
  65. package/dist/rooms/room-reference.js.map +1 -0
  66. package/dist/rooms/timeline.d.ts +26 -0
  67. package/dist/rooms/timeline.js +68 -0
  68. package/dist/rooms/timeline.js.map +1 -0
  69. package/package.json +35 -0
  70. package/skills/fold/SKILL.md +81 -0
  71. package/skills/fold/agents/openai.yaml +4 -0
  72. package/skills/fold/references/cli.md +33 -0
  73. package/skills/fold/references/security.md +14 -0
  74. package/skills/fold/references/workflow.md +48 -0
@@ -0,0 +1,59 @@
1
+ import { normalizeServerUrl } from '../rooms/room-reference.js';
2
+ export function resolvePublicOrigin(options) {
3
+ const env = options.env ?? process.env;
4
+ const providerUrl = publicOriginFromProviderEnv(env);
5
+ const appUrl = options.appUrl
6
+ ?? options.serverUrl
7
+ ?? options.syncUrl
8
+ ?? env.FOLD_PUBLIC_APP_URL
9
+ ?? env.FOLD_PUBLIC_URL
10
+ ?? providerUrl
11
+ ?? options.defaultUrl;
12
+ const syncUrl = options.syncUrl
13
+ ?? options.serverUrl
14
+ ?? options.appUrl
15
+ ?? env.FOLD_PUBLIC_SYNC_URL
16
+ ?? env.FOLD_PUBLIC_URL
17
+ ?? providerUrl
18
+ ?? options.defaultUrl;
19
+ return {
20
+ appUrl: normalizeServerUrl(appUrl),
21
+ syncUrl: normalizeServerUrl(syncUrl),
22
+ source: publicOriginSource(options, env, providerUrl),
23
+ };
24
+ }
25
+ export function publicOriginFromProviderEnv(env = process.env) {
26
+ return firstPresent(env.RENDER_EXTERNAL_URL, env.URL, env.DEPLOY_PRIME_URL, withHttps(env.RAILWAY_PUBLIC_DOMAIN), withHttps(env.VERCEL_URL), withHttps(env.FLY_APP_NAME ? `${env.FLY_APP_NAME}.fly.dev` : undefined));
27
+ }
28
+ export function hostedPortFromEnv(env = process.env, fallback = 3000) {
29
+ const raw = env.PORT;
30
+ if (!raw)
31
+ return fallback;
32
+ const port = Number(raw);
33
+ if (!Number.isInteger(port) || port < 0 || port > 65535) {
34
+ throw new Error(`Invalid PORT value: ${raw}`);
35
+ }
36
+ return port;
37
+ }
38
+ function firstPresent(...values) {
39
+ return values.find((value) => value && value.trim().length > 0);
40
+ }
41
+ function withHttps(value) {
42
+ if (!value)
43
+ return undefined;
44
+ if (/^https?:\/\//.test(value))
45
+ return value;
46
+ return `https://${value}`;
47
+ }
48
+ function publicOriginSource(options, env, providerUrl) {
49
+ if (options.appUrl || options.syncUrl || options.serverUrl)
50
+ return 'explicit';
51
+ if (env.FOLD_PUBLIC_APP_URL || env.FOLD_PUBLIC_SYNC_URL)
52
+ return 'split-environment';
53
+ if (env.FOLD_PUBLIC_URL)
54
+ return 'fold-public-url';
55
+ if (providerUrl)
56
+ return 'provider';
57
+ return 'default';
58
+ }
59
+ //# sourceMappingURL=public-origin.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"public-origin.js","sourceRoot":"","sources":["../../../../src/deploy/public-origin.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,16 @@
1
+ import type { EncryptedUpdateRecord, IncomingEncryptedUpdate } from '../server/append-log.js';
2
+ import type { RoomAccess } from './room-reference.js';
3
+ export interface RoomStatusResponse {
4
+ roomId: string;
5
+ recordCount: number;
6
+ latestSeq: number | null;
7
+ }
8
+ export interface AppendEncryptedUpdateResponse {
9
+ record: EncryptedUpdateRecord;
10
+ }
11
+ export interface ListEncryptedUpdatesResponse {
12
+ updates: EncryptedUpdateRecord[];
13
+ }
14
+ export declare function appendEncryptedUpdate(access: RoomAccess, update: IncomingEncryptedUpdate): Promise<EncryptedUpdateRecord>;
15
+ export declare function listEncryptedUpdates(access: RoomAccess): Promise<EncryptedUpdateRecord[]>;
16
+ export declare function fetchRoomStatus(access: RoomAccess): Promise<RoomStatusResponse>;
@@ -0,0 +1,72 @@
1
+ export async function appendEncryptedUpdate(access, update) {
2
+ const response = await fetch(roomApiUrl(access, 'updates'), {
3
+ method: 'POST',
4
+ headers: { 'content-type': 'application/json' },
5
+ body: JSON.stringify({ update }),
6
+ });
7
+ const json = await readJsonResponse(response);
8
+ if (!isEncryptedUpdateRecord(json.record)) {
9
+ throw new Error('Server returned an invalid encrypted append-log record');
10
+ }
11
+ return json.record;
12
+ }
13
+ export async function listEncryptedUpdates(access) {
14
+ const response = await fetch(roomApiUrl(access, 'updates'));
15
+ const json = await readJsonResponse(response);
16
+ if (!Array.isArray(json.updates) || !json.updates.every(isEncryptedUpdateRecord)) {
17
+ throw new Error('Server returned invalid encrypted append-log updates');
18
+ }
19
+ return json.updates;
20
+ }
21
+ export async function fetchRoomStatus(access) {
22
+ const response = await fetch(roomApiUrl(access, 'status'));
23
+ const json = await readJsonResponse(response);
24
+ if (!isRoomStatusResponse(json)) {
25
+ throw new Error('Server returned invalid room status');
26
+ }
27
+ return json;
28
+ }
29
+ function roomApiUrl(access, endpoint) {
30
+ const base = new URL(access.syncUrl ?? access.serverUrl);
31
+ const basePath = base.pathname.replace(/\/+$/, '');
32
+ base.pathname = `${basePath}/rooms/${encodeURIComponent(access.roomId)}/${endpoint}`;
33
+ base.search = '';
34
+ base.hash = '';
35
+ return base.toString();
36
+ }
37
+ async function readJsonResponse(response) {
38
+ const text = await response.text();
39
+ if (!response.ok) {
40
+ throw new Error(`Server returned ${response.status}: ${text}`);
41
+ }
42
+ try {
43
+ return JSON.parse(text);
44
+ }
45
+ catch (error) {
46
+ throw new Error('Server returned invalid JSON', { cause: error });
47
+ }
48
+ }
49
+ function isEncryptedUpdateRecord(value) {
50
+ if (!value || typeof value !== 'object')
51
+ return false;
52
+ const record = value;
53
+ return (typeof record.roomId === 'string' &&
54
+ Number.isSafeInteger(record.seq) &&
55
+ typeof record.senderId === 'string' &&
56
+ isEncryptedPayload(record));
57
+ }
58
+ function isEncryptedPayload(value) {
59
+ if (!value || typeof value !== 'object')
60
+ return false;
61
+ const payload = value;
62
+ return typeof payload.nonce === 'string' && typeof payload.ciphertext === 'string';
63
+ }
64
+ function isRoomStatusResponse(value) {
65
+ if (!value || typeof value !== 'object')
66
+ return false;
67
+ const status = value;
68
+ return (typeof status.roomId === 'string' &&
69
+ Number.isSafeInteger(status.recordCount) &&
70
+ (status.latestSeq === null || Number.isSafeInteger(status.latestSeq)));
71
+ }
72
+ //# sourceMappingURL=append-log-api.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"append-log-api.js","sourceRoot":"","sources":["../../../../src/rooms/append-log-api.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ import type { EncryptedUpdateRecord } from '../server/append-log.js';
2
+ export declare function assertContiguousRecords(records: EncryptedUpdateRecord[], roomId: string): void;
@@ -0,0 +1,16 @@
1
+ export function assertContiguousRecords(records, roomId) {
2
+ let expectedSeq = 1;
3
+ for (const record of records) {
4
+ if (record.roomId !== roomId) {
5
+ throw new Error(`Received update for unexpected room ${JSON.stringify(record.roomId)}`);
6
+ }
7
+ if (!Number.isSafeInteger(record.seq) || record.seq < 1) {
8
+ throw new Error(`Received invalid append-log sequence ${record.seq}`);
9
+ }
10
+ if (record.seq !== expectedSeq) {
11
+ throw new Error(`Detected missing, duplicate, or reordered append-log sequence ${record.seq}; expected ${expectedSeq}`);
12
+ }
13
+ expectedSeq += 1;
14
+ }
15
+ }
16
+ //# sourceMappingURL=append-log-validation.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"append-log-validation.js","sourceRoot":"","sources":["../../../../src/rooms/append-log-validation.ts"],"names":[],"mappings":"AAEA,MAAM,UAAU,uBAAuB,CAAC,OAAgC,EAAE,MAAc;IACtF,IAAI,WAAW,GAAG,CAAC,CAAC;IACpB,KAAK,MAAM,MAAM,IAAI,OAAO,EAAE,CAAC;QAC7B,IAAI,MAAM,CAAC,MAAM,KAAK,MAAM,EAAE,CAAC;YAC7B,MAAM,IAAI,KAAK,CAAC,uCAAuC,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;QAC1F,CAAC;QAED,IAAI,CAAC,MAAM,CAAC,aAAa,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,MAAM,CAAC,GAAG,GAAG,CAAC,EAAE,CAAC;YACxD,MAAM,IAAI,KAAK,CAAC,wCAAwC,MAAM,CAAC,GAAG,EAAE,CAAC,CAAC;QACxE,CAAC;QAED,IAAI,MAAM,CAAC,GAAG,KAAK,WAAW,EAAE,CAAC;YAC/B,MAAM,IAAI,KAAK,CAAC,iEAAiE,MAAM,CAAC,GAAG,cAAc,WAAW,EAAE,CAAC,CAAC;QAC1H,CAAC;QAED,WAAW,IAAI,CAAC,CAAC;IACnB,CAAC;AACH,CAAC"}
@@ -0,0 +1,63 @@
1
+ import type { EncryptedUpdateRecord, IncomingEncryptedUpdate } from '../server/append-log.js';
2
+ import type { RoomPersona } from './personas.js';
3
+ import type { RoomAccess } from './room-reference.js';
4
+ export declare const COMMENT_EVENT_SENDER_ID_PREFIX = "web-client:comment-event";
5
+ export declare const COMMENT_SENDER_ID_PREFIX = "web-client:comment";
6
+ export declare const CLI_COMMENT_EVENT_SENDER_ID_PREFIX = "fold-cli:comment-event";
7
+ export declare const CLI_COMMENT_SENDER_ID_PREFIX = "fold-cli:comment";
8
+ export type ThreadAnchorType = 'text-range' | 'insertion-point' | 'block' | 'document';
9
+ export type ThreadType = 'note' | 'request';
10
+ export interface CommentReply {
11
+ id: string;
12
+ authorPersonaId: string;
13
+ persona: RoomPersona;
14
+ text: string;
15
+ createdAt: string;
16
+ parentId?: string;
17
+ parentAuthorPersonaId?: string;
18
+ parentAuthorName?: string;
19
+ parentText?: string;
20
+ }
21
+ export interface RoomComment {
22
+ id: string;
23
+ authorPersonaId: string;
24
+ persona: RoomPersona;
25
+ filePath?: string;
26
+ text: string;
27
+ replies?: CommentReply[];
28
+ createdAt: string;
29
+ resolvedAt?: string;
30
+ resolvedByPersonaId?: string;
31
+ type: ThreadType;
32
+ anchorType?: ThreadAnchorType;
33
+ selectedQuote?: string;
34
+ createdFromMarkdown?: string;
35
+ beforeContext?: string;
36
+ afterContext?: string;
37
+ }
38
+ export interface CommentEvent {
39
+ id: string;
40
+ type: 'comment_replied' | 'comment_resolved' | 'comment_reopened';
41
+ createdAt: string;
42
+ actorPersonaId: string;
43
+ message: string;
44
+ commentId: string;
45
+ filePath?: string;
46
+ reply?: CommentReply;
47
+ }
48
+ export declare function createEncryptedCommentRecord(access: RoomAccess, comment: RoomComment): Promise<IncomingEncryptedUpdate>;
49
+ export declare function createEncryptedCommentEvent(access: RoomAccess, event: CommentEvent): Promise<IncomingEncryptedUpdate>;
50
+ export declare function createComment(input: {
51
+ persona: RoomPersona;
52
+ text: string;
53
+ markdown: string;
54
+ filePath: string;
55
+ selectedQuote?: string;
56
+ type?: ThreadType;
57
+ }): RoomComment;
58
+ export declare function createCommentReplyEvent(input: {
59
+ comment: RoomComment;
60
+ persona: RoomPersona;
61
+ text: string;
62
+ }): CommentEvent;
63
+ export declare function replayCommentsFromRecords(access: RoomAccess, records: EncryptedUpdateRecord[]): Promise<RoomComment[]>;
@@ -0,0 +1,136 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { decryptJsonRecord, encryptJsonRecord } from './encrypted-records.js';
3
+ import { assertContiguousRecords } from './append-log-validation.js';
4
+ export const COMMENT_EVENT_SENDER_ID_PREFIX = 'web-client:comment-event';
5
+ export const COMMENT_SENDER_ID_PREFIX = 'web-client:comment';
6
+ export const CLI_COMMENT_EVENT_SENDER_ID_PREFIX = 'fold-cli:comment-event';
7
+ export const CLI_COMMENT_SENDER_ID_PREFIX = 'fold-cli:comment';
8
+ export async function createEncryptedCommentRecord(access, comment) {
9
+ return encryptJsonRecord(access, `${CLI_COMMENT_SENDER_ID_PREFIX}:${comment.id}`, comment);
10
+ }
11
+ export async function createEncryptedCommentEvent(access, event) {
12
+ return encryptJsonRecord(access, `${CLI_COMMENT_EVENT_SENDER_ID_PREFIX}:${event.id}`, event);
13
+ }
14
+ export function createComment(input) {
15
+ return {
16
+ id: randomUUID().slice(0, 12),
17
+ authorPersonaId: input.persona.id,
18
+ persona: input.persona,
19
+ filePath: input.filePath,
20
+ text: input.text,
21
+ createdAt: new Date().toISOString(),
22
+ type: input.type ?? 'note',
23
+ ...createCommentAnchor(input.markdown, input.selectedQuote ?? ''),
24
+ };
25
+ }
26
+ export function createCommentReplyEvent(input) {
27
+ const createdAt = new Date().toISOString();
28
+ const reply = {
29
+ id: randomUUID().slice(0, 12),
30
+ authorPersonaId: input.persona.id,
31
+ persona: input.persona,
32
+ text: input.text,
33
+ createdAt,
34
+ };
35
+ return {
36
+ id: `ev-comment-reply-${input.comment.id}-${reply.id}`,
37
+ type: 'comment_replied',
38
+ createdAt,
39
+ actorPersonaId: input.persona.id,
40
+ commentId: input.comment.id,
41
+ filePath: input.comment.filePath,
42
+ message: `Replied to ${input.comment.type === 'request' ? 'request' : 'comment'} on ${input.comment.selectedQuote || input.comment.filePath || 'document'}`,
43
+ reply,
44
+ };
45
+ }
46
+ export async function replayCommentsFromRecords(access, records) {
47
+ assertContiguousRecords(records, access.roomId);
48
+ let comments = [];
49
+ for (const record of records) {
50
+ if (isCommentEventSender(record.senderId)) {
51
+ const value = await decryptJsonRecord(access, record, record.senderId);
52
+ if (isCommentEvent(value))
53
+ comments = applyCommentEvent(comments, value);
54
+ continue;
55
+ }
56
+ if (isCommentSender(record.senderId)) {
57
+ const value = await decryptJsonRecord(access, record, record.senderId);
58
+ if (isRoomComment(value))
59
+ comments = upsertComment(comments, value);
60
+ }
61
+ }
62
+ return comments.sort((left, right) => right.createdAt.localeCompare(left.createdAt));
63
+ }
64
+ function upsertComment(comments, next) {
65
+ if (comments.some((comment) => comment.id === next.id))
66
+ return comments;
67
+ return [{ ...next, replies: sortReplies(next.replies || []) }, ...comments];
68
+ }
69
+ function applyCommentEvent(comments, event) {
70
+ return comments.map((comment) => {
71
+ if (comment.id !== event.commentId)
72
+ return comment;
73
+ if (event.type === 'comment_replied' && event.reply) {
74
+ const replies = comment.replies || [];
75
+ if (replies.some((reply) => reply.id === event.reply?.id))
76
+ return comment;
77
+ return { ...comment, replies: sortReplies([...replies, event.reply]) };
78
+ }
79
+ if (event.type === 'comment_resolved') {
80
+ return { ...comment, resolvedAt: event.createdAt, resolvedByPersonaId: event.actorPersonaId };
81
+ }
82
+ if (event.type === 'comment_reopened') {
83
+ const { resolvedAt, resolvedByPersonaId, ...reopened } = comment;
84
+ return reopened;
85
+ }
86
+ return comment;
87
+ });
88
+ }
89
+ function createCommentAnchor(markdown, selectedQuote) {
90
+ const quote = selectedQuote.trim();
91
+ if (!quote)
92
+ return { anchorType: 'document', createdFromMarkdown: markdown.slice(0, 320) };
93
+ const index = markdown.indexOf(quote);
94
+ if (index === -1) {
95
+ return { anchorType: 'text-range', selectedQuote: quote, createdFromMarkdown: markdown.slice(0, 320) };
96
+ }
97
+ return {
98
+ anchorType: 'text-range',
99
+ selectedQuote: quote,
100
+ createdFromMarkdown: markdown.slice(Math.max(0, index - 160), index + quote.length + 160),
101
+ beforeContext: markdown.slice(Math.max(0, index - 120), index),
102
+ afterContext: markdown.slice(index + quote.length, index + quote.length + 120),
103
+ };
104
+ }
105
+ function sortReplies(replies) {
106
+ return [...replies].sort((left, right) => left.createdAt.localeCompare(right.createdAt));
107
+ }
108
+ function isRoomComment(value) {
109
+ if (!value || typeof value !== 'object')
110
+ return false;
111
+ const candidate = value;
112
+ return typeof candidate.id === 'string'
113
+ && typeof candidate.authorPersonaId === 'string'
114
+ && candidate.persona?.schema === 'fold.persona.v1'
115
+ && typeof candidate.text === 'string'
116
+ && typeof candidate.createdAt === 'string'
117
+ && (candidate.type === 'note' || candidate.type === 'request');
118
+ }
119
+ function isCommentSender(senderId) {
120
+ return senderId.startsWith(COMMENT_SENDER_ID_PREFIX) || senderId.startsWith(CLI_COMMENT_SENDER_ID_PREFIX);
121
+ }
122
+ function isCommentEventSender(senderId) {
123
+ return senderId.startsWith(COMMENT_EVENT_SENDER_ID_PREFIX) || senderId.startsWith(CLI_COMMENT_EVENT_SENDER_ID_PREFIX);
124
+ }
125
+ function isCommentEvent(value) {
126
+ if (!value || typeof value !== 'object')
127
+ return false;
128
+ const candidate = value;
129
+ return typeof candidate.id === 'string'
130
+ && (candidate.type === 'comment_replied' || candidate.type === 'comment_resolved' || candidate.type === 'comment_reopened')
131
+ && typeof candidate.createdAt === 'string'
132
+ && typeof candidate.actorPersonaId === 'string'
133
+ && typeof candidate.commentId === 'string'
134
+ && typeof candidate.message === 'string';
135
+ }
136
+ //# sourceMappingURL=comments.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"comments.js","sourceRoot":"","sources":["../../../../src/rooms/comments.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,11 @@
1
+ export interface EncryptedPayload {
2
+ nonce: string;
3
+ ciphertext: string;
4
+ }
5
+ export interface EncryptedUpdateMetadata {
6
+ roomId: string;
7
+ senderId: string;
8
+ }
9
+ export declare function deriveRoomKey(roomId: string, roomSecret: string): Promise<CryptoKey>;
10
+ export declare function encryptUpdate(update: Uint8Array, key: CryptoKey, metadata: EncryptedUpdateMetadata): Promise<EncryptedPayload>;
11
+ export declare function decryptUpdate(payload: EncryptedPayload, key: CryptoKey, metadata: EncryptedUpdateMetadata): Promise<Uint8Array>;
@@ -0,0 +1,44 @@
1
+ import { webcrypto } from 'node:crypto';
2
+ const encoder = new TextEncoder();
3
+ export async function deriveRoomKey(roomId, roomSecret) {
4
+ const inputKey = await webcrypto.subtle.importKey('raw', encoder.encode(roomSecret), 'HKDF', false, ['deriveKey']);
5
+ return webcrypto.subtle.deriveKey({
6
+ name: 'HKDF',
7
+ hash: 'SHA-256',
8
+ salt: encoder.encode(`fold:${roomId}`),
9
+ info: encoder.encode('yjs-update-append-log:v1'),
10
+ }, inputKey, { name: 'AES-GCM', length: 256 }, false, ['encrypt', 'decrypt']);
11
+ }
12
+ export async function encryptUpdate(update, key, metadata) {
13
+ const nonce = webcrypto.getRandomValues(new Uint8Array(12));
14
+ const ciphertext = await webcrypto.subtle.encrypt({
15
+ name: 'AES-GCM',
16
+ iv: nonce,
17
+ additionalData: metadataAdditionalData(metadata),
18
+ }, key, update);
19
+ return {
20
+ nonce: toBase64Url(nonce),
21
+ ciphertext: toBase64Url(new Uint8Array(ciphertext)),
22
+ };
23
+ }
24
+ export async function decryptUpdate(payload, key, metadata) {
25
+ const plaintext = await webcrypto.subtle.decrypt({
26
+ name: 'AES-GCM',
27
+ iv: fromBase64Url(payload.nonce),
28
+ additionalData: metadataAdditionalData(metadata),
29
+ }, key, fromBase64Url(payload.ciphertext));
30
+ return new Uint8Array(plaintext);
31
+ }
32
+ function metadataAdditionalData(metadata) {
33
+ return encoder.encode(JSON.stringify({
34
+ roomId: metadata.roomId,
35
+ senderId: metadata.senderId,
36
+ }));
37
+ }
38
+ function toBase64Url(bytes) {
39
+ return Buffer.from(bytes).toString('base64url');
40
+ }
41
+ function fromBase64Url(value) {
42
+ return new Uint8Array(Buffer.from(value, 'base64url'));
43
+ }
44
+ //# sourceMappingURL=crypto.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"crypto.js","sourceRoot":"","sources":["../../../../src/rooms/crypto.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAExC,MAAM,OAAO,GAAG,IAAI,WAAW,EAAE,CAAC;AAYlC,MAAM,CAAC,KAAK,UAAU,aAAa,CAAC,MAAc,EAAE,UAAkB;IACpE,MAAM,QAAQ,GAAG,MAAM,SAAS,CAAC,MAAM,CAAC,SAAS,CAC/C,KAAK,EACL,OAAO,CAAC,MAAM,CAAC,UAAU,CAAC,EAC1B,MAAM,EACN,KAAK,EACL,CAAC,WAAW,CAAC,CACd,CAAC;IAEF,OAAO,SAAS,CAAC,MAAM,CAAC,SAAS,CAC/B;QACE,IAAI,EAAE,MAAM;QACZ,IAAI,EAAE,SAAS;QACf,IAAI,EAAE,OAAO,CAAC,MAAM,CAAC,QAAQ,MAAM,EAAE,CAAC;QACtC,IAAI,EAAE,OAAO,CAAC,MAAM,CAAC,0BAA0B,CAAC;KACjD,EACD,QAAQ,EACR,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,EAAE,GAAG,EAAE,EAChC,KAAK,EACL,CAAC,SAAS,EAAE,SAAS,CAAC,CACvB,CAAC;AACJ,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,aAAa,CACjC,MAAkB,EAClB,GAAc,EACd,QAAiC;IAEjC,MAAM,KAAK,GAAG,SAAS,CAAC,eAAe,CAAC,IAAI,UAAU,CAAC,EAAE,CAAC,CAAC,CAAC;IAC5D,MAAM,UAAU,GAAG,MAAM,SAAS,CAAC,MAAM,CAAC,OAAO,CAC/C;QACE,IAAI,EAAE,SAAS;QACf,EAAE,EAAE,KAAK;QACT,cAAc,EAAE,sBAAsB,CAAC,QAAQ,CAAC;KACjD,EACD,GAAG,EACH,MAAM,CACP,CAAC;IAEF,OAAO;QACL,KAAK,EAAE,WAAW,CAAC,KAAK,CAAC;QACzB,UAAU,EAAE,WAAW,CAAC,IAAI,UAAU,CAAC,UAAU,CAAC,CAAC;KACpD,CAAC;AACJ,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,aAAa,CACjC,OAAyB,EACzB,GAAc,EACd,QAAiC;IAEjC,MAAM,SAAS,GAAG,MAAM,SAAS,CAAC,MAAM,CAAC,OAAO,CAC9C;QACE,IAAI,EAAE,SAAS;QACf,EAAE,EAAE,aAAa,CAAC,OAAO,CAAC,KAAK,CAAC;QAChC,cAAc,EAAE,sBAAsB,CAAC,QAAQ,CAAC;KACjD,EACD,GAAG,EACH,aAAa,CAAC,OAAO,CAAC,UAAU,CAAC,CAClC,CAAC;IAEF,OAAO,IAAI,UAAU,CAAC,SAAS,CAAC,CAAC;AACnC,CAAC;AAED,SAAS,sBAAsB,CAAC,QAAiC;IAC/D,OAAO,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC;QACnC,MAAM,EAAE,QAAQ,CAAC,MAAM;QACvB,QAAQ,EAAE,QAAQ,CAAC,QAAQ;KAC5B,CAAC,CAAC,CAAC;AACN,CAAC;AAED,SAAS,WAAW,CAAC,KAAiB;IACpC,OAAO,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC;AAClD,CAAC;AAED,SAAS,aAAa,CAAC,KAAa;IAClC,OAAO,IAAI,UAAU,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,EAAE,WAAW,CAAC,CAAC,CAAC;AACzD,CAAC"}
@@ -0,0 +1,5 @@
1
+ import type { IncomingEncryptedUpdate } from '../server/append-log.js';
2
+ import { type EncryptedPayload } from './crypto.js';
3
+ import type { RoomAccess } from './room-reference.js';
4
+ export declare function encryptJsonRecord(access: RoomAccess, senderId: string, value: unknown): Promise<IncomingEncryptedUpdate>;
5
+ export declare function decryptJsonRecord(access: RoomAccess, payload: EncryptedPayload, senderId: string): Promise<unknown>;
@@ -0,0 +1,21 @@
1
+ import { decryptUpdate, deriveRoomKey, encryptUpdate } from './crypto.js';
2
+ export async function encryptJsonRecord(access, senderId, value) {
3
+ const roomKey = await deriveRoomKey(access.roomId, access.roomSecret);
4
+ const encrypted = await encryptUpdate(Buffer.from(JSON.stringify(value), 'utf8'), roomKey, {
5
+ roomId: access.roomId,
6
+ senderId,
7
+ });
8
+ return {
9
+ senderId,
10
+ ...encrypted,
11
+ };
12
+ }
13
+ export async function decryptJsonRecord(access, payload, senderId) {
14
+ const roomKey = await deriveRoomKey(access.roomId, access.roomSecret);
15
+ const bytes = await decryptUpdate(payload, roomKey, {
16
+ roomId: access.roomId,
17
+ senderId,
18
+ });
19
+ return JSON.parse(Buffer.from(bytes).toString('utf8'));
20
+ }
21
+ //# sourceMappingURL=encrypted-records.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"encrypted-records.js","sourceRoot":"","sources":["../../../../src/rooms/encrypted-records.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,aAAa,EAAE,aAAa,EAAE,aAAa,EAAyB,MAAM,aAAa,CAAC;AAGjG,MAAM,CAAC,KAAK,UAAU,iBAAiB,CACrC,MAAkB,EAClB,QAAgB,EAChB,KAAc;IAEd,MAAM,OAAO,GAAG,MAAM,aAAa,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,UAAU,CAAC,CAAC;IACtE,MAAM,SAAS,GAAG,MAAM,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC,EAAE,OAAO,EAAE;QACzF,MAAM,EAAE,MAAM,CAAC,MAAM;QACrB,QAAQ;KACT,CAAC,CAAC;IACH,OAAO;QACL,QAAQ;QACR,GAAG,SAAS;KACb,CAAC;AACJ,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,iBAAiB,CACrC,MAAkB,EAClB,OAAyB,EACzB,QAAgB;IAEhB,MAAM,OAAO,GAAG,MAAM,aAAa,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,UAAU,CAAC,CAAC;IACtE,MAAM,KAAK,GAAG,MAAM,aAAa,CAAC,OAAO,EAAE,OAAO,EAAE;QAClD,MAAM,EAAE,MAAM,CAAC,MAAM;QACrB,QAAQ;KACT,CAAC,CAAC;IACH,OAAO,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAY,CAAC;AACpE,CAAC"}
@@ -0,0 +1,23 @@
1
+ import { type EncryptedPayload } from './crypto.js';
2
+ import type { EncryptedUpdateRecord, IncomingEncryptedUpdate } from '../server/append-log.js';
3
+ import type { RoomAccess } from './room-reference.js';
4
+ export declare const MARKDOWN_YTEXT_NAME = "markdown";
5
+ export declare const MARKDOWN_CANONICAL = "y.text:markdown";
6
+ export declare const ENCRYPTED_SNAPSHOT_FORMAT = "encrypted-yjs-update-v1";
7
+ export declare const DOCUMENT_UPDATE_SENDER_ID = "fold-cli:document";
8
+ export interface EncryptedMarkdownSnapshot extends EncryptedPayload {
9
+ format: typeof ENCRYPTED_SNAPSHOT_FORMAT;
10
+ senderId: string;
11
+ }
12
+ export interface MarkdownDocumentSummary {
13
+ canonical: typeof MARKDOWN_CANONICAL;
14
+ bytes: number;
15
+ sha256: string;
16
+ }
17
+ export declare function createEncryptedMarkdownSnapshot(markdown: string, access: RoomAccess, senderId: string): Promise<EncryptedMarkdownSnapshot>;
18
+ export declare function createEncryptedMarkdownUpdate(markdown: string, access: RoomAccess, senderId?: string): Promise<IncomingEncryptedUpdate>;
19
+ export declare function createEncryptedMarkdownReplacementUpdate(currentMarkdown: string, replacementMarkdown: string, access: RoomAccess, senderId?: string): Promise<IncomingEncryptedUpdate>;
20
+ export declare function createEncryptedMarkdownReplacementUpdateFromRecords(records: EncryptedUpdateRecord[], replacementMarkdown: string, access: RoomAccess, senderId?: string): Promise<IncomingEncryptedUpdate>;
21
+ export declare function decryptMarkdownSnapshot(snapshot: EncryptedMarkdownSnapshot, access: RoomAccess): Promise<string>;
22
+ export declare function decryptMarkdownFromRecords(records: EncryptedUpdateRecord[], access: RoomAccess): Promise<string>;
23
+ export declare function summarizeMarkdown(markdown: string): MarkdownDocumentSummary;
@@ -0,0 +1,126 @@
1
+ import { createHash } from 'node:crypto';
2
+ import * as Y from 'yjs';
3
+ import { decryptUpdate, deriveRoomKey, encryptUpdate, } from './crypto.js';
4
+ import { assertContiguousRecords } from './append-log-validation.js';
5
+ export const MARKDOWN_YTEXT_NAME = 'markdown';
6
+ export const MARKDOWN_CANONICAL = 'y.text:markdown';
7
+ export const ENCRYPTED_SNAPSHOT_FORMAT = 'encrypted-yjs-update-v1';
8
+ export const DOCUMENT_UPDATE_SENDER_ID = 'fold-cli:document';
9
+ export async function createEncryptedMarkdownSnapshot(markdown, access, senderId) {
10
+ const encrypted = await createEncryptedMarkdownUpdate(markdown, access, senderId);
11
+ return {
12
+ format: ENCRYPTED_SNAPSHOT_FORMAT,
13
+ senderId,
14
+ nonce: encrypted.nonce,
15
+ ciphertext: encrypted.ciphertext,
16
+ };
17
+ }
18
+ export async function createEncryptedMarkdownUpdate(markdown, access, senderId = DOCUMENT_UPDATE_SENDER_ID) {
19
+ const doc = new Y.Doc();
20
+ try {
21
+ doc.getText(MARKDOWN_YTEXT_NAME).insert(0, markdown);
22
+ const update = Y.encodeStateAsUpdate(doc);
23
+ return encryptMarkdownYjsUpdate(update, access, senderId);
24
+ }
25
+ finally {
26
+ doc.destroy();
27
+ }
28
+ }
29
+ export async function createEncryptedMarkdownReplacementUpdate(currentMarkdown, replacementMarkdown, access, senderId = DOCUMENT_UPDATE_SENDER_ID) {
30
+ const doc = new Y.Doc();
31
+ try {
32
+ const text = doc.getText(MARKDOWN_YTEXT_NAME);
33
+ text.insert(0, currentMarkdown);
34
+ const beforeReplacement = Y.encodeStateVector(doc);
35
+ text.delete(0, text.length);
36
+ text.insert(0, replacementMarkdown);
37
+ const update = Y.encodeStateAsUpdate(doc, beforeReplacement);
38
+ return encryptMarkdownYjsUpdate(update, access, senderId);
39
+ }
40
+ finally {
41
+ doc.destroy();
42
+ }
43
+ }
44
+ export async function createEncryptedMarkdownReplacementUpdateFromRecords(records, replacementMarkdown, access, senderId = DOCUMENT_UPDATE_SENDER_ID) {
45
+ const doc = new Y.Doc();
46
+ try {
47
+ assertContiguousRecords(records, access.roomId);
48
+ const roomKey = await deriveRoomKey(access.roomId, access.roomSecret);
49
+ for (const record of records) {
50
+ if (record.senderId !== DOCUMENT_UPDATE_SENDER_ID)
51
+ continue;
52
+ const update = await decryptUpdate(record, roomKey, {
53
+ roomId: record.roomId,
54
+ senderId: record.senderId,
55
+ });
56
+ Y.applyUpdate(doc, update, 'server-replacement');
57
+ }
58
+ const text = doc.getText(MARKDOWN_YTEXT_NAME);
59
+ const beforeReplacement = Y.encodeStateVector(doc);
60
+ text.delete(0, text.length);
61
+ text.insert(0, replacementMarkdown);
62
+ const update = Y.encodeStateAsUpdate(doc, beforeReplacement);
63
+ return encryptMarkdownYjsUpdate(update, access, senderId);
64
+ }
65
+ finally {
66
+ doc.destroy();
67
+ }
68
+ }
69
+ export async function decryptMarkdownSnapshot(snapshot, access) {
70
+ if (snapshot.format !== ENCRYPTED_SNAPSHOT_FORMAT) {
71
+ throw new Error(`Unsupported encrypted snapshot format ${JSON.stringify(snapshot.format)}`);
72
+ }
73
+ const doc = new Y.Doc();
74
+ try {
75
+ const roomKey = await deriveRoomKey(access.roomId, access.roomSecret);
76
+ const update = await decryptUpdate(snapshot, roomKey, {
77
+ roomId: access.roomId,
78
+ senderId: snapshot.senderId,
79
+ });
80
+ Y.applyUpdate(doc, update, 'local-export');
81
+ return doc.getText(MARKDOWN_YTEXT_NAME).toString();
82
+ }
83
+ finally {
84
+ doc.destroy();
85
+ }
86
+ }
87
+ export async function decryptMarkdownFromRecords(records, access) {
88
+ const doc = new Y.Doc();
89
+ try {
90
+ assertContiguousRecords(records, access.roomId);
91
+ const roomKey = await deriveRoomKey(access.roomId, access.roomSecret);
92
+ for (const record of records) {
93
+ if (record.senderId !== DOCUMENT_UPDATE_SENDER_ID)
94
+ continue;
95
+ const update = await decryptUpdate(record, roomKey, {
96
+ roomId: record.roomId,
97
+ senderId: record.senderId,
98
+ });
99
+ Y.applyUpdate(doc, update, 'server-export');
100
+ }
101
+ return doc.getText(MARKDOWN_YTEXT_NAME).toString();
102
+ }
103
+ finally {
104
+ doc.destroy();
105
+ }
106
+ }
107
+ export function summarizeMarkdown(markdown) {
108
+ const bytes = Buffer.byteLength(markdown, 'utf8');
109
+ return {
110
+ canonical: MARKDOWN_CANONICAL,
111
+ bytes,
112
+ sha256: createHash('sha256').update(markdown).digest('hex'),
113
+ };
114
+ }
115
+ async function encryptMarkdownYjsUpdate(update, access, senderId) {
116
+ const roomKey = await deriveRoomKey(access.roomId, access.roomSecret);
117
+ const encrypted = await encryptUpdate(update, roomKey, {
118
+ roomId: access.roomId,
119
+ senderId,
120
+ });
121
+ return {
122
+ senderId,
123
+ ...encrypted,
124
+ };
125
+ }
126
+ //# sourceMappingURL=markdown-snapshot.js.map