@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.
Files changed (59) hide show
  1. package/package.json +9 -10
  2. package/src/auth-mfa/__tests__/enable-flow.integration.test.ts +78 -6
  3. package/src/auth-mfa/__tests__/screen.test.ts +58 -0
  4. package/src/auth-mfa/feature.ts +6 -16
  5. package/src/auth-mfa/handlers/enable-start.write.ts +21 -12
  6. package/src/auth-mfa/i18n.ts +19 -0
  7. package/src/auth-mfa/screens.ts +52 -0
  8. package/src/auth-mfa/web/client-plugin.ts +4 -5
  9. package/src/auth-mfa/web/i18n.ts +0 -7
  10. package/src/auth-mfa/web/index.ts +0 -2
  11. package/src/auth-mfa/web/mfa-disable-dialog.tsx +2 -2
  12. package/src/auth-mfa/web/mfa-recovery-codes-reveal.tsx +4 -4
  13. package/src/auth-mfa/web/mfa-setup-preauth-screen.tsx +5 -5
  14. package/src/auth-mfa/web/qrcode-browser.d.ts +2 -2
  15. package/src/folders/__tests__/folders-read-gate.integration.test.ts +179 -0
  16. package/src/folders/entity.ts +47 -28
  17. package/src/folders/feature.ts +58 -4
  18. package/src/folders/handlers/clear-folder.write.ts +4 -4
  19. package/src/folders/handlers/set-folder.write.ts +4 -4
  20. package/src/ledger/__tests__/ledger.integration.test.ts +111 -4
  21. package/src/ledger/entity.ts +30 -0
  22. package/src/ledger/handlers/confirm-schedule-period.write.ts +7 -0
  23. package/src/ledger/handlers/create-transaction.write.ts +2 -0
  24. package/src/ledger/schemas.ts +3 -0
  25. package/src/notes-history/__tests__/add-note-author-name-kms.integration.test.ts +2 -1
  26. package/src/notes-history/__tests__/notes-history-ownership.integration.test.ts +4 -1
  27. package/src/notes-history/__tests__/notes-history-parent-visibility.integration.test.ts +3 -1
  28. package/src/notes-history/__tests__/notes-history.integration.test.ts +2 -1
  29. package/src/notes-history/__tests__/notes-read-gate.integration.test.ts +243 -0
  30. package/src/notes-history/entity.ts +11 -1
  31. package/src/notes-history/feature.ts +12 -11
  32. package/src/notes-history/handlers/add-note.write.ts +8 -12
  33. package/src/personal-access-tokens/__tests__/expiry.test.ts +19 -0
  34. package/src/personal-access-tokens/__tests__/feature.test.ts +93 -0
  35. package/src/personal-access-tokens/__tests__/pat.integration.test.ts +143 -13
  36. package/src/personal-access-tokens/constants.ts +5 -2
  37. package/src/personal-access-tokens/expiry.ts +7 -0
  38. package/src/personal-access-tokens/feature.ts +8 -17
  39. package/src/personal-access-tokens/handlers/list.query.ts +75 -16
  40. package/src/personal-access-tokens/i18n.ts +54 -0
  41. package/src/personal-access-tokens/resolver.ts +2 -7
  42. package/src/personal-access-tokens/screens.ts +107 -0
  43. package/src/shared/index.ts +1 -1
  44. package/src/shared/parent-visibility.ts +30 -0
  45. package/src/tags/__tests__/tags-read-gate.integration.test.ts +366 -0
  46. package/src/tags/__tests__/tags.integration.test.ts +1 -0
  47. package/src/tags/entity.ts +11 -1
  48. package/src/tags/feature.ts +25 -8
  49. package/src/tags/handlers/assign-tag.write.ts +2 -8
  50. package/src/tags/handlers/remove-tag.write.ts +2 -8
  51. package/src/tags/web/__tests__/entity-tags.test.tsx +13 -4
  52. package/src/tags/web/entity-tags.tsx +9 -2
  53. package/src/tags/web/tag-section.tsx +9 -4
  54. package/src/auth-mfa/web/__tests__/mfa-enable-screen.test.tsx +0 -182
  55. package/src/auth-mfa/web/mfa-enable-screen.tsx +0 -224
  56. package/src/personal-access-tokens/web/client-plugin.tsx +0 -29
  57. package/src/personal-access-tokens/web/i18n.ts +0 -40
  58. package/src/personal-access-tokens/web/index.ts +0 -7
  59. 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
- }