@peers-app/peers-sdk 0.21.7 → 0.22.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.
@@ -239,4 +239,39 @@ describe("local user-context contract routing", () => {
239
239
  await expect(setup.consumerTransport.emit(index_1.CONTRACT_REQUEST_CHANNEL, makeToolCall(setup.contractId))).rejects.toThrow("contract");
240
240
  router.dispose();
241
241
  });
242
+ it("appends host-only variable access to local package tool calls", async () => {
243
+ const setup = setupLocalRouter();
244
+ const resolveVariable = jest.fn(async ({ dataContext, variableName, requestingToolId }) => [dataContext.dataContextId, variableName, requestingToolId].join(":"));
245
+ (0, index_1.registerContractResolver)(setup.contractId, 1, () => ({
246
+ contractId: setup.contractId,
247
+ version: 1,
248
+ tools: {
249
+ inspect: {
250
+ tool: {
251
+ toolId: "00localcontractsecrettool",
252
+ name: "inspect",
253
+ usageDescription: "Resolve a local variable.",
254
+ inputSchema: { type: "simple", fields: [] },
255
+ outputSchema: { type: "simple", fields: [] },
256
+ code: "",
257
+ },
258
+ toolFn: async (variableName, context) => context.getVariable(variableName, "00localcontractsecrettool"),
259
+ },
260
+ },
261
+ }));
262
+ const router = (0, index_1.createLocalUserContextContractProviderRouter)(setup.providerTransport, {
263
+ getUserContext: async () => setup.userContext,
264
+ resolveVariable,
265
+ });
266
+ await expect(setup.consumerTransport.emit(index_1.CONTRACT_REQUEST_CHANNEL, {
267
+ ...makeToolCall(setup.contractId),
268
+ args: ["OPENAI_API_KEY"],
269
+ })).resolves.toBe(`${setup.userId}:OPENAI_API_KEY:00localcontractsecrettool`);
270
+ expect(resolveVariable).toHaveBeenCalledWith(expect.objectContaining({
271
+ dataContext: expect.objectContaining({ dataContextId: setup.userId }),
272
+ variableName: "OPENAI_API_KEY",
273
+ requestingToolId: "00localcontractsecrettool",
274
+ }));
275
+ router.dispose();
276
+ });
242
277
  });
@@ -195,6 +195,22 @@ export interface IContractConsumer extends IContractConsumerBase {
195
195
  subscribe: (handler: (value: any) => void) => ISubscriptionResult;
196
196
  }>;
197
197
  }
198
+ /**
199
+ * Trusted host capabilities appended to local package-contract tool calls.
200
+ *
201
+ * The context is constructed by the provider host after it authorizes the data-context
202
+ * route. It is never serialized to the consumer and is intentionally absent for remote
203
+ * contract calls unless a host explicitly opts in.
204
+ */
205
+ export interface IContractToolContext {
206
+ /** Authorized data context in which the tool is executing. */
207
+ dataContextId: string;
208
+ /**
209
+ * Resolve a persistent variable in the authorized context. Secret values are decrypted
210
+ * by the host and returned only to the provider-side tool implementation.
211
+ */
212
+ getVariable(variableName: string, requestingToolId: string): Promise<string>;
213
+ }
198
214
  /**
199
215
  * Build the **consumer** side of a contract proxy from a contract definition and a
200
216
  * transport. Member names come from the contract shape; table members are backed by a
@@ -234,6 +250,11 @@ export interface IContractResolution {
234
250
  version: number;
235
251
  tables?: Record<string, Table<any>>;
236
252
  tools?: Record<string, IToolInstance>;
253
+ /**
254
+ * Host-only capabilities appended as the final argument to provider tool functions.
255
+ * Omit this for remote or otherwise untrusted provider routes.
256
+ */
257
+ toolContext?: IContractToolContext;
237
258
  observables?: Record<string, Observable<any>>;
238
259
  /**
239
260
  * Provider-owned write policy for live observables. A `set` is allowed only when the
@@ -394,7 +394,9 @@ function createContractProvider(resolution, opts = {}) {
394
394
  if (!tool) {
395
395
  throw new Error(`Contract ${call.contractId} does not provide tool '${call.name}'`);
396
396
  }
397
- return tool.toolFn(...call.args);
397
+ return resolution.toolContext
398
+ ? tool.toolFn(...call.args, resolution.toolContext)
399
+ : tool.toolFn(...call.args);
398
400
  }
399
401
  case "observable": {
400
402
  const obs = resolution.observables?.[call.name];
@@ -12,6 +12,14 @@ import type { IContractTransport } from "./contract-transport";
12
12
  export type ContractResolver = (ctx: {
13
13
  dataContext: DataContext;
14
14
  }) => IContractResolution | Promise<IContractResolution>;
15
+ /** Input supplied to a trusted local host when a package tool resolves a pvar. */
16
+ export interface ILocalContractVariableRequest {
17
+ dataContext: DataContext;
18
+ variableName: string;
19
+ requestingToolId: string;
20
+ }
21
+ /** Host callback that resolves plaintext only inside a local provider process. */
22
+ export type LocalContractVariableResolver = (request: ILocalContractVariableRequest) => Promise<string>;
15
23
  /** Connection-authenticated identity fields used to authorize remote contract calls. */
