1import { Endpoint } from "@distilled.cloud/aws";
2import * as kv from "@distilled.cloud/aws/kinesis-video";
3import * as Effect from "effect/Effect";
4import * as Binding from "../../Binding.ts";
5import { isBindingHost } from "../Lambda/Function.ts";
6import { discoverDataEndpoint, discoverSignalingEndpoint } from "./internal.ts";
7import type { SignalingChannel } from "./SignalingChannel.ts";
8import type { Stream } from "./Stream.ts";
9
10/**
11 * Shared scaffolding for AWS Kinesis Video HTTP bindings.
12 *
13 * NOT exported from `index.ts` — every near-identical `{Op}Http.ts` in this
14 * service is a thin `Layer.effect(Cap, make…HttpBinding({ … }))` over one of
15 * the builders below. Kinesis Video data-plane calls are two-step: discover
16 * the per-stream data endpoint (`GetDataEndpoint`) or per-channel signaling
17 * endpoint (`GetSignalingChannelEndpoint`), then issue the signed data-plane
18 * call against that endpoint. Everything except the operation, the endpoint
19 * `APIName`/protocol, and the IAM action is boilerplate.
20 */
21
22/**
23 * Build the impl Effect for a stream-scoped media/archived-media operation:
24 * the runtime callable resolves the per-stream data endpoint for `apiName`
25 * (cached), injects the bound {@link Stream}'s ARN as `StreamARN`, and the
26 * deploy-time half grants `kinesisvideo:GetDataEndpoint` + `actions` on the
27 * stream's ARN.
28 */
29export const makeStreamMediaHttpBinding = <
30 I extends { StreamARN?: string; StreamName?: string },
31 A,
32 E,
33 R,
34>(options: {
35 /** Fully-qualified binding tag, e.g. `AWS.KinesisVideo.GetClip`. */
36 tag: string;
37 /** The `GetDataEndpoint` API name the operation is served under. */
38 apiName: kv.APIName;
39 /** IAM actions granted on the stream ARN (GetDataEndpoint is implied). */
40 actions: readonly string[];
41 /** The distilled operation; `StreamARN` is injected from the stream. */
42 operation: Effect.Effect<(input: I) => Effect.Effect<A, E>, never, R>;
43}) =>
44 Effect.gen(function* () {
45 // Yield-first captures the operations' services (Credentials/Region/
46 // HttpClient) at layer init so the runtime callable is requirement-free.
47 const getDataEndpoint = yield* kv.getDataEndpoint;
48 const op = yield* options.operation;
49
50 return Effect.fn(function* (stream: Stream) {
51 const StreamArn = yield* stream.streamArn;
52 if (!globalThis.__ALCHEMY_RUNTIME__) {
53 const host = yield* Binding.Host;
54 if (isBindingHost(host)) {
55 yield* host.bind`Allow(${host}, ${options.tag}(${stream}))`({
56 policyStatements: [
57 {
58 Effect: "Allow",
59 Action: [
60 // the data plane requires per-stream endpoint discovery
61 "kinesisvideo:GetDataEndpoint",
62 ...options.actions,
63 ],
64 Resource: [stream.streamArn],
65 },
66 ],
67 });
68 }
69 }
70 return Effect.fn(`${options.tag}(${stream.LogicalId})`)(function* (
71 request?: Omit<I, "StreamARN" | "StreamName">,
72 ) {
73 const streamArn = yield* StreamArn;
74 const endpoint = yield* discoverDataEndpoint(
75 streamArn,
76 options.apiName,
77 getDataEndpoint,
78 );
79 return yield* op({ ...request, StreamARN: streamArn } as I).pipe(
80 Effect.provideService(Endpoint.Endpoint, Effect.succeed(endpoint)),
81 );
82 });
83 });
84 });
85
86/**
87 * Build the impl Effect for a channel-scoped signaling/WebRTC-storage
88 * operation: the runtime callable resolves the per-channel endpoint for
89 * `protocol` + `role` (cached), injects the bound {@link SignalingChannel}'s
90 * ARN under `key`, and the deploy-time half grants
91 * `kinesisvideo:GetSignalingChannelEndpoint` + `actions` on the channel ARN.
92 */
93export const makeChannelSignalingHttpBinding = <
94 K extends "ChannelARN" | "channelArn",
95 I extends { [P in K]?: string },
96 A,
97 E,
98 R,
99>(options: {
100 /** Fully-qualified binding tag, e.g. `AWS.KinesisVideo.JoinStorageSession`. */
101 tag: string;
102 /** The signaling endpoint protocol the operation is served under. */
103 protocol: kv.ChannelProtocol;
104 /** The peer role the endpoint is resolved for. */
105 role: kv.ChannelRole;
106 /**
107 * IAM actions granted on the channel ARN (GetSignalingChannelEndpoint is
108 * implied).
109 */
110 actions: readonly string[];
111 /** The request field the channel ARN is injected under. */
112 key: K;
113 /** The distilled operation; the channel ARN is injected under `key`. */
114 operation: Effect.Effect<(input: I) => Effect.Effect<A, E>, never, R>;
115}) =>
116 Effect.gen(function* () {
117 // Yield-first captures the operations' services (Credentials/Region/
118 // HttpClient) at layer init so the runtime callable is requirement-free.
119 const getSignalingChannelEndpoint = yield* kv.getSignalingChannelEndpoint;
120 const op = yield* options.operation;
121
122 return Effect.fn(function* (channel: SignalingChannel) {
123 const ChannelArn = yield* channel.channelArn;
124 if (!globalThis.__ALCHEMY_RUNTIME__) {
125 const host = yield* Binding.Host;
126 if (isBindingHost(host)) {
127 yield* host.bind`Allow(${host}, ${options.tag}(${channel}))`({
128 policyStatements: [
129 {
130 Effect: "Allow",
131 Action: [
132 // the data plane requires per-channel endpoint discovery
133 "kinesisvideo:GetSignalingChannelEndpoint",
134 ...options.actions,
135 ],
136 Resource: [channel.channelArn],
137 },
138 ],
139 });
140 }
141 }
142 return Effect.fn(`${options.tag}(${channel.LogicalId})`)(function* (
143 request?: Omit<I, K>,
144 ) {
145 const channelArn = yield* ChannelArn;
146 const endpoint = yield* discoverSignalingEndpoint(
147 channelArn,
148 options.protocol,
149 options.role,
150 getSignalingChannelEndpoint,
151 );
152 return yield* op({
153 ...request,
154 [options.key]: channelArn,
155 } as I).pipe(
156 Effect.provideService(Endpoint.Endpoint, Effect.succeed(endpoint)),
157 );
158 });
159 });
160 });