create-svc 0.1.10 → 0.1.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (168) hide show
  1. package/README.md +46 -43
  2. package/bin/create-service.mjs +2 -0
  3. package/package.json +12 -9
  4. package/src/cli.test.ts +28 -10
  5. package/src/cli.ts +195 -30
  6. package/src/git-bootstrap.test.ts +40 -0
  7. package/src/git-bootstrap.ts +110 -0
  8. package/src/naming.test.ts +1 -0
  9. package/src/naming.ts +23 -0
  10. package/src/post-scaffold.test.ts +19 -0
  11. package/src/post-scaffold.ts +17 -4
  12. package/src/profiles.ts +2 -5
  13. package/src/scaffold.test.ts +231 -40
  14. package/src/scaffold.ts +84 -29
  15. package/src/vault.test.ts +61 -1
  16. package/src/vault.ts +77 -15
  17. package/templates/shared/.github/workflows/ci.yml +2 -1
  18. package/templates/shared/.github/workflows/deploy.yml +2 -0
  19. package/templates/shared/README.md +124 -47
  20. package/templates/shared/grafana/alerts.yaml +54 -0
  21. package/templates/shared/grafana/waitlist-dashboard.json +63 -0
  22. package/templates/shared/scripts/authctl.ts +231 -0
  23. package/templates/shared/scripts/cloudrun/bootstrap.ts +14 -5
  24. package/templates/shared/scripts/cloudrun/cleanup.ts +64 -4
  25. package/templates/shared/scripts/cloudrun/cli.ts +324 -7
  26. package/templates/shared/scripts/cloudrun/config.ts +11 -4
  27. package/templates/shared/scripts/cloudrun/deploy.ts +0 -4
  28. package/templates/shared/scripts/cloudrun/lib.ts +174 -41
  29. package/templates/shared/scripts/cloudrun/neon.ts +45 -0
  30. package/templates/shared/scripts/dev.ts +22 -0
  31. package/templates/shared/scripts/ensure-local-db.ts +3 -0
  32. package/templates/shared/scripts/local-docker.ts +63 -0
  33. package/templates/shared/scripts/local-env.ts +27 -0
  34. package/templates/shared/scripts/seed.ts +73 -0
  35. package/templates/shared/scripts/wait-for-db.ts +32 -0
  36. package/templates/shared/service.config.ts +59 -0
  37. package/templates/shared/service.yaml +24 -44
  38. package/templates/targets/workers/.github/workflows/ci.yml +19 -0
  39. package/templates/targets/workers/.github/workflows/deploy.yml +19 -0
  40. package/templates/targets/workers/Makefile +33 -0
  41. package/templates/targets/workers/README.md +75 -0
  42. package/templates/targets/workers/package.json +35 -0
  43. package/templates/targets/workers/scripts/workers/cli.ts +397 -0
  44. package/templates/targets/workers/src/auth.ts +178 -0
  45. package/templates/targets/workers/src/index.ts +198 -0
  46. package/templates/targets/workers/src/storage.ts +370 -0
  47. package/templates/targets/workers/test/app.test.ts +108 -0
  48. package/templates/targets/workers/tsconfig.json +11 -0
  49. package/templates/targets/workers/wrangler.toml +24 -0
  50. package/templates/variants/bun-connectrpc/Makefile +14 -8
  51. package/templates/variants/bun-connectrpc/gen/protos/waitlist/v1/waitlist_pb.ts +424 -0
  52. package/templates/variants/bun-connectrpc/migrations/0000_init.sql +12 -55
  53. package/templates/variants/bun-connectrpc/package.json +12 -5
  54. package/templates/variants/bun-connectrpc/protos/waitlist/v1/waitlist.proto +91 -0
  55. package/templates/variants/bun-connectrpc/scripts/codegen.ts +1 -1
  56. package/templates/variants/bun-connectrpc/scripts/migrate.ts +4 -1
  57. package/templates/variants/bun-connectrpc/src/auth.ts +200 -0
  58. package/templates/variants/bun-connectrpc/src/db/repository.ts +67 -420
  59. package/templates/variants/bun-connectrpc/src/db/schema.ts +15 -64
  60. package/templates/variants/bun-connectrpc/src/index.ts +76 -176
  61. package/templates/variants/bun-connectrpc/src/temporal/activities.ts +14 -0
  62. package/templates/variants/bun-connectrpc/src/temporal/worker.ts +38 -0
  63. package/templates/variants/bun-connectrpc/src/temporal/workflows.ts +10 -0
  64. package/templates/variants/bun-connectrpc/src/waitlist/service.ts +172 -0
  65. package/templates/variants/bun-connectrpc/src/waitlist/types.ts +45 -0
  66. package/templates/variants/bun-connectrpc/test/app.test.ts +4 -4
  67. package/templates/variants/bun-connectrpc/test/waitlist.integration.test.ts +71 -0
  68. package/templates/variants/bun-hono/Makefile +14 -8
  69. package/templates/variants/bun-hono/migrations/0000_init.sql +12 -55
  70. package/templates/variants/bun-hono/package.json +12 -5
  71. package/templates/variants/bun-hono/scripts/migrate.ts +4 -1
  72. package/templates/variants/bun-hono/src/auth.ts +181 -0
  73. package/templates/variants/bun-hono/src/db/repository.ts +68 -421
  74. package/templates/variants/bun-hono/src/db/schema.ts +15 -64
  75. package/templates/variants/bun-hono/src/index.ts +65 -180
  76. package/templates/variants/bun-hono/src/temporal/activities.ts +14 -0
  77. package/templates/variants/bun-hono/src/temporal/worker.ts +38 -0
  78. package/templates/variants/bun-hono/src/temporal/workflows.ts +10 -0
  79. package/templates/variants/bun-hono/src/waitlist/service.ts +166 -0
  80. package/templates/variants/bun-hono/src/waitlist/types.ts +50 -0
  81. package/templates/variants/bun-hono/test/app.test.ts +72 -41
  82. package/templates/variants/bun-hono/test/waitlist.integration.test.ts +102 -0
  83. package/templates/variants/go-chi/Makefile +27 -11
  84. package/templates/variants/go-chi/atlas.hcl +8 -0
  85. package/templates/variants/go-chi/cmd/server/main.go +21 -10
  86. package/templates/variants/go-chi/go.mod +1 -3
  87. package/templates/variants/go-chi/internal/app/service.go +202 -685
  88. package/templates/variants/go-chi/internal/auth/middleware.go +289 -0
  89. package/templates/variants/go-chi/internal/auth/middleware_test.go +38 -0
  90. package/templates/variants/go-chi/internal/config/config.go +27 -11
  91. package/templates/variants/go-chi/internal/httpapi/routes.go +78 -157
  92. package/templates/variants/go-chi/internal/httpapi/waitlist_integration_test.go +199 -0
  93. package/templates/variants/go-chi/internal/temporal/activities.go +27 -0
  94. package/templates/variants/go-chi/internal/temporal/worker.go +42 -0
  95. package/templates/variants/go-chi/internal/temporal/workflows.go +18 -0
  96. package/templates/variants/go-chi/migrations/0000_init.sql +12 -55
  97. package/templates/variants/go-chi/migrations/atlas.sum +2 -0
  98. package/templates/variants/go-chi/package.json +7 -1
  99. package/templates/variants/go-connectrpc/Makefile +26 -9
  100. package/templates/variants/go-connectrpc/atlas.hcl +8 -0
  101. package/templates/variants/go-connectrpc/buf.gen.yaml +2 -2
  102. package/templates/variants/go-connectrpc/cmd/server/main.go +23 -12
  103. package/templates/variants/go-connectrpc/gen/waitlist/v1/waitlist.pb.go +960 -0
  104. package/templates/variants/go-connectrpc/gen/waitlist/v1/waitlistv1connect/waitlist.connect.go +283 -0
  105. package/templates/variants/go-connectrpc/go.mod +1 -1
  106. package/templates/variants/go-connectrpc/internal/app/service.go +202 -685
  107. package/templates/variants/go-connectrpc/internal/auth/middleware.go +289 -0
  108. package/templates/variants/go-connectrpc/internal/auth/middleware_test.go +38 -0
  109. package/templates/variants/go-connectrpc/internal/config/config.go +27 -11
  110. package/templates/variants/go-connectrpc/internal/connectapi/handler.go +78 -201
  111. package/templates/variants/go-connectrpc/internal/connectapi/waitlist_integration_test.go +122 -0
  112. package/templates/variants/go-connectrpc/internal/httpapi/routes.go +147 -9
  113. package/templates/variants/go-connectrpc/internal/temporal/activities.go +27 -0
  114. package/templates/variants/go-connectrpc/internal/temporal/worker.go +42 -0
  115. package/templates/variants/go-connectrpc/internal/temporal/workflows.go +18 -0
  116. package/templates/variants/go-connectrpc/migrations/0000_init.sql +12 -55
  117. package/templates/variants/go-connectrpc/migrations/atlas.sum +2 -0
  118. package/templates/variants/go-connectrpc/package.json +7 -1
  119. package/templates/variants/go-connectrpc/protos/waitlist/v1/waitlist.proto +93 -0
  120. package/templates/root/.github/workflows/buf-publish.yml +0 -19
  121. package/templates/root/.github/workflows/ci.yml +0 -26
  122. package/templates/root/.github/workflows/deploy.yml +0 -22
  123. package/templates/root/Dockerfile +0 -23
  124. package/templates/root/README.md +0 -69
  125. package/templates/root/buf.gen.yaml +0 -10
  126. package/templates/root/buf.yaml +0 -9
  127. package/templates/root/cmd/server/main.go +0 -44
  128. package/templates/root/gen/dns/v1/dns.pb.go +0 -623
  129. package/templates/root/gen/dns/v1/dnsv1connect/dns.connect.go +0 -192
  130. package/templates/root/go.mod +0 -10
  131. package/templates/root/internal/app/service.go +0 -152
  132. package/templates/root/internal/app/token_source.go +0 -50
  133. package/templates/root/internal/cloudflare/client.go +0 -160
  134. package/templates/root/internal/config/config.go +0 -55
  135. package/templates/root/internal/connectapi/handler.go +0 -79
  136. package/templates/root/internal/httpapi/routes.go +0 -93
  137. package/templates/root/internal/vault/client.go +0 -148
  138. package/templates/root/package.json +0 -12
  139. package/templates/root/protos/dns/v1/dns.proto +0 -58
  140. package/templates/root/scripts/cloudrun/bootstrap.ts +0 -65
  141. package/templates/root/scripts/cloudrun/config.ts +0 -50
  142. package/templates/root/scripts/cloudrun/deploy.ts +0 -41
  143. package/templates/root/scripts/cloudrun/lib.ts +0 -244
  144. package/templates/root/service.yaml +0 -50
  145. package/templates/root/test/go.test.ts +0 -19
  146. package/templates/shared/scripts/cloudrun/integrations.ts +0 -111
  147. package/templates/variants/bun-connectrpc/gen/protos/chat/v1/chat_pb.ts +0 -1078
  148. package/templates/variants/bun-connectrpc/protos/chat/v1/chat.proto +0 -228
  149. package/templates/variants/bun-connectrpc/src/chat/service.ts +0 -384
  150. package/templates/variants/bun-connectrpc/src/chat/types.ts +0 -142
  151. package/templates/variants/bun-connectrpc/src/storage.ts +0 -72
  152. package/templates/variants/bun-connectrpc/src/webhooks.ts +0 -35
  153. package/templates/variants/bun-connectrpc/test/list-messages.integration.test.ts +0 -182
  154. package/templates/variants/bun-hono/src/chat/service.ts +0 -384
  155. package/templates/variants/bun-hono/src/chat/types.ts +0 -142
  156. package/templates/variants/bun-hono/src/storage.ts +0 -72
  157. package/templates/variants/bun-hono/src/webhooks.ts +0 -35
  158. package/templates/variants/bun-hono/test/list-messages.integration.test.ts +0 -256
  159. package/templates/variants/go-chi/buf.gen.yaml +0 -12
  160. package/templates/variants/go-chi/buf.yaml +0 -9
  161. package/templates/variants/go-chi/cmd/migrate/main.go +0 -101
  162. package/templates/variants/go-chi/internal/httpapi/list_messages_integration_test.go +0 -298
  163. package/templates/variants/go-chi/protos/chat/v1/chat.proto +0 -219
  164. package/templates/variants/go-connectrpc/cmd/migrate/main.go +0 -101
  165. package/templates/variants/go-connectrpc/gen/chat/v1/chat.pb.go +0 -2512
  166. package/templates/variants/go-connectrpc/gen/chat/v1/chatv1connect/chat.connect.go +0 -571
  167. package/templates/variants/go-connectrpc/internal/connectapi/list_messages_integration_test.go +0 -216
  168. package/templates/variants/go-connectrpc/protos/chat/v1/chat.proto +0 -232
