Files
NetMesh/electron/plugins/extensionProviderService.cjs

1367 lines
56 KiB
JavaScript
Raw Normal View History

"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,
};