@peers-app/peers-sdk 0.25.16 → 0.25.18
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/dist/contracts/__tests__/contract-proxy.test.js +24 -1
- package/dist/contracts/__tests__/contract-resolvers.test.js +3 -0
- package/dist/contracts/contract-provider-router.d.ts +16 -2
- package/dist/contracts/contract-provider-router.js +4 -0
- package/dist/contracts/contract-providers.table.d.ts +2 -2
- package/dist/contracts/contract-proxy.d.ts +11 -0
- package/dist/contracts/contract-proxy.js +11 -3
- package/dist/contracts/contract-resolvers.js +12 -1
- package/dist/contracts/index.d.ts +3 -0
- package/dist/contracts/index.js +18 -2
- package/dist/contracts/system/contract-catalog-contract.d.ts +38 -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/tool-access-approvals.d.ts +3 -3
- 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 +37 -0
- package/dist/package-loader/isolated-artifact.js +79 -2
- package/dist/package-loader/isolated-artifact.test.js +25 -0
- package/dist/package-loader/isolated-persistent-var-gateway.js +19 -0
- package/dist/package-loader/isolated-runtime.d.ts +28 -1
- package/dist/package-loader/package-loader.js +4 -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
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { IContractConsumerBase } from "../contract-proxy";
|
|
2
|
+
import type { IContractDefinition } from "../types";
|
|
3
|
+
/** Version of the built-in System Scheduler contract. */
|
|
4
|
+
export declare const SCHEDULER_CONTRACT_VERSION = 1;
|
|
5
|
+
/** Package id of the System Scheduler provider. */
|
|
6
|
+
export declare const schedulerProviderPackageId = "000peers0system0000000001";
|
|
7
|
+
/** Furthest a schedule may sit in the future. */
|
|
8
|
+
export declare const SCHEDULER_MAX_DELAY_MS: number;
|
|
9
|
+
/** Input for scheduling a wake-up of the caller's own tool. */
|
|
10
|
+
export interface IScheduleInput {
|
|
11
|
+
/** Unix epoch milliseconds when the tool should run. */
|
|
12
|
+
at: number;
|
|
13
|
+
contractId: string;
|
|
14
|
+
version: number;
|
|
15
|
+
toolName: string;
|
|
16
|
+
payload?: unknown;
|
|
17
|
+
}
|
|
18
|
+
/** One device-local schedule owned by a package. */
|
|
19
|
+
export interface IScheduleInfo {
|
|
20
|
+
scheduleId: string;
|
|
21
|
+
packageId: string;
|
|
22
|
+
dataContextId: string;
|
|
23
|
+
contractId: string;
|
|
24
|
+
version: number;
|
|
25
|
+
toolName: string;
|
|
26
|
+
at: number;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Consumer shape for System Scheduler.
|
|
30
|
+
*
|
|
31
|
+
* Schedules are stored on this device only. Another device does not run them.
|
|
32
|
+
*/
|
|
33
|
+
export interface ISchedulerConsumer extends IContractConsumerBase {
|
|
34
|
+
tools: {
|
|
35
|
+
schedule(input: IScheduleInput): Promise<{
|
|
36
|
+
scheduleId: string;
|
|
37
|
+
}>;
|
|
38
|
+
cancel(input: {
|
|
39
|
+
scheduleId: string;
|
|
40
|
+
}): Promise<{
|
|
41
|
+
cancelled: boolean;
|
|
42
|
+
}>;
|
|
43
|
+
list(): Promise<{
|
|
44
|
+
schedules: IScheduleInfo[];
|
|
45
|
+
}>;
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Return the local-only System Scheduler contract shape.
|
|
50
|
+
*
|
|
51
|
+
* A package may schedule a tool that it provides. When `at` arrives, the host
|
|
52
|
+
* starts that package's worker if needed and runs the tool as Self.
|
|
53
|
+
*/
|
|
54
|
+
export declare function getSchedulerContractDefinition(): IContractDefinition;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SCHEDULER_MAX_DELAY_MS = exports.schedulerProviderPackageId = exports.SCHEDULER_CONTRACT_VERSION = void 0;
|
|
4
|
+
exports.getSchedulerContractDefinition = getSchedulerContractDefinition;
|
|
5
|
+
const system_ids_1 = require("../../system-ids");
|
|
6
|
+
const field_type_1 = require("../../types/field-type");
|
|
7
|
+
const system_contracts_1 = require("../system-contracts");
|
|
8
|
+
/** Version of the built-in System Scheduler contract. */
|
|
9
|
+
exports.SCHEDULER_CONTRACT_VERSION = 1;
|
|
10
|
+
/** Package id of the System Scheduler provider. */
|
|
11
|
+
exports.schedulerProviderPackageId = system_ids_1.peersSystemPackageId;
|
|
12
|
+
/** Furthest a schedule may sit in the future. */
|
|
13
|
+
exports.SCHEDULER_MAX_DELAY_MS = 366 * 24 * 60 * 60 * 1000;
|
|
14
|
+
const field = (name, type, description, opts = {}) => ({ name, type, description, ...opts });
|
|
15
|
+
let definition;
|
|
16
|
+
/**
|
|
17
|
+
* Return the local-only System Scheduler contract shape.
|
|
18
|
+
*
|
|
19
|
+
* A package may schedule a tool that it provides. When `at` arrives, the host
|
|
20
|
+
* starts that package's worker if needed and runs the tool as Self.
|
|
21
|
+
*/
|
|
22
|
+
function getSchedulerContractDefinition() {
|
|
23
|
+
definition ??= {
|
|
24
|
+
contractId: system_ids_1.schedulerContractId,
|
|
25
|
+
version: exports.SCHEDULER_CONTRACT_VERSION,
|
|
26
|
+
devTag: "dev",
|
|
27
|
+
name: "System Scheduler",
|
|
28
|
+
description: "Device-local schedules that wake a package's own tool. Schedules do not sync to other devices.",
|
|
29
|
+
tables: [],
|
|
30
|
+
tools: [
|
|
31
|
+
{
|
|
32
|
+
name: "schedule",
|
|
33
|
+
usageDescription: "Run one of this package's tools at a future time on this device. Returns a scheduleId.",
|
|
34
|
+
inputFields: [
|
|
35
|
+
field("at", field_type_1.FieldType.number, "Unix epoch milliseconds when the tool should run."),
|
|
36
|
+
field("contractId", field_type_1.FieldType.id, "Contract this package provides."),
|
|
37
|
+
field("version", field_type_1.FieldType.number, "Contract version."),
|
|
38
|
+
field("toolName", field_type_1.FieldType.string, "Tool on that contract to run."),
|
|
39
|
+
field("payload", field_type_1.FieldType.any, "JSON value passed to the tool.", { optional: true }),
|
|
40
|
+
],
|
|
41
|
+
outputFields: [field("scheduleId", field_type_1.FieldType.id, "Id to pass to cancel.")],
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
name: "cancel",
|
|
45
|
+
usageDescription: "Cancel a schedule this package created.",
|
|
46
|
+
inputFields: [field("scheduleId", field_type_1.FieldType.id, "Schedule to cancel.")],
|
|
47
|
+
outputFields: [
|
|
48
|
+
field("cancelled", field_type_1.FieldType.boolean, "True when the schedule existed and was removed."),
|
|
49
|
+
],
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
name: "list",
|
|
53
|
+
usageDescription: "List schedules owned by the calling package on this device.",
|
|
54
|
+
inputFields: [],
|
|
55
|
+
outputFields: [
|
|
56
|
+
field("schedules", field_type_1.FieldType.object, "Schedules with scheduleId, contractId, version, toolName, and at.", { isArray: true }),
|
|
57
|
+
],
|
|
58
|
+
},
|
|
59
|
+
],
|
|
60
|
+
observables: [],
|
|
61
|
+
events: [],
|
|
62
|
+
};
|
|
63
|
+
return definition;
|
|
64
|
+
}
|
|
65
|
+
(0, system_contracts_1.registerSystemContract)({
|
|
66
|
+
providerPackageId: exports.schedulerProviderPackageId,
|
|
67
|
+
contractId: system_ids_1.schedulerContractId,
|
|
68
|
+
version: exports.SCHEDULER_CONTRACT_VERSION,
|
|
69
|
+
getDefinition: getSchedulerContractDefinition,
|
|
70
|
+
});
|
|
@@ -52,8 +52,8 @@ export declare const toolAccessApprovalSchema: z.ZodObject<{
|
|
|
52
52
|
dataContextId: string;
|
|
53
53
|
createdAt: Date;
|
|
54
54
|
expiresAt: Date;
|
|
55
|
-
toolAccessApprovalId: string;
|
|
56
55
|
toolName: string;
|
|
56
|
+
toolAccessApprovalId: string;
|
|
57
57
|
argsHash: string;
|
|
58
58
|
argsPreview: string;
|
|
59
59
|
frozenArgs: {} & {
|
|
@@ -73,8 +73,8 @@ export declare const toolAccessApprovalSchema: z.ZodObject<{
|
|
|
73
73
|
toolId: string;
|
|
74
74
|
dataContextId: string;
|
|
75
75
|
expiresAt: Date;
|
|
76
|
-
toolAccessApprovalId: string;
|
|
77
76
|
toolName: string;
|
|
77
|
+
toolAccessApprovalId: string;
|
|
78
78
|
argsHash: string;
|
|
79
79
|
frozenArgs: {} & {
|
|
80
80
|
[k: string]: any;
|
|
@@ -108,8 +108,8 @@ export declare function ToolAccessApprovals(dataContext?: DataContext): import("
|
|
|
108
108
|
dataContextId: string;
|
|
109
109
|
createdAt: Date;
|
|
110
110
|
expiresAt: Date;
|
|
111
|
-
toolAccessApprovalId: string;
|
|
112
111
|
toolName: string;
|
|
112
|
+
toolAccessApprovalId: string;
|
|
113
113
|
argsHash: string;
|
|
114
114
|
argsPreview: string;
|
|
115
115
|
frozenArgs: {} & {
|
|
@@ -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);
|
|
@@ -36,6 +36,41 @@ export interface IIsolatedContractTool extends Omit<IContractTool, "inputFields"
|
|
|
36
36
|
* Omit to keep the fail-closed Self default.
|
|
37
37
|
*/
|
|
38
38
|
suggestedAccessLevel?: number;
|
|
39
|
+
/**
|
|
40
|
+
* Per-invoke deadline in milliseconds for this tool.
|
|
41
|
+
*
|
|
42
|
+
* When omitted, the runtime's `invokeTimeoutMs` applies. Capped at
|
|
43
|
+
* {@link ISOLATED_TOOL_TIMEOUT_MAX_MS}. Host calls made during the invoke,
|
|
44
|
+
* including System HTTP, count against this deadline.
|
|
45
|
+
*/
|
|
46
|
+
timeoutMs?: number;
|
|
47
|
+
}
|
|
48
|
+
/** Longest per-tool invoke deadline an isolated manifest may declare. */
|
|
49
|
+
export declare const ISOLATED_TOOL_TIMEOUT_MAX_MS = 120000;
|
|
50
|
+
/**
|
|
51
|
+
* Secret the host may attach to an outbound request.
|
|
52
|
+
*
|
|
53
|
+
* The guest passes `ref`. The host reads `secretVar` and sets `header`.
|
|
54
|
+
*/
|
|
55
|
+
export interface IIsolatedPackageNetworkCredential {
|
|
56
|
+
ref: string;
|
|
57
|
+
/** Logical persistent-variable name. The host prefixes it with the package id. */
|
|
58
|
+
secretVar: string;
|
|
59
|
+
/** Defaults to `user` when omitted. */
|
|
60
|
+
scope?: "user" | "device";
|
|
61
|
+
header: string;
|
|
62
|
+
/** Must contain the literal `{secret}`. */
|
|
63
|
+
format: string;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Outbound HTTPS this package is allowed to perform through System HTTP.
|
|
67
|
+
*
|
|
68
|
+
* Honored at install the same way `suggestedAccessLevel` is: there is no
|
|
69
|
+
* separate approval step yet. The Packages screen displays the declaration.
|
|
70
|
+
*/
|
|
71
|
+
export interface IIsolatedPackageNetwork {
|
|
72
|
+
allowedHosts: string[];
|
|
73
|
+
credentials?: IIsolatedPackageNetworkCredential[];
|
|
39
74
|
}
|
|
40
75
|
/**
|
|
41
76
|
* Private table schema declared by an isolated package.
|
|
@@ -111,6 +146,8 @@ export interface IIsolatedPackageManifest {
|
|
|
111
146
|
consumes: IIsolatedConsumedContract[];
|
|
112
147
|
/** Package-owned table schemas. Contract references decide public exposure. */
|
|
113
148
|
tables?: IIsolatedPackageTable[];
|
|
149
|
+
/** Host allowlist and credential refs for System HTTP. Omit to deny outbound HTTP. */
|
|
150
|
+
network?: IIsolatedPackageNetwork;
|
|
114
151
|
appNavs?: IAppNav[];
|
|
115
152
|
}
|
|
116
153
|
/**
|
|
@@ -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. */
|
|
@@ -240,6 +242,7 @@ function validateProvidedContract(contract, index, packageTables) {
|
|
|
240
242
|
"outputFields",
|
|
241
243
|
"suggestedAccessLevel",
|
|
242
244
|
"remoteAccessLevel",
|
|
245
|
+
"timeoutMs",
|
|
243
246
|
], toolLabel);
|
|
244
247
|
if (typeof tool.name !== "string" || tool.name.trim() === "") {
|
|
245
248
|
throw new Error(`${toolLabel}.name is required`);
|
|
@@ -267,6 +270,13 @@ function validateProvidedContract(contract, index, packageTables) {
|
|
|
267
270
|
throw new Error(`${toolLabel}.remoteAccessLevel must be an integer 0–100`);
|
|
268
271
|
}
|
|
269
272
|
}
|
|
273
|
+
if (tool.timeoutMs !== undefined) {
|
|
274
|
+
if (!Number.isInteger(tool.timeoutMs) ||
|
|
275
|
+
tool.timeoutMs < 1 ||
|
|
276
|
+
tool.timeoutMs > exports.ISOLATED_TOOL_TIMEOUT_MAX_MS) {
|
|
277
|
+
throw new Error(`${toolLabel}.timeoutMs must be an integer from 1 to ${exports.ISOLATED_TOOL_TIMEOUT_MAX_MS}`);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
270
280
|
}
|
|
271
281
|
if (contract.tables === undefined) {
|
|
272
282
|
contract.tables = [];
|
|
@@ -430,6 +440,72 @@ function validateConsumedContract(dep, index) {
|
|
|
430
440
|
}
|
|
431
441
|
}
|
|
432
442
|
}
|
|
443
|
+
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])?)*$/;
|
|
444
|
+
const NETWORK_REF_PATTERN = /^[A-Za-z][A-Za-z0-9_-]{0,63}$/;
|
|
445
|
+
const NETWORK_HEADER_PATTERN = /^[A-Za-z0-9-]{1,64}$/;
|
|
446
|
+
const NETWORK_SECRET_VAR_PATTERN = /^[A-Za-z][A-Za-z0-9_:-]{0,80}$/;
|
|
447
|
+
function validatePackageNetwork(manifest) {
|
|
448
|
+
if (manifest.network === undefined) {
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
const network = manifest.network;
|
|
452
|
+
if (!network || typeof network !== "object" || Array.isArray(network)) {
|
|
453
|
+
throw new Error("manifest.network must be an object");
|
|
454
|
+
}
|
|
455
|
+
assertOnlyKeys(network, ["allowedHosts", "credentials"], "manifest.network");
|
|
456
|
+
if (!Array.isArray(network.allowedHosts) || network.allowedHosts.length === 0) {
|
|
457
|
+
throw new Error("manifest.network.allowedHosts must be a non-empty array");
|
|
458
|
+
}
|
|
459
|
+
const hosts = new Set();
|
|
460
|
+
for (const [index, host] of network.allowedHosts.entries()) {
|
|
461
|
+
if (typeof host !== "string" || !NETWORK_HOST_PATTERN.test(host)) {
|
|
462
|
+
throw new Error(`manifest.network.allowedHosts[${index}] must be a lowercase hostname with no scheme, port, or path`);
|
|
463
|
+
}
|
|
464
|
+
if (host.split(".").every((label) => /^\d+$/.test(label))) {
|
|
465
|
+
throw new Error(`manifest.network.allowedHosts[${index}] must be a hostname, not an IP address`);
|
|
466
|
+
}
|
|
467
|
+
if (hosts.has(host)) {
|
|
468
|
+
throw new Error(`manifest.network declares duplicate host "${host}"`);
|
|
469
|
+
}
|
|
470
|
+
hosts.add(host);
|
|
471
|
+
}
|
|
472
|
+
if (network.credentials === undefined) {
|
|
473
|
+
network.credentials = [];
|
|
474
|
+
}
|
|
475
|
+
if (!Array.isArray(network.credentials)) {
|
|
476
|
+
throw new Error("manifest.network.credentials must be an array");
|
|
477
|
+
}
|
|
478
|
+
const refs = new Set();
|
|
479
|
+
for (const [index, credential] of network.credentials.entries()) {
|
|
480
|
+
const label = `manifest.network.credentials[${index}]`;
|
|
481
|
+
if (!credential || typeof credential !== "object" || Array.isArray(credential)) {
|
|
482
|
+
throw new Error(`${label} must be an object`);
|
|
483
|
+
}
|
|
484
|
+
assertOnlyKeys(credential, ["ref", "secretVar", "scope", "header", "format"], label);
|
|
485
|
+
if (typeof credential.ref !== "string" || !NETWORK_REF_PATTERN.test(credential.ref)) {
|
|
486
|
+
throw new Error(`${label}.ref must be a short identifier`);
|
|
487
|
+
}
|
|
488
|
+
if (refs.has(credential.ref)) {
|
|
489
|
+
throw new Error(`manifest.network declares duplicate credential ref "${credential.ref}"`);
|
|
490
|
+
}
|
|
491
|
+
refs.add(credential.ref);
|
|
492
|
+
if (typeof credential.secretVar !== "string" ||
|
|
493
|
+
!NETWORK_SECRET_VAR_PATTERN.test(credential.secretVar)) {
|
|
494
|
+
throw new Error(`${label}.secretVar must be a logical persistent-variable name`);
|
|
495
|
+
}
|
|
496
|
+
if (credential.scope !== undefined &&
|
|
497
|
+
credential.scope !== "user" &&
|
|
498
|
+
credential.scope !== "device") {
|
|
499
|
+
throw new Error(`${label}.scope must be "user" or "device"`);
|
|
500
|
+
}
|
|
501
|
+
if (typeof credential.header !== "string" || !NETWORK_HEADER_PATTERN.test(credential.header)) {
|
|
502
|
+
throw new Error(`${label}.header must be an HTTP header name`);
|
|
503
|
+
}
|
|
504
|
+
if (typeof credential.format !== "string" || !credential.format.includes("{secret}")) {
|
|
505
|
+
throw new Error(`${label}.format must contain {secret}`);
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
}
|
|
433
509
|
function validateAppNavs(appNavs) {
|
|
434
510
|
if (appNavs === undefined) {
|
|
435
511
|
return undefined;
|
|
@@ -494,7 +570,7 @@ function validateIsolatedPackageArtifact(artifact) {
|
|
|
494
570
|
if (!manifest || typeof manifest !== "object" || Array.isArray(manifest)) {
|
|
495
571
|
throw new Error("Package artifact.manifest is required");
|
|
496
572
|
}
|
|
497
|
-
assertOnlyKeys(manifest, ["packageId", "version", "provides", "consumes", "tables", "appNavs"], "manifest");
|
|
573
|
+
assertOnlyKeys(manifest, ["packageId", "version", "provides", "consumes", "tables", "network", "appNavs"], "manifest");
|
|
498
574
|
if (typeof providerSource !== "string" || providerSource.trim() === "") {
|
|
499
575
|
throw new Error("Package artifact.providerSource must be a non-empty string");
|
|
500
576
|
}
|
|
@@ -523,6 +599,7 @@ function validateIsolatedPackageArtifact(artifact) {
|
|
|
523
599
|
validateConsumedContract(dep, index);
|
|
524
600
|
}
|
|
525
601
|
manifest.appNavs = validateAppNavs(manifest.appNavs);
|
|
602
|
+
validatePackageNetwork(manifest);
|
|
526
603
|
return manifest;
|
|
527
604
|
}
|
|
528
605
|
/**
|
|
@@ -315,4 +315,29 @@ describe("isolated package artifact", () => {
|
|
|
315
315
|
};
|
|
316
316
|
expect(() => (0, isolated_artifact_1.validateIsolatedPackageArtifact)(artifact)).toThrow(/JSON-compatible/);
|
|
317
317
|
});
|
|
318
|
+
test("accepts a network allowlist and rejects a host with a scheme", () => {
|
|
319
|
+
const { artifact } = makeArtifact();
|
|
320
|
+
artifact.manifest.network = {
|
|
321
|
+
allowedHosts: ["api.openai.com"],
|
|
322
|
+
credentials: [
|
|
323
|
+
{
|
|
324
|
+
ref: "openai",
|
|
325
|
+
secretVar: "openaiApiKey",
|
|
326
|
+
header: "Authorization",
|
|
327
|
+
format: "Bearer {secret}",
|
|
328
|
+
},
|
|
329
|
+
],
|
|
330
|
+
};
|
|
331
|
+
const manifest = (0, isolated_artifact_1.validateIsolatedPackageArtifact)(artifact);
|
|
332
|
+
expect(manifest.network?.allowedHosts).toEqual(["api.openai.com"]);
|
|
333
|
+
artifact.manifest.network = { allowedHosts: ["https://api.openai.com"] };
|
|
334
|
+
expect(() => (0, isolated_artifact_1.validateIsolatedPackageArtifact)(artifact)).toThrow(/lowercase hostname/);
|
|
335
|
+
artifact.manifest.network = { allowedHosts: ["127.0.0.1"] };
|
|
336
|
+
expect(() => (0, isolated_artifact_1.validateIsolatedPackageArtifact)(artifact)).toThrow(/hostname, not an IP address/);
|
|
337
|
+
});
|
|
338
|
+
test("rejects a tool timeout above the cap", () => {
|
|
339
|
+
const { artifact } = makeArtifact();
|
|
340
|
+
artifact.manifest.provides[0].tools[0].timeoutMs = 120_001;
|
|
341
|
+
expect(() => (0, isolated_artifact_1.validateIsolatedPackageArtifact)(artifact)).toThrow(/timeoutMs/);
|
|
342
|
+
});
|
|
318
343
|
});
|
|
@@ -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
|
});
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
+
import type { DataContext } from "../context/data-context";
|
|
1
2
|
import type { IContractProviderEndpointRequest } from "../contracts/contract-provider-router";
|
|
2
3
|
import type { IContractCall, IContractProviderEndpoint } from "../contracts/contract-proxy";
|
|
3
4
|
import type { ContractRegistry } from "../contracts/registry";
|
|
4
5
|
import type { PersistentVarScope } from "../data/persistent-vars";
|
|
5
6
|
import type { IField } from "../types/field-type";
|
|
6
|
-
import type { IIsolatedInstallResult, IIsolatedPackageArtifact } from "./isolated-artifact";
|
|
7
|
+
import type { IIsolatedContractToolRef, IIsolatedInstallResult, IIsolatedPackageArtifact } from "./isolated-artifact";
|
|
7
8
|
/** Standard table methods exposed through isolated contract-table references. */
|
|
8
9
|
export declare const ISOLATED_CONTRACT_TABLE_OPERATIONS: readonly ["get", "list", "count", "findOne", "save", "insert", "update", "delete"];
|
|
9
10
|
/** Consumer-callable operation on an isolated contract table. */
|
|
@@ -73,6 +74,18 @@ export interface IIsolatedPackageRuntime {
|
|
|
73
74
|
provides(contractId: string, version: number): boolean;
|
|
74
75
|
/** True when this runtime has installed `packageId`. */
|
|
75
76
|
isInstalled(packageId: string): boolean;
|
|
77
|
+
/**
|
|
78
|
+
* Run a tool the package provides, starting its worker if needed.
|
|
79
|
+
*
|
|
80
|
+
* Used by System Scheduler. The principal defaults to Self for compatibility,
|
|
81
|
+
* but schedulers should pass the current user's trusted access level.
|
|
82
|
+
*
|
|
83
|
+
* @param packageId - Installed package that declares the tool.
|
|
84
|
+
* @param ref - Contract tool to invoke.
|
|
85
|
+
* @param args - JSON value passed as the tool argument.
|
|
86
|
+
* @param principal - Trusted caller authority. Defaults to Self when omitted.
|
|
87
|
+
*/
|
|
88
|
+
invokeOwnedTool?(packageId: string, ref: IIsolatedContractToolRef, args: unknown, principal?: IIsolatedCallPrincipal): Promise<unknown>;
|
|
76
89
|
/** Terminate every worker and drop installed state. */
|
|
77
90
|
dispose(): Promise<void>;
|
|
78
91
|
}
|
|
@@ -199,12 +212,26 @@ export interface IIsolatedContractProviderGateway {
|
|
|
199
212
|
*/
|
|
200
213
|
resolveEndpoint(request: IIsolatedContractProviderEndpointRequest): Promise<IContractProviderEndpoint | undefined>;
|
|
201
214
|
}
|
|
215
|
+
/**
|
|
216
|
+
* Resolve the signed-in local user for a runtime's owning data context.
|
|
217
|
+
*
|
|
218
|
+
* Invoked only by the host for guest-originated gateway calls. The user id and
|
|
219
|
+
* data context must come from authenticated host state, never guest payloads.
|
|
220
|
+
* Return `undefined` when no signed-in identity is available; protected tools
|
|
221
|
+
* then retain their existing missing-caller-identity denial.
|
|
222
|
+
*/
|
|
223
|
+
export type IsolatedLocalUserContextResolver = () => {
|
|
224
|
+
userId: string;
|
|
225
|
+
dataContext: DataContext;
|
|
226
|
+
} | undefined;
|
|
202
227
|
/** Options passed to a registered {@link IsolatedPackageRuntimeFactory}. */
|
|
203
228
|
export interface IIsolatedPackageRuntimeFactoryOptions {
|
|
204
229
|
/** Shared per-data-context contract registry from {@link PackageLoader}. */
|
|
205
230
|
registry: ContractRegistry;
|
|
206
231
|
/** Owning data-context id (personal user id or group id). */
|
|
207
232
|
dataContextId: string;
|
|
233
|
+
/** Trusted local identity and role-lookup context for guest-to-host calls. */
|
|
234
|
+
getLocalUserContext?: IsolatedLocalUserContextResolver;
|
|
208
235
|
/**
|
|
209
236
|
* Host-owned table capability for this data context. Required when an
|
|
210
237
|
* isolated package declares tables.
|
|
@@ -74,6 +74,10 @@ class PackageLoader {
|
|
|
74
74
|
this.isolatedRuntime = factory({
|
|
75
75
|
registry: this.contractRegistry,
|
|
76
76
|
dataContextId: dataContext.dataContextId,
|
|
77
|
+
getLocalUserContext: () => {
|
|
78
|
+
const userId = this.dataContext.userContext?.userId;
|
|
79
|
+
return userId ? { userId, dataContext: this.dataContext } : undefined;
|
|
80
|
+
},
|
|
77
81
|
tableGateway: dataContext.tableContainer
|
|
78
82
|
? (0, isolated_table_gateway_1.createIsolatedPackageTableGateway)(dataContext)
|
|
79
83
|
: undefined,
|
|
@@ -12,6 +12,7 @@ jest.mock("../data/package-versions", () => ({
|
|
|
12
12
|
}));
|
|
13
13
|
const contract_resolvers_1 = require("../contracts/contract-resolvers");
|
|
14
14
|
const registry_1 = require("../contracts/registry");
|
|
15
|
+
const access_level_1 = require("../data/access-level");
|
|
15
16
|
const system_ids_1 = require("../system-ids");
|
|
16
17
|
const utils_1 = require("../utils");
|
|
17
18
|
const isolated_artifact_1 = require("./isolated-artifact");
|
|
@@ -142,6 +143,46 @@ describe("PackageLoader.contractRegistry", () => {
|
|
|
142
143
|
expect(resolver).toHaveBeenCalledWith({ dataContext });
|
|
143
144
|
endpoint.dispose();
|
|
144
145
|
});
|
|
146
|
+
it("does not supply local identity to ordinary callers that omit it", async () => {
|
|
147
|
+
const userId = (0, utils_1.newid)();
|
|
148
|
+
const dataContext = {
|
|
149
|
+
dataContextId: userId,
|
|
150
|
+
userContext: { userId, deviceId: () => "local-device" },
|
|
151
|
+
};
|
|
152
|
+
const loader = new package_loader_1.PackageLoader(dataContext);
|
|
153
|
+
const contractId = (0, utils_1.newid)();
|
|
154
|
+
const write = jest.fn();
|
|
155
|
+
(0, contract_resolvers_1.registerContractResolver)(contractId, 1, () => ({
|
|
156
|
+
contractId,
|
|
157
|
+
version: 1,
|
|
158
|
+
tools: {
|
|
159
|
+
invoke: {
|
|
160
|
+
tool: { toolId: (0, utils_1.newid)(), name: "invoke", accessLevel: access_level_1.AccessLevel.Writer },
|
|
161
|
+
toolFn: write,
|
|
162
|
+
},
|
|
163
|
+
},
|
|
164
|
+
}));
|
|
165
|
+
const endpoint = await loader.resolveContractEndpoint({
|
|
166
|
+
contractId,
|
|
167
|
+
version: 1,
|
|
168
|
+
context: {},
|
|
169
|
+
notify: async () => undefined,
|
|
170
|
+
});
|
|
171
|
+
try {
|
|
172
|
+
await expect(endpoint.handleCall({
|
|
173
|
+
contractId,
|
|
174
|
+
version: 1,
|
|
175
|
+
member: "tool",
|
|
176
|
+
name: "invoke",
|
|
177
|
+
operation: "run",
|
|
178
|
+
args: [],
|
|
179
|
+
})).rejects.toThrow("missing caller identity for tool 'invoke'");
|
|
180
|
+
expect(write).not.toHaveBeenCalled();
|
|
181
|
+
}
|
|
182
|
+
finally {
|
|
183
|
+
endpoint.dispose();
|
|
184
|
+
}
|
|
185
|
+
});
|
|
145
186
|
});
|
|
146
187
|
const mockGetFileContents = jest.fn();
|
|
147
188
|
jest.mock("../data/files", () => ({
|
|
@@ -364,8 +405,8 @@ describe("PackageLoader isolated artifacts", () => {
|
|
|
364
405
|
await expect(loader.loadPackage(pkg)).rejects.toThrow(/JSON is invalid/);
|
|
365
406
|
expect(globalThis.__shouldNotEvalOnHost).toBeUndefined();
|
|
366
407
|
});
|
|
367
|
-
it("passes
|
|
368
|
-
const factory = jest.fn(() => ({
|
|
408
|
+
it("passes gateways and a trusted local identity resolver from its data context", () => {
|
|
409
|
+
const factory = jest.fn((_options) => ({
|
|
369
410
|
install: jest.fn(async () => ({ packageId, provided: [], consumes: [] })),
|
|
370
411
|
replace: jest.fn(async () => ({ packageId, provided: [], consumes: [] })),
|
|
371
412
|
uninstall: jest.fn(async () => undefined),
|
|
@@ -381,11 +422,13 @@ describe("PackageLoader isolated artifacts", () => {
|
|
|
381
422
|
tableContainer,
|
|
382
423
|
};
|
|
383
424
|
dataContext.userContext = {
|
|
425
|
+
userId: (0, utils_1.newid)(),
|
|
384
426
|
userDataContext: dataContext,
|
|
385
427
|
};
|
|
386
428
|
new package_loader_1.PackageLoader(dataContext);
|
|
387
429
|
expect(factory).toHaveBeenCalledWith(expect.objectContaining({
|
|
388
430
|
dataContextId: "ctx-tables",
|
|
431
|
+
getLocalUserContext: expect.any(Function),
|
|
389
432
|
providerGateway: expect.objectContaining({
|
|
390
433
|
resolveEndpoint: expect.any(Function),
|
|
391
434
|
}),
|
|
@@ -399,6 +442,11 @@ describe("PackageLoader isolated artifacts", () => {
|
|
|
399
442
|
save: expect.any(Function),
|
|
400
443
|
}),
|
|
401
444
|
}));
|
|
445
|
+
const resolveLocalUser = factory.mock.calls[0][0].getLocalUserContext;
|
|
446
|
+
expect(resolveLocalUser?.()).toEqual({ userId: dataContext.userContext.userId, dataContext });
|
|
447
|
+
expect(resolveLocalUser?.()?.dataContext).toBe(dataContext);
|
|
448
|
+
dataContext.userContext = undefined;
|
|
449
|
+
expect(resolveLocalUser?.()).toBeUndefined();
|
|
402
450
|
});
|
|
403
451
|
it("unloads an isolated package on force reload and disable", async () => {
|
|
404
452
|
const runtime = makeRuntime();
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Host-side record of an isolated package's declared outbound HTTP policy.
|
|
3
|
+
*
|
|
4
|
+
* Populated when the isolated runtime installs a manifest and read by System HTTP.
|
|
5
|
+
* Secrets are not stored here; {@link IPackageNetworkCredential.secretVar} is a
|
|
6
|
+
* logical name the host resolves at request time.
|
|
7
|
+
*/
|
|
8
|
+
/** How System HTTP injects one secret the guest never receives. */
|
|
9
|
+
export interface IPackageNetworkCredential {
|
|
10
|
+
/** Name the guest passes as `credentialRef`. */
|
|
11
|
+
ref: string;
|
|
12
|
+
/** Logical persistent-variable name. The host prefixes it with the package id. */
|
|
13
|
+
secretVar: string;
|
|
14
|
+
/** Where the secret persistent variable lives. Defaults to `user`. */
|
|
15
|
+
scope?: "user" | "device";
|
|
16
|
+
/** Request header the host sets from the secret. */
|
|
17
|
+
header: string;
|
|
18
|
+
/** Header value template. Must contain the literal `{secret}`. */
|
|
19
|
+
format: string;
|
|
20
|
+
}
|
|
21
|
+
/** Outbound HTTP permission recorded for one installed package in one data context. */
|
|
22
|
+
export interface IPackageNetworkPolicy {
|
|
23
|
+
/** Exact hostnames the package may request. `https` only. */
|
|
24
|
+
allowedHosts: string[];
|
|
25
|
+
/** Secrets the host may attach. The guest names a `ref`, never the value. */
|
|
26
|
+
credentials: IPackageNetworkCredential[];
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Record or clear the network policy for one package in one data context.
|
|
30
|
+
*
|
|
31
|
+
* Pass `undefined` when the package is uninstalled or declares no network block.
|
|
32
|
+
* The policy is process memory; install republishes it.
|
|
33
|
+
*
|
|
34
|
+
* @param dataContextId - Personal or group context that installed the package.
|
|
35
|
+
* @param packageId - Trusted package id from the manifest, not from a guest payload.
|
|
36
|
+
* @param policy - Declared allowlist and credential refs, or `undefined` to forget it.
|
|
37
|
+
*/
|
|
38
|
+
export declare function setPackageNetworkPolicy(dataContextId: string, packageId: string, policy: IPackageNetworkPolicy | undefined): void;
|
|
39
|
+
/**
|
|
40
|
+
* Return the network policy installed for this package, if it declared one.
|
|
41
|
+
*
|
|
42
|
+
* @param dataContextId - Context whose runtime installed the package.
|
|
43
|
+
* @param packageId - Package that is calling System HTTP.
|
|
44
|
+
* @returns A copy of the policy, or `undefined` when HTTP is not declared.
|
|
45
|
+
*/
|
|
46
|
+
export declare function getPackageNetworkPolicy(dataContextId: string, packageId: string): IPackageNetworkPolicy | undefined;
|
|
47
|
+
/**
|
|
48
|
+
* Drop every recorded network policy.
|
|
49
|
+
*
|
|
50
|
+
* @internal Test-only. Production install and uninstall update one package at a time.
|
|
51
|
+
*/
|
|
52
|
+
export declare function _clearPackageNetworkPoliciesForTests(): void;
|