# @fungi\.computer/puffball 中文文档

规范地址：https://npmcn.com/package/%40fungi.computer/puffball/

版本：0\.9\.0
npm 元数据更新时间：2026-11-14T11:56:36.157Z
中文译文更新时间：2026-11-15T18:04:37.261Z
软件包许可：MIT

本文由上游 README 翻译，可能存在偏差，请结合原文核对 API、命令与配置。版权与许可归原作者所有。

原始 npm 页面：https://www.npmjs.com/package/%40fungi.computer/puffball
原文对照：https://npmcn.com/package/%40fungi.computer/puffball/?lang=original
项目主页：https://github.com/fungi-computer/puffball#readme
源码仓库：https://github.com/fungi-computer/puffball

## 关键信息（来自 npm 元数据）

- 包体积（解压后）：990 kB，110 个文件
- 模块格式：ES Module
- TypeScript 类型：包内自带
- 运行依赖：6 个
- 首次发布：2026-10-15T07:59:23.114Z
- 最新版本发布：2026-11-14T11:56:35.954Z
- 近 30 天下载量：15470（2026-10-16 至 2026-11-14，npm 官方下载统计）
- 已发布版本数：17（近 12 个月 17 个）

## 中文简介

具有经核实的公开网站交付的不可移动应用程序/发布目录

## 安装

```sh
npm install @fungi.computer/puffball
```

也可以使用：`pnpm add @fungi.computer/puffball`、`yarn add @fungi.computer/puffball`、`bun add @fungi.computer/puffball`

## README

# 弹出球( Puffball )`@fungi.computer/puffball`(中文(简体) )

Puffball是网络应用及其不可改变发行的目录. 它记录 哪些 App 存在,哪些发布它拥有,哪些发布每个公共路线 服务。 在送达或选择任何物品之前,它会读取文物字节和 检查他们与摘要 记录的释放.

当您为他人托管应用程序并需要三个保证时使用它: a Release 创建后永远不变, 路径只服务于您明确发布的 升职,取消的释放 立刻停止服务。 团队安装, 权限、帐单和执行保留在您的主机.

## 安装或设置

```sh
npm install @fungi.computer/puffball
```

