lambda-deadline-middleware 0.0.0 → 1.1.0

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.
Files changed (51) hide show
  1. package/LICENSE +1 -1
  2. package/LICENSES/MIT.txt +18 -0
  3. package/README.md +99 -159
  4. package/REUSE.toml +9 -0
  5. package/SECURITY.md +34 -38
  6. package/dist/context-store.d.ts +2 -2
  7. package/dist/context-store.d.ts.map +1 -1
  8. package/dist/context-store.js +9 -17
  9. package/dist/context-store.js.map +1 -1
  10. package/dist/error.d.ts +4 -4
  11. package/dist/error.d.ts.map +1 -1
  12. package/dist/error.js +2 -2
  13. package/dist/error.js.map +1 -1
  14. package/dist/handler-wrapper.d.ts +5 -4
  15. package/dist/handler-wrapper.d.ts.map +1 -1
  16. package/dist/handler-wrapper.js +2 -4
  17. package/dist/handler-wrapper.js.map +1 -1
  18. package/dist/index.d.ts +2 -2
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +2 -2
  21. package/dist/index.js.map +1 -1
  22. package/dist/middleware.d.ts +7 -10
  23. package/dist/middleware.d.ts.map +1 -1
  24. package/dist/middleware.js +53 -67
  25. package/dist/middleware.js.map +1 -1
  26. package/dist/types.d.ts +1 -19
  27. package/dist/types.d.ts.map +1 -1
  28. package/dist/types.js +2 -12
  29. package/dist/types.js.map +1 -1
  30. package/package.json +39 -33
  31. package/src/context-store.ts +12 -24
  32. package/src/error.ts +6 -6
  33. package/src/handler-wrapper.ts +9 -10
  34. package/src/index.ts +3 -10
  35. package/src/middleware.ts +76 -89
  36. package/src/types.ts +5 -33
  37. package/dist/config.d.ts +0 -4
  38. package/dist/config.d.ts.map +0 -1
  39. package/dist/config.js +0 -15
  40. package/dist/config.js.map +0 -1
  41. package/dist/registration.d.ts +0 -10
  42. package/dist/registration.d.ts.map +0 -1
  43. package/dist/registration.js +0 -23
  44. package/dist/registration.js.map +0 -1
  45. package/dist/telemetry.d.ts +0 -5
  46. package/dist/telemetry.d.ts.map +0 -1
  47. package/dist/telemetry.js +0 -82
  48. package/dist/telemetry.js.map +0 -1
  49. package/src/config.ts +0 -16
  50. package/src/registration.ts +0 -36
  51. package/src/telemetry.ts +0 -129
package/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2024 lambda-deadline-middleware contributors
3
+ Copyright (c) 2026 lambda-deadline-middleware contributors
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
@@ -0,0 +1,18 @@
1
+ MIT License
2
+
3
+ Copyright (c) <year> <copyright holders>
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
6
+ associated documentation files (the "Software"), to deal in the Software without restriction, including
7
+ without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the
9
+ following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be included in all copies or substantial
12
+ portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
15
+ LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO
16
+ EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
17
+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
18
+ USE OR OTHER DEALINGS IN THE SOFTWARE.
package/README.md CHANGED
@@ -1,21 +1,23 @@
1
- <!-- SPDX-FileCopyrightText: 2024 lambda-deadline-middleware contributors -->
1
+ <!-- SPDX-FileCopyrightText: 2026 lambda-deadline-middleware contributors -->
2
2
  <!-- SPDX-License-Identifier: MIT -->
3
3
 
4
4
  # lambda-deadline-middleware
5
5
 
6
- Zero-dependency AWS SDK v3 middleware that automatically propagates Lambda execution deadlines to outgoing SDK calls via `AbortController`-based timeouts.
6
+ Zero-dependency AWS SDK v3 middleware that automatically propagates Lambda execution deadlines to outgoing SDK calls via
7
+ `AbortController`-based timeouts.
7
8
 
8
- When an AWS SDK call hangs inside a Lambda function, the runtime terminates the process at the configured timeout — destroying in-flight OpenTelemetry/X-Ray spans without export. This library prevents that by computing per-request deadlines from the Lambda's remaining execution time and aborting requests before the hard timeout fires.
9
+ When an AWS SDK call hangs inside a Lambda function, the runtime terminates the process at the configured timeout
10
+ without throwing an error or giving your code a chance to react. This library prevents that by computing per-request
11
+ deadlines from the Lambda's remaining execution time and aborting requests before the hard timeout fires.
9
12
 
10
13
  ## Features
11
14
 
12
- - **Automatic deadline propagation** no manual timeout configuration per call
13
- - **Fresh deadline per retry** each SDK retry attempt gets a deadline based on _current_ remaining time
14
- - **Signal composition** preserves caller-provided `AbortSignal` via `AbortSignal.any()`
15
- - **Zero runtime dependencies** — uses `@smithy/types` for compile-time types only
16
- - **Safe outside Lambda** — complete no-op when no Lambda context is available
17
- - **OpenTelemetry integration** optional span events on deadline aborts (detected dynamically)
18
- - **Type-safe** — branded types prevent millisecond/buffer interchange at compile time
15
+ - Automatic deadline propagation, no manual timeout configuration per call
16
+ - Fresh deadline per retry: each SDK retry attempt uses _current_ remaining time
17
+ - Signal composition: preserves caller-provided `AbortSignal` via `AbortSignal.any()`
18
+ - Zero runtime dependencies (`@smithy/types` is compile-time only)
19
+ - Complete no-op when no Lambda context is available
20
+ - Branded types prevent millisecond/buffer interchange at compile time
19
21
 
20
22
  ## Requirements
21
23
 
@@ -28,13 +30,23 @@ When an AWS SDK call hangs inside a Lambda function, the runtime terminates the
28
30
  pnpm add lambda-deadline-middleware
