@cosmicdrift/kumiko-bundled-features 0.387.0 → 0.388.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent-tools/tool-dispatch.d.ts +7 -7
- package/dist/agent-tools/tool-dispatch.js +9 -9
- package/dist/audit/handlers/audit-events.js +1 -1
- package/dist/auth-mfa/feature-exports.d.ts +2 -2
- package/dist/auth-mfa/feature.js +8 -0
- package/dist/auth-mfa/web/i18n.js +5 -1
- package/dist/auth-mfa/web/mfa-code-or-recovery-field.d.ts +11 -0
- package/dist/auth-mfa/web/mfa-code-or-recovery-field.js +12 -0
- package/dist/auth-mfa/web/mfa-disable-dialog.js +14 -3
- package/dist/auth-mfa/web/mfa-regenerate-recovery-dialog.js +14 -3
- package/dist/auth-mfa/web/mfa-setup-preauth-screen.js +10 -5
- package/dist/auth-mfa/web/mfa-verify-screen.js +26 -5
- package/dist/billing-foundation/checkout-core.d.ts +9 -14
- package/dist/billing-foundation/checkout-core.js +24 -39
- package/dist/billing-foundation/consumer-protection/confirmation-mail.js +8 -2
- package/dist/billing-foundation/consumer-protection/consent-gate.d.ts +1 -0
- package/dist/billing-foundation/consumer-protection/consent-gate.js +1 -0
- package/dist/billing-foundation/consumer-protection/issue-confirmation.js +9 -3
- package/dist/billing-foundation/consumer-protection/mail-notifications.js +3 -1
- package/dist/billing-foundation/consumer-protection/termination-receipt-limit.d.ts +4 -2
- package/dist/billing-foundation/consumer-protection/termination-receipt-limit.js +6 -5
- package/dist/billing-foundation/consumer-protection/termination-record.d.ts +5 -1
- package/dist/billing-foundation/consumer-protection/termination-record.js +27 -17
- package/dist/billing-foundation/consumer-protection/termination-request.js +12 -7
- package/dist/billing-foundation/consumer-protection/termination-revoke.js +12 -7
- package/dist/billing-foundation/consumer-protection/termination-texts.d.ts +1 -1
- package/dist/billing-foundation/consumer-protection/termination-texts.js +3 -0
- package/dist/billing-foundation/consumer-protection/withdrawal-period.d.ts +5 -2
- package/dist/billing-foundation/consumer-protection/withdrawal-period.js +8 -9
- package/dist/billing-foundation/events.d.ts +1 -0
- package/dist/billing-foundation/events.js +1 -0
- package/dist/billing-foundation/feature.js +3 -1
- package/dist/billing-foundation/handlers/billing-plans.query.js +35 -5
- package/dist/billing-foundation/handlers/create-checkout-session.write.js +1 -0
- package/dist/billing-foundation/handlers/plan-switch-preview.query.d.ts +13 -0
- package/dist/billing-foundation/handlers/plan-switch-preview.query.js +48 -0
- package/dist/billing-foundation/handlers/process-event.write.d.ts +6 -0
- package/dist/billing-foundation/handlers/process-event.write.js +8 -2
- package/dist/billing-foundation/handlers/start-plan-checkout.write.js +32 -5
- package/dist/billing-foundation/handlers/switch-plan.write.d.ts +2 -2
- package/dist/billing-foundation/handlers/switch-plan.write.js +56 -64
- package/dist/billing-foundation/handlers/sync-subscription.write.js +12 -1
- package/dist/billing-foundation/i18n.js +22 -0
- package/dist/billing-foundation/index.d.ts +1 -1
- package/dist/billing-foundation/plan-catalog.d.ts +21 -5
- package/dist/billing-foundation/plan-catalog.js +25 -16
- package/dist/billing-foundation/plan-switch.d.ts +12 -0
- package/dist/billing-foundation/plan-switch.js +60 -0
- package/dist/billing-foundation/types.d.ts +57 -1
- package/dist/billing-foundation/types.js +9 -0
- package/dist/billing-foundation/validate-options.js +17 -0
- package/dist/billing-foundation/web/billing-plans-panel.js +78 -32
- package/dist/billing-foundation/web/checkout-consent-dialog.d.ts +17 -3
- package/dist/billing-foundation/web/checkout-consent-dialog.js +14 -10
- package/dist/billing-foundation/web/constants.d.ts +4 -0
- package/dist/billing-foundation/web/constants.js +4 -0
- package/dist/billing-foundation/web/plan-price.d.ts +15 -0
- package/dist/billing-foundation/web/plan-price.js +32 -0
- package/dist/billing-foundation/web/plan-switch-confirm-dialog.d.ts +15 -0
- package/dist/billing-foundation/web/plan-switch-confirm-dialog.js +66 -0
- package/dist/cap-counter/cap-reservation.js +5 -1
- package/dist/cap-counter/enforce-cap.js +2 -1
- package/dist/cap-counter/stock-cap-guard.js +7 -1
- package/dist/channel-email/feature.js +1 -1
- package/dist/config/handlers/cascade.query.d.ts +5 -0
- package/dist/config/handlers/cascade.query.js +4 -3
- package/dist/config/handlers/values.query.d.ts +5 -0
- package/dist/config/handlers/values.query.js +4 -3
- package/dist/config/write-helpers.d.ts +1 -0
- package/dist/config/write-helpers.js +18 -9
- package/dist/crypto-shredding/handlers/forget-subject.write.js +13 -10
- package/dist/delivery/channel-context.js +6 -3
- package/dist/delivery/db/queries/preferences.js +2 -2
- package/dist/delivery/delivery-service.js +22 -7
- package/dist/delivery/handlers/preferences.query.d.ts +1 -1
- package/dist/delivery/handlers/preferences.query.js +20 -1
- package/dist/delivery/handlers/unsubscribe-page-chrome.query.js +1 -1
- package/dist/delivery/unsubscribe-link.d.ts +3 -1
- package/dist/delivery/unsubscribe-page-strings.d.ts +1 -0
- package/dist/delivery/unsubscribe-page-strings.js +1 -0
- package/dist/delivery/unsubscribe-page.d.ts +2 -0
- package/dist/delivery/unsubscribe-page.js +5 -0
- package/dist/delivery/unsubscribe.js +25 -7
- package/dist/file-foundation/index.d.ts +1 -1
- package/dist/file-foundation/index.js +1 -1
- package/dist/file-foundation/read-file-ref-content.d.ts +3 -0
- package/dist/file-foundation/read-file-ref-content.js +6 -1
- package/dist/file-provider-s3/env-named-feature.d.ts +0 -2
- package/dist/form-draft/db/queries/cleanup.js +2 -2
- package/dist/form-draft/decrypt-draft.js +5 -1
- package/dist/host-routing/dispatch.js +11 -20
- package/dist/inbound-mail-foundation/handlers/update-account.write.d.ts +2 -2
- package/dist/inbound-mail-foundation/handlers/update-account.write.js +12 -4
- package/dist/inbound-mail-foundation/i18n.js +0 -1
- package/dist/inbound-mail-foundation/screens.js +0 -3
- package/dist/inbound-mail-foundation/types.d.ts +4 -3
- package/dist/inbound-mail-foundation/watch-supervisor.js +28 -11
- package/dist/jobs/feature.d.ts +1 -0
- package/dist/jobs/feature.js +8 -4
- package/dist/jobs/handlers/tenant-failures.query.d.ts +1 -1
- package/dist/jobs/handlers/tenant-failures.query.js +45 -41
- package/dist/jobs/handlers/tenant-runs.query.d.ts +1 -1
- package/dist/jobs/handlers/tenant-runs.query.js +54 -50
- package/dist/jobs/job-run-logger.js +5 -4
- package/dist/mail-foundation/feature.d.ts +3 -3
- package/dist/mail-foundation/feature.js +3 -0
- package/dist/personal-access-tokens/feature.d.ts +1 -1
- package/dist/personal-access-tokens/feature.js +4 -0
- package/dist/renderer-pdf/render-table-pdf.d.ts +1 -1
- package/dist/renderer-pdf/render-table-pdf.js +22 -10
- package/dist/sessions/feature.d.ts +2 -2
- package/dist/sessions/feature.js +4 -0
- package/dist/shared/decrypt-stored-pii.d.ts +1 -0
- package/dist/shared/decrypt-stored-pii.js +6 -1
- package/dist/shared/index.d.ts +1 -1
- package/dist/shared/index.js +1 -1
- package/dist/shared/mail-template-body.d.ts +6 -2
- package/dist/shared/mail-template-body.js +7 -4
- package/dist/step-dispatcher/feature.js +1 -0
- package/dist/subscription-dodo/client.d.ts +17 -0
- package/dist/subscription-dodo/client.js +31 -0
- package/dist/subscription-dodo/constants.d.ts +12 -0
- package/dist/subscription-dodo/constants.js +19 -0
- package/dist/subscription-dodo/feature.d.ts +37 -0
- package/dist/subscription-dodo/feature.js +144 -0
- package/dist/subscription-dodo/index.d.ts +2 -0
- package/dist/subscription-dodo/index.js +4 -0
- package/dist/subscription-dodo/plugin-methods.d.ts +23 -0
- package/dist/subscription-dodo/plugin-methods.js +260 -0
- package/dist/subscription-dodo/runtime.d.ts +27 -0
- package/dist/subscription-dodo/runtime.js +89 -0
- package/dist/subscription-dodo/verify-webhook.d.ts +37 -0
- package/dist/subscription-dodo/verify-webhook.js +189 -0
- package/dist/subscription-stripe/feature.js +1 -0
- package/dist/subscription-stripe/runtime.js +7 -6
- package/dist/template-resolver/api.d.ts +3 -2
- package/dist/template-resolver/api.js +24 -2
- package/dist/template-resolver/compile-document-body.js +1 -0
- package/dist/template-resolver/feature.js +2 -2
- package/dist/template-resolver/handlers/bundle-import.js +3 -3
- package/dist/template-resolver/handlers/copy-locale.write.d.ts +5 -1
- package/dist/template-resolver/handlers/copy-locale.write.js +22 -7
- package/dist/template-resolver/handlers/diff-to-system.query.js +3 -2
- package/dist/template-resolver/handlers/document-base.js +3 -2
- package/dist/template-resolver/handlers/find-by-id.query.js +3 -2
- package/dist/template-resolver/handlers/list.query.js +5 -6
- package/dist/template-resolver/handlers/overview.query.d.ts +3 -0
- package/dist/template-resolver/handlers/overview.query.js +6 -3
- package/dist/template-resolver/handlers/tenant-override-counts.query.js +1 -3
- package/dist/template-resolver/handlers/tenant-override-selection.d.ts +2 -1
- package/dist/template-resolver/handlers/tenant-overrides.query.js +28 -5
- package/dist/template-resolver/handlers/usage.query.d.ts +2 -1
- package/dist/template-resolver/handlers/usage.query.js +26 -16
- package/dist/template-resolver/handlers/validate.query.js +7 -2
- package/dist/template-resolver/seeding.js +26 -10
- package/dist/template-resolver/store-template-resource.d.ts +1 -0
- package/dist/template-resolver/store-template-resource.js +19 -4
- package/dist/template-resolver/template-delivery-policy.d.ts +0 -5
- package/dist/template-resolver/template-delivery-policy.js +0 -6
- package/dist/template-resolver/template-meta.js +7 -4
- package/dist/template-resolver/template-preview.js +4 -3
- package/dist/template-resolver/web/constants.d.ts +2 -2
- package/dist/template-resolver/web/constants.js +3 -3
- package/dist/template-resolver/web/index.d.ts +1 -0
- package/dist/template-resolver/web/index.js +1 -0
- package/dist/template-resolver/web/mail-layout-constants.d.ts +5 -0
- package/dist/template-resolver/web/mail-layout-constants.js +7 -0
- package/dist/tenant/handlers/member-directory.query.d.ts +1 -0
- package/dist/tenant/handlers/member-directory.query.js +10 -3
- package/dist/tenant/handlers/tenant-directory.query.d.ts +1 -0
- package/dist/tenant/handlers/tenant-directory.query.js +8 -4
- package/dist/tenant-lifecycle/constants.d.ts +1 -0
- package/dist/tenant-lifecycle/constants.js +4 -0
- package/dist/tenant-lifecycle/run-tenant-destroy.js +2 -2
- package/dist/tier-engine/handlers/set-tenant-tier.write.d.ts +2 -3
- package/dist/tier-engine/handlers/set-tenant-tier.write.js +4 -1
- package/dist/tier-engine/web/constants.js +2 -2
- package/dist/user-data-rights/run-export-jobs.js +3 -4
- package/dist/user-data-rights/run-forget-cleanup.js +2 -1
- package/dist/waitlist/handlers/export-purge.write.js +2 -1
- package/dist/waitlist/handlers/submit.write.js +5 -5
- package/dist/waitlist/import.d.ts +1 -0
- package/dist/waitlist/import.js +4 -0
- package/dist/waitlist/legacy-migrate.js +17 -6
- package/dist/waitlist/link-on-signup.js +18 -6
- package/dist/waitlist/options.d.ts +3 -0
- package/package.json +19 -9
- package/src/agent-tools/changes.json +6 -0
- package/src/audit/changes.json +12 -0
- package/src/auth-mfa/changes.json +6 -0
- package/src/billing-foundation/changes.json +55 -0
- package/src/cap-counter/changes.json +18 -0
- package/src/channel-email/changes.json +6 -0
- package/src/config/changes.json +12 -0
- package/src/crypto-shredding/changes.json +6 -0
- package/src/delivery/changes.json +30 -0
- package/src/form-draft/changes.json +6 -0
- package/src/host-routing/changes.json +6 -0
- package/src/inbound-mail-foundation/changes.json +18 -0
- package/src/jobs/changes.json +13 -0
- package/src/mail-foundation/changes.json +6 -0
- package/src/renderer-pdf/changes.json +6 -0
- package/src/step-dispatcher/changes.json +6 -0
- package/src/subscription-dodo/changes.json +14 -0
- package/src/subscription-stripe/changes.json +12 -0
- package/src/template-resolver/changes.json +48 -0
- package/src/tenant/changes.json +6 -0
- package/src/tenant-lifecycle/changes.json +6 -0
- package/src/tier-engine/changes.json +6 -0
- package/src/waitlist/changes.json +18 -0
- package/dist/billing-foundation/consumer-protection/stored-user-email.d.ts +0 -1
- package/dist/billing-foundation/consumer-protection/stored-user-email.js +0 -8
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { type PaymentEvent, type SubscriptionEvent, type SubscriptionStatus } from "@cosmicdrift/kumiko-bundled-features/billing-foundation";
|
|
2
|
+
import type { SecretsContext } from "@cosmicdrift/kumiko-framework/secrets";
|
|
3
|
+
import * as z from "zod";
|
|
4
|
+
import type { DodoWebhookRuntime } from "./runtime.ts";
|
|
5
|
+
export declare const WEBHOOK_TOLERANCE_SECONDS = 300;
|
|
6
|
+
export type StandardWebhookHeaders = Readonly<Record<string, string>>;
|
|
7
|
+
/** Throws unless the Standard Webhooks signature over `rawBody` is valid and
|
|
8
|
+
* the timestamp is within the tolerance in both directions. */
|
|
9
|
+
export declare function verifyStandardWebhook(rawBody: string, headers: StandardWebhookHeaders, secret: string, nowSeconds: number): void;
|
|
10
|
+
export declare const dodoSubscriptionSchema: z.ZodObject<{
|
|
11
|
+
subscription_id: z.ZodString;
|
|
12
|
+
product_id: z.ZodString;
|
|
13
|
+
status: z.ZodString;
|
|
14
|
+
customer: z.ZodObject<{
|
|
15
|
+
customer_id: z.ZodString;
|
|
16
|
+
}, z.core.$strip>;
|
|
17
|
+
metadata: z.ZodOptional<z.ZodNullable<z.ZodRecord<z.ZodString, z.ZodUnknown>>>;
|
|
18
|
+
next_billing_date: z.ZodISODateTime;
|
|
19
|
+
cancel_at_next_billing_date: z.ZodOptional<z.ZodNullable<z.ZodBoolean>>;
|
|
20
|
+
}, z.core.$strip>;
|
|
21
|
+
export type DodoSubscription = z.infer<typeof dodoSubscriptionSchema>;
|
|
22
|
+
export type MappedDodoSubscriptionState = {
|
|
23
|
+
readonly status: SubscriptionStatus;
|
|
24
|
+
readonly tier: string;
|
|
25
|
+
readonly currentPeriodEnd: string;
|
|
26
|
+
readonly cancelAt: string | null;
|
|
27
|
+
};
|
|
28
|
+
/** Shared by the webhook and retrieveSubscription so both map Dodo state
|
|
29
|
+
* identically. Null when the status is unknown or the product is not in
|
|
30
|
+
* `priceToTier` (SubscriptionEvent.tier is required). */
|
|
31
|
+
export declare function mapDodoSubscriptionState(sub: DodoSubscription, priceToTier: Readonly<Record<string, string>>): MappedDodoSubscriptionState | null;
|
|
32
|
+
export type DodoWebhookOptions = {
|
|
33
|
+
readonly priceToTier: Readonly<Record<string, string>>;
|
|
34
|
+
/** Test seam: Unix seconds. Defaults to the wall clock. */
|
|
35
|
+
readonly nowSeconds?: () => number;
|
|
36
|
+
};
|
|
37
|
+
export declare function verifyAndParseDodoWebhook(runtime: DodoWebhookRuntime, options: DodoWebhookOptions): (rawBody: string, headers: Record<string, string>, systemSecrets?: SecretsContext) => Promise<SubscriptionEvent | PaymentEvent | null>;
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
// Dodo Payments webhook: Standard Webhooks signature verification plus mapping
|
|
2
|
+
// of Dodo's event envelope onto the foundation's normalized events.
|
|
3
|
+
//
|
|
4
|
+
// Verification always runs against the raw body string; re-serialized JSON
|
|
5
|
+
// would change whitespace and break the HMAC. The signature check happens
|
|
6
|
+
// before any parsing, so no path bypasses it.
|
|
7
|
+
import { createHmac, timingSafeEqual } from "node:crypto";
|
|
8
|
+
import { BillingEventKinds, parseProviderConsentId, SubscriptionEventTypes, SubscriptionStatuses, } from "@cosmicdrift/kumiko-bundled-features/billing-foundation";
|
|
9
|
+
import { parseJsonOrThrow } from "@cosmicdrift/kumiko-framework/utils";
|
|
10
|
+
import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
|
|
11
|
+
import * as z from "zod";
|
|
12
|
+
import { DODO_PROVIDER_NAME } from "./constants.js";
|
|
13
|
+
export const WEBHOOK_TOLERANCE_SECONDS = 300;
|
|
14
|
+
const SECRET_PREFIX = "whsec_";
|
|
15
|
+
const SIGNATURE_VERSION = "v1";
|
|
16
|
+
const TIMESTAMP_PATTERN = /^\d+$/;
|
|
17
|
+
function signatureFor(secretKey, signedContent) {
|
|
18
|
+
return createHmac("sha256", secretKey).update(signedContent).digest();
|
|
19
|
+
}
|
|
20
|
+
function matchesAnyV1Signature(header, expected) {
|
|
21
|
+
return header.split(" ").some((entry) => {
|
|
22
|
+
const [version, encoded] = entry.split(",");
|
|
23
|
+
if (version !== SIGNATURE_VERSION || !encoded)
|
|
24
|
+
return false;
|
|
25
|
+
const candidate = Buffer.from(encoded, "base64");
|
|
26
|
+
return candidate.length === expected.length && timingSafeEqual(candidate, expected);
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
/** Throws unless the Standard Webhooks signature over `rawBody` is valid and
|
|
30
|
+
* the timestamp is within the tolerance in both directions. */
|
|
31
|
+
export function verifyStandardWebhook(rawBody, headers, secret, nowSeconds) {
|
|
32
|
+
const id = headers["webhook-id"];
|
|
33
|
+
const timestamp = headers["webhook-timestamp"];
|
|
34
|
+
const signatureHeader = headers["webhook-signature"];
|
|
35
|
+
if (!id || !timestamp || !signatureHeader) {
|
|
36
|
+
throw new Error("subscription-dodo: webhook-id, webhook-timestamp or webhook-signature missing");
|
|
37
|
+
}
|
|
38
|
+
if (!TIMESTAMP_PATTERN.test(timestamp)) {
|
|
39
|
+
throw new Error("subscription-dodo: webhook-timestamp is not a Unix timestamp in seconds");
|
|
40
|
+
}
|
|
41
|
+
if (Math.abs(nowSeconds - Number(timestamp)) > WEBHOOK_TOLERANCE_SECONDS) {
|
|
42
|
+
throw new Error("subscription-dodo: webhook timestamp outside the tolerance window");
|
|
43
|
+
}
|
|
44
|
+
const secretKey = Buffer.from(secret.startsWith(SECRET_PREFIX) ? secret.slice(SECRET_PREFIX.length) : secret, "base64");
|
|
45
|
+
const expected = signatureFor(secretKey, `${id}.${timestamp}.${rawBody}`);
|
|
46
|
+
if (!matchesAnyV1Signature(signatureHeader, expected)) {
|
|
47
|
+
throw new Error("subscription-dodo: webhook signature verify failed");
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
// =============================================================================
|
|
51
|
+
// Event mapping
|
|
52
|
+
// =============================================================================
|
|
53
|
+
const DODO_SUBSCRIPTION_EVENT_TYPES = {
|
|
54
|
+
"subscription.active": SubscriptionEventTypes.created,
|
|
55
|
+
"subscription.updated": SubscriptionEventTypes.updated,
|
|
56
|
+
"subscription.plan_changed": SubscriptionEventTypes.updated,
|
|
57
|
+
"subscription.paused": SubscriptionEventTypes.updated,
|
|
58
|
+
"subscription.unpaused": SubscriptionEventTypes.updated,
|
|
59
|
+
"subscription.update_payment_method": SubscriptionEventTypes.updated,
|
|
60
|
+
"subscription.renewed": SubscriptionEventTypes.invoicePaid,
|
|
61
|
+
"subscription.past_due": SubscriptionEventTypes.invoicePaymentFailed,
|
|
62
|
+
"subscription.on_hold": SubscriptionEventTypes.invoicePaymentFailed,
|
|
63
|
+
"subscription.cancelled": SubscriptionEventTypes.canceled,
|
|
64
|
+
"subscription.expired": SubscriptionEventTypes.canceled,
|
|
65
|
+
"subscription.failed": SubscriptionEventTypes.canceled,
|
|
66
|
+
};
|
|
67
|
+
const DODO_SUBSCRIPTION_STATUSES = {
|
|
68
|
+
pending: SubscriptionStatuses.incomplete,
|
|
69
|
+
active: SubscriptionStatuses.active,
|
|
70
|
+
on_hold: SubscriptionStatuses.pastDue,
|
|
71
|
+
past_due: SubscriptionStatuses.pastDue,
|
|
72
|
+
paused: SubscriptionStatuses.pastDue,
|
|
73
|
+
cancelled: SubscriptionStatuses.canceled,
|
|
74
|
+
failed: SubscriptionStatuses.canceled,
|
|
75
|
+
expired: SubscriptionStatuses.canceled,
|
|
76
|
+
};
|
|
77
|
+
const PAYMENT_SUCCEEDED = "payment.succeeded";
|
|
78
|
+
const metadataSchema = z.record(z.string(), z.unknown()).nullish();
|
|
79
|
+
const customerSchema = z.object({ customer_id: z.string().min(1) });
|
|
80
|
+
const envelopeSchema = z.object({
|
|
81
|
+
type: z.string(),
|
|
82
|
+
timestamp: z.iso.datetime({ offset: true }),
|
|
83
|
+
data: z.unknown(),
|
|
84
|
+
});
|
|
85
|
+
export const dodoSubscriptionSchema = z.object({
|
|
86
|
+
subscription_id: z.string().min(1),
|
|
87
|
+
product_id: z.string().min(1),
|
|
88
|
+
status: z.string(),
|
|
89
|
+
customer: customerSchema,
|
|
90
|
+
metadata: metadataSchema,
|
|
91
|
+
next_billing_date: z.iso.datetime({ offset: true }),
|
|
92
|
+
cancel_at_next_billing_date: z.boolean().nullish(),
|
|
93
|
+
});
|
|
94
|
+
const dodoPaymentSchema = z.object({
|
|
95
|
+
subscription_id: z.string().nullish(),
|
|
96
|
+
checkout_session_id: z.string().nullish(),
|
|
97
|
+
customer: customerSchema,
|
|
98
|
+
metadata: metadataSchema,
|
|
99
|
+
product_cart: z.array(z.object({ product_id: z.string().min(1) })).nullish(),
|
|
100
|
+
});
|
|
101
|
+
function metadataString(metadata, key) {
|
|
102
|
+
const value = metadata?.[key];
|
|
103
|
+
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
104
|
+
}
|
|
105
|
+
/** Shared by the webhook and retrieveSubscription so both map Dodo state
|
|
106
|
+
* identically. Null when the status is unknown or the product is not in
|
|
107
|
+
* `priceToTier` (SubscriptionEvent.tier is required). */
|
|
108
|
+
export function mapDodoSubscriptionState(sub, priceToTier) {
|
|
109
|
+
const status = DODO_SUBSCRIPTION_STATUSES[sub.status];
|
|
110
|
+
const tier = priceToTier[sub.product_id];
|
|
111
|
+
if (!status || !tier)
|
|
112
|
+
return null;
|
|
113
|
+
return {
|
|
114
|
+
status,
|
|
115
|
+
tier,
|
|
116
|
+
currentPeriodEnd: sub.next_billing_date,
|
|
117
|
+
cancelAt: sub.cancel_at_next_billing_date === true ? sub.next_billing_date : null,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
function currentUnixSeconds() {
|
|
121
|
+
return Math.floor(Temporal.Now.instant().epochMilliseconds / 1000);
|
|
122
|
+
}
|
|
123
|
+
function parsePaymentEvent(providerEventId, data) {
|
|
124
|
+
const parsed = dodoPaymentSchema.safeParse(data);
|
|
125
|
+
if (!parsed.success)
|
|
126
|
+
return null;
|
|
127
|
+
const payment = parsed.data;
|
|
128
|
+
// A subscription's own payments are covered by its subscription.* events.
|
|
129
|
+
if (payment.subscription_id)
|
|
130
|
+
return null;
|
|
131
|
+
const tenantId = metadataString(payment.metadata, "tenantId");
|
|
132
|
+
const priceId = payment.product_cart?.[0]?.product_id;
|
|
133
|
+
if (!tenantId || !priceId)
|
|
134
|
+
return null;
|
|
135
|
+
const consentId = parseProviderConsentId(payment.metadata?.["consentId"]);
|
|
136
|
+
return {
|
|
137
|
+
kind: BillingEventKinds.payment,
|
|
138
|
+
providerEventId,
|
|
139
|
+
providerName: DODO_PROVIDER_NAME,
|
|
140
|
+
tenantId,
|
|
141
|
+
...(consentId !== undefined && { consentId }),
|
|
142
|
+
...(payment.checkout_session_id && { providerCheckoutId: payment.checkout_session_id }),
|
|
143
|
+
providerCustomerId: payment.customer.customer_id,
|
|
144
|
+
priceId,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
function parseSubscriptionEvent(providerEventId, type, providerEventAt, data, priceToTier) {
|
|
148
|
+
const parsed = dodoSubscriptionSchema.safeParse(data);
|
|
149
|
+
if (!parsed.success)
|
|
150
|
+
return null;
|
|
151
|
+
const sub = parsed.data;
|
|
152
|
+
const tenantId = metadataString(sub.metadata, "tenantId");
|
|
153
|
+
const state = mapDodoSubscriptionState(sub, priceToTier);
|
|
154
|
+
if (!tenantId || !state)
|
|
155
|
+
return null;
|
|
156
|
+
const consentId = parseProviderConsentId(sub.metadata?.["consentId"]);
|
|
157
|
+
return {
|
|
158
|
+
providerEventId,
|
|
159
|
+
providerName: DODO_PROVIDER_NAME,
|
|
160
|
+
type,
|
|
161
|
+
tenantId,
|
|
162
|
+
...(consentId !== undefined && { consentId }),
|
|
163
|
+
providerCustomerId: sub.customer.customer_id,
|
|
164
|
+
providerSubscriptionId: sub.subscription_id,
|
|
165
|
+
providerEventAt,
|
|
166
|
+
...state,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
export function verifyAndParseDodoWebhook(runtime, options) {
|
|
170
|
+
const nowSeconds = options.nowSeconds ?? currentUnixSeconds;
|
|
171
|
+
return async (rawBody, headers, systemSecrets) => {
|
|
172
|
+
const secret = await runtime.resolveWebhookSecret(systemSecrets);
|
|
173
|
+
verifyStandardWebhook(rawBody, headers, secret, nowSeconds());
|
|
174
|
+
// Past verification the body is trusted to be Dodo's; a malformed shape is
|
|
175
|
+
// schema drift, so it is dropped (200 ignored) instead of answered with a 401.
|
|
176
|
+
const envelope = envelopeSchema.safeParse(parseJsonOrThrow(rawBody, "subscription-dodo webhook body"));
|
|
177
|
+
// verifyStandardWebhook has checked webhook-id is present.
|
|
178
|
+
const providerEventId = headers["webhook-id"];
|
|
179
|
+
if (!envelope.success || !providerEventId)
|
|
180
|
+
return null;
|
|
181
|
+
const { type, timestamp, data } = envelope.data;
|
|
182
|
+
if (type === PAYMENT_SUCCEEDED)
|
|
183
|
+
return parsePaymentEvent(providerEventId, data);
|
|
184
|
+
const normalizedType = DODO_SUBSCRIPTION_EVENT_TYPES[type];
|
|
185
|
+
if (!normalizedType)
|
|
186
|
+
return null;
|
|
187
|
+
return parseSubscriptionEvent(providerEventId, normalizedType, timestamp, data, options.priceToTier);
|
|
188
|
+
};
|
|
189
|
+
}
|
|
@@ -172,6 +172,7 @@ export function createSubscriptionStripeFeature(options = {}) {
|
|
|
172
172
|
const priceCache = createStripePriceCache();
|
|
173
173
|
const portalConfigurationCache = new Map();
|
|
174
174
|
const plugin = {
|
|
175
|
+
displayName: "Stripe",
|
|
175
176
|
verifyAndParseWebhook: verifyAndParseStripeWebhook(runtimes.webhook, {
|
|
176
177
|
priceToTier: options.priceToTier ?? {},
|
|
177
178
|
}),
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
import { requireSecretsContext } from "@cosmicdrift/kumiko-bundled-features/secrets";
|
|
28
28
|
import { SYSTEM_TENANT_ID, } from "@cosmicdrift/kumiko-framework/engine";
|
|
29
29
|
import { FeatureDisabledError, UnconfiguredError } from "@cosmicdrift/kumiko-framework/errors";
|
|
30
|
-
import {
|
|
30
|
+
import { parseJsonSafe } from "@cosmicdrift/kumiko-framework/utils";
|
|
31
31
|
import Stripe from "stripe";
|
|
32
32
|
import { SUBSCRIPTION_STRIPE_FEATURE } from "./constants.js";
|
|
33
33
|
const API_KEY_HINT = "Set the Stripe API key via config:write:set on `subscription-stripe:config:api-key` (or seed it from STRIPE_API_KEY during the env bridge).";
|
|
@@ -52,14 +52,15 @@ export function createStripeClientCache() {
|
|
|
52
52
|
// (the un-audited webhook path + the audited ctx read here) therefore returns
|
|
53
53
|
// the JSON-quoted string and must parse it back. The factory fallback is a
|
|
54
54
|
// plain env string and stays raw — so parsing is applied only to the store
|
|
55
|
-
// read, never to the fallback.
|
|
55
|
+
// read, never to the fallback. The error must not carry the input: the JSON
|
|
56
|
+
// parser's own message would repeat the credential into logs and the 401 body.
|
|
56
57
|
function parseStoredSecret(raw) {
|
|
57
58
|
if (raw === undefined || raw.length === 0)
|
|
58
59
|
return undefined;
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
60
|
+
const parsed = parseJsonSafe(raw, undefined);
|
|
61
|
+
if (parsed === undefined) {
|
|
62
|
+
throw new Error("subscription-stripe credential (backing:secrets) is not valid JSON");
|
|
63
|
+
}
|
|
63
64
|
return typeof parsed === "string" && parsed.length > 0 ? parsed : undefined;
|
|
64
65
|
}
|
|
65
66
|
export function createStripeRuntimes(deps) {
|
|
@@ -80,10 +80,11 @@ export type TemplateResolverApi = {
|
|
|
80
80
|
readonly resolveLayout: (args: ResolveLayoutRequest) => Promise<TemplateResource>;
|
|
81
81
|
};
|
|
82
82
|
export declare function createTemplateResolverApi(db: DbRunner | TenantDb, registry: Registry): TemplateResolverApi;
|
|
83
|
-
|
|
83
|
+
type DeliveredRowKey = Pick<TemplateResourceRow, "tenantId" | "kind" | "slug">;
|
|
84
|
+
export declare function isRowDelivered(db: DbRunner | TenantDb, policy: TemplateDeliveryPolicy, row: DeliveredRowKey): Promise<boolean>;
|
|
84
85
|
export declare function isRowDeliveredTo(db: DbRunner | TenantDb, policy: TemplateDeliveryPolicy, user: {
|
|
85
86
|
readonly roles: readonly string[];
|
|
86
|
-
}, row:
|
|
87
|
+
}, row: DeliveredRowKey): Promise<boolean>;
|
|
87
88
|
export declare function filterDeliveredTo(db: DbRunner | TenantDb, policy: TemplateDeliveryPolicy, user: {
|
|
88
89
|
readonly roles: readonly string[];
|
|
89
90
|
}, rows: readonly TemplateResourceRow[]): Promise<TemplateResourceRow[]>;
|
|
@@ -69,9 +69,31 @@ export async function isRowDelivered(db, policy, row) {
|
|
|
69
69
|
export async function isRowDeliveredTo(db, policy, user, row) {
|
|
70
70
|
return isSystemAdmin(user) || (await isRowDelivered(db, policy, row));
|
|
71
71
|
}
|
|
72
|
+
// One system-slug lookup per kind instead of one query per row, because by-tenant is anonymous.
|
|
72
73
|
export async function filterDeliveredTo(db, policy, user, rows) {
|
|
73
|
-
|
|
74
|
-
|
|
74
|
+
if (isSystemAdmin(user))
|
|
75
|
+
return [...rows];
|
|
76
|
+
const served = rows.filter((row) => isRowServed(policy, row));
|
|
77
|
+
const shadowable = served.filter((row) => row.tenantId !== SYSTEM_TENANT_ID && isServedByCollectionOnly(policy, row));
|
|
78
|
+
const slugsByKind = new Map();
|
|
79
|
+
for (const row of shadowable) {
|
|
80
|
+
const slugs = slugsByKind.get(row.kind) ?? new Set();
|
|
81
|
+
slugs.add(row.slug);
|
|
82
|
+
slugsByKind.set(row.kind, slugs);
|
|
83
|
+
}
|
|
84
|
+
const systemKeys = new Set();
|
|
85
|
+
for (const [kind, slugs] of slugsByKind) {
|
|
86
|
+
const systemRows = await selectMany(db, templateResourcesTable, {
|
|
87
|
+
tenantId: SYSTEM_TENANT_ID,
|
|
88
|
+
kind,
|
|
89
|
+
slug: [...slugs],
|
|
90
|
+
});
|
|
91
|
+
for (const systemRow of systemRows)
|
|
92
|
+
systemKeys.add(`${kind}|${systemRow.slug}`);
|
|
93
|
+
}
|
|
94
|
+
return served.filter((row) => row.tenantId === SYSTEM_TENANT_ID ||
|
|
95
|
+
!isServedByCollectionOnly(policy, row) ||
|
|
96
|
+
!systemKeys.has(`${row.kind}|${row.slug}`));
|
|
75
97
|
}
|
|
76
98
|
export async function systemTemplateExists(db, kind, slug) {
|
|
77
99
|
const rows = await selectMany(db, templateResourcesTable, { tenantId: SYSTEM_TENANT_ID, slug, kind }, { limit: 1 });
|
|
@@ -320,6 +320,7 @@ export function compileDocumentBodyToHandlebars(html, variables, options = {}) {
|
|
|
320
320
|
for (const element of Array.from(root.querySelectorAll("div[data-images]"))) {
|
|
321
321
|
rewriteImagesBlock(element, state, options);
|
|
322
322
|
}
|
|
323
|
+
// Order matters: the conditional wraps first so the row's #each lands inside the #if (evaluated once per list).
|
|
323
324
|
for (const block of Array.from(root.querySelectorAll(`[${DOCUMENT_CONDITION_ATTRIBUTE}]`))) {
|
|
324
325
|
rewriteConditionalBlock(block, state);
|
|
325
326
|
}
|
|
@@ -29,7 +29,7 @@ import { tenantOverridesQuery } from "./handlers/tenant-overrides.query.js";
|
|
|
29
29
|
import { makeArchiveSystemWrite, makeArchiveWrite, makePublishSystemWrite, makePublishWrite, } from "./handlers/toggle-status.write.js";
|
|
30
30
|
import { upsertSystemWrite } from "./handlers/upsert-system.write.js";
|
|
31
31
|
import { makeUpsertTenantWrite } from "./handlers/upsert-tenant.write.js";
|
|
32
|
-
import {
|
|
32
|
+
import { makeUsageQuery } from "./handlers/usage.query.js";
|
|
33
33
|
import { validateQuery } from "./handlers/validate.query.js";
|
|
34
34
|
import { TEMPLATE_RESOLVER_I18N } from "./i18n.js";
|
|
35
35
|
import { templateResourceEntity } from "./table.js";
|
|
@@ -127,7 +127,7 @@ export function createTemplateResolverFeature(opts = {}) {
|
|
|
127
127
|
list: r.queryHandler(listQuery),
|
|
128
128
|
bySlug: r.queryHandler(bySlugQuery),
|
|
129
129
|
byTenant: r.queryHandler(byTenantQuery),
|
|
130
|
-
usage: r.queryHandler(
|
|
130
|
+
usage: r.queryHandler(makeUsageQuery(policy)),
|
|
131
131
|
overview: r.queryHandler(makeOverviewQuery(policy)),
|
|
132
132
|
layoutSource: r.queryHandler(layoutSourceQuery),
|
|
133
133
|
tenantOverrides: r.queryHandler(tenantOverridesQuery),
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { fetchOne } from "@cosmicdrift/kumiko-framework/db";
|
|
2
2
|
import { ValidationError, writeFailure, } from "@cosmicdrift/kumiko-framework/errors";
|
|
3
3
|
import { requireForTenant } from "../../shared/index.js";
|
|
4
|
-
import { attachmentLimitFailure, attachmentNeedsMailTemplate, createPreparedBundleResourceEntry, prepareTemplateAttachmentBeforeTransaction, prepareTemplateResourceBeforeTransaction, readTemplateResourceMaxBytes, releaseAll, } from "../store-template-resource.js";
|
|
4
|
+
import { attachmentLimitFailure, attachmentNeedsMailTemplate, createPreparedBundleResourceEntry, prepareTemplateAttachmentBeforeTransaction, prepareTemplateResourceBeforeTransaction, readTemplateResourceMaxBytes, releaseAfterFailure, releaseAll, } from "../store-template-resource.js";
|
|
5
5
|
import { templateResourcesTable } from "../table.js";
|
|
6
6
|
import { attachTemplateResources } from "../template-resources.js";
|
|
7
7
|
import { resolveTemplateVariables } from "../template-validation.js";
|
|
@@ -94,7 +94,7 @@ export async function prepareTemplateBundleBeforeTransaction(args) {
|
|
|
94
94
|
})
|
|
95
95
|
: await prepareTemplateResourceBeforeTransaction(common);
|
|
96
96
|
if (!result.isSuccess) {
|
|
97
|
-
await
|
|
97
|
+
await releaseAfterFailure(args.ctx, releases);
|
|
98
98
|
return result;
|
|
99
99
|
}
|
|
100
100
|
if (result.release)
|
|
@@ -103,7 +103,7 @@ export async function prepareTemplateBundleBeforeTransaction(args) {
|
|
|
103
103
|
}
|
|
104
104
|
}
|
|
105
105
|
catch (cause) {
|
|
106
|
-
await
|
|
106
|
+
await releaseAfterFailure(args.ctx, releases);
|
|
107
107
|
throw cause;
|
|
108
108
|
}
|
|
109
109
|
return { isSuccess: true, data: prepared, release: () => releaseAll(releases) };
|
|
@@ -17,4 +17,8 @@ export declare function makeCopyLocaleWrite(policy: TemplateEditPolicy): import(
|
|
|
17
17
|
system: "system";
|
|
18
18
|
tenant: "tenant";
|
|
19
19
|
}>>;
|
|
20
|
-
}, z.core.$strip>, import("./template-write.ts").WrittenTemplateRow, import("@cosmicdrift/kumiko-types/event-type-map").KumikoEventTypeMap,
|
|
20
|
+
}, z.core.$strip>, import("./template-write.ts").WrittenTemplateRow, import("@cosmicdrift/kumiko-types/event-type-map").KumikoEventTypeMap, {
|
|
21
|
+
sourceId: string | number;
|
|
22
|
+
sourceVersion: number;
|
|
23
|
+
copies: readonly import("../store-template-resource.ts").PreparedLinkedResourceCopy[];
|
|
24
|
+
}>;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { fetchOne } from "@cosmicdrift/kumiko-framework/db";
|
|
2
2
|
import { defineWriteHandler, SYSTEM_TENANT_ID, } from "@cosmicdrift/kumiko-framework/engine";
|
|
3
|
-
import { AccessDeniedError, NotFoundError, ValidationError, writeFailure, } from "@cosmicdrift/kumiko-framework/errors";
|
|
3
|
+
import { AccessDeniedError, NotFoundError, ValidationError, VersionConflictError, writeFailure, } from "@cosmicdrift/kumiko-framework/errors";
|
|
4
4
|
import { isOneOf } from "@cosmicdrift/kumiko-types/type-guards";
|
|
5
5
|
import * as z from "zod";
|
|
6
6
|
import { requireForTenant } from "../../shared/index.js";
|
|
@@ -111,14 +111,19 @@ export function makeCopyLocaleWrite(policy) {
|
|
|
111
111
|
const request = await checkCopyRequest(ctx, policy, event);
|
|
112
112
|
if (!request.isSuccess)
|
|
113
113
|
return request;
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
114
|
+
const { source, tenantId, sourceIsForeign, sourceResources } = request.data;
|
|
115
|
+
const sourceStamp = { sourceId: source.id, sourceVersion: source.version };
|
|
116
|
+
if (!sourceIsForeign)
|
|
117
|
+
return { isSuccess: true, data: { ...sourceStamp, copies: [] } };
|
|
118
|
+
const prepared = await prepareLinkedResourceCopiesBeforeTransaction({
|
|
117
119
|
ctx,
|
|
118
120
|
user: event.user,
|
|
119
|
-
tenantId
|
|
120
|
-
resources:
|
|
121
|
+
tenantId,
|
|
122
|
+
resources: sourceResources,
|
|
121
123
|
});
|
|
124
|
+
if (!prepared.isSuccess)
|
|
125
|
+
return prepared;
|
|
126
|
+
return { ...prepared, data: { ...sourceStamp, copies: prepared.data } };
|
|
122
127
|
},
|
|
123
128
|
handler: async (event, ctx) => {
|
|
124
129
|
const { slug, kind, toLocale } = event.payload;
|
|
@@ -126,6 +131,16 @@ export function makeCopyLocaleWrite(policy) {
|
|
|
126
131
|
if (!request.isSuccess)
|
|
127
132
|
return request;
|
|
128
133
|
const { scope, tenantId, source, contentFormat, sourceResources, sourceIsForeign } = request.data;
|
|
134
|
+
// The uploads were prepared for the source as it was before the transaction; a source that
|
|
135
|
+
// changed since (own row created or deleted, edited) no longer matches them.
|
|
136
|
+
if (event.prepared.sourceId !== source.id ||
|
|
137
|
+
event.prepared.sourceVersion !== source.version) {
|
|
138
|
+
return writeFailure(new VersionConflictError({
|
|
139
|
+
entityId: source.id,
|
|
140
|
+
expectedVersion: event.prepared.sourceVersion,
|
|
141
|
+
currentVersion: source.version,
|
|
142
|
+
}));
|
|
143
|
+
}
|
|
129
144
|
const variableSchema = storedRecord(source.variableSchema);
|
|
130
145
|
const written = await writeTemplateRow({
|
|
131
146
|
ctx,
|
|
@@ -149,7 +164,7 @@ export function makeCopyLocaleWrite(policy) {
|
|
|
149
164
|
if (!written.isSuccess)
|
|
150
165
|
return written;
|
|
151
166
|
const resources = sourceIsForeign
|
|
152
|
-
? await createLinkedResourceCopies({ ctx, user: event.user, tenantId }, event.prepared)
|
|
167
|
+
? await createLinkedResourceCopies({ ctx, user: event.user, tenantId }, event.prepared.copies)
|
|
153
168
|
: { isSuccess: true, data: sourceResources };
|
|
154
169
|
if (!resources.isSuccess)
|
|
155
170
|
return resources;
|
|
@@ -2,8 +2,9 @@ import { fetchOne } from "@cosmicdrift/kumiko-framework/db";
|
|
|
2
2
|
import { defineQueryHandler, SYSTEM_TENANT_ID } from "@cosmicdrift/kumiko-framework/engine";
|
|
3
3
|
import { NotFoundError } from "@cosmicdrift/kumiko-framework/errors";
|
|
4
4
|
import * as z from "zod";
|
|
5
|
+
import { isRowDeliveredTo } from "../api.js";
|
|
5
6
|
import { templateResourcesTable } from "../table.js";
|
|
6
|
-
import { deliveryPolicyOf
|
|
7
|
+
import { deliveryPolicyOf } from "../template-delivery-policy.js";
|
|
7
8
|
import { isHiddenSystemDraft } from "../template-draft.js";
|
|
8
9
|
import { buildOverrideDiff } from "./override-diff.js";
|
|
9
10
|
export const diffToSystemQuery = defineQueryHandler({
|
|
@@ -18,7 +19,7 @@ export const diffToSystemQuery = defineQueryHandler({
|
|
|
18
19
|
if (!tenantRow ||
|
|
19
20
|
tenantRow.tenantId !== query.user.tenantId ||
|
|
20
21
|
tenantRow.scope !== "tenant" ||
|
|
21
|
-
!
|
|
22
|
+
!(await isRowDeliveredTo(ctx.db, deliveryPolicyOf(ctx.registry), query.user, tenantRow))) {
|
|
22
23
|
throw new NotFoundError("template-resource", query.payload.id);
|
|
23
24
|
}
|
|
24
25
|
const systemRow = await fetchOne(ctx.db, templateResourcesTable, {
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { fetchOne } from "@cosmicdrift/kumiko-framework/db";
|
|
2
2
|
import { SYSTEM_TENANT_ID, } from "@cosmicdrift/kumiko-framework/engine";
|
|
3
3
|
import { isPlainObject } from "@cosmicdrift/kumiko-types/type-guards";
|
|
4
|
+
import { isRowDeliveredTo } from "../api.js";
|
|
4
5
|
import { templateResourcesTable } from "../table.js";
|
|
5
|
-
import { deliveryPolicyOf
|
|
6
|
+
import { deliveryPolicyOf } from "../template-delivery-policy.js";
|
|
6
7
|
import { isHiddenSystemDraft, toWorkingRow } from "../template-draft.js";
|
|
7
8
|
import { isSystemOnlyTemplateHiddenFrom } from "../template-system-only.js";
|
|
8
9
|
import { parseStoredJson } from "./template-gates.js";
|
|
@@ -20,7 +21,7 @@ export async function loadDocumentBase(ctx, user, where) {
|
|
|
20
21
|
// A locked override is neither shown nor edited by a non-SystemAdmin; the served system default is.
|
|
21
22
|
const ownIsVisible = own !== undefined &&
|
|
22
23
|
!isSystemOnlyTemplateHiddenFrom(user, ctx.registry, own) &&
|
|
23
|
-
|
|
24
|
+
(await isRowDeliveredTo(ctx.db, deliveryPolicyOf(ctx.registry), user, own));
|
|
24
25
|
if (own !== undefined && ownIsVisible) {
|
|
25
26
|
const working = toWorkingRow(own, user.tenantId);
|
|
26
27
|
return {
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { fetchOne } from "@cosmicdrift/kumiko-framework/db";
|
|
2
2
|
import { defineQueryHandler } from "@cosmicdrift/kumiko-framework/engine";
|
|
3
3
|
import * as z from "zod";
|
|
4
|
+
import { isRowDeliveredTo } from "../api.js";
|
|
4
5
|
import { templateResourcesTable } from "../table.js";
|
|
5
|
-
import { deliveryPolicyOf
|
|
6
|
+
import { deliveryPolicyOf } from "../template-delivery-policy.js";
|
|
6
7
|
import { draftViewerTenantId, isHiddenSystemDraft, toWorkingRow } from "../template-draft.js";
|
|
7
8
|
import { isSystemOnlyTemplateHiddenFrom } from "../template-system-only.js";
|
|
8
9
|
// Admin lookup for the UI editing flow. Returns the raw template, including draft/
|
|
@@ -22,7 +23,7 @@ export const findByIdQuery = defineQueryHandler({
|
|
|
22
23
|
if (!stored ||
|
|
23
24
|
isHiddenSystemDraft(query.user, stored) ||
|
|
24
25
|
isSystemOnlyTemplateHiddenFrom(query.user, ctx.registry, stored) ||
|
|
25
|
-
!
|
|
26
|
+
!(await isRowDeliveredTo(ctx.db, deliveryPolicyOf(ctx.registry), query.user, stored))) {
|
|
26
27
|
return null;
|
|
27
28
|
}
|
|
28
29
|
const row = toWorkingRow(stored, draftViewerTenantId(query.user, stored));
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { selectMany } from "@cosmicdrift/kumiko-framework/db";
|
|
2
2
|
import { defineQueryHandler } from "@cosmicdrift/kumiko-framework/engine";
|
|
3
3
|
import * as z from "zod";
|
|
4
|
+
import { filterDeliveredTo } from "../api.js";
|
|
4
5
|
import { templateResourcesTable } from "../table.js";
|
|
5
|
-
import { deliveryPolicyOf
|
|
6
|
+
import { deliveryPolicyOf } from "../template-delivery-policy.js";
|
|
6
7
|
import { isHiddenSystemDraft } from "../template-draft.js";
|
|
7
8
|
import { isSystemOnlyTemplateHiddenFrom } from "../template-system-only.js";
|
|
8
9
|
import { TEMPLATE_KINDS, TEMPLATE_STATUSES } from "../web/constants.js";
|
|
@@ -41,11 +42,9 @@ export const listQuery = defineQueryHandler({
|
|
|
41
42
|
limit: 500,
|
|
42
43
|
}));
|
|
43
44
|
const deliveryPolicy = deliveryPolicyOf(ctx.registry);
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
isRowServedTo(deliveryPolicy, query.user, row))
|
|
48
|
-
.map((row) => ({
|
|
45
|
+
const candidates = rows.filter((row) => !isHiddenSystemDraft(query.user, row) &&
|
|
46
|
+
!isSystemOnlyTemplateHiddenFrom(query.user, ctx.registry, row));
|
|
47
|
+
return (await filterDeliveredTo(ctx.db, deliveryPolicy, query.user, candidates)).map((row) => ({
|
|
49
48
|
id: String(row.id),
|
|
50
49
|
tenantId: row.tenantId,
|
|
51
50
|
slug: row.slug,
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import * as z from "zod";
|
|
2
2
|
import { type TemplateResourceRow } from "../table.ts";
|
|
3
3
|
import { type TemplateDeliveryPolicy } from "../template-delivery-policy.ts";
|
|
4
|
+
import { isHiddenSystemDraft } from "../template-draft.ts";
|
|
4
5
|
import { type TemplateEditPolicy } from "../template-edit-policy.ts";
|
|
6
|
+
import { isSystemOnlyTemplateHiddenFrom } from "../template-system-only.ts";
|
|
5
7
|
import { type EffectiveMailLayoutSlugs } from "../template-usage.ts";
|
|
6
8
|
import { type TemplateStatus } from "../web/constants.ts";
|
|
7
9
|
declare const OVERVIEW_KINDS: readonly ["mail", "document-pdf", "mail-layout", "pdf-layout"];
|
|
@@ -51,6 +53,7 @@ export type OverviewResponse = {
|
|
|
51
53
|
/** True when the tenant has more template rows than the overview reads; rows and total are then incomplete. */
|
|
52
54
|
readonly truncated: boolean;
|
|
53
55
|
};
|
|
56
|
+
export declare function isRowVisibleToCaller(policy: TemplateEditPolicy, user: Parameters<typeof isHiddenSystemDraft>[0], registry: Parameters<typeof isSystemOnlyTemplateHiddenFrom>[1], row: TemplateResourceRow): boolean;
|
|
54
57
|
export declare function effectiveMailLayoutSlugs(rows: readonly TemplateResourceRow[], policy: TemplateDeliveryPolicy): EffectiveMailLayoutSlugs;
|
|
55
58
|
export declare function makeOverviewQuery(policy: TemplateEditPolicy): import("@cosmicdrift/kumiko-types/define-handler").QueryHandlerDefinition<"overview", z.ZodObject<{
|
|
56
59
|
search: z.ZodOptional<z.ZodString>;
|
|
@@ -69,6 +69,11 @@ function effectiveRowsOf(rows, policy) {
|
|
|
69
69
|
? { origin: "tenant", rows: tenantRows, lockedOverride: false, lockedOverrideId: null }
|
|
70
70
|
: null;
|
|
71
71
|
}
|
|
72
|
+
export function isRowVisibleToCaller(policy, user, registry, row) {
|
|
73
|
+
return (!isHiddenSystemDraft(user, row) &&
|
|
74
|
+
!isSystemOnlyTemplateHiddenFrom(user, registry, row) &&
|
|
75
|
+
mayEditTemplate(policy, user, row));
|
|
76
|
+
}
|
|
72
77
|
export function effectiveMailLayoutSlugs(rows, policy) {
|
|
73
78
|
const byLocale = new Map();
|
|
74
79
|
for (const row of rows) {
|
|
@@ -242,9 +247,7 @@ export function makeOverviewQuery(policy) {
|
|
|
242
247
|
limit: OVERVIEW_ROW_LIMIT,
|
|
243
248
|
});
|
|
244
249
|
}
|
|
245
|
-
const visibleRows = rows.filter((row) =>
|
|
246
|
-
!isSystemOnlyTemplateHiddenFrom(query.user, ctx.registry, row) &&
|
|
247
|
-
mayEditTemplate(policy, query.user, row));
|
|
250
|
+
const visibleRows = rows.filter((row) => isRowVisibleToCaller(policy, query.user, ctx.registry, row));
|
|
248
251
|
const deliveryPolicy = deliveryPolicyOf(ctx.registry);
|
|
249
252
|
const { drafts, layoutSlugByDocument } = buildDrafts(groupBySlugAndKind(visibleRows), query.user, deliveryPolicy);
|
|
250
253
|
const effectiveLayouts = effectiveMailLayoutSlugs(visibleRows, deliveryPolicy);
|
|
@@ -14,9 +14,7 @@ export const tenantOverrideCountsQuery = defineQueryHandler({
|
|
|
14
14
|
description: "Counts, per template slug and kind, the enabled tenants that override it with an active version, next to the number of enabled tenants; one query for the whole template overview instead of one tenant-overrides call per row.",
|
|
15
15
|
escapeHatch: { grants: ["unsafeRaw"], reason: TENANT_OVERRIDES_REASON },
|
|
16
16
|
handler: async (_query, ctx) => {
|
|
17
|
-
const found = await selectMany(ctx.db.unsafeRaw(), templateResourcesTable, {
|
|
18
|
-
scope: "tenant",
|
|
19
|
-
});
|
|
17
|
+
const found = await selectMany(ctx.db.unsafeRaw(), templateResourcesTable, { scope: "tenant" }, { fields: ["tenantId", "slug", "kind", "status", "version"] });
|
|
20
18
|
const enabledTenantIds = await loadEnabledTenantIds(ctx);
|
|
21
19
|
const rowsBySlugAndKind = new Map();
|
|
22
20
|
for (const row of found) {
|
|
@@ -8,5 +8,6 @@ type TenantIdQueryContext = {
|
|
|
8
8
|
};
|
|
9
9
|
/** Null when the tenant feature is not installed: every tenant row then counts. */
|
|
10
10
|
export declare function loadEnabledTenantIds(ctx: TenantIdQueryContext): Promise<ReadonlySet<string> | null>;
|
|
11
|
-
export
|
|
11
|
+
export type OverrideCandidate = Pick<TemplateResourceRow, "tenantId" | "status" | "version">;
|
|
12
|
+
export declare function pickBestRowPerTenant<TRow extends OverrideCandidate>(found: readonly TRow[], enabledTenantIds: ReadonlySet<string> | null): Map<string, TRow>;
|
|
12
13
|
export {};
|