@pyric/ui 0.1.0-alpha.10 → 0.1.0-alpha.12
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 +5 -4
- package/src/agents/ContextWindowUsage.tsx +514 -0
- package/src/agents/EmptyState.tsx +27 -0
- package/src/agents/Fold.tsx +64 -0
- package/src/agents/Modal.tsx +65 -0
- package/src/agents/PulsingDot.tsx +28 -0
- package/src/agents/inbrowser-agent-usage.d.ts +141 -0
- package/src/agents/index.ts +28 -0
- package/src/auth/authApi.ts +72 -0
- package/src/auth/claims.ts +63 -0
- package/src/auth/components/AuthProviderToggles.tsx +147 -0
- package/src/auth/components/AuthSignInHelper.tsx +197 -0
- package/src/auth/components/AuthUserForm.tsx +328 -0
- package/src/auth/components/AuthUserList.tsx +219 -0
- package/src/auth/components/ClaimsField.tsx +50 -0
- package/src/auth/components/confirmActions.tsx +114 -0
- package/src/auth/controller.ts +173 -0
- package/src/auth/hooks/index.ts +36 -0
- package/src/auth/hooks/useAuthFlowHelper.ts +55 -0
- package/src/auth/hooks/useAuthProviderConfig.ts +139 -0
- package/src/auth/hooks/useAuthUserEditor.ts +76 -0
- package/src/auth/hooks/useAuthUsers.ts +154 -0
- package/src/auth/index.ts +42 -0
- package/src/auth/providers.ts +28 -0
- package/src/auth/reducers/userEditor.ts +186 -0
- package/src/events/components/ActivityActionItems.tsx +136 -0
- package/src/events/components/ActivityGrid.tsx +197 -0
- package/src/events/components/ActivityGridRow.tsx +65 -0
- package/src/events/components/ProposedChangeDiff.tsx +175 -0
- package/src/events/components/format.ts +21 -0
- package/src/events/components/index.ts +17 -0
- package/src/events/digest.ts +630 -0
- package/src/events/hooks/index.ts +9 -0
- package/src/events/hooks/useActivityDigest.ts +42 -0
- package/src/events/hooks/useActivityStream.ts +86 -0
- package/src/events/index.ts +39 -0
- package/src/events/types.ts +152 -0
- package/src/firestore/components/CollectionList.tsx +78 -0
- package/src/firestore/components/DeleteWithConfirm.tsx +88 -0
- package/src/firestore/components/DocumentEditor.tsx +350 -0
- package/src/firestore/components/DocumentList.tsx +217 -0
- package/src/firestore/components/DocumentPreview.tsx +265 -0
- package/src/firestore/components/FieldRenderer.tsx +25 -0
- package/src/firestore/components/QueryBuilder.tsx +181 -0
- package/src/firestore/components/ReferencePicker.tsx +212 -0
- package/src/firestore/components/TreeEntry.tsx +58 -0
- package/src/firestore/components/context.ts +25 -0
- package/src/firestore/fieldEditors/array.tsx +30 -0
- package/src/firestore/fieldEditors/boolean.tsx +39 -0
- package/src/firestore/fieldEditors/bytes.tsx +53 -0
- package/src/firestore/fieldEditors/geopoint.tsx +71 -0
- package/src/firestore/fieldEditors/map.tsx +33 -0
- package/src/firestore/fieldEditors/null.tsx +27 -0
- package/src/firestore/fieldEditors/number.tsx +43 -0
- package/src/firestore/fieldEditors/reference.tsx +87 -0
- package/src/firestore/fieldEditors/registry.ts +45 -0
- package/src/firestore/fieldEditors/string.tsx +33 -0
- package/src/firestore/fieldEditors/timestamp.tsx +75 -0
- package/src/firestore/fieldEditors/types.ts +68 -0
- package/src/firestore/fieldEditors/vector.tsx +142 -0
- package/src/firestore/firestoreApi.ts +86 -0
- package/src/firestore/hooks/coerceError.ts +34 -0
- package/src/firestore/hooks/index.ts +46 -0
- package/src/firestore/hooks/useCollectionList.ts +102 -0
- package/src/firestore/hooks/useDocumentEditor.ts +161 -0
- package/src/firestore/hooks/useDocumentList.ts +242 -0
- package/src/firestore/hooks/useDocumentSubcollections.ts +86 -0
- package/src/firestore/hooks/useFirestoreCollection.ts +51 -0
- package/src/firestore/hooks/useFirestoreDoc.ts +55 -0
- package/src/firestore/hooks/useQueryBuilder.ts +188 -0
- package/src/firestore/hooks/useRecursiveDelete.ts +77 -0
- package/src/firestore/hooks/useReferencePicker.ts +228 -0
- package/src/firestore/import/parseImport.ts +137 -0
- package/src/firestore/index.ts +87 -0
- package/src/firestore/reducers/defaults.ts +38 -0
- package/src/firestore/reducers/documentEditor.ts +239 -0
- package/src/firestore/reducers/tree.ts +140 -0
- package/src/firestore/reducers/types.ts +92 -0
- package/src/firestore/reducers/validation.ts +129 -0
- package/src/firestore/types.ts +231 -0
- package/src/firestore/validation/ids.ts +45 -0
- package/src/firestore/valueEquality.ts +43 -0
- package/src/index.ts +10 -0
- package/src/primitives/Badge.tsx +40 -0
- package/src/primitives/ConfirmDialog.tsx +137 -0
- package/src/primitives/CopyButton.tsx +62 -0
- package/src/primitives/JsonView.tsx +151 -0
- package/src/primitives/SegmentedControl.tsx +72 -0
- package/src/primitives/Toast.tsx +161 -0
- package/src/primitives/VirtualList.tsx +104 -0
- package/src/primitives/hooks/useContainerSize.ts +53 -0
- package/src/primitives/hooks/useUpdateHighlights.ts +109 -0
- package/src/primitives/index.ts +39 -0
- package/src/primitives/useConfirm.tsx +113 -0
- package/src/rtdb/components/RtdbPathBar.tsx +135 -0
- package/src/rtdb/components/RtdbTree.tsx +409 -0
- package/src/rtdb/editor.ts +79 -0
- package/src/rtdb/hooks/useRtdbTree.ts +137 -0
- package/src/rtdb/index.ts +66 -0
- package/src/rtdb/pathInput.ts +47 -0
- package/src/rtdb/reducers/tree.ts +191 -0
- package/src/rtdb/rtdbApi.ts +23 -0
- package/src/rtdb/values.ts +188 -0
- package/src/rules/components/DenialInspector.tsx +227 -0
- package/src/rules/components/format.ts +171 -0
- package/src/rules/components/index.ts +12 -0
- package/src/rules/components/scope.ts +75 -0
- package/src/rules/hooks/index.ts +5 -0
- package/src/rules/hooks/useDenialTrace.ts +100 -0
- package/src/rules/index.ts +24 -0
- package/src/rules/types.ts +91 -0
- package/src/storage/collisionRename.ts +114 -0
- package/src/storage/components/DeleteSelectionWithConfirm.tsx +193 -0
- package/src/storage/components/ObjectBrowser.tsx +219 -0
- package/src/storage/components/ObjectInspector.tsx +169 -0
- package/src/storage/components/PathBreadcrumb.tsx +84 -0
- package/src/storage/components/UploadDropzone.tsx +182 -0
- package/src/storage/folderPlaceholder.ts +40 -0
- package/src/storage/hooks/index.ts +59 -0
- package/src/storage/hooks/useMetadataEditor.ts +329 -0
- package/src/storage/hooks/useObjectUpload.ts +262 -0
- package/src/storage/hooks/usePathState.ts +94 -0
- package/src/storage/hooks/useStorageDelete.ts +195 -0
- package/src/storage/hooks/useStorageList.ts +261 -0
- package/src/storage/hooks/useStorageObject.ts +162 -0
- package/src/storage/hooks/useStorageRulesGate.ts +270 -0
- package/src/storage/hooks/useStorageSelection.ts +90 -0
- package/src/storage/index.ts +59 -0
- package/src/storage/pendingPrefixes.ts +125 -0
- package/src/storage/previews.tsx +120 -0
- package/src/storage/storageApi.ts +54 -0
- package/src/traffic/components/RuleHeatmap.tsx +97 -0
- package/src/traffic/components/TrafficDetail.tsx +160 -0
- package/src/traffic/components/TrafficGroupRow.tsx +91 -0
- package/src/traffic/components/TrafficLineChart.tsx +139 -0
- package/src/traffic/components/TrafficLog.tsx +175 -0
- package/src/traffic/components/TrafficMetricCards.tsx +77 -0
- package/src/traffic/components/TrafficRow.tsx +69 -0
- package/src/traffic/components/TrafficStats.tsx +73 -0
- package/src/traffic/components/TrafficTimeline.tsx +289 -0
- package/src/traffic/components/format.ts +22 -0
- package/src/traffic/components/index.ts +22 -0
- package/src/traffic/hooks/index.ts +60 -0
- package/src/traffic/hooks/useRuleHeatmap.ts +92 -0
- package/src/traffic/hooks/useTrafficBuckets.ts +146 -0
- package/src/traffic/hooks/useTrafficFilter.ts +74 -0
- package/src/traffic/hooks/useTrafficGroups.ts +126 -0
- package/src/traffic/hooks/useTrafficMetrics.ts +250 -0
- package/src/traffic/hooks/useTrafficMonitor.ts +111 -0
- package/src/traffic/hooks/useTrafficStats.ts +77 -0
- package/src/traffic/index.ts +13 -0
- package/src/traffic/types.ts +85 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export interface PulsingDotProps {
|
|
2
|
+
/** Forwarded to the outer wrapper. Consumers attach all visual
|
|
3
|
+
* styling (size, animation, color) via this hook or by selecting
|
|
4
|
+
* on `[data-pyric-ui="pulsing-dot"]`. */
|
|
5
|
+
className?: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Headless "live / streaming" indicator. Ships zero visual styling.
|
|
10
|
+
*
|
|
11
|
+
* Renders three nested spans so consumers can style ring + core
|
|
12
|
+
* independently:
|
|
13
|
+
*
|
|
14
|
+
* <span data-pyric-ui="pulsing-dot">
|
|
15
|
+
* <span data-pyric-pulse-ring />
|
|
16
|
+
* <span data-pyric-pulse-core />
|
|
17
|
+
* </span>
|
|
18
|
+
*
|
|
19
|
+
* Animation, color, and size are entirely consumer-defined.
|
|
20
|
+
*/
|
|
21
|
+
export function PulsingDot({ className }: PulsingDotProps) {
|
|
22
|
+
return (
|
|
23
|
+
<span data-pyric-ui="pulsing-dot" className={className}>
|
|
24
|
+
<span data-pyric-pulse-ring aria-hidden="true" />
|
|
25
|
+
<span data-pyric-pulse-core aria-hidden="true" />
|
|
26
|
+
</span>
|
|
27
|
+
);
|
|
28
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
declare module '@inbrowser/agent/usage' {
|
|
2
|
+
export type ContextWindowStatus = 'unknown' | 'low' | 'medium' | 'high' | 'critical';
|
|
3
|
+
|
|
4
|
+
export interface ContextWindowBreakdownRow {
|
|
5
|
+
id: string;
|
|
6
|
+
label: string;
|
|
7
|
+
tokens: number;
|
|
8
|
+
color: string;
|
|
9
|
+
estimated: boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface ContextWindowCompactionStats {
|
|
13
|
+
compacted: boolean;
|
|
14
|
+
originalChars: number;
|
|
15
|
+
compactedChars: number;
|
|
16
|
+
bytesSaved: number;
|
|
17
|
+
turnsCompacted: number;
|
|
18
|
+
messagesCompacted: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface ContextWindowPricingEstimate {
|
|
22
|
+
totalUsd: number | null;
|
|
23
|
+
promptUsd: number | null;
|
|
24
|
+
cachedPromptUsd?: number | null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface ContextWindowPricing {
|
|
28
|
+
current: ContextWindowPricingEstimate | null;
|
|
29
|
+
compacted: ContextWindowPricingEstimate | null;
|
|
30
|
+
savedCostUsd: number | null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface ContextWindowCompactionPreview {
|
|
34
|
+
rawTokens: number;
|
|
35
|
+
currentTokens: number;
|
|
36
|
+
compactedTokens: number;
|
|
37
|
+
automaticSavedTokens: number;
|
|
38
|
+
manualSavedTokens: number;
|
|
39
|
+
savedTokens: number;
|
|
40
|
+
stats: ContextWindowCompactionStats;
|
|
41
|
+
retains: readonly string[];
|
|
42
|
+
loses: readonly string[];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface RequestInputComposition {
|
|
46
|
+
system: number;
|
|
47
|
+
history: number;
|
|
48
|
+
resentToolResults: number;
|
|
49
|
+
currentPrompt: number;
|
|
50
|
+
toolSchemas: number;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface SessionToolRef {
|
|
54
|
+
name: string;
|
|
55
|
+
callId: string;
|
|
56
|
+
messageId?: string;
|
|
57
|
+
tokens?: number;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface SessionRequestUsage {
|
|
61
|
+
id: string;
|
|
62
|
+
requestId: string;
|
|
63
|
+
turnId: string;
|
|
64
|
+
iteration: number;
|
|
65
|
+
ts?: number;
|
|
66
|
+
providerId?: string;
|
|
67
|
+
providerLabel?: string;
|
|
68
|
+
modelLabel?: string;
|
|
69
|
+
strategy?: string;
|
|
70
|
+
strategySource?: string;
|
|
71
|
+
inputTokens: number;
|
|
72
|
+
outputTokens: number;
|
|
73
|
+
cachedInputTokens: number;
|
|
74
|
+
reasoningTokens: number;
|
|
75
|
+
freshInputTokens: number;
|
|
76
|
+
visibleOutputTokens: number;
|
|
77
|
+
tokensTotal: number;
|
|
78
|
+
costUsd?: number;
|
|
79
|
+
usageSource: 'provider' | 'estimate';
|
|
80
|
+
composition: RequestInputComposition;
|
|
81
|
+
messageCount: number;
|
|
82
|
+
toolResultMessageCount: number;
|
|
83
|
+
toolNames: readonly string[];
|
|
84
|
+
toolSchemaNames: readonly string[];
|
|
85
|
+
emittedToolCalls: readonly SessionToolRef[];
|
|
86
|
+
resentToolResults: readonly SessionToolRef[];
|
|
87
|
+
cacheInsight?: string;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export interface SessionTurnUsage {
|
|
91
|
+
turnId: string;
|
|
92
|
+
inputTokens: number;
|
|
93
|
+
outputTokens: number;
|
|
94
|
+
cachedInputTokens: number;
|
|
95
|
+
reasoningTokens: number;
|
|
96
|
+
tokensTotal: number;
|
|
97
|
+
requests: number;
|
|
98
|
+
costUsd?: number;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export interface SessionUsageCategoryDetail {
|
|
102
|
+
label: string;
|
|
103
|
+
tokens: number;
|
|
104
|
+
share?: number;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export interface SessionTokenUsage {
|
|
108
|
+
turns: number | null;
|
|
109
|
+
requests: number | null;
|
|
110
|
+
tokensTotal: number;
|
|
111
|
+
inputTokens: number;
|
|
112
|
+
outputTokens: number;
|
|
113
|
+
cachedInputTokens: number;
|
|
114
|
+
reasoningTokens: number;
|
|
115
|
+
costUsdTotal?: number;
|
|
116
|
+
workMultiplier?: number;
|
|
117
|
+
averageRequestTokens?: number;
|
|
118
|
+
turnRows: readonly SessionTurnUsage[];
|
|
119
|
+
requestRows: readonly SessionRequestUsage[];
|
|
120
|
+
categoryDetails: Record<string, SessionUsageCategoryDetail>;
|
|
121
|
+
teachingNotes?: {
|
|
122
|
+
providerUsage: string;
|
|
123
|
+
estimatedComposition: string;
|
|
124
|
+
contextVsSpend: string;
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export interface ContextWindowSnapshot {
|
|
129
|
+
basis: 'estimated-next-send';
|
|
130
|
+
usedTokens: number;
|
|
131
|
+
limitTokens: number | null;
|
|
132
|
+
percentFull?: number;
|
|
133
|
+
status: ContextWindowStatus;
|
|
134
|
+
breakdown: readonly ContextWindowBreakdownRow[];
|
|
135
|
+
compaction: ContextWindowCompactionStats;
|
|
136
|
+
compactionPreview: ContextWindowCompactionPreview;
|
|
137
|
+
pricing: ContextWindowPricing;
|
|
138
|
+
toolCount: number;
|
|
139
|
+
sessionUsage?: SessionTokenUsage;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@pyric/ui/agents` — headless structural components for agentic UIs
|
|
3
|
+
* (chat surfaces, tool-call drill-ins, streaming state indicators).
|
|
4
|
+
*
|
|
5
|
+
* Matches the rest of `@pyric/ui`: zero shipped CSS. Components emit
|
|
6
|
+
* `data-pyric-*` attributes and accept `className` slots. Consumers
|
|
7
|
+
* bring their own design system (Tailwind, CSS modules, plain CSS)
|
|
8
|
+
* and attach styling via attribute selectors or class names.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export { Fold, type FoldProps, type FoldTone } from './Fold.js';
|
|
12
|
+
export { Modal, type ModalProps } from './Modal.js';
|
|
13
|
+
export { EmptyState, type EmptyStateProps } from './EmptyState.js';
|
|
14
|
+
export { PulsingDot, type PulsingDotProps } from './PulsingDot.js';
|
|
15
|
+
export {
|
|
16
|
+
ContextWindowMeter,
|
|
17
|
+
ContextWindowPanel,
|
|
18
|
+
ContextWindowRing,
|
|
19
|
+
RequestUsageTimeline,
|
|
20
|
+
SessionSpendSummary,
|
|
21
|
+
TokenUsageInline,
|
|
22
|
+
type ContextWindowMeterProps,
|
|
23
|
+
type ContextWindowPanelProps,
|
|
24
|
+
type ContextWindowRingProps,
|
|
25
|
+
type RequestUsageTimelineProps,
|
|
26
|
+
type SessionSpendSummaryProps,
|
|
27
|
+
type TokenUsageInlineProps,
|
|
28
|
+
} from './ContextWindowUsage.js';
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { createContext, createElement, useContext, type ReactNode } from 'react';
|
|
2
|
+
import { sandbox as authSandbox } from 'pyric/auth';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The sandbox auth admin ops `useAuthUsers` drives, as an INJECTABLE bundle.
|
|
6
|
+
*
|
|
7
|
+
* WHY (same rationale as `@pyric/ui/firestore`'s FirestoreApi): the hook defaults
|
|
8
|
+
* to the in-process `pyric/auth` `sandbox` ops, but Pyric Studio's served mode
|
|
9
|
+
* drives the SAME ops over a SharedWorker (a parallel client over a MessagePort).
|
|
10
|
+
* Reading them from this context lets a consumer inject the worker client's fns
|
|
11
|
+
* so the hook operates on the live worker user DB without knowing the backend.
|
|
12
|
+
*
|
|
13
|
+
* The bundle is typed to the in-process signatures; a worker bundle is adapted
|
|
14
|
+
* (cast) at the Studio boundary. NOTE the worker `listUsers` is ASYNC (an RPC)
|
|
15
|
+
* whereas the in-process one is sync, so `useAuthUsers` tolerates a promise (it
|
|
16
|
+
* wraps the result in `Promise.resolve`).
|
|
17
|
+
*
|
|
18
|
+
* Default = the real `pyric/auth` sandbox ops, so every existing consumer is
|
|
19
|
+
* unchanged: no provider needed unless swapping the backend.
|
|
20
|
+
*/
|
|
21
|
+
export type AuthApi = Pick<
|
|
22
|
+
typeof authSandbox,
|
|
23
|
+
| 'listUsers'
|
|
24
|
+
| 'subscribeUsers'
|
|
25
|
+
| 'createUser'
|
|
26
|
+
| 'updateUser'
|
|
27
|
+
| 'deleteUser'
|
|
28
|
+
| 'clearUsers'
|
|
29
|
+
// Sign-in provider config (S-AUTH "Sign-in providers" section). Same shape
|
|
30
|
+
// as the user-DB ops above: a getter/setter plus a coarse subscription that
|
|
31
|
+
// re-reads on any change. `setAuthProviderConfig` ALSO fires a
|
|
32
|
+
// `provider_config_update` sandbox event, so a worker-backed bundle can ride
|
|
33
|
+
// the SAME event feed `subscribeUsers` already rides instead of standing up
|
|
34
|
+
// a second live channel — see `worker-live.ts`.
|
|
35
|
+
| 'getAuthProviderConfig'
|
|
36
|
+
| 'setAuthProviderConfig'
|
|
37
|
+
| 'subscribeAuthProviderConfig'
|
|
38
|
+
>;
|
|
39
|
+
|
|
40
|
+
const inProcessAuthApi: AuthApi = {
|
|
41
|
+
listUsers: authSandbox.listUsers,
|
|
42
|
+
subscribeUsers: authSandbox.subscribeUsers,
|
|
43
|
+
createUser: authSandbox.createUser,
|
|
44
|
+
updateUser: authSandbox.updateUser,
|
|
45
|
+
deleteUser: authSandbox.deleteUser,
|
|
46
|
+
clearUsers: authSandbox.clearUsers,
|
|
47
|
+
getAuthProviderConfig: authSandbox.getAuthProviderConfig,
|
|
48
|
+
setAuthProviderConfig: authSandbox.setAuthProviderConfig,
|
|
49
|
+
subscribeAuthProviderConfig: authSandbox.subscribeAuthProviderConfig,
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const AuthApiContext = createContext<AuthApi>(inProcessAuthApi);
|
|
53
|
+
|
|
54
|
+
/** Read the active auth API bundle (defaults to in-process `pyric/auth`). */
|
|
55
|
+
export function useAuthApi(): AuthApi {
|
|
56
|
+
return useContext(AuthApiContext);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Provide an auth API bundle to the subtree. Pyric Studio supplies the
|
|
61
|
+
* in-process bundle for dev-seed review and the SharedWorker client bundle under
|
|
62
|
+
* `pyric dev --ui`.
|
|
63
|
+
*/
|
|
64
|
+
export function AuthApiProvider({
|
|
65
|
+
value,
|
|
66
|
+
children,
|
|
67
|
+
}: {
|
|
68
|
+
value: AuthApi;
|
|
69
|
+
children: ReactNode;
|
|
70
|
+
}) {
|
|
71
|
+
return createElement(AuthApiContext.Provider, { value }, children);
|
|
72
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Custom-claims validation for the add-account form — same checks and
|
|
3
|
+
* user-facing messages as the Firebase emulator UI (which reuses the
|
|
4
|
+
* Auth emulator's server validation: JSON-object shape, 1000-char cap,
|
|
5
|
+
* reserved-key deny list).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/** Reserved JWT/OIDC keys the Auth emulator rejects as custom claims.
|
|
9
|
+
* https://firebase.google.com/docs/auth/admin/create-custom-tokens */
|
|
10
|
+
export const FORBIDDEN_CUSTOM_CLAIMS: readonly string[] = [
|
|
11
|
+
'iss',
|
|
12
|
+
'aud',
|
|
13
|
+
'sub',
|
|
14
|
+
'iat',
|
|
15
|
+
'exp',
|
|
16
|
+
'nbf',
|
|
17
|
+
'jti',
|
|
18
|
+
'nonce',
|
|
19
|
+
'azp',
|
|
20
|
+
'acr',
|
|
21
|
+
'amr',
|
|
22
|
+
'cnf',
|
|
23
|
+
'auth_time',
|
|
24
|
+
'firebase',
|
|
25
|
+
'at_hash',
|
|
26
|
+
'c_hash',
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
/** Serialized-length cap, matching the emulator's `CUSTOM_ATTRIBUTES_MAX_LENGTH`. */
|
|
30
|
+
export const CUSTOM_CLAIMS_MAX_LENGTH = 1000;
|
|
31
|
+
|
|
32
|
+
export type ClaimsValidationResult =
|
|
33
|
+
/** `claims` is `undefined` when the input was empty/whitespace. */
|
|
34
|
+
| { ok: true; claims: Record<string, unknown> | undefined }
|
|
35
|
+
| { ok: false; message: string };
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Validate the claims textarea's raw text. Empty input is valid (no
|
|
39
|
+
* claims). Messages match the emulator UI verbatim so users see the
|
|
40
|
+
* same wording in both tools.
|
|
41
|
+
*/
|
|
42
|
+
export function validateSerializedClaims(text: string): ClaimsValidationResult {
|
|
43
|
+
const trimmed = text.trim();
|
|
44
|
+
if (!trimmed) return { ok: true, claims: undefined };
|
|
45
|
+
if (trimmed.length > CUSTOM_CLAIMS_MAX_LENGTH) {
|
|
46
|
+
return { ok: false, message: 'Custom claims length must not exceed 1000 characters' };
|
|
47
|
+
}
|
|
48
|
+
let parsed: unknown;
|
|
49
|
+
try {
|
|
50
|
+
parsed = JSON.parse(trimmed);
|
|
51
|
+
} catch {
|
|
52
|
+
return { ok: false, message: 'Custom claims must be a valid JSON object' };
|
|
53
|
+
}
|
|
54
|
+
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
|
|
55
|
+
return { ok: false, message: 'Custom claims must be a valid JSON object' };
|
|
56
|
+
}
|
|
57
|
+
for (const key of FORBIDDEN_CUSTOM_CLAIMS) {
|
|
58
|
+
if (key in parsed) {
|
|
59
|
+
return { ok: false, message: `Custom claims must not have forbidden key: ${key}` };
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return { ok: true, claims: parsed as Record<string, unknown> };
|
|
63
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { useState, type FormEvent } from 'react';
|
|
2
|
+
import { providerLabel } from '../providers.js';
|
|
3
|
+
import type { AuthProviderConfigEntry } from '../hooks/useAuthProviderConfig.js';
|
|
4
|
+
|
|
5
|
+
/** Providers always shown as a toggle row, regardless of whether the
|
|
6
|
+
* backend has an explicit entry for them yet (an unconfigured provider
|
|
7
|
+
* simply reads as disabled — same default the sandbox backend applies). */
|
|
8
|
+
export const DEFAULT_KNOWN_PROVIDER_IDS = [
|
|
9
|
+
'password',
|
|
10
|
+
'anonymous',
|
|
11
|
+
'google.com',
|
|
12
|
+
'github.com',
|
|
13
|
+
'apple.com',
|
|
14
|
+
'microsoft.com',
|
|
15
|
+
] as const;
|
|
16
|
+
|
|
17
|
+
export interface AuthProviderTogglesProps {
|
|
18
|
+
/** Current config — usually `useAuthProviderConfig(auth).config`. */
|
|
19
|
+
config: AuthProviderConfigEntry[];
|
|
20
|
+
/** Fired when a toggle (known or custom) flips. */
|
|
21
|
+
onToggle: (providerId: string, enabled: boolean) => void;
|
|
22
|
+
/** Always-shown rows, in this order. Default: {@link DEFAULT_KNOWN_PROVIDER_IDS}. */
|
|
23
|
+
knownProviderIds?: readonly string[];
|
|
24
|
+
isLoading?: boolean;
|
|
25
|
+
error?: Error;
|
|
26
|
+
className?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function isEnabled(config: AuthProviderConfigEntry[], providerId: string): boolean {
|
|
30
|
+
return config.find((c) => c.providerId === providerId)?.enabled ?? false;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Headless "Sign-in providers" toggle grid — the Authentication → Sign-in
|
|
35
|
+
* method surface. Known providers (password / anonymous / the built-in OAuth
|
|
36
|
+
* set) always render as a row; any OTHER provider already present in
|
|
37
|
+
* `config` (a custom OAuth id a host previously added) also gets a row. A
|
|
38
|
+
* free-text field lets a consumer enable an arbitrary OAuth provider id not
|
|
39
|
+
* in the known set — this is a SECTION, not a dialog: the add row lives
|
|
40
|
+
* inline, no modal.
|
|
41
|
+
*
|
|
42
|
+
* Fully headless: styling hangs off `data-pyric-*`, matching the rest of
|
|
43
|
+
* `@pyric/ui/auth` (`AuthUserList`, `AuthUserForm`, …). Data + mutation come
|
|
44
|
+
* from `useAuthProviderConfig`; this component only renders + fires events.
|
|
45
|
+
*/
|
|
46
|
+
export function AuthProviderToggles({
|
|
47
|
+
config,
|
|
48
|
+
onToggle,
|
|
49
|
+
knownProviderIds = DEFAULT_KNOWN_PROVIDER_IDS,
|
|
50
|
+
isLoading,
|
|
51
|
+
error,
|
|
52
|
+
className,
|
|
53
|
+
}: AuthProviderTogglesProps) {
|
|
54
|
+
const [customId, setCustomId] = useState('');
|
|
55
|
+
|
|
56
|
+
const customEntries = config.filter(
|
|
57
|
+
(c) => !knownProviderIds.includes(c.providerId),
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
const handleAdd = (e: FormEvent) => {
|
|
61
|
+
e.preventDefault();
|
|
62
|
+
const id = customId.trim();
|
|
63
|
+
if (!id) return;
|
|
64
|
+
onToggle(id, true);
|
|
65
|
+
setCustomId('');
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
return (
|
|
69
|
+
<div className={className} data-pyric-ui="auth-provider-toggles">
|
|
70
|
+
{error ? (
|
|
71
|
+
<p data-pyric-provider-toggles-error role="alert">
|
|
72
|
+
{error.message}
|
|
73
|
+
</p>
|
|
74
|
+
) : null}
|
|
75
|
+
<div role="list" aria-label="Sign-in providers" data-pyric-provider-toggle-list data-pyric-loading={isLoading ? '' : undefined}>
|
|
76
|
+
{knownProviderIds.map((providerId) => (
|
|
77
|
+
<ProviderToggleRow
|
|
78
|
+
key={providerId}
|
|
79
|
+
providerId={providerId}
|
|
80
|
+
enabled={isEnabled(config, providerId)}
|
|
81
|
+
onToggle={onToggle}
|
|
82
|
+
/>
|
|
83
|
+
))}
|
|
84
|
+
{customEntries.map((entry) => (
|
|
85
|
+
<ProviderToggleRow
|
|
86
|
+
key={entry.providerId}
|
|
87
|
+
providerId={entry.providerId}
|
|
88
|
+
enabled={entry.enabled}
|
|
89
|
+
onToggle={onToggle}
|
|
90
|
+
custom
|
|
91
|
+
/>
|
|
92
|
+
))}
|
|
93
|
+
</div>
|
|
94
|
+
|
|
95
|
+
<form data-pyric-add-provider-form onSubmit={handleAdd}>
|
|
96
|
+
<label data-pyric-field-label="custom-provider-id">
|
|
97
|
+
<span data-pyric-label-text>Add a custom OAuth provider</span>
|
|
98
|
+
<input
|
|
99
|
+
type="text"
|
|
100
|
+
data-pyric-field="custom-provider-id"
|
|
101
|
+
placeholder="e.g. yahoo.com"
|
|
102
|
+
value={customId}
|
|
103
|
+
onChange={(e) => setCustomId(e.target.value)}
|
|
104
|
+
/>
|
|
105
|
+
</label>
|
|
106
|
+
<button type="submit" data-pyric-add-provider disabled={!customId.trim()}>
|
|
107
|
+
Add
|
|
108
|
+
</button>
|
|
109
|
+
</form>
|
|
110
|
+
</div>
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function ProviderToggleRow({
|
|
115
|
+
providerId,
|
|
116
|
+
enabled,
|
|
117
|
+
onToggle,
|
|
118
|
+
custom,
|
|
119
|
+
}: {
|
|
120
|
+
providerId: string;
|
|
121
|
+
enabled: boolean;
|
|
122
|
+
onToggle: (providerId: string, enabled: boolean) => void;
|
|
123
|
+
custom?: boolean;
|
|
124
|
+
}) {
|
|
125
|
+
const inputId = `auth-provider-toggle-${providerId}`;
|
|
126
|
+
return (
|
|
127
|
+
<div
|
|
128
|
+
role="listitem"
|
|
129
|
+
data-pyric-provider-toggle
|
|
130
|
+
data-pyric-provider-id={providerId}
|
|
131
|
+
data-pyric-provider-enabled={enabled ? '' : undefined}
|
|
132
|
+
data-pyric-provider-custom={custom ? '' : undefined}
|
|
133
|
+
>
|
|
134
|
+
<label htmlFor={inputId} data-pyric-provider-toggle-label>
|
|
135
|
+
<input
|
|
136
|
+
id={inputId}
|
|
137
|
+
type="checkbox"
|
|
138
|
+
data-pyric-field="enabled"
|
|
139
|
+
checked={enabled}
|
|
140
|
+
onChange={(e) => onToggle(providerId, e.target.checked)}
|
|
141
|
+
/>
|
|
142
|
+
<span data-pyric-provider-toggle-name>{providerLabel(providerId)}</span>
|
|
143
|
+
<span data-pyric-provider-toggle-id>{providerId}</span>
|
|
144
|
+
</label>
|
|
145
|
+
</div>
|
|
146
|
+
);
|
|
147
|
+
}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { useState, type FormEvent, type ReactNode } from 'react';
|
|
2
|
+
import type { HelperState, NewIdentitySpec, SandboxIdentity } from '../controller.js';
|
|
3
|
+
import { validateSerializedClaims } from '../claims.js';
|
|
4
|
+
import { providerLabel } from '../providers.js';
|
|
5
|
+
|
|
6
|
+
export interface AuthSignInHelperProps {
|
|
7
|
+
/** Snapshot from `useAuthFlowHelper`. Renders nothing while
|
|
8
|
+
* `state.request` is null. */
|
|
9
|
+
state: HelperState;
|
|
10
|
+
/** Settle with an existing identity (wire to the hook's `pick`). */
|
|
11
|
+
onPick: (uid: string) => void;
|
|
12
|
+
/** Create + sign in as a new identity (wire to the hook's `add`). */
|
|
13
|
+
onAdd: (spec: NewIdentitySpec) => void;
|
|
14
|
+
/** Dismiss the flow (wire to the hook's `cancel`). Rejects the app's
|
|
15
|
+
* sign-in promise with `auth/popup-closed-by-user`. */
|
|
16
|
+
onCancel: () => void;
|
|
17
|
+
/**
|
|
18
|
+
* Optional renderer for an account row's content. Default renders
|
|
19
|
+
* the display name (or email, or uid) plus the email when both
|
|
20
|
+
* exist. The row button + data attributes stay owned by the
|
|
21
|
+
* component; this slot only fills the button's children.
|
|
22
|
+
*/
|
|
23
|
+
renderAccount?: (identity: SandboxIdentity) => ReactNode;
|
|
24
|
+
/** Heading text. Default: `Sign in with <provider label>`. */
|
|
25
|
+
title?: ReactNode;
|
|
26
|
+
/** Optional helper text rendered under the title
|
|
27
|
+
* (`[data-pyric-helper-description]`). Default: none. */
|
|
28
|
+
description?: ReactNode;
|
|
29
|
+
/** Prefill for the add-account form (e.g. a host-suggested email).
|
|
30
|
+
* Read once on mount; `claims` is the raw textarea JSON text. */
|
|
31
|
+
initialValues?: { email?: string; displayName?: string; claims?: string };
|
|
32
|
+
className?: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Headless emulator-style sign-in helper: an account picker over the
|
|
37
|
+
* sandbox's known identities plus an add-account form (email, display
|
|
38
|
+
* name, custom-claims JSON with emulator-grade validation messages).
|
|
39
|
+
*
|
|
40
|
+
* Ships zero styling. Structure is addressable via the
|
|
41
|
+
* `data-pyric-*` contract:
|
|
42
|
+
*
|
|
43
|
+
* - root: `[data-pyric-ui="auth-signin-helper"]`,
|
|
44
|
+
* `[data-pyric-provider-id]`, `[data-pyric-auth-type]`
|
|
45
|
+
* - picker: `[data-pyric-account-list]` > `[data-pyric-account-entry]`
|
|
46
|
+
* > `button[data-pyric-account-pick]`
|
|
47
|
+
* - form: `form[data-pyric-add-account-form]`, fields
|
|
48
|
+
* `[data-pyric-field="email" | "display-name" | "claims"]`,
|
|
49
|
+
* `[data-pyric-claims-error]` (role=alert),
|
|
50
|
+
* `button[data-pyric-cancel]`, `button[data-pyric-submit]`
|
|
51
|
+
*
|
|
52
|
+
* Positioning is the consumer's job — render it inside your own modal
|
|
53
|
+
* or panel (the flow is host-UI-agnostic; only `onCancel` carries the
|
|
54
|
+
* popup-closed semantics).
|
|
55
|
+
*/
|
|
56
|
+
export function AuthSignInHelper({
|
|
57
|
+
state,
|
|
58
|
+
onPick,
|
|
59
|
+
onAdd,
|
|
60
|
+
onCancel,
|
|
61
|
+
renderAccount,
|
|
62
|
+
title,
|
|
63
|
+
description,
|
|
64
|
+
initialValues,
|
|
65
|
+
className,
|
|
66
|
+
}: AuthSignInHelperProps) {
|
|
67
|
+
const [email, setEmail] = useState(initialValues?.email ?? '');
|
|
68
|
+
const [displayName, setDisplayName] = useState(initialValues?.displayName ?? '');
|
|
69
|
+
const [claims, setClaims] = useState(initialValues?.claims ?? '');
|
|
70
|
+
const [claimsError, setClaimsError] = useState<string | null>(null);
|
|
71
|
+
|
|
72
|
+
if (!state.request) return null;
|
|
73
|
+
const { providerId, authType } = state.request;
|
|
74
|
+
const label = providerLabel(providerId);
|
|
75
|
+
|
|
76
|
+
const reset = () => {
|
|
77
|
+
setEmail('');
|
|
78
|
+
setDisplayName('');
|
|
79
|
+
setClaims('');
|
|
80
|
+
setClaimsError(null);
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const cancel = () => {
|
|
84
|
+
onCancel();
|
|
85
|
+
reset();
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const submit = (e: FormEvent) => {
|
|
89
|
+
e.preventDefault();
|
|
90
|
+
const result = validateSerializedClaims(claims);
|
|
91
|
+
if (!result.ok) {
|
|
92
|
+
setClaimsError(result.message);
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
if (!email.trim()) return;
|
|
96
|
+
onAdd({
|
|
97
|
+
email: email.trim(),
|
|
98
|
+
displayName: displayName.trim() || undefined,
|
|
99
|
+
customClaims: result.claims,
|
|
100
|
+
});
|
|
101
|
+
reset();
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
return (
|
|
105
|
+
<section
|
|
106
|
+
className={className}
|
|
107
|
+
data-pyric-ui="auth-signin-helper"
|
|
108
|
+
data-pyric-provider-id={providerId}
|
|
109
|
+
data-pyric-auth-type={authType}
|
|
110
|
+
aria-label={`Sign in with ${label}`}
|
|
111
|
+
>
|
|
112
|
+
<header data-pyric-helper-header>
|
|
113
|
+
<h2 data-pyric-helper-title>{title ?? `Sign in with ${label}`}</h2>
|
|
114
|
+
{description != null && <p data-pyric-helper-description>{description}</p>}
|
|
115
|
+
</header>
|
|
116
|
+
|
|
117
|
+
{state.identities.length > 0 && (
|
|
118
|
+
<ul data-pyric-account-list>
|
|
119
|
+
{state.identities.map((identity) => (
|
|
120
|
+
<li
|
|
121
|
+
key={identity.uid}
|
|
122
|
+
data-pyric-account-entry
|
|
123
|
+
data-pyric-account-uid={identity.uid}
|
|
124
|
+
data-pyric-provider-id={identity.providerId}
|
|
125
|
+
>
|
|
126
|
+
<button
|
|
127
|
+
type="button"
|
|
128
|
+
data-pyric-account-pick
|
|
129
|
+
onClick={() => {
|
|
130
|
+
onPick(identity.uid);
|
|
131
|
+
reset();
|
|
132
|
+
}}
|
|
133
|
+
>
|
|
134
|
+
{renderAccount ? (
|
|
135
|
+
renderAccount(identity)
|
|
136
|
+
) : (
|
|
137
|
+
<>
|
|
138
|
+
<span data-pyric-account-name>
|
|
139
|
+
{identity.displayName || identity.email || identity.uid}
|
|
140
|
+
</span>
|
|
141
|
+
{identity.email && identity.displayName ? (
|
|
142
|
+
<span data-pyric-account-email>{identity.email}</span>
|
|
143
|
+
) : null}
|
|
144
|
+
</>
|
|
145
|
+
)}
|
|
146
|
+
</button>
|
|
147
|
+
</li>
|
|
148
|
+
))}
|
|
149
|
+
</ul>
|
|
150
|
+
)}
|
|
151
|
+
|
|
152
|
+
<form data-pyric-add-account-form onSubmit={submit}>
|
|
153
|
+
<input
|
|
154
|
+
type="email"
|
|
155
|
+
data-pyric-field="email"
|
|
156
|
+
aria-label="Email"
|
|
157
|
+
placeholder="email@example.com"
|
|
158
|
+
value={email}
|
|
159
|
+
onChange={(e) => setEmail(e.target.value)}
|
|
160
|
+
/>
|
|
161
|
+
<input
|
|
162
|
+
type="text"
|
|
163
|
+
data-pyric-field="display-name"
|
|
164
|
+
aria-label="Display name (optional)"
|
|
165
|
+
placeholder="Display name (optional)"
|
|
166
|
+
value={displayName}
|
|
167
|
+
onChange={(e) => setDisplayName(e.target.value)}
|
|
168
|
+
/>
|
|
169
|
+
<textarea
|
|
170
|
+
data-pyric-field="claims"
|
|
171
|
+
data-pyric-claims-invalid={claimsError != null ? '' : undefined}
|
|
172
|
+
aria-label="Custom claims (optional)"
|
|
173
|
+
aria-invalid={claimsError != null || undefined}
|
|
174
|
+
placeholder={'Enter valid json, e.g. {"role":"admin"}'}
|
|
175
|
+
value={claims}
|
|
176
|
+
onChange={(e) => {
|
|
177
|
+
setClaims(e.target.value);
|
|
178
|
+
setClaimsError(null);
|
|
179
|
+
}}
|
|
180
|
+
/>
|
|
181
|
+
{claimsError != null && (
|
|
182
|
+
<p role="alert" data-pyric-claims-error>
|
|
183
|
+
{claimsError}
|
|
184
|
+
</p>
|
|
185
|
+
)}
|
|
186
|
+
<footer data-pyric-helper-actions>
|
|
187
|
+
<button type="button" data-pyric-cancel onClick={cancel}>
|
|
188
|
+
Cancel
|
|
189
|
+
</button>
|
|
190
|
+
<button type="submit" data-pyric-submit disabled={!email.trim()}>
|
|
191
|
+
Sign in
|
|
192
|
+
</button>
|
|
193
|
+
</footer>
|
|
194
|
+
</form>
|
|
195
|
+
</section>
|
|
196
|
+
);
|
|
197
|
+
}
|