@xenosystem/blocks 0.6.0 → 0.7.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.
@@ -1,8 +1,6 @@
1
1
  import * as react from 'react';
2
2
  import { ReactNode } from 'react';
3
3
  import { PanelModule, PanelManifest } from '@xenosystem/panel-sdk';
4
- import { XenoAccountStatus, XenoAccountIdentity, XenoAccountActionKind } from '@xenosystem/panel-account';
5
- export * from '@xenosystem/panel-account';
6
4
  import { BlockDeclaration } from '@xenosystem/block-sdk';
7
5
 
8
6
  interface CreateAuthGatePanelOptions {
@@ -22,6 +20,205 @@ declare const AUTH_GATE_PANEL_ID = "xeno.core.auth-gate";
22
20
  */
23
21
  declare const authGateManifest: PanelManifest;
24
22
 
23
+ /**
24
+ * The `xeno.core.account` contract.
25
+ *
26
+ * ## 🔴 What this panel must NOT do, per `XENO AUTH - SPEC.md`
27
+ *
28
+ * A naive account panel violates the locked auth spec in four separate ways. Each is prevented
29
+ * structurally here, not by convention:
30
+ *
31
+ * | Locked rule | The naive mistake | How this contract prevents it |
32
+ * |---|---|---|
33
+ * | **L9** — refresh tokens live ONLY in the OS keystore; `~/.xeno/*` holds no secrets; **never** `localStorage` | Persisting a session or token through `serialize()`. The panel's ONE capability is `storage.local`, which is *exactly* the store L9 forbids for a token. | **No token type exists.** `XenoAccountIdentity` has no field a token could occupy, and `serialize()` returns view preferences only. What the panel never receives, it cannot persist. |
34
+ * | **L10** — the real gate is server-side; client entitlement checks are "legitimacy + friction", never a security boundary | Gating a feature locally on `plan.tier`. | `XenoEntitlement` is explicitly **display-only** and documented as such. The panel exposes no "may I?" predicate for anyone to mistake for a gate. |
35
+ * | **L12** — step-up re-auth for money/account (plan cancel, account delete, logout-everywhere, payout change) must be a **fresh interactive** auth the broker cannot satisfy silently | Emitting `signOut` or `cancelPlan` as an ordinary intent and rendering success. | Sensitive actions carry `stepUp: true` on the intent, and the panel **never renders a result** — it waits for the host to push new state. |
36
+ * | **L5** — Hub is the preferred broker, **never a hard dependency**; every product must authenticate with Hub uninstalled | Assuming a broker-supplied identity and rendering nothing without one. | `signedOut` is a first-class, fully-rendered state with its own action. The panel neither knows nor asks whether a broker exists. |
37
+ *
38
+ * The panel holds **no network capability** — the same posture as the rest of the family. Identity,
39
+ * plan, credits and ledger are **host-pushed state**.
40
+ *
41
+ * @module
42
+ */
43
+ /** Who is signed in. **Carries no token, by construction.** */
44
+ interface XenoAccountIdentity {
45
+ /** The OIDC `sub`. An opaque identifier, not a secret. */
46
+ sub: string;
47
+ /** Display name. */
48
+ name?: string;
49
+ /** Email, for display. */
50
+ email?: string;
51
+ /** Avatar URL. */
52
+ avatarUrl?: string;
53
+ }
54
+ /** Plan tier. Display only — see L10 above. */
55
+ interface XenoAccountPlan {
56
+ /** Stable id (`'free'`, `'pro'`, `'team'`). */
57
+ id: string;
58
+ /** Display name. */
59
+ label: string;
60
+ /** Epoch ms the current period ends. */
61
+ renewsAt?: number;
62
+ /** The plan is set to lapse rather than renew. */
63
+ cancelAtPeriodEnd?: boolean;
64
+ /** Seats held, for a team plan. */
65
+ seats?: {
66
+ used: number;
67
+ total: number;
68
+ };
69
+ }
70
+ /**
71
+ * A credit balance.
72
+ *
73
+ * **`asOf` is mandatory.** A balance is a number that goes out of date the moment anything runs, and
74
+ * a stale one rendered as fresh is worse than no balance at all — a user acts on it. The view must
75
+ * show the age or show a loading state; there is no third option, which is why the field cannot be
76
+ * omitted.
77
+ */
78
+ interface XenoAccountCredits {
79
+ /** Remaining credits. */
80
+ balance: number;
81
+ /** Epoch ms this balance was true. **Mandatory.** */
82
+ asOf: number;
83
+ /** Granted this period, when the host tracks it. */
84
+ included?: number;
85
+ /** Consumed this period. */
86
+ used?: number;
87
+ /** Epoch ms the allowance resets. */
88
+ resetsAt?: number;
89
+ /** Currency-style unit label (`'credits'`, `'tokens'`). */
90
+ unit?: string;
91
+ }
92
+ /** One ledger entry. */
93
+ interface XenoLedgerEntry {
94
+ /** Stable id. */
95
+ id: string;
96
+ /** Epoch ms. */
97
+ ts: number;
98
+ /** What happened, in the host's words. */
99
+ description: string;
100
+ /** Signed: negative is a spend, positive a grant or purchase. */
101
+ amount: number;
102
+ /** Which product or surface spent it. */
103
+ source?: string;
104
+ /** Balance immediately after, when the host knows it. */
105
+ balanceAfter?: number;
106
+ }
107
+ /** An append-oriented ledger update — the same discipline as runs and console. */
108
+ interface XenoLedgerDelta {
109
+ rev?: number;
110
+ append?: XenoLedgerEntry[];
111
+ replace?: XenoLedgerEntry[];
112
+ clear?: boolean;
113
+ /** More pages exist before the oldest held entry. */
114
+ hasMore?: boolean;
115
+ }
116
+ /**
117
+ * An entitlement, for DISPLAY.
118
+ *
119
+ * **Not a gate.** L10: the only unbreakable enforcement is `api.xenostudio.ai` validating the token
120
+ * and debiting the ledger. Rendering a lock icon is friction and honesty; it is not security, and
121
+ * nothing here should be mistaken for a permission check.
122
+ */
123
+ interface XenoEntitlement {
124
+ id: string;
125
+ label: string;
126
+ /** Whether the host says this plan includes it. */
127
+ included: boolean;
128
+ /** Host-authored explanation of what unlocks it. */
129
+ detail?: string;
130
+ }
131
+ /** Session lifecycle. */
132
+ type XenoAccountStatus = 'unknown' | 'signedOut' | 'signedIn'
133
+ /** A silent refresh is in flight. Balance and plan may be stale. */
134
+ | 'refreshing'
135
+ /** The session expired and interactive sign-in is required. */
136
+ | 'expired'
137
+ /** The host could not reach the origin. Cached state may still be shown, honestly. */
138
+ | 'offline';
139
+ /** Everything the host pushes. */
140
+ interface XenoAccountState {
141
+ status: XenoAccountStatus;
142
+ identity?: XenoAccountIdentity;
143
+ plan?: XenoAccountPlan;
144
+ credits?: XenoAccountCredits;
145
+ entitlements?: XenoEntitlement[];
146
+ /** Host-authored message for the current status, rendered verbatim. */
147
+ message?: string;
148
+ /**
149
+ * The active seat, when the host manages several.
150
+ *
151
+ * Per L6 there is no shared multi-reader credential; a seat switch is a host flow, not a token
152
+ * swap the panel performs.
153
+ */
154
+ seatId?: string;
155
+ /** Seats the user may switch to. */
156
+ seats?: {
157
+ id: string;
158
+ label: string;
159
+ active: boolean;
160
+ }[];
161
+ }
162
+ /** Actions the panel asks for. **Every one is an intent; the host runs the flow.** */
163
+ type XenoAccountActionKind = 'signIn' | 'signOut' | 'purchase' | 'manageBilling' | 'switchSeat' | 'refresh';
164
+ /** An account intent. */
165
+ interface XenoAccountAction {
166
+ kind: XenoAccountActionKind;
167
+ /** For `switchSeat`. */
168
+ seatId?: string;
169
+ /** For `purchase` — a host-defined product/pack id. */
170
+ productId?: string;
171
+ /**
172
+ * **L12**: this action needs a FRESH interactive re-auth, which the broker must not satisfy
173
+ * silently. The panel flags it; the host enforces it.
174
+ */
175
+ stepUp?: boolean;
176
+ }
177
+ /**
178
+ * Actions that require step-up re-auth per `XENO AUTH - SPEC.md` L12.
179
+ *
180
+ * Irreversible or money-adjacent operations. `manageBilling` is here because it is the door to plan
181
+ * cancellation and payout changes, both named in L12.
182
+ */
183
+ declare const STEP_UP_ACTIONS: ReadonlySet<XenoAccountActionKind>;
184
+ /** The panel's serialized state. **No identity, no session, no balance — see L9.** */
185
+ interface AccountPanelState {
186
+ /** Whether the ledger section is expanded. A view preference and nothing more. */
187
+ ledgerExpanded: boolean;
188
+ }
189
+ /** What the controller exposes to its view. */
190
+ interface AccountViewState {
191
+ status: XenoAccountStatus;
192
+ identity: XenoAccountIdentity | null;
193
+ plan: XenoAccountPlan | null;
194
+ credits: XenoAccountCredits | null;
195
+ entitlements: XenoEntitlement[];
196
+ seats: NonNullable<XenoAccountState['seats']>;
197
+ message: string | null;
198
+ ledger: XenoLedgerEntry[];
199
+ ledgerHasMore: boolean;
200
+ ledgerExpanded: boolean;
201
+ /**
202
+ * Age of the balance in ms, or `null` when there is none.
203
+ *
204
+ * The view MUST surface this (or a loading state) rather than rendering a number as though it
205
+ * were current.
206
+ */
207
+ creditsAgeMs: number | null;
208
+ /** The balance is older than the staleness threshold. */
209
+ creditsStale: boolean;
210
+ }
211
+ /** Is a user signed in right now? */
212
+ declare function isSignedIn(status: XenoAccountStatus): boolean;
213
+ /** Does this action need a fresh interactive auth (L12)? */
214
+ declare function requiresStepUp(kind: XenoAccountActionKind): boolean;
215
+ /** Age of a balance in ms. */
216
+ declare function creditsAge(credits: XenoAccountCredits | undefined, now: number): number | null;
217
+ /** Format a signed ledger amount with an explicit sign. */
218
+ declare function formatAmount(amount: number, unit?: string): string;
219
+ /** Format a balance age compactly. */
220
+ declare function formatAge(ms: number): string;
221
+
25
222
  /**
26
223
  * `xeno.core.auth-gate` — the door in front of a XENO product.
27
224
  *
@@ -219,4 +416,203 @@ interface MountedXenoAuthGate {
219
416
  }
220
417
  declare function mountXenoAuthGate(root: HTMLElement, options: MountXenoAuthGateOptions): MountedXenoAuthGate;
221
418
 
222
- export { AUTH_GATE_DECLARATION, AUTH_GATE_INITIAL, AUTH_GATE_PANEL_ID, type AuthGateAction, AuthGateController, type AuthGateHostBridge, AuthGateView$1 as AuthGateView, type AuthGateView as AuthGateViewModel, type AuthGateViewProps, BEHAVIOURS, type CreateAuthGatePanelOptions, type MountXenoAuthGateOptions, type MountedXenoAuthGate, PHASE_INTENTS, type XenoAuthGateBridge, type XenoAuthGateIntent, type XenoAuthGateIntentKind, type XenoAuthGatePhase, type XenoAuthGateState, type XenoLicenceState, authGateManifest, authGatePanel, authGateView, createAuthGatePanel, deriveAuthGatePhase, formatSince, hostContradiction, intentOffered, mountXenoAuthGate, plateCopy };
419
+ /**
420
+ * The Account controller.
421
+ *
422
+ * Everything it holds arrives from the host. **The panel never fetches**: it has no network
423
+ * capability, no origin, no token, and no knowledge of whether a broker exists. It renders what it
424
+ * was pushed, timestamps it honestly, and emits intents.
425
+ *
426
+ * @module
427
+ */
428
+
429
+ /** The host seam. */
430
+ interface AccountHostBridge {
431
+ emit(portId: string, value: unknown): void;
432
+ now?: () => number;
433
+ }
434
+ /** Construction options. */
435
+ interface AccountControllerOptions {
436
+ host: AccountHostBridge;
437
+ /** ms after which a balance is called stale. Default 60 000. */
438
+ staleAfterMs?: number;
439
+ /** Ledger entries retained. Default 500. */
440
+ maxLedger?: number;
441
+ }
442
+ /** The Account panel controller. */
443
+ declare class AccountController {
444
+ private readonly host;
445
+ private readonly now;
446
+ private readonly staleAfterMs;
447
+ private readonly maxLedger;
448
+ private account;
449
+ private ledger;
450
+ private ledgerHasMore;
451
+ private ledgerRev;
452
+ private ledgerExpanded;
453
+ private readonly listeners;
454
+ private snapshot;
455
+ constructor(options: AccountControllerOptions);
456
+ subscribe: (listener: () => void) => (() => void);
457
+ getState: () => AccountViewState;
458
+ private notify;
459
+ /**
460
+ * Replace the account state.
461
+ *
462
+ * The host is authoritative. The panel does not merge cleverly, does not keep a "better" older
463
+ * value, and does not infer a status the host did not send — a panel that second-guesses its host
464
+ * is a second source of truth about who is signed in.
465
+ *
466
+ * @param next - The new state.
467
+ */
468
+ setAccount(next: XenoAccountState): void;
469
+ /**
470
+ * A partial update, for hosts that push only what changed.
471
+ *
472
+ * `credits` is replaced wholesale rather than field-merged: a balance and its `asOf` are one fact,
473
+ * and merging a new number onto an old timestamp manufactures a lie.
474
+ *
475
+ * @param patch - Fields to overwrite.
476
+ */
477
+ patchAccount(patch: Partial<XenoAccountState>): void;
478
+ /** Set the session status alone. */
479
+ setStatus(status: XenoAccountStatus, message?: string): void;
480
+ /**
481
+ * Apply a ledger delta.
482
+ *
483
+ * Append-oriented, with `rev` gating out-of-order deliveries — the same discipline as runs and
484
+ * console.
485
+ *
486
+ * @param delta - The update.
487
+ * @returns Whether it was applied.
488
+ */
489
+ applyLedger(delta: XenoLedgerDelta): boolean;
490
+ /**
491
+ * Re-evaluate the balance's age.
492
+ *
493
+ * `getState` memoizes — it must, or `useSyncExternalStore` re-renders forever — which means the
494
+ * age would otherwise freeze at the moment of the last push, and a balance would sit on screen
495
+ * reading "as of 2s ago" for an hour. That is precisely the stale-as-fresh failure `asOf` exists
496
+ * to prevent, reintroduced by a caching bug.
497
+ *
498
+ * So time is an INPUT here, and the panel drives this on an interval. Notifying only when the
499
+ * rendered age actually changes keeps it off the re-render hot path: a one-second tick that
500
+ * re-renders once per second is honest; one that re-renders sixty times is waste.
501
+ *
502
+ * @returns Whether anything the view shows changed.
503
+ */
504
+ tick(): boolean;
505
+ /** Expand or collapse the ledger. A view preference. */
506
+ setLedgerExpanded(expanded: boolean): void;
507
+ /**
508
+ * Ask the host to do something.
509
+ *
510
+ * **The panel never renders the result.** It does not flip to `signedOut` because the user
511
+ * clicked Sign out, and it does not add credits because the user clicked Purchase. It emits, and
512
+ * waits to be told — the same intents-only doctrine the rest of the family follows, and the only
513
+ * shape compatible with `XENO AUTH - SPEC.md` L12, where a sensitive action may be REFUSED at the
514
+ * step-up prompt after the click.
515
+ *
516
+ * @param kind - What to ask for.
517
+ * @param extra - `seatId` or `productId`.
518
+ * @returns The emitted action.
519
+ */
520
+ request(kind: XenoAccountActionKind, extra?: Omit<XenoAccountAction, 'kind' | 'stepUp'>): XenoAccountAction;
521
+ /** Ask the host to re-read the balance. Cheap, idempotent, no step-up. */
522
+ refresh(): XenoAccountAction;
523
+ /**
524
+ * Serialize.
525
+ *
526
+ * **View preferences only.** No identity, no plan, no balance, no token.
527
+ *
528
+ * `XENO AUTH - SPEC.md` L9 forbids a refresh token in `localStorage` or `~/.xeno/*`, and this
529
+ * panel's one capability is `storage.local` — the very store L9 names. Persisting a balance would
530
+ * also reintroduce the stale-as-fresh problem `asOf` exists to solve: a number restored from disk
531
+ * has no honest timestamp.
532
+ */
533
+ serialize(): {
534
+ ledgerExpanded: boolean;
535
+ };
536
+ /** Restore view preferences. */
537
+ deserialize(state: unknown): void;
538
+ /** Tear down. */
539
+ dispose(): void;
540
+ }
541
+
542
+ /**
543
+ * The `PanelModule` — view wired inside the package (one React copy).
544
+ *
545
+ * @module
546
+ */
547
+
548
+ /** Everything a renderer needs: the controller plus the resolved config. */
549
+ interface AccountRenderContext {
550
+ controller: AccountController;
551
+ config: {
552
+ showLedger: boolean;
553
+ showEntitlements: boolean;
554
+ emptyHint?: string;
555
+ };
556
+ }
557
+ /** Options for {@link createAccountPanel}. */
558
+ interface CreateAccountPanelOptions {
559
+ /** Override the view. Rarely needed — mounting inside the package keeps React singular. */
560
+ render?: (root: HTMLElement, context: AccountRenderContext) => () => void;
561
+ }
562
+ /**
563
+ * Build the Account panel module.
564
+ *
565
+ * @param options - Optional renderer override.
566
+ * @returns The module.
567
+ */
568
+ declare function createAccountPanel(options?: CreateAccountPanelOptions): PanelModule;
569
+ /** The default Account panel module — view already wired. Register THIS. */
570
+ declare const accountPanel: PanelModule;
571
+
572
+ /**
573
+ * The `xeno.core.account` manifest.
574
+ *
575
+ * Capabilities: `storage.local` only — **no `net.fetch`**. That is not an oversight. Identity, plan,
576
+ * credits and ledger are host-pushed; a panel that could reach the origin itself would need a token,
577
+ * and a token is exactly what `XENO AUTH - SPEC.md` L9 says must never live where this panel can
578
+ * reach.
579
+ *
580
+ * @module
581
+ */
582
+
583
+ /** The canonical manifest id. */
584
+ declare const ACCOUNT_PANEL_ID = "xeno.core.account";
585
+ /** The `xeno.core.account` manifest. */
586
+ declare const accountManifest: PanelManifest;
587
+
588
+ /**
589
+ * The Account panel view.
590
+ *
591
+ * Composes `@xenosystem/workbench/primitives`; the host must ensure `@xenosystem/workbench/primitives.css` is
592
+ * present.
593
+ *
594
+ * Three deliberate absences, each traceable to `XENO AUTH - SPEC.md`:
595
+ *
596
+ * - **No secret-reveal affordance.** `xeno-hub` has one (a masked API key with an eye toggle). It is
597
+ * deliberately not lifted: a shared panel that can render a secret is a shared panel that can leak
598
+ * one into a screenshot, a screen share, or a `serialize()`. L9 keeps secrets in the keystore, and
599
+ * the surest way to honour that is to have nothing to reveal.
600
+ * - **No optimistic success.** Clicking Sign out does not sign you out on screen. L12 says a
601
+ * sensitive action needs a fresh interactive re-auth that the user may cancel or fail; a view that
602
+ * had already cleared the name would be lying about state it does not own.
603
+ * - **No local gate.** Entitlements render as facts, never as enforcement (L10).
604
+ *
605
+ * @module
606
+ */
607
+
608
+ /** Props for {@link AccountPanelView}. */
609
+ interface AccountPanelViewProps {
610
+ controller: AccountController;
611
+ showLedger?: boolean;
612
+ showEntitlements?: boolean;
613
+ emptyHint?: string;
614
+ }
615
+ /** The Account panel view. */
616
+ declare function AccountPanelView({ controller, showLedger, showEntitlements, emptyHint, }: AccountPanelViewProps): ReactNode;
617
+
618
+ export { ACCOUNT_PANEL_ID, AUTH_GATE_DECLARATION, AUTH_GATE_INITIAL, AUTH_GATE_PANEL_ID, AccountController, type AccountControllerOptions, type AccountHostBridge, type AccountPanelState, AccountPanelView, type AccountPanelViewProps, type AccountRenderContext, type AccountViewState, type AuthGateAction, AuthGateController, type AuthGateHostBridge, AuthGateView$1 as AuthGateView, type AuthGateView as AuthGateViewModel, type AuthGateViewProps, BEHAVIOURS, type CreateAccountPanelOptions, type CreateAuthGatePanelOptions, type MountXenoAuthGateOptions, type MountedXenoAuthGate, PHASE_INTENTS, STEP_UP_ACTIONS, type XenoAccountAction, type XenoAccountActionKind, type XenoAccountCredits, type XenoAccountIdentity, type XenoAccountPlan, type XenoAccountState, type XenoAccountStatus, type XenoAuthGateBridge, type XenoAuthGateIntent, type XenoAuthGateIntentKind, type XenoAuthGatePhase, type XenoAuthGateState, type XenoEntitlement, type XenoLedgerDelta, type XenoLedgerEntry, type XenoLicenceState, accountManifest, accountPanel, authGateManifest, authGatePanel, authGateView, createAccountPanel, createAuthGatePanel, creditsAge, deriveAuthGatePhase, formatAge, formatAmount, formatSince, hostContradiction, intentOffered, isSignedIn, mountXenoAuthGate, plateCopy, requiresStepUp };