@xenosystem/blocks 0.10.0 → 0.11.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,6 +1,7 @@
1
1
  import * as react from 'react';
2
2
  import { ReactNode } from 'react';
3
3
  import { PanelModule, PanelManifest, BlockDeclaration } from '@xenosystem/block-sdk';
4
+ import { PanelTone } from '@xenosystem/components/chrome';
4
5
 
5
6
  interface CreateAuthGatePanelOptions {
6
7
  /** What renders behind the gate once it is `open`. A product passes its app; the harness a fixture. */
@@ -371,9 +372,6 @@ declare function plateCopy(phase: XenoAuthGatePhase, product: string, message: s
371
372
  body: string;
372
373
  note?: string;
373
374
  };
374
- declare const BEHAVIOURS: {
375
- 'auth/authGateView': (inputs: Record<string, unknown>, config: Readonly<Record<string, unknown>>) => AuthGateView;
376
- };
377
375
 
378
376
  /**
379
377
  * `mountXenoAuthGate` — the per-product plug, exactly as `XENO AUTH - AUTH GATE DELTA.md` §4 specifies:
@@ -611,7 +609,109 @@ interface AccountPanelViewProps {
611
609
  showEntitlements?: boolean;
612
610
  emptyHint?: string;
613
611
  }
614
- /** The Account panel view. */
612
+ /**
613
+ * The Account panel view — the code twin of `ACCOUNT_DECLARATION`, rendering the SAME model
614
+ * (`accountPanelView`) so the two cannot drift; `test/auth-account/declaration.test.tsx` holds the markup
615
+ * byte-identical in every status.
616
+ */
615
617
  declare function AccountPanelView({ controller, showLedger, showEntitlements, emptyHint, }: AccountPanelViewProps): ReactNode;
616
618
 
617
- 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 };
619
+ /**
620
+ * The account panel AS A DECLARATION — the third block written the §8d way (auth gate, agent, account).
621
+ * Chrome units + bindings + emits over ONE behaviour, `auth/accountPanelView`, which derives every label,
622
+ * tone and row shape from `AccountViewState`. The code view renders the same model; the test holds the
623
+ * two byte-identical in every account status.
624
+ *
625
+ * Presentation contract: input `view` (an `AccountViewState` push from the controller), output `ui`
626
+ * (`{ kind, seatId?, expanded? }`), applied by the controller (`request(kind)`, `refresh()`,
627
+ * `setLedgerExpanded()`). The declaration holds no state.
628
+ */
629
+
630
+ type AccountUiIntent = {
631
+ kind: 'signIn';
632
+ } | {
633
+ kind: 'signOut';
634
+ } | {
635
+ kind: 'manageBilling';
636
+ } | {
637
+ kind: 'refresh';
638
+ } | {
639
+ kind: 'switchSeat';
640
+ seatId: string;
641
+ } | {
642
+ kind: 'ledgerCollapsed';
643
+ collapsed: boolean;
644
+ };
645
+ declare const accountViewManifest: PanelManifest;
646
+ declare const STATUS_LABEL: Record<XenoAccountStatus, string>;
647
+ declare const STATUS_TONE: Record<XenoAccountStatus, PanelTone>;
648
+ interface AccountPanelViewModel {
649
+ loading: boolean;
650
+ signedOut: boolean;
651
+ signedIn: boolean;
652
+ loadingHint: string;
653
+ statusLabel: string;
654
+ statusTone: PanelTone;
655
+ signedOutHint: string;
656
+ planLabel?: string;
657
+ planTitle?: string;
658
+ name?: string;
659
+ email?: string;
660
+ message?: string;
661
+ hasCredits: boolean;
662
+ creditsUnit: string;
663
+ creditsBalance: string;
664
+ creditsSub?: string;
665
+ creditsStale: boolean;
666
+ creditsTitle: string;
667
+ hasIncluded: boolean;
668
+ includedValue: string;
669
+ includedSub?: string;
670
+ showSeats: boolean;
671
+ seats: {
672
+ id: string;
673
+ label: string;
674
+ active: boolean;
675
+ inactive: boolean;
676
+ }[];
677
+ showEntitlements: boolean;
678
+ entitlements: {
679
+ id: string;
680
+ label: string;
681
+ detail?: string;
682
+ tone: PanelTone;
683
+ text: string;
684
+ }[];
685
+ showLedger: boolean;
686
+ ledgerCollapsed: boolean;
687
+ ledgerCount?: string;
688
+ hasLedger: boolean;
689
+ ledgerEmpty: boolean;
690
+ ledger: {
691
+ id: string;
692
+ description: string;
693
+ source?: string;
694
+ amount: string;
695
+ }[];
696
+ ledgerHasMore: boolean;
697
+ renews?: string;
698
+ offlineNote?: string;
699
+ }
700
+ interface AccountPanelViewOptions {
701
+ showLedger?: boolean;
702
+ showEntitlements?: boolean;
703
+ emptyHint?: string;
704
+ }
705
+ declare function accountPanelView(state: AccountViewState, options?: AccountPanelViewOptions): AccountPanelViewModel;
706
+ declare const BEHAVIOURS$1: {
707
+ 'auth/accountPanelView': (inputs: Record<string, unknown>, config: Readonly<Record<string, unknown>>) => AccountPanelViewModel;
708
+ };
709
+ declare const ACCOUNT_DECLARATION: BlockDeclaration;
710
+
711
+ /** Every behaviour of the auth family — the gate's and the account's — for a host's block registry. */
712
+ declare const BEHAVIOURS: {
713
+ 'auth/accountPanelView': (inputs: Record<string, unknown>, config: Readonly<Record<string, unknown>>) => AccountPanelViewModel;
714
+ 'auth/authGateView': (inputs: Record<string, unknown>, config: Readonly<Record<string, unknown>>) => AuthGateView;
715
+ };
716
+
717
+ export { BEHAVIOURS$1 as ACCOUNT_BEHAVIOURS, ACCOUNT_DECLARATION, ACCOUNT_PANEL_ID, AUTH_GATE_DECLARATION, AUTH_GATE_INITIAL, AUTH_GATE_PANEL_ID, AccountController, type AccountControllerOptions, type AccountHostBridge, type AccountPanelState, AccountPanelView, type AccountPanelViewModel, type AccountPanelViewOptions, type AccountPanelViewProps, type AccountRenderContext, type AccountUiIntent, 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, STATUS_LABEL, STATUS_TONE, 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, accountPanelView, accountViewManifest, authGateManifest, authGatePanel, authGateView, createAccountPanel, createAuthGatePanel, creditsAge, deriveAuthGatePhase, formatAge, formatAmount, formatSince, hostContradiction, intentOffered, isSignedIn, mountXenoAuthGate, plateCopy, requiresStepUp };
@@ -318,6 +318,365 @@ var AUTH_GATE_DECLARATION = {
318
318
  }
