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.
Files changed (171) hide show
  1. package/README.md +51 -47
  2. package/index.ts +2 -2
  3. package/package.json +10 -9
  4. package/src/cli.test.ts +28 -10
  5. package/src/cli.ts +196 -33
  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 +232 -41
  14. package/src/scaffold.ts +81 -36
  15. package/src/service.test.ts +30 -0
  16. package/src/service.ts +65 -0
  17. package/src/vault.test.ts +61 -1
  18. package/src/vault.ts +77 -15
  19. package/templates/shared/.github/workflows/ci.yml +2 -1
  20. package/templates/shared/.github/workflows/deploy.yml +2 -0
  21. package/templates/shared/README.md +124 -47
  22. package/templates/shared/grafana/alerts.yaml +54 -0
  23. package/templates/shared/grafana/waitlist-dashboard.json +63 -0
  24. package/templates/shared/scripts/authctl.ts +231 -0
  25. package/templates/shared/scripts/cloudrun/bootstrap.ts +14 -5
  26. package/templates/shared/scripts/cloudrun/cleanup.ts +64 -4
  27. package/templates/shared/scripts/cloudrun/cli.ts +329 -7
  28. package/templates/shared/scripts/cloudrun/config.ts +11 -4
  29. package/templates/shared/scripts/cloudrun/deploy.ts +0 -4
  30. package/templates/shared/scripts/cloudrun/lib.ts +174 -41
  31. package/templates/shared/scripts/cloudrun/neon.ts +45 -0
  32. package/templates/shared/scripts/dev.ts +22 -0
  33. package/templates/shared/scripts/ensure-local-db.ts +3 -0
  34. package/templates/shared/scripts/local-docker.ts +63 -0
  35. package/templates/shared/scripts/local-env.ts +27 -0
  36. package/templates/shared/scripts/seed.ts +73 -0
  37. package/templates/shared/scripts/wait-for-db.ts +32 -0
  38. package/templates/shared/service.config.ts +59 -0
  39. package/templates/shared/service.yaml +24 -44
  40. package/templates/targets/workers/.github/workflows/ci.yml +19 -0
  41. package/templates/targets/workers/.github/workflows/deploy.yml +19 -0
  42. package/templates/targets/workers/Makefile +33 -0
  43. package/templates/targets/workers/README.md +75 -0
  44. package/templates/targets/workers/package.json +35 -0
  45. package/templates/targets/workers/scripts/workers/cli.ts +402 -0
  46. package/templates/targets/workers/src/auth.ts +178 -0
  47. package/templates/targets/workers/src/index.ts +198 -0
  48. package/templates/targets/workers/src/storage.ts +370 -0
  49. package/templates/targets/workers/test/app.test.ts +108 -0
  50. package/templates/targets/workers/tsconfig.json +11 -0
  51. package/templates/targets/workers/wrangler.toml +24 -0
  52. package/templates/variants/bun-connectrpc/Makefile +14 -8
  53. package/templates/variants/bun-connectrpc/gen/protos/waitlist/v1/waitlist_pb.ts +424 -0
  54. package/templates/variants/bun-connectrpc/migrations/0000_init.sql +12 -55
  55. package/templates/variants/bun-connectrpc/package.json +12 -5
  56. package/templates/variants/bun-connectrpc/protos/waitlist/v1/waitlist.proto +91 -0
  57. package/templates/variants/bun-connectrpc/scripts/codegen.ts +1 -1
  58. package/templates/variants/bun-connectrpc/scripts/migrate.ts +4 -1
  59. package/templates/variants/bun-connectrpc/src/auth.ts +200 -0
  60. package/templates/variants/bun-connectrpc/src/db/repository.ts +67 -420
  61. package/templates/variants/bun-connectrpc/src/db/schema.ts +15 -64
  62. package/templates/variants/bun-connectrpc/src/index.ts +76 -176
  63. package/templates/variants/bun-connectrpc/src/temporal/activities.ts +14 -0
  64. package/templates/variants/bun-connectrpc/src/temporal/worker.ts +38 -0
  65. package/templates/variants/bun-connectrpc/src/temporal/workflows.ts +10 -0
  66. package/templates/variants/bun-connectrpc/src/waitlist/service.ts +172 -0
  67. package/templates/variants/bun-connectrpc/src/waitlist/types.ts +45 -0
  68. package/templates/variants/bun-connectrpc/test/app.test.ts +4 -4
  69. package/templates/variants/bun-connectrpc/test/waitlist.integration.test.ts +71 -0
  70. package/templates/variants/bun-hono/Makefile +14 -8
  71. package/templates/variants/bun-hono/migrations/0000_init.sql +12 -55
  72. package/templates/variants/bun-hono/package.json +12 -5
  73. package/templates/variants/bun-hono/scripts/migrate.ts +4 -1
  74. package/templates/variants/bun-hono/src/auth.ts +181 -0
  75. package/templates/variants/bun-hono/src/db/repository.ts +68 -421
  76. package/templates/variants/bun-hono/src/db/schema.ts +15 -64
  77. package/templates/variants/bun-hono/src/index.ts +65 -180
  78. package/templates/variants/bun-hono/src/temporal/activities.ts +14 -0
  79. package/templates/variants/bun-hono/src/temporal/worker.ts +38 -0
  80. package/templates/variants/bun-hono/src/temporal/workflows.ts +10 -0
  81. package/templates/variants/bun-hono/src/waitlist/service.ts +166 -0
  82. package/templates/variants/bun-hono/src/waitlist/types.ts +50 -0
  83. package/templates/variants/bun-hono/test/app.test.ts +72 -41
  84. package/templates/variants/bun-hono/test/waitlist.integration.test.ts +102 -0
  85. package/templates/variants/go-chi/Makefile +27 -11
  86. package/templates/variants/go-chi/atlas.hcl +8 -0
  87. package/templates/variants/go-chi/cmd/server/main.go +21 -10
  88. package/templates/variants/go-chi/go.mod +1 -3
  89. package/templates/variants/go-chi/internal/app/service.go +202 -685
  90. package/templates/variants/go-chi/internal/auth/middleware.go +289 -0
  91. package/templates/variants/go-chi/internal/auth/middleware_test.go +38 -0
  92. package/templates/variants/go-chi/internal/config/config.go +27 -11
  93. package/templates/variants/go-chi/internal/httpapi/routes.go +78 -157
  94. package/templates/variants/go-chi/internal/httpapi/waitlist_integration_test.go +199 -0
  95. package/templates/variants/go-chi/internal/temporal/activities.go +27 -0
  96. package/templates/variants/go-chi/internal/temporal/worker.go +42 -0
  97. package/templates/variants/go-chi/internal/temporal/workflows.go +18 -0
  98. package/templates/variants/go-chi/migrations/0000_init.sql +12 -55
  99. package/templates/variants/go-chi/migrations/atlas.sum +2 -0
  100. package/templates/variants/go-chi/package.json +7 -1
  101. package/templates/variants/go-connectrpc/Makefile +26 -9
  102. package/templates/variants/go-connectrpc/atlas.hcl +8 -0
  103. package/templates/variants/go-connectrpc/buf.gen.yaml +2 -2
  104. package/templates/variants/go-connectrpc/cmd/server/main.go +23 -12
  105. package/templates/variants/go-connectrpc/gen/waitlist/v1/waitlist.pb.go +960 -0
  106. package/templates/variants/go-connectrpc/gen/waitlist/v1/waitlistv1connect/waitlist.connect.go +283 -0
  107. package/templates/variants/go-connectrpc/go.mod +1 -1
  108. package/templates/variants/go-connectrpc/internal/app/service.go +202 -685
  109. package/templates/variants/go-connectrpc/internal/auth/middleware.go +289 -0
  110. package/templates/variants/go-connectrpc/internal/auth/middleware_test.go +38 -0
  111. package/templates/variants/go-connectrpc/internal/config/config.go +27 -11
  112. package/templates/variants/go-connectrpc/internal/connectapi/handler.go +78 -201
  113. package/templates/variants/go-connectrpc/internal/connectapi/waitlist_integration_test.go +122 -0
  114. package/templates/variants/go-connectrpc/internal/httpapi/routes.go +147 -9
  115. package/templates/variants/go-connectrpc/internal/temporal/activities.go +27 -0
  116. package/templates/variants/go-connectrpc/internal/temporal/worker.go +42 -0
  117. package/templates/variants/go-connectrpc/internal/temporal/workflows.go +18 -0
  118. package/templates/variants/go-connectrpc/migrations/0000_init.sql +12 -55
  119. package/templates/variants/go-connectrpc/migrations/atlas.sum +2 -0
  120. package/templates/variants/go-connectrpc/package.json +7 -1
  121. package/templates/variants/go-connectrpc/protos/waitlist/v1/waitlist.proto +93 -0
  122. package/templates/root/.github/workflows/buf-publish.yml +0 -19
  123. package/templates/root/.github/workflows/ci.yml +0 -26
  124. package/templates/root/.github/workflows/deploy.yml +0 -22
  125. package/templates/root/Dockerfile +0 -23
  126. package/templates/root/README.md +0 -69
  127. package/templates/root/buf.gen.yaml +0 -10
  128. package/templates/root/buf.yaml +0 -9
  129. package/templates/root/cmd/server/main.go +0 -44
  130. package/templates/root/gen/dns/v1/dns.pb.go +0 -623
  131. package/templates/root/gen/dns/v1/dnsv1connect/dns.connect.go +0 -192
  132. package/templates/root/go.mod +0 -10
  133. package/templates/root/internal/app/service.go +0 -152
  134. package/templates/root/internal/app/token_source.go +0 -50
  135. package/templates/root/internal/cloudflare/client.go +0 -160
  136. package/templates/root/internal/config/config.go +0 -55
  137. package/templates/root/internal/connectapi/handler.go +0 -79
  138. package/templates/root/internal/httpapi/routes.go +0 -93
  139. package/templates/root/internal/vault/client.go +0 -148
  140. package/templates/root/package.json +0 -12
  141. package/templates/root/protos/dns/v1/dns.proto +0 -58
  142. package/templates/root/scripts/cloudrun/bootstrap.ts +0 -65
  143. package/templates/root/scripts/cloudrun/config.ts +0 -50
  144. package/templates/root/scripts/cloudrun/deploy.ts +0 -41
  145. package/templates/root/scripts/cloudrun/lib.ts +0 -244
  146. package/templates/root/service.yaml +0 -50
  147. package/templates/root/test/go.test.ts +0 -19
  148. package/templates/shared/scripts/cloudrun/integrations.ts +0 -111
  149. package/templates/variants/bun-connectrpc/gen/protos/chat/v1/chat_pb.ts +0 -1078
  150. package/templates/variants/bun-connectrpc/protos/chat/v1/chat.proto +0 -228
  151. package/templates/variants/bun-connectrpc/src/chat/service.ts +0 -384
  152. package/templates/variants/bun-connectrpc/src/chat/types.ts +0 -142
  153. package/templates/variants/bun-connectrpc/src/storage.ts +0 -72
  154. package/templates/variants/bun-connectrpc/src/webhooks.ts +0 -35
  155. package/templates/variants/bun-connectrpc/test/list-messages.integration.test.ts +0 -182
  156. package/templates/variants/bun-hono/src/chat/service.ts +0 -384
  157. package/templates/variants/bun-hono/src/chat/types.ts +0 -142
  158. package/templates/variants/bun-hono/src/storage.ts +0 -72
  159. package/templates/variants/bun-hono/src/webhooks.ts +0 -35
  160. package/templates/variants/bun-hono/test/list-messages.integration.test.ts +0 -256
  161. package/templates/variants/go-chi/buf.gen.yaml +0 -12
  162. package/templates/variants/go-chi/buf.yaml +0 -9
  163. package/templates/variants/go-chi/cmd/migrate/main.go +0 -101
  164. package/templates/variants/go-chi/internal/httpapi/list_messages_integration_test.go +0 -298
  165. package/templates/variants/go-chi/protos/chat/v1/chat.proto +0 -219
  166. package/templates/variants/go-connectrpc/cmd/migrate/main.go +0 -101
  167. package/templates/variants/go-connectrpc/gen/chat/v1/chat.pb.go +0 -2512
  168. package/templates/variants/go-connectrpc/gen/chat/v1/chatv1connect/chat.connect.go +0 -571
  169. package/templates/variants/go-connectrpc/internal/connectapi/list_messages_integration_test.go +0 -216
  170. package/templates/variants/go-connectrpc/protos/chat/v1/chat.proto +0 -232
  171. /package/bin/{create-svc.mjs → service.mjs} +0 -0
@@ -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
+ }