@ai-slot/operations 0.0.0-stage → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 iannil
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,47 @@
1
- # Temporary Holding Version
1
+ # @ai-slot/operations
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ **English** | [简体中文](https://github.com/iannil/ai-slot-component/blob/master/packages/operations/README.zh-CN.md)
4
+
5
+ An experimental public server-side package that provides version-state validation and a local single-writer file store. The 0.x API and persisted format do not yet guarantee compatibility across versions. Requires Node.js 22 or later. `openFileStore(directory)` maintains `state.json` and an exclusive `writer.lock` in the directory. Transactions commit drafts, approvals, publication records, pointers, and idempotency receipts as a single snapshot. The current limits are 64 MiB and 1,000 publication records. Only local filesystems with atomic rename semantics are supported; NFS, cloud-synced directories, and multiple service instances are not supported.
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ npm install @ai-slot/operations
11
+ ```
12
+
13
+ Import `openFileStore` and `createOperations`; the workflow is described below. A complete runnable reference service is available at [`examples/managed-slot`](https://github.com/iannil/ai-slot-component/tree/master/examples/managed-slot).
14
+
15
+ ## Transactions and manual recovery
16
+
17
+ Transactions execute in call order, and reads enter the same queue. A commit first validates all references, then writes a temporary file in the same directory, syncs the file, renames it to `state.json`, and finally syncs the directory. A failure at `beforeRename` leaves the previous snapshot unchanged. A failure after rename returns `commit_unknown`, and the handle stops accepting further writes. Callers should check the idempotency receipt to determine the commit outcome, rather than treating this error as a definitive failure and retrying blindly.
18
+
19
+ 1. Stop the sole service and back up the entire storage directory, including `state.json`, temporary files, and `writer.lock`.
20
+ 2. Check the PID and random nonce in `writer.lock` and confirm that the original service process has exited. Do not automatically take over a lock based on its age.
21
+ 3. Have an administrator manually move the leftover `writer.lock` out of the directory, then call `openFileStore` again. Startup checks the snapshot size before parsing and uses `assertState` to validate the structure, hashes, and all references. A corrupt snapshot prevents startup; it is never reset to an empty store.
22
+ 4. Look up the corresponding receipt using the idempotency key to confirm whether the publication was persisted before deciding on the next business action.
23
+
24
+ The `fault` injection option is for fault testing only, not application configuration.
25
+
26
+ ## Drafts, publication, and public delivery
27
+
28
+ `createOperations(store, env)` is the unified entry point for governance and delivery. `listSlots(actor)` lists only slots with a policy for which the actor has `read` permission. `readSlot(actor, slotId)` returns only that slot's publication pointer and drafts. `previewDraft` and `getStatus` also require `read` permission for the slot. Draft creation validates the tree, source, and live baseline, then stores the content as an immutable artifact addressed by SHA-256. The preview's `previous` is pinned to the version at draft creation. Approval rechecks the hash, baseline, source, and component contract. Approval validity is limited to 15 minutes and must not extend beyond the source's expiry.
29
+
30
+ A complete workflow follows. Both publication and rollback require the approver to review the content hash and expiry again:
31
+
32
+ 1. `createDraft(actor, { slotId, tree, baseRevision, sourceVersion })` creates a draft.
33
+ 2. `previewDraft(actor, draft.id)` shows the draft, artifact hash, and differences from the baseline.
34
+ 3. `approveDraft(actor, { draftId, artifactHash, expiresAt })` approves the draft.
35
+ 4. `publishDraft(actor, { draftId, artifactHash, expectedRevision, idempotencyKey })` publishes atomically and returns a receipt for the new revision.
36
+ 5. `deliver(slotId)` publicly delivers the current version. It accepts no administrator identity and returns no drafts, approver information, or source credentials.
37
+ 6. To roll back, `prepareRollback(actor, { slotId, targetRevision, expectedRevision })` creates a new rollback draft from a historical artifact.
38
+ 7. Run `previewDraft` and `approveDraft` again to review the rollback draft.
39
+ 8. `rollbackSlot(actor, { draftId, artifactHash, expectedRevision, idempotencyKey })` publishes a new revision rather than moving the pointer back to an old revision number.
40
+
41
+ `deliver` returns version 1 JSON: `{ version, slot, tree, meta: { revision, artifactHash, sourceVersion, expiresAt } }`. `expiresAt` is the Unix timestamp in milliseconds stored in the artifact. All responses use `Cache-Control: no-store`. A known slot with no readable published content, or whose current source, component contract, or expiry fails validation, returns `503 { "error": "ai_unavailable" }`, allowing the page to retain its original content. Unknown slots return 404. Exceptions thrown while reading the source are normalized to `source_unavailable`; invalid source data still produces `source_invalid`. Public responses do not expose these internal details.
42
+
43
+ `getStatus(actor, slotId)` returns `{ desiredRevision, readableRevision, browserObservation }`. `desiredRevision` comes from the publication pointer and is retained even when the current source becomes invalid. `readableRevision` is populated only if the same live readability checks pass. `browserObservation` is always `null` because the server has no observation of browser rendering. Every delivery rereads the external source. An external source and a file transaction cannot be locked atomically across systems, so this package does not guarantee that the source remains unchanged after validation. This is a single-site reference implementation; it does not provide arbitrary factual or semantic verification, or automatic approval of financial promotions.
44
+
45
+ ## Validation status and scope
46
+
47
+ This package does not start an HTTP service automatically or provide support for multiple instances or tenant isolation. Protocol and scripted validation have passed; validation with real operators and customer pilots has not yet been completed. Back up state before upgrading. Automatic migration of historical experimental snapshots is not guaranteed.
@@ -0,0 +1,43 @@
1
+ # @ai-slot/operations
2
+
3
+ [English](https://github.com/iannil/ai-slot-component/blob/master/packages/operations/README.md) | **简体中文**
4
+
5
+ 实验性服务端公开包(0.x API 和持久格式尚不承诺跨版本兼容),提供版本状态校验与本地单写者文件库。需要 Node.js 22 或更新版本。`openFileStore(directory)` 在目录中维护 `state.json` 和独占的 `writer.lock`,事务把草稿、审批、发布记录、指针及幂等回执作为一个快照提交。当前边界为 64 MiB 和 1000 条发布记录;只支持本地具备原子 rename 语义的文件系统,不支持 NFS、云同步目录或多个服务实例。
6
+
7
+ 事务按调用顺序执行,读取也进入同一队列。提交先校验完整引用关系,再写同目录临时文件、同步文件、rename 到 `state.json`,最后同步目录。`beforeRename` 失败不会改变旧快照;rename 后失败返回 `commit_unknown`,该句柄停止后续写入。调用方应通过幂等回执核对提交结果,不能把该错误视为明确失败并盲目重试。
8
+
9
+ ## 人工恢复
10
+
11
+ 1. 停止唯一服务,备份整个存储目录(包括 `state.json`、临时文件及 `writer.lock`)。
12
+ 2. 核对 `writer.lock` 中的 PID 与随机 nonce,并确认原服务进程确已退出;不要按锁文件年龄自动抢占。
13
+ 3. 管理员人工移走残留的 `writer.lock`,重新调用 `openFileStore`。启动会在解析前检查快照大小,并用 `assertState` 校验结构、hash 和所有引用;损坏快照会拒绝启动,不会重置为空库。
14
+ 4. 用幂等键查询对应回执确认发布是否已经落盘,再决定后续业务动作。
15
+
16
+ `fault` 注入选项只供故障测试使用,不是业务配置。
17
+
18
+ ## 草稿、发布与公开投递
19
+
20
+ `createOperations(store, env)` 是治理和交付的统一入口。`listSlots(actor)` 只列出有 `read` 授权且存在策略的槽位;`readSlot(actor, slotId)` 只返回该槽位的发布指针与草稿。`previewDraft` 和 `getStatus` 同样需要本槽位的 `read` 授权。草稿建立时校验树、来源与线上基线,将内容写成以 SHA-256 寻址的不可变 artifact;预览的 `previous` 固定到草稿创建时的版本。批准再次核对哈希、基线、来源及组件契约,且有效期最多 15 分钟并不得超过来源期限。
21
+
22
+ 一次完整的操作顺序如下,其中发布和回滚都需要批准人重新审核内容哈希与有效期:
23
+
24
+ 1. `createDraft(actor, { slotId, tree, baseRevision, sourceVersion })` 建立草稿。
25
+ 2. `previewDraft(actor, draft.id)` 查看草稿、artifact 哈希和基线差异。
26
+ 3. `approveDraft(actor, { draftId, artifactHash, expiresAt })` 批准该草稿。
27
+ 4. `publishDraft(actor, { draftId, artifactHash, expectedRevision, idempotencyKey })` 原子发布,并取得新 revision 回执。
28
+ 5. `deliver(slotId)` 公开交付当前版本;它不接受管理员身份,也不会返回草稿、批准人或来源凭据。
29
+ 6. 需要回滚时,`prepareRollback(actor, { slotId, targetRevision, expectedRevision })` 将历史 artifact 建成新的回滚草稿。
30
+ 7. 再次 `previewDraft` 和 `approveDraft`,审核这份回滚草稿。
31
+ 8. `rollbackSlot(actor, { draftId, artifactHash, expectedRevision, idempotencyKey })` 发布新的 revision,而非移动指针回旧编号。
32
+
33
+ `deliver` 返回版本 1 的 JSON:`{ version, slot, tree, meta: { revision, artifactHash, sourceVersion, expiresAt } }`。`expiresAt` 是 artifact 中的 Unix 毫秒时间。所有响应均使用 `Cache-Control: no-store`。已知槽位没有可读发布内容,或当前来源、组件契约、有效期校验失败时,返回 `503 { "error": "ai_unavailable" }`,由页面保留原始内容;未知槽位返回 404。来源读取抛出的异常会统一转成 `source_unavailable`,来源数据本身不合法仍是 `source_invalid`,公开响应不会暴露这些内部细节。
34
+
35
+ `getStatus(actor, slotId)` 返回 `{ desiredRevision, readableRevision, browserObservation }`。`desiredRevision` 来自发布指针,即使当前来源失效也保留;`readableRevision` 只有通过同一实时可读性检查才有值;`browserObservation` 固定为 `null`,因为服务端没有浏览器渲染观测。每次交付都会重读外部来源,外部来源与文件事务不能跨系统原子锁定,因此不承诺来源在校验后永不变化。此包是单站点参考实现,不负责任意事实语义验证或金融促销自动审批。
36
+
37
+ ## 安装与验收边界
38
+
39
+ ```bash
40
+ npm install @ai-slot/operations
41
+ ```
42
+
43
+ 可导入 `openFileStore`、`createOperations`,调用流程见上文。完整可运行参考服务位于仓库 `examples/managed-slot`;此包不自动启动 HTTP 服务,也不提供多实例或租户隔离。协议和脚本验收已通过,真实运营人员及客户试点尚未完成。升级前备份状态;不承诺自动迁移历史试验快照。
@@ -0,0 +1,193 @@
1
+ import { ComponentNode, Registry } from '@ai-slot/registry';
2
+
3
+ type Action = 'read' | 'draft' | 'approve' | 'publish' | 'rollback';
4
+ interface Principal {
5
+ id: string;
6
+ grants: Record<string, Action[]>;
7
+ }
8
+ interface Source {
9
+ id: string;
10
+ version: string;
11
+ expiresAt: number;
12
+ data: Record<string, unknown>;
13
+ }
14
+ interface SlotPolicy {
15
+ slotId: string;
16
+ registryVersion: string;
17
+ rendererVersion: string;
18
+ sourceId: string;
19
+ allowedComponents: string[];
20
+ lockedProps: {
21
+ component: string;
22
+ prop: string;
23
+ sourceField: string;
24
+ }[];
25
+ urlProps: {
26
+ component: string;
27
+ prop: string;
28
+ }[];
29
+ allowedOrigins: string[];
30
+ }
31
+ interface Artifact {
32
+ hash: string;
33
+ slotId: string;
34
+ tree: ComponentNode;
35
+ sourceId: string;
36
+ sourceVersion: string;
37
+ registryVersion: string;
38
+ rendererVersion: string;
39
+ expiresAt: number;
40
+ }
41
+ interface Draft {
42
+ id: string;
43
+ artifactHash: string;
44
+ slotId: string;
45
+ baseRevision: number;
46
+ createdBy: string;
47
+ intent: 'publish' | 'rollback';
48
+ createdAt: number;
49
+ }
50
+ interface Approval {
51
+ draftId: string;
52
+ artifactHash: string;
53
+ baseRevision: number;
54
+ approvedBy: string;
55
+ expiresAt: number;
56
+ }
57
+ interface Release {
58
+ slotId: string;
59
+ revision: number;
60
+ artifactHash: string;
61
+ draftId: string;
62
+ actor: string;
63
+ action: 'publish' | 'rollback';
64
+ at: number;
65
+ }
66
+ interface Receipt {
67
+ revision: number;
68
+ artifactHash: string;
69
+ slotId: string;
70
+ }
71
+ interface State {
72
+ format: 1;
73
+ artifacts: Record<string, Artifact>;
74
+ drafts: Record<string, Draft>;
75
+ approvals: Record<string, Approval>;
76
+ releases: Release[];
77
+ pointers: Record<string, Receipt>;
78
+ idempotency: Record<string, {
79
+ fingerprint: string;
80
+ receipt: Receipt;
81
+ }>;
82
+ }
83
+ interface Store {
84
+ read(): Promise<State>;
85
+ transact<T>(change: (state: State) => T | Promise<T>): Promise<T>;
86
+ close(): Promise<void>;
87
+ }
88
+ interface Environment {
89
+ registry: Registry;
90
+ policies: Record<string, SlotPolicy>;
91
+ readSource: (slotId: string) => Promise<Source>;
92
+ now: () => number;
93
+ notify: (slotId: string) => void | Promise<void>;
94
+ }
95
+ declare class OperationError extends Error {
96
+ code: string;
97
+ constructor(code: string);
98
+ }
99
+
100
+ declare function canonical(value: unknown): string;
101
+ declare const hash: (v: unknown) => string;
102
+
103
+ declare function emptyState(): State;
104
+ /** 启动和提交共同使用的完整结构及引用校验。 */
105
+ declare function assertState(input: unknown): asserts input is State;
106
+
107
+ type Phase = 'beforeRename' | 'afterRename';
108
+ type Fault = (phase: Phase) => void;
109
+ /** 单进程写者;锁残留必须由管理员核实后人工移走。 */
110
+ declare function openFileStore(directory: string, fault?: Fault): Promise<Store>;
111
+
112
+ /** 权限必须由调用者对该槽位显式授予。 */
113
+ declare function authorize(actor: Principal, slotId: string, action: Action): void;
114
+ /** 检查组件、来源事实和 URL;返回与调用方输入隔离的已验证快照。 */
115
+ declare function validateArtifact(env: Environment, slotId: string, tree: unknown, source: Source): ComponentNode;
116
+
117
+ interface DraftInput {
118
+ slotId: string;
119
+ tree: unknown;
120
+ baseRevision: number;
121
+ sourceVersion: string;
122
+ }
123
+ interface ApprovalInput {
124
+ draftId: string;
125
+ artifactHash: string;
126
+ expiresAt: number;
127
+ }
128
+ declare function readSource(env: Environment, slotId: string): Promise<Source>;
129
+ /** 发布、批准和回滚共同使用当前契约与来源快照校验。 */
130
+ declare function assertCurrentArtifact(env: Environment, artifact: Artifact): Promise<void>;
131
+ /** 在单个事务中校验来源与基线,然后固定草稿及内容哈希。 */
132
+ declare function createDraft(store: Store, env: Environment, actor: Principal, input: DraftInput): Promise<Draft>;
133
+ /** 差异基线固定为草稿创建时的版本。 */
134
+ declare function previewDraft(store: Store, _env: Environment, actor: Principal, draftId: string): Promise<{
135
+ draft: Draft;
136
+ artifact: Artifact;
137
+ previous: Artifact | null;
138
+ }>;
139
+ /** 批准前再次校验来源、组件契约和线上基线。 */
140
+ declare function approveDraft(store: Store, env: Environment, actor: Principal, input: ApprovalInput): Promise<Approval>;
141
+ interface RollbackInput {
142
+ slotId: string;
143
+ targetRevision: number;
144
+ expectedRevision: number;
145
+ }
146
+ /** 历史内容重新进入草稿和审批链;线上指针保持原状。 */
147
+ declare function prepareRollback(store: Store, env: Environment, actor: Principal, input: RollbackInput): Promise<Draft>;
148
+
149
+ interface PublishInput {
150
+ draftId: string;
151
+ artifactHash: string;
152
+ expectedRevision: number;
153
+ idempotencyKey: string;
154
+ }
155
+ /** 原子 CAS 发布;幂等命中前仍检查当前授权。 */
156
+ declare function commitDraft(store: Store, env: Environment, actor: Principal, input: PublishInput, action: Extract<Action, 'publish' | 'rollback'>): Promise<Receipt>;
157
+
158
+ /** 只交付当前活动版本;来源读取失败时不使用先前快照补位。 */
159
+ declare function deliver(store: Store, env: Environment, slotId: string): Promise<Response>;
160
+ /** desired来自发布指针;readable必须通过与公开投递相同的实时检查。 */
161
+ declare function getStatus(store: Store, env: Environment, actor: Principal, slotId: string): Promise<{
162
+ desiredRevision: number;
163
+ readableRevision: number | null;
164
+ browserObservation: null;
165
+ }>;
166
+
167
+ /** 管理操作与公开投递的唯一入口。 */
168
+ interface Operations {
169
+ listSlots(actor: Principal): Promise<string[]>;
170
+ readSlot(actor: Principal, slotId: string): Promise<{
171
+ pointer: Receipt | null;
172
+ drafts: Draft[];
173
+ }>;
174
+ createDraft(actor: Principal, input: DraftInput): Promise<Draft>;
175
+ previewDraft(actor: Principal, draftId: string): Promise<{
176
+ draft: Draft;
177
+ artifact: Artifact;
178
+ previous: Artifact | null;
179
+ }>;
180
+ approveDraft(actor: Principal, input: ApprovalInput): Promise<Approval>;
181
+ publishDraft(actor: Principal, input: PublishInput): Promise<Receipt>;
182
+ prepareRollback(actor: Principal, input: RollbackInput): Promise<Draft>;
183
+ rollbackSlot(actor: Principal, input: PublishInput): Promise<Receipt>;
184
+ getStatus(actor: Principal, slotId: string): Promise<{
185
+ desiredRevision: number;
186
+ readableRevision: number | null;
187
+ browserObservation: null;
188
+ }>;
189
+ deliver(slotId: string): Promise<Response>;
190
+ }
191
+ declare function createOperations(store: Store, env: Environment): Operations;
192
+
193
+ export { type Action, type Approval, type ApprovalInput, type Artifact, type Draft, type DraftInput, type Environment, OperationError, type Operations, type Principal, type PublishInput, type Receipt, type Release, type RollbackInput, type SlotPolicy, type Source, type State, type Store, approveDraft, assertCurrentArtifact, assertState, authorize, canonical, commitDraft, createDraft, createOperations, deliver, emptyState, getStatus, hash, openFileStore, prepareRollback, previewDraft, readSource, validateArtifact };
package/dist/index.js ADDED
@@ -0,0 +1,688 @@
1
+ // src/types.ts
2
+ var OperationError = class extends Error {
3
+ constructor(code) {
4
+ super(code);
5
+ this.code = code;
6
+ }
7
+ code;
8
+ };
9
+
10
+ // src/canonical.ts
11
+ import { createHash } from "crypto";
12
+ function canonical(value) {
13
+ const active = /* @__PURE__ */ new Set();
14
+ function visit(v) {
15
+ if (v === null || typeof v === "string" || typeof v === "boolean") return JSON.stringify(v);
16
+ if (typeof v === "number" && Number.isFinite(v)) return JSON.stringify(v);
17
+ if (typeof v !== "object" || v === null) throw Error("non_json");
18
+ if (active.has(v)) throw Error("cycle");
19
+ if (!Array.isArray(v) && Object.getPrototypeOf(v) !== Object.prototype && Object.getPrototypeOf(v) !== null) throw Error("non_plain");
20
+ active.add(v);
21
+ try {
22
+ if (Array.isArray(v)) return "[" + Array.from(v, (x) => visit(x)).join(",") + "]";
23
+ return "{" + Object.keys(v).sort().map((k) => JSON.stringify(k) + ":" + visit(v[k])).join(",") + "}";
24
+ } finally {
25
+ active.delete(v);
26
+ }
27
+ }
28
+ return visit(value);
29
+ }
30
+ var hash = (v) => createHash("sha256").update(canonical(v)).digest("hex");
31
+
32
+ // src/state.ts
33
+ var fail = () => {
34
+ throw new OperationError("corrupt_state");
35
+ };
36
+ var record = (value) => value !== null && typeof value === "object" && !Array.isArray(value) && (Object.getPrototypeOf(value) === Object.prototype || Object.getPrototypeOf(value) === null);
37
+ var own = (value, key) => Object.hasOwn(value, key) ? value[key] : void 0;
38
+ var validString = (value) => typeof value === "string" && value.length > 0 && value.length <= 200;
39
+ var validHash = (value) => typeof value === "string" && /^[a-f0-9]{64}$/.test(value);
40
+ var revision = (value, minimum = 0) => Number.isSafeInteger(value) && value >= minimum;
41
+ var time = (value) => typeof value === "number" && Number.isFinite(value);
42
+ function requireString(value) {
43
+ if (!validString(value)) fail();
44
+ }
45
+ function requireHash(value) {
46
+ if (!validHash(value)) fail();
47
+ }
48
+ function requireRevision(value, minimum = 0) {
49
+ if (!revision(value, minimum)) fail();
50
+ }
51
+ function requireTime(value) {
52
+ if (!time(value)) fail();
53
+ }
54
+ function requireRecord(value) {
55
+ if (!record(value)) fail();
56
+ }
57
+ function requireEnum(value, allowed) {
58
+ if (!allowed.includes(value)) fail();
59
+ }
60
+ function json(value, active = /* @__PURE__ */ new Set(), depth = 0) {
61
+ if (depth > 256) fail();
62
+ if (typeof value === "string") return;
63
+ if (value === null || typeof value === "boolean" || time(value)) return;
64
+ if (typeof value !== "object" || value === null) fail();
65
+ const object = value;
66
+ if (active.has(object)) fail();
67
+ if (!Array.isArray(value) && !record(value)) fail();
68
+ active.add(object);
69
+ try {
70
+ if (Array.isArray(value)) {
71
+ for (let i = 0; i < value.length; i++) {
72
+ if (!Object.hasOwn(value, i)) fail();
73
+ json(value[i], active, depth + 1);
74
+ }
75
+ } else {
76
+ for (const [key, child] of Object.entries(object)) {
77
+ requireString(key);
78
+ json(child, active, depth + 1);
79
+ }
80
+ }
81
+ } finally {
82
+ active.delete(object);
83
+ }
84
+ }
85
+ function requireTree(value, depth = 0) {
86
+ if (depth > 256) fail();
87
+ requireRecord(value);
88
+ requireString(own(value, "component"));
89
+ const props = own(value, "props");
90
+ if (props !== void 0) requireRecord(props);
91
+ const children = own(value, "children");
92
+ if (children !== void 0) {
93
+ if (!Array.isArray(children)) fail();
94
+ for (const child of children) requireTree(child, depth + 1);
95
+ }
96
+ const slots = own(value, "slots");
97
+ if (slots !== void 0) {
98
+ requireRecord(slots);
99
+ for (const nodes of Object.values(slots)) {
100
+ if (!Array.isArray(nodes)) fail();
101
+ for (const child of nodes) requireTree(child, depth + 1);
102
+ }
103
+ }
104
+ }
105
+ function emptyState() {
106
+ return { format: 1, artifacts: {}, drafts: {}, approvals: {}, releases: [], pointers: {}, idempotency: {} };
107
+ }
108
+ function assertState(input) {
109
+ requireRecord(input);
110
+ if (own(input, "format") !== 1) fail();
111
+ for (const name of ["artifacts", "drafts", "approvals", "pointers", "idempotency"]) requireRecord(own(input, name));
112
+ const releases = own(input, "releases");
113
+ if (!Array.isArray(releases)) fail();
114
+ json(input);
115
+ const state = input;
116
+ if (state.releases.length > 1e3) throw new OperationError("storage_limit");
117
+ for (const [key, artifact] of Object.entries(state.artifacts)) {
118
+ requireHash(key);
119
+ requireRecord(artifact);
120
+ const stored = own(artifact, "hash");
121
+ requireHash(stored);
122
+ requireString(own(artifact, "slotId"));
123
+ requireTree(own(artifact, "tree"));
124
+ requireString(own(artifact, "sourceId"));
125
+ requireString(own(artifact, "sourceVersion"));
126
+ requireString(own(artifact, "registryVersion"));
127
+ requireString(own(artifact, "rendererVersion"));
128
+ requireTime(own(artifact, "expiresAt"));
129
+ const { hash: _, ...payload } = artifact;
130
+ if (key !== stored || stored !== hash(payload)) fail();
131
+ }
132
+ for (const [key, draft] of Object.entries(state.drafts)) {
133
+ requireString(key);
134
+ requireRecord(draft);
135
+ const id = own(draft, "id");
136
+ requireString(id);
137
+ const artifactHash = own(draft, "artifactHash");
138
+ requireHash(artifactHash);
139
+ const slotId = own(draft, "slotId");
140
+ requireString(slotId);
141
+ requireRevision(own(draft, "baseRevision"));
142
+ requireString(own(draft, "createdBy"));
143
+ requireEnum(own(draft, "intent"), ["publish", "rollback"]);
144
+ requireTime(own(draft, "createdAt"));
145
+ const artifact = Object.hasOwn(state.artifacts, artifactHash) ? state.artifacts[artifactHash] : void 0;
146
+ if (key !== id || !artifact || artifact.slotId !== slotId) fail();
147
+ }
148
+ for (const [key, approval] of Object.entries(state.approvals)) {
149
+ requireString(key);
150
+ requireRecord(approval);
151
+ const draftId = own(approval, "draftId");
152
+ requireString(draftId);
153
+ const artifactHash = own(approval, "artifactHash");
154
+ requireHash(artifactHash);
155
+ const baseRevision = own(approval, "baseRevision");
156
+ requireRevision(baseRevision);
157
+ requireString(own(approval, "approvedBy"));
158
+ requireTime(own(approval, "expiresAt"));
159
+ const draft = Object.hasOwn(state.drafts, draftId) ? state.drafts[draftId] : void 0;
160
+ if (key !== draftId || !draft || draft.artifactHash !== artifactHash || draft.baseRevision !== baseRevision) fail();
161
+ }
162
+ const last = /* @__PURE__ */ new Map();
163
+ for (const release of state.releases) {
164
+ requireRecord(release);
165
+ const slotId = own(release, "slotId");
166
+ requireString(slotId);
167
+ const currentRevision = own(release, "revision");
168
+ requireRevision(currentRevision, 1);
169
+ const artifactHash = own(release, "artifactHash");
170
+ requireHash(artifactHash);
171
+ const draftId = own(release, "draftId");
172
+ requireString(draftId);
173
+ requireString(own(release, "actor"));
174
+ requireEnum(own(release, "action"), ["publish", "rollback"]);
175
+ requireTime(own(release, "at"));
176
+ const draft = Object.hasOwn(state.drafts, draftId) ? state.drafts[draftId] : void 0;
177
+ const artifact = Object.hasOwn(state.artifacts, artifactHash) ? state.artifacts[artifactHash] : void 0;
178
+ if (!draft || !artifact || draft.slotId !== slotId || artifact.slotId !== slotId || draft.artifactHash !== artifactHash || draft.intent !== release.action) fail();
179
+ if (currentRevision !== (last.get(slotId)?.revision ?? 0) + 1) fail();
180
+ last.set(slotId, release);
181
+ }
182
+ for (const [slot, pointer] of Object.entries(state.pointers)) {
183
+ requireString(slot);
184
+ requireRecord(pointer);
185
+ const pointerSlot = own(pointer, "slotId");
186
+ requireString(pointerSlot);
187
+ const pointerRevision = own(pointer, "revision");
188
+ requireRevision(pointerRevision, 1);
189
+ const pointerHash = own(pointer, "artifactHash");
190
+ requireHash(pointerHash);
191
+ const release = last.get(slot);
192
+ if (!release || pointerSlot !== slot || pointerRevision !== release.revision || pointerHash !== release.artifactHash) fail();
193
+ }
194
+ if (last.size !== Object.keys(state.pointers).length) fail();
195
+ for (const [key, entry] of Object.entries(state.idempotency)) {
196
+ requireString(key);
197
+ requireRecord(entry);
198
+ requireHash(own(entry, "fingerprint"));
199
+ const receipt = own(entry, "receipt");
200
+ requireRecord(receipt);
201
+ const slotId = own(receipt, "slotId");
202
+ requireString(slotId);
203
+ const receiptRevision = own(receipt, "revision");
204
+ requireRevision(receiptRevision, 1);
205
+ const artifactHash = own(receipt, "artifactHash");
206
+ requireHash(artifactHash);
207
+ if (!state.releases.some((r) => r.slotId === slotId && r.revision === receiptRevision && r.artifactHash === artifactHash)) fail();
208
+ }
209
+ }
210
+
211
+ // src/file-store.ts
212
+ import { randomUUID } from "crypto";
213
+ import { mkdir, open, readFile, rename, rm } from "fs/promises";
214
+ import { join } from "path";
215
+ var MAX_BYTES = 64 * 1024 * 1024;
216
+ async function readState(directory) {
217
+ const file = await open(join(directory, "state.json"), "r");
218
+ try {
219
+ if ((await file.stat()).size > MAX_BYTES) throw new OperationError("storage_limit");
220
+ const bytes = await file.readFile();
221
+ if (bytes.length > MAX_BYTES) throw new OperationError("storage_limit");
222
+ let state;
223
+ try {
224
+ state = JSON.parse(bytes.toString("utf8"));
225
+ } catch {
226
+ throw new OperationError("corrupt_state");
227
+ }
228
+ assertState(state);
229
+ return state;
230
+ } finally {
231
+ await file.close();
232
+ }
233
+ }
234
+ async function writeState(directory, state, fault) {
235
+ assertState(state);
236
+ const bytes = Buffer.from(canonical(state));
237
+ if (bytes.length > MAX_BYTES || state.releases.length > 1e3) throw new OperationError("storage_limit");
238
+ const temp = join(directory, `state-${randomUUID()}.tmp`);
239
+ let renamed = false;
240
+ try {
241
+ const file = await open(temp, "wx", 384);
242
+ try {
243
+ await file.writeFile(bytes);
244
+ await file.sync();
245
+ } finally {
246
+ await file.close();
247
+ }
248
+ fault?.("beforeRename");
249
+ await rename(temp, join(directory, "state.json"));
250
+ renamed = true;
251
+ fault?.("afterRename");
252
+ const dir = await open(directory, "r");
253
+ try {
254
+ await dir.sync();
255
+ } finally {
256
+ await dir.close();
257
+ }
258
+ } catch (error) {
259
+ if (renamed) throw new OperationError("commit_unknown");
260
+ throw error;
261
+ } finally {
262
+ try {
263
+ await rm(temp, { force: true });
264
+ } catch (error) {
265
+ if (renamed) throw new OperationError("commit_unknown");
266
+ throw error;
267
+ }
268
+ }
269
+ }
270
+ async function unlockOwned(lock, identity) {
271
+ try {
272
+ if (await readFile(lock, "utf8") === identity) await rm(lock);
273
+ } catch (error) {
274
+ if (error.code !== "ENOENT") throw error;
275
+ }
276
+ }
277
+ async function openFileStore(directory, fault) {
278
+ await mkdir(directory, { recursive: true, mode: 448 });
279
+ const lock = join(directory, "writer.lock");
280
+ const identity = `${process.pid}:${randomUUID()}`;
281
+ const handle = await open(lock, "wx", 384);
282
+ try {
283
+ await handle.writeFile(identity);
284
+ await handle.sync();
285
+ await handle.close();
286
+ } catch (error) {
287
+ await handle.close().catch(() => void 0);
288
+ await unlockOwned(lock, identity);
289
+ throw error;
290
+ }
291
+ try {
292
+ try {
293
+ await readState(directory);
294
+ } catch (error) {
295
+ if (error.code !== "ENOENT") throw error;
296
+ await writeState(directory, emptyState());
297
+ }
298
+ } catch (error) {
299
+ await unlockOwned(lock, identity);
300
+ throw error;
301
+ }
302
+ let tail = Promise.resolve();
303
+ let closed = false;
304
+ let poisoned = false;
305
+ function enqueue(work) {
306
+ const job = tail.then(work);
307
+ tail = job.then(() => void 0, () => void 0);
308
+ return job;
309
+ }
310
+ return {
311
+ read() {
312
+ if (closed) return Promise.reject(new OperationError("store_closed"));
313
+ return enqueue(async () => structuredClone(await readState(directory)));
314
+ },
315
+ transact(change) {
316
+ if (closed) return Promise.reject(new OperationError("store_closed"));
317
+ return enqueue(async () => {
318
+ if (poisoned) throw new OperationError("commit_unknown");
319
+ const state = structuredClone(await readState(directory));
320
+ const result = structuredClone(await change(state));
321
+ try {
322
+ await writeState(directory, structuredClone(state), fault);
323
+ } catch (error) {
324
+ if (error instanceof OperationError && error.code === "commit_unknown") poisoned = true;
325
+ throw error;
326
+ }
327
+ return result;
328
+ });
329
+ },
330
+ close() {
331
+ if (closed) return tail;
332
+ closed = true;
333
+ return enqueue(async () => {
334
+ await unlockOwned(lock, identity);
335
+ });
336
+ }
337
+ };
338
+ }
339
+
340
+ // src/policy.ts
341
+ import { validateComponentTree } from "@ai-slot/registry";
342
+ var MAX_TREE_BYTES = 64 * 1024;
343
+ function authorize(actor, slotId, action) {
344
+ if (!actor.grants || !Object.hasOwn(actor.grants, slotId) || !Array.isArray(actor.grants[slotId]) || !actor.grants[slotId].includes(action)) {
345
+ throw new OperationError("forbidden");
346
+ }
347
+ }
348
+ function normalizeNode(value) {
349
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return;
350
+ const node = value;
351
+ if (!Object.hasOwn(node, "props")) node.props = {};
352
+ if (Array.isArray(node.children)) for (const child of node.children) normalizeNode(child);
353
+ if (node.slots !== null && typeof node.slots === "object" && !Array.isArray(node.slots)) {
354
+ for (const children of Object.values(node.slots)) {
355
+ if (Array.isArray(children)) for (const child of children) normalizeNode(child);
356
+ }
357
+ }
358
+ }
359
+ function validateArtifact(env, slotId, tree, source) {
360
+ const policy = Object.hasOwn(env.policies, slotId) ? env.policies[slotId] : void 0;
361
+ if (!policy) throw new OperationError("forbidden");
362
+ const activePolicy = policy;
363
+ if (!source || typeof source !== "object" || Array.isArray(source) || typeof source.id !== "string" || !source.id.trim() || source.id !== policy.sourceId || typeof source.version !== "string" || !source.version.trim() || !Number.isFinite(source.expiresAt) || !source.data || typeof source.data !== "object" || Array.isArray(source.data)) {
364
+ throw new OperationError("source_invalid");
365
+ }
366
+ if (source.expiresAt <= env.now()) throw new OperationError("expired");
367
+ let snapshot;
368
+ try {
369
+ const encoded = canonical(tree);
370
+ if (Buffer.byteLength(encoded, "utf8") > MAX_TREE_BYTES) throw new OperationError("invalid_tree");
371
+ snapshot = JSON.parse(encoded);
372
+ normalizeNode(snapshot);
373
+ } catch {
374
+ throw new OperationError("invalid_tree");
375
+ }
376
+ if (!validateComponentTree(env.registry, snapshot).ok) throw new OperationError("invalid_tree");
377
+ const root = snapshot;
378
+ function visit(node) {
379
+ if (!activePolicy.allowedComponents.includes(node.component)) throw new OperationError("component_denied");
380
+ for (const rule of activePolicy.lockedProps) {
381
+ if (rule.component !== node.component) continue;
382
+ if (!Object.hasOwn(source.data, rule.sourceField) || !Object.hasOwn(node.props ?? {}, rule.prop)) {
383
+ throw new OperationError("locked_prop");
384
+ }
385
+ try {
386
+ if (canonical(node.props?.[rule.prop]) !== canonical(source.data[rule.sourceField])) {
387
+ throw new OperationError("locked_prop");
388
+ }
389
+ } catch {
390
+ throw new OperationError("locked_prop");
391
+ }
392
+ }
393
+ for (const rule of activePolicy.urlProps) {
394
+ if (rule.component !== node.component) continue;
395
+ let url;
396
+ try {
397
+ url = new URL(String(node.props?.[rule.prop]));
398
+ } catch {
399
+ throw new OperationError("url_denied");
400
+ }
401
+ if (url.protocol !== "https:" || url.username || url.password || !activePolicy.allowedOrigins.includes(url.origin)) {
402
+ throw new OperationError("url_denied");
403
+ }
404
+ }
405
+ for (const child of node.children ?? []) visit(child);
406
+ for (const children of Object.values(node.slots ?? {})) for (const child of children) visit(child);
407
+ }
408
+ visit(root);
409
+ return root;
410
+ }
411
+
412
+ // src/drafts.ts
413
+ import { randomUUID as randomUUID2 } from "crypto";
414
+ async function readSource(env, slotId) {
415
+ try {
416
+ return await env.readSource(slotId);
417
+ } catch {
418
+ throw new OperationError("source_unavailable");
419
+ }
420
+ }
421
+ async function assertCurrentArtifact(env, artifact) {
422
+ const policy = Object.hasOwn(env.policies, artifact.slotId) ? env.policies[artifact.slotId] : void 0;
423
+ if (!policy || artifact.registryVersion !== policy.registryVersion || artifact.rendererVersion !== policy.rendererVersion) {
424
+ throw new OperationError("contract_changed");
425
+ }
426
+ if (artifact.sourceId !== policy.sourceId) throw new OperationError("source_changed");
427
+ const source = await readSource(env, artifact.slotId);
428
+ validateArtifact(env, artifact.slotId, artifact.tree, source);
429
+ if (source.id !== artifact.sourceId || source.version !== artifact.sourceVersion || source.expiresAt !== artifact.expiresAt) {
430
+ throw new OperationError("source_changed");
431
+ }
432
+ if (artifact.expiresAt <= env.now()) throw new OperationError("expired");
433
+ }
434
+ function requireDraftInput(input) {
435
+ if (!input || typeof input.slotId !== "string" || !input.slotId || !Number.isSafeInteger(input.baseRevision) || input.baseRevision < 0 || typeof input.sourceVersion !== "string" || !input.sourceVersion) {
436
+ throw new OperationError("invalid_input");
437
+ }
438
+ }
439
+ function createDraft(store, env, actor, input) {
440
+ requireDraftInput(input);
441
+ return store.transact(async (state) => {
442
+ authorize(actor, input.slotId, "draft");
443
+ const policy = Object.hasOwn(env.policies, input.slotId) ? env.policies[input.slotId] : void 0;
444
+ if (!policy) throw new OperationError("forbidden");
445
+ if (input.baseRevision !== (state.pointers[input.slotId]?.revision ?? 0)) throw new OperationError("revision_conflict");
446
+ const source = await readSource(env, input.slotId);
447
+ const tree = validateArtifact(env, input.slotId, input.tree, source);
448
+ if (source.version !== input.sourceVersion) throw new OperationError("source_changed");
449
+ const payload = {
450
+ slotId: input.slotId,
451
+ tree,
452
+ sourceId: source.id,
453
+ sourceVersion: source.version,
454
+ registryVersion: policy.registryVersion,
455
+ rendererVersion: policy.rendererVersion,
456
+ expiresAt: source.expiresAt
457
+ };
458
+ const artifact = { hash: hash(payload), ...payload };
459
+ const existing = state.artifacts[artifact.hash];
460
+ if (existing && canonical(existing) !== canonical(artifact)) throw new OperationError("hash_collision");
461
+ if (!existing) state.artifacts[artifact.hash] = artifact;
462
+ const draft = {
463
+ id: randomUUID2(),
464
+ artifactHash: artifact.hash,
465
+ slotId: input.slotId,
466
+ baseRevision: input.baseRevision,
467
+ createdBy: actor.id,
468
+ intent: "publish",
469
+ createdAt: env.now()
470
+ };
471
+ state.drafts[draft.id] = draft;
472
+ return draft;
473
+ });
474
+ }
475
+ async function previewDraft(store, _env, actor, draftId) {
476
+ const state = await store.read();
477
+ const draft = Object.hasOwn(state.drafts, draftId) ? state.drafts[draftId] : void 0;
478
+ if (!draft) throw new OperationError("draft_not_found");
479
+ authorize(actor, draft.slotId, "read");
480
+ const artifact = state.artifacts[draft.artifactHash];
481
+ if (!artifact) throw new OperationError("corrupt_state");
482
+ const release = state.releases.find((r) => r.slotId === draft.slotId && r.revision === draft.baseRevision);
483
+ const previous = release ? state.artifacts[release.artifactHash] ?? null : null;
484
+ return structuredClone({ draft, artifact, previous });
485
+ }
486
+ function approveDraft(store, env, actor, input) {
487
+ if (!input || typeof input.draftId !== "string" || typeof input.artifactHash !== "string" || typeof input.expiresAt !== "number" || !Number.isFinite(input.expiresAt)) throw new OperationError("invalid_input");
488
+ return store.transact(async (state) => {
489
+ const draft = Object.hasOwn(state.drafts, input.draftId) ? state.drafts[input.draftId] : void 0;
490
+ if (!draft) throw new OperationError("draft_not_found");
491
+ authorize(actor, draft.slotId, "approve");
492
+ if (draft.artifactHash !== input.artifactHash) throw new OperationError("hash_mismatch");
493
+ if (draft.baseRevision !== (state.pointers[draft.slotId]?.revision ?? 0)) throw new OperationError("revision_conflict");
494
+ const artifact = state.artifacts[draft.artifactHash];
495
+ if (!artifact) throw new OperationError("corrupt_state");
496
+ await assertCurrentArtifact(env, artifact);
497
+ const now = env.now();
498
+ if (!(input.expiresAt > now && input.expiresAt <= Math.min(now + 15 * 6e4, artifact.expiresAt))) {
499
+ throw new OperationError("invalid_expiry");
500
+ }
501
+ const approval = {
502
+ draftId: draft.id,
503
+ artifactHash: artifact.hash,
504
+ baseRevision: draft.baseRevision,
505
+ approvedBy: actor.id,
506
+ expiresAt: input.expiresAt
507
+ };
508
+ state.approvals[draft.id] = approval;
509
+ return approval;
510
+ });
511
+ }
512
+ function prepareRollback(store, env, actor, input) {
513
+ if (!input || typeof input.slotId !== "string" || !input.slotId || !Number.isSafeInteger(input.targetRevision) || input.targetRevision < 1 || !Number.isSafeInteger(input.expectedRevision) || input.expectedRevision < 0) {
514
+ throw new OperationError("invalid_input");
515
+ }
516
+ return store.transact(async (state) => {
517
+ authorize(actor, input.slotId, "rollback");
518
+ if (input.expectedRevision !== (state.pointers[input.slotId]?.revision ?? 0)) throw new OperationError("conflict");
519
+ const release = state.releases.find((r) => r.slotId === input.slotId && r.revision === input.targetRevision);
520
+ if (!release) throw new OperationError("not_found");
521
+ const artifact = state.artifacts[release.artifactHash];
522
+ if (!artifact) throw new OperationError("corrupt_state");
523
+ await assertCurrentArtifact(env, artifact);
524
+ const draft = {
525
+ id: randomUUID2(),
526
+ artifactHash: artifact.hash,
527
+ slotId: input.slotId,
528
+ baseRevision: input.expectedRevision,
529
+ createdBy: actor.id,
530
+ intent: "rollback",
531
+ createdAt: env.now()
532
+ };
533
+ state.drafts[draft.id] = draft;
534
+ return draft;
535
+ });
536
+ }
537
+
538
+ // src/publishing.ts
539
+ async function assertPublishable(state, env, draft, input) {
540
+ const artifact = state.artifacts[draft.artifactHash];
541
+ const approval = state.approvals[draft.id];
542
+ if (!artifact || !approval) throw new OperationError("approval_required");
543
+ if (input.artifactHash !== artifact.hash || approval.artifactHash !== artifact.hash) throw new OperationError("hash_mismatch");
544
+ const current = state.pointers[draft.slotId]?.revision ?? 0;
545
+ if (current !== input.expectedRevision || current !== draft.baseRevision || current !== approval.baseRevision) {
546
+ throw new OperationError("conflict");
547
+ }
548
+ if (approval.expiresAt <= env.now()) throw new OperationError("approval_expired");
549
+ await assertCurrentArtifact(env, artifact);
550
+ if (approval.expiresAt <= env.now()) throw new OperationError("approval_expired");
551
+ }
552
+ async function commitDraft(store, env, actor, input, action) {
553
+ if (!input || typeof input.draftId !== "string" || !input.draftId || typeof input.artifactHash !== "string" || !input.artifactHash || !Number.isSafeInteger(input.expectedRevision) || input.expectedRevision < 0) {
554
+ throw new OperationError("invalid_input");
555
+ }
556
+ const receipt = await store.transact(async (state) => {
557
+ const draft = Object.hasOwn(state.drafts, input.draftId) ? state.drafts[input.draftId] : void 0;
558
+ if (!draft) throw new OperationError("not_found");
559
+ authorize(actor, draft.slotId, action);
560
+ if (draft.intent !== action) throw new OperationError("wrong_action");
561
+ if (typeof input.idempotencyKey !== "string" || !/^[a-zA-Z0-9_-]{1,100}$/.test(input.idempotencyKey)) {
562
+ throw new OperationError("invalid_key");
563
+ }
564
+ const key = hash([actor.id, draft.slotId, action, input.idempotencyKey]);
565
+ const fingerprint = hash(input);
566
+ const previous = Object.hasOwn(state.idempotency, key) ? state.idempotency[key] : void 0;
567
+ if (previous) {
568
+ if (previous.fingerprint !== fingerprint) throw new OperationError("idempotency_conflict");
569
+ return previous.receipt;
570
+ }
571
+ await assertPublishable(state, env, draft, input);
572
+ const revision2 = (state.pointers[draft.slotId]?.revision ?? 0) + 1;
573
+ const result = { slotId: draft.slotId, revision: revision2, artifactHash: draft.artifactHash };
574
+ state.releases.push({ ...result, draftId: draft.id, actor: actor.id, action, at: env.now() });
575
+ state.pointers[draft.slotId] = result;
576
+ state.idempotency[key] = { fingerprint, receipt: result };
577
+ return result;
578
+ });
579
+ try {
580
+ await env.notify(receipt.slotId);
581
+ } catch {
582
+ }
583
+ return receipt;
584
+ }
585
+
586
+ // src/delivery.ts
587
+ var headers = { "content-type": "application/json; charset=utf-8", "cache-control": "no-store" };
588
+ async function readableArtifact(state, env, slotId, pointer) {
589
+ if (!pointer || pointer.slotId !== slotId) return null;
590
+ const artifact = Object.hasOwn(state.artifacts, pointer.artifactHash) ? state.artifacts[pointer.artifactHash] : void 0;
591
+ if (!artifact || artifact.hash !== pointer.artifactHash || artifact.slotId !== slotId) return null;
592
+ try {
593
+ await assertCurrentArtifact(env, artifact);
594
+ } catch {
595
+ return null;
596
+ }
597
+ return artifact;
598
+ }
599
+ async function deliver(store, env, slotId) {
600
+ if (!Object.hasOwn(env.policies, slotId)) {
601
+ return new Response(JSON.stringify({ error: "not_found" }), { status: 404, headers });
602
+ }
603
+ try {
604
+ const state = await store.read();
605
+ const pointer = Object.hasOwn(state.pointers, slotId) ? state.pointers[slotId] : void 0;
606
+ const artifact = await readableArtifact(state, env, slotId, pointer);
607
+ if (pointer && artifact) {
608
+ return new Response(JSON.stringify({
609
+ version: 1,
610
+ slot: slotId,
611
+ tree: artifact.tree,
612
+ meta: {
613
+ revision: pointer.revision,
614
+ artifactHash: artifact.hash,
615
+ sourceVersion: artifact.sourceVersion,
616
+ expiresAt: artifact.expiresAt
617
+ }
618
+ }), { headers });
619
+ }
620
+ } catch {
621
+ }
622
+ return new Response(JSON.stringify({ error: "ai_unavailable" }), { status: 503, headers });
623
+ }
624
+ async function getStatus(store, env, actor, slotId) {
625
+ authorize(actor, slotId, "read");
626
+ if (!Object.hasOwn(env.policies, slotId)) throw new OperationError("not_found");
627
+ const state = await store.read();
628
+ const pointer = Object.hasOwn(state.pointers, slotId) ? state.pointers[slotId] : void 0;
629
+ const artifact = await readableArtifact(state, env, slotId, pointer);
630
+ return {
631
+ desiredRevision: pointer?.revision ?? 0,
632
+ readableRevision: pointer && artifact ? pointer.revision : null,
633
+ browserObservation: null
634
+ };
635
+ }
636
+
637
+ // src/operations.ts
638
+ function createOperations(store, env) {
639
+ return {
640
+ async listSlots(actor) {
641
+ return Object.keys(env.policies).filter((slotId) => {
642
+ try {
643
+ authorize(actor, slotId, "read");
644
+ return true;
645
+ } catch {
646
+ return false;
647
+ }
648
+ }).sort();
649
+ },
650
+ async readSlot(actor, slotId) {
651
+ authorize(actor, slotId, "read");
652
+ if (!Object.hasOwn(env.policies, slotId)) throw new OperationError("not_found");
653
+ const state = await store.read();
654
+ return structuredClone({
655
+ pointer: state.pointers[slotId] ?? null,
656
+ drafts: Object.values(state.drafts).filter((draft) => draft.slotId === slotId)
657
+ });
658
+ },
659
+ createDraft: (actor, input) => createDraft(store, env, actor, input),
660
+ previewDraft: (actor, draftId) => previewDraft(store, env, actor, draftId),
661
+ approveDraft: (actor, input) => approveDraft(store, env, actor, input),
662
+ publishDraft: (actor, input) => commitDraft(store, env, actor, input, "publish"),
663
+ prepareRollback: (actor, input) => prepareRollback(store, env, actor, input),
664
+ rollbackSlot: (actor, input) => commitDraft(store, env, actor, input, "rollback"),
665
+ getStatus: (actor, slotId) => getStatus(store, env, actor, slotId),
666
+ deliver: (slotId) => deliver(store, env, slotId)
667
+ };
668
+ }
669
+ export {
670
+ OperationError,
671
+ approveDraft,
672
+ assertCurrentArtifact,
673
+ assertState,
674
+ authorize,
675
+ canonical,
676
+ commitDraft,
677
+ createDraft,
678
+ createOperations,
679
+ deliver,
680
+ emptyState,
681
+ getStatus,
682
+ hash,
683
+ openFileStore,
684
+ prepareRollback,
685
+ previewDraft,
686
+ readSource,
687
+ validateArtifact
688
+ };
package/package.json CHANGED
@@ -1,6 +1,46 @@
1
1
  {
2
2
  "name": "@ai-slot/operations",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.1.1",
4
+ "type": "module",
5
+ "engines": {
6
+ "node": ">=22"
7
+ },
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js"
12
+ }
13
+ },
14
+ "dependencies": {
15
+ "@ai-slot/registry": "0.1.2"
16
+ },
17
+ "devDependencies": {
18
+ "@types/node": "^22.0.0",
19
+ "tsup": "^8.3.0",
20
+ "typescript": "^5.6.0",
21
+ "vitest": "^2.1.0"
22
+ },
23
+ "license": "MIT",
24
+ "files": [
25
+ "dist"
26
+ ],
27
+ "publishConfig": {
28
+ "access": "public",
29
+ "registry": "https://registry.npmjs.org/"
30
+ },
31
+ "repository": {
32
+ "type": "git",
33
+ "url": "git+https://github.com/iannil/ai-slot-component.git",
34
+ "directory": "packages/operations"
35
+ },
36
+ "bugs": {
37
+ "url": "https://github.com/iannil/ai-slot-component/issues"
38
+ },
39
+ "homepage": "https://github.com/iannil/ai-slot-component/tree/master/packages/operations#readme",
40
+ "description": "Experimental single-writer local version store and approval/publish governance for AI-SLOT.",
41
+ "scripts": {
42
+ "build": "tsup src/index.ts --format esm --dts --clean",
43
+ "test": "vitest run",
44
+ "typecheck": "tsc --noEmit"
45
+ }
6
46
  }