319
319
  };
320
320
 
321
+ // src/auth/account/manifest.ts
322
+ import { WELL_KNOWN_PORT_SCHEMAS } from "@xenosystem/block-sdk";
323
+ var ACCOUNT_PANEL_ID = "xeno.core.account";
324
+ var accountManifest = {
325
+ id: ACCOUNT_PANEL_ID,
326
+ version: "0.1.0",
327
+ title: "Account",
328
+ icon: "user-round",
329
+ description: "Identity, plan, credits, entitlements and ledger \u2014 all host-pushed, never fetched. Balances carry the moment they were true. Sensitive actions are emitted as step-up intents, never performed.",
330
+ defaultSlot: "inspector",
331
+ inputs: [
332
+ {
333
+ id: "account",
334
+ name: "Account",
335
+ type: "object",
336
+ schema: WELL_KNOWN_PORT_SCHEMAS.ACCOUNT,
337
+ description: "{status, identity?, plan?, credits?, entitlements?, seats?, message?}. The host is authoritative. Contains NO token \u2014 see AUTH L9.",
338
+ multiple: false
339
+ },
340
+ {
341
+ id: "ledger",
342
+ name: "Ledger",
343
+ type: "object",
344
+ schema: WELL_KNOWN_PORT_SCHEMAS.LEDGER,
345
+ description: "{rev?, append?, replace?, clear?, hasMore?}. Append-oriented; `rev` gates out-of-order deliveries.",
346
+ multiple: true
347
+ }
348
+ ],
349
+ outputs: [
350
+ {
351
+ id: "action",
352
+ name: "Action",
353
+ type: "object",
354
+ schema: WELL_KNOWN_PORT_SCHEMAS.ACCOUNT_ACTION,
355
+ description: "{kind, seatId?, productId?, stepUp?}. An INTENT. `stepUp: true` marks an action AUTH L12 requires a fresh interactive re-auth for; the host must not satisfy it silently from the broker."
356
+ }
357
+ ],
358
+ commands: [
359
+ {
360
+ id: "refresh",
361
+ title: "Refresh",
362
+ description: "Ask the host to re-read the balance. Idempotent; no step-up.",
363
+ parameters: {}
364
+ },
365
+ {
366
+ id: "get_account",
367
+ title: "Get Account",
368
+ description: "Return the rendered account state \u2014 status, plan, balance and its asOf. Never a token.",
369
+ parameters: {}
370
+ },
371
+ {
372
+ id: "get_ledger",
373
+ title: "Get Ledger",
374
+ description: "Return the held ledger entries, newest first.",
375
+ parameters: {}
376
+ }
377
+ // NOTE: `signOut`, `purchase` and `manageBilling` are deliberately NOT commands. They require a
378
+ // fresh interactive re-auth (AUTH L12), and an agent-invocable command is by definition not
379
+ // interactive. Exposing them would make this panel the bypass for the step-up requirement.
380
+ ],
381
+ config: [
382
+ {
383
+ key: "showLedger",
384
+ label: "Show ledger",
385
+ type: "boolean",
386
+ defaultValue: true,
387
+ description: "Show recent credit activity."
388
+ },
389
+ {
390
+ key: "showEntitlements",
391
+ label: "Show entitlements",
392
+ type: "boolean",
393
+ defaultValue: true,
394
+ description: "List what the plan includes. Display only \u2014 entitlement is enforced server-side (AUTH L10)."
395
+ },
396
+ {
397
+ key: "staleAfterMs",
398
+ label: "Stale after (ms)",
399
+ type: "number",
400
+ defaultValue: 6e4,
401
+ description: "How old a balance may be before it is labelled stale."
402
+ },
403
+ {
404
+ key: "maxLedger",
405
+ label: "Max ledger entries",
406
+ type: "number",
407
+ defaultValue: 500,
408
+ description: "Entries retained in memory."
409
+ },
410
+ {
411
+ key: "emptyHint",
412
+ label: "Empty hint",
413
+ type: "text",
414
+ description: "Shown when no account state has arrived.",
415
+ placeholder: "Waiting for account state\u2026"
416
+ }
417
+ ],
418
+ capabilities: ["storage.local"],
419
+ sdk: "^1.1.0"
420
+ };
421
+
422
+ // src/auth/account/types.ts
423
+ var STEP_UP_ACTIONS = /* @__PURE__ */ new Set([
424
+ "signOut",
425
+ "manageBilling",
426
+ "purchase"
427
+ ]);
428
+ function isSignedIn(status) {
429
+ return status === "signedIn" || status === "refreshing" || status === "offline";
430
+ }
431
+ function requiresStepUp(kind) {
432
+ return STEP_UP_ACTIONS.has(kind);
433
+ }
434
+ function creditsAge(credits, now) {
435
+ if (!credits) return null;
436
+ return Math.max(0, now - credits.asOf);
437
+ }
438
+ function formatAmount(amount, unit = "") {
439
+ const sign = amount > 0 ? "+" : "";
440
+ const suffix = unit ? ` ${unit}` : "";
441
+ return `${sign}${amount.toLocaleString()}${suffix}`;
442
+ }
443
+ function formatAge(ms) {
444
+ if (ms < 6e4) return `${Math.round(ms / 1e3)}s ago`;
445
+ if (ms < 36e5) return `${Math.round(ms / 6e4)}m ago`;
446
+ if (ms < 864e5) return `${Math.round(ms / 36e5)}h ago`;
447
+ return `${Math.round(ms / 864e5)}d ago`;
448
+ }
449
+
450
+ // src/auth/account/declaration.ts
451
+ var accountViewManifest = {
452
+ id: ACCOUNT_PANEL_ID,
453
+ version: "1.0.0",
454
+ title: "Account",
455
+ category: "account",
456
+ layout: { slot: "side" },
457
+ inputs: [{ id: "view", type: "object", label: "View state", description: "The AccountViewState the controller derives from the host ports." }],
458
+ outputs: [{ id: "ui", type: "object", label: "UI intent", description: "{ kind, seatId?, collapsed? } \u2014 applied by the controller." }],
459
+ commands: []
460
+ };
461
+ var STATUS_LABEL = {
462
+ unknown: "Loading",
463
+ signedOut: "Signed out",
464
+ signedIn: "Signed in",
465
+ refreshing: "Refreshing",
466
+ expired: "Session expired",
467
+ offline: "Offline"
468
+ };
469
+ var STATUS_TONE = {
470
+ unknown: "neutral",
471
+ signedOut: "neutral",
472
+ signedIn: "success",
473
+ refreshing: "neutral",
474
+ expired: "warning",
475
+ offline: "warning"
476
+ };
477
+ function accountPanelView(state, options = {}) {
478
+ const showLedger = options.showLedger ?? true;
479
+ const showEntitlements = options.showEntitlements ?? true;
480
+ const { identity, plan, credits } = state;
481
+ const signedIn = state.status !== "unknown" && isSignedIn(state.status);
482
+ return {
483
+ loading: state.status === "unknown",
484
+ signedOut: state.status !== "unknown" && !isSignedIn(state.status),
485
+ signedIn,
486
+ loadingHint: options.emptyHint ?? "Waiting for account state\u2026",
487
+ statusLabel: STATUS_LABEL[state.status],
488
+ statusTone: STATUS_TONE[state.status],
489
+ signedOutHint: state.message ?? "Sign in to see your plan and credits.",
490
+ planLabel: plan?.label,
491
+ planTitle: plan ? `Plan: ${plan.label}` : void 0,
492
+ name: identity?.name ?? identity?.sub,
493
+ email: identity?.email,
494
+ message: state.message ?? void 0,
495
+ hasCredits: credits !== null,
496
+ creditsUnit: credits?.unit ?? "credits",
497
+ creditsBalance: credits ? credits.balance.toLocaleString() : "",
498
+ creditsSub: credits && state.creditsAgeMs !== null ? state.creditsStale ? `stale \u2014 as of ${formatAge(state.creditsAgeMs)}` : `as of ${formatAge(state.creditsAgeMs)}` : void 0,
499
+ creditsStale: state.creditsStale,
500
+ creditsTitle: "Pushed by the host, not fetched by this panel.",
501
+ hasIncluded: credits?.included !== void 0,
502
+ includedValue: credits?.included !== void 0 ? credits.included.toLocaleString() : "",
503
+ includedSub: credits?.used === void 0 ? void 0 : `${credits.used.toLocaleString()} used`,
504
+ showSeats: state.seats.length > 1,
505
+ seats: state.seats.map((s) => ({ id: s.id, label: s.label, active: !!s.active, inactive: !s.active })),
506
+ showEntitlements: showEntitlements && state.entitlements.length > 0,
507
+ entitlements: state.entitlements.map((e) => ({ id: e.id, label: e.label, detail: e.detail, tone: e.included ? "success" : "neutral", text: e.included ? "yes" : "no" })),
508
+ showLedger,
509
+ ledgerCollapsed: !state.ledgerExpanded,
510
+ ledgerCount: state.ledger.length > 0 ? String(state.ledger.length) : void 0,
511
+ hasLedger: state.ledger.length > 0,
512
+ ledgerEmpty: state.ledger.length === 0,
513
+ ledger: state.ledger.map((e) => ({ id: e.id, description: e.description, source: e.source, amount: formatAmount(e.amount) })),
514
+ ledgerHasMore: state.ledgerHasMore,
515
+ renews: plan?.renewsAt ? `renews ${new Date(plan.renewsAt).toLocaleDateString()}` : void 0,
516
+ offlineNote: state.status === "offline" ? "offline \u2014 showing last known" : void 0
517
+ };
518
+ }
519
+ var EMPTY = { status: "unknown", identity: null, plan: null, credits: null, entitlements: [], seats: [], message: null, ledger: [], ledgerHasMore: false, ledgerExpanded: false, creditsAgeMs: null, creditsStale: false };
520
+ var BEHAVIOURS2 = {
521
+ "auth/accountPanelView": (inputs, config) => accountPanelView(inputs.view ?? EMPTY, {
522
+ showLedger: typeof config.showLedger === "boolean" ? config.showLedger : void 0,
523
+ showEntitlements: typeof config.showEntitlements === "boolean" ? config.showEntitlements : void 0,
524
+ emptyHint: typeof config.emptyHint === "string" ? config.emptyHint : void 0
525
+ })
526
+ };
527
+ var ui = (kind) => ({ emit: "ui", value: { object: { kind: { value: kind } } } });
528
+ var text = (b) => ({ component: "text", props: { text: b } });
529
+ var badge = (b, when, props) => ({ component: "chrome/Badge", ...when ? { when } : {}, ...props ? { props } : {}, children: [text(b)] });
530
+ var ACCOUNT_DECLARATION = {
531
+ format: "xeno.block@1",
532
+ manifest: accountViewManifest,
533
+ behaviour: "auth/accountPanelView",
534
+ composition: {
535
+ component: "fragment",
536
+ children: [
537
+ { component: "chrome/EmptyState", when: { input: "view.loading" }, props: { title: { value: "Account" }, hint: { input: "view.loadingHint" } } },
538
+ {
539
+ component: "chrome/EmptyState",
540
+ when: { input: "view.signedOut" },
541
+ props: {
542
+ title: { input: "view.statusLabel" },
543
+ hint: { input: "view.signedOutHint" },
544
+ action: { node: { component: "chrome/TextButton", props: { strong: { value: true } }, on: { onClick: ui("signIn") }, children: [text({ value: "Sign in" })] } }
545
+ }
546
+ },
547
+ {
548
+ component: "chrome/Column",
549
+ when: { input: "view.signedIn" },
550
+ children: [
551
+ {
552
+ component: "chrome/Toolbar",
553
+ props: {
554
+ left: { node: { component: "chrome/ToolbarGroup", children: [badge({ input: "view.statusLabel" }, void 0, { tone: { input: "view.statusTone" } }), badge({ input: "view.planLabel" }, { input: "view.planLabel" }, { title: { input: "view.planTitle" } })] } },
555
+ right: { node: { component: "chrome/ToolbarGroup", props: { end: { value: true } }, children: [{ component: "chrome/TextButton", on: { onClick: ui("refresh") }, children: [text({ value: "Refresh" })] }] } }
556
+ }
557
+ },
558
+ {
559
+ component: "chrome/ScrollArea",
560
+ children: [
561
+ {
562
+ component: "chrome/Stack",
563
+ children: [
564
+ { component: "chrome/Text", props: { role: { value: "title" } }, children: [text({ input: "view.name" })] },
565
+ { component: "chrome/Text", when: { input: "view.email" }, props: { role: { value: "meta" } }, children: [text({ input: "view.email" })] },
566
+ { component: "chrome/Text", when: { input: "view.message" }, props: { role: { value: "meta" } }, children: [text({ input: "view.message" })] }
567
+ ]
568
+ },
569
+ {
570
+ component: "chrome/Stack",
571
+ when: { input: "view.hasCredits" },
572
+ props: { direction: { value: "row" }, pad: { value: "inset" } },
573
+ children: [
574
+ {
575
+ component: "chrome/StatTile",
576
+ props: {
577
+ label: { input: "view.creditsUnit" },
578
+ value: { input: "view.creditsBalance" },
579
+ sub: { input: "view.creditsSub" },
580
+ icon: { node: badge({ value: "stale" }, { input: "view.creditsStale" }, { tone: { value: "warning" } }) },
581
+ title: { input: "view.creditsTitle" }
582
+ }
583
+ },
584
+ { component: "chrome/StatTile", when: { input: "view.hasIncluded" }, props: { label: { value: "included" }, value: { input: "view.includedValue" }, sub: { input: "view.includedSub" } } }
585
+ ]
586
+ },
587
+ {
588
+ component: "chrome/Section",
589
+ when: { input: "view.showSeats" },
590
+ props: { title: { value: "Seats" }, flush: { value: true } },
591
+ children: [
592
+ {
593
+ component: "chrome/RowList",
594
+ children: [
595
+ {
596
+ component: "chrome/Row",
597
+ each: { input: "view.seats", key: "id" },
598
+ props: {
599
+ noIcon: { value: true },
600
+ label: { item: "label" },
601
+ trailing: {
602
+ node: {
603
+ component: "fragment",
604
+ children: [
605
+ badge({ value: "active" }, { item: "active" }),
606
+ { component: "chrome/TextButton", when: { item: "inactive" }, on: { onClick: { emit: "ui", value: { object: { kind: { value: "switchSeat" }, seatId: { item: "id" } } } } }, children: [text({ value: "Switch" })] }
607
+ ]
608
+ }
609
+ }
610
+ }
611
+ }
612
+ ]
613
+ }
614
+ ]
615
+ },
616
+ {
617
+ component: "chrome/Section",
618
+ when: { input: "view.showEntitlements" },
619
+ props: { title: { value: "Included" }, flush: { value: true } },
620
+ children: [
621
+ {
622
+ component: "chrome/RowList",
623
+ children: [
624
+ {
625
+ component: "chrome/Row",
626
+ each: { input: "view.entitlements", key: "id" },
627
+ props: { noIcon: { value: true }, label: { item: "label" }, meta: { item: "detail" }, trailing: { node: badge({ item: "text" }, void 0, { tone: { item: "tone" } }) } }
628
+ }
629
+ ]
630
+ }
631
+ ]
632
+ },
633
+ {
634
+ component: "chrome/Section",
635
+ when: { input: "view.showLedger" },
636
+ props: { title: { value: "Recent activity" }, flush: { value: true }, collapsed: { input: "view.ledgerCollapsed" }, badge: { node: badge({ input: "view.ledgerCount" }, { input: "view.ledgerCount" }) } },
637
+ on: { onCollapsedChange: { emit: "ui", value: { object: { kind: { value: "ledgerCollapsed" }, collapsed: { arg: "" } } } } },
638
+ children: [
639
+ { component: "chrome/EmptyState", when: { input: "view.ledgerEmpty" }, props: { title: { value: "No activity" }, hint: { value: "Credit spends and grants appear here." } } },
640
+ {
641
+ component: "chrome/RowList",
642
+ when: { input: "view.hasLedger" },
643
+ children: [
644
+ {
645
+ component: "chrome/Row",
646
+ each: { input: "view.ledger", key: "id" },
647
+ props: { noIcon: { value: true }, label: { item: "description" }, meta: { item: "source" }, trailing: { node: { component: "chrome/Text", props: { role: { value: "number" } }, children: [text({ item: "amount" })] } } }
648
+ }
649
+ ]
650
+ },
651
+ { component: "chrome/Stack", when: { input: "view.ledgerHasMore" }, props: { pad: { value: "inset" } }, children: [{ component: "chrome/Text", props: { role: { value: "caption" } }, children: [text({ value: "Older entries are not held by this panel." })] }] }
652
+ ]
653
+ }
654
+ ]
655
+ },
656
+ { component: "chrome/Stack", props: { pad: { value: "inset" } }, children: [{ component: "chrome/Text", props: { role: { value: "caption" } }, children: [text({ value: "Billing and sign-out ask you to confirm who you are." })] }] },
657
+ {
658
+ component: "chrome/Toolbar",
659
+ props: {
660
+ divided: { value: false },
661
+ right: {
662
+ node: {
663
+ component: "chrome/ToolbarGroup",
664
+ props: { end: { value: true } },
665
+ children: [
666
+ { component: "chrome/TextButton", on: { onClick: ui("manageBilling") }, children: [text({ value: "Billing\u2026" })] },
667
+ { component: "chrome/TextButton", on: { onClick: ui("signOut") }, children: [text({ value: "Sign out\u2026" })] }
668
+ ]
669
+ }
670
+ }
671
+ }
672
+ },
673
+ { component: "chrome/StatusBar", props: { left: { input: "view.renews" }, right: { input: "view.offlineNote" } } }
674
+ ]
675
+ }
676
+ ]
677
+ }
678
+ };
679
+
321
680
  // src/auth/gate/mount.ts
