@cosmicdrift/kumiko-samples 0.352.0 → 0.354.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/packages/bundled-features/package.json +18 -2
- package/packages/bundled-features/src/agent-tools/agent-doc-lint.ts +66 -0
- package/packages/bundled-features/src/agent-tools/agent-manifest.ts +26 -7
- package/packages/bundled-features/src/agent-tools/changes.json +12 -0
- package/packages/bundled-features/src/agent-tools/feature.ts +13 -2
- package/packages/bundled-features/src/agent-tools/index.ts +6 -1
- package/packages/bundled-features/src/agent-tools/manifest-deny-qns.ts +25 -0
- package/packages/bundled-features/src/agent-tools/tool-catalog.ts +4 -1
- package/packages/bundled-features/src/agent-tools/tool-dispatch.ts +1 -1
- package/packages/bundled-features/src/audit/changes.json +6 -0
- package/packages/bundled-features/src/audit/handlers/list.query.ts +20 -10
- package/packages/bundled-features/src/auth-email-password/changes.json +24 -0
- package/packages/bundled-features/src/auth-email-password/handlers/change-password.write.ts +1 -0
- package/packages/bundled-features/src/auth-email-password/handlers/invite-accept-with-login.write.ts +3 -2
- package/packages/bundled-features/src/auth-email-password/handlers/invite-accept.write.ts +3 -2
- package/packages/bundled-features/src/auth-email-password/handlers/invite-create.write.ts +21 -0
- package/packages/bundled-features/src/auth-email-password/handlers/invite-signup-complete.write.ts +4 -1
- package/packages/bundled-features/src/auth-email-password/handlers/login.write.ts +1 -0
- package/packages/bundled-features/src/auth-email-password/handlers/reset-password.write.ts +1 -0
- package/packages/bundled-features/src/auth-email-password/handlers/signup-confirm.write.ts +7 -3
- package/packages/bundled-features/src/auth-email-password/handlers/signup-request.write.ts +53 -96
- package/packages/bundled-features/src/auth-email-password/handlers/token-request-handler.ts +1 -0
- package/packages/bundled-features/src/auth-email-password/handlers/verify-email.write.ts +1 -0
- package/packages/bundled-features/src/auth-email-password/index.ts +12 -0
- package/packages/bundled-features/src/auth-email-password/seeding.ts +38 -7
- package/packages/bundled-features/src/auth-email-password/signup-activation.ts +83 -0
- package/packages/bundled-features/src/auth-email-password/signup-token-store.ts +9 -3
- package/packages/bundled-features/src/auth-email-password/web/auth-gate.tsx +7 -7
- package/packages/bundled-features/src/auth-email-password/web/auth-redirect.ts +15 -0
- package/packages/bundled-features/src/auth-email-password/web/login-screen.tsx +2 -2
- package/packages/bundled-features/src/auth-email-password/web/user-menu.tsx +1 -1
- package/packages/bundled-features/src/auth-mfa/changes.json +7 -0
- package/packages/bundled-features/src/auth-mfa/feature.ts +5 -0
- package/packages/bundled-features/src/auth-mfa/handlers/enable-confirm-preauth.write.ts +1 -0
- package/packages/bundled-features/src/auth-mfa/handlers/verify.write.ts +1 -0
- package/packages/bundled-features/src/auth-mfa/screens.ts +2 -2
- package/packages/bundled-features/src/auth-mfa/web/mfa-recovery-codes-reveal.tsx +6 -8
- package/packages/bundled-features/src/auth-mfa/web/mfa-setup-preauth-screen.tsx +18 -21
- package/packages/bundled-features/src/auth-mfa/web/mfa-verify-screen.tsx +2 -2
- package/packages/bundled-features/src/billing-foundation/billing-info-query.ts +7 -8
- package/packages/bundled-features/src/billing-foundation/changes.json +6 -0
- package/packages/bundled-features/src/billing-foundation/checkout-core.ts +14 -2
- package/packages/bundled-features/src/billing-foundation/handlers/start-plan-checkout.write.ts +7 -1
- package/packages/bundled-features/src/cap-overview/access.ts +2 -9
- package/packages/bundled-features/src/cap-overview/handlers/tenant-caps-list.query.ts +5 -2
- package/packages/bundled-features/src/cap-overview/handlers/tenant-options.query.ts +6 -9
- package/packages/bundled-features/src/channel-in-app/changes.json +8 -1
- package/packages/bundled-features/src/channel-in-app/in-app-channel.ts +1 -1
- package/packages/bundled-features/src/channel-in-app/tables.ts +18 -12
- package/packages/bundled-features/src/compliance-profiles-ops/handlers/tenants-missing-profile.query.ts +5 -4
- package/packages/bundled-features/src/config/handlers/values.query.ts +7 -2
- package/packages/bundled-features/src/crypto-shredding/changes.json +12 -0
- package/packages/bundled-features/src/crypto-shredding/handlers/forget-subject.write.ts +154 -69
- package/packages/bundled-features/src/delivery/changes.json +6 -0
- package/packages/bundled-features/src/delivery/db/queries/address-opt-outs.ts +18 -12
- package/packages/bundled-features/src/delivery/delivery-service.ts +103 -16
- package/packages/bundled-features/src/delivery/public-names.ts +1 -1
- package/packages/bundled-features/src/delivery/unsubscribe.ts +81 -13
- package/packages/bundled-features/src/feature-toggles/feature.ts +2 -3
- package/packages/bundled-features/src/feature-toggles/toggle-runtime.ts +34 -5
- package/packages/bundled-features/src/feature-toggles/toggle-sync-signal.ts +31 -8
- package/packages/bundled-features/src/file-derivatives/handlers/public-variant.query.ts +10 -1
- package/packages/bundled-features/src/files/changes.json +24 -0
- package/packages/bundled-features/src/files-tenant-data/changes.json +5 -0
- package/packages/bundled-features/src/files-tenant-data/handlers/sweep-orphaned-derivatives.job.ts +4 -0
- package/packages/bundled-features/src/files-tenant-data/hooks.ts +75 -25
- package/packages/bundled-features/src/folders/changes.json +6 -0
- package/packages/bundled-features/src/folders/feature.ts +2 -12
- package/packages/bundled-features/src/folders/handlers/delete-folder.write.ts +41 -9
- package/packages/bundled-features/src/form-draft/changes.json +7 -1
- package/packages/bundled-features/src/form-draft/db/queries/cleanup.ts +4 -2
- package/packages/bundled-features/src/form-draft/handlers/cleanup.job.ts +18 -6
- package/packages/bundled-features/src/foundation-shared/index.ts +1 -0
- package/packages/bundled-features/src/foundation-shared/mail-host-policy.ts +16 -4
- package/packages/bundled-features/src/inbound-provider-imap/changes.json +6 -0
- package/packages/bundled-features/src/inbound-provider-imap/feature.ts +2 -0
- package/packages/bundled-features/src/inbound-provider-imap/imap-client.ts +3 -4
- package/packages/bundled-features/src/jobs/changes.json +30 -0
- package/packages/bundled-features/src/jobs/constants.ts +1 -0
- package/packages/bundled-features/src/jobs/db/queries/retention.ts +12 -0
- package/packages/bundled-features/src/jobs/db/queries/stale-run-sweep.ts +26 -2
- package/packages/bundled-features/src/jobs/feature.ts +7 -1
- package/packages/bundled-features/src/jobs/handlers/list.query.ts +45 -12
- package/packages/bundled-features/src/jobs/handlers/tenant-failures.query.ts +1 -1
- package/packages/bundled-features/src/jobs/handlers/tenant-runs.query.ts +94 -0
- package/packages/bundled-features/src/jobs/i18n.ts +1 -0
- package/packages/bundled-features/src/jobs/index.ts +2 -0
- package/packages/bundled-features/src/jobs/job-run-logger.ts +177 -10
- package/packages/bundled-features/src/jobs/tenant-job-failure-table.ts +27 -8
- package/packages/bundled-features/src/jobs/tenant-job-run-table.ts +55 -0
- package/packages/bundled-features/src/ledger/changes.json +23 -0
- package/packages/bundled-features/src/ledger/entity.ts +17 -5
- package/packages/bundled-features/src/ledger/feature.ts +3 -0
- package/packages/bundled-features/src/ledger/handlers/confirm-schedule-period.write.ts +15 -3
- package/packages/bundled-features/src/ledger/handlers/create-transaction.write.ts +1 -0
- package/packages/bundled-features/src/ledger/handlers/reverse-transaction.write.ts +1 -0
- package/packages/bundled-features/src/ledger/schedule-subject-pair.ts +27 -0
- package/packages/bundled-features/src/ledger/schemas.ts +8 -2
- package/packages/bundled-features/src/mail-transport-smtp/feature.ts +4 -15
- package/packages/bundled-features/src/managed-pages/changes.json +6 -0
- package/packages/bundled-features/src/managed-pages/feature.ts +24 -11
- package/packages/bundled-features/src/notes-history/changes.json +13 -0
- package/packages/bundled-features/src/notes-history/entity.ts +4 -0
- package/packages/bundled-features/src/notes-history/feature.ts +2 -12
- package/packages/bundled-features/src/personal-access-tokens/handlers/create.write.ts +1 -0
- package/packages/bundled-features/src/personal-access-tokens/screens.ts +24 -3
- package/packages/bundled-features/src/seo/changes.json +5 -0
- package/packages/bundled-features/src/seo/feature.ts +23 -7
- package/packages/bundled-features/src/sessions/feature.ts +21 -8
- package/packages/bundled-features/src/sessions/handlers/detail.query.ts +34 -33
- package/packages/bundled-features/src/sessions/handlers/list.query.ts +68 -65
- package/packages/bundled-features/src/sessions/screens.ts +10 -8
- package/packages/bundled-features/src/shared/assert-no-where-rule-in-ownership-write.ts +25 -0
- package/packages/bundled-features/src/shared/encrypt-for-direct-write.ts +15 -8
- package/packages/bundled-features/src/shared/index.ts +4 -2
- package/packages/bundled-features/src/shared/parent-visibility.ts +31 -1
- package/packages/bundled-features/src/shared/rate-limited-text-response.ts +11 -0
- package/packages/bundled-features/src/shared/row-bound-grant.ts +18 -2
- package/packages/bundled-features/src/shared/session-locale-field.ts +7 -0
- package/packages/bundled-features/src/tags/feature.ts +2 -12
- package/packages/bundled-features/src/template-resolver/api.ts +3 -3
- package/packages/bundled-features/src/template-resolver/changes.json +5 -0
- package/packages/bundled-features/src/template-resolver/handlers/list.query.ts +2 -2
- package/packages/bundled-features/src/tenant/changes.json +19 -0
- package/packages/bundled-features/src/tenant/decrypt-tenant-names.ts +17 -0
- package/packages/bundled-features/src/tenant/feature.ts +8 -1
- package/packages/bundled-features/src/tenant/handlers/me.query.ts +4 -1
- package/packages/bundled-features/src/tenant/handlers/memberships.query.ts +7 -3
- package/packages/bundled-features/src/tenant/handlers/tenant-directory.query.ts +11 -5
- package/packages/bundled-features/src/tenant/handlers/toggle-enabled.write.ts +2 -0
- package/packages/bundled-features/src/tenant/handlers/update-member-roles.write.ts +1 -0
- package/packages/bundled-features/src/tenant/index.ts +1 -0
- package/packages/bundled-features/src/tenant/invitation-table.ts +6 -0
- package/packages/bundled-features/src/tenant/invited-membership.ts +45 -12
- package/packages/bundled-features/src/tenant/schema/tenant.ts +4 -6
- package/packages/bundled-features/src/tenant/screens.ts +1 -1
- package/packages/bundled-features/src/tenant-handover/changes.json +12 -0
- package/packages/bundled-features/src/tenant-handover/move-entity-graph.ts +12 -19
- package/packages/bundled-features/src/tenant-handover/transfer-graph.ts +14 -96
- package/packages/bundled-features/src/tenant-lifecycle/boot-checks.ts +16 -0
- package/packages/bundled-features/src/tenant-lifecycle/changes.json +22 -0
- package/packages/bundled-features/src/tenant-lifecycle/constants.ts +8 -0
- package/packages/bundled-features/src/tenant-lifecycle/events.ts +6 -0
- package/packages/bundled-features/src/tenant-lifecycle/feature.ts +14 -1
- package/packages/bundled-features/src/tenant-lifecycle/run-tenant-destroy.ts +105 -29
- package/packages/bundled-features/src/tenant-lifecycle/stages.ts +64 -10
- package/packages/bundled-features/src/tenant-lifecycle/testing.ts +58 -0
- package/packages/bundled-features/src/tier-engine/changes.json +5 -0
- package/packages/bundled-features/src/tier-engine/handlers/set-tenant-tier.write.ts +1 -0
- package/packages/bundled-features/src/user/changes.json +15 -0
- package/packages/bundled-features/src/user/handlers/detail.query.ts +1 -0
- package/packages/bundled-features/src/user/handlers/list.query.ts +8 -2
- package/packages/bundled-features/src/user/handlers/update.write.ts +1 -0
- package/packages/bundled-features/src/user/seeding.ts +34 -0
- package/packages/bundled-features/src/user-data-rights/boot-checks.ts +7 -0
- package/packages/bundled-features/src/user-data-rights/changes.json +6 -0
- package/packages/bundled-features/src/user-data-rights/deletion-token.ts +1 -1
- package/packages/bundled-features/src/user-data-rights/handlers/confirm-deletion-by-token.write.ts +2 -2
- package/packages/bundled-features/src/user-data-rights/handlers/request-deletion.write.ts +1 -0
- package/packages/bundled-features/src/user-data-rights/index.ts +1 -0
- package/packages/bundled-features/src/user-data-rights/schema/download-attempt.ts +17 -9
- package/packages/bundled-features/src/user-profile/handlers/change-email.write.ts +8 -2
- package/packages/bundled-features/src/workflow-runner/changes.json +5 -0
- package/packages/bundled-features/src/workflow-runner/event-trigger.ts +80 -14
- package/packages/bundled-features/src/workflow-runner/handlers/resume-run.write.ts +11 -1
- package/packages/bundled-features/src/workflow-runner/runner.ts +8 -2
- package/samples/apps/showcase/src/features/items/feature.ts +2 -1
- package/samples/apps/showcase/src/features/items/schema/item.ts +0 -1
- package/samples/apps/styleguide/src/features/widgets/demo-locale.ts +47 -0
- package/samples/apps/styleguide/src/features/widgets/feature.ts +131 -15
- package/samples/apps/styleguide/src/features/widgets/i18n.ts +40 -0
- package/samples/apps/styleguide/src/features/widgets/web/DashboardFilterEcho.tsx +5 -4
- package/samples/apps/styleguide/src/features/widgets/web/Widgets.tsx +4 -0
- package/samples/apps/ui-walkthrough/package.json +3 -0
- package/samples/apps/use-all-bundled/README.md +1 -2
- package/samples/apps/use-all-bundled/feature-manifest.json +1 -1
- package/samples/apps/user-data-rights-demo/README.md +1 -3
- package/samples/recipes/legal-pages/README.md +3 -4
- package/samples/recipes/managed-pages/README.md +2 -2
- package/samples/recipes/public-share-token/README.md +4 -5
- package/samples/recipes/public-share-token/src/feature.ts +1 -1
- package/samples/recipes/security-baseline/README.md +6 -0
- package/samples/recipes/seed-migration/README.md +0 -2
- package/samples/recipes/tenant-settings/README.md +1 -1
- package/packages/bundled-features/src/shared/has-where-rule.ts +0 -11
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-samples",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.354.0",
|
|
4
4
|
"description": "Source trees of the Kumiko sample recipes, sample apps and bundled features in repo layout, for tooling such as few-shot corpus builds.",
|
|
5
5
|
"license": "BUSL-1.1",
|
|
6
6
|
"author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-bundled-features",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.354.0",
|
|
4
4
|
"description": "Built-in features — tenant, user, auth, delivery. The stuff you'd rewrite anyway, already typed.",
|
|
5
5
|
"license": "BUSL-1.1",
|
|
6
6
|
"author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
|
|
@@ -17,7 +17,8 @@
|
|
|
17
17
|
"kumiko": {
|
|
18
18
|
"runtime": "runtime",
|
|
19
19
|
"bunOnlyExports": [
|
|
20
|
-
"./template-resolver/testing"
|
|
20
|
+
"./template-resolver/testing",
|
|
21
|
+
"./tenant-lifecycle/testing"
|
|
21
22
|
]
|
|
22
23
|
},
|
|
23
24
|
"exports": {
|
|
@@ -81,12 +82,14 @@
|
|
|
81
82
|
"./files-tenant-data": "./src/files-tenant-data/index.ts",
|
|
82
83
|
"./user-data-rights": "./src/user-data-rights/index.ts",
|
|
83
84
|
"./user-data-rights/web": "./src/user-data-rights/web/index.ts",
|
|
85
|
+
"./user-data-rights/email-templates": "./src/user-data-rights/email-templates.ts",
|
|
84
86
|
"./user-data-rights-defaults": "./src/user-data-rights-defaults/index.ts",
|
|
85
87
|
"./tenant": "./src/tenant/index.ts",
|
|
86
88
|
"./tenant/constants": "./src/tenant/constants.ts",
|
|
87
89
|
"./tenant/web": "./src/tenant/web/index.ts",
|
|
88
90
|
"./tenant/seeding": "./src/tenant/seeding.ts",
|
|
89
91
|
"./tenant-lifecycle": "./src/tenant-lifecycle/index.ts",
|
|
92
|
+
"./tenant-lifecycle/testing": "./src/tenant-lifecycle/testing.ts",
|
|
90
93
|
"./tenant-handover": "./src/tenant-handover/index.ts",
|
|
91
94
|
"./tenant/testing": "./src/tenant/testing.ts",
|
|
92
95
|
"./user": "./src/user/index.ts",
|
|
@@ -96,6 +99,7 @@
|
|
|
96
99
|
"./user-profile/web": "./src/user-profile/web/index.ts",
|
|
97
100
|
"./auth-email-password": "./src/auth-email-password/index.ts",
|
|
98
101
|
"./auth-email-password/constants": "./src/auth-email-password/constants.ts",
|
|
102
|
+
"./auth-email-password/email-templates": "./src/auth-email-password/email-templates.ts",
|
|
99
103
|
"./auth-email-password/seeding": "./src/auth-email-password/seeding.ts",
|
|
100
104
|
"./auth-email-password/testing": "./src/auth-email-password/testing.ts",
|
|
101
105
|
"./auth-email-password/web": "./src/auth-email-password/web/index.ts",
|
|
@@ -411,6 +415,10 @@
|
|
|
411
415
|
"types": "./dist/user-data-rights/index.d.ts",
|
|
412
416
|
"default": "./dist/user-data-rights/index.js"
|
|
413
417
|
},
|
|
418
|
+
"./user-data-rights/email-templates": {
|
|
419
|
+
"types": "./dist/user-data-rights/email-templates.d.ts",
|
|
420
|
+
"default": "./dist/user-data-rights/email-templates.js"
|
|
421
|
+
},
|
|
414
422
|
"./user-data-rights/web": {
|
|
415
423
|
"types": "./dist/user-data-rights/web/index.d.ts",
|
|
416
424
|
"default": "./dist/user-data-rights/web/index.js"
|
|
@@ -439,6 +447,10 @@
|
|
|
439
447
|
"types": "./dist/tenant-lifecycle/index.d.ts",
|
|
440
448
|
"default": "./dist/tenant-lifecycle/index.js"
|
|
441
449
|
},
|
|
450
|
+
"./tenant-lifecycle/testing": {
|
|
451
|
+
"types": "./dist/tenant-lifecycle/testing.d.ts",
|
|
452
|
+
"default": "./dist/tenant-lifecycle/testing.js"
|
|
453
|
+
},
|
|
442
454
|
"./tenant-handover": {
|
|
443
455
|
"types": "./dist/tenant-handover/index.d.ts",
|
|
444
456
|
"default": "./dist/tenant-handover/index.js"
|
|
@@ -471,6 +483,10 @@
|
|
|
471
483
|
"types": "./dist/auth-email-password/index.d.ts",
|
|
472
484
|
"default": "./dist/auth-email-password/index.js"
|
|
473
485
|
},
|
|
486
|
+
"./auth-email-password/email-templates": {
|
|
487
|
+
"types": "./dist/auth-email-password/email-templates.d.ts",
|
|
488
|
+
"default": "./dist/auth-email-password/email-templates.js"
|
|
489
|
+
},
|
|
474
490
|
"./auth-email-password/constants": {
|
|
475
491
|
"types": "./dist/auth-email-password/constants.d.ts",
|
|
476
492
|
"default": "./dist/auth-email-password/constants.js"
|
|
@@ -4,6 +4,8 @@ import type {
|
|
|
4
4
|
WriteHandlerDef,
|
|
5
5
|
} from "@cosmicdrift/kumiko-framework/engine";
|
|
6
6
|
import {
|
|
7
|
+
handlerFieldLabelKey,
|
|
8
|
+
handlerTitleKey,
|
|
7
9
|
isAgentVisibleScreen,
|
|
8
10
|
QnTypes,
|
|
9
11
|
qn,
|
|
@@ -11,12 +13,14 @@ import {
|
|
|
11
13
|
toKebab,
|
|
12
14
|
} from "@cosmicdrift/kumiko-framework/engine";
|
|
13
15
|
import * as z from "zod";
|
|
16
|
+
import { systemOnlyPropertyNames } from "./agent-manifest.js";
|
|
14
17
|
|
|
15
18
|
export const AgentDocGapKinds = {
|
|
16
19
|
handlerSchemaNotExpressible: "handler-schema-not-expressible",
|
|
17
20
|
handlerWithoutDescription: "handler-without-description",
|
|
18
21
|
customScreenWithoutDescription: "custom-screen-without-description",
|
|
19
22
|
exposedEntityWithoutDescription: "exposed-entity-without-description",
|
|
23
|
+
handlerWithoutTranslation: "handler-without-translation",
|
|
20
24
|
} as const;
|
|
21
25
|
|
|
22
26
|
export type AgentDocGapKind = (typeof AgentDocGapKinds)[keyof typeof AgentDocGapKinds];
|
|
@@ -144,6 +148,58 @@ function exposedEntityDocGaps(feature: FeatureDefinition): readonly AgentDocGap[
|
|
|
144
148
|
return gaps;
|
|
145
149
|
}
|
|
146
150
|
|
|
151
|
+
function actionFormHandlerQns(features: readonly FeatureDefinition[]): ReadonlySet<string> {
|
|
152
|
+
const handlerQns = new Set<string>();
|
|
153
|
+
for (const feature of features) {
|
|
154
|
+
for (const screen of Object.values(feature.screens ?? {})) {
|
|
155
|
+
if (screen.type === "actionForm") handlerQns.add(screen.handler);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return handlerQns;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function agentVisibleInputFieldNames(schema: z.ZodType): readonly string[] {
|
|
162
|
+
if (!(schema instanceof z.ZodObject)) return [];
|
|
163
|
+
const fieldNames = Object.keys(schema.shape);
|
|
164
|
+
try {
|
|
165
|
+
const systemOnly = new Set(systemOnlyPropertyNames(z.toJSONSchema(schema, { io: "input" })));
|
|
166
|
+
return fieldNames.filter((field) => !systemOnly.has(field));
|
|
167
|
+
} catch {
|
|
168
|
+
// skip: a schema without JSON Schema form is reported as its own gap kind
|
|
169
|
+
return fieldNames;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function translationGapsOfFeature(
|
|
174
|
+
feature: FeatureDefinition,
|
|
175
|
+
coveredHandlerQns: ReadonlySet<string>,
|
|
176
|
+
): readonly AgentDocGap[] {
|
|
177
|
+
const gaps: AgentDocGap[] = [];
|
|
178
|
+
// The client resolves translation keys verbatim (no feature prefix), so only
|
|
179
|
+
// fully spelled-out keys count.
|
|
180
|
+
const translated = new Set(Object.keys(feature.translations ?? {}));
|
|
181
|
+
for (const [name, def] of Object.entries(feature.writeHandlers ?? {})) {
|
|
182
|
+
if (!resolveAgentExposure(def, "write").expose) continue;
|
|
183
|
+
if (feature.handlerEntityMappings?.[name] !== undefined) continue;
|
|
184
|
+
const handlerQn = qn(toKebab(feature.name), QnTypes.write, toKebab(name));
|
|
185
|
+
if (coveredHandlerQns.has(handlerQn)) continue;
|
|
186
|
+
const missing = [
|
|
187
|
+
handlerTitleKey(handlerQn),
|
|
188
|
+
...agentVisibleInputFieldNames(def.schema).map((field) =>
|
|
189
|
+
handlerFieldLabelKey(handlerQn, field),
|
|
190
|
+
),
|
|
191
|
+
].filter((key) => !translated.has(key));
|
|
192
|
+
if (missing.length === 0) continue;
|
|
193
|
+
gaps.push({
|
|
194
|
+
qn: handlerQn,
|
|
195
|
+
feature: feature.name,
|
|
196
|
+
kind: AgentDocGapKinds.handlerWithoutTranslation,
|
|
197
|
+
message: `This write handler has no entity and no actionForm screen, so its title and field labels can only come from translations — missing keys: ${missing.join(", ")}.`,
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
return gaps;
|
|
201
|
+
}
|
|
202
|
+
|
|
147
203
|
export function findAgentDocGaps(features: readonly FeatureDefinition[]): readonly AgentDocGap[] {
|
|
148
204
|
const gaps: AgentDocGap[] = [];
|
|
149
205
|
for (const feature of features) {
|
|
@@ -161,6 +217,16 @@ export function findAgentDocGaps(features: readonly FeatureDefinition[]): readon
|
|
|
161
217
|
return [...gaps].sort((a, b) => compareByCodePoint(a.qn, b.qn));
|
|
162
218
|
}
|
|
163
219
|
|
|
220
|
+
// Kept out of findAgentDocGaps on purpose: the published agent-manifest guard
|
|
221
|
+
// fails on every gap that function returns, and these are advisory.
|
|
222
|
+
export function findHandlerTranslationGaps(
|
|
223
|
+
features: readonly FeatureDefinition[],
|
|
224
|
+
): readonly AgentDocGap[] {
|
|
225
|
+
const coveredHandlerQns = actionFormHandlerQns(features);
|
|
226
|
+
const gaps = features.flatMap((feature) => translationGapsOfFeature(feature, coveredHandlerQns));
|
|
227
|
+
return [...gaps].sort((a, b) => compareByCodePoint(a.qn, b.qn));
|
|
228
|
+
}
|
|
229
|
+
|
|
164
230
|
export function formatAgentDocGap(gap: AgentDocGap): string {
|
|
165
231
|
return `${gap.qn} — ${gap.message}`;
|
|
166
232
|
}
|
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
} from "@cosmicdrift/kumiko-framework/engine";
|
|
20
20
|
import { translationValueOtherText } from "@cosmicdrift/kumiko-framework/ui-types";
|
|
21
21
|
import * as z from "zod";
|
|
22
|
+
import { recordManifestDenyQns } from "./manifest-deny-qns.js";
|
|
22
23
|
import type {
|
|
23
24
|
AgentManifest,
|
|
24
25
|
AgentManifestEntity,
|
|
@@ -193,16 +194,22 @@ function buildFeatures(
|
|
|
193
194
|
|
|
194
195
|
// Agent dispatch never runs as a system identity, so properties the engine only
|
|
195
196
|
// honors for system callers would be silently dropped — hide them from the tool.
|
|
196
|
-
function
|
|
197
|
-
jsonSchema: Record<string, unknown>,
|
|
198
|
-
): Readonly<Record<string, unknown>> {
|
|
197
|
+
export function systemOnlyPropertyNames(jsonSchema: Record<string, unknown>): readonly string[] {
|
|
199
198
|
const properties = jsonSchema["properties"];
|
|
200
|
-
if (typeof properties !== "object" || properties === null) return
|
|
201
|
-
|
|
199
|
+
if (typeof properties !== "object" || properties === null) return [];
|
|
200
|
+
return Object.entries(properties)
|
|
202
201
|
.filter(
|
|
203
202
|
([, property]) => (property as Record<string, unknown>)[SYSTEM_ONLY_JSON_SCHEMA_KEY] === true,
|
|
204
203
|
)
|
|
205
204
|
.map(([name]) => name);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function withoutSystemOnlyProperties(
|
|
208
|
+
jsonSchema: Record<string, unknown>,
|
|
209
|
+
): Readonly<Record<string, unknown>> {
|
|
210
|
+
const properties = jsonSchema["properties"];
|
|
211
|
+
if (typeof properties !== "object" || properties === null) return jsonSchema;
|
|
212
|
+
const hidden = systemOnlyPropertyNames(jsonSchema);
|
|
206
213
|
if (hidden.length === 0) return jsonSchema;
|
|
207
214
|
const required = jsonSchema["required"];
|
|
208
215
|
return {
|
|
@@ -334,6 +341,16 @@ function detailIdParam(screen: ScreenDefinition): string {
|
|
|
334
341
|
return screen.type === "projectionDetail" ? (screen.idParam ?? "id") : "id";
|
|
335
342
|
}
|
|
336
343
|
|
|
344
|
+
function screenTitles(
|
|
345
|
+
translations: TranslationKeys,
|
|
346
|
+
screenId: string,
|
|
347
|
+
shortId: string,
|
|
348
|
+
): AgentManifestScreen["titles"] {
|
|
349
|
+
return entryAsLabels(
|
|
350
|
+
translations[`${screenId}.title`] ?? translations[`screen:${shortId}.title`] ?? {},
|
|
351
|
+
);
|
|
352
|
+
}
|
|
353
|
+
|
|
337
354
|
function buildScreens(
|
|
338
355
|
screens: ReadonlyMap<string, ScreenDefinition>,
|
|
339
356
|
navs: ReadonlyMap<string, NavDefinition>,
|
|
@@ -372,7 +389,7 @@ function buildScreens(
|
|
|
372
389
|
result.push({
|
|
373
390
|
id: screen.id,
|
|
374
391
|
type: screen.type,
|
|
375
|
-
titles:
|
|
392
|
+
titles: screenTitles(translations, screen.id, shortId),
|
|
376
393
|
// `screen.description` may be a raw i18n key (`registry.getAllTranslations()`
|
|
377
394
|
// double-prefixes it, see build-app-schema.ts:77-81) — match on suffix like
|
|
378
395
|
// `labelsForSuffix` does for entity/field labels, else fall back to the
|
|
@@ -444,7 +461,7 @@ export function buildAgentManifest(
|
|
|
444
461
|
);
|
|
445
462
|
const workspaces = buildWorkspaces(workspaceMap, translations, roles);
|
|
446
463
|
|
|
447
|
-
|
|
464
|
+
const manifest: AgentManifest = {
|
|
448
465
|
builtForRoles: [...roles],
|
|
449
466
|
features: sortedByKey(features, (f) => f.name),
|
|
450
467
|
entities: sortedByKey(entities, (e) => e.name),
|
|
@@ -457,4 +474,6 @@ export function buildAgentManifest(
|
|
|
457
474
|
...(options.currency && { currency: options.currency }),
|
|
458
475
|
},
|
|
459
476
|
};
|
|
477
|
+
recordManifestDenyQns(manifest, new Set(options.denyQns ?? []));
|
|
478
|
+
return manifest;
|
|
460
479
|
}
|
|
@@ -1,4 +1,16 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.353.0",
|
|
4
|
+
"type": "fix",
|
|
5
|
+
"title": "buildToolCatalog throws when the manifest was built with denyQns the catalog options lack",
|
|
6
|
+
"migration": "Pass the same denyQns list to buildAgentManifest and buildToolCatalog."
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
"version": "0.353.0",
|
|
10
|
+
"type": "fix",
|
|
11
|
+
"title": "Agent manifest screen titles resolve per feature instead of by mount order",
|
|
12
|
+
"detail": "Agent manifest screen titles no longer depend on feature mount order\nTwo features with a screen of the same short id used to get the title of whichever feature was mounted first. The manifest now looks up the screen's own feature-prefixed title key first and falls back to the unprefixed key."
|
|
13
|
+
},
|
|
2
14
|
{
|
|
3
15
|
"version": "0.338.0",
|
|
4
16
|
"type": "improvement",
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { defineFeature, type FeatureDefinition } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
findAgentDocGaps,
|
|
4
|
+
findHandlerTranslationGaps,
|
|
5
|
+
formatAgentDocGap,
|
|
6
|
+
} from "./agent-doc-lint.js";
|
|
3
7
|
|
|
4
8
|
export const AGENT_TOOLS_FEATURE_NAME = "agent-tools";
|
|
5
9
|
|
|
@@ -19,8 +23,15 @@ export function createAgentToolsFeature(): FeatureDefinition {
|
|
|
19
23
|
// would turn every incrementally-documented app into an outage.
|
|
20
24
|
r.bootCheck(({ features }) => {
|
|
21
25
|
const gaps = findAgentDocGaps(features);
|
|
26
|
+
const warningCount = findHandlerTranslationGaps(features).length;
|
|
27
|
+
if (warningCount > 0) {
|
|
28
|
+
// biome-ignore lint/suspicious/noConsole: operator-visibility at boot for agent-doc gaps
|
|
29
|
+
console.warn(
|
|
30
|
+
`[agent-tools] ${warningCount} agent-exposed write handler(s) lack title/field-label translations — run \`kumiko agent lint\` for details.`,
|
|
31
|
+
);
|
|
32
|
+
}
|
|
22
33
|
if (gaps.length === 0) {
|
|
23
|
-
// skip: no documentation gaps, nothing to warn about
|
|
34
|
+
// skip: no blocking documentation gaps, nothing more to warn about
|
|
24
35
|
return;
|
|
25
36
|
}
|
|
26
37
|
// biome-ignore lint/suspicious/noConsole: operator-visibility at boot for agent-doc gaps
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
export type { AgentDocGap, AgentDocGapKind } from "./agent-doc-lint.js";
|
|
2
|
-
export {
|
|
2
|
+
export {
|
|
3
|
+
AgentDocGapKinds,
|
|
4
|
+
findAgentDocGaps,
|
|
5
|
+
findHandlerTranslationGaps,
|
|
6
|
+
formatAgentDocGap,
|
|
7
|
+
} from "./agent-doc-lint.js";
|
|
3
8
|
export { buildAgentManifest } from "./agent-manifest.js";
|
|
4
9
|
export { AGENT_TOOLS_FEATURE_NAME, createAgentToolsFeature } from "./feature.js";
|
|
5
10
|
export { buildToolCatalog, OPEN_FORM_TOOL_NAME, toolNameForQn } from "./tool-catalog.js";
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { AgentManifest } from "./types.js";
|
|
2
|
+
|
|
3
|
+
// The deny list is deliberately not a manifest field (the manifest is prompt
|
|
4
|
+
// payload), so the catalog builder learns it through this side channel to
|
|
5
|
+
// detect a manifest cut that the catalog would silently not mirror.
|
|
6
|
+
const denyQnsByManifest = new WeakMap<AgentManifest, ReadonlySet<string>>();
|
|
7
|
+
|
|
8
|
+
export function recordManifestDenyQns(manifest: AgentManifest, denyQns: ReadonlySet<string>): void {
|
|
9
|
+
denyQnsByManifest.set(manifest, denyQns);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function assertCatalogMirrorsManifestDenyQns(
|
|
13
|
+
manifest: AgentManifest,
|
|
14
|
+
catalogDenyQns: ReadonlySet<string>,
|
|
15
|
+
): void {
|
|
16
|
+
const manifestDenyQns = denyQnsByManifest.get(manifest);
|
|
17
|
+
// skip: manifest has no registered deny list, nothing to mirror
|
|
18
|
+
if (!manifestDenyQns) return;
|
|
19
|
+
const missing = [...manifestDenyQns].filter((qn) => !catalogDenyQns.has(qn));
|
|
20
|
+
if (missing.length > 0) {
|
|
21
|
+
throw new Error(
|
|
22
|
+
`agent denyQns passed to buildAgentManifest but not to buildToolCatalog: ${missing.join(", ")} — the catalog would still expose their entity CRUD tools`,
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -6,6 +6,7 @@ import type {
|
|
|
6
6
|
} from "@cosmicdrift/kumiko-framework/engine";
|
|
7
7
|
import { hasAccess, normalizeAccessEntry } from "@cosmicdrift/kumiko-framework/engine";
|
|
8
8
|
import { FILTER_OPS, isRecord } from "./filter-ops.js";
|
|
9
|
+
import { assertCatalogMirrorsManifestDenyQns } from "./manifest-deny-qns.js";
|
|
9
10
|
import type {
|
|
10
11
|
AgentManifest,
|
|
11
12
|
AgentManifestEntity,
|
|
@@ -628,7 +629,8 @@ function addClientTools(manifest: AgentManifest, mode: AgentToolMode, sink: Cata
|
|
|
628
629
|
* to drop an entity CRUD tool, which no `agent.expose` on the manifest side can reach. It is
|
|
629
630
|
* NOT read off the manifest on purpose — the manifest is prompt payload, and a list of the
|
|
630
631
|
* handlers the model may not call has no business travelling to the provider. Pass the same
|
|
631
|
-
* list to `buildAgentManifest` so the manifest stops describing what the catalog withholds
|
|
632
|
+
* list to `buildAgentManifest` so the manifest stops describing what the catalog withholds;
|
|
633
|
+
* a manifest built with `denyQns` that the catalog lacks throws. */
|
|
632
634
|
export function buildToolCatalog(
|
|
633
635
|
registry: RegistrySearchView,
|
|
634
636
|
manifest: AgentManifest,
|
|
@@ -639,6 +641,7 @@ export function buildToolCatalog(
|
|
|
639
641
|
const locale = manifest.tenantSettings.locale;
|
|
640
642
|
const entityByName = new Map(manifest.entities.map((entity) => [entity.name, entity]));
|
|
641
643
|
const denyQns = new Set(options.denyQns ?? []);
|
|
644
|
+
assertCatalogMirrorsManifestDenyQns(manifest, denyQns);
|
|
642
645
|
|
|
643
646
|
addRegistrySearchTools(registry, roleFilter, denyQns, sink);
|
|
644
647
|
const { detailQnByEntity, listQnByEntity, entityListDetailQns } = collectEntityHandlerQns(
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { runAsDirectCallEntry } from "@cosmicdrift/kumiko-framework/api";
|
|
2
1
|
import type { SessionUser, WriteResult } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
|
+
import { runAsDirectCallEntry } from "@cosmicdrift/kumiko-framework/internal/request-context";
|
|
3
3
|
import { FILTER_OPS, isRecord } from "./filter-ops.js";
|
|
4
4
|
import type { ToolDispatchDescriptor } from "./types.js";
|
|
5
5
|
|
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.353.0",
|
|
4
|
+
"type": "fix",
|
|
5
|
+
"title": "Audit list paging no longer repeats rows with ascending sort",
|
|
6
|
+
"detail": "Audit list pages forward without repeats when sorted ascending\n`audit:query:list` with `sortDirection: \"asc\"` returned the same rows again on the next page because the cursor always meant \"older than\". The cursor now follows the sort direction, and ties are broken by event id. Sorting by `type` returns a single page without `nextCursor`, since an event id cannot resume a type-ordered listing."
|
|
7
|
+
},
|
|
2
8
|
{
|
|
3
9
|
"version": "0.346.0",
|
|
4
10
|
"type": "improvement",
|
|
@@ -52,6 +52,7 @@ function buildAuditWhere(
|
|
|
52
52
|
cursor?: string;
|
|
53
53
|
before?: string;
|
|
54
54
|
},
|
|
55
|
+
direction: "asc" | "desc",
|
|
55
56
|
): WhereObject {
|
|
56
57
|
const where: WhereObject = { tenantId };
|
|
57
58
|
if (p.aggregateType) where["aggregateType"] = p.aggregateType;
|
|
@@ -62,7 +63,9 @@ function buildAuditWhere(
|
|
|
62
63
|
const range = buildDateRange(p.from, p.to);
|
|
63
64
|
if (range) where["createdAt"] = range;
|
|
64
65
|
const cursor = p.cursor ?? p.before;
|
|
65
|
-
|
|
66
|
+
// The cursor is an event id, so it only walks pages in id order: ascending
|
|
67
|
+
// pages continue past it, descending pages stop before it.
|
|
68
|
+
if (cursor) where["id"] = direction === "asc" ? { gt: BigInt(cursor) } : { lt: BigInt(cursor) };
|
|
66
69
|
return where;
|
|
67
70
|
}
|
|
68
71
|
|
|
@@ -94,10 +97,15 @@ export const listQuery = defineQueryHandler({
|
|
|
94
97
|
handler: async (query, ctx) => {
|
|
95
98
|
const p = query.payload;
|
|
96
99
|
const scopeFilter = resolveAuditScopeFilter(query.user, p.scope);
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
100
|
+
const direction = p.sortDirection ?? "desc";
|
|
101
|
+
const where = buildAuditWhere(
|
|
102
|
+
scopeFilter.tenantId,
|
|
103
|
+
{
|
|
104
|
+
...p,
|
|
105
|
+
aggregateType: scopeFilter.aggregateType ?? p.aggregateType,
|
|
106
|
+
},
|
|
107
|
+
direction,
|
|
108
|
+
);
|
|
101
109
|
|
|
102
110
|
const rows = await selectMany<{
|
|
103
111
|
id: bigint;
|
|
@@ -110,10 +118,11 @@ export const listQuery = defineQueryHandler({
|
|
|
110
118
|
createdAt: unknown;
|
|
111
119
|
createdBy: string;
|
|
112
120
|
}>(ctx.db, eventsTable, where, {
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
121
|
+
// `id` as tie-breaker keeps the order total, so pages never overlap.
|
|
122
|
+
orderBy: [
|
|
123
|
+
{ col: p.sort === "type" ? "type" : "createdAt", direction },
|
|
124
|
+
{ col: "id", direction },
|
|
125
|
+
],
|
|
117
126
|
limit: p.limit,
|
|
118
127
|
});
|
|
119
128
|
|
|
@@ -129,7 +138,8 @@ export const listQuery = defineQueryHandler({
|
|
|
129
138
|
createdBy: r.createdBy,
|
|
130
139
|
}));
|
|
131
140
|
const last = serialised[serialised.length - 1];
|
|
132
|
-
|
|
141
|
+
// An id cursor cannot resume a type-ordered listing, so that sort is one page.
|
|
142
|
+
const nextId = serialised.length === p.limit && last && p.sort !== "type" ? last.id : null;
|
|
133
143
|
return {
|
|
134
144
|
rows: serialised,
|
|
135
145
|
nextCursor: nextId,
|
|
@@ -1,4 +1,28 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.354.0",
|
|
4
|
+
"type": "breaking",
|
|
5
|
+
"title": "Invitations pin the invitee's membership version; superseded accept no longer relies on timestamps",
|
|
6
|
+
"migration": "New nullable column read_tenant_invitations.membership_version: run `kumiko schema generate` and apply the migration before deploying. Invitations issued before the migration keep the timestamp comparison."
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
"version": "0.354.0",
|
|
10
|
+
"type": "fix",
|
|
11
|
+
"title": "Login, MFA verify and MFA preauth setup follow the server landingPath when no valid next parameter is present",
|
|
12
|
+
"detail": "AuthGate onSuccess receives the landing path. A validated next parameter still wins; a landing path equal to the current path is not followed, so the screen does not reload itself."
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"version": "0.354.0",
|
|
16
|
+
"type": "fix",
|
|
17
|
+
"title": "Signup-confirm and invite-signup-complete store the request locale on the new user and in the session"
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
"version": "0.354.0",
|
|
21
|
+
"type": "fix",
|
|
22
|
+
"title": "INITIAL_SIGNUP_ROLES stays [\"TenantAdmin\", \"Admin\"] as a transition",
|
|
23
|
+
"detail": "New self-signup owners hold TenantAdmin plus the legacy Admin literal, because handlers that gate on the Admin literal would otherwise lock them out. Both roles show in Team, Members until those handlers gate on access.admin. This supersedes the earlier note that the owner role is only TenantAdmin.",
|
|
24
|
+
"migration": "Move your own handlers that gate on the Admin literal to access.admin; after that the Admin entry can be dropped from the constant."
|
|
25
|
+
},
|
|
2
26
|
{
|
|
3
27
|
"version": "0.348.0",
|
|
4
28
|
"type": "fix",
|
|
@@ -22,6 +22,7 @@ export const changePasswordWrite = defineWriteHandler({
|
|
|
22
22
|
}),
|
|
23
23
|
access: { roles: access.authenticated },
|
|
24
24
|
escapeHatch: {
|
|
25
|
+
grants: ["systemIdentity"],
|
|
25
26
|
reason:
|
|
26
27
|
"Reads the caller's own passwordHash (privileged-only field) via ctx.queryAs(SYSTEM, ...) " +
|
|
27
28
|
"and writes the new hash via ctx.writeAs(SYSTEM, user:update) — the caller's own " +
|
package/packages/bundled-features/src/auth-email-password/handlers/invite-accept-with-login.write.ts
CHANGED
|
@@ -41,7 +41,7 @@ import {
|
|
|
41
41
|
tenantInvitationsTable,
|
|
42
42
|
} from "../../tenant/invitation-table.js";
|
|
43
43
|
// kumiko-lint-ignore cross-feature-import membership grant for a privileged cross-tenant add
|
|
44
|
-
import { grantInvitedMembershipRole
|
|
44
|
+
import { grantInvitedMembershipRole } from "../../tenant/invited-membership.js";
|
|
45
45
|
// kumiko-lint-ignore cross-feature-import global roles granted on invite accept live on the user row
|
|
46
46
|
import {
|
|
47
47
|
grantInvitedGlobalRoles,
|
|
@@ -150,6 +150,7 @@ export function createInviteAcceptWithLoginHandler(opts: InviteAcceptWithLoginOp
|
|
|
150
150
|
readonly role: string;
|
|
151
151
|
readonly globalRoles: unknown;
|
|
152
152
|
readonly version: number;
|
|
153
|
+
readonly membershipVersion: number | null;
|
|
153
154
|
readonly insertedAt: Temporal.Instant;
|
|
154
155
|
readonly modifiedAt: Temporal.Instant | null;
|
|
155
156
|
};
|
|
@@ -224,7 +225,7 @@ export function createInviteAcceptWithLoginHandler(opts: InviteAcceptWithLoginOp
|
|
|
224
225
|
userId,
|
|
225
226
|
tenantId: invitationTenantId,
|
|
226
227
|
role: invitationRole,
|
|
227
|
-
|
|
228
|
+
invitation,
|
|
228
229
|
});
|
|
229
230
|
if (!grant.isSuccess) return grant;
|
|
230
231
|
|
|
@@ -33,7 +33,7 @@ import {
|
|
|
33
33
|
tenantInvitationsTable,
|
|
34
34
|
} from "../../tenant/invitation-table.js";
|
|
35
35
|
// kumiko-lint-ignore cross-feature-import membership grant for a privileged cross-tenant add (like provisionSignupAccount)
|
|
36
|
-
import { grantInvitedMembershipRole
|
|
36
|
+
import { grantInvitedMembershipRole } from "../../tenant/invited-membership.js";
|
|
37
37
|
// kumiko-lint-ignore cross-feature-import global roles granted on invite accept live on the user row
|
|
38
38
|
import {
|
|
39
39
|
grantInvitedGlobalRoles,
|
|
@@ -110,6 +110,7 @@ export function createInviteAcceptHandler() {
|
|
|
110
110
|
readonly role: string;
|
|
111
111
|
readonly globalRoles: unknown;
|
|
112
112
|
readonly version: number;
|
|
113
|
+
readonly membershipVersion: number | null;
|
|
113
114
|
readonly insertedAt: Temporal.Instant;
|
|
114
115
|
readonly modifiedAt: Temporal.Instant | null;
|
|
115
116
|
};
|
|
@@ -154,7 +155,7 @@ export function createInviteAcceptHandler() {
|
|
|
154
155
|
userId: event.user.id,
|
|
155
156
|
tenantId: invitationTenantId,
|
|
156
157
|
role: invitationRole,
|
|
157
|
-
|
|
158
|
+
invitation,
|
|
158
159
|
});
|
|
159
160
|
if (!grant.isSuccess) return grant;
|
|
160
161
|
const { alreadyMember } = grant.data;
|
|
@@ -40,6 +40,8 @@ import {
|
|
|
40
40
|
tenantInvitationEntity,
|
|
41
41
|
tenantInvitationsTable,
|
|
42
42
|
} from "../../tenant/invitation-table.js";
|
|
43
|
+
// kumiko-lint-ignore cross-feature-import membership version read is owned by the tenant feature
|
|
44
|
+
import { membershipVersionOf } from "../../tenant/invited-membership.js";
|
|
43
45
|
// kumiko-lint-ignore cross-feature-import membership-role validation owned by tenant-feature
|
|
44
46
|
import {
|
|
45
47
|
findForbiddenMembershipRole,
|
|
@@ -49,6 +51,8 @@ import {
|
|
|
49
51
|
} from "../../tenant/membership-roles.js";
|
|
50
52
|
// kumiko-lint-ignore cross-feature-import global-role options are owned by the user feature
|
|
51
53
|
import { GLOBAL_ROLE_OPTIONS } from "../../user/constants.js";
|
|
54
|
+
// kumiko-lint-ignore cross-feature-import invitee lookup by email reads the user row
|
|
55
|
+
import { userTable } from "../../user/schema/user.js";
|
|
52
56
|
import { AUTH_INVITE_DEFAULT_TTL_MINUTES } from "../constants.js";
|
|
53
57
|
import type { AuthMailLocale } from "../email-templates.js";
|
|
54
58
|
import { renderInviteEmail } from "../email-templates.js";
|
|
@@ -144,6 +148,20 @@ function checkAssignableInviteRole(
|
|
|
144
148
|
return undefined;
|
|
145
149
|
}
|
|
146
150
|
|
|
151
|
+
// 0 = the invitee has no membership in the tenant yet (or no account at all).
|
|
152
|
+
async function currentMembershipVersion(
|
|
153
|
+
ctx: HandlerContext,
|
|
154
|
+
tenantId: string,
|
|
155
|
+
email: string,
|
|
156
|
+
): Promise<number> {
|
|
157
|
+
const user = await ctx.db.global(userTable).fetchOne<{ readonly id: string }>({
|
|
158
|
+
email,
|
|
159
|
+
isDeleted: false,
|
|
160
|
+
});
|
|
161
|
+
if (!user) return 0;
|
|
162
|
+
return membershipVersionOf(ctx.db, tenantId, user.id);
|
|
163
|
+
}
|
|
164
|
+
|
|
147
165
|
async function issueInvitation(
|
|
148
166
|
opts: InviteCreateOptions,
|
|
149
167
|
ttlSeconds: number,
|
|
@@ -166,6 +184,7 @@ async function issueInvitation(
|
|
|
166
184
|
// The unique index allows one row per (tenantId, email). Whatever its
|
|
167
185
|
// status, a re-invite resets it to pending with a fresh token.
|
|
168
186
|
const existing = await ctx.db.fetchOne(tenantInvitationsTable, { tenantId, email });
|
|
187
|
+
const membershipVersion = await currentMembershipVersion(ctx, tenantId, email);
|
|
169
188
|
|
|
170
189
|
let invitationId: string;
|
|
171
190
|
if (existing) {
|
|
@@ -193,6 +212,7 @@ async function issueInvitation(
|
|
|
193
212
|
globalRoles: [...request.globalRoles],
|
|
194
213
|
status: INVITATION_STATUS.pending,
|
|
195
214
|
invitedBy: inviter.id,
|
|
215
|
+
membershipVersion,
|
|
196
216
|
expiresAt,
|
|
197
217
|
},
|
|
198
218
|
},
|
|
@@ -208,6 +228,7 @@ async function issueInvitation(
|
|
|
208
228
|
globalRoles: [...request.globalRoles],
|
|
209
229
|
status: INVITATION_STATUS.pending,
|
|
210
230
|
invitedBy: inviter.id,
|
|
231
|
+
membershipVersion,
|
|
211
232
|
expiresAt,
|
|
212
233
|
},
|
|
213
234
|
inviter,
|
package/packages/bundled-features/src/auth-email-password/handlers/invite-signup-complete.write.ts
CHANGED
|
@@ -32,7 +32,7 @@ import {
|
|
|
32
32
|
} from "@cosmicdrift/kumiko-framework/engine";
|
|
33
33
|
import { InternalError, writeFailure } from "@cosmicdrift/kumiko-framework/errors";
|
|
34
34
|
import * as z from "zod";
|
|
35
|
-
import { decryptStoredPii } from "../../shared/index.js";
|
|
35
|
+
import { decryptStoredPii, registrationLocaleFromRequest } from "../../shared/index.js";
|
|
36
36
|
// kumiko-lint-ignore cross-feature-import invite-flow
|
|
37
37
|
import {
|
|
38
38
|
INVITATION_STATUS,
|
|
@@ -161,11 +161,13 @@ export function createInviteSignupCompleteHandler(opts: InviteSignupCompleteOpti
|
|
|
161
161
|
// @cast-boundary db-runner — helpers use only the query API that
|
|
162
162
|
// DbConnection and DbTx share.
|
|
163
163
|
const dbConn = ctx.db.unsafeRaw() as DbConnection;
|
|
164
|
+
const registrationLocale = registrationLocaleFromRequest();
|
|
164
165
|
const { id: userId } = await seedUserWithPassword(dbConn, {
|
|
165
166
|
email: invitationEmail,
|
|
166
167
|
password: event.payload.password,
|
|
167
168
|
displayName: invitationEmail.split("@")[0] ?? invitationEmail,
|
|
168
169
|
emailVerified: true,
|
|
170
|
+
...registrationLocale,
|
|
169
171
|
...(invitationGlobalRoles.length > 0 && { roles: invitationGlobalRoles }),
|
|
170
172
|
});
|
|
171
173
|
|
|
@@ -221,6 +223,7 @@ export function createInviteSignupCompleteHandler(opts: InviteSignupCompleteOpti
|
|
|
221
223
|
id: userId,
|
|
222
224
|
tenantId: invitationTenantId,
|
|
223
225
|
roles: mergedRoles,
|
|
226
|
+
...registrationLocale,
|
|
224
227
|
};
|
|
225
228
|
|
|
226
229
|
committed = true;
|
|
@@ -277,6 +277,7 @@ export function createLoginHandler(opts: LoginHandlerOptions = {}) {
|
|
|
277
277
|
access: { roles: ["anonymous"] },
|
|
278
278
|
rateLimit: { per: "ip+handler", limit: 20, windowSeconds: 60 },
|
|
279
279
|
escapeHatch: {
|
|
280
|
+
grants: ["systemIdentity", "unsafeRaw"],
|
|
280
281
|
reason:
|
|
281
282
|
"Unauthenticated login has no caller identity yet — it looks up the user row by " +
|
|
282
283
|
"email via ctx.queryAs(SYSTEM, ...) and resolves tenant membership via " +
|