@peers-app/peers-sdk 0.25.11 → 0.25.12

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.
@@ -10,4 +10,5 @@ export * from "./package-remote-checker";
10
10
  export * from "./package-seed-installer";
11
11
  export * from "./package-tarball";
12
12
  export * from "./peers-core-installer";
13
+ export * from "./peers-core-trust-anchor";
13
14
  export * from "./types";
@@ -26,4 +26,5 @@ __exportStar(require("./package-remote-checker"), exports);
26
26
  __exportStar(require("./package-seed-installer"), exports);
27
27
  __exportStar(require("./package-tarball"), exports);
28
28
  __exportStar(require("./peers-core-installer"), exports);
29
+ __exportStar(require("./peers-core-trust-anchor"), exports);
29
30
  __exportStar(require("./types"), exports);
@@ -2,6 +2,16 @@ import type { IPackageVersion } from "../data/package-versions";
2
2
  import type { IAppNav } from "../types/app-nav";
3
3
  /** Prefix for the persistent variable name that stores a package's signing secret key. */
4
4
  export declare const PACKAGE_SIGNING_KEY_PREFIX = "packageSigningKey_";
5
+ /**
6
+ * Whether `publicKey` is a publisher key that must never be trusted again.
7
+ *
8
+ * The built-in list is {@link peersCoreRevokedPublishPublicKeys}; callers (and tests) may pass
9
+ * their own. An empty or missing key is not "revoked", it is simply not a key.
10
+ *
11
+ * @param publicKey - base64url Ed25519 public key to check
12
+ * @param revoked - list to check against; defaults to the peers-core revocations
13
+ */
14
+ export declare function isRevokedPublisherKey(publicKey: string | undefined, revoked?: readonly string[]): boolean;
5
15
  /**
6
16
  * The subset of PackageVersion fields that the publisher signs.
7
17
  * The `publicKey` field is included so verifiers know which key to check against.
@@ -47,8 +57,13 @@ export interface IVerifyAuthorSignatureResult {
47
57
  * is NOT in the signature itself — it must be provided via `expectedPublicKey` (TOFU check)
48
58
  * to reconstruct the payload and verify.
49
59
  *
60
+ * A key in {@link peersCoreRevokedPublishPublicKeys} never verifies, even with a
61
+ * mathematically valid signature: a revoked key may be in an attacker's hands, so anything it
62
+ * signed after revocation is untrustworthy and anything before is already installed.
63
+ *
50
64
  * @param pv - The PackageVersion record with `packageAuthorSignature` set
51
65
  * @param expectedPublicKey - The public key to verify against (from Package.publishPublicKey)
52
- * @returns `{ valid, publicKey }` — valid is true if signature verifies and key matches
66
+ * @returns `{ valid, publicKey }` — valid is true if signature verifies, key matches, and the
67
+ * key is not revoked
53
68
  */
54
69
  export declare function verifyPackageAuthorSignature(pv: AuthorSignableFields & Pick<IPackageVersion, "packageAuthorSignature">, expectedPublicKey: string): IVerifyAuthorSignatureResult;
@@ -1,14 +1,28 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.PACKAGE_SIGNING_KEY_PREFIX = void 0;
4
+ exports.isRevokedPublisherKey = isRevokedPublisherKey;
4
5
  exports.buildAuthorSignedPayload = buildAuthorSignedPayload;
5
6
  exports.signPackageAuthor = signPackageAuthor;
6
7
  exports.verifyPackageAuthorSignature = verifyPackageAuthorSignature;
7
8
  const nacl = require("tweetnacl");
8
9
  const tweetnacl_util_1 = require("tweetnacl-util");
9
10
  const keys_1 = require("../keys");
11
+ const system_ids_1 = require("../system-ids");
10
12
  /** Prefix for the persistent variable name that stores a package's signing secret key. */
11
13
  exports.PACKAGE_SIGNING_KEY_PREFIX = "packageSigningKey_";
14
+ /**
15
+ * Whether `publicKey` is a publisher key that must never be trusted again.
16
+ *
17
+ * The built-in list is {@link peersCoreRevokedPublishPublicKeys}; callers (and tests) may pass
18
+ * their own. An empty or missing key is not "revoked", it is simply not a key.
19
+ *
20
+ * @param publicKey - base64url Ed25519 public key to check
21
+ * @param revoked - list to check against; defaults to the peers-core revocations
22
+ */
23
+ function isRevokedPublisherKey(publicKey, revoked = system_ids_1.peersCoreRevokedPublishPublicKeys) {
24
+ return !!publicKey && revoked.includes(publicKey);
25
+ }
12
26
  /**
13
27
  * Extracts the signable payload from a PackageVersion record.
14
28
  * Only includes optional fields when they have a value (undefined fields are omitted
@@ -59,13 +73,18 @@ function signPackageAuthor(pv, secretKey) {
59
73
  * is NOT in the signature itself — it must be provided via `expectedPublicKey` (TOFU check)
60
74
  * to reconstruct the payload and verify.
61
75
  *
76
+ * A key in {@link peersCoreRevokedPublishPublicKeys} never verifies, even with a
77
+ * mathematically valid signature: a revoked key may be in an attacker's hands, so anything it
78
+ * signed after revocation is untrustworthy and anything before is already installed.
79
+ *
62
80
  * @param pv - The PackageVersion record with `packageAuthorSignature` set
63
81
  * @param expectedPublicKey - The public key to verify against (from Package.publishPublicKey)
64
- * @returns `{ valid, publicKey }` — valid is true if signature verifies and key matches
82
+ * @returns `{ valid, publicKey }` — valid is true if signature verifies, key matches, and the
83
+ * key is not revoked
65
84
  */
