chore: session 4 polish — CI, lint, prestige UI, balance pass
CI / security (push) Failing after 4s
CI / test (push) Failing after 17s
CI / static (push) Failing after 19s

Adds the CI workflow, ESLint flat config, basic prestige action,
and tunes three off-balance numbers in the day-1 content.

CI runs three parallel jobs: `static` (lint, typecheck, prettier,
build), `test` (unit + Postgres-backed integration via service
container, with coverage gate), and `security` (gitleaks + trivy).
Scratch crypto keys are generated per-run from openssl so the
server can boot in CI without any committed secrets.

Prestige UI: a "git commit" / "git tag -a" button in each
prestigeable layer's canvas header, gated on the upstream `best`
threshold. Displays projected gain inline (`// +N `) so the
trade is visible before clicking.

Balance fixes:
- Releases requirement was 1000 commits (≈5B LoC, unreachable)
  with a gain formula that returned 0 at the threshold. Aligned
  both at 100 commits with prestigePoints exponent 0.5.
- Tech Debt accrual was 0.0005 × code/s (500 debt/s at 1M LoC).
  Reduced to 0.0001 — still meaningful, no longer punitive.
- Pay-Down Debt was effect=-10 per buyable, instantly turning the
  production multiplier to -9. Changed to -0.1 (each pay-down
  slows accrual by 10%; 10 pay-downs stops it; further buys
  reverse the flow). Also corrected costLayer to "code" so it
  spends LoC, not Debt.

Schema: added `costLayer` to Buyable to match Upgrade (needed for
the Tech Debt fix). RightPanel now honors `costLayer` for both
upgrades and buyables — the cost-side affordability check and the
spendAmount target both follow the override.

