@cosmicdrift/kumiko-framework 0.297.0 → 0.304.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/package.json +4 -4
- package/src/api/__tests__/api.test.ts +3 -2
- package/src/api/__tests__/body-limit.test.ts +4 -4
- package/src/api/__tests__/extra-routes.integration.test.ts +584 -0
- package/src/api/__tests__/http-route-entry.integration.test.ts +114 -0
- package/src/api/auth-routes.ts +53 -24
- package/src/api/extra-route.ts +146 -0
- package/src/api/index.ts +16 -1
- package/src/api/server.ts +376 -72
- package/src/changes.json +37 -0
- package/src/engine/__tests__/http-route-anonymous-required.test.ts +43 -0
- package/src/engine/__tests__/membership-roles.test.ts +13 -4
- package/src/engine/boot-validator/__tests__/access-declarations.test.ts +127 -0
- package/src/engine/boot-validator/__tests__/no-all-role-in-handler-access.test.ts +77 -0
- package/src/engine/boot-validator/access-declarations.ts +59 -7
- package/src/engine/boot-validator/entity-handler.ts +20 -0
- package/src/engine/feature-ast/__tests__/patch.test.ts +1 -0
- package/src/engine/feature-ast/__tests__/patcher.test.ts +1 -0
- package/src/engine/feature-ast/__tests__/read-optional-access-rule.test.ts +14 -0
- package/src/engine/feature-ast/extractors/hooks.ts +3 -1
- package/src/engine/feature-ast/extractors/jobs-routes.ts +5 -2
- package/src/engine/feature-ast/patcher.ts +2 -2
- package/src/engine/feature-ast/patterns.ts +1 -1
- package/src/engine/feature-ast/render.ts +1 -1
- package/src/engine/feature-ui-extensions.ts +6 -0
- package/src/engine/index.ts +2 -0
- package/src/engine/membership-roles.ts +20 -4
- package/src/engine/pattern-library/__tests__/library.test.ts +1 -0
- package/src/engine/pattern-library/mixed-schemas.ts +1 -0
- package/src/engine/system-user.ts +5 -5
- package/src/engine/types/index.ts +2 -0
- package/src/entrypoint/index.ts +2 -0
- package/src/observability/__tests__/metrics-wiring.test.ts +61 -0
- package/src/observability/index.ts +5 -0
- package/src/observability/metrics-wiring.ts +32 -0
- package/src/stack/test-stack.ts +8 -2
- package/src/testing/handler-context.ts +3 -1
- package/src/ui-types/index.ts +2 -0
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { composeEnvSchema, readKumikoMeta } from "../../env";
|
|
4
|
+
import { prometheusMetricsEnvSchema, resolveObservabilityWiring } from "../metrics-wiring";
|
|
5
|
+
|
|
6
|
+
describe("resolveObservabilityWiring", () => {
|
|
7
|
+
it("returns {} without a token", () => {
|
|
8
|
+
expect(resolveObservabilityWiring(undefined)).toEqual({});
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it("returns {} for an empty token", () => {
|
|
12
|
+
expect(resolveObservabilityWiring("")).toEqual({});
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it("wires a prometheus provider and metrics route when a token is set", () => {
|
|
16
|
+
const token = "a".repeat(32);
|
|
17
|
+
const wiring = resolveObservabilityWiring(token);
|
|
18
|
+
|
|
19
|
+
expect("metrics" in wiring).toBe(true);
|
|
20
|
+
if (!("metrics" in wiring)) throw new Error("expected wiring to include metrics");
|
|
21
|
+
|
|
22
|
+
expect(wiring.metrics).toEqual({ path: "/metrics", token });
|
|
23
|
+
expect(wiring.observability.name).toBe("prometheus");
|
|
24
|
+
expect(wiring.observability.meter.snapshot()).toEqual(new Map());
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
describe("prometheusMetricsEnvSchema", () => {
|
|
29
|
+
it("rejects tokens shorter than 32 characters", () => {
|
|
30
|
+
const result = prometheusMetricsEnvSchema.safeParse({
|
|
31
|
+
PROMETHEUS_METRICS_TOKEN: "a".repeat(31),
|
|
32
|
+
});
|
|
33
|
+
expect(result.success).toBe(false);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("accepts a 32-character token", () => {
|
|
37
|
+
const result = prometheusMetricsEnvSchema.safeParse({
|
|
38
|
+
PROMETHEUS_METRICS_TOKEN: "a".repeat(32),
|
|
39
|
+
});
|
|
40
|
+
expect(result.success).toBe(true);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("accepts a missing token", () => {
|
|
44
|
+
const result = prometheusMetricsEnvSchema.safeParse({});
|
|
45
|
+
expect(result.success).toBe(true);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("exposes pulumi secret metadata for consumer env-schemas", () => {
|
|
49
|
+
const { schema } = composeEnvSchema({
|
|
50
|
+
features: [],
|
|
51
|
+
extend: z.object({ FOO: z.string() }).extend(prometheusMetricsEnvSchema.shape),
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
const field = schema.shape["PROMETHEUS_METRICS_TOKEN"];
|
|
55
|
+
if (!(field instanceof z.ZodType))
|
|
56
|
+
throw new Error("expected PROMETHEUS_METRICS_TOKEN in composed schema");
|
|
57
|
+
const meta = readKumikoMeta(field);
|
|
58
|
+
expect(meta.pulumi?.secret).toBe(true);
|
|
59
|
+
expect(meta.pulumi?.generator).toBe("openssl rand -base64 32");
|
|
60
|
+
});
|
|
61
|
+
});
|
|
@@ -16,6 +16,11 @@ export {
|
|
|
16
16
|
createSafeMetricsHandle,
|
|
17
17
|
createUnboundMetricsHandle,
|
|
18
18
|
} from "./metrics-handle";
|
|
19
|
+
export {
|
|
20
|
+
type ObservabilityWiring,
|
|
21
|
+
prometheusMetricsEnvSchema,
|
|
22
|
+
resolveObservabilityWiring,
|
|
23
|
+
} from "./metrics-wiring";
|
|
19
24
|
export { createNoopProvider } from "./noop-provider";
|
|
20
25
|
export {
|
|
21
26
|
createPrometheusMeter,
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { createNoopProvider } from "./noop-provider";
|
|
3
|
+
import { createPrometheusMeter, type PrometheusMeter } from "./prometheus-meter";
|
|
4
|
+
import type { ObservabilityProvider } from "./types";
|
|
5
|
+
|
|
6
|
+
export const prometheusMetricsEnvSchema = z.object({
|
|
7
|
+
PROMETHEUS_METRICS_TOKEN: z
|
|
8
|
+
.string()
|
|
9
|
+
.min(32)
|
|
10
|
+
.optional()
|
|
11
|
+
.describe("Bearer token for /metrics; unset keeps the endpoint off.")
|
|
12
|
+
.meta({ kumiko: { pulumi: { secret: true, generator: "openssl rand -base64 32" } } }),
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
type PrometheusObservabilityProvider = ObservabilityProvider & { readonly meter: PrometheusMeter };
|
|
16
|
+
|
|
17
|
+
export type ObservabilityWiring =
|
|
18
|
+
| {
|
|
19
|
+
readonly observability: PrometheusObservabilityProvider;
|
|
20
|
+
readonly metrics: { readonly path: string; readonly token: string };
|
|
21
|
+
}
|
|
22
|
+
| Record<string, never>;
|
|
23
|
+
|
|
24
|
+
// Fail-closed: public tenant hosts share the /metrics port, so no token means no endpoint.
|
|
25
|
+
export function resolveObservabilityWiring(metricsToken: string | undefined): ObservabilityWiring {
|
|
26
|
+
if (!metricsToken) return {};
|
|
27
|
+
return {
|
|
28
|
+
// Overrides the spread's name "noop", which would otherwise show up in diagnostics/logs.
|
|
29
|
+
observability: { ...createNoopProvider(), name: "prometheus", meter: createPrometheusMeter() },
|
|
30
|
+
metrics: { path: "/metrics", token: metricsToken },
|
|
31
|
+
};
|
|
32
|
+
}
|
package/src/stack/test-stack.ts
CHANGED
|
@@ -42,8 +42,9 @@ export type TestStack = {
|
|
|
42
42
|
// to assert a consumer pushed an invalidation without opening a real SSE
|
|
43
43
|
// connection.
|
|
44
44
|
sseBroker: SseBroker;
|
|
45
|
-
// Command-dispatcher behind the HTTP routes — for direct system-writes
|
|
46
|
-
//
|
|
45
|
+
// Command-dispatcher behind the HTTP routes — for direct system-writes in
|
|
46
|
+
// tests and behind entry:"signature" extraRoutes / the `wire` hook
|
|
47
|
+
// (provider-webhook wiring).
|
|
47
48
|
dispatcher: Dispatcher;
|
|
48
49
|
// The AppContext buildServer handed the request path, incl. the fields it
|
|
49
50
|
// wires itself (_fileProviderResolver). A dev-server that starts its own
|
|
@@ -105,6 +106,10 @@ export type TestStackOptions = {
|
|
|
105
106
|
* The resolver is auto-built from the test Redis. Mirrors
|
|
106
107
|
* buildServer's `rateLimit` option 1:1 — see there for shape. */
|
|
107
108
|
rateLimit?: import("../api/server").ServerOptions["rateLimit"];
|
|
109
|
+
/** Forwarded to buildServer — Integration-Tests that exercise `extraRoutes`
|
|
110
|
+
* MUST go through here (real HTTP via `stack.http`/`stack.app.fetch`),
|
|
111
|
+
* never `createTestDispatcher`. */
|
|
112
|
+
extraRoutes?: import("../api/server").ServerOptions["extraRoutes"];
|
|
108
113
|
/** Inject a MasterKeyProvider for secrets-backed tests. Lands typed in
|
|
109
114
|
* AppContext — set/delete/get + rotation job pick it up. Omit for
|
|
110
115
|
* suites that don't touch secrets. */
|
|
@@ -404,6 +409,7 @@ export async function setupTestStack(options: TestStackOptions): Promise<TestSta
|
|
|
404
409
|
},
|
|
405
410
|
eventDedup,
|
|
406
411
|
sseBroker,
|
|
412
|
+
...(options.extraRoutes && { extraRoutes: options.extraRoutes }),
|
|
407
413
|
// Tests drive the dispatcher via stack.eventDispatcher.runOnce() for
|
|
408
414
|
// deterministic drains — no timer-induced flakiness. pollIntervalMs
|
|
409
415
|
// stays short anyway in case a test opts into `.start()`. pgClient
|
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
// production services (delivery-service uses it to run cross-feature notify
|
|
6
6
|
// calls without a real dispatcher). Hence the runtime classification despite
|
|
7
7
|
// living under `testing/` — no vitest imports, no test side-effects.
|
|
8
|
+
|
|
9
|
+
import { ANONYMOUS_ROLE } from "../engine/system-user";
|
|
8
10
|
import type {
|
|
9
11
|
AppendEventArgs,
|
|
10
12
|
FetchForWritingArgs,
|
|
@@ -76,7 +78,7 @@ export function bridgeStub(opts?: {
|
|
|
76
78
|
const stubUser: SessionUser = opts?.user ?? {
|
|
77
79
|
id: "00000000-0000-0000-0000-000000000000",
|
|
78
80
|
tenantId: "00000000-0000-0000-0000-000000000000" as SessionUser["tenantId"], // @cast-boundary engine-bridge
|
|
79
|
-
roles: [
|
|
81
|
+
roles: [ANONYMOUS_ROLE],
|
|
80
82
|
};
|
|
81
83
|
return {
|
|
82
84
|
user: stubUser,
|
package/src/ui-types/index.ts
CHANGED
|
@@ -61,6 +61,8 @@ export type {
|
|
|
61
61
|
OpenToAllAccessRule,
|
|
62
62
|
OpenToAllDeclaration,
|
|
63
63
|
OpenToAllPersonalData,
|
|
64
|
+
RoleAccessPersonalData,
|
|
65
|
+
RoleAccessRule,
|
|
64
66
|
} from "../engine/types/handlers";
|
|
65
67
|
export { isOpenToAllGranted } from "../engine/types/handlers";
|
|
66
68
|
export type { IconKey, NavDefinition, NavIconKey } from "../engine/types/nav";
|