@cosmicdrift/kumiko-bundled-features 0.158.2 → 0.160.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 +7 -6
- package/src/auth-email-password/__tests__/account-unlock.integration.test.ts +283 -0
- package/src/auth-email-password/__tests__/auth-user-row.test.ts +36 -0
- package/src/auth-email-password/__tests__/auth.integration.test.ts +21 -0
- package/src/auth-email-password/__tests__/email-verification.integration.test.ts +1 -1
- package/src/auth-email-password/__tests__/password-policy.test.ts +15 -0
- package/src/auth-email-password/__tests__/password-reset.integration.test.ts +15 -1
- package/src/auth-email-password/__tests__/session-strict-mode.integration.test.ts +7 -14
- package/src/auth-email-password/__tests__/signup-flow.integration.test.ts +12 -0
- package/src/auth-email-password/constants.ts +15 -0
- package/src/auth-email-password/email-templates.ts +32 -0
- package/src/auth-email-password/errors.ts +9 -0
- package/src/auth-email-password/feature.ts +37 -5
- package/src/auth-email-password/handlers/change-password.write.ts +2 -1
- package/src/auth-email-password/handlers/confirm-account-unlock.write.ts +59 -0
- package/src/auth-email-password/handlers/invite-accept-with-login.write.ts +1 -0
- package/src/auth-email-password/handlers/invite-accept.write.ts +6 -2
- package/src/auth-email-password/handlers/invite-signup-complete.write.ts +1 -0
- package/src/auth-email-password/handlers/request-account-unlock.write.ts +38 -0
- package/src/auth-email-password/handlers/reset-password.write.ts +2 -1
- package/src/auth-email-password/handlers/signup-confirm.write.ts +2 -1
- package/src/auth-email-password/i18n.ts +42 -0
- package/src/auth-email-password/index.ts +2 -0
- package/src/auth-email-password/lockout-store.ts +10 -5
- package/src/auth-email-password/password-policy.ts +2108 -0
- package/src/auth-email-password/signed-token.ts +1 -0
- package/src/auth-email-password/unlock-token.ts +28 -0
- package/src/auth-email-password/web/__tests__/auth-client.test.ts +525 -0
- package/src/auth-email-password/web/__tests__/auth-gate.test.tsx +65 -3
- package/src/auth-email-password/web/__tests__/confirm-account-unlock-screen.test.tsx +57 -0
- package/src/auth-email-password/web/__tests__/invite-accept-screen.test.tsx +126 -2
- package/src/auth-email-password/web/__tests__/login-screen.test.tsx +125 -0
- package/src/auth-email-password/web/__tests__/request-account-unlock-screen.test.tsx +131 -0
- package/src/auth-email-password/web/auth-client.ts +33 -0
- package/src/auth-email-password/web/confirm-account-unlock-screen.tsx +95 -0
- package/src/auth-email-password/web/index.ts +6 -0
- package/src/auth-email-password/web/login-screen.tsx +15 -0
- package/src/auth-email-password/web/request-account-unlock-screen.tsx +112 -0
- package/src/auth-foundation/__tests__/feature.test.ts +33 -0
- package/src/auth-foundation/__tests__/session-store.test.ts +78 -0
- package/src/auth-foundation/__tests__/token-verifier.test.ts +117 -0
- package/src/auth-foundation/boot-checks.ts +91 -0
- package/src/auth-foundation/feature.ts +103 -0
- package/src/auth-foundation/index.ts +23 -0
- package/src/auth-foundation/types.ts +107 -0
- package/src/auth-mfa/__tests__/errors-i18n-parity.test.ts +8 -0
- package/src/auth-mfa/__tests__/verify.integration.test.ts +44 -0
- package/src/auth-mfa/handlers/verify.write.ts +1 -1
- package/src/auth-mfa/web/__tests__/mfa-client.test.ts +118 -0
- package/src/auth-mfa/web/__tests__/mfa-enable-screen.test.tsx +173 -0
- package/src/auth-mfa/web/__tests__/mfa-verify-screen.test.tsx +100 -0
- package/src/billing-foundation/__tests__/subscription-provider-contract.ts +69 -0
- package/src/channel-email/pii-guard.ts +6 -3
- package/src/config/__tests__/app-overrides.test.ts +11 -0
- package/src/config/__tests__/backing-secrets.integration.test.ts +18 -0
- package/src/config/__tests__/cascade.integration.test.ts +39 -0
- package/src/config/__tests__/config.integration.test.ts +14 -0
- package/src/config/__tests__/pii-encrypted.integration.test.ts +23 -2
- package/src/config/__tests__/reencrypt-job-concurrent-write-race.integration.test.ts +20 -3
- package/src/config/__tests__/reencrypt-job-kek-rotation.integration.test.ts +2 -3
- package/src/config/feature.ts +2 -4
- package/src/config/handlers/set.write.ts +7 -1
- package/src/config/resolver.ts +1 -1
- package/src/custom-fields/__tests__/user-data-rights.integration.test.ts +2 -0
- package/src/delivery/handlers/log.query.ts +1 -1
- package/src/files-provider-s3/__tests__/s3-provider.integration.test.ts +7 -0
- package/src/folders/web/__tests__/folder-manager.test.tsx +27 -2
- package/src/inbound-mail-foundation/__tests__/account-state.test.ts +30 -0
- package/src/inbound-mail-foundation/__tests__/inbound-mail-foundation.integration.test.ts +57 -6
- package/src/inbound-mail-foundation/__tests__/inbound-mail-provider-contract.ts +93 -0
- package/src/inbound-mail-foundation/__tests__/watch-supervisor.integration.test.ts +154 -0
- package/src/inbound-mail-foundation/handlers/ingest-message.write.ts +60 -50
- package/src/inbound-provider-imap/__tests__/imap-live.integration.test.ts +12 -0
- package/src/inbound-provider-imap/__tests__/plugin-mocked.test.ts +331 -0
- package/src/inbound-provider-inmemory/__tests__/feature.test.ts +48 -0
- package/src/inbound-provider-inmemory/feature.ts +2 -2
- package/src/jobs/__tests__/reindex-entity-job.integration.test.ts +53 -1
- package/src/jobs/handlers/detail.query.ts +1 -1
- package/src/jobs/handlers/list.query.ts +1 -1
- package/src/mail-foundation/__tests__/mail-transport-contract.ts +42 -0
- package/src/mail-transport-inmemory/__tests__/feature.test.ts +19 -0
- package/src/mail-transport-smtp/__tests__/feature.test.ts +60 -0
- package/src/managed-pages/handlers/branding.query.ts +5 -1
- package/src/personal-access-tokens/__tests__/feature.test.ts +18 -0
- package/src/personal-access-tokens/__tests__/pat.integration.test.ts +8 -11
- package/src/personal-access-tokens/feature.ts +23 -8
- package/src/personal-access-tokens/handlers/list.query.ts +1 -1
- package/src/personal-access-tokens/index.ts +0 -14
- package/src/personal-access-tokens/resolver.ts +2 -2
- package/src/secrets/__tests__/rotate.integration.test.ts +31 -0
- package/src/sessions/__tests__/cleanup.integration.test.ts +2 -1
- package/src/sessions/__tests__/password-auto-revoke.integration.test.ts +5 -3
- package/src/sessions/__tests__/rebuild-survival.integration.test.ts +11 -2
- package/src/sessions/__tests__/session-store-registration.test.ts +21 -0
- package/src/sessions/__tests__/sessions-screens.boot.test.ts +10 -1
- package/src/sessions/__tests__/sessions.integration.test.ts +28 -17
- package/src/sessions/__tests__/test-helpers.ts +15 -2
- package/src/sessions/feature.ts +31 -3
- package/src/sessions/handlers/detail.query.ts +2 -2
- package/src/sessions/handlers/list.query.ts +4 -2
- package/src/sessions/handlers/mine.query.ts +4 -2
- package/src/sessions/testing.ts +14 -1
- package/src/shared/__tests__/chunked-entity-migration.test.ts +145 -0
- package/src/shared/decrypt-stored-pii.ts +12 -5
- package/src/subscription-mollie/__tests__/feature.test.ts +87 -0
- package/src/subscription-stripe/__tests__/feature.test.ts +120 -1
- package/src/tags/web/__tests__/tag-manager.test.tsx +277 -8
- package/src/tenant/__tests__/members-query-kms.integration.test.ts +98 -0
- package/src/tenant/handlers/invitations.query.ts +4 -2
- package/src/tenant/handlers/members.query.ts +2 -2
- package/src/tenant/seeding.ts +2 -2
- package/src/tenant-lifecycle/__tests__/tenant-lifecycle.integration.test.ts +2 -0
- package/src/tenant-lifecycle/boot-checks.ts +53 -0
- package/src/tenant-lifecycle/feature.ts +7 -0
- package/src/tenant-lifecycle/handlers/request-destruction.write.ts +1 -1
- package/src/tier-engine/feature.ts +2 -2
- package/src/user-data-rights/COMPLIANCE.md +20 -0
- package/src/user-data-rights/__tests__/anonymous-deletion-kms.integration.test.ts +2 -0
- package/src/user-data-rights/__tests__/anonymous-deletion.integration.test.ts +3 -0
- package/src/user-data-rights/__tests__/audit-log.integration.test.ts +2 -0
- package/src/user-data-rights/__tests__/boot-checks.test.ts +172 -0
- package/src/user-data-rights/__tests__/cross-data-matrix.integration.test.ts +2 -0
- package/src/user-data-rights/__tests__/download-by-token-resolver-trust.integration.test.ts +2 -0
- package/src/user-data-rights/__tests__/download.integration.test.ts +59 -4
- package/src/user-data-rights/__tests__/export-encrypted-fields.integration.test.ts +2 -0
- package/src/user-data-rights/__tests__/export-job-idempotency.integration.test.ts +2 -0
- package/src/user-data-rights/__tests__/export-job-list-cross-tenant.integration.test.ts +2 -0
- package/src/user-data-rights/__tests__/file-binary-forget-cleanup.integration.test.ts +2 -0
- package/src/user-data-rights/__tests__/file-binary-forget-failure.integration.test.ts +2 -0
- package/src/user-data-rights/__tests__/file-retention.integration.test.ts +2 -0
- package/src/user-data-rights/__tests__/file-storage-unification.integration.test.ts +2 -0
- package/src/user-data-rights/__tests__/forget-cleanup-hook-ordering.integration.test.ts +2 -0
- package/src/user-data-rights/__tests__/forget-hook-registry.integration.test.ts +2 -0
- package/src/user-data-rights/__tests__/inspector-screens.boot.test.ts +10 -1
- package/src/user-data-rights/__tests__/mail-default-bridge.integration.test.ts +2 -0
- package/src/user-data-rights/__tests__/read-users-rebuild-survives-lifecycle.integration.test.ts +15 -7
- package/src/user-data-rights/__tests__/request-cancel-deletion.integration.test.ts +2 -0
- package/src/user-data-rights/__tests__/request-deletion-callback.integration.test.ts +2 -0
- package/src/user-data-rights/__tests__/request-export.integration.test.ts +2 -0
- package/src/user-data-rights/__tests__/restriction-flow.integration.test.ts +48 -15
- package/src/user-data-rights/__tests__/run-export-jobs-cron-context.integration.test.ts +2 -0
- package/src/user-data-rights/__tests__/run-export-jobs.integration.test.ts +184 -1
- package/src/user-data-rights/__tests__/run-forget-cleanup.integration.test.ts +2 -0
- package/src/user-data-rights/__tests__/run-user-export.integration.test.ts +2 -0
- package/src/user-data-rights/__tests__/tenant-model-erasure.integration.test.ts +2 -0
- package/src/user-data-rights/__tests__/user-data-rights.integration.test.ts +9 -1
- package/src/user-data-rights/boot-checks.ts +86 -0
- package/src/user-data-rights/feature.ts +116 -6
- package/src/user-data-rights/handlers/deletion-grace-period.ts +1 -1
- package/src/user-data-rights/handlers/download-by-token.query.ts +1 -1
- package/src/user-data-rights/handlers/lift-restriction.write.ts +13 -19
- package/src/user-data-rights/handlers/restrict-account.write.ts +30 -8
- package/src/user-data-rights/run-export-jobs.ts +3 -3
- package/src/user-data-rights/run-forget-cleanup.ts +1 -1
- package/src/user-data-rights/web/__tests__/privacy-center-screen.test.tsx +1 -0
- package/src/user-data-rights-defaults/__tests__/bundled-entities.userdata-hooks.integration.test.ts +3 -0
- package/src/user-data-rights-defaults/__tests__/feature-mounted.test.ts +24 -0
- package/src/user-data-rights-defaults/__tests__/user-data-rights-defaults.integration.test.ts +2 -0
- package/src/user-data-rights-defaults/hooks/tenant-invitation.userdata-hook.ts +1 -1
- package/src/user-profile/__tests__/change-email.integration.test.ts +2 -0
- package/src/user-profile/__tests__/profile-screen.test.tsx +1 -0
- package/src/user-profile/handlers/change-email.write.ts +1 -1
- package/src/config/__tests__/encrypted-legacy-migration.integration.test.ts +0 -179
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import type { EmailMessage } from "@cosmicdrift/kumiko-bundled-features/channel-email";
|
|
3
|
+
import type { MailTransportContext, MailTransportPlugin } from "../feature";
|
|
4
|
+
|
|
5
|
+
export type MailTransportContractFixture = {
|
|
6
|
+
readonly plugin: MailTransportPlugin;
|
|
7
|
+
readonly ctx: MailTransportContext;
|
|
8
|
+
readonly tenantId: string;
|
|
9
|
+
// Local providers (inmemory) expose readBack to assert real delivery;
|
|
10
|
+
// remote providers (smtp) omit it — send() needs a live server.
|
|
11
|
+
readonly readBack?: (tenantId: string) => readonly EmailMessage[];
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export function describeMailTransportContract(
|
|
15
|
+
name: string,
|
|
16
|
+
factory: () => MailTransportContractFixture | Promise<MailTransportContractFixture>,
|
|
17
|
+
): void {
|
|
18
|
+
describe(`${name} — MailTransportPlugin contract`, () => {
|
|
19
|
+
let fixture: MailTransportContractFixture;
|
|
20
|
+
|
|
21
|
+
beforeEach(async () => {
|
|
22
|
+
fixture = await factory();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test("build resolves to an EmailTransport with a send function", async () => {
|
|
26
|
+
const transport = await fixture.plugin.build(fixture.ctx, fixture.tenantId);
|
|
27
|
+
expect(typeof transport.send).toBe("function");
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test("send delivers the message — verified via readBack", async () => {
|
|
31
|
+
if (!fixture.readBack) return;
|
|
32
|
+
const message: EmailMessage = {
|
|
33
|
+
to: "contract-recipient@example.test",
|
|
34
|
+
subject: "contract-test-subject",
|
|
35
|
+
html: "<p>contract-test-body</p>",
|
|
36
|
+
};
|
|
37
|
+
const transport = await fixture.plugin.build(fixture.ctx, fixture.tenantId);
|
|
38
|
+
await transport.send(message);
|
|
39
|
+
expect(fixture.readBack(fixture.tenantId)).toContainEqual(message);
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
}
|
|
@@ -1,8 +1,27 @@
|
|
|
1
1
|
// feature.ts contract tests for mail-transport-inmemory.
|
|
2
2
|
|
|
3
3
|
import { describe, expect, test } from "bun:test";
|
|
4
|
+
import { isMailTransportPlugin, type MailTransportPlugin } from "../../mail-foundation";
|
|
5
|
+
import { describeMailTransportContract } from "../../mail-foundation/__tests__/mail-transport-contract";
|
|
4
6
|
import { clearInbox, getInbox, mailTransportInMemoryFeature } from "../feature";
|
|
5
7
|
|
|
8
|
+
function registeredPlugin(): MailTransportPlugin {
|
|
9
|
+
const usage = mailTransportInMemoryFeature.extensionUsages.find(
|
|
10
|
+
(u) => u.extensionName === "mailTransport" && u.entityName === "inmemory",
|
|
11
|
+
);
|
|
12
|
+
if (!usage || !isMailTransportPlugin(usage.options)) {
|
|
13
|
+
throw new Error("mail-transport-inmemory: plugin not registered under 'inmemory'");
|
|
14
|
+
}
|
|
15
|
+
return usage.options;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
describeMailTransportContract("mail-transport-inmemory", () => ({
|
|
19
|
+
plugin: registeredPlugin(),
|
|
20
|
+
ctx: {},
|
|
21
|
+
tenantId: "contract-test-tenant",
|
|
22
|
+
readBack: getInbox,
|
|
23
|
+
}));
|
|
24
|
+
|
|
6
25
|
describe("mailTransportInMemoryFeature — shape", () => {
|
|
7
26
|
test("has the expected name", () => {
|
|
8
27
|
expect(mailTransportInMemoryFeature.name).toBe("mail-transport-inmemory");
|
|
@@ -4,8 +4,68 @@
|
|
|
4
4
|
// build-fn presence).
|
|
5
5
|
|
|
6
6
|
import { describe, expect, test } from "bun:test";
|
|
7
|
+
import type { ConfigAccessor } from "@cosmicdrift/kumiko-framework/engine";
|
|
8
|
+
import type { SecretsContext } from "@cosmicdrift/kumiko-framework/secrets";
|
|
9
|
+
import { createSecret } from "@cosmicdrift/kumiko-framework/secrets";
|
|
10
|
+
import { isMailTransportPlugin, type MailTransportPlugin } from "../../mail-foundation";
|
|
11
|
+
import { describeMailTransportContract } from "../../mail-foundation/__tests__/mail-transport-contract";
|
|
7
12
|
import { mailTransportSmtpFeature, SMTP_PASSWORD } from "../feature";
|
|
8
13
|
|
|
14
|
+
function registeredPlugin(): MailTransportPlugin {
|
|
15
|
+
const usage = mailTransportSmtpFeature.extensionUsages.find(
|
|
16
|
+
(u) => u.extensionName === "mailTransport" && u.entityName === "smtp",
|
|
17
|
+
);
|
|
18
|
+
if (!usage || !isMailTransportPlugin(usage.options)) {
|
|
19
|
+
throw new Error("mail-transport-smtp: plugin not registered under 'smtp'");
|
|
20
|
+
}
|
|
21
|
+
return usage.options;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function fakeConfig(
|
|
25
|
+
overrides: Partial<
|
|
26
|
+
Record<"host" | "port" | "secure" | "from" | "authUser", string | number | boolean>
|
|
27
|
+
> = {},
|
|
28
|
+
): ConfigAccessor {
|
|
29
|
+
const keys = mailTransportSmtpFeature.exports.configKeys;
|
|
30
|
+
const values = new Map<string, string | number | boolean>([
|
|
31
|
+
[keys.host.name, overrides.host ?? "smtp.contract-test.invalid"],
|
|
32
|
+
[keys.port.name, overrides.port ?? 587],
|
|
33
|
+
[keys.secure.name, overrides.secure ?? false],
|
|
34
|
+
[keys.from.name, overrides.from ?? "noreply@contract-test.invalid"],
|
|
35
|
+
[keys.authUser.name, overrides.authUser ?? "contract-test-user"],
|
|
36
|
+
]);
|
|
37
|
+
return (async (handle: string | { readonly name: string }) => {
|
|
38
|
+
const key = typeof handle === "string" ? handle : handle.name;
|
|
39
|
+
return values.get(key);
|
|
40
|
+
}) as ConfigAccessor;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function fakeSecrets(): SecretsContext {
|
|
44
|
+
return {
|
|
45
|
+
get: async () => createSecret("contract-test-password"),
|
|
46
|
+
has: async () => true,
|
|
47
|
+
set: async () => {},
|
|
48
|
+
delete: async () => true,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
describeMailTransportContract("mail-transport-smtp", () => ({
|
|
53
|
+
plugin: registeredPlugin(),
|
|
54
|
+
ctx: { config: fakeConfig(), secrets: fakeSecrets(), _userId: "contract-test-user" },
|
|
55
|
+
tenantId: "contract-test-tenant",
|
|
56
|
+
}));
|
|
57
|
+
|
|
58
|
+
describe("mailTransportSmtpFeature — build error path", () => {
|
|
59
|
+
test("build rejects when a required config-key (host) is unset", async () => {
|
|
60
|
+
const ctx = {
|
|
61
|
+
config: fakeConfig({ host: "" }),
|
|
62
|
+
secrets: fakeSecrets(),
|
|
63
|
+
_userId: "contract-test-user",
|
|
64
|
+
};
|
|
65
|
+
await expect(registeredPlugin().build(ctx, "contract-test-tenant")).rejects.toThrow(/host/);
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
9
69
|
describe("mailTransportSmtpFeature — shape", () => {
|
|
10
70
|
test("has the expected name", () => {
|
|
11
71
|
expect(mailTransportSmtpFeature.name).toBe("mail-transport-smtp");
|
|
@@ -21,7 +21,11 @@ export function createBrandingQuery(opts: { readonly allowCustomCss: boolean })
|
|
|
21
21
|
access: { roles: ["anonymous", "User", "TenantAdmin", "SystemAdmin"] },
|
|
22
22
|
handler: async (_query, ctx) => {
|
|
23
23
|
const base = await readBranding(ctx.config);
|
|
24
|
-
if (
|
|
24
|
+
if (
|
|
25
|
+
!opts.allowCustomCss ||
|
|
26
|
+
!ctx.config ||
|
|
27
|
+
!(await ctx.hasFeature(MANAGED_PAGES_CSS_FEATURE))
|
|
28
|
+
) {
|
|
25
29
|
return base;
|
|
26
30
|
}
|
|
27
31
|
return { ...base, customCss: await readCustomCss(ctx.config) };
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { PAT_TOKEN_PREFIX } from "@cosmicdrift/kumiko-framework/api";
|
|
3
|
+
import { EXT_TOKEN_VERIFIER } from "../../auth-foundation";
|
|
2
4
|
import { createPersonalAccessTokensFeature } from "../feature";
|
|
3
5
|
|
|
4
6
|
describe("createPersonalAccessTokensFeature toggleable-option (tier-gating)", () => {
|
|
@@ -22,3 +24,19 @@ describe("createPersonalAccessTokensFeature toggleable-option (tier-gating)", ()
|
|
|
22
24
|
expect(feature.toggleableDefault).toBe(true);
|
|
23
25
|
});
|
|
24
26
|
});
|
|
27
|
+
|
|
28
|
+
describe("createPersonalAccessTokensFeature — tokenVerifier registration (#1369)", () => {
|
|
29
|
+
test('registers via r.useExtension(EXT_TOKEN_VERIFIER, "pat", ...) instead of a patResolver field', () => {
|
|
30
|
+
const feature = createPersonalAccessTokensFeature({ scopes: {} });
|
|
31
|
+
expect(feature.extensionUsages).toHaveLength(1);
|
|
32
|
+
const [usage] = feature.extensionUsages;
|
|
33
|
+
expect(usage?.extensionName).toBe(EXT_TOKEN_VERIFIER);
|
|
34
|
+
expect(usage?.entityName).toBe("pat");
|
|
35
|
+
expect(usage?.options).toMatchObject({ shape: { kind: "prefix", prefix: PAT_TOKEN_PREFIX } });
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test("requires auth-foundation (owner of EXT_TOKEN_VERIFIER)", () => {
|
|
39
|
+
const feature = createPersonalAccessTokensFeature({ scopes: {} });
|
|
40
|
+
expect(feature.requires).toContain("auth-foundation");
|
|
41
|
+
});
|
|
42
|
+
});
|
|
@@ -1,9 +1,6 @@
|
|
|
1
1
|
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
2
2
|
import { randomBytes } from "node:crypto";
|
|
3
|
-
import {
|
|
4
|
-
createInMemoryLoginRateLimiter,
|
|
5
|
-
type PatResolver,
|
|
6
|
-
} from "@cosmicdrift/kumiko-framework/api";
|
|
3
|
+
import { createInMemoryLoginRateLimiter } from "@cosmicdrift/kumiko-framework/api";
|
|
7
4
|
import { selectMany, updateMany } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
8
5
|
import {
|
|
9
6
|
configureBlindIndexKey,
|
|
@@ -25,6 +22,7 @@ import { createTestEnvelopeCipher, deleteRows } from "@cosmicdrift/kumiko-framew
|
|
|
25
22
|
import { Temporal } from "temporal-polyfill";
|
|
26
23
|
import { AuthHandlers } from "../../auth-email-password/constants";
|
|
27
24
|
import { createAuthEmailPasswordFeature } from "../../auth-email-password/feature";
|
|
25
|
+
import { authFoundationFeature, resolveTokenVerifier } from "../../auth-foundation";
|
|
28
26
|
import { createConfigFeature } from "../../config";
|
|
29
27
|
import { createConfigResolver } from "../../config/resolver";
|
|
30
28
|
import { configValuesTable } from "../../config/table";
|
|
@@ -39,7 +37,6 @@ import { createUserFeature } from "../../user/feature";
|
|
|
39
37
|
import { userEntity } from "../../user/schema/user";
|
|
40
38
|
import { PatHandlers, PatQueries } from "../constants";
|
|
41
39
|
import { createPersonalAccessTokensFeature } from "../feature";
|
|
42
|
-
import { createPatResolver } from "../resolver";
|
|
43
40
|
import { apiTokenEntity, apiTokenTable } from "../schema/api-token";
|
|
44
41
|
import type { PatScopeConfig } from "../scopes";
|
|
45
42
|
|
|
@@ -51,7 +48,6 @@ import type { PatScopeConfig } from "../scopes";
|
|
|
51
48
|
|
|
52
49
|
let stack: TestStack;
|
|
53
50
|
let h: ReturnType<typeof makeSessionHelpers>;
|
|
54
|
-
let patResolver: PatResolver | undefined;
|
|
55
51
|
|
|
56
52
|
const encryptionKey = randomBytes(32).toString("base64");
|
|
57
53
|
const TENANT: TenantId = testTenantId(1);
|
|
@@ -91,21 +87,22 @@ beforeAll(async () => {
|
|
|
91
87
|
createAuthEmailPasswordFeature(),
|
|
92
88
|
createSessionsFeature(),
|
|
93
89
|
createPersonalAccessTokensFeature({ scopes: SCOPES }),
|
|
90
|
+
authFoundationFeature,
|
|
94
91
|
],
|
|
95
92
|
extraContext: { configResolver: resolver, configEncryption: encryption },
|
|
96
93
|
authConfig: {
|
|
97
94
|
membershipQuery: "tenant:query:memberships",
|
|
98
95
|
loginHandler: AuthHandlers.login,
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
96
|
+
// PAT registers itself as a tokenVerifier provider (auth-foundation)
|
|
97
|
+
// instead of the app wiring a dedicated patResolver — resolved
|
|
98
|
+
// generically here, same as run-prod-app/run-dev-app do.
|
|
99
|
+
tokenVerifier: (raw: string) =>
|
|
100
|
+
resolveTokenVerifier({ db: stack.db, registry: stack.registry }, raw),
|
|
103
101
|
// Low per-token cap so the rate-limit test can exhaust it. Other tests do
|
|
104
102
|
// ≤2 requests per (distinct) token, so this ceiling never trips them.
|
|
105
103
|
patRateLimiter: createInMemoryLoginRateLimiter(3, 60_000),
|
|
106
104
|
},
|
|
107
105
|
});
|
|
108
|
-
patResolver = createPatResolver({ db: stack.db, scopes: SCOPES });
|
|
109
106
|
h = makeSessionHelpers(stack, TENANT);
|
|
110
107
|
|
|
111
108
|
await unsafeCreateEntityTable(stack.db, userEntity);
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type AuthProviderPlugin,
|
|
3
|
+
EXT_TOKEN_VERIFIER,
|
|
4
|
+
} from "@cosmicdrift/kumiko-bundled-features/auth-foundation";
|
|
5
|
+
import { PAT_TOKEN_PREFIX } from "@cosmicdrift/kumiko-framework/api";
|
|
1
6
|
import { buildEntityTableMeta } from "@cosmicdrift/kumiko-framework/db";
|
|
2
7
|
import { defineFeature, type FeatureDefinition } from "@cosmicdrift/kumiko-framework/engine";
|
|
3
8
|
import { PAT_DEFAULT_RATE_LIMIT, PAT_FEATURE, PAT_SCREEN_ID, type PatRateLimit } from "./constants";
|
|
@@ -6,6 +11,7 @@ import { createPatWrite } from "./handlers/create.write";
|
|
|
6
11
|
import { listPatQuery } from "./handlers/list.query";
|
|
7
12
|
import { revokePatWrite } from "./handlers/revoke.write";
|
|
8
13
|
import { PAT_FEATURE_I18N } from "./i18n";
|
|
14
|
+
import { createPatResolver } from "./resolver";
|
|
9
15
|
import { apiTokenEntity } from "./schema/api-token";
|
|
10
16
|
import type { PatScopeConfig } from "./scopes";
|
|
11
17
|
|
|
@@ -25,14 +31,13 @@ export type PersonalAccessTokensOptions = {
|
|
|
25
31
|
};
|
|
26
32
|
|
|
27
33
|
export type PatFeatureExports = {
|
|
28
|
-
readonly scopes: PatScopeConfig;
|
|
29
34
|
readonly rateLimit: PatRateLimit;
|
|
30
35
|
};
|
|
31
36
|
|
|
32
37
|
// Personal Access Tokens — long-lived, revocable bearer credentials for the
|
|
33
38
|
// HTTP API. Like `sessions`, the hot-path resolver is NOT a handler: it runs on
|
|
34
39
|
// every PAT-authenticated request and does a direct-DB point-read (see
|
|
35
|
-
// createPatResolver /
|
|
40
|
+
// createPatResolver / the tokenVerifier extension-point registration below). The dispatcher-side handlers here
|
|
36
41
|
// only mint/list/revoke tokens and expose the scope catalog.
|
|
37
42
|
export function createPersonalAccessTokensFeature(
|
|
38
43
|
options: PersonalAccessTokensOptions,
|
|
@@ -40,16 +45,25 @@ export function createPersonalAccessTokensFeature(
|
|
|
40
45
|
const { scopes } = options;
|
|
41
46
|
return defineFeature(PAT_FEATURE, (r) => {
|
|
42
47
|
r.describe(
|
|
43
|
-
"Long-lived, revocable Personal Access Tokens for headless HTTP-API access. Stores SHA-256 token hashes in the `store_api_tokens` direct-write table; the plaintext is returned once at creation. `create`/`revoke`/`mine` manage a user's own tokens and `available-scopes` lists the app-declared scope catalog. Bearer tokens carrying the PAT prefix are resolved before jwt.verify (roles resolved live, granted scopes enforced fail-closed at the API boundary) —
|
|
48
|
+
"Long-lived, revocable Personal Access Tokens for headless HTTP-API access. Stores SHA-256 token hashes in the `store_api_tokens` direct-write table; the plaintext is returned once at creation. `create`/`revoke`/`mine` manage a user's own tokens and `available-scopes` lists the app-declared scope catalog. Bearer tokens carrying the PAT prefix are resolved before jwt.verify (roles resolved live, granted scopes enforced fail-closed at the API boundary) — registered as an auth-foundation tokenVerifier provider, resolved generically by the middleware. Pass { toggleable: { default: false } } to tier-gate the whole feature.",
|
|
44
49
|
);
|
|
45
|
-
r.uiHints({ displayLabel: "Personal Access Tokens", category: "identity", recommended:
|
|
50
|
+
r.uiHints({ displayLabel: "Personal Access Tokens", category: "identity", recommended: true });
|
|
46
51
|
// Opt-in tier-gating (mirrors ledger/tags): when set, the feature declares
|
|
47
52
|
// itself r.toggleable so the dispatcher gate + tier-engine can switch PAT
|
|
48
53
|
// on/off per tenant. { default: false } = fail-closed until a tier grants it.
|
|
49
54
|
if (options.toggleable !== undefined) r.toggleable(options.toggleable);
|
|
50
55
|
// Resolver reads memberships + users on every PAT request to build live
|
|
51
56
|
// roles — make both boot-time deps so a mis-wiring fails validateBoot.
|
|
52
|
-
|
|
57
|
+
// auth-foundation owns EXT_TOKEN_VERIFIER, which the useExtension below
|
|
58
|
+
// registers against.
|
|
59
|
+
r.requires("user", "tenant", "auth-foundation");
|
|
60
|
+
// Self-registers as a bearer-auth provider instead of the app wiring a
|
|
61
|
+
// dedicated patResolver callback — the middleware finds it generically
|
|
62
|
+
// via resolveTokenVerifier(), shape-matched by the PAT token prefix.
|
|
63
|
+
r.useExtension(EXT_TOKEN_VERIFIER, "pat", {
|
|
64
|
+
shape: { kind: "prefix", prefix: PAT_TOKEN_PREFIX },
|
|
65
|
+
build: (deps) => createPatResolver({ db: deps.db, scopes }),
|
|
66
|
+
} satisfies AuthProviderPlugin);
|
|
53
67
|
// Direct-write store like store_user_sessions: create/revoke write it, the
|
|
54
68
|
// resolver point-reads it. r.entity would make it a rebuildable projection
|
|
55
69
|
// whose replay (no token events) would wipe every live token (#498/#494).
|
|
@@ -78,12 +92,13 @@ export function createPersonalAccessTokensFeature(
|
|
|
78
92
|
});
|
|
79
93
|
r.translations({ keys: PAT_FEATURE_I18N });
|
|
80
94
|
|
|
81
|
-
//
|
|
82
|
-
//
|
|
95
|
+
// rateLimit flows into feature.exports so run-prod-app builds the
|
|
96
|
+
// limiter from the same declaration — single source of truth. `scopes`
|
|
97
|
+
// is closed over directly by the useExtension build() above and by
|
|
98
|
+
// available-scopes, so it doesn't need to round-trip through exports.
|
|
83
99
|
return {
|
|
84
100
|
handlers,
|
|
85
101
|
queries,
|
|
86
|
-
scopes,
|
|
87
102
|
rateLimit: options.rateLimit ?? PAT_DEFAULT_RATE_LIMIT,
|
|
88
103
|
} satisfies { handlers: unknown; queries: unknown } & PatFeatureExports;
|
|
89
104
|
});
|
|
@@ -29,7 +29,7 @@ export const listPatQuery = defineQueryHandler({
|
|
|
29
29
|
return Promise.all(
|
|
30
30
|
rows.map(async (r) => ({
|
|
31
31
|
id: r.id,
|
|
32
|
-
name: await decryptStoredPii(r.name, "pat:list"),
|
|
32
|
+
name: await decryptStoredPii(r.name, "name", "pat:list"),
|
|
33
33
|
prefix: r.prefix,
|
|
34
34
|
scopes: parseScopeNames(r.scopes),
|
|
35
35
|
createdAt: r.createdAt,
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import type { FeatureDefinition } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
2
|
import { PAT_DEFAULT_RATE_LIMIT, type PatRateLimit } from "./constants";
|
|
3
|
-
import type { PatScopeConfig } from "./scopes";
|
|
4
3
|
|
|
5
4
|
export type { PatRateLimit } from "./constants";
|
|
6
5
|
export { PAT_DEFAULT_RATE_LIMIT, PAT_FEATURE, PatHandlers, PatQueries } from "./constants";
|
|
@@ -12,19 +11,6 @@ export { apiTokenEntity, apiTokenTable } from "./schema/api-token";
|
|
|
12
11
|
export type { PatScopeConfig, PatScopeDef } from "./scopes";
|
|
13
12
|
export { expandScopes } from "./scopes";
|
|
14
13
|
|
|
15
|
-
// Reads the scope catalog off a mounted PAT feature's exports (set from the
|
|
16
|
-
// setup-callback return). run-prod-app uses this to build the resolver from the
|
|
17
|
-
// same config the handlers were given. Returns {} for a non-PAT / malformed
|
|
18
|
-
// definition — the caller has already matched on feature.name.
|
|
19
|
-
export function patScopesFromFeature(feature: FeatureDefinition): PatScopeConfig {
|
|
20
|
-
const exports = feature.exports;
|
|
21
|
-
if (exports && typeof exports === "object" && "scopes" in exports) {
|
|
22
|
-
const { scopes } = exports as { scopes: unknown };
|
|
23
|
-
if (scopes && typeof scopes === "object") return scopes as PatScopeConfig;
|
|
24
|
-
}
|
|
25
|
-
return {};
|
|
26
|
-
}
|
|
27
|
-
|
|
28
14
|
// Reads the per-token rate-limit config off a mounted PAT feature's exports.
|
|
29
15
|
// Falls back to the default when absent. run-prod-app uses this to build the
|
|
30
16
|
// limiter from the same declaration the feature was given.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { TokenVerifier } from "@cosmicdrift/kumiko-framework/api";
|
|
2
2
|
import { fetchOne } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
3
3
|
import type { DbConnection } from "@cosmicdrift/kumiko-framework/db";
|
|
4
4
|
import type { SessionUser, TenantId } from "@cosmicdrift/kumiko-framework/engine";
|
|
@@ -16,7 +16,7 @@ import { expandScopes, type PatScopeConfig } from "./scopes";
|
|
|
16
16
|
export function createPatResolver(opts: {
|
|
17
17
|
readonly db: DbConnection;
|
|
18
18
|
readonly scopes: PatScopeConfig;
|
|
19
|
-
}):
|
|
19
|
+
}): TokenVerifier {
|
|
20
20
|
const { db, scopes } = opts;
|
|
21
21
|
return async (rawToken: string): Promise<SessionUser | null> => {
|
|
22
22
|
const row = await fetchOne<{
|
|
@@ -7,6 +7,7 @@ import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
|
7
7
|
import { randomBytes } from "node:crypto";
|
|
8
8
|
import { deleteMany, selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
9
9
|
import type { AppContext } from "@cosmicdrift/kumiko-framework/engine";
|
|
10
|
+
import { rebuildProjection } from "@cosmicdrift/kumiko-framework/pipeline";
|
|
10
11
|
import {
|
|
11
12
|
createEnvMasterKeyProvider,
|
|
12
13
|
decodeStoredEnvelope,
|
|
@@ -235,5 +236,35 @@ describe("rotate-job circuit-breaker", () => {
|
|
|
235
236
|
const plaintext = await decryptValue(decodeStoredEnvelope(row.envelope), verifier);
|
|
236
237
|
expect(plaintext).toBe(`secret-${expectedIndex}`);
|
|
237
238
|
}
|
|
239
|
+
|
|
240
|
+
// Regression guard (kumiko-framework#1189): rotate.job.ts's
|
|
241
|
+
// executor.update() call carries no options, so it deliberately
|
|
242
|
+
// never sets skipUnchanged — every rotation writes a real .updated
|
|
243
|
+
// event, not a raw UPDATE. A full projection rebuild replays those
|
|
244
|
+
// events; if a future refactor added skipUnchanged (or swapped the
|
|
245
|
+
// event write for a raw column update) the rebuild would resurrect
|
|
246
|
+
// the pre-rotation V1 envelopes instead of landing on V2, exactly
|
|
247
|
+
// the #464 failure class config/auth-mfa already guard against.
|
|
248
|
+
//
|
|
249
|
+
// Vacuous-rebuild guard: confirm the projection name actually exists
|
|
250
|
+
// and the rebuild really replayed events, so a wrong projection name
|
|
251
|
+
// can't silently no-op the assertion below into a false pass.
|
|
252
|
+
const projectionName = "secrets:projection:tenant-secret-entity";
|
|
253
|
+
expect(stack.registry.getAllProjections().has(projectionName)).toBe(true);
|
|
254
|
+
const rebuildResult = await rebuildProjection(projectionName, {
|
|
255
|
+
db: stack.db,
|
|
256
|
+
registry: stack.registry,
|
|
257
|
+
});
|
|
258
|
+
expect(rebuildResult.eventsProcessed).toBeGreaterThan(0);
|
|
259
|
+
|
|
260
|
+
const rowsAfterRebuild = await selectMany<{ key: string; kekVersion: number }>(
|
|
261
|
+
stack.db,
|
|
262
|
+
tenantSecretsTable,
|
|
263
|
+
{ tenantId: admin.tenantId },
|
|
264
|
+
);
|
|
265
|
+
expect(rowsAfterRebuild).toHaveLength(20);
|
|
266
|
+
for (const row of rowsAfterRebuild) {
|
|
267
|
+
expect(row.kekVersion).toBe(2);
|
|
268
|
+
}
|
|
238
269
|
});
|
|
239
270
|
});
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
// expired/revoked rows go, live rows stay, batching + signal work.
|
|
6
6
|
|
|
7
7
|
import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test";
|
|
8
|
+
import { authFoundationFeature } from "@cosmicdrift/kumiko-bundled-features/auth-foundation";
|
|
8
9
|
import { selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
9
10
|
import { sql } from "@cosmicdrift/kumiko-framework/db";
|
|
10
11
|
import type { AppContext } from "@cosmicdrift/kumiko-framework/engine";
|
|
@@ -40,7 +41,7 @@ let stack: TestStack;
|
|
|
40
41
|
|
|
41
42
|
beforeAll(async () => {
|
|
42
43
|
stack = await setupTestStack({
|
|
43
|
-
features: [createSessionsFeature(), createUserFeature()],
|
|
44
|
+
features: [authFoundationFeature, createSessionsFeature(), createUserFeature()],
|
|
44
45
|
});
|
|
45
46
|
await unsafeCreateEntityTable(stack.db, userSessionEntity);
|
|
46
47
|
await unsafeCreateEntityTable(stack.db, userEntity);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { afterAll, beforeAll, beforeEach, describe, expect, mock, test } from "bun:test";
|
|
2
2
|
import { randomBytes } from "node:crypto";
|
|
3
|
+
import { authFoundationFeature } from "@cosmicdrift/kumiko-bundled-features/auth-foundation";
|
|
3
4
|
import { asRawClient, selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
4
5
|
import type { TenantId } from "@cosmicdrift/kumiko-framework/engine";
|
|
5
6
|
import {
|
|
@@ -66,6 +67,7 @@ beforeAll(async () => {
|
|
|
66
67
|
createUserFeature(),
|
|
67
68
|
createTenantFeature(),
|
|
68
69
|
createAuthEmailPasswordFeature(),
|
|
70
|
+
authFoundationFeature,
|
|
69
71
|
createSessionsFeature({
|
|
70
72
|
autoRevokeOnPasswordChange: massRevokeSpy,
|
|
71
73
|
}),
|
|
@@ -78,7 +80,7 @@ beforeAll(async () => {
|
|
|
78
80
|
},
|
|
79
81
|
});
|
|
80
82
|
callbacks.set(createSessionCallbacks({ db: stack.db }));
|
|
81
|
-
h = makeSessionHelpers(stack, TENANT);
|
|
83
|
+
h = makeSessionHelpers(stack, TENANT, bound.asAuthConfig().sessionCreator);
|
|
82
84
|
|
|
83
85
|
await unsafeCreateEntityTable(stack.db, userEntity);
|
|
84
86
|
await unsafeCreateEntityTable(stack.db, tenantEntity);
|
|
@@ -99,7 +101,7 @@ beforeEach(async () => {
|
|
|
99
101
|
|
|
100
102
|
describe("password change mass-revokes every live session", () => {
|
|
101
103
|
test("changing the password revokes ALL sessions including the caller's", async () => {
|
|
102
|
-
await h.seedUser("rotate@example.com", "first-password");
|
|
104
|
+
const { userId } = await h.seedUser("rotate@example.com", "first-password");
|
|
103
105
|
|
|
104
106
|
const a = await h.login("rotate@example.com", "first-password");
|
|
105
107
|
const b = await h.login("rotate@example.com", "first-password");
|
|
@@ -147,7 +149,7 @@ describe("password change mass-revokes every live session", () => {
|
|
|
147
149
|
|
|
148
150
|
// DB state confirms: zero live rows for this user
|
|
149
151
|
const liveRows = await selectMany(stack.db, userSessionTable);
|
|
150
|
-
const stillLive = liveRows.filter((r) => r["revokedAt"] === null);
|
|
152
|
+
const stillLive = liveRows.filter((r) => r["userId"] === userId && r["revokedAt"] === null);
|
|
151
153
|
expect(stillLive).toHaveLength(0);
|
|
152
154
|
|
|
153
155
|
// And logging in again with the NEW password works
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import { authFoundationFeature } from "@cosmicdrift/kumiko-bundled-features/auth-foundation";
|
|
2
3
|
import { asRawClient, selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
3
4
|
import { createTenantDb, type DbConnection } from "@cosmicdrift/kumiko-framework/db";
|
|
4
5
|
import { createRegistry, type TenantId } from "@cosmicdrift/kumiko-framework/engine";
|
|
@@ -72,14 +73,22 @@ async function insertRevokedSession(db: DbConnection): Promise<void> {
|
|
|
72
73
|
|
|
73
74
|
describe("sessions / store_user_sessions survives projection rebuild", () => {
|
|
74
75
|
test("is NOT registered as a rebuildable implicit projection", () => {
|
|
75
|
-
const registry = createRegistry([
|
|
76
|
+
const registry = createRegistry([
|
|
77
|
+
authFoundationFeature,
|
|
78
|
+
createSessionsFeature(),
|
|
79
|
+
createUserFeature(),
|
|
80
|
+
]);
|
|
76
81
|
expect(registry.getAllProjections().has(IMPLICIT_PROJECTION)).toBe(false);
|
|
77
82
|
});
|
|
78
83
|
|
|
79
84
|
test("direct-written rows (incl. revoked state) survive a rebuild", async () => {
|
|
80
85
|
await insertRevokedSession(createTenantDb(testDb.db, TENANT));
|
|
81
86
|
|
|
82
|
-
const registry = createRegistry([
|
|
87
|
+
const registry = createRegistry([
|
|
88
|
+
authFoundationFeature,
|
|
89
|
+
createSessionsFeature(),
|
|
90
|
+
createUserFeature(),
|
|
91
|
+
]);
|
|
83
92
|
// Pre-fix: the implicit projection exists → rebuild swaps an empty shadow
|
|
84
93
|
// → rows wiped. Post-fix: absent → no rebuild → rows untouched. Either way
|
|
85
94
|
// a regression (re-adding r.entity) makes this fail.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// sessions self-registers as auth-foundation's sessionStore provider (#1371)
|
|
2
|
+
// instead of only being wired by hand into buildServer({ auth: { ... } }).
|
|
3
|
+
|
|
4
|
+
import { describe, expect, test } from "bun:test";
|
|
5
|
+
import { EXT_SESSION_STORE } from "@cosmicdrift/kumiko-bundled-features/auth-foundation";
|
|
6
|
+
import { createSessionsFeature } from "../feature";
|
|
7
|
+
|
|
8
|
+
describe("createSessionsFeature — sessionStore registration (#1371)", () => {
|
|
9
|
+
test('registers via r.useExtension(EXT_SESSION_STORE, "default", ...)', () => {
|
|
10
|
+
const feature = createSessionsFeature();
|
|
11
|
+
const usage = feature.extensionUsages.find((u) => u.extensionName === EXT_SESSION_STORE);
|
|
12
|
+
expect(usage).toBeDefined();
|
|
13
|
+
expect(usage?.entityName).toBe("default");
|
|
14
|
+
expect(typeof (usage?.options as { build?: unknown } | undefined)?.build).toBe("function");
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test("requires auth-foundation (owner of EXT_SESSION_STORE)", () => {
|
|
18
|
+
const feature = createSessionsFeature();
|
|
19
|
+
expect(feature.requires).toContain("auth-foundation");
|
|
20
|
+
});
|
|
21
|
+
});
|
|
@@ -1,17 +1,26 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
|
-
import { validateBoot } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
|
+
import { defineFeature, validateBoot } from "@cosmicdrift/kumiko-framework/engine";
|
|
3
3
|
import { rolesOf } from "@cosmicdrift/kumiko-framework/testing";
|
|
4
|
+
import { EXT_SESSION_STORE } from "../../auth-foundation";
|
|
4
5
|
import { createConfigFeature } from "../../config/feature";
|
|
5
6
|
import { createTenantFeature } from "../../tenant";
|
|
6
7
|
import { createUserFeature } from "../../user/feature";
|
|
7
8
|
import { SESSION_DETAIL_SCREEN_ID, SESSION_LIST_SCREEN_ID } from "../constants";
|
|
8
9
|
import { createSessionsFeature } from "../feature";
|
|
9
10
|
|
|
11
|
+
// Stub instead of the real auth-foundation — this test is about screen/query
|
|
12
|
+
// wiring, not auth; the real feature also mandates a tokenVerifier provider
|
|
13
|
+
// which is out of scope here.
|
|
14
|
+
const sessionStoreOwnerStub = defineFeature("auth-foundation", (r) => {
|
|
15
|
+
r.extendsRegistrar(EXT_SESSION_STORE, {});
|
|
16
|
+
});
|
|
17
|
+
|
|
10
18
|
describe("sessions screens + query access alignment (kumiko-framework#255)", () => {
|
|
11
19
|
const features = [
|
|
12
20
|
createConfigFeature(),
|
|
13
21
|
createUserFeature(),
|
|
14
22
|
createTenantFeature(),
|
|
23
|
+
sessionStoreOwnerStub,
|
|
15
24
|
createSessionsFeature(),
|
|
16
25
|
];
|
|
17
26
|
|