@detail-dev/sahara

中文文档
0.1.3发布于 2026/10/16Apache-2.0TypeScriptES Module零依赖近 30 天下载 2618

为撒哈拉公共API所打入的TypeScript客户端:生命周期,处决,SSE事件,TCP隧道和SSH材料.

中文使用文档
文档目录12 节
  1. 失职和错误
  2. 创建、准备、执行和关闭
  3. 普查准备页
  4. 事件和预先确认
  5. TCP 隧道
  6. 逻辑沙盒的 SSH 材料
  7. 消费者形象
  8. 来宾文件:有界流和最后收据
  9. 认证和装订检查
  10. 从详细单曲播放
  11. 复兴合同
  12. 测试

@ detail-dev/shahara (英语)

用于撒哈拉公共控制平面 API 和代理数据的 TypeScript 客户端 飞机 20号节点(针对22号节点开发),零运行时间依赖,双轨 建立无害环境管理。 出版和部署的版本必须从发行收据中确定. 这个包暴露了被支持的消费者SDK,而不是操作员CLI.

安装公开发布 npm install @detail-dev/sahara; 预释放使用 @detail-dev/sahara@next。用于本地开发,使用一个包装的归档或工作空间 路径。 维护者通过审查的 SDK 版本发布 SDK 发布,独立于服务/运行时间发布.