66
85
  function verifyPackageAuthorSignature(pv, expectedPublicKey) {
67
86
  const result = { valid: false, publicKey: expectedPublicKey };
68
- if (!pv.packageAuthorSignature) {
87
+ if (!pv.packageAuthorSignature || isRevokedPublisherKey(expectedPublicKey)) {
69
88
  return result;
70
89
  }
71
90
  try {
@@ -11,6 +11,7 @@ const package_versions_1 = require("../data/package-versions");
11
11
  const packages_1 = require("../data/packages");
12
12
  const utils_1 = require("../utils");
13
13
  const package_author_signing_1 = require("./package-author-signing");
14
+ const peers_core_trust_anchor_1 = require("./peers-core-trust-anchor");
14
15
  // --- Module-level state ---
15
16
  const RETRY_DELAYS_MS = [2_000, 10_000, 30_000, 60_000, 120_000];
16
17
  const pvPropagateMap = {};
@@ -190,6 +191,12 @@ async function retryOrFail(source, dest, pv, retryCnt, reason) {
190
191
  * transient failures (missing source key).
191
192
  */
192
193
  async function verifyOrEstablishTofu(sourceContext, targetContext, targetPkg, pv) {
194
+ // peers-core pinned to a revoked key: move the pin to the host's current key first so a
195
+ // version signed by the replacement key can be accepted.
196
+ const repinned = await (0, peers_core_trust_anchor_1.repinPeersCoreIfRevoked)(targetContext, targetPkg);
197
+ if (repinned !== targetPkg) {
198
+ targetPkg.publishPublicKey = repinned.publishPublicKey;
199
+ }
193
200
  if (targetPkg.publishPublicKey) {
194
201
  if (!(0, package_author_signing_1.verifyPackageAuthorSignature)(pv, targetPkg.publishPublicKey).valid) {
195
202
  console.error(`[propagation] Invalid signature for ${pv.packageVersionId}`);
@@ -201,6 +208,11 @@ async function verifyOrEstablishTofu(sourceContext, targetContext, targetPkg, pv
201
208
  const sourcePkg = await (0, packages_1.Packages)(sourceContext).get(targetPkg.packageId);
202
209
  if (!sourcePkg?.publishPublicKey)
203
210
  return false;
211
+ // Never adopt a revoked key as a trust anchor, however it reached the source group.
212
+ if ((0, package_author_signing_1.isRevokedPublisherKey)(sourcePkg.publishPublicKey)) {
213
+ console.error(`[propagation] Refusing revoked publisher key for ${targetPkg.packageId} from ${sourceContext.dataContextId}`);
214
+ return false;
215
+ }
204
216
  if (!(0, package_author_signing_1.verifyPackageAuthorSignature)(pv, sourcePkg.publishPublicKey).valid) {
205
217
  console.error(`[propagation] Invalid signature for ${pv.packageVersionId} (TOFU)`);
206
218
  return false;
@@ -2,10 +2,26 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const package_versions_1 = require("../data/package-versions");
4
4
  const keys_1 = require("../keys");
5
+ const systemIds = require("../system-ids");
5
6
  const utils_1 = require("../utils");
6
7
  const package_author_signing_1 = require("./package-author-signing");
7
8
  const package_propagation_1 = require("./package-propagation");
8
9
  // --- Mocks ---
10
+ // Pin the host to a fresh "current" peers-core key and revoke a fresh "old" one so the
11
+ // revocation tests do not depend on the real constants.
12
+ jest.mock("../system-ids", () => {
13
+ const actual = jest.requireActual("../system-ids");
14
+ const { newKeys: makeKeys } = jest.requireActual("../keys");
15
+ const revoked = makeKeys();
16
+ const current = makeKeys();
17
+ return {
18
+ ...actual,
19
+ peersCorePublishPublicKey: current.publicKey,
20
+ peersCoreRevokedPublishPublicKeys: [revoked.publicKey],
21
+ __testKeys: { revoked, current },
22
+ };
23
+ });
24
+ const { revoked: revokedCoreKeys, current: currentCoreKeys } = systemIds.__testKeys;
9
25
  const mockGetUserRole = jest.fn();
10
26
  jest.mock("../data/group-permissions", () => ({
11
27
  getUserRole: (...args) => mockGetUserRole(...args),
@@ -589,6 +605,114 @@ describe("discoverAndPropagateVersions", () => {
589
605
  expect(results).toHaveLength(0);
590
606
  consoleError.mockRestore();
591
607
  });
608
+ describe("revoked publisher keys", () => {
609
+ function twoGroupSetup() {
610
+ const groupA = "group-a";
611
+ const groupB = "group-b";
612
+ const groups = new Map([
613
+ [groupA, makeDataContext(groupA)],
614
+ [groupB, makeDataContext(groupB)],
615
+ ]);
616
+ const roles = new Map([
617
+ [groupA, 60],
618
+ [groupB, 60],
619
+ ]);
620
+ return { groupA, groupB, userContext: makeUserContext({ userId, groups }), roles };
621
+ }
622
+ let consoleError;
623
+ let consoleWarn;
624
+ beforeEach(() => {
625
+ consoleError = jest.spyOn(console, "error").mockImplementation(() => { });
626
+ consoleWarn = jest.spyOn(console, "warn").mockImplementation(() => { });
627
+ });
628
+ afterEach(() => {
629
+ consoleError.mockRestore();
630
+ consoleWarn.mockRestore();
631
+ });
632
+ it("refuses to establish TOFU from a source pinned to a revoked key", async () => {
633
+ const { groupA, groupB, userContext, roles } = twoGroupSetup();
634
+ const packageId = (0, utils_1.newid)();
635
+ const sourcePkg = makePkg({ packageId, publishPublicKey: revokedCoreKeys.publicKey });
636
+ const targetPkg = makePkg({ packageId, publishPublicKey: "" });
637
+ const pv = makeSignedPVWithSecretKey(packageId, revokedCoreKeys.secretKey, {
638
+ version: "2.0.0",
639
+ });
640
+ setupMocksForScenario({
641
+ packages: new Map([[groupB, [targetPkg]]]),
642
+ pvs: new Map([
643
+ [groupA, [pv]],
644
+ [groupB, []],
645
+ ]),
646
+ roles,
647
+ sourcePackages: new Map([[groupA, new Map([[packageId, sourcePkg]])]]),
648
+ });
649
+ const results = await (0, package_propagation_1.discoverAndPropagateVersions)(userContext);
650
+ expect(results).toHaveLength(0);
651
+ expect(mockPackagesSignAndSave).not.toHaveBeenCalled();
652
+ expect(mockPvSignAndSave).not.toHaveBeenCalled();
653
+ expect(consoleError).toHaveBeenCalledWith(expect.stringContaining("Refusing revoked publisher key"));
654
+ });
655
+ it("re-pins peers-core from a revoked key and accepts a version signed by the current key", async () => {
656
+ const { groupA, groupB, userContext, roles } = twoGroupSetup();
657
+ const packageId = systemIds.peersCorePackageId;
658
+ const sourcePkg = makePkg({ packageId, publishPublicKey: currentCoreKeys.publicKey });
659
+ const targetPkg = makePkg({ packageId, publishPublicKey: revokedCoreKeys.publicKey });
660
+ const pv = makeSignedPVWithSecretKey(packageId, currentCoreKeys.secretKey, {
661
+ version: "2.0.0",
662
+ });
663
+ setupMocksForScenario({
664
+ packages: new Map([[groupB, [targetPkg]]]),
665
+ pvs: new Map([
666
+ [groupA, [pv]],
667
+ [groupB, []],
668
+ ]),
669
+ roles,
670
+ sourcePackages: new Map([[groupA, new Map([[packageId, sourcePkg]])]]),
671
+ });
672
+ const results = await (0, package_propagation_1.discoverAndPropagateVersions)(userContext);
673
+ expect(results).toHaveLength(1);
674
+ expect(results[0]).toMatchObject({ groupId: groupB, packageId, version: "2.0.0" });
675
+ expect(mockPackagesSignAndSave).toHaveBeenCalledWith(expect.objectContaining({ packageId, publishPublicKey: currentCoreKeys.publicKey }));
676
+ });
677
+ it("rejects a peers-core version signed by the revoked key even where that key is pinned", async () => {
678
+ const { groupA, groupB, userContext, roles } = twoGroupSetup();
679
+ const packageId = systemIds.peersCorePackageId;
680
+ const targetPkg = makePkg({ packageId, publishPublicKey: revokedCoreKeys.publicKey });
681
+ const pv = makeSignedPVWithSecretKey(packageId, revokedCoreKeys.secretKey, {
682
+ version: "2.0.0",
683
+ });
684
+ setupMocksForScenario({
685
+ packages: new Map([[groupB, [targetPkg]]]),
686
+ pvs: new Map([
687
+ [groupA, [pv]],
688
+ [groupB, []],
689
+ ]),
690
+ roles,
691
+ });
692
+ const results = await (0, package_propagation_1.discoverAndPropagateVersions)(userContext);
693
+ expect(results).toHaveLength(0);
694
+ expect(mockPvSignAndSave).not.toHaveBeenCalled();
695
+ });
696
+ it("does not re-pin other packages pinned to a revoked key; their versions are rejected", async () => {
697
+ const { groupA, groupB, userContext, roles } = twoGroupSetup();
698
+ const packageId = (0, utils_1.newid)();
699
+ const targetPkg = makePkg({ packageId, publishPublicKey: revokedCoreKeys.publicKey });
700
+ const pv = makeSignedPVWithSecretKey(packageId, revokedCoreKeys.secretKey, {
701
+ version: "2.0.0",
702
+ });
703
+ setupMocksForScenario({
704
+ packages: new Map([[groupB, [targetPkg]]]),
705
+ pvs: new Map([
706
+ [groupA, [pv]],
707
+ [groupB, []],
708
+ ]),
709
+ roles,
710
+ });
711
+ const results = await (0, package_propagation_1.discoverAndPropagateVersions)(userContext);
712
+ expect(results).toHaveLength(0);
713
+ expect(mockPackagesSignAndSave).not.toHaveBeenCalled();
714
+ });
715
+ });
592
716
  it("skips PV when packageVersionId already exists in target (dedup)", async () => {
593
717
  const groupA = "group-a";
594
718
  const groupB = "group-b";
@@ -19,6 +19,7 @@ const package_author_signing_1 = require("./package-author-signing");
19
19
  const package_installer_1 = require("./package-installer");
20
20
  const package_propagation_1 = require("./package-propagation");
21
21
  const package_tarball_1 = require("./package-tarball");
22
+ const peers_core_trust_anchor_1 = require("./peers-core-trust-anchor");
22
23
  // Module-level memoization: unique by `updateUrl + ":" + tag`
23
24
  const remoteCheckCache = new Map();
24
25
  /**
@@ -44,9 +45,13 @@ function checkPackageRemoteForNewVersion(packageId, updateUrl, tag, http) {
44
45
  * group does not already have the remote PackageVersion.
45
46
  */
46
47
  async function checkAndInstallPackageRemoteVersion(dataContext, pkg, tag, http) {
47
- if (!pkg.updateUrl || pkg.disabled)
48
+ const updateUrl = pkg.updateUrl;
49
+ if (!updateUrl || pkg.disabled)
48
50
  return null;
49
- const result = await checkPackageRemoteForNewVersion(pkg.packageId, pkg.updateUrl, tag, http);
51
+ // A peers-core row pinned to a revoked publisher key would reject every new release; move
52
+ // it to the host's current anchor before looking at the remote.
53
+ await (0, peers_core_trust_anchor_1.repinPeersCoreIfRevoked)(dataContext, pkg);
54
+ const result = await checkPackageRemoteForNewVersion(pkg.packageId, updateUrl, tag, http);
50
55
  if (!result)
51
56
  return null;
52
57
  const existing = await (0, package_versions_1.PackageVersions)(dataContext).get(result.pointer.packageVersionId);
@@ -32,13 +32,30 @@ const mockResolveDevicePackageVersion = jest.fn();
32
32
  jest.mock("../data/package-version-resolver", () => ({
33
33
  resolveDevicePackageVersion: (...args) => mockResolveDevicePackageVersion(...args),
34
34
  }));
35
+ // Pin the host to a fresh "current" peers-core key and revoke a fresh "old" one so the
36
+ // revocation tests do not depend on the real constants.
37
+ jest.mock("../system-ids", () => {
38
+ const actual = jest.requireActual("../system-ids");
39
+ const { newKeys: makeKeys } = jest.requireActual("../keys");
40
+ const revoked = makeKeys();
41
+ const current = makeKeys();
42
+ return {
43
+ ...actual,
44
+ peersCorePublishPublicKey: current.publicKey,
45
+ peersCoreRevokedPublishPublicKeys: [revoked.publicKey],
46
+ __testKeys: { revoked, current },
47
+ };
48
+ });
35
49
  const file_types_1 = require("../data/files/file.types");
36
50
  const keys_1 = require("../keys");
51
+ const systemIds = require("../system-ids");
37
52
  const package_author_signing_1 = require("./package-author-signing");
38
53
  const package_remote_checker_1 = require("./package-remote-checker");
39
54
  const package_tarball_1 = require("./package-tarball");
55
+ // PKG_ID is peers-core's id so the trust-anchor re-pin path is exercised.
40
56
  const PKG_ID = "00mh0wlipjixk2gqmurbwee0o";
41
57
  const PV_ID = "00mpdg7xstwt3oe51x9brk15i";
58
+ const { revoked: revokedCoreKeys, current: currentCoreKeys } = systemIds.__testKeys;
42
59
  function makeContext(dataContextId) {
43
60
  return { dataContextId };
44
61
  }
@@ -234,6 +251,54 @@ describe("package-remote-checker", () => {
234
251
  expect(http.fetchJson).toHaveBeenCalledTimes(1);
235
252
  expect(mockSaveBundleFile).toHaveBeenCalled();
236
253
  });
254
+ it("re-pins peers-core from a revoked key and installs a release signed by the current key", async () => {
255
+ expect(PKG_ID).toBe(systemIds.peersCorePackageId);
256
+ const ctx = makeContext("user-ctx");
257
+ const { pointer, tarball } = await makeSignedRemoteResult(currentCoreKeys.secretKey, currentCoreKeys.publicKey);
258
+ let pkg = {
259
+ packageId: PKG_ID,
260
+ name: "peers-core",
261
+ publishPublicKey: revokedCoreKeys.publicKey,
262
+ updateUrl: "https://example.com/peers-core",
263
+ };
264
+ const http = {
265
+ fetchJson: jest.fn().mockResolvedValue(pointer),
266
+ fetchBinary: jest.fn().mockResolvedValue(tarball),
267
+ };
268
+ mockPackageVersionsGet.mockResolvedValue(undefined);
269
+ mockPackagesGet.mockImplementation(async () => pkg);
270
+ mockPackagesSignAndSave.mockImplementation(async (_ctx, saved) => {
271
+ pkg = saved;
272
+ });
273
+ const warn = jest.spyOn(console, "warn").mockImplementation(() => { });
274
+ const installed = await (0, package_remote_checker_1.checkAndInstallPackageRemoteVersion)(ctx, pkg, "stable", http);
275
+ expect(installed?.packageVersion.packageVersionId).toBe(PV_ID);
276
+ expect(mockPackagesSignAndSave).toHaveBeenCalledWith(expect.anything(), expect.objectContaining({ packageId: PKG_ID, publishPublicKey: currentCoreKeys.publicKey }));
277
+ expect(pkg.publishPublicKey).toBe(currentCoreKeys.publicKey);
278
+ warn.mockRestore();
279
+ });
280
+ it("rejects a remote release signed by a revoked key", async () => {
281
+ const ctx = makeContext("user-ctx");
282
+ const { pointer, tarball } = await makeSignedRemoteResult(revokedCoreKeys.secretKey, revokedCoreKeys.publicKey);
283
+ const pkg = {
284
+ packageId: PKG_ID,
285
+ name: "peers-core",
286
+ publishPublicKey: currentCoreKeys.publicKey,
287
+ updateUrl: "https://example.com/peers-core-old",
288
+ };
289
+ const http = {
290
+ fetchJson: jest.fn().mockResolvedValue(pointer),
291
+ fetchBinary: jest.fn().mockResolvedValue(tarball),
292
+ };
293
+ mockPackageVersionsGet.mockResolvedValue(undefined);
294
+ mockPackagesGet.mockResolvedValue(pkg);
295
+ const error = jest.spyOn(console, "error").mockImplementation(() => { });
296
+ const installed = await (0, package_remote_checker_1.checkAndInstallPackageRemoteVersion)(ctx, pkg, "stable", http);
297
+ expect(installed).toBeNull();
298
+ expect(mockSaveBundleFile).not.toHaveBeenCalled();
299
+ expect(mockPackageVersionsSignAndSave).not.toHaveBeenCalled();
300
+ error.mockRestore();
301
+ });
237
302
  });
238
303
  describe("installRemotePackageVersion", () => {
239
304
  it("rejects publisher key mismatch before saving files", async () => {
@@ -43,6 +43,10 @@ export interface IEnsurePeersCoreOpts {
43
43
  * Ensure peers-core is installed (with a usable, loaded version) in
44
44
  * `dataContext`, applying the layered strategy described in the module docs.
45
45
  *
46
+ * Before anything else, an already-installed peers-core whose pinned publisher key has been
47
+ * revoked is re-pinned to the host's current key (see `peers-core-trust-anchor.ts`), so the
48
+ * later remote check can accept releases signed by the replacement key.
49
+ *
46
50
  * @returns true if an install occurred, false if it was already present or no
47
51
  * strategy succeeded.
48
52
  */
@@ -23,16 +23,22 @@ const packages_utils_1 = require("../data/packages.utils");
23
23
  const system_ids_1 = require("../system-ids");
24
24
  const package_remote_checker_1 = require("./package-remote-checker");
25
25
  const package_seed_installer_1 = require("./package-seed-installer");
26
+ const peers_core_trust_anchor_1 = require("./peers-core-trust-anchor");
26
27
  const PEERS_CORE_NAME = "peers-core";
27
28
  const PEERS_CORE_DESCRIPTION = "Core system package for Peers platform";
28
29
  /**
29
30
  * Ensure peers-core is installed (with a usable, loaded version) in
30
31
  * `dataContext`, applying the layered strategy described in the module docs.
31
32
  *
33
+ * Before anything else, an already-installed peers-core whose pinned publisher key has been
34
+ * revoked is re-pinned to the host's current key (see `peers-core-trust-anchor.ts`), so the
35
+ * later remote check can accept releases signed by the replacement key.
36
+ *
32
37
  * @returns true if an install occurred, false if it was already present or no
33
38
  * strategy succeeded.
34
39
  */
35
40
  async function ensurePeersCore(userContext, dataContext, opts) {
41
+ await (0, peers_core_trust_anchor_1.repinInstalledPeersCoreIfRevoked)(dataContext);
36
42
  const existingPvs = await (0, package_versions_1.PackageVersions)(dataContext).list({ packageId: system_ids_1.peersCorePackageId });
37
43
  if (existingPvs.length > 0)
38
44
  return false;
@@ -0,0 +1,49 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const mockPvList = jest.fn();
4
+ jest.mock("../data/package-versions", () => ({
5
+ PackageVersions: (ctx) => ({
6
+ list: (...args) => mockPvList(ctx, ...args),
7
+ }),
8
+ }));
9
+ const mockRepinInstalled = jest.fn();
10
+ jest.mock("./peers-core-trust-anchor", () => ({
11
+ repinInstalledPeersCoreIfRevoked: (...args) => mockRepinInstalled(...args),
12
+ }));
13
+ jest.mock("../data/packages", () => ({ Packages: () => ({}) }));
14
+ jest.mock("../data/packages.utils", () => ({ copyPackageToAnotherDataContext: jest.fn() }));
15
+ jest.mock("../data/package-version-resolver", () => ({
16
+ resolveDevicePackageVersion: jest.fn(),
17
+ }));
18
+ jest.mock("./package-remote-checker", () => ({
19
+ checkAndInstallPackageRemoteVersion: jest.fn(),
20
+ }));
21
+ jest.mock("./package-seed-installer", () => ({
22
+ ensurePackageRecord: jest.fn(),
23
+ seedPackageInContext: jest.fn(),
24
+ }));
25
+ const system_ids_1 = require("../system-ids");
26
+ const peers_core_installer_1 = require("./peers-core-installer");
27
+ describe("ensurePeersCore", () => {
28
+ beforeEach(() => {
29
+ jest.clearAllMocks();
30
+ mockRepinInstalled.mockResolvedValue(null);
31
+ });
32
+ it("re-pins a revoked peers-core key before checking for existing versions", async () => {
33
+ const dataContext = { dataContextId: "user-ctx" };
34
+ const order = [];
35
+ mockRepinInstalled.mockImplementation(async () => {
36
+ order.push("repin");
37
+ return null;
38
+ });
39
+ mockPvList.mockImplementation(async () => {
40
+ order.push("list");
41
+ return [{ packageVersionId: "existing" }];
42
+ });
43
+ const installed = await (0, peers_core_installer_1.ensurePeersCore)({}, dataContext, {});
44
+ expect(installed).toBe(false);
45
+ expect(mockRepinInstalled).toHaveBeenCalledWith(dataContext);
46
+ expect(mockPvList).toHaveBeenCalledWith(dataContext, { packageId: system_ids_1.peersCorePackageId });
47
+ expect(order).toEqual(["repin", "list"]);
48
+ });
49
+ });
@@ -0,0 +1,47 @@
1
+ import type { DataContext } from "../context/data-context";
2
+ import { type IPackage } from "../data/packages";
3
+ /**
4
+ * Migration of the peers-core trust anchor after a publisher-key rotation.
5
+ *
6
+ * `Packages.publishPublicKey` is pinned on first install (TOFU) and otherwise never changes, so
7
+ * a client that installed peers-core under a key that was later revoked would keep that key
8
+ * forever and reject every version signed by its replacement. This module replaces a pinned
9
+ * key that appears in {@link peersCoreRevokedPublishPublicKeys} with the host's current
10
+ * {@link peersCorePublishPublicKey}. Only peers-core is handled here: it is the one package
11
+ * whose trust anchor ships inside the host.
12
+ */
13
+ /** Options for {@link repinPeersCoreIfRevoked}, mainly for tests. */
14
+ export interface IRepinOptions {
15
+ /** Current trusted key. Defaults to {@link peersCorePublishPublicKey}. */
16
+ currentPublicKey?: string;
17
+ /** Keys that must be replaced. Defaults to {@link peersCoreRevokedPublishPublicKeys}. */
18
+ revokedPublicKeys?: readonly string[];
19
+ }
20
+ /**
21
+ * Whether `pkg` is the peers-core package pinned to a revoked publisher key.
22
+ *
23
+ * @param pkg - package row to inspect
24
+ * @param opts - alternative key lists (tests)
25
+ */
26
+ export declare function needsPeersCoreRepin(pkg: Pick<IPackage, "packageId" | "publishPublicKey">, opts?: IRepinOptions): boolean;
27
+ /**
28
+ * If `pkg` is peers-core pinned to a revoked key, re-pin it to the current key in
29
+ * `dataContext` and return the updated row; otherwise return `pkg` unchanged.
30
+ *
31
+ * Safe to call on every remote check and propagation: it is a no-op unless the pin is revoked.
32
+ * In a group where the caller may not write package rows, `signAndSave` throws; the error is
33
+ * logged and the original row returned, so verification keeps failing closed there until an
34
+ * admin's device performs the re-pin and the row syncs.
35
+ *
36
+ * @param dataContext - context holding the package row
37
+ * @param pkg - the package row as currently loaded
38
+ * @param opts - alternative key lists (tests)
39
+ * @returns the (possibly re-pinned) package row
40
+ */
41
+ export declare function repinPeersCoreIfRevoked(dataContext: DataContext, pkg: IPackage, opts?: IRepinOptions): Promise<IPackage>;
42
+ /**
43
+ * Look up peers-core in `dataContext` and re-pin it if its key is revoked.
44
+ *
45
+ * @returns the package row when peers-core is installed in the context, else null
46
+ */
47
+ export declare function repinInstalledPeersCoreIfRevoked(dataContext: DataContext, opts?: IRepinOptions): Promise<IPackage | null>;
@@ -0,0 +1,62 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.needsPeersCoreRepin = needsPeersCoreRepin;
4
+ exports.repinPeersCoreIfRevoked = repinPeersCoreIfRevoked;
5
+ exports.repinInstalledPeersCoreIfRevoked = repinInstalledPeersCoreIfRevoked;
6
+ const packages_1 = require("../data/packages");
7
+ const system_ids_1 = require("../system-ids");
8
+ const package_author_signing_1 = require("./package-author-signing");
9
+ /**
10
+ * Whether `pkg` is the peers-core package pinned to a revoked publisher key.
11
+ *
12
+ * @param pkg - package row to inspect
13
+ * @param opts - alternative key lists (tests)
14
+ */
15
+ function needsPeersCoreRepin(pkg, opts = {}) {
16
+ const current = opts.currentPublicKey ?? system_ids_1.peersCorePublishPublicKey;
17
+ const revoked = opts.revokedPublicKeys ?? system_ids_1.peersCoreRevokedPublishPublicKeys;
18
+ return (pkg.packageId === system_ids_1.peersCorePackageId &&
19
+ !!current &&
20
+ pkg.publishPublicKey !== current &&
21
+ (0, package_author_signing_1.isRevokedPublisherKey)(pkg.publishPublicKey, revoked));
22
+ }
23
+ /**
24
+ * If `pkg` is peers-core pinned to a revoked key, re-pin it to the current key in
25
+ * `dataContext` and return the updated row; otherwise return `pkg` unchanged.
26
+ *
27
+ * Safe to call on every remote check and propagation: it is a no-op unless the pin is revoked.
28
+ * In a group where the caller may not write package rows, `signAndSave` throws; the error is
29
+ * logged and the original row returned, so verification keeps failing closed there until an
30
+ * admin's device performs the re-pin and the row syncs.
31
+ *
32
+ * @param dataContext - context holding the package row
33
+ * @param pkg - the package row as currently loaded
34
+ * @param opts - alternative key lists (tests)
35
+ * @returns the (possibly re-pinned) package row
36
+ */
37
+ async function repinPeersCoreIfRevoked(dataContext, pkg, opts = {}) {
38
+ if (!needsPeersCoreRepin(pkg, opts))
39
+ return pkg;
40
+ const current = opts.currentPublicKey ?? system_ids_1.peersCorePublishPublicKey;
41
+ try {
42
+ const updated = { ...pkg, publishPublicKey: current };
43
+ await (0, packages_1.Packages)(dataContext).signAndSave(updated);
44
+ console.warn(`[peers-core-trust-anchor] Replaced revoked peers-core publisher key in ${dataContext.dataContextId}`);
45
+ return updated;
46
+ }
47
+ catch (err) {
48
+ console.error(`[peers-core-trust-anchor] Could not re-pin peers-core in ${dataContext.dataContextId}:`, err);
49
+ return pkg;
50
+ }
51
+ }
52
+ /**
53
+ * Look up peers-core in `dataContext` and re-pin it if its key is revoked.
54
+ *
55
+ * @returns the package row when peers-core is installed in the context, else null
56
+ */
57
+ async function repinInstalledPeersCoreIfRevoked(dataContext, opts = {}) {
58
+ const pkg = await (0, packages_1.Packages)(dataContext).get(system_ids_1.peersCorePackageId);
59
+ if (!pkg)
60
+ return null;
61
+ return repinPeersCoreIfRevoked(dataContext, pkg, opts);
62
+ }
@@ -0,0 +1,155 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const keys_1 = require("../keys");
4
+ const systemIds = require("../system-ids");
5
+ const utils_1 = require("../utils");
6
+ const package_author_signing_1 = require("./package-author-signing");
7
+ const peers_core_trust_anchor_1 = require("./peers-core-trust-anchor");
8
+ // Pin the host to a fresh "current" key and revoke a fresh "old" key so the tests do not
9
+ // depend on the real constants. Keys are created inside the factory because jest hoists
10
+ // `jest.mock` above module-level declarations.
11
+ jest.mock("../system-ids", () => {
12
+ const actual = jest.requireActual("../system-ids");
13
+ const { newKeys: makeKeys } = jest.requireActual("../keys");
14
+ const revoked = makeKeys();
15
+ const current = makeKeys();
16
+ return {
17
+ ...actual,
18
+ peersCorePublishPublicKey: current.publicKey,
19
+ peersCoreRevokedPublishPublicKeys: [revoked.publicKey],
20
+ __testKeys: { revoked, current },
21
+ };
22
+ });
23
+ const { revoked: revokedKeys, current: currentKeys } = systemIds.__testKeys;
24
+ const mockPackagesGet = jest.fn();
25
+ const mockPackagesSignAndSave = jest.fn();
26
+ jest.mock("../data/packages", () => ({
27
+ Packages: () => ({
28
+ get: (...args) => mockPackagesGet(...args),
29
+ signAndSave: (...args) => mockPackagesSignAndSave(...args),
30
+ }),
31
+ }));
32
+ function makeCtx(id = "ctx") {
33
+ return { dataContextId: id };
34
+ }
35
+ function makePkg(overrides) {
36
+ return {
37
+ packageId: systemIds.peersCorePackageId,
38
+ name: "peers-core",
39
+ publishPublicKey: revokedKeys.publicKey,
40
+ ...overrides,
41
+ };
42
+ }
43
+ function makePV() {
44
+ return {
45
+ packageId: systemIds.peersCorePackageId,
46
+ packageVersionId: (0, utils_1.newid)(),
47
+ version: "1.0.0",
48
+ versionTag: "stable",
49
+ packageBundleFileHash: "hash",
50
+ };
51
+ }
52
+ beforeEach(() => {
53
+ jest.clearAllMocks();
54
+ jest.spyOn(console, "warn").mockImplementation(() => { });
55
+ jest.spyOn(console, "error").mockImplementation(() => { });
56
+ });
57
+ afterEach(() => {
58
+ jest.restoreAllMocks();
59
+ });
60
+ describe("isRevokedPublisherKey", () => {
61
+ it("uses the host revocation list by default", () => {
62
+ expect((0, package_author_signing_1.isRevokedPublisherKey)(revokedKeys.publicKey)).toBe(true);
63
+ expect((0, package_author_signing_1.isRevokedPublisherKey)(currentKeys.publicKey)).toBe(false);
64
+ });
65
+ it("accepts an explicit list", () => {
66
+ expect((0, package_author_signing_1.isRevokedPublisherKey)("abc", ["abc"])).toBe(true);
67
+ expect((0, package_author_signing_1.isRevokedPublisherKey)(revokedKeys.publicKey, [])).toBe(false);
68
+ });
69
+ it("treats empty or missing keys as not revoked", () => {
70
+ expect((0, package_author_signing_1.isRevokedPublisherKey)("", ["", "x"])).toBe(false);
71
+ expect((0, package_author_signing_1.isRevokedPublisherKey)(undefined)).toBe(false);
72
+ });
73
+ });
74
+ describe("verifyPackageAuthorSignature with a revoked key", () => {
75
+ it("rejects a mathematically valid signature from a revoked key", () => {
76
+ const pv = { ...makePV(), packageAuthorSignature: "" };
77
+ pv.packageAuthorSignature = (0, package_author_signing_1.signPackageAuthor)(pv, revokedKeys.secretKey);
78
+ const result = (0, package_author_signing_1.verifyPackageAuthorSignature)(pv, revokedKeys.publicKey);
79
+ expect(result).toEqual({ valid: false, publicKey: revokedKeys.publicKey });
80
+ });
81
+ it("still accepts the current key", () => {
82
+ const pv = { ...makePV(), packageAuthorSignature: "" };
83
+ pv.packageAuthorSignature = (0, package_author_signing_1.signPackageAuthor)(pv, currentKeys.secretKey);
84
+ expect((0, package_author_signing_1.verifyPackageAuthorSignature)(pv, currentKeys.publicKey).valid).toBe(true);
85
+ });
86
+ });
87
+ describe("needsPeersCoreRepin", () => {
88
+ it("is true only for peers-core pinned to a revoked key", () => {
89
+ expect((0, peers_core_trust_anchor_1.needsPeersCoreRepin)(makePkg())).toBe(true);
90
+ expect((0, peers_core_trust_anchor_1.needsPeersCoreRepin)(makePkg({ publishPublicKey: currentKeys.publicKey }))).toBe(false);
91
+ expect((0, peers_core_trust_anchor_1.needsPeersCoreRepin)(makePkg({ publishPublicKey: "" }))).toBe(false);
92
+ expect((0, peers_core_trust_anchor_1.needsPeersCoreRepin)(makePkg({ packageId: (0, utils_1.newid)() }))).toBe(false);
93
+ });
94
+ it("honours explicit key lists", () => {
95
+ const other = (0, keys_1.newKeys)();
96
+ const pkg = makePkg({ publishPublicKey: other.publicKey });
97
+ expect((0, peers_core_trust_anchor_1.needsPeersCoreRepin)(pkg)).toBe(false);
98
+ expect((0, peers_core_trust_anchor_1.needsPeersCoreRepin)(pkg, {
99
+ currentPublicKey: currentKeys.publicKey,
100
+ revokedPublicKeys: [other.publicKey],
101
+ })).toBe(true);
102
+ });
103
+ it("is false when the host has no current key to pin", () => {
104
+ expect((0, peers_core_trust_anchor_1.needsPeersCoreRepin)(makePkg(), { currentPublicKey: "" })).toBe(false);
105
+ });
106
+ });
107
+ describe("repinPeersCoreIfRevoked", () => {
108
+ it("re-pins and saves when the key is revoked", async () => {
109
+ mockPackagesSignAndSave.mockResolvedValue(undefined);
110
+ const pkg = makePkg();
111
+ const result = await (0, peers_core_trust_anchor_1.repinPeersCoreIfRevoked)(makeCtx(), pkg);
112
+ expect(result).not.toBe(pkg);
113
+ expect(result.publishPublicKey).toBe(currentKeys.publicKey);
114
+ expect(mockPackagesSignAndSave).toHaveBeenCalledWith(expect.objectContaining({
115
+ packageId: systemIds.peersCorePackageId,
116
+ publishPublicKey: currentKeys.publicKey,
117
+ }));
118
+ expect(console.warn).toHaveBeenCalled();
119
+ });
120
+ it("returns the same row and does not save when nothing is revoked", async () => {
121
+ const pkg = makePkg({ publishPublicKey: currentKeys.publicKey });
122
+ const result = await (0, peers_core_trust_anchor_1.repinPeersCoreIfRevoked)(makeCtx(), pkg);
123
+ expect(result).toBe(pkg);
124
+ expect(mockPackagesSignAndSave).not.toHaveBeenCalled();
125
+ });
126
+ it("does not touch other packages even when their key is revoked", async () => {
127
+ const pkg = makePkg({ packageId: (0, utils_1.newid)() });
128
+ const result = await (0, peers_core_trust_anchor_1.repinPeersCoreIfRevoked)(makeCtx(), pkg);
129
+ expect(result).toBe(pkg);
130
+ expect(mockPackagesSignAndSave).not.toHaveBeenCalled();
131
+ });
132
+ it("returns the original row when the save is not permitted", async () => {
133
+ mockPackagesSignAndSave.mockRejectedValue(new Error("not a writer"));
134
+ const pkg = makePkg();
135
+ const result = await (0, peers_core_trust_anchor_1.repinPeersCoreIfRevoked)(makeCtx("group-ro"), pkg);
136
+ expect(result).toBe(pkg);
137
+ expect(result.publishPublicKey).toBe(revokedKeys.publicKey);
138
+ expect(console.error).toHaveBeenCalled();
139
+ });
140
+ });
141
+ describe("repinInstalledPeersCoreIfRevoked", () => {
142
+ it("returns null when peers-core is not installed", async () => {
143
+ mockPackagesGet.mockResolvedValue(undefined);
144
+ expect(await (0, peers_core_trust_anchor_1.repinInstalledPeersCoreIfRevoked)(makeCtx())).toBeNull();
145
+ expect(mockPackagesSignAndSave).not.toHaveBeenCalled();
146
+ });
147
+ it("loads peers-core and re-pins it", async () => {
148
+ mockPackagesGet.mockResolvedValue(makePkg());
149
+ mockPackagesSignAndSave.mockResolvedValue(undefined);
150
+ const result = await (0, peers_core_trust_anchor_1.repinInstalledPeersCoreIfRevoked)(makeCtx());
151
+ expect(mockPackagesGet).toHaveBeenCalledWith(systemIds.peersCorePackageId);
152
+ expect(result?.publishPublicKey).toBe(currentKeys.publicKey);
153
+ expect(mockPackagesSignAndSave).toHaveBeenCalledTimes(1);
154
+ });
155
+ });
@@ -23,8 +23,28 @@ export declare const createGroupToolId = "000peers0tool0creategroup";
23
23
  export declare const peersCorePackageId = "00mh0wlipjixk2gqmurbwee0o";
24
24
  /** Canonical source repository for peers-core. */
25
25
  export declare const peersCorePackageRepoUrl = "https://github.com/peers-app/peers-core";
26
- /** Trusted publishing key for official peers-core releases. */
27
- export declare const peersCorePublishPublicKey = "S7cQqnvVz-FVDO6Lk5zC7epSvcP_-NyBVZz9gdcj73c";
26
+ /**
27
+ * Trusted publishing key for official peers-core releases (Ed25519, base64url).
28
+ *
29
+ * This is the bootstrap trust anchor shipped with every host. It seeds `Packages.publishPublicKey`
30
+ * for peers-core and is what `verifyPackageAuthorSignature` checks remote and propagated
31
+ * peers-core versions against. The same key is registered as the active key of subject
32
+ * {@link peersCorePackageId} in the peers-services key registry (`GET /api/v1/keys/:subjectId`).
33
+ *
34
+ * Rotation: generate a new pair with `scripts/generate-publisher-key.mjs`, move the previous
35
+ * value into {@link peersCoreRevokedPublishPublicKeys}, seed the registry, and ship a host release.
36
+ */
37
+ export declare const peersCorePublishPublicKey = "8S1pAWvka0RZauJnwM13n7qO01NtdGV7FSU_aSJw0dc";
38
+ /**
39
+ * peers-core publishing keys that must never be trusted again, whatever a local `Packages` row
40
+ * or a propagating peer says. A version signed by one of these fails author verification, a
41
+ * peer offering one as a TOFU anchor is refused, and `ensurePeersCore` replaces a pinned
42
+ * revoked key with {@link peersCorePublishPublicKey}.
43
+ *
44
+ * Append-only. Entries are retained forever so a client coming back after a long time still
45
+ * rejects them.
46
+ */
47
+ export declare const peersCoreRevokedPublishPublicKeys: readonly string[];
28
48
  /**
29
49
  * Stable id of the peers **system** itself, used as the `providerPackageId` for
30
50
  * built-in system contracts (contracts the platform provides rather than a package).
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.localFoldersToolIds = exports.localFoldersContractId = exports.identityToolIds = exports.identityContractId = exports.invitesToolIds = exports.invitesContractId = exports.toolAccessControlToolIds = exports.toolAccessControlContractId = exports.networkControlContractId = exports.networkDiagnosticsContractId = exports.deviceOperationsContractId = exports.logsContractId = exports.peersSystemPackageId = exports.peersCorePublishPublicKey = exports.peersCorePackageRepoUrl = exports.peersCorePackageId = exports.createGroupToolId = exports.queryDatabaseToolId = exports.runWorkflowToolId = exports.defaultSendMessageToolId = exports.defaultAssistantRunnerToolId = exports.adminAssistantId = exports.openCodeAssistantId = exports.defaultAssistantId = exports.peersRootUserId = void 0;
3
+ exports.localFoldersToolIds = exports.localFoldersContractId = exports.identityToolIds = exports.identityContractId = exports.invitesToolIds = exports.invitesContractId = exports.toolAccessControlToolIds = exports.toolAccessControlContractId = exports.networkControlContractId = exports.networkDiagnosticsContractId = exports.deviceOperationsContractId = exports.logsContractId = exports.peersSystemPackageId = exports.peersCoreRevokedPublishPublicKeys = exports.peersCorePublishPublicKey = exports.peersCorePackageRepoUrl = exports.peersCorePackageId = exports.createGroupToolId = exports.queryDatabaseToolId = exports.runWorkflowToolId = exports.defaultSendMessageToolId = exports.defaultAssistantRunnerToolId = exports.adminAssistantId = exports.openCodeAssistantId = exports.defaultAssistantId = exports.peersRootUserId = void 0;
4
4
  /** Stable user ID that owns built-in Peers system records. */
5
5
  exports.peersRootUserId = "000peers0user000000000001";
6
6
  /** Default assistant used when no user-selected assistant is available. */
@@ -27,8 +27,31 @@ exports.createGroupToolId = "000peers0tool0creategroup";
27
27
  exports.peersCorePackageId = "00mh0wlipjixk2gqmurbwee0o";
28
28
  /** Canonical source repository for peers-core. */
29
29
  exports.peersCorePackageRepoUrl = "https://github.com/peers-app/peers-core";
30
- /** Trusted publishing key for official peers-core releases. */
31
- exports.peersCorePublishPublicKey = "S7cQqnvVz-FVDO6Lk5zC7epSvcP_-NyBVZz9gdcj73c";
30
+ /**
31
+ * Trusted publishing key for official peers-core releases (Ed25519, base64url).
32
+ *
33
+ * This is the bootstrap trust anchor shipped with every host. It seeds `Packages.publishPublicKey`
34
+ * for peers-core and is what `verifyPackageAuthorSignature` checks remote and propagated
35
+ * peers-core versions against. The same key is registered as the active key of subject
36
+ * {@link peersCorePackageId} in the peers-services key registry (`GET /api/v1/keys/:subjectId`).
37
+ *
38
+ * Rotation: generate a new pair with `scripts/generate-publisher-key.mjs`, move the previous
39
+ * value into {@link peersCoreRevokedPublishPublicKeys}, seed the registry, and ship a host release.
40
+ */
41
+ exports.peersCorePublishPublicKey = "8S1pAWvka0RZauJnwM13n7qO01NtdGV7FSU_aSJw0dc";
42
+ /**
43
+ * peers-core publishing keys that must never be trusted again, whatever a local `Packages` row
44
+ * or a propagating peer says. A version signed by one of these fails author verification, a
45
+ * peer offering one as a TOFU anchor is refused, and `ensurePeersCore` replaces a pinned
46
+ * revoked key with {@link peersCorePublishPublicKey}.
47
+ *
48
+ * Append-only. Entries are retained forever so a client coming back after a long time still
49
+ * rejects them.
50
+ */
51
+ exports.peersCoreRevokedPublishPublicKeys = [
52
+ // Exposed in packaged macOS builds v0.19.9 through v0.25.11 (2026-10-02 incident).
53
+ "S7cQqnvVz-FVDO6Lk5zC7epSvcP_-NyBVZz9gdcj73c",
54
+ ];
32
55
  /**
33
56
  * Stable id of the peers **system** itself, used as the `providerPackageId` for
34
57
  * built-in system contracts (contracts the platform provides rather than a package).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@peers-app/peers-sdk",
3
- "version": "0.25.11",
3
+ "version": "0.25.12",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/peers-app/peers-sdk.git"
@@ -28,9 +28,11 @@
28
28
  "build": "npm run clean && tsc",
29
29
  "dev": "tsc -w",
30
30
  "test": "jest",
31
- "release:patch": "npm run build && npm version patch && git push && git push --tags",
32
- "release:minor": "npm run build && npm version minor && git push && git push --tags",
33
- "release:major": "npm run build && npm version major && git push && git push --tags",
31
+ "release:guard": "node release-guard.js",
32
+ "prepublishOnly": "npm run release:guard",
33
+ "release:patch": "npm run release:guard && npm run build && npm version patch && git push && git push --tags",
34
+ "release:minor": "npm run release:guard && npm run build && npm version minor && git push && git push --tags",
35
+ "release:major": "npm run release:guard && npm run build && npm version major && git push && git push --tags",
34
36
  "release": "npm run release:patch"
35
37
  },
36
38
  "dependencies": {