@pkcprotocol/pkc-js 0.0.41 → 0.0.43

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 (80) hide show
  1. package/README.md +11 -11
  2. package/dist/browser/clients/base-client-manager.d.ts +5 -1
  3. package/dist/browser/clients/base-client-manager.js +87 -12
  4. package/dist/browser/clients/base-client-manager.js.map +1 -1
  5. package/dist/browser/community/community-client-manager.d.ts +1 -0
  6. package/dist/browser/community/community-client-manager.js +176 -37
  7. package/dist/browser/community/community-client-manager.js.map +1 -1
  8. package/dist/browser/community/community-gateway-selection.d.ts +32 -0
  9. package/dist/browser/community/community-gateway-selection.js +28 -0
  10. package/dist/browser/community/community-gateway-selection.js.map +1 -0
  11. package/dist/browser/community/remote-community.d.ts +8 -0
  12. package/dist/browser/community/remote-community.js +67 -5
  13. package/dist/browser/community/remote-community.js.map +1 -1
  14. package/dist/browser/community/rpc-remote-community.js +19 -4
  15. package/dist/browser/community/rpc-remote-community.js.map +1 -1
  16. package/dist/browser/community/schema.d.ts +1 -0
  17. package/dist/browser/community/schema.js +3 -1
  18. package/dist/browser/community/schema.js.map +1 -1
  19. package/dist/browser/errors.d.ts +3 -0
  20. package/dist/browser/errors.js +3 -0
  21. package/dist/browser/errors.js.map +1 -1
  22. package/dist/browser/generated-version.d.ts +1 -1
  23. package/dist/browser/generated-version.js +1 -1
  24. package/dist/browser/helia/helia-for-pkc.js +71 -22
  25. package/dist/browser/helia/helia-for-pkc.js.map +1 -1
  26. package/dist/browser/pkc-error.d.ts +1 -1
  27. package/dist/browser/publications/publication.d.ts +3 -1
  28. package/dist/browser/publications/publication.js +1 -1
  29. package/dist/browser/publications/publication.js.map +1 -1
  30. package/dist/browser/rpc/src/index.js +1 -1
  31. package/dist/browser/rpc/src/index.js.map +1 -1
  32. package/dist/browser/runtime/node/community/local-community/ipns-publishing.js +1 -1
  33. package/dist/browser/runtime/node/community/local-community/ipns-publishing.js.map +1 -1
  34. package/dist/browser/schema/schema-util.d.ts +1 -0
  35. package/dist/browser/test/test-util.d.ts +334 -1
  36. package/dist/browser/test/test-util.js +87 -6
  37. package/dist/browser/test/test-util.js.map +1 -1
  38. package/dist/browser/util.d.ts +8 -1
  39. package/dist/browser/util.js +175 -2
  40. package/dist/browser/util.js.map +1 -1
  41. package/dist/node/clients/base-client-manager.d.ts +5 -1
  42. package/dist/node/clients/base-client-manager.js +87 -12
  43. package/dist/node/clients/base-client-manager.js.map +1 -1
  44. package/dist/node/community/community-client-manager.d.ts +1 -0
  45. package/dist/node/community/community-client-manager.js +176 -37
  46. package/dist/node/community/community-client-manager.js.map +1 -1
  47. package/dist/node/community/community-gateway-selection.d.ts +32 -0
  48. package/dist/node/community/community-gateway-selection.js +28 -0
  49. package/dist/node/community/community-gateway-selection.js.map +1 -0
  50. package/dist/node/community/remote-community.d.ts +8 -0
  51. package/dist/node/community/remote-community.js +67 -5
  52. package/dist/node/community/remote-community.js.map +1 -1
  53. package/dist/node/community/rpc-remote-community.js +19 -4
  54. package/dist/node/community/rpc-remote-community.js.map +1 -1
  55. package/dist/node/community/schema.d.ts +1 -0
  56. package/dist/node/community/schema.js +3 -1
  57. package/dist/node/community/schema.js.map +1 -1
  58. package/dist/node/errors.d.ts +3 -0
  59. package/dist/node/errors.js +3 -0
  60. package/dist/node/errors.js.map +1 -1
  61. package/dist/node/generated-version.d.ts +1 -1
  62. package/dist/node/generated-version.js +1 -1
  63. package/dist/node/helia/helia-for-pkc.js +71 -22
  64. package/dist/node/helia/helia-for-pkc.js.map +1 -1
  65. package/dist/node/pkc-error.d.ts +1 -1
  66. package/dist/node/publications/publication.d.ts +3 -1
  67. package/dist/node/publications/publication.js +1 -1
  68. package/dist/node/publications/publication.js.map +1 -1
  69. package/dist/node/rpc/src/index.js +1 -1
  70. package/dist/node/rpc/src/index.js.map +1 -1
  71. package/dist/node/runtime/node/community/local-community/ipns-publishing.js +1 -1
  72. package/dist/node/runtime/node/community/local-community/ipns-publishing.js.map +1 -1
  73. package/dist/node/schema/schema-util.d.ts +1 -0
  74. package/dist/node/test/test-util.d.ts +334 -1
  75. package/dist/node/test/test-util.js +87 -6
  76. package/dist/node/test/test-util.js.map +1 -1
  77. package/dist/node/util.d.ts +8 -1
  78. package/dist/node/util.js +175 -2
  79. package/dist/node/util.js.map +1 -1
  80. package/package.json +2 -2
