@gjermundgaraba/effect-actions 0.20.0 → 0.21.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 +19 -41
- package/dist/Action.d.mts +140 -46
- package/dist/Action.mjs +87 -42
- package/dist/ActionCli.d.mts +69 -33
- package/dist/ActionCli.mjs +51 -32
- package/dist/ActionHttp.d.mts +69 -35
- package/dist/ActionHttp.mjs +75 -23
- package/dist/ActionMcp.d.mts +56 -22
- package/dist/ActionMcp.mjs +94 -69
- package/dist/ActionRpc.d.mts +222 -55
- package/dist/ActionRpc.mjs +299 -46
- package/dist/ActionToolkit.d.mts +22 -5
- package/dist/ActionToolkit.mjs +36 -12
- package/dist/Authentication.d.mts +42 -10
- package/dist/Authentication.mjs +37 -30
- package/dist/Event.d.mts +370 -0
- package/dist/Event.mjs +103 -0
- package/dist/Testing.mjs +1 -1
- package/dist/Webhook.d.mts +146 -0
- package/dist/Webhook.mjs +72 -0
- package/dist/authentication/provider.d.mts +35 -10
- package/dist/authentication/provider.mjs +1 -1
- package/dist/authentication/refusal.mjs +2 -2
- package/dist/cli/commands.d.mts +9 -5
- package/dist/cli/commands.mjs +84 -33
- package/dist/contract/ast.mjs +4 -1
- package/dist/contract/authorizer.d.mts +8 -0
- package/dist/contract/authorizer.mjs +20 -0
- package/dist/contract/call.d.mts +9 -6
- package/dist/contract/call.mjs +48 -6
- package/dist/contract/callback.d.mts +7 -0
- package/dist/contract/callback.mjs +27 -0
- package/dist/contract/caller.d.mts +13 -2
- package/dist/contract/caller.mjs +22 -3
- package/dist/contract/canonical.mjs +10 -0
- package/dist/contract/codec.d.mts +9 -0
- package/dist/contract/codec.mjs +9 -0
- package/dist/contract/codes.mjs +81 -0
- package/dist/contract/declared.mjs +25 -9
- package/dist/contract/digest.mjs +6 -0
- package/dist/contract/errors.d.mts +9 -3
- package/dist/contract/errors.mjs +1 -1
- package/dist/contract/feed.d.mts +127 -0
- package/dist/contract/feed.mjs +249 -0
- package/dist/contract/hub.d.mts +94 -0
- package/dist/contract/hub.mjs +133 -0
- package/dist/contract/idempotency.d.mts +47 -0
- package/dist/contract/idempotency.mjs +99 -0
- package/dist/contract/implementation.d.mts +73 -47
- package/dist/contract/implementation.mjs +92 -58
- package/dist/contract/input-check.d.mts +15 -0
- package/dist/contract/input-check.mjs +16 -0
- package/dist/contract/lease.mjs +28 -0
- package/dist/contract/listed.d.mts +4 -0
- package/dist/contract/listed.mjs +22 -0
- package/dist/contract/log.d.mts +41 -0
- package/dist/contract/log.mjs +144 -0
- package/dist/contract/names.mjs +14 -4
- package/dist/contract/occurrence.d.mts +29 -0
- package/dist/contract/occurrence.mjs +45 -0
- package/dist/contract/pages.d.mts +13 -0
- package/dist/contract/pages.mjs +39 -0
- package/dist/contract/progress.d.mts +26 -0
- package/dist/contract/progress.mjs +72 -0
- package/dist/contract/push.d.mts +20 -0
- package/dist/contract/push.mjs +96 -0
- package/dist/contract/receiving.d.mts +29 -0
- package/dist/contract/receiving.mjs +42 -0
- package/dist/contract/recent.mjs +12 -0
- package/dist/contract/reported.mjs +16 -0
- package/dist/contract/serving.mjs +63 -0
- package/dist/contract/status.mjs +14 -5
- package/dist/contract/stream.d.mts +34 -0
- package/dist/contract/stream.mjs +31 -0
- package/dist/contract/subscribe.d.mts +45 -0
- package/dist/contract/subscribe.mjs +50 -0
- package/dist/contract/transient.mjs +28 -0
- package/dist/contract/watch.d.mts +18 -0
- package/dist/contract/watch.mjs +105 -0
- package/dist/contract/webhooks.d.mts +42 -0
- package/dist/contract/webhooks.mjs +53 -0
- package/dist/http/client.d.mts +37 -12
- package/dist/http/client.mjs +51 -12
- package/dist/http/events.d.mts +56 -0
- package/dist/http/events.mjs +102 -0
- package/dist/http/placed.d.mts +36 -0
- package/dist/http/placed.mjs +71 -0
- package/dist/http/schema-errors.mjs +1 -1
- package/dist/http/streamed.d.mts +21 -0
- package/dist/http/streamed.mjs +94 -0
- package/dist/http/watch.mjs +50 -0
- package/dist/mcp/bridge.mjs +283 -0
- package/dist/mcp/client.d.mts +30 -97
- package/dist/mcp/client.mjs +59 -132
- package/dist/mcp/events.mjs +142 -0
- package/dist/mcp/progress.mjs +34 -0
- package/dist/mcp/protocol.mjs +39 -13
- package/dist/mcp/reply.d.mts +79 -0
- package/dist/mcp/reply.mjs +99 -0
- package/dist/mcp/request.d.mts +40 -0
- package/dist/mcp/request.mjs +51 -0
- package/dist/mcp/watch.mjs +115 -0
- package/dist/rpc/watch.mjs +39 -0
- package/dist/stdio/console.mjs +13 -5
- package/dist/testing/memory.d.mts +1 -1
- package/dist/testing/memory.mjs +3 -3
- package/dist/webhook/address.mjs +47 -0
- package/dist/webhook/attempt.mjs +65 -0
- package/dist/webhook/receiver.mjs +67 -0
- package/dist/webhook/signing.mjs +28 -0
- package/dist/webhook/stored.mjs +57 -0
- package/dist/webhook/subscriptions.mjs +342 -0
- package/docs/Action-failures.md +2 -2
- package/docs/Action-streams.md +115 -0
- package/docs/Action.md +58 -124
- package/docs/ActionCli-failures.md +1 -2
- package/docs/ActionCli.md +83 -88
- package/docs/ActionHttp-failures.md +3 -3
- package/docs/ActionHttp.md +90 -70
- package/docs/ActionMcp-failures.md +2 -1
- package/docs/ActionMcp.md +120 -92
- package/docs/ActionRpc-events.md +126 -0
- package/docs/ActionRpc-failures.md +1 -2
- package/docs/ActionRpc.md +108 -79
- package/docs/ActionToolkit-failures.md +1 -2
- package/docs/ActionToolkit.md +33 -90
- package/docs/Authentication-advanced.md +52 -60
- package/docs/Authentication-failures.md +5 -4
- package/docs/Authentication.md +23 -58
- package/docs/Event-failures.md +15 -0
- package/docs/Event.md +281 -0
- package/docs/README.md +51 -82
- package/docs/Testing-failures.md +1 -5
- package/docs/Testing.md +126 -86
- package/docs/Webhook-failures.md +8 -0
- package/docs/Webhook.md +319 -0
- package/docs/setup-failures.md +2 -3
- package/docs/setup.md +32 -52
- package/examples/README.md +12 -37
- package/examples/assistant/README.md +21 -26
- package/examples/assistant/src/agent.ts +66 -24
- package/examples/assistant/src/cli.ts +7 -4
- package/examples/assistant/src/commands.ts +9 -4
- package/examples/assistant/src/identity/Identity.ts +5 -4
- package/examples/assistant/src/identity/authorize.ts +13 -5
- package/examples/assistant/src/identity/handlers.ts +3 -2
- package/examples/assistant/src/identity/permissions.ts +9 -3
- package/examples/assistant/src/platform/tasks-file.ts +15 -11
- package/examples/assistant/src/scripted.ts +7 -4
- package/examples/assistant/src/tasks/Tasks.ts +20 -6
- package/examples/assistant/src/tasks/handlers.ts +9 -5
- package/examples/assistant/src/tasks/store.ts +38 -11
- package/examples/assistant/src/toolkit.ts +11 -7
- package/examples/assistant/test/commands.test.ts +53 -27
- package/examples/assistant/test/identity/identity.test.ts +27 -11
- package/examples/assistant/test/platform/tasks-file.test.ts +39 -23
- package/examples/assistant/test/tasks/tasks.test.ts +62 -34
- package/examples/assistant/test/toolkit.test.ts +104 -75
- package/examples/build-events/README.md +83 -0
- package/examples/build-events/src/Api.ts +27 -0
- package/examples/build-events/src/Rpc.ts +26 -0
- package/examples/build-events/src/builds/Builds.ts +108 -0
- package/examples/build-events/src/builds/events.ts +54 -0
- package/examples/build-events/src/builds/handlers.ts +58 -0
- package/examples/build-events/src/builds/log.ts +30 -0
- package/examples/build-events/src/builds/status.ts +28 -0
- package/examples/build-events/src/builds/store.ts +130 -0
- package/examples/build-events/src/cli.ts +18 -0
- package/examples/build-events/src/commands.ts +32 -0
- package/examples/build-events/src/deploys/Deploys.ts +44 -0
- package/examples/build-events/src/deploys/approvers.ts +55 -0
- package/examples/build-events/src/deploys/events.ts +32 -0
- package/examples/build-events/src/deploys/handlers.ts +38 -0
- package/examples/build-events/src/identity/Identity.ts +29 -0
- package/examples/build-events/src/identity/members.ts +7 -0
- package/examples/build-events/src/identity/verifier.ts +22 -0
- package/examples/build-events/src/notifier/Notices.ts +26 -0
- package/examples/build-events/src/notifier/routes.ts +71 -0
- package/examples/build-events/src/notifier.ts +48 -0
- package/examples/build-events/src/routes.ts +71 -0
- package/examples/build-events/src/server.ts +62 -0
- package/examples/build-events/test/builds/events.test.ts +68 -0
- package/examples/build-events/test/builds/log.test.ts +71 -0
- package/examples/build-events/test/builds/progress.test.ts +57 -0
- package/examples/build-events/test/builds/status.test.ts +42 -0
- package/examples/build-events/test/commands.test.ts +103 -0
- package/examples/build-events/test/deploys/events.test.ts +52 -0
- package/examples/build-events/test/http.test.ts +95 -0
- package/examples/build-events/test/notifier.test.ts +80 -0
- package/examples/build-events/test/routes.test.ts +96 -0
- package/examples/build-events/test/rpc.test.ts +84 -0
- package/examples/build-events/test/support/as.ts +8 -0
- package/examples/build-events/test/support/memory.ts +43 -0
- package/examples/build-events/test/webhooks.test.ts +143 -0
- package/examples/kitchen-sink/README.md +178 -166
- package/examples/kitchen-sink/src/Actions.ts +11 -3
- package/examples/kitchen-sink/src/Api.ts +12 -6
- package/examples/kitchen-sink/src/Rpc.ts +7 -5
- package/examples/kitchen-sink/src/admin/Admin.ts +21 -10
- package/examples/kitchen-sink/src/admin/authenticate.ts +8 -4
- package/examples/kitchen-sink/src/admin/authorize.ts +13 -5
- package/examples/kitchen-sink/src/admin/grants.ts +5 -3
- package/examples/kitchen-sink/src/admin/handlers.ts +9 -5
- package/examples/kitchen-sink/src/agent.ts +14 -6
- package/examples/kitchen-sink/src/assistant/Assistant.ts +21 -15
- package/examples/kitchen-sink/src/assistant/conversations.ts +14 -9
- package/examples/kitchen-sink/src/assistant/demo-model.ts +2 -2
- package/examples/kitchen-sink/src/assistant/handlers.ts +42 -22
- package/examples/kitchen-sink/src/attachments/Attachments.ts +6 -6
- package/examples/kitchen-sink/src/attachments/download.ts +7 -6
- package/examples/kitchen-sink/src/attachments/handlers.ts +9 -4
- package/examples/kitchen-sink/src/attachments/store.ts +25 -18
- package/examples/kitchen-sink/src/browser.ts +10 -4
- package/examples/kitchen-sink/src/cli.ts +28 -12
- package/examples/kitchen-sink/src/config.ts +1 -1
- package/examples/kitchen-sink/src/identity/Identity.ts +24 -10
- package/examples/kitchen-sink/src/identity/authenticate.ts +19 -11
- package/examples/kitchen-sink/src/identity/authorize.ts +14 -4
- package/examples/kitchen-sink/src/identity/directory.ts +16 -7
- package/examples/kitchen-sink/src/identity/handlers.ts +1 -0
- package/examples/kitchen-sink/src/identity/members.ts +7 -2
- package/examples/kitchen-sink/src/identity/quota.ts +23 -20
- package/examples/kitchen-sink/src/issues/Issues.ts +91 -15
- package/examples/kitchen-sink/src/issues/assistant.ts +4 -2
- package/examples/kitchen-sink/src/issues/events.ts +46 -0
- package/examples/kitchen-sink/src/issues/export.ts +35 -0
- package/examples/kitchen-sink/src/issues/guide.ts +4 -3
- package/examples/kitchen-sink/src/issues/handlers.ts +20 -7
- package/examples/kitchen-sink/src/issues/store.ts +156 -67
- package/examples/kitchen-sink/src/issues/write-limit.ts +44 -18
- package/examples/kitchen-sink/src/memory.ts +11 -2
- package/examples/kitchen-sink/src/ops.ts +10 -5
- package/examples/kitchen-sink/src/request-policy.ts +21 -9
- package/examples/kitchen-sink/src/routes.ts +67 -27
- package/examples/kitchen-sink/src/sdk.ts +22 -12
- package/examples/kitchen-sink/src/seed.ts +15 -9
- package/examples/kitchen-sink/src/server.ts +24 -10
- package/examples/kitchen-sink/src/stdio.ts +9 -3
- package/examples/kitchen-sink/src/workspaces/Workspaces.ts +10 -7
- package/examples/kitchen-sink/src/workspaces/handlers.ts +3 -2
- package/examples/kitchen-sink/src/workspaces/registry.ts +16 -10
- package/examples/kitchen-sink/src/workspaces/tenant.ts +12 -5
- package/examples/kitchen-sink/test/admin/admin.test.ts +38 -23
- package/examples/kitchen-sink/test/assistant/assistant.test.ts +471 -299
- package/examples/kitchen-sink/test/attachments/attachments.test.ts +135 -94
- package/examples/kitchen-sink/test/identity/identity.test.ts +110 -82
- package/examples/kitchen-sink/test/identity/session.test.ts +65 -43
- package/examples/kitchen-sink/test/issues/assistant.test.ts +50 -31
- package/examples/kitchen-sink/test/issues/events.test.ts +273 -0
- package/examples/kitchen-sink/test/issues/export.test.ts +120 -0
- package/examples/kitchen-sink/test/issues/issues.test.ts +105 -75
- package/examples/kitchen-sink/test/request-policy.test.ts +108 -47
- package/examples/kitchen-sink/test/routes.test.ts +102 -57
- package/examples/kitchen-sink/test/support/as.ts +9 -0
- package/examples/kitchen-sink/test/support/scripted.ts +6 -5
- package/examples/kitchen-sink/test/support/services.ts +17 -0
- package/examples/live-rpc/README.md +33 -35
- package/examples/live-rpc/src/Rpc.ts +9 -5
- package/examples/live-rpc/src/browser.ts +39 -11
- package/examples/live-rpc/src/chat/Chat.ts +30 -16
- package/examples/live-rpc/src/chat/authorize.ts +2 -1
- package/examples/live-rpc/src/chat/events.ts +74 -0
- package/examples/live-rpc/src/chat/handlers.ts +13 -5
- package/examples/live-rpc/src/chat/room.ts +9 -5
- package/examples/live-rpc/src/chat/store.ts +51 -14
- package/examples/live-rpc/src/client.ts +22 -5
- package/examples/live-rpc/src/identity/Identity.ts +3 -3
- package/examples/live-rpc/src/identity/verifier.ts +8 -4
- package/examples/live-rpc/src/page.ts +58 -12
- package/examples/live-rpc/src/routes.ts +14 -10
- package/examples/live-rpc/src/server.ts +15 -5
- package/examples/live-rpc/test/browser.test.ts +127 -50
- package/examples/live-rpc/test/chat/chat.test.ts +215 -23
- package/examples/live-rpc/test/routes.test.ts +130 -46
- package/examples/live-rpc/test/server.test.ts +94 -33
- package/examples/mcp-tools/README.md +24 -38
- package/examples/mcp-tools/src/platform/fake-farm.ts +63 -24
- package/examples/mcp-tools/src/platform/print-farm.ts +59 -29
- package/examples/mcp-tools/src/printers/Printers.ts +18 -7
- package/examples/mcp-tools/src/printers/farm.ts +15 -5
- package/examples/mcp-tools/src/printers/handlers.ts +12 -6
- package/examples/mcp-tools/src/routes.ts +8 -6
- package/examples/mcp-tools/src/server.ts +12 -5
- package/examples/mcp-tools/src/stdio.ts +3 -2
- package/examples/mcp-tools/test/printers/printers.test.ts +132 -89
- package/examples/mcp-tools/test/routes.test.ts +140 -81
- package/examples/notes-service/README.md +30 -45
- package/examples/notes-service/src/Api.ts +1 -0
- package/examples/notes-service/src/browser.ts +15 -14
- package/examples/notes-service/src/cli.ts +8 -3
- package/examples/notes-service/src/commands.ts +46 -20
- package/examples/notes-service/src/identity/Identity.ts +2 -2
- package/examples/notes-service/src/identity/authorize.ts +6 -2
- package/examples/notes-service/src/identity/handlers.ts +3 -2
- package/examples/notes-service/src/identity/users.ts +2 -1
- package/examples/notes-service/src/identity/verifier.ts +12 -6
- package/examples/notes-service/src/notes/Notes.ts +46 -22
- package/examples/notes-service/src/notes/handlers.ts +18 -9
- package/examples/notes-service/src/notes/store.ts +70 -43
- package/examples/notes-service/src/routes.ts +7 -5
- package/examples/notes-service/src/server.ts +12 -5
- package/examples/notes-service/src/status/Status.ts +1 -1
- package/examples/notes-service/src/status/handlers.ts +5 -3
- package/examples/notes-service/src/stdio.ts +7 -3
- package/examples/notes-service/src/worker.ts +6 -3
- package/examples/notes-service/test/commands.test.ts +164 -91
- package/examples/notes-service/test/notes/as.ts +3 -1
- package/examples/notes-service/test/notes/idempotency.test.ts +97 -67
- package/examples/notes-service/test/notes/in-process.test.ts +26 -15
- package/examples/notes-service/test/notes/notes.test.ts +49 -31
- package/examples/notes-service/test/routes.test.ts +106 -71
- package/examples/notes-service/test/worker.test.ts +21 -13
- package/examples/quickstart/README.md +2 -6
- package/examples/quickstart/src/Api.ts +1 -0
- package/examples/quickstart/src/client.ts +6 -3
- package/examples/quickstart/src/greeting/Greeting.ts +1 -1
- package/examples/quickstart/src/greeting/handlers.ts +5 -2
- package/examples/quickstart/src/routes.ts +3 -2
- package/examples/quickstart/src/server.ts +6 -2
- package/examples/quickstart/test/routes.test.ts +3 -2
- package/package.json +10 -4
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import * as Action from "@gjermundgaraba/effect-actions/Action";
|
|
2
|
+
import { Effect, Stream } from "effect";
|
|
3
|
+
|
|
4
|
+
import { authorize } from "../identity/authorize.js";
|
|
5
|
+
import { ExportIssues } from "./Issues.js";
|
|
6
|
+
import { IssueStore } from "./store.js";
|
|
7
|
+
import { exportLimit } from "./write-limit.js";
|
|
8
|
+
|
|
9
|
+
export const exports = Action.implement(
|
|
10
|
+
ExportIssues,
|
|
11
|
+
Effect.gen(function* () {
|
|
12
|
+
const store = yield* IssueStore;
|
|
13
|
+
const limited = yield* exportLimit;
|
|
14
|
+
|
|
15
|
+
// The handler returns a `Stream`. The limit is spent before the first issue, so a refused
|
|
16
|
+
// export fails as an action answering one value does, a 429 over HTTP; a failure after an
|
|
17
|
+
// issue would end the stream instead, the issues before it already sent.
|
|
18
|
+
return ({ status }, { progress }) =>
|
|
19
|
+
Stream.unwrap(
|
|
20
|
+
limited((member) =>
|
|
21
|
+
Effect.map(store.count(member.workspace, status), (total) =>
|
|
22
|
+
store.all(member.workspace, status).pipe(
|
|
23
|
+
Stream.zipWithIndex,
|
|
24
|
+
// `progress` never waits for the caller, dropping the oldest values a slow one has not taken:
|
|
25
|
+
// `exported` may skip numbers, so it says how far the export is, not what arrived.
|
|
26
|
+
Stream.mapEffect(([issue, index]) =>
|
|
27
|
+
Effect.as(progress({ exported: index + 1, total }), issue)
|
|
28
|
+
)
|
|
29
|
+
)
|
|
30
|
+
)
|
|
31
|
+
)
|
|
32
|
+
);
|
|
33
|
+
}),
|
|
34
|
+
{ authorize }
|
|
35
|
+
);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import * as Action from "@gjermundgaraba/effect-actions/Action";
|
|
1
2
|
import { Effect, Layer, Schema } from "effect";
|
|
2
3
|
import { McpServer } from "effect/ai";
|
|
3
|
-
|
|
4
|
+
|
|
4
5
|
import { Issues } from "./Issues.js";
|
|
5
6
|
|
|
6
7
|
const guide = `# Triage
|
|
@@ -30,7 +31,7 @@ export const features = Layer.mergeAll(
|
|
|
30
31
|
parameters: { focus: Schema.String },
|
|
31
32
|
content: ({ focus }) =>
|
|
32
33
|
Effect.succeed(
|
|
33
|
-
`Call ${tools.listIssues.name} for the open issues and ${tools.getIssue.name} for those about ${focus}. For each, propose ${tools.commentOnIssue.name} or ${tools.closeIssue.name}. Follow tracker://guides/triage
|
|
34
|
+
`Call ${tools.listIssues.name} for the open issues and ${tools.getIssue.name} for those about ${focus}. For each, propose ${tools.commentOnIssue.name} or ${tools.closeIssue.name}. Follow tracker://guides/triage.`
|
|
34
35
|
),
|
|
35
|
-
})
|
|
36
|
+
})
|
|
36
37
|
);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { Effect } from "effect";
|
|
2
1
|
import * as Action from "@gjermundgaraba/effect-actions/Action";
|
|
2
|
+
import { Effect } from "effect";
|
|
3
|
+
|
|
3
4
|
import { authorize } from "../identity/authorize.js";
|
|
4
5
|
import { CurrentMember } from "../identity/Identity.js";
|
|
5
6
|
import { Issues } from "./Issues.js";
|
|
@@ -22,16 +23,28 @@ export const issues = Action.implement(
|
|
|
22
23
|
const member = yield* CurrentMember;
|
|
23
24
|
const listed = yield* store.list(member.workspace, status, limit);
|
|
24
25
|
|
|
25
|
-
return {
|
|
26
|
+
return {
|
|
27
|
+
issues: listed.map((issue) => ({
|
|
28
|
+
id: issue.id,
|
|
29
|
+
title: issue.title,
|
|
30
|
+
status: issue.status,
|
|
31
|
+
})),
|
|
32
|
+
};
|
|
26
33
|
}),
|
|
27
34
|
getIssue: ({ id }) =>
|
|
28
|
-
Effect.flatMap(CurrentMember, (member) =>
|
|
35
|
+
Effect.flatMap(CurrentMember, (member) =>
|
|
36
|
+
store.get(member.workspace, id)
|
|
37
|
+
),
|
|
29
38
|
// One function wraps every limited handler, and gives it the member it counted.
|
|
30
|
-
createIssue: (draft) =>
|
|
39
|
+
createIssue: (draft) =>
|
|
40
|
+
limited((member) => store.create(member.workspace, draft)),
|
|
31
41
|
commentOnIssue: ({ id, text }) =>
|
|
32
|
-
limited((member) =>
|
|
33
|
-
|
|
42
|
+
limited((member) =>
|
|
43
|
+
store.comment(member.workspace, id, { author: member.id, text })
|
|
44
|
+
),
|
|
45
|
+
closeIssue: ({ id }) =>
|
|
46
|
+
limited((member) => store.close(member.workspace, id)),
|
|
34
47
|
};
|
|
35
48
|
}),
|
|
36
|
-
{ authorize }
|
|
49
|
+
{ authorize }
|
|
37
50
|
);
|
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
import { Context, Effect, Layer } from "effect";
|
|
2
|
-
|
|
1
|
+
import { Context, Effect, Layer, PubSub, Stream } from "effect";
|
|
2
|
+
|
|
3
|
+
import { IssueNotFound } from "./Issues.js";
|
|
4
|
+
import type { Comment, Issue, IssueChange, Status } from "./Issues.js";
|
|
3
5
|
|
|
4
6
|
/**
|
|
5
|
-
* Where issues live, each in one workspace. Every method takes the workspace, so
|
|
6
|
-
* isolation is the data access's: no surface and no handler can skip it, and an issue of
|
|
7
|
+
* Where issues live, each in one workspace. Every method but `changes` takes the workspace, so
|
|
8
|
+
* tenant isolation is the data access's: no surface and no handler can skip it, and an issue of
|
|
7
9
|
* another workspace is simply not found.
|
|
8
10
|
*/
|
|
9
11
|
export class IssueStore extends Context.Service<
|
|
@@ -12,85 +14,172 @@ export class IssueStore extends Context.Service<
|
|
|
12
14
|
readonly list: (
|
|
13
15
|
workspace: string,
|
|
14
16
|
status: typeof Status.Type | undefined,
|
|
15
|
-
limit: number
|
|
16
|
-
) => Effect.Effect<
|
|
17
|
-
|
|
17
|
+
limit: number
|
|
18
|
+
) => Effect.Effect<readonly Issue[]>;
|
|
19
|
+
/** Every issue, oldest first. */
|
|
20
|
+
readonly all: (
|
|
21
|
+
workspace: string,
|
|
22
|
+
status: typeof Status.Type | undefined
|
|
23
|
+
) => Stream.Stream<Issue>;
|
|
24
|
+
readonly count: (
|
|
25
|
+
workspace: string,
|
|
26
|
+
status: typeof Status.Type | undefined
|
|
27
|
+
) => Effect.Effect<number>;
|
|
28
|
+
readonly get: (
|
|
29
|
+
workspace: string,
|
|
30
|
+
id: string
|
|
31
|
+
) => Effect.Effect<Issue, IssueNotFound>;
|
|
18
32
|
readonly create: (
|
|
19
33
|
workspace: string,
|
|
20
|
-
draft: Pick<Issue, "title" | "body" | "assignee"
|
|
34
|
+
draft: Pick<Issue, "title" | "body" | "assignee">
|
|
21
35
|
) => Effect.Effect<Issue>;
|
|
22
36
|
readonly comment: (
|
|
23
37
|
workspace: string,
|
|
24
38
|
id: string,
|
|
25
|
-
comment: typeof Comment.Type
|
|
39
|
+
comment: typeof Comment.Type
|
|
26
40
|
) => Effect.Effect<Issue, IssueNotFound>;
|
|
27
|
-
readonly close: (
|
|
41
|
+
readonly close: (
|
|
42
|
+
workspace: string,
|
|
43
|
+
id: string
|
|
44
|
+
) => Effect.Effect<Issue, IssueNotFound>;
|
|
45
|
+
/**
|
|
46
|
+
* Every change from now on, in the order the store makes them, of every workspace: whatever
|
|
47
|
+
* reads it filters by workspace itself, as `issueEvents`' `match` does.
|
|
48
|
+
*/
|
|
49
|
+
readonly changes: Stream.Stream<typeof IssueChange.Type>;
|
|
28
50
|
}
|
|
29
51
|
>()("tracker/IssueStore") {
|
|
30
52
|
// DEMO ONLY: memory, lost when the process stops. A real store is another Layer of this
|
|
31
53
|
// service, chosen by the entrypoint.
|
|
32
|
-
static readonly layerMemory = Layer.
|
|
33
|
-
|
|
54
|
+
static readonly layerMemory = Layer.effect(
|
|
55
|
+
IssueStore,
|
|
56
|
+
Effect.gen(function* () {
|
|
57
|
+
const issues = new Map<string, Issue[]>();
|
|
58
|
+
const changes = yield* PubSub.unbounded<typeof IssueChange.Type>();
|
|
34
59
|
|
|
35
|
-
|
|
60
|
+
// Published in the step of the write, after it, so a subscriber never sees a change the
|
|
61
|
+
// store did not make, and sees them in the order they were made. A database publishes
|
|
62
|
+
// from its own change feed, or an outbox written in the same transaction.
|
|
63
|
+
const publish = (
|
|
64
|
+
workspace: string,
|
|
65
|
+
issue: Issue,
|
|
66
|
+
change: (typeof IssueChange.Type)["change"],
|
|
67
|
+
comment?: typeof Comment.Type
|
|
68
|
+
) =>
|
|
69
|
+
PubSub.publishUnsafe(changes, {
|
|
70
|
+
workspace,
|
|
71
|
+
id: issue.id,
|
|
72
|
+
title: issue.title,
|
|
73
|
+
status: issue.status,
|
|
74
|
+
change,
|
|
75
|
+
...(comment === undefined ? {} : { comment }),
|
|
76
|
+
});
|
|
36
77
|
|
|
37
|
-
|
|
38
|
-
const issue = of(workspace).find((candidate) => candidate.id === id);
|
|
78
|
+
const of = (workspace: string) => issues.get(workspace) ?? [];
|
|
39
79
|
|
|
40
|
-
|
|
80
|
+
const having = (
|
|
81
|
+
workspace: string,
|
|
82
|
+
status: typeof Status.Type | undefined
|
|
83
|
+
) =>
|
|
84
|
+
of(workspace).filter(
|
|
85
|
+
(issue) => status === undefined || issue.status === status
|
|
86
|
+
);
|
|
41
87
|
|
|
42
|
-
|
|
43
|
-
|
|
88
|
+
const get = Effect.fn("IssueStore.get")(function* (
|
|
89
|
+
workspace: string,
|
|
90
|
+
id: string
|
|
91
|
+
) {
|
|
92
|
+
const issue = of(workspace).find((candidate) => candidate.id === id);
|
|
44
93
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
to: (issue: Issue) => Issue,
|
|
52
|
-
) {
|
|
53
|
-
const issue = of(workspace).find((candidate) => candidate.id === id);
|
|
54
|
-
|
|
55
|
-
if (issue === undefined) return yield* new IssueNotFound({ id });
|
|
56
|
-
|
|
57
|
-
const changed = to(issue);
|
|
58
|
-
|
|
59
|
-
issues.set(
|
|
60
|
-
workspace,
|
|
61
|
-
of(workspace).map((current) => (current === issue ? changed : current)),
|
|
62
|
-
);
|
|
63
|
-
|
|
64
|
-
return changed;
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
return IssueStore.of({
|
|
68
|
-
list: (workspace, status, limit) =>
|
|
69
|
-
Effect.sync(() =>
|
|
70
|
-
of(workspace)
|
|
71
|
-
.filter((issue) => status === undefined || issue.status === status)
|
|
72
|
-
.toReversed()
|
|
73
|
-
.slice(0, limit),
|
|
74
|
-
),
|
|
75
|
-
get,
|
|
76
|
-
create: (workspace, draft) =>
|
|
77
|
-
Effect.sync(() => {
|
|
78
|
-
const existing = of(workspace);
|
|
79
|
-
|
|
80
|
-
const issue: Issue = {
|
|
81
|
-
...draft,
|
|
82
|
-
id: `${workspace.toUpperCase()}-${existing.length + 1}`,
|
|
83
|
-
status: "open",
|
|
84
|
-
comments: [],
|
|
85
|
-
};
|
|
86
|
-
|
|
87
|
-
issues.set(workspace, [...existing, issue]);
|
|
94
|
+
if (issue === undefined) {
|
|
95
|
+
return yield* new IssueNotFound({ id });
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return issue;
|
|
99
|
+
});
|
|
88
100
|
|
|
101
|
+
// No `yield*` between the read and the write: a fiber may be set aside at one, and two
|
|
102
|
+
// changes that each read the issue before either wrote would keep only the second. A
|
|
103
|
+
// database does this in one statement or a transaction.
|
|
104
|
+
// A change `to` answers with the same issue for is no change, and is not published.
|
|
105
|
+
const change = Effect.fn("IssueStore.change")(function* (
|
|
106
|
+
workspace: string,
|
|
107
|
+
id: string,
|
|
108
|
+
to: (issue: Issue) => Issue,
|
|
109
|
+
kind: (typeof IssueChange.Type)["change"],
|
|
110
|
+
comment?: typeof Comment.Type
|
|
111
|
+
) {
|
|
112
|
+
const issue = of(workspace).find((candidate) => candidate.id === id);
|
|
113
|
+
|
|
114
|
+
if (issue === undefined) {
|
|
115
|
+
return yield* new IssueNotFound({ id });
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const changed = to(issue);
|
|
119
|
+
|
|
120
|
+
if (changed === issue) {
|
|
89
121
|
return issue;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
issues.set(
|
|
125
|
+
workspace,
|
|
126
|
+
of(workspace).map((current) =>
|
|
127
|
+
current === issue ? changed : current
|
|
128
|
+
)
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
publish(workspace, changed, kind, comment);
|
|
132
|
+
|
|
133
|
+
return changed;
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
return IssueStore.of({
|
|
137
|
+
list: (workspace, status, limit) =>
|
|
138
|
+
Effect.sync(() =>
|
|
139
|
+
having(workspace, status).toReversed().slice(0, limit)
|
|
140
|
+
),
|
|
141
|
+
// A database reads it in pages, by id, so an export never holds the workspace whole.
|
|
142
|
+
all: (workspace, status) =>
|
|
143
|
+
Stream.suspend(() => Stream.fromIterable(having(workspace, status))),
|
|
144
|
+
count: (workspace, status) =>
|
|
145
|
+
Effect.sync(() => having(workspace, status).length),
|
|
146
|
+
get,
|
|
147
|
+
create: (workspace, draft) =>
|
|
148
|
+
Effect.sync(() => {
|
|
149
|
+
const existing = of(workspace);
|
|
150
|
+
|
|
151
|
+
const issue: Issue = {
|
|
152
|
+
...draft,
|
|
153
|
+
id: `${workspace.toUpperCase()}-${existing.length + 1}`,
|
|
154
|
+
status: "open",
|
|
155
|
+
comments: [],
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
issues.set(workspace, [...existing, issue]);
|
|
159
|
+
publish(workspace, issue, "filed");
|
|
160
|
+
|
|
161
|
+
return issue;
|
|
162
|
+
}),
|
|
163
|
+
comment: (workspace, id, comment) =>
|
|
164
|
+
change(
|
|
165
|
+
workspace,
|
|
166
|
+
id,
|
|
167
|
+
(issue) => ({ ...issue, comments: [...issue.comments, comment] }),
|
|
168
|
+
"commented",
|
|
169
|
+
comment
|
|
170
|
+
),
|
|
171
|
+
close: (workspace, id) =>
|
|
172
|
+
change(
|
|
173
|
+
workspace,
|
|
174
|
+
id,
|
|
175
|
+
(issue) =>
|
|
176
|
+
issue.status === "closed"
|
|
177
|
+
? issue
|
|
178
|
+
: { ...issue, status: "closed" },
|
|
179
|
+
"closed"
|
|
180
|
+
),
|
|
181
|
+
changes: Stream.fromPubSub(changes),
|
|
182
|
+
});
|
|
183
|
+
})
|
|
184
|
+
);
|
|
96
185
|
}
|
|
@@ -1,28 +1,25 @@
|
|
|
1
1
|
import { Duration, Effect } from "effect";
|
|
2
2
|
import { RateLimiter } from "effect/persistence";
|
|
3
|
-
|
|
3
|
+
|
|
4
|
+
import { CurrentMember } from "../identity/Identity.js";
|
|
5
|
+
import type { Member } from "../identity/Identity.js";
|
|
4
6
|
import { RateLimited } from "./Issues.js";
|
|
5
7
|
|
|
6
|
-
|
|
7
|
-
// holds wherever they run: HTTP, MCP, RPC, stdio and the assistant's tools all count against
|
|
8
|
-
// the one limiter. A builder yields this once and wraps each limited handler in the function
|
|
9
|
-
// it gets, which fails with an error those actions' contracts declare; every surface sends it
|
|
10
|
-
// as any declared error. The limiter is a service, so its counts are shared wherever it is
|
|
11
|
-
// provided: memory.ts provides Effect's own, `RateLimiter.layer` over a store. `consume` counts
|
|
12
|
-
// in a fixed window unless given `algorithm: "token-bucket"`: ten writes in each minute, so
|
|
13
|
-
// twenty may pass across the boundary between two. A strict limit, ten in any sixty seconds,
|
|
14
|
-
// is a service of your own of the same shape, yielded here, never part of the issue store.
|
|
15
|
-
export const writeLimit = Effect.gen(function* () {
|
|
8
|
+
const limitedBy = Effect.gen(function* () {
|
|
16
9
|
const limiter = yield* RateLimiter.RateLimiter;
|
|
17
10
|
|
|
18
|
-
return <A, E, R>(
|
|
11
|
+
return <A, E, R>(
|
|
12
|
+
name: string,
|
|
13
|
+
limit: number,
|
|
14
|
+
run: (member: Member) => Effect.Effect<A, E, R>
|
|
15
|
+
) =>
|
|
19
16
|
Effect.gen(function* () {
|
|
20
17
|
const member = yield* CurrentMember;
|
|
21
18
|
|
|
22
19
|
yield* limiter
|
|
23
20
|
.consume({
|
|
24
|
-
key: JSON.stringify([
|
|
25
|
-
limit
|
|
21
|
+
key: JSON.stringify([name, member.workspace, member.id]),
|
|
22
|
+
limit,
|
|
26
23
|
window: "1 minute",
|
|
27
24
|
onExceeded: "fail",
|
|
28
25
|
})
|
|
@@ -31,12 +28,41 @@ export const writeLimit = Effect.gen(function* () {
|
|
|
31
28
|
"RateLimiterError",
|
|
32
29
|
"RateLimitExceeded",
|
|
33
30
|
({ retryAfter }) =>
|
|
34
|
-
Effect.fail(
|
|
31
|
+
Effect.fail(
|
|
32
|
+
new RateLimited({
|
|
33
|
+
retryAfterSeconds: Duration.toSeconds(retryAfter),
|
|
34
|
+
})
|
|
35
|
+
),
|
|
35
36
|
// Any other reason is the limiter's store failing: a defect, not the caller's.
|
|
36
|
-
Effect.die
|
|
37
|
-
)
|
|
37
|
+
Effect.die
|
|
38
|
+
)
|
|
38
39
|
);
|
|
39
40
|
|
|
40
|
-
return yield*
|
|
41
|
+
return yield* run(member);
|
|
41
42
|
});
|
|
42
43
|
});
|
|
44
|
+
|
|
45
|
+
// The write limit: ten writes a minute per member. It is the handlers', not a surface's, so it
|
|
46
|
+
// holds wherever they run: HTTP, MCP, RPC, stdio and the assistant's tools all count against
|
|
47
|
+
// the one limiter. A builder yields this once and wraps each limited handler in the function
|
|
48
|
+
// it gets, which fails with an error those actions' contracts declare; every surface sends it
|
|
49
|
+
// as any declared error. The limiter is a service, so its counts are shared wherever it is
|
|
50
|
+
// provided: memory.ts provides Effect's own, `RateLimiter.layer` over a store. `consume` counts
|
|
51
|
+
// in a fixed window unless given `algorithm: "token-bucket"`: ten writes in each minute, so
|
|
52
|
+
// twenty may pass across the boundary between two. A strict limit, ten in any sixty seconds,
|
|
53
|
+
// is a service of your own of the same shape, yielded here, never part of the issue store.
|
|
54
|
+
export const writeLimit = Effect.gen(function* () {
|
|
55
|
+
const limited = yield* limitedBy;
|
|
56
|
+
|
|
57
|
+
return <A, E, R>(write: (member: Member) => Effect.Effect<A, E, R>) =>
|
|
58
|
+
limited("writes", 10, write);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
// The export limit: two exports a minute per member, on the same limiter under a key of its
|
|
62
|
+
// own, so exporting spends no write.
|
|
63
|
+
export const exportLimit = Effect.gen(function* () {
|
|
64
|
+
const limited = yield* limitedBy;
|
|
65
|
+
|
|
66
|
+
return <A, E, R>(read: (member: Member) => Effect.Effect<A, E, R>) =>
|
|
67
|
+
limited("exports", 2, read);
|
|
68
|
+
});
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import * as Event from "@gjermundgaraba/effect-actions/Event";
|
|
1
2
|
import { Layer } from "effect";
|
|
2
3
|
import { RateLimiter } from "effect/persistence";
|
|
4
|
+
|
|
3
5
|
import { Grants } from "./admin/grants.js";
|
|
4
6
|
import { Conversations } from "./assistant/conversations.js";
|
|
5
7
|
import { demoModel } from "./assistant/demo-model.js";
|
|
@@ -13,10 +15,14 @@ import { WorkspaceRegistry } from "./workspaces/registry.js";
|
|
|
13
15
|
// test replaces the one it is about.
|
|
14
16
|
|
|
15
17
|
/** Both stores. The attachment store is built on the issue store: `provideMerge` keeps it. */
|
|
16
|
-
export const stores = AttachmentStore.layerMemory.pipe(
|
|
18
|
+
export const stores = AttachmentStore.layerMemory.pipe(
|
|
19
|
+
Layer.provideMerge(IssueStore.layerMemory)
|
|
20
|
+
);
|
|
17
21
|
|
|
18
22
|
/** Effect's own limiter, counting in memory. `RateLimiter.layerStoreRedis` counts across processes. */
|
|
19
|
-
export const limiter = RateLimiter.layer.pipe(
|
|
23
|
+
export const limiter = RateLimiter.layer.pipe(
|
|
24
|
+
Layer.provide(RateLimiter.layerStoreMemory)
|
|
25
|
+
);
|
|
20
26
|
|
|
21
27
|
/** Everything the server needs, for both ports. A local entrypoint provides only what it serves. */
|
|
22
28
|
export const memory = Layer.mergeAll(
|
|
@@ -27,4 +33,7 @@ export const memory = Layer.mergeAll(
|
|
|
27
33
|
Grants.layerDemo,
|
|
28
34
|
Conversations.layerMemory,
|
|
29
35
|
demoModel,
|
|
36
|
+
// The issue changes' log, kept in this process for the subscribers its sockets and streams
|
|
37
|
+
// hold.
|
|
38
|
+
Event.layerMemory
|
|
30
39
|
);
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { NodeRuntime, NodeServices } from "@effect/platform-node";
|
|
2
|
+
import * as ActionCli from "@gjermundgaraba/effect-actions/ActionCli";
|
|
2
3
|
import { Effect, Layer } from "effect";
|
|
3
4
|
import { Command } from "effect/cli";
|
|
4
|
-
|
|
5
|
-
import { Operator
|
|
5
|
+
|
|
6
|
+
import { Operator } from "./admin/Admin.js";
|
|
7
|
+
import type { TrustedOperator } from "./admin/Admin.js";
|
|
6
8
|
import { Grants } from "./admin/grants.js";
|
|
7
9
|
import { admin } from "./admin/handlers.js";
|
|
8
10
|
import { WorkspaceRegistry } from "./workspaces/registry.js";
|
|
@@ -19,7 +21,10 @@ const ops = ActionCli.make(admin, {
|
|
|
19
21
|
listWorkspaces: {
|
|
20
22
|
render: ({ workspaces }) =>
|
|
21
23
|
workspaces
|
|
22
|
-
.map(
|
|
24
|
+
.map(
|
|
25
|
+
({ slug, name, suspended }) =>
|
|
26
|
+
`${slug} ${name}${suspended ? " (suspended)" : ""}`
|
|
27
|
+
)
|
|
23
28
|
.join("\n"),
|
|
24
29
|
},
|
|
25
30
|
suspendWorkspace: { positional: ["workspace"] },
|
|
@@ -29,11 +34,11 @@ const ops = ActionCli.make(admin, {
|
|
|
29
34
|
// Only what these actions are built from. DEMO ONLY: memory, so this console sees its own
|
|
30
35
|
// process, not the server's data. A real one provides the database Layers the server does.
|
|
31
36
|
Command.provide(Layer.merge(WorkspaceRegistry.layerMemory, Grants.layerDemo)),
|
|
32
|
-
Command.provideSync(Operator, operator)
|
|
37
|
+
Command.provideSync(Operator, operator)
|
|
33
38
|
);
|
|
34
39
|
|
|
35
40
|
Command.run(ops, { version: "1.0.0" }).pipe(
|
|
36
41
|
Effect.provide(NodeServices.layer),
|
|
37
42
|
ActionCli.logToStderr,
|
|
38
|
-
NodeRuntime.runMain
|
|
43
|
+
NodeRuntime.runMain()
|
|
39
44
|
);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Effect, Layer, Option } from "effect";
|
|
2
2
|
import { HttpRouter, HttpServerRequest, HttpServerResponse } from "effect/http";
|
|
3
|
+
|
|
3
4
|
import { allowedOrigins, publicUrl } from "./config.js";
|
|
4
5
|
|
|
5
6
|
const loopback = ["localhost", "127.0.0.1"];
|
|
@@ -13,32 +14,39 @@ const loopback = ["localhost", "127.0.0.1"];
|
|
|
13
14
|
// authentication, too late.
|
|
14
15
|
export const requestPolicy = HttpRouter.middleware(
|
|
15
16
|
Effect.gen(function* () {
|
|
16
|
-
const hosts = [...loopback, new URL(yield* publicUrl).hostname];
|
|
17
|
+
const hosts = new Set([...loopback, new URL(yield* publicUrl).hostname]);
|
|
17
18
|
const origins = yield* allowedOrigins;
|
|
18
19
|
|
|
19
20
|
return (httpEffect) =>
|
|
20
21
|
Effect.gen(function* () {
|
|
21
22
|
const request = yield* HttpServerRequest.HttpServerRequest;
|
|
22
|
-
const url = HttpServerRequest.toURL(request.modify({ url: request.originalUrl }));
|
|
23
23
|
|
|
24
|
-
|
|
24
|
+
const url = HttpServerRequest.toURL(
|
|
25
|
+
request.modify({ url: request.originalUrl })
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
if (Option.isNone(url) || !hosts.has(url.value.hostname)) {
|
|
25
29
|
return HttpServerResponse.text("Host not allowed", { status: 403 });
|
|
26
30
|
}
|
|
27
31
|
|
|
28
|
-
const origin = request.headers
|
|
32
|
+
const { origin } = request.headers;
|
|
29
33
|
|
|
30
34
|
// The request's own origin is that of a page served from this port directly. Behind a
|
|
31
35
|
// proxy ending TLS it is an `http:` one from Effect 4.0.6, whose `originalUrl` takes the
|
|
32
36
|
// scheme of the connection, whatever `X-Forwarded-Proto` says, so the public URL's
|
|
33
37
|
// origin is among `origins`.
|
|
34
|
-
if (
|
|
38
|
+
if (
|
|
39
|
+
origin !== undefined &&
|
|
40
|
+
origin !== url.value.origin &&
|
|
41
|
+
!origins.includes(origin)
|
|
42
|
+
) {
|
|
35
43
|
return HttpServerResponse.text("Origin not allowed", { status: 403 });
|
|
36
44
|
}
|
|
37
45
|
|
|
38
46
|
return yield* httpEffect;
|
|
39
47
|
});
|
|
40
48
|
}),
|
|
41
|
-
{ global: true }
|
|
49
|
+
{ global: true }
|
|
42
50
|
);
|
|
43
51
|
|
|
44
52
|
// CORS for the one cross-origin caller, a browser MCP client. `HttpRouter.cors` is global to
|
|
@@ -61,7 +69,11 @@ export const cors = Layer.unwrap(
|
|
|
61
69
|
"MCP-Name",
|
|
62
70
|
"Mcp-Session-Id",
|
|
63
71
|
],
|
|
64
|
-
exposedHeaders: [
|
|
65
|
-
|
|
66
|
-
|
|
72
|
+
exposedHeaders: [
|
|
73
|
+
"WWW-Authenticate",
|
|
74
|
+
"MCP-Protocol-Version",
|
|
75
|
+
"Mcp-Session-Id",
|
|
76
|
+
],
|
|
77
|
+
})
|
|
78
|
+
)
|
|
67
79
|
);
|