@cosmicdrift/kumiko-bundled-features 0.340.0 → 0.341.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/bootstrap.d.ts +74 -0
- package/dist/auth-email-password/bootstrap.js +165 -0
- package/dist/auth-email-password/constants.d.ts +2 -0
- package/dist/auth-email-password/constants.js +5 -0
- package/dist/auth-email-password/errors.d.ts +1 -0
- package/dist/auth-email-password/errors.js +6 -0
- package/dist/auth-email-password/feature.js +2 -1
- package/dist/auth-email-password/handlers/invite-accept-with-login.write.js +9 -1
- package/dist/auth-email-password/handlers/invite-accept.write.js +8 -0
- package/dist/auth-email-password/handlers/invite-create.write.d.ts +11 -0
- package/dist/auth-email-password/handlers/invite-create.write.js +134 -87
- package/dist/auth-email-password/handlers/invite-signup-complete.write.js +5 -1
- package/dist/auth-email-password/handlers/signup-request.write.js +1 -0
- package/dist/auth-email-password/handlers/token-request-handler.js +1 -0
- package/dist/auth-email-password/index.d.ts +2 -0
- package/dist/auth-email-password/index.js +1 -0
- package/dist/auth-email-password/invite-token-store.d.ts +3 -0
- package/dist/auth-email-password/invite-token-store.js +5 -0
- package/dist/auth-email-password/magic-link-mail.js +1 -0
- package/dist/auth-mfa/config.d.ts +1 -1
- package/dist/auth-mfa/config.js +8 -3
- package/dist/auth-mfa/feature.d.ts +2 -0
- package/dist/auth-mfa/feature.js +2 -1
- package/dist/auth-mfa/mfa-status-checker.d.ts +2 -0
- package/dist/auth-mfa/mfa-status-checker.js +1 -1
- package/dist/billing-foundation/consumer-protection/issue-confirmation.js +1 -0
- package/dist/billing-foundation/consumer-protection/termination-mail.js +1 -0
- package/dist/billing-foundation/consumer-protection/termination-notify.js +1 -0
- package/dist/billing-foundation/consumer-protection/termination-pages.d.ts +8 -1
- package/dist/billing-foundation/consumer-protection/termination-pages.js +59 -32
- package/dist/billing-foundation/consumer-protection/termination-request.js +2 -0
- package/dist/billing-foundation/consumer-protection/termination-texts.d.ts +1 -0
- package/dist/billing-foundation/consumer-protection/termination-texts.js +3 -1
- package/dist/channel-discord/discord-channel.d.ts +4 -0
- package/dist/channel-discord/discord-channel.js +4 -0
- package/dist/channel-discord/feature.js +7 -2
- package/dist/channel-discord/index.d.ts +1 -1
- package/dist/channel-discord/index.js +1 -1
- package/dist/channel-email/email-channel.js +1 -0
- package/dist/channel-slack/feature.js +4 -2
- package/dist/channel-slack/index.d.ts +1 -1
- package/dist/channel-slack/index.js +1 -1
- package/dist/channel-slack/slack-channel.d.ts +4 -0
- package/dist/channel-slack/slack-channel.js +4 -0
- package/dist/channel-teams/feature.js +4 -2
- package/dist/channel-teams/index.d.ts +1 -1
- package/dist/channel-teams/index.js +1 -1
- package/dist/channel-teams/teams-channel.d.ts +4 -0
- package/dist/channel-teams/teams-channel.js +4 -0
- package/dist/channel-telegram/feature.js +4 -2
- package/dist/channel-telegram/index.d.ts +1 -1
- package/dist/channel-telegram/index.js +1 -1
- package/dist/channel-telegram/telegram-channel.d.ts +8 -0
- package/dist/channel-telegram/telegram-channel.js +28 -12
- package/dist/delivery/chat-webhook-channel.d.ts +5 -1
- package/dist/delivery/chat-webhook-channel.js +10 -5
- package/dist/delivery/chat-webhook-sender.d.ts +6 -2
- package/dist/delivery/chat-webhook-sender.js +13 -8
- package/dist/delivery/delivery-service.d.ts +4 -3
- package/dist/delivery/delivery-service.js +53 -33
- package/dist/delivery/index.d.ts +2 -2
- package/dist/delivery/index.js +2 -2
- package/dist/delivery/jobs.js +3 -1
- package/dist/delivery/types.d.ts +5 -3
- package/dist/legal-pages/feature.d.ts +3 -15
- package/dist/legal-pages/feature.js +20 -50
- package/dist/legal-pages/lib/boot-check.d.ts +10 -0
- package/dist/legal-pages/lib/boot-check.js +33 -0
- package/dist/legal-pages/lib/validate-routes.d.ts +2 -0
- package/dist/legal-pages/lib/validate-routes.js +17 -0
- package/dist/page-render/index.d.ts +2 -1
- package/dist/page-render/index.js +1 -0
- package/dist/page-render/layout.d.ts +7 -0
- package/dist/page-render/layout.js +5 -0
- package/dist/page-render/trailing-slash.d.ts +2 -0
- package/dist/page-render/trailing-slash.js +15 -0
- package/dist/renderer-simple/feature.js +1 -0
- package/dist/renderer-simple/index.d.ts +1 -1
- package/dist/renderer-simple/index.js +1 -1
- package/dist/renderer-simple/simple-renderer.d.ts +12 -5
- package/dist/renderer-simple/simple-renderer.js +66 -12
- package/dist/secrets/announce-secret-change.d.ts +8 -0
- package/dist/secrets/announce-secret-change.js +18 -0
- package/dist/secrets/feature.js +1 -1
- package/dist/secrets/handlers/delete.write.js +2 -0
- package/dist/secrets/handlers/set.write.js +6 -1
- package/dist/secrets/i18n.d.ts +4 -0
- package/dist/secrets/i18n.js +4 -0
- package/dist/secrets/secrets-context.d.ts +2 -0
- package/dist/secrets/secrets-context.js +3 -0
- package/dist/secrets/write-gate.d.ts +5 -1
- package/dist/secrets/write-gate.js +24 -1
- package/dist/tenant/constants.d.ts +1 -0
- package/dist/tenant/constants.js +1 -0
- package/dist/tenant/handlers/add-member.write.js +14 -3
- package/dist/tenant/handlers/invitations.query.d.ts +8 -0
- package/dist/tenant/handlers/invitations.query.js +15 -1
- package/dist/tenant/index.d.ts +1 -0
- package/dist/tenant/index.js +1 -0
- package/dist/tenant/invitation-table.d.ts +2 -0
- package/dist/tenant/invitation-table.js +10 -1
- package/dist/tenant-lifecycle/lifecycle-gate.d.ts +1 -0
- package/dist/tenant-lifecycle/lifecycle-gate.js +2 -0
- package/dist/tenant-lifecycle/run-tenant-destroy.js +2 -2
- package/dist/tier-engine/feature.js +83 -14
- package/dist/tier-engine/handlers/set-tenant-tier.write.d.ts +3 -3
- package/dist/tier-engine/handlers/set-tenant-tier.write.js +17 -10
- package/dist/user/invited-global-roles.d.ts +14 -0
- package/dist/user/invited-global-roles.js +35 -0
- package/dist/user-data-rights/feature.js +3 -0
- package/dist/waitlist/constants.d.ts +33 -0
- package/dist/waitlist/constants.js +35 -0
- package/dist/waitlist/entity.d.ts +433 -0
- package/dist/waitlist/entity.js +64 -0
- package/dist/waitlist/feature.d.ts +3 -0
- package/dist/waitlist/feature.js +38 -0
- package/dist/waitlist/handlers/invite.write.d.ts +9 -0
- package/dist/waitlist/handlers/invite.write.js +119 -0
- package/dist/waitlist/handlers/list.query.d.ts +1 -0
- package/dist/waitlist/handlers/list.query.js +10 -0
- package/dist/waitlist/handlers/reject.write.d.ts +7 -0
- package/dist/waitlist/handlers/reject.write.js +80 -0
- package/dist/waitlist/handlers/submit.write.d.ts +13 -0
- package/dist/waitlist/handlers/submit.write.js +112 -0
- package/dist/waitlist/i18n.d.ts +6 -0
- package/dist/waitlist/i18n.js +31 -0
- package/dist/waitlist/index.d.ts +5 -0
- package/dist/waitlist/index.js +4 -0
- package/dist/waitlist/lib.d.ts +10 -0
- package/dist/waitlist/lib.js +24 -0
- package/dist/waitlist/mail.d.ts +15 -0
- package/dist/waitlist/mail.js +73 -0
- package/dist/waitlist/options.d.ts +29 -0
- package/dist/waitlist/options.js +8 -0
- package/dist/waitlist/payloads.d.ts +14 -0
- package/dist/waitlist/payloads.js +47 -0
- package/dist/waitlist/screens.d.ts +2 -0
- package/dist/waitlist/screens.js +42 -0
- package/dist/waitlist/user-data-hooks.d.ts +3 -0
- package/dist/waitlist/user-data-hooks.js +71 -0
- package/package.json +17 -9
- package/src/auth-email-password/changes.json +18 -0
- package/src/auth-mfa/changes.json +6 -0
- package/src/billing-foundation/changes.json +6 -0
- package/src/channel-telegram/changes.json +9 -0
- package/src/delivery/changes.json +7 -0
- package/src/files/changes.json +13 -1
- package/src/renderer-simple/changes.json +6 -0
- package/src/secrets/changes.json +12 -0
- package/src/tenant/changes.json +6 -0
- package/src/waitlist/changes.json +17 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { AccessDeniedError, NotFoundError, writeFailure, } from "@cosmicdrift/kumiko-framework/errors";
|
|
1
|
+
import { AccessDeniedError, NotFoundError, ValidationError, writeFailure, } from "@cosmicdrift/kumiko-framework/errors";
|
|
2
2
|
function unknownKeyFailure(key) {
|
|
3
3
|
return {
|
|
4
4
|
ok: false,
|
|
@@ -34,3 +34,26 @@ export function checkSecretKeyWrite(registry, userRoles, key) {
|
|
|
34
34
|
}
|
|
35
35
|
return { ok: true, keyDef };
|
|
36
36
|
}
|
|
37
|
+
// Carries neither the value nor the schema's issues.
|
|
38
|
+
export function invalidSecretValueError() {
|
|
39
|
+
return new ValidationError({
|
|
40
|
+
fields: [
|
|
41
|
+
{
|
|
42
|
+
path: "value",
|
|
43
|
+
code: "invalid_secret_value",
|
|
44
|
+
i18nKey: "secrets.errors.invalidValue",
|
|
45
|
+
},
|
|
46
|
+
],
|
|
47
|
+
}, { i18nKey: "secrets.errors.invalidValue" });
|
|
48
|
+
}
|
|
49
|
+
export function isSecretValueValid(schemas, key, value) {
|
|
50
|
+
const valueSchema = schemas.getSecretKey(key)?.valueSchema ?? schemas.findSecretNamespace(key)?.valueSchema;
|
|
51
|
+
return valueSchema === undefined || valueSchema.safeParse(value).success;
|
|
52
|
+
}
|
|
53
|
+
// Runs after checkSecretKeyWrite, so a caller without the write role never learns
|
|
54
|
+
// whether a value would have passed.
|
|
55
|
+
export function checkSecretValue(registry, key, value) {
|
|
56
|
+
if (isSecretValueValid(registry, key, value))
|
|
57
|
+
return undefined;
|
|
58
|
+
return writeFailure(invalidSecretValueError());
|
|
59
|
+
}
|
|
@@ -39,4 +39,5 @@ export declare const TenantErrors: {
|
|
|
39
39
|
readonly membershipAlreadyExists: "membership_already_exists";
|
|
40
40
|
readonly lastTenantAdmin: "last_tenant_admin";
|
|
41
41
|
readonly invitationSuperseded: "invitation_superseded";
|
|
42
|
+
readonly crossTenantMembershipDenied: "cross_tenant_membership_denied";
|
|
42
43
|
};
|
package/dist/tenant/constants.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { fetchOne } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
2
2
|
import { createEventStoreExecutor } from "@cosmicdrift/kumiko-framework/db";
|
|
3
|
-
import { defineWriteHandler } from "@cosmicdrift/kumiko-framework/engine";
|
|
4
|
-
import { ConflictError, InternalError, writeFailure } from "@cosmicdrift/kumiko-framework/errors";
|
|
3
|
+
import { defineWriteHandler, SYSTEM_ROLE, SYSTEM_USER_ID, } from "@cosmicdrift/kumiko-framework/engine";
|
|
4
|
+
import { AccessDeniedError, ConflictError, InternalError, writeFailure, } from "@cosmicdrift/kumiko-framework/errors";
|
|
5
5
|
import * as z from "zod";
|
|
6
6
|
import { TenantErrors } from "../constants.js";
|
|
7
7
|
import { findForbiddenMembershipRole, reservedMembershipRoleError } from "../membership-roles.js";
|
|
@@ -9,6 +9,11 @@ import { tenantMembershipEntity, tenantMembershipsTable } from "../membership-ta
|
|
|
9
9
|
const executor = createEventStoreExecutor(tenantMembershipsTable, tenantMembershipEntity, {
|
|
10
10
|
entityName: "tenant-membership",
|
|
11
11
|
});
|
|
12
|
+
// Both halves: test stacks can mint JWTs with the "system" role, but only the
|
|
13
|
+
// framework operator carries SYSTEM_USER_ID.
|
|
14
|
+
function isFrameworkSystemUser(user) {
|
|
15
|
+
return user.id === SYSTEM_USER_ID && user.roles.includes(SYSTEM_ROLE);
|
|
16
|
+
}
|
|
12
17
|
export const addMemberWrite = defineWriteHandler({
|
|
13
18
|
name: "addMember",
|
|
14
19
|
schema: z.object({
|
|
@@ -16,7 +21,7 @@ export const addMemberWrite = defineWriteHandler({
|
|
|
16
21
|
tenantId: z.string(),
|
|
17
22
|
roles: z.array(z.string()).min(1),
|
|
18
23
|
}),
|
|
19
|
-
access: { roles: ["SystemAdmin"] },
|
|
24
|
+
access: { roles: [SYSTEM_ROLE, "SystemAdmin"] },
|
|
20
25
|
description: "Grants an existing user membership in a tenant with the given roles, refusing reserved role names and a user who is already a member; use it to add someone to a workspace without going through an invitation.",
|
|
21
26
|
handler: async (event, ctx) => {
|
|
22
27
|
if (!ctx.systemDb) {
|
|
@@ -25,6 +30,12 @@ export const addMemberWrite = defineWriteHandler({
|
|
|
25
30
|
});
|
|
26
31
|
}
|
|
27
32
|
const db = ctx.systemDb.acknowledgeCrossTenant("SystemAdmin manages memberships across tenants");
|
|
33
|
+
if (!isFrameworkSystemUser(event.user) && event.payload.tenantId !== event.user.tenantId) {
|
|
34
|
+
return writeFailure(new AccessDeniedError({
|
|
35
|
+
message: "only the system context may add members to another tenant",
|
|
36
|
+
details: { reason: TenantErrors.crossTenantMembershipDenied },
|
|
37
|
+
}));
|
|
38
|
+
}
|
|
28
39
|
const forbidden = findForbiddenMembershipRole(event.payload.roles);
|
|
29
40
|
if (forbidden !== undefined)
|
|
30
41
|
return writeFailure(reservedMembershipRoleError(forbidden));
|
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
import * as z from "zod";
|
|
2
2
|
export declare const invitationsQuery: import("@cosmicdrift/kumiko-types/define-handler").QueryHandlerDefinition<"invitations", z.ZodObject<{}, z.core.$strip>, {
|
|
3
|
+
id: unknown;
|
|
4
|
+
tenantId: unknown;
|
|
3
5
|
email: unknown;
|
|
6
|
+
role: unknown;
|
|
7
|
+
status: unknown;
|
|
4
8
|
invitedBy: unknown;
|
|
9
|
+
expiresAt: unknown;
|
|
10
|
+
insertedAt: unknown;
|
|
11
|
+
version: unknown;
|
|
12
|
+
globalRoles?: unknown;
|
|
5
13
|
}[], import("@cosmicdrift/kumiko-types/event-type-map").KumikoEventTypeMap>;
|
|
@@ -4,6 +4,7 @@ import { InternalError } from "@cosmicdrift/kumiko-framework/errors";
|
|
|
4
4
|
import * as z from "zod";
|
|
5
5
|
import { decryptStoredPii, mapWithConcurrency } from "../../shared/index.js";
|
|
6
6
|
import { INVITATION_STATUS, tenantInvitationsTable } from "../invitation-table.js";
|
|
7
|
+
import { isSystemAdmin } from "./is-system-admin.js";
|
|
7
8
|
// Bounded, not Promise.all/sequential: each decrypt hits the KMS adapter's
|
|
8
9
|
// own small dedicated pool (PgKmsAdapter default max: 4). Promise.all fires
|
|
9
10
|
// one call per row unbounded and exhausts the pool once invitation counts
|
|
@@ -46,7 +47,20 @@ export const invitationsQuery = defineQueryHandler({
|
|
|
46
47
|
const decryptedInvitedBy = typeof invitedBy === "string"
|
|
47
48
|
? await decryptStoredPii(invitedBy, "invitedBy", "tenant:invitations")
|
|
48
49
|
: invitedBy;
|
|
49
|
-
|
|
50
|
+
// Explicit allowlist: the raw row also carries blind-index columns and
|
|
51
|
+
// globalRoles, which only a SystemAdmin may see.
|
|
52
|
+
return {
|
|
53
|
+
id: row["id"],
|
|
54
|
+
tenantId: row["tenantId"],
|
|
55
|
+
email: decryptedEmail,
|
|
56
|
+
role: row["role"],
|
|
57
|
+
status: row["status"],
|
|
58
|
+
invitedBy: decryptedInvitedBy,
|
|
59
|
+
expiresAt: row["expiresAt"],
|
|
60
|
+
insertedAt: row["insertedAt"],
|
|
61
|
+
version: row["version"],
|
|
62
|
+
...(isSystemAdmin(query.user) && { globalRoles: row["globalRoles"] }),
|
|
63
|
+
};
|
|
50
64
|
});
|
|
51
65
|
},
|
|
52
66
|
});
|
package/dist/tenant/index.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
export { cancelPendingInvitation } from "./cancel-pending-invitation.js";
|
|
1
2
|
export { TenantCommandSchemas } from "./command-schemas.js";
|
|
2
3
|
export { OWNER_INVITE_ROLE_OPTIONS, TENANT_FEATURE, TenantErrors, TenantHandlers, TenantQueries, } from "./constants.js";
|
|
3
4
|
export { collectAssignableAppRoles, createTenantFeature } from "./feature.js";
|
package/dist/tenant/index.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
export { cancelPendingInvitation } from "./cancel-pending-invitation.js";
|
|
1
2
|
export { TenantCommandSchemas } from "./command-schemas.js";
|
|
2
3
|
export { OWNER_INVITE_ROLE_OPTIONS, TENANT_FEATURE, TenantErrors, TenantHandlers, TenantQueries, } from "./constants.js";
|
|
3
4
|
export { collectAssignableAppRoles, createTenantFeature } from "./feature.js";
|
|
@@ -49,6 +49,7 @@ export declare const tenantInvitationEntity: import("@cosmicdrift/kumiko-types/f
|
|
|
49
49
|
} & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
|
|
50
50
|
required: true;
|
|
51
51
|
};
|
|
52
|
+
globalRoles: import("@cosmicdrift/kumiko-types/fields").MultiSelectFieldDef<readonly ["SystemAdmin"]>;
|
|
52
53
|
status: {
|
|
53
54
|
readonly type: "select";
|
|
54
55
|
readonly description?: string;
|
|
@@ -151,6 +152,7 @@ export declare const tenantInvitationsTable: import("@cosmicdrift/kumiko-framewo
|
|
|
151
152
|
} & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
|
|
152
153
|
required: true;
|
|
153
154
|
};
|
|
155
|
+
globalRoles: import("@cosmicdrift/kumiko-types/fields").MultiSelectFieldDef<readonly ["SystemAdmin"]>;
|
|
154
156
|
status: {
|
|
155
157
|
readonly type: "select";
|
|
156
158
|
readonly description?: string;
|
|
@@ -22,7 +22,9 @@
|
|
|
22
22
|
// pending → re-use existing row + refresh Redis-token + send mail
|
|
23
23
|
// (analog zu signup-Resend).
|
|
24
24
|
import { buildEntityTable } from "@cosmicdrift/kumiko-framework/db";
|
|
25
|
-
import { createEntity, createSelectField, createTextField, createTimestampField, } from "@cosmicdrift/kumiko-framework/engine";
|
|
25
|
+
import { createEntity, createMultiSelectField, createSelectField, createTextField, createTimestampField, } from "@cosmicdrift/kumiko-framework/engine";
|
|
26
|
+
// kumiko-lint-ignore cross-feature-import global-role options are owned by the user feature
|
|
27
|
+
import { GLOBAL_ROLE_OPTIONS } from "../user/constants.js";
|
|
26
28
|
// Status-const-Object damit Handler-Code keine Magic-Strings nutzt.
|
|
27
29
|
// Bei rename (z.B. "cancelled" → "revoked") fällt jeder caller auf
|
|
28
30
|
// einmal auf statt verstreut über 5 Stellen.
|
|
@@ -57,6 +59,13 @@ export const tenantInvitationEntity = createEntity({
|
|
|
57
59
|
personal: false,
|
|
58
60
|
reason: "technical_reference",
|
|
59
61
|
}),
|
|
62
|
+
// Global (users.roles) roles granted on accept, on top of the membership
|
|
63
|
+
// role. Only the system-only invite variant sets them; every tenant-admin
|
|
64
|
+
// re-invite resets them to [] so a resend can never carry them over.
|
|
65
|
+
globalRoles: createMultiSelectField({
|
|
66
|
+
options: GLOBAL_ROLE_OPTIONS,
|
|
67
|
+
default: [],
|
|
68
|
+
}),
|
|
60
69
|
// Lifecycle-State. Default "pending"; transitions:
|
|
61
70
|
// pending → accepted | cancelled | expired
|
|
62
71
|
status: createSelectField({
|
|
@@ -4,6 +4,7 @@ export type TenantLifecycleGate = {
|
|
|
4
4
|
readonly status: string;
|
|
5
5
|
readonly gracePeriodEnd: string | null;
|
|
6
6
|
};
|
|
7
|
+
export declare const TEARDOWN_GATE_SETTLE_MS = 60000;
|
|
7
8
|
/** @internal exported for the bounded-cache test */
|
|
8
9
|
export declare const GATE_CACHE_MAX_ENTRIES = 5000;
|
|
9
10
|
export declare function invalidateTenantLifecycleGate(tenantId: TenantId): void;
|
|
@@ -15,6 +15,8 @@ import { tenantTable } from "../tenant/index.js";
|
|
|
15
15
|
// (cached as null) must not grow the Map without bound: past the cap, expired
|
|
16
16
|
// entries are swept and, if still full, the oldest-inserted entries are evicted.
|
|
17
17
|
const GATE_TTL_MS = 3000;
|
|
18
|
+
// Covers other pods' GATE_TTL_MS plus requests still in flight when the grace period (0 under a compliance override) ends.
|
|
19
|
+
export const TEARDOWN_GATE_SETTLE_MS = 60_000;
|
|
18
20
|
/** @internal exported for the bounded-cache test */
|
|
19
21
|
export const GATE_CACHE_MAX_ENTRIES = 5000;
|
|
20
22
|
const gateCache = new Map();
|
|
@@ -6,7 +6,7 @@ import { append, loadAggregate, VersionConflictError, } from "@cosmicdrift/kumik
|
|
|
6
6
|
import { getTemporal } from "@cosmicdrift/kumiko-framework/time";
|
|
7
7
|
import { tenantEntity, tenantTable } from "../tenant/index.js";
|
|
8
8
|
import { TENANT_AGGREGATE_TYPE, TENANT_DESTRUCTION_COMPLETED_EVENT_QN, TENANT_DESTRUCTION_FAILED_EVENT_QN, TENANT_DESTRUCTION_STAGE_ABANDONED_EVENT_SHORT, TENANT_DESTRUCTION_STAGE_FAILED_EVENT_SHORT, TENANT_DESTRUCTION_STAGE_STARTED_EVENT_SHORT, TENANT_DESTRUCTION_STAGE_SUCCEEDED_EVENT_SHORT, TENANT_DESTRUCTION_STARTED_EVENT_SHORT, } from "./constants.js";
|
|
9
|
-
import { invalidateTenantLifecycleGate } from "./lifecycle-gate.js";
|
|
9
|
+
import { invalidateTenantLifecycleGate, TEARDOWN_GATE_SETTLE_MS } from "./lifecycle-gate.js";
|
|
10
10
|
import { isDestructionPipelineComplete, pickNextStage, } from "./stages.js";
|
|
11
11
|
const tenantCrud = createEventStoreExecutor(tenantTable, tenantEntity, { entityName: "tenant" });
|
|
12
12
|
const FEATURE_PREFIX = "tenant-lifecycle:event:";
|
|
@@ -173,7 +173,7 @@ export async function runTenantDestructionSweep(args) {
|
|
|
173
173
|
const now = args.now ?? T.Now.instant();
|
|
174
174
|
const due = await selectMany(args.db, tenantTable, {
|
|
175
175
|
status: "destroyRequested",
|
|
176
|
-
gracePeriodEnd: { lte: now },
|
|
176
|
+
gracePeriodEnd: { lte: now.subtract({ milliseconds: TEARDOWN_GATE_SETTLE_MS }) },
|
|
177
177
|
});
|
|
178
178
|
let triggered = 0;
|
|
179
179
|
for (const row of due) {
|
|
@@ -44,6 +44,8 @@ import { fetchOne, selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
|
44
44
|
import { buildEntityTable, createEventStoreExecutor, createTenantDb, } from "@cosmicdrift/kumiko-framework/db";
|
|
45
45
|
import { defineEntityCreateHandler, defineEntityListHandler, defineEntityUpdateHandler, defineFeature, defineQueryHandler, HookPhases, SYSTEM_TENANT_ID, TENANT_TIER_RESOLVER_EXT, } from "@cosmicdrift/kumiko-framework/engine";
|
|
46
46
|
import { getUnscopedAggregateStreamMaxVersion } from "@cosmicdrift/kumiko-framework/event-store";
|
|
47
|
+
import { createFallbackLogger } from "@cosmicdrift/kumiko-framework/logging";
|
|
48
|
+
import { CACHE_SYNC_TOPICS, isTierAssignmentSyncMessage, } from "@cosmicdrift/kumiko-framework/redis";
|
|
47
49
|
import { getTemporal } from "@cosmicdrift/kumiko-framework/time";
|
|
48
50
|
import * as z from "zod";
|
|
49
51
|
import { tenantTable } from "../tenant/index.js";
|
|
@@ -59,6 +61,7 @@ import { isTrialActive } from "./trial.js";
|
|
|
59
61
|
// from the entity definition — same shape buildEntityTable would produce
|
|
60
62
|
// in the App's drizzle/schema.generated.ts.
|
|
61
63
|
const tierAssignmentTable = buildEntityTable("tier-assignment", tierAssignmentEntity);
|
|
64
|
+
const log = createFallbackLogger("tier-engine");
|
|
62
65
|
// Event-store-executor für direct-write aus dem auto-default-tier-hook.
|
|
63
66
|
// Pattern wie tenant/seeding.ts: hook sieht AppContext (kein ctx.write),
|
|
64
67
|
// muss aber atomisch mit tenant-create im selben TX schreiben → executor
|
|
@@ -146,10 +149,9 @@ export function createTierEngineFeature(opts = {}) {
|
|
|
146
149
|
// Cross-tenant set + read f\u00fcr den tier-admin-Screen. tier-options liefert
|
|
147
150
|
// dem Client die App-Tier-Namen aus der tierMap-Closure (sonst hartkodiert).
|
|
148
151
|
//
|
|
149
|
-
// onAssigned
|
|
150
|
-
//
|
|
151
|
-
//
|
|
152
|
-
// die echte Cache-Update-Funktion eingeh\u00e4ngt \u2014 analog alwaysOnHolder.
|
|
152
|
+
// onAssigned runs after commit (the direct executor write fires no postSave
|
|
153
|
+
// hook). Late-bound: without tierMap it stays a no-op, the tierMap block
|
|
154
|
+
// below installs the real cache update + publish.
|
|
153
155
|
const onTierAssigned = { fn: () => { } };
|
|
154
156
|
r.writeHandler(createSetTenantTierWrite({
|
|
155
157
|
onAssigned: (tenantId, tier) => onTierAssigned.fn(tenantId, tier),
|
|
@@ -223,6 +225,21 @@ export function createTierEngineFeature(opts = {}) {
|
|
|
223
225
|
// mergeAlwaysOn-calls — Late-bind via mutable holder, gefüllt vor allen
|
|
224
226
|
// Requests (build läuft pre-listen via runDevApp/runProdApp-pickup).
|
|
225
227
|
const alwaysOnHolder = { set: new Set() };
|
|
228
|
+
// Bus handed to build(); hooks fire outside build and publish through it.
|
|
229
|
+
const busHolder = { bus: undefined };
|
|
230
|
+
// Out-of-order guard: every cache mutation or reload start takes a ticket; a
|
|
231
|
+
// reload result is applied only while its ticket is still the tenant's latest.
|
|
232
|
+
let ticketCounter = 0;
|
|
233
|
+
const latestTicket = new Map();
|
|
234
|
+
const takeTicket = (tenantId) => {
|
|
235
|
+
ticketCounter += 1;
|
|
236
|
+
latestTicket.set(tenantId, ticketCounter);
|
|
237
|
+
return ticketCounter;
|
|
238
|
+
};
|
|
239
|
+
const publishAssignmentChange = (tenantId) => {
|
|
240
|
+
const message = { tenantId };
|
|
241
|
+
busHolder.bus?.publish(CACHE_SYNC_TOPICS.tierAssignment, message);
|
|
242
|
+
};
|
|
226
243
|
// Trial: zeit-abgeleitet aus tenant.inserted_at (≈ Signup), live am
|
|
227
244
|
// Feature-Gate geprüft — NICHT im Resolver-Cache. Der Resolver ist
|
|
228
245
|
// boot-cached + synchron und sieht weder frische Signups noch den
|
|
@@ -237,15 +254,15 @@ export function createTierEngineFeature(opts = {}) {
|
|
|
237
254
|
? featuresForTier(tierMap, opts.trial.tier)
|
|
238
255
|
: new Set();
|
|
239
256
|
const nowMs = () => getTemporal().Now.instant().epochMilliseconds;
|
|
240
|
-
//
|
|
241
|
-
//
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
// Semantik wie der Hook.
|
|
245
|
-
onTierAssigned.fn = (tenantId, tier) => {
|
|
257
|
+
// Immediate local visibility on the writing pod; other pods learn via the
|
|
258
|
+
// bus and re-read the row.
|
|
259
|
+
const applyAssignment = (tenantId, tier) => {
|
|
260
|
+
takeTicket(tenantId);
|
|
246
261
|
cache.set(tenantId, mergeAlwaysOn(alwaysOnHolder.set, featuresForTier(tierMap, tier)));
|
|
262
|
+
publishAssignmentChange(tenantId);
|
|
247
263
|
};
|
|
248
|
-
|
|
264
|
+
onTierAssigned.fn = applyAssignment;
|
|
265
|
+
// Invalidation: tier-assignment CRUD events (e.g. the billing webhook) update the cache.
|
|
249
266
|
r.hook("postSave", { allOf: "tier-assignment" }, async (result) => {
|
|
250
267
|
// result.data has tenantId + tier (after entity-update merge)
|
|
251
268
|
const data = result.data; // @cast-boundary engine-payload
|
|
@@ -256,15 +273,17 @@ export function createTierEngineFeature(opts = {}) {
|
|
|
256
273
|
// blocken (afterCommit-pattern, side-effect-best-effort).
|
|
257
274
|
if (typeof data.tenantId !== "string" || typeof data.tier !== "string")
|
|
258
275
|
return;
|
|
259
|
-
|
|
260
|
-
cache.set(tenantId, mergeAlwaysOn(alwaysOnHolder.set, featuresForTier(tierMap, data.tier)));
|
|
276
|
+
applyAssignment(data.tenantId, data.tier);
|
|
261
277
|
});
|
|
262
278
|
r.hook("postDelete", { allOf: "tier-assignment" }, async (payload) => {
|
|
263
279
|
const data = payload.data; // @cast-boundary engine-payload
|
|
264
280
|
// skip: gleiche type-guard semantik wie postSave-hook oben.
|
|
265
281
|
if (typeof data.tenantId !== "string")
|
|
266
282
|
return;
|
|
267
|
-
|
|
283
|
+
const tenantId = data.tenantId;
|
|
284
|
+
takeTicket(tenantId);
|
|
285
|
+
cache.delete(tenantId);
|
|
286
|
+
publishAssignmentChange(tenantId);
|
|
268
287
|
});
|
|
269
288
|
// Auto-default-tier-on-tenant-signup: hook fires inTransaction (atomic
|
|
270
289
|
// mit tenant-create rollback), schreibt tier-assignment via Direct-
|
|
@@ -359,6 +378,56 @@ export function createTierEngineFeature(opts = {}) {
|
|
|
359
378
|
for (const row of rows) {
|
|
360
379
|
cache.set(row.tenantId, mergeAlwaysOn(computedAlwaysOn, featuresForTier(tierMap, row.tier)));
|
|
361
380
|
}
|
|
381
|
+
// Read first, then set — never clear-then-refill: a cache miss falls back
|
|
382
|
+
// to the default tier, which would lock paying tenants out meanwhile.
|
|
383
|
+
const reloadTenant = async (tenantId) => {
|
|
384
|
+
const ticket = takeTicket(tenantId);
|
|
385
|
+
const row = await fetchOne(deps.db, tierAssignmentTable, { tenantId });
|
|
386
|
+
// skip: a newer write or reload for this tenant started meanwhile and owns the entry
|
|
387
|
+
if (latestTicket.get(tenantId) !== ticket)
|
|
388
|
+
return;
|
|
389
|
+
if (row) {
|
|
390
|
+
cache.set(tenantId, mergeAlwaysOn(computedAlwaysOn, featuresForTier(tierMap, row.tier)));
|
|
391
|
+
}
|
|
392
|
+
else {
|
|
393
|
+
cache.delete(tenantId);
|
|
394
|
+
}
|
|
395
|
+
};
|
|
396
|
+
const reloadAllTenants = async () => {
|
|
397
|
+
ticketCounter += 1;
|
|
398
|
+
const startedAtTicket = ticketCounter;
|
|
399
|
+
const current = await selectMany(deps.db, tierAssignmentTable);
|
|
400
|
+
const loaded = new Map();
|
|
401
|
+
for (const row of current) {
|
|
402
|
+
loaded.set(row.tenantId, mergeAlwaysOn(computedAlwaysOn, featuresForTier(tierMap, row.tier)));
|
|
403
|
+
}
|
|
404
|
+
const isNewerThanSnapshot = (tenantId) => (latestTicket.get(tenantId) ?? 0) > startedAtTicket;
|
|
405
|
+
for (const tenantId of [...cache.keys()]) {
|
|
406
|
+
if (!loaded.has(tenantId) && !isNewerThanSnapshot(tenantId))
|
|
407
|
+
cache.delete(tenantId);
|
|
408
|
+
}
|
|
409
|
+
for (const [tenantId, features] of loaded) {
|
|
410
|
+
if (!isNewerThanSnapshot(tenantId))
|
|
411
|
+
cache.set(tenantId, features);
|
|
412
|
+
}
|
|
413
|
+
};
|
|
414
|
+
busHolder.bus = deps.cacheSync;
|
|
415
|
+
deps.cacheSync?.subscribe(CACHE_SYNC_TOPICS.tierAssignment, (message) => {
|
|
416
|
+
// skip: malformed message from another process
|
|
417
|
+
if (!isTierAssignmentSyncMessage(message))
|
|
418
|
+
return;
|
|
419
|
+
reloadTenant(message.tenantId).catch((err) => {
|
|
420
|
+
log.error("tier reload failed, keeping cached set", {
|
|
421
|
+
tenantId: message.tenantId,
|
|
422
|
+
err,
|
|
423
|
+
});
|
|
424
|
+
});
|
|
425
|
+
});
|
|
426
|
+
deps.cacheSync?.onResync(() => {
|
|
427
|
+
reloadAllTenants().catch((err) => {
|
|
428
|
+
log.error("tier resync failed, keeping cached sets", { err });
|
|
429
|
+
});
|
|
430
|
+
});
|
|
362
431
|
// Synchronous resolver-callback for dispatcher hot-path.
|
|
363
432
|
const resolver = (tenantId) => {
|
|
364
433
|
// Operator-tooling + async-event-dispatch convention: SYSTEM_TENANT_ID
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { type TenantId } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
2
|
import * as z from "zod";
|
|
3
3
|
export type SetTenantTierOptions = {
|
|
4
|
-
/**
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
/** Runs after the write commits so feature.ts can update the resolver cache
|
|
5
|
+
* and notify the other processes. Without a tierMap there is no resolver
|
|
6
|
+
* and this is a no-op. */
|
|
7
7
|
readonly onAssigned?: (tenantId: TenantId, tier: string) => void;
|
|
8
8
|
/** Tier-Namen aus der tierMap-Closure — ohne sie (storage-only-Mode) bleibt
|
|
9
9
|
* `tier` unvalidiert außer Length/Non-Empty. Mit ihr rejected der Handler
|
|
@@ -19,14 +19,13 @@ import { tierAssignmentEntity } from "../entity.js";
|
|
|
19
19
|
// `source: "manual"` markiert den Grant, damit ein späterer Stripe→Tier-Sync ihn
|
|
20
20
|
// nicht plättet. Upsert: ein Aggregat pro Tenant (deterministische aggregate-id).
|
|
21
21
|
//
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
//
|
|
26
|
-
//
|
|
27
|
-
//
|
|
28
|
-
//
|
|
29
|
-
// (storage-only ohne tierMap = no-op).
|
|
22
|
+
// Effective-set invalidation: the executor write does not fire the
|
|
23
|
+
// `tier-assignment:postSave` entity hook, and a per-handler postSave would not
|
|
24
|
+
// fire either because this handler returns no lifecycle result (kind "save").
|
|
25
|
+
// Without an explicit update the feature gate would stay on the old tier, so
|
|
26
|
+
// `opts.onAssigned(tenantId, tier)` runs after commit (ctx.scheduleAfterCommit).
|
|
27
|
+
// feature.ts uses it to update its cache and notify the other processes; in
|
|
28
|
+
// storage-only mode without a tierMap it is a no-op.
|
|
30
29
|
const tierAssignmentTable = buildEntityTable("tier-assignment", tierAssignmentEntity);
|
|
31
30
|
const executor = createEventStoreExecutor(tierAssignmentTable, tierAssignmentEntity, {
|
|
32
31
|
entityName: "tier-assignment",
|
|
@@ -50,6 +49,14 @@ export function createSetTenantTierWrite(opts = {}) {
|
|
|
50
49
|
reason: SET_TENANT_TIER_REASON,
|
|
51
50
|
},
|
|
52
51
|
handler: async (event, ctx) => {
|
|
52
|
+
// Without a commit sink (unit harness) there is no transaction to wait for.
|
|
53
|
+
const afterCommit = (tenantId, tier) => {
|
|
54
|
+
const notify = async () => opts.onAssigned?.(tenantId, tier);
|
|
55
|
+
if (ctx.scheduleAfterCommit)
|
|
56
|
+
ctx.scheduleAfterCommit(notify);
|
|
57
|
+
else
|
|
58
|
+
opts.onAssigned?.(tenantId, tier);
|
|
59
|
+
};
|
|
53
60
|
const tenantId = event.payload.tenantId; // @cast-boundary engine-bridge
|
|
54
61
|
const rawDb = ctx.db.unsafeRaw();
|
|
55
62
|
const tdb = createTenantDb(rawDb, tenantId, "system");
|
|
@@ -64,13 +71,13 @@ export function createSetTenantTierWrite(opts = {}) {
|
|
|
64
71
|
}, systemUser, tdb);
|
|
65
72
|
if (!result.isSuccess)
|
|
66
73
|
return result;
|
|
67
|
-
|
|
74
|
+
afterCommit(tenantId, tier);
|
|
68
75
|
return { isSuccess: true, data: { tenantId, tier, isNew: false } };
|
|
69
76
|
}
|
|
70
77
|
const result = await executor.create({ id: tierAssignmentAggregateId(tenantId), tier, source: "manual", tenantId }, systemUser, tdb);
|
|
71
78
|
if (!result.isSuccess)
|
|
72
79
|
return result;
|
|
73
|
-
|
|
80
|
+
afterCommit(tenantId, tier);
|
|
74
81
|
return { isSuccess: true, data: { tenantId, tier, isNew: true } };
|
|
75
82
|
},
|
|
76
83
|
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type DbRunner } from "@cosmicdrift/kumiko-framework/db";
|
|
2
|
+
import { GLOBAL_ROLE_OPTIONS } from "./constants.js";
|
|
3
|
+
type GlobalRole = (typeof GLOBAL_ROLE_OPTIONS)[number];
|
|
4
|
+
export declare function parseInvitedGlobalRoles(raw: unknown): readonly GlobalRole[];
|
|
5
|
+
export declare function grantInvitedGlobalRoles(db: DbRunner, options: {
|
|
6
|
+
readonly userId: string;
|
|
7
|
+
readonly globalRoles: readonly string[];
|
|
8
|
+
}): Promise<import("@cosmicdrift/kumiko-types/write-error-info-types").WriteFailure | {
|
|
9
|
+
readonly isSuccess: true;
|
|
10
|
+
readonly data: {
|
|
11
|
+
readonly roles: readonly string[];
|
|
12
|
+
};
|
|
13
|
+
}>;
|
|
14
|
+
export {};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { fetchOne } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
2
|
+
import { createEventStoreExecutor, createTenantDb, } from "@cosmicdrift/kumiko-framework/db";
|
|
3
|
+
import { createSystemUser, SYSTEM_TENANT_ID } from "@cosmicdrift/kumiko-framework/engine";
|
|
4
|
+
import { NotFoundError, writeFailure } from "@cosmicdrift/kumiko-framework/errors";
|
|
5
|
+
import { parseRoles } from "@cosmicdrift/kumiko-framework/utils";
|
|
6
|
+
import { GLOBAL_ROLE_OPTIONS } from "./constants.js";
|
|
7
|
+
import { userEntity, userTable } from "./schema/user.js";
|
|
8
|
+
const userExecutor = createEventStoreExecutor(userTable, userEntity, { entityName: "user" });
|
|
9
|
+
function isGlobalRole(role) {
|
|
10
|
+
return GLOBAL_ROLE_OPTIONS.some((option) => option === role);
|
|
11
|
+
}
|
|
12
|
+
// Reads the invitation's stored globalRoles column, keeping only assignable
|
|
13
|
+
// global roles — a hand-edited row cannot smuggle "system" into users.roles.
|
|
14
|
+
export function parseInvitedGlobalRoles(raw) {
|
|
15
|
+
return parseRoles(raw).filter(isGlobalRole);
|
|
16
|
+
}
|
|
17
|
+
// Add-only: an accepted invitation never removes a global role the user
|
|
18
|
+
// already holds. Users live on SYSTEM_TENANT_ID (tenancy "global").
|
|
19
|
+
export async function grantInvitedGlobalRoles(db, options) {
|
|
20
|
+
const existing = await fetchOne(db, userTable, { id: options.userId });
|
|
21
|
+
if (!existing)
|
|
22
|
+
return writeFailure(new NotFoundError("user", options.userId));
|
|
23
|
+
const currentRoles = parseRoles(existing.roles);
|
|
24
|
+
const missingRoles = options.globalRoles.filter((role) => !currentRoles.includes(role));
|
|
25
|
+
if (missingRoles.length === 0)
|
|
26
|
+
return grantedGlobalRoles(currentRoles);
|
|
27
|
+
const roles = [...currentRoles, ...missingRoles];
|
|
28
|
+
const updateResult = await userExecutor.update({ id: existing.id, version: existing.version, changes: { roles } }, createSystemUser(SYSTEM_TENANT_ID), createTenantDb(db, SYSTEM_TENANT_ID, "system"));
|
|
29
|
+
if (!updateResult.isSuccess)
|
|
30
|
+
return updateResult;
|
|
31
|
+
return grantedGlobalRoles(roles);
|
|
32
|
+
}
|
|
33
|
+
function grantedGlobalRoles(roles) {
|
|
34
|
+
return { isSuccess: true, data: { roles } };
|
|
35
|
+
}
|
|
@@ -308,6 +308,9 @@ export function createUserDataRightsFeature(opts = {}) {
|
|
|
308
308
|
method: "GET",
|
|
309
309
|
path: "/user-export/by-token",
|
|
310
310
|
anonymous: true,
|
|
311
|
+
// Entry point of the emailed download link; cheap static page, but
|
|
312
|
+
// anonymous and token-bearing, so bound per-IP probing.
|
|
313
|
+
rateLimit: { per: "ip+handler", limit: 30, windowSeconds: 60 },
|
|
311
314
|
handler: async (c) => {
|
|
312
315
|
return c.body(TOKEN_EXCHANGE_PAGE_HTML, 200, {
|
|
313
316
|
"content-type": "text/html; charset=utf-8",
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export declare const WAITLIST_FEATURE: "waitlist";
|
|
2
|
+
export declare const WAITLIST_STATUS: {
|
|
3
|
+
readonly Pending: "pending";
|
|
4
|
+
readonly Invited: "invited";
|
|
5
|
+
readonly Rejected: "rejected";
|
|
6
|
+
};
|
|
7
|
+
export type WaitlistStatus = (typeof WAITLIST_STATUS)[keyof typeof WAITLIST_STATUS];
|
|
8
|
+
export declare const WaitlistHandlers: {
|
|
9
|
+
readonly submit: "waitlist:write:submit";
|
|
10
|
+
readonly invite: "waitlist:write:invite";
|
|
11
|
+
readonly reject: "waitlist:write:reject";
|
|
12
|
+
};
|
|
13
|
+
export declare const WaitlistQueries: {
|
|
14
|
+
readonly list: "waitlist:query:waitlist-entry:list";
|
|
15
|
+
};
|
|
16
|
+
export declare const WaitlistErrors: {
|
|
17
|
+
readonly notInvitable: "waitlist_not_invitable";
|
|
18
|
+
readonly notRejectable: "waitlist_not_rejectable";
|
|
19
|
+
};
|
|
20
|
+
export declare const WAITLIST_NOTIFICATION_TYPES: {
|
|
21
|
+
readonly confirmation: "waitlist:notify:confirmation";
|
|
22
|
+
readonly adminNotice: "waitlist:notify:admin-notice";
|
|
23
|
+
};
|
|
24
|
+
export declare const DEFAULT_OWN_TENANT_INVITE_ROLE = "TenantAdmin";
|
|
25
|
+
export declare const WAITLIST_FIELD_LIMITS: {
|
|
26
|
+
readonly name: 120;
|
|
27
|
+
readonly email: 254;
|
|
28
|
+
readonly company: 200;
|
|
29
|
+
readonly portfolio: 200;
|
|
30
|
+
readonly message: 2000;
|
|
31
|
+
readonly locale: 35;
|
|
32
|
+
readonly honeypot: 500;
|
|
33
|
+
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// @runtime client
|
|
2
|
+
// Pure string constants, safe for browser code (see auth-email-password/constants.ts).
|
|
3
|
+
export const WAITLIST_FEATURE = "waitlist";
|
|
4
|
+
export const WAITLIST_STATUS = {
|
|
5
|
+
Pending: "pending",
|
|
6
|
+
Invited: "invited",
|
|
7
|
+
Rejected: "rejected",
|
|
8
|
+
};
|
|
9
|
+
export const WaitlistHandlers = {
|
|
10
|
+
submit: "waitlist:write:submit",
|
|
11
|
+
invite: "waitlist:write:invite",
|
|
12
|
+
reject: "waitlist:write:reject",
|
|
13
|
+
};
|
|
14
|
+
// Entity-convention list QN: entityList screens resolve their query by this shape.
|
|
15
|
+
export const WaitlistQueries = {
|
|
16
|
+
list: "waitlist:query:waitlist-entry:list",
|
|
17
|
+
};
|
|
18
|
+
export const WaitlistErrors = {
|
|
19
|
+
notInvitable: "waitlist_not_invitable",
|
|
20
|
+
notRejectable: "waitlist_not_rejectable",
|
|
21
|
+
};
|
|
22
|
+
export const WAITLIST_NOTIFICATION_TYPES = {
|
|
23
|
+
confirmation: "waitlist:notify:confirmation",
|
|
24
|
+
adminNotice: "waitlist:notify:admin-notice",
|
|
25
|
+
};
|
|
26
|
+
export const DEFAULT_OWN_TENANT_INVITE_ROLE = "TenantAdmin";
|
|
27
|
+
export const WAITLIST_FIELD_LIMITS = {
|
|
28
|
+
name: 120,
|
|
29
|
+
email: 254,
|
|
30
|
+
company: 200,
|
|
31
|
+
portfolio: 200,
|
|
32
|
+
message: 2000,
|
|
33
|
+
locale: 35,
|
|
34
|
+
honeypot: 500,
|
|
35
|
+
};
|