29
31
  ```
30
32
 
31
- ## Quick Start
33
+ ## Usage
34
+
35
+ Setup requires two pieces:
36
+
37
+ 1. **Wrap your handler** with `withLambdaDeadline`. This stores the Lambda `context` (specifically
38
+ `getRemainingTimeInMillis()`) in `AsyncLocalStorage` so the SDK middleware can read it. The SDK middleware stack has
39
+ no access to the Lambda context on its own.
40
+
41
+ 2. **Register the middleware** on each SDK client via the standard `middlewareStack.use()` pattern.
32
42
 
33
43
  ```typescript
34
- import { withLambdaDeadline, withDeadline } from "lambda-deadline-middleware";
35
- import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
44
+ import { withLambdaDeadline, deadlineMiddleware } from "lambda-deadline-middleware";
45
+ import { DynamoDBClient, GetItemCommand } from "@aws-sdk/client-dynamodb";
46
+
47
+ const dynamodb = new DynamoDBClient({});
48
+ dynamodb.middlewareStack.use(deadlineMiddleware());
36
49
 
37
- // 1. Wrap your Lambda handler
38
50
  export const handler = withLambdaDeadline(async (event, context) => {
39
51
  const result = await dynamodb.send(
40
52
  new GetItemCommand({
@@ -43,112 +55,107 @@ export const handler = withLambdaDeadline(async (event, context) => {
43
55
  );
44
56
  return { statusCode: 200, body: JSON.stringify(result) };
45
57
  });
46
-
47
- // 2. Register middleware on SDK clients
48
- const dynamodb = withDeadline(new DynamoDBClient({}));
49
58
  ```
50
59
 
51
- That's it. Every SDK call through `dynamodb` will automatically receive a timeout derived from the Lambda's remaining execution time minus a 1000ms flush buffer.
60
+ Every SDK call through `dynamodb` now receives a timeout derived from the Lambda's remaining execution time minus a
61
+ configurable flush buffer (default: 1000ms).
52
62
 
53
- ## Architecture
63
+ ## How It Works
54
64
 
55
65
  ```mermaid
56
- graph TB
57
- subgraph "Lambda Runtime"
58
- LR[Lambda Runtime] -->|invokes| HW[Handler Wrapper<br/>withLambdaDeadline]
59
- HW -->|stores context| ALS[AsyncLocalStorage<br/>Lambda Context Store]
60
- HW -->|delegates to| UH[User Handler]
61
- UH -->|sends command| SDK[AWS SDK v3 Client]
62
-
63
- subgraph "SDK Middleware Stack"
64
- direction TB
65
- INIT[Initialize Step]
66
- FIN[Finalize Step]
67
- RETRY[Retry Middleware]
68
- DM[Deadline Middleware<br/>← attempt level]
69
- HTTP[HTTP Handler]
70
-
71
- INIT --> FIN --> RETRY
72
- RETRY -->|per attempt| DM --> HTTP
73
- end
74
-
75
- DM -->|reads remaining time| ALS
76
- DM -->|creates| AC[AbortController<br/>+ setTimeout]
77
- AC -->|signal attached to| HTTP
66
+ flowchart LR
67
+ subgraph Lambda Invocation
68
+ direction LR
69
+ A[Lambda Runtime] --> B[withLambdaDeadline]
70
+ B --> C[Your Handler]
71
+ C --> D[SDK .send]
78
72
  end
79
73
 
80
- SDK -->|middleware stack| INIT
74
+ subgraph Per Attempt
75
+ direction LR
76
+ D --> E[Deadline Middleware]
77
+ E -->|getRemainingTimeInMillis\nminus flush buffer| F[AbortController\n+ setTimeout]
78
+ F --> G[HTTP Request]
79
+ end
81
80
 
82
- style DM fill:#f9a825,stroke:#f57f17
83
- style ALS fill:#42a5f5,stroke:#1565c0
84
- style HW fill:#66bb6a,stroke:#2e7d32
81
+ style E fill:#f9a825,stroke:#f57f17
82
+ style B fill:#66bb6a,stroke:#2e7d32
85
83
  ```
86
84
 
87
- The middleware registers at the **attempt level** of the SDK stack it executes once per HTTP request dispatch including retries, computing a fresh deadline each time.
85
+ `withLambdaDeadline` stores the Lambda context in `AsyncLocalStorage`. The deadline middleware reads it on every attempt
86
+ (including retries), computes a fresh timeout, and attaches an `AbortSignal` to the outgoing HTTP request.
88
87
 
89
88
  ## Configuration
90
89
 
91
90
  ### Flush Buffer
92
91
 
93
- The flush buffer is subtracted from the remaining Lambda time to leave room for telemetry export and error handling:
92
+ The flush buffer is subtracted from the remaining Lambda time to leave room for graceful shutdown and error handling:
94
93
 
95
94
  ```typescript
96
- // Default: 1000ms flush buffer
97
- const client = withDeadline(new DynamoDBClient({}));
98
-
99
- // Custom: 500ms flush buffer
100
- const client = withDeadline(new DynamoDBClient({}), { flushBufferMs: 500 });
95
+ // Default: 1000ms
96
+ dynamodb.middlewareStack.use(deadlineMiddleware());
101
97
 
102
- // No buffer: use full remaining time
103
- const client = withDeadline(new DynamoDBClient({}), { flushBufferMs: 0 });
98
+ // Custom: 500ms
99
+ dynamodb.middlewareStack.use(deadlineMiddleware({ flushBufferMs: 500 }));
104
100
  ```
105
101
 
106
- ### Handler-Level Defaults
102
+ ## Error Handling
103
+
104
+ When remaining time is less than or equal to the flush buffer, the middleware throws `DeadlineExceededError` immediately
105
+ without dispatching an HTTP request.
107
106
 
108
107
  ```typescript
109
- export const handler = withLambdaDeadline(
110
- async (event, context) => {
111
- /* ... */
112
- },
113
- { flushBufferMs: 2000, telemetryEnabled: false },
114
- );
108
+ import { isDeadlineExceeded } from "lambda-deadline-middleware";
109
+
110
+ try {
111
+ await dynamodb.send(
112
+ new GetItemCommand({
113
+ /* ... */
114
+ }),
115
+ );
116
+ } catch (error) {
117
+ if (isDeadlineExceeded(error)) {
118
+ console.log(`Deadline exceeded: ${error.deadlineMs}ms`);
119
+ console.log(`Remaining time was: ${error.remainingMs}ms`);
120
+ }
121
+ throw error;
122
+ }
115
123
  ```
116
124
 
117
- ### Telemetry
125
+ ## Signal Composition
118
126
 
119
- OpenTelemetry integration is detected dynamically. If `@opentelemetry/api` is installed, span events are emitted on deadline aborts. Disable with:
127
+ If you pass an `AbortSignal` to a request, the middleware composes both signals:
120
128
 
121
129
  ```typescript
122
- const client = withDeadline(new DynamoDBClient({}), { telemetryEnabled: false });
130
+ const controller = new AbortController();
131
+ setTimeout(() => controller.abort(), 5000);
132
+
133
+ await dynamodb.send(
134
+ new GetItemCommand({
135
+ /* ... */
136
+ }),
137
+ {
138
+ abortSignal: controller.signal,
139
+ },
140
+ );
123
141
  ```
124
142
 
125
143
  ## API Reference
126
144
 
127
- ### `withLambdaDeadline(handler, options?)`
145
+ ### `withLambdaDeadline(handler)`
128
146
 
129
- Wraps a Lambda handler to initialize the context store.
147
+ Wraps a Lambda handler to store the Lambda context in `AsyncLocalStorage`. Required for the middleware to access
148
+ `getRemainingTimeInMillis()`.
130
149
 
131
150
  ```typescript
132
151
  function withLambdaDeadline<TEvent, TResult>(
133
152
  handler: (event: TEvent, context: LambdaContextLike) => Promise<TResult>,
134
- options?: DeadlineOptions,
135
153
  ): (event: TEvent, context: LambdaContextLike) => Promise<TResult>;
136
154
  ```
137
155
 
138
- ### `withDeadline(client, options?)`
139
-
140
- Registers the deadline middleware on an SDK client. Returns the same client instance. Idempotent — subsequent calls on the same client are no-ops.
141
-
142
- ```typescript
143
- function withDeadline<T extends { middlewareStack: { use: Function } }>(
144
- client: T,
145
- options?: DeadlineOptions,
146
- ): T;
147
- ```
148
-
149
156
  ### `deadlineMiddleware(options?)`
150
157
 
151
- Factory returning a `Pluggable` object for `client.middlewareStack.use()`.
158
+ Returns a `Pluggable` for `client.middlewareStack.use()`.
152
159
 
153
160
  ```typescript
154
161
  function deadlineMiddleware(options?: DeadlineOptions): Pluggable<object, object>;
@@ -162,106 +169,39 @@ Accessor for the current Lambda's remaining execution time. Returns `undefined`
162
169
  function getRemainingTimeInMillis(): number | undefined;
163
170
  ```
164
171
 
165
- ### `DeadlineExceededError`
172
+ ### `isDeadlineExceeded(error)`
173
+
174
+ Type guard for deadline-triggered abort errors.
175
+
176
+ ```typescript
177
+ function isDeadlineExceeded(error: unknown): error is DeadlineExceededError;
178
+ ```
166
179
 
167
- Error thrown when a request is aborted due to deadline expiration.
180
+ ### `DeadlineExceededError`
168
181
 
169
182
  ```typescript
170
183
  class DeadlineExceededError extends Error {
171
184
  readonly name: "DeadlineExceededError";
172
185
  readonly deadlineMs: Milliseconds;
173
- readonly flushBufferMs: FlushBufferMs;
186
+ readonly flushBufferMs: Milliseconds;
174
187
  readonly remainingMs: Milliseconds;
175
188
  }
176
189
  ```
177
190
 
178
- ### `isDeadlineExceeded(error)`
179
-
180
- Type guard for deadline-triggered abort errors.
181
-
182
- ```typescript
183
- function isDeadlineExceeded(error: unknown): error is DeadlineExceededError;
184
- ```
185
-
186
191
  ### `DeadlineOptions`
187
192
 
188
193
  ```typescript
189
194
  interface DeadlineOptions {
190
195
  readonly flushBufferMs?: number; // Default: 1000
191
- readonly telemetryEnabled?: boolean; // Default: true
192
196
  }
193
197
  ```
194
198
 
195
199
  ### Types
196
200
 
197
- | Type | Description |
198
- | -------------------------- | ---------------------------------------------------------------------------- |
199
- | `Milliseconds` | Branded number representing a duration in ms |
200
- | `FlushBufferMs` | Branded number for the flush buffer |
201
- | `RequestDeadlineMs` | Branded number for a computed deadline |
202
- | `DeadlineComputation` | Discriminated union: `"deadline"` \| `"insufficient-time"` \| `"no-context"` |
203
- | `DeadlineMiddlewareConfig` | Validated configuration (internal) |
204
- | `LambdaContextLike` | Minimal interface: `{ getRemainingTimeInMillis?(): number }` |
205
-
206
- ## Error Handling
207
-
208
- ```typescript
209
- import { isDeadlineExceeded } from "lambda-deadline-middleware";
210
-
211
- try {
212
- await client.send(
213
- new GetItemCommand({
214
- /* ... */
215
- }),
216
- );
217
- } catch (error) {
218
- if (isDeadlineExceeded(error)) {
219
- console.log(`Deadline exceeded: ${error.deadlineMs}ms`);
220
- console.log(`Remaining time was: ${error.remainingMs}ms`);
221
- // Handle graceful degradation
222
- }
223
- throw error;
224
- }
225
- ```
226
-
227
- When remaining time is less than or equal to the flush buffer, the middleware throws `DeadlineExceededError` immediately without dispatching an HTTP request.
228
-
229
- ## Advanced Usage
230
-
231
- ### Direct Pluggable Registration
232
-
233
- ```typescript
234
- import { deadlineMiddleware } from "lambda-deadline-middleware";
235
-
236
- const client = new S3Client({});
237
- client.middlewareStack.use(deadlineMiddleware({ flushBufferMs: 750 }));
238
- ```
239
-
240
- ### Combining with Existing Abort Signals
241
-
242
- The middleware composes signals — your existing signal is preserved:
243
-
244
- ```typescript
245
- const controller = new AbortController();
246
- setTimeout(() => controller.abort(), 5000); // your own timeout
247
-
248
- await client.send(
249
- new GetItemCommand({
250
- /* ... */
251
- }),
252
- {
253
- abortSignal: controller.signal, // both signals are respected
254
- },
255
- );
256
- ```
257
-
258
- ### Multiple Clients
259
-
260
- ```typescript
261
- const dynamodb = withDeadline(new DynamoDBClient({}));
262
- const sqs = withDeadline(new SQSClient({}), { flushBufferMs: 2000 });
263
- const s3 = withDeadline(new S3Client({}), { flushBufferMs: 500 });
264
- ```
201
+ | Type | Description |
202
+ | ------------------- | ------------------------------------------------------------ |
203
+ | `Milliseconds` | Branded number representing a duration in ms |
204
+ | `LambdaContextLike` | Minimal interface: `{ getRemainingTimeInMillis?(): number }` |
265
205
 
266
206
  ## Reporting Bugs
267
207
 
package/REUSE.toml ADDED
@@ -0,0 +1,9 @@
1
+ version = 1
2
+ SPDX-PackageName = "lambda-deadline-middleware"
3
+ SPDX-PackageSupplier = "https://github.com/mikkopiu/lambda-deadline-middleware/issues"
4
+
5
+ [[annotations]]
6
+ path = ["**/*"]
7
+ precedence = "aggregate"
8
+ SPDX-FileCopyrightText = "2026 lambda-deadline-middleware contributors"
9
+ SPDX-License-Identifier = "MIT"
package/SECURITY.md CHANGED
@@ -1,14 +1,15 @@
1
- <!-- SPDX-FileCopyrightText: 2024 lambda-deadline-middleware contributors -->
1
+ <!-- SPDX-FileCopyrightText: 2026 lambda-deadline-middleware contributors -->
2
2
  <!-- SPDX-License-Identifier: MIT -->
3
3
 
4
4
  # Security
5
5
 
6
- This document describes the supply chain security practices for `lambda-deadline-middleware`.
6
+ Supply chain security practices for `lambda-deadline-middleware`.
7
7
 
8
8
  ## Provenance Verification
9
9
 
10
- Every published release includes SLSA Level 3 provenance attestation, generated
11
- automatically by GitHub Actions using the `--provenance` flag during `npm publish`.
10
+ Every published release includes SLSA Level 3 provenance attestation, generated by GitHub Actions via npm trusted
11
+ publishing (OIDC). No long-lived tokens are used. Authentication and provenance signing use short-lived OIDC
12
+ credentials.
12
13
 
13
14
  ### Verifying provenance
14
15
 
@@ -16,10 +17,10 @@ automatically by GitHub Actions using the `--provenance` flag during `npm publis
16
17
  npm audit signatures
17
18
  ```
18
19
 
19
- This command verifies that the package was:
20
+ This verifies that the package was:
20
21
 
21
22
  - Built from the source repository (not a compromised local machine)
22
- - Published by the expected GitHub Actions workflow
23
+ - Published by the expected GitHub Actions workflow via OIDC
23
24
  - Signed with a valid sigstore certificate tied to the workflow identity
24
25
 
25
26
  You can also inspect the provenance bundle directly:
@@ -30,9 +31,8 @@ npm provenance --package-name lambda-deadline-middleware
30
31
 
31
32
  ## SBOM (Software Bill of Materials)
32
33
 
33
- Each release includes a CycloneDX SBOM (`sbom.cdx.json`) attached as a GitHub
34
- Release asset. The SBOM lists all runtime and development dependencies with their
35
- versions, providing full transparency into the package's dependency tree.
34
+ Each release includes a CycloneDX SBOM (`sbom.cdx.json`) attached as a GitHub Release asset. The SBOM lists all runtime
35
+ and development dependencies with their versions.
36
36
 
37
37
  ### Generating the SBOM locally
38
38
 
@@ -40,7 +40,7 @@ versions, providing full transparency into the package's dependency tree.
40
40
  pnpm run sbom
41
41
  ```
42
42
 
43
- This produces `sbom.cdx.json` in the project root using pnpm's native CycloneDX SBOM generation.
43
+ This produces `sbom.cdx.json` in the project root using pnpm's native CycloneDX generation.
44
44
 
45
45
  ### SBOM format
46
46
 
@@ -49,30 +49,32 @@ This produces `sbom.cdx.json` in the project root using pnpm's native CycloneDX
49
49
  - **Tool**: `pnpm sbom` (native, since pnpm 11)
50
50
  - **Contents**: Component inventory including direct and transitive dependencies
51
51
 
52
- ## Sigstore Signing
52
+ ## Trusted Publishing (OIDC)
53
53
 
54
- All published artifacts are signed using **keyless sigstore signing** via OIDC
55
- identity federation. This means:
54
+ This project uses npm trusted publishing via OpenID Connect (OIDC) identity federation. No long-lived npm tokens
55
+ (`NPM_TOKEN`) are stored as secrets. Authentication to the npm registry uses short-lived OIDC tokens issued by GitHub
56
+ Actions.
56
57
 
57
- - **No long-lived signing keys**: Signing happens via short-lived certificates
58
- issued by Fulcio, tied to the GitHub Actions OIDC identity.
59
- - **Transparency log**: All signatures are recorded in the Rekor transparency log,
60
- providing a tamper-evident audit trail.
61
- - **Verification without publisher keys**: Consumers verify signatures using the
62
- sigstore root of trust, not a publisher-managed key.
58
+ ### How it works
63
59
 
64
- ### How it works in CI
60
+ 1. The release workflow requests an OIDC token from GitHub (via `id-token: write` permission).
61
+ 2. semantic-release exchanges the OIDC token with the npm registry for a short-lived publish credential.
62
+ 3. The package is published with provenance (`publishConfig.provenance: true`).
63
+ 4. Fulcio issues a signing certificate tied to the workflow identity.
64
+ 5. The signature is recorded in the Rekor transparency log.
65
+ 6. npm stores the provenance bundle alongside the package tarball.
65
66
 
66
- 1. The GitHub Actions release workflow requests an OIDC token from GitHub.
67
- 2. The OIDC token is exchanged with Fulcio for a short-lived signing certificate.
68
- 3. The npm package is signed with the certificate during `npm publish --provenance`.
69
- 4. The signature and certificate are recorded in the Rekor transparency log.
70
- 5. npm stores the provenance bundle alongside the package tarball.
67
+ ### Security benefits
68
+
69
+ - **No long-lived secrets**: No `NPM_TOKEN` to leak, rotate, or compromise.
70
+ - **Scoped trust**: Only the specific workflow in this repository can publish.
71
+ - **Automatic provenance**: SLSA Level 3 provenance without extra configuration.
72
+ - **Transparency log**: All signatures are recorded in Rekor for tamper-evident auditing.
73
+ - **No publisher keys needed**: Consumers verify using the sigstore root of trust.
71
74
 
72
75
  ### Verifying signatures
73
76
 
74
- Signature verification is automatic when running `npm audit signatures`. The npm
75
- CLI checks:
77
+ Signature verification is automatic when running `npm audit signatures`. The npm CLI checks:
76
78
 
77
79
  - The certificate was issued by Fulcio
78
80
  - The OIDC identity matches the expected repository and workflow
@@ -81,15 +83,9 @@ CLI checks:
81
83
 
82
84
  ## Reporting Vulnerabilities
83
85
 
84
- If you discover a security vulnerability in this package, please report it
85
- responsibly by opening a [GitHub Security Advisory](https://github.com/mikkopiu/lambda-deadline-middleware/security/advisories/new) on this repository. Do not
86
- file a public issue for security vulnerabilities.
87
-
88
- ### Response Timeline
89
-
90
- We aim to acknowledge vulnerability reports within 72 hours. For critical
91
- issues, we target a fix or mitigation plan within 14 days. Lower-severity
92
- issues are addressed as part of normal maintenance cycles.
86
+ If you discover a security vulnerability, report it by opening a
87
+ [GitHub Security Advisory](https://github.com/mikkopiu/lambda-deadline-middleware/security/advisories/new) on this
88
+ repository. Do not file a public issue for security vulnerabilities.
93
89
 
94
- Resolved vulnerabilities are disclosed publicly via GitHub Security Advisories
95
- once a fix is released.
90
+ Reports are handled on a best-effort basis. Resolved vulnerabilities are disclosed via GitHub Security Advisories once a
91
+ fix is released.
@@ -1,7 +1,7 @@
1
1
  export interface LambdaContextLike {
2
2
  getRemainingTimeInMillis?: () => number;
3
3
  }
4
- export declare function run<T>(context: LambdaContextLike | null | undefined, fn: () => T): T;
5
- export declare function getRemainingTimeInMillis(): number | undefined;
4
+ export declare const run: <T>(context: LambdaContextLike | null | undefined, fn: () => T) => T;
5
+ export declare const getRemainingTimeInMillis: () => number | undefined;
6
6
 
7
7
  //# sourceMappingURL=context-store.d.ts.map
@@ -1 +1 @@
1
- {"mappings":"AAKA,iBAAiB,kBAAkB;CACjC;AACF;AAWA,OAAO,iBAAS,IAAI,GAAG,SAAS,sCAAsC,UAAU,IAAI;AAKpF,OAAO,iBAAS","names":[],"sources":["src/context-store.ts"],"version":3,"sourcesContent":["// SPDX-FileCopyrightText: 2024 lambda-deadline-middleware contributors\n// SPDX-License-Identifier: MIT\n\nimport { AsyncLocalStorage } from \"node:async_hooks\";\n\nexport interface LambdaContextLike {\n getRemainingTimeInMillis?: () => number;\n}\n\n// Sentinel allows AsyncLocalStorage.run() to accept null/undefined context\n// without throwing, while the accessor can distinguish \"no context stored\"\n// from \"context present but missing the method\".\nconst NO_CONTEXT: unique symbol = Symbol(\"no-context\");\n\ntype StoreValue = LambdaContextLike | typeof NO_CONTEXT;\n\nconst contextStorage = new AsyncLocalStorage<StoreValue>();\n\nexport function run<T>(context: LambdaContextLike | null | undefined, fn: () => T): T {\n const value: StoreValue = context ?? NO_CONTEXT;\n return contextStorage.run(value, fn);\n}\n\nexport function getRemainingTimeInMillis(): number | undefined {\n const store = contextStorage.getStore();\n\n if (store === undefined || store === NO_CONTEXT) {\n return undefined;\n }\n\n if (typeof store.getRemainingTimeInMillis !== \"function\") {\n return undefined;\n }\n\n return store.getRemainingTimeInMillis();\n}\n"]}
1
+ {"mappings":"AAOA,iBAAiB,kBAAkB;CACjC;AACF;AAIA,OAAO,cAAM,MAAO,GAAG,SAAS,sCAAsC,UAAU,MAAI;AAKpF,OAAO,cAAM","names":[],"sources":["src/context-store.ts"],"version":3,"sourcesContent":["// SPDX-FileCopyrightText: 2026 lambda-deadline-middleware contributors\n// SPDX-License-Identifier: MIT\n\nimport { AsyncLocalStorage } from \"node:async_hooks\";\n\n// AsyncLocalStorage propagates the Lambda context through the entire async call chain without parameter threading.\n// SDK middleware executes deep in the Smithy stack where we can't pass the context through function signatures.\nexport interface LambdaContextLike {\n getRemainingTimeInMillis?: () => number;\n}\n\nconst contextStorage = new AsyncLocalStorage<LambdaContextLike>();\n\nexport const run = <T>(context: LambdaContextLike | null | undefined, fn: () => T): T => {\n if (context === null || context === undefined) return fn();\n return contextStorage.run(context, fn);\n};\n\nexport const getRemainingTimeInMillis = (): number | undefined => {\n const store = contextStorage.getStore();\n if (store === undefined) return undefined;\n if (typeof store.getRemainingTimeInMillis !== \"function\") return undefined;\n return store.getRemainingTimeInMillis();\n};\n"]}
@@ -1,24 +1,16 @@
1
- // SPDX-FileCopyrightText: 2024 lambda-deadline-middleware contributors
1
+ // SPDX-FileCopyrightText: 2026 lambda-deadline-middleware contributors
2
2
  // SPDX-License-Identifier: MIT
3
3
  import { AsyncLocalStorage } from "node:async_hooks";
4
- // Sentinel allows AsyncLocalStorage.run() to accept null/undefined context
5
- // without throwing, while the accessor can distinguish "no context stored"
6
- // from "context present but missing the method".
7
- const NO_CONTEXT = Symbol("no-context");
8
4
  const contextStorage = new AsyncLocalStorage();
9
- export function run(context, fn) {
10
- const value = context ?? NO_CONTEXT;
11
- return contextStorage.run(value, fn);
12
- }
13
- export function getRemainingTimeInMillis() {
5
+ export const run = (context, fn) => {
6
+ if (context === null || context === undefined) return fn();
7
+ return contextStorage.run(context, fn);
8
+ };
9
+ export const getRemainingTimeInMillis = () => {
14
10
  const store = contextStorage.getStore();
15
- if (store === undefined || store === NO_CONTEXT) {
16
- return undefined;
17
- }
18
- if (typeof store.getRemainingTimeInMillis !== "function") {
19
- return undefined;
20
- }
11
+ if (store === undefined) return undefined;
12
+ if (typeof store.getRemainingTimeInMillis !== "function") return undefined;
21
13
  return store.getRemainingTimeInMillis();
22
- }
14
+ };
23
15
 
24
16
  //# sourceMappingURL=context-store.js.map
@@ -1 +1 @@
1
- {"mappings":";;AAGA,SAAS,yBAAyB;;;;AASlC,MAAM,aAA4B,OAAO,YAAY;AAIrD,MAAM,iBAAiB,IAAI,kBAA8B;AAEzD,OAAO,SAAS,IAAO,SAA+C,IAAgB;CACpF,MAAM,QAAoB,WAAW;CACrC,OAAO,eAAe,IAAI,OAAO,EAAE;AACrC;AAEA,OAAO,SAAS,2BAA+C;CAC7D,MAAM,QAAQ,eAAe,SAAS;CAEtC,IAAI,UAAU,aAAa,UAAU,YAAY;EAC/C,OAAO;CACT;CAEA,IAAI,OAAO,MAAM,6BAA6B,YAAY;EACxD,OAAO;CACT;CAEA,OAAO,MAAM,yBAAyB;AACxC","names":[],"sources":["src/context-store.ts"],"version":3,"sourcesContent":["// SPDX-FileCopyrightText: 2024 lambda-deadline-middleware contributors\n// SPDX-License-Identifier: MIT\n\nimport { AsyncLocalStorage } from \"node:async_hooks\";\n\nexport interface LambdaContextLike {\n getRemainingTimeInMillis?: () => number;\n}\n\n// Sentinel allows AsyncLocalStorage.run() to accept null/undefined context\n// without throwing, while the accessor can distinguish \"no context stored\"\n// from \"context present but missing the method\".\nconst NO_CONTEXT: unique symbol = Symbol(\"no-context\");\n\ntype StoreValue = LambdaContextLike | typeof NO_CONTEXT;\n\nconst contextStorage = new AsyncLocalStorage<StoreValue>();\n\nexport function run<T>(context: LambdaContextLike | null | undefined, fn: () => T): T {\n const value: StoreValue = context ?? NO_CONTEXT;\n return contextStorage.run(value, fn);\n}\n\nexport function getRemainingTimeInMillis(): number | undefined {\n const store = contextStorage.getStore();\n\n if (store === undefined || store === NO_CONTEXT) {\n return undefined;\n }\n\n if (typeof store.getRemainingTimeInMillis !== \"function\") {\n return undefined;\n }\n\n return store.getRemainingTimeInMillis();\n}\n"]}
1
+ {"mappings":";;AAGA,SAAS,yBAAyB;AAQlC,MAAM,iBAAiB,IAAI,kBAAqC;AAEhE,OAAO,MAAM,OAAU,SAA+C,OAAmB;CACvF,IAAI,YAAY,QAAQ,YAAY,WAAW,OAAO,GAAG;CACzD,OAAO,eAAe,IAAI,SAAS,EAAE;AACvC;AAEA,OAAO,MAAM,iCAAqD;CAChE,MAAM,QAAQ,eAAe,SAAS;CACtC,IAAI,UAAU,WAAW,OAAO;CAChC,IAAI,OAAO,MAAM,6BAA6B,YAAY,OAAO;CACjE,OAAO,MAAM,yBAAyB;AACxC","names":[],"sources":["src/context-store.ts"],"version":3,"sourcesContent":["// SPDX-FileCopyrightText: 2026 lambda-deadline-middleware contributors\n// SPDX-License-Identifier: MIT\n\nimport { AsyncLocalStorage } from \"node:async_hooks\";\n\n// AsyncLocalStorage propagates the Lambda context through the entire async call chain without parameter threading.\n// SDK middleware executes deep in the Smithy stack where we can't pass the context through function signatures.\nexport interface LambdaContextLike {\n getRemainingTimeInMillis?: () => number;\n}\n\nconst contextStorage = new AsyncLocalStorage<LambdaContextLike>();\n\nexport const run = <T>(context: LambdaContextLike | null | undefined, fn: () => T): T => {\n if (context === null || context === undefined) return fn();\n return contextStorage.run(context, fn);\n};\n\nexport const getRemainingTimeInMillis = (): number | undefined => {\n const store = contextStorage.getStore();\n if (store === undefined) return undefined;\n if (typeof store.getRemainingTimeInMillis !== \"function\") return undefined;\n return store.getRemainingTimeInMillis();\n};\n"]}
package/dist/error.d.ts CHANGED
@@ -1,17 +1,17 @@
1
- import type { FlushBufferMs, Milliseconds } from "./types.js";
1
+ import type { Milliseconds } from "./types.js";
2
2
  interface DeadlineExceededInit {
3
3
  readonly deadlineMs: Milliseconds;
4
- readonly flushBufferMs: FlushBufferMs;
4
+ readonly flushBufferMs: Milliseconds;
5
5
  readonly remainingMs: Milliseconds;
6
6
  }
7
7
  export declare class DeadlineExceededError extends Error {
8
8
  override readonly name = "DeadlineExceededError";
9
9
  readonly deadlineMs: Milliseconds;
10
- readonly flushBufferMs: FlushBufferMs;
10
+ readonly flushBufferMs: Milliseconds;
11
11
  readonly remainingMs: Milliseconds;
12
12
  constructor(init: DeadlineExceededInit);
13
13
  }
14
- export declare function isDeadlineExceeded(error: unknown): error is DeadlineExceededError;
14
+ export declare const isDeadlineExceeded: (error: unknown) => error is DeadlineExceededError;
15
15
  export {};
16
16
 
17
17
  //# sourceMappingURL=error.d.ts.map
@@ -1 +1 @@
1
- {"mappings":"AAGA,cAAc,eAAe,oBAAoB;UAEvC,qBAAqB;UACpB,YAAY;UACZ,eAAe;UACf,aAAa;AACxB;AAEA,OAAO,cAAM,8BAA8B,MAAM;CAC/C,kBAAkB,OAAO;CACzB,SAAS,YAAY;CACrB,SAAS,eAAe;CACxB,SAAS,aAAa;CAEtB,YAAY,MAAM;AAQpB;AAIA,OAAO,iBAAS,mBAAmB,iBAAiB,SAAS","names":[],"sources":["src/error.ts"],"version":3,"sourcesContent":["// SPDX-FileCopyrightText: 2024 lambda-deadline-middleware contributors\n// SPDX-License-Identifier: MIT\n\nimport type { FlushBufferMs, Milliseconds } from \"./types.js\";\n\ninterface DeadlineExceededInit {\n readonly deadlineMs: Milliseconds;\n readonly flushBufferMs: FlushBufferMs;\n readonly remainingMs: Milliseconds;\n}\n\nexport class DeadlineExceededError extends Error {\n override readonly name = \"DeadlineExceededError\" as const;\n readonly deadlineMs: Milliseconds;\n readonly flushBufferMs: FlushBufferMs;\n readonly remainingMs: Milliseconds;\n\n constructor(init: DeadlineExceededInit) {\n super(\n `Request deadline exceeded: ${init.deadlineMs}ms deadline (${init.flushBufferMs}ms flush buffer)`,\n );\n this.deadlineMs = init.deadlineMs;\n this.flushBufferMs = init.flushBufferMs;\n this.remainingMs = init.remainingMs;\n }\n}\n\n// Structural check rather than instanceof — works across module boundaries\n// and serialization boundaries where prototype chain may be broken.\nexport function isDeadlineExceeded(error: unknown): error is DeadlineExceededError {\n if (error === null || error === undefined) return false;\n if (typeof error !== \"object\") return false;\n return (error as { name?: unknown }).name === \"DeadlineExceededError\";\n}\n"]}
1
+ {"mappings":"AAGA,cAAc,oBAAoB;UAExB,qBAAqB;UACpB,YAAY;UACZ,eAAe;UACf,aAAa;AACxB;AAEA,OAAO,cAAM,8BAA8B,MAAM;CAC/C,kBAAkB,OAAO;CACzB,SAAS,YAAY;CACrB,SAAS,eAAe;CACxB,SAAS,aAAa;CAEtB,YAAY,MAAM;AAQpB;AAIA,OAAO,cAAM,qBAAsB,mBAAiB,SAAS","names":[],"sources":["src/error.ts"],"version":3,"sourcesContent":["// SPDX-FileCopyrightText: 2026 lambda-deadline-middleware contributors\n// SPDX-License-Identifier: MIT\n\nimport type { Milliseconds } from \"./types.js\";\n\ninterface DeadlineExceededInit {\n readonly deadlineMs: Milliseconds;\n readonly flushBufferMs: Milliseconds;\n readonly remainingMs: Milliseconds;\n}\n\nexport class DeadlineExceededError extends Error {\n override readonly name = \"DeadlineExceededError\" as const;\n readonly deadlineMs: Milliseconds;\n readonly flushBufferMs: Milliseconds;\n readonly remainingMs: Milliseconds;\n\n constructor(init: DeadlineExceededInit) {\n super(\n `Request deadline exceeded: ${init.deadlineMs}ms deadline (${init.flushBufferMs}ms flush buffer)`,\n );\n this.deadlineMs = init.deadlineMs;\n this.flushBufferMs = init.flushBufferMs;\n this.remainingMs = init.remainingMs;\n }\n}\n\n// Structural check rather than instanceof — works across module boundaries\n// and serialization boundaries where prototype chain may be broken.\nexport const isDeadlineExceeded = (error: unknown): error is DeadlineExceededError => {\n if (error === null || error === undefined) return false;\n if (typeof error !== \"object\") return false;\n return (error as { name?: unknown }).name === \"DeadlineExceededError\";\n};\n"]}
package/dist/error.js CHANGED
@@ -12,10 +12,10 @@ export class DeadlineExceededError extends Error {
12
12
  }
13
13
  // Structural check rather than instanceof — works across module boundaries
14
14
  // and serialization boundaries where prototype chain may be broken.
15
- export function isDeadlineExceeded(error) {
15
+ export const isDeadlineExceeded = (error) => {
16
16
  if (error === null || error === undefined) return false;
17
17
  if (typeof error !== "object") return false;
18
18
  return error.name === "DeadlineExceededError";
19
- }
19
+ };
20
20
 
21
21
  //# sourceMappingURL=error.js.map
package/dist/error.js.map CHANGED
@@ -1 +1 @@
1
- {"mappings":"AAWA,OAAO,MAAM,8BAA8B,MAAM;CAC/C,AAAkB,OAAO;CACzB,AAAS;CACT,AAAS;CACT,AAAS;CAET,YAAY,MAA4B;EACtC,MACE,8BAA8B,KAAK,WAAW,eAAe,KAAK,cAAc,iBAClF;EACA,KAAK,aAAa,KAAK;EACvB,KAAK,gBAAgB,KAAK;EAC1B,KAAK,cAAc,KAAK;CAC1B;AACF;;;AAIA,OAAO,SAAS,mBAAmB,OAAgD;CACjF,IAAI,UAAU,QAAQ,UAAU,WAAW,OAAO;CAClD,IAAI,OAAO,UAAU,UAAU,OAAO;CACtC,OAAQ,MAA6B,SAAS;AAChD","names":[],"sources":["src/error.ts"],"version":3,"sourcesContent":["// SPDX-FileCopyrightText: 2024 lambda-deadline-middleware contributors\n// SPDX-License-Identifier: MIT\n\nimport type { FlushBufferMs, Milliseconds } from \"./types.js\";\n\ninterface DeadlineExceededInit {\n readonly deadlineMs: Milliseconds;\n readonly flushBufferMs: FlushBufferMs;\n readonly remainingMs: Milliseconds;\n}\n\nexport class DeadlineExceededError extends Error {\n override readonly name = \"DeadlineExceededError\" as const;\n readonly deadlineMs: Milliseconds;\n readonly flushBufferMs: FlushBufferMs;\n readonly remainingMs: Milliseconds;\n\n constructor(init: DeadlineExceededInit) {\n super(\n `Request deadline exceeded: ${init.deadlineMs}ms deadline (${init.flushBufferMs}ms flush buffer)`,\n );\n this.deadlineMs = init.deadlineMs;\n this.flushBufferMs = init.flushBufferMs;\n this.remainingMs = init.remainingMs;\n }\n}\n\n// Structural check rather than instanceof — works across module boundaries\n// and serialization boundaries where prototype chain may be broken.\nexport function isDeadlineExceeded(error: unknown): error is DeadlineExceededError {\n if (error === null || error === undefined) return false;\n if (typeof error !== \"object\") return false;\n return (error as { name?: unknown }).name === \"DeadlineExceededError\";\n}\n"]}
1
+ {"mappings":"AAWA,OAAO,MAAM,8BAA8B,MAAM;CAC/C,AAAkB,OAAO;CACzB,AAAS;CACT,AAAS;CACT,AAAS;CAET,YAAY,MAA4B;EACtC,MACE,8BAA8B,KAAK,WAAW,eAAe,KAAK,cAAc,iBAClF;EACA,KAAK,aAAa,KAAK;EACvB,KAAK,gBAAgB,KAAK;EAC1B,KAAK,cAAc,KAAK;CAC1B;AACF;;;AAIA,OAAO,MAAM,sBAAsB,UAAmD;CACpF,IAAI,UAAU,QAAQ,UAAU,WAAW,OAAO;CAClD,IAAI,OAAO,UAAU,UAAU,OAAO;CACtC,OAAQ,MAA6B,SAAS;AAChD","names":[],"sources":["src/error.ts"],"version":3,"sourcesContent":["// SPDX-FileCopyrightText: 2026 lambda-deadline-middleware contributors\n// SPDX-License-Identifier: MIT\n\nimport type { Milliseconds } from \"./types.js\";\n\ninterface DeadlineExceededInit {\n readonly deadlineMs: Milliseconds;\n readonly flushBufferMs: Milliseconds;\n readonly remainingMs: Milliseconds;\n}\n\nexport class DeadlineExceededError extends Error {\n override readonly name = \"DeadlineExceededError\" as const;\n readonly deadlineMs: Milliseconds;\n readonly flushBufferMs: Milliseconds;\n readonly remainingMs: Milliseconds;\n\n constructor(init: DeadlineExceededInit) {\n super(\n `Request deadline exceeded: ${init.deadlineMs}ms deadline (${init.flushBufferMs}ms flush buffer)`,\n );\n this.deadlineMs = init.deadlineMs;\n this.flushBufferMs = init.flushBufferMs;\n this.remainingMs = init.remainingMs;\n }\n}\n\n// Structural check rather than instanceof — works across module boundaries\n// and serialization boundaries where prototype chain may be broken.\nexport const isDeadlineExceeded = (error: unknown): error is DeadlineExceededError => {\n if (error === null || error === undefined) return false;\n if (typeof error !== \"object\") return false;\n return (error as { name?: unknown }).name === \"DeadlineExceededError\";\n};\n"]}