@ixo/oracle-runtime-workers 0.21.2 → 0.23.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 (59) hide show
  1. package/README.md +16 -13
  2. package/package.json +10 -4
  3. package/src/action-proposals/policy.ts +54 -0
  4. package/src/action-proposals/reconcile.ts +87 -0
  5. package/src/action-proposals/store.ts +863 -0
  6. package/src/action-proposals/topic-budget.ts +84 -0
  7. package/src/action-proposals/topic-chain.ts +272 -0
  8. package/src/action-proposals/topic-effects.ts +44 -0
  9. package/src/action-proposals/topic-errors.ts +79 -0
  10. package/src/action-proposals/topic-history.ts +267 -0
  11. package/src/action-proposals/topic-matrix.ts +292 -0
  12. package/src/action-proposals/topic-prepare.ts +299 -0
  13. package/src/action-proposals/topic-store.ts +999 -0
  14. package/src/action-proposals/topic-verify.ts +279 -0
  15. package/src/action-proposals/verify.ts +223 -0
  16. package/src/channels/auth.ts +6 -1
  17. package/src/channels/contract.ts +4 -0
  18. package/src/channels/turns.ts +1 -0
  19. package/src/core/env.ts +107 -5
  20. package/src/core/index.ts +8 -2
  21. package/src/core/main-agent-types.ts +16 -0
  22. package/src/core/main-agent.ts +26 -0
  23. package/src/core/middlewares/tool-execution.ts +87 -46
  24. package/src/core/middlewares/tool-marks.ts +54 -19
  25. package/src/core/runtime-context.ts +27 -1
  26. package/src/core/state.ts +2 -0
  27. package/src/core/tool-scheduler.ts +129 -24
  28. package/src/delivery/plan.ts +43 -2
  29. package/src/delivery/schema.ts +25 -1
  30. package/src/delivery/types.ts +13 -2
  31. package/src/do/active-runs.ts +171 -0
  32. package/src/do/ambient.ts +6 -0
  33. package/src/do/contracts.ts +86 -3
  34. package/src/do/run-coordinator.ts +30 -0
  35. package/src/do/run-request.ts +15 -2
  36. package/src/do/run-store-test-do.ts +10 -0
  37. package/src/do/run-store.ts +66 -13
  38. package/src/do/turn-body.ts +13 -1
  39. package/src/do/user-oracle-do.ts +1420 -17
  40. package/src/hub-actions/client.ts +163 -0
  41. package/src/hub-actions/request.ts +110 -0
  42. package/src/hub-actions/store.ts +485 -0
  43. package/src/matrix/gateway-do.ts +176 -0
  44. package/src/matrix/reply-parts.ts +8 -2
  45. package/src/plugin-api/tool-helper.ts +6 -0
  46. package/src/plugin-api/types.ts +117 -7
  47. package/src/plugins/action-proposals/action-proposals.plugin.ts +486 -0
  48. package/src/plugins/action-proposals/evidence.ts +67 -0
  49. package/src/plugins/agui/agui.plugin.ts +7 -5
  50. package/src/plugins/hub-actions/hub-actions.plugin.ts +244 -0
  51. package/src/plugins/index.ts +6 -0
  52. package/src/plugins/ixo-transaction/wallet-signing.ts +5 -2
  53. package/src/plugins/portal/declared-tools.ts +12 -0
  54. package/src/plugins/portal/portal.plugin.ts +22 -8
  55. package/src/realtime/frontend-call-registry.ts +147 -27
  56. package/src/realtime/realtime-endpoint.ts +335 -41
  57. package/src/realtime/session-socket-hub.ts +90 -19
  58. package/src/shell/app.ts +218 -2
  59. package/src/sqlite/sessions-store.ts +22 -0
