@peers-app/peers-sdk 0.25.17 → 0.25.19

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/README.md +9 -1
  2. package/dist/contracts/__tests__/action-dispatch.test.d.ts +1 -0
  3. package/dist/contracts/__tests__/action-dispatch.test.js +339 -0
  4. package/dist/contracts/__tests__/contract-proxy.test.js +24 -1
  5. package/dist/contracts/__tests__/contract-resolvers.test.js +3 -0
  6. package/dist/contracts/__tests__/generic-tool-dispatch.test.d.ts +1 -0
  7. package/dist/contracts/__tests__/generic-tool-dispatch.test.js +173 -0
  8. package/dist/contracts/action-dispatch.d.ts +12 -0
  9. package/dist/contracts/action-dispatch.js +73 -0
  10. package/dist/contracts/contract-provider-router.d.ts +16 -2
  11. package/dist/contracts/contract-provider-router.js +11 -1
  12. package/dist/contracts/contract-providers.table.d.ts +2 -2
  13. package/dist/contracts/contract-proxy.d.ts +58 -0
  14. package/dist/contracts/contract-proxy.js +163 -19
  15. package/dist/contracts/contract-resolvers.js +12 -1
  16. package/dist/contracts/index.d.ts +5 -1
  17. package/dist/contracts/index.js +21 -2
  18. package/dist/contracts/system/contract-catalog-contract.d.ts +40 -0
  19. package/dist/contracts/system/contract-catalog-contract.js +51 -0
  20. package/dist/contracts/system/http-contract.d.ts +80 -0
  21. package/dist/contracts/system/http-contract.js +93 -0
  22. package/dist/contracts/system/index.d.ts +3 -0
  23. package/dist/contracts/system/index.js +3 -0
  24. package/dist/contracts/system/scheduler-contract.d.ts +54 -0
  25. package/dist/contracts/system/scheduler-contract.js +70 -0
  26. package/dist/data/messages.d.ts +3 -3
  27. package/dist/data/package-versions.d.ts +10 -10
  28. package/dist/data/tool-access-approvals.d.ts +3 -3
  29. package/dist/data/tool-access.d.ts +3 -1
  30. package/dist/data/tool-access.js +5 -2
  31. package/dist/data/tool-access.test.js +7 -0
  32. package/dist/data/tools.d.ts +7 -3
  33. package/dist/data/tools.js +7 -1
  34. package/dist/invites/invite-messages.d.ts +2 -2
  35. package/dist/package-loader/host-authority-loading.test.d.ts +1 -0
  36. package/dist/package-loader/host-authority-loading.test.js +87 -0
  37. package/dist/package-loader/host-dispatch-policy.test.d.ts +1 -0
  38. package/dist/package-loader/host-dispatch-policy.test.js +56 -0
  39. package/dist/package-loader/index.d.ts +1 -0
  40. package/dist/package-loader/index.js +1 -0
  41. package/dist/package-loader/isolated-artifact.d.ts +53 -2
  42. package/dist/package-loader/isolated-artifact.js +118 -4
  43. package/dist/package-loader/isolated-artifact.test.js +63 -0
  44. package/dist/package-loader/isolated-persistent-var-gateway.js +19 -0
  45. package/dist/package-loader/isolated-runtime.d.ts +43 -1
  46. package/dist/package-loader/package-loader.d.ts +2 -0
  47. package/dist/package-loader/package-loader.js +64 -0
  48. package/dist/package-loader/package-loader.test.js +50 -2
  49. package/dist/package-loader/package-network-policy.d.ts +52 -0
  50. package/dist/package-loader/package-network-policy.js +62 -0
  51. package/dist/system-ids.d.ts +31 -0
  52. package/dist/system-ids.js +32 -1
  53. package/package.json +1 -1
@@ -1704,16 +1704,16 @@ export declare const inviteEnvelopeSchema: z.ZodObject<{
1704
1704
  fromPublicKey: string;
1705
1705
  }>;
