@cosmicdrift/kumiko-bundled-features 0.256.0 → 0.258.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 -10
- package/src/auth-mfa/__tests__/enable-flow.integration.test.ts +78 -6
- package/src/auth-mfa/__tests__/screen.test.ts +58 -0
- package/src/auth-mfa/feature.ts +6 -16
- package/src/auth-mfa/handlers/enable-start.write.ts +21 -12
- package/src/auth-mfa/i18n.ts +19 -0
- package/src/auth-mfa/screens.ts +52 -0
- package/src/auth-mfa/web/client-plugin.ts +4 -5
- package/src/auth-mfa/web/i18n.ts +0 -7
- package/src/auth-mfa/web/index.ts +0 -2
- package/src/auth-mfa/web/mfa-disable-dialog.tsx +2 -2
- package/src/auth-mfa/web/mfa-recovery-codes-reveal.tsx +4 -4
- package/src/auth-mfa/web/mfa-setup-preauth-screen.tsx +5 -5
- package/src/auth-mfa/web/qrcode-browser.d.ts +2 -2
- package/src/folders/__tests__/folders-read-gate.integration.test.ts +179 -0
- package/src/folders/entity.ts +47 -28
- package/src/folders/feature.ts +58 -4
- package/src/folders/handlers/clear-folder.write.ts +4 -4
- package/src/folders/handlers/set-folder.write.ts +4 -4
- package/src/ledger/__tests__/ledger.integration.test.ts +111 -4
- package/src/ledger/entity.ts +30 -0
- package/src/ledger/handlers/confirm-schedule-period.write.ts +7 -0
- package/src/ledger/handlers/create-transaction.write.ts +2 -0
- package/src/ledger/schemas.ts +3 -0
- package/src/notes-history/__tests__/add-note-author-name-kms.integration.test.ts +2 -1
- package/src/notes-history/__tests__/notes-history-ownership.integration.test.ts +4 -1
- package/src/notes-history/__tests__/notes-history-parent-visibility.integration.test.ts +3 -1
- package/src/notes-history/__tests__/notes-history.integration.test.ts +2 -1
- package/src/notes-history/__tests__/notes-read-gate.integration.test.ts +243 -0
- package/src/notes-history/entity.ts +11 -1
- package/src/notes-history/feature.ts +12 -11
- package/src/notes-history/handlers/add-note.write.ts +8 -12
- package/src/personal-access-tokens/__tests__/expiry.test.ts +19 -0
- package/src/personal-access-tokens/__tests__/feature.test.ts +93 -0
- package/src/personal-access-tokens/__tests__/pat.integration.test.ts +143 -13
- package/src/personal-access-tokens/constants.ts +5 -2
- package/src/personal-access-tokens/expiry.ts +7 -0
- package/src/personal-access-tokens/feature.ts +8 -17
- package/src/personal-access-tokens/handlers/list.query.ts +75 -16
- package/src/personal-access-tokens/i18n.ts +54 -0
- package/src/personal-access-tokens/resolver.ts +2 -7
- package/src/personal-access-tokens/screens.ts +107 -0
- package/src/shared/index.ts +1 -1
- package/src/shared/parent-visibility.ts +30 -0
- package/src/tags/__tests__/tags-read-gate.integration.test.ts +366 -0
- package/src/tags/__tests__/tags.integration.test.ts +1 -0
- package/src/tags/entity.ts +11 -1
- package/src/tags/feature.ts +25 -8
- package/src/tags/handlers/assign-tag.write.ts +2 -8
- package/src/tags/handlers/remove-tag.write.ts +2 -8
- package/src/tags/web/__tests__/entity-tags.test.tsx +13 -4
- package/src/tags/web/entity-tags.tsx +9 -2
- package/src/tags/web/tag-section.tsx +9 -4
- package/src/auth-mfa/web/__tests__/mfa-enable-screen.test.tsx +0 -182
- package/src/auth-mfa/web/mfa-enable-screen.tsx +0 -224
- package/src/personal-access-tokens/web/client-plugin.tsx +0 -29
- package/src/personal-access-tokens/web/i18n.ts +0 -40
- package/src/personal-access-tokens/web/index.ts +0 -7
- package/src/personal-access-tokens/web/pat-tokens-screen.tsx +0 -296
|
@@ -1,296 +0,0 @@
|
|
|
1
|
-
// @runtime client
|
|
2
|
-
// PatTokensScreen — logged-in self-service for Personal Access Tokens. Two-axis
|
|
3
|
-
// scopes (like GitHub fine-grained PATs): per API domain pick a permission LEVEL
|
|
4
|
-
// (no access / read / read & write). Mint → copy the plaintext ONCE → it's never
|
|
5
|
-
// re-displayed. Layout follows the framework's polished-screen convention (Form
|
|
6
|
-
// primitive + Tailwind), cards match the mh style. The feature registers this
|
|
7
|
-
// dormant (r.screen); the app places it via r.nav.
|
|
8
|
-
|
|
9
|
-
import {
|
|
10
|
-
useDispatcher,
|
|
11
|
-
usePrimitives,
|
|
12
|
-
useQuery,
|
|
13
|
-
useTranslation,
|
|
14
|
-
} from "@cosmicdrift/kumiko-renderer";
|
|
15
|
-
import { FormScreenShell } from "@cosmicdrift/kumiko-renderer-web";
|
|
16
|
-
import { type ReactNode, useState } from "react";
|
|
17
|
-
import { PatHandlers, PatQueries } from "../constants";
|
|
18
|
-
import { parseGrant } from "../scopes";
|
|
19
|
-
|
|
20
|
-
type ScopeDomain = { readonly name: string; readonly label: string; readonly canWrite: boolean };
|
|
21
|
-
type TokenRow = {
|
|
22
|
-
readonly id: string;
|
|
23
|
-
readonly name: string;
|
|
24
|
-
readonly scopes: readonly string[];
|
|
25
|
-
readonly createdAt: string | null;
|
|
26
|
-
readonly expiresAt: string | null;
|
|
27
|
-
readonly revokedAt: string | null;
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
type Level = "none" | "read" | "write";
|
|
31
|
-
type ExpiryKey = "30d" | "90d" | "1y" | "never";
|
|
32
|
-
const EXPIRY_DAYS: Record<ExpiryKey, number | undefined> = {
|
|
33
|
-
"30d": 30,
|
|
34
|
-
"90d": 90,
|
|
35
|
-
"1y": 365,
|
|
36
|
-
never: undefined,
|
|
37
|
-
};
|
|
38
|
-
|
|
39
|
-
// Timestamps arrive as ISO strings over the JSON API — slice the date part
|
|
40
|
-
// without touching the banned Date API.
|
|
41
|
-
function isoDate(value: string | null): string | null {
|
|
42
|
-
return typeof value === "string" && value.length >= 10 ? value.slice(0, 10) : null;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export function PatTokensScreen({
|
|
46
|
-
embedded = false,
|
|
47
|
-
}: {
|
|
48
|
-
readonly embedded?: boolean;
|
|
49
|
-
} = {}): ReactNode {
|
|
50
|
-
const t = useTranslation();
|
|
51
|
-
const { Form, Field, Input, Button, Banner, Card, Heading } = usePrimitives();
|
|
52
|
-
const dispatcher = useDispatcher();
|
|
53
|
-
|
|
54
|
-
const scopesQuery = useQuery<readonly ScopeDomain[]>(PatQueries.availableScopes, {});
|
|
55
|
-
const listQuery = useQuery<readonly TokenRow[]>(PatQueries.mine, {});
|
|
56
|
-
|
|
57
|
-
const [name, setName] = useState("");
|
|
58
|
-
const [levels, setLevels] = useState<Readonly<Record<string, Level>>>({});
|
|
59
|
-
const [expiry, setExpiry] = useState<ExpiryKey>("90d");
|
|
60
|
-
const [currentPassword, setCurrentPassword] = useState("");
|
|
61
|
-
const [mfaCode, setMfaCode] = useState("");
|
|
62
|
-
const [minted, setMinted] = useState<{ token: string } | null>(null);
|
|
63
|
-
const [copied, setCopied] = useState(false);
|
|
64
|
-
const [error, setError] = useState<string | null>(null);
|
|
65
|
-
const [busy, setBusy] = useState(false);
|
|
66
|
-
|
|
67
|
-
const domains = scopesQuery.data ?? [];
|
|
68
|
-
const tokens = listQuery.data ?? [];
|
|
69
|
-
const labelOf = (domain: string): string =>
|
|
70
|
-
domains.find((d) => d.name === domain)?.label ?? domain;
|
|
71
|
-
|
|
72
|
-
// Granted scopes = "<domain>:<level>" for every domain not set to "none".
|
|
73
|
-
const grants = (): string[] =>
|
|
74
|
-
Object.entries(levels)
|
|
75
|
-
.filter(([, l]) => l !== "none")
|
|
76
|
-
.map(([domain, l]) => `${domain}:${l}`);
|
|
77
|
-
|
|
78
|
-
const setLevel = (domain: string, level: Level): void =>
|
|
79
|
-
setLevels((cur) => ({ ...cur, [domain]: level }));
|
|
80
|
-
|
|
81
|
-
const create = async (): Promise<void> => {
|
|
82
|
-
if (name.trim() === "") return setError(t("pat.create.needName"));
|
|
83
|
-
const scopes = grants();
|
|
84
|
-
if (scopes.length === 0) return setError(t("pat.create.needScope"));
|
|
85
|
-
if (currentPassword === "") return setError(t("pat.create.needPassword"));
|
|
86
|
-
setBusy(true);
|
|
87
|
-
setError(null);
|
|
88
|
-
const days = EXPIRY_DAYS[expiry];
|
|
89
|
-
const res = await dispatcher.write(PatHandlers.create, {
|
|
90
|
-
name: name.trim(),
|
|
91
|
-
scopes,
|
|
92
|
-
currentPassword,
|
|
93
|
-
...(days !== undefined ? { expiresInDays: days } : {}),
|
|
94
|
-
...(mfaCode.trim() !== "" ? { mfaCode: mfaCode.trim() } : {}),
|
|
95
|
-
});
|
|
96
|
-
setBusy(false);
|
|
97
|
-
// Never leave a submitted password/MFA code sitting in state, success or not.
|
|
98
|
-
setCurrentPassword("");
|
|
99
|
-
setMfaCode("");
|
|
100
|
-
if (!res.isSuccess) return setError(t("pat.error.generic"));
|
|
101
|
-
setMinted({ token: (res.data as { token: string }).token });
|
|
102
|
-
setCopied(false);
|
|
103
|
-
setName("");
|
|
104
|
-
setLevels({});
|
|
105
|
-
setExpiry("90d");
|
|
106
|
-
void listQuery.refetch?.();
|
|
107
|
-
};
|
|
108
|
-
|
|
109
|
-
const copy = async (): Promise<void> => {
|
|
110
|
-
if (!minted) return;
|
|
111
|
-
try {
|
|
112
|
-
await navigator.clipboard.writeText(minted.token);
|
|
113
|
-
setCopied(true);
|
|
114
|
-
} catch {
|
|
115
|
-
// clipboard blocked (non-secure context) — the token stays selectable in place
|
|
116
|
-
}
|
|
117
|
-
};
|
|
118
|
-
|
|
119
|
-
const revoke = async (id: string): Promise<void> => {
|
|
120
|
-
const res = await dispatcher.write(PatHandlers.revoke, { id });
|
|
121
|
-
if (!res.isSuccess) return setError(t("pat.error.generic"));
|
|
122
|
-
void listQuery.refetch?.();
|
|
123
|
-
};
|
|
124
|
-
|
|
125
|
-
// "Pages (read & write) · Tags (read) · Valid until … · Created …"
|
|
126
|
-
const meta = (row: TokenRow): string => {
|
|
127
|
-
const scopeText = row.scopes
|
|
128
|
-
.map((g) => {
|
|
129
|
-
const p = parseGrant(g);
|
|
130
|
-
if (!p) return g;
|
|
131
|
-
const lvl = p.level === "write" ? t("pat.level.write") : t("pat.level.read");
|
|
132
|
-
return `${labelOf(p.domain)} (${lvl})`;
|
|
133
|
-
})
|
|
134
|
-
.join(" · ");
|
|
135
|
-
const d = isoDate(row.expiresAt);
|
|
136
|
-
const parts = [
|
|
137
|
-
scopeText,
|
|
138
|
-
d ? t("pat.list.validUntil", { date: d }) : t("pat.list.neverExpires"),
|
|
139
|
-
];
|
|
140
|
-
const created = isoDate(row.createdAt);
|
|
141
|
-
if (created) parts.push(t("pat.list.created", { date: created }));
|
|
142
|
-
return parts.join(" · ");
|
|
143
|
-
};
|
|
144
|
-
|
|
145
|
-
const content = (
|
|
146
|
-
<>
|
|
147
|
-
<Heading>{t("pat.title")}</Heading>
|
|
148
|
-
|
|
149
|
-
{minted && (
|
|
150
|
-
<Banner
|
|
151
|
-
variant="info"
|
|
152
|
-
actions={
|
|
153
|
-
<>
|
|
154
|
-
<Button variant="primary" onClick={copy}>
|
|
155
|
-
{copied ? t("pat.created.copied") : t("pat.created.copy")}
|
|
156
|
-
</Button>
|
|
157
|
-
<Button variant="secondary" onClick={() => setMinted(null)}>
|
|
158
|
-
{t("pat.created.dismiss")}
|
|
159
|
-
</Button>
|
|
160
|
-
</>
|
|
161
|
-
}
|
|
162
|
-
>
|
|
163
|
-
<div className="flex flex-col gap-2">
|
|
164
|
-
<span className="text-sm">{t("pat.created.hint")}</span>
|
|
165
|
-
<code className="block break-all rounded bg-muted px-3 py-2 font-mono text-sm">
|
|
166
|
-
{minted.token}
|
|
167
|
-
</code>
|
|
168
|
-
</div>
|
|
169
|
-
</Banner>
|
|
170
|
-
)}
|
|
171
|
-
|
|
172
|
-
{error && <Banner variant="error">{error}</Banner>}
|
|
173
|
-
|
|
174
|
-
<Form
|
|
175
|
-
title={t("pat.create.title")}
|
|
176
|
-
subtitle={t("pat.create.subtitle")}
|
|
177
|
-
onSubmit={create}
|
|
178
|
-
actions={
|
|
179
|
-
<Button type="submit" variant="primary" onClick={create} loading={busy} disabled={busy}>
|
|
180
|
-
{t("pat.create.submit")}
|
|
181
|
-
</Button>
|
|
182
|
-
}
|
|
183
|
-
>
|
|
184
|
-
<Field id="pat-name" label={t("pat.create.name")} required>
|
|
185
|
-
<Input
|
|
186
|
-
kind="text"
|
|
187
|
-
id="pat-name"
|
|
188
|
-
name="name"
|
|
189
|
-
value={name}
|
|
190
|
-
onChange={setName}
|
|
191
|
-
placeholder={t("pat.create.namePlaceholder")}
|
|
192
|
-
disabled={busy}
|
|
193
|
-
/>
|
|
194
|
-
</Field>
|
|
195
|
-
|
|
196
|
-
{domains.map((d) => (
|
|
197
|
-
<Field key={d.name} id={`pat-scope-${d.name}`} label={d.label}>
|
|
198
|
-
<Input
|
|
199
|
-
kind="select"
|
|
200
|
-
id={`pat-scope-${d.name}`}
|
|
201
|
-
name={d.name}
|
|
202
|
-
value={levels[d.name] ?? "none"}
|
|
203
|
-
onChange={(v) => setLevel(d.name, v as Level)}
|
|
204
|
-
options={[
|
|
205
|
-
{ value: "none", label: t("pat.level.none") },
|
|
206
|
-
{ value: "read", label: t("pat.level.read") },
|
|
207
|
-
...(d.canWrite ? [{ value: "write", label: t("pat.level.write") }] : []),
|
|
208
|
-
]}
|
|
209
|
-
disabled={busy}
|
|
210
|
-
/>
|
|
211
|
-
</Field>
|
|
212
|
-
))}
|
|
213
|
-
|
|
214
|
-
<Field id="pat-expiry" label={t("pat.create.expiry")}>
|
|
215
|
-
<Input
|
|
216
|
-
kind="select"
|
|
217
|
-
id="pat-expiry"
|
|
218
|
-
name="expiry"
|
|
219
|
-
value={expiry}
|
|
220
|
-
onChange={(v) => setExpiry(v as ExpiryKey)}
|
|
221
|
-
options={[
|
|
222
|
-
{ value: "30d", label: t("pat.expiry.30d") },
|
|
223
|
-
{ value: "90d", label: t("pat.expiry.90d") },
|
|
224
|
-
{ value: "1y", label: t("pat.expiry.1y") },
|
|
225
|
-
{ value: "never", label: t("pat.expiry.never") },
|
|
226
|
-
]}
|
|
227
|
-
disabled={busy}
|
|
228
|
-
/>
|
|
229
|
-
</Field>
|
|
230
|
-
|
|
231
|
-
<Field id="pat-current-password" label={t("pat.create.currentPassword")} required>
|
|
232
|
-
<Input
|
|
233
|
-
kind="password"
|
|
234
|
-
id="pat-current-password"
|
|
235
|
-
name="currentPassword"
|
|
236
|
-
value={currentPassword}
|
|
237
|
-
onChange={setCurrentPassword}
|
|
238
|
-
disabled={busy}
|
|
239
|
-
/>
|
|
240
|
-
</Field>
|
|
241
|
-
|
|
242
|
-
<Field id="pat-mfa-code" label={t("pat.create.mfaCode")}>
|
|
243
|
-
<Input
|
|
244
|
-
kind="text"
|
|
245
|
-
id="pat-mfa-code"
|
|
246
|
-
name="mfaCode"
|
|
247
|
-
value={mfaCode}
|
|
248
|
-
onChange={setMfaCode}
|
|
249
|
-
placeholder={t("pat.create.mfaCodePlaceholder")}
|
|
250
|
-
disabled={busy}
|
|
251
|
-
/>
|
|
252
|
-
</Field>
|
|
253
|
-
</Form>
|
|
254
|
-
|
|
255
|
-
<div className="flex flex-col gap-3">
|
|
256
|
-
<Heading>{t("pat.list.title")}</Heading>
|
|
257
|
-
{tokens.length === 0 && (
|
|
258
|
-
<span className="text-sm text-muted-foreground">{t("pat.list.empty")}</span>
|
|
259
|
-
)}
|
|
260
|
-
{tokens.map((row) => {
|
|
261
|
-
const revoked = row.revokedAt !== null;
|
|
262
|
-
return (
|
|
263
|
-
<Card
|
|
264
|
-
key={row.id}
|
|
265
|
-
options={{ padded: false }}
|
|
266
|
-
className={`p-4 ${revoked ? "opacity-60" : ""}`}
|
|
267
|
-
>
|
|
268
|
-
<div className="flex items-center justify-between gap-3">
|
|
269
|
-
<div className="flex min-w-0 flex-col gap-0.5">
|
|
270
|
-
<span className="truncate text-sm font-semibold">{row.name}</span>
|
|
271
|
-
<span className="whitespace-normal text-xs text-muted-foreground">
|
|
272
|
-
{revoked ? t("pat.list.revoked") : meta(row)}
|
|
273
|
-
</span>
|
|
274
|
-
</div>
|
|
275
|
-
{!revoked && (
|
|
276
|
-
<Button variant="danger" onClick={() => revoke(row.id)}>
|
|
277
|
-
{t("pat.list.revoke")}
|
|
278
|
-
</Button>
|
|
279
|
-
)}
|
|
280
|
-
</div>
|
|
281
|
-
</Card>
|
|
282
|
-
);
|
|
283
|
-
})}
|
|
284
|
-
</div>
|
|
285
|
-
</>
|
|
286
|
-
);
|
|
287
|
-
|
|
288
|
-
if (embedded) {
|
|
289
|
-
return <div className="flex flex-col gap-6">{content}</div>;
|
|
290
|
-
}
|
|
291
|
-
return (
|
|
292
|
-
<FormScreenShell testId="pat-tokens-screen" className="flex flex-col gap-6">
|
|
293
|
-
{content}
|
|
294
|
-
</FormScreenShell>
|
|
295
|
-
);
|
|
296
|
-
}
|