16
24
  export type ContractRemoteIdentity = Pick<IDeviceInfo, "deviceId" | "userId" | "publicKey" | "publicBoxKey">;
17
25
  /**
@@ -45,10 +53,12 @@ export declare function isSelfTrustedContractCaller(userContext: UserContext, re
45
53
  * @param transport Contract transport backed by an authenticated local session.
46
54
  * @param opts.getUserContext Lazy accessor for the host's loaded user context.
47
55
  * @param opts.allowedContractIds Optional contract allowlist for this session.
56
+ * @param opts.resolveVariable Optional host-only pvar resolver exposed to local package tools.
48
57
  */
49
58
  export declare function createLocalUserContextContractProviderRouter(transport: IContractTransport, opts: {
50
59
  getUserContext: () => Promise<UserContext>;
51
60
  allowedContractIds?: ReadonlySet<string>;
61
+ resolveVariable?: LocalContractVariableResolver;
52
62
  }): IContractProviderRouter;
53
63
  /**
54
64
  * Create the local-object provider router for one verified device connection.
@@ -100,9 +100,10 @@ function getAuthorizedLocalDataContext(userContext, requestedId) {
100
100
  * @param transport Contract transport backed by an authenticated local session.
101
101
  * @param opts.getUserContext Lazy accessor for the host's loaded user context.
102
102
  * @param opts.allowedContractIds Optional contract allowlist for this session.
103
+ * @param opts.resolveVariable Optional host-only pvar resolver exposed to local package tools.
103
104
  */
