弹出球( Puffball )@fungi.computer/puffball(中文(简体) )
Puffball是网络应用及其不可改变发行的目录. 它记录 哪些 App 存在,哪些发布它拥有,哪些发布每个公共路线 服务。 在送达或选择任何物品之前,它会读取文物字节和 检查他们与摘要 记录的释放.
当您为他人托管应用程序并需要三个保证时使用它: a Release 创建后永远不变, 路径只服务于您明确发布的 升职,取消的释放 立刻停止服务。 团队安装, 权限、帐单和执行保留在您的主机.
安装或设置
npm install @fungi.computer/puffball
一揽子措施是无害环境管理。 这取决于 effect 继续 凯恩
用于过渡标识。 核心随处可见 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数据库的公开测试条目:
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页面,再推广"发布"并读取该页面通过路由返回. 它在节点运行 与内幕商店 和一个十行 文物阅读器:
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数字 :
[{ "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 参考。切入点是
@fungi.computer/puffball 联 合 国 ./effect, (中文(简体) ), ./app, (中文(简体) ), ./backend, (中文(简体) ), ./assets, (中文(简体) ),
./cloudflare, (中文(简体) ), ./cloudflare-assets 财务报告和审定财务报表 ./publisher 潜行道.
实例和检查
撤销发行
继续快速启动 撤销释放 路线停止服务 就再也不能促进它了:
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。 。 。 .
运行工人
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, 检查寒冷而温暖的读取返回相同的字节,然后撤销该版本 并检查它停止服务。 它不需要供应商证书:
pnpm --filter @fungi.computer/puffball demo:a1.2:self-check
demo:a1.2 交互运行: 打开打印 /orchid URL, 按键 r 改为
撤销和 q 或Ctrl-C 退出。 演示版的"取回"适配器服务于犬科
获取路径, 对错误或未选择的路径返回 404, 对不安全的路径返回 503
交付和405 其他方法。 这些状态属于演示,而不是
每个主机的HTTP合同.
测试套件包括创建和重播、过渡、一元冲突以及 公共网站的发布, 加上本地的 Cloudflare 证明重建, 储存和接收冲突以及生命周期种族:
pnpm --filter @fungi.computer/puffball test
限制和错误
- 默认的文物政策接受的文物可达65,536字节. 过
artifactPolicy: { maxBytes, maxManifestBytes }改为createAppReleaseCatalog来改变它. - 每个失败都是从根导出的一个命名错误类, 例如
AppReleaseInputError, (中文(简体) ),AppReleaseImmutableConflictError, (中文(简体) ),AppReleaseArtifactDigestMismatchError, (中文(简体) ),AppReleaseTransitionReleaseUnverifiedError, (中文(简体) ),AppReleaseTransitionIdempotencyConflictError, (中文(简体) ),AppReleasePublicWebsiteNotFoundError财务报告和审定财务报表AppReleasePublicWebsiteUnavailableError。 。 。 . - 返回错误结果的商店
AppReleaseInvalidStoreResultError。无法到达的存储原因AppReleaseStoreUnavailableError。 。 。 . - 目录不部署、配置域名、从源头构建或发布. 那些留在你的主人身边.
贡献和许可证
麻省理工学院. 见 自由主义。 。 。 .
@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 装订.