@wabicloud/turborepo-remote-cache-serverless 0.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.
- package/LICENSE +21 -0
- package/README.md +97 -0
- package/dist/cli.js +90 -0
- package/dist/handler/index.mjs +1 -0
- package/dist/index.cjs +106 -0
- package/dist/index.d.cts +29 -0
- package/dist/index.d.ts +29 -0
- package/dist/index.js +76 -0
- package/package.json +72 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 wabicloud
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# @wabicloud/turborepo-remote-cache-serverless
|
|
2
|
+
|
|
3
|
+
An AWS CDK construct that deploys a fully serverless Turborepo remote cache using S3, Lambda, and Secrets Manager. No servers to manage, scales to zero, costs almost nothing for small teams.
|
|
4
|
+
|
|
5
|
+
## Quick Start
|
|
6
|
+
|
|
7
|
+
### Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install @wabicloud/turborepo-remote-cache-serverless
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
### Add to your CDK stack
|
|
14
|
+
|
|
15
|
+
```typescript
|
|
16
|
+
import { TurborepoRemoteCache } from "@wabicloud/turborepo-remote-cache-serverless";
|
|
17
|
+
|
|
18
|
+
const cache = new TurborepoRemoteCache(this, "TurboCache");
|
|
19
|
+
|
|
20
|
+
new cdk.CfnOutput(this, "TurboCacheUrl", {
|
|
21
|
+
value: cache.functionUrl.url,
|
|
22
|
+
});
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
### Deploy
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
cdk deploy
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### Generate a token
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
npx wabicloud-turbo-cache generate-token \
|
|
35
|
+
--team team_myproject \
|
|
36
|
+
--secret-name turborepo/cache-token \
|
|
37
|
+
--region us-east-1
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### Configure Turborepo
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
export TURBO_API="https://<your-function-url>"
|
|
44
|
+
export TURBO_TOKEN="<generated-token>"
|
|
45
|
+
export TURBO_TEAM="team_myproject"
|
|
46
|
+
|
|
47
|
+
pnpm turbo build --preflight
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Configuration
|
|
51
|
+
|
|
52
|
+
| Prop | Type | Default | Description |
|
|
53
|
+
|------|------|---------|-------------|
|
|
54
|
+
| `expiration` | `Duration` | 30 days | How long cached artifacts are kept |
|
|
55
|
+
| `secretName` | `string` | `turborepo/cache-token` | Secrets Manager secret name |
|
|
56
|
+
|
|
57
|
+
```typescript
|
|
58
|
+
new TurborepoRemoteCache(this, "TurboCache", {
|
|
59
|
+
expiration: cdk.Duration.days(7),
|
|
60
|
+
secretName: "my-project/turbo-token",
|
|
61
|
+
});
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## How It Works
|
|
65
|
+
|
|
66
|
+
The construct creates:
|
|
67
|
+
|
|
68
|
+
- **S3 bucket** - stores cached build artifacts with automatic expiration
|
|
69
|
+
- **Lambda function** with a public Function URL - handles Turborepo's remote cache API
|
|
70
|
+
- **Secrets Manager secret** - holds the JWT signing key for token authentication
|
|
71
|
+
|
|
72
|
+
Turborepo uses `--preflight` mode: it sends an OPTIONS request to get a presigned S3 URL, then uploads/downloads directly to S3. This means the Lambda only handles lightweight auth + URL generation, while S3 handles the heavy lifting.
|
|
73
|
+
|
|
74
|
+
## CLI Reference
|
|
75
|
+
|
|
76
|
+
```
|
|
77
|
+
wabicloud-turbo-cache generate-token
|
|
78
|
+
|
|
79
|
+
Flags:
|
|
80
|
+
--team Team ID (must start with "team_") [required]
|
|
81
|
+
--secret-name Secrets Manager secret name [default: turborepo/cache-token]
|
|
82
|
+
--region AWS region [default: us-east-1]
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Uses the standard AWS credential chain. Set `AWS_PROFILE` for named profiles.
|
|
86
|
+
|
|
87
|
+
## Exposed Properties
|
|
88
|
+
|
|
89
|
+
The construct exposes these for further customization:
|
|
90
|
+
|
|
91
|
+
- `cache.functionUrl` - Lambda Function URL (use `.url` for the endpoint)
|
|
92
|
+
- `cache.secret` - Secrets Manager secret
|
|
93
|
+
- `cache.bucket` - S3 bucket
|
|
94
|
+
|
|
95
|
+
## License
|
|
96
|
+
|
|
97
|
+
MIT
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// bin/cli.ts
|
|
4
|
+
import { SignJWT } from "jose";
|
|
5
|
+
import {
|
|
6
|
+
SecretsManagerClient,
|
|
7
|
+
GetSecretValueCommand
|
|
8
|
+
} from "@aws-sdk/client-secrets-manager";
|
|
9
|
+
function usage() {
|
|
10
|
+
console.error(`Usage: turborepo-remote-cache-serverless generate-token \\
|
|
11
|
+
--team <team_id> \\
|
|
12
|
+
--secret-name <secret_name> \\
|
|
13
|
+
--region <aws_region>
|
|
14
|
+
|
|
15
|
+
Flags:
|
|
16
|
+
--team Team ID (must start with "team_"), e.g. team_myproject
|
|
17
|
+
--secret-name Secrets Manager secret name (default: turborepo/cache-token)
|
|
18
|
+
--region AWS region (default: us-east-1)
|
|
19
|
+
|
|
20
|
+
Uses the standard AWS credential chain (env vars, profiles, instance roles).
|
|
21
|
+
Set AWS_PROFILE to use a named profile.`);
|
|
22
|
+
process.exit(1);
|
|
23
|
+
}
|
|
24
|
+
function parseArgs(argv) {
|
|
25
|
+
const args = argv.slice(2);
|
|
26
|
+
if (args[0] !== "generate-token") {
|
|
27
|
+
console.error(`Unknown command: ${args[0] ?? "(none)"}`);
|
|
28
|
+
usage();
|
|
29
|
+
}
|
|
30
|
+
let team;
|
|
31
|
+
let secretName = "turborepo/cache-token";
|
|
32
|
+
let region = "us-east-1";
|
|
33
|
+
for (let i = 1; i < args.length; i++) {
|
|
34
|
+
switch (args[i]) {
|
|
35
|
+
case "--team":
|
|
36
|
+
team = args[++i];
|
|
37
|
+
break;
|
|
38
|
+
case "--secret-name":
|
|
39
|
+
secretName = args[++i];
|
|
40
|
+
break;
|
|
41
|
+
case "--region":
|
|
42
|
+
region = args[++i];
|
|
43
|
+
break;
|
|
44
|
+
default:
|
|
45
|
+
console.error(`Unknown flag: ${args[i]}`);
|
|
46
|
+
usage();
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
if (!team) {
|
|
50
|
+
console.error("Error: --team is required");
|
|
51
|
+
usage();
|
|
52
|
+
}
|
|
53
|
+
if (!/^team_\w+$/.test(team)) {
|
|
54
|
+
console.error(
|
|
55
|
+
'Error: Team ID must start with "team_" followed by alphanumeric characters'
|
|
56
|
+
);
|
|
57
|
+
console.error("Example: team_wabicloud, team_myproject");
|
|
58
|
+
process.exit(1);
|
|
59
|
+
}
|
|
60
|
+
return { team, secretName, region };
|
|
61
|
+
}
|
|
62
|
+
async function main() {
|
|
63
|
+
const { team, secretName, region } = parseArgs(process.argv);
|
|
64
|
+
const client = new SecretsManagerClient({ region });
|
|
65
|
+
const response = await client.send(
|
|
66
|
+
new GetSecretValueCommand({ SecretId: secretName })
|
|
67
|
+
);
|
|
68
|
+
const jwtSecret = response.SecretString;
|
|
69
|
+
if (!jwtSecret) {
|
|
70
|
+
console.error(
|
|
71
|
+
"Error: Could not retrieve JWT secret from Secrets Manager"
|
|
72
|
+
);
|
|
73
|
+
process.exit(1);
|
|
74
|
+
}
|
|
75
|
+
const secretKey = new TextEncoder().encode(jwtSecret);
|
|
76
|
+
const token = await new SignJWT({ teamId: team }).setProtectedHeader({ alg: "HS256" }).setIssuedAt().sign(secretKey);
|
|
77
|
+
console.log("\n=== Turborepo Remote Cache Token ===\n");
|
|
78
|
+
console.log(`Team: ${team}
|
|
79
|
+
`);
|
|
80
|
+
console.log("Add these to your environment:\n");
|
|
81
|
+
console.log(`export TURBO_TOKEN="${token}"`);
|
|
82
|
+
console.log(`export TURBO_TEAM="${team}"`);
|
|
83
|
+
console.log(
|
|
84
|
+
"\nSet TURBO_API to your Function URL, then run: pnpm turbo build --preflight\n"
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
main().catch((err) => {
|
|
88
|
+
console.error("Error:", err.message);
|
|
89
|
+
process.exit(1);
|
|
90
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{S3Client as h,HeadObjectCommand as p}from"@aws-sdk/client-s3";import{S3RequestPresigner as y}from"@aws-sdk/s3-request-presigner";import{Hash as C}from"@smithy/hash-node";import{HttpRequest as w}from"@smithy/protocol-http";import{parseUrl as E}from"@smithy/url-parser";import{formatUrl as S}from"@aws-sdk/util-format-url";import{SecretsManagerClient as T,GetSecretValueCommand as b}from"@aws-sdk/client-secrets-manager";import{fromNodeProviderChain as P}from"@aws-sdk/credential-providers";import*as f from"jose";var R=new h({}),N=new T({}),g=process.env.CACHE_BUCKET,l=process.env.AWS_REGION||"eu-central-1",I=process.env.TURBO_TOKEN_SECRET_ARN,U=3600,d=null;async function A(){return d||(d=(await N.send(new b({SecretId:I}))).SecretString,d)}async function O(e,n){let r=e.replace("Bearer ",""),s=new TextEncoder().encode(n),{payload:o}=await f.jwtVerify(r,s),t=o.teamId;if(!t||!/^team_\w+$/.test(t))throw new Error("Invalid token: missing or invalid teamId");return{teamId:t}}async function q(e,n,r){let s=new y({credentials:P(),region:l,sha256:C.bind(null,"sha256")}),o=E(`https://${g}.s3.${l}.amazonaws.com/${n}`),t=new w({...o,method:e});t.query={...t.query,slug:r};let a=await s.presign(t,{expiresIn:U});return S(a).replace("&slug="+encodeURIComponent(r),"")}var k=async e=>{let{method:n,path:r}=e.requestContext.http;if(n==="GET"&&r==="/v8/artifacts/status")return{statusCode:200,body:JSON.stringify({enabled:!0})};if(n==="POST"&&r==="/v8/artifacts/events")return{statusCode:200,body:"{}"};let s=r.match(/^\/v8\/artifacts\/([a-f0-9]+)$/);if(!s)return{statusCode:404,body:"Not found"};let o=await A(),t=e.headers.authorization||"",a;try{a=(await O(t,o)).teamId}catch{return{statusCode:401,body:"Unauthorized"}}let u=s[1],m=`${a}/${u}`;if(n==="OPTIONS"){let i=e.headers["access-control-request-method"]||e.headers["Access-Control-Request-Method"];if(i==="GET")try{await R.send(new p({Bucket:g,Key:m}))}catch(c){if(c instanceof Error&&(c.name==="NotFound"||c.name==="NoSuchKey"))return{statusCode:404,body:"Not found"};throw c}return i==="GET"||i==="PUT"?{statusCode:200,headers:{location:await q(i,m,a),"Access-Control-Allow-Origin":"*","Access-Control-Allow-Methods":"GET, PUT, OPTIONS","Access-Control-Allow-Headers":"Content-Type, User-Agent, x-artifact-duration, x-artifact-tag"},body:""}:{statusCode:404,body:"Not found"}}return{statusCode:404,body:"Not found"}};export{k as handler};
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/index.ts
|
|
31
|
+
var index_exports = {};
|
|
32
|
+
__export(index_exports, {
|
|
33
|
+
TurborepoRemoteCache: () => TurborepoRemoteCache
|
|
34
|
+
});
|
|
35
|
+
module.exports = __toCommonJS(index_exports);
|
|
36
|
+
|
|
37
|
+
// src/construct.ts
|
|
38
|
+
var cdk = __toESM(require("aws-cdk-lib"), 1);
|
|
39
|
+
var s3 = __toESM(require("aws-cdk-lib/aws-s3"), 1);
|
|
40
|
+
var lambda = __toESM(require("aws-cdk-lib/aws-lambda"), 1);
|
|
41
|
+
var logs = __toESM(require("aws-cdk-lib/aws-logs"), 1);
|
|
42
|
+
var secretsmanager = __toESM(require("aws-cdk-lib/aws-secretsmanager"), 1);
|
|
43
|
+
var import_constructs = require("constructs");
|
|
44
|
+
var import_node_path = require("path");
|
|
45
|
+
var TurborepoRemoteCache = class extends import_constructs.Construct {
|
|
46
|
+
/** The Lambda Function URL endpoint (use as TURBO_API). */
|
|
47
|
+
functionUrl;
|
|
48
|
+
/** The Secrets Manager secret holding the JWT signing key. */
|
|
49
|
+
secret;
|
|
50
|
+
/** The S3 bucket storing cached artifacts. */
|
|
51
|
+
bucket;
|
|
52
|
+
constructor(scope, id, props = {}) {
|
|
53
|
+
super(scope, id);
|
|
54
|
+
const {
|
|
55
|
+
expiration = cdk.Duration.days(30),
|
|
56
|
+
secretName = "turborepo/cache-token"
|
|
57
|
+
} = props;
|
|
58
|
+
this.bucket = new s3.Bucket(this, "CacheBucket", {
|
|
59
|
+
encryption: s3.BucketEncryption.S3_MANAGED,
|
|
60
|
+
blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
|
|
61
|
+
lifecycleRules: [{ expiration }],
|
|
62
|
+
removalPolicy: cdk.RemovalPolicy.DESTROY,
|
|
63
|
+
autoDeleteObjects: true,
|
|
64
|
+
cors: [
|
|
65
|
+
{
|
|
66
|
+
allowedMethods: [s3.HttpMethods.GET, s3.HttpMethods.PUT],
|
|
67
|
+
allowedOrigins: ["*"],
|
|
68
|
+
allowedHeaders: ["*"],
|
|
69
|
+
maxAge: 3e3
|
|
70
|
+
}
|
|
71
|
+
]
|
|
72
|
+
});
|
|
73
|
+
this.secret = new secretsmanager.Secret(this, "TurboToken", {
|
|
74
|
+
secretName,
|
|
75
|
+
generateSecretString: {
|
|
76
|
+
excludePunctuation: true,
|
|
77
|
+
passwordLength: 32
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
const cacheHandler = new lambda.Function(this, "CacheHandler", {
|
|
81
|
+
runtime: lambda.Runtime.NODEJS_22_X,
|
|
82
|
+
handler: "index.handler",
|
|
83
|
+
code: lambda.Code.fromAsset((0, import_node_path.join)(__dirname, "handler")),
|
|
84
|
+
memorySize: 1024,
|
|
85
|
+
timeout: cdk.Duration.seconds(30),
|
|
86
|
+
environment: {
|
|
87
|
+
CACHE_BUCKET: this.bucket.bucketName,
|
|
88
|
+
TURBO_TOKEN_SECRET_ARN: this.secret.secretArn
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
new logs.LogGroup(this, "CacheHandlerLogGroup", {
|
|
92
|
+
logGroupName: `/aws/lambda/${cacheHandler.functionName}`,
|
|
93
|
+
retention: logs.RetentionDays.ONE_MONTH,
|
|
94
|
+
removalPolicy: cdk.RemovalPolicy.DESTROY
|
|
95
|
+
});
|
|
96
|
+
this.bucket.grantReadWrite(cacheHandler);
|
|
97
|
+
this.secret.grantRead(cacheHandler);
|
|
98
|
+
this.functionUrl = cacheHandler.addFunctionUrl({
|
|
99
|
+
authType: lambda.FunctionUrlAuthType.NONE
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
104
|
+
0 && (module.exports = {
|
|
105
|
+
TurborepoRemoteCache
|
|
106
|
+
});
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import * as cdk from 'aws-cdk-lib';
|
|
2
|
+
import * as s3 from 'aws-cdk-lib/aws-s3';
|
|
3
|
+
import * as lambda from 'aws-cdk-lib/aws-lambda';
|
|
4
|
+
import * as secretsmanager from 'aws-cdk-lib/aws-secretsmanager';
|
|
5
|
+
import { Construct } from 'constructs';
|
|
6
|
+
|
|
7
|
+
interface TurborepoRemoteCacheProps {
|
|
8
|
+
/**
|
|
9
|
+
* How long cached artifacts are kept before automatic deletion.
|
|
10
|
+
* @default Duration.days(30)
|
|
11
|
+
*/
|
|
12
|
+
readonly expiration?: cdk.Duration;
|
|
13
|
+
/**
|
|
14
|
+
* Name of the Secrets Manager secret that stores the JWT signing key.
|
|
15
|
+
* @default 'turborepo/cache-token'
|
|
16
|
+
*/
|
|
17
|
+
readonly secretName?: string;
|
|
18
|
+
}
|
|
19
|
+
declare class TurborepoRemoteCache extends Construct {
|
|
20
|
+
/** The Lambda Function URL endpoint (use as TURBO_API). */
|
|
21
|
+
readonly functionUrl: lambda.FunctionUrl;
|
|
22
|
+
/** The Secrets Manager secret holding the JWT signing key. */
|
|
23
|
+
readonly secret: secretsmanager.Secret;
|
|
24
|
+
/** The S3 bucket storing cached artifacts. */
|
|
25
|
+
readonly bucket: s3.Bucket;
|
|
26
|
+
constructor(scope: Construct, id: string, props?: TurborepoRemoteCacheProps);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export { TurborepoRemoteCache, type TurborepoRemoteCacheProps };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import * as cdk from 'aws-cdk-lib';
|
|
2
|
+
import * as s3 from 'aws-cdk-lib/aws-s3';
|
|
3
|
+
import * as lambda from 'aws-cdk-lib/aws-lambda';
|
|
4
|
+
import * as secretsmanager from 'aws-cdk-lib/aws-secretsmanager';
|
|
5
|
+
import { Construct } from 'constructs';
|
|
6
|
+
|
|
7
|
+
interface TurborepoRemoteCacheProps {
|
|
8
|
+
/**
|
|
9
|
+
* How long cached artifacts are kept before automatic deletion.
|
|
10
|
+
* @default Duration.days(30)
|
|
11
|
+
*/
|
|
12
|
+
readonly expiration?: cdk.Duration;
|
|
13
|
+
/**
|
|
14
|
+
* Name of the Secrets Manager secret that stores the JWT signing key.
|
|
15
|
+
* @default 'turborepo/cache-token'
|
|
16
|
+
*/
|
|
17
|
+
readonly secretName?: string;
|
|
18
|
+
}
|
|
19
|
+
declare class TurborepoRemoteCache extends Construct {
|
|
20
|
+
/** The Lambda Function URL endpoint (use as TURBO_API). */
|
|
21
|
+
readonly functionUrl: lambda.FunctionUrl;
|
|
22
|
+
/** The Secrets Manager secret holding the JWT signing key. */
|
|
23
|
+
readonly secret: secretsmanager.Secret;
|
|
24
|
+
/** The S3 bucket storing cached artifacts. */
|
|
25
|
+
readonly bucket: s3.Bucket;
|
|
26
|
+
constructor(scope: Construct, id: string, props?: TurborepoRemoteCacheProps);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export { TurborepoRemoteCache, type TurborepoRemoteCacheProps };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// node_modules/.pnpm/tsup@8.5.1_postcss@8.5.6_typescript@5.9.3/node_modules/tsup/assets/esm_shims.js
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { fileURLToPath } from "url";
|
|
4
|
+
var getFilename = () => fileURLToPath(import.meta.url);
|
|
5
|
+
var getDirname = () => path.dirname(getFilename());
|
|
6
|
+
var __dirname = /* @__PURE__ */ getDirname();
|
|
7
|
+
|
|
8
|
+
// src/construct.ts
|
|
9
|
+
import * as cdk from "aws-cdk-lib";
|
|
10
|
+
import * as s3 from "aws-cdk-lib/aws-s3";
|
|
11
|
+
import * as lambda from "aws-cdk-lib/aws-lambda";
|
|
12
|
+
import * as logs from "aws-cdk-lib/aws-logs";
|
|
13
|
+
import * as secretsmanager from "aws-cdk-lib/aws-secretsmanager";
|
|
14
|
+
import { Construct } from "constructs";
|
|
15
|
+
import { join } from "path";
|
|
16
|
+
var TurborepoRemoteCache = class extends Construct {
|
|
17
|
+
/** The Lambda Function URL endpoint (use as TURBO_API). */
|
|
18
|
+
functionUrl;
|
|
19
|
+
/** The Secrets Manager secret holding the JWT signing key. */
|
|
20
|
+
secret;
|
|
21
|
+
/** The S3 bucket storing cached artifacts. */
|
|
22
|
+
bucket;
|
|
23
|
+
constructor(scope, id, props = {}) {
|
|
24
|
+
super(scope, id);
|
|
25
|
+
const {
|
|
26
|
+
expiration = cdk.Duration.days(30),
|
|
27
|
+
secretName = "turborepo/cache-token"
|
|
28
|
+
} = props;
|
|
29
|
+
this.bucket = new s3.Bucket(this, "CacheBucket", {
|
|
30
|
+
encryption: s3.BucketEncryption.S3_MANAGED,
|
|
31
|
+
blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
|
|
32
|
+
lifecycleRules: [{ expiration }],
|
|
33
|
+
removalPolicy: cdk.RemovalPolicy.DESTROY,
|
|
34
|
+
autoDeleteObjects: true,
|
|
35
|
+
cors: [
|
|
36
|
+
{
|
|
37
|
+
allowedMethods: [s3.HttpMethods.GET, s3.HttpMethods.PUT],
|
|
38
|
+
allowedOrigins: ["*"],
|
|
39
|
+
allowedHeaders: ["*"],
|
|
40
|
+
maxAge: 3e3
|
|
41
|
+
}
|
|
42
|
+
]
|
|
43
|
+
});
|
|
44
|
+
this.secret = new secretsmanager.Secret(this, "TurboToken", {
|
|
45
|
+
secretName,
|
|
46
|
+
generateSecretString: {
|
|
47
|
+
excludePunctuation: true,
|
|
48
|
+
passwordLength: 32
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
const cacheHandler = new lambda.Function(this, "CacheHandler", {
|
|
52
|
+
runtime: lambda.Runtime.NODEJS_22_X,
|
|
53
|
+
handler: "index.handler",
|
|
54
|
+
code: lambda.Code.fromAsset(join(__dirname, "handler")),
|
|
55
|
+
memorySize: 1024,
|
|
56
|
+
timeout: cdk.Duration.seconds(30),
|
|
57
|
+
environment: {
|
|
58
|
+
CACHE_BUCKET: this.bucket.bucketName,
|
|
59
|
+
TURBO_TOKEN_SECRET_ARN: this.secret.secretArn
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
new logs.LogGroup(this, "CacheHandlerLogGroup", {
|
|
63
|
+
logGroupName: `/aws/lambda/${cacheHandler.functionName}`,
|
|
64
|
+
retention: logs.RetentionDays.ONE_MONTH,
|
|
65
|
+
removalPolicy: cdk.RemovalPolicy.DESTROY
|
|
66
|
+
});
|
|
67
|
+
this.bucket.grantReadWrite(cacheHandler);
|
|
68
|
+
this.secret.grantRead(cacheHandler);
|
|
69
|
+
this.functionUrl = cacheHandler.addFunctionUrl({
|
|
70
|
+
authType: lambda.FunctionUrlAuthType.NONE
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
export {
|
|
75
|
+
TurborepoRemoteCache
|
|
76
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@wabicloud/turborepo-remote-cache-serverless",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "AWS CDK construct for deploying a Turborepo remote cache backed by S3 and Lambda",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.cjs",
|
|
7
|
+
"module": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"import": {
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"default": "./dist/index.js"
|
|
14
|
+
},
|
|
15
|
+
"require": {
|
|
16
|
+
"types": "./dist/index.d.cts",
|
|
17
|
+
"default": "./dist/index.cjs"
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"bin": {
|
|
22
|
+
"wabicloud-turbo-cache": "./dist/cli.js"
|
|
23
|
+
},
|
|
24
|
+
"files": [
|
|
25
|
+
"dist"
|
|
26
|
+
],
|
|
27
|
+
"scripts": {
|
|
28
|
+
"build": "tsup",
|
|
29
|
+
"test": "vitest run",
|
|
30
|
+
"test:watch": "vitest",
|
|
31
|
+
"typecheck": "tsc --noEmit",
|
|
32
|
+
"prepublishOnly": "pnpm build"
|
|
33
|
+
},
|
|
34
|
+
"keywords": [
|
|
35
|
+
"turborepo",
|
|
36
|
+
"remote-cache",
|
|
37
|
+
"aws",
|
|
38
|
+
"cdk",
|
|
39
|
+
"serverless",
|
|
40
|
+
"s3",
|
|
41
|
+
"lambda"
|
|
42
|
+
],
|
|
43
|
+
"license": "MIT",
|
|
44
|
+
"peerDependencies": {
|
|
45
|
+
"aws-cdk-lib": "^2.0.0",
|
|
46
|
+
"constructs": "^10.0.0"
|
|
47
|
+
},
|
|
48
|
+
"dependencies": {
|
|
49
|
+
"jose": "^6.0.0"
|
|
50
|
+
},
|
|
51
|
+
"devDependencies": {
|
|
52
|
+
"@aws-sdk/client-s3": "^3.0.0",
|
|
53
|
+
"@aws-sdk/client-secrets-manager": "^3.0.0",
|
|
54
|
+
"@aws-sdk/credential-providers": "^3.0.0",
|
|
55
|
+
"@aws-sdk/s3-request-presigner": "^3.0.0",
|
|
56
|
+
"@aws-sdk/util-format-url": "^3.0.0",
|
|
57
|
+
"@smithy/hash-node": "^4.0.0",
|
|
58
|
+
"@smithy/protocol-http": "^5.0.0",
|
|
59
|
+
"@smithy/url-parser": "^4.0.0",
|
|
60
|
+
"@types/node": "^25.3.0",
|
|
61
|
+
"aws-cdk-lib": "^2.0.0",
|
|
62
|
+
"constructs": "^10.0.0",
|
|
63
|
+
"tsup": "^8.0.0",
|
|
64
|
+
"typescript": "^5.0.0",
|
|
65
|
+
"vitest": "^3.0.0"
|
|
66
|
+
},
|
|
67
|
+
"pnpm": {
|
|
68
|
+
"onlyBuiltDependencies": [
|
|
69
|
+
"esbuild"
|
|
70
|
+
]
|
|
71
|
+
}
|
|
72
|
+
}
|