sst-http 1.3.0 → 1.3.3-beta.2
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 +87 -28
- package/dist/bus/index.cjs +185 -0
- package/dist/bus/index.d.cts +7 -0
- package/dist/bus/index.d.ts +7 -0
- package/dist/bus/index.js +9 -0
- package/dist/{chunk-5MOJ3SW6.js → chunk-5OUNKYO5.js} +1 -1
- package/dist/chunk-LLR3DQ65.js +140 -0
- package/dist/chunk-SENBWWVV.js +499 -0
- package/dist/chunk-YMGEGOSD.js +117 -0
- package/dist/cli.cjs +49 -13
- package/dist/cli.js +49 -13
- package/dist/handler-DaM4Racx.d.cts +4 -0
- package/dist/handler-DaM4Racx.d.ts +4 -0
- package/dist/http/index.cjs +644 -0
- package/dist/http/index.d.cts +51 -0
- package/dist/http/index.d.ts +51 -0
- package/dist/http/index.js +53 -0
- package/dist/index.cjs +205 -14
- package/dist/index.d.cts +6 -45
- package/dist/index.d.ts +6 -45
- package/dist/index.js +33 -516
- package/dist/infra.cjs +273 -126
- package/dist/infra.d.cts +35 -18
- package/dist/infra.d.ts +35 -18
- package/dist/infra.js +268 -121
- package/dist/{types-BF3w-wTx.d.cts → types-w1A7o_rd.d.cts} +5 -1
- package/dist/{types-BF3w-wTx.d.ts → types-w1A7o_rd.d.ts} +5 -1
- package/package.json +16 -5
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# sst-http
|
|
2
2
|
|
|
3
|
-
Decorator-based HTTP routing for
|
|
3
|
+
Decorator-based HTTP routing and EventBridge helpers for SST v3. Keep a single Lambda for your API, scan routes into a manifest, and wire everything into API Gateway. The bus helpers let you subscribe handlers with `@On()` and publish events from anywhere.
|
|
4
4
|
|
|
5
5
|
## Install
|
|
6
6
|
|
|
@@ -8,13 +8,44 @@ Decorator-based HTTP routing for [SST v3](https://sst.dev) that keeps your app o
|
|
|
8
8
|
pnpm add sst-http
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
##
|
|
11
|
+
## Import style
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
You can keep using the root entrypoint, or import by domain:
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
import { createHandler, Get, Post } from "sst-http/http";
|
|
17
|
+
import { On, publish } from "sst-http/bus";
|
|
18
|
+
|
|
19
|
+
// Root entrypoint also works
|
|
20
|
+
import { createHandler as createHttpHandler } from "sst-http";
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Examples
|
|
24
|
+
|
|
25
|
+
The repo ships three SST v3 examples under `examples/`:
|
|
26
|
+
|
|
27
|
+
- `examples/http`: four HTTP routes (path param, query string, JSON body, plus a ping route).
|
|
28
|
+
- `examples/bus-publisher`: exposes `GET /` and publishes a `demo.created` event to the bus.
|
|
29
|
+
- `examples/bus-receiver`: a single `@On("demo.created")` handler that receives the event.
|
|
30
|
+
|
|
31
|
+
To run one of them:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
cd examples/http
|
|
35
|
+
pnpm install
|
|
36
|
+
pnpm run routes:scan
|
|
37
|
+
pnpm run dev
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
For the bus pair, you can deploy either example in any order since events target the default bus.
|
|
41
|
+
|
|
42
|
+
## HTTP routes
|
|
43
|
+
|
|
44
|
+
Create routed functions anywhere in your project—no controllers required.
|
|
14
45
|
|
|
15
46
|
```ts
|
|
16
47
|
// src/routes/users.ts
|
|
17
|
-
import { Get, Post, FirebaseAuth, json } from "sst-http";
|
|
48
|
+
import { Get, Post, FirebaseAuth, json } from "sst-http/http";
|
|
18
49
|
|
|
19
50
|
export class UserRoutes {
|
|
20
51
|
@Get("/users/{id}")
|
|
@@ -34,18 +65,18 @@ export const getUser = UserRoutes.getUser;
|
|
|
34
65
|
export const createUser = UserRoutes.createUser;
|
|
35
66
|
```
|
|
36
67
|
|
|
37
|
-
Enable name-based inference
|
|
68
|
+
Enable name-based inference if you prefer omitting explicit paths:
|
|
38
69
|
|
|
39
70
|
```ts
|
|
40
|
-
import { configureRoutes } from "sst-http";
|
|
71
|
+
import { configureRoutes } from "sst-http/http";
|
|
41
72
|
|
|
42
73
|
configureRoutes({ inferPathFromName: true });
|
|
43
74
|
```
|
|
44
75
|
|
|
45
|
-
Parameter decorators
|
|
76
|
+
### Parameter decorators
|
|
46
77
|
|
|
47
78
|
```ts
|
|
48
|
-
import { Body, Post } from "sst-http";
|
|
79
|
+
import { Body, Post } from "sst-http/http";
|
|
49
80
|
import { z } from "zod/v4";
|
|
50
81
|
|
|
51
82
|
const CreateTodo = z.object({ title: z.string().min(1) });
|
|
@@ -53,7 +84,6 @@ const CreateTodo = z.object({ title: z.string().min(1) });
|
|
|
53
84
|
export class TodoRoutes {
|
|
54
85
|
@Post("/todos")
|
|
55
86
|
static createTodo(@Body(CreateTodo) payload: z.infer<typeof CreateTodo>) {
|
|
56
|
-
// payload is validated JSON
|
|
57
87
|
return { statusCode: 201, body: JSON.stringify(payload) };
|
|
58
88
|
}
|
|
59
89
|
}
|
|
@@ -62,16 +92,16 @@ export const createTodo = TodoRoutes.createTodo;
|
|
|
62
92
|
```
|
|
63
93
|
|
|
64
94
|
> **Note**
|
|
65
|
-
> API Gateway route keys expect `{param}` placeholders. The router accepts
|
|
95
|
+
> API Gateway route keys expect `{param}` placeholders. The router accepts `{param}` or `:param` at runtime, but manifests and infra wiring emit `{param}` so your deployed routes line up with AWS.
|
|
66
96
|
|
|
67
|
-
## Single Lambda
|
|
97
|
+
## Single Lambda entry
|
|
68
98
|
|
|
69
|
-
All decorated modules register themselves on import. The
|
|
99
|
+
All decorated modules register themselves on import. The handler handles routing for both REST and HTTP API Gateway payloads.
|
|
70
100
|
|
|
71
101
|
```ts
|
|
72
102
|
// src/server.ts
|
|
73
103
|
import "reflect-metadata";
|
|
74
|
-
import { createHandler } from "sst-http";
|
|
104
|
+
import { createHandler } from "sst-http/http";
|
|
75
105
|
|
|
76
106
|
import "./routes/users";
|
|
77
107
|
import "./routes/health";
|
|
@@ -79,25 +109,56 @@ import "./routes/health";
|
|
|
79
109
|
export const handler = createHandler();
|
|
80
110
|
```
|
|
81
111
|
|
|
82
|
-
Helpers such as `json()`, `text()`, and `noContent()` are available for concise responses
|
|
112
|
+
Helpers such as `json()`, `text()`, and `noContent()` are available for concise responses. Throw `HttpError` to return a normalized JSON error payload.
|
|
113
|
+
|
|
114
|
+
## Event bus
|
|
83
115
|
|
|
84
|
-
|
|
116
|
+
Use `@On()` to register EventBridge handlers and `publish()` to emit events.
|
|
85
117
|
|
|
86
|
-
|
|
118
|
+
```ts
|
|
119
|
+
// src/events/user-events.ts
|
|
120
|
+
import { On } from "sst-http/bus";
|
|
121
|
+
|
|
122
|
+
export class UserEvents {
|
|
123
|
+
@On("user.created")
|
|
124
|
+
static async onUserCreated(detail: { id: string }) {
|
|
125
|
+
console.log("New user", detail.id);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export const onUserCreated = UserEvents.onUserCreated;
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
```ts
|
|
133
|
+
import { publish } from "sst-http/bus";
|
|
134
|
+
|
|
135
|
+
await publish("user.created", { id: "123" });
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
`publish()` signs requests with the current AWS credentials and requires `AWS_REGION`/`AWS_DEFAULT_REGION` in the environment.
|
|
139
|
+
|
|
140
|
+
## Scan & manifest
|
|
141
|
+
|
|
142
|
+
Use the CLI to inspect your source tree and materialize a manifest for infra wiring.
|
|
87
143
|
|
|
88
144
|
```bash
|
|
89
|
-
pnpm sst-http scan --glob "src
|
|
145
|
+
pnpm sst-http scan --glob "src/**/*.ts" --out routes.manifest.json
|
|
90
146
|
```
|
|
91
147
|
|
|
92
|
-
Pass `--infer-name` to map routes without explicit paths using the kebab-case function name (matching
|
|
148
|
+
Pass `--infer-name` to map routes without explicit paths using the kebab-case function name (matching `configureRoutes({ inferPathFromName: true })`). When `@On()` is used, events are emitted into the same manifest under `events`.
|
|
149
|
+
|
|
150
|
+
## Firebase JWT authorizer
|
|
93
151
|
|
|
94
|
-
|
|
152
|
+
Mark a route with `@FirebaseAuth()` and the manifest records it as protected. The wiring utilities configure an API Gateway JWT authorizer using:
|
|
95
153
|
|
|
96
|
-
|
|
154
|
+
- Issuer: `https://securetoken.google.com/<projectId>`
|
|
155
|
+
- Audience: `<projectId>`
|
|
97
156
|
|
|
98
|
-
|
|
157
|
+
Optional roles and optional-auth flags flow into the adapter so you can fine-tune scopes.
|
|
99
158
|
|
|
100
|
-
|
|
159
|
+
## Wire API Gateway + EventBridge
|
|
160
|
+
|
|
161
|
+
`sst-http/infra` ships with a manifest-driven wiring utility plus adapters for HTTP API (ApiGatewayV2) and REST API (ApiGateway). The example below wires all routes to a single Lambda function inside `sst.config.ts` and connects event subscriptions from the same manifest.
|
|
101
162
|
|
|
102
163
|
```ts
|
|
103
164
|
// sst.config.ts
|
|
@@ -110,12 +171,12 @@ export default $config({
|
|
|
110
171
|
loadRoutesManifest,
|
|
111
172
|
wireApiFromManifest,
|
|
112
173
|
httpApiAdapter,
|
|
174
|
+
createBus,
|
|
113
175
|
} = await import("sst-http/infra");
|
|
114
176
|
|
|
115
177
|
const manifest = loadRoutesManifest("routes.manifest.json");
|
|
116
178
|
const { api, registerRoute, ensureJwtAuthorizer } = httpApiAdapter({ apiName: "Api" });
|
|
117
179
|
|
|
118
|
-
// Single Lambda for all routes
|
|
119
180
|
const handler = new sst.aws.Function("Handler", {
|
|
120
181
|
handler: "src/server.handler",
|
|
121
182
|
runtime: "nodejs20.x",
|
|
@@ -123,11 +184,14 @@ export default $config({
|
|
|
123
184
|
memory: "512 MB",
|
|
124
185
|
});
|
|
125
186
|
|
|
187
|
+
const bus = createBus();
|
|
188
|
+
|
|
126
189
|
wireApiFromManifest(manifest, {
|
|
127
190
|
handler,
|
|
128
191
|
firebaseProjectId: process.env.FIREBASE_PROJECT_ID!,
|
|
129
192
|
registerRoute,
|
|
130
193
|
ensureJwtAuthorizer,
|
|
194
|
+
buses: [bus],
|
|
131
195
|
});
|
|
132
196
|
|
|
133
197
|
return { ApiUrl: api.url };
|
|
@@ -137,9 +201,6 @@ export default $config({
|
|
|
137
201
|
|
|
138
202
|
Swap in `restApiAdapter` if you prefer API Gateway REST APIs—the wiring contract is identical.
|
|
139
203
|
|
|
140
|
-
> Tip
|
|
141
|
-
> Set `FIREBASE_PROJECT_ID` in your environment when using `@FirebaseAuth()` so the JWT authorizer is configured correctly.
|
|
142
|
-
|
|
143
204
|
## Publishing
|
|
144
205
|
|
|
145
206
|
```bash
|
|
@@ -148,8 +209,6 @@ npm version patch
|
|
|
148
209
|
pnpm run release
|
|
149
210
|
```
|
|
150
211
|
|
|
151
|
-
The release script builds the ESM/CJS bundles via `tsup` before publishing.
|
|
152
|
-
|
|
153
212
|
## License
|
|
154
213
|
|
|
155
214
|
MIT
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/bus/index.ts
|
|
21
|
+
var bus_exports = {};
|
|
22
|
+
__export(bus_exports, {
|
|
23
|
+
On: () => On,
|
|
24
|
+
publish: () => publish
|
|
25
|
+
});
|
|
26
|
+
module.exports = __toCommonJS(bus_exports);
|
|
27
|
+
|
|
28
|
+
// src/core/handler.ts
|
|
29
|
+
function resolveHandler(target, propertyKey, descriptor) {
|
|
30
|
+
if (descriptor?.value && typeof descriptor.value === "function") {
|
|
31
|
+
return descriptor.value;
|
|
32
|
+
}
|
|
33
|
+
if (typeof target === "function" && propertyKey === void 0) {
|
|
34
|
+
return target;
|
|
35
|
+
}
|
|
36
|
+
if (target && propertyKey && typeof target[propertyKey] === "function") {
|
|
37
|
+
return target[propertyKey];
|
|
38
|
+
}
|
|
39
|
+
throw new Error("Unable to determine decorated function. Ensure decorators are applied to functions.");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// src/core/registry.ts
|
|
43
|
+
var eventMeta = /* @__PURE__ */ new Map();
|
|
44
|
+
function registerEvent(target, event) {
|
|
45
|
+
const handler = target;
|
|
46
|
+
const list = eventMeta.get(handler) ?? [];
|
|
47
|
+
list.push({ event });
|
|
48
|
+
eventMeta.set(handler, list);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// src/bus/decorators.ts
|
|
52
|
+
function On(event) {
|
|
53
|
+
return (target, propertyKey, descriptor) => {
|
|
54
|
+
if (!event) {
|
|
55
|
+
throw new Error("@On() requires an event name.");
|
|
56
|
+
}
|
|
57
|
+
const handler = resolveHandler(target, propertyKey, descriptor);
|
|
58
|
+
registerEvent(handler, event);
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// src/bus/event-bus.ts
|
|
63
|
+
var import_node_crypto = require("crypto");
|
|
64
|
+
var AWS_TARGET = "AWSEvents.PutEvents";
|
|
65
|
+
var AWS_SERVICE = "events";
|
|
66
|
+
var DEFAULT_SOURCE = "sst-http";
|
|
67
|
+
async function publish(event, message) {
|
|
68
|
+
if (!event) {
|
|
69
|
+
throw new Error("publish() requires an event name.");
|
|
70
|
+
}
|
|
71
|
+
const payload = {
|
|
72
|
+
Entries: [
|
|
73
|
+
{
|
|
74
|
+
EventBusName: "default",
|
|
75
|
+
Source: DEFAULT_SOURCE,
|
|
76
|
+
DetailType: event,
|
|
77
|
+
Detail: JSON.stringify(message ?? null)
|
|
78
|
+
}
|
|
79
|
+
]
|
|
80
|
+
};
|
|
81
|
+
console.log(payload);
|
|
82
|
+
await putEventsViaFetch(payload);
|
|
83
|
+
}
|
|
84
|
+
async function putEventsViaFetch(payload) {
|
|
85
|
+
const region = resolveRegion();
|
|
86
|
+
const creds = resolveCredentials();
|
|
87
|
+
const host = `events.${region}.amazonaws.com`;
|
|
88
|
+
const url = `https://${host}/`;
|
|
89
|
+
const body = JSON.stringify(payload);
|
|
90
|
+
const amzDate = toAmzDate(/* @__PURE__ */ new Date());
|
|
91
|
+
const dateStamp = amzDate.slice(0, 8);
|
|
92
|
+
const headers = {
|
|
93
|
+
"content-type": "application/x-amz-json-1.1",
|
|
94
|
+
"x-amz-date": amzDate,
|
|
95
|
+
"x-amz-target": AWS_TARGET,
|
|
96
|
+
host
|
|
97
|
+
};
|
|
98
|
+
if (creds.sessionToken) {
|
|
99
|
+
headers["x-amz-security-token"] = creds.sessionToken;
|
|
100
|
+
}
|
|
101
|
+
const signedHeaders = getSignedHeaders(headers);
|
|
102
|
+
const canonicalRequest = [
|
|
103
|
+
"POST",
|
|
104
|
+
"/",
|
|
105
|
+
"",
|
|
106
|
+
canonicalizeHeaders(headers),
|
|
107
|
+
signedHeaders,
|
|
108
|
+
sha256(body)
|
|
109
|
+
].join("\n");
|
|
110
|
+
const scope = `${dateStamp}/${region}/${AWS_SERVICE}/aws4_request`;
|
|
111
|
+
const stringToSign = [
|
|
112
|
+
"AWS4-HMAC-SHA256",
|
|
113
|
+
amzDate,
|
|
114
|
+
scope,
|
|
115
|
+
sha256(canonicalRequest)
|
|
116
|
+
].join("\n");
|
|
117
|
+
const signingKey = getSigningKey(creds.secretAccessKey, dateStamp, region, AWS_SERVICE);
|
|
118
|
+
const signature = hmac(signingKey, stringToSign);
|
|
119
|
+
const authorization = `AWS4-HMAC-SHA256 Credential=${creds.accessKeyId}/${scope}, SignedHeaders=${signedHeaders}, Signature=${signature}`;
|
|
120
|
+
const response = await fetch(url, {
|
|
121
|
+
method: "POST",
|
|
122
|
+
headers: {
|
|
123
|
+
...headers,
|
|
124
|
+
Authorization: authorization
|
|
125
|
+
},
|
|
126
|
+
body
|
|
127
|
+
});
|
|
128
|
+
if (!response.ok) {
|
|
129
|
+
const text = await response.text();
|
|
130
|
+
throw new Error(`EventBridge PutEvents failed: ${response.status} ${text}`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
function resolveRegion() {
|
|
134
|
+
const region = process.env.AWS_REGION || process.env.AWS_DEFAULT_REGION;
|
|
135
|
+
if (!region) {
|
|
136
|
+
throw new Error("AWS region is not set");
|
|
137
|
+
}
|
|
138
|
+
return region;
|
|
139
|
+
}
|
|
140
|
+
function resolveCredentials() {
|
|
141
|
+
const accessKeyId = process.env.AWS_ACCESS_KEY_ID;
|
|
142
|
+
const secretAccessKey = process.env.AWS_SECRET_ACCESS_KEY;
|
|
143
|
+
const sessionToken = process.env.AWS_SESSION_TOKEN;
|
|
144
|
+
if (!accessKeyId || !secretAccessKey) {
|
|
145
|
+
throw new Error("AWS credentials are not set");
|
|
146
|
+
}
|
|
147
|
+
return { accessKeyId, secretAccessKey, sessionToken };
|
|
148
|
+
}
|
|
149
|
+
function toAmzDate(date) {
|
|
150
|
+
const pad = (value) => value.toString().padStart(2, "0");
|
|
151
|
+
return [
|
|
152
|
+
date.getUTCFullYear(),
|
|
153
|
+
pad(date.getUTCMonth() + 1),
|
|
154
|
+
pad(date.getUTCDate()),
|
|
155
|
+
"T",
|
|
156
|
+
pad(date.getUTCHours()),
|
|
157
|
+
pad(date.getUTCMinutes()),
|
|
158
|
+
pad(date.getUTCSeconds()),
|
|
159
|
+
"Z"
|
|
160
|
+
].join("");
|
|
161
|
+
}
|
|
162
|
+
function sha256(value) {
|
|
163
|
+
return (0, import_node_crypto.createHash)("sha256").update(value, "utf8").digest("hex");
|
|
164
|
+
}
|
|
165
|
+
function hmac(key, value) {
|
|
166
|
+
return (0, import_node_crypto.createHmac)("sha256", key).update(value, "utf8").digest("hex");
|
|
167
|
+
}
|
|
168
|
+
function getSigningKey(secret, date, region, service) {
|
|
169
|
+
const kDate = (0, import_node_crypto.createHmac)("sha256", `AWS4${secret}`).update(date, "utf8").digest();
|
|
170
|
+
const kRegion = (0, import_node_crypto.createHmac)("sha256", kDate).update(region, "utf8").digest();
|
|
171
|
+
const kService = (0, import_node_crypto.createHmac)("sha256", kRegion).update(service, "utf8").digest();
|
|
172
|
+
return (0, import_node_crypto.createHmac)("sha256", kService).update("aws4_request", "utf8").digest();
|
|
173
|
+
}
|
|
174
|
+
function canonicalizeHeaders(headers) {
|
|
175
|
+
return Object.keys(headers).map((key) => key.toLowerCase()).sort().map((key) => `${key}:${headers[key].trim()}
|
|
176
|
+
`).join("");
|
|
177
|
+
}
|
|
178
|
+
function getSignedHeaders(headers) {
|
|
179
|
+
return Object.keys(headers).map((key) => key.toLowerCase()).sort().join(";");
|
|
180
|
+
}
|
|
181
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
182
|
+
0 && (module.exports = {
|
|
183
|
+
On,
|
|
184
|
+
publish
|
|
185
|
+
});
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import {
|
|
2
|
+
registerEvent,
|
|
3
|
+
resolveHandler
|
|
4
|
+
} from "./chunk-YMGEGOSD.js";
|
|
5
|
+
|
|
6
|
+
// src/bus/decorators.ts
|
|
7
|
+
function On(event) {
|
|
8
|
+
return (target, propertyKey, descriptor) => {
|
|
9
|
+
if (!event) {
|
|
10
|
+
throw new Error("@On() requires an event name.");
|
|
11
|
+
}
|
|
12
|
+
const handler = resolveHandler(target, propertyKey, descriptor);
|
|
13
|
+
registerEvent(handler, event);
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// src/bus/event-bus.ts
|
|
18
|
+
import { createHash, createHmac } from "crypto";
|
|
19
|
+
var AWS_TARGET = "AWSEvents.PutEvents";
|
|
20
|
+
var AWS_SERVICE = "events";
|
|
21
|
+
var DEFAULT_SOURCE = "sst-http";
|
|
22
|
+
async function publish(event, message) {
|
|
23
|
+
if (!event) {
|
|
24
|
+
throw new Error("publish() requires an event name.");
|
|
25
|
+
}
|
|
26
|
+
const payload = {
|
|
27
|
+
Entries: [
|
|
28
|
+
{
|
|
29
|
+
EventBusName: "default",
|
|
30
|
+
Source: DEFAULT_SOURCE,
|
|
31
|
+
DetailType: event,
|
|
32
|
+
Detail: JSON.stringify(message ?? null)
|
|
33
|
+
}
|
|
34
|
+
]
|
|
35
|
+
};
|
|
36
|
+
console.log(payload);
|
|
37
|
+
await putEventsViaFetch(payload);
|
|
38
|
+
}
|
|
39
|
+
async function putEventsViaFetch(payload) {
|
|
40
|
+
const region = resolveRegion();
|
|
41
|
+
const creds = resolveCredentials();
|
|
42
|
+
const host = `events.${region}.amazonaws.com`;
|
|
43
|
+
const url = `https://${host}/`;
|
|
44
|
+
const body = JSON.stringify(payload);
|
|
45
|
+
const amzDate = toAmzDate(/* @__PURE__ */ new Date());
|
|
46
|
+
const dateStamp = amzDate.slice(0, 8);
|
|
47
|
+
const headers = {
|
|
48
|
+
"content-type": "application/x-amz-json-1.1",
|
|
49
|
+
"x-amz-date": amzDate,
|
|
50
|
+
"x-amz-target": AWS_TARGET,
|
|
51
|
+
host
|
|
52
|
+
};
|
|
53
|
+
if (creds.sessionToken) {
|
|
54
|
+
headers["x-amz-security-token"] = creds.sessionToken;
|
|
55
|
+
}
|
|
56
|
+
const signedHeaders = getSignedHeaders(headers);
|
|
57
|
+
const canonicalRequest = [
|
|
58
|
+
"POST",
|
|
59
|
+
"/",
|
|
60
|
+
"",
|
|
61
|
+
canonicalizeHeaders(headers),
|
|
62
|
+
signedHeaders,
|
|
63
|
+
sha256(body)
|
|
64
|
+
].join("\n");
|
|
65
|
+
const scope = `${dateStamp}/${region}/${AWS_SERVICE}/aws4_request`;
|
|
66
|
+
const stringToSign = [
|
|
67
|
+
"AWS4-HMAC-SHA256",
|
|
68
|
+
amzDate,
|
|
69
|
+
scope,
|
|
70
|
+
sha256(canonicalRequest)
|
|
71
|
+
].join("\n");
|
|
72
|
+
const signingKey = getSigningKey(creds.secretAccessKey, dateStamp, region, AWS_SERVICE);
|
|
73
|
+
const signature = hmac(signingKey, stringToSign);
|
|
74
|
+
const authorization = `AWS4-HMAC-SHA256 Credential=${creds.accessKeyId}/${scope}, SignedHeaders=${signedHeaders}, Signature=${signature}`;
|
|
75
|
+
const response = await fetch(url, {
|
|
76
|
+
method: "POST",
|
|
77
|
+
headers: {
|
|
78
|
+
...headers,
|
|
79
|
+
Authorization: authorization
|
|
80
|
+
},
|
|
81
|
+
body
|
|
82
|
+
});
|
|
83
|
+
if (!response.ok) {
|
|
84
|
+
const text = await response.text();
|
|
85
|
+
throw new Error(`EventBridge PutEvents failed: ${response.status} ${text}`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
function resolveRegion() {
|
|
89
|
+
const region = process.env.AWS_REGION || process.env.AWS_DEFAULT_REGION;
|
|
90
|
+
if (!region) {
|
|
91
|
+
throw new Error("AWS region is not set");
|
|
92
|
+
}
|
|
93
|
+
return region;
|
|
94
|
+
}
|
|
95
|
+
function resolveCredentials() {
|
|
96
|
+
const accessKeyId = process.env.AWS_ACCESS_KEY_ID;
|
|
97
|
+
const secretAccessKey = process.env.AWS_SECRET_ACCESS_KEY;
|
|
98
|
+
const sessionToken = process.env.AWS_SESSION_TOKEN;
|
|
99
|
+
if (!accessKeyId || !secretAccessKey) {
|
|
100
|
+
throw new Error("AWS credentials are not set");
|
|
101
|
+
}
|
|
102
|
+
return { accessKeyId, secretAccessKey, sessionToken };
|
|
103
|
+
}
|
|
104
|
+
function toAmzDate(date) {
|
|
105
|
+
const pad = (value) => value.toString().padStart(2, "0");
|
|
106
|
+
return [
|
|
107
|
+
date.getUTCFullYear(),
|
|
108
|
+
pad(date.getUTCMonth() + 1),
|
|
109
|
+
pad(date.getUTCDate()),
|
|
110
|
+
"T",
|
|
111
|
+
pad(date.getUTCHours()),
|
|
112
|
+
pad(date.getUTCMinutes()),
|
|
113
|
+
pad(date.getUTCSeconds()),
|
|
114
|
+
"Z"
|
|
115
|
+
].join("");
|
|
116
|
+
}
|
|
117
|
+
function sha256(value) {
|
|
118
|
+
return createHash("sha256").update(value, "utf8").digest("hex");
|
|
119
|
+
}
|
|
120
|
+
function hmac(key, value) {
|
|
121
|
+
return createHmac("sha256", key).update(value, "utf8").digest("hex");
|
|
122
|
+
}
|
|
123
|
+
function getSigningKey(secret, date, region, service) {
|
|
124
|
+
const kDate = createHmac("sha256", `AWS4${secret}`).update(date, "utf8").digest();
|
|
125
|
+
const kRegion = createHmac("sha256", kDate).update(region, "utf8").digest();
|
|
126
|
+
const kService = createHmac("sha256", kRegion).update(service, "utf8").digest();
|
|
127
|
+
return createHmac("sha256", kService).update("aws4_request", "utf8").digest();
|
|
128
|
+
}
|
|
129
|
+
function canonicalizeHeaders(headers) {
|
|
130
|
+
return Object.keys(headers).map((key) => key.toLowerCase()).sort().map((key) => `${key}:${headers[key].trim()}
|
|
131
|
+
`).join("");
|
|
132
|
+
}
|
|
133
|
+
function getSignedHeaders(headers) {
|
|
134
|
+
return Object.keys(headers).map((key) => key.toLowerCase()).sort().join(";");
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export {
|
|
138
|
+
On,
|
|
139
|
+
publish
|
|
140
|
+
};
|