@@ -202,8 +202,334 @@ export declare function getAvailablePKCConfigsToTestAgainst(opts?: {
202
202
  export declare function createNewIpns(): Promise<{
203
203
  signer: import("../signer/index.js").SignerWithPublicKeyAddress;
204
204
  publishToIpns: (content: string) => Promise<void>;
205
+ publishToIpnsValue: (value: string, opts?: {
206
+ verifyResolves?: boolean;
207
+ }) => Promise<void>;
205
208
  pkc: PKC;
206
209
  }>;
210
+ export declare function createDelegatedCommunityIpns(communityOpts?: Partial<CommunityIpfsType>, opts?: {
211
+ contentSigner?: SignerType;
212
+ intermediateHopsCount?: number;
213
+ }): Promise<{
214
+ anchorName: string;
215
+ terminalName: string;
216
+ intermediateNames: string[];
217
+ ipnsHops: string[];
218
+ communityRecord: {
219
+ challenges: {
220
+ [x: string]: unknown;
221
+ type: string;
222
+ exclude?: {
223
+ [x: string]: unknown;
224
+ community?: {
225
+ addresses: string[];
226
+ maxCommentCids: number;
227
+ postScore?: number | undefined;
228
+ replyScore?: number | undefined;
229
+ firstCommentTimestamp?: number | undefined;
230
+ } | undefined;
231
+ postScore?: number | undefined;
232
+ replyScore?: number | undefined;
233
+ postCount?: number | undefined;
234
+ replyCount?: number | undefined;
235
+ firstCommentTimestamp?: number | undefined;
236
+ challenges?: number[] | undefined;
237
+ role?: string[] | undefined;
238
+ address?: string[] | undefined;
239
+ rateLimit?: number | undefined;
240
+ rateLimitChallengeSuccess?: boolean | undefined;
241
+ publicationType?: {
242
+ [x: string]: unknown;
243
+ post?: boolean | undefined;
244
+ reply?: boolean | undefined;
245
+ vote?: boolean | undefined;
246
+ commentEdit?: boolean | undefined;
247
+ commentModeration?: boolean | undefined;
248
+ communityEdit?: boolean | undefined;
249
+ } | undefined;
250
+ }[] | undefined;
251
+ description?: string | undefined;
252
+ challenge?: string | undefined;
253
+ caseInsensitive?: boolean | undefined;
254
+ pendingApproval?: boolean | undefined;
255
+ }[];
256
+ signature: {
257
+ type: string;
258
+ signature: string;
259
+ publicKey: string;
260
+ signedPropertyNames: string[];
261
+ };
262
+ encryption: {
263
+ [x: string]: unknown;
264
+ type: string;
265
+ publicKey: string;
266
+ };
267
+ createdAt: number;
268
+ updatedAt: number;
269
+ statsCid: string;
270
+ protocolVersion: string;
271
+ posts?: {
272
+ pages: Record<string, {
273
+ comments: {
274
+ comment: {
275
+ [x: string]: unknown;
276
+ timestamp: number;
277
+ signature: {
278
+ type: string;
279
+ signature: string;
280
+ publicKey: string;
281
+ signedPropertyNames: string[];
282
+ };
283
+ protocolVersion: string;
284
+ depth: number;
285
+ flairs?: {
286
+ [x: string]: unknown;
287
+ text: string;
288
+ backgroundColor?: string | undefined;
289
+ textColor?: string | undefined;
290
+ expiresAt?: number | undefined;
291
+ }[] | undefined;
292
+ communityPublicKey?: string | undefined;
293
+ communityName?: string | undefined;
294
+ link?: string | undefined;
295
+ spoiler?: boolean | undefined;
296
+ nsfw?: boolean | undefined;
297
+ content?: string | undefined;
298
+ title?: string | undefined;
299
+ linkWidth?: number | undefined;
300
+ linkHeight?: number | undefined;
301
+ linkHtmlTagName?: string | undefined;
302
+ parentCid?: string | undefined;
303
+ postCid?: string | undefined;
304
+ quotedCids?: string[] | undefined;
305
+ author?: {
306
+ [x: string]: unknown;
307
+ name?: string | undefined;
308
+ previousCommentCid?: string | undefined;
309
+ displayName?: string | undefined;
310
+ wallets?: Record<string, {
311
+ address: string;
312
+ timestamp: number;
313
+ signature: {
314
+ signature: string;
315
+ type: string;
316
+ };
317
+ }> | undefined;
318
+ avatar?: {
319
+ [x: string]: unknown;
320
+ chainTicker: string;
321
+ address: string;
322
+ id: string;
323
+ timestamp: number;
324
+ signature: {
325
+ signature: string;
326
+ type: string;
327
+ };
328
+ } | undefined;
329
+ flairs?: {
330
+ [x: string]: unknown;
331
+ text: string;
332
+ backgroundColor?: string | undefined;
333
+ textColor?: string | undefined;
334
+ expiresAt?: number | undefined;
335
+ }[] | undefined;
336
+ } | undefined;
337
+ thumbnailUrl?: string | undefined;
338
+ thumbnailUrlWidth?: number | undefined;
339
+ thumbnailUrlHeight?: number | undefined;
340
+ previousCid?: string | undefined;
341
+ pseudonymityMode?: "per-post" | "per-reply" | "per-author" | undefined;
342
+ };
343
+ commentUpdate: {
344
+ [x: string]: unknown;
345
+ cid: string;
346
+ upvoteCount: number;
347
+ downvoteCount: number;
348
+ replyCount: number;
349
+ updatedAt: number;
350
+ signature: {
351
+ type: string;
352
+ signature: string;
353
+ publicKey: string;
354
+ signedPropertyNames: string[];
355
+ };
356
+ protocolVersion: string;
357
+ childCount?: number | undefined;
358
+ number?: number | undefined;
359
+ postNumber?: number | undefined;
360
+ edit?: {
361
+ [x: string]: unknown;
362
+ timestamp: number;
363
+ signature: {
364
+ type: string;
365
+ signature: string;
366
+ publicKey: string;
367
+ signedPropertyNames: string[];
368
+ };
369
+ protocolVersion: string;
370
+ commentCid: string;
371
+ flairs?: {
372
+ [x: string]: unknown;
373
+ text: string;
374
+ backgroundColor?: string | undefined;
375
+ textColor?: string | undefined;
376
+ expiresAt?: number | undefined;
377
+ }[] | undefined;
378
+ communityPublicKey?: string | undefined;
379
+ communityName?: string | undefined;
380
+ spoiler?: boolean | undefined;
381
+ nsfw?: boolean | undefined;
382
+ reason?: string | undefined;
383
+ content?: string | undefined;
384
+ deleted?: boolean | undefined;
385
+ author?: {
386
+ [x: string]: unknown;
387
+ name?: string | undefined;
388
+ previousCommentCid?: string | undefined;
389
+ displayName?: string | undefined;
390
+ wallets?: Record<string, {
391
+ address: string;
392
+ timestamp: number;
393
+ signature: {
394
+ signature: string;
395
+ type: string;
396
+ };
397
+ }> | undefined;
398
+ avatar?: {
399
+ [x: string]: unknown;
400
+ chainTicker: string;
401
+ address: string;
402
+ id: string;
403
+ timestamp: number;
404
+ signature: {
405
+ signature: string;
406
+ type: string;
407
+ };
408
+ } | undefined;
409
+ flairs?: {
410
+ [x: string]: unknown;
411
+ text: string;
412
+ backgroundColor?: string | undefined;
413
+ textColor?: string | undefined;
414
+ expiresAt?: number | undefined;
415
+ }[] | undefined;
416
+ } | undefined;
417
+ } | undefined;
418
+ flairs?: {
419
+ [x: string]: unknown;
420
+ text: string;
421
+ backgroundColor?: string | undefined;
422
+ textColor?: string | undefined;
423
+ expiresAt?: number | undefined;
424
+ }[] | undefined;
425
+ spoiler?: boolean | undefined;
426
+ nsfw?: boolean | undefined;
427
+ pinned?: boolean | undefined;
428
+ locked?: boolean | undefined;
429
+ archived?: boolean | undefined;
430
+ removed?: boolean | undefined;
431
+ reason?: string | undefined;
432
+ approved?: boolean | undefined;
433
+ author?: {
434
+ [x: string]: unknown;
435
+ community?: {
436
+ [x: string]: unknown;
437
+ postScore: number;
438
+ replyScore: number;
439
+ firstCommentTimestamp: number;
440
+ lastCommentCid: string;
441
+ banExpiresAt?: number | undefined;
442
+ flairs?: {
443
+ [x: string]: unknown;
444
+ text: string;
445
+ backgroundColor?: string | undefined;
446
+ textColor?: string | undefined;
447
+ expiresAt?: number | undefined;
448
+ }[] | undefined;
449
+ } | undefined;
450
+ } | undefined;
451
+ lastChildCid?: string | undefined;
452
+ lastReplyTimestamp?: number | undefined;
453
+ replies?: {
454
+ pages: Record<string, /*elided*/ any>;
455
+ pageCids?: Record<string, string> | undefined;
456
+ } | undefined;
457
+ };
458
+ }[];
459
+ nextCid?: string | undefined;
460
+ }>;
461
+ pageCids?: Record<string, string> | undefined;
462
+ } | undefined;
463
+ modQueue?: {
464
+ pageCids: Record<string, string>;
465
+ } | undefined;
466
+ name?: string | undefined;
467
+ pubsubTopic?: string | undefined;
468
+ postUpdates?: Record<string, string> | undefined;
469
+ title?: string | undefined;
470
+ description?: string | undefined;
471
+ roles?: Record<string, {
472
+ [x: string]: unknown;
473
+ role: string;
474
+ }> | undefined;
475
+ rules?: string[] | undefined;
476
+ lastPostCid?: string | undefined;
477
+ lastCommentCid?: string | undefined;
478
+ features?: {
479
+ [x: string]: unknown;
480
+ noVideos?: boolean | undefined;
481
+ noSpoilers?: boolean | undefined;
482
+ noImages?: boolean | undefined;
483
+ noVideoReplies?: boolean | undefined;
484
+ noSpoilerReplies?: boolean | undefined;
485
+ noImageReplies?: boolean | undefined;
486
+ noReplyLinks?: boolean | undefined;
487
+ noPolls?: boolean | undefined;
488
+ noCrossposts?: boolean | undefined;
489
+ noNestedReplies?: boolean | undefined;
490
+ safeForWork?: boolean | undefined;
491
+ authorFlairs?: boolean | undefined;
492
+ requireAuthorFlairs?: boolean | undefined;
493
+ postFlairs?: boolean | undefined;
494
+ requirePostFlairs?: boolean | undefined;
495
+ noMarkdownImages?: boolean | undefined;
496
+ noMarkdownVideos?: boolean | undefined;
497
+ noMarkdownAudio?: boolean | undefined;
498
+ noAudio?: boolean | undefined;
499
+ noAudioReplies?: boolean | undefined;
500
+ markdownImageReplies?: boolean | undefined;
501
+ markdownVideoReplies?: boolean | undefined;
502
+ noPostUpvotes?: boolean | undefined;
503
+ noReplyUpvotes?: boolean | undefined;
504
+ noPostDownvotes?: boolean | undefined;
505
+ noReplyDownvotes?: boolean | undefined;
506
+ noUpvotes?: boolean | undefined;
507
+ noDownvotes?: boolean | undefined;
508
+ requirePostLink?: boolean | undefined;
509
+ requirePostLinkIsMedia?: boolean | undefined;
510
+ requireReplyLink?: boolean | undefined;
511
+ requireReplyLinkIsMedia?: boolean | undefined;
512
+ pseudonymityMode?: "per-post" | "per-reply" | "per-author" | undefined;
513
+ } | undefined;
514
+ suggested?: {
515
+ [x: string]: unknown;
516
+ primaryColor?: string | undefined;
517
+ secondaryColor?: string | undefined;
518
+ avatarUrl?: string | undefined;
519
+ bannerUrl?: string | undefined;
520
+ backgroundUrl?: string | undefined;
521
+ language?: string | undefined;
522
+ } | undefined;
523
+ flairs?: Record<string, {
524
+ [x: string]: unknown;
525
+ text: string;
526
+ backgroundColor?: string | undefined;
527
+ textColor?: string | undefined;
528
+ expiresAt?: number | undefined;
529
+ }[]> | undefined;
530
+ };
531
+ cid: string;
532
+ }>;
207
533
  export declare function publishCommunityRecordWithExtraProp(opts?: {
208
534
  includeExtraPropInSignedPropertyNames: boolean;
209
535
  extraProps: Object;
@@ -212,6 +538,9 @@ export declare function publishCommunityRecordWithExtraProp(opts?: {
212
538
  ipnsObj: {
213
539
  signer: import("../signer/index.js").SignerWithPublicKeyAddress;
214
540
  publishToIpns: (content: string) => Promise<void>;
541
+ publishToIpnsValue: (value: string, opts?: {
542
+ verifyResolves?: boolean;
543
+ }) => Promise<void>;
215
544
  pkc: PKC;
216
545
  };
217
546
  }>;
@@ -533,6 +862,9 @@ export declare function createMockedCommunityIpns(communityOpts: CreateNewLocalC
533
862
  ipnsObj: {
534
863
  signer: import("../signer/index.js").SignerWithPublicKeyAddress;
535
864
  publishToIpns: (content: string) => Promise<void>;
865
+ publishToIpnsValue: (value: string, opts?: {
866
+ verifyResolves?: boolean;
867
+ }) => Promise<void>;
536
868
  pkc: PKC;
537
869
  };
538
870
  }>;
@@ -546,7 +878,7 @@ export declare function createStaticCommunityRecordForComment(opts?: {
546
878
  commentCid: string;
547
879
  communityAddress: string;
548
880
  }>;
549
- export declare function jsonifyCommunityAndRemoveInternalProps(community: RemoteCommunity): Omit<any, "signer" | "state" | "clients" | "settings" | "updatingState" | "startedState" | "editable" | "started">;
881
+ export declare function jsonifyCommunityAndRemoveInternalProps(community: RemoteCommunity): Omit<any, "signer" | "state" | "clients" | "settings" | "updatingState" | "ipnsHops" | "startedState" | "editable" | "started">;
550
882
  export declare function jsonifyLocalCommunityWithNoInternalProps(community: LocalCommunity): Omit<{
551
883
  address: string;
552
884
  publicKey?: string | undefined;
@@ -632,6 +964,7 @@ export declare function jsonifyLocalCommunityWithNoInternalProps(community: Loca
632
964
  readonly updatingState: RemoteCommunity["updatingState"];
633
965
  raw: RpcLocalCommunity["raw"];
634
966
  updateCid?: string | undefined;
967
+ ipnsHops?: string[] | undefined;
635
968
  startedState: import("../community/types.js").CommunityStartedState;
636
969
  editable: Pick<RpcLocalCommunity, keyof import("../community/types.js").CommunityEditOptions>;
637
970
  started: boolean;
@@ -751,7 +751,7 @@ export async function generatePostToAnswerMathQuestion(props, pkc) {
751
751
  const mockPost = await generateMockPost({ communityAddress: props.communityAddress, pkc, postProps: props });
752
752
  mockPost.removeAllListeners("challenge");
753
753
  mockPost.once("challenge", (challengeMessage) => {
754
- mockPost.publishChallengeAnswers(["2"]);
754
+ mockPost.publishChallengeAnswers({ challengeAnswers: ["2"] });
755
755
  });
756
756
  return mockPost;
757
757
  }
@@ -1193,8 +1193,14 @@ export async function createNewIpns() {
1193
1193
  url: pkc.kuboRpcClientsOptions[0].url.toString(),
1194
1194
  headers: pkc.kuboRpcClientsOptions[0].headers
1195
1195
  });
1196
- const publishToIpns = async (content) => {
1197
- const cid = await addStringToIpfs(content);
1196
+ // Publishes an arbitrary IPNS value under this signer's key. `value` may be a bare CID,
1197
+ // an "/ipfs/<cid>" path, or an "/ipns/<name>" path (the latter creates a recursive/
1198
+ // delegated IPNS record). Records are published with allowOffline: true. `verifyResolves`
1199
+ // ONLY controls the helper's own post-publish name.resolve() sanity check (which syncs Kubo's
1200
+ // cache for RPC tests) — it does NOT change what name.publish does. Set it false when
1201
+ // publishing a hop whose target isn't resolvable yet, so that resolve check doesn't fail.
1202
+ const publishToIpnsValue = async (value, opts) => {
1203
+ const verifyResolves = opts?.verifyResolves ?? true;
1198
1204
  // Wrapped in retry because Kubo can transiently ETIMEDOUT in CI
1199
1205
  await new Promise((resolve, reject) => {
1200
1206
  const operation = retry.operation({
@@ -1204,7 +1210,7 @@ export async function createNewIpns() {
1204
1210
  });
1205
1211
  operation.attempt(async () => {
1206
1212
  try {
1207
- await ipfsClient._client.name.publish(cid, {
1213
+ await ipfsClient._client.name.publish(value, {
1208
1214
  key: signer.address,
1209
1215
  allowOffline: true
1210
1216
  });
@@ -1217,10 +1223,12 @@ export async function createNewIpns() {
1217
1223
  }
1218
1224
  });
1219
1225
  });
1226
+ if (!verifyResolves)
1227
+ return;
1220
1228
  // Verify the IPNS record is resolvable before returning
1221
1229
  // This ensures Kubo's cache is properly synced for RPC tests
1222
1230
  // Wrapped in retry because Kubo can transiently ETIMEDOUT in CI
1223
- const resolvedCid = await new Promise((resolve, reject) => {
1231
+ await new Promise((resolve, reject) => {
1224
1232
  const operation = retry.operation({
1225
1233
  retries: 3,
1226
1234
  factor: 2,
@@ -1245,12 +1253,70 @@ export async function createNewIpns() {
1245
1253
  });
1246
1254
  });
1247
1255
  };
1256
+ const publishToIpns = async (content) => {
1257
+ const cid = await addStringToIpfs(content);
1258
+ await publishToIpnsValue(cid);
1259
+ };
1248
1260
  return {
1249
1261
  signer,
1250
1262
  publishToIpns,
1263
+ publishToIpnsValue,
1251
1264
  pkc
1252
1265
  };
1253
1266
  }
1267
+ // Builds a DELEGATED community IPNS chain for tests, mirroring issue #93 (delegated IPNS
1268
+ // publishing): an anchor IPNS name (An) whose record points to a minter IPNS name (Mn),
1269
+ // whose record points to the /ipfs/ CID of a community record signed by Mn. Clients load
1270
+ // the anchor (An) but the content is signed by the terminal/minter key (Mn).
1271
+ // See docs/protocol/delegated-ipns.md. Note: delegate-side publishing is NOT part of pkc-js,
1272
+ // so this helper constructs the chain directly via kubo.
1273
+ export async function createDelegatedCommunityIpns(communityOpts, opts) {
1274
+ // anchor (owner) keypair (An)
1275
+ const anchor = await createNewIpns();
1276
+ // optional intermediate delegate keypairs: anchor -> intermediate[0] -> ... -> minter
1277
+ const intermediates = [];
1278
+ for (let i = 0; i < (opts?.intermediateHopsCount ?? 0); i++)
1279
+ intermediates.push(await createNewIpns());
1280
+ // minter (terminal delegate) keypair (Mn) — signs the content
1281
+ const minter = await createNewIpns();
1282
+ // ordered chain [anchor, ...intermediates, minter]
1283
+ const chain = [anchor, ...intermediates, minter];
1284
+ try {
1285
+ const communityRecord = {
1286
+ ...(await getTemplateCommunityRecord(anchor.pkc)),
1287
+ posts: undefined,
1288
+ pubsubTopic: minter.signer.address,
1289
+ ...communityOpts
1290
+ };
1291
+ if (!communityRecord.posts)
1292
+ delete communityRecord.posts;
1293
+ // content is signed by the MINTER key (Mn) by default — the terminal of the chain. A test
1294
+ // may override contentSigner to simulate a record whose signer is NOT the terminal key.
1295
+ communityRecord.signature = await signCommunity({ community: communityRecord, signer: opts?.contentSigner ?? minter.signer });
1296
+ const communityRecordJson = JSON.stringify(communityRecord);
1297
+ const cid = await addStringToIpfs(communityRecordJson);
1298
+ // Publish TERMINAL-FIRST so each hop's target already resolves before its parent is
1299
+ // published — this keeps the reliable verifyResolves path for every hop:
1300
+ // minter -> /ipfs/<cid>, then each node -> /ipns/<next node>, finally anchor -> /ipns/<first delegate>.
1301
+ await minter.publishToIpnsValue(cid);
1302
+ for (let i = chain.length - 2; i >= 0; i--)
1303
+ await chain[i].publishToIpnsValue(`/ipns/${chain[i + 1].signer.address}`);
1304
+ return {
1305
+ anchorName: anchor.signer.address, // == community.publicKey / user-facing identity (An)
1306
+ terminalName: minter.signer.address, // signs the content (Mn)
1307
+ intermediateNames: intermediates.map((node) => node.signer.address),
1308
+ ipnsHops: chain.map((node) => node.signer.address), // [anchor, ...intermediates, terminal]
1309
+ communityRecord,
1310
+ cid
1311
+ };
1312
+ }
1313
+ finally {
1314
+ // The records now live on the kubo node; the helper PKCs are no longer needed. Destroy them
1315
+ // even if signing/publishing threw, otherwise their Kubo/libp2p handles leak.
1316
+ for (const node of chain)
1317
+ await node.pkc.destroy();
1318
+ }
1319
+ }
1254
1320
  async function getTemplateCommunityRecord(pkc) {
1255
1321
  const community = await pkc.createCommunity({ address: "12D3KooWANwdyPERMQaCgiMnTT1t3Lr4XLFbK1z4ptFVhW2ozg1z" });
1256
1322
  await community.update();
@@ -1373,7 +1439,22 @@ export function jsonifyCommunityAndRemoveInternalProps(community) {
1373
1439
  }
1374
1440
  _stripNameResolvedFromPages(jsonfied["posts"]);
1375
1441
  _stripNameResolvedFromPages(jsonfied["modQueue"]);
1376
- return remeda.omit(jsonfied, ["startedState", "started", "signer", "settings", "editable", "clients", "updatingState", "state"]);
1442
+ // ipnsHops records the IPNS delegation chain traversed to RESOLVE a community, so it only exists
1443
+ // on a freshly-resolved remote instance — a local community (which owns the keys and publishes
1444
+ // rather than resolves) and a not-yet-resolved instance legitimately have none. It is a runtime
1445
+ // resolution artifact (a CommunityIpfs reserved field, like nameResolved), so strip it before
1446
+ // comparing community content/identity. See docs/protocol/delegated-ipns.md.
1447
+ return remeda.omit(jsonfied, [
1448
+ "ipnsHops",
1449
+ "startedState",
1450
+ "started",
1451
+ "signer",
1452
+ "settings",
1453
+ "editable",
1454
+ "clients",
1455
+ "updatingState",
1456
+ "state"
1457
+ ]);
1377
1458
  }
1378
1459
  export function jsonifyLocalCommunityWithNoInternalProps(community) {
1379
1460
  const localJson = JSON.parse(JSON.stringify(community));