@@ -0,0 +1,267 @@
1
+ import {
2
+ assertContract,
3
+ resolveEffectiveTopicShape,
4
+ parseTopicOperationPayload,
5
+ } from '@ixo/topic-protocol';
6
+ import { z } from 'zod';
7
+ import { canonicalizeIxoJson } from '@ixo/ixo-transaction/action-proposals';
8
+ import type { TopicMatrixReader } from './topic-matrix';
9
+ import {
10
+ TopicMatrixEventSchema,
11
+ readTopicEvent,
12
+ type TopicMatrixEvent,
13
+ } from './topic-matrix';
14
+ import { digestTopicProposalEffect } from './topic-store';
15
+ import { ActionProposalConflict } from './store';
16
+
17
+ const State = z.object({
18
+ version: z.literal(4),
19
+ topicId: z.string(),
20
+ anchor: z.object({ roomId: z.string(), rootEventId: z.string() }),
21
+ contracts: z.object({
22
+ proposed: z.unknown().optional(),
23
+ effective: z.unknown().optional(),
24
+ }),
25
+ projection: z.object({ revision: z.string(), contractRevision: z.string() }),
26
+ shape: z.object({ digest: z.string() }),
27
+ });
28
+ const Ref = z.object({
29
+ revision: z.string(),
30
+ bodyHash: z.string().regex(/^sha256:[a-f0-9]{64}$/),
31
+ bodyRef: z.object({
32
+ provider: z.literal('matrix'),
33
+ roomId: z.string(),
34
+ eventId: z.string(),
35
+ contentHash: z.string(),
36
+ }),
37
+ });
38
+ const Body = z
39
+ .object({
40
+ version: z.literal(4),
41
+ revision: z.string(),
42
+ authoredBy: z.string(),
43
+ kindRef: z.object({
44
+ source: z.literal('standard'),
45
+ kind: z.enum([
46
+ 'project',
47
+ 'task',
48
+ 'agent_task',
49
+ 'proposal',
50
+ 'evaluation',
51
+ 'claims',
52
+ 'question',
53
+ 'discussion',
54
+ 'incident',
55
+ ]),
56
+ }),
57
+ baseRecipe: z.enum([
58
+ 'project',
59
+ 'flow',
60
+ 'proposal',
61
+ 'evaluation',
62
+ 'claims',
63
+ 'research',
64
+ 'discussion',
65
+ 'incident',
66
+ ]),
67
+ shapeSources: z.array(z.unknown()),
68
+ shapeDigest: z.string(),
69
+ claimBinding: z
70
+ .object({ entityDid: z.string(), collectionId: z.string() })
71
+ .passthrough()
72
+ .optional(),
73
+ roles: z
74
+ .array(z.object({ roleId: z.string(), assignees: z.array(z.string()) }))
75
+ .default([]),
76
+ ownerId: z.string().optional(),
77
+ })
78
+ .passthrough();
79
+ const ContractOperation = z.object({
80
+ version: z.literal(2),
81
+ operationId: z.string(),
82
+ topicId: z.string(),
83
+ type: z.enum(['propose-contract', 'update-contract', 'supersede-contract']),
84
+ previousRevision: z.string(),
85
+ actor: z.string(),
86
+ createdAt: z.string(),
87
+ payload: z.record(z.string(), z.unknown()),
88
+ capability: z.string().optional(),
89
+ });
90
+ export async function readTopicAuthority(
91
+ reader: TopicMatrixReader,
92
+ room: string,
93
+ topicId: string,
94
+ owner: string,
95
+ /** The room state the caller already read and parsed, if any. */
96
+ roomState?: TopicMatrixEvent[],
97
+ ) {
98
+ const stateEvents =
99
+ roomState ??
100
+ z
101
+ .array(TopicMatrixEventSchema)
102
+ .max(2048)
103
+ .parse(await reader.state(room));
104
+ const stateEvent = stateEvents.find(
105
+ (e) => e.type === 'ixo.topic.contract' && e.state_key === topicId,
106
+ );
107
+ if (!stateEvent)
108
+ throw new ActionProposalConflict('Canonical topic state is unavailable');
109
+ const state = State.parse(stateEvent.content);
110
+ if (state.topicId !== topicId || state.anchor.roomId !== room)
111
+ throw new ActionProposalConflict('Topic locator mismatch');
112
+ const root = await readTopicEvent(reader, room, state.anchor.rootEventId);
113
+ if (
114
+ root.sender !== owner ||
115
+ root.type !== 'm.room.message' ||
116
+ root.room_id !== room
117
+ )
118
+ throw new ActionProposalConflict(
119
+ 'Only owner-authored topic history is supported',
120
+ );
121
+ const history = z
122
+ .array(TopicMatrixEventSchema)
123
+ .max(256)
124
+ // Only the owner's relations are fetched and decrypted: authority here
125
+ // is owner-authored, and another member's relations must neither cost a
126
+ // decrypt nor fork or overflow the history read.
127
+ .parse(await reader.history(room, root.event_id, { sender: owner }));
128
+ const contracts = history
129
+ .filter((e) => e.sender === owner && e.type === 'ixo.topic.operation')
130
+ .flatMap((event) => {
131
+ const raw = z
132
+ .object({
133
+ operation: z
134
+ .object({ topicId: z.string(), type: z.string() })
135
+ .passthrough(),
136
+ })
137
+ .parse(event.content).operation;
138
+ if (
139
+ raw.topicId !== topicId ||
140
+ !['propose-contract', 'update-contract', 'supersede-contract'].includes(
141
+ raw.type,
142
+ )
143
+ )
144
+ return [];
145
+ const operation = ContractOperation.parse(raw);
146
+ if (
147
+ event.room_id !== room ||
148
+ z
149
+ .object({
150
+ 'm.relates_to': z.object({
151
+ rel_type: z.literal('m.reference'),
152
+ event_id: z.literal(root.event_id),
153
+ }),
154
+ })
155
+ .safeParse(event.content).success !== true
156
+ )
157
+ throw new ActionProposalConflict(
158
+ 'Topic operation root binding mismatch',
159
+ );
160
+ parseTopicOperationPayload(operation);
161
+ return [{ event, operation }];
162
+ });
163
+ let prior = root.event_id;
164
+ let selected: (typeof contracts)[number] | undefined;
165
+ const remaining = [...contracts];
166
+ while (remaining.length) {
167
+ const next = remaining.filter(
168
+ (e) => e.operation?.previousRevision === prior,
169
+ );
170
+ if (next.length !== 1)
171
+ throw new ActionProposalConflict('Topic history is incomplete or forked');
172
+ const entry = next[0]!;
173
+ const op = entry.operation;
174
+ if (entry.event.sender !== owner || op.actor !== owner)
175
+ throw new ActionProposalConflict(
176
+ 'Delegated contract mutation requires an unsupported authority proof',
177
+ );
178
+ if (
179
+ op.payload.previousRevision !== prior &&
180
+ op.payload.topicPreviousRevision !== prior
181
+ )
182
+ throw new ActionProposalConflict('Topic operation revision mismatch');
183
+ prior = op.operationId;
184
+ selected = entry;
185
+ remaining.splice(remaining.indexOf(entry), 1);
186
+ }
187
+ if (!selected?.operation || prior !== state.projection.contractRevision)
188
+ throw new ActionProposalConflict(
189
+ 'Topic projection is not backed by complete contract history',
190
+ );
191
+ const ref = Ref.parse(
192
+ selected.operation.payload.contractRef ??
193
+ selected.operation.payload.contract,
194
+ );
195
+ if (
196
+ ref.revision !== prior ||
197
+ ref.bodyRef.roomId !== room ||
198
+ ref.bodyRef.contentHash !== ref.bodyHash
199
+ )
200
+ throw new ActionProposalConflict('Pinned body reference mismatch');
201
+ const selectedHead = Ref.parse(
202
+ state.contracts.proposed ?? state.contracts.effective,
203
+ );
204
+ if (canonicalizeIxoJson(selectedHead) !== canonicalizeIxoJson(ref))
205
+ throw new ActionProposalConflict(
206
+ 'Projection body head differs from authenticated history',
207
+ );
208
+ const bodyEvent = await readTopicEvent(reader, room, ref.bodyRef.eventId);
209
+ if (
210
+ bodyEvent.sender !== owner ||
211
+ bodyEvent.room_id !== room ||
212
+ bodyEvent.type !== 'ixo.topic.contract.body' ||
213
+ z
214
+ .object({ 'm.relates_to': z.object({ event_id: z.string() }) })
215
+ .parse(bodyEvent.content)['m.relates_to'].event_id !== root.event_id
216
+ )
217
+ throw new ActionProposalConflict('Pinned body sender mismatch');
218
+ const body = Body.parse(bodyEvent.content.contract);
219
+ assertContract('topic-contract-body', body);
220
+ const bodyHash = `sha256:${await digestTopicProposalEffect(bodyEvent.content.contract)}`;
221
+ if (
222
+ bodyHash !== ref.bodyHash ||
223
+ body.revision !== prior ||
224
+ body.authoredBy !== owner
225
+ )
226
+ throw new ActionProposalConflict('Pinned body integrity mismatch');
227
+ const shape = resolveEffectiveTopicShape({
228
+ baseRecipe: body.baseRecipe,
229
+ kind: body.kindRef.kind,
230
+ // The shape release (`TOPIC_SHAPE_RELEASE`), not the package version.
231
+ sourceVersion: '1.0.0-rc.7',
232
+ });
233
+ if (
234
+ shape.digest !== body.shapeDigest ||
235
+ shape.digest !== state.shape.digest ||
236
+ canonicalizeIxoJson(shape.sources) !==
237
+ canonicalizeIxoJson(body.shapeSources)
238
+ )
239
+ throw new ActionProposalConflict('Pinned shape is unsupported or changed');
240
+ const authority = {
241
+ rootEventId: root.event_id,
242
+ contractBodyEventId: bodyEvent.event_id,
243
+ operationEventId: selected.event.event_id,
244
+ stateEventId: stateEvent.event_id,
245
+ contractBodyHash: bodyHash,
246
+ revision: prior,
247
+ shapeDigest: shape.digest,
248
+ };
249
+ // The state event is re-emitted by ordinary projection activity; the
250
+ // contract head it points at is what carries authority.
251
+ const { stateEventId: _state, ...pinned } = authority;
252
+ return {
253
+ authority,
254
+ body,
255
+ // The fingerprint is the authority chain's head and the pinned body: a
256
+ // new contract operation changes `authority.revision`, while ordinary
257
+ // activity in the topic thread (a reply, a reaction) does not block the
258
+ // owner's claim.
259
+ dependencies: [
260
+ {
261
+ kind: 'topic' as const,
262
+ key: topicId,
263
+ digest: await digestTopicProposalEffect({ authority: pinned, body }),
264
+ },
265
+ ],
266
+ };
267
+ }
@@ -0,0 +1,292 @@
1
+ import { z } from 'zod';
2
+ import { boundedBytes } from '../core/domain-context/transport';
3
+ import { digestTopicProposalEffect } from './topic-store';
4
+ import { ActionProposalConflict } from './store';
5
+ import { TopicReadUnavailable } from './topic-errors';
6
+ import type { TopicActionProposal } from '@ixo/ixo-transaction/topic-proposals';
7
+
8
+ export const TopicMatrixEventSchema = z.object({
9
+ event_id: z.string().startsWith('$').max(512),
10
+ type: z.string().min(1),
11
+ state_key: z.string().optional(),
12
+ content: z.record(z.string(), z.unknown()),
13
+ sender: z.string().regex(/^@[^:]+:.+$/),
14
+ room_id: z.string().optional(),
15
+ origin_server_ts: z.number().optional(),
16
+ });
17
+ export type TopicMatrixEvent = z.infer<typeof TopicMatrixEventSchema>;
18
+ /**
19
+ * What a topic read may tell the gateway about itself: the moment its
20
+ * budget ends (the gateway stops before it, decrypt waits included) and,
21
+ * for history, the only sender whose relations matter (others are never
22
+ * decrypted or counted).
23
+ */
24
+ export type TopicReadOptions = { deadlineAt?: number; sender?: string };
25
+ export interface TopicMatrixReader {
26
+ state(roomId: string): Promise<unknown>;
27
+ event(
28
+ roomId: string,
29
+ eventId: string,
30
+ opts?: TopicReadOptions,
31
+ ): Promise<unknown>;
32
+ history(
33
+ roomId: string,
34
+ root: string,
35
+ opts?: TopicReadOptions,
36
+ ): Promise<unknown>;
37
+ }
38
+ const DidResponse = z.object({
39
+ data: z.object({
40
+ iids: z.object({
41
+ nodes: z
42
+ .array(
43
+ z.object({
44
+ id: z.string(),
45
+ service: z.array(
46
+ z.object({ type: z.string(), serviceEndpoint: z.string() }),
47
+ ),
48
+ }),
49
+ )
50
+ .max(2),
51
+ }),
52
+ }),
53
+ errors: z.array(z.unknown()).optional(),
54
+ });
55
+ export async function resolveTopicIdentity(
56
+ did: string,
57
+ blocksync: string | undefined,
58
+ fetcher: typeof fetch = fetch,
59
+ ) {
60
+ if (!blocksync)
61
+ throw new TopicReadUnavailable('Trusted DID resolver is unavailable');
62
+ const endpoint = new URL(blocksync);
63
+ if (endpoint.protocol !== 'https:' || endpoint.username || endpoint.password)
64
+ throw new TopicReadUnavailable('Invalid trusted DID endpoint');
65
+ const response = await fetcher(endpoint, {
66
+ method: 'POST',
67
+ redirect: 'error',
68
+ headers: { 'content-type': 'application/json' },
69
+ body: JSON.stringify({
70
+ query:
71
+ 'query TopicIdentity($id:String!){iids(filter:{id:{equalTo:$id}}){nodes{id service}}}',
72
+ variables: { id: did },
73
+ }),
74
+ signal: AbortSignal.timeout(5000),
75
+ });
76
+ if (!response.ok) throw new TopicReadUnavailable('DID lookup unavailable');
77
+ const envelope = DidResponse.safeParse(
78
+ JSON.parse(new TextDecoder().decode(await boundedBytes(response, 65536))),
79
+ );
80
+ // A GraphQL error envelope (`data: null`, `errors`) is the resolver
81
+ // failing, not a fact about the DID.
82
+ if (!envelope.success || envelope.data.errors?.length)
83
+ throw new TopicReadUnavailable('DID lookup unavailable');
84
+ const raw = envelope.data;
85
+ if (raw.data.iids.nodes.length !== 1)
86
+ throw new ActionProposalConflict('Identity is not indexed');
87
+ const record = raw.data.iids.nodes[0]!;
88
+ if (record.id !== did)
89
+ throw new ActionProposalConflict('DID lookup mismatch');
90
+ const services = record.service.filter((s) => s.type === 'MatrixHomeServer');
91
+ if (services.length !== 1)
92
+ throw new ActionProposalConflict(
93
+ 'Exactly one Matrix homeserver is required',
94
+ );
95
+ const url = new URL(services[0]!.serviceEndpoint);
96
+ if (
97
+ url.protocol !== 'https:' ||
98
+ url.username ||
99
+ url.password ||
100
+ url.search ||
101
+ url.hash ||
102
+ url.pathname !== '/' ||
103
+ url.port
104
+ )
105
+ throw new ActionProposalConflict('Invalid DID Matrix service');
106
+ const homeserver = url.origin;
107
+ const matrixUserId = `@${did.replaceAll(':', '-')}:${url.hostname}`;
108
+ return {
109
+ did,
110
+ matrixUserId,
111
+ homeserver,
112
+ bindingDigest: await digestTopicProposalEffect({
113
+ did,
114
+ service: services[0],
115
+ }),
116
+ };
117
+ }
118
+ /**
119
+ * One event, parsed. An event the gateway cannot hand over (not decrypted
120
+ * yet, not found) comes back empty: a read that failed, not a fact about the
121
+ * card.
122
+ */
123
+ export async function readTopicEvent(
124
+ reader: TopicMatrixReader,
125
+ room: string,
126
+ eventId: string,
127
+ ) {
128
+ const raw = await reader.event(room, eventId);
129
+ if (raw === null || raw === undefined)
130
+ throw new TopicReadUnavailable('Topic event is not readable');
131
+ return TopicMatrixEventSchema.parse(raw);
132
+ }
133
+ export async function readTopicRoom(
134
+ reader: TopicMatrixReader,
135
+ room: string,
136
+ owner: string,
137
+ ) {
138
+ const state = z
139
+ .array(TopicMatrixEventSchema)
140
+ .max(2048)
141
+ .parse(await reader.state(room));
142
+ if (
143
+ new Set(state.map((e) => `${e.type}\0${e.state_key ?? ''}`)).size !==
144
+ state.length
145
+ )
146
+ throw new TopicReadUnavailable('Room state has duplicate keys');
147
+ const membership = state.find(
148
+ (e) => e.type === 'm.room.member' && e.state_key === owner,
149
+ );
150
+ const power = state.find(
151
+ (e) => e.type === 'm.room.power_levels' && e.state_key === '',
152
+ );
153
+ const encryption = state.find(
154
+ (e) => e.type === 'm.room.encryption' && e.state_key === '',
155
+ );
156
+ const creation = state.find(
157
+ (e) => e.type === 'm.room.create' && e.state_key === '',
158
+ );
159
+ if (membership?.content.membership !== 'join' || !power || !creation)
160
+ throw new ActionProposalConflict(
161
+ 'The owner is not a member of the room, or its policy is unreadable',
162
+ );
163
+ const levels = z
164
+ .object({
165
+ users: z.record(z.string(), z.number()).default({}),
166
+ users_default: z.number().default(0),
167
+ events: z.record(z.string(), z.number()).default({}),
168
+ events_default: z.number().default(0),
169
+ state_default: z.number().default(50),
170
+ })
171
+ .parse(power.content);
172
+ const level = levels.users[owner] ?? levels.users_default;
173
+ for (const type of [
174
+ 'm.room.message',
175
+ 'ixo.topic.contract.body',
176
+ 'ixo.topic.operation',
177
+ 'ixo.topic.contract',
178
+ ]) {
179
+ const required =
180
+ levels.events[type] ??
181
+ (type === 'ixo.topic.contract'
182
+ ? levels.state_default
183
+ : levels.events_default);
184
+ if (level < required)
185
+ throw new ActionProposalConflict(
186
+ 'Owner cannot write the topic event types',
187
+ );
188
+ }
189
+ const selected = [
190
+ creation,
191
+ membership,
192
+ power,
193
+ ...(encryption ? [encryption] : []),
194
+ ];
195
+ // Only the facts that carry authority are fingerprinted: the owner's
196
+ // membership value (not their avatar or display name) and each policy
197
+ // event's content (not the event id a re-send would change).
198
+ const topicTypes = [
199
+ 'm.room.message',
200
+ 'ixo.topic.contract.body',
201
+ 'ixo.topic.operation',
202
+ 'ixo.topic.contract',
203
+ ];
204
+ const authority = (e: TopicMatrixEvent) => ({
205
+ type: e.type,
206
+ state_key: e.state_key ?? '',
207
+ content:
208
+ e.type === 'm.room.member'
209
+ ? { membership: e.content.membership }
210
+ : e.type === 'm.room.power_levels'
211
+ ? // Only the owner's level and the thresholds for the topic event
212
+ // types: another member's level changing is not this card's
213
+ // business.
214
+ {
215
+ owner: levels.users[owner] ?? levels.users_default,
216
+ events: Object.fromEntries(
217
+ topicTypes.map((type) => [
218
+ type,
219
+ levels.events[type] ??
220
+ (type === 'ixo.topic.contract'
221
+ ? levels.state_default
222
+ : levels.events_default),
223
+ ]),
224
+ ),
225
+ }
226
+ : e.content,
227
+ });
228
+ const dependencies = await Promise.all(
229
+ selected.map(async (e) => ({
230
+ kind: 'matrix' as const,
231
+ key: `${e.type}\0${e.state_key ?? ''}`,
232
+ digest: await digestTopicProposalEffect(authority(e)),
233
+ })),
234
+ );
235
+ if (!encryption)
236
+ dependencies.push({
237
+ kind: 'matrix',
238
+ key: 'm.room.encryption\0',
239
+ digest: await digestTopicProposalEffect(null),
240
+ });
241
+ return { state, dependencies, encrypted: !!encryption };
242
+ }
243
+ export async function assertTopicRoomUnchanged(
244
+ proposal: TopicActionProposal,
245
+ reader: TopicMatrixReader,
246
+ blocksync: string | undefined,
247
+ fetcher: typeof fetch = fetch,
248
+ ) {
249
+ const owner = await resolveTopicIdentity(
250
+ proposal.ownerDid,
251
+ blocksync,
252
+ fetcher,
253
+ );
254
+ if (
255
+ owner.bindingDigest !== proposal.action.owner.bindingDigest ||
256
+ owner.matrixUserId !== proposal.action.owner.matrixUserId ||
257
+ owner.homeserver !== proposal.action.owner.homeserver
258
+ )
259
+ throw new ActionProposalConflict('Owner Matrix identity changed');
260
+ const current = await readTopicRoom(
261
+ reader,
262
+ proposal.action.roomId,
263
+ owner.matrixUserId,
264
+ );
265
+ for (const dep of proposal.action.dependencies.filter(
266
+ (d) => d.kind === 'matrix',
267
+ ))
268
+ if (
269
+ !current.dependencies.some(
270
+ (d) => d.key === dep.key && d.digest === dep.digest,
271
+ )
272
+ )
273
+ throw new ActionProposalConflict('Room authority changed');
274
+ const people =
275
+ proposal.action.name === 'create_topic'
276
+ ? proposal.action.participants
277
+ : proposal.action.grants.map((g) => g.grantee);
278
+ const bindings = await Promise.all(
279
+ people.map((person) =>
280
+ resolveTopicIdentity(person.did, blocksync, fetcher),
281
+ ),
282
+ );
283
+ people.forEach((person, i) => {
284
+ const binding = bindings[i]!;
285
+ if (
286
+ binding.bindingDigest !== person.bindingDigest ||
287
+ binding.matrixUserId !== person.matrixUserId
288
+ )
289
+ throw new ActionProposalConflict('Participant identity changed');
290
+ });
291
+ return current;
292
+ }