@enbox/dwn-sdk-js 0.3.9 → 0.4.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/dist/browser.mjs +11 -11
- package/dist/browser.mjs.map +4 -4
- package/dist/esm/generated/precompiled-validators.js +175 -512
- package/dist/esm/generated/precompiled-validators.js.map +1 -1
- package/dist/esm/src/core/dwn-error.js +1 -3
- package/dist/esm/src/core/dwn-error.js.map +1 -1
- package/dist/esm/src/core/messages-grant-authorization.js +1 -17
- package/dist/esm/src/core/messages-grant-authorization.js.map +1 -1
- package/dist/esm/src/core/protocol-authorization-validation.js +1 -1
- package/dist/esm/src/core/protocol-authorization-validation.js.map +1 -1
- package/dist/esm/src/core/replication-apply.js +200 -0
- package/dist/esm/src/core/replication-apply.js.map +1 -0
- package/dist/esm/src/dwn.js +212 -0
- package/dist/esm/src/dwn.js.map +1 -1
- package/dist/esm/src/handlers/messages-sync.js +66 -369
- package/dist/esm/src/handlers/messages-sync.js.map +1 -1
- package/dist/esm/src/index.js +1 -1
- package/dist/esm/src/index.js.map +1 -1
- package/dist/esm/src/interfaces/messages-sync.js +0 -11
- package/dist/esm/src/interfaces/messages-sync.js.map +1 -1
- package/dist/esm/tests/core/replication-apply.spec.js +220 -0
- package/dist/esm/tests/core/replication-apply.spec.js.map +1 -0
- package/dist/esm/tests/dwn.spec.js +139 -2
- package/dist/esm/tests/dwn.spec.js.map +1 -1
- package/dist/esm/tests/handlers/messages-sync.spec.js +1 -684
- package/dist/esm/tests/handlers/messages-sync.spec.js.map +1 -1
- package/dist/esm/tests/handlers/records-write.spec.js +2 -2
- package/dist/esm/tests/handlers/records-write.spec.js.map +1 -1
- package/dist/esm/tests/test-suite.js +0 -2
- package/dist/esm/tests/test-suite.js.map +1 -1
- package/dist/types/generated/precompiled-validators.d.ts.map +1 -1
- package/dist/types/src/core/dwn-error.d.ts +1 -3
- package/dist/types/src/core/dwn-error.d.ts.map +1 -1
- package/dist/types/src/core/messages-grant-authorization.d.ts +0 -1
- package/dist/types/src/core/messages-grant-authorization.d.ts.map +1 -1
- package/dist/types/src/core/replication-apply.d.ts +93 -0
- package/dist/types/src/core/replication-apply.d.ts.map +1 -0
- package/dist/types/src/dwn.d.ts +22 -1
- package/dist/types/src/dwn.d.ts.map +1 -1
- package/dist/types/src/handlers/messages-sync.d.ts +10 -54
- package/dist/types/src/handlers/messages-sync.d.ts.map +1 -1
- package/dist/types/src/index.d.ts +3 -3
- package/dist/types/src/index.d.ts.map +1 -1
- package/dist/types/src/interfaces/messages-sync.d.ts +0 -3
- package/dist/types/src/interfaces/messages-sync.d.ts.map +1 -1
- package/dist/types/src/types/messages-types.d.ts +0 -18
- package/dist/types/src/types/messages-types.d.ts.map +1 -1
- package/dist/types/tests/core/replication-apply.spec.d.ts +2 -0
- package/dist/types/tests/core/replication-apply.spec.d.ts.map +1 -0
- package/dist/types/tests/dwn.spec.d.ts.map +1 -1
- package/dist/types/tests/handlers/messages-sync.spec.d.ts.map +1 -1
- package/dist/types/tests/test-suite.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/core/dwn-error.ts +1 -3
- package/src/core/messages-grant-authorization.ts +1 -31
- package/src/core/protocol-authorization-validation.ts +2 -2
- package/src/core/replication-apply.ts +272 -0
- package/src/dwn.ts +296 -2
- package/src/handlers/messages-sync.ts +92 -585
- package/src/index.ts +3 -4
- package/src/interfaces/messages-sync.ts +8 -25
- package/src/types/messages-types.ts +0 -20
- package/dist/esm/src/sync/records-projection.js +0 -228
- package/dist/esm/src/sync/records-projection.js.map +0 -1
- package/dist/esm/tests/sync/records-projection.spec.js +0 -245
- package/dist/esm/tests/sync/records-projection.spec.js.map +0 -1
- package/dist/types/src/sync/records-projection.d.ts +0 -98
- package/dist/types/src/sync/records-projection.d.ts.map +0 -1
- package/dist/types/tests/sync/records-projection.spec.d.ts +0 -2
- package/dist/types/tests/sync/records-projection.spec.d.ts.map +0 -1
- package/src/sync/records-projection.ts +0 -328
|
@@ -74,8 +74,8 @@ export async function verifyProtocolPathAndContextId(
|
|
|
74
74
|
}
|
|
75
75
|
|
|
76
76
|
throw new DwnError(
|
|
77
|
-
DwnErrorCode.
|
|
78
|
-
`Could not find
|
|
77
|
+
DwnErrorCode.ProtocolAuthorizationParentRecordNotFound,
|
|
78
|
+
`Could not find parent record '${parentId}' to verify declared protocol path '${declaredProtocolPath}'.`
|
|
79
79
|
);
|
|
80
80
|
}
|
|
81
81
|
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
import type { GenericMessage } from '../types/message-types.js';
|
|
2
|
+
import type { ProtocolDefinition } from '../types/protocols-types.js';
|
|
3
|
+
|
|
4
|
+
import { DwnErrorCode } from './dwn-error.js';
|
|
5
|
+
import { Encoder } from '../utils/encoder.js';
|
|
6
|
+
import { Message } from './message.js';
|
|
7
|
+
import { DwnInterfaceName, DwnMethodName } from '../enums/dwn-interface-method.js';
|
|
8
|
+
import { isCrossProtocolRef, parseCrossProtocolRef } from '../utils/protocols.js';
|
|
9
|
+
|
|
10
|
+
export type ReplicationApplyOptions = {
|
|
11
|
+
dataStream?: ReadableStream<Uint8Array>;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export type ReplicationApplyResult =
|
|
15
|
+
| { kind: 'Applied' }
|
|
16
|
+
| { kind: 'Duplicate' }
|
|
17
|
+
| { kind: 'Superseded' }
|
|
18
|
+
| { kind: 'Incomplete'; missing: DependencyRef[] }
|
|
19
|
+
| { kind: 'Invalid'; reason: string }
|
|
20
|
+
| { kind: 'Deferred'; reason: 'tenant-inactive' | 'resolver-unavailable' | 'storage' };
|
|
21
|
+
|
|
22
|
+
export type ReplicationApplyResultContext = {
|
|
23
|
+
protocolDefinition?: ProtocolDefinition;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export type DependencyRef =
|
|
27
|
+
| { type: 'Protocol'; protocol: string; messageCid?: string; terminal?: boolean }
|
|
28
|
+
| { type: 'InitialWrite'; recordId: string; protocol?: string; messageCid?: string; terminal?: boolean }
|
|
29
|
+
| { type: 'Parent'; recordId: string; protocol: string; messageCid?: string; terminal?: boolean }
|
|
30
|
+
| { type: 'Ancestor'; recordId: string; protocol?: string; messageCid?: string; terminal?: boolean }
|
|
31
|
+
| { type: 'Role'; protocol: string; protocolPath: string; recipient: string; contextPrefix?: string; messageCid?: string; terminal?: boolean }
|
|
32
|
+
| { type: 'Grant'; permissionGrantId: string; messageCid?: string; terminal?: boolean }
|
|
33
|
+
| { type: 'KeyDelivery'; protocol: string; contextId: string; messageCid?: string; terminal?: boolean }
|
|
34
|
+
| { type: 'CrossProtocolRef'; protocol: string; recordId: string; messageCid?: string; terminal?: boolean }
|
|
35
|
+
| { type: 'RecordData'; recordId: string; dataCid: string; protocol?: string; messageCid?: string; terminal?: boolean };
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Converts a regular handler reply into the structured result consumed by
|
|
39
|
+
* replication sync. The DWN handler remains the dependency authority; this
|
|
40
|
+
* adapter only gives the sync transport a typed way to distinguish missing
|
|
41
|
+
* dependencies from terminal invalid messages.
|
|
42
|
+
*/
|
|
43
|
+
export function replicationApplyResultFromReply(
|
|
44
|
+
message: GenericMessage,
|
|
45
|
+
reply: { status: { code: number; detail?: string } },
|
|
46
|
+
context: ReplicationApplyResultContext = {},
|
|
47
|
+
): ReplicationApplyResult {
|
|
48
|
+
const { code, detail = '' } = reply.status;
|
|
49
|
+
|
|
50
|
+
if (code === 202 || code === 204) {
|
|
51
|
+
return { kind: 'Applied' };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (code === 409) {
|
|
55
|
+
return { kind: 'Superseded' };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (getDwnErrorCode(detail) === DwnErrorCode.RecordsWriteNotAllowedAfterDelete) {
|
|
59
|
+
return { kind: 'Superseded' };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (isResolverFailure(detail)) {
|
|
63
|
+
return { kind: 'Deferred', reason: 'resolver-unavailable' };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const missing = dependencyRefFromStatus(message, code, detail, context);
|
|
67
|
+
if (missing !== undefined) {
|
|
68
|
+
return { kind: 'Incomplete', missing: [missing] };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (code >= 500) {
|
|
72
|
+
return { kind: 'Deferred', reason: 'storage' };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return { kind: 'Invalid', reason: detail || `replicated message rejected with status ${code}` };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function dependencyRefFromStatus(
|
|
79
|
+
message: GenericMessage,
|
|
80
|
+
code: number,
|
|
81
|
+
detail: string,
|
|
82
|
+
context: ReplicationApplyResultContext,
|
|
83
|
+
): DependencyRef | undefined {
|
|
84
|
+
const errorCode = getDwnErrorCode(detail);
|
|
85
|
+
switch (errorCode) {
|
|
86
|
+
case DwnErrorCode.ProtocolAuthorizationProtocolNotFound:
|
|
87
|
+
case DwnErrorCode.ProtocolsConfigureComposedProtocolNotInstalled:
|
|
88
|
+
return protocolDependencyFromMessage(message, detail);
|
|
89
|
+
case DwnErrorCode.ProtocolAuthorizationParentRecordNotFound:
|
|
90
|
+
case DwnErrorCode.ProtocolAuthorizationCrossProtocolParentNotFound:
|
|
91
|
+
return parentDependencyFromMessage(message, detail);
|
|
92
|
+
case DwnErrorCode.ProtocolAuthorizationParentNotFoundConstructingRecordChain:
|
|
93
|
+
return ancestorDependencyFromMessage(message, detail);
|
|
94
|
+
case DwnErrorCode.RecordsWriteGetInitialWriteNotFound:
|
|
95
|
+
return initialWriteDependencyFromMessage(message);
|
|
96
|
+
case DwnErrorCode.GrantAuthorizationGrantMissing:
|
|
97
|
+
return grantDependencyFromMessage(message);
|
|
98
|
+
case DwnErrorCode.ProtocolAuthorizationMatchingRoleRecordNotFound:
|
|
99
|
+
return roleDependencyFromMessage(message, context);
|
|
100
|
+
case DwnErrorCode.RecordsWriteMissingDataInPrevious:
|
|
101
|
+
case DwnErrorCode.RecordsWriteMissingEncodedDataInPrevious:
|
|
102
|
+
return recordDataDependencyFromMessage(message);
|
|
103
|
+
default:
|
|
104
|
+
break;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (code === 404 && isRecordsDelete(message)) {
|
|
108
|
+
return { type: 'InitialWrite', recordId: getRecordsDeleteRecordId(message) };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return undefined;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function getDwnErrorCode(detail: string): string | undefined {
|
|
115
|
+
const delimiter = detail.indexOf(':');
|
|
116
|
+
return delimiter === -1 ? undefined : detail.slice(0, delimiter);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function isResolverFailure(detail: string): boolean {
|
|
120
|
+
return getDwnErrorCode(detail) === DwnErrorCode.GeneralJwsVerifierGetPublicKeyNotFound;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function protocolDependencyFromMessage(message: GenericMessage, detail: string): DependencyRef | undefined {
|
|
124
|
+
const composedProtocol = /composed protocol '([^']+)'/.exec(detail)?.[1];
|
|
125
|
+
if (composedProtocol !== undefined) {
|
|
126
|
+
return { type: 'Protocol', protocol: composedProtocol };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const protocol = (message.descriptor as Record<string, unknown>).protocol;
|
|
130
|
+
return typeof protocol === 'string' ? { type: 'Protocol', protocol } : undefined;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function parentDependencyFromMessage(message: GenericMessage, detail: string): DependencyRef | undefined {
|
|
134
|
+
const descriptor = message.descriptor as Record<string, unknown>;
|
|
135
|
+
const parentId = typeof descriptor.parentId === 'string'
|
|
136
|
+
? descriptor.parentId
|
|
137
|
+
: /parent record '([^']+)'/.exec(detail)?.[1];
|
|
138
|
+
const protocol = /in protocol '([^']+)'/.exec(detail)?.[1] ?? descriptor.protocol;
|
|
139
|
+
|
|
140
|
+
if (typeof parentId !== 'string' || typeof protocol !== 'string') {
|
|
141
|
+
return undefined;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return { type: 'Parent', recordId: parentId, protocol };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function ancestorDependencyFromMessage(message: GenericMessage, detail: string): DependencyRef | undefined {
|
|
148
|
+
const recordId = /ID ([^ ]+)/.exec(detail)?.[1];
|
|
149
|
+
if (recordId === undefined) {
|
|
150
|
+
return undefined;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const protocol = (message.descriptor as Record<string, unknown>).protocol;
|
|
154
|
+
return {
|
|
155
|
+
type: 'Ancestor',
|
|
156
|
+
recordId,
|
|
157
|
+
...(typeof protocol === 'string' ? { protocol } : {}),
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function initialWriteDependencyFromMessage(message: GenericMessage): DependencyRef | undefined {
|
|
162
|
+
if (!isRecordsWrite(message)) {
|
|
163
|
+
return undefined;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const protocol = (message.descriptor as Record<string, unknown>).protocol;
|
|
167
|
+
return {
|
|
168
|
+
type : 'InitialWrite',
|
|
169
|
+
recordId : message.recordId,
|
|
170
|
+
...(typeof protocol === 'string' ? { protocol } : {}),
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function grantDependencyFromMessage(message: GenericMessage): DependencyRef | undefined {
|
|
175
|
+
const descriptorGrantId = (message.descriptor as Record<string, unknown>).permissionGrantId;
|
|
176
|
+
if (typeof descriptorGrantId === 'string') {
|
|
177
|
+
return { type: 'Grant', permissionGrantId: descriptorGrantId };
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const payloadGrantId = getSignaturePayload(message)?.permissionGrantId;
|
|
181
|
+
return typeof payloadGrantId === 'string'
|
|
182
|
+
? { type: 'Grant', permissionGrantId: payloadGrantId }
|
|
183
|
+
: undefined;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function recordDataDependencyFromMessage(message: GenericMessage): DependencyRef | undefined {
|
|
187
|
+
if (!isRecordsWrite(message)) {
|
|
188
|
+
return undefined;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const descriptor = message.descriptor as Record<string, unknown>;
|
|
192
|
+
const dataCid = descriptor.dataCid;
|
|
193
|
+
const protocol = descriptor.protocol;
|
|
194
|
+
return typeof dataCid === 'string'
|
|
195
|
+
? {
|
|
196
|
+
type : 'RecordData',
|
|
197
|
+
recordId : message.recordId,
|
|
198
|
+
dataCid,
|
|
199
|
+
...(typeof protocol === 'string' ? { protocol } : {}),
|
|
200
|
+
}
|
|
201
|
+
: undefined;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function roleDependencyFromMessage(message: GenericMessage, context: ReplicationApplyResultContext): DependencyRef | undefined {
|
|
205
|
+
const descriptor = message.descriptor as Record<string, unknown>;
|
|
206
|
+
const filter = descriptor.filter as Record<string, unknown> | undefined;
|
|
207
|
+
const protocol = descriptor.protocol ?? filter?.protocol;
|
|
208
|
+
const protocolRole = getSignaturePayload(message)?.protocolRole;
|
|
209
|
+
const recipient = Message.getAuthor(message);
|
|
210
|
+
|
|
211
|
+
if (typeof protocol !== 'string' || typeof protocolRole !== 'string' || recipient === undefined) {
|
|
212
|
+
return undefined;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
let roleProtocol = protocol;
|
|
216
|
+
let roleProtocolPath = protocolRole;
|
|
217
|
+
if (isCrossProtocolRef(protocolRole)) {
|
|
218
|
+
const parsed = parseCrossProtocolRef(protocolRole);
|
|
219
|
+
const referencedProtocol = parsed === undefined ? undefined : context.protocolDefinition?.uses?.[parsed.alias];
|
|
220
|
+
if (parsed !== undefined && referencedProtocol !== undefined) {
|
|
221
|
+
roleProtocol = referencedProtocol;
|
|
222
|
+
roleProtocolPath = parsed.protocolPath;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
let contextId: string | undefined;
|
|
227
|
+
if (typeof descriptor.contextId === 'string') {
|
|
228
|
+
contextId = descriptor.contextId;
|
|
229
|
+
} else if (typeof filter?.contextId === 'string') {
|
|
230
|
+
contextId = filter.contextId;
|
|
231
|
+
}
|
|
232
|
+
const roleSegments = roleProtocolPath.split('/').length - 1;
|
|
233
|
+
const contextPrefix = roleSegments > 0 && contextId !== undefined
|
|
234
|
+
? contextId.split('/').slice(0, roleSegments).join('/')
|
|
235
|
+
: undefined;
|
|
236
|
+
|
|
237
|
+
return {
|
|
238
|
+
type : 'Role',
|
|
239
|
+
protocol : roleProtocol,
|
|
240
|
+
protocolPath : roleProtocolPath,
|
|
241
|
+
recipient,
|
|
242
|
+
...(contextPrefix === undefined ? {} : { contextPrefix }),
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function isRecordsWrite(message: GenericMessage): message is GenericMessage & { recordId: string } {
|
|
247
|
+
return message.descriptor.interface === DwnInterfaceName.Records &&
|
|
248
|
+
message.descriptor.method === DwnMethodName.Write &&
|
|
249
|
+
typeof (message as { recordId?: unknown }).recordId === 'string';
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function isRecordsDelete(message: GenericMessage): boolean {
|
|
253
|
+
return message.descriptor.interface === DwnInterfaceName.Records &&
|
|
254
|
+
message.descriptor.method === DwnMethodName.Delete;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function getRecordsDeleteRecordId(message: GenericMessage): string {
|
|
258
|
+
return (message.descriptor as unknown as { recordId: string }).recordId;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function getSignaturePayload(message: GenericMessage): Record<string, unknown> | undefined {
|
|
262
|
+
const payload = message.authorization?.signature.payload;
|
|
263
|
+
if (payload === undefined) {
|
|
264
|
+
return undefined;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
try {
|
|
268
|
+
return Encoder.base64UrlToObject(payload) as Record<string, unknown>;
|
|
269
|
+
} catch {
|
|
270
|
+
return undefined;
|
|
271
|
+
}
|
|
272
|
+
}
|
package/src/dwn.ts
CHANGED
|
@@ -1,33 +1,41 @@
|
|
|
1
1
|
import type { DataStore } from './types/data-store.js';
|
|
2
2
|
import type { DidResolver } from '@enbox/dids';
|
|
3
|
+
import type { KeyValues } from './types/query-types.js';
|
|
3
4
|
import type { MessageStore } from './types/message-store.js';
|
|
4
5
|
import type { ResumableTaskStore } from './types/resumable-task-store.js';
|
|
5
6
|
import type { StateIndex } from './types/state-index.js';
|
|
6
7
|
import type { TenantGate } from './core/tenant-gate.js';
|
|
7
8
|
import type { UnionMessageReply } from './core/message-reply.js';
|
|
8
|
-
import type { EventLog, SubscriptionListener } from './types/subscriptions.js';
|
|
9
|
+
import type { EventLog, MessageEvent, SubscriptionListener } from './types/subscriptions.js';
|
|
9
10
|
import type { GenericMessage, GenericMessageReply } from './types/message-types.js';
|
|
10
11
|
import type { HandlerDependencies, MethodHandler } from './types/method-handler.js';
|
|
11
12
|
import type { MessagesReadMessage, MessagesReadReply, MessagesSubscribeMessage, MessagesSubscribeMessageOptions, MessagesSubscribeReply, MessagesSyncMessage, MessagesSyncReply } from './types/messages-types.js';
|
|
12
|
-
import type { ProtocolsConfigureMessage, ProtocolsQueryMessage, ProtocolsQueryReply } from './types/protocols-types.js';
|
|
13
|
+
import type { ProtocolDefinition, ProtocolsConfigureMessage, ProtocolsQueryMessage, ProtocolsQueryReply } from './types/protocols-types.js';
|
|
13
14
|
import type { RecordsCountMessage, RecordsCountReply, RecordsDeleteMessage, RecordsQueryMessage, RecordsQueryReply, RecordsReadMessage, RecordsReadReply, RecordsSubscribeMessage, RecordsSubscribeMessageOptions, RecordsSubscribeReply, RecordsWriteMessage, RecordsWriteMessageOptions } from './types/records-types.js';
|
|
15
|
+
import type { ReplicationApplyOptions, ReplicationApplyResult } from './core/replication-apply.js';
|
|
14
16
|
|
|
15
17
|
import { AllowAllTenantGate } from './core/tenant-gate.js';
|
|
16
18
|
import { CoreProtocolRegistry } from './core/core-protocol.js';
|
|
19
|
+
import { DwnErrorCode } from './core/dwn-error.js';
|
|
17
20
|
import { Message } from './core/message.js';
|
|
18
21
|
import { messageReplyFromError } from './core/message-reply.js';
|
|
19
22
|
import { MessagesReadHandler } from './handlers/messages-read.js';
|
|
20
23
|
import { MessagesSubscribeHandler } from './handlers/messages-subscribe.js';
|
|
21
24
|
import { MessagesSyncHandler } from './handlers/messages-sync.js';
|
|
22
25
|
import { PermissionsProtocol } from './protocols/permissions.js';
|
|
26
|
+
import { ProtocolAuthorization } from './core/protocol-authorization.js';
|
|
27
|
+
import { ProtocolsConfigure } from './interfaces/protocols-configure.js';
|
|
23
28
|
import { ProtocolsConfigureHandler } from './handlers/protocols-configure.js';
|
|
24
29
|
import { ProtocolsQueryHandler } from './handlers/protocols-query.js';
|
|
25
30
|
import { RecordsCountHandler } from './handlers/records-count.js';
|
|
31
|
+
import { RecordsDelete } from './interfaces/records-delete.js';
|
|
26
32
|
import { RecordsDeleteHandler } from './handlers/records-delete.js';
|
|
27
33
|
import { RecordsQueryHandler } from './handlers/records-query.js';
|
|
28
34
|
import { RecordsReadHandler } from './handlers/records-read.js';
|
|
29
35
|
import { RecordsSubscribeHandler } from './handlers/records-subscribe.js';
|
|
36
|
+
import { RecordsWrite } from './interfaces/records-write.js';
|
|
30
37
|
import { RecordsWriteHandler } from './handlers/records-write.js';
|
|
38
|
+
import { replicationApplyResultFromReply } from './core/replication-apply.js';
|
|
31
39
|
import { ResumableTaskManager } from './core/resumable-task-manager.js';
|
|
32
40
|
import { StorageController } from './store/storage-controller.js';
|
|
33
41
|
import { DidDht, DidJwk, DidKey, DidResolverCacheMemory, DidWeb, UniversalResolver } from '@enbox/dids';
|
|
@@ -229,6 +237,292 @@ export class Dwn {
|
|
|
229
237
|
return methodHandlerReply;
|
|
230
238
|
}
|
|
231
239
|
|
|
240
|
+
/**
|
|
241
|
+
* Applies a message obtained through replication and returns a structured
|
|
242
|
+
* outcome instead of an HTTP-like handler status. Normal authoring still
|
|
243
|
+
* uses `processMessage`; sync uses this entry point so missing local
|
|
244
|
+
* dependencies can be fetched and retried without treating the replicated
|
|
245
|
+
* message as permanently invalid.
|
|
246
|
+
*/
|
|
247
|
+
public async applyReplicatedMessage(
|
|
248
|
+
tenant: string,
|
|
249
|
+
rawMessage: GenericMessage,
|
|
250
|
+
options: ReplicationApplyOptions = {},
|
|
251
|
+
): Promise<ReplicationApplyResult> {
|
|
252
|
+
const tenantError = await this.validateTenant(tenant);
|
|
253
|
+
if (tenantError !== undefined) {
|
|
254
|
+
return { kind: 'Deferred', reason: 'tenant-inactive' };
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const integrityError = await this.validateMessageIntegrity(rawMessage);
|
|
258
|
+
if (integrityError !== undefined) {
|
|
259
|
+
return { kind: 'Invalid', reason: integrityError.status.detail };
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if (await this.replicatedMessageAlreadyStored(tenant, rawMessage, options)) {
|
|
263
|
+
return { kind: 'Duplicate' };
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const reply = await this.processMessage(tenant, rawMessage, options);
|
|
267
|
+
const protocolDefinition = await this.getReplicationApplyProtocolDefinition(tenant, rawMessage, reply);
|
|
268
|
+
return replicationApplyResultFromReply(rawMessage, reply, { protocolDefinition });
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
private async getReplicationApplyProtocolDefinition(
|
|
272
|
+
tenant: string,
|
|
273
|
+
message: GenericMessage,
|
|
274
|
+
reply: { status: { detail?: string } },
|
|
275
|
+
): Promise<ProtocolDefinition | undefined> {
|
|
276
|
+
const detail = reply.status.detail ?? '';
|
|
277
|
+
if (!detail.startsWith(`${DwnErrorCode.ProtocolAuthorizationMatchingRoleRecordNotFound}:`)) {
|
|
278
|
+
return undefined;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const protocol = Dwn.getMessageProtocolForReplicationApply(message);
|
|
282
|
+
if (protocol === undefined) {
|
|
283
|
+
return undefined;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
try {
|
|
287
|
+
return await ProtocolAuthorization.fetchProtocolDefinition(
|
|
288
|
+
tenant,
|
|
289
|
+
protocol,
|
|
290
|
+
this.messageStore,
|
|
291
|
+
message.descriptor.messageTimestamp,
|
|
292
|
+
this._coreProtocols,
|
|
293
|
+
);
|
|
294
|
+
} catch {
|
|
295
|
+
return undefined;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
private static getMessageProtocolForReplicationApply(message: GenericMessage): string | undefined {
|
|
300
|
+
const descriptor = message.descriptor as { protocol?: unknown; filter?: { protocol?: unknown } };
|
|
301
|
+
if (typeof descriptor.protocol === 'string') {
|
|
302
|
+
return descriptor.protocol;
|
|
303
|
+
}
|
|
304
|
+
if (typeof descriptor.filter?.protocol === 'string') {
|
|
305
|
+
return descriptor.filter.protocol;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
private async replicatedMessageAlreadyStored(
|
|
310
|
+
tenant: string,
|
|
311
|
+
message: GenericMessage,
|
|
312
|
+
options: ReplicationApplyOptions,
|
|
313
|
+
): Promise<boolean> {
|
|
314
|
+
const existingMessages = await this.getExistingMessagesForReplicationDedup(tenant, message);
|
|
315
|
+
if (existingMessages.length === 0) {
|
|
316
|
+
return false;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
const incomingCid = await Message.getCid(message);
|
|
320
|
+
for (const existing of existingMessages) {
|
|
321
|
+
if (await Message.getCid(existing) !== incomingCid) {
|
|
322
|
+
continue;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
if (options.dataStream !== undefined && Dwn.existingReplicatedWriteMayNeedDataCompletion(existing, message)) {
|
|
326
|
+
return false;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
await this.repairReplicationIndexesForDuplicate(tenant, message, existingMessages, incomingCid);
|
|
330
|
+
return true;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
return false;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
private async getExistingMessagesForReplicationDedup(
|
|
337
|
+
tenant: string,
|
|
338
|
+
message: GenericMessage,
|
|
339
|
+
): Promise<GenericMessage[]> {
|
|
340
|
+
const { descriptor } = message;
|
|
341
|
+
if (descriptor.interface === DwnInterfaceName.Records && descriptor.method === DwnMethodName.Write) {
|
|
342
|
+
const recordId = (message as { recordId?: unknown }).recordId;
|
|
343
|
+
if (typeof recordId !== 'string') {
|
|
344
|
+
return [];
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const { messages } = await this.messageStore.query(tenant, [{
|
|
348
|
+
interface: DwnInterfaceName.Records,
|
|
349
|
+
recordId,
|
|
350
|
+
}]);
|
|
351
|
+
return messages;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
if (descriptor.interface === DwnInterfaceName.Records && descriptor.method === DwnMethodName.Delete) {
|
|
355
|
+
const recordId = (descriptor as { recordId?: unknown }).recordId;
|
|
356
|
+
if (typeof recordId !== 'string') {
|
|
357
|
+
return [];
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
const { messages } = await this.messageStore.query(tenant, [{
|
|
361
|
+
interface: DwnInterfaceName.Records,
|
|
362
|
+
recordId,
|
|
363
|
+
}]);
|
|
364
|
+
return messages;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
if (descriptor.interface === DwnInterfaceName.Protocols && descriptor.method === DwnMethodName.Configure) {
|
|
368
|
+
const protocol = (descriptor as { definition?: { protocol?: unknown } }).definition?.protocol;
|
|
369
|
+
if (typeof protocol !== 'string') {
|
|
370
|
+
return [];
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
const { messages } = await this.messageStore.query(tenant, [{
|
|
374
|
+
interface : DwnInterfaceName.Protocols,
|
|
375
|
+
method : DwnMethodName.Configure,
|
|
376
|
+
protocol,
|
|
377
|
+
}]);
|
|
378
|
+
return messages;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
return [];
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
private static existingReplicatedWriteMayNeedDataCompletion(existing: GenericMessage, incoming: GenericMessage): boolean {
|
|
385
|
+
if (
|
|
386
|
+
incoming.descriptor.interface !== DwnInterfaceName.Records ||
|
|
387
|
+
incoming.descriptor.method !== DwnMethodName.Write ||
|
|
388
|
+
existing.descriptor.interface !== DwnInterfaceName.Records ||
|
|
389
|
+
existing.descriptor.method !== DwnMethodName.Write
|
|
390
|
+
) {
|
|
391
|
+
return false;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
const existingWrite = existing as { encodedData?: string; descriptor: { dateCreated?: string; messageTimestamp?: string } };
|
|
395
|
+
const isInitialWrite = existingWrite.descriptor.dateCreated === existingWrite.descriptor.messageTimestamp;
|
|
396
|
+
return isInitialWrite && existingWrite.encodedData === undefined;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
private async repairReplicationIndexesForDuplicate(
|
|
400
|
+
tenant: string,
|
|
401
|
+
message: GenericMessage,
|
|
402
|
+
existingMessages: GenericMessage[],
|
|
403
|
+
messageCid: string,
|
|
404
|
+
): Promise<void> {
|
|
405
|
+
const leaves = await this.stateIndex.getLeaves(tenant, []);
|
|
406
|
+
const stateIndexHasMessage = leaves.includes(messageCid);
|
|
407
|
+
if (stateIndexHasMessage && this.eventLog === undefined) {
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
const repair = await this.constructReplicationIndexRepair(tenant, message, existingMessages);
|
|
412
|
+
if (repair === undefined) {
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
if (!stateIndexHasMessage) {
|
|
417
|
+
await this.stateIndex.insert(tenant, messageCid, repair.indexes);
|
|
418
|
+
}
|
|
419
|
+
if (repair.emitEvent && !await this.eventLogHasMessage(tenant, messageCid, repair.indexes)) {
|
|
420
|
+
await this.eventLog?.emit(tenant, repair.event, repair.indexes, messageCid);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
private async eventLogHasMessage(tenant: string, messageCid: string, indexes: KeyValues): Promise<boolean> {
|
|
425
|
+
if (this.eventLog === undefined) {
|
|
426
|
+
return true;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
const { events } = await this.eventLog.read(tenant, { filters: [indexes] });
|
|
430
|
+
for (const event of events) {
|
|
431
|
+
if (event.messageCid === messageCid || await Message.getCid(event.event.message) === messageCid) {
|
|
432
|
+
return true;
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
return false;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
private async constructReplicationIndexRepair(
|
|
439
|
+
tenant: string,
|
|
440
|
+
message: GenericMessage,
|
|
441
|
+
existingMessages: GenericMessage[],
|
|
442
|
+
): Promise<{ indexes: KeyValues; event: MessageEvent; emitEvent: boolean } | undefined> {
|
|
443
|
+
const { descriptor } = message;
|
|
444
|
+
|
|
445
|
+
if (descriptor.interface === DwnInterfaceName.Records && descriptor.method === DwnMethodName.Write) {
|
|
446
|
+
const isLatest = await Dwn.isNewestStoredMessage(message, existingMessages);
|
|
447
|
+
const eventMessage = await Dwn.getStoredMessageForCid(existingMessages, await Message.getCid(message)) ?? message;
|
|
448
|
+
const recordsWrite = await RecordsWrite.parse(eventMessage as RecordsWriteMessage);
|
|
449
|
+
const indexes = await recordsWrite.constructIndexes(isLatest);
|
|
450
|
+
const initialWrite = await this.getInitialWriteForReplicationEvent(tenant, eventMessage as RecordsWriteMessage);
|
|
451
|
+
return {
|
|
452
|
+
indexes,
|
|
453
|
+
event : { message: eventMessage, initialWrite },
|
|
454
|
+
emitEvent : isLatest && Dwn.replicatedWriteHasQueryableData(eventMessage),
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
if (descriptor.interface === DwnInterfaceName.Records && descriptor.method === DwnMethodName.Delete) {
|
|
459
|
+
const initialWrite = await RecordsWrite.fetchInitialRecordsWriteMessage(
|
|
460
|
+
this.messageStore,
|
|
461
|
+
tenant,
|
|
462
|
+
(message as RecordsDeleteMessage).descriptor.recordId,
|
|
463
|
+
);
|
|
464
|
+
if (initialWrite === undefined) {
|
|
465
|
+
return undefined;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
const recordsDelete = await RecordsDelete.parse(message as RecordsDeleteMessage);
|
|
469
|
+
const isLatest = await Dwn.isNewestStoredMessage(message, existingMessages);
|
|
470
|
+
return {
|
|
471
|
+
indexes : recordsDelete.constructIndexes(initialWrite),
|
|
472
|
+
event : { message, initialWrite },
|
|
473
|
+
emitEvent : isLatest,
|
|
474
|
+
};
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
if (descriptor.interface === DwnInterfaceName.Protocols && descriptor.method === DwnMethodName.Configure) {
|
|
478
|
+
const protocolsConfigure = await ProtocolsConfigure.parse(message as ProtocolsConfigureMessage);
|
|
479
|
+
const isLatest = await Dwn.isNewestStoredMessage(message, existingMessages);
|
|
480
|
+
return {
|
|
481
|
+
indexes : ProtocolsConfigureHandler.constructIndexes(protocolsConfigure, isLatest),
|
|
482
|
+
event : { message },
|
|
483
|
+
emitEvent : isLatest,
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
return undefined;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
private static async isNewestStoredMessage(
|
|
491
|
+
message: GenericMessage,
|
|
492
|
+
existingMessages: GenericMessage[],
|
|
493
|
+
): Promise<boolean> {
|
|
494
|
+
const newestMessage = await Message.getNewestMessage(existingMessages);
|
|
495
|
+
return newestMessage !== undefined && await Message.getCid(newestMessage) === await Message.getCid(message);
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
private static async getStoredMessageForCid(existingMessages: GenericMessage[], messageCid: string): Promise<GenericMessage | undefined> {
|
|
499
|
+
for (const existingMessage of existingMessages) {
|
|
500
|
+
if (await Message.getCid(existingMessage) === messageCid) {
|
|
501
|
+
return existingMessage;
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
private async getInitialWriteForReplicationEvent(
|
|
507
|
+
tenant: string,
|
|
508
|
+
message: RecordsWriteMessage,
|
|
509
|
+
): Promise<RecordsWriteMessage | undefined> {
|
|
510
|
+
if (await RecordsWrite.isInitialWrite(message)) {
|
|
511
|
+
return message;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
return RecordsWrite.fetchInitialRecordsWriteMessage(this.messageStore, tenant, message.recordId);
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
private static replicatedWriteHasQueryableData(message: GenericMessage): boolean {
|
|
518
|
+
if (message.descriptor.interface !== DwnInterfaceName.Records || message.descriptor.method !== DwnMethodName.Write) {
|
|
519
|
+
return false;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
return (message as { encodedData?: unknown }).encodedData !== undefined ||
|
|
523
|
+
(message as RecordsWriteMessage).descriptor.dateCreated !== (message as RecordsWriteMessage).descriptor.messageTimestamp;
|
|
524
|
+
}
|
|
525
|
+
|
|
232
526
|
/**
|
|
233
527
|
* Checks tenant gate to see if tenant is allowed.
|
|
234
528
|
* @param tenant The tenant DID to route the given message to.
|