@aws-blocks/core 0.1.13 → 0.1.18
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 +180 -17
- package/dist/cdk/blocks-backend.d.ts +4 -0
- package/dist/cdk/blocks-backend.d.ts.map +1 -1
- package/dist/cdk/blocks-backend.js +23 -1
- package/dist/cdk/blocks-backend.test.js +71 -1
- package/dist/cdk/blocks-stack.test.js +32 -1
- package/dist/cdk/index.d.ts +13 -0
- package/dist/cdk/index.d.ts.map +1 -1
- package/dist/cdk/index.js +24 -0
- package/dist/cors.d.ts +27 -1
- package/dist/cors.d.ts.map +1 -1
- package/dist/cors.js +55 -2
- package/dist/cors.test.js +81 -2
- package/dist/errors.test.js +26 -1
- package/dist/hosting.d.ts.map +1 -1
- package/dist/hosting.js +26 -1
- package/dist/hosting.test.js +73 -0
- package/dist/lambda-handler.d.ts.map +1 -1
- package/dist/lambda-handler.js +4 -17
- package/dist/lambda-handler.test.js +59 -2
- package/dist/rpc.test.js +77 -1
- package/dist/scripts/console.d.ts.map +1 -1
- package/dist/scripts/console.js +30 -2
- package/dist/scripts/deploy-stream.d.ts +181 -0
- package/dist/scripts/deploy-stream.d.ts.map +1 -0
- package/dist/scripts/deploy-stream.js +332 -0
- package/dist/scripts/deploy-stream.test.d.ts +2 -0
- package/dist/scripts/deploy-stream.test.d.ts.map +1 -0
- package/dist/scripts/deploy-stream.test.js +845 -0
- package/dist/scripts/deploy.d.ts.map +1 -1
- package/dist/scripts/deploy.js +16 -9
- package/dist/scripts/dev-server-cors.test.js +19 -1
- package/dist/scripts/dev-server-rpc.test.js +50 -0
- package/dist/scripts/dev-server.d.ts +8 -0
- package/dist/scripts/dev-server.d.ts.map +1 -1
- package/dist/scripts/dev-server.js +35 -8
- package/dist/scripts/sandbox.js +1 -1
- package/dist/telemetry/client.js +4 -4
- package/dist/telemetry/telemetry-send-worker.js +4 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +10 -1
- package/src/cdk/blocks-backend.test.ts +90 -1
- package/src/cdk/blocks-backend.ts +24 -1
- package/src/cdk/blocks-stack.test.ts +41 -1
- package/src/cdk/index.ts +25 -0
- package/src/cors.test.ts +96 -2
- package/src/cors.ts +59 -2
- package/src/errors.test.ts +29 -1
- package/src/hosting.test.ts +107 -0
- package/src/hosting.ts +27 -1
- package/src/lambda-handler.test.ts +71 -2
- package/src/lambda-handler.ts +4 -20
- package/src/rpc.test.ts +96 -1
- package/src/scripts/console.ts +29 -2
- package/src/scripts/deploy-stream.test.ts +1035 -0
- package/src/scripts/deploy-stream.ts +475 -0
- package/src/scripts/deploy.ts +18 -11
- package/src/scripts/dev-server-cors.test.ts +26 -1
- package/src/scripts/dev-server-rpc.test.ts +54 -0
- package/src/scripts/dev-server.ts +38 -8
- package/src/scripts/sandbox.ts +1 -1
- package/src/telemetry/client.ts +4 -4
- package/src/telemetry/telemetry-send-worker.ts +5 -0
- package/src/version.ts +1 -1
package/README.md
CHANGED
|
@@ -63,7 +63,7 @@ The typed `import { api } from 'aws-blocks'` client is the normal path. The HTTP
|
|
|
63
63
|
|
|
64
64
|
`POST` to the RPC path `/aws-blocks/api`:
|
|
65
65
|
|
|
66
|
-
- Local dev: `http://localhost:
|
|
66
|
+
- Local dev: `http://localhost:3000/aws-blocks/api` (the default template serves the backend and frontend from a single front door on `:3000`). Only the `backend` and `amplify` templates serve the API on `:3001`.
|
|
67
67
|
- Deployed: the API Gateway stage URL + `/aws-blocks/api`
|
|
68
68
|
|
|
69
69
|
The body is JSON-RPC 2.0:
|
|
@@ -79,15 +79,70 @@ The body is JSON-RPC 2.0:
|
|
|
79
79
|
Working example:
|
|
80
80
|
|
|
81
81
|
```bash
|
|
82
|
-
curl -X POST http://localhost:
|
|
82
|
+
curl -X POST http://localhost:3000/aws-blocks/api \
|
|
83
83
|
-H 'Content-Type: application/json' \
|
|
84
84
|
-d '{"jsonrpc":"2.0","method":"api.greet","params":["World"],"id":1}'
|
|
85
85
|
# → {"jsonrpc":"2.0","result":{"message":"Hello, World!"},"id":1}
|
|
86
86
|
```
|
|
87
87
|
|
|
88
|
-
|
|
88
|
+
#### How the server reads `params` (and what it rejects)
|
|
89
89
|
|
|
90
|
-
|
|
90
|
+
The request body is parsed by `parseRpcRequest`, which turns `params` into the positional argument list your method is called with:
|
|
91
|
+
|
|
92
|
+
| Body `params` | Arguments the method receives |
|
|
93
|
+
|---|---|
|
|
94
|
+
| `["World", 42]` (array) | `('World', 42)`, used as-is |
|
|
95
|
+
| `{"name":"World","times":42}` (object) | `('World', 42)` via `Object.values()`, so **key insertion order decides argument order** |
|
|
96
|
+
| omitted or `null` | `()`, no arguments |
|
|
97
|
+
|
|
98
|
+
A named object is convenient for `curl`, but it is only safe when the keys are written in the same order as the method signature. Prefer the array form in anything automated.
|
|
99
|
+
|
|
100
|
+
One shape that looks reasonable and is **not** supported: a top-level JSON **array** as the whole body (a JSON-RPC batch). The parser reads `jsonrpc` and `method` off the body object, so an array body fails validation instead of running anything:
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
curl -X POST http://localhost:3000/aws-blocks/api \
|
|
104
|
+
-H 'Content-Type: application/json' \
|
|
105
|
+
-d '[{"jsonrpc":"2.0","method":"api.greet","params":["a"],"id":1}]'
|
|
106
|
+
# → HTTP 200
|
|
107
|
+
# {"jsonrpc":"2.0","error":{"code":-32600,"message":"Invalid Request: expected JSON-RPC 2.0 — ...",
|
|
108
|
+
# "data":{"name":"InvalidRequest"}},"id":null}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
`id` is `null` in that response because an array body has no `id` to echo. Send one call per request. Body that isn't valid JSON at all returns `-32700 Parse error`, also with HTTP `200`.
|
|
112
|
+
|
|
113
|
+
#### Runtime config (`/.blocks-sandbox/config.json`)
|
|
114
|
+
|
|
115
|
+
The generated client does not hardcode the API URL; it resolves one at first call, in this order:
|
|
116
|
+
|
|
117
|
+
1. `BLOCKS_API_URL` env var (set by the Hosting construct on SSR compute).
|
|
118
|
+
2. `BLOCKS_CONFIG` env var (the whole config as JSON).
|
|
119
|
+
3. Node only: the file `.blocks-sandbox/config.json`, read from the **process working directory**.
|
|
120
|
+
4. Browser only: `fetch('/.blocks-sandbox/config.json')`.
|
|
121
|
+
|
|
122
|
+
Both env vars are normally written for you: `Hosting` injects them into every SSR compute function at synth, and `npm run sandbox` sets `BLOCKS_API_URL` on the dev server it spawns. You set `BLOCKS_API_URL` yourself only when you run the SSR host outside that tooling, like a framework dev server on its own port (`BLOCKS_API_URL=http://localhost:3001/aws-blocks/api next dev`) or your own container. `BLOCKS_CONFIG` is Hosting's serialized `backendConfig` rather than a knob to hand-write, so prefer `BLOCKS_API_URL` for a custom host.
|
|
123
|
+
|
|
124
|
+
Two things about that path burn time when debugging:
|
|
125
|
+
|
|
126
|
+
- The directory is **dotted**: `/.blocks-sandbox/config.json`. `/config.json` is not a route and never was: locally the dev server only answers `GET /.blocks-sandbox/config.json`, and in production the Hosting construct only adds a `/.blocks-sandbox/*` static behaviour. A `404` from `curl http://localhost:3000/config.json` says nothing about your config.
|
|
127
|
+
- `{"_placeholder":true}` is a **valid, expected** body in the frontend build output. The Hosting construct writes that stub into the static assets directory during CDK synth so the file exists as a static route while the real `apiUrl` is still an unresolved CloudFormation token; the deploy then uploads the resolved config over it. Finding the stub in `dist/.blocks-sandbox/config.json` (or on the origin between synth and deploy) is the design working, not a broken config.
|
|
128
|
+
|
|
129
|
+
What you should see instead, per environment:
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
# Local dev / sandbox: served by the dev server itself, Cache-Control: no-store
|
|
133
|
+
curl http://localhost:3000/.blocks-sandbox/config.json
|
|
134
|
+
# → {"apiUrl":"http://localhost:3000/aws-blocks/api","environment":"local"}
|
|
135
|
+
|
|
136
|
+
# After a deploy: written by the deploy script and uploaded to the origin
|
|
137
|
+
cat .blocks-sandbox/config.json
|
|
138
|
+
# → { "apiUrl": "https://<id>.execute-api.<region>.amazonaws.com/prod/aws-blocks/api", "environment": "production" }
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
So a real config problem looks like the client throwing `Blocks API URL not configured` (or `... is not configured (source: ...)`), not like a `404` on `/config.json`. If a **deployed** origin keeps serving `{"_placeholder":true}` after a successful deploy, that is a genuine bug: the config upload or the CloudFront invalidation did not land.
|
|
142
|
+
|
|
143
|
+
### ApiError / isBlocksError / hasAuthError
|
|
144
|
+
|
|
145
|
+
Typed error handling across the wire. All three are exported from `@aws-blocks/core`, and re-exported from `@aws-blocks/blocks`.
|
|
91
146
|
|
|
92
147
|
```typescript
|
|
93
148
|
import { ApiError, isBlocksError } from '@aws-blocks/core';
|
|
@@ -101,32 +156,140 @@ catch (e) {
|
|
|
101
156
|
}
|
|
102
157
|
```
|
|
103
158
|
|
|
159
|
+
#### `new ApiError(message, status, options?)`
|
|
160
|
+
|
|
161
|
+
```typescript
|
|
162
|
+
new ApiError(
|
|
163
|
+
message: string,
|
|
164
|
+
status: number,
|
|
165
|
+
options?: { name?: string; cause?: unknown; retriable?: boolean },
|
|
166
|
+
)
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
| Argument | Required | What it does |
|
|
170
|
+
|---|---|---|
|
|
171
|
+
| `message` | yes | Human-facing text. Crosses the wire, so don't put internals in it. |
|
|
172
|
+
| `status` | yes | HTTP status code. Any number; nothing validates it against a known status. |
|
|
173
|
+
| `options.name` | no | The structured error name `isBlocksError` / `hasAuthError` match on (e.g. `'ItemNotFoundException'`). Defaults to `'ApiError'`, which carries no meaning, so treat it as "unnamed". |
|
|
174
|
+
| `options.cause` | no | Underlying error. **Stays server-side**, never serialized. |
|
|
175
|
+
| `options.retriable` | no | `true` when the caller can retry the same action without restarting the flow (wrong MFA code, wrong password on re-prompt). Defaults to `false`. |
|
|
176
|
+
|
|
177
|
+
The instance exposes `message`, `name`, `status` and `retriable` as readable properties, and it is a real `Error`, so `instanceof Error` and stack traces behave normally.
|
|
178
|
+
|
|
179
|
+
How `status` reaches the client over JSON-RPC: an uncaught error inside an RPC method is encoded as an error response whose **`code` is the `ApiError`'s `status`** (positive numbers can't collide with the reserved `-32xxx` range). `name` and `retriable` ride along in `error.data`. A non-`ApiError` throw becomes code `500` with no `data.name`.
|
|
180
|
+
|
|
181
|
+
```jsonc
|
|
182
|
+
// throw new ApiError('Username already taken', 409,
|
|
183
|
+
// { name: 'ConditionalCheckFailedException', retriable: true })
|
|
184
|
+
{ "jsonrpc": "2.0", "id": 1, "error": {
|
|
185
|
+
"code": 409,
|
|
186
|
+
"message": "Username already taken",
|
|
187
|
+
"data": { "name": "ConditionalCheckFailedException", "retriable": true }
|
|
188
|
+
}}
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
The client decodes that back into an `ApiError` with the same `status`, `name` and `retriable`, which is why the same `isBlocksError(e, ...)` check works on both sides. Reserved JSON-RPC codes (`-32600`, `-32700`, …) decode to `status: 500`.
|
|
192
|
+
|
|
193
|
+
#### `hasAuthError(state, name)`
|
|
194
|
+
|
|
195
|
+
```typescript
|
|
196
|
+
import { hasAuthError } from '@aws-blocks/core';
|
|
197
|
+
|
|
198
|
+
function hasAuthError<T extends { errorName?: string }, N extends string>(
|
|
199
|
+
state: T | null | undefined,
|
|
200
|
+
name: N,
|
|
201
|
+
): state is T & { errorName: N }
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
The auth blocks' recommended client path (`setAuthState()` / `getAuthState()`) **returns** a failed `AuthState` instead of throwing, so there is no `Error` for `isBlocksError` to inspect. `hasAuthError` is the equivalent guard for that returned object: it compares `state.errorName` to `name` and narrows the type. It's a plain equality check, so a `null` / `undefined` state and a state with no `errorName` both return `false` and no defensive wrapping is needed.
|
|
205
|
+
|
|
206
|
+
```typescript
|
|
207
|
+
const next = await authApi.setAuthState({ action: 'signIn', username, password });
|
|
208
|
+
if (hasAuthError(next, AuthBasicErrors.InvalidCredentials)) {
|
|
209
|
+
// unknown user or wrong password → offer sign-up
|
|
210
|
+
}
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
Rule of thumb: **thrown error → `isBlocksError`; returned `AuthState` → `hasAuthError`.** Match on the block's error constant, never on the human-facing `error` string.
|
|
214
|
+
|
|
104
215
|
### RawRoute
|
|
105
216
|
|
|
106
|
-
Path-based HTTP routing Building Block for endpoints that need full request/response control
|
|
217
|
+
Path-based HTTP routing Building Block for endpoints that need full request/response control: webhooks, health checks, redirects, file downloads, anything a browser or third party has to hit with a plain `GET`. Use `ApiNamespace` (RPC) for typed function calls; use `RawRoute` when you need raw HTTP semantics.
|
|
218
|
+
|
|
219
|
+
```typescript
|
|
220
|
+
new RawRoute(scope: ScopeParent, id: string, options: {
|
|
221
|
+
method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'HEAD' | 'OPTIONS';
|
|
222
|
+
path?: string;
|
|
223
|
+
handler: (context: BlocksContext) => Promise<void>;
|
|
224
|
+
})
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
A full `GET` that sets its own status, content type and body:
|
|
107
228
|
|
|
108
229
|
```typescript
|
|
109
230
|
import { RawRoute } from '@aws-blocks/blocks';
|
|
110
231
|
|
|
111
|
-
|
|
112
|
-
new RawRoute(scope, 'GetUser', {
|
|
232
|
+
new RawRoute(scope, 'status', {
|
|
113
233
|
method: 'GET',
|
|
114
|
-
path: '/
|
|
115
|
-
handler: async (
|
|
116
|
-
|
|
117
|
-
|
|
234
|
+
path: '/status',
|
|
235
|
+
handler: async (ctx) => {
|
|
236
|
+
ctx.response.status = 200;
|
|
237
|
+
ctx.response.headers.set('Content-Type', 'text/html; charset=utf-8');
|
|
238
|
+
ctx.response.send('<h1>ok</h1>');
|
|
118
239
|
},
|
|
119
240
|
});
|
|
241
|
+
```
|
|
120
242
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
243
|
+
```bash
|
|
244
|
+
curl -i http://localhost:3000/status
|
|
245
|
+
# HTTP/1.1 200 OK
|
|
246
|
+
# content-type: text/html; charset=utf-8
|
|
247
|
+
#
|
|
248
|
+
# <h1>ok</h1>
|
|
125
249
|
```
|
|
126
250
|
|
|
127
|
-
|
|
251
|
+
The handler returns nothing; you write the response through `ctx.response`:
|
|
252
|
+
|
|
253
|
+
| On `ctx.response` | Notes |
|
|
254
|
+
|---|---|
|
|
255
|
+
| `status: number` | Assignable. Defaults to `200`. |
|
|
256
|
+
| `headers: Headers` | Standard `Headers`. Set `Content-Type` yourself; a string body is sent as-is, an object is serialized as JSON. |
|
|
257
|
+
| `send(body)` | Call once with the body. `send('')` for an empty body (redirects, `204`). |
|
|
258
|
+
|
|
259
|
+
And what you read from `ctx.request`:
|
|
260
|
+
|
|
261
|
+
| On `ctx.request` | Notes |
|
|
262
|
+
|---|---|
|
|
263
|
+
| `params` | Path parameters, e.g. route `/users/{id}` + request `/users/42` → `{ id: '42' }`. Always `{}` for RPC methods. |
|
|
264
|
+
| `url` | Absolute `URL` of the request. Use `url.searchParams` for the query string. |
|
|
265
|
+
| `headers` | Request `Headers`, including `cookie`. |
|
|
266
|
+
| `json()` / `text()` / `body` | Body as parsed JSON, raw text, or a `ReadableStream`. |
|
|
267
|
+
| `signal` | `AbortSignal` that fires just before the platform's timeout response. Pass it to `fetch`/SDK calls. `undefined` in local dev. |
|
|
268
|
+
|
|
269
|
+
Reading a path parameter and a query parameter:
|
|
270
|
+
|
|
271
|
+
```typescript
|
|
272
|
+
new RawRoute(scope, 'user', {
|
|
273
|
+
method: 'GET',
|
|
274
|
+
path: '/users/{id}',
|
|
275
|
+
handler: async (ctx) => {
|
|
276
|
+
ctx.response.headers.set('Content-Type', 'application/json');
|
|
277
|
+
ctx.response.send({ id: ctx.request.params.id, q: ctx.request.url.searchParams.get('q') });
|
|
278
|
+
},
|
|
279
|
+
});
|
|
280
|
+
// GET /users/42?q=hello → {"id":"42","q":"hello"}
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
Path syntax: exact (`/health`), named parameter capturing one segment (`/users/{id}`), or a trailing wildcard capturing the rest (`/files/*`, available as `params['*']`). One wildcard per route, last segment only. Named parameters are URL-decoded; wildcard captures are not, so validate them before touching a filesystem or an S3 key.
|
|
284
|
+
|
|
285
|
+
`path` can be omitted, in which case it is derived from the scope-chain IDs, so `Scope('app') → Scope('v1') → RawRoute('health')` gives `/v1/health`. That means restructuring your construct tree silently changes URLs, so pass an explicit `path` for anything a client depends on.
|
|
286
|
+
|
|
287
|
+
Registration rules worth knowing before you hit them at runtime:
|
|
128
288
|
|
|
129
|
-
|
|
289
|
+
- Routes must be constructed while the backend module is loading (top level of `aws-blocks/index.ts`, or from a block's constructor). Registering after the handler is created throws.
|
|
290
|
+
- `/aws-blocks` itself and `/aws-blocks/api` (plus anything under it) are reserved for RPC dispatch, and `/` is not routable, so use a sub-path.
|
|
291
|
+
- The same `method` + `path` twice throws `RawRouteErrors.DuplicateRoute`; catch it with `isBlocksError(e, RawRouteErrors.DuplicateRoute)`.
|
|
292
|
+
- No extra AWS resources are created. The existing API Gateway proxy already forwards every path to the same Lambda, which checks the route registry before falling through to RPC.
|
|
130
293
|
|
|
131
294
|
### Pipeline
|
|
132
295
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as cdk from 'aws-cdk-lib';
|
|
2
2
|
import * as apigateway from 'aws-cdk-lib/aws-apigateway';
|
|
3
|
+
import * as iam from 'aws-cdk-lib/aws-iam';
|
|
3
4
|
import { Construct } from 'constructs';
|
|
4
5
|
/**
|
|
5
6
|
* Validate that the Node.js process was started with `--conditions=cdk`.
|
|
@@ -19,6 +20,7 @@ export declare function setupBlocksInfra(scope: Construct, props: BlocksBackendP
|
|
|
19
20
|
handler: cdk.aws_lambda_nodejs.NodejsFunction;
|
|
20
21
|
gateway: cdk.aws_apigateway.RestApi;
|
|
21
22
|
apiUrl: string;
|
|
23
|
+
executionRole: cdk.aws_iam.Role;
|
|
22
24
|
};
|
|
23
25
|
/**
|
|
24
26
|
* Standalone CDK construct that provisions the Blocks backend: a single Lambda
|
|
@@ -42,6 +44,8 @@ export declare class BlocksBackend extends Construct {
|
|
|
42
44
|
readonly gateway: apigateway.RestApi;
|
|
43
45
|
readonly handler: cdk.aws_lambda_nodejs.NodejsFunction;
|
|
44
46
|
readonly backendHandlerPath: string;
|
|
47
|
+
/** Shared IAM role assumed by all Blocks compute. Building Blocks grant to this role. */
|
|
48
|
+
readonly executionRole: iam.IRole;
|
|
45
49
|
/**
|
|
46
50
|
* The fullId used by child Scopes to compute their env var names,
|
|
47
51
|
* construct IDs, and physical resource names (e.g., DynamoDB table names).
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"blocks-backend.d.ts","sourceRoot":"","sources":["../../src/cdk/blocks-backend.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,GAAG,MAAM,aAAa,CAAC;AAEnC,OAAO,KAAK,UAAU,MAAM,4BAA4B,CAAC;
|
|
1
|
+
{"version":3,"file":"blocks-backend.d.ts","sourceRoot":"","sources":["../../src/cdk/blocks-backend.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,GAAG,MAAM,aAAa,CAAC;AAEnC,OAAO,KAAK,UAAU,MAAM,4BAA4B,CAAC;AACzD,OAAO,KAAK,GAAG,MAAM,qBAAqB,CAAC;AAE3C,OAAO,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAQvC;;;;;;;GAOG;AACH,wBAAgB,wBAAwB,IAAI,IAAI,CAkB/C;AAED,MAAM,WAAW,kBAAkB;IACjC,kBAAkB,EAAE,MAAM,CAAC;IAC3B,cAAc,EAAE,MAAM,CAAC;CACxB;AAED,4EAA4E;AAC5E,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,SAAS,EAAE,KAAK,EAAE,kBAAkB,EAAE,EAAE,CAAC,EAAE,MAAM;;;;;EA6HxF;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,qBAAa,aAAc,SAAQ,SAAS;IAC1C,SAAgB,MAAM,EAAE,MAAM,CAAC;IAC/B,SAAgB,OAAO,EAAE,UAAU,CAAC,OAAO,CAAC;IAC5C,SAAgB,OAAO,EAAE,GAAG,CAAC,iBAAiB,CAAC,cAAc,CAAC;IAC9D,SAAgB,kBAAkB,EAAE,MAAM,CAAC;IAC3C,yFAAyF;IACzF,SAAgB,aAAa,EAAE,GAAG,CAAC,KAAK,CAAC;IAEzC;;;;;;;;;;;;;;;;;;OAkBG;IACH,IAAI,MAAM,IAAI,MAAM,CAUnB;IAED,OAAO;WAoBM,MAAM,CAAC,KAAK,EAAE,SAAS,EAAE,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,kBAAkB;CAsB5E"}
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import * as cdk from 'aws-cdk-lib';
|
|
4
4
|
import * as lambda from 'aws-cdk-lib/aws-lambda-nodejs';
|
|
5
5
|
import * as apigateway from 'aws-cdk-lib/aws-apigateway';
|
|
6
|
+
import * as iam from 'aws-cdk-lib/aws-iam';
|
|
6
7
|
import { CfnGroup } from 'aws-cdk-lib/aws-resourcegroups';
|
|
7
8
|
import { Construct } from 'constructs';
|
|
8
9
|
import { pathToFileURL } from 'node:url';
|
|
@@ -35,10 +36,28 @@ export function assertCdkConditionActive() {
|
|
|
35
36
|
}
|
|
36
37
|
/** Shared infra setup — creates Lambda + API Gateway on the given scope. */
|
|
37
38
|
export function setupBlocksInfra(scope, props, id) {
|
|
39
|
+
// ── Shared execution role ──────────────────────────────────────────────
|
|
40
|
+
// A single IAM role that every Building Block grants to. Provisioned here so
|
|
41
|
+
// it exists before the backend module is imported (Building Blocks reach it
|
|
42
|
+
// via `scope.executionRole`). Block grants sit on the role's default (inline)
|
|
43
|
+
// policy, exactly as they did on the auto-generated NodejsFunction role.
|
|
44
|
+
//
|
|
45
|
+
// AWSLambdaBasicExecutionRole is attached explicitly because the auto-role
|
|
46
|
+
// included it by default — omitting it would silently break CloudWatch Logs.
|
|
47
|
+
const executionRole = new iam.Role(scope, 'BlocksRole', {
|
|
48
|
+
// CompositePrincipal (rather than a bare ServicePrincipal) so additional
|
|
49
|
+
// compute types can assume this same shared role as they are introduced
|
|
50
|
+
// (e.g. ECS tasks via ecs-tasks.amazonaws.com), by adding principals here.
|
|
51
|
+
assumedBy: new iam.CompositePrincipal(new iam.ServicePrincipal('lambda.amazonaws.com')),
|
|
52
|
+
managedPolicies: [
|
|
53
|
+
iam.ManagedPolicy.fromAwsManagedPolicyName('service-role/AWSLambdaBasicExecutionRole'),
|
|
54
|
+
],
|
|
55
|
+
});
|
|
38
56
|
const handler = new lambda.NodejsFunction(scope, 'Handler', {
|
|
39
57
|
entry: props.backendHandlerPath,
|
|
40
58
|
runtime: DEFAULT_NODE_RUNTIME,
|
|
41
59
|
handler: 'handler',
|
|
60
|
+
role: executionRole,
|
|
42
61
|
memorySize: 2048,
|
|
43
62
|
timeout: cdk.Duration.seconds(60 * 15),
|
|
44
63
|
environment: {
|
|
@@ -126,7 +145,7 @@ export function setupBlocksInfra(scope, props, id) {
|
|
|
126
145
|
registerConfig(scope, 'BB_RESOURCES_GROUP_URL', resourcesUrl);
|
|
127
146
|
registerConfig(scope, 'BB_SETTINGS_GROUP_URL', settingsUrl);
|
|
128
147
|
registerBuiltinRoutes();
|
|
129
|
-
return { handler, gateway: api, apiUrl: `${api.url}${BLOCKS_RPC_PREFIX.slice(1)}
|
|
148
|
+
return { handler, gateway: api, apiUrl: `${api.url}${BLOCKS_RPC_PREFIX.slice(1)}`, executionRole };
|
|
130
149
|
}
|
|
131
150
|
/**
|
|
132
151
|
* Standalone CDK construct that provisions the Blocks backend: a single Lambda
|
|
@@ -150,6 +169,8 @@ export class BlocksBackend extends Construct {
|
|
|
150
169
|
gateway;
|
|
151
170
|
handler;
|
|
152
171
|
backendHandlerPath;
|
|
172
|
+
/** Shared IAM role assumed by all Blocks compute. Building Blocks grant to this role. */
|
|
173
|
+
executionRole;
|
|
153
174
|
/**
|
|
154
175
|
* The fullId used by child Scopes to compute their env var names,
|
|
155
176
|
* construct IDs, and physical resource names (e.g., DynamoDB table names).
|
|
@@ -189,6 +210,7 @@ export class BlocksBackend extends Construct {
|
|
|
189
210
|
this.handler = infra.handler;
|
|
190
211
|
this.gateway = infra.gateway;
|
|
191
212
|
this.apiUrl = infra.apiUrl;
|
|
213
|
+
this.executionRole = infra.executionRole;
|
|
192
214
|
// Override BLOCKS_STACK_NAME to include the parent stack name so runtime
|
|
193
215
|
// resource lookups (DynamoDB table names) match the CDK-time fullId
|
|
194
216
|
// and are unique per deployment.
|
|
@@ -5,8 +5,10 @@ import assert from 'node:assert';
|
|
|
5
5
|
import { fileURLToPath } from 'node:url';
|
|
6
6
|
import { dirname, join } from 'node:path';
|
|
7
7
|
import * as cdk from 'aws-cdk-lib';
|
|
8
|
-
import { Template } from 'aws-cdk-lib/assertions';
|
|
8
|
+
import { Template, Match } from 'aws-cdk-lib/assertions';
|
|
9
|
+
import { PolicyStatement } from 'aws-cdk-lib/aws-iam';
|
|
9
10
|
import { BlocksBackend } from './blocks-backend.js';
|
|
11
|
+
import { Scope } from './index.js';
|
|
10
12
|
// Simulate the CDK condition being active (tests import CDK files directly)
|
|
11
13
|
before(() => {
|
|
12
14
|
process.env.NODE_OPTIONS = (process.env.NODE_OPTIONS ?? '') + ' --conditions=cdk';
|
|
@@ -16,6 +18,7 @@ const handlerPath = join(__dirname, '__fixtures__', 'handler.js');
|
|
|
16
18
|
const sideEffectBackendPath = join(__dirname, '__fixtures__', 'side-effect-backend.js');
|
|
17
19
|
const factoryBackendPath = join(__dirname, '__fixtures__', 'factory-backend.js');
|
|
18
20
|
const fullIdConstructBackendPath = join(__dirname, '__fixtures__', 'fullid-construct-backend.js');
|
|
21
|
+
const EXECUTION_ROLE_MARKER_ACTION = 'blocks-test:MarkerAction';
|
|
19
22
|
describe('ESM cache-busting (multi-stage)', () => {
|
|
20
23
|
test('BlocksBackend.create() with same backendCDKPath but different IDs produces constructs in each', async () => {
|
|
21
24
|
const app = new cdk.App();
|
|
@@ -66,6 +69,73 @@ describe('synth shape (drop into existing stack)', () => {
|
|
|
66
69
|
template.resourceCountIs('AWS::ApiGateway::RestApi', 2);
|
|
67
70
|
});
|
|
68
71
|
});
|
|
72
|
+
describe('shared execution role', () => {
|
|
73
|
+
test('exposes executionRole on the backend', async () => {
|
|
74
|
+
const app = new cdk.App();
|
|
75
|
+
const parent = new cdk.Stack(app, 'RoleSurfaceStack');
|
|
76
|
+
const backend = await BlocksBackend.create(parent, 'Blocks', {
|
|
77
|
+
backendHandlerPath: handlerPath,
|
|
78
|
+
backendCDKPath: sideEffectBackendPath,
|
|
79
|
+
});
|
|
80
|
+
assert.ok(backend.executionRole, 'BlocksBackend should expose .executionRole');
|
|
81
|
+
});
|
|
82
|
+
test('synth produces a Lambda-assumable role with basic execution, and the handler uses it', async () => {
|
|
83
|
+
const app = new cdk.App();
|
|
84
|
+
const parent = new cdk.Stack(app, 'RoleSynthStack');
|
|
85
|
+
await BlocksBackend.create(parent, 'Blocks', {
|
|
86
|
+
backendHandlerPath: handlerPath,
|
|
87
|
+
backendCDKPath: sideEffectBackendPath,
|
|
88
|
+
});
|
|
89
|
+
const template = Template.fromStack(parent);
|
|
90
|
+
// The shared role (logical id derived from the 'BlocksRole' construct id)
|
|
91
|
+
// is assumable by Lambda and carries AWSLambdaBasicExecutionRole (so
|
|
92
|
+
// CloudWatch Logs keep working after swapping off the auto-role). Other
|
|
93
|
+
// roles exist (API Gateway CloudWatch role, config BucketDeployment role),
|
|
94
|
+
// so we target ours by logical id.
|
|
95
|
+
const roles = template.findResources('AWS::IAM::Role');
|
|
96
|
+
const blocksRoleId = Object.keys(roles).find(k => k.includes('BlocksRole'));
|
|
97
|
+
assert.ok(blocksRoleId, 'expected a role from the BlocksRole construct');
|
|
98
|
+
const blocksRole = roles[blocksRoleId];
|
|
99
|
+
assert.deepStrictEqual(blocksRole.Properties.AssumeRolePolicyDocument.Statement[0], {
|
|
100
|
+
Action: 'sts:AssumeRole',
|
|
101
|
+
Effect: 'Allow',
|
|
102
|
+
Principal: { Service: 'lambda.amazonaws.com' },
|
|
103
|
+
});
|
|
104
|
+
assert.ok(JSON.stringify(blocksRole.Properties.ManagedPolicyArns ?? []).includes('AWSLambdaBasicExecutionRole'), 'BlocksRole should attach AWSLambdaBasicExecutionRole');
|
|
105
|
+
// The Blocks handler references the shared role, not an auto-generated one.
|
|
106
|
+
template.hasResourceProperties('AWS::Lambda::Function', {
|
|
107
|
+
Role: { 'Fn::GetAtt': [blocksRoleId, 'Arn'] },
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
test('a nested block resolves executionRole via the construct-tree walk', async () => {
|
|
111
|
+
const app = new cdk.App();
|
|
112
|
+
const parent = new cdk.Stack(app, 'RoleResolveStack');
|
|
113
|
+
const backend = await BlocksBackend.create(parent, 'Blocks', {
|
|
114
|
+
backendHandlerPath: handlerPath,
|
|
115
|
+
backendCDKPath: sideEffectBackendPath,
|
|
116
|
+
});
|
|
117
|
+
// Build nested Scopes under the backend (outer → inner), the same shape a
|
|
118
|
+
// real Building Block tree has, and grant a uniquely-named marker action to
|
|
119
|
+
// `this.executionRole` from the innermost scope. If the getter's tree-walk
|
|
120
|
+
// failed, it would resolve the wrong role (or throw), and the marker would
|
|
121
|
+
// not land on the backend's shared role.
|
|
122
|
+
// `create()` sets globalThis.CURRENT_BLOCKS_STACK = backend, so a parent-less
|
|
123
|
+
// Scope attaches under the backend (the same way a real backend module's
|
|
124
|
+
// top-level blocks do); `inner` is then nested one level deeper.
|
|
125
|
+
const outer = new Scope('outer');
|
|
126
|
+
const inner = new Scope('inner', { parent: outer });
|
|
127
|
+
// Resolves to the backend's shared role from two levels deep.
|
|
128
|
+
assert.strictEqual(inner.executionRole, backend.executionRole);
|
|
129
|
+
inner.executionRole.addToPrincipalPolicy(new PolicyStatement({ actions: [EXECUTION_ROLE_MARKER_ACTION], resources: ['*'] }));
|
|
130
|
+
// The grant lands on the shared role's default inline policy (AWS::IAM::Policy).
|
|
131
|
+
const template = Template.fromStack(parent);
|
|
132
|
+
template.hasResourceProperties('AWS::IAM::Policy', {
|
|
133
|
+
PolicyDocument: {
|
|
134
|
+
Statement: Match.arrayWith([Match.objectLike({ Action: EXECUTION_ROLE_MARKER_ACTION })]),
|
|
135
|
+
},
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
});
|
|
69
139
|
describe('factory function support', () => {
|
|
70
140
|
test('BlocksBackend.create() calls default export function with the backend instance', async () => {
|
|
71
141
|
const app = new cdk.App();
|
|
@@ -6,7 +6,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
6
6
|
import { dirname, join } from 'node:path';
|
|
7
7
|
import * as cdk from 'aws-cdk-lib';
|
|
8
8
|
import { BlocksBackend } from './blocks-backend.js';
|
|
9
|
-
import { BlocksStack } from './index.js';
|
|
9
|
+
import { BlocksStack, Scope } from './index.js';
|
|
10
10
|
// Simulate the CDK condition being active (tests import CDK files directly)
|
|
11
11
|
before(() => {
|
|
12
12
|
process.env.NODE_OPTIONS = (process.env.NODE_OPTIONS ?? '') + ' --conditions=cdk';
|
|
@@ -54,6 +54,37 @@ describe('legacy side-effect mode (no default export)', () => {
|
|
|
54
54
|
assert.ok(marker, 'Side-effect-only module should register construct via globalThis.CURRENT_BLOCKS_STACK');
|
|
55
55
|
});
|
|
56
56
|
});
|
|
57
|
+
describe('shared execution role (BlocksStack)', () => {
|
|
58
|
+
// The role synth shape and the Scope.executionRole tree-walk are shared code
|
|
59
|
+
// (setupBlocksInfra + the getter), covered in blocks-backend.test.ts. The only
|
|
60
|
+
// BlocksStack-specific behavior is that its own constructor wires
|
|
61
|
+
// executionRole — a separate code path from BlocksBackend's constructor.
|
|
62
|
+
test('BlocksStack wires executionRole via its constructor', async () => {
|
|
63
|
+
const app = new cdk.App();
|
|
64
|
+
const stack = await BlocksStack.create(app, 'StackRoleStack', {
|
|
65
|
+
backendHandlerPath: handlerPath,
|
|
66
|
+
backendCDKPath: sideEffectBackendPath,
|
|
67
|
+
});
|
|
68
|
+
assert.ok(stack.executionRole, 'BlocksStack should expose a populated .executionRole');
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
describe('executionRole globalThis fallback', () => {
|
|
72
|
+
test('resolves via globalThis.CURRENT_BLOCKS_STACK when no owner is in the tree', async () => {
|
|
73
|
+
const app = new cdk.App();
|
|
74
|
+
const stack = await BlocksStack.create(app, 'FallbackStack', {
|
|
75
|
+
backendHandlerPath: handlerPath,
|
|
76
|
+
backendCDKPath: sideEffectBackendPath,
|
|
77
|
+
});
|
|
78
|
+
// A Scope whose construct-tree ancestry has no BlocksStack/BlocksBackend
|
|
79
|
+
// (parented under a plain cdk.Stack) exhausts the tree-walk and falls back
|
|
80
|
+
// to globalThis.CURRENT_BLOCKS_STACK. The `as any` is test plumbing — a
|
|
81
|
+
// plain Stack isn't a ScopeParent, but it IS a valid Construct parent.
|
|
82
|
+
const plainStack = new cdk.Stack(app, 'PlainStack');
|
|
83
|
+
globalThis.CURRENT_BLOCKS_STACK = stack;
|
|
84
|
+
const orphan = new Scope('orphan', { parent: plainStack });
|
|
85
|
+
assert.strictEqual(orphan.executionRole, stack.executionRole, 'fallback resolves to the ambient stack role');
|
|
86
|
+
});
|
|
87
|
+
});
|
|
57
88
|
describe('assertCdkConditionActive', () => {
|
|
58
89
|
test('BlocksStack.create() throws when --conditions=cdk is missing', async () => {
|
|
59
90
|
const origNodeOptions = process.env.NODE_OPTIONS;
|
package/dist/cdk/index.d.ts
CHANGED
|
@@ -14,6 +14,8 @@ export declare class BlocksStack extends cdk.Stack implements BaseBlocksStack {
|
|
|
14
14
|
readonly gateway: cdk.aws_apigateway.RestApi;
|
|
15
15
|
readonly handler: cdk.aws_lambda_nodejs.NodejsFunction;
|
|
16
16
|
readonly backendHandlerPath: string;
|
|
17
|
+
/** Shared IAM role assumed by all Blocks compute. Building Blocks grant to this role. */
|
|
18
|
+
readonly executionRole: cdk.aws_iam.IRole;
|
|
17
19
|
private constructor();
|
|
18
20
|
static create(scope: Construct, id: string, props: BlocksStackProps): Promise<BlocksStack>;
|
|
19
21
|
}
|
|
@@ -24,6 +26,17 @@ export declare class Scope extends Construct {
|
|
|
24
26
|
readonly bbVersion?: string;
|
|
25
27
|
constructor(id: string, options?: ScopeOptions);
|
|
26
28
|
get handler(): cdk.aws_lambda_nodejs.NodejsFunction;
|
|
29
|
+
/**
|
|
30
|
+
* The shared IAM role assumed by all Blocks compute. Building Blocks grant
|
|
31
|
+
* their permissions to this role instead of to an individual function's
|
|
32
|
+
* auto-role. CDK's `grant*()` / `addToPrincipalPolicy()` route those grants
|
|
33
|
+
* to the role's default (inline) policy — exactly where they landed on the
|
|
34
|
+
* auto-generated role before.
|
|
35
|
+
*
|
|
36
|
+
* Resolves the same way as {@link handler}: walk up to the owning
|
|
37
|
+
* BlocksStack/BlocksBackend, falling back to the ambient stack.
|
|
38
|
+
*/
|
|
39
|
+
get executionRole(): cdk.aws_iam.IRole;
|
|
27
40
|
get fullId(): string;
|
|
28
41
|
protected buildUserAgentChain(): [string, string][];
|
|
29
42
|
registerClientMiddleware(_packageSpecifier: string): void;
|
package/dist/cdk/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/cdk/index.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,GAAG,MAAM,aAAa,CAAC;AACnC,OAAO,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAGvC,OAAO,EACL,KAAK,gBAAgB,EACrB,KAAK,WAAW,IAAI,eAAe,EACnC,KAAK,WAAW,EAChB,KAAK,YAAY,EAElB,MAAM,oBAAoB,CAAC;AAK5B,OAAO,EAAE,aAAa,EAAE,KAAK,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;AAC7E,OAAO,EAAE,oBAAoB,EAAE,MAAM,mBAAmB,CAAC;AACzD,OAAO,EAAE,gCAAgC,EAAE,MAAM,aAAa,CAAC;AAC/D,OAAO,EAAE,cAAc,EAAE,sBAAsB,EAAE,MAAM,sBAAsB,CAAC;AAC9E,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAC9C,YAAY,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAChD,OAAO,EAAE,QAAQ,EAAE,aAAa,EAAE,YAAY,EAAE,sBAAsB,EAAE,MAAM,cAAc,CAAC;AAE7F,qBAAa,WAAY,SAAQ,GAAG,CAAC,KAAM,YAAW,eAAe;IACnE,SAAgB,EAAE,EAAE,MAAM,CAAC;IAC3B,SAAgB,MAAM,EAAE,MAAM,CAAC;IAC/B,SAAgB,OAAO,EAAE,GAAG,CAAC,cAAc,CAAC,OAAO,CAAC;IACpD,SAAgB,OAAO,EAAE,GAAG,CAAC,iBAAiB,CAAC,cAAc,CAAC;IAC9D,SAAgB,kBAAkB,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/cdk/index.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,GAAG,MAAM,aAAa,CAAC;AACnC,OAAO,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAGvC,OAAO,EACL,KAAK,gBAAgB,EACrB,KAAK,WAAW,IAAI,eAAe,EACnC,KAAK,WAAW,EAChB,KAAK,YAAY,EAElB,MAAM,oBAAoB,CAAC;AAK5B,OAAO,EAAE,aAAa,EAAE,KAAK,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;AAC7E,OAAO,EAAE,oBAAoB,EAAE,MAAM,mBAAmB,CAAC;AACzD,OAAO,EAAE,gCAAgC,EAAE,MAAM,aAAa,CAAC;AAC/D,OAAO,EAAE,cAAc,EAAE,sBAAsB,EAAE,MAAM,sBAAsB,CAAC;AAC9E,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAC9C,YAAY,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAChD,OAAO,EAAE,QAAQ,EAAE,aAAa,EAAE,YAAY,EAAE,sBAAsB,EAAE,MAAM,cAAc,CAAC;AAE7F,qBAAa,WAAY,SAAQ,GAAG,CAAC,KAAM,YAAW,eAAe;IACnE,SAAgB,EAAE,EAAE,MAAM,CAAC;IAC3B,SAAgB,MAAM,EAAE,MAAM,CAAC;IAC/B,SAAgB,OAAO,EAAE,GAAG,CAAC,cAAc,CAAC,OAAO,CAAC;IACpD,SAAgB,OAAO,EAAE,GAAG,CAAC,iBAAiB,CAAC,cAAc,CAAC;IAC9D,SAAgB,kBAAkB,EAAE,MAAM,CAAC;IAC3C,yFAAyF;IACzF,SAAgB,aAAa,EAAE,GAAG,CAAC,OAAO,CAAC,KAAK,CAAC;IAEjD,OAAO;WAeM,MAAM,CAAC,KAAK,EAAE,SAAS,EAAE,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,gBAAgB;CA6B1E;AAED,qBAAa,KAAM,SAAQ,SAAS;IAClC,SAAgB,EAAE,EAAE,MAAM,CAAC;IAC3B,SAAgB,MAAM,EAAE,WAAW,CAAC;IAEpC,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;gBAEhB,EAAE,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,YAAY;IAO9C,IAAI,OAAO,yCAWV;IAED;;;;;;;;;OASG;IACH,IAAI,aAAa,IAAI,GAAG,CAAC,OAAO,CAAC,KAAK,CAUrC;IAED,IAAI,MAAM,IAAI,MAAM,CAEnB;IAED,SAAS,CAAC,mBAAmB,IAAI,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE;IAKnD,wBAAwB,CAAC,iBAAiB,EAAE,MAAM,GAAG,IAAI;IACzD,qBAAqB,CAAC,iBAAiB,EAAE,MAAM,GAAG,IAAI;IACtD,0BAA0B,CAAC,YAAY,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,MAAM,EAAE,GAAG,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI;IACrH,IAAI,gBAAgB,IAAI,SAAS,MAAM,EAAE,CAAe;IACxD,IAAI,cAAc,IAAI,SAAS,MAAM,EAAE,CAAe;CACvD"}
|
package/dist/cdk/index.js
CHANGED
|
@@ -20,6 +20,8 @@ export class BlocksStack extends cdk.Stack {
|
|
|
20
20
|
gateway;
|
|
21
21
|
handler;
|
|
22
22
|
backendHandlerPath;
|
|
23
|
+
/** Shared IAM role assumed by all Blocks compute. Building Blocks grant to this role. */
|
|
24
|
+
executionRole;
|
|
23
25
|
constructor(scope, id, props) {
|
|
24
26
|
super(scope, id, props);
|
|
25
27
|
this.id = id;
|
|
@@ -30,6 +32,7 @@ export class BlocksStack extends cdk.Stack {
|
|
|
30
32
|
this.handler = infra.handler;
|
|
31
33
|
this.gateway = infra.gateway;
|
|
32
34
|
this.apiUrl = infra.apiUrl;
|
|
35
|
+
this.executionRole = infra.executionRole;
|
|
33
36
|
}
|
|
34
37
|
static async create(scope, id, props) {
|
|
35
38
|
assertCdkConditionActive();
|
|
@@ -80,6 +83,27 @@ export class Scope extends Construct {
|
|
|
80
83
|
// Fallback to globalThis for backward compatibility
|
|
81
84
|
return globalThis.CURRENT_BLOCKS_STACK.handler;
|
|
82
85
|
}
|
|
86
|
+
/**
|
|
87
|
+
* The shared IAM role assumed by all Blocks compute. Building Blocks grant
|
|
88
|
+
* their permissions to this role instead of to an individual function's
|
|
89
|
+
* auto-role. CDK's `grant*()` / `addToPrincipalPolicy()` route those grants
|
|
90
|
+
* to the role's default (inline) policy — exactly where they landed on the
|
|
91
|
+
* auto-generated role before.
|
|
92
|
+
*
|
|
93
|
+
* Resolves the same way as {@link handler}: walk up to the owning
|
|
94
|
+
* BlocksStack/BlocksBackend, falling back to the ambient stack.
|
|
95
|
+
*/
|
|
96
|
+
get executionRole() {
|
|
97
|
+
let current = this;
|
|
98
|
+
while (current.node.scope) {
|
|
99
|
+
current = current.node.scope;
|
|
100
|
+
if (current instanceof BlocksStack || current instanceof BlocksBackend) {
|
|
101
|
+
return current.executionRole;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
// Fallback to globalThis for backward compatibility
|
|
105
|
+
return globalThis.CURRENT_BLOCKS_STACK.executionRole;
|
|
106
|
+
}
|
|
83
107
|
get fullId() {
|
|
84
108
|
return computeScopeFullId(this);
|
|
85
109
|
}
|
package/dist/cors.d.ts
CHANGED
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `Access-Control-Max-Age` for preflight responses, in seconds.
|
|
3
|
+
*
|
|
4
|
+
* Chromium caps the preflight cache at 7200s and silently clamps anything
|
|
5
|
+
* higher, so a larger value buys nothing while widening the window in which a
|
|
6
|
+
* stale per-origin grant can be served. Shared by the Lambda handler and the
|
|
7
|
+
* local dev server so the two can't drift.
|
|
8
|
+
*/
|
|
9
|
+
export declare const CORS_MAX_AGE = "7200";
|
|
1
10
|
/**
|
|
2
11
|
* Parse a comma-separated CORS origin string into anchored RegExp patterns.
|
|
3
12
|
*
|
|
@@ -24,6 +33,23 @@ export declare function getCorsPatterns(): RegExp[] | null;
|
|
|
24
33
|
* @returns `true` if the origin matches at least one pattern, `false` otherwise
|
|
25
34
|
*/
|
|
26
35
|
export declare function isOriginAllowed(origin: string): boolean;
|
|
36
|
+
/**
|
|
37
|
+
* Build the CORS response headers for a request origin.
|
|
38
|
+
*
|
|
39
|
+
* Only reflects the origin when it matches the configured allowlist. When no
|
|
40
|
+
* allowlist is configured, or the origin is configured-but-not-allowed, no
|
|
41
|
+
* `Access-Control-Allow-Origin` / `Access-Control-Allow-Credentials` headers
|
|
42
|
+
* are emitted, so a disallowed origin is never reflected back.
|
|
43
|
+
*
|
|
44
|
+
* `Vary: Origin` is always emitted, including on the not-allowed path: the
|
|
45
|
+
* response headers depend on the request `Origin`, so any shared cache (CDN,
|
|
46
|
+
* forward proxy) must key on it or it can serve one origin's grant — or one
|
|
47
|
+
* origin's *absence* of a grant — to a different origin.
|
|
48
|
+
*
|
|
49
|
+
* @param origin - The `Origin` header value from the request (may be empty)
|
|
50
|
+
* @returns The CORS headers to merge into the response
|
|
51
|
+
*/
|
|
52
|
+
export declare function buildCorsHeaders(origin: string): Record<string, string>;
|
|
27
53
|
/**
|
|
28
54
|
* Build a 403 Forbidden response for cross-origin requests from disallowed origins.
|
|
29
55
|
*/
|
|
@@ -33,7 +59,7 @@ export declare function corsRejection(): {
|
|
|
33
59
|
body: string;
|
|
34
60
|
};
|
|
35
61
|
/**
|
|
36
|
-
* Reset the lazy CORS pattern cache. **For testing only.**
|
|
62
|
+
* Reset the lazy CORS pattern cache and the warned-origin set. **For testing only.**
|
|
37
63
|
*/
|
|
38
64
|
export declare function _resetCorsPatterns(): void;
|
|
39
65
|
//# sourceMappingURL=cors.d.ts.map
|
package/dist/cors.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cors.d.ts","sourceRoot":"","sources":["../src/cors.ts"],"names":[],"mappings":"AAGA;;;;;;;;;;GAUG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,EAAE,CAYvD;AAcD;;;;;GAKG;AACH,wBAAgB,eAAe,IAAI,MAAM,EAAE,GAAG,IAAI,CAcjD;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAIvD;AAED;;GAEG;AACH,wBAAgB,aAAa,IAAI;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAMrG;AAED;;GAEG;AACH,wBAAgB,kBAAkB,IAAI,IAAI,
|
|
1
|
+
{"version":3,"file":"cors.d.ts","sourceRoot":"","sources":["../src/cors.ts"],"names":[],"mappings":"AAGA;;;;;;;GAOG;AACH,eAAO,MAAM,YAAY,SAAS,CAAC;AAEnC;;;;;;;;;;GAUG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,EAAE,CAYvD;AAcD;;;;;GAKG;AACH,wBAAgB,eAAe,IAAI,MAAM,EAAE,GAAG,IAAI,CAcjD;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAIvD;AAqBD;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CASvE;AAED;;GAEG;AACH,wBAAgB,aAAa,IAAI;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAMrG;AAED;;GAEG;AACH,wBAAgB,kBAAkB,IAAI,IAAI,CAGzC"}
|