@peers-app/peers-sdk 0.21.7 → 0.23.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 (53) hide show
  1. package/dist/context/user-context-singleton.d.ts +42 -0
  2. package/dist/context/user-context-singleton.js +45 -0
  3. package/dist/contracts/__tests__/contract-proxy.test.js +51 -1
  4. package/dist/contracts/__tests__/contract-resolvers.test.js +65 -0
  5. package/dist/contracts/__tests__/playground.test.js +5 -2
  6. package/dist/contracts/__tests__/system-contracts.test.js +3 -0
  7. package/dist/contracts/contract-proxy.d.ts +27 -0
  8. package/dist/contracts/contract-proxy.js +37 -1
  9. package/dist/contracts/contract-resolvers.d.ts +10 -0
  10. package/dist/contracts/contract-resolvers.js +21 -3
  11. package/dist/contracts/index.d.ts +3 -2
  12. package/dist/contracts/index.js +5 -2
  13. package/dist/contracts/system/index.d.ts +2 -0
  14. package/dist/contracts/system/index.js +5 -1
  15. package/dist/contracts/system/tool-access-control-contract.d.ts +35 -0
  16. package/dist/contracts/system/tool-access-control-contract.js +70 -0
  17. package/dist/data/access-level.d.ts +72 -0
  18. package/dist/data/access-level.js +109 -0
  19. package/dist/data/access-level.test.d.ts +1 -0
  20. package/dist/data/access-level.test.js +53 -0
  21. package/dist/data/assistants.d.ts +24 -0
  22. package/dist/data/assistants.js +21 -0
  23. package/dist/data/group-member-roles.d.ts +9 -0
  24. package/dist/data/group-member-roles.js +9 -0
  25. package/dist/data/group-permissions.d.ts +55 -1
  26. package/dist/data/group-permissions.js +76 -6
  27. package/dist/data/index.d.ts +3 -1
  28. package/dist/data/index.js +3 -1
  29. package/dist/data/orm/sql.data-source.d.ts +10 -0
  30. package/dist/data/orm/sql.data-source.js +48 -30
  31. package/dist/data/orm/sql.data-source.test.js +13 -0
  32. package/dist/data/package-version-resolver.test.js +2 -0
  33. package/dist/data/tool-access-approvals.d.ts +126 -0
  34. package/dist/data/tool-access-approvals.js +86 -0
  35. package/dist/data/tool-access.d.ts +162 -0
  36. package/dist/data/tool-access.js +235 -0
  37. package/dist/data/tool-access.test.d.ts +1 -0
  38. package/dist/data/tool-access.test.js +159 -0
  39. package/dist/data/tools.d.ts +15 -0
  40. package/dist/data/tools.js +11 -0
  41. package/dist/data/workflow-logs.d.ts +5 -5
  42. package/dist/data/workflow-runs.d.ts +2 -2
  43. package/dist/device-pairing/types.d.ts +6 -6
  44. package/dist/package-installer/package-creator.d.ts +31 -0
  45. package/dist/package-installer/package-creator.js +140 -40
  46. package/dist/package-installer/package-creator.test.js +113 -11
  47. package/dist/rpc-types.d.ts +0 -17
  48. package/dist/rpc-types.js +0 -15
  49. package/dist/system-ids.d.ts +13 -0
  50. package/dist/system-ids.js +14 -1
  51. package/package.json +1 -1
  52. package/dist/data/voice-messages.d.ts +0 -33
  53. package/dist/data/voice-messages.js +0 -33
