@peers-app/peers-sdk 0.18.6 → 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/README.md +74 -1
  2. package/dist/data/files/file-read-stream.js +7 -0
  3. package/dist/data/files/file.types.d.ts +6 -0
  4. package/dist/data/files/file.types.js +18 -0
  5. package/dist/data/files/files.test.js +50 -7
  6. package/dist/data/package-version-resolver.test.js +1 -0
  7. package/dist/data/package-versions.d.ts +3 -0
  8. package/dist/data/package-versions.js +5 -0
  9. package/dist/data/packages.d.ts +6 -0
  10. package/dist/data/packages.js +8 -0
  11. package/dist/index.d.ts +1 -0
  12. package/dist/index.js +1 -0
  13. package/dist/package-installer/index.d.ts +10 -0
  14. package/dist/package-installer/index.js +26 -0
  15. package/dist/package-installer/package-author-signing.d.ts +48 -0
  16. package/dist/package-installer/package-author-signing.js +73 -0
  17. package/dist/package-installer/package-author-signing.test.d.ts +1 -0
  18. package/dist/package-installer/package-author-signing.test.js +189 -0
  19. package/dist/package-installer/package-cloner.d.ts +16 -0
  20. package/dist/package-installer/package-cloner.js +115 -0
  21. package/dist/package-installer/package-cloner.test.d.ts +1 -0
  22. package/dist/package-installer/package-cloner.test.js +276 -0
  23. package/dist/package-installer/package-creator.d.ts +22 -0
  24. package/dist/package-installer/package-creator.js +154 -0
  25. package/dist/package-installer/package-creator.test.d.ts +1 -0
  26. package/dist/package-installer/package-creator.test.js +354 -0
  27. package/dist/package-installer/package-installer.d.ts +32 -0
  28. package/dist/package-installer/package-installer.js +247 -0
  29. package/dist/package-installer/package-installer.test.d.ts +1 -0
  30. package/dist/package-installer/package-installer.test.js +666 -0
  31. package/dist/package-installer/package-propagation.d.ts +29 -0
  32. package/dist/package-installer/package-propagation.js +363 -0
  33. package/dist/package-installer/package-propagation.test.d.ts +1 -0
  34. package/dist/package-installer/package-propagation.test.js +1145 -0
  35. package/dist/package-installer/package-publisher.d.ts +50 -0
  36. package/dist/package-installer/package-publisher.js +67 -0
  37. package/dist/package-installer/package-publisher.test.d.ts +1 -0
  38. package/dist/package-installer/package-publisher.test.js +142 -0
  39. package/dist/package-installer/package-remote-checker.d.ts +54 -0
  40. package/dist/package-installer/package-remote-checker.js +186 -0
  41. package/dist/package-installer/package-remote-checker.test.d.ts +1 -0
  42. package/dist/package-installer/package-remote-checker.test.js +263 -0
  43. package/dist/package-installer/package-seed-installer.d.ts +45 -0
  44. package/dist/package-installer/package-seed-installer.js +108 -0
  45. package/dist/package-installer/package-seed-installer.test.d.ts +1 -0
  46. package/dist/package-installer/package-seed-installer.test.js +123 -0
  47. package/dist/package-installer/package-tarball.d.ts +35 -0
  48. package/dist/package-installer/package-tarball.js +57 -0
  49. package/dist/package-installer/package-tarball.test.d.ts +1 -0
  50. package/dist/package-installer/package-tarball.test.js +75 -0
  51. package/dist/package-installer/types.d.ts +110 -0
  52. package/dist/package-installer/types.js +2 -0
  53. package/dist/rpc-types.d.ts +14 -0
  54. package/dist/rpc-types.js +6 -0
  55. package/dist/system-ids.d.ts +1 -0
  56. package/dist/system-ids.js +2 -1
  57. package/package.json +3 -2