322
681
  import { createElement as createElement2 } from "react";
323
682
  import { createRoot as createRoot2 } from "react-dom/client";
@@ -355,34 +714,6 @@ import {
355
714
  bindConfig as bindConfig2
356
715
  } from "@xenosystem/block-sdk";
357
716
 
358
- // src/auth/account/types.ts
359
- var STEP_UP_ACTIONS = /* @__PURE__ */ new Set([
360
- "signOut",
361
- "manageBilling",
362
- "purchase"
363
- ]);
364
- function isSignedIn(status) {
365
- return status === "signedIn" || status === "refreshing" || status === "offline";
366
- }
367
- function requiresStepUp(kind) {
368
- return STEP_UP_ACTIONS.has(kind);
369
- }
370
- function creditsAge(credits, now) {
371
- if (!credits) return null;
372
- return Math.max(0, now - credits.asOf);
373
- }
374
- function formatAmount(amount, unit = "") {
375
- const sign = amount > 0 ? "+" : "";
376
- const suffix = unit ? ` ${unit}` : "";
377
- return `${sign}${amount.toLocaleString()}${suffix}`;
378
- }
379
- function formatAge(ms) {
380
- if (ms < 6e4) return `${Math.round(ms / 1e3)}s ago`;
381
- if (ms < 36e5) return `${Math.round(ms / 6e4)}m ago`;
382
- if (ms < 864e5) return `${Math.round(ms / 36e5)}h ago`;
383
- return `${Math.round(ms / 864e5)}d ago`;
384
- }
385
-
386
717
  // src/auth/account/controller.ts
