@cosmicdrift/kumiko-samples 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/package.json +1 -1
- package/packages/bundled-features/package.json +8 -1
- package/packages/bundled-features/src/agent-tools/changes.json +6 -0
- package/packages/bundled-features/src/agent-tools/tool-dispatch.ts +9 -9
- package/packages/bundled-features/src/audit/changes.json +12 -0
- package/packages/bundled-features/src/audit/handlers/audit-events.ts +1 -1
- package/packages/bundled-features/src/auth-mfa/changes.json +6 -0
- package/packages/bundled-features/src/auth-mfa/feature-exports.ts +2 -2
- package/packages/bundled-features/src/auth-mfa/feature.ts +6 -0
- package/packages/bundled-features/src/auth-mfa/web/i18n.ts +6 -1
- package/packages/bundled-features/src/auth-mfa/web/mfa-code-or-recovery-field.tsx +68 -0
- package/packages/bundled-features/src/auth-mfa/web/mfa-disable-dialog.tsx +23 -13
- package/packages/bundled-features/src/auth-mfa/web/mfa-regenerate-recovery-dialog.tsx +23 -13
- package/packages/bundled-features/src/auth-mfa/web/mfa-setup-preauth-screen.tsx +15 -6
- package/packages/bundled-features/src/auth-mfa/web/mfa-verify-screen.tsx +65 -18
- package/packages/bundled-features/src/billing-foundation/changes.json +55 -0
- package/packages/bundled-features/src/billing-foundation/checkout-core.ts +32 -42
- package/packages/bundled-features/src/billing-foundation/consumer-protection/confirmation-mail.ts +8 -2
- package/packages/bundled-features/src/billing-foundation/consumer-protection/consent-gate.ts +2 -0
- package/packages/bundled-features/src/billing-foundation/consumer-protection/issue-confirmation.ts +14 -3
- package/packages/bundled-features/src/billing-foundation/consumer-protection/mail-notifications.ts +3 -4
- package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-receipt-limit.ts +12 -6
- package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-record.ts +35 -19
- package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-request.ts +12 -8
- package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-revoke.ts +14 -8
- package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-texts.ts +5 -0
- package/packages/bundled-features/src/billing-foundation/consumer-protection/withdrawal-period.ts +8 -10
- package/packages/bundled-features/src/billing-foundation/events.ts +1 -0
- package/packages/bundled-features/src/billing-foundation/feature.ts +3 -1
- package/packages/bundled-features/src/billing-foundation/handlers/billing-plans.query.ts +71 -6
- package/packages/bundled-features/src/billing-foundation/handlers/create-checkout-session.write.ts +1 -0
- package/packages/bundled-features/src/billing-foundation/handlers/plan-switch-preview.query.ts +65 -0
- package/packages/bundled-features/src/billing-foundation/handlers/process-event.write.ts +9 -2
- package/packages/bundled-features/src/billing-foundation/handlers/start-plan-checkout.write.ts +44 -8
- package/packages/bundled-features/src/billing-foundation/handlers/switch-plan.write.ts +72 -82
- package/packages/bundled-features/src/billing-foundation/handlers/sync-subscription.write.ts +14 -1
- package/packages/bundled-features/src/billing-foundation/i18n.ts +22 -0
- package/packages/bundled-features/src/billing-foundation/index.ts +2 -0
- package/packages/bundled-features/src/billing-foundation/plan-catalog.ts +62 -27
- package/packages/bundled-features/src/billing-foundation/plan-switch.ts +84 -0
- package/packages/bundled-features/src/billing-foundation/types.ts +80 -1
- package/packages/bundled-features/src/billing-foundation/validate-options.ts +19 -0
- package/packages/bundled-features/src/billing-foundation/web/billing-plans-panel.tsx +119 -47
- package/packages/bundled-features/src/billing-foundation/web/checkout-consent-dialog.tsx +89 -42
- package/packages/bundled-features/src/billing-foundation/web/constants.ts +4 -0
- package/packages/bundled-features/src/billing-foundation/web/plan-price.ts +55 -0
- package/packages/bundled-features/src/billing-foundation/web/plan-switch-confirm-dialog.tsx +224 -0
- package/packages/bundled-features/src/cap-counter/cap-reservation.ts +5 -1
- package/packages/bundled-features/src/cap-counter/changes.json +18 -0
- package/packages/bundled-features/src/cap-counter/enforce-cap.ts +2 -1
- package/packages/bundled-features/src/cap-counter/stock-cap-guard.ts +8 -1
- package/packages/bundled-features/src/channel-email/changes.json +6 -0
- package/packages/bundled-features/src/channel-email/feature.ts +1 -1
- package/packages/bundled-features/src/config/changes.json +12 -0
- package/packages/bundled-features/src/config/handlers/cascade.query.ts +4 -2
- package/packages/bundled-features/src/config/handlers/values.query.ts +4 -2
- package/packages/bundled-features/src/config/write-helpers.ts +27 -8
- package/packages/bundled-features/src/crypto-shredding/changes.json +6 -0
- package/packages/bundled-features/src/crypto-shredding/handlers/forget-subject.write.ts +10 -9
- package/packages/bundled-features/src/delivery/changes.json +30 -0
- package/packages/bundled-features/src/delivery/channel-context.ts +9 -3
- package/packages/bundled-features/src/delivery/db/queries/preferences.ts +2 -2
- package/packages/bundled-features/src/delivery/delivery-service.ts +35 -5
- package/packages/bundled-features/src/delivery/handlers/preferences.query.ts +20 -1
- package/packages/bundled-features/src/delivery/handlers/unsubscribe-page-chrome.query.ts +1 -1
- package/packages/bundled-features/src/delivery/unsubscribe-link.ts +3 -1
- package/packages/bundled-features/src/delivery/unsubscribe-page-strings.ts +1 -0
- package/packages/bundled-features/src/delivery/unsubscribe-page.ts +6 -0
- package/packages/bundled-features/src/delivery/unsubscribe.ts +36 -15
- package/packages/bundled-features/src/file-foundation/index.ts +1 -0
- package/packages/bundled-features/src/file-foundation/read-file-ref-content.ts +7 -1
- package/packages/bundled-features/src/file-provider-s3/env-named-feature.ts +0 -2
- package/packages/bundled-features/src/files/README.md +1 -1
- package/packages/bundled-features/src/form-draft/changes.json +6 -0
- package/packages/bundled-features/src/form-draft/db/queries/cleanup.ts +2 -2
- package/packages/bundled-features/src/form-draft/decrypt-draft.ts +4 -1
- package/packages/bundled-features/src/host-routing/README.md +1 -1
- package/packages/bundled-features/src/host-routing/changes.json +6 -0
- package/packages/bundled-features/src/host-routing/dispatch.ts +16 -17
- package/packages/bundled-features/src/host-routing/options.ts +2 -1
- package/packages/bundled-features/src/inbound-mail-foundation/changes.json +18 -0
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/update-account.write.ts +17 -5
- package/packages/bundled-features/src/inbound-mail-foundation/i18n.ts +0 -1
- package/packages/bundled-features/src/inbound-mail-foundation/screens.ts +0 -3
- package/packages/bundled-features/src/inbound-mail-foundation/types.ts +4 -3
- package/packages/bundled-features/src/inbound-mail-foundation/watch-supervisor.ts +32 -16
- package/packages/bundled-features/src/jobs/changes.json +13 -0
- package/packages/bundled-features/src/jobs/feature.ts +14 -4
- package/packages/bundled-features/src/jobs/handlers/tenant-failures.query.ts +45 -40
- package/packages/bundled-features/src/jobs/handlers/tenant-runs.query.ts +63 -59
- package/packages/bundled-features/src/jobs/job-run-logger.ts +5 -4
- package/packages/bundled-features/src/legal-pages/README.md +1 -1
- package/packages/bundled-features/src/mail-foundation/changes.json +6 -0
- package/packages/bundled-features/src/mail-foundation/feature.ts +5 -3
- package/packages/bundled-features/src/personal-access-tokens/feature.ts +4 -1
- package/packages/bundled-features/src/renderer-pdf/changes.json +6 -0
- package/packages/bundled-features/src/renderer-pdf/render-table-pdf.ts +27 -10
- package/packages/bundled-features/src/sessions/feature.ts +7 -2
- package/packages/bundled-features/src/shared/decrypt-stored-pii.ts +10 -0
- package/packages/bundled-features/src/shared/index.ts +1 -1
- package/packages/bundled-features/src/shared/mail-template-body.ts +10 -3
- package/packages/bundled-features/src/step-dispatcher/changes.json +6 -0
- package/packages/bundled-features/src/step-dispatcher/feature.ts +1 -0
- package/packages/bundled-features/src/subscription-dodo/changes.json +14 -0
- package/packages/bundled-features/src/subscription-dodo/client.ts +45 -0
- package/packages/bundled-features/src/subscription-dodo/constants.ts +24 -0
- package/packages/bundled-features/src/subscription-dodo/feature.ts +207 -0
- package/packages/bundled-features/src/subscription-dodo/index.ts +16 -0
- package/packages/bundled-features/src/subscription-dodo/plugin-methods.ts +345 -0
- package/packages/bundled-features/src/subscription-dodo/runtime.ts +126 -0
- package/packages/bundled-features/src/subscription-dodo/verify-webhook.ts +262 -0
- package/packages/bundled-features/src/subscription-stripe/changes.json +12 -0
- package/packages/bundled-features/src/subscription-stripe/feature.ts +1 -0
- package/packages/bundled-features/src/subscription-stripe/runtime.ts +7 -6
- package/packages/bundled-features/src/template-resolver/api.ts +31 -4
- package/packages/bundled-features/src/template-resolver/changes.json +48 -0
- package/packages/bundled-features/src/template-resolver/compile-document-body.ts +1 -0
- package/packages/bundled-features/src/template-resolver/feature.ts +2 -2
- package/packages/bundled-features/src/template-resolver/handlers/bundle-import.ts +3 -2
- package/packages/bundled-features/src/template-resolver/handlers/copy-locale.write.ts +27 -5
- package/packages/bundled-features/src/template-resolver/handlers/diff-to-system.query.ts +3 -2
- package/packages/bundled-features/src/template-resolver/handlers/document-base.ts +3 -2
- package/packages/bundled-features/src/template-resolver/handlers/find-by-id.query.ts +3 -2
- package/packages/bundled-features/src/template-resolver/handlers/list.query.ts +18 -19
- package/packages/bundled-features/src/template-resolver/handlers/overview.query.ts +15 -5
- package/packages/bundled-features/src/template-resolver/handlers/tenant-override-counts.query.ts +9 -5
- package/packages/bundled-features/src/template-resolver/handlers/tenant-override-selection.ts +8 -6
- package/packages/bundled-features/src/template-resolver/handlers/tenant-overrides.query.ts +44 -5
- package/packages/bundled-features/src/template-resolver/handlers/usage.query.ts +37 -26
- package/packages/bundled-features/src/template-resolver/handlers/validate.query.ts +7 -2
- package/packages/bundled-features/src/template-resolver/seeding.ts +32 -14
- package/packages/bundled-features/src/template-resolver/store-template-resource.ts +22 -4
- package/packages/bundled-features/src/template-resolver/template-delivery-policy.ts +0 -11
- package/packages/bundled-features/src/template-resolver/template-meta.ts +12 -4
- package/packages/bundled-features/src/template-resolver/template-preview.ts +11 -3
- package/packages/bundled-features/src/template-resolver/web/constants.ts +7 -5
- package/packages/bundled-features/src/template-resolver/web/index.ts +7 -0
- package/packages/bundled-features/src/template-resolver/web/mail-layout-constants.ts +8 -0
- package/packages/bundled-features/src/tenant/changes.json +6 -0
- package/packages/bundled-features/src/tenant/handlers/member-directory.query.ts +9 -3
- package/packages/bundled-features/src/tenant/handlers/tenant-directory.query.ts +7 -4
- package/packages/bundled-features/src/tenant-lifecycle/changes.json +6 -0
- package/packages/bundled-features/src/tenant-lifecycle/constants.ts +5 -0
- package/packages/bundled-features/src/tenant-lifecycle/run-tenant-destroy.ts +5 -1
- package/packages/bundled-features/src/tier-engine/changes.json +6 -0
- package/packages/bundled-features/src/tier-engine/handlers/set-tenant-tier.write.ts +4 -1
- package/packages/bundled-features/src/tier-engine/web/constants.ts +3 -3
- package/packages/bundled-features/src/user-data-rights/run-export-jobs.ts +3 -4
- package/packages/bundled-features/src/user-data-rights/run-forget-cleanup.ts +2 -1
- package/packages/bundled-features/src/waitlist/changes.json +18 -0
- package/packages/bundled-features/src/waitlist/handlers/export-purge.write.ts +2 -1
- package/packages/bundled-features/src/waitlist/handlers/submit.write.ts +5 -5
- package/packages/bundled-features/src/waitlist/import.ts +5 -0
- package/packages/bundled-features/src/waitlist/legacy-migrate.ts +22 -3
- package/packages/bundled-features/src/waitlist/link-on-signup.ts +20 -10
- package/packages/bundled-features/src/waitlist/options.ts +3 -0
- package/samples/apps/styleguide/src/features/widgets/i18n.ts +11 -0
- package/samples/apps/styleguide/src/features/widgets/web/Widgets.tsx +24 -0
- package/samples/apps/use-all-bundled/e2e-billing-gallery/gallery-constants.ts +5 -0
- package/samples/apps/use-all-bundled/e2e-billing-gallery/gallery-features.ts +90 -15
- package/samples/apps/use-all-bundled/e2e-billing-gallery/server.ts +16 -6
- package/samples/apps/use-all-bundled/feature-manifest.json +120 -3
- package/samples/apps/use-all-bundled/src/run-config.ts +9 -3
- package/samples/recipes/auth-mfa/README.md +5 -0
- package/samples/recipes/auth-mfa/package.json +5 -2
- package/samples/recipes/auth-mfa/src/web/confirmation-code-step.tsx +49 -0
- package/samples/recipes/auth-mfa/tsconfig.json +8 -0
- package/samples/recipes/env-composition/README.md +3 -1
- package/samples/recipes/env-composition/src/env.ts +2 -0
- package/samples/recipes/legal-pages/README.md +1 -1
- package/samples/recipes/rate-limiting/README.md +18 -0
- package/samples/recipes/rate-limiting/src/feature.ts +12 -0
- package/packages/bundled-features/src/billing-foundation/consumer-protection/stored-user-email.ts +0 -12
|
@@ -0,0 +1,262 @@
|
|
|
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
|
+
|
|
8
|
+
import { createHmac, timingSafeEqual } from "node:crypto";
|
|
9
|
+
import {
|
|
10
|
+
BillingEventKinds,
|
|
11
|
+
type PaymentEvent,
|
|
12
|
+
parseProviderConsentId,
|
|
13
|
+
type SubscriptionEvent,
|
|
14
|
+
type SubscriptionEventType,
|
|
15
|
+
SubscriptionEventTypes,
|
|
16
|
+
type SubscriptionStatus,
|
|
17
|
+
SubscriptionStatuses,
|
|
18
|
+
} from "@cosmicdrift/kumiko-bundled-features/billing-foundation";
|
|
19
|
+
import type { SecretsContext } from "@cosmicdrift/kumiko-framework/secrets";
|
|
20
|
+
import { parseJsonOrThrow } from "@cosmicdrift/kumiko-framework/utils";
|
|
21
|
+
import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
|
|
22
|
+
import * as z from "zod";
|
|
23
|
+
import { DODO_PROVIDER_NAME } from "./constants.ts";
|
|
24
|
+
import type { DodoWebhookRuntime } from "./runtime.ts";
|
|
25
|
+
|
|
26
|
+
export const WEBHOOK_TOLERANCE_SECONDS = 300;
|
|
27
|
+
|
|
28
|
+
const SECRET_PREFIX = "whsec_";
|
|
29
|
+
const SIGNATURE_VERSION = "v1";
|
|
30
|
+
const TIMESTAMP_PATTERN = /^\d+$/;
|
|
31
|
+
|
|
32
|
+
export type StandardWebhookHeaders = Readonly<Record<string, string>>;
|
|
33
|
+
|
|
34
|
+
function signatureFor(secretKey: Buffer, signedContent: string): Buffer {
|
|
35
|
+
return createHmac("sha256", secretKey).update(signedContent).digest();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function matchesAnyV1Signature(header: string, expected: Buffer): boolean {
|
|
39
|
+
return header.split(" ").some((entry) => {
|
|
40
|
+
const [version, encoded] = entry.split(",");
|
|
41
|
+
if (version !== SIGNATURE_VERSION || !encoded) return false;
|
|
42
|
+
const candidate = Buffer.from(encoded, "base64");
|
|
43
|
+
return candidate.length === expected.length && timingSafeEqual(candidate, expected);
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Throws unless the Standard Webhooks signature over `rawBody` is valid and
|
|
48
|
+
* the timestamp is within the tolerance in both directions. */
|
|
49
|
+
export function verifyStandardWebhook(
|
|
50
|
+
rawBody: string,
|
|
51
|
+
headers: StandardWebhookHeaders,
|
|
52
|
+
secret: string,
|
|
53
|
+
nowSeconds: number,
|
|
54
|
+
): void {
|
|
55
|
+
const id = headers["webhook-id"];
|
|
56
|
+
const timestamp = headers["webhook-timestamp"];
|
|
57
|
+
const signatureHeader = headers["webhook-signature"];
|
|
58
|
+
if (!id || !timestamp || !signatureHeader) {
|
|
59
|
+
throw new Error(
|
|
60
|
+
"subscription-dodo: webhook-id, webhook-timestamp or webhook-signature missing",
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
if (!TIMESTAMP_PATTERN.test(timestamp)) {
|
|
64
|
+
throw new Error("subscription-dodo: webhook-timestamp is not a Unix timestamp in seconds");
|
|
65
|
+
}
|
|
66
|
+
if (Math.abs(nowSeconds - Number(timestamp)) > WEBHOOK_TOLERANCE_SECONDS) {
|
|
67
|
+
throw new Error("subscription-dodo: webhook timestamp outside the tolerance window");
|
|
68
|
+
}
|
|
69
|
+
const secretKey = Buffer.from(
|
|
70
|
+
secret.startsWith(SECRET_PREFIX) ? secret.slice(SECRET_PREFIX.length) : secret,
|
|
71
|
+
"base64",
|
|
72
|
+
);
|
|
73
|
+
const expected = signatureFor(secretKey, `${id}.${timestamp}.${rawBody}`);
|
|
74
|
+
if (!matchesAnyV1Signature(signatureHeader, expected)) {
|
|
75
|
+
throw new Error("subscription-dodo: webhook signature verify failed");
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// =============================================================================
|
|
80
|
+
// Event mapping
|
|
81
|
+
// =============================================================================
|
|
82
|
+
|
|
83
|
+
const DODO_SUBSCRIPTION_EVENT_TYPES: Readonly<Record<string, SubscriptionEventType>> = {
|
|
84
|
+
"subscription.active": SubscriptionEventTypes.created,
|
|
85
|
+
"subscription.updated": SubscriptionEventTypes.updated,
|
|
86
|
+
"subscription.plan_changed": SubscriptionEventTypes.updated,
|
|
87
|
+
"subscription.paused": SubscriptionEventTypes.updated,
|
|
88
|
+
"subscription.unpaused": SubscriptionEventTypes.updated,
|
|
89
|
+
"subscription.update_payment_method": SubscriptionEventTypes.updated,
|
|
90
|
+
"subscription.renewed": SubscriptionEventTypes.invoicePaid,
|
|
91
|
+
"subscription.past_due": SubscriptionEventTypes.invoicePaymentFailed,
|
|
92
|
+
"subscription.on_hold": SubscriptionEventTypes.invoicePaymentFailed,
|
|
93
|
+
"subscription.cancelled": SubscriptionEventTypes.canceled,
|
|
94
|
+
"subscription.expired": SubscriptionEventTypes.canceled,
|
|
95
|
+
"subscription.failed": SubscriptionEventTypes.canceled,
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const DODO_SUBSCRIPTION_STATUSES: Readonly<Record<string, SubscriptionStatus>> = {
|
|
99
|
+
pending: SubscriptionStatuses.incomplete,
|
|
100
|
+
active: SubscriptionStatuses.active,
|
|
101
|
+
on_hold: SubscriptionStatuses.pastDue,
|
|
102
|
+
past_due: SubscriptionStatuses.pastDue,
|
|
103
|
+
paused: SubscriptionStatuses.pastDue,
|
|
104
|
+
cancelled: SubscriptionStatuses.canceled,
|
|
105
|
+
failed: SubscriptionStatuses.canceled,
|
|
106
|
+
expired: SubscriptionStatuses.canceled,
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const PAYMENT_SUCCEEDED = "payment.succeeded";
|
|
110
|
+
|
|
111
|
+
const metadataSchema = z.record(z.string(), z.unknown()).nullish();
|
|
112
|
+
const customerSchema = z.object({ customer_id: z.string().min(1) });
|
|
113
|
+
|
|
114
|
+
const envelopeSchema = z.object({
|
|
115
|
+
type: z.string(),
|
|
116
|
+
timestamp: z.iso.datetime({ offset: true }),
|
|
117
|
+
data: z.unknown(),
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
export const dodoSubscriptionSchema = z.object({
|
|
121
|
+
subscription_id: z.string().min(1),
|
|
122
|
+
product_id: z.string().min(1),
|
|
123
|
+
status: z.string(),
|
|
124
|
+
customer: customerSchema,
|
|
125
|
+
metadata: metadataSchema,
|
|
126
|
+
next_billing_date: z.iso.datetime({ offset: true }),
|
|
127
|
+
cancel_at_next_billing_date: z.boolean().nullish(),
|
|
128
|
+
});
|
|
129
|
+
export type DodoSubscription = z.infer<typeof dodoSubscriptionSchema>;
|
|
130
|
+
|
|
131
|
+
const dodoPaymentSchema = z.object({
|
|
132
|
+
subscription_id: z.string().nullish(),
|
|
133
|
+
checkout_session_id: z.string().nullish(),
|
|
134
|
+
customer: customerSchema,
|
|
135
|
+
metadata: metadataSchema,
|
|
136
|
+
product_cart: z.array(z.object({ product_id: z.string().min(1) })).nullish(),
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
function metadataString(metadata: DodoSubscription["metadata"], key: string): string | undefined {
|
|
140
|
+
const value = metadata?.[key];
|
|
141
|
+
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export type MappedDodoSubscriptionState = {
|
|
145
|
+
readonly status: SubscriptionStatus;
|
|
146
|
+
readonly tier: string;
|
|
147
|
+
readonly currentPeriodEnd: string;
|
|
148
|
+
readonly cancelAt: string | null;
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
/** Shared by the webhook and retrieveSubscription so both map Dodo state
|
|
152
|
+
* identically. Null when the status is unknown or the product is not in
|
|
153
|
+
* `priceToTier` (SubscriptionEvent.tier is required). */
|
|
154
|
+
export function mapDodoSubscriptionState(
|
|
155
|
+
sub: DodoSubscription,
|
|
156
|
+
priceToTier: Readonly<Record<string, string>>,
|
|
157
|
+
): MappedDodoSubscriptionState | null {
|
|
158
|
+
const status = DODO_SUBSCRIPTION_STATUSES[sub.status];
|
|
159
|
+
const tier = priceToTier[sub.product_id];
|
|
160
|
+
if (!status || !tier) return null;
|
|
161
|
+
return {
|
|
162
|
+
status,
|
|
163
|
+
tier,
|
|
164
|
+
currentPeriodEnd: sub.next_billing_date,
|
|
165
|
+
cancelAt: sub.cancel_at_next_billing_date === true ? sub.next_billing_date : null,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export type DodoWebhookOptions = {
|
|
170
|
+
readonly priceToTier: Readonly<Record<string, string>>;
|
|
171
|
+
/** Test seam: Unix seconds. Defaults to the wall clock. */
|
|
172
|
+
readonly nowSeconds?: () => number;
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
function currentUnixSeconds(): number {
|
|
176
|
+
return Math.floor(Temporal.Now.instant().epochMilliseconds / 1000);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function parsePaymentEvent(providerEventId: string, data: unknown): PaymentEvent | null {
|
|
180
|
+
const parsed = dodoPaymentSchema.safeParse(data);
|
|
181
|
+
if (!parsed.success) return null;
|
|
182
|
+
const payment = parsed.data;
|
|
183
|
+
// A subscription's own payments are covered by its subscription.* events.
|
|
184
|
+
if (payment.subscription_id) return null;
|
|
185
|
+
const tenantId = metadataString(payment.metadata, "tenantId");
|
|
186
|
+
const priceId = payment.product_cart?.[0]?.product_id;
|
|
187
|
+
if (!tenantId || !priceId) return null;
|
|
188
|
+
const consentId = parseProviderConsentId(payment.metadata?.["consentId"]);
|
|
189
|
+
return {
|
|
190
|
+
kind: BillingEventKinds.payment,
|
|
191
|
+
providerEventId,
|
|
192
|
+
providerName: DODO_PROVIDER_NAME,
|
|
193
|
+
tenantId,
|
|
194
|
+
...(consentId !== undefined && { consentId }),
|
|
195
|
+
...(payment.checkout_session_id && { providerCheckoutId: payment.checkout_session_id }),
|
|
196
|
+
providerCustomerId: payment.customer.customer_id,
|
|
197
|
+
priceId,
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function parseSubscriptionEvent(
|
|
202
|
+
providerEventId: string,
|
|
203
|
+
type: SubscriptionEventType,
|
|
204
|
+
providerEventAt: string,
|
|
205
|
+
data: unknown,
|
|
206
|
+
priceToTier: Readonly<Record<string, string>>,
|
|
207
|
+
): SubscriptionEvent | null {
|
|
208
|
+
const parsed = dodoSubscriptionSchema.safeParse(data);
|
|
209
|
+
if (!parsed.success) return null;
|
|
210
|
+
const sub = parsed.data;
|
|
211
|
+
const tenantId = metadataString(sub.metadata, "tenantId");
|
|
212
|
+
const state = mapDodoSubscriptionState(sub, priceToTier);
|
|
213
|
+
if (!tenantId || !state) return null;
|
|
214
|
+
const consentId = parseProviderConsentId(sub.metadata?.["consentId"]);
|
|
215
|
+
return {
|
|
216
|
+
providerEventId,
|
|
217
|
+
providerName: DODO_PROVIDER_NAME,
|
|
218
|
+
type,
|
|
219
|
+
tenantId,
|
|
220
|
+
...(consentId !== undefined && { consentId }),
|
|
221
|
+
providerCustomerId: sub.customer.customer_id,
|
|
222
|
+
providerSubscriptionId: sub.subscription_id,
|
|
223
|
+
providerEventAt,
|
|
224
|
+
...state,
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export function verifyAndParseDodoWebhook(
|
|
229
|
+
runtime: DodoWebhookRuntime,
|
|
230
|
+
options: DodoWebhookOptions,
|
|
231
|
+
): (
|
|
232
|
+
rawBody: string,
|
|
233
|
+
headers: Record<string, string>,
|
|
234
|
+
systemSecrets?: SecretsContext,
|
|
235
|
+
) => Promise<SubscriptionEvent | PaymentEvent | null> {
|
|
236
|
+
const nowSeconds = options.nowSeconds ?? currentUnixSeconds;
|
|
237
|
+
return async (rawBody, headers, systemSecrets) => {
|
|
238
|
+
const secret = await runtime.resolveWebhookSecret(systemSecrets);
|
|
239
|
+
verifyStandardWebhook(rawBody, headers, secret, nowSeconds());
|
|
240
|
+
|
|
241
|
+
// Past verification the body is trusted to be Dodo's; a malformed shape is
|
|
242
|
+
// schema drift, so it is dropped (200 ignored) instead of answered with a 401.
|
|
243
|
+
const envelope = envelopeSchema.safeParse(
|
|
244
|
+
parseJsonOrThrow<unknown>(rawBody, "subscription-dodo webhook body"),
|
|
245
|
+
);
|
|
246
|
+
// verifyStandardWebhook has checked webhook-id is present.
|
|
247
|
+
const providerEventId = headers["webhook-id"];
|
|
248
|
+
if (!envelope.success || !providerEventId) return null;
|
|
249
|
+
|
|
250
|
+
const { type, timestamp, data } = envelope.data;
|
|
251
|
+
if (type === PAYMENT_SUCCEEDED) return parsePaymentEvent(providerEventId, data);
|
|
252
|
+
const normalizedType = DODO_SUBSCRIPTION_EVENT_TYPES[type];
|
|
253
|
+
if (!normalizedType) return null;
|
|
254
|
+
return parseSubscriptionEvent(
|
|
255
|
+
providerEventId,
|
|
256
|
+
normalizedType,
|
|
257
|
+
timestamp,
|
|
258
|
+
data,
|
|
259
|
+
options.priceToTier,
|
|
260
|
+
);
|
|
261
|
+
};
|
|
262
|
+
}
|
|
@@ -1,4 +1,16 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.388.0",
|
|
4
|
+
"type": "improvement",
|
|
5
|
+
"title": "Stripe plugin has a displayName",
|
|
6
|
+
"detail": "The Stripe plugin sets displayName \"Stripe\", which the provider choice in the billing panel shows."
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
"version": "0.388.0",
|
|
10
|
+
"type": "fix",
|
|
11
|
+
"title": "A malformed stored credential no longer leaks into the error",
|
|
12
|
+
"detail": "A stored api-key or webhook-secret that is not valid JSON used to raise the JSON parser's own message, which repeats the value into logs and the webhook 401 body. The error now has a fixed text and names no input. subscription-dodo behaves the same."
|
|
13
|
+
},
|
|
2
14
|
{
|
|
3
15
|
"version": "0.370.0",
|
|
4
16
|
"type": "improvement",
|
|
@@ -250,6 +250,7 @@ export function createSubscriptionStripeFeature(
|
|
|
250
250
|
const portalConfigurationCache = new Map<string, string>();
|
|
251
251
|
|
|
252
252
|
const plugin: SubscriptionProviderPlugin = {
|
|
253
|
+
displayName: "Stripe",
|
|
253
254
|
verifyAndParseWebhook: verifyAndParseStripeWebhook(runtimes.webhook, {
|
|
254
255
|
priceToTier: options.priceToTier ?? {},
|
|
255
256
|
}),
|
|
@@ -33,7 +33,7 @@ import {
|
|
|
33
33
|
} from "@cosmicdrift/kumiko-framework/engine";
|
|
34
34
|
import { FeatureDisabledError, UnconfiguredError } from "@cosmicdrift/kumiko-framework/errors";
|
|
35
35
|
import type { SecretsContext } from "@cosmicdrift/kumiko-framework/secrets";
|
|
36
|
-
import {
|
|
36
|
+
import { parseJsonSafe } from "@cosmicdrift/kumiko-framework/utils";
|
|
37
37
|
import Stripe from "stripe";
|
|
38
38
|
import { SUBSCRIPTION_STRIPE_FEATURE } from "./constants.ts";
|
|
39
39
|
|
|
@@ -68,13 +68,14 @@ export type StripeRuntimeDeps = {
|
|
|
68
68
|
// (the un-audited webhook path + the audited ctx read here) therefore returns
|
|
69
69
|
// the JSON-quoted string and must parse it back. The factory fallback is a
|
|
70
70
|
// plain env string and stays raw — so parsing is applied only to the store
|
|
71
|
-
// read, never to the fallback.
|
|
71
|
+
// read, never to the fallback. The error must not carry the input: the JSON
|
|
72
|
+
// parser's own message would repeat the credential into logs and the 401 body.
|
|
72
73
|
function parseStoredSecret(raw: string | undefined): string | undefined {
|
|
73
74
|
if (raw === undefined || raw.length === 0) return undefined;
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
75
|
+
const parsed = parseJsonSafe<unknown>(raw, undefined);
|
|
76
|
+
if (parsed === undefined) {
|
|
77
|
+
throw new Error("subscription-stripe credential (backing:secrets) is not valid JSON");
|
|
78
|
+
}
|
|
78
79
|
return typeof parsed === "string" && parsed.length > 0 ? parsed : undefined;
|
|
79
80
|
}
|
|
80
81
|
|
|
@@ -158,12 +158,14 @@ export function createTemplateResolverApi(
|
|
|
158
158
|
};
|
|
159
159
|
}
|
|
160
160
|
|
|
161
|
+
type DeliveredRowKey = Pick<TemplateResourceRow, "tenantId" | "kind" | "slug">;
|
|
162
|
+
|
|
161
163
|
// The one delivery rule for a stored row: unlocked, and a collection entry never shadows a system
|
|
162
164
|
// template of the same slug that the app did not unlock.
|
|
163
165
|
export async function isRowDelivered(
|
|
164
166
|
db: DbRunner | TenantDb,
|
|
165
167
|
policy: TemplateDeliveryPolicy,
|
|
166
|
-
row:
|
|
168
|
+
row: DeliveredRowKey,
|
|
167
169
|
): Promise<boolean> {
|
|
168
170
|
if (!isRowServed(policy, row)) return false;
|
|
169
171
|
if (row.tenantId === SYSTEM_TENANT_ID || !isServedByCollectionOnly(policy, row)) return true;
|
|
@@ -174,19 +176,44 @@ export async function isRowDeliveredTo(
|
|
|
174
176
|
db: DbRunner | TenantDb,
|
|
175
177
|
policy: TemplateDeliveryPolicy,
|
|
176
178
|
user: { readonly roles: readonly string[] },
|
|
177
|
-
row:
|
|
179
|
+
row: DeliveredRowKey,
|
|
178
180
|
): Promise<boolean> {
|
|
179
181
|
return isSystemAdmin(user) || (await isRowDelivered(db, policy, row));
|
|
180
182
|
}
|
|
181
183
|
|
|
184
|
+
// One system-slug lookup per kind instead of one query per row, because by-tenant is anonymous.
|
|
182
185
|
export async function filterDeliveredTo(
|
|
183
186
|
db: DbRunner | TenantDb,
|
|
184
187
|
policy: TemplateDeliveryPolicy,
|
|
185
188
|
user: { readonly roles: readonly string[] },
|
|
186
189
|
rows: readonly TemplateResourceRow[],
|
|
187
190
|
): Promise<TemplateResourceRow[]> {
|
|
188
|
-
|
|
189
|
-
|
|
191
|
+
if (isSystemAdmin(user)) return [...rows];
|
|
192
|
+
const served = rows.filter((row) => isRowServed(policy, row));
|
|
193
|
+
const shadowable = served.filter(
|
|
194
|
+
(row) => row.tenantId !== SYSTEM_TENANT_ID && isServedByCollectionOnly(policy, row),
|
|
195
|
+
);
|
|
196
|
+
const slugsByKind = new Map<string, Set<string>>();
|
|
197
|
+
for (const row of shadowable) {
|
|
198
|
+
const slugs = slugsByKind.get(row.kind) ?? new Set<string>();
|
|
199
|
+
slugs.add(row.slug);
|
|
200
|
+
slugsByKind.set(row.kind, slugs);
|
|
201
|
+
}
|
|
202
|
+
const systemKeys = new Set<string>();
|
|
203
|
+
for (const [kind, slugs] of slugsByKind) {
|
|
204
|
+
const systemRows = await selectMany<TemplateResourceRow>(db, templateResourcesTable, {
|
|
205
|
+
tenantId: SYSTEM_TENANT_ID,
|
|
206
|
+
kind,
|
|
207
|
+
slug: [...slugs],
|
|
208
|
+
});
|
|
209
|
+
for (const systemRow of systemRows) systemKeys.add(`${kind}|${systemRow.slug}`);
|
|
210
|
+
}
|
|
211
|
+
return served.filter(
|
|
212
|
+
(row) =>
|
|
213
|
+
row.tenantId === SYSTEM_TENANT_ID ||
|
|
214
|
+
!isServedByCollectionOnly(policy, row) ||
|
|
215
|
+
!systemKeys.has(`${row.kind}|${row.slug}`),
|
|
216
|
+
);
|
|
190
217
|
}
|
|
191
218
|
|
|
192
219
|
export async function systemTemplateExists(
|
|
@@ -1,4 +1,52 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.388.0",
|
|
4
|
+
"type": "fix",
|
|
5
|
+
"title": "Template overrides, key collisions and by-tenant queries",
|
|
6
|
+
"detail": "link-pdf refuses a key that already holds an inline image (key_in_use). by-tenant no longer runs a query per row, tenant-override-counts selects only the needed columns, and tenant-overrides resolves names beyond the first 200 tenants or users through the new ids filter of the tenant and member directories. Notification preference and form-draft cleanup queries bind array parameters as Postgres array literals."
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
"version": "0.388.0",
|
|
10
|
+
"type": "fix",
|
|
11
|
+
"title": "Re-seeding keeps archived system templates, drops stale drafts and adds only missing bundle locales",
|
|
12
|
+
"detail": "A boot-time re-seed no longer reactivates an archived system template and clears an open system draft when the seeded content changes. A bundle with resources seeded with ifExists skip imports only the missing locales instead of overwriting existing system rows. copy-locale fails with version_conflict when its source changed between the upload and the transaction, and releases the uploaded files."
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"version": "0.388.0",
|
|
16
|
+
"type": "fix",
|
|
17
|
+
"title": "isCollectionRefusedKind is a type predicate",
|
|
18
|
+
"detail": "isCollectionRefusedKind accepts unknown and narrows to the refused kinds; tier-engine's isTierAssignmentSource shares the isOneOf implementation. Behavior is unchanged."
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"version": "0.388.0",
|
|
22
|
+
"type": "fix",
|
|
23
|
+
"title": "Layout usage list matches the overview",
|
|
24
|
+
"detail": "The usage query of a mail layout filters the mail rows by the caller's visibility like the overview and logs a warning when the 5000-row limit cuts the result."
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"version": "0.388.0",
|
|
28
|
+
"type": "fix",
|
|
29
|
+
"title": "Admin views follow the delivery rule of rendering",
|
|
30
|
+
"detail": "List, find-by-id, diff-to-system, validate, preview and the document base hide a collection row that a locked system template of the same slug shadows, as mail, PDF and the resolver already do."
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"version": "0.388.0",
|
|
34
|
+
"type": "fix",
|
|
35
|
+
"title": "Seeded title key must look like a namespace:key",
|
|
36
|
+
"detail": "seedSystemTemplate rejects a titleI18nKey without a namespace prefix or with whitespace, so free text cannot pass as a key and show up raw in the UI."
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
"version": "0.388.0",
|
|
40
|
+
"type": "fix",
|
|
41
|
+
"title": "Failing release keeps the upload error",
|
|
42
|
+
"detail": "When releasing already uploaded files fails after a refused upload in copy-locale or bundle import, the release error is logged and the original error is returned, and the releases run once."
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
"version": "0.388.0",
|
|
46
|
+
"type": "fix",
|
|
47
|
+
"title": "Mail layout enums and hex color pattern are exported client-side",
|
|
48
|
+
"detail": "The layout editor imports the width, font, accent line and button style lists plus the hex pattern from the template-resolver web barrel instead of keeping copies, so editor and server validation cannot drift."
|
|
49
|
+
},
|
|
2
50
|
{
|
|
3
51
|
"version": "0.384.0",
|
|
4
52
|
"type": "improvement",
|
|
@@ -432,6 +432,7 @@ export function compileDocumentBodyToHandlebars(
|
|
|
432
432
|
for (const element of Array.from(root.querySelectorAll("div[data-images]"))) {
|
|
433
433
|
rewriteImagesBlock(element, state, options);
|
|
434
434
|
}
|
|
435
|
+
// Order matters: the conditional wraps first so the row's #each lands inside the #if (evaluated once per list).
|
|
435
436
|
for (const block of Array.from(root.querySelectorAll(`[${DOCUMENT_CONDITION_ATTRIBUTE}]`))) {
|
|
436
437
|
rewriteConditionalBlock(block, state);
|
|
437
438
|
}
|
|
@@ -41,7 +41,7 @@ import {
|
|
|
41
41
|
} from "./handlers/toggle-status.write.ts";
|
|
42
42
|
import { upsertSystemWrite } from "./handlers/upsert-system.write.ts";
|
|
43
43
|
import { makeUpsertTenantWrite } from "./handlers/upsert-tenant.write.ts";
|
|
44
|
-
import {
|
|
44
|
+
import { makeUsageQuery } from "./handlers/usage.query.ts";
|
|
45
45
|
import { validateQuery } from "./handlers/validate.query.ts";
|
|
46
46
|
import { TEMPLATE_RESOLVER_I18N } from "./i18n.ts";
|
|
47
47
|
import { templateResourceEntity } from "./table.ts";
|
|
@@ -181,7 +181,7 @@ export function createTemplateResolverFeature(opts: TemplateResolverOptions = {}
|
|
|
181
181
|
list: r.queryHandler(listQuery),
|
|
182
182
|
bySlug: r.queryHandler(bySlugQuery),
|
|
183
183
|
byTenant: r.queryHandler(byTenantQuery),
|
|
184
|
-
usage: r.queryHandler(
|
|
184
|
+
usage: r.queryHandler(makeUsageQuery(policy)),
|
|
185
185
|
overview: r.queryHandler(makeOverviewQuery(policy)),
|
|
186
186
|
layoutSource: r.queryHandler(layoutSourceQuery),
|
|
187
187
|
tenantOverrides: r.queryHandler(tenantOverridesQuery),
|
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
prepareTemplateResourceBeforeTransaction,
|
|
24
24
|
type Release,
|
|
25
25
|
readTemplateResourceMaxBytes,
|
|
26
|
+
releaseAfterFailure,
|
|
26
27
|
releaseAll,
|
|
27
28
|
} from "../store-template-resource.ts";
|
|
28
29
|
import { type TemplateResourceRow, templateResourcesTable } from "../table.ts";
|
|
@@ -152,14 +153,14 @@ export async function prepareTemplateBundleBeforeTransaction(
|
|
|
152
153
|
})
|
|
153
154
|
: await prepareTemplateResourceBeforeTransaction(common);
|
|
154
155
|
if (!result.isSuccess) {
|
|
155
|
-
await
|
|
156
|
+
await releaseAfterFailure(args.ctx, releases);
|
|
156
157
|
return result;
|
|
157
158
|
}
|
|
158
159
|
if (result.release) releases.push(result.release);
|
|
159
160
|
prepared.set(key, result.data);
|
|
160
161
|
}
|
|
161
162
|
} catch (cause) {
|
|
162
|
-
await
|
|
163
|
+
await releaseAfterFailure(args.ctx, releases);
|
|
163
164
|
throw cause;
|
|
164
165
|
}
|
|
165
166
|
return { isSuccess: true, data: prepared, release: () => releaseAll(releases) };
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
AccessDeniedError,
|
|
12
12
|
NotFoundError,
|
|
13
13
|
ValidationError,
|
|
14
|
+
VersionConflictError,
|
|
14
15
|
writeFailure,
|
|
15
16
|
} from "@cosmicdrift/kumiko-framework/errors";
|
|
16
17
|
import { isOneOf } from "@cosmicdrift/kumiko-types/type-guards";
|
|
@@ -172,13 +173,17 @@ export function makeCopyLocaleWrite(policy: TemplateEditPolicy) {
|
|
|
172
173
|
beforeTransaction: async (event, ctx) => {
|
|
173
174
|
const request = await checkCopyRequest(ctx, policy, event);
|
|
174
175
|
if (!request.isSuccess) return request;
|
|
175
|
-
|
|
176
|
-
|
|
176
|
+
const { source, tenantId, sourceIsForeign, sourceResources } = request.data;
|
|
177
|
+
const sourceStamp = { sourceId: source.id, sourceVersion: source.version };
|
|
178
|
+
if (!sourceIsForeign) return { isSuccess: true, data: { ...sourceStamp, copies: [] } };
|
|
179
|
+
const prepared = await prepareLinkedResourceCopiesBeforeTransaction({
|
|
177
180
|
ctx,
|
|
178
181
|
user: event.user,
|
|
179
|
-
tenantId
|
|
180
|
-
resources:
|
|
182
|
+
tenantId,
|
|
183
|
+
resources: sourceResources,
|
|
181
184
|
});
|
|
185
|
+
if (!prepared.isSuccess) return prepared;
|
|
186
|
+
return { ...prepared, data: { ...sourceStamp, copies: prepared.data } };
|
|
182
187
|
},
|
|
183
188
|
handler: async (event, ctx) => {
|
|
184
189
|
const { slug, kind, toLocale } = event.payload;
|
|
@@ -186,6 +191,20 @@ export function makeCopyLocaleWrite(policy: TemplateEditPolicy) {
|
|
|
186
191
|
if (!request.isSuccess) return request;
|
|
187
192
|
const { scope, tenantId, source, contentFormat, sourceResources, sourceIsForeign } =
|
|
188
193
|
request.data;
|
|
194
|
+
// The uploads were prepared for the source as it was before the transaction; a source that
|
|
195
|
+
// changed since (own row created or deleted, edited) no longer matches them.
|
|
196
|
+
if (
|
|
197
|
+
event.prepared.sourceId !== source.id ||
|
|
198
|
+
event.prepared.sourceVersion !== source.version
|
|
199
|
+
) {
|
|
200
|
+
return writeFailure(
|
|
201
|
+
new VersionConflictError({
|
|
202
|
+
entityId: source.id,
|
|
203
|
+
expectedVersion: event.prepared.sourceVersion,
|
|
204
|
+
currentVersion: source.version,
|
|
205
|
+
}),
|
|
206
|
+
);
|
|
207
|
+
}
|
|
189
208
|
|
|
190
209
|
const variableSchema = storedRecord(source.variableSchema);
|
|
191
210
|
const written = await writeTemplateRow({
|
|
@@ -209,7 +228,10 @@ export function makeCopyLocaleWrite(policy: TemplateEditPolicy) {
|
|
|
209
228
|
});
|
|
210
229
|
if (!written.isSuccess) return written;
|
|
211
230
|
const resources = sourceIsForeign
|
|
212
|
-
? await createLinkedResourceCopies(
|
|
231
|
+
? await createLinkedResourceCopies(
|
|
232
|
+
{ ctx, user: event.user, tenantId },
|
|
233
|
+
event.prepared.copies,
|
|
234
|
+
)
|
|
213
235
|
: { isSuccess: true as const, data: sourceResources };
|
|
214
236
|
if (!resources.isSuccess) return resources;
|
|
215
237
|
const attached = await attachTemplateResources(ctx, {
|
|
@@ -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.ts";
|
|
5
6
|
import { type TemplateResourceRow, templateResourcesTable } from "../table.ts";
|
|
6
|
-
import { deliveryPolicyOf
|
|
7
|
+
import { deliveryPolicyOf } from "../template-delivery-policy.ts";
|
|
7
8
|
import { isHiddenSystemDraft } from "../template-draft.ts";
|
|
8
9
|
import { buildOverrideDiff } from "./override-diff.ts";
|
|
9
10
|
|
|
@@ -21,7 +22,7 @@ export const diffToSystemQuery = defineQueryHandler({
|
|
|
21
22
|
!tenantRow ||
|
|
22
23
|
tenantRow.tenantId !== query.user.tenantId ||
|
|
23
24
|
tenantRow.scope !== "tenant" ||
|
|
24
|
-
!
|
|
25
|
+
!(await isRowDeliveredTo(ctx.db, deliveryPolicyOf(ctx.registry), query.user, tenantRow))
|
|
25
26
|
) {
|
|
26
27
|
throw new NotFoundError("template-resource", query.payload.id);
|
|
27
28
|
}
|
|
@@ -5,8 +5,9 @@ import {
|
|
|
5
5
|
SYSTEM_TENANT_ID,
|
|
6
6
|
} from "@cosmicdrift/kumiko-framework/engine";
|
|
7
7
|
import { isPlainObject } from "@cosmicdrift/kumiko-types/type-guards";
|
|
8
|
+
import { isRowDeliveredTo } from "../api.ts";
|
|
8
9
|
import { type TemplateResourceRow, templateResourcesTable } from "../table.ts";
|
|
9
|
-
import { deliveryPolicyOf
|
|
10
|
+
import { deliveryPolicyOf } from "../template-delivery-policy.ts";
|
|
10
11
|
import { isHiddenSystemDraft, toWorkingRow } from "../template-draft.ts";
|
|
11
12
|
import { isSystemOnlyTemplateHiddenFrom } from "../template-system-only.ts";
|
|
12
13
|
import type { DocumentHtmlKind } from "../web/constants.ts";
|
|
@@ -50,7 +51,7 @@ export async function loadDocumentBase(
|
|
|
50
51
|
const ownIsVisible =
|
|
51
52
|
own !== undefined &&
|
|
52
53
|
!isSystemOnlyTemplateHiddenFrom(user, ctx.registry, own) &&
|
|
53
|
-
|
|
54
|
+
(await isRowDeliveredTo(ctx.db, deliveryPolicyOf(ctx.registry), user, own));
|
|
54
55
|
if (own !== undefined && ownIsVisible) {
|
|
55
56
|
const working = toWorkingRow(own, user.tenantId);
|
|
56
57
|
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.ts";
|
|
4
5
|
import { type TemplateResourceRow, templateResourcesTable } from "../table.ts";
|
|
5
|
-
import { deliveryPolicyOf
|
|
6
|
+
import { deliveryPolicyOf } from "../template-delivery-policy.ts";
|
|
6
7
|
import { draftViewerTenantId, isHiddenSystemDraft, toWorkingRow } from "../template-draft.ts";
|
|
7
8
|
import { isSystemOnlyTemplateHiddenFrom } from "../template-system-only.ts";
|
|
8
9
|
|
|
@@ -25,7 +26,7 @@ export const findByIdQuery = defineQueryHandler({
|
|
|
25
26
|
!stored ||
|
|
26
27
|
isHiddenSystemDraft(query.user, stored) ||
|
|
27
28
|
isSystemOnlyTemplateHiddenFrom(query.user, ctx.registry, stored) ||
|
|
28
|
-
!
|
|
29
|
+
!(await isRowDeliveredTo(ctx.db, deliveryPolicyOf(ctx.registry), query.user, stored))
|
|
29
30
|
) {
|
|
30
31
|
return null;
|
|
31
32
|
}
|