@cosmicdrift/kumiko-bundled-features 0.333.0 → 0.335.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/admin-shell/overview-panels.js +2 -2
- package/dist/agent-tools/agent-doc-lint.d.ts +1 -0
- package/dist/agent-tools/agent-doc-lint.js +33 -3
- package/dist/agent-tools/agent-manifest.js +47 -6
- package/dist/agent-tools/filter-ops.d.ts +2 -0
- package/dist/agent-tools/filter-ops.js +4 -0
- package/dist/agent-tools/tool-catalog.js +1 -4
- package/dist/agent-tools/tool-dispatch.js +1 -4
- package/dist/audit/constants.d.ts +1 -1
- package/dist/audit/constants.js +1 -1
- package/dist/audit/handlers/list.query.js +4 -3
- package/dist/auth-email-password/handlers/change-password.write.js +2 -0
- package/dist/auth-email-password/handlers/invite-create.write.js +12 -2
- package/dist/auth-email-password/handlers/login.write.js +2 -0
- package/dist/auth-email-password/handlers/self-registration-status.query.js +1 -1
- package/dist/auth-email-password/i18n.js +2 -0
- package/dist/auth-email-password/web/auth-client.d.ts +1 -0
- package/dist/auth-email-password/web/auth-client.js +1 -1
- package/dist/auth-email-password/web/auth-gate.js +6 -8
- package/dist/auth-email-password/web/index.d.ts +1 -1
- package/dist/auth-email-password/web/index.js +1 -1
- package/dist/auth-email-password/web/invite-accept-screen.js +16 -12
- package/dist/auth-email-password/web/login-screen.js +1 -1
- package/dist/auth-email-password/web/session-bootstrap-error.d.ts +5 -1
- package/dist/auth-email-password/web/session-bootstrap-error.js +2 -2
- package/dist/auth-email-password/web/session.d.ts +4 -0
- package/dist/auth-email-password/web/session.js +15 -0
- package/dist/auth-mfa/feature.js +3 -2
- package/dist/auth-mfa/handlers/enable-confirm-preauth.write.js +2 -2
- package/dist/auth-mfa/handlers/verify.write.js +2 -0
- package/dist/auth-mfa/hmac-token-codec.js +3 -0
- package/dist/auth-mfa/mfa-challenge-token.js +4 -0
- package/dist/auth-mfa/mfa-preauth-setup-token.js +3 -0
- package/dist/auth-mfa/schema/user-mfa.d.ts +6 -0
- package/dist/auth-mfa/web/mfa-client.d.ts +1 -1
- package/dist/auth-mfa/web/mfa-client.js +4 -10
- package/dist/billing-foundation/billing-info-query.js +1 -1
- package/dist/billing-foundation/checkout-core.d.ts +5 -5
- package/dist/billing-foundation/checkout-core.js +10 -6
- package/dist/billing-foundation/entities.d.ts +8 -0
- package/dist/billing-foundation/handlers/switch-plan.write.js +5 -8
- package/dist/billing-foundation/projection.d.ts +8 -0
- package/dist/cap-counter/book-cap-usage.js +15 -5
- package/dist/cap-counter/enforce-cap.js +9 -3
- package/dist/cap-counter/entity.d.ts +1 -0
- package/dist/cap-counter/feature.js +3 -2
- package/dist/cap-counter/i18n.d.ts +1 -0
- package/dist/cap-counter/i18n.js +6 -5
- package/dist/cap-overview/handlers/caps-usage.query.js +9 -16
- package/dist/cap-overview/handlers/tenant-caps-list.query.js +41 -15
- package/dist/cap-overview/i18n.js +5 -0
- package/dist/cap-overview/web/cap-cards-panel.js +4 -1
- package/dist/cap-overview/web/cap-usage-bar.js +1 -1
- package/dist/channel-email/email-channel.js +12 -1
- package/dist/channel-in-app/tables.js +2 -6
- package/dist/compliance-profiles/handlers/sub-processors.query.js +1 -1
- package/dist/compliance-profiles/schema/profile-selection.d.ts +2 -2
- package/dist/config/db/queries/resolver.js +3 -1
- package/dist/config/handlers/set.write.js +8 -4
- package/dist/config/table.d.ts +6 -0
- package/dist/config/web/i18n.js +13 -4
- package/dist/config/write-helpers.d.ts +1 -1
- package/dist/config/write-helpers.js +11 -5
- package/dist/crypto-shredding/handlers/forget-subject.write.js +15 -8
- package/dist/custom-fields/constants.js +1 -4
- package/dist/custom-fields/entity.d.ts +4 -0
- package/dist/data-retention/resolve-for-tenant.d.ts +2 -3
- package/dist/data-retention/run-retention-cleanup.js +100 -44
- package/dist/data-retention/schema/tenant-retention-override.d.ts +4 -0
- package/dist/delivery/tables.d.ts +6 -0
- package/dist/delivery/types.d.ts +2 -1
- package/dist/delivery/types.js +7 -0
- package/dist/delivery/unsubscribe.js +5 -11
- package/dist/derivatives-sharp/render.js +27 -9
- package/dist/document-ingest-foundation/entity.d.ts +4 -0
- package/dist/document-ingest-foundation/feature.js +5 -5
- package/dist/document-ingest-foundation/providers.d.ts +1 -0
- package/dist/document-ingest-foundation/providers.js +6 -0
- package/dist/document-ingest-foundation/write-document-extract.d.ts +2 -2
- package/dist/document-ingest-foundation/write-document-extract.js +10 -0
- package/dist/feature-toggles/global-feature-state-table.js +2 -7
- package/dist/feature-toggles/toggle-sync-signal.d.ts +2 -1
- package/dist/feature-toggles/toggle-sync-signal.js +3 -2
- package/dist/file-provider-s3/feature.js +1 -1
- package/dist/files-tenant-data/index.js +3 -2
- package/dist/folders/entity.d.ts +8 -0
- package/dist/folders/handlers/clear-folder.write.js +4 -11
- package/dist/folders/handlers/set-folder.write.js +4 -10
- package/dist/form-draft/entity.d.ts +2 -0
- package/dist/form-draft/feature.js +3 -2
- package/dist/form-draft/handlers/discard.write.js +7 -8
- package/dist/form-draft/handlers/get.query.js +7 -1
- package/dist/foundation-shared/host-allowlist.d.ts +4 -0
- package/dist/foundation-shared/host-allowlist.js +14 -0
- package/dist/foundation-shared/index.d.ts +1 -0
- package/dist/foundation-shared/index.js +1 -0
- package/dist/foundation-shared/mail-host-policy.d.ts +0 -2
- package/dist/foundation-shared/mail-host-policy.js +3 -13
- package/dist/inbound-mail-foundation/connect-routes.js +7 -5
- package/dist/inbound-mail-foundation/entities.d.ts +25 -0
- package/dist/inbound-mail-foundation/projection.d.ts +20 -0
- package/dist/inbound-mail-foundation/watch-supervisor.js +47 -43
- package/dist/jobs/handlers/list.query.d.ts +7 -2
- package/dist/jobs/handlers/list.query.js +9 -2
- package/dist/jobs/handlers/retry.write.js +3 -2
- package/dist/jobs/handlers/tenant-failures.query.js +3 -9
- package/dist/jobs/job-run-logger.js +18 -2
- package/dist/jobs/job-run-table.d.ts +6 -0
- package/dist/jobs/tenant-job-failure-table.js +2 -6
- package/dist/ledger/entity.d.ts +16 -3
- package/dist/ledger/handlers/confirm-schedule-period.write.js +2 -2
- package/dist/ledger/handlers/reverse-transaction.write.js +2 -2
- package/dist/ledger/subject-reference.d.ts +4 -0
- package/dist/ledger/subject-reference.js +7 -0
- package/dist/legal-pages/feature.js +8 -1
- package/dist/managed-pages/handlers/branding.query.js +1 -1
- package/dist/managed-pages/handlers/by-slug.query.js +1 -1
- package/dist/managed-pages/handlers/by-tenant-published.query.js +1 -1
- package/dist/managed-pages/table.d.ts +12 -0
- package/dist/metrics/compute.js +7 -1
- package/dist/metrics/window.js +5 -0
- package/dist/notes-history/entity.d.ts +12 -0
- package/dist/notes-history/handlers/add-note.write.js +4 -9
- package/dist/notes-history-user-data/hooks.js +8 -1
- package/dist/personal-access-tokens/constants.d.ts +1 -0
- package/dist/personal-access-tokens/constants.js +1 -0
- package/dist/personal-access-tokens/feature.js +2 -2
- package/dist/personal-access-tokens/handlers/create.write.js +2 -2
- package/dist/personal-access-tokens/handlers/revoke.write.js +2 -2
- package/dist/personal-access-tokens/i18n.js +12 -13
- package/dist/personal-access-tokens/schema/api-token.d.ts +6 -0
- package/dist/personal-access-tokens/schema/api-token.js +2 -1
- package/dist/personal-access-tokens/screens.js +5 -1
- package/dist/rate-limiting/feature.js +2 -0
- package/dist/rate-limiting/handlers/bucket-access.js +15 -7
- package/dist/rate-limiting/i18n.d.ts +10 -0
- package/dist/rate-limiting/i18n.js +10 -0
- package/dist/secrets/feature.js +3 -2
- package/dist/secrets/table.d.ts +1 -0
- package/dist/seo/handlers/seo-config.query.js +1 -1
- package/dist/sessions/constants.d.ts +1 -0
- package/dist/sessions/constants.js +1 -0
- package/dist/sessions/feature.js +5 -3
- package/dist/sessions/handlers/revoke-all-for-user.write.d.ts +1 -0
- package/dist/sessions/handlers/revoke-all-for-user.write.js +12 -2
- package/dist/sessions/handlers/revoke.write.js +2 -2
- package/dist/sessions/schema/user-session.d.ts +4 -0
- package/dist/sessions/schema/user-session.js +2 -1
- package/dist/sessions/screens.js +5 -1
- package/dist/sessions/session-callbacks.js +2 -2
- package/dist/shared/index.d.ts +1 -1
- package/dist/shared/index.js +1 -1
- package/dist/shared/lockout-counter.js +1 -1
- package/dist/shared/parent-visibility.d.ts +5 -0
- package/dist/shared/parent-visibility.js +17 -1
- package/dist/shared/row-bound-grant.js +20 -9
- package/dist/shared/single-use-token-store.js +5 -13
- package/dist/step-dispatcher/webhook-runner.d.ts +0 -2
- package/dist/step-dispatcher/webhook-runner.js +18 -17
- package/dist/subscription-mollie/plugin-methods.d.ts +3 -2
- package/dist/subscription-mollie/plugin-methods.js +7 -1
- package/dist/subscription-mollie/verify-webhook.js +6 -0
- package/dist/subscription-stripe/feature.js +3 -3
- package/dist/subscription-stripe/plugin-methods.js +25 -6
- package/dist/subscription-stripe/verify-webhook.js +13 -6
- package/dist/tags/entity.d.ts +9 -0
- package/dist/tags/handlers/assign-tag.write.js +4 -10
- package/dist/tags/handlers/remove-tag.write.js +4 -11
- package/dist/template-resolver/api.d.ts +2 -1
- package/dist/template-resolver/api.js +1 -1
- package/dist/template-resolver/handlers/by-slug.query.d.ts +1 -1
- package/dist/template-resolver/handlers/by-slug.query.js +2 -2
- package/dist/template-resolver/handlers/by-tenant.query.d.ts +3 -2
- package/dist/template-resolver/handlers/by-tenant.query.js +2 -2
- package/dist/template-resolver/handlers/collection-item.query.d.ts +1 -1
- package/dist/template-resolver/handlers/collection-list.query.d.ts +1 -1
- package/dist/template-resolver/handlers/collection-shared.d.ts +3 -2
- package/dist/template-resolver/handlers/collection-shared.js +1 -1
- package/dist/template-resolver/handlers/find-by-id.query.d.ts +1 -1
- package/dist/template-resolver/handlers/find-by-id.query.js +1 -1
- package/dist/template-resolver/handlers/list.query.d.ts +1 -1
- package/dist/template-resolver/handlers/list.query.js +1 -1
- package/dist/template-resolver/seeding.js +41 -39
- package/dist/template-resolver/table.d.ts +26 -13
- package/dist/template-resolver/testing.js +2 -2
- package/dist/template-resolver/user-content-table.d.ts +16 -9
- package/dist/template-resolver/web/client-plugin.d.ts +1 -1
- package/dist/template-resolver-user-data/hooks.js +1 -1
- package/dist/tenant/cancel-pending-invitation.d.ts +8 -0
- package/dist/tenant/cancel-pending-invitation.js +22 -0
- package/dist/tenant/feature.js +4 -2
- package/dist/tenant/handlers/cancel-invitation.write.js +6 -21
- package/dist/tenant/handlers/member-directory.query.d.ts +1 -0
- package/dist/tenant/handlers/member-directory.query.js +17 -4
- package/dist/tenant/handlers/remove-member.write.js +9 -14
- package/dist/tenant/handlers/team-list.query.js +2 -2
- package/dist/tenant/handlers/tenant-directory.query.d.ts +1 -0
- package/dist/tenant/handlers/tenant-directory.query.js +13 -3
- package/dist/tenant/handlers/update-member-roles.write.d.ts +1 -1
- package/dist/tenant/handlers/update-member-roles.write.js +28 -12
- package/dist/tenant/i18n.js +4 -0
- package/dist/tenant/index.d.ts +1 -1
- package/dist/tenant/index.js +1 -1
- package/dist/tenant/invitation-table.d.ts +8 -2
- package/dist/tenant/last-tenant-admin.d.ts +1 -1
- package/dist/tenant/last-tenant-admin.js +5 -3
- package/dist/tenant/membership-table.d.ts +4 -0
- package/dist/tenant/schema/tenant.d.ts +8 -2
- package/dist/tenant/seeding.d.ts +4 -0
- package/dist/tenant/seeding.js +9 -2
- package/dist/tenant-lifecycle/boot-checks.d.ts +1 -0
- package/dist/tenant-lifecycle/boot-checks.js +27 -1
- package/dist/tenant-lifecycle/feature.js +2 -1
- package/dist/tenant-lifecycle/stages.js +2 -2
- package/dist/tenant-settings/feature.js +9 -6
- package/dist/tier-engine/entity.d.ts +2 -0
- package/dist/tier-engine/feature.js +3 -2
- package/dist/tier-engine/tier-resolver.d.ts +2 -0
- package/dist/user/command-schemas.d.ts +2 -2
- package/dist/user/handlers/create.write.d.ts +1 -1
- package/dist/user/handlers/create.write.js +8 -2
- package/dist/user/handlers/update.write.d.ts +1 -1
- package/dist/user/handlers/update.write.js +8 -2
- package/dist/user/schema/user.d.ts +14 -2
- package/dist/user-data-rights/db/queries/export-jobs.d.ts +2 -0
- package/dist/user-data-rights/feature.js +22 -9
- package/dist/user-data-rights/handlers/confirm-deletion-by-token.write.js +4 -7
- package/dist/user-data-rights/handlers/deletion-grace-period.d.ts +3 -2
- package/dist/user-data-rights/handlers/deletion-grace-period.js +30 -8
- package/dist/user-data-rights/handlers/download-by-token.query.js +1 -1
- package/dist/user-data-rights/handlers/request-deletion-by-email.write.js +1 -1
- package/dist/user-data-rights/handlers/request-deletion.write.js +3 -6
- package/dist/user-data-rights/lib/update-user-lifecycle.js +7 -14
- package/dist/user-data-rights/run-export-jobs.d.ts +1 -0
- package/dist/user-data-rights/run-export-jobs.js +74 -41
- package/dist/user-data-rights/run-forget-cleanup.js +2 -2
- package/dist/user-data-rights/run-user-export.js +8 -4
- package/dist/user-data-rights/schema/download-attempt.d.ts +14 -0
- package/dist/user-data-rights/schema/download-token.d.ts +8 -0
- package/dist/user-data-rights/schema/export-job.d.ts +8 -2
- package/dist/user-data-rights-defaults/hooks/file-ref.userdata-hook.js +4 -1
- package/dist/user-profile/feature.js +6 -1
- package/dist/user-profile/handlers/change-email.write.js +2 -0
- package/dist/workflow-runner/db/queries/due-runs.js +6 -9
- package/dist/workflow-runner/event-subscriber.js +29 -9
- package/dist/workflow-runner/handlers/resume-run.write.js +8 -5
- package/dist/workflow-runner/pending-projection.js +8 -1
- package/package.json +11 -9
- package/src/agent-tools/changes.json +12 -0
- package/src/auth-email-password/changes.json +18 -0
- package/src/auth-mfa/changes.json +6 -0
- package/src/billing-foundation/changes.json +7 -0
- package/src/cap-counter/changes.json +7 -0
- package/src/cap-overview/changes.json +12 -0
- package/src/channel-email/changes.json +6 -0
- package/src/config/changes.json +6 -0
- package/src/crypto-shredding/changes.json +6 -0
- package/src/data-retention/changes.json +12 -0
- package/src/delivery/changes.json +7 -0
- package/src/derivatives-sharp/changes.json +11 -0
- package/src/document-ingest-foundation/changes.json +12 -0
- package/src/inbound-mail-foundation/changes.json +12 -0
- package/src/jobs/changes.json +13 -0
- package/src/legal-pages/changes.json +8 -1
- package/src/managed-pages/changes.json +5 -0
- package/src/metrics/changes.json +6 -0
- package/src/rate-limiting/changes.json +5 -0
- package/src/secrets/changes.json +7 -0
- package/src/sessions/changes.json +5 -0
- package/src/shared/changes.json +6 -0
- package/src/step-dispatcher/changes.json +5 -0
- package/src/subscription-mollie/changes.json +7 -0
- package/src/subscription-stripe/changes.json +11 -0
- package/src/template-resolver/changes.json +7 -0
- package/src/tenant/changes.json +29 -0
- package/src/user-data-rights/changes.json +22 -0
- package/src/workflow-runner/changes.json +6 -0
|
@@ -33,6 +33,8 @@ export function createMfaVerifyHandler(opts) {
|
|
|
33
33
|
"Also reads the MFA enrollment of the tenant named in the signed login/setup token, not the " +
|
|
34
34
|
"guest dispatch tenant.",
|
|
35
35
|
},
|
|
36
|
+
// Payload carries the TOTP/recovery code and the challenge token.
|
|
37
|
+
agent: { expose: false },
|
|
36
38
|
description: "Finishes a two-step sign-in by checking a TOTP or recovery code against the challenge token that login handed back, under a per-account attempt cap, and derives the resulting session.",
|
|
37
39
|
handler: async (event, ctx) => {
|
|
38
40
|
const verified = verifyMfaChallengeToken(event.payload.challengeToken, opts.challengeTokenSecret);
|
|
@@ -3,6 +3,9 @@ import { Temporal } from "temporal-polyfill";
|
|
|
3
3
|
function hmacSign(input, secret) {
|
|
4
4
|
return createHmac("sha256", secret).update(input).digest("base64url");
|
|
5
5
|
}
|
|
6
|
+
// `domain` is part of the signed input: it is the only thing that stops a token
|
|
7
|
+
// minted for one MFA step from verifying as another (e.g. a preauth-setup token
|
|
8
|
+
// replayed as a challenge token), since both share the same secret and payload shape.
|
|
6
9
|
export function createHmacTokenCodec(domain, isPayload) {
|
|
7
10
|
function isEncodedBody(value) {
|
|
8
11
|
if (typeof value !== "object" || value === null)
|
|
@@ -6,6 +6,10 @@ function isPayload(value) {
|
|
|
6
6
|
const v = value;
|
|
7
7
|
return typeof v["userId"] === "string" && typeof v["tenantId"] === "string";
|
|
8
8
|
}
|
|
9
|
+
// Domain-separated from mfa-preauth-setup (see hmac-token-codec). Single-use is
|
|
10
|
+
// enforced by the consumer via burnToken (purpose "mfa-challenge"); this codec
|
|
11
|
+
// only signs and verifies. Brute-forcing the code behind a valid token is
|
|
12
|
+
// limited separately in mfa-verify-attempts.ts.
|
|
9
13
|
const codec = createHmacTokenCodec("mfa-challenge", isPayload);
|
|
10
14
|
export function signMfaChallengeToken(payload, ttlMinutes, secret, now = Temporal.Now.instant()) {
|
|
11
15
|
return codec.sign(payload, ttlMinutes, secret, now);
|
|
@@ -6,6 +6,9 @@ function isPayload(value) {
|
|
|
6
6
|
const v = value;
|
|
7
7
|
return typeof v["userId"] === "string" && typeof v["tenantId"] === "string";
|
|
8
8
|
}
|
|
9
|
+
// Domain "mfa-preauth-setup" must never equal the challenge domain: a setup
|
|
10
|
+
// token has to fail verification as a challenge token and vice versa. The
|
|
11
|
+
// challenge token secret is reused deliberately; the domain carries the separation.
|
|
9
12
|
const codec = createHmacTokenCodec("mfa-preauth-setup", isPayload);
|
|
10
13
|
export function signMfaPreauthSetupToken(payload, ttlMinutes, secret, now = Temporal.Now.instant()) {
|
|
11
14
|
return codec.sign(payload, ttlMinutes, secret, now);
|
|
@@ -9,6 +9,7 @@ export declare const userMfaEntity: import("@cosmicdrift/kumiko-types/fields").E
|
|
|
9
9
|
readonly filterable?: boolean;
|
|
10
10
|
readonly encrypted?: boolean;
|
|
11
11
|
readonly sensitive?: boolean;
|
|
12
|
+
readonly writeOnly?: boolean;
|
|
12
13
|
readonly readAsInstruction?: boolean;
|
|
13
14
|
readonly format?: "email" | "url" | "phone" | "password";
|
|
14
15
|
readonly default?: string;
|
|
@@ -29,6 +30,7 @@ export declare const userMfaEntity: import("@cosmicdrift/kumiko-types/fields").E
|
|
|
29
30
|
readonly filterable?: boolean;
|
|
30
31
|
readonly encrypted?: boolean;
|
|
31
32
|
readonly sensitive?: boolean;
|
|
33
|
+
readonly writeOnly?: boolean;
|
|
32
34
|
readonly readAsInstruction?: boolean;
|
|
33
35
|
readonly format?: "email" | "url" | "phone" | "password";
|
|
34
36
|
readonly default?: string;
|
|
@@ -49,6 +51,7 @@ export declare const userMfaEntity: import("@cosmicdrift/kumiko-types/fields").E
|
|
|
49
51
|
readonly filterable?: boolean;
|
|
50
52
|
readonly encrypted?: boolean;
|
|
51
53
|
readonly sensitive?: boolean;
|
|
54
|
+
readonly writeOnly?: boolean;
|
|
52
55
|
readonly readAsInstruction?: boolean;
|
|
53
56
|
readonly format?: "email" | "url" | "phone" | "password";
|
|
54
57
|
readonly default?: string;
|
|
@@ -101,6 +104,7 @@ export declare const userMfaTable: import("@cosmicdrift/kumiko-framework/db").En
|
|
|
101
104
|
readonly filterable?: boolean;
|
|
102
105
|
readonly encrypted?: boolean;
|
|
103
106
|
readonly sensitive?: boolean;
|
|
107
|
+
readonly writeOnly?: boolean;
|
|
104
108
|
readonly readAsInstruction?: boolean;
|
|
105
109
|
readonly format?: "email" | "url" | "phone" | "password";
|
|
106
110
|
readonly default?: string;
|
|
@@ -121,6 +125,7 @@ export declare const userMfaTable: import("@cosmicdrift/kumiko-framework/db").En
|
|
|
121
125
|
readonly filterable?: boolean;
|
|
122
126
|
readonly encrypted?: boolean;
|
|
123
127
|
readonly sensitive?: boolean;
|
|
128
|
+
readonly writeOnly?: boolean;
|
|
124
129
|
readonly readAsInstruction?: boolean;
|
|
125
130
|
readonly format?: "email" | "url" | "phone" | "password";
|
|
126
131
|
readonly default?: string;
|
|
@@ -141,6 +146,7 @@ export declare const userMfaTable: import("@cosmicdrift/kumiko-framework/db").En
|
|
|
141
146
|
readonly filterable?: boolean;
|
|
142
147
|
readonly encrypted?: boolean;
|
|
143
148
|
readonly sensitive?: boolean;
|
|
149
|
+
readonly writeOnly?: boolean;
|
|
144
150
|
readonly readAsInstruction?: boolean;
|
|
145
151
|
readonly format?: "email" | "url" | "phone" | "password";
|
|
146
152
|
readonly default?: string;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type LoginFailure, type LoginResponse } from "../../auth-email-password/web/index.js";
|
|
2
2
|
export type MfaVerifyResult = {
|
|
3
3
|
readonly kind: "success";
|
|
4
4
|
readonly data: LoginResponse;
|
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
// byte-identical to /auth/login's ({isSuccess, token, user} | {isSuccess:
|
|
7
7
|
// false, error}) since auth-routes.ts mints via the same
|
|
8
8
|
// mintSessionAndRespond() for both routes.
|
|
9
|
+
// kumiko-lint-ignore cross-feature-import client-only types, the feature's server barrel has no web/ re-export
|
|
10
|
+
import { toLoginResponse, } from "../../auth-email-password/web/index.js";
|
|
9
11
|
export async function verifyMfaChallenge(challengeToken, code) {
|
|
10
12
|
const res = await fetch("/api/auth/mfa/verify", {
|
|
11
13
|
method: "POST",
|
|
@@ -21,11 +23,7 @@ export async function verifyMfaChallenge(challengeToken, code) {
|
|
|
21
23
|
if (body.isSuccess === true && body.token !== undefined && body.user !== undefined) {
|
|
22
24
|
return {
|
|
23
25
|
kind: "success",
|
|
24
|
-
data:
|
|
25
|
-
token: body.token,
|
|
26
|
-
user: body.user,
|
|
27
|
-
...(typeof body.landingPath === "string" && { landingPath: body.landingPath }),
|
|
28
|
-
},
|
|
26
|
+
data: toLoginResponse(body.token, body.user, body.landingPath),
|
|
29
27
|
};
|
|
30
28
|
}
|
|
31
29
|
const err = body.error;
|
|
@@ -109,11 +107,7 @@ export async function confirmMfaSetupPreauth(setupToken, code) {
|
|
|
109
107
|
if (body.isSuccess === true && body.token !== undefined && body.user !== undefined) {
|
|
110
108
|
return {
|
|
111
109
|
kind: "success",
|
|
112
|
-
data:
|
|
113
|
-
token: body.token,
|
|
114
|
-
user: body.user,
|
|
115
|
-
...(typeof body.landingPath === "string" && { landingPath: body.landingPath }),
|
|
116
|
-
},
|
|
110
|
+
data: toLoginResponse(body.token, body.user, body.landingPath),
|
|
117
111
|
};
|
|
118
112
|
}
|
|
119
113
|
const err = body.error;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Generic billing-info query-handler-config factory. Extracted from the
|
|
2
2
|
// near-identical billing-info.query.ts app copies in show-pony and
|
|
3
|
-
// publicstatus
|
|
3
|
+
// publicstatus — the only per-app variables were the
|
|
4
4
|
// TTier union/resolver, the allowed roles, and how Stripe prices are read
|
|
5
5
|
// off the app's extraContext.
|
|
6
6
|
//
|
|
@@ -19,11 +19,11 @@ export declare function findProviderPlugin(ctx: HandlerContext, providerName: st
|
|
|
19
19
|
export declare function resolveProviderPlugin(ctx: HandlerContext, providerName: string): ResolvedProvider;
|
|
20
20
|
/** Picks the catalog's provider — an explicit `catalog.providerName`, or the
|
|
21
21
|
* single registered provider exposing `priceToTier` when there is exactly
|
|
22
|
-
* one. Returns null when
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
22
|
+
* one. Returns null when `providerName` is unset and no registered plugin
|
|
23
|
+
* exposes `priceToTier` — a state `billing-plans` renders as
|
|
24
|
+
* `enabled: false` instead of erroring. An explicit `providerName` that is
|
|
25
|
+
* not registered, or more than one candidate, is a config bug and throws
|
|
26
|
+
* `UnconfiguredError`. */
|
|
27
27
|
export declare function findCatalogProvider(ctx: HandlerContext, catalog: BillingPlanCatalog): ResolvedProvider | null;
|
|
28
28
|
/** `findCatalogProvider` for callers that require a provider to proceed —
|
|
29
29
|
* start-plan-checkout and switch-plan. `FeatureDisabledError` (not
|
|
@@ -37,16 +37,20 @@ export function resolveProviderPlugin(ctx, providerName) {
|
|
|
37
37
|
}
|
|
38
38
|
/** Picks the catalog's provider — an explicit `catalog.providerName`, or the
|
|
39
39
|
* single registered provider exposing `priceToTier` when there is exactly
|
|
40
|
-
* one. Returns null when
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
40
|
+
* one. Returns null when `providerName` is unset and no registered plugin
|
|
41
|
+
* exposes `priceToTier` — a state `billing-plans` renders as
|
|
42
|
+
* `enabled: false` instead of erroring. An explicit `providerName` that is
|
|
43
|
+
* not registered, or more than one candidate, is a config bug and throws
|
|
44
|
+
* `UnconfiguredError`. */
|
|
45
45
|
export function findCatalogProvider(ctx, catalog) {
|
|
46
46
|
if (catalog.providerName) {
|
|
47
47
|
const found = findProviderPlugin(ctx, catalog.providerName);
|
|
48
48
|
if (!found) {
|
|
49
|
-
|
|
49
|
+
throw new UnconfiguredError({
|
|
50
|
+
feature: "billing-foundation",
|
|
51
|
+
key: "catalog.providerName",
|
|
52
|
+
hint: `provider "${catalog.providerName}" not registered`,
|
|
53
|
+
});
|
|
50
54
|
}
|
|
51
55
|
return found;
|
|
52
56
|
}
|
|
@@ -9,6 +9,7 @@ export declare const subscriptionEntity: import("@cosmicdrift/kumiko-types/field
|
|
|
9
9
|
readonly filterable?: boolean;
|
|
10
10
|
readonly encrypted?: boolean;
|
|
11
11
|
readonly sensitive?: boolean;
|
|
12
|
+
readonly writeOnly?: boolean;
|
|
12
13
|
readonly readAsInstruction?: boolean;
|
|
13
14
|
readonly format?: "email" | "url" | "phone" | "password";
|
|
14
15
|
readonly default?: string;
|
|
@@ -29,6 +30,7 @@ export declare const subscriptionEntity: import("@cosmicdrift/kumiko-types/field
|
|
|
29
30
|
readonly filterable?: boolean;
|
|
30
31
|
readonly encrypted?: boolean;
|
|
31
32
|
readonly sensitive?: boolean;
|
|
33
|
+
readonly writeOnly?: boolean;
|
|
32
34
|
readonly readAsInstruction?: boolean;
|
|
33
35
|
readonly format?: "email" | "url" | "phone" | "password";
|
|
34
36
|
readonly default?: string;
|
|
@@ -49,6 +51,7 @@ export declare const subscriptionEntity: import("@cosmicdrift/kumiko-types/field
|
|
|
49
51
|
readonly filterable?: boolean;
|
|
50
52
|
readonly encrypted?: boolean;
|
|
51
53
|
readonly sensitive?: boolean;
|
|
54
|
+
readonly writeOnly?: boolean;
|
|
52
55
|
readonly readAsInstruction?: boolean;
|
|
53
56
|
readonly format?: "email" | "url" | "phone" | "password";
|
|
54
57
|
readonly default?: string;
|
|
@@ -69,6 +72,7 @@ export declare const subscriptionEntity: import("@cosmicdrift/kumiko-types/field
|
|
|
69
72
|
readonly filterable?: boolean;
|
|
70
73
|
readonly encrypted?: boolean;
|
|
71
74
|
readonly sensitive?: boolean;
|
|
75
|
+
readonly writeOnly?: boolean;
|
|
72
76
|
readonly readAsInstruction?: boolean;
|
|
73
77
|
readonly format?: "email" | "url" | "phone" | "password";
|
|
74
78
|
readonly default?: string;
|
|
@@ -89,6 +93,7 @@ export declare const subscriptionEntity: import("@cosmicdrift/kumiko-types/field
|
|
|
89
93
|
readonly filterable?: boolean;
|
|
90
94
|
readonly encrypted?: boolean;
|
|
91
95
|
readonly sensitive?: boolean;
|
|
96
|
+
readonly writeOnly?: boolean;
|
|
92
97
|
readonly readAsInstruction?: boolean;
|
|
93
98
|
readonly format?: "email" | "url" | "phone" | "password";
|
|
94
99
|
readonly default?: string;
|
|
@@ -142,6 +147,7 @@ export declare const paymentEntity: import("@cosmicdrift/kumiko-types/fields").E
|
|
|
142
147
|
readonly filterable?: boolean;
|
|
143
148
|
readonly encrypted?: boolean;
|
|
144
149
|
readonly sensitive?: boolean;
|
|
150
|
+
readonly writeOnly?: boolean;
|
|
145
151
|
readonly readAsInstruction?: boolean;
|
|
146
152
|
readonly format?: "email" | "url" | "phone" | "password";
|
|
147
153
|
readonly default?: string;
|
|
@@ -162,6 +168,7 @@ export declare const paymentEntity: import("@cosmicdrift/kumiko-types/fields").E
|
|
|
162
168
|
readonly filterable?: boolean;
|
|
163
169
|
readonly encrypted?: boolean;
|
|
164
170
|
readonly sensitive?: boolean;
|
|
171
|
+
readonly writeOnly?: boolean;
|
|
165
172
|
readonly readAsInstruction?: boolean;
|
|
166
173
|
readonly format?: "email" | "url" | "phone" | "password";
|
|
167
174
|
readonly default?: string;
|
|
@@ -182,6 +189,7 @@ export declare const paymentEntity: import("@cosmicdrift/kumiko-types/fields").E
|
|
|
182
189
|
readonly filterable?: boolean;
|
|
183
190
|
readonly encrypted?: boolean;
|
|
184
191
|
readonly sensitive?: boolean;
|
|
192
|
+
readonly writeOnly?: boolean;
|
|
185
193
|
readonly readAsInstruction?: boolean;
|
|
186
194
|
readonly format?: "email" | "url" | "phone" | "password";
|
|
187
195
|
readonly default?: string;
|
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
// gets a `catalog`.
|
|
5
5
|
import { ConflictError, UnprocessableError } from "@cosmicdrift/kumiko-framework/errors";
|
|
6
6
|
import * as z from "zod";
|
|
7
|
-
import { assertBillingEnabled, assertRedirectOrigins, isNonEmptyStringArray, joinBaseUrl, resolveCatalogProvider,
|
|
8
|
-
import { isSwitchableSubscriptionStatus
|
|
7
|
+
import { assertBillingEnabled, assertRedirectOrigins, isNonEmptyStringArray, joinBaseUrl, resolveCatalogProvider, } from "../checkout-core.js";
|
|
8
|
+
import { isSwitchableSubscriptionStatus } from "../constants.js";
|
|
9
9
|
import { getSubscriptionForTenant } from "../get-subscription-for-tenant.js";
|
|
10
10
|
import { purchaseRolesOf, resolvePlanPrices } from "../plan-catalog.js";
|
|
11
11
|
export function createSwitchPlanHandler(options, catalog) {
|
|
@@ -25,9 +25,7 @@ export function createSwitchPlanHandler(options, catalog) {
|
|
|
25
25
|
const { name: catalogProviderName, plugin } = resolveCatalogProvider(ctx, catalog, "switch-plan");
|
|
26
26
|
await assertBillingEnabled(ctx, plugin, "switch-plan");
|
|
27
27
|
const sub = await getSubscriptionForTenant(ctx, event.user.tenantId);
|
|
28
|
-
if (!sub ||
|
|
29
|
-
isTerminalSubscriptionStatus(sub.status) ||
|
|
30
|
-
!isSwitchableSubscriptionStatus(sub.status)) {
|
|
28
|
+
if (!sub || !isSwitchableSubscriptionStatus(sub.status)) {
|
|
31
29
|
throw new ConflictError({
|
|
32
30
|
i18nKey: "billing-foundation.errors.noActiveSubscription",
|
|
33
31
|
message: "tenant has no switchable subscription; use billing-foundation:write:start-plan-checkout to start one",
|
|
@@ -47,14 +45,13 @@ export function createSwitchPlanHandler(options, catalog) {
|
|
|
47
45
|
}
|
|
48
46
|
// providerSubscriptionId only exists at the subscription's own provider; after a
|
|
49
47
|
// provider migration the catalog provider can differ, which is not a plan switch.
|
|
50
|
-
const { plugin: switchPlugin } = resolveProviderPlugin(ctx, sub.providerName);
|
|
51
48
|
if (sub.providerName !== catalogProviderName) {
|
|
52
49
|
throw new ConflictError({
|
|
53
50
|
i18nKey: "billing-foundation.errors.providerMismatch",
|
|
54
51
|
message: `tenant's active subscription is on provider "${sub.providerName}", the catalog resolves provider "${catalogProviderName}" — switch-plan cannot switch across providers`,
|
|
55
52
|
});
|
|
56
53
|
}
|
|
57
|
-
if (!
|
|
54
|
+
if (!plugin.createPlanSwitchSession) {
|
|
58
55
|
throw new UnprocessableError("plan_switch_not_supported", {
|
|
59
56
|
i18nKey: "billing-foundation.errors.planSwitchNotSupported",
|
|
60
57
|
message: `subscription-foundation: provider "${sub.providerName}" has no createPlanSwitchSession-method`,
|
|
@@ -74,7 +71,7 @@ export function createSwitchPlanHandler(options, catalog) {
|
|
|
74
71
|
const baseUrl = options.baseUrl ?? "";
|
|
75
72
|
const returnUrl = joinBaseUrl(baseUrl, catalog.returnPath ?? catalog.successPath);
|
|
76
73
|
assertRedirectOrigins([returnUrl], options.baseUrl);
|
|
77
|
-
const result = await
|
|
74
|
+
const result = await plugin.createPlanSwitchSession(ctx, {
|
|
78
75
|
providerSubscriptionId: sub.providerSubscriptionId,
|
|
79
76
|
targetPriceId: target.priceId,
|
|
80
77
|
allowedPriceIds,
|
|
@@ -9,6 +9,7 @@ export declare const subscriptionsProjectionTable: import("@cosmicdrift/kumiko-f
|
|
|
9
9
|
readonly filterable?: boolean;
|
|
10
10
|
readonly encrypted?: boolean;
|
|
11
11
|
readonly sensitive?: boolean;
|
|
12
|
+
readonly writeOnly?: boolean;
|
|
12
13
|
readonly readAsInstruction?: boolean;
|
|
13
14
|
readonly format?: "email" | "url" | "phone" | "password";
|
|
14
15
|
readonly default?: string;
|
|
@@ -29,6 +30,7 @@ export declare const subscriptionsProjectionTable: import("@cosmicdrift/kumiko-f
|
|
|
29
30
|
readonly filterable?: boolean;
|
|
30
31
|
readonly encrypted?: boolean;
|
|
31
32
|
readonly sensitive?: boolean;
|
|
33
|
+
readonly writeOnly?: boolean;
|
|
32
34
|
readonly readAsInstruction?: boolean;
|
|
33
35
|
readonly format?: "email" | "url" | "phone" | "password";
|
|
34
36
|
readonly default?: string;
|
|
@@ -49,6 +51,7 @@ export declare const subscriptionsProjectionTable: import("@cosmicdrift/kumiko-f
|
|
|
49
51
|
readonly filterable?: boolean;
|
|
50
52
|
readonly encrypted?: boolean;
|
|
51
53
|
readonly sensitive?: boolean;
|
|
54
|
+
readonly writeOnly?: boolean;
|
|
52
55
|
readonly readAsInstruction?: boolean;
|
|
53
56
|
readonly format?: "email" | "url" | "phone" | "password";
|
|
54
57
|
readonly default?: string;
|
|
@@ -69,6 +72,7 @@ export declare const subscriptionsProjectionTable: import("@cosmicdrift/kumiko-f
|
|
|
69
72
|
readonly filterable?: boolean;
|
|
70
73
|
readonly encrypted?: boolean;
|
|
71
74
|
readonly sensitive?: boolean;
|
|
75
|
+
readonly writeOnly?: boolean;
|
|
72
76
|
readonly readAsInstruction?: boolean;
|
|
73
77
|
readonly format?: "email" | "url" | "phone" | "password";
|
|
74
78
|
readonly default?: string;
|
|
@@ -89,6 +93,7 @@ export declare const subscriptionsProjectionTable: import("@cosmicdrift/kumiko-f
|
|
|
89
93
|
readonly filterable?: boolean;
|
|
90
94
|
readonly encrypted?: boolean;
|
|
91
95
|
readonly sensitive?: boolean;
|
|
96
|
+
readonly writeOnly?: boolean;
|
|
92
97
|
readonly readAsInstruction?: boolean;
|
|
93
98
|
readonly format?: "email" | "url" | "phone" | "password";
|
|
94
99
|
readonly default?: string;
|
|
@@ -141,6 +146,7 @@ export declare const paymentsProjectionTable: import("@cosmicdrift/kumiko-framew
|
|
|
141
146
|
readonly filterable?: boolean;
|
|
142
147
|
readonly encrypted?: boolean;
|
|
143
148
|
readonly sensitive?: boolean;
|
|
149
|
+
readonly writeOnly?: boolean;
|
|
144
150
|
readonly readAsInstruction?: boolean;
|
|
145
151
|
readonly format?: "email" | "url" | "phone" | "password";
|
|
146
152
|
readonly default?: string;
|
|
@@ -161,6 +167,7 @@ export declare const paymentsProjectionTable: import("@cosmicdrift/kumiko-framew
|
|
|
161
167
|
readonly filterable?: boolean;
|
|
162
168
|
readonly encrypted?: boolean;
|
|
163
169
|
readonly sensitive?: boolean;
|
|
170
|
+
readonly writeOnly?: boolean;
|
|
164
171
|
readonly readAsInstruction?: boolean;
|
|
165
172
|
readonly format?: "email" | "url" | "phone" | "password";
|
|
166
173
|
readonly default?: string;
|
|
@@ -181,6 +188,7 @@ export declare const paymentsProjectionTable: import("@cosmicdrift/kumiko-framew
|
|
|
181
188
|
readonly filterable?: boolean;
|
|
182
189
|
readonly encrypted?: boolean;
|
|
183
190
|
readonly sensitive?: boolean;
|
|
191
|
+
readonly writeOnly?: boolean;
|
|
184
192
|
readonly readAsInstruction?: boolean;
|
|
185
193
|
readonly format?: "email" | "url" | "phone" | "password";
|
|
186
194
|
readonly default?: string;
|
|
@@ -8,19 +8,28 @@ import { capCounterAggregateId, rollingCapAggregateId } from "./aggregate-id.js"
|
|
|
8
8
|
import { CAP_COUNTER_ROLLING_AGGREGATE_TYPE, ROLLING_INCREMENTED_EVENT_QN } from "./constants.js";
|
|
9
9
|
import { capCounterEntity } from "./entity.js";
|
|
10
10
|
const { table, executor } = createEntityExecutor("cap-counter", capCounterEntity);
|
|
11
|
-
|
|
11
|
+
// periodStartIso feeds the aggregate id and Temporal.Instant.from(); a non-instant string would
|
|
12
|
+
// silently fork a counter row on update and throw a RangeError (500) on create.
|
|
13
|
+
const capPeriodSchema = z.object({
|
|
12
14
|
capName: z.string().min(1).max(100),
|
|
13
|
-
periodStartIso: z.
|
|
15
|
+
periodStartIso: z.iso.datetime({ offset: true }),
|
|
16
|
+
});
|
|
17
|
+
const capBookingSchema = capPeriodSchema.extend({
|
|
14
18
|
amount: z.number().int().positive().default(1),
|
|
15
19
|
});
|
|
16
20
|
function isLostCounterRace(result) {
|
|
17
21
|
return !result.isSuccess && result.error.code === "version_conflict";
|
|
18
22
|
}
|
|
19
|
-
// Each attempt writes event + projection atomically, so a conflict means another booker just committed
|
|
23
|
+
// Each attempt writes event + projection atomically, so a conflict means another booker just committed. Contention is bounded by the bookers running at once across ALL replicas on the same DB (not per pool), so the jittered backoff spreads out losers instead of letting them collide again in lockstep; the ceiling only guards against non-convergence.
|
|
20
24
|
const MAX_COUNTER_WRITE_ATTEMPTS = 20;
|
|
25
|
+
const BACKOFF_JITTER_MS_PER_ATTEMPT = 5;
|
|
26
|
+
function sleepMs(ms) {
|
|
27
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
28
|
+
}
|
|
21
29
|
async function retryCounterWriteOnVersionConflict(writeAttempt) {
|
|
22
30
|
let result = await writeAttempt();
|
|
23
31
|
for (let attempt = 1; attempt < MAX_COUNTER_WRITE_ATTEMPTS && isLostCounterRace(result); attempt++) {
|
|
32
|
+
await sleepMs(Math.random() * attempt * BACKOFF_JITTER_MS_PER_ATTEMPT);
|
|
24
33
|
result = await writeAttempt();
|
|
25
34
|
}
|
|
26
35
|
return result;
|
|
@@ -64,11 +73,12 @@ export async function bookCapUsage(ctx, options) {
|
|
|
64
73
|
return retryCounterWriteOnVersionConflict(() => attemptWrite(ctx.db));
|
|
65
74
|
}
|
|
66
75
|
export async function markCapSoftWarned(ctx, options) {
|
|
67
|
-
const
|
|
76
|
+
const parsed = capPeriodSchema.parse(options);
|
|
77
|
+
const aggregateId = capCounterAggregateId(ctx.user.tenantId, parsed.capName, parsed.periodStartIso);
|
|
68
78
|
return retryCounterWriteOnVersionConflict(async () => {
|
|
69
79
|
const existing = await ctx.db.selectMany(table, { id: aggregateId }, { limit: 1 });
|
|
70
80
|
if (existing.length === 0) {
|
|
71
|
-
throw new Error(`cap-counter: cannot mark-soft-warned, no counter found for tenant=${ctx.user.tenantId} cap=${
|
|
81
|
+
throw new Error(`cap-counter: cannot mark-soft-warned, no counter found for tenant=${ctx.user.tenantId} cap=${parsed.capName} period=${parsed.periodStartIso}`);
|
|
72
82
|
}
|
|
73
83
|
const row = existing[0];
|
|
74
84
|
if (!row) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/// <reference types="temporal-polyfill/global" preserve="true" />
|
|
2
2
|
import { createEntityExecutor } from "@cosmicdrift/kumiko-framework/engine";
|
|
3
|
-
import { KumikoError } from "@cosmicdrift/kumiko-framework/errors";
|
|
3
|
+
import { KumikoError, reraiseAsKumikoError } from "@cosmicdrift/kumiko-framework/errors";
|
|
4
4
|
import { markCapSoftWarned, readRollingCapUsage } from "./book-cap-usage.js";
|
|
5
5
|
import { capCounterEntity } from "./entity.js";
|
|
6
6
|
// Temporal globally provided by the framework's polyfill init
|
|
@@ -48,6 +48,10 @@ export async function enforceCap(ctx, options) {
|
|
|
48
48
|
if (!ctx.db) {
|
|
49
49
|
throw new Error("cap-counter.enforceCap: ctx.db missing — run inside a handler context");
|
|
50
50
|
}
|
|
51
|
+
const amount = options.amount ?? 1;
|
|
52
|
+
if (!Number.isInteger(amount) || amount < 1) {
|
|
53
|
+
throw new Error(`cap-counter.enforceCap: amount must be a positive integer, got ${options.amount}`);
|
|
54
|
+
}
|
|
51
55
|
const tolerance = CAP_TOLERANCES[options.profile];
|
|
52
56
|
const softThreshold = options.limit * tolerance.soft;
|
|
53
57
|
const hardThreshold = options.limit * tolerance.hard;
|
|
@@ -55,7 +59,7 @@ export async function enforceCap(ctx, options) {
|
|
|
55
59
|
const row = rows[0];
|
|
56
60
|
const storedValue = row ? row["value"] : 0; // @cast-boundary db-row
|
|
57
61
|
// The last of `amount` units sees this value before its own increment.
|
|
58
|
-
const value = storedValue +
|
|
62
|
+
const value = storedValue + amount - 1;
|
|
59
63
|
if (value >= hardThreshold) {
|
|
60
64
|
throw new CapExceededError(options.capName, options.limit, value, tolerance);
|
|
61
65
|
}
|
|
@@ -193,10 +197,12 @@ export async function enforceCapAndMaybeNotify(ctx, options) {
|
|
|
193
197
|
tenantId: ctx.user.tenantId,
|
|
194
198
|
});
|
|
195
199
|
// Flip the soft-warned flag so the same period doesn't re-notify.
|
|
196
|
-
await markCapSoftWarned(ctx, {
|
|
200
|
+
const marked = await markCapSoftWarned(ctx, {
|
|
197
201
|
capName: options.capName,
|
|
198
202
|
periodStartIso: options.periodStartIso,
|
|
199
203
|
});
|
|
204
|
+
if (!marked.isSuccess)
|
|
205
|
+
throw reraiseAsKumikoError(marked.error);
|
|
200
206
|
}
|
|
201
207
|
return result;
|
|
202
208
|
}
|
|
@@ -9,6 +9,7 @@ export declare const capCounterEntity: import("@cosmicdrift/kumiko-types/fields"
|
|
|
9
9
|
readonly filterable?: boolean;
|
|
10
10
|
readonly encrypted?: boolean;
|
|
11
11
|
readonly sensitive?: boolean;
|
|
12
|
+
readonly writeOnly?: boolean;
|
|
12
13
|
readonly readAsInstruction?: boolean;
|
|
13
14
|
readonly format?: "email" | "url" | "phone" | "password";
|
|
14
15
|
readonly default?: string;
|
|
@@ -91,7 +91,9 @@ export const capCounterFeature = defineFeature(CAP_COUNTER_FEATURE, (r) => {
|
|
|
91
91
|
entity: "cap-counter",
|
|
92
92
|
columns: ["tenantId", "capName", "value", "periodStart", "lastSoftWarnedAt"],
|
|
93
93
|
defaultSort: { field: "capName", dir: "asc" },
|
|
94
|
-
|
|
94
|
+
// Search resolves against the caller's own tenant stream, so on this
|
|
95
|
+
// cross-tenant operator list it would only ever hit the SystemAdmin's tenant.
|
|
96
|
+
searchable: false,
|
|
95
97
|
access: { roles: ["SystemAdmin"] },
|
|
96
98
|
});
|
|
97
99
|
r.nav({
|
|
@@ -105,7 +107,6 @@ export const capCounterFeature = defineFeature(CAP_COUNTER_FEATURE, (r) => {
|
|
|
105
107
|
keys: {
|
|
106
108
|
...CAP_COUNTER_I18N,
|
|
107
109
|
"cap-counter:nav.cap-list": { de: "Limits", en: "Caps" },
|
|
108
|
-
"cap-counter:entity:cap-counter:field:tenantId": { de: "Mandant", en: "Tenant" },
|
|
109
110
|
},
|
|
110
111
|
});
|
|
111
112
|
});
|
package/dist/cap-counter/i18n.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
export const CAP_COUNTER_I18N = {
|
|
2
|
-
"screen:cap-list.title": { en: "Usage caps" },
|
|
3
|
-
"cap-counter:entity:cap-counter:field:tenantId": { en: "Tenant" },
|
|
4
|
-
"cap-counter:entity:cap-counter:field:capName": { en: "Cap" },
|
|
5
|
-
"cap-counter:entity:cap-counter:field:value": { en: "Value" },
|
|
6
|
-
"cap-counter:entity:cap-counter:field:periodStart": { en: "Period start" },
|
|
2
|
+
"screen:cap-list.title": { de: "Nutzungslimits", en: "Usage caps" },
|
|
3
|
+
"cap-counter:entity:cap-counter:field:tenantId": { de: "Mandant", en: "Tenant" },
|
|
4
|
+
"cap-counter:entity:cap-counter:field:capName": { de: "Limit", en: "Cap" },
|
|
5
|
+
"cap-counter:entity:cap-counter:field:value": { de: "Wert", en: "Value" },
|
|
6
|
+
"cap-counter:entity:cap-counter:field:periodStart": { de: "Periodenbeginn", en: "Period start" },
|
|
7
7
|
"cap-counter:entity:cap-counter:field:lastSoftWarnedAt": {
|
|
8
|
+
de: "Letzte Soft-Warnung",
|
|
8
9
|
en: "Last soft warning",
|
|
9
10
|
},
|
|
10
11
|
};
|
|
@@ -51,30 +51,23 @@ export function createCapsUsageQuery(caps) {
|
|
|
51
51
|
const rows = await Promise.all(caps.map(async (cap) => {
|
|
52
52
|
const used = await cap.usage(db, targetTenantId);
|
|
53
53
|
const limit = await cap.limit(tier, { config: ctx.config });
|
|
54
|
+
const base = {
|
|
55
|
+
id: cap.id,
|
|
56
|
+
label: cap.label,
|
|
57
|
+
limit,
|
|
58
|
+
...(cap.icon !== undefined && { icon: cap.icon }),
|
|
59
|
+
...(cap.accentColor !== undefined && { accentColor: cap.accentColor }),
|
|
60
|
+
};
|
|
54
61
|
if (used === null) {
|
|
55
|
-
return {
|
|
56
|
-
id: cap.id,
|
|
57
|
-
label: cap.label,
|
|
58
|
-
used: null,
|
|
59
|
-
limit,
|
|
60
|
-
fraction: 0,
|
|
61
|
-
tone: "default",
|
|
62
|
-
percent: null,
|
|
63
|
-
...(cap.icon !== undefined && { icon: cap.icon }),
|
|
64
|
-
...(cap.accentColor !== undefined && { accentColor: cap.accentColor }),
|
|
65
|
-
};
|
|
62
|
+
return { ...base, used: null, fraction: 0, tone: "default", percent: null };
|
|
66
63
|
}
|
|
67
64
|
const fraction = computeFraction(used, limit);
|
|
68
65
|
return {
|
|
69
|
-
|
|
70
|
-
label: cap.label,
|
|
66
|
+
...base,
|
|
71
67
|
used,
|
|
72
|
-
limit,
|
|
73
68
|
fraction,
|
|
74
69
|
tone: computeTone(fraction),
|
|
75
70
|
percent: limit === null ? null : Math.round(computeUnclampedFraction(used, limit) * 100),
|
|
76
|
-
...(cap.icon !== undefined && { icon: cap.icon }),
|
|
77
|
-
...(cap.accentColor !== undefined && { accentColor: cap.accentColor }),
|
|
78
71
|
};
|
|
79
72
|
}));
|
|
80
73
|
return { rows };
|
|
@@ -52,6 +52,26 @@ function assertSupportedTierFilterOp(op) {
|
|
|
52
52
|
});
|
|
53
53
|
}
|
|
54
54
|
}
|
|
55
|
+
function extractTierFilter(filters) {
|
|
56
|
+
const tierFilters = filters.filter((f) => f.field === "tier");
|
|
57
|
+
if (tierFilters.length !== filters.length || tierFilters.length > 1) {
|
|
58
|
+
const unsupportedField = filters.find((f) => f.field !== "tier")?.field ?? "tier";
|
|
59
|
+
throw new ValidationError({
|
|
60
|
+
fields: [
|
|
61
|
+
{
|
|
62
|
+
path: "filters",
|
|
63
|
+
code: "unsupported_field",
|
|
64
|
+
i18nKey: "cap-overview.errors.filterFieldUnsupported",
|
|
65
|
+
params: { field: unsupportedField },
|
|
66
|
+
},
|
|
67
|
+
],
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
const tierFilter = tierFilters[0];
|
|
71
|
+
if (tierFilter !== undefined)
|
|
72
|
+
assertSupportedTierFilterOp(tierFilter.op);
|
|
73
|
+
return tierFilter;
|
|
74
|
+
}
|
|
55
75
|
function matchesTierFilter(tier, filter) {
|
|
56
76
|
if (filter === undefined)
|
|
57
77
|
return true;
|
|
@@ -104,10 +124,7 @@ export function createTenantCapsListQuery(caps, listCaps) {
|
|
|
104
124
|
// query builder (only `like`, SQL-LIKE case-sensitive) — filtered here
|
|
105
125
|
// in JS, same as tenant/team-list.query.ts's search.
|
|
106
126
|
const search = query.payload.search?.trim().toLowerCase();
|
|
107
|
-
const
|
|
108
|
-
const tierFilter = filters.find((f) => f.field === "tier");
|
|
109
|
-
if (tierFilter !== undefined)
|
|
110
|
-
assertSupportedTierFilterOp(tierFilter.op);
|
|
127
|
+
const tierFilter = extractTierFilter(query.payload.filters ?? []);
|
|
111
128
|
const merged = tenants
|
|
112
129
|
.filter((tenant) => search === undefined || search === "" || tenant.name.toLowerCase().includes(search))
|
|
113
130
|
.map((tenant) => {
|
|
@@ -144,24 +161,33 @@ export function createTenantCapsListQuery(caps, listCaps) {
|
|
|
144
161
|
// that the fix is a real combined read-projection, not a cursor
|
|
145
162
|
// bolted onto this merge.
|
|
146
163
|
const offset = query.payload.cursor ? Number(decodeCursor(query.payload.cursor)) : 0;
|
|
164
|
+
if (!Number.isInteger(offset) || offset < 0) {
|
|
165
|
+
throw new ValidationError({
|
|
166
|
+
fields: [
|
|
167
|
+
{
|
|
168
|
+
path: "cursor",
|
|
169
|
+
code: "invalid_cursor",
|
|
170
|
+
i18nKey: "cap-overview.errors.invalidCursor",
|
|
171
|
+
},
|
|
172
|
+
],
|
|
173
|
+
});
|
|
174
|
+
}
|
|
147
175
|
const limit = query.payload.limit;
|
|
148
176
|
const page = sorted.slice(offset, offset + limit);
|
|
149
|
-
//
|
|
150
|
-
//
|
|
177
|
+
// Usage is computed only for this page's tenant ids, never for the
|
|
178
|
+
// full tenant set; caps without usageBatch still cost one query per
|
|
179
|
+
// tenant of the page, so those run in parallel.
|
|
151
180
|
const pageTenantIds = page.map((row) => row.tenantId);
|
|
152
181
|
const usageByCap = new Map();
|
|
153
|
-
|
|
182
|
+
await Promise.all(listedCaps.map(async (cap) => {
|
|
154
183
|
if (cap.usageBatch) {
|
|
155
184
|
usageByCap.set(cap.id, await cap.usageBatch(db, pageTenantIds));
|
|
185
|
+
// skip: batch result already stored, no per-tenant fallback needed
|
|
186
|
+
return;
|
|
156
187
|
}
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
perTenant.set(tenantId, await cap.usage(db, tenantId));
|
|
161
|
-
}
|
|
162
|
-
usageByCap.set(cap.id, perTenant);
|
|
163
|
-
}
|
|
164
|
-
}
|
|
188
|
+
const perTenantEntries = await Promise.all(pageTenantIds.map(async (tenantId) => [tenantId, await cap.usage(db, tenantId)]));
|
|
189
|
+
usageByCap.set(cap.id, new Map(perTenantEntries));
|
|
190
|
+
}));
|
|
165
191
|
const limitByCapAndTier = await resolveLimitsByCapAndTier(listedCaps, new Set(page.map((row) => row.tier)), { config: ctx.config });
|
|
166
192
|
const rows = page.map((row) => {
|
|
167
193
|
const capFields = {};
|