@dloizides/ui-access-admin 1.0.1
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/CHANGELOG.md +51 -0
- package/LICENSE +21 -0
- package/README.md +136 -0
- package/dist/index.d.mts +544 -0
- package/dist/index.d.ts +544 -0
- package/dist/index.js +576 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +558 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +121 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,544 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { ControlOption } from '@dloizides/ui-forms';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* How a settled write should be reported. Its own file per the repo's
|
|
6
|
+
* "one exported enum per file" rule.
|
|
7
|
+
*
|
|
8
|
+
* Two values, not three. There is no "warning" because every branch this package
|
|
9
|
+
* reports is either a thing that worked or a thing the user has to act on — and
|
|
10
|
+
* the one case that tempts a warning is the most important error in the package:
|
|
11
|
+
* a mint that SUCCEEDED but whose URL never reached the clipboard. That is
|
|
12
|
+
* reported as `Error`, loudly, because the URL is readable exactly once and a
|
|
13
|
+
* user who dismisses it without copying by hand has to revoke and re-mint. An
|
|
14
|
+
* amber "heads up" would let that slide past.
|
|
15
|
+
*/
|
|
16
|
+
declare const enum AccessResultTone {
|
|
17
|
+
Success = "success",
|
|
18
|
+
Error = "error"
|
|
19
|
+
}
|
|
20
|
+
/** Reports a settled outcome with an ALREADY-LOCALIZED message. */
|
|
21
|
+
type AccessResultReporter = (message: string, tone: AccessResultTone) => void;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* How long a freshly minted capability link should live.
|
|
25
|
+
*
|
|
26
|
+
* ── THE GENERALIZATION ────────────────────────────────────────────────────────
|
|
27
|
+
* kefi computed this as "the event date, plus a two-week payout-reconciliation
|
|
28
|
+
* tail, clamped to what the API accepts". Every noun there is kefi's, but the
|
|
29
|
+
* SHAPE is not: a capability is nearly always anchored to some real-world moment
|
|
30
|
+
* (an event, a deadline, a booking, a review window) and needs to outlive that
|
|
31
|
+
* moment by however long the follow-up work takes. So the policy is expressed as
|
|
32
|
+
* an anchor plus a tail, and the host names them.
|
|
33
|
+
*
|
|
34
|
+
* ── THE ONE RULE THAT DOES NOT BEND ───────────────────────────────────────────
|
|
35
|
+
* There is NO code path here that returns "never". Not for a missing anchor, not
|
|
36
|
+
* for an unparseable one, not for an anchor in the past. A capability link is a
|
|
37
|
+
* bearer token pasted into a chat app; the expiry is the only thing that limits
|
|
38
|
+
* the blast radius of it being forwarded. A fallback to "never" would turn the
|
|
39
|
+
* least-informed case — we could not read the date — into the most dangerous
|
|
40
|
+
* outcome, which is exactly backwards. A missing anchor yields the bare tail: a
|
|
41
|
+
* link that still dies on a clock.
|
|
42
|
+
*/
|
|
43
|
+
/**
|
|
44
|
+
* The host's expiry rule.
|
|
45
|
+
*
|
|
46
|
+
* `minDays`/`maxDays` are REQUIRED rather than defaulted, because they encode
|
|
47
|
+
* what a specific API will accept. A default here would be a guess that fails
|
|
48
|
+
* server-side at mint time, which is the worst place to find out.
|
|
49
|
+
*/
|
|
50
|
+
interface ExpiryPolicy {
|
|
51
|
+
/**
|
|
52
|
+
* ISO date/timestamp the capability is anchored to, or null/undefined when
|
|
53
|
+
* there is no anchor. Unparseable values are treated as absent.
|
|
54
|
+
*/
|
|
55
|
+
readonly anchorDate?: string | null;
|
|
56
|
+
/** Whole days the link survives PAST the anchor (or from now, when unanchored). */
|
|
57
|
+
readonly tailDays: number;
|
|
58
|
+
/** Smallest expiry the host's API accepts. */
|
|
59
|
+
readonly minDays: number;
|
|
60
|
+
/** Largest expiry the host's API accepts. Anchors beyond this clamp, never unbound. */
|
|
61
|
+
readonly maxDays: number;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Whole days from `now` until `anchorDate`, or null when the date is unusable.
|
|
65
|
+
*
|
|
66
|
+
* Both sides are floored to UTC midnight before subtracting, so the answer does
|
|
67
|
+
* not flip by one depending on what time of day the button happens to be pressed.
|
|
68
|
+
* Negative when the anchor is in the past — the caller decides what that means.
|
|
69
|
+
*/
|
|
70
|
+
declare function daysUntilAnchor(anchorDate: string | null | undefined, now: Date): number | null;
|
|
71
|
+
/**
|
|
72
|
+
* The expiry, in whole days from now, for a link minted under this policy.
|
|
73
|
+
*
|
|
74
|
+
* Three cases, all of which still expire:
|
|
75
|
+
* - no usable anchor → the bare tail;
|
|
76
|
+
* - anchor in the past → tail minus however long ago it was, floored at `minDays`,
|
|
77
|
+
* so a link minted while settling up afterwards still dies soon;
|
|
78
|
+
* - anchor beyond the ceiling → clamped to `maxDays`.
|
|
79
|
+
*/
|
|
80
|
+
declare function capabilityExpiryDays(policy: ExpiryPolicy, now: Date): number;
|
|
81
|
+
/** The `yyyy-mm-dd` date a link minted now under this policy would stop working on. */
|
|
82
|
+
declare function capabilityExpiryDate(policy: ExpiryPolicy, now: Date): string;
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* The neutral vocabulary this package speaks.
|
|
86
|
+
*
|
|
87
|
+
* ── WHY THESE TYPES AND NOT THE HOST'S ────────────────────────────────────────
|
|
88
|
+
* This package was extracted from kefi-web, where the same screens are written in
|
|
89
|
+
* terms of a `PromoterDto` (an act on an event, with a role, a headcount, an
|
|
90
|
+
* Instagram handle and a payout flag). None of that survived the extraction, on
|
|
91
|
+
* purpose: a shared component that still needs a `PromoterDto` has not been
|
|
92
|
+
* shared, it has been MOVED, and the next portal to want it has to either invent
|
|
93
|
+
* a fake promoter or fork the file.
|
|
94
|
+
*
|
|
95
|
+
* So the package knows about exactly three things:
|
|
96
|
+
* - a SUBJECT: something with an id and a name you can put in a heading;
|
|
97
|
+
* - a CAPABILITY LINK: a URL that grants access to whoever holds it, until it
|
|
98
|
+
* expires or is revoked;
|
|
99
|
+
* - an ACCOUNT: something a subject can be bound to so it can sign in.
|
|
100
|
+
*
|
|
101
|
+
* A survey respondent, a restaurant's menu editor, a wallet-screening reviewer
|
|
102
|
+
* and a party promoter are all subjects. None of them is in this file.
|
|
103
|
+
*
|
|
104
|
+
* ── THE HOST OWNS THE WIRE ────────────────────────────────────────────────────
|
|
105
|
+
* There are no API calls in this package and no react-query dependency. Every
|
|
106
|
+
* write is an injected async callback, so a host wires its own endpoints, its own
|
|
107
|
+
* client, its own cache invalidation. The package supplies the screen, the
|
|
108
|
+
* sequencing and the pure rules — not the transport.
|
|
109
|
+
*/
|
|
110
|
+
/**
|
|
111
|
+
* A person (or act, or seat) an access link or a sign-in account can be bound to.
|
|
112
|
+
*
|
|
113
|
+
* Deliberately two fields. Anything else a host wants to show belongs in the
|
|
114
|
+
* host's own row, not in the panel — the moment this grows a `role` or an
|
|
115
|
+
* `email` it starts encoding one product's idea of who a subject is.
|
|
116
|
+
*/
|
|
117
|
+
interface AccessSubject {
|
|
118
|
+
/** Stable external id. Used to match links back to their subject. */
|
|
119
|
+
readonly id: string;
|
|
120
|
+
/** Display name, already in the user's language. Shown in headings. */
|
|
121
|
+
readonly displayName: string;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* One capability link, normalized by the host from whatever its API returns.
|
|
125
|
+
*
|
|
126
|
+
* "Capability" is the security-relevant word: holding the URL IS the access,
|
|
127
|
+
* with no sign-in in the way. That is why the panel warns about sharing, why it
|
|
128
|
+
* insists on an expiry, and why revocation is always one press away.
|
|
129
|
+
*/
|
|
130
|
+
interface CapabilityLink {
|
|
131
|
+
/** Stable id, used as the React key and passed to the host's revoke action. */
|
|
132
|
+
readonly id: string;
|
|
133
|
+
/**
|
|
134
|
+
* The subject this link grants access to.
|
|
135
|
+
*
|
|
136
|
+
* The HOST resolves this. That matters: in kefi the binding is not on the wire
|
|
137
|
+
* at all (the API echoes back a recipient name, not the promoter id), so the
|
|
138
|
+
* kefi adapter matches on name and documents why. Pushing that resolution to
|
|
139
|
+
* the host keeps a product-specific coupling out of the shared filter, and
|
|
140
|
+
* lets a host with a proper foreign key just read it.
|
|
141
|
+
*/
|
|
142
|
+
readonly subjectId: string;
|
|
143
|
+
/**
|
|
144
|
+
* The link URL, when it is known.
|
|
145
|
+
*
|
|
146
|
+
* Usually `null` for a listed link: a well-built API returns the URL exactly
|
|
147
|
+
* ONCE, at mint time, and never again. The panel is built around that — it
|
|
148
|
+
* reveals a freshly minted URL and tells the user it cannot be re-read.
|
|
149
|
+
*/
|
|
150
|
+
readonly url?: string | null;
|
|
151
|
+
/** ISO timestamp the link was created, or null when unknown. */
|
|
152
|
+
readonly createdAt?: string | null;
|
|
153
|
+
/**
|
|
154
|
+
* ISO timestamp the link stops working, or null when it never expires.
|
|
155
|
+
*
|
|
156
|
+
* Null renders as the host's "never" string. It is representable because some
|
|
157
|
+
* APIs allow it — NOT because this package mints them. See `expiryPolicy`.
|
|
158
|
+
*/
|
|
159
|
+
readonly expiresAt?: string | null;
|
|
160
|
+
/** False once revoked or expired — only active links are worth revoking. */
|
|
161
|
+
readonly isActive: boolean;
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* A sign-in account a subject can be linked to.
|
|
165
|
+
*
|
|
166
|
+
* `displayName` and `email` are both optional because directories genuinely
|
|
167
|
+
* return entries with neither; the panel falls back to a host-supplied label
|
|
168
|
+
* rather than rendering an empty row.
|
|
169
|
+
*/
|
|
170
|
+
interface LinkableAccount {
|
|
171
|
+
/** Stable account id — the value submitted when linking. */
|
|
172
|
+
readonly userId: string;
|
|
173
|
+
readonly displayName?: string | null;
|
|
174
|
+
readonly email?: string | null;
|
|
175
|
+
}
|
|
176
|
+
/** What the host is asked to mint. Days, not a date, so the host's API decides the clock. */
|
|
177
|
+
interface MintCapabilityLinkInput {
|
|
178
|
+
/** The subject the link is bound to. */
|
|
179
|
+
readonly subject: AccessSubject;
|
|
180
|
+
/** Whole days from now until the link should stop working. Never zero, never absent. */
|
|
181
|
+
readonly expiresInDays: number;
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Mint a capability link.
|
|
185
|
+
*
|
|
186
|
+
* Resolves with the minted link — which MUST carry `url`, because revealing it
|
|
187
|
+
* once is the entire point of the press. Rejects with an `Error`; the host maps
|
|
188
|
+
* that to a message via `describeError`, since only the host knows its own API's
|
|
189
|
+
* status codes and problem bodies.
|
|
190
|
+
*/
|
|
191
|
+
type MintCapabilityLink = (input: MintCapabilityLinkInput) => Promise<CapabilityLink>;
|
|
192
|
+
/**
|
|
193
|
+
* Link a subject to an account, or unlink it when `accountId` is null.
|
|
194
|
+
*
|
|
195
|
+
* One callback for both directions rather than two, because every host that has
|
|
196
|
+
* built this has backed both with the same endpoint (a nullable id in the body),
|
|
197
|
+
* and splitting it here would push a fake distinction onto them.
|
|
198
|
+
*/
|
|
199
|
+
type LinkSubjectAccount = (subject: AccessSubject, accountId: string | null) => Promise<void>;
|
|
200
|
+
/**
|
|
201
|
+
* Turn a rejected write into a message for the user.
|
|
202
|
+
*
|
|
203
|
+
* The host supplies this because the interesting failures are the ones only its
|
|
204
|
+
* server can explain — "that account already belongs to someone else on this
|
|
205
|
+
* event", "that account is disabled". A shared component that swallowed those
|
|
206
|
+
* into "something went wrong" would delete the only actionable half.
|
|
207
|
+
*
|
|
208
|
+
* Returns an ALREADY-LOCALIZED string.
|
|
209
|
+
*/
|
|
210
|
+
type DescribeError = (error: Error) => string;
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* The capability-link panel for ONE subject.
|
|
214
|
+
*
|
|
215
|
+
* ── SCOPED TO ONE SUBJECT ON PURPOSE ──────────────────────────────────────────
|
|
216
|
+
* These links are CAPABILITIES: whoever holds the URL has the access, with no
|
|
217
|
+
* sign-in in the way. A screen listing every subject's live link would itself be
|
|
218
|
+
* a high-value secret. So this panel shows one subject's links, and only while
|
|
219
|
+
* the user is looking at that subject.
|
|
220
|
+
*
|
|
221
|
+
* The primary action mints AND copies in a single press, because the real
|
|
222
|
+
* workflow is pasting the link into a chat app. Email is a secondary COMPOSE
|
|
223
|
+
* action (see `buildCapabilityLinkMailto`). Revoking any of this subject's live
|
|
224
|
+
* links is reachable here, so a leaked link can be killed without disturbing
|
|
225
|
+
* anyone else's.
|
|
226
|
+
*
|
|
227
|
+
* ── WHAT THE HOST OWNS ────────────────────────────────────────────────────────
|
|
228
|
+
* The mint call, the error wording, and the per-link revoke control (via
|
|
229
|
+
* `renderLinkActions`) — revocation usually needs a confirm step wired to the
|
|
230
|
+
* host's own dialog and mutation, so the package renders the row and lets the
|
|
231
|
+
* host fill the action slot rather than shipping half a revoke flow.
|
|
232
|
+
*/
|
|
233
|
+
|
|
234
|
+
interface AccessLinkPanelProps {
|
|
235
|
+
/** The subject whose links are being managed. */
|
|
236
|
+
readonly subject: AccessSubject;
|
|
237
|
+
/** How long a link minted now should live. */
|
|
238
|
+
readonly policy: ExpiryPolicy;
|
|
239
|
+
/**
|
|
240
|
+
* Every capability link the host knows about, normalized. Filtered to this
|
|
241
|
+
* subject here — the host does not need to pre-filter.
|
|
242
|
+
*/
|
|
243
|
+
readonly links: readonly CapabilityLink[];
|
|
244
|
+
/**
|
|
245
|
+
* True when the link list FAILED to load.
|
|
246
|
+
*
|
|
247
|
+
* Distinguished from "loaded, and there are none" deliberately: an empty list
|
|
248
|
+
* caused by a failed fetch reads as "this subject has no live link", which
|
|
249
|
+
* invites minting a SECOND live capability for someone who already has one —
|
|
250
|
+
* and leaves the user believing there is nothing to revoke.
|
|
251
|
+
*/
|
|
252
|
+
readonly linksFailed?: boolean;
|
|
253
|
+
readonly mint: MintCapabilityLink;
|
|
254
|
+
readonly describeError: DescribeError;
|
|
255
|
+
readonly onClose: () => void;
|
|
256
|
+
readonly onResult: AccessResultReporter;
|
|
257
|
+
/** Renders the host's revoke control for one live link. */
|
|
258
|
+
readonly renderLinkActions?: (link: CapabilityLink) => React.ReactNode;
|
|
259
|
+
/** Injectable clock, for tests. */
|
|
260
|
+
readonly now?: () => Date;
|
|
261
|
+
/** Optional platform clipboard writer. */
|
|
262
|
+
readonly clipboardWriter?: (text: string) => void | Promise<void>;
|
|
263
|
+
/** Root testID; children derive from it. Defaults to `access-link-panel`. */
|
|
264
|
+
readonly testIDPrefix?: string;
|
|
265
|
+
}
|
|
266
|
+
declare const AccessLinkPanel: ({ subject, policy, links, linksFailed, mint, describeError, onClose, onResult, renderLinkActions, now, clipboardWriter, testIDPrefix, }: AccessLinkPanelProps) => React.ReactElement;
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* The account picker for ONE subject.
|
|
270
|
+
*
|
|
271
|
+
* Binds a subject to the account it signs in with, or removes that binding.
|
|
272
|
+
* Unlinking sits behind its own press rather than being a value in the picker,
|
|
273
|
+
* because a mis-tap in a list must not be able to detach someone from their work.
|
|
274
|
+
*
|
|
275
|
+
* The picker is the shared `SelectControl` from `@dloizides/ui-forms` rather than
|
|
276
|
+
* a local one — the fleet already grew six independent select implementations
|
|
277
|
+
* before that was promoted, and this is not going to be the seventh.
|
|
278
|
+
*/
|
|
279
|
+
|
|
280
|
+
interface AccountLinkPanelProps {
|
|
281
|
+
readonly subject: AccessSubject;
|
|
282
|
+
/** The account currently bound to this subject, or null. */
|
|
283
|
+
readonly boundAccountId: string | null;
|
|
284
|
+
/** The directory of accounts that could be bound. */
|
|
285
|
+
readonly accounts: readonly LinkableAccount[];
|
|
286
|
+
/**
|
|
287
|
+
* Which account each subject currently holds, keyed by subject id.
|
|
288
|
+
*
|
|
289
|
+
* The whole map, not just this subject's: accounts already taken by ANOTHER
|
|
290
|
+
* subject are hidden from the picker, because offering them can only produce a
|
|
291
|
+
* uniqueness error the user cannot act on.
|
|
292
|
+
*/
|
|
293
|
+
readonly bindings: ReadonlyMap<string, string | null>;
|
|
294
|
+
readonly linkAccount: LinkSubjectAccount;
|
|
295
|
+
readonly describeError: DescribeError;
|
|
296
|
+
readonly onClose: () => void;
|
|
297
|
+
readonly onResult: AccessResultReporter;
|
|
298
|
+
/** Root testID; children derive from it. Defaults to `account-link-panel`. */
|
|
299
|
+
readonly testIDPrefix?: string;
|
|
300
|
+
}
|
|
301
|
+
declare const AccountLinkPanel: ({ subject, boundAccountId, accounts, bindings, linkAccount, describeError, onClose, onResult, testIDPrefix, }: AccountLinkPanelProps) => React.ReactElement;
|
|
302
|
+
|
|
303
|
+
/** Translate function — mirrors the fleet's `FM(key, p1?, p2?, p3?)` shape. */
|
|
304
|
+
type AccessTranslate = (key: string, p1?: string, p2?: string, p3?: string) => string;
|
|
305
|
+
interface UseCapabilityLinkMintOptions {
|
|
306
|
+
readonly subject: AccessSubject;
|
|
307
|
+
readonly policy: ExpiryPolicy;
|
|
308
|
+
readonly mint: MintCapabilityLink;
|
|
309
|
+
readonly describeError: DescribeError;
|
|
310
|
+
readonly onResult: AccessResultReporter;
|
|
311
|
+
readonly t: AccessTranslate;
|
|
312
|
+
/** Injectable clock, so the expiry maths is testable. Defaults to the real one. */
|
|
313
|
+
readonly now?: () => Date;
|
|
314
|
+
/** Optional platform clipboard writer (React Native's `Clipboard.setString`, say). */
|
|
315
|
+
readonly clipboardWriter?: (text: string) => void | Promise<void>;
|
|
316
|
+
}
|
|
317
|
+
interface CapabilityLinkMintState {
|
|
318
|
+
/** The just-minted link, revealed once, or null before any mint. */
|
|
319
|
+
readonly minted: CapabilityLink | null;
|
|
320
|
+
/** True while a mint is in flight. */
|
|
321
|
+
readonly isPending: boolean;
|
|
322
|
+
/** True when the last mint's URL actually reached the clipboard. */
|
|
323
|
+
readonly copied: boolean;
|
|
324
|
+
/** Mint a fresh link for this subject and copy it. */
|
|
325
|
+
readonly mint: () => void;
|
|
326
|
+
/** Re-copy the revealed URL, after a refused write. */
|
|
327
|
+
readonly recopy: () => void;
|
|
328
|
+
/** Forget the revealed URL — it cannot be read again afterwards. */
|
|
329
|
+
readonly dismiss: () => void;
|
|
330
|
+
}
|
|
331
|
+
declare function useCapabilityLinkMint(options: UseCapabilityLinkMintOptions): CapabilityLinkMintState;
|
|
332
|
+
|
|
333
|
+
interface UseSubjectAccountLinkOptions {
|
|
334
|
+
readonly subject: AccessSubject;
|
|
335
|
+
/** The account currently bound to this subject, or null when it has none. */
|
|
336
|
+
readonly boundAccountId: string | null;
|
|
337
|
+
readonly linkAccount: LinkSubjectAccount;
|
|
338
|
+
readonly describeError: DescribeError;
|
|
339
|
+
readonly onResult: AccessResultReporter;
|
|
340
|
+
readonly t: AccessTranslate;
|
|
341
|
+
}
|
|
342
|
+
interface SubjectAccountLinkState {
|
|
343
|
+
/** The account chosen in the picker, or null when none is. */
|
|
344
|
+
readonly selectedAccountId: string | null;
|
|
345
|
+
readonly select: (accountId: string) => void;
|
|
346
|
+
readonly isPending: boolean;
|
|
347
|
+
/** True when the chosen account differs from the one already bound. */
|
|
348
|
+
readonly canLink: boolean;
|
|
349
|
+
/** True when there is a binding to remove. */
|
|
350
|
+
readonly canUnlink: boolean;
|
|
351
|
+
readonly link: () => void;
|
|
352
|
+
readonly unlink: () => void;
|
|
353
|
+
}
|
|
354
|
+
declare function useSubjectAccountLink(options: UseSubjectAccountLinkOptions): SubjectAccountLinkState;
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Pure rules for presenting a subject's capability links.
|
|
358
|
+
*
|
|
359
|
+
* Logic-only (no JSX) so the parts that are quietly easy to get wrong — which
|
|
360
|
+
* links belong to whom, which are still worth revoking, and what goes into a
|
|
361
|
+
* mail compose window — are unit-tested without a renderer.
|
|
362
|
+
*/
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* A subject's links, newest first.
|
|
366
|
+
*
|
|
367
|
+
* Matching is on `subjectId` and nothing else. The messy part — working out
|
|
368
|
+
* WHICH subject an API's link row belongs to when the wire format does not say —
|
|
369
|
+
* is the host's job, done once when it normalizes its DTOs. That keeps a
|
|
370
|
+
* product's data-model compromise out of the shared filter.
|
|
371
|
+
*/
|
|
372
|
+
declare function linksForSubject(links: readonly CapabilityLink[], subjectId: string): CapabilityLink[];
|
|
373
|
+
/** The still-usable links belonging to a subject — the ones worth revoking. */
|
|
374
|
+
declare function activeLinksForSubject(links: readonly CapabilityLink[], subjectId: string): CapabilityLink[];
|
|
375
|
+
/** An ISO timestamp as a bare `yyyy-mm-dd`, or the supplied fallback when absent. */
|
|
376
|
+
declare function formatLinkDate(value: string | null | undefined, fallback: string): string;
|
|
377
|
+
/**
|
|
378
|
+
* A `mailto:` compose URL carrying the link, or null when there is nothing to send.
|
|
379
|
+
*
|
|
380
|
+
* NO recipient address is filled in, and that is deliberate rather than an
|
|
381
|
+
* omission: an `AccessSubject` has no email field (see `types.ts` for why it
|
|
382
|
+
* stays that small), so the sender picks the address in their own mail client.
|
|
383
|
+
* It is also why the panel calls this "open in email" and warns that nothing is
|
|
384
|
+
* sent on the user's behalf — a compose window they abandon looks identical to
|
|
385
|
+
* one they send.
|
|
386
|
+
*/
|
|
387
|
+
declare function buildCapabilityLinkMailto(subject: string, body: string, url: string): string | null;
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Pure rules for the account picker.
|
|
391
|
+
*
|
|
392
|
+
* Logic-only so the two decisions that actually matter — what an account is
|
|
393
|
+
* CALLED when the directory gives you almost nothing, and which accounts must
|
|
394
|
+
* not be offered at all — are tested without a renderer.
|
|
395
|
+
*/
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* What to call an account: its display name, else its email, else the caller's
|
|
399
|
+
* already-localized fallback.
|
|
400
|
+
*
|
|
401
|
+
* Directories genuinely return entries with neither — a freshly invited user who
|
|
402
|
+
* has not completed a profile is the common one. Rendering those as a blank row
|
|
403
|
+
* gives an unpickable option that looks like a rendering fault.
|
|
404
|
+
*/
|
|
405
|
+
declare function accountDisplayLabel(account: LinkableAccount, unnamedLabel: string): string;
|
|
406
|
+
/**
|
|
407
|
+
* Build the picker's options for one subject.
|
|
408
|
+
*
|
|
409
|
+
* Accounts already bound to a DIFFERENT subject are dropped. That is not tidying:
|
|
410
|
+
* every host that has built this backs it with a uniqueness constraint and
|
|
411
|
+
* answers a duplicate binding with a 409, so offering such an account can only
|
|
412
|
+
* ever produce an error the user cannot act on.
|
|
413
|
+
*
|
|
414
|
+
* The subject's OWN currently-bound account is KEPT, so the picker can show it as
|
|
415
|
+
* the current selection instead of looking empty on a subject that is in fact
|
|
416
|
+
* linked — which would read as "not linked" and invite a pointless re-link.
|
|
417
|
+
*
|
|
418
|
+
* Directory order is preserved; hosts return these sorted.
|
|
419
|
+
*/
|
|
420
|
+
declare function buildAccountOptions(accounts: readonly LinkableAccount[], bindings: ReadonlyMap<string, string | null>, subject: AccessSubject, unnamedLabel: string): ControlOption[];
|
|
421
|
+
|
|
422
|
+
interface AccessAdminPalette {
|
|
423
|
+
/** Background of the panel itself — a recessed surface inside a section. */
|
|
424
|
+
readonly panelSurface: string;
|
|
425
|
+
/** Hairline borders. */
|
|
426
|
+
readonly border: string;
|
|
427
|
+
readonly textPrimary: string;
|
|
428
|
+
readonly textSecondary: string;
|
|
429
|
+
/** The security warning, and the destructive action. */
|
|
430
|
+
readonly danger: string;
|
|
431
|
+
/** Fill and edge of the "here is your new link" box. */
|
|
432
|
+
readonly revealSurface: string;
|
|
433
|
+
readonly revealBorder: string;
|
|
434
|
+
/** Heading inside the reveal box — the strongest brand tone available. */
|
|
435
|
+
readonly revealHeading: string;
|
|
436
|
+
}
|
|
437
|
+
declare function useAccessAdminPalette(): AccessAdminPalette;
|
|
438
|
+
|
|
439
|
+
/**
|
|
440
|
+
* The package's two host contracts: the translation keys it resolves, and the
|
|
441
|
+
* testIDs it renders.
|
|
442
|
+
*/
|
|
443
|
+
/**
|
|
444
|
+
* Every translation key this package resolves through `t(...)` — the host's i18n contract.
|
|
445
|
+
*
|
|
446
|
+
* ── WHY A MACHINE-READABLE MAP AND NOT A README TABLE ─────────────────────────
|
|
447
|
+
* The kit is deliberately fallback-free: `t` is the host's own resolver (kefi passes
|
|
448
|
+
* `FM`), and a missing key renders the RAW DOTTED NAME to the user — a panel warning
|
|
449
|
+
* `accessAdmin.capabilityWarning` instead of explaining that anyone holding the URL
|
|
450
|
+
* has the access. Because every `t(...)` call site in this package lives inside
|
|
451
|
+
* `node_modules` once published, a host grepping its own source for untranslated
|
|
452
|
+
* strings cannot see them. Hand-maintained key lists rot on exactly this path: one
|
|
453
|
+
* portal in this fleet was found missing 26 kit keys, rendering raw names to users.
|
|
454
|
+
*
|
|
455
|
+
* So: bind a missing-key guard to this map, the way kefi-web's
|
|
456
|
+
* `kitTranslationKeys.ts` already binds `LAYOUT_I18N` / `TABLE_I18N`. Then a kit
|
|
457
|
+
* upgrade that adds a string FAILS the host's test suite instead of shipping raw
|
|
458
|
+
* key text. `accessAdminI18n.test.ts` asserts this map stays complete and unused-free,
|
|
459
|
+
* so the map cannot drift from the call sites either.
|
|
460
|
+
*
|
|
461
|
+
* The KEY NAMES are the package's; the WORDING is entirely the host's. That split is
|
|
462
|
+
* what lets kefi keep its warm, specific copy ("This link lets them see their own
|
|
463
|
+
* guests and what they are owed") under a key a survey tool can reuse with its own.
|
|
464
|
+
*/
|
|
465
|
+
declare const ACCESS_ADMIN_I18N: {
|
|
466
|
+
/** Panel heading. `{{p1}}` = subject display name. */
|
|
467
|
+
readonly panelHeading: "accessAdmin.link.panelHeading";
|
|
468
|
+
/** Supporting line explaining what the link grants. */
|
|
469
|
+
readonly panelNote: "accessAdmin.link.panelNote";
|
|
470
|
+
/** The security warning. Shown always, not only after a mint — see AccessLinkPanel. */
|
|
471
|
+
readonly capabilityWarning: "accessAdmin.link.capabilityWarning";
|
|
472
|
+
/** "A link created now stops working on {{p1}}." `{{p1}}` = yyyy-mm-dd. */
|
|
473
|
+
readonly expiryPreview: "accessAdmin.link.expiryPreview";
|
|
474
|
+
readonly mint: "accessAdmin.link.mint";
|
|
475
|
+
readonly mintHint: "accessAdmin.link.mintHint";
|
|
476
|
+
/** Heading over the revealed URL. `{{p1}}` = subject display name. */
|
|
477
|
+
readonly mintedTitle: "accessAdmin.link.mintedTitle";
|
|
478
|
+
/** "Shown once — copy it now." The honesty that makes the one-shot reveal safe. */
|
|
479
|
+
readonly mintedHint: "accessAdmin.link.mintedHint";
|
|
480
|
+
/** Reported when the mint landed AND the clipboard write succeeded. `{{p1}}` = name. */
|
|
481
|
+
readonly copySuccess: "accessAdmin.link.copySuccess";
|
|
482
|
+
/** Reported when the mint landed but the clipboard REFUSED. `{{p1}}` = name. */
|
|
483
|
+
readonly mintNotCopied: "accessAdmin.link.mintNotCopied";
|
|
484
|
+
/** Reported when a re-copy of an already-revealed URL failed. `{{p1}}` = name. */
|
|
485
|
+
readonly copyFailed: "accessAdmin.link.copyFailed";
|
|
486
|
+
readonly recopy: "accessAdmin.link.recopy";
|
|
487
|
+
readonly recopyHint: "accessAdmin.link.recopyHint";
|
|
488
|
+
readonly copied: "accessAdmin.link.copied";
|
|
489
|
+
readonly dismiss: "accessAdmin.link.dismiss";
|
|
490
|
+
readonly dismissHint: "accessAdmin.link.dismissHint";
|
|
491
|
+
readonly email: "accessAdmin.link.email";
|
|
492
|
+
readonly emailHint: "accessAdmin.link.emailHint";
|
|
493
|
+
/** "This opens YOUR mail app — we do not send it for you." */
|
|
494
|
+
readonly emailNote: "accessAdmin.link.emailNote";
|
|
495
|
+
/** Mail subject. `{{p1}}` = subject display name. */
|
|
496
|
+
readonly emailSubject: "accessAdmin.link.emailSubject";
|
|
497
|
+
/** Mail body. `{{p1}}` = name, `{{p2}}` = url, `{{p3}}` = expiry date. */
|
|
498
|
+
readonly emailBody: "accessAdmin.link.emailBody";
|
|
499
|
+
/** "Live links ({{p1}})". `{{p1}}` = count. */
|
|
500
|
+
readonly liveHeading: "accessAdmin.link.liveHeading";
|
|
501
|
+
/** "Created {{p1}} · stops working {{p2}}". */
|
|
502
|
+
readonly liveMeta: "accessAdmin.link.liveMeta";
|
|
503
|
+
/** Shown in `liveMeta`'s `{{p2}}` slot when a link has no expiry at all. */
|
|
504
|
+
readonly expiresNever: "accessAdmin.link.expiresNever";
|
|
505
|
+
readonly noLive: "accessAdmin.link.noLive";
|
|
506
|
+
/** Shown when the link list FAILED to load — never conflated with "there are none". */
|
|
507
|
+
readonly liveLoadError: "accessAdmin.link.liveLoadError";
|
|
508
|
+
readonly close: "accessAdmin.link.close";
|
|
509
|
+
readonly closeHint: "accessAdmin.link.closeHint";
|
|
510
|
+
/** Panel heading. `{{p1}}` = subject display name. */
|
|
511
|
+
readonly accountPanelHeading: "accessAdmin.account.panelHeading";
|
|
512
|
+
readonly accountPanelNote: "accessAdmin.account.panelNote";
|
|
513
|
+
readonly accountPickerLabel: "accessAdmin.account.pickerLabel";
|
|
514
|
+
readonly accountPickerHint: "accessAdmin.account.pickerHint";
|
|
515
|
+
readonly accountPickerPlaceholder: "accessAdmin.account.pickerPlaceholder";
|
|
516
|
+
readonly accountOptionHint: "accessAdmin.account.optionHint";
|
|
517
|
+
/** Label for a directory entry carrying neither a display name nor an email. */
|
|
518
|
+
readonly accountUnnamed: "accessAdmin.account.unnamed";
|
|
519
|
+
/** Shown when the directory is empty (or every entry is taken by another subject). */
|
|
520
|
+
readonly accountNone: "accessAdmin.account.none";
|
|
521
|
+
readonly accountLink: "accessAdmin.account.link";
|
|
522
|
+
readonly accountLinkHint: "accessAdmin.account.linkHint";
|
|
523
|
+
/** `{{p1}}` = subject display name. */
|
|
524
|
+
readonly accountLinkSuccess: "accessAdmin.account.linkSuccess";
|
|
525
|
+
readonly accountUnlink: "accessAdmin.account.unlink";
|
|
526
|
+
readonly accountUnlinkHint: "accessAdmin.account.unlinkHint";
|
|
527
|
+
/** `{{p1}}` = subject display name. */
|
|
528
|
+
readonly accountUnlinkSuccess: "accessAdmin.account.unlinkSuccess";
|
|
529
|
+
readonly accountClose: "accessAdmin.account.close";
|
|
530
|
+
readonly accountCloseHint: "accessAdmin.account.closeHint";
|
|
531
|
+
};
|
|
532
|
+
/** The shape of {@link ACCESS_ADMIN_I18N} — useful for typing an override map. */
|
|
533
|
+
type AccessAdminI18n = typeof ACCESS_ADMIN_I18N;
|
|
534
|
+
/**
|
|
535
|
+
* Default testIDs. Every one is overridable via each panel's `testIDPrefix`, because
|
|
536
|
+
* Playwright selectors in an adopting app already exist and a shared component is not
|
|
537
|
+
* a good enough reason to rewrite someone's E2E suite.
|
|
538
|
+
*/
|
|
539
|
+
declare const ACCESS_ADMIN_TEST_IDS: {
|
|
540
|
+
readonly linkPanel: "access-link-panel";
|
|
541
|
+
readonly accountPanel: "account-link-panel";
|
|
542
|
+
};
|
|
543
|
+
|
|
544
|
+
export { ACCESS_ADMIN_I18N, ACCESS_ADMIN_TEST_IDS, type AccessAdminI18n, type AccessAdminPalette, AccessLinkPanel, type AccessLinkPanelProps, type AccessResultReporter, AccessResultTone, type AccessSubject, type AccessTranslate, AccountLinkPanel, type AccountLinkPanelProps, type CapabilityLink, type CapabilityLinkMintState, type DescribeError, type ExpiryPolicy, type LinkSubjectAccount, type LinkableAccount, type MintCapabilityLink, type MintCapabilityLinkInput, type SubjectAccountLinkState, type UseCapabilityLinkMintOptions, type UseSubjectAccountLinkOptions, accountDisplayLabel, activeLinksForSubject, buildAccountOptions, buildCapabilityLinkMailto, capabilityExpiryDate, capabilityExpiryDays, daysUntilAnchor, formatLinkDate, linksForSubject, useAccessAdminPalette, useCapabilityLinkMint, useSubjectAccountLink };
|