@cosmicdrift/kumiko-bundled-features 0.1.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 +90 -0
- package/src/audit/__tests__/audit.integration.ts +328 -0
- package/src/audit/constants.ts +7 -0
- package/src/audit/feature.ts +23 -0
- package/src/audit/handlers/list.query.ts +98 -0
- package/src/audit/index.ts +2 -0
- package/src/auth-email-password/__tests__/account-lockout-no-redis.integration.ts +149 -0
- package/src/auth-email-password/__tests__/account-lockout.integration.ts +308 -0
- package/src/auth-email-password/__tests__/auth-claims.integration.ts +512 -0
- package/src/auth-email-password/__tests__/auth.integration.ts +610 -0
- package/src/auth-email-password/__tests__/confirm-token-flow.test.ts +67 -0
- package/src/auth-email-password/__tests__/email-templates.test.ts +106 -0
- package/src/auth-email-password/__tests__/email-verification.integration.ts +327 -0
- package/src/auth-email-password/__tests__/identity-v3-hash.test.ts +174 -0
- package/src/auth-email-password/__tests__/identity-v3-login.integration.ts +150 -0
- package/src/auth-email-password/__tests__/invite-flow.integration.ts +458 -0
- package/src/auth-email-password/__tests__/multi-roles.integration.ts +256 -0
- package/src/auth-email-password/__tests__/password-reset.integration.ts +346 -0
- package/src/auth-email-password/__tests__/public-routes-rate-limit.integration.ts +144 -0
- package/src/auth-email-password/__tests__/seed-admin.integration.ts +176 -0
- package/src/auth-email-password/__tests__/session-callbacks.integration.ts +310 -0
- package/src/auth-email-password/__tests__/session-strict-mode.integration.ts +101 -0
- package/src/auth-email-password/__tests__/signed-token.test.ts +78 -0
- package/src/auth-email-password/__tests__/signup-flow.integration.ts +259 -0
- package/src/auth-email-password/auth-user-row.ts +41 -0
- package/src/auth-email-password/constants.ts +101 -0
- package/src/auth-email-password/email-templates.ts +283 -0
- package/src/auth-email-password/feature.ts +140 -0
- package/src/auth-email-password/handlers/change-password.write.ts +58 -0
- package/src/auth-email-password/handlers/confirm-token-flow.ts +191 -0
- package/src/auth-email-password/handlers/invite-accept-with-login.write.ts +203 -0
- package/src/auth-email-password/handlers/invite-accept.write.ts +189 -0
- package/src/auth-email-password/handlers/invite-create.write.ts +145 -0
- package/src/auth-email-password/handlers/invite-signup-complete.write.ts +192 -0
- package/src/auth-email-password/handlers/login.write.ts +208 -0
- package/src/auth-email-password/handlers/logout.write.ts +12 -0
- package/src/auth-email-password/handlers/request-email-verification.write.ts +29 -0
- package/src/auth-email-password/handlers/request-password-reset.write.ts +31 -0
- package/src/auth-email-password/handlers/reset-password.write.ts +61 -0
- package/src/auth-email-password/handlers/signup-confirm.write.ts +170 -0
- package/src/auth-email-password/handlers/signup-request.write.ts +104 -0
- package/src/auth-email-password/handlers/token-request-handler.ts +114 -0
- package/src/auth-email-password/handlers/verify-email.write.ts +62 -0
- package/src/auth-email-password/i18n.ts +211 -0
- package/src/auth-email-password/identity-v3-hash.ts +97 -0
- package/src/auth-email-password/index.ts +35 -0
- package/src/auth-email-password/invite-token-store.ts +92 -0
- package/src/auth-email-password/lockout-store.ts +118 -0
- package/src/auth-email-password/password-hashing.ts +43 -0
- package/src/auth-email-password/reset-token.ts +28 -0
- package/src/auth-email-password/seeding.ts +183 -0
- package/src/auth-email-password/signed-token.ts +85 -0
- package/src/auth-email-password/signup-token-store.ts +104 -0
- package/src/auth-email-password/stream-tenant.ts +31 -0
- package/src/auth-email-password/testing.ts +5 -0
- package/src/auth-email-password/token-burn-store.ts +57 -0
- package/src/auth-email-password/verification-token.ts +27 -0
- package/src/auth-email-password/web/__tests__/auth-gate.test.tsx +51 -0
- package/src/auth-email-password/web/__tests__/forgot-password-screen.test.tsx +80 -0
- package/src/auth-email-password/web/__tests__/login-screen.test.tsx +94 -0
- package/src/auth-email-password/web/__tests__/reset-password-screen.test.tsx +108 -0
- package/src/auth-email-password/web/__tests__/session-roles.test.ts +54 -0
- package/src/auth-email-password/web/__tests__/tenant-switcher.test.tsx +100 -0
- package/src/auth-email-password/web/__tests__/test-utils.tsx +73 -0
- package/src/auth-email-password/web/__tests__/user-menu.test.tsx +55 -0
- package/src/auth-email-password/web/__tests__/verify-email-screen.test.tsx +59 -0
- package/src/auth-email-password/web/auth-client.ts +350 -0
- package/src/auth-email-password/web/auth-form-primitives.tsx +70 -0
- package/src/auth-email-password/web/auth-gate.tsx +33 -0
- package/src/auth-email-password/web/client-plugin.ts +48 -0
- package/src/auth-email-password/web/default-topbar-actions.tsx +47 -0
- package/src/auth-email-password/web/forgot-password-screen.tsx +110 -0
- package/src/auth-email-password/web/index.ts +56 -0
- package/src/auth-email-password/web/invite-accept-screen.tsx +220 -0
- package/src/auth-email-password/web/login-screen.tsx +150 -0
- package/src/auth-email-password/web/reset-password-screen.tsx +152 -0
- package/src/auth-email-password/web/session.tsx +171 -0
- package/src/auth-email-password/web/signup-complete-screen.tsx +150 -0
- package/src/auth-email-password/web/signup-screen.tsx +130 -0
- package/src/auth-email-password/web/tenant-switcher.tsx +116 -0
- package/src/auth-email-password/web/use-shell-user.ts +34 -0
- package/src/auth-email-password/web/user-menu.tsx +89 -0
- package/src/auth-email-password/web/verify-email-screen.tsx +102 -0
- package/src/billing-foundation/__tests__/billing-foundation.integration.ts +568 -0
- package/src/billing-foundation/__tests__/feature.test.ts +110 -0
- package/src/billing-foundation/__tests__/webhook-handler.test.ts +199 -0
- package/src/billing-foundation/aggregate-id.ts +21 -0
- package/src/billing-foundation/constants.ts +70 -0
- package/src/billing-foundation/entities.ts +50 -0
- package/src/billing-foundation/events.ts +71 -0
- package/src/billing-foundation/feature.ts +122 -0
- package/src/billing-foundation/get-subscription-for-tenant.ts +39 -0
- package/src/billing-foundation/handlers/create-checkout-session.write.ts +79 -0
- package/src/billing-foundation/handlers/create-portal-session.write.ts +73 -0
- package/src/billing-foundation/handlers/list-subscriptions.query.ts +20 -0
- package/src/billing-foundation/handlers/process-event.write.ts +160 -0
- package/src/billing-foundation/index.ts +42 -0
- package/src/billing-foundation/projection.ts +135 -0
- package/src/billing-foundation/types.ts +157 -0
- package/src/billing-foundation/webhook-handler.ts +184 -0
- package/src/cap-counter/__tests__/cap-counter.integration.ts +566 -0
- package/src/cap-counter/__tests__/enforce-cap.test.ts +422 -0
- package/src/cap-counter/__tests__/with-cap-enforcement.integration.ts +265 -0
- package/src/cap-counter/aggregate-id.ts +61 -0
- package/src/cap-counter/constants.ts +32 -0
- package/src/cap-counter/enforce-cap.ts +404 -0
- package/src/cap-counter/entity.ts +48 -0
- package/src/cap-counter/feature.ts +90 -0
- package/src/cap-counter/handlers/get-counter.query.ts +43 -0
- package/src/cap-counter/handlers/increment-rolling.write.ts +79 -0
- package/src/cap-counter/handlers/increment.write.ts +92 -0
- package/src/cap-counter/handlers/mark-soft-warned.write.ts +57 -0
- package/src/cap-counter/index.ts +34 -0
- package/src/cap-counter/with-cap-enforcement.ts +179 -0
- package/src/channel-email/email-channel.ts +48 -0
- package/src/channel-email/feature.ts +15 -0
- package/src/channel-email/index.ts +4 -0
- package/src/channel-email/smtp-transport.ts +65 -0
- package/src/channel-email/types.ts +34 -0
- package/src/channel-in-app/constants.ts +11 -0
- package/src/channel-in-app/feature.ts +30 -0
- package/src/channel-in-app/handlers/inbox.query.ts +28 -0
- package/src/channel-in-app/handlers/mark-all-read.write.ts +21 -0
- package/src/channel-in-app/handlers/mark-read.write.ts +32 -0
- package/src/channel-in-app/handlers/unread-count.query.ts +20 -0
- package/src/channel-in-app/in-app-channel.ts +44 -0
- package/src/channel-in-app/index.ts +4 -0
- package/src/channel-in-app/tables.ts +22 -0
- package/src/channel-push/feature.ts +15 -0
- package/src/channel-push/index.ts +3 -0
- package/src/channel-push/push-channel.ts +33 -0
- package/src/channel-push/types.ts +22 -0
- package/src/config/__tests__/app-overrides.test.ts +118 -0
- package/src/config/__tests__/config.integration.ts +1246 -0
- package/src/config/constants.ts +23 -0
- package/src/config/feature.ts +117 -0
- package/src/config/handlers/__tests__/prepare-config-write.test.ts +209 -0
- package/src/config/handlers/reset.write.ts +45 -0
- package/src/config/handlers/schema.query.ts +22 -0
- package/src/config/handlers/set.write.ts +93 -0
- package/src/config/handlers/values.query.ts +43 -0
- package/src/config/index.ts +15 -0
- package/src/config/resolver.ts +283 -0
- package/src/config/table.ts +35 -0
- package/src/config/write-helpers.ts +268 -0
- package/src/delivery/__tests__/delivery-events.integration.ts +166 -0
- package/src/delivery/__tests__/delivery.integration.ts +1405 -0
- package/src/delivery/constants.ts +33 -0
- package/src/delivery/delivery-service.ts +489 -0
- package/src/delivery/events.ts +18 -0
- package/src/delivery/feature.ts +70 -0
- package/src/delivery/handlers/log.query.ts +21 -0
- package/src/delivery/handlers/preferences.query.ts +18 -0
- package/src/delivery/handlers/set-preference.write.ts +28 -0
- package/src/delivery/index.ts +35 -0
- package/src/delivery/tables.ts +74 -0
- package/src/delivery/testing.ts +47 -0
- package/src/delivery/types.ts +71 -0
- package/src/delivery/unsubscribe.ts +99 -0
- package/src/delivery/upsert-preference.ts +145 -0
- package/src/feature-toggles/__tests__/feature-toggles.integration.ts +687 -0
- package/src/feature-toggles/constants.ts +20 -0
- package/src/feature-toggles/events.ts +18 -0
- package/src/feature-toggles/feature.ts +98 -0
- package/src/feature-toggles/global-feature-state-table.ts +28 -0
- package/src/feature-toggles/handlers/list.query.ts +26 -0
- package/src/feature-toggles/handlers/registered.query.ts +56 -0
- package/src/feature-toggles/handlers/set.write.ts +158 -0
- package/src/feature-toggles/index.ts +9 -0
- package/src/feature-toggles/toggle-runtime.ts +73 -0
- package/src/file-foundation/__tests__/feature.test.ts +35 -0
- package/src/file-foundation/__tests__/file-foundation.integration.ts +235 -0
- package/src/file-foundation/feature.ts +123 -0
- package/src/file-foundation/index.ts +7 -0
- package/src/file-provider-inmemory/__tests__/feature.test.ts +35 -0
- package/src/file-provider-inmemory/feature.ts +73 -0
- package/src/file-provider-inmemory/index.ts +3 -0
- package/src/file-provider-s3/__tests__/feature.test.ts +54 -0
- package/src/file-provider-s3/feature.ts +169 -0
- package/src/file-provider-s3/index.ts +3 -0
- package/src/files-provider-s3/__tests__/env-helper.test.ts +161 -0
- package/src/files-provider-s3/__tests__/s3-provider.integration.ts +134 -0
- package/src/files-provider-s3/__tests__/s3-provider.test.ts +36 -0
- package/src/files-provider-s3/env-helper.ts +49 -0
- package/src/files-provider-s3/index.ts +3 -0
- package/src/files-provider-s3/s3-provider.ts +114 -0
- package/src/foundation-shared/config-helpers.ts +67 -0
- package/src/foundation-shared/index.ts +4 -0
- package/src/jobs/__tests__/job-system-user.integration.ts +194 -0
- package/src/jobs/__tests__/jobs-events.integration.ts +143 -0
- package/src/jobs/__tests__/jobs-feature.integration.ts +342 -0
- package/src/jobs/constants.ts +21 -0
- package/src/jobs/events.ts +39 -0
- package/src/jobs/feature.ts +150 -0
- package/src/jobs/handlers/detail.query.ts +30 -0
- package/src/jobs/handlers/list.query.ts +36 -0
- package/src/jobs/handlers/retry.write.ts +69 -0
- package/src/jobs/handlers/trigger.write.ts +39 -0
- package/src/jobs/index.ts +5 -0
- package/src/jobs/job-run-logger.ts +213 -0
- package/src/jobs/job-run-table.ts +55 -0
- package/src/legal-pages/README.md +195 -0
- package/src/legal-pages/__tests__/legal-pages.integration.ts +361 -0
- package/src/legal-pages/constants.ts +36 -0
- package/src/legal-pages/feature.ts +187 -0
- package/src/legal-pages/index.ts +13 -0
- package/src/legal-pages/markdown.ts +69 -0
- package/src/mail-foundation/__tests__/feature.test.ts +46 -0
- package/src/mail-foundation/__tests__/mail-foundation.integration.ts +247 -0
- package/src/mail-foundation/feature.ts +160 -0
- package/src/mail-foundation/index.ts +14 -0
- package/src/mail-transport-inmemory/__tests__/feature.test.ts +37 -0
- package/src/mail-transport-inmemory/feature.ts +90 -0
- package/src/mail-transport-inmemory/index.ts +3 -0
- package/src/mail-transport-smtp/__tests__/feature.test.ts +61 -0
- package/src/mail-transport-smtp/feature.ts +182 -0
- package/src/mail-transport-smtp/index.ts +3 -0
- package/src/rate-limiting/__tests__/rate-limiting.integration.ts +84 -0
- package/src/rate-limiting/constants.ts +9 -0
- package/src/rate-limiting/feature.ts +16 -0
- package/src/rate-limiting/handlers/status.query.ts +52 -0
- package/src/rate-limiting/index.ts +2 -0
- package/src/renderer-simple/__tests__/simple-renderer.test.ts +97 -0
- package/src/renderer-simple/feature.ts +12 -0
- package/src/renderer-simple/index.ts +2 -0
- package/src/renderer-simple/simple-renderer.ts +72 -0
- package/src/secrets/__tests__/rotate.integration.ts +176 -0
- package/src/secrets/__tests__/secrets-events.integration.ts +125 -0
- package/src/secrets/__tests__/secrets.integration.ts +118 -0
- package/src/secrets/feature.ts +84 -0
- package/src/secrets/handlers/delete.write.ts +20 -0
- package/src/secrets/handlers/list.query.ts +38 -0
- package/src/secrets/handlers/rotate.job.ts +193 -0
- package/src/secrets/handlers/set.write.ts +50 -0
- package/src/secrets/index.ts +16 -0
- package/src/secrets/secrets-context.ts +296 -0
- package/src/secrets/table.ts +68 -0
- package/src/sessions/__tests__/cleanup.integration.ts +175 -0
- package/src/sessions/__tests__/password-auto-revoke.integration.ts +202 -0
- package/src/sessions/__tests__/sessions.integration.ts +472 -0
- package/src/sessions/__tests__/test-helpers.ts +66 -0
- package/src/sessions/constants.ts +43 -0
- package/src/sessions/feature.ts +84 -0
- package/src/sessions/handlers/cleanup.job.ts +109 -0
- package/src/sessions/handlers/list.query.ts +35 -0
- package/src/sessions/handlers/mine.query.ts +37 -0
- package/src/sessions/handlers/revoke-all-others.write.ts +42 -0
- package/src/sessions/handlers/revoke.write.ts +76 -0
- package/src/sessions/index.ts +17 -0
- package/src/sessions/schema/index.ts +5 -0
- package/src/sessions/schema/user-session.ts +67 -0
- package/src/sessions/session-callbacks.ts +110 -0
- package/src/sessions/testing.ts +42 -0
- package/src/subscription-mollie/__tests__/feature.test.ts +106 -0
- package/src/subscription-mollie/__tests__/mollie-foundation.integration.ts +421 -0
- package/src/subscription-mollie/__tests__/verify-webhook.test.ts +388 -0
- package/src/subscription-mollie/constants.ts +33 -0
- package/src/subscription-mollie/feature.ts +144 -0
- package/src/subscription-mollie/index.ts +13 -0
- package/src/subscription-mollie/plugin-methods.ts +79 -0
- package/src/subscription-mollie/verify-webhook.ts +244 -0
- package/src/subscription-stripe/__tests__/feature.test.ts +98 -0
- package/src/subscription-stripe/__tests__/plugin-methods.test.ts +161 -0
- package/src/subscription-stripe/__tests__/stripe-foundation.integration.ts +315 -0
- package/src/subscription-stripe/__tests__/verify-webhook.test.ts +306 -0
- package/src/subscription-stripe/constants.ts +20 -0
- package/src/subscription-stripe/feature.ts +120 -0
- package/src/subscription-stripe/index.ts +14 -0
- package/src/subscription-stripe/plugin-methods.ts +91 -0
- package/src/subscription-stripe/verify-webhook.ts +235 -0
- package/src/tenant/__tests__/multi-tenant.integration.ts +278 -0
- package/src/tenant/__tests__/seed-testing.integration.ts +229 -0
- package/src/tenant/__tests__/tenant.integration.ts +347 -0
- package/src/tenant/command-schemas.ts +37 -0
- package/src/tenant/constants.ts +37 -0
- package/src/tenant/feature.ts +109 -0
- package/src/tenant/handlers/active-tenant-ids.query.ts +19 -0
- package/src/tenant/handlers/add-member.write.ts +53 -0
- package/src/tenant/handlers/cancel-invitation.write.ts +87 -0
- package/src/tenant/handlers/create.write.ts +21 -0
- package/src/tenant/handlers/disable.write.ts +18 -0
- package/src/tenant/handlers/invitations.query.ts +31 -0
- package/src/tenant/handlers/list.query.ts +17 -0
- package/src/tenant/handlers/me.query.ts +17 -0
- package/src/tenant/handlers/members.query.ts +22 -0
- package/src/tenant/handlers/memberships.query.ts +24 -0
- package/src/tenant/handlers/remove-member.write.ts +40 -0
- package/src/tenant/handlers/resolve-user-ids.query.ts +43 -0
- package/src/tenant/handlers/update-member-roles.write.ts +54 -0
- package/src/tenant/handlers/update.write.ts +20 -0
- package/src/tenant/index.ts +12 -0
- package/src/tenant/invitation-table.ts +93 -0
- package/src/tenant/membership-table.ts +35 -0
- package/src/tenant/schema/index.ts +5 -0
- package/src/tenant/schema/tenant.ts +27 -0
- package/src/tenant/seeding.ts +155 -0
- package/src/tenant/testing.ts +8 -0
- package/src/text-content/README.md +190 -0
- package/src/text-content/__tests__/text-content.integration.ts +415 -0
- package/src/text-content/api.ts +92 -0
- package/src/text-content/constants.ts +19 -0
- package/src/text-content/feature.ts +29 -0
- package/src/text-content/handlers/by-slug.query.ts +55 -0
- package/src/text-content/handlers/set.write.ts +118 -0
- package/src/text-content/index.ts +14 -0
- package/src/text-content/seeding.ts +91 -0
- package/src/text-content/table.ts +45 -0
- package/src/tier-engine/__tests__/compose-app.test.ts +182 -0
- package/src/tier-engine/__tests__/drift.test.ts +42 -0
- package/src/tier-engine/__tests__/tier-engine.integration.ts +241 -0
- package/src/tier-engine/aggregate-id.ts +27 -0
- package/src/tier-engine/compose-app.ts +150 -0
- package/src/tier-engine/constants.ts +15 -0
- package/src/tier-engine/entity.ts +30 -0
- package/src/tier-engine/feature.ts +72 -0
- package/src/tier-engine/handlers/active-tier.query.ts +23 -0
- package/src/tier-engine/index.ts +22 -0
- package/src/user/__tests__/seed-testing.integration.ts +127 -0
- package/src/user/__tests__/user.integration.ts +198 -0
- package/src/user/command-schemas.ts +15 -0
- package/src/user/constants.ts +23 -0
- package/src/user/feature.ts +32 -0
- package/src/user/handlers/create.write.ts +54 -0
- package/src/user/handlers/detail.query.ts +9 -0
- package/src/user/handlers/find-for-auth.query.ts +38 -0
- package/src/user/handlers/list.query.ts +8 -0
- package/src/user/handlers/me.query.ts +15 -0
- package/src/user/handlers/update.write.ts +54 -0
- package/src/user/index.ts +4 -0
- package/src/user/schema/index.ts +5 -0
- package/src/user/schema/user.ts +69 -0
- package/src/user/seeding.ts +93 -0
- package/src/user/testing.ts +5 -0
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
// Full-stack integration test for file-foundation. Drives the
|
|
2
|
+
// provider-factory through the dispatcher so the real config-resolver
|
|
3
|
+
// + secrets-context + tenant-scoped reads are exercised.
|
|
4
|
+
|
|
5
|
+
import { randomBytes } from "node:crypto";
|
|
6
|
+
import { createEncryptionProvider, type DbConnection } from "@cosmicdrift/kumiko-framework/db";
|
|
7
|
+
import { defineFeature, defineWriteHandler } from "@cosmicdrift/kumiko-framework/engine";
|
|
8
|
+
import { createEventsTable } from "@cosmicdrift/kumiko-framework/event-store";
|
|
9
|
+
import { createEnvMasterKeyProvider } from "@cosmicdrift/kumiko-framework/secrets";
|
|
10
|
+
import {
|
|
11
|
+
createEntityTable,
|
|
12
|
+
createTestUser,
|
|
13
|
+
pushTables,
|
|
14
|
+
setupTestStack,
|
|
15
|
+
type TestStack,
|
|
16
|
+
testTenantId,
|
|
17
|
+
} from "@cosmicdrift/kumiko-framework/stack";
|
|
18
|
+
import {
|
|
19
|
+
createMutableMasterKeyProvider,
|
|
20
|
+
type MutableMasterKeyProvider,
|
|
21
|
+
} from "@cosmicdrift/kumiko-framework/testing";
|
|
22
|
+
import { afterAll, beforeAll, describe, expect, test } from "vitest";
|
|
23
|
+
import { z } from "zod";
|
|
24
|
+
import { createConfigFeature } from "../../config";
|
|
25
|
+
import { ConfigHandlers } from "../../config/constants";
|
|
26
|
+
import { createConfigAccessorFactory } from "../../config/feature";
|
|
27
|
+
import { type ConfigResolver, createConfigResolver } from "../../config/resolver";
|
|
28
|
+
import { configValuesTable } from "../../config/table";
|
|
29
|
+
import { fileProviderS3Feature, S3_SECRET_ACCESS_KEY } from "../../file-provider-s3";
|
|
30
|
+
import { createSecretsContext, createSecretsFeature, tenantSecretsTable } from "../../secrets";
|
|
31
|
+
import { createTenantFeature } from "../../tenant/feature";
|
|
32
|
+
import { tenantEntity } from "../../tenant/schema/tenant";
|
|
33
|
+
import { createFileProviderForTenant, fileFoundationFeature } from "../feature";
|
|
34
|
+
|
|
35
|
+
// --- Test-Handler that exercises the factory end-to-end ---
|
|
36
|
+
|
|
37
|
+
const TEST_HANDLER_QN = "file-test:write:build-provider";
|
|
38
|
+
const testProbeFeature = defineFeature("file-test", (r) => {
|
|
39
|
+
r.requires("config");
|
|
40
|
+
r.requires("secrets");
|
|
41
|
+
r.writeHandler(
|
|
42
|
+
defineWriteHandler({
|
|
43
|
+
name: "build-provider",
|
|
44
|
+
schema: z.object({}),
|
|
45
|
+
access: { roles: ["TenantAdmin", "SystemAdmin"] },
|
|
46
|
+
handler: async (event, ctx) => {
|
|
47
|
+
const provider = await createFileProviderForTenant(
|
|
48
|
+
ctx,
|
|
49
|
+
event.user.tenantId,
|
|
50
|
+
TEST_HANDLER_QN,
|
|
51
|
+
);
|
|
52
|
+
return {
|
|
53
|
+
isSuccess: true,
|
|
54
|
+
data: {
|
|
55
|
+
hasWrite: typeof provider.write === "function",
|
|
56
|
+
hasRead: typeof provider.read === "function",
|
|
57
|
+
hasDelete: typeof provider.delete === "function",
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
},
|
|
61
|
+
}),
|
|
62
|
+
);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
// --- Setup ---
|
|
66
|
+
|
|
67
|
+
let stack: TestStack;
|
|
68
|
+
let db: DbConnection;
|
|
69
|
+
let resolver: ConfigResolver;
|
|
70
|
+
let providerRef: MutableMasterKeyProvider;
|
|
71
|
+
|
|
72
|
+
const testEncryptionKey = randomBytes(32).toString("base64");
|
|
73
|
+
|
|
74
|
+
beforeAll(async () => {
|
|
75
|
+
const encryption = createEncryptionProvider(testEncryptionKey);
|
|
76
|
+
resolver = createConfigResolver({ encryption });
|
|
77
|
+
|
|
78
|
+
const initialKp = createEnvMasterKeyProvider({
|
|
79
|
+
env: {
|
|
80
|
+
KUMIKO_SECRETS_MASTER_KEY_V1: randomBytes(32).toString("base64"),
|
|
81
|
+
KUMIKO_SECRETS_MASTER_KEY_CURRENT_VERSION: "1",
|
|
82
|
+
},
|
|
83
|
+
});
|
|
84
|
+
providerRef = createMutableMasterKeyProvider(initialKp);
|
|
85
|
+
|
|
86
|
+
stack = await setupTestStack({
|
|
87
|
+
features: [
|
|
88
|
+
createConfigFeature(),
|
|
89
|
+
createTenantFeature(),
|
|
90
|
+
createSecretsFeature(),
|
|
91
|
+
fileFoundationFeature,
|
|
92
|
+
fileProviderS3Feature,
|
|
93
|
+
testProbeFeature,
|
|
94
|
+
],
|
|
95
|
+
masterKeyProvider: providerRef,
|
|
96
|
+
extraContext: ({ db, registry }) => ({
|
|
97
|
+
configResolver: resolver,
|
|
98
|
+
configEncryption: encryption,
|
|
99
|
+
_configAccessorFactory: createConfigAccessorFactory(registry, resolver),
|
|
100
|
+
secrets: createSecretsContext({ db, masterKeyProvider: providerRef }),
|
|
101
|
+
}),
|
|
102
|
+
});
|
|
103
|
+
db = stack.db;
|
|
104
|
+
|
|
105
|
+
await createEntityTable(db, tenantEntity);
|
|
106
|
+
await pushTables(db, { configValuesTable, tenant_secrets: tenantSecretsTable });
|
|
107
|
+
await createEventsTable(db);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
afterAll(async () => {
|
|
111
|
+
await stack.cleanup();
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
function adminFor(tenantNumber: number) {
|
|
115
|
+
return createTestUser({
|
|
116
|
+
id: tenantNumber,
|
|
117
|
+
tenantId: testTenantId(tenantNumber),
|
|
118
|
+
roles: ["TenantAdmin", "SystemAdmin"],
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async function setConfig(admin: ReturnType<typeof adminFor>, key: string, value: unknown) {
|
|
123
|
+
await stack.http.writeOk(ConfigHandlers.set, { key, value }, admin);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** Set the file-foundation provider-selector to "s3". */
|
|
127
|
+
async function selectS3Provider(admin: ReturnType<typeof adminFor>) {
|
|
128
|
+
await setConfig(admin, "file-foundation:config:provider", "s3");
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// --- Scenario 1: full happy-path roundtrip (Hetzner Object Storage shape) ---
|
|
132
|
+
|
|
133
|
+
describe("scenario 1: happy path", () => {
|
|
134
|
+
test("admin sets config + secret → factory builds working file-storage provider", async () => {
|
|
135
|
+
const admin = adminFor(501);
|
|
136
|
+
|
|
137
|
+
await selectS3Provider(admin);
|
|
138
|
+
// Hetzner Object Storage typical config — covers MinIO/R2/S3 too via
|
|
139
|
+
// endpoint + forcePathStyle. AccessKeyId is public-ish, secret goes
|
|
140
|
+
// into the encrypted secrets store.
|
|
141
|
+
await setConfig(admin, "file-provider-s3:config:bucket", "test-bucket");
|
|
142
|
+
await setConfig(admin, "file-provider-s3:config:region", "fsn1");
|
|
143
|
+
await setConfig(
|
|
144
|
+
admin,
|
|
145
|
+
"file-provider-s3:config:endpoint",
|
|
146
|
+
"https://fsn1.your-objectstorage.com",
|
|
147
|
+
);
|
|
148
|
+
await setConfig(admin, "file-provider-s3:config:force-path-style", true);
|
|
149
|
+
await setConfig(admin, "file-provider-s3:config:access-key-id", "AKIATEST123");
|
|
150
|
+
|
|
151
|
+
await stack.http.writeOk(
|
|
152
|
+
"secrets:write:set",
|
|
153
|
+
{ key: S3_SECRET_ACCESS_KEY.name, value: "secret-key-not-actually-real" },
|
|
154
|
+
admin,
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
const result = (await stack.http.writeOk(TEST_HANDLER_QN, {}, admin)) as Record<
|
|
158
|
+
string,
|
|
159
|
+
unknown
|
|
160
|
+
>;
|
|
161
|
+
expect(result["hasWrite"]).toBe(true);
|
|
162
|
+
expect(result["hasRead"]).toBe(true);
|
|
163
|
+
expect(result["hasDelete"]).toBe(true);
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
// --- Scenario 2: validation errors ---
|
|
168
|
+
|
|
169
|
+
describe("scenario 2: validation errors", () => {
|
|
170
|
+
test("missing bucket → factory throws with hint instead of cryptic SDK error", async () => {
|
|
171
|
+
const admin = adminFor(502);
|
|
172
|
+
|
|
173
|
+
await selectS3Provider(admin);
|
|
174
|
+
// Set everything except bucket. requireNonEmpty rejects with a clear
|
|
175
|
+
// message naming `bucket`.
|
|
176
|
+
await setConfig(admin, "file-provider-s3:config:region", "us-east-1");
|
|
177
|
+
await setConfig(admin, "file-provider-s3:config:access-key-id", "AKIATEST");
|
|
178
|
+
await stack.http.writeOk(
|
|
179
|
+
"secrets:write:set",
|
|
180
|
+
{ key: S3_SECRET_ACCESS_KEY.name, value: "secret" },
|
|
181
|
+
admin,
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
const error = await stack.http.writeErr(TEST_HANDLER_QN, {}, admin);
|
|
185
|
+
expect(JSON.stringify(error)).toMatch(/'bucket' is empty/);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
test("missing secret-access-key → factory throws naming the secret", async () => {
|
|
189
|
+
const admin = adminFor(503);
|
|
190
|
+
|
|
191
|
+
await selectS3Provider(admin);
|
|
192
|
+
await setConfig(admin, "file-provider-s3:config:bucket", "b");
|
|
193
|
+
await setConfig(admin, "file-provider-s3:config:region", "us-east-1");
|
|
194
|
+
await setConfig(admin, "file-provider-s3:config:access-key-id", "AKIATEST");
|
|
195
|
+
// Skip the secret. Factory throws referencing S3_SECRET_ACCESS_KEY.name.
|
|
196
|
+
|
|
197
|
+
const error = await stack.http.writeErr(TEST_HANDLER_QN, {}, admin);
|
|
198
|
+
expect(JSON.stringify(error)).toMatch(/s3-secret-access-key/);
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
// --- Scenario 3: tenant isolation ---
|
|
203
|
+
|
|
204
|
+
describe("scenario 3: tenant isolation", () => {
|
|
205
|
+
test("tenant A's S3 config doesn't bleed into tenant B's provider", async () => {
|
|
206
|
+
const adminA = adminFor(504);
|
|
207
|
+
const adminB = adminFor(505);
|
|
208
|
+
|
|
209
|
+
await selectS3Provider(adminA);
|
|
210
|
+
await selectS3Provider(adminB);
|
|
211
|
+
|
|
212
|
+
await setConfig(adminA, "file-provider-s3:config:bucket", "tenant-a-bucket");
|
|
213
|
+
await setConfig(adminA, "file-provider-s3:config:region", "fsn1");
|
|
214
|
+
await setConfig(adminA, "file-provider-s3:config:access-key-id", "A-KEY");
|
|
215
|
+
await stack.http.writeOk(
|
|
216
|
+
"secrets:write:set",
|
|
217
|
+
{ key: S3_SECRET_ACCESS_KEY.name, value: "secret-a" },
|
|
218
|
+
adminA,
|
|
219
|
+
);
|
|
220
|
+
|
|
221
|
+
await setConfig(adminB, "file-provider-s3:config:bucket", "tenant-b-bucket");
|
|
222
|
+
await setConfig(adminB, "file-provider-s3:config:region", "us-east-1");
|
|
223
|
+
await setConfig(adminB, "file-provider-s3:config:access-key-id", "B-KEY");
|
|
224
|
+
await stack.http.writeOk(
|
|
225
|
+
"secrets:write:set",
|
|
226
|
+
{ key: S3_SECRET_ACCESS_KEY.name, value: "secret-b" },
|
|
227
|
+
adminB,
|
|
228
|
+
);
|
|
229
|
+
|
|
230
|
+
const a = (await stack.http.writeOk(TEST_HANDLER_QN, {}, adminA)) as Record<string, unknown>;
|
|
231
|
+
const b = (await stack.http.writeOk(TEST_HANDLER_QN, {}, adminB)) as Record<string, unknown>;
|
|
232
|
+
expect(a["hasWrite"]).toBe(true);
|
|
233
|
+
expect(b["hasWrite"]).toBe(true);
|
|
234
|
+
});
|
|
235
|
+
});
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
// kumiko-feature-version: 1
|
|
2
|
+
//
|
|
3
|
+
// file-foundation as a Kumiko bundled feature — plugin-API shape.
|
|
4
|
+
//
|
|
5
|
+
// **Pattern-Vorbild:** identisch zu `mail-foundation`. Foundation
|
|
6
|
+
// deklariert extension-point `fileProvider`, Provider-Features (file-
|
|
7
|
+
// provider-s3, später file-provider-azure-blob, file-provider-gcs)
|
|
8
|
+
// registrieren sich namentlich. Tenant wählt zur Runtime via config-
|
|
9
|
+
// key `provider`.
|
|
10
|
+
//
|
|
11
|
+
// **Was diese Foundation NICHT mehr macht:**
|
|
12
|
+
// - Keine S3-spezifischen Config-Keys mehr (bucket/region/endpoint/
|
|
13
|
+
// forcePathStyle/accessKeyId) — die leben im Provider-Plugin.
|
|
14
|
+
// - Kein direkter Import von `createS3Provider`. Foundation kennt
|
|
15
|
+
// nur das `FileStorageProvider`-Interface (Type-Import, kein
|
|
16
|
+
// runtime-coupling).
|
|
17
|
+
//
|
|
18
|
+
// **Standalone:** Foundation ist ohne tier-engine nutzbar. Existing
|
|
19
|
+
// `files-provider-s3` (App-wide-Library) bleibt unangetastet.
|
|
20
|
+
//
|
|
21
|
+
// **Boot-Dependencies:** config (für provider-selector). Kein secrets,
|
|
22
|
+
// weil Foundation selbst keine Secrets hält.
|
|
23
|
+
|
|
24
|
+
import { requireDefined } from "@cosmicdrift/kumiko-bundled-features/foundation-shared";
|
|
25
|
+
import {
|
|
26
|
+
access,
|
|
27
|
+
createTenantConfig,
|
|
28
|
+
defineFeature,
|
|
29
|
+
type HandlerContext,
|
|
30
|
+
} from "@cosmicdrift/kumiko-framework/engine";
|
|
31
|
+
import type { FileStorageProvider } from "@cosmicdrift/kumiko-framework/files";
|
|
32
|
+
|
|
33
|
+
const FEATURE_NAME = "file-foundation";
|
|
34
|
+
|
|
35
|
+
// =============================================================================
|
|
36
|
+
// Plugin-Interface — what a Provider-Plugin must implement
|
|
37
|
+
// =============================================================================
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* File-Storage-Plugin contract. Each provider-feature (file-provider-s3,
|
|
41
|
+
* file-provider-azure-blob, ...) registers an implementation via
|
|
42
|
+
* `r.useExtension("fileProvider", "<name>", { build })`.
|
|
43
|
+
*/
|
|
44
|
+
export type FileProviderPlugin = {
|
|
45
|
+
readonly build: (ctx: HandlerContext, tenantId: string) => Promise<FileStorageProvider>;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
// =============================================================================
|
|
49
|
+
// Feature-definition
|
|
50
|
+
// =============================================================================
|
|
51
|
+
|
|
52
|
+
export const fileFoundationFeature = defineFeature(FEATURE_NAME, (r) => {
|
|
53
|
+
r.requires("config");
|
|
54
|
+
|
|
55
|
+
r.extendsRegistrar("fileProvider", {
|
|
56
|
+
onRegister: () => {
|
|
57
|
+
// No side-effects at register-time — registry stores the usage,
|
|
58
|
+
// factory looks it up at request-time.
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
const configKeys = r.config({
|
|
63
|
+
keys: {
|
|
64
|
+
provider: createTenantConfig("text", {
|
|
65
|
+
default: "",
|
|
66
|
+
write: access.roles("TenantAdmin", "SystemAdmin"),
|
|
67
|
+
read: access.roles("TenantAdmin", "SystemAdmin", "User"),
|
|
68
|
+
}),
|
|
69
|
+
},
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
return { configKeys };
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
// =============================================================================
|
|
76
|
+
// Provider-factory — looks up the registered plugin + delegates
|
|
77
|
+
// =============================================================================
|
|
78
|
+
|
|
79
|
+
export async function createFileProviderForTenant(
|
|
80
|
+
ctx: HandlerContext,
|
|
81
|
+
tenantId: string,
|
|
82
|
+
handlerName = "file-foundation:provider-factory",
|
|
83
|
+
): Promise<FileStorageProvider> {
|
|
84
|
+
const ctxConfig = ctx.config;
|
|
85
|
+
if (!ctxConfig) {
|
|
86
|
+
throw new Error(
|
|
87
|
+
`${handlerName}: ctx.config is missing — feature requires the config-feature mounted in the registry`,
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
if (!ctx.registry) {
|
|
91
|
+
throw new Error(
|
|
92
|
+
`${handlerName}: ctx.registry is missing — required to look up registered file-provider plugins`,
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const provider = requireDefined(
|
|
97
|
+
await ctxConfig(fileFoundationFeature.exports.configKeys.provider),
|
|
98
|
+
FEATURE_NAME,
|
|
99
|
+
"provider",
|
|
100
|
+
) as string;
|
|
101
|
+
if (provider.length === 0) {
|
|
102
|
+
const usages = ctx.registry.getExtensionUsages("fileProvider");
|
|
103
|
+
const known = usages.map((u) => u.entityName).join(", ") || "<none>";
|
|
104
|
+
throw new Error(
|
|
105
|
+
`${FEATURE_NAME}: no provider selected — set the 'provider' config-key to one of: ${known}. ` +
|
|
106
|
+
`Mount a file-provider-* feature first if no plugins are registered.`,
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const usages = ctx.registry.getExtensionUsages("fileProvider");
|
|
111
|
+
const usage = usages.find((u) => u.entityName === provider);
|
|
112
|
+
if (!usage) {
|
|
113
|
+
const known = usages.map((u) => u.entityName).join(", ") || "<none>";
|
|
114
|
+
throw new Error(
|
|
115
|
+
`${FEATURE_NAME}: provider "${provider}" not registered. Known: ${known}. ` +
|
|
116
|
+
`Mount the matching file-provider-${provider} feature.`,
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// @cast-boundary engine-payload — extension-usage carries unknown options
|
|
121
|
+
const plugin = usage.options as FileProviderPlugin;
|
|
122
|
+
return plugin.build(ctx, tenantId);
|
|
123
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// feature.ts contract tests for file-provider-inmemory.
|
|
2
|
+
|
|
3
|
+
import { describe, expect, test } from "vitest";
|
|
4
|
+
import { clearStorage, fileProviderInMemoryFeature, listKeys } from "../feature";
|
|
5
|
+
|
|
6
|
+
describe("fileProviderInMemoryFeature — shape", () => {
|
|
7
|
+
test("has the expected name", () => {
|
|
8
|
+
expect(fileProviderInMemoryFeature.name).toBe("file-provider-inmemory");
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
test("requires only file-foundation (no config, no secrets)", () => {
|
|
12
|
+
expect(fileProviderInMemoryFeature.requires).toContain("file-foundation");
|
|
13
|
+
expect(fileProviderInMemoryFeature.requires).not.toContain("config");
|
|
14
|
+
expect(fileProviderInMemoryFeature.requires).not.toContain("secrets");
|
|
15
|
+
});
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
describe("fileProviderInMemoryFeature — plugin-registration", () => {
|
|
19
|
+
test("registers itself under entityName 'inmemory' for file-foundation's extension", () => {
|
|
20
|
+
const usages = fileProviderInMemoryFeature.extensionUsages;
|
|
21
|
+
expect(
|
|
22
|
+
usages.some((u) => u.extensionName === "fileProvider" && u.entityName === "inmemory"),
|
|
23
|
+
).toBe(true);
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
describe("listKeys / clearStorage — per-tenant store helpers", () => {
|
|
28
|
+
test("listKeys liefert empty-array für unbekannten Tenant", () => {
|
|
29
|
+
expect(listKeys("never-touched")).toEqual([]);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test("clearStorage auf unbekannten Tenant ist no-op", () => {
|
|
33
|
+
expect(() => clearStorage("never-touched")).not.toThrow();
|
|
34
|
+
});
|
|
35
|
+
});
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// kumiko-feature-version: 1
|
|
2
|
+
//
|
|
3
|
+
// file-provider-inmemory — In-Memory-FileProvider für die file-
|
|
4
|
+
// foundation Plugin-API. Speichert Files in einem per-Tenant-Map
|
|
5
|
+
// statt in S3/Hetzner-Object-Storage. Für Demos, Sample-Apps und
|
|
6
|
+
// Tests ohne MinIO-Container.
|
|
7
|
+
//
|
|
8
|
+
// **Was diese Feature liefert:**
|
|
9
|
+
// 1. Plugin-Registration via `r.useExtension("fileProvider",
|
|
10
|
+
// "inmemory", { build })`.
|
|
11
|
+
// 2. **Pro-Tenant Storage.** Jeder Tenant kriegt einen eigenen
|
|
12
|
+
// InMemoryFileProvider — Tenant-Isolation by-design, keine
|
|
13
|
+
// Pfad-Konvention nötig.
|
|
14
|
+
//
|
|
15
|
+
// **Pattern-Vorbild:** mirrors file-provider-s3.
|
|
16
|
+
//
|
|
17
|
+
// **NICHT für Production.** Buffer ist Process-Memory, geht beim
|
|
18
|
+
// Restart verloren + wächst monoton mit jedem write.
|
|
19
|
+
|
|
20
|
+
import type { FileProviderPlugin } from "@cosmicdrift/kumiko-bundled-features/file-foundation";
|
|
21
|
+
import { defineFeature, type HandlerContext } from "@cosmicdrift/kumiko-framework/engine";
|
|
22
|
+
import {
|
|
23
|
+
createInMemoryFileProvider,
|
|
24
|
+
type FileStorageProvider,
|
|
25
|
+
type InMemoryFileProvider,
|
|
26
|
+
} from "@cosmicdrift/kumiko-framework/files";
|
|
27
|
+
|
|
28
|
+
const FEATURE_NAME = "file-provider-inmemory";
|
|
29
|
+
|
|
30
|
+
// =============================================================================
|
|
31
|
+
// Per-tenant in-memory store
|
|
32
|
+
// =============================================================================
|
|
33
|
+
|
|
34
|
+
const providersByTenant = new Map<string, InMemoryFileProvider>();
|
|
35
|
+
|
|
36
|
+
function getOrCreateProviderForTenant(tenantId: string): InMemoryFileProvider {
|
|
37
|
+
let provider = providersByTenant.get(tenantId);
|
|
38
|
+
if (!provider) {
|
|
39
|
+
provider = createInMemoryFileProvider();
|
|
40
|
+
providersByTenant.set(tenantId, provider);
|
|
41
|
+
}
|
|
42
|
+
return provider;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Demo/Test-Helper: liste die Keys eines Tenant-Storage. */
|
|
46
|
+
export function listKeys(tenantId: string): readonly string[] {
|
|
47
|
+
return providersByTenant.get(tenantId)?.keys() ?? [];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Demo/Test-Helper: leere den Tenant-Storage. */
|
|
51
|
+
export function clearStorage(tenantId: string): void {
|
|
52
|
+
providersByTenant.get(tenantId)?.clear();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// =============================================================================
|
|
56
|
+
// Feature-definition
|
|
57
|
+
// =============================================================================
|
|
58
|
+
|
|
59
|
+
export const fileProviderInMemoryFeature = defineFeature(FEATURE_NAME, (r) => {
|
|
60
|
+
// Kein r.requires("config") + kein r.requires("secrets") — der
|
|
61
|
+
// In-Memory-Provider hat keine Config + kein Secret. Nur die
|
|
62
|
+
// file-foundation muss da sein (Plugin-extension-point).
|
|
63
|
+
r.requires("file-foundation");
|
|
64
|
+
|
|
65
|
+
const plugin: FileProviderPlugin = {
|
|
66
|
+
build: async (_ctx: HandlerContext, tenantId: string): Promise<FileStorageProvider> => {
|
|
67
|
+
// Returnt den per-tenant Storage. Identitätsstabil zwischen calls
|
|
68
|
+
// damit accumulated state erhalten bleibt.
|
|
69
|
+
return getOrCreateProviderForTenant(tenantId);
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
r.useExtension("fileProvider", "inmemory", plugin);
|
|
73
|
+
});
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// feature.ts contract tests for file-provider-s3.
|
|
2
|
+
|
|
3
|
+
import { describe, expect, test } from "vitest";
|
|
4
|
+
import { fileProviderS3Feature, S3_SECRET_ACCESS_KEY } from "../feature";
|
|
5
|
+
|
|
6
|
+
describe("fileProviderS3Feature — shape", () => {
|
|
7
|
+
test("has the expected name", () => {
|
|
8
|
+
expect(fileProviderS3Feature.name).toBe("file-provider-s3");
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
test("requires config + secrets + file-foundation", () => {
|
|
12
|
+
expect(fileProviderS3Feature.requires).toContain("config");
|
|
13
|
+
expect(fileProviderS3Feature.requires).toContain("secrets");
|
|
14
|
+
expect(fileProviderS3Feature.requires).toContain("file-foundation");
|
|
15
|
+
});
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
describe("fileProviderS3Feature.exports — typed handles", () => {
|
|
19
|
+
test("exports.configKeys covers the S3-config knobs", () => {
|
|
20
|
+
const keys = fileProviderS3Feature.exports.configKeys;
|
|
21
|
+
expect(keys.bucket).toBeDefined();
|
|
22
|
+
expect(keys.region).toBeDefined();
|
|
23
|
+
expect(keys.endpoint).toBeDefined();
|
|
24
|
+
expect(keys.forcePathStyle).toBeDefined();
|
|
25
|
+
expect(keys.accessKeyId).toBeDefined();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test("exports.secretAccessKey is the S3_SECRET_ACCESS_KEY secret-handle (drift-pin)", () => {
|
|
29
|
+
expect(fileProviderS3Feature.exports.secretAccessKey).toBe(S3_SECRET_ACCESS_KEY);
|
|
30
|
+
expect(S3_SECRET_ACCESS_KEY.name).toBe("file-provider-s3:secret:s3-secret-access-key");
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
describe("S3_SECRET_ACCESS_KEY — generic redaction", () => {
|
|
35
|
+
const secretDef = fileProviderS3Feature.secretKeys["s3.secretAccessKey"];
|
|
36
|
+
|
|
37
|
+
test("redact preserves first 4 + last 4 chars on long keys", () => {
|
|
38
|
+
expect(secretDef?.redact).toBeDefined();
|
|
39
|
+
expect(secretDef?.redact?.("AKIA1234567890ABCDEFGHIJ7890klmn")).toMatch(/^AKIA\.\.\.klmn$/);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test("redact masks short keys completely", () => {
|
|
43
|
+
expect(secretDef?.redact?.("short")).toBe("•".repeat(5));
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
describe("fileProviderS3Feature — plugin-registration", () => {
|
|
48
|
+
test("registers itself under entityName 's3' for file-foundation's extension", () => {
|
|
49
|
+
const usages = fileProviderS3Feature.extensionUsages;
|
|
50
|
+
expect(usages.some((u) => u.extensionName === "fileProvider" && u.entityName === "s3")).toBe(
|
|
51
|
+
true,
|
|
52
|
+
);
|
|
53
|
+
});
|
|
54
|
+
});
|