@cosmicdrift/kumiko-framework 0.264.1 → 0.266.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 (30) hide show
  1. package/package.json +3 -3
  2. package/src/api/__tests__/api.test.ts +4 -0
  3. package/src/api/__tests__/auth-routes-cookie.test.ts +21 -2
  4. package/src/api/__tests__/auth-routes-invalid-body-invite.test.ts +4 -0
  5. package/src/api/__tests__/auth-routes-mfa-preauth-confirm.test.ts +4 -0
  6. package/src/api/__tests__/auth-routes-mfa-preauth-enable-start.test.ts +4 -0
  7. package/src/api/__tests__/auth-routes-mfa-verify.test.ts +4 -0
  8. package/src/api/__tests__/auth-routes-trusted-proxy.test.ts +4 -0
  9. package/src/api/__tests__/server-boot-guards.test.ts +60 -0
  10. package/src/api/auth-middleware.ts +2 -7
  11. package/src/api/auth-routes.ts +28 -14
  12. package/src/api/server.ts +14 -1
  13. package/src/changes.json +27 -0
  14. package/src/engine/__tests__/hook-identity-switch-grant.test.ts +84 -3
  15. package/src/engine/active-membership.ts +56 -0
  16. package/src/engine/boot-validator/__tests__/access-declarations.test.ts +331 -15
  17. package/src/engine/boot-validator/access-declarations.ts +101 -27
  18. package/src/engine/boot-validator/zod-shape.ts +92 -1
  19. package/src/engine/extension-names.ts +16 -0
  20. package/src/engine/feature-ast/__tests__/read-optional-access-rule.test.ts +28 -0
  21. package/src/engine/feature-ast/extractors/hooks.ts +8 -4
  22. package/src/engine/index.ts +15 -0
  23. package/src/engine/types/index.ts +4 -0
  24. package/src/pipeline/__tests__/system-identity-switch.integration.test.ts +38 -0
  25. package/src/pipeline/active-membership.ts +178 -0
  26. package/src/pipeline/dispatch-shared.ts +14 -1
  27. package/src/pipeline/dispatcher.ts +22 -1
  28. package/src/pipeline/system-identity-switch.ts +44 -13
  29. package/src/testing/handler-context.ts +4 -0
  30. package/src/ui-types/index.ts +1 -0
@@ -1,14 +1,20 @@
1
1
  import { SYSTEM_ROLE, SYSTEM_USER_ID } from "../engine/system-user";
2
2
  import type {
3
+ ActiveMembershipResult,
3
4
  EscapeHatchDeclaration,
4
5
  LifecycleHookFn,
5
6
  SessionUser,
6
7
  WriteResult,
7
8
  } from "../engine/types";
9
+ import type { TenantId } from "../engine/types/identifiers";
8
10
  import { AccessDeniedError, FrameworkReasons, InternalError } from "../errors";
9
11
 
10
12
  export type QueryAsFn = (user: SessionUser, qn: string, payload: unknown) => Promise<unknown>;
11
13
  export type WriteAsFn = (user: SessionUser, qn: string, payload: unknown) => Promise<WriteResult>;
14
+ export type ResolveActiveMembershipFn = (
15
+ userId: string,
16
+ tenantId: TenantId,
17
+ ) => Promise<ActiveMembershipResult>;
12
18
  export type IdentitySwitch = { readonly queryAs: QueryAsFn; readonly writeAs: WriteAsFn };
13
19
 
