@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.
- package/README.md +9 -1
- package/dist/contracts/__tests__/action-dispatch.test.d.ts +1 -0
- package/dist/contracts/__tests__/action-dispatch.test.js +339 -0
- package/dist/contracts/__tests__/contract-proxy.test.js +24 -1
- package/dist/contracts/__tests__/contract-resolvers.test.js +3 -0
- package/dist/contracts/__tests__/generic-tool-dispatch.test.d.ts +1 -0
- package/dist/contracts/__tests__/generic-tool-dispatch.test.js +173 -0
- package/dist/contracts/action-dispatch.d.ts +12 -0
- package/dist/contracts/action-dispatch.js +73 -0
- package/dist/contracts/contract-provider-router.d.ts +16 -2
- package/dist/contracts/contract-provider-router.js +11 -1
- package/dist/contracts/contract-providers.table.d.ts +2 -2
- package/dist/contracts/contract-proxy.d.ts +58 -0
- package/dist/contracts/contract-proxy.js +163 -19
- package/dist/contracts/contract-resolvers.js +12 -1
- package/dist/contracts/index.d.ts +5 -1
- package/dist/contracts/index.js +21 -2
- package/dist/contracts/system/contract-catalog-contract.d.ts +40 -0
- package/dist/contracts/system/contract-catalog-contract.js +51 -0
- package/dist/contracts/system/http-contract.d.ts +80 -0
- package/dist/contracts/system/http-contract.js +93 -0
- package/dist/contracts/system/index.d.ts +3 -0
- package/dist/contracts/system/index.js +3 -0
- package/dist/contracts/system/scheduler-contract.d.ts +54 -0
- package/dist/contracts/system/scheduler-contract.js +70 -0
- package/dist/data/messages.d.ts +3 -3
- package/dist/data/package-versions.d.ts +10 -10
- package/dist/data/tool-access-approvals.d.ts +3 -3
- package/dist/data/tool-access.d.ts +3 -1
- package/dist/data/tool-access.js +5 -2
- package/dist/data/tool-access.test.js +7 -0
- package/dist/data/tools.d.ts +7 -3
- package/dist/data/tools.js +7 -1
- package/dist/invites/invite-messages.d.ts +2 -2
- package/dist/package-loader/host-authority-loading.test.d.ts +1 -0
- package/dist/package-loader/host-authority-loading.test.js +87 -0
- package/dist/package-loader/host-dispatch-policy.test.d.ts +1 -0
- package/dist/package-loader/host-dispatch-policy.test.js +56 -0
- package/dist/package-loader/index.d.ts +1 -0
- package/dist/package-loader/index.js +1 -0
- package/dist/package-loader/isolated-artifact.d.ts +53 -2
- package/dist/package-loader/isolated-artifact.js +118 -4
- package/dist/package-loader/isolated-artifact.test.js +63 -0
- package/dist/package-loader/isolated-persistent-var-gateway.js +19 -0
- package/dist/package-loader/isolated-runtime.d.ts +43 -1
- package/dist/package-loader/package-loader.d.ts +2 -0
- package/dist/package-loader/package-loader.js +64 -0
- package/dist/package-loader/package-loader.test.js +50 -2
- package/dist/package-loader/package-network-policy.d.ts +52 -0
- package/dist/package-loader/package-network-policy.js +62 -0
- package/dist/system-ids.d.ts +31 -0
- package/dist/system-ids.js +32 -1
- 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 @@
|
|
|
1
|
+
export {};
|
|
@@ -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
|
+
});
|
|
@@ -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
|
-
*
|
|
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
|
|
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, [
|
|
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
|
|
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
|
|
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
|
});
|