1import * as Effect from "effect/Effect";
2import * as Binding from "../../Binding.ts";
3import type { PolicyStatement } from "../IAM/Policy.ts";
4import { isBindingHost } from "../Lambda/Function.ts";
5import type { Dataset } from "./Dataset.ts";
6import type { EventTracker } from "./EventTracker.ts";
7
8/**
9 * Shared HTTP scaffolding for the Amazon Personalize runtime bindings.
10 *
11 * NOT exported from `index.ts` — every `{Op}Http.ts` in this service is a
12 * thin `Layer.effect(Cap, make…HttpBinding({ … }))` over one of the three
13 * builders below. Everything except the distilled operation, the identifier
14 * injection, and the IAM action list is boilerplate:
15 *
16 * - dataset-scoped operations (`PutItems`, `PutUsers`, `PutActions`) inject
17 * the bound {@link Dataset}'s ARN as `datasetArn` and grant on it,
18 * - event-tracker-scoped operations (`PutEvents`, `PutActionInteractions`)
19 * inject the bound {@link EventTracker}'s `trackingId`; per the service
20 * authorization reference these actions support no resource types, so the
21 * grant is on `Resource: ["*"]`,
22 * - account-level operations (the `personalize-runtime` recommenders and the
23 * MLOps retraining loop) address campaigns / solutions / import jobs whose
24 * ARNs are created at runtime, so they grant on `Resource: ["*"]`.
25 */
26
27/**
28 * Build the impl Effect for a dataset-scoped Personalize operation: the
29 * runtime callable injects the bound {@link Dataset}'s ARN as `datasetArn`
30 * and the deploy-time half grants `actions` on the dataset ARN.
31 */
32export const makePersonalizeDatasetHttpBinding = <
33 I extends { datasetArn: string },
34 A,
35 E,
36 R,
37>(options: {
38 /** Fully-qualified binding tag, e.g. `AWS.Personalize.PutItems`. */
39 tag: string;
40 /** The distilled operation; `datasetArn` is injected from the dataset. */
41 operation: Effect.Effect<(input: I) => Effect.Effect<A, E>, never, R>;
42 /** IAM actions granted on the dataset ARN. */
43 actions: readonly string[];
44}) =>
45 Effect.gen(function* () {
46 const op = yield* options.operation;
47
48 return Effect.fn(function* (dataset: Dataset) {
49 const datasetArn = yield* dataset.datasetArn;
50 if (!globalThis.__ALCHEMY_RUNTIME__) {
51 const host = yield* Binding.Host;
52 if (isBindingHost(host)) {
53 yield* host.bind`Allow(${host}, ${options.tag}(${dataset}))`({
54 policyStatements: [
55 {
56 Effect: "Allow",
57 Action: [...options.actions],
58 Resource: [dataset.datasetArn],
59 },
60 ],
61 });
62 }
63 }
64 return Effect.fn(`${options.tag}(${dataset.LogicalId})`)(function* (
65 request: Omit<I, "datasetArn">,
66 ) {
67 return yield* op({
68 ...request,
69 datasetArn: yield* datasetArn,
70 } as I);
71 });
72 });
73 });
74
75/**
76 * Build the impl Effect for an event-tracker-scoped Personalize operation:
77 * the runtime callable injects the bound {@link EventTracker}'s tracking ID
78 * as `trackingId`. The deploy-time half grants `actions` on `Resource: ["*"]`
79 * — `personalize:PutEvents` / `personalize:PutActionInteractions` support no
80 * resource types (a tracker-ARN-scoped grant answers AccessDeniedException).
81 */
82export const makePersonalizeEventTrackerHttpBinding = <
83 I extends { trackingId: string },
84 A,
85 E,
86 R,
87>(options: {
88 /** Fully-qualified binding tag, e.g. `AWS.Personalize.PutEvents`. */
89 tag: string;
90 /** The distilled operation; `trackingId` is injected from the tracker. */
91 operation: Effect.Effect<(input: I) => Effect.Effect<A, E>, never, R>;
92 /** IAM actions granted on the event tracker ARN. */
93 actions: readonly string[];
94}) =>
95 Effect.gen(function* () {
96 const op = yield* options.operation;
97
98 return Effect.fn(function* (tracker: EventTracker) {
99 const trackingId = yield* tracker.trackingId;
100 if (!globalThis.__ALCHEMY_RUNTIME__) {
101 const host = yield* Binding.Host;
102 if (isBindingHost(host)) {
103 yield* host.bind`Allow(${host}, ${options.tag}(${tracker}))`({
104 policyStatements: [
105 {
106 Effect: "Allow",
107 Action: [...options.actions],
108 // The event-ingestion actions support no resource types.
109 Resource: ["*"],
110 },
111 ],
112 });
113 }
114 }
115 return Effect.fn(`${options.tag}(${tracker.LogicalId})`)(function* (
116 request: Omit<I, "trackingId">,
117 ) {
118 return yield* op({
119 ...request,
120 trackingId: yield* trackingId,
121 } as I);
122 });
123 });
124 });
125
126/**
127 * Build the impl Effect for an account-level Personalize operation (the
128 * `personalize-runtime` recommendation calls and the MLOps retraining loop):
129 * campaigns, solutions, and import jobs are addressed by ARNs the caller
130 * supplies at runtime, so the deploy-time half grants `actions` on
131 * `Resource: ["*"]`.
132 */
133export const makePersonalizeAccountHttpBinding = <I, A, E, R>(options: {
134 /** Fully-qualified binding tag, e.g. `AWS.Personalize.GetRecommendations`. */
135 tag: string;
136 /** The distilled operation implementing the capability. */
137 operation: Effect.Effect<(input: I) => Effect.Effect<A, E>, never, R>;
138 /**
139 * IAM actions granted on `Resource: ["*"]` (the target ARNs are chosen by
140 * the caller at runtime).
141 */
142 actions: readonly string[];
143 /**
144 * Grant `iam:PassRole` (conditioned to `personalize.amazonaws.com`) so the
145 * function can hand Personalize the role it assumes to read S3 training
146 * data. Set on `CreateDatasetImportJob`, which accepts a `roleArn`.
147 */
148 passRole?: boolean;
149}) =>
150 Effect.gen(function* () {
151 const op = yield* options.operation;
152
153 return Effect.fn(function* () {
154 if (!globalThis.__ALCHEMY_RUNTIME__) {
155 const host = yield* Binding.Host;
156 if (isBindingHost(host)) {
157 const policyStatements: PolicyStatement[] = [
158 {
159 Effect: "Allow",
160 Action: [...options.actions],
161 // Campaign/solution/import-job ARNs are chosen at runtime.
162 Resource: ["*"],
163 },
164 ];
165 if (options.passRole) {
166 policyStatements.push({
167 Effect: "Allow",
168 Action: ["iam:PassRole"],
169 Resource: ["*"],
170 Condition: {
171 StringEquals: {
172 "iam:PassedToService": "personalize.amazonaws.com",
173 },
174 },
175 });
176 }
177 yield* host.bind`Allow(${host}, ${options.tag}())`({
178 policyStatements,
179 });
180 }
181 }
182 return Effect.fn(options.tag)(function* (request: I) {
183 return yield* op(request);
184 });
185 });
186 });