@xenosystem/blocks 0.10.1 → 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,107 +924,6 @@ 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 {
@@ -712,23 +942,7 @@ import {
712
942
  Toolbar,
713
943
  ToolbarGroup
714
944
  } from "@xenosystem/components/chrome";
715
- import { jsx as jsx2, jsxs } from "react/jsx-runtime";
716
- var STATUS_LABEL = {
717
- unknown: "Loading",
718
- signedOut: "Signed out",
719
- signedIn: "Signed in",
720
- refreshing: "Refreshing",
721
- expired: "Session expired",
722
- offline: "Offline"
723
- };
724
- var STATUS_TONE = {
725
- unknown: "neutral",
726
- signedOut: "neutral",
727
- signedIn: "success",
728
- refreshing: "neutral",
729
- expired: "warning",
730
- offline: "warning"
731
- };
945
+ import { Fragment as Fragment2, jsx as jsx2, jsxs } from "react/jsx-runtime";
732
946
  function AccountPanelView({
733
947
  controller,
734
948
  showLedger = true,
@@ -736,103 +950,76 @@ function AccountPanelView({
736
950
  emptyHint
737
951
  }) {
738
952
  const state = useSyncExternalStore(controller.subscribe, controller.getState, controller.getState);
739
- if (state.status === "unknown") {
740
- return /* @__PURE__ */ jsx2(EmptyState, { title: "Account", hint: emptyHint ?? "Waiting for account state\u2026" });
741
- }
742
- 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) {
743
956
  return /* @__PURE__ */ jsx2(
744
957
  EmptyState,
745
958
  {
746
- title: STATUS_LABEL[state.status],
747
- hint: state.message ?? "Sign in to see your plan and credits.",
959
+ title: v.statusLabel,
960
+ hint: v.signedOutHint,
748
961
  action: /* @__PURE__ */ jsx2(TextButton, { strong: true, onClick: () => controller.request("signIn"), children: "Sign in" })
749
962
  }
750
963
  );
751
964
  }
752
- const { identity, plan, credits } = state;
753
- const unit = credits?.unit ?? "credits";
754
965
  return /* @__PURE__ */ jsxs(Column, { children: [
755
966
  /* @__PURE__ */ jsx2(
756
967
  Toolbar,
757
968
  {
758
969
  left: /* @__PURE__ */ jsxs(ToolbarGroup, { children: [
759
- /* @__PURE__ */ jsx2(Badge, { tone: STATUS_TONE[state.status], children: STATUS_LABEL[state.status] }),
760
- plan ? /* @__PURE__ */ jsx2(Badge, { title: `Plan: ${plan.label}`, children: plan.label }) : null
970
+ /* @__PURE__ */ jsx2(Badge, { tone: v.statusTone, children: v.statusLabel }),
971
+ v.planLabel ? /* @__PURE__ */ jsx2(Badge, { title: v.planTitle, children: v.planLabel }) : null
761
972
  ] }),
762
973
  right: /* @__PURE__ */ jsx2(ToolbarGroup, { end: true, children: /* @__PURE__ */ jsx2(TextButton, { onClick: () => controller.refresh(), children: "Refresh" }) })
763
974
  }
764
975
  ),
765
976
  /* @__PURE__ */ jsxs(ScrollArea, { children: [
766
977
  /* @__PURE__ */ jsxs(Stack, { children: [
767
- /* @__PURE__ */ jsx2(Text, { role: "title", children: identity?.name ?? identity?.sub }),
768
- identity?.email ? /* @__PURE__ */ jsx2(Text, { role: "meta", children: identity.email }) : null,
769
- state.message ? /* @__PURE__ */ jsx2(Text, { role: "meta", children: state.message }) : null
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
770
981
  ] }),
771
- credits ? /* @__PURE__ */ jsxs(Stack, { direction: "row", pad: "inset", children: [
982
+ v.hasCredits ? /* @__PURE__ */ jsxs(Stack, { direction: "row", pad: "inset", children: [
772
983
  /* @__PURE__ */ jsx2(
773
984
  StatTile,
774
985
  {
775
- label: unit,
776
- value: credits.balance.toLocaleString(),
777
- sub: state.creditsAgeMs === null ? void 0 : state.creditsStale ? `as of ${formatAge(state.creditsAgeMs)} \u2014 may be out of date` : `as of ${formatAge(state.creditsAgeMs)}`,
778
- icon: state.creditsStale ? /* @__PURE__ */ jsx2(Badge, { tone: "warning", children: "stale" }) : void 0,
779
- 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
780
991
  }
781
992
  ),
782
- credits.included !== void 0 ? /* @__PURE__ */ jsx2(
783
- StatTile,
784
- {
785
- label: "included",
786
- value: credits.included.toLocaleString(),
787
- sub: credits.used === void 0 ? void 0 : `${credits.used.toLocaleString()} used`
788
- }
789
- ) : null
993
+ v.hasIncluded ? /* @__PURE__ */ jsx2(StatTile, { label: "included", value: v.includedValue, sub: v.includedSub }) : null
790
994
  ] }) : null,
791
- 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(
792
996
  Row,
793
997
  {
794
998
  noIcon: true,
795
999
  label: seat.label,
796
- trailing: seat.active ? /* @__PURE__ */ jsx2(Badge, { children: "active" }) : /* @__PURE__ */ jsx2(
797
- TextButton,
798
- {
799
- onClick: () => controller.request("switchSeat", { seatId: seat.id }),
800
- children: "Switch"
801
- }
802
- )
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
+ ] })
803
1004
  },
804
1005
  seat.id
805
1006
  )) }) }) : null,
806
- showEntitlements && state.entitlements.length > 0 ? /* @__PURE__ */ jsx2(Section, { title: "Included", flush: true, children: /* @__PURE__ */ jsx2(RowList, { children: state.entitlements.map((ent) => /* @__PURE__ */ jsx2(
807
- Row,
808
- {
809
- noIcon: true,
810
- label: ent.label,
811
- meta: ent.detail,
812
- trailing: /* @__PURE__ */ jsx2(Badge, { tone: ent.included ? "success" : "neutral", children: ent.included ? "yes" : "no" })
813
- },
814
- 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)
815
1010
  )) }) }) : null,
816
- showLedger ? /* @__PURE__ */ jsxs(
1011
+ v.showLedger ? /* @__PURE__ */ jsxs(
817
1012
  Section,
818
1013
  {
819
1014
  title: "Recent activity",
820
1015
  flush: true,
821
- collapsed: !state.ledgerExpanded,
1016
+ collapsed: v.ledgerCollapsed,
822
1017
  onCollapsedChange: (collapsed) => controller.setLedgerExpanded(!collapsed),
823
- 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,
824
1019
  children: [
825
- 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(
826
- Row,
827
- {
828
- noIcon: true,
829
- label: entry.description,
830
- meta: entry.source,
831
- trailing: /* @__PURE__ */ jsx2(Text, { role: "number", children: formatAmount(entry.amount) })
832
- },
833
- entry.id
834
- )) }),
835
- state.ledgerHasMore ? /* @__PURE__ */ jsx2(Stack, { pad: "inset", children: /* @__PURE__ */ jsx2(Text, { role: "caption", 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
836
1023
  ]
837
1024
  }
838
1025
  ) : null
@@ -848,13 +1035,7 @@ function AccountPanelView({
848
1035
  ] })
849
1036
  }
850
1037
  ),
851
- /* @__PURE__ */ jsx2(
852
- StatusBar,
853
- {
854
- left: plan?.renewsAt ? `renews ${new Date(plan.renewsAt).toLocaleDateString()}` : void 0,
855
- right: state.status === "offline" ? "offline \u2014 showing last known" : void 0
856
- }
857
- )
1038
+ /* @__PURE__ */ jsx2(StatusBar, { left: v.renews, right: v.offlineNote })
858
1039
  ] });
