@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.
@@ -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 not yet enabled on this oracle — the chain ' +
87
- 'gateway is not configured. The design blueprint is saved; creation can ' +
88
- 'proceed once the operator wires the IXO MCP chain gateway.';
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
- { ...batch, riskConfirmation: options.riskConfirmation },
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 (_args, ctx) => {
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
- 'Build the on-chain POD creation batch (unsigned messages) from the approved blueprint and stash it for the user to sign. Returns its summary, message names and risks. Only works once the launch-readiness gate has passed; refuses on mainnet unless the operator opted in.',
326
- schema: z.object({}),
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
- ...batch,
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 { created: false, message: CHAIN_UNAVAILABLE_MESSAGE };
667
+ return {
668
+ created: false,
669
+ status: 'unavailable',
670
+ retryable: false,
671
+ message: CHAIN_UNAVAILABLE_MESSAGE,
672
+ };
513
673
  }
514
- const { network } = readPodCreatorConfig(ctx);
515
- const created = await gateway.confirmPodCreation(
516
- { txHash, network },
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
- await sessionsFor(ctx).clear(ctx.user.did, threadId(ctx));
520
- ctx.logger.log(
521
- `[pod-creator] confirmed POD ${created.podDid} tx=${txHash} user=${ctx.user.did} thread=${threadId(ctx)}`,
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: created.podDid,
526
- summary: created.summary,
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-chain from the signed transaction hash; returns the new POD DID.',
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 CreatedPod,
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';