@cosmicdrift/kumiko-bundled-features 0.334.0 → 0.336.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/dist/admin-shell/overview-panels.js +2 -2
- package/dist/agent-tools/agent-doc-lint.d.ts +1 -0
- package/dist/agent-tools/agent-doc-lint.js +33 -3
- package/dist/agent-tools/agent-manifest.js +47 -6
- package/dist/agent-tools/filter-ops.d.ts +2 -0
- package/dist/agent-tools/filter-ops.js +4 -0
- package/dist/agent-tools/tool-catalog.js +1 -4
- package/dist/agent-tools/tool-dispatch.js +1 -4
- package/dist/audit/constants.d.ts +1 -1
- package/dist/audit/constants.js +1 -1
- package/dist/auth-email-password/handlers/change-password.write.js +2 -0
- package/dist/auth-email-password/handlers/invite-create.write.js +12 -2
- package/dist/auth-email-password/handlers/login.write.js +2 -0
- package/dist/auth-email-password/handlers/self-registration-status.query.js +1 -1
- package/dist/auth-email-password/i18n.js +2 -0
- package/dist/auth-email-password/web/auth-client.d.ts +1 -0
- package/dist/auth-email-password/web/auth-client.js +1 -1
- package/dist/auth-email-password/web/auth-gate.js +6 -8
- package/dist/auth-email-password/web/index.d.ts +1 -1
- package/dist/auth-email-password/web/index.js +1 -1
- package/dist/auth-email-password/web/invite-accept-screen.js +16 -12
- package/dist/auth-email-password/web/login-screen.js +1 -1
- package/dist/auth-email-password/web/session-bootstrap-error.d.ts +5 -1
- package/dist/auth-email-password/web/session-bootstrap-error.js +2 -2
- package/dist/auth-email-password/web/session.d.ts +4 -0
- package/dist/auth-email-password/web/session.js +15 -0
- package/dist/auth-mfa/feature.js +3 -2
- package/dist/auth-mfa/handlers/enable-confirm-preauth.write.js +2 -2
- package/dist/auth-mfa/handlers/status.query.js +1 -0
- package/dist/auth-mfa/handlers/verify.write.js +2 -0
- package/dist/auth-mfa/hmac-token-codec.js +3 -0
- package/dist/auth-mfa/mfa-challenge-token.js +4 -0
- package/dist/auth-mfa/mfa-preauth-setup-token.js +3 -0
- package/dist/auth-mfa/web/mfa-client.d.ts +1 -1
- package/dist/auth-mfa/web/mfa-client.js +4 -10
- package/dist/auth-mfa-user-data/hooks.js +4 -2
- package/dist/billing-foundation/billing-info-query.js +1 -1
- package/dist/billing-foundation/checkout-core.d.ts +5 -5
- package/dist/billing-foundation/checkout-core.js +10 -6
- package/dist/billing-foundation/handlers/switch-plan.write.js +5 -8
- package/dist/cap-counter/book-cap-usage.js +20 -5
- package/dist/cap-counter/enforce-cap.js +9 -3
- package/dist/cap-counter/feature.js +3 -2
- package/dist/cap-counter/i18n.d.ts +1 -0
- package/dist/cap-counter/i18n.js +6 -5
- package/dist/cap-overview/handlers/caps-usage.query.js +9 -16
- package/dist/cap-overview/handlers/tenant-caps-list.query.js +41 -15
- package/dist/cap-overview/i18n.js +5 -0
- package/dist/cap-overview/web/cap-cards-panel.js +4 -1
- package/dist/cap-overview/web/cap-usage-bar.js +1 -1
- package/dist/channel-email/email-channel.js +12 -1
- package/dist/channel-in-app/tables.js +2 -6
- package/dist/compliance-profiles/handlers/sub-processors.query.js +1 -1
- package/dist/config/db/queries/resolver.js +3 -1
- package/dist/config/handlers/set.write.js +8 -4
- package/dist/config/web/i18n.js +1 -0
- package/dist/config/write-helpers.d.ts +1 -1
- package/dist/config/write-helpers.js +11 -5
- package/dist/crypto-shredding/handlers/forget-subject.write.js +15 -8
- package/dist/custom-fields/constants.js +1 -4
- package/dist/data-retention/resolve-for-tenant.d.ts +2 -3
- package/dist/data-retention/run-retention-cleanup.js +100 -44
- package/dist/delivery/handlers/log.query.js +33 -24
- package/dist/delivery/types.d.ts +2 -1
- package/dist/delivery/types.js +7 -0
- package/dist/delivery/unsubscribe.js +5 -11
- package/dist/derivatives-sharp/render.js +27 -9
- package/dist/document-ingest-foundation/feature.js +5 -5
- package/dist/document-ingest-foundation/providers.d.ts +1 -0
- package/dist/document-ingest-foundation/providers.js +6 -0
- package/dist/document-ingest-foundation/write-document-extract.d.ts +2 -2
- package/dist/document-ingest-foundation/write-document-extract.js +10 -0
- package/dist/feature-toggles/global-feature-state-table.js +2 -7
- package/dist/feature-toggles/toggle-runtime.d.ts +1 -0
- package/dist/feature-toggles/toggle-runtime.js +16 -2
- package/dist/feature-toggles/toggle-sync-signal.d.ts +2 -1
- package/dist/feature-toggles/toggle-sync-signal.js +3 -2
- package/dist/files-tenant-data/index.js +3 -2
- package/dist/folders/handlers/clear-folder.write.js +4 -11
- package/dist/folders/handlers/set-folder.write.js +4 -10
- package/dist/form-draft/feature.js +3 -2
- package/dist/form-draft/handlers/discard.write.js +7 -8
- package/dist/form-draft/handlers/get.query.js +7 -1
- package/dist/foundation-shared/host-allowlist.d.ts +4 -0
- package/dist/foundation-shared/host-allowlist.js +14 -0
- package/dist/foundation-shared/index.d.ts +1 -0
- package/dist/foundation-shared/index.js +1 -0
- package/dist/foundation-shared/mail-host-policy.d.ts +0 -2
- package/dist/foundation-shared/mail-host-policy.js +3 -13
- package/dist/inbound-mail-foundation/connect-routes.js +7 -5
- package/dist/inbound-mail-foundation/watch-supervisor.js +47 -43
- package/dist/jobs/db/queries/stale-run-sweep.js +22 -15
- package/dist/jobs/handlers/list.query.d.ts +7 -2
- package/dist/jobs/handlers/list.query.js +9 -2
- package/dist/jobs/handlers/retry.write.js +3 -2
- package/dist/jobs/handlers/tenant-failures.query.js +3 -9
- package/dist/jobs/job-run-logger.js +18 -2
- package/dist/jobs/tenant-job-failure-table.js +2 -6
- package/dist/ledger/handlers/confirm-schedule-period.write.js +2 -2
- package/dist/ledger/handlers/reverse-transaction.write.js +2 -2
- package/dist/ledger/subject-reference.d.ts +4 -0
- package/dist/ledger/subject-reference.js +7 -0
- package/dist/legal-pages/feature.js +8 -1
- package/dist/managed-pages/handlers/branding.query.js +1 -1
- package/dist/managed-pages/handlers/by-slug.query.js +1 -1
- package/dist/managed-pages/handlers/by-tenant-published.query.js +1 -1
- package/dist/metrics/compute.js +7 -1
- package/dist/metrics/window.js +5 -0
- package/dist/notes-history/handlers/add-note.write.js +4 -9
- package/dist/notes-history-user-data/hooks.js +28 -14
- package/dist/personal-access-tokens/constants.d.ts +1 -0
- package/dist/personal-access-tokens/constants.js +1 -0
- package/dist/personal-access-tokens/feature.js +2 -2
- package/dist/personal-access-tokens/handlers/create.write.js +2 -2
- package/dist/personal-access-tokens/handlers/revoke.write.js +2 -2
- package/dist/personal-access-tokens/i18n.js +12 -13
- package/dist/personal-access-tokens/revoke-for-user.js +12 -1
- package/dist/personal-access-tokens/schema/api-token.js +2 -1
- package/dist/personal-access-tokens/screens.js +5 -1
- package/dist/rate-limiting/feature.js +2 -0
- package/dist/rate-limiting/handlers/bucket-access.js +15 -7
- package/dist/rate-limiting/i18n.d.ts +10 -0
- package/dist/rate-limiting/i18n.js +10 -0
- package/dist/secrets/feature.js +3 -2
- package/dist/seo/handlers/seo-config.query.js +1 -1
- package/dist/sessions/constants.d.ts +1 -0
- package/dist/sessions/constants.js +1 -0
- package/dist/sessions/feature.js +5 -3
- package/dist/sessions/handlers/revoke-all-for-user.write.d.ts +1 -0
- package/dist/sessions/handlers/revoke-all-for-user.write.js +12 -2
- package/dist/sessions/handlers/revoke.write.js +2 -2
- package/dist/sessions/schema/user-session.js +2 -1
- package/dist/sessions/screens.js +5 -1
- package/dist/sessions/session-callbacks.js +88 -77
- package/dist/shared/assert-erased.d.ts +2 -1
- package/dist/shared/assert-erased.js +12 -4
- package/dist/shared/index.d.ts +2 -2
- package/dist/shared/index.js +2 -2
- package/dist/shared/lockout-counter.js +1 -1
- package/dist/shared/parent-visibility.d.ts +5 -0
- package/dist/shared/parent-visibility.js +17 -1
- package/dist/shared/row-bound-grant.js +20 -9
- package/dist/shared/single-use-token-store.js +5 -13
- package/dist/step-dispatcher/webhook-runner.d.ts +0 -2
- package/dist/step-dispatcher/webhook-runner.js +18 -17
- package/dist/subscription-mollie/plugin-methods.d.ts +3 -2
- package/dist/subscription-mollie/plugin-methods.js +7 -1
- package/dist/subscription-mollie/verify-webhook.js +6 -0
- package/dist/subscription-stripe/plugin-methods.js +25 -6
- package/dist/subscription-stripe/verify-webhook.js +13 -6
- package/dist/tags/handlers/assign-tag.write.js +4 -10
- package/dist/tags/handlers/remove-tag.write.js +4 -11
- package/dist/template-resolver/api.d.ts +2 -1
- package/dist/template-resolver/api.js +1 -1
- package/dist/template-resolver/handlers/by-slug.query.d.ts +1 -1
- package/dist/template-resolver/handlers/by-slug.query.js +2 -2
- package/dist/template-resolver/handlers/by-tenant.query.d.ts +3 -2
- package/dist/template-resolver/handlers/by-tenant.query.js +2 -2
- package/dist/template-resolver/handlers/collection-item.query.d.ts +1 -1
- package/dist/template-resolver/handlers/collection-list.query.d.ts +1 -1
- package/dist/template-resolver/handlers/collection-shared.d.ts +3 -2
- package/dist/template-resolver/handlers/collection-shared.js +1 -1
- package/dist/template-resolver/handlers/find-by-id.query.d.ts +1 -1
- package/dist/template-resolver/handlers/find-by-id.query.js +1 -1
- package/dist/template-resolver/handlers/list.query.d.ts +1 -1
- package/dist/template-resolver/handlers/list.query.js +1 -1
- package/dist/template-resolver/seeding.js +41 -39
- package/dist/template-resolver/table.d.ts +8 -5
- package/dist/template-resolver/testing.js +2 -2
- package/dist/template-resolver/user-content-table.d.ts +2 -5
- package/dist/template-resolver/web/client-plugin.d.ts +1 -1
- package/dist/template-resolver-user-data/hooks.js +1 -1
- package/dist/tenant/cancel-pending-invitation.d.ts +8 -0
- package/dist/tenant/cancel-pending-invitation.js +22 -0
- package/dist/tenant/feature.js +4 -2
- package/dist/tenant/handlers/cancel-invitation.write.js +6 -21
- package/dist/tenant/handlers/member-directory.query.d.ts +1 -0
- package/dist/tenant/handlers/member-directory.query.js +17 -4
- package/dist/tenant/handlers/remove-member.write.js +9 -14
- package/dist/tenant/handlers/team-list.query.js +2 -2
- package/dist/tenant/handlers/tenant-directory.query.d.ts +1 -0
- package/dist/tenant/handlers/tenant-directory.query.js +13 -3
- package/dist/tenant/handlers/update-member-roles.write.d.ts +1 -1
- package/dist/tenant/handlers/update-member-roles.write.js +28 -12
- package/dist/tenant/i18n.js +4 -0
- package/dist/tenant/index.d.ts +1 -1
- package/dist/tenant/index.js +1 -1
- package/dist/tenant/last-tenant-admin.d.ts +1 -1
- package/dist/tenant/last-tenant-admin.js +5 -3
- package/dist/tenant/seeding.d.ts +4 -0
- package/dist/tenant/seeding.js +9 -2
- package/dist/tenant-lifecycle/boot-checks.d.ts +1 -0
- package/dist/tenant-lifecycle/boot-checks.js +27 -1
- package/dist/tenant-lifecycle/feature.js +2 -1
- package/dist/tenant-lifecycle/stages.js +2 -2
- package/dist/tier-engine/feature.js +3 -2
- package/dist/user/command-schemas.d.ts +2 -2
- package/dist/user/handlers/create.write.d.ts +1 -1
- package/dist/user/handlers/create.write.js +8 -2
- package/dist/user/handlers/detail.query.d.ts +1 -0
- package/dist/user/handlers/update.write.d.ts +1 -1
- package/dist/user/handlers/update.write.js +8 -2
- package/dist/user-data-rights/db/queries/export-jobs.d.ts +2 -0
- package/dist/user-data-rights/feature.js +22 -9
- package/dist/user-data-rights/handlers/confirm-deletion-by-token.write.js +4 -7
- package/dist/user-data-rights/handlers/deletion-grace-period.d.ts +3 -2
- package/dist/user-data-rights/handlers/deletion-grace-period.js +30 -8
- package/dist/user-data-rights/handlers/download-by-token.query.js +1 -1
- package/dist/user-data-rights/handlers/request-deletion-by-email.write.js +1 -1
- package/dist/user-data-rights/handlers/request-deletion.write.js +3 -6
- package/dist/user-data-rights/lib/update-user-lifecycle.js +7 -14
- package/dist/user-data-rights/run-export-jobs.d.ts +1 -0
- package/dist/user-data-rights/run-export-jobs.js +74 -41
- package/dist/user-data-rights/run-forget-cleanup.js +2 -2
- package/dist/user-data-rights/run-user-export.js +2 -2
- package/dist/user-data-rights-defaults/hooks/config-value.userdata-hook.js +4 -2
- package/dist/user-data-rights-defaults/hooks/file-ref.userdata-hook.js +8 -3
- package/dist/user-data-rights-defaults/hooks/notification-preference.userdata-hook.js +4 -2
- package/dist/user-data-rights-defaults/hooks/tenant-invitation.userdata-hook.js +4 -2
- package/dist/user-profile/feature.js +6 -1
- package/dist/user-profile/handlers/change-email.write.js +2 -0
- package/dist/workflow-runner/db/queries/due-runs.js +6 -9
- package/dist/workflow-runner/event-subscriber.js +29 -9
- package/dist/workflow-runner/handlers/resume-run.write.js +8 -5
- package/dist/workflow-runner/pending-projection.js +8 -1
- package/package.json +11 -9
- package/src/agent-tools/changes.json +12 -0
- package/src/auth-email-password/changes.json +18 -0
- package/src/auth-mfa/changes.json +6 -0
- package/src/billing-foundation/changes.json +7 -0
- package/src/cap-counter/changes.json +13 -0
- package/src/cap-overview/changes.json +12 -0
- package/src/channel-email/changes.json +6 -0
- package/src/config/changes.json +6 -0
- package/src/crypto-shredding/changes.json +6 -0
- package/src/data-retention/changes.json +12 -0
- package/src/delivery/changes.json +13 -0
- package/src/derivatives-sharp/changes.json +11 -0
- package/src/document-ingest-foundation/changes.json +12 -0
- package/src/feature-toggles/changes.json +5 -0
- package/src/inbound-mail-foundation/changes.json +12 -0
- package/src/jobs/changes.json +18 -0
- package/src/legal-pages/changes.json +8 -1
- package/src/managed-pages/changes.json +5 -0
- package/src/metrics/changes.json +6 -0
- package/src/personal-access-tokens/changes.json +5 -0
- package/src/rate-limiting/changes.json +5 -0
- package/src/secrets/changes.json +7 -0
- package/src/sessions/changes.json +11 -0
- package/src/shared/changes.json +6 -0
- package/src/step-dispatcher/changes.json +5 -0
- package/src/subscription-mollie/changes.json +7 -0
- package/src/subscription-stripe/changes.json +11 -0
- package/src/template-resolver/changes.json +7 -0
- package/src/tenant/changes.json +29 -0
- package/src/user-data-rights/changes.json +22 -0
- package/src/user-data-rights-defaults/changes.json +5 -0
- package/src/workflow-runner/changes.json +6 -0
|
@@ -114,7 +114,7 @@ export function platformOverviewPanels(metrics) {
|
|
|
114
114
|
}),
|
|
115
115
|
recentFailuresList(JobQueries.list, { status: "failed", sort: "startedAt", sortDirection: "desc", limit: 5 }, [
|
|
116
116
|
{ field: "jobName", label: `${I}.col.job` },
|
|
117
|
-
{ field: "startedAt", label: `${I}.col.startedAt
|
|
117
|
+
{ field: "startedAt", label: `${I}.col.startedAt`, display: "datetime" },
|
|
118
118
|
], "full"),
|
|
119
119
|
];
|
|
120
120
|
}
|
|
@@ -161,7 +161,7 @@ export function tenantOverviewPanels(metrics, options) {
|
|
|
161
161
|
recentFailuresList(JobQueries.failures, { limit: 5 }, [
|
|
162
162
|
{ field: "jobName", label: `${I}.col.job` },
|
|
163
163
|
{ field: "messageKey", label: `${I}.col.reason` },
|
|
164
|
-
{ field: "failedAt", label: `${I}.col.failedAt
|
|
164
|
+
{ field: "failedAt", label: `${I}.col.failedAt`, display: "datetime" },
|
|
165
165
|
], "half"),
|
|
166
166
|
];
|
|
167
167
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { FeatureDefinition } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
2
|
export declare const AgentDocGapKinds: {
|
|
3
|
+
readonly handlerSchemaNotExpressible: "handler-schema-not-expressible";
|
|
3
4
|
readonly handlerWithoutDescription: "handler-without-description";
|
|
4
5
|
readonly customScreenWithoutDescription: "custom-screen-without-description";
|
|
5
6
|
readonly exposedEntityWithoutDescription: "exposed-entity-without-description";
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { isAgentVisibleScreen, QnTypes, qn, resolveAgentExposure, toKebab, } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
|
+
import * as z from "zod";
|
|
2
3
|
export const AgentDocGapKinds = {
|
|
4
|
+
handlerSchemaNotExpressible: "handler-schema-not-expressible",
|
|
3
5
|
handlerWithoutDescription: "handler-without-description",
|
|
4
6
|
customScreenWithoutDescription: "custom-screen-without-description",
|
|
5
7
|
exposedEntityWithoutDescription: "exposed-entity-without-description",
|
|
@@ -15,16 +17,44 @@ const ENTITY_QN_SEGMENT = "entity";
|
|
|
15
17
|
function compareByCodePoint(a, b) {
|
|
16
18
|
return a < b ? -1 : a > b ? 1 : 0;
|
|
17
19
|
}
|
|
20
|
+
// agent-manifest.ts silently drops an exposed handler whose schema can't be
|
|
21
|
+
// converted; surface it here so the missing tool doesn't go unnoticed.
|
|
22
|
+
function schemaNotExpressibleGap(feature, handlerQn, def, handlerNoun) {
|
|
23
|
+
try {
|
|
24
|
+
z.toJSONSchema(def.schema, { io: "input" });
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
return {
|
|
29
|
+
qn: handlerQn,
|
|
30
|
+
feature: feature.name,
|
|
31
|
+
kind: AgentDocGapKinds.handlerSchemaNotExpressible,
|
|
32
|
+
message: `This ${handlerNoun} is exposed to the AI agent but its input schema can't be converted to JSON Schema (${error instanceof Error ? error.message : String(error)}), so it is left out of the agent manifest.`,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
}
|
|
18
36
|
function handlerDocGaps(feature, handlers, handlerQnType, handlerNoun) {
|
|
19
37
|
const gaps = [];
|
|
38
|
+
const handlerKind = handlerQnType === QnTypes.write ? "write" : "query";
|
|
20
39
|
for (const [name, def] of Object.entries(handlers)) {
|
|
21
|
-
|
|
40
|
+
const handlerQn = qn(toKebab(feature.name), handlerQnType, toKebab(name));
|
|
41
|
+
const isExposed = resolveAgentExposure(def, handlerKind).expose;
|
|
42
|
+
if (isExposed) {
|
|
43
|
+
const schemaGap = schemaNotExpressibleGap(feature, handlerQn, def, handlerNoun);
|
|
44
|
+
if (schemaGap)
|
|
45
|
+
gaps.push(schemaGap);
|
|
46
|
+
}
|
|
47
|
+
if (def.agent?.expose === false)
|
|
48
|
+
continue;
|
|
49
|
+
if (def.description !== undefined && def.description.trim() !== "")
|
|
22
50
|
continue;
|
|
23
51
|
gaps.push({
|
|
24
|
-
qn:
|
|
52
|
+
qn: handlerQn,
|
|
25
53
|
feature: feature.name,
|
|
26
54
|
kind: AgentDocGapKinds.handlerWithoutDescription,
|
|
27
|
-
message:
|
|
55
|
+
message: isExposed
|
|
56
|
+
? `This ${handlerNoun} is exposed to the AI agent but has no usable description, so the agent sees an empty tool text — set a non-empty \`description\`, or \`agent: { expose: false }\` to opt out deliberately.`
|
|
57
|
+
: `This ${handlerNoun} has no description, so it stays invisible to the AI agent — set \`description\` to expose it, or \`agent: { expose: false }\` to opt out deliberately.`,
|
|
28
58
|
});
|
|
29
59
|
}
|
|
30
60
|
return gaps;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { hasAccess, isAgentVisibleScreen, resolveAgentExposure, } from "@cosmicdrift/kumiko-framework/engine";
|
|
1
|
+
import { hasAccess, isAgentVisibleScreen, resolveAgentExposure, SYSTEM_ONLY_JSON_SCHEMA_KEY, } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
2
|
import { translationValueOtherText } from "@cosmicdrift/kumiko-framework/ui-types";
|
|
3
3
|
import * as z from "zod";
|
|
4
4
|
/** The manifest surfaces one string per locale — a plural entry reads as its
|
|
@@ -92,6 +92,24 @@ function buildFeatures(features) {
|
|
|
92
92
|
}
|
|
93
93
|
return result;
|
|
94
94
|
}
|
|
95
|
+
// Agent dispatch never runs as a system identity, so properties the engine only
|
|
96
|
+
// honors for system callers would be silently dropped — hide them from the tool.
|
|
97
|
+
function withoutSystemOnlyProperties(jsonSchema) {
|
|
98
|
+
const properties = jsonSchema["properties"];
|
|
99
|
+
if (typeof properties !== "object" || properties === null)
|
|
100
|
+
return jsonSchema;
|
|
101
|
+
const hidden = Object.entries(properties)
|
|
102
|
+
.filter(([, property]) => property[SYSTEM_ONLY_JSON_SCHEMA_KEY] === true)
|
|
103
|
+
.map(([name]) => name);
|
|
104
|
+
if (hidden.length === 0)
|
|
105
|
+
return jsonSchema;
|
|
106
|
+
const required = jsonSchema["required"];
|
|
107
|
+
return {
|
|
108
|
+
...jsonSchema,
|
|
109
|
+
properties: Object.fromEntries(Object.entries(properties).filter(([name]) => !hidden.includes(name))),
|
|
110
|
+
...(Array.isArray(required) && { required: required.filter((name) => !hidden.includes(name)) }),
|
|
111
|
+
};
|
|
112
|
+
}
|
|
95
113
|
function buildHandlerEntry(qn, kind, def, roles, getHandlerEntity, denyQns) {
|
|
96
114
|
if (denyQns.has(qn))
|
|
97
115
|
return undefined;
|
|
@@ -108,7 +126,7 @@ function buildHandlerEntry(qn, kind, def, roles, getHandlerEntity, denyQns) {
|
|
|
108
126
|
// A handler whose schema can't be expressed as JSON Schema (transforms,
|
|
109
127
|
// z.instanceof, ...) is unusable for an agent tool call — leave it out
|
|
110
128
|
// rather than ship a broken tool.
|
|
111
|
-
inputSchema = z.toJSONSchema(def.schema, { io: "input" });
|
|
129
|
+
inputSchema = withoutSystemOnlyProperties(z.toJSONSchema(def.schema, { io: "input" }));
|
|
112
130
|
}
|
|
113
131
|
catch {
|
|
114
132
|
return undefined;
|
|
@@ -137,6 +155,16 @@ function buildHandlers(queryHandlers, writeHandlers, roles, getHandlerEntity, de
|
|
|
137
155
|
}
|
|
138
156
|
return result;
|
|
139
157
|
}
|
|
158
|
+
// A typo or a QN renamed by a framework update would otherwise deny nothing
|
|
159
|
+
// and leave the handler exposed without anyone noticing.
|
|
160
|
+
function assertDenyQnsExist(denyQns, registry) {
|
|
161
|
+
const queryHandlers = registry.getAllQueryHandlers();
|
|
162
|
+
const writeHandlers = registry.getAllWriteHandlers();
|
|
163
|
+
const unknown = denyQns.filter((qn) => !queryHandlers.has(qn) && !writeHandlers.has(qn));
|
|
164
|
+
if (unknown.length > 0) {
|
|
165
|
+
throw new Error(`agent denyQns names handlers that are not registered: ${unknown.join(", ")}`);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
140
168
|
function buildNavs(navs, workspaces, translations, roles, screens) {
|
|
141
169
|
const result = [];
|
|
142
170
|
for (const [, nav] of navs) {
|
|
@@ -159,13 +187,25 @@ function buildNavs(navs, workspaces, translations, roles, screens) {
|
|
|
159
187
|
workspaces: visibleWorkspaces,
|
|
160
188
|
});
|
|
161
189
|
}
|
|
162
|
-
return result;
|
|
190
|
+
return withoutOrphanedNavs(result);
|
|
191
|
+
}
|
|
192
|
+
// A child under a parent hidden from this role would point at an id that
|
|
193
|
+
// isn't in the manifest; hiding cascades down the whole subtree.
|
|
194
|
+
function withoutOrphanedNavs(navs) {
|
|
195
|
+
let remaining = navs;
|
|
196
|
+
for (;;) {
|
|
197
|
+
const ids = new Set(remaining.map((nav) => nav.id));
|
|
198
|
+
const reachable = remaining.filter((nav) => nav.parent === undefined || ids.has(nav.parent));
|
|
199
|
+
if (reachable.length === remaining.length)
|
|
200
|
+
return remaining;
|
|
201
|
+
remaining = reachable;
|
|
202
|
+
}
|
|
163
203
|
}
|
|
164
204
|
function lastSegment(qualifiedId) {
|
|
165
205
|
const idx = qualifiedId.lastIndexOf(":");
|
|
166
206
|
return idx === -1 ? qualifiedId : qualifiedId.slice(idx + 1);
|
|
167
207
|
}
|
|
168
|
-
function buildScreens(screens, navs, workspaces, translations, roles) {
|
|
208
|
+
function buildScreens(screens, navs, workspaces, translations, roles, visibleNavIds) {
|
|
169
209
|
const allNavs = [...navs.values()];
|
|
170
210
|
const result = [];
|
|
171
211
|
for (const [, screen] of screens) {
|
|
@@ -174,7 +214,7 @@ function buildScreens(screens, navs, workspaces, translations, roles) {
|
|
|
174
214
|
if (!isAgentVisibleScreen(screen))
|
|
175
215
|
continue;
|
|
176
216
|
const matchingNavs = allNavs.filter((nav) => nav.screen === screen.id);
|
|
177
|
-
const accessibleNavs = matchingNavs.filter((nav) => uiVisible(nav.access, roles));
|
|
217
|
+
const accessibleNavs = matchingNavs.filter((nav) => visibleNavIds.has(nav.id) && uiVisible(nav.access, roles));
|
|
178
218
|
// A screen with no nav pointing at it is a detail view, reached only via
|
|
179
219
|
// navigation from a list row — never hidden by nav/workspace gating.
|
|
180
220
|
const reachable = matchingNavs.length === 0 ||
|
|
@@ -229,12 +269,13 @@ export function buildAgentManifest(registry, options) {
|
|
|
229
269
|
const workspaceMap = registry.getAllWorkspaces();
|
|
230
270
|
const navMap = registry.getAllNavs();
|
|
231
271
|
const { roles } = options;
|
|
272
|
+
assertDenyQnsExist(options.denyQns ?? [], registry);
|
|
232
273
|
const features = buildFeatures(registry.features);
|
|
233
274
|
const entities = buildEntities(registry.getAllEntities(), translations, (name) => registry.getSearchableFields(name));
|
|
234
275
|
const handlers = buildHandlers(registry.getAllQueryHandlers(), registry.getAllWriteHandlers(), roles, (qn) => registry.getHandlerEntity(qn), new Set(options.denyQns ?? []));
|
|
235
276
|
const screenMap = registry.getAllScreens();
|
|
236
277
|
const navs = buildNavs(navMap, workspaceMap, translations, roles, screenMap);
|
|
237
|
-
const screens = buildScreens(screenMap, navMap, workspaceMap, translations, roles);
|
|
278
|
+
const screens = buildScreens(screenMap, navMap, workspaceMap, translations, roles, new Set(navs.map((nav) => nav.id)));
|
|
238
279
|
const workspaces = buildWorkspaces(workspaceMap, translations, roles);
|
|
239
280
|
return {
|
|
240
281
|
builtForRoles: [...roles],
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { hasAccess } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
|
-
|
|
2
|
+
import { FILTER_OPS, isRecord } from "./filter-ops.js";
|
|
3
3
|
const MAX_TOOL_NAME_LENGTH = 64;
|
|
4
4
|
/** Field types that declare `filterable` (per `packages/framework/src/engine/types/fields.ts`).
|
|
5
5
|
* Mapped to the JSON-Schema type an LLM tool-call argument should use. `undefined` = field type
|
|
@@ -173,9 +173,6 @@ function filterableFieldsOf(entity) {
|
|
|
173
173
|
function compareByCodePoint(a, b) {
|
|
174
174
|
return a < b ? -1 : a > b ? 1 : 0;
|
|
175
175
|
}
|
|
176
|
-
function isRecord(value) {
|
|
177
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
178
|
-
}
|
|
179
176
|
function schemaRequiresField(schema, field) {
|
|
180
177
|
const required = schema["required"];
|
|
181
178
|
return Array.isArray(required) && required.includes(field);
|
|
@@ -1,11 +1,8 @@
|
|
|
1
1
|
import { runAsDirectCallEntry } from "@cosmicdrift/kumiko-framework/api";
|
|
2
|
+
import { FILTER_OPS, isRecord } from "./filter-ops.js";
|
|
2
3
|
const SEARCH_RESULT_LIMIT = 10;
|
|
3
4
|
const DEFAULT_LIST_LIMIT = 10;
|
|
4
5
|
const MAX_LIST_LIMIT = 200;
|
|
5
|
-
const FILTER_OPS = ["eq", "ne", "lt", "gt", "lte", "gte", "in"];
|
|
6
|
-
function isRecord(value) {
|
|
7
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
8
|
-
}
|
|
9
6
|
function isFilterOp(value) {
|
|
10
7
|
return typeof value === "string" && FILTER_OPS.includes(value);
|
|
11
8
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export declare const AUDIT_FEATURE: "audit";
|
|
2
2
|
/** Literal `createdBy` written by hand-built system actors (secrets/jobs/…). */
|
|
3
3
|
export declare const SYSTEM_ACTOR_ID: "system";
|
|
4
|
-
/** Literal `createdBy` for anonymous / unauthenticated actors. */
|
|
4
|
+
/** Literal `createdBy` for anonymous / unauthenticated actors. Duplicated from `/engine` on purpose: a value import would pull the engine into the client bundle; constants.test.ts guards against drift. */
|
|
5
5
|
export declare const ANONYMOUS_USER_ID: "anonymous";
|
|
6
6
|
/** All `createdBy` values that mean "system" in the audit UI (nil-UUID from createSystemUser + literal). */
|
|
7
7
|
export declare const SYSTEM_ACTOR_IDS: ReadonlySet<string>;
|
package/dist/audit/constants.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
export const AUDIT_FEATURE = "audit";
|
|
4
4
|
/** Literal `createdBy` written by hand-built system actors (secrets/jobs/…). */
|
|
5
5
|
export const SYSTEM_ACTOR_ID = "system";
|
|
6
|
-
/** Literal `createdBy` for anonymous / unauthenticated actors. */
|
|
6
|
+
/** Literal `createdBy` for anonymous / unauthenticated actors. Duplicated from `/engine` on purpose: a value import would pull the engine into the client bundle; constants.test.ts guards against drift. */
|
|
7
7
|
export const ANONYMOUS_USER_ID = "anonymous";
|
|
8
8
|
/** All `createdBy` values that mean "system" in the audit UI (nil-UUID from createSystemUser + literal). */
|
|
9
9
|
export const SYSTEM_ACTOR_IDS = new Set([
|
|
@@ -20,6 +20,8 @@ export const changePasswordWrite = defineWriteHandler({
|
|
|
20
20
|
"and writes the new hash via ctx.writeAs(SYSTEM, user:update) — the caller's own " +
|
|
21
21
|
"identity can't reach either, field-access on passwordHash is privileged-only.",
|
|
22
22
|
},
|
|
23
|
+
// Payload carries the old and new password.
|
|
24
|
+
agent: { expose: false },
|
|
23
25
|
description: "Replaces the signed-in caller's own password after re-checking the current one; use it for a self-service password change rather than the emailed reset flow.",
|
|
24
26
|
handler: async (event, ctx) => {
|
|
25
27
|
const systemUser = createSystemUser(event.user.tenantId);
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
// invite-create.
|
|
19
19
|
import { generateToken } from "@cosmicdrift/kumiko-framework/api";
|
|
20
20
|
import { createEventStoreExecutor } from "@cosmicdrift/kumiko-framework/db";
|
|
21
|
-
import { access,
|
|
21
|
+
import { access, assignableAppRolesOf, defineWriteHandler, } from "@cosmicdrift/kumiko-framework/engine";
|
|
22
22
|
import { InternalError, writeFailure } from "@cosmicdrift/kumiko-framework/errors";
|
|
23
23
|
import { Temporal } from "temporal-polyfill";
|
|
24
24
|
import * as z from "zod";
|
|
@@ -38,7 +38,17 @@ const InviteCreateSchema = z.object({
|
|
|
38
38
|
const executor = createEventStoreExecutor(tenantInvitationsTable, tenantInvitationEntity, {
|
|
39
39
|
entityName: "tenant-invitation",
|
|
40
40
|
});
|
|
41
|
+
// A framework-ranked role is one the highest actor ("system") may assign without the
|
|
42
|
+
// unknown-role fail-closed rejection; allow-listing it would let a lower-ranked inviter
|
|
43
|
+
// bypass the elevation guard for it.
|
|
44
|
+
function findRankedRole(roles) {
|
|
45
|
+
return roles.find((role) => findForbiddenRoleAssignment(["system"], [role], []) === undefined);
|
|
46
|
+
}
|
|
41
47
|
export function createInviteCreateHandler(opts) {
|
|
48
|
+
const rankedRole = findRankedRole(opts.additionalAssignableRoles ?? []);
|
|
49
|
+
if (rankedRole !== undefined) {
|
|
50
|
+
throw new Error(`[auth-email-password] invite.additionalAssignableRoles must list app-defined roles only; "${rankedRole}" is a framework-ranked role`);
|
|
51
|
+
}
|
|
42
52
|
const ttlMinutes = opts.tokenTtlMinutes ?? AUTH_INVITE_DEFAULT_TTL_MINUTES;
|
|
43
53
|
const ttlSeconds = ttlMinutes * 60;
|
|
44
54
|
return defineWriteHandler({
|
|
@@ -54,7 +64,7 @@ export function createInviteCreateHandler(opts) {
|
|
|
54
64
|
if (forbiddenRole !== undefined) {
|
|
55
65
|
return writeFailure(reservedMembershipRoleError(forbiddenRole));
|
|
56
66
|
}
|
|
57
|
-
const elevationForbidden = findForbiddenRoleAssignment(event.user.roles, [event.payload.role], [],
|
|
67
|
+
const elevationForbidden = findForbiddenRoleAssignment(event.user.roles, [event.payload.role], [], assignableAppRolesOf(ctx.registry));
|
|
58
68
|
if (elevationForbidden !== undefined &&
|
|
59
69
|
!opts.additionalAssignableRoles?.includes(elevationForbidden)) {
|
|
60
70
|
return writeFailure(unassignableMembershipRoleError(elevationForbidden));
|
|
@@ -157,6 +157,8 @@ export function createLoginHandler(opts = {}) {
|
|
|
157
157
|
"enrollment of the tenant named in the signed login/setup token, not the guest " +
|
|
158
158
|
"dispatch tenant, via the mfaStatusChecker callback.",
|
|
159
159
|
},
|
|
160
|
+
// Anonymous credential intake (password) — must never enter an LLM transcript.
|
|
161
|
+
agent: { expose: false },
|
|
160
162
|
description: "Signs a user in with email and password, running the lockout, email-verification, account-status, tenant-membership and MFA gates, and answering with a session or with an MFA challenge or setup requirement.",
|
|
161
163
|
handler: async (event, ctx) => {
|
|
162
164
|
const systemUser = createSystemUser(SYSTEM_USER_ID);
|
|
@@ -13,7 +13,7 @@ export const selfRegistrationStatusQuery = defineQueryHandler({
|
|
|
13
13
|
name: "signup-registration-status",
|
|
14
14
|
schema: z.object({}),
|
|
15
15
|
access: { roles: ["anonymous", "User", "TenantAdmin", "SystemAdmin"] },
|
|
16
|
-
rateLimit: { per: "ip", limit: 30, windowSeconds: 60 },
|
|
16
|
+
rateLimit: { per: "ip+handler", limit: 30, windowSeconds: 60 },
|
|
17
17
|
description: "Reports whether self-registration is currently switched on, readable without signing in, so a signup page can hide its own form instead of collecting an address that would be silently ignored.",
|
|
18
18
|
handler: async (_query, ctx) => {
|
|
19
19
|
// Same fail-open reasoning as signup-request.write.ts (#1468): only
|
|
@@ -15,6 +15,7 @@ export const defaultTranslations = {
|
|
|
15
15
|
"auth.login.submitting": "…",
|
|
16
16
|
"auth.login.forgotPassword": "Forgot password?",
|
|
17
17
|
"auth.login.unlockAccount": "Unlock account?",
|
|
18
|
+
"auth.login.sessionEnded": "Your session has ended. Please sign in again.",
|
|
18
19
|
"auth.login.resendVerification": "Send verification email again",
|
|
19
20
|
"auth.login.resendSuccess": "We've sent you a new verification email.",
|
|
20
21
|
"auth.login.resendRateLimited": "Please wait a moment and try again.",
|
|
@@ -119,6 +120,7 @@ export const defaultTranslations = {
|
|
|
119
120
|
"auth.sessionBootstrap.rateLimitedBody": "Too many requests in a short time. Please wait a moment and try again.",
|
|
120
121
|
"auth.sessionBootstrap.retry": "Try again",
|
|
121
122
|
"auth.sessionBootstrap.retrying": "Retrying…",
|
|
123
|
+
"auth.sessionBootstrap.signOut": "Sign out",
|
|
122
124
|
"auth.user.menu.label": "Account",
|
|
123
125
|
"auth.user.menu.logout": "Sign out",
|
|
124
126
|
"auth.tenant.switcher.label": "Tenant",
|
|
@@ -45,6 +45,7 @@ export type LoginResult = {
|
|
|
45
45
|
readonly kind: "failure";
|
|
46
46
|
readonly error: LoginFailure;
|
|
47
47
|
};
|
|
48
|
+
export declare function toLoginResponse(token: string, user: LoginResponse["user"], landingPath: unknown): LoginResponse;
|
|
48
49
|
export declare function login(req: LoginRequest): Promise<LoginResult>;
|
|
49
50
|
export declare function logout(): Promise<void>;
|
|
50
51
|
export type AuthTokenFailure = {
|
|
@@ -33,7 +33,7 @@ export function parseRetryAfterSeconds(header) {
|
|
|
33
33
|
return undefined;
|
|
34
34
|
return Number(header);
|
|
35
35
|
}
|
|
36
|
-
function toLoginResponse(token, user, landingPath) {
|
|
36
|
+
export function toLoginResponse(token, user, landingPath) {
|
|
37
37
|
return typeof landingPath === "string" ? { token, user, landingPath } : { token, user };
|
|
38
38
|
}
|
|
39
39
|
// POST /api/auth/login. Success → token + user; MFA-enrolled user → a
|
|
@@ -30,7 +30,7 @@ export function createLoginRoute(opts = {}) {
|
|
|
30
30
|
const MfaVerifyComponent = opts.mfaVerifyScreen;
|
|
31
31
|
const MfaSetupComponent = opts.mfaSetupScreen;
|
|
32
32
|
function LoginRoute() {
|
|
33
|
-
const { status, refresh, bootstrapFailure } = useSession();
|
|
33
|
+
const { status, refresh, bootstrapFailure, logout } = useSession();
|
|
34
34
|
const { onAuthenticated } = opts;
|
|
35
35
|
// Pending challenge-token from LoginScreen's onMfaChallenge. Lives here
|
|
36
36
|
// (not in SessionState) because it's a UI-only transition — the server
|
|
@@ -46,7 +46,7 @@ export function createLoginRoute(opts = {}) {
|
|
|
46
46
|
return (_jsx("div", { className: "min-h-screen flex items-center justify-center text-muted-foreground text-sm" }));
|
|
47
47
|
}
|
|
48
48
|
if (status === "error" && bootstrapFailure !== null) {
|
|
49
|
-
return _jsx(SessionBootstrapErrorScreen, { failure: bootstrapFailure, onRetry: refresh });
|
|
49
|
+
return (_jsx(SessionBootstrapErrorScreen, { failure: bootstrapFailure, onRetry: refresh, onSignOut: logout }));
|
|
50
50
|
}
|
|
51
51
|
// A standalone mount (no parent gate, no onAuthenticated wired — e.g. an
|
|
52
52
|
// apex/marketing surface that just places <LoginRoute /> directly) has no
|
|
@@ -65,12 +65,10 @@ export function createLoginRoute(opts = {}) {
|
|
|
65
65
|
return (_jsx(MfaSetupComponent, { preauthSetupToken: setupRequest.preauthSetupToken, accountLabel: setupRequest.accountLabel, onSuccess: () => {
|
|
66
66
|
// MfaSetupPreauthScreen has no session to refresh itself with
|
|
67
67
|
// (it runs pre-auth) — the gate owns the session, so it refreshes.
|
|
68
|
-
//
|
|
69
|
-
//
|
|
70
|
-
//
|
|
71
|
-
void refresh().then(() => setSetupRequest(null)
|
|
72
|
-
// leave setupRequest set so the user can cancel/retry
|
|
73
|
-
});
|
|
68
|
+
// refresh() never rejects: a failed refresh surfaces as status
|
|
69
|
+
// "error" (bootstrap error screen with retry), so only the
|
|
70
|
+
// success path needs to clear setupRequest.
|
|
71
|
+
void refresh().then(() => setSetupRequest(null));
|
|
74
72
|
}, onCancel: () => setSetupRequest(null) }));
|
|
75
73
|
}
|
|
76
74
|
return (_jsx(LoginComponent, { ...opts.loginScreenProps, onMfaChallenge: MfaVerifyComponent ? setChallengeToken : opts.loginScreenProps?.onMfaChallenge, onMfaSetupRequired: MfaSetupComponent
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { defaultTranslations, mergeTranslations } from "../i18n.js";
|
|
2
2
|
export type { AuthTokenFailure, CurrentUserProfile, LoginFailure, LoginRequest, LoginResponse, ResetPasswordFailure, SignupConfirmSuccess, TenantSummary, VerifyEmailFailure, } from "./auth-client.js";
|
|
3
|
-
export { AuthRequestError, confirmAccountUnlock, confirmSignup, requestAccountUnlock, requestEmailVerification, requestPasswordReset, requestSignup, resetPassword, verifyEmail, } from "./auth-client.js";
|
|
3
|
+
export { AuthRequestError, confirmAccountUnlock, confirmSignup, requestAccountUnlock, requestEmailVerification, requestPasswordReset, requestSignup, resetPassword, toLoginResponse, verifyEmail, } from "./auth-client.js";
|
|
4
4
|
export type { AuthCardProps, AuthShellRenderer } from "./auth-form-primitives.js";
|
|
5
5
|
export { AuthCard, AuthShellProvider, useAuthShell } from "./auth-form-primitives.js";
|
|
6
6
|
export type { LoginRouteOptions, MfaSetupComponentProps, MfaVerifyComponentProps, } from "./auth-gate.js";
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// `@cosmicdrift/kumiko-bundled-features/auth-email-password` und hat keine
|
|
6
6
|
// React-/DOM-Deps. Trennung bleibt sauber so wie renderer vs renderer-web.
|
|
7
7
|
export { defaultTranslations, mergeTranslations } from "../i18n.js";
|
|
8
|
-
export { AuthRequestError, confirmAccountUnlock, confirmSignup, requestAccountUnlock, requestEmailVerification, requestPasswordReset, requestSignup, resetPassword, verifyEmail, } from "./auth-client.js";
|
|
8
|
+
export { AuthRequestError, confirmAccountUnlock, confirmSignup, requestAccountUnlock, requestEmailVerification, requestPasswordReset, requestSignup, resetPassword, toLoginResponse, verifyEmail, } from "./auth-client.js";
|
|
9
9
|
export { AuthCard, AuthShellProvider, useAuthShell } from "./auth-form-primitives.js";
|
|
10
10
|
export { createLoginRoute, makeAuthGate, makeSessionAuthGate } from "./auth-gate.js";
|
|
11
11
|
export { emailPasswordClient } from "./client-plugin.js";
|
|
@@ -42,6 +42,19 @@ export function InviteAcceptScreen({ title, token: tokenProp, loggedInHref = "/"
|
|
|
42
42
|
const [submitting, setSubmitting] = useState(false);
|
|
43
43
|
const [error, setError] = useState(null);
|
|
44
44
|
const effectiveTitle = title ?? t("auth.inviteAccept.title");
|
|
45
|
+
// The invitation is already consumed when the server answers with an MFA
|
|
46
|
+
// challenge / setup requirement, but no session cookie was minted: send the
|
|
47
|
+
// user to the login screen (now a member) instead of a tenant URL.
|
|
48
|
+
const finishAccept = (data) => {
|
|
49
|
+
if (data.mfaRequired === true || data.mfaSetupRequired === true) {
|
|
50
|
+
window.location.assign(loginHref);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
window.location.assign(resolvePostAuthHref(data.landingPath, loggedInHref, {
|
|
54
|
+
tenantId: data.tenantId,
|
|
55
|
+
roles: grantedRoles(data),
|
|
56
|
+
}));
|
|
57
|
+
};
|
|
45
58
|
const acceptLoggedIn = async () => {
|
|
46
59
|
setSubmitting(true);
|
|
47
60
|
setError(null);
|
|
@@ -56,10 +69,7 @@ export function InviteAcceptScreen({ title, token: tokenProp, loggedInHref = "/"
|
|
|
56
69
|
if (res.ok) {
|
|
57
70
|
// @cast-boundary engine-payload — auth route JSON
|
|
58
71
|
const data = (await res.json());
|
|
59
|
-
|
|
60
|
-
tenantId: data.tenantId,
|
|
61
|
-
roles: grantedRoles(data),
|
|
62
|
-
}));
|
|
72
|
+
finishAccept(data);
|
|
63
73
|
return;
|
|
64
74
|
}
|
|
65
75
|
setError(t("auth.errors.invalidInviteToken"));
|
|
@@ -78,10 +88,7 @@ export function InviteAcceptScreen({ title, token: tokenProp, loggedInHref = "/"
|
|
|
78
88
|
if (res.ok) {
|
|
79
89
|
// @cast-boundary engine-payload — auth route JSON
|
|
80
90
|
const data = (await res.json());
|
|
81
|
-
|
|
82
|
-
tenantId: data.tenantId,
|
|
83
|
-
roles: grantedRoles(data),
|
|
84
|
-
}));
|
|
91
|
+
finishAccept(data);
|
|
85
92
|
return;
|
|
86
93
|
}
|
|
87
94
|
setError(t("auth.errors.invalidInviteToken"));
|
|
@@ -100,10 +107,7 @@ export function InviteAcceptScreen({ title, token: tokenProp, loggedInHref = "/"
|
|
|
100
107
|
if (res.ok) {
|
|
101
108
|
// @cast-boundary engine-payload — auth route JSON
|
|
102
109
|
const data = (await res.json());
|
|
103
|
-
|
|
104
|
-
tenantId: data.tenantId,
|
|
105
|
-
roles: grantedRoles(data),
|
|
106
|
-
}));
|
|
110
|
+
finishAccept(data);
|
|
107
111
|
return;
|
|
108
112
|
}
|
|
109
113
|
setError(t("auth.errors.invalidInviteToken"));
|
|
@@ -113,7 +113,7 @@ export function LoginScreen({ title, subtitle, submitLabel, forgotPasswordHref,
|
|
|
113
113
|
};
|
|
114
114
|
const effectiveTitle = title ?? t("auth.login.title");
|
|
115
115
|
const effectiveSubmit = submitLabel ?? t("auth.login.submit");
|
|
116
|
-
return (_jsx(AuthCard, { title: effectiveTitle, subtitle: subtitle, children: _jsxs("div", { className: "p-6 pt-0 flex flex-col gap-4", children: [_jsxs(Form, { onSubmit: onSubmit, children: [_jsx(Field, { id: "login-email", label: t("auth.login.email"), required: true, children: _jsx(Input, { kind: "email", id: "login-email", name: "login-email", value: email, onChange: setEmail, disabled: submitting, required: true, autoComplete: "email" }) }), _jsx(Field, { id: "login-password", label: t("auth.login.password"), required: true, children: _jsx(Input, { kind: "password", id: "login-password", name: "login-password", value: password, onChange: setPassword, disabled: submitting, required: true, autoComplete: "current-password" }) }), resendStatus.kind === "success" ? (_jsx(Banner, { variant: "info", children: t("auth.login.resendSuccess") })) : error !== null ? (_jsx(Banner, { variant: "error", children: _jsxs("div", { className: "flex flex-col gap-1", children: [_jsx("span", { children: (() => {
|
|
116
|
+
return (_jsx(AuthCard, { title: effectiveTitle, subtitle: subtitle, children: _jsxs("div", { className: "p-6 pt-0 flex flex-col gap-4", children: [_jsxs(Form, { onSubmit: onSubmit, children: [_jsx(Field, { id: "login-email", label: t("auth.login.email"), required: true, children: _jsx(Input, { kind: "email", id: "login-email", name: "login-email", value: email, onChange: setEmail, disabled: submitting, required: true, autoComplete: "email" }) }), _jsx(Field, { id: "login-password", label: t("auth.login.password"), required: true, children: _jsx(Input, { kind: "password", id: "login-password", name: "login-password", value: password, onChange: setPassword, disabled: submitting, required: true, autoComplete: "current-password" }) }), session.signedOutReason === "session-ended" && (_jsx(Banner, { variant: "info", children: t("auth.login.sessionEnded") })), resendStatus.kind === "success" ? (_jsx(Banner, { variant: "info", children: t("auth.login.resendSuccess") })) : error !== null ? (_jsx(Banner, { variant: "error", children: _jsxs("div", { className: "flex flex-col gap-1", children: [_jsx("span", { children: (() => {
|
|
117
117
|
const { key, params } = reasonToKey(error);
|
|
118
118
|
return t(key, params);
|
|
119
119
|
})() }), error.reason === "email_not_verified" &&
|
|
@@ -3,7 +3,11 @@ import type { SessionBootstrapFailure } from "./session.js";
|
|
|
3
3
|
export type SessionBootstrapErrorScreenProps = {
|
|
4
4
|
readonly failure: SessionBootstrapFailure;
|
|
5
5
|
readonly onRetry: () => Promise<void>;
|
|
6
|
+
/** A permanent bootstrap failure (e.g. 403/500 on the membership query) would
|
|
7
|
+
* otherwise trap the user: the HttpOnly session cookie can only be cleared
|
|
8
|
+
* server-side. */
|
|
9
|
+
readonly onSignOut?: () => Promise<void>;
|
|
6
10
|
};
|
|
7
11
|
/** Retry delay measured from failure time, not click time. */
|
|
8
12
|
export declare function retryDelayMs(failure: SessionBootstrapFailure, nowEpochMs: number): number;
|
|
9
|
-
export declare function SessionBootstrapErrorScreen({ failure, onRetry, }: SessionBootstrapErrorScreenProps): ReactNode;
|
|
13
|
+
export declare function SessionBootstrapErrorScreen({ failure, onRetry, onSignOut, }: SessionBootstrapErrorScreenProps): ReactNode;
|
|
@@ -14,7 +14,7 @@ function wait(ms) {
|
|
|
14
14
|
return Promise.resolve();
|
|
15
15
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
16
16
|
}
|
|
17
|
-
export function SessionBootstrapErrorScreen({ failure, onRetry, }) {
|
|
17
|
+
export function SessionBootstrapErrorScreen({ failure, onRetry, onSignOut, }) {
|
|
18
18
|
const t = useTranslation();
|
|
19
19
|
const { Banner, Button } = usePrimitives();
|
|
20
20
|
const [retrying, setRetrying] = useState(false);
|
|
@@ -26,5 +26,5 @@ export function SessionBootstrapErrorScreen({ failure, onRetry, }) {
|
|
|
26
26
|
};
|
|
27
27
|
return (_jsx("div", { "data-testid": "session-bootstrap-error", "data-http-status": failure.httpStatus ?? "network", children: _jsx(AuthCard, { title: t("auth.sessionBootstrap.errorTitle"), children: _jsxs("div", { className: "flex flex-col gap-4 px-6 pb-4", children: [_jsx(Banner, { variant: "error", children: failure.httpStatus === 429
|
|
28
28
|
? t("auth.sessionBootstrap.rateLimitedBody")
|
|
29
|
-
: t("auth.sessionBootstrap.errorBody") }), _jsx(Button, { variant: "primary", onClick: handleRetry, disabled: retrying, children: retrying ? t("auth.sessionBootstrap.retrying") : t("auth.sessionBootstrap.retry") })] }) }) }));
|
|
29
|
+
: t("auth.sessionBootstrap.errorBody") }), _jsx(Button, { variant: "primary", onClick: handleRetry, disabled: retrying, children: retrying ? t("auth.sessionBootstrap.retrying") : t("auth.sessionBootstrap.retry") }), onSignOut !== undefined && (_jsx(Button, { variant: "secondary", onClick: () => void onSignOut(), disabled: retrying, children: t("auth.sessionBootstrap.signOut") }))] }) }) }));
|
|
30
30
|
}
|
|
@@ -18,6 +18,10 @@ export type SessionState = {
|
|
|
18
18
|
* tenants[active].roles damit nav-filtering greift. Dedupliziert. */
|
|
19
19
|
readonly roles: readonly string[];
|
|
20
20
|
readonly bootstrapFailure: SessionBootstrapFailure | null;
|
|
21
|
+
/** Set when an authenticated session was ended server-side (revoked,
|
|
22
|
+
* expired) so the login screen can explain why it appeared. Optional so
|
|
23
|
+
* SessionApi values apps build themselves (test doubles) stay valid. */
|
|
24
|
+
readonly signedOutReason?: "session-ended" | null;
|
|
21
25
|
};
|
|
22
26
|
export type SessionApi = SessionState & {
|
|
23
27
|
readonly login: (req: LoginRequest) => Promise<LoginResult>;
|
|
@@ -10,6 +10,7 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
10
10
|
// unter `<SessionProvider>`; der `useSession()`-Hook liefert den State
|
|
11
11
|
// und die Transitions.
|
|
12
12
|
import { readCsrfToken } from "@cosmicdrift/kumiko-dispatcher-live";
|
|
13
|
+
import { useSessionEndedSignal } from "@cosmicdrift/kumiko-renderer";
|
|
13
14
|
import { createContext, useCallback, useContext, useEffect, useState } from "react";
|
|
14
15
|
import { AuthRequestError, fetchCurrentUser, fetchTenants, login as loginApi, logout as logoutApi, switchTenant as switchTenantApi, } from "./auth-client.js";
|
|
15
16
|
// Exported so screens that render on BOTH authenticated and anonymous public
|
|
@@ -23,6 +24,7 @@ export const UNAUTHENTICATED = {
|
|
|
23
24
|
tenants: [],
|
|
24
25
|
roles: [],
|
|
25
26
|
bootstrapFailure: null,
|
|
27
|
+
signedOutReason: null,
|
|
26
28
|
};
|
|
27
29
|
const INITIAL = {
|
|
28
30
|
status: "loading",
|
|
@@ -31,6 +33,7 @@ const INITIAL = {
|
|
|
31
33
|
tenants: [],
|
|
32
34
|
roles: [],
|
|
33
35
|
bootstrapFailure: null,
|
|
36
|
+
signedOutReason: null,
|
|
34
37
|
};
|
|
35
38
|
// kumiko_auth ist HttpOnly — kumiko_csrf wird beim Login gemeinsam gesetzt.
|
|
36
39
|
export function hasLikelyAuthSession(cookieSource) {
|
|
@@ -75,6 +78,7 @@ async function refresh() {
|
|
|
75
78
|
tenants: tenants.tenants,
|
|
76
79
|
roles: computeActiveRoles(user, tenants.activeTenantId, tenants.tenants),
|
|
77
80
|
bootstrapFailure: null,
|
|
81
|
+
signedOutReason: null,
|
|
78
82
|
};
|
|
79
83
|
}
|
|
80
84
|
// Non-AuthRequestError throws (offline, DNS, CORS) have no status, so httpStatus stays null.
|
|
@@ -104,6 +108,17 @@ export function SessionProvider({ children }) {
|
|
|
104
108
|
useEffect(() => {
|
|
105
109
|
void doRefresh();
|
|
106
110
|
}, [doRefresh]);
|
|
111
|
+
const sessionEndedSignal = useSessionEndedSignal();
|
|
112
|
+
// Only an authenticated session can "end": an anonymous 401 on a public
|
|
113
|
+
// surface must not show the hint.
|
|
114
|
+
// kumiko-lint-ignore no-raw-hooks Phase-3 conversion tracked in #2312
|
|
115
|
+
useEffect(() => {
|
|
116
|
+
return sessionEndedSignal?.subscribe(() => {
|
|
117
|
+
setState((prev) => prev.status === "authenticated"
|
|
118
|
+
? { ...UNAUTHENTICATED, signedOutReason: "session-ended" }
|
|
119
|
+
: prev);
|
|
120
|
+
});
|
|
121
|
+
}, [sessionEndedSignal]);
|
|
107
122
|
const login = useCallback(async (req) => {
|
|
108
123
|
const res = await loginApi(req);
|
|
109
124
|
if (res.kind === "success") {
|
package/dist/auth-mfa/feature.js
CHANGED
|
@@ -14,6 +14,7 @@ import { createMfaCodeVerifier } from "./mfa-code-verifier.js";
|
|
|
14
14
|
import { createMfaStatusChecker } from "./mfa-status-checker.js";
|
|
15
15
|
import { userMfaEntity } from "./schema/user-mfa.js";
|
|
16
16
|
import { mfaDisableScreen, mfaEnableScreen, mfaRegenerateRecoveryScreen } from "./screens.js";
|
|
17
|
+
const REENCRYPT_ESCAPE_HATCH_REASON = "re-encrypts MFA secrets of every tenant's users with the new master key";
|
|
17
18
|
// Reads the late-bind setter off a mounted auth-mfa feature's exports —
|
|
18
19
|
// mirrors sessions' own bindAutoRevokeFromFeature. run{Prod,Dev}App call
|
|
19
20
|
// this once the sessions feature (if mounted) has produced a concrete
|
|
@@ -102,9 +103,9 @@ export function createAuthMfaFeature(opts) {
|
|
|
102
103
|
name: "reencrypt",
|
|
103
104
|
trigger: { manual: true },
|
|
104
105
|
escapeHatch: {
|
|
105
|
-
reason:
|
|
106
|
+
reason: REENCRYPT_ESCAPE_HATCH_REASON,
|
|
106
107
|
},
|
|
107
|
-
handler: (payload, ctx) => mfaReencryptJob(payload, ctx, ctx.db.unsafeRaw(
|
|
108
|
+
handler: (payload, ctx) => mfaReencryptJob(payload, ctx, ctx.db.unsafeRaw(REENCRYPT_ESCAPE_HATCH_REASON)),
|
|
108
109
|
});
|
|
109
110
|
r.entity("user-mfa", userMfaEntity);
|
|
110
111
|
// Late-bound by run-prod-app once the sessions feature (if mounted) has
|
|
@@ -42,8 +42,8 @@ export function createEnableConfirmPreauthHandler(opts) {
|
|
|
42
42
|
"guest dispatch tenant.",
|
|
43
43
|
},
|
|
44
44
|
description: "Completes the enrollment that unblocks a sign-in forced into two-factor setup: verifies the code against the pre-auth setup token, stores the factor and derives the session the blocked login never got.",
|
|
45
|
-
//
|
|
46
|
-
agent: {
|
|
45
|
+
// Payload carries the pre-auth setup token and the TOTP code; the call mints a session.
|
|
46
|
+
agent: { expose: false },
|
|
47
47
|
handler: async (event, ctx) => {
|
|
48
48
|
const verify = verifyMfaSetupToken(event.payload.setupToken, opts.setupTokenSecret);
|
|
49
49
|
if (!verify.ok)
|
|
@@ -12,6 +12,7 @@ import { userMfaTable } from "../schema/user-mfa.js";
|
|
|
12
12
|
export const mfaStatusQuery = defineQueryHandler({
|
|
13
13
|
name: "user-mfa:status",
|
|
14
14
|
schema: z.object({}),
|
|
15
|
+
outputSchema: z.object({ enabled: z.boolean() }),
|
|
15
16
|
access: {
|
|
16
17
|
openToAll: {
|
|
17
18
|
reason: "each signed-in user reads only their own MFA status; the query filters " +
|