@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.
Files changed (38) hide show
  1. package/package.json +4 -4
  2. package/src/api/__tests__/api.test.ts +3 -2
  3. package/src/api/__tests__/body-limit.test.ts +4 -4
  4. package/src/api/__tests__/extra-routes.integration.test.ts +584 -0
  5. package/src/api/__tests__/http-route-entry.integration.test.ts +114 -0
  6. package/src/api/auth-routes.ts +53 -24
  7. package/src/api/extra-route.ts +146 -0
  8. package/src/api/index.ts +16 -1
  9. package/src/api/server.ts +376 -72
  10. package/src/changes.json +37 -0
  11. package/src/engine/__tests__/http-route-anonymous-required.test.ts +43 -0
  12. package/src/engine/__tests__/membership-roles.test.ts +13 -4
  13. package/src/engine/boot-validator/__tests__/access-declarations.test.ts +127 -0
  14. package/src/engine/boot-validator/__tests__/no-all-role-in-handler-access.test.ts +77 -0
  15. package/src/engine/boot-validator/access-declarations.ts +59 -7
  16. package/src/engine/boot-validator/entity-handler.ts +20 -0
  17. package/src/engine/feature-ast/__tests__/patch.test.ts +1 -0
  18. package/src/engine/feature-ast/__tests__/patcher.test.ts +1 -0
  19. package/src/engine/feature-ast/__tests__/read-optional-access-rule.test.ts +14 -0
  20. package/src/engine/feature-ast/extractors/hooks.ts +3 -1
  21. package/src/engine/feature-ast/extractors/jobs-routes.ts +5 -2
  22. package/src/engine/feature-ast/patcher.ts +2 -2
  23. package/src/engine/feature-ast/patterns.ts +1 -1
  24. package/src/engine/feature-ast/render.ts +1 -1
  25. package/src/engine/feature-ui-extensions.ts +6 -0
  26. package/src/engine/index.ts +2 -0
  27. package/src/engine/membership-roles.ts +20 -4
  28. package/src/engine/pattern-library/__tests__/library.test.ts +1 -0
  29. package/src/engine/pattern-library/mixed-schemas.ts +1 -0
  30. package/src/engine/system-user.ts +5 -5
  31. package/src/engine/types/index.ts +2 -0
  32. package/src/entrypoint/index.ts +2 -0
  33. package/src/observability/__tests__/metrics-wiring.test.ts +61 -0
  34. package/src/observability/index.ts +5 -0
  35. package/src/observability/metrics-wiring.ts +32 -0
  36. package/src/stack/test-stack.ts +8 -2
  37. package/src/testing/handler-context.ts +3 -1
  38. 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
+ }
@@ -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
- // in tests and dev-server extraRoutes (provider-webhook wiring).
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: ["all"],
81
+ roles: [ANONYMOUS_ROLE],
80
82
  };
81
83
  return {
82
84
  user: stubUser,
@@ -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";