synomem 0.9.6 → 0.10.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/CHANGELOG.md +9 -0
- package/README.md +5 -3
- package/dist/actor-directory.d.ts +34 -0
- package/dist/actor-directory.d.ts.map +1 -0
- package/dist/actor-directory.js +52 -0
- package/dist/actor-directory.js.map +1 -0
- package/dist/backend.d.ts +3 -0
- package/dist/backend.d.ts.map +1 -1
- package/dist/backend.js +26 -3
- package/dist/backend.js.map +1 -1
- package/dist/bookmarks.d.ts +30 -0
- package/dist/bookmarks.d.ts.map +1 -0
- package/dist/bookmarks.js +87 -0
- package/dist/bookmarks.js.map +1 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +291 -61
- package/dist/cli.js.map +1 -1
- package/dist/client.d.ts +115 -4
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +854 -138
- package/dist/client.js.map +1 -1
- package/dist/cursors.d.ts +30 -0
- package/dist/cursors.d.ts.map +1 -0
- package/dist/cursors.js +101 -0
- package/dist/cursors.js.map +1 -0
- package/dist/errors.d.ts +1 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +2 -0
- package/dist/errors.js.map +1 -1
- package/dist/import.d.ts +605 -39
- package/dist/import.d.ts.map +1 -1
- package/dist/index.d.ts +19 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11 -1
- package/dist/index.js.map +1 -1
- package/dist/mcp/index.d.ts +1 -1
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js +329 -22
- package/dist/mcp/index.js.map +1 -1
- package/dist/mutation-receipts.d.ts +8 -0
- package/dist/mutation-receipts.d.ts.map +1 -0
- package/dist/mutation-receipts.js +2 -0
- package/dist/mutation-receipts.js.map +1 -0
- package/dist/notifications.d.ts +73 -0
- package/dist/notifications.d.ts.map +1 -0
- package/dist/notifications.js +309 -0
- package/dist/notifications.js.map +1 -0
- package/dist/participation-repository.d.ts +46 -0
- package/dist/participation-repository.d.ts.map +1 -0
- package/dist/participation-repository.js +296 -0
- package/dist/participation-repository.js.map +1 -0
- package/dist/participation.d.ts +97 -0
- package/dist/participation.d.ts.map +1 -0
- package/dist/participation.js +10 -0
- package/dist/participation.js.map +1 -0
- package/dist/policy.d.ts +31 -0
- package/dist/policy.d.ts.map +1 -0
- package/dist/policy.js +71 -0
- package/dist/policy.js.map +1 -0
- package/dist/ports/repository.d.ts +23 -0
- package/dist/ports/repository.d.ts.map +1 -1
- package/dist/profiles.d.ts +12 -0
- package/dist/profiles.d.ts.map +1 -1
- package/dist/profiles.js +2 -1
- package/dist/profiles.js.map +1 -1
- package/dist/projections.d.ts.map +1 -1
- package/dist/projections.js +29 -11
- package/dist/projections.js.map +1 -1
- package/dist/remote.d.ts +79 -0
- package/dist/remote.d.ts.map +1 -1
- package/dist/remote.js +65 -6
- package/dist/remote.js.map +1 -1
- package/dist/schemas.d.ts +746 -58
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +114 -16
- package/dist/schemas.js.map +1 -1
- package/dist/search.d.ts +25 -0
- package/dist/search.d.ts.map +1 -0
- package/dist/search.js +2 -0
- package/dist/search.js.map +1 -0
- package/dist/service.d.ts +89 -0
- package/dist/service.d.ts.map +1 -1
- package/dist/storage.d.ts +38 -3
- package/dist/storage.d.ts.map +1 -1
- package/dist/storage.js +494 -365
- package/dist/storage.js.map +1 -1
- package/dist/types.d.ts +65 -34
- package/dist/types.d.ts.map +1 -1
- package/docs/cli.md +6 -0
- package/docs/human-participation.md +20 -0
- package/docs/mcp.md +2 -0
- package/docs/storage-format.md +4 -15
- package/openapi/synomem-v1.yaml +187 -10
- package/package.json +2 -1
- package/skills/synomem/SKILL.md +2 -0
- package/src/actor-directory.ts +87 -0
- package/src/backend.ts +29 -3
- package/src/bookmarks.ts +127 -0
- package/src/cli.ts +941 -546
- package/src/client.ts +1143 -279
- package/src/cursors.ts +132 -0
- package/src/errors.ts +2 -0
- package/src/index.ts +50 -0
- package/src/mcp/index.ts +412 -96
- package/src/mutation-receipts.ts +7 -0
- package/src/notifications.ts +483 -0
- package/src/participation-repository.ts +406 -0
- package/src/participation.ts +103 -0
- package/src/policy.ts +109 -0
- package/src/ports/repository.ts +33 -0
- package/src/profiles.ts +2 -1
- package/src/projections.ts +49 -48
- package/src/remote.ts +227 -37
- package/src/schemas.ts +116 -16
- package/src/search.ts +24 -0
- package/src/service.ts +122 -8
- package/src/storage.ts +1039 -697
- package/src/types.ts +67 -32
package/src/cursors.ts
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { createHash, createHmac, timingSafeEqual } from 'node:crypto';
|
|
2
|
+
import { SynomemError } from './errors.js';
|
|
3
|
+
import type { ActorIdentity } from './types.js';
|
|
4
|
+
export interface CursorBinding {
|
|
5
|
+
purpose: string;
|
|
6
|
+
workspaceId: string;
|
|
7
|
+
actor: Pick<ActorIdentity, 'kind' | 'id'>;
|
|
8
|
+
filter: string;
|
|
9
|
+
generation?: string;
|
|
10
|
+
}
|
|
11
|
+
export interface CursorPosition {
|
|
12
|
+
sequence: string;
|
|
13
|
+
watermark: string;
|
|
14
|
+
id?: string;
|
|
15
|
+
}
|
|
16
|
+
interface CursorEnvelope extends CursorBinding, CursorPosition {
|
|
17
|
+
v: 1;
|
|
18
|
+
kid: string;
|
|
19
|
+
expiresAt: number;
|
|
20
|
+
}
|
|
21
|
+
const decimal = /^(0|[1-9][0-9]{0,18})$/;
|
|
22
|
+
const maximumSequence = 9223372036854775807n;
|
|
23
|
+
export function exactSequence(value: string | bigint | number): string {
|
|
24
|
+
if (typeof value === 'number' && !Number.isSafeInteger(value))
|
|
25
|
+
throw new SynomemError('INVALID_INPUT', 'A sequence must be an exact integer.');
|
|
26
|
+
const text = String(value);
|
|
27
|
+
if (!decimal.test(text) || BigInt(text) > maximumSequence)
|
|
28
|
+
throw new SynomemError('INVALID_INPUT', 'Invalid sequence.');
|
|
29
|
+
return text;
|
|
30
|
+
}
|
|
31
|
+
export function canonicalJson(value: unknown): string {
|
|
32
|
+
if (Array.isArray(value)) return `[${value.map(canonicalJson).join(',')}]`;
|
|
33
|
+
if (value && typeof value === 'object')
|
|
34
|
+
return `{${Object.entries(value)
|
|
35
|
+
.filter(([, entry]) => entry !== undefined)
|
|
36
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
37
|
+
.map(([key, entry]) => `${JSON.stringify(key)}:${canonicalJson(entry)}`)
|
|
38
|
+
.join(',')}}`;
|
|
39
|
+
return JSON.stringify(value) ?? 'null';
|
|
40
|
+
}
|
|
41
|
+
export function cursorFilter(value: unknown): string {
|
|
42
|
+
return createHash('sha256').update(canonicalJson(value)).digest('base64url');
|
|
43
|
+
}
|
|
44
|
+
/** Integrity codec shared by adapters; callers always recheck current record authority. */
|
|
45
|
+
export class SignedCursorCodec {
|
|
46
|
+
private readonly keys: ReadonlyMap<string, string | Uint8Array>;
|
|
47
|
+
constructor(
|
|
48
|
+
private readonly options: {
|
|
49
|
+
keyId: string;
|
|
50
|
+
key: string | Uint8Array;
|
|
51
|
+
verificationKeys?: ReadonlyMap<string, string | Uint8Array>;
|
|
52
|
+
now?: () => number;
|
|
53
|
+
},
|
|
54
|
+
) {
|
|
55
|
+
if (Buffer.byteLength(options.key) < 32)
|
|
56
|
+
throw new SynomemError('CONFIG_INVALID', 'Cursor signing keys require at least 32 bytes.');
|
|
57
|
+
this.keys = new Map([...(options.verificationKeys ?? []), [options.keyId, options.key]]);
|
|
58
|
+
}
|
|
59
|
+
encode(binding: CursorBinding, position: CursorPosition): string {
|
|
60
|
+
const envelope: CursorEnvelope = {
|
|
61
|
+
...binding,
|
|
62
|
+
...position,
|
|
63
|
+
sequence: exactSequence(position.sequence),
|
|
64
|
+
watermark: exactSequence(position.watermark),
|
|
65
|
+
v: 1,
|
|
66
|
+
kid: this.options.keyId,
|
|
67
|
+
expiresAt:
|
|
68
|
+
(this.options.now?.() ?? Date.now()) +
|
|
69
|
+
(binding.purpose.includes('changes') ? 90 * 86400_000 : 86400_000),
|
|
70
|
+
};
|
|
71
|
+
if (BigInt(envelope.sequence) > BigInt(envelope.watermark))
|
|
72
|
+
throw new SynomemError('INVALID_INPUT', 'Cursor position exceeds its watermark.');
|
|
73
|
+
const body = Buffer.from(canonicalJson(envelope)).toString('base64url');
|
|
74
|
+
return `${body}.${createHmac('sha256', this.options.key).update(body).digest('base64url')}`;
|
|
75
|
+
}
|
|
76
|
+
decode(token: string, binding: CursorBinding, maximum: string): CursorPosition {
|
|
77
|
+
try {
|
|
78
|
+
if (token.length > 4096 || !/^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]{43}$/.test(token))
|
|
79
|
+
throw new Error();
|
|
80
|
+
const [body, signature] = token.split('.') as [string, string];
|
|
81
|
+
const value = JSON.parse(Buffer.from(body, 'base64url').toString('utf8')) as CursorEnvelope;
|
|
82
|
+
const key = this.keys.get(value.kid);
|
|
83
|
+
if (!key) throw new Error();
|
|
84
|
+
const expected = createHmac('sha256', key).update(body).digest();
|
|
85
|
+
const actual = Buffer.from(signature, 'base64url');
|
|
86
|
+
if (
|
|
87
|
+
actual.toString('base64url') !== signature ||
|
|
88
|
+
actual.length !== expected.length ||
|
|
89
|
+
!timingSafeEqual(actual, expected)
|
|
90
|
+
)
|
|
91
|
+
throw new Error();
|
|
92
|
+
if (
|
|
93
|
+
value.v !== 1 ||
|
|
94
|
+
value.purpose !== binding.purpose ||
|
|
95
|
+
value.workspaceId !== binding.workspaceId ||
|
|
96
|
+
value.actor?.kind !== binding.actor.kind ||
|
|
97
|
+
value.actor?.id !== binding.actor.id ||
|
|
98
|
+
value.filter !== binding.filter ||
|
|
99
|
+
!Number.isSafeInteger(value.expiresAt)
|
|
100
|
+
)
|
|
101
|
+
throw new Error();
|
|
102
|
+
if (value.generation !== binding.generation)
|
|
103
|
+
throw new SynomemError(
|
|
104
|
+
'CURSOR_EXPIRED',
|
|
105
|
+
'This projection was rebuilt. Start from the latest page.',
|
|
106
|
+
);
|
|
107
|
+
if (value.expiresAt <= (this.options.now?.() ?? Date.now()))
|
|
108
|
+
throw new SynomemError(
|
|
109
|
+
'CURSOR_EXPIRED',
|
|
110
|
+
'This cursor expired. Refresh to synchronize again.',
|
|
111
|
+
);
|
|
112
|
+
exactSequence(value.sequence);
|
|
113
|
+
exactSequence(value.watermark);
|
|
114
|
+
exactSequence(maximum);
|
|
115
|
+
if (
|
|
116
|
+
BigInt(value.sequence) > BigInt(value.watermark) ||
|
|
117
|
+
BigInt(value.watermark) > BigInt(maximum)
|
|
118
|
+
)
|
|
119
|
+
throw new Error();
|
|
120
|
+
if (value.id !== undefined && (typeof value.id !== 'string' || value.id.length > 512))
|
|
121
|
+
throw new Error();
|
|
122
|
+
return {
|
|
123
|
+
sequence: value.sequence,
|
|
124
|
+
watermark: value.watermark,
|
|
125
|
+
...(value.id ? { id: value.id } : {}),
|
|
126
|
+
};
|
|
127
|
+
} catch (error) {
|
|
128
|
+
if (error instanceof SynomemError && error.code === 'CURSOR_EXPIRED') throw error;
|
|
129
|
+
throw new SynomemError('INVALID_INPUT', `Invalid ${binding.purpose} cursor.`);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
package/src/errors.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { ZodError } from 'zod';
|
|
|
2
2
|
|
|
3
3
|
export const errorCodes = [
|
|
4
4
|
'INVALID_INPUT',
|
|
5
|
+
'CURSOR_EXPIRED',
|
|
5
6
|
'INVALID_AGENT_ID',
|
|
6
7
|
'INVALID_EVENT',
|
|
7
8
|
'UNSUPPORTED_EVENT',
|
|
@@ -17,6 +18,7 @@ export const errorCodes = [
|
|
|
17
18
|
'TODO_NOT_FOUND',
|
|
18
19
|
'REVISION_CONFLICT',
|
|
19
20
|
'IDEMPOTENCY_CONFLICT',
|
|
21
|
+
'IDEMPOTENCY_EXPIRED',
|
|
20
22
|
'MUTATION_FORBIDDEN',
|
|
21
23
|
'SELF_AWARD_FORBIDDEN',
|
|
22
24
|
'ACKNOWLEDGMENT_FORBIDDEN',
|
package/src/index.ts
CHANGED
|
@@ -1,3 +1,14 @@
|
|
|
1
|
+
export type {
|
|
2
|
+
ActorDirectoryInput,
|
|
3
|
+
ActorDirectoryPage,
|
|
4
|
+
ActorProfile,
|
|
5
|
+
ActorProfileInput,
|
|
6
|
+
} from './actor-directory.js';
|
|
7
|
+
export {
|
|
8
|
+
actorDirectoryBinding,
|
|
9
|
+
actorDirectoryKey,
|
|
10
|
+
parseActorDirectoryKey,
|
|
11
|
+
} from './actor-directory.js';
|
|
1
12
|
export { SynomemClient, SynomemCore } from './client.js';
|
|
2
13
|
export type { SynomemCoreOptions } from './client.js';
|
|
3
14
|
export type { ProjectionWriter } from './ports/projections.js';
|
|
@@ -85,6 +96,7 @@ export {
|
|
|
85
96
|
createTaskSchema,
|
|
86
97
|
createTodoSchema,
|
|
87
98
|
eventSchema,
|
|
99
|
+
overrideTaskDecisionSchema,
|
|
88
100
|
evidenceSchema,
|
|
89
101
|
giveKudosSchema,
|
|
90
102
|
giveKudosMcpSchema,
|
|
@@ -108,3 +120,41 @@ export {
|
|
|
108
120
|
topicListInputSchema,
|
|
109
121
|
} from './schemas.js';
|
|
110
122
|
export type * from './types.js';
|
|
123
|
+
|
|
124
|
+
export {
|
|
125
|
+
recordVisibilityPredicate,
|
|
126
|
+
actorKey,
|
|
127
|
+
sameActor,
|
|
128
|
+
resolveAuthority,
|
|
129
|
+
isRecordAdministrator,
|
|
130
|
+
overseesAgent,
|
|
131
|
+
localOwnerAuthority,
|
|
132
|
+
} from './policy.js';
|
|
133
|
+
export type { ActorRef, AddressableActor, RecordAuthority } from './policy.js';
|
|
134
|
+
|
|
135
|
+
export { createLocalOwnerClient } from './client.js';
|
|
136
|
+
|
|
137
|
+
export { exportRawLocalEvents } from './backend.js';
|
|
138
|
+
|
|
139
|
+
export type { OverrideTaskDecisionInput, TaskDecisionOverriddenEvent } from './types.js';
|
|
140
|
+
|
|
141
|
+
export { SignedCursorCodec, exactSequence, canonicalJson, cursorFilter } from './cursors.js';
|
|
142
|
+
export type { CursorBinding, CursorPosition } from './cursors.js';
|
|
143
|
+
|
|
144
|
+
export type { MutationReceipt } from './mutation-receipts.js';
|
|
145
|
+
|
|
146
|
+
export * from './participation.js';
|
|
147
|
+
export * from './participation-repository.js';
|
|
148
|
+
|
|
149
|
+
export * from './notifications.js';
|
|
150
|
+
|
|
151
|
+
export type { SearchInput, SearchPage, SearchHit } from './search.js';
|
|
152
|
+
|
|
153
|
+
export { SqlBookmarkRepository, bookmarkTables } from './bookmarks.js';
|
|
154
|
+
export type { BookmarkInput, BookmarkPage, BookmarkRepository } from './bookmarks.js';
|
|
155
|
+
|
|
156
|
+
export {
|
|
157
|
+
defaultParticipationBudgets,
|
|
158
|
+
validateParticipationBudgets,
|
|
159
|
+
} from './participation-repository.js';
|
|
160
|
+
export type { ParticipationBudgets } from './participation-repository.js';
|