@ixo/oracle-runtime-workers 0.22.0 → 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.
- package/README.md +15 -13
- package/package.json +10 -4
- package/src/action-proposals/policy.ts +54 -0
- package/src/action-proposals/reconcile.ts +87 -0
- package/src/action-proposals/store.ts +863 -0
- package/src/action-proposals/topic-budget.ts +84 -0
- package/src/action-proposals/topic-chain.ts +272 -0
- package/src/action-proposals/topic-effects.ts +44 -0
- package/src/action-proposals/topic-errors.ts +79 -0
- package/src/action-proposals/topic-history.ts +267 -0
- package/src/action-proposals/topic-matrix.ts +292 -0
- package/src/action-proposals/topic-prepare.ts +299 -0
- package/src/action-proposals/topic-store.ts +999 -0
- package/src/action-proposals/topic-verify.ts +279 -0
- package/src/action-proposals/verify.ts +223 -0
- package/src/channels/auth.ts +6 -1
- package/src/channels/contract.ts +4 -0
- package/src/channels/turns.ts +1 -0
- package/src/core/env.ts +89 -5
- package/src/core/main-agent-types.ts +8 -0
- package/src/core/main-agent.ts +2 -0
- package/src/core/runtime-context.ts +12 -0
- package/src/delivery/plan.ts +43 -2
- package/src/delivery/schema.ts +25 -1
- package/src/delivery/types.ts +13 -2
- package/src/do/ambient.ts +6 -0
- package/src/do/contracts.ts +39 -0
- package/src/do/user-oracle-do.ts +1253 -7
- package/src/hub-actions/client.ts +163 -0
- package/src/hub-actions/request.ts +110 -0
- package/src/hub-actions/store.ts +485 -0
- package/src/matrix/gateway-do.ts +176 -0
- package/src/matrix/reply-parts.ts +8 -2
- package/src/plugin-api/types.ts +54 -0
- package/src/plugins/action-proposals/action-proposals.plugin.ts +486 -0
- package/src/plugins/action-proposals/evidence.ts +67 -0
- package/src/plugins/hub-actions/hub-actions.plugin.ts +244 -0
- package/src/plugins/index.ts +6 -0
- package/src/shell/app.ts +195 -1
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { TopicReadUnavailable, typedTopicError } from './topic-errors';
|
|
2
|
+
import type { TopicMatrixReader } from './topic-matrix';
|
|
3
|
+
export const TOPIC_READ_BUDGET_MS = 10_000;
|
|
4
|
+
export async function withTopicReadBudget<T>(
|
|
5
|
+
reader: TopicMatrixReader,
|
|
6
|
+
fetcher: typeof fetch,
|
|
7
|
+
run: (reader: TopicMatrixReader, fetcher: typeof fetch) => Promise<T>,
|
|
8
|
+
/** An outer budget (the alarm tick's): its abort ends this read too. */
|
|
9
|
+
outer?: AbortSignal,
|
|
10
|
+
): Promise<T> {
|
|
11
|
+
const controller = new AbortController();
|
|
12
|
+
const deadlineAt = Date.now() + TOPIC_READ_BUDGET_MS;
|
|
13
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
14
|
+
let rejectDeadline: (error: unknown) => void = () => undefined;
|
|
15
|
+
const deadline = new Promise<never>((_resolve, reject) => {
|
|
16
|
+
rejectDeadline = reject;
|
|
17
|
+
timer = setTimeout(() => {
|
|
18
|
+
controller.abort();
|
|
19
|
+
reject(
|
|
20
|
+
new TopicReadUnavailable(
|
|
21
|
+
'Topic verification exceeded the ten second budget',
|
|
22
|
+
),
|
|
23
|
+
);
|
|
24
|
+
}, TOPIC_READ_BUDGET_MS);
|
|
25
|
+
});
|
|
26
|
+
const onOuterAbort = () => {
|
|
27
|
+
controller.abort();
|
|
28
|
+
rejectDeadline(
|
|
29
|
+
new TopicReadUnavailable('Topic verification budget expired'),
|
|
30
|
+
);
|
|
31
|
+
};
|
|
32
|
+
if (outer?.aborted) onOuterAbort();
|
|
33
|
+
else outer?.addEventListener('abort', onOuterAbort, { once: true });
|
|
34
|
+
const active = () => {
|
|
35
|
+
if (controller.signal.aborted)
|
|
36
|
+
throw new TopicReadUnavailable('Topic verification budget expired');
|
|
37
|
+
};
|
|
38
|
+
const bounded: TopicMatrixReader = {
|
|
39
|
+
state: async (room) => {
|
|
40
|
+
active();
|
|
41
|
+
const value = await reader.state(room);
|
|
42
|
+
active();
|
|
43
|
+
return value;
|
|
44
|
+
},
|
|
45
|
+
event: async (room, id, opts) => {
|
|
46
|
+
active();
|
|
47
|
+
const value = await reader.event(room, id, {
|
|
48
|
+
...opts,
|
|
49
|
+
deadlineAt: Math.min(opts?.deadlineAt ?? Infinity, deadlineAt),
|
|
50
|
+
});
|
|
51
|
+
active();
|
|
52
|
+
return value;
|
|
53
|
+
},
|
|
54
|
+
history: async (room, root, opts) => {
|
|
55
|
+
active();
|
|
56
|
+
const value = await reader.history(room, root, {
|
|
57
|
+
...opts,
|
|
58
|
+
deadlineAt: Math.min(opts?.deadlineAt ?? Infinity, deadlineAt),
|
|
59
|
+
});
|
|
60
|
+
active();
|
|
61
|
+
return value;
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
const fetchBounded: typeof fetch = (input, init) => {
|
|
65
|
+
active();
|
|
66
|
+
return fetcher(input, { ...init, signal: controller.signal }).then(
|
|
67
|
+
(response) => {
|
|
68
|
+
active();
|
|
69
|
+
return response;
|
|
70
|
+
},
|
|
71
|
+
);
|
|
72
|
+
};
|
|
73
|
+
try {
|
|
74
|
+
return await Promise.race([run(bounded, fetchBounded), deadline]);
|
|
75
|
+
} catch (error) {
|
|
76
|
+
// The budget is where every authority read ends: whatever leaves it is
|
|
77
|
+
// a conflict, unavailable, or a bug.
|
|
78
|
+
throw typedTopicError(error);
|
|
79
|
+
} finally {
|
|
80
|
+
if (timer) clearTimeout(timer);
|
|
81
|
+
outer?.removeEventListener('abort', onOuterAbort);
|
|
82
|
+
controller.abort();
|
|
83
|
+
}
|
|
84
|
+
}
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
import protobuf from 'protobufjs/minimal.js';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import {
|
|
4
|
+
QueryEntityRequest,
|
|
5
|
+
QueryEntityResponse,
|
|
6
|
+
QueryParamsRequest,
|
|
7
|
+
QueryParamsResponse,
|
|
8
|
+
} from '@ixo/impactxclient-sdk/module/codegen/ixo/entity/v1beta1/query.js';
|
|
9
|
+
import {
|
|
10
|
+
QueryCollectionRequest,
|
|
11
|
+
QueryCollectionResponse,
|
|
12
|
+
} from '@ixo/impactxclient-sdk/module/codegen/ixo/claims/v1beta1/query.js';
|
|
13
|
+
import {
|
|
14
|
+
QueryGrantsRequest,
|
|
15
|
+
QueryGrantsResponse,
|
|
16
|
+
} from '@ixo/impactxclient-sdk/module/codegen/cosmos/authz/v1beta1/query.js';
|
|
17
|
+
import {
|
|
18
|
+
QuerySmartContractStateRequest,
|
|
19
|
+
QuerySmartContractStateResponse,
|
|
20
|
+
} from '@ixo/impactxclient-sdk/module/codegen/cosmwasm/wasm/v1/query.js';
|
|
21
|
+
import { IxoAddressSchema } from '@ixo/ixo-transaction/action-proposals';
|
|
22
|
+
import { boundedBytes } from '../core/domain-context/transport';
|
|
23
|
+
import { digestTopicProposalEffect } from './topic-store';
|
|
24
|
+
import { ActionProposalConflict } from './store';
|
|
25
|
+
import { TopicReadUnavailable } from './topic-errors';
|
|
26
|
+
import { chainNetwork } from './verify';
|
|
27
|
+
const encode64 = (v: Uint8Array) =>
|
|
28
|
+
btoa(Array.from(v, (c) => String.fromCharCode(c)).join(''));
|
|
29
|
+
const decode64 = (v: string) =>
|
|
30
|
+
Uint8Array.from(atob(v), (c) => c.charCodeAt(0));
|
|
31
|
+
const hex = (v: Uint8Array) =>
|
|
32
|
+
Array.from(v, (c) => c.toString(16).padStart(2, '0')).join('');
|
|
33
|
+
export class TopicChainReader {
|
|
34
|
+
constructor(
|
|
35
|
+
private readonly rpc: string,
|
|
36
|
+
private readonly chainId: string,
|
|
37
|
+
private readonly fetcher: typeof fetch = fetch,
|
|
38
|
+
) {}
|
|
39
|
+
private async request(path: string) {
|
|
40
|
+
const base = new URL(this.rpc);
|
|
41
|
+
if (
|
|
42
|
+
base.protocol !== 'https:' ||
|
|
43
|
+
base.username ||
|
|
44
|
+
base.password ||
|
|
45
|
+
base.search ||
|
|
46
|
+
base.hash
|
|
47
|
+
)
|
|
48
|
+
throw new TopicReadUnavailable('Invalid trusted RPC');
|
|
49
|
+
const res = await this.fetcher(
|
|
50
|
+
new URL(path, `${base.href.replace(/\/$/, '')}/`),
|
|
51
|
+
{ redirect: 'error', signal: AbortSignal.timeout(10000) },
|
|
52
|
+
);
|
|
53
|
+
if (!res.ok) throw new TopicReadUnavailable('Chain authority unavailable');
|
|
54
|
+
return JSON.parse(
|
|
55
|
+
new TextDecoder().decode(await boundedBytes(res, 524288)),
|
|
56
|
+
) as unknown;
|
|
57
|
+
}
|
|
58
|
+
// The node's envelopes are transport: an answer this reader does not
|
|
59
|
+
// understand is the node failing, not a fact about the chain.
|
|
60
|
+
// The network an origin serves comes from the per-isolate `/status`
|
|
61
|
+
// cache the transfer verifier uses (5 minutes), not a read per call.
|
|
62
|
+
async checkChain() {
|
|
63
|
+
let network: string;
|
|
64
|
+
try {
|
|
65
|
+
network = await chainNetwork(new URL(this.rpc).href, () =>
|
|
66
|
+
this.request('status'),
|
|
67
|
+
);
|
|
68
|
+
} catch (error) {
|
|
69
|
+
if (error instanceof TopicReadUnavailable) throw error;
|
|
70
|
+
throw new TopicReadUnavailable('Chain authority unavailable');
|
|
71
|
+
}
|
|
72
|
+
if (network !== this.chainId)
|
|
73
|
+
throw new TopicReadUnavailable('Chain authority mismatch');
|
|
74
|
+
}
|
|
75
|
+
private async query(path: string, data: Uint8Array) {
|
|
76
|
+
const r = z
|
|
77
|
+
.object({
|
|
78
|
+
result: z.object({
|
|
79
|
+
response: z.object({
|
|
80
|
+
code: z.number().optional(),
|
|
81
|
+
value: z.string(),
|
|
82
|
+
height: z.string(),
|
|
83
|
+
}),
|
|
84
|
+
}),
|
|
85
|
+
})
|
|
86
|
+
.safeParse(
|
|
87
|
+
await this.request(
|
|
88
|
+
`abci_query?path=${encodeURIComponent(JSON.stringify(path))}&data=0x${hex(data)}&prove=false`,
|
|
89
|
+
),
|
|
90
|
+
);
|
|
91
|
+
if (!r.success)
|
|
92
|
+
throw new TopicReadUnavailable('Chain authority unavailable');
|
|
93
|
+
const response = r.data.result.response;
|
|
94
|
+
if (response.code)
|
|
95
|
+
throw new TopicReadUnavailable('Authoritative chain query failed');
|
|
96
|
+
return decode64(response.value);
|
|
97
|
+
}
|
|
98
|
+
async collection(
|
|
99
|
+
entityDid: string,
|
|
100
|
+
collectionId: string,
|
|
101
|
+
signerAddress: string,
|
|
102
|
+
) {
|
|
103
|
+
await this.checkChain();
|
|
104
|
+
const entity = QueryEntityResponse.decode(
|
|
105
|
+
await this.query(
|
|
106
|
+
'/ixo.entity.v1beta1.Query/Entity',
|
|
107
|
+
QueryEntityRequest.encode(
|
|
108
|
+
{ id: entityDid },
|
|
109
|
+
new protobuf.Writer(),
|
|
110
|
+
).finish(),
|
|
111
|
+
),
|
|
112
|
+
).entity;
|
|
113
|
+
const collection = QueryCollectionResponse.decode(
|
|
114
|
+
await this.query(
|
|
115
|
+
'/ixo.claims.v1beta1.Query/Collection',
|
|
116
|
+
QueryCollectionRequest.encode(
|
|
117
|
+
{ id: collectionId },
|
|
118
|
+
new protobuf.Writer(),
|
|
119
|
+
).finish(),
|
|
120
|
+
),
|
|
121
|
+
).collection;
|
|
122
|
+
const params = QueryParamsResponse.decode(
|
|
123
|
+
await this.query(
|
|
124
|
+
'/ixo.entity.v1beta1.Query/Params',
|
|
125
|
+
QueryParamsRequest.encode({}, new protobuf.Writer()).finish(),
|
|
126
|
+
),
|
|
127
|
+
).params;
|
|
128
|
+
const admin = entity?.accounts.find((a) => a.name === 'admin');
|
|
129
|
+
if (
|
|
130
|
+
!entity ||
|
|
131
|
+
entity.id !== entityDid ||
|
|
132
|
+
!collection ||
|
|
133
|
+
collection.id !== collectionId ||
|
|
134
|
+
collection.entity !== entityDid ||
|
|
135
|
+
!admin ||
|
|
136
|
+
collection.admin !== admin.address ||
|
|
137
|
+
!params?.nftContractAddress
|
|
138
|
+
)
|
|
139
|
+
throw new ActionProposalConflict(
|
|
140
|
+
'Entity collection and admin binding mismatch',
|
|
141
|
+
);
|
|
142
|
+
IxoAddressSchema.parse(admin.address);
|
|
143
|
+
const smart = QuerySmartContractStateRequest.fromPartial({
|
|
144
|
+
address: params.nftContractAddress,
|
|
145
|
+
queryData: new TextEncoder().encode(
|
|
146
|
+
JSON.stringify({
|
|
147
|
+
owner_of: { token_id: entityDid, include_expired: false },
|
|
148
|
+
}),
|
|
149
|
+
),
|
|
150
|
+
});
|
|
151
|
+
const owner = QuerySmartContractStateResponse.decode(
|
|
152
|
+
await this.query(
|
|
153
|
+
'/cosmwasm.wasm.v1.Query/SmartContractState',
|
|
154
|
+
QuerySmartContractStateRequest.encode(
|
|
155
|
+
smart,
|
|
156
|
+
new protobuf.Writer(),
|
|
157
|
+
).finish(),
|
|
158
|
+
),
|
|
159
|
+
);
|
|
160
|
+
const nft = z
|
|
161
|
+
.object({ owner: IxoAddressSchema })
|
|
162
|
+
.parse(JSON.parse(new TextDecoder().decode(owner.data)));
|
|
163
|
+
if (nft.owner !== signerAddress)
|
|
164
|
+
throw new ActionProposalConflict(
|
|
165
|
+
'Signing account is not the entity NFT owner',
|
|
166
|
+
);
|
|
167
|
+
return {
|
|
168
|
+
adminAddress: admin.address,
|
|
169
|
+
dependencies: [
|
|
170
|
+
{
|
|
171
|
+
kind: 'entity' as const,
|
|
172
|
+
key: entityDid,
|
|
173
|
+
digest: await digestTopicProposalEffect({
|
|
174
|
+
id: entity.id,
|
|
175
|
+
accounts: entity.accounts,
|
|
176
|
+
}),
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
kind: 'collection' as const,
|
|
180
|
+
key: collectionId,
|
|
181
|
+
digest: await digestTopicProposalEffect({
|
|
182
|
+
id: collection.id,
|
|
183
|
+
entity: collection.entity,
|
|
184
|
+
admin: collection.admin,
|
|
185
|
+
}),
|
|
186
|
+
},
|
|
187
|
+
{
|
|
188
|
+
kind: 'nft-owner' as const,
|
|
189
|
+
key: params.nftContractAddress,
|
|
190
|
+
digest: await digestTopicProposalEffect({
|
|
191
|
+
entityDid,
|
|
192
|
+
owner: nft.owner,
|
|
193
|
+
}),
|
|
194
|
+
},
|
|
195
|
+
],
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
async authorization(
|
|
199
|
+
admin: string,
|
|
200
|
+
grantee: string,
|
|
201
|
+
role: 'submit' | 'evaluate',
|
|
202
|
+
) {
|
|
203
|
+
const typeUrl =
|
|
204
|
+
role === 'submit'
|
|
205
|
+
? ('/ixo.claims.v1beta1.SubmitClaimAuthorization' as const)
|
|
206
|
+
: ('/ixo.claims.v1beta1.EvaluateClaimAuthorization' as const);
|
|
207
|
+
const messageType =
|
|
208
|
+
role === 'submit'
|
|
209
|
+
? '/ixo.claims.v1beta1.MsgSubmitClaim'
|
|
210
|
+
: '/ixo.claims.v1beta1.MsgEvaluateClaim';
|
|
211
|
+
const request = QueryGrantsRequest.fromPartial({
|
|
212
|
+
granter: admin,
|
|
213
|
+
grantee,
|
|
214
|
+
msgTypeUrl: messageType,
|
|
215
|
+
});
|
|
216
|
+
const response = QueryGrantsResponse.decode(
|
|
217
|
+
await this.query(
|
|
218
|
+
'/cosmos.authz.v1beta1.Query/Grants',
|
|
219
|
+
QueryGrantsRequest.encode(request, new protobuf.Writer()).finish(),
|
|
220
|
+
),
|
|
221
|
+
);
|
|
222
|
+
if (response.pagination?.nextKey.length || response.grants.length > 1)
|
|
223
|
+
throw new ActionProposalConflict(
|
|
224
|
+
'Authorization query exceeds exact single-grant bound',
|
|
225
|
+
);
|
|
226
|
+
const grant = response.grants[0];
|
|
227
|
+
if (grant?.authorization && grant.authorization.typeUrl !== typeUrl)
|
|
228
|
+
throw new ActionProposalConflict('Authorization role mismatch');
|
|
229
|
+
// The grant's expiry on chain covers every constraint in it; the grant
|
|
230
|
+
// builder refuses to merge into it under another expiry.
|
|
231
|
+
const expiresAt = (() => {
|
|
232
|
+
if (!grant?.expiration) return null;
|
|
233
|
+
const seconds = Number(grant.expiration.seconds.toString());
|
|
234
|
+
if (!Number.isSafeInteger(seconds))
|
|
235
|
+
throw new TopicReadUnavailable('Invalid grant expiration');
|
|
236
|
+
return new Date(
|
|
237
|
+
seconds * 1000 + grant.expiration.nanos / 1e6,
|
|
238
|
+
).toISOString();
|
|
239
|
+
})();
|
|
240
|
+
return {
|
|
241
|
+
existing: grant?.authorization
|
|
242
|
+
? {
|
|
243
|
+
typeUrl,
|
|
244
|
+
valueBase64: encode64(grant.authorization.value),
|
|
245
|
+
expiresAt,
|
|
246
|
+
}
|
|
247
|
+
: undefined,
|
|
248
|
+
dependency: {
|
|
249
|
+
kind: 'authorization' as const,
|
|
250
|
+
key: `${admin}\0${grantee}\0${role}`,
|
|
251
|
+
digest: await digestTopicProposalEffect(
|
|
252
|
+
grant
|
|
253
|
+
? {
|
|
254
|
+
authorization: grant.authorization
|
|
255
|
+
? {
|
|
256
|
+
typeUrl: grant.authorization.typeUrl,
|
|
257
|
+
valueBase64: encode64(grant.authorization.value),
|
|
258
|
+
}
|
|
259
|
+
: null,
|
|
260
|
+
expiration: grant.expiration
|
|
261
|
+
? {
|
|
262
|
+
seconds: grant.expiration.seconds.toString(),
|
|
263
|
+
nanos: grant.expiration.nanos,
|
|
264
|
+
}
|
|
265
|
+
: null,
|
|
266
|
+
}
|
|
267
|
+
: null,
|
|
268
|
+
),
|
|
269
|
+
},
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import {
|
|
2
|
+
materializeCreateStep,
|
|
3
|
+
TopicCreatePlanSchema,
|
|
4
|
+
} from '@ixo/ixo-transaction/topic-create-plan';
|
|
5
|
+
import {
|
|
6
|
+
ExpectedTopicEffectSchema,
|
|
7
|
+
type TopicActionProposal,
|
|
8
|
+
type TopicStepId,
|
|
9
|
+
type ExpectedTopicEffect,
|
|
10
|
+
} from '@ixo/ixo-transaction/topic-proposals';
|
|
11
|
+
export const topicIdFromPlan = (plan: unknown): string =>
|
|
12
|
+
TopicCreatePlanSchema.parse(plan).identity.topicId;
|
|
13
|
+
export async function topicExpectedEffect(
|
|
14
|
+
proposal: TopicActionProposal,
|
|
15
|
+
step: TopicStepId,
|
|
16
|
+
): Promise<ExpectedTopicEffect> {
|
|
17
|
+
const action = proposal.action;
|
|
18
|
+
if (action.name === 'grant_topic_access') {
|
|
19
|
+
if (step !== 'grant') throw new Error('Invalid grant step');
|
|
20
|
+
return {
|
|
21
|
+
kind: 'chain',
|
|
22
|
+
chainId: action.chainId,
|
|
23
|
+
signerAddress: action.signerAddress,
|
|
24
|
+
memo: action.memo,
|
|
25
|
+
messages: action.grants.map((g) => g.message),
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
if (step === 'grant') throw new Error('Invalid creation step');
|
|
29
|
+
const verified = (name: TopicStepId) => {
|
|
30
|
+
const s = proposal.lifecycle.steps.find((s) => s.step === name);
|
|
31
|
+
return s?.status === 'verified' && s.receipt.kind === 'matrix'
|
|
32
|
+
? s.receipt.eventId
|
|
33
|
+
: undefined;
|
|
34
|
+
};
|
|
35
|
+
const effect = await materializeCreateStep(action.plan, step, {
|
|
36
|
+
rootEventId: verified('anchor'),
|
|
37
|
+
bodyEventId: verified('body'),
|
|
38
|
+
});
|
|
39
|
+
const { step: _step, ...wire } = effect;
|
|
40
|
+
return ExpectedTopicEffectSchema.parse({
|
|
41
|
+
...wire,
|
|
42
|
+
transactionId: wire.transactionId ?? `${proposal.id}:${step}`,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { TopicProtocolError } from '@ixo/topic-protocol';
|
|
2
|
+
import { ZodError } from 'zod';
|
|
3
|
+
import { ActionProposalConflict } from './store';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* A room, DID or chain read that failed, timed out or came back oversized.
|
|
7
|
+
* Nothing about the card or its authority is known to have changed; the
|
|
8
|
+
* caller retries later. The shell answers it with 503 and a generic message,
|
|
9
|
+
* the reason stays in the log. Everything the reads DID establish about a
|
|
10
|
+
* card having changed is an `ActionProposalConflict` (409) instead, and any
|
|
11
|
+
* other error is a bug that surfaces as one.
|
|
12
|
+
*/
|
|
13
|
+
export class TopicReadUnavailable extends Error {
|
|
14
|
+
constructor(message: string) {
|
|
15
|
+
super(message);
|
|
16
|
+
this.name = 'TopicReadUnavailable';
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** The failures `boundedBytes` (`core/domain-context/transport`) reports. */
|
|
21
|
+
const TRANSPORT_FAILURES = new Set([
|
|
22
|
+
'document-unavailable',
|
|
23
|
+
'document-too-large',
|
|
24
|
+
]);
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Every failure leaving an authority read, typed. The readers throw their
|
|
28
|
+
* own conclusions typed already; what is left is the transport (a fetch
|
|
29
|
+
* that failed or timed out, a body too large or not JSON) and content an
|
|
30
|
+
* authority answered with that this contract does not accept (a Zod
|
|
31
|
+
* refusal of room state, a DID document or a chain query). The first is
|
|
32
|
+
* unavailable, the second a conflict reported without the schema's issue
|
|
33
|
+
* dump. Anything else is a bug and is rethrown as it was.
|
|
34
|
+
*/
|
|
35
|
+
export function typedTopicError(
|
|
36
|
+
error: unknown,
|
|
37
|
+
): ActionProposalConflict | TopicReadUnavailable {
|
|
38
|
+
if (
|
|
39
|
+
error instanceof ActionProposalConflict ||
|
|
40
|
+
error instanceof TopicReadUnavailable
|
|
41
|
+
)
|
|
42
|
+
return error;
|
|
43
|
+
if (
|
|
44
|
+
error instanceof TopicProtocolError ||
|
|
45
|
+
(error instanceof Error && error.name === 'TopicProtocolError')
|
|
46
|
+
) {
|
|
47
|
+
// A contract body or operation in the room that the topic protocol
|
|
48
|
+
// itself rejects: a fact about the authority, reported by its code. The
|
|
49
|
+
// name check covers a second copy of the package in the bundle.
|
|
50
|
+
const code =
|
|
51
|
+
error instanceof TopicProtocolError
|
|
52
|
+
? error.code
|
|
53
|
+
: 'code' in error
|
|
54
|
+
? String(error.code)
|
|
55
|
+
: 'unknown';
|
|
56
|
+
console.warn(
|
|
57
|
+
`[topic-actions] authority content rejected by the topic protocol: ${code}`,
|
|
58
|
+
);
|
|
59
|
+
return new ActionProposalConflict('Topic authority content is malformed');
|
|
60
|
+
}
|
|
61
|
+
if (error instanceof ZodError) {
|
|
62
|
+
console.warn(
|
|
63
|
+
`[topic-actions] authority content rejected: ${error.issues
|
|
64
|
+
.slice(0, 3)
|
|
65
|
+
.map((i) => `${i.path.join('.')}: ${i.message}`)
|
|
66
|
+
.join('; ')}`,
|
|
67
|
+
);
|
|
68
|
+
return new ActionProposalConflict('Topic authority content is malformed');
|
|
69
|
+
}
|
|
70
|
+
if (error instanceof SyntaxError)
|
|
71
|
+
return new TopicReadUnavailable('Topic authority answer is not JSON');
|
|
72
|
+
if (
|
|
73
|
+
error instanceof TypeError ||
|
|
74
|
+
error instanceof DOMException ||
|
|
75
|
+
(error instanceof Error && TRANSPORT_FAILURES.has(error.message))
|
|
76
|
+
)
|
|
77
|
+
return new TopicReadUnavailable(error.message);
|
|
78
|
+
throw error;
|
|
79
|
+
}
|