@fungi.computer/oyster 0.6.0 → 0.8.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/README.md +133 -78
- package/dist/agent-archive-projection.d.ts +8 -0
- package/dist/agent-archive-projection.js +49 -0
- package/dist/agent-schedules.d.ts +61 -0
- package/dist/agent-schedules.js +19 -0
- package/dist/agents.d.ts +35 -14
- package/dist/agents.js +93 -144
- package/dist/app-builds.d.ts +7 -2
- package/dist/app-builds.js +19 -2
- package/dist/approval-capability.d.ts +1 -1
- package/dist/approval-capability.js +11 -16
- package/dist/approval-home.d.ts +2 -1
- package/dist/approval-home.js +5 -3
- package/dist/apps/catalog.js +1 -1
- package/dist/apps/gate.d.ts +4 -7
- package/dist/apps/gate.js +14 -29
- package/dist/apps/index.d.ts +1 -1
- package/dist/apps/installation.d.ts +4 -42
- package/dist/apps/installation.js +7 -75
- package/dist/apps/operations.js +0 -10
- package/dist/apps/permissions.d.ts +18 -8
- package/dist/apps/permissions.js +7 -1
- package/dist/auth.d.ts +1 -24
- package/dist/auth.js +9 -39
- package/dist/authorization.d.ts +307 -0
- package/dist/authorization.js +542 -0
- package/dist/boot-host-script.js +1 -1
- package/dist/build/build-jobs.d.ts +2 -2
- package/dist/build/candidate-builder.d.ts +2 -0
- package/dist/build/candidate-builder.js +5 -1
- package/dist/build/candidate-cli.js +3 -0
- package/dist/build/candidate-cli.mjs +70 -22
- package/dist/build/job-log.d.ts +23 -0
- package/dist/build/job-log.js +88 -0
- package/dist/build/local-build.d.ts +9 -1
- package/dist/build/local-build.js +11 -9
- package/dist/cloudflare.d.ts +4 -1
- package/dist/cloudflare.js +1 -1
- package/dist/files.js +9 -8
- package/dist/git-admission.d.ts +3 -3
- package/dist/git-admission.js +4 -6
- package/dist/git-door.d.ts +8 -7
- package/dist/git-door.js +18 -17
- package/dist/git.d.ts +1 -1
- package/dist/host.d.ts +33 -40
- package/dist/host.js +156 -275
- package/dist/installation-execution.js +14 -5
- package/dist/installation-operations.d.ts +4 -180
- package/dist/installation-operations.js +1 -5
- package/dist/installation-store-generation.d.ts +4 -1
- package/dist/installation-store-generation.js +59 -6
- package/dist/internal/delivery.js +0 -1
- package/dist/internal/host-gate.d.ts +3 -2
- package/dist/internal/host-gate.js +5 -18
- package/dist/internal/invocation-timing.d.ts +16 -0
- package/dist/internal/invocation-timing.js +23 -0
- package/dist/internal/registry-installations.js +1 -0
- package/dist/invocation.d.ts +15 -14
- package/dist/invocation.js +58 -26
- package/dist/keys.d.ts +2 -19
- package/dist/keys.js +1 -37
- package/dist/mcp.d.ts +15 -27
- package/dist/mcp.js +105 -155
- package/dist/page/agent-browser.d.ts +15 -0
- package/dist/page/agent-browser.js +162 -0
- package/dist/page/app-permissions-port.d.ts +3 -0
- package/dist/page/app-permissions-port.js +43 -0
- package/dist/page/app-permissions.d.ts +11 -0
- package/dist/page/app-permissions.js +296 -0
- package/dist/page/isolated-page-theme.d.ts +2 -0
- package/dist/page/isolated-page-theme.js +11 -0
- package/dist/page/mount.d.ts +2 -13
- package/dist/page/mount.js +7 -93
- package/dist/page-script.d.ts +0 -3
- package/dist/page-script.js +3 -4
- package/dist/standalone/agent-archive-owner.d.ts +42 -0
- package/dist/standalone/agent-archive-owner.js +116 -0
- package/dist/standalone/agent-archive-schedules.d.ts +6 -0
- package/dist/standalone/agent-archive-schedules.js +76 -0
- package/dist/standalone/agent-archive.d.ts +31 -0
- package/dist/standalone/agent-archive.js +67 -0
- package/dist/standalone/agent-computers.d.ts +7 -6
- package/dist/standalone/agent-computers.js +4 -7
- package/dist/standalone/agent-mail.d.ts +2 -2
- package/dist/standalone/agents.d.ts +186 -20
- package/dist/standalone/agents.js +251 -30
- package/dist/standalone/app-builds-binding.js +1 -1
- package/dist/standalone/app-set.js +2 -54
- package/dist/standalone/app-source.d.ts +3 -4
- package/dist/standalone/app-source.js +12 -9
- package/dist/standalone/builds.d.ts +2 -14
- package/dist/standalone/builds.js +15 -13
- package/dist/standalone/computer-fleet.d.ts +4 -4
- package/dist/standalone/computer-fleet.js +13 -42
- package/dist/standalone/computer.d.ts +4 -3
- package/dist/standalone/computer.js +52 -53
- package/dist/standalone/dashboard-approval.d.ts +10 -0
- package/dist/standalone/dashboard-approval.js +66 -0
- package/dist/standalone/machine-link.d.ts +4 -221
- package/dist/standalone/machine-link.js +21 -15
- package/dist/standalone/media.d.ts +1 -0
- package/dist/standalone/media.js +20 -14
- package/dist/standalone/owner-routes.d.ts +16 -0
- package/dist/standalone/owner-routes.js +21 -0
- package/dist/standalone/permission-resources.d.ts +5 -0
- package/dist/standalone/permission-resources.js +31 -0
- package/dist/standalone/registry.d.ts +7 -0
- package/dist/standalone/registry.js +3 -0
- package/dist/standalone/tenancy.d.ts +0 -3
- package/dist/standalone/tenancy.js +2 -5
- package/dist/standalone/worker-routes.d.ts +19 -0
- package/dist/standalone/worker-routes.js +9 -0
- package/dist/standalone/worker.d.ts +1 -1
- package/dist/standalone/worker.js +27 -35
- package/dist/standalone/workflow-binding.d.ts +10 -3
- package/dist/standalone/workflow-binding.js +22 -5
- package/dist/standalone/workflow-git-capture.js +2 -1
- package/dist/standalone/workflow-host.d.ts +8 -103
- package/dist/standalone/workflow-host.js +1 -1
- package/dist/standalone/workflow-node-capture.js +2 -2
- package/dist/standalone/workflow-root-authority.d.ts +1 -1
- package/dist/standalone/workflow-root-authority.js +1 -1
- package/dist/standalone/workflow-schedules.d.ts +31 -4
- package/dist/standalone/workflow-schedules.js +18 -5
- package/dist/standalone/workflow-webhooks.d.ts +4 -18
- package/dist/standalone/workflow-webhooks.js +5 -5
- package/dist/standalone/workflows.d.ts +53 -27
- package/dist/standalone/workflows.js +9 -3
- package/dist/storage-frame.d.ts +6 -6
- package/dist/testing/capabilities.d.ts +7 -6
- package/dist/testing/capabilities.js +3 -6
- package/dist/testing/installation.d.ts +0 -1
- package/dist/testing/installation.js +2 -5
- package/dist/workers-git.d.ts +12 -4
- package/dist/workers-git.js +38 -35
- package/dist/workers.d.ts +9 -1
- package/dist/workers.js +44 -8
- package/dist/workflows.d.ts +4 -0
- package/dist/workflows.js +2 -0
- package/docs/api/README.md +7 -0
- package/docs/api/agent-schedules.md +176 -0
- package/docs/api/agents.md +34 -13
- package/docs/api/app-storage.md +1 -1
- package/docs/api/apps.md +323 -225
- package/docs/api/auth.md +0 -55
- package/docs/api/authorization.md +1300 -0
- package/docs/api/build.md +42 -38
- package/docs/api/cloudflare.md +3 -3
- package/docs/api/git.md +20 -11
- package/docs/api/host.md +64 -192
- package/docs/api/index.md +1 -1
- package/docs/api/invocation.md +14 -14
- package/docs/api/keys.md +1 -98
- package/docs/api/mcp.md +43 -40
- package/docs/api/modules.md +2 -0
- package/docs/api/page/mount.md +16 -19
- package/docs/api/workers-git.md +46 -12
- package/docs/api/workers.md +111 -222
- package/package.json +41 -22
- package/dist/page/computer-target.d.ts +0 -3
- package/dist/page/computer-target.js +0 -9
- package/dist/standalone/channel-session-door.d.ts +0 -4
- package/dist/standalone/channel-session-door.js +0 -72
package/dist/apps/gate.js
CHANGED
|
@@ -59,9 +59,15 @@ export const gateRefusalSchema = z.lazy(() => z.strictObject({
|
|
|
59
59
|
"approval_required",
|
|
60
60
|
"requires_not_granted",
|
|
61
61
|
"invalid_input",
|
|
62
|
-
"visitor_read_only",
|
|
63
62
|
]),
|
|
64
63
|
}));
|
|
64
|
+
/** Availability uses "any"; a selected target must match both its kind and identity. */
|
|
65
|
+
function grantsResource(resources, resource) {
|
|
66
|
+
return (resources === "tenant-scope" ||
|
|
67
|
+
(resource === "any"
|
|
68
|
+
? resources.length > 0
|
|
69
|
+
: resources.some((ref) => ref.kind === resource.kind && ref.id === resource.id)));
|
|
70
|
+
}
|
|
65
71
|
/** Intersect stored approvals with the current verified Release without widening resource selectors. */
|
|
66
72
|
export function grantsFor(installation, declaration, snapshot, capability, resource = "any") {
|
|
67
73
|
if (installation.status !== "enabled" ||
|
|
@@ -73,17 +79,14 @@ export function grantsFor(installation, declaration, snapshot, capability, resou
|
|
|
73
79
|
const declared = snapshot.capabilities.find((row) => row.capability === capability)
|
|
74
80
|
?.operations ?? [];
|
|
75
81
|
if (!approved?.capabilityApproved ||
|
|
76
|
-
(approved.resources
|
|
77
|
-
(resource === "any"
|
|
78
|
-
? approved.resources.length === 0
|
|
79
|
-
: !approved.resources.some((ref) => ref.kind === resource.kind && ref.id === resource.id))))
|
|
82
|
+
!grantsResource(approved.resources, resource))
|
|
80
83
|
return null;
|
|
81
84
|
const operations = approved.operations.filter((operation) => declared.includes(operation));
|
|
82
85
|
return { ...approved, operations };
|
|
83
86
|
}
|
|
84
87
|
/** Approved binding operations from an already verified declaration. Hosts still pin
|
|
85
88
|
* the selection/version/artifact and compare running frame grants as a subset. */
|
|
86
|
-
export function approvedBindingGrants(installation, declaration) {
|
|
89
|
+
export function approvedBindingGrants(installation, declaration, resource = "any") {
|
|
87
90
|
if (installation.status !== "enabled")
|
|
88
91
|
return [];
|
|
89
92
|
const bindings = [];
|
|
@@ -96,8 +99,7 @@ export function approvedBindingGrants(installation, declaration) {
|
|
|
96
99
|
const approved = installation.authority.approvedGrants.find((grant) => grant.capability === capability);
|
|
97
100
|
return (declaration.capabilities.includes(capability) &&
|
|
98
101
|
approved?.capabilityApproved &&
|
|
99
|
-
(approved.resources
|
|
100
|
-
approved.resources.length > 0) &&
|
|
102
|
+
grantsResource(approved.resources, resource) &&
|
|
101
103
|
approved.operations.includes(bindingOperationKey(binding.name, name)));
|
|
102
104
|
}));
|
|
103
105
|
});
|
|
@@ -124,10 +126,7 @@ export function scopedBindings(captured, method, ceiling = "write") {
|
|
|
124
126
|
/** Principal ceiling for invocation and metadata capture. Captures must pass this
|
|
125
127
|
* to scopedBindings; its default write ceiling does not encode caller access. */
|
|
126
128
|
export function appInvocationCeiling(facts) {
|
|
127
|
-
return facts.access?.level === "read"
|
|
128
|
-
facts.principal.actor.kind === "visitor"
|
|
129
|
-
? "read"
|
|
130
|
-
: "write";
|
|
129
|
+
return facts.access?.level === "read" ? "read" : "write";
|
|
131
130
|
}
|
|
132
131
|
function admittedBindings(facts, method) {
|
|
133
132
|
return scopedBindings(approvedBindingGrants(facts.installation, facts.declaration), method, appInvocationCeiling(facts));
|
|
@@ -135,9 +134,7 @@ function admittedBindings(facts, method) {
|
|
|
135
134
|
const refusal = (code) => ({ kind: "refused", code });
|
|
136
135
|
function admissionRefusal(facts) {
|
|
137
136
|
const { installation, principal } = facts;
|
|
138
|
-
|
|
139
|
-
if (installation.status !== "enabled" ||
|
|
140
|
-
(visitor && installation.visibility !== "public"))
|
|
137
|
+
if (installation.status !== "enabled")
|
|
141
138
|
return refusal("unavailable");
|
|
142
139
|
if (facts.declaredOperations.digest !==
|
|
143
140
|
installation.authority.declaredOperationsDigest ||
|
|
@@ -153,8 +150,7 @@ function admissionRefusal(facts) {
|
|
|
153
150
|
return null;
|
|
154
151
|
}
|
|
155
152
|
function methodMetadata(facts, name) {
|
|
156
|
-
const { installation
|
|
157
|
-
const visitor = principal.actor.kind === "visitor";
|
|
153
|
+
const { installation } = facts;
|
|
158
154
|
if (installation.instanceId === null)
|
|
159
155
|
return refusal("unavailable");
|
|
160
156
|
if (!Schema.is(appApiMethodNameSchema)(name) ||
|
|
@@ -166,8 +162,6 @@ function methodMetadata(facts, name) {
|
|
|
166
162
|
return refusal("not_declared");
|
|
167
163
|
if (method.effect === "external")
|
|
168
164
|
return refusal("approval_required");
|
|
169
|
-
if (visitor && method.effect !== "read")
|
|
170
|
-
return refusal("visitor_read_only");
|
|
171
165
|
if (method.effect === "write" && facts.access?.level === "read")
|
|
172
166
|
return refusal("level_too_low");
|
|
173
167
|
return { kind: "admitted", method };
|
|
@@ -199,16 +193,13 @@ export function admits(facts, rawRequest) {
|
|
|
199
193
|
if (!parsed.success)
|
|
200
194
|
return refusal("invalid_input");
|
|
201
195
|
const request = parsed.data;
|
|
202
|
-
const { installation
|
|
203
|
-
const visitor = principal.actor.kind === "visitor";
|
|
196
|
+
const { installation } = facts;
|
|
204
197
|
const denied = admissionRefusal(facts);
|
|
205
198
|
if (denied !== null)
|
|
206
199
|
return denied;
|
|
207
200
|
switch (request.kind) {
|
|
208
201
|
case "backend":
|
|
209
202
|
// Opaque App requests have no verified method selector; they require write authority.
|
|
210
|
-
if (visitor)
|
|
211
|
-
return refusal("visitor_read_only");
|
|
212
203
|
if (installation.instanceId === null)
|
|
213
204
|
return refusal("unavailable");
|
|
214
205
|
if (facts.access?.level === "read")
|
|
@@ -242,8 +233,6 @@ export function admits(facts, rawRequest) {
|
|
|
242
233
|
};
|
|
243
234
|
}
|
|
244
235
|
case "capability": {
|
|
245
|
-
if (visitor)
|
|
246
|
-
return refusal("visitor_read_only");
|
|
247
236
|
if (!facts.declaration.capabilities.includes(request.capability) ||
|
|
248
237
|
(request.operation !== undefined &&
|
|
249
238
|
facts.declaredOperations.capabilities
|
|
@@ -258,8 +247,6 @@ export function admits(facts, rawRequest) {
|
|
|
258
247
|
return { kind: "admitted", grants: [] };
|
|
259
248
|
}
|
|
260
249
|
case "capability-route": {
|
|
261
|
-
if (visitor)
|
|
262
|
-
return refusal("visitor_read_only");
|
|
263
250
|
if (request.effect === "write" && facts.access?.level === "read")
|
|
264
251
|
return refusal("level_too_low");
|
|
265
252
|
if (!request.operation.startsWith("route:") ||
|
|
@@ -273,8 +260,6 @@ export function admits(facts, rawRequest) {
|
|
|
273
260
|
return { kind: "admitted", grants: [] };
|
|
274
261
|
}
|
|
275
262
|
case "binding": {
|
|
276
|
-
if (visitor)
|
|
277
|
-
return refusal("visitor_read_only");
|
|
278
263
|
if (installation.instanceId === null)
|
|
279
264
|
return refusal("unavailable");
|
|
280
265
|
const operation = appBackendBindingOperation(request.binding, request.operation);
|
package/dist/apps/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/** Installation records and pure admission rules; tenant state and commits stay host-owned. */
|
|
2
2
|
export { applyGrantCommand, appAuthoritySchema, grantCommandSchema, type AppAuthority, type GrantCommand, type GrantContext, type GrantRefusal, applyInstallationCommand, approvedGrantSchema, declaredOperationsSchema, installationCommandSchema, installationIdSchema, installationRefusalSchema, installationSchema, type Approval, type ApprovedGrant, type CapabilityOperationDescriptor, type DeclaredOperations, grantChangeSchema, type GrantChange, type Installation, type InstallationCommand, type InstallationContext, type InstallationReader, type InstallationRefusal, type InstallationUpdate, type ResourceRef, } from "./installation.js";
|
|
3
|
-
export { permissionPinsSchema, permissionSaveSchema, type PermissionPins, appPermissionsWireSchema, appPermissionPageSchema, permissionScreen, draftOf, editPermissions, planGrantChange, rebaseDraft, type PermissionDraft, type PermissionEdit, type PermissionScreen, type AppPermissionsWire, grantRequestFor, pendingCapabilities, type GrantRequestDecision, type ResourceOption, } from "./permissions.js";
|
|
3
|
+
export { permissionPinsSchema, permissionSaveSchema, type PermissionPins, appPermissionsWireSchema, appPermissionPageSchema, permissionScreen, draftOf, editPermissions, planGrantChange, rebaseDraft, type PermissionDraft, type PermissionEdit, type PermissionScreen, type AppPermissionsWire, type AppPermissionsPort, type AppPermissionsViewData, grantRequestFor, pendingCapabilities, type GrantRequestDecision, type ResourceOption, } from "./permissions.js";
|
|
4
4
|
export { admits, admitMethodMetadata, appInvocationCeiling, approvedBindingGrants, scopedBindings, grantsFor, gateRefusalSchema, gateRequestSchema, invocationGrantsSchema, type AppMethodDeclaration, type AppMethodMetadataDecision, type GateDecision, type GateFacts, type GateRequest, type HostPrincipal, type InvocationGrants, } from "./gate.js";
|
|
5
5
|
export { deriveDeclaredOperations, appCallOperation } from "./operations.js";
|
|
6
6
|
export { appToolPolicySchema, type AppToolAccess, type AppToolEntry, type AppToolPolicy, } from "./tools.js";
|
|
@@ -110,6 +110,8 @@ export type GrantContext = Readonly<{
|
|
|
110
110
|
declaration: AppDeclaration;
|
|
111
111
|
declaredOperations: DeclaredOperations;
|
|
112
112
|
now: number;
|
|
113
|
+
/** Host-attested configured built-in Shell; never a manifest or command field. */
|
|
114
|
+
builtInShell?: boolean;
|
|
113
115
|
}>;
|
|
114
116
|
export type GrantRefusal = Readonly<{
|
|
115
117
|
kind: "refused";
|
|
@@ -121,10 +123,6 @@ export declare function applyGrantCommand(current: AppAuthority | null, rawComma
|
|
|
121
123
|
export declare const declaredOperationsSchema: z.ZodLazy<z.ZodObject<{
|
|
122
124
|
artifactDigest: z.ZodPipe<z.ZodUnknown, z.ZodTransform<import("@fungi.computer/puffball").Sha256Digest, unknown>>;
|
|
123
125
|
digest: z.ZodPipe<z.ZodUnknown, z.ZodTransform<import("@fungi.computer/puffball").Sha256Digest, unknown>>;
|
|
124
|
-
anonymousMethods: z.ZodReadonly<z.ZodArray<z.ZodReadonly<z.ZodObject<{
|
|
125
|
-
name: z.ZodPipe<z.ZodUnknown, z.ZodTransform<string, unknown>>;
|
|
126
|
-
digest: z.ZodPipe<z.ZodUnknown, z.ZodTransform<import("@fungi.computer/puffball").Sha256Digest, unknown>>;
|
|
127
|
-
}, z.core.$strict>>>>;
|
|
128
126
|
capabilities: z.ZodReadonly<z.ZodArray<z.ZodReadonly<z.ZodObject<{
|
|
129
127
|
capability: z.ZodPipe<z.ZodUnknown, z.ZodTransform<"approvals" | "agent.list" | "agent.manage" | "agent.rename" | "agent.avatar" | "agent.toolPolicy" | "agent.read" | "agent.send" | "computer.list" | "computer.readFile" | "computer.writeFile" | "computer.execute" | "llm.generation" | "media.generation" | "games:servers" | "git.repository" | "apps.open" | "apps.call" | "app.storage" | "app.events.emit" | "team.workbenchHost" | "marketplace.browse" | "marketplace.purchase" | "workflow.run" | "workflow.publish" | "workflow.arm" | "world" | "channels.read" | "channels.personal.manage", unknown>>;
|
|
130
128
|
operations: z.ZodLazy<z.ZodReadonly<z.ZodArray<z.ZodString>>>;
|
|
@@ -189,28 +187,6 @@ export declare const installationSchema: z.ZodObject<{
|
|
|
189
187
|
disabled: "disabled";
|
|
190
188
|
deleted: "deleted";
|
|
191
189
|
}>;
|
|
192
|
-
visibility: z.ZodEnum<{
|
|
193
|
-
private: "private";
|
|
194
|
-
public: "public";
|
|
195
|
-
}>;
|
|
196
|
-
approvedAnonymousMethods: z.ZodReadonly<z.ZodArray<z.ZodReadonly<z.ZodObject<{
|
|
197
|
-
name: z.ZodPipe<z.ZodUnknown, z.ZodTransform<string, unknown>>;
|
|
198
|
-
digest: z.ZodPipe<z.ZodUnknown, z.ZodTransform<import("@fungi.computer/puffball").Sha256Digest, unknown>>;
|
|
199
|
-
}, z.core.$strict>>>>;
|
|
200
|
-
anonymousApproval: z.ZodNullable<z.ZodLazy<z.ZodReadonly<z.ZodObject<{
|
|
201
|
-
by: import("zod/mini").ZodMiniDiscriminatedUnion<[import("zod/mini").ZodMiniObject<{
|
|
202
|
-
kind: import("zod/mini").ZodMiniLiteral<"member">;
|
|
203
|
-
memberId: import("zod/mini").ZodMiniString<string>;
|
|
204
|
-
}, z.core.$strip>, import("zod/mini").ZodMiniObject<{
|
|
205
|
-
kind: import("zod/mini").ZodMiniLiteral<"agent">;
|
|
206
|
-
agentId: import("zod/mini").ZodMiniString<string>;
|
|
207
|
-
}, z.core.$strip>, import("zod/mini").ZodMiniObject<{
|
|
208
|
-
kind: import("zod/mini").ZodMiniLiteral<"team-key">;
|
|
209
|
-
keyId: import("zod/mini").ZodMiniString<string>;
|
|
210
|
-
}, z.core.$strip>], "kind">;
|
|
211
|
-
at: z.ZodNumber;
|
|
212
|
-
authorityVersion: z.ZodLazy<z.ZodNumber>;
|
|
213
|
-
}, z.core.$strict>>>>;
|
|
214
190
|
}, z.core.$strict>;
|
|
215
191
|
export type Installation = Readonly<z.output<typeof installationSchema>>;
|
|
216
192
|
/** Host commands; floating targets have already been resolved to immutable Releases. */
|
|
@@ -259,12 +235,6 @@ export declare const installationCommandSchema: z.ZodLazy<z.ZodDiscriminatedUnio
|
|
|
259
235
|
disabled: "disabled";
|
|
260
236
|
deleted: "deleted";
|
|
261
237
|
}>;
|
|
262
|
-
}, z.core.$strict>, z.ZodObject<{
|
|
263
|
-
kind: z.ZodLiteral<"set-visibility">;
|
|
264
|
-
visibility: z.ZodEnum<{
|
|
265
|
-
private: "private";
|
|
266
|
-
public: "public";
|
|
267
|
-
}>;
|
|
268
238
|
}, z.core.$strict>, z.ZodObject<{
|
|
269
239
|
kind: z.ZodLiteral<"change-grants">;
|
|
270
240
|
changes: z.ZodReadonly<z.ZodArray<z.ZodReadonly<z.ZodObject<{
|
|
@@ -277,12 +247,6 @@ export declare const installationCommandSchema: z.ZodLazy<z.ZodDiscriminatedUnio
|
|
|
277
247
|
}, z.core.$strict>>>>>]>;
|
|
278
248
|
}, z.core.$strict>>>;
|
|
279
249
|
}, z.core.$strict>>>>;
|
|
280
|
-
}, z.core.$strict>, z.ZodObject<{
|
|
281
|
-
kind: z.ZodLiteral<"approve-anonymous">;
|
|
282
|
-
methods: z.ZodReadonly<z.ZodArray<z.ZodReadonly<z.ZodObject<{
|
|
283
|
-
name: z.ZodPipe<z.ZodUnknown, z.ZodTransform<string, unknown>>;
|
|
284
|
-
digest: z.ZodPipe<z.ZodUnknown, z.ZodTransform<import("@fungi.computer/puffball").Sha256Digest, unknown>>;
|
|
285
|
-
}, z.core.$strict>>>>;
|
|
286
250
|
}, z.core.$strict>], "kind">>;
|
|
287
251
|
export type InstallationCommand = z.output<typeof installationCommandSchema>;
|
|
288
252
|
/** Values captured by the host; identities and time are never generated by the reducer. */
|
|
@@ -294,10 +258,8 @@ export type InstallationContext = Readonly<{
|
|
|
294
258
|
declaredOperations: DeclaredOperations;
|
|
295
259
|
now: number;
|
|
296
260
|
identity: Pick<Installation, "installationId" | "instanceId">;
|
|
297
|
-
/**
|
|
298
|
-
|
|
299
|
-
memberId: string;
|
|
300
|
-
}>;
|
|
261
|
+
/** Operator-selected built-in Shell, attested by the executing host, not the App. */
|
|
262
|
+
builtInShell?: boolean;
|
|
301
263
|
}>;
|
|
302
264
|
/** Closed refusals: failed commands produce no installation update. */
|
|
303
265
|
export declare const installationRefusalSchema: z.ZodLazy<z.ZodObject<{
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as z from "zod";
|
|
2
2
|
import * as Schema from "effect/Schema";
|
|
3
3
|
import * as Result from "effect/Result";
|
|
4
|
-
import { appCapabilityIdSchema,
|
|
4
|
+
import { appCapabilityIdSchema, } from "@fungi.computer/puffball/app";
|
|
5
5
|
import { hostActorSchema } from "@fungi.computer/puffball/app-method-runtime";
|
|
6
6
|
import { appIdSchema, releaseSchema, sha256DigestSchema, } from "@fungi.computer/puffball/effect";
|
|
7
7
|
import { instanceIdSchema } from "../instance-id.js";
|
|
@@ -119,7 +119,6 @@ export function applyGrantCommand(current, rawCommand, context) {
|
|
|
119
119
|
current.declaredOperationsDigest !== context.declaredOperations.digest))
|
|
120
120
|
return refuse("stale_authority");
|
|
121
121
|
if (context.principal.role === null ||
|
|
122
|
-
context.principal.actor.kind === "visitor" ||
|
|
123
122
|
(command.kind === "change-grants" && context.principal.role !== "owner"))
|
|
124
123
|
return refuse("not_owner");
|
|
125
124
|
const declared = (capability) => context.declaredOperations.capabilities.find((row) => row.capability === capability)?.operations ?? [];
|
|
@@ -131,7 +130,9 @@ export function applyGrantCommand(current, rawCommand, context) {
|
|
|
131
130
|
grants =
|
|
132
131
|
context.principal.role === "owner"
|
|
133
132
|
? context.declaration.capabilities
|
|
134
|
-
.filter((capability) => permissionCatalog[capability].consent === "install"
|
|
133
|
+
.filter((capability) => permissionCatalog[capability].consent === "install" ||
|
|
134
|
+
(capability === "team.workbenchHost" &&
|
|
135
|
+
context.builtInShell === true))
|
|
135
136
|
.map((capability) => ({
|
|
136
137
|
capability,
|
|
137
138
|
capabilityApproved: true,
|
|
@@ -190,23 +191,10 @@ export function applyGrantCommand(current, rawCommand, context) {
|
|
|
190
191
|
approval,
|
|
191
192
|
});
|
|
192
193
|
}
|
|
193
|
-
const anonymousMethodsSchema = z
|
|
194
|
-
.array(z
|
|
195
|
-
.strictObject({
|
|
196
|
-
name: ownerCodec(appApiMethodNameSchema),
|
|
197
|
-
digest: ownerCodec(sha256DigestSchema),
|
|
198
|
-
})
|
|
199
|
-
.readonly())
|
|
200
|
-
.refine((methods) => methods.every((method, index) => {
|
|
201
|
-
const previous = methods[index - 1];
|
|
202
|
-
return previous === undefined || previous.name < method.name;
|
|
203
|
-
}), "Anonymous method identities must have unique, sorted names")
|
|
204
|
-
.readonly();
|
|
205
194
|
/** Immutable operation facts derived from descriptor methods and the binding registry. */
|
|
206
195
|
export const declaredOperationsSchema = z.lazy(() => z.strictObject({
|
|
207
196
|
artifactDigest: ownerCodec(sha256DigestSchema),
|
|
208
197
|
digest: ownerCodec(sha256DigestSchema),
|
|
209
|
-
anonymousMethods: anonymousMethodsSchema,
|
|
210
198
|
capabilities: z
|
|
211
199
|
.array(z
|
|
212
200
|
.strictObject({
|
|
@@ -226,20 +214,8 @@ export const installationSchema = z
|
|
|
226
214
|
releasePin: z.enum(["pinned", "floating"]),
|
|
227
215
|
authority: appAuthoritySchema,
|
|
228
216
|
status: z.enum(["enabled", "disabled", "deleted"]),
|
|
229
|
-
visibility: z.enum(["private", "public"]),
|
|
230
|
-
approvedAnonymousMethods: anonymousMethodsSchema,
|
|
231
|
-
anonymousApproval: approvalSchema.nullable(),
|
|
232
217
|
})
|
|
233
|
-
.refine((installation) => installation.appId === installation.release.appId, "Release must belong to the installation's App")
|
|
234
|
-
.refine((installation) => {
|
|
235
|
-
const approval = installation.anonymousApproval;
|
|
236
|
-
return approval === null
|
|
237
|
-
? installation.approvedAnonymousMethods.length === 0
|
|
238
|
-
: installation.status !== "deleted" &&
|
|
239
|
-
installation.approvedAnonymousMethods.length > 0 &&
|
|
240
|
-
approval.by.kind === "member" &&
|
|
241
|
-
approval.authorityVersion <= installation.authority.authorityVersion;
|
|
242
|
-
}, "Anonymous authority requires a retained human approval receipt");
|
|
218
|
+
.refine((installation) => installation.appId === installation.release.appId, "Release must belong to the installation's App");
|
|
243
219
|
const releaseFields = {
|
|
244
220
|
release: ownerCodec(releaseSchema),
|
|
245
221
|
releasePin: installationSchema.shape.releasePin,
|
|
@@ -255,18 +231,10 @@ export const installationCommandSchema = z.lazy(() => z.discriminatedUnion("kind
|
|
|
255
231
|
kind: z.literal("set-status"),
|
|
256
232
|
status: installationSchema.shape.status,
|
|
257
233
|
}),
|
|
258
|
-
z.strictObject({
|
|
259
|
-
kind: z.literal("set-visibility"),
|
|
260
|
-
visibility: installationSchema.shape.visibility,
|
|
261
|
-
}),
|
|
262
234
|
z.strictObject({
|
|
263
235
|
kind: z.literal("change-grants"),
|
|
264
236
|
changes: grantChangesSchema,
|
|
265
237
|
}),
|
|
266
|
-
z.strictObject({
|
|
267
|
-
kind: z.literal("approve-anonymous"),
|
|
268
|
-
methods: anonymousMethodsSchema,
|
|
269
|
-
}),
|
|
270
238
|
]));
|
|
271
239
|
/** Closed refusals: failed commands produce no installation update. */
|
|
272
240
|
export const installationRefusalSchema = z.lazy(() => z.strictObject({
|
|
@@ -301,13 +269,6 @@ export function applyInstallationCommand(current, rawCommand, context) {
|
|
|
301
269
|
}
|
|
302
270
|
if (command.kind === "change-grants" && context.role !== "owner")
|
|
303
271
|
return refuse("not_owner");
|
|
304
|
-
const anonymousConsent = context.by.kind === "member" &&
|
|
305
|
-
context.anonymousConsent?.memberId === context.by.memberId;
|
|
306
|
-
if (command.kind === "approve-anonymous") {
|
|
307
|
-
const narrows = command.methods.every((method) => current?.approvedAnonymousMethods.some((approved) => approved.name === method.name && approved.digest === method.digest));
|
|
308
|
-
if (context.role !== "owner" || (!narrows && !anonymousConsent))
|
|
309
|
-
return refuse("not_owner");
|
|
310
|
-
}
|
|
311
272
|
const release = command.kind === "install" || command.kind === "select-release"
|
|
312
273
|
? command.release
|
|
313
274
|
: current?.release;
|
|
@@ -327,7 +288,7 @@ export function applyInstallationCommand(current, rawCommand, context) {
|
|
|
327
288
|
? { kind: command.kind }
|
|
328
289
|
: command.kind === "change-grants"
|
|
329
290
|
? command
|
|
330
|
-
: command.
|
|
291
|
+
: command.status === "deleted"
|
|
331
292
|
? { kind: "delete" }
|
|
332
293
|
: null;
|
|
333
294
|
const authority = grantCommand === null
|
|
@@ -348,34 +309,10 @@ export function applyInstallationCommand(current, rawCommand, context) {
|
|
|
348
309
|
declaration: context.declaration,
|
|
349
310
|
declaredOperations: context.declaredOperations,
|
|
350
311
|
now: context.now,
|
|
312
|
+
builtInShell: context.builtInShell === true,
|
|
351
313
|
});
|
|
352
314
|
if (Result.isFailure(authority))
|
|
353
315
|
return refuse(authority.failure.code);
|
|
354
|
-
let anonymousMethods = current?.approvedAnonymousMethods ?? [];
|
|
355
|
-
let anonymousApproval = current?.anonymousApproval ?? null;
|
|
356
|
-
if (command.kind === "approve-anonymous") {
|
|
357
|
-
if (command.methods.some((method) => !context.declaredOperations.anonymousMethods.some((declared) => declared.name === method.name &&
|
|
358
|
-
declared.digest === method.digest)))
|
|
359
|
-
return refuse("not_declared");
|
|
360
|
-
anonymousMethods = command.methods;
|
|
361
|
-
if (command.methods.length === 0)
|
|
362
|
-
anonymousApproval = null;
|
|
363
|
-
else if (anonymousConsent)
|
|
364
|
-
anonymousApproval = {
|
|
365
|
-
by: context.by,
|
|
366
|
-
at: context.now,
|
|
367
|
-
authorityVersion: version,
|
|
368
|
-
};
|
|
369
|
-
}
|
|
370
|
-
if (command.kind === "select-release") {
|
|
371
|
-
anonymousMethods = anonymousMethods.filter((method) => context.declaredOperations.anonymousMethods.some((declared) => declared.name === method.name && declared.digest === method.digest));
|
|
372
|
-
if (anonymousMethods.length === 0)
|
|
373
|
-
anonymousApproval = null;
|
|
374
|
-
}
|
|
375
|
-
if (command.kind === "set-status" && command.status === "deleted") {
|
|
376
|
-
anonymousMethods = [];
|
|
377
|
-
anonymousApproval = null;
|
|
378
|
-
}
|
|
379
316
|
const next = installationSchema.safeParse({
|
|
380
317
|
installationId: current?.installationId ?? context.identity.installationId,
|
|
381
318
|
instanceId: current === null ? context.identity.instanceId : current.instanceId,
|
|
@@ -387,12 +324,7 @@ export function applyInstallationCommand(current, rawCommand, context) {
|
|
|
387
324
|
status: command.kind === "set-status"
|
|
388
325
|
? command.status
|
|
389
326
|
: (current?.status ?? "enabled"),
|
|
390
|
-
visibility: command.kind === "set-visibility"
|
|
391
|
-
? command.visibility
|
|
392
|
-
: (current?.visibility ?? "private"),
|
|
393
327
|
authority: authority.success,
|
|
394
|
-
approvedAnonymousMethods: anonymousMethods,
|
|
395
|
-
anonymousApproval,
|
|
396
328
|
});
|
|
397
329
|
return next.success
|
|
398
330
|
? Result.succeed({ next: next.data, authorityChanged: true })
|
package/dist/apps/operations.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { appBackendBindingOperation, } from "@fungi.computer/puffball/app";
|
|
2
2
|
import { sha256 } from "../release.js";
|
|
3
|
-
import canonicalJson from "fast-json-stable-stringify";
|
|
4
3
|
import { declaredOperationsSchema, } from "./installation.js";
|
|
5
4
|
/** Qualify backend operations so a same-named browser method grants no backend authority. */
|
|
6
5
|
export function bindingOperationKey(binding, operation) {
|
|
@@ -75,24 +74,15 @@ export async function deriveDeclaredOperations(release, declaration, descriptors
|
|
|
75
74
|
operations: [...operations].sort(),
|
|
76
75
|
}));
|
|
77
76
|
const artifactDigest = release.artifact.artifactDigest;
|
|
78
|
-
const anonymousMethods = await Promise.all(Object.entries(declaration.api?.methods ?? {})
|
|
79
|
-
.filter(([, method]) => method.anonymous === true)
|
|
80
|
-
.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
|
|
81
|
-
.map(async ([name, method]) => ({
|
|
82
|
-
name,
|
|
83
|
-
digest: await sha256(new TextEncoder().encode(canonicalJson(method))),
|
|
84
|
-
})));
|
|
85
77
|
const digest = await sha256(new TextEncoder().encode(JSON.stringify({
|
|
86
78
|
artifactDigest,
|
|
87
79
|
capabilities: rows,
|
|
88
80
|
routes,
|
|
89
81
|
bindings,
|
|
90
|
-
anonymousMethods,
|
|
91
82
|
})));
|
|
92
83
|
return declaredOperationsSchema.parse({
|
|
93
84
|
artifactDigest,
|
|
94
85
|
digest,
|
|
95
86
|
capabilities: rows,
|
|
96
|
-
anonymousMethods,
|
|
97
87
|
});
|
|
98
88
|
}
|
|
@@ -103,10 +103,6 @@ export declare const appPermissionsWireSchema: z.ZodObject<{
|
|
|
103
103
|
declaredOperations: z.ZodLazy<z.ZodObject<{
|
|
104
104
|
artifactDigest: z.ZodPipe<z.ZodUnknown, z.ZodTransform<import("@fungi.computer/puffball").Sha256Digest, unknown>>;
|
|
105
105
|
digest: z.ZodPipe<z.ZodUnknown, z.ZodTransform<import("@fungi.computer/puffball").Sha256Digest, unknown>>;
|
|
106
|
-
anonymousMethods: z.ZodReadonly<z.ZodArray<z.ZodReadonly<z.ZodObject<{
|
|
107
|
-
name: z.ZodPipe<z.ZodUnknown, z.ZodTransform<string, unknown>>;
|
|
108
|
-
digest: z.ZodPipe<z.ZodUnknown, z.ZodTransform<import("@fungi.computer/puffball").Sha256Digest, unknown>>;
|
|
109
|
-
}, z.core.$strict>>>>;
|
|
110
106
|
capabilities: z.ZodReadonly<z.ZodArray<z.ZodReadonly<z.ZodObject<{
|
|
111
107
|
capability: z.ZodPipe<z.ZodUnknown, z.ZodTransform<"approvals" | "agent.list" | "agent.manage" | "agent.rename" | "agent.avatar" | "agent.toolPolicy" | "agent.read" | "agent.send" | "computer.list" | "computer.readFile" | "computer.writeFile" | "computer.execute" | "llm.generation" | "media.generation" | "games:servers" | "git.repository" | "apps.open" | "apps.call" | "app.storage" | "app.events.emit" | "team.workbenchHost" | "marketplace.browse" | "marketplace.purchase" | "workflow.run" | "workflow.publish" | "workflow.arm" | "world" | "channels.read" | "channels.personal.manage", unknown>>;
|
|
112
108
|
operations: z.ZodLazy<z.ZodReadonly<z.ZodArray<z.ZodString>>>;
|
|
@@ -234,14 +230,28 @@ export declare const appPermissionPageSchema: z.ZodObject<{
|
|
|
234
230
|
declaredOperations: z.ZodLazy<z.ZodObject<{
|
|
235
231
|
artifactDigest: z.ZodPipe<z.ZodUnknown, z.ZodTransform<import("@fungi.computer/puffball").Sha256Digest, unknown>>;
|
|
236
232
|
digest: z.ZodPipe<z.ZodUnknown, z.ZodTransform<import("@fungi.computer/puffball").Sha256Digest, unknown>>;
|
|
237
|
-
anonymousMethods: z.ZodReadonly<z.ZodArray<z.ZodReadonly<z.ZodObject<{
|
|
238
|
-
name: z.ZodPipe<z.ZodUnknown, z.ZodTransform<string, unknown>>;
|
|
239
|
-
digest: z.ZodPipe<z.ZodUnknown, z.ZodTransform<import("@fungi.computer/puffball").Sha256Digest, unknown>>;
|
|
240
|
-
}, z.core.$strict>>>>;
|
|
241
233
|
capabilities: z.ZodReadonly<z.ZodArray<z.ZodReadonly<z.ZodObject<{
|
|
242
234
|
capability: z.ZodPipe<z.ZodUnknown, z.ZodTransform<"approvals" | "agent.list" | "agent.manage" | "agent.rename" | "agent.avatar" | "agent.toolPolicy" | "agent.read" | "agent.send" | "computer.list" | "computer.readFile" | "computer.writeFile" | "computer.execute" | "llm.generation" | "media.generation" | "games:servers" | "git.repository" | "apps.open" | "apps.call" | "app.storage" | "app.events.emit" | "team.workbenchHost" | "marketplace.browse" | "marketplace.purchase" | "workflow.run" | "workflow.publish" | "workflow.arm" | "world" | "channels.read" | "channels.personal.manage", unknown>>;
|
|
243
235
|
operations: z.ZodLazy<z.ZodReadonly<z.ZodArray<z.ZodString>>>;
|
|
244
236
|
}, z.core.$strict>>>>;
|
|
245
237
|
}, z.core.$strict>>;
|
|
246
238
|
}, z.core.$strict>;
|
|
239
|
+
canEdit: z.ZodBoolean;
|
|
240
|
+
resources: z.ZodArray<z.ZodObject<{
|
|
241
|
+
kind: z.ZodString;
|
|
242
|
+
id: z.ZodString;
|
|
243
|
+
label: z.ZodString;
|
|
244
|
+
}, z.core.$strict>>;
|
|
247
245
|
}, z.core.$strict>;
|
|
246
|
+
/** A host's live authority and resource directory, never browser-minted grants. */
|
|
247
|
+
export type AppPermissionsViewData = z.output<typeof appPermissionPageSchema>;
|
|
248
|
+
/**
|
|
249
|
+
* Hosts authenticate, fence and commit; the shared view owns the permission draft.
|
|
250
|
+
* @interface
|
|
251
|
+
*/
|
|
252
|
+
export type AppPermissionsPort = Readonly<{
|
|
253
|
+
/** Read the installation's current permissions, whether the caller can edit them, and the available resources. */
|
|
254
|
+
read(appKey: string): Promise<AppPermissionsViewData | "unavailable">;
|
|
255
|
+
/** Submit edited grants with the store generation, authority version and artifact digest. The host checks authorization and freshness before saving. */
|
|
256
|
+
submit(appKey: string, pins: PermissionPins, changes: readonly GrantChange[]): Promise<"accepted" | "stale" | "refused" | "unavailable" | "unconfirmed">;
|
|
257
|
+
}>;
|
package/dist/apps/permissions.js
CHANGED
|
@@ -214,7 +214,7 @@ export function permissionScreen(input) {
|
|
|
214
214
|
? []
|
|
215
215
|
: [
|
|
216
216
|
row("all", scope === "computer" ? "All Computers" : "All Agents"),
|
|
217
|
-
...[...resources.values()].map((resource) => row(resource, resource.label)),
|
|
217
|
+
...[...resources.values()].map((resource) => row({ kind: resource.kind, id: resource.id }, resource.label)),
|
|
218
218
|
],
|
|
219
219
|
switches: capabilities
|
|
220
220
|
.filter((capability) => permissionCatalog[capability].scope === null)
|
|
@@ -252,4 +252,10 @@ export const appPermissionPageSchema = z.strictObject({
|
|
|
252
252
|
appName: z.string(),
|
|
253
253
|
artifactDigest: ownerCodec(sha256DigestSchema),
|
|
254
254
|
permissions: appPermissionsWireSchema,
|
|
255
|
+
canEdit: z.boolean(),
|
|
256
|
+
resources: z.array(z.strictObject({
|
|
257
|
+
kind: z.string(),
|
|
258
|
+
id: z.string(),
|
|
259
|
+
label: z.string(),
|
|
260
|
+
})),
|
|
255
261
|
});
|
package/dist/auth.d.ts
CHANGED
|
@@ -1,24 +1,7 @@
|
|
|
1
|
-
import type { OysterAuth
|
|
1
|
+
import type { OysterAuth } from "./host.js";
|
|
2
2
|
import type { OysterKeyStore } from "./keys.js";
|
|
3
3
|
/** Cookie emitted by ownerKeyAuth, for composing credential-specific adapters. */
|
|
4
4
|
export declare const ownerKeyCookieName = "oyster_session";
|
|
5
|
-
/** Host-minted opaque sessions for named keys, separate from the owner cookie. */
|
|
6
|
-
export declare const keySessionCookieName = "oyster_key_session";
|
|
7
|
-
/**
|
|
8
|
-
* The host owns session minting, hashing/storage and expiry.
|
|
9
|
-
* A session resolves only to a key id, never to owner or grant authority.
|
|
10
|
-
* Hosts should use Path=/, HttpOnly, Secure and SameSite=Lax (or stricter)
|
|
11
|
-
* as defense in depth; Oyster independently checks request provenance.
|
|
12
|
-
* Cookie-only admission allows GET/HEAD navigation. Every other method requires
|
|
13
|
-
* Origin to be present and equal the request URL's origin, and Sec-Fetch-Site
|
|
14
|
-
* not to be cross-site. Missing Origin is refused.
|
|
15
|
-
*/
|
|
16
|
-
export type OysterKeySessionResolver = Readonly<{
|
|
17
|
-
/** Resolve an opaque host session to its named key, or null when unavailable. */
|
|
18
|
-
resolve: (value: string) => Promise<Readonly<{
|
|
19
|
-
keyId: string;
|
|
20
|
-
}> | null>;
|
|
21
|
-
}>;
|
|
22
5
|
/**
|
|
23
6
|
* Single-owner sign-in: a bearer token or a session cookie holding the
|
|
24
7
|
* owner key's digest. The key itself is never stored in the browser.
|
|
@@ -26,9 +9,3 @@ export type OysterKeySessionResolver = Readonly<{
|
|
|
26
9
|
export declare function ownerKeyAuth(ownerKey: string, ownerId?: string): OysterAuth;
|
|
27
10
|
/** The owner key, or a named key admitted as its canonical Team-key actor. */
|
|
28
11
|
export declare function ownerOrKeyAuth(ownerKey: string, ownerId: string, keys: () => Pick<OysterKeyStore, "verify">): OysterAuth;
|
|
29
|
-
/**
|
|
30
|
-
* Add host-owned browser sessions to ownerOrKeyAuth. Explicit credentials and
|
|
31
|
-
* non-401 refusals keep their ordinary result; ambient cookies cannot undo them.
|
|
32
|
-
* Every cookie admission rechecks the key's liveness and canonical owner.
|
|
33
|
-
*/
|
|
34
|
-
export declare function keySessionAuth(auth: OysterAuth, ownerId: string, keys: () => NonNullable<OysterHost["keys"]>, sessions: OysterKeySessionResolver): OysterAuth;
|
package/dist/auth.js
CHANGED
|
@@ -1,13 +1,9 @@
|
|
|
1
1
|
import * as Option from "effect/Option";
|
|
2
2
|
import * as Schema from "effect/Schema";
|
|
3
|
-
import { parse } from "hono/utils/cookie";
|
|
4
3
|
import { presentedGitKey } from "./git-auth.js";
|
|
5
4
|
import { sha256 } from "./release.js";
|
|
6
|
-
import { sameOriginRequest } from "./request-origin.js";
|
|
7
5
|
/** Cookie emitted by ownerKeyAuth, for composing credential-specific adapters. */
|
|
8
6
|
export const ownerKeyCookieName = "oyster_session";
|
|
9
|
-
/** Host-minted opaque sessions for named keys, separate from the owner cookie. */
|
|
10
|
-
export const keySessionCookieName = "oyster_key_session";
|
|
11
7
|
const loginFormSchema = Schema.Struct({ key: Schema.String });
|
|
12
8
|
/** Compare two secrets through their fixed-length digests, in constant time. */
|
|
13
9
|
async function constantTimeEqual(a, b) {
|
|
@@ -52,7 +48,15 @@ export function ownerKeyAuth(ownerKey, ownerId = "owner") {
|
|
|
52
48
|
return { kind: "refused", status: 401 };
|
|
53
49
|
},
|
|
54
50
|
async login(request) {
|
|
55
|
-
|
|
51
|
+
// A body that is not a form is a malformed sign-in, not a server fault.
|
|
52
|
+
const fields = await request.formData().catch((cause) => {
|
|
53
|
+
if (cause instanceof TypeError)
|
|
54
|
+
return null;
|
|
55
|
+
throw cause;
|
|
56
|
+
});
|
|
57
|
+
if (fields === null)
|
|
58
|
+
return new Response("Expected a sign-in form", { status: 400 });
|
|
59
|
+
const form = Schema.decodeUnknownOption(loginFormSchema)(Object.fromEntries(fields));
|
|
56
60
|
if (Option.isNone(form) ||
|
|
57
61
|
!(await constantTimeEqual(form.value.key, ownerKey)))
|
|
58
62
|
return new Response("Wrong key", { status: 401 });
|
|
@@ -95,37 +99,3 @@ export function ownerOrKeyAuth(ownerKey, ownerId, keys) {
|
|
|
95
99
|
Promise.resolve(new Response("Sign-in is not available", { status: 404 })),
|
|
96
100
|
};
|
|
97
101
|
}
|
|
98
|
-
/**
|
|
99
|
-
* Add host-owned browser sessions to ownerOrKeyAuth. Explicit credentials and
|
|
100
|
-
* non-401 refusals keep their ordinary result; ambient cookies cannot undo them.
|
|
101
|
-
* Every cookie admission rechecks the key's liveness and canonical owner.
|
|
102
|
-
*/
|
|
103
|
-
export function keySessionAuth(auth, ownerId, keys, sessions) {
|
|
104
|
-
return {
|
|
105
|
-
...auth,
|
|
106
|
-
async authenticate(request) {
|
|
107
|
-
const result = await auth.authenticate(request);
|
|
108
|
-
if (result.kind === "admitted" ||
|
|
109
|
-
result.status !== 401 ||
|
|
110
|
-
request.headers.has("Authorization"))
|
|
111
|
-
return result;
|
|
112
|
-
if (request.method !== "GET" &&
|
|
113
|
-
request.method !== "HEAD" &&
|
|
114
|
-
!sameOriginRequest(request))
|
|
115
|
-
return result;
|
|
116
|
-
const value = parse(request.headers.get("Cookie") ?? "", keySessionCookieName)[keySessionCookieName];
|
|
117
|
-
if (value === undefined || value.length === 0)
|
|
118
|
-
return result;
|
|
119
|
-
const session = await sessions.resolve(value);
|
|
120
|
-
if (session === null)
|
|
121
|
-
return result;
|
|
122
|
-
const key = await keys().live(session.keyId);
|
|
123
|
-
return key !== null && key.owner === ownerId
|
|
124
|
-
? {
|
|
125
|
-
kind: "admitted",
|
|
126
|
-
principal: { actor: { kind: "team-key", keyId: key.keyId } },
|
|
127
|
-
}
|
|
128
|
-
: result;
|
|
129
|
-
},
|
|
130
|
-
};
|
|
131
|
-
}
|