create-svc 0.1.10 → 0.1.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +51 -47
- package/index.ts +2 -2
- package/package.json +10 -9
- package/src/cli.test.ts +28 -10
- package/src/cli.ts +196 -33
- package/src/git-bootstrap.test.ts +40 -0
- package/src/git-bootstrap.ts +110 -0
- package/src/naming.test.ts +1 -0
- package/src/naming.ts +23 -0
- package/src/post-scaffold.test.ts +19 -0
- package/src/post-scaffold.ts +17 -4
- package/src/profiles.ts +2 -5
- package/src/scaffold.test.ts +232 -41
- package/src/scaffold.ts +81 -36
- package/src/service.test.ts +30 -0
- package/src/service.ts +65 -0
- package/src/vault.test.ts +61 -1
- package/src/vault.ts +77 -15
- package/templates/shared/.github/workflows/ci.yml +2 -1
- package/templates/shared/.github/workflows/deploy.yml +2 -0
- package/templates/shared/README.md +124 -47
- package/templates/shared/grafana/alerts.yaml +54 -0
- package/templates/shared/grafana/waitlist-dashboard.json +63 -0
- package/templates/shared/scripts/authctl.ts +231 -0
- package/templates/shared/scripts/cloudrun/bootstrap.ts +14 -5
- package/templates/shared/scripts/cloudrun/cleanup.ts +64 -4
- package/templates/shared/scripts/cloudrun/cli.ts +329 -7
- package/templates/shared/scripts/cloudrun/config.ts +11 -4
- package/templates/shared/scripts/cloudrun/deploy.ts +0 -4
- package/templates/shared/scripts/cloudrun/lib.ts +174 -41
- package/templates/shared/scripts/cloudrun/neon.ts +45 -0
- package/templates/shared/scripts/dev.ts +22 -0
- package/templates/shared/scripts/ensure-local-db.ts +3 -0
- package/templates/shared/scripts/local-docker.ts +63 -0
- package/templates/shared/scripts/local-env.ts +27 -0
- package/templates/shared/scripts/seed.ts +73 -0
- package/templates/shared/scripts/wait-for-db.ts +32 -0
- package/templates/shared/service.config.ts +59 -0
- package/templates/shared/service.yaml +24 -44
- package/templates/targets/workers/.github/workflows/ci.yml +19 -0
- package/templates/targets/workers/.github/workflows/deploy.yml +19 -0
- package/templates/targets/workers/Makefile +33 -0
- package/templates/targets/workers/README.md +75 -0
- package/templates/targets/workers/package.json +35 -0
- package/templates/targets/workers/scripts/workers/cli.ts +402 -0
- package/templates/targets/workers/src/auth.ts +178 -0
- package/templates/targets/workers/src/index.ts +198 -0
- package/templates/targets/workers/src/storage.ts +370 -0
- package/templates/targets/workers/test/app.test.ts +108 -0
- package/templates/targets/workers/tsconfig.json +11 -0
- package/templates/targets/workers/wrangler.toml +24 -0
- package/templates/variants/bun-connectrpc/Makefile +14 -8
- package/templates/variants/bun-connectrpc/gen/protos/waitlist/v1/waitlist_pb.ts +424 -0
- package/templates/variants/bun-connectrpc/migrations/0000_init.sql +12 -55
- package/templates/variants/bun-connectrpc/package.json +12 -5
- package/templates/variants/bun-connectrpc/protos/waitlist/v1/waitlist.proto +91 -0
- package/templates/variants/bun-connectrpc/scripts/codegen.ts +1 -1
- package/templates/variants/bun-connectrpc/scripts/migrate.ts +4 -1
- package/templates/variants/bun-connectrpc/src/auth.ts +200 -0
- package/templates/variants/bun-connectrpc/src/db/repository.ts +67 -420
- package/templates/variants/bun-connectrpc/src/db/schema.ts +15 -64
- package/templates/variants/bun-connectrpc/src/index.ts +76 -176
- package/templates/variants/bun-connectrpc/src/temporal/activities.ts +14 -0
- package/templates/variants/bun-connectrpc/src/temporal/worker.ts +38 -0
- package/templates/variants/bun-connectrpc/src/temporal/workflows.ts +10 -0
- package/templates/variants/bun-connectrpc/src/waitlist/service.ts +172 -0
- package/templates/variants/bun-connectrpc/src/waitlist/types.ts +45 -0
- package/templates/variants/bun-connectrpc/test/app.test.ts +4 -4
- package/templates/variants/bun-connectrpc/test/waitlist.integration.test.ts +71 -0
- package/templates/variants/bun-hono/Makefile +14 -8
- package/templates/variants/bun-hono/migrations/0000_init.sql +12 -55
- package/templates/variants/bun-hono/package.json +12 -5
- package/templates/variants/bun-hono/scripts/migrate.ts +4 -1
- package/templates/variants/bun-hono/src/auth.ts +181 -0
- package/templates/variants/bun-hono/src/db/repository.ts +68 -421
- package/templates/variants/bun-hono/src/db/schema.ts +15 -64
- package/templates/variants/bun-hono/src/index.ts +65 -180
- package/templates/variants/bun-hono/src/temporal/activities.ts +14 -0
- package/templates/variants/bun-hono/src/temporal/worker.ts +38 -0
- package/templates/variants/bun-hono/src/temporal/workflows.ts +10 -0
- package/templates/variants/bun-hono/src/waitlist/service.ts +166 -0
- package/templates/variants/bun-hono/src/waitlist/types.ts +50 -0
- package/templates/variants/bun-hono/test/app.test.ts +72 -41
- package/templates/variants/bun-hono/test/waitlist.integration.test.ts +102 -0
- package/templates/variants/go-chi/Makefile +27 -11
- package/templates/variants/go-chi/atlas.hcl +8 -0
- package/templates/variants/go-chi/cmd/server/main.go +21 -10
- package/templates/variants/go-chi/go.mod +1 -3
- package/templates/variants/go-chi/internal/app/service.go +202 -685
- package/templates/variants/go-chi/internal/auth/middleware.go +289 -0
- package/templates/variants/go-chi/internal/auth/middleware_test.go +38 -0
- package/templates/variants/go-chi/internal/config/config.go +27 -11
- package/templates/variants/go-chi/internal/httpapi/routes.go +78 -157
- package/templates/variants/go-chi/internal/httpapi/waitlist_integration_test.go +199 -0
- package/templates/variants/go-chi/internal/temporal/activities.go +27 -0
- package/templates/variants/go-chi/internal/temporal/worker.go +42 -0
- package/templates/variants/go-chi/internal/temporal/workflows.go +18 -0
- package/templates/variants/go-chi/migrations/0000_init.sql +12 -55
- package/templates/variants/go-chi/migrations/atlas.sum +2 -0
- package/templates/variants/go-chi/package.json +7 -1
- package/templates/variants/go-connectrpc/Makefile +26 -9
- package/templates/variants/go-connectrpc/atlas.hcl +8 -0
- package/templates/variants/go-connectrpc/buf.gen.yaml +2 -2
- package/templates/variants/go-connectrpc/cmd/server/main.go +23 -12
- package/templates/variants/go-connectrpc/gen/waitlist/v1/waitlist.pb.go +960 -0
- package/templates/variants/go-connectrpc/gen/waitlist/v1/waitlistv1connect/waitlist.connect.go +283 -0
- package/templates/variants/go-connectrpc/go.mod +1 -1
- package/templates/variants/go-connectrpc/internal/app/service.go +202 -685
- package/templates/variants/go-connectrpc/internal/auth/middleware.go +289 -0
- package/templates/variants/go-connectrpc/internal/auth/middleware_test.go +38 -0
- package/templates/variants/go-connectrpc/internal/config/config.go +27 -11
- package/templates/variants/go-connectrpc/internal/connectapi/handler.go +78 -201
- package/templates/variants/go-connectrpc/internal/connectapi/waitlist_integration_test.go +122 -0
- package/templates/variants/go-connectrpc/internal/httpapi/routes.go +147 -9
- package/templates/variants/go-connectrpc/internal/temporal/activities.go +27 -0
- package/templates/variants/go-connectrpc/internal/temporal/worker.go +42 -0
- package/templates/variants/go-connectrpc/internal/temporal/workflows.go +18 -0
- package/templates/variants/go-connectrpc/migrations/0000_init.sql +12 -55
- package/templates/variants/go-connectrpc/migrations/atlas.sum +2 -0
- package/templates/variants/go-connectrpc/package.json +7 -1
- package/templates/variants/go-connectrpc/protos/waitlist/v1/waitlist.proto +93 -0
- package/templates/root/.github/workflows/buf-publish.yml +0 -19
- package/templates/root/.github/workflows/ci.yml +0 -26
- package/templates/root/.github/workflows/deploy.yml +0 -22
- package/templates/root/Dockerfile +0 -23
- package/templates/root/README.md +0 -69
- package/templates/root/buf.gen.yaml +0 -10
- package/templates/root/buf.yaml +0 -9
- package/templates/root/cmd/server/main.go +0 -44
- package/templates/root/gen/dns/v1/dns.pb.go +0 -623
- package/templates/root/gen/dns/v1/dnsv1connect/dns.connect.go +0 -192
- package/templates/root/go.mod +0 -10
- package/templates/root/internal/app/service.go +0 -152
- package/templates/root/internal/app/token_source.go +0 -50
- package/templates/root/internal/cloudflare/client.go +0 -160
- package/templates/root/internal/config/config.go +0 -55
- package/templates/root/internal/connectapi/handler.go +0 -79
- package/templates/root/internal/httpapi/routes.go +0 -93
- package/templates/root/internal/vault/client.go +0 -148
- package/templates/root/package.json +0 -12
- package/templates/root/protos/dns/v1/dns.proto +0 -58
- package/templates/root/scripts/cloudrun/bootstrap.ts +0 -65
- package/templates/root/scripts/cloudrun/config.ts +0 -50
- package/templates/root/scripts/cloudrun/deploy.ts +0 -41
- package/templates/root/scripts/cloudrun/lib.ts +0 -244
- package/templates/root/service.yaml +0 -50
- package/templates/root/test/go.test.ts +0 -19
- package/templates/shared/scripts/cloudrun/integrations.ts +0 -111
- package/templates/variants/bun-connectrpc/gen/protos/chat/v1/chat_pb.ts +0 -1078
- package/templates/variants/bun-connectrpc/protos/chat/v1/chat.proto +0 -228
- package/templates/variants/bun-connectrpc/src/chat/service.ts +0 -384
- package/templates/variants/bun-connectrpc/src/chat/types.ts +0 -142
- package/templates/variants/bun-connectrpc/src/storage.ts +0 -72
- package/templates/variants/bun-connectrpc/src/webhooks.ts +0 -35
- package/templates/variants/bun-connectrpc/test/list-messages.integration.test.ts +0 -182
- package/templates/variants/bun-hono/src/chat/service.ts +0 -384
- package/templates/variants/bun-hono/src/chat/types.ts +0 -142
- package/templates/variants/bun-hono/src/storage.ts +0 -72
- package/templates/variants/bun-hono/src/webhooks.ts +0 -35
- package/templates/variants/bun-hono/test/list-messages.integration.test.ts +0 -256
- package/templates/variants/go-chi/buf.gen.yaml +0 -12
- package/templates/variants/go-chi/buf.yaml +0 -9
- package/templates/variants/go-chi/cmd/migrate/main.go +0 -101
- package/templates/variants/go-chi/internal/httpapi/list_messages_integration_test.go +0 -298
- package/templates/variants/go-chi/protos/chat/v1/chat.proto +0 -219
- package/templates/variants/go-connectrpc/cmd/migrate/main.go +0 -101
- package/templates/variants/go-connectrpc/gen/chat/v1/chat.pb.go +0 -2512
- package/templates/variants/go-connectrpc/gen/chat/v1/chatv1connect/chat.connect.go +0 -571
- package/templates/variants/go-connectrpc/internal/connectapi/list_messages_integration_test.go +0 -216
- package/templates/variants/go-connectrpc/protos/chat/v1/chat.proto +0 -232
- /package/bin/{create-svc.mjs → service.mjs} +0 -0
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
package auth
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"context"
|
|
5
|
+
"crypto"
|
|
6
|
+
"crypto/ecdsa"
|
|
7
|
+
"crypto/ed25519"
|
|
8
|
+
"crypto/elliptic"
|
|
9
|
+
"crypto/rsa"
|
|
10
|
+
"crypto/sha256"
|
|
11
|
+
"encoding/base64"
|
|
12
|
+
"encoding/json"
|
|
13
|
+
"errors"
|
|
14
|
+
"math/big"
|
|
15
|
+
"net/http"
|
|
16
|
+
"strings"
|
|
17
|
+
"sync"
|
|
18
|
+
"time"
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
type Config struct {
|
|
22
|
+
Enabled bool
|
|
23
|
+
Issuer string
|
|
24
|
+
Audience string
|
|
25
|
+
JWKSURL string
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
type jwksCache struct {
|
|
29
|
+
mu sync.Mutex
|
|
30
|
+
expiresAt time.Time
|
|
31
|
+
keys []jwk
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
type jwtHeader struct {
|
|
35
|
+
Alg string `json:"alg"`
|
|
36
|
+
Kid string `json:"kid"`
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
type jwtClaims struct {
|
|
40
|
+
Issuer string `json:"iss"`
|
|
41
|
+
Audience json.RawMessage `json:"aud"`
|
|
42
|
+
ExpiresAt int64 `json:"exp"`
|
|
43
|
+
NotBefore int64 `json:"nbf"`
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
type jwksDocument struct {
|
|
47
|
+
Keys []jwk `json:"keys"`
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
type jwk struct {
|
|
51
|
+
Kty string `json:"kty"`
|
|
52
|
+
Kid string `json:"kid"`
|
|
53
|
+
Alg string `json:"alg"`
|
|
54
|
+
N string `json:"n"`
|
|
55
|
+
E string `json:"e"`
|
|
56
|
+
Crv string `json:"crv"`
|
|
57
|
+
X string `json:"x"`
|
|
58
|
+
Y string `json:"y"`
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
func Middleware(cfg Config) func(http.Handler) http.Handler {
|
|
62
|
+
cache := &jwksCache{}
|
|
63
|
+
return func(next http.Handler) http.Handler {
|
|
64
|
+
return http.HandlerFunc(func(w http.ResponseWriter, request *http.Request) {
|
|
65
|
+
if !cfg.Enabled || publicPath(request) {
|
|
66
|
+
next.ServeHTTP(w, request)
|
|
67
|
+
return
|
|
68
|
+
}
|
|
69
|
+
token := bearerToken(request.Header.Get("Authorization"))
|
|
70
|
+
if token == "" || verifyToken(request.Context(), token, cfg, cache) != nil {
|
|
71
|
+
writeUnauthorized(w)
|
|
72
|
+
return
|
|
73
|
+
}
|
|
74
|
+
next.ServeHTTP(w, request)
|
|
75
|
+
})
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
func publicPath(request *http.Request) bool {
|
|
80
|
+
path := request.URL.Path
|
|
81
|
+
return path == "/" || path == "/healthz" || path == "/readyz" || strings.HasPrefix(path, "/webhooks/")
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
func verifyToken(ctx context.Context, token string, cfg Config, cache *jwksCache) error {
|
|
85
|
+
if cfg.Issuer == "" || cfg.Audience == "" || cfg.JWKSURL == "" {
|
|
86
|
+
return errors.New("auth config is incomplete")
|
|
87
|
+
}
|
|
88
|
+
parts := strings.Split(token, ".")
|
|
89
|
+
if len(parts) != 3 {
|
|
90
|
+
return errors.New("token must have three parts")
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
var header jwtHeader
|
|
94
|
+
if err := decodeJSON(parts[0], &header); err != nil {
|
|
95
|
+
return err
|
|
96
|
+
}
|
|
97
|
+
var claims jwtClaims
|
|
98
|
+
if err := decodeJSON(parts[1], &claims); err != nil {
|
|
99
|
+
return err
|
|
100
|
+
}
|
|
101
|
+
key, err := cache.key(ctx, cfg.JWKSURL, header.Kid)
|
|
102
|
+
if err != nil {
|
|
103
|
+
return err
|
|
104
|
+
}
|
|
105
|
+
if err := verifySignature(header.Alg, key, []byte(parts[0]+"."+parts[1]), parts[2]); err != nil {
|
|
106
|
+
return err
|
|
107
|
+
}
|
|
108
|
+
return validateClaims(claims, cfg)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
func (cache *jwksCache) key(ctx context.Context, jwksURL string, kid string) (jwk, error) {
|
|
112
|
+
cache.mu.Lock()
|
|
113
|
+
defer cache.mu.Unlock()
|
|
114
|
+
|
|
115
|
+
if time.Now().After(cache.expiresAt) {
|
|
116
|
+
request, err := http.NewRequestWithContext(ctx, http.MethodGet, jwksURL, nil)
|
|
117
|
+
if err != nil {
|
|
118
|
+
return jwk{}, err
|
|
119
|
+
}
|
|
120
|
+
response, err := http.DefaultClient.Do(request)
|
|
121
|
+
if err != nil {
|
|
122
|
+
return jwk{}, err
|
|
123
|
+
}
|
|
124
|
+
defer response.Body.Close()
|
|
125
|
+
if response.StatusCode < 200 || response.StatusCode >= 300 {
|
|
126
|
+
return jwk{}, errors.New("jwks fetch failed")
|
|
127
|
+
}
|
|
128
|
+
var document jwksDocument
|
|
129
|
+
if err := json.NewDecoder(response.Body).Decode(&document); err != nil {
|
|
130
|
+
return jwk{}, err
|
|
131
|
+
}
|
|
132
|
+
cache.keys = document.Keys
|
|
133
|
+
cache.expiresAt = time.Now().Add(5 * time.Minute)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if kid == "" && len(cache.keys) == 1 {
|
|
137
|
+
return cache.keys[0], nil
|
|
138
|
+
}
|
|
139
|
+
for _, key := range cache.keys {
|
|
140
|
+
if key.Kid == kid {
|
|
141
|
+
return key, nil
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return jwk{}, errors.New("jwk not found")
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
func verifySignature(alg string, key jwk, signingInput []byte, encodedSignature string) error {
|
|
148
|
+
signature, err := base64.RawURLEncoding.DecodeString(encodedSignature)
|
|
149
|
+
if err != nil {
|
|
150
|
+
return err
|
|
151
|
+
}
|
|
152
|
+
digest := sha256.Sum256(signingInput)
|
|
153
|
+
switch alg {
|
|
154
|
+
case "RS256":
|
|
155
|
+
publicKey, err := rsaPublicKey(key)
|
|
156
|
+
if err != nil {
|
|
157
|
+
return err
|
|
158
|
+
}
|
|
159
|
+
return rsa.VerifyPKCS1v15(publicKey, crypto.SHA256, digest[:], signature)
|
|
160
|
+
case "ES256":
|
|
161
|
+
publicKey, err := ecdsaPublicKey(key)
|
|
162
|
+
if err != nil {
|
|
163
|
+
return err
|
|
164
|
+
}
|
|
165
|
+
if len(signature) != 64 {
|
|
166
|
+
return errors.New("invalid ES256 signature")
|
|
167
|
+
}
|
|
168
|
+
r := new(big.Int).SetBytes(signature[:32])
|
|
169
|
+
s := new(big.Int).SetBytes(signature[32:])
|
|
170
|
+
if !ecdsa.Verify(publicKey, digest[:], r, s) {
|
|
171
|
+
return errors.New("invalid ES256 signature")
|
|
172
|
+
}
|
|
173
|
+
return nil
|
|
174
|
+
case "EdDSA":
|
|
175
|
+
publicKey, err := ed25519PublicKey(key)
|
|
176
|
+
if err != nil {
|
|
177
|
+
return err
|
|
178
|
+
}
|
|
179
|
+
if !ed25519.Verify(publicKey, signingInput, signature) {
|
|
180
|
+
return errors.New("invalid EdDSA signature")
|
|
181
|
+
}
|
|
182
|
+
return nil
|
|
183
|
+
default:
|
|
184
|
+
return errors.New("unsupported jwt alg")
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
func rsaPublicKey(key jwk) (*rsa.PublicKey, error) {
|
|
189
|
+
n, err := base64.RawURLEncoding.DecodeString(key.N)
|
|
190
|
+
if err != nil {
|
|
191
|
+
return nil, err
|
|
192
|
+
}
|
|
193
|
+
eBytes, err := base64.RawURLEncoding.DecodeString(key.E)
|
|
194
|
+
if err != nil {
|
|
195
|
+
return nil, err
|
|
196
|
+
}
|
|
197
|
+
e := 0
|
|
198
|
+
for _, b := range eBytes {
|
|
199
|
+
e = e<<8 + int(b)
|
|
200
|
+
}
|
|
201
|
+
return &rsa.PublicKey{N: new(big.Int).SetBytes(n), E: e}, nil
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
func ecdsaPublicKey(key jwk) (*ecdsa.PublicKey, error) {
|
|
205
|
+
if key.Crv != "P-256" {
|
|
206
|
+
return nil, errors.New("unsupported ecdsa curve")
|
|
207
|
+
}
|
|
208
|
+
x, err := base64.RawURLEncoding.DecodeString(key.X)
|
|
209
|
+
if err != nil {
|
|
210
|
+
return nil, err
|
|
211
|
+
}
|
|
212
|
+
y, err := base64.RawURLEncoding.DecodeString(key.Y)
|
|
213
|
+
if err != nil {
|
|
214
|
+
return nil, err
|
|
215
|
+
}
|
|
216
|
+
return &ecdsa.PublicKey{Curve: elliptic.P256(), X: new(big.Int).SetBytes(x), Y: new(big.Int).SetBytes(y)}, nil
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
func ed25519PublicKey(key jwk) (ed25519.PublicKey, error) {
|
|
220
|
+
if key.Crv != "Ed25519" {
|
|
221
|
+
return nil, errors.New("unsupported eddsa curve")
|
|
222
|
+
}
|
|
223
|
+
x, err := base64.RawURLEncoding.DecodeString(key.X)
|
|
224
|
+
if err != nil {
|
|
225
|
+
return nil, err
|
|
226
|
+
}
|
|
227
|
+
if len(x) != ed25519.PublicKeySize {
|
|
228
|
+
return nil, errors.New("invalid Ed25519 public key")
|
|
229
|
+
}
|
|
230
|
+
return ed25519.PublicKey(x), nil
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
func validateClaims(claims jwtClaims, cfg Config) error {
|
|
234
|
+
now := time.Now().Unix()
|
|
235
|
+
if claims.Issuer != cfg.Issuer {
|
|
236
|
+
return errors.New("issuer mismatch")
|
|
237
|
+
}
|
|
238
|
+
if !audienceMatches(claims.Audience, cfg.Audience) {
|
|
239
|
+
return errors.New("audience mismatch")
|
|
240
|
+
}
|
|
241
|
+
if claims.ExpiresAt == 0 || claims.ExpiresAt <= now-30 {
|
|
242
|
+
return errors.New("token expired")
|
|
243
|
+
}
|
|
244
|
+
if claims.NotBefore != 0 && claims.NotBefore > now+30 {
|
|
245
|
+
return errors.New("token not active")
|
|
246
|
+
}
|
|
247
|
+
return nil
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
func audienceMatches(raw json.RawMessage, expected string) bool {
|
|
251
|
+
var single string
|
|
252
|
+
if err := json.Unmarshal(raw, &single); err == nil {
|
|
253
|
+
return single == expected
|
|
254
|
+
}
|
|
255
|
+
var many []string
|
|
256
|
+
if err := json.Unmarshal(raw, &many); err == nil {
|
|
257
|
+
for _, audience := range many {
|
|
258
|
+
if audience == expected {
|
|
259
|
+
return true
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
return false
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
func decodeJSON(encoded string, out any) error {
|
|
267
|
+
payload, err := base64.RawURLEncoding.DecodeString(encoded)
|
|
268
|
+
if err != nil {
|
|
269
|
+
return err
|
|
270
|
+
}
|
|
271
|
+
return json.Unmarshal(payload, out)
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
func bearerToken(value string) string {
|
|
275
|
+
fields := strings.Fields(value)
|
|
276
|
+
if len(fields) != 2 || !strings.EqualFold(fields[0], "Bearer") {
|
|
277
|
+
return ""
|
|
278
|
+
}
|
|
279
|
+
return fields[1]
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
func writeUnauthorized(w http.ResponseWriter) {
|
|
283
|
+
w.Header().Set("Content-Type", "application/json")
|
|
284
|
+
w.WriteHeader(http.StatusUnauthorized)
|
|
285
|
+
_ = json.NewEncoder(w).Encode(map[string]string{
|
|
286
|
+
"error": "invalid bearer token",
|
|
287
|
+
"code": "unauthorized",
|
|
288
|
+
})
|
|
289
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
package auth
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"net/http"
|
|
5
|
+
"net/http/httptest"
|
|
6
|
+
"testing"
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
func TestMiddlewareRejectsProtectedPathWithoutBearerToken(t *testing.T) {
|
|
10
|
+
handler := Middleware(Config{
|
|
11
|
+
Enabled: true,
|
|
12
|
+
Issuer: "https://auth.anmho.com",
|
|
13
|
+
Audience: "api://{{SERVICE_ID}}",
|
|
14
|
+
JWKSURL: "https://auth.anmho.com/api/auth/jwks",
|
|
15
|
+
})(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
|
16
|
+
w.WriteHeader(http.StatusNoContent)
|
|
17
|
+
}))
|
|
18
|
+
|
|
19
|
+
response := httptest.NewRecorder()
|
|
20
|
+
handler.ServeHTTP(response, httptest.NewRequest(http.MethodPost, "/v1/waitlist", nil))
|
|
21
|
+
|
|
22
|
+
if response.Code != http.StatusUnauthorized {
|
|
23
|
+
t.Fatalf("expected 401, got %d", response.Code)
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
func TestMiddlewareLeavesHealthPublic(t *testing.T) {
|
|
28
|
+
handler := Middleware(Config{Enabled: true})(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
|
29
|
+
w.WriteHeader(http.StatusNoContent)
|
|
30
|
+
}))
|
|
31
|
+
|
|
32
|
+
response := httptest.NewRecorder()
|
|
33
|
+
handler.ServeHTTP(response, httptest.NewRequest(http.MethodGet, "/healthz", nil))
|
|
34
|
+
|
|
35
|
+
if response.Code != http.StatusNoContent {
|
|
36
|
+
t.Fatalf("expected health to pass through, got %d", response.Code)
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -7,28 +7,44 @@ import (
|
|
|
7
7
|
)
|
|
8
8
|
|
|
9
9
|
type Config struct {
|
|
10
|
-
Port
|
|
11
|
-
DatabaseURL
|
|
12
|
-
|
|
13
|
-
|
|
10
|
+
Port string
|
|
11
|
+
DatabaseURL string
|
|
12
|
+
TemporalEnabled bool
|
|
13
|
+
TemporalAddress string
|
|
14
|
+
TemporalNamespace string
|
|
15
|
+
TemporalTaskQueue string
|
|
16
|
+
TemporalAPIKey string
|
|
17
|
+
AuthEnabled bool
|
|
18
|
+
AuthIssuer string
|
|
19
|
+
AuthAudience string
|
|
20
|
+
AuthJWKSURL string
|
|
14
21
|
}
|
|
15
22
|
|
|
16
23
|
func Load() (Config, error) {
|
|
17
24
|
cfg := Config{
|
|
18
|
-
Port:
|
|
19
|
-
DatabaseURL:
|
|
20
|
-
|
|
21
|
-
|
|
25
|
+
Port: envOr("PORT", "8080"),
|
|
26
|
+
DatabaseURL: strings.TrimSpace(os.Getenv("DATABASE_URL")),
|
|
27
|
+
TemporalEnabled: envBool("TEMPORAL_ENABLED"),
|
|
28
|
+
TemporalAddress: envOr("TEMPORAL_ADDRESS", "localhost:7233"),
|
|
29
|
+
TemporalNamespace: envOr("TEMPORAL_NAMESPACE", "default"),
|
|
30
|
+
TemporalTaskQueue: envOr("TEMPORAL_TASK_QUEUE", "{{SERVICE_NAME}}"),
|
|
31
|
+
TemporalAPIKey: strings.TrimSpace(os.Getenv("TEMPORAL_API_KEY")),
|
|
32
|
+
AuthEnabled: envBool("AUTH_ENABLED"),
|
|
33
|
+
AuthIssuer: envOr("AUTH_ISSUER", "{{AUTH_ISSUER}}"),
|
|
34
|
+
AuthAudience: envOr("AUTH_AUDIENCE", "{{AUTH_AUDIENCE}}"),
|
|
35
|
+
AuthJWKSURL: envOr("AUTH_JWKS_URL", "{{AUTH_JWKS_URL}}"),
|
|
22
36
|
}
|
|
23
37
|
if cfg.DatabaseURL == "" {
|
|
24
38
|
return Config{}, errors.New("DATABASE_URL is required")
|
|
25
39
|
}
|
|
26
|
-
if cfg.AttachmentBucket == "" {
|
|
27
|
-
return Config{}, errors.New("ATTACHMENT_BUCKET is required")
|
|
28
|
-
}
|
|
29
40
|
return cfg, nil
|
|
30
41
|
}
|
|
31
42
|
|
|
43
|
+
func envBool(key string) bool {
|
|
44
|
+
value := strings.TrimSpace(strings.ToLower(os.Getenv(key)))
|
|
45
|
+
return value == "1" || value == "true" || value == "yes" || value == "on"
|
|
46
|
+
}
|
|
47
|
+
|
|
32
48
|
func envOr(key string, fallback string) string {
|
|
33
49
|
value := os.Getenv(key)
|
|
34
50
|
if value != "" {
|