Verification:
- pnpm -r typecheck: clean
- pnpm -r lint: clean
- pnpm format:check: clean
- pnpm -r test: 73 unit tests pass
- pnpm --filter @teh-riehl/server test:integration: 7/7 pass
- 80 tests total

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-17 12:50:45 -07:00
parent 29f50ba845
commit 66a44354cd
40 changed files with 1183 additions and 185 deletions
+136
View File
@@ -0,0 +1,136 @@
name: CI
on:
push:
branches: [main]
pull_request:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
env:
NODE_VERSION: '20'
PNPM_VERSION: '9.12.0'
jobs:
# -----------------------------------------------------------------------------
# Static analysis. Runs in parallel with the test job — faster signal on lint
# / typecheck failures than waiting for Postgres to come up.
# -----------------------------------------------------------------------------
static:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: ${{ env.PNPM_VERSION }}
- uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'pnpm'
- name: Install
run: pnpm install --frozen-lockfile
- name: Prisma generate
run: pnpm --filter @teh-riehl/server prisma generate
- name: Lint
run: pnpm -r lint
- name: Typecheck
run: pnpm -r typecheck
- name: Format check
run: pnpm format:check
- name: Build
run: pnpm -r build
# -----------------------------------------------------------------------------
# Tests, including the Postgres-backed server integration suite.
# The coverage threshold gate fires here — if shared/server branches drop
# below 90/85 respectively, CI fails.
# -----------------------------------------------------------------------------
test:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:16-alpine
env:
POSTGRES_USER: teh_riehl
POSTGRES_PASSWORD: dev_password
POSTGRES_DB: teh_riehl
ports:
- 5432:5432
options: >-
--health-cmd "pg_isready -U teh_riehl"
--health-interval 5s
--health-timeout 5s
--health-retries 10
env:
DATABASE_URL: postgresql://teh_riehl:dev_password@localhost:5432/teh_riehl?schema=public
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: ${{ env.PNPM_VERSION }}
- uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'pnpm'
- name: Generate scratch crypto keys
# 32-byte KEK / EHK and 64-byte JWT secrets, fresh per run. These are
# CI-only. Production keys live in the deploy environment, never the repo.
run: |
echo "ENCRYPTION_MASTER_KEY=$(openssl rand -base64 32)" >> $GITHUB_ENV
echo "EMAIL_HASH_SECRET=$(openssl rand -base64 32)" >> $GITHUB_ENV
echo "JWT_SECRET=$(openssl rand -base64 64 | tr -d '\n')" >> $GITHUB_ENV
echo "JWT_REFRESH_SECRET=$(openssl rand -base64 64 | tr -d '\n')" >> $GITHUB_ENV
- name: Install
run: pnpm install --frozen-lockfile
- name: Prisma generate
run: pnpm --filter @teh-riehl/server prisma generate
- name: Prisma migrate (deploy)
run: pnpm --filter @teh-riehl/server prisma migrate deploy
- name: Unit tests (all packages, with coverage gate)
run: pnpm -r test:coverage
- name: Integration tests (server + real Postgres)
run: pnpm --filter @teh-riehl/server test:integration
# -----------------------------------------------------------------------------
# Secrets + dependency scans. Run on every commit; fail loudly on findings.
# -----------------------------------------------------------------------------
security:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # gitleaks needs history to scan PR-only diffs
- name: Gitleaks (secrets scan)
uses: gitleaks/gitleaks-action@v2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Trivy (dependency + filesystem scan)
uses: aquasecurity/trivy-action@0.28.0
with:
scan-type: fs
severity: CRITICAL,HIGH
ignore-unfixed: true
exit-code: '1'
+16 -17
View File
@@ -6,28 +6,28 @@ This document outlines the architecture, tech stack, game design, and developmen
To maximize player retention right out of the gate, the game will launch with a massive initial content drop, taking inspiration from the deep, multi-layered progression systems found in titles like Idleon. The core loop revolves around unlocking nodes on a highly interconnected tree structure.
* **Extensive Day-1 Content:** Multiple resource layers, prestige mechanics, and overlapping progression paths available at launch so players have diverse short-term and long-term goals.
* **Expansion-Ready Architecture:** The underlying data structure for the tree will be strictly modular. "Expansions" will function as drop-in content packs that bolt new sub-trees onto existing late-game nodes without requiring a hard reset of base calculations.
* **Integrated Patch Notes System:** A dedicated, stylish UI modal parsing markdown/JSON files directly from the repository. This keeps players informed of balancing tweaks and new branches seamlessly between versions.
- **Extensive Day-1 Content:** Multiple resource layers, prestige mechanics, and overlapping progression paths available at launch so players have diverse short-term and long-term goals.
- **Expansion-Ready Architecture:** The underlying data structure for the tree will be strictly modular. "Expansions" will function as drop-in content packs that bolt new sub-trees onto existing late-game nodes without requiring a hard reset of base calculations.
- **Integrated Patch Notes System:** A dedicated, stylish UI modal parsing markdown/JSON files directly from the repository. This keeps players informed of balancing tweaks and new branches seamlessly between versions.
## 2. Technical Architecture & Tech Stack
The application will be structured as a monorepo managed by pnpm, optimizing dependency installation and workspace management across both the client and server.
| Component | Technology | Implementation Strategy |
| --- | --- | --- |
| **Frontend Client** | React, TypeScript, Vite | Highly performant UI rendering utilizing strict TypeScript types to prevent runtime errors during deep tree calculations. |
| **Styling & UI Components** | TailwindCSS, ShadCN | Rapid, consistent design system. ShadCN will provide the accessible primitives (modals for patch notes, tooltips for tree nodes). |
| **State Management** | Redux | Centralized store for the complex game state, handling offline progress calculations, resource ticks, and node unlocks. |
| **Backend API** | NestJS | Enterprise-grade architecture for handling authentication, secure cloud saves, and potential future multiplayer features. |
| **Database** | PostgreSQL | Robust relational storage for user profiles, JSON save blobs, and game economy analytics. |
| Component | Technology | Implementation Strategy |
| --------------------------- | ----------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
| **Frontend Client** | React, TypeScript, Vite | Highly performant UI rendering utilizing strict TypeScript types to prevent runtime errors during deep tree calculations. |
| **Styling & UI Components** | TailwindCSS, ShadCN | Rapid, consistent design system. ShadCN will provide the accessible primitives (modals for patch notes, tooltips for tree nodes). |
| **State Management** | Redux | Centralized store for the complex game state, handling offline progress calculations, resource ticks, and node unlocks. |
| **Backend API** | NestJS | Enterprise-grade architecture for handling authentication, secure cloud saves, and potential future multiplayer features. |
| **Database** | PostgreSQL | Robust relational storage for user profiles, JSON save blobs, and game economy analytics. |
## 3. Development Methodology: Test-Driven
Given the mathematically intensive nature of incremental games, strict adherence to Test-Driven Development (TDD) ensures formulas remain stable as numbers scale exponentially.
* **TDD Workflow:** Tests for resource generation formulas, cost scaling, and unlock requirements will be written before the implementation logic.
* **Coverage Goal:** Strict enforcement of **90%+ unit test coverage** using Vitest/Jest, particularly focusing on the Redux reducers and the core math utility functions.
- **TDD Workflow:** Tests for resource generation formulas, cost scaling, and unlock requirements will be written before the implementation logic.
- **Coverage Goal:** Strict enforcement of **90%+ unit test coverage** using Vitest/Jest, particularly focusing on the Redux reducers and the core math utility functions.
## 4. CI/CD Pipeline (GitHub Actions)
@@ -56,12 +56,11 @@ jobs:
# 3. Testing
- name: Run Unit Tests with Coverage
- name: Enforce 90% Coverage Threshold
```
## 5. Next Steps
* Initialize the pnpm monorepo workspace.
* Scaffold the Vite/React frontend and NestJS backend.
* Draft the initial JSON/TypeScript structure for the base "Modding Tree" data format.
* Establish the GitHub Actions workflow file.
- Initialize the pnpm monorepo workspace.
- Scaffold the Vite/React frontend and NestJS backend.
- Draft the initial JSON/TypeScript structure for the base "Modding Tree" data format.
- Establish the GitHub Actions workflow file.
+49 -19
View File
@@ -2,7 +2,7 @@
A meta/dev-themed incremental game. You play a developer; resources are Lines of Code, Commits, Coffee, Closed Tickets, Releases, and Tech Debt. Built on a TMT-compatible (The Modding Tree) JSON content schema with a React + Redux frontend and a NestJS + Postgres backend.
> **Status**: Pre-alpha. Foundation only. See [`ProjectPlan.md`](./ProjectPlan.md) for the design plan and [`CLAUDE.md`](./CLAUDE.md) for AI-assistant working notes.
> **Status**: v0.1 — playable. Six day-1 layers, full auth + cloud saves with envelope encryption, Tailwind-themed IDE UI. See [`ProjectPlan.md`](./ProjectPlan.md) for the original design plan.
---
@@ -34,34 +34,47 @@ The shared package is the contract between client and server: both import the sa
# 1. Install dependencies
pnpm install
# 2. Copy env file and regenerate secrets
# 2. Generate local-dev secrets into .env. The example file ships with
# REPLACE_ME placeholders; the snippet below picks fresh keys.
cp .env.example .env
# Then edit .env. For local dev you can use these one-liners:
# openssl rand -base64 32 # ENCRYPTION_MASTER_KEY
# openssl rand -base64 32 # EMAIL_HASH_SECRET
# openssl rand -base64 64 # JWT_SECRET, JWT_REFRESH_SECRET
node -e "
const c=require('node:crypto'),fs=require('node:fs'),p='.env';
let e=fs.readFileSync(p,'utf8')
.replace(/^ENCRYPTION_MASTER_KEY=.*/m,'ENCRYPTION_MASTER_KEY=\"'+c.randomBytes(32).toString('base64')+'\"')
.replace(/^EMAIL_HASH_SECRET=.*/m,'EMAIL_HASH_SECRET=\"'+c.randomBytes(32).toString('base64')+'\"')
.replace(/^JWT_SECRET=.*/m,'JWT_SECRET=\"'+c.randomBytes(64).toString('base64')+'\"')
.replace(/^JWT_REFRESH_SECRET=.*/m,'JWT_REFRESH_SECRET=\"'+c.randomBytes(64).toString('base64')+'\"');
fs.writeFileSync(p,e);
"
# 3. Start Postgres
docker compose up -d
# 4. Run migrations (once the server package exists)
# 4. Generate Prisma client + apply migrations
pnpm --filter @teh-riehl/server prisma generate
pnpm --filter @teh-riehl/server prisma migrate dev
# 5. Start everything
# 5. Start everything (server on :3000, client on :5173)
pnpm dev
```
Open <http://localhost:5173>. Sign up, then play. Saves sync to Postgres every 30s; localStorage holds the truth in between.
## Useful scripts
| Command | What it does |
| --- | --- |
| `pnpm dev` | Run all packages in dev mode (parallel) |
| `pnpm build` | Build all packages |
| `pnpm test` | Run all package test suites |
| `pnpm test:coverage` | Test with coverage; CI fails if formulas/services <90% |
| `pnpm typecheck` | `tsc --noEmit` across packages |
| `pnpm lint` | ESLint across packages |
| `pnpm format` | Prettier write |
| Command | What it does |
| ---------------------------------------------------- | ----------------------------------------------------------- |
| `pnpm dev` | Run client + server + watchers in parallel |
| `pnpm build` | Build all packages |
| `pnpm test` | Run all unit suites |
| `pnpm test:coverage` | Unit tests + coverage gate (≥90% on shared formulas) |
| `pnpm --filter @teh-riehl/server test:integration` | E2E against real Postgres — requires `docker compose up -d` |
| `pnpm typecheck` | `tsc --noEmit` across packages |
| `pnpm lint` | ESLint across packages |
| `pnpm format` / `pnpm format:check` | Prettier write / verify |
| `pnpm --filter @teh-riehl/server prisma migrate dev` | Apply / regenerate the DB schema |
CI (`.github/workflows/ci.yml`) runs three jobs in parallel on every push: `static` (lint + typecheck + format + build), `test` (unit + integration against a Postgres service container), `security` (gitleaks + trivy).
## Security model
@@ -75,9 +88,26 @@ pnpm dev
## Game content
Layer definitions live in `packages/content/layers/*.json` and conform to the TS schema in `packages/shared/src/schema/`. Adding a new layer = drop a new JSON file in and (if you reference a new formula) register it in `packages/shared/src/formulas/registry.ts`. No frontend changes required for content-only additions — the React app reads the registered content list at build time.
Layer definitions live in `packages/content/src/layers/*.json` and conform to the TS schema in `packages/shared/src/schema/`. Adding a new layer = drop a new JSON file in, re-export it from `layers.ts`, and (if you reference a new formula) register it in `packages/shared/src/formulas/builtins.ts`. No frontend changes required for content-only additions — the React app reads the layer list at build time.
Formulas are referenced by ID (e.g. `{ "fn": "polynomial", "args": [10, 1.15] }`) instead of being evaluated from strings, so content stays data-only and there is no `eval` / `new Function` attack surface.
Day-1 layers (in unlock order):
| Layer | Resource | Role | Color |
| ------------- | -------------- | ------------------------------- | ------- |
| **Code** | Lines of Code | Base producer | cyan |
| **Commits** | Commits | Prestige (resets Code) | magenta |
| **Coffee** | Cups | Multiplier buyables | amber |
| **Tickets** | Closed Tickets | Challenge layer (debuffs+goals) | green |
| **Releases** | Release Tags | Hard prestige + expansion hook | violet |
| **Tech Debt** | Debt | Antagonist mechanic | red |
Formulas are referenced by ID (e.g. `{ "fn": "polynomial", "args": [10, 1.15] }`) instead of being evaluated from strings, so content stays data-only and there is no `eval` / `new Function` attack surface. The full registry (10 formulas, 9 predicates) is in `packages/shared/src/formulas/builtins.ts` and `predicates/builtins.ts`.
## Visual system
The design commits to one concept: _the game lives inside an IDE._ Type pairing is **Geist** (UI) + **JetBrains Mono** (numbers and system text). Six layer accents drawn from Tailwind's `-400` family so they read as one palette. Diff gutters on cards (`+` affordable, `~` owned, `` locked). Hairline 1px borders, never CSS shadow blur. One signature element: the `>_` blinking prompt, recurring at hero moments.
Tokens live in `packages/client/tailwind.config.ts`; the per-layer `--accent` CSS variable is set on `data-layer="..."` parents, so every component themes itself from one CSS var rather than needing layer-specific class variants.
## License
+2 -2
View File
@@ -8,11 +8,11 @@ services:
POSTGRES_PASSWORD: dev_password
POSTGRES_DB: teh_riehl
ports:
- "5432:5432"
- '5432:5432'
volumes:
- teh-riehl-pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U teh_riehl -d teh_riehl"]
test: ['CMD-SHELL', 'pg_isready -U teh_riehl -d teh_riehl']
interval: 5s
timeout: 5s
retries: 5
+51
View File
@@ -0,0 +1,51 @@
// @ts-check
import js from '@eslint/js';
import tseslint from 'typescript-eslint';
import globals from 'globals';
/**
* Single ESLint config for the whole monorepo. Flat config (ESLint 9+).
* Per-package configs would just duplicate rules; the only divergence
* across packages is the environment globals (node vs browser), which
* we infer from the file path below.
*/
export default tseslint.config(
{
ignores: [
'**/dist/**',
'**/node_modules/**',
'**/coverage/**',
'**/.turbo/**',
'**/prisma/migrations/**',
'**/*.config.{ts,js,mjs,cjs}',
// The generated Prisma client lives in node_modules; nothing to lint anyway.
],
},
js.configs.recommended,
...tseslint.configs.recommended,
{
languageOptions: {
ecmaVersion: 2022,
sourceType: 'module',
globals: { ...globals.node, ...globals.browser },
},
rules: {
'@typescript-eslint/no-unused-vars': [
'warn',
{ argsIgnorePattern: '^_', varsIgnorePattern: '^_', caughtErrorsIgnorePattern: '^_' },
],
'@typescript-eslint/no-explicit-any': 'warn',
'@typescript-eslint/no-non-null-assertion': 'off',
'no-empty': ['error', { allowEmptyCatch: true }],
'no-console': ['warn', { allow: ['warn', 'error'] }],
},
},
// Tests can be loose about console + any.
{
files: ['**/test/**', '**/*.test.{ts,tsx}', '**/*.spec.{ts,tsx}'],
rules: {
'no-console': 'off',
'@typescript-eslint/no-explicit-any': 'off',
},
},
);
+5 -1
View File
@@ -19,7 +19,11 @@
"format:check": "prettier --check ."
},
"devDependencies": {
"@eslint/js": "^9.12.0",
"eslint": "^9.12.0",
"globals": "^15.11.0",
"prettier": "^3.3.3",
"typescript": "^5.6.2"
"typescript": "^5.6.2",
"typescript-eslint": "^8.8.1"
}
}
@@ -55,7 +55,8 @@ export function BuyableCard({
'group w-full text-left bg-surface hairline rounded-sm pl-2.5 pr-3 py-2.5',
'transition-[background-color,border-color,transform] duration-150',
gutter,
interactive && 'hover:-translate-y-px hover:bg-surface-hover hover:border-hairline-bright cursor-pointer',
interactive &&
'hover:-translate-y-px hover:bg-surface-hover hover:border-hairline-bright cursor-pointer',
!interactive && 'opacity-70 cursor-not-allowed',
)}
>
@@ -75,8 +76,7 @@ export function BuyableCard({
<span className="text-[rgb(var(--accent))] uppercase tracking-[0.08em] glow">max</span>
) : (
<span className={cn('num', affordable ? 'text-diff-add' : 'text-fg-dim')}>
{formatNumber(nextCost)}{' '}
<span className="text-fg-muted">{costLabel}</span>
{formatNumber(nextCost)} <span className="text-fg-muted">{costLabel}</span>
</span>
)}
</div>
@@ -20,7 +20,11 @@ export const GlowText = forwardRef<HTMLSpanElement, GlowTextProps>(
// @ts-expect-error - polymorphic ref through a union is awkward, runtime is fine
ref={ref}
data-layer={layer}
className={cn(intensity === 'strong' ? 'glow-strong' : 'glow', 'text-[rgb(var(--accent))]', className)}
className={cn(
intensity === 'strong' ? 'glow-strong' : 'glow',
'text-[rgb(var(--accent))]',
className,
)}
style={style}
{...props}
/>
@@ -1,6 +1,7 @@
import { useMemo } from 'react';
import type { Layer } from '@teh-riehl/shared';
import { TreeEdge, TreeNode } from './TreeNode';
import { PrestigeButton } from './PrestigeButton';
interface LayerCanvasProps {
layer: Layer;
@@ -16,10 +17,10 @@ interface LayerCanvasProps {
* an "ambient" affordance with no rendered edge).
*/
export function LayerCanvas({ layer, ownedUpgrades, ownedBuyables }: LayerCanvasProps) {
const { nodes, edges, viewBox } = useMemo(() => layoutLayer(layer, ownedUpgrades), [
layer,
ownedUpgrades,
]);
const { nodes, edges, viewBox } = useMemo(
() => layoutLayer(layer, ownedUpgrades),
[layer, ownedUpgrades],
);
const buyableEntries = layer.buyables ?? [];
@@ -28,16 +29,19 @@ export function LayerCanvas({ layer, ownedUpgrades, ownedBuyables }: LayerCanvas
data-layer={layer.id}
className="flex-1 min-h-0 relative overflow-hidden bg-bg-elev hairline rounded-sm"
>
{/* Header: file-path breadcrumb */}
{/* Header: file-path breadcrumb + prestige action */}
<div className="absolute top-0 left-0 right-0 z-10 flex items-center justify-between px-3 py-2 hairline-b bg-bg-elev/90 backdrop-blur-sm">
<span className="font-mono text-[11px] text-fg-muted">
<span className="text-comment">~/layers/</span>
<span className="text-[rgb(var(--accent))]">{layer.id}</span>
<span className="text-fg-muted">.json</span>
</span>
<span className="font-mono text-[11px] text-comment uppercase tracking-[0.1em]">
{layer.type} layer
</span>
<div className="flex items-center gap-3">
{layer.reset && <PrestigeButton layer={layer} />}
<span className="font-mono text-[11px] text-comment uppercase tracking-[0.1em]">
{layer.type} layer
</span>
</div>
</div>
{/* The canvas itself */}
@@ -105,7 +109,11 @@ interface LaidOutNode {
function layoutLayer(
layer: Layer,
ownedUpgrades: ReadonlySet<string>,
): { nodes: LaidOutNode[]; edges: { x1: number; y1: number; x2: number; y2: number; unlocked: boolean }[]; viewBox: string } {
): {
nodes: LaidOutNode[];
edges: { x1: number; y1: number; x2: number; y2: number; unlocked: boolean }[];
viewBox: string;
} {
const COL_W = 140;
const ROW_H = 130;
const LEFT = 80;
@@ -0,0 +1,63 @@
import type { Layer } from '@teh-riehl/shared';
import { useAppDispatch, useAppSelector } from '@/app/store';
import { Button } from '@/components/ui/button';
import { canPrestige, performPrestige, prestigeGain } from '@/features/layers/prestige';
import { formatNumber } from '@/lib/utils';
interface Props {
layer: Layer;
}
/**
* The reset action button. Reads like a terminal command:
*
* > git commit -m "+3 commits"
*
* Disabled when the player doesn't meet the threshold. Shows the projected
* gain inline so you know what you're trading.
*/
export function PrestigeButton({ layer }: Props) {
const dispatch = useAppDispatch();
const can = useAppSelector((s) => canPrestige(layer, s));
const gain = useAppSelector((s) => prestigeGain(layer, s) ?? 0);
if (!layer.reset) return null;
const symbol = layer.resource.symbol ?? '';
const verb = verbForLayer(layer.id);
return (
<Button
size="sm"
variant={can && gain > 0 ? 'primary' : 'default'}
disabled={!can || gain <= 0}
onClick={() => dispatch(performPrestige(layer))}
title={
can
? `Reset upstream layers; gain ${formatNumber(gain)} ${layer.resource.name}`
: 'Requirement not met'
}
>
<span className="text-[rgb(var(--accent))] font-bold mr-1.5">$</span>
<span>{verb}</span>
{gain > 0 && (
<span className="num text-fg-muted ml-1.5">
<span className="text-comment">// +</span>
<span className={can ? 'text-[rgb(var(--accent))]' : 'text-fg-muted'}>
{formatNumber(gain)}
</span>
{symbol && <span className="text-comment ml-0.5">{symbol}</span>}
</span>
)}
</Button>
);
}
/** Map each prestige layer to its terminal-style verb. */
function verbForLayer(layerId: string): string {
const verbs: Record<string, string> = {
commits: 'git commit',
releases: 'git tag -a',
};
return verbs[layerId] ?? `reset ${layerId}`;
}
@@ -50,8 +50,7 @@ export function ResourceReadout({
}
}, [amount]);
const numCls =
size === 'lg' ? 'text-num-lg' : size === 'sm' ? 'text-num-sm' : 'text-num-md';
const numCls = size === 'lg' ? 'text-num-lg' : size === 'sm' ? 'text-num-sm' : 'text-num-md';
return (
<div className={cn('flex flex-col gap-1', className)} {...props}>
@@ -1,4 +1,5 @@
import { evalFormula, evalPredicate, type Layer } from '@teh-riehl/shared';
import { layerById } from '@teh-riehl/content';
import { useAppDispatch, useAppSelector } from '@/app/store';
import { buildEvalContext } from '@/lib/formula-runtime';
import { buyBuyable, buyUpgrade, spendAmount } from '@/features/layers/layersSlice';
@@ -50,7 +51,8 @@ export function RightPanel({ layer }: RightPanelProps) {
{layer.upgrades!.map((u) => {
const owned = ownedUpgrades.has(u.id);
const cost = evalFormula(u.cost, 0, ctx);
const costLayerState = layersState[u.costLayer ?? layer.id];
const costLayerId = u.costLayer ?? layer.id;
const costLayerState = layersState[costLayerId];
const canAfford = (costLayerState?.amount ?? 0) >= cost;
const unlocked = u.unlocked ? evalPredicate(u.unlocked, ctx) : true;
const effectVal = evalFormula(u.effect, 0, ctx);
@@ -60,12 +62,20 @@ export function RightPanel({ layer }: RightPanelProps) {
title={u.title}
description={u.description}
cost={cost}
costLabel={layer.resource.symbol ?? ''}
costLabel={layerById[costLayerId]?.resource.symbol ?? ''}
effect={`x${effectVal.toFixed(2)}`}
state={owned ? 'owned' : !unlocked ? 'locked' : canAfford ? 'affordable' : 'unaffordable'}
state={
owned
? 'owned'
: !unlocked
? 'locked'
: canAfford
? 'affordable'
: 'unaffordable'
}
lockHint={!unlocked ? 'unlock prerequisites first' : undefined}
onBuy={() => {
dispatch(spendAmount({ layerId: u.costLayer ?? layer.id, cost }));
dispatch(spendAmount({ layerId: costLayerId, cost }));
dispatch(buyUpgrade({ layerId: layer.id, upgradeId: u.id }));
}}
/>
@@ -83,8 +93,9 @@ export function RightPanel({ layer }: RightPanelProps) {
{layer.buyables!.map((b) => {
const count = ownedBuyables[b.id] ?? 0;
const nextCost = evalFormula(b.cost, count, ctx);
const layerAmount = layersState[layer.id]?.amount ?? 0;
const canAfford = layerAmount >= nextCost;
const costLayerId = b.costLayer ?? layer.id;
const costLayerAmount = layersState[costLayerId]?.amount ?? 0;
const canAfford = costLayerAmount >= nextCost;
const unlocked = b.unlocked ? evalPredicate(b.unlocked, ctx) : true;
const maxedOut = b.maxCount !== undefined && count >= b.maxCount;
const effectPerUnit =
@@ -96,14 +107,14 @@ export function RightPanel({ layer }: RightPanelProps) {
description={b.description}
count={count}
nextCost={nextCost}
costLabel={layer.resource.symbol ?? ''}
costLabel={layerById[costLayerId]?.resource.symbol ?? ''}
effectPerUnit={`+${effectPerUnit.toFixed(2)} ${layer.resource.symbol ?? ''}/s`}
affordable={canAfford}
maxedOut={maxedOut}
locked={!unlocked}
lockHint={!unlocked ? 'unlock prerequisites first' : undefined}
onBuy={() => {
dispatch(spendAmount({ layerId: layer.id, cost: nextCost }));
dispatch(spendAmount({ layerId: costLayerId, cost: nextCost }));
dispatch(buyBuyable({ layerId: layer.id, buyableId: b.id }));
}}
/>
@@ -26,10 +26,7 @@ export function SettingsModal() {
<DialogContent className="w-[min(480px,calc(100vw-2rem))]">
<DialogHeader>~/.config/teh-riehl/settings.toml</DialogHeader>
<DialogBody className="flex flex-col gap-4">
<SettingRow
label="display.scanlines"
help="CRT line texture over the entire UI."
>
<SettingRow label="display.scanlines" help="CRT line texture over the entire UI.">
<ToggleSwitch
checked={scanlines}
onChange={(v) => dispatch(setScanlines(v))}
@@ -58,7 +58,8 @@ export function UpgradeCard({
'group w-full text-left bg-surface hairline rounded-sm pl-2.5 pr-3 py-2.5',
'flex flex-col gap-1 transition-[background-color,border-color,transform] duration-150',
gutter,
isBuyable && 'hover:-translate-y-px hover:bg-surface-hover hover:border-hairline-bright cursor-pointer',
isBuyable &&
'hover:-translate-y-px hover:bg-surface-hover hover:border-hairline-bright cursor-pointer',
isOwned && 'opacity-90',
isLocked && 'opacity-60 cursor-not-allowed',
)}
@@ -95,8 +96,7 @@ export function UpgradeCard({
</span>
) : (
<span className={cn('num', isBuyable ? 'text-diff-add' : 'text-fg-dim')}>
{formatNumber(cost)}{' '}
<span className="text-fg-muted">{costLabel}</span>
{formatNumber(cost)} <span className="text-fg-muted">{costLabel}</span>
</span>
)}
</div>
+1 -2
View File
@@ -59,8 +59,7 @@ const buttonVariants = cva(
);
export interface ButtonProps
extends ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
extends ButtonHTMLAttributes<HTMLButtonElement>, VariantProps<typeof buttonVariants> {
asChild?: boolean;
}
+5 -4
View File
@@ -39,7 +39,10 @@ export const CardTitle = forwardRef<HTMLHeadingElement, HTMLAttributes<HTMLHeadi
({ className, ...props }, ref) => (
<h3
ref={ref}
className={cn('font-sans text-sm font-medium text-fg-bright normal-case tracking-normal', className)}
className={cn(
'font-sans text-sm font-medium text-fg-bright normal-case tracking-normal',
className,
)}
{...props}
/>
),
@@ -47,9 +50,7 @@ export const CardTitle = forwardRef<HTMLHeadingElement, HTMLAttributes<HTMLHeadi
CardTitle.displayName = 'CardTitle';
export const CardBody = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => (
<div ref={ref} className={cn('p-3', className)} {...props} />
),
({ className, ...props }, ref) => <div ref={ref} className={cn('p-3', className)} {...props} />,
);
CardBody.displayName = 'CardBody';
+6 -1
View File
@@ -1,6 +1,11 @@
import * as DialogPrimitive from '@radix-ui/react-dialog';
import { X } from 'lucide-react';
import { forwardRef, type ComponentPropsWithoutRef, type ElementRef, type HTMLAttributes } from 'react';
import {
forwardRef,
type ComponentPropsWithoutRef,
type ElementRef,
type HTMLAttributes,
} from 'react';
import { cn } from '@/lib/utils';
/**
+6 -6
View File
@@ -14,11 +14,7 @@ export const TabsList = forwardRef<
>(({ className, ...props }, ref) => (
<TabsPrimitive.List
ref={ref}
className={cn(
'flex items-stretch hairline-b bg-bg-elev',
'font-mono text-[12px]',
className,
)}
className={cn('flex items-stretch hairline-b bg-bg-elev', 'font-mono text-[12px]', className)}
{...props}
/>
));
@@ -50,6 +46,10 @@ export const TabsContent = forwardRef<
ElementRef<typeof TabsPrimitive.Content>,
ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Content ref={ref} className={cn('focus-visible:outline-none', className)} {...props} />
<TabsPrimitive.Content
ref={ref}
className={cn('focus-visible:outline-none', className)}
{...props}
/>
));
TabsContent.displayName = TabsPrimitive.Content.displayName;
+17 -19
View File
@@ -8,8 +8,7 @@ interface AuthResponse {
}
export const signup =
(email: string, password: string, displayName?: string) =>
async (dispatch: AppDispatch) => {
(email: string, password: string, displayName?: string) => async (dispatch: AppDispatch) => {
dispatch(loadingStart());
try {
const res = await api<AuthResponse>(
@@ -26,23 +25,22 @@ export const signup =
}
};
export const login =
(email: string, password: string) => async (dispatch: AppDispatch) => {
dispatch(loadingStart());
try {
const res = await api<AuthResponse>(
'/auth/login',
{ method: 'POST', body: JSON.stringify({ email, password }) },
{ skipAuth: true },
);
setTokens(res.tokens.accessToken, res.tokens.refreshToken);
dispatch(authenticated(res.user));
} catch (e: unknown) {
const message = errorMessage(e, 'Login failed');
dispatch(failed(message));
throw e;
}
};
export const login = (email: string, password: string) => async (dispatch: AppDispatch) => {
dispatch(loadingStart());
try {
const res = await api<AuthResponse>(
'/auth/login',
{ method: 'POST', body: JSON.stringify({ email, password }) },
{ skipAuth: true },
);
setTokens(res.tokens.accessToken, res.tokens.refreshToken);
dispatch(authenticated(res.user));
} catch (e: unknown) {
const message = errorMessage(e, 'Login failed');
dispatch(failed(message));
throw e;
}
};
export const logout = () => async (dispatch: AppDispatch) => {
try {
@@ -0,0 +1,60 @@
import { evalFormula, evalPredicate } from '@teh-riehl/shared';
import type { Layer } from '@teh-riehl/shared';
import type { AppDispatch, RootState } from '@/app/store';
import { buildEvalContext } from '@/lib/formula-runtime';
import { addAmount, resetLayer } from './layersSlice';
/**
* Resolve the prestige currency the player would gain right now.
* Returns `null` when the layer has no reset config (i.e. not prestigeable).
*/
export function prestigeGain(layer: Layer, state: RootState): number | null {
if (!layer.reset) return null;
const ctx = buildEvalContext(state.layers, state.game.elapsedMs);
// The TMT convention: gainFormula's input is the upstream layer's "best".
// For most layers there's a clear primary branch. We use the first one;
// future expansion layers with multiple branches will need a different rule.
const primary = layer.branches[0];
if (!primary) return 0;
const best = state.layers[primary]?.best ?? 0;
return evalFormula(layer.reset.gainFormula, best, ctx);
}
/**
* Whether the player meets the requirement to perform a reset right now.
* Currently checks the primary upstream layer's amount against requirement.
*/
export function canPrestige(layer: Layer, state: RootState): boolean {
if (!layer.reset) return false;
const ctx = buildEvalContext(state.layers, state.game.elapsedMs);
if (layer.reset.canReset && !evalPredicate(layer.reset.canReset, ctx)) return false;
const requirement = evalFormula(layer.reset.requirement, 0, ctx);
const primary = layer.branches[0];
if (!primary) return false;
return (state.layers[primary]?.best ?? 0) >= requirement;
}
/**
* Perform the reset: award the gain to this layer, then wipe `resetsLayers`
* (or the implicit branches) back to their starting amounts. Owned upgrades
* are wiped by default; the layer's milestones decide if any are kept via
* effectTags (interpreted by future game code).
*/
export const performPrestige =
(layer: Layer) => (dispatch: AppDispatch, getState: () => RootState) => {
if (!layer.reset) return;
const state = getState();
if (!canPrestige(layer, state)) return;
const gained = prestigeGain(layer, state) ?? 0;
if (gained <= 0) return;
// Wipe the resetsLayers list (or fall back to branches).
const toWipe = layer.reset.resetsLayers ?? layer.branches;
for (const id of toWipe) {
dispatch(resetLayer({ layerId: id }));
}
// Add gained currency to this layer.
dispatch(addAmount({ layerId: layer.id, delta: gained }));
};
+6 -1
View File
@@ -44,7 +44,12 @@ export async function loadCloud(): Promise<SaveBlob | null> {
const res = await api<{ state: SaveBlob }>('/saves/me');
return res.state;
} catch (e: unknown) {
if (typeof e === 'object' && e !== null && 'status' in e && (e as { status: number }).status === 404) {
if (
typeof e === 'object' &&
e !== null &&
'status' in e &&
(e as { status: number }).status === 404
) {
return null;
}
throw e;
+1 -4
View File
@@ -2,10 +2,7 @@ import React from 'react';
import ReactDOM from 'react-dom/client';
import { Provider } from 'react-redux';
import { RouterProvider, createBrowserRouter, Navigate } from 'react-router-dom';
import {
registerBuiltinFormulas,
registerBuiltinPredicates,
} from '@teh-riehl/shared';
import { registerBuiltinFormulas, registerBuiltinPredicates } from '@teh-riehl/shared';
import { store } from './app/store';
import { GamePage } from './pages/Game';
import { LoginPage } from './pages/Login';
+3 -6
View File
@@ -33,13 +33,10 @@ export function LoginPage() {
<div className="flex items-center gap-2.5 text-fg-muted">
<Terminal className="h-4 w-4 text-code" />
<span className="font-mono text-[12px] tracking-tight text-fg-dim">
teh-riehl-incremental{' '}
<span className="text-comment">// v0.1.0</span>
teh-riehl-incremental <span className="text-comment">// v0.1.0</span>
</span>
</div>
<h1 className="font-sans text-2xl font-semibold leading-tight">
Sign in to ship code.
</h1>
<h1 className="font-sans text-2xl font-semibold leading-tight">Sign in to ship code.</h1>
<Prompt path="~/auth">login</Prompt>
</header>
@@ -76,7 +73,7 @@ export function LoginPage() {
<span className="text-techdebt">Traceback</span>{' '}
<span className="text-fg-muted">(most recent call last):</span>
<div className="text-fg-dim mt-1">
<span className="text-comment"> File</span>{' '}
<span className="text-comment"> File</span>{' '}
<span className="text-fg">"auth/login.ts"</span>
{', '}
<span className="text-comment">in</span>{' '}
+5 -5
View File
@@ -27,7 +27,10 @@ export function SignupPage() {
};
return (
<div className="min-h-screen flex items-center justify-center px-6 bg-grid bg-bg" data-layer="commits">
<div
className="min-h-screen flex items-center justify-center px-6 bg-grid bg-bg"
data-layer="commits"
>
<div className="w-full max-w-md">
<header className="mb-8 flex flex-col gap-3">
<div className="flex items-center gap-2.5 text-fg-muted">
@@ -42,10 +45,7 @@ export function SignupPage() {
<Prompt path="~/auth">git init</Prompt>
</header>
<form
onSubmit={onSubmit}
className="bg-surface hairline rounded-sm shadow-panel-raised"
>
<form onSubmit={onSubmit} className="bg-surface hairline rounded-sm shadow-panel-raised">
<div className="px-3 py-2 hairline-b bg-bg-elev font-mono text-[11px] uppercase tracking-[0.12em] text-fg-muted flex justify-between">
<span>auth/signup.ts</span>
<span className="text-comment">new player</span>
+54 -18
View File
@@ -15,12 +15,24 @@
--accent: 34 211 238;
}
[data-layer='code'] { --accent: 34 211 238; }
[data-layer='commits'] { --accent: 232 121 249; }
[data-layer='coffee'] { --accent: 251 191 36; }
[data-layer='tickets'] { --accent: 74 222 128; }
[data-layer='releases'] { --accent: 167 139 250; }
[data-layer='techdebt'] { --accent: 248 113 113; }
[data-layer='code'] {
--accent: 34 211 238;
}
[data-layer='commits'] {
--accent: 232 121 249;
}
[data-layer='coffee'] {
--accent: 251 191 36;
}
[data-layer='tickets'] {
--accent: 74 222 128;
}
[data-layer='releases'] {
--accent: 167 139 250;
}
[data-layer='techdebt'] {
--accent: 248 113 113;
}
html,
body,
@@ -108,20 +120,40 @@
.gutter-change pending / partial
.gutter-accent themed to current layer accent
------------------------------------------------------------------------ */
.gutter-add { box-shadow: inset 2px 0 0 0 theme('colors.diff-add'); }
.gutter-remove { box-shadow: inset 2px 0 0 0 theme('colors.diff-remove'); }
.gutter-change { box-shadow: inset 2px 0 0 0 theme('colors.diff-change'); }
.gutter-neutral { box-shadow: inset 2px 0 0 0 theme('colors.diff-neutral'); }
.gutter-accent { box-shadow: inset 2px 0 0 0 rgb(var(--accent)); }
.gutter-add {
box-shadow: inset 2px 0 0 0 theme('colors.diff-add');
}
.gutter-remove {
box-shadow: inset 2px 0 0 0 theme('colors.diff-remove');
}
.gutter-change {
box-shadow: inset 2px 0 0 0 theme('colors.diff-change');
}
.gutter-neutral {
box-shadow: inset 2px 0 0 0 theme('colors.diff-neutral');
}
.gutter-accent {
box-shadow: inset 2px 0 0 0 rgb(var(--accent));
}
/* ---------------------------------------------------------------------------
Hairline borders — 1px solid, NOT shadow blur. Like Finder rows.
------------------------------------------------------------------------ */
.hairline { border: 1px solid theme('colors.hairline'); }
.hairline-t { border-top: 1px solid theme('colors.hairline'); }
.hairline-b { border-bottom: 1px solid theme('colors.hairline'); }
.hairline-l { border-left: 1px solid theme('colors.hairline'); }
.hairline-r { border-right: 1px solid theme('colors.hairline'); }
.hairline {
border: 1px solid theme('colors.hairline');
}
.hairline-t {
border-top: 1px solid theme('colors.hairline');
}
.hairline-b {
border-bottom: 1px solid theme('colors.hairline');
}
.hairline-l {
border-left: 1px solid theme('colors.hairline');
}
.hairline-r {
border-right: 1px solid theme('colors.hairline');
}
/* ---------------------------------------------------------------------------
Terminal prompt. Use as <span class="prompt">PS1$</span>. The blinking
@@ -159,7 +191,9 @@
------------------------------------------------------------------------ */
*:focus-visible {
outline: none;
box-shadow: 0 0 0 1px theme('colors.bg'), 0 0 0 2px rgb(var(--accent) / 0.7);
box-shadow:
0 0 0 1px theme('colors.bg'),
0 0 0 2px rgb(var(--accent) / 0.7);
}
}
@@ -174,7 +208,9 @@
/* Drop a soft inner shadow so panels feel recessed against the bg. */
.recessed {
box-shadow: inset 0 1px 0 rgba(0, 0, 0, 0.4), inset 0 0 0 1px theme('colors.hairline');
box-shadow:
inset 0 1px 0 rgba(0, 0, 0, 0.4),
inset 0 0 0 1px theme('colors.hairline');
}
}
+1 -2
View File
@@ -75,8 +75,7 @@ export default {
},
boxShadow: {
// Inset panel highlight + lowlight, like macOS Finder rows.
panel:
'0 1px 0 0 rgba(255, 255, 255, 0.025) inset, 0 -1px 0 0 rgba(0, 0, 0, 0.4) inset',
panel: '0 1px 0 0 rgba(255, 255, 255, 0.025) inset, 0 -1px 0 0 rgba(0, 0, 0, 0.4) inset',
'panel-raised':
'0 1px 0 0 rgba(255, 255, 255, 0.04) inset, 0 8px 24px -16px rgba(0, 0, 0, 0.8)',
// Neon ring + bloom keyed to the --accent CSS var (set per-layer).
+2 -2
View File
@@ -10,8 +10,8 @@
"unlocked": { "pred": "amountAtLeast", "args": ["tickets", 3] },
"reset": {
"type": "normal",
"requirement": { "fn": "constant", "args": [1000] },
"gainFormula": { "fn": "prestigePoints", "args": [1000, 0.4] },
"requirement": { "fn": "constant", "args": [100] },
"gainFormula": { "fn": "prestigePoints", "args": [100, 0.5] },
"resetsLayers": ["code", "commits", "coffee", "tickets"]
},
"upgrades": [
+4 -3
View File
@@ -8,14 +8,15 @@
"branches": [],
"startData": { "amount": 0 },
"unlocked": { "pred": "totalAtLeast", "args": ["code", 5000] },
"production": { "fn": "fromLayerAmount", "args": ["code", 0.0005] },
"production": { "fn": "fromLayerAmount", "args": ["code", 0.0001] },
"buyables": [
{
"id": "pay-down",
"title": "Pay Down Debt",
"description": "Spend LoC (cost in 'code') to remove Debt. -10 Debt per buy.",
"description": "Each pay-down slows debt accrual by 10%. 10 pay-downs stops it; further buys reverse the flow.",
"costLayer": "code",
"cost": { "fn": "polynomial", "args": [500, 1.2] },
"effect": { "fn": "linear", "args": [0, -10] },
"effect": { "fn": "linear", "args": [0, -0.1] },
"row": 1,
"col": 1
}
+1 -4
View File
@@ -91,10 +91,7 @@ describe('day-1 content', () => {
it('every formula reference names a registered formula', () => {
for (const layer of layers) {
for (const ref of collectFormulas(layer)) {
expect(
hasFormula(ref.fn),
`${layer.id}: formula '${ref.fn}' is not registered`,
).toBe(true);
expect(hasFormula(ref.fn), `${layer.id}: formula '${ref.fn}' is not registered`).toBe(true);
}
}
});
+1 -9
View File
@@ -1,12 +1,4 @@
import {
Body,
Controller,
HttpCode,
HttpStatus,
Post,
Req,
UseGuards,
} from '@nestjs/common';
import { Body, Controller, HttpCode, HttpStatus, Post, Req, UseGuards } from '@nestjs/common';
import { AuthService } from './auth.service';
import { SignupDto } from './dto/signup.dto';
import { LoginDto } from './dto/login.dto';
+7 -9
View File
@@ -1,8 +1,4 @@
import {
ConflictException,
Injectable,
UnauthorizedException,
} from '@nestjs/common';
import { ConflictException, Injectable, UnauthorizedException } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { JwtService } from '@nestjs/jwt';
import { randomBytes } from 'node:crypto';
@@ -56,10 +52,12 @@ export class AuthService {
if (!user) {
// Verify against a dummy hash to keep response time uniform with the
// "user exists, password wrong" branch — small but real timing-attack mitigation.
await argon2.verify(
'$argon2id$v=19$m=19456,t=2,p=1$YWFhYWFhYWFhYWFhYWFhYQ$0000000000000000000000000000000000000000000',
password,
).catch(() => false);
await argon2
.verify(
'$argon2id$v=19$m=19456,t=2,p=1$YWFhYWFhYWFhYWFhYWFhYQ$0000000000000000000000000000000000000000000',
password,
)
.catch(() => false);
throw new UnauthorizedException('Invalid credentials');
}
const ok = await argon2.verify(user.passwordHash, password);
@@ -11,7 +11,10 @@ export interface AuthenticatedRequestUser {
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') {
constructor(config: ConfigService, private readonly users: UsersService) {
constructor(
config: ConfigService,
private readonly users: UsersService,
) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
@@ -38,15 +38,10 @@ export class EncryptionService {
constructor(config: ConfigService) {
const kek = Buffer.from(config.getOrThrow<string>('ENCRYPTION_MASTER_KEY'), 'base64');
if (kek.length !== KEY_BYTES) {
throw new Error(
`ENCRYPTION_MASTER_KEY must decode to ${KEY_BYTES} bytes; got ${kek.length}`,
);
throw new Error(`ENCRYPTION_MASTER_KEY must decode to ${KEY_BYTES} bytes; got ${kek.length}`);
}
this.kek = kek;
this.emailHashSecret = Buffer.from(
config.getOrThrow<string>('EMAIL_HASH_SECRET'),
'base64',
);
this.emailHashSecret = Buffer.from(config.getOrThrow<string>('EMAIL_HASH_SECRET'), 'base64');
if (this.emailHashSecret.length < KEY_BYTES) {
this.logger.warn(
`EMAIL_HASH_SECRET is shorter than ${KEY_BYTES} bytes; recommended length is 32+`,
@@ -30,10 +30,7 @@ export class SavesController {
@Put('me')
@HttpCode(HttpStatus.OK)
upsertMine(
@Req() req: Request & { user: AuthenticatedRequestUser },
@Body() dto: UpsertSaveDto,
) {
upsertMine(@Req() req: Request & { user: AuthenticatedRequestUser }, @Body() dto: UpsertSaveDto) {
return this.saves.upsert(req.user.id, dto.state, dto.version ?? 1);
}
+7 -1
View File
@@ -52,7 +52,13 @@ export class SavesService {
},
});
return { id: row.id, userId: row.userId, state, version: row.version, updatedAt: row.updatedAt };
return {
id: row.id,
userId: row.userId,
state,
version: row.version,
updatedAt: row.updatedAt,
};
}
async loadForUser(userId: string): Promise<DecryptedSave | null> {
+3 -1
View File
@@ -54,7 +54,9 @@ beforeAll(async () => {
app = moduleRef.createNestApplication();
app.setGlobalPrefix('api');
app.useGlobalPipes(new ValidationPipe({ whitelist: true, forbidNonWhitelisted: true, transform: true }));
app.useGlobalPipes(
new ValidationPipe({ whitelist: true, forbidNonWhitelisted: true, transform: true }),
);
await app.init();
prisma = new PrismaClient({ datasourceUrl: process.env.DATABASE_URL });
+2
View File
@@ -7,6 +7,8 @@ export interface Buyable {
description: string;
/** Cost formula receives current `count` in `args[0]` slot via the registry. */
cost: FormulaRef;
/** Optional foreign layer to spend cost in (defaults to the buyable's home layer). */
costLayer?: string;
/** Effect formula also receives `count`. */
effect: FormulaRef;
/** Optional cap; omit for unlimited. */
+3 -6
View File
@@ -62,12 +62,9 @@ describe('predicate arg coercion', () => {
it('amountAtLeast treats non-string layerId as empty (fallback branch)', () => {
const ctx = makeEvalContext({}, 0);
expect(
evalPredicate(
{ pred: 'amountAtLeast', args: [42 as unknown as string, 0] },
ctx,
),
).toBe(true);
expect(evalPredicate({ pred: 'amountAtLeast', args: [42 as unknown as string, 0] }, ctx)).toBe(
true,
);
});
it('bestAtLeast / totalAtLeast return false against a missing layer (?? 0 branch)', () => {
+609
View File
@@ -8,12 +8,24 @@ importers:
.:
devDependencies:
'@eslint/js':
specifier: ^9.12.0
version: 9.39.4
eslint:
specifier: ^9.12.0
version: 9.39.4(jiti@1.21.7)
globals:
specifier: ^15.11.0
version: 15.15.0
prettier:
specifier: ^3.3.3
version: 3.8.3
typescript:
specifier: ^5.6.2
version: 5.9.3
typescript-eslint:
specifier: ^8.8.1
version: 8.59.3(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3)
packages/client:
dependencies:
@@ -535,6 +547,44 @@ packages:
cpu: [x64]
os: [win32]
'@eslint-community/eslint-utils@4.9.1':
resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
peerDependencies:
eslint: ^6.0.0 || ^7.0.0 || >=8.0.0
'@eslint-community/regexpp@4.12.2':
resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==}
engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0}
'@eslint/config-array@0.21.2':
resolution: {integrity: sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@eslint/config-helpers@0.4.2':
resolution: {integrity: sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@eslint/core@0.17.0':
resolution: {integrity: sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@eslint/eslintrc@3.3.5':
resolution: {integrity: sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@eslint/js@9.39.4':
resolution: {integrity: sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@eslint/object-schema@2.1.7':
resolution: {integrity: sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@eslint/plugin-kit@0.4.1':
resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@floating-ui/core@1.7.5':
resolution: {integrity: sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==}
@@ -550,6 +600,26 @@ packages:
'@floating-ui/utils@0.2.11':
resolution: {integrity: sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==}
'@humanfs/core@0.19.2':
resolution: {integrity: sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==}
engines: {node: '>=18.18.0'}
'@humanfs/node@0.16.8':
resolution: {integrity: sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==}
engines: {node: '>=18.18.0'}
'@humanfs/types@0.15.0':
resolution: {integrity: sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==}
engines: {node: '>=18.18.0'}
'@humanwhocodes/module-importer@1.0.1':
resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==}
engines: {node: '>=12.22'}
'@humanwhocodes/retry@0.4.3':
resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==}
engines: {node: '>=18.18'}
'@isaacs/cliui@8.0.2':
resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==}
engines: {node: '>=12'}
@@ -1426,6 +1496,65 @@ packages:
'@types/validator@13.15.10':
resolution: {integrity: sha512-T8L6i7wCuyoK8A/ZeLYt1+q0ty3Zb9+qbSSvrIVitzT3YjZqkTZ40IbRsPanlB4h1QB3JVL1SYCdR6ngtFYcuA==}
'@typescript-eslint/eslint-plugin@8.59.3':
resolution: {integrity: sha512-PwFvSKsXGShKGW6n5bZOhGHEcCZXM8HofLK9fNsEwZXzFRjoY+XT1Vsf1zgyXdwTr0ZYz1/2tkZ0DBTT9jZjhw==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
'@typescript-eslint/parser': ^8.59.3
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
typescript: '>=4.8.4 <6.1.0'
'@typescript-eslint/parser@8.59.3':
resolution: {integrity: sha512-HPwA+hVkfcriajbNvTmZv4VRauibay+cWArYUYq7u7W7PmGShMxbPxLvrwDme55a6d5alG3nrYfhyJ/G28XlLg==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
typescript: '>=4.8.4 <6.1.0'
'@typescript-eslint/project-service@8.59.3':
resolution: {integrity: sha512-ECiUWa/KYRGDFUqTNehaRgzDshnJfkTABJxVemHk4ko22gcr0ukloKjWvyQ64g8YCV/UI47kN1dbmjf/GaQYng==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
typescript: '>=4.8.4 <6.1.0'
'@typescript-eslint/scope-manager@8.59.3':
resolution: {integrity: sha512-t2LvZnoEfzKtnPjgeEu41xw5gxq9mQVfYy4OoZ4Vlt0sk3JwxmhCca/AR7DwOiHrjWgjAj6as4AhRLKSDfvZIA==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@typescript-eslint/tsconfig-utils@8.59.3':
resolution: {integrity: sha512-PcIJHjmaREXLgIAIzLnSY9VucEzz8FKXsRgFa1DmdGCK/5tJpW03TKJF01Q6VZd1lLdz2sIKPWaDUZN9dp//dw==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
typescript: '>=4.8.4 <6.1.0'
'@typescript-eslint/type-utils@8.59.3':
resolution: {integrity: sha512-g71d8QD8UaiHGvrJwyIS1hCX5r63w6Jll+4VEYhEAHXTDIqX1JgxhTAbEHtKntL9kuc4jRo7/GWw5xfCepSccQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
typescript: '>=4.8.4 <6.1.0'
'@typescript-eslint/types@8.59.3':
resolution: {integrity: sha512-ePFoH0g4ludssdRFqqDxQePCxU4WQyRa9+XVwjm7yLn0FKhMeoetC+qBEEI1Eyb1pGSDveTIT09Bvw2WhlGayg==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@typescript-eslint/typescript-estree@8.59.3':
resolution: {integrity: sha512-CbRjVRAf7Lr9Kr8RopKcbY45p2VfmmHrm0ygOCYFi7oU8q19m0Fs/6iHS7kNOmwpp+ob07ZVcAqlxUod9lYdmg==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
typescript: '>=4.8.4 <6.1.0'
'@typescript-eslint/utils@8.59.3':
resolution: {integrity: sha512-JAvT14goBzRzzzZyqq3P9BLArIxTtQURUtFgQ/V7FO+eU+Gg6ES+5ymOPP1wRxXcxAYeivCk4uS3jCKWI1K8Zg==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
typescript: '>=4.8.4 <6.1.0'
'@typescript-eslint/visitor-keys@8.59.3':
resolution: {integrity: sha512-f1UQF7ggd42YiwI5wGrRaPsa+P0CINBlrkLPmGfpq/u/I/oVtecoEIfFR9ag/oa1sLOsRNZ6xehf6qMZhQGBDg==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@vitejs/plugin-react@4.7.0':
resolution: {integrity: sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==}
engines: {node: ^14.18.0 || >=16.0.0}
@@ -1525,6 +1654,11 @@ packages:
resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==}
engines: {node: '>= 0.6'}
acorn-jsx@5.3.2:
resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==}
peerDependencies:
acorn: ^6.0.0 || ^7.0.0 || ^8.0.0
acorn-walk@8.3.5:
resolution: {integrity: sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==}
engines: {node: '>=0.4.0'}
@@ -1929,6 +2063,9 @@ packages:
resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==}
engines: {node: '>=6'}
deep-is@0.1.4:
resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==}
deepmerge@4.3.1:
resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==}
engines: {node: '>=0.10.0'}
@@ -2058,15 +2195,53 @@ packages:
resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==}
engines: {node: '>=0.8.0'}
escape-string-regexp@4.0.0:
resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==}
engines: {node: '>=10'}
eslint-scope@5.1.1:
resolution: {integrity: sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==}
engines: {node: '>=8.0.0'}
eslint-scope@8.4.0:
resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
eslint-visitor-keys@3.4.3:
resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
eslint-visitor-keys@4.2.1:
resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
eslint-visitor-keys@5.0.1:
resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==}
engines: {node: ^20.19.0 || ^22.13.0 || >=24}
eslint@9.39.4:
resolution: {integrity: sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
hasBin: true
peerDependencies:
jiti: '*'
peerDependenciesMeta:
jiti:
optional: true
espree@10.4.0:
resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
esprima@4.0.1:
resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==}
engines: {node: '>=4'}
hasBin: true
esquery@1.7.0:
resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==}
engines: {node: '>=0.10'}
esrecurse@4.3.0:
resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==}
engines: {node: '>=4.0'}
@@ -2085,6 +2260,10 @@ packages:
estree-walker@3.0.3:
resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==}
esutils@2.0.3:
resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==}
engines: {node: '>=0.10.0'}
etag@1.8.1:
resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==}
engines: {node: '>= 0.6'}
@@ -2115,6 +2294,9 @@ packages:
fast-json-stable-stringify@2.1.0:
resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==}
fast-levenshtein@2.0.6:
resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==}
fast-safe-stringify@2.1.1:
resolution: {integrity: sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==}
@@ -2140,6 +2322,10 @@ packages:
resolution: {integrity: sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==}
engines: {node: '>=8'}
file-entry-cache@8.0.0:
resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==}
engines: {node: '>=16.0.0'}
file-type@20.4.1:
resolution: {integrity: sha512-hw9gNZXUfZ02Jo0uafWLaFVPter5/k2rfcrjFJJHX/77xtSDOfJuEFb6oKlFV86FLP1SuyHMW1PSk0U9M5tKkQ==}
engines: {node: '>=18'}
@@ -2152,6 +2338,17 @@ packages:
resolution: {integrity: sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==}
engines: {node: '>= 0.8'}
find-up@5.0.0:
resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==}
engines: {node: '>=10'}
flat-cache@4.0.1:
resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==}
engines: {node: '>=16'}
flatted@3.4.2:
resolution: {integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==}
foreground-child@3.3.1:
resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==}
engines: {node: '>=14'}
@@ -2234,6 +2431,14 @@ packages:
deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me
hasBin: true
globals@14.0.0:
resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==}
engines: {node: '>=18'}
globals@15.15.0:
resolution: {integrity: sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==}
engines: {node: '>=18'}
gopd@1.2.0:
resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==}
engines: {node: '>= 0.4'}
@@ -2294,6 +2499,14 @@ packages:
ieee754@1.2.1:
resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==}
ignore@5.3.2:
resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==}
engines: {node: '>= 4'}
ignore@7.0.5:
resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==}
engines: {node: '>= 4'}
immer@11.1.8:
resolution: {integrity: sha512-/tbkHMW7y10Lx6i1crLjD4/OhNkRG+Fo7byZHtah0547nIeXYcpIXaUh0IAQY6gO5459qpGGYapcEOHtFXkIuA==}
@@ -2301,6 +2514,10 @@ packages:
resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==}
engines: {node: '>=6'}
imurmurhash@0.1.4:
resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==}
engines: {node: '>=0.8.19'}
indent-string@4.0.0:
resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==}
engines: {node: '>=8'}
@@ -2413,6 +2630,9 @@ packages:
engines: {node: '>=6'}
hasBin: true
json-buffer@3.0.1:
resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==}
json-parse-even-better-errors@2.3.1:
resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==}
@@ -2422,6 +2642,9 @@ packages:
json-schema-traverse@1.0.0:
resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==}
json-stable-stringify-without-jsonify@1.0.1:
resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==}
json5@2.2.3:
resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==}
engines: {node: '>=6'}
@@ -2456,6 +2679,13 @@ packages:
jws@4.0.1:
resolution: {integrity: sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==}
keyv@4.5.4:
resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==}
levn@0.4.1:
resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==}
engines: {node: '>= 0.8.0'}
libphonenumber-js@1.13.2:
resolution: {integrity: sha512-S3kmBrptp3yRTm83NUcHy9g1vbwiWMzI8WvY22+koBJ6zkRteLnedBL2VX0MIAGwx2yiyxX4J85pceZyQ6ffgg==}
@@ -2474,6 +2704,10 @@ packages:
resolution: {integrity: sha512-DFEqQ3ihfS9blba08cLfYf1NRAIEm+dDjic073DRDc3/JspI/8wYmtDsHwd3+4hwvdxSK7PGaElfTmm0awWJ4w==}
engines: {node: '>=6.11.5'}
locate-path@6.0.0:
resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==}
engines: {node: '>=10'}
lodash.includes@4.3.0:
resolution: {integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==}
@@ -2492,6 +2726,9 @@ packages:
lodash.isstring@4.0.1:
resolution: {integrity: sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==}
lodash.merge@4.6.2:
resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==}
lodash.once@4.1.1:
resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==}
@@ -2647,6 +2884,9 @@ packages:
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
hasBin: true
natural-compare@1.4.0:
resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
negotiator@0.6.3:
resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==}
engines: {node: '>= 0.6'}
@@ -2710,6 +2950,10 @@ packages:
resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==}
engines: {node: '>=6'}
optionator@0.9.4:
resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==}
engines: {node: '>= 0.8.0'}
ora@5.4.1:
resolution: {integrity: sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==}
engines: {node: '>=10'}
@@ -2718,6 +2962,14 @@ packages:
resolution: {integrity: sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==}
engines: {node: '>=0.10.0'}
p-limit@3.1.0:
resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==}
engines: {node: '>=10'}
p-locate@5.0.0:
resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==}
engines: {node: '>=10'}
package-json-from-dist@1.0.1:
resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==}
@@ -2751,6 +3003,10 @@ packages:
resolution: {integrity: sha512-cPLl+qZpSc+ireUvt+IzqbED1cHHkDoVYMo30jbJIdOOjQ1MQYZBPiNvmi8UM6lJuOpTPXJGZQk0DtC4y61MYQ==}
engines: {node: '>= 0.4.0'}
path-exists@4.0.0:
resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==}
engines: {node: '>=8'}
path-key@3.1.1:
resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==}
engines: {node: '>=8'}
@@ -2856,6 +3112,10 @@ packages:
resolution: {integrity: sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==}
engines: {node: ^10 || ^12 || >=14}
prelude-ls@1.2.1:
resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
engines: {node: '>= 0.8.0'}
prettier@3.8.3:
resolution: {integrity: sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==}
engines: {node: '>=14'}
@@ -3181,6 +3441,10 @@ packages:
resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==}
engines: {node: '>=8'}
strip-json-comments@3.1.1:
resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==}
engines: {node: '>=8'}
strtok3@10.3.5:
resolution: {integrity: sha512-ki4hZQfh5rX0QDLLkOCj+h+CVNkqmp/CMf8v8kZpkNVK6jGQooMytqzLZYUVYIZcFZ6yDB70EfD8POcFXiF5oA==}
engines: {node: '>=18'}
@@ -3351,6 +3615,12 @@ packages:
resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==}
hasBin: true
ts-api-utils@2.5.0:
resolution: {integrity: sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==}
engines: {node: '>=18.12'}
peerDependencies:
typescript: '>=4.8.4'
ts-interface-checker@0.1.13:
resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==}
@@ -3379,6 +3649,10 @@ packages:
tslib@2.8.1:
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
type-check@0.4.0:
resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
engines: {node: '>= 0.8.0'}
type-fest@0.21.3:
resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==}
engines: {node: '>=10'}
@@ -3390,6 +3664,13 @@ packages:
typedarray@0.0.6:
resolution: {integrity: sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==}
typescript-eslint@8.59.3:
resolution: {integrity: sha512-KgusgyDgG4LI8Ih/sWaCtZ06tckLAS5CvT5A4D1Q7bYVoAAyzwiZvE4BmwDHkhRVkvhRBepKeASoFzQetha7Fg==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
typescript: '>=4.8.4 <6.1.0'
typescript@5.7.2:
resolution: {integrity: sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==}
engines: {node: '>=14.17'}
@@ -3606,6 +3887,10 @@ packages:
engines: {node: '>=8'}
hasBin: true
word-wrap@1.2.5:
resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==}
engines: {node: '>=0.10.0'}
wrap-ansi@6.2.0:
resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==}
engines: {node: '>=8'}
@@ -3655,6 +3940,10 @@ packages:
resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==}
engines: {node: '>=6'}
yocto-queue@0.1.0:
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
engines: {node: '>=10'}
snapshots:
'@adobe/css-tools@4.4.4': {}
@@ -3920,6 +4209,52 @@ snapshots:
'@esbuild/win32-x64@0.21.5':
optional: true
'@eslint-community/eslint-utils@4.9.1(eslint@9.39.4(jiti@1.21.7))':
dependencies:
eslint: 9.39.4(jiti@1.21.7)
eslint-visitor-keys: 3.4.3
'@eslint-community/regexpp@4.12.2': {}
'@eslint/config-array@0.21.2':
dependencies:
'@eslint/object-schema': 2.1.7
debug: 4.4.3
minimatch: 3.1.5
transitivePeerDependencies:
- supports-color
'@eslint/config-helpers@0.4.2':
dependencies:
'@eslint/core': 0.17.0
'@eslint/core@0.17.0':
dependencies:
'@types/json-schema': 7.0.15
'@eslint/eslintrc@3.3.5':
dependencies:
ajv: 6.15.0
debug: 4.4.3
espree: 10.4.0
globals: 14.0.0
ignore: 5.3.2
import-fresh: 3.3.1
js-yaml: 4.1.1
minimatch: 3.1.5
strip-json-comments: 3.1.1
transitivePeerDependencies:
- supports-color
'@eslint/js@9.39.4': {}
'@eslint/object-schema@2.1.7': {}
'@eslint/plugin-kit@0.4.1':
dependencies:
'@eslint/core': 0.17.0
levn: 0.4.1
'@floating-ui/core@1.7.5':
dependencies:
'@floating-ui/utils': 0.2.11
@@ -3937,6 +4272,22 @@ snapshots:
'@floating-ui/utils@0.2.11': {}
'@humanfs/core@0.19.2':
dependencies:
'@humanfs/types': 0.15.0
'@humanfs/node@0.16.8':
dependencies:
'@humanfs/core': 0.19.2
'@humanfs/types': 0.15.0
'@humanwhocodes/retry': 0.4.3
'@humanfs/types@0.15.0': {}
'@humanwhocodes/module-importer@1.0.1': {}
'@humanwhocodes/retry@0.4.3': {}
'@isaacs/cliui@8.0.2':
dependencies:
string-width: 5.1.2
@@ -4782,6 +5133,97 @@ snapshots:
'@types/validator@13.15.10': {}
'@typescript-eslint/eslint-plugin@8.59.3(@typescript-eslint/parser@8.59.3(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3))(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3)':
dependencies:
'@eslint-community/regexpp': 4.12.2
'@typescript-eslint/parser': 8.59.3(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3)
'@typescript-eslint/scope-manager': 8.59.3
'@typescript-eslint/type-utils': 8.59.3(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3)
'@typescript-eslint/utils': 8.59.3(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3)
'@typescript-eslint/visitor-keys': 8.59.3
eslint: 9.39.4(jiti@1.21.7)
ignore: 7.0.5
natural-compare: 1.4.0
ts-api-utils: 2.5.0(typescript@5.9.3)
typescript: 5.9.3
transitivePeerDependencies:
- supports-color
'@typescript-eslint/parser@8.59.3(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3)':
dependencies:
'@typescript-eslint/scope-manager': 8.59.3
'@typescript-eslint/types': 8.59.3
'@typescript-eslint/typescript-estree': 8.59.3(typescript@5.9.3)
'@typescript-eslint/visitor-keys': 8.59.3
debug: 4.4.3
eslint: 9.39.4(jiti@1.21.7)
typescript: 5.9.3
transitivePeerDependencies:
- supports-color
'@typescript-eslint/project-service@8.59.3(typescript@5.9.3)':
dependencies:
'@typescript-eslint/tsconfig-utils': 8.59.3(typescript@5.9.3)
'@typescript-eslint/types': 8.59.3
debug: 4.4.3
typescript: 5.9.3
transitivePeerDependencies:
- supports-color
'@typescript-eslint/scope-manager@8.59.3':
dependencies:
'@typescript-eslint/types': 8.59.3
'@typescript-eslint/visitor-keys': 8.59.3
'@typescript-eslint/tsconfig-utils@8.59.3(typescript@5.9.3)':
dependencies:
typescript: 5.9.3
'@typescript-eslint/type-utils@8.59.3(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3)':
dependencies:
'@typescript-eslint/types': 8.59.3
'@typescript-eslint/typescript-estree': 8.59.3(typescript@5.9.3)
'@typescript-eslint/utils': 8.59.3(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3)
debug: 4.4.3
eslint: 9.39.4(jiti@1.21.7)
ts-api-utils: 2.5.0(typescript@5.9.3)
typescript: 5.9.3
transitivePeerDependencies:
- supports-color
'@typescript-eslint/types@8.59.3': {}
'@typescript-eslint/typescript-estree@8.59.3(typescript@5.9.3)':
dependencies:
'@typescript-eslint/project-service': 8.59.3(typescript@5.9.3)
'@typescript-eslint/tsconfig-utils': 8.59.3(typescript@5.9.3)
'@typescript-eslint/types': 8.59.3
'@typescript-eslint/visitor-keys': 8.59.3
debug: 4.4.3
minimatch: 10.2.5
semver: 7.8.0
tinyglobby: 0.2.16
ts-api-utils: 2.5.0(typescript@5.9.3)
typescript: 5.9.3
transitivePeerDependencies:
- supports-color
'@typescript-eslint/utils@8.59.3(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3)':
dependencies:
'@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4(jiti@1.21.7))
'@typescript-eslint/scope-manager': 8.59.3
'@typescript-eslint/types': 8.59.3
'@typescript-eslint/typescript-estree': 8.59.3(typescript@5.9.3)
eslint: 9.39.4(jiti@1.21.7)
typescript: 5.9.3
transitivePeerDependencies:
- supports-color
'@typescript-eslint/visitor-keys@8.59.3':
dependencies:
'@typescript-eslint/types': 8.59.3
eslint-visitor-keys: 5.0.1
'@vitejs/plugin-react@4.7.0(vite@5.4.21(@types/node@22.19.19)(terser@5.47.1))':
dependencies:
'@babel/core': 7.29.0
@@ -4937,6 +5379,10 @@ snapshots:
mime-types: 2.1.35
negotiator: 0.6.3
acorn-jsx@5.3.2(acorn@8.16.0):
dependencies:
acorn: 8.16.0
acorn-walk@8.3.5:
dependencies:
acorn: 8.16.0
@@ -5317,6 +5763,8 @@ snapshots:
deep-eql@5.0.2: {}
deep-is@0.1.4: {}
deepmerge@4.3.1: {}
defaults@1.0.4:
@@ -5440,13 +5888,77 @@ snapshots:
escape-string-regexp@1.0.5: {}
escape-string-regexp@4.0.0: {}
eslint-scope@5.1.1:
dependencies:
esrecurse: 4.3.0
estraverse: 4.3.0
eslint-scope@8.4.0:
dependencies:
esrecurse: 4.3.0
estraverse: 5.3.0
eslint-visitor-keys@3.4.3: {}
eslint-visitor-keys@4.2.1: {}
eslint-visitor-keys@5.0.1: {}
eslint@9.39.4(jiti@1.21.7):
dependencies:
'@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4(jiti@1.21.7))
'@eslint-community/regexpp': 4.12.2
'@eslint/config-array': 0.21.2
'@eslint/config-helpers': 0.4.2
'@eslint/core': 0.17.0
'@eslint/eslintrc': 3.3.5
'@eslint/js': 9.39.4
'@eslint/plugin-kit': 0.4.1
'@humanfs/node': 0.16.8
'@humanwhocodes/module-importer': 1.0.1
'@humanwhocodes/retry': 0.4.3
'@types/estree': 1.0.9
ajv: 6.15.0
chalk: 4.1.2
cross-spawn: 7.0.6
debug: 4.4.3
escape-string-regexp: 4.0.0
eslint-scope: 8.4.0
eslint-visitor-keys: 4.2.1
espree: 10.4.0
esquery: 1.7.0
esutils: 2.0.3
fast-deep-equal: 3.1.3
file-entry-cache: 8.0.0
find-up: 5.0.0
glob-parent: 6.0.2
ignore: 5.3.2
imurmurhash: 0.1.4
is-glob: 4.0.3
json-stable-stringify-without-jsonify: 1.0.1
lodash.merge: 4.6.2
minimatch: 3.1.5
natural-compare: 1.4.0
optionator: 0.9.4
optionalDependencies:
jiti: 1.21.7
transitivePeerDependencies:
- supports-color
espree@10.4.0:
dependencies:
acorn: 8.16.0
acorn-jsx: 5.3.2(acorn@8.16.0)
eslint-visitor-keys: 4.2.1
esprima@4.0.1: {}
esquery@1.7.0:
dependencies:
estraverse: 5.3.0
esrecurse@4.3.0:
dependencies:
estraverse: 5.3.0
@@ -5461,6 +5973,8 @@ snapshots:
dependencies:
'@types/estree': 1.0.9
esutils@2.0.3: {}
etag@1.8.1: {}
events@3.3.0: {}
@@ -5521,6 +6035,8 @@ snapshots:
fast-json-stable-stringify@2.1.0: {}
fast-levenshtein@2.0.6: {}
fast-safe-stringify@2.1.1: {}
fast-uri@3.1.2: {}
@@ -5539,6 +6055,10 @@ snapshots:
dependencies:
escape-string-regexp: 1.0.5
file-entry-cache@8.0.0:
dependencies:
flat-cache: 4.0.1
file-type@20.4.1:
dependencies:
'@tokenizer/inflate': 0.2.7
@@ -5564,6 +6084,18 @@ snapshots:
transitivePeerDependencies:
- supports-color
find-up@5.0.0:
dependencies:
locate-path: 6.0.0
path-exists: 4.0.0
flat-cache@4.0.1:
dependencies:
flatted: 3.4.2
keyv: 4.5.4
flatted@3.4.2: {}
foreground-child@3.3.1:
dependencies:
cross-spawn: 7.0.6
@@ -5669,6 +6201,10 @@ snapshots:
package-json-from-dist: 1.0.1
path-scurry: 1.11.1
globals@14.0.0: {}
globals@15.15.0: {}
gopd@1.2.0: {}
graceful-fs@4.2.11: {}
@@ -5729,6 +6265,10 @@ snapshots:
ieee754@1.2.1: {}
ignore@5.3.2: {}
ignore@7.0.5: {}
immer@11.1.8: {}
import-fresh@3.3.1:
@@ -5736,6 +6276,8 @@ snapshots:
parent-module: 1.0.1
resolve-from: 4.0.0
imurmurhash@0.1.4: {}
indent-string@4.0.0: {}
inherits@2.0.4: {}
@@ -5879,12 +6421,16 @@ snapshots:
jsesc@3.1.0: {}
json-buffer@3.0.1: {}
json-parse-even-better-errors@2.3.1: {}
json-schema-traverse@0.4.1: {}
json-schema-traverse@1.0.0: {}
json-stable-stringify-without-jsonify@1.0.1: {}
json5@2.2.3: {}
jsonc-parser@3.2.1: {}
@@ -5945,6 +6491,15 @@ snapshots:
jwa: 2.0.1
safe-buffer: 5.2.1
keyv@4.5.4:
dependencies:
json-buffer: 3.0.1
levn@0.4.1:
dependencies:
prelude-ls: 1.2.1
type-check: 0.4.0
libphonenumber-js@1.13.2: {}
lilconfig@3.1.3: {}
@@ -5955,6 +6510,10 @@ snapshots:
loader-runner@4.3.2: {}
locate-path@6.0.0:
dependencies:
p-locate: 5.0.0
lodash.includes@4.3.0: {}
lodash.isboolean@3.0.3: {}
@@ -5967,6 +6526,8 @@ snapshots:
lodash.isstring@4.0.1: {}
lodash.merge@4.6.2: {}
lodash.once@4.1.1: {}
lodash@4.17.21: {}
@@ -6097,6 +6658,8 @@ snapshots:
nanoid@3.3.12: {}
natural-compare@1.4.0: {}
negotiator@0.6.3: {}
neo-async@2.6.2: {}
@@ -6139,6 +6702,15 @@ snapshots:
dependencies:
mimic-fn: 2.1.0
optionator@0.9.4:
dependencies:
deep-is: 0.1.4
fast-levenshtein: 2.0.6
levn: 0.4.1
prelude-ls: 1.2.1
type-check: 0.4.0
word-wrap: 1.2.5
ora@5.4.1:
dependencies:
bl: 4.1.0
@@ -6153,6 +6725,14 @@ snapshots:
os-tmpdir@1.0.2: {}
p-limit@3.1.0:
dependencies:
yocto-queue: 0.1.0
p-locate@5.0.0:
dependencies:
p-limit: 3.1.0
package-json-from-dist@1.0.1: {}
parent-module@1.0.1:
@@ -6189,6 +6769,8 @@ snapshots:
pause: 0.0.1
utils-merge: 1.0.1
path-exists@4.0.0: {}
path-key@3.1.1: {}
path-parse@1.0.7: {}
@@ -6261,6 +6843,8 @@ snapshots:
picocolors: 1.1.1
source-map-js: 1.2.1
prelude-ls@1.2.1: {}
prettier@3.8.3: {}
pretty-format@27.5.1:
@@ -6615,6 +7199,8 @@ snapshots:
dependencies:
min-indent: 1.0.1
strip-json-comments@3.1.1: {}
strtok3@10.3.5:
dependencies:
'@tokenizer/token': 0.3.0
@@ -6779,6 +7365,10 @@ snapshots:
tree-kill@1.2.2: {}
ts-api-utils@2.5.0(typescript@5.9.3):
dependencies:
typescript: 5.9.3
ts-interface-checker@0.1.13: {}
ts-node@10.9.2(@swc/core@1.15.33)(@types/node@22.19.19)(typescript@5.9.3):
@@ -6816,6 +7406,10 @@ snapshots:
tslib@2.8.1: {}
type-check@0.4.0:
dependencies:
prelude-ls: 1.2.1
type-fest@0.21.3: {}
type-is@1.6.18:
@@ -6825,6 +7419,17 @@ snapshots:
typedarray@0.0.6: {}
typescript-eslint@8.59.3(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3):
dependencies:
'@typescript-eslint/eslint-plugin': 8.59.3(@typescript-eslint/parser@8.59.3(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3))(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3)
'@typescript-eslint/parser': 8.59.3(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3)
'@typescript-eslint/typescript-estree': 8.59.3(typescript@5.9.3)
'@typescript-eslint/utils': 8.59.3(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3)
eslint: 9.39.4(jiti@1.21.7)
typescript: 5.9.3
transitivePeerDependencies:
- supports-color
typescript@5.7.2: {}
typescript@5.9.3: {}
@@ -7047,6 +7652,8 @@ snapshots:
siginfo: 2.0.0
stackback: 0.0.2
word-wrap@1.2.5: {}
wrap-ansi@6.2.0:
dependencies:
ansi-styles: 4.3.0
@@ -7080,3 +7687,5 @@ snapshots:
yargs-parser@21.1.1: {}
yn@3.1.1: {}
yocto-queue@0.1.0: {}
+1 -1
View File
@@ -1,2 +1,2 @@
packages:
- "packages/*"
- 'packages/*'