@cosmicdrift/kumiko-bundled-features 0.355.0 → 0.357.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/auth-mfa/i18n.js +1 -0
- package/dist/auth-mfa/schema/user-mfa.d.ts +6 -0
- package/dist/auth-mfa/screens.js +8 -1
- package/dist/billing-foundation/entities.d.ts +8 -0
- package/dist/billing-foundation/projection.d.ts +8 -0
- package/dist/cap-counter/entity.d.ts +1 -0
- package/dist/cap-counter/feature.js +7 -1
- package/dist/cap-overview/screens.js +3 -1
- package/dist/compliance-profiles/handlers/set-profile.write.js +14 -22
- package/dist/config/table.d.ts +6 -0
- package/dist/crypto-shredding/handlers/forget-subject.write.js +15 -3
- package/dist/custom-fields/entity.d.ts +4 -0
- package/dist/custom-fields/handlers/define-system-field.write.js +8 -16
- package/dist/custom-fields/handlers/delete-system-field.write.js +5 -2
- package/dist/custom-fields/lib/define-or-resurrect.d.ts +2 -1
- package/dist/custom-fields/lib/define-or-resurrect.js +11 -4
- package/dist/data-retention/schema/tenant-retention-override.d.ts +4 -0
- package/dist/delivery/chat-webhook-sender.js +1 -1
- package/dist/delivery/constants.d.ts +1 -0
- package/dist/delivery/constants.js +4 -0
- package/dist/delivery/delivery-service.js +62 -25
- package/dist/delivery/feature.js +7 -1
- package/dist/delivery/jobs.js +25 -1
- package/dist/delivery/tables.d.ts +6 -0
- package/dist/delivery/testing.d.ts +1 -0
- package/dist/document-ingest-foundation/entity.d.ts +4 -0
- package/dist/folders/entity.d.ts +8 -0
- package/dist/form-draft/entity.d.ts +2 -0
- package/dist/inbound-mail-foundation/entities.d.ts +25 -0
- package/dist/inbound-mail-foundation/projection.d.ts +20 -0
- package/dist/jobs/job-run-table.d.ts +6 -0
- package/dist/ledger/entity.d.ts +13 -0
- package/dist/managed-pages/handlers/set.write.js +6 -17
- package/dist/managed-pages/table.d.ts +12 -0
- package/dist/notes-history/entity.d.ts +12 -0
- package/dist/notes-history/entity.js +7 -6
- package/dist/notes-history-user-data/hooks.d.ts +10 -1
- package/dist/notes-history-user-data/hooks.js +78 -61
- package/dist/notes-history-user-data/index.d.ts +1 -0
- package/dist/notes-history-user-data/index.js +2 -1
- package/dist/personal-access-tokens/schema/api-token.d.ts +6 -0
- package/dist/personal-access-tokens/screens.js +2 -0
- package/dist/secrets/table.d.ts +1 -0
- package/dist/sessions/schema/user-session.d.ts +4 -0
- package/dist/sessions/screens.js +2 -0
- package/dist/shared/index.d.ts +1 -0
- package/dist/shared/index.js +1 -0
- package/dist/shared/require-for-tenant.d.ts +2 -0
- package/dist/shared/require-for-tenant.js +9 -0
- package/dist/tags/entity.d.ts +9 -0
- package/dist/template-resolver/handlers/collection-set.write.js +4 -4
- package/dist/template-resolver/handlers/set.write.js +6 -7
- package/dist/template-resolver/handlers/upsert-system.write.js +6 -6
- package/dist/template-resolver/handlers/upsert-tenant.write.js +6 -4
- package/dist/template-resolver/table.d.ts +10 -0
- package/dist/template-resolver/user-content-table.d.ts +10 -0
- package/dist/tenant/cancel-pending-invitation.d.ts +2 -2
- package/dist/tenant/cancel-pending-invitation.js +2 -2
- package/dist/tenant/constants.d.ts +1 -0
- package/dist/tenant/constants.js +6 -3
- package/dist/tenant/feature.js +2 -0
- package/dist/tenant/handlers/member-role-availability.query.d.ts +7 -0
- package/dist/tenant/handlers/member-role-availability.query.js +18 -0
- package/dist/tenant/handlers/remove-member.write.js +13 -10
- package/dist/tenant/handlers/update-member-roles.write.js +12 -9
- package/dist/tenant/invitation-table.d.ts +6 -0
- package/dist/tenant/member-role-options.d.ts +2 -0
- package/dist/tenant/member-role-options.js +4 -0
- package/dist/tenant/membership-table.d.ts +4 -0
- package/dist/tenant/schema/tenant.d.ts +6 -0
- package/dist/tenant/screens.d.ts +4 -2
- package/dist/tenant/screens.js +10 -6
- package/dist/tier-engine/entity.d.ts +2 -0
- package/dist/tier-engine/handlers/set-tenant-tier.write.js +7 -18
- package/dist/tier-engine/tier-resolver.d.ts +2 -0
- package/dist/user/schema/user.d.ts +12 -0
- package/dist/user/screens.js +8 -1
- package/dist/user-data-rights/schema/download-attempt.d.ts +14 -0
- package/dist/user-data-rights/schema/download-token.d.ts +8 -0
- package/dist/user-data-rights/schema/export-job.d.ts +6 -0
- package/dist/user-data-rights/screens.js +8 -1
- package/dist/waitlist/entity.d.ts +14 -0
- package/dist/waitlist/screens.js +8 -1
- package/package.json +9 -9
- package/src/auth-mfa/changes.json +6 -0
- package/src/crypto-shredding/changes.json +6 -0
- package/src/delivery/changes.json +6 -0
- package/src/tenant/changes.json +6 -0
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// notes consumers without the user-data-rights pipeline don't pull a hard
|
|
4
4
|
// dependency. Mirrors job-run/delivery-attempt (user-data-rights-defaults):
|
|
5
5
|
// export-only, erasure via crypto-shredding.
|
|
6
|
+
import { selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
6
7
|
import { configuredPiiSubjectKms } from "@cosmicdrift/kumiko-framework/crypto";
|
|
7
8
|
import { resolveRetentionPolicyForTenant, resolveTenantRetentionPreset, } from "../data-retention/index.js";
|
|
8
9
|
import { noteEntryTable, noteMentionTable } from "../notes-history/index.js";
|
|
@@ -26,93 +27,109 @@ export const noteEntryExportHook = async (ctx) => {
|
|
|
26
27
|
})),
|
|
27
28
|
};
|
|
28
29
|
};
|
|
30
|
+
// What erasing a subject's key reaches: notes that @-mention the subject plus notes
|
|
31
|
+
// attached to it as host. Export and erasure both go through this, so Art. 15
|
|
32
|
+
// lists exactly what Art. 17 shreds.
|
|
33
|
+
async function selectNotesReachingSubject(db, subject) {
|
|
34
|
+
if (subject.kind === "tenant")
|
|
35
|
+
return [];
|
|
36
|
+
const subjectId = subject.kind === "user" ? subject.userId : subject.id;
|
|
37
|
+
const attachedWhere = subject.kind === "record"
|
|
38
|
+
? { entityType: subject.entity, entityId: subjectId }
|
|
39
|
+
: { entityId: subjectId };
|
|
40
|
+
const mentions = await selectMany(db, noteMentionTable, { subjectId });
|
|
41
|
+
const mentioned = mentions.length === 0
|
|
42
|
+
? []
|
|
43
|
+
: await selectMany(db, noteEntryTable, {
|
|
44
|
+
id: { in: [...new Set(mentions.map((m) => m.noteId))] },
|
|
45
|
+
});
|
|
46
|
+
const attached = await selectMany(db, noteEntryTable, attachedWhere);
|
|
47
|
+
const byId = new Map();
|
|
48
|
+
for (const note of [...mentioned, ...attached])
|
|
49
|
+
byId.set(note.id, note);
|
|
50
|
+
return [...byId.values()];
|
|
51
|
+
}
|
|
29
52
|
// `authorName` is separately annotated `personal: { of: "authorId" }`
|
|
30
53
|
// (entity.ts), so forgetting the AUTHOR already crypto-shreds that field on
|
|
31
|
-
// its own
|
|
32
|
-
//
|
|
33
|
-
//
|
|
34
|
-
//
|
|
35
|
-
// that's the documented gap in entity.ts, not a bug here.
|
|
54
|
+
// its own. `body` is Row-Subject (`personal: { of: "id" }`): its key is per
|
|
55
|
+
// note, so forgetting a subject only reaches a body when the note structurally
|
|
56
|
+
// @-mentions the subject (`note-mention`, fw#2787) or hangs on it as host.
|
|
57
|
+
// Free-text mentions are NOT detected — the documented gap in entity.ts.
|
|
36
58
|
//
|
|
37
|
-
// Per reached note
|
|
38
|
-
//
|
|
39
|
-
//
|
|
40
|
-
//
|
|
41
|
-
// for note-entry, not the host, so it is deliberately unused here.
|
|
59
|
+
// Per reached note the HOST entity's retention strategy (entityType, resolved
|
|
60
|
+
// for the note's own tenant — the manual forget-subject path runs cross-tenant)
|
|
61
|
+
// is consulted first: anonymize/blockDelete win over erasure, same rule the
|
|
62
|
+
// automated forget-cleanup applies to every other entity.
|
|
42
63
|
//
|
|
43
|
-
//
|
|
44
|
-
//
|
|
45
|
-
//
|
|
46
|
-
|
|
47
|
-
// specific to this hook; apps that need Art.17 coverage without KMS must
|
|
48
|
-
// mount one.
|
|
49
|
-
export const noteEntryDeleteHook = async (ctx) => {
|
|
64
|
+
// Without a mounted KMS this erases nothing: record-owned fields fall back to
|
|
65
|
+
// plaintext framework-wide (see pii-field-encryption.ts), so apps that need
|
|
66
|
+
// Art. 17 coverage must mount one.
|
|
67
|
+
export async function eraseNoteBodiesReachingSubject(args) {
|
|
50
68
|
const kms = configuredPiiSubjectKms();
|
|
51
|
-
// skip: no KMS adapter mounted —
|
|
69
|
+
// skip: no KMS adapter mounted — forget is a true no-op
|
|
52
70
|
if (!kms)
|
|
53
71
|
return;
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
if (mentions.length === 0)
|
|
59
|
-
return;
|
|
60
|
-
const noteIds = new Set(mentions.map((m) => m.noteId));
|
|
61
|
-
// Once per hook run: the preset depends on the tenant only, not the note.
|
|
62
|
-
const tenantPreset = await resolveTenantRetentionPreset({
|
|
63
|
-
db: ctx.db,
|
|
64
|
-
registry: ctx.registry,
|
|
65
|
-
tenantId: ctx.tenantId,
|
|
66
|
-
});
|
|
67
|
-
// Append-only rows are never hard-deleted, so every mentioned note should
|
|
68
|
-
// resolve; a missing one has no host and thus no retention policy to consult.
|
|
69
|
-
const notes = await ctx.db.selectMany(noteEntryTable, {
|
|
70
|
-
id: { in: [...noteIds] },
|
|
71
|
-
});
|
|
72
|
-
const strategyByEntityType = new Map();
|
|
72
|
+
const notes = await selectNotesReachingSubject(args.db, args.subject);
|
|
73
|
+
const dataRetentionMounted = args.registry.features.has("data-retention");
|
|
74
|
+
const presetByTenant = new Map();
|
|
75
|
+
const strategyByTenantAndEntityType = new Map();
|
|
73
76
|
for (const note of notes) {
|
|
74
|
-
|
|
77
|
+
const strategyKey = `${note.tenantId}:${note.entityType}`;
|
|
78
|
+
let strategy = strategyByTenantAndEntityType.get(strategyKey);
|
|
75
79
|
if (strategy === undefined) {
|
|
80
|
+
let tenantPreset = presetByTenant.get(note.tenantId);
|
|
81
|
+
if (tenantPreset === undefined) {
|
|
82
|
+
tenantPreset = await resolveTenantRetentionPreset({
|
|
83
|
+
db: args.db,
|
|
84
|
+
registry: args.registry,
|
|
85
|
+
tenantId: note.tenantId,
|
|
86
|
+
});
|
|
87
|
+
presetByTenant.set(note.tenantId, tenantPreset);
|
|
88
|
+
}
|
|
76
89
|
const hostPolicy = await resolveRetentionPolicyForTenant({
|
|
77
|
-
db:
|
|
78
|
-
registry:
|
|
79
|
-
tenantId:
|
|
90
|
+
db: args.db,
|
|
91
|
+
registry: args.registry,
|
|
92
|
+
tenantId: note.tenantId,
|
|
80
93
|
entityName: note.entityType,
|
|
81
94
|
preloadedTenantPreset: tenantPreset,
|
|
95
|
+
// Without data-retention there is no override table to read.
|
|
96
|
+
...(!dataRetentionMounted && { preloadedOverride: null }),
|
|
82
97
|
});
|
|
83
98
|
strategy = policyToStrategy(hostPolicy.policy?.strategy ?? null);
|
|
84
|
-
|
|
99
|
+
strategyByTenantAndEntityType.set(strategyKey, strategy);
|
|
85
100
|
}
|
|
86
101
|
if (strategy === "anonymize")
|
|
87
102
|
continue;
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
eraseReason: "notes-history:mention-forget",
|
|
103
|
+
await kms.eraseKey({ kind: "record", entity: "note-entry", id: note.id }, {
|
|
104
|
+
requestId: args.requestId,
|
|
105
|
+
userId: args.erasedBy,
|
|
106
|
+
eraseReason: "notes-history:subject-forget",
|
|
93
107
|
});
|
|
94
108
|
}
|
|
109
|
+
}
|
|
110
|
+
export const noteEntryDeleteHook = async (ctx) => {
|
|
111
|
+
await eraseNoteBodiesReachingSubject({
|
|
112
|
+
db: ctx.db,
|
|
113
|
+
registry: ctx.registry,
|
|
114
|
+
subject: { kind: "user", userId: ctx.userId },
|
|
115
|
+
erasedBy: ctx.userId,
|
|
116
|
+
requestId: "notes-history-user-data:forget-reached-notes",
|
|
117
|
+
});
|
|
95
118
|
};
|
|
96
|
-
// Erasure reaches every note that
|
|
97
|
-
//
|
|
119
|
+
// Erasure reaches every note that mentions the user or hangs on the user as host
|
|
120
|
+
// (see eraseNoteBodiesReachingSubject), so the Art. 15/20 export lists those too.
|
|
98
121
|
export const noteMentionExportHook = async (ctx) => {
|
|
99
|
-
const
|
|
100
|
-
subjectId: ctx.userId,
|
|
101
|
-
});
|
|
102
|
-
if (mentions.length === 0)
|
|
103
|
-
return null;
|
|
104
|
-
const noteIds = [...new Set(mentions.map((m) => m.noteId))];
|
|
105
|
-
const notes = await ctx.db.selectMany(noteEntryTable, { id: { in: noteIds } });
|
|
122
|
+
const notes = await selectNotesReachingSubject(ctx.db, { kind: "user", userId: ctx.userId });
|
|
106
123
|
if (notes.length === 0)
|
|
107
124
|
return null;
|
|
108
125
|
return {
|
|
109
126
|
entity: "note-mention",
|
|
110
127
|
rows: notes.map((note) => ({
|
|
111
|
-
noteId: note
|
|
112
|
-
entityType: note
|
|
113
|
-
entityId: note
|
|
114
|
-
body: note
|
|
115
|
-
insertedAt: note
|
|
128
|
+
noteId: note.id,
|
|
129
|
+
entityType: note.entityType,
|
|
130
|
+
entityId: note.entityId,
|
|
131
|
+
body: note.body,
|
|
132
|
+
insertedAt: note.insertedAt,
|
|
116
133
|
})),
|
|
117
134
|
};
|
|
118
135
|
};
|
|
@@ -6,8 +6,9 @@
|
|
|
6
6
|
// user-data-rights stack. Mirrors folders-user-data.
|
|
7
7
|
import { defineFeature, EXT_USER_DATA } from "@cosmicdrift/kumiko-framework/engine";
|
|
8
8
|
import { noteEntryDeleteHook, noteEntryExportHook, noteMentionDeleteHook, noteMentionExportHook, } from "./hooks.js";
|
|
9
|
+
export { eraseNoteBodiesReachingSubject } from "./hooks.js";
|
|
9
10
|
export const notesHistoryUserDataFeature = defineFeature("notes-history-user-data", (r) => {
|
|
10
|
-
r.describe("GDPR (Art. 20 export / Art. 17 erasure) coverage for the `notes-history` feature's `note-entry` and `note-mention` entities. Mounts the export hook so a user's authored notes are included in the user-data export bundle; the note-entry delete hook
|
|
11
|
+
r.describe("GDPR (Art. 20 export / Art. 17 erasure) coverage for the `notes-history` feature's `note-entry` and `note-mention` entities. Mounts the export hook so a user's authored notes are included in the user-data export bundle; the note-entry delete hook crypto-shreds the row-subject key of each note that structurally @-mentions the forgotten user (`note-mention`) or hangs on the user as host entity (after consulting that note's host entity's retention strategy — blockDelete/anonymize win over erasure), keeping the append-only history intact for all other notes. The manual `crypto-shredding` forget-subject command reaches the same notes for user and record subjects through the exported `eraseNoteBodiesReachingSubject`, and the export lists them under `note-mention`. `note-mention`'s own hooks are a no-op — it is a plain FK pointer, not separately exportable content. Kept separate from `notes-history` so notes consumers without the user-data-rights pipeline don't pull a hard dependency — requires `user-data-rights`, optionalRequires `notes-history`.");
|
|
11
12
|
// user-data-rights is the hard dependency (EXT_USER_DATA host). notes-history
|
|
12
13
|
// is OPTIONAL: if it's mounted toggleable(default=false), a hard r.requires
|
|
13
14
|
// would throw an "effectively disabled" boot warning even though the
|
|
@@ -4,6 +4,7 @@ export declare const apiTokenEntity: import("@cosmicdrift/kumiko-types/fields").
|
|
|
4
4
|
readonly description?: string;
|
|
5
5
|
readonly maxLength?: number;
|
|
6
6
|
readonly minLength?: number;
|
|
7
|
+
readonly autoComplete?: string;
|
|
7
8
|
readonly required?: boolean;
|
|
8
9
|
readonly searchable?: boolean;
|
|
9
10
|
readonly sortable?: boolean;
|
|
@@ -26,6 +27,7 @@ export declare const apiTokenEntity: import("@cosmicdrift/kumiko-types/fields").
|
|
|
26
27
|
readonly description?: string;
|
|
27
28
|
readonly maxLength?: number;
|
|
28
29
|
readonly minLength?: number;
|
|
30
|
+
readonly autoComplete?: string;
|
|
29
31
|
readonly required?: boolean;
|
|
30
32
|
readonly searchable?: boolean;
|
|
31
33
|
readonly sortable?: boolean;
|
|
@@ -48,6 +50,7 @@ export declare const apiTokenEntity: import("@cosmicdrift/kumiko-types/fields").
|
|
|
48
50
|
readonly description?: string;
|
|
49
51
|
readonly maxLength?: number;
|
|
50
52
|
readonly minLength?: number;
|
|
53
|
+
readonly autoComplete?: string;
|
|
51
54
|
readonly required?: boolean;
|
|
52
55
|
readonly searchable?: boolean;
|
|
53
56
|
readonly sortable?: boolean;
|
|
@@ -70,6 +73,7 @@ export declare const apiTokenEntity: import("@cosmicdrift/kumiko-types/fields").
|
|
|
70
73
|
readonly description?: string;
|
|
71
74
|
readonly maxLength?: number;
|
|
72
75
|
readonly minLength?: number;
|
|
76
|
+
readonly autoComplete?: string;
|
|
73
77
|
readonly required?: boolean;
|
|
74
78
|
readonly searchable?: boolean;
|
|
75
79
|
readonly sortable?: boolean;
|
|
@@ -92,6 +96,7 @@ export declare const apiTokenEntity: import("@cosmicdrift/kumiko-types/fields").
|
|
|
92
96
|
readonly description?: string;
|
|
93
97
|
readonly maxLength?: number;
|
|
94
98
|
readonly minLength?: number;
|
|
99
|
+
readonly autoComplete?: string;
|
|
95
100
|
readonly required?: boolean;
|
|
96
101
|
readonly searchable?: boolean;
|
|
97
102
|
readonly sortable?: boolean;
|
|
@@ -114,6 +119,7 @@ export declare const apiTokenEntity: import("@cosmicdrift/kumiko-types/fields").
|
|
|
114
119
|
readonly description?: string;
|
|
115
120
|
readonly maxLength?: number;
|
|
116
121
|
readonly minLength?: number;
|
|
122
|
+
readonly autoComplete?: string;
|
|
117
123
|
readonly required?: boolean;
|
|
118
124
|
readonly searchable?: boolean;
|
|
119
125
|
readonly sortable?: boolean;
|
|
@@ -41,11 +41,13 @@ export const patListScreen = {
|
|
|
41
41
|
{
|
|
42
42
|
field: "createdAt",
|
|
43
43
|
label: i18nKey("pat.list.col.created"),
|
|
44
|
+
hideOnNarrow: true,
|
|
44
45
|
renderer: { format: "timestamp" },
|
|
45
46
|
},
|
|
46
47
|
{
|
|
47
48
|
field: "expiresAt",
|
|
48
49
|
label: i18nKey("pat.list.col.expires"),
|
|
50
|
+
hideOnNarrow: true,
|
|
49
51
|
renderer: { format: "timestamp" },
|
|
50
52
|
},
|
|
51
53
|
],
|
package/dist/secrets/table.d.ts
CHANGED
|
@@ -9,6 +9,7 @@ export declare const tenantSecretEntity: import("@cosmicdrift/kumiko-types/field
|
|
|
9
9
|
readonly description?: string;
|
|
10
10
|
readonly maxLength?: number;
|
|
11
11
|
readonly minLength?: number;
|
|
12
|
+
readonly autoComplete?: string;
|
|
12
13
|
readonly required?: boolean;
|
|
13
14
|
readonly searchable?: boolean;
|
|
14
15
|
readonly sortable?: boolean;
|
|
@@ -4,6 +4,7 @@ export declare const userSessionEntity: import("@cosmicdrift/kumiko-types/fields
|
|
|
4
4
|
readonly description?: string;
|
|
5
5
|
readonly maxLength?: number;
|
|
6
6
|
readonly minLength?: number;
|
|
7
|
+
readonly autoComplete?: string;
|
|
7
8
|
readonly required?: boolean;
|
|
8
9
|
readonly searchable?: boolean;
|
|
9
10
|
readonly sortable?: boolean;
|
|
@@ -26,6 +27,7 @@ export declare const userSessionEntity: import("@cosmicdrift/kumiko-types/fields
|
|
|
26
27
|
readonly description?: string;
|
|
27
28
|
readonly maxLength?: number;
|
|
28
29
|
readonly minLength?: number;
|
|
30
|
+
readonly autoComplete?: string;
|
|
29
31
|
readonly required?: boolean;
|
|
30
32
|
readonly searchable?: boolean;
|
|
31
33
|
readonly sortable?: boolean;
|
|
@@ -108,6 +110,7 @@ export declare const userSessionEntity: import("@cosmicdrift/kumiko-types/fields
|
|
|
108
110
|
readonly description?: string;
|
|
109
111
|
readonly maxLength?: number;
|
|
110
112
|
readonly minLength?: number;
|
|
113
|
+
readonly autoComplete?: string;
|
|
111
114
|
readonly required?: boolean;
|
|
112
115
|
readonly searchable?: boolean;
|
|
113
116
|
readonly sortable?: boolean;
|
|
@@ -130,6 +133,7 @@ export declare const userSessionEntity: import("@cosmicdrift/kumiko-types/fields
|
|
|
130
133
|
readonly description?: string;
|
|
131
134
|
readonly maxLength?: number;
|
|
132
135
|
readonly minLength?: number;
|
|
136
|
+
readonly autoComplete?: string;
|
|
133
137
|
readonly required?: boolean;
|
|
134
138
|
readonly searchable?: boolean;
|
|
135
139
|
readonly sortable?: boolean;
|
package/dist/sessions/screens.js
CHANGED
|
@@ -33,6 +33,7 @@ export const createSessionListScreen = (adminRoles = access.admin) => ({
|
|
|
33
33
|
{
|
|
34
34
|
field: "revokedAt",
|
|
35
35
|
label: i18nKey("sessions.list.col.revokedAt"),
|
|
36
|
+
hideOnNarrow: true,
|
|
36
37
|
renderer: { format: "timestamp" },
|
|
37
38
|
},
|
|
38
39
|
],
|
|
@@ -104,6 +105,7 @@ export const sessionMineScreen = {
|
|
|
104
105
|
{
|
|
105
106
|
field: "current",
|
|
106
107
|
label: i18nKey("sessions.mine.col.current"),
|
|
108
|
+
hideOnNarrow: true,
|
|
107
109
|
renderer: { format: "boolean" },
|
|
108
110
|
},
|
|
109
111
|
],
|
package/dist/shared/index.d.ts
CHANGED
|
@@ -14,6 +14,7 @@ export { denyUnlessJoinRowParentVisible, hostRowIsGone, joinRowParentIsVisible,
|
|
|
14
14
|
export { hashPassword, verifyDummyPassword, verifyPassword } from "./password-hashing.js";
|
|
15
15
|
export { rateLimitedTextResponse } from "./rate-limited-text-response.js";
|
|
16
16
|
export { redactBotTokens, redactEmailAddresses, redactErrorText, redactUrls, } from "./redact.js";
|
|
17
|
+
export { requireForTenant } from "./require-for-tenant.js";
|
|
17
18
|
export { type RowBoundGrantResult, redeemRowBoundGrant, signRowBoundGrant, } from "./row-bound-grant.js";
|
|
18
19
|
export { runInSubTransaction } from "./run-in-sub-transaction.js";
|
|
19
20
|
export { registrationLocaleFromRequest, sessionLocaleField } from "./session-locale-field.js";
|
package/dist/shared/index.js
CHANGED
|
@@ -14,6 +14,7 @@ export { denyUnlessJoinRowParentVisible, hostRowIsGone, joinRowParentIsVisible,
|
|
|
14
14
|
export { hashPassword, verifyDummyPassword, verifyPassword } from "./password-hashing.js";
|
|
15
15
|
export { rateLimitedTextResponse } from "./rate-limited-text-response.js";
|
|
16
16
|
export { redactBotTokens, redactEmailAddresses, redactErrorText, redactUrls, } from "./redact.js";
|
|
17
|
+
export { requireForTenant } from "./require-for-tenant.js";
|
|
17
18
|
export { redeemRowBoundGrant, signRowBoundGrant, } from "./row-bound-grant.js";
|
|
18
19
|
export { runInSubTransaction } from "./run-in-sub-transaction.js";
|
|
19
20
|
export { registrationLocaleFromRequest, sessionLocaleField } from "./session-locale-field.js";
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { InternalError } from "@cosmicdrift/kumiko-framework/errors";
|
|
2
|
+
// ctx.forTenant is absent for r.systemScope() handlers and without a db; a write handler that
|
|
3
|
+
// needs a cross-tenant target must fail loudly instead of falling back to ctx.db.
|
|
4
|
+
export function requireForTenant(ctx, targetTenantId) {
|
|
5
|
+
if (!ctx.forTenant) {
|
|
6
|
+
throw new InternalError({ message: "ctx.forTenant is not available in this handler context" });
|
|
7
|
+
}
|
|
8
|
+
return ctx.forTenant(targetTenantId);
|
|
9
|
+
}
|
package/dist/tags/entity.d.ts
CHANGED
|
@@ -5,6 +5,7 @@ export declare const tagEntity: EntityDefinition<{
|
|
|
5
5
|
readonly description?: string;
|
|
6
6
|
readonly maxLength?: number;
|
|
7
7
|
readonly minLength?: number;
|
|
8
|
+
readonly autoComplete?: string;
|
|
8
9
|
readonly required?: boolean;
|
|
9
10
|
readonly searchable?: boolean;
|
|
10
11
|
readonly sortable?: boolean;
|
|
@@ -27,6 +28,7 @@ export declare const tagEntity: EntityDefinition<{
|
|
|
27
28
|
readonly description?: string;
|
|
28
29
|
readonly maxLength?: number;
|
|
29
30
|
readonly minLength?: number;
|
|
31
|
+
readonly autoComplete?: string;
|
|
30
32
|
readonly required?: boolean;
|
|
31
33
|
readonly searchable?: boolean;
|
|
32
34
|
readonly sortable?: boolean;
|
|
@@ -49,6 +51,7 @@ export declare const tagEntity: EntityDefinition<{
|
|
|
49
51
|
readonly description?: string;
|
|
50
52
|
readonly maxLength?: number;
|
|
51
53
|
readonly minLength?: number;
|
|
54
|
+
readonly autoComplete?: string;
|
|
52
55
|
readonly required?: boolean;
|
|
53
56
|
readonly searchable?: boolean;
|
|
54
57
|
readonly sortable?: boolean;
|
|
@@ -73,6 +76,7 @@ export declare function createTagAssignmentEntity(access?: EntityDefinition["acc
|
|
|
73
76
|
readonly description?: string;
|
|
74
77
|
readonly maxLength?: number;
|
|
75
78
|
readonly minLength?: number;
|
|
79
|
+
readonly autoComplete?: string;
|
|
76
80
|
readonly required?: boolean;
|
|
77
81
|
readonly searchable?: boolean;
|
|
78
82
|
readonly sortable?: boolean;
|
|
@@ -95,6 +99,7 @@ export declare function createTagAssignmentEntity(access?: EntityDefinition["acc
|
|
|
95
99
|
readonly description?: string;
|
|
96
100
|
readonly maxLength?: number;
|
|
97
101
|
readonly minLength?: number;
|
|
102
|
+
readonly autoComplete?: string;
|
|
98
103
|
readonly required?: boolean;
|
|
99
104
|
readonly searchable?: boolean;
|
|
100
105
|
readonly sortable?: boolean;
|
|
@@ -117,6 +122,7 @@ export declare function createTagAssignmentEntity(access?: EntityDefinition["acc
|
|
|
117
122
|
readonly description?: string;
|
|
118
123
|
readonly maxLength?: number;
|
|
119
124
|
readonly minLength?: number;
|
|
125
|
+
readonly autoComplete?: string;
|
|
120
126
|
readonly required?: boolean;
|
|
121
127
|
readonly searchable?: boolean;
|
|
122
128
|
readonly sortable?: boolean;
|
|
@@ -141,6 +147,7 @@ export declare const tagAssignmentEntity: EntityDefinition<{
|
|
|
141
147
|
readonly description?: string;
|
|
142
148
|
readonly maxLength?: number;
|
|
143
149
|
readonly minLength?: number;
|
|
150
|
+
readonly autoComplete?: string;
|
|
144
151
|
readonly required?: boolean;
|
|
145
152
|
readonly searchable?: boolean;
|
|
146
153
|
readonly sortable?: boolean;
|
|
@@ -163,6 +170,7 @@ export declare const tagAssignmentEntity: EntityDefinition<{
|
|
|
163
170
|
readonly description?: string;
|
|
164
171
|
readonly maxLength?: number;
|
|
165
172
|
readonly minLength?: number;
|
|
173
|
+
readonly autoComplete?: string;
|
|
166
174
|
readonly required?: boolean;
|
|
167
175
|
readonly searchable?: boolean;
|
|
168
176
|
readonly sortable?: boolean;
|
|
@@ -185,6 +193,7 @@ export declare const tagAssignmentEntity: EntityDefinition<{
|
|
|
185
193
|
readonly description?: string;
|
|
186
194
|
readonly maxLength?: number;
|
|
187
195
|
readonly minLength?: number;
|
|
196
|
+
readonly autoComplete?: string;
|
|
188
197
|
readonly required?: boolean;
|
|
189
198
|
readonly searchable?: boolean;
|
|
190
199
|
readonly sortable?: boolean;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { fetchOne } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
2
2
|
import { defineWriteHandler, } from "@cosmicdrift/kumiko-framework/engine";
|
|
3
3
|
import * as z from "zod";
|
|
4
|
+
import { requireForTenant } from "../../shared/index.js";
|
|
4
5
|
import { collectionStore, DEFAULT_COLLECTION_ACCESS, } from "./collection-shared.js";
|
|
5
6
|
import { contentFormatSchema, folderSchema, localeSchema, slugSchema } from "./shared.js";
|
|
6
7
|
// Upsert inside one collection. Mirrors set.write for text-blocks, but the
|
|
@@ -29,12 +30,11 @@ export function makeCollectionSetWrite(collection) {
|
|
|
29
30
|
? `Creates or overwrites the caller's own "${collection.id}" entry at a slug and locale, live on save with no draft stage; it always writes the acting user's entry, never another user's.`
|
|
30
31
|
: `Creates or overwrites the tenant-wide "${collection.id}" entry at a slug and locale, live on save with no draft stage; the change is visible to everyone who may reach this shared collection.`,
|
|
31
32
|
handler: async (event, ctx) => {
|
|
32
|
-
const db = ctx.db;
|
|
33
33
|
const override = event.payload.tenantIdOverride;
|
|
34
34
|
// @cast-boundary engine-payload — override is a zod-validated string, the
|
|
35
35
|
// user's own tenantId is already TenantId-branded.
|
|
36
36
|
const tenantId = (override ?? event.user.tenantId);
|
|
37
|
-
const
|
|
37
|
+
const { db, streamTenantId } = requireForTenant(ctx, tenantId);
|
|
38
38
|
// Scoped to the acting user even under a tenant override: a SystemAdmin
|
|
39
39
|
// writing into another tenant still writes their own entry, never
|
|
40
40
|
// someone else's signature.
|
|
@@ -59,7 +59,7 @@ export function makeCollectionSetWrite(collection) {
|
|
|
59
59
|
contentFormat: event.payload.contentFormat,
|
|
60
60
|
folder: event.payload.folder ?? null,
|
|
61
61
|
},
|
|
62
|
-
},
|
|
62
|
+
}, event.user, db, { streamTenantId });
|
|
63
63
|
if (!result.isSuccess)
|
|
64
64
|
return result;
|
|
65
65
|
return {
|
|
@@ -78,7 +78,7 @@ export function makeCollectionSetWrite(collection) {
|
|
|
78
78
|
folder: event.payload.folder ?? null,
|
|
79
79
|
...store.createDefaults(tenantId),
|
|
80
80
|
...owner,
|
|
81
|
-
},
|
|
81
|
+
}, event.user, db, { streamTenantId });
|
|
82
82
|
if (!result.isSuccess)
|
|
83
83
|
return result;
|
|
84
84
|
return {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { fetchOne } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
2
2
|
import { defineWriteHandler, SYSTEM_TENANT_ID, } from "@cosmicdrift/kumiko-framework/engine";
|
|
3
3
|
import * as z from "zod";
|
|
4
|
+
import { requireForTenant } from "../../shared/index.js";
|
|
4
5
|
import { TEMPLATE_KINDS, TEXT_BLOCK_KIND } from "../constants.js";
|
|
5
6
|
import { templateResourcesTable } from "../table.js";
|
|
6
7
|
import { contentFormatSchema, executor, folderSchema, localeSchema, slugSchema } from "./shared.js";
|
|
@@ -42,15 +43,11 @@ export const setWrite = defineWriteHandler({
|
|
|
42
43
|
access: { roles: ["TenantAdmin", "SystemAdmin"] },
|
|
43
44
|
description: "Creates or overwrites one content-tree resource addressed by slug, kind and locale, carrying a title and folder and going live on save with no draft stage; a SystemAdmin may target the system tenant, where shared legal and marketing copy lives.",
|
|
44
45
|
handler: async (event, ctx) => {
|
|
45
|
-
const db = ctx.db;
|
|
46
46
|
const override = event.payload.tenantIdOverride;
|
|
47
47
|
// @cast-boundary engine-payload — override is a zod-validated string, the
|
|
48
48
|
// user's own tenantId is already TenantId-branded.
|
|
49
49
|
const tenantId = (override ?? event.user.tenantId);
|
|
50
|
-
|
|
51
|
-
// otherwise the event-store stream lookup runs against event.user.tenantId
|
|
52
|
-
// and reports a version conflict although the projection row exists.
|
|
53
|
-
const executorUser = override !== undefined ? { ...event.user, tenantId } : event.user;
|
|
50
|
+
const { db, streamTenantId } = requireForTenant(ctx, tenantId);
|
|
54
51
|
const kind = event.payload.kind ?? TEXT_BLOCK_KIND;
|
|
55
52
|
const existing = await fetchOne(db, templateResourcesTable, {
|
|
56
53
|
tenantId,
|
|
@@ -87,7 +84,7 @@ export const setWrite = defineWriteHandler({
|
|
|
87
84
|
contentFormat: fields.contentFormat,
|
|
88
85
|
folder: fields.folder,
|
|
89
86
|
},
|
|
90
|
-
},
|
|
87
|
+
}, event.user, db, { streamTenantId });
|
|
91
88
|
if (!result.isSuccess)
|
|
92
89
|
return result;
|
|
93
90
|
return {
|
|
@@ -95,7 +92,9 @@ export const setWrite = defineWriteHandler({
|
|
|
95
92
|
data: { slug: event.payload.slug, locale: event.payload.locale, isNew: false },
|
|
96
93
|
};
|
|
97
94
|
}
|
|
98
|
-
const result = await executor.create({ ...fields, tenantId },
|
|
95
|
+
const result = await executor.create({ ...fields, tenantId }, event.user, db, {
|
|
96
|
+
streamTenantId,
|
|
97
|
+
});
|
|
99
98
|
if (!result.isSuccess)
|
|
100
99
|
return result;
|
|
101
100
|
return {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { fetchOne } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
2
2
|
import { defineWriteHandler, SYSTEM_TENANT_ID, } from "@cosmicdrift/kumiko-framework/engine";
|
|
3
|
+
import { requireForTenant } from "../../shared/index.js";
|
|
3
4
|
import { templateResourcesTable } from "../table.js";
|
|
4
5
|
import { executor, upsertPayloadSchema } from "./shared.js";
|
|
5
6
|
// System-Template anlegen/updaten. Liegt unter SYSTEM_TENANT_ID,
|
|
@@ -12,13 +13,10 @@ export const upsertSystemWrite = defineWriteHandler({
|
|
|
12
13
|
access: { roles: ["SystemAdmin"] },
|
|
13
14
|
description: "Creates or overwrites a system-default template under the system tenant with its full variable schema and linked resources, active straight away with no draft stage; SystemAdmin only, since tenants shadow these defaults through upsert-tenant instead.",
|
|
14
15
|
handler: async (event, ctx) => {
|
|
15
|
-
const db = ctx.db;
|
|
16
16
|
// @cast-boundary engine-payload — SYSTEM_TENANT_ID is a UUID literal,
|
|
17
17
|
// asserted to the branded TenantId (parseTenantId equivalent).
|
|
18
18
|
const tenantId = SYSTEM_TENANT_ID;
|
|
19
|
-
|
|
20
|
-
// event store looks up the stream under user.tenantId → conflict.
|
|
21
|
-
const executorUser = { ...event.user, tenantId };
|
|
19
|
+
const { db, streamTenantId } = requireForTenant(ctx, tenantId);
|
|
22
20
|
const existing = await fetchOne(db, templateResourcesTable, {
|
|
23
21
|
tenantId,
|
|
24
22
|
slug: event.payload.slug,
|
|
@@ -41,7 +39,7 @@ export const upsertSystemWrite = defineWriteHandler({
|
|
|
41
39
|
status: "active",
|
|
42
40
|
};
|
|
43
41
|
if (existing) {
|
|
44
|
-
const result = await executor.update({ id: existing.id, version: existing.version, changes: fields },
|
|
42
|
+
const result = await executor.update({ id: existing.id, version: existing.version, changes: fields }, event.user, db, { streamTenantId });
|
|
45
43
|
if (!result.isSuccess)
|
|
46
44
|
return result;
|
|
47
45
|
return {
|
|
@@ -49,7 +47,9 @@ export const upsertSystemWrite = defineWriteHandler({
|
|
|
49
47
|
data: { id: String(existing.id), slug: event.payload.slug, isNew: false },
|
|
50
48
|
};
|
|
51
49
|
}
|
|
52
|
-
const result = await executor.create({ ...fields, tenantId },
|
|
50
|
+
const result = await executor.create({ ...fields, tenantId }, event.user, db, {
|
|
51
|
+
streamTenantId,
|
|
52
|
+
});
|
|
53
53
|
if (!result.isSuccess)
|
|
54
54
|
return result;
|
|
55
55
|
// @cast-boundary db-row — executor.create returnt Record-row aus
|
|
@@ -2,6 +2,7 @@ import { fetchOne } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
|
2
2
|
import { defineWriteHandler, SYSTEM_TENANT_ID, } from "@cosmicdrift/kumiko-framework/engine";
|
|
3
3
|
import { AccessDeniedError, writeFailure } from "@cosmicdrift/kumiko-framework/errors";
|
|
4
4
|
import * as z from "zod";
|
|
5
|
+
import { requireForTenant } from "../../shared/index.js";
|
|
5
6
|
import { templateResourcesTable } from "../table.js";
|
|
6
7
|
import { executor, upsertPayloadSchema } from "./shared.js";
|
|
7
8
|
// Tenant-Override anlegen/updaten. Liegt unter event.user.tenantId,
|
|
@@ -18,7 +19,6 @@ export const upsertTenantWrite = defineWriteHandler({
|
|
|
18
19
|
access: { roles: ["TenantAdmin", "SystemAdmin"] },
|
|
19
20
|
description: "Creates or overwrites a tenant-scoped template override with its variable schema, landing as a draft that only publish makes live; it refuses a system-tenant target, which is what upsert-system is for.",
|
|
20
21
|
handler: async (event, ctx) => {
|
|
21
|
-
const db = ctx.db;
|
|
22
22
|
const override = event.payload.tenantIdOverride;
|
|
23
23
|
// upsertTenant erzeugt scope='tenant'. SYSTEM_TENANT_ID-Override würde
|
|
24
24
|
// scope='tenant' unter SYSTEM_TENANT_ID schreiben → inkonsistenter Zustand
|
|
@@ -34,7 +34,7 @@ export const upsertTenantWrite = defineWriteHandler({
|
|
|
34
34
|
// event.user.tenantId schon TenantId-branded; union als TenantId casten
|
|
35
35
|
// ist legit (override ist UUID-Format-validiert in schema).
|
|
36
36
|
const tenantId = (override ?? event.user.tenantId);
|
|
37
|
-
const
|
|
37
|
+
const { db, streamTenantId } = requireForTenant(ctx, tenantId);
|
|
38
38
|
const existing = await fetchOne(db, templateResourcesTable, {
|
|
39
39
|
tenantId,
|
|
40
40
|
slug: event.payload.slug,
|
|
@@ -56,7 +56,7 @@ export const upsertTenantWrite = defineWriteHandler({
|
|
|
56
56
|
status: event.payload.status,
|
|
57
57
|
};
|
|
58
58
|
if (existing) {
|
|
59
|
-
const result = await executor.update({ id: existing.id, version: existing.version, changes: fields },
|
|
59
|
+
const result = await executor.update({ id: existing.id, version: existing.version, changes: fields }, event.user, db, { streamTenantId });
|
|
60
60
|
if (!result.isSuccess)
|
|
61
61
|
return result;
|
|
62
62
|
return {
|
|
@@ -64,7 +64,9 @@ export const upsertTenantWrite = defineWriteHandler({
|
|
|
64
64
|
data: { id: String(existing.id), slug: event.payload.slug, isNew: false },
|
|
65
65
|
};
|
|
66
66
|
}
|
|
67
|
-
const result = await executor.create({ ...fields, tenantId },
|
|
67
|
+
const result = await executor.create({ ...fields, tenantId }, event.user, db, {
|
|
68
|
+
streamTenantId,
|
|
69
|
+
});
|
|
68
70
|
if (!result.isSuccess)
|
|
69
71
|
return result;
|
|
70
72
|
// @cast-boundary db-row — executor.create returnt Record-row aus
|