@cosmicdrift/kumiko-bundled-features 0.352.0 → 0.354.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent-tools/agent-doc-lint.d.ts +2 -0
- package/dist/agent-tools/agent-doc-lint.js +61 -1
- package/dist/agent-tools/agent-manifest.d.ts +1 -0
- package/dist/agent-tools/agent-manifest.js +17 -5
- package/dist/agent-tools/feature.js +7 -2
- package/dist/agent-tools/index.d.ts +1 -1
- package/dist/agent-tools/index.js +1 -1
- package/dist/agent-tools/manifest-deny-qns.d.ts +3 -0
- package/dist/agent-tools/manifest-deny-qns.js +17 -0
- package/dist/agent-tools/tool-catalog.d.ts +2 -1
- package/dist/agent-tools/tool-catalog.js +4 -1
- package/dist/agent-tools/tool-dispatch.js +1 -1
- package/dist/audit/handlers/list.query.js +13 -8
- package/dist/auth-email-password/handlers/change-password.write.js +1 -0
- package/dist/auth-email-password/handlers/invite-accept-with-login.write.js +2 -2
- package/dist/auth-email-password/handlers/invite-accept.write.js +2 -2
- package/dist/auth-email-password/handlers/invite-create.write.js +17 -0
- package/dist/auth-email-password/handlers/invite-signup-complete.write.js +4 -1
- package/dist/auth-email-password/handlers/login.write.js +1 -0
- package/dist/auth-email-password/handlers/reset-password.write.js +1 -0
- package/dist/auth-email-password/handlers/signup-confirm.write.js +6 -4
- package/dist/auth-email-password/handlers/signup-request.write.js +51 -77
- package/dist/auth-email-password/handlers/token-request-handler.js +1 -0
- package/dist/auth-email-password/handlers/verify-email.write.js +1 -0
- package/dist/auth-email-password/index.d.ts +2 -0
- package/dist/auth-email-password/index.js +2 -0
- package/dist/auth-email-password/seeding.d.ts +2 -0
- package/dist/auth-email-password/seeding.js +30 -8
- package/dist/auth-email-password/signup-activation.d.ts +24 -0
- package/dist/auth-email-password/signup-activation.js +41 -0
- package/dist/auth-email-password/signup-token-store.d.ts +5 -0
- package/dist/auth-email-password/signup-token-store.js +8 -3
- package/dist/auth-email-password/web/auth-gate.d.ts +2 -2
- package/dist/auth-email-password/web/auth-gate.js +5 -5
- package/dist/auth-email-password/web/auth-redirect.d.ts +1 -0
- package/dist/auth-email-password/web/auth-redirect.js +14 -0
- package/dist/auth-email-password/web/login-screen.js +2 -2
- package/dist/auth-email-password/web/user-menu.js +1 -1
- package/dist/auth-mfa/feature.js +5 -0
- package/dist/auth-mfa/handlers/enable-confirm-preauth.write.js +1 -0
- package/dist/auth-mfa/handlers/verify.write.js +1 -0
- package/dist/auth-mfa/schema/user-mfa.d.ts +6 -0
- package/dist/auth-mfa/screens.js +2 -2
- package/dist/auth-mfa/web/mfa-recovery-codes-reveal.js +1 -1
- package/dist/auth-mfa/web/mfa-setup-preauth-screen.d.ts +1 -1
- package/dist/auth-mfa/web/mfa-setup-preauth-screen.js +3 -3
- package/dist/auth-mfa/web/mfa-verify-screen.d.ts +1 -1
- package/dist/auth-mfa/web/mfa-verify-screen.js +1 -1
- package/dist/billing-foundation/billing-info-query.d.ts +5 -2
- package/dist/billing-foundation/checkout-core.d.ts +7 -0
- package/dist/billing-foundation/checkout-core.js +7 -2
- package/dist/billing-foundation/entities.d.ts +8 -0
- package/dist/billing-foundation/handlers/start-plan-checkout.write.js +7 -1
- package/dist/billing-foundation/projection.d.ts +8 -0
- package/dist/cap-counter/entity.d.ts +1 -0
- package/dist/cap-overview/access.js +2 -9
- package/dist/cap-overview/handlers/tenant-caps-list.query.js +2 -2
- package/dist/cap-overview/handlers/tenant-options.query.js +4 -5
- package/dist/channel-in-app/in-app-channel.js +1 -1
- package/dist/channel-in-app/tables.js +4 -2
- package/dist/compliance-profiles/schema/profile-selection.d.ts +10 -8
- package/dist/compliance-profiles-ops/handlers/tenants-missing-profile.query.js +2 -4
- package/dist/config/handlers/values.query.d.ts +3 -1
- package/dist/config/handlers/values.query.js +7 -2
- package/dist/config/table.d.ts +6 -0
- package/dist/crypto-shredding/handlers/forget-subject.write.d.ts +1 -0
- package/dist/crypto-shredding/handlers/forget-subject.write.js +110 -61
- package/dist/custom-fields/entity.d.ts +4 -0
- package/dist/data-retention/schema/tenant-retention-override.d.ts +4 -0
- package/dist/delivery/db/queries/address-opt-outs.d.ts +3 -1
- package/dist/delivery/db/queries/address-opt-outs.js +12 -7
- package/dist/delivery/delivery-service.js +60 -12
- package/dist/delivery/public-names.js +1 -1
- package/dist/delivery/tables.d.ts +6 -0
- package/dist/delivery/unsubscribe.js +66 -14
- package/dist/document-ingest-foundation/entity.d.ts +4 -0
- package/dist/feature-toggles/feature.js +2 -3
- package/dist/feature-toggles/toggle-runtime.d.ts +5 -1
- package/dist/feature-toggles/toggle-runtime.js +30 -6
- package/dist/feature-toggles/toggle-sync-signal.d.ts +3 -2
- package/dist/feature-toggles/toggle-sync-signal.js +29 -10
- package/dist/file-derivatives/handlers/public-variant.query.d.ts +1 -0
- package/dist/file-derivatives/handlers/public-variant.query.js +7 -1
- package/dist/files-tenant-data/handlers/sweep-orphaned-derivatives.job.js +5 -0
- package/dist/files-tenant-data/hooks.js +61 -24
- package/dist/folders/entity.d.ts +8 -0
- package/dist/folders/feature.js +2 -10
- package/dist/folders/handlers/delete-folder.write.js +31 -4
- package/dist/form-draft/db/queries/cleanup.d.ts +1 -1
- package/dist/form-draft/db/queries/cleanup.js +4 -3
- package/dist/form-draft/entity.d.ts +2 -0
- package/dist/form-draft/handlers/cleanup.job.d.ts +5 -1
- package/dist/form-draft/handlers/cleanup.job.js +11 -5
- package/dist/foundation-shared/index.d.ts +1 -1
- package/dist/foundation-shared/index.js +1 -1
- package/dist/foundation-shared/mail-host-policy.d.ts +4 -0
- package/dist/foundation-shared/mail-host-policy.js +13 -4
- package/dist/inbound-mail-foundation/entities.d.ts +25 -0
- package/dist/inbound-mail-foundation/projection.d.ts +20 -0
- package/dist/inbound-provider-imap/feature.js +2 -0
- package/dist/inbound-provider-imap/imap-client.js +3 -4
- package/dist/jobs/constants.d.ts +1 -0
- package/dist/jobs/constants.js +1 -0
- package/dist/jobs/db/queries/retention.d.ts +1 -0
- package/dist/jobs/db/queries/retention.js +5 -0
- package/dist/jobs/db/queries/stale-run-sweep.d.ts +2 -0
- package/dist/jobs/db/queries/stale-run-sweep.js +13 -2
- package/dist/jobs/feature.js +7 -1
- package/dist/jobs/handlers/list.query.d.ts +2 -1
- package/dist/jobs/handlers/list.query.js +40 -15
- package/dist/jobs/handlers/tenant-failures.query.js +1 -1
- package/dist/jobs/handlers/tenant-runs.query.d.ts +17 -0
- package/dist/jobs/handlers/tenant-runs.query.js +72 -0
- package/dist/jobs/i18n.js +1 -0
- package/dist/jobs/index.d.ts +2 -0
- package/dist/jobs/index.js +1 -0
- package/dist/jobs/job-run-logger.d.ts +2 -2
- package/dist/jobs/job-run-logger.js +98 -9
- package/dist/jobs/job-run-table.d.ts +6 -0
- package/dist/jobs/tenant-job-failure-table.js +15 -2
- package/dist/jobs/tenant-job-run-table.d.ts +4 -0
- package/dist/jobs/tenant-job-run-table.js +34 -0
- package/dist/ledger/entity.d.ts +24 -8
- package/dist/ledger/entity.js +17 -5
- package/dist/ledger/feature.js +3 -0
- package/dist/ledger/handlers/confirm-schedule-period.write.js +9 -3
- package/dist/ledger/handlers/create-transaction.write.js +1 -0
- package/dist/ledger/handlers/reverse-transaction.write.js +1 -0
- package/dist/ledger/schedule-subject-pair.d.ts +2 -0
- package/dist/ledger/schedule-subject-pair.js +24 -0
- package/dist/ledger/schemas.js +8 -2
- package/dist/mail-transport-smtp/feature.d.ts +3 -4
- package/dist/mail-transport-smtp/feature.js +4 -13
- package/dist/managed-pages/feature.js +28 -10
- package/dist/managed-pages/table.d.ts +12 -0
- package/dist/notes-history/entity.d.ts +12 -0
- package/dist/notes-history/entity.js +4 -0
- package/dist/notes-history/feature.js +2 -10
- package/dist/personal-access-tokens/handlers/create.write.js +1 -0
- package/dist/personal-access-tokens/schema/api-token.d.ts +6 -0
- package/dist/personal-access-tokens/screens.js +14 -3
- package/dist/secrets/table.d.ts +1 -0
- package/dist/seo/feature.js +28 -6
- package/dist/sessions/feature.d.ts +1 -0
- package/dist/sessions/feature.js +9 -8
- package/dist/sessions/handlers/detail.query.d.ts +1 -1
- package/dist/sessions/handlers/detail.query.js +2 -2
- package/dist/sessions/handlers/list.query.d.ts +1 -1
- package/dist/sessions/handlers/list.query.js +5 -3
- package/dist/sessions/schema/user-session.d.ts +4 -0
- package/dist/sessions/screens.d.ts +2 -2
- package/dist/sessions/screens.js +6 -7
- package/dist/shared/assert-no-where-rule-in-ownership-write.d.ts +2 -0
- package/dist/shared/assert-no-where-rule-in-ownership-write.js +18 -0
- package/dist/shared/encrypt-for-direct-write.js +10 -2
- package/dist/shared/index.d.ts +4 -3
- package/dist/shared/index.js +4 -3
- package/dist/shared/parent-visibility.d.ts +2 -1
- package/dist/shared/parent-visibility.js +21 -0
- package/dist/shared/rate-limited-text-response.d.ts +2 -0
- package/dist/shared/rate-limited-text-response.js +9 -0
- package/dist/shared/row-bound-grant.js +16 -2
- package/dist/shared/session-locale-field.d.ts +1 -0
- package/dist/shared/session-locale-field.js +6 -0
- package/dist/tags/entity.d.ts +9 -0
- package/dist/tags/feature.js +2 -10
- package/dist/template-resolver/api.d.ts +2 -2
- package/dist/template-resolver/handlers/list.query.d.ts +2 -0
- package/dist/template-resolver/handlers/list.query.js +2 -2
- package/dist/template-resolver/table.d.ts +48 -30
- package/dist/template-resolver/user-content-table.d.ts +34 -20
- package/dist/tenant/decrypt-tenant-names.d.ts +3 -0
- package/dist/tenant/decrypt-tenant-names.js +10 -0
- package/dist/tenant/feature.js +7 -1
- package/dist/tenant/handlers/me.query.js +5 -1
- package/dist/tenant/handlers/memberships.query.js +3 -2
- package/dist/tenant/handlers/tenant-directory.query.js +9 -3
- package/dist/tenant/handlers/toggle-enabled.write.js +2 -0
- package/dist/tenant/handlers/update-member-roles.write.js +1 -0
- package/dist/tenant/index.d.ts +1 -0
- package/dist/tenant/index.js +1 -0
- package/dist/tenant/invitation-table.d.ts +54 -8
- package/dist/tenant/invitation-table.js +6 -1
- package/dist/tenant/invited-membership.d.ts +8 -6
- package/dist/tenant/invited-membership.js +28 -7
- package/dist/tenant/membership-table.d.ts +4 -0
- package/dist/tenant/schema/tenant.d.ts +18 -10
- package/dist/tenant/schema/tenant.js +4 -6
- package/dist/tenant/screens.js +1 -1
- package/dist/tenant-handover/move-entity-graph.js +11 -18
- package/dist/tenant-handover/transfer-graph.d.ts +2 -21
- package/dist/tenant-handover/transfer-graph.js +8 -75
- package/dist/tenant-lifecycle/boot-checks.d.ts +1 -0
- package/dist/tenant-lifecycle/boot-checks.js +13 -0
- package/dist/tenant-lifecycle/constants.d.ts +3 -0
- package/dist/tenant-lifecycle/constants.js +5 -0
- package/dist/tenant-lifecycle/events.d.ts +5 -0
- package/dist/tenant-lifecycle/events.js +5 -0
- package/dist/tenant-lifecycle/feature.js +6 -3
- package/dist/tenant-lifecycle/run-tenant-destroy.d.ts +10 -5
- package/dist/tenant-lifecycle/run-tenant-destroy.js +59 -17
- package/dist/tenant-lifecycle/stages.d.ts +8 -1
- package/dist/tenant-lifecycle/stages.js +33 -4
- package/dist/tenant-lifecycle/testing.d.ts +8 -0
- package/dist/tenant-lifecycle/testing.js +42 -0
- package/dist/tier-engine/entity.d.ts +2 -0
- package/dist/tier-engine/handlers/set-tenant-tier.write.js +1 -0
- package/dist/tier-engine/tier-resolver.d.ts +2 -0
- package/dist/user/handlers/detail.query.js +2 -0
- package/dist/user/handlers/list.query.js +6 -2
- package/dist/user/handlers/update.write.js +1 -0
- package/dist/user/schema/user.d.ts +22 -8
- package/dist/user/seeding.d.ts +1 -0
- package/dist/user/seeding.js +20 -0
- package/dist/user-data-rights/boot-checks.js +5 -0
- package/dist/user-data-rights/handlers/confirm-deletion-by-token.write.js +2 -2
- package/dist/user-data-rights/handlers/request-deletion.write.js +1 -0
- package/dist/user-data-rights/index.d.ts +1 -0
- package/dist/user-data-rights/index.js +1 -0
- package/dist/user-data-rights/schema/download-attempt.d.ts +14 -0
- package/dist/user-data-rights/schema/download-attempt.js +17 -9
- package/dist/user-data-rights/schema/download-token.d.ts +8 -0
- package/dist/user-data-rights/schema/export-job.d.ts +16 -8
- package/dist/user-profile/handlers/change-email.write.js +8 -2
- package/dist/waitlist/entity.d.ts +22 -6
- package/dist/workflow-runner/event-trigger.d.ts +1 -0
- package/dist/workflow-runner/event-trigger.js +63 -15
- package/dist/workflow-runner/handlers/resume-run.write.js +8 -2
- package/dist/workflow-runner/runner.d.ts +2 -1
- package/dist/workflow-runner/runner.js +5 -1
- package/package.json +35 -10
- package/src/agent-tools/changes.json +12 -0
- package/src/audit/changes.json +6 -0
- package/src/auth-email-password/changes.json +24 -0
- package/src/auth-mfa/changes.json +7 -0
- package/src/billing-foundation/changes.json +6 -0
- package/src/channel-in-app/changes.json +8 -1
- package/src/crypto-shredding/changes.json +12 -0
- package/src/delivery/changes.json +6 -0
- package/src/files/changes.json +24 -0
- package/src/files-tenant-data/changes.json +5 -0
- package/src/folders/changes.json +6 -0
- package/src/form-draft/changes.json +7 -1
- package/src/inbound-provider-imap/changes.json +6 -0
- package/src/jobs/changes.json +30 -0
- package/src/ledger/changes.json +23 -0
- package/src/managed-pages/changes.json +6 -0
- package/src/notes-history/changes.json +13 -0
- package/src/seo/changes.json +5 -0
- package/src/template-resolver/changes.json +5 -0
- package/src/tenant/changes.json +19 -0
- package/src/tenant-handover/changes.json +12 -0
- package/src/tenant-lifecycle/changes.json +22 -0
- package/src/tier-engine/changes.json +5 -0
- package/src/user/changes.json +15 -0
- package/src/user-data-rights/changes.json +6 -0
- package/src/workflow-runner/changes.json +5 -0
- package/dist/shared/has-where-rule.d.ts +0 -2
- package/dist/shared/has-where-rule.js +0 -10
|
@@ -81,6 +81,8 @@ export function createNoteEntryEntity(access, parents) {
|
|
|
81
81
|
find: "none",
|
|
82
82
|
}),
|
|
83
83
|
},
|
|
84
|
+
// Every host detail screen reads its notes by tenant + host entity.
|
|
85
|
+
indexes: [{ columns: ["tenantId", "entityType", "entityId"] }],
|
|
84
86
|
});
|
|
85
87
|
}
|
|
86
88
|
export const noteEntryEntity = createNoteEntryEntity();
|
|
@@ -109,6 +111,8 @@ export function createNoteMentionEntity() {
|
|
|
109
111
|
}),
|
|
110
112
|
subjectId: createTextField({ required: true, maxLength: 64, personal: "ref" }),
|
|
111
113
|
},
|
|
114
|
+
// The forget cascade looks mentions up by subject within one tenant.
|
|
115
|
+
indexes: [{ columns: ["tenantId", "subjectId"] }],
|
|
112
116
|
});
|
|
113
117
|
}
|
|
114
118
|
export const noteMentionEntity = createNoteMentionEntity();
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
// a new entry, not an edit; see entity.ts for why that still satisfies GDPR
|
|
14
14
|
// erasure without a delete path.
|
|
15
15
|
import { defineEntityListHandler, defineFeature, } from "@cosmicdrift/kumiko-framework/engine";
|
|
16
|
-
import {
|
|
16
|
+
import { assertNoWhereRuleInOwnershipWrite } from "../shared/index.js";
|
|
17
17
|
import { DEFAULT_NOTES_HISTORY_ACCESS, NOTES_HISTORY_FEATURE_NAME } from "./constants.js";
|
|
18
18
|
import { createNoteEntryEntity, noteMentionEntity } from "./entity.js";
|
|
19
19
|
import { createNoteEntryExecutor } from "./executor.js";
|
|
@@ -58,15 +58,7 @@ export function createNotesHistoryFeature(opts = {}) {
|
|
|
58
58
|
opts.parents === undefined) {
|
|
59
59
|
return notesHistoryFeature;
|
|
60
60
|
}
|
|
61
|
-
|
|
62
|
-
throw new Error("createNotesHistoryFeature({ ownership }): ownership.write must not contain a " +
|
|
63
|
-
'`{ kind: "where" }` rule — where-rules are evaluated only at the SQL ' +
|
|
64
|
-
"layer (the read path, via buildOwnershipClause). Write paths that " +
|
|
65
|
-
"consult access.write (userCanCreateFieldRow/userCanWriteFieldRow) can't " +
|
|
66
|
-
"evaluate them, so such a rule can only ever deny — boot validation " +
|
|
67
|
-
"rejects it too (fw#2626). Use a `from()` rule for ownership.write, or " +
|
|
68
|
-
"leave it unset.");
|
|
69
|
-
}
|
|
61
|
+
assertNoWhereRuleInOwnershipWrite("createNotesHistoryFeature", opts.ownership?.write);
|
|
70
62
|
// A mount that accepts no parents at all can never take a note write —
|
|
71
63
|
// that's a config mistake, not a valid allowlist. Omit `parents` instead
|
|
72
64
|
// of passing an empty array.
|
|
@@ -41,6 +41,7 @@ export function createPatCreateHandler(opts = {}) {
|
|
|
41
41
|
},
|
|
42
42
|
},
|
|
43
43
|
escapeHatch: {
|
|
44
|
+
grants: ["systemIdentity"],
|
|
44
45
|
reason: "Reads the caller's own passwordHash (privileged-only field) via ctx.queryAs(SYSTEM, " +
|
|
45
46
|
"user:findForAuth) to re-verify the current password before minting the token. Also " +
|
|
46
47
|
"reads the MFA enrollment of the user being re-authenticated via the mfaVerifier callback.",
|
|
@@ -3,6 +3,7 @@ export declare const apiTokenEntity: import("@cosmicdrift/kumiko-types/fields").
|
|
|
3
3
|
readonly type: "text";
|
|
4
4
|
readonly description?: string;
|
|
5
5
|
readonly maxLength?: number;
|
|
6
|
+
readonly minLength?: number;
|
|
6
7
|
readonly required?: boolean;
|
|
7
8
|
readonly searchable?: boolean;
|
|
8
9
|
readonly sortable?: boolean;
|
|
@@ -24,6 +25,7 @@ export declare const apiTokenEntity: import("@cosmicdrift/kumiko-types/fields").
|
|
|
24
25
|
readonly type: "text";
|
|
25
26
|
readonly description?: string;
|
|
26
27
|
readonly maxLength?: number;
|
|
28
|
+
readonly minLength?: number;
|
|
27
29
|
readonly required?: boolean;
|
|
28
30
|
readonly searchable?: boolean;
|
|
29
31
|
readonly sortable?: boolean;
|
|
@@ -45,6 +47,7 @@ export declare const apiTokenEntity: import("@cosmicdrift/kumiko-types/fields").
|
|
|
45
47
|
readonly type: "text";
|
|
46
48
|
readonly description?: string;
|
|
47
49
|
readonly maxLength?: number;
|
|
50
|
+
readonly minLength?: number;
|
|
48
51
|
readonly required?: boolean;
|
|
49
52
|
readonly searchable?: boolean;
|
|
50
53
|
readonly sortable?: boolean;
|
|
@@ -66,6 +69,7 @@ export declare const apiTokenEntity: import("@cosmicdrift/kumiko-types/fields").
|
|
|
66
69
|
readonly type: "text";
|
|
67
70
|
readonly description?: string;
|
|
68
71
|
readonly maxLength?: number;
|
|
72
|
+
readonly minLength?: number;
|
|
69
73
|
readonly required?: boolean;
|
|
70
74
|
readonly searchable?: boolean;
|
|
71
75
|
readonly sortable?: boolean;
|
|
@@ -87,6 +91,7 @@ export declare const apiTokenEntity: import("@cosmicdrift/kumiko-types/fields").
|
|
|
87
91
|
readonly type: "text";
|
|
88
92
|
readonly description?: string;
|
|
89
93
|
readonly maxLength?: number;
|
|
94
|
+
readonly minLength?: number;
|
|
90
95
|
readonly required?: boolean;
|
|
91
96
|
readonly searchable?: boolean;
|
|
92
97
|
readonly sortable?: boolean;
|
|
@@ -108,6 +113,7 @@ export declare const apiTokenEntity: import("@cosmicdrift/kumiko-types/fields").
|
|
|
108
113
|
readonly type: "text";
|
|
109
114
|
readonly description?: string;
|
|
110
115
|
readonly maxLength?: number;
|
|
116
|
+
readonly minLength?: number;
|
|
111
117
|
readonly required?: boolean;
|
|
112
118
|
readonly searchable?: boolean;
|
|
113
119
|
readonly sortable?: boolean;
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { i18nKey, MAX_LIST_LIMIT, } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
|
-
import {
|
|
2
|
+
import { ACTION_FORM_ENTITY } from "@cosmicdrift/kumiko-framework/ui-types";
|
|
3
|
+
import { fieldOptionLabelKeyPrefix } from "@cosmicdrift/kumiko-headless";
|
|
4
|
+
import { PAT_FEATURE, PAT_MINT_SCREEN_ID, PAT_SCREEN_ID, PatHandlers, PatQueries, } from "./constants.js";
|
|
3
5
|
const PAT_STATUS_OPTION_KEY_PREFIX = "pat.list.status.";
|
|
6
|
+
// Same keys the mint form's `scopes` multiSelect options resolve through
|
|
7
|
+
// (see patScopeOptionTranslations), so list and form show identical labels.
|
|
8
|
+
const PAT_SCOPE_OPTION_KEY_PREFIX = fieldOptionLabelKeyPrefix(PAT_FEATURE, ACTION_FORM_ENTITY, "scopes");
|
|
4
9
|
// Both screens render only for tenants whose tier includes the feature; the
|
|
5
10
|
// availability query is rejected by the feature gate otherwise.
|
|
6
11
|
const PAT_SCREEN_VISIBLE_WHEN = {
|
|
@@ -20,11 +25,17 @@ export const patListScreen = {
|
|
|
20
25
|
defaultSort: { field: "createdAt", dir: "desc" },
|
|
21
26
|
columns: [
|
|
22
27
|
{ field: "name", label: i18nKey("pat.list.col.name") },
|
|
23
|
-
{ field: "prefix", label: i18nKey("pat.list.col.prefix") },
|
|
24
|
-
{
|
|
28
|
+
{ field: "prefix", label: i18nKey("pat.list.col.prefix"), sortable: false },
|
|
29
|
+
{
|
|
30
|
+
field: "scopes",
|
|
31
|
+
label: i18nKey("pat.list.col.scopes"),
|
|
32
|
+
sortable: false,
|
|
33
|
+
renderer: { format: "enumOption", keyPrefix: PAT_SCOPE_OPTION_KEY_PREFIX },
|
|
34
|
+
},
|
|
25
35
|
{
|
|
26
36
|
field: "status",
|
|
27
37
|
label: i18nKey("pat.list.col.status"),
|
|
38
|
+
sortable: false,
|
|
28
39
|
renderer: { format: "enumOption", keyPrefix: PAT_STATUS_OPTION_KEY_PREFIX },
|
|
29
40
|
},
|
|
30
41
|
{
|
package/dist/secrets/table.d.ts
CHANGED
|
@@ -8,6 +8,7 @@ export declare const tenantSecretEntity: import("@cosmicdrift/kumiko-types/field
|
|
|
8
8
|
readonly type: "text";
|
|
9
9
|
readonly description?: string;
|
|
10
10
|
readonly maxLength?: number;
|
|
11
|
+
readonly minLength?: number;
|
|
11
12
|
readonly required?: boolean;
|
|
12
13
|
readonly searchable?: boolean;
|
|
13
14
|
readonly sortable?: boolean;
|
package/dist/seo/feature.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
// kumiko-lint-ignore app-feature-structure Phase-3 conversion tracked in #2312 (feature.ts > 300 lines)
|
|
2
2
|
import { computeRevisionEtag } from "@cosmicdrift/kumiko-framework/api";
|
|
3
3
|
import { defineFeature, SYSTEM_TENANT_ID, } from "@cosmicdrift/kumiko-framework/engine";
|
|
4
|
+
import { RateLimitError } from "@cosmicdrift/kumiko-framework/errors";
|
|
4
5
|
import { LEGAL_ROUTES } from "../legal-pages/index.js";
|
|
5
6
|
import { cachedSecurePageResponse } from "../page-render/index.js";
|
|
7
|
+
import { rateLimitedTextResponse } from "../shared/index.js";
|
|
6
8
|
import { SEO_CONFIG_KEYS, SEO_DEFAULT_PATHS } from "./constants.js";
|
|
7
9
|
import { seoConfigQuery } from "./handlers/seo-config.query.js";
|
|
8
10
|
import { buildLlmsTxt } from "./llms-txt.js";
|
|
@@ -62,7 +64,10 @@ async function gatherEntries(opts, systemQuery, origin, host) {
|
|
|
62
64
|
});
|
|
63
65
|
}
|
|
64
66
|
}
|
|
65
|
-
catch {
|
|
67
|
+
catch (error) {
|
|
68
|
+
// A rate limit must not look like "pages removed" to a crawler.
|
|
69
|
+
if (error instanceof RateLimitError)
|
|
70
|
+
throw error;
|
|
66
71
|
// managed-pages unreachable/not mounted — degrade to callback-only entries.
|
|
67
72
|
}
|
|
68
73
|
}
|
|
@@ -113,7 +118,15 @@ export function createSeoFeature(opts) {
|
|
|
113
118
|
anonymous: true,
|
|
114
119
|
handler: async (c, { systemQuery }) => {
|
|
115
120
|
const { origin, host } = requestHost(c);
|
|
116
|
-
|
|
121
|
+
let entries;
|
|
122
|
+
try {
|
|
123
|
+
entries = await gatherEntries(opts, systemQuery, origin, host);
|
|
124
|
+
}
|
|
125
|
+
catch (error) {
|
|
126
|
+
if (error instanceof RateLimitError)
|
|
127
|
+
return rateLimitedTextResponse(error);
|
|
128
|
+
throw error;
|
|
129
|
+
}
|
|
117
130
|
const xml = buildSitemapXml(entries);
|
|
118
131
|
const etag = computeRevisionEtag([host, xml]);
|
|
119
132
|
return cachedSecurePageResponse(c.req.raw, {
|
|
@@ -133,10 +146,19 @@ export function createSeoFeature(opts) {
|
|
|
133
146
|
const tenantId = opts.managedPages
|
|
134
147
|
? ((await opts.managedPages.resolveApexTenant(host)) ?? SYSTEM_TENANT_ID)
|
|
135
148
|
: SYSTEM_TENANT_ID;
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
149
|
+
let entries;
|
|
150
|
+
let seoConfig;
|
|
151
|
+
try {
|
|
152
|
+
[entries, seoConfig] = await Promise.all([
|
|
153
|
+
gatherEntries(opts, systemQuery, origin, host),
|
|
154
|
+
readSeoConfig(systemQuery, tenantId),
|
|
155
|
+
]);
|
|
156
|
+
}
|
|
157
|
+
catch (error) {
|
|
158
|
+
if (error instanceof RateLimitError)
|
|
159
|
+
return rateLimitedTextResponse(error);
|
|
160
|
+
throw error;
|
|
161
|
+
}
|
|
140
162
|
const sections = entries.length > 0
|
|
141
163
|
? [
|
|
142
164
|
{
|
|
@@ -3,6 +3,7 @@ import { type SessionAllOthersRevoker, type SessionMassRevoker } from "./session
|
|
|
3
3
|
export type SessionsFeatureOptions = {
|
|
4
4
|
readonly autoRevokeOnPasswordChange?: SessionMassRevoker;
|
|
5
5
|
readonly expiresInMs?: number;
|
|
6
|
+
readonly adminAccess?: "admin" | "systemAdmin";
|
|
6
7
|
};
|
|
7
8
|
export type BindAutoRevokeOnPasswordChange = (revoker: SessionMassRevoker) => void;
|
|
8
9
|
export type BindRevokeAllOtherSessions = (revoker: SessionAllOthersRevoker) => void;
|
package/dist/sessions/feature.js
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
import { EXT_SESSION_STORE, } from "@cosmicdrift/kumiko-bundled-features/auth-foundation";
|
|
2
2
|
import { deriveEntityTableMeta } from "@cosmicdrift/kumiko-framework/db";
|
|
3
|
-
import { defineFeature } from "@cosmicdrift/kumiko-framework/engine";
|
|
3
|
+
import { access, defineFeature, } from "@cosmicdrift/kumiko-framework/engine";
|
|
4
4
|
import { USER_SESSION_ENTITY_NAME } from "./constants.js";
|
|
5
5
|
import { cleanupJob } from "./handlers/cleanup.job.js";
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
6
|
+
import { createDetailQuery } from "./handlers/detail.query.js";
|
|
7
|
+
import { createListQuery } from "./handlers/list.query.js";
|
|
8
8
|
import { mineQuery } from "./handlers/mine.query.js";
|
|
9
9
|
import { revokeWrite } from "./handlers/revoke.write.js";
|
|
10
10
|
import { revokeAllForUserWrite } from "./handlers/revoke-all-for-user.write.js";
|
|
11
11
|
import { revokeAllOthersWrite } from "./handlers/revoke-all-others.write.js";
|
|
12
12
|
import { SESSIONS_I18N } from "./i18n.js";
|
|
13
13
|
import { userSessionEntity } from "./schema/user-session.js";
|
|
14
|
-
import {
|
|
14
|
+
import { createSessionDetailScreen, createSessionListScreen, sessionMineScreen, } from "./screens.js";
|
|
15
15
|
import { createSessionCallbacks, } from "./session-callbacks.js";
|
|
16
16
|
import { SESSION_REVOKED_EVENT_SHORT, sessionRevokedSchema } from "./session-revoked-event.js";
|
|
17
17
|
const CLEANUP_ESCAPE_HATCH_REASON = "deletes expired session rows of every tenant";
|
|
@@ -47,6 +47,7 @@ export function bindAutoRevokeFromFeature(feature) {
|
|
|
47
47
|
// see rows in the caller's active tenant.
|
|
48
48
|
export function createSessionsFeature(options) {
|
|
49
49
|
return defineFeature("sessions", (r) => {
|
|
50
|
+
const adminRoles = access[options?.adminAccess ?? "admin"];
|
|
50
51
|
r.describe("Tracks signed-in clients in the `store_user_sessions` table (one row per JWT, keyed by the `sid`/`jti` claim) and exposes handlers for `mine` (list your sessions), `revoke`, and `revokeAllOthers`. Session creation and revocation on the hot auth path are handled by `createSessionCallbacks()`, wired into `buildServer({ auth: { ... } })` outside the dispatcher; the same callbacks are also registered as an auth-foundation sessionStore provider, resolvable generically via `resolveSessionStore()`. The feature also ships a manual-trigger cleanup job for pruning expired rows and an optional `autoRevokeOnPasswordChange` hook that mass-revokes all sessions for a user whenever their `passwordHash` changes.");
|
|
51
52
|
r.uiHints({
|
|
52
53
|
displayLabel: "Sessions · Server-side Logout",
|
|
@@ -111,8 +112,8 @@ export function createSessionsFeature(options) {
|
|
|
111
112
|
r.exposesApi("sessions.revokeAllForUser");
|
|
112
113
|
const queries = {
|
|
113
114
|
mine: r.queryHandler(mineQuery),
|
|
114
|
-
list: r.queryHandler(
|
|
115
|
-
detail: r.queryHandler(
|
|
115
|
+
list: r.queryHandler(createListQuery(adminRoles)),
|
|
116
|
+
detail: r.queryHandler(createDetailQuery(adminRoles)),
|
|
116
117
|
};
|
|
117
118
|
// Retention: chunked DELETE of expired/revoked rows. Manual trigger
|
|
118
119
|
// only so dev environments don't churn. Ops wires a cron in the app's
|
|
@@ -164,8 +165,8 @@ export function createSessionsFeature(options) {
|
|
|
164
165
|
autoRevoke ??= revoker;
|
|
165
166
|
};
|
|
166
167
|
r.translations({ keys: SESSIONS_I18N });
|
|
167
|
-
r.screen(
|
|
168
|
-
r.screen(
|
|
168
|
+
r.screen(createSessionListScreen(adminRoles));
|
|
169
|
+
r.screen(createSessionDetailScreen(adminRoles));
|
|
169
170
|
r.screen(sessionMineScreen);
|
|
170
171
|
r.nav({
|
|
171
172
|
id: "session-list",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as z from "zod";
|
|
2
|
-
export declare const
|
|
2
|
+
export declare const createDetailQuery: (adminRoles?: readonly string[]) => import("@cosmicdrift/kumiko-types/define-handler").QueryHandlerDefinition<"user-session:detail", z.ZodObject<{
|
|
3
3
|
id: z.ZodUUID;
|
|
4
4
|
}, z.core.$strip>, {
|
|
5
5
|
id: string;
|
|
@@ -14,10 +14,10 @@ const sessionRowSchema = z.object({
|
|
|
14
14
|
ip: z.string().nullable(),
|
|
15
15
|
userAgent: z.string().nullable(),
|
|
16
16
|
});
|
|
17
|
-
export const
|
|
17
|
+
export const createDetailQuery = (adminRoles = access.admin) => defineQueryHandler({
|
|
18
18
|
name: "user-session:detail",
|
|
19
19
|
schema: z.object({ id: z.uuid() }),
|
|
20
|
-
access: { roles:
|
|
20
|
+
access: { roles: adminRoles },
|
|
21
21
|
description: "Loads one session of the active tenant by its id with owner, timestamps and decrypted IP/user-agent; use it when an admin investigates a specific session found through the session list.",
|
|
22
22
|
// Decrypted IP and user-agent.
|
|
23
23
|
agent: { risk: "high" },
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as z from "zod";
|
|
2
|
-
export declare const
|
|
2
|
+
export declare const createListQuery: (adminRoles?: readonly string[]) => import("@cosmicdrift/kumiko-framework/engine").PagedQueryHandlerDefinition<"user-session:list", z.ZodObject<{
|
|
3
3
|
limit: z.ZodOptional<z.ZodNumber>;
|
|
4
4
|
sort: z.ZodOptional<z.ZodString>;
|
|
5
5
|
sortDirection: z.ZodOptional<z.ZodEnum<{
|
|
@@ -17,14 +17,14 @@ function isSortableColumn(value) {
|
|
|
17
17
|
// tenantId column. Includes revoked rows; UI shows revokedAt distinct.
|
|
18
18
|
// No cursor — nextCursor is always null; `limit` only caps the page size,
|
|
19
19
|
// it doesn't offset into a further one.
|
|
20
|
-
export const
|
|
20
|
+
export const createListQuery = (adminRoles = access.admin) => definePagedQueryHandler({
|
|
21
21
|
name: "user-session:list",
|
|
22
22
|
schema: z.object({
|
|
23
23
|
limit: z.number().int().nonnegative().max(MAX_LIST_LIMIT).optional(),
|
|
24
24
|
sort: z.string().optional(),
|
|
25
25
|
sortDirection: z.enum(["asc", "desc"]).optional(),
|
|
26
26
|
}),
|
|
27
|
-
access: { roles:
|
|
27
|
+
access: { roles: adminRoles },
|
|
28
28
|
description: "Lists every session in the active tenant, live and revoked, sortable by id, user, creation, expiry or revocation, for an admin auditing who is signed in.",
|
|
29
29
|
outputSchema: z.object({
|
|
30
30
|
rows: z.array(z.object({
|
|
@@ -40,7 +40,9 @@ export const listQuery = definePagedQueryHandler({
|
|
|
40
40
|
}),
|
|
41
41
|
handler: async (query, ctx) => {
|
|
42
42
|
const requestedSort = query.payload.sort;
|
|
43
|
-
const sortColumn = requestedSort !== undefined && isSortableColumn(requestedSort)
|
|
43
|
+
const sortColumn = requestedSort !== undefined && isSortableColumn(requestedSort)
|
|
44
|
+
? requestedSort
|
|
45
|
+
: "createdAt";
|
|
44
46
|
const rows = await selectMany(ctx.db, userSessionTable, undefined, {
|
|
45
47
|
// `id` as a tie-breaker keeps row order (and, with `limit` set, row
|
|
46
48
|
// selection) deterministic across identical requests — sortColumn
|
|
@@ -3,6 +3,7 @@ export declare const userSessionEntity: import("@cosmicdrift/kumiko-types/fields
|
|
|
3
3
|
readonly type: "text";
|
|
4
4
|
readonly description?: string;
|
|
5
5
|
readonly maxLength?: number;
|
|
6
|
+
readonly minLength?: number;
|
|
6
7
|
readonly required?: boolean;
|
|
7
8
|
readonly searchable?: boolean;
|
|
8
9
|
readonly sortable?: boolean;
|
|
@@ -24,6 +25,7 @@ export declare const userSessionEntity: import("@cosmicdrift/kumiko-types/fields
|
|
|
24
25
|
readonly type: "text";
|
|
25
26
|
readonly description?: string;
|
|
26
27
|
readonly maxLength?: number;
|
|
28
|
+
readonly minLength?: number;
|
|
27
29
|
readonly required?: boolean;
|
|
28
30
|
readonly searchable?: boolean;
|
|
29
31
|
readonly sortable?: boolean;
|
|
@@ -105,6 +107,7 @@ export declare const userSessionEntity: import("@cosmicdrift/kumiko-types/fields
|
|
|
105
107
|
readonly type: "text";
|
|
106
108
|
readonly description?: string;
|
|
107
109
|
readonly maxLength?: number;
|
|
110
|
+
readonly minLength?: number;
|
|
108
111
|
readonly required?: boolean;
|
|
109
112
|
readonly searchable?: boolean;
|
|
110
113
|
readonly sortable?: boolean;
|
|
@@ -126,6 +129,7 @@ export declare const userSessionEntity: import("@cosmicdrift/kumiko-types/fields
|
|
|
126
129
|
readonly type: "text";
|
|
127
130
|
readonly description?: string;
|
|
128
131
|
readonly maxLength?: number;
|
|
132
|
+
readonly minLength?: number;
|
|
129
133
|
readonly required?: boolean;
|
|
130
134
|
readonly searchable?: boolean;
|
|
131
135
|
readonly sortable?: boolean;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import { type ProjectionDetailScreenDefinition, type ProjectionListScreenDefinition } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
|
-
export declare const
|
|
3
|
-
export declare const
|
|
2
|
+
export declare const createSessionListScreen: (adminRoles?: readonly string[]) => ProjectionListScreenDefinition;
|
|
3
|
+
export declare const createSessionDetailScreen: (adminRoles?: readonly string[]) => ProjectionDetailScreenDefinition;
|
|
4
4
|
export declare const sessionMineScreen: ProjectionListScreenDefinition;
|
package/dist/sessions/screens.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { access, i18nKey, MAX_LIST_LIMIT, } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
2
|
import { SESSION_DETAIL_SCREEN_ID, SESSION_LIST_SCREEN_ID, SESSION_MINE_SCREEN_ID, SessionHandlers, SessionQueries, } from "./constants.js";
|
|
3
|
-
const
|
|
4
|
-
export const sessionListScreen = {
|
|
3
|
+
export const createSessionListScreen = (adminRoles = access.admin) => ({
|
|
5
4
|
id: SESSION_LIST_SCREEN_ID,
|
|
6
5
|
type: "projectionList",
|
|
7
6
|
query: SessionQueries.list,
|
|
@@ -47,9 +46,9 @@ export const sessionListScreen = {
|
|
|
47
46
|
rowClick: true,
|
|
48
47
|
},
|
|
49
48
|
],
|
|
50
|
-
access:
|
|
51
|
-
};
|
|
52
|
-
export const
|
|
49
|
+
access: { roles: adminRoles },
|
|
50
|
+
});
|
|
51
|
+
export const createSessionDetailScreen = (adminRoles = access.admin) => ({
|
|
53
52
|
id: SESSION_DETAIL_SCREEN_ID,
|
|
54
53
|
type: "projectionDetail",
|
|
55
54
|
query: SessionQueries.detail,
|
|
@@ -82,8 +81,8 @@ export const sessionDetailScreen = {
|
|
|
82
81
|
ip: i18nKey("sessions.detail.field.ip"),
|
|
83
82
|
userAgent: i18nKey("sessions.detail.field.userAgent"),
|
|
84
83
|
},
|
|
85
|
-
access:
|
|
86
|
-
};
|
|
84
|
+
access: { roles: adminRoles },
|
|
85
|
+
});
|
|
87
86
|
// Revoke is hidden on the current session — it would sign the user out mid-click; logout covers it.
|
|
88
87
|
export const sessionMineScreen = {
|
|
89
88
|
id: SESSION_MINE_SCREEN_ID,
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// Boot validation rejects `{ kind: "where" }` in access.write too (fw#2626);
|
|
2
|
+
// failing at factory time names the option instead of an entity scope.
|
|
3
|
+
export function assertNoWhereRuleInOwnershipWrite(factoryName, writeOwnership) {
|
|
4
|
+
// skip: no write ownership declared, nothing to validate
|
|
5
|
+
if (!writeOwnership)
|
|
6
|
+
return;
|
|
7
|
+
const hasWhereRule = Object.values(writeOwnership).some((rule) => rule !== "all" && rule.kind === "where");
|
|
8
|
+
// skip: no where-rule present, the option is valid
|
|
9
|
+
if (!hasWhereRule)
|
|
10
|
+
return;
|
|
11
|
+
throw new Error(`${factoryName}({ ownership }): ownership.write must not contain a ` +
|
|
12
|
+
'`{ kind: "where" }` rule — where-rules are evaluated only at the SQL ' +
|
|
13
|
+
"layer (the read path, via buildOwnershipClause). Write paths that " +
|
|
14
|
+
"consult access.write (userCanCreateFieldRow/userCanWriteFieldRow) can't " +
|
|
15
|
+
"evaluate them, so such a rule can only ever deny — boot validation " +
|
|
16
|
+
"rejects it too (fw#2626). Use a `from()` rule for ownership.write, or " +
|
|
17
|
+
"leave it unset.");
|
|
18
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { requestContext } from "@cosmicdrift/kumiko-framework/api";
|
|
2
|
-
import { collectPiiSubjectFields, configuredPiiSubjectKms, encryptPiiFieldValues, } from "@cosmicdrift/kumiko-framework/crypto";
|
|
2
|
+
import { collectPiiSubjectFields, configuredPiiSubjectKms, encryptPiiFieldValues, KeyErasedError, } from "@cosmicdrift/kumiko-framework/crypto";
|
|
3
|
+
import { subjectErasedConflict } from "@cosmicdrift/kumiko-framework/errors";
|
|
3
4
|
// Unmanaged direct-write stores (r.unmanagedTable) skip the executor, so its
|
|
4
5
|
// PII encryption never runs — every insert of subject-annotated fields must
|
|
5
6
|
// go through this instead, and the feature declares { piiEncryptedOnWrite:
|
|
@@ -8,5 +9,12 @@ export async function encryptForDirectWrite(entity, entityName, row, fallbackReq
|
|
|
8
9
|
const kms = configuredPiiSubjectKms();
|
|
9
10
|
if (!kms)
|
|
10
11
|
return row;
|
|
11
|
-
|
|
12
|
+
try {
|
|
13
|
+
return await encryptPiiFieldValues(row, entity, collectPiiSubjectFields(entity), kms, { requestId: requestContext.get()?.requestId ?? fallbackRequestId }, { entityName });
|
|
14
|
+
}
|
|
15
|
+
catch (e) {
|
|
16
|
+
if (e instanceof KeyErasedError)
|
|
17
|
+
throw subjectErasedConflict(e);
|
|
18
|
+
throw e;
|
|
19
|
+
}
|
|
12
20
|
}
|
package/dist/shared/index.d.ts
CHANGED
|
@@ -1,21 +1,22 @@
|
|
|
1
1
|
export { collectErasureFailure, throwIfErasureFailed } from "./assert-erased.js";
|
|
2
|
+
export { assertNoWhereRuleInOwnershipWrite } from "./assert-no-where-rule-in-ownership-write.js";
|
|
2
3
|
export { type ChunkedMigrationOptions, type ChunkedMigrationResult, type ChunkedMigrationStopReason, type MigrationRowOutcome, runChunkedMigration, } from "./chunked-entity-migration.js";
|
|
3
4
|
export { classifyStoredEnvelope, type StoredEnvelopeClassification, } from "./classify-stored-envelope.js";
|
|
4
5
|
export { decryptStoredPii } from "./decrypt-stored-pii.js";
|
|
5
6
|
export { encryptForDirectWrite } from "./encrypt-for-direct-write.js";
|
|
6
7
|
export { entitiesOf } from "./entities-of.js";
|
|
7
8
|
export { isWithinGracePeriod } from "./grace-period.js";
|
|
8
|
-
export { hasWhereRule } from "./has-where-rule.js";
|
|
9
9
|
export { isIdentityV3Hash, verifyIdentityV3Hash } from "./identity-v3-hash.js";
|
|
10
10
|
export { isTenantDb } from "./is-tenant-db.js";
|
|
11
11
|
export { createLockoutCounter, type LockoutCounterState } from "./lockout-counter.js";
|
|
12
12
|
export { mapWithConcurrency } from "./map-with-concurrency.js";
|
|
13
|
-
export { denyUnlessJoinRowParentVisible, joinRowParentIsVisible, parentRowIsVisible, } from "./parent-visibility.js";
|
|
13
|
+
export { denyUnlessJoinRowParentVisible, hostRowIsGone, joinRowParentIsVisible, parentRowIsVisible, } from "./parent-visibility.js";
|
|
14
14
|
export { hashPassword, verifyDummyPassword, verifyPassword } from "./password-hashing.js";
|
|
15
|
+
export { rateLimitedTextResponse } from "./rate-limited-text-response.js";
|
|
15
16
|
export { redactBotTokens, redactEmailAddresses, redactErrorText, redactUrls, } from "./redact.js";
|
|
16
17
|
export { type RowBoundGrantResult, redeemRowBoundGrant, signRowBoundGrant, } from "./row-bound-grant.js";
|
|
17
18
|
export { runInSubTransaction } from "./run-in-sub-transaction.js";
|
|
18
|
-
export { sessionLocaleField } from "./session-locale-field.js";
|
|
19
|
+
export { registrationLocaleFromRequest, sessionLocaleField } from "./session-locale-field.js";
|
|
19
20
|
export { sessionTimezoneField } from "./session-timezone-field.js";
|
|
20
21
|
export { peekTokenSubject, signToken, TokenPurpose, type VerifyResult, verifyToken, } from "./signed-token.js";
|
|
21
22
|
export { EXT_SIGNUP_HANDOVER, findSignupHandoverProvider, isSignupHandoverProvider, SIGNUP_HANDOVER_BENIGN_CLAIM_REJECTION_CODE, type SignupHandoverBinding, type SignupHandoverProvider, } from "./signup-handover.js";
|
package/dist/shared/index.js
CHANGED
|
@@ -1,21 +1,22 @@
|
|
|
1
1
|
export { collectErasureFailure, throwIfErasureFailed } from "./assert-erased.js";
|
|
2
|
+
export { assertNoWhereRuleInOwnershipWrite } from "./assert-no-where-rule-in-ownership-write.js";
|
|
2
3
|
export { runChunkedMigration, } from "./chunked-entity-migration.js";
|
|
3
4
|
export { classifyStoredEnvelope, } from "./classify-stored-envelope.js";
|
|
4
5
|
export { decryptStoredPii } from "./decrypt-stored-pii.js";
|
|
5
6
|
export { encryptForDirectWrite } from "./encrypt-for-direct-write.js";
|
|
6
7
|
export { entitiesOf } from "./entities-of.js";
|
|
7
8
|
export { isWithinGracePeriod } from "./grace-period.js";
|
|
8
|
-
export { hasWhereRule } from "./has-where-rule.js";
|
|
9
9
|
export { isIdentityV3Hash, verifyIdentityV3Hash } from "./identity-v3-hash.js";
|
|
10
10
|
export { isTenantDb } from "./is-tenant-db.js";
|
|
11
11
|
export { createLockoutCounter } from "./lockout-counter.js";
|
|
12
12
|
export { mapWithConcurrency } from "./map-with-concurrency.js";
|
|
13
|
-
export { denyUnlessJoinRowParentVisible, joinRowParentIsVisible, parentRowIsVisible, } from "./parent-visibility.js";
|
|
13
|
+
export { denyUnlessJoinRowParentVisible, hostRowIsGone, joinRowParentIsVisible, parentRowIsVisible, } from "./parent-visibility.js";
|
|
14
14
|
export { hashPassword, verifyDummyPassword, verifyPassword } from "./password-hashing.js";
|
|
15
|
+
export { rateLimitedTextResponse } from "./rate-limited-text-response.js";
|
|
15
16
|
export { redactBotTokens, redactEmailAddresses, redactErrorText, redactUrls, } from "./redact.js";
|
|
16
17
|
export { redeemRowBoundGrant, signRowBoundGrant, } from "./row-bound-grant.js";
|
|
17
18
|
export { runInSubTransaction } from "./run-in-sub-transaction.js";
|
|
18
|
-
export { sessionLocaleField } from "./session-locale-field.js";
|
|
19
|
+
export { registrationLocaleFromRequest, sessionLocaleField } from "./session-locale-field.js";
|
|
19
20
|
export { sessionTimezoneField } from "./session-timezone-field.js";
|
|
20
21
|
export { peekTokenSubject, signToken, TokenPurpose, verifyToken, } from "./signed-token.js";
|
|
21
22
|
export { EXT_SIGNUP_HANDOVER, findSignupHandoverProvider, isSignupHandoverProvider, SIGNUP_HANDOVER_BENIGN_CLAIM_REJECTION_CODE, } from "./signup-handover.js";
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type TenantDb } from "@cosmicdrift/kumiko-framework/db";
|
|
2
2
|
import { type Registry, type SessionUser } from "@cosmicdrift/kumiko-framework/engine";
|
|
3
3
|
import { type WriteFailure } from "@cosmicdrift/kumiko-framework/errors";
|
|
4
4
|
export declare function parentRowIsVisible(registry: Registry, entityType: string, entityId: string, user: SessionUser, db: TenantDb): Promise<boolean>;
|
|
5
|
+
export declare function hostRowIsGone(registry: Registry, entityType: string, entityId: string, db: TenantDb): Promise<boolean>;
|
|
5
6
|
export declare function joinRowParentIsVisible(registry: Registry, joinEntityName: string, payload: Readonly<Record<string, unknown>>, user: SessionUser, db: TenantDb): Promise<boolean>;
|
|
6
7
|
export declare function denyUnlessJoinRowParentVisible(registry: Registry, joinEntityName: string, payload: {
|
|
7
8
|
readonly entityType: string;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { buildEntityTable, } from "@cosmicdrift/kumiko-framework/db";
|
|
1
2
|
import { createEntityExecutor, isUuid, } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
3
|
import { NotFoundError, writeFailure, } from "@cosmicdrift/kumiko-framework/errors";
|
|
3
4
|
// Keyed by definition identity, not by entity name — two test stacks can
|
|
@@ -39,6 +40,26 @@ export async function parentRowIsVisible(registry, entityType, entityId, user, d
|
|
|
39
40
|
}
|
|
40
41
|
return (await executor.detail({ id: entityId }, user, db)) !== null;
|
|
41
42
|
}
|
|
43
|
+
const hostTablesByEntity = new WeakMap();
|
|
44
|
+
// True when the host row can never be read again by anyone: its entityType no
|
|
45
|
+
// longer names a registered entity, or the row is hard-deleted / soft-deleted.
|
|
46
|
+
// Deliberately skips the caller's `access.read` ownership (unlike
|
|
47
|
+
// parentRowIsVisible): a row hidden from this caller but alive for another team
|
|
48
|
+
// is NOT gone, and its join row must stay.
|
|
49
|
+
export async function hostRowIsGone(registry, entityType, entityId, db) {
|
|
50
|
+
const entity = registry.getEntity(entityType);
|
|
51
|
+
if (!entity)
|
|
52
|
+
return true;
|
|
53
|
+
if (!idShapeMatchesEntity(entity, entityId))
|
|
54
|
+
return true;
|
|
55
|
+
let table = hostTablesByEntity.get(entity);
|
|
56
|
+
if (!table) {
|
|
57
|
+
table = buildEntityTable(entityType, entity, { relations: registry.getRelations(entityType) });
|
|
58
|
+
hostTablesByEntity.set(entity, table);
|
|
59
|
+
}
|
|
60
|
+
const row = await db.fetchOne(table, { id: entityId });
|
|
61
|
+
return row === undefined || row["isDeleted"] === true;
|
|
62
|
+
}
|
|
42
63
|
// Write-path half of the parentRef gate (fw#2766). Resolves the join entity's
|
|
43
64
|
// own `parentRef` declaration from the registry — the same object the read
|
|
44
65
|
// path's SQL gate reads — so the field names and the allowedTypes allowlist
|
|
@@ -36,7 +36,21 @@ const FAILED = { ok: false };
|
|
|
36
36
|
function anchoredPurpose(purpose, anchor) {
|
|
37
37
|
return `${purpose}:${anchor}`;
|
|
38
38
|
}
|
|
39
|
+
// The HMAC input is `${purpose}:${anchor}:${subject}.${expiresAtMs}` with no
|
|
40
|
+
// length prefixes. Colon-free anchor and subject make the split unambiguous
|
|
41
|
+
// from the right, so two different (purpose, anchor, subject) triples can
|
|
42
|
+
// never share a signature. The purpose may keep colons (tenant-handover
|
|
43
|
+
// namespaces its purpose by entity type).
|
|
44
|
+
function hasDelimiter(value) {
|
|
45
|
+
return value.includes(":");
|
|
46
|
+
}
|
|
47
|
+
function isUnambiguousPart(value) {
|
|
48
|
+
return !!value && !hasDelimiter(value);
|
|
49
|
+
}
|
|
39
50
|
export function signRowBoundGrant(args) {
|
|
51
|
+
if (hasDelimiter(args.anchor) || hasDelimiter(args.subject)) {
|
|
52
|
+
throw new Error("row-bound grant: subject and anchor must not contain ':'");
|
|
53
|
+
}
|
|
40
54
|
return signToken(args.subject, anchoredPurpose(args.purpose, args.anchor), args.ttlMinutes, args.secret, args.now);
|
|
41
55
|
}
|
|
42
56
|
// Every rejection returns the same bare `{ ok: false }` — no reason, by
|
|
@@ -51,7 +65,7 @@ export async function redeemRowBoundGrant(args) {
|
|
|
51
65
|
if (!args.secret)
|
|
52
66
|
return FAILED;
|
|
53
67
|
const subject = peekTokenSubject(args.token);
|
|
54
|
-
if (!subject)
|
|
68
|
+
if (!isUnambiguousPart(subject))
|
|
55
69
|
return FAILED;
|
|
56
70
|
// The subject is unverified attacker input at this point, so a lookup that
|
|
57
71
|
// throws on it (e.g. a non-uuid value against a uuid column) must not
|
|
@@ -69,7 +83,7 @@ export async function redeemRowBoundGrant(args) {
|
|
|
69
83
|
});
|
|
70
84
|
return FAILED;
|
|
71
85
|
}
|
|
72
|
-
if (!anchor)
|
|
86
|
+
if (!isUnambiguousPart(anchor))
|
|
73
87
|
return FAILED;
|
|
74
88
|
const verified = verifyToken(args.token, anchoredPurpose(args.purpose, anchor), args.secret, args.now);
|
|
75
89
|
if (!verified.ok)
|
|
@@ -1,2 +1,3 @@
|
|
|
1
1
|
import type { SessionUser } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
2
|
export declare function sessionLocaleField(locale: string | null | undefined): Pick<SessionUser, "locale"> | Record<string, never>;
|
|
3
|
+
export declare function registrationLocaleFromRequest(): ReturnType<typeof sessionLocaleField>;
|
|
@@ -1,6 +1,12 @@
|
|
|
1
|
+
import { requestContext } from "@cosmicdrift/kumiko-framework/api";
|
|
1
2
|
import { canonicalizeLocaleTag, isValidLocaleTag } from "@cosmicdrift/kumiko-framework/i18n";
|
|
2
3
|
export function sessionLocaleField(locale) {
|
|
3
4
|
if (locale === null || locale === undefined || !isValidLocaleTag(locale))
|
|
4
5
|
return {};
|
|
5
6
|
return { locale: canonicalizeLocaleTag(locale) };
|
|
6
7
|
}
|
|
8
|
+
// Only an explicit request signal (X-Locale) counts: the boot default is not a
|
|
9
|
+
// choice the new user made.
|
|
10
|
+
export function registrationLocaleFromRequest() {
|
|
11
|
+
return sessionLocaleField(requestContext.get()?.locale);
|
|
12
|
+
}
|