1import * as Lambda from "@/AWS/Lambda";
2import * as AOSS from "@/AWS/OpenSearchServerless";
3import * as Effect from "effect/Effect";
4import * as Layer from "effect/Layer";
5import { HttpServerRequest } from "effect/http/HttpServerRequest";
6import * as HttpServerResponse from "effect/http/HttpServerResponse";
7import path from "pathe";
8
9const main = path.resolve(import.meta.dirname, "bindings-handler.ts");
10
11export class AossBindingsFunction extends Lambda.Function<Lambda.Function>()(
12 "AossBindingsFunction",
13) {}
14
15export default AossBindingsFunction.make(
16 {
17 main,
18 functionUrl: true,
19 },
20 Effect.gen(function* () {
21 // Account-level bindings only — free and instant (no collection needed).
22 // The collection-scoped index bindings are exercised by the gated
23 // IndexBindings.test.ts fixture.
24 const getAccountSettings = yield* AOSS.GetAccountSettings();
25 const updateAccountSettings = yield* AOSS.UpdateAccountSettings();
26 const getPoliciesStats = yield* AOSS.GetPoliciesStats();
27 const batchGetEffectiveLifecyclePolicy =
28 yield* AOSS.BatchGetEffectiveLifecyclePolicy();
29
30 const bound = {
31 getAccountSettings,
32 updateAccountSettings,
33 getPoliciesStats,
34 batchGetEffectiveLifecyclePolicy,
35 };
36
37 return {
38 fetch: Effect.gen(function* () {
39 const request = yield* HttpServerRequest;
40 const url = new URL(request.originalUrl);
41 const pathname = url.pathname;
42
43 if (request.method === "GET" && pathname === "/bindings") {
44 return yield* HttpServerResponse.json({
45 bound: Object.keys(bound),
46 });
47 }
48
49 if (request.method === "GET" && pathname === "/account-settings") {
50 const response = yield* getAccountSettings();
51 return yield* HttpServerResponse.json({
52 capacityLimits:
53 response.accountSettingsDetail?.capacityLimits ?? null,
54 });
55 }
56
57 if (
58 request.method === "POST" &&
59 pathname === "/account-settings/noop"
60 ) {
61 // Read-modify-write with the observed values (falling back to the
62 // service defaults) — proves the UpdateAccountSettings grant
63 // without changing the account's effective configuration.
64 const current = yield* getAccountSettings().pipe(
65 Effect.map((r) => r.accountSettingsDetail?.capacityLimits),
66 );
67 const response = yield* updateAccountSettings({
68 capacityLimits: {
69 maxIndexingCapacityInOCU: current?.maxIndexingCapacityInOCU ?? 10,
70 maxSearchCapacityInOCU: current?.maxSearchCapacityInOCU ?? 10,
71 },
72 });
73 return yield* HttpServerResponse.json({
74 capacityLimits:
75 response.accountSettingsDetail?.capacityLimits ?? null,
76 });
77 }
78
79 if (request.method === "GET" && pathname === "/policies-stats") {
80 const response = yield* getPoliciesStats();
81 return yield* HttpServerResponse.json({
82 total: response.TotalPolicyCount ?? 0,
83 securityPolicies:
84 response.SecurityPolicyStats?.EncryptionPolicyCount ?? 0,
85 });
86 }
87
88 if (request.method === "POST" && pathname === "/effective-lifecycle") {
89 // A nonexistent index resolves through the error-detail channel —
90 // the call round-tripping at all proves the grant end-to-end (an
91 // IAM gap would surface AccessDeniedException, a 500).
92 const response = yield* batchGetEffectiveLifecyclePolicy({
93 resourceIdentifiers: [
94 {
95 type: "retention",
96 resource: "index/alchemy-nonexistent-probe/probe",
97 },
98 ],
99 });
100 return yield* HttpServerResponse.json({
101 details: (response.effectiveLifecyclePolicyDetails ?? []).length,
102 errors: (response.effectiveLifecyclePolicyErrorDetails ?? [])
103 .length,
104 });
105 }
106
107 return yield* HttpServerResponse.json(
108 { error: "Not found", method: request.method, pathname },
109 { status: 404 },
110 );
111 }).pipe(Effect.orDie),
112 };
113 }).pipe(
114 Effect.provide(
115 Layer.mergeAll(
116 AOSS.GetAccountSettingsHttp,
117 AOSS.UpdateAccountSettingsHttp,
118 AOSS.GetPoliciesStatsHttp,
119 AOSS.BatchGetEffectiveLifecyclePolicyHttp,
120 ),
121 ),
122 ),
123);