@@ -29,8 +29,8 @@ export declare const workflowLogSchema: z.ZodObject<{
29
29
  resultObject: z.ZodOptional<z.ZodUnion<[z.ZodObject<{}, "strip", z.ZodAny, z.objectOutputType<{}, z.ZodAny, "strip">, z.objectInputType<{}, z.ZodAny, "strip">>, z.ZodArray<z.ZodAny, "many">]>>;
30
30
  isError: z.ZodOptional<z.ZodBoolean>;
31
31
  }, "strip", z.ZodTypeAny, {
32
- workflowLogId: string;
33
32
  workflowRunId: string;
33
+ workflowLogId: string;
34
34
  contextId: string;
35
35
  logDT: Date;
36
36
  logText: string;
@@ -47,13 +47,13 @@ export declare const workflowLogSchema: z.ZodObject<{
47
47
  resultObject?: any[] | z.objectOutputType<{}, z.ZodAny, "strip"> | undefined;
48
48
  isError?: boolean | undefined;
49
49
  }, {
50
- workflowLogId: string;
51
50
  workflowRunId: string;
51
+ workflowLogId: string;
52
52
  contextId: string;
53
53
  logText: string;
54
54
  toolId?: string | undefined;
55
- logDT?: Date | undefined;
56
55
  result?: string | undefined;
56
+ logDT?: Date | undefined;
57
57
  instruction?: {
58
58
  markdown?: string | undefined;
59
59
  onError?: string | undefined;
@@ -67,8 +67,8 @@ export declare const workflowLogSchema: z.ZodObject<{
67
67
  }>;
68
68
  export type IWorkflowLog = z.infer<typeof workflowLogSchema>;
69
69
  export declare function WorkflowLogs(dataContext?: DataContext): import("..").Table<{
70
- workflowLogId: string;
71
70
  workflowRunId: string;
71
+ workflowLogId: string;
72
72
  contextId: string;
73
73
  logDT: Date;
74
74
  logText: string;
@@ -86,8 +86,8 @@ export declare function WorkflowLogs(dataContext?: DataContext): import("..").Ta
86
86
  isError?: boolean | undefined;
87
87
  }>;
88
88
  export declare const getLogger: (workflowRunId: string, contextId: string) => (logData: string | Omit<Partial<IWorkflowLog>, "workflowRunId" | "workflowLogId">) => Promise<{
89
- workflowLogId: string;
90
89
  workflowRunId: string;
90
+ workflowLogId: string;
91
91
  contextId: string;
92
92
  logDT: Date;
93
93
  logText: string;
@@ -33,6 +33,7 @@ export declare const workflowRunSchema: z.ZodObject<{
33
33
  defaultAssistantId: z.ZodEffects<z.ZodString, string, string>;
34
34
  }, "strip", z.ZodTypeAny, {
35
35
  createdAt: Date;
36
+ workflowRunId: string;
36
37
  defaultAssistantId: string;
37
38
  instructions: {
38
39
  markdown?: string | undefined;
@@ -40,7 +41,6 @@ export declare const workflowRunSchema: z.ZodObject<{
40
41
  directCallToolId?: string | undefined;
41
42
  subWorkflowId?: string | undefined;
42
43
  }[];
43
- workflowRunId: string;
44
44
  parentMessageId: string;
45
45
  currentInstructionIndex: number;
46
46
  instructionResults: any[];
@@ -54,8 +54,8 @@ export declare const workflowRunSchema: z.ZodObject<{
54
54
  completedAt?: Date | undefined;
55
55
  parentWorkflowRunId?: string | undefined;
56
56
  }, {
57
- defaultAssistantId: string;
58
57
  workflowRunId: string;
58
+ defaultAssistantId: string;
59
59
  parentMessageId: string;
60
60
  createdAt?: Date | undefined;
61
61
  workflowId?: string | undefined;
@@ -715,14 +715,14 @@ export declare const pairingApprovalContentsSchema: z.ZodObject<{
715
715
  role: "source" | "destination";
716
716
  version: 1;
717
717
  expiresAt: number;
718
- transcriptHash: string;
719
718
  approvalId: string;
719
+ transcriptHash: string;
720
720
  }, {
721
721
  role: "source" | "destination";
722
722
  version: 1;
723
723
  expiresAt: number;
724
- transcriptHash: string;
725
724
  approvalId: string;
725
+ transcriptHash: string;
726
726
  }>;
727
727
  /** Contents of a signed pairing approval. */
728
728
  export type PairingApprovalContents = z.infer<typeof pairingApprovalContentsSchema>;
@@ -738,14 +738,14 @@ export declare const signedPairingApprovalSchema: z.ZodObject<{
738
738
  role: "source" | "destination";
739
739
  version: 1;
740
740
  expiresAt: number;
741
- transcriptHash: string;
742
741
  approvalId: string;
742
+ transcriptHash: string;
743
743
  }, {
744
744
  role: "source" | "destination";
745
745
  version: 1;
746
746
  expiresAt: number;
747
- transcriptHash: string;
748
747
  approvalId: string;
748
+ transcriptHash: string;
749
749
  }>;
750
750
  signature: z.ZodString;
751
751
  publicKey: z.ZodString;
@@ -756,8 +756,8 @@ export declare const signedPairingApprovalSchema: z.ZodObject<{
756
756
  role: "source" | "destination";
757
757
  version: 1;
758
758
  expiresAt: number;
759
- transcriptHash: string;
760
759
  approvalId: string;
760
+ transcriptHash: string;
761
761
  };
762
762
  }, {
763
763
  publicKey: string;
@@ -766,8 +766,8 @@ export declare const signedPairingApprovalSchema: z.ZodObject<{
766
766
  role: "source" | "destination";
767
767
  version: 1;
768
768
  expiresAt: number;
769
- transcriptHash: string;
770
769
  approvalId: string;
770
+ transcriptHash: string;
771
771
  };
772
772
  }>;
773
773
  /** Signed approval bound to one transcript and endpoint identity. */
@@ -12,11 +12,42 @@ export declare function nameToFolderSlug(name: string): string;
12
12
  * @returns The path to the cached template directory.
13
13
  */
14
14
  export declare function ensureTemplateCache(deps: IPackageInstallerDeps, templateRepo: string): Promise<string>;
15
+ /**
16
+ * Resolve (or create) the Ed25519 publish public key for a package.
17
+ *
18
+ * Idempotent: never rotates an established key. Reuses `existingPublishPublicKey`
19
+ * when present, derives the public key from an existing secret pvar when possible,
20
+ * and only generates a new keypair when both are missing.
21
+ *
22
+ * @throws When a readable secret key does not match an established `publishPublicKey`.
23
+ */
24
+ export declare function ensurePackageSigningKey(opts: {
25
+ personalContext?: DataContext;
26
+ packageId: string;
27
+ packageName: string;
28
+ existingPublishPublicKey?: string;
29
+ }): Promise<string>;
30
+ /**
31
+ * Import an existing on-disk Peers package directory into the current data context.
32
+ *
33
+ * Validates the manifest packageId, creates/updates the Packages record, persists
34
+ * `packageLocalPath_*`, provisions a signing key when missing, then installs the
35
+ * local dev bundles.
36
+ */
37
+ export declare function importExistingPackage(dataContext: DataContext, deps: IPackageInstallerDeps, opts: {
38
+ location: string;
39
+ personalContext?: DataContext;
40
+ /** Display name fallback when the manifest name is a slug. */
41
+ displayName?: string;
42
+ }): Promise<ICreatePackageResult>;
15
43
  /**
16
44
  * Create a new Peers package from the package template.
17
45
  *
18
46
  * Clones the template repo, patches IDs and metadata, runs npm install + build,
19
47
  * initializes a fresh git repo, then delegates to {@link installPackageFromBundles}
20
48
  * to register the dev bundle in the database.
49
+ *
50
+ * When `location` already exists and contains a valid Peers package manifest,
51
+ * imports that directory instead (see {@link importExistingPackage}).
21
52
  */
22
53
  export declare function createPackage(dataContext: DataContext, deps: IPackageInstallerDeps, opts: ICreatePackageOpts): Promise<ICreatePackageResult>;
@@ -2,6 +2,8 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.nameToFolderSlug = nameToFolderSlug;
4
4
  exports.ensureTemplateCache = ensureTemplateCache;
5
+ exports.ensurePackageSigningKey = ensurePackageSigningKey;
6
+ exports.importExistingPackage = importExistingPackage;
5
7
  exports.createPackage = createPackage;
6
8
  const packages_1 = require("../data/packages");
7
9
  const persistent_vars_1 = require("../data/persistent-vars");
@@ -42,29 +44,148 @@ async function ensureTemplateCache(deps, templateRepo) {
42
44
  }
43
45
  return cachePath;
44
46
  }
47
+ /**
48
+ * Resolve (or create) the Ed25519 publish public key for a package.
49
+ *
50
+ * Idempotent: never rotates an established key. Reuses `existingPublishPublicKey`
51
+ * when present, derives the public key from an existing secret pvar when possible,
52
+ * and only generates a new keypair when both are missing.
53
+ *
54
+ * @throws When a readable secret key does not match an established `publishPublicKey`.
55
+ */
56
+ async function ensurePackageSigningKey(opts) {
57
+ const { personalContext, packageId, packageName, existingPublishPublicKey } = opts;
58
+ const establishedKey = existingPublishPublicKey?.trim() || "";
59
+ if (personalContext) {
60
+ const keyName = `${package_author_signing_1.PACKAGE_SIGNING_KEY_PREFIX}${packageId}`;
61
+ const existingKey = await (0, persistent_vars_1.PersistentVars)(personalContext).findOne({ name: keyName });
62
+ const rawSecret = existingKey?.value?.value;
63
+ if (typeof rawSecret === "string" && rawSecret.length > 0) {
64
+ try {
65
+ const derivedPublicKey = (0, keys_1.hydrateKeys)(rawSecret).publicKey;
66
+ if (establishedKey && derivedPublicKey !== establishedKey) {
67
+ throw new Error(`Package signing key mismatch for "${packageName}" (${packageId}): ` +
68
+ "stored secret does not match Packages.publishPublicKey. " +
69
+ "Refusing to rotate the established key.");
70
+ }
71
+ return establishedKey || derivedPublicKey;
72
+ }
73
+ catch (err) {
74
+ if (err instanceof Error && err.message.includes("signing key mismatch")) {
75
+ throw err;
76
+ }
77
+ // Secret is likely encrypted at rest — cannot derive here without the user key.
78
+ if (establishedKey) {
79
+ return establishedKey;
80
+ }
81
+ throw new Error(`Package "${packageName}" (${packageId}) has a signing secret pvar but no ` +
82
+ "publishPublicKey, and the secret could not be hydrated. " +
83
+ "Restore publishPublicKey or replace the unreadable key before importing.");
84
+ }
85
+ }
86
+ }
87
+ if (establishedKey) {
88
+ return establishedKey;
89
+ }
90
+ if (!personalContext) {
91
+ return "";
92
+ }
93
+ const keys = (0, keys_1.newKeys)();
94
+ const keyName = `${package_author_signing_1.PACKAGE_SIGNING_KEY_PREFIX}${packageId}`;
95
+ await (0, persistent_vars_1.PersistentVars)(personalContext).save({
96
+ persistentVarId: (0, utils_1.deterministicPvarId)(keyName),
97
+ name: keyName,
98
+ scope: "user",
99
+ value: { value: keys.secretKey },
100
+ isSecret: true,
101
+ description: `Signing key for package "${packageName}"`,
102
+ modifiedAt: Date.now(),
103
+ });
104
+ return keys.publicKey;
105
+ }
106
+ async function registerPackageRecord(dataContext, opts) {
107
+ const { packageId, name, description, publishPublicKey, remoteRepo, existing } = opts;
108
+ await (0, packages_1.Packages)(dataContext).signAndSave({
109
+ packageId,
110
+ name,
111
+ description: description || existing?.description || "",
112
+ createdBy: existing?.createdBy || system_ids_1.peersRootUserId,
113
+ disabled: existing?.disabled ?? false,
114
+ remoteRepo: remoteRepo || existing?.remoteRepo,
115
+ activePackageVersionId: existing?.activePackageVersionId,
116
+ versionFollowRange: existing?.versionFollowRange,
117
+ followVersionTags: existing?.followVersionTags,
118
+ updateUrl: existing?.updateUrl,
119
+ publishPublicKey: publishPublicKey || existing?.publishPublicKey || "",
120
+ signature: existing?.signature || "",
121
+ }, { restoreIfDeleted: true, saveAsSnapshot: true });
122
+ }
123
+ async function persistPackageLocalPath(dataContext, deps, packageId, location) {
124
+ const pvar = (0, persistent_vars_1.groupDeviceVar)(`packageLocalPath_${packageId}`, {
125
+ defaultValue: deps.packagesRootDir,
126
+ dataContext,
127
+ });
128
+ await pvar.loadingPromise;
129
+ pvar(location);
130
+ }
131
+ /**
132
+ * Import an existing on-disk Peers package directory into the current data context.
133
+ *
134
+ * Validates the manifest packageId, creates/updates the Packages record, persists
135
+ * `packageLocalPath_*`, provisions a signing key when missing, then installs the
136
+ * local dev bundles.
137
+ */
138
+ async function importExistingPackage(dataContext, deps, opts) {
139
+ const info = await (0, package_installer_1.getPackageInfo)(deps, opts.location);
140
+ if (!info.packageId) {
141
+ throw new Error(`Directory already exists at ${opts.location} but does not contain a valid Peers package`);
142
+ }
143
+ const packageId = info.packageId;
144
+ const existing = await (0, packages_1.Packages)(dataContext).get(packageId);
145
+ const packageName = opts.displayName || info.name || packageId;
146
+ const publishPublicKey = await ensurePackageSigningKey({
147
+ personalContext: opts.personalContext,
148
+ packageId,
149
+ packageName,
150
+ existingPublishPublicKey: existing?.publishPublicKey,
151
+ });
152
+ await registerPackageRecord(dataContext, {
153
+ packageId,
154
+ name: packageName,
155
+ description: info.description || "",
156
+ publishPublicKey,
157
+ remoteRepo: info.remoteRepo,
158
+ existing,
159
+ });
160
+ await persistPackageLocalPath(dataContext, deps, packageId, opts.location);
161
+ const result = await (0, package_installer_1.installPackageFromBundles)(dataContext, deps, packageId, {
162
+ localPath: opts.location,
163
+ });
164
+ return {
165
+ packageId,
166
+ localPath: opts.location,
167
+ packageVersion: result.packageVersion,
168
+ };
169
+ }
45
170
  /**
46
171
  * Create a new Peers package from the package template.
47
172
  *
48
173
  * Clones the template repo, patches IDs and metadata, runs npm install + build,
49
174
  * initializes a fresh git repo, then delegates to {@link installPackageFromBundles}
50
175
  * to register the dev bundle in the database.
176
+ *
177
+ * When `location` already exists and contains a valid Peers package manifest,
178
+ * imports that directory instead (see {@link importExistingPackage}).
51
179
  */
52
180
  async function createPackage(dataContext, deps, opts) {
53
181
  const location = opts.location ?? deps.resolvePath(deps.packagesRootDir, nameToFolderSlug(opts.name));
54
182
  // If directory already exists, treat as an import rather than a fresh create
55
183
  if (await deps.fs.exists(location)) {
56
- const info = await (0, package_installer_1.getPackageInfo)(deps, location);
57
- if (info.packageId) {
58
- const result = await (0, package_installer_1.installPackageFromBundles)(dataContext, deps, info.packageId, {
59
- localPath: location,
60
- });
61
- return {
62
- packageId: info.packageId,
63
- localPath: location,
64
- packageVersion: result.packageVersion,
65
- };
66
- }
67
- throw new Error(`Directory already exists at ${location} but does not contain a valid Peers package`);
184
+ return importExistingPackage(dataContext, deps, {
185
+ location,
186
+ personalContext: opts.personalContext,
187
+ displayName: opts.name,
188
+ });
68
189
  }
69
190
  // Generate fresh IDs
70
191
  const packageId = (0, utils_1.newid)();
@@ -110,39 +231,18 @@ async function createPackage(dataContext, deps, opts) {
110
231
  await deps.shell.exec("git init", { cwd: location });
111
232
  await deps.shell.exec("git add .", { cwd: location });
112
233
  await deps.shell.exec('git commit -m "Initial commit"', { cwd: location });
113
- // Generate signing keypair and store secret in user's personal group
114
- let publishPublicKey = "";
115
- if (opts.personalContext) {
116
- const keys = (0, keys_1.newKeys)();
117
- publishPublicKey = keys.publicKey;
118
- const keyName = `${package_author_signing_1.PACKAGE_SIGNING_KEY_PREFIX}${packageId}`;
119
- await (0, persistent_vars_1.PersistentVars)(opts.personalContext).save({
120
- persistentVarId: (0, utils_1.deterministicPvarId)(keyName),
121
- name: keyName,
122
- scope: "user",
123
- value: { value: keys.secretKey },
124
- isSecret: true,
125
- description: `Signing key for package "${opts.name}"`,
126
- modifiedAt: Date.now(),
127
- });
128
- }
129
- // Register the package record
130
- await (0, packages_1.Packages)(dataContext).signAndSave({
234
+ const publishPublicKey = await ensurePackageSigningKey({
235
+ personalContext: opts.personalContext,
236
+ packageId,
237
+ packageName: opts.name,
238
+ });
239
+ await registerPackageRecord(dataContext, {
131
240
  packageId,
132
241
  name: opts.name,
133
242
  description: "",
134
- createdBy: system_ids_1.peersRootUserId,
135
- disabled: false,
136
243
  publishPublicKey,
137
- signature: "",
138
- }, { restoreIfDeleted: true, saveAsSnapshot: true });
139
- // Set the packageLocalPath device var
140
- const pvar = (0, persistent_vars_1.groupDeviceVar)(`packageLocalPath_${packageId}`, {
141
- defaultValue: deps.packagesRootDir,
142
- dataContext,
143
244
  });
144
- await pvar.loadingPromise;
145
- pvar(location);
245
+ await persistPackageLocalPath(dataContext, deps, packageId, location);
146
246
  // Install the dev bundle
147
247
  const result = await (0, package_installer_1.installPackageFromBundles)(dataContext, deps, packageId, {
148
248
  localPath: location,
@@ -3,17 +3,21 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const package_creator_1 = require("./package-creator");
4
4
  // --- Mocks for SDK internals ---
5
5
  const mockSignAndSavePkg = jest.fn();
6
+ const mockGetPkg = jest.fn();
6
7
  jest.mock("../data/packages", () => ({
7
8
  Packages: () => ({
8
9
  signAndSave: mockSignAndSavePkg,
10
+ get: mockGetPkg,
9
11
  }),
10
12
  }));
11
13
  const mockGroupDeviceVar = jest.fn();
12
14
  const mockPersistentVarsSave = jest.fn();
15
+ const mockPersistentVarsFindOne = jest.fn();
13
16
  jest.mock("../data/persistent-vars", () => ({
14
17
  groupDeviceVar: (...args) => mockGroupDeviceVar(...args),
15
18
  PersistentVars: () => ({
16
19
  save: mockPersistentVarsSave,
20
+ findOne: mockPersistentVarsFindOne,
17
21
  }),
18
22
  }));
19
23
  const mockNewKeys = jest.fn();
@@ -70,6 +74,13 @@ function mockDataContext() {
70
74
  // --- Tests ---
71
75
  beforeEach(() => {
72
76
  jest.clearAllMocks();
77
+ mockGetPkg.mockResolvedValue(undefined);
78
+ mockPersistentVarsFindOne.mockResolvedValue(undefined);
79
+ mockNewKeys.mockReturnValue({
80
+ publicKey: "test-public-key-abc",
81
+ secretKey: "test-secret-key-xyz",
82
+ publicBoxKey: "test-box-key",
83
+ });
73
84
  });
74
85
  describe("nameToFolderSlug", () => {
75
86
  it("converts a simple name", () => {
@@ -170,16 +181,18 @@ describe("createPackage", () => {
170
181
  expect(result.localPath).toBe("/packages/my-widget");
171
182
  expect(result.packageVersion).toBe(mockPv);
172
183
  });
173
- it("delegates to installPackageFromBundles when directory already exists with valid package", async () => {
184
+ it("registers metadata, path, and signing key when importing an existing package directory", async () => {
174
185
  const deps = makeMockDeps({});
175
186
  const dc = mockDataContext();
176
- // Location exists
187
+ const personalCtx = mockDataContext();
177
188
  deps.fs.exists.mockResolvedValue(true);
178
- // getPackageInfo returns a valid packageId
179
189
  mockGetPackageInfo.mockResolvedValue({
180
190
  packageId: "00mnkoyo7mizav6tseubjzr3a",
181
191
  name: "existing-pkg",
192
+ description: "Imported package",
182
193
  });
194
+ const pvarSetter = jest.fn();
195
+ mockGroupDeviceVar.mockReturnValue(pvarSetter);
183
196
  const mockPv = { packageVersionId: "pv002", packageId: "00mnkoyo7mizav6tseubjzr3a" };
184
197
  mockInstallPackageFromBundles.mockResolvedValue({
185
198
  packageVersion: mockPv,
@@ -187,16 +200,67 @@ describe("createPackage", () => {
187
200
  unchanged: false,
188
201
  });
189
202
  const result = await (0, package_creator_1.createPackage)(dc, deps, {
190
- name: "existing-pkg",
203
+ name: "Existing Pkg",
191
204
  location: "/custom/path",
205
+ personalContext: personalCtx,
192
206
  });
193
- // Should NOT clone
194
207
  expect(deps.shell.exec).not.toHaveBeenCalled();
195
- // Should delegate to install
208
+ expect(mockGetPkg).toHaveBeenCalledWith("00mnkoyo7mizav6tseubjzr3a");
209
+ expect(mockNewKeys).toHaveBeenCalled();
210
+ expect(mockPersistentVarsSave).toHaveBeenCalledWith(expect.objectContaining({
211
+ name: "packageSigningKey_00mnkoyo7mizav6tseubjzr3a",
212
+ isSecret: true,
213
+ value: { value: "test-secret-key-xyz" },
214
+ }));
215
+ expect(mockSignAndSavePkg).toHaveBeenCalledWith(expect.objectContaining({
216
+ packageId: "00mnkoyo7mizav6tseubjzr3a",
217
+ name: "Existing Pkg",
218
+ description: "Imported package",
219
+ publishPublicKey: "test-public-key-abc",
220
+ }), { restoreIfDeleted: true, saveAsSnapshot: true });
221
+ expect(mockGroupDeviceVar).toHaveBeenCalledWith("packageLocalPath_00mnkoyo7mizav6tseubjzr3a", expect.any(Object));
222
+ expect(pvarSetter).toHaveBeenCalledWith("/custom/path");
196
223
  expect(mockInstallPackageFromBundles).toHaveBeenCalledWith(dc, deps, "00mnkoyo7mizav6tseubjzr3a", { localPath: "/custom/path" });
197
224
  expect(result.packageId).toBe("00mnkoyo7mizav6tseubjzr3a");
198
225
  expect(result.localPath).toBe("/custom/path");
199
226
  });
227
+ it("reuses an existing publishPublicKey and does not rotate the signing key on re-import", async () => {
228
+ const deps = makeMockDeps({});
229
+ const dc = mockDataContext();
230
+ const personalCtx = mockDataContext();
231
+ deps.fs.exists.mockResolvedValue(true);
232
+ mockGetPackageInfo.mockResolvedValue({
233
+ packageId: "00mnkoyo7mizav6tseubjzr3a",
234
+ name: "existing-pkg",
235
+ });
236
+ mockGetPkg.mockResolvedValue({
237
+ packageId: "00mnkoyo7mizav6tseubjzr3a",
238
+ name: "existing-pkg",
239
+ description: "Old description",
240
+ createdBy: "00m87fbwu96jzf8qnwllly0fd",
241
+ publishPublicKey: "already-established-public-key",
242
+ signature: "sig",
243
+ disabled: false,
244
+ });
245
+ const pvarSetter = jest.fn();
246
+ mockGroupDeviceVar.mockReturnValue(pvarSetter);
247
+ mockInstallPackageFromBundles.mockResolvedValue({
248
+ packageVersion: { packageVersionId: "pv002", packageId: "00mnkoyo7mizav6tseubjzr3a" },
249
+ loaded: undefined,
250
+ unchanged: false,
251
+ });
252
+ await (0, package_creator_1.createPackage)(dc, deps, {
253
+ name: "existing-pkg",
254
+ location: "/custom/path",
255
+ personalContext: personalCtx,
256
+ });
257
+ expect(mockNewKeys).not.toHaveBeenCalled();
258
+ expect(mockPersistentVarsSave).not.toHaveBeenCalled();
259
+ expect(mockSignAndSavePkg).toHaveBeenCalledWith(expect.objectContaining({
260
+ publishPublicKey: "already-established-public-key",
261
+ createdBy: "00m87fbwu96jzf8qnwllly0fd",
262
+ }), expect.any(Object));
263
+ });
200
264
  it("throws when directory exists but has no valid package", async () => {
201
265
  const deps = makeMockDeps({});
202
266
  const dc = mockDataContext();
@@ -268,11 +332,6 @@ describe("createPackage", () => {
268
332
  loaded: undefined,
269
333
  unchanged: false,
270
334
  });
271
- mockNewKeys.mockReturnValue({
272
- publicKey: "test-public-key-abc",
273
- secretKey: "test-secret-key-xyz",
274
- publicBoxKey: "test-box-key",
275
- });
276
335
  await (0, package_creator_1.createPackage)(dc, deps, {
277
336
  name: "Signed Pkg",
278
337
  personalContext: personalCtx,
@@ -319,6 +378,49 @@ describe("createPackage", () => {
319
378
  }), expect.any(Object));
320
379
  });
321
380
  });
381
+ describe("ensurePackageSigningKey", () => {
382
+ it("generates a key once when none exists", async () => {
383
+ const personalCtx = mockDataContext();
384
+ const publicKey = await (0, package_creator_1.ensurePackageSigningKey)({
385
+ personalContext: personalCtx,
386
+ packageId: "00mnkoyo7mizav6tseubjzr3a",
387
+ packageName: "Test Pkg",
388
+ });
389
+ expect(publicKey).toBe("test-public-key-abc");
390
+ expect(mockNewKeys).toHaveBeenCalledTimes(1);
391
+ expect(mockPersistentVarsSave).toHaveBeenCalledTimes(1);
392
+ });
393
+ it("reuses an established publishPublicKey without generating a new key", async () => {
394
+ const personalCtx = mockDataContext();
395
+ const publicKey = await (0, package_creator_1.ensurePackageSigningKey)({
396
+ personalContext: personalCtx,
397
+ packageId: "00mnkoyo7mizav6tseubjzr3a",
398
+ packageName: "Test Pkg",
399
+ existingPublishPublicKey: "established-key",
400
+ });
401
+ expect(publicKey).toBe("established-key");
402
+ expect(mockNewKeys).not.toHaveBeenCalled();
403
+ expect(mockPersistentVarsSave).not.toHaveBeenCalled();
404
+ });
405
+ it("throws on an unrecoverable public/private key mismatch", async () => {
406
+ const { newKeys: realNewKeys } = jest.requireActual("../keys");
407
+ const realKeys = realNewKeys();
408
+ const otherKeys = realNewKeys();
409
+ const personalCtx = mockDataContext();
410
+ mockPersistentVarsFindOne.mockResolvedValue({
411
+ name: "packageSigningKey_00mnkoyo7mizav6tseubjzr3a",
412
+ value: { value: realKeys.secretKey },
413
+ isSecret: true,
414
+ });
415
+ await expect((0, package_creator_1.ensurePackageSigningKey)({
416
+ personalContext: personalCtx,
417
+ packageId: "00mnkoyo7mizav6tseubjzr3a",
418
+ packageName: "Test Pkg",
419
+ existingPublishPublicKey: otherKeys.publicKey,
420
+ })).rejects.toThrow("signing key mismatch");
421
+ expect(mockNewKeys).not.toHaveBeenCalled();
422
+ });
423
+ });
322
424
  describe("ensureTemplateCache", () => {
323
425
  it("clones template when cache directory does not exist", async () => {
324
426
  const deps = makeMockDeps({});
@@ -174,23 +174,6 @@ export declare const rpcServerCalls: {
174
174
  success: boolean;
175
175
  error?: string;
176
176
  }>;
177
- voiceGetState: () => Promise<{
178
- state: "disabled" | "idle" | "listening" | "recording" | "processing" | "speaking";
179
- keyError?: "refused" | "invalid" | "limit" | "throttled";
180
- }>;
181
- voiceStartRecording: () => Promise<void>;
182
- voiceStopRecording: () => Promise<void>;
183
- voiceCancelRecording: () => Promise<void>;
184
- voiceSetTargetChannel: (channelId: string) => Promise<void>;
185
- voiceGetAudioDevices: () => Promise<string[]>;
186
- voiceTestTTS: (text: string) => Promise<void>;
187
- voiceNotifyPlaybackComplete: () => Promise<void>;
188
- voiceStopPlayback: () => Promise<void>;
189
- voiceGetThreadId: () => Promise<string | null>;
190
- voiceSetThreadId: (threadId: string | null) => Promise<void>;
191
- voiceNotifyTextActivity: () => Promise<void>;
192
- voiceDisable: () => Promise<void>;
193
- voiceEnable: () => Promise<void>;
194
177
  flushDatabases: () => Promise<void>;
195
178
  subscribeEvents: (names: string[]) => Promise<void>;
196
179
  unsubscribeEvents: (names: string[]) => Promise<void>;
package/dist/rpc-types.js CHANGED
@@ -84,21 +84,6 @@ exports.rpcServerCalls = {
84
84
  uiClick: rpcStub("uiClick"),
85
85
  uiSet: rpcStub("uiSet"),
86
86
  uiScroll: rpcStub("uiScroll"),
87
- // Voice control (settings managed via voiceSettings pvar, keys via secret pvars)
88
- voiceGetState: rpcStub("voiceGetState"),
89
- voiceStartRecording: rpcStub("voiceStartRecording"),
90
- voiceStopRecording: rpcStub("voiceStopRecording"),
91
- voiceCancelRecording: rpcStub("voiceCancelRecording"),
92
- voiceSetTargetChannel: rpcStub("voiceSetTargetChannel"),
93
- voiceGetAudioDevices: rpcStub("voiceGetAudioDevices"),
94
- voiceTestTTS: rpcStub("voiceTestTTS"),
95
- voiceNotifyPlaybackComplete: rpcStub("voiceNotifyPlaybackComplete"),
96
- voiceStopPlayback: rpcStub("voiceStopPlayback"),
97
- voiceGetThreadId: rpcStub("voiceGetThreadId"),
98
- voiceSetThreadId: rpcStub("voiceSetThreadId"),
99
- voiceNotifyTextActivity: rpcStub("voiceNotifyTextActivity"),
100
- voiceDisable: rpcStub("voiceDisable"),
101
- voiceEnable: rpcStub("voiceEnable"),
102
87
  // Flush all in-memory database snapshots to durable storage (IndexedDB in PWA).
103
88
  // No-op on Electron where better-sqlite3 writes are synchronous to disk.
104
89
  flushDatabases: (async () => { }),
@@ -4,6 +4,8 @@ export declare const peersRootUserId = "000peers0user000000000001";
4
4
  export declare const defaultAssistantId = "000peers0bot00000000shell";
5
5
  /** Stable ID of the bundled OpenCode assistant. */
6
6
  export declare const openCodeAssistantId = "00mlwl2km2opencode0000001";
7
+ /** Stable ID of the built-in Admin assistant. */
8
+ export declare const adminAssistantId = "000peers0bot00000000admin";
7
9
  /** Stable ID of the default assistant-runner tool. */
8
10
  export declare const defaultAssistantRunnerToolId = "000peers0tool00000runner1";
9
11
  /** Stable ID of the built-in send-message tool. */
@@ -36,3 +38,14 @@ export declare const deviceOperationsContractId = "00mriijkhq94msfe9m2j6quwp";
36
38
  export declare const networkDiagnosticsContractId = "00ms1yb50w29oi116uwwrg7rl";
37
39
  /** Contract id for explicit local network and synchronization control operations. */
38
40
  export declare const networkControlContractId = "00ms1yb50yj9w5qbytl69qfht";
41
+ /**
42
+ * Contract id for the local-only tool-access approval control surface.
43
+ * Not available over device-to-device connections.
44
+ */
45
+ export declare const toolAccessControlContractId = "000peers0toolaccessctl0001";
46
+ /** Stable IDs for tools in System Tool Access Control v1. */
47
+ export declare const toolAccessControlToolIds: {
48
+ readonly approve: "000peers0tacapprove000001";
49
+ readonly reject: "000peers0tacreject0000001";
50
+ readonly listPending: "000peers0taclist000000001";
51
+ };