diff --git a/.dependency-cruiser.jsonc b/.dependency-cruiser.jsonc new file mode 100644 index 00000000..55a9b248 --- /dev/null +++ b/.dependency-cruiser.jsonc @@ -0,0 +1,131 @@ +{ + "extends": "dependency-cruiser/configs/recommended-strict", + "forbidden": [ + { + "name": "no-orphans", + "from": { + "orphan": true, + "pathNot": [ + "(^|/)\\.[^/]+\\.(js|cjs|mjs|ts|json)$", + "\\.d\\.(c|m)?ts$", + "(^|/)tsconfig\\.json$", + "(^|/)(?:babel|webpack)\\.config\\.(?:js|cjs|mjs|ts|json)$", + "^scripts/run-node-tests[.](?:mts|ts)$", + "^scripts/tooling/build/rewrite-dts-imports[.](?:mts|ts)$", + "^scripts/tooling/dead-members[.]ts$", + "^scripts/tooling/eslint/plugins[.]ts$", + "^scripts/test/.+[.](?:mts|ts)$", + "^sdk/node/test/.+[.]ts$", + "^(?:sdk|sdk/node|testkit|interface)/vitest[.]config[.]mts$", + "^interface/vite[.]config[.]ts$", + "^interface/eslint[.]config[.]mts$", + ], + }, + }, + { + "name": "not-prod-to-test-source", + "comment": "Production source must not import test-only files. Keep test fixtures behind test/ boundaries.", + "severity": "error", + "from": { + "path": "^(?:(?:sdk|sdk/node|testkit|interface)/src/|scripts/)", + "pathNot": [ + "(?:^|/)test/", + "[.]test[.](?:mts|ts|tsx)$", + "(?:^|/)[^/]*(?:Suite[0-9]*[.]test|Suite|Test(?:Assertions|CommandFixtures|Constants|Fixtures|ProcessFixtures|RunFixtures|Support)|TestFixtures|TestSupport)[.](?:mts|ts|tsx)$", + "(?:^|/)[^/]*(?:Suite[0-9]*[.]test|Suite|Test(?:Assertions|CommandFixtures|Constants|Fixtures|ProcessFixtures|RunFixtures|Support)|TestFixtures|TestSupport)[.][^/]+[.](?:mts|ts|tsx)$", + "(?:^|/)[^/]*(?:_test_|_test_constants[.]|_test_fixtures[.]|_test_support[.])[^/]*$", + ], + }, + "to": { + "path": [ + "(?:^|/)test/", + "[.]test[.](?:mts|ts|tsx)$", + "(?:^|/)[^/]*(?:Suite[0-9]*[.]test|Suite|Test(?:Assertions|CommandFixtures|Constants|Fixtures|ProcessFixtures|RunFixtures|Support)|TestFixtures|TestSupport)[.](?:mts|ts|tsx)$", + "(?:^|/)[^/]*(?:Suite[0-9]*[.]test|Suite|Test(?:Assertions|CommandFixtures|Constants|Fixtures|ProcessFixtures|RunFixtures|Support)|TestFixtures|TestSupport)[.][^/]+[.](?:mts|ts|tsx)$", + "(?:^|/)[^/]*(?:_test_|_test_constants[.]|_test_fixtures[.]|_test_support[.])[^/]*$", + ], + }, + }, + { + "name": "not-prod-to-testkit", + "comment": "Production source must not import @ickb/testkit. Keep test harness dependencies under test/.", + "severity": "error", + "from": { + "path": "^(?:sdk|sdk/node|testkit|interface)/src/", + "pathNot": [ + "^testkit/src/", + "(?:^|/)test/", + "[.]test[.](?:mts|ts|tsx)$", + "(?:^|/)[^/]*(?:Suite[0-9]*[.]test|Suite|Test(?:Assertions|CommandFixtures|Constants|Fixtures|ProcessFixtures|RunFixtures|Support)|TestFixtures|TestSupport)[.](?:mts|ts|tsx)$", + "(?:^|/)[^/]*(?:Suite[0-9]*[.]test|Suite|Test(?:Assertions|CommandFixtures|Constants|Fixtures|ProcessFixtures|RunFixtures|Support)|TestFixtures|TestSupport)[.][^/]+[.](?:mts|ts|tsx)$", + "(?:^|/)[^/]*(?:_test_|_test_constants[.]|_test_fixtures[.]|_test_support[.])[^/]*$", + ], + }, + "to": { + "path": "^testkit/src/", + }, + }, + { + "name": "oracle-independent", + "comment": "The contract oracle imports nothing from the implementation it judges.", + "severity": "error", + "from": { "path": "^testkit/src/contract_oracle[.]ts$" }, + "to": {}, + }, + { + "name": "node-actors-apart", + "comment": "The bot, the sampler, and the stimulus generator share only src/shared; they never import each other.", + "severity": "error", + "from": { "path": "^sdk/node/src/(bot|sampler|stimulus)/" }, + "to": { + "path": "^sdk/node/src/(?:bot|sampler|stimulus)/", + "pathNot": "^sdk/node/src/$1/", + }, + }, + { + "name": "node-shared-neutral", + "comment": "src/shared serves every actor and depends on none of them.", + "severity": "error", + "from": { "path": "^sdk/node/src/shared/" }, + "to": { "path": "^sdk/node/src/(?:bot|sampler|stimulus)" }, + }, + { + "name": "sdk-browser-safe", + "comment": "The SDK runs in the browser: no Node built-ins.", + "severity": "error", + "from": { "path": "^sdk/src/" }, + "to": { "dependencyTypes": ["core"] }, + }, + { + "name": "sdk-not-node", + "comment": "The published SDK never imports the Node actors that live beside it.", + "severity": "error", + "from": { "path": "^sdk/src/" }, + "to": { "path": "^sdk/node/" }, + }, + { + "name": "no-library-to-actor", + "comment": "The SDK and the test kit must not depend on the actors.", + "severity": "error", + "from": { "path": "^(?:sdk/src|testkit)/" }, + "to": { "path": "^(?:interface|sdk/node)/" }, + }, + { + "name": "no-actor-to-actor", + "comment": "The interface and the Node actors must not depend on each other.", + "severity": "error", + "from": { "path": "^(interface|sdk/node)/" }, + "to": { "path": "^(?:interface|sdk/node)/", "pathNot": "^$1/" }, + }, + ], + "options": { + "exclude": { + "path": "(?:^|/)(?:coverage|dist)/", + }, + "includeOnly": "^(sdk|testkit|interface|scripts)/", + "tsConfig": { + "fileName": "tsconfig.json", + }, + "tsPreCompilationDeps": "specify", + }, +} diff --git a/.github/workflows/check.yaml b/.github/workflows/check.yaml index 7b518c0d..ee5b880f 100644 --- a/.github/workflows/check.yaml +++ b/.github/workflows/check.yaml @@ -5,19 +5,90 @@ on: push: branches: [master] +permissions: + contents: read + jobs: build: runs-on: ubuntu-latest + outputs: + interface: ${{ steps.releases.outputs.interface }} + sdk: ${{ steps.releases.outputs.sdk }} steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 with: - node-version: 24 - - uses: pnpm/action-setup@v4 - - - name: Install jq for fork bootstrap - run: sudo apt-get update -qq && sudo apt-get install -y -qq --no-install-recommends jq + persist-credentials: false + # The release rule below reads the previous commit. + fetch-depth: 2 + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 + with: + # The declared Node floor; the example units run the current release. + node-version: 22.19.0 + - uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 + - name: Install pinned dependencies + run: pnpm install --frozen-lockfile - name: Run pnpm check run: pnpm check + + # A release is a push to the default branch whose commit changes a package's version + # field. Every such push is one squash commit, so the + # previous commit is the comparison; a path absent there counts as changed. + - id: releases + if: github.event_name == 'push' && github.ref == format('refs/heads/{0}', github.event.repository.default_branch) + run: | + released() { + now=$(node -p "require('./$1/package.json').version") + before=$(git show "HEAD^:$1/package.json" 2>/dev/null | node -p "JSON.parse(require('fs').readFileSync(0, 'utf8')).version" 2>/dev/null || echo none) + if [ "$now" != "$before" ]; then echo true; else echo false; fi + } + echo "interface=$(released interface)" >> "$GITHUB_OUTPUT" + echo "sdk=$(released sdk)" >> "$GITHUB_OUTPUT" + - if: steps.releases.outputs.interface == 'true' + uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 + with: + path: interface/dist + + # Deploy the bundle the gate just built; a failed run leaves the previous site live. + deploy: + needs: build + if: needs.build.outputs.interface == 'true' + runs-on: ubuntu-latest + permissions: + pages: write + id-token: write + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + + steps: + - id: deployment + uses: actions/deploy-pages@368f82528645a54fb793d4d04e342629a3f51346 + + # npm trusted publishing: the id token stands in for a registry token, and npm records + # the provenance of this run with the package. + publish: + needs: build + if: needs.build.outputs.sdk == 'true' + runs-on: ubuntu-latest + permissions: + contents: read + id-token: write + + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 + with: + persist-credentials: false + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 + with: + node-version: 22.19.0 + - uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 + + - name: Install pinned dependencies + run: pnpm install --frozen-lockfile + - name: Build the SDK + run: pnpm build + # The checkout is a detached commit, so pnpm's branch and remote checks do not apply. + - name: Publish @ickb/sdk + run: pnpm --filter @ickb/sdk publish --provenance --access public --no-git-checks diff --git a/.gitignore b/.gitignore index 74e2f24e..27814d7d 100644 --- a/.gitignore +++ b/.gitignore @@ -2,15 +2,9 @@ **/node_modules/ # Artifacts +**/coverage/ **/dist/ -log/ -apps/*/log_*.json - -# Local runtime config files -config/ - -# Local logs -logs/ +**/*.tsbuildinfo # Local scratch files -.scratch/ +/private/ diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 00000000..433b6fdf --- /dev/null +++ b/.prettierignore @@ -0,0 +1,6 @@ +**/node_modules/ +**/dist/ +**/*.tsbuildinfo +coverage/ +.git/ +pnpm-lock.yaml diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000..67c43185 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,38 @@ +# Working in This Repository + +## Keep information with its owner + +The package READMEs and their focused documents describe behavior and its rationale. A comment at the code explains a surprising branch or constant. Repository-wide boundaries and release behavior live in the root [README](README.md); this file owns contributor procedure. + +- Put each material decision at the narrowest owner that covers its consumers. Keep the choice, decisive reason, useful rejected alternatives and accepted tradeoff together. Link from other readers rather than maintaining another account of the same decision. +- Do not require a README entry, a central record and a code comment for every decision. Add the local explanation where someone changing the implementation would otherwise miss it. +- Keep current rules in the tree. Git history preserves superseded plans and experiments; retain a rejected alternative only while it explains a choice that still matters. +- Keep `CLAUDE.md` a relative symlink to this file. + +## Correctness and change + +The deployed contracts and CKB consensus constrain every implementation choice. The owning documentation states the intended behavior; code and tests are evidence of what is implemented. When they disagree, establish which is wrong: fix an implementation regression rather than documenting it as intended behavior, and update documentation when behavior changes deliberately. + +- Reopen a settled decision for a contradiction, infeasibility or previously unweighed risk, not merely because a test passes. +- A finding earns new code for an observed or reproduced failure. Otherwise delete, narrow or leave it. Prefer deletion and inlining to abstractions without an evidenced shared reason to change. +- Follow the [workspace boundaries](README.md#workspace-boundaries) and [dependency policy](README.md#dependencies-and-checks). Do not change a dependency or runtime pin just to make something resolve. + +For a material revision: + +1. Pin the evidence and identify the behavior being changed. +2. Trace every affected consumer: the bot, generator, app and public SDK surface. For transaction changes, follow build, completion, signing, send, confirmation and the next rebuild. +3. Update the owning explanation with the implementation. Remove superseded rules and repair or delete pointers to them. +4. Run the gate and check its exit code. + +## The gate + +Run the [whole gate](README.md#dependencies-and-checks), `CI=true pnpm check`, once per commit and check its exit code. A green result on that tree makes the commit publishable. + +Keep each commit small and scoped to one concern. Its message explains the concern and what it prevents. Changes share a commit only when they share that reason to change. + +## Keys and secrets + +- Signing keys live in mode `0600` files outside the checkout, named by the unit. Never read the operator's key files. +- Keys are for signing only. Never pass a key, signer or secret-bearing context to a logger, event, error, test hook, formatter, redaction or masking helper, guard, or command text. Stop at the boundary; masking does not make the transfer safe. +- The tests use a canary from outside the production path. Never copy its value into a command, report or written file. +- If a secret appears in evidence, name only its location. Deployment's host-isolation assumption is documented with the [unit setup](sdk/node/README.md#systemd-deployment). diff --git a/CLAUDE.md b/CLAUDE.md new file mode 120000 index 00000000..47dc3e3d --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1 @@ +AGENTS.md \ No newline at end of file diff --git a/README.md b/README.md index 9bee59af..79307ff0 100644 --- a/README.md +++ b/README.md @@ -1,93 +1,76 @@ # iCKB Stack -iCKB Stack is the monorepo for the current TypeScript iCKB libraries and apps built on top of [CCC](https://github.com/ckb-devrel/ccc). +iCKB Stack is the TypeScript SDK, liquidity bot and browser interface for iCKB, built on [CCC](https://github.com/ckb-devrel/ccc). -## Transaction Completion Boundary +## Where to start -`@ickb/sdk` builders still return partial `ccc.Transaction` values. Callers explicitly choose when to finalize, and the shared completion path now also lives in `@ickb/sdk`. +- [SDK](sdk/README.md): read account state, build a funded conversion, sign, send and wait. +- [Node actors](sdk/node/README.md): run the bot, testnet stimulus generator or rate sampler. +- [Bot policy](sdk/node/docs/policy.md): matching, rebalancing, funding and their tradeoffs. +- [Interface](interface/README.md): run the app and understand its wallet and conversion model. +- [Maturity estimates](sdk/docs/pool_maturity_estimates.md): how the interface estimates when funds return. +- [Test kit](testkit/README.md) and [scripts](scripts/README.md): test helpers and repository tooling. +- [Working agreement](AGENTS.md): how to change and validate this tree. -Callers own the final completion pipeline: +Design rationale lives with the feature it explains: a reason comment for a branch or constant, or a section in its owning documentation for a choice spanning several functions. Follow the links above for both behavior and the reasons behind it. -1. Build the partial transaction through `IckbSdk` and the package managers. -2. Before send, call `sdk.completeTransaction(...)` or `completeIckbTransaction(...)` from `@ickb/sdk`. -3. Only then send the transaction. +## Workspace boundaries -Withdrawal requests built from public pool ready deposits may include `requiredLiveDeposits`. `@ickb/sdk` adds those cells as live `cell_dep` checks so a transaction fails if a protected pool anchor disappears before inclusion. +`sdk/` is the only published package, `@ickb/sdk`. It has one public entry point and depends at runtime only on `@ckb-ccc/core`. It contains no Node built-ins and never imports `sdk/node`, so integrators can use it in the browser. -## Scan Page Size Boundary +`sdk/node/`, `interface/` and `testkit/` are private workspaces. The three Node entrypoints share preflight, configuration and logging under `sdk/node/src/shared/`; they never import each other. Development, tests and bot deployments run from TypeScript source. The SDK's build emits `dist/` for publishing, not for local workspace execution. -Stack cell scans that feed account state, pool state, order books, or maturity estimates use a per-request page size. SDK state APIs expose it as `cellPageSize`; lower-level scan wrappers expose it as `pageSize` and pass it to CCC as `limit`. +The former library boundaries separated concepts rather than independent consumer contracts. One package removes the cost of maintaining several entry points and releases; its internal layering is a convention rather than a second dependency graph. There are no `advanced` or `internal` public entry points. The [SDK layout](sdk/README.md#layout) follows the on-chain scripts and the conversion workflow. -## User Lock Assumption +### User locks -Current stack flows assume user-owned cells are protected by locks whose signatures bind the whole transaction, such as standard `sighash` wallet flows. Passing a raw `ccc.Script` is only safe when that lock gives the same output and recipient binding. Delegated-signature or OTX-style locks are integration-specific and must account for the weak-lock boundary documented in the iCKB whitepaper and contracts audit. +Stack assumes user-owned cells have [locks whose signatures bind the whole transaction](https://github.com/ickb/contracts/blob/master/ICKB-Audit-Report.md#authorization-boundary), as standard sighash wallet flows do. An output lock does not execute when the output is created, so it cannot itself protect the recipient of that newly created output. Passing a raw `ccc.Script` is safe only when its lock provides the same input, output and recipient binding. Delegated-signature and OTX-style integrations must establish that separately. -## Workspace Map +## Dependencies and checks -Apps: +Use Node 22.19 or later and the pinned pnpm 12.4.2. From a plain checkout: -- `apps/bot`: Node order-fulfillment and rebalance bot for matching profitable orders, collecting owned orders, completing receipts and withdrawals, and rebalancing pool exposure. -- `apps/interface`: Browser interface for CCC wallet connection, conversion previews, transaction completion, signing, sending, and confirmation. -- `apps/sampler`: Mainnet sampling utility that writes historical iCKB exchange-rate CSV output. -- `apps/supervisor`: Deterministic live testnet supervisor for bounded bot/tester stress cycles, ignored artifacts, and incident bundles. -- `apps/tester`: Node simulator that creates random conversion orders to exercise the order and conversion flows. - -The Node app packages (`@ickb/bot`, `@ickb/sampler`, and `@ickb/tester`) publish their built entrypoints for distribution, but the supported reusable API surface lives in the packages below. `@ickb/interface` is a deployable browser app package and does not expose a library entrypoint. - -Packages: - -- `packages/core`: iCKB protocol primitives, cells, UDT conversion helpers, and low-level transaction builders. -- `packages/dao`: Nervos DAO cell classification, readiness, deposit, request, and withdrawal helpers. -- `packages/node-utils`: Private Node app utilities for env parsing, RPC client setup, signer locks, sleeps, and JSON logs. -- `packages/order`: UDT limit-order entities, grouping, matching, minting, melting, and deployed-script confusion mitigation. -- `packages/sdk`: Stack-level SDK that composes core, DAO, and order packages into account state, conversion planning, completion, sending, and confirmation helpers. -- `packages/testkit`: Private test helpers and fixtures for workspace tests. -- `packages/utils`: Shared low-level utilities such as complete-scan enforcement, binary search, collection helpers, and bounded subset selection. - -## Dependencies +```bash +pnpm install --frozen-lockfile +CI=true pnpm check +``` -CCC packages are normal package dependencies resolved through `pnpm-workspace.yaml` catalog entries and `pnpm-lock.yaml`. From a plain checkout, run `pnpm install`; no local CCC fork, build step, or workspace alias is required. +CCC is an ordinary dependency resolved through the catalog in `pnpm-workspace.yaml` and the lockfile. No local fork, build step or workspace alias is required. The catalog range is also what the published SDK gives integrators, so an exact catalog pin would constrain them too. -`pnpm check` is the validation gate. It always runs with `CI=true`. +The lockfile holds the CCC 1.23.0 set. CCC 1.23 made `mol.union` dynamic-size, so the order master pointer uses `mol.fixedUnion`, whose two variants are the same 36 bytes. The gate exercises core and the SDK paths; connector changes are only exercised by live wallet testing, so a CCC update that moves the connector is followed by one. -## Live Testnet Supervisor +TypeScript stays on 6. The 7.x package ships the native compiler only, without the JavaScript compiler API that typescript-eslint and `scripts/tooling/dead-members.ts` use, and typescript-eslint declares `typescript <6.1.0`. Type checking already runs on the native preview. -Provide ignored bounded configs, then run the supervisor from the repo root: +pnpm 12 supplies the trusted-publishing support the previous pin lacked. Its lockfile format is not backward-compatible with pnpm 10. The seven-day release age gives newly published dependencies time to be scrutinized; build-script permissions and advisory exceptions live beside their entries in [pnpm-workspace.yaml](pnpm-workspace.yaml). -```bash -pnpm live:supervisor -``` +`pnpm check` runs the audit, full lint suite and interface build against installed dependencies. CI first installs from the pinned lockfile, then runs the same gate on the declared Node floor. [Tooling](scripts/README.md#what-the-gate-enforces) explains the checks worth maintaining. -By default the supervisor uses ignored `config/bot-testnet.json` and `config/tester-testnet.json`, writes standalone artifacts under ignored `logs/live-supervisor//` paths, and runs deterministic bounded bot/tester commands only. +## Releases -Rebuild disposable live configs from `ICKB_TESTNET_BOT_PRIVATE_KEY` and `ICKB_TESTNET_TESTER_PRIVATE_KEY` with `pnpm live:config-from-env -- --force` when they are missing or stale; `ICKB_TESTNET_RPC_URL` is optional. The supervisor does not patch, verify, rebuild, relaunch, or invoke an LLM; external loops and operators consume `summary.json` between runs. +A push to the default branch releases a package only when its version differs from the previous commit. The reviewed version change is the release decision: -`pnpm live:preflight -- --config config/bot-testnet.json --role bot` prints public balance evidence for funding checks. Use `key.recommendedAddress` as the funding address, then rerun preflight and check `balances.CKB.available`, `balances.CKB.reserve`, `balances.CKB.spendable`, `balances.CKB.projectedAvailable`, `balances.CKB.total`, and `capital.minimumCkbCapital`; `available` and `spendable` are actual plain-cell values, while `projectedAvailable` and `total` are projected accounting values. For machine-readable JSON without package-manager output, run `node scripts/ickb-live-preflight.mjs --config config/bot-testnet.json --role bot` directly. +- A change to `sdk/package.json` publishes `@ickb/sdk` to npm through trusted publishing, with provenance and no registry token in the workflow. +- A change to `interface/package.json` deploys the bundle that the check job built to GitHub Pages at `ickb.org`. -For repeated bounded invocations, keep loop-owned options before `--` and supervisor options after it. The loop owns child run directories through `--out-root`, so do not pass supervisor `--out-dir` after `--`: +Both jobs require the gate to pass. Other merges release nothing. This assumes each reviewed change lands as a squash commit, so comparing with the previous commit covers it. Tags, a manual trigger or a second confirmation environment would add another release step for the same maintainer without another reviewer. A mistaken publish still consumes a version number and needs a bump. -```bash -pnpm live:supervisor:loop --max-runs 1 -- --scenario standard-cycle --max-cycles 1 -``` +The rewrite's versioned packages start at `9000.0.0`, above the old `@ickb/sdk` line at `1000.0.82`. The new API is not a patch of the old one, and keeping the package name avoids changing integrators' install lines. The high version preserves increasing version order; it is not a stability claim. A future lower-numbered line would need deliberate dist-tag management and would not satisfy a consumer's `^9000` range. -By default the loop prebuilds bot, tester, and supervisor runtime before the first run. Use loop-owned `--skip-build` only when another wrapper has already built those artifacts. Use loop-owned `--child-timeout-seconds` to bound the outer supervisor child process when running long watches; keep it long enough for the whole supervisor invocation, including actor preflights and actor commands, so the supervisor remains alive to enforce its own `--command-timeout-seconds` process-group cleanup. +### First release setup -For continuous live matching, use the dynamic external loop. It reads only tester preflight balance summaries, chooses `all-ckb-limit-order` when `CKB.available >= 3001`, otherwise chooses `ickb-to-ckb-limit-order` with `--tester-fee 1 --tester-fee-base 1000` when `CKB.available >= 2100` and `ICKB.available >= 100`, otherwise leaves the tester scenario as `auto`, then runs bounded `scripts/ickb-supervisor-loop.mjs` chunks: +The maintainer must configure the external services before the release merge: -```bash -pnpm live:supervisor:dynamic-loop -``` +1. Register the npm trusted publisher for `@ickb/sdk`: organization `ickb`, repository `stack`, workflow `check.yaml`, no environment. Configure the package to require two-factor authentication and disallow tokens. +2. Enable GitHub Pages with GitHub Actions as its source. Move the `ickb.org` domain from `ickb/legacy-lumos-interface` to this repository, then archive the old repository after the new site is live. -Dynamic validation sessions default to ignored `log/validation/dynamic-