一揽子措施是无害环境管理。 这取决于 `effect` 继续 [凯恩](https://npmcn.com/package/%40fungi.computer/cairn/README.md) 用于过渡标识。 核心随处可见 `Promise` 和网络API. 这个 `./cloudflare` 适配器在Cloudflare Workers运行,带有D1和R2捆绑.

你提供两样东西:目录记录商店和文物阅读器 将不透明的文物引用转换为已验证的字节。 包裹载着一个 用于测试和实例的入室商店,以及适用于工人的D1和R2适配器.

## 方法发现

方法可以申报 `hidden: true` 从工作流程中删除维护操作 和特工/MCP的发现。 标记保存在摘要覆盖的App API; 它不授予或否认任何直接召唤的权力。 未标记的方法仍然可见.

API 可选择使用动态工具命名空间 `toolFamily: { name: "flows", list: "listWorkflowTools", call: "runWorkflow" }`。 。 。 . 描述符与释相签. 其列名必须是申报的读数 它的调用必须是可发现的和非外部的,它的名称空间必须 不重复静态方法。 表示它保留了普通的API字节. 库存 和计划元数据 不允许引用权限.

## App 事件声明

App API 可能包括 `events: Record<string, { description, payload }>` 旁边 `methods`。 。 。 . `payload` 是整个发射的 JSON Schema 投影 载荷。 Agar从后端的"Zod事件计划"中生成. 纸球 `appApiSchema` 通过 App 声明和永久释放来保存它 codec,因此对事件元数据的更改会改变"释放"字节和文摘. 一个API 并且没有事件省略字段。 事件申报没有授权;主办方 拥有安装的放行选择,排放录入和路由.

Emiting Apps 请求 `app.events.emit` 能力。 维持 `EVENTS.emit` 行动需要这种能力,明确按 通过东道主现有能力赠款进行安装。 宣布一个事件或 约束不批准排放.

该目录最多接受64个事件,1–128个字符的名字,描述 和同样的20 000字节/深度16个投影 256000字节的总API边界作为方法元数据. 这些是读宣言的 限制,而不是事件交付或保留限制.

## App 调用的主机归属

运行时间中立 `./app-method-runtime` 条目拥有 `HostActor`, (中文(简体) ), `WorkflowAttestation`, (中文(简体) ), `AppMethodCallContext` 和他们所持的经文. 演员是成员,特工或由身份识别的团队钥匙. 东道主可能 附加一个角色( e)`owner` 或者说 `member`和工作流程证明,其中包含 `workflowId`, (中文(简体) ), `runId`, (中文(简体) ), `nodeId` 键入 `armedBy` 演员。 普通电话 删除工作流程字段。 效应调用器解码器代表也参加 语法.

这些事实来自主机的收录,与App方法输入分开. 解码 验证其代表资格;它不授权或核实运行 已激活。 调用和 Git 所有人 重新接收存储的演员并运行之前 效应.

## 原生 SQLite 测试适配器

使用调用者拥有的节点SQLite数据库的公开测试条目:

```ts
import { DatabaseSync } from "node:sqlite";
import { APP_RELEASE_D1_FRESH_SCHEMA } from "@fungi.computer/puffball/cloudflare";
import { sqliteD1 } from "@fungi.computer/puffball/testing";

const sqlite = new DatabaseSync(":memory:");
sqlite.exec("PRAGMA foreign_keys = ON");
sqlite.exec(APP_RELEASE_D1_FRESH_SCHEMA);
const database = sqliteD1(sqlite);
const apps = await database
  .prepare("SELECT app_id FROM app_release_apps")
  .all();
console.log(apps.results); // []
sqlite.close();
```

适配器使用公共监视犬节点SQLite所有者. 准备了 用于测试的语句和原子批次; 它不是一个 Cloudflare 仿真器或 隔离边界。 呼叫者拥有Schema设置和数据库寿命.

## 快开始

这创造了一个App,一个公共路线, 一个释放它的艺术品是小的 HTML页面,再推广"发布"并读取该页面通过路由返回. 它在节点运行 与内幕商店 和一个十行 文物阅读器:

```ts
import { createHash } from "node:crypto";
import {
  createAppReleaseCatalog,
  parseAppId,
  parseArtifactRef,
  parseIdempotencyKey,
  parsePublicSurfaceId,
  parseReleaseId,
  parseSha256Digest,
  parseSourceRevision,
  parseStableRoute,
  type ArtifactMediaType,
  type ArtifactReader,
  type ArtifactRef,
} from "@fungi.computer/puffball";
import { createDeterministicMemoryAppReleaseStore } from "@fungi.computer/puffball/effect";

const sha256 = (bytes: Uint8Array) =>
  parseSha256Digest(
    `sha256:${createHash("sha256").update(bytes).digest("hex")}`,
  );

// Two artifacts: the HTML page, and the App root that points at it.
const html = new TextEncoder().encode("<!doctype html><h1>Hello</h1>");
const htmlRef = parseArtifactRef("artifact:hello.ui");
const root = new TextEncoder().encode(
  JSON.stringify({
    format: "fungi-app-v1",
    ui: {
      artifactRef: htmlRef,
      digest: sha256(html),
      version: "v1",
      byteLength: html.byteLength,
      mediaType: "text/html; charset=utf-8",
    },
  }),
);
const rootRef = parseArtifactRef("artifact:hello.release");

// A tiny artifact reader: look the bytes up, hash them, compare digests.
const files = new Map<
  ArtifactRef,
  { bytes: Uint8Array; mediaType: ArtifactMediaType }
>([
  [htmlRef, { bytes: html, mediaType: "text/html; charset=utf-8" }],
  [rootRef, { bytes: root, mediaType: "application/vnd.fungi.app+json" }],
]);
const artifactReader: ArtifactReader = {
  read: async ({ artifactRef, expectedDigest }) => {
    const file = files.get(artifactRef);
    if (file === undefined) return { kind: "missing" };
    const digest = sha256(file.bytes);
    if (digest !== expectedDigest)
      return { kind: "digest-mismatch", actualDigest: digest };
    return {
      kind: "verified",
      digest,
      mediaType: file.mediaType,
      byteLength: file.bytes.byteLength,
      bytes: {
        byteLength: file.bytes.byteLength,
        copy: () => file.bytes.slice(),
      },
    };
  },
};

const catalog = createAppReleaseCatalog({
  artifactReader,
  store: createDeterministicMemoryAppReleaseStore(),
});

const appId = parseAppId("app_hello");
const publicSurfaceId = parsePublicSurfaceId("ps_hello");
const releaseId = parseReleaseId("rel_hello1");
const route = parseStableRoute("/hello");

await catalog.createApp({ appId });
await catalog.createPublicSurface({
  publicSurfaceId,
  appId,
  stableRoute: route,
  publisher: {
    kind: "platform",
    teamId: "team_hello",
    userId: "user_hello",
    requestKey: publicSurfaceId,
  },
});
await catalog.createRelease({
  releaseId,
  appId,
  artifact: {
    artifactRef: rootRef,
    artifactDigest: sha256(root),
    provenance: {
      sourceRevision: parseSourceRevision(`git:${"a".repeat(40)}`),
      sourceTreeDigest: sha256(new TextEncoder().encode("source tree")),
      buildRecipeDigest: sha256(new TextEncoder().encode("build recipe")),
    },
  },
});
await catalog.verifyReleaseArtifact(releaseId);
await catalog.transition({
  kind: "promotion",
  publicSurfaceId,
  releaseId,
  idempotencyKey: parseIdempotencyKey("idem_hello-promote-1"),
});

const website = await catalog.resolvePublicWebsite(route);
console.log(new TextDecoder().decode(website.bytes.copy()));
// <!doctype html><h1>Hello</h1>
```

## 核心概念

### 记录

| 记录 | 拥有 |
| --- | --- |
| 应用 | 在目录中注册的稳定的全球身份. |
| 释放 | 不可移动的App工艺品参考、文摘和来源/建造来源. |
| 公众表面 | App自有的稳定路线,并有明确的选择版. |
| 过渡收据 | 操作范围选择或撤销的记录结果. |

身份证上写着身份 他们不准许改变这种身份. 每个ID都有固定的语法,例如: `app_<a-z0-9>`, (中文(简体) ), `rel_<a-z0-9>`, (中文(简体) ), `ps_<a-z0-9>`, (中文(简体) ), `artifact:<name>`, (中文(简体) ), `idem_<key>`, (中文(简体) ), `git:<40 hex>`, (中文(简体) ), `sha256:<64 hex>` 和一条单行道 `/hello`解析未信任 根的字符串 `parse*` 函数。 拒绝的值抛出 `AppReleaseInputError` 命名解析器.

### 入境点

根入点出口 `createAppReleaseCatalog`,则 `parse*` 边界 解析器、记录类型、错误类别和App声明词汇. 该目录是一个承诺的外表 一个效果核心。 这个 `./effect` 切入点 暴露了这种核心`createAppReleaseCore`页:1 `decode*` 函数和 记忆商店。 两者使用相同的语法和决定.

### 应用根

释放文物是具有介质类型的 App 根 `application/vnd.fungi.app+json` 财务报告和审定财务报表 `format: "fungi-app-v1"`连接视觉 部分(HTML页或资产清单),可选可持久后端文物, 或两者兼而有之,在一个文摘所覆盖的描述下。 App可以是视觉的,无头的或者 两者,但永不空出. `./app` 建立和剖析 App 根, `./backend` 剖析 后端描述符,以及 `./assets` 分析资产清单.

声明的能力、处理器、角色和命令描述 App 可能要求什么 它的宿主将揭露。 他们不批准这些行动.

### 应用程序存储声明

App 可以在 `fungi.app.storage`数字 :

```json
[{ "key": "desktop", "scopes": ["team", "member"], "maxBytes": 16384 }]
```

每一份摘要覆盖的声明最多允许16个不同的密钥。 密钥匹配 `[a-z][a-z0-9.-]{0,63}`;其非空瞄准镜是 `team` 财务报告和审定财务报表 `member`,以及 `maxBytes` 是从1到65 536 此请求存储 App自己的安装数据. 它不允许他人进入App、安装或 团队;枢纽拥有范围化招生和写作权限.

### 艺术阅读器

文物阅读器解决了所储存的不透明的参考,并验证. 校验检查预期摘要、 内容类型、 配置大小界限 和自有的字节副本。 读者从不使用呼叫器提供的文摘 查找不同的字节 。 资产清单还包含对象版本、摘要、, 长度和座标。 您的东道主继续录用和记录隔离.

### 创建、核实和选择

在其发布和表面之前创建 App 。 释放或表面用于 缺少 App 抛出 `AppReleaseOwnerAppAbsentError` 或者说 `AppReleaseSurfaceOwnerAppAbsentError`。重放同样的不可改变的记录 返回它。 用已改变的材料重用身份失败, 无法释放 这是 `AppReleaseImmutableConflictError`。 。 。 .

创造自由并不能促进它。 验证其文物,然后要求 明确 `promotion` 或者说 `rollback` 符合资格的释放 App. 未验证,被撤销和交叉App选择失败. 正在重试过渡 使用相同的一能键并输入返回其收件。 已更改的输入 键投出 `AppReleaseTransitionIdempotencyConflictError` 离开 单独选择.

A 类 `revocation` 停止符合条件的交付和选择。 无法编辑 文物字节或决定团队如何恢复安装。 你们的主人 后端状态生命周期和计划迁移与释放事实是分开的.

### 服务所选字节

`resolvePublicWebsite(route)` 采取划线稳定路线,读取当前 选择、核实文物并在返回字节之前重新检查权限. 私人缓存永远不会跳过重新检查。 请求身体,饼干和 调用程序提供的版本 ID 无法选择未发布的内容 .

### 云浮适配器

`./cloudflare` 提供 `createD1AppReleaseStore` 为目录状态, `createR2ArtifactReader` 用于文物阅读 `APP_RELEASE_D1_FRESH_SCHEMA` 用于空数据库. `./cloudflare-assets` 阅读和核实资产建造 从R2. `./publisher` 持有用于注册 App 源代码的计划 带有出版团队的仓库。 适配方不提供资源或 供应小组的授权.

## API 参考

见生成 [API 参考](https://npmcn.com/package/%40fungi.computer/puffball/docs/api/README.md)。切入点是 `@fungi.computer/puffball` 联 合 国 `./effect`, (中文(简体) ), `./app`, (中文(简体) ), `./backend`, (中文(简体) ), `./assets`, (中文(简体) ), `./cloudflare`, (中文(简体) ), `./cloudflare-assets` 财务报告和审定财务报表 `./publisher` 潜行道.

## 实例和检查

### 撤销发行

继续快速启动 撤销释放 路线停止服务 就再也不能促进它了:

```ts
await catalog.transition({
  kind: "revocation",
  releaseId,
  idempotencyKey: parseIdempotencyKey("idem_hello-revoke-1"),
});

await catalog.resolvePublicWebsite(route);
// throws AppReleasePublicWebsiteUnavailableError

await catalog.transition({
  kind: "promotion",
  publicSurfaceId,
  releaseId,
  idempotencyKey: parseIdempotencyKey("idem_hello-promote-2"),
});
// throws AppReleaseTransitionReleaseRevokedError
```

无表面抛出的道路 `AppReleasePublicWebsiteNotFoundError`。 。 。 .

### 运行工人

```ts
import { createAppReleaseCatalog } from "@fungi.computer/puffball";
import {
  createD1AppReleaseStore,
  createR2ArtifactReader,
  type AppReleaseD1Database,
  type AppReleaseR2Bucket,
} from "@fungi.computer/puffball/cloudflare";

type Env = { DB: AppReleaseD1Database; ARTIFACTS: AppReleaseR2Bucket };

export function catalogFor(env: Env) {
  return createAppReleaseCatalog({
    store: createD1AppReleaseStore(env.DB),
    artifactReader: createR2ArtifactReader(env.ARTIFACTS),
  });
}
```

R2读取器使用文物参考物作为对象键和对象的 `contentType` 作为媒体类型.

### 本地网站演示

从一次退房,一个自我检查演示服务 固定App 相对于回回回回回回HTTP, 检查寒冷而温暖的读取返回相同的字节,然后撤销该版本 并检查它停止服务。 它不需要供应商证书:

```sh
pnpm --filter @fungi.computer/puffball demo:a1.2:self-check
```

`demo:a1.2` 交互运行: 打开打印 `/orchid` URL, 按键 `r` 改为 撤销和 `q` 或Ctrl-C 退出。 演示版的"取回"适配器服务于犬科 获取路径, 对错误或未选择的路径返回 404, 对不安全的路径返回 503 交付和405 其他方法。 这些状态属于演示,而不是 每个主机的HTTP合同.

测试套件包括创建和重播、过渡、一元冲突以及 公共网站的发布, 加上本地的 Cloudflare 证明重建, 储存和接收冲突以及生命周期种族:

```sh
pnpm --filter @fungi.computer/puffball test
```

## 限制和错误

-   默认的文物政策接受的文物可达65,536字节. 过 `artifactPolicy: { maxBytes, maxManifestBytes }` 改为 `createAppReleaseCatalog` 来改变它.
-   每个失败都是从根导出的一个命名错误类, 例如 `AppReleaseInputError`, (中文(简体) ), `AppReleaseImmutableConflictError`, (中文(简体) ), `AppReleaseArtifactDigestMismatchError`, (中文(简体) ), `AppReleaseTransitionReleaseUnverifiedError`, (中文(简体) ), `AppReleaseTransitionIdempotencyConflictError`, (中文(简体) ), `AppReleasePublicWebsiteNotFoundError` 财务报告和审定财务报表 `AppReleasePublicWebsiteUnavailableError`。 。 。 .
-   返回错误结果的商店 `AppReleaseInvalidStoreResultError`。无法到达的存储原因 `AppReleaseStoreUnavailableError`。 。 。 .
-   目录不部署、配置域名、从源头构建或发布. 那些留在你的主人身边.

## 贡献和许可证

麻省理工学院. 见 [自由主义](https://npmcn.com/package/%40fungi.computer/puffball/LICENSE)。 。 。 .

`@fungi.computer/puffball/app-event-facts` 拥有 Cononical App 事件汇 输入和保存主机事实解码器. `git-representation` 拥有仓库名称, SHA-1, ref-update and push-event-id schemas 与阿加尔共享. 这些解码 陈述;他们不接纳打电话者或允许吉特进入.

背景工作用途 `@fungi.computer/puffball/background-replay`。主机呼叫 `backgroundContext` 与经核实的方法名称/效果,实际引用 祖先和宿主封印 `backgroundOrigin`应用程序检查 `mayEnqueueBackground` 并保留原产地 被接受的工作; 重播永远不会有排队资格。 探员,钥匙, 工作流节点和筑巢电话将工作保留在前地. 醒来 仅保留成员身份和观察到的修订. `readmitBackground` 解决成员并重新接纳已宣布的 `backend.sync.method` 每页 交货。 普通的写法必须涵盖每个写法的能力 所需经费。 不可用的安装是主机死信; a 仍然可以使用的被拒航道可能会收到 `syncRefused` 没有有约束力的赠款.

主机获得其不可复制的签名密钥 `backgroundOriginKey` 财务报告和审定财务报表 封存实例、成员道并用 `sealBackgroundOrigin`在重播中, `BACKGROUND.admit` 检查声称的工作 与新承认的方法相对应。 每个框架允许检查一次: 其他能力影响等待其认可的决定,同时 `APP_STORAGE` 仍可申请和定居。 拒绝归还索赔 `{kind: "refused", code: "access_changed"}` 在这些效果之前; App 无法 仅声明主机 `BACKGROUND` 装订.

## 常见问题

### @fungi\.computer/puffball 是什么？

具有经核实的公开网站交付的不可移动应用程序/发布目录

### 如何安装 @fungi\.computer/puffball？

在项目目录运行 npm install @fungi\.computer/puffball，或使用 pnpm add / yarn add / bun add。

### @fungi\.computer/puffball 支持 TypeScript 吗？

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

### @fungi\.computer/puffball 支持 ESM 还是 CommonJS？

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

### @fungi\.computer/puffball 可以商用吗？

@fungi\.computer/puffball 使用 MIT 许可。宽松许可：允许商业使用、修改和再分发，通常只需在分发时保留原版权与许可声明。以上为概括，不构成法律意见，请以许可原文为准。

### @fungi\.computer/puffball 的下载量有多少？

根据 npm 官方下载统计，@fungi\.computer/puffball 在 2026/10/16 – 2026/11/14 这 30 天内被下载 15,470 次，日均约 516 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入，适合横向比较热度，不能直接等同于使用人数。

### @fungi\.computer/puffball 还在维护吗？

最新版本 0\.9\.0 发布于 2026/11/14（1 天前）；至今共发布 17 个版本，其中过去 12 个月发布了 17 个；首次发布于 2026/10/15。发布频率只是参考信号：功能稳定的工具可能很少需要更新，建议结合源码仓库的 issue 和提交记录判断。

