@cosmicdrift/kumiko-samples 0.341.0 → 0.343.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/packages/bundled-features/package.json +1 -1
- package/packages/bundled-features/src/audit/changes.json +6 -0
- package/packages/bundled-features/src/audit/constants.ts +9 -0
- package/packages/bundled-features/src/audit/escape-hatch-audit-sink.ts +1 -0
- package/packages/bundled-features/src/audit/feature.ts +47 -6
- package/packages/bundled-features/src/audit/run-escape-hatch-retention.ts +144 -0
- package/packages/bundled-features/src/auth-email-password/bootstrap.ts +18 -2
- package/packages/bundled-features/src/auth-email-password/changes.json +19 -0
- package/packages/bundled-features/src/auth-email-password/email-templates.ts +76 -16
- package/packages/bundled-features/src/auth-email-password/handlers/invite-create.write.ts +4 -1
- package/packages/bundled-features/src/auth-email-password/handlers/signup-request.write.ts +4 -1
- package/packages/bundled-features/src/auth-email-password/handlers/token-request-handler.ts +3 -0
- package/packages/bundled-features/src/auth-email-password/magic-link-mail.ts +4 -0
- package/packages/bundled-features/src/auth-email-password/web/auth-form-primitives.tsx +24 -6
- package/packages/bundled-features/src/auth-email-password/web/confirm-account-unlock-screen.tsx +12 -18
- package/packages/bundled-features/src/auth-email-password/web/forgot-password-screen.tsx +4 -4
- package/packages/bundled-features/src/auth-email-password/web/invite-accept-screen.tsx +65 -69
- package/packages/bundled-features/src/auth-email-password/web/login-screen.tsx +93 -95
- package/packages/bundled-features/src/auth-email-password/web/request-account-unlock-screen.tsx +4 -4
- package/packages/bundled-features/src/auth-email-password/web/reset-password-screen.tsx +8 -10
- package/packages/bundled-features/src/auth-email-password/web/session-bootstrap-error.tsx +13 -15
- package/packages/bundled-features/src/auth-email-password/web/signup-complete-screen.tsx +43 -49
- package/packages/bundled-features/src/auth-email-password/web/signup-screen.tsx +4 -4
- package/packages/bundled-features/src/auth-email-password/web/verify-email-screen.tsx +12 -18
- package/packages/bundled-features/src/auth-mfa/web/mfa-setup-preauth-screen.tsx +83 -85
- package/packages/bundled-features/src/auth-mfa/web/mfa-verify-screen.tsx +24 -26
- package/packages/bundled-features/src/channel-discord/feature.ts +1 -0
- package/packages/bundled-features/src/channel-email/changes.json +6 -0
- package/packages/bundled-features/src/channel-email/email-channel.ts +8 -1
- package/packages/bundled-features/src/channel-email/feature.ts +1 -0
- package/packages/bundled-features/src/channel-email/pii-guard.ts +6 -5
- package/packages/bundled-features/src/channel-email/smtp-transport.ts +27 -1
- package/packages/bundled-features/src/channel-email/types.ts +4 -0
- package/packages/bundled-features/src/channel-in-app/feature.ts +1 -0
- package/packages/bundled-features/src/channel-push/feature.ts +1 -0
- package/packages/bundled-features/src/channel-slack/feature.ts +1 -0
- package/packages/bundled-features/src/channel-teams/changes.json +8 -0
- package/packages/bundled-features/src/channel-teams/feature.ts +1 -0
- package/packages/bundled-features/src/channel-teams/teams-channel.ts +16 -0
- package/packages/bundled-features/src/channel-telegram/feature.ts +1 -0
- package/packages/bundled-features/src/delivery/changes.json +32 -0
- package/packages/bundled-features/src/delivery/chat-webhook-channel.ts +7 -2
- package/packages/bundled-features/src/delivery/chat-webhook-sender.ts +60 -14
- package/packages/bundled-features/src/delivery/constants.ts +4 -0
- package/packages/bundled-features/src/delivery/delivery-service.ts +70 -5
- package/packages/bundled-features/src/delivery/events.ts +15 -1
- package/packages/bundled-features/src/delivery/feature.ts +32 -4
- package/packages/bundled-features/src/delivery/handlers/log.query.ts +65 -20
- package/packages/bundled-features/src/delivery/i18n.ts +55 -0
- package/packages/bundled-features/src/delivery/index.ts +2 -0
- package/packages/bundled-features/src/delivery/jobs.ts +43 -20
- package/packages/bundled-features/src/delivery/notify-factory.ts +19 -0
- package/packages/bundled-features/src/delivery/public-names.ts +4 -0
- package/packages/bundled-features/src/delivery/tables.ts +3 -0
- package/packages/bundled-features/src/delivery/testing.ts +5 -9
- package/packages/bundled-features/src/delivery/types.ts +6 -2
- package/packages/bundled-features/src/delivery/web/client-plugin.tsx +16 -1
- package/packages/bundled-features/src/delivery/web/delivery-channel-cell.tsx +13 -0
- package/packages/bundled-features/src/delivery/web/delivery-error-cell.tsx +17 -0
- package/packages/bundled-features/src/delivery/web/delivery-status-cell.tsx +10 -3
- package/packages/bundled-features/src/delivery/web/delivery-time-cell.tsx +8 -0
- package/packages/bundled-features/src/delivery/web/delivery-type-cell.tsx +20 -0
- package/packages/bundled-features/src/jobs/changes.json +6 -0
- package/packages/bundled-features/src/secrets/changes.json +6 -0
- package/packages/bundled-features/src/secrets/constants.ts +10 -0
- package/packages/bundled-features/src/secrets/feature.ts +4 -0
- package/packages/bundled-features/src/secrets/i18n.ts +5 -3
- package/packages/bundled-features/src/secrets/index.ts +8 -1
- package/packages/bundled-features/src/secrets/write-gate.ts +17 -5
- package/packages/bundled-features/src/shared/index.ts +6 -0
- package/packages/bundled-features/src/shared/redact.ts +27 -0
- package/packages/bundled-features/src/step-dispatcher/feature.ts +2 -6
- package/packages/bundled-features/src/step-dispatcher/mail-runner.ts +0 -6
- package/packages/bundled-features/src/tenant/web/member-roles-cell.tsx +1 -1
- package/packages/bundled-features/src/tenant/web/member-status-cell.tsx +1 -1
- package/packages/bundled-features/src/tenant-lifecycle/feature.ts +12 -2
- package/packages/bundled-features/src/tenant-lifecycle/run-tenant-destroy.ts +23 -8
- package/packages/bundled-features/src/user/index.ts +1 -0
- package/packages/bundled-features/src/user/resolve-display-names.ts +35 -0
- package/packages/bundled-features/src/waitlist/changes.json +6 -0
- package/packages/bundled-features/src/waitlist/handlers/submit.write.ts +1 -1
- package/packages/bundled-features/src/waitlist/options.ts +3 -2
- package/samples/apps/use-all-bundled/feature-manifest.json +27 -2
- /package/packages/bundled-features/src/{tenant → shared}/web/translate-or-raw.ts +0 -0
|
@@ -31,9 +31,3 @@ export function setMailRunner(fn: (spec: MailSpec) => Promise<MailDispatchResult
|
|
|
31
31
|
export async function performMailDispatch(spec: MailSpec): Promise<MailDispatchResult> {
|
|
32
32
|
return mailRunner(spec);
|
|
33
33
|
}
|
|
34
|
-
|
|
35
|
-
// Adapter errors usually quote the rejected recipient ("550 <ops@example.com> rejected"); the
|
|
36
|
-
// address must not outlive the crypto-shredded dispatch payload in logs.
|
|
37
|
-
export function redactEmailAddresses(text: string): string {
|
|
38
|
-
return text.replace(/[^\s<>,;"']+@[^\s<>,;"']+/g, "[redacted-address]");
|
|
39
|
-
}
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
import { type ColumnRendererProps, useTranslation } from "@cosmicdrift/kumiko-renderer";
|
|
7
7
|
import type { ReactNode } from "react";
|
|
8
|
-
import { translateOrRaw } from "
|
|
8
|
+
import { translateOrRaw } from "../../shared/web/translate-or-raw.js";
|
|
9
9
|
|
|
10
10
|
export function MemberRolesCell({ row }: ColumnRendererProps): ReactNode {
|
|
11
11
|
const t = useTranslation();
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
import { type ColumnRendererProps, useTranslation } from "@cosmicdrift/kumiko-renderer";
|
|
8
8
|
import { StatusBadge, type StatusTone } from "@cosmicdrift/kumiko-renderer-web";
|
|
9
9
|
import type { ReactNode } from "react";
|
|
10
|
-
import { translateOrRaw } from "
|
|
10
|
+
import { translateOrRaw } from "../../shared/web/translate-or-raw.js";
|
|
11
11
|
|
|
12
12
|
const STATUS_TONE: Readonly<Record<string, StatusTone>> = {
|
|
13
13
|
active: "ok",
|
|
@@ -34,7 +34,11 @@ import {
|
|
|
34
34
|
} from "./events.js";
|
|
35
35
|
import { cancelDestructionWrite } from "./handlers/cancel-destruction.write.js";
|
|
36
36
|
import { requestDestructionWrite } from "./handlers/request-destruction.write.js";
|
|
37
|
-
import {
|
|
37
|
+
import {
|
|
38
|
+
hasTenantDestructionWork,
|
|
39
|
+
resolveTenantLifecycleGate,
|
|
40
|
+
runTenantDestructionSweep,
|
|
41
|
+
} from "./run-tenant-destroy.js";
|
|
38
42
|
|
|
39
43
|
const tenantLifecycleStatusPlugin: TenantLifecycleStatusPlugin = {
|
|
40
44
|
async resolveStatus(tenantId, { db }) {
|
|
@@ -125,10 +129,16 @@ export function createTenantLifecycleFeature(): FeatureDefinition {
|
|
|
125
129
|
throw new Error("run-tenant-destruction: ctx.registry required (JobContext incomplete)");
|
|
126
130
|
}
|
|
127
131
|
const T = (await import("@cosmicdrift/kumiko-framework/time")).getTemporal();
|
|
132
|
+
const now = T.Now.instant();
|
|
133
|
+
// Without crossTenantReads (undeclared hatch) fall through to the sweep.
|
|
134
|
+
if (ctx.crossTenantReads && !(await hasTenantDestructionWork(ctx.crossTenantReads, now))) {
|
|
135
|
+
// skip: no tenant is due, so the sweep would not touch anything
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
128
138
|
await runTenantDestructionSweep({
|
|
129
139
|
db: ctx.db.unsafeRaw() as import("@cosmicdrift/kumiko-framework/db").DbConnection, // @cast-boundary db-operator — jobs never run inside a DbTx
|
|
130
140
|
registry: ctx.registry,
|
|
131
|
-
now
|
|
141
|
+
now,
|
|
132
142
|
log: (message) => ctx.log?.warn(message),
|
|
133
143
|
fileProviderResolver: ctx._fileProviderResolver,
|
|
134
144
|
escapeHatchAuditSink: ctx._escapeHatchAuditSink,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/// <reference types="temporal-polyfill/global" preserve="true" />
|
|
2
2
|
import { selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
3
|
-
import type { DbRunner } from "@cosmicdrift/kumiko-framework/db";
|
|
3
|
+
import type { DbRunner, TenantDb, WhereObject } from "@cosmicdrift/kumiko-framework/db";
|
|
4
4
|
import { createEventStoreExecutor, createTenantDb } from "@cosmicdrift/kumiko-framework/db";
|
|
5
5
|
import {
|
|
6
6
|
createSystemUser,
|
|
@@ -307,6 +307,26 @@ export async function runNextDestructionStage(args: {
|
|
|
307
307
|
}
|
|
308
308
|
}
|
|
309
309
|
|
|
310
|
+
function destroyRequestedDueWhere(now: Temporal.Instant): WhereObject {
|
|
311
|
+
return {
|
|
312
|
+
status: "destroyRequested",
|
|
313
|
+
gracePeriodEnd: { lte: now.subtract({ milliseconds: TEARDOWN_GATE_SETTLE_MS }) },
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const destroyingWhere: WhereObject = { status: "destroying" };
|
|
318
|
+
|
|
319
|
+
export async function hasTenantDestructionWork(
|
|
320
|
+
reads: Pick<TenantDb, "count">,
|
|
321
|
+
now: Temporal.Instant,
|
|
322
|
+
): Promise<boolean> {
|
|
323
|
+
const [due, destroying] = await Promise.all([
|
|
324
|
+
reads.count(tenantTable, destroyRequestedDueWhere(now)),
|
|
325
|
+
reads.count(tenantTable, destroyingWhere),
|
|
326
|
+
]);
|
|
327
|
+
return due > 0 || destroying > 0;
|
|
328
|
+
}
|
|
329
|
+
|
|
310
330
|
/** Cron entry: start destruction for tenants past grace, then advance stages. */
|
|
311
331
|
export async function runTenantDestructionSweep(args: {
|
|
312
332
|
readonly db: DbRunner;
|
|
@@ -319,10 +339,7 @@ export async function runTenantDestructionSweep(args: {
|
|
|
319
339
|
}): Promise<{ readonly triggered: number; readonly advanced: number }> {
|
|
320
340
|
const T = getTemporal();
|
|
321
341
|
const now = args.now ?? T.Now.instant();
|
|
322
|
-
const due = await selectMany<{ id: string }>(args.db, tenantTable,
|
|
323
|
-
status: "destroyRequested",
|
|
324
|
-
gracePeriodEnd: { lte: now.subtract({ milliseconds: TEARDOWN_GATE_SETTLE_MS }) },
|
|
325
|
-
});
|
|
342
|
+
const due = await selectMany<{ id: string }>(args.db, tenantTable, destroyRequestedDueWhere(now));
|
|
326
343
|
|
|
327
344
|
let triggered = 0;
|
|
328
345
|
for (const row of due) {
|
|
@@ -354,9 +371,7 @@ export async function runTenantDestructionSweep(args: {
|
|
|
354
371
|
}
|
|
355
372
|
|
|
356
373
|
let advanced = 0;
|
|
357
|
-
const destroying = await selectMany<{ id: string }>(args.db, tenantTable,
|
|
358
|
-
status: "destroying",
|
|
359
|
-
});
|
|
374
|
+
const destroying = await selectMany<{ id: string }>(args.db, tenantTable, destroyingWhere);
|
|
360
375
|
for (const row of destroying) {
|
|
361
376
|
try {
|
|
362
377
|
const result = await runNextDestructionStage({
|
|
@@ -6,6 +6,7 @@ export {
|
|
|
6
6
|
} from "./db/queries/stream-tenant-backfill.js";
|
|
7
7
|
export { createUserFeature } from "./feature.js";
|
|
8
8
|
export { isPrincipalBlocked, principalStatusPlugin } from "./principal-status.js";
|
|
9
|
+
export { resolveUserDisplayNames } from "./resolve-display-names.js";
|
|
9
10
|
export type { UserStatus } from "./schema/user.js";
|
|
10
11
|
export {
|
|
11
12
|
USER_ANONYMIZED_DISPLAY_NAME,
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
2
|
+
import type { TenantDb } from "@cosmicdrift/kumiko-framework/db";
|
|
3
|
+
import { decryptStoredPii, mapWithConcurrency } from "../shared/index.js";
|
|
4
|
+
import { userTable } from "./schema/user.js";
|
|
5
|
+
|
|
6
|
+
// Shares the KMS adapter's small dedicated pool, same as member-directory.
|
|
7
|
+
const KMS_POOL_CONCURRENCY = 4;
|
|
8
|
+
|
|
9
|
+
// One batched lookup for a page of user ids. Users that are missing, have no
|
|
10
|
+
// display name, or whose name cannot be decrypted (e.g. crypto-shredded) are
|
|
11
|
+
// left out, so callers fall back to the raw id instead of failing the page.
|
|
12
|
+
export async function resolveUserDisplayNames(
|
|
13
|
+
db: TenantDb,
|
|
14
|
+
userIds: readonly string[],
|
|
15
|
+
): Promise<ReadonlyMap<string, string>> {
|
|
16
|
+
const names = new Map<string, string>();
|
|
17
|
+
if (userIds.length === 0) return names;
|
|
18
|
+
const users = await selectMany(db, userTable, { id: [...userIds] });
|
|
19
|
+
const named = users.filter((user) => typeof user.displayName === "string");
|
|
20
|
+
await mapWithConcurrency(named, KMS_POOL_CONCURRENCY, async (user) => {
|
|
21
|
+
try {
|
|
22
|
+
names.set(
|
|
23
|
+
String(user.id),
|
|
24
|
+
await decryptStoredPii(
|
|
25
|
+
String(user.displayName),
|
|
26
|
+
"displayName",
|
|
27
|
+
"user:resolve-display-names",
|
|
28
|
+
),
|
|
29
|
+
);
|
|
30
|
+
} catch {
|
|
31
|
+
// Unreadable name: the caller's fallback applies.
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
return names;
|
|
35
|
+
}
|
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.342.0",
|
|
4
|
+
"type": "improvement",
|
|
5
|
+
"title": "Waitlist notifyRecipient may return a promise",
|
|
6
|
+
"detail": "Waitlist `notifyRecipient` may return a promise, so apps can read the admin address from config"
|
|
7
|
+
},
|
|
2
8
|
{
|
|
3
9
|
"version": "0.341.0",
|
|
4
10
|
"type": "improvement",
|
|
@@ -140,7 +140,7 @@ export function createSubmitHandler(opts: WaitlistOptions) {
|
|
|
140
140
|
);
|
|
141
141
|
|
|
142
142
|
await sendBestEffort(ctx, "admin notice", async (notify) => {
|
|
143
|
-
const adminRecipient = opts.notifyRecipient?.(ctx);
|
|
143
|
+
const adminRecipient = await opts.notifyRecipient?.(ctx);
|
|
144
144
|
// skip: no admin recipient configured
|
|
145
145
|
if (!adminRecipient) return;
|
|
146
146
|
await notify(WAITLIST_NOTIFICATION_TYPES.adminNotice, {
|
|
@@ -17,8 +17,9 @@ export type WaitlistInviteOptions =
|
|
|
17
17
|
|
|
18
18
|
export type WaitlistOptions = {
|
|
19
19
|
/** Address for an admin notification mail on each new entry. Null, an empty
|
|
20
|
-
* string or an absent option sends none. Best effort, never fails the submit
|
|
21
|
-
|
|
20
|
+
* string or an absent option sends none. Best effort, never fails the submit,
|
|
21
|
+
* also when the resolver throws or rejects. */
|
|
22
|
+
readonly notifyRecipient?: (ctx: HandlerContext) => string | null | Promise<string | null>;
|
|
22
23
|
/** Default `{ mode: "own-tenant", role: "TenantAdmin" }`. */
|
|
23
24
|
readonly invite?: WaitlistInviteOptions;
|
|
24
25
|
/** Default: 5 submits per IP per 10 minutes. */
|
|
@@ -55,14 +55,39 @@
|
|
|
55
55
|
"toggleableDefault": null,
|
|
56
56
|
"requires": [
|
|
57
57
|
"tenant",
|
|
58
|
-
"user"
|
|
58
|
+
"user",
|
|
59
|
+
"config"
|
|
59
60
|
],
|
|
60
61
|
"optionalRequires": [],
|
|
61
62
|
"configReads": [],
|
|
62
63
|
"exposesApis": [],
|
|
63
64
|
"usesApis": [],
|
|
64
65
|
"extensionsUsed": [],
|
|
65
|
-
"configKeys": [
|
|
66
|
+
"configKeys": [
|
|
67
|
+
{
|
|
68
|
+
"key": "escape-hatch-retention-days",
|
|
69
|
+
"qualifiedName": "audit:config:escape-hatch-retention-days",
|
|
70
|
+
"type": "number",
|
|
71
|
+
"scope": "system",
|
|
72
|
+
"default": 90,
|
|
73
|
+
"encrypted": false,
|
|
74
|
+
"computed": false,
|
|
75
|
+
"validated": false,
|
|
76
|
+
"options": null,
|
|
77
|
+
"bounds": {
|
|
78
|
+
"min": 1
|
|
79
|
+
},
|
|
80
|
+
"pattern": null,
|
|
81
|
+
"writeRoles": [
|
|
82
|
+
"system"
|
|
83
|
+
],
|
|
84
|
+
"readRoles": [
|
|
85
|
+
"TenantAdmin",
|
|
86
|
+
"Admin",
|
|
87
|
+
"SystemAdmin"
|
|
88
|
+
]
|
|
89
|
+
}
|
|
90
|
+
],
|
|
66
91
|
"secrets": [],
|
|
67
92
|
"writeHandlers": [],
|
|
68
93
|
"uiHints": {
|
|
File without changes
|