├── .archetect-history
├── .env
├── .env.live
├── .env.nonlive
├── .eslintrc.json
├── .github
├── ISSUE_TEMPLATE
│ ├── bug_report.md
│ └── feature_request.md
└── workflows
│ ├── live.yml
│ └── nonlive.yml
├── .gitignore
├── .openapis
├── .prettierrc
├── .scaffoldly
├── .env
├── .env.live
├── .env.nonlive
├── env-vars.json
├── live
│ ├── env-vars.json
│ └── services.json
├── nonlive
│ ├── env-vars.json
│ └── services.json
└── services.json
├── .vscode
├── launch.json
├── settings.json
└── tasks.json
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── openapitools.json
├── package.json
├── public
└── swagger.html
├── scripts
└── prepare.sh
├── serverless.config.js
├── serverless.yml
├── src
├── app.ts
├── auth.ts
├── controllers
│ ├── ControllerV1.ts
│ └── HealthController.ts
├── interfaces
│ └── errors.ts
├── lambda.ts
├── models
│ ├── IdentityModel.ts
│ ├── StateLockModel.ts
│ ├── StateLockRequestModel.ts
│ ├── StateModel.ts
│ └── schemas
│ │ ├── EncryptedField.ts
│ │ ├── Identity.ts
│ │ ├── State.ts
│ │ ├── StateLock.ts
│ │ └── StateLockRequest.ts
└── services
│ ├── GithubService.ts
│ └── StateService.ts
├── tsconfig.json
├── tsoa.js
├── types.ts
└── yarn.lock
/.archetect-history:
--------------------------------------------------------------------------------
1 | 2012857519:
2 | source: 'https://github.com/scaffoldly/archetype-express-serverless-rest-api.git#1.0.0-47'
3 | options: ' -s overwrite -s github -s serverless -s entity -a repository-name=github-sls-rest-api -a persistence=dynamodb -a entities=example -a auth=true'
4 |
--------------------------------------------------------------------------------
/.env:
--------------------------------------------------------------------------------
1 | # Service-specific environment variables
2 | # These are set in every enviornment unless overridden
3 |
4 | LOCALSTACK=true
5 | AWS_XRAY_CONTEXT_MISSING=LOG_ERROR
6 | AWS_XRAY_LOG_LEVEL=silent
7 |
--------------------------------------------------------------------------------
/.env.live:
--------------------------------------------------------------------------------
1 | # Service-specific environment variables
2 | # Overrides .env
3 |
4 | LOCALSTACK=false
5 |
--------------------------------------------------------------------------------
/.env.nonlive:
--------------------------------------------------------------------------------
1 | # Service-specific environment variables
2 | # Overrides .env
3 |
4 | LOCALSTACK=false
5 |
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "parser": "@typescript-eslint/parser",
3 | "plugins": ["@typescript-eslint"],
4 | "parserOptions": {
5 | "ecmaVersion": 2020,
6 | "sourceType": "module",
7 | "project": "./tsconfig.json"
8 | },
9 | "extends": [
10 | "eslint:recommended",
11 | "airbnb-typescript/base",
12 | "plugin:@typescript-eslint/eslint-recommended",
13 | "plugin:@typescript-eslint/recommended",
14 | "prettier",
15 | "plugin:prettier/recommended",
16 | "plugin:import/recommended"
17 | ],
18 | "rules": {
19 | "no-console": "off",
20 | "class-methods-use-this": "off",
21 | "import/prefer-default-export": "off",
22 | "@typescript-eslint/no-explicit-any": "off"
23 | },
24 | "ignorePatterns": [
25 | // Auto-generated files
26 | "src/env.ts",
27 | "src/routes.ts",
28 | "src/models/interfaces/**/*.ts",
29 | "src/services/openapi/**/*.ts"
30 | ]
31 | }
32 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | title: "[BUG] "
5 | labels: bug
6 | assignees: cnuss
7 |
8 | ---
9 |
10 | **Describe the bug**
11 | A clear and concise description of what the bug is.
12 |
13 | **To Reproduce**
14 | Steps to reproduce the behavior:
15 | 1.
16 |
17 | **Expected behavior**
18 | A clear and concise description of what you expected to happen.
19 |
20 | **Screenshots**
21 | If applicable, add screenshots to help explain your problem.
22 |
23 | **Desktop (please complete the following information):**
24 | - OS:
25 | - Terraform Version:
26 |
27 | **Additional context**
28 | Add any other context about the problem here.
29 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 | title: "[FEATURE REQUEST}"
5 | labels: enhancement
6 | assignees: cnuss
7 |
8 | ---
9 |
10 | **Is your feature request related to a problem? Please describe.**
11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
12 |
13 | **Describe the solution you'd like**
14 | A clear and concise description of what you want to happen.
15 |
16 | **Describe alternatives you've considered**
17 | A clear and concise description of any alternative solutions or features you've considered.
18 |
19 | **Additional context**
20 | Add any other context or screenshots about the feature request here.
21 |
--------------------------------------------------------------------------------
/.github/workflows/live.yml:
--------------------------------------------------------------------------------
1 | name: "Live Deploy"
2 |
3 | on:
4 | workflow_dispatch:
5 | release:
6 | types:
7 | - published
8 |
9 | env:
10 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
11 | AWS_PARTITION: ${{ secrets.LIVE_AWS_PARTITION }}
12 | AWS_ACCOUNT_ID: ${{ secrets.LIVE_AWS_ACCOUNT_ID }}
13 | AWS_ACCESS_KEY_ID: ${{ secrets.LIVE_AWS_ACCESS_KEY_ID }}
14 | AWS_SECRET_ACCESS_KEY: ${{ secrets.LIVE_AWS_SECRET_ACCESS_KEY }}
15 | AWS_REST_API_ID: ${{ secrets.LIVE_AWS_REST_API_ID }}
16 | AWS_REST_API_ROOT_RESOURCE_ID: ${{ secrets.LIVE_AWS_REST_API_ROOT_RESOURCE_ID }}
17 | NODE_ENV: live
18 |
19 | jobs:
20 | deploy:
21 | runs-on: ubuntu-latest
22 | if: >-
23 | !startsWith(github.event.head_commit.message, 'Initial commit') &&
24 | !startsWith(github.event.head_commit.message, '🤖')
25 | steps:
26 | - uses: actions/checkout@v2
27 | - uses: actions/setup-node@v2-beta
28 | with:
29 | node-version: '14'
30 | - uses: actions/cache@v2
31 | with:
32 | path: ./node_modules
33 | key: ${{ runner.os }}-yarn-${{ hashFiles('./yarn.lock') }}
34 | restore-keys: |
35 | ${{ runner.os }}-yarn-
36 | - uses: actions/cache@v2
37 | with:
38 | path: ./src/services/openapi
39 | key: ${{ runner.os }}-openapi-${{ hashFiles('./.openapis') }}
40 | restore-keys: |
41 | ${{ runner.os }}-openapi-
42 | - run: yarn
43 | - uses: scaffoldly/bump-version-action@v1
44 | with:
45 | action: postrelease
46 | version-file: package.json
47 | repo-token: ${{ secrets.GITHUB_TOKEN }}
48 | - run: yarn deploy --stage $NODE_ENV
49 |
--------------------------------------------------------------------------------
/.github/workflows/nonlive.yml:
--------------------------------------------------------------------------------
1 | name: "Nonlive Deploy"
2 |
3 | on:
4 | workflow_dispatch:
5 | push:
6 | branches:
7 | - main
8 |
9 | env:
10 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
11 | AWS_PARTITION: ${{ secrets.NONLIVE_AWS_PARTITION }}
12 | AWS_ACCOUNT_ID: ${{ secrets.NONLIVE_AWS_ACCOUNT_ID }}
13 | AWS_ACCESS_KEY_ID: ${{ secrets.NONLIVE_AWS_ACCESS_KEY_ID }}
14 | AWS_SECRET_ACCESS_KEY: ${{ secrets.NONLIVE_AWS_SECRET_ACCESS_KEY }}
15 | AWS_REST_API_ID: ${{ secrets.NONLIVE_AWS_REST_API_ID }}
16 | AWS_REST_API_ROOT_RESOURCE_ID: ${{ secrets.NONLIVE_AWS_REST_API_ROOT_RESOURCE_ID }}
17 | NODE_ENV: nonlive
18 |
19 | jobs:
20 | deploy:
21 | runs-on: ubuntu-latest
22 | if: >-
23 | !startsWith(github.event.head_commit.message, 'Initial commit') &&
24 | !startsWith(github.event.head_commit.message, '🤖')
25 | steps:
26 | - uses: actions/checkout@v2
27 | - uses: actions/setup-node@v2-beta
28 | with:
29 | node-version: '14'
30 | - uses: actions/cache@v2
31 | with:
32 | path: ./node_modules
33 | key: ${{ runner.os }}-yarn-${{ hashFiles('./yarn.lock') }}
34 | restore-keys: |
35 | ${{ runner.os }}-yarn-
36 | - uses: actions/cache@v2
37 | with:
38 | path: ./src/services/openapi
39 | key: ${{ runner.os }}-openapi-${{ hashFiles('./.openapis') }}
40 | restore-keys: |
41 | ${{ runner.os }}-openapi-
42 | - run: yarn
43 | - uses: scaffoldly/bump-version-action@v1
44 | with:
45 | action: prerelease
46 | version-file: package.json
47 | repo-token: ${{ secrets.GITHUB_TOKEN }}
48 | - run: yarn deploy --stage $NODE_ENV
49 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | yarn-error.log
3 | .serverless/
4 | .webpack/
5 | .dynamodb/
6 | .build/
7 | .DS_Store
8 |
9 | # Autogenerated files
10 | src/env.ts
11 | src/models/interfaces
12 | src/services/openapi
13 | src/routes.ts
14 | src/swagger.json
15 |
--------------------------------------------------------------------------------
/.openapis:
--------------------------------------------------------------------------------
1 |
2 | # Do not edit this file, it is managed by @scaffoldly/openapi-generator
3 | #
4 | # This file assists caching of auto-generated APIs in `src/services/openapi` during builds
5 | #
6 | # This file is *safe* to add to source control and will increase the speed of builds
7 | ---
8 | - serviceName: auth-sls-rest-api
9 | version: 1.0.1-6
10 |
11 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "semi": true,
3 | "trailingComma": "all",
4 | "singleQuote": true,
5 | "printWidth": 100,
6 | "tabWidth": 2
7 | }
8 |
--------------------------------------------------------------------------------
/.scaffoldly/.env:
--------------------------------------------------------------------------------
1 | api-gateway-domain="api-nonlive.tfstate.dev"
2 | api-gateway-websocket="false"
3 | api-gateway-websocket-domain="null"
4 | stage-domain="tfstate.dev"
5 | subdomain="api"
6 | subdomain-suffix="nonlive"
7 | zone-id="Z0754609150R99BSVKDUQ"
8 | key-alias="alias/nonlive"
9 | key-id="ab356c75-5a89-4363-9186-921ed12e0d2c"
10 | mail-domain="slyses-nonlive.tfstate.dev"
11 | noreply-address="no-reply@slyses-nonlive.tfstate.dev"
12 | api-id="75zwl9dq4c"
13 | api-resource-id="g9jdpe3fba"
14 | base-url="https://api-nonlive.tfstate.dev/github"
15 | bucket="nonlive-github-sls-rest-api20220405142407316800000001"
16 | bucket-topic-arn="arn:aws:sns:us-east-1:592292130354:nonlive-github-sls-rest-api-s3"
17 | role-arn="arn:aws:iam::592292130354:role/github-sls-rest-api-nonlive"
18 | service-name="github-sls-rest-api"
19 | service-slug="github"
20 | topic-arn="arn:aws:sns:us-east-1:592292130354:nonlive-github-sls-rest-api"
21 | websocket-api-id="null"
22 | websocket-url="null"
23 | application-name="github-sls-rest-api"
24 | organization-name="tfstate"
--------------------------------------------------------------------------------
/.scaffoldly/.env.live:
--------------------------------------------------------------------------------
1 | api-gateway-domain="api.tfstate.dev"
2 | api-gateway-websocket="false"
3 | api-gateway-websocket-domain="null"
4 | stage-domain="tfstate.dev"
5 | subdomain="api"
6 | subdomain-suffix=""
7 | zone-id="Z0754609150R99BSVKDUQ"
8 | key-alias="alias/live"
9 | key-id="ac36be06-1df1-42c0-8fb0-9a129aa05a16"
10 | mail-domain="slyses.tfstate.dev"
11 | noreply-address="no-reply@slyses.tfstate.dev"
12 | api-id="6y5dgy46hf"
13 | api-resource-id="h30pmz3pa7"
14 | base-url="https://api.tfstate.dev/github"
15 | bucket="live-github-sls-rest-api20220405142407327000000002"
16 | bucket-topic-arn="arn:aws:sns:us-east-1:592292130354:live-github-sls-rest-api-s3"
17 | role-arn="arn:aws:iam::592292130354:role/github-sls-rest-api-live"
18 | service-name="github-sls-rest-api"
19 | service-slug="github"
20 | topic-arn="arn:aws:sns:us-east-1:592292130354:live-github-sls-rest-api"
21 | websocket-api-id="null"
22 | websocket-url="null"
23 | application-name="github-sls-rest-api"
24 | organization-name="tfstate"
--------------------------------------------------------------------------------
/.scaffoldly/.env.nonlive:
--------------------------------------------------------------------------------
1 | api-gateway-domain="api-nonlive.tfstate.dev"
2 | api-gateway-websocket="false"
3 | api-gateway-websocket-domain="null"
4 | stage-domain="tfstate.dev"
5 | subdomain="api"
6 | subdomain-suffix="nonlive"
7 | zone-id="Z0754609150R99BSVKDUQ"
8 | key-alias="alias/nonlive"
9 | key-id="ab356c75-5a89-4363-9186-921ed12e0d2c"
10 | mail-domain="slyses-nonlive.tfstate.dev"
11 | noreply-address="no-reply@slyses-nonlive.tfstate.dev"
12 | api-id="75zwl9dq4c"
13 | api-resource-id="g9jdpe3fba"
14 | base-url="https://api-nonlive.tfstate.dev/github"
15 | bucket="nonlive-github-sls-rest-api20220405142407316800000001"
16 | bucket-topic-arn="arn:aws:sns:us-east-1:592292130354:nonlive-github-sls-rest-api-s3"
17 | role-arn="arn:aws:iam::592292130354:role/github-sls-rest-api-nonlive"
18 | service-name="github-sls-rest-api"
19 | service-slug="github"
20 | topic-arn="arn:aws:sns:us-east-1:592292130354:nonlive-github-sls-rest-api"
21 | websocket-api-id="null"
22 | websocket-url="null"
23 | application-name="github-sls-rest-api"
24 | organization-name="tfstate"
--------------------------------------------------------------------------------
/.scaffoldly/env-vars.json:
--------------------------------------------------------------------------------
1 | {
2 | "api-gateway-domain": "api-nonlive.tfstate.dev",
3 | "api-gateway-websocket": "false",
4 | "api-gateway-websocket-domain": null,
5 | "stage-domain": "tfstate.dev",
6 | "subdomain": "api",
7 | "subdomain-suffix": "nonlive",
8 | "zone-id": "Z0754609150R99BSVKDUQ",
9 | "key-alias": "alias/nonlive",
10 | "key-id": "ab356c75-5a89-4363-9186-921ed12e0d2c",
11 | "mail-domain": "slyses-nonlive.tfstate.dev",
12 | "noreply-address": "no-reply@slyses-nonlive.tfstate.dev",
13 | "api-id": "75zwl9dq4c",
14 | "api-resource-id": "g9jdpe3fba",
15 | "base-url": "https://api-nonlive.tfstate.dev/github",
16 | "bucket": "nonlive-github-sls-rest-api20220405142407316800000001",
17 | "bucket-topic-arn": "arn:aws:sns:us-east-1:592292130354:nonlive-github-sls-rest-api-s3",
18 | "role-arn": "arn:aws:iam::592292130354:role/github-sls-rest-api-nonlive",
19 | "service-name": "github-sls-rest-api",
20 | "service-slug": "github",
21 | "topic-arn": "arn:aws:sns:us-east-1:592292130354:nonlive-github-sls-rest-api",
22 | "websocket-api-id": null,
23 | "websocket-url": null,
24 | "application-name": "github-sls-rest-api",
25 | "organization-name": "tfstate"
26 | }
--------------------------------------------------------------------------------
/.scaffoldly/live/env-vars.json:
--------------------------------------------------------------------------------
1 | {
2 | "api-gateway-domain": "api.tfstate.dev",
3 | "api-gateway-websocket": "false",
4 | "api-gateway-websocket-domain": null,
5 | "stage-domain": "tfstate.dev",
6 | "subdomain": "api",
7 | "subdomain-suffix": "",
8 | "zone-id": "Z0754609150R99BSVKDUQ",
9 | "key-alias": "alias/live",
10 | "key-id": "ac36be06-1df1-42c0-8fb0-9a129aa05a16",
11 | "mail-domain": "slyses.tfstate.dev",
12 | "noreply-address": "no-reply@slyses.tfstate.dev",
13 | "api-id": "6y5dgy46hf",
14 | "api-resource-id": "h30pmz3pa7",
15 | "base-url": "https://api.tfstate.dev/github",
16 | "bucket": "live-github-sls-rest-api20220405142407327000000002",
17 | "bucket-topic-arn": "arn:aws:sns:us-east-1:592292130354:live-github-sls-rest-api-s3",
18 | "role-arn": "arn:aws:iam::592292130354:role/github-sls-rest-api-live",
19 | "service-name": "github-sls-rest-api",
20 | "service-slug": "github",
21 | "topic-arn": "arn:aws:sns:us-east-1:592292130354:live-github-sls-rest-api",
22 | "websocket-api-id": null,
23 | "websocket-url": null,
24 | "application-name": "github-sls-rest-api",
25 | "organization-name": "tfstate"
26 | }
--------------------------------------------------------------------------------
/.scaffoldly/live/services.json:
--------------------------------------------------------------------------------
1 | {
2 | "auth-sls-rest-api": {
3 | "api-id": "g5rxxul2ma",
4 | "api-resource-id": "gw01ym9mfh",
5 | "base-url": "https://api.tfstate.dev/auth",
6 | "service-name": "auth-sls-rest-api",
7 | "service-slug": "auth"
8 | },
9 | "github-sls-rest-api": {
10 | "api-id": "6y5dgy46hf",
11 | "api-resource-id": "h30pmz3pa7",
12 | "base-url": "https://api.tfstate.dev/github",
13 | "bucket": "live-github-sls-rest-api20220405142407327000000002",
14 | "bucket-topic-arn": "arn:aws:sns:us-east-1:592292130354:live-github-sls-rest-api-s3",
15 | "role-arn": "arn:aws:iam::592292130354:role/github-sls-rest-api-live",
16 | "service-name": "github-sls-rest-api",
17 | "service-slug": "github",
18 | "topic-arn": "arn:aws:sns:us-east-1:592292130354:live-github-sls-rest-api",
19 | "websocket-api-id": null,
20 | "websocket-url": null
21 | }
22 | }
--------------------------------------------------------------------------------
/.scaffoldly/nonlive/env-vars.json:
--------------------------------------------------------------------------------
1 | {
2 | "api-gateway-domain": "api-nonlive.tfstate.dev",
3 | "api-gateway-websocket": "false",
4 | "api-gateway-websocket-domain": null,
5 | "stage-domain": "tfstate.dev",
6 | "subdomain": "api",
7 | "subdomain-suffix": "nonlive",
8 | "zone-id": "Z0754609150R99BSVKDUQ",
9 | "key-alias": "alias/nonlive",
10 | "key-id": "ab356c75-5a89-4363-9186-921ed12e0d2c",
11 | "mail-domain": "slyses-nonlive.tfstate.dev",
12 | "noreply-address": "no-reply@slyses-nonlive.tfstate.dev",
13 | "api-id": "75zwl9dq4c",
14 | "api-resource-id": "g9jdpe3fba",
15 | "base-url": "https://api-nonlive.tfstate.dev/github",
16 | "bucket": "nonlive-github-sls-rest-api20220405142407316800000001",
17 | "bucket-topic-arn": "arn:aws:sns:us-east-1:592292130354:nonlive-github-sls-rest-api-s3",
18 | "role-arn": "arn:aws:iam::592292130354:role/github-sls-rest-api-nonlive",
19 | "service-name": "github-sls-rest-api",
20 | "service-slug": "github",
21 | "topic-arn": "arn:aws:sns:us-east-1:592292130354:nonlive-github-sls-rest-api",
22 | "websocket-api-id": null,
23 | "websocket-url": null,
24 | "application-name": "github-sls-rest-api",
25 | "organization-name": "tfstate"
26 | }
--------------------------------------------------------------------------------
/.scaffoldly/nonlive/services.json:
--------------------------------------------------------------------------------
1 | {
2 | "auth-sls-rest-api": {
3 | "api-id": "hbjxolg96l",
4 | "api-resource-id": "el756beg20",
5 | "base-url": "https://api-nonlive.tfstate.dev/auth",
6 | "service-name": "auth-sls-rest-api",
7 | "service-slug": "auth"
8 | },
9 | "github-sls-rest-api": {
10 | "api-id": "75zwl9dq4c",
11 | "api-resource-id": "g9jdpe3fba",
12 | "base-url": "https://api-nonlive.tfstate.dev/github",
13 | "bucket": "nonlive-github-sls-rest-api20220405142407316800000001",
14 | "bucket-topic-arn": "arn:aws:sns:us-east-1:592292130354:nonlive-github-sls-rest-api-s3",
15 | "role-arn": "arn:aws:iam::592292130354:role/github-sls-rest-api-nonlive",
16 | "service-name": "github-sls-rest-api",
17 | "service-slug": "github",
18 | "topic-arn": "arn:aws:sns:us-east-1:592292130354:nonlive-github-sls-rest-api",
19 | "websocket-api-id": null,
20 | "websocket-url": null
21 | }
22 | }
--------------------------------------------------------------------------------
/.scaffoldly/services.json:
--------------------------------------------------------------------------------
1 | {
2 | "auth-sls-rest-api": {
3 | "api-id": "hbjxolg96l",
4 | "api-resource-id": "el756beg20",
5 | "base-url": "https://api-nonlive.tfstate.dev/auth",
6 | "service-name": "auth-sls-rest-api",
7 | "service-slug": "auth"
8 | },
9 | "github-sls-rest-api": {
10 | "api-id": "75zwl9dq4c",
11 | "api-resource-id": "g9jdpe3fba",
12 | "base-url": "https://api-nonlive.tfstate.dev/github",
13 | "bucket": "nonlive-github-sls-rest-api20220405142407316800000001",
14 | "bucket-topic-arn": "arn:aws:sns:us-east-1:592292130354:nonlive-github-sls-rest-api-s3",
15 | "role-arn": "arn:aws:iam::592292130354:role/github-sls-rest-api-nonlive",
16 | "service-name": "github-sls-rest-api",
17 | "service-slug": "github",
18 | "topic-arn": "arn:aws:sns:us-east-1:592292130354:nonlive-github-sls-rest-api",
19 | "websocket-api-id": null,
20 | "websocket-url": null
21 | }
22 | }
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | // Use IntelliSense to learn about possible attributes.
3 | // Hover to view descriptions of existing attributes.
4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
5 | "version": "0.2.0",
6 | "configurations": [
7 | {
8 | "preLaunchTask": "prepare",
9 | "type": "node",
10 | "request": "launch",
11 | "name": "Local Debug",
12 | "program": "${workspaceRoot}/node_modules/serverless/bin/serverless",
13 | "args": ["offline", "start", "--noTimeout"],
14 | "outFiles": ["${workspaceFolder}/.webpack/**/*.js"],
15 | "sourceMaps": true,
16 | "internalConsoleOptions": "openOnSessionStart",
17 | "presentation": {
18 | "hidden": false,
19 | "group": "api",
20 | "order": 1
21 | }
22 | }
23 | ]
24 | }
25 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "editor.formatOnSave": true,
3 | "editor.codeActionsOnSave": {
4 | "source.fixAll.eslint": true
5 | },
6 | "filewatcher.commands": [
7 | {
8 | "match": "src/(controllers|interfaces|models/interfaces|services)/(.*).ts",
9 | "cmd": "cd ${workspaceRoot} && yarn tsoa",
10 | "event": "onFileChange"
11 | },
12 | {
13 | "match": "src/models/schemas/(.*).ts",
14 | "cmd": "cd ${workspaceRoot} && yarn types && yarn tsoa",
15 | "event": "onFileChange"
16 | }
17 | ],
18 | "typescript.preferences.importModuleSpecifier": "relative",
19 | "[html]": {
20 | "editor.defaultFormatter": "vscode.html-language-features"
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/.vscode/tasks.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "2.0.0",
3 | "tasks": [
4 | {
5 | "label": "prepare",
6 | "type": "shell",
7 | "presentation": {
8 | "group": "api",
9 | "echo": true,
10 | "reveal": "always",
11 | "focus": true,
12 | "panel": "dedicated",
13 | "clear": true
14 | },
15 | "command": "yarn",
16 | "args": ["prepare"]
17 | }
18 | ]
19 | }
20 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing
2 |
3 | When contributing to this repository, please first discuss the change you wish to make via issue,
4 | email, or any other method with the owners of this repository before making a change.
5 |
6 | Please note we have a code of conduct, please follow it in all your interactions with the project.
7 |
8 | ## Pull Request Process
9 |
10 | 1. Ensure any install or build dependencies are removed before the end of the layer when doing a
11 | build.
12 | 2. Update the README.md with details of changes to the interface, this includes new environment
13 | variables, exposed ports, useful file locations and container parameters.
14 | 3. Increase the version numbers in any examples files and the README.md to the new version that this
15 | Pull Request would represent. The versioning scheme we use is [SemVer](http://semver.org/).
16 | 4. You may merge the Pull Request in once you have the sign-off of one other developer, or if you
17 | do not have permission to do that, you may request the second reviewer to merge it for you.
18 |
19 | ## Code of Conduct
20 |
21 | ### Our Pledge
22 |
23 | In the interest of fostering an open and welcoming environment, we as
24 | contributors and maintainers pledge to making participation in our project and
25 | our community a harassment-free experience for everyone, regardless of age, body
26 | size, disability, ethnicity, gender identity and expression, level of experience,
27 | nationality, personal appearance, race, religion, or sexual identity and
28 | orientation.
29 |
30 | ### Our Standards
31 |
32 | Examples of behavior that contributes to creating a positive environment
33 | include:
34 |
35 | - Using welcoming and inclusive language
36 | - Being respectful of differing viewpoints and experiences
37 | - Gracefully accepting constructive criticism
38 | - Focusing on what is best for the community
39 | - Showing empathy towards other community members
40 |
41 | Examples of unacceptable behavior by participants include:
42 |
43 | - The use of sexualized language or imagery and unwelcome sexual attention or
44 | advances
45 | - Trolling, insulting/derogatory comments, and personal or political attacks
46 | - Public or private harassment
47 | - Publishing others' private information, such as a physical or electronic
48 | address, without explicit permission
49 | - Other conduct which could reasonably be considered inappropriate in a
50 | professional setting
51 |
52 | ### Our Responsibilities
53 |
54 | Project maintainers are responsible for clarifying the standards of acceptable
55 | behavior and are expected to take appropriate and fair corrective action in
56 | response to any instances of unacceptable behavior.
57 |
58 | Project maintainers have the right and responsibility to remove, edit, or
59 | reject comments, commits, code, wiki edits, issues, and other contributions
60 | that are not aligned to this Code of Conduct, or to ban temporarily or
61 | permanently any contributor for other behaviors that they deem inappropriate,
62 | threatening, offensive, or harmful.
63 |
64 | ### Scope
65 |
66 | This Code of Conduct applies both within project spaces and in public spaces
67 | when an individual is representing the project or its community. Examples of
68 | representing a project or community include using an official project e-mail
69 | address, posting via an official social media account, or acting as an appointed
70 | representative at an online or offline event. Representation of a project may be
71 | further defined and clarified by project maintainers.
72 |
73 | ### Enforcement
74 |
75 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
76 | reported by contacting the project team at gitter.im/tfstate/community. All
77 | complaints will be reviewed and investigated and will result in a response that
78 | is deemed necessary and appropriate to the circumstances. The project team is
79 | obligated to maintain confidentiality with regard to the reporter of an incident.
80 | Further details of specific enforcement policies may be posted separately.
81 |
82 | Project maintainers who do not follow or enforce the Code of Conduct in good
83 | faith may face temporary or permanent repercussions as determined by other
84 | members of the project's leadership.
85 |
86 | ### Attribution
87 |
88 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
89 | available at [http://contributor-covenant.org/version/1/4][version]
90 |
91 | [homepage]: http://contributor-covenant.org
92 | [version]: http://contributor-covenant.org/version/1/4/
93 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Terraform State Storage HTTP Backend
2 |
3 | **TFstate.dev** is a free [Terraform State Provider](https://www.terraform.io/language/settings/backends/http) and [Open Source Hosted Service](https://github.com/tfstate/github-sls-rest-api) for secure Terraform Remote State hosting using a GitHub Token, courtsey of [Scaffoldly](https://scaffold.ly)
4 |
5 | Features:
6 |
7 | - GitHub Token used for Authentication and Authorization to Terraform State
8 | - Encrypted State in Amazon S3 using Amazon KMS
9 | - State Locking
10 | - Highly available [Hosted API](https://api.tfstate.dev/github/swagger.html) in AWS Lambda + API Gateway
11 | - Plug and Play: Only a GitHub Token is needed to use TFstate.dev
12 |
13 | ✅ We do not store or save the provided GitHub token.
14 |
15 | ---
16 |
17 | ## Getting started 🚀
18 |
19 | First, a GitHub token is needed. This can be a [Personal Access Token](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/creating-a-personal-access-token), a [GitHub Actions Secret](https://docs.github.com/en/actions/security-guides/automatic-token-authentication), or any other form of [GitHub Oauth Token](https://github.blog/2021-04-05-behind-githubs-new-authentication-token-formats/). At a minimum, the token needs `repo:read` access for the configured repository.
20 |
21 | ➡ See our [example repository](https://github.com/tfstate/example).
22 |
23 | To use TFstate.dev in Terraform, add the following [backend configuration](https://www.terraform.io/language/settings/backends/http) to Terraform:
24 |
25 | ```hcl
26 | terraform {
27 | backend "http" {
28 | address = "https://api.tfstate.dev/github/v1"
29 | lock_address = "https://api.tfstate.dev/github/v1/lock"
30 | unlock_address = "https://api.tfstate.dev/github/v1/lock"
31 | lock_method = "PUT"
32 | unlock_method = "DELETE"
33 | username = "{your-github-org}/{your-github-repo}"
34 | }
35 | }
36 | ```
37 |
38 | Then, Terraform can be configured to use the TFstate.dev backend using the GitHub token:
39 |
40 | ```bash
41 | terraform init -backend-config="password={your-github-token}"
42 | terraform plan
43 | terraform apply
44 | ```
45 |
46 | Alternatively, the `TF_HTTP_PASSWORD` environment variable can be set with the GitHub token:
47 |
48 | ```bash
49 | export TF_HTTP_PASSWORD="{your-github-token}"
50 | terraform init
51 | terraform plan
52 | terraform apply
53 | ```
54 |
55 | For more information go to [TFstate.dev](https://tfstate.dev)!
56 |
57 | ---
58 |
59 | ## Want to Contribute?
60 |
61 | ### Developing/Contributing to this API
62 |
63 | We'd love contributions from the community to improve this API.
64 |
65 | #### Running
66 |
67 | Requirements:
68 |
69 | - NodeJS 14+
70 | - Yarn
71 |
72 | Running instructions:
73 |
74 | 1. Fork and clone this repo
75 | 1. Run `yarn`
76 | 1. Run `yarn start` (launches Serverless in Local mode)
77 |
78 | The main controller is [`ControllerV1`](src/controllers/ControllerV1.ts). It contains the primary endpoints for State Storage.
79 |
80 | Once running locally, the OpenAPI docs can be found at:
81 |
82 | https://localhost:3000/github/swagger.html
83 |
84 | #### Verifying Locally
85 |
86 | While running the API locally, create a basic Terraform structure to test state functions:
87 |
88 | ```hcl
89 | terraform {
90 | backend "http" {
91 | address = "http://localhost:3000/github/v1"
92 | lock_address = "http://localhost:3000/github/v1/lock"
93 | unlock_address = "http://localhost:3000/github/v1/lock"
94 | lock_method = "PUT"
95 | unlock_method = "DELETE"
96 |
97 | # Make sure this is a real repository that your token has access to
98 | username = "{your-github-user}/github-sls-rest-api"
99 | }
100 | }
101 |
102 | resource "null_resource" "example" {
103 |
104 | }
105 |
106 | output "null_resource_id" {
107 | value = null_resource.example.id
108 | }
109 | ```
110 |
111 | Then, run:
112 |
113 | ```bash
114 | export TF_HTTP_PASSWORD={your-github-token}
115 | terraform init
116 | terraform plan
117 | terraform apply
118 | ```
119 |
120 | Other command to verify with:
121 |
122 | ```bash
123 | terraform state ...
124 | terraform force-unlock ...
125 | ```
126 |
127 | ## Contributing Guidelines
128 |
129 | See [CONTRIBUTING](./CONTRIBUTING.md)
130 |
131 | ## Maintainers
132 |
133 | - [cnuss](https://github.com/cnuss)
134 | - [Scaffoldly](https://github.com/scaffoldly)
135 |
136 | ## License
137 |
138 | Copyright 2022 Scaffoldly LLC
139 |
140 | Licensed under the Apache License, Version 2.0 (the "License");
141 | you may not use this file except in compliance with the License.
142 | You may obtain a copy of the License at
143 |
144 | http://www.apache.org/licenses/LICENSE-2.0
145 |
146 | Unless required by applicable law or agreed to in writing, software
147 | distributed under the License is distributed on an "AS IS" BASIS,
148 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
149 | See the License for the specific language governing permissions and
150 | limitations under the License.
151 |
152 | 
153 |
--------------------------------------------------------------------------------
/openapitools.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "node_modules/@openapitools/openapi-generator-cli/config.schema.json",
3 | "spaces": 2,
4 | "generator-cli": {
5 | "version": "5.1.1",
6 | "storageDir": "/tmp"
7 | }
8 | }
9 |
10 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "github-sls-rest-api",
3 | "version": "1.0.25",
4 | "license": "UNLICENSED",
5 | "engines": {
6 | "node": ">=0.14"
7 | },
8 | "scripts": {
9 | "prepare": "./scripts/prepare.sh",
10 | "build": "tsc",
11 | "start": "SLS_DEBUG=* serverless offline start",
12 | "deploy": "SLS_DEBUG=* serverless deploy",
13 | "serverless": "SLS_DEBUG=* serverless",
14 | "dotenv": "dotenv-out -f typescript -e .env -e .scaffoldly/.env -c $NODE_ENV -o src",
15 | "openapi": "openapi-generator -g axios -i .scaffoldly/$NODE_ENV -o src/services/openapi -r auth-sls-rest-api",
16 | "types": "ts-node types.ts",
17 | "tsoa": "node tsoa.js",
18 | "dynamodb": "serverless dynamodb install",
19 | "lint": "eslint '*/**/*.{js,ts,tsx}' --quiet --fix && yarn run prettier --write '*/**/*.{js,ts,tsx}'"
20 | },
21 | "devDependencies": {
22 | "@babel/core": "^7.16.0",
23 | "@babel/eslint-parser": "^7.16.0",
24 | "@scaffoldly/openapi-generator": "^1.0.21",
25 | "@types/aws-lambda": "^8.10.77",
26 | "@types/dotenv": "^8.2.0",
27 | "@types/express": "^4.17.11",
28 | "@types/node": "14",
29 | "@types/seedrandom": "^3.0.2",
30 | "@typescript-eslint/eslint-plugin": "^4.29.3",
31 | "@typescript-eslint/parser": "^4.29.3",
32 | "cross-env": "^7.0.3",
33 | "dotenv-out": "^1.0.6",
34 | "eslint": "^7.2.0",
35 | "eslint-config-airbnb": "18.2.1",
36 | "eslint-config-airbnb-typescript": "14.0.2",
37 | "eslint-config-prettier": "^8.3.0",
38 | "eslint-plugin-import": "^2.22.1",
39 | "eslint-plugin-jsx-a11y": "^6.4.1",
40 | "eslint-plugin-prettier": "^4.0.0",
41 | "prettier": "^2.4.1",
42 | "serverless": "2.57.0",
43 | "serverless-bundle": "^4.4.0",
44 | "serverless-dotenv-plugin": "^3.9.0",
45 | "serverless-dynamodb-local": "^0.2.40",
46 | "serverless-offline": "^8.1.0",
47 | "serverless-offline-dynamodb-streams": "^5.0.0",
48 | "serverless-plugin-resource-tagging": "^1.1.1",
49 | "ts-node": "^10.4.0",
50 | "typescript": "^4.4.4"
51 | },
52 | "dependencies": {
53 | "@octokit/rest": "^18.12.0",
54 | "@scaffoldly/serverless-util": "^4.0.33",
55 | "@vendia/serverless-express": "^4.5.2",
56 | "aws-lambda": "^1.0.6",
57 | "axios": "^0.21.1",
58 | "express": "^4.17.1",
59 | "joi": "^17.4.0",
60 | "joi-to-typescript": "^1.12.0",
61 | "moment": "^2.29.1",
62 | "seedrandom": "^3.0.5",
63 | "tsoa": "^3.8.0",
64 | "ulid": "^2.3.0"
65 | }
66 | }
--------------------------------------------------------------------------------
/public/swagger.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Swagger UI
8 |
9 |
10 |
11 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
55 |
56 |
57 |
--------------------------------------------------------------------------------
/scripts/prepare.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | set -e
4 | set -x
5 |
6 | yarn dotenv
7 | yarn openapi
8 |
9 | yarn types
10 | yarn dynamodb
11 |
12 | yarn tsoa
13 | yarn build
14 |
--------------------------------------------------------------------------------
/serverless.config.js:
--------------------------------------------------------------------------------
1 | // eslint-disable-next-line @typescript-eslint/no-var-requires
2 | const fs = require('fs');
3 |
4 | const { NODE_ENV } = process.env;
5 | const envVars = NODE_ENV
6 | ? JSON.parse(fs.readFileSync(fs.openSync(`.scaffoldly/${NODE_ENV}/env-vars.json`)))
7 | : JSON.parse(fs.readFileSync(fs.openSync(`.scaffoldly/env-vars.json`)));
8 |
9 | module.exports.apiGatewayDomain = envVars['api-gateway-domain'];
10 | module.exports.stageDomain = envVars['stage-domain'];
11 | module.exports.serviceName = envVars['service-name'];
12 | module.exports.serviceSlug = envVars['service-slug'];
13 |
--------------------------------------------------------------------------------
/serverless.yml:
--------------------------------------------------------------------------------
1 | service: ${file(serverless.config.js):serviceName}
2 |
3 | frameworkVersion: 2.57.0
4 | variablesResolutionMode: '20210326'
5 | configValidationMode: off
6 | disabledDeprecations:
7 | - '*'
8 |
9 | plugins:
10 | - serverless-bundle
11 | - serverless-dotenv-plugin
12 | - serverless-dynamodb-local
13 | - serverless-plugin-resource-tagging
14 | - serverless-offline
15 |
16 | provider:
17 | name: aws
18 | runtime: nodejs14.x
19 | lambdaHashingVersion: '20201221'
20 | cfnRole: arn:${env:AWS_PARTITION, ""}:iam::${env:AWS_ACCOUNT_ID, ""}:role/${self:service}-cloudformation
21 | stage: ${opt:stage, "local"}
22 | logRetentionInDays: 1
23 | apiGateway:
24 | restApiId: ${env:AWS_REST_API_ID, "0000000000"}
25 | restApiRootResourceId: ${env:AWS_REST_API_ROOT_RESOURCE_ID, "0000000000"}
26 | tracing:
27 | lambda: true
28 | environment:
29 | API_GATEWAY_DOMAIN: ${file(serverless.config.js):apiGatewayDomain}
30 | STAGE_DOMAIN: ${file(serverless.config.js):stageDomain}
31 | SERVICE_NAME: ${file(serverless.config.js):serviceName}
32 | SERVICE_SLUG: ${file(serverless.config.js):serviceSlug}
33 | STAGE: ${opt:stage, "local"}
34 | stackTags:
35 | ServiceName: ${self:service}
36 | ServiceSlug: ${file(serverless.config.js):serviceSlug}
37 | ServiceStage: ${opt:stage, "local"}
38 |
39 | functions:
40 | lambda-handler:
41 | role: arn:${env:AWS_PARTITION, ""}:iam::${env:AWS_ACCOUNT_ID, ""}:role/${self:service}-${opt:stage, "local"}
42 | handler: src/lambda.handler
43 | timeout: 30
44 | events:
45 | - http:
46 | path: /
47 | method: any
48 | - http:
49 | path: /
50 | method: options
51 | - http:
52 | path: /{proxy+}
53 | method: any
54 | - http:
55 | path: /{proxy+}
56 | method: options
57 |
58 | resources:
59 | Resources:
60 | Secret:
61 | Type: AWS::SecretsManager::Secret
62 | Properties:
63 | Name: lambda/${opt:stage, "local"}/${self:service}
64 | SecretString: '{}'
65 |
66 | Table:
67 | Type: AWS::DynamoDB::Table
68 | Properties:
69 | TableName: ${opt:stage, "local"}-${self:service}
70 | KeySchema:
71 | - AttributeName: pk
72 | KeyType: HASH
73 | - AttributeName: sk
74 | KeyType: RANGE
75 | AttributeDefinitions:
76 | - AttributeName: pk
77 | AttributeType: S
78 | - AttributeName: sk
79 | AttributeType: S
80 | GlobalSecondaryIndexes:
81 | - IndexName: sk-pk-index
82 | KeySchema:
83 | - AttributeName: sk
84 | KeyType: HASH
85 | - AttributeName: pk
86 | KeyType: RANGE
87 | Projection:
88 | ProjectionType: ALL
89 | StreamSpecification:
90 | StreamViewType: NEW_AND_OLD_IMAGES
91 | TimeToLiveSpecification:
92 | AttributeName: expires
93 | Enabled: true
94 | PointInTimeRecoverySpecification:
95 | PointInTimeRecoveryEnabled: true
96 | SSESpecification:
97 | SSEEnabled: true
98 | BillingMode: PAY_PER_REQUEST
99 |
100 | IdentityTable:
101 | Type: AWS::DynamoDB::Table
102 | Properties:
103 | TableName: ${opt:stage, "local"}-${self:service}-identity
104 | KeySchema:
105 | - AttributeName: pk
106 | KeyType: HASH
107 | - AttributeName: sk
108 | KeyType: RANGE
109 | AttributeDefinitions:
110 | - AttributeName: pk
111 | AttributeType: S
112 | - AttributeName: sk
113 | AttributeType: S
114 | GlobalSecondaryIndexes:
115 | - IndexName: sk-pk-index
116 | KeySchema:
117 | - AttributeName: sk
118 | KeyType: HASH
119 | - AttributeName: pk
120 | KeyType: RANGE
121 | Projection:
122 | ProjectionType: ALL
123 | StreamSpecification:
124 | StreamViewType: NEW_AND_OLD_IMAGES
125 | TimeToLiveSpecification:
126 | AttributeName: expires
127 | Enabled: true
128 | PointInTimeRecoverySpecification:
129 | PointInTimeRecoveryEnabled: true
130 | SSESpecification:
131 | SSEEnabled: true
132 | BillingMode: PAY_PER_REQUEST
133 |
134 | custom:
135 | serverless-offline:
136 | useChildProcesses: false
137 | noPrependStageInUrl: true
138 | prefix: ${file(serverless.config.js):serviceSlug}
139 |
140 | bundle:
141 | packager: yarn
142 | externals:
143 | - tsoa
144 | copyFiles:
145 | - from: 'public/*'
146 | to: './'
147 |
148 | dynamodb:
149 | stages:
150 | - local
151 | start:
152 | port: 8100
153 | dbPath: .dynamodb
154 | migrate: true
155 | serverless-offline-dynamodb-streams:
156 | endpoint: http://0.0.0.0:8100
157 |
--------------------------------------------------------------------------------
/src/app.ts:
--------------------------------------------------------------------------------
1 | /**
2 | Copyright 2022 Scaffoldly LLC
3 |
4 | Licensed under the Apache License, Version 2.0 (the "License");
5 | you may not use this file except in compliance with the License.
6 | You may obtain a copy of the License at
7 |
8 | http://www.apache.org/licenses/LICENSE-2.0
9 |
10 | Unless required by applicable law or agreed to in writing, software
11 | distributed under the License is distributed on an "AS IS" BASIS,
12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | See the License for the specific language governing permissions and
14 | limitations under the License.
15 | */
16 | import {
17 | corsHandler,
18 | CorsOptions,
19 | createApp,
20 | registerDocs,
21 | registerVersion,
22 | } from '@scaffoldly/serverless-util';
23 | import express from 'express';
24 | import { readFileSync } from 'fs';
25 | import packageJson from '../package.json';
26 | import { RegisterRoutes } from './routes';
27 |
28 | import swaggerJson from './swagger.json';
29 |
30 | const app = createApp({ logHeaders: true });
31 |
32 | const corsOptions: CorsOptions = {};
33 |
34 | app.use(corsHandler(corsOptions));
35 |
36 | RegisterRoutes(app);
37 |
38 | registerDocs(app, swaggerJson);
39 | registerVersion(app, packageJson.version);
40 |
41 | app.get('/swagger.html', (_req: express.Request, res: express.Response) => {
42 | const file = readFileSync('./public/swagger.html');
43 | res.type('html');
44 | res.send(file);
45 | });
46 |
47 | export default app;
48 |
--------------------------------------------------------------------------------
/src/auth.ts:
--------------------------------------------------------------------------------
1 | /**
2 | Copyright 2022 Scaffoldly LLC
3 |
4 | Licensed under the Apache License, Version 2.0 (the "License");
5 | you may not use this file except in compliance with the License.
6 | You may obtain a copy of the License at
7 |
8 | http://www.apache.org/licenses/LICENSE-2.0
9 |
10 | Unless required by applicable law or agreed to in writing, software
11 | distributed under the License is distributed on an "AS IS" BASIS,
12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | See the License for the specific language governing permissions and
14 | limitations under the License.
15 | */
16 | import { authorize } from '@scaffoldly/serverless-util';
17 | import { env } from './env';
18 |
19 | const DOMAIN = env['stage-domain'];
20 |
21 | export const expressAuthentication = authorize(DOMAIN);
22 |
--------------------------------------------------------------------------------
/src/controllers/ControllerV1.ts:
--------------------------------------------------------------------------------
1 | /**
2 | Copyright 2022 Scaffoldly LLC
3 |
4 | Licensed under the Apache License, Version 2.0 (the "License");
5 | you may not use this file except in compliance with the License.
6 | You may obtain a copy of the License at
7 |
8 | http://www.apache.org/licenses/LICENSE-2.0
9 |
10 | Unless required by applicable law or agreed to in writing, software
11 | distributed under the License is distributed on an "AS IS" BASIS,
12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | See the License for the specific language governing permissions and
14 | limitations under the License.
15 | */
16 | import { HttpRequest } from '@scaffoldly/serverless-util';
17 | import {
18 | Body,
19 | Controller,
20 | Delete,
21 | Get,
22 | Post,
23 | Put,
24 | Query,
25 | Request,
26 | Res,
27 | Route,
28 | Tags,
29 | TsoaResponse,
30 | } from 'tsoa';
31 | import { TerraformError } from '../interfaces/errors';
32 | import { StateLockRequest } from '../models/interfaces/StateLockRequest';
33 | import { GithubService } from '../services/GithubService';
34 | import { StateService } from '../services/StateService';
35 |
36 | @Route('/v1')
37 | @Tags('v1')
38 | export class ControllerV1 extends Controller {
39 | githubService: GithubService;
40 |
41 | stateService: StateService;
42 |
43 | constructor() {
44 | super();
45 | this.githubService = new GithubService();
46 | this.stateService = new StateService();
47 | }
48 |
49 | @Get()
50 | public async getState(
51 | @Request() request: HttpRequest,
52 | @Res() res: TsoaResponse<200 | 401 | 403 | 404, any>,
53 | ): Promise {
54 | try {
55 | const identity = await this.githubService.getIdentity(request);
56 | const state = await this.stateService.getState(identity);
57 | const response = res(200, state);
58 | return response;
59 | } catch (e) {
60 | if (e instanceof TerraformError) {
61 | return e.respond(res);
62 | }
63 | throw e;
64 | }
65 | }
66 |
67 | @Post()
68 | public async saveState(
69 | @Request() request: HttpRequest,
70 | @Query('ID') id: string,
71 | // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
72 | @Body() state: any,
73 | @Res() res: TsoaResponse<200 | 400 | 401 | 403 | 404 | 409, void>,
74 | ): Promise {
75 | try {
76 | const stateLockRequest = await this.stateService.getRequest(id);
77 | const identity = await this.githubService.getIdentity(request, stateLockRequest);
78 | await this.stateService.saveState(identity, id, state);
79 | const response = res(200);
80 | return response;
81 | } catch (e) {
82 | if (e instanceof TerraformError) {
83 | return e.respond(res);
84 | }
85 | throw e;
86 | }
87 | }
88 |
89 | @Put('lock')
90 | public async lockState(
91 | @Request() request: HttpRequest,
92 | @Body() lockRequest: StateLockRequest,
93 | @Res() res: TsoaResponse<200 | 401 | 403 | 404 | 409, boolean>,
94 | ): Promise {
95 | try {
96 | const stateLockRequest = await this.stateService.saveRequest(lockRequest);
97 | const identity = await this.githubService.getIdentity(request, stateLockRequest);
98 | await this.stateService.lockState(identity, stateLockRequest);
99 | const response = res(200, true);
100 | return response;
101 | } catch (e) {
102 | if (e instanceof TerraformError) {
103 | return e.respond(res);
104 | }
105 | throw e;
106 | }
107 | }
108 |
109 | @Delete('lock')
110 | public async unlockState(
111 | @Request() request: HttpRequest,
112 | @Res() res: TsoaResponse<200 | 401 | 403 | 404 | 409, boolean>,
113 | @Body() lockRequest?: StateLockRequest,
114 | ): Promise {
115 | try {
116 | if (lockRequest && lockRequest.ID) {
117 | const stateLockRequest = await this.stateService.getRequest(lockRequest.ID);
118 | const identity = await this.githubService.getIdentity(request, stateLockRequest);
119 | await this.stateService.unlockState(identity, stateLockRequest);
120 | } else {
121 | const identity = await this.githubService.getIdentity(request);
122 | await this.stateService.unlockState(identity);
123 | }
124 | const response = res(200, true);
125 | return response;
126 | } catch (e) {
127 | if (e instanceof TerraformError) {
128 | return e.respond(res);
129 | }
130 | throw e;
131 | }
132 | }
133 | }
134 |
--------------------------------------------------------------------------------
/src/controllers/HealthController.ts:
--------------------------------------------------------------------------------
1 | /**
2 | Copyright 2022 Scaffoldly LLC
3 |
4 | Licensed under the Apache License, Version 2.0 (the "License");
5 | you may not use this file except in compliance with the License.
6 | You may obtain a copy of the License at
7 |
8 | http://www.apache.org/licenses/LICENSE-2.0
9 |
10 | Unless required by applicable law or agreed to in writing, software
11 | distributed under the License is distributed on an "AS IS" BASIS,
12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | See the License for the specific language governing permissions and
14 | limitations under the License.
15 | */
16 | import { Controller, Get, Route, Tags } from 'tsoa';
17 | import packageJson from '../../package.json';
18 |
19 | export type HealthResponse = {
20 | name: string;
21 | healthy: boolean;
22 | now: Date;
23 | version: string;
24 | };
25 |
26 | @Route('/health')
27 | @Tags('Health')
28 | export class HealthController extends Controller {
29 | @Get()
30 | public async get(): Promise {
31 | return {
32 | name: packageJson.name,
33 | healthy: true,
34 | now: new Date(),
35 | version: packageJson.version,
36 | };
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/interfaces/errors.ts:
--------------------------------------------------------------------------------
1 | /**
2 | Copyright 2022 Scaffoldly LLC
3 |
4 | Licensed under the Apache License, Version 2.0 (the "License");
5 | you may not use this file except in compliance with the License.
6 | You may obtain a copy of the License at
7 |
8 | http://www.apache.org/licenses/LICENSE-2.0
9 |
10 | Unless required by applicable law or agreed to in writing, software
11 | distributed under the License is distributed on an "AS IS" BASIS,
12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | See the License for the specific language governing permissions and
14 | limitations under the License.
15 | */
16 | import { TsoaResponse } from 'tsoa';
17 |
18 | export class TerraformError extends Error {
19 | // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
20 | constructor(public statusCode: number, public body?: any) {
21 | super();
22 | }
23 |
24 | respond = (res: TsoaResponse): any => {
25 | if (!this.body) {
26 | return res(this.statusCode, {});
27 | }
28 |
29 | return res(this.statusCode, this.body);
30 | };
31 | }
32 |
--------------------------------------------------------------------------------
/src/lambda.ts:
--------------------------------------------------------------------------------
1 | /**
2 | Copyright 2022 Scaffoldly LLC
3 |
4 | Licensed under the Apache License, Version 2.0 (the "License");
5 | you may not use this file except in compliance with the License.
6 | You may obtain a copy of the License at
7 |
8 | http://www.apache.org/licenses/LICENSE-2.0
9 |
10 | Unless required by applicable law or agreed to in writing, software
11 | distributed under the License is distributed on an "AS IS" BASIS,
12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | See the License for the specific language governing permissions and
14 | limitations under the License.
15 | */
16 | import { configure } from '@vendia/serverless-express';
17 | import app from './app';
18 |
19 | // import { AWS } from '@scaffoldly/serverless-util';
20 | // AWS.config.logger = console;
21 |
22 | exports.handler = configure({
23 | app,
24 | eventSourceRoutes: {},
25 | });
26 |
--------------------------------------------------------------------------------
/src/models/IdentityModel.ts:
--------------------------------------------------------------------------------
1 | /**
2 | Copyright 2022 Scaffoldly LLC
3 |
4 | Licensed under the Apache License, Version 2.0 (the "License");
5 | you may not use this file except in compliance with the License.
6 | You may obtain a copy of the License at
7 |
8 | http://www.apache.org/licenses/LICENSE-2.0
9 |
10 | Unless required by applicable law or agreed to in writing, software
11 | distributed under the License is distributed on an "AS IS" BASIS,
12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | See the License for the specific language governing permissions and
14 | limitations under the License.
15 | */
16 | import {
17 | Joi,
18 | Model,
19 | SERVICE_NAME,
20 | STAGE,
21 | Table,
22 | unmarshallDynamoDBImage,
23 | } from '@scaffoldly/serverless-util';
24 | import { StreamRecord } from 'aws-lambda';
25 | import { Identity } from './interfaces';
26 | import { identity } from './schemas/Identity';
27 | const TABLE_SUFFIX = 'identity';
28 |
29 | export class IdentityModel {
30 | public readonly table: Table;
31 |
32 | public readonly model: Model;
33 |
34 | constructor() {
35 | this.table = new Table(TABLE_SUFFIX, SERVICE_NAME, STAGE, identity, 'pk', 'sk', [
36 | { hashKey: 'sk', rangeKey: 'pk', name: 'sk-pk-index', type: 'global' },
37 | ]);
38 |
39 | this.model = this.table.model;
40 | }
41 |
42 | // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
43 | static prefix = (col: 'pk' | 'sk', value?: any): string => {
44 | if (col === 'pk') {
45 | return `github_${value || ''}`;
46 | }
47 | return `identity`;
48 | };
49 |
50 | // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
51 | static isIdentity = (record: StreamRecord): boolean => {
52 | if (!record) {
53 | return false;
54 | }
55 |
56 | const check = unmarshallDynamoDBImage(record.Keys) as { pk: string; sk: string };
57 |
58 | if (!check.pk || !check.sk || typeof check.pk !== 'string' || typeof check.sk !== 'string') {
59 | return false;
60 | }
61 |
62 | const { pk, sk } = check;
63 |
64 | try {
65 | Joi.assert(pk, identity.pk);
66 | Joi.assert(sk, identity.sk);
67 | } catch (e) {
68 | return false;
69 | }
70 |
71 | return true;
72 | };
73 | }
74 |
--------------------------------------------------------------------------------
/src/models/StateLockModel.ts:
--------------------------------------------------------------------------------
1 | /**
2 | Copyright 2022 Scaffoldly LLC
3 |
4 | Licensed under the Apache License, Version 2.0 (the "License");
5 | you may not use this file except in compliance with the License.
6 | You may obtain a copy of the License at
7 |
8 | http://www.apache.org/licenses/LICENSE-2.0
9 |
10 | Unless required by applicable law or agreed to in writing, software
11 | distributed under the License is distributed on an "AS IS" BASIS,
12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | See the License for the specific language governing permissions and
14 | limitations under the License.
15 | */
16 | import {
17 | Joi,
18 | Model,
19 | SERVICE_NAME,
20 | STAGE,
21 | Table,
22 | unmarshallDynamoDBImage,
23 | } from '@scaffoldly/serverless-util';
24 | import { StreamRecord } from 'aws-lambda';
25 | import { StateLock } from './interfaces';
26 | import { stateLock } from './schemas/StateLock';
27 |
28 | const TABLE_SUFFIX = '';
29 |
30 | export class StateLockModel {
31 | public readonly table: Table;
32 |
33 | public readonly model: Model;
34 |
35 | constructor() {
36 | this.table = new Table(TABLE_SUFFIX, SERVICE_NAME, STAGE, stateLock, 'pk', 'sk', [
37 | { hashKey: 'sk', rangeKey: 'pk', name: 'sk-pk-index', type: 'global' },
38 | ]);
39 |
40 | this.model = this.table.model;
41 | }
42 |
43 | // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
44 | static prefix = (col: 'pk' | 'sk', value?: any): string => {
45 | if (col === 'pk') {
46 | return `github_${value || ''}`;
47 | }
48 | return `statelock_${value || ''}`;
49 | };
50 |
51 | // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
52 | static isState = (record: StreamRecord): boolean => {
53 | if (!record) {
54 | return false;
55 | }
56 |
57 | const check = unmarshallDynamoDBImage(record.Keys) as { pk: string; sk: string };
58 |
59 | if (!check.pk || !check.sk || typeof check.pk !== 'string' || typeof check.sk !== 'string') {
60 | return false;
61 | }
62 |
63 | const { pk, sk } = check;
64 |
65 | try {
66 | Joi.assert(pk, stateLock.pk);
67 | Joi.assert(sk, stateLock.sk);
68 | } catch (e) {
69 | return false;
70 | }
71 |
72 | return true;
73 | };
74 | }
75 |
--------------------------------------------------------------------------------
/src/models/StateLockRequestModel.ts:
--------------------------------------------------------------------------------
1 | /**
2 | Copyright 2022 Scaffoldly LLC
3 |
4 | Licensed under the Apache License, Version 2.0 (the "License");
5 | you may not use this file except in compliance with the License.
6 | You may obtain a copy of the License at
7 |
8 | http://www.apache.org/licenses/LICENSE-2.0
9 |
10 | Unless required by applicable law or agreed to in writing, software
11 | distributed under the License is distributed on an "AS IS" BASIS,
12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | See the License for the specific language governing permissions and
14 | limitations under the License.
15 | */
16 | import {
17 | Joi,
18 | Model,
19 | SERVICE_NAME,
20 | STAGE,
21 | Table,
22 | unmarshallDynamoDBImage,
23 | } from '@scaffoldly/serverless-util';
24 | import { StreamRecord } from 'aws-lambda';
25 | import { StateLockRequest } from './interfaces/StateLockRequest';
26 | import { stateLockRequest } from './schemas/StateLockRequest';
27 |
28 | const TABLE_SUFFIX = '';
29 |
30 | export class StateLockRequestModel {
31 | public readonly table: Table;
32 |
33 | public readonly model: Model;
34 |
35 | constructor() {
36 | this.table = new Table(TABLE_SUFFIX, SERVICE_NAME, STAGE, stateLockRequest, 'pk', 'sk', [
37 | { hashKey: 'sk', rangeKey: 'pk', name: 'sk-pk-index', type: 'global' },
38 | ]);
39 |
40 | this.model = this.table.model;
41 | }
42 |
43 | // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
44 | static prefix = (col: 'pk' | 'sk', value?: any): string => {
45 | if (col === 'pk') {
46 | return `lock_${value || ''}`;
47 | }
48 | return `statelock`;
49 | };
50 |
51 | // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
52 | static isStateLock = (record: StreamRecord): boolean => {
53 | if (!record) {
54 | return false;
55 | }
56 |
57 | const check = unmarshallDynamoDBImage(record.Keys) as { pk: string; sk: string };
58 |
59 | if (!check.pk || !check.sk || typeof check.pk !== 'string' || typeof check.sk !== 'string') {
60 | return false;
61 | }
62 |
63 | const { pk, sk } = check;
64 |
65 | try {
66 | Joi.assert(pk, stateLockRequest.pk);
67 | Joi.assert(sk, stateLockRequest.sk);
68 | } catch (e) {
69 | return false;
70 | }
71 |
72 | return true;
73 | };
74 | }
75 |
--------------------------------------------------------------------------------
/src/models/StateModel.ts:
--------------------------------------------------------------------------------
1 | /**
2 | Copyright 2022 Scaffoldly LLC
3 |
4 | Licensed under the Apache License, Version 2.0 (the "License");
5 | you may not use this file except in compliance with the License.
6 | You may obtain a copy of the License at
7 |
8 | http://www.apache.org/licenses/LICENSE-2.0
9 |
10 | Unless required by applicable law or agreed to in writing, software
11 | distributed under the License is distributed on an "AS IS" BASIS,
12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | See the License for the specific language governing permissions and
14 | limitations under the License.
15 | */
16 | import {
17 | Joi,
18 | Model,
19 | SERVICE_NAME,
20 | STAGE,
21 | Table,
22 | unmarshallDynamoDBImage,
23 | } from '@scaffoldly/serverless-util';
24 | import { StreamRecord } from 'aws-lambda';
25 | import { State } from './interfaces';
26 | import { state } from './schemas/State';
27 |
28 | const TABLE_SUFFIX = '';
29 |
30 | export class StateModel {
31 | public readonly table: Table;
32 |
33 | public readonly model: Model;
34 |
35 | constructor() {
36 | this.table = new Table(TABLE_SUFFIX, SERVICE_NAME, STAGE, state, 'pk', 'sk', [
37 | { hashKey: 'sk', rangeKey: 'pk', name: 'sk-pk-index', type: 'global' },
38 | ]);
39 |
40 | this.model = this.table.model;
41 | }
42 |
43 | // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
44 | static prefix = (col: 'pk' | 'sk', value?: any): string => {
45 | if (col === 'pk') {
46 | return `github_${value || ''}`;
47 | }
48 | return `state_${value || ''}`;
49 | };
50 |
51 | // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
52 | static isState = (record: StreamRecord): boolean => {
53 | if (!record) {
54 | return false;
55 | }
56 |
57 | const check = unmarshallDynamoDBImage(record.Keys) as { pk: string; sk: string };
58 |
59 | if (!check.pk || !check.sk || typeof check.pk !== 'string' || typeof check.sk !== 'string') {
60 | return false;
61 | }
62 |
63 | const { pk, sk } = check;
64 |
65 | try {
66 | Joi.assert(pk, state.pk);
67 | Joi.assert(sk, state.sk);
68 | } catch (e) {
69 | return false;
70 | }
71 |
72 | return true;
73 | };
74 | }
75 |
--------------------------------------------------------------------------------
/src/models/schemas/EncryptedField.ts:
--------------------------------------------------------------------------------
1 | /**
2 | Copyright 2022 Scaffoldly LLC
3 |
4 | Licensed under the Apache License, Version 2.0 (the "License");
5 | you may not use this file except in compliance with the License.
6 | You may obtain a copy of the License at
7 |
8 | http://www.apache.org/licenses/LICENSE-2.0
9 |
10 | Unless required by applicable law or agreed to in writing, software
11 | distributed under the License is distributed on an "AS IS" BASIS,
12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | See the License for the specific language governing permissions and
14 | limitations under the License.
15 | */
16 | import Joi from 'joi';
17 |
18 | export const encryptedFieldSchema = Joi.object({
19 | keyId: Joi.string().required(),
20 | encryptedValue: Joi.string().required(),
21 | }).label('EncryptedField');
22 |
--------------------------------------------------------------------------------
/src/models/schemas/Identity.ts:
--------------------------------------------------------------------------------
1 | /**
2 | Copyright 2022 Scaffoldly LLC
3 |
4 | Licensed under the Apache License, Version 2.0 (the "License");
5 | you may not use this file except in compliance with the License.
6 | You may obtain a copy of the License at
7 |
8 | http://www.apache.org/licenses/LICENSE-2.0
9 |
10 | Unless required by applicable law or agreed to in writing, software
11 | distributed under the License is distributed on an "AS IS" BASIS,
12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | See the License for the specific language governing permissions and
14 | limitations under the License.
15 | */
16 | import Joi from 'joi';
17 |
18 | export const metaSchema = Joi.object({
19 | name: Joi.string().required(),
20 | createdAt: Joi.string().required(),
21 | }).label('IdentityMeta');
22 |
23 | export const identity = {
24 | pk: Joi.string()
25 | .regex(/github_(.*)/) // github_${tokenSha}
26 | .required(),
27 | sk: Joi.string()
28 | .regex(/identity/) // identity
29 | .required(),
30 | tokenSha: Joi.string().required(),
31 | owner: Joi.string().required(),
32 | ownerId: Joi.number().required(),
33 | repo: Joi.string().required(),
34 | repoId: Joi.number().required(),
35 | workspace: Joi.string().required(),
36 | meta: metaSchema.required(),
37 | };
38 |
39 | export const identitySchema = Joi.object(identity).label('Identity');
40 |
--------------------------------------------------------------------------------
/src/models/schemas/State.ts:
--------------------------------------------------------------------------------
1 | /**
2 | Copyright 2022 Scaffoldly LLC
3 |
4 | Licensed under the Apache License, Version 2.0 (the "License");
5 | you may not use this file except in compliance with the License.
6 | You may obtain a copy of the License at
7 |
8 | http://www.apache.org/licenses/LICENSE-2.0
9 |
10 | Unless required by applicable law or agreed to in writing, software
11 | distributed under the License is distributed on an "AS IS" BASIS,
12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | See the License for the specific language governing permissions and
14 | limitations under the License.
15 | */
16 | import Joi from 'joi';
17 |
18 | export const s3Meta = Joi.object({
19 | bucket: Joi.string().required(),
20 | key: Joi.string().required(),
21 | etag: Joi.string().required(),
22 | location: Joi.string().required(),
23 | kmsKeyId: Joi.string().required(),
24 | }).label('S3Meta');
25 |
26 | export const state = {
27 | pk: Joi.string()
28 | .regex(/github_(.*)/) // github_${ownerId}
29 | .required(),
30 | sk: Joi.string()
31 | .regex(/state_(.*)/) // state_${repoId}_${workspace}
32 | .required(),
33 | ownerId: Joi.number().required(),
34 | repoId: Joi.number().required(),
35 | workspace: Joi.string().required(),
36 | owner: Joi.string().required(),
37 | repo: Joi.string().required(),
38 | s3Meta: s3Meta.required(),
39 | };
40 |
41 | export const stateSchema = Joi.object(state).label('State');
42 |
--------------------------------------------------------------------------------
/src/models/schemas/StateLock.ts:
--------------------------------------------------------------------------------
1 | /**
2 | Copyright 2022 Scaffoldly LLC
3 |
4 | Licensed under the Apache License, Version 2.0 (the "License");
5 | you may not use this file except in compliance with the License.
6 | You may obtain a copy of the License at
7 |
8 | http://www.apache.org/licenses/LICENSE-2.0
9 |
10 | Unless required by applicable law or agreed to in writing, software
11 | distributed under the License is distributed on an "AS IS" BASIS,
12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | See the License for the specific language governing permissions and
14 | limitations under the License.
15 | */
16 | import Joi from 'joi';
17 |
18 | export const stateLock = {
19 | pk: Joi.string()
20 | .regex(/github_(.*)/) // github_${orgId}
21 | .required(),
22 | sk: Joi.string()
23 | .regex(/statelock_(.*)_(.*)_(.*)/) // statelock_${repoId}_${workspace}_${path}
24 | .required(),
25 | ownerId: Joi.number().required(),
26 | repoId: Joi.number().required(),
27 | workspace: Joi.string().required(),
28 | owner: Joi.string().required(),
29 | repo: Joi.string().required(),
30 | id: Joi.string().required(),
31 | path: Joi.string().allow('').required(),
32 | lockedBy: Joi.string().required(),
33 | };
34 |
35 | export const stateLockSchema = Joi.object(stateLock).label('StateLock');
36 |
--------------------------------------------------------------------------------
/src/models/schemas/StateLockRequest.ts:
--------------------------------------------------------------------------------
1 | /**
2 | Copyright 2022 Scaffoldly LLC
3 |
4 | Licensed under the Apache License, Version 2.0 (the "License");
5 | you may not use this file except in compliance with the License.
6 | You may obtain a copy of the License at
7 |
8 | http://www.apache.org/licenses/LICENSE-2.0
9 |
10 | Unless required by applicable law or agreed to in writing, software
11 | distributed under the License is distributed on an "AS IS" BASIS,
12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | See the License for the specific language governing permissions and
14 | limitations under the License.
15 | */
16 | import Joi from 'joi';
17 |
18 | export const stateLockRequest = {
19 | pk: Joi.string()
20 | .regex(/lock_(.*)/) // lock_${ID}
21 | .optional(),
22 | sk: Joi.string()
23 | .regex(/statelock/) // statelock
24 | .optional(),
25 | ID: Joi.string().optional(),
26 | Operation: Joi.string().optional(),
27 | Info: Joi.string().allow('').optional(),
28 | Who: Joi.string().optional(),
29 | Version: Joi.string().optional(),
30 | Created: Joi.string().optional(),
31 | Path: Joi.string().allow('').optional(),
32 | stateLock: Joi.object({
33 | pk: Joi.string().required(),
34 | sk: Joi.string().required(),
35 | }).optional(),
36 | identity: Joi.object({
37 | pk: Joi.string().required(),
38 | sk: Joi.string().required(),
39 | }).optional(),
40 | };
41 |
42 | export const stateLockRequestSchema = Joi.object(stateLockRequest).label('StateLockRequest');
43 |
--------------------------------------------------------------------------------
/src/services/GithubService.ts:
--------------------------------------------------------------------------------
1 | /**
2 | Copyright 2022 Scaffoldly LLC
3 |
4 | Licensed under the Apache License, Version 2.0 (the "License");
5 | you may not use this file except in compliance with the License.
6 | You may obtain a copy of the License at
7 |
8 | http://www.apache.org/licenses/LICENSE-2.0
9 |
10 | Unless required by applicable law or agreed to in writing, software
11 | distributed under the License is distributed on an "AS IS" BASIS,
12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | See the License for the specific language governing permissions and
14 | limitations under the License.
15 | */
16 | import { HttpRequest } from '@scaffoldly/serverless-util';
17 | import { Octokit } from '@octokit/rest';
18 | import { TerraformError } from '../interfaces/errors';
19 | import { Identity } from '../models/interfaces';
20 | import crypto from 'crypto';
21 | import { IdentityModel } from '../models/IdentityModel';
22 | import { StateLockRequest } from '../models/interfaces/StateLockRequest';
23 | import moment from 'moment';
24 | // import seedrandom from 'seedrandom';
25 |
26 | export type IdentityWithToken = Identity & {
27 | token: string;
28 | };
29 |
30 | const lowerCase = (str?: string): string | undefined => {
31 | if (!str) {
32 | return undefined;
33 | }
34 |
35 | return str.toLowerCase();
36 | };
37 |
38 | export class GithubService {
39 | identityModel: IdentityModel;
40 |
41 | constructor() {
42 | this.identityModel = new IdentityModel();
43 | }
44 |
45 | public getIdentity = async (
46 | request: HttpRequest,
47 | stateLockRequest?: StateLockRequest,
48 | ): Promise => {
49 | const { authorization } = request.headers;
50 | if (!authorization) {
51 | throw new TerraformError(401);
52 | }
53 |
54 | const [method, token] = authorization.split(' ');
55 |
56 | if (!method || !token) {
57 | throw new TerraformError(401);
58 | }
59 |
60 | if (lowerCase(method) !== 'basic') {
61 | console.warn(`Method ${method} is not 'basic'`);
62 | throw new TerraformError(401);
63 | }
64 |
65 | const decoded = Buffer.from(token, 'base64').toString('utf8');
66 |
67 | const [username, password] = decoded.split(':');
68 |
69 | let owner: string | undefined;
70 | let repo: string | undefined;
71 | let workspace: string | undefined;
72 |
73 | if (!password) {
74 | console.warn(`Missing password from authorization token`);
75 | throw new TerraformError(403);
76 | }
77 |
78 | if (username) {
79 | if (username.indexOf('@') !== -1) {
80 | [, workspace] = username.split('@');
81 | }
82 |
83 | [owner, repo] = username.split('/');
84 | if (!owner || !repo) {
85 | console.warn(
86 | `Username must be in the format of \`[{owner}/{repository}][@{workspace}]\``,
87 | username,
88 | );
89 | throw new TerraformError(403);
90 | }
91 |
92 | if (repo.indexOf('@') !== -1) {
93 | [repo] = repo.split('@');
94 | }
95 | }
96 |
97 | try {
98 | const identity = await this.inferIdentity(password, owner, repo, workspace, stateLockRequest);
99 |
100 | console.log(
101 | `Using identity: ${identity.owner}/${identity.repo} [${identity.ownerId}/${identity.repoId}]`,
102 | );
103 |
104 | return { ...identity, token: password };
105 | } catch (e) {
106 | if (e instanceof Error) {
107 | console.warn(`Error inferring identity`, e);
108 | throw new TerraformError(403);
109 | }
110 | throw e;
111 | }
112 | };
113 |
114 | private inferIdentity = async (
115 | auth: string,
116 | owner?: string,
117 | repo?: string,
118 | workspace?: string,
119 | stateLockRequest?: StateLockRequest,
120 | ): Promise => {
121 | const now = moment();
122 | const tokenSha = crypto.createHash('sha256').update(auth).digest().toString('base64');
123 |
124 | console.log(
125 | `Inferring identity (auth: ${auth.substring(
126 | 0,
127 | 10,
128 | )} sha: ${tokenSha} owner: ${owner}, repo: ${repo}, workspace: ${workspace}, stateLockRequest: ${JSON.stringify(
129 | stateLockRequest,
130 | )})`,
131 | );
132 |
133 | const storedIdentity = await this.identityModel.model.get(
134 | IdentityModel.prefix('pk', tokenSha),
135 | IdentityModel.prefix('sk'),
136 | );
137 |
138 | if (storedIdentity && stateLockRequest && stateLockRequest.Operation === 'OperationTypeApply') {
139 | // Terraform planfiles contain credentials from plan operations
140 | // Return the previously known identity from the plan operation
141 | console.log(`Found previously known identity (sha: ${tokenSha})`);
142 | return { ...storedIdentity.attrs, workspace: workspace || 'default' };
143 | }
144 |
145 | if (
146 | storedIdentity &&
147 | !stateLockRequest &&
148 | storedIdentity.attrs.meta.createdAt &&
149 | moment(storedIdentity.attrs.meta.createdAt).add(1, 'hour').isAfter(now)
150 | ) {
151 | console.log(`Found previously known identity used within 1 hour (sha: ${tokenSha})`);
152 | return { ...storedIdentity.attrs, workspace: workspace || 'default' };
153 | }
154 |
155 | const octokit = new Octokit({ auth });
156 |
157 | // Server-to-server tokens from GH Actions are permitted on a single repository
158 | const repositories = auth.startsWith('ghs_')
159 | ? (await octokit.apps.listReposAccessibleToInstallation()).data.repositories
160 | : [];
161 |
162 | const repository = repositories.length === 1 ? repositories[0] : undefined;
163 | let name = repository ? repository.full_name : undefined;
164 |
165 | if (!name && !auth.startsWith('ghs_')) {
166 | name = (await octokit.users.getAuthenticated()).data.login;
167 | }
168 |
169 | if (!name && (owner || repo)) {
170 | name = `${owner}/${repo}`;
171 | }
172 |
173 | if (!name) {
174 | name = 'unknown';
175 | }
176 |
177 | let identity: Identity | undefined;
178 |
179 | if (repository && !owner && !repo) {
180 | identity = {
181 | pk: IdentityModel.prefix('pk', tokenSha),
182 | sk: IdentityModel.prefix('sk'),
183 | owner: repository.owner.login,
184 | ownerId: repository.owner.id,
185 | repo: repository.name,
186 | repoId: repository.id,
187 | workspace: workspace || 'default',
188 | tokenSha,
189 | meta: {
190 | name,
191 | createdAt: now.toISOString(),
192 | },
193 | };
194 | }
195 |
196 | if (
197 | !identity &&
198 | repository &&
199 | lowerCase(owner) === lowerCase(repository.owner.login) &&
200 | lowerCase(repo) === lowerCase(repository.name)
201 | ) {
202 | identity = {
203 | pk: IdentityModel.prefix('pk', tokenSha),
204 | sk: IdentityModel.prefix('sk'),
205 | owner: repository.owner.login,
206 | ownerId: repository.owner.id,
207 | repo: repository.name,
208 | repoId: repository.id,
209 | workspace: workspace || 'default',
210 | tokenSha,
211 | meta: {
212 | name,
213 | createdAt: now.toISOString(),
214 | },
215 | };
216 | }
217 |
218 | if (!identity && owner && repo) {
219 | console.log(`Fetching repository ${owner}/${repo}`);
220 | const data = await octokit.repos.get({ owner, repo });
221 |
222 | // TODO: Restrict access to state for public repositories?
223 |
224 | identity = {
225 | pk: IdentityModel.prefix('pk', tokenSha),
226 | sk: IdentityModel.prefix('sk'),
227 | owner: data.data.owner.login,
228 | ownerId: data.data.owner.id,
229 | repo: data.data.name,
230 | repoId: data.data.id,
231 | workspace: workspace || 'default',
232 | tokenSha,
233 | meta: {
234 | name,
235 | createdAt: now.toISOString(),
236 | },
237 | };
238 | }
239 |
240 | if (identity) {
241 | // Terraform Stores the backend identity in the planfile, store a hash of the token to use later
242 | const saved = await this.identityModel.model.create(identity);
243 | return saved.attrs;
244 | }
245 |
246 | console.warn(`Unable to infer repository (auth: ${auth.substring(0, 10)}`);
247 |
248 | throw new Error(
249 | `Unable to determine owner and/or repository from token privileges. Ensure \`username\` is in the format of \`{owner}/{repository}\`, and the provided \`password\` (a GitHub token) has access to that repository.`,
250 | );
251 | };
252 | }
253 |
--------------------------------------------------------------------------------
/src/services/StateService.ts:
--------------------------------------------------------------------------------
1 | /**
2 | Copyright 2022 Scaffoldly LLC
3 |
4 | Licensed under the Apache License, Version 2.0 (the "License");
5 | you may not use this file except in compliance with the License.
6 | You may obtain a copy of the License at
7 |
8 | http://www.apache.org/licenses/LICENSE-2.0
9 |
10 | Unless required by applicable law or agreed to in writing, software
11 | distributed under the License is distributed on an "AS IS" BASIS,
12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | See the License for the specific language governing permissions and
14 | limitations under the License.
15 | */
16 | import { StateLockModel } from '../models/StateLockModel';
17 | import { StateModel } from '../models/StateModel';
18 | import crypto from 'crypto';
19 | import { TerraformError } from '../interfaces/errors';
20 | import { IdentityWithToken } from './GithubService';
21 | import { S3 } from '@scaffoldly/serverless-util';
22 | import { env } from '../env';
23 | import { StateLockRequest } from '../models/interfaces/StateLockRequest';
24 | import { StateLockRequestModel } from '../models/StateLockRequestModel';
25 |
26 | export class StateService {
27 | stateModel: StateModel;
28 |
29 | stateLockModel: StateLockModel;
30 |
31 | stateLockRequestModel: StateLockRequestModel;
32 |
33 | constructor() {
34 | this.stateModel = new StateModel();
35 | this.stateLockModel = new StateLockModel();
36 | this.stateLockRequestModel = new StateLockRequestModel();
37 | }
38 |
39 | // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
40 | public saveState = async (identity: IdentityWithToken, id: string, state: any): Promise => {
41 | const lockedBy = crypto.createHash('sha256').update(identity.token, 'utf8').digest('base64');
42 |
43 | const stateLockRequest = await this.getRequest(id);
44 |
45 | const [stateLocks] = await this.stateLockModel.model
46 | .query(StateLockModel.prefix('pk', identity.ownerId))
47 | .where('sk')
48 | .beginsWith(StateLockModel.prefix('sk', `${identity.repoId}_${identity.workspace}`))
49 | .filter('id')
50 | .eq(id)
51 | .exec()
52 | .promise();
53 |
54 | if (!stateLocks || !stateLocks.Count) {
55 | throw new TerraformError(400);
56 | }
57 |
58 | const [stateLock] = stateLocks.Items;
59 |
60 | if (stateLock.attrs.lockedBy !== lockedBy) {
61 | console.warn(
62 | `State is locked by ${identity.meta.name} for ${identity.owner}/${identity.repo} on workspace ${identity.workspace}.`,
63 | );
64 | throw new TerraformError(409, stateLockRequest);
65 | }
66 |
67 | const s3 = await S3();
68 | const upload = await s3
69 | .upload({
70 | Bucket: env.bucket,
71 | Key: `${identity.ownerId}/${identity.repoId}/${identity.workspace}.tfstate`,
72 | ServerSideEncryption: 'aws:kms',
73 | SSEKMSKeyId: env['key-id'],
74 | Body: JSON.stringify(state),
75 | })
76 | .promise();
77 |
78 | await this.stateModel.model.create({
79 | pk: StateModel.prefix('pk', identity.ownerId),
80 | sk: StateModel.prefix('sk', `${identity.repoId}_${identity.workspace}`),
81 | ownerId: identity.ownerId,
82 | owner: identity.owner,
83 | repoId: identity.repoId,
84 | repo: identity.repo,
85 | workspace: identity.workspace,
86 | s3Meta: {
87 | bucket: upload.Bucket,
88 | key: upload.Key,
89 | etag: upload.ETag,
90 | location: upload.Location,
91 | kmsKeyId: env['key-id'],
92 | },
93 | });
94 | };
95 |
96 | public getState = async (identity: IdentityWithToken): Promise => {
97 | const state = await this.stateModel.model.get(
98 | StateModel.prefix('pk', identity.ownerId),
99 | StateModel.prefix('sk', `${identity.repoId}_${identity.workspace}`),
100 | );
101 |
102 | if (!state) {
103 | console.warn(
104 | `State not found (pk: ${StateModel.prefix('pk', identity.ownerId)} sk: ${StateModel.prefix(
105 | 'sk',
106 | `${identity.repoId}_${identity.workspace}`,
107 | )})`,
108 | );
109 | return null;
110 | }
111 |
112 | const { s3Meta } = state.attrs;
113 |
114 | console.log(`Fetching state from S3`, s3Meta);
115 |
116 | const s3 = await S3();
117 | const download = await s3.getObject({ Bucket: s3Meta.bucket, Key: s3Meta.key }).promise();
118 |
119 | const { Body } = download;
120 |
121 | if (!Body) {
122 | console.warn(`State not found in S3`);
123 | return null;
124 | }
125 |
126 | return JSON.parse(Body.toString());
127 | };
128 |
129 | public lockState = async (
130 | identity: IdentityWithToken,
131 | stateLockRequest: StateLockRequest,
132 | ): Promise => {
133 | const lockedBy = crypto.createHash('sha256').update(identity.token, 'utf8').digest('base64');
134 | const path = stateLockRequest.Path || '';
135 |
136 | const pk = StateLockModel.prefix('pk', identity.ownerId);
137 | const sk = StateLockModel.prefix('sk', `${identity.repoId}_${identity.workspace}_${path}`);
138 |
139 | console.log(`Acquiring state lock (pk: ${pk} sk: ${sk})`);
140 |
141 | let stateLock = await this.stateLockModel.model.get(pk, sk);
142 |
143 | if (stateLock && stateLock.attrs.lockedBy !== lockedBy) {
144 | console.warn(
145 | `State is locked by ${identity.meta.name} for ${identity.owner}/${identity.repo} on workspace ${identity.workspace}.`,
146 | );
147 | throw new TerraformError(409, stateLockRequest);
148 | }
149 |
150 | if (!stateLockRequest.ID) {
151 | console.warn(`Missing ID on stateLockRequest`);
152 | throw new TerraformError(400);
153 | }
154 |
155 | try {
156 | stateLock = await this.stateLockModel.model.create(
157 | {
158 | pk,
159 | sk,
160 | ownerId: identity.ownerId,
161 | owner: identity.owner,
162 | repoId: identity.repoId,
163 | repo: identity.repo,
164 | workspace: identity.workspace,
165 | id: stateLockRequest.ID,
166 | path,
167 | lockedBy,
168 | },
169 | { overwrite: false },
170 | );
171 | } catch (e) {
172 | if ((e as AWS.AWSError).code === 'ConditionalCheckFailedException') {
173 | throw new TerraformError(409, stateLockRequest);
174 | }
175 | throw e;
176 | }
177 |
178 | await this.stateLockRequestModel.model.update({
179 | pk: stateLockRequest.pk,
180 | sk: stateLockRequest.sk,
181 | stateLock: {
182 | pk,
183 | sk,
184 | },
185 | identity: {
186 | pk: identity.pk,
187 | sk: identity.sk,
188 | },
189 | });
190 | };
191 |
192 | public unlockState = async (
193 | identity: IdentityWithToken,
194 | stateLockRequest?: StateLockRequest,
195 | ): Promise => {
196 | const path = stateLockRequest ? stateLockRequest.Path || '' : '';
197 | const pk = StateLockModel.prefix('pk', identity.ownerId);
198 | const sk = StateLockModel.prefix('sk', `${identity.repoId}_${identity.workspace}_${path}`);
199 |
200 | console.log(`Releasing state lock (pk: ${pk} sk: ${sk})`);
201 |
202 | const stateLock = await this.stateLockModel.model.get(pk, sk);
203 |
204 | if (!stateLock) {
205 | console.log(
206 | `No state locks for ${identity.ownerId}/${identity.repoId} on workspace ${identity.workspace} with path ${path}`,
207 | );
208 | return;
209 | }
210 |
211 | await this.stateLockModel.model.destroy(stateLock.attrs.pk, stateLock.attrs.sk);
212 | };
213 |
214 | public saveRequest = async (stateLockRequest: StateLockRequest): Promise => {
215 | const saved = await this.stateLockRequestModel.model.create(
216 | {
217 | ...stateLockRequest,
218 | pk: StateLockRequestModel.prefix('pk', stateLockRequest.ID),
219 | sk: StateLockRequestModel.prefix('sk'),
220 | },
221 | { overwrite: false },
222 | );
223 |
224 | console.log('Saved state lock request', saved.attrs);
225 |
226 | return saved.attrs;
227 | };
228 |
229 | public getRequest = async (id: string): Promise => {
230 | const saved = await this.stateLockRequestModel.model.get(
231 | StateLockRequestModel.prefix('pk', id),
232 | StateLockRequestModel.prefix('sk'),
233 | );
234 |
235 | if (!saved) {
236 | throw new TerraformError(404);
237 | }
238 |
239 | return saved.attrs;
240 | };
241 | }
242 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compileOnSave": true,
3 | "compilerOptions": {
4 | /* Basic Options */
5 | "incremental": true,
6 | "target": "ESNext",
7 | "outDir": ".build/",
8 | "sourceMap": true,
9 |
10 | /* Strict Type-Checking Options */
11 | "strict": true,
12 | "noImplicitAny": true,
13 | "strictNullChecks": true,
14 | "strictFunctionTypes": true,
15 | "strictBindCallApply": true,
16 | "strictPropertyInitialization": true,
17 | "noImplicitThis": true,
18 | "alwaysStrict": true,
19 |
20 | /* Additional Checks */
21 | "noUnusedLocals": true,
22 | "noUnusedParameters": true,
23 | "noImplicitReturns": true,
24 | "noFallthroughCasesInSwitch": true,
25 |
26 | /* Module Resolution Options */
27 | "moduleResolution": "node",
28 | "baseUrl": ".",
29 | "esModuleInterop": true,
30 | "resolveJsonModule": true,
31 |
32 | /* Experimental Options */
33 | "experimentalDecorators": true,
34 | "emitDecoratorMetadata": true,
35 |
36 | /* Advanced Options */
37 | "forceConsistentCasingInFileNames": true
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/tsoa.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/no-var-requires */
2 | const { generateRoutes, generateSpec } = require('tsoa');
3 | const fs = require('fs');
4 | const packageJson = require('./package.json');
5 |
6 | const { NODE_ENV } = process.env;
7 | const envVars = NODE_ENV
8 | ? JSON.parse(fs.readFileSync(fs.openSync(`.scaffoldly/${NODE_ENV}/env-vars.json`)))
9 | : JSON.parse(fs.readFileSync(fs.openSync(`.scaffoldly/env-vars.json`)));
10 |
11 | const services = NODE_ENV
12 | ? JSON.parse(fs.readFileSync(fs.openSync(`.scaffoldly/${NODE_ENV}/services.json`)))
13 | : JSON.parse(fs.readFileSync(fs.openSync(`.scaffoldly/services.json`)));
14 |
15 | (async () => {
16 | console.log('Generating spec...');
17 | await generateSpec({
18 | basePath: `/${envVars['service-slug']}`,
19 | name: envVars['application-name'],
20 | version: packageJson.version,
21 | description: `Terraform Remote State API`,
22 | entryFile: 'src/app.ts',
23 | noImplicitAdditionalProperties: 'throw-on-extras',
24 | controllerPathGlobs: ['src/**/*Controller*.ts'],
25 | outputDirectory: 'src',
26 | specVersion: 3,
27 | // securityDefinitions: {
28 | // jwt: {
29 | // type: 'http',
30 | // scheme: 'bearer',
31 | // bearerFormat: 'JWT',
32 | // },
33 | // },
34 | });
35 |
36 | console.log('Generating routes...');
37 | await generateRoutes({
38 | entryFile: 'src/app.ts',
39 | noImplicitAdditionalProperties: 'throw-on-extras',
40 | controllerPathGlobs: ['src/**/*Controller*.ts'],
41 | routesDir: 'src',
42 | // authenticationModule: 'src/auth.ts',
43 | noWriteIfUnchanged: true,
44 | });
45 | })();
46 |
--------------------------------------------------------------------------------
/types.ts:
--------------------------------------------------------------------------------
1 | import { convertFromDirectory } from 'joi-to-typescript';
2 |
3 | async function types(): Promise {
4 | console.log('Running joi-to-typescript...');
5 |
6 | const result = await convertFromDirectory({
7 | schemaDirectory: './src/models/schemas',
8 | typeOutputDirectory: './src/models/interfaces',
9 | });
10 |
11 | if (result) {
12 | console.log('Completed joi-to-typescript');
13 | } else {
14 | console.log('Failed to run joi-to-typescript');
15 | }
16 | }
17 |
18 | (async () => {
19 | try {
20 | await types();
21 | } catch (e: any) {
22 | console.warn('Error generating types', e.message);
23 | }
24 | })();
25 |
--------------------------------------------------------------------------------