@ixo/oracle-runtime-workers 0.41.3 → 0.42.1
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/package.json +5 -5
- package/src/attachments/download.ts +1 -1
- package/src/core/runtime-context.ts +2 -2
- package/src/do/ambient.ts +2 -1
- package/src/plugin-api/types.ts +4 -1
- package/src/plugins/domain-indexer/domain-indexer.plugin.ts +1 -1
- package/src/plugins/ixo-chain/http.ts +8 -4
- package/src/plugins/ixo-chain/ixo-chain.plugin.ts +14 -1
- package/src/plugins/ixo-chain/queries.ts +26 -0
- package/src/plugins/pod-creator/chain-gateway.ts +123 -31
- package/src/plugins/pod-creator/config.ts +81 -1
- package/src/plugins/pod-creator/create-session-store.ts +159 -7
- package/src/plugins/pod-creator/create-tools.ts +403 -28
- package/src/plugins/pod-creator/domain-card.ts +67 -0
- package/src/plugins/pod-creator/index.ts +24 -2
- package/src/plugins/pod-creator/ixo-chain-gateway.ts +901 -0
- package/src/plugins/pod-creator/matrix-media.ts +486 -0
- package/src/plugins/pod-creator/orchestration-tools.ts +5 -2
- package/src/plugins/pod-creator/pod-creator.plugin.ts +30 -9
- package/src/plugins/pod-creator/stage.ts +30 -0
- package/src/plugins/pod-creator/sub-agents.ts +204 -4
|
@@ -6,6 +6,8 @@ import {
|
|
|
6
6
|
buildBatchSignTransactionActionArgs,
|
|
7
7
|
describeValidationError,
|
|
8
8
|
validateTransactionBatch,
|
|
9
|
+
type BatchMessageRoute,
|
|
10
|
+
type ITrxMsg,
|
|
9
11
|
type Network,
|
|
10
12
|
type RiskConfirmation,
|
|
11
13
|
type ValidatedTransactionBatch,
|
|
@@ -22,6 +24,7 @@ import {
|
|
|
22
24
|
notConfiguredChainGateway,
|
|
23
25
|
podBatchProblem,
|
|
24
26
|
type ChainGateway,
|
|
27
|
+
type PodConfirmation,
|
|
25
28
|
} from './chain-gateway';
|
|
26
29
|
import { readPodCreatorConfig } from './config';
|
|
27
30
|
import type { CreateSessionStoreFor } from './create-session-store';
|
|
@@ -48,6 +51,49 @@ const TX_HASH_PATTERN = /^[0-9a-fA-F]{64}$/;
|
|
|
48
51
|
/** The blob store name of a prepared batch. */
|
|
49
52
|
export const POD_BATCH_BLOB_NAME = 'pod-batch-messages';
|
|
50
53
|
|
|
54
|
+
/**
|
|
55
|
+
* Characters plain text never carries: format characters (bidi overrides,
|
|
56
|
+
* zero-width), line and paragraph separators, private-use and unassigned
|
|
57
|
+
* code points.
|
|
58
|
+
*/
|
|
59
|
+
const INVISIBLE_CHARACTERS = /[\p{Cf}\p{Zl}\p{Zp}\p{Co}\p{Cn}]/u;
|
|
60
|
+
/** Control characters, which a one-line name never carries. */
|
|
61
|
+
const CONTROL_CHARACTERS = /\p{Cc}/u;
|
|
62
|
+
/** Control characters other than a line break or a tab. */
|
|
63
|
+
const CONTROL_EXCEPT_LINES = /[^\P{Cc}\n\t]/u;
|
|
64
|
+
|
|
65
|
+
/** The longest POD description: it is quoted in full in the batch summary the user approves. */
|
|
66
|
+
export const MAX_POD_DESCRIPTION_LENGTH = 600;
|
|
67
|
+
|
|
68
|
+
const prepareSchema = z.object({
|
|
69
|
+
name: z
|
|
70
|
+
.string()
|
|
71
|
+
.trim()
|
|
72
|
+
.min(1)
|
|
73
|
+
.max(100)
|
|
74
|
+
.refine(
|
|
75
|
+
(name) =>
|
|
76
|
+
!CONTROL_CHARACTERS.test(name) && !INVISIBLE_CHARACTERS.test(name),
|
|
77
|
+
{ message: 'The name is one line of plain text.' },
|
|
78
|
+
)
|
|
79
|
+
.describe(
|
|
80
|
+
"The POD's name exactly as the user agreed it in the design (plain text, at most 100 characters). It is the name on the POD's public profile.",
|
|
81
|
+
),
|
|
82
|
+
description: z
|
|
83
|
+
.string()
|
|
84
|
+
.trim()
|
|
85
|
+
.max(MAX_POD_DESCRIPTION_LENGTH)
|
|
86
|
+
.refine(
|
|
87
|
+
(text) =>
|
|
88
|
+
!CONTROL_EXCEPT_LINES.test(text) && !INVISIBLE_CHARACTERS.test(text),
|
|
89
|
+
{ message: 'The description is plain text.' },
|
|
90
|
+
)
|
|
91
|
+
.optional()
|
|
92
|
+
.describe(
|
|
93
|
+
`One or two plain sentences on what the POD does, from the design (at most ${MAX_POD_DESCRIPTION_LENGTH} characters). Shown on its public profile.`,
|
|
94
|
+
),
|
|
95
|
+
});
|
|
96
|
+
|
|
51
97
|
const blobIdSchema = z.object({
|
|
52
98
|
blobId: z
|
|
53
99
|
.string()
|
|
@@ -60,6 +106,17 @@ const approveSchema = blobIdSchema.extend({
|
|
|
60
106
|
),
|
|
61
107
|
});
|
|
62
108
|
|
|
109
|
+
const emptySchema = z.object({});
|
|
110
|
+
|
|
111
|
+
const ADD_LINKED_RESOURCE = '/ixo.iid.v1beta1.MsgAddLinkedResource';
|
|
112
|
+
/** The linked resource id of an entity's domain card, which the chain fills in. */
|
|
113
|
+
const DOMAIN_CARD_RESOURCE_ID = '{id}#dmn';
|
|
114
|
+
|
|
115
|
+
const domainCardMessageSchema = z.object({
|
|
116
|
+
id: z.string(),
|
|
117
|
+
linkedResource: z.object({ id: z.string() }),
|
|
118
|
+
});
|
|
119
|
+
|
|
63
120
|
const confirmSchema = z.object({
|
|
64
121
|
txHash: z
|
|
65
122
|
.string()
|
|
@@ -77,15 +134,31 @@ const storedBatchSchema = z
|
|
|
77
134
|
network: NetworkSchema,
|
|
78
135
|
summary: z.string(),
|
|
79
136
|
messages: z.array(ITrxMsgSchema),
|
|
137
|
+
/** The profile the user agreed, for the POD's domain card later. */
|
|
138
|
+
profile: z.object({
|
|
139
|
+
name: z.string(),
|
|
140
|
+
description: z.string().optional(),
|
|
141
|
+
}),
|
|
142
|
+
/** The documents the messages name, uploaded after approval. */
|
|
143
|
+
documents: z.array(
|
|
144
|
+
z
|
|
145
|
+
.object({
|
|
146
|
+
mxc: z.string(),
|
|
147
|
+
contentType: z.string(),
|
|
148
|
+
filename: z.string(),
|
|
149
|
+
text: z.string(),
|
|
150
|
+
})
|
|
151
|
+
.strict(),
|
|
152
|
+
),
|
|
80
153
|
})
|
|
81
154
|
.strict();
|
|
82
155
|
|
|
83
156
|
type StoredBatch = z.infer<typeof storedBatchSchema>;
|
|
84
157
|
|
|
85
158
|
const CHAIN_UNAVAILABLE_MESSAGE =
|
|
86
|
-
'On-chain POD creation is
|
|
87
|
-
'
|
|
88
|
-
'
|
|
159
|
+
'On-chain POD creation is turned off on this oracle: it was built ' +
|
|
160
|
+
'without a chain gateway. The design blueprint is saved; creating the ' +
|
|
161
|
+
'POD on chain needs an oracle that has one.';
|
|
89
162
|
|
|
90
163
|
/**
|
|
91
164
|
* Validate a blobId and return the stored batch, throwing when it is missing,
|
|
@@ -137,7 +210,12 @@ function validateStoredBatch(
|
|
|
137
210
|
},
|
|
138
211
|
): ValidatedTransactionBatch {
|
|
139
212
|
return validateTransactionBatch(
|
|
140
|
-
{
|
|
213
|
+
{
|
|
214
|
+
network: batch.network,
|
|
215
|
+
summary: batch.summary,
|
|
216
|
+
messages: batch.messages,
|
|
217
|
+
riskConfirmation: options.riskConfirmation,
|
|
218
|
+
},
|
|
141
219
|
{
|
|
142
220
|
allowMainnet: options.mainnetAllowed,
|
|
143
221
|
requireRiskConfirmation: options.riskConfirmation !== undefined,
|
|
@@ -183,6 +261,42 @@ export type RequestPodSignatureResult =
|
|
|
183
261
|
}
|
|
184
262
|
| { status: 'unavailable'; error: string; message: string };
|
|
185
263
|
|
|
264
|
+
/** What `confirm_pod_creation` returns to the model. */
|
|
265
|
+
export type ConfirmPodCreationResult =
|
|
266
|
+
| {
|
|
267
|
+
created: true;
|
|
268
|
+
podDid: string;
|
|
269
|
+
/** The POD in the Portal. */
|
|
270
|
+
portalUrl: string;
|
|
271
|
+
summary: string;
|
|
272
|
+
message: string;
|
|
273
|
+
}
|
|
274
|
+
| {
|
|
275
|
+
created: false;
|
|
276
|
+
status: Exclude<PodConfirmation['status'], 'created'>;
|
|
277
|
+
/** True: call again later with the same hash. */
|
|
278
|
+
retryable: boolean;
|
|
279
|
+
message: string;
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
/** What `add_pod_domain_card` returns to the model. */
|
|
283
|
+
export type AddPodDomainCardResult =
|
|
284
|
+
| { status: 'added'; podDid: string; txHash?: string; message: string }
|
|
285
|
+
| { status: 'refused'; message: string }
|
|
286
|
+
| {
|
|
287
|
+
status: 'failed';
|
|
288
|
+
code: number;
|
|
289
|
+
txHash?: string;
|
|
290
|
+
error: string;
|
|
291
|
+
message: string;
|
|
292
|
+
}
|
|
293
|
+
| {
|
|
294
|
+
status: 'rejected' | 'error' | 'unavailable';
|
|
295
|
+
error: string;
|
|
296
|
+
message: string;
|
|
297
|
+
}
|
|
298
|
+
| { status: 'timeout'; outcome: 'unknown'; error: string };
|
|
299
|
+
|
|
186
300
|
const RETRY_AFTER_SPENT_APPROVAL =
|
|
187
301
|
'Nothing was signed and the approval was spent: to retry, the user confirms again in a new message and you call approve_pod_transaction, then request_pod_signature.';
|
|
188
302
|
|
|
@@ -229,7 +343,8 @@ export function createCreateTools(
|
|
|
229
343
|
const chainUnavailable = gateway === notConfiguredChainGateway;
|
|
230
344
|
|
|
231
345
|
const prepare = tool(
|
|
232
|
-
async (
|
|
346
|
+
async (args, ctx) => {
|
|
347
|
+
const profile = prepareSchema.parse(args);
|
|
233
348
|
const bp = await blueprintsFor(ctx).get(threadId(ctx));
|
|
234
349
|
if (!bp) {
|
|
235
350
|
return {
|
|
@@ -261,9 +376,21 @@ export function createCreateTools(
|
|
|
261
376
|
}
|
|
262
377
|
const blueprint = assembleServicePodBlueprint(bp);
|
|
263
378
|
const prepared = await gateway.preparePodBatch(
|
|
264
|
-
{ blueprint, network },
|
|
379
|
+
{ blueprint, network, profile },
|
|
265
380
|
ctx,
|
|
266
381
|
);
|
|
382
|
+
if (!prepared.ok) {
|
|
383
|
+
ctx.logger.warn(
|
|
384
|
+
`[pod-creator] prepare refused user=${ctx.user.did} thread=${threadId(ctx)}: ${prepared.message}`,
|
|
385
|
+
);
|
|
386
|
+
return {
|
|
387
|
+
prepared: false,
|
|
388
|
+
retryable: prepared.retryable,
|
|
389
|
+
message: prepared.retryable
|
|
390
|
+
? `${prepared.message} Nothing was prepared; call prepare_pod_transaction again shortly.`
|
|
391
|
+
: `${prepared.message} Nothing was prepared; tell the user what is needed.`,
|
|
392
|
+
};
|
|
393
|
+
}
|
|
267
394
|
// The wallet's handler refuses anything outside the catalog, so a
|
|
268
395
|
// batch it would refuse is caught here, before the user is asked.
|
|
269
396
|
let batch: ValidatedTransactionBatch;
|
|
@@ -289,6 +416,8 @@ export function createCreateTools(
|
|
|
289
416
|
network: batch.network,
|
|
290
417
|
summary: batch.summary,
|
|
291
418
|
messages: batch.messages,
|
|
419
|
+
profile,
|
|
420
|
+
documents: prepared.documents,
|
|
292
421
|
};
|
|
293
422
|
const blobId = await ctx.blobStore.put({
|
|
294
423
|
userDid: ctx.user.did,
|
|
@@ -307,6 +436,8 @@ export function createCreateTools(
|
|
|
307
436
|
return {
|
|
308
437
|
prepared: true,
|
|
309
438
|
blobId,
|
|
439
|
+
name: profile.name,
|
|
440
|
+
description: profile.description ?? '',
|
|
310
441
|
summary: batch.summary,
|
|
311
442
|
messageCount: batch.messages.length,
|
|
312
443
|
messages: batch.routes.map((route) => route.messageName),
|
|
@@ -316,14 +447,14 @@ export function createCreateTools(
|
|
|
316
447
|
? { estimatedCost: prepared.estimatedCost }
|
|
317
448
|
: {}),
|
|
318
449
|
message:
|
|
319
|
-
'Transaction batch prepared. Show the user the summary and every risk, word for word, and stop: approve_pod_transaction only works on a later message in which the user explicitly confirms, and needs each risk they accepted quoted exactly.',
|
|
450
|
+
'Transaction batch prepared. Show the user the summary (with the exact public name and description) and every risk, word for word, and stop: approve_pod_transaction only works on a later message in which the user explicitly confirms, and needs each risk they accepted quoted exactly. Nothing is public yet: the profile is published only after their approval.',
|
|
320
451
|
};
|
|
321
452
|
},
|
|
322
453
|
{
|
|
323
454
|
name: 'prepare_pod_transaction',
|
|
324
455
|
description:
|
|
325
|
-
|
|
326
|
-
schema:
|
|
456
|
+
"Build the on-chain POD creation batch (unsigned messages) from the approved blueprint, with the POD's name and description from the design, and stash it for the user to sign in their own wallet. Reserves the POD's public profile (name and description), which is published only once the user approved the batch. Returns its summary, message names and risks, or prepared: false with what is missing. Only works once the launch-readiness gate has passed; refuses on mainnet unless the operator opted in.",
|
|
457
|
+
schema: prepareSchema,
|
|
327
458
|
},
|
|
328
459
|
);
|
|
329
460
|
|
|
@@ -418,21 +549,45 @@ export function createCreateTools(
|
|
|
418
549
|
const risks = validateStoredBatch(batch, { mainnetAllowed }).risks;
|
|
419
550
|
const signArgs = buildBatchSignTransactionActionArgs(
|
|
420
551
|
{
|
|
421
|
-
|
|
552
|
+
network: batch.network,
|
|
553
|
+
summary: batch.summary,
|
|
554
|
+
messages: batch.messages,
|
|
422
555
|
riskConfirmation: { confirmed: true, acceptedRisks: risks },
|
|
423
556
|
},
|
|
424
557
|
{ allowMainnet: mainnetAllowed, chainIds },
|
|
425
558
|
);
|
|
559
|
+
const notApproved = new Error(
|
|
560
|
+
'Transaction not approved (or its approval was already used). Call approve_pod_transaction after the user explicitly confirms the batch.',
|
|
561
|
+
);
|
|
562
|
+
const session = await sessionsFor(ctx).read(ctx.user.did, threadId(ctx));
|
|
563
|
+
if (session?.approvedBlobId !== blobId) throw notApproved;
|
|
564
|
+
// The profile becomes public only now, after the user's approval and
|
|
565
|
+
// before it is spent: a homeserver that is down keeps the approval.
|
|
566
|
+
if (batch.documents.length > 0) {
|
|
567
|
+
const published = await gateway.publishDocuments(
|
|
568
|
+
{ documents: batch.documents },
|
|
569
|
+
ctx,
|
|
570
|
+
);
|
|
571
|
+
if (!published.ok) {
|
|
572
|
+
ctx.logger.warn(
|
|
573
|
+
`[pod-creator] profile not published for batch ${blobId} user=${ctx.user.did}: ${published.message}`,
|
|
574
|
+
);
|
|
575
|
+
return {
|
|
576
|
+
status: 'unavailable',
|
|
577
|
+
error: published.message,
|
|
578
|
+
message: published.retryable
|
|
579
|
+
? "Nothing was sent and the approval is kept: the POD's profile could not be published yet. Call request_pod_signature again in a moment."
|
|
580
|
+
: "Nothing was sent: the POD's profile cannot be published from this batch. Call prepare_pod_transaction again, show the user the new batch and ask them to approve it.",
|
|
581
|
+
};
|
|
582
|
+
}
|
|
583
|
+
}
|
|
426
584
|
const consumed = await sessionsFor(ctx).consume(
|
|
427
585
|
ctx.user.did,
|
|
428
586
|
threadId(ctx),
|
|
429
587
|
blobId,
|
|
588
|
+
{ messages: batch.messages, profile: batch.profile },
|
|
430
589
|
);
|
|
431
|
-
if (!consumed)
|
|
432
|
-
throw new Error(
|
|
433
|
-
'Transaction not approved (or its approval was already used). Call approve_pod_transaction after the user explicitly confirms the batch.',
|
|
434
|
-
);
|
|
435
|
-
}
|
|
590
|
+
if (!consumed) throw notApproved;
|
|
436
591
|
const outcome = await requestWalletSignature(ctx, {
|
|
437
592
|
args: signArgs,
|
|
438
593
|
callIdPrefix: 'pod',
|
|
@@ -506,33 +661,253 @@ export function createCreateTools(
|
|
|
506
661
|
);
|
|
507
662
|
|
|
508
663
|
const confirm = tool(
|
|
509
|
-
async (args, ctx) => {
|
|
664
|
+
async (args, ctx): Promise<ConfirmPodCreationResult> => {
|
|
510
665
|
const { txHash } = confirmSchema.parse(args);
|
|
511
666
|
if (chainUnavailable) {
|
|
512
|
-
return {
|
|
667
|
+
return {
|
|
668
|
+
created: false,
|
|
669
|
+
status: 'unavailable',
|
|
670
|
+
retryable: false,
|
|
671
|
+
message: CHAIN_UNAVAILABLE_MESSAGE,
|
|
672
|
+
};
|
|
513
673
|
}
|
|
514
|
-
const
|
|
515
|
-
const
|
|
516
|
-
|
|
674
|
+
const sessions = sessionsFor(ctx);
|
|
675
|
+
const session = await sessions.read(ctx.user.did, threadId(ctx));
|
|
676
|
+
if (!session?.sent) {
|
|
677
|
+
return {
|
|
678
|
+
created: false,
|
|
679
|
+
status: 'failed',
|
|
680
|
+
retryable: false,
|
|
681
|
+
message:
|
|
682
|
+
'No POD batch was sent for signing in this conversation (or it was more than an hour ago), so there is nothing to confirm here. A POD is confirmed in the conversation that prepared and sent it.',
|
|
683
|
+
};
|
|
684
|
+
}
|
|
685
|
+
const { network, portalUrl } = readPodCreatorConfig(ctx);
|
|
686
|
+
const outcome = await gateway.confirmPodCreation(
|
|
687
|
+
{ txHash, network, sent: session.sent },
|
|
517
688
|
ctx,
|
|
518
689
|
);
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
690
|
+
const audit = `tx=${txHash} user=${ctx.user.did} thread=${threadId(ctx)}`;
|
|
691
|
+
if (outcome.status !== 'created') {
|
|
692
|
+
ctx.logger.warn(
|
|
693
|
+
`[pod-creator] confirm ${outcome.status} ${audit}: ${outcome.message}`,
|
|
694
|
+
);
|
|
695
|
+
return {
|
|
696
|
+
created: false,
|
|
697
|
+
status: outcome.status,
|
|
698
|
+
retryable: outcome.retryable,
|
|
699
|
+
message: outcome.message,
|
|
700
|
+
};
|
|
701
|
+
}
|
|
702
|
+
await sessions.confirmed(ctx.user.did, threadId(ctx), {
|
|
703
|
+
podDid: outcome.podDid,
|
|
704
|
+
txHash,
|
|
705
|
+
});
|
|
706
|
+
ctx.logger.log(`[pod-creator] confirmed POD ${outcome.podDid} ${audit}`);
|
|
523
707
|
return {
|
|
524
708
|
created: true,
|
|
525
|
-
podDid:
|
|
526
|
-
|
|
709
|
+
podDid: outcome.podDid,
|
|
710
|
+
portalUrl: `${portalUrl}/domain/${outcome.podDid}/overview`,
|
|
711
|
+
summary: outcome.summary,
|
|
712
|
+
message:
|
|
713
|
+
"Tell the user the POD exists and give them its Portal link; its public profile (name and description) is published. Then offer the next step: add the POD's public domain card (add_pod_domain_card, one more wallet signature), so the Portal lists it and search finds it.",
|
|
527
714
|
};
|
|
528
715
|
},
|
|
529
716
|
{
|
|
530
717
|
name: 'confirm_pod_creation',
|
|
531
718
|
description:
|
|
532
|
-
'Confirm the POD was created on
|
|
719
|
+
'Confirm the POD was created on chain from the signed transaction hash; returns the new POD DID and its Portal link. Reads the chain for up to 20 s: created: false with status pending (not on chain yet) or unavailable (chain not readable) and retryable: true means call it again later with the same hash; status failed means no POD of this conversation was created (do not retry).',
|
|
533
720
|
schema: confirmSchema,
|
|
721
|
+
effect: 'read',
|
|
722
|
+
},
|
|
723
|
+
);
|
|
724
|
+
|
|
725
|
+
const addDomainCard = tool(
|
|
726
|
+
async (args, ctx): Promise<AddPodDomainCardResult> => {
|
|
727
|
+
emptySchema.parse(args);
|
|
728
|
+
if (chainUnavailable) {
|
|
729
|
+
return {
|
|
730
|
+
status: 'unavailable',
|
|
731
|
+
error: CHAIN_UNAVAILABLE_MESSAGE,
|
|
732
|
+
message: 'Nothing was sent.',
|
|
733
|
+
};
|
|
734
|
+
}
|
|
735
|
+
const { network, mainnetAllowed, chainIds } = readPodCreatorConfig(ctx);
|
|
736
|
+
if (network === 'mainnet' && !mainnetAllowed) {
|
|
737
|
+
return {
|
|
738
|
+
status: 'refused',
|
|
739
|
+
message:
|
|
740
|
+
'Mainnet POD creation is disabled on this oracle (POD_CREATOR_ALLOW_MAINNET), so no domain card is added.',
|
|
741
|
+
};
|
|
742
|
+
}
|
|
743
|
+
const unavailable = walletUnavailableReason(ctx);
|
|
744
|
+
if (unavailable !== null) {
|
|
745
|
+
return {
|
|
746
|
+
status: 'unavailable',
|
|
747
|
+
error: unavailable,
|
|
748
|
+
message:
|
|
749
|
+
'Nothing was sent: call add_pod_domain_card again once the user has the chat open in the Portal.',
|
|
750
|
+
};
|
|
751
|
+
}
|
|
752
|
+
const sessions = sessionsFor(ctx);
|
|
753
|
+
const claim = await sessions.claimCard(ctx.user.did, threadId(ctx));
|
|
754
|
+
if (claim.status === 'no-pod') {
|
|
755
|
+
return {
|
|
756
|
+
status: 'refused',
|
|
757
|
+
message:
|
|
758
|
+
'No POD was confirmed in this conversation (confirm_pod_creation first), so there is no POD to add a domain card to.',
|
|
759
|
+
};
|
|
760
|
+
}
|
|
761
|
+
if (claim.status === 'claimed') {
|
|
762
|
+
return {
|
|
763
|
+
status: 'refused',
|
|
764
|
+
message:
|
|
765
|
+
claim.state === 'added'
|
|
766
|
+
? 'The domain card of this POD was already added; it is added once.'
|
|
767
|
+
: 'The domain card of this POD was already sent to the wallet and its outcome is not known. Ask the user whether they signed it; do not send it again.',
|
|
768
|
+
};
|
|
769
|
+
}
|
|
770
|
+
const { podDid } = claim;
|
|
771
|
+
const release = () =>
|
|
772
|
+
sessions.settleCard(ctx.user.did, threadId(ctx), 'released');
|
|
773
|
+
const prepared = await gateway.prepareDomainCard(
|
|
774
|
+
{ network, podDid, profile: claim.sent.profile },
|
|
775
|
+
ctx,
|
|
776
|
+
);
|
|
777
|
+
if (!prepared.ok) {
|
|
778
|
+
await release();
|
|
779
|
+
return {
|
|
780
|
+
status: 'unavailable',
|
|
781
|
+
error: prepared.message,
|
|
782
|
+
message: prepared.retryable
|
|
783
|
+
? 'Nothing was sent: call add_pod_domain_card again in a moment.'
|
|
784
|
+
: 'Nothing was sent: tell the user what is needed.',
|
|
785
|
+
};
|
|
786
|
+
}
|
|
787
|
+
let signArgs: ReturnType<typeof buildBatchSignTransactionActionArgs>;
|
|
788
|
+
try {
|
|
789
|
+
const batch = validateTransactionBatch(
|
|
790
|
+
{ messages: prepared.messages, summary: prepared.summary, network },
|
|
791
|
+
{ allowMainnet: mainnetAllowed },
|
|
792
|
+
);
|
|
793
|
+
const notCard = domainCardBatchProblem(batch.routes, prepared, podDid);
|
|
794
|
+
if (notCard !== null) throw new Error(notCard);
|
|
795
|
+
// The user reviews and signs the card in the Portal's own consent
|
|
796
|
+
// prompt: an update of an entity they own, nothing else.
|
|
797
|
+
signArgs = buildBatchSignTransactionActionArgs(
|
|
798
|
+
{
|
|
799
|
+
network,
|
|
800
|
+
summary: batch.summary,
|
|
801
|
+
messages: batch.messages,
|
|
802
|
+
riskConfirmation: { confirmed: true, acceptedRisks: batch.risks },
|
|
803
|
+
},
|
|
804
|
+
{ allowMainnet: mainnetAllowed, chainIds },
|
|
805
|
+
);
|
|
806
|
+
} catch (error) {
|
|
807
|
+
await release();
|
|
808
|
+
throw new Error(
|
|
809
|
+
`The chain gateway built a domain card batch the wallet would refuse: ${describeValidationError(error)}`,
|
|
810
|
+
);
|
|
811
|
+
}
|
|
812
|
+
const published = await gateway.publishDocuments(
|
|
813
|
+
{ documents: prepared.documents },
|
|
814
|
+
ctx,
|
|
815
|
+
);
|
|
816
|
+
if (!published.ok) {
|
|
817
|
+
await release();
|
|
818
|
+
return {
|
|
819
|
+
status: 'unavailable',
|
|
820
|
+
error: published.message,
|
|
821
|
+
message: published.retryable
|
|
822
|
+
? 'Nothing was sent: the card could not be published yet. Call add_pod_domain_card again in a moment.'
|
|
823
|
+
: 'Nothing was sent: the card could not be published. Tell the user.',
|
|
824
|
+
};
|
|
825
|
+
}
|
|
826
|
+
const outcome = await requestWalletSignature(ctx, {
|
|
827
|
+
args: signArgs,
|
|
828
|
+
callIdPrefix: 'pod_card',
|
|
829
|
+
timeoutMs: SIGN_TIMEOUT_MS,
|
|
830
|
+
});
|
|
831
|
+
const audit = `card of ${podDid} user=${ctx.user.did} thread=${threadId(ctx)}`;
|
|
832
|
+
switch (outcome.status) {
|
|
833
|
+
case 'signed': {
|
|
834
|
+
await sessions.settleCard(ctx.user.did, threadId(ctx), 'added');
|
|
835
|
+
ctx.logger.log(
|
|
836
|
+
`[pod-creator] wallet signed ${audit} tx=${outcome.transactionHash ?? 'none'}`,
|
|
837
|
+
);
|
|
838
|
+
await gateway.requestIndexing({ network, podDid }, ctx);
|
|
839
|
+
return {
|
|
840
|
+
status: 'added',
|
|
841
|
+
podDid,
|
|
842
|
+
...(outcome.transactionHash !== undefined
|
|
843
|
+
? { txHash: outcome.transactionHash }
|
|
844
|
+
: {}),
|
|
845
|
+
message:
|
|
846
|
+
'The domain card was added. The domain indexer was asked to index the POD; it can take a few minutes to show in the Portal lists and search.',
|
|
847
|
+
};
|
|
848
|
+
}
|
|
849
|
+
case 'failed':
|
|
850
|
+
await release();
|
|
851
|
+
ctx.logger.warn(
|
|
852
|
+
`[pod-creator] card failed on chain ${audit} code=${outcome.code}`,
|
|
853
|
+
);
|
|
854
|
+
return {
|
|
855
|
+
status: 'failed',
|
|
856
|
+
code: outcome.code,
|
|
857
|
+
...(outcome.transactionHash !== undefined
|
|
858
|
+
? { txHash: outcome.transactionHash }
|
|
859
|
+
: {}),
|
|
860
|
+
error: outcome.error,
|
|
861
|
+
message:
|
|
862
|
+
'The transaction failed on chain, so no card was added. Tell the user the code and error.',
|
|
863
|
+
};
|
|
864
|
+
case 'rejected':
|
|
865
|
+
case 'error':
|
|
866
|
+
case 'unavailable':
|
|
867
|
+
await release();
|
|
868
|
+
ctx.logger.warn(
|
|
869
|
+
`[pod-creator] card ${outcome.status} ${audit}: ${outcome.error}`,
|
|
870
|
+
);
|
|
871
|
+
return {
|
|
872
|
+
status: outcome.status,
|
|
873
|
+
error: outcome.error,
|
|
874
|
+
message:
|
|
875
|
+
'Nothing was signed. If the user wants the card, call add_pod_domain_card again.',
|
|
876
|
+
};
|
|
877
|
+
case 'timeout':
|
|
878
|
+
ctx.logger.warn(`[pod-creator] card outcome unknown ${audit}`);
|
|
879
|
+
return outcome;
|
|
880
|
+
}
|
|
881
|
+
},
|
|
882
|
+
{
|
|
883
|
+
name: 'add_pod_domain_card',
|
|
884
|
+
description:
|
|
885
|
+
"After confirm_pod_creation: add the new POD's public domain card (its name and description, as the Portal publishes for a domain it creates) with one wallet signature in the Portal, then ask the domain indexer to index it, so the POD shows in the Portal's lists and search. Offer it to the user; it runs once per POD.",
|
|
886
|
+
schema: emptySchema,
|
|
534
887
|
},
|
|
535
888
|
);
|
|
536
889
|
|
|
537
|
-
return [prepare, approve, requestSignature, confirm];
|
|
890
|
+
return [prepare, approve, requestSignature, confirm, addDomainCard];
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
/**
|
|
894
|
+
* Why a validated batch is not the domain card of `podDid`, or null when it
|
|
895
|
+
* is: one `MsgAddLinkedResource` on the POD, its `{id}#dmn` card.
|
|
896
|
+
*/
|
|
897
|
+
function domainCardBatchProblem(
|
|
898
|
+
routes: readonly BatchMessageRoute[],
|
|
899
|
+
prepared: { messages: readonly ITrxMsg[] },
|
|
900
|
+
podDid: string,
|
|
901
|
+
): string | null {
|
|
902
|
+
const [message] = prepared.messages;
|
|
903
|
+
const card = domainCardMessageSchema.safeParse(message?.value);
|
|
904
|
+
if (
|
|
905
|
+
routes.length !== 1 ||
|
|
906
|
+
routes[0]?.typeUrl !== ADD_LINKED_RESOURCE ||
|
|
907
|
+
!card.success ||
|
|
908
|
+
card.data.id !== podDid ||
|
|
909
|
+
card.data.linkedResource.id !== DOMAIN_CARD_RESOURCE_ID
|
|
910
|
+
)
|
|
911
|
+
return `A domain card batch is one ${ADD_LINKED_RESOURCE} of ${DOMAIN_CARD_RESOURCE_ID} on ${podDid}`;
|
|
912
|
+
return null;
|
|
538
913
|
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { PodProfile } from './chain-gateway';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A POD's domain card, as the Portal composes a fresh one for a domain it
|
|
5
|
+
* creates (ixo-portal `lib/entity/domainCard.ts`, `freshDomainCardDocument`):
|
|
6
|
+
* a W3C VerifiableCredential of type `ixo:DomainCard` issued by the owner,
|
|
7
|
+
* whose subject is the entity. The domain indexer reads it from the entity's
|
|
8
|
+
* `{id}#dmn` linked resource to list and search the domain. No logo, image,
|
|
9
|
+
* tags or purpose: the POD creator collects only the name and description.
|
|
10
|
+
*/
|
|
11
|
+
export function podDomainCardDocument(input: {
|
|
12
|
+
podDid: string;
|
|
13
|
+
ownerDid: string;
|
|
14
|
+
profile: PodProfile;
|
|
15
|
+
validFrom: string;
|
|
16
|
+
}): Record<string, unknown> {
|
|
17
|
+
const { podDid, ownerDid, profile, validFrom } = input;
|
|
18
|
+
return {
|
|
19
|
+
'@context': [
|
|
20
|
+
'https://www.w3.org/ns/credentials/v2',
|
|
21
|
+
'https://w3id.org/ixo/context/v1',
|
|
22
|
+
{
|
|
23
|
+
attribute: 'https://w3id.org/ixo/attributes/v1/',
|
|
24
|
+
schema: 'https://schema.org/',
|
|
25
|
+
ixo: 'https://w3id.org/ixo/vocab/v1',
|
|
26
|
+
prov: 'http://www.w3.org/ns/prov#',
|
|
27
|
+
proj: 'https://linked.data.gov.au/def/project#',
|
|
28
|
+
xsd: 'http://www.w3.org/2001/XMLSchema#',
|
|
29
|
+
id: '@id',
|
|
30
|
+
type: '@type',
|
|
31
|
+
'ixo:vector': { '@container': '@list', '@type': 'xsd:double' },
|
|
32
|
+
'@protected': true,
|
|
33
|
+
},
|
|
34
|
+
],
|
|
35
|
+
id: `${podDid}#dmn`,
|
|
36
|
+
type: ['VerifiableCredential', 'ixo:DomainCard'],
|
|
37
|
+
issuer: { id: ownerDid },
|
|
38
|
+
validFrom,
|
|
39
|
+
credentialSchema: {
|
|
40
|
+
id: 'https://w3id.org/ixo/schemas/ixo-domain-card-1.json',
|
|
41
|
+
type: 'JsonSchema',
|
|
42
|
+
},
|
|
43
|
+
credentialSubject: {
|
|
44
|
+
id: podDid,
|
|
45
|
+
type: ['ixo:Domain'],
|
|
46
|
+
additionalType: ['schema:Organization'],
|
|
47
|
+
name: profile.name,
|
|
48
|
+
description: profile.description ?? '',
|
|
49
|
+
sameAs: [],
|
|
50
|
+
logo: { type: 'schema:ImageObject', id: '', contentUrl: '' },
|
|
51
|
+
image: [{ type: 'schema:ImageObject', id: '', contentUrl: '' }],
|
|
52
|
+
keywords: [],
|
|
53
|
+
knowsAbout: [],
|
|
54
|
+
composition: { type: 'schema:Collection', hasPart: [] },
|
|
55
|
+
makesOffer: [],
|
|
56
|
+
relationships: {
|
|
57
|
+
'prov:wasAssociatedWith': [],
|
|
58
|
+
memberOf: [],
|
|
59
|
+
funding: [],
|
|
60
|
+
},
|
|
61
|
+
agents: [],
|
|
62
|
+
credentials: [],
|
|
63
|
+
attributes: [],
|
|
64
|
+
project: {},
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
}
|
|
@@ -37,10 +37,17 @@ export {
|
|
|
37
37
|
type Readiness,
|
|
38
38
|
} from './stage';
|
|
39
39
|
export { createOrchestrationTools } from './orchestration-tools';
|
|
40
|
-
export { buildStageSubAgents } from './sub-agents';
|
|
41
40
|
export {
|
|
41
|
+
buildSpecialistSubAgents,
|
|
42
|
+
buildStageSubAgents,
|
|
43
|
+
createSpecialistStageMiddleware,
|
|
44
|
+
} from './sub-agents';
|
|
45
|
+
export {
|
|
46
|
+
MAX_POD_DESCRIPTION_LENGTH,
|
|
42
47
|
SIGN_TRANSACTION_ACTION,
|
|
43
48
|
createCreateTools,
|
|
49
|
+
type AddPodDomainCardResult,
|
|
50
|
+
type ConfirmPodCreationResult,
|
|
44
51
|
type RequestPodSignatureResult,
|
|
45
52
|
} from './create-tools';
|
|
46
53
|
export {
|
|
@@ -48,17 +55,32 @@ export {
|
|
|
48
55
|
notConfiguredChainGateway,
|
|
49
56
|
podBatchProblem,
|
|
50
57
|
type ChainGateway,
|
|
51
|
-
type
|
|
58
|
+
type GatewayRefusal,
|
|
59
|
+
type GatewayResult,
|
|
60
|
+
type PendingDocument,
|
|
61
|
+
type PodConfirmation,
|
|
62
|
+
type PodProfile,
|
|
52
63
|
type PreparedPodBatch,
|
|
64
|
+
type SentPodBatch,
|
|
53
65
|
} from './chain-gateway';
|
|
66
|
+
export {
|
|
67
|
+
createIxoChainGateway,
|
|
68
|
+
podProfileDocument,
|
|
69
|
+
type IxoChainGatewayDeps,
|
|
70
|
+
} from './ixo-chain-gateway';
|
|
71
|
+
export { podDomainCardDocument } from './domain-card';
|
|
54
72
|
export {
|
|
55
73
|
KvCreateSessionStore,
|
|
56
74
|
type ApproveOutcome,
|
|
75
|
+
type CardClaim,
|
|
57
76
|
type CreateSessionStore,
|
|
77
|
+
type CreateSessionView,
|
|
58
78
|
type CreateSessionStoreFor,
|
|
59
79
|
} from './create-session-store';
|
|
60
80
|
export {
|
|
61
81
|
DEFAULT_NETWORK,
|
|
82
|
+
POD_RELAYER_NODE_DIDS,
|
|
83
|
+
PORTAL_URLS,
|
|
62
84
|
podCreatorConfigSchema,
|
|
63
85
|
readPodCreatorConfig,
|
|
64
86
|
} from './config';
|