@gjermundgaraba/effect-actions 0.19.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 -48
- package/dist/Action.mjs +88 -44
- package/dist/ActionCli.d.mts +70 -35
- package/dist/ActionCli.mjs +53 -34
- package/dist/ActionHttp.d.mts +70 -38
- package/dist/ActionHttp.mjs +76 -25
- package/dist/ActionMcp.d.mts +56 -24
- package/dist/ActionMcp.mjs +104 -61
- package/dist/ActionRpc.d.mts +222 -57
- package/dist/ActionRpc.mjs +301 -49
- package/dist/ActionToolkit.d.mts +22 -7
- package/dist/ActionToolkit.mjs +37 -14
- package/dist/Authentication.d.mts +42 -12
- package/dist/Authentication.mjs +38 -32
- package/dist/Event.d.mts +370 -0
- package/dist/Event.mjs +103 -0
- package/dist/Testing.d.mts +1 -3
- package/dist/Testing.mjs +2 -3
- package/dist/Webhook.d.mts +146 -0
- package/dist/Webhook.mjs +72 -0
- package/dist/authentication/provider.d.mts +35 -12
- package/dist/authentication/provider.mjs +2 -3
- package/dist/authentication/refusal.mjs +3 -4
- package/dist/cli/commands.d.mts +9 -7
- package/dist/cli/commands.mjs +85 -35
- package/dist/contract/ast.mjs +5 -3
- package/dist/contract/authorizer.d.mts +8 -0
- package/dist/contract/authorizer.mjs +20 -0
- package/dist/contract/call.d.mts +9 -8
- package/dist/contract/call.mjs +49 -8
- package/dist/contract/callback.d.mts +7 -0
- package/dist/contract/callback.mjs +27 -0
- package/dist/contract/caller.d.mts +12 -3
- package/dist/contract/caller.mjs +23 -5
- 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.d.mts +0 -2
- package/dist/contract/declared.mjs +26 -11
- package/dist/contract/digest.mjs +6 -0
- package/dist/contract/errors.d.mts +9 -5
- package/dist/contract/errors.mjs +2 -3
- package/dist/contract/feed.d.mts +127 -0
- package/dist/contract/feed.mjs +249 -0
- package/dist/contract/hints.mjs +1 -2
- 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 -51
- package/dist/contract/implementation.mjs +94 -60
- 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/media.d.mts +1 -3
- package/dist/contract/media.mjs +1 -2
- package/dist/contract/names.mjs +15 -6
- 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 +15 -7
- 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 -14
- package/dist/http/client.mjs +52 -14
- 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 +2 -3
- 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 -99
- package/dist/mcp/client.mjs +60 -134
- package/dist/mcp/events.mjs +142 -0
- package/dist/mcp/progress.mjs +34 -0
- package/dist/mcp/protocol.mjs +40 -15
- 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 +14 -7
- package/dist/testing/memory.d.mts +1 -3
- package/dist/testing/memory.mjs +4 -5
- 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 +57 -61
- 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 -53
- 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 +25 -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 +130 -33
- 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 +19 -17
|
@@ -1,45 +1,65 @@
|
|
|
1
|
-
import { Effect, Layer } from "effect";
|
|
2
|
-
import { HttpRouter, HttpServerResponse } from "effect/http";
|
|
3
|
-
import { HttpApiSwagger, OpenApi } from "effect/http-api";
|
|
4
1
|
import * as Action from "@gjermundgaraba/effect-actions/Action";
|
|
5
2
|
import * as ActionHttp from "@gjermundgaraba/effect-actions/ActionHttp";
|
|
6
3
|
import * as ActionMcp from "@gjermundgaraba/effect-actions/ActionMcp";
|
|
7
4
|
import * as ActionRpc from "@gjermundgaraba/effect-actions/ActionRpc";
|
|
5
|
+
import * as Webhook from "@gjermundgaraba/effect-actions/Webhook";
|
|
6
|
+
import { Effect, Layer } from "effect";
|
|
7
|
+
import { HttpRouter, HttpServerResponse } from "effect/http";
|
|
8
|
+
import { HttpApiSwagger, OpenApi } from "effect/http-api";
|
|
9
|
+
|
|
8
10
|
import { MemberActions, Tools } from "./Actions.js";
|
|
9
11
|
import { authenticateOperator } from "./admin/authenticate.js";
|
|
10
|
-
import { assistant } from "./assistant/handlers.js";
|
|
11
12
|
import { admin } from "./admin/handlers.js";
|
|
12
13
|
import { AdminHttp, Http } from "./Api.js";
|
|
14
|
+
import { assistant } from "./assistant/handlers.js";
|
|
13
15
|
import { download } from "./attachments/download.js";
|
|
14
16
|
import { attachments } from "./attachments/handlers.js";
|
|
15
17
|
import { allowedOrigins } from "./config.js";
|
|
16
|
-
import {
|
|
18
|
+
import {
|
|
19
|
+
authenticateBearer,
|
|
20
|
+
authenticateSession,
|
|
21
|
+
} from "./identity/authenticate.js";
|
|
17
22
|
import { identity } from "./identity/handlers.js";
|
|
18
23
|
import { Login } from "./identity/Identity.js";
|
|
19
24
|
import { HttpQuota, quota, RpcQuota } from "./identity/quota.js";
|
|
20
25
|
import { layer as assistantTools } from "./issues/assistant.js";
|
|
26
|
+
import { issueEvents } from "./issues/events.js";
|
|
27
|
+
import { exports } from "./issues/export.js";
|
|
21
28
|
import { features } from "./issues/guide.js";
|
|
22
29
|
import { issues } from "./issues/handlers.js";
|
|
30
|
+
import { IssueChanged } from "./issues/Issues.js";
|
|
23
31
|
import { cors, requestPolicy } from "./request-policy.js";
|
|
24
32
|
import { Rpc } from "./Rpc.js";
|
|
25
33
|
import { status } from "./workspaces/handlers.js";
|
|
26
34
|
import { resolveTenant } from "./workspaces/tenant.js";
|
|
27
35
|
import { GetStatus } from "./workspaces/Workspaces.js";
|
|
28
36
|
|
|
29
|
-
// Every domain's implementation of the public port's actions, once. Each surface
|
|
30
|
-
// this one list and serves
|
|
31
|
-
const implementations = [
|
|
37
|
+
// Every domain's implementation of the public port's actions and events, once. Each surface
|
|
38
|
+
// below takes this one list and serves what its binding or its `actions` names.
|
|
39
|
+
const implementations = [
|
|
40
|
+
status,
|
|
41
|
+
identity,
|
|
42
|
+
issues,
|
|
43
|
+
issueEvents,
|
|
44
|
+
exports,
|
|
45
|
+
attachments,
|
|
46
|
+
assistant,
|
|
47
|
+
];
|
|
32
48
|
|
|
33
|
-
// JSON routes at `/api/v1/<action>`, in two layers of one binding, each naming its `actions
|
|
34
|
-
//
|
|
35
|
-
// layer without it.
|
|
49
|
+
// JSON routes at `/api/v1/<action>`, in two layers of one binding, each naming its `actions`,
|
|
50
|
+
// an event among them: a layer given `actions` serves exactly what it names. The quota requires
|
|
51
|
+
// the caller, and a public route has none, so `getStatus` is served by a layer without it.
|
|
36
52
|
const api = Layer.mergeAll(
|
|
37
53
|
ActionHttp.layer(Http, implementations, { actions: [GetStatus] }),
|
|
38
|
-
ActionHttp.layer(Http, implementations, {
|
|
54
|
+
ActionHttp.layer(Http, implementations, {
|
|
55
|
+
actions: [...MemberActions, IssueChanged],
|
|
56
|
+
middleware: [HttpQuota],
|
|
57
|
+
})
|
|
39
58
|
);
|
|
40
59
|
|
|
41
60
|
// The same actions as MCP tools at `/mcp`: a mixed endpoint, serving the public `getStatus`
|
|
42
|
-
// beside the members' tools.
|
|
61
|
+
// beside the members' tools, and `issue.changed` as an MCP event, polled, streamed or
|
|
62
|
+
// delivered by webhook.
|
|
43
63
|
const mcp = Layer.unwrap(
|
|
44
64
|
Effect.map(allowedOrigins, (origins) =>
|
|
45
65
|
ActionMcp.layerHttp(implementations, {
|
|
@@ -47,24 +67,24 @@ const mcp = Layer.unwrap(
|
|
|
47
67
|
version: "1.0.0",
|
|
48
68
|
instructions: "Read an issue before commenting on it or closing it.",
|
|
49
69
|
authentication: Login,
|
|
50
|
-
actions: Tools,
|
|
70
|
+
actions: [...Tools, IssueChanged],
|
|
51
71
|
features,
|
|
52
72
|
// The endpoint's own Origin allowlist, narrower than the request policy, which has
|
|
53
73
|
// already refused a foreign Origin: that policy also lets through a page of any name
|
|
54
74
|
// this host answers to, such as http://127.0.0.1:3000. Not listed here, such a page gets
|
|
55
75
|
// a 403 from the endpoint.
|
|
56
76
|
allowedOrigins: origins,
|
|
57
|
-
})
|
|
58
|
-
)
|
|
77
|
+
})
|
|
78
|
+
)
|
|
59
79
|
);
|
|
60
80
|
|
|
61
81
|
// The members' actions as Effect RPC, for the web app, behind its session cookie and the same
|
|
62
82
|
// quota as native RPC middleware. It still requires its protocol and serialization, chosen by
|
|
63
83
|
// whoever serves it: server.ts gives it a WebSocket, and a test Effect's HTTP protocol, which
|
|
64
84
|
// answers in memory.
|
|
65
|
-
export const rpc = ActionRpc.layer(Rpc, implementations, {
|
|
66
|
-
|
|
67
|
-
);
|
|
85
|
+
export const rpc = ActionRpc.layer(Rpc, implementations, {
|
|
86
|
+
middleware: [RpcQuota],
|
|
87
|
+
}).pipe(Layer.provide([authenticateSession, quota, assistantTools]));
|
|
68
88
|
|
|
69
89
|
// Effect's own tools read the binding's native `HttpApi`: the OpenAPI document and a Swagger
|
|
70
90
|
// UI, each showing the bearer scheme on every route but `getStatus`.
|
|
@@ -72,9 +92,9 @@ const documentation = Layer.mergeAll(
|
|
|
72
92
|
HttpRouter.add(
|
|
73
93
|
"GET",
|
|
74
94
|
`${Http.prefix}/openapi.json`,
|
|
75
|
-
HttpServerResponse.jsonUnsafe(OpenApi.fromApi(Http.api))
|
|
95
|
+
HttpServerResponse.jsonUnsafe(OpenApi.fromApi(Http.api))
|
|
76
96
|
),
|
|
77
|
-
HttpApiSwagger.layer(Http.api, { path: "/docs" })
|
|
97
|
+
HttpApiSwagger.layer(Http.api, { path: "/docs" })
|
|
78
98
|
);
|
|
79
99
|
|
|
80
100
|
const health = HttpRouter.add("GET", "/healthz", HttpServerResponse.text("ok"));
|
|
@@ -94,12 +114,25 @@ const health = HttpRouter.add("GET", "/healthz", HttpServerResponse.text("ok"));
|
|
|
94
114
|
// The assistant's builder yields its tools, so their handlers are provided to the layers
|
|
95
115
|
// serving it, here and on `rpc`: one Layer, built once in the layer graph serving both, where
|
|
96
116
|
// the tools run the issue implementation the routes serve, with no second build of it.
|
|
97
|
-
|
|
117
|
+
//
|
|
118
|
+
// Webhook delivery serves MCP's `events/subscribe` and HTTP's
|
|
119
|
+
// `POST /api/v1/events/issue.changed/subscribe`: a member names a callback URL and a secret,
|
|
120
|
+
// and each change it may see is POSTed there, signed. One layer holds the subscriptions of
|
|
121
|
+
// both. It POSTs with an `HttpClient` and checks where each callback resolves with `Dns`, which
|
|
122
|
+
// server.ts provides.
|
|
123
|
+
export const publicRoutes = Layer.mergeAll(
|
|
124
|
+
api,
|
|
125
|
+
mcp,
|
|
126
|
+
download,
|
|
127
|
+
documentation,
|
|
128
|
+
health
|
|
129
|
+
).pipe(
|
|
130
|
+
Layer.provide(Webhook.layer()),
|
|
98
131
|
Layer.provide(assistantTools),
|
|
99
132
|
Layer.provide([authenticateBearer, authenticateSession, quota]),
|
|
100
133
|
Layer.provide(resolveTenant.layer),
|
|
101
134
|
Layer.provide(cors),
|
|
102
|
-
Layer.provide(requestPolicy)
|
|
135
|
+
Layer.provide(requestPolicy)
|
|
103
136
|
);
|
|
104
137
|
|
|
105
138
|
// The admin port: what operators reach, and nothing else. It is a router of its own, with its
|
|
@@ -107,12 +140,19 @@ export const publicRoutes = Layer.mergeAll(api, mcp, download, documentation, he
|
|
|
107
140
|
// serves one implementation the public port serves too, `status`.
|
|
108
141
|
export const adminRoutes = Layer.mergeAll(
|
|
109
142
|
ActionHttp.layer(AdminHttp, [status, admin]),
|
|
110
|
-
health
|
|
143
|
+
health
|
|
111
144
|
).pipe(Layer.provide(authenticateOperator), Layer.provide(requestPolicy));
|
|
112
145
|
|
|
113
146
|
// Every builder as one layer. The two ports are two layer graphs, and each would run the
|
|
114
147
|
// builder of an implementation it serves: `status`, which both serve, twice, each port then
|
|
115
148
|
// reporting an `instance` of its own. Provided above both, with the services, each builder
|
|
116
|
-
// runs once for every surface of both ports. The assistant's
|
|
117
|
-
// alone serves
|
|
118
|
-
|
|
149
|
+
// runs once for every surface of both ports. The assistant's, the export's and the event's are
|
|
150
|
+
// left out: the public port alone serves them, and builds each once for its routes and its
|
|
151
|
+
// rpcs.
|
|
152
|
+
export const builders = Action.layer([
|
|
153
|
+
status,
|
|
154
|
+
identity,
|
|
155
|
+
issues,
|
|
156
|
+
attachments,
|
|
157
|
+
admin,
|
|
158
|
+
]);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { Effect, flow } from "effect";
|
|
2
|
-
import { HttpClient, HttpClientRequest } from "effect/http";
|
|
3
1
|
import * as ActionHttp from "@gjermundgaraba/effect-actions/ActionHttp";
|
|
2
|
+
import { Effect, flow, Stream } from "effect";
|
|
3
|
+
import { HttpClient, HttpClientRequest } from "effect/http";
|
|
4
|
+
|
|
4
5
|
import { Http } from "./Api.js";
|
|
5
6
|
import type { Issue } from "./issues/Issues.js";
|
|
6
7
|
|
|
@@ -19,28 +20,37 @@ export const connect = (options: {
|
|
|
19
20
|
transformClient: HttpClient.mapRequest(
|
|
20
21
|
flow(
|
|
21
22
|
HttpClientRequest.bearerToken(options.token),
|
|
22
|
-
HttpClientRequest.setHeader("x-workspace", options.workspace)
|
|
23
|
-
)
|
|
23
|
+
HttpClientRequest.setHeader("x-workspace", options.workspace)
|
|
24
|
+
)
|
|
24
25
|
),
|
|
25
26
|
});
|
|
26
27
|
|
|
27
28
|
return {
|
|
28
29
|
// A method takes the action's decoded input, so a field with a decoding default is given
|
|
29
30
|
// here; only a call without any argument sends `{}` and gets the defaults.
|
|
30
|
-
openIssues: (limit = 20) =>
|
|
31
|
-
Effect.runPromise(
|
|
32
|
-
Effect.map(
|
|
31
|
+
openIssues: async (limit = 20) =>
|
|
32
|
+
await Effect.runPromise(
|
|
33
|
+
Effect.map(
|
|
34
|
+
api.listIssues({ status: "open", limit }),
|
|
35
|
+
({ issues }) => issues
|
|
36
|
+
)
|
|
33
37
|
),
|
|
34
38
|
|
|
35
39
|
// A declared error is a typed failure, handled by its tag: a missing issue is `undefined`.
|
|
36
|
-
issue: (id: string): Promise<Issue | undefined> =>
|
|
37
|
-
api.getIssue({ id }).pipe(
|
|
38
|
-
Effect.catchTag("IssueNotFound", () => Effect.
|
|
39
|
-
Effect.runPromise
|
|
40
|
+
issue: async (id: string): Promise<Issue | undefined> =>
|
|
41
|
+
await api.getIssue({ id }).pipe(
|
|
42
|
+
Effect.catchTag("IssueNotFound", () => Effect.undefined),
|
|
43
|
+
Effect.runPromise
|
|
40
44
|
),
|
|
41
45
|
|
|
42
46
|
// Left unhandled, a declared error rejects the promise as its decoded value, so a caller's
|
|
43
47
|
// `catch` may test `error instanceof RateLimited`.
|
|
44
|
-
file: (title: string, body: string) =>
|
|
48
|
+
file: async (title: string, body: string) =>
|
|
49
|
+
await Effect.runPromise(api.createIssue({ title, body })),
|
|
50
|
+
|
|
51
|
+
// A stream action's method returns a `Stream`; as an async iterable, `for await` reads each
|
|
52
|
+
// issue as the server sends it.
|
|
53
|
+
exportAll: (): AsyncIterable<Issue> =>
|
|
54
|
+
Stream.toAsyncIterable(api.exportIssues({})),
|
|
45
55
|
};
|
|
46
56
|
};
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { Buffer } from "node:buffer";
|
|
2
|
+
|
|
2
3
|
import { NodeRuntime } from "@effect/platform-node";
|
|
4
|
+
import * as ActionHttp from "@gjermundgaraba/effect-actions/ActionHttp";
|
|
3
5
|
import { Config, Console, Effect, flow } from "effect";
|
|
4
6
|
import { FetchHttpClient, HttpClient, HttpClientRequest } from "effect/http";
|
|
5
|
-
|
|
7
|
+
|
|
6
8
|
import { Http } from "./Api.js";
|
|
7
9
|
|
|
8
10
|
// Entrypoint: `node --import tsx examples/kitchen-sink/src/seed.ts`, with the server running.
|
|
@@ -13,12 +15,12 @@ import { Http } from "./Api.js";
|
|
|
13
15
|
// A one-pixel PNG stands in for a screenshot.
|
|
14
16
|
const screenshot = Buffer.from(
|
|
15
17
|
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=",
|
|
16
|
-
"base64"
|
|
18
|
+
"base64"
|
|
17
19
|
);
|
|
18
20
|
|
|
19
21
|
const seed = Effect.gen(function* () {
|
|
20
22
|
const baseUrl = yield* Config.String("TRACKER_URL").pipe(
|
|
21
|
-
Config.withDefault("http://127.0.0.1:3000")
|
|
23
|
+
Config.withDefault("http://127.0.0.1:3000")
|
|
22
24
|
);
|
|
23
25
|
|
|
24
26
|
const tracker = yield* ActionHttp.client(Http, {
|
|
@@ -27,8 +29,8 @@ const seed = Effect.gen(function* () {
|
|
|
27
29
|
transformClient: HttpClient.mapRequest(
|
|
28
30
|
flow(
|
|
29
31
|
HttpClientRequest.bearerToken("ada"),
|
|
30
|
-
HttpClientRequest.setHeader("x-workspace", "acme")
|
|
31
|
-
)
|
|
32
|
+
HttpClientRequest.setHeader("x-workspace", "acme")
|
|
33
|
+
)
|
|
32
34
|
),
|
|
33
35
|
});
|
|
34
36
|
|
|
@@ -48,7 +50,10 @@ const seed = Effect.gen(function* () {
|
|
|
48
50
|
|
|
49
51
|
const { issues } = yield* tracker.listIssues();
|
|
50
52
|
|
|
51
|
-
yield* Console.log(
|
|
53
|
+
yield* Console.log(
|
|
54
|
+
`Filed ${issue.id} with attachment ${attachment.id}. Issues:`,
|
|
55
|
+
issues
|
|
56
|
+
);
|
|
52
57
|
}).pipe(
|
|
53
58
|
// A declared error is a typed failure, the binding's and the verifier's included: each is
|
|
54
59
|
// handled by its tag. Anything else, such as an unreachable server, stays Effect's own.
|
|
@@ -57,8 +62,9 @@ const seed = Effect.gen(function* () {
|
|
|
57
62
|
Console.error(`Write limit reached: retry in ${retryAfterSeconds}s.`),
|
|
58
63
|
QuotaExceeded: ({ retryAfterSeconds }) =>
|
|
59
64
|
Console.error(`Request quota spent: retry in ${retryAfterSeconds}s.`),
|
|
60
|
-
IssuerUnavailable: () =>
|
|
61
|
-
|
|
65
|
+
IssuerUnavailable: () =>
|
|
66
|
+
Console.error("The sign-in service is down: try again later."),
|
|
67
|
+
})
|
|
62
68
|
);
|
|
63
69
|
|
|
64
|
-
seed.pipe(Effect.provide(FetchHttpClient.layer), NodeRuntime.runMain);
|
|
70
|
+
seed.pipe(Effect.provide(FetchHttpClient.layer), NodeRuntime.runMain());
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { createServer } from "node:http";
|
|
2
|
-
|
|
2
|
+
|
|
3
|
+
import { NodeDns, NodeHttpServer, NodeRuntime } from "@effect/platform-node";
|
|
3
4
|
import { ByteSize, Config, Effect, Layer } from "effect";
|
|
4
|
-
import { HttpRouter, HttpServerRequest } from "effect/http";
|
|
5
|
+
import { FetchHttpClient, HttpRouter, HttpServerRequest } from "effect/http";
|
|
5
6
|
import { RpcSerialization, RpcServer } from "effect/rpc";
|
|
7
|
+
|
|
6
8
|
import { memory } from "./memory.js";
|
|
7
9
|
import { adminRoutes, builders, publicRoutes, rpc } from "./routes.js";
|
|
8
10
|
|
|
@@ -14,15 +16,23 @@ import { adminRoutes, builders, publicRoutes, rpc } from "./routes.js";
|
|
|
14
16
|
// serves the same layer over Effect's HTTP protocol.
|
|
15
17
|
const socket = rpc.pipe(
|
|
16
18
|
Layer.provide(RpcServer.layerProtocolWebsocket({ path: "/rpc" })),
|
|
17
|
-
Layer.provide(RpcSerialization.layerJson)
|
|
19
|
+
Layer.provide(RpcSerialization.layerJson)
|
|
18
20
|
);
|
|
19
21
|
|
|
20
22
|
const server = Layer.unwrap(
|
|
21
23
|
Effect.gen(function* () {
|
|
22
24
|
// The loopback default serves this machine alone; HOST=0.0.0.0 lets others reach it.
|
|
23
|
-
const host = yield* Config.String("HOST").pipe(
|
|
24
|
-
|
|
25
|
-
|
|
25
|
+
const host = yield* Config.String("HOST").pipe(
|
|
26
|
+
Config.withDefault("127.0.0.1")
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
const publicPort = yield* Config.Port("PORT").pipe(
|
|
30
|
+
Config.withDefault(3000)
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
const adminPort = yield* Config.Port("ADMIN_PORT").pipe(
|
|
34
|
+
Config.withDefault(3001)
|
|
35
|
+
);
|
|
26
36
|
|
|
27
37
|
const limit = ByteSize.mebibytes(1);
|
|
28
38
|
|
|
@@ -42,8 +52,10 @@ const server = Layer.unwrap(
|
|
|
42
52
|
// it is a route of that router: the request policy, global to the router, refuses a
|
|
43
53
|
// foreign Origin's upgrade as it does a request, and the rpcs spend the quota the routes
|
|
44
54
|
// spend, built once.
|
|
45
|
-
HttpRouter.serve(Layer.merge(publicRoutes, socket)).pipe(
|
|
46
|
-
|
|
55
|
+
HttpRouter.serve(Layer.merge(publicRoutes, socket)).pipe(
|
|
56
|
+
Layer.provide(listen(publicPort))
|
|
57
|
+
),
|
|
58
|
+
HttpRouter.serve(adminRoutes).pipe(Layer.provide(listen(adminPort)))
|
|
47
59
|
).pipe(
|
|
48
60
|
// Request bodies have no size limit unless the host sets one. This covers every route
|
|
49
61
|
// that reads its body whole, on both ports, the MCP endpoint included.
|
|
@@ -53,9 +65,11 @@ const server = Layer.unwrap(
|
|
|
53
65
|
// suspends on one port is refused on the other.
|
|
54
66
|
Layer.provide(builders),
|
|
55
67
|
Layer.provide(memory),
|
|
68
|
+
// Webhook delivery POSTs with fetch, and resolves each callback with the system's resolver.
|
|
69
|
+
Layer.provide([FetchHttpClient.layer, NodeDns.layer])
|
|
56
70
|
);
|
|
57
|
-
})
|
|
71
|
+
})
|
|
58
72
|
);
|
|
59
73
|
|
|
60
74
|
// NodeRuntime handles signals: the scoped layers stop both listeners and release the services.
|
|
61
|
-
Layer.launch(server).pipe(NodeRuntime.runMain);
|
|
75
|
+
Layer.launch(server).pipe(NodeRuntime.runMain());
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { NodeRuntime, NodeStdio } from "@effect/platform-node";
|
|
2
|
-
import { Effect } from "effect";
|
|
3
2
|
import * as ActionCli from "@gjermundgaraba/effect-actions/ActionCli";
|
|
4
3
|
import * as ActionMcp from "@gjermundgaraba/effect-actions/ActionMcp";
|
|
4
|
+
import { Effect } from "effect";
|
|
5
|
+
|
|
5
6
|
import { Tools } from "./Actions.js";
|
|
6
7
|
import { attachments } from "./attachments/handlers.js";
|
|
7
8
|
import { identity } from "./identity/handlers.js";
|
|
@@ -29,8 +30,13 @@ ActionMcp.runStdio([status, identity, issues, attachments], {
|
|
|
29
30
|
// The host names the caller itself, around every call. DEMO ONLY: a real host reads who
|
|
30
31
|
// started it, from its own login or configuration.
|
|
31
32
|
Effect.provideService(CurrentMember, members.ada),
|
|
32
|
-
Effect.provide([
|
|
33
|
+
Effect.provide([
|
|
34
|
+
NodeStdio.layer,
|
|
35
|
+
stores,
|
|
36
|
+
limiter,
|
|
37
|
+
WorkspaceRegistry.layerMemory,
|
|
38
|
+
]),
|
|
33
39
|
// stdout carries the protocol alone, so logs and failures go to stderr.
|
|
34
40
|
ActionCli.logToStderr,
|
|
35
|
-
NodeRuntime.runMain
|
|
41
|
+
NodeRuntime.runMain()
|
|
36
42
|
);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { Schema } from "effect";
|
|
2
1
|
import * as Action from "@gjermundgaraba/effect-actions/Action";
|
|
2
|
+
import { Schema } from "effect";
|
|
3
3
|
|
|
4
4
|
export const Workspace = Schema.Struct({
|
|
5
5
|
slug: Schema.String,
|
|
@@ -9,11 +9,10 @@ export const Workspace = Schema.Struct({
|
|
|
9
9
|
|
|
10
10
|
export type Workspace = typeof Workspace.Type;
|
|
11
11
|
|
|
12
|
-
export class WorkspaceNotFound extends Schema.TaggedError<
|
|
13
|
-
|
|
14
|
-
{
|
|
15
|
-
|
|
16
|
-
) {}
|
|
12
|
+
export class WorkspaceNotFound extends Schema.TaggedError<
|
|
13
|
+
WorkspaceNotFound,
|
|
14
|
+
{ readonly brand: unique symbol }
|
|
15
|
+
>()("WorkspaceNotFound", { slug: Schema.String }, { httpApiStatus: 404 }) {}
|
|
17
16
|
|
|
18
17
|
// Public: `caller: Action.Anyone`. It answers before anyone has signed in, on every surface,
|
|
19
18
|
// so it tells nothing that depends on the caller.
|
|
@@ -22,7 +21,11 @@ export const GetStatus = Action.make("getStatus", {
|
|
|
22
21
|
"Report whether the tracker is up, since which start, and how many workspaces it serves.",
|
|
23
22
|
// `instance` names one start of the process: it changes on a restart, and with it the demo's
|
|
24
23
|
// memory stores are empty again.
|
|
25
|
-
success: {
|
|
24
|
+
success: {
|
|
25
|
+
service: Schema.String,
|
|
26
|
+
instance: Schema.String,
|
|
27
|
+
workspaces: Schema.Finite,
|
|
28
|
+
},
|
|
26
29
|
readOnly: true,
|
|
27
30
|
caller: Action.Anyone,
|
|
28
31
|
});
|
|
@@ -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 { WorkspaceRegistry } from "./registry.js";
|
|
4
5
|
import { GetStatus } from "./Workspaces.js";
|
|
5
6
|
|
|
@@ -19,5 +20,5 @@ export const status = Action.implement(
|
|
|
19
20
|
instance,
|
|
20
21
|
workspaces: workspaces.length,
|
|
21
22
|
}));
|
|
22
|
-
})
|
|
23
|
+
})
|
|
23
24
|
);
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { Context, Effect, Layer } from "effect";
|
|
2
|
-
|
|
2
|
+
|
|
3
|
+
import { WorkspaceNotFound } from "./Workspaces.js";
|
|
4
|
+
import type { Workspace } from "./Workspaces.js";
|
|
3
5
|
|
|
4
6
|
/**
|
|
5
7
|
* The tenants. Operators list and suspend them on the admin port; authentication reads the
|
|
@@ -8,11 +10,11 @@ import { type Workspace, WorkspaceNotFound } from "./Workspaces.js";
|
|
|
8
10
|
export class WorkspaceRegistry extends Context.Service<
|
|
9
11
|
WorkspaceRegistry,
|
|
10
12
|
{
|
|
11
|
-
readonly list: Effect.Effect<
|
|
13
|
+
readonly list: Effect.Effect<readonly Workspace[]>;
|
|
12
14
|
readonly get: (slug: string) => Effect.Effect<Workspace, WorkspaceNotFound>;
|
|
13
15
|
readonly setSuspended: (
|
|
14
16
|
slug: string,
|
|
15
|
-
suspended: boolean
|
|
17
|
+
suspended: boolean
|
|
16
18
|
) => Effect.Effect<Workspace, WorkspaceNotFound>;
|
|
17
19
|
}
|
|
18
20
|
>()("tracker/WorkspaceRegistry") {
|
|
@@ -26,21 +28,25 @@ export class WorkspaceRegistry extends Context.Service<
|
|
|
26
28
|
const get = Effect.fn("WorkspaceRegistry.get")(function* (slug: string) {
|
|
27
29
|
const workspace = workspaces.get(slug);
|
|
28
30
|
|
|
29
|
-
if (workspace === undefined)
|
|
31
|
+
if (workspace === undefined) {
|
|
32
|
+
return yield* new WorkspaceNotFound({ slug });
|
|
33
|
+
}
|
|
30
34
|
|
|
31
35
|
return workspace;
|
|
32
36
|
});
|
|
33
37
|
|
|
34
38
|
return WorkspaceRegistry.of({
|
|
35
|
-
list: Effect.sync(() =>
|
|
39
|
+
list: Effect.sync(() => [...workspaces.values()]),
|
|
36
40
|
get,
|
|
37
|
-
setSuspended: Effect.fn("WorkspaceRegistry.setSuspended")(
|
|
38
|
-
|
|
41
|
+
setSuspended: Effect.fn("WorkspaceRegistry.setSuspended")(
|
|
42
|
+
function* (slug, suspended) {
|
|
43
|
+
const workspace = { ...(yield* get(slug)), suspended };
|
|
39
44
|
|
|
40
|
-
|
|
45
|
+
workspaces.set(slug, workspace);
|
|
41
46
|
|
|
42
|
-
|
|
43
|
-
|
|
47
|
+
return workspace;
|
|
48
|
+
}
|
|
49
|
+
),
|
|
44
50
|
});
|
|
45
51
|
});
|
|
46
52
|
}
|
|
@@ -6,14 +6,21 @@ import { HttpRouter, HttpServerRequest } from "effect/http";
|
|
|
6
6
|
* an identity: the verifier reads it to decide which of an account's memberships the caller
|
|
7
7
|
* acts in. Handlers never read it; they read the workspace of the authenticated member.
|
|
8
8
|
*/
|
|
9
|
-
export class Tenant extends Context.Service<Tenant, Option.Option<string>>()(
|
|
9
|
+
export class Tenant extends Context.Service<Tenant, Option.Option<string>>()(
|
|
10
|
+
"tracker/Tenant"
|
|
11
|
+
) {}
|
|
10
12
|
|
|
11
13
|
// Native router middleware. It provides `Tenant` to whatever it is provided to, per request:
|
|
12
14
|
// to the verifier too, when it is provided after the authentication, in a `Layer.provide` of
|
|
13
15
|
// its own: in one array with the authentication it would leave `Tenant` owed, since an array's
|
|
14
16
|
// layers provide to the routes, not to one another.
|
|
15
|
-
export const resolveTenant = HttpRouter.middleware<{ provides: Tenant }>()(
|
|
16
|
-
|
|
17
|
-
Effect.
|
|
18
|
-
|
|
17
|
+
export const resolveTenant = HttpRouter.middleware<{ provides: Tenant }>()(
|
|
18
|
+
(route) =>
|
|
19
|
+
Effect.flatMap(HttpServerRequest.HttpServerRequest, (request) =>
|
|
20
|
+
Effect.provideService(
|
|
21
|
+
route,
|
|
22
|
+
Tenant,
|
|
23
|
+
Option.fromNullishOr(request.headers["x-workspace"])
|
|
24
|
+
)
|
|
25
|
+
)
|
|
19
26
|
);
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { describe, expect, it } from "@effect/vitest";
|
|
2
|
-
import { Effect, Layer } from "effect";
|
|
3
|
-
import { HttpClient, HttpClientRequest } from "effect/http";
|
|
4
2
|
import * as Action from "@gjermundgaraba/effect-actions/Action";
|
|
5
3
|
import * as ActionHttp from "@gjermundgaraba/effect-actions/ActionHttp";
|
|
6
4
|
import * as Testing from "@gjermundgaraba/effect-actions/Testing";
|
|
5
|
+
import { Effect, Layer } from "effect";
|
|
6
|
+
import { HttpClient, HttpClientRequest } from "effect/http";
|
|
7
|
+
|
|
7
8
|
import { Operator } from "../../src/admin/Admin.js";
|
|
8
9
|
import { Grants } from "../../src/admin/grants.js";
|
|
9
10
|
import { admin } from "../../src/admin/handlers.js";
|
|
@@ -16,7 +17,7 @@ const served = Testing.layer(adminRoutes).pipe(Layer.provide(memory));
|
|
|
16
17
|
|
|
17
18
|
const key = (operatorKey: string) => ({
|
|
18
19
|
transformClient: HttpClient.mapRequest(
|
|
19
|
-
HttpClientRequest.setHeader("x-operator-key", operatorKey)
|
|
20
|
+
HttpClientRequest.setHeader("x-operator-key", operatorKey)
|
|
20
21
|
),
|
|
21
22
|
});
|
|
22
23
|
|
|
@@ -28,39 +29,53 @@ describe("the operators", () => {
|
|
|
28
29
|
|
|
29
30
|
expect((yield* support.listWorkspaces()).workspaces).toHaveLength(2);
|
|
30
31
|
|
|
31
|
-
const refused = yield* Effect.flip(
|
|
32
|
+
const refused = yield* Effect.flip(
|
|
33
|
+
support.suspendWorkspace({ workspace: "acme" })
|
|
34
|
+
);
|
|
32
35
|
|
|
33
36
|
expect(refused).toBeInstanceOf(Action.Forbidden);
|
|
34
37
|
expect(refused).not.toHaveProperty("scopes");
|
|
35
38
|
expect(refused.message).toBe("Requires the workspaces:suspend grant.");
|
|
36
|
-
expect(yield* ops.suspendWorkspace({ workspace: "acme" })).toMatchObject({
|
|
37
|
-
|
|
39
|
+
expect(yield* ops.suspendWorkspace({ workspace: "acme" })).toMatchObject({
|
|
40
|
+
suspended: true,
|
|
41
|
+
});
|
|
42
|
+
}).pipe(Effect.provide(served))
|
|
38
43
|
);
|
|
39
44
|
|
|
40
45
|
it.effect("are not the members: a member's token is no credential here", () =>
|
|
41
46
|
Effect.gen(function* () {
|
|
42
47
|
const member = yield* ActionHttp.client(AdminHttp, {
|
|
43
|
-
transformClient: HttpClient.mapRequest(
|
|
48
|
+
transformClient: HttpClient.mapRequest(
|
|
49
|
+
HttpClientRequest.bearerToken("ada")
|
|
50
|
+
),
|
|
44
51
|
});
|
|
45
52
|
|
|
46
|
-
expect(yield* Effect.flip(member.listWorkspaces())).toBeInstanceOf(
|
|
47
|
-
|
|
53
|
+
expect(yield* Effect.flip(member.listWorkspaces())).toBeInstanceOf(
|
|
54
|
+
Action.Unauthenticated
|
|
55
|
+
);
|
|
56
|
+
}).pipe(Effect.provide(served))
|
|
48
57
|
);
|
|
49
58
|
|
|
50
|
-
it.effect(
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
59
|
+
it.effect(
|
|
60
|
+
"include the console's trusted operator, admitted by the same rule in process",
|
|
61
|
+
() =>
|
|
62
|
+
Effect.gen(function* () {
|
|
63
|
+
const client = yield* Action.client(admin);
|
|
64
|
+
const suspend = client.suspendWorkspace({ workspace: "globex" });
|
|
54
65
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
66
|
+
// No grants are stored for either: only the operator a host supplies is trusted.
|
|
67
|
+
expect(
|
|
68
|
+
yield* suspend.pipe(
|
|
69
|
+
Effect.provideService(Operator, { id: "console", trusted: true })
|
|
70
|
+
)
|
|
71
|
+
).toMatchObject({ suspended: true });
|
|
72
|
+
expect(
|
|
73
|
+
yield* Effect.flip(
|
|
74
|
+
suspend.pipe(
|
|
75
|
+
Effect.provideService(Operator, { id: "intern", trusted: false })
|
|
76
|
+
)
|
|
77
|
+
)
|
|
78
|
+
).toBeInstanceOf(Action.Forbidden);
|
|
79
|
+
}).pipe(Effect.provide([WorkspaceRegistry.layerMemory, Grants.layerDemo]))
|
|
65
80
|
);
|
|
66
81
|
});
|