387
718
  var EMPTY_STATE = { status: "unknown" };
388
719
  function ageBucket(ms) {
@@ -593,139 +924,25 @@ var AccountController = class {
593
924
  }
594
925
  };
595
926
 
596
- // src/auth/account/manifest.ts
597
- import { WELL_KNOWN_PORT_SCHEMAS } from "@xenosystem/block-sdk";
598
- var ACCOUNT_PANEL_ID = "xeno.core.account";
599
- var accountManifest = {
600
- id: ACCOUNT_PANEL_ID,
601
- version: "0.1.0",
602
- title: "Account",
603
- icon: "user-round",
604
- description: "Identity, plan, credits, entitlements and ledger \u2014 all host-pushed, never fetched. Balances carry the moment they were true. Sensitive actions are emitted as step-up intents, never performed.",
605
- defaultSlot: "inspector",
606
- inputs: [
607
- {
608
- id: "account",
609
- name: "Account",
610
- type: "object",
611
- schema: WELL_KNOWN_PORT_SCHEMAS.ACCOUNT,
612
- description: "{status, identity?, plan?, credits?, entitlements?, seats?, message?}. The host is authoritative. Contains NO token \u2014 see AUTH L9.",
613
- multiple: false
614
- },
615
- {
616
- id: "ledger",
617
- name: "Ledger",
618
- type: "object",
619
- schema: WELL_KNOWN_PORT_SCHEMAS.LEDGER,
620
- description: "{rev?, append?, replace?, clear?, hasMore?}. Append-oriented; `rev` gates out-of-order deliveries.",
621
- multiple: true
622
- }
623
- ],
624
- outputs: [
625
- {
626
- id: "action",
627
- name: "Action",
628
- type: "object",
629
- schema: WELL_KNOWN_PORT_SCHEMAS.ACCOUNT_ACTION,
630
- description: "{kind, seatId?, productId?, stepUp?}. An INTENT. `stepUp: true` marks an action AUTH L12 requires a fresh interactive re-auth for; the host must not satisfy it silently from the broker."
631
- }
632
- ],
633
- commands: [
634
- {
635
- id: "refresh",
636
- title: "Refresh",
637
- description: "Ask the host to re-read the balance. Idempotent; no step-up.",
638
- parameters: {}
639
- },
640
- {
641
- id: "get_account",
642
- title: "Get Account",
643
- description: "Return the rendered account state \u2014 status, plan, balance and its asOf. Never a token.",
644
- parameters: {}
645
- },
646
- {
647
- id: "get_ledger",
648
- title: "Get Ledger",
649
- description: "Return the held ledger entries, newest first.",
650
- parameters: {}
651
- }
652
- // NOTE: `signOut`, `purchase` and `manageBilling` are deliberately NOT commands. They require a
653
- // fresh interactive re-auth (AUTH L12), and an agent-invocable command is by definition not
654
- // interactive. Exposing them would make this panel the bypass for the step-up requirement.
655
- ],
656
- config: [
657
- {
658
- key: "showLedger",
659
- label: "Show ledger",
660
- type: "boolean",
661
- defaultValue: true,
662
- description: "Show recent credit activity."
663
- },
664
- {
665
- key: "showEntitlements",
666
- label: "Show entitlements",
667
- type: "boolean",
668
- defaultValue: true,
669
- description: "List what the plan includes. Display only \u2014 entitlement is enforced server-side (AUTH L10)."
670
- },
671
- {
672
- key: "staleAfterMs",
673
- label: "Stale after (ms)",
674
- type: "number",
675
- defaultValue: 6e4,
676
- description: "How old a balance may be before it is labelled stale."
677
- },
678
- {
679
- key: "maxLedger",
680
- label: "Max ledger entries",
681
- type: "number",
682
- defaultValue: 500,
683
- description: "Entries retained in memory."
684
- },
685
- {
686
- key: "emptyHint",
687
- label: "Empty hint",
688
- type: "text",
689
- description: "Shown when no account state has arrived.",
690
- placeholder: "Waiting for account state\u2026"
691
- }
692
- ],
693
- capabilities: ["storage.local"],
694
- sdk: "^1.1.0"
695
- };
696
-
697
927
  // src/auth/account/react/AccountPanelView.tsx
