@cosmicdrift/kumiko-bundled-features 0.331.0 → 0.333.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/auth-email-password/handlers/invite-create.write.d.ts +2 -1
- package/dist/auth-email-password/handlers/invite-create.write.js +2 -2
- package/dist/cap-counter/aggregate-id.d.ts +11 -12
- package/dist/cap-counter/aggregate-id.js +11 -12
- package/dist/cap-counter/feature.js +17 -18
- package/dist/cap-overview/handlers/tenant-caps-list.query.js +3 -0
- package/dist/crypto-shredding/handlers/forget-subject.write.js +32 -16
- package/dist/subscription-stripe/plugin-methods.js +1 -4
- package/dist/subscription-stripe/verify-webhook.d.ts +2 -1
- package/dist/subscription-stripe/verify-webhook.js +29 -18
- package/dist/tenant/constants.d.ts +1 -1
- package/dist/tenant/constants.js +7 -1
- package/dist/tenant/feature.d.ts +4 -1
- package/dist/tenant/feature.js +13 -4
- package/dist/tenant/handlers/update-member-roles.write.js +2 -2
- package/dist/tenant/i18n.js +2 -0
- package/dist/tenant/index.d.ts +1 -1
- package/dist/tenant/index.js +1 -1
- package/dist/tenant/screens.d.ts +59 -3
- package/dist/tenant/screens.js +54 -36
- package/package.json +9 -9
- package/src/cap-overview/changes.json +5 -0
- package/src/crypto-shredding/changes.json +5 -0
- package/src/subscription-stripe/changes.json +5 -0
- package/src/tenant/changes.json +5 -0
|
@@ -23,7 +23,8 @@ export type InviteCreateOptions = {
|
|
|
23
23
|
readonly locale?: AuthMailLocale;
|
|
24
24
|
readonly canAssignRole?: (inviterRoles: readonly string[], targetRole: string) => boolean;
|
|
25
25
|
/** Opt-in allow-list for app-defined (unranked) roles the elevation guard
|
|
26
|
-
* would otherwise reject with unassignable_membership_role (fw#2398).
|
|
26
|
+
* would otherwise reject with unassignable_membership_role (fw#2398).
|
|
27
|
+
* Rank-aware alternative: declare roles via EXT_ASSIGNABLE_ROLE. */
|
|
27
28
|
readonly additionalAssignableRoles?: readonly string[];
|
|
28
29
|
};
|
|
29
30
|
export declare function createInviteCreateHandler(opts: InviteCreateOptions): import("@cosmicdrift/kumiko-types/define-handler").WriteHandlerDefinition<"invite-create", z.ZodObject<{
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
// invite-create.
|
|
19
19
|
import { generateToken } from "@cosmicdrift/kumiko-framework/api";
|
|
20
20
|
import { createEventStoreExecutor } from "@cosmicdrift/kumiko-framework/db";
|
|
21
|
-
import { access, defineWriteHandler } from "@cosmicdrift/kumiko-framework/engine";
|
|
21
|
+
import { access, assignableAppRolesFromUsages, defineWriteHandler, EXT_ASSIGNABLE_ROLE, } from "@cosmicdrift/kumiko-framework/engine";
|
|
22
22
|
import { InternalError, writeFailure } from "@cosmicdrift/kumiko-framework/errors";
|
|
23
23
|
import { Temporal } from "temporal-polyfill";
|
|
24
24
|
import * as z from "zod";
|
|
@@ -54,7 +54,7 @@ export function createInviteCreateHandler(opts) {
|
|
|
54
54
|
if (forbiddenRole !== undefined) {
|
|
55
55
|
return writeFailure(reservedMembershipRoleError(forbiddenRole));
|
|
56
56
|
}
|
|
57
|
-
const elevationForbidden = findForbiddenRoleAssignment(event.user.roles, [event.payload.role], []);
|
|
57
|
+
const elevationForbidden = findForbiddenRoleAssignment(event.user.roles, [event.payload.role], [], assignableAppRolesFromUsages(ctx.registry.getExtensionUsages(EXT_ASSIGNABLE_ROLE)));
|
|
58
58
|
if (elevationForbidden !== undefined &&
|
|
59
59
|
!opts.additionalAssignableRoles?.includes(elevationForbidden)) {
|
|
60
60
|
return writeFailure(unassignableMembershipRoleError(elevationForbidden));
|
|
@@ -6,19 +6,18 @@
|
|
|
6
6
|
*/
|
|
7
7
|
export declare function capCounterAggregateId(tenantId: string, capName: string, periodStartIso: string): string;
|
|
8
8
|
/**
|
|
9
|
-
* Deterministic aggregate
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
9
|
+
* Deterministic aggregate id for a rolling-window counter, derived from
|
|
10
|
+
* (tenantId, capName). Exactly ONE rolling aggregate stream exists per
|
|
11
|
+
* tenant + cap; window semantics come purely from the read path
|
|
12
|
+
* (filtering on the event-store timestamp), so no period is part of the id.
|
|
13
13
|
*
|
|
14
|
-
* **
|
|
15
|
-
* `capCounterAggregateId(tenantId, capName, "1970-01-01...")
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* Rolling-Stream trifft.
|
|
14
|
+
* **Separate namespace:** does NOT collide with
|
|
15
|
+
* `capCounterAggregateId(tenantId, capName, "1970-01-01...")`. Same inputs,
|
|
16
|
+
* different uuidv5 namespace, different output UUID. This also prevents an
|
|
17
|
+
* accidental calendar increment from landing on the rolling stream.
|
|
19
18
|
*
|
|
20
|
-
* **
|
|
21
|
-
* tenantId + capName
|
|
22
|
-
*
|
|
19
|
+
* **Caller:** `CapCounterHandlers.incrementRolling` (via `ctx.write`) calls
|
|
20
|
+
* this with tenantId + capName and appends increment events to the stream.
|
|
21
|
+
* Race-free because the event store appends with an auto-incrementing version.
|
|
23
22
|
*/
|
|
24
23
|
export declare function rollingCapAggregateId(tenantId: string, capName: string): string;
|
|
@@ -21,20 +21,19 @@ export function capCounterAggregateId(tenantId, capName, periodStartIso) {
|
|
|
21
21
|
return uuidv5(`${tenantId}|${capName}|${periodStartIso}`, CAP_COUNTER_NAMESPACE);
|
|
22
22
|
}
|
|
23
23
|
/**
|
|
24
|
-
* Deterministic aggregate
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
24
|
+
* Deterministic aggregate id for a rolling-window counter, derived from
|
|
25
|
+
* (tenantId, capName). Exactly ONE rolling aggregate stream exists per
|
|
26
|
+
* tenant + cap; window semantics come purely from the read path
|
|
27
|
+
* (filtering on the event-store timestamp), so no period is part of the id.
|
|
28
28
|
*
|
|
29
|
-
* **
|
|
30
|
-
* `capCounterAggregateId(tenantId, capName, "1970-01-01...")
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
* Rolling-Stream trifft.
|
|
29
|
+
* **Separate namespace:** does NOT collide with
|
|
30
|
+
* `capCounterAggregateId(tenantId, capName, "1970-01-01...")`. Same inputs,
|
|
31
|
+
* different uuidv5 namespace, different output UUID. This also prevents an
|
|
32
|
+
* accidental calendar increment from landing on the rolling stream.
|
|
34
33
|
*
|
|
35
|
-
* **
|
|
36
|
-
* tenantId + capName
|
|
37
|
-
*
|
|
34
|
+
* **Caller:** `CapCounterHandlers.incrementRolling` (via `ctx.write`) calls
|
|
35
|
+
* this with tenantId + capName and appends increment events to the stream.
|
|
36
|
+
* Race-free because the event store appends with an auto-incrementing version.
|
|
38
37
|
*/
|
|
39
38
|
// @wrapper-known uuid-domain
|
|
40
39
|
export function rollingCapAggregateId(tenantId, capName) {
|
|
@@ -17,25 +17,24 @@
|
|
|
17
17
|
// Pre-Save-Wrapper mit asymmetrischen Soft/Hard-Toleranzen pro
|
|
18
18
|
// Cap-Profile.
|
|
19
19
|
//
|
|
20
|
-
// **Calendar vs. Rolling —
|
|
21
|
-
// - **Calendar
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
// - **Rolling
|
|
25
|
-
//
|
|
26
|
-
//
|
|
27
|
-
//
|
|
20
|
+
// **Calendar vs. Rolling — when to use which:**
|
|
21
|
+
// - **Calendar period** (bookCapUsage + enforceCap): the cap resets at
|
|
22
|
+
// period start (1st of the month etc.). The counter is one row in the
|
|
23
|
+
// projection. Fast read.
|
|
24
|
+
// - **Rolling window** (CapCounterHandlers.incrementRolling + enforceRollingCap):
|
|
25
|
+
// the cap rolls continuously (e.g. "last 7 days"). Values expire
|
|
26
|
+
// event by event without a reset. No projection; the read sums the
|
|
27
|
+
// events of the last N days.
|
|
28
28
|
//
|
|
29
|
-
// **
|
|
30
|
-
// - **
|
|
31
|
-
// ai-foundation
|
|
32
|
-
//
|
|
33
|
-
//
|
|
34
|
-
// `
|
|
35
|
-
// - **
|
|
36
|
-
//
|
|
37
|
-
//
|
|
38
|
-
// bleiben für Audit liegen.
|
|
29
|
+
// **What this feature does NOT do:**
|
|
30
|
+
// - **No foundation wiring.** mail-foundation / file-foundation /
|
|
31
|
+
// ai-foundation are BYOK by default today and have no platform pool
|
|
32
|
+
// to count. cap-counter is generic: an app owner who wants to use the
|
|
33
|
+
// counter calls `bookCapUsage(...)` /
|
|
34
|
+
// `ctx.write(CapCounterHandlers.incrementRolling, ...)` from their own handler.
|
|
35
|
+
// - **No reset cron for the calendar period.** Works without one: the
|
|
36
|
+
// periodStartIso part of the aggregate id rolls over to a fresh
|
|
37
|
+
// counter at the period tick. Old rows stay for audit.
|
|
39
38
|
// - **Kein Notification-Pfad als Hard-Wiring.** Cap-counter
|
|
40
39
|
// entkoppelt — `enforceCapAndMaybeNotify` (siehe enforce-cap.ts)
|
|
41
40
|
// ist ein Convenience-Helper, der einen Caller-supplied
|
|
@@ -85,6 +85,9 @@ export function createTenantCapsListQuery(caps, listCaps) {
|
|
|
85
85
|
totalCount: z.boolean().optional(),
|
|
86
86
|
}),
|
|
87
87
|
access: { roles: ["SystemAdmin"] },
|
|
88
|
+
escapeHatch: {
|
|
89
|
+
reason: "cap-overview:tenant-caps:list — grants app-owned CapSpec.usage()/usageBatch() providers raw SQL for aggregates TenantDb has no typed helper for (e.g. a SUM)",
|
|
90
|
+
},
|
|
88
91
|
handler: async (query, ctx) => {
|
|
89
92
|
if (!ctx.systemDb) {
|
|
90
93
|
throw new InternalError({
|
|
@@ -2,7 +2,7 @@ import { createHash } from "node:crypto";
|
|
|
2
2
|
import { requestContext } from "@cosmicdrift/kumiko-framework/api";
|
|
3
3
|
import { ROLES } from "@cosmicdrift/kumiko-framework/auth";
|
|
4
4
|
import { configuredPiiSubjectKms, subjectIdSchema, subjectIdToKey, } from "@cosmicdrift/kumiko-framework/crypto";
|
|
5
|
-
import { nullBlindIndexesForSubject, recordRowExistsInTenant, subjectRowExistsInTenant, } from "@cosmicdrift/kumiko-framework/db";
|
|
5
|
+
import { nullBlindIndexesForSubject, recordRowExistsInTenant, recordRowOwningTenantId, subjectRowExistsInTenant, } from "@cosmicdrift/kumiko-framework/db";
|
|
6
6
|
import { defineWriteHandler, } from "@cosmicdrift/kumiko-framework/engine";
|
|
7
7
|
import { AccessDeniedError, InternalError, writeFailure, } from "@cosmicdrift/kumiko-framework/errors";
|
|
8
8
|
import { append } from "@cosmicdrift/kumiko-framework/event-store";
|
|
@@ -10,6 +10,7 @@ import { assertIrreversibleOperationAllowed } from "@cosmicdrift/kumiko-framewor
|
|
|
10
10
|
import { purgeSearchDocumentsForSubject } from "@cosmicdrift/kumiko-framework/search";
|
|
11
11
|
import { generateId } from "@cosmicdrift/kumiko-framework/utils";
|
|
12
12
|
import * as z from "zod";
|
|
13
|
+
import { resolveRetentionPolicyForTenant } from "../../data-retention/index.js";
|
|
13
14
|
import { revokeAllPatTokensForUser } from "../../personal-access-tokens/index.js";
|
|
14
15
|
import { USER_STATUS } from "../../user/index.js";
|
|
15
16
|
import { denyIfTargetOutsideAdminTenant, isSystemAdminActor, updateUserLifecycle, } from "../../user-data-rights/index.js";
|
|
@@ -99,23 +100,38 @@ async function resolveTenantScopeDenial(db, features, user, raw) {
|
|
|
99
100
|
const ownedInTenant = await subjectRowExistsInTenant(db, features, raw.userId, user.tenantId);
|
|
100
101
|
return ownedInTenant ? undefined : memberDenied;
|
|
101
102
|
}
|
|
102
|
-
// fw#2789:
|
|
103
|
-
//
|
|
104
|
-
//
|
|
105
|
-
//
|
|
106
|
-
//
|
|
107
|
-
//
|
|
108
|
-
//
|
|
109
|
-
//
|
|
110
|
-
|
|
111
|
-
//
|
|
112
|
-
|
|
103
|
+
// fw#2789/#2805: blockDelete (legally mandated physical retention, e.g.
|
|
104
|
+
// ledger/invoice text) means this command must refuse, not silently
|
|
105
|
+
// anonymize or proceed. Refused when the host entity itself declares
|
|
106
|
+
// blockDelete OR the owning tenant's effective data-retention policy
|
|
107
|
+
// (preset/override layering) does, so a tenant preset cannot be bypassed here.
|
|
108
|
+
// The entity declaration is checked first and independent of the tenant
|
|
109
|
+
// feature/gate and of data-retention being mounted: a retention obligation
|
|
110
|
+
// holds in single-tenant apps too.
|
|
111
|
+
async function resolveRetentionDenial(ctx, raw,
|
|
112
|
+
// Lazy: unsafeRaw writes an audit entry per call, so only request it once a read is needed.
|
|
113
|
+
getRunner) {
|
|
113
114
|
if (raw.kind !== "record")
|
|
114
115
|
return undefined;
|
|
116
|
+
const features = ctx.registry.features;
|
|
117
|
+
const deny = () => writeFailure(new AccessDeniedError({ details: { reason: TARGET_RECORD_RETENTION_BLOCK_DELETE } }));
|
|
115
118
|
const entity = findRegisteredEntity(features, raw.entity);
|
|
116
|
-
if (entity?.retention?.strategy
|
|
119
|
+
if (entity?.retention?.strategy === "blockDelete")
|
|
120
|
+
return deny();
|
|
121
|
+
if (!features.has("data-retention"))
|
|
117
122
|
return undefined;
|
|
118
|
-
|
|
123
|
+
// The row's own tenant, not the actor's: a SystemAdmin acts across tenants.
|
|
124
|
+
const runner = getRunner();
|
|
125
|
+
const owningTenantId = await recordRowOwningTenantId(runner, features, raw.entity, raw.id);
|
|
126
|
+
if (owningTenantId === undefined)
|
|
127
|
+
return undefined;
|
|
128
|
+
const effective = await resolveRetentionPolicyForTenant({
|
|
129
|
+
db: runner,
|
|
130
|
+
registry: ctx.registry,
|
|
131
|
+
tenantId: owningTenantId,
|
|
132
|
+
entityName: raw.entity,
|
|
133
|
+
});
|
|
134
|
+
return effective.policy?.strategy === "blockDelete" ? deny() : undefined;
|
|
119
135
|
}
|
|
120
136
|
// Denied cross-tenant probes must still leave an audit trail (fw#2348).
|
|
121
137
|
// Appended outside the handler tx (fw#2592): the caller's
|
|
@@ -190,7 +206,7 @@ export const forgetSubjectWrite = defineWriteHandler({
|
|
|
190
206
|
agent: { expose: false, risk: "high" },
|
|
191
207
|
escapeHatch: {
|
|
192
208
|
reason: "denial audit append names the prober's own tenant stream on the outside-transaction db; " +
|
|
193
|
-
"the tenant-scope
|
|
209
|
+
"the tenant-scope and retention checks run against the subject's tenant, not necessarily the caller's; " +
|
|
194
210
|
"the blind-index sweep and search purge address the subject across tenants; the user " +
|
|
195
211
|
"lifecycle update and PAT revoke run on the SYSTEM user stream.",
|
|
196
212
|
},
|
|
@@ -218,7 +234,7 @@ export const forgetSubjectWrite = defineWriteHandler({
|
|
|
218
234
|
// Runs AFTER the tenant gate (VORHER only means "before the shred"): a
|
|
219
235
|
// retention check ahead of the tenant gate would leak a foreign
|
|
220
236
|
// entity's retention posture to a cross-tenant prober.
|
|
221
|
-
const retentionDenial = resolveRetentionDenial(ctx.
|
|
237
|
+
const retentionDenial = await resolveRetentionDenial(ctx, raw, () => ctx.db.unsafeRaw("retention check reads the record row's owning tenant, not the caller's"));
|
|
222
238
|
if (retentionDenial) {
|
|
223
239
|
// Own reason constant, not tenantScopeDenial's `.error.code` pattern:
|
|
224
240
|
// AccessDeniedError.code is the generic "access_denied" for every
|
|
@@ -17,7 +17,7 @@ import { createHash } from "node:crypto";
|
|
|
17
17
|
import { KNOWN_RECURRING_INTERVALS, } from "@cosmicdrift/kumiko-bundled-features/billing-foundation";
|
|
18
18
|
import { ConflictError, UnprocessableError } from "@cosmicdrift/kumiko-framework/errors";
|
|
19
19
|
import Stripe from "stripe";
|
|
20
|
-
import { mapStripeSubscriptionState } from "./verify-webhook.js";
|
|
20
|
+
import { isResourceMissingStripeError, mapStripeSubscriptionState } from "./verify-webhook.js";
|
|
21
21
|
export function createStripeCheckoutSession(runtime, { paymentInvoiceCreation = true } = {}) {
|
|
22
22
|
return async (ctx, options) => {
|
|
23
23
|
// #104-Invariante: ohne billing-live darf keine Stripe-Session
|
|
@@ -79,9 +79,6 @@ export function createStripeCancelSubscription(runtime) {
|
|
|
79
79
|
await stripe.subscriptions.cancel(providerSubscriptionId);
|
|
80
80
|
};
|
|
81
81
|
}
|
|
82
|
-
function isResourceMissingStripeError(error) {
|
|
83
|
-
return (error instanceof Stripe.errors.StripeInvalidRequestError && error.code === "resource_missing");
|
|
84
|
-
}
|
|
85
82
|
export function createStripeRetrieveSubscription(runtime, { priceToTier }) {
|
|
86
83
|
return async (ctx, providerSubscriptionId) => {
|
|
87
84
|
const stripe = await runtime.clientForCtx(ctx);
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import type { PaymentEvent, SubscriptionEvent } from "@cosmicdrift/kumiko-bundled-features/billing-foundation";
|
|
2
2
|
import { type SubscriptionEventType, type SubscriptionStatus } from "@cosmicdrift/kumiko-bundled-features/billing-foundation";
|
|
3
3
|
import type { SecretsContext } from "@cosmicdrift/kumiko-framework/secrets";
|
|
4
|
-
import
|
|
4
|
+
import Stripe from "stripe";
|
|
5
5
|
import type { StripeWebhookRuntime } from "./runtime.js";
|
|
6
|
+
export declare function isResourceMissingStripeError(error: unknown): boolean;
|
|
6
7
|
export type StripeWebhookOptions = {
|
|
7
8
|
/** Price-to-tier-Map. Plugin liest die price-id aus dem event und
|
|
8
9
|
* mapped auf tier-name. Fehlt die price-id im Mapping → null. App-
|
|
@@ -25,17 +25,25 @@
|
|
|
25
25
|
// sig-verify, kein split-brain.
|
|
26
26
|
//
|
|
27
27
|
// **Invoice-event lazy-fetch:**
|
|
28
|
-
//
|
|
29
|
-
//
|
|
30
|
-
//
|
|
31
|
-
//
|
|
32
|
-
//
|
|
33
|
-
//
|
|
34
|
-
// returnt der Plugin defensiv null — der nächste subscription-event
|
|
35
|
-
// wird den state korrekt handhaben.
|
|
28
|
+
// Invoice payloads carry only the subscription id (Stripe does not expand
|
|
29
|
+
// subscription in webhooks), so the plugin lazy-fetches via
|
|
30
|
+
// `stripe.subscriptions.retrieve(subId)` to get the full subscription for
|
|
31
|
+
// status/tier/period-end mapping. resource_missing (deleted between webhook
|
|
32
|
+
// and retrieve) returns null; any other Stripe error becomes 503 so Stripe
|
|
33
|
+
// redelivers.
|
|
36
34
|
import { BillingEventKinds, SubscriptionEventTypes, SubscriptionStatuses, } from "@cosmicdrift/kumiko-bundled-features/billing-foundation";
|
|
35
|
+
import { ExtraRouteRejection } from "@cosmicdrift/kumiko-framework/api";
|
|
36
|
+
import Stripe from "stripe";
|
|
37
37
|
import { Temporal } from "temporal-polyfill";
|
|
38
38
|
import { STRIPE_PROVIDER_NAME, StripeEventTypes } from "./constants.js";
|
|
39
|
+
export function isResourceMissingStripeError(error) {
|
|
40
|
+
return (error instanceof Stripe.errors.StripeInvalidRequestError && error.code === "resource_missing");
|
|
41
|
+
}
|
|
42
|
+
// Anything but resource_missing (network, 5xx, rate limit) is transient: a 503 makes Stripe retry,
|
|
43
|
+
// whereas a silent null would be acknowledged as 200 and the event lost for good.
|
|
44
|
+
function transientProviderRejection(error) {
|
|
45
|
+
return new ExtraRouteRejection(503, { error: { code: "subscription_provider_transient" } }, `stripe retrieve failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
46
|
+
}
|
|
39
47
|
/**
|
|
40
48
|
* Stripe-webhook-handler. Implementiert den Plugin-Contract
|
|
41
49
|
* `verifyAndParseWebhook`. **Pre-tenant-resolution** — kein
|
|
@@ -231,12 +239,13 @@ async function extractSubscriptionFromEvent(event, stripe) {
|
|
|
231
239
|
try {
|
|
232
240
|
return await stripe.subscriptions.retrieve(subId);
|
|
233
241
|
}
|
|
234
|
-
catch {
|
|
235
|
-
//
|
|
236
|
-
//
|
|
237
|
-
//
|
|
238
|
-
|
|
239
|
-
|
|
242
|
+
catch (error) {
|
|
243
|
+
// Subscription deleted between webhook and retrieve: null makes the
|
|
244
|
+
// foundation answer 200 ignored; the next subscription event carries
|
|
245
|
+
// the state.
|
|
246
|
+
if (isResourceMissingStripeError(error))
|
|
247
|
+
return null;
|
|
248
|
+
throw transientProviderRejection(error);
|
|
240
249
|
}
|
|
241
250
|
}
|
|
242
251
|
default:
|
|
@@ -263,16 +272,18 @@ async function parsePaymentEvent(event, stripe) {
|
|
|
263
272
|
// Lazy-fetch with expand: the raw webhook payload carries neither
|
|
264
273
|
// line_items (needed for priceId) nor an expanded payment_intent (needed
|
|
265
274
|
// for tenantId — see below). Same lazy-fetch pattern as
|
|
266
|
-
// extractSubscriptionFromEvent's invoice-branch above,
|
|
267
|
-
//
|
|
275
|
+
// extractSubscriptionFromEvent's invoice-branch above, null only for resource_missing
|
|
276
|
+
// (session gone between webhook + retrieve), other failures reject with 503.
|
|
268
277
|
let expanded;
|
|
269
278
|
try {
|
|
270
279
|
expanded = await stripe.checkout.sessions.retrieve(session.id, {
|
|
271
280
|
expand: ["line_items", "payment_intent"],
|
|
272
281
|
});
|
|
273
282
|
}
|
|
274
|
-
catch {
|
|
275
|
-
|
|
283
|
+
catch (error) {
|
|
284
|
+
if (isResourceMissingStripeError(error))
|
|
285
|
+
return null;
|
|
286
|
+
throw transientProviderRejection(error);
|
|
276
287
|
}
|
|
277
288
|
// Tenant-resolution: mode:"payment" checkout sessions carry tenantId on
|
|
278
289
|
// the PaymentIntent's OWN metadata (plugin-methods.ts sets it via
|
|
@@ -11,7 +11,7 @@ export declare const MEMBER_STATUS_CELL_COMPONENT: "MemberStatusCell";
|
|
|
11
11
|
export declare const MEMBER_ROLES_CELL_COMPONENT: "MemberRolesCell";
|
|
12
12
|
/** Closed allowlist for invite-role picker — never free text (escalation guard). */
|
|
13
13
|
export declare const DEFAULT_INVITE_ROLE_OPTIONS: readonly ["User", "Editor", "Admin"];
|
|
14
|
-
export declare const OWNER_INVITE_ROLE_OPTIONS: readonly ["User", "Editor", "Admin", "TenantAdmin"];
|
|
14
|
+
export declare const OWNER_INVITE_ROLE_OPTIONS: readonly ["User", "Member", "Editor", "Admin", "TenantAdmin"];
|
|
15
15
|
export declare const TenantHandlers: {
|
|
16
16
|
readonly create: "tenant:write:create";
|
|
17
17
|
readonly update: "tenant:write:update";
|
package/dist/tenant/constants.js
CHANGED
|
@@ -22,7 +22,13 @@ export const DEFAULT_INVITE_ROLE_OPTIONS = ["User", "Editor", "Admin"];
|
|
|
22
22
|
// Full assignable rank list, used by memberRolesEditScreen: that screen is
|
|
23
23
|
// prefilled with a member's *current* roles, so it must be able to represent
|
|
24
24
|
// every rank a membership can actually hold, including TenantAdmin (fw#2452).
|
|
25
|
-
export const OWNER_INVITE_ROLE_OPTIONS = [
|
|
25
|
+
export const OWNER_INVITE_ROLE_OPTIONS = [
|
|
26
|
+
"User",
|
|
27
|
+
"Member",
|
|
28
|
+
"Editor",
|
|
29
|
+
"Admin",
|
|
30
|
+
"TenantAdmin",
|
|
31
|
+
];
|
|
26
32
|
// Qualified write handler names (QN format: scope:type:name)
|
|
27
33
|
export const TenantHandlers = {
|
|
28
34
|
create: "tenant:write:create",
|
package/dist/tenant/feature.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type FeatureDefinition } from "@cosmicdrift/kumiko-framework/engine";
|
|
1
|
+
import { type AssignableAppRoles, type FeatureDefinition } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
2
|
export { tenantEntity, tenantTable } from "./schema/tenant.js";
|
|
3
3
|
export type TenantFeatureOptions = {
|
|
4
4
|
/** Adds the /members "invite" drawer button + its `invite-create`
|
|
@@ -9,5 +9,8 @@ export type TenantFeatureOptions = {
|
|
|
9
9
|
* handler lookup fails). Off by default: `tenant` cannot see whether an
|
|
10
10
|
* app configured that optional auth-email-password flow. */
|
|
11
11
|
readonly inviteScreen?: boolean;
|
|
12
|
+
/** Screen options only — the elevation guard always reads the registry. */
|
|
13
|
+
readonly assignableAppRoles?: AssignableAppRoles;
|
|
12
14
|
};
|
|
15
|
+
export declare function collectAssignableAppRoles(features: readonly FeatureDefinition[]): AssignableAppRoles;
|
|
13
16
|
export declare function createTenantFeature(options?: TenantFeatureOptions): FeatureDefinition;
|
package/dist/tenant/feature.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { access, createSystemConfig, createTenantConfig, defineEntityDetailHandler, defineEntityListHandler, defineEntityUpdateHandler, defineFeature, i18nKey, } from "@cosmicdrift/kumiko-framework/engine";
|
|
1
|
+
import { access, assignableAppRolesFromUsages, createSystemConfig, createTenantConfig, defineEntityDetailHandler, defineEntityListHandler, defineEntityUpdateHandler, defineFeature, EXT_ASSIGNABLE_ROLE, i18nKey, } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
2
|
import { activeTenantIdsQuery } from "./handlers/active-tenant-ids.query.js";
|
|
3
3
|
import { addMemberWrite } from "./handlers/add-member.write.js";
|
|
4
4
|
import { cancelInvitationWrite } from "./handlers/cancel-invitation.write.js";
|
|
@@ -20,8 +20,13 @@ import { TENANT_I18N } from "./i18n.js";
|
|
|
20
20
|
import { tenantInvitationEntity } from "./invitation-table.js";
|
|
21
21
|
import { tenantMembershipEntity } from "./membership-table.js";
|
|
22
22
|
import { tenantEntity } from "./schema/tenant.js";
|
|
23
|
-
import {
|
|
23
|
+
import { createInviteCreateScreen, createMemberRolesEditScreen, createMembersScreen, tenantEditScreen, tenantListScreen, } from "./screens.js";
|
|
24
24
|
export { tenantEntity, tenantTable } from "./schema/tenant.js";
|
|
25
|
+
export function collectAssignableAppRoles(features) {
|
|
26
|
+
return assignableAppRolesFromUsages(features
|
|
27
|
+
.flatMap((feature) => feature.extensionUsages)
|
|
28
|
+
.filter((usage) => usage.extensionName === EXT_ASSIGNABLE_ROLE));
|
|
29
|
+
}
|
|
25
30
|
// --- Feature ---
|
|
26
31
|
export function createTenantFeature(options) {
|
|
27
32
|
return defineFeature("tenant", (r) => {
|
|
@@ -33,6 +38,10 @@ export function createTenantFeature(options) {
|
|
|
33
38
|
});
|
|
34
39
|
r.systemScope();
|
|
35
40
|
r.requires("config");
|
|
41
|
+
r.extendsRegistrar(EXT_ASSIGNABLE_ROLE, {});
|
|
42
|
+
r.bootCheck(({ features }) => {
|
|
43
|
+
collectAssignableAppRoles(features);
|
|
44
|
+
});
|
|
36
45
|
r.entity("tenant", tenantEntity);
|
|
37
46
|
r.entity("tenant-membership", tenantMembershipEntity);
|
|
38
47
|
r.entity("tenant-invitation", tenantInvitationEntity);
|
|
@@ -128,9 +137,9 @@ export function createTenantFeature(options) {
|
|
|
128
137
|
// §2.6), invite via a drawer-hosted actionForm, role-edit via
|
|
129
138
|
// actionForm. Screen access matches handler access.admin.
|
|
130
139
|
r.screen(createMembersScreen(options));
|
|
131
|
-
r.screen(
|
|
140
|
+
r.screen(createMemberRolesEditScreen(options?.assignableAppRoles));
|
|
132
141
|
if (options?.inviteScreen) {
|
|
133
|
-
r.screen(
|
|
142
|
+
r.screen(createInviteCreateScreen(options.assignableAppRoles));
|
|
134
143
|
}
|
|
135
144
|
r.nav({
|
|
136
145
|
id: "members",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { fetchOne } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
2
2
|
import { createEventStoreExecutor } from "@cosmicdrift/kumiko-framework/db";
|
|
3
|
-
import { access, createSystemUser, defineWriteHandler, findForbiddenRoleAssignment, withResponseData, } from "@cosmicdrift/kumiko-framework/engine";
|
|
3
|
+
import { access, assignableAppRolesFromUsages, createSystemUser, defineWriteHandler, EXT_ASSIGNABLE_ROLE, findForbiddenRoleAssignment, withResponseData, } from "@cosmicdrift/kumiko-framework/engine";
|
|
4
4
|
import { InternalError, NotFoundError, ValidationError, writeFailure, } from "@cosmicdrift/kumiko-framework/errors";
|
|
5
5
|
import { parseRoles } from "@cosmicdrift/kumiko-framework/utils";
|
|
6
6
|
import * as z from "zod";
|
|
@@ -70,7 +70,7 @@ export const updateMemberRolesWrite = defineWriteHandler({
|
|
|
70
70
|
const row = existing; // @cast-boundary generic-record
|
|
71
71
|
const currentTargetRoles = parseRoles(row["roles"]);
|
|
72
72
|
if (!event.user.roles.includes("system")) {
|
|
73
|
-
const forbiddenElevation = findForbiddenRoleAssignment(event.user.roles, event.payload.roles, currentTargetRoles);
|
|
73
|
+
const forbiddenElevation = findForbiddenRoleAssignment(event.user.roles, event.payload.roles, currentTargetRoles, assignableAppRolesFromUsages(ctx.registry.getExtensionUsages(EXT_ASSIGNABLE_ROLE)));
|
|
74
74
|
if (forbiddenElevation !== undefined) {
|
|
75
75
|
return writeFailure(unassignableMembershipRoleError(forbiddenElevation));
|
|
76
76
|
}
|
package/dist/tenant/i18n.js
CHANGED
|
@@ -24,12 +24,14 @@ export const TENANT_I18N = {
|
|
|
24
24
|
"tenant:entity:__action-form__:field:email": { en: "Email" },
|
|
25
25
|
"tenant:entity:__action-form__:field:role": { en: "Role" },
|
|
26
26
|
"tenant:entity:__action-form__:field:role:option:User": { en: "User" },
|
|
27
|
+
"tenant:entity:__action-form__:field:role:option:Member": { en: "Member" },
|
|
27
28
|
"tenant:entity:__action-form__:field:role:option:Admin": { en: "Admin" },
|
|
28
29
|
"tenant:entity:__action-form__:field:role:option:TenantAdmin": { en: "Tenant Admin" },
|
|
29
30
|
"tenant:entity:__action-form__:field:role:option:Editor": { en: "Editor" },
|
|
30
31
|
"tenant:entity:__action-form__:field:userId": { en: "User ID" },
|
|
31
32
|
"tenant:entity:__action-form__:field:roles": { en: "Roles" },
|
|
32
33
|
"tenant:entity:__action-form__:field:roles:option:User": { en: "User" },
|
|
34
|
+
"tenant:entity:__action-form__:field:roles:option:Member": { en: "Member" },
|
|
33
35
|
"tenant:entity:__action-form__:field:roles:option:Admin": { en: "Admin" },
|
|
34
36
|
"tenant:entity:__action-form__:field:roles:option:TenantAdmin": { en: "Tenant Admin" },
|
|
35
37
|
"tenant:entity:__action-form__:field:roles:option:Editor": { en: "Editor" },
|
package/dist/tenant/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { TenantCommandSchemas } from "./command-schemas.js";
|
|
2
2
|
export { TENANT_FEATURE, TenantErrors, TenantHandlers, TenantQueries } from "./constants.js";
|
|
3
|
-
export { createTenantFeature } from "./feature.js";
|
|
3
|
+
export { collectAssignableAppRoles, createTenantFeature } from "./feature.js";
|
|
4
4
|
export type { InvitationStatus } from "./invitation-table.js";
|
|
5
5
|
export { INVITATION_STATUS, INVITATION_STATUSES, tenantInvitationEntity, tenantInvitationsTable, } from "./invitation-table.js";
|
|
6
6
|
export { isTenantServingPublicContent } from "./is-tenant-serving-public-content.js";
|
package/dist/tenant/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { TenantCommandSchemas } from "./command-schemas.js";
|
|
2
2
|
export { TENANT_FEATURE, TenantErrors, TenantHandlers, TenantQueries } from "./constants.js";
|
|
3
|
-
export { createTenantFeature } from "./feature.js";
|
|
3
|
+
export { collectAssignableAppRoles, createTenantFeature } from "./feature.js";
|
|
4
4
|
export { INVITATION_STATUS, INVITATION_STATUSES, tenantInvitationEntity, tenantInvitationsTable, } from "./invitation-table.js";
|
|
5
5
|
export { isTenantServingPublicContent } from "./is-tenant-serving-public-content.js";
|
|
6
6
|
export { tenantMembershipEntity, tenantMembershipsTable } from "./membership-table.js";
|
package/dist/tenant/screens.d.ts
CHANGED
|
@@ -1,9 +1,35 @@
|
|
|
1
|
-
import { type EntityEditScreenDefinition, type EntityListScreenDefinition, type ScreenDefinition } from "@cosmicdrift/kumiko-framework/engine";
|
|
1
|
+
import { type AssignableAppRoles, type EntityEditScreenDefinition, type EntityListScreenDefinition, type ScreenDefinition } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
2
|
export declare const tenantListScreen: EntityListScreenDefinition;
|
|
3
3
|
export declare const tenantEditScreen: EntityEditScreenDefinition;
|
|
4
4
|
export declare function createMembersScreen(options?: {
|
|
5
5
|
readonly inviteScreen?: boolean;
|
|
6
6
|
}): ScreenDefinition;
|
|
7
|
+
export declare function createInviteCreateScreen(assignableAppRoles?: AssignableAppRoles): {
|
|
8
|
+
id: "invite-create";
|
|
9
|
+
type: "actionForm";
|
|
10
|
+
handler: "auth-email-password:write:invite-create";
|
|
11
|
+
fields: {
|
|
12
|
+
email: {
|
|
13
|
+
type: "text";
|
|
14
|
+
required: true;
|
|
15
|
+
format: "email";
|
|
16
|
+
};
|
|
17
|
+
role: {
|
|
18
|
+
type: "select";
|
|
19
|
+
options: string[];
|
|
20
|
+
required: true;
|
|
21
|
+
};
|
|
22
|
+
};
|
|
23
|
+
layout: {
|
|
24
|
+
sections: {
|
|
25
|
+
fields: string[];
|
|
26
|
+
}[];
|
|
27
|
+
};
|
|
28
|
+
submitLabel: string;
|
|
29
|
+
access: {
|
|
30
|
+
roles: readonly string[];
|
|
31
|
+
};
|
|
32
|
+
};
|
|
7
33
|
export declare const inviteCreateScreen: {
|
|
8
34
|
id: "invite-create";
|
|
9
35
|
type: "actionForm";
|
|
@@ -16,7 +42,7 @@ export declare const inviteCreateScreen: {
|
|
|
16
42
|
};
|
|
17
43
|
role: {
|
|
18
44
|
type: "select";
|
|
19
|
-
options:
|
|
45
|
+
options: string[];
|
|
20
46
|
required: true;
|
|
21
47
|
};
|
|
22
48
|
};
|
|
@@ -30,6 +56,36 @@ export declare const inviteCreateScreen: {
|
|
|
30
56
|
roles: readonly string[];
|
|
31
57
|
};
|
|
32
58
|
};
|
|
59
|
+
export declare function createMemberRolesEditScreen(assignableAppRoles?: AssignableAppRoles): {
|
|
60
|
+
id: "member-roles-edit";
|
|
61
|
+
type: "actionForm";
|
|
62
|
+
handler: "tenant:write:update-member-roles";
|
|
63
|
+
redirect: "members";
|
|
64
|
+
cancelTarget: "members";
|
|
65
|
+
fields: {
|
|
66
|
+
userId: {
|
|
67
|
+
type: "text";
|
|
68
|
+
required: true;
|
|
69
|
+
};
|
|
70
|
+
roles: {
|
|
71
|
+
type: "multiSelect";
|
|
72
|
+
options: string[];
|
|
73
|
+
required: true;
|
|
74
|
+
};
|
|
75
|
+
};
|
|
76
|
+
layout: {
|
|
77
|
+
sections: {
|
|
78
|
+
fields: (string | {
|
|
79
|
+
field: string;
|
|
80
|
+
readOnly: true;
|
|
81
|
+
})[];
|
|
82
|
+
}[];
|
|
83
|
+
};
|
|
84
|
+
submitLabel: string;
|
|
85
|
+
access: {
|
|
86
|
+
roles: readonly string[];
|
|
87
|
+
};
|
|
88
|
+
};
|
|
33
89
|
export declare const memberRolesEditScreen: {
|
|
34
90
|
id: "member-roles-edit";
|
|
35
91
|
type: "actionForm";
|
|
@@ -43,7 +99,7 @@ export declare const memberRolesEditScreen: {
|
|
|
43
99
|
};
|
|
44
100
|
roles: {
|
|
45
101
|
type: "multiSelect";
|
|
46
|
-
options:
|
|
102
|
+
options: string[];
|
|
47
103
|
required: true;
|
|
48
104
|
};
|
|
49
105
|
};
|
package/dist/tenant/screens.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { AuthHandlers } from "@cosmicdrift/kumiko-bundled-features/auth-email-password/constants";
|
|
2
|
-
import { access, i18nKey, } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
|
+
import { access, i18nKey, isAssignableByRole, } from "@cosmicdrift/kumiko-framework/engine";
|
|
3
3
|
import { DEFAULT_INVITE_ROLE_OPTIONS, INVITE_CREATE_SCREEN_ID, MEMBER_ROLES_CELL_COMPONENT, MEMBER_ROLES_EDIT_SCREEN_ID, MEMBER_STATUS_CELL_COMPONENT, MEMBERS_SCREEN_ID, OWNER_INVITE_ROLE_OPTIONS, TenantHandlers, TenantQueries, } from "./constants.js";
|
|
4
4
|
// Cross-tenant SystemAdmin platform view of the tenants themselves. The tenant
|
|
5
5
|
// feature runs with `r.systemScope()`, so the entityList returns every tenant.
|
|
@@ -138,38 +138,56 @@ export function createMembersScreen(options) {
|
|
|
138
138
|
// only entry point is the drawer button on /members. Only registered by
|
|
139
139
|
// feature.ts when `TenantFeatureOptions.inviteScreen` is set — see
|
|
140
140
|
// createMembersScreen's doc comment above for why.
|
|
141
|
-
export
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
141
|
+
export function createInviteCreateScreen(assignableAppRoles = new Map()) {
|
|
142
|
+
// Admin sees this screen too, so only app roles Admin may grant are offered (fw#2414).
|
|
143
|
+
const appRoleOptions = [...assignableAppRoles]
|
|
144
|
+
.filter(([, assignableFrom]) => isAssignableByRole(assignableFrom, "Admin"))
|
|
145
|
+
.map(([role]) => role);
|
|
146
|
+
return {
|
|
147
|
+
id: INVITE_CREATE_SCREEN_ID,
|
|
148
|
+
type: "actionForm",
|
|
149
|
+
handler: AuthHandlers.inviteCreate,
|
|
150
|
+
fields: {
|
|
151
|
+
email: { type: "text", required: true, format: "email" },
|
|
152
|
+
role: {
|
|
153
|
+
type: "select",
|
|
154
|
+
options: [...DEFAULT_INVITE_ROLE_OPTIONS, ...appRoleOptions],
|
|
155
|
+
required: true,
|
|
156
|
+
},
|
|
157
|
+
},
|
|
158
|
+
layout: {
|
|
159
|
+
sections: [{ fields: ["email", "role"] }],
|
|
160
|
+
},
|
|
161
|
+
submitLabel: i18nKey("tenant.members.invite.submit"),
|
|
162
|
+
access: { roles: access.admin },
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
export const inviteCreateScreen = createInviteCreateScreen();
|
|
166
|
+
export function createMemberRolesEditScreen(assignableAppRoles = new Map()) {
|
|
167
|
+
return {
|
|
168
|
+
id: MEMBER_ROLES_EDIT_SCREEN_ID,
|
|
169
|
+
type: "actionForm",
|
|
170
|
+
handler: TenantHandlers.updateMemberRoles,
|
|
171
|
+
redirect: MEMBERS_SCREEN_ID,
|
|
172
|
+
cancelTarget: MEMBERS_SCREEN_ID,
|
|
173
|
+
fields: {
|
|
174
|
+
userId: { type: "text", required: true },
|
|
175
|
+
// Prefilled with the member's current roles, so the option list must be
|
|
176
|
+
// able to represent every assignable rank — DEFAULT_INVITE_ROLE_OPTIONS
|
|
177
|
+
// omits TenantAdmin and would silently strip it on submit. Escalation
|
|
178
|
+
// still stays server-side via findForbiddenRoleAssignment (update-member-roles.write.ts).
|
|
179
|
+
roles: {
|
|
180
|
+
type: "multiSelect",
|
|
181
|
+
options: [...OWNER_INVITE_ROLE_OPTIONS, ...assignableAppRoles.keys()],
|
|
182
|
+
required: true,
|
|
183
|
+
},
|
|
184
|
+
},
|
|
185
|
+
layout: {
|
|
186
|
+
// Prefill userId from rowAction; readOnly so the operator cannot retarget.
|
|
187
|
+
sections: [{ fields: [{ field: "userId", readOnly: true }, "roles"] }],
|
|
188
|
+
},
|
|
189
|
+
submitLabel: i18nKey("tenant.members.roles.edit.submit"),
|
|
190
|
+
access: { roles: access.admin },
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
export const memberRolesEditScreen = createMemberRolesEditScreen();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-bundled-features",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.333.0",
|
|
4
4
|
"description": "Built-in features — tenant, user, auth, delivery. The stuff you'd rewrite anyway, already typed.",
|
|
5
5
|
"license": "BUSL-1.1",
|
|
6
6
|
"author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
|
|
@@ -483,12 +483,12 @@
|
|
|
483
483
|
}
|
|
484
484
|
},
|
|
485
485
|
"dependencies": {
|
|
486
|
-
"@cosmicdrift/kumiko-dispatcher-live": "0.
|
|
487
|
-
"@cosmicdrift/kumiko-framework": "0.
|
|
488
|
-
"@cosmicdrift/kumiko-headless": "0.
|
|
489
|
-
"@cosmicdrift/kumiko-renderer": "0.
|
|
490
|
-
"@cosmicdrift/kumiko-renderer-web": "0.
|
|
491
|
-
"@cosmicdrift/kumiko-types": "0.
|
|
486
|
+
"@cosmicdrift/kumiko-dispatcher-live": "0.333.0",
|
|
487
|
+
"@cosmicdrift/kumiko-framework": "0.333.0",
|
|
488
|
+
"@cosmicdrift/kumiko-headless": "0.333.0",
|
|
489
|
+
"@cosmicdrift/kumiko-renderer": "0.333.0",
|
|
490
|
+
"@cosmicdrift/kumiko-renderer-web": "0.333.0",
|
|
491
|
+
"@cosmicdrift/kumiko-types": "0.333.0",
|
|
492
492
|
"@mollie/api-client": "^4.5.0",
|
|
493
493
|
"@node-rs/argon2": "^2.0.2",
|
|
494
494
|
"@types/mailparser": "^3.4.6",
|
|
@@ -983,8 +983,8 @@
|
|
|
983
983
|
],
|
|
984
984
|
"devDependencies": {
|
|
985
985
|
"@testing-library/user-event": "^14.6.1",
|
|
986
|
-
"@cosmicdrift/kumiko-locale-de": "0.
|
|
987
|
-
"@cosmicdrift/kumiko-locale-es": "0.
|
|
986
|
+
"@cosmicdrift/kumiko-locale-de": "0.333.0",
|
|
987
|
+
"@cosmicdrift/kumiko-locale-es": "0.333.0",
|
|
988
988
|
"jsqr": "^1.4.0"
|
|
989
989
|
}
|
|
990
990
|
}
|