@cosmicdrift/kumiko-bundled-features 0.337.1 → 0.339.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/agent-tools/tool-catalog.js +37 -17
- package/dist/auth-email-password/handlers/invite-accept-with-login.write.js +3 -2
- package/dist/auth-email-password/handlers/invite-accept.write.js +3 -4
- package/dist/auth-email-password/handlers/invite-info.query.js +4 -1
- package/dist/auth-email-password/handlers/invite-signup-complete.write.js +3 -4
- package/dist/auth-email-password/handlers/signup-confirm.write.js +2 -1
- package/dist/auth-email-password/handlers/signup-request.write.js +2 -2
- package/dist/auth-email-password/web/auth-gate.d.ts +12 -2
- package/dist/auth-email-password/web/auth-gate.js +25 -1
- package/dist/auth-email-password/web/auth-redirect.d.ts +5 -0
- package/dist/auth-email-password/web/auth-redirect.js +58 -0
- package/dist/auth-email-password/web/client-plugin.d.ts +8 -1
- package/dist/auth-email-password/web/client-plugin.js +2 -0
- package/dist/auth-email-password/web/index.d.ts +2 -1
- package/dist/auth-email-password/web/index.js +1 -0
- package/dist/auth-email-password/web/session.d.ts +3 -1
- package/dist/auth-email-password/web/session.js +8 -2
- package/dist/auth-mfa/feature.js +1 -1
- package/dist/auth-mfa/handlers/enable-confirm-preauth.write.js +1 -1
- package/dist/auth-mfa/handlers/enable-start-preauth.write.js +2 -1
- package/dist/auth-mfa/handlers/verify.write.js +1 -1
- package/dist/auth-mfa/mfa-code-verifier.js +1 -1
- package/dist/auth-mfa/mfa-status-checker.js +1 -1
- package/dist/billing-foundation/aggregate-id.d.ts +2 -0
- package/dist/billing-foundation/aggregate-id.js +8 -0
- package/dist/billing-foundation/checkout-core.d.ts +3 -0
- package/dist/billing-foundation/checkout-core.js +3 -0
- package/dist/billing-foundation/constants.d.ts +38 -0
- package/dist/billing-foundation/constants.js +43 -0
- package/dist/billing-foundation/consumer-protection/confirmation-mail.d.ts +21 -0
- package/dist/billing-foundation/consumer-protection/confirmation-mail.js +125 -0
- package/dist/billing-foundation/consumer-protection/consent-gate.d.ts +32 -0
- package/dist/billing-foundation/consumer-protection/consent-gate.js +96 -0
- package/dist/billing-foundation/consumer-protection/consent-locale.d.ts +5 -0
- package/dist/billing-foundation/consumer-protection/consent-locale.js +17 -0
- package/dist/billing-foundation/consumer-protection/consent-text.d.ts +11 -0
- package/dist/billing-foundation/consumer-protection/consent-text.js +28 -0
- package/dist/billing-foundation/consumer-protection/issue-confirmation.d.ts +15 -0
- package/dist/billing-foundation/consumer-protection/issue-confirmation.js +149 -0
- package/dist/billing-foundation/consumer-protection/register-consent.d.ts +3 -0
- package/dist/billing-foundation/consumer-protection/register-consent.js +63 -0
- package/dist/billing-foundation/consumer-protection/register-termination.d.ts +3 -0
- package/dist/billing-foundation/consumer-protection/register-termination.js +40 -0
- package/dist/billing-foundation/consumer-protection/termination-mail.d.ts +45 -0
- package/dist/billing-foundation/consumer-protection/termination-mail.js +76 -0
- package/dist/billing-foundation/consumer-protection/termination-notify.d.ts +18 -0
- package/dist/billing-foundation/consumer-protection/termination-notify.js +21 -0
- package/dist/billing-foundation/consumer-protection/termination-pages.d.ts +8 -0
- package/dist/billing-foundation/consumer-protection/termination-pages.js +268 -0
- package/dist/billing-foundation/consumer-protection/termination-record.d.ts +73 -0
- package/dist/billing-foundation/consumer-protection/termination-record.js +230 -0
- package/dist/billing-foundation/consumer-protection/termination-request.d.ts +45 -0
- package/dist/billing-foundation/consumer-protection/termination-request.js +231 -0
- package/dist/billing-foundation/consumer-protection/termination-texts.d.ts +62 -0
- package/dist/billing-foundation/consumer-protection/termination-texts.js +168 -0
- package/dist/billing-foundation/events.d.ts +105 -2
- package/dist/billing-foundation/events.js +88 -2
- package/dist/billing-foundation/feature.js +6 -2
- package/dist/billing-foundation/handlers/billing-plans.query.js +19 -1
- package/dist/billing-foundation/handlers/create-checkout-session.write.js +33 -2
- package/dist/billing-foundation/handlers/process-event.write.d.ts +2 -2
- package/dist/billing-foundation/handlers/process-event.write.js +43 -15
- package/dist/billing-foundation/handlers/process-payment-event.write.d.ts +2 -1
- package/dist/billing-foundation/handlers/process-payment-event.write.js +6 -2
- package/dist/billing-foundation/handlers/start-plan-checkout.write.js +17 -1
- package/dist/billing-foundation/handlers/sync-subscription.write.js +0 -1
- package/dist/billing-foundation/i18n.js +61 -0
- package/dist/billing-foundation/index.d.ts +3 -2
- package/dist/billing-foundation/index.js +2 -1
- package/dist/billing-foundation/tenant-destroy-hook.js +4 -4
- package/dist/billing-foundation/types.d.ts +54 -9
- package/dist/billing-foundation/validate-options.js +51 -10
- package/dist/billing-foundation/web/billing-plans-panel.js +40 -3
- package/dist/billing-foundation/web/cancel-contract-dialog.d.ts +7 -0
- package/dist/billing-foundation/web/cancel-contract-dialog.js +57 -0
- package/dist/billing-foundation/web/checkout-consent-dialog.d.ts +28 -0
- package/dist/billing-foundation/web/checkout-consent-dialog.js +38 -0
- package/dist/billing-foundation/webhook-handler.js +5 -2
- package/dist/cap-counter/with-cap-enforcement.js +7 -3
- package/dist/cap-overview/access.d.ts +2 -1
- package/dist/cap-overview/access.js +26 -5
- package/dist/cap-overview/feature.d.ts +4 -0
- package/dist/cap-overview/feature.js +8 -6
- package/dist/cap-overview/handlers/caps-usage.query.d.ts +1 -1
- package/dist/cap-overview/handlers/caps-usage.query.js +6 -4
- package/dist/cap-overview/handlers/tenant-caps-list.query.js +12 -7
- package/dist/cap-overview/index.d.ts +2 -2
- package/dist/cap-overview/index.js +2 -2
- package/dist/cap-overview/screens.d.ts +1 -1
- package/dist/cap-overview/screens.js +17 -16
- package/dist/channel-discord/discord-channel.d.ts +6 -0
- package/dist/channel-discord/discord-channel.js +19 -0
- package/dist/channel-discord/feature.d.ts +3 -0
- package/dist/channel-discord/feature.js +30 -0
- package/dist/channel-discord/index.d.ts +2 -0
- package/dist/channel-discord/index.js +2 -0
- package/dist/channel-slack/feature.d.ts +3 -0
- package/dist/channel-slack/feature.js +30 -0
- package/dist/channel-slack/index.d.ts +2 -0
- package/dist/channel-slack/index.js +2 -0
- package/dist/channel-slack/slack-channel.d.ts +5 -0
- package/dist/channel-slack/slack-channel.js +16 -0
- package/dist/channel-teams/feature.d.ts +3 -0
- package/dist/channel-teams/feature.js +34 -0
- package/dist/channel-teams/index.d.ts +2 -0
- package/dist/channel-teams/index.js +2 -0
- package/dist/channel-teams/teams-channel.d.ts +5 -0
- package/dist/channel-teams/teams-channel.js +38 -0
- package/dist/channel-telegram/feature.d.ts +3 -0
- package/dist/channel-telegram/feature.js +28 -0
- package/dist/channel-telegram/index.d.ts +2 -0
- package/dist/channel-telegram/index.js +2 -0
- package/dist/channel-telegram/telegram-channel.d.ts +8 -0
- package/dist/channel-telegram/telegram-channel.js +54 -0
- package/dist/config/handlers/set.write.js +18 -2
- package/dist/crypto-shredding/feature.js +3 -2
- package/dist/crypto-shredding/handlers/forget-subject.write.d.ts +1 -1
- package/dist/crypto-shredding/handlers/forget-subject.write.js +30 -19
- package/dist/custom-fields/feature.js +9 -6
- package/dist/custom-fields/handlers/clear-custom-field.write.js +2 -1
- package/dist/custom-fields/handlers/define-tenant-field.write.js +2 -1
- package/dist/custom-fields/handlers/set-custom-field.write.js +2 -1
- package/dist/custom-fields/wire-user-data-rights.js +1 -1
- package/dist/data-retention/feature.js +1 -1
- package/dist/delivery/channel-context.d.ts +2 -1
- package/dist/delivery/channel-context.js +2 -2
- package/dist/delivery/chat-webhook-channel.d.ts +19 -0
- package/dist/delivery/chat-webhook-channel.js +48 -0
- package/dist/delivery/chat-webhook-sender.d.ts +21 -0
- package/dist/delivery/chat-webhook-sender.js +102 -0
- package/dist/delivery/delivery-service.js +12 -7
- package/dist/delivery/index.d.ts +2 -0
- package/dist/delivery/index.js +2 -0
- package/dist/delivery/jobs.js +1 -1
- package/dist/delivery/testing.d.ts +2 -0
- package/dist/delivery/types.d.ts +3 -1
- package/dist/delivery/types.js +1 -2
- package/dist/document-ingest-foundation/feature.js +3 -2
- package/dist/file-derivatives/handlers/public-variant-by-file-ref.query.js +1 -1
- package/dist/files-tenant-data/index.js +1 -1
- package/dist/form-draft/feature.js +1 -1
- package/dist/form-draft/handlers/discard.write.js +2 -1
- package/dist/form-draft/handlers/save.write.js +2 -1
- package/dist/inbound-mail-foundation/connect-routes.d.ts +7 -0
- package/dist/inbound-mail-foundation/connect-routes.js +13 -8
- package/dist/inbound-mail-foundation/feature.js +1 -1
- package/dist/inbound-mail-foundation/handlers/ingest-message.write.js +2 -1
- package/dist/inbound-mail-foundation/index.d.ts +1 -0
- package/dist/inbound-mail-foundation/index.js +1 -0
- package/dist/inbound-mail-foundation/oauth-access-token.d.ts +28 -0
- package/dist/inbound-mail-foundation/oauth-access-token.js +131 -0
- package/dist/inbound-mail-foundation/tenant-destroy-hook.js +1 -1
- package/dist/inbound-mail-foundation/types.d.ts +18 -1
- package/dist/inbound-mail-foundation/watch-supervisor.d.ts +5 -0
- package/dist/inbound-mail-foundation/watch-supervisor.js +42 -14
- package/dist/managed-pages/handlers/set.write.js +2 -1
- package/dist/notes-history/executor.d.ts +2 -0
- package/dist/notes-history/executor.js +9 -1
- package/dist/notes-history/feature.d.ts +2 -3
- package/dist/notes-history/feature.js +2 -1
- package/dist/notes-history/handlers/add-note.write.d.ts +2 -1
- package/dist/notes-history/handlers/add-note.write.js +21 -4
- package/dist/notes-history-user-data/hooks.js +26 -8
- package/dist/renderer-simple/feature.d.ts +8 -2
- package/dist/renderer-simple/feature.js +8 -5
- package/dist/renderer-simple/index.d.ts +2 -2
- package/dist/renderer-simple/index.js +1 -1
- package/dist/renderer-simple/simple-renderer.d.ts +15 -0
- package/dist/renderer-simple/simple-renderer.js +74 -27
- package/dist/secrets/feature.js +1 -1
- package/dist/sessions/feature.js +1 -1
- package/dist/sessions/handlers/revoke-all-for-user.write.js +2 -1
- package/dist/subscription-mollie/plugin-methods.js +1 -0
- package/dist/subscription-mollie/verify-webhook.js +5 -3
- package/dist/subscription-stripe/plugin-methods.d.ts +3 -1
- package/dist/subscription-stripe/plugin-methods.js +78 -13
- package/dist/subscription-stripe/verify-webhook.js +7 -3
- package/dist/tags/executor.d.ts +2 -0
- package/dist/tags/executor.js +9 -1
- package/dist/tags/feature.d.ts +4 -3
- package/dist/tags/feature.js +4 -2
- package/dist/tags/handlers/assign-tag.write.d.ts +2 -1
- package/dist/tags/handlers/assign-tag.write.js +4 -4
- package/dist/tags/handlers/remove-tag.write.d.ts +2 -1
- package/dist/tags/handlers/remove-tag.write.js +3 -3
- package/dist/tenant-handover/handlers/claim.write.js +2 -2
- package/dist/tenant-lifecycle/feature.js +1 -1
- package/dist/tenant-lifecycle/handlers/cancel-destruction.write.js +4 -1
- package/dist/tenant-lifecycle/handlers/request-destruction.write.js +2 -1
- package/dist/tier-engine/feature.js +1 -1
- package/dist/tier-engine/handlers/get-tenant-tier.query.js +2 -1
- package/dist/tier-engine/handlers/set-tenant-tier.write.js +2 -1
- package/dist/user/handlers/detail.query.js +1 -0
- package/dist/user/handlers/list.query.js +1 -0
- package/dist/user-data-rights/feature.js +2 -2
- package/dist/user-data-rights/handlers/cancel-deletion.write.js +2 -1
- package/dist/user-data-rights/handlers/confirm-deletion-by-token.write.js +1 -1
- package/dist/user-data-rights/handlers/download-attempt-list.query.js +1 -0
- package/dist/user-data-rights/handlers/download-by-job.query.js +2 -1
- package/dist/user-data-rights/handlers/download-by-token.query.js +2 -1
- package/dist/user-data-rights/handlers/export-status.query.js +2 -1
- package/dist/user-data-rights/handlers/lift-restriction.write.js +3 -4
- package/dist/user-data-rights/handlers/my-audit-log.query.js +2 -1
- package/dist/user-data-rights/handlers/request-deletion-by-email.write.js +2 -1
- package/dist/user-data-rights/handlers/request-deletion.write.js +1 -1
- package/dist/user-data-rights/handlers/request-export.write.js +2 -1
- package/dist/user-data-rights/handlers/restrict-account.write.js +2 -2
- package/dist/user-data-rights/handlers/run-forget-cleanup.write.js +2 -1
- package/dist/user-data-rights/lib/update-user-lifecycle.js +1 -1
- package/dist/user-data-rights/run-forget-cleanup.js +30 -15
- package/dist/workflow-runner/event-subscriber.js +6 -2
- package/dist/workflow-runner/event-trigger.js +5 -0
- package/dist/workflow-runner/handlers/resume-run.write.js +147 -59
- package/dist/workflow-runner/pending-projection.js +1 -0
- package/dist/workflow-runner/runner.d.ts +7 -1
- package/dist/workflow-runner/runner.js +1 -1
- package/dist/workflow-runner/tables.js +8 -1
- package/package.json +41 -9
- package/src/agent-tools/changes.json +14 -0
- package/src/auth-email-password/changes.json +7 -0
- package/src/billing-foundation/changes.json +83 -0
- package/src/cap-counter/changes.json +7 -0
- package/src/cap-overview/changes.json +14 -0
- package/src/config/changes.json +7 -0
- package/src/crypto-shredding/changes.json +13 -0
- package/src/custom-fields/changes.json +7 -0
- package/src/delivery/changes.json +14 -0
- package/src/inbound-mail-foundation/changes.json +7 -0
- package/src/notes-history/changes.json +7 -0
- package/src/renderer-simple/changes.json +9 -1
- package/src/subscription-mollie/changes.json +13 -0
- package/src/subscription-stripe/changes.json +13 -0
- package/src/tags/changes.json +7 -0
- package/src/user-data-rights/changes.json +6 -0
- package/src/workflow-runner/changes.json +7 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { hasAccess } from "@cosmicdrift/kumiko-framework/engine";
|
|
1
|
+
import { hasAccess, normalizeAccessEntry } 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`).
|
|
@@ -74,14 +74,28 @@ function isDetailHandlerQn(qn, entityName) {
|
|
|
74
74
|
function isExplicitlyAgentHidden(def) {
|
|
75
75
|
return def.agent?.expose === false;
|
|
76
76
|
}
|
|
77
|
-
|
|
77
|
+
/** The model's filters feed the WHERE clause, and only the output is field-filtered — a field
|
|
78
|
+
* whose read access is role- or row-restricted would leak through `totalCount` and hit/miss
|
|
79
|
+
* probing. Only fields every one of these roles may read without a row condition are offered. */
|
|
80
|
+
function isReadableWithoutRowCondition(field, roles) {
|
|
81
|
+
const accessMap = normalizeAccessEntry(field.access?.read);
|
|
82
|
+
if (!accessMap || Object.keys(accessMap).length === 0)
|
|
83
|
+
return true;
|
|
84
|
+
return roles.every((role) => accessMap[role] === "all");
|
|
85
|
+
}
|
|
86
|
+
/** Entity CRUD tools are registry-derived, so an explicit `agent.risk` on the handler is read
|
|
87
|
+
* here; without a hint the read tools stay "low". */
|
|
88
|
+
function entityHandlerRisk(registry, qn) {
|
|
89
|
+
return registry.getAllQueryHandlers().get(qn)?.agent?.risk ?? "low";
|
|
90
|
+
}
|
|
91
|
+
function addToolsForListHandler(registry, qn, entityName, entity, roles, sink) {
|
|
78
92
|
const searchableFields = registry.getSearchableFields(entityName);
|
|
79
93
|
if (searchableFields.length > 0) {
|
|
80
94
|
const tool = buildSearchTool(entityName, searchableFields);
|
|
81
95
|
addTool(sink, tool, { kind: "search", entityName, qn });
|
|
82
96
|
}
|
|
83
97
|
for (const [fieldName, field] of Object.entries(entity.fields)) {
|
|
84
|
-
if (!isFilterable(field))
|
|
98
|
+
if (!isFilterable(field) || !isReadableWithoutRowCondition(field, roles))
|
|
85
99
|
continue;
|
|
86
100
|
const fieldSchema = jsonSchemaTypeForField(field);
|
|
87
101
|
if (!fieldSchema)
|
|
@@ -98,7 +112,7 @@ function entityDisplayLabel(entity, entityName, locale) {
|
|
|
98
112
|
return label;
|
|
99
113
|
return entityName;
|
|
100
114
|
}
|
|
101
|
-
function buildGetTool(entityName, qn, label) {
|
|
115
|
+
function buildGetTool(entityName, qn, label, risk) {
|
|
102
116
|
return {
|
|
103
117
|
tool: {
|
|
104
118
|
name: `get_${entityName}`,
|
|
@@ -110,7 +124,7 @@ function buildGetTool(entityName, qn, label) {
|
|
|
110
124
|
additionalProperties: false,
|
|
111
125
|
},
|
|
112
126
|
},
|
|
113
|
-
descriptor: { kind: "server", op: "query", qn, risk
|
|
127
|
+
descriptor: { kind: "server", op: "query", qn, risk, entity: entityName, detail: true },
|
|
114
128
|
};
|
|
115
129
|
}
|
|
116
130
|
function buildListInputSchema(searchableFields, filterableFields) {
|
|
@@ -140,7 +154,7 @@ function buildListInputSchema(searchableFields, filterableFields) {
|
|
|
140
154
|
properties["limit"] = { type: "integer", minimum: 1, maximum: 200 };
|
|
141
155
|
return { type: "object", properties, required: [], additionalProperties: false };
|
|
142
156
|
}
|
|
143
|
-
function buildListTool(entityName, qn, label, searchableFields, filterableFields) {
|
|
157
|
+
function buildListTool(entityName, qn, label, searchableFields, filterableFields, risk) {
|
|
144
158
|
const descriptionParts = [`List ${label} records.`];
|
|
145
159
|
if (searchableFields.length > 0) {
|
|
146
160
|
descriptionParts.push(`Free-text search over: ${searchableFields.join(", ")}.`);
|
|
@@ -159,16 +173,22 @@ function buildListTool(entityName, qn, label, searchableFields, filterableFields
|
|
|
159
173
|
kind: "server",
|
|
160
174
|
op: "query",
|
|
161
175
|
qn,
|
|
162
|
-
risk
|
|
176
|
+
risk,
|
|
163
177
|
entity: entityName,
|
|
164
178
|
list: { searchableFields, filterableFields },
|
|
165
179
|
},
|
|
166
180
|
};
|
|
167
181
|
}
|
|
168
|
-
function filterableFieldsOf(entity) {
|
|
182
|
+
function filterableFieldsOf(entity, definition, roles) {
|
|
169
183
|
if (!entity)
|
|
170
184
|
return [];
|
|
171
|
-
return entity.fields
|
|
185
|
+
return entity.fields
|
|
186
|
+
.filter((field) => field.filterable === true)
|
|
187
|
+
.map((field) => field.name)
|
|
188
|
+
.filter((name) => {
|
|
189
|
+
const fieldDefinition = definition?.fields[name];
|
|
190
|
+
return fieldDefinition !== undefined && isReadableWithoutRowCondition(fieldDefinition, roles);
|
|
191
|
+
});
|
|
172
192
|
}
|
|
173
193
|
function compareByCodePoint(a, b) {
|
|
174
194
|
return a < b ? -1 : a > b ? 1 : 0;
|
|
@@ -321,7 +341,7 @@ function addRegistrySearchTools(registry, roleFilter, denyQns, sink) {
|
|
|
321
341
|
const entity = registry.getEntity(entityName);
|
|
322
342
|
if (!entity)
|
|
323
343
|
continue;
|
|
324
|
-
addToolsForListHandler(registry, qn, entityName, entity, sink);
|
|
344
|
+
addToolsForListHandler(registry, qn, entityName, entity, roleFilter.roles, sink);
|
|
325
345
|
}
|
|
326
346
|
}
|
|
327
347
|
function collectEntityHandlerQns(registry, roleFilter, denyQns) {
|
|
@@ -352,21 +372,21 @@ function collectEntityHandlerQns(registry, roleFilter, denyQns) {
|
|
|
352
372
|
}
|
|
353
373
|
return { detailQnByEntity, listQnByEntity, entityListDetailQns };
|
|
354
374
|
}
|
|
355
|
-
function addGetTools(detailQnByEntity, entityByName, locale, sink) {
|
|
375
|
+
function addGetTools(registry, detailQnByEntity, entityByName, locale, sink) {
|
|
356
376
|
const getEntries = [...detailQnByEntity.entries()].sort((a, b) => compareByCodePoint(a[0], b[0]));
|
|
357
377
|
for (const [entityName, qn] of getEntries) {
|
|
358
378
|
const label = entityDisplayLabel(entityByName.get(entityName), entityName, locale);
|
|
359
|
-
const { tool, descriptor } = buildGetTool(entityName, qn, label);
|
|
379
|
+
const { tool, descriptor } = buildGetTool(entityName, qn, label, entityHandlerRisk(registry, qn));
|
|
360
380
|
addTool(sink, tool, descriptor);
|
|
361
381
|
}
|
|
362
382
|
}
|
|
363
|
-
function addListTools(registry, listQnByEntity, entityByName, locale, sink) {
|
|
383
|
+
function addListTools(registry, listQnByEntity, entityByName, roles, locale, sink) {
|
|
364
384
|
const listEntries = [...listQnByEntity.entries()].sort((a, b) => compareByCodePoint(a[0], b[0]));
|
|
365
385
|
for (const [entityName, qn] of listEntries) {
|
|
366
386
|
const label = entityDisplayLabel(entityByName.get(entityName), entityName, locale);
|
|
367
387
|
const searchableFields = registry.getSearchableFields(entityName);
|
|
368
|
-
const filterableFields = filterableFieldsOf(entityByName.get(entityName));
|
|
369
|
-
const { tool, descriptor } = buildListTool(entityName, qn, label, searchableFields, filterableFields);
|
|
388
|
+
const filterableFields = filterableFieldsOf(entityByName.get(entityName), registry.getEntity(entityName), roles);
|
|
389
|
+
const { tool, descriptor } = buildListTool(entityName, qn, label, searchableFields, filterableFields, entityHandlerRisk(registry, qn));
|
|
370
390
|
addTool(sink, tool, descriptor);
|
|
371
391
|
}
|
|
372
392
|
}
|
|
@@ -455,8 +475,8 @@ export function buildToolCatalog(registry, manifest, options) {
|
|
|
455
475
|
const denyQns = new Set(options.denyQns ?? []);
|
|
456
476
|
addRegistrySearchTools(registry, roleFilter, denyQns, sink);
|
|
457
477
|
const { detailQnByEntity, listQnByEntity, entityListDetailQns } = collectEntityHandlerQns(registry, roleFilter, denyQns);
|
|
458
|
-
addGetTools(detailQnByEntity, entityByName, locale, sink);
|
|
459
|
-
addListTools(registry, listQnByEntity, entityByName, locale, sink);
|
|
478
|
+
addGetTools(registry, detailQnByEntity, entityByName, locale, sink);
|
|
479
|
+
addListTools(registry, listQnByEntity, entityByName, roleFilter.roles, locale, sink);
|
|
460
480
|
addQueryHandlerTools(manifest, entityListDetailQns, denyQns, sink);
|
|
461
481
|
if (options.mode !== "read-only") {
|
|
462
482
|
addWriteHandlerTools(manifest, detailQnByEntity, denyQns, sink);
|
|
@@ -53,6 +53,7 @@ export function createInviteAcceptWithLoginHandler(opts = {}) {
|
|
|
53
53
|
access: { roles: ["anonymous"] },
|
|
54
54
|
rateLimit: { per: "ip+handler", limit: 20, windowSeconds: 60 },
|
|
55
55
|
escapeHatch: {
|
|
56
|
+
grants: ["unsafeRaw"],
|
|
56
57
|
reason: "Anonymous invite-accept has no session in the invited tenant yet — checks existing " +
|
|
57
58
|
"membership via ctx.queryAs(SYSTEM, tenant:query:memberships) of the invitation's tenant. " +
|
|
58
59
|
"Also reads the pending invitation by id (the invitee is not yet a member of the " +
|
|
@@ -74,7 +75,7 @@ export function createInviteAcceptWithLoginHandler(opts = {}) {
|
|
|
74
75
|
return invalidInviteToken();
|
|
75
76
|
let committed = false;
|
|
76
77
|
try {
|
|
77
|
-
const invitation = await fetchOne(ctx.db.unsafeRaw(
|
|
78
|
+
const invitation = await fetchOne(ctx.db.unsafeRaw(), tenantInvitationsTable, { id: invitationId });
|
|
78
79
|
if (!invitation || invitation.status !== INVITATION_STATUS.pending)
|
|
79
80
|
return invalidInviteToken();
|
|
80
81
|
const invitationTenantId = invitation.tenantId;
|
|
@@ -110,7 +111,7 @@ export function createInviteAcceptWithLoginHandler(opts = {}) {
|
|
|
110
111
|
if (!statusGate.ok)
|
|
111
112
|
return statusGate.result;
|
|
112
113
|
const userId = userRow.id;
|
|
113
|
-
const dbConn = ctx.db.unsafeRaw(
|
|
114
|
+
const dbConn = ctx.db.unsafeRaw();
|
|
114
115
|
const grant = await grantInvitedMembershipRole(dbConn, {
|
|
115
116
|
userId,
|
|
116
117
|
tenantId: invitationTenantId,
|
|
@@ -33,8 +33,6 @@ const InviteAcceptSchema = z.object({
|
|
|
33
33
|
});
|
|
34
34
|
const invitationExecutor = createEventStoreExecutor(tenantInvitationsTable, tenantInvitationEntity, { entityName: "tenant-invitation" });
|
|
35
35
|
const INVITE_ACCEPT_ESCAPE_HATCH_REASON = "reads the pending invitation by id; the invitee is not yet a member of the invitation's tenant. Adds the membership and accepts the invitation in the invitation's tenant, which differs from the caller's tenant.";
|
|
36
|
-
const READ_PENDING_INVITATION_REASON = "reads the pending invitation by id; the invitee is not yet a member of the invitation's tenant";
|
|
37
|
-
const ADD_MEMBERSHIP_INVITATION_TENANT_REASON = "adds the membership and accepts the invitation in the invitation's tenant, which differs from the caller's tenant";
|
|
38
36
|
export function createInviteAcceptHandler() {
|
|
39
37
|
return defineWriteHandler({
|
|
40
38
|
name: "invite-accept",
|
|
@@ -51,6 +49,7 @@ export function createInviteAcceptHandler() {
|
|
|
51
49
|
},
|
|
52
50
|
agent: { expose: false },
|
|
53
51
|
escapeHatch: {
|
|
52
|
+
grants: ["unsafeRaw"],
|
|
54
53
|
reason: INVITE_ACCEPT_ESCAPE_HATCH_REASON,
|
|
55
54
|
},
|
|
56
55
|
// kumiko-lint-ignore complexity-budget invite branches (auth/anon/burn) stay in one handler
|
|
@@ -66,7 +65,7 @@ export function createInviteAcceptHandler() {
|
|
|
66
65
|
return invalidInviteToken();
|
|
67
66
|
let committed = false;
|
|
68
67
|
try {
|
|
69
|
-
const invitation = await fetchOne(ctx.db.unsafeRaw(
|
|
68
|
+
const invitation = await fetchOne(ctx.db.unsafeRaw(), tenantInvitationsTable, { id: invitationId });
|
|
70
69
|
if (!invitation || invitation.status !== INVITATION_STATUS.pending)
|
|
71
70
|
return invalidInviteToken();
|
|
72
71
|
const invitationTenantId = invitation.tenantId;
|
|
@@ -87,7 +86,7 @@ export function createInviteAcceptHandler() {
|
|
|
87
86
|
}
|
|
88
87
|
// Executor instead of dispatcher.writeAs(addMember): addMember only
|
|
89
88
|
// accepts SystemAdmin, createSystemUser carries "system".
|
|
90
|
-
const dbConn = ctx.db.unsafeRaw(
|
|
89
|
+
const dbConn = ctx.db.unsafeRaw();
|
|
91
90
|
const grant = await grantInvitedMembershipRole(dbConn, {
|
|
92
91
|
userId: event.user.id,
|
|
93
92
|
tenantId: invitationTenantId,
|
|
@@ -34,6 +34,7 @@ export const inviteInfoQuery = defineQueryHandler({
|
|
|
34
34
|
rateLimit: { per: "ip+handler", limit: 20, windowSeconds: 60 },
|
|
35
35
|
agent: { expose: false },
|
|
36
36
|
escapeHatch: {
|
|
37
|
+
grants: ["unsafeRaw"],
|
|
37
38
|
reason: READ_PENDING_INVITATION_REASON,
|
|
38
39
|
},
|
|
39
40
|
description: "Anonymous, read-only invite lookup for the invite-acceptance page: reveals the invited " +
|
|
@@ -44,7 +45,9 @@ export const inviteInfoQuery = defineQueryHandler({
|
|
|
44
45
|
const invitationId = await getInvitationIdForToken(ctx.redis, query.payload.token);
|
|
45
46
|
if (!invitationId)
|
|
46
47
|
throwInvalidInviteToken();
|
|
47
|
-
const invitation = await fetchOne(ctx.db.unsafeRaw(
|
|
48
|
+
const invitation = await fetchOne(ctx.db.unsafeRaw(), tenantInvitationsTable, {
|
|
49
|
+
id: invitationId,
|
|
50
|
+
});
|
|
48
51
|
if (!invitation || invitation.status !== INVITATION_STATUS.pending) {
|
|
49
52
|
throwInvalidInviteToken();
|
|
50
53
|
}
|
|
@@ -41,8 +41,6 @@ const InviteSignupCompleteSchema = z.object({
|
|
|
41
41
|
});
|
|
42
42
|
const invitationExecutor = createEventStoreExecutor(tenantInvitationsTable, tenantInvitationEntity, { entityName: "tenant-invitation" });
|
|
43
43
|
const INVITE_SIGNUP_COMPLETE_ESCAPE_HATCH_REASON = "reads the pending invitation by id; the invitee is not yet a member of the invitation's tenant. Creates the user and adds the membership and accepts the invitation in the invitation's tenant, before any caller tenant context exists.";
|
|
44
|
-
const READ_PENDING_INVITATION_REASON = "reads the pending invitation by id; the invitee is not yet a member of the invitation's tenant";
|
|
45
|
-
const CREATE_USER_AND_MEMBERSHIP_INVITATION_TENANT_REASON = "creates the user and adds the membership and accepts the invitation in the invitation's tenant, before any caller tenant context exists";
|
|
46
44
|
export function createInviteSignupCompleteHandler() {
|
|
47
45
|
return defineWriteHandler({
|
|
48
46
|
name: "invite-signup-complete",
|
|
@@ -51,6 +49,7 @@ export function createInviteSignupCompleteHandler() {
|
|
|
51
49
|
rateLimit: { per: "ip+handler", limit: 20, windowSeconds: 60 },
|
|
52
50
|
agent: { expose: false },
|
|
53
51
|
escapeHatch: {
|
|
52
|
+
grants: ["unsafeRaw", "globalWrites"],
|
|
54
53
|
reason: INVITE_SIGNUP_COMPLETE_ESCAPE_HATCH_REASON,
|
|
55
54
|
},
|
|
56
55
|
handler: async (event, ctx) => {
|
|
@@ -65,7 +64,7 @@ export function createInviteSignupCompleteHandler() {
|
|
|
65
64
|
return invalidInviteToken();
|
|
66
65
|
let committed = false;
|
|
67
66
|
try {
|
|
68
|
-
const invitation = await fetchOne(ctx.db.unsafeRaw(
|
|
67
|
+
const invitation = await fetchOne(ctx.db.unsafeRaw(), tenantInvitationsTable, { id: invitationId });
|
|
69
68
|
if (!invitation || invitation.status !== INVITATION_STATUS.pending)
|
|
70
69
|
return invalidInviteToken();
|
|
71
70
|
const invitationTenantId = invitation.tenantId;
|
|
@@ -88,7 +87,7 @@ export function createInviteSignupCompleteHandler() {
|
|
|
88
87
|
// signup-confirm), emailVerified=true wegen Magic-Link.
|
|
89
88
|
// @cast-boundary db-runner — helpers use only the query API that
|
|
90
89
|
// DbConnection and DbTx share.
|
|
91
|
-
const dbConn = ctx.db.unsafeRaw(
|
|
90
|
+
const dbConn = ctx.db.unsafeRaw();
|
|
92
91
|
const { id: userId } = await seedUserWithPassword(dbConn, {
|
|
93
92
|
email: invitationEmail,
|
|
94
93
|
password: event.payload.password,
|
|
@@ -78,6 +78,7 @@ export function createSignupConfirmHandler() {
|
|
|
78
78
|
rateLimit: { per: "ip+handler", limit: 20, windowSeconds: 60 },
|
|
79
79
|
agent: { expose: false },
|
|
80
80
|
escapeHatch: {
|
|
81
|
+
grants: ["unsafeRaw", "systemIdentity"],
|
|
81
82
|
reason: SIGNUP_CONFIRM_PROVISION_REASON,
|
|
82
83
|
},
|
|
83
84
|
handler: async (event, ctx) => {
|
|
@@ -103,7 +104,7 @@ export function createSignupConfirmHandler() {
|
|
|
103
104
|
// Fallback bei Kollision (siehe generateUniqueName).
|
|
104
105
|
// @cast-boundary db-runner — helpers use only the query API that
|
|
105
106
|
// DbConnection and DbTx share.
|
|
106
|
-
const dbConn = ctx.db.unsafeRaw(
|
|
107
|
+
const dbConn = ctx.db.unsafeRaw();
|
|
107
108
|
const tenantKey = await generateUniqueName({
|
|
108
109
|
isAvailable: async (slug) => {
|
|
109
110
|
const existing = await fetchOne(dbConn, tenantTable, { key: slug });
|
|
@@ -55,7 +55,7 @@ export function createSignupRequestHandler(opts) {
|
|
|
55
55
|
access: { roles: ["anonymous"] },
|
|
56
56
|
rateLimit: { per: "ip+handler", limit: 10, windowSeconds: 60 },
|
|
57
57
|
description: "Starts magic-link self-registration by mailing a fresh activation link to an address and invalidating any link still outstanding for it; the answer looks the same whether or not the address is already registered.",
|
|
58
|
-
escapeHatch: { reason: HANDOVER_GRANT_VERIFY_REASON },
|
|
58
|
+
escapeHatch: { reason: HANDOVER_GRANT_VERIFY_REASON, grants: ["unsafeRaw"] },
|
|
59
59
|
handler: async (event, ctx) => {
|
|
60
60
|
// Silent no-op when off, matching the route's own always-200
|
|
61
61
|
// anti-enumeration contract (registerTokenRequestRoute swallows every
|
|
@@ -123,7 +123,7 @@ export function createSignupRequestHandler(opts) {
|
|
|
123
123
|
else {
|
|
124
124
|
try {
|
|
125
125
|
const verified = await provider.verifyGrant({
|
|
126
|
-
db: ctx.db.unsafeRaw(
|
|
126
|
+
db: ctx.db.unsafeRaw(),
|
|
127
127
|
registry: ctx.registry,
|
|
128
128
|
entityType,
|
|
129
129
|
token: grantToken,
|
|
@@ -23,9 +23,19 @@ export type LoginRouteOptions = {
|
|
|
23
23
|
readonly onAuthenticated?: () => void;
|
|
24
24
|
};
|
|
25
25
|
export declare function createLoginRoute(opts?: LoginRouteOptions): ComponentType<Record<string, never>>;
|
|
26
|
-
export
|
|
26
|
+
export type AuthGateOptions = LoginRouteOptions & {
|
|
27
|
+
/** Login page outside the SPA (root-relative path or http(s) URL). Unauthenticated
|
|
28
|
+
* visitors are sent there with `next=<current path>`; the built-in login screen
|
|
29
|
+
* is not rendered. */
|
|
30
|
+
readonly loginUrl?: string;
|
|
31
|
+
};
|
|
32
|
+
export type SessionAuthGateOptions = AuthGateOptions & {
|
|
33
|
+
/** Where logout navigates to (root-relative path or http(s) URL). Without it, logout reloads the page. */
|
|
34
|
+
readonly postLogoutUrl?: string;
|
|
35
|
+
};
|
|
36
|
+
export declare function makeAuthGate(opts?: AuthGateOptions): ComponentType<{
|
|
27
37
|
children: ReactNode;
|
|
28
38
|
}>;
|
|
29
|
-
export declare function makeSessionAuthGate(opts?:
|
|
39
|
+
export declare function makeSessionAuthGate(opts?: SessionAuthGateOptions): ComponentType<{
|
|
30
40
|
children: ReactNode;
|
|
31
41
|
}>;
|
|
@@ -10,6 +10,7 @@ import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
|
|
|
10
10
|
// (nur `{ children }`-Prop). Der Sample kann so einen eigenen Login-
|
|
11
11
|
// Screen rein konfigurieren, ohne den Gate selbst ersetzen zu müssen.
|
|
12
12
|
import { useEffect, useState } from "react";
|
|
13
|
+
import { assertNavigableUrl, buildLoginRedirectUrl } from "./auth-redirect.js";
|
|
13
14
|
import { LoginScreen } from "./login-screen.js";
|
|
14
15
|
import { SessionProvider, useSession } from "./session.js";
|
|
15
16
|
import { SessionBootstrapErrorScreen } from "./session-bootstrap-error.js";
|
|
@@ -77,12 +78,32 @@ export function createLoginRoute(opts = {}) {
|
|
|
77
78
|
}
|
|
78
79
|
return LoginRoute;
|
|
79
80
|
}
|
|
81
|
+
function redirectToLoginUrl(loginUrl) {
|
|
82
|
+
const { origin, pathname, search, hash } = window.location;
|
|
83
|
+
// Compared before `next` is appended: the target always differs from the current
|
|
84
|
+
// URL once it carries next, so a gate mounted on the login page itself would loop.
|
|
85
|
+
const loginLocation = new URL(loginUrl, origin);
|
|
86
|
+
if (loginLocation.origin === origin && loginLocation.pathname === pathname)
|
|
87
|
+
return;
|
|
88
|
+
window.location.replace(buildLoginRedirectUrl(loginUrl, `${pathname}${search}${hash}`, origin));
|
|
89
|
+
}
|
|
80
90
|
export function makeAuthGate(opts = {}) {
|
|
91
|
+
const { loginUrl } = opts;
|
|
92
|
+
if (loginUrl !== undefined)
|
|
93
|
+
assertNavigableUrl(loginUrl, "loginUrl");
|
|
81
94
|
const LoginRoute = createLoginRoute(opts);
|
|
82
95
|
function AuthGate({ children }) {
|
|
83
96
|
const { status } = useSession();
|
|
97
|
+
const redirectsToLoginUrl = loginUrl !== undefined && status === "unauthenticated";
|
|
98
|
+
// kumiko-lint-ignore no-raw-hooks Phase-3 conversion tracked in #2312
|
|
99
|
+
useEffect(() => {
|
|
100
|
+
if (redirectsToLoginUrl)
|
|
101
|
+
redirectToLoginUrl(loginUrl);
|
|
102
|
+
}, [redirectsToLoginUrl]);
|
|
84
103
|
if (status === "authenticated")
|
|
85
104
|
return _jsx(_Fragment, { children: children });
|
|
105
|
+
if (redirectsToLoginUrl)
|
|
106
|
+
return null;
|
|
86
107
|
return _jsx(LoginRoute, {});
|
|
87
108
|
}
|
|
88
109
|
return AuthGate;
|
|
@@ -91,9 +112,12 @@ export function makeAuthGate(opts = {}) {
|
|
|
91
112
|
// /calculator) doesn't mount the session bootstrap — createKumikoApp stacks
|
|
92
113
|
// providers outside all gates, so SessionProvider can't be a provider anymore.
|
|
93
114
|
export function makeSessionAuthGate(opts = {}) {
|
|
115
|
+
const { postLogoutUrl } = opts;
|
|
116
|
+
if (postLogoutUrl !== undefined)
|
|
117
|
+
assertNavigableUrl(postLogoutUrl, "postLogoutUrl");
|
|
94
118
|
const AuthGate = makeAuthGate(opts);
|
|
95
119
|
function SessionAuthGate({ children }) {
|
|
96
|
-
return (_jsx(SessionProvider, { children: _jsx(AuthGate, { children: children }) }));
|
|
120
|
+
return (_jsx(SessionProvider, { postLogoutUrl: postLogoutUrl, children: _jsx(AuthGate, { children: children }) }));
|
|
97
121
|
}
|
|
98
122
|
return SessionAuthGate;
|
|
99
123
|
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export declare const NEXT_QUERY_PARAM = "next";
|
|
2
|
+
export declare function isSafeNextPath(candidate: unknown): candidate is string;
|
|
3
|
+
export declare function readNextFromSearch(search: string): string | null;
|
|
4
|
+
export declare function assertNavigableUrl(url: string, optionName: string): void;
|
|
5
|
+
export declare function buildLoginRedirectUrl(loginUrl: string, returnPath: string, currentOrigin: string): string;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// @runtime client
|
|
2
|
+
// Redirect targets for external login pages. `next` travels through the query
|
|
3
|
+
// string, so anything read back from it is untrusted: only same-origin
|
|
4
|
+
// root-relative paths pass (no "//host", no "/\host", no scheme).
|
|
5
|
+
export const NEXT_QUERY_PARAM = "next";
|
|
6
|
+
function hasControlOrWhitespace(candidate) {
|
|
7
|
+
for (let i = 0; i < candidate.length; i++) {
|
|
8
|
+
const code = candidate.charCodeAt(i);
|
|
9
|
+
if (code <= 0x20 || code === 0x7f)
|
|
10
|
+
return true;
|
|
11
|
+
}
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
// The URL parse is a backstop: browsers strip tabs/newlines, so "/\t/evil.com"
|
|
15
|
+
// would otherwise normalize to "//evil.com" past the leading-slash check.
|
|
16
|
+
export function isSafeNextPath(candidate) {
|
|
17
|
+
if (typeof candidate !== "string" || !candidate.startsWith("/"))
|
|
18
|
+
return false;
|
|
19
|
+
if (candidate[1] === "/" || candidate.includes("\\"))
|
|
20
|
+
return false;
|
|
21
|
+
if (hasControlOrWhitespace(candidate))
|
|
22
|
+
return false;
|
|
23
|
+
try {
|
|
24
|
+
const parsed = new URL(candidate, "https://next.invalid");
|
|
25
|
+
return parsed.origin === "https://next.invalid" && !parsed.pathname.startsWith("//");
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
export function readNextFromSearch(search) {
|
|
32
|
+
const candidate = new URLSearchParams(search).get(NEXT_QUERY_PARAM);
|
|
33
|
+
return isSafeNextPath(candidate) ? candidate : null;
|
|
34
|
+
}
|
|
35
|
+
// loginUrl/postLogoutUrl come from app code (trusted), but a typo like
|
|
36
|
+
// "javascript:" must fail at startup instead of becoming a navigation target.
|
|
37
|
+
export function assertNavigableUrl(url, optionName) {
|
|
38
|
+
// skip: a safe root-relative path needs no protocol check
|
|
39
|
+
if (url.startsWith("/") && isSafeNextPath(url))
|
|
40
|
+
return;
|
|
41
|
+
let protocol;
|
|
42
|
+
try {
|
|
43
|
+
protocol = new URL(url).protocol;
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
throw new Error(`${optionName} must be a root-relative path or an http(s) URL, got "${url}"`);
|
|
47
|
+
}
|
|
48
|
+
if (protocol !== "http:" && protocol !== "https:") {
|
|
49
|
+
throw new Error(`${optionName} must be a root-relative path or an http(s) URL, got "${url}"`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
export function buildLoginRedirectUrl(loginUrl, returnPath, currentOrigin) {
|
|
53
|
+
const target = new URL(loginUrl, currentOrigin);
|
|
54
|
+
if (isSafeNextPath(returnPath))
|
|
55
|
+
target.searchParams.set(NEXT_QUERY_PARAM, returnPath);
|
|
56
|
+
const isSameOrigin = target.origin === currentOrigin;
|
|
57
|
+
return isSameOrigin ? `${target.pathname}${target.search}${target.hash}` : target.href;
|
|
58
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { TranslationsByLocale } from "@cosmicdrift/kumiko-renderer";
|
|
2
2
|
import type { ComponentType, ReactNode } from "react";
|
|
3
|
-
import { type MfaSetupComponentProps, type MfaVerifyComponentProps } from "./auth-gate.js";
|
|
3
|
+
import { type AuthGateOptions, type MfaSetupComponentProps, type MfaVerifyComponentProps } from "./auth-gate.js";
|
|
4
4
|
import type { LoginScreenProps } from "./login-screen.js";
|
|
5
5
|
export type EmailPasswordClientOptions = {
|
|
6
6
|
/** Eigener Login-Screen. Default: der shadcn-stylte LoginScreen
|
|
@@ -19,6 +19,13 @@ export type EmailPasswordClientOptions = {
|
|
|
19
19
|
* as mfaVerifyScreen above. Apps not mounting auth-mfa simply don't
|
|
20
20
|
* pass this. */
|
|
21
21
|
readonly mfaSetupScreen?: ComponentType<MfaSetupComponentProps>;
|
|
22
|
+
/** Login page outside the SPA (root-relative path or http(s) URL). Unauthenticated
|
|
23
|
+
* visitors are redirected there with `next=<current path>` instead of seeing the
|
|
24
|
+
* built-in login screen. Read `next` back with readNextFromSearch (same-origin
|
|
25
|
+
* paths only). */
|
|
26
|
+
readonly loginUrl?: AuthGateOptions["loginUrl"];
|
|
27
|
+
/** Where logout navigates to (root-relative path or http(s) URL). Default: reload. */
|
|
28
|
+
readonly postLogoutUrl?: string;
|
|
22
29
|
/** Key-Overrides pro Locale. Wird mit den Default-Bundles (de/en)
|
|
23
30
|
* aus `translations.ts` gemerged — jeder hier gesetzte Key gewinnt.
|
|
24
31
|
* Für Branding ("Sign in" → "Login to Acme") oder weitere Sprachen
|
|
@@ -17,6 +17,8 @@ export function emailPasswordClient(options = {}) {
|
|
|
17
17
|
loginScreenProps: options.loginScreenProps,
|
|
18
18
|
mfaVerifyScreen: options.mfaVerifyScreen,
|
|
19
19
|
mfaSetupScreen: options.mfaSetupScreen,
|
|
20
|
+
loginUrl: options.loginUrl,
|
|
21
|
+
postLogoutUrl: options.postLogoutUrl,
|
|
20
22
|
}),
|
|
21
23
|
],
|
|
22
24
|
translations,
|
|
@@ -3,8 +3,9 @@ export type { AuthTokenFailure, CurrentUserProfile, LoginFailure, LoginRequest,
|
|
|
3
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
|
-
export type { LoginRouteOptions, MfaSetupComponentProps, MfaVerifyComponentProps, } from "./auth-gate.js";
|
|
6
|
+
export type { AuthGateOptions, LoginRouteOptions, MfaSetupComponentProps, MfaVerifyComponentProps, SessionAuthGateOptions, } from "./auth-gate.js";
|
|
7
7
|
export { createLoginRoute, makeAuthGate, makeSessionAuthGate } from "./auth-gate.js";
|
|
8
|
+
export { buildLoginRedirectUrl, isSafeNextPath, NEXT_QUERY_PARAM, readNextFromSearch, } from "./auth-redirect.js";
|
|
8
9
|
export type { EmailPasswordClientFeature, EmailPasswordClientOptions, } from "./client-plugin.js";
|
|
9
10
|
export { emailPasswordClient } from "./client-plugin.js";
|
|
10
11
|
export type { ConfirmAccountUnlockScreenProps } from "./confirm-account-unlock-screen.js";
|
|
@@ -8,6 +8,7 @@ export { defaultTranslations, mergeTranslations } from "../i18n.js";
|
|
|
8
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
|
+
export { buildLoginRedirectUrl, isSafeNextPath, NEXT_QUERY_PARAM, readNextFromSearch, } from "./auth-redirect.js";
|
|
11
12
|
export { emailPasswordClient } from "./client-plugin.js";
|
|
12
13
|
export { ConfirmAccountUnlockScreen } from "./confirm-account-unlock-screen.js";
|
|
13
14
|
export { DefaultTopbarActions } from "./default-topbar-actions.js";
|
|
@@ -40,7 +40,9 @@ export declare function computeActiveRoles(user: CurrentUserProfile | null, acti
|
|
|
40
40
|
* Code nutzt SessionProvider + useSession; direkter Context-Zugriff ist
|
|
41
41
|
* für Tests/Stories. */
|
|
42
42
|
export declare const SessionContext: import("react").Context<SessionApi | undefined>;
|
|
43
|
-
export declare function SessionProvider({ children }: {
|
|
43
|
+
export declare function SessionProvider({ children, postLogoutUrl, }: {
|
|
44
44
|
readonly children: ReactNode;
|
|
45
|
+
/** Logout navigates here instead of reloading. Validated by makeSessionAuthGate. */
|
|
46
|
+
readonly postLogoutUrl?: string;
|
|
45
47
|
}): ReactNode;
|
|
46
48
|
export declare function useSession(): SessionApi;
|
|
@@ -92,7 +92,7 @@ function toBootstrapFailure(err) {
|
|
|
92
92
|
}
|
|
93
93
|
return { httpStatus: null, retryAfterSeconds: null, failedAtEpochMs: Date.now() };
|
|
94
94
|
}
|
|
95
|
-
export function SessionProvider({ children }) {
|
|
95
|
+
export function SessionProvider({ children, postLogoutUrl, }) {
|
|
96
96
|
const [state, setState] = useState(INITIAL);
|
|
97
97
|
// Never rejects, so bootstrap can't strand the UI on "loading" forever.
|
|
98
98
|
const doRefresh = useCallback(async () => {
|
|
@@ -128,6 +128,12 @@ export function SessionProvider({ children }) {
|
|
|
128
128
|
}, [doRefresh]);
|
|
129
129
|
const logout = useCallback(async () => {
|
|
130
130
|
await logoutApi();
|
|
131
|
+
if (postLogoutUrl !== undefined && typeof window !== "undefined") {
|
|
132
|
+
// State stays untouched: flipping to unauthenticated would let an AuthGate
|
|
133
|
+
// with loginUrl race this navigation to its own redirect.
|
|
134
|
+
window.location.assign(postLogoutUrl);
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
131
137
|
setState(UNAUTHENTICATED);
|
|
132
138
|
// Hard-Reload: React-Tree, dispatcher-live-Caches, EventSource —
|
|
133
139
|
// alles fliegt auf Null. Nach Logout ist das der billigste Weg zu
|
|
@@ -136,7 +142,7 @@ export function SessionProvider({ children }) {
|
|
|
136
142
|
if (typeof window !== "undefined") {
|
|
137
143
|
window.location.reload();
|
|
138
144
|
}
|
|
139
|
-
}, []);
|
|
145
|
+
}, [postLogoutUrl]);
|
|
140
146
|
const switchTenant = useCallback(async (tenantId) => {
|
|
141
147
|
await switchTenantApi(tenantId);
|
|
142
148
|
// Tenant-Wechsel rotiert JWT + Cookies. React-Tree enthält
|
package/dist/auth-mfa/feature.js
CHANGED
|
@@ -105,7 +105,7 @@ export function createAuthMfaFeature(opts) {
|
|
|
105
105
|
escapeHatch: {
|
|
106
106
|
reason: REENCRYPT_ESCAPE_HATCH_REASON,
|
|
107
107
|
},
|
|
108
|
-
handler: (payload, ctx) => mfaReencryptJob(payload, ctx, ctx.db.unsafeRaw(
|
|
108
|
+
handler: (payload, ctx) => mfaReencryptJob(payload, ctx, ctx.db.unsafeRaw()),
|
|
109
109
|
});
|
|
110
110
|
r.entity("user-mfa", userMfaEntity);
|
|
111
111
|
// Late-bound by run-prod-app once the sessions feature (if mounted) has
|
|
@@ -91,7 +91,7 @@ export function createEnableConfirmPreauthHandler(opts) {
|
|
|
91
91
|
return invalidSetupToken();
|
|
92
92
|
// "system" mode: no session exists yet, tenantId comes from the
|
|
93
93
|
// verified token, mirroring enable-start-preauth.write.ts.
|
|
94
|
-
const scopedDb = createTenantDb(ctx.db.unsafeRaw(
|
|
94
|
+
const scopedDb = createTenantDb(ctx.db.unsafeRaw(), tenantId, "system");
|
|
95
95
|
const scopedUser = { id: userId, tenantId, roles: ["User"] };
|
|
96
96
|
const existing = await findUserMfaRow(scopedDb, scopedUser);
|
|
97
97
|
if (existing)
|
|
@@ -30,6 +30,7 @@ export function createEnableStartPreauthHandler(opts) {
|
|
|
30
30
|
// Same secret-bearing result as enable-start.
|
|
31
31
|
agent: { expose: false },
|
|
32
32
|
escapeHatch: {
|
|
33
|
+
grants: ["unsafeRaw"],
|
|
33
34
|
reason: ENABLE_START_PREAUTH_TENANT_REASON,
|
|
34
35
|
},
|
|
35
36
|
handler: async (event, ctx) => {
|
|
@@ -43,7 +44,7 @@ export function createEnableStartPreauthHandler(opts) {
|
|
|
43
44
|
// "system" mode: the guest dispatch identity's own tenantId is
|
|
44
45
|
// meaningless here — the preauthSetupToken is the source of truth
|
|
45
46
|
// for which tenant's row to read, mirroring verify.write.ts.
|
|
46
|
-
const scopedDb = createTenantDb(ctx.db.unsafeRaw(
|
|
47
|
+
const scopedDb = createTenantDb(ctx.db.unsafeRaw(), tenantId, "system");
|
|
47
48
|
const scopedUser = { id: userId, tenantId, roles: ["User"] };
|
|
48
49
|
const existing = await findUserMfaRow(scopedDb, scopedUser);
|
|
49
50
|
if (existing)
|
|
@@ -68,7 +68,7 @@ export function createMfaVerifyHandler(opts) {
|
|
|
68
68
|
// "system" mode: this handler runs with a guest identity whose own
|
|
69
69
|
// tenantId is meaningless here — the challenge token is the source
|
|
70
70
|
// of truth for which tenant's row to read.
|
|
71
|
-
const scopedDb = createTenantDb(ctx.db.unsafeRaw(
|
|
71
|
+
const scopedDb = createTenantDb(ctx.db.unsafeRaw(), tenantId, "system");
|
|
72
72
|
const scopedUser = { id: userId, tenantId, roles: ["User"] };
|
|
73
73
|
const row = await findUserMfaRow(scopedDb, scopedUser);
|
|
74
74
|
// MFA got disabled between login and verify (race, or a stale
|
|
@@ -14,7 +14,7 @@ export function createMfaCodeVerifier() {
|
|
|
14
14
|
declareEscapeHatch({
|
|
15
15
|
reason: "reads the MFA enrollment of the user being re-authenticated on the caller's handler context; the calling handler declares its own escapeHatch",
|
|
16
16
|
});
|
|
17
|
-
const scopedDb = createTenantDb(ctx.db.unsafeRaw(
|
|
17
|
+
const scopedDb = createTenantDb(ctx.db.unsafeRaw(), tenantId, "system");
|
|
18
18
|
const row = await findUserMfaRow(scopedDb, { id: userId, tenantId, roles: [] });
|
|
19
19
|
if (!row)
|
|
20
20
|
return { enrolled: false, ok: false };
|
|
@@ -10,7 +10,7 @@ export function createMfaStatusChecker(opts) {
|
|
|
10
10
|
declareEscapeHatch({
|
|
11
11
|
reason: "reads the MFA enrollment of the tenant named in the signed login/setup token, not the guest dispatch tenant, on the caller's handler context; the calling handler declares its own escapeHatch",
|
|
12
12
|
});
|
|
13
|
-
const scopedDb = createTenantDb(ctx.db.unsafeRaw(
|
|
13
|
+
const scopedDb = createTenantDb(ctx.db.unsafeRaw(), tenantId, "system");
|
|
14
14
|
const scopedUser = { id: userId, tenantId, roles: ["User"] };
|
|
15
15
|
const row = await findUserMfaRow(scopedDb, scopedUser);
|
|
16
16
|
if (row) {
|
|
@@ -18,3 +18,5 @@ export declare function paymentAggregateId(tenantId: string): string;
|
|
|
18
18
|
* providerEventId)` rather than the event-store's bigserial `event.id` (not
|
|
19
19
|
* a UUID) or the shared per-tenant aggregateId (one row per payment). */
|
|
20
20
|
export declare function paymentRowId(tenantId: string, providerName: string, providerEventId: string): string;
|
|
21
|
+
/** Stream id of one unmatched termination declaration, derived from its request id. */
|
|
22
|
+
export declare function terminationUnmatchedAggregateId(requestId: string): string;
|
|
@@ -16,6 +16,9 @@ const PAYMENT_NAMESPACE = "dec0b897-646d-4da3-be3c-e9a35a83f4e0";
|
|
|
16
16
|
* UUID, so it can't back a uuid-typed PK. Generated 2026-09-11, set in
|
|
17
17
|
* stone (same rationale as the two namespaces above). */
|
|
18
18
|
const PAYMENT_ROW_NAMESPACE = "3a1c9f4e-6b2d-4c8a-9e5f-7d4b1a2c8e6f";
|
|
19
|
+
/** Namespace for the system-tenant stream of an unmatched termination
|
|
20
|
+
* declaration (one stream per request). Generated 2026-10-03, set in stone. */
|
|
21
|
+
const TERMINATION_UNMATCHED_NAMESPACE = "7e2f4b9a-3c1d-4a68-8d05-b6c9e1f27a34";
|
|
19
22
|
/**
|
|
20
23
|
* Deterministic aggregate-id für die subscription eines Plattform-
|
|
21
24
|
* Tenants. EINE Subscription pro Tenant (Add-Ons sind line-items in
|
|
@@ -51,3 +54,8 @@ export function paymentRowId(tenantId, providerName, providerEventId) {
|
|
|
51
54
|
// the payment with no error anywhere.
|
|
52
55
|
return uuidv5(`${tenantId}:${providerName}:${providerEventId}`, PAYMENT_ROW_NAMESPACE);
|
|
53
56
|
}
|
|
57
|
+
/** Stream id of one unmatched termination declaration, derived from its request id. */
|
|
58
|
+
// @wrapper-known uuid-domain
|
|
59
|
+
export function terminationUnmatchedAggregateId(requestId) {
|
|
60
|
+
return uuidv5(requestId, TERMINATION_UNMATCHED_NAMESPACE);
|
|
61
|
+
}
|