14
20
  export function isSystemIdentity(user: SessionUser): boolean {
@@ -22,7 +28,7 @@ export function isSystemIdentitySwitchAllowed(
22
28
  return !isSystemIdentity(asUser) || allowSystemIdentity;
23
29
  }
24
30
 
25
- function systemIdentitySwitchDenied(callerLabel: string): AccessDeniedError {
31
+ export function systemIdentitySwitchDenied(callerLabel: string): AccessDeniedError {
26
32
  return new AccessDeniedError({
27
33
  message:
28
34
  `${callerLabel} may not switch identity to SYSTEM — declare r.systemScope() or ` +
@@ -57,9 +63,9 @@ export function createGatedIdentitySwitch(
57
63
  return gated;
58
64
  }
59
65
 
60
- function readIdentitySwitchFn<TFn extends QueryAsFn | WriteAsFn>(
66
+ function readIdentitySwitchFn<TFn extends QueryAsFn | WriteAsFn | ResolveActiveMembershipFn>(
61
67
  context: object,
62
- key: "queryAs" | "writeAs",
68
+ key: "queryAs" | "writeAs" | "resolveActiveMembership",
63
69
  ): TFn | undefined {
64
70
  if (!(key in context)) return undefined;
65
71
  const value = (context as Record<string, unknown>)[key];
@@ -74,6 +80,12 @@ function unavailableIdentitySwitchFn(callerLabel: string, kind: "queryAs" | "wri
74
80
  };
75
81
  }
76
82
 
83
+ function deniedResolveActiveMembership(callerLabel: string): ResolveActiveMembershipFn {
84
+ return async () => {
85
+ throw systemIdentitySwitchDenied(callerLabel);
86
+ };
87
+ }
88
+
77
89
  // Fail-closed fallback for a ctx not built by the dispatcher bridge (unit-test stub, duplicated module instance).
78
90
  function fallbackUngatedIdentitySwitch(
79
91
  callerLabel: string,
@@ -86,29 +98,48 @@ function fallbackUngatedIdentitySwitch(
86
98
  };
87
99
  }
88
100
 
89
- export function withHookIdentitySwitchGrant<TContext extends object>(
90
- context: TContext,
101
+ // Reuses the ORIGINAL ungated pair when known, so this grant doesn't compose with the caller's.
102
+ function gatedIdentitySwitchFields(
91
103
  callerLabel: string,
92
104
  escapeHatch: EscapeHatchDeclaration | undefined,
93
- ): TContext {
94
- const ctxQueryAs = readIdentitySwitchFn<QueryAsFn>(context, "queryAs");
95
- const ctxWriteAs = readIdentitySwitchFn<WriteAsFn>(context, "writeAs");
96
- if (!ctxQueryAs && !ctxWriteAs) return context;
97
-
98
- // Reuse the ORIGINAL ungated pair when known, so this grant doesn't compose with the caller's.
105
+ ctxQueryAs: QueryAsFn | undefined,
106
+ ctxWriteAs: WriteAsFn | undefined,
107
+ ): Partial<IdentitySwitch> {
108
+ if (!ctxQueryAs && !ctxWriteAs) return {};
99
109
  const ungated: IdentitySwitch =
100
110
  (ctxQueryAs && ungatedByGated.get(ctxQueryAs)) ??
101
111
  (ctxWriteAs && ungatedByGated.get(ctxWriteAs)) ??
102
112
  fallbackUngatedIdentitySwitch(callerLabel, ctxQueryAs, ctxWriteAs);
103
-
104
113
  const gated = createGatedIdentitySwitch(callerLabel, escapeHatch !== undefined, ungated);
105
114
  return {
106
- ...context,
107
115
  ...(ctxQueryAs && { queryAs: gated.queryAs }),
108
116
  ...(ctxWriteAs && { writeAs: gated.writeAs }),
109
117
  };
110
118
  }
111
119
 
120
+ export function withHookIdentitySwitchGrant<TContext extends object>(
121
+ context: TContext,
122
+ callerLabel: string,
123
+ escapeHatch: EscapeHatchDeclaration | undefined,
124
+ ): TContext {
125
+ const ctxQueryAs = readIdentitySwitchFn<QueryAsFn>(context, "queryAs");
126
+ const ctxWriteAs = readIdentitySwitchFn<WriteAsFn>(context, "writeAs");
127
+ const ctxResolveActiveMembership = readIdentitySwitchFn<ResolveActiveMembershipFn>(
128
+ context,
129
+ "resolveActiveMembership",
130
+ );
131
+ if (!ctxQueryAs && !ctxWriteAs && !ctxResolveActiveMembership) return context;
132
+
133
+ return {
134
+ ...context,
135
+ ...gatedIdentitySwitchFields(callerLabel, escapeHatch, ctxQueryAs, ctxWriteAs),
136
+ ...(ctxResolveActiveMembership &&
137
+ escapeHatch === undefined && {
138
+ resolveActiveMembership: deniedResolveActiveMembership(callerLabel),
139
+ }),
140
+ };
141
+ }
142
+
112
143
  // Re-gates a hook's own ctx.queryAs/ctx.writeAs instead of inheriting the handler's grant.
113
144
  export function bindHookIdentitySwitchGrant(
114
145
  fn: LifecycleHookFn,
@@ -57,6 +57,7 @@ export function bridgeStub(opts?: {
57
57
  | "loadAggregateWithSnapshot"
58
58
  | "queryProjection"
59
59
  | "resolveAuthClaims"
60
+ | "resolveActiveMembership"
60
61
  | "hasFeature"
61
62
  | "metrics"
62
63
  | "metricsFor"
@@ -118,6 +119,9 @@ export function bridgeStub(opts?: {
118
119
  resolveAuthClaims: notAvailable(
119
120
  "resolveAuthClaims",
120
121
  ) as unknown as HandlerContext["resolveAuthClaims"],
122
+ resolveActiveMembership: notAvailable(
123
+ "resolveActiveMembership",
124
+ ) as unknown as HandlerContext["resolveActiveMembership"],
121
125
  // Stub defaults to always-enabled — matches the dispatcher's behaviour
122
126
  // when no effectiveFeatures resolver is wired (tests without toggles).
123
127
  hasFeature: async () => true,
@@ -56,6 +56,7 @@ export type {
56
56
  AccessRule,
57
57
  OpenToAllAccessRule,
58
58
  OpenToAllDeclaration,
59
+ OpenToAllPersonalData,
59
60
  } from "../engine/types/handlers";
60
61
  export { isOpenToAllGranted } from "../engine/types/handlers";
61
62
  export type { IconKey, NavDefinition, NavIconKey } from "../engine/types/nav";