Files
NetMesh/electron/plugins/extensionProviderService.cjs
zhaolei 3c72efcb7f
Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
[Init] Initial commit - NetMesh terminal manager
2026-09-13 18:24:01 +08:00

1367 lines
56 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use strict";
const { randomUUID } = require("node:crypto");
const { performance } = require("node:perf_hooks");
const {
assertProviderRequest,
assertProviderResult,
createDefinitionValidator,
formatValidationErrors,
} = require("./contractValidator.cjs");
const { assertPluginJsonValue } = require("./jsonBoundary.cjs");
const { PluginRpcError, RPC_ERRORS, raceWithAbort } = require("./rpcRouter.cjs");
const { compileRestrictedJsonSchema } = require("./restrictedJsonSchema.cjs");
const pluginContractSchema = require("./generated/plugin-contract.schema.json");
const EXTENSION_PROVIDER_KINDS = Object.freeze(["connection", "authentication", "importer", "sync"]);
const PROVIDER_PERMISSIONS = Object.freeze({
connection: "provider.connection",
authentication: "provider.authentication",
importer: "provider.importer",
sync: "provider.sync",
});
const OPERATIONS = Object.freeze({
connection: new Set(["validateConfiguration", "probe", "open", "resize", "signal", "reconnect", "close", "getStatus"]),
authentication: new Set(["begin", "respond", "cancel"]),
importer: new Set(["detect", "parse"]),
sync: new Set(["connect", "disconnect", "getAccount", "getCapabilities", "readObject", "writeObject", "deleteObject"]),
});
const MAX_PROVIDER_JSON_BYTES = 128 * 1024;
const DEFAULT_DEADLINE_MS = 30_000;
const STREAM_WINDOW_BYTES = 256 * 1024;
const MAX_AUTH_CHALLENGES = 32;
const AUTHENTICATION_SECRET_LEASE_TTL_MS = 30_000;
const AUTHENTICATION_CANCEL_DEADLINE_MS = 1_000;
const importerLimits = pluginContractSchema.$defs.ImporterLimits.const;
const MAX_IMPORT_BYTES = importerLimits.maxInputBytes;
const MAX_IMPORT_OUTPUT_BYTES = importerLimits.maxOutputBytes;
const MAX_IMPORT_RECORDS = importerLimits.maxRecords;
const MAX_IMPORT_RECORD_BYTES = importerLimits.maxRecordBytes;
const SYNC_SECRET_CONFIG_KEYS = new Set(["password", "token", "secret", "apiKey", "accessToken"]);
/** Drop known secret / writeOnly field names from required[] so stripped sync configs still validate. */
function schemaWithoutSyncSecretRequirements(schema) {
if (!schema || typeof schema !== "object" || Array.isArray(schema)) return schema;
if (!Array.isArray(schema.required)) return schema;
const writeOnlyNames = new Set();
if (schema.properties && typeof schema.properties === "object" && !Array.isArray(schema.properties)) {
for (const [name, child] of Object.entries(schema.properties)) {
if (child && typeof child === "object" && !Array.isArray(child) && child.writeOnly === true) {
writeOnlyNames.add(name);
}
}
}
const required = schema.required.filter(
(name) => !SYNC_SECRET_CONFIG_KEYS.has(name) && !writeOnlyNames.has(name),
);
if (required.length === schema.required.length) return schema;
return { ...schema, required };
}
const syncLimits = pluginContractSchema.$defs.SyncLimits.const;
const MAX_SYNC_OBJECT_BYTES = syncLimits.maxObjectBytes;
const MAX_SYNC_OBJECT_KEY_LENGTH = syncLimits.maxObjectKeyLength;
const MAX_SYNC_REVISION_LENGTH = syncLimits.maxRevisionLength;
const INLINE_SYNC_OBJECT_BYTES = syncLimits.inlineObjectBytes;
// inlineObjectBytes is sized to fit under the 128 KiB provider JSON budget after
// base64 expansion (~4/3) with envelope headroom. Keep the runtime cutoff equal
// to the public SyncLimits constant so plugins that honor the schema are not
// rejected with dataLoss in the 9096 KiB window.
const INLINE_SYNC_OBJECT_SAFE_BYTES = INLINE_SYNC_OBJECT_BYTES;
const definitionValidators = Object.freeze({
AuthenticationResult: createDefinitionValidator("AuthenticationResult"),
ConnectionOpenResult: createDefinitionValidator("ConnectionOpenResult"),
ConnectionProbeResult: createDefinitionValidator("ConnectionProbeResult"),
ConnectionStatusResult: createDefinitionValidator("ConnectionStatusResult"),
ConnectionControlResult: createDefinitionValidator("ConnectionControlResult"),
ConnectionValidateResult: createDefinitionValidator("ConnectionValidateResult"),
ImporterDetectResult: createDefinitionValidator("ImporterDetectResult"),
ImporterParseResult: createDefinitionValidator("ImporterParseResult"),
ImporterRecord: createDefinitionValidator("ImporterRecord"),
SyncConnectResult: createDefinitionValidator("SyncConnectResult"),
SyncDisconnectResult: createDefinitionValidator("SyncDisconnectResult"),
SyncGetAccountResult: createDefinitionValidator("SyncGetAccountResult"),
SyncCapabilitiesResult: createDefinitionValidator("SyncCapabilitiesResult"),
SyncReadObjectResult: createDefinitionValidator("SyncReadObjectResult"),
SyncWriteObjectResult: createDefinitionValidator("SyncWriteObjectResult"),
SyncDeleteObjectResult: createDefinitionValidator("SyncDeleteObjectResult"),
});
function invalidArgument(message) {
return new PluginRpcError(RPC_ERRORS.invalidArgument, message);
}
function freezeJson(value) {
const clone = structuredClone(value);
const freeze = (item) => {
if (!item || typeof item !== "object" || Object.isFrozen(item)) return item;
for (const child of Array.isArray(item) ? item : Object.values(item)) freeze(child);
return Object.freeze(item);
};
return freeze(clone);
}
function assertBoundedJson(value, label, maxBytes = MAX_PROVIDER_JSON_BYTES) {
try { assertPluginJsonValue(value, { maxBytes }); }
catch (error) { throw invalidArgument(`${label} must be bounded JSON: ${error?.message ?? error}`); }
return value;
}
function assertString(value, label, maximum = 256) {
if (typeof value !== "string"
|| value.length < 1
|| Array.from(value).length > maximum
|| value.includes("\0")) {
throw invalidArgument(`${label} is invalid`);
}
return value;
}
function buildImporterDetectPayload({ bytes, fileName, mediaType }) {
const metadata = {
...(fileName ? { fileName: assertString(fileName, "Import file name", 1_024) } : {}),
...(mediaType ? { mediaType: assertString(mediaType, "Import media type", 256) } : {}),
sample: { encoding: "base64", data: "" },
};
const metadataBytes = Buffer.byteLength(JSON.stringify(metadata), "utf8");
const encodedBudget = MAX_PROVIDER_JSON_BYTES - metadataBytes;
if (encodedBudget < 4) throw invalidArgument("Importer sample metadata is too large");
const rawBudget = Math.floor(encodedBudget / 4) * 3;
if (rawBudget < 1) throw invalidArgument("Importer sample metadata is too large");
const sampleBytes = bytes.byteLength > rawBudget ? bytes.subarray(0, rawBudget) : bytes;
return {
...metadata,
sample: {
encoding: "base64",
data: Buffer.from(sampleBytes).toString("base64"),
},
};
}
function assertDefinition(name, value, label = name) {
const validator = definitionValidators[name];
if (!validator(value)) {
throw new TypeError(`${label} violates the plugin contract: ${formatValidationErrors(validator.errors)}`);
}
return value;
}
function assertKind(kind) {
if (!EXTENSION_PROVIDER_KINDS.includes(kind)) throw invalidArgument("Extension Provider kind is invalid");
return kind;
}
function assertOperation(kind, operation) {
const value = assertString(operation, "Extension Provider operation", 128);
if (!OPERATIONS[kind].has(value)) throw invalidArgument(`Unsupported ${kind} Provider operation: ${value}`);
return value;
}
function normalizeDeadlineMs(value) {
const deadline = value ?? DEFAULT_DEADLINE_MS;
if (!Number.isSafeInteger(deadline) || deadline < 1 || deadline > 300_000) {
throw invalidArgument("Extension Provider deadline is invalid");
}
return deadline;
}
function normalizeIdentity(activation) {
const identity = activation?.identity;
if (!identity
|| identity.pluginId !== activation.plugin.id
|| identity.pluginVersion !== activation.plugin.activeVersion
|| typeof identity.runtimeId !== "string" || !identity.runtimeId
|| (identity.runtimeKind !== "browser" && identity.runtimeKind !== "utility")
|| typeof identity.securityPrincipal !== "string" || !identity.securityPrincipal) {
throw new PluginRpcError(RPC_ERRORS.unavailable, "Extension Provider activation identity is unavailable or stale");
}
return Object.freeze({
pluginId: identity.pluginId,
pluginVersion: identity.pluginVersion,
runtimeId: identity.runtimeId,
runtimeKind: identity.runtimeKind,
securityPrincipal: identity.securityPrincipal,
});
}
function runtimeContext(activation, identity, signal) {
return Object.freeze({
pluginId: activation.plugin.id,
pluginVersion: activation.plugin.activeVersion,
runtimeId: identity.runtimeId,
runtimeKind: identity.runtimeKind,
manifest: activation.plugin.manifest,
securityPrincipal: identity.securityPrincipal,
signal,
});
}
function identityKey(identity, streamId) {
return `${identity.pluginId}\0${identity.pluginVersion}\0${identity.runtimeId}\0${streamId}`;
}
function waitForStreamOrRequestFailure(streamPromise, requestPromise) {
return Promise.race([
streamPromise,
requestPromise.then(
() => new Promise(() => {}),
(error) => Promise.reject(error),
),
]);
}
function waitUntilDeadline(promise, deadlineAt, signal, message) {
const operation = signal ? raceWithAbort(Promise.resolve(promise), signal) : Promise.resolve(promise);
const remainingMs = Math.max(0, Math.ceil(deadlineAt - performance.now()));
let timer;
const deadline = new Promise((_, reject) => {
timer = setTimeout(() => {
reject(new PluginRpcError(RPC_ERRORS.deadlineExceeded, message));
}, remainingMs);
});
return Promise.race([operation, deadline]).finally(() => clearTimeout(timer));
}
function validateProviderResult(rawResult, requestId) {
try {
assertProviderResult(rawResult);
assertBoundedJson(rawResult, "Extension Provider result");
} catch (error) {
throw new PluginRpcError(RPC_ERRORS.dataLoss, `Extension Provider returned an invalid result: ${error?.message ?? error}`);
}
if (rawResult.requestId !== requestId) {
throw new PluginRpcError(RPC_ERRORS.dataLoss, "Extension Provider returned a mismatched request ID");
}
if (rawResult.status === "failed") {
throw new PluginRpcError(rawResult.error.code, rawResult.error.message, rawResult.error.data);
}
if (rawResult.status === "cancelled") {
throw new PluginRpcError(RPC_ERRORS.cancelled, "Extension Provider request was cancelled");
}
return rawResult.result;
}
function assertConnectionResult(operation, value) {
if (["resize", "signal", "reconnect", "close"].includes(operation)) {
assertDefinition("ConnectionControlResult", value, `Connection ${operation} result`);
if (value !== null) throw new TypeError(`Connection ${operation} result must be null`);
assertBoundedJson(value, `Connection ${operation} result`);
return value;
}
if (!value || typeof value !== "object" || Array.isArray(value)) {
throw new TypeError(`Connection ${operation} result must be an object`);
}
if (operation === "validateConfiguration") {
assertDefinition("ConnectionValidateResult", value, "Connection validation result");
if (typeof value.valid !== "boolean" || !Array.isArray(value.issues)) throw new TypeError("Connection validation result is invalid");
} else if (operation === "probe") {
assertDefinition("ConnectionProbeResult", value, "Connection probe result");
if (typeof value.available !== "boolean") throw new TypeError("Connection probe result is invalid");
} else if (operation === "open") {
assertDefinition("ConnectionOpenResult", value, "Connection open result");
assertString(value.connectionId, "Plugin connection ID");
if (value.status !== "connecting" && value.status !== "connected") throw new TypeError("Connection open status is invalid");
} else if (operation === "getStatus") {
assertDefinition("ConnectionStatusResult", value, "Connection status result");
if (!["connecting", "connected", "reconnecting", "closed", "error"].includes(value.status)) {
throw new TypeError("Connection status is invalid");
}
}
assertBoundedJson(value, `Connection ${operation} result`);
return freezeJson(value);
}
function assertAuthenticationResult(value) {
assertDefinition("AuthenticationResult", value, "Authentication result");
if (!value || typeof value !== "object" || Array.isArray(value)
|| !["challenge", "authenticated", "cancelled", "failed"].includes(value.status)) {
throw new TypeError("Authentication Provider result is invalid");
}
if (value.status === "challenge") {
const challenge = value.challenge;
if (!challenge || typeof challenge !== "object" || Array.isArray(challenge)) {
throw new TypeError("Authentication challenge is missing");
}
assertString(challenge.id, "Authentication challenge ID", 128);
if (!["text", "password", "otp", "choice", "confirmation", "browser", "deviceCode"].includes(challenge.kind)) {
throw new TypeError("Authentication challenge kind is invalid");
}
assertString(challenge.title, "Authentication challenge title", 512);
}
assertBoundedJson(value, "Authentication result");
return freezeJson(value);
}
function assertAuthenticationResponse(challenge, response) {
if (["text", "password", "otp"].includes(challenge.kind)) {
if (typeof response !== "string" || response.length < 1 || Array.from(response).length > 8_192) {
throw invalidArgument("Authentication text response is invalid or too large");
}
return response;
}
if (challenge.kind === "choice") {
const allowed = new Set(challenge.choices.map((choice) => choice.id));
if (challenge.multiple) {
if (!Array.isArray(response) || response.length < 1 || response.length > 64
|| new Set(response).size !== response.length
|| response.some((choiceId) => typeof choiceId !== "string" || !allowed.has(choiceId))) {
throw invalidArgument("Authentication choice response is invalid");
}
return Object.freeze([...response]);
}
if (typeof response !== "string" || !allowed.has(response)) {
throw invalidArgument("Authentication choice response is invalid");
}
return response;
}
if (typeof response !== "boolean") throw invalidArgument("Authentication confirmation response is invalid");
return response;
}
function assertImporterDetectResult(value) {
assertDefinition("ImporterDetectResult", value, "Importer detection result");
if (!value || typeof value !== "object" || Array.isArray(value)
|| typeof value.confidence !== "number" || !Number.isFinite(value.confidence)
|| value.confidence < 0 || value.confidence > 1) {
throw new TypeError("Importer detection result is invalid");
}
assertBoundedJson(value, "Importer detection result");
return freezeJson(value);
}
function normalizeImportRecord(value) {
assertDefinition("ImporterRecord", value, "Importer record");
if (!value || typeof value !== "object" || Array.isArray(value)) throw new TypeError("Importer record must be an object");
if (value.type === "draft") {
if (!value.draft || typeof value.draft !== "object" || Array.isArray(value.draft)
|| !["host", "identity", "key", "snippet", "group"].includes(value.draft.kind)) {
throw new TypeError("Importer draft is invalid");
}
} else if (value.type === "warning" || value.type === "error") {
assertString(value.message, "Importer diagnostic message", 2_048);
} else if (value.type === "progress") {
if (!Number.isSafeInteger(value.completed) || value.completed < 0) throw new TypeError("Importer progress is invalid");
if (value.total != null && (!Number.isSafeInteger(value.total) || value.total < value.completed)) {
throw new TypeError("Importer progress total is invalid");
}
} else {
throw new TypeError("Importer record type is invalid");
}
assertBoundedJson(value, "Importer record", MAX_IMPORT_RECORD_BYTES);
return freezeJson(value);
}
function assertSyncObjectKey(value) {
return assertString(value, "Sync object key", MAX_SYNC_OBJECT_KEY_LENGTH);
}
function assertSyncRevision(value, label = "Sync object revision") {
if (value == null) return value;
return assertString(value, label, MAX_SYNC_REVISION_LENGTH);
}
function assertSyncResult(operation, value) {
if (operation === "disconnect") {
assertDefinition("SyncDisconnectResult", value, "Sync disconnect result");
if (value !== null) throw new TypeError("Sync disconnect result must be null");
return value;
}
if (!value || typeof value !== "object" || Array.isArray(value)) {
throw new TypeError(`Sync ${operation} result must be an object`);
}
if (operation === "connect") {
assertDefinition("SyncConnectResult", value, "Sync connect result");
assertString(value.account?.id, "Sync account ID", 512);
} else if (operation === "getAccount") {
assertDefinition("SyncGetAccountResult", value, "Sync getAccount result");
if (value.account != null) assertString(value.account.id, "Sync account ID", 512);
} else if (operation === "getCapabilities") {
assertDefinition("SyncCapabilitiesResult", value, "Sync capabilities result");
if (typeof value.revisions !== "boolean"
|| typeof value.conditionalWrites !== "boolean"
|| typeof value.atomicReplacement !== "boolean") {
throw new TypeError("Sync capabilities result is invalid");
}
if (value.maxObjectBytes != null
&& (!Number.isSafeInteger(value.maxObjectBytes)
|| value.maxObjectBytes < 1
|| value.maxObjectBytes > MAX_SYNC_OBJECT_BYTES)) {
throw new TypeError("Sync maxObjectBytes is invalid");
}
} else if (operation === "readObject") {
assertDefinition("SyncReadObjectResult", value, "Sync readObject result");
if (value.found === true) {
if (!Number.isSafeInteger(value.byteLength) || value.byteLength < 0 || value.byteLength > MAX_SYNC_OBJECT_BYTES) {
throw new TypeError("Sync readObject byteLength is invalid");
}
if (value.streamed === true) {
// Host streams the ciphertext separately.
} else if (value.encoding === "base64") {
if (typeof value.data !== "string") throw new TypeError("Sync readObject data is invalid");
const decoded = Buffer.byteLength(value.data, "base64");
// Use the safe inline cutoff (base64 + envelope must fit control-plane JSON).
if (value.byteLength > INLINE_SYNC_OBJECT_SAFE_BYTES) {
throw new TypeError("Sync readObject inline payload exceeds the inline limit");
}
if (decoded < value.byteLength) {
throw new TypeError("Sync readObject data is shorter than byteLength");
}
} else {
throw new TypeError("Sync readObject result encoding is invalid");
}
if (value.revision !== undefined) assertSyncRevision(value.revision);
} else if (value.found !== false) {
throw new TypeError("Sync readObject found flag is invalid");
}
} else if (operation === "writeObject") {
assertDefinition("SyncWriteObjectResult", value, "Sync writeObject result");
if (typeof value.created !== "boolean") throw new TypeError("Sync writeObject created flag is invalid");
if (value.revision !== undefined) assertSyncRevision(value.revision);
} else if (operation === "deleteObject") {
assertDefinition("SyncDeleteObjectResult", value, "Sync deleteObject result");
if (typeof value.deleted !== "boolean") throw new TypeError("Sync deleteObject deleted flag is invalid");
} else {
throw new TypeError(`Unsupported sync operation result: ${operation}`);
}
assertBoundedJson(value, `Sync ${operation} result`);
return freezeJson(value);
}
function decodeBase64Bytes(data, expectedLength, label) {
if (typeof data !== "string") throw invalidArgument(`${label} data is invalid`);
let bytes;
try {
bytes = Buffer.from(data, "base64");
} catch {
throw invalidArgument(`${label} data is not valid base64`);
}
if (bytes.byteLength !== expectedLength) {
throw invalidArgument(`${label} byteLength does not match decoded data`);
}
return bytes;
}
class PluginExtensionProviderService {
constructor(options) {
if (!options?.contributionService || !options?.permissionEngine || !options?.runtimeSupervisor
|| !options?.rpcRegistry || !options?.leaseStore) {
throw new TypeError("Extension Provider service requires contribution, permission, runtime, RPC, and secret lease services");
}
this.contributionService = options.contributionService;
this.permissionEngine = options.permissionEngine;
this.runtimeSupervisor = options.runtimeSupervisor;
this.leaseStore = options.leaseStore;
this.maxImportRecordBytes = options.maxImportRecordBytes ?? MAX_IMPORT_RECORD_BYTES;
if (!Number.isSafeInteger(this.maxImportRecordBytes)
|| this.maxImportRecordBytes < 1
|| this.maxImportRecordBytes > MAX_IMPORT_RECORD_BYTES) {
throw new TypeError("Extension Provider importer record limit is invalid");
}
this.expectations = new Map();
this.sessions = new Map();
this.streamRegistration = options.rpcRegistry.registerIncomingStream((stream, context) => (
this.acceptIncomingStream(stream, context)
));
this.runtimeRegistration = this.runtimeSupervisor.onDidChangeRuntime?.((event) => {
if (["running", "starting"].includes(event.status)) return;
for (const [sessionId, session] of this.sessions) {
if (session.identity.runtimeId === event.runtimeId || session.identity.pluginId === event.pluginId) {
this.closeSessionLocal(sessionId, new PluginRpcError(RPC_ERRORS.unavailable, "Plugin connection runtime stopped"));
}
}
});
}
listProviders(options = {}) {
const kind = assertKind(options.kind);
return freezeJson(this.contributionService.listProviders({ kind, locale: options.locale }));
}
async activate(providerId, kind, signal) {
const activationOperation = Promise.resolve(this.contributionService.activateProvider(providerId));
const activation = signal ? await raceWithAbort(activationOperation, signal) : await activationOperation;
if (activation.provider.kind !== kind) {
throw new PluginRpcError(RPC_ERRORS.failedPrecondition, "Extension Provider kind changed during activation");
}
const identity = normalizeIdentity(activation);
await this.permissionEngine.authorize(runtimeContext(activation, identity, signal), {
permission: PROVIDER_PERMISSIONS[kind],
resources: [activation.provider.id],
reason: `Use ${activation.provider.id} as a ${kind} Provider`,
operationId: `${kind}:${activation.provider.id}`,
});
return { activation, identity };
}
async invoke(params, options = {}) {
const kind = assertKind(params?.kind);
const operation = assertOperation(kind, params?.operation);
const deadlineMs = normalizeDeadlineMs(params?.deadlineMs);
const requestId = params?.requestId ?? `provider-${randomUUID()}`;
const request = {
providerId: params?.providerId,
operation,
requestId,
...(params?.payload === undefined ? {} : { payload: assertBoundedJson(params.payload, "Extension Provider payload") }),
deadlineMs,
};
try { assertProviderRequest(request); }
catch (error) { throw invalidArgument(error?.message ?? "Extension Provider request is invalid"); }
const { activation, identity } = options.activation ?? await this.activate(request.providerId, kind, options.signal);
const configuration = request.payload?.configuration;
if (activation.provider.configurationSchema !== undefined && configuration !== undefined) {
try {
const schema = kind === "sync"
? schemaWithoutSyncSecretRequirements(activation.provider.configurationSchema)
: activation.provider.configurationSchema;
compileRestrictedJsonSchema(schema)(configuration);
} catch (error) {
throw invalidArgument(`Provider configuration failed host schema validation: ${error?.message ?? error}`);
}
}
const rawResult = await this.runtimeSupervisor.request(activation.plugin.id, "provider.invoke", {
...request,
kind,
}, {
signal: options.signal,
timeoutMs: deadlineMs,
expectedIdentity: identity,
});
const result = validateProviderResult(rawResult, requestId);
try {
if (kind === "connection") return assertConnectionResult(operation, result);
if (kind === "authentication") return assertAuthenticationResult(result);
if (kind === "importer" && operation === "detect") return assertImporterDetectResult(result);
if (kind === "sync") return assertSyncResult(operation, result);
return freezeJson(assertBoundedJson(result, "Extension Provider result"));
} catch (error) {
throw new PluginRpcError(RPC_ERRORS.dataLoss, `Extension Provider result failed validation: ${error?.message ?? error}`);
}
}
expectIncoming(identity, streamId, bind, signal, timeoutMs) {
assertString(streamId, "Plugin stream ID", 128);
const key = identityKey(identity, streamId);
if (this.expectations.has(key)) throw new PluginRpcError(RPC_ERRORS.alreadyExists, "Plugin stream expectation already exists");
let timer;
let abortListener;
const promise = new Promise((resolve, reject) => {
const cleanup = () => {
clearTimeout(timer);
signal?.removeEventListener("abort", abortListener);
};
const expectation = {
bind,
resolve: (value) => { cleanup(); resolve(value); },
reject: (error) => { cleanup(); reject(error); },
};
this.expectations.set(key, expectation);
timer = setTimeout(() => {
if (this.expectations.get(key) !== expectation) return;
this.expectations.delete(key);
expectation.reject(new PluginRpcError(RPC_ERRORS.deadlineExceeded, "Plugin output stream did not open"));
}, timeoutMs);
timer.unref?.();
abortListener = () => {
if (this.expectations.get(key) !== expectation) return;
this.expectations.delete(key);
expectation.reject(signal.reason ?? new PluginRpcError(RPC_ERRORS.cancelled, "Plugin stream was cancelled"));
};
signal?.addEventListener("abort", abortListener, { once: true });
});
return { key, promise, cancel: (error) => {
const expectation = this.expectations.get(key);
if (!expectation) return;
this.expectations.delete(key);
expectation.reject(error);
} };
}
async acceptIncomingStream(stream, context) {
const key = identityKey(context, stream.streamId);
const expectation = this.expectations.get(key);
if (!expectation) return false;
this.expectations.delete(key);
try {
const value = await expectation.bind(stream, context);
expectation.resolve(value);
return true;
} catch (error) {
expectation.reject(error);
throw error;
}
}
async openConnection(params, options = {}) {
const providerId = assertString(params?.providerId, "Connection Provider ID");
const configuration = freezeJson(assertBoundedJson(
params?.configuration === undefined ? {} : params.configuration,
"Connection configuration",
));
const columns = params?.columns;
const rows = params?.rows;
if (!Number.isInteger(columns) || columns < 1 || columns > 16_384
|| !Number.isInteger(rows) || rows < 1 || rows > 16_384) {
throw invalidArgument("Plugin connection dimensions are invalid");
}
const activation = await this.activate(providerId, "connection", options.signal);
const sessionId = assertString(params?.sessionId ?? `plugin-session-${randomUUID()}`, "Plugin session ID", 128);
if (this.sessions.has(sessionId)) throw new PluginRpcError(RPC_ERRORS.alreadyExists, "Plugin session already exists");
const sessionOwner = options.sessionOwner ?? Symbol(`plugin-connection:${sessionId}`);
const operationId = `connection:${randomUUID()}`;
const inputStreamId = `${operationId}:input`;
const outputStreamId = `${operationId}:output`;
let outputClosed = null;
const expected = this.expectIncoming(activation.identity, outputStreamId, async (stream) => {
stream.bind({
onChunk: async (chunk, release) => {
if (chunk.encoding !== "binary") throw new Error("Plugin connection output must be binary");
await options.onData?.(chunk.bytes);
release();
},
onClose: (reason) => {
outputClosed = reason ?? "closed";
this.closeSessionLocal(sessionId, undefined, sessionOwner);
return options.onOutputClose?.(reason);
},
});
return stream;
}, options.signal, normalizeDeadlineMs(params?.deadlineMs));
const request = this.invoke({
providerId,
kind: "connection",
operation: "open",
payload: {
operationId,
configuration,
columns,
rows,
inputStreamId,
outputStreamId,
windowBytes: STREAM_WINDOW_BYTES,
...(params.credential === undefined ? {} : { credential: params.credential }),
...(params.authenticationProviderId === undefined ? {} : { authenticationProviderId: params.authenticationProviderId }),
},
deadlineMs: params?.deadlineMs,
}, { ...options, activation });
let output;
let input;
try {
output = await waitForStreamOrRequestFailure(expected.promise, request);
input = await this.runtimeSupervisor.openStream(activation.activation.plugin.id, inputStreamId, STREAM_WINDOW_BYTES, {
expectedIdentity: activation.identity,
});
const result = await request;
if (outputClosed != null) throw new PluginRpcError(RPC_ERRORS.unavailable, "Plugin connection output closed during startup");
const session = Object.freeze({
sessionId,
providerId,
pluginConnectionId: result.connectionId,
activation: activation.activation,
identity: activation.identity,
input,
output,
configuration,
sessionOwner,
});
this.sessions.set(sessionId, session);
return freezeJson({ sessionId, providerId, status: result.status, diagnostics: result.diagnostics ?? [] });
} catch (error) {
expected.cancel(error);
try { input?.cancel?.(); } catch {}
try { output?.cancel?.(); } catch {}
throw error;
} finally {
this.leaseStore.revokeOperation(activation.identity.pluginId, operationId);
}
}
getSession(sessionId, sessionOwner) {
const session = this.sessions.get(assertString(sessionId, "Plugin session ID", 128));
if (!session) throw new PluginRpcError(RPC_ERRORS.notFound, "Plugin connection session was not found");
if (sessionOwner !== undefined && session.sessionOwner !== sessionOwner) {
throw new PluginRpcError(RPC_ERRORS.notFound, "Plugin connection session was replaced");
}
return session;
}
async write(sessionId, data, sessionOwner) {
const session = this.getSession(sessionId, sessionOwner);
const bytes = typeof data === "string"
? new TextEncoder().encode(data)
: data instanceof Uint8Array
? data
: new Uint8Array(data);
if (bytes.byteLength === 0) {
await session.input.write(bytes);
return;
}
for (let offset = 0; offset < bytes.byteLength; offset += STREAM_WINDOW_BYTES) {
await session.input.write(bytes.subarray(offset, Math.min(bytes.byteLength, offset + STREAM_WINDOW_BYTES)));
}
}
async control(sessionId, operation, payload = {}, options = {}) {
const session = this.getSession(sessionId, options.sessionOwner);
const operationId = `connection:${operation}:${randomUUID()}`;
const isClose = operation === "close";
if (isClose && this.sessions.get(sessionId) === session) {
this.sessions.delete(sessionId);
}
try {
const result = await this.invoke({
providerId: session.providerId,
kind: "connection",
operation,
payload: { ...payload, connectionId: session.pluginConnectionId, operationId },
deadlineMs: options.deadlineMs,
}, { ...options, activation: { activation: session.activation, identity: session.identity } });
return result;
} finally {
this.leaseStore.revokeOperation(session.identity.pluginId, operationId);
if (isClose) this.disposeSession(session);
}
}
disposeSession(session, error) {
try { session.input.cancel?.(); } catch {}
try { session.output.cancel?.(error); } catch {}
}
closeSessionLocal(sessionId, error, sessionOwner) {
const session = this.sessions.get(sessionId);
if (!session) return false;
if (sessionOwner !== undefined && session.sessionOwner !== sessionOwner) return false;
this.sessions.delete(sessionId);
this.disposeSession(session, error);
return true;
}
async authenticate(params, requestChallenge, options = {}) {
if (typeof requestChallenge !== "function") throw invalidArgument("Authentication challenge renderer is unavailable");
const providerId = assertString(params?.providerId, "Authentication Provider ID");
const operationId = `authentication:${randomUUID()}`;
const activation = await this.activate(providerId, "authentication", options.signal);
const connectionProviderId = assertString(params?.connectionProviderId, "Connection Provider ID");
const connectionProvider = this.contributionService.listProviders({ kind: "connection" })
.find((entry) => entry.provider.id === connectionProviderId);
if (!connectionProvider) throw new PluginRpcError(RPC_ERRORS.notFound, "Connection Provider was not found");
let completed = false;
try {
let result = await this.invoke({
providerId,
kind: "authentication",
operation: "begin",
payload: {
operationId,
connectionProviderId,
configuration: freezeJson(assertBoundedJson(
params?.configuration === undefined ? {} : params.configuration,
"Authentication configuration",
)),
...(params.credential === undefined ? {} : { credential: params.credential }),
},
}, { ...options, activation });
for (let index = 0; result.status === "challenge"; index += 1) {
if (index >= MAX_AUTH_CHALLENGES) throw new PluginRpcError(RPC_ERRORS.resourceExhausted, "Authentication challenge limit exceeded");
let response = assertAuthenticationResponse(
result.challenge,
await requestChallenge(result.challenge, { signal: options.signal }),
);
if (result.challenge.kind === "password" || result.challenge.kind === "otp") {
const secretValue = response;
response = this.leaseStore.issue({
pluginId: activation.identity.pluginId,
runtimeId: activation.identity.runtimeId,
credential: Object.freeze({
kind: "authentication-challenge",
challengeId: result.challenge.id,
}),
operationId,
purpose: `Respond to ${result.challenge.kind} challenge ${result.challenge.id}`,
ttlMs: AUTHENTICATION_SECRET_LEASE_TTL_MS,
signal: options.signal,
resolveSecret: () => secretValue,
});
}
result = await this.invoke({
providerId,
kind: "authentication",
operation: "respond",
payload: { operationId, challengeId: result.challenge.id, response },
}, { ...options, activation });
}
if (result.status === "authenticated" && result.credential?.kind === "secret"
&& connectionProvider.pluginId !== activation.identity.pluginId) {
throw new PluginRpcError(
RPC_ERRORS.failedPrecondition,
"Cross-plugin authentication must return a host-owned CredentialRef",
);
}
completed = true;
return result;
} catch (error) {
if (!completed) {
const cleanupController = new AbortController();
const cleanupTimer = setTimeout(() => {
cleanupController.abort(new DOMException("Authentication cancellation timed out", "TimeoutError"));
}, AUTHENTICATION_CANCEL_DEADLINE_MS);
cleanupTimer.unref?.();
try {
await this.invoke({
providerId,
kind: "authentication",
operation: "cancel",
payload: { operationId },
deadlineMs: AUTHENTICATION_CANCEL_DEADLINE_MS,
}, { activation, signal: cleanupController.signal });
} catch {
// Cancellation is best-effort, but it must use a fresh signal so the
// runtime has a bounded chance to release operation-owned resources.
} finally {
clearTimeout(cleanupTimer);
}
}
throw error;
} finally {
this.leaseStore.revokeOperation(activation.identity.pluginId, operationId);
}
}
async detectImporter(params, options = {}) {
const bytes = params?.sample instanceof Uint8Array ? params.sample : new Uint8Array(params?.sample ?? []);
if (bytes.byteLength < 1 || bytes.byteLength > MAX_IMPORT_BYTES) throw invalidArgument("Importer sample size is invalid");
return this.invoke({
providerId: params.providerId,
kind: "importer",
operation: "detect",
payload: buildImporterDetectPayload({
bytes,
fileName: params.fileName,
mediaType: params.mediaType,
}),
deadlineMs: params.deadlineMs,
}, options);
}
async parseImporter(params, options = {}) {
const providerId = assertString(params?.providerId, "Importer Provider ID");
const bufferedSource = params?.data === undefined
? null
: params.data instanceof Uint8Array ? params.data : new Uint8Array(params.data ?? []);
const sourceByteLength = bufferedSource?.byteLength ?? params?.sourceByteLength;
if (!Number.isSafeInteger(sourceByteLength) || sourceByteLength < 1 || sourceByteLength > MAX_IMPORT_BYTES) {
throw invalidArgument("Importer input size is invalid");
}
const source = bufferedSource ? [bufferedSource] : params?.source;
if (!source || (typeof source[Symbol.asyncIterator] !== "function"
&& typeof source[Symbol.iterator] !== "function")) {
throw invalidArgument("Importer input stream is unavailable");
}
const activation = await this.activate(providerId, "importer", options.signal);
const deadlineMs = normalizeDeadlineMs(params?.deadlineMs);
const deadlineAt = performance.now() + deadlineMs;
const operationId = `importer:${randomUUID()}`;
const inputStreamId = `${operationId}:input`;
const outputStreamId = `${operationId}:output`;
const records = [];
const decoder = new TextDecoder("utf-8", { fatal: true });
let pending = "";
let outputBytes = 0;
let resolveOutputDone;
let rejectOutputDone;
const outputDone = new Promise((resolve, reject) => {
resolveOutputDone = resolve;
rejectOutputDone = reject;
});
void outputDone.catch(() => {});
const assertPendingLineWithinLimit = () => {
const line = pending.endsWith("\r") ? pending.slice(0, -1) : pending;
if (Buffer.byteLength(line, "utf8") > this.maxImportRecordBytes) {
throw new PluginRpcError(RPC_ERRORS.resourceExhausted, "Importer output exceeds its record limits");
}
};
const consumeLines = (final = false) => {
const lines = pending.split("\n");
pending = final ? "" : lines.pop();
for (const raw of lines) {
const line = raw.endsWith("\r") ? raw.slice(0, -1) : raw;
if (!line) continue;
if (Buffer.byteLength(line, "utf8") > this.maxImportRecordBytes || records.length >= MAX_IMPORT_RECORDS) {
throw new PluginRpcError(RPC_ERRORS.resourceExhausted, "Importer output exceeds its record limits");
}
const record = normalizeImportRecord(JSON.parse(line));
records.push(record);
if (record.type === "progress") options.onProgress?.(record);
}
};
const expected = this.expectIncoming(activation.identity, outputStreamId, async (stream) => {
stream.bind({
onChunk: (chunk, release) => {
try {
if (chunk.encoding !== "binary") throw new Error("Importer output must be UTF-8 JSONL bytes");
outputBytes += chunk.bytes.byteLength;
if (outputBytes > MAX_IMPORT_OUTPUT_BYTES) throw new PluginRpcError(RPC_ERRORS.resourceExhausted, "Importer output is too large");
pending += decoder.decode(chunk.bytes, { stream: true });
consumeLines(false);
assertPendingLineWithinLimit();
release();
} catch (error) {
rejectOutputDone(error);
throw error;
}
},
onClose: (reason) => {
try {
if (reason !== "end") throw new PluginRpcError(RPC_ERRORS.dataLoss, "Importer output stream did not end normally");
pending += decoder.decode();
if (pending) pending += "\n";
consumeLines(true);
resolveOutputDone();
} catch (error) {
rejectOutputDone(error);
}
},
});
return stream;
}, options.signal, deadlineMs);
const request = this.invoke({
providerId,
kind: "importer",
operation: "parse",
payload: {
operationId,
...(params.fileName ? { fileName: assertString(params.fileName, "Import file name", 1_024) } : {}),
...(params.mediaType ? { mediaType: assertString(params.mediaType, "Import media type", 256) } : {}),
inputStreamId,
outputStreamId,
windowBytes: STREAM_WINDOW_BYTES,
...(params.options === undefined ? {} : { options: assertBoundedJson(params.options, "Importer options") }),
},
deadlineMs,
}, { ...options, activation });
const waitForImporterIo = (promise, message) => waitUntilDeadline(
waitForStreamOrRequestFailure(promise, request),
deadlineAt,
options.signal,
message,
);
let output;
let input;
let sourceIterator;
let sourceCompleted = false;
try {
sourceIterator = typeof source[Symbol.asyncIterator] === "function"
? source[Symbol.asyncIterator]()
: source[Symbol.iterator]();
output = await waitUntilDeadline(
waitForStreamOrRequestFailure(expected.promise, request),
deadlineAt,
options.signal,
"Importer output stream exceeded its deadline",
);
input = await waitForImporterIo(
this.runtimeSupervisor.openStream(activation.activation.plugin.id, inputStreamId, STREAM_WINDOW_BYTES, {
expectedIdentity: activation.identity,
}),
"Importer input stream exceeded its deadline",
);
let inputBytes = 0;
while (true) {
const next = await waitForImporterIo(
Promise.resolve(sourceIterator.next()),
"Importer input source exceeded its deadline",
);
if (next.done) {
sourceCompleted = true;
break;
}
const rawChunk = next.value;
const chunk = rawChunk instanceof Uint8Array ? rawChunk : new Uint8Array(rawChunk);
inputBytes += chunk.byteLength;
if (chunk.byteLength < 1 || inputBytes > sourceByteLength) {
throw new PluginRpcError(RPC_ERRORS.dataLoss, "Importer input stream changed while it was being read");
}
for (let offset = 0; offset < chunk.byteLength; offset += STREAM_WINDOW_BYTES) {
await waitForImporterIo(
input.write(chunk.subarray(offset, Math.min(chunk.byteLength, offset + STREAM_WINDOW_BYTES))),
"Importer input stream exceeded its deadline",
);
}
}
if (inputBytes !== sourceByteLength) {
throw new PluginRpcError(RPC_ERRORS.dataLoss, "Importer input stream ended at an unexpected size");
}
await waitForImporterIo(input.end(), "Importer input stream exceeded its deadline");
const result = await waitUntilDeadline(
request,
deadlineAt,
options.signal,
"Importer Provider exceeded its deadline",
);
await waitUntilDeadline(
outputDone,
deadlineAt,
options.signal,
"Importer output stream exceeded its deadline",
);
assertDefinition("ImporterParseResult", result, "Importer completion result");
if (!result || typeof result !== "object" || Array.isArray(result)
|| !Number.isSafeInteger(result.parsed) || result.parsed < 0
|| !Number.isSafeInteger(result.warnings) || result.warnings < 0
|| !Number.isSafeInteger(result.errors) || result.errors < 0) {
throw new PluginRpcError(RPC_ERRORS.dataLoss, "Importer completion result is invalid");
}
const observed = records.reduce((counts, record) => {
if (record.type === "draft") counts.parsed += 1;
else if (record.type === "warning") counts.warnings += 1;
else if (record.type === "error") counts.errors += 1;
return counts;
}, { parsed: 0, warnings: 0, errors: 0 });
if (result.parsed !== observed.parsed
|| result.warnings !== observed.warnings
|| result.errors !== observed.errors) {
throw new PluginRpcError(RPC_ERRORS.dataLoss, "Importer completion counts do not match its streamed records");
}
return freezeJson({ providerId, result, records });
} catch (error) {
expected.cancel(error);
if (!sourceCompleted && typeof sourceIterator?.return === "function") {
try { void Promise.resolve(sourceIterator.return()).catch(() => {}); } catch {}
}
try { input?.cancel?.(); } catch {}
try { output?.cancel?.(); } catch {}
throw error;
} finally {
this.leaseStore.revokeOperation(activation.identity.pluginId, operationId);
}
}
async connectSync(params, options = {}) {
const providerId = assertString(params?.providerId, "Sync Provider ID");
const operationId = `sync:connect:${randomUUID()}`;
const activation = options.activation ?? await this.activate(providerId, "sync", options.signal);
try {
return await this.invoke({
providerId,
kind: "sync",
operation: "connect",
payload: {
operationId,
configuration: freezeJson(assertBoundedJson(
params?.configuration === undefined ? {} : params.configuration,
"Sync configuration",
)),
...(params.credential === undefined ? {} : { credential: params.credential }),
},
deadlineMs: params?.deadlineMs,
}, { ...options, activation });
} finally {
this.leaseStore.revokeOperation(activation.identity.pluginId, operationId);
}
}
async disconnectSync(params, options = {}) {
return this.invoke({
providerId: assertString(params?.providerId, "Sync Provider ID"),
kind: "sync",
operation: "disconnect",
payload: {
operationId: `sync:disconnect:${randomUUID()}`,
},
deadlineMs: params?.deadlineMs,
}, options);
}
async getSyncAccount(params, options = {}) {
return this.invoke({
providerId: assertString(params?.providerId, "Sync Provider ID"),
kind: "sync",
operation: "getAccount",
payload: {
operationId: `sync:getAccount:${randomUUID()}`,
},
deadlineMs: params?.deadlineMs,
}, options);
}
async getSyncCapabilities(params, options = {}) {
return this.invoke({
providerId: assertString(params?.providerId, "Sync Provider ID"),
kind: "sync",
operation: "getCapabilities",
payload: {
operationId: `sync:getCapabilities:${randomUUID()}`,
},
deadlineMs: params?.deadlineMs,
}, options);
}
async readSyncObject(params, options = {}) {
const providerId = assertString(params?.providerId, "Sync Provider ID");
const key = assertSyncObjectKey(params?.key);
const deadlineMs = normalizeDeadlineMs(params?.deadlineMs);
const deadlineAt = performance.now() + deadlineMs;
const activation = options.activation ?? await this.activate(providerId, "sync", options.signal);
const operationId = `sync:read:${randomUUID()}`;
// Always offer an output stream so large encrypted objects can leave the
// JSON control plane; small objects may still return inline base64.
const outputStreamId = `${operationId}:output`;
const chunks = [];
let totalBytes = 0;
let resolveOutputDone;
let rejectOutputDone;
let outputSettled = false;
const outputDone = new Promise((resolve, reject) => {
resolveOutputDone = (value) => {
if (outputSettled) return;
outputSettled = true;
resolve(value);
};
rejectOutputDone = (error) => {
if (outputSettled) return;
outputSettled = true;
reject(error);
};
});
void outputDone.catch(() => {});
let acceptedStream = null;
const expected = this.expectIncoming(activation.identity, outputStreamId, async (stream) => {
acceptedStream = stream;
stream.bind({
onChunk: (chunk, release) => {
try {
if (chunk.encoding !== "binary") throw new Error("Sync object stream must be binary");
totalBytes += chunk.bytes.byteLength;
if (totalBytes > MAX_SYNC_OBJECT_BYTES) {
throw new PluginRpcError(RPC_ERRORS.resourceExhausted, "Sync object exceeds its size limit");
}
chunks.push(Buffer.from(chunk.bytes));
release();
} catch (error) {
rejectOutputDone(error);
throw error;
}
},
onClose: (reason) => {
try {
if (reason !== "end") {
throw new PluginRpcError(RPC_ERRORS.dataLoss, "Sync object stream did not end normally");
}
resolveOutputDone(Buffer.concat(chunks));
} catch (error) {
rejectOutputDone(error);
}
},
});
return stream;
}, options.signal, deadlineMs);
const request = this.invoke({
providerId,
kind: "sync",
operation: "readObject",
payload: {
key,
operationId,
outputStreamId,
windowBytes: STREAM_WINDOW_BYTES,
},
deadlineMs,
}, { ...options, activation });
const cancelAcceptedStream = (error) => {
try {
if (acceptedStream && typeof acceptedStream.cancel === "function") {
acceptedStream.cancel(error);
}
} catch {
// Best-effort containment after timeout/failure.
}
};
try {
const result = await waitUntilDeadline(
request,
deadlineAt,
options.signal,
"Sync readObject exceeded its deadline",
);
if (result.found !== true) {
// Swallow the cancelled expectation; inline/not-found paths intentionally
// do not consume the pre-opened output stream.
void expected.promise.catch(() => {});
expected.cancel(new PluginRpcError(RPC_ERRORS.cancelled, "Sync object not found"));
cancelAcceptedStream(new PluginRpcError(RPC_ERRORS.cancelled, "Sync object not found"));
return Object.freeze({ found: false, key, bytes: null, revision: undefined });
}
if (result.streamed === true) {
await waitUntilDeadline(
waitForStreamOrRequestFailure(expected.promise, request),
deadlineAt,
options.signal,
"Sync readObject stream exceeded its deadline",
);
const bytes = await waitUntilDeadline(
outputDone,
deadlineAt,
options.signal,
"Sync readObject stream exceeded its deadline",
);
if (bytes.byteLength !== result.byteLength) {
throw new PluginRpcError(RPC_ERRORS.dataLoss, "Sync readObject stream size does not match byteLength");
}
// Bytes are mutable buffers; freeze only the metadata envelope.
return Object.freeze({
found: true,
key,
bytes: new Uint8Array(bytes),
revision: result.revision,
contentType: result.contentType,
});
}
// Inline base64 path — cancel unused stream expectation.
void expected.promise.catch(() => {});
expected.cancel(new PluginRpcError(RPC_ERRORS.cancelled, "Sync object returned inline"));
cancelAcceptedStream(new PluginRpcError(RPC_ERRORS.cancelled, "Sync object returned inline"));
const bytes = decodeBase64Bytes(result.data, result.byteLength, "Sync readObject");
return Object.freeze({
found: true,
key,
bytes: new Uint8Array(bytes),
revision: result.revision,
contentType: result.contentType,
});
} catch (error) {
void expected?.promise?.catch?.(() => {});
expected?.cancel?.(error);
cancelAcceptedStream(error);
throw error;
}
}
async writeSyncObject(params, options = {}) {
const providerId = assertString(params?.providerId, "Sync Provider ID");
const key = assertSyncObjectKey(params?.key);
const rawBytes = params?.bytes instanceof Uint8Array
? params.bytes
: params?.bytes == null
? null
: new Uint8Array(params.bytes);
if (!rawBytes) throw invalidArgument("Sync writeObject bytes are required");
if (rawBytes.byteLength > MAX_SYNC_OBJECT_BYTES) {
throw invalidArgument("Sync writeObject payload exceeds the size limit");
}
const deadlineMs = normalizeDeadlineMs(params?.deadlineMs);
const deadlineAt = performance.now() + deadlineMs;
const activation = options.activation ?? await this.activate(providerId, "sync", options.signal);
const operationId = `sync:write:${randomUUID()}`;
const useStream = rawBytes.byteLength > INLINE_SYNC_OBJECT_SAFE_BYTES || params?.preferStream === true;
const inputStreamId = useStream ? `${operationId}:input` : undefined;
const expectedRevision = params?.expectedRevision === undefined
? undefined
: params.expectedRevision === null
? null
: assertSyncRevision(params.expectedRevision, "Sync expectedRevision");
const request = this.invoke({
providerId,
kind: "sync",
operation: "writeObject",
payload: {
key,
operationId,
byteLength: rawBytes.byteLength,
...(expectedRevision === undefined ? {} : { expectedRevision }),
...(useStream
? { inputStreamId, windowBytes: STREAM_WINDOW_BYTES }
: {
encoding: "base64",
data: Buffer.from(rawBytes).toString("base64"),
}),
},
deadlineMs,
}, { ...options, activation });
let input;
try {
if (useStream) {
input = await waitUntilDeadline(
waitForStreamOrRequestFailure(
this.runtimeSupervisor.openStream(activation.activation.plugin.id, inputStreamId, STREAM_WINDOW_BYTES, {
expectedIdentity: activation.identity,
}),
request,
),
deadlineAt,
options.signal,
"Sync writeObject input stream exceeded its deadline",
);
for (let offset = 0; offset < rawBytes.byteLength; offset += STREAM_WINDOW_BYTES) {
await waitUntilDeadline(
input.write(rawBytes.subarray(offset, Math.min(rawBytes.byteLength, offset + STREAM_WINDOW_BYTES))),
deadlineAt,
options.signal,
"Sync writeObject input stream exceeded its deadline",
);
}
await waitUntilDeadline(
input.end(),
deadlineAt,
options.signal,
"Sync writeObject input stream exceeded its deadline",
);
}
return await waitUntilDeadline(
request,
deadlineAt,
options.signal,
"Sync writeObject exceeded its deadline",
);
} catch (error) {
try { input?.cancel?.(); } catch {}
throw error;
}
}
async deleteSyncObject(params, options = {}) {
return this.invoke({
providerId: assertString(params?.providerId, "Sync Provider ID"),
kind: "sync",
operation: "deleteObject",
payload: {
key: assertSyncObjectKey(params?.key),
operationId: `sync:delete:${randomUUID()}`,
...(params?.expectedRevision === undefined
? {}
: { expectedRevision: assertSyncRevision(params.expectedRevision, "Sync expectedRevision") }),
},
deadlineMs: params?.deadlineMs,
}, options);
}
shutdown() {
this.streamRegistration?.dispose();
this.runtimeRegistration?.dispose();
for (const sessionId of [...this.sessions.keys()]) this.closeSessionLocal(sessionId);
const error = new PluginRpcError(RPC_ERRORS.unavailable, "Extension Provider service stopped");
for (const expectation of this.expectations.values()) expectation.reject(error);
this.expectations.clear();
}
}
module.exports = {
DEFAULT_DEADLINE_MS,
EXTENSION_PROVIDER_KINDS,
INLINE_SYNC_OBJECT_BYTES,
INLINE_SYNC_OBJECT_SAFE_BYTES,
MAX_IMPORT_BYTES,
MAX_IMPORT_RECORDS,
MAX_SYNC_OBJECT_BYTES,
PluginExtensionProviderService,
STREAM_WINDOW_BYTES,
assertSyncResult,
normalizeImportRecord,
};