@cosmicdrift/kumiko-bundled-features 0.307.0 → 0.309.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 +9 -9
- package/src/__tests__/access-invalidation-consumer.integration.test.ts +107 -0
- package/src/__tests__/access-invalidation-stream.integration.test.ts +1 -1
- package/src/__tests__/credential-scoped-sse-revoke.integration.test.ts +296 -0
- package/src/agent-tools/__tests__/agent-doc-lint.test.ts +1 -1
- package/src/agent-tools/__tests__/agent-manifest.test.ts +1 -1
- package/src/agent-tools/__tests__/agent-optout.test.ts +1 -1
- package/src/agent-tools/__tests__/agent-tools.integration.test.ts +1 -1
- package/src/agent-tools/__tests__/feature.test.ts +1 -1
- package/src/agent-tools/__tests__/tool-catalog.test.ts +1 -1
- package/src/agent-tools/agent-manifest.ts +1 -1
- package/src/audit/__tests__/escape-hatch-audit.integration.test.ts +1 -1
- package/src/audit/escape-hatch-audit-sink.ts +1 -1
- package/src/audit/handlers/details.query.ts +1 -1
- package/src/audit/handlers/list.query.ts +1 -1
- package/src/auth-email-password/__tests__/query-as-member.integration.test.ts +1 -1
- package/src/auth-email-password/__tests__/seed-admin-guarded.integration.test.ts +31 -0
- package/src/auth-email-password/changes.json +6 -0
- package/src/auth-email-password/feature.ts +1 -1
- package/src/auth-email-password/handlers/change-password.write.ts +1 -1
- package/src/auth-email-password/handlers/confirm-account-unlock.write.ts +1 -1
- package/src/auth-email-password/handlers/invite-accept-with-login.write.ts +1 -1
- package/src/auth-email-password/handlers/invite-accept.write.ts +1 -1
- package/src/auth-email-password/handlers/invite-create.write.ts +1 -1
- package/src/auth-email-password/handlers/invite-signup-complete.write.ts +1 -1
- package/src/auth-email-password/handlers/login.write.ts +1 -1
- package/src/auth-email-password/handlers/logout.write.ts +1 -1
- package/src/auth-email-password/handlers/reset-password.write.ts +1 -1
- package/src/auth-email-password/handlers/self-registration-status.query.ts +1 -1
- package/src/auth-email-password/handlers/signup-confirm.write.ts +1 -1
- package/src/auth-email-password/handlers/signup-request.write.ts +1 -1
- package/src/auth-email-password/handlers/token-request-handler.ts +1 -1
- package/src/auth-email-password/handlers/verify-email.write.ts +1 -1
- package/src/auth-email-password/password-policy.ts +1 -1
- package/src/auth-email-password/seeding.ts +16 -3
- package/src/auth-mfa/__tests__/session-scoped-sse-revoke.integration.test.ts +1 -1
- package/src/auth-mfa/handlers/disable.write.ts +1 -1
- package/src/auth-mfa/handlers/enable-confirm-preauth.write.ts +1 -1
- package/src/auth-mfa/handlers/enable-confirm.write.ts +1 -1
- package/src/auth-mfa/handlers/enable-start-preauth.write.ts +1 -1
- package/src/auth-mfa/handlers/enable-start.write.ts +1 -1
- package/src/auth-mfa/handlers/regenerate-recovery.write.ts +1 -1
- package/src/auth-mfa/handlers/status.query.ts +1 -1
- package/src/auth-mfa/handlers/verify.write.ts +1 -1
- package/src/billing-foundation/billing-info-query.ts +1 -1
- package/src/billing-foundation/events.ts +1 -1
- package/src/billing-foundation/handlers/create-checkout-session.write.ts +1 -1
- package/src/billing-foundation/handlers/create-portal-session.write.ts +1 -1
- package/src/billing-foundation/handlers/list-subscriptions.query.ts +1 -1
- package/src/billing-foundation/handlers/process-event.write.ts +1 -1
- package/src/billing-foundation/handlers/process-payment-event.write.ts +1 -1
- package/src/cap-counter/__tests__/cap-counter.integration.test.ts +1 -1
- package/src/cap-counter/__tests__/stock-cap-guard.integration.test.ts +1 -1
- package/src/cap-counter/__tests__/with-cap-enforcement.integration.test.ts +1 -1
- package/src/cap-counter/book-cap-usage.ts +1 -1
- package/src/cap-counter/handlers/get-counter.query.ts +1 -1
- package/src/cap-counter/handlers/increment-rolling.write.ts +1 -1
- package/src/cap-counter/handlers/increment.write.ts +1 -1
- package/src/cap-counter/handlers/mark-soft-warned.write.ts +1 -1
- package/src/cap-overview/handlers/caps-usage.query.ts +1 -1
- package/src/cap-overview/handlers/tenant-caps-list.query.ts +1 -1
- package/src/cap-overview/handlers/tenant-options.query.ts +1 -1
- package/src/channel-in-app/handlers/inbox.query.ts +1 -1
- package/src/channel-in-app/handlers/mark-all-read.write.ts +1 -1
- package/src/channel-in-app/handlers/mark-read.write.ts +1 -1
- package/src/channel-in-app/handlers/unread-count.query.ts +1 -1
- package/src/compliance-profiles/handlers/for-tenant.query.ts +1 -1
- package/src/compliance-profiles/handlers/list-profiles.query.ts +1 -1
- package/src/compliance-profiles/handlers/needs-profile.query.ts +1 -1
- package/src/compliance-profiles/handlers/set-profile.write.ts +1 -1
- package/src/compliance-profiles/handlers/sub-processors.query.ts +1 -1
- package/src/compliance-profiles-ops/handlers/tenants-missing-profile.query.ts +1 -1
- package/src/config/__tests__/backing-secrets.integration.test.ts +1 -1
- package/src/config/__tests__/config.integration.test.ts +1 -1
- package/src/config/__tests__/tz-resolution.integration.test.ts +1 -1
- package/src/config/handlers/cascade.query.ts +1 -1
- package/src/config/handlers/readiness.query.ts +1 -1
- package/src/config/handlers/reset.write.ts +1 -1
- package/src/config/handlers/schema.query.ts +1 -1
- package/src/config/handlers/set.write.ts +1 -1
- package/src/config/handlers/values.query.ts +1 -1
- package/src/crypto-shredding/handlers/forget-subject.write.ts +1 -1
- package/src/custom-fields/__tests__/audit-integration.integration.test.ts +1 -1
- package/src/custom-fields/__tests__/cross-tenant-field-delete.integration.test.ts +1 -1
- package/src/custom-fields/__tests__/cross-tenant-set-write.integration.test.ts +1 -1
- package/src/custom-fields/__tests__/custom-fields.integration.test.ts +1 -1
- package/src/custom-fields/__tests__/field-access.integration.test.ts +1 -1
- package/src/custom-fields/__tests__/quota.integration.test.ts +1 -1
- package/src/custom-fields/__tests__/retention.integration.test.ts +1 -1
- package/src/custom-fields/__tests__/user-data-rights.integration.test.ts +1 -1
- package/src/custom-fields/events.ts +1 -1
- package/src/custom-fields/feature.ts +1 -1
- package/src/custom-fields/handlers/clear-custom-field.write.ts +1 -1
- package/src/custom-fields/handlers/set-custom-field.write.ts +1 -1
- package/src/custom-fields/lib/value-schema.ts +1 -1
- package/src/custom-fields/schemas.ts +1 -1
- package/src/data-retention/__tests__/retention-cleanup-files.integration.test.ts +454 -0
- package/src/data-retention/changes.json +6 -0
- package/src/data-retention/feature.ts +1 -0
- package/src/data-retention/handlers/policy-for.query.ts +1 -1
- package/src/data-retention/override-schema.ts +1 -1
- package/src/data-retention/run-retention-cleanup.ts +312 -5
- package/src/delivery/__tests__/delivery-pii-kms.integration.test.ts +1 -1
- package/src/delivery/__tests__/delivery.integration.test.ts +1 -1
- package/src/delivery/events.ts +1 -1
- package/src/delivery/feature.ts +1 -1
- package/src/delivery/handlers/log.query.ts +1 -1
- package/src/delivery/handlers/preferences.query.ts +1 -1
- package/src/delivery/handlers/set-preference.write.ts +1 -1
- package/src/delivery/jobs.ts +1 -1
- package/src/delivery/unsubscribe.ts +1 -1
- package/src/derivatives-sharp/__tests__/derivatives-sharp.integration.test.ts +1 -1
- package/src/document-ingest-foundation/events.ts +1 -1
- package/src/document-ingest-foundation/feature.ts +1 -1
- package/src/feature-toggles/__tests__/feature-toggles.integration.test.ts +1 -1
- package/src/feature-toggles/events.ts +1 -1
- package/src/feature-toggles/handlers/list.query.ts +1 -1
- package/src/feature-toggles/handlers/registered.query.ts +1 -1
- package/src/feature-toggles/handlers/set.write.ts +1 -1
- package/src/file-derivatives/handlers/public-variant.query.ts +1 -1
- package/src/file-foundation/__tests__/file-foundation.integration.test.ts +1 -1
- package/src/files-tenant-data/handlers/sweep-orphaned-derivatives.job.ts +1 -1
- package/src/folders/handlers/delete-folder.write.ts +1 -1
- package/src/folders/schemas.ts +1 -1
- package/src/form-draft/schemas.ts +1 -1
- package/src/inbound-mail-foundation/events.ts +1 -1
- package/src/inbound-mail-foundation/handlers/connect-account.write.ts +1 -1
- package/src/inbound-mail-foundation/handlers/disconnect-account.write.ts +1 -1
- package/src/inbound-mail-foundation/handlers/ingest-message.write.ts +1 -1
- package/src/inbound-mail-foundation/handlers/list-accounts.query.ts +1 -1
- package/src/inbound-mail-foundation/handlers/list-messages.query.ts +1 -1
- package/src/inbound-mail-foundation/handlers/update-account.write.ts +1 -1
- package/src/inbound-provider-imap/credential-document.ts +1 -1
- package/src/jobs/__tests__/jobs-catalog.integration.test.ts +1 -1
- package/src/jobs/__tests__/tenant-job-failures.integration.test.ts +1 -1
- package/src/jobs/events.ts +1 -1
- package/src/jobs/handlers/catalog.query.ts +1 -1
- package/src/jobs/handlers/detail.query.ts +1 -1
- package/src/jobs/handlers/list.query.ts +1 -1
- package/src/jobs/handlers/projection-rebuild.job.ts +1 -1
- package/src/jobs/handlers/reindex-entity.job.ts +1 -1
- package/src/jobs/handlers/retry.write.ts +1 -1
- package/src/jobs/handlers/tenant-failures.query.ts +1 -1
- package/src/jobs/handlers/trigger.write.ts +1 -1
- package/src/ledger/handlers/reports.query.ts +1 -1
- package/src/ledger/schemas.ts +1 -1
- package/src/mail-foundation/__tests__/mail-foundation.integration.test.ts +1 -1
- package/src/managed-pages/handlers/branding.query.ts +1 -1
- package/src/managed-pages/handlers/by-slug.query.ts +1 -1
- package/src/managed-pages/handlers/by-tenant-published.query.ts +1 -1
- package/src/managed-pages/handlers/set.write.ts +1 -1
- package/src/notes-history/changes.json +7 -0
- package/src/notes-history/schemas.ts +1 -1
- package/src/notes-history/web/notes-section.tsx +37 -36
- package/src/personal-access-tokens/feature.ts +10 -0
- package/src/personal-access-tokens/handlers/available-scopes.query.ts +1 -1
- package/src/personal-access-tokens/handlers/create.write.ts +1 -1
- package/src/personal-access-tokens/handlers/list.query.ts +1 -1
- package/src/personal-access-tokens/handlers/revoke.write.ts +18 -2
- package/src/personal-access-tokens/index.ts +7 -0
- package/src/personal-access-tokens/pat-revoked-event.ts +23 -0
- package/src/personal-access-tokens/revoke-for-user.ts +31 -0
- package/src/rate-limiting/__tests__/rate-limiting.integration.test.ts +1 -1
- package/src/rate-limiting/handlers/status.query.ts +1 -1
- package/src/readiness/handlers/status.query.ts +1 -1
- package/src/secrets/feature.ts +1 -1
- package/src/secrets/handlers/delete.write.ts +1 -1
- package/src/secrets/handlers/list.query.ts +1 -1
- package/src/secrets/handlers/set.write.ts +1 -1
- package/src/secrets/secrets-context.ts +1 -1
- package/src/seo/handlers/seo-config.query.ts +1 -1
- package/src/sessions/handlers/detail.query.ts +1 -1
- package/src/sessions/handlers/list.query.ts +1 -1
- package/src/sessions/handlers/mine.query.ts +1 -1
- package/src/sessions/handlers/revoke-all-for-user.write.ts +1 -1
- package/src/sessions/handlers/revoke-all-others.write.ts +1 -1
- package/src/sessions/handlers/revoke.write.ts +1 -1
- package/src/sessions/session-callbacks.ts +25 -1
- package/src/sessions/session-revoked-event.ts +6 -1
- package/src/shared/__tests__/row-bound-grant.integration.test.ts +1 -1
- package/src/step-dispatcher/mail-runner.ts +1 -1
- package/src/step-dispatcher/webhook-runner.ts +1 -1
- package/src/subscription-mollie/feature.ts +1 -1
- package/src/subscription-stripe/feature.ts +1 -1
- package/src/tags/schemas.ts +1 -1
- package/src/template-resolver/handlers/by-slug.query.ts +1 -1
- package/src/template-resolver/handlers/by-tenant.query.ts +1 -1
- package/src/template-resolver/handlers/collection-item.query.ts +1 -1
- package/src/template-resolver/handlers/collection-list.query.ts +1 -1
- package/src/template-resolver/handlers/collection-set.write.ts +1 -1
- package/src/template-resolver/handlers/find-by-id.query.ts +1 -1
- package/src/template-resolver/handlers/list.query.ts +1 -1
- package/src/template-resolver/handlers/set.write.ts +1 -1
- package/src/template-resolver/handlers/shared.ts +1 -1
- package/src/template-resolver/handlers/toggle-status.write.ts +1 -1
- package/src/template-resolver/handlers/upsert-tenant.write.ts +1 -1
- package/src/tenant/__tests__/tenant-timezone-boot.integration.test.ts +1 -1
- package/src/tenant/handlers/active-tenant-ids.query.ts +1 -1
- package/src/tenant/handlers/add-member.write.ts +1 -1
- package/src/tenant/handlers/cancel-invitation.write.ts +1 -1
- package/src/tenant/handlers/create.write.ts +1 -1
- package/src/tenant/handlers/invitations.query.ts +1 -1
- package/src/tenant/handlers/list.query.ts +1 -1
- package/src/tenant/handlers/me.query.ts +1 -1
- package/src/tenant/handlers/member-directory.query.ts +1 -1
- package/src/tenant/handlers/members.query.ts +1 -1
- package/src/tenant/handlers/memberships.query.ts +1 -1
- package/src/tenant/handlers/remove-member.write.ts +1 -1
- package/src/tenant/handlers/resolve-user-ids.query.ts +1 -1
- package/src/tenant/handlers/team-list.query.ts +1 -1
- package/src/tenant/handlers/tenant-directory.query.ts +1 -1
- package/src/tenant/handlers/toggle-enabled.write.ts +1 -1
- package/src/tenant/handlers/update-member-roles.write.ts +1 -1
- package/src/tenant/handlers/update.write.ts +1 -1
- package/src/tenant-handover/events.ts +1 -1
- package/src/tenant-handover/handlers/claim.write.ts +1 -1
- package/src/tenant-lifecycle/__tests__/tenant-lifecycle.integration.test.ts +8 -24
- package/src/tenant-lifecycle/events.ts +1 -1
- package/src/tenant-lifecycle/handlers/cancel-destruction.write.ts +1 -1
- package/src/tenant-lifecycle/handlers/request-destruction.write.ts +1 -1
- package/src/tenant-settings/tenant-defaults.ts +1 -1
- package/src/tier-engine/__tests__/tier-resolver.integration.test.ts +1 -1
- package/src/tier-engine/feature.ts +1 -1
- package/src/tier-engine/handlers/active-tier.query.ts +1 -1
- package/src/tier-engine/handlers/get-tenant-tier.query.ts +1 -1
- package/src/tier-engine/handlers/set-tenant-tier.write.ts +1 -1
- package/src/user/__tests__/user-global-tenancy.integration.test.ts +1 -1
- package/src/user/handlers/create.write.ts +1 -1
- package/src/user/handlers/find-for-auth.query.ts +1 -1
- package/src/user/handlers/me.query.ts +1 -1
- package/src/user/handlers/update.write.ts +1 -1
- package/src/user/roles-input-schema.ts +1 -1
- package/src/user/seeding.ts +29 -20
- package/src/user-data-rights/__tests__/boot-checks.test.ts +1 -1
- package/src/user-data-rights/changes.json +6 -0
- package/src/user-data-rights/feature.ts +1 -0
- package/src/user-data-rights/handlers/cancel-deletion.write.ts +1 -1
- package/src/user-data-rights/handlers/confirm-deletion-by-token.write.ts +1 -1
- package/src/user-data-rights/handlers/download-by-job.query.ts +1 -1
- package/src/user-data-rights/handlers/download-by-token.query.ts +1 -1
- package/src/user-data-rights/handlers/export-status.query.ts +1 -1
- package/src/user-data-rights/handlers/lift-restriction.write.ts +1 -1
- package/src/user-data-rights/handlers/list-download-attempts.query.ts +1 -1
- package/src/user-data-rights/handlers/my-audit-log.query.ts +1 -1
- package/src/user-data-rights/handlers/request-deletion-by-email.write.ts +1 -1
- package/src/user-data-rights/handlers/request-deletion.write.ts +1 -1
- package/src/user-data-rights/handlers/request-export.write.ts +1 -1
- package/src/user-data-rights/handlers/restrict-account.write.ts +1 -1
- package/src/user-data-rights/handlers/run-forget-cleanup.write.ts +1 -1
- package/src/user-data-rights-defaults/hooks/file-ref.userdata-hook.ts +8 -61
- package/src/user-profile/handlers/change-email.write.ts +1 -1
- package/src/workflow-runner/__tests__/pending-projection.integration.test.ts +1 -1
- package/src/workflow-runner/handlers/resume-run.write.ts +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-bundled-features",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.309.0",
|
|
4
4
|
"description": "Built-in features — tenant, user, auth, delivery. The stuff you'd rewrite anyway, already typed.",
|
|
5
5
|
"license": "BUSL-1.1",
|
|
6
6
|
"author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
|
|
@@ -133,12 +133,12 @@
|
|
|
133
133
|
"./workflow-runner": "./src/workflow-runner/index.ts"
|
|
134
134
|
},
|
|
135
135
|
"dependencies": {
|
|
136
|
-
"@cosmicdrift/kumiko-dispatcher-live": "0.
|
|
137
|
-
"@cosmicdrift/kumiko-framework": "0.
|
|
138
|
-
"@cosmicdrift/kumiko-headless": "0.
|
|
139
|
-
"@cosmicdrift/kumiko-renderer": "0.
|
|
140
|
-
"@cosmicdrift/kumiko-renderer-web": "0.
|
|
141
|
-
"@cosmicdrift/kumiko-types": "0.
|
|
136
|
+
"@cosmicdrift/kumiko-dispatcher-live": "0.309.0",
|
|
137
|
+
"@cosmicdrift/kumiko-framework": "0.309.0",
|
|
138
|
+
"@cosmicdrift/kumiko-headless": "0.309.0",
|
|
139
|
+
"@cosmicdrift/kumiko-renderer": "0.309.0",
|
|
140
|
+
"@cosmicdrift/kumiko-renderer-web": "0.309.0",
|
|
141
|
+
"@cosmicdrift/kumiko-types": "0.309.0",
|
|
142
142
|
"@mollie/api-client": "^4.5.0",
|
|
143
143
|
"@node-rs/argon2": "^2.0.2",
|
|
144
144
|
"@types/mailparser": "^3.4.6",
|
|
@@ -167,8 +167,8 @@
|
|
|
167
167
|
],
|
|
168
168
|
"devDependencies": {
|
|
169
169
|
"@testing-library/user-event": "^14.6.1",
|
|
170
|
-
"@cosmicdrift/kumiko-locale-de": "0.
|
|
171
|
-
"@cosmicdrift/kumiko-locale-es": "0.
|
|
170
|
+
"@cosmicdrift/kumiko-locale-de": "0.309.0",
|
|
171
|
+
"@cosmicdrift/kumiko-locale-es": "0.309.0",
|
|
172
172
|
"jsqr": "^1.4.0"
|
|
173
173
|
}
|
|
174
174
|
}
|
|
@@ -44,6 +44,11 @@ import { createAuthEmailPasswordFeature } from "../auth-email-password/feature";
|
|
|
44
44
|
import { createConfigFeature } from "../config";
|
|
45
45
|
import { createConfigResolver } from "../config/resolver";
|
|
46
46
|
import { configValuesTable } from "../config/table";
|
|
47
|
+
import { PatHandlers } from "../personal-access-tokens/constants";
|
|
48
|
+
import { createPersonalAccessTokensFeature } from "../personal-access-tokens/feature";
|
|
49
|
+
import { revokeAllPatTokensForUser } from "../personal-access-tokens/revoke-for-user";
|
|
50
|
+
import { apiTokenEntity, apiTokenTable } from "../personal-access-tokens/schema/api-token";
|
|
51
|
+
import type { PatScopeConfig } from "../personal-access-tokens/scopes";
|
|
47
52
|
import { makeSessionHelpers } from "../sessions/__tests__/test-helpers";
|
|
48
53
|
import { SessionHandlers } from "../sessions/constants";
|
|
49
54
|
import { createSessionsFeature } from "../sessions/feature";
|
|
@@ -66,6 +71,10 @@ const callbacks = createLateBoundHolder<SessionCallbacks>("session-callbacks");
|
|
|
66
71
|
const encryptionKey = randomBytes(32).toString("base64");
|
|
67
72
|
const TENANT = testTenantId(1);
|
|
68
73
|
|
|
74
|
+
const PAT_SCOPES: PatScopeConfig = {
|
|
75
|
+
probe: { label: "Probe", read: ["personal-access-tokens:query:mine"] },
|
|
76
|
+
};
|
|
77
|
+
|
|
69
78
|
// Collects every userId the consumer pushed an invalidation for during the
|
|
70
79
|
// running test, in push order. Reset in beforeEach.
|
|
71
80
|
let invalidated: string[];
|
|
@@ -90,6 +99,10 @@ beforeAll(async () => {
|
|
|
90
99
|
createAuthEmailPasswordFeature(),
|
|
91
100
|
authFoundationFeature,
|
|
92
101
|
createSessionsFeature({ autoRevokeOnPasswordChange: bound.asMassRevoker() }),
|
|
102
|
+
createPersonalAccessTokensFeature({
|
|
103
|
+
scopes: PAT_SCOPES,
|
|
104
|
+
autoRevokeOnPasswordChange: (userId) => revokeAllPatTokensForUser(stack.db, userId),
|
|
105
|
+
}),
|
|
93
106
|
],
|
|
94
107
|
extraContext: { configResolver: resolver, configEncryption: encryption },
|
|
95
108
|
authConfig: {
|
|
@@ -107,6 +120,7 @@ beforeAll(async () => {
|
|
|
107
120
|
await unsafeCreateEntityTable(stack.db, userEntity);
|
|
108
121
|
await unsafeCreateEntityTable(stack.db, tenantEntity);
|
|
109
122
|
await unsafeCreateEntityTable(stack.db, userSessionEntity);
|
|
123
|
+
await unsafeCreateEntityTable(stack.db, apiTokenEntity);
|
|
110
124
|
await unsafePushTables(stack.db, { configValuesTable, tenantMembershipsTable });
|
|
111
125
|
});
|
|
112
126
|
|
|
@@ -119,6 +133,7 @@ beforeEach(async () => {
|
|
|
119
133
|
userTable,
|
|
120
134
|
tenantMembershipsTable,
|
|
121
135
|
userSessionTable,
|
|
136
|
+
apiTokenTable,
|
|
122
137
|
eventsTable,
|
|
123
138
|
]);
|
|
124
139
|
invalidated = [];
|
|
@@ -309,4 +324,96 @@ describe("access-invalidation event consumer", () => {
|
|
|
309
324
|
expect(sessionRow).toBeTruthy();
|
|
310
325
|
expect(sessionRow?.revokedAt).not.toBeNull();
|
|
311
326
|
});
|
|
327
|
+
|
|
328
|
+
test("session-revoked via logout (sessionRevoker, streamScope: revoked-sessions) still pushes an invalidation for that user", async () => {
|
|
329
|
+
const { userId } = await h.seedUser("logout-invalidates@example.com", "pw-long-enough");
|
|
330
|
+
const { token } = await h.login("logout-invalidates@example.com", "pw-long-enough");
|
|
331
|
+
trackInvalidation(userId);
|
|
332
|
+
|
|
333
|
+
// The raw sessionRevoker callback behind /api/auth/logout used to append
|
|
334
|
+
// no event at all — it now sets streamScope: "revoked-sessions"
|
|
335
|
+
// instead of the old userwide default, but a broad
|
|
336
|
+
// subscribeAccessInvalidation listener (no credential passed here, same
|
|
337
|
+
// as every other test in this file) still fires: this proves the new
|
|
338
|
+
// narrow scope still reaches the consumer, not just the pre-existing
|
|
339
|
+
// userwide paths above.
|
|
340
|
+
const logoutRes = await h.authedPost("/api/auth/logout", token);
|
|
341
|
+
expect(logoutRes.status).toBe(200);
|
|
342
|
+
|
|
343
|
+
const events = await selectMany(stack.db, eventsTable, {
|
|
344
|
+
type: "sessions:event:session-revoked",
|
|
345
|
+
});
|
|
346
|
+
expect(events.length).toBeGreaterThan(0);
|
|
347
|
+
|
|
348
|
+
await stack.eventDispatcher?.runOnce();
|
|
349
|
+
|
|
350
|
+
expect(invalidated).toEqual([userId]);
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
test("pat-revoked (self-service PAT revoke) pushes an invalidation for that user", async () => {
|
|
354
|
+
const { userId } = await h.seedUser("pat-revoke-invalidates@example.com", "pw-long-enough");
|
|
355
|
+
const { token } = await h.login("pat-revoke-invalidates@example.com", "pw-long-enough");
|
|
356
|
+
trackInvalidation(userId);
|
|
357
|
+
|
|
358
|
+
const mint = await h.authedPost("/api/write", token, {
|
|
359
|
+
type: PatHandlers.create,
|
|
360
|
+
payload: {
|
|
361
|
+
name: "probe",
|
|
362
|
+
scopes: ["probe:read"],
|
|
363
|
+
currentPassword: "pw-long-enough",
|
|
364
|
+
},
|
|
365
|
+
});
|
|
366
|
+
expect(mint.status).toBe(200);
|
|
367
|
+
const { data } = (await mint.json()) as { data: { id: string } };
|
|
368
|
+
|
|
369
|
+
const revokeRes = await h.authedPost("/api/write", token, {
|
|
370
|
+
type: PatHandlers.revoke,
|
|
371
|
+
payload: { id: data.id },
|
|
372
|
+
});
|
|
373
|
+
expect(revokeRes.status).toBe(200);
|
|
374
|
+
|
|
375
|
+
const events = await selectMany(stack.db, eventsTable, {
|
|
376
|
+
type: "personal-access-tokens:event:pat-revoked",
|
|
377
|
+
});
|
|
378
|
+
expect(events.length).toBeGreaterThan(0);
|
|
379
|
+
|
|
380
|
+
await stack.eventDispatcher?.runOnce();
|
|
381
|
+
|
|
382
|
+
expect(invalidated).toEqual([userId]);
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
test("revokeAllPatTokensForUser (password-change auto-revoke) pushes an invalidation for that user", async () => {
|
|
386
|
+
const { userId } = await h.seedUser("pat-mass-invalidates@example.com", "old-pw-long-enough");
|
|
387
|
+
const { token } = await h.login("pat-mass-invalidates@example.com", "old-pw-long-enough");
|
|
388
|
+
trackInvalidation(userId);
|
|
389
|
+
|
|
390
|
+
const mint = await h.authedPost("/api/write", token, {
|
|
391
|
+
type: PatHandlers.create,
|
|
392
|
+
payload: {
|
|
393
|
+
name: "probe",
|
|
394
|
+
scopes: ["probe:read"],
|
|
395
|
+
currentPassword: "old-pw-long-enough",
|
|
396
|
+
},
|
|
397
|
+
});
|
|
398
|
+
expect(mint.status).toBe(200);
|
|
399
|
+
|
|
400
|
+
const res = await h.authedPost("/api/write", token, {
|
|
401
|
+
type: AuthHandlers.changePassword,
|
|
402
|
+
payload: { oldPassword: "old-pw-long-enough", newPassword: "new-pw-long-enough" },
|
|
403
|
+
});
|
|
404
|
+
expect(res.status).toBe(200);
|
|
405
|
+
|
|
406
|
+
const events = await selectMany(stack.db, eventsTable, {
|
|
407
|
+
type: "personal-access-tokens:event:pat-revoked",
|
|
408
|
+
});
|
|
409
|
+
expect(events.length).toBeGreaterThan(0);
|
|
410
|
+
|
|
411
|
+
await stack.eventDispatcher?.runOnce();
|
|
412
|
+
|
|
413
|
+
// A password change fires both sessionMassRevoker (userwide,
|
|
414
|
+
// session-revoked) and this suite's PAT autoRevokeOnPasswordChange
|
|
415
|
+
// (pat-tokens-scoped, pat-revoked) — the broad listener here (no
|
|
416
|
+
// credential passed) fires once per publish, so twice for the same user.
|
|
417
|
+
expect(invalidated).toEqual([userId, userId]);
|
|
418
|
+
});
|
|
312
419
|
});
|
|
@@ -26,7 +26,7 @@ import {
|
|
|
26
26
|
createTestEnvelopeCipher,
|
|
27
27
|
resetTestTables,
|
|
28
28
|
} from "@cosmicdrift/kumiko-framework/testing";
|
|
29
|
-
import
|
|
29
|
+
import * as z from "zod";
|
|
30
30
|
import { AuthHandlers } from "../auth-email-password/constants";
|
|
31
31
|
import { createAuthEmailPasswordFeature } from "../auth-email-password/feature";
|
|
32
32
|
import { createConfigFeature } from "../config";
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
// Credential-scoped SSE revoke.
|
|
2
|
+
//
|
|
3
|
+
// Logout/tenant-switch (sessionRevoker, session-callbacks.ts) and PAT revoke
|
|
4
|
+
// (revoke.write.ts / revokeAllPatTokensForUser) used to make no event at
|
|
5
|
+
// all, so the access-invalidation consumer never heard about them and an
|
|
6
|
+
// already-open SSE stream on that exact credential survived until its
|
|
7
|
+
// JWT/token expired on its own. Both now append a scoped session-revoked /
|
|
8
|
+
// pat-revoked event and the consumer closes exactly the matching stream(s),
|
|
9
|
+
// leaving every other credential's stream of the same user open.
|
|
10
|
+
//
|
|
11
|
+
// Real stack throughout: setupTestStack + real HTTP (createLiveDispatcher for
|
|
12
|
+
// the SSE streams, stack.http/h.authedPost for the writes), never
|
|
13
|
+
// createTestDispatcher.
|
|
14
|
+
|
|
15
|
+
import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test";
|
|
16
|
+
import { randomBytes } from "node:crypto";
|
|
17
|
+
import {
|
|
18
|
+
authFoundationFeature,
|
|
19
|
+
resolveTokenVerifier,
|
|
20
|
+
} from "@cosmicdrift/kumiko-bundled-features/auth-foundation";
|
|
21
|
+
import { createLiveDispatcher } from "@cosmicdrift/kumiko-dispatcher-live";
|
|
22
|
+
import type { SessionCreator } from "@cosmicdrift/kumiko-framework/api";
|
|
23
|
+
import { createInMemoryLoginRateLimiter, generateToken } from "@cosmicdrift/kumiko-framework/api";
|
|
24
|
+
import { defineFeature } from "@cosmicdrift/kumiko-framework/engine";
|
|
25
|
+
import { eventsTable } from "@cosmicdrift/kumiko-framework/event-store";
|
|
26
|
+
import {
|
|
27
|
+
setupTestStack,
|
|
28
|
+
type TestStack,
|
|
29
|
+
testTenantId,
|
|
30
|
+
unsafeCreateEntityTable,
|
|
31
|
+
unsafePushTables,
|
|
32
|
+
} from "@cosmicdrift/kumiko-framework/stack";
|
|
33
|
+
import {
|
|
34
|
+
createLateBoundHolder,
|
|
35
|
+
createTestEnvelopeCipher,
|
|
36
|
+
resetTestTables,
|
|
37
|
+
} from "@cosmicdrift/kumiko-framework/testing";
|
|
38
|
+
import * as z from "zod";
|
|
39
|
+
import { AuthHandlers } from "../auth-email-password/constants";
|
|
40
|
+
import { createAuthEmailPasswordFeature } from "../auth-email-password/feature";
|
|
41
|
+
import { createConfigFeature } from "../config";
|
|
42
|
+
import { createConfigResolver } from "../config/resolver";
|
|
43
|
+
import { configValuesTable } from "../config/table";
|
|
44
|
+
import { PatHandlers } from "../personal-access-tokens/constants";
|
|
45
|
+
import { createPersonalAccessTokensFeature } from "../personal-access-tokens/feature";
|
|
46
|
+
import { revokeAllPatTokensForUser } from "../personal-access-tokens/revoke-for-user";
|
|
47
|
+
import { apiTokenEntity, apiTokenTable } from "../personal-access-tokens/schema/api-token";
|
|
48
|
+
import type { PatScopeConfig } from "../personal-access-tokens/scopes";
|
|
49
|
+
import { makeSessionHelpers } from "../sessions/__tests__/test-helpers";
|
|
50
|
+
import { createSessionsFeature } from "../sessions/feature";
|
|
51
|
+
import { userSessionEntity, userSessionTable } from "../sessions/schema/user-session";
|
|
52
|
+
import { createSessionCallbacks, type SessionCallbacks } from "../sessions/session-callbacks";
|
|
53
|
+
import { sessionCallbacksFromLateBound } from "../sessions/testing";
|
|
54
|
+
import { createTenantFeature } from "../tenant";
|
|
55
|
+
import { tenantMembershipsTable } from "../tenant/membership-table";
|
|
56
|
+
import { tenantEntity } from "../tenant/schema/tenant";
|
|
57
|
+
import { createUserFeature } from "../user/feature";
|
|
58
|
+
import { userEntity, userTable } from "../user/schema/user";
|
|
59
|
+
|
|
60
|
+
let stack: TestStack;
|
|
61
|
+
let h: ReturnType<typeof makeSessionHelpers>;
|
|
62
|
+
const callbacks = createLateBoundHolder<SessionCallbacks>("session-callbacks");
|
|
63
|
+
|
|
64
|
+
const encryptionKey = randomBytes(32).toString("base64");
|
|
65
|
+
const TENANT = testTenantId(1);
|
|
66
|
+
|
|
67
|
+
const SCOPES: PatScopeConfig = {
|
|
68
|
+
probe: { label: "Probe stream", read: ["stream-probe:stream:probe:tail"] },
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const chunkGates = new Map<string, { promise: Promise<void>; release: () => void }>();
|
|
72
|
+
|
|
73
|
+
function createGate(gateId: string): () => void {
|
|
74
|
+
let release!: () => void;
|
|
75
|
+
const promise = new Promise<void>((resolve) => {
|
|
76
|
+
release = resolve;
|
|
77
|
+
});
|
|
78
|
+
chunkGates.set(gateId, { promise, release });
|
|
79
|
+
return release;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const streamProbeFeature = defineFeature("stream-probe", (r) => {
|
|
83
|
+
r.streamHandler(
|
|
84
|
+
"probe:tail",
|
|
85
|
+
z.object({ gateId: z.string().min(1) }),
|
|
86
|
+
async function* (event) {
|
|
87
|
+
yield { phase: "open" as const };
|
|
88
|
+
const gate = chunkGates.get(event.payload.gateId);
|
|
89
|
+
if (!gate) throw new Error(`missing gate ${event.payload.gateId}`);
|
|
90
|
+
await gate.promise;
|
|
91
|
+
yield { phase: "survived" as const };
|
|
92
|
+
},
|
|
93
|
+
{ access: { roles: ["User"] } },
|
|
94
|
+
);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
function buildLiveFetch(bearerToken: string): {
|
|
98
|
+
readonly fetch: typeof fetch;
|
|
99
|
+
readonly csrfToken: string;
|
|
100
|
+
} {
|
|
101
|
+
const csrfToken = generateToken();
|
|
102
|
+
const fetchImpl = (async (url: unknown, init: RequestInit | undefined) => {
|
|
103
|
+
const reqInit: RequestInit = {
|
|
104
|
+
...(init ?? {}),
|
|
105
|
+
headers: {
|
|
106
|
+
...(init?.headers ?? {}),
|
|
107
|
+
Authorization: `Bearer ${bearerToken}`,
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
return stack.app.request(String(url), reqInit);
|
|
111
|
+
}) as unknown as typeof fetch;
|
|
112
|
+
return { fetch: fetchImpl, csrfToken };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async function openProbeStream(bearerToken: string): Promise<{
|
|
116
|
+
iter: AsyncIterator<{ phase: string }>;
|
|
117
|
+
release: () => void;
|
|
118
|
+
}> {
|
|
119
|
+
const gateId = generateToken();
|
|
120
|
+
const release = createGate(gateId);
|
|
121
|
+
const { fetch, csrfToken } = buildLiveFetch(bearerToken);
|
|
122
|
+
const dispatcher = createLiveDispatcher({ fetch, readCsrf: () => csrfToken });
|
|
123
|
+
const iter = dispatcher
|
|
124
|
+
.stream<{ phase: string }>("stream-probe:stream:probe:tail", { gateId })
|
|
125
|
+
[Symbol.asyncIterator]();
|
|
126
|
+
const first = await iter.next();
|
|
127
|
+
expect(first.done).toBe(false);
|
|
128
|
+
expect(first.value).toEqual({ phase: "open" });
|
|
129
|
+
return { iter, release };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// The append's NOTIFY can wake a dispatcher pass before the test's own
|
|
133
|
+
// runOnce(), and runOnce() skips a consumer that pass is still busy with.
|
|
134
|
+
// Draining first makes the invalidation land before the stream assertions.
|
|
135
|
+
async function deliverPendingEvents(): Promise<void> {
|
|
136
|
+
await stack.eventDispatcher?.drain();
|
|
137
|
+
await stack.eventDispatcher?.runOnce();
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async function mintPat(cookieToken: string): Promise<{ id: string; token: string }> {
|
|
141
|
+
const res = await h.authedPost("/api/write", cookieToken, {
|
|
142
|
+
type: PatHandlers.create,
|
|
143
|
+
payload: {
|
|
144
|
+
name: "probe-token",
|
|
145
|
+
scopes: ["probe:read"],
|
|
146
|
+
currentPassword: "pw-long-enough",
|
|
147
|
+
},
|
|
148
|
+
});
|
|
149
|
+
expect(res.status).toBe(200);
|
|
150
|
+
const body = (await res.json()) as { data: { id: string; token: string } };
|
|
151
|
+
return body.data;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
beforeAll(async () => {
|
|
155
|
+
const encryption = createTestEnvelopeCipher(encryptionKey);
|
|
156
|
+
const resolver = createConfigResolver({ cipher: encryption });
|
|
157
|
+
const bound = sessionCallbacksFromLateBound(callbacks);
|
|
158
|
+
|
|
159
|
+
stack = await setupTestStack({
|
|
160
|
+
features: [
|
|
161
|
+
createConfigFeature(),
|
|
162
|
+
createUserFeature(),
|
|
163
|
+
createTenantFeature(),
|
|
164
|
+
createAuthEmailPasswordFeature(),
|
|
165
|
+
authFoundationFeature,
|
|
166
|
+
createSessionsFeature({ autoRevokeOnPasswordChange: bound.asMassRevoker() }),
|
|
167
|
+
createPersonalAccessTokensFeature({
|
|
168
|
+
scopes: SCOPES,
|
|
169
|
+
autoRevokeOnPasswordChange: (userId) => revokeAllPatTokensForUser(stack.db, userId),
|
|
170
|
+
}),
|
|
171
|
+
streamProbeFeature,
|
|
172
|
+
],
|
|
173
|
+
extraContext: { configResolver: resolver, configEncryption: encryption },
|
|
174
|
+
authConfig: {
|
|
175
|
+
...bound.asAuthConfig(),
|
|
176
|
+
membershipQuery: "tenant:query:memberships",
|
|
177
|
+
loginHandler: AuthHandlers.login,
|
|
178
|
+
tokenVerifier: (raw: string) =>
|
|
179
|
+
resolveTokenVerifier({ db: stack.db, registry: stack.registry }, raw),
|
|
180
|
+
patRateLimiter: createInMemoryLoginRateLimiter(100, 60_000),
|
|
181
|
+
},
|
|
182
|
+
});
|
|
183
|
+
callbacks.set(createSessionCallbacks({ db: stack.db }));
|
|
184
|
+
h = makeSessionHelpers(stack, TENANT, bound.asAuthConfig().sessionCreator as SessionCreator);
|
|
185
|
+
|
|
186
|
+
await unsafeCreateEntityTable(stack.db, userEntity);
|
|
187
|
+
await unsafeCreateEntityTable(stack.db, tenantEntity);
|
|
188
|
+
await unsafeCreateEntityTable(stack.db, userSessionEntity);
|
|
189
|
+
await unsafeCreateEntityTable(stack.db, apiTokenEntity);
|
|
190
|
+
await unsafePushTables(stack.db, { configValuesTable, tenantMembershipsTable });
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
afterAll(async () => {
|
|
194
|
+
await stack.cleanup();
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
beforeEach(async () => {
|
|
198
|
+
await resetTestTables(stack.db, [
|
|
199
|
+
userTable,
|
|
200
|
+
tenantMembershipsTable,
|
|
201
|
+
userSessionTable,
|
|
202
|
+
apiTokenTable,
|
|
203
|
+
eventsTable,
|
|
204
|
+
]);
|
|
205
|
+
chunkGates.clear();
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
// Tests release a stream's gate BEFORE asserting it closed: the invalidation
|
|
209
|
+
// already fired during runOnce(), so a revoked stream still rejects, while a
|
|
210
|
+
// stream that was wrongly left open yields its next chunk and fails the
|
|
211
|
+
// assertion instead of hanging on a gate nobody releases.
|
|
212
|
+
describe("logout closes only that cookie session's SSE stream", () => {
|
|
213
|
+
test("session 1 logs out, session 1's stream closes, session 2's stream survives", async () => {
|
|
214
|
+
await h.seedUser("logout-scope@example.com", "pw-long-enough");
|
|
215
|
+
const session1 = await h.login("logout-scope@example.com", "pw-long-enough");
|
|
216
|
+
const session2 = await h.login("logout-scope@example.com", "pw-long-enough");
|
|
217
|
+
|
|
218
|
+
const stream1 = await openProbeStream(session1.token);
|
|
219
|
+
const stream2 = await openProbeStream(session2.token);
|
|
220
|
+
|
|
221
|
+
const logoutRes = await h.authedPost("/api/auth/logout", session1.token);
|
|
222
|
+
expect(logoutRes.status).toBe(200);
|
|
223
|
+
|
|
224
|
+
await deliverPendingEvents();
|
|
225
|
+
|
|
226
|
+
stream1.release();
|
|
227
|
+
|
|
228
|
+
await expect(stream1.iter.next()).rejects.toMatchObject({ code: "access_denied" });
|
|
229
|
+
|
|
230
|
+
stream2.release();
|
|
231
|
+
const survivedChunk = await stream2.iter.next();
|
|
232
|
+
expect(survivedChunk.done).toBe(false);
|
|
233
|
+
expect(survivedChunk.value).toEqual({ phase: "survived" });
|
|
234
|
+
});
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
describe("PAT revoke closes only that token's SSE stream", () => {
|
|
238
|
+
test("revoking PAT1 closes PAT1's stream, PAT2's stream and the cookie stream survive", async () => {
|
|
239
|
+
await h.seedUser("pat-scope@example.com", "pw-long-enough");
|
|
240
|
+
const session = await h.login("pat-scope@example.com", "pw-long-enough");
|
|
241
|
+
const pat1 = await mintPat(session.token);
|
|
242
|
+
const pat2 = await mintPat(session.token);
|
|
243
|
+
|
|
244
|
+
const cookieStream = await openProbeStream(session.token);
|
|
245
|
+
const pat1Stream = await openProbeStream(pat1.token);
|
|
246
|
+
const pat2Stream = await openProbeStream(pat2.token);
|
|
247
|
+
|
|
248
|
+
const revokeRes = await h.authedPost("/api/write", session.token, {
|
|
249
|
+
type: PatHandlers.revoke,
|
|
250
|
+
payload: { id: pat1.id },
|
|
251
|
+
});
|
|
252
|
+
expect(revokeRes.status).toBe(200);
|
|
253
|
+
|
|
254
|
+
await deliverPendingEvents();
|
|
255
|
+
|
|
256
|
+
pat1Stream.release();
|
|
257
|
+
|
|
258
|
+
await expect(pat1Stream.iter.next()).rejects.toMatchObject({ code: "access_denied" });
|
|
259
|
+
|
|
260
|
+
cookieStream.release();
|
|
261
|
+
pat2Stream.release();
|
|
262
|
+
const cookieSurvived = await cookieStream.iter.next();
|
|
263
|
+
expect(cookieSurvived.done).toBe(false);
|
|
264
|
+
expect(cookieSurvived.value).toEqual({ phase: "survived" });
|
|
265
|
+
const pat2Survived = await pat2Stream.iter.next();
|
|
266
|
+
expect(pat2Survived.done).toBe(false);
|
|
267
|
+
expect(pat2Survived.value).toEqual({ phase: "survived" });
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
test("revokeAllPatTokensForUser closes every PAT stream of the user, cookie stream survives", async () => {
|
|
271
|
+
const { userId } = await h.seedUser("pat-mass-scope@example.com", "pw-long-enough");
|
|
272
|
+
const session = await h.login("pat-mass-scope@example.com", "pw-long-enough");
|
|
273
|
+
const pat1 = await mintPat(session.token);
|
|
274
|
+
const pat2 = await mintPat(session.token);
|
|
275
|
+
|
|
276
|
+
const cookieStream = await openProbeStream(session.token);
|
|
277
|
+
const pat1Stream = await openProbeStream(pat1.token);
|
|
278
|
+
const pat2Stream = await openProbeStream(pat2.token);
|
|
279
|
+
|
|
280
|
+
const revokedCount = await revokeAllPatTokensForUser(stack.db, userId);
|
|
281
|
+
expect(revokedCount).toBe(2);
|
|
282
|
+
|
|
283
|
+
await deliverPendingEvents();
|
|
284
|
+
|
|
285
|
+
pat1Stream.release();
|
|
286
|
+
|
|
287
|
+
await expect(pat1Stream.iter.next()).rejects.toMatchObject({ code: "access_denied" });
|
|
288
|
+
pat2Stream.release();
|
|
289
|
+
await expect(pat2Stream.iter.next()).rejects.toMatchObject({ code: "access_denied" });
|
|
290
|
+
|
|
291
|
+
cookieStream.release();
|
|
292
|
+
const cookieSurvived = await cookieStream.iter.next();
|
|
293
|
+
expect(cookieSurvived.done).toBe(false);
|
|
294
|
+
expect(cookieSurvived.value).toEqual({ phase: "survived" });
|
|
295
|
+
});
|
|
296
|
+
});
|
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
defineWriteHandler,
|
|
5
5
|
resolveAgentExposure,
|
|
6
6
|
} from "@cosmicdrift/kumiko-framework/engine";
|
|
7
|
-
import
|
|
7
|
+
import * as z from "zod";
|
|
8
8
|
import { AgentDocGapKinds, findAgentDocGaps, formatAgentDocGap } from "../agent-doc-lint";
|
|
9
9
|
|
|
10
10
|
const OPEN_ACCESS = {
|
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
defineFeature,
|
|
8
8
|
} from "@cosmicdrift/kumiko-framework/engine";
|
|
9
9
|
import type { ReferenceFieldDef, TextFieldDef } from "@cosmicdrift/kumiko-framework/engine/types";
|
|
10
|
-
import
|
|
10
|
+
import * as z from "zod";
|
|
11
11
|
import { buildAgentManifest } from "../agent-manifest";
|
|
12
12
|
|
|
13
13
|
const gatedEntity = createEntity({
|
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
defineEntityListHandler,
|
|
8
8
|
defineFeature,
|
|
9
9
|
} from "@cosmicdrift/kumiko-framework/engine";
|
|
10
|
-
import
|
|
10
|
+
import * as z from "zod";
|
|
11
11
|
import { authFoundationFeature } from "../../auth-foundation";
|
|
12
12
|
import { AuthMfaHandlers, AuthMfaQueries, createAuthMfaFeature } from "../../auth-mfa";
|
|
13
13
|
import { createConfigFeature } from "../../config";
|
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
type TestStack,
|
|
17
17
|
unsafeCreateEntityTable,
|
|
18
18
|
} from "@cosmicdrift/kumiko-framework/stack";
|
|
19
|
-
import
|
|
19
|
+
import * as z from "zod";
|
|
20
20
|
import { buildAgentManifest } from "../agent-manifest";
|
|
21
21
|
import { buildToolCatalog, toolNameForQn } from "../tool-catalog";
|
|
22
22
|
import type { ToolDispatcher } from "../tool-dispatch";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, expect, spyOn, test } from "bun:test";
|
|
2
2
|
import { defineFeature, validateBoot } from "@cosmicdrift/kumiko-framework/engine";
|
|
3
|
-
import
|
|
3
|
+
import * as z from "zod";
|
|
4
4
|
import { AGENT_TOOLS_FEATURE_NAME, createAgentToolsFeature } from "../feature";
|
|
5
5
|
|
|
6
6
|
async function noopWriteHandler() {
|
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
createTextField,
|
|
7
7
|
defineFeature,
|
|
8
8
|
} from "@cosmicdrift/kumiko-framework/engine";
|
|
9
|
-
import
|
|
9
|
+
import * as z from "zod";
|
|
10
10
|
import { buildAgentManifest } from "../agent-manifest";
|
|
11
11
|
import { buildToolCatalog, toolNameForQn } from "../tool-catalog";
|
|
12
12
|
import type { AgentManifest, AgentToolMode, RegistrySearchView } from "../types";
|
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
type TestStack,
|
|
13
13
|
testTenantId,
|
|
14
14
|
} from "@cosmicdrift/kumiko-framework/stack";
|
|
15
|
-
import
|
|
15
|
+
import * as z from "zod";
|
|
16
16
|
import { createConfigFeature } from "../../config";
|
|
17
17
|
import { createTenantFeature } from "../../tenant";
|
|
18
18
|
import { createUserFeature } from "../../user";
|
|
@@ -2,7 +2,7 @@ import type { DbConnection } from "@cosmicdrift/kumiko-framework/db";
|
|
|
2
2
|
import type { EscapeHatchAuditSink, EscapeHatchKind } from "@cosmicdrift/kumiko-framework/engine";
|
|
3
3
|
import { append } from "@cosmicdrift/kumiko-framework/event-store";
|
|
4
4
|
import { generateId } from "@cosmicdrift/kumiko-framework/utils";
|
|
5
|
-
import
|
|
5
|
+
import * as z from "zod";
|
|
6
6
|
|
|
7
7
|
export const ESCAPE_HATCH_USED_EVENT = "audit:event:escape-hatch-used";
|
|
8
8
|
export const ESCAPE_HATCH_USE_AGGREGATE_TYPE = "escapeHatchUse";
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import { selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
6
6
|
import { access, defineQueryHandler } from "@cosmicdrift/kumiko-framework/engine";
|
|
7
7
|
import { eventsTable } from "@cosmicdrift/kumiko-framework/event-store";
|
|
8
|
-
import
|
|
8
|
+
import * as z from "zod";
|
|
9
9
|
|
|
10
10
|
export const detailsQuery = defineQueryHandler({
|
|
11
11
|
name: "details",
|
|
@@ -15,7 +15,7 @@ import { selectMany, type WhereObject } from "@cosmicdrift/kumiko-framework/bun-
|
|
|
15
15
|
import { access, defineQueryHandler } from "@cosmicdrift/kumiko-framework/engine";
|
|
16
16
|
import { eventsTable } from "@cosmicdrift/kumiko-framework/event-store";
|
|
17
17
|
import { Temporal } from "temporal-polyfill";
|
|
18
|
-
import
|
|
18
|
+
import * as z from "zod";
|
|
19
19
|
|
|
20
20
|
const MAX_LIMIT = 100;
|
|
21
21
|
|
|
@@ -179,4 +179,35 @@ describe("seedAdminGuarded (offlot#114)", () => {
|
|
|
179
179
|
expect(await userIds()).toHaveLength(2);
|
|
180
180
|
expect((await userIds()).slice().sort()).toEqual([unrelated.id, admin.id].sort());
|
|
181
181
|
});
|
|
182
|
+
|
|
183
|
+
test("a KMS without a blind-index key aborts instead of seeding a blind duplicate", async () => {
|
|
184
|
+
await seedAdminGuarded(stack.db, seedOptions);
|
|
185
|
+
resetBlindIndexKeyForTests();
|
|
186
|
+
|
|
187
|
+
await expect(seedAdminGuarded(stack.db, seedOptions)).rejects.toThrow(/blind-index key/);
|
|
188
|
+
expect(await userIds()).toHaveLength(1);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
test("existing-user branch reconciles emailVerified onto the canonical row", async () => {
|
|
192
|
+
const first = await seedAdminGuarded(stack.db, seedOptions);
|
|
193
|
+
await blankBlindIndex();
|
|
194
|
+
|
|
195
|
+
const second = await seedAdminGuarded(stack.db, { ...seedOptions, emailVerified: true });
|
|
196
|
+
|
|
197
|
+
expect(second.id).toBe(first.id);
|
|
198
|
+
expect(await userIds()).toEqual([first.id]);
|
|
199
|
+
const rows = await selectMany<{ id: string; emailVerified: boolean }>(stack.db, userTable);
|
|
200
|
+
expect(rows.find((r) => r.id === first.id)?.emailVerified).toBe(true);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
test("without a configured KMS, seeding runs the plaintext path unguarded", async () => {
|
|
204
|
+
resetPiiSubjectKmsForTests();
|
|
205
|
+
resetBlindIndexKeyForTests();
|
|
206
|
+
|
|
207
|
+
const first = await seedAdminGuarded(stack.db, seedOptions);
|
|
208
|
+
const second = await seedAdminGuarded(stack.db, seedOptions);
|
|
209
|
+
|
|
210
|
+
expect(second.id).toBe(first.id);
|
|
211
|
+
expect(await userIds()).toEqual([first.id]);
|
|
212
|
+
});
|
|
182
213
|
});
|
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.309.0",
|
|
4
|
+
"type": "fix",
|
|
5
|
+
"title": "Boot admin seed no longer duplicates the admin when the email blind-index lookup misses (fw#3101)",
|
|
6
|
+
"detail": "runProdApp and runDevApp used to call the unguarded `seedAdmin`, whose idempotency check is a plain email fetchOne that the query layer rewrites into `(email = $plaintext OR email_bidx = $hmac)` once a blind-index key is configured. Under KMS encryption `email` holds per-row ciphertext, so only the bidx arm can ever match — and it misses whenever a row's `email_bidx` is NULL (written before the key existed, or after a subject-key erase), silently inserting a second admin account for the same email. Both boot paths now call `seedAdminGuarded`, which under KMS decrypt-scans the active users for the email and reuses the oldest matching row instead of duplicating it. `seedAdminGuarded` also now aborts with a named error if a PII KMS is configured but no blind-index key is — that combination makes the lookup blind by construction — and reconciles `emailVerified: true` onto the existing row it resolves through the decrypt-scan branch, matching the existing-row reconcile `seedUser` already does on its own idempotency hit."
|
|
7
|
+
},
|
|
2
8
|
{
|
|
3
9
|
"version": "0.307.0",
|
|
4
10
|
"type": "improvement",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { defineFeature, type FeatureDefinition } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
|
-
import
|
|
2
|
+
import * as z from "zod";
|
|
3
3
|
import { MIN_HMAC_SECRET_LENGTH } from "./constants";
|
|
4
4
|
import type { AuthMailLocale } from "./email-templates";
|
|
5
5
|
import { changePasswordWrite } from "./handlers/change-password.write";
|