@cosmicdrift/kumiko-bundled-features 0.340.0 → 0.342.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/audit/constants.d.ts +3 -0
- package/dist/audit/constants.js +3 -0
- package/dist/audit/feature.js +34 -7
- package/dist/audit/run-escape-hatch-retention.d.ts +10 -0
- package/dist/audit/run-escape-hatch-retention.js +27 -0
- package/dist/auth-email-password/bootstrap.d.ts +80 -0
- package/dist/auth-email-password/bootstrap.js +170 -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-email-password/web/auth-form-primitives.d.ts +7 -1
- package/dist/auth-email-password/web/auth-form-primitives.js +5 -5
- package/dist/auth-email-password/web/confirm-account-unlock-screen.js +3 -3
- package/dist/auth-email-password/web/forgot-password-screen.js +2 -2
- package/dist/auth-email-password/web/invite-accept-screen.js +9 -9
- package/dist/auth-email-password/web/login-screen.js +8 -8
- package/dist/auth-email-password/web/request-account-unlock-screen.js +2 -2
- package/dist/auth-email-password/web/reset-password-screen.js +3 -3
- package/dist/auth-email-password/web/session-bootstrap-error.js +3 -3
- package/dist/auth-email-password/web/signup-complete-screen.js +3 -3
- package/dist/auth-email-password/web/signup-screen.js +2 -2
- package/dist/auth-email-password/web/verify-email-screen.js +3 -3
- 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/auth-mfa/web/mfa-setup-preauth-screen.js +5 -5
- package/dist/auth-mfa/web/mfa-verify-screen.js +3 -3
- 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 +8 -1
- package/dist/channel-email/pii-guard.js +6 -5
- package/dist/channel-email/smtp-transport.js +22 -1
- package/dist/channel-email/types.d.ts +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 +30 -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 +30 -0
- package/src/auth-mfa/changes.json +6 -0
- package/src/billing-foundation/changes.json +6 -0
- package/src/channel-email/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 +23 -0
|
@@ -13,3 +13,6 @@ export declare const AuditQueries: {
|
|
|
13
13
|
export declare const AUDIT_LOG_SCREEN_ID: "audit-log";
|
|
14
14
|
/** Single-event detail screen, breadcrumb-linked to the audit-log list. */
|
|
15
15
|
export declare const AUDIT_LOG_DETAIL_SCREEN_ID: "audit-log-detail";
|
|
16
|
+
/** Config key (system scope) for how long `escapeHatchUse` audit events are kept. */
|
|
17
|
+
export declare const ESCAPE_HATCH_RETENTION_DAYS_KEY: "audit:config:escape-hatch-retention-days";
|
|
18
|
+
export declare const DEFAULT_ESCAPE_HATCH_RETENTION_DAYS = 90;
|
package/dist/audit/constants.js
CHANGED
|
@@ -18,3 +18,6 @@ export const AuditQueries = {
|
|
|
18
18
|
export const AUDIT_LOG_SCREEN_ID = "audit-log";
|
|
19
19
|
/** Single-event detail screen, breadcrumb-linked to the audit-log list. */
|
|
20
20
|
export const AUDIT_LOG_DETAIL_SCREEN_ID = "audit-log-detail";
|
|
21
|
+
/** Config key (system scope) for how long `escapeHatchUse` audit events are kept. */
|
|
22
|
+
export const ESCAPE_HATCH_RETENTION_DAYS_KEY = "audit:config:escape-hatch-retention-days";
|
|
23
|
+
export const DEFAULT_ESCAPE_HATCH_RETENTION_DAYS = 90;
|
package/dist/audit/feature.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import { access, defineFeature, i18nKey, } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
|
-
import { AUDIT_LOG_DETAIL_SCREEN_ID, AUDIT_LOG_SCREEN_ID, AuditQueries } from "./constants.js";
|
|
1
|
+
import { access, createSystemConfig, defineFeature, i18nKey, SYSTEM_USER_ID, } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
|
+
import { AUDIT_LOG_DETAIL_SCREEN_ID, AUDIT_LOG_SCREEN_ID, AuditQueries, DEFAULT_ESCAPE_HATCH_RETENTION_DAYS, } from "./constants.js";
|
|
3
3
|
import { escapeHatchUsedSchema } from "./escape-hatch-audit-sink.js";
|
|
4
4
|
import { detailsQuery } from "./handlers/details.query.js";
|
|
5
5
|
import { listQuery } from "./handlers/list.query.js";
|
|
6
6
|
import { AUDIT_I18N } from "./i18n.js";
|
|
7
|
+
import { runEscapeHatchRetention } from "./run-escape-hatch-retention.js";
|
|
7
8
|
// Audit feature — exposes a filtered read over the framework's event log.
|
|
8
9
|
//
|
|
9
10
|
// Design: the event-store IS the audit trail (every entity write produces
|
|
@@ -12,10 +13,11 @@ import { AUDIT_I18N } from "./i18n.js";
|
|
|
12
13
|
// the existing `events` table. See handlers/list.query.ts for the filter
|
|
13
14
|
// surface.
|
|
14
15
|
//
|
|
15
|
-
// Retention
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
// the
|
|
16
|
+
// Retention: only `escapeHatchUse` events are pruned (daily job, config key
|
|
17
|
+
// `escapeHatchRetentionDays`). All other events stay as the source of truth
|
|
18
|
+
// for state; archive or compress policies are a separate concern (tracked
|
|
19
|
+
// with the snapshot/archive infrastructure that already exists in the
|
|
20
|
+
// framework).
|
|
19
21
|
export function createAuditFeature() {
|
|
20
22
|
return defineFeature("audit", (r) => {
|
|
21
23
|
r.describe("Exposes the framework's event store as a paginated, filterable audit log via the `audit:query:list` handler (accessible to `Admin` and `SystemAdmin` roles). No separate table or projection \u2014 the event store is the audit trail by construction: every entity write already records who, when, what entity, and the event payload with PII stripped. Filter by `aggregateType`, `aggregateId`, `eventType`, `userId`, or time range. Also records `audit:event:escape-hatch-used` whenever a handler uses one of the framework's escape hatches (unsafeRaw, acknowledgeCrossTenant, db.global() writes, or a granted identity switch).");
|
|
@@ -25,9 +27,34 @@ export function createAuditFeature() {
|
|
|
25
27
|
recommended: false,
|
|
26
28
|
});
|
|
27
29
|
r.translations({ keys: AUDIT_I18N });
|
|
28
|
-
r.requires("tenant", "user");
|
|
30
|
+
r.requires("tenant", "user", "config");
|
|
29
31
|
// Registered so ops tools/MSPs can discover the type — escape-hatch-audit-sink.ts appends it via the low-level event-store API.
|
|
30
32
|
r.defineEvent("escape-hatch-used", escapeHatchUsedSchema, { piiFields: "none" });
|
|
33
|
+
r.config("escapeHatchRetentionDays", createSystemConfig("number", {
|
|
34
|
+
default: DEFAULT_ESCAPE_HATCH_RETENTION_DAYS,
|
|
35
|
+
bounds: { min: 1 },
|
|
36
|
+
}));
|
|
37
|
+
r.job({
|
|
38
|
+
name: "escape-hatch-retention",
|
|
39
|
+
trigger: { cron: "30 3 * * *" },
|
|
40
|
+
concurrency: "skip",
|
|
41
|
+
// pruneEvents needs a transaction-capable connection, which only unsafeRaw() hands a job.
|
|
42
|
+
escapeHatch: {
|
|
43
|
+
reason: "prunes expired escapeHatchUse audit events through pruneEvents",
|
|
44
|
+
grants: ["unsafeRaw"],
|
|
45
|
+
},
|
|
46
|
+
handler: async (_payload, ctx) => {
|
|
47
|
+
if (!ctx.registry) {
|
|
48
|
+
throw new Error("escape-hatch-retention: ctx.registry required (JobContext incomplete)");
|
|
49
|
+
}
|
|
50
|
+
await runEscapeHatchRetention({
|
|
51
|
+
db: ctx.db.unsafeRaw(),
|
|
52
|
+
registry: ctx.registry,
|
|
53
|
+
configResolver: ctx.configResolver,
|
|
54
|
+
userId: ctx._userId ?? SYSTEM_USER_ID,
|
|
55
|
+
});
|
|
56
|
+
},
|
|
57
|
+
});
|
|
31
58
|
const queries = {
|
|
32
59
|
list: r.queryHandler(listQuery),
|
|
33
60
|
details: r.queryHandler(detailsQuery),
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { DbConnection, DbRunner } from "@cosmicdrift/kumiko-framework/db";
|
|
2
|
+
import { type ConfigResolver, type Registry } from "@cosmicdrift/kumiko-framework/engine";
|
|
3
|
+
import { type PruneEventsResult } from "@cosmicdrift/kumiko-framework/pipeline";
|
|
4
|
+
export declare function isDbConnection(runner: DbRunner): runner is DbConnection;
|
|
5
|
+
export declare function runEscapeHatchRetention(args: {
|
|
6
|
+
readonly db: DbRunner;
|
|
7
|
+
readonly registry: Registry;
|
|
8
|
+
readonly configResolver: ConfigResolver | undefined;
|
|
9
|
+
readonly userId: string;
|
|
10
|
+
}): Promise<PruneEventsResult>;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { SYSTEM_TENANT_ID, } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
|
+
import { pruneEvents } from "@cosmicdrift/kumiko-framework/pipeline";
|
|
3
|
+
import { createConfigAccessor } from "../config/index.js";
|
|
4
|
+
import { DEFAULT_ESCAPE_HATCH_RETENTION_DAYS, ESCAPE_HATCH_RETENTION_DAYS_KEY, } from "./constants.js";
|
|
5
|
+
import { ESCAPE_HATCH_USE_AGGREGATE_TYPE } from "./escape-hatch-audit-sink.js";
|
|
6
|
+
// pruneEvents opens its own transaction, so a runner that cannot begin one (a tx handle) is rejected instead of nested.
|
|
7
|
+
export function isDbConnection(runner) {
|
|
8
|
+
return "begin" in runner && typeof runner.begin === "function";
|
|
9
|
+
}
|
|
10
|
+
async function resolveRetentionDays(args) {
|
|
11
|
+
if (!args.configResolver)
|
|
12
|
+
return DEFAULT_ESCAPE_HATCH_RETENTION_DAYS;
|
|
13
|
+
const config = createConfigAccessor(args.registry, args.configResolver, SYSTEM_TENANT_ID, args.userId, args.db);
|
|
14
|
+
const raw = await config(ESCAPE_HATCH_RETENTION_DAYS_KEY);
|
|
15
|
+
return typeof raw === "number" && raw >= 1 ? raw : DEFAULT_ESCAPE_HATCH_RETENTION_DAYS;
|
|
16
|
+
}
|
|
17
|
+
export async function runEscapeHatchRetention(args) {
|
|
18
|
+
const { db } = args;
|
|
19
|
+
if (!isDbConnection(db)) {
|
|
20
|
+
throw new Error("audit escape-hatch retention: a transaction-free DbConnection is required");
|
|
21
|
+
}
|
|
22
|
+
const olderThanDays = await resolveRetentionDays({ ...args, db });
|
|
23
|
+
return pruneEvents(db, {
|
|
24
|
+
aggregateTypes: [ESCAPE_HATCH_USE_AGGREGATE_TYPE],
|
|
25
|
+
olderThanDays,
|
|
26
|
+
});
|
|
27
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { makeDispatchSystemWrite, type SystemDispatchArgs } from "@cosmicdrift/kumiko-framework/api";
|
|
2
|
+
import type { DbConnection } from "@cosmicdrift/kumiko-framework/db";
|
|
3
|
+
import { type TenantId, type WriteResult } from "@cosmicdrift/kumiko-framework/engine";
|
|
4
|
+
import type { Dispatcher } from "@cosmicdrift/kumiko-framework/pipeline";
|
|
5
|
+
import type { Redis } from "ioredis";
|
|
6
|
+
import type { GLOBAL_ROLE_OPTIONS } from "../user/constants.js";
|
|
7
|
+
type GlobalRole = (typeof GLOBAL_ROLE_OPTIONS)[number];
|
|
8
|
+
export type BootstrapInvite = {
|
|
9
|
+
readonly email: string;
|
|
10
|
+
/** Membership role in the tenant, validated like a tenant-admin invite
|
|
11
|
+
* (reserved roles such as SystemAdmin are rejected here). */
|
|
12
|
+
readonly role: string;
|
|
13
|
+
};
|
|
14
|
+
export type BootstrapSeedDeps = {
|
|
15
|
+
readonly tenantId: TenantId;
|
|
16
|
+
readonly db: DbConnection;
|
|
17
|
+
readonly dispatchSystemWrite: ReturnType<typeof makeDispatchSystemWrite>;
|
|
18
|
+
/** Writes as the system user with the given tenant roles in the seeded
|
|
19
|
+
* tenant; use it when the app gates tenant data on its own roles rather
|
|
20
|
+
* than SystemAdmin. */
|
|
21
|
+
readonly dispatchWriteAs: (args: Pick<SystemDispatchArgs, "handlerQn" | "payload"> & {
|
|
22
|
+
readonly roles: readonly string[];
|
|
23
|
+
}) => Promise<WriteResult>;
|
|
24
|
+
};
|
|
25
|
+
export type BootstrapConfigValue = string | number | boolean;
|
|
26
|
+
export type BootstrapTenant = {
|
|
27
|
+
readonly id: TenantId;
|
|
28
|
+
readonly key: string;
|
|
29
|
+
readonly name: string;
|
|
30
|
+
/** Tenant-scope config values by qualified key (e.g.
|
|
31
|
+
* `"auth-mfa:config:required": "admins"`), written as system writes on
|
|
32
|
+
* EVERY run: config is declarative, so a rerun converges to the plan and
|
|
33
|
+
* a value that already matches is not rewritten. Unlike `seed` it also
|
|
34
|
+
* applies to tenants that already exist. */
|
|
35
|
+
readonly config?: Readonly<Record<string, BootstrapConfigValue>>;
|
|
36
|
+
readonly invites?: readonly BootstrapInvite[];
|
|
37
|
+
};
|
|
38
|
+
export type BootstrapSystemAdmin = {
|
|
39
|
+
readonly email: string;
|
|
40
|
+
/** Home tenant: login needs a membership, so the SystemAdmin is invited
|
|
41
|
+
* into this tenant with `role` and receives the global role on accept. */
|
|
42
|
+
readonly tenantId: TenantId;
|
|
43
|
+
readonly role: string;
|
|
44
|
+
};
|
|
45
|
+
export type BootstrapPlan = {
|
|
46
|
+
readonly tenants: readonly BootstrapTenant[];
|
|
47
|
+
readonly systemAdmins?: readonly BootstrapSystemAdmin[];
|
|
48
|
+
/** Runs for each tenant right after this run created it. An existing
|
|
49
|
+
* tenant is never seeded again — also not when a previous run failed
|
|
50
|
+
* inside the seed (that run fails; clean up by hand). */
|
|
51
|
+
readonly seed?: (deps: BootstrapSeedDeps) => Promise<void>;
|
|
52
|
+
};
|
|
53
|
+
export type BootstrapDeps = {
|
|
54
|
+
readonly db: DbConnection;
|
|
55
|
+
readonly redis: Redis;
|
|
56
|
+
readonly dispatcher: Dispatcher;
|
|
57
|
+
};
|
|
58
|
+
export type BootstrapTenantOutcome = "created" | "exists";
|
|
59
|
+
export type BootstrapInviteOutcome = "invited" | "resent" | "pending" | "active" | "cancelled" | "role-mismatch";
|
|
60
|
+
export type BootstrapReport = {
|
|
61
|
+
readonly tenants: ReadonlyArray<{
|
|
62
|
+
readonly id: TenantId;
|
|
63
|
+
readonly outcome: BootstrapTenantOutcome;
|
|
64
|
+
readonly seeded: boolean;
|
|
65
|
+
/** Qualified keys this run wrote (new or changed); empty on a no-op rerun. */
|
|
66
|
+
readonly configApplied: readonly string[];
|
|
67
|
+
}>;
|
|
68
|
+
readonly invites: ReadonlyArray<{
|
|
69
|
+
readonly tenantId: TenantId;
|
|
70
|
+
readonly email: string;
|
|
71
|
+
readonly globalRoles: readonly GlobalRole[];
|
|
72
|
+
readonly outcome: BootstrapInviteOutcome;
|
|
73
|
+
}>;
|
|
74
|
+
};
|
|
75
|
+
export declare class BootstrapPlanError extends Error {
|
|
76
|
+
}
|
|
77
|
+
export declare class BootstrapWriteError extends Error {
|
|
78
|
+
}
|
|
79
|
+
export declare function bootstrapTenants(deps: BootstrapDeps, plan: BootstrapPlan): Promise<BootstrapReport>;
|
|
80
|
+
export {};
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
// Passwordless, idempotent first-run provisioning: tenants plus invitations
|
|
2
|
+
// (optionally carrying global roles such as SystemAdmin). Every write goes
|
|
3
|
+
// through the dispatcher as the system user, so hooks, events and jobs fire
|
|
4
|
+
// exactly as for any other writer. Nobody gets a role before accepting: the
|
|
5
|
+
// invitation row holds membership role + global roles until the invitee
|
|
6
|
+
// accepts through the regular invite-accept routes.
|
|
7
|
+
import { makeDispatchSystemWrite, } from "@cosmicdrift/kumiko-framework/api";
|
|
8
|
+
import { fetchOne } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
9
|
+
import { ConfigScopes, createSystemUser, } from "@cosmicdrift/kumiko-framework/engine";
|
|
10
|
+
import { parseRoles } from "@cosmicdrift/kumiko-framework/utils";
|
|
11
|
+
// kumiko-lint-ignore cross-feature-import bootstrap sets per-tenant config through the config feature's write handler
|
|
12
|
+
import { ConfigHandlers } from "../config/constants.js";
|
|
13
|
+
// kumiko-lint-ignore cross-feature-import bootstrap compares against the stored config row to stay idempotent
|
|
14
|
+
import { findConfigRow } from "../config/write-helpers.js";
|
|
15
|
+
// kumiko-lint-ignore cross-feature-import bootstrap provisions tenant + invitation rows owned by the tenant feature
|
|
16
|
+
import { TenantHandlers } from "../tenant/constants.js";
|
|
17
|
+
// kumiko-lint-ignore cross-feature-import bootstrap provisions tenant + invitation rows owned by the tenant feature
|
|
18
|
+
import { INVITATION_STATUS, tenantInvitationsTable } from "../tenant/invitation-table.js";
|
|
19
|
+
// kumiko-lint-ignore cross-feature-import bootstrap checks existing memberships
|
|
20
|
+
import { tenantMembershipsTable } from "../tenant/membership-table.js";
|
|
21
|
+
// kumiko-lint-ignore cross-feature-import bootstrap checks for an existing tenant
|
|
22
|
+
import { tenantTable } from "../tenant/schema/tenant.js";
|
|
23
|
+
// kumiko-lint-ignore cross-feature-import bootstrap skips already activated users
|
|
24
|
+
import { userTable } from "../user/schema/user.js";
|
|
25
|
+
import { AuthHandlers } from "./constants.js";
|
|
26
|
+
import { hasLiveInviteToken } from "./invite-token-store.js";
|
|
27
|
+
export class BootstrapPlanError extends Error {
|
|
28
|
+
}
|
|
29
|
+
export class BootstrapWriteError extends Error {
|
|
30
|
+
}
|
|
31
|
+
function planInvites(plan) {
|
|
32
|
+
const tenantIds = new Set(plan.tenants.map((t) => t.id));
|
|
33
|
+
const invites = [];
|
|
34
|
+
for (const admin of plan.systemAdmins ?? []) {
|
|
35
|
+
if (!tenantIds.has(admin.tenantId)) {
|
|
36
|
+
throw new BootstrapPlanError(`systemAdmins: ${admin.email} references tenant ${admin.tenantId}, which is not in tenants`);
|
|
37
|
+
}
|
|
38
|
+
invites.push({
|
|
39
|
+
tenantId: admin.tenantId,
|
|
40
|
+
email: admin.email.toLowerCase(),
|
|
41
|
+
role: admin.role,
|
|
42
|
+
globalRoles: ["SystemAdmin"],
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
for (const tenant of plan.tenants) {
|
|
46
|
+
for (const invite of tenant.invites ?? []) {
|
|
47
|
+
invites.push({
|
|
48
|
+
tenantId: tenant.id,
|
|
49
|
+
email: invite.email.toLowerCase(),
|
|
50
|
+
role: invite.role,
|
|
51
|
+
globalRoles: [],
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
// One invitation row per (tenant, email): a second entry would overwrite
|
|
56
|
+
// the first one's role or global roles.
|
|
57
|
+
const seen = new Set();
|
|
58
|
+
for (const invite of invites) {
|
|
59
|
+
const key = `${invite.tenantId}\u0000${invite.email}`;
|
|
60
|
+
if (seen.has(key)) {
|
|
61
|
+
throw new BootstrapPlanError(`${invite.email} is listed twice for tenant ${invite.tenantId} (systemAdmins and tenants[].invites share one invitation per tenant)`);
|
|
62
|
+
}
|
|
63
|
+
seen.add(key);
|
|
64
|
+
}
|
|
65
|
+
return invites;
|
|
66
|
+
}
|
|
67
|
+
async function ensureTenant(deps, tenant) {
|
|
68
|
+
const existing = await fetchOne(deps.db, tenantTable, { id: tenant.id });
|
|
69
|
+
if (existing)
|
|
70
|
+
return "exists";
|
|
71
|
+
const result = await deps.dispatcher.write(TenantHandlers.create, { id: tenant.id, key: tenant.key, name: tenant.name }, createSystemUser(tenant.id));
|
|
72
|
+
if (!result.isSuccess) {
|
|
73
|
+
throw new BootstrapWriteError(`creating tenant ${tenant.key} (${tenant.id}) failed: ${result.error.code} — ${result.error.message}`);
|
|
74
|
+
}
|
|
75
|
+
return "created";
|
|
76
|
+
}
|
|
77
|
+
async function applyTenantConfig(deps, tenant) {
|
|
78
|
+
const applied = [];
|
|
79
|
+
for (const [key, value] of Object.entries(tenant.config ?? {})) {
|
|
80
|
+
const existing = await findConfigRow(deps.db, key, tenant.id, null);
|
|
81
|
+
if (existing?.value === JSON.stringify(value))
|
|
82
|
+
continue;
|
|
83
|
+
const result = await deps.dispatcher.write(ConfigHandlers.set, { key, value, scope: ConfigScopes.tenant },
|
|
84
|
+
// SystemAdmin on top of the system role: keys with the default tenant
|
|
85
|
+
// write access (admin roles) reject a bare system user, system-only keys
|
|
86
|
+
// accept it.
|
|
87
|
+
createSystemUser(tenant.id, ["SystemAdmin"]));
|
|
88
|
+
if (!result.isSuccess) {
|
|
89
|
+
throw new BootstrapWriteError(`setting config ${key} for tenant ${tenant.id} failed: ${result.error.code} — ${result.error.message}`);
|
|
90
|
+
}
|
|
91
|
+
applied.push(key);
|
|
92
|
+
}
|
|
93
|
+
return applied;
|
|
94
|
+
}
|
|
95
|
+
async function holdsInvitedAccess(deps, invite) {
|
|
96
|
+
const user = await fetchOne(deps.db, userTable, {
|
|
97
|
+
email: invite.email,
|
|
98
|
+
isDeleted: false,
|
|
99
|
+
});
|
|
100
|
+
if (!user)
|
|
101
|
+
return false;
|
|
102
|
+
const membership = await fetchOne(deps.db, tenantMembershipsTable, {
|
|
103
|
+
userId: user.id,
|
|
104
|
+
tenantId: invite.tenantId,
|
|
105
|
+
});
|
|
106
|
+
if (!membership)
|
|
107
|
+
return false;
|
|
108
|
+
const userGlobalRoles = parseRoles(user.roles);
|
|
109
|
+
return invite.globalRoles.every((role) => userGlobalRoles.includes(role));
|
|
110
|
+
}
|
|
111
|
+
async function decideInvite(deps, invite) {
|
|
112
|
+
if (await holdsInvitedAccess(deps, invite))
|
|
113
|
+
return "active";
|
|
114
|
+
const invitation = await fetchOne(deps.db, tenantInvitationsTable, {
|
|
115
|
+
tenantId: invite.tenantId,
|
|
116
|
+
email: invite.email,
|
|
117
|
+
});
|
|
118
|
+
if (!invitation)
|
|
119
|
+
return "invited";
|
|
120
|
+
if (invitation.status === INVITATION_STATUS.cancelled)
|
|
121
|
+
return "cancelled";
|
|
122
|
+
const storedGlobalRoles = parseRoles(invitation.globalRoles);
|
|
123
|
+
if (!invite.globalRoles.every((role) => storedGlobalRoles.includes(role))) {
|
|
124
|
+
return "role-mismatch";
|
|
125
|
+
}
|
|
126
|
+
if (invitation.status === INVITATION_STATUS.accepted)
|
|
127
|
+
return "active";
|
|
128
|
+
if (await hasLiveInviteToken(deps.redis, invitation.id))
|
|
129
|
+
return "pending";
|
|
130
|
+
return "resent";
|
|
131
|
+
}
|
|
132
|
+
function isMailingOutcome(outcome) {
|
|
133
|
+
return outcome === "invited" || outcome === "resent";
|
|
134
|
+
}
|
|
135
|
+
export async function bootstrapTenants(deps, plan) {
|
|
136
|
+
const plannedInvites = planInvites(plan);
|
|
137
|
+
const dispatchSystemWrite = makeDispatchSystemWrite(deps.dispatcher);
|
|
138
|
+
const tenants = [];
|
|
139
|
+
for (const tenant of plan.tenants) {
|
|
140
|
+
const outcome = await ensureTenant(deps, tenant);
|
|
141
|
+
const shouldSeed = outcome === "created" && plan.seed !== undefined;
|
|
142
|
+
if (shouldSeed) {
|
|
143
|
+
await plan.seed?.({
|
|
144
|
+
tenantId: tenant.id,
|
|
145
|
+
db: deps.db,
|
|
146
|
+
dispatchSystemWrite,
|
|
147
|
+
dispatchWriteAs: ({ handlerQn, payload, roles }) => deps.dispatcher.write(handlerQn, payload, createSystemUser(tenant.id, roles)),
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
const configApplied = await applyTenantConfig(deps, tenant);
|
|
151
|
+
tenants.push({ id: tenant.id, outcome, seeded: shouldSeed, configApplied });
|
|
152
|
+
}
|
|
153
|
+
const invites = [];
|
|
154
|
+
for (const invite of plannedInvites) {
|
|
155
|
+
const outcome = await decideInvite(deps, invite);
|
|
156
|
+
if (isMailingOutcome(outcome)) {
|
|
157
|
+
const result = await deps.dispatcher.write(AuthHandlers.systemInviteCreate, { email: invite.email, role: invite.role, globalRoles: invite.globalRoles }, createSystemUser(invite.tenantId));
|
|
158
|
+
if (!result.isSuccess) {
|
|
159
|
+
throw new BootstrapWriteError(`inviting ${invite.email} into tenant ${invite.tenantId} failed: ${result.error.code} — ${result.error.message}`);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
invites.push({
|
|
163
|
+
tenantId: invite.tenantId,
|
|
164
|
+
email: invite.email,
|
|
165
|
+
globalRoles: invite.globalRoles,
|
|
166
|
+
outcome,
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
return { tenants, invites };
|
|
170
|
+
}
|
|
@@ -13,6 +13,7 @@ export declare const AuthHandlers: {
|
|
|
13
13
|
readonly signupRequest: "auth-email-password:write:signup-request";
|
|
14
14
|
readonly signupConfirm: "auth-email-password:write:signup-confirm";
|
|
15
15
|
readonly inviteCreate: "auth-email-password:write:invite-create";
|
|
16
|
+
readonly systemInviteCreate: "auth-email-password:write:system-invite-create";
|
|
16
17
|
readonly inviteAccept: "auth-email-password:write:invite-accept";
|
|
17
18
|
readonly inviteAcceptWithLogin: "auth-email-password:write:invite-accept-with-login";
|
|
18
19
|
readonly inviteSignupComplete: "auth-email-password:write:invite-signup-complete";
|
|
@@ -38,6 +39,7 @@ export declare const AuthErrors: {
|
|
|
38
39
|
readonly invalidInviteToken: "invalid_invite_token";
|
|
39
40
|
readonly inviteEmailMismatch: "invite_email_mismatch";
|
|
40
41
|
readonly inviteAlreadyMember: "invite_already_member";
|
|
42
|
+
readonly inviteAlreadyAccepted: "invite_already_accepted";
|
|
41
43
|
readonly accountLocked: "account_locked";
|
|
42
44
|
readonly accountRestricted: "account_restricted";
|
|
43
45
|
};
|
|
@@ -38,6 +38,9 @@ export const AuthHandlers = {
|
|
|
38
38
|
// inviteSignupComplete: anon user with new email → POST token + password
|
|
39
39
|
// inviteCancel: admin cancels a pending invite
|
|
40
40
|
inviteCreate: "auth-email-password:write:invite-create",
|
|
41
|
+
// System-only variant (access.system) that may carry global roles — see
|
|
42
|
+
// invite-create.write.ts. Dispatched in-process by runBootstrap.
|
|
43
|
+
systemInviteCreate: "auth-email-password:write:system-invite-create",
|
|
41
44
|
inviteAccept: "auth-email-password:write:invite-accept",
|
|
42
45
|
inviteAcceptWithLogin: "auth-email-password:write:invite-accept-with-login",
|
|
43
46
|
inviteSignupComplete: "auth-email-password:write:invite-signup-complete",
|
|
@@ -92,6 +95,8 @@ export const AuthErrors = {
|
|
|
92
95
|
invalidInviteToken: "invalid_invite_token",
|
|
93
96
|
inviteEmailMismatch: "invite_email_mismatch",
|
|
94
97
|
inviteAlreadyMember: "invite_already_member",
|
|
98
|
+
// System invite (waitlist) refuses to reset an invitation the invitee already accepted.
|
|
99
|
+
inviteAlreadyAccepted: "invite_already_accepted",
|
|
95
100
|
// Account-lockout: login refuses with this code when the user's streak of
|
|
96
101
|
// failed attempts has crossed the configured threshold. The error detail
|
|
97
102
|
// carries `retryAfterSeconds` so the UI can show a countdown. Returning a
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export declare function invalidCredentials(): import("@cosmicdrift/kumiko-types/write-error-info-types").WriteFailure;
|
|
2
2
|
export declare function invalidInviteToken(): import("@cosmicdrift/kumiko-types/write-error-info-types").WriteFailure;
|
|
3
3
|
export declare function inviteEmailMismatch(): import("@cosmicdrift/kumiko-types/write-error-info-types").WriteFailure;
|
|
4
|
+
export declare function inviteAlreadyAccepted(): import("@cosmicdrift/kumiko-types/write-error-info-types").WriteFailure;
|
|
4
5
|
export declare function invalidResetToken(): import("@cosmicdrift/kumiko-types/write-error-info-types").WriteFailure;
|
|
5
6
|
export declare function invalidUnlockToken(): import("@cosmicdrift/kumiko-types/write-error-info-types").WriteFailure;
|
|
6
7
|
export declare function invalidVerificationToken(): import("@cosmicdrift/kumiko-types/write-error-info-types").WriteFailure;
|
|
@@ -19,6 +19,12 @@ export function inviteEmailMismatch() {
|
|
|
19
19
|
}));
|
|
20
20
|
}
|
|
21
21
|
// @wrapper-known error-helper
|
|
22
|
+
export function inviteAlreadyAccepted() {
|
|
23
|
+
return writeFailure(new UnprocessableError(AuthErrors.inviteAlreadyAccepted, {
|
|
24
|
+
i18nKey: "auth.errors.inviteAlreadyAccepted",
|
|
25
|
+
}));
|
|
26
|
+
}
|
|
27
|
+
// @wrapper-known error-helper
|
|
22
28
|
export function invalidResetToken() {
|
|
23
29
|
return writeFailure(new UnprocessableError(AuthErrors.invalidResetToken, {
|
|
24
30
|
i18nKey: "auth.errors.invalidResetToken",
|
|
@@ -5,7 +5,7 @@ import { changePasswordWrite } from "./handlers/change-password.write.js";
|
|
|
5
5
|
import { createConfirmAccountUnlockHandler } from "./handlers/confirm-account-unlock.write.js";
|
|
6
6
|
import { createInviteAcceptHandler } from "./handlers/invite-accept.write.js";
|
|
7
7
|
import { createInviteAcceptWithLoginHandler } from "./handlers/invite-accept-with-login.write.js";
|
|
8
|
-
import { createInviteCreateHandler, } from "./handlers/invite-create.write.js";
|
|
8
|
+
import { createInviteCreateHandler, createSystemInviteCreateHandler, } from "./handlers/invite-create.write.js";
|
|
9
9
|
import { inviteInfoQuery } from "./handlers/invite-info.query.js";
|
|
10
10
|
import { createInviteSignupCompleteHandler } from "./handlers/invite-signup-complete.write.js";
|
|
11
11
|
import { createLoginHandler } from "./handlers/login.write.js";
|
|
@@ -123,6 +123,7 @@ export function createAuthEmailPasswordFeature(opts = {}) {
|
|
|
123
123
|
}
|
|
124
124
|
if (opts.invite) {
|
|
125
125
|
r.writeHandler(createInviteCreateHandler(opts.invite));
|
|
126
|
+
r.writeHandler(createSystemInviteCreateHandler(opts.invite));
|
|
126
127
|
r.writeHandler(createInviteAcceptHandler());
|
|
127
128
|
// Same gates as the `login` handler above (lockout, password,
|
|
128
129
|
// email-verified, account-status, MFA) — see invite-accept-with-login.write.ts.
|
|
@@ -30,6 +30,8 @@ import { decryptStoredPii, sessionLocaleField, sessionTimezoneField } from "../.
|
|
|
30
30
|
import { INVITATION_STATUS, tenantInvitationEntity, tenantInvitationsTable, } from "../../tenant/invitation-table.js";
|
|
31
31
|
// kumiko-lint-ignore cross-feature-import membership grant for a privileged cross-tenant add
|
|
32
32
|
import { grantInvitedMembershipRole, invitationIssuedAt } from "../../tenant/invited-membership.js";
|
|
33
|
+
// kumiko-lint-ignore cross-feature-import global roles granted on invite accept live on the user row
|
|
34
|
+
import { grantInvitedGlobalRoles, parseInvitedGlobalRoles, } from "../../user/invited-global-roles.js";
|
|
33
35
|
// kumiko-lint-ignore cross-feature-import login-style password-check
|
|
34
36
|
import { userTable } from "../../user/schema/user.js";
|
|
35
37
|
import { AUTH_LOCKOUT_DEFAULT_DURATION_MINUTES, AUTH_LOCKOUT_DEFAULT_MAX_FAILED_ATTEMPTS, } from "../constants.js";
|
|
@@ -120,6 +122,12 @@ export function createInviteAcceptWithLoginHandler(opts = {}) {
|
|
|
120
122
|
});
|
|
121
123
|
if (!grant.isSuccess)
|
|
122
124
|
return grant;
|
|
125
|
+
const globalGrant = await grantInvitedGlobalRoles(dbConn, {
|
|
126
|
+
userId,
|
|
127
|
+
globalRoles: parseInvitedGlobalRoles(invitation.globalRoles),
|
|
128
|
+
});
|
|
129
|
+
if (!globalGrant.isSuccess)
|
|
130
|
+
return globalGrant;
|
|
123
131
|
// Invitation → accepted: TenantDb for the invitation's tenant.
|
|
124
132
|
const invitationTdb = createTenantDb(dbConn, invitationTenantId, "system");
|
|
125
133
|
const updateResult = await invitationExecutor.update({
|
|
@@ -132,7 +140,7 @@ export function createInviteAcceptWithLoginHandler(opts = {}) {
|
|
|
132
140
|
await deleteInviteToken(ctx.redis, { invitationId, token: event.payload.token });
|
|
133
141
|
// buildSessionRoles calls stripForbiddenMembershipRoles internally —
|
|
134
142
|
// a reserved role on an existing membership must never reach the session.
|
|
135
|
-
const mergedRoles = buildSessionRoles(
|
|
143
|
+
const mergedRoles = buildSessionRoles(globalGrant.data.roles, grant.data.roles);
|
|
136
144
|
// MFA gate runs after membership is granted (mirrors login.write.ts's
|
|
137
145
|
// own order: membership resolution before MFA) — a challenge halts
|
|
138
146
|
// the session mint, not the invite acceptance itself.
|
|
@@ -24,6 +24,8 @@ import { decryptStoredPii } from "../../shared/index.js";
|
|
|
24
24
|
import { INVITATION_STATUS, tenantInvitationEntity, tenantInvitationsTable, } from "../../tenant/invitation-table.js";
|
|
25
25
|
// kumiko-lint-ignore cross-feature-import membership grant for a privileged cross-tenant add (like provisionSignupAccount)
|
|
26
26
|
import { grantInvitedMembershipRole, invitationIssuedAt } from "../../tenant/invited-membership.js";
|
|
27
|
+
// kumiko-lint-ignore cross-feature-import global roles granted on invite accept live on the user row
|
|
28
|
+
import { grantInvitedGlobalRoles, parseInvitedGlobalRoles, } from "../../user/invited-global-roles.js";
|
|
27
29
|
// kumiko-lint-ignore cross-feature-import auth handler reads user-row für email-match
|
|
28
30
|
import { userTable } from "../../user/schema/user.js";
|
|
29
31
|
import { invalidInviteToken, inviteEmailMismatch } from "../errors.js";
|
|
@@ -96,6 +98,12 @@ export function createInviteAcceptHandler() {
|
|
|
96
98
|
if (!grant.isSuccess)
|
|
97
99
|
return grant;
|
|
98
100
|
const { alreadyMember } = grant.data;
|
|
101
|
+
const globalGrant = await grantInvitedGlobalRoles(dbConn, {
|
|
102
|
+
userId: event.user.id,
|
|
103
|
+
globalRoles: parseInvitedGlobalRoles(invitation.globalRoles),
|
|
104
|
+
});
|
|
105
|
+
if (!globalGrant.isSuccess)
|
|
106
|
+
return globalGrant;
|
|
99
107
|
// Invitation-Status → accepted via event-store-executor.
|
|
100
108
|
// Tenant-scoping: ctx.db ist auf event.user.tenantId gescopt
|
|
101
109
|
// (= NICHT der invitation-tenant). Eigene TenantDb für den
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import * as z from "zod";
|
|
2
|
+
import { GLOBAL_ROLE_OPTIONS } from "../../user/constants.js";
|
|
2
3
|
import type { AuthMailLocale } from "../email-templates.js";
|
|
4
|
+
type GlobalRole = (typeof GLOBAL_ROLE_OPTIONS)[number];
|
|
3
5
|
export type InviteCreateData = {
|
|
4
6
|
readonly kind: "invite-created";
|
|
5
7
|
readonly invitationId: string;
|
|
6
8
|
readonly tenantId: string;
|
|
7
9
|
readonly email: string;
|
|
8
10
|
readonly role: string;
|
|
11
|
+
readonly globalRoles: readonly GlobalRole[];
|
|
9
12
|
readonly expiresAt: string;
|
|
10
13
|
};
|
|
11
14
|
export type InviteCreateOptions = {
|
|
@@ -31,3 +34,11 @@ export declare function createInviteCreateHandler(opts: InviteCreateOptions): im
|
|
|
31
34
|
email: z.ZodEmail;
|
|
32
35
|
role: z.ZodString;
|
|
33
36
|
}, z.core.$strip>, InviteCreateData, import("@cosmicdrift/kumiko-types/event-type-map").KumikoEventTypeMap>;
|
|
37
|
+
export declare function createSystemInviteCreateHandler(opts: InviteCreateOptions): import("@cosmicdrift/kumiko-types/define-handler").WriteHandlerDefinition<"system-invite-create", z.ZodObject<{
|
|
38
|
+
email: z.ZodEmail;
|
|
39
|
+
role: z.ZodString;
|
|
40
|
+
globalRoles: z.ZodDefault<z.ZodArray<z.ZodEnum<{
|
|
41
|
+
SystemAdmin: "SystemAdmin";
|
|
42
|
+
}>>>;
|
|
43
|
+
}, z.core.$strip>, InviteCreateData, import("@cosmicdrift/kumiko-types/event-type-map").KumikoEventTypeMap>;
|
|
44
|
+
export {};
|