@cosmicdrift/kumiko-bundled-features 0.7.0 → 0.8.0
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/CHANGELOG.md +30 -0
- package/package.json +5 -5
- package/src/__tests__/env-schemas.test.ts +210 -0
- package/src/auth-email-password/feature.ts +27 -0
- package/src/auth-email-password/index.ts +1 -1
- package/src/secrets/feature.ts +38 -0
- package/src/secrets/index.ts +1 -0
- package/src/subscription-mollie/feature.ts +16 -0
- package/src/subscription-mollie/index.ts +1 -0
- package/src/subscription-stripe/feature.ts +25 -0
- package/src/subscription-stripe/index.ts +5 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,35 @@
|
|
|
1
1
|
# @cosmicdrift/kumiko-bundled-features
|
|
2
2
|
|
|
3
|
+
## 0.8.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 145b8df: Add env-var contracts for four bundled-features (Sprint 9.3, Migration Phase 2).
|
|
8
|
+
|
|
9
|
+
**New API:**
|
|
10
|
+
|
|
11
|
+
- `secretsEnvSchema` — `KUMIKO_SECRETS_MASTER_KEY_V1` (base64-32 KEK, refined for length) + `KUMIKO_SECRETS_MASTER_KEY_CURRENT_VERSION` (default `"1"`).
|
|
12
|
+
- `authEmailPasswordEnvSchema` — `JWT_SECRET` (≥32 chars) + `JWT_ISSUER` (optional).
|
|
13
|
+
- `subscriptionStripeEnvSchema` — `STRIPE_WEBHOOK_SECRET` + `STRIPE_API_KEY` (both non-empty, both `pulumi.secret=true`).
|
|
14
|
+
- `subscriptionMollieEnvSchema` — `MOLLIE_API_KEY` (`test_` or `live_` prefix, `pulumi.secret=true`).
|
|
15
|
+
|
|
16
|
+
Each schema is exported from its feature's barrel and attached via `r.envSchema(...)` at feature-mount-time. Apps that mount these features via `composeEnvSchema({ features, ... })` get aggregated boot-validation for the relevant env-vars with source-attribution (`(auth-email-password)`, `(secrets)`, `(subscription-stripe)`, `(subscription-mollie)`).
|
|
17
|
+
|
|
18
|
+
**Plan-Doc-Drift dokumentiert:** `mail-transport-smtp` bekommt KEIN envSchema. SMTP_HOST/PORT/SECURE/FROM/AUTH-USER sind tenant-config, SMTP_PASSWORD ist tenant-secret via `r.secret()` — keine process.env-Vars im Feature. Apps die SMTP_HOST etc. aus env seeden, deklarieren das in ihrem `extend`-block.
|
|
19
|
+
|
|
20
|
+
**Kumiko-Pattern:** Das schema ist Contract, nicht Doku. Wenn eine App die var anders nennt (z.B. `MY_JWT` statt `JWT_SECRET`), ist sie off-pattern — `composeEnvSchema` würde sie unter dem standardisierten Namen erwarten.
|
|
21
|
+
|
|
22
|
+
**Backward-compat:** Purely additive. Apps ohne `composeEnvSchema({features})` behavior unverändert.
|
|
23
|
+
|
|
24
|
+
### Patch Changes
|
|
25
|
+
|
|
26
|
+
- Updated dependencies [f34af9a]
|
|
27
|
+
- Updated dependencies [dff4123]
|
|
28
|
+
- @cosmicdrift/kumiko-framework@0.8.0
|
|
29
|
+
- @cosmicdrift/kumiko-renderer@0.8.0
|
|
30
|
+
- @cosmicdrift/kumiko-dispatcher-live@0.8.0
|
|
31
|
+
- @cosmicdrift/kumiko-renderer-web@0.8.0
|
|
32
|
+
|
|
3
33
|
## 0.7.0
|
|
4
34
|
|
|
5
35
|
### Minor Changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-bundled-features",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "Built-in features — tenant, user, auth, delivery. The stuff you'd rewrite anyway, already typed.",
|
|
5
5
|
"license": "BUSL-1.1",
|
|
6
6
|
"author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
|
|
@@ -74,10 +74,10 @@
|
|
|
74
74
|
"@aws-sdk/client-s3": "^3.1045.0",
|
|
75
75
|
"@aws-sdk/lib-storage": "^3.1045.0",
|
|
76
76
|
"@aws-sdk/s3-request-presigner": "^3.1045.0",
|
|
77
|
-
"@cosmicdrift/kumiko-dispatcher-live": "0.
|
|
78
|
-
"@cosmicdrift/kumiko-framework": "0.
|
|
79
|
-
"@cosmicdrift/kumiko-renderer": "0.
|
|
80
|
-
"@cosmicdrift/kumiko-renderer-web": "0.
|
|
77
|
+
"@cosmicdrift/kumiko-dispatcher-live": "0.8.0",
|
|
78
|
+
"@cosmicdrift/kumiko-framework": "0.8.0",
|
|
79
|
+
"@cosmicdrift/kumiko-renderer": "0.8.0",
|
|
80
|
+
"@cosmicdrift/kumiko-renderer-web": "0.8.0",
|
|
81
81
|
"@mollie/api-client": "^4.5.0",
|
|
82
82
|
"@node-rs/argon2": "^2.0.2",
|
|
83
83
|
"@types/nodemailer": "^8.0.0",
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
2
|
+
import {
|
|
3
|
+
composeEnvSchema,
|
|
4
|
+
type KumikoBootError,
|
|
5
|
+
parseEnv,
|
|
6
|
+
} from "@cosmicdrift/kumiko-framework/env";
|
|
7
|
+
import { describe, expect, it } from "vitest";
|
|
8
|
+
import { authEmailPasswordEnvSchema, createAuthEmailPasswordFeature } from "../auth-email-password";
|
|
9
|
+
import { createSecretsFeature, secretsEnvSchema } from "../secrets";
|
|
10
|
+
import {
|
|
11
|
+
createSubscriptionMollieFeature,
|
|
12
|
+
subscriptionMollieEnvSchema,
|
|
13
|
+
} from "../subscription-mollie";
|
|
14
|
+
import {
|
|
15
|
+
createSubscriptionStripeFeature,
|
|
16
|
+
subscriptionStripeEnvSchema,
|
|
17
|
+
} from "../subscription-stripe";
|
|
18
|
+
|
|
19
|
+
const validKek = randomBytes(32).toString("base64");
|
|
20
|
+
|
|
21
|
+
describe("secretsEnvSchema", () => {
|
|
22
|
+
it("accepts a base64-32 KEK and defaults CURRENT_VERSION to '1'", () => {
|
|
23
|
+
const env = parseEnv(secretsEnvSchema, {
|
|
24
|
+
KUMIKO_SECRETS_MASTER_KEY_V1: validKek,
|
|
25
|
+
});
|
|
26
|
+
expect(env.KUMIKO_SECRETS_MASTER_KEY_V1).toBe(validKek);
|
|
27
|
+
expect(env.KUMIKO_SECRETS_MASTER_KEY_CURRENT_VERSION).toBe("1");
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("rejects a base64 value that decodes to !=32 bytes", () => {
|
|
31
|
+
try {
|
|
32
|
+
parseEnv(secretsEnvSchema, { KUMIKO_SECRETS_MASTER_KEY_V1: "dGVzdA==" });
|
|
33
|
+
throw new Error("should have thrown");
|
|
34
|
+
} catch (err) {
|
|
35
|
+
const boot = err as KumikoBootError;
|
|
36
|
+
const v1 = boot.errors.find((e) => e.name === "KUMIKO_SECRETS_MASTER_KEY_V1");
|
|
37
|
+
expect(v1?.kind).toBe("invalid");
|
|
38
|
+
expect(v1?.message).toContain("32 bytes");
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("rejects a non-numeric CURRENT_VERSION", () => {
|
|
43
|
+
try {
|
|
44
|
+
parseEnv(secretsEnvSchema, {
|
|
45
|
+
KUMIKO_SECRETS_MASTER_KEY_V1: validKek,
|
|
46
|
+
KUMIKO_SECRETS_MASTER_KEY_CURRENT_VERSION: "two",
|
|
47
|
+
});
|
|
48
|
+
throw new Error("should have thrown");
|
|
49
|
+
} catch (err) {
|
|
50
|
+
const cur = (err as KumikoBootError).errors.find(
|
|
51
|
+
(e) => e.name === "KUMIKO_SECRETS_MASTER_KEY_CURRENT_VERSION",
|
|
52
|
+
);
|
|
53
|
+
expect(cur?.kind).toBe("invalid");
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("attaches the schema via r.envSchema() on createSecretsFeature()", () => {
|
|
58
|
+
const f = createSecretsFeature();
|
|
59
|
+
expect(f.envSchema).toBe(secretsEnvSchema);
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
describe("authEmailPasswordEnvSchema", () => {
|
|
64
|
+
it("accepts JWT_SECRET ≥32 chars; JWT_ISSUER stays optional", () => {
|
|
65
|
+
const env = parseEnv(authEmailPasswordEnvSchema, {
|
|
66
|
+
JWT_SECRET: "x".repeat(32),
|
|
67
|
+
});
|
|
68
|
+
expect(env.JWT_SECRET.length).toBe(32);
|
|
69
|
+
expect(env.JWT_ISSUER).toBeUndefined();
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("rejects a short JWT_SECRET", () => {
|
|
73
|
+
try {
|
|
74
|
+
parseEnv(authEmailPasswordEnvSchema, { JWT_SECRET: "short" });
|
|
75
|
+
throw new Error("should have thrown");
|
|
76
|
+
} catch (err) {
|
|
77
|
+
const jwt = (err as KumikoBootError).errors.find((e) => e.name === "JWT_SECRET");
|
|
78
|
+
expect(jwt?.kind).toBe("invalid");
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("attaches the schema via r.envSchema() on createAuthEmailPasswordFeature()", () => {
|
|
83
|
+
const f = createAuthEmailPasswordFeature();
|
|
84
|
+
expect(f.envSchema).toBe(authEmailPasswordEnvSchema);
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
describe("subscriptionStripeEnvSchema", () => {
|
|
89
|
+
it("accepts non-empty webhookSecret + apiKey", () => {
|
|
90
|
+
const env = parseEnv(subscriptionStripeEnvSchema, {
|
|
91
|
+
STRIPE_WEBHOOK_SECRET: "whsec_abc",
|
|
92
|
+
STRIPE_API_KEY: "sk_test_xyz",
|
|
93
|
+
});
|
|
94
|
+
expect(env.STRIPE_WEBHOOK_SECRET).toBe("whsec_abc");
|
|
95
|
+
expect(env.STRIPE_API_KEY).toBe("sk_test_xyz");
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("rejects empty values", () => {
|
|
99
|
+
try {
|
|
100
|
+
parseEnv(subscriptionStripeEnvSchema, {
|
|
101
|
+
STRIPE_WEBHOOK_SECRET: "",
|
|
102
|
+
STRIPE_API_KEY: "",
|
|
103
|
+
});
|
|
104
|
+
throw new Error("should have thrown");
|
|
105
|
+
} catch (err) {
|
|
106
|
+
const boot = err as KumikoBootError;
|
|
107
|
+
expect(boot.errors.length).toBe(2);
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("attaches the schema via r.envSchema() on the factory", () => {
|
|
112
|
+
const f = createSubscriptionStripeFeature({
|
|
113
|
+
webhookSecret: "whsec_x",
|
|
114
|
+
apiKey: "sk_test_y",
|
|
115
|
+
priceToTier: {},
|
|
116
|
+
});
|
|
117
|
+
expect(f.envSchema).toBe(subscriptionStripeEnvSchema);
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
describe("subscriptionMollieEnvSchema", () => {
|
|
122
|
+
it("accepts test_ and live_ prefixes", () => {
|
|
123
|
+
expect(parseEnv(subscriptionMollieEnvSchema, { MOLLIE_API_KEY: "test_abc" })).toBeDefined();
|
|
124
|
+
expect(parseEnv(subscriptionMollieEnvSchema, { MOLLIE_API_KEY: "live_xyz" })).toBeDefined();
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("rejects an unprefixed key", () => {
|
|
128
|
+
try {
|
|
129
|
+
parseEnv(subscriptionMollieEnvSchema, { MOLLIE_API_KEY: "no-prefix" });
|
|
130
|
+
throw new Error("should have thrown");
|
|
131
|
+
} catch (err) {
|
|
132
|
+
const k = (err as KumikoBootError).errors.find((e) => e.name === "MOLLIE_API_KEY");
|
|
133
|
+
expect(k?.kind).toBe("invalid");
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it("attaches the schema via r.envSchema() on the factory", () => {
|
|
138
|
+
const f = createSubscriptionMollieFeature({
|
|
139
|
+
apiKey: "test_x",
|
|
140
|
+
webhookUrl: "https://example.com/webhook",
|
|
141
|
+
priceToTier: {},
|
|
142
|
+
priceToConfig: {},
|
|
143
|
+
});
|
|
144
|
+
expect(f.envSchema).toBe(subscriptionMollieEnvSchema);
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
describe("compose across all Phase-2 features", () => {
|
|
149
|
+
it("merges all four schemas with correct source attribution", () => {
|
|
150
|
+
const features = [
|
|
151
|
+
createSecretsFeature(),
|
|
152
|
+
createAuthEmailPasswordFeature(),
|
|
153
|
+
createSubscriptionStripeFeature({
|
|
154
|
+
webhookSecret: "whsec_x",
|
|
155
|
+
apiKey: "sk_test_y",
|
|
156
|
+
priceToTier: {},
|
|
157
|
+
}),
|
|
158
|
+
createSubscriptionMollieFeature({
|
|
159
|
+
apiKey: "test_x",
|
|
160
|
+
webhookUrl: "https://example.com",
|
|
161
|
+
priceToTier: {},
|
|
162
|
+
priceToConfig: {},
|
|
163
|
+
}),
|
|
164
|
+
];
|
|
165
|
+
const { schema, sources } = composeEnvSchema({ features });
|
|
166
|
+
const keys = Object.keys(schema.shape).sort();
|
|
167
|
+
expect(keys).toEqual([
|
|
168
|
+
"JWT_ISSUER",
|
|
169
|
+
"JWT_SECRET",
|
|
170
|
+
"KUMIKO_SECRETS_MASTER_KEY_CURRENT_VERSION",
|
|
171
|
+
"KUMIKO_SECRETS_MASTER_KEY_V1",
|
|
172
|
+
"MOLLIE_API_KEY",
|
|
173
|
+
"STRIPE_API_KEY",
|
|
174
|
+
"STRIPE_WEBHOOK_SECRET",
|
|
175
|
+
]);
|
|
176
|
+
expect(sources["JWT_SECRET"]).toBe("auth-email-password");
|
|
177
|
+
expect(sources["KUMIKO_SECRETS_MASTER_KEY_V1"]).toBe("secrets");
|
|
178
|
+
expect(sources["STRIPE_API_KEY"]).toBe("subscription-stripe");
|
|
179
|
+
expect(sources["MOLLIE_API_KEY"]).toBe("subscription-mollie");
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it("KumikoBootError.format() shows feature-source for missing feature-env-vars", () => {
|
|
183
|
+
const features = [
|
|
184
|
+
createSecretsFeature(),
|
|
185
|
+
createAuthEmailPasswordFeature(),
|
|
186
|
+
createSubscriptionStripeFeature({
|
|
187
|
+
webhookSecret: "whsec_x",
|
|
188
|
+
apiKey: "sk_test_y",
|
|
189
|
+
priceToTier: {},
|
|
190
|
+
}),
|
|
191
|
+
createSubscriptionMollieFeature({
|
|
192
|
+
apiKey: "test_x",
|
|
193
|
+
webhookUrl: "https://example.com",
|
|
194
|
+
priceToTier: {},
|
|
195
|
+
priceToConfig: {},
|
|
196
|
+
}),
|
|
197
|
+
];
|
|
198
|
+
const composed = composeEnvSchema({ features });
|
|
199
|
+
try {
|
|
200
|
+
parseEnv(composed.schema, {}, { sources: composed.sources });
|
|
201
|
+
throw new Error("should have thrown");
|
|
202
|
+
} catch (err) {
|
|
203
|
+
const out = (err as KumikoBootError).format();
|
|
204
|
+
expect(out).toContain("✗ JWT_SECRET (auth-email-password, required, missing)");
|
|
205
|
+
expect(out).toContain("✗ KUMIKO_SECRETS_MASTER_KEY_V1 (secrets, required, missing)");
|
|
206
|
+
expect(out).toContain("✗ STRIPE_API_KEY (subscription-stripe, required, missing)");
|
|
207
|
+
expect(out).toContain("✗ MOLLIE_API_KEY (subscription-mollie, required, missing)");
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
});
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { defineFeature, type FeatureDefinition } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
|
+
import { z } from "zod";
|
|
2
3
|
import { changePasswordWrite } from "./handlers/change-password.write";
|
|
3
4
|
import { createInviteAcceptHandler } from "./handlers/invite-accept.write";
|
|
4
5
|
import { createInviteAcceptWithLoginHandler } from "./handlers/invite-accept-with-login.write";
|
|
@@ -19,6 +20,31 @@ import {
|
|
|
19
20
|
} from "./handlers/signup-request.write";
|
|
20
21
|
import { createVerifyEmailHandler } from "./handlers/verify-email.write";
|
|
21
22
|
|
|
23
|
+
/**
|
|
24
|
+
* Env-vars contract for the `auth-email-password` feature.
|
|
25
|
+
*
|
|
26
|
+
* `JWT_SECRET` is read by `runProdApp` at boot to sign session JWTs.
|
|
27
|
+
* Apps mount this feature via `createAuthEmailPasswordFeature(opts)` and
|
|
28
|
+
* pass `jwtSecret` to `runProdApp` separately — declaring it here means
|
|
29
|
+
* `composeEnvSchema({ features: [authFeature, ...] })` flags a missing
|
|
30
|
+
* or short JWT_SECRET at the aggregated boot-validation stage instead of
|
|
31
|
+
* letting it surface as a JWT-decode-failure on first login.
|
|
32
|
+
*
|
|
33
|
+
* `JWT_ISSUER` is optional (Hono-JWT pins the `iss` claim when set).
|
|
34
|
+
*/
|
|
35
|
+
export const authEmailPasswordEnvSchema = z.object({
|
|
36
|
+
JWT_SECRET: z
|
|
37
|
+
.string()
|
|
38
|
+
.min(32, "JWT_SECRET must be ≥32 chars (HS256 minimum)")
|
|
39
|
+
.describe("Symmetric secret for signing session JWTs (HS256).")
|
|
40
|
+
.meta({ kumiko: { pulumi: { generator: "openssl rand -base64 48", secret: true } } }),
|
|
41
|
+
JWT_ISSUER: z
|
|
42
|
+
.string()
|
|
43
|
+
.min(1)
|
|
44
|
+
.optional()
|
|
45
|
+
.describe("Optional `iss` claim pinned on every minted JWT."),
|
|
46
|
+
});
|
|
47
|
+
|
|
22
48
|
// Opt-in configuration for the password-reset flow. When omitted the
|
|
23
49
|
// request-password-reset / reset-password handlers are not registered —
|
|
24
50
|
// the framework-level routes stay 404 and callers know the flow is off.
|
|
@@ -101,6 +127,7 @@ export function createAuthEmailPasswordFeature(
|
|
|
101
127
|
return defineFeature("auth-email-password", (r) => {
|
|
102
128
|
r.requires("user");
|
|
103
129
|
r.requires("tenant");
|
|
130
|
+
r.envSchema(authEmailPasswordEnvSchema);
|
|
104
131
|
|
|
105
132
|
const handlers = {
|
|
106
133
|
login: r.writeHandler(
|
|
@@ -25,7 +25,7 @@ export type {
|
|
|
25
25
|
PasswordResetOptions,
|
|
26
26
|
SignupOptions,
|
|
27
27
|
} from "./feature";
|
|
28
|
-
export { createAuthEmailPasswordFeature } from "./feature";
|
|
28
|
+
export { authEmailPasswordEnvSchema, createAuthEmailPasswordFeature } from "./feature";
|
|
29
29
|
export { hashPassword, verifyPassword } from "./password-hashing";
|
|
30
30
|
// Generic HMAC-signed single-purpose token helpers. Re-exported damit
|
|
31
31
|
// app-spezifische out-of-band-Flows (subscriber-confirm, magic-links,
|
package/src/secrets/feature.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { defineFeature, type FeatureDefinition } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
2
|
import { InternalError } from "@cosmicdrift/kumiko-framework/errors";
|
|
3
3
|
import type { SecretsContext } from "@cosmicdrift/kumiko-framework/secrets";
|
|
4
|
+
import { z } from "zod";
|
|
4
5
|
import { deleteWrite } from "./handlers/delete.write";
|
|
5
6
|
import { listQuery } from "./handlers/list.query";
|
|
6
7
|
import { rotateJob } from "./handlers/rotate.job";
|
|
@@ -8,6 +9,41 @@ import { setWrite } from "./handlers/set.write";
|
|
|
8
9
|
import { secretReadSchema } from "./secrets-context";
|
|
9
10
|
import { tenantSecretEntity } from "./table";
|
|
10
11
|
|
|
12
|
+
/**
|
|
13
|
+
* Env-vars contract for the `secrets` feature. Apps merge this via
|
|
14
|
+
* `composeEnvSchema({ features: [secretsFeature, ...] })` so boot-time
|
|
15
|
+
* validation catches a missing/short KEK before the first `.get()` call.
|
|
16
|
+
*
|
|
17
|
+
* Rotation: declare additional versions (V2, V3, …) in the app's `extend`
|
|
18
|
+
* block — the env-master-key-provider scans the env for any
|
|
19
|
+
* `KUMIKO_SECRETS_MASTER_KEY_V<n>` matching `/^KUMIKO_SECRETS_MASTER_KEY_V(\d+)$/`
|
|
20
|
+
* and picks the highest as the active KEK unless
|
|
21
|
+
* `KUMIKO_SECRETS_MASTER_KEY_CURRENT_VERSION` pins one explicitly.
|
|
22
|
+
*/
|
|
23
|
+
export const secretsEnvSchema = z.object({
|
|
24
|
+
KUMIKO_SECRETS_MASTER_KEY_V1: z
|
|
25
|
+
.string()
|
|
26
|
+
.refine(
|
|
27
|
+
(v) => {
|
|
28
|
+
try {
|
|
29
|
+
return Buffer.from(v, "base64").length === 32;
|
|
30
|
+
} catch {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
{ message: "must be base64-encoded 32 bytes (AES-256 KEK)" },
|
|
35
|
+
)
|
|
36
|
+
.describe("AES-256 master-key (KEK) for tenant-secrets encryption.")
|
|
37
|
+
.meta({ kumiko: { pulumi: { generator: "openssl rand -base64 32", secret: true } } }),
|
|
38
|
+
KUMIKO_SECRETS_MASTER_KEY_CURRENT_VERSION: z
|
|
39
|
+
.string()
|
|
40
|
+
.regex(/^\d+$/, "must be a positive integer (V<n> selector)")
|
|
41
|
+
.default("1")
|
|
42
|
+
.describe(
|
|
43
|
+
"Pins the active KEK version. Default '1'. Bump after writing a higher KUMIKO_SECRETS_MASTER_KEY_V<n>.",
|
|
44
|
+
),
|
|
45
|
+
});
|
|
46
|
+
|
|
11
47
|
export {
|
|
12
48
|
createSecretsContext,
|
|
13
49
|
type SecretsContext,
|
|
@@ -53,6 +89,8 @@ export function requireSecretsContext(
|
|
|
53
89
|
|
|
54
90
|
export function createSecretsFeature(): FeatureDefinition {
|
|
55
91
|
return defineFeature("secrets", (r) => {
|
|
92
|
+
r.envSchema(secretsEnvSchema);
|
|
93
|
+
|
|
56
94
|
// ES entity: set/delete go through the executor, `tenantSecret.created/
|
|
57
95
|
// .updated/.deleted` events land on the aggregate stream. Reads fire a
|
|
58
96
|
// separate `tenantSecretRead` event per call (see secrets-context.get
|
package/src/secrets/index.ts
CHANGED
|
@@ -42,10 +42,25 @@
|
|
|
42
42
|
import type { SubscriptionProviderPlugin } from "@cosmicdrift/kumiko-bundled-features/billing-foundation";
|
|
43
43
|
import { defineFeature, type FeatureDefinition } from "@cosmicdrift/kumiko-framework/engine";
|
|
44
44
|
import { createMollieClient } from "@mollie/api-client";
|
|
45
|
+
import { z } from "zod";
|
|
45
46
|
import { MOLLIE_PROVIDER_NAME, SUBSCRIPTION_MOLLIE_FEATURE } from "./constants";
|
|
46
47
|
import { createMollieCheckoutSession, type MolliePriceConfig } from "./plugin-methods";
|
|
47
48
|
import { type MollieClientShape, verifyAndParseMollieWebhook } from "./verify-webhook";
|
|
48
49
|
|
|
50
|
+
/**
|
|
51
|
+
* Env-vars contract for the `subscription-mollie` feature.
|
|
52
|
+
* Apps load `MOLLIE_API_KEY` from env and forward it to
|
|
53
|
+
* `createSubscriptionMollieFeature({ apiKey, ... })`.
|
|
54
|
+
*/
|
|
55
|
+
export const subscriptionMollieEnvSchema = z.object({
|
|
56
|
+
MOLLIE_API_KEY: z
|
|
57
|
+
.string()
|
|
58
|
+
.min(1, "MOLLIE_API_KEY must not be empty")
|
|
59
|
+
.regex(/^(test|live)_/, "MOLLIE_API_KEY must start with 'test_' or 'live_'")
|
|
60
|
+
.describe("Mollie API key (`test_...` for sandbox, `live_...` for production).")
|
|
61
|
+
.meta({ kumiko: { pulumi: { secret: true } } }),
|
|
62
|
+
});
|
|
63
|
+
|
|
49
64
|
export type SubscriptionMollieOptions = {
|
|
50
65
|
/** Mollie-API-key (`test_...` oder `live_...`). App-wide, beim Plugin-
|
|
51
66
|
* mount aus process.env oder system-config. */
|
|
@@ -132,6 +147,7 @@ export function createSubscriptionMollieFeature(
|
|
|
132
147
|
|
|
133
148
|
return defineFeature(SUBSCRIPTION_MOLLIE_FEATURE, (r) => {
|
|
134
149
|
r.requires("billing-foundation");
|
|
150
|
+
r.envSchema(subscriptionMollieEnvSchema);
|
|
135
151
|
|
|
136
152
|
const plugin: SubscriptionProviderPlugin = {
|
|
137
153
|
verifyAndParseWebhook: verifyAndParse,
|
|
@@ -9,5 +9,6 @@ export { MOLLIE_PROVIDER_NAME, SUBSCRIPTION_MOLLIE_FEATURE } from "./constants";
|
|
|
9
9
|
export {
|
|
10
10
|
createSubscriptionMollieFeature,
|
|
11
11
|
type SubscriptionMollieOptions,
|
|
12
|
+
subscriptionMollieEnvSchema,
|
|
12
13
|
} from "./feature";
|
|
13
14
|
export type { MolliePriceConfig } from "./plugin-methods";
|
|
@@ -38,6 +38,7 @@
|
|
|
38
38
|
import type { SubscriptionProviderPlugin } from "@cosmicdrift/kumiko-bundled-features/billing-foundation";
|
|
39
39
|
import { defineFeature, type FeatureDefinition } from "@cosmicdrift/kumiko-framework/engine";
|
|
40
40
|
import Stripe from "stripe";
|
|
41
|
+
import { z } from "zod";
|
|
41
42
|
import { STRIPE_PROVIDER_NAME, SUBSCRIPTION_STRIPE_FEATURE } from "./constants";
|
|
42
43
|
import {
|
|
43
44
|
createStripeCancelSubscription,
|
|
@@ -46,6 +47,29 @@ import {
|
|
|
46
47
|
} from "./plugin-methods";
|
|
47
48
|
import { verifyAndParseStripeWebhook } from "./verify-webhook";
|
|
48
49
|
|
|
50
|
+
/**
|
|
51
|
+
* Env-vars contract for the `subscription-stripe` feature.
|
|
52
|
+
*
|
|
53
|
+
* The feature itself reads via factory-options (`createSubscriptionStripeFeature({
|
|
54
|
+
* webhookSecret, apiKey })`), so the schema is a Kumiko-pattern contract:
|
|
55
|
+
* apps that mount stripe SHOULD load `STRIPE_WEBHOOK_SECRET` / `STRIPE_API_KEY`
|
|
56
|
+
* from env and forward them. `composeEnvSchema({ features: [stripeFeature] })`
|
|
57
|
+
* surfaces missing/empty values at boot, before
|
|
58
|
+
* `createSubscriptionStripeFeature` throws on `webhookSecret.length === 0`.
|
|
59
|
+
*/
|
|
60
|
+
export const subscriptionStripeEnvSchema = z.object({
|
|
61
|
+
STRIPE_WEBHOOK_SECRET: z
|
|
62
|
+
.string()
|
|
63
|
+
.min(1, "STRIPE_WEBHOOK_SECRET must not be empty")
|
|
64
|
+
.describe("Stripe webhook-signing secret (`whsec_...` from the Stripe dashboard).")
|
|
65
|
+
.meta({ kumiko: { pulumi: { secret: true } } }),
|
|
66
|
+
STRIPE_API_KEY: z
|
|
67
|
+
.string()
|
|
68
|
+
.min(1, "STRIPE_API_KEY must not be empty")
|
|
69
|
+
.describe("Stripe API key (`sk_live_...` / `sk_test_...`).")
|
|
70
|
+
.meta({ kumiko: { pulumi: { secret: true } } }),
|
|
71
|
+
});
|
|
72
|
+
|
|
49
73
|
export type SubscriptionStripeOptions = {
|
|
50
74
|
/** Webhook-secret aus dem Stripe-Dashboard. App-wide. Plugin throws
|
|
51
75
|
* beim runtime wenn empty (= App-Owner hat sub-stripe gemountet
|
|
@@ -105,6 +129,7 @@ export function createSubscriptionStripeFeature(
|
|
|
105
129
|
// tenant-config noch tenant-secrets (alles app-wide via factory-
|
|
106
130
|
// options).
|
|
107
131
|
r.requires("billing-foundation");
|
|
132
|
+
r.envSchema(subscriptionStripeEnvSchema);
|
|
108
133
|
|
|
109
134
|
// Plugin: register against subscription-foundation's
|
|
110
135
|
// "subscriptionProvider" extension. entityName "stripe" matcht den
|
|
@@ -11,4 +11,8 @@ export {
|
|
|
11
11
|
StripeEventTypes,
|
|
12
12
|
SUBSCRIPTION_STRIPE_FEATURE,
|
|
13
13
|
} from "./constants";
|
|
14
|
-
export {
|
|
14
|
+
export {
|
|
15
|
+
createSubscriptionStripeFeature,
|
|
16
|
+
type SubscriptionStripeOptions,
|
|
17
|
+
subscriptionStripeEnvSchema,
|
|
18
|
+
} from "./feature";
|