@cosmicdrift/kumiko-bundled-features 0.269.2 → 0.270.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 +9 -9
- package/src/__tests__/account-security-composition.boot.test.ts +25 -7
- package/src/__tests__/account-security-composition.integration.test.ts +23 -1
- package/src/agent-tools/__tests__/agent-doc-lint.test.ts +3 -1
- package/src/agent-tools/__tests__/feature.test.ts +10 -4
- package/src/auth-email-password/handlers/invite-accept.write.ts +8 -1
- package/src/auth-mfa/handlers/disable.write.ts +7 -1
- package/src/auth-mfa/handlers/enable-confirm.write.ts +7 -1
- package/src/auth-mfa/handlers/enable-start.write.ts +7 -1
- package/src/auth-mfa/handlers/regenerate-recovery.write.ts +7 -1
- package/src/auth-mfa/handlers/status.query.ts +7 -1
- package/src/auth-mfa/screens.ts +22 -3
- package/src/channel-in-app/handlers/inbox.query.ts +7 -1
- package/src/channel-in-app/handlers/mark-all-read.write.ts +7 -1
- package/src/channel-in-app/handlers/mark-read.write.ts +7 -1
- package/src/channel-in-app/handlers/unread-count.query.ts +7 -1
- package/src/compliance-profiles/handlers/for-tenant.query.ts +7 -1
- package/src/compliance-profiles/handlers/list-profiles.query.ts +7 -1
- package/src/config/__tests__/config.integration.test.ts +1 -1
- package/src/config/__tests__/tz-resolution.integration.test.ts +1 -1
- package/src/config/handlers/cascade.query.ts +7 -1
- package/src/config/handlers/readiness.query.ts +7 -1
- package/src/config/handlers/reset.write.ts +7 -1
- package/src/config/handlers/schema.query.ts +7 -1
- package/src/config/handlers/set.write.ts +7 -1
- package/src/config/handlers/values.query.ts +7 -1
- package/src/data-retention/handlers/policy-for.query.ts +8 -1
- package/src/delivery/__tests__/delivery-pii-kms.integration.test.ts +2 -2
- package/src/delivery/__tests__/delivery-screens.boot.test.ts +2 -4
- package/src/delivery/__tests__/delivery.integration.test.ts +5 -5
- package/src/delivery/handlers/preferences.query.ts +7 -1
- package/src/delivery/handlers/set-preference.write.ts +7 -1
- package/src/derivatives-sharp/__tests__/derivatives-sharp.integration.test.ts +1 -1
- package/src/folders/__tests__/feature.test.ts +14 -5
- package/src/folders/__tests__/folders-parent-visibility.integration.test.ts +6 -1
- package/src/folders/__tests__/folders-read-gate.integration.test.ts +6 -1
- package/src/folders/__tests__/folders.integration.test.ts +6 -1
- package/src/folders/feature.ts +2 -2
- package/src/form-draft/constants.ts +7 -1
- package/src/notes-history/__tests__/notes-read-gate.integration.test.ts +10 -2
- package/src/notes-history/feature.ts +2 -2
- package/src/personal-access-tokens/handlers/available-scopes.query.ts +7 -1
- package/src/personal-access-tokens/handlers/create.write.ts +7 -1
- package/src/personal-access-tokens/handlers/list.query.ts +7 -1
- package/src/personal-access-tokens/handlers/revoke.write.ts +7 -1
- package/src/personal-access-tokens/screens.ts +15 -2
- package/src/secrets/__tests__/access-options-precedence.test.ts +11 -4
- package/src/secrets/__tests__/access-options.integration.test.ts +5 -1
- package/src/secrets/feature.ts +2 -2
- package/src/sessions/handlers/mine.query.ts +7 -1
- package/src/sessions/handlers/revoke-all-others.write.ts +7 -1
- package/src/sessions/handlers/revoke.write.ts +7 -1
- package/src/sessions/screens.ts +7 -1
- package/src/tags/__tests__/feature.test.ts +25 -8
- package/src/tags/__tests__/tags-parent-visibility.integration.test.ts +6 -1
- package/src/tags/__tests__/tags-read-gate.integration.test.ts +7 -2
- package/src/tags/__tests__/tags.integration.test.ts +6 -1
- package/src/tags/constants.ts +1 -1
- package/src/tags/feature.ts +2 -2
- package/src/tenant/__tests__/tenant-timezone-boot.integration.test.ts +2 -2
- package/src/tenant/__tests__/tenant.integration.test.ts +5 -1
- package/src/tenant/handlers/me.query.ts +7 -1
- package/src/user/handlers/me.query.ts +7 -1
- package/src/user-data-rights/__tests__/inspector-screens.boot.test.ts +6 -1
- package/src/user-data-rights/feature.ts +8 -1
- package/src/user-data-rights/handlers/cancel-deletion.write.ts +7 -1
- package/src/user-data-rights/handlers/download-by-job.query.ts +7 -1
- package/src/user-data-rights/handlers/export-status.query.ts +7 -1
- package/src/user-data-rights/handlers/my-audit-log.query.ts +8 -1
- package/src/user-data-rights/handlers/request-deletion.write.ts +7 -1
- package/src/user-data-rights/handlers/request-export.write.ts +7 -1
- package/src/user-data-rights/handlers/restrict-account.write.ts +8 -1
- package/src/user-data-rights/lib/is-admin-actor.ts +1 -1
- package/src/user-profile/__tests__/profile-screen.boot.test.ts +6 -1
- package/src/user-profile/feature.ts +8 -1
- package/src/workflow-runner/__tests__/pending-projection.integration.test.ts +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-bundled-features",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.270.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>",
|
|
@@ -129,12 +129,12 @@
|
|
|
129
129
|
"./workflow-runner": "./src/workflow-runner/index.ts"
|
|
130
130
|
},
|
|
131
131
|
"dependencies": {
|
|
132
|
-
"@cosmicdrift/kumiko-dispatcher-live": "0.
|
|
133
|
-
"@cosmicdrift/kumiko-framework": "0.
|
|
134
|
-
"@cosmicdrift/kumiko-headless": "0.
|
|
135
|
-
"@cosmicdrift/kumiko-renderer": "0.
|
|
136
|
-
"@cosmicdrift/kumiko-renderer-web": "0.
|
|
137
|
-
"@cosmicdrift/kumiko-types": "0.
|
|
132
|
+
"@cosmicdrift/kumiko-dispatcher-live": "0.270.0",
|
|
133
|
+
"@cosmicdrift/kumiko-framework": "0.270.0",
|
|
134
|
+
"@cosmicdrift/kumiko-headless": "0.270.0",
|
|
135
|
+
"@cosmicdrift/kumiko-renderer": "0.270.0",
|
|
136
|
+
"@cosmicdrift/kumiko-renderer-web": "0.270.0",
|
|
137
|
+
"@cosmicdrift/kumiko-types": "0.270.0",
|
|
138
138
|
"@mollie/api-client": "^4.5.0",
|
|
139
139
|
"@node-rs/argon2": "^2.0.2",
|
|
140
140
|
"@types/mailparser": "^3.4.6",
|
|
@@ -163,7 +163,7 @@
|
|
|
163
163
|
],
|
|
164
164
|
"devDependencies": {
|
|
165
165
|
"@testing-library/user-event": "^14.6.1",
|
|
166
|
-
"@cosmicdrift/kumiko-locale-de": "0.
|
|
167
|
-
"@cosmicdrift/kumiko-locale-es": "0.
|
|
166
|
+
"@cosmicdrift/kumiko-locale-de": "0.270.0",
|
|
167
|
+
"@cosmicdrift/kumiko-locale-es": "0.270.0"
|
|
168
168
|
}
|
|
169
169
|
}
|
|
@@ -40,14 +40,32 @@ describe("account-security composed from bundled screens (fw#2841)", () => {
|
|
|
40
40
|
|
|
41
41
|
test("the self-service screens are open to every signed-in user", () => {
|
|
42
42
|
const [, , , , , sessions, mfa] = bootFeatures();
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
43
|
+
const screensWithReason: ReadonlyArray<{
|
|
44
|
+
readonly screen: { readonly access?: unknown } | undefined;
|
|
45
|
+
readonly reason: string;
|
|
46
|
+
}> = [
|
|
47
|
+
{
|
|
48
|
+
screen: sessions?.screens[SESSION_MINE_SCREEN_ID],
|
|
49
|
+
reason:
|
|
50
|
+
"each signed-in user views and revokes only their own sessions, mirroring the mine/revoke handlers",
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
screen: mfa?.screens[MFA_ENABLE_SCREEN_ID],
|
|
54
|
+
reason:
|
|
55
|
+
"each signed-in user may enroll their own account in MFA, mirroring the enable-start handler",
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
screen: mfa?.screens[MFA_DISABLE_SCREEN_ID],
|
|
59
|
+
reason: "each signed-in user may turn off their own MFA, mirroring the disable handler",
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
screen: mfa?.screens[MFA_REGENERATE_RECOVERY_SCREEN_ID],
|
|
63
|
+
reason:
|
|
64
|
+
"each signed-in user may regenerate their own recovery codes, mirroring the regenerate-recovery handler",
|
|
65
|
+
},
|
|
48
66
|
];
|
|
49
|
-
for (const screen of
|
|
50
|
-
expect(screen?.access).toEqual({ openToAll:
|
|
67
|
+
for (const { screen, reason } of screensWithReason) {
|
|
68
|
+
expect(screen?.access).toEqual({ openToAll: { reason } });
|
|
51
69
|
}
|
|
52
70
|
});
|
|
53
71
|
|
|
@@ -114,7 +114,29 @@ describe("account-security dashboard on a real stack", () => {
|
|
|
114
114
|
"auth-mfa:screen:auth-mfa-disable",
|
|
115
115
|
"sessions:screen:my-sessions",
|
|
116
116
|
]);
|
|
117
|
-
|
|
117
|
+
const expectedReasons: ReadonlyArray<{ readonly qn: string; readonly reason: string }> = [
|
|
118
|
+
{
|
|
119
|
+
qn: "auth-mfa:screen:auth-mfa-enable",
|
|
120
|
+
reason:
|
|
121
|
+
"each signed-in user may enroll their own account in MFA, mirroring the enable-start handler",
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
qn: "auth-mfa:screen:auth-mfa-regenerate-recovery",
|
|
125
|
+
reason:
|
|
126
|
+
"each signed-in user may regenerate their own recovery codes, mirroring the regenerate-recovery handler",
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
qn: "auth-mfa:screen:auth-mfa-disable",
|
|
130
|
+
reason: "each signed-in user may turn off their own MFA, mirroring the disable handler",
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
qn: "sessions:screen:my-sessions",
|
|
134
|
+
reason:
|
|
135
|
+
"each signed-in user views and revokes only their own sessions, mirroring the mine/revoke handlers",
|
|
136
|
+
},
|
|
137
|
+
];
|
|
138
|
+
for (const { qn, reason } of expectedReasons)
|
|
139
|
+
expect(embeddedScreen(qn).access).toEqual({ openToAll: { reason } });
|
|
118
140
|
});
|
|
119
141
|
|
|
120
142
|
test("a plain User gets the flat visibleWhen record and their own sessions over HTTP", async () => {
|
|
@@ -7,7 +7,9 @@ import {
|
|
|
7
7
|
import { z } from "zod";
|
|
8
8
|
import { AgentDocGapKinds, findAgentDocGaps, formatAgentDocGap } from "../agent-doc-lint";
|
|
9
9
|
|
|
10
|
-
const OPEN_ACCESS = {
|
|
10
|
+
const OPEN_ACCESS = {
|
|
11
|
+
openToAll: { reason: "test handler callable by any signed-in test user" },
|
|
12
|
+
} as const;
|
|
11
13
|
|
|
12
14
|
async function noopWriteHandler() {
|
|
13
15
|
return { isSuccess: true as const, data: {} };
|
|
@@ -9,9 +9,15 @@ async function noopWriteHandler() {
|
|
|
9
9
|
|
|
10
10
|
function undocumentedHandlersFeature() {
|
|
11
11
|
return defineFeature("doc-gap-demo", (r) => {
|
|
12
|
-
r.writeHandler("do-a", z.object({}), noopWriteHandler, {
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
r.writeHandler("do-a", z.object({}), noopWriteHandler, {
|
|
13
|
+
access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
|
|
14
|
+
});
|
|
15
|
+
r.writeHandler("do-b", z.object({}), noopWriteHandler, {
|
|
16
|
+
access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
|
|
17
|
+
});
|
|
18
|
+
r.writeHandler("do-c", z.object({}), noopWriteHandler, {
|
|
19
|
+
access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
|
|
20
|
+
});
|
|
15
21
|
});
|
|
16
22
|
}
|
|
17
23
|
|
|
@@ -41,7 +47,7 @@ describe("createAgentToolsFeature", () => {
|
|
|
41
47
|
test("boot check stays silent for a fully-described registry", () => {
|
|
42
48
|
const describedFeature = defineFeature("doc-clean-demo", (r) => {
|
|
43
49
|
r.writeHandler("do-a", z.object({}), noopWriteHandler, {
|
|
44
|
-
access: { openToAll:
|
|
50
|
+
access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
|
|
45
51
|
description: "Does A.",
|
|
46
52
|
});
|
|
47
53
|
});
|
|
@@ -81,7 +81,14 @@ export function createInviteAcceptHandler() {
|
|
|
81
81
|
// openToAll: any authenticated user (Branch 1). Branch 2+3 (anon)
|
|
82
82
|
// nutzen `roles: ["all"]` weil dort GUEST_USER mit ["all"]-role
|
|
83
83
|
// dispatched wird.
|
|
84
|
-
access: {
|
|
84
|
+
access: {
|
|
85
|
+
openToAll: {
|
|
86
|
+
reason:
|
|
87
|
+
"any signed-in user may accept an invitation; the token identifies the pending " +
|
|
88
|
+
"invitation and its tenant, and the handler verifies the caller's own email matches " +
|
|
89
|
+
"the invitation before adding membership",
|
|
90
|
+
},
|
|
91
|
+
},
|
|
85
92
|
agent: { expose: false },
|
|
86
93
|
escapeHatch: {
|
|
87
94
|
reason: INVITE_ACCEPT_ESCAPE_HATCH_REASON,
|
|
@@ -30,7 +30,13 @@ export function createDisableHandler(opts: DisableOptions) {
|
|
|
30
30
|
return defineWriteHandler({
|
|
31
31
|
name: "disable",
|
|
32
32
|
schema: z.object({ code: z.string().min(6).max(9) }),
|
|
33
|
-
access: {
|
|
33
|
+
access: {
|
|
34
|
+
openToAll: {
|
|
35
|
+
reason:
|
|
36
|
+
"each signed-in user may disable only their own MFA enrollment after proving " +
|
|
37
|
+
"possession of a TOTP or recovery code; the handler looks up and removes the caller's own user-mfa row",
|
|
38
|
+
},
|
|
39
|
+
},
|
|
34
40
|
description:
|
|
35
41
|
"Removes the caller's TOTP enrollment for good after they prove possession with a TOTP or recovery code, and signs their other sessions and access tokens out; use it when a user wants two-factor authentication turned off.",
|
|
36
42
|
agent: { risk: "high" },
|
|
@@ -37,7 +37,13 @@ export function createEnableConfirmHandler(opts: EnableConfirmOptions) {
|
|
|
37
37
|
setupToken: z.string().min(1),
|
|
38
38
|
code: z.string().length(6),
|
|
39
39
|
}),
|
|
40
|
-
access: {
|
|
40
|
+
access: {
|
|
41
|
+
openToAll: {
|
|
42
|
+
reason:
|
|
43
|
+
"each signed-in user may complete their own MFA enrollment; the setup token is bound " +
|
|
44
|
+
"to the caller's id and any token minted for another user is rejected",
|
|
45
|
+
},
|
|
46
|
+
},
|
|
41
47
|
description:
|
|
42
48
|
"Completes TOTP enrollment for the signed-in user by checking a code against the secret carried in the setup token from enable-start, then storing the factor and signing every other session and access token out.",
|
|
43
49
|
// Changes the caller's authentication state and signs their other sessions out.
|
|
@@ -29,7 +29,13 @@ export function createEnableStartHandler(opts: EnableStartOptions) {
|
|
|
29
29
|
// input fields) — derived server-side from the caller's own email below.
|
|
30
30
|
accountLabel: z.string().min(1).max(200).optional(),
|
|
31
31
|
}),
|
|
32
|
-
access: {
|
|
32
|
+
access: {
|
|
33
|
+
openToAll: {
|
|
34
|
+
reason:
|
|
35
|
+
"each signed-in user may begin MFA enrollment for their own account only; nothing is " +
|
|
36
|
+
"persisted until enable-confirm and the label defaults from the caller's own session",
|
|
37
|
+
},
|
|
38
|
+
},
|
|
33
39
|
description:
|
|
34
40
|
"Begins TOTP enrollment for the signed-in user by generating a secret plus recovery codes and returning them as a short-lived setup token, an otpauth:// URI, the base32 secret for manual entry and the one-time plaintext recovery codes; nothing is persisted until enable-confirm. accountLabel defaults to the caller's own email.",
|
|
35
41
|
// The result carries the TOTP secret, the otpauth:// URI and the plaintext
|
|
@@ -26,7 +26,13 @@ export function createRegenerateRecoveryHandler(opts: RegenerateRecoveryOptions)
|
|
|
26
26
|
return defineWriteHandler({
|
|
27
27
|
name: "regenerate-recovery",
|
|
28
28
|
schema: z.object({ code: z.string().min(6).max(9) }),
|
|
29
|
-
access: {
|
|
29
|
+
access: {
|
|
30
|
+
openToAll: {
|
|
31
|
+
reason:
|
|
32
|
+
"each signed-in user may regenerate only their own recovery codes after proving " +
|
|
33
|
+
"possession of a TOTP or recovery code; the handler looks up the caller's own user-mfa row",
|
|
34
|
+
},
|
|
35
|
+
},
|
|
30
36
|
description:
|
|
31
37
|
"Destroys every existing recovery code of the caller, including unused ones, and returns a fresh set once in plaintext; use it when recovery codes may have leaked but TOTP itself should stay enrolled.",
|
|
32
38
|
// Returns a fresh set of plaintext recovery codes — same transcript leak as
|
|
@@ -13,7 +13,13 @@ import { userMfaTable } from "../schema/user-mfa";
|
|
|
13
13
|
export const mfaStatusQuery = defineQueryHandler({
|
|
14
14
|
name: "user-mfa:status",
|
|
15
15
|
schema: z.object({}),
|
|
16
|
-
access: {
|
|
16
|
+
access: {
|
|
17
|
+
openToAll: {
|
|
18
|
+
reason:
|
|
19
|
+
"each signed-in user reads only their own MFA status; the query filters " +
|
|
20
|
+
"userMfaTable by the caller's own id",
|
|
21
|
+
},
|
|
22
|
+
},
|
|
17
23
|
description:
|
|
18
24
|
"Reports whether the calling user has TOTP two-factor authentication enrolled; use it before offering either the enrollment flow or the disable and recovery-code actions.",
|
|
19
25
|
handler: async (query, ctx) => {
|
package/src/auth-mfa/screens.ts
CHANGED
|
@@ -10,6 +10,13 @@ import {
|
|
|
10
10
|
MFA_REGENERATE_RECOVERY_SCREEN_ID,
|
|
11
11
|
} from "./constants";
|
|
12
12
|
|
|
13
|
+
const MFA_ENABLE_OPEN_REASON =
|
|
14
|
+
"each signed-in user may enroll their own account in MFA, mirroring the enable-start handler";
|
|
15
|
+
const MFA_DISABLE_OPEN_REASON =
|
|
16
|
+
"each signed-in user may turn off their own MFA, mirroring the disable handler";
|
|
17
|
+
const MFA_REGENERATE_RECOVERY_OPEN_REASON =
|
|
18
|
+
"each signed-in user may regenerate their own recovery codes, mirroring the regenerate-recovery handler";
|
|
19
|
+
|
|
13
20
|
// Declarative TOTP-enrollment screen: mint (no input) -> reveal QR/secret/
|
|
14
21
|
// recovery-codes -> confirm with a 6-digit code. `setupToken` is carried
|
|
15
22
|
// from the mint payload into the confirm payload but never listed in
|
|
@@ -21,7 +28,11 @@ export const mfaEnableScreen: SecretMintScreenDefinition = {
|
|
|
21
28
|
fields: {},
|
|
22
29
|
layout: { sections: [] },
|
|
23
30
|
submitLabel: i18nKey("mfa.enable.start"),
|
|
24
|
-
access: {
|
|
31
|
+
access: {
|
|
32
|
+
openToAll: {
|
|
33
|
+
reason: MFA_ENABLE_OPEN_REASON,
|
|
34
|
+
},
|
|
35
|
+
},
|
|
25
36
|
description:
|
|
26
37
|
"Self-service screen where a signed-in user enrolls in TOTP two-factor authentication: it shows the QR code and manual secret, reveals the recovery codes once, and confirms enrollment with a code from their authenticator app.",
|
|
27
38
|
reveal: {
|
|
@@ -73,7 +84,11 @@ export const mfaDisableScreen: ActionFormScreenDefinition = {
|
|
|
73
84
|
submitLabel: i18nKey("mfa.disable.submit"),
|
|
74
85
|
submitStyle: "danger",
|
|
75
86
|
cancelTarget: false,
|
|
76
|
-
access: {
|
|
87
|
+
access: {
|
|
88
|
+
openToAll: {
|
|
89
|
+
reason: MFA_DISABLE_OPEN_REASON,
|
|
90
|
+
},
|
|
91
|
+
},
|
|
77
92
|
description:
|
|
78
93
|
"Self-service screen where a signed-in user turns two-factor authentication off by entering a code from their authenticator app or a recovery code; their other sessions and access tokens are signed out.",
|
|
79
94
|
};
|
|
@@ -86,7 +101,11 @@ export const mfaRegenerateRecoveryScreen: SecretMintScreenDefinition = {
|
|
|
86
101
|
layout: { sections: [{ fields: ["code"] }] },
|
|
87
102
|
submitLabel: i18nKey("mfa.regenerate.submit"),
|
|
88
103
|
cancelTarget: false,
|
|
89
|
-
access: {
|
|
104
|
+
access: {
|
|
105
|
+
openToAll: {
|
|
106
|
+
reason: MFA_REGENERATE_RECOVERY_OPEN_REASON,
|
|
107
|
+
},
|
|
108
|
+
},
|
|
90
109
|
description:
|
|
91
110
|
"Self-service screen where a signed-in user with two-factor authentication replaces all recovery codes after confirming with a current code; the new codes are shown once.",
|
|
92
111
|
reveal: {
|
|
@@ -9,7 +9,13 @@ export const inboxQuery = defineQueryHandler({
|
|
|
9
9
|
limit: z.number().min(1).max(100).default(50),
|
|
10
10
|
unreadOnly: z.boolean().default(false),
|
|
11
11
|
}),
|
|
12
|
-
access: {
|
|
12
|
+
access: {
|
|
13
|
+
openToAll: {
|
|
14
|
+
reason:
|
|
15
|
+
"each signed-in user reads only their own in-app inbox; the query filters " +
|
|
16
|
+
"inAppMessagesTable by the caller's own id",
|
|
17
|
+
},
|
|
18
|
+
},
|
|
13
19
|
description:
|
|
14
20
|
"Lists the calling user's own in-app notification messages newest-first, optionally only the unread ones; use it to show or inspect a user's notification inbox.",
|
|
15
21
|
handler: async (query, ctx) => {
|
|
@@ -6,7 +6,13 @@ import { inAppMessagesTable } from "../tables";
|
|
|
6
6
|
export const markAllReadWrite = defineWriteHandler({
|
|
7
7
|
name: "markAllRead",
|
|
8
8
|
schema: z.object({}),
|
|
9
|
-
access: {
|
|
9
|
+
access: {
|
|
10
|
+
openToAll: {
|
|
11
|
+
reason:
|
|
12
|
+
"each signed-in user marks only their own unread in-app messages as read; the " +
|
|
13
|
+
"update is scoped to the caller's own userId",
|
|
14
|
+
},
|
|
15
|
+
},
|
|
10
16
|
description:
|
|
11
17
|
"Marks every unread in-app message of the calling user as read and returns how many rows were touched; use it for a mark-all-as-read action.",
|
|
12
18
|
handler: async (event, ctx) => {
|
|
@@ -10,7 +10,13 @@ export const markReadWrite = defineWriteHandler({
|
|
|
10
10
|
// inAppMessages.id is a serial integer (table is infra, not an ES aggregate).
|
|
11
11
|
id: z.number().int(),
|
|
12
12
|
}),
|
|
13
|
-
access: {
|
|
13
|
+
access: {
|
|
14
|
+
openToAll: {
|
|
15
|
+
reason:
|
|
16
|
+
"each signed-in user marks only their own in-app message as read; the update " +
|
|
17
|
+
"requires id + the caller's own userId to match, otherwise it fails not-found",
|
|
18
|
+
},
|
|
19
|
+
},
|
|
14
20
|
description:
|
|
15
21
|
"Marks one in-app message of the calling user as read by its numeric id, failing with not-found when no such message belongs to the caller; use it when a user opens a single notification.",
|
|
16
22
|
handler: async (event, ctx) => {
|
|
@@ -6,7 +6,13 @@ import { inAppMessagesTable } from "../tables";
|
|
|
6
6
|
export const unreadCountQuery = defineQueryHandler({
|
|
7
7
|
name: "unreadCount",
|
|
8
8
|
schema: z.object({}),
|
|
9
|
-
access: {
|
|
9
|
+
access: {
|
|
10
|
+
openToAll: {
|
|
11
|
+
reason:
|
|
12
|
+
"each signed-in user counts only their own unread in-app messages; the query " +
|
|
13
|
+
"filters inAppMessagesTable by the caller's own id",
|
|
14
|
+
},
|
|
15
|
+
},
|
|
10
16
|
description:
|
|
11
17
|
"Counts the calling user's unread in-app notification messages; use it for an inbox badge or to check whether a user has pending notifications.",
|
|
12
18
|
handler: async (query, ctx) => {
|
|
@@ -19,7 +19,13 @@ import { tenantComplianceProfileTable } from "../schema/profile-selection";
|
|
|
19
19
|
export const forTenantQuery = defineQueryHandler({
|
|
20
20
|
name: "for-tenant",
|
|
21
21
|
schema: z.object({}),
|
|
22
|
-
access: {
|
|
22
|
+
access: {
|
|
23
|
+
openToAll: {
|
|
24
|
+
reason:
|
|
25
|
+
"any signed-in tenant member may read their own tenant's effective compliance " +
|
|
26
|
+
"profile; the result carries policy metadata, never PII",
|
|
27
|
+
},
|
|
28
|
+
},
|
|
23
29
|
description:
|
|
24
30
|
"Returns the effective compliance profile for the caller's tenant with any tenant override merged in, falling back to minimal-no-region plus a no-profile-selected warning when the tenant has not picked one yet.",
|
|
25
31
|
handler: async (query, ctx): Promise<EffectiveComplianceProfile> => {
|
|
@@ -16,7 +16,13 @@ import { z } from "zod";
|
|
|
16
16
|
export const listProfilesQuery = defineQueryHandler({
|
|
17
17
|
name: "list-profiles",
|
|
18
18
|
schema: z.object({}),
|
|
19
|
-
access: {
|
|
19
|
+
access: {
|
|
20
|
+
openToAll: {
|
|
21
|
+
reason:
|
|
22
|
+
"any signed-in user may list the selectable compliance profiles; the data is " +
|
|
23
|
+
"static, deployment-wide reference information, not tenant- or user-specific",
|
|
24
|
+
},
|
|
25
|
+
},
|
|
20
26
|
description:
|
|
21
27
|
"Lists the compliance profiles a tenant can choose from (key, region, label, supervisory-authority contact, notification languages), for rendering the onboarding profile picker; the minimal-no-region fallback is excluded because it is not selectable.",
|
|
22
28
|
handler: async (): Promise<{ profiles: readonly ComplianceProfileSummary[] }> => {
|
|
@@ -173,7 +173,7 @@ const probeFeature = defineFeature("probe", (r) => {
|
|
|
173
173
|
data: { orders: probe.orders, push: probe.push, betaEnabled: probe.betaEnabled },
|
|
174
174
|
};
|
|
175
175
|
},
|
|
176
|
-
{ access: { openToAll:
|
|
176
|
+
{ access: { openToAll: { reason: "test handler callable by any signed-in test user" } } },
|
|
177
177
|
);
|
|
178
178
|
});
|
|
179
179
|
|
|
@@ -38,7 +38,7 @@ const probeFeature = defineFeature("probe", (r) => {
|
|
|
38
38
|
isSuccess: true,
|
|
39
39
|
data: { tenant: ctx.tz.tenant, user: ctx.tz.user },
|
|
40
40
|
}),
|
|
41
|
-
{ access: { openToAll:
|
|
41
|
+
{ access: { openToAll: { reason: "test handler callable by any signed-in test user" } } },
|
|
42
42
|
);
|
|
43
43
|
});
|
|
44
44
|
|
|
@@ -16,7 +16,13 @@ export const cascadeQuery = defineQueryHandler({
|
|
|
16
16
|
schema: z.object({
|
|
17
17
|
keys: z.array(z.string()).optional(),
|
|
18
18
|
}),
|
|
19
|
-
access: {
|
|
19
|
+
access: {
|
|
20
|
+
openToAll: {
|
|
21
|
+
reason:
|
|
22
|
+
"any signed-in user may call cascade; hasConfigAccess filters the result to " +
|
|
23
|
+
"only the config keys the caller's roles may read, before any value is returned",
|
|
24
|
+
},
|
|
25
|
+
},
|
|
20
26
|
handler: async (query, ctx) => {
|
|
21
27
|
const db = requireSystemDb(ctx, "config:query:cascade", query.user.tenantId);
|
|
22
28
|
const registry = ctx.registry;
|
|
@@ -131,7 +131,13 @@ export const readinessQuery = defineQueryHandler({
|
|
|
131
131
|
"Lists the required config keys that are still unset for the caller's tenant, each with its scope and type; use it to find out what is missing before a feature can run.",
|
|
132
132
|
schema: z.object({}),
|
|
133
133
|
// Per-key read access enforced via hasConfigAccess inside the handler.
|
|
134
|
-
access: {
|
|
134
|
+
access: {
|
|
135
|
+
openToAll: {
|
|
136
|
+
reason:
|
|
137
|
+
"any signed-in user may call readiness; collectMissingRequiredConfig filters " +
|
|
138
|
+
"candidate keys through hasConfigAccess so only keys the caller's roles may read are reported",
|
|
139
|
+
},
|
|
140
|
+
},
|
|
135
141
|
handler: async (query, ctx) => {
|
|
136
142
|
const db = requireSystemDb(ctx, "config:query:readiness", query.user.tenantId);
|
|
137
143
|
const missing = await collectMissingRequiredConfig(
|
|
@@ -26,7 +26,13 @@ export const resetWrite = defineWriteHandler({
|
|
|
26
26
|
scope: scopeEnum.optional(),
|
|
27
27
|
}),
|
|
28
28
|
// Per-key access enforcement lives inside the handler via checkWriteAccess.
|
|
29
|
-
access: {
|
|
29
|
+
access: {
|
|
30
|
+
openToAll: {
|
|
31
|
+
reason:
|
|
32
|
+
"any signed-in user may call reset; prepareConfigWrite enforces the per-key " +
|
|
33
|
+
"write access rule before any value is touched",
|
|
34
|
+
},
|
|
35
|
+
},
|
|
30
36
|
handler: async (event, ctx) => {
|
|
31
37
|
const db = requireSystemDb(ctx, "config:write:reset", event.user.tenantId);
|
|
32
38
|
|
|
@@ -8,7 +8,13 @@ export const schemaQuery = defineQueryHandler({
|
|
|
8
8
|
"Returns the definitions of all config keys the caller may read (scope, type, default, bounds, required flag) without any values; use it to discover which settings exist and how they may be set.",
|
|
9
9
|
schema: z.object({}),
|
|
10
10
|
// Per-key read access enforced via hasConfigAccess inside the handler.
|
|
11
|
-
access: {
|
|
11
|
+
access: {
|
|
12
|
+
openToAll: {
|
|
13
|
+
reason:
|
|
14
|
+
"any signed-in user may call schema; hasConfigAccess filters the result to " +
|
|
15
|
+
"only the config key definitions the caller's roles may read",
|
|
16
|
+
},
|
|
17
|
+
},
|
|
12
18
|
handler: async (query, ctx) => {
|
|
13
19
|
const registry = ctx.registry;
|
|
14
20
|
const allKeys = registry.getAllConfigKeys();
|
|
@@ -44,7 +44,13 @@ export const setWrite = defineWriteHandler({
|
|
|
44
44
|
scope: scopeEnum.optional(),
|
|
45
45
|
}),
|
|
46
46
|
// Per-key access enforcement lives inside the handler via checkWriteAccess.
|
|
47
|
-
access: {
|
|
47
|
+
access: {
|
|
48
|
+
openToAll: {
|
|
49
|
+
reason:
|
|
50
|
+
"any signed-in user may call set; prepareConfigWrite enforces the per-key " +
|
|
51
|
+
"write access rule before any value is stored",
|
|
52
|
+
},
|
|
53
|
+
},
|
|
48
54
|
handler: async (event, ctx) => {
|
|
49
55
|
const db = requireSystemDb(ctx, "config:write:set", event.user.tenantId);
|
|
50
56
|
|
|
@@ -16,7 +16,13 @@ export const valuesQuery = defineQueryHandler({
|
|
|
16
16
|
"Returns the effective value, scope and winning source for every config key the caller may read, with encrypted and secret-backed values masked; use it to inspect the current settings.",
|
|
17
17
|
schema: z.object({}),
|
|
18
18
|
// Per-key read access enforced via hasConfigAccess inside the handler.
|
|
19
|
-
access: {
|
|
19
|
+
access: {
|
|
20
|
+
openToAll: {
|
|
21
|
+
reason:
|
|
22
|
+
"any signed-in user may call values; hasConfigAccess filters the result to " +
|
|
23
|
+
"only the config keys the caller's roles may read, before any value is returned",
|
|
24
|
+
},
|
|
25
|
+
},
|
|
20
26
|
handler: async (query, ctx) => {
|
|
21
27
|
const db = requireSystemDb(ctx, "config:query:values", query.user.tenantId);
|
|
22
28
|
const registry = ctx.registry;
|
|
@@ -22,7 +22,14 @@ export const policyForQuery = defineQueryHandler({
|
|
|
22
22
|
schema: z.object({
|
|
23
23
|
entityName: z.string().min(1).max(100),
|
|
24
24
|
}),
|
|
25
|
-
access: {
|
|
25
|
+
access: {
|
|
26
|
+
openToAll: {
|
|
27
|
+
reason:
|
|
28
|
+
"any signed-in tenant member (and other features calling cross-feature) may " +
|
|
29
|
+
"resolve the retention policy for an entity name in their own tenant; the result " +
|
|
30
|
+
"carries only policy metadata, no PII",
|
|
31
|
+
},
|
|
32
|
+
},
|
|
26
33
|
description:
|
|
27
34
|
"Resolves the effective retention policy for one entity name in the caller's tenant (keep-for duration plus delete or anonymize strategy) by layering the entity default, tenant preset and tenant override, so a forget flow or cleanup job knows how that data may be removed.",
|
|
28
35
|
handler: async (query, ctx): Promise<EffectiveRetentionPolicy> => {
|
|
@@ -65,7 +65,7 @@ const appFeature = defineFeature("app", (r) => {
|
|
|
65
65
|
});
|
|
66
66
|
return { isSuccess: true, data: { sent: true } };
|
|
67
67
|
},
|
|
68
|
-
access: { openToAll:
|
|
68
|
+
access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
|
|
69
69
|
}),
|
|
70
70
|
);
|
|
71
71
|
|
|
@@ -82,7 +82,7 @@ const appFeature = defineFeature("app", (r) => {
|
|
|
82
82
|
});
|
|
83
83
|
return { isSuccess: true, data: { sent: true } };
|
|
84
84
|
},
|
|
85
|
-
access: { openToAll:
|
|
85
|
+
access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
|
|
86
86
|
}),
|
|
87
87
|
);
|
|
88
88
|
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
|
-
import { access, validateBoot } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
|
+
import { access, isOpenToAllGranted, validateBoot } from "@cosmicdrift/kumiko-framework/engine";
|
|
3
3
|
import { rolesOf } from "@cosmicdrift/kumiko-framework/testing";
|
|
4
4
|
import { DELIVERY_LOG_SCREEN_ID } from "../constants";
|
|
5
5
|
import { createDeliveryFeature } from "../feature";
|
|
@@ -33,9 +33,7 @@ describe("delivery screens + handler access alignment", () => {
|
|
|
33
33
|
test("access option leaves the preferences handler openToAll — that's per-user, not an admin surface", () => {
|
|
34
34
|
const delivery = createDeliveryFeature({ access: access.systemAdmin });
|
|
35
35
|
const preferencesAccess = delivery.queryHandlers["preferences"]?.access;
|
|
36
|
-
expect(
|
|
37
|
-
preferencesAccess && "openToAll" in preferencesAccess && preferencesAccess.openToAll,
|
|
38
|
-
).toBe(true);
|
|
36
|
+
expect(preferencesAccess !== undefined && isOpenToAllGranted(preferencesAccess)).toBe(true);
|
|
39
37
|
});
|
|
40
38
|
|
|
41
39
|
test("boot-validates with a narrowed access option", () => {
|
|
@@ -101,7 +101,7 @@ const appFeature = defineFeature("app", (r) => {
|
|
|
101
101
|
|
|
102
102
|
return { isSuccess: true, data: { assigned: true } };
|
|
103
103
|
},
|
|
104
|
-
access: { openToAll:
|
|
104
|
+
access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
|
|
105
105
|
}),
|
|
106
106
|
);
|
|
107
107
|
|
|
@@ -124,7 +124,7 @@ const appFeature = defineFeature("app", (r) => {
|
|
|
124
124
|
|
|
125
125
|
return { isSuccess: true, data: { sent: true } };
|
|
126
126
|
},
|
|
127
|
-
access: { openToAll:
|
|
127
|
+
access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
|
|
128
128
|
}),
|
|
129
129
|
);
|
|
130
130
|
|
|
@@ -141,7 +141,7 @@ const appFeature = defineFeature("app", (r) => {
|
|
|
141
141
|
title: z.string(),
|
|
142
142
|
body: z.string(),
|
|
143
143
|
}),
|
|
144
|
-
access: { openToAll:
|
|
144
|
+
access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
|
|
145
145
|
handler: async (event, ctx) => {
|
|
146
146
|
const notify = ctx.notify as NotifyFn;
|
|
147
147
|
await notify(event.payload.notificationType, {
|
|
@@ -173,7 +173,7 @@ const appFeature = defineFeature("app", (r) => {
|
|
|
173
173
|
|
|
174
174
|
return { isSuccess: true, data: { sent: true } };
|
|
175
175
|
},
|
|
176
|
-
access: { openToAll:
|
|
176
|
+
access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
|
|
177
177
|
}),
|
|
178
178
|
);
|
|
179
179
|
});
|
|
@@ -206,7 +206,7 @@ const ticketFeature = defineFeature("tickets", (r) => {
|
|
|
206
206
|
status: z.string(),
|
|
207
207
|
}),
|
|
208
208
|
async (event, ctx) => ticketExecutor().create(event.payload, event.user, ctx.db),
|
|
209
|
-
{ access: { openToAll:
|
|
209
|
+
{ access: { openToAll: { reason: "test handler callable by any signed-in test user" } } },
|
|
210
210
|
);
|
|
211
211
|
|
|
212
212
|
// Declarative: notify assignee when ticket is created with assigneeId
|
|
@@ -7,7 +7,13 @@ import { notificationPreferencesTable } from "../tables";
|
|
|
7
7
|
export const preferencesQuery = defineQueryHandler({
|
|
8
8
|
name: "preferences",
|
|
9
9
|
schema: z.object({}),
|
|
10
|
-
access: {
|
|
10
|
+
access: {
|
|
11
|
+
openToAll: {
|
|
12
|
+
reason:
|
|
13
|
+
"each signed-in user reads only their own notification preference rows; the " +
|
|
14
|
+
"query filters notificationPreferencesTable by the caller's own tenantId and userId",
|
|
15
|
+
},
|
|
16
|
+
},
|
|
11
17
|
description:
|
|
12
18
|
"Returns the calling user's notification preference rows for their own tenant; use it to show which notification types and channels that user has enabled or muted.",
|
|
13
19
|
handler: async (query, ctx) => {
|
|
@@ -11,7 +11,13 @@ export const setPreferenceWrite = defineWriteHandler({
|
|
|
11
11
|
enabled: z.boolean(),
|
|
12
12
|
}),
|
|
13
13
|
// Every user manages their own preferences; tenant+user scoping is on the WHERE.
|
|
14
|
-
access: {
|
|
14
|
+
access: {
|
|
15
|
+
openToAll: {
|
|
16
|
+
reason:
|
|
17
|
+
"each signed-in user changes only their own notification preferences; the " +
|
|
18
|
+
"write is scoped to the caller's own tenantId and userId via assertTenantMatch",
|
|
19
|
+
},
|
|
20
|
+
},
|
|
15
21
|
description:
|
|
16
22
|
"Enables or disables one notification type and channel combination for the calling user, where either side may be the wildcard * as a catch-all; use it when a user changes their own notification settings.",
|
|
17
23
|
handler: async (event, ctx) => {
|