104
105
  function createLocalUserContextContractProviderRouter(transport, opts) {
105
- const { getUserContext, allowedContractIds } = opts;
106
+ const { getUserContext, allowedContractIds, resolveVariable } = opts;
106
107
  return (0, contract_provider_router_1.createContractProviderRouter)(transport, {
107
108
  authorizeCall: async (call) => {
108
109
  if (allowedContractIds && !allowedContractIds.has(call.contractId)) {
@@ -127,7 +128,16 @@ function createLocalUserContextContractProviderRouter(transport, opts) {
127
128
  }
128
129
  const dataContext = getAuthorizedLocalDataContext(userContext, dataContextId);
129
130
  const resolution = await resolver({ dataContext });
130
- return (0, contract_proxy_1.createContractProviderEndpoint)(resolution, { context, notify });
131
+ const localResolution = resolveVariable
132
+ ? {
133
+ ...resolution,
134
+ toolContext: {
135
+ dataContextId: dataContext.dataContextId,
136
+ getVariable: (variableName, requestingToolId) => resolveVariable({ dataContext, variableName, requestingToolId }),
137
+ },
138
+ }
139
+ : resolution;
140
+ return (0, contract_proxy_1.createContractProviderEndpoint)(localResolution, { context, notify });
131
141
  },
132
142
  });
133
143
  }
@@ -1,8 +1,8 @@
1
1
  export { ContractBuilder, definePackage, PackageBuilder } from "./builder";
2
2
  export { type ContractCallAuthorizer, type ContractProviderEndpointResolver, createContractProviderRouter, type IAuthorizedContractRoute, type IContractProviderEndpointRequest, type IContractProviderRouter, type IContractProviderRouterOptions, } from "./contract-provider-router";
3
3
  export { ContractProviders, ContractProvidersTable, contractProvidersMetaData, contractProvidersSchema, type IContractProviderRecord, } from "./contract-providers.table";
4
- export { CONTRACT_CALL_RPC, type ContractMemberType, type ContractNotificationSink, type ContractPermissionCheck, createContractConsumer, createContractProvider, createContractProviderEndpoint, createContractProviderSession, type IContractCall, type IContractCallContext, type IContractConsumer, type IContractConsumerBase, type IContractNotification, type IContractProviderEndpoint, type IContractProviderEndpointOptions, type IContractProviderSession, type IContractProviderSessionOptions, type IContractResolution, type IContractTableProxy, type ITableProxy, sameUserContractPermissionCheck, } from "./contract-proxy";
5
- export { type ContractRemoteIdentity, type ContractResolver, createLocalUserContextContractProviderRouter, createUserContextContractProviderRouter, getContractResolver, handleContractCall, isSelfTrustedContractCaller, registerContractResolver, } from "./contract-resolvers";
4
+ export { CONTRACT_CALL_RPC, type ContractMemberType, type ContractNotificationSink, type ContractPermissionCheck, createContractConsumer, createContractProvider, createContractProviderEndpoint, createContractProviderSession, type IContractCall, type IContractCallContext, type IContractConsumer, type IContractConsumerBase, type IContractNotification, type IContractProviderEndpoint, type IContractProviderEndpointOptions, type IContractProviderSession, type IContractProviderSessionOptions, type IContractResolution, type IContractTableProxy, type IContractToolContext, type ITableProxy, sameUserContractPermissionCheck, } from "./contract-proxy";
5
+ export { type ContractRemoteIdentity, type ContractResolver, createLocalUserContextContractProviderRouter, createUserContextContractProviderRouter, getContractResolver, handleContractCall, type ILocalContractVariableRequest, isSelfTrustedContractCaller, type LocalContractVariableResolver, registerContractResolver, } from "./contract-resolvers";
6
6
  export { CONTRACT_NOTIFY_CHANNEL, CONTRACT_REQUEST_CHANNEL, connectionContractTransport, type IContractTransport, type IDuplexEmitter, inProcessTransportPair, } from "./contract-transport";
7
7
  export { Contracts, ContractsTable, contractsMetaData, contractsSchema, type IContractRecord, } from "./contracts.table";
8
8
  export { createLocalContractConsumer } from "./create-local-consumer";
@@ -25,7 +25,6 @@ export * from "./tools";
25
25
  export * from "./user-permissions";
26
26
  export * from "./user-trust-levels";
27
27
  export * from "./users";
28
- export * from "./voice-messages";
29
28
  export * from "./welcome-modal.pvar";
30
29
  export * from "./workflow-logs";
31
30
  export * from "./workflow-runs";
@@ -41,7 +41,6 @@ __exportStar(require("./tools"), exports);
41
41
  __exportStar(require("./user-permissions"), exports);
42
42
  __exportStar(require("./user-trust-levels"), exports);
43
43
  __exportStar(require("./users"), exports);
44
- __exportStar(require("./voice-messages"), exports);
45
44
  __exportStar(require("./welcome-modal.pvar"), exports);
46
45
  __exportStar(require("./workflow-logs"), exports);
47
46
  __exportStar(require("./workflow-runs"), exports);
@@ -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 () => { }),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@peers-app/peers-sdk",
3
- "version": "0.21.7",
3
+ "version": "0.22.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/peers-app/peers-sdk.git"
@@ -1,33 +0,0 @@
1
- import { z } from "zod";
2
- import type { DataContext } from "../context/data-context";
3
- export declare const voiceMessageSchema: z.ZodObject<{
4
- voiceMessageId: z.ZodDefault<z.ZodEffects<z.ZodString, string, string>>;
5
- sessionId: z.ZodString;
6
- role: z.ZodEnum<["user", "assistant"]>;
7
- content: z.ZodString;
8
- threadId: z.ZodOptional<z.ZodString>;
9
- createdAt: z.ZodDefault<z.ZodDate>;
10
- }, "strip", z.ZodTypeAny, {
11
- content: string;
12
- role: "user" | "assistant";
13
- sessionId: string;
14
- createdAt: Date;
15
- voiceMessageId: string;
16
- threadId?: string | undefined;
17
- }, {
18
- content: string;
19
- role: "user" | "assistant";
20
- sessionId: string;
21
- createdAt?: Date | undefined;
22
- voiceMessageId?: string | undefined;
23
- threadId?: string | undefined;
24
- }>;
25
- export type IVoiceMessage = z.infer<typeof voiceMessageSchema>;
26
- export declare function VoiceMessages(dataContext?: DataContext): import("..").Table<{
27
- content: string;
28
- role: "user" | "assistant";
29
- sessionId: string;
30
- createdAt: Date;
31
- voiceMessageId: string;
32
- threadId?: string | undefined;
33
- }>;
@@ -1,33 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.voiceMessageSchema = void 0;
4
- exports.VoiceMessages = VoiceMessages;
5
- const zod_1 = require("zod");
6
- const user_context_singleton_1 = require("../context/user-context-singleton");
7
- const zod_types_1 = require("../types/zod-types");
8
- const utils_1 = require("../utils");
9
- const table_definitions_system_1 = require("./orm/table-definitions.system");
10
- const types_1 = require("./orm/types");
11
- exports.voiceMessageSchema = zod_1.z.object({
12
- voiceMessageId: zod_types_1.zodPeerId.default(() => (0, utils_1.newid)()),
13
- sessionId: zod_1.z.string().describe("Groups messages belonging to one voice session"),
14
- role: zod_1.z.enum(["user", "assistant"]),
15
- content: zod_1.z.string(),
16
- threadId: zod_1.z
17
- .string()
18
- .optional()
19
- .describe("messageId of the channel thread created when an action was dispatched to the background agent"),
20
- createdAt: zod_1.z.date().default(() => new Date()),
21
- });
22
- const metaData = {
23
- name: "VoiceMessages",
24
- description: "Voice conversation history for the self-contained voice assistant",
25
- primaryKeyName: "voiceMessageId",
26
- fields: (0, types_1.schemaToFields)(exports.voiceMessageSchema),
27
- localOnly: true,
28
- indexes: [{ fields: ["sessionId"] }, { fields: ["sessionId", "createdAt"] }],
29
- };
30
- (0, table_definitions_system_1.registerSystemTableDefinition)(metaData, exports.voiceMessageSchema);
31
- function VoiceMessages(dataContext) {
32
- return (0, user_context_singleton_1.getTableContainer)(dataContext).getTable(metaData, exports.voiceMessageSchema);
33
- }