package/README.md CHANGED
@@ -1 +1,74 @@
1
- Common types, zod schemas, functions, and classes that are used internally in Peers and for building packages.
1
+ # @peers-app/peers-sdk
2
+
3
+ The core SDK for building [Peers](https://peers.app) packages.
4
+
5
+ ## What is Peers?
6
+
7
+ Peers is a local-first personal computing platform. Your data lives on your devices, syncs peer-to-peer, and is end-to-end encrypted — no servers in the middle. You own your data and your identity. Build apps with the SDK or use AI coding tools to create them; either way, the platform handles sync, encryption, and persistence automatically.
8
+
9
+ ## What this package provides
10
+
11
+ ### Package System
12
+
13
+ `definePackage()` and the contract builder API let you declare tables, tools, assistants, screens, and navigation entries. The runtime installs, loads, and hot-reloads your package across all devices.
14
+
15
+ ### ORM
16
+
17
+ Table definitions with Zod schemas, data queries with a Mongo-style filter syntax, and async cursors. Tables sync across devices and encrypt automatically — you just define the schema.
18
+
19
+ ### Observables and Persistent Variables
20
+
21
+ Lightweight reactive primitives (`observable()`, `computed()`) with `.subscribe()`. Persistent variables (`deviceVar`, `userVar`, `groupVar`) are observables backed by the database — write once, subscribe everywhere, synced across devices.
22
+
23
+ ### Types and Schemas
24
+
25
+ Zod schemas and TypeScript types for the entire Peers data model: users, groups, devices, packages, tools, assistants, workflows, messages, channels, and more.
26
+
27
+ ### Encryption
28
+
29
+ NaCl-based key management, message signing and verification, box encryption, and deterministic hashing. Identity is a cryptographic key pair — no passwords, no email.
30
+
31
+ ### Tools Framework
32
+
33
+ Define AI-callable tools with `ITool` and `IToolInstance`. Tools are registered at runtime and available to built-in and user-defined AI assistants and workflows.
34
+
35
+ ### Identity and Groups
36
+
37
+ User, group, and permission management types. Group-scoped data contexts for multi-tenant data isolation. Invite flows and trust levels.
38
+
39
+ ## Quick Start
40
+
41
+ ```bash
42
+ npm install @peers-app/peers-sdk
43
+ ```
44
+
45
+ The fastest way to build a Peers package is to start from the template:
46
+
47
+ **[peers-package-template](https://github.com/peers-app/peers-package-template)** — scaffold, build, and install a custom package into the Peers runtime. This will be setup automatically when you create a new package in the desktop app.
48
+
49
+ ## Key Interfaces
50
+
51
+
52
+ | Export | Purpose |
53
+ | ------------------------------------ | ----------------------------------------------------------------------- |
54
+ | `definePackage()` | Entry point for declaring a package's contracts, tools, tables, and nav |
55
+ | `Table` | ORM table with CRUD, Zod validation, and `dataChanged` events |
56
+ | `DataQuery` / `DataFilter` | Mongo-style query filters translated to SQL |
57
+ | `observable()` / `computed()` | Reactive state primitives |
58
+ | `deviceVar` / `userVar` / `groupVar` | Persistent variables — observables backed by the database |
59
+ | `ITool` / `IToolInstance` | AI-callable tool definitions |
60
+ | `IPeersUI` / `IPeersUIRoute` | Screen and route declarations for package UIs |
61
+ | `newid()` | Generate 25-character time-sortable peer IDs |
62
+ | `newKeys()` / `hydrateKeys()` | Cryptographic key pair generation and hydration |
63
+ | `UserContext` / `DataContext` | Multi-group data scoping and package loading |
64
+
65
+
66
+ ## Links
67
+
68
+ - [peers.app](https://peers.app) — try Peers in your browser or download the desktop app
69
+ - [Documentation](https://peers-app.github.io) — architecture, package development, and API reference
70
+ - [GitHub](https://github.com/peers-app) — source repositories and package template
71
+
72
+ ## License
73
+
74
+ MIT
@@ -36,6 +36,13 @@ class FileReadStream {
36
36
  else {
37
37
  throw new Error(`File ${this.fileRecord.fileId} has neither chunkHashes nor indexFileId`);
38
38
  }
39
+ if (!this.skipVerification) {
40
+ const computedFileHash = (0, keys_1.hashBytes)(new TextEncoder().encode(JSON.stringify(this.chunkHashes)));
41
+ if (computedFileHash !== this.fileRecord.fileHash) {
42
+ throw new Error(`File integrity check failed for ${this.fileRecord.fileId}: ` +
43
+ `chunk hashes produce ${computedFileHash}, expected ${this.fileRecord.fileHash}`);
44
+ }
45
+ }
39
46
  }
40
47
  async loadChunkByIndex(chunkIndex) {
41
48
  await this.loadChunkHashes();
@@ -35,6 +35,12 @@ export declare const FILE_CHUNK_SIZE: number;
35
35
  export declare const CHUNKS_DIR = "file_chunks";
36
36
  export declare let CHUNK_INDEX_THRESHOLD: number;
37
37
  export declare function setChunkIndexThreshold(threshold: number): void;
38
+ /**
39
+ * Compute the file-system hash for content without saving it.
40
+ * Mirrors the chunking + hashing logic of FileWriteStream so the result
41
+ * matches `IFile.fileHash` for the same bytes.
42
+ */
43
+ export declare function computeFileHash(content: string | Uint8Array): string;
38
44
  export interface FileOps {
39
45
  downloadFileChunk(chunkHash: string): Promise<Uint8Array | null>;
40
46
  fileExists(path: string): Promise<boolean>;
@@ -2,10 +2,12 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.CHUNK_INDEX_THRESHOLD = exports.CHUNKS_DIR = exports.FILE_CHUNK_SIZE = exports.filesMetaData = exports.fileSchema = void 0;
4
4
  exports.setChunkIndexThreshold = setChunkIndexThreshold;
5
+ exports.computeFileHash = computeFileHash;
5
6
  exports.setFileOps = setFileOps;
6
7
  exports.getFileOps = getFileOps;
7
8
  exports.resetFileOps = resetFileOps;
8
9
  const zod_1 = require("zod");
10
+ const keys_1 = require("../../keys");
9
11
  const zod_types_1 = require("../../types/zod-types");
10
12
  const types_1 = require("../orm/types");
11
13
  exports.fileSchema = zod_1.z.object({
@@ -38,6 +40,22 @@ exports.CHUNK_INDEX_THRESHOLD = 1000; // Use chunk index file for files with >10
38
40
  function setChunkIndexThreshold(threshold) {
39
41
  exports.CHUNK_INDEX_THRESHOLD = threshold;
40
42
  }
43
+ /**
44
+ * Compute the file-system hash for content without saving it.
45
+ * Mirrors the chunking + hashing logic of FileWriteStream so the result
46
+ * matches `IFile.fileHash` for the same bytes.
47
+ */
48
+ function computeFileHash(content) {
49
+ const data = typeof content === "string" ? new Uint8Array(Buffer.from(content, "utf8")) : content;
50
+ const chunkHashes = [];
51
+ let offset = 0;
52
+ while (offset < data.length) {
53
+ const end = Math.min(offset + exports.FILE_CHUNK_SIZE, data.length);
54
+ chunkHashes.push((0, keys_1.hashBytes)(data.subarray(offset, end)));
55
+ offset = end;
56
+ }
57
+ return (0, keys_1.hashBytes)(new Uint8Array(Buffer.from(JSON.stringify(chunkHashes), "utf8")));
58
+ }
41
59
  let fileOps = null;
42
60
  let fileOpsReady = null;
43
61
  function setFileOps(ops) {
@@ -1,5 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ const keys_1 = require("../../keys");
3
4
  const field_type_1 = require("../../types/field-type");
4
5
  const utils_1 = require("../../utils");
5
6
  const file_types_1 = require("./file.types");
@@ -201,7 +202,7 @@ describe("FileTable", () => {
201
202
  const result = await fileTable.getFileContents("non-existent");
202
203
  expect(result).toBeNull();
203
204
  });
204
- it("should return null when chunk is missing", async () => {
205
+ it("should throw integrity error when fileHash doesn't match chunk hashes", async () => {
205
206
  const fileId = (0, utils_1.newid)();
206
207
  const metadata = {
207
208
  fileId,
@@ -210,10 +211,9 @@ describe("FileTable", () => {
210
211
  fileHash: "hash123",
211
212
  chunkHashes: ["hash1", "hash2"],
212
213
  };
213
- // Insert metadata directly without saving chunks
214
+ // Insert metadata directly with mismatched fileHash
214
215
  await fileTable.dataSource.insert(metadata);
215
- const result = await fileTable.getFileContents(fileId);
216
- expect(result).toBeNull();
216
+ await expect(fileTable.getFileContents(fileId)).rejects.toThrow("File integrity check failed");
217
217
  });
218
218
  it("should return null when chunk is missing during read", async () => {
219
219
  const result = await fileTable.getFileContents("non-existent");
@@ -674,20 +674,63 @@ describe("FileTable", () => {
674
674
  });
675
675
  it("should return null when chunk is unavailable", async () => {
676
676
  const fileId = (0, utils_1.newid)();
677
+ const chunkHashes = ["missing_chunk_hash"];
678
+ const fileHash = (0, keys_1.hashBytes)(new TextEncoder().encode(JSON.stringify(chunkHashes)));
677
679
  const metadata = {
678
680
  fileId,
679
681
  name: "missing-chunk.txt",
680
682
  fileSize: 50,
681
- fileHash: "hash123",
682
- chunkHashes: ["missing_chunk_hash"],
683
+ fileHash,
684
+ chunkHashes,
683
685
  };
684
686
  // Insert file metadata without storing the actual chunk
685
687
  await fileTable.dataSource.insert(metadata);
686
688
  // This should return null when the chunk can't be found
687
- // instead of throwing an error or hanging
688
689
  const result = await fileTable.getFileContents(fileId);
689
690
  expect(result).toBeNull();
690
691
  });
692
+ it("should throw when fileHash does not match chunk hashes", async () => {
693
+ const fileId = (0, utils_1.newid)();
694
+ const data = new Uint8Array(Buffer.from("Valid content", "utf8"));
695
+ const metadata = {
696
+ fileId,
697
+ name: "tampered.txt",
698
+ fileSize: data.length,
699
+ mimeType: "text/plain",
700
+ };
701
+ // Save file normally to get valid chunks on disk
702
+ const saved = await fileTable.saveFile(metadata, data);
703
+ // Tamper with the fileHash in the database record
704
+ const tamperedRecord = {
705
+ ...saved,
706
+ fileHash: "tampered-hash-value",
707
+ };
708
+ await fileTable.dataSource.save(tamperedRecord);
709
+ // Reading should throw a file integrity error
710
+ await expect(fileTable.getFileContents(fileId)).rejects.toThrow("File integrity check failed");
711
+ });
712
+ it("should skip fileHash verification when skipVerification is true", async () => {
713
+ const fileId = (0, utils_1.newid)();
714
+ const data = new Uint8Array(Buffer.from("Valid content", "utf8"));
715
+ const metadata = {
716
+ fileId,
717
+ name: "skip-verify.txt",
718
+ fileSize: data.length,
719
+ mimeType: "text/plain",
720
+ };
721
+ // Save file normally to get valid chunks on disk
722
+ const saved = await fileTable.saveFile(metadata, data);
723
+ // Tamper with the fileHash in the database record
724
+ const tamperedRecord = {
725
+ ...saved,
726
+ fileHash: "tampered-hash-value",
727
+ };
728
+ await fileTable.dataSource.save(tamperedRecord);
729
+ // Reading with skipVerification should succeed
730
+ const result = await fileTable.getFileContents(fileId, { skipVerification: true });
731
+ expect(result).not.toBeNull();
732
+ expect(new Uint8Array(result)).toEqual(data);
733
+ });
691
734
  });
692
735
  describe("Round-trip streaming", () => {
693
736
  it("should write and read back identical data using streams", async () => {
@@ -72,6 +72,7 @@ function makePkg(overrides = {}) {
72
72
  name: "test-package",
73
73
  description: "test",
74
74
  createdBy: testUserId,
75
+ publishPublicKey: "",
75
76
  signature: "",
76
77
  ...overrides,
77
78
  };
@@ -30,6 +30,7 @@ declare const schema: z.ZodObject<{
30
30
  navigationPath: string;
31
31
  displayName?: string | undefined;
32
32
  }>, "many">>;
33
+ packageAuthorSignature: z.ZodOptional<z.ZodString>;
33
34
  history: z.ZodOptional<z.ZodArray<z.ZodObject<{
34
35
  action: z.ZodString;
35
36
  by: z.ZodString;
@@ -76,6 +77,7 @@ declare const schema: z.ZodObject<{
76
77
  navigationPath: string;
77
78
  displayName?: string | undefined;
78
79
  }[] | undefined;
80
+ packageAuthorSignature?: string | undefined;
79
81
  }, {
80
82
  version: string;
81
83
  signature: string;
@@ -103,6 +105,7 @@ declare const schema: z.ZodObject<{
103
105
  navigationPath: string;
104
106
  displayName?: string | undefined;
105
107
  }[] | undefined;
108
+ packageAuthorSignature?: string | undefined;
106
109
  }>;
107
110
  export type IPackageVersion = z.infer<typeof schema>;
108
111
  export declare class PackageVersionsTable extends Table<IPackageVersion> {
@@ -66,6 +66,11 @@ const schema = zod_1.z.object({
66
66
  .array()
67
67
  .optional()
68
68
  .describe("The app navigation items that this version provides"),
69
+ packageAuthorSignature: zod_1.z
70
+ .string()
71
+ .optional()
72
+ .describe("Publisher's Ed25519 detached signature over the author-signed payload. " +
73
+ "Enables cross-group trust verification without needing the original source."),
69
74
  history: zod_1.z
70
75
  .array(zod_1.z.object({
71
76
  action: zod_1.z.string().describe("created | promoted:beta | promoted:stable | activated"),
@@ -29,6 +29,8 @@ declare const schema: z.ZodObject<{
29
29
  activePackageVersionId: z.ZodOptional<z.ZodEffects<z.ZodString, string, string>>;
30
30
  versionFollowRange: z.ZodOptional<z.ZodEnum<["pinned", "patch", "minor", "latest"]>>;
31
31
  followVersionTags: z.ZodOptional<z.ZodString>;
32
+ updateUrl: z.ZodOptional<z.ZodString>;
33
+ publishPublicKey: z.ZodString;
32
34
  signature: z.ZodString;
33
35
  }, "strip", z.ZodTypeAny, {
34
36
  name: string;
@@ -36,6 +38,7 @@ declare const schema: z.ZodObject<{
36
38
  signature: string;
37
39
  packageId: string;
38
40
  createdBy: string;
41
+ publishPublicKey: string;
39
42
  disabled?: boolean | undefined;
40
43
  appNavs?: {
41
44
  name: string;
@@ -47,12 +50,14 @@ declare const schema: z.ZodObject<{
47
50
  activePackageVersionId?: string | undefined;
48
51
  versionFollowRange?: "pinned" | "patch" | "minor" | "latest" | undefined;
49
52
  followVersionTags?: string | undefined;
53
+ updateUrl?: string | undefined;
50
54
  }, {
51
55
  name: string;
52
56
  description: string;
53
57
  signature: string;
54
58
  packageId: string;
55
59
  createdBy: string;
60
+ publishPublicKey: string;
56
61
  disabled?: boolean | undefined;
57
62
  appNavs?: {
58
63
  name: string;
@@ -64,6 +69,7 @@ declare const schema: z.ZodObject<{
64
69
  activePackageVersionId?: string | undefined;
65
70
  versionFollowRange?: "pinned" | "patch" | "minor" | "latest" | undefined;
66
71
  followVersionTags?: string | undefined;
72
+ updateUrl?: string | undefined;
67
73
  }>;
68
74
  export type IPackage = z.infer<typeof schema>;
69
75
  export declare class PackagesTable extends Table<IPackage> {
@@ -72,6 +72,14 @@ const schema = zod_1.z.object({
72
72
  .string()
73
73
  .optional()
74
74
  .describe('Tag policy: undefined="current" (follow active tag), "*"=any tag, or CSV like "stable,prod"'),
75
+ updateUrl: zod_1.z
76
+ .string()
77
+ .optional()
78
+ .describe("Base URL for remote package seeding. Admin devices check <updateUrl>/latest-<tag>.json on startup."),
79
+ publishPublicKey: zod_1.z
80
+ .string()
81
+ .describe("Ed25519 public key (base64url) of the package publisher. " +
82
+ "Set at package creation; used as TOFU anchor for verifying packageAuthorSignature on versions."),
75
83
  signature: zod_1.z.string().describe("The signed hash of this data excluding the signature itself"),
76
84
  });
77
85
  const metaData = {
package/dist/index.d.ts CHANGED
@@ -19,6 +19,7 @@ export * from "./keys";
19
19
  export * from "./logging";
20
20
  export * from "./mentions";
21
21
  export * from "./observable";
22
+ export * from "./package-installer";
22
23
  export * from "./package-loader";
23
24
  export * from "./peers-ui/peers-ui";
24
25
  export * from "./peers-ui/peers-ui.types";
package/dist/index.js CHANGED
@@ -40,6 +40,7 @@ __exportStar(require("./keys"), exports);
40
40
  __exportStar(require("./logging"), exports);
41
41
  __exportStar(require("./mentions"), exports);
42
42
  __exportStar(require("./observable"), exports);
43
+ __exportStar(require("./package-installer"), exports);
43
44
  __exportStar(require("./package-loader"), exports);
44
45
  __exportStar(require("./peers-ui/peers-ui"), exports);
45
46
  __exportStar(require("./peers-ui/peers-ui.types"), exports);
@@ -0,0 +1,10 @@
1
+ export * from "./package-author-signing";
2
+ export * from "./package-cloner";
3
+ export * from "./package-creator";
4
+ export * from "./package-installer";
5
+ export * from "./package-propagation";
6
+ export * from "./package-publisher";
7
+ export * from "./package-remote-checker";
8
+ export * from "./package-seed-installer";
9
+ export * from "./package-tarball";
10
+ export * from "./types";
@@ -0,0 +1,26 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ __exportStar(require("./package-author-signing"), exports);
18
+ __exportStar(require("./package-cloner"), exports);
19
+ __exportStar(require("./package-creator"), exports);
20
+ __exportStar(require("./package-installer"), exports);
21
+ __exportStar(require("./package-propagation"), exports);
22
+ __exportStar(require("./package-publisher"), exports);
23
+ __exportStar(require("./package-remote-checker"), exports);
24
+ __exportStar(require("./package-seed-installer"), exports);
25
+ __exportStar(require("./package-tarball"), exports);
26
+ __exportStar(require("./types"), exports);
@@ -0,0 +1,48 @@
1
+ import type { IPackageVersion } from "../data/package-versions";
2
+ /** Prefix for the persistent variable name that stores a package's signing secret key. */
3
+ export declare const PACKAGE_SIGNING_KEY_PREFIX = "packageSigningKey_";
4
+ /**
5
+ * The subset of PackageVersion fields that the publisher signs.
6
+ * The `publicKey` field is included so verifiers know which key to check against.
7
+ */
8
+ export interface IAuthorSignedPayload {
9
+ packageId: string;
10
+ packageVersionId: string;
11
+ version: string;
12
+ versionTag: string;
13
+ packageBundleFileHash: string;
14
+ routesBundleFileHash?: string;
15
+ uiBundleFileHash?: string;
16
+ publicKey: string;
17
+ }
18
+ /**
19
+ * Extracts the signable payload from a PackageVersion record.
20
+ * Only includes optional hash fields when they have a value (undefined fields are omitted
21
+ * entirely so `stableStringify` produces a consistent output).
22
+ */
23
+ export declare function buildAuthorSignedPayload(pv: Pick<IPackageVersion, "packageId" | "packageVersionId" | "version" | "versionTag" | "packageBundleFileHash" | "routesBundleFileHash" | "uiBundleFileHash">, publicKey: string): IAuthorSignedPayload;
24
+ /**
25
+ * Signs a PackageVersion with the publisher's Ed25519 secret key.
26
+ * @returns A base64url-encoded detached Ed25519 signature over `stableStringify(payload)`.
27
+ */
28
+ export declare function signPackageAuthor(pv: Pick<IPackageVersion, "packageId" | "packageVersionId" | "version" | "versionTag" | "packageBundleFileHash" | "routesBundleFileHash" | "uiBundleFileHash">, secretKey: string): string;
29
+ /**
30
+ * Result of verifying a packageAuthorSignature.
31
+ * `publicKey` is the key embedded in the payload (extracted from the signature verification process).
32
+ */
33
+ export interface IVerifyAuthorSignatureResult {
34
+ valid: boolean;
35
+ publicKey: string;
36
+ }
37
+ /**
38
+ * Verifies the `packageAuthorSignature` on a PackageVersion record.
39
+ *
40
+ * The signature format is a raw base64url detached Ed25519 signature. The public key
41
+ * is NOT in the signature itself — it must be provided via `expectedPublicKey` (TOFU check)
42
+ * to reconstruct the payload and verify.
43
+ *
44
+ * @param pv - The PackageVersion record with `packageAuthorSignature` set
45
+ * @param expectedPublicKey - The public key to verify against (from Package.publishPublicKey)
46
+ * @returns `{ valid, publicKey }` — valid is true if signature verifies and key matches
47
+ */
48
+ export declare function verifyPackageAuthorSignature(pv: Pick<IPackageVersion, "packageId" | "packageVersionId" | "version" | "versionTag" | "packageBundleFileHash" | "routesBundleFileHash" | "uiBundleFileHash" | "packageAuthorSignature">, expectedPublicKey: string): IVerifyAuthorSignatureResult;
@@ -0,0 +1,73 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.PACKAGE_SIGNING_KEY_PREFIX = void 0;
4
+ exports.buildAuthorSignedPayload = buildAuthorSignedPayload;
5
+ exports.signPackageAuthor = signPackageAuthor;
6
+ exports.verifyPackageAuthorSignature = verifyPackageAuthorSignature;
7
+ const nacl = require("tweetnacl");
8
+ const tweetnacl_util_1 = require("tweetnacl-util");
9
+ const keys_1 = require("../keys");
10
+ /** Prefix for the persistent variable name that stores a package's signing secret key. */
11
+ exports.PACKAGE_SIGNING_KEY_PREFIX = "packageSigningKey_";
12
+ /**
13
+ * Extracts the signable payload from a PackageVersion record.
14
+ * Only includes optional hash fields when they have a value (undefined fields are omitted
15
+ * entirely so `stableStringify` produces a consistent output).
16
+ */
17
+ function buildAuthorSignedPayload(pv, publicKey) {
18
+ const payload = {
19
+ packageId: pv.packageId,
20
+ packageVersionId: pv.packageVersionId,
21
+ version: pv.version,
22
+ versionTag: pv.versionTag ?? "",
23
+ packageBundleFileHash: pv.packageBundleFileHash,
24
+ publicKey,
25
+ };
26
+ if (pv.routesBundleFileHash) {
27
+ payload.routesBundleFileHash = pv.routesBundleFileHash;
28
+ }
29
+ if (pv.uiBundleFileHash) {
30
+ payload.uiBundleFileHash = pv.uiBundleFileHash;
31
+ }
32
+ return payload;
33
+ }
34
+ /**
35
+ * Signs a PackageVersion with the publisher's Ed25519 secret key.
36
+ * @returns A base64url-encoded detached Ed25519 signature over `stableStringify(payload)`.
37
+ */
38
+ function signPackageAuthor(pv, secretKey) {
39
+ const keys = (0, keys_1.hydrateKeys)(secretKey);
40
+ const payload = buildAuthorSignedPayload(pv, keys.publicKey);
41
+ const message = (0, tweetnacl_util_1.decodeUTF8)((0, keys_1.stableStringify)(payload));
42
+ const sk = (0, keys_1.decodeBase64)(secretKey);
43
+ const signature = nacl.sign.detached(message, sk);
44
+ return (0, keys_1.encodeBase64)(signature);
45
+ }
46
+ /**
47
+ * Verifies the `packageAuthorSignature` on a PackageVersion record.
48
+ *
49
+ * The signature format is a raw base64url detached Ed25519 signature. The public key
50
+ * is NOT in the signature itself — it must be provided via `expectedPublicKey` (TOFU check)
51
+ * to reconstruct the payload and verify.
52
+ *
53
+ * @param pv - The PackageVersion record with `packageAuthorSignature` set
54
+ * @param expectedPublicKey - The public key to verify against (from Package.publishPublicKey)
55
+ * @returns `{ valid, publicKey }` — valid is true if signature verifies and key matches
56
+ */
57
+ function verifyPackageAuthorSignature(pv, expectedPublicKey) {
58
+ const result = { valid: false, publicKey: expectedPublicKey };
59
+ if (!pv.packageAuthorSignature) {
60
+ return result;
61
+ }
62
+ try {
63
+ const payload = buildAuthorSignedPayload(pv, expectedPublicKey);
64
+ const message = (0, tweetnacl_util_1.decodeUTF8)((0, keys_1.stableStringify)(payload));
65
+ const signature = (0, keys_1.decodeBase64)(pv.packageAuthorSignature);
66
+ const pk = (0, keys_1.decodeBase64)(expectedPublicKey);
67
+ result.valid = nacl.sign.detached.verify(message, signature, pk);
68
+ }
69
+ catch {
70
+ result.valid = false;
71
+ }
72
+ return result;
73
+ }