@cosmicdrift/kumiko-samples 0.365.1 → 0.367.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 +1 -1
- package/packages/bundled-features/src/admin-shell/feature.ts +9 -9
- package/packages/bundled-features/src/agent-tools/changes.json +6 -0
- package/packages/bundled-features/src/agent-tools/tool-dispatch.ts +87 -5
- package/packages/bundled-features/src/audit/feature.ts +1 -1
- package/packages/bundled-features/src/auth-email-password/changes.json +13 -0
- package/packages/bundled-features/src/auth-email-password/constants.ts +0 -2
- package/packages/bundled-features/src/auth-email-password/feature.ts +3 -3
- package/packages/bundled-features/src/auth-email-password/handlers/invite-create.write.ts +1 -0
- package/packages/bundled-features/src/auth-mfa/feature.ts +4 -4
- package/packages/bundled-features/src/auth-mfa-user-data/index.ts +1 -1
- package/packages/bundled-features/src/billing-foundation/consumer-protection/register-consent.ts +1 -1
- package/packages/bundled-features/src/billing-foundation/feature.ts +1 -1
- package/packages/bundled-features/src/cap-counter/cap-reservation.ts +89 -28
- package/packages/bundled-features/src/cap-counter/changes.json +7 -0
- package/packages/bundled-features/src/cap-counter/constants.ts +3 -0
- package/packages/bundled-features/src/cap-counter/feature.ts +2 -2
- package/packages/bundled-features/src/cap-counter/tables.ts +5 -2
- package/packages/bundled-features/src/cap-overview/feature.ts +3 -3
- package/packages/bundled-features/src/channel-discord/feature.ts +2 -2
- package/packages/bundled-features/src/channel-email/changes.json +6 -0
- package/packages/bundled-features/src/channel-email/email-channel.ts +12 -2
- package/packages/bundled-features/src/channel-email/feature.ts +2 -2
- package/packages/bundled-features/src/channel-in-app/feature.ts +1 -1
- package/packages/bundled-features/src/channel-push/feature.ts +1 -1
- package/packages/bundled-features/src/channel-slack/feature.ts +2 -2
- package/packages/bundled-features/src/channel-teams/feature.ts +2 -2
- package/packages/bundled-features/src/channel-telegram/feature.ts +2 -2
- package/packages/bundled-features/src/compliance-profiles/feature.ts +1 -1
- package/packages/bundled-features/src/compliance-profiles-ops/index.ts +3 -3
- package/packages/bundled-features/src/config/feature.ts +1 -1
- package/packages/bundled-features/src/crypto-shredding/changes.json +6 -0
- package/packages/bundled-features/src/custom-fields/feature.ts +2 -2
- package/packages/bundled-features/src/custom-fields/wire-for-entity.ts +17 -13
- package/packages/bundled-features/src/data-retention/feature.ts +1 -1
- package/packages/bundled-features/src/data-retention/presets.ts +4 -4
- package/packages/bundled-features/src/delivery/changes.json +18 -0
- package/packages/bundled-features/src/delivery/channel-context.ts +1 -0
- package/packages/bundled-features/src/delivery/db/queries/address-opt-outs.ts +6 -2
- package/packages/bundled-features/src/delivery/db/queries/preferences.ts +4 -4
- package/packages/bundled-features/src/delivery/delivery-service.ts +71 -4
- package/packages/bundled-features/src/delivery/feature.ts +10 -6
- package/packages/bundled-features/src/delivery/handlers/resubscribe-address.write.ts +12 -8
- package/packages/bundled-features/src/delivery/handlers/resubscribe-user.write.ts +3 -1
- package/packages/bundled-features/src/delivery/handlers/set-preference.write.ts +3 -1
- package/packages/bundled-features/src/delivery/handlers/unsubscribe-address.write.ts +13 -6
- package/packages/bundled-features/src/delivery/handlers/unsubscribe-user.write.ts +3 -1
- package/packages/bundled-features/src/delivery/index.ts +6 -0
- package/packages/bundled-features/src/delivery/notification-type-aliases.ts +13 -0
- package/packages/bundled-features/src/delivery/testing.ts +2 -0
- package/packages/bundled-features/src/delivery/types.ts +4 -0
- package/packages/bundled-features/src/delivery/unsubscribe-link.ts +73 -0
- package/packages/bundled-features/src/delivery/unsubscribe-page-strings.ts +17 -0
- package/packages/bundled-features/src/delivery/unsubscribe.ts +94 -31
- package/packages/bundled-features/src/derivatives-sharp/feature.ts +1 -1
- package/packages/bundled-features/src/document-ingest-foundation/feature.ts +2 -2
- package/packages/bundled-features/src/file-derivatives/feature.ts +3 -3
- package/packages/bundled-features/src/file-foundation/feature.ts +1 -1
- package/packages/bundled-features/src/file-provider-inmemory/feature.ts +2 -2
- package/packages/bundled-features/src/file-provider-s3/feature.ts +3 -3
- package/packages/bundled-features/src/file-provider-s3-env/feature.ts +2 -2
- package/packages/bundled-features/src/files/README.md +1 -1
- package/packages/bundled-features/src/files-tenant-data/index.ts +1 -1
- package/packages/bundled-features/src/folders/feature.ts +2 -2
- package/packages/bundled-features/src/folders-user-data/index.ts +1 -1
- package/packages/bundled-features/src/form-draft/feature.ts +2 -2
- package/packages/bundled-features/src/form-draft-user-data/index.ts +1 -1
- package/packages/bundled-features/src/inbound-mail-foundation/changes.json +13 -0
- package/packages/bundled-features/src/inbound-mail-foundation/connect-routes.ts +9 -1
- package/packages/bundled-features/src/inbound-mail-foundation/feature.ts +1 -1
- package/packages/bundled-features/src/inbound-mail-foundation/types.ts +19 -7
- package/packages/bundled-features/src/inbound-mail-foundation/watch-supervisor.ts +21 -2
- package/packages/bundled-features/src/inbound-provider-imap/feature.ts +2 -2
- package/packages/bundled-features/src/inbound-provider-inmemory/feature.ts +1 -1
- package/packages/bundled-features/src/ledger/feature.ts +5 -5
- package/packages/bundled-features/src/legal-pages/feature.ts +1 -1
- package/packages/bundled-features/src/mail-foundation/feature.ts +51 -10
- package/packages/bundled-features/src/mail-foundation/index.ts +3 -0
- package/packages/bundled-features/src/mail-transport-inmemory/feature.ts +2 -2
- package/packages/bundled-features/src/mail-transport-smtp/feature.ts +3 -3
- package/packages/bundled-features/src/managed-pages/css-gate.ts +1 -1
- package/packages/bundled-features/src/managed-pages/feature.ts +2 -2
- package/packages/bundled-features/src/metrics/feature.ts +2 -2
- package/packages/bundled-features/src/notes-history/feature.ts +2 -2
- package/packages/bundled-features/src/notes-history-user-data/index.ts +1 -1
- package/packages/bundled-features/src/personal-access-tokens/feature.ts +15 -11
- package/packages/bundled-features/src/readiness/feature.ts +2 -2
- package/packages/bundled-features/src/renderer-foundation/README.md +1 -1
- package/packages/bundled-features/src/renderer-foundation/feature.ts +1 -1
- package/packages/bundled-features/src/renderer-pdf/feature.ts +1 -1
- package/packages/bundled-features/src/secrets/feature.ts +1 -1
- package/packages/bundled-features/src/seo/feature.ts +1 -1
- package/packages/bundled-features/src/sessions/feature.ts +17 -13
- package/packages/bundled-features/src/step-dispatcher/feature.ts +1 -1
- package/packages/bundled-features/src/subscription-mollie/feature.ts +1 -1
- package/packages/bundled-features/src/subscription-stripe/feature.ts +3 -3
- package/packages/bundled-features/src/tags/feature.ts +2 -2
- package/packages/bundled-features/src/template-resolver/changes.json +25 -0
- package/packages/bundled-features/src/template-resolver/compile-document-body.ts +61 -9
- package/packages/bundled-features/src/template-resolver/document-sanitizer.ts +19 -0
- package/packages/bundled-features/src/template-resolver/document-schema.ts +33 -0
- package/packages/bundled-features/src/template-resolver/feature.ts +2 -2
- package/packages/bundled-features/src/template-resolver/handlers/bundle-import.ts +9 -1
- package/packages/bundled-features/src/template-resolver/handlers/copy-locale.write.ts +8 -2
- package/packages/bundled-features/src/template-resolver/handlers/find-by-id.query.ts +8 -1
- package/packages/bundled-features/src/template-resolver/handlers/list.query.ts +6 -1
- package/packages/bundled-features/src/template-resolver/handlers/overview.query.ts +7 -2
- package/packages/bundled-features/src/template-resolver/handlers/template-write.ts +21 -1
- package/packages/bundled-features/src/template-resolver/index.ts +4 -0
- package/packages/bundled-features/src/template-resolver/qualified-names.ts +1 -0
- package/packages/bundled-features/src/template-resolver/seeding.ts +54 -7
- package/packages/bundled-features/src/template-resolver/template-preview.ts +2 -0
- package/packages/bundled-features/src/template-resolver/template-system-only.ts +21 -0
- package/packages/bundled-features/src/template-resolver/template-validation.ts +48 -24
- package/packages/bundled-features/src/template-resolver/web/i18n.ts +2 -0
- package/packages/bundled-features/src/template-resolver-user-data/index.ts +1 -1
- package/packages/bundled-features/src/tenant/feature.ts +4 -4
- package/packages/bundled-features/src/tenant/seeding.ts +1 -1
- package/packages/bundled-features/src/tenant-lifecycle/feature.ts +2 -3
- package/packages/bundled-features/src/tenant-settings/feature.ts +1 -1
- package/packages/bundled-features/src/tier-engine/feature.ts +36 -27
- package/packages/bundled-features/src/user/feature.ts +1 -1
- package/packages/bundled-features/src/user-data-rights/README.md +1 -1
- package/packages/bundled-features/src/user-data-rights/changes.json +6 -0
- package/packages/bundled-features/src/user-data-rights/feature.ts +4 -4
- package/packages/bundled-features/src/user-data-rights-defaults/feature.ts +2 -2
- package/packages/bundled-features/src/user-profile/feature.ts +4 -4
- package/packages/bundled-features/src/waitlist/feature.ts +2 -2
- package/samples/apps/cap-billing-demo/README.md +1 -1
- package/samples/apps/cap-billing-demo/src/feature.ts +4 -4
- package/samples/apps/cap-billing-demo/src/run-config.ts +1 -1
- package/samples/apps/config-demo/src/features/demo/feature.ts +2 -2
- package/samples/apps/styleguide/src/features/ai-settings/feature.ts +43 -28
- package/samples/apps/styleguide/src/features/demo/feature.ts +18 -18
- package/samples/apps/styleguide/src/features/widgets/ai-text-demo.ts +10 -12
- package/samples/apps/styleguide/src/features/widgets/feature.ts +90 -108
- package/samples/apps/ui-walkthrough/src/app/seed-templates.ts +2 -0
- package/samples/apps/ui-walkthrough/src/app/template-seed-data.ts +12 -0
- package/samples/apps/ui-walkthrough/src/features/billing/feature.ts +7 -0
- package/samples/apps/ui-walkthrough/src/features/billing/i18n.ts +2 -0
- package/samples/apps/ui-walkthrough/src/features/rental/feature.ts +36 -36
- package/samples/apps/ui-walkthrough/src/features/vehicles/feature.ts +6 -6
- package/samples/apps/use-all-bundled/e2e-billing-gallery/gallery-features.ts +2 -2
- package/samples/apps/use-all-bundled/feature-manifest.json +4 -4
- package/samples/apps/use-all-bundled/package.json +1 -0
- package/samples/apps/use-all-bundled/scripts/gen-sample-index.ts +7 -7
- package/samples/apps/use-all-bundled/src/app/notes-feature.ts +3 -3
- package/samples/apps/use-all-bundled/src/app/screens-feature.ts +1 -1
- package/samples/apps/user-data-rights-demo/src/feature.ts +5 -5
- package/samples/recipes/access-control/src/feature.ts +21 -25
- package/samples/recipes/anonymous-access/src/feature.ts +36 -43
- package/samples/recipes/anonymous-access-multitenant/src/feature.ts +14 -16
- package/samples/recipes/auth-foundation-providers/src/feature.ts +1 -1
- package/samples/recipes/basic-entity/README.md +1 -1
- package/samples/recipes/boot-check/README.md +1 -1
- package/samples/recipes/boot-check/src/feature.ts +2 -2
- package/samples/recipes/chat-channels/src/feature.ts +1 -1
- package/samples/recipes/cross-feature-events/src/feature.ts +7 -7
- package/samples/recipes/currencies-global/src/feature.ts +3 -3
- package/samples/recipes/currencies-per-tenant/src/feature.ts +2 -2
- package/samples/recipes/custom-fields-basic/README.md +1 -1
- package/samples/recipes/custom-fields-basic/src/feature.ts +3 -2
- package/samples/recipes/custom-handlers/README.md +3 -3
- package/samples/recipes/custom-handlers/src/feature.ts +37 -41
- package/samples/recipes/dashboard-stats/src/feature.ts +6 -6
- package/samples/recipes/delivery-notifications/README.md +1 -1
- package/samples/recipes/delivery-notifications/src/feature.ts +8 -8
- package/samples/recipes/designer-demo/src/feature.ts +2 -1
- package/samples/recipes/embedded/src/feature.ts +1 -1
- package/samples/recipes/embedded-entity-form/src/feature.ts +13 -13
- package/samples/recipes/encrypted-tenant-config/src/feature.ts +1 -1
- package/samples/recipes/error-contract/src/feature.ts +25 -25
- package/samples/recipes/event-sourcing/src/feature.ts +70 -76
- package/samples/recipes/feature-toggles/src/feature.ts +28 -20
- package/samples/recipes/files-post-processing/src/feature.ts +22 -26
- package/samples/recipes/idempotency/src/feature.ts +16 -18
- package/samples/recipes/inbound-mail-triage/src/feature.ts +1 -1
- package/samples/recipes/lane-routing/src/feature.ts +10 -12
- package/samples/recipes/legal-pages/src/feature.ts +1 -1
- package/samples/recipes/lifecycle-hooks/README.md +4 -4
- package/samples/recipes/lifecycle-hooks/src/feature.ts +55 -35
- package/samples/recipes/managed-config/src/feature.ts +8 -8
- package/samples/recipes/managed-pages/src/feature.ts +1 -1
- package/samples/recipes/notes-history-basic/README.md +1 -1
- package/samples/recipes/notes-history-basic/src/feature.ts +2 -2
- package/samples/recipes/pdf-audit-export/src/feature.ts +7 -7
- package/samples/recipes/pipeline-basics/README.md +1 -1
- package/samples/recipes/pipeline-basics/src/feature.ts +7 -7
- package/samples/recipes/public-share-token/src/feature.ts +1 -1
- package/samples/recipes/rate-limiting/src/feature.ts +7 -9
- package/samples/recipes/record-detail-layout/src/feature.ts +18 -18
- package/samples/recipes/reference-data/src/feature.ts +1 -1
- package/samples/recipes/relations/src/feature.ts +2 -2
- package/samples/recipes/screens-nav/src/feature.ts +1 -1
- package/samples/recipes/search/src/feature.ts +17 -19
- package/samples/recipes/security-baseline/README.md +1 -1
- package/samples/recipes/state-machine/src/feature.ts +1 -1
- package/samples/recipes/tags-basic/README.md +1 -1
- package/samples/recipes/tags-basic/src/feature.ts +2 -2
- package/samples/recipes/tenant-settings/README.md +1 -1
- package/samples/recipes/tenant-settings/src/feature.ts +1 -1
- package/samples/recipes/timezones/src/feature.ts +12 -12
- package/samples/recipes/user-data-rights/README.md +2 -2
- package/samples/recipes/user-data-rights/src/feature.ts +1 -1
- package/samples/recipes/user-profile/src/feature.ts +1 -1
- package/samples/recipes/webhook-step/src/feature.ts +1 -1
- package/samples/recipes/writeform-section/src/feature.ts +12 -12
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-samples",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.367.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.367.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>",
|
|
@@ -65,15 +65,15 @@ export function createAdminShellFeature(options: CreateAdminShellOptions = {}):
|
|
|
65
65
|
category: "operations",
|
|
66
66
|
recommended: false,
|
|
67
67
|
});
|
|
68
|
-
r.requires("user");
|
|
69
|
-
r.requires("tenant");
|
|
70
|
-
r.requires("audit");
|
|
71
|
-
r.requires("jobs");
|
|
72
|
-
r.requires("metrics");
|
|
73
|
-
r.requires("metrics-system");
|
|
74
|
-
if (includeTierAdmin) r.requires("tier-engine");
|
|
75
|
-
if (includeCapOverview) r.requires("cap-overview");
|
|
76
|
-
if (includeDocumentDesigner) r.requires("document-designer");
|
|
68
|
+
r.requires({ features: ["user"] });
|
|
69
|
+
r.requires({ features: ["tenant"] });
|
|
70
|
+
r.requires({ features: ["audit"] });
|
|
71
|
+
r.requires({ features: ["jobs"] });
|
|
72
|
+
r.requires({ features: ["metrics"] });
|
|
73
|
+
r.requires({ features: ["metrics-system"] });
|
|
74
|
+
if (includeTierAdmin) r.requires({ features: ["tier-engine"] });
|
|
75
|
+
if (includeCapOverview) r.requires({ features: ["cap-overview"] });
|
|
76
|
+
if (includeDocumentDesigner) r.requires({ features: ["document-designer"] });
|
|
77
77
|
|
|
78
78
|
r.translations({ keys: ADMIN_SHELL_I18N });
|
|
79
79
|
|
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.366.0",
|
|
4
|
+
"type": "improvement",
|
|
5
|
+
"title": "Agent tool failures are classified instead of passing raw messages through",
|
|
6
|
+
"detail": "dispatchToolCall now adds a code to failed tool results. Client errors keep their message, rate limits also set retryable and retryAfterSeconds. Server errors and unknown throws are logged with their cause and returned to the model as \"Tool failed unexpectedly\", so internal details such as database messages no longer reach the model. ToolCallRequest accepts an optional log."
|
|
7
|
+
},
|
|
2
8
|
{
|
|
3
9
|
"version": "0.353.0",
|
|
4
10
|
"type": "fix",
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { SessionUser, WriteResult } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
|
+
import { isKumikoError } from "@cosmicdrift/kumiko-framework/errors";
|
|
2
3
|
import { runAsDirectCallEntry } from "@cosmicdrift/kumiko-framework/internal/request-context";
|
|
4
|
+
import { createFallbackLogger, type Logger } from "@cosmicdrift/kumiko-framework/logging";
|
|
3
5
|
import { isOneOf } from "@cosmicdrift/kumiko-types/type-guards";
|
|
4
6
|
import { FILTER_OPS, isRecord } from "./filter-ops.js";
|
|
5
7
|
import type { ToolDispatchDescriptor } from "./types.js";
|
|
@@ -23,7 +25,13 @@ export type ToolDispatcher = {
|
|
|
23
25
|
|
|
24
26
|
export type ToolCallResult =
|
|
25
27
|
| { readonly ok: true; readonly data: unknown }
|
|
26
|
-
| {
|
|
28
|
+
| {
|
|
29
|
+
readonly ok: false;
|
|
30
|
+
readonly error: string;
|
|
31
|
+
readonly code?: string;
|
|
32
|
+
readonly retryable?: boolean;
|
|
33
|
+
readonly retryAfterSeconds?: number;
|
|
34
|
+
};
|
|
27
35
|
|
|
28
36
|
export type ToolCallRequest = {
|
|
29
37
|
readonly dispatcher: ToolDispatcher;
|
|
@@ -37,8 +45,62 @@ export type ToolCallRequest = {
|
|
|
37
45
|
/** Idempotency scope: `${runId}:${toolCallId}` becomes the write requestId. */
|
|
38
46
|
readonly runId: string;
|
|
39
47
|
readonly toolCallId: string;
|
|
48
|
+
/** Receives the cause of unexpected (5xx) failures, which the tool result never carries. */
|
|
49
|
+
readonly log?: Pick<Logger, "error">;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
type ToolFailure = {
|
|
53
|
+
readonly code: string;
|
|
54
|
+
readonly httpStatus: number;
|
|
55
|
+
readonly message: string;
|
|
56
|
+
readonly retryAfterSeconds: number | undefined;
|
|
40
57
|
};
|
|
41
58
|
|
|
59
|
+
type FailureClassifier = (failure: ToolFailure, cause: unknown) => ToolCallResult;
|
|
60
|
+
|
|
61
|
+
const HTTP_TOO_MANY_REQUESTS = 429;
|
|
62
|
+
const HTTP_SERVER_ERROR = 500;
|
|
63
|
+
const UNEXPECTED_FAILURE_MESSAGE = "Tool failed unexpectedly";
|
|
64
|
+
const fallbackLog = createFallbackLogger("agent-tools");
|
|
65
|
+
|
|
66
|
+
function toolFailureOf(err: unknown): ToolFailure {
|
|
67
|
+
if (isKumikoError(err)) {
|
|
68
|
+
return {
|
|
69
|
+
code: err.code,
|
|
70
|
+
httpStatus: err.httpStatus,
|
|
71
|
+
message: err.message,
|
|
72
|
+
retryAfterSeconds: err.retryAfterSeconds,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
return {
|
|
76
|
+
code: "internal_error",
|
|
77
|
+
httpStatus: HTTP_SERVER_ERROR,
|
|
78
|
+
message: err instanceof Error ? err.message : String(err),
|
|
79
|
+
retryAfterSeconds: undefined,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function createFailureClassifier(toolName: string, log: Pick<Logger, "error">): FailureClassifier {
|
|
84
|
+
return (failure, cause) => {
|
|
85
|
+
if (failure.httpStatus === HTTP_TOO_MANY_REQUESTS) {
|
|
86
|
+
return {
|
|
87
|
+
ok: false,
|
|
88
|
+
error: failure.message,
|
|
89
|
+
code: failure.code,
|
|
90
|
+
retryable: true,
|
|
91
|
+
...(failure.retryAfterSeconds !== undefined && {
|
|
92
|
+
retryAfterSeconds: failure.retryAfterSeconds,
|
|
93
|
+
}),
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
if (failure.httpStatus < HTTP_SERVER_ERROR) {
|
|
97
|
+
return { ok: false, error: failure.message, code: failure.code };
|
|
98
|
+
}
|
|
99
|
+
log.error("agent tool call failed unexpectedly", { toolName, code: failure.code, cause });
|
|
100
|
+
return { ok: false, error: UNEXPECTED_FAILURE_MESSAGE, code: failure.code };
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
42
104
|
const SEARCH_RESULT_LIMIT = 10;
|
|
43
105
|
const DEFAULT_LIST_LIMIT = 10;
|
|
44
106
|
const MAX_LIST_LIMIT = 200;
|
|
@@ -216,6 +278,7 @@ async function dispatchServerWrite(
|
|
|
216
278
|
input: Readonly<Record<string, unknown>>,
|
|
217
279
|
runId: string,
|
|
218
280
|
toolCallId: string,
|
|
281
|
+
classifyFailure: FailureClassifier,
|
|
219
282
|
): Promise<ToolCallResult> {
|
|
220
283
|
let payload: Readonly<Record<string, unknown>> = input;
|
|
221
284
|
|
|
@@ -240,7 +303,15 @@ async function dispatchServerWrite(
|
|
|
240
303
|
const result = await dispatcher.write(descriptor.qn, payload, user, requestId);
|
|
241
304
|
|
|
242
305
|
if (!result.isSuccess) {
|
|
243
|
-
return
|
|
306
|
+
return classifyFailure(
|
|
307
|
+
{
|
|
308
|
+
code: result.error.code,
|
|
309
|
+
httpStatus: result.error.httpStatus,
|
|
310
|
+
message: result.error.message,
|
|
311
|
+
retryAfterSeconds: result.error.retryAfterSeconds,
|
|
312
|
+
},
|
|
313
|
+
result.error,
|
|
314
|
+
);
|
|
244
315
|
}
|
|
245
316
|
|
|
246
317
|
const { id, version } = extractIdAndVersion(result.data);
|
|
@@ -343,10 +414,13 @@ function dispatchClient(
|
|
|
343
414
|
/** Executes one LLM-issued tool call. Never throws — a missing tool, a missing argument, a
|
|
344
415
|
* disallowed filter field, or a rejected handler call (cap check, validation, ...) all come
|
|
345
416
|
* back as `{ ok: false }` so the agent loop can feed the error back to the model instead of
|
|
346
|
-
* crashing.
|
|
417
|
+
* crashing. Client errors (< 500) keep their message and carry a `code`; 429 adds
|
|
418
|
+
* `retryable`. Server-side failures (>= 500, unknown throws) are logged and returned as a
|
|
419
|
+
* generic message so internals never reach the model. `request.user` must be the caller's real identity — every server-kind tool runs
|
|
347
420
|
* through the same permission-checked dispatcher pipeline as a normal HTTP request. */
|
|
348
421
|
export async function dispatchToolCall(request: ToolCallRequest): Promise<ToolCallResult> {
|
|
349
422
|
const { dispatcher, user, toolName, input, dispatchTable, runId, toolCallId } = request;
|
|
423
|
+
const classifyFailure = createFailureClassifier(toolName, request.log ?? fallbackLog);
|
|
350
424
|
const descriptor = dispatchTable.get(toolName);
|
|
351
425
|
if (!descriptor) {
|
|
352
426
|
return { ok: false, error: `Unknown tool "${toolName}"` };
|
|
@@ -366,10 +440,18 @@ export async function dispatchToolCall(request: ToolCallRequest): Promise<ToolCa
|
|
|
366
440
|
case "server":
|
|
367
441
|
return descriptor.op === "query"
|
|
368
442
|
? await dispatchServerQuery(descriptor, dispatcher, user, input)
|
|
369
|
-
: await dispatchServerWrite(
|
|
443
|
+
: await dispatchServerWrite(
|
|
444
|
+
descriptor,
|
|
445
|
+
dispatcher,
|
|
446
|
+
user,
|
|
447
|
+
input,
|
|
448
|
+
runId,
|
|
449
|
+
toolCallId,
|
|
450
|
+
classifyFailure,
|
|
451
|
+
);
|
|
370
452
|
}
|
|
371
453
|
});
|
|
372
454
|
} catch (err) {
|
|
373
|
-
return
|
|
455
|
+
return classifyFailure(toolFailureOf(err), err);
|
|
374
456
|
}
|
|
375
457
|
}
|
|
@@ -44,7 +44,7 @@ export function createAuditFeature(): FeatureDefinition {
|
|
|
44
44
|
recommended: false,
|
|
45
45
|
});
|
|
46
46
|
r.translations({ keys: AUDIT_I18N });
|
|
47
|
-
r.requires("tenant", "user", "config");
|
|
47
|
+
r.requires({ features: ["tenant", "user", "config"] });
|
|
48
48
|
|
|
49
49
|
// Registered so ops tools/MSPs can discover the type — escape-hatch-audit-sink.ts appends it via the low-level event-store API.
|
|
50
50
|
r.defineEvent("escape-hatch-used", escapeHatchUsedSchema, { piiFields: "none" });
|
|
@@ -1,4 +1,17 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.366.0",
|
|
4
|
+
"type": "fix",
|
|
5
|
+
"title": "Removed unused AuthHandlers.inviteCancel constant",
|
|
6
|
+
"detail": "AuthHandlers.inviteCancel pointed at auth-email-password:write:invite-cancel, a handler that never existed. The constant is removed.",
|
|
7
|
+
"migration": "If you referenced AuthHandlers.inviteCancel, use tenant:write:cancel-invitation."
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
"version": "0.366.0",
|
|
11
|
+
"type": "fix",
|
|
12
|
+
"title": "invite-create is no longer exposed to agents",
|
|
13
|
+
"detail": "The admin invite-create handler is now opted out of the agent tool catalog with agent: { expose: false }, like the other invite handlers, because it mails an accept link to an arbitrary address."
|
|
14
|
+
},
|
|
2
15
|
{
|
|
3
16
|
"version": "0.365.0",
|
|
4
17
|
"type": "fix",
|
|
@@ -38,7 +38,6 @@ export const AuthHandlers = {
|
|
|
38
38
|
// inviteAccept: logged-in user → POST token (membership-add)
|
|
39
39
|
// inviteAcceptWithLogin: anon user with existing email → POST token + email + password
|
|
40
40
|
// inviteSignupComplete: anon user with new email → POST token + password
|
|
41
|
-
// inviteCancel: admin cancels a pending invite
|
|
42
41
|
inviteCreate: "auth-email-password:write:invite-create",
|
|
43
42
|
// System-only variant (access.system) that may carry global roles — see
|
|
44
43
|
// invite-create.write.ts. Dispatched in-process by runBootstrap.
|
|
@@ -47,7 +46,6 @@ export const AuthHandlers = {
|
|
|
47
46
|
inviteAcceptWithLogin: "auth-email-password:write:invite-accept-with-login",
|
|
48
47
|
inviteSignupComplete: "auth-email-password:write:invite-signup-complete",
|
|
49
48
|
switchTenantMfaGate: "auth-email-password:write:switch-tenant-mfa-gate",
|
|
50
|
-
inviteCancel: "auth-email-password:write:invite-cancel",
|
|
51
49
|
} as const;
|
|
52
50
|
|
|
53
51
|
// Qualified query names. Anonymous-readable status so the (unauthenticated)
|
|
@@ -196,12 +196,12 @@ export function createAuthEmailPasswordFeature(
|
|
|
196
196
|
{ key: "accountUnlock", label: "Account-Unlock-Flow", type: "boolean", default: false },
|
|
197
197
|
],
|
|
198
198
|
});
|
|
199
|
-
r.requires("user");
|
|
200
|
-
r.requires("tenant");
|
|
199
|
+
r.requires({ features: ["user"] });
|
|
200
|
+
r.requires({ features: ["tenant"] });
|
|
201
201
|
// All five magic-link flows (reset/verify/signup/invite/unlock) dispatch
|
|
202
202
|
// via ctx.notify → delivery must be mounted. Fail closed at boot instead
|
|
203
203
|
// of silently dropping the mail.
|
|
204
|
-
if (sendsMagicLinkMail(opts)) r.requires("delivery");
|
|
204
|
+
if (sendsMagicLinkMail(opts)) r.requires({ features: ["delivery"] });
|
|
205
205
|
r.envSchema(authEmailPasswordEnvSchema);
|
|
206
206
|
r.translations({ keys: AUTH_EMAIL_PASSWORD_MAIL_I18N });
|
|
207
207
|
|
|
@@ -292,6 +292,7 @@ export function createInviteCreateHandler(opts: InviteCreateOptions) {
|
|
|
292
292
|
name: "invite-create",
|
|
293
293
|
schema: InviteCreateSchema,
|
|
294
294
|
access: { roles: access.admin },
|
|
295
|
+
agent: { expose: false },
|
|
295
296
|
description:
|
|
296
297
|
"Invites an email address into the caller's tenant with a chosen membership role, creating or reusing the invitation and mailing the invitee a fresh accept link; re-inviting the same address invalidates the previous link.",
|
|
297
298
|
handler: async (event, ctx) => {
|
|
@@ -128,12 +128,12 @@ export function createAuthMfaFeature(opts: AuthMfaFeatureOptions): FeatureDefini
|
|
|
128
128
|
// shared variable; an app mounting auth-mfa without `secrets` still gets the
|
|
129
129
|
// KEK slot (and its _CIPHERTEXT twin) declared.
|
|
130
130
|
r.envSchema(secretsEnvSchema);
|
|
131
|
-
r.requires("user");
|
|
132
|
-
r.requires("config");
|
|
131
|
+
r.requires({ features: ["user"] });
|
|
132
|
+
r.requires({ features: ["config"] });
|
|
133
133
|
// verify.write.ts dispatches "tenant:query:memberships" — without this
|
|
134
134
|
// the app boots clean and the first MFA login dies at runtime on an
|
|
135
135
|
// unknown handler instead of failing at mount time.
|
|
136
|
-
r.requires("tenant");
|
|
136
|
+
r.requires({ features: ["tenant"] });
|
|
137
137
|
r.config("required", mfaRequiredConfigKey(opts.requiredPolicy));
|
|
138
138
|
|
|
139
139
|
// Declarative secretMint screen — mint (no input) -> reveal QR/secret/
|
|
@@ -162,7 +162,7 @@ export function createAuthMfaFeature(opts: AuthMfaFeatureOptions): FeatureDefini
|
|
|
162
162
|
),
|
|
163
163
|
});
|
|
164
164
|
|
|
165
|
-
r.entity("user-mfa", userMfaEntity);
|
|
165
|
+
r.entity({ name: "user-mfa", ...userMfaEntity });
|
|
166
166
|
|
|
167
167
|
// Late-bound by run-prod-app once the sessions feature (if mounted) has
|
|
168
168
|
// produced a concrete `sessionRevokeAllOthers` callback — mirrors
|
|
@@ -12,7 +12,7 @@ export const authMfaUserDataFeature = defineFeature("auth-mfa-user-data", (r) =>
|
|
|
12
12
|
r.describe(
|
|
13
13
|
"GDPR (Art. 20 export / Art. 17 erasure) coverage for the `auth-mfa` feature's `user-mfa` entity. Mounts the EXT_USER_DATA export + delete hooks so 2FA enrollment status is included in the user-data export bundle and a user's TOTP secret + recovery codes are hard-deleted (via executor.forget, rebuild-safe) on a data-subject erasure request. Kept separate from `auth-mfa` so consumers without the user-data-rights pipeline don't pull a hard dependency — requires `user-data-rights`, optionalRequires `auth-mfa`.",
|
|
14
14
|
);
|
|
15
|
-
r.requires("user-data-rights");
|
|
15
|
+
r.requires({ features: ["user-data-rights"] });
|
|
16
16
|
r.optionalRequires("auth-mfa");
|
|
17
17
|
r.useExtension(EXT_USER_DATA, "user-mfa", {
|
|
18
18
|
export: userMfaExportHook,
|
package/packages/bundled-features/src/billing-foundation/consumer-protection/register-consent.ts
CHANGED
|
@@ -46,7 +46,7 @@ export function registerConsumerProtection(
|
|
|
46
46
|
options: ResolvedBillingFoundationOptions,
|
|
47
47
|
consumerProtection: ConsumerProtectionOptions,
|
|
48
48
|
): void {
|
|
49
|
-
r.requires("template-resolver", "delivery", "user", "tenant");
|
|
49
|
+
r.requires({ features: ["template-resolver", "delivery", "user", "tenant"] });
|
|
50
50
|
registerConsumerProtectionMails(r);
|
|
51
51
|
registerContractTermination(r, options, consumerProtection);
|
|
52
52
|
r.defineEvent(CHECKOUT_CONSENT_RECORDED_EVENT_SHORT, checkoutConsentRecordedPayloadSchema, {
|
|
@@ -143,7 +143,7 @@ export function createBillingFoundationFeature<TTier extends string = string>(
|
|
|
143
143
|
category: "billing",
|
|
144
144
|
recommended: false,
|
|
145
145
|
});
|
|
146
|
-
r.requires("tenant-lifecycle", "compliance-profiles");
|
|
146
|
+
r.requires({ features: ["tenant-lifecycle", "compliance-profiles"] });
|
|
147
147
|
// 5 fine-grained domain-events. All 5 share the same payload shape (a
|
|
148
148
|
// subscription-state snapshot); the event type tags what happened.
|
|
149
149
|
// Future consumers (billing-history, accounting) listen directly on
|
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
deleteMany,
|
|
3
|
+
isUniqueViolation,
|
|
4
|
+
runInOwnTransaction,
|
|
5
|
+
type TenantDb,
|
|
6
|
+
} from "@cosmicdrift/kumiko-framework/db";
|
|
2
7
|
import type { SessionUser, WriteResult } from "@cosmicdrift/kumiko-framework/engine";
|
|
3
8
|
import { reraiseAsKumikoError } from "@cosmicdrift/kumiko-framework/errors";
|
|
4
9
|
import {
|
|
@@ -25,6 +30,7 @@ import {
|
|
|
25
30
|
import {
|
|
26
31
|
CAP_COUNTER_ROLLING_AGGREGATE_TYPE,
|
|
27
32
|
CAP_RESERVATION_TTL_MINUTES,
|
|
33
|
+
CAP_SETTLED_TOMBSTONE_TTL_MINUTES,
|
|
28
34
|
ROLLING_INCREMENTED_EVENT_QN,
|
|
29
35
|
ROLLING_RELEASED_EVENT_QN,
|
|
30
36
|
} from "./constants.js";
|
|
@@ -38,6 +44,7 @@ type CapReservationRow = {
|
|
|
38
44
|
readonly amount: number;
|
|
39
45
|
readonly expiresAt: Temporal.Instant;
|
|
40
46
|
readonly createdAt: Temporal.Instant;
|
|
47
|
+
readonly settledAt: Temporal.Instant | null;
|
|
41
48
|
};
|
|
42
49
|
|
|
43
50
|
const NOTHING_TO_RELEASE: WriteResult = { isSuccess: true, data: {} };
|
|
@@ -61,6 +68,7 @@ async function insertReservationRow(
|
|
|
61
68
|
amount: draft.amount,
|
|
62
69
|
expiresAt: now.add({ minutes: CAP_RESERVATION_TTL_MINUTES }),
|
|
63
70
|
createdAt: now,
|
|
71
|
+
settledAt: null,
|
|
64
72
|
});
|
|
65
73
|
}
|
|
66
74
|
|
|
@@ -185,7 +193,11 @@ async function confirmCapReservation(
|
|
|
185
193
|
confirm: ReservationConfirmContext,
|
|
186
194
|
id: string,
|
|
187
195
|
): Promise<void> {
|
|
188
|
-
await deleteMany(confirm.tx, capReservationsTable, {
|
|
196
|
+
await deleteMany(confirm.tx, capReservationsTable, {
|
|
197
|
+
id,
|
|
198
|
+
tenantId: confirm.tenantId,
|
|
199
|
+
settledAt: null,
|
|
200
|
+
});
|
|
189
201
|
}
|
|
190
202
|
|
|
191
203
|
async function giveBackCalendar(
|
|
@@ -229,7 +241,10 @@ async function giveBackReservation(
|
|
|
229
241
|
id: string,
|
|
230
242
|
): Promise<WriteResult> {
|
|
231
243
|
const ownRow = { id, tenantId: user.tenantId };
|
|
232
|
-
const row = await txDb.fetchOne<CapReservationRow>(capReservationsTable,
|
|
244
|
+
const row = await txDb.fetchOne<CapReservationRow>(capReservationsTable, {
|
|
245
|
+
...ownRow,
|
|
246
|
+
settledAt: null,
|
|
247
|
+
});
|
|
233
248
|
if (!row) return NOTHING_TO_RELEASE;
|
|
234
249
|
const claimed = await claimReservationRow(txDb, user, row);
|
|
235
250
|
if (!claimed) return NOTHING_TO_RELEASE;
|
|
@@ -242,7 +257,8 @@ async function giveBackReservation(
|
|
|
242
257
|
return given;
|
|
243
258
|
}
|
|
244
259
|
|
|
245
|
-
// Waits for a confirm still holding the row; false means that confirm
|
|
260
|
+
// Waits for a confirm still holding the row; false means that confirm, a settle or another release
|
|
261
|
+
// won (Postgres re-checks settled_at after the lock wait).
|
|
246
262
|
async function claimReservationRow(
|
|
247
263
|
txDb: TenantDb,
|
|
248
264
|
user: SessionUser,
|
|
@@ -251,7 +267,7 @@ async function claimReservationRow(
|
|
|
251
267
|
const claimed = await txDb.updateMany(
|
|
252
268
|
capReservationsTable,
|
|
253
269
|
{ expiresAt: row.expiresAt },
|
|
254
|
-
{ id: row.id, tenantId: user.tenantId },
|
|
270
|
+
{ id: row.id, tenantId: user.tenantId, settledAt: null },
|
|
255
271
|
);
|
|
256
272
|
return claimed.length > 0;
|
|
257
273
|
}
|
|
@@ -291,8 +307,61 @@ function bookCalendarDelta(
|
|
|
291
307
|
);
|
|
292
308
|
}
|
|
293
309
|
|
|
294
|
-
|
|
295
|
-
|
|
310
|
+
async function settleInTransaction(
|
|
311
|
+
txDb: TenantDb,
|
|
312
|
+
user: SessionUser,
|
|
313
|
+
id: string,
|
|
314
|
+
options: SettleCalendarCapOptions,
|
|
315
|
+
): Promise<WriteResult> {
|
|
316
|
+
const ownRow = { id, tenantId: user.tenantId };
|
|
317
|
+
const open = await txDb.fetchOne<CapReservationRow>(capReservationsTable, {
|
|
318
|
+
...ownRow,
|
|
319
|
+
settledAt: null,
|
|
320
|
+
});
|
|
321
|
+
if (open && (await claimReservationRow(txDb, user, open))) {
|
|
322
|
+
const booked = await bookCalendarDelta(txDb, user, options, options.actualAmount - open.amount);
|
|
323
|
+
if (!booked.isSuccess) return booked;
|
|
324
|
+
const now = Temporal.Now.instant();
|
|
325
|
+
await txDb.updateMany(
|
|
326
|
+
capReservationsTable,
|
|
327
|
+
{ settledAt: now, expiresAt: now.add({ minutes: CAP_SETTLED_TOMBSTONE_TTL_MINUTES }) },
|
|
328
|
+
ownRow,
|
|
329
|
+
);
|
|
330
|
+
return booked;
|
|
331
|
+
}
|
|
332
|
+
// The claim lost to a concurrent settle or release: a tombstone means the amount is booked.
|
|
333
|
+
if (await txDb.fetchOne<CapReservationRow>(capReservationsTable, ownRow)) {
|
|
334
|
+
return NOTHING_TO_RELEASE;
|
|
335
|
+
}
|
|
336
|
+
// Row swept or released: its amount was given back, so the full actual amount is booked and a
|
|
337
|
+
// tombstone makes a repeated settle a no-op. A parallel settle inserting first fails this insert.
|
|
338
|
+
const now = Temporal.Now.instant();
|
|
339
|
+
await txDb.insertOne(capReservationsTable, {
|
|
340
|
+
...ownRow,
|
|
341
|
+
capName: options.capName,
|
|
342
|
+
kind: "calendar",
|
|
343
|
+
periodStartIso: options.periodStartIso,
|
|
344
|
+
amount: options.actualAmount,
|
|
345
|
+
expiresAt: now.add({ minutes: CAP_SETTLED_TOMBSTONE_TTL_MINUTES }),
|
|
346
|
+
createdAt: now,
|
|
347
|
+
settledAt: now,
|
|
348
|
+
});
|
|
349
|
+
return bookCalendarDelta(txDb, user, options, options.actualAmount);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// The failed tombstone insert aborts the whole attempt, so nothing was booked; the repeat sees the
|
|
353
|
+
// winner's tombstone.
|
|
354
|
+
async function retryOnceOnTombstoneRace(attempt: () => Promise<WriteResult>): Promise<WriteResult> {
|
|
355
|
+
try {
|
|
356
|
+
return await attempt();
|
|
357
|
+
} catch (error) {
|
|
358
|
+
if (!isUniqueViolation(error)) throw error;
|
|
359
|
+
return attempt();
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// Replaces the reserved amount by the actual one in a single transaction and keeps the row as a
|
|
364
|
+
// settled tombstone, so a repeated settle (retry, lost ack) books nothing twice.
|
|
296
365
|
export async function settleCalendarCapReservation(
|
|
297
366
|
ctx: CapCounterContext,
|
|
298
367
|
id: string,
|
|
@@ -306,41 +375,33 @@ export async function settleCalendarCapReservation(
|
|
|
306
375
|
}
|
|
307
376
|
const { user, writeDb } = resolveCapCounterScope(ctx, outsideTransaction);
|
|
308
377
|
const settled = await retryCounterWriteOnVersionConflict(() =>
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
tenantId: user.tenantId,
|
|
313
|
-
});
|
|
314
|
-
const claimed = row ? await claimReservationRow(txDb, user, row) : false;
|
|
315
|
-
if (!row || !claimed) {
|
|
316
|
-
return bookCalendarDelta(txDb, user, options, options.actualAmount);
|
|
317
|
-
}
|
|
318
|
-
const booked = await bookCalendarDelta(
|
|
319
|
-
txDb,
|
|
320
|
-
user,
|
|
321
|
-
options,
|
|
322
|
-
options.actualAmount - row.amount,
|
|
323
|
-
);
|
|
324
|
-
if (!booked.isSuccess) return booked;
|
|
325
|
-
await txDb.deleteMany(capReservationsTable, { id, tenantId: user.tenantId });
|
|
326
|
-
return booked;
|
|
327
|
-
}),
|
|
378
|
+
retryOnceOnTombstoneRace(() =>
|
|
379
|
+
runInOwnTransaction(writeDb, (txDb) => settleInTransaction(txDb, user, id, options)),
|
|
380
|
+
),
|
|
328
381
|
);
|
|
329
382
|
if (!settled.isSuccess) throw reraiseAsKumikoError(settled.error);
|
|
330
383
|
}
|
|
331
384
|
|
|
332
385
|
// A failed release leaves its row behind; giving it back once the TTL passed bounds how long that
|
|
333
|
-
// capacity stays counted.
|
|
386
|
+
// capacity stays counted. Expired settled tombstones are only deleted: their amount stays booked.
|
|
334
387
|
export async function releaseExpiredCapReservations(
|
|
335
388
|
ctx: CapCounterContext,
|
|
336
389
|
capName: string,
|
|
337
390
|
outsideTransaction = true,
|
|
338
391
|
): Promise<void> {
|
|
339
392
|
const { user, writeDb } = resolveCapCounterScope(ctx, outsideTransaction);
|
|
340
|
-
const
|
|
393
|
+
const expiredWhere = {
|
|
341
394
|
tenantId: user.tenantId,
|
|
342
395
|
capName,
|
|
343
396
|
expiresAt: { lt: Temporal.Now.instant() },
|
|
397
|
+
};
|
|
398
|
+
await writeDb.deleteMany(capReservationsTable, {
|
|
399
|
+
...expiredWhere,
|
|
400
|
+
settledAt: { ne: null },
|
|
401
|
+
});
|
|
402
|
+
const expired = await writeDb.selectMany<Pick<CapReservationRow, "id">>(capReservationsTable, {
|
|
403
|
+
...expiredWhere,
|
|
404
|
+
settledAt: null,
|
|
344
405
|
});
|
|
345
406
|
for (const { id } of expired) {
|
|
346
407
|
try {
|
|
@@ -1,4 +1,11 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.366.0",
|
|
4
|
+
"type": "fix",
|
|
5
|
+
"title": "cap-counter calendar settle is idempotent",
|
|
6
|
+
"detail": "Settling a calendar cap reservation now keeps the row as a tombstone (new column store_cap_reservations.settled_at, retained for 24 hours) instead of deleting it. A repeated settle with the same reservation id is a no-op, and confirm, give-back and expiry sweep ignore settled rows. The sweep deletes expired tombstones.",
|
|
7
|
+
"migration": "Apps with cap-counter generate a migration for the new column store_cap_reservations.settled_at (kumiko migrate generate)."
|
|
8
|
+
},
|
|
2
9
|
{
|
|
3
10
|
"version": "0.365.0",
|
|
4
11
|
"type": "breaking",
|
|
@@ -28,6 +28,9 @@ export const ROLLING_RELEASED_EVENT_QN = "cap-counter:event:rolling-released" as
|
|
|
28
28
|
// reservation for the same cap, which bounds the over-count a failed release can leave behind.
|
|
29
29
|
export const CAP_RESERVATION_TTL_MINUTES = 60;
|
|
30
30
|
|
|
31
|
+
// A settled reservation stays as a tombstone this long so a retried or lost-ack settle stays a no-op.
|
|
32
|
+
export const CAP_SETTLED_TOMBSTONE_TTL_MINUTES = 24 * 60;
|
|
33
|
+
|
|
31
34
|
// Qualified write handler names (QN format: scope:type:name).
|
|
32
35
|
export const CapCounterHandlers = {
|
|
33
36
|
increment: "cap-counter:write:increment",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// cap-counter — Counter-Storage + Increment-API + Soft-Warn-State.
|
|
2
2
|
//
|
|
3
3
|
// **Was diese Feature liefert:**
|
|
4
|
-
// 1. r.entity("cap-counter") — Counter-Rows pro (tenant, capName,
|
|
4
|
+
// 1. r.entity({ name: "cap-counter" }) — Counter-Rows pro (tenant, capName,
|
|
5
5
|
// period) für Calendar-Period-Caps (Mails/Monat, Egress/Monat).
|
|
6
6
|
// 2. increment-Handler — atomic counter increment via deterministic
|
|
7
7
|
// aggregate-id (try-create / executor-update). Race-frei via
|
|
@@ -77,7 +77,7 @@ export const capCounterFeature = defineFeature(CAP_COUNTER_FEATURE, (r) => {
|
|
|
77
77
|
category: "operations",
|
|
78
78
|
recommended: false,
|
|
79
79
|
});
|
|
80
|
-
r.entity("cap-counter", capCounterEntity);
|
|
80
|
+
r.entity({ name: "cap-counter", ...capCounterEntity });
|
|
81
81
|
r.storeTable(capReservationsTableMeta, {
|
|
82
82
|
reason: "coordination.cap_reservation_ledger",
|
|
83
83
|
});
|
|
@@ -14,8 +14,9 @@ const CAP_RESERVATIONS_TENANT_CAP_EXPIRES_INDEX =
|
|
|
14
14
|
|
|
15
15
|
// One row per capacity that was booked before a handler transaction and is not settled yet. The
|
|
16
16
|
// handler transaction deletes it on its own commit (confirm); a release or the TTL sweep deletes it
|
|
17
|
-
// together with the counter decrement.
|
|
18
|
-
//
|
|
17
|
+
// together with the counter decrement. A settle keeps the row as a tombstone (settled_at set) until
|
|
18
|
+
// its shortened TTL passes, so a repeated settle or a late release finds it and does nothing.
|
|
19
|
+
// Unmanaged: the rows are a coordination aid, not state with an event history.
|
|
19
20
|
export const capReservationsTable = pgTable(
|
|
20
21
|
"store_cap_reservations",
|
|
21
22
|
{
|
|
@@ -28,6 +29,7 @@ export const capReservationsTable = pgTable(
|
|
|
28
29
|
amount: integer("amount").notNull(),
|
|
29
30
|
expiresAt: instant("expires_at").notNull(),
|
|
30
31
|
createdAt: instant("created_at").notNull(),
|
|
32
|
+
settledAt: instant("settled_at"),
|
|
31
33
|
},
|
|
32
34
|
(t) => [index(CAP_RESERVATIONS_TENANT_CAP_EXPIRES_INDEX).on(t.tenantId, t.capName, t.expiresAt)],
|
|
33
35
|
);
|
|
@@ -49,5 +51,6 @@ export const capReservationsTableMeta: EntityTableMeta = defineUnmanagedTable({
|
|
|
49
51
|
{ name: "amount", pgType: "integer", notNull: true },
|
|
50
52
|
{ name: "expires_at", pgType: "timestamptz", notNull: true },
|
|
51
53
|
{ name: "created_at", pgType: "timestamptz", notNull: true },
|
|
54
|
+
{ name: "settled_at", pgType: "timestamptz", notNull: false },
|
|
52
55
|
],
|
|
53
56
|
});
|
|
@@ -70,9 +70,9 @@ export function createCapOverviewFeature(opts: CreateCapOverviewOptions): Featur
|
|
|
70
70
|
recommended: false,
|
|
71
71
|
});
|
|
72
72
|
r.systemScope();
|
|
73
|
-
r.requires("tenant");
|
|
74
|
-
r.requires("tier-engine");
|
|
75
|
-
r.requires("billing-foundation");
|
|
73
|
+
r.requires({ features: ["tenant"] });
|
|
74
|
+
r.requires({ features: ["tier-engine"] });
|
|
75
|
+
r.requires({ features: ["billing-foundation"] });
|
|
76
76
|
|
|
77
77
|
r.queryHandler(createTenantCapsListQuery(opts.caps, listCaps));
|
|
78
78
|
r.queryHandler(createCapsUsageQuery(opts.caps, usageRoles));
|
|
@@ -24,8 +24,8 @@ export function createChannelDiscordFeature(
|
|
|
24
24
|
category: "notifications",
|
|
25
25
|
recommended: false,
|
|
26
26
|
});
|
|
27
|
-
r.requires("delivery");
|
|
28
|
-
r.requires("secrets");
|
|
27
|
+
r.requires({ features: ["delivery"] });
|
|
28
|
+
r.requires({ features: ["secrets"] });
|
|
29
29
|
|
|
30
30
|
const webhookTarget = resolveChatWebhookTarget(
|
|
31
31
|
{
|
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.367.0",
|
|
4
|
+
"type": "improvement",
|
|
5
|
+
"title": "Email channel can send through the tenant's own mail provider",
|
|
6
|
+
"detail": "EmailChannelOptions gets tenantTransport, a resolver called per send with the delivery channel context. mail-foundation exports tenantMailTransport for it: a tenant with a provider configured sends through its own transport, a broken provider fails the send, a tenant without a provider uses the app transport. The transport is built per send and never cached, and the tenant comes from the server-side delivery context only. ChannelContext now carries the tenant's config accessor as config. Without tenantTransport nothing changes."
|
|
7
|
+
},
|
|
2
8
|
{
|
|
3
9
|
"version": "0.365.0",
|
|
4
10
|
"type": "fix",
|