@fungi.computer/oyster 0.0.0-stage → 0.1.0
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 +21 -0
- package/README.md +1134 -2
- package/dist/agents.d.ts +114 -0
- package/dist/agents.js +322 -0
- package/dist/app-builds.d.ts +210 -0
- package/dist/app-builds.js +499 -0
- package/dist/app-sandbox.d.ts +2 -0
- package/dist/app-sandbox.js +2 -0
- package/dist/app-sources.d.ts +31 -0
- package/dist/app-sources.js +370 -0
- package/dist/app-storage.d.ts +141 -0
- package/dist/app-storage.js +281 -0
- package/dist/apps/gate.d.ts +105 -0
- package/dist/apps/gate.js +273 -0
- package/dist/apps/index.d.ts +5 -0
- package/dist/apps/index.js +5 -0
- package/dist/apps/installation.d.ts +248 -0
- package/dist/apps/installation.js +307 -0
- package/dist/apps/operations.d.ts +8 -0
- package/dist/apps/operations.js +98 -0
- package/dist/apps/tools.d.ts +33 -0
- package/dist/apps/tools.js +17 -0
- package/dist/auth.d.ts +11 -0
- package/dist/auth.js +93 -0
- package/dist/backend-request.d.ts +3 -0
- package/dist/backend-request.js +53 -0
- package/dist/build/artifact-writer.d.ts +40 -0
- package/dist/build/artifact-writer.js +181 -0
- package/dist/build/build-jobs.d.ts +128 -0
- package/dist/build/build-jobs.js +203 -0
- package/dist/build/build-policy.d.ts +10 -0
- package/dist/build/build-policy.js +10 -0
- package/dist/build/build-step.d.ts +37 -0
- package/dist/build/build-step.js +28 -0
- package/dist/build/bundled-html.d.ts +7 -0
- package/dist/build/bundled-html.js +64 -0
- package/dist/build/candidate-builder.d.ts +68 -0
- package/dist/build/candidate-builder.js +91 -0
- package/dist/build/candidate-cli.d.ts +1 -0
- package/dist/build/candidate-cli.js +119 -0
- package/dist/build/candidate-cli.mjs +28170 -0
- package/dist/build/content-objects.d.ts +37 -0
- package/dist/build/content-objects.js +53 -0
- package/dist/build/definition-emission.d.ts +34 -0
- package/dist/build/definition-emission.js +201 -0
- package/dist/build/effect.d.ts +249 -0
- package/dist/build/effect.js +436 -0
- package/dist/build/git-environment.d.ts +9 -0
- package/dist/build/git-environment.js +60 -0
- package/dist/build/html-recipe.d.ts +20 -0
- package/dist/build/html-recipe.js +51 -0
- package/dist/build/local-build.d.ts +67 -0
- package/dist/build/local-build.js +1128 -0
- package/dist/build/snapshot-package-store.d.ts +12 -0
- package/dist/build/snapshot-package-store.js +36 -0
- package/dist/build/snapshot-program-scripts.d.ts +6 -0
- package/dist/build/snapshot-program-scripts.js +2 -0
- package/dist/build/snapshot-programs/snapshot-path.d.ts +2 -0
- package/dist/build/snapshot-programs/snapshot-path.js +6 -0
- package/dist/build/submission-contract.d.ts +828 -0
- package/dist/build/submission-contract.js +180 -0
- package/dist/build/workspace-build.d.ts +55 -0
- package/dist/build/workspace-build.js +200 -0
- package/dist/build-contract.d.ts +7 -0
- package/dist/build-contract.js +7 -0
- package/dist/build-egress.d.ts +62 -0
- package/dist/build-egress.js +148 -0
- package/dist/build-jobs.d.ts +2 -0
- package/dist/build-jobs.js +2 -0
- package/dist/build.d.ts +12 -0
- package/dist/build.js +14 -0
- package/dist/capability-descriptor.d.ts +46 -0
- package/dist/capability-descriptor.js +51 -0
- package/dist/catalog-frame.d.ts +39 -0
- package/dist/catalog-frame.js +72 -0
- package/dist/cause-class.d.ts +19 -0
- package/dist/cause-class.js +54 -0
- package/dist/cloudflare.d.ts +123 -0
- package/dist/cloudflare.js +391 -0
- package/dist/computer-terminal.d.ts +38 -0
- package/dist/computer-terminal.js +410 -0
- package/dist/computer.d.ts +60 -0
- package/dist/computer.js +33 -0
- package/dist/event-recipients.d.ts +57 -0
- package/dist/event-recipients.js +144 -0
- package/dist/facet-invocation.d.ts +4 -0
- package/dist/facet-invocation.js +45 -0
- package/dist/files.d.ts +42 -0
- package/dist/files.js +123 -0
- package/dist/frame-binding-wire-script.d.ts +2 -0
- package/dist/frame-binding-wire-script.js +1 -0
- package/dist/frame-binding-wire.d.ts +28 -0
- package/dist/frame-binding-wire.js +54 -0
- package/dist/git-admission.d.ts +52 -0
- package/dist/git-admission.js +188 -0
- package/dist/git-auth.d.ts +11 -0
- package/dist/git-auth.js +27 -0
- package/dist/git-door.d.ts +35 -0
- package/dist/git-door.js +108 -0
- package/dist/git-journal.d.ts +31 -0
- package/dist/git-journal.js +188 -0
- package/dist/git.d.ts +31 -0
- package/dist/git.js +64 -0
- package/dist/guest-policy.d.ts +9 -0
- package/dist/guest-policy.js +13 -0
- package/dist/host.d.ts +244 -0
- package/dist/host.js +1043 -0
- package/dist/index.d.ts +166 -0
- package/dist/index.js +320 -0
- package/dist/installation-consent.d.ts +79 -0
- package/dist/installation-consent.js +82 -0
- package/dist/internal/background-origins.d.ts +3 -0
- package/dist/internal/background-origins.js +8 -0
- package/dist/internal/delivery.d.ts +12 -0
- package/dist/internal/delivery.js +167 -0
- package/dist/internal/host-gate.d.ts +14 -0
- package/dist/internal/host-gate.js +62 -0
- package/dist/internal/registry-installations.d.ts +6 -0
- package/dist/internal/registry-installations.js +12 -0
- package/dist/invocation.d.ts +251 -0
- package/dist/invocation.js +377 -0
- package/dist/keys.d.ts +48 -0
- package/dist/keys.js +52 -0
- package/dist/mcp-owner.d.ts +27 -0
- package/dist/mcp-owner.js +155 -0
- package/dist/mcp.d.ts +89 -0
- package/dist/mcp.js +429 -0
- package/dist/media.d.ts +23 -0
- package/dist/media.js +12 -0
- package/dist/pack.d.ts +79 -0
- package/dist/pack.js +151 -0
- package/dist/page/capabilities.d.ts +23 -0
- package/dist/page/capabilities.js +256 -0
- package/dist/page/computer-handles.d.ts +79 -0
- package/dist/page/computer-handles.js +44 -0
- package/dist/page/gill-observation.d.ts +4 -0
- package/dist/page/gill-observation.js +31 -0
- package/dist/page/lifetime.d.ts +6 -0
- package/dist/page/lifetime.js +19 -0
- package/dist/page/mount.d.ts +163 -0
- package/dist/page/mount.js +390 -0
- package/dist/page/storage.d.ts +3 -0
- package/dist/page/storage.js +82 -0
- package/dist/page/terminal.d.ts +5 -0
- package/dist/page/terminal.js +7 -0
- package/dist/page/window-launch.d.ts +18 -0
- package/dist/page/window-launch.js +11 -0
- package/dist/page-script.d.ts +14 -0
- package/dist/page-script.js +5 -0
- package/dist/r2-files.d.ts +3 -0
- package/dist/r2-files.js +79 -0
- package/dist/release.d.ts +83 -0
- package/dist/release.js +167 -0
- package/dist/root-data.d.ts +86 -0
- package/dist/root-data.js +69 -0
- package/dist/shell/app-directory.d.ts +542 -0
- package/dist/shell/app-directory.js +199 -0
- package/dist/shell/browser-actions.d.ts +7 -0
- package/dist/shell/browser-actions.js +56 -0
- package/dist/shell/command-host.d.ts +112 -0
- package/dist/shell/command-host.js +502 -0
- package/dist/shell/commands.d.ts +25 -0
- package/dist/shell/commands.js +78 -0
- package/dist/shell/index.d.ts +8 -0
- package/dist/shell/index.js +8 -0
- package/dist/shell/keys.d.ts +41 -0
- package/dist/shell/keys.js +134 -0
- package/dist/shell/shell-module.d.ts +55 -0
- package/dist/shell/shell-module.js +86 -0
- package/dist/shell/window-manager.d.ts +124 -0
- package/dist/shell/window-manager.js +711 -0
- package/dist/shell/workbench-contract.d.ts +68 -0
- package/dist/shell/workbench-contract.js +1 -0
- package/dist/shell/workbench-directory.d.ts +332 -0
- package/dist/shell/workbench-directory.js +35 -0
- package/dist/shell/workbench-host-types.d.ts +96 -0
- package/dist/shell/workbench-host-types.js +1 -0
- package/dist/shell/workbench-host.d.ts +34 -0
- package/dist/shell/workbench-host.js +347 -0
- package/dist/shell/workbench-model.d.ts +192 -0
- package/dist/shell/workbench-model.js +53 -0
- package/dist/shell/workbench.d.ts +4 -0
- package/dist/shell/workbench.js +2 -0
- package/dist/source/fork.d.ts +27 -0
- package/dist/source/fork.js +46 -0
- package/dist/source/index.d.ts +10 -0
- package/dist/source/index.js +10 -0
- package/dist/source/seed-receipt.d.ts +53 -0
- package/dist/source/seed-receipt.js +18 -0
- package/dist/source/team-source.d.ts +32 -0
- package/dist/source/team-source.js +42 -0
- package/dist/source/template-push.d.ts +33 -0
- package/dist/source/template-push.js +223 -0
- package/dist/source/template-repository.d.ts +4 -0
- package/dist/source/template-repository.js +21 -0
- package/dist/source/template-seed.d.ts +68 -0
- package/dist/source/template-seed.js +80 -0
- package/dist/standalone/agent-directory.d.ts +4 -0
- package/dist/standalone/agent-directory.js +4 -0
- package/dist/standalone/agents.d.ts +220 -0
- package/dist/standalone/agents.js +795 -0
- package/dist/standalone/app-builds-binding.d.ts +36 -0
- package/dist/standalone/app-builds-binding.js +119 -0
- package/dist/standalone/app-set.d.ts +91 -0
- package/dist/standalone/app-set.js +395 -0
- package/dist/standalone/app-source.d.ts +41 -0
- package/dist/standalone/app-source.js +59 -0
- package/dist/standalone/builds.d.ts +18 -0
- package/dist/standalone/builds.js +101 -0
- package/dist/standalone/computer-egress.d.ts +37 -0
- package/dist/standalone/computer-egress.js +111 -0
- package/dist/standalone/computer.d.ts +122 -0
- package/dist/standalone/computer.js +714 -0
- package/dist/standalone/install-apps-cli.d.ts +2 -0
- package/dist/standalone/install-apps-cli.js +126 -0
- package/dist/standalone/instance.d.ts +35 -0
- package/dist/standalone/instance.js +74 -0
- package/dist/standalone/media.d.ts +76 -0
- package/dist/standalone/media.js +227 -0
- package/dist/standalone/preview.d.ts +51 -0
- package/dist/standalone/preview.js +167 -0
- package/dist/standalone/registry.d.ts +102 -0
- package/dist/standalone/registry.js +193 -0
- package/dist/standalone/tenancy.d.ts +42 -0
- package/dist/standalone/tenancy.js +241 -0
- package/dist/standalone/worker.d.ts +55 -0
- package/dist/standalone/worker.js +147 -0
- package/dist/standalone/workflow-authority-readers.d.ts +16 -0
- package/dist/standalone/workflow-authority-readers.js +35 -0
- package/dist/standalone/workflow-binding.d.ts +216 -0
- package/dist/standalone/workflow-binding.js +217 -0
- package/dist/standalone/workflow-events.d.ts +46 -0
- package/dist/standalone/workflow-events.js +39 -0
- package/dist/standalone/workflow-git-capture.d.ts +7 -0
- package/dist/standalone/workflow-git-capture.js +62 -0
- package/dist/standalone/workflow-host.d.ts +931 -0
- package/dist/standalone/workflow-host.js +751 -0
- package/dist/standalone/workflow-node-capture.d.ts +13 -0
- package/dist/standalone/workflow-node-capture.js +72 -0
- package/dist/standalone/workflow-root-authority.d.ts +27 -0
- package/dist/standalone/workflow-root-authority.js +87 -0
- package/dist/standalone/workflow-schedules.d.ts +52 -0
- package/dist/standalone/workflow-schedules.js +40 -0
- package/dist/standalone/workflow-trigger-host.d.ts +1694 -0
- package/dist/standalone/workflow-trigger-host.js +52 -0
- package/dist/standalone/workflows.d.ts +657 -0
- package/dist/standalone/workflows.js +554 -0
- package/dist/storage-frame.d.ts +64 -0
- package/dist/storage-frame.js +28 -0
- package/dist/tenancy/auth0.d.ts +64 -0
- package/dist/tenancy/auth0.js +198 -0
- package/dist/tenancy/cloudflare/rate-limit.d.ts +8 -0
- package/dist/tenancy/cloudflare/rate-limit.js +19 -0
- package/dist/tenancy/cloudflare/registry.d.ts +50 -0
- package/dist/tenancy/cloudflare/registry.js +301 -0
- package/dist/tenancy/cloudflare.d.ts +2 -0
- package/dist/tenancy/cloudflare.js +2 -0
- package/dist/tenancy/cold-directory.d.ts +13 -0
- package/dist/tenancy/cold-directory.js +43 -0
- package/dist/tenancy/contracts.d.ts +195 -0
- package/dist/tenancy/contracts.js +1 -0
- package/dist/tenancy/index.d.ts +19 -0
- package/dist/tenancy/index.js +156 -0
- package/dist/tenancy/owner-key.d.ts +26 -0
- package/dist/tenancy/owner-key.js +63 -0
- package/dist/tenancy/oyster.d.ts +4 -0
- package/dist/tenancy/oyster.js +25 -0
- package/dist/tenancy/registry-record.d.ts +7 -0
- package/dist/tenancy/registry-record.js +44 -0
- package/dist/tenancy/registry.d.ts +27 -0
- package/dist/tenancy/registry.js +138 -0
- package/dist/tenancy/resolve.d.ts +7 -0
- package/dist/tenancy/resolve.js +64 -0
- package/dist/tenant.d.ts +81 -0
- package/dist/tenant.js +71 -0
- package/dist/testing/capabilities.d.ts +29 -0
- package/dist/testing/capabilities.js +130 -0
- package/dist/testing/catalog.d.ts +3 -0
- package/dist/testing/catalog.js +53 -0
- package/dist/testing/index.d.ts +10 -0
- package/dist/testing/index.js +10 -0
- package/dist/testing/installation.d.ts +11 -0
- package/dist/testing/installation.js +25 -0
- package/dist/testing/native-worker.d.ts +21 -0
- package/dist/testing/native-worker.js +106 -0
- package/dist/testing/worker.d.ts +18 -0
- package/dist/testing/worker.js +67 -0
- package/dist/testing/workers-runtime.d.ts +24 -0
- package/dist/testing/workers-runtime.js +33 -0
- package/dist/webhook-delivery.d.ts +14 -0
- package/dist/webhook-delivery.js +17 -0
- package/dist/workers-git.d.ts +285 -0
- package/dist/workers-git.js +1174 -0
- package/dist/workers.d.ts +352 -0
- package/dist/workers.js +868 -0
- package/docs/api/README.md +82 -0
- package/docs/api/agents.md +332 -0
- package/docs/api/app-storage.md +209 -0
- package/docs/api/apps.md +1375 -0
- package/docs/api/auth.md +63 -0
- package/docs/api/build-contract.md +1304 -0
- package/docs/api/build.md +1712 -0
- package/docs/api/cloudflare.md +421 -0
- package/docs/api/computer.md +203 -0
- package/docs/api/files.md +117 -0
- package/docs/api/git.md +470 -0
- package/docs/api/host.md +1328 -0
- package/docs/api/index.md +430 -0
- package/docs/api/invocation.md +738 -0
- package/docs/api/keys.md +175 -0
- package/docs/api/mcp.md +301 -0
- package/docs/api/media.md +40 -0
- package/docs/api/modules.md +50 -0
- package/docs/api/pack.md +141 -0
- package/docs/api/page/mount.md +298 -0
- package/docs/api/root-data.md +111 -0
- package/docs/api/shell/app-directory.md +599 -0
- package/docs/api/shell/command-host.md +224 -0
- package/docs/api/shell/commands.md +197 -0
- package/docs/api/shell/keys.md +183 -0
- package/docs/api/shell/shell-module.md +204 -0
- package/docs/api/shell/window-manager.md +267 -0
- package/docs/api/shell/workbench-directory.md +119 -0
- package/docs/api/shell/workbench.md +205 -0
- package/docs/api/shell.md +1915 -0
- package/docs/api/tenancy/auth0.md +110 -0
- package/docs/api/tenancy/cloudflare.md +221 -0
- package/docs/api/tenancy/cold-directory.md +46 -0
- package/docs/api/tenancy/owner-key.md +63 -0
- package/docs/api/tenancy/registry.md +77 -0
- package/docs/api/tenancy/resolve.md +69 -0
- package/docs/api/tenancy.md +430 -0
- package/docs/api/tenant.md +194 -0
- package/docs/api/workers-git.md +1646 -0
- package/docs/api/workers.md +1691 -0
- package/docs/site/agent-quickstart.mdx +346 -0
- package/examples/standalone/worker.ts +7 -0
- package/examples/standalone/wrangler.jsonc +82 -0
- package/package.json +300 -3
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
import * as Effect from "effect/Effect";
|
|
2
|
+
import * as Schema from "effect/Schema";
|
|
3
|
+
import { appIdSchema, sha256DigestSchema, sourceRevisionSchema, } from "@fungi.computer/puffball/effect";
|
|
4
|
+
import { teamAppPublisherRegistrationSchema, } from "@fungi.computer/puffball/publisher";
|
|
5
|
+
import { capturedPinnedRecipeSchema, digestPinnedRecipe, } from "./build-contract.js";
|
|
6
|
+
import { sameReceipt, seedReceiptInputSchema, templateRepositoryForApp, } from "./source/index.js";
|
|
7
|
+
import { smartHttpExchange } from "./git-door.js";
|
|
8
|
+
import { parseAppId } from "@fungi.computer/puffball";
|
|
9
|
+
/**
|
|
10
|
+
* Oyster's own storage for App source templates, Team-owned fork
|
|
11
|
+
* registrations and the recorded Artifacts repository metadata their fork
|
|
12
|
+
* provenance depends on. Each factory is a pure adapter over a generic
|
|
13
|
+
* synchronous SQLite port, so it is unit-testable with a Node `DatabaseSync`
|
|
14
|
+
* and identically correct over a Durable Object's `ctx.storage.sql`.
|
|
15
|
+
*/
|
|
16
|
+
export function ensureAppSourceTables(sql) {
|
|
17
|
+
sql.exec(`CREATE TABLE IF NOT EXISTS oyster_app_sources (
|
|
18
|
+
app_id TEXT NOT NULL,
|
|
19
|
+
seed_revision TEXT NOT NULL,
|
|
20
|
+
source_revision TEXT NOT NULL,
|
|
21
|
+
source_tree_digest TEXT NOT NULL,
|
|
22
|
+
recipe_json TEXT NOT NULL,
|
|
23
|
+
build_recipe_digest TEXT NOT NULL,
|
|
24
|
+
PRIMARY KEY (app_id, seed_revision)
|
|
25
|
+
) STRICT`);
|
|
26
|
+
sql.exec(`CREATE TABLE IF NOT EXISTS oyster_app_registrations (
|
|
27
|
+
app_id TEXT PRIMARY KEY,
|
|
28
|
+
record TEXT NOT NULL
|
|
29
|
+
) STRICT`);
|
|
30
|
+
sql.exec(`CREATE TABLE IF NOT EXISTS oyster_artifacts_repositories (
|
|
31
|
+
repository_name TEXT PRIMARY KEY,
|
|
32
|
+
provider_id TEXT NOT NULL,
|
|
33
|
+
default_branch TEXT NOT NULL,
|
|
34
|
+
remote TEXT NOT NULL,
|
|
35
|
+
parent_repository TEXT
|
|
36
|
+
) STRICT`);
|
|
37
|
+
sql.exec(`CREATE TABLE IF NOT EXISTS oyster_artifacts_repositories_pending (
|
|
38
|
+
repository_name TEXT PRIMARY KEY,
|
|
39
|
+
provider_id TEXT NOT NULL,
|
|
40
|
+
default_branch TEXT NOT NULL,
|
|
41
|
+
remote TEXT NOT NULL,
|
|
42
|
+
parent_repository TEXT,
|
|
43
|
+
initial_token TEXT NOT NULL
|
|
44
|
+
) STRICT`);
|
|
45
|
+
}
|
|
46
|
+
const seedReceiptRowSchema = Schema.Struct({
|
|
47
|
+
app_id: appIdSchema,
|
|
48
|
+
seed_revision: sourceRevisionSchema,
|
|
49
|
+
source_revision: sourceRevisionSchema,
|
|
50
|
+
source_tree_digest: sha256DigestSchema,
|
|
51
|
+
recipe_json: Schema.String,
|
|
52
|
+
build_recipe_digest: sha256DigestSchema,
|
|
53
|
+
});
|
|
54
|
+
function receiptFromRow(row) {
|
|
55
|
+
return {
|
|
56
|
+
appId: row.app_id,
|
|
57
|
+
repository: templateRepositoryForApp(row.app_id),
|
|
58
|
+
sourceRevision: row.source_revision,
|
|
59
|
+
sourceTreeDigest: row.source_tree_digest,
|
|
60
|
+
seedRevision: row.seed_revision,
|
|
61
|
+
recipe: Schema.decodeUnknownSync(Schema.fromJsonString(capturedPinnedRecipeSchema), { onExcessProperty: "error" })(row.recipe_json),
|
|
62
|
+
buildRecipeDigest: row.build_recipe_digest,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
/** Append-only App source seed receipts, owned by one Oyster's registry. */
|
|
66
|
+
export function createOysterAppSourceReceipts(sql) {
|
|
67
|
+
const read = (appId, seedRevision) => {
|
|
68
|
+
const row = sql
|
|
69
|
+
.exec(`SELECT app_id, seed_revision, source_revision, source_tree_digest,
|
|
70
|
+
recipe_json, build_recipe_digest
|
|
71
|
+
FROM oyster_app_sources WHERE app_id = ? AND seed_revision = ?`, appId, seedRevision)
|
|
72
|
+
.toArray()[0];
|
|
73
|
+
if (row === undefined)
|
|
74
|
+
return Promise.resolve(null);
|
|
75
|
+
const receipt = receiptFromRow(Schema.decodeUnknownSync(seedReceiptRowSchema, {
|
|
76
|
+
onExcessProperty: "error",
|
|
77
|
+
})(row));
|
|
78
|
+
if (digestPinnedRecipe(receipt.recipe) !== receipt.buildRecipeDigest)
|
|
79
|
+
throw new Error("Oyster App source seed receipt recipe conflicts");
|
|
80
|
+
return Promise.resolve(receipt);
|
|
81
|
+
};
|
|
82
|
+
const record = async (untrusted) => {
|
|
83
|
+
const input = Schema.decodeUnknownSync(seedReceiptInputSchema, {
|
|
84
|
+
onExcessProperty: "error",
|
|
85
|
+
})(untrusted);
|
|
86
|
+
const receipt = {
|
|
87
|
+
...input,
|
|
88
|
+
repository: templateRepositoryForApp(input.appId),
|
|
89
|
+
};
|
|
90
|
+
if (digestPinnedRecipe(receipt.recipe) !== receipt.buildRecipeDigest)
|
|
91
|
+
throw new Error("Oyster App source seed recipe digest conflicts");
|
|
92
|
+
sql.exec(`INSERT OR IGNORE INTO oyster_app_sources (
|
|
93
|
+
app_id, seed_revision, source_revision, source_tree_digest,
|
|
94
|
+
recipe_json, build_recipe_digest
|
|
95
|
+
) VALUES (?, ?, ?, ?, ?, ?)`, receipt.appId, receipt.seedRevision, receipt.sourceRevision, receipt.sourceTreeDigest, JSON.stringify(receipt.recipe), receipt.buildRecipeDigest);
|
|
96
|
+
const stored = await read(receipt.appId, receipt.seedRevision);
|
|
97
|
+
if (stored === null || !sameReceipt(stored, receipt))
|
|
98
|
+
throw new Error("Oyster App source seed receipt conflicts");
|
|
99
|
+
return stored;
|
|
100
|
+
};
|
|
101
|
+
return Object.freeze({ read, record });
|
|
102
|
+
}
|
|
103
|
+
const registrationRowSchema = Schema.Struct({
|
|
104
|
+
app_id: Schema.String,
|
|
105
|
+
record: Schema.String,
|
|
106
|
+
});
|
|
107
|
+
/** Team-owned App publisher registrations (fork destinations), one per App. */
|
|
108
|
+
export function createOysterAppRegistrations(sql) {
|
|
109
|
+
const read = (appId) => {
|
|
110
|
+
const row = sql
|
|
111
|
+
.exec("SELECT app_id, record FROM oyster_app_registrations WHERE app_id = ?", appId)
|
|
112
|
+
.toArray()[0];
|
|
113
|
+
if (row === undefined)
|
|
114
|
+
return Promise.resolve({ kind: "absent" });
|
|
115
|
+
const parsed = Schema.decodeUnknownSync(registrationRowSchema, {
|
|
116
|
+
onExcessProperty: "error",
|
|
117
|
+
})(row);
|
|
118
|
+
return Promise.resolve({
|
|
119
|
+
kind: "found",
|
|
120
|
+
registration: Schema.decodeUnknownSync(teamAppPublisherRegistrationSchema, { onExcessProperty: "error" })(JSON.parse(parsed.record)),
|
|
121
|
+
});
|
|
122
|
+
};
|
|
123
|
+
return {
|
|
124
|
+
read,
|
|
125
|
+
register: async (registration) => {
|
|
126
|
+
const record = JSON.stringify(registration);
|
|
127
|
+
const inserted = sql
|
|
128
|
+
.exec(`INSERT INTO oyster_app_registrations (app_id, record) VALUES (?, ?)
|
|
129
|
+
ON CONFLICT (app_id) DO NOTHING
|
|
130
|
+
RETURNING app_id`, registration.appId, record)
|
|
131
|
+
.toArray();
|
|
132
|
+
const current = await read(registration.appId);
|
|
133
|
+
if (current.kind === "absent")
|
|
134
|
+
throw new Error("Oyster App registration was not retained");
|
|
135
|
+
const matches = JSON.stringify(current.registration) === record;
|
|
136
|
+
if (!matches)
|
|
137
|
+
return { kind: "conflict" };
|
|
138
|
+
return {
|
|
139
|
+
kind: inserted.length > 0 ? "created" : "existing",
|
|
140
|
+
registration: current.registration,
|
|
141
|
+
};
|
|
142
|
+
},
|
|
143
|
+
authorize: async (appId, teamId) => {
|
|
144
|
+
const current = await read(appId);
|
|
145
|
+
if (current.kind === "absent")
|
|
146
|
+
return current;
|
|
147
|
+
return current.registration.teamId === teamId
|
|
148
|
+
? { kind: "authorized", registration: current.registration }
|
|
149
|
+
: { kind: "denied" };
|
|
150
|
+
},
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
const repositoryMetadataSchema = Schema.Struct({
|
|
154
|
+
id: Schema.String.pipe(Schema.check(Schema.isMinLength(1))),
|
|
155
|
+
name: Schema.String.pipe(Schema.check(Schema.isMinLength(1)), Schema.check(Schema.isMaxLength(128))),
|
|
156
|
+
defaultBranch: Schema.Literal("main"),
|
|
157
|
+
remote: Schema.String.pipe(Schema.check(Schema.isMinLength(1))),
|
|
158
|
+
parentRepository: Schema.NullOr(Schema.String.pipe(Schema.check(Schema.isMinLength(1)), Schema.check(Schema.isMaxLength(128)))),
|
|
159
|
+
});
|
|
160
|
+
const repositoryRowSchema = Schema.Struct({
|
|
161
|
+
repository_name: Schema.String,
|
|
162
|
+
provider_id: Schema.String,
|
|
163
|
+
default_branch: Schema.String,
|
|
164
|
+
remote: Schema.String,
|
|
165
|
+
parent_repository: Schema.NullOr(Schema.String),
|
|
166
|
+
});
|
|
167
|
+
const pendingRepositoryRowSchema = Schema.Struct({
|
|
168
|
+
repository_name: Schema.String,
|
|
169
|
+
provider_id: Schema.String,
|
|
170
|
+
default_branch: Schema.String,
|
|
171
|
+
remote: Schema.String,
|
|
172
|
+
parent_repository: Schema.NullOr(Schema.String),
|
|
173
|
+
initial_token: Schema.String.pipe(Schema.check(Schema.isMinLength(1))),
|
|
174
|
+
});
|
|
175
|
+
function metadataFromRow(row) {
|
|
176
|
+
return Schema.decodeUnknownSync(repositoryMetadataSchema, {
|
|
177
|
+
onExcessProperty: "error",
|
|
178
|
+
})({
|
|
179
|
+
id: row.provider_id,
|
|
180
|
+
name: row.repository_name,
|
|
181
|
+
defaultBranch: row.default_branch,
|
|
182
|
+
remote: row.remote,
|
|
183
|
+
parentRepository: row.parent_repository,
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
function sameMetadata(left, right) {
|
|
187
|
+
return (left.id === right.id &&
|
|
188
|
+
left.name === right.name &&
|
|
189
|
+
left.defaultBranch === right.defaultBranch &&
|
|
190
|
+
left.remote === right.remote &&
|
|
191
|
+
left.parentRepository === right.parentRepository);
|
|
192
|
+
}
|
|
193
|
+
/** Durable repository provenance for Oyster's own `createCloudflareArtifacts`. */
|
|
194
|
+
export function createOysterArtifactsRepositoryMetadataStore(sql) {
|
|
195
|
+
const readPending = (name) => {
|
|
196
|
+
const row = sql
|
|
197
|
+
.exec(`SELECT repository_name, provider_id, default_branch, remote,
|
|
198
|
+
parent_repository, initial_token
|
|
199
|
+
FROM oyster_artifacts_repositories_pending WHERE repository_name = ?`, name)
|
|
200
|
+
.toArray()[0];
|
|
201
|
+
if (row === undefined)
|
|
202
|
+
return Promise.resolve(null);
|
|
203
|
+
const decoded = Schema.decodeUnknownSync(pendingRepositoryRowSchema, {
|
|
204
|
+
onExcessProperty: "error",
|
|
205
|
+
})(row);
|
|
206
|
+
return Promise.resolve(Object.freeze({
|
|
207
|
+
metadata: metadataFromRow(decoded),
|
|
208
|
+
initialToken: decoded.initial_token,
|
|
209
|
+
}));
|
|
210
|
+
};
|
|
211
|
+
/** The active row alone; activation checks its own promotion with this. */
|
|
212
|
+
const readActive = (name) => {
|
|
213
|
+
const row = sql
|
|
214
|
+
.exec(`SELECT repository_name, provider_id, default_branch, remote,
|
|
215
|
+
parent_repository
|
|
216
|
+
FROM oyster_artifacts_repositories WHERE repository_name = ?`, name)
|
|
217
|
+
.toArray()[0];
|
|
218
|
+
return Promise.resolve(row === undefined
|
|
219
|
+
? null
|
|
220
|
+
: metadataFromRow(Schema.decodeUnknownSync(repositoryRowSchema, {
|
|
221
|
+
onExcessProperty: "error",
|
|
222
|
+
})(row)));
|
|
223
|
+
};
|
|
224
|
+
/** Public reads hide a repository while its initial token is pending. */
|
|
225
|
+
const read = async (name) => (await readPending(name)) === null ? readActive(name) : null;
|
|
226
|
+
return Object.freeze({
|
|
227
|
+
read,
|
|
228
|
+
readPending,
|
|
229
|
+
recordPending: async (untrusted, initialToken) => {
|
|
230
|
+
const metadata = Schema.decodeUnknownSync(repositoryMetadataSchema, {
|
|
231
|
+
onExcessProperty: "error",
|
|
232
|
+
})(untrusted);
|
|
233
|
+
sql.exec(`INSERT OR IGNORE INTO oyster_artifacts_repositories_pending (
|
|
234
|
+
repository_name, provider_id, default_branch, remote,
|
|
235
|
+
parent_repository, initial_token
|
|
236
|
+
) VALUES (?, ?, ?, ?, ?, ?)`, metadata.name, metadata.id, metadata.defaultBranch, metadata.remote, metadata.parentRepository, initialToken);
|
|
237
|
+
const pending = await readPending(metadata.name);
|
|
238
|
+
if (pending !== null) {
|
|
239
|
+
if (!sameMetadata(pending.metadata, metadata) ||
|
|
240
|
+
pending.initialToken !== initialToken)
|
|
241
|
+
throw new Error("Oyster Artifacts recovery state conflicts");
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
const stored = await read(metadata.name);
|
|
245
|
+
if (stored === null || !sameMetadata(stored, metadata))
|
|
246
|
+
throw new Error("Oyster Artifacts recovery state was not saved");
|
|
247
|
+
},
|
|
248
|
+
activate: async (untrusted, initialToken) => {
|
|
249
|
+
const metadata = Schema.decodeUnknownSync(repositoryMetadataSchema, {
|
|
250
|
+
onExcessProperty: "error",
|
|
251
|
+
})(untrusted);
|
|
252
|
+
sql.exec(`INSERT OR IGNORE INTO oyster_artifacts_repositories (
|
|
253
|
+
repository_name, provider_id, default_branch, remote, parent_repository
|
|
254
|
+
) SELECT repository_name, provider_id, default_branch, remote, parent_repository
|
|
255
|
+
FROM oyster_artifacts_repositories_pending
|
|
256
|
+
WHERE repository_name = ? AND provider_id = ? AND default_branch = ?
|
|
257
|
+
AND remote = ? AND parent_repository IS ? AND initial_token = ?`, metadata.name, metadata.id, metadata.defaultBranch, metadata.remote, metadata.parentRepository, initialToken);
|
|
258
|
+
const active = await readActive(metadata.name);
|
|
259
|
+
if (active === null || !sameMetadata(active, metadata))
|
|
260
|
+
throw new Error("Oyster Artifacts metadata conflicts");
|
|
261
|
+
sql.exec(`DELETE FROM oyster_artifacts_repositories_pending
|
|
262
|
+
WHERE repository_name = ? AND provider_id = ? AND default_branch = ?
|
|
263
|
+
AND remote = ? AND parent_repository IS ? AND initial_token = ?`, metadata.name, metadata.id, metadata.defaultBranch, metadata.remote, metadata.parentRepository, initialToken);
|
|
264
|
+
},
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
/** Translate one local Git smart-HTTP request into an admitted Artifacts
|
|
268
|
+
* exchange; the identical shape Hub's Publisher keeps for its own template
|
|
269
|
+
* writes (`services/app-publisher/src/source-git.ts`), scoped to Oyster's own
|
|
270
|
+
* Artifacts composition since a package cannot depend on that service. */
|
|
271
|
+
export async function exchangeOysterTemplateGit(artifacts, remote, repository, request, permission = "write") {
|
|
272
|
+
const url = new URL(request.url);
|
|
273
|
+
const registered = new URL(remote);
|
|
274
|
+
const internal = url.origin === "https://git.team.internal";
|
|
275
|
+
const base = internal ? `/${repository}.git` : registered.pathname;
|
|
276
|
+
if (url.username ||
|
|
277
|
+
url.password ||
|
|
278
|
+
url.hash ||
|
|
279
|
+
(!internal && url.origin !== registered.origin))
|
|
280
|
+
return new Response(null, { status: 403 });
|
|
281
|
+
const protocol = request.headers.get("Git-Protocol");
|
|
282
|
+
const signal = AbortSignal.any([
|
|
283
|
+
request.signal,
|
|
284
|
+
AbortSignal.timeout(120_000),
|
|
285
|
+
]);
|
|
286
|
+
const common = { repository, signal };
|
|
287
|
+
let input;
|
|
288
|
+
if (request.method === "GET" && url.pathname === `${base}/info/refs`) {
|
|
289
|
+
if (url.search === "?service=git-upload-pack")
|
|
290
|
+
input = { ...common, operation: "upload-pack-advertisement" };
|
|
291
|
+
else if (url.search === "?service=git-receive-pack")
|
|
292
|
+
input = { ...common, operation: "receive-pack-advertisement" };
|
|
293
|
+
else
|
|
294
|
+
return new Response(null, { status: 403 });
|
|
295
|
+
}
|
|
296
|
+
else if (request.method === "POST" && url.search === "") {
|
|
297
|
+
const raw = request.body;
|
|
298
|
+
if (raw === null)
|
|
299
|
+
return new Response(null, { status: 403 });
|
|
300
|
+
// A real identity passthrough, not a type assertion: the ambient
|
|
301
|
+
// `Request.body` resolves to `ReadableStream<any>` in this workspace's
|
|
302
|
+
// merged lib types, so this pipes it through a stream genuinely typed
|
|
303
|
+
// `ReadableStream<Uint8Array>` for `GitExchange`'s own `body` field.
|
|
304
|
+
const body = raw.pipeThrough(new TransformStream());
|
|
305
|
+
if (url.pathname === `${base}/git-upload-pack`)
|
|
306
|
+
input = { ...common, operation: "upload-pack", body };
|
|
307
|
+
else if (url.pathname === `${base}/git-receive-pack`)
|
|
308
|
+
input = { ...common, operation: "receive-pack", body };
|
|
309
|
+
else
|
|
310
|
+
return new Response(null, { status: 403 });
|
|
311
|
+
}
|
|
312
|
+
else
|
|
313
|
+
return new Response(null, { status: 403 });
|
|
314
|
+
if (protocol !== null)
|
|
315
|
+
input = { ...input, protocol };
|
|
316
|
+
if (permission === "read" && input.operation.startsWith("receive-pack"))
|
|
317
|
+
return new Response(null, { status: 403 });
|
|
318
|
+
const response = await Effect.runPromise(Effect.tryPromise(() => artifacts.exchangeGit(input)), { signal });
|
|
319
|
+
const type = `application/x-git-${input.operation.startsWith("upload-pack") ? "upload-pack" : "receive-pack"}-${input.operation.endsWith("advertisement") ? "advertisement" : "result"}`;
|
|
320
|
+
if (!response.ok || response.headers.get("content-type") !== type) {
|
|
321
|
+
await response.body?.cancel();
|
|
322
|
+
return new Response(null, { status: 503 });
|
|
323
|
+
}
|
|
324
|
+
return new Response(response.body, {
|
|
325
|
+
status: response.status,
|
|
326
|
+
headers: { "Content-Type": type, "Cache-Control": "no-store" },
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
/** The operator's smart-HTTP door to one App's template repository: an
|
|
330
|
+
* ordinary `git push` (or the fetch before it) to
|
|
331
|
+
* `<base>/<suffix>` reaches `template-<app>` through the same admitted
|
|
332
|
+
* Artifacts exchange, which streams both bodies and holds every provider
|
|
333
|
+
* credential. An advertisement first opens the repository, so a fresh Oyster's
|
|
334
|
+
* first seed creates it. Admission (the owner) is the caller's. */
|
|
335
|
+
export async function exchangeOysterTemplateSource(artifacts, rawAppId, suffix, request) {
|
|
336
|
+
let appId;
|
|
337
|
+
try {
|
|
338
|
+
appId = parseAppId(rawAppId);
|
|
339
|
+
}
|
|
340
|
+
catch {
|
|
341
|
+
return new Response("Not an App", { status: 404 });
|
|
342
|
+
}
|
|
343
|
+
const url = new URL(request.url);
|
|
344
|
+
const exchange = smartHttpExchange(request.method, url, suffix);
|
|
345
|
+
if (exchange === null)
|
|
346
|
+
return new Response("Not a git route", { status: 404 });
|
|
347
|
+
// The provider exchange carries no content coding; refuse rather than corrupt.
|
|
348
|
+
if (request.headers.has("content-encoding"))
|
|
349
|
+
return new Response("Compressed Git requests are not supported", {
|
|
350
|
+
status: 400,
|
|
351
|
+
});
|
|
352
|
+
const repository = templateRepositoryForApp(appId);
|
|
353
|
+
if (exchange.advertise)
|
|
354
|
+
await artifacts.ensureRepository(repository);
|
|
355
|
+
const headers = new Headers();
|
|
356
|
+
for (const name of ["content-type", "accept", "git-protocol"]) {
|
|
357
|
+
const value = request.headers.get(name);
|
|
358
|
+
if (value !== null)
|
|
359
|
+
headers.set(name, value);
|
|
360
|
+
}
|
|
361
|
+
const init = {
|
|
362
|
+
method: request.method,
|
|
363
|
+
headers,
|
|
364
|
+
body: exchange.advertise ? null : request.body,
|
|
365
|
+
signal: request.signal,
|
|
366
|
+
};
|
|
367
|
+
if (init.body !== null)
|
|
368
|
+
Object.assign(init, { duplex: "half" });
|
|
369
|
+
return exchangeOysterTemplateGit(artifacts, `https://git.team.internal/${repository}.git`, repository, new Request(`https://git.team.internal/${repository}.git/${suffix}${url.search}`, init), "write");
|
|
370
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `env.APP_STORAGE`: the App's own key/value store, as its backend receives
|
|
3
|
+
* it. One contract for every Oyster host (standalone and the Fungi Hub).
|
|
4
|
+
*
|
|
5
|
+
* - **Same handle everywhere.** Method handlers and lifecycle handlers (such
|
|
6
|
+
* as `onGitPush`) run in the same facet and receive the same `env`. When the
|
|
7
|
+
* Release declares the `APP_STORAGE` binding, both get this handle with the
|
|
8
|
+
* same operations.
|
|
9
|
+
* - **Who writes.** In a method call, a member writes their own `member`
|
|
10
|
+
* layer; an admitted owner may also write the `team` layer, as from the UI.
|
|
11
|
+
* A lifecycle handler has no
|
|
12
|
+
* member, so it writes the installation's shared `team` layer; a `member`
|
|
13
|
+
* write from a lifecycle handler is rejected with `owner_required`.
|
|
14
|
+
* - **Visible to watchers.** Every accepted write or reset is committed by the
|
|
15
|
+
* host's storage owner, bumps the key's revision and is published to
|
|
16
|
+
* `appStorage.watch` subscribers, exactly like a write from the App's UI.
|
|
17
|
+
* A push that writes a key therefore refreshes every open UI watching it.
|
|
18
|
+
* - **Same limits.** Declared keys, scopes, per-key size and quotas, and
|
|
19
|
+
* `basedOn` compare-and-set apply unchanged; rejections use Agar's
|
|
20
|
+
* `AppStorageError` codes.
|
|
21
|
+
*/
|
|
22
|
+
import * as Effect from "effect/Effect";
|
|
23
|
+
import * as Schema from "effect/Schema";
|
|
24
|
+
import type { WatchdogSqliteOwner } from "@fungi.computer/watchdog";
|
|
25
|
+
import { type AppStorageKey } from "@fungi.computer/puffball";
|
|
26
|
+
import * as Stream from "effect/Stream";
|
|
27
|
+
import { AppStorageError, type AppStorageResult, type AppStorageWrite, type AppStorageReset, type ResolvedAppStorage } from "@fungi.computer/agar/storage";
|
|
28
|
+
/** The App storage handle in `env.APP_STORAGE`. */
|
|
29
|
+
export type AppStorageBinding = Readonly<{
|
|
30
|
+
/** Read one declared key, resolved across the team and member layers. */
|
|
31
|
+
read(key: string): Promise<AppStorageResult<ResolvedAppStorage>>;
|
|
32
|
+
/** Write one layer of a declared key; `basedOn` is the expected revision. */
|
|
33
|
+
write(input: AppStorageWrite): Promise<AppStorageResult<{
|
|
34
|
+
revision: number;
|
|
35
|
+
}>>;
|
|
36
|
+
/** Remove one layer of a declared key. */
|
|
37
|
+
reset(input: AppStorageReset): Promise<AppStorageResult<{
|
|
38
|
+
revision: number;
|
|
39
|
+
}>>;
|
|
40
|
+
}>;
|
|
41
|
+
/** Trusted backend writer: owners may also edit the Team layer; lifecycle has no member. */
|
|
42
|
+
export type AppStorageWriter = Readonly<{
|
|
43
|
+
kind: "member";
|
|
44
|
+
memberId: string;
|
|
45
|
+
isOwner: boolean;
|
|
46
|
+
}> | Readonly<{
|
|
47
|
+
kind: "lifecycle";
|
|
48
|
+
handler: string;
|
|
49
|
+
}>;
|
|
50
|
+
/** Whether `writer` may mutate `scope`; anything else is `owner_required`. */
|
|
51
|
+
export declare function appStorageWritable(writer: AppStorageWriter, scope: AppStorageWrite["scope"]): boolean;
|
|
52
|
+
/** Trusted live App admission, never supplied by the App or an HTTP body. */
|
|
53
|
+
export type AppStorageAdmission = Readonly<{
|
|
54
|
+
address: string;
|
|
55
|
+
appId: string;
|
|
56
|
+
memberId: string;
|
|
57
|
+
membershipVersion: number;
|
|
58
|
+
isOwner: boolean;
|
|
59
|
+
keys: readonly AppStorageKey[];
|
|
60
|
+
/** Recheck local membership, installation and Release inside the transaction. */
|
|
61
|
+
assertCurrent: () => void;
|
|
62
|
+
}>;
|
|
63
|
+
/** Trusted installation lifecycle admission, with no member identity or private layer. */
|
|
64
|
+
export type AppStorageInstallationAdmission = Pick<AppStorageAdmission, "address" | "appId" | "keys" | "assertCurrent">;
|
|
65
|
+
/** SQLite tables, indexes and initial revision state used by the App storage owner. */
|
|
66
|
+
export declare const appStorageSchemaStatements: readonly ["CREATE TABLE IF NOT EXISTS hub_app_storage (\n address TEXT NOT NULL, app_id TEXT NOT NULL, key TEXT NOT NULL,\n scope TEXT NOT NULL CHECK(scope IN ('team','member')), member_id TEXT NOT NULL, membership_version INTEGER NOT NULL,\n value TEXT NOT NULL, revision INTEGER NOT NULL, bytes INTEGER NOT NULL,\n PRIMARY KEY(address,app_id,key,scope,member_id,membership_version),\n CHECK((scope='team' AND member_id='' AND membership_version=0) OR (scope='member' AND member_id<>'' AND membership_version>0))) STRICT", "CREATE INDEX IF NOT EXISTS hub_app_storage_member ON hub_app_storage(member_id) WHERE scope='member'", "CREATE TABLE IF NOT EXISTS hub_app_storage_clock (id INTEGER PRIMARY KEY CHECK(id=1), revision INTEGER NOT NULL) STRICT", "INSERT OR IGNORE INTO hub_app_storage_clock VALUES(1,0)", "CREATE TABLE IF NOT EXISTS hub_app_storage_writes (address TEXT NOT NULL, member_id TEXT NOT NULL, membership_version INTEGER NOT NULL, started_at INTEGER NOT NULL, count INTEGER NOT NULL, PRIMARY KEY(address,member_id,membership_version)) STRICT"];
|
|
67
|
+
/** Called only inside the installation owner's delete transaction. */
|
|
68
|
+
export declare function purgeAppStorageInCommit(sql: WatchdogSqliteOwner["sql"], address: string): void;
|
|
69
|
+
/** Called only inside the membership owner's removal transaction. */
|
|
70
|
+
export declare function purgeAppMemberStorageInCommit(sql: WatchdogSqliteOwner["sql"], memberId: string, throughVersion: number): void;
|
|
71
|
+
/** One SQLite owner for scoped App values; its views bind the admitted member. */
|
|
72
|
+
export declare function createAppStorage(options: {
|
|
73
|
+
/** The SQLite connection shared with installation and membership changes. */
|
|
74
|
+
sql: WatchdogSqliteOwner["sql"];
|
|
75
|
+
/** Millisecond clock used for the existing per-principal write-rate limit. */
|
|
76
|
+
now?: () => number;
|
|
77
|
+
/** Commit the supplied mutation atomically before publishing its invalidation. */
|
|
78
|
+
commit: <A>(body: () => A) => Effect.Effect<A, Error>;
|
|
79
|
+
}): {
|
|
80
|
+
/** Remove an installation's values and write counters inside its delete transaction. */
|
|
81
|
+
purgeInstallation: (address: string) => void;
|
|
82
|
+
/** Remove a member's private values and counters through the revoked membership version. */
|
|
83
|
+
purgeMember: (memberId: string, throughVersion: number) => void;
|
|
84
|
+
/** Bind a lifecycle view to fresh installation admission; only declared team layers are accessible. */
|
|
85
|
+
openInstallation(authorize: () => Effect.Effect<AppStorageInstallationAdmission, Error>): {
|
|
86
|
+
/** Read the declared team layer without exposing any member's private value. */
|
|
87
|
+
read: (key: string) => Effect.Effect<{
|
|
88
|
+
readonly value: Schema.Json;
|
|
89
|
+
readonly source: "team" | "member" | "none";
|
|
90
|
+
readonly team: {
|
|
91
|
+
readonly value: Schema.Json;
|
|
92
|
+
readonly revision: number;
|
|
93
|
+
} | null;
|
|
94
|
+
readonly member: {
|
|
95
|
+
readonly value: Schema.Json;
|
|
96
|
+
readonly revision: number;
|
|
97
|
+
} | null;
|
|
98
|
+
readonly canWriteTeam: boolean;
|
|
99
|
+
}, AppStorageError, never>;
|
|
100
|
+
/** Commit a declared team-layer write and publish its new revision to watchers. */
|
|
101
|
+
write: (input: AppStorageWrite) => Effect.Effect<{
|
|
102
|
+
revision: number;
|
|
103
|
+
}, AppStorageError, never>;
|
|
104
|
+
/** Reset a declared team layer; member scope is rejected with owner_required. */
|
|
105
|
+
reset: (input: AppStorageReset) => Effect.Effect<{
|
|
106
|
+
revision: number;
|
|
107
|
+
}, AppStorageError, never>;
|
|
108
|
+
};
|
|
109
|
+
/** Bind a member view to live admission, declared keys and the current membership version. */
|
|
110
|
+
open(authorize: () => Effect.Effect<AppStorageAdmission, Error>): {
|
|
111
|
+
/** Resolve declared member and team layers under current live admission. */
|
|
112
|
+
read: (key: string) => Effect.Effect<{
|
|
113
|
+
readonly value: Schema.Json;
|
|
114
|
+
readonly source: "team" | "member" | "none";
|
|
115
|
+
readonly team: {
|
|
116
|
+
readonly value: Schema.Json;
|
|
117
|
+
readonly revision: number;
|
|
118
|
+
} | null;
|
|
119
|
+
readonly member: {
|
|
120
|
+
readonly value: Schema.Json;
|
|
121
|
+
readonly revision: number;
|
|
122
|
+
} | null;
|
|
123
|
+
readonly canWriteTeam: boolean;
|
|
124
|
+
}, AppStorageError, never>;
|
|
125
|
+
/** Commit an admitted layer write with revision and quota checks, then notify watchers. */
|
|
126
|
+
write: (input: AppStorageWrite) => Effect.Effect<{
|
|
127
|
+
revision: number;
|
|
128
|
+
}, AppStorageError, never>;
|
|
129
|
+
/** Remove an admitted layer at its expected revision and notify watchers. */
|
|
130
|
+
reset: (input: AppStorageReset) => Effect.Effect<{
|
|
131
|
+
revision: number;
|
|
132
|
+
}, AppStorageError, never>;
|
|
133
|
+
/** One bounded invalidation stream. Null marks an armed subscription. */
|
|
134
|
+
events: (key: string) => Stream.Stream<{
|
|
135
|
+
readonly appId: string;
|
|
136
|
+
readonly key: string;
|
|
137
|
+
readonly scope: "team" | "member";
|
|
138
|
+
readonly revision: number;
|
|
139
|
+
} | null, AppStorageError, never>;
|
|
140
|
+
};
|
|
141
|
+
};
|