@zerotal/inertia 1.0.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.
@@ -0,0 +1,82 @@
1
+ import { Command } from "@zerotal/core";
2
+ import { pruneBuildOutput } from "@zerotal/core/dev";
3
+ import { generatePageRegistry } from "../PageRegistry.ts";
4
+ import { detectCssPlugins } from "../css.ts";
5
+ import { detectVuePlugin } from "../vuePlugin.ts";
6
+
7
+ /**
8
+ * `inertia:build` — regenerate the page registry and bundle the frontend with
9
+ * `Bun.build` (code-split into per-page chunks) into `public/assets/`. Pass
10
+ * `--production` (`-p`) for a minified, source-map-free build.
11
+ *
12
+ * @category Build
13
+ * @example
14
+ * ```ts
15
+ * // Dev build:
16
+ * // bun zt inertia:build
17
+ * // Production build:
18
+ * // bun zt inertia:build --production
19
+ * ```
20
+ */
21
+ export class InertiaBuildCommand extends Command {
22
+ static override commandName = "inertia:build";
23
+ static override description =
24
+ "Build the Inertia frontend bundle and regenerate the page registry";
25
+ static override needsApp = false;
26
+
27
+ static override flags = [
28
+ {
29
+ name: "production",
30
+ short: "p",
31
+ type: "boolean" as const,
32
+ description: "Build for production (minified, no source maps)",
33
+ default: false,
34
+ },
35
+ ];
36
+
37
+ override async run(): Promise<void> {
38
+ const isProd = this.flags["production"] as boolean;
39
+ const cwd = process.cwd();
40
+
41
+ // Step 1: Regenerate the page registry
42
+ this.section("Regenerating page registry...");
43
+ await generatePageRegistry(cwd);
44
+ this.info("Page registry updated.");
45
+
46
+ // Step 2: Build the frontend bundle with Bun.build
47
+ this.section("Building frontend bundle...");
48
+
49
+ const plugins = [...(await detectCssPlugins(cwd)), ...(await detectVuePlugin(cwd))];
50
+ const outdir = `${cwd}/public/assets`;
51
+ const result = await Bun.build({
52
+ entrypoints: [`${cwd}/resources/js/app.tsx`],
53
+ outdir,
54
+ target: "browser",
55
+ splitting: true, // REQUIRED: creates per-page chunks from dynamic imports
56
+ minify: isProd,
57
+ sourcemap: isProd ? "none" : "external",
58
+ ...(plugins.length > 0 ? { plugins } : {}),
59
+ });
60
+
61
+ if (!result.success) {
62
+ for (const log of result.logs) {
63
+ this.error(String(log));
64
+ }
65
+ throw new Error("Frontend build failed.");
66
+ }
67
+
68
+ // Chunks are named after their content, so the ones this build replaced
69
+ // would otherwise stay behind — and ship.
70
+ const removed = await pruneBuildOutput(outdir, result.outputs);
71
+
72
+ this.info(`Build complete: ${result.outputs.length} files → public/assets/`);
73
+ if (removed.length > 0) this.dim(` Removed ${removed.length} stale file(s).`);
74
+
75
+ this.table(
76
+ result.outputs.map((o) => [
77
+ o.path.replace(cwd, "").replace(/\\/g, "/"),
78
+ `${(o.size / 1024).toFixed(1)} KB`,
79
+ ]) as [string, string][],
80
+ );
81
+ }
82
+ }
@@ -0,0 +1,207 @@
1
+ import { Command, config } from "@zerotal/core";
2
+ import { generatePageRegistry } from "../PageRegistry.ts";
3
+ import { DEFAULT_PAGES_DIR } from "../config.ts";
4
+
5
+ /**
6
+ * `make:page` — scaffold a new Inertia page component under the configured pages dir
7
+ * and regenerate the page registry. The framework (React `.tsx` / Vue `.vue`) is
8
+ * auto-detected from the installed adapter unless forced with `--framework`; pass
9
+ * `--layout` to wrap the page in a persistent layout.
10
+ *
11
+ * @category Scaffolding
12
+ * @example
13
+ * ```ts
14
+ * // bun zt make:page Users/Index
15
+ * // bun zt make:page Dashboard --layout MainLayout
16
+ * // bun zt make:page Settings --framework vue
17
+ * ```
18
+ */
19
+ export class MakePageCommand extends Command {
20
+ static override commandName = "make:page";
21
+ static override description = "Create a new Inertia page component";
22
+ static override needsApp = false;
23
+
24
+ static override args = [
25
+ {
26
+ name: "name",
27
+ required: true,
28
+ description: "Page name, e.g. Dashboard or Users/Index",
29
+ },
30
+ ];
31
+
32
+ static override flags = [
33
+ {
34
+ name: "layout",
35
+ type: "string" as const,
36
+ description: "Wrap the page in a persistent layout, e.g. --layout MainLayout",
37
+ default: "",
38
+ },
39
+ {
40
+ name: "framework",
41
+ type: "string" as const,
42
+ description: "Force the frontend framework: 'vue' or 'react' (auto-detected by default)",
43
+ default: "",
44
+ },
45
+ ];
46
+
47
+ override async run(): Promise<void> {
48
+ const name = this.args["name"]!; // e.g. 'Users/Index'
49
+ const layout = (this.flags["layout"] as string | undefined) ?? "";
50
+ const pagesDir = config
51
+ .safe("inertia.pagesDir", DEFAULT_PAGES_DIR)
52
+ .replace(/\\/g, "/")
53
+ .replace(/\/+$/, "");
54
+
55
+ // Pick the framework: explicit --framework flag, else auto-detect from the
56
+ // installed Inertia adapter (@inertiajs/vue3 → vue, otherwise react).
57
+ const forced = (this.flags["framework"] as string | undefined)?.toLowerCase() ?? "";
58
+ const framework: Framework =
59
+ forced === "vue" || forced === "react"
60
+ ? (forced as Framework)
61
+ : _detectFramework(process.cwd());
62
+ const ext = framework === "vue" ? "vue" : "tsx";
63
+
64
+ const path = `${pagesDir}/${name}.${ext}`;
65
+
66
+ if (await Bun.file(path).exists()) {
67
+ this.error(`File already exists: ${path}`);
68
+ return;
69
+ }
70
+
71
+ const componentName = name.includes("/") ? name.split("/").pop()! : name;
72
+
73
+ // Compute how many levels deep the page is so the layout import is correct
74
+ const depth = name.split("/").length;
75
+ const prefix = "../".repeat(depth);
76
+ const stub =
77
+ framework === "vue"
78
+ ? layout
79
+ ? _vueLayoutPageStub(componentName, layout, prefix)
80
+ : _vuePageStub(componentName)
81
+ : layout
82
+ ? _reactLayoutPageStub(componentName, layout, prefix)
83
+ : _reactPageStub(componentName);
84
+
85
+ await Bun.write(path, stub);
86
+ this.info(`Created: ${path}`);
87
+
88
+ if (layout) {
89
+ this.dim(` Layout: ${prefix}layouts/${layout}.${ext}`);
90
+ this.dim(" Adjust the import path if your layouts live elsewhere.");
91
+ }
92
+
93
+ this.dim(" Regenerating page registry...");
94
+ await generatePageRegistry(process.cwd());
95
+ this.info("Page registry updated.");
96
+ }
97
+ }
98
+
99
+ type Framework = "vue" | "react";
100
+
101
+ /** Detect the frontend framework from the Inertia adapter installed in `cwd`. */
102
+ function _detectFramework(cwd: string): Framework {
103
+ try {
104
+ Bun.resolveSync("@inertiajs/vue3", cwd);
105
+ return "vue";
106
+ } catch {
107
+ return "react";
108
+ }
109
+ }
110
+
111
+ function _reactPageStub(name: string): string {
112
+ return `import { Link, usePage } from '@inertiajs/react';
113
+
114
+ // Define the props this page receives from the controller
115
+ interface Props {
116
+ // TODO: add your props here
117
+ }
118
+
119
+ export default function ${name}({}: Props) {
120
+ const { auth } = usePage<{ auth: { user: { name: string } | null } }>().props;
121
+
122
+ return (
123
+ <main>
124
+ <h1>${name}</h1>
125
+ {auth.user && <p>Hello, {auth.user.name}</p>}
126
+ <Link href="/">Home</Link>
127
+ </main>
128
+ );
129
+ }
130
+ `;
131
+ }
132
+
133
+ function _reactLayoutPageStub(name: string, layout: string, prefix: string): string {
134
+ return `import type { ReactNode } from 'react';
135
+ import { Link } from '@inertiajs/react';
136
+ // Adjust the path if your layouts directory is elsewhere
137
+ import ${layout} from '${prefix}layouts/${layout}.tsx';
138
+
139
+ // Define the props this page receives from the controller
140
+ interface Props {
141
+ // TODO: add your props here
142
+ }
143
+
144
+ function ${name}({}: Props) {
145
+ return (
146
+ <main>
147
+ <h1>${name}</h1>
148
+ <Link href="/">Home</Link>
149
+ </main>
150
+ );
151
+ }
152
+
153
+ // Inertia persistent layout — preserved across client-side navigations.
154
+ // The layout renders once; only the page content re-renders on navigation.
155
+ (${name} as { layout?: (page: ReactNode) => ReactNode }).layout =
156
+ (page) => <${layout}>{page}</${layout}>;
157
+
158
+ export default ${name};
159
+ `;
160
+ }
161
+
162
+ function _vuePageStub(name: string): string {
163
+ return `<script setup lang="ts">
164
+ import { Link, usePage } from '@inertiajs/vue3';
165
+
166
+ const page = usePage<{ auth: { user: { name: string } | null } }>();
167
+
168
+ // Define the props this page receives from the controller
169
+ defineProps<{
170
+ // TODO: add your props here
171
+ }>();
172
+ </script>
173
+
174
+ <template>
175
+ <main>
176
+ <h1>${name}</h1>
177
+ <p v-if="page.props.auth.user">Hello, {{ page.props.auth.user.name }}</p>
178
+ <Link href="/">Home</Link>
179
+ </main>
180
+ </template>
181
+ `;
182
+ }
183
+
184
+ function _vueLayoutPageStub(name: string, layout: string, prefix: string): string {
185
+ return `<script setup lang="ts">
186
+ import { Link } from '@inertiajs/vue3';
187
+ // Adjust the path if your layouts directory is elsewhere
188
+ import ${layout} from '${prefix}layouts/${layout}.vue';
189
+
190
+ // Inertia persistent layout — preserved across client-side navigations.
191
+ // The layout renders once; only the page content re-renders on navigation.
192
+ defineOptions({ layout: ${layout} });
193
+
194
+ // Define the props this page receives from the controller
195
+ defineProps<{
196
+ // TODO: add your props here
197
+ }>();
198
+ </script>
199
+
200
+ <template>
201
+ <main>
202
+ <h1>${name}</h1>
203
+ <Link href="/">Home</Link>
204
+ </main>
205
+ </template>
206
+ `;
207
+ }
@@ -0,0 +1,3 @@
1
+ /** The `@zerotal/inertia` CLI commands (`inertia:build`, `make:page`), registered by InertiaProvider. */
2
+ export { InertiaBuildCommand } from "./InertiaBuildCommand.ts";
3
+ export { MakePageCommand } from "./MakePageCommand.ts";
package/src/config.ts ADDED
@@ -0,0 +1,82 @@
1
+ import { deepMerge } from "@zerotal/core";
2
+
3
+ export interface InertiaConfigShape {
4
+ /** Path to the HTML template file. Default: './resources/app.html' */
5
+ htmlTemplate: string;
6
+ /** Asset version string for cache-busting. Default: '1' */
7
+ version: string;
8
+ /** Public URL prefix for built assets. Default: '/' */
9
+ assetsUrl: string;
10
+ /**
11
+ * Directory (relative to the project root) where Inertia page components live.
12
+ * Used by the page-registry generator, the SSR handler, and `inertiaStream()`.
13
+ * Default: 'resources/js/pages'
14
+ */
15
+ pagesDir: string;
16
+ /**
17
+ * Enable server-side rendering.
18
+ *
19
+ * When true, InertiaProvider registers POST /__ssr which accepts
20
+ * { component, props, url } and returns { body, head } - the same
21
+ * contract as the Inertia Node SSR server. The Inertia client calls
22
+ * this endpoint when rendering the first page load on the server.
23
+ *
24
+ * Pages are rendered with the framework they're authored in: React `.tsx`
25
+ * via `react-dom/server`, or Vue `.vue` via `@inertiajs/vue3` + `vue/server-renderer`.
26
+ * Install the server renderer for the framework(s) the app uses.
27
+ *
28
+ * Default: false
29
+ */
30
+ ssr: boolean;
31
+ /**
32
+ * Shared secret required to reach `POST /__ssr` from off-box.
33
+ *
34
+ * The endpoint is loopback-only by default, because it takes an arbitrary component name
35
+ * and a props bag and does real rendering work with them — upstream Inertia runs SSR as a
36
+ * separate process on a private port for exactly this reason. Set this (and send it as
37
+ * `X-Inertia-SSR-Secret`) only when the renderer runs on another host.
38
+ *
39
+ * Default: `""` — loopback only.
40
+ */
41
+ ssrSecret: string;
42
+ /**
43
+ * Encrypt browser history state by default for every page. Individual pages can still opt in/out
44
+ * per request via `Inertia.encryptHistory()` / `clearHistory()`. Default: false.
45
+ */
46
+ encryptHistory: boolean;
47
+ }
48
+
49
+ /** Default directory (relative to the project root) for Inertia page components. */
50
+ export const DEFAULT_PAGES_DIR = "resources/js/pages";
51
+
52
+ const defaults: InertiaConfigShape = {
53
+ htmlTemplate: "./resources/app.html",
54
+ version: "1",
55
+ assetsUrl: "/",
56
+ pagesDir: DEFAULT_PAGES_DIR,
57
+ ssr: false,
58
+ ssrSecret: "",
59
+ encryptHistory: false,
60
+ };
61
+
62
+ /**
63
+ * Create a typed Inertia configuration object with defaults.
64
+ *
65
+ * @example
66
+ * import { InertiaConfig } from '@zerotal/inertia';
67
+ * export default InertiaConfig({
68
+ * htmlTemplate: './resources/app.html',
69
+ * version: Bun.env['ASSET_VERSION'] ?? '1',
70
+ * ssr: true, // enable SSR endpoint at POST /__ssr
71
+ * });
72
+ */
73
+ export function InertiaConfig(options: Partial<InertiaConfigShape> = {}): InertiaConfigShape {
74
+ return deepMerge(defaults, options);
75
+ }
76
+
77
+ // Register this package's config namespace for typed config() dot-paths.
78
+ declare module "@zerotal/core" {
79
+ interface ConfigRegistry {
80
+ inertia: InertiaConfigShape;
81
+ }
82
+ }
package/src/css.ts ADDED
@@ -0,0 +1,7 @@
1
+ /**
2
+ * CSS plugin detection for Inertia's Bun.build() pipeline.
3
+ *
4
+ * Re-exports from @zerotal/core so Pulse and other packages can share the same
5
+ * detection logic without creating cross-package dependencies.
6
+ */
7
+ export { detectCssPlugins } from "@zerotal/core/dev";
package/src/errors.ts ADDED
@@ -0,0 +1,40 @@
1
+ import { ZerotalError } from "@zerotal/core";
2
+
3
+ /** Base class for all @zerotal/inertia errors. */
4
+ export class InertiaError extends ZerotalError {
5
+ constructor(
6
+ message: string,
7
+ code = "E_INERTIA",
8
+ status = 500,
9
+ context?: Record<string, unknown>,
10
+ ) {
11
+ super(message, code, status, context);
12
+ }
13
+ }
14
+
15
+ /**
16
+ * Thrown when `inertia()` / `inertiaStream()` runs before the HTML template has been
17
+ * loaded — almost always because `InertiaProvider` is not registered.
18
+ */
19
+ export class InertiaTemplateNotLoadedError extends InertiaError {
20
+ constructor() {
21
+ super(
22
+ "[Zerotal Inertia] HTML template not loaded. Did you register InertiaProvider in bootstrap/app.ts?",
23
+ "E_INERTIA_TEMPLATE_NOT_LOADED",
24
+ );
25
+ }
26
+ }
27
+
28
+ /** Thrown when a component name is unsafe (path traversal) or has no resolvable default export. */
29
+ export class InvalidComponentError extends InertiaError {
30
+ constructor(component: string, reason = "is not a valid component") {
31
+ super(
32
+ `[Zerotal Inertia] Invalid component name '${component}': ${reason}.`,
33
+ "E_INERTIA_INVALID_COMPONENT",
34
+ 500,
35
+ {
36
+ component,
37
+ },
38
+ );
39
+ }
40
+ }
@@ -0,0 +1,55 @@
1
+ import { inertia, inertiaStream } from "../inertia.ts";
2
+ import { optional, lazy, always, defer, merge, deepMerge, scroll } from "../props/PropTypes.ts";
3
+ import { share } from "../share.ts";
4
+ import { encryptHistory, clearHistory } from "../historyState.ts";
5
+ import { location } from "../location.ts";
6
+
7
+ /**
8
+ * Unified Inertia facade.
9
+ *
10
+ * @example
11
+ * import { Inertia } from "@zerotal/inertia";
12
+ *
13
+ * async index(http: HttpContext) {
14
+ * return Inertia.render("Users/Index", {
15
+ * users: Inertia.optional(() => User.all()), // only on partial reload
16
+ * stats: Inertia.defer(() => computeStats()), // loaded after first paint
17
+ * feed: Inertia.merge(() => Post.paginate()), // append on "load more"
18
+ * });
19
+ * }
20
+ *
21
+ * Standalone helpers (`optional`, `always`, `defer`, `merge`, `deepMerge`, `lazy`) are also exported
22
+ * for direct import if you prefer not to go through the facade.
23
+ */
24
+ export const Inertia = {
25
+ /** Render an Inertia page (= the `inertia()` helper). */
26
+ render: inertia,
27
+ /** Render an Inertia page using React streaming SSR. */
28
+ stream: inertiaStream,
29
+
30
+ /** Prop only included when explicitly requested via a partial reload's `only`. */
31
+ optional,
32
+ /** Alias of {@link optional}. */
33
+ lazy,
34
+ /** Prop always included, even on partial reloads that would otherwise exclude it. */
35
+ always,
36
+ /** Prop deferred to a follow-up request after the initial render. */
37
+ defer,
38
+ /** Prop the client appends (merges) into existing data on partial reloads. */
39
+ merge,
40
+ /** Prop the client deep-merges into existing data on partial reloads. */
41
+ deepMerge,
42
+ /** Infinite-scroll prop: merges paginated data and emits scroll metadata. */
43
+ scroll,
44
+
45
+ /** Register shared props included on every page. */
46
+ share,
47
+
48
+ /** Encrypt the current page's browser history state. */
49
+ encryptHistory,
50
+ /** Clear any encrypted history state (e.g. on logout). */
51
+ clearHistory,
52
+
53
+ /** External redirect (full-page visit). */
54
+ location,
55
+ } as const;
@@ -0,0 +1,70 @@
1
+ // Ambient declarations specific to this package.
2
+ // Bun, Node (node:*), and bun:test types come from @types/bun (→ bun-types).
3
+ // Only declarations bun-types does NOT provide are kept here.
4
+
5
+ // ── Bun globals ───────────────────────────────────────────────────────────
6
+ interface Request {
7
+ readonly params?: Record<string, string>;
8
+ }
9
+
10
+ // ── SQLInstance ───────────────────────────────────────────────────────────
11
+ interface SQLInstance {
12
+ <T = Record<string, unknown>>(
13
+ strings: TemplateStringsArray,
14
+ ...values: unknown[]
15
+ ): Promise<T[]>;
16
+ begin<T>(fn: (tx: SQLInstance) => Promise<T>): Promise<T>;
17
+ end(): Promise<void>;
18
+ }
19
+
20
+ // ── React type stubs ──────────────────────────────────────────────────────
21
+ declare module 'react' {
22
+ export function createElement(type: unknown, props?: unknown, ...children: unknown[]): unknown;
23
+ export const version: string;
24
+ }
25
+
26
+ declare module 'react-dom/client' {
27
+ export function createRoot(container: Element | null): { render(el: unknown): void };
28
+ }
29
+
30
+ declare module 'react-dom/server' {
31
+ /** Render a React element to an HTML string (SSR). */
32
+ export function renderToString(element: unknown): string;
33
+ /** Render a React element to a static HTML string (no data-react attributes). */
34
+ export function renderToStaticMarkup(element: unknown): string;
35
+ /** Render a React element to a streaming ReadableStream (React 18+). */
36
+ export function renderToReadableStream(
37
+ element: unknown,
38
+ options?: {
39
+ signal?: AbortSignal;
40
+ onError?: (error: unknown) => void;
41
+ },
42
+ ): Promise<ReadableStream<Uint8Array>>;
43
+ }
44
+
45
+ // ── @inertiajs/react type stub ────────────────────────────────────────────
46
+ declare module '@inertiajs/react' {
47
+ export function createInertiaApp(options: {
48
+ resolve: (name: string) => unknown | Promise<unknown>;
49
+ setup: (args: { el: Element; App: unknown; props: unknown }) => void;
50
+ }): Promise<void>;
51
+
52
+ export function usePage<T = Record<string, unknown>>(): { props: T };
53
+ export function Link(props: { href: string; children?: unknown }): unknown;
54
+ export const router: {
55
+ visit(url: string): void;
56
+ post(url: string, data?: unknown): void;
57
+ put(url: string, data?: unknown): void;
58
+ delete(url: string): void;
59
+ };
60
+ export function useForm<T extends Record<string, unknown>>(initial: T): {
61
+ data: T;
62
+ errors: Partial<Record<keyof T, string>>;
63
+ processing: boolean;
64
+ setData(key: keyof T, value: unknown): void;
65
+ post(url: string): void;
66
+ put(url: string): void;
67
+ delete(url: string): void;
68
+ reset(): void;
69
+ };
70
+ }
@@ -0,0 +1,82 @@
1
+ import { RequestContext } from "@zerotal/core";
2
+
3
+ /**
4
+ * Per-request Inertia history-encryption flags.
5
+ *
6
+ * The server does not encrypt anything itself — encryption happens client-side. The server's job is
7
+ * to set the `encryptHistory` / `clearHistory` flags on the page object; the Inertia client reacts
8
+ * to them. Flags are stored per-request (keyed by the active HttpContext) so concurrent requests
9
+ * don't interfere.
10
+ */
11
+ interface HistoryFlags {
12
+ encrypt?: boolean;
13
+ clear?: boolean;
14
+ }
15
+
16
+ const _store = new WeakMap<object, HistoryFlags>();
17
+ let _defaultEncrypt = false;
18
+
19
+ /**
20
+ * Set the global default for history encryption (from `inertia.encryptHistory` config).
21
+ *
22
+ * @param on - When `true`, every page encrypts its history state unless overridden per request.
23
+ * @internal
24
+ */
25
+ export function setHistoryEncryptionDefault(on: boolean): void {
26
+ _defaultEncrypt = on;
27
+ }
28
+
29
+ function flagsForCurrent(): HistoryFlags {
30
+ const ctx = RequestContext.get() as unknown as object;
31
+ let flags = _store.get(ctx);
32
+ if (!flags) {
33
+ flags = {};
34
+ _store.set(ctx, flags);
35
+ }
36
+ return flags;
37
+ }
38
+
39
+ /**
40
+ * Encrypt the current page's history state in the browser (protects props cached in
41
+ * `history.state` for a page holding sensitive data). Effective for the current request.
42
+ *
43
+ * @param on - Pass `false` to opt this page out when encryption is the global default. Default `true`.
44
+ * @example
45
+ * ```ts
46
+ * async show(http: HttpContext): Promise<void> {
47
+ * Inertia.encryptHistory();
48
+ * return inertia('Account/Settings', { account });
49
+ * }
50
+ * ```
51
+ */
52
+ export function encryptHistory(on = true): void {
53
+ flagsForCurrent().encrypt = on;
54
+ }
55
+
56
+ /**
57
+ * Clear any previously encrypted history state — call on logout so cached page data
58
+ * can't be recovered via the browser Back button.
59
+ *
60
+ * @example
61
+ * ```ts
62
+ * async destroy(http: HttpContext): Promise<void> {
63
+ * await Auth.logout();
64
+ * Inertia.clearHistory();
65
+ * return redirect('/login');
66
+ * }
67
+ * ```
68
+ */
69
+ export function clearHistory(): void {
70
+ flagsForCurrent().clear = true;
71
+ }
72
+
73
+ /** @internal Resolve the effective flags for the current request, applying the global default. */
74
+ export function readHistoryFlags(): { encryptHistory?: boolean; clearHistory?: boolean } {
75
+ const ctx = RequestContext.tryGet() as unknown as object | undefined;
76
+ const flags = ctx ? _store.get(ctx) : undefined;
77
+ const encrypt = flags?.encrypt ?? _defaultEncrypt;
78
+ const out: { encryptHistory?: boolean; clearHistory?: boolean } = {};
79
+ if (encrypt) out.encryptHistory = true;
80
+ if (flags?.clear) out.clearHistory = true;
81
+ return out;
82
+ }