沙盒手柄是不透明的字符串(sb_ 之后是16个小写十六进制数字,即使忘记也永远不再使用. 其他64位身份(执行,化身,流位置, 试图先发制人) bigint;线带小数线 并使他们变成正人。 答复文件严格解码: 拒绝未知字段, 匹配服务器的 deny_unknown_fields 合同.

import { SaharaClient } from "@detail-dev/sahara";

const client = new SaharaClient({
    apiBase: "http://control-plane.internal/", // private local example only
    proxyBase: "http://proxy.internal/", // required for files, tunnels and SSH material
    token: process.env.SAHARA_TOKEN!,
    // proxyToken: process.env.SAHARA_PROXY_TOKEN, // when the proxy token differs
});

使用TLS或加密地道进行公交或其他不可信交通. 受限制的可信任集群/VPC链接可能使用平文. 私人运行时间/ 帮助交通 单独的认证和栅栏要求; 该客户端不批准 通过公共无记名标志实现内部所有权。 每个有效的公共标志都有完全的服务使用权.

失职和错误

承认是确认的持久承诺。 超时,倒出 连接,或 5xx 是 含糊不清 请求可能已经提出。 持久性 变异返回一个持久 Ack 改为:createSandbox, (中文(简体) ), exec, (中文(简体) ), closeSandbox, (中文(简体) ), cancelExecution, (中文(简体) ), ackPreemption在相同的呼叫者选择身份下重试或调和 在限定的预算范围内(默认为 retry) (中文(简体) ). 他们从来没有 以新的身份重新发布. heartbeatSandbox 重复只更新缓存 同一逻辑的沙盒,处理预算范围内的瞬时故障;它没有 延长绝对过期或提供持久的ACK。 文件写入执行一次有界限的尝试; mayHaveApplied 记录不清 发布反应,不重复播放或检查.

  • 409 表面为 SaharaConflictError 与服务器的 kind - 包括: 已发现的、身份已与不同意图有关的情况.
  • 401 是致命的(SaharaAuthenticationError从来没有重复过.
  • 404 实值 SaharaNotFoundError。 。 。 .
  • 错误的电线文档是 SaharaContractError (种类) "contract", (中文(简体) ), 从未再审).
  • 呼叫器取消通过 CallOptions 或操作的 查询/绑定 signal;中止的尝试永远不会被重审.

创建、准备、执行和关闭

沙盒形状固定为8 vCPU (8,000 mCPU), 32 GiB RAM (34359738368字节),并有128个GiB稀疏的磁盘(137438953472字节). 没有 资源选择器属于 CreateSandbox; 汇总主机过度承诺是一个 分开的录取政策,不是每个沙盒的上限.

const id = "sb_0000000000000001"; // choose an unused service identity
await client.createSandbox(id, {
    image: 1n, // published image identity available to this service
    expires_at_ms: BigInt(Date.now()) + 3_600_000n,
    metadata: { cohort: "detail-five" },
});

let sandbox = await client.getSandbox(id);
while (sandbox.phase === "Pending" || sandbox.phase === "Booting") {
    await new Promise((resolve) => setTimeout(resolve, 500));
    sandbox = await client.getSandbox(id);
}
if (sandbox.phase !== "Ready") throw new Error(`sandbox phase: ${sandbox.phase}`);

const executionId = 100n;
await client.exec(id, executionId, {
    command: "/work/run.sh",
    env: { MODE: "eval" },
});
let poll = await client.getExecution(executionId);
while (poll.status === "Accepted") {
    await new Promise((resolve) => setTimeout(resolve, 500));
    poll = await client.getExecution(executionId);
}
if (typeof poll.status !== "object" || !("Completed" in poll.status)) {
    throw new Error("execution did not complete");
}
const exitCode = poll.status.Completed;
const output = await client.readExecutionOutput(id, executionId, {
    stream: "stdout",
    offset: 0n,
    limit: 1_048_576n,
});
// output.bytes is bounded; output.complete means the process domain is quiescent.

对于大输出, 使用一个代理流而不是请求连续切片 :

const output = await client.streamExecutionOutput(id, executionId, { stream: "stdout" });
for await (const bytes of output.chunks) await sink.write(bytes);
const receipt = output.receipt(); // exact length + SHA-256, only after verified trailers

输出仍保留在来宾磁盘上. 代理认证 本地请求, 解析一次运行,然后从一个保留描述符中接取被绑定的块. 主动执行可以提供经过验证的快照 complete === false· ; 完成该快照不能证明进程域为 quescent 。 每个 所请求的拉动有配置的请求超时;调用器处理时间介于 拉不是闲时间 输出方法都不自动重试或退后 到控制飞机的字节路线. readExecutionOutput 最多还收 1 MiB在所配置的每试请求截止日期下. 使用 cancel() 用于 未消耗的流; 放弃一个活式的转发器取消其响应.

匹配的来宾/运行时间二进制必须使用当前保留输出格式, 包括靴子、沙盒、创造、化身和执行。 旧产出 名称不会从新读者处获得创建身份。 部署和 持续产出的采用需要单独的限定.

整个恢复过程的沙盒手柄保持稳定。 服务可持久地约束接受 执行 ID 到他们的第一次运行; 重试从不重瞄准它们 。 文件调用 解决每个请求的一次运行. creation 财务报告和审定财务报表 incarnation 是诊断 字段;心跳和SSH只取逻辑沙盒手柄.

普查准备页

固定前缀普查,要求每页最多500行,并遵循 next_cursor 通过终端页面,包括当该页面为空时:

let cursor: string | undefined;
let through: bigint | undefined;
let position: bigint | undefined;
const readyIds = new Set<string>();
do {
    const page = await client.listSandboxesPage({ phase: "Ready", limit: 500, cursor });
    if (through !== undefined && page.through !== through) throw new Error("cutoff moved");
    if (position !== undefined && page.position !== position) throw new Error("prefix moved");
    through = page.through;
    position = page.position;
    for (const row of page.sandboxes) readyIds.add(row.id);
    cursor = page.next_cursor ?? undefined;
} while (cursor !== undefined);

这是历史性的 承诺前缀观察, 不是证明所有客人 或提供者的能力和详细工作已经过去。 检查精确 身份证, creation, (中文(简体) ), incarnation,固定资源,运行元数据以及 计数,然后保留单独的运行时间和拆卸证据.

事件和预先确认

const controller = new AbortController();
const signal = controller.signal;
for await (const frame of client.events(sandbox.id, { signal })) {
    if (frame.type === "overflow") {
        // cursor fell outside the retained window — reconcile by reading
        continue;
    }
    const event = frame.event;
    if (event.kind === "preemption") {
        await client.ackPreemption(sandbox.id, event.attempt);
    }
    // frame.id is the durable cursor; persist it to resume with
    // client.events(id, { lastEventId: frame.id })
}

执行器将运输故障与 Last-Event-ID,宽容 保存相框,检测出摊位, 结束在干净的关闭上.

TCP 隧道

const stream = await client.connectTcp(sandbox.id, 8080);
stream.write(payload);
stream.on("data", (chunk) => {
    /* ... */
});

或转发回路端口——每个被接受的本地套接字都会得到自己的地道 流线 :

const forward = await client.forwardLocalPort(sandbox.id, 8080);
console.log(`localhost:${forward.port}`);
await forward.stop();

逻辑沙盒的 SSH 材料

const ssh = await client.sshMaterial(sandbox.id);
// { user, host_public_key, port, private_key_pem }
// present only while the resolved runtime is live

消费者形象

for await (const entry of client.listImages()) {
    const image = await client.getImage(entry.id);
    console.log(image.id, image.phase);
    // await client.deleteImage(image.id); // tombstones this shared catalog entry
}
// Public list/get include captured images and operator-imported base images.
// Every accepted public token sees the same catalog; there is no per-token owner.
// Public deletion uses the same catalog operation as operator deletion.
// Image import remains an operator API, not a public client operation.

来宾文件:有界流和最后收据

import { createHash } from "node:crypto";
import { createReadStream, createWriteStream } from "node:fs";
import { stat } from "node:fs/promises";
import { pipeline } from "node:stream/promises";

const local = "/tmp/report.json";
const { size } = await stat(local, { bigint: true });
const hash = createHash("sha256");
for await (const chunk of createReadStream(local, { highWaterMark: 1 << 20 })) {
    hash.update(chunk);
}
const sha256 = hash.digest("hex");
const binding = {
    path: "/home/user/report.json", // inside the default confined guest roots
    length: size,
    sha256,
    mode: "0640",
};
const written = await client.writeFile(id, binding, () =>
    createReadStream(local, { highWaterMark: 1 << 20 }),
);
if (written.size !== size || written.sha256 !== sha256) throw new Error("receipt mismatch");

const download = await client.readFile(id, {
    path: binding.path,
});
await pipeline(download.chunks, createWriteStream("/tmp/roundtrip.json"));
const receipt = download.receipt(); // only available after final HTTP trailers verify
if (receipt.length !== size || receipt.sha256 !== sha256) {
    throw new Error("file changed between upload and download");
}
await client.closeSandbox(id); // logical close; verify physical teardown separately

上传源头在每次明确通话时都会被消耗. 委托人把范围拉大了 拖取并只接受匹配大小/ digest 出版收据 。 丢失的答复 可能隐藏已公布的文件:检查 mayHaveApplied,并决定是否另作决定 独立写作是适当的。 没有公开转移身份或检查 终点. writeFileBytes编号 :writeFileText 处理呼叫者拥有的小型缓冲器; readFileBytes 需求 maxBytes 并在返回前验证终端完整性. 流出大型文件和舞台下载字节直至 receipt() 成功 .

来源必须是 HTTP( S) 根源, 没有用户信息、 路径前缀、 查询或 碎片。 重定向已禁用 。 文件使用代理证书. 普通JSON语 30 秒 15次尝试预算 250毫秒指数 5秒后就关闭了 SSE 重接连接 EOF 和部分 框架来自上一个完全解码光标; 只交付应用程序事件 重新设置其连续失败窗口 。 空成功打开无法重置 . A 类 closed 事件或取消结束订阅。 串流设置包括 - 健康订阅没有终身总上限.

上传有单独的 30分钟 完全尝试的截止日期,包括字节和发布回复。 覆盖 attemptTimeoutMs 在流媒体或字节/文本上传;这绝不允许重放。 普通的JSON电话保留了较短的重试预算.

文件下载使用 retry.requestTimeoutMs (默认时为15秒) 对于单取设置,包括任何拒绝体,然后45秒闲置读取,没有完全健康-流的最后期限. 闲置的截止时间仅在执行者请求字节或终端拖车时运行,因此下游回压不会超过健康的一流. 打电话 cancel() 或中止信号以阻止它; 部分文件永远不会被重放.

认证和装订检查

  • 无效的公共令牌失败 SaharaAuthenticationError;所有有效符号共享服务访问.
  • 重用具有不同不可改变意图冲突的操作ID. 接受的工作从未默默地跟随康复.
  • 烂透了 expected_sha256_before 先决条件不能覆盖已更改的目的地.
  • 路径仍然局限于服务器允许的来宾根.
  • 出处测试包括单试出版,粘糊糊糊的模糊性,坏坏的收据和拖车故障等.

沙盒事件流上的压力观测结果为 event.kind === "pressure" 边框( e)resource, (中文(简体) ), level, (中文(简体) ), cleared, (中文(简体) ), value, (中文(简体) ), threshold, (中文(简体) ), since_ms) (中文(简体) ).