698
928
  import { useSyncExternalStore } from "react";
699
929
  import {
700
930
  Badge,
931
+ Column,
701
932
  EmptyState,
702
933
  Row,
703
934
  RowList,
704
935
  ScrollArea,
705
936
  Section,
706
937
  StatTile,
938
+ Stack,
707
939
  StatusBar,
940
+ Text,
708
941
  TextButton,
709
942
  Toolbar,
710
943
  ToolbarGroup
711
944
  } from "@xenosystem/components/chrome";
712
945
  import { Fragment as Fragment2, jsx as jsx2, jsxs } from "react/jsx-runtime";
713
- var STATUS_LABEL = {
714
- unknown: "Loading",
715
- signedOut: "Signed out",
716
- signedIn: "Signed in",
717
- refreshing: "Refreshing",
718
- expired: "Session expired",
719
- offline: "Offline"
720
- };
721
- var STATUS_TONE = {
722
- unknown: "neutral",
723
- signedOut: "neutral",
724
- signedIn: "success",
725
- refreshing: "neutral",
726
- expired: "warning",
727
- offline: "warning"
728
- };
729
946
  function AccountPanelView({
730
947
  controller,
731
948
  showLedger = true,
@@ -733,108 +950,81 @@ function AccountPanelView({
733
950
  emptyHint
734
951
  }) {
735
952
  const state = useSyncExternalStore(controller.subscribe, controller.getState, controller.getState);
736
- if (state.status === "unknown") {
737
- return /* @__PURE__ */ jsx2(EmptyState, { title: "Account", hint: emptyHint ?? "Waiting for account state\u2026" });
738
- }
739
- if (!isSignedIn(state.status)) {
953
+ const v = accountPanelView(state, { showLedger, showEntitlements, emptyHint });
954
+ if (v.loading) return /* @__PURE__ */ jsx2(EmptyState, { title: "Account", hint: v.loadingHint });
955
+ if (v.signedOut) {
740
956
  return /* @__PURE__ */ jsx2(
741
957
  EmptyState,
742
958
  {
743
- title: STATUS_LABEL[state.status],
744
- hint: state.message ?? "Sign in to see your plan and credits.",
959
+ title: v.statusLabel,
960
+ hint: v.signedOutHint,
745
961
  action: /* @__PURE__ */ jsx2(TextButton, { strong: true, onClick: () => controller.request("signIn"), children: "Sign in" })
746
962
  }
747
963
  );
748
964
  }
749
- const { identity, plan, credits } = state;
750
- const unit = credits?.unit ?? "credits";
751
- return /* @__PURE__ */ jsxs("div", { style: { display: "flex", flexDirection: "column", height: "100%", minHeight: 0 }, children: [
965
+ return /* @__PURE__ */ jsxs(Column, { children: [
752
966
  /* @__PURE__ */ jsx2(
753
967
  Toolbar,
754
968
  {
755
- left: /* @__PURE__ */ jsxs(Fragment2, { children: [
756
- /* @__PURE__ */ jsx2(Badge, { tone: STATUS_TONE[state.status], children: STATUS_LABEL[state.status] }),
757
- plan ? /* @__PURE__ */ jsx2(Badge, { title: `Plan: ${plan.label}`, children: plan.label }) : null
969
+ left: /* @__PURE__ */ jsxs(ToolbarGroup, { children: [
970
+ /* @__PURE__ */ jsx2(Badge, { tone: v.statusTone, children: v.statusLabel }),
971
+ v.planLabel ? /* @__PURE__ */ jsx2(Badge, { title: v.planTitle, children: v.planLabel }) : null
758
972
  ] }),
759
973
  right: /* @__PURE__ */ jsx2(ToolbarGroup, { end: true, children: /* @__PURE__ */ jsx2(TextButton, { onClick: () => controller.refresh(), children: "Refresh" }) })
760
974
  }
761
975
  ),
762
976
  /* @__PURE__ */ jsxs(ScrollArea, { children: [
763
- /* @__PURE__ */ jsxs("div", { style: { padding: 8, display: "flex", flexDirection: "column", gap: 2 }, children: [
764
- /* @__PURE__ */ jsx2("div", { style: { fontSize: 12, fontWeight: 600 }, children: identity?.name ?? identity?.sub }),
765
- identity?.email ? /* @__PURE__ */ jsx2("div", { style: { fontSize: 10, opacity: 0.65 }, children: identity.email }) : null,
766
- state.message ? /* @__PURE__ */ jsx2("div", { style: { fontSize: 10, opacity: 0.75, marginTop: 4 }, children: state.message }) : null
977
+ /* @__PURE__ */ jsxs(Stack, { children: [
978
+ /* @__PURE__ */ jsx2(Text, { role: "title", children: v.name }),
979
+ v.email ? /* @__PURE__ */ jsx2(Text, { role: "meta", children: v.email }) : null,
980
+ v.message ? /* @__PURE__ */ jsx2(Text, { role: "meta", children: v.message }) : null
767
981
  ] }),
768
- credits ? /* @__PURE__ */ jsxs("div", { style: { display: "flex", gap: 6, padding: "0 8px 8px" }, children: [
982
+ v.hasCredits ? /* @__PURE__ */ jsxs(Stack, { direction: "row", pad: "inset", children: [
769
983
  /* @__PURE__ */ jsx2(
770
984
  StatTile,
771
985
  {
772
- label: unit,
773
- value: credits.balance.toLocaleString(),
774
- sub: state.creditsAgeMs === null ? void 0 : state.creditsStale ? `as of ${formatAge(state.creditsAgeMs)} \u2014 may be out of date` : `as of ${formatAge(state.creditsAgeMs)}`,
775
- icon: state.creditsStale ? /* @__PURE__ */ jsx2(Badge, { tone: "warning", children: "stale" }) : void 0,
776
- title: "Pushed by the host, not fetched by this panel."
986
+ label: v.creditsUnit,
987
+ value: v.creditsBalance,
988
+ sub: v.creditsSub,
989
+ icon: v.creditsStale ? /* @__PURE__ */ jsx2(Badge, { tone: "warning", children: "stale" }) : null,
990
+ title: v.creditsTitle
777
991
  }
778
992
  ),
779
- credits.included !== void 0 ? /* @__PURE__ */ jsx2(
780
- StatTile,
781
- {
782
- label: "included",
783
- value: credits.included.toLocaleString(),
784
- sub: credits.used === void 0 ? void 0 : `${credits.used.toLocaleString()} used`
785
- }
786
- ) : null
993
+ v.hasIncluded ? /* @__PURE__ */ jsx2(StatTile, { label: "included", value: v.includedValue, sub: v.includedSub }) : null
787
994
  ] }) : null,
788
- state.seats.length > 1 ? /* @__PURE__ */ jsx2(Section, { title: "Seats", flush: true, children: /* @__PURE__ */ jsx2(RowList, { children: state.seats.map((seat) => /* @__PURE__ */ jsx2(
995
+ v.showSeats ? /* @__PURE__ */ jsx2(Section, { title: "Seats", flush: true, children: /* @__PURE__ */ jsx2(RowList, { children: v.seats.map((seat) => /* @__PURE__ */ jsx2(
789
996
  Row,
790
997
  {
791
998
  noIcon: true,
792
999
  label: seat.label,
793
- trailing: seat.active ? /* @__PURE__ */ jsx2(Badge, { children: "active" }) : /* @__PURE__ */ jsx2(
794
- TextButton,
795
- {
796
- onClick: () => controller.request("switchSeat", { seatId: seat.id }),
797
- children: "Switch"
798
- }
799
- )
1000
+ trailing: /* @__PURE__ */ jsxs(Fragment2, { children: [
1001
+ seat.active ? /* @__PURE__ */ jsx2(Badge, { children: "active" }) : null,
1002
+ seat.inactive ? /* @__PURE__ */ jsx2(TextButton, { onClick: () => controller.request("switchSeat", { seatId: seat.id }), children: "Switch" }) : null
1003
+ ] })
800
1004
  },
801
1005
  seat.id
802
1006
  )) }) }) : null,
803
- showEntitlements && state.entitlements.length > 0 ? /* @__PURE__ */ jsx2(Section, { title: "Included", flush: true, children: /* @__PURE__ */ jsx2(RowList, { children: state.entitlements.map((ent) => /* @__PURE__ */ jsx2(
804
- Row,
805
- {
806
- noIcon: true,
807
- label: ent.label,
808
- meta: ent.detail,
809
- trailing: /* @__PURE__ */ jsx2(Badge, { tone: ent.included ? "success" : "neutral", children: ent.included ? "yes" : "no" })
810
- },
811
- ent.id
1007
+ v.showEntitlements ? /* @__PURE__ */ jsx2(Section, { title: "Included", flush: true, children: /* @__PURE__ */ jsx2(RowList, { children: v.entitlements.map((ent) => (
1008
+ // A fact about the plan, not a permission check. The gate is server-side (L10).
1009
+ /* @__PURE__ */ jsx2(Row, { noIcon: true, label: ent.label, meta: ent.detail, trailing: /* @__PURE__ */ jsx2(Badge, { tone: ent.tone, children: ent.text }) }, ent.id)
812
1010
  )) }) }) : null,
813
- showLedger ? /* @__PURE__ */ jsxs(
1011
+ v.showLedger ? /* @__PURE__ */ jsxs(
814
1012
  Section,
815
1013
  {
816
1014
  title: "Recent activity",
817
1015
  flush: true,
818
- collapsed: !state.ledgerExpanded,
1016
+ collapsed: v.ledgerCollapsed,
819
1017
  onCollapsedChange: (collapsed) => controller.setLedgerExpanded(!collapsed),
820
- badge: state.ledger.length > 0 ? /* @__PURE__ */ jsx2(Badge, { children: state.ledger.length }) : void 0,
1018
+ badge: v.ledgerCount ? /* @__PURE__ */ jsx2(Badge, { children: v.ledgerCount }) : null,
821
1019
  children: [
822
- state.ledger.length === 0 ? /* @__PURE__ */ jsx2(EmptyState, { title: "No activity", hint: "Credit spends and grants appear here." }) : /* @__PURE__ */ jsx2(RowList, { children: state.ledger.map((entry) => /* @__PURE__ */ jsx2(
823
- Row,
824
- {
825
- noIcon: true,
826
- label: entry.description,
827
- meta: entry.source,
828
- trailing: /* @__PURE__ */ jsx2("span", { style: { fontVariantNumeric: "tabular-nums", fontSize: 11 }, children: formatAmount(entry.amount) })
829
- },
830
- entry.id
831
- )) }),
832
- state.ledgerHasMore ? /* @__PURE__ */ jsx2("div", { style: { padding: "2px 8px", fontSize: 9, opacity: 0.6 }, children: "Older entries are not held by this panel." }) : null
1020
+ v.ledgerEmpty ? /* @__PURE__ */ jsx2(EmptyState, { title: "No activity", hint: "Credit spends and grants appear here." }) : null,
1021
+ v.hasLedger ? /* @__PURE__ */ jsx2(RowList, { children: v.ledger.map((entry) => /* @__PURE__ */ jsx2(Row, { noIcon: true, label: entry.description, meta: entry.source, trailing: /* @__PURE__ */ jsx2(Text, { role: "number", children: entry.amount }) }, entry.id)) }) : null,
1022
+ v.ledgerHasMore ? /* @__PURE__ */ jsx2(Stack, { pad: "inset", children: /* @__PURE__ */ jsx2(Text, { role: "caption", children: "Older entries are not held by this panel." }) }) : null
833
1023
  ]
834
1024
  }
835
1025
  ) : null
836
1026
  ] }),
837
- /* @__PURE__ */ jsx2("div", { style: { padding: "0 8px", fontSize: 9, opacity: 0.55 }, children: "Billing and sign-out ask you to confirm who you are." }),
1027
+ /* @__PURE__ */ jsx2(Stack, { pad: "inset", children: /* @__PURE__ */ jsx2(Text, { role: "caption", children: "Billing and sign-out ask you to confirm who you are." }) }),
838
1028
  /* @__PURE__ */ jsx2(
839
1029
  Toolbar,
840
1030
  {
@@ -845,13 +1035,7 @@ function AccountPanelView({
845
1035
  ] })
846
1036
  }
847
1037
  ),
848
- /* @__PURE__ */ jsx2(
849
- StatusBar,
850
- {
851
- left: plan?.renewsAt ? `renews ${new Date(plan.renewsAt).toLocaleDateString()}` : void 0,
852
- right: state.status === "offline" ? "offline \u2014 showing last known" : void 0
853
- }
854
- )
1038
+ /* @__PURE__ */ jsx2(StatusBar, { left: v.renews, right: v.offlineNote })
855
1039
  ] });
856
1040
  }
857
1041
 
@@ -955,7 +1139,12 @@ function createAccountPanel(options = {}) {
955
1139
  };
956
1140
  }
957
1141
  var accountPanel = createAccountPanel();
1142
+
1143
+ // src/auth/index.ts
1144
+ var BEHAVIOURS3 = { ...BEHAVIOURS, ...BEHAVIOURS2 };
958
1145
  export {
1146
+ BEHAVIOURS2 as ACCOUNT_BEHAVIOURS,
1147
+ ACCOUNT_DECLARATION,
959
1148
  ACCOUNT_PANEL_ID,
960
1149
  AUTH_GATE_DECLARATION,
961
1150
  AUTH_GATE_INITIAL,
@@ -964,11 +1153,15 @@ export {
964
1153
  AccountPanelView,
965
1154
  AuthGateController,
966
1155
  AuthGateView,
967
- BEHAVIOURS,
1156
+ BEHAVIOURS3 as BEHAVIOURS,
968
1157
  PHASE_INTENTS,
1158
+ STATUS_LABEL,
1159
+ STATUS_TONE,
969
1160
  STEP_UP_ACTIONS,
970
1161
  accountManifest,
971
1162
  accountPanel,
1163
+ accountPanelView,
1164
+ accountViewManifest,
972
1165
  authGateManifest,
973
1166
  authGatePanel,
974
1167
  authGateView,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xenosystem/blocks",
3
- "version": "0.10.0",
3
+ "version": "0.11.0",
4
4
  "description": "XENO Blocks — rung 3 of the front ladder: components composed into a contract-bearing unit (manifest: ports, intents, commands, agent surface), rendered by @xenosystem/workbench. ONE package, one subpath per family (/auth, /edit, /data, /time, /ops, /trust, /agent), one named export per block — XENO PACKAGE NAMING - STANDARD.md.",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",
@@ -60,7 +60,7 @@
60
60
  "peerDependencies": {
61
61
  "@duckdb/duckdb-wasm": ">=1.28.0",
62
62
  "@xenosystem/block-sdk": "^0.3.0",
63
- "@xenosystem/components": ">=0.9.2 <1",
63
+ "@xenosystem/components": ">=0.9.3 <1",
64
64
  "@xenosystem/data-core": "^0.1.0",
65
65
  "@xenosystem/elements-react": ">=0.0.1",
66
66
  "@xenosystem/time-core": "^0.1.0",
@@ -76,7 +76,7 @@
76
76
  "@types/react": "^19.0.0",
77
77
  "@types/react-dom": "^19.0.0",
78
78
  "@xenosystem/block-sdk": "^0.3.1",
79
- "@xenosystem/components": "^0.9.2",
79
+ "@xenosystem/components": "^0.9.3",
80
80
  "@xenosystem/data-core": "^0.1.0",
81
81
  "@xenosystem/elements-react": "0.0.1",
82
82
  "@xenosystem/time-core": "^0.1.0",