859
1040
  }
860
1041
 
@@ -958,7 +1139,12 @@ function createAccountPanel(options = {}) {
958
1139
  };
959
1140
  }
960
1141
  var accountPanel = createAccountPanel();
1142
+
1143
+ // src/auth/index.ts
1144
+ var BEHAVIOURS3 = { ...BEHAVIOURS, ...BEHAVIOURS2 };
961
1145
  export {
1146
+ BEHAVIOURS2 as ACCOUNT_BEHAVIOURS,
1147
+ ACCOUNT_DECLARATION,
962
1148
  ACCOUNT_PANEL_ID,
963
1149
  AUTH_GATE_DECLARATION,
964
1150
  AUTH_GATE_INITIAL,
@@ -967,11 +1153,15 @@ export {
967
1153
  AccountPanelView,
968
1154
  AuthGateController,
969
1155
  AuthGateView,
970
- BEHAVIOURS,
1156
+ BEHAVIOURS3 as BEHAVIOURS,
971
1157
  PHASE_INTENTS,
1158
+ STATUS_LABEL,
1159
+ STATUS_TONE,
972
1160
  STEP_UP_ACTIONS,
973
1161
  accountManifest,
974
1162
  accountPanel,
1163
+ accountPanelView,
1164
+ accountViewManifest,
975
1165
  authGateManifest,
976
1166
  authGatePanel,
977
1167
  authGateView,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xenosystem/blocks",
3
- "version": "0.10.1",
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",