从详细单曲播放

建造焦油球并安装所保留的, 作为文件依赖的源头类文物 :

cd clients/typescript
pnpm install --frozen-lockfile && pnpm check:generated && pnpm build
pnpm pack && pnpm smoke:installed

单位 apps/backend/package.json数字 :

{
    "dependencies": {
        "@detail-dev/sahara": "file:../../vendor/detail-dev-sahara-<VERSION>.tgz"
    }
}

或者销售整个软件包目录并使用 pnpm 工作空间 link: 编号 : file: 找到它的道路。 货船 dist/esm 与 .d.ts 声明。 普通JS消费者必须使用动态 import()。 。 。 .

复兴合同

pnpm generate 运行 cargo xtask contracts 重播和重播时 src/generated/* (类型+代码+操作表). pnpm check:generated 漂移失败; 在服务器侧 API 更改后在 CI 中运行 .

测试

pnpm test    # unit tests + contract suite against owned postgres + valkey
             # containers plus sahara-sim-runtime + sahara-control-plane
             # (binaries built at the repo root; requires Docker)

将套房定位为真正的部署——例如 cargo xtask physical-e2e --hold 集合,它印出每一个这些 设定 :

变量含义
SAHARA_API公共 API 基础; 逗号分离的复制基地被接受, 领导者通过 /metrics
SAHARA_TOKEN公共 API 无记名符号
SAHARA_PROXY代理数据飞机基础(可进行隧道/SSH合同测试)
SAHARA_PROXY_TOKEN当代理无记名符号与 SAHARA_TOKEN
SAHARA_OPERATOR_TOKEN运算符无记名符 (默认为 SAHARA_TOKEN(中文(简体) )
SAHARA_IMAGE_ID已发布图像 ID 沙盒规格应参考
SAHARA_ID_BASE第一个呼叫者选择的沙盒身份( 默认为100000); 重跑时与状态部署相撞

SSE恢复ID是精确的字符串:服务输出出一个20位零加法u64位置,一个连字符,以及一个u64正数加法到至少四个数字. 承诺量较大的批量可释放出更宽的正弦,如一万个. 这两个字段被限定为20个小数位和u64最高(1844644073709551615). 客户端也接受未加限的十进制对等,但呼叫者应当不加修改地通过收到的光标,而不是构造或使光标正常化. 客户端发送时保留其精确的字节 Last-Event-ID; 贴花与JSON DTOs中使用的克尼克十进制字符串不同.

版本与发布记录

@detail-dev/sahara 共发布 5 个版本,首次发布于 2026/10/09;过去 12 个月发布了 5 个版本。

版本发布日期距今
0.1.3latest2026/10/1627 天前
0.1.22026/10/091 个月前
0.1.12026/10/091 个月前
0.1.02026/10/091 个月前
0.0.0-stage2026/10/091 个月前

关于 @detail-dev/sahara 的常见问题

@detail-dev/sahara 是什么?

为撒哈拉公共API所打入的TypeScript客户端:生命周期,处决,SSE事件,TCP隧道和SSH材料.

如何安装 @detail-dev/sahara?

在项目目录运行 npm install @detail-dev/sahara,或使用 pnpm add / yarn add / bun add。

@detail-dev/sahara 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@detail-dev/sahara 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@detail-dev/sahara 需要什么版本的 Node.js?

package.json 的 engines 字段要求 Node.js >=20。版本不符时 npm 默认只给出警告,开启 engine-strict 后会拒绝安装。

@detail-dev/sahara 可以商用吗?

@detail-dev/sahara 使用 Apache-2.0 许可。宽松许可:允许商业使用、修改和再分发,并附带明确的专利授权;分发时需保留许可与 NOTICE 文件,修改过的文件要注明改动。以上为概括,不构成法律意见,请以许可原文为准。

@detail-dev/sahara 的下载量有多少?

根据 npm 官方下载统计,@detail-dev/sahara 在 2026/10/12 – 2026/11/10 这 30 天内被下载 2,618 次,日均约 87 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@detail-dev/sahara 还在维护吗?

最新版本 0.1.3 发布于 2026/10/16(27 天前);至今共发布 5 个版本,其中过去 12 个月发布了 5 个;首次发布于 2026/10/09。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。