@nuxtjp/local-runtime 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/LICENSE-PREVIOUS +21 -0
- package/NOTICE +4 -0
- package/README.md +86 -2
- package/dist/module.d.mts +10 -0
- package/dist/module.json +12 -0
- package/dist/module.mjs +33 -0
- package/dist/runtime/app/assets/local-runtime.css +1 -0
- package/dist/runtime/app/components/LocalRuntimeStatus.d.vue.ts +8 -0
- package/dist/runtime/app/components/LocalRuntimeStatus.vue +25 -0
- package/dist/runtime/app/components/LocalRuntimeStatus.vue.d.ts +8 -0
- package/dist/runtime/app/composables/useLocalRuntime.d.ts +92 -0
- package/dist/runtime/app/composables/useLocalRuntime.js +51 -0
- package/dist/runtime/client/client.d.ts +22 -0
- package/dist/runtime/client/client.js +117 -0
- package/dist/runtime/client/endpoint.d.ts +5 -0
- package/dist/runtime/client/endpoint.js +14 -0
- package/dist/runtime/client/index.d.ts +6 -0
- package/dist/runtime/client/index.js +4 -0
- package/dist/runtime/client/request.d.ts +5 -0
- package/dist/runtime/client/request.js +22 -0
- package/dist/runtime/client/response.d.ts +1 -0
- package/dist/runtime/client/response.js +36 -0
- package/dist/runtime/client/transport.d.ts +1 -0
- package/dist/runtime/client/transport.js +11 -0
- package/dist/runtime/shared/guards/manifest.d.ts +2 -0
- package/dist/runtime/shared/guards/manifest.js +71 -0
- package/dist/runtime/shared/guards/payload.d.ts +3 -0
- package/dist/runtime/shared/guards/payload.js +29 -0
- package/dist/runtime/shared/guards/primitives.d.ts +5 -0
- package/dist/runtime/shared/guards/primitives.js +16 -0
- package/dist/runtime/shared/guards/session.d.ts +2 -0
- package/dist/runtime/shared/guards/session.js +27 -0
- package/dist/runtime/shared/guards/view.d.ts +2 -0
- package/dist/runtime/shared/guards/view.js +22 -0
- package/dist/runtime/shared/guards.d.ts +4 -0
- package/dist/runtime/shared/guards.js +4 -0
- package/dist/runtime/shared/nuxt-imports.d.ts +9 -0
- package/dist/runtime/shared/security.d.ts +7 -0
- package/dist/runtime/shared/security.js +13 -0
- package/dist/runtime/shared/types.d.ts +95 -0
- package/dist/runtime/shared/types.js +0 -0
- package/dist/types.d.mts +3 -0
- package/package.json +74 -3
- package/schemas/local-runtime-v1.schema.json +332 -0
- package/src/runtime/shared/types.ts +126 -0
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import {
|
|
2
|
+
isLocalRuntimeManifest,
|
|
3
|
+
isSessionGrant,
|
|
4
|
+
isViewEnvelope
|
|
5
|
+
} from "../shared/guards.js";
|
|
6
|
+
import { requireLoopbackEndpoint, runtimeUrl } from "./endpoint.js";
|
|
7
|
+
import { localRequest } from "./request.js";
|
|
8
|
+
import { localJson } from "./transport.js";
|
|
9
|
+
export class LocalRuntimeClient {
|
|
10
|
+
endpoint;
|
|
11
|
+
origin;
|
|
12
|
+
fetcher;
|
|
13
|
+
maximumResponseBytes;
|
|
14
|
+
timeoutMilliseconds;
|
|
15
|
+
grant = null;
|
|
16
|
+
runtimeManifest = null;
|
|
17
|
+
constructor(options) {
|
|
18
|
+
this.endpoint = requireLoopbackEndpoint(options.endpoint);
|
|
19
|
+
this.origin = new URL(options.origin).origin;
|
|
20
|
+
this.fetcher = options.fetcher || fetch;
|
|
21
|
+
this.maximumResponseBytes = options.maximumResponseBytes || 256 * 1024;
|
|
22
|
+
this.timeoutMilliseconds = options.timeoutMilliseconds || 5e3;
|
|
23
|
+
if (!Number.isSafeInteger(this.maximumResponseBytes) || this.maximumResponseBytes <= 0 || this.maximumResponseBytes > 8 * 1024 * 1024) {
|
|
24
|
+
throw new Error("maximum response bytes are outside the local boundary");
|
|
25
|
+
}
|
|
26
|
+
if (!Number.isSafeInteger(this.timeoutMilliseconds) || this.timeoutMilliseconds < 50 || this.timeoutMilliseconds > 3e4) {
|
|
27
|
+
throw new Error("timeout is outside the local boundary");
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
async manifest() {
|
|
31
|
+
const value = await localJson(
|
|
32
|
+
this.fetcher,
|
|
33
|
+
runtimeUrl(this.endpoint, "/v1/manifest"),
|
|
34
|
+
localRequest("GET"),
|
|
35
|
+
this.maximumResponseBytes,
|
|
36
|
+
this.timeoutMilliseconds
|
|
37
|
+
);
|
|
38
|
+
if (!isLocalRuntimeManifest(value) || !value.security.allowed_origins.includes(this.origin)) {
|
|
39
|
+
throw new Error("invalid runtime manifest");
|
|
40
|
+
}
|
|
41
|
+
this.runtimeManifest = value;
|
|
42
|
+
return value;
|
|
43
|
+
}
|
|
44
|
+
async connect(capabilities) {
|
|
45
|
+
if (!this.runtimeManifest) throw new Error("runtime manifest is not loaded");
|
|
46
|
+
if (capabilities.length > 64 || capabilities.some((capability) => !capability || capability.length > 128)) {
|
|
47
|
+
throw new Error("requested capabilities exceed the local boundary");
|
|
48
|
+
}
|
|
49
|
+
const available = new Set(
|
|
50
|
+
this.runtimeManifest.capabilities.map((capability) => capability.id)
|
|
51
|
+
);
|
|
52
|
+
if (capabilities.some((capability) => !available.has(capability))) {
|
|
53
|
+
throw new Error("requested capability is not declared");
|
|
54
|
+
}
|
|
55
|
+
const request = {
|
|
56
|
+
schema: "nuxtjp://local-runtime/session-request/v1",
|
|
57
|
+
origin: this.origin,
|
|
58
|
+
audience: this.origin,
|
|
59
|
+
client_nonce: crypto.randomUUID(),
|
|
60
|
+
requested_capabilities: [...new Set(capabilities)]
|
|
61
|
+
};
|
|
62
|
+
const value = await localJson(
|
|
63
|
+
this.fetcher,
|
|
64
|
+
runtimeUrl(this.endpoint, "/v1/session"),
|
|
65
|
+
localRequest("POST", { body: request }),
|
|
66
|
+
this.maximumResponseBytes,
|
|
67
|
+
this.timeoutMilliseconds
|
|
68
|
+
);
|
|
69
|
+
const expected = [...new Set(capabilities)].sort();
|
|
70
|
+
const observed = isSessionGrant(value) ? [...value.granted_capabilities].sort() : [];
|
|
71
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
72
|
+
const limits = this.runtimeManifest.security.limits;
|
|
73
|
+
if (!isSessionGrant(value) || value.audience !== this.origin || value.expires_at_unix_seconds <= now || value.expires_at_unix_seconds > now + limits.session_ttl_seconds + 5 || JSON.stringify(observed) !== JSON.stringify(expected) || value.budget.max_messages > limits.max_messages_per_minute || value.budget.max_bytes > limits.max_bytes_per_minute) {
|
|
74
|
+
throw new Error("invalid runtime session grant");
|
|
75
|
+
}
|
|
76
|
+
this.grant = value;
|
|
77
|
+
return value;
|
|
78
|
+
}
|
|
79
|
+
async view(capabilityId, viewId) {
|
|
80
|
+
if (!this.grant || !this.runtimeManifest) {
|
|
81
|
+
throw new Error("local runtime session is not connected");
|
|
82
|
+
}
|
|
83
|
+
if (!this.grant.granted_capabilities.includes(capabilityId)) {
|
|
84
|
+
throw new Error("capability was not granted");
|
|
85
|
+
}
|
|
86
|
+
const request = {
|
|
87
|
+
schema: "nuxtjp://local-runtime/view-request/v1",
|
|
88
|
+
capability_id: capabilityId,
|
|
89
|
+
view_id: viewId,
|
|
90
|
+
request_nonce: crypto.randomUUID()
|
|
91
|
+
};
|
|
92
|
+
const value = await localJson(
|
|
93
|
+
this.fetcher,
|
|
94
|
+
runtimeUrl(this.endpoint, "/v1/view"),
|
|
95
|
+
localRequest("POST", { body: request, token: this.grant.token }),
|
|
96
|
+
this.maximumResponseBytes,
|
|
97
|
+
this.timeoutMilliseconds
|
|
98
|
+
);
|
|
99
|
+
const capability = this.runtimeManifest.capabilities.find((item) => item.id === capabilityId);
|
|
100
|
+
if (!capability || !isViewEnvelope(value, capability.payload_schema) || value.capability_id !== capabilityId || value.view_id !== viewId) {
|
|
101
|
+
throw new Error("invalid runtime view");
|
|
102
|
+
}
|
|
103
|
+
return value;
|
|
104
|
+
}
|
|
105
|
+
async disconnect() {
|
|
106
|
+
const token = this.grant?.token;
|
|
107
|
+
this.grant = null;
|
|
108
|
+
if (!token) return;
|
|
109
|
+
await localJson(
|
|
110
|
+
this.fetcher,
|
|
111
|
+
runtimeUrl(this.endpoint, "/v1/session"),
|
|
112
|
+
localRequest("DELETE", { token }),
|
|
113
|
+
this.maximumResponseBytes,
|
|
114
|
+
this.timeoutMilliseconds
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
const numericLoopbackHosts = /* @__PURE__ */ new Set(["127.0.0.1", "[::1]"]);
|
|
2
|
+
export function requireLoopbackEndpoint(endpoint) {
|
|
3
|
+
const url = new URL(endpoint);
|
|
4
|
+
if (!["http:", "https:"].includes(url.protocol) || !numericLoopbackHosts.has(url.hostname) || url.username || url.password || url.search || url.hash || url.pathname !== "/" && url.pathname !== "") {
|
|
5
|
+
throw new Error("local runtime endpoint must be a bare numeric loopback origin");
|
|
6
|
+
}
|
|
7
|
+
return url;
|
|
8
|
+
}
|
|
9
|
+
export function runtimeUrl(endpoint, path) {
|
|
10
|
+
if (!path.startsWith("/") || path.includes("..")) {
|
|
11
|
+
throw new Error("local runtime path is invalid");
|
|
12
|
+
}
|
|
13
|
+
return new URL(path, endpoint);
|
|
14
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { LocalRuntimeClient } from './client.js';
|
|
2
|
+
export type { LocalRuntimeClientOptions } from './client.js';
|
|
3
|
+
export { requireLoopbackEndpoint } from './endpoint.js';
|
|
4
|
+
export { localJson } from './transport.js';
|
|
5
|
+
export { bandAllowsTarget, isBrowserVisibleBand } from '../shared/security.js';
|
|
6
|
+
export type * from '../shared/types';
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export function localRequest(method, options = {}) {
|
|
2
|
+
const headers = {
|
|
3
|
+
accept: "application/json",
|
|
4
|
+
"x-nuxtjp-local-runtime": "1"
|
|
5
|
+
};
|
|
6
|
+
let body;
|
|
7
|
+
if (options.body !== void 0) {
|
|
8
|
+
body = JSON.stringify(options.body);
|
|
9
|
+
headers["content-type"] = "application/json";
|
|
10
|
+
}
|
|
11
|
+
if (options.token) headers.authorization = `Bearer ${options.token}`;
|
|
12
|
+
return {
|
|
13
|
+
method,
|
|
14
|
+
headers,
|
|
15
|
+
...body === void 0 ? {} : { body },
|
|
16
|
+
cache: "no-store",
|
|
17
|
+
credentials: "omit",
|
|
18
|
+
redirect: "error",
|
|
19
|
+
referrerPolicy: "no-referrer",
|
|
20
|
+
mode: "cors"
|
|
21
|
+
};
|
|
22
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function boundedJson(response: Response, maximumBytes: number): Promise<unknown>;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export async function boundedJson(response, maximumBytes) {
|
|
2
|
+
if (!response.ok) {
|
|
3
|
+
throw new Error(`local runtime returned HTTP ${response.status}`);
|
|
4
|
+
}
|
|
5
|
+
const contentType = response.headers.get("content-type")?.split(";")[0]?.trim();
|
|
6
|
+
if (contentType !== "application/json") {
|
|
7
|
+
throw new Error("local runtime response must be application/json");
|
|
8
|
+
}
|
|
9
|
+
const declaredLength = response.headers.get("content-length");
|
|
10
|
+
const contentLength = declaredLength === null ? null : Number(declaredLength);
|
|
11
|
+
if (contentLength !== null && (!Number.isSafeInteger(contentLength) || contentLength < 0 || contentLength > maximumBytes)) {
|
|
12
|
+
throw new Error("local runtime response exceeds the byte limit");
|
|
13
|
+
}
|
|
14
|
+
if (!response.body) throw new Error("local runtime response body is missing");
|
|
15
|
+
const reader = response.body.getReader();
|
|
16
|
+
const chunks = [];
|
|
17
|
+
let byteLength = 0;
|
|
18
|
+
while (true) {
|
|
19
|
+
const result = await reader.read();
|
|
20
|
+
if (result.done) break;
|
|
21
|
+
byteLength += result.value.byteLength;
|
|
22
|
+
if (byteLength > maximumBytes) {
|
|
23
|
+
await reader.cancel();
|
|
24
|
+
throw new Error("local runtime response exceeds the byte limit");
|
|
25
|
+
}
|
|
26
|
+
chunks.push(result.value);
|
|
27
|
+
}
|
|
28
|
+
const bytes = new Uint8Array(byteLength);
|
|
29
|
+
let offset = 0;
|
|
30
|
+
for (const chunk of chunks) {
|
|
31
|
+
bytes.set(chunk, offset);
|
|
32
|
+
offset += chunk.byteLength;
|
|
33
|
+
}
|
|
34
|
+
const text = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
35
|
+
return JSON.parse(text);
|
|
36
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function localJson(fetcher: typeof fetch, url: URL, init: RequestInit, maximumBytes: number, timeoutMilliseconds: number): Promise<unknown>;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { boundedJson } from "./response.js";
|
|
2
|
+
export async function localJson(fetcher, url, init, maximumBytes, timeoutMilliseconds) {
|
|
3
|
+
const controller = new AbortController();
|
|
4
|
+
const timer = setTimeout(() => controller.abort(), timeoutMilliseconds);
|
|
5
|
+
try {
|
|
6
|
+
const response = await fetcher(url, { ...init, signal: controller.signal });
|
|
7
|
+
return await boundedJson(response, maximumBytes);
|
|
8
|
+
} finally {
|
|
9
|
+
clearTimeout(timer);
|
|
10
|
+
}
|
|
11
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import {
|
|
2
|
+
hasExactKeys,
|
|
3
|
+
isRecord,
|
|
4
|
+
isSafeCount,
|
|
5
|
+
isStringArray,
|
|
6
|
+
isText
|
|
7
|
+
} from "./primitives.js";
|
|
8
|
+
import { isPayloadSchema } from "./payload.js";
|
|
9
|
+
const manifestKeys = [
|
|
10
|
+
"capabilities",
|
|
11
|
+
"engine",
|
|
12
|
+
"external_actions",
|
|
13
|
+
"mode",
|
|
14
|
+
"protocol_version",
|
|
15
|
+
"runtime_id",
|
|
16
|
+
"schema",
|
|
17
|
+
"security"
|
|
18
|
+
];
|
|
19
|
+
function isLimits(value) {
|
|
20
|
+
if (!isRecord(value) || !hasExactKeys(value, [
|
|
21
|
+
"max_bytes_per_minute",
|
|
22
|
+
"max_messages_per_minute",
|
|
23
|
+
"max_request_bytes",
|
|
24
|
+
"max_response_bytes",
|
|
25
|
+
"session_ttl_seconds"
|
|
26
|
+
])) return false;
|
|
27
|
+
return isSafeCount(value.max_request_bytes) && value.max_request_bytes > 0 && value.max_request_bytes <= 1024 * 1024 && isSafeCount(value.max_response_bytes) && value.max_response_bytes > 0 && value.max_response_bytes <= 8 * 1024 * 1024 && isSafeCount(value.max_messages_per_minute) && value.max_messages_per_minute > 0 && value.max_messages_per_minute <= 600 && isSafeCount(value.max_bytes_per_minute) && value.max_bytes_per_minute > 0 && value.max_bytes_per_minute <= 64 * 1024 * 1024 && isSafeCount(value.session_ttl_seconds) && value.session_ttl_seconds > 0 && value.session_ttl_seconds <= 3600;
|
|
28
|
+
}
|
|
29
|
+
function isCapability(value) {
|
|
30
|
+
if (!isRecord(value) || !hasExactKeys(value, [
|
|
31
|
+
"description",
|
|
32
|
+
"id",
|
|
33
|
+
"output_band",
|
|
34
|
+
"payload_schema",
|
|
35
|
+
"service_id"
|
|
36
|
+
])) return false;
|
|
37
|
+
return isText(value.id) && value.id.length <= 128 && isText(value.service_id) && value.service_id.length <= 128 && isText(value.description) && value.description.length <= 4096 && ["session", "public"].includes(String(value.output_band)) && isPayloadSchema(value.payload_schema);
|
|
38
|
+
}
|
|
39
|
+
function isAllowedOrigins(value) {
|
|
40
|
+
if (!isStringArray(value) || value.length === 0 || value.length > 16) {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
if (new Set(value).size !== value.length) return false;
|
|
44
|
+
return value.every((origin) => {
|
|
45
|
+
try {
|
|
46
|
+
const url = new URL(origin);
|
|
47
|
+
return ["http:", "https:"].includes(url.protocol) && url.origin === origin && !url.username && !url.password;
|
|
48
|
+
} catch {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
export function isLocalRuntimeManifest(value) {
|
|
54
|
+
if (!isRecord(value) || !hasExactKeys(value, manifestKeys)) return false;
|
|
55
|
+
const { engine, security } = value;
|
|
56
|
+
if (!isRecord(engine) || !hasExactKeys(engine, [
|
|
57
|
+
"arbitrary_filesystem",
|
|
58
|
+
"arbitrary_network",
|
|
59
|
+
"isolation",
|
|
60
|
+
"kind",
|
|
61
|
+
"status"
|
|
62
|
+
])) return false;
|
|
63
|
+
if (!isRecord(security) || !hasExactKeys(security, [
|
|
64
|
+
"allowed_origins",
|
|
65
|
+
"default_band",
|
|
66
|
+
"external_egress",
|
|
67
|
+
"limits"
|
|
68
|
+
])) return false;
|
|
69
|
+
const capabilitiesValid = Array.isArray(value.capabilities) && value.capabilities.length <= 64 && value.capabilities.every(isCapability) && new Set(value.capabilities.map((item) => String(item.id))).size === value.capabilities.length;
|
|
70
|
+
return value.schema === "nuxtjp://local-runtime/manifest/v1" && isText(value.runtime_id) && value.protocol_version === "1" && ["local-simulation", "local-production"].includes(String(value.mode)) && value.external_actions === false && engine.kind === "rust-v8" && ["disabled", "simulated", "ready"].includes(String(engine.status)) && (engine.status === "ready" && engine.isolation === "process" || engine.status !== "ready" && engine.isolation === "in-process-simulation") && engine.arbitrary_network === false && engine.arbitrary_filesystem === false && security.default_band === "sealed" && security.external_egress === "deny" && isAllowedOrigins(security.allowed_origins) && isLimits(security.limits) && capabilitiesValid;
|
|
71
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { PayloadScalar, RuntimePayloadSchema } from '../types.js';
|
|
2
|
+
export declare function isPayloadSchema(value: unknown): value is RuntimePayloadSchema;
|
|
3
|
+
export declare function matchesPayloadSchema(value: unknown, schema: RuntimePayloadSchema): value is Record<string, PayloadScalar>;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import {
|
|
2
|
+
hasExactKeys,
|
|
3
|
+
isRecord,
|
|
4
|
+
isSafeCount,
|
|
5
|
+
isText
|
|
6
|
+
} from "./primitives.js";
|
|
7
|
+
const fieldName = /^[a-z][a-z0-9_]{0,63}$/;
|
|
8
|
+
function isPayloadField(value) {
|
|
9
|
+
return isRecord(value) && hasExactKeys(value, ["kind", "name"]) && isText(value.name) && fieldName.test(value.name) && ["text", "count", "boolean"].includes(String(value.kind));
|
|
10
|
+
}
|
|
11
|
+
export function isPayloadSchema(value) {
|
|
12
|
+
if (!isRecord(value) || !hasExactKeys(value, ["fields", "id"])) return false;
|
|
13
|
+
if (!isText(value.id) || !/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(value.id) || !Array.isArray(value.fields)) return false;
|
|
14
|
+
if (value.fields.length === 0 || value.fields.length > 32) return false;
|
|
15
|
+
if (!value.fields.every(isPayloadField)) return false;
|
|
16
|
+
return new Set(value.fields.map((field) => field.name)).size === value.fields.length;
|
|
17
|
+
}
|
|
18
|
+
function matchesKind(value, kind) {
|
|
19
|
+
if (kind === "text") {
|
|
20
|
+
return isText(value) && new TextEncoder().encode(value).byteLength <= 4096;
|
|
21
|
+
}
|
|
22
|
+
if (kind === "count") return isSafeCount(value);
|
|
23
|
+
return kind === "boolean" && typeof value === "boolean";
|
|
24
|
+
}
|
|
25
|
+
export function matchesPayloadSchema(value, schema) {
|
|
26
|
+
if (!isPayloadSchema(schema) || !isRecord(value)) return false;
|
|
27
|
+
if (!hasExactKeys(value, schema.fields.map((field) => field.name))) return false;
|
|
28
|
+
return schema.fields.every((field) => matchesKind(value[field.name], field.kind));
|
|
29
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export declare function isRecord(value: unknown): value is Record<string, unknown>;
|
|
2
|
+
export declare function hasExactKeys(value: Record<string, unknown>, keys: readonly string[]): boolean;
|
|
3
|
+
export declare function isText(value: unknown): value is string;
|
|
4
|
+
export declare function isStringArray(value: unknown): value is string[];
|
|
5
|
+
export declare function isSafeCount(value: unknown): value is number;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export function isRecord(value) {
|
|
2
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3
|
+
}
|
|
4
|
+
export function hasExactKeys(value, keys) {
|
|
5
|
+
const actual = Object.keys(value).sort();
|
|
6
|
+
return actual.length === keys.length && actual.every((key, index) => key === [...keys].sort()[index]);
|
|
7
|
+
}
|
|
8
|
+
export function isText(value) {
|
|
9
|
+
return typeof value === "string" && value.length > 0;
|
|
10
|
+
}
|
|
11
|
+
export function isStringArray(value) {
|
|
12
|
+
return Array.isArray(value) && value.every(isText);
|
|
13
|
+
}
|
|
14
|
+
export function isSafeCount(value) {
|
|
15
|
+
return Number.isSafeInteger(value) && Number(value) >= 0;
|
|
16
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import {
|
|
2
|
+
hasExactKeys,
|
|
3
|
+
isRecord,
|
|
4
|
+
isSafeCount,
|
|
5
|
+
isStringArray,
|
|
6
|
+
isText
|
|
7
|
+
} from "./primitives.js";
|
|
8
|
+
export function isSessionGrant(value) {
|
|
9
|
+
if (!isRecord(value) || !hasExactKeys(value, [
|
|
10
|
+
"audience",
|
|
11
|
+
"budget",
|
|
12
|
+
"classification_ceiling",
|
|
13
|
+
"expires_at_unix_seconds",
|
|
14
|
+
"external_actions",
|
|
15
|
+
"granted_capabilities",
|
|
16
|
+
"schema",
|
|
17
|
+
"session_id",
|
|
18
|
+
"subject_id",
|
|
19
|
+
"token"
|
|
20
|
+
])) return false;
|
|
21
|
+
const budget = value.budget;
|
|
22
|
+
if (!isRecord(budget) || !hasExactKeys(budget, [
|
|
23
|
+
"max_bytes",
|
|
24
|
+
"max_messages"
|
|
25
|
+
])) return false;
|
|
26
|
+
return value.schema === "nuxtjp://local-runtime/session-grant/v1" && isText(value.session_id) && isText(value.token) && isText(value.subject_id) && isText(value.audience) && isSafeCount(value.expires_at_unix_seconds) && isStringArray(value.granted_capabilities) && value.granted_capabilities.length <= 64 && new Set(value.granted_capabilities).size === value.granted_capabilities.length && value.classification_ceiling === "session" && isSafeCount(budget.max_messages) && budget.max_messages > 0 && isSafeCount(budget.max_bytes) && budget.max_bytes > 0 && value.external_actions === false;
|
|
27
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import {
|
|
2
|
+
hasExactKeys,
|
|
3
|
+
isRecord,
|
|
4
|
+
isSafeCount,
|
|
5
|
+
isText
|
|
6
|
+
} from "./primitives.js";
|
|
7
|
+
import { isBrowserVisibleBand } from "../security.js";
|
|
8
|
+
import { matchesPayloadSchema } from "./payload.js";
|
|
9
|
+
export function isViewEnvelope(value, payloadSchema) {
|
|
10
|
+
if (!isRecord(value) || !hasExactKeys(value, [
|
|
11
|
+
"band",
|
|
12
|
+
"capability_id",
|
|
13
|
+
"expires_at_unix_seconds",
|
|
14
|
+
"external_actions",
|
|
15
|
+
"generated_at_unix_seconds",
|
|
16
|
+
"payload",
|
|
17
|
+
"payload_schema_id",
|
|
18
|
+
"schema",
|
|
19
|
+
"view_id"
|
|
20
|
+
])) return false;
|
|
21
|
+
return value.schema === "nuxtjp://local-runtime/view/v1" && isText(value.capability_id) && isText(value.view_id) && value.payload_schema_id === payloadSchema.id && isBrowserVisibleBand(value.band) && isSafeCount(value.generated_at_unix_seconds) && isSafeCount(value.expires_at_unix_seconds) && value.expires_at_unix_seconds >= value.generated_at_unix_seconds && value.external_actions === false && matchesPayloadSchema(value.payload, payloadSchema);
|
|
22
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { BoundaryTarget, InformationBand } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Information may cross only the boundary named by its reviewed band.
|
|
4
|
+
* Transforming or declassifying a record is a separate host responsibility.
|
|
5
|
+
*/
|
|
6
|
+
export declare function bandAllowsTarget(band: InformationBand, target: BoundaryTarget): boolean;
|
|
7
|
+
export declare function isBrowserVisibleBand(band: InformationBand): band is 'session' | 'public';
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
const boundaryByBand = {
|
|
2
|
+
sealed: "none",
|
|
3
|
+
local: "local-mesh",
|
|
4
|
+
session: "loopback-browser",
|
|
5
|
+
control: "external-control",
|
|
6
|
+
public: "public-network"
|
|
7
|
+
};
|
|
8
|
+
export function bandAllowsTarget(band, target) {
|
|
9
|
+
return boundaryByBand[band] === target;
|
|
10
|
+
}
|
|
11
|
+
export function isBrowserVisibleBand(band) {
|
|
12
|
+
return band === "session" || band === "public";
|
|
13
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
export type InformationBand = 'sealed' | 'local' | 'session' | 'control' | 'public';
|
|
2
|
+
export type BoundaryTarget = 'none' | 'local-mesh' | 'loopback-browser' | 'external-control' | 'public-network';
|
|
3
|
+
export interface RuntimeEngine {
|
|
4
|
+
kind: 'rust-v8';
|
|
5
|
+
status: 'disabled' | 'simulated' | 'ready';
|
|
6
|
+
isolation: 'in-process-simulation' | 'process';
|
|
7
|
+
arbitrary_network: false;
|
|
8
|
+
arbitrary_filesystem: false;
|
|
9
|
+
}
|
|
10
|
+
export interface RuntimeLimits {
|
|
11
|
+
max_request_bytes: number;
|
|
12
|
+
max_response_bytes: number;
|
|
13
|
+
max_messages_per_minute: number;
|
|
14
|
+
max_bytes_per_minute: number;
|
|
15
|
+
session_ttl_seconds: number;
|
|
16
|
+
}
|
|
17
|
+
export interface RuntimeSecurity {
|
|
18
|
+
default_band: 'sealed';
|
|
19
|
+
external_egress: 'deny';
|
|
20
|
+
allowed_origins: string[];
|
|
21
|
+
limits: RuntimeLimits;
|
|
22
|
+
}
|
|
23
|
+
export type PayloadValueKind = 'text' | 'count' | 'boolean';
|
|
24
|
+
export type PayloadScalar = string | number | boolean;
|
|
25
|
+
export interface RuntimePayloadField {
|
|
26
|
+
name: string;
|
|
27
|
+
kind: PayloadValueKind;
|
|
28
|
+
}
|
|
29
|
+
export interface RuntimePayloadSchema {
|
|
30
|
+
id: string;
|
|
31
|
+
fields: RuntimePayloadField[];
|
|
32
|
+
}
|
|
33
|
+
export interface RuntimeCapability {
|
|
34
|
+
id: string;
|
|
35
|
+
service_id: string;
|
|
36
|
+
description: string;
|
|
37
|
+
output_band: 'session' | 'public';
|
|
38
|
+
payload_schema: RuntimePayloadSchema;
|
|
39
|
+
}
|
|
40
|
+
export interface LocalRuntimeManifest {
|
|
41
|
+
schema: 'nuxtjp://local-runtime/manifest/v1';
|
|
42
|
+
runtime_id: string;
|
|
43
|
+
protocol_version: '1';
|
|
44
|
+
mode: 'local-simulation' | 'local-production';
|
|
45
|
+
external_actions: false;
|
|
46
|
+
engine: RuntimeEngine;
|
|
47
|
+
security: RuntimeSecurity;
|
|
48
|
+
capabilities: RuntimeCapability[];
|
|
49
|
+
}
|
|
50
|
+
export interface SessionRequest {
|
|
51
|
+
schema: 'nuxtjp://local-runtime/session-request/v1';
|
|
52
|
+
origin: string;
|
|
53
|
+
audience: string;
|
|
54
|
+
client_nonce: string;
|
|
55
|
+
requested_capabilities: string[];
|
|
56
|
+
}
|
|
57
|
+
export interface SessionBudget {
|
|
58
|
+
max_messages: number;
|
|
59
|
+
max_bytes: number;
|
|
60
|
+
}
|
|
61
|
+
export interface SessionGrant {
|
|
62
|
+
schema: 'nuxtjp://local-runtime/session-grant/v1';
|
|
63
|
+
session_id: string;
|
|
64
|
+
token: string;
|
|
65
|
+
subject_id: string;
|
|
66
|
+
audience: string;
|
|
67
|
+
expires_at_unix_seconds: number;
|
|
68
|
+
granted_capabilities: string[];
|
|
69
|
+
classification_ceiling: 'session';
|
|
70
|
+
budget: SessionBudget;
|
|
71
|
+
external_actions: false;
|
|
72
|
+
}
|
|
73
|
+
export interface ViewRequest {
|
|
74
|
+
schema: 'nuxtjp://local-runtime/view-request/v1';
|
|
75
|
+
capability_id: string;
|
|
76
|
+
view_id: string;
|
|
77
|
+
request_nonce: string;
|
|
78
|
+
}
|
|
79
|
+
export interface ViewEnvelope {
|
|
80
|
+
schema: 'nuxtjp://local-runtime/view/v1';
|
|
81
|
+
capability_id: string;
|
|
82
|
+
view_id: string;
|
|
83
|
+
payload_schema_id: string;
|
|
84
|
+
band: 'session' | 'public';
|
|
85
|
+
generated_at_unix_seconds: number;
|
|
86
|
+
expires_at_unix_seconds: number;
|
|
87
|
+
external_actions: false;
|
|
88
|
+
payload: Record<string, PayloadScalar>;
|
|
89
|
+
}
|
|
90
|
+
export type ConnectionPhase = 'idle' | 'connecting' | 'connected' | 'denied' | 'unavailable';
|
|
91
|
+
export interface LocalRuntimeState {
|
|
92
|
+
phase: ConnectionPhase;
|
|
93
|
+
manifest: LocalRuntimeManifest | null;
|
|
94
|
+
error: string | null;
|
|
95
|
+
}
|
|
File without changes
|
package/dist/types.d.mts
ADDED