1706
1706
  }, "strict", z.ZodTypeAny, {
1707
- v: 1;
1708
1707
  to: string;
1708
+ v: 1;
1709
1709
  box: {
1710
1710
  nonce: string;
1711
1711
  contents: string;
1712
1712
  fromPublicKey: string;
1713
1713
  };
1714
1714
  }, {
1715
- v: 1;
1716
1715
  to: string;
1716
+ v: 1;
1717
1717
  box: {
1718
1718
  nonce: string;
1719
1719
  contents: string;
@@ -0,0 +1,87 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const index_1 = require("../index");
4
+ test("host-backed package endpoints honor declared propagation and reread live metadata", async () => {
5
+ const packageId = (0, index_1.newid)();
6
+ const contractId = (0, index_1.newid)();
7
+ const userId = (0, index_1.newid)();
8
+ const groupId = (0, index_1.newid)();
9
+ const dataContext = {
10
+ dataContextId: groupId,
11
+ groupId,
12
+ tableContainer: {
13
+ getTable: (meta) => meta.name === "Groups"
14
+ ? { get: async () => ({ founderUserId: (0, index_1.newid)() }) }
15
+ : { findOne: async () => ({ userId, role: index_1.GroupMemberRole.Admin }) },
16
+ },
17
+ };
18
+ Object.assign(dataContext, {
19
+ userContext: {
20
+ userId,
21
+ deviceId: () => (0, index_1.newid)(),
22
+ loadingPromise: Promise.resolve(),
23
+ userDataContext: dataContext,
24
+ getDataContext: () => dataContext,
25
+ },
26
+ });
27
+ const loader = new index_1.PackageLoader(dataContext);
28
+ const make = (name, level, fn) => ({
29
+ tool: {
30
+ toolId: (0, index_1.newid)(),
31
+ name,
32
+ accessLevel: level,
33
+ code: "",
34
+ usageDescription: name,
35
+ inputSchema: { type: index_1.IOSchemaType.simple, fields: [] },
36
+ outputSchema: { type: index_1.IOSchemaType.simple, fields: [] },
37
+ },
38
+ toolFn: fn,
39
+ });
40
+ const route = make("route", 20, (_, ctx) => ctx.dispatchAction("act", {}));
41
+ route.tool.authorityPropagation = "preserve";
42
+ const effect = jest.fn(() => "admin target");
43
+ (0, index_1.registerContractResolver)(contractId, 1, () => ({
44
+ contractId,
45
+ version: 1,
46
+ tools: { route },
47
+ actionAliases: { act: "target" },
48
+ actionTools: { target: make("target", 60, effect) },
49
+ }), { providerPackageId: packageId });
50
+ loader.contractRegistry.register(packageId, {
51
+ contractId,
52
+ version: 1,
53
+ name: "Arbitrary",
54
+ description: "",
55
+ tools: [{ name: "route", usageDescription: "route", inputFields: [], outputFields: [] }],
56
+ tables: [],
57
+ observables: [],
58
+ events: [],
59
+ });
60
+ const endpoint = await loader.resolveContractEndpoint({
61
+ contractId,
62
+ version: 1,
63
+ providerPackageId: packageId,
64
+ context: { callerUserId: userId, dataContext },
65
+ notify: async () => undefined,
66
+ });
67
+ const call = {
68
+ contractId,
69
+ version: 1,
70
+ member: "tool",
71
+ name: "route",
72
+ operation: "run",
73
+ args: [{}],
74
+ dataContextId: groupId,
75
+ };
76
+ try {
77
+ await expect(endpoint.handleCall(call)).resolves.toBe("admin target");
78
+ // Metadata is read on every call: an ordinary Reader wrapper caps the Admin at Reader.
79
+ route.tool.authorityPropagation = "cap";
80
+ await expect(endpoint.handleCall(call)).rejects.toThrow("strict delegation");
81
+ expect(effect).toHaveBeenCalledTimes(1);
82
+ }
83
+ finally {
84
+ endpoint.dispose();
85
+ await loader.dispose();
86
+ }
87
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,56 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const index_1 = require("../index");
4
+ it("host dispatch honors preferred providers, missing-provider policy, and owner pinning", async () => {
5
+ const dataContext = { dataContextId: (0, index_1.newid)() };
6
+ const loader = new index_1.PackageLoader(dataContext);
7
+ const caller = (0, index_1.newid)();
8
+ const preferred = (0, index_1.newid)();
9
+ const alternate = (0, index_1.newid)();
10
+ const contractId = (0, index_1.newid)();
11
+ const definition = {
12
+ contractId,
13
+ version: 1,
14
+ name: "Target",
15
+ description: "",
16
+ tables: [],
17
+ events: [],
18
+ observables: [],
19
+ tools: [{ name: "target", usageDescription: "", inputFields: [], outputFields: [] }],
20
+ };
21
+ loader.contractRegistry.register(alternate, definition);
22
+ loader.contractRegistry.register(preferred, definition);
23
+ loader.hostConsumes.set(caller, [
24
+ { contractId, version: 1, providerPackageIds: [preferred], fallbackToDefaultProvider: false },
25
+ ]);
26
+ const resolve = jest
27
+ .spyOn(loader, "resolveContractEndpoint")
28
+ .mockImplementation(async (request) => ({ handleCall: async () => request.providerPackageId, dispose: () => undefined }));
29
+ const context = {
30
+ dataContext,
31
+ callerUserId: (0, index_1.newid)(),
32
+ authority: {
33
+ userId: (0, index_1.newid)(),
34
+ userAccessLevel: 60,
35
+ assistantAccessLevel: 40,
36
+ strictActionScope: true,
37
+ },
38
+ };
39
+ const dispatch = (providerPackageId) => loader.dispatchToolFromProvider(caller, { contractId, version: 1, toolName: "target", providerPackageId }, {}, context);
40
+ try {
41
+ await expect(dispatch()).resolves.toBe(preferred);
42
+ await expect(dispatch(alternate)).rejects.toThrow("Provider substitution");
43
+ loader.contractRegistry.unregister(preferred);
44
+ await expect(dispatch()).rejects.toThrow("No declared contract provider");
45
+ loader.hostConsumes.get(caller)[0].fallbackToDefaultProvider = true;
46
+ await expect(dispatch()).resolves.toBe(alternate);
47
+ loader.contractRegistry.register(caller, definition);
48
+ loader.hostConsumes.delete(caller);
49
+ await expect(dispatch(alternate)).rejects.toThrow("Provider substitution");
50
+ await expect(dispatch()).resolves.toBe(caller);
51
+ expect(resolve).toHaveBeenCalledTimes(3);
52
+ }
53
+ finally {
54
+ await loader.dispose();
55
+ }
56
+ });
@@ -4,3 +4,4 @@ export * from "./isolated-persistent-var-gateway";
4
4
  export * from "./isolated-runtime";
5
5
  export * from "./isolated-table-gateway";
6
6
  export * from "./package-loader";
7
+ export * from "./package-network-policy";
@@ -20,3 +20,4 @@ __exportStar(require("./isolated-persistent-var-gateway"), exports);
20
20
  __exportStar(require("./isolated-runtime"), exports);
21
21
  __exportStar(require("./isolated-table-gateway"), exports);
22
22
  __exportStar(require("./package-loader"), exports);
23
+ __exportStar(require("./package-network-policy"), exports);
@@ -22,7 +22,8 @@ export interface IIsolatedContractToolRef {
22
22
  *
23
23
  * Shape matches {@link IContractTool}. `suggestedAccessLevel` is the default
24
24
  * effective level until install-time approval exists and calls `approveTool`.
25
- * Omit it to keep the fail-closed Self default.
25
+ * Omitted admission is None (0) for a preservation request, otherwise fail-closed Self (100).
26
+ * Operator approval changes propagation, never admission.
26
27
  */
27
28
  export interface IIsolatedContractTool extends Omit<IContractTool, "inputFields" | "outputFields"> {
28
29
  /** Defaults to `[]` during validation when omitted. */
@@ -33,9 +34,48 @@ export interface IIsolatedContractTool extends Omit<IContractTool, "inputFields"
33
34
  * Default effective access level for this tool.
34
35
  *
35
36
  * Used until an administrator explicitly approves the tool at install time.
36
- * Omit to keep the fail-closed Self default.
37
+ * Omit for None (0) when requesting preservation; other tools default to Self (100).
37
38
  */
38
39
  suggestedAccessLevel?: number;
40
+ /** Declared outgoing authority mode; `preserve` omits this tool's own level from strict narrowing. */
41
+ authorityPropagation?: "cap" | "preserve";
42
+ /** Checked package-internal target, omitted from the public contract definition. */
43
+ private?: boolean;
44
+ /**
45
+ * Per-invoke deadline in milliseconds for this tool.
46
+ *
47
+ * When omitted, the runtime's `invokeTimeoutMs` applies. Capped at
48
+ * {@link ISOLATED_TOOL_TIMEOUT_MAX_MS}. Host calls made during the invoke,
49
+ * including System HTTP, count against this deadline.
50
+ */
51
+ timeoutMs?: number;
52
+ }
53
+ /** Longest per-tool invoke deadline an isolated manifest may declare. */
54
+ export declare const ISOLATED_TOOL_TIMEOUT_MAX_MS = 120000;
55
+ /**
56
+ * Secret the host may attach to an outbound request.
57
+ *
58
+ * The guest passes `ref`. The host reads `secretVar` and sets `header`.
59
+ */
60
+ export interface IIsolatedPackageNetworkCredential {
61
+ ref: string;
62
+ /** Logical persistent-variable name. The host prefixes it with the package id. */
63
+ secretVar: string;
64
+ /** Defaults to `user` when omitted. */
65
+ scope?: "user" | "device";
66
+ header: string;
67
+ /** Must contain the literal `{secret}`. */
68
+ format: string;
69
+ }
70
+ /**
71
+ * Outbound HTTPS this package is allowed to perform through System HTTP.
72
+ *
73
+ * Honored at install the same way `suggestedAccessLevel` is: there is no
74
+ * separate approval step yet. The Packages screen displays the declaration.
75
+ */
76
+ export interface IIsolatedPackageNetwork {
77
+ allowedHosts: string[];
78
+ credentials?: IIsolatedPackageNetworkCredential[];
39
79
  }
40
80
  /**
41
81
  * Private table schema declared by an isolated package.
@@ -101,6 +141,11 @@ export interface IIsolatedConsumedContract {
101
141
  providerPackageIds?: string[];
102
142
  fallbackToDefaultProvider?: boolean;
103
143
  }
144
+ /** Package-owned catalog selector bound to a registered canonical action. */
145
+ export interface IIsolatedActionAlias {
146
+ name: string;
147
+ target: IIsolatedContractToolRef;
148
+ }
104
149
  /**
105
150
  * Pure-data package description. Validated on the host; never executed there.
106
151
  */
@@ -111,7 +156,11 @@ export interface IIsolatedPackageManifest {
111
156
  consumes: IIsolatedConsumedContract[];
112
157
  /** Package-owned table schemas. Contract references decide public exposure. */
113
158
  tables?: IIsolatedPackageTable[];
159
+ /** Host allowlist and credential refs for System HTTP. Omit to deny outbound HTTP. */
160
+ network?: IIsolatedPackageNetwork;
114
161
  appNavs?: IAppNav[];
162
+ /** Private checked targets, independent of the frozen Voice Skill catalog. */
163
+ actionAliases?: IIsolatedActionAlias[];
115
164
  }
116
165
  /**
117
166
  * Installable isolated package: declarative manifest plus provider source
@@ -144,6 +193,8 @@ export interface IIsolatedInstalledPackage {
144
193
  tables: IIsolatedPackageTable[];
145
194
  definitions: IContractDefinition[];
146
195
  appNavs?: IAppNav[];
196
+ /** Private checked targets, independent of the frozen Voice Skill catalog. */
197
+ actionAliases?: IIsolatedActionAlias[];
147
198
  }
148
199
  /** Host-contract member guests use to request a consumed-contract tool call. */
149
200
  export declare const ISOLATED_CONSUME_HOST_MEMBER = "contract.consume";
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.ISOLATED_EVENT_HOST_MEMBER = exports.ISOLATED_PERSISTENT_VAR_HOST_MEMBER = exports.ISOLATED_TABLE_HOST_MEMBER = exports.ISOLATED_CONSUME_HOST_MEMBER = exports.ISOLATED_PACKAGE_MAGIC = void 0;
3
+ exports.ISOLATED_EVENT_HOST_MEMBER = exports.ISOLATED_PERSISTENT_VAR_HOST_MEMBER = exports.ISOLATED_TABLE_HOST_MEMBER = exports.ISOLATED_CONSUME_HOST_MEMBER = exports.ISOLATED_TOOL_TIMEOUT_MAX_MS = exports.ISOLATED_PACKAGE_MAGIC = void 0;
4
4
  exports.looksLikeIsolatedPackageArtifact = looksLikeIsolatedPackageArtifact;
5
5
  exports.validateIsolatedPackageArtifact = validateIsolatedPackageArtifact;
6
6
  exports.parseIsolatedPackageArtifact = parseIsolatedPackageArtifact;
@@ -25,6 +25,8 @@ const utils_1 = require("../utils");
25
25
  * opt-in format without evaluating source.
26
26
  */
27
27
  exports.ISOLATED_PACKAGE_MAGIC = "PEERS_ISOLATED_PACKAGE_V1";
28
+ /** Longest per-tool invoke deadline an isolated manifest may declare. */
29
+ exports.ISOLATED_TOOL_TIMEOUT_MAX_MS = 120_000;
28
30
  /** Host-contract member guests use to request a consumed-contract tool call. */
29
31
  exports.ISOLATED_CONSUME_HOST_MEMBER = "contract.consume";
30
32
  /** Host-contract member guests use to read or write a package-owned table. */
@@ -239,7 +241,10 @@ function validateProvidedContract(contract, index, packageTables) {
239
241
  "inputFields",
240
242
  "outputFields",
241
243
  "suggestedAccessLevel",
244
+ "authorityPropagation",
245
+ "private",
242
246
  "remoteAccessLevel",
247
+ "timeoutMs",
243
248
  ], toolLabel);
244
249
  if (typeof tool.name !== "string" || tool.name.trim() === "") {
245
250
  throw new Error(`${toolLabel}.name is required`);
@@ -253,6 +258,12 @@ function validateProvidedContract(contract, index, packageTables) {
253
258
  }
254
259
  tool.inputFields = validateFields(tool.inputFields, `${toolLabel}.inputFields`);
255
260
  tool.outputFields = validateFields(tool.outputFields, `${toolLabel}.outputFields`);
261
+ if (tool.authorityPropagation !== undefined &&
262
+ tool.authorityPropagation !== "cap" &&
263
+ tool.authorityPropagation !== "preserve")
264
+ throw new Error(`${toolLabel}.authorityPropagation must be cap or preserve`);
265
+ if (tool.private !== undefined && typeof tool.private !== "boolean")
266
+ throw new Error(`${toolLabel}.private must be a boolean`);
256
267
  if (tool.suggestedAccessLevel !== undefined) {
257
268
  if (!Number.isInteger(tool.suggestedAccessLevel) ||
258
269
  tool.suggestedAccessLevel < access_level_1.MIN_ACCESS_LEVEL ||
@@ -267,6 +278,13 @@ function validateProvidedContract(contract, index, packageTables) {
267
278
  throw new Error(`${toolLabel}.remoteAccessLevel must be an integer 0–100`);
268
279
  }
269
280
  }
281
+ if (tool.timeoutMs !== undefined) {
282
+ if (!Number.isInteger(tool.timeoutMs) ||
283
+ tool.timeoutMs < 1 ||
284
+ tool.timeoutMs > exports.ISOLATED_TOOL_TIMEOUT_MAX_MS) {
285
+ throw new Error(`${toolLabel}.timeoutMs must be an integer from 1 to ${exports.ISOLATED_TOOL_TIMEOUT_MAX_MS}`);
286
+ }
287
+ }
270
288
  }
271
289
  if (contract.tables === undefined) {
272
290
  contract.tables = [];
@@ -430,6 +448,72 @@ function validateConsumedContract(dep, index) {
430
448
  }
431
449
  }
432
450
  }
451
+ const NETWORK_HOST_PATTERN = /^(?=.{1,253}$)[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)*$/;
452
+ const NETWORK_REF_PATTERN = /^[A-Za-z][A-Za-z0-9_-]{0,63}$/;
453
+ const NETWORK_HEADER_PATTERN = /^[A-Za-z0-9-]{1,64}$/;
454
+ const NETWORK_SECRET_VAR_PATTERN = /^[A-Za-z][A-Za-z0-9_:-]{0,80}$/;
455
+ function validatePackageNetwork(manifest) {
456
+ if (manifest.network === undefined) {
457
+ return;
458
+ }
459
+ const network = manifest.network;
460
+ if (!network || typeof network !== "object" || Array.isArray(network)) {
461
+ throw new Error("manifest.network must be an object");
462
+ }
463
+ assertOnlyKeys(network, ["allowedHosts", "credentials"], "manifest.network");
464
+ if (!Array.isArray(network.allowedHosts) || network.allowedHosts.length === 0) {
465
+ throw new Error("manifest.network.allowedHosts must be a non-empty array");
466
+ }
467
+ const hosts = new Set();
468
+ for (const [index, host] of network.allowedHosts.entries()) {
469
+ if (typeof host !== "string" || !NETWORK_HOST_PATTERN.test(host)) {
470
+ throw new Error(`manifest.network.allowedHosts[${index}] must be a lowercase hostname with no scheme, port, or path`);
471
+ }
472
+ if (host.split(".").every((label) => /^\d+$/.test(label))) {
473
+ throw new Error(`manifest.network.allowedHosts[${index}] must be a hostname, not an IP address`);
474
+ }
475
+ if (hosts.has(host)) {
476
+ throw new Error(`manifest.network declares duplicate host "${host}"`);
477
+ }
478
+ hosts.add(host);
479
+ }
480
+ if (network.credentials === undefined) {
481
+ network.credentials = [];
482
+ }
483
+ if (!Array.isArray(network.credentials)) {
484
+ throw new Error("manifest.network.credentials must be an array");
485
+ }
486
+ const refs = new Set();
487
+ for (const [index, credential] of network.credentials.entries()) {
488
+ const label = `manifest.network.credentials[${index}]`;
489
+ if (!credential || typeof credential !== "object" || Array.isArray(credential)) {
490
+ throw new Error(`${label} must be an object`);
491
+ }
492
+ assertOnlyKeys(credential, ["ref", "secretVar", "scope", "header", "format"], label);
493
+ if (typeof credential.ref !== "string" || !NETWORK_REF_PATTERN.test(credential.ref)) {
494
+ throw new Error(`${label}.ref must be a short identifier`);
495
+ }
496
+ if (refs.has(credential.ref)) {
497
+ throw new Error(`manifest.network declares duplicate credential ref "${credential.ref}"`);
498
+ }
499
+ refs.add(credential.ref);
500
+ if (typeof credential.secretVar !== "string" ||
501
+ !NETWORK_SECRET_VAR_PATTERN.test(credential.secretVar)) {
502
+ throw new Error(`${label}.secretVar must be a logical persistent-variable name`);
503
+ }
504
+ if (credential.scope !== undefined &&
505
+ credential.scope !== "user" &&
506
+ credential.scope !== "device") {
507
+ throw new Error(`${label}.scope must be "user" or "device"`);
508
+ }
509
+ if (typeof credential.header !== "string" || !NETWORK_HEADER_PATTERN.test(credential.header)) {
510
+ throw new Error(`${label}.header must be an HTTP header name`);
511
+ }
512
+ if (typeof credential.format !== "string" || !credential.format.includes("{secret}")) {
513
+ throw new Error(`${label}.format must contain {secret}`);
514
+ }
515
+ }
516
+ }
433
517
  function validateAppNavs(appNavs) {
434
518
  if (appNavs === undefined) {
435
519
  return undefined;
@@ -494,7 +578,16 @@ function validateIsolatedPackageArtifact(artifact) {
494
578
  if (!manifest || typeof manifest !== "object" || Array.isArray(manifest)) {
495
579
  throw new Error("Package artifact.manifest is required");
496
580
  }
497
- assertOnlyKeys(manifest, ["packageId", "version", "provides", "consumes", "tables", "appNavs"], "manifest");
581
+ assertOnlyKeys(manifest, [
582
+ "packageId",
583
+ "version",
584
+ "provides",
585
+ "consumes",
586
+ "tables",
587
+ "network",
588
+ "appNavs",
589
+ "actionAliases",
590
+ ], "manifest");
498
591
  if (typeof providerSource !== "string" || providerSource.trim() === "") {
499
592
  throw new Error("Package artifact.providerSource must be a non-empty string");
500
593
  }
@@ -522,7 +615,24 @@ function validateIsolatedPackageArtifact(artifact) {
522
615
  for (const [index, dep] of manifest.consumes.entries()) {
523
616
  validateConsumedContract(dep, index);
524
617
  }
618
+ if (manifest.actionAliases !== undefined) {
619
+ if (!Array.isArray(manifest.actionAliases))
620
+ throw new Error("actionAliases must be an array");
621
+ const names = new Set();
622
+ for (const alias of manifest.actionAliases) {
623
+ if (!alias || typeof alias.name !== "string" || !alias.name || names.has(alias.name)) {
624
+ throw new Error("Invalid or duplicate action alias");
625
+ }
626
+ names.add(alias.name);
627
+ const target = alias.target;
628
+ if (!target ||
629
+ !packageProvidesTool(manifest.provides, target.contractId, target.version, target.toolName)) {
630
+ throw new Error(`Action alias ${alias.name} has no registered target`);
631
+ }
632
+ }
633
+ }
525
634
  manifest.appNavs = validateAppNavs(manifest.appNavs);
635
+ validatePackageNetwork(manifest);
526
636
  return manifest;
527
637
  }
528
638
  /**
@@ -573,12 +683,16 @@ function serializeIsolatedPackageArtifact(artifact) {
573
683
  * @param packageTables - Package-owned schemas available for contract exposure.
574
684
  */
575
685
  function isolatedContractToDefinition(contract, packageTables) {
576
- const tools = contract.tools.map((tool) => ({
686
+ const tools = contract.tools
687
+ .filter((tool) => !tool.private)
688
+ .map((tool) => ({
577
689
  name: tool.name,
578
690
  usageDescription: tool.usageDescription,
579
691
  inputFields: tool.inputFields ?? [],
580
692
  outputFields: tool.outputFields ?? [],
581
- ...(tool.remoteAccessLevel !== undefined ? { remoteAccessLevel: tool.remoteAccessLevel } : {}),
693
+ ...(tool.remoteAccessLevel !== undefined
694
+ ? { remoteAccessLevel: tool.remoteAccessLevel }
695
+ : {}),
582
696
  }));
583
697
  const tables = (contract.tables ?? []).map((tableRef) => {
584
698
  const table = findPackageTable(packageTables, tableRef.name);
@@ -40,6 +40,16 @@ function makeArtifact(overrides = {}) {
40
40
  };
41
41
  }
42
42
  describe("isolated package artifact", () => {
43
+ test("round-trips a generic authority request and rejects unknown modes", () => {
44
+ const { artifact } = makeArtifact();
45
+ const tool = artifact.manifest.provides[0].tools[0];
46
+ tool.authorityPropagation = "preserve";
47
+ expect(() => (0, isolated_artifact_1.serializeIsolatedPackageArtifact)(artifact)).not.toThrow();
48
+ expect((0, isolated_artifact_1.parseIsolatedPackageArtifact)((0, isolated_artifact_1.serializeIsolatedPackageArtifact)(artifact)).manifest.provides[0]
49
+ .tools[0]).toMatchObject({ authorityPropagation: "preserve" });
50
+ tool.authorityPropagation = "elevate";
51
+ expect(() => (0, isolated_artifact_1.validateIsolatedPackageArtifact)(artifact)).toThrow(/authorityPropagation/);
52
+ });
43
53
  test("serializes and parses a valid envelope", () => {
44
54
  const { artifact, contractId } = makeArtifact();
45
55
  const bundle = (0, isolated_artifact_1.serializeIsolatedPackageArtifact)(artifact);
@@ -62,6 +72,34 @@ describe("isolated package artifact", () => {
62
72
  const manifest = (0, isolated_artifact_1.validateIsolatedPackageArtifact)(artifact);
63
73
  expect(manifest.tables).toEqual([]);
64
74
  });
75
+ test("validates private action aliases without extending the public contract", () => {
76
+ const { artifact, contractId } = makeArtifact();
77
+ artifact.manifest.provides[0].tools.push({
78
+ name: "private-write",
79
+ usageDescription: "Internal write",
80
+ private: true,
81
+ suggestedAccessLevel: access_level_1.AccessLevel.Writer,
82
+ });
83
+ artifact.manifest.actionAliases = [
84
+ {
85
+ name: "write",
86
+ target: { contractId, version: 1, toolName: "private-write" },
87
+ },
88
+ ];
89
+ const validated = (0, isolated_artifact_1.validateIsolatedPackageArtifact)(artifact);
90
+ expect((0, isolated_artifact_1.isolatedContractToDefinition)(validated.provides[0], []).tools.map((tool) => tool.name)).toEqual(["greet"]);
91
+ expect((0, isolated_artifact_1.parseIsolatedPackageArtifact)((0, isolated_artifact_1.serializeIsolatedPackageArtifact)(artifact)).manifest
92
+ .actionAliases).toEqual(artifact.manifest.actionAliases);
93
+ artifact.manifest.actionAliases.push(artifact.manifest.actionAliases[0]);
94
+ expect(() => (0, isolated_artifact_1.validateIsolatedPackageArtifact)(artifact)).toThrow("duplicate action alias");
95
+ artifact.manifest.actionAliases = [
96
+ {
97
+ name: "write",
98
+ target: { contractId, version: 1, toolName: "missing" },
99
+ },
100
+ ];
101
+ expect(() => (0, isolated_artifact_1.validateIsolatedPackageArtifact)(artifact)).toThrow("no registered target");
102
+ });
65
103
  test("accepts a package-private table without adding it to the contract definition", () => {
66
104
  const { artifact } = makeArtifact();
67
105
  artifact.manifest.tables = [
@@ -315,4 +353,29 @@ describe("isolated package artifact", () => {
315
353
  };
316
354
  expect(() => (0, isolated_artifact_1.validateIsolatedPackageArtifact)(artifact)).toThrow(/JSON-compatible/);
317
355
  });
356
+ test("accepts a network allowlist and rejects a host with a scheme", () => {
357
+ const { artifact } = makeArtifact();
358
+ artifact.manifest.network = {
359
+ allowedHosts: ["api.openai.com"],
360
+ credentials: [
361
+ {
362
+ ref: "openai",
363
+ secretVar: "openaiApiKey",
364
+ header: "Authorization",
365
+ format: "Bearer {secret}",
366
+ },
367
+ ],
368
+ };
369
+ const manifest = (0, isolated_artifact_1.validateIsolatedPackageArtifact)(artifact);
370
+ expect(manifest.network?.allowedHosts).toEqual(["api.openai.com"]);
371
+ artifact.manifest.network = { allowedHosts: ["https://api.openai.com"] };
372
+ expect(() => (0, isolated_artifact_1.validateIsolatedPackageArtifact)(artifact)).toThrow(/lowercase hostname/);
373
+ artifact.manifest.network = { allowedHosts: ["127.0.0.1"] };
374
+ expect(() => (0, isolated_artifact_1.validateIsolatedPackageArtifact)(artifact)).toThrow(/hostname, not an IP address/);
375
+ });
376
+ test("rejects a tool timeout above the cap", () => {
377
+ const { artifact } = makeArtifact();
378
+ artifact.manifest.provides[0].tools[0].timeoutMs = 120_001;
379
+ expect(() => (0, isolated_artifact_1.validateIsolatedPackageArtifact)(artifact)).toThrow(/timeoutMs/);
380
+ });
318
381
  });
@@ -7,6 +7,19 @@ const isolated_value_1 = require("./isolated-value");
7
7
  function memoryKey(ref) {
8
8
  return `${ref.packageId}:${ref.scope}:${ref.name}`;
9
9
  }
10
+ async function assertPackagePersistentVarIsNotSecret(dataContext, ref) {
11
+ const userContext = dataContext.userContext;
12
+ if (!userContext) {
13
+ throw new Error("Package persistent variables require a signed-in user");
14
+ }
15
+ const storageContext = ref.scope === "group" ? dataContext : userContext.userDataContext;
16
+ const storageName = (0, persistent_var_identity_1.packageScopedPersistentVarStorageName)(ref.packageId, ref.name, ref.scope, dataContext.dataContextId);
17
+ const { PersistentVars } = await Promise.resolve().then(() => require("../data/persistent-vars"));
18
+ const record = await PersistentVars(storageContext).findOne({ name: storageName });
19
+ if (record?.isSecret) {
20
+ throw new Error("Secret persistent variables are not readable by packages");
21
+ }
22
+ }
10
23
  async function resolvePersistentVar(dataContext, ref) {
11
24
  const { deviceVar, groupDeviceVar, groupUserVar, groupVar, userVar } = await Promise.resolve().then(() => require("../data/persistent-vars"));
12
25
  const name = (0, persistent_var_identity_1.packageScopedPersistentVarName)(ref.packageId, ref.name);
@@ -77,8 +90,10 @@ function createMemoryIsolatedPackagePersistentVarGateway() {
77
90
  function createIsolatedPackagePersistentVarGateway(dataContext) {
78
91
  return {
79
92
  async get(ref) {
93
+ await assertPackagePersistentVarIsNotSecret(dataContext, ref);
80
94
  const persistentVar = await resolvePersistentVar(dataContext, ref);
81
95
  await persistentVar.loadingPromise;
96
+ await assertPackagePersistentVarIsNotSecret(dataContext, ref);
82
97
  const value = persistentVar();
83
98
  return value === undefined ? undefined : (0, isolated_value_1.copyIsolatedValue)(value);
84
99
  },
@@ -86,17 +101,21 @@ function createIsolatedPackagePersistentVarGateway(dataContext) {
86
101
  if (value === undefined) {
87
102
  throw new Error("Persistent variable value must be copyable");
88
103
  }
104
+ await assertPackagePersistentVarIsNotSecret(dataContext, ref);
89
105
  const persistentVar = await resolvePersistentVar(dataContext, ref);
90
106
  const copy = (0, isolated_value_1.copyIsolatedValue)(value);
91
107
  await persistentVar.loadingPromise;
108
+ await assertPackagePersistentVarIsNotSecret(dataContext, ref);
92
109
  persistentVar(copy);
93
110
  await persistentVar.loadingPromise;
94
111
  const saved = persistentVar();
95
112
  return saved === undefined ? undefined : (0, isolated_value_1.copyIsolatedValue)(saved);
96
113
  },
97
114
  async subscribe(ref, handler) {
115
+ await assertPackagePersistentVarIsNotSecret(dataContext, ref);
98
116
  const persistentVar = await resolvePersistentVar(dataContext, ref);
99
117
  await persistentVar.loadingPromise;
118
+ await assertPackagePersistentVarIsNotSecret(dataContext, ref);
100
119
  const subscription = persistentVar.subscribe((value) => {
101
120
  handler((0, isolated_value_1.copyIsolatedValue)(value));
102
121
  });