@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.
- package/package.json +3 -3
- package/src/api/__tests__/api.test.ts +4 -0
- package/src/api/__tests__/auth-routes-cookie.test.ts +21 -2
- package/src/api/__tests__/auth-routes-invalid-body-invite.test.ts +4 -0
- package/src/api/__tests__/auth-routes-mfa-preauth-confirm.test.ts +4 -0
- package/src/api/__tests__/auth-routes-mfa-preauth-enable-start.test.ts +4 -0
- package/src/api/__tests__/auth-routes-mfa-verify.test.ts +4 -0
- package/src/api/__tests__/auth-routes-trusted-proxy.test.ts +4 -0
- package/src/api/__tests__/server-boot-guards.test.ts +60 -0
- package/src/api/auth-middleware.ts +2 -7
- package/src/api/auth-routes.ts +28 -14
- package/src/api/server.ts +14 -1
- package/src/changes.json +27 -0
- package/src/engine/__tests__/hook-identity-switch-grant.test.ts +84 -3
- package/src/engine/active-membership.ts +56 -0
- package/src/engine/boot-validator/__tests__/access-declarations.test.ts +331 -15
- package/src/engine/boot-validator/access-declarations.ts +101 -27
- package/src/engine/boot-validator/zod-shape.ts +92 -1
- package/src/engine/extension-names.ts +16 -0
- package/src/engine/feature-ast/__tests__/read-optional-access-rule.test.ts +28 -0
- package/src/engine/feature-ast/extractors/hooks.ts +8 -4
- package/src/engine/index.ts +15 -0
- package/src/engine/types/index.ts +4 -0
- package/src/pipeline/__tests__/system-identity-switch.integration.test.ts +38 -0
- package/src/pipeline/active-membership.ts +178 -0
- package/src/pipeline/dispatch-shared.ts +14 -1
- package/src/pipeline/dispatcher.ts +22 -1
- package/src/pipeline/system-identity-switch.ts +44 -13
- package/src/testing/handler-context.ts +4 -0
- 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
|
-
|
|
90
|
-
|
|
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
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
if (!ctxQueryAs && !ctxWriteAs) return
|
|
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,
|
package/src/ui-types/index.ts
CHANGED
|
@@ -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";
|