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
package/src/service.ts
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { run as runScaffoldCli } from "./cli";
|
|
4
|
+
|
|
5
|
+
const SCAFFOLD_COMMANDS = new Set(["create", "new", "init"]);
|
|
6
|
+
|
|
7
|
+
export async function runServiceCommand(argv: string[], cwd = process.cwd()) {
|
|
8
|
+
const serviceRoot = findGeneratedServiceRoot(cwd);
|
|
9
|
+
if (serviceRoot) {
|
|
10
|
+
delegateToGeneratedService(serviceRoot, argv);
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
await runScaffoldCli(normalizeScaffoldArgs(argv));
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function normalizeScaffoldArgs(argv: string[]) {
|
|
18
|
+
const [command, ...rest] = argv;
|
|
19
|
+
if (command && SCAFFOLD_COMMANDS.has(command)) {
|
|
20
|
+
return rest;
|
|
21
|
+
}
|
|
22
|
+
if (command === "help") {
|
|
23
|
+
return ["--help", ...rest];
|
|
24
|
+
}
|
|
25
|
+
return argv;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function findGeneratedServiceRoot(start: string): string | undefined {
|
|
29
|
+
let current = start;
|
|
30
|
+
while (true) {
|
|
31
|
+
if (isGeneratedServiceRoot(current)) {
|
|
32
|
+
return current;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const parent = dirname(current);
|
|
36
|
+
if (parent === current) {
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
current = parent;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function isGeneratedServiceRoot(path: string) {
|
|
44
|
+
return (
|
|
45
|
+
existsSync(join(path, "service.config.ts")) &&
|
|
46
|
+
(existsSync(join(path, "scripts", "cloudrun", "cli.ts")) || existsSync(join(path, "scripts", "workers", "cli.ts")))
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function delegateToGeneratedService(serviceRoot: string, argv: string[]) {
|
|
51
|
+
const cliPath = existsSync(join(serviceRoot, "scripts", "cloudrun", "cli.ts"))
|
|
52
|
+
? "./scripts/cloudrun/cli.ts"
|
|
53
|
+
: "./scripts/workers/cli.ts";
|
|
54
|
+
const result = Bun.spawnSync(["bun", "run", cliPath, ...argv], {
|
|
55
|
+
cwd: serviceRoot,
|
|
56
|
+
env: process.env,
|
|
57
|
+
stdin: "inherit",
|
|
58
|
+
stdout: "inherit",
|
|
59
|
+
stderr: "inherit",
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
if (!result.success) {
|
|
63
|
+
process.exit(result.exitCode || 1);
|
|
64
|
+
}
|
|
65
|
+
}
|
package/src/vault.test.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { afterEach, expect, mock, test } from "bun:test";
|
|
2
2
|
import { mkdir } from "node:fs/promises";
|
|
3
|
-
import { readVaultSecret, resolveNeonApiKey } from "./vault";
|
|
3
|
+
import { readVaultSecret, resolveNeonApiKey, upsertVaultSecretFields } from "./vault";
|
|
4
4
|
|
|
5
5
|
const originalEnv = { ...process.env };
|
|
6
6
|
|
|
@@ -97,3 +97,63 @@ test("readVaultSecret falls back to ~/.vault-token", async () => {
|
|
|
97
97
|
})
|
|
98
98
|
).resolves.toBe("vault-token");
|
|
99
99
|
});
|
|
100
|
+
|
|
101
|
+
test("upsertVaultSecretFields writes merged KV v2 data", async () => {
|
|
102
|
+
process.env.VAULT_ADDR = "https://vault.example.com";
|
|
103
|
+
process.env.VAULT_TOKEN = "token-123";
|
|
104
|
+
|
|
105
|
+
const requests: Array<{ method: string; url: string; body?: unknown }> = [];
|
|
106
|
+
const fetchMock = mock(async (input: string | URL | Request, init?: RequestInit) => {
|
|
107
|
+
const url = String(input);
|
|
108
|
+
requests.push({
|
|
109
|
+
method: init?.method ?? "GET",
|
|
110
|
+
url,
|
|
111
|
+
body: init?.body ? JSON.parse(String(init.body)) : undefined,
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
if ((init?.method ?? "GET") === "GET") {
|
|
115
|
+
return new Response(
|
|
116
|
+
JSON.stringify({
|
|
117
|
+
data: {
|
|
118
|
+
data: {
|
|
119
|
+
existing_field: "keep-me",
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
}),
|
|
123
|
+
{ status: 200 }
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return new Response(JSON.stringify({}), { status: 200 });
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
globalThis.fetch = fetchMock as unknown as typeof fetch;
|
|
131
|
+
|
|
132
|
+
await upsertVaultSecretFields({
|
|
133
|
+
path: "prod/providers/clerk",
|
|
134
|
+
fields: {
|
|
135
|
+
publishable_key: "pk_live_example",
|
|
136
|
+
secret_key: "sk_live_example",
|
|
137
|
+
webhook_secret: "whsec_example",
|
|
138
|
+
},
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
expect(requests).toEqual([
|
|
142
|
+
{
|
|
143
|
+
method: "GET",
|
|
144
|
+
url: "https://vault.example.com/v1/secret/data/prod/providers/clerk",
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
method: "POST",
|
|
148
|
+
url: "https://vault.example.com/v1/secret/data/prod/providers/clerk",
|
|
149
|
+
body: {
|
|
150
|
+
data: {
|
|
151
|
+
existing_field: "keep-me",
|
|
152
|
+
publishable_key: "pk_live_example",
|
|
153
|
+
secret_key: "sk_live_example",
|
|
154
|
+
webhook_secret: "whsec_example",
|
|
155
|
+
},
|
|
156
|
+
},
|
|
157
|
+
},
|
|
158
|
+
]);
|
|
159
|
+
});
|
package/src/vault.ts
CHANGED
|
@@ -13,6 +13,14 @@ type VaultSecretOptions = {
|
|
|
13
13
|
field?: string;
|
|
14
14
|
};
|
|
15
15
|
|
|
16
|
+
type VaultWriteOptions = {
|
|
17
|
+
addr?: string;
|
|
18
|
+
token?: string;
|
|
19
|
+
mount?: string;
|
|
20
|
+
path: string;
|
|
21
|
+
fields: Record<string, string>;
|
|
22
|
+
};
|
|
23
|
+
|
|
16
24
|
export async function resolveNeonApiKey() {
|
|
17
25
|
const direct = process.env.NEON_API_KEY?.trim();
|
|
18
26
|
if (direct) {
|
|
@@ -26,24 +34,59 @@ export async function resolveNeonApiKey() {
|
|
|
26
34
|
}
|
|
27
35
|
|
|
28
36
|
export async function readVaultSecret(options: VaultSecretOptions = {}) {
|
|
29
|
-
const
|
|
30
|
-
const
|
|
37
|
+
const field = options.field?.trim() ?? "value";
|
|
38
|
+
const payload = await readVaultSecretData(options);
|
|
31
39
|
const mount = options.mount ?? process.env.VAULT_SECRET_MOUNT?.trim() ?? DEFAULT_VAULT_SECRET_MOUNT;
|
|
32
40
|
const path = options.path?.trim() ?? "";
|
|
33
|
-
const field = options.field?.trim() ?? "value";
|
|
34
|
-
|
|
35
|
-
if (!addr || !token || !path) {
|
|
36
|
-
throw new Error("Vault secret resolution requires VAULT_ADDR, a Vault token, and a secret path");
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
const normalizedAddr = addr.replace(/\/+$/g, "");
|
|
40
41
|
const normalizedMount = mount.replace(/^\/+|\/+$/g, "");
|
|
41
42
|
const normalizedPath = path.replace(/^\/+/g, "");
|
|
42
|
-
const
|
|
43
|
+
const value = payload[field]?.trim();
|
|
44
|
+
if (!value) {
|
|
45
|
+
throw new Error(`Vault secret field ${field} is empty at ${normalizedMount}/${normalizedPath}`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return value;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export async function readVaultSecretFields(options: VaultSecretOptions = {}) {
|
|
52
|
+
return readVaultSecretData(options);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export async function upsertVaultSecretFields(options: VaultWriteOptions) {
|
|
56
|
+
const connection = await resolveVaultConnection(options);
|
|
57
|
+
const url = vaultKv2Url(connection);
|
|
58
|
+
|
|
59
|
+
const existing = await readVaultSecretData({ ...options, path: connection.normalizedPath }).catch((error) => {
|
|
60
|
+
if (error instanceof Error && error.message.startsWith("Vault read failed: 404")) {
|
|
61
|
+
return {};
|
|
62
|
+
}
|
|
63
|
+
throw error;
|
|
64
|
+
});
|
|
43
65
|
|
|
44
66
|
const response = await fetch(url, {
|
|
67
|
+
method: "POST",
|
|
45
68
|
headers: {
|
|
46
|
-
"
|
|
69
|
+
"Content-Type": "application/json",
|
|
70
|
+
"X-Vault-Token": connection.token,
|
|
71
|
+
},
|
|
72
|
+
body: JSON.stringify({
|
|
73
|
+
data: {
|
|
74
|
+
...existing,
|
|
75
|
+
...trimFields(options.fields),
|
|
76
|
+
},
|
|
77
|
+
}),
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
if (!response.ok) {
|
|
81
|
+
throw new Error(`Vault write failed: ${response.status} ${response.statusText}`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async function readVaultSecretData(options: VaultSecretOptions = {}) {
|
|
86
|
+
const connection = await resolveVaultConnection(options);
|
|
87
|
+
const response = await fetch(vaultKv2Url(connection), {
|
|
88
|
+
headers: {
|
|
89
|
+
"X-Vault-Token": connection.token,
|
|
47
90
|
},
|
|
48
91
|
});
|
|
49
92
|
|
|
@@ -57,12 +100,31 @@ export async function readVaultSecret(options: VaultSecretOptions = {}) {
|
|
|
57
100
|
};
|
|
58
101
|
};
|
|
59
102
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
103
|
+
return payload.data?.data ?? {};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async function resolveVaultConnection(options: Omit<VaultWriteOptions, "fields"> | VaultSecretOptions) {
|
|
107
|
+
const addr = options.addr ?? process.env.VAULT_ADDR?.trim() ?? "";
|
|
108
|
+
const token = options.token ?? (await resolveVaultToken());
|
|
109
|
+
const mount = options.mount ?? process.env.VAULT_SECRET_MOUNT?.trim() ?? DEFAULT_VAULT_SECRET_MOUNT;
|
|
110
|
+
const path = options.path?.trim() ?? "";
|
|
111
|
+
|
|
112
|
+
if (!addr || !token || !path) {
|
|
113
|
+
throw new Error("Vault secret resolution requires VAULT_ADDR, a Vault token, and a secret path");
|
|
63
114
|
}
|
|
64
115
|
|
|
65
|
-
|
|
116
|
+
const normalizedAddr = addr.replace(/\/+$/g, "");
|
|
117
|
+
const normalizedMount = mount.replace(/^\/+|\/+$/g, "");
|
|
118
|
+
const normalizedPath = path.replace(/^\/+/g, "");
|
|
119
|
+
return { normalizedAddr, normalizedMount, normalizedPath, token };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function vaultKv2Url(connection: Awaited<ReturnType<typeof resolveVaultConnection>>) {
|
|
123
|
+
return `${connection.normalizedAddr}/v1/${connection.normalizedMount}/data/${connection.normalizedPath}`;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function trimFields(fields: Record<string, string>) {
|
|
127
|
+
return Object.fromEntries(Object.entries(fields).map(([key, value]) => [key, value.trim()]));
|
|
66
128
|
}
|
|
67
129
|
|
|
68
130
|
async function resolveVaultToken() {
|
|
@@ -1,23 +1,24 @@
|
|
|
1
1
|
# {{SERVICE_NAME}}
|
|
2
2
|
|
|
3
|
-
Generated by `create
|
|
3
|
+
Generated by `service create`.
|
|
4
4
|
|
|
5
5
|
This `{{PROFILE}}` profile targets `{{RUNTIME}} + {{FRAMEWORK}}` on Cloud Run with:
|
|
6
6
|
|
|
7
7
|
- one generated `service.yaml` manifest
|
|
8
8
|
- a lightweight `{{EXAMPLE_LABEL}}` example surface
|
|
9
9
|
- local Docker Compose Postgres for first-run development
|
|
10
|
-
-
|
|
11
|
-
- GCP project
|
|
12
|
-
- Neon-backed remote database provisioning during
|
|
13
|
-
-
|
|
14
|
-
-
|
|
10
|
+
- the `service` CLI for create, deploy, doctor, dashboards, and destroy
|
|
11
|
+
- GCP project create with billing and quota-project-aware `gcloud` calls
|
|
12
|
+
- Neon-backed remote database provisioning during create and deploy
|
|
13
|
+
- Better Auth client-credentials resource-server registration through `authctl`
|
|
14
|
+
- stage-aware waitlist data and trigger ingestion
|
|
15
|
+
- typed HTTP webhook ingestion where the selected template supports it
|
|
15
16
|
- a production API origin at `https://{{API_HOSTNAME}}`
|
|
16
17
|
|
|
17
18
|
The default happy path is standalone. Terraform is optional: advanced users can
|
|
18
19
|
precreate shared foundations and point this package at them, but a generated app
|
|
19
20
|
does not need Terraform state, Terraform plans, a control plane, or a platform
|
|
20
|
-
console to
|
|
21
|
+
console to create and deploy.
|
|
21
22
|
|
|
22
23
|
## Commands
|
|
23
24
|
|
|
@@ -29,6 +30,8 @@ console to bootstrap and deploy.
|
|
|
29
30
|
{{COMMAND_TEST}}
|
|
30
31
|
{{COMMAND_BOOTSTRAP}}
|
|
31
32
|
{{COMMAND_DEPLOY}}
|
|
33
|
+
{{COMMAND_AUTH_RESOURCE}}
|
|
34
|
+
{{COMMAND_AUTH_CLIENT}}
|
|
32
35
|
{{COMMAND_DEPLOY_PERSONAL}}
|
|
33
36
|
{{COMMAND_DEPLOY_DESTROY}}
|
|
34
37
|
{{COMMAND_CLEANUP}}
|
|
@@ -42,7 +45,6 @@ The scaffold writes a ready-to-use `.env.local` and includes a local Postgres se
|
|
|
42
45
|
First local run:
|
|
43
46
|
|
|
44
47
|
```bash
|
|
45
|
-
docker compose up -d
|
|
46
48
|
{{COMMAND_MIGRATE}}
|
|
47
49
|
{{COMMAND_DEV}}
|
|
48
50
|
```
|
|
@@ -50,21 +52,21 @@ docker compose up -d
|
|
|
50
52
|
Local runtime uses:
|
|
51
53
|
|
|
52
54
|
- `DATABASE_URL` from `.env.local`, pointed at Docker Compose Postgres
|
|
53
|
-
- `
|
|
54
|
-
- `
|
|
55
|
+
- `{{COMMAND_MIGRATE}}` and `{{COMMAND_DEV}}`, which open Docker Desktop if needed, wait for Docker readiness, and start Docker Compose Postgres
|
|
56
|
+
- `TEMPORAL_ENABLED=false` by default; set Temporal env vars locally only when you want to run against a real Temporal server
|
|
55
57
|
|
|
56
|
-
|
|
58
|
+
No cloud credentials are required for local HTTP development after Docker and Postgres are running.
|
|
57
59
|
|
|
58
60
|
## Remote provisioning
|
|
59
61
|
|
|
60
62
|
The generated Cloud Run config lives in [scripts/cloudrun/config.ts](scripts/cloudrun/config.ts).
|
|
61
63
|
|
|
62
|
-
|
|
64
|
+
Create, deploy, and destroy use:
|
|
63
65
|
|
|
64
66
|
- known-good CLIs first, especially `gcloud`
|
|
65
67
|
- `gcloud`
|
|
66
68
|
- `NEON_API_KEY`, or a working Vault login via `VAULT_ADDR` plus `VAULT_TOKEN`, `VAULT_TOKEN_FILE`, or `~/.vault-token`
|
|
67
|
-
- the
|
|
69
|
+
- the repo-aware `service` CLI from this package
|
|
68
70
|
|
|
69
71
|
Local provisioning intentionally prefers known-good CLIs over SDKs for Google Cloud operations.
|
|
70
72
|
|
|
@@ -82,6 +84,12 @@ gcloud auth application-default login
|
|
|
82
84
|
|
|
83
85
|
The generated backend scripts still use `gcloud` as the primary control plane even when ADC is present.
|
|
84
86
|
|
|
87
|
+
Go variants use Atlas for migrations:
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
atlas version
|
|
91
|
+
```
|
|
92
|
+
|
|
85
93
|
For the Neon admin credential, prefer a normal Vault login flow:
|
|
86
94
|
|
|
87
95
|
```bash
|
|
@@ -96,36 +104,110 @@ The scaffold will use, in order:
|
|
|
96
104
|
|
|
97
105
|
That keeps stable settings in the repo and keeps the token out of `~/.zshrc`.
|
|
98
106
|
|
|
107
|
+
For production auth registration, `authctl` also needs the auth service's
|
|
108
|
+
Cloudflare Access service token:
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
export AUTH_INTERNAL_BASE_URL="$(vault kv get -mount=secret -field=AUTH_INTERNAL_BASE_URL prod/apps/auth/authctl/cloudflare-access)"
|
|
112
|
+
export CLOUDFLARE_ACCESS_SERVICE_TOKEN_CLIENT_ID="$(vault kv get -mount=secret -field=CLOUDFLARE_ACCESS_SERVICE_TOKEN_CLIENT_ID prod/apps/auth/authctl/cloudflare-access)"
|
|
113
|
+
export CLOUDFLARE_ACCESS_SERVICE_TOKEN_CLIENT_SECRET="$(vault kv get -mount=secret -field=CLOUDFLARE_ACCESS_SERVICE_TOKEN_CLIENT_SECRET prod/apps/auth/authctl/cloudflare-access)"
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Before first production create, verify the installed `authctl` exposes the
|
|
117
|
+
resource-server control-plane command:
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
{{COMMAND_AUTH_RESOURCE}} --help
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
If this fails with `authctl is installed but does not expose resource-server
|
|
124
|
+
commands`, make sure the generated package installed `@anmho/authctl@0.1.1` or
|
|
125
|
+
newer before running `{{COMMAND_BOOTSTRAP}}`.
|
|
126
|
+
|
|
99
127
|
Optional remote-only Vault overrides for Neon admin key lookup:
|
|
100
128
|
|
|
101
129
|
- `VAULT_SECRET_MOUNT` default `secret`
|
|
102
130
|
- `VAULT_NEON_API_KEY_PATH` default `prod/providers/neon`
|
|
103
131
|
- `VAULT_NEON_API_KEY_FIELD` default `api_key`
|
|
104
132
|
|
|
105
|
-
The generator only stores application-facing connection material in Secret Manager. Neon admin credentials stay local to
|
|
133
|
+
The generator only stores application-facing connection material in Secret Manager. Neon admin credentials stay local to create and deploy.
|
|
134
|
+
|
|
135
|
+
## Temporal
|
|
136
|
+
|
|
137
|
+
Cloud Run variants include an in-process Temporal worker in the service process.
|
|
138
|
+
Production Temporal is enabled when you set `TEMPORAL_ENABLED=true`, or when
|
|
139
|
+
`TEMPORAL_ADDRESS`, `TEMPORAL_API_KEY`, or `TEMPORAL_API_KEY_SECRET` is present
|
|
140
|
+
during deploy rendering.
|
|
141
|
+
|
|
142
|
+
For Temporal Cloud, provide:
|
|
106
143
|
|
|
107
|
-
|
|
144
|
+
```bash
|
|
145
|
+
TEMPORAL_ENABLED=true
|
|
146
|
+
TEMPORAL_ADDRESS=<namespace>.<account>.tmprl.cloud:7233
|
|
147
|
+
TEMPORAL_NAMESPACE=<namespace>.<account>
|
|
148
|
+
TEMPORAL_API_KEY=<one-time local value for service create>
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
`service create` writes `TEMPORAL_API_KEY` to Secret Manager as
|
|
152
|
+
`{{SERVICE_ID}}-temporal-api-key` and grants the runtime service account access.
|
|
153
|
+
Later deploys can set `TEMPORAL_API_KEY_SECRET={{SERVICE_ID}}-temporal-api-key`
|
|
154
|
+
without exposing the key locally.
|
|
155
|
+
|
|
156
|
+
## Service auth
|
|
157
|
+
|
|
158
|
+
Generated services are resource servers for the Better Auth client-credentials
|
|
159
|
+
server at `https://auth.anmho.com`.
|
|
160
|
+
|
|
161
|
+
`service create` registers this service as an auth resource server through
|
|
162
|
+
`authctl` before the first production deploy. The generated resource server is:
|
|
163
|
+
|
|
164
|
+
- id: `{{SERVICE_ID}}`
|
|
165
|
+
- audience: `api://{{SERVICE_ID}}`
|
|
166
|
+
- default scopes: `{{SERVICE_ID}}:read`, `{{SERVICE_ID}}:write`
|
|
167
|
+
|
|
168
|
+
Production runtime has `AUTH_ENABLED=true` and verifies JWT bearer tokens from
|
|
169
|
+
the Better Auth JWKS endpoint on `/v1/*` and ConnectRPC service paths. Local
|
|
170
|
+
development defaults to `AUTH_ENABLED=false` in `.env.local`.
|
|
108
171
|
|
|
109
|
-
|
|
110
|
-
- `ATTACHMENT_PUBLIC_BASE_URL`
|
|
172
|
+
Use `service auth` for follow-up auth operations:
|
|
111
173
|
|
|
112
|
-
|
|
174
|
+
```bash
|
|
175
|
+
{{COMMAND_BOOTSTRAP}} # includes resource-server registration
|
|
176
|
+
{{COMMAND_AUTH_RESOURCE}}
|
|
177
|
+
{{COMMAND_AUTH_CLIENT}} --resource-server <target-service> --scope <scope>
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
`authctl clients create` prints a one-time client secret plus the recommended
|
|
181
|
+
Vault command. By convention, generated services store outgoing service-client
|
|
182
|
+
credentials under:
|
|
183
|
+
|
|
184
|
+
```text
|
|
185
|
+
prod/apps/{{SERVICE_ID}}/server/oauth-clients/<resource_server_id>
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
When requesting a client-credentials token for a generated service, include the
|
|
189
|
+
target resource server as `resource=api://<resource_server_id>`. The generated
|
|
190
|
+
runtime expects a JWT with that audience; omitting `resource` can return an
|
|
191
|
+
opaque access token that the service will reject.
|
|
192
|
+
|
|
193
|
+
Webhook signature hooks are provider-specific and optional in v1. Add provider
|
|
194
|
+
secrets only when you add a provider adapter. A generic adapter can honor:
|
|
113
195
|
|
|
114
196
|
- `WEBHOOK_<PROVIDER>_SECRET`
|
|
115
197
|
|
|
116
|
-
## One-command production
|
|
198
|
+
## One-command production create
|
|
117
199
|
|
|
118
|
-
The one-command production
|
|
200
|
+
The one-command production create path is designed for a fresh standalone service.
|
|
119
201
|
|
|
120
|
-
|
|
202
|
+
The intended one-command flow is:
|
|
121
203
|
|
|
122
204
|
```bash
|
|
123
|
-
|
|
205
|
+
service create {{SERVICE_NAME}} --yes
|
|
124
206
|
```
|
|
125
207
|
|
|
126
|
-
That command scaffolds this package, runs
|
|
127
|
-
Cloud Run service
|
|
128
|
-
|
|
208
|
+
That command scaffolds this package, runs `service create`, deploys the production
|
|
209
|
+
Cloud Run service through `service deploy`, and fails loudly with resumable
|
|
210
|
+
instructions if a required cloud credential is missing. The generated package can also be run
|
|
129
211
|
manually:
|
|
130
212
|
|
|
131
213
|
```bash
|
|
@@ -133,9 +215,9 @@ manually:
|
|
|
133
215
|
{{COMMAND_DEPLOY}}
|
|
134
216
|
```
|
|
135
217
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
to Cloud Run through app-project Secret Manager.
|
|
218
|
+
Create reads Neon credentials from environment variables or from Vault when
|
|
219
|
+
`VAULT_ADDR` and a Vault token are available. Runtime database credentials are
|
|
220
|
+
delivered to Cloud Run through app-project Secret Manager.
|
|
139
221
|
|
|
140
222
|
## Production API
|
|
141
223
|
|
|
@@ -153,28 +235,23 @@ The generated microservice profile is a waitlist/launch service example. It is
|
|
|
153
235
|
kept deliberately small so the integration plumbing is easy to remove or adapt:
|
|
154
236
|
|
|
155
237
|
- public launch/waitlist submission
|
|
156
|
-
-
|
|
157
|
-
-
|
|
158
|
-
- provider webhook
|
|
159
|
-
- Pro entitlement checks for paid capabilities
|
|
160
|
-
|
|
161
|
-
The current backend plumbing includes:
|
|
238
|
+
- status lookup
|
|
239
|
+
- trigger ingestion for scheduled, webhook, and manual follow-up work
|
|
240
|
+
- provider webhook ingress where the selected template supports it
|
|
162
241
|
|
|
163
|
-
|
|
164
|
-
- `conversations`
|
|
165
|
-
- `conversation_participants`
|
|
166
|
-
- `messages`
|
|
167
|
-
- `attachments`
|
|
168
|
-
- `webhook_events`
|
|
242
|
+
The current Hono backend plumbing includes:
|
|
169
243
|
|
|
170
|
-
|
|
244
|
+
- `waitlist_entries`
|
|
245
|
+
- `waitlist_triggers`
|
|
171
246
|
|
|
172
|
-
|
|
247
|
+
Hono variants expose:
|
|
173
248
|
|
|
174
|
-
|
|
249
|
+
- `POST /v1/waitlist`
|
|
250
|
+
- `GET /v1/waitlist?email=...`
|
|
251
|
+
- `GET /v1/waitlist/{entryId}`
|
|
252
|
+
- `POST /v1/triggers/waitlist`
|
|
253
|
+
- `POST /webhooks/:provider`
|
|
175
254
|
|
|
176
|
-
|
|
177
|
-
-
|
|
178
|
-
- default page size `50`, max page size `100`
|
|
179
|
-
- message payloads include lightweight attachment metadata only
|
|
255
|
+
ConnectRPC variants expose typed unary waitlist RPCs and will usually be the
|
|
256
|
+
first place to adapt domain-specific contracts after scaffold.
|
|
180
257
|
{{LOCAL_INTROSPECTION_NOTE}}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
apiVersion: 1
|
|
2
|
+
groups:
|
|
3
|
+
- orgId: 1
|
|
4
|
+
name: "{{SERVICE_NAME}} baseline"
|
|
5
|
+
folder: "create-service"
|
|
6
|
+
interval: 1m
|
|
7
|
+
rules:
|
|
8
|
+
- uid: "{{SERVICE_ID}}-high-error-rate"
|
|
9
|
+
title: "{{SERVICE_NAME}} high error rate"
|
|
10
|
+
condition: C
|
|
11
|
+
data:
|
|
12
|
+
- refId: A
|
|
13
|
+
relativeTimeRange:
|
|
14
|
+
from: 600
|
|
15
|
+
to: 0
|
|
16
|
+
datasourceUid: "${datasource}"
|
|
17
|
+
model:
|
|
18
|
+
expr: "sum(rate(http_requests_total{service=\"{{SERVICE_NAME}}\",status=~\"5..\"}[5m]))"
|
|
19
|
+
intervalMs: 1000
|
|
20
|
+
maxDataPoints: 43200
|
|
21
|
+
refId: A
|
|
22
|
+
- refId: C
|
|
23
|
+
relativeTimeRange:
|
|
24
|
+
from: 600
|
|
25
|
+
to: 0
|
|
26
|
+
datasourceUid: __expr__
|
|
27
|
+
model:
|
|
28
|
+
conditions:
|
|
29
|
+
- evaluator:
|
|
30
|
+
params: [0.05]
|
|
31
|
+
type: gt
|
|
32
|
+
operator:
|
|
33
|
+
type: and
|
|
34
|
+
query:
|
|
35
|
+
params: [A]
|
|
36
|
+
reducer:
|
|
37
|
+
type: last
|
|
38
|
+
type: query
|
|
39
|
+
datasource:
|
|
40
|
+
type: __expr__
|
|
41
|
+
uid: __expr__
|
|
42
|
+
expression: A
|
|
43
|
+
intervalMs: 1000
|
|
44
|
+
maxDataPoints: 43200
|
|
45
|
+
refId: C
|
|
46
|
+
type: threshold
|
|
47
|
+
noDataState: NoData
|
|
48
|
+
execErrState: Error
|
|
49
|
+
for: 5m
|
|
50
|
+
annotations:
|
|
51
|
+
summary: "{{SERVICE_NAME}} 5xx rate is elevated"
|
|
52
|
+
labels:
|
|
53
|
+
service_id: "{{SERVICE_ID}}"
|
|
54
|
+
target: "{{TARGET}}"
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
{
|
|
2
|
+
"uid": "{{SERVICE_ID}}-waitlist",
|
|
3
|
+
"title": "{{SERVICE_NAME}} Waitlist Service",
|
|
4
|
+
"tags": ["create-service", "{{TARGET}}", "{{RUNTIME}}", "{{FRAMEWORK}}"],
|
|
5
|
+
"timezone": "browser",
|
|
6
|
+
"schemaVersion": 39,
|
|
7
|
+
"version": 1,
|
|
8
|
+
"refresh": "30s",
|
|
9
|
+
"panels": [
|
|
10
|
+
{
|
|
11
|
+
"id": 1,
|
|
12
|
+
"type": "timeseries",
|
|
13
|
+
"title": "Request rate",
|
|
14
|
+
"datasource": { "type": "prometheus", "uid": "${datasource}" },
|
|
15
|
+
"targets": [
|
|
16
|
+
{
|
|
17
|
+
"refId": "A",
|
|
18
|
+
"expr": "sum(rate(http_requests_total{service=\"{{SERVICE_NAME}}\"}[5m]))"
|
|
19
|
+
}
|
|
20
|
+
],
|
|
21
|
+
"gridPos": { "h": 8, "w": 12, "x": 0, "y": 0 }
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"id": 2,
|
|
25
|
+
"type": "timeseries",
|
|
26
|
+
"title": "Error rate",
|
|
27
|
+
"datasource": { "type": "prometheus", "uid": "${datasource}" },
|
|
28
|
+
"targets": [
|
|
29
|
+
{
|
|
30
|
+
"refId": "A",
|
|
31
|
+
"expr": "sum(rate(http_requests_total{service=\"{{SERVICE_NAME}}\",status=~\"5..\"}[5m]))"
|
|
32
|
+
}
|
|
33
|
+
],
|
|
34
|
+
"gridPos": { "h": 8, "w": 12, "x": 12, "y": 0 }
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
"id": 3,
|
|
38
|
+
"type": "timeseries",
|
|
39
|
+
"title": "p95 latency",
|
|
40
|
+
"datasource": { "type": "prometheus", "uid": "${datasource}" },
|
|
41
|
+
"targets": [
|
|
42
|
+
{
|
|
43
|
+
"refId": "A",
|
|
44
|
+
"expr": "histogram_quantile(0.95, sum(rate(http_request_duration_seconds_bucket{service=\"{{SERVICE_NAME}}\"}[5m])) by (le))"
|
|
45
|
+
}
|
|
46
|
+
],
|
|
47
|
+
"gridPos": { "h": 8, "w": 12, "x": 0, "y": 8 }
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
"id": 4,
|
|
51
|
+
"type": "stat",
|
|
52
|
+
"title": "Queued triggers",
|
|
53
|
+
"datasource": { "type": "prometheus", "uid": "${datasource}" },
|
|
54
|
+
"targets": [
|
|
55
|
+
{
|
|
56
|
+
"refId": "A",
|
|
57
|
+
"expr": "sum(waitlist_triggers_queued{service=\"{{SERVICE_NAME}}\"})"
|
|
58
|
+
}
|
|
59
|
+
],
|
|
60
|
+
"gridPos": { "h": 8, "w": 12, "x": 12, "y": 8 }
|
|
61
|
+
}
|
|
62
|
+
]
|
|
63
|
+
}
|