@@ -0,0 +1,200 @@
1
+ type AuthConfig = {
2
+ enabled: boolean;
3
+ issuer: string;
4
+ audience: string;
5
+ jwksUrl: string;
6
+ };
7
+
8
+ type JwtHeader = {
9
+ alg?: string;
10
+ kid?: string;
11
+ };
12
+
13
+ type JwtClaims = {
14
+ iss?: string;
15
+ aud?: string | string[];
16
+ exp?: number;
17
+ nbf?: number;
18
+ };
19
+
20
+ type Jwk = JsonWebKey & {
21
+ kid?: string;
22
+ };
23
+
24
+ type Jwks = {
25
+ keys: Jwk[];
26
+ };
27
+
28
+ type NodeHandler = (request: any, response: any) => void;
29
+
30
+ const encoder = new TextEncoder();
31
+ const jwksCache = new Map<string, { expiresAt: number; jwks: Jwks }>();
32
+
33
+ export function withServiceAuth(handler: NodeHandler): NodeHandler {
34
+ return (request, response) => {
35
+ void authorizeRequest(request)
36
+ .then((authorized) => {
37
+ if (!authorized) {
38
+ respondUnauthorized(response);
39
+ return;
40
+ }
41
+ handler(request, response);
42
+ })
43
+ .catch(() => respondUnauthorized(response));
44
+ };
45
+ }
46
+
47
+ async function authorizeRequest(request: any) {
48
+ const config = authConfigFromEnv();
49
+ if (!config.enabled || isPublicPath(request)) {
50
+ return true;
51
+ }
52
+
53
+ const authorization = Array.isArray(request.headers.authorization)
54
+ ? request.headers.authorization[0]
55
+ : request.headers.authorization ?? "";
56
+ const token = bearerToken(authorization);
57
+ if (!token) {
58
+ return false;
59
+ }
60
+
61
+ await verifyAccessToken(token, config);
62
+ return true;
63
+ }
64
+
65
+ function authConfigFromEnv(): AuthConfig {
66
+ return {
67
+ enabled: truthy(Bun.env.AUTH_ENABLED),
68
+ issuer: Bun.env.AUTH_ISSUER ?? "",
69
+ audience: Bun.env.AUTH_AUDIENCE ?? "",
70
+ jwksUrl: Bun.env.AUTH_JWKS_URL ?? "",
71
+ };
72
+ }
73
+
74
+ function isPublicPath(request: any) {
75
+ const path = new URL(request.url ?? "/", "http://localhost").pathname;
76
+ return path === "/" || path === "/healthz" || path === "/readyz" || path.startsWith("/webhooks/");
77
+ }
78
+
79
+ async function verifyAccessToken(token: string, config: AuthConfig): Promise<JwtClaims> {
80
+ const parts = token.split(".");
81
+ if (parts.length !== 3 || !config.issuer || !config.audience || !config.jwksUrl) {
82
+ throw new Error("invalid auth config or token");
83
+ }
84
+
85
+ const [encodedHeader, encodedPayload, encodedSignature] = parts;
86
+ const header = decodeJSON<JwtHeader>(encodedHeader);
87
+ const claims = decodeJSON<JwtClaims>(encodedPayload);
88
+ const jwks = await fetchJwks(config.jwksUrl);
89
+ const key = selectKey(jwks, header);
90
+ if (!key || !header.alg) {
91
+ throw new Error("matching jwk not found");
92
+ }
93
+
94
+ const algorithm = importAlgorithm(header.alg, key);
95
+ const cryptoKey = await crypto.subtle.importKey("jwk", key, algorithm.import, false, ["verify"]);
96
+ const verified = await crypto.subtle.verify(
97
+ algorithm.verify,
98
+ cryptoKey,
99
+ toArrayBuffer(decodeBase64Url(encodedSignature)),
100
+ encoder.encode(`${encodedHeader}.${encodedPayload}`)
101
+ );
102
+ if (!verified) {
103
+ throw new Error("bad signature");
104
+ }
105
+
106
+ validateClaims(claims, config);
107
+ return claims;
108
+ }
109
+
110
+ async function fetchJwks(jwksUrl: string): Promise<Jwks> {
111
+ const cached = jwksCache.get(jwksUrl);
112
+ if (cached && cached.expiresAt > Date.now()) {
113
+ return cached.jwks;
114
+ }
115
+
116
+ const response = await fetch(jwksUrl);
117
+ if (!response.ok) {
118
+ throw new Error(`jwks fetch failed: ${response.status}`);
119
+ }
120
+ const jwks = (await response.json()) as Jwks;
121
+ jwksCache.set(jwksUrl, { jwks, expiresAt: Date.now() + 5 * 60 * 1000 });
122
+ return jwks;
123
+ }
124
+
125
+ function selectKey(jwks: Jwks, header: JwtHeader): Jwk | undefined {
126
+ if (header.kid) {
127
+ return jwks.keys.find((key) => key.kid === header.kid);
128
+ }
129
+ return jwks.keys.length === 1 ? jwks.keys[0] : undefined;
130
+ }
131
+
132
+ function importAlgorithm(alg: string, key: JsonWebKey) {
133
+ if (alg === "RS256") {
134
+ return {
135
+ import: { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" },
136
+ verify: { name: "RSASSA-PKCS1-v1_5" },
137
+ } as const;
138
+ }
139
+ if (alg === "ES256" && key.crv === "P-256") {
140
+ return {
141
+ import: { name: "ECDSA", namedCurve: "P-256" },
142
+ verify: { name: "ECDSA", hash: "SHA-256" },
143
+ } as const;
144
+ }
145
+ if (alg === "EdDSA" && key.crv === "Ed25519") {
146
+ return {
147
+ import: { name: "Ed25519" },
148
+ verify: { name: "Ed25519" },
149
+ } as const;
150
+ }
151
+ throw new Error(`unsupported jwt alg: ${alg}`);
152
+ }
153
+
154
+ function validateClaims(claims: JwtClaims, config: AuthConfig) {
155
+ const now = Math.floor(Date.now() / 1000);
156
+ if (claims.iss !== config.issuer) {
157
+ throw new Error("issuer mismatch");
158
+ }
159
+ if (!audienceMatches(claims.aud, config.audience)) {
160
+ throw new Error("audience mismatch");
161
+ }
162
+ if (typeof claims.exp !== "number" || claims.exp <= now - 30) {
163
+ throw new Error("token expired");
164
+ }
165
+ if (typeof claims.nbf === "number" && claims.nbf > now + 30) {
166
+ throw new Error("token not active");
167
+ }
168
+ }
169
+
170
+ function audienceMatches(audience: JwtClaims["aud"], expected: string) {
171
+ return Array.isArray(audience) ? audience.includes(expected) : audience === expected;
172
+ }
173
+
174
+ function decodeJSON<T>(value: string): T {
175
+ return JSON.parse(new TextDecoder().decode(decodeBase64Url(value))) as T;
176
+ }
177
+
178
+ function decodeBase64Url(value: string): Uint8Array {
179
+ const base64 = value.replace(/-/g, "+").replace(/_/g, "/").padEnd(Math.ceil(value.length / 4) * 4, "=");
180
+ return Uint8Array.from(atob(base64), (char) => char.charCodeAt(0));
181
+ }
182
+
183
+ function toArrayBuffer(bytes: Uint8Array): ArrayBuffer {
184
+ return bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength) as ArrayBuffer;
185
+ }
186
+
187
+ function bearerToken(value: string) {
188
+ const [scheme, token] = value.trim().split(/\s+/, 2);
189
+ return /^Bearer$/i.test(scheme) ? token : "";
190
+ }
191
+
192
+ function respondUnauthorized(response: any) {
193
+ response.statusCode = 401;
194
+ response.setHeader("Content-Type", "application/json");
195
+ response.end(JSON.stringify({ error: "invalid bearer token", code: "unauthorized" }));
196
+ }
197
+
198
+ function truthy(value: string | undefined) {
199
+ return ["1", "true", "yes", "on"].includes((value ?? "").trim().toLowerCase());
200
+ }