Initial Commit
This commit is contained in:
@@ -0,0 +1,126 @@
|
||||
import {
|
||||
SAND_PERM_CODES,
|
||||
SandboxPermissionError,
|
||||
} from "@suaveplan/api-sandbox-permissions";
|
||||
import type {
|
||||
SerializedRequest,
|
||||
SerializedResponse,
|
||||
} from "@suaveplan/api-types/sandbox-bridge";
|
||||
import {
|
||||
SAND_WASM_BRIDGE_CODES,
|
||||
SandboxWasmBridgeError,
|
||||
} from "../error-codes/error-codes.js";
|
||||
|
||||
/**
|
||||
* Shape the bridge factory threads through its `RequestContext` slot.
|
||||
* Defined locally to avoid leaking the host's `RequestContext<T>`
|
||||
* generic into this utility — both variants need the same five fields
|
||||
* and nothing more.
|
||||
*/
|
||||
export interface RequestContextLike {
|
||||
readonly request: Request;
|
||||
readonly principal: { readonly id: string } | null;
|
||||
readonly tenant: { readonly id: string } | null;
|
||||
readonly capabilities: Readonly<Record<string, unknown>>;
|
||||
readonly requestId: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Marshal a `RequestContextLike` into the wire `SerializedRequest`
|
||||
* shape both variants exchange with their guest plugins.
|
||||
*/
|
||||
export async function marshalContextRequest(
|
||||
ctx: RequestContextLike
|
||||
): Promise<SerializedRequest> {
|
||||
const request = ctx.request;
|
||||
const headers: Array<readonly [string, string]> = [];
|
||||
request.headers.forEach((value, name) => {
|
||||
headers.push([name.toLowerCase(), value] as const);
|
||||
});
|
||||
let bodyBase64 = "";
|
||||
if (request.body !== null) {
|
||||
const buffer = await request.arrayBuffer();
|
||||
bodyBase64 = bytesToBase64UrlNoPad(new Uint8Array(buffer));
|
||||
}
|
||||
return {
|
||||
url: request.url,
|
||||
method: request.method.toUpperCase(),
|
||||
headers,
|
||||
bodyBase64,
|
||||
principalId: (ctx.principal === null
|
||||
? null
|
||||
: ctx.principal.id) as SerializedRequest["principalId"],
|
||||
tenantId: (ctx.tenant === null
|
||||
? null
|
||||
: ctx.tenant.id) as SerializedRequest["tenantId"],
|
||||
capabilities: Object.keys(ctx.capabilities),
|
||||
requestId: ctx.requestId,
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert the wire `SerializedResponse` into a host `Response`. When
|
||||
* the wire response carries an `errorCode`, surface the matching
|
||||
* sandbox error (preserving the canonical `SAND_PERM.DENIED` mapping).
|
||||
*/
|
||||
export function buildHostResponse(
|
||||
wire: SerializedResponse,
|
||||
pluginName: string
|
||||
): Response {
|
||||
if (wire.errorCode !== undefined) {
|
||||
if (wire.errorCode === SAND_PERM_CODES.DENIED) {
|
||||
throw new SandboxPermissionError(
|
||||
SAND_PERM_CODES.DENIED,
|
||||
wire.errorMessage ?? "capability denied",
|
||||
{ context: { plugin: pluginName } }
|
||||
);
|
||||
}
|
||||
throw new SandboxWasmBridgeError(
|
||||
SAND_WASM_BRIDGE_CODES.CAPABILITY_DENIED,
|
||||
wire.errorCode,
|
||||
{
|
||||
context: {
|
||||
plugin: pluginName,
|
||||
sandboxErrorCode: wire.errorCode,
|
||||
sandboxMessage: wire.errorMessage ?? "",
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
const headers = new Headers();
|
||||
for (const [name, value] of wire.headers) headers.append(name, value);
|
||||
const body = base64UrlNoPadToBytes(wire.bodyBase64);
|
||||
return new Response(
|
||||
body.length > 0 ? (body as unknown as BodyInit) : null,
|
||||
{
|
||||
status: wire.status,
|
||||
headers,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function bytesToBase64UrlNoPad(bytes: Uint8Array): string {
|
||||
let binary = "";
|
||||
for (let i = 0; i < bytes.length; i++) {
|
||||
binary += String.fromCharCode(bytes[i] as number);
|
||||
}
|
||||
return btoa(binary)
|
||||
.replace(/\+/g, "-")
|
||||
.replace(/\//g, "_")
|
||||
.replace(/=+$/, "");
|
||||
}
|
||||
|
||||
function base64UrlNoPadToBytes(value: string): Uint8Array {
|
||||
if (value.length === 0) return new Uint8Array(0);
|
||||
const padded = value
|
||||
.replace(/-/g, "+")
|
||||
.replace(/_/g, "/")
|
||||
.padEnd(value.length + ((4 - (value.length % 4)) % 4), "=");
|
||||
const binary = atob(padded);
|
||||
const out = new Uint8Array(binary.length);
|
||||
for (let i = 0; i < binary.length; i++) {
|
||||
out[i] = binary.charCodeAt(i);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
Reference in New Issue
Block a user