@ic-reactor/mcp 0.0.0-stage → 0.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/ISSUE_TEMPLATE/bug_report.yml +108 -0
- package/.github/ISSUE_TEMPLATE/config.yml +8 -0
- package/.github/ISSUE_TEMPLATE/feature_request.yml +77 -0
- package/.github/actions/setup-rust-wasm/action.yml +25 -0
- package/.github/dependabot.yml +107 -0
- package/.github/workflows/auth-latest.yml +67 -0
- package/.github/workflows/b3pay-site.yml +49 -0
- package/.github/workflows/ci.yml +430 -0
- package/.github/workflows/codeql.yml +41 -0
- package/.github/workflows/dependency-review.yml +68 -0
- package/.github/workflows/docs-check.yml +98 -0
- package/.github/workflows/docs.yml +265 -0
- package/.github/workflows/e2e.yml +90 -0
- package/.github/workflows/examples-published.yml +97 -0
- package/.github/workflows/release.yml +326 -0
- package/.husky/pre-commit +12 -0
- package/.prettierignore +109 -0
- package/.prettierrc +6 -0
- package/AGENTS.md +103 -0
- package/CHANGELOG.md +3268 -0
- package/CLAUDE.md +93 -0
- package/CNAME +1 -0
- package/CODE_OF_CONDUCT.md +7 -0
- package/CONTRIBUTING.md +433 -0
- package/LICENSE +21 -0
- package/README.md +2 -2
- package/SECURITY.md +74 -0
- package/b3pay-module.json +194 -0
- package/docs/.remarkrc.json +10 -0
- package/docs/README.md +64 -0
- package/docs/astro.config.mjs +205 -0
- package/docs/favicon.svg +15 -0
- package/docs/package.json +39 -0
- package/docs/public/favicon.svg +15 -0
- package/docs/public/ic-reactor-social-card.png +0 -0
- package/docs/scripts/b3pay-static.mjs +22 -0
- package/docs/scripts/check-example-docs.mjs +214 -0
- package/docs/scripts/check-links.mjs +212 -0
- package/docs/scripts/fix-dist-links.js +125 -0
- package/docs/scripts/fix-frontmatter.js +71 -0
- package/docs/scripts/remove-redundant-h1.cjs +48 -0
- package/docs/scripts/rewrite-typedoc-links.js +110 -0
- package/docs/scripts/split-index.js +69 -0
- package/docs/src/assets/ic-reactor-hero-dark.svg +23 -0
- package/docs/src/assets/ic-reactor-hero-light.svg +23 -0
- package/docs/src/assets/ic-reactor-lockup-dark.svg +15 -0
- package/docs/src/assets/ic-reactor-lockup-light.svg +15 -0
- package/docs/src/assets/ic-reactor-mark-dark.svg +14 -0
- package/docs/src/assets/ic-reactor-mark-light.svg +14 -0
- package/docs/src/assets/layers/candid-core-layer-icon-dark.svg +4 -0
- package/docs/src/assets/layers/candid-core-layer-icon-light.svg +4 -0
- package/docs/src/assets/layers/candid-profile-layer-icon-dark.svg +4 -0
- package/docs/src/assets/layers/candid-profile-layer-icon-light.svg +4 -0
- package/docs/src/assets/layers/ic-reactor-layer-icon-dark.svg +4 -0
- package/docs/src/assets/layers/ic-reactor-layer-icon-light.svg +4 -0
- package/docs/src/components/LibraryCards.astro +172 -0
- package/docs/src/components/LibraryIcon.astro +28 -0
- package/docs/src/components/SiteTitle.astro +241 -0
- package/docs/src/content/docs/404.mdx +19 -0
- package/docs/src/content/docs/build/agent-tools.mdx +593 -0
- package/docs/src/content/docs/build/icrc1-wallet.mdx +619 -0
- package/docs/src/content/docs/build/ssr-per-user.mdx +552 -0
- package/docs/src/content/docs/examples/canister-studio.mdx +95 -0
- package/docs/src/content/docs/examples/icrc-ledger.mdx +45 -0
- package/docs/src/content/docs/examples/index.mdx +38 -0
- package/docs/src/content/docs/examples/next-ssr.mdx +73 -0
- package/docs/src/content/docs/examples/node-agent-tool.mdx +80 -0
- package/docs/src/content/docs/examples/vite-wallet.mdx +71 -0
- package/docs/src/content/docs/guides/auth.mdx +1181 -0
- package/docs/src/content/docs/guides/client.mdx +551 -0
- package/docs/src/content/docs/guides/deploy.mdx +180 -0
- package/docs/src/content/docs/guides/errors.mdx +475 -0
- package/docs/src/content/docs/guides/getting-started.mdx +620 -0
- package/docs/src/content/docs/guides/reads.mdx +586 -0
- package/docs/src/content/docs/guides/ssr.mdx +529 -0
- package/docs/src/content/docs/guides/testing.mdx +401 -0
- package/docs/src/content/docs/guides/values.mdx +261 -0
- package/docs/src/content/docs/guides/vite-plugin.mdx +690 -0
- package/docs/src/content/docs/guides/writes.mdx +572 -0
- package/docs/src/content/docs/ic-reactor/index.mdx +167 -0
- package/docs/src/content/docs/index.mdx +65 -0
- package/docs/src/content/docs/migrating-from-3.mdx +535 -0
- package/docs/src/content/docs/packages/core.mdx +186 -0
- package/docs/src/content/docs/packages/mcp.mdx +163 -0
- package/docs/src/content/docs/packages/react.mdx +207 -0
- package/docs/src/content/docs/packages/vite-plugin.mdx +73 -0
- package/docs/src/content/docs/versioning.mdx +104 -0
- package/docs/src/content.config.ts +7 -0
- package/docs/src/styles/custom.css +189 -0
- package/docs/tsconfig.typedoc.json +26 -0
- package/docs/typedoc-entry.ts +17 -0
- package/docs/typedoc.json +37 -0
- package/e2e/Cargo.lock +646 -0
- package/e2e/Cargo.toml +3 -0
- package/e2e/README.md +101 -0
- package/e2e/frontends/build.mjs +94 -0
- package/e2e/frontends/index.html +10 -0
- package/e2e/global-setup.ts +65 -0
- package/e2e/icp.yaml +38 -0
- package/e2e/package.json +40 -0
- package/e2e/setup.ts +6 -0
- package/e2e/src/actor/Cargo.toml +14 -0
- package/e2e/src/actor/hello_actor.did +10 -0
- package/e2e/src/actor/src/lib.rs +73 -0
- package/e2e/src/calls.test.ts +132 -0
- package/e2e/src/declarations/hello_actor.envelope.json +175 -0
- package/e2e/src/declarations/hello_actor.ts +15 -0
- package/e2e/src/errors.test.ts +107 -0
- package/e2e/src/frontend-headers.test.ts +253 -0
- package/e2e/src/identity.test.ts +61 -0
- package/e2e/src/react.test.tsx +59 -0
- package/e2e/src/replica.ts +70 -0
- package/e2e/src/vite-env.test.ts +108 -0
- package/e2e/test.sh +47 -0
- package/e2e/tsconfig.json +20 -0
- package/e2e/tsconfig.typecheck.json +9 -0
- package/e2e/vitest.config.ts +22 -0
- package/eslint.config.mjs +100 -0
- package/evals/PREREGISTRATION.md +1346 -0
- package/evals/README.md +756 -0
- package/evals/conditions/thin/condition.json +8 -0
- package/evals/conditions/thin/docs/candid-core-cli.README.md +90 -0
- package/evals/conditions/thin/docs/candid-core-schema.README.md +339 -0
- package/evals/conditions/thin/package.json +25 -0
- package/evals/conditions/thin/starter/src/generated/icrc1.ts +57 -0
- package/evals/conditions/thin-guide/condition.json +10 -0
- package/evals/conditions/thin-guide/docs/llms.txt +292 -0
- package/evals/conditions/v3/condition.json +9 -0
- package/evals/conditions/v3/docs/ic-reactor-core.llms.txt +232 -0
- package/evals/conditions/v3/docs/ic-reactor-react.llms.txt +280 -0
- package/evals/conditions/v3/docs/llms-full.txt +1053 -0
- package/evals/conditions/v3/package.json +26 -0
- package/evals/conditions/v3/starter/src/declarations/icrc1.did.d.ts +95 -0
- package/evals/conditions/v3/starter/src/declarations/icrc1.did.js +140 -0
- package/evals/conditions/v4/PORTING.md +384 -0
- package/evals/conditions/v4/README.md +106 -0
- package/evals/conditions/v4/condition.json +5 -0
- package/evals/conditions/v4/docs/llms.txt +323 -0
- package/evals/conditions/v4/package.json +27 -0
- package/evals/conditions/v4/starter/src/generated/icrc1.ts +78 -0
- package/evals/conditions/v4-proto/condition.json +6 -0
- package/evals/conditions/v4-proto/docs/llms.txt +291 -0
- package/evals/conditions/v4-proto/lib/SHIM-NOTES.md +20 -0
- package/evals/conditions/v4-proto/lib/package.json +27 -0
- package/evals/conditions/v4-proto/lib/src/client.ts +471 -0
- package/evals/conditions/v4-proto/lib/src/errors.ts +214 -0
- package/evals/conditions/v4-proto/lib/src/index.ts +36 -0
- package/evals/conditions/v4-proto/lib/src/principal.ts +175 -0
- package/evals/conditions/v4-proto/lib/src/service.ts +69 -0
- package/evals/conditions/v4-proto/lib/src/types.ts +174 -0
- package/evals/conditions/v4-proto/lib/test/runtime.test.ts +170 -0
- package/evals/conditions/v4-proto/lib/test/tsconfig.json +10 -0
- package/evals/conditions/v4-proto/lib/test/types.test-d.ts +145 -0
- package/evals/conditions/v4-proto/lib/test/vitest.config.mjs +8 -0
- package/evals/conditions/v4-proto/lib/tsconfig.json +17 -0
- package/evals/conditions/v4-proto/package.json +26 -0
- package/evals/conditions/v4-proto/starter/src/generated/icrc1.service.ts +19 -0
- package/evals/conditions/v4-proto/starter/src/generated/icrc1.ts +57 -0
- package/evals/drive.mjs +1221 -0
- package/evals/gate.mjs +161 -0
- package/evals/harness/aggregate.mjs +534 -0
- package/evals/harness/aggregate.test.mjs +344 -0
- package/evals/harness/assemble.mjs +216 -0
- package/evals/harness/assemble.test.mjs +143 -0
- package/evals/harness/auth.mjs +91 -0
- package/evals/harness/check-docs.mjs +157 -0
- package/evals/harness/declarations/icrc1.did.d.ts +95 -0
- package/evals/harness/declarations/icrc1.did.js +140 -0
- package/evals/harness/drive-auth.test.mjs +584 -0
- package/evals/harness/drive.test.mjs +509 -0
- package/evals/harness/fake-auth.ts +67 -0
- package/evals/harness/fake-ledger.ts +178 -0
- package/evals/harness/gate-plan.mjs +104 -0
- package/evals/harness/gate-plan.test.mjs +243 -0
- package/evals/harness/icrc1.did +88 -0
- package/evals/harness/judge.mjs +63 -0
- package/evals/harness/judge.test.mjs +81 -0
- package/evals/harness/leak-scan.mjs +1373 -0
- package/evals/harness/leak-scan.test.mjs +1300 -0
- package/evals/harness/probes/amounts.ts +14 -0
- package/evals/harness/probes/node-tool.probe.ts +48 -0
- package/evals/harness/probes/probe.mjs +149 -0
- package/evals/harness/probes/react-wallet.probe.ts +96 -0
- package/evals/harness/public/support/auth.ts +38 -0
- package/evals/harness/public/support/fake-replica.d.ts +28 -0
- package/evals/harness/public/support/fake-replica.js +434 -0
- package/evals/harness/public/support/ledger.idl.d.ts +95 -0
- package/evals/harness/public/support/ledger.idl.js +140 -0
- package/evals/harness/public/support/world.ts +179 -0
- package/evals/harness/runs.mjs +139 -0
- package/evals/harness/sandbox.mjs +88 -0
- package/evals/harness/sandbox.test.mjs +134 -0
- package/evals/harness/ship.mjs +264 -0
- package/evals/harness/ship.test.mjs +552 -0
- package/evals/harness/world.ts +291 -0
- package/evals/package.json +26 -0
- package/evals/pnpm-lock.yaml +1537 -0
- package/evals/pnpm-workspace.yaml +12 -0
- package/evals/results/2026-09-30T14-22-05-924Z/pilot.json +384 -0
- package/evals/results/2026-09-30T14-22-05-924Z/pilot.rescanned.json +404 -0
- package/evals/results/2026-09-30T14-22-05-924Z/pilot.rescanned.rescored.json +404 -0
- package/evals/results/2026-09-30T14-22-05-924Z/plan.json +240 -0
- package/evals/results/2026-09-30T14-22-05-924Z/summary.json +1800 -0
- package/evals/results/2026-09-30T14-22-05-924Z/summary.rescanned.json +1843 -0
- package/evals/results/2026-09-30T14-22-05-924Z/summary.rescanned.rescored.json +2100 -0
- package/evals/results/2026-09-30T15-48-23-343Z/pilot.json +404 -0
- package/evals/results/2026-09-30T15-48-23-343Z/pilot.rescored.json +404 -0
- package/evals/results/2026-09-30T15-48-23-343Z/plan.json +242 -0
- package/evals/results/2026-09-30T15-48-23-343Z/summary.json +1843 -0
- package/evals/results/2026-09-30T15-48-23-343Z/summary.rescored.json +2195 -0
- package/evals/results/2026-10-03-beta1-gate/pilot.json +162 -0
- package/evals/results/2026-10-03-beta1-gate/plan.json +141 -0
- package/evals/results/2026-10-03-beta1-gate/summary.json +983 -0
- package/evals/results/2026-10-05-ga-gate/contaminated-run.json +55 -0
- package/evals/results/2026-10-05-ga-gate/pilot.json +162 -0
- package/evals/results/2026-10-05-ga-gate/plan.json +457 -0
- package/evals/results/2026-10-05-ga-gate/summary.json +1037 -0
- package/evals/results/2026-10-05-ga-gate/summary.rescanned.json +1037 -0
- package/evals/results/README.md +31 -0
- package/evals/runs-pilot.log +150 -0
- package/evals/runs-pilot2.log +150 -0
- package/evals/score.mjs +256 -0
- package/evals/setup.mjs +547 -0
- package/evals/tasks/node-tool/faulty/thin-accepts-exponent/meta.json +5 -0
- package/evals/tasks/node-tool/faulty/thin-accepts-exponent/src/index.ts +121 -0
- package/evals/tasks/node-tool/faulty/thin-anonymous-transfer/meta.json +5 -0
- package/evals/tasks/node-tool/faulty/thin-anonymous-transfer/src/index.ts +116 -0
- package/evals/tasks/node-tool/faulty/thin-every-reject-may-have-executed/meta.json +9 -0
- package/evals/tasks/node-tool/faulty/thin-every-reject-may-have-executed/src/index.ts +118 -0
- package/evals/tasks/node-tool/faulty/thin-fetches-root-key-off-local/meta.json +5 -0
- package/evals/tasks/node-tool/faulty/thin-fetches-root-key-off-local/src/index.ts +118 -0
- package/evals/tasks/node-tool/faulty/thin-ignores-root-key/meta.json +5 -0
- package/evals/tasks/node-tool/faulty/thin-ignores-root-key/src/index.ts +117 -0
- package/evals/tasks/node-tool/faulty/thin-misclassifies-reject-codes/meta.json +9 -0
- package/evals/tasks/node-tool/faulty/thin-misclassifies-reject-codes/src/index.ts +119 -0
- package/evals/tasks/node-tool/faulty/thin-no-nat64-cap/meta.json +9 -0
- package/evals/tasks/node-tool/faulty/thin-no-nat64-cap/src/index.ts +118 -0
- package/evals/tasks/node-tool/faulty/thin-number-amount/meta.json +10 -0
- package/evals/tasks/node-tool/faulty/thin-number-amount/src/index.ts +117 -0
- package/evals/tasks/node-tool/faulty/thin-truncates-fraction-digits/meta.json +5 -0
- package/evals/tasks/node-tool/faulty/thin-truncates-fraction-digits/src/index.ts +120 -0
- package/evals/tasks/node-tool/faulty/thin-unvalidated-recipient/meta.json +5 -0
- package/evals/tasks/node-tool/faulty/thin-unvalidated-recipient/src/index.ts +118 -0
- package/evals/tasks/node-tool/faulty/v3-retry-call-errors/meta.json +11 -0
- package/evals/tasks/node-tool/faulty/v3-retry-call-errors/src/index.ts +149 -0
- package/evals/tasks/node-tool/faulty/v4-anonymous-identity-sent/meta.json +5 -0
- package/evals/tasks/node-tool/faulty/v4-anonymous-identity-sent/src/index.ts +71 -0
- package/evals/tasks/node-tool/faulty/v4-never-may-have-executed/meta.json +9 -0
- package/evals/tasks/node-tool/faulty/v4-never-may-have-executed/src/index.ts +84 -0
- package/evals/tasks/node-tool/faulty/v4-proto-never-may-have-executed/meta.json +9 -0
- package/evals/tasks/node-tool/faulty/v4-proto-never-may-have-executed/src/index.ts +72 -0
- package/evals/tasks/node-tool/faulty/v4-proto-refuses-nat64-max/meta.json +5 -0
- package/evals/tasks/node-tool/faulty/v4-proto-refuses-nat64-max/src/index.ts +72 -0
- package/evals/tasks/node-tool/faulty/v4-refuses-nat64-max/meta.json +5 -0
- package/evals/tasks/node-tool/faulty/v4-refuses-nat64-max/src/index.ts +84 -0
- package/evals/tasks/node-tool/hidden/node-tool.test.ts +312 -0
- package/evals/tasks/node-tool/prompt.md +55 -0
- package/evals/tasks/node-tool/prompt.minimal.md +30 -0
- package/evals/tasks/node-tool/solutions/thin/reference/src/index.ts +118 -0
- package/evals/tasks/node-tool/solutions/thin/reference-module-scope/src/index.ts +145 -0
- package/evals/tasks/node-tool/solutions/thin-guide/reference/src/index.ts +118 -0
- package/evals/tasks/node-tool/solutions/thin-guide/reference-module-scope/src/index.ts +145 -0
- package/evals/tasks/node-tool/solutions/v3/reference/src/index.ts +140 -0
- package/evals/tasks/node-tool/solutions/v3/reference-module-scope/src/index.ts +153 -0
- package/evals/tasks/node-tool/solutions/v4/reference/src/index.ts +84 -0
- package/evals/tasks/node-tool/solutions/v4/reference-module-scope/src/index.ts +105 -0
- package/evals/tasks/node-tool/solutions/v4-proto/reference/src/index.ts +72 -0
- package/evals/tasks/node-tool/solutions/v4-proto/reference-module-scope/src/index.ts +87 -0
- package/evals/tasks/node-tool/starter/src/contract.ts +31 -0
- package/evals/tasks/node-tool/starter/src/index.ts +7 -0
- package/evals/tasks/node-tool/starter/test/smoke.test.ts +22 -0
- package/evals/tasks/node-tool/starter/tsconfig.json +17 -0
- package/evals/tasks/node-tool/task.json +63 -0
- package/evals/tasks/react-wallet/faulty/thin-accepts-exponent/meta.json +5 -0
- package/evals/tasks/react-wallet/faulty/thin-accepts-exponent/src/Wallet.tsx +216 -0
- package/evals/tasks/react-wallet/faulty/thin-anonymous-transfer/meta.json +5 -0
- package/evals/tasks/react-wallet/faulty/thin-anonymous-transfer/src/Wallet.tsx +212 -0
- package/evals/tasks/react-wallet/faulty/thin-both-auth-buttons/meta.json +5 -0
- package/evals/tasks/react-wallet/faulty/thin-both-auth-buttons/src/Wallet.tsx +210 -0
- package/evals/tasks/react-wallet/faulty/thin-constant-balance-key/meta.json +8 -0
- package/evals/tasks/react-wallet/faulty/thin-constant-balance-key/src/Wallet.tsx +213 -0
- package/evals/tasks/react-wallet/faulty/thin-no-nat64-cap/meta.json +9 -0
- package/evals/tasks/react-wallet/faulty/thin-no-nat64-cap/src/Wallet.tsx +213 -0
- package/evals/tasks/react-wallet/faulty/thin-number-balance/meta.json +5 -0
- package/evals/tasks/react-wallet/faulty/thin-number-balance/src/Wallet.tsx +213 -0
- package/evals/tasks/react-wallet/faulty/thin-transfer-as-query/meta.json +5 -0
- package/evals/tasks/react-wallet/faulty/thin-transfer-as-query/src/Wallet.tsx +254 -0
- package/evals/tasks/react-wallet/faulty/thin-truncates-fraction-digits/meta.json +5 -0
- package/evals/tasks/react-wallet/faulty/thin-truncates-fraction-digits/src/Wallet.tsx +215 -0
- package/evals/tasks/react-wallet/faulty/thin-unguarded-balance-effect/meta.json +5 -0
- package/evals/tasks/react-wallet/faulty/thin-unguarded-balance-effect/src/Wallet.tsx +214 -0
- package/evals/tasks/react-wallet/faulty/v3-http-4xx-unknown/meta.json +5 -0
- package/evals/tasks/react-wallet/faulty/v3-http-4xx-unknown/src/Wallet.tsx +224 -0
- package/evals/tasks/react-wallet/faulty/v3-mutation-retry-3/meta.json +12 -0
- package/evals/tasks/react-wallet/faulty/v3-mutation-retry-3/src/Wallet.tsx +225 -0
- package/evals/tasks/react-wallet/faulty/v3-unknown-as-error/meta.json +11 -0
- package/evals/tasks/react-wallet/faulty/v3-unknown-as-error/src/Wallet.tsx +182 -0
- package/evals/tasks/react-wallet/faulty/v4-every-reject-unknown/meta.json +5 -0
- package/evals/tasks/react-wallet/faulty/v4-every-reject-unknown/src/Wallet.tsx +160 -0
- package/evals/tasks/react-wallet/faulty/v4-keep-previous-data/meta.json +5 -0
- package/evals/tasks/react-wallet/faulty/v4-keep-previous-data/src/Wallet.tsx +166 -0
- package/evals/tasks/react-wallet/faulty/v4-proto-every-reject-unknown/meta.json +5 -0
- package/evals/tasks/react-wallet/faulty/v4-proto-every-reject-unknown/src/Wallet.tsx +161 -0
- package/evals/tasks/react-wallet/faulty/v4-proto-keep-previous-data/meta.json +5 -0
- package/evals/tasks/react-wallet/faulty/v4-proto-keep-previous-data/src/Wallet.tsx +163 -0
- package/evals/tasks/react-wallet/faulty/v4-proto-retry-spread/meta.json +12 -0
- package/evals/tasks/react-wallet/faulty/v4-proto-retry-spread/src/Wallet.tsx +162 -0
- package/evals/tasks/react-wallet/faulty/v4-proto-status-not-idle/meta.json +5 -0
- package/evals/tasks/react-wallet/faulty/v4-proto-status-not-idle/src/Wallet.tsx +163 -0
- package/evals/tasks/react-wallet/faulty/v4-retry-spread/meta.json +12 -0
- package/evals/tasks/react-wallet/faulty/v4-retry-spread/src/Wallet.tsx +163 -0
- package/evals/tasks/react-wallet/faulty/v4-status-not-idle/meta.json +5 -0
- package/evals/tasks/react-wallet/faulty/v4-status-not-idle/src/Wallet.tsx +162 -0
- package/evals/tasks/react-wallet/hidden/react-wallet.test.ts +536 -0
- package/evals/tasks/react-wallet/prompt.md +88 -0
- package/evals/tasks/react-wallet/prompt.minimal.md +51 -0
- package/evals/tasks/react-wallet/solutions/thin/reference/src/Wallet.tsx +213 -0
- package/evals/tasks/react-wallet/solutions/thin/reference-module-scope/src/Wallet.tsx +227 -0
- package/evals/tasks/react-wallet/solutions/thin-guide/reference/src/Wallet.tsx +213 -0
- package/evals/tasks/react-wallet/solutions/thin-guide/reference-module-scope/src/Wallet.tsx +227 -0
- package/evals/tasks/react-wallet/solutions/v3/reference/src/Wallet.tsx +224 -0
- package/evals/tasks/react-wallet/solutions/v3/reference-module-scope/src/Wallet.tsx +230 -0
- package/evals/tasks/react-wallet/solutions/v4/reference/src/Wallet.tsx +160 -0
- package/evals/tasks/react-wallet/solutions/v4/reference-module-scope/src/Wallet.tsx +178 -0
- package/evals/tasks/react-wallet/solutions/v4-proto/reference/src/Wallet.tsx +161 -0
- package/evals/tasks/react-wallet/solutions/v4-proto/reference-module-scope/src/Wallet.tsx +161 -0
- package/evals/tasks/react-wallet/starter/src/Wallet.tsx +12 -0
- package/evals/tasks/react-wallet/starter/src/auth.ts +16 -0
- package/evals/tasks/react-wallet/starter/src/config.ts +14 -0
- package/evals/tasks/react-wallet/starter/src/ii-auth.ts +43 -0
- package/evals/tasks/react-wallet/starter/src/main.tsx +15 -0
- package/evals/tasks/react-wallet/starter/test/smoke.test.ts +34 -0
- package/evals/tasks/react-wallet/starter/tsconfig.json +18 -0
- package/evals/tasks/react-wallet/task.json +84 -0
- package/examples/canister-studio/.stackblitzrc +4 -0
- package/examples/canister-studio/README.md +183 -0
- package/examples/canister-studio/did/ckbtc_minter.did +806 -0
- package/examples/canister-studio/did/icp_ledger.did +586 -0
- package/examples/canister-studio/did/icrc1.did +88 -0
- package/examples/canister-studio/package.json +45 -0
- package/examples/canister-studio/scripts/smoke.ts +188 -0
- package/examples/canister-studio/src/calls/CacheKeyPanel.tsx +104 -0
- package/examples/canister-studio/src/calls/ConfirmWrite.tsx +265 -0
- package/examples/canister-studio/src/calls/ErrorPanel.test.tsx +113 -0
- package/examples/canister-studio/src/calls/ErrorPanel.tsx +214 -0
- package/examples/canister-studio/src/calls/MethodRunner.test.tsx +354 -0
- package/examples/canister-studio/src/calls/MethodRunner.tsx +57 -0
- package/examples/canister-studio/src/calls/ReadRunner.tsx +244 -0
- package/examples/canister-studio/src/calls/ResendPanel.tsx +165 -0
- package/examples/canister-studio/src/calls/ShareLink.tsx +59 -0
- package/examples/canister-studio/src/calls/WriteRunner.test.tsx +678 -0
- package/examples/canister-studio/src/calls/WriteRunner.tsx +670 -0
- package/examples/canister-studio/src/calls/dedupe.ts +189 -0
- package/examples/canister-studio/src/calls/error-info.ts +168 -0
- package/examples/canister-studio/src/calls/history.test.ts +284 -0
- package/examples/canister-studio/src/calls/history.ts +287 -0
- package/examples/canister-studio/src/calls/kinds.ts +68 -0
- package/examples/canister-studio/src/calls/prepare.ts +32 -0
- package/examples/canister-studio/src/calls/query-options.ts +30 -0
- package/examples/canister-studio/src/calls/read.ts +47 -0
- package/examples/canister-studio/src/calls/readback.ts +100 -0
- package/examples/canister-studio/src/calls/record.ts +123 -0
- package/examples/canister-studio/src/calls/safe-calls.test.ts +299 -0
- package/examples/canister-studio/src/calls/share.ts +46 -0
- package/examples/canister-studio/src/calls/testing/doubles.tsx +108 -0
- package/examples/canister-studio/src/calls/testing/fixtures.ts +185 -0
- package/examples/canister-studio/src/calls/useCanisterHandle.ts +35 -0
- package/examples/canister-studio/src/canisters/ckbtc_minter.envelope.json +4228 -0
- package/examples/canister-studio/src/canisters/ckbtc_minter.ts +868 -0
- package/examples/canister-studio/src/canisters/icp_ledger.envelope.json +3454 -0
- package/examples/canister-studio/src/canisters/icp_ledger.ts +584 -0
- package/examples/canister-studio/src/canisters/icrc1.envelope.json +840 -0
- package/examples/canister-studio/src/canisters/icrc1.ts +78 -0
- package/examples/canister-studio/src/components/ActionButton.tsx +32 -0
- package/examples/canister-studio/src/components/AppHeader.tsx +83 -0
- package/examples/canister-studio/src/components/AppShell.tsx +29 -0
- package/examples/canister-studio/src/components/AuthButton.tsx +79 -0
- package/examples/canister-studio/src/components/Badge.tsx +79 -0
- package/examples/canister-studio/src/components/CanisterHeader.tsx +200 -0
- package/examples/canister-studio/src/components/CanisterJump.tsx +130 -0
- package/examples/canister-studio/src/components/Card.tsx +64 -0
- package/examples/canister-studio/src/components/Code.tsx +101 -0
- package/examples/canister-studio/src/components/CopyButton.tsx +126 -0
- package/examples/canister-studio/src/components/Disclosure.tsx +34 -0
- package/examples/canister-studio/src/components/EmptyState.tsx +34 -0
- package/examples/canister-studio/src/components/MethodList.test.tsx +137 -0
- package/examples/canister-studio/src/components/MethodList.tsx +244 -0
- package/examples/canister-studio/src/components/Notice.tsx +36 -0
- package/examples/canister-studio/src/components/ProvenanceBadge.tsx +44 -0
- package/examples/canister-studio/src/components/RouteError.tsx +42 -0
- package/examples/canister-studio/src/components/SectionBoundary.tsx +76 -0
- package/examples/canister-studio/src/components/Spinner.tsx +33 -0
- package/examples/canister-studio/src/components/Tabs.tsx +96 -0
- package/examples/canister-studio/src/components/canister/CanisterLoadFailure.test.tsx +56 -0
- package/examples/canister-studio/src/components/canister/CanisterLoadFailure.tsx +165 -0
- package/examples/canister-studio/src/components/canister/CanisterNotFound.tsx +59 -0
- package/examples/canister-studio/src/components/canister/CanisterPending.tsx +33 -0
- package/examples/canister-studio/src/components/canister/MethodOverview.tsx +93 -0
- package/examples/canister-studio/src/components/canister/MethodWorkspace.test.tsx +201 -0
- package/examples/canister-studio/src/components/canister/MethodWorkspace.tsx +187 -0
- package/examples/canister-studio/src/components/canister/shared-call.test.ts +186 -0
- package/examples/canister-studio/src/components/canister/shared-call.ts +133 -0
- package/examples/canister-studio/src/components/canister/shared-link-loader.test.ts +116 -0
- package/examples/canister-studio/src/components/canister/use-canister-page.ts +44 -0
- package/examples/canister-studio/src/components/canister/workspace-key.test.ts +27 -0
- package/examples/canister-studio/src/components/canister/workspace-key.ts +36 -0
- package/examples/canister-studio/src/components/canister-id-input.ts +69 -0
- package/examples/canister-studio/src/components/canister-names.ts +11 -0
- package/examples/canister-studio/src/components/download.ts +34 -0
- package/examples/canister-studio/src/components/fallbacks.tsx +33 -0
- package/examples/canister-studio/src/components/helpers.test.ts +107 -0
- package/examples/canister-studio/src/components/history/HistoryList.tsx +284 -0
- package/examples/canister-studio/src/components/home/FeaturedLedger.tsx +149 -0
- package/examples/canister-studio/src/components/home/OpenCanister.tsx +99 -0
- package/examples/canister-studio/src/components/home/featured.test.tsx +92 -0
- package/examples/canister-studio/src/components/home/featured.ts +64 -0
- package/examples/canister-studio/src/components/interface/CandidTab.tsx +38 -0
- package/examples/canister-studio/src/components/interface/CompatReport.tsx +92 -0
- package/examples/canister-studio/src/components/interface/CompatibilityTab.tsx +190 -0
- package/examples/canister-studio/src/components/interface/ProjectTab.tsx +196 -0
- package/examples/canister-studio/src/components/interface/TypeScriptTab.tsx +114 -0
- package/examples/canister-studio/src/components/interface/tools.test.tsx +169 -0
- package/examples/canister-studio/src/components/modes.ts +31 -0
- package/examples/canister-studio/src/components/playground/MethodPicker.tsx +67 -0
- package/examples/canister-studio/src/components/playground/PlaygroundForms.tsx +112 -0
- package/examples/canister-studio/src/components/playground/PlaygroundTools.tsx +219 -0
- package/examples/canister-studio/src/components/playground/sample.ts +43 -0
- package/examples/canister-studio/src/components/playground/target.test.tsx +86 -0
- package/examples/canister-studio/src/components/primitives.test.tsx +121 -0
- package/examples/canister-studio/src/components/provenance.ts +38 -0
- package/examples/canister-studio/src/components/route-render.test.tsx +71 -0
- package/examples/canister-studio/src/components/use-task.ts +56 -0
- package/examples/canister-studio/src/components/wallet/BalancePanel.tsx +110 -0
- package/examples/canister-studio/src/components/wallet/LedgerSummary.tsx +63 -0
- package/examples/canister-studio/src/components/wallet/MinterPanel.tsx +255 -0
- package/examples/canister-studio/src/components/wallet/SendPanel.test.tsx +297 -0
- package/examples/canister-studio/src/components/wallet/SendPanel.tsx +446 -0
- package/examples/canister-studio/src/components/wallet/ledgers.test.ts +36 -0
- package/examples/canister-studio/src/components/wallet/ledgers.ts +111 -0
- package/examples/canister-studio/src/forms/JsonPanel.tsx +157 -0
- package/examples/canister-studio/src/forms/MethodForm.test.tsx +369 -0
- package/examples/canister-studio/src/forms/MethodForm.tsx +368 -0
- package/examples/canister-studio/src/forms/TypedForm.tsx +55 -0
- package/examples/canister-studio/src/forms/context.ts +62 -0
- package/examples/canister-studio/src/forms/convert.ts +131 -0
- package/examples/canister-studio/src/forms/edge.test.ts +252 -0
- package/examples/canister-studio/src/forms/engine.test.ts +358 -0
- package/examples/canister-studio/src/forms/engine.ts +1492 -0
- package/examples/canister-studio/src/forms/fields/BytesField.tsx +210 -0
- package/examples/canister-studio/src/forms/fields/Field.tsx +106 -0
- package/examples/canister-studio/src/forms/fields/FieldShell.tsx +101 -0
- package/examples/canister-studio/src/forms/fields/Group.tsx +74 -0
- package/examples/canister-studio/src/forms/fields/ListField.tsx +256 -0
- package/examples/canister-studio/src/forms/fields/Segmented.tsx +78 -0
- package/examples/canister-studio/src/forms/fields/TimestampField.tsx +108 -0
- package/examples/canister-studio/src/forms/fields/UnitsField.tsx +119 -0
- package/examples/canister-studio/src/forms/fields/composites.tsx +389 -0
- package/examples/canister-studio/src/forms/fields/leaves.tsx +350 -0
- package/examples/canister-studio/src/forms/fields.test.tsx +396 -0
- package/examples/canister-studio/src/forms/helpers.test.ts +225 -0
- package/examples/canister-studio/src/forms/hints.ts +128 -0
- package/examples/canister-studio/src/forms/json.ts +119 -0
- package/examples/canister-studio/src/forms/parsing.test.ts +217 -0
- package/examples/canister-studio/src/forms/useMethodForm.ts +251 -0
- package/examples/canister-studio/src/interface/browser.test.ts +23 -0
- package/examples/canister-studio/src/interface/browser.ts +153 -0
- package/examples/canister-studio/src/interface/cache.ts +68 -0
- package/examples/canister-studio/src/interface/cli-cache.test.ts +155 -0
- package/examples/canister-studio/src/interface/cli-cache.ts +125 -0
- package/examples/canister-studio/src/interface/compile.server.test.ts +198 -0
- package/examples/canister-studio/src/interface/compile.server.ts +262 -0
- package/examples/canister-studio/src/interface/compile.test.ts +108 -0
- package/examples/canister-studio/src/interface/compile.ts +305 -0
- package/examples/canister-studio/src/interface/docs.test.ts +75 -0
- package/examples/canister-studio/src/interface/docs.ts +100 -0
- package/examples/canister-studio/src/interface/functions.test.ts +42 -0
- package/examples/canister-studio/src/interface/functions.ts +200 -0
- package/examples/canister-studio/src/interface/live-candid.server.ts +91 -0
- package/examples/canister-studio/src/interface/live-loader.test.ts +515 -0
- package/examples/canister-studio/src/interface/live-loader.ts +364 -0
- package/examples/canister-studio/src/interface/load.test.ts +328 -0
- package/examples/canister-studio/src/interface/load.ts +249 -0
- package/examples/canister-studio/src/interface/read-candid.test.ts +185 -0
- package/examples/canister-studio/src/interface/read-candid.ts +233 -0
- package/examples/canister-studio/src/interface/server-result.ts +42 -0
- package/examples/canister-studio/src/interface/typed.ts +68 -0
- package/examples/canister-studio/src/interface/types.ts +121 -0
- package/examples/canister-studio/src/lib/format.test.ts +107 -0
- package/examples/canister-studio/src/lib/format.ts +139 -0
- package/examples/canister-studio/src/lib/hex.test.ts +43 -0
- package/examples/canister-studio/src/lib/hex.ts +33 -0
- package/examples/canister-studio/src/lib/ids.test.ts +70 -0
- package/examples/canister-studio/src/lib/ids.ts +93 -0
- package/examples/canister-studio/src/lib/reactor.test.ts +55 -0
- package/examples/canister-studio/src/lib/reactor.ts +55 -0
- package/examples/canister-studio/src/lib/registry.test.ts +74 -0
- package/examples/canister-studio/src/lib/registry.ts +238 -0
- package/examples/canister-studio/src/lib/schema.ts +148 -0
- package/examples/canister-studio/src/routeTree.gen.ts +227 -0
- package/examples/canister-studio/src/router.test.ts +290 -0
- package/examples/canister-studio/src/router.tsx +127 -0
- package/examples/canister-studio/src/routes/__root.tsx +58 -0
- package/examples/canister-studio/src/routes/canister.$canisterId.index.tsx +172 -0
- package/examples/canister-studio/src/routes/canister.$canisterId.interface.tsx +98 -0
- package/examples/canister-studio/src/routes/canister.$canisterId.tsx +91 -0
- package/examples/canister-studio/src/routes/history.tsx +55 -0
- package/examples/canister-studio/src/routes/index.tsx +169 -0
- package/examples/canister-studio/src/routes/playground.tsx +286 -0
- package/examples/canister-studio/src/routes/safety.tsx +57 -0
- package/examples/canister-studio/src/routes/wallet.tsx +213 -0
- package/examples/canister-studio/src/safety/LabTransferForm.tsx +84 -0
- package/examples/canister-studio/src/safety/RequestLog.tsx +79 -0
- package/examples/canister-studio/src/safety/SafetyLab.test.tsx +89 -0
- package/examples/canister-studio/src/safety/SafetyLab.tsx +140 -0
- package/examples/canister-studio/src/safety/ScenarioCard.tsx +262 -0
- package/examples/canister-studio/src/safety/lab-run.ts +283 -0
- package/examples/canister-studio/src/safety/mock-ledger.test.ts +99 -0
- package/examples/canister-studio/src/safety/mock-ledger.ts +263 -0
- package/examples/canister-studio/src/safety/scenarios.test.ts +157 -0
- package/examples/canister-studio/src/safety/scenarios.ts +924 -0
- package/examples/canister-studio/src/styles.css +3453 -0
- package/examples/canister-studio/src/styles.test.ts +46 -0
- package/examples/canister-studio/src/values/CallPreview.test.tsx +86 -0
- package/examples/canister-studio/src/values/CallPreview.tsx +148 -0
- package/examples/canister-studio/src/values/ErrValueView.tsx +66 -0
- package/examples/canister-studio/src/values/Inspector.tsx +133 -0
- package/examples/canister-studio/src/values/ReplyView.test.tsx +341 -0
- package/examples/canister-studio/src/values/ReplyView.tsx +164 -0
- package/examples/canister-studio/src/values/candid-text.test.ts +516 -0
- package/examples/canister-studio/src/values/candid-text.ts +479 -0
- package/examples/canister-studio/src/values/hints.ts +58 -0
- package/examples/canister-studio/src/values/json.test.ts +66 -0
- package/examples/canister-studio/src/values/json.ts +31 -0
- package/examples/canister-studio/src/values/leaves.tsx +309 -0
- package/examples/canister-studio/src/values/schema.test.ts +123 -0
- package/examples/canister-studio/src/values/schema.ts +78 -0
- package/examples/canister-studio/src/values/test-router.tsx +21 -0
- package/examples/canister-studio/src/values/tree.tsx +670 -0
- package/examples/canister-studio/src/values/use-sha256.ts +29 -0
- package/examples/canister-studio/tsconfig.json +20 -0
- package/examples/canister-studio/vite.config.ts +50 -0
- package/examples/canister-studio/vitest.config.ts +20 -0
- package/examples/icrc-ledger/.stackblitzrc +4 -0
- package/examples/icrc-ledger/README.md +100 -0
- package/examples/icrc-ledger/ckbtc_minter.did +12 -0
- package/examples/icrc-ledger/icp_ledger.did +147 -0
- package/examples/icrc-ledger/icrc1.did +88 -0
- package/examples/icrc-ledger/index.html +13 -0
- package/examples/icrc-ledger/package.json +40 -0
- package/examples/icrc-ledger/scripts/smoke.ts +151 -0
- package/examples/icrc-ledger/src/App.tsx +71 -0
- package/examples/icrc-ledger/src/ErrorPanel.tsx +105 -0
- package/examples/icrc-ledger/src/MainnetTab.tsx +450 -0
- package/examples/icrc-ledger/src/SandboxTab.test.tsx +379 -0
- package/examples/icrc-ledger/src/SandboxTab.tsx +761 -0
- package/examples/icrc-ledger/src/blocks.test.ts +247 -0
- package/examples/icrc-ledger/src/blocks.ts +140 -0
- package/examples/icrc-ledger/src/canisters/ckbtc_minter.envelope.json +96 -0
- package/examples/icrc-ledger/src/canisters/ckbtc_minter.ts +16 -0
- package/examples/icrc-ledger/src/canisters/icp_ledger.envelope.json +690 -0
- package/examples/icrc-ledger/src/canisters/icp_ledger.ts +223 -0
- package/examples/icrc-ledger/src/canisters/icrc1.envelope.json +840 -0
- package/examples/icrc-ledger/src/canisters/icrc1.ts +78 -0
- package/examples/icrc-ledger/src/format.test.ts +41 -0
- package/examples/icrc-ledger/src/format.ts +68 -0
- package/examples/icrc-ledger/src/ledger.ts +15 -0
- package/examples/icrc-ledger/src/main.tsx +10 -0
- package/examples/icrc-ledger/src/minter.ts +16 -0
- package/examples/icrc-ledger/src/sandbox.test.ts +464 -0
- package/examples/icrc-ledger/src/sandbox.ts +281 -0
- package/examples/icrc-ledger/src/styles.css +391 -0
- package/examples/icrc-ledger/src/transfer-form.test.ts +62 -0
- package/examples/icrc-ledger/src/transfer-form.ts +97 -0
- package/examples/icrc-ledger/tsconfig.json +19 -0
- package/examples/icrc-ledger/vite.config.ts +44 -0
- package/examples/icrc-ledger/vitest.config.ts +12 -0
- package/examples/next-ssr/.stackblitzrc +4 -0
- package/examples/next-ssr/README.md +112 -0
- package/examples/next-ssr/icrc1.did +88 -0
- package/examples/next-ssr/next.config.ts +10 -0
- package/examples/next-ssr/package.json +41 -0
- package/examples/next-ssr/scripts/smoke.ts +169 -0
- package/examples/next-ssr/src/app/account/CertifiedBalances.tsx +58 -0
- package/examples/next-ssr/src/app/account/account.test.tsx +165 -0
- package/examples/next-ssr/src/app/account/page.tsx +82 -0
- package/examples/next-ssr/src/app/api/balance/[ledger]/[principal]/route.ts +10 -0
- package/examples/next-ssr/src/app/globals.css +278 -0
- package/examples/next-ssr/src/app/layout.tsx +34 -0
- package/examples/next-ssr/src/app/page.tsx +46 -0
- package/examples/next-ssr/src/app/providers.browser.test.tsx +127 -0
- package/examples/next-ssr/src/app/providers.test.tsx +73 -0
- package/examples/next-ssr/src/app/providers.tsx +60 -0
- package/examples/next-ssr/src/canisters/icrc1.envelope.json +840 -0
- package/examples/next-ssr/src/canisters/icrc1.ts +78 -0
- package/examples/next-ssr/src/components/BalanceTable.tsx +43 -0
- package/examples/next-ssr/src/components/LedgerCard.test.tsx +96 -0
- package/examples/next-ssr/src/components/LedgerCard.tsx +230 -0
- package/examples/next-ssr/src/components/LedgerError.tsx +32 -0
- package/examples/next-ssr/src/components/LedgerSections.tsx +22 -0
- package/examples/next-ssr/src/components/MyBalances.test.tsx +253 -0
- package/examples/next-ssr/src/components/MyBalances.tsx +129 -0
- package/examples/next-ssr/src/components/SessionBadge.tsx +18 -0
- package/examples/next-ssr/src/components/hydration.test.tsx +259 -0
- package/examples/next-ssr/src/components/server-data.ts +10 -0
- package/examples/next-ssr/src/format.ts +46 -0
- package/examples/next-ssr/src/ledgers.ts +38 -0
- package/examples/next-ssr/src/server/balance-route.test.ts +151 -0
- package/examples/next-ssr/src/server/balance-route.ts +157 -0
- package/examples/next-ssr/src/server/error-summary.test.ts +34 -0
- package/examples/next-ssr/src/server/error-summary.ts +37 -0
- package/examples/next-ssr/src/server/parse-owner.test.ts +39 -0
- package/examples/next-ssr/src/server/parse-owner.ts +34 -0
- package/examples/next-ssr/src/server/prefetch-ledgers.test.ts +137 -0
- package/examples/next-ssr/src/server/prefetch-ledgers.ts +70 -0
- package/examples/next-ssr/src/server/read-balances.test.ts +103 -0
- package/examples/next-ssr/src/server/read-balances.ts +78 -0
- package/examples/next-ssr/src/server/request-client.rsc.test.tsx +110 -0
- package/examples/next-ssr/src/server/request-client.ts +24 -0
- package/examples/next-ssr/src/testing/mock-ledgers.ts +115 -0
- package/examples/next-ssr/src/testing/react-server-dom-turbopack.d.ts +15 -0
- package/examples/next-ssr/src/testing/setup.ts +10 -0
- package/examples/next-ssr/tsconfig.json +31 -0
- package/examples/next-ssr/vitest.config.ts +59 -0
- package/examples/node-agent-tool/.stackblitzrc +4 -0
- package/examples/node-agent-tool/README.md +165 -0
- package/examples/node-agent-tool/icp.yaml +11 -0
- package/examples/node-agent-tool/icrc1.did +88 -0
- package/examples/node-agent-tool/package.json +32 -0
- package/examples/node-agent-tool/src/canisters/icrc1.envelope.json +840 -0
- package/examples/node-agent-tool/src/canisters/icrc1.ts +78 -0
- package/examples/node-agent-tool/src/cli.test.ts +64 -0
- package/examples/node-agent-tool/src/cli.ts +23 -0
- package/examples/node-agent-tool/src/command-line.ts +159 -0
- package/examples/node-agent-tool/src/commands/balance.ts +73 -0
- package/examples/node-agent-tool/src/commands/demo.test.ts +51 -0
- package/examples/node-agent-tool/src/commands/demo.ts +359 -0
- package/examples/node-agent-tool/src/commands/info.ts +69 -0
- package/examples/node-agent-tool/src/commands/reads.test.ts +133 -0
- package/examples/node-agent-tool/src/commands/resend.test.ts +363 -0
- package/examples/node-agent-tool/src/commands/transfer.test.ts +313 -0
- package/examples/node-agent-tool/src/commands/transfer.ts +415 -0
- package/examples/node-agent-tool/src/commands/watch.test.ts +159 -0
- package/examples/node-agent-tool/src/commands/watch.ts +135 -0
- package/examples/node-agent-tool/src/commands/whoami.ts +36 -0
- package/examples/node-agent-tool/src/context.ts +20 -0
- package/examples/node-agent-tool/src/dispatch.ts +70 -0
- package/examples/node-agent-tool/src/failure.test.ts +59 -0
- package/examples/node-agent-tool/src/failure.ts +176 -0
- package/examples/node-agent-tool/src/identity.test.ts +242 -0
- package/examples/node-agent-tool/src/identity.ts +228 -0
- package/examples/node-agent-tool/src/input.ts +110 -0
- package/examples/node-agent-tool/src/ledgers.test.ts +74 -0
- package/examples/node-agent-tool/src/ledgers.ts +62 -0
- package/examples/node-agent-tool/src/mock-ledger.ts +211 -0
- package/examples/node-agent-tool/src/network.test.ts +128 -0
- package/examples/node-agent-tool/src/network.ts +101 -0
- package/examples/node-agent-tool/src/output.test.ts +208 -0
- package/examples/node-agent-tool/src/output.ts +163 -0
- package/examples/node-agent-tool/src/run.ts +73 -0
- package/examples/node-agent-tool/src/test-kit.ts +167 -0
- package/examples/node-agent-tool/tsconfig.json +18 -0
- package/examples/node-agent-tool/vitest.config.ts +8 -0
- package/examples/vite-wallet/.stackblitzrc +4 -0
- package/examples/vite-wallet/Cargo.lock +646 -0
- package/examples/vite-wallet/Cargo.toml +5 -0
- package/examples/vite-wallet/README.md +107 -0
- package/examples/vite-wallet/backend/Cargo.toml +12 -0
- package/examples/vite-wallet/backend/backend.did +37 -0
- package/examples/vite-wallet/backend/src/lib.rs +133 -0
- package/examples/vite-wallet/icp.yaml +44 -0
- package/examples/vite-wallet/index.html +13 -0
- package/examples/vite-wallet/ledger.did +54 -0
- package/examples/vite-wallet/package.json +46 -0
- package/examples/vite-wallet/public/.ic-assets.json5 +26 -0
- package/examples/vite-wallet/scripts/faucet-args.test.ts +30 -0
- package/examples/vite-wallet/scripts/faucet-args.ts +41 -0
- package/examples/vite-wallet/scripts/faucet.ts +41 -0
- package/examples/vite-wallet/src/AddressBook.test.tsx +191 -0
- package/examples/vite-wallet/src/AddressBook.tsx +147 -0
- package/examples/vite-wallet/src/App.tsx +53 -0
- package/examples/vite-wallet/src/Balance.test.tsx +57 -0
- package/examples/vite-wallet/src/Balance.tsx +58 -0
- package/examples/vite-wallet/src/EnvironmentPanel.test.tsx +83 -0
- package/examples/vite-wallet/src/EnvironmentPanel.tsx +103 -0
- package/examples/vite-wallet/src/ErrorNote.tsx +70 -0
- package/examples/vite-wallet/src/Profile.test.tsx +145 -0
- package/examples/vite-wallet/src/Profile.tsx +92 -0
- package/examples/vite-wallet/src/SendIcp.test.tsx +231 -0
- package/examples/vite-wallet/src/SendIcp.tsx +244 -0
- package/examples/vite-wallet/src/SendToContact.test.tsx +113 -0
- package/examples/vite-wallet/src/SendToContact.tsx +102 -0
- package/examples/vite-wallet/src/SignIn.test.tsx +106 -0
- package/examples/vite-wallet/src/SignIn.tsx +120 -0
- package/examples/vite-wallet/src/Wallet.test.tsx +89 -0
- package/examples/vite-wallet/src/account-switch.test.tsx +108 -0
- package/examples/vite-wallet/src/auth/dev-accounts.test.ts +113 -0
- package/examples/vite-wallet/src/auth/dev-accounts.ts +216 -0
- package/examples/vite-wallet/src/auth/local-page.test.ts +28 -0
- package/examples/vite-wallet/src/auth/local-page.ts +20 -0
- package/examples/vite-wallet/src/auth/wallet-auth.test.ts +159 -0
- package/examples/vite-wallet/src/auth/wallet-auth.ts +131 -0
- package/examples/vite-wallet/src/canisters/backend.envelope.json +275 -0
- package/examples/vite-wallet/src/canisters/backend.ts +58 -0
- package/examples/vite-wallet/src/canisters/ledger.envelope.json +451 -0
- package/examples/vite-wallet/src/canisters/ledger.ts +56 -0
- package/examples/vite-wallet/src/client.ts +45 -0
- package/examples/vite-wallet/src/main.tsx +18 -0
- package/examples/vite-wallet/src/optimistic-contacts.ts +113 -0
- package/examples/vite-wallet/src/refusal.ts +17 -0
- package/examples/vite-wallet/src/styles.css +282 -0
- package/examples/vite-wallet/src/test/test-wallet.tsx +322 -0
- package/examples/vite-wallet/src/token.ts +50 -0
- package/examples/vite-wallet/src/transfer.test.ts +178 -0
- package/examples/vite-wallet/src/transfer.ts +110 -0
- package/examples/vite-wallet/src/use-canisters.ts +49 -0
- package/examples/vite-wallet/tsconfig.json +19 -0
- package/examples/vite-wallet/vite.config.ts +47 -0
- package/examples/vite-wallet/vitest.config.ts +17 -0
- package/package.json +9 -4
- package/packages/core/.size-limit.js +47 -0
- package/packages/core/LICENSE +21 -0
- package/packages/core/README.md +84 -0
- package/packages/core/llms.txt +610 -0
- package/packages/core/package.json +89 -0
- package/packages/core/src/call.ts +961 -0
- package/packages/core/src/canister.ts +259 -0
- package/packages/core/src/client.ts +2061 -0
- package/packages/core/src/errors.ts +1079 -0
- package/packages/core/src/hydration.ts +239 -0
- package/packages/core/src/index.ts +23 -0
- package/packages/core/src/keys.ts +108 -0
- package/packages/core/src/management.ts +138 -0
- package/packages/core/src/network.ts +1020 -0
- package/packages/core/src/options.ts +1000 -0
- package/packages/core/src/runtime.ts +50 -0
- package/packages/core/src/testing/fake-replica.ts +1166 -0
- package/packages/core/src/testing/index.ts +16 -0
- package/packages/core/src/testing/test-auth.ts +340 -0
- package/packages/core/src/testing/test-client.ts +559 -0
- package/packages/core/src/types.ts +541 -0
- package/packages/core/src/units.ts +299 -0
- package/packages/core/src/version.ts +4 -0
- package/packages/core/tests/canister-helpers.ts +187 -0
- package/packages/core/tests/canister-id.test.ts +158 -0
- package/packages/core/tests/canister-unwrap.test.ts +94 -0
- package/packages/core/tests/canister.test-d.ts +406 -0
- package/packages/core/tests/canister.test.ts +1921 -0
- package/packages/core/tests/client-account-change.test.ts +131 -0
- package/packages/core/tests/client-agents.test.ts +402 -0
- package/packages/core/tests/client-auth-network.test.ts +719 -0
- package/packages/core/tests/client-disposed.test.ts +403 -0
- package/packages/core/tests/client-helpers.ts +136 -0
- package/packages/core/tests/client-hydration.test.ts +166 -0
- package/packages/core/tests/client-pinned.test.ts +626 -0
- package/packages/core/tests/client-session.test.ts +348 -0
- package/packages/core/tests/client.test-d.ts +181 -0
- package/packages/core/tests/client.test.ts +1162 -0
- package/packages/core/tests/encode.test.ts +171 -0
- package/packages/core/tests/env-canister-id.test.ts +666 -0
- package/packages/core/tests/env-cookie-copies.test.ts +182 -0
- package/packages/core/tests/error-guard.test.ts +222 -0
- package/packages/core/tests/errors.test-d.ts +280 -0
- package/packages/core/tests/errors.test.ts +1359 -0
- package/packages/core/tests/fixtures/README.md +28 -0
- package/packages/core/tests/fixtures/accounts.did +21 -0
- package/packages/core/tests/fixtures/accounts.envelope.json +367 -0
- package/packages/core/tests/fixtures/accounts.ts +26 -0
- package/packages/core/tests/fixtures/archive.did +31 -0
- package/packages/core/tests/fixtures/archive.envelope.json +259 -0
- package/packages/core/tests/fixtures/archive.ts +55 -0
- package/packages/core/tests/fixtures/icrc1.did +88 -0
- package/packages/core/tests/fixtures/icrc1.envelope.json +840 -0
- package/packages/core/tests/fixtures/icrc1.ts +78 -0
- package/packages/core/tests/fixtures/management.did +724 -0
- package/packages/core/tests/fixtures/management.envelope.json +4455 -0
- package/packages/core/tests/fixtures/management.ts +486 -0
- package/packages/core/tests/fixtures/shapes.did +33 -0
- package/packages/core/tests/fixtures/shapes.envelope.json +440 -0
- package/packages/core/tests/fixtures/shapes.ts +56 -0
- package/packages/core/tests/fixtures/skippable.did +15 -0
- package/packages/core/tests/fixtures/skippable.envelope.json +182 -0
- package/packages/core/tests/fixtures/skippable.ts +33 -0
- package/packages/core/tests/func.test.ts +154 -0
- package/packages/core/tests/identity-attributes.test.ts +214 -0
- package/packages/core/tests/keys.test.ts +289 -0
- package/packages/core/tests/management.test.ts +206 -0
- package/packages/core/tests/network-helpers.ts +121 -0
- package/packages/core/tests/network.test-d.ts +78 -0
- package/packages/core/tests/network.test.ts +1051 -0
- package/packages/core/tests/options.test.ts +1594 -0
- package/packages/core/tests/redirect-sign-in.test.ts +393 -0
- package/packages/core/tests/resend.test-d.ts +81 -0
- package/packages/core/tests/resend.test.ts +723 -0
- package/packages/core/tests/skip.test-d.ts +125 -0
- package/packages/core/tests/testing-entry.test.ts +73 -0
- package/packages/core/tests/testing-fake-replica.test.ts +1242 -0
- package/packages/core/tests/testing-test-auth.test-d.ts +84 -0
- package/packages/core/tests/testing-test-auth.test.ts +483 -0
- package/packages/core/tests/testing-test-client.test-d.ts +184 -0
- package/packages/core/tests/testing-test-client.test.ts +1272 -0
- package/packages/core/tests/time-realm.test.ts +63 -0
- package/packages/core/tests/time.test.ts +238 -0
- package/packages/core/tests/traps/README.md +39 -0
- package/packages/core/tests/traps/anonymous-write.test.ts +145 -0
- package/packages/core/tests/traps/classification-table.test.ts +256 -0
- package/packages/core/tests/traps/err-arm.test.ts +144 -0
- package/packages/core/tests/traps/fetch-spy.ts +39 -0
- package/packages/core/tests/traps/invalidation-after-write.test.ts +178 -0
- package/packages/core/tests/traps/ledger.ts +248 -0
- package/packages/core/tests/traps/root-key-off-local.test.ts +130 -0
- package/packages/core/tests/traps/stale-data-across-users.test.ts +173 -0
- package/packages/core/tests/traps/unresolved-name.test.ts +155 -0
- package/packages/core/tests/traps/update-as-query.test.ts +244 -0
- package/packages/core/tests/traps/update-not-resent.test.ts +126 -0
- package/packages/core/tests/traps/update-resent-when-never-delivered.test.ts +159 -0
- package/packages/core/tests/traps.test-d.ts +476 -0
- package/packages/core/tests/units.test-d.ts +118 -0
- package/packages/core/tests/units.test.ts +634 -0
- package/packages/core/tsconfig.json +19 -0
- package/packages/core/tsconfig.typecheck.json +16 -0
- package/packages/core/vitest.config.ts +13 -0
- package/packages/mcp/LICENSE +21 -0
- package/packages/mcp/README.md +189 -0
- package/packages/mcp/package.json +62 -0
- package/packages/mcp/src/candid/candid-text.test.ts +455 -0
- package/packages/mcp/src/candid/candid-text.ts +501 -0
- package/packages/mcp/src/candid/contract.test.ts +398 -0
- package/packages/mcp/src/candid/formats.ts +155 -0
- package/packages/mcp/src/candid/hex.ts +29 -0
- package/packages/mcp/src/candid/json-schema.test.ts +758 -0
- package/packages/mcp/src/candid/json-schema.ts +454 -0
- package/packages/mcp/src/candid/json.test.ts +1207 -0
- package/packages/mcp/src/candid/json.ts +1053 -0
- package/packages/mcp/src/candid/schema-walk.ts +189 -0
- package/packages/mcp/src/candid/type-text.test.ts +179 -0
- package/packages/mcp/src/candid/type-text.ts +238 -0
- package/packages/mcp/src/cli.ts +134 -0
- package/packages/mcp/src/identity.ts +188 -0
- package/packages/mcp/src/interface/cache.test.ts +48 -0
- package/packages/mcp/src/interface/cache.ts +68 -0
- package/packages/mcp/src/interface/cli-cache.test.ts +150 -0
- package/packages/mcp/src/interface/cli-cache.ts +122 -0
- package/packages/mcp/src/interface/compile-worker.test.ts +246 -0
- package/packages/mcp/src/interface/compile-worker.ts +290 -0
- package/packages/mcp/src/interface/compile.test.ts +155 -0
- package/packages/mcp/src/interface/compile.ts +281 -0
- package/packages/mcp/src/interface/docs.test.ts +105 -0
- package/packages/mcp/src/interface/docs.ts +86 -0
- package/packages/mcp/src/interface/fixtures/icrc1.did +88 -0
- package/packages/mcp/src/interface/hex.ts +8 -0
- package/packages/mcp/src/interface/ids.test.ts +71 -0
- package/packages/mcp/src/interface/ids.ts +87 -0
- package/packages/mcp/src/interface/loader.test.ts +804 -0
- package/packages/mcp/src/interface/loader.ts +519 -0
- package/packages/mcp/src/interface/read-candid.test.ts +300 -0
- package/packages/mcp/src/interface/read-candid.ts +259 -0
- package/packages/mcp/src/interface/types.ts +113 -0
- package/packages/mcp/src/network.ts +122 -0
- package/packages/mcp/src/options.ts +149 -0
- package/packages/mcp/src/server/approval.ts +159 -0
- package/packages/mcp/src/server/naming.ts +43 -0
- package/packages/mcp/src/server/results.ts +102 -0
- package/packages/mcp/src/server/server.ts +797 -0
- package/packages/mcp/src/server/tools.ts +116 -0
- package/packages/mcp/src/usage.ts +40 -0
- package/packages/mcp/tests/approval.test.ts +60 -0
- package/packages/mcp/tests/identity.test.ts +234 -0
- package/packages/mcp/tests/naming.test.ts +62 -0
- package/packages/mcp/tests/options.test.ts +140 -0
- package/packages/mcp/tests/server.test.ts +997 -0
- package/packages/mcp/tsconfig.json +19 -0
- package/packages/mcp/tsconfig.typecheck.json +10 -0
- package/packages/mcp/vitest.config.ts +10 -0
- package/packages/react/LICENSE +21 -0
- package/packages/react/README.md +328 -0
- package/packages/react/llms.txt +95 -0
- package/packages/react/package.json +95 -0
- package/packages/react/src/index.tsx +678 -0
- package/packages/react/tests/abandoned-render.test.tsx +816 -0
- package/packages/react/tests/auth.test.tsx +315 -0
- package/packages/react/tests/caller-keys.test.tsx +2334 -0
- package/packages/react/tests/client-time.test.ts +40 -0
- package/packages/react/tests/entry.test.ts +52 -0
- package/packages/react/tests/helpers.tsx +291 -0
- package/packages/react/tests/hydration.test.tsx +262 -0
- package/packages/react/tests/ownership.test.tsx +260 -0
- package/packages/react/tests/provider.server.test.tsx +169 -0
- package/packages/react/tests/query-client.test.tsx +107 -0
- package/packages/react/tests/strict-mode.test.tsx +288 -0
- package/packages/react/tests/suspense.test-d.tsx +357 -0
- package/packages/react/tests/two-query-cores.test.tsx +135 -0
- package/packages/react/tests/types.test-d.tsx +63 -0
- package/packages/react/tsconfig.json +23 -0
- package/packages/react/tsconfig.test.json +8 -0
- package/packages/react/tsconfig.typecheck.json +16 -0
- package/packages/react/vitest.config.ts +39 -0
- package/packages/vite-plugin/LICENSE +21 -0
- package/packages/vite-plugin/README.md +532 -0
- package/packages/vite-plugin/package.json +76 -0
- package/packages/vite-plugin/src/describe-value.test.ts +43 -0
- package/packages/vite-plugin/src/describe-value.ts +167 -0
- package/packages/vite-plugin/src/dev-environment.test.ts +747 -0
- package/packages/vite-plugin/src/dev-environment.ts +510 -0
- package/packages/vite-plugin/src/env.ts +431 -0
- package/packages/vite-plugin/src/fetch.ts +348 -0
- package/packages/vite-plugin/src/file-identity.ts +106 -0
- package/packages/vite-plugin/src/generate.ts +691 -0
- package/packages/vite-plugin/src/index.test.ts +1243 -0
- package/packages/vite-plugin/src/index.ts +2565 -0
- package/packages/vite-plugin/src/interface.ts +339 -0
- package/packages/vite-plugin/src/local-host.ts +34 -0
- package/packages/vite-plugin/src/options.ts +641 -0
- package/packages/vite-plugin/src/principal-text.test.ts +87 -0
- package/packages/vite-plugin/src/principal-text.ts +98 -0
- package/packages/vite-plugin/tests/close.test.ts +141 -0
- package/packages/vite-plugin/tests/environment-mode.test.ts +136 -0
- package/packages/vite-plugin/tests/fake-ic.ts +214 -0
- package/packages/vite-plugin/tests/fetch-sdk.test.ts +27 -0
- package/packages/vite-plugin/tests/fetch.test.ts +1196 -0
- package/packages/vite-plugin/tests/fixtures/fake-cli/bin.js +90 -0
- package/packages/vite-plugin/tests/fixtures/fake-cli/package.json +8 -0
- package/packages/vite-plugin/tests/fixtures/icrc1.did +88 -0
- package/packages/vite-plugin/tests/generation.test.ts +1036 -0
- package/packages/vite-plugin/tests/icp-command.test.ts +194 -0
- package/packages/vite-plugin/tests/icp-timeout.test.ts +83 -0
- package/packages/vite-plugin/tests/no-process-exit.test.ts +83 -0
- package/packages/vite-plugin/tests/on-generated.test.ts +323 -0
- package/packages/vite-plugin/tests/output-collision.test.ts +329 -0
- package/packages/vite-plugin/tests/package.test.ts +62 -0
- package/packages/vite-plugin/tests/project.test.ts +2993 -0
- package/packages/vite-plugin/tests/support.ts +339 -0
- package/packages/vite-plugin/tests/timeout.test.ts +107 -0
- package/packages/vite-plugin/tsconfig.json +23 -0
- package/packages/vite-plugin/tsconfig.typecheck.json +16 -0
- package/packages/vite-plugin/vitest.config.ts +9 -0
- package/pnpm-workspace.yaml +23 -0
- package/scripts/ai-context-files.js +72 -0
- package/scripts/assemble-docs-site.mjs +485 -0
- package/scripts/assemble-docs-site.test.mjs +568 -0
- package/scripts/build-examples.js +58 -0
- package/scripts/check-ai-context.js +963 -0
- package/scripts/check-ai-context.test.mjs +811 -0
- package/scripts/check-exports.mjs +767 -0
- package/scripts/check-exports.test.mjs +1136 -0
- package/scripts/check-snippets/ambient.d.ts +428 -0
- package/scripts/check-snippets/app/generated/backend.did +7 -0
- package/scripts/check-snippets/app/generated/backend.ts +15 -0
- package/scripts/check-snippets/app/generated/icrc1.did +88 -0
- package/scripts/check-snippets/app/generated/icrc1.ts +78 -0
- package/scripts/check-snippets/app/globals.ts +26 -0
- package/scripts/check-snippets/docs/guides/App.tsx +21 -0
- package/scripts/check-snippets/docs/guides/app/providers.tsx +23 -0
- package/scripts/check-snippets/docs/guides/auth-client.ts +47 -0
- package/scripts/check-snippets/docs/guides/canisters/backend.ts +3 -0
- package/scripts/check-snippets/docs/guides/canisters/icrc1.ts +3 -0
- package/scripts/check-snippets/docs/guides/components/LedgerCard.tsx +26 -0
- package/scripts/check-snippets/docs/guides/components/shell.tsx +11 -0
- package/scripts/check-snippets/docs/guides/components/sign-in-page.tsx +29 -0
- package/scripts/check-snippets/docs/guides/components/slow-section.tsx +5 -0
- package/scripts/check-snippets/docs/guides/generated/accounts.ts +95 -0
- package/scripts/check-snippets/docs/guides/generated/ckbtc_minter.ts +40 -0
- package/scripts/check-snippets/docs/guides/generated/icp_ledger.ts +267 -0
- package/scripts/check-snippets/docs/guides/ledger.ts +10 -0
- package/scripts/check-snippets/docs/guides/server/request-client.ts +8 -0
- package/scripts/check-snippets.mjs +993 -0
- package/scripts/check-snippets.test.mjs +338 -0
- package/scripts/check-stack.mjs +501 -0
- package/scripts/check-stack.test.mjs +800 -0
- package/scripts/clean-node-modules.js +67 -0
- package/scripts/example-projects.js +58 -0
- package/scripts/export-budget.mjs +145 -0
- package/scripts/faults.json +6749 -0
- package/scripts/lib/fault-workspace.mjs +210 -0
- package/scripts/lib/io.mjs +29 -0
- package/scripts/lib/io.test.mjs +38 -0
- package/scripts/lib/markdown-fences.mjs +56 -0
- package/scripts/lib/markdown-fences.test.mjs +84 -0
- package/scripts/lib/package-exports.mjs +54 -0
- package/scripts/lib/package-exports.test.mjs +66 -0
- package/scripts/lib/release-pr-steps.mjs +47 -0
- package/scripts/release-tag.mjs +95 -0
- package/scripts/release-tag.test.mjs +278 -0
- package/scripts/release-workflow.test.mjs +98 -0
- package/scripts/release.js +167 -0
- package/scripts/release.test.mjs +347 -0
- package/scripts/removed-v3-names.js +125 -0
- package/scripts/size-app/.size-limit.js +90 -0
- package/scripts/size-app/core.js +3 -0
- package/scripts/size-app/package.json +24 -0
- package/scripts/size-app/react.js +4 -0
- package/scripts/size-app.test.mjs +73 -0
- package/scripts/sync-ai-context-versions.js +102 -0
- package/scripts/sync-example-versions.js +151 -0
- package/scripts/test-examples-published.mjs +1585 -0
- package/scripts/test-examples-published.test.mjs +1586 -0
- package/scripts/traps/any-method-vars-not-a-list.json +5 -0
- package/scripts/traps/canister-method-unknown.json +5 -0
- package/scripts/traps/client-says-who-calls-once.json +5 -0
- package/scripts/traps/client-says-who-calls.json +5 -0
- package/scripts/traps/err-only-on-canister-err.json +14 -0
- package/scripts/traps/format-units-refuses-missing-value.json +5 -0
- package/scripts/traps/format-units-refuses-number.json +5 -0
- package/scripts/traps/format-units-unknown-option.json +5 -0
- package/scripts/traps/guard-err-is-the-methods.json +5 -0
- package/scripts/traps/guard-false-keeps-every-kind.json +5 -0
- package/scripts/traps/internals-not-exported.json +9 -0
- package/scripts/traps/network-refuses-unknown-name.json +5 -0
- package/scripts/traps/never-err-has-no-type.json +5 -0
- package/scripts/traps/options-method-unknown.json +5 -0
- package/scripts/traps/options-vars-wrong-type.json +5 -0
- package/scripts/traps/parse-units-refuses-number.json +5 -0
- package/scripts/traps/parse-units-unknown-option.json +5 -0
- package/scripts/traps/principal-is-not-plain-text.json +14 -0
- package/scripts/traps/query-options-refuses-oneway.json +5 -0
- package/scripts/traps/query-options-refuses-update.json +5 -0
- package/scripts/traps/refuse-next-unknown-option.json +5 -0
- package/scripts/traps/resend-key-reads-the-methods-arg.json +5 -0
- package/scripts/traps/resend-states-its-dedupe-key.json +5 -0
- package/scripts/traps/suspense-refuses-argument-skip.json +5 -0
- package/scripts/traps/suspense-refuses-skippable-read.json +5 -0
- package/scripts/traps/suspense-refuses-vars-skip-fits.json +5 -0
- package/scripts/traps/test-handler-method-unknown.json +5 -0
- package/scripts/traps/test-handler-nat-is-bigint.json +5 -0
- package/scripts/traps/test-handler-takes-the-domain-value.json +5 -0
- package/scripts/traps/vars-of-two-arguments-are-the-tuple.json +5 -0
- package/scripts/typecheck-examples.js +122 -0
- package/scripts/verify-faults.mjs +375 -0
- package/scripts/verify-faults.test.mjs +402 -0
- package/scripts/verify-package-artifacts.mjs +590 -0
- package/scripts/verify-peer-floors.js +399 -0
- package/scripts/verify-peer-floors.test.mjs +422 -0
- package/scripts/verify-test-fails-rules.mjs +104 -0
- package/scripts/verify-test-fails-rules.test.mjs +215 -0
- package/scripts/verify-test-fails.js +371 -0
- package/scripts/verify-traps.mjs +505 -0
- package/scripts/verify-traps.test.mjs +521 -0
- package/scripts/verify-typescript-floor.js +239 -0
- package/skill-packages/ic-reactor/SKILL.md +103 -0
- package/stack.json +8 -0
- package/tsconfig.base.json +23 -0
- package/tsconfig.json +8 -0
|
@@ -0,0 +1,2061 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `createClient`: the one object an app builds, per browser tab or per server
|
|
3
|
+
* request. It decides who calls (a fixed identity, or the signed-in user of an
|
|
4
|
+
* auth it owns), keeps one immutable agent per principal, and owns the
|
|
5
|
+
* `QueryClient` every read is cached in.
|
|
6
|
+
*
|
|
7
|
+
* Public: {@link createClient}, {@link Client}, {@link ClientOptions},
|
|
8
|
+
* {@link AuthLike} and {@link AuthState}. {@link internalsOf} and the types it
|
|
9
|
+
* returns are for the canister builders of this package (`canister.ts`,
|
|
10
|
+
* `options.ts`, `call.ts`) and are not exported from the package entry.
|
|
11
|
+
*
|
|
12
|
+
* Who calls is decided in one place, {@link ClientInternals.agentFor}, and by
|
|
13
|
+
* one rule: a call goes out signed by the principal it was made for, or not at
|
|
14
|
+
* all. An agent is never re-pointed at another identity (`replaceIdentity` is
|
|
15
|
+
* never called); a sign-in, a sign-out or a switch of account makes the next
|
|
16
|
+
* call use another principal's agent, and a call made for the principal that
|
|
17
|
+
* was current before is cancelled instead of being sent as the new one.
|
|
18
|
+
*
|
|
19
|
+
* @module
|
|
20
|
+
*/
|
|
21
|
+
import type { FuncValue, Principal, Schema } from "@meantjs/schema"
|
|
22
|
+
import { DEFAULT_MAX_DEPTH, DEFAULT_MAX_ELEMENTS } from "@meantjs/schema/codec"
|
|
23
|
+
import {
|
|
24
|
+
AnonymousIdentity,
|
|
25
|
+
HttpAgent,
|
|
26
|
+
type Identity,
|
|
27
|
+
} from "@icp-sdk/core/agent"
|
|
28
|
+
import { Principal as SdkPrincipal } from "@icp-sdk/core/principal"
|
|
29
|
+
import { MutationCache, QueryClient, type QueryKey } from "@tanstack/query-core"
|
|
30
|
+
import {
|
|
31
|
+
createReactorError,
|
|
32
|
+
fromCanister,
|
|
33
|
+
notSigned,
|
|
34
|
+
retryDefault,
|
|
35
|
+
} from "./errors.js"
|
|
36
|
+
import { deserializeData, serializeData } from "./hydration.js"
|
|
37
|
+
import { KEY_ROOT } from "./keys.js"
|
|
38
|
+
import {
|
|
39
|
+
agentOptionsFor,
|
|
40
|
+
agentUrlOf,
|
|
41
|
+
authNetworkFor,
|
|
42
|
+
checkKeys,
|
|
43
|
+
resolveCanisterId,
|
|
44
|
+
resolveNetwork,
|
|
45
|
+
type Network,
|
|
46
|
+
type ResolvedNetwork,
|
|
47
|
+
} from "./network.js"
|
|
48
|
+
import { keptUnknown, type CallRequest } from "./call.js"
|
|
49
|
+
import { readTarget, recordOf } from "./canister.js"
|
|
50
|
+
import { createBuilders } from "./options.js"
|
|
51
|
+
import { isDevelopment, isServer } from "./runtime.js"
|
|
52
|
+
import type {
|
|
53
|
+
Canister,
|
|
54
|
+
CanisterMutationOptions,
|
|
55
|
+
CanisterQueryOptions,
|
|
56
|
+
CanisterTarget,
|
|
57
|
+
DataOf,
|
|
58
|
+
ErrorOf,
|
|
59
|
+
MutationOptionsOptions,
|
|
60
|
+
OptInMethod,
|
|
61
|
+
OptInQueryArgs,
|
|
62
|
+
QueryArgs,
|
|
63
|
+
ReadMethod,
|
|
64
|
+
Resend,
|
|
65
|
+
ResendOptions,
|
|
66
|
+
VarsOf,
|
|
67
|
+
} from "./types.js"
|
|
68
|
+
|
|
69
|
+
/** The anonymous principal, `2vxsx-fae`: the caller of every call nobody signed. */
|
|
70
|
+
const ANONYMOUS = SdkPrincipal.anonymous().toText()
|
|
71
|
+
|
|
72
|
+
// ---------------------------------------------------------------------------
|
|
73
|
+
// Public types
|
|
74
|
+
// ---------------------------------------------------------------------------
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* A sign-in the client can own: what `@icp-sdk/auth` 10's `AuthClient` and the
|
|
78
|
+
* `auth` of `createTestClient()` from `@ic-reactor/core/testing` both are, with
|
|
79
|
+
* no adapter.
|
|
80
|
+
*
|
|
81
|
+
* The client reads `getStatus()` and `getPrincipal()` synchronously to decide
|
|
82
|
+
* who calls, asks `getIdentity()` for the identity to sign with only when it
|
|
83
|
+
* builds or checks an agent for a signed-in principal, and re-reads both after
|
|
84
|
+
* every `subscribe()` notification. `signIn()` and `signOut()` are forwarded
|
|
85
|
+
* from {@link Client.signIn} and {@link Client.signOut}, and `dispose()`, when
|
|
86
|
+
* present, is called from {@link Client.dispose}: the client owns the auth its
|
|
87
|
+
* factory built.
|
|
88
|
+
*
|
|
89
|
+
* Another sign-in source that hands over an `Identity` (a delegation, such
|
|
90
|
+
* as a hand-rolled session gives, or a local key) fits by implementing these
|
|
91
|
+
* six methods, and `dispose()` if it holds anything. A wallet that signs each call itself (ICRC-25 and
|
|
92
|
+
* ICRC-49: OISY, Plug, NFID accounts) gives no `Identity` and does not fit:
|
|
93
|
+
* the Auth guide shows writes through such a wallet beside the client. A
|
|
94
|
+
* source's `getIdentity()` should return the same object until the identity
|
|
95
|
+
* really changes: the client builds a new agent for every new identity object
|
|
96
|
+
* it is handed, and reads a new object for the same principal as a renewal.
|
|
97
|
+
*/
|
|
98
|
+
export interface AuthLike {
|
|
99
|
+
/** Who calls are accepted as, or `undefined` when nobody can act. */
|
|
100
|
+
getPrincipal(): { toText(): string } | undefined
|
|
101
|
+
/**
|
|
102
|
+
* Who is signed in. Only `"signed-in"` makes the client call as
|
|
103
|
+
* {@link AuthLike.getPrincipal}; every other state calls anonymously. The
|
|
104
|
+
* four named states are `@icp-sdk/auth` 10's; any other string, such as a
|
|
105
|
+
* state a later `@icp-sdk/auth` adds, is accepted and reads as
|
|
106
|
+
* `"anonymous"` in {@link AuthState}, so an upgrade of the auth library
|
|
107
|
+
* does not stop an app compiling.
|
|
108
|
+
*/
|
|
109
|
+
getStatus(): {
|
|
110
|
+
readonly state:
|
|
111
|
+
| "signed-in"
|
|
112
|
+
| "signed-in-elsewhere"
|
|
113
|
+
| "expired"
|
|
114
|
+
| "signed-out"
|
|
115
|
+
| (string & {})
|
|
116
|
+
/**
|
|
117
|
+
* When the session ends, in milliseconds since the epoch (`Date.now()`'s
|
|
118
|
+
* scale), as `AuthClient` 10 reports it. While the status is
|
|
119
|
+
* `"signed-in"` or `"signed-in-elsewhere"`, the client sets a timer for it
|
|
120
|
+
* and tells its listeners when the session ends: an auth whose status
|
|
121
|
+
* turns `"expired"` by the clock, as `AuthClient`'s does, need not notify
|
|
122
|
+
* for it. It is {@link AuthState.expiresAt} while signed in.
|
|
123
|
+
*/
|
|
124
|
+
readonly expiresAtMs?: number
|
|
125
|
+
}
|
|
126
|
+
/** The identity to sign calls with while signed in. */
|
|
127
|
+
getIdentity(): Promise<Identity>
|
|
128
|
+
/** Calls `listener` after each change of who is signed in. Returns a function that stops listening. */
|
|
129
|
+
subscribe(listener: () => void): () => void
|
|
130
|
+
signIn(options?: unknown): Promise<unknown>
|
|
131
|
+
signOut(options?: unknown): Promise<unknown>
|
|
132
|
+
/** Releases what the auth holds. Called once, from {@link Client.dispose}. */
|
|
133
|
+
dispose?(): void
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Who a client calls as, and why. {@link Client.authState} returns it.
|
|
138
|
+
*
|
|
139
|
+
* `principal` is always the principal calls go out as, the same text
|
|
140
|
+
* {@link Client.caller} returns: the signed-in user's when `status` is
|
|
141
|
+
* `"signed-in"`, and the anonymous principal (`2vxsx-fae`) in every other
|
|
142
|
+
* state. An expired session, or one signed in on a sibling origin that this
|
|
143
|
+
* origin holds no credential for, cannot sign a call, so it does not name its
|
|
144
|
+
* principal here either: a check such as `principal !== anonymous` means what
|
|
145
|
+
* it looks like it means. To say whose session ended, read the auth's own
|
|
146
|
+
* `getStatus()`, which keeps the principal in those states.
|
|
147
|
+
*
|
|
148
|
+
* A client built with an `identity` reports `"signed-in"` with that
|
|
149
|
+
* identity's principal, or `"anonymous"` for `identity: "anonymous"` and for
|
|
150
|
+
* an explicit `AnonymousIdentity`. A client on a server reports
|
|
151
|
+
* `"anonymous"`.
|
|
152
|
+
*/
|
|
153
|
+
export interface AuthState {
|
|
154
|
+
/**
|
|
155
|
+
* - `anonymous`: nobody is signed in (the auth says `signed-out`, or a state
|
|
156
|
+
* the client does not know), the client runs on a server, or it was built
|
|
157
|
+
* anonymous.
|
|
158
|
+
* - `signed-in`: calls are signed by `principal`.
|
|
159
|
+
* - `expired`: the session ended; calls are anonymous until a sign-in.
|
|
160
|
+
* - `signed-in-elsewhere`: someone is signed in on this domain but this
|
|
161
|
+
* origin holds no credential for them yet; calls are anonymous.
|
|
162
|
+
*/
|
|
163
|
+
readonly status: "anonymous" | "signed-in" | "expired" | "signed-in-elsewhere"
|
|
164
|
+
/** The principal calls go out as, as text. */
|
|
165
|
+
readonly principal: string
|
|
166
|
+
/**
|
|
167
|
+
* When the session ends, in milliseconds since the epoch (`Date.now()`'s
|
|
168
|
+
* scale; `new Date(expiresAt)` for a date), so an app can show "signed in
|
|
169
|
+
* until". Present only while `status` is `"signed-in"` through an auth whose
|
|
170
|
+
* `getStatus()` carries `expiresAtMs`, as `AuthClient` 10's does; absent for
|
|
171
|
+
* a client built with `identity`. The client tells its listeners when that
|
|
172
|
+
* time passes, and the status turns `"expired"`.
|
|
173
|
+
*
|
|
174
|
+
* A `"signed-in"` session may still be read-only: Internet Identity can
|
|
175
|
+
* scope a session to queries, `AuthClient` reports it `"signed-in"` all the
|
|
176
|
+
* same, and the network refuses its updates. Nothing here tells the two
|
|
177
|
+
* apart yet.
|
|
178
|
+
*/
|
|
179
|
+
readonly expiresAt?: number
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Options for {@link createClient}: a network, and exactly one of `identity`
|
|
184
|
+
* and `auth`, so that who calls is always written down.
|
|
185
|
+
*/
|
|
186
|
+
export type ClientOptions = {
|
|
187
|
+
/** Where the canisters are. See {@link Network}. */
|
|
188
|
+
readonly network: Network
|
|
189
|
+
/**
|
|
190
|
+
* Whether the `ic_env` cookie (root key and canister ids) is believed. See
|
|
191
|
+
* the `network` option; absent trusts it only when both the page and the
|
|
192
|
+
* replica are local. Even `true` never takes the cookie's root key for a
|
|
193
|
+
* mainnet host, and `true` is refused (a `TypeError`) on a page under
|
|
194
|
+
* `ic0.app`, `github.dev`, `gitpod.io`, `loca.lt` or `devtunnels.ms`, which
|
|
195
|
+
* are not public suffixes.
|
|
196
|
+
*/
|
|
197
|
+
readonly allowEnvConfig?: boolean
|
|
198
|
+
/**
|
|
199
|
+
* The `fetch` every agent of this client sends with. Defaults to the global
|
|
200
|
+
* one. Tests pass a fake replica's `fetch` here; nothing global is touched.
|
|
201
|
+
*/
|
|
202
|
+
readonly fetch?: typeof globalThis.fetch
|
|
203
|
+
/**
|
|
204
|
+
* How deep a decoded reply may nest. Defaults to 256. Raise it for replies
|
|
205
|
+
* that nest deeper, such as an ICRC-3 block log's values.
|
|
206
|
+
*/
|
|
207
|
+
readonly maxDepth?: number
|
|
208
|
+
/**
|
|
209
|
+
* How many values a decoded reply may hold, beyond one per byte of the
|
|
210
|
+
* reply. Defaults to 1,000,000. A reply is always allowed one value per byte
|
|
211
|
+
* on top of this, so a blob (a wasm module, an asset chunk) of any size the
|
|
212
|
+
* IC returns decodes; this bounds only the values that take no bytes, such
|
|
213
|
+
* as a long `vec null`.
|
|
214
|
+
*/
|
|
215
|
+
readonly maxElements?: number
|
|
216
|
+
} & (
|
|
217
|
+
| {
|
|
218
|
+
/**
|
|
219
|
+
* A fixed caller. `"anonymous"` makes a read-only client: every update
|
|
220
|
+
* is refused before it is sent. An `Identity` signs every call, updates
|
|
221
|
+
* included; pass an explicit `new AnonymousIdentity()` for an anonymous
|
|
222
|
+
* client that may send updates.
|
|
223
|
+
*/
|
|
224
|
+
readonly identity: Identity | "anonymous"
|
|
225
|
+
readonly auth?: never
|
|
226
|
+
}
|
|
227
|
+
| {
|
|
228
|
+
/**
|
|
229
|
+
* Builds the sign-in this client calls as, such as
|
|
230
|
+
* `(network) => new AuthClient(network)`. It is called once, on first
|
|
231
|
+
* use, and only in a browser: on a server the client is anonymous and
|
|
232
|
+
* never calls it. The client owns what it returns and disposes it with
|
|
233
|
+
* itself.
|
|
234
|
+
*
|
|
235
|
+
* A factory that throws (`new AuthClient()` reads `localStorage`, which
|
|
236
|
+
* throws where site data is blocked) is not called again: the client
|
|
237
|
+
* calls anonymously, so public reads still work, logs the error once in
|
|
238
|
+
* development, and rejects {@link Client.signIn} and
|
|
239
|
+
* {@link Client.signOut} with what the factory threw. The same holds
|
|
240
|
+
* for a factory that returns something other than an {@link AuthLike},
|
|
241
|
+
* such as a Promise (an `async` factory, or `AuthClient.create()` of
|
|
242
|
+
* `@icp-sdk/auth` 5 and earlier): the error is then a `TypeError`
|
|
243
|
+
* naming what the result lacks.
|
|
244
|
+
*
|
|
245
|
+
* `network` is the client's network under `@icp-sdk/auth` 10's own
|
|
246
|
+
* option names, so `new AuthClient(network)` signs in where the client
|
|
247
|
+
* calls:
|
|
248
|
+
*
|
|
249
|
+
* - `agentOptions`: the host, root key and `shouldFetchRootKey` of the
|
|
250
|
+
* client's own agents, and its `fetch`, when you gave one.
|
|
251
|
+
* - `identityProvider`: the Internet Identity to sign in with. The
|
|
252
|
+
* `INTERNET_IDENTITY_PROVIDER` of an `ic_env` cookie believed in full,
|
|
253
|
+
* on a local page or with `allowEnvConfig: true` (with its
|
|
254
|
+
* `PUBLIC_CANISTER_ID:internet_identity`, or
|
|
255
|
+
* `rdmx6-jaaaa-aaaaa-aaadq-cai`); never that of a cookie believed for
|
|
256
|
+
* canister ids only, as on `<id>.icp0.io`; else, for `"local"` or a local host,
|
|
257
|
+
* icp-cli's built-in one at `http://id.ai.localhost:<port>/authorize`;
|
|
258
|
+
* else absent, which `AuthClient` reads as mainnet's.
|
|
259
|
+
*
|
|
260
|
+
* Absent on a network that is not mainnet's (a project that deploys its
|
|
261
|
+
* own `internet_identity`, `"env"` on a local page with no provider in
|
|
262
|
+
* the cookie, a replica with its own root key), sign-in cannot work: a
|
|
263
|
+
* replica rejects the delegations mainnet's Internet Identity mints.
|
|
264
|
+
* The client says so once, in development, when the factory reads
|
|
265
|
+
* `network.identityProvider`, as `new AuthClient(network)` does. Name
|
|
266
|
+
* the provider:
|
|
267
|
+
* `(network) => new AuthClient({ ...network, identityProvider: { authorizeUrl, canisterId } })`.
|
|
268
|
+
* A factory that names its own provider, as that one does, or builds
|
|
269
|
+
* an auth that is not Internet Identity, never sees the warning. Nor
|
|
270
|
+
* does a spread that adds other options and names no provider, such as
|
|
271
|
+
* `{ ...network, derivationOrigin }`: it must name one on such a network.
|
|
272
|
+
*
|
|
273
|
+
* A factory that takes no argument, such as `() => new AuthClient()`,
|
|
274
|
+
* still works on mainnet. It ignores `network`, so on any other network
|
|
275
|
+
* it signs in with mainnet's Internet Identity: pass `network` through.
|
|
276
|
+
*/
|
|
277
|
+
readonly auth: (network: {
|
|
278
|
+
readonly agentOptions: {
|
|
279
|
+
readonly host: string
|
|
280
|
+
readonly rootKey?: Uint8Array
|
|
281
|
+
readonly shouldFetchRootKey: boolean
|
|
282
|
+
readonly fetch?: typeof globalThis.fetch
|
|
283
|
+
}
|
|
284
|
+
readonly identityProvider?: {
|
|
285
|
+
readonly authorizeUrl: string
|
|
286
|
+
readonly canisterId: string
|
|
287
|
+
}
|
|
288
|
+
}) => AuthLike
|
|
289
|
+
readonly identity?: never
|
|
290
|
+
}
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
/** What the `auth` factory is handed: `network` in `(network) => new AuthClient(network)`. */
|
|
294
|
+
type AuthNetworkArgument = Parameters<
|
|
295
|
+
Extract<ClientOptions, { readonly auth: unknown }>["auth"]
|
|
296
|
+
>[0]
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* A client made by {@link createClient}: one per browser tab, or one per
|
|
300
|
+
* request on a server, so that no cache or agent is shared between users.
|
|
301
|
+
*/
|
|
302
|
+
export interface Client {
|
|
303
|
+
/** The network's segment in query keys: `"ic"`, `"local"`, a custom network's `name ?? host`, or the resolved host for `"env"`. */
|
|
304
|
+
readonly network: string
|
|
305
|
+
/**
|
|
306
|
+
* The `QueryClient` this client owns. Its default `retry` retries a
|
|
307
|
+
* `ReactorError` only after a failure that proves it was not delivered, and
|
|
308
|
+
* any other error (an app's own REST query on this `QueryClient`) as
|
|
309
|
+
* TanStack Query does by default: up to 3 times; neither is retried on a
|
|
310
|
+
* server. Its dehydrate and hydrate defaults keep `bigint`, `Uint8Array`
|
|
311
|
+
* and the floats JSON cannot write, so a server's `dehydrate()` survives
|
|
312
|
+
* `JSON.stringify` and comes back exact.
|
|
313
|
+
*/
|
|
314
|
+
readonly queryClient: QueryClient
|
|
315
|
+
/** The principal calls go out as now, as text: the anonymous principal unless signed in. */
|
|
316
|
+
caller(): string
|
|
317
|
+
/**
|
|
318
|
+
* Who calls go out as, and why. The object is the same until the status,
|
|
319
|
+
* the principal or `expiresAt` changes, so it can be read with
|
|
320
|
+
* `useSyncExternalStore`.
|
|
321
|
+
*/
|
|
322
|
+
authState(): AuthState
|
|
323
|
+
/**
|
|
324
|
+
* Calls `listener` once after each change of {@link Client.authState}, and
|
|
325
|
+
* never for a notification of the auth that changed none of the status,
|
|
326
|
+
* the principal and `expiresAt` (a renewed delegation, for one). A session
|
|
327
|
+
* that ends by the clock is a change too: the client sets a timer for its
|
|
328
|
+
* {@link AuthState.expiresAt}, so listeners hear of it without any
|
|
329
|
+
* notification of the auth, and a read of {@link Client.caller} or
|
|
330
|
+
* {@link Client.authState} that sees a change first tells them in a
|
|
331
|
+
* microtask. Returns a function that stops listening. On a disposed client
|
|
332
|
+
* it adds nothing.
|
|
333
|
+
*/
|
|
334
|
+
subscribe(listener: () => void): () => void
|
|
335
|
+
/**
|
|
336
|
+
* Signs in through the client's auth, passing `options` on. Listeners are
|
|
337
|
+
* told once the auth settles, even if the auth itself did not notify.
|
|
338
|
+
* Rejects a `TypeError` on a client built with `identity`, which has no
|
|
339
|
+
* sign-in, an `Error` on a server or after {@link Client.dispose}, and
|
|
340
|
+
* with what the auth factory threw when it threw (a `TypeError` when it
|
|
341
|
+
* returned something other than an {@link AuthLike}).
|
|
342
|
+
*
|
|
343
|
+
* A call in the same synchronous run as an earlier one with the same
|
|
344
|
+
* options (the same value, or an object with the same own enumerable keys
|
|
345
|
+
* and values) returns that call's promise and does not ask the auth again.
|
|
346
|
+
* React's `StrictMode` runs an effect twice in one run, and an Internet
|
|
347
|
+
* Identity sign-in over `transport: "redirect"` that starts twice fails on
|
|
348
|
+
* the load it comes back to. A call in a later run, such as a second click,
|
|
349
|
+
* signs in again.
|
|
350
|
+
*/
|
|
351
|
+
signIn(options?: unknown): Promise<void>
|
|
352
|
+
/**
|
|
353
|
+
* Signs out through the client's auth, passing `options` on (such as
|
|
354
|
+
* `AuthClient`'s `{ returnTo }`). Rejects like {@link Client.signIn}.
|
|
355
|
+
*/
|
|
356
|
+
signOut(options?: unknown): Promise<void>
|
|
357
|
+
/**
|
|
358
|
+
* Releases everything the client holds: it stops listening to its auth and
|
|
359
|
+
* disposes it, clears the `QueryClient`, and drops its agents. Calling it
|
|
360
|
+
* again does nothing.
|
|
361
|
+
*
|
|
362
|
+
* Every call made on the client afterwards, a write included, rejects
|
|
363
|
+
* `cancelled` with code `client_disposed` and `mayHaveExecuted: false`, and
|
|
364
|
+
* sends nothing: a direct call, a query or mutation function, a func
|
|
365
|
+
* reference's function. A call still waiting to be sent (for its caller's
|
|
366
|
+
* identity, or to be sent again) is cancelled the same way. A direct call
|
|
367
|
+
* already sent settles as the replica answers; a read the client's
|
|
368
|
+
* `QueryClient` was running is dropped with the cache.
|
|
369
|
+
*/
|
|
370
|
+
dispose(): void
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* A canister to call: a frozen object with one plain async method per
|
|
374
|
+
* Candid method of `service`, typed from the generated `Actor` you name.
|
|
375
|
+
*
|
|
376
|
+
* ```ts
|
|
377
|
+
* import { actor, type Actor } from "./canisters/icrc1"
|
|
378
|
+
* const ledger = client.canister<Actor>(actor, { id: LEDGER_ID })
|
|
379
|
+
* const balance = await ledger.icrc1_balance_of({ owner, subaccount: null })
|
|
380
|
+
* ```
|
|
381
|
+
*
|
|
382
|
+
* `service` is the `actor` export of a module `meant gen` wrote,
|
|
383
|
+
* or the `actor` of `schemaFromContract()`: both make the same calls and the
|
|
384
|
+
* same keys. `<A>` is not checked against `service`; pass the `Actor` of
|
|
385
|
+
* the same module.
|
|
386
|
+
*
|
|
387
|
+
* A method sends its call as the caller signed in at the moment it is
|
|
388
|
+
* called, decodes the reply, and resolves with it as the `Actor` types it,
|
|
389
|
+
* except that a method whose one result is an `Ok`/`Err` variant resolves
|
|
390
|
+
* with the `Ok` payload and rejects `canister_err` with the `Err` one. An
|
|
391
|
+
* update or a oneway by a caller who is not signed in rejects
|
|
392
|
+
* `unauthenticated` and sends nothing. Every failure is a `ReactorError`;
|
|
393
|
+
* see {@link Canister} for what is re-sent.
|
|
394
|
+
*
|
|
395
|
+
* The same service object and the same target give the same canister
|
|
396
|
+
* object on every call, so it can be made during render. See
|
|
397
|
+
* {@link CanisterTarget} for `{ id }`, `{ name }` and `certified`.
|
|
398
|
+
*
|
|
399
|
+
* @throws TypeError for a `service` that is not a service schema, or a
|
|
400
|
+
* target that is not one of the allowed shapes or whose `id` is not
|
|
401
|
+
* principal text.
|
|
402
|
+
*/
|
|
403
|
+
canister<A>(service: Schema<Principal>, target: CanisterTarget): Canister<A>
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* The canister id a target resolves to now, by the same resolver the calls
|
|
407
|
+
* of `client.canister(service, target)` use, without making a call.
|
|
408
|
+
*
|
|
409
|
+
* ```ts
|
|
410
|
+
* const backend = client.canisterId({ name: "backend" })
|
|
411
|
+
* if (!backend.ok && backend.reason === "untrusted_host") showSetupHint()
|
|
412
|
+
* ```
|
|
413
|
+
*
|
|
414
|
+
* `{ id }` gives that id. `{ name }` is read from the `ic_env` cookie, as a
|
|
415
|
+
* call reads it, and gives the id or one of three reasons, each with its
|
|
416
|
+
* own fix:
|
|
417
|
+
*
|
|
418
|
+
* - `no_browser`: there is no page, so no cookie to read (a server render,
|
|
419
|
+
* a web worker).
|
|
420
|
+
* - `untrusted_host`: the cookie is not trusted for this network, so it was
|
|
421
|
+
* not read; or a page that is not local holds more than one `ic_env`
|
|
422
|
+
* cookie and they differ, so none is believed.
|
|
423
|
+
* - `absent`: the cookie was read and has no principal text under that
|
|
424
|
+
* name.
|
|
425
|
+
*
|
|
426
|
+
* `message` is the resolver's reason, the text a call on that target puts
|
|
427
|
+
* in its error after the method and target. The answer is not kept: each call reads the cookie again, as each call of a
|
|
428
|
+
* `{ name }` canister does. It sends nothing, and a `certified` flag in the
|
|
429
|
+
* target is ignored. An id it gives is the one the cookie names; whether a
|
|
430
|
+
* canister answers there is not checked.
|
|
431
|
+
*
|
|
432
|
+
* @throws TypeError for a target that is not one of the allowed shapes or
|
|
433
|
+
* whose `id` is not principal text.
|
|
434
|
+
*/
|
|
435
|
+
canisterId(target: CanisterTarget):
|
|
436
|
+
| { readonly ok: true; readonly id: string }
|
|
437
|
+
| {
|
|
438
|
+
readonly ok: false
|
|
439
|
+
readonly reason: "no_browser" | "untrusted_host" | "absent"
|
|
440
|
+
readonly message: string
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/**
|
|
444
|
+
* The query key of a read, or the prefix of a group of reads, for
|
|
445
|
+
* `queryClient.invalidateQueries`, `getQueryData` and filters:
|
|
446
|
+
*
|
|
447
|
+
* - `queryKey(c)`: every read of the canister by the current caller.
|
|
448
|
+
* - `queryKey(c, method)`: every read of that method by the current caller,
|
|
449
|
+
* whatever its arguments. For a method without arguments, which has one
|
|
450
|
+
* read, this is that read's key, the same as `queryKey(c, method,
|
|
451
|
+
* undefined)`, so `queryClient.getQueryData(client.queryKey(ledger,
|
|
452
|
+
* "icrc1_fee"))` finds what `queryOptions(ledger, "icrc1_fee")` cached.
|
|
453
|
+
* - `queryKey(c, method, vars)`: the key `queryOptions(c, method, vars)`
|
|
454
|
+
* gives, as built now.
|
|
455
|
+
*
|
|
456
|
+
* `getQueryData` and `setQueryData` match a key exactly: hand them a read's
|
|
457
|
+
* whole key, never a prefix (a method's with arguments, or a canister's).
|
|
458
|
+
*
|
|
459
|
+
* Keys are `['ic-reactor', network, caller, canisterId, method, args]` plus
|
|
460
|
+
* `'certified'` for a certified canister (DECISIONS Q5). Build them with
|
|
461
|
+
* this method, never by hand: a key typed out as an array literal is a
|
|
462
|
+
* `QueryKey` like any other, and matches nothing the client invalidates.
|
|
463
|
+
* It never throws for `vars` that do not encode: the key then holds
|
|
464
|
+
* `'$invalid'` and a text of the value.
|
|
465
|
+
*
|
|
466
|
+
* @throws TypeError for a canister of another client, or a method the
|
|
467
|
+
* service does not have.
|
|
468
|
+
*/
|
|
469
|
+
queryKey<A, M extends keyof A & string>(
|
|
470
|
+
canister: Canister<A>,
|
|
471
|
+
method?: M,
|
|
472
|
+
vars?: VarsOf<A, M>
|
|
473
|
+
): QueryKey
|
|
474
|
+
|
|
475
|
+
/**
|
|
476
|
+
* TanStack Query options for a read: `useQuery(client.queryOptions(ledger,
|
|
477
|
+
* "icrc1_balance_of", account))`, `queryClient.fetchQuery(...)`, a
|
|
478
|
+
* `QueryObserver`.
|
|
479
|
+
*
|
|
480
|
+
* The variables after `method` follow one rule (DECISIONS Q3): nothing for
|
|
481
|
+
* a method without arguments, the value for one argument, the tuple for two
|
|
482
|
+
* or more. Pass `skipToken` (from `@tanstack/query-core` or
|
|
483
|
+
* `@tanstack/react-query`) in their place while they are not known yet.
|
|
484
|
+
*
|
|
485
|
+
* The read is made as the caller current when the options are built: the
|
|
486
|
+
* key holds their principal, and the query function refuses to run
|
|
487
|
+
* (`cancelled`, nothing sent) once someone else is signed in. Build the
|
|
488
|
+
* options again when the caller changes, as a component does on every
|
|
489
|
+
* render, and the read moves to the new caller's key: one principal's data
|
|
490
|
+
* is never shown under another's.
|
|
491
|
+
*
|
|
492
|
+
* Arguments that do not encode give a key tagged `'$invalid'` and a query
|
|
493
|
+
* function that rejects `invalid_args` with the codec's issues, so a render
|
|
494
|
+
* never throws on half-typed input.
|
|
495
|
+
*
|
|
496
|
+
* `retry` retries only a failure that proves the call was not delivered, at
|
|
497
|
+
* most 3 times, and never on a server.
|
|
498
|
+
*
|
|
499
|
+
* An update or a oneway method of a generated `Actor` does not compile
|
|
500
|
+
* here (`method` is a `ReadMethod<A>`): a query refetches, and every
|
|
501
|
+
* refetch would run the update again. Use {@link Client.mutationOptions}
|
|
502
|
+
* for a write. The generated `Actor` says each method's mode; a method of
|
|
503
|
+
* an `Actor` written by hand says none and compiles, and this checks it at
|
|
504
|
+
* run time instead, throwing a `TypeError` for an update or a oneway. An
|
|
505
|
+
* update that returns the same answer however often it runs (ckBTC's
|
|
506
|
+
* `get_btc_address`) can be read with `{ update: "idempotent" }` as the
|
|
507
|
+
* fourth argument (the third signature; a oneway does not compile even
|
|
508
|
+
* then): it is fetched once per key and caller (`staleTime: Infinity`, no
|
|
509
|
+
* refetch on mount, focus or reconnect), needs a signed-in caller like any
|
|
510
|
+
* update, and is retried only after a failure that proves it never got in
|
|
511
|
+
* (DECISIONS Q2). It is still a read of its canister, so a write to that
|
|
512
|
+
* canister through {@link Client.mutationOptions} invalidates it by default
|
|
513
|
+
* and it runs once more, as a replicated call: safe for an idempotent
|
|
514
|
+
* method, but a cost. To keep it cached across writes, name the reads a
|
|
515
|
+
* write changes in `invalidates`.
|
|
516
|
+
*
|
|
517
|
+
* A method TypeScript cannot tell is a `ReadMethod<A>` does not compile,
|
|
518
|
+
* even when every value of it names a query: a type parameter
|
|
519
|
+
* (`M extends keyof A & string`), a union that may name an update
|
|
520
|
+
* (`keyof Actor & string`), or any method of a `Canister<A>` whose `A` is
|
|
521
|
+
* itself generic. Give the type arguments,
|
|
522
|
+
* `queryOptions<A, any>(canister, method, vars)`, which keeps the run-time
|
|
523
|
+
* check and types the data `unknown`.
|
|
524
|
+
*
|
|
525
|
+
* A method without results (`() -> () query`) has nothing to cache: its
|
|
526
|
+
* call resolves `undefined`, which TanStack Query takes for a failed read.
|
|
527
|
+
* This throws a `TypeError` for it too; call it directly, as
|
|
528
|
+
* `await canister.method()`.
|
|
529
|
+
*
|
|
530
|
+
* Built from variables that cannot be `skipToken`, the options' `queryFn`
|
|
531
|
+
* is never `skipToken` either, so `useSuspenseQuery` and
|
|
532
|
+
* `useSuspenseQueries` take them as they are. Variables of a type
|
|
533
|
+
* `skipToken` is assignable to, such as `unknown` (a Candid `reserved`
|
|
534
|
+
* argument) or `{}`, may be, so every read of such a method takes the next
|
|
535
|
+
* signature and keeps `SkipToken`.
|
|
536
|
+
*
|
|
537
|
+
* @throws TypeError for an update or oneway method without the opt-in, a
|
|
538
|
+
* method without results, a composite query of a certified canister, a
|
|
539
|
+
* canister of another client, a method the service does not have, or a
|
|
540
|
+
* fourth argument other than `{ update: "idempotent" }`. An update or a
|
|
541
|
+
* oneway reaches this check only through an `Actor` that does not say the
|
|
542
|
+
* method's mode.
|
|
543
|
+
*/
|
|
544
|
+
queryOptions<A, M extends ReadMethod<A>>(
|
|
545
|
+
canister: Canister<A>,
|
|
546
|
+
method: M,
|
|
547
|
+
...args: QueryArgs<A, M, never>
|
|
548
|
+
): CanisterQueryOptions<DataOf<A, M>, ErrorOf<A, M>, never>
|
|
549
|
+
/**
|
|
550
|
+
* TanStack Query options for a read whose variables may be `skipToken`
|
|
551
|
+
* (`V | SkipToken`, or `skipToken` itself): the options' `queryFn` is
|
|
552
|
+
* `skipToken` for a skipped read, which `useQuery` takes and
|
|
553
|
+
* `useSuspenseQuery` does not. Otherwise the same as the call with
|
|
554
|
+
* variables that cannot be skipped: see that signature for the rules.
|
|
555
|
+
*
|
|
556
|
+
* @throws TypeError as the call with variables that cannot be skipped.
|
|
557
|
+
*/
|
|
558
|
+
queryOptions<A, M extends ReadMethod<A>>(
|
|
559
|
+
canister: Canister<A>,
|
|
560
|
+
method: M,
|
|
561
|
+
...args: QueryArgs<A, M>
|
|
562
|
+
): CanisterQueryOptions<DataOf<A, M>, ErrorOf<A, M>>
|
|
563
|
+
/**
|
|
564
|
+
* TanStack Query options for an update read like a query, with
|
|
565
|
+
* `{ update: "idempotent" }`: only for an update that returns the same
|
|
566
|
+
* answer however often it runs, such as ckBTC's `get_btc_address`.
|
|
567
|
+
*
|
|
568
|
+
* ```ts
|
|
569
|
+
* useQuery(client.queryOptions(minter, "get_btc_address", arg, { update: "idempotent" }))
|
|
570
|
+
* ```
|
|
571
|
+
*
|
|
572
|
+
* The variables come first, even `undefined` for a method without
|
|
573
|
+
* arguments. A oneway method does not compile here either (`method` is an
|
|
574
|
+
* `OptInMethod<A>`): it has no reply to cache. The read is fetched once per
|
|
575
|
+
* key and caller; see the first signature for the rules every read
|
|
576
|
+
* follows.
|
|
577
|
+
*
|
|
578
|
+
* @throws TypeError as the call without the opt-in, except for an update.
|
|
579
|
+
*/
|
|
580
|
+
queryOptions<A, M extends OptInMethod<A>>(
|
|
581
|
+
canister: Canister<A>,
|
|
582
|
+
method: M,
|
|
583
|
+
...args: OptInQueryArgs<A, M, never>
|
|
584
|
+
): CanisterQueryOptions<DataOf<A, M>, ErrorOf<A, M>, never>
|
|
585
|
+
/**
|
|
586
|
+
* The opt-in read of an update, with variables that may be `skipToken`:
|
|
587
|
+
* its `queryFn` is `skipToken` for a skipped read. Otherwise the same as
|
|
588
|
+
* the opt-in call with variables that cannot be skipped.
|
|
589
|
+
*
|
|
590
|
+
* @throws TypeError as the call without the opt-in, except for an update.
|
|
591
|
+
*/
|
|
592
|
+
queryOptions<A, M extends OptInMethod<A>>(
|
|
593
|
+
canister: Canister<A>,
|
|
594
|
+
method: M,
|
|
595
|
+
...args: OptInQueryArgs<A, M>
|
|
596
|
+
): CanisterQueryOptions<DataOf<A, M>, ErrorOf<A, M>>
|
|
597
|
+
|
|
598
|
+
/**
|
|
599
|
+
* TanStack Query options for a write: `useMutation(client.mutationOptions(
|
|
600
|
+
* ledger, "icrc1_transfer"))`, then `mutate(arg)` with the same argument
|
|
601
|
+
* convention as {@link Client.queryOptions}.
|
|
602
|
+
*
|
|
603
|
+
* The mutation function calls the method as the caller current when
|
|
604
|
+
* `mutate()` was called (read in `onMutate`), or not at all: a write that
|
|
605
|
+
* TanStack Query holds back (offline, or behind another mutation of its
|
|
606
|
+
* `scope`) while the account changes rejects `cancelled`, code
|
|
607
|
+
* `caller_changed`, `mayHaveExecuted: false`, and sends nothing; so does a
|
|
608
|
+
* mutation restored from a persister, whose caller is not known. The
|
|
609
|
+
* client's own `queryClient` reads each run's caller in its
|
|
610
|
+
* `MutationCache`, so this holds with an `onMutate` of the app's that
|
|
611
|
+
* replaces this one; the JSDoc of the returned `mutationFn` covers a run on
|
|
612
|
+
* another `QueryClient`. The options of a view (what `useClient()` returns)
|
|
613
|
+
* also call only as the view's principal, and end their `mutationKey` with
|
|
614
|
+
* it: while someone else is current their mutation function rejects
|
|
615
|
+
* `cancelled` the same way. An update by a caller who is not signed in
|
|
616
|
+
* rejects `unauthenticated` before anything is sent. `retry` is `false`: an
|
|
617
|
+
* update
|
|
618
|
+
* re-sends itself only when the failure proves the first attempt never got
|
|
619
|
+
* in (see {@link Canister}), and a TanStack retry would re-send after
|
|
620
|
+
* failures that do not prove it, running the write twice.
|
|
621
|
+
*
|
|
622
|
+
* `onSettled` invalidates the reads the write may have changed, on the
|
|
623
|
+
* `QueryClient` the mutation ran on (which TanStack Query 5.89 and later
|
|
624
|
+
* hands it; an app's own `QueryClientProvider` nested inside the client's)
|
|
625
|
+
* and on this client's `queryClient`, for every caller, certified or not,
|
|
626
|
+
* and waits for the active ones to refetch: after a success, a
|
|
627
|
+
* `canister_err`, and any failure whose `mayHaveExecuted` is `true` (a lost
|
|
628
|
+
* reply: the re-read shows whether it happened). Not after a failure that proves nothing ran
|
|
629
|
+
* (`invalid_args`, `unauthenticated`, `not_delivered`, a reject 1 or 3, a
|
|
630
|
+
* `cancelled` before sending). The reads are every read of the canister
|
|
631
|
+
* written to (DECISIONS Q10), or those listed in `invalidates`: canisters
|
|
632
|
+
* and `[canister, method]` pairs of this client; `[]` invalidates nothing.
|
|
633
|
+
* "Every read" includes an update read with `{ update: "idempotent" }`
|
|
634
|
+
* (such as a ckBTC minter's `get_btc_address` after `update_balance`),
|
|
635
|
+
* whose re-read is one more replicated call; list the reads in
|
|
636
|
+
* `invalidates` to leave it cached.
|
|
637
|
+
*
|
|
638
|
+
* A `{ name }` (the canister written to, or one in `invalidates`) is
|
|
639
|
+
* resolved once per run, in `onMutate`, which TanStack Query runs first
|
|
640
|
+
* and whose result it hands to `onSettled`: a write whose `ic_env` cookie
|
|
641
|
+
* entry changes before it settles (a local redeploy) invalidates the reads
|
|
642
|
+
* of the canister it wrote to. To add your own `onSettled`, call this one
|
|
643
|
+
* from it with every argument it gets; to add your own `onMutate`, call
|
|
644
|
+
* this one from it with both and spread its result into yours:
|
|
645
|
+
* `{ ...options.onMutate(variables, context), previous }`. An `onMutate`
|
|
646
|
+
* that replaces this one keeps the write and its invalidation together
|
|
647
|
+
* from TanStack Query 5.89 on, which hands `mutationFn` and `onSettled` the
|
|
648
|
+
* same run context: `mutationFn` resolves the targets when it starts, and
|
|
649
|
+
* `onSettled` reads them from there. Before 5.89 it leaves `mutationFn` and
|
|
650
|
+
* `onSettled` each to resolve the targets when it runs, so a cookie
|
|
651
|
+
* rewritten between the two can split the write from its invalidation.
|
|
652
|
+
*
|
|
653
|
+
* @throws TypeError for a canister of another client, a method a service
|
|
654
|
+
* does not have, or a third argument other than `{ invalidates }`.
|
|
655
|
+
*/
|
|
656
|
+
mutationOptions<A, M extends keyof A & string>(
|
|
657
|
+
canister: Canister<A>,
|
|
658
|
+
method: M,
|
|
659
|
+
options?: MutationOptionsOptions
|
|
660
|
+
): CanisterMutationOptions<VarsOf<A, M>, DataOf<A, M>, ErrorOf<A, M>>
|
|
661
|
+
|
|
662
|
+
/**
|
|
663
|
+
* The write `error` reports, offered to send again exactly as it was sent,
|
|
664
|
+
* by whoever sent it, when its outcome is unknown; `undefined` when there
|
|
665
|
+
* is nothing to offer.
|
|
666
|
+
*
|
|
667
|
+
* Only for a method that deduplicates: one that runs the same argument
|
|
668
|
+
* from the same sender at most once, as an ICRC-1 ledger does for a
|
|
669
|
+
* transfer with `created_at_time` set (it answers `Duplicate` the second
|
|
670
|
+
* time). Sending again a write that does not deduplicate can run it twice:
|
|
671
|
+
* there, read the state back instead. `dedupedBy` states what the canister
|
|
672
|
+
* deduplicates on, read from the argument:
|
|
673
|
+
*
|
|
674
|
+
* ```ts
|
|
675
|
+
* const transfer = useMutation(client.mutationOptions(ledger, "icrc1_transfer"))
|
|
676
|
+
* const again = client.resendOf(transfer.error, ledger, "icrc1_transfer", {
|
|
677
|
+
* dedupedBy: (arg) => arg.created_at_time,
|
|
678
|
+
* })
|
|
679
|
+
* // again && <button onClick={() => transfer.mutate(again.arg)}>Send the same transfer again</button>
|
|
680
|
+
* ```
|
|
681
|
+
*
|
|
682
|
+
* It offers a re-send only when all of these hold, and is read again on
|
|
683
|
+
* every call, so read it during render, in a component that re-renders
|
|
684
|
+
* when the caller changes (one that calls `useAuth()`):
|
|
685
|
+
*
|
|
686
|
+
* - `error` is one a write of `method` on `canister` rejected with (a
|
|
687
|
+
* direct call, or a mutation function built from the canister), with
|
|
688
|
+
* `mayHaveExecuted: true`. Any other failure proves the outcome, and a
|
|
689
|
+
* success needs no re-send.
|
|
690
|
+
* - Its argument is an object: a record or a variant, or the list of
|
|
691
|
+
* several arguments.
|
|
692
|
+
* - `dedupedBy(arg)` is neither `undefined` nor `null`.
|
|
693
|
+
* - The argument still encodes to the bytes the attempt sent: an argument
|
|
694
|
+
* object changed since then is not offered.
|
|
695
|
+
* - `canister` still resolves to the canister id the attempt went to: a
|
|
696
|
+
* `{ name }` the `ic_env` cookie maps elsewhere since is not offered.
|
|
697
|
+
* - The principal that sent it is the caller now. After a sign-out, or a
|
|
698
|
+
* sign-in as another principal, nothing is offered: the same argument
|
|
699
|
+
* from another sender is a new write. Signing in again as the sender
|
|
700
|
+
* offers it again. A view of the client pinned to a principal (what
|
|
701
|
+
* `useClient()` returns) answers for the live caller, whom a write goes
|
|
702
|
+
* out as.
|
|
703
|
+
*
|
|
704
|
+
* The offer goes out only as the same write. `send()` sends the bytes the
|
|
705
|
+
* attempt sent, to the canister id it went to. `mutate(arg)`, on the
|
|
706
|
+
* mutation that failed (which keeps its pending state and invalidation) or
|
|
707
|
+
* any other of this client, sends `arg` only to the method and canister id
|
|
708
|
+
* the attempt went to, and only while it encodes to the same bytes. Either
|
|
709
|
+
* one goes out only as `from`. Each refusal sends nothing, and the code
|
|
710
|
+
* says which it is: `caller_changed` once someone else is the caller,
|
|
711
|
+
* `target_changed` for another method or canister id, and `arg_changed`
|
|
712
|
+
* for an argument changed since it was sent. A re-send that is refused, or
|
|
713
|
+
* turned away when sent (an HTTP 4xx, reject code 1 to 3), still rejects
|
|
714
|
+
* `outcome_unknown` with `mayHaveExecuted: true`, because the attempt it
|
|
715
|
+
* re-sends may have run, and this method offers that write again for that
|
|
716
|
+
* error, under the same conditions as for the first. Once
|
|
717
|
+
* offered, that object stays held to that write: build a new argument for
|
|
718
|
+
* a new write, rather than change this one or send it from another account.
|
|
719
|
+
*
|
|
720
|
+
* @throws TypeError for a canister of another client, a method the service
|
|
721
|
+
* does not have, or no `dedupedBy` function.
|
|
722
|
+
*/
|
|
723
|
+
resendOf<A, M extends keyof A & string>(
|
|
724
|
+
error: unknown,
|
|
725
|
+
canister: Canister<A>,
|
|
726
|
+
method: M,
|
|
727
|
+
options: ResendOptions<VarsOf<A, M>>
|
|
728
|
+
): Resend<VarsOf<A, M>, DataOf<A, M>> | undefined
|
|
729
|
+
|
|
730
|
+
/**
|
|
731
|
+
* The Candid bytes a call of `method` with `vars` sends, as a `Uint8Array`:
|
|
732
|
+
* made by the call path's own encoder with its own variables rule (the
|
|
733
|
+
* value for one argument, the tuple for several, nothing for none), so
|
|
734
|
+
* they are the bytes that go out, not a second encoding. Pass them where
|
|
735
|
+
* the exact argument is wanted, such as an ICRC-21 consent request's `arg`:
|
|
736
|
+
*
|
|
737
|
+
* ```ts
|
|
738
|
+
* const arg = client.encode(ledger, "icrc1_transfer", transfer)
|
|
739
|
+
* const consent = await ledger.icrc21_canister_call_consent_message({
|
|
740
|
+
* method: "icrc1_transfer",
|
|
741
|
+
* arg,
|
|
742
|
+
* user_preferences: preferences,
|
|
743
|
+
* })
|
|
744
|
+
* ```
|
|
745
|
+
*
|
|
746
|
+
* It sends nothing and needs no canister id, so a `{ name }` the `ic_env`
|
|
747
|
+
* cookie does not resolve encodes too.
|
|
748
|
+
*
|
|
749
|
+
* It checks only the variables: not the caller, the canister id or
|
|
750
|
+
* whether the client is disposed, which a call checks before it encodes.
|
|
751
|
+
*
|
|
752
|
+
* @throws ReactorError `invalid_args` for variables that do not encode,
|
|
753
|
+
* with the `issues` and `reason` a call rejects with when its variables are
|
|
754
|
+
* all that is wrong.
|
|
755
|
+
* @throws TypeError for a canister of another client, or a method the
|
|
756
|
+
* service does not have.
|
|
757
|
+
*/
|
|
758
|
+
encode<A, M extends keyof A & string>(
|
|
759
|
+
canister: Canister<A>,
|
|
760
|
+
method: M,
|
|
761
|
+
vars: VarsOf<A, M>
|
|
762
|
+
): Uint8Array
|
|
763
|
+
|
|
764
|
+
/**
|
|
765
|
+
* An async function for a func reference a reply carried, such as an ICRC
|
|
766
|
+
* ledger's archive callback:
|
|
767
|
+
*
|
|
768
|
+
* ```ts
|
|
769
|
+
* import { QueryArchiveFn, type GetBlocksArgs, type BlockRange } from "./canisters/ledger"
|
|
770
|
+
* for (const range of reply.archived_blocks) {
|
|
771
|
+
* const read = client.func<(arg: GetBlocksArgs) => Promise<BlockRange>>(QueryArchiveFn, range.callback)
|
|
772
|
+
* const { blocks } = await read({ start: range.start, length: range.length })
|
|
773
|
+
* }
|
|
774
|
+
* ```
|
|
775
|
+
*
|
|
776
|
+
* The call goes to `ref.principal` and `ref.method`, with the arguments,
|
|
777
|
+
* results and mode of `funcSchema` (the generated schema of the func type),
|
|
778
|
+
* through the same path as a canister's methods: the same errors, the same
|
|
779
|
+
* caller rules, the same unwrapping. `F` is not checked against
|
|
780
|
+
* `funcSchema`: a generated func type carries no signature, so write the
|
|
781
|
+
* one its `.did` gives, with the `Ok` payload as the reply of a result.
|
|
782
|
+
*
|
|
783
|
+
* @throws TypeError for a schema that is not a func schema, or a `ref`
|
|
784
|
+
* whose principal is not principal text or whose method is not a name.
|
|
785
|
+
*/
|
|
786
|
+
func<F extends (...args: never[]) => Promise<unknown>>(
|
|
787
|
+
funcSchema: Schema<FuncValue>,
|
|
788
|
+
ref: FuncValue
|
|
789
|
+
): F
|
|
790
|
+
|
|
791
|
+
/**
|
|
792
|
+
* The IC's time, in nanoseconds since the epoch (the unit of an ICRC-1
|
|
793
|
+
* `created_at_time`), from a certified `read_state` of the `time` path,
|
|
794
|
+
* routed by `canister`'s subnet. Stamp a deduplication key with it rather
|
|
795
|
+
* than with the host's clock, which a ledger checks against its own.
|
|
796
|
+
*
|
|
797
|
+
* ```ts
|
|
798
|
+
* const created_at_time = await client.time(ledger)
|
|
799
|
+
* ```
|
|
800
|
+
*
|
|
801
|
+
* The IC's time moves about once a second (once per round), so two writes
|
|
802
|
+
* stamped close together can carry the same time: give each its own
|
|
803
|
+
* `memo` as well.
|
|
804
|
+
*
|
|
805
|
+
* Its request carries an ingress expiry from the host's clock, as every
|
|
806
|
+
* request does: a host clock so far off that the IC refuses its requests
|
|
807
|
+
* gets `not_delivered` here too.
|
|
808
|
+
*
|
|
809
|
+
* The certificate is verified against the network's root key, as every
|
|
810
|
+
* reply is; its age is not compared with the host's clock, which may be the
|
|
811
|
+
* clock that is wrong. A certificate that does not verify rejects
|
|
812
|
+
* `not_delivered` with `mayHaveExecuted: false`, and its time is never
|
|
813
|
+
* returned. It needs no signed-in caller, and is sent by the caller's
|
|
814
|
+
* agent, as a read does. A `{ name }` the `ic_env` cookie
|
|
815
|
+
* does not resolve rejects `invalid_args` (`canister_id_unresolved`), as
|
|
816
|
+
* does the management canister `aaaaa-aa` (`management_canister`), which no
|
|
817
|
+
* `read_state` is routed by, and a disposed client rejects `cancelled`
|
|
818
|
+
* (`client_disposed`); nothing is sent then.
|
|
819
|
+
*
|
|
820
|
+
* A canister object made with `timeoutMs` bounds the wait, counted from the
|
|
821
|
+
* moment `time()` is called, as it bounds a call through it. When the bound
|
|
822
|
+
* passes, `time()` rejects `cancelled` with code `"timeout"` and
|
|
823
|
+
* `mayHaveExecuted: false`; a request already out is not torn down, only
|
|
824
|
+
* the waiting stops, and one not sent yet is never sent. Without
|
|
825
|
+
* `timeoutMs`, a `read_state` that gets no HTTP answer waits as long as the
|
|
826
|
+
* `fetch` in use does.
|
|
827
|
+
*/
|
|
828
|
+
time(canister: Canister<object>): Promise<bigint>
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
// ---------------------------------------------------------------------------
|
|
832
|
+
// Internals for the canister builders
|
|
833
|
+
// ---------------------------------------------------------------------------
|
|
834
|
+
|
|
835
|
+
/** Who calls now. */
|
|
836
|
+
export interface Caller {
|
|
837
|
+
/** The principal calls go out as, as text. */
|
|
838
|
+
readonly principal: string
|
|
839
|
+
/**
|
|
840
|
+
* Whether an update may be sent: signed in with a principal that is not the
|
|
841
|
+
* anonymous one, or built with an `Identity` object (an explicit
|
|
842
|
+
* `AnonymousIdentity` included, the deliberate way to write anonymously).
|
|
843
|
+
* `identity: "anonymous"` never is.
|
|
844
|
+
*/
|
|
845
|
+
readonly authenticated: boolean
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
/** The call an agent is asked for, named in the error that cancels it. */
|
|
849
|
+
export interface AgentContext {
|
|
850
|
+
readonly method: string
|
|
851
|
+
readonly canisterId: string
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
/** What the canister builders read from a client. Not part of the package entry. */
|
|
855
|
+
export interface ClientInternals {
|
|
856
|
+
/** The network, with every decision made. */
|
|
857
|
+
readonly network: ResolvedNetwork
|
|
858
|
+
/** The decode depth bound for replies, from {@link ClientOptions.maxDepth}. */
|
|
859
|
+
readonly maxDepth: number
|
|
860
|
+
/** The decode element budget for replies, from {@link ClientOptions.maxElements}. */
|
|
861
|
+
readonly maxElements: number
|
|
862
|
+
/** Who calls now. Reads the auth (building it on first use, in a browser). */
|
|
863
|
+
current(): Caller
|
|
864
|
+
/**
|
|
865
|
+
* Whether {@link Client.dispose} has run. The call path asks it before
|
|
866
|
+
* anything else, so that every call made on a disposed client is cancelled
|
|
867
|
+
* (`client_disposed`): a disposed client calls as the anonymous principal,
|
|
868
|
+
* and a write would otherwise be refused as `unauthenticated`.
|
|
869
|
+
*/
|
|
870
|
+
disposed(): boolean
|
|
871
|
+
/**
|
|
872
|
+
* The caller current when each mutation run of the client's `queryClient`
|
|
873
|
+
* started, by its `Mutation` and (TanStack Query 5.89 and later) by the
|
|
874
|
+
* function context TanStack made for the run. Read by the `MutationCache`'s
|
|
875
|
+
* own `onMutate`, which `mutate()` runs before the mutation's `onMutate`
|
|
876
|
+
* and which a mutation's options cannot replace. Weak, so an entry lives as
|
|
877
|
+
* long as its mutation. A restored mutation (hydrated from a persister) has
|
|
878
|
+
* none: TanStack runs no `onMutate` for it.
|
|
879
|
+
*/
|
|
880
|
+
readonly mutationCallers: WeakMap<object, Caller>
|
|
881
|
+
/**
|
|
882
|
+
* The `mutationFn` of every `mutationOptions` of this client: the only
|
|
883
|
+
* mutations whose caller the `MutationCache` reads.
|
|
884
|
+
*/
|
|
885
|
+
readonly ownMutationFns: WeakSet<object>
|
|
886
|
+
/**
|
|
887
|
+
* The argument of each re-send `resendOf` offered, and the write it was
|
|
888
|
+
* first sent in: a mutation given that very argument sends it as that
|
|
889
|
+
* write's caller, to its canister id, with its bytes, or not at all.
|
|
890
|
+
*/
|
|
891
|
+
readonly offered: WeakMap<object, CallRequest>
|
|
892
|
+
/**
|
|
893
|
+
* The agent that signs as `principal`, and only as `principal`. It is the
|
|
894
|
+
* one place the client enforces "a call goes out as the principal it was
|
|
895
|
+
* made for, or not at all".
|
|
896
|
+
*
|
|
897
|
+
* A read's query function passes the principal captured with its query key,
|
|
898
|
+
* so data cached under a principal's key is only ever fetched as that
|
|
899
|
+
* principal. A direct call and a mutation function pass
|
|
900
|
+
* `current().principal` read at call time.
|
|
901
|
+
*
|
|
902
|
+
* It resolves the identity for `principal` (the fixed identity, the
|
|
903
|
+
* client's own `AnonymousIdentity`, or the auth's `getIdentity()` for a
|
|
904
|
+
* signed-in principal), and then checks again: if by then the current
|
|
905
|
+
* principal is another one, the client was disposed, or the identity is not
|
|
906
|
+
* `principal`'s, it rejects a `cancelled` `ReactorError` (code
|
|
907
|
+
* `"caller_changed"` or `"client_disposed"`, `mayHaveExecuted: false`)
|
|
908
|
+
* and nothing is sent. An auth whose `getIdentity()` rejects gives an
|
|
909
|
+
* `unauthenticated` one (code `"identity_unavailable"`).
|
|
910
|
+
*
|
|
911
|
+
* A principal's agent is built once and reused; a new identity object for
|
|
912
|
+
* the same principal (a renewed delegation) builds a new agent and replaces
|
|
913
|
+
* the old one, which is never handed out again. Every agent is built with
|
|
914
|
+
* `retryTimes: 0`, so it never re-sends a request on its own: the call path
|
|
915
|
+
* decides every re-send from the classified failure.
|
|
916
|
+
*
|
|
917
|
+
* `retryTimes` also governs the agent's other requests, and setting it to 0
|
|
918
|
+
* costs two recoveries the call path (`call.ts`) has to make up for
|
|
919
|
+
* itself, as read in `@icp-sdk/core` 6.1:
|
|
920
|
+
*
|
|
921
|
+
* - Update polling. An update the replica answers 202, or with a v4 reply
|
|
922
|
+
* whose certificate has no status for the request, falls back to polling
|
|
923
|
+
* `read_state`. One fetch failure or 5xx during that polling now ends the
|
|
924
|
+
* call, and it classifies as `outcome_unknown` with
|
|
925
|
+
* `mayHaveExecuted: true`: safe, but it gives up on a call the IC may
|
|
926
|
+
* still answer. `read_state` executes nothing, so the call path can send
|
|
927
|
+
* the update with `agent.call` and poll its request id with
|
|
928
|
+
* `pollForResponse`, polling the same request id again after a transient
|
|
929
|
+
* polling failure. That never runs the update twice, and keeps
|
|
930
|
+
* `retryTimes: 0` for the `call` and `query` requests themselves, which
|
|
931
|
+
* is what the setting is for.
|
|
932
|
+
* - A stale query signature. A query reply signed longer ago than the
|
|
933
|
+
* ingress expiry window no longer makes the agent sync its clock and ask
|
|
934
|
+
* again: it fails as a `Trust` error (`CertificateOutdatedErrorCode`),
|
|
935
|
+
* which a read classifies as a retryable `not_delivered`, so a browser
|
|
936
|
+
* read is asked again through `retryQuery` without a clock sync.
|
|
937
|
+
*
|
|
938
|
+
* Recovery from a refused ingress expiry (the agent syncs its clock once
|
|
939
|
+
* and sends again) does not depend on `retryTimes` and still happens.
|
|
940
|
+
*/
|
|
941
|
+
agentFor(principal: string, context: AgentContext): Promise<HttpAgent>
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
const INTERNALS = new WeakMap<Client, ClientInternals>()
|
|
945
|
+
|
|
946
|
+
/**
|
|
947
|
+
* The internals of a client made by {@link createClient}.
|
|
948
|
+
*
|
|
949
|
+
* @throws TypeError for anything else.
|
|
950
|
+
*/
|
|
951
|
+
export function internalsOf(client: Client): ClientInternals {
|
|
952
|
+
const internals = INTERNALS.get(client)
|
|
953
|
+
if (internals === undefined) {
|
|
954
|
+
throw new TypeError("[ic-reactor] expected a client made by createClient()")
|
|
955
|
+
}
|
|
956
|
+
return internals
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
// ---------------------------------------------------------------------------
|
|
960
|
+
// Option checks
|
|
961
|
+
// ---------------------------------------------------------------------------
|
|
962
|
+
|
|
963
|
+
const AUTH_MEMBERS = [
|
|
964
|
+
"getPrincipal",
|
|
965
|
+
"getStatus",
|
|
966
|
+
"getIdentity",
|
|
967
|
+
"subscribe",
|
|
968
|
+
"signIn",
|
|
969
|
+
"signOut",
|
|
970
|
+
] as const
|
|
971
|
+
|
|
972
|
+
/** The options {@link createClient} takes. */
|
|
973
|
+
const OPTION_NAMES = [
|
|
974
|
+
"network",
|
|
975
|
+
"identity",
|
|
976
|
+
"auth",
|
|
977
|
+
"allowEnvConfig",
|
|
978
|
+
"fetch",
|
|
979
|
+
"maxDepth",
|
|
980
|
+
"maxElements",
|
|
981
|
+
]
|
|
982
|
+
|
|
983
|
+
/**
|
|
984
|
+
* Checks what a JavaScript caller (or a cast) can get past the types: no
|
|
985
|
+
* option it does not have, exactly one of `identity` and `auth`, an identity
|
|
986
|
+
* that is one, an `auth` that is a factory, a `fetch` that is a function, and
|
|
987
|
+
* a `maxDepth` and `maxElements` `@meantjs/schema` would accept.
|
|
988
|
+
* {@link resolveNetwork} checks the network and `allowEnvConfig`.
|
|
989
|
+
*/
|
|
990
|
+
function checkOptions(options: ClientOptions): void {
|
|
991
|
+
if (typeof options !== "object" || options === null) {
|
|
992
|
+
throw new TypeError(
|
|
993
|
+
"[ic-reactor] createClient() takes an options object: { network, identity } or { network, auth }."
|
|
994
|
+
)
|
|
995
|
+
}
|
|
996
|
+
checkKeys(options, OPTION_NAMES, "createClient()")
|
|
997
|
+
const { identity, auth, fetch, maxDepth, maxElements } = options as {
|
|
998
|
+
identity?: unknown
|
|
999
|
+
auth?: unknown
|
|
1000
|
+
fetch?: unknown
|
|
1001
|
+
maxDepth?: unknown
|
|
1002
|
+
maxElements?: unknown
|
|
1003
|
+
}
|
|
1004
|
+
if (fetch !== undefined && typeof fetch !== "function") {
|
|
1005
|
+
throw new TypeError(
|
|
1006
|
+
`[ic-reactor] fetch is a function, got ${typeof fetch}.`
|
|
1007
|
+
)
|
|
1008
|
+
}
|
|
1009
|
+
if (identity !== undefined && auth !== undefined) {
|
|
1010
|
+
throw new TypeError(
|
|
1011
|
+
"[ic-reactor] createClient() takes identity or auth, not both: identity fixes the caller, auth signs users in and out."
|
|
1012
|
+
)
|
|
1013
|
+
}
|
|
1014
|
+
if (identity === undefined && auth === undefined) {
|
|
1015
|
+
throw new TypeError(
|
|
1016
|
+
'[ic-reactor] createClient() needs to know who calls: identity: "anonymous" for a read-only client, ' +
|
|
1017
|
+
"identity: <Identity> for a fixed signer, or auth: (network) => new AuthClient(network) to sign users in, in a browser."
|
|
1018
|
+
)
|
|
1019
|
+
}
|
|
1020
|
+
if (auth !== undefined && typeof auth !== "function") {
|
|
1021
|
+
throw new TypeError(
|
|
1022
|
+
"[ic-reactor] auth is a factory, called only in a browser: auth: (network) => new AuthClient(network), not the AuthClient itself."
|
|
1023
|
+
)
|
|
1024
|
+
}
|
|
1025
|
+
if (
|
|
1026
|
+
identity !== undefined &&
|
|
1027
|
+
identity !== "anonymous" &&
|
|
1028
|
+
(typeof identity !== "object" ||
|
|
1029
|
+
identity === null ||
|
|
1030
|
+
typeof (identity as { getPrincipal?: unknown }).getPrincipal !==
|
|
1031
|
+
"function")
|
|
1032
|
+
) {
|
|
1033
|
+
throw new TypeError(
|
|
1034
|
+
'[ic-reactor] identity is an Identity (such as an Ed25519KeyIdentity or a new AnonymousIdentity()) or "anonymous".'
|
|
1035
|
+
)
|
|
1036
|
+
}
|
|
1037
|
+
if (
|
|
1038
|
+
maxDepth !== undefined &&
|
|
1039
|
+
!(Number.isSafeInteger(maxDepth) && (maxDepth as number) >= 0)
|
|
1040
|
+
) {
|
|
1041
|
+
throw new TypeError(
|
|
1042
|
+
`[ic-reactor] maxDepth is a non-negative safe integer, got ${String(maxDepth)}.`
|
|
1043
|
+
)
|
|
1044
|
+
}
|
|
1045
|
+
if (
|
|
1046
|
+
maxElements !== undefined &&
|
|
1047
|
+
!(Number.isSafeInteger(maxElements) && (maxElements as number) >= 0)
|
|
1048
|
+
) {
|
|
1049
|
+
throw new TypeError(
|
|
1050
|
+
`[ic-reactor] maxElements is a non-negative safe integer, got ${String(maxElements)}.`
|
|
1051
|
+
)
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
/**
|
|
1056
|
+
* Whether two `signIn()` options are the same: the same value, or objects
|
|
1057
|
+
* with the same own enumerable keys whose values are the same (`Object.is`),
|
|
1058
|
+
* as two renders' `{ returnTo: "/account" }` are.
|
|
1059
|
+
*/
|
|
1060
|
+
function sameOptions(a: unknown, b: unknown): boolean {
|
|
1061
|
+
if (Object.is(a, b)) return true
|
|
1062
|
+
if (typeof a !== "object" || typeof b !== "object" || !a || !b) return false
|
|
1063
|
+
const first = a as Record<string, unknown>
|
|
1064
|
+
const second = b as Record<string, unknown>
|
|
1065
|
+
const keys = Object.keys(first)
|
|
1066
|
+
return (
|
|
1067
|
+
keys.length === Object.keys(second).length &&
|
|
1068
|
+
keys.every(
|
|
1069
|
+
(key) =>
|
|
1070
|
+
Object.prototype.hasOwnProperty.call(second, key) &&
|
|
1071
|
+
Object.is(first[key], second[key])
|
|
1072
|
+
)
|
|
1073
|
+
)
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
/** Checks that an auth factory returned an {@link AuthLike}, naming what it lacks. */
|
|
1077
|
+
function checkAuth(auth: unknown): AuthLike {
|
|
1078
|
+
const missing =
|
|
1079
|
+
typeof auth === "object" && auth !== null
|
|
1080
|
+
? AUTH_MEMBERS.filter(
|
|
1081
|
+
(name) =>
|
|
1082
|
+
typeof (auth as Record<string, unknown>)[name] !== "function"
|
|
1083
|
+
)
|
|
1084
|
+
: [...AUTH_MEMBERS]
|
|
1085
|
+
if (missing.length > 0) {
|
|
1086
|
+
throw new TypeError(
|
|
1087
|
+
`[ic-reactor] the auth factory returned something that is not an AuthLike: it has no ${missing
|
|
1088
|
+
.map((name) => `${name}()`)
|
|
1089
|
+
.join(", ")}.`
|
|
1090
|
+
)
|
|
1091
|
+
}
|
|
1092
|
+
return auth as AuthLike
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
/**
|
|
1096
|
+
* Checks that `host`, read as `HttpAgent` reads it ({@link agentUrlOf}: a
|
|
1097
|
+
* host without a scheme is read against the page's protocol), is an `http:`
|
|
1098
|
+
* or `https:` URL, so a bad host is a `TypeError` from `createClient` rather
|
|
1099
|
+
* than a failure of every later call. `localhost:4943` reads as the scheme
|
|
1100
|
+
* `localhost:`. A host `"env"` chose came from the page's origin or from
|
|
1101
|
+
* `ICP_HOST` or `IC_HOST`, not from the caller, and the message says so.
|
|
1102
|
+
*/
|
|
1103
|
+
function checkHost(host: string, fromEnv: boolean): void {
|
|
1104
|
+
const url = agentUrlOf(host)
|
|
1105
|
+
if (url?.protocol === "http:" || url?.protocol === "https:") return
|
|
1106
|
+
throw new TypeError(
|
|
1107
|
+
`[ic-reactor] the network host ${JSON.stringify(host)}${
|
|
1108
|
+
fromEnv
|
|
1109
|
+
? ', which network "env" took from the page\'s origin or from ICP_HOST or IC_HOST,'
|
|
1110
|
+
: ""
|
|
1111
|
+
} is not a URL the agent can read (${
|
|
1112
|
+
url === undefined
|
|
1113
|
+
? "it does not parse as one"
|
|
1114
|
+
: `its scheme is ${url.protocol}, and the agent calls over http: and https: only`
|
|
1115
|
+
}). Give one with a scheme, such as "https://icp-api.io"${
|
|
1116
|
+
fromEnv ? ", in ICP_HOST or as network: { host }" : ""
|
|
1117
|
+
}.`
|
|
1118
|
+
)
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
let warnedEnvOffPage = false
|
|
1122
|
+
|
|
1123
|
+
/**
|
|
1124
|
+
* `network: "env"` outside a browser page works for `{ id }` targets only:
|
|
1125
|
+
* there is no `ic_env` cookie, so a `{ name }` target never resolves. Said
|
|
1126
|
+
* once per process, in development, not once per client: a server builds a
|
|
1127
|
+
* client per request.
|
|
1128
|
+
*/
|
|
1129
|
+
function warnEnvOffPage(network: Network): void {
|
|
1130
|
+
if (
|
|
1131
|
+
network !== "env" ||
|
|
1132
|
+
!isServer() ||
|
|
1133
|
+
warnedEnvOffPage ||
|
|
1134
|
+
!isDevelopment()
|
|
1135
|
+
) {
|
|
1136
|
+
return
|
|
1137
|
+
}
|
|
1138
|
+
warnedEnvOffPage = true
|
|
1139
|
+
console.warn(
|
|
1140
|
+
'[ic-reactor] createClient({ network: "env" }) outside a browser page: there is no ic_env cookie here, ' +
|
|
1141
|
+
"so every call to a { name } canister rejects invalid_args (canister_id_unresolved). " +
|
|
1142
|
+
"Call canisters by { id } from a server render, a route handler or a script. (Logged once, in development.)"
|
|
1143
|
+
)
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
// ---------------------------------------------------------------------------
|
|
1147
|
+
// What @ic-reactor/react reads of a client
|
|
1148
|
+
// ---------------------------------------------------------------------------
|
|
1149
|
+
|
|
1150
|
+
/*
|
|
1151
|
+
* An internal contract between this module and `@ic-reactor/react`'s
|
|
1152
|
+
* `ReactorProvider`, not public API: no type names these keys and no entry
|
|
1153
|
+
* exports them. Changing a key or what it means breaks the provider.
|
|
1154
|
+
*
|
|
1155
|
+
* - `ic-reactor.clients.created`, on `globalThis`: how many clients
|
|
1156
|
+
* `createClient` has made in this realm, the client of each
|
|
1157
|
+
* `createTestClient()` among them: both are built by
|
|
1158
|
+
* {@link createClientWith}, which stamps every client it builds.
|
|
1159
|
+
* - `ic-reactor.client.serial`, on each client: that count once the client
|
|
1160
|
+
* was made, so the first client's is 1.
|
|
1161
|
+
* - `ic-reactor.client.disposed`, on each client: a getter, `true` once
|
|
1162
|
+
* `dispose()` has run.
|
|
1163
|
+
* - `ic-reactor.client.as`, on each client: a method that `useClient()`
|
|
1164
|
+
* calls with the client as `this` and the principal (text) a render
|
|
1165
|
+
* shows: the live caller's, or the server's anonymous one while a page
|
|
1166
|
+
* hydrates in a browser that holds a session. It returns a frozen view of
|
|
1167
|
+
* `this` for that principal (see `viewAs` in {@link createClientWith}),
|
|
1168
|
+
* the same one for the same principal while it is kept, or `this` itself
|
|
1169
|
+
* for a client built with `identity`, whose caller never changes.
|
|
1170
|
+
* - `ic-reactor.client.of`, on each view: the object the view was made over.
|
|
1171
|
+
* The provider holds that object, never a view, when its factory returns
|
|
1172
|
+
* one (a component's `useClient()` handed to a nested provider): a view
|
|
1173
|
+
* keeps its principal for good, so a tree on it would never follow the
|
|
1174
|
+
* caller.
|
|
1175
|
+
*
|
|
1176
|
+
* The provider reads the count, calls its `client` factory, and compares the
|
|
1177
|
+
* serial of the client it gets: a higher serial means the factory created the
|
|
1178
|
+
* client, and the provider disposes it when it unmounts; a client made before
|
|
1179
|
+
* (one at module scope) is borrowed, and the provider never disposes it. The
|
|
1180
|
+
* disposal flag lets it say so loudly when it is handed a borrowed client that
|
|
1181
|
+
* its owner already disposed.
|
|
1182
|
+
*
|
|
1183
|
+
* `Symbol.for` keys and a count kept on `globalThis`, so that two copies of
|
|
1184
|
+
* this package in one bundle count together, and the provider reads them
|
|
1185
|
+
* without importing anything of this package at run time. The keys on a
|
|
1186
|
+
* client or a view are non-enumerable, so spreading or logging one does not
|
|
1187
|
+
* show them.
|
|
1188
|
+
*/
|
|
1189
|
+
const CLIENTS_CREATED = Symbol.for("ic-reactor.clients.created")
|
|
1190
|
+
const CLIENT_SERIAL = Symbol.for("ic-reactor.client.serial")
|
|
1191
|
+
const CLIENT_DISPOSED = Symbol.for("ic-reactor.client.disposed")
|
|
1192
|
+
const CLIENT_AS = Symbol.for("ic-reactor.client.as")
|
|
1193
|
+
const CLIENT_OF = Symbol.for("ic-reactor.client.of")
|
|
1194
|
+
|
|
1195
|
+
/** Counts one more client in this realm, and returns the count. */
|
|
1196
|
+
function nextSerial(): number {
|
|
1197
|
+
const count = (globalThis as { [CLIENTS_CREATED]?: unknown })[CLIENTS_CREATED]
|
|
1198
|
+
const serial = (typeof count === "number" ? count : 0) + 1
|
|
1199
|
+
try {
|
|
1200
|
+
Object.defineProperty(globalThis, CLIENTS_CREATED, {
|
|
1201
|
+
value: serial,
|
|
1202
|
+
writable: true,
|
|
1203
|
+
configurable: true,
|
|
1204
|
+
})
|
|
1205
|
+
} catch {
|
|
1206
|
+
// A frozen global (an SES lockdown) keeps no count. The provider then
|
|
1207
|
+
// reads 0 before every factory call and owns every client it is given.
|
|
1208
|
+
}
|
|
1209
|
+
return serial
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
// ---------------------------------------------------------------------------
|
|
1213
|
+
// createClient
|
|
1214
|
+
// ---------------------------------------------------------------------------
|
|
1215
|
+
|
|
1216
|
+
const ANONYMOUS_STATE: AuthState = Object.freeze({
|
|
1217
|
+
status: "anonymous",
|
|
1218
|
+
principal: ANONYMOUS,
|
|
1219
|
+
})
|
|
1220
|
+
|
|
1221
|
+
const ANONYMOUS_CALLER: Caller = Object.freeze({
|
|
1222
|
+
principal: ANONYMOUS,
|
|
1223
|
+
authenticated: false,
|
|
1224
|
+
})
|
|
1225
|
+
|
|
1226
|
+
const sameState = (a: AuthState, b: AuthState): boolean =>
|
|
1227
|
+
a.status === b.status &&
|
|
1228
|
+
a.principal === b.principal &&
|
|
1229
|
+
a.expiresAt === b.expiresAt
|
|
1230
|
+
|
|
1231
|
+
/**
|
|
1232
|
+
* The longest delay `setTimeout` keeps: a longer one fires at once. A later
|
|
1233
|
+
* deadline is reached in steps of this length.
|
|
1234
|
+
*/
|
|
1235
|
+
const MAX_TIMER_MS = 2 ** 31 - 1
|
|
1236
|
+
|
|
1237
|
+
/**
|
|
1238
|
+
* Who `auth` says calls: its principal only while its status is `signed-in`
|
|
1239
|
+
* and that principal is not the anonymous one. Every other state, including
|
|
1240
|
+
* one this version does not know, is anonymous.
|
|
1241
|
+
*/
|
|
1242
|
+
function callerOf(auth: AuthLike): Caller {
|
|
1243
|
+
if (auth.getStatus().state !== "signed-in") return ANONYMOUS_CALLER
|
|
1244
|
+
const principal = auth.getPrincipal()?.toText()
|
|
1245
|
+
return principal === undefined || principal === ANONYMOUS
|
|
1246
|
+
? ANONYMOUS_CALLER
|
|
1247
|
+
: { principal, authenticated: true }
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
/** The {@link AuthState} `auth` stands for. See {@link AuthState} for the rules. */
|
|
1251
|
+
function stateOf(auth: AuthLike): AuthState {
|
|
1252
|
+
const caller = callerOf(auth)
|
|
1253
|
+
if (caller.authenticated) {
|
|
1254
|
+
const { expiresAtMs } = auth.getStatus()
|
|
1255
|
+
return Number.isFinite(expiresAtMs)
|
|
1256
|
+
? {
|
|
1257
|
+
status: "signed-in",
|
|
1258
|
+
principal: caller.principal,
|
|
1259
|
+
expiresAt: expiresAtMs,
|
|
1260
|
+
}
|
|
1261
|
+
: { status: "signed-in", principal: caller.principal }
|
|
1262
|
+
}
|
|
1263
|
+
// A state the client does not know, such as one a later @icp-sdk/auth
|
|
1264
|
+
// adds, is anonymous.
|
|
1265
|
+
const { state } = auth.getStatus()
|
|
1266
|
+
return state === "expired" || state === "signed-in-elsewhere"
|
|
1267
|
+
? { status: state as AuthState["status"], principal: ANONYMOUS }
|
|
1268
|
+
: ANONYMOUS_STATE
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
/**
|
|
1272
|
+
* Creates a client: who calls, on which network, with which cache.
|
|
1273
|
+
*
|
|
1274
|
+
* Say who calls with exactly one of `identity` and `auth`. `auth` takes a
|
|
1275
|
+
* factory, which the client calls on first use and only in a browser, so the
|
|
1276
|
+
* same `createClient(...)` line runs in a server render (anonymous, no auth
|
|
1277
|
+
* built) and in the browser (signed in).
|
|
1278
|
+
*
|
|
1279
|
+
* Build one client per browser tab, or one per request on a server: a client's
|
|
1280
|
+
* cache and agents belong to whoever it calls as, and two clients share
|
|
1281
|
+
* neither.
|
|
1282
|
+
*
|
|
1283
|
+
* @throws TypeError for options that do not say who calls, say it twice, or
|
|
1284
|
+
* name a network or `maxDepth` that cannot be used; for an option it does not
|
|
1285
|
+
* have (in the options or in a network object); and for an `allowEnvConfig`
|
|
1286
|
+
* or `fetchRootKey` that is not a boolean, a `rootKey` that is not 133 bytes
|
|
1287
|
+
* in a `Uint8Array`, or a `fetch` that is not a function.
|
|
1288
|
+
*
|
|
1289
|
+
* @example
|
|
1290
|
+
* ```ts
|
|
1291
|
+
* import { AuthClient } from "@icp-sdk/auth/client"
|
|
1292
|
+
* import { createClient } from "@ic-reactor/core"
|
|
1293
|
+
*
|
|
1294
|
+
* const client = createClient({ network: "ic", auth: (network) => new AuthClient(network) })
|
|
1295
|
+
* client.subscribe(() => console.log(client.authState()))
|
|
1296
|
+
* await client.signIn()
|
|
1297
|
+
* ```
|
|
1298
|
+
*/
|
|
1299
|
+
export function createClient(options: ClientOptions): Client {
|
|
1300
|
+
return createClientWith(options, {})
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
/**
|
|
1304
|
+
* What the test client (`@ic-reactor/core/testing`) changes about how a client
|
|
1305
|
+
* runs, which no app may. Not part of {@link ClientOptions}, which is public,
|
|
1306
|
+
* and not exported from the package entry: {@link createClientWith} is the
|
|
1307
|
+
* only way in.
|
|
1308
|
+
*/
|
|
1309
|
+
export interface ClientSeams {
|
|
1310
|
+
/**
|
|
1311
|
+
* Builds the auth factory on a server too. {@link createClient} builds it
|
|
1312
|
+
* only in a browser, so that one `createClient({ auth })` line is anonymous
|
|
1313
|
+
* in a server render and signed in in the browser. A test client lives in
|
|
1314
|
+
* Node, where a test signs users in and out and expects the calls to follow:
|
|
1315
|
+
* with the auth never built there, `auth.switchTo(2)` would change nothing a
|
|
1316
|
+
* call can see.
|
|
1317
|
+
*/
|
|
1318
|
+
readonly authOnServer?: boolean
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
/**
|
|
1322
|
+
* {@link createClient} with the {@link ClientSeams} a test client needs.
|
|
1323
|
+
* Internal: not exported from the package entry.
|
|
1324
|
+
*/
|
|
1325
|
+
export function createClientWith(
|
|
1326
|
+
options: ClientOptions,
|
|
1327
|
+
seams: ClientSeams
|
|
1328
|
+
): Client {
|
|
1329
|
+
checkOptions(options)
|
|
1330
|
+
const network = resolveNetwork(options.network, {
|
|
1331
|
+
allowEnvConfig: options.allowEnvConfig,
|
|
1332
|
+
})
|
|
1333
|
+
checkHost(network.host, options.network === "env")
|
|
1334
|
+
warnEnvOffPage(options.network)
|
|
1335
|
+
const maxDepth = options.maxDepth ?? DEFAULT_MAX_DEPTH
|
|
1336
|
+
const maxElements = options.maxElements ?? DEFAULT_MAX_ELEMENTS
|
|
1337
|
+
const authFactory = options.auth
|
|
1338
|
+
const fixedIdentity =
|
|
1339
|
+
options.identity === undefined || options.identity === "anonymous"
|
|
1340
|
+
? undefined
|
|
1341
|
+
: options.identity
|
|
1342
|
+
|
|
1343
|
+
// A user's `fetch` is called as a plain function. The agent would call it as
|
|
1344
|
+
// its own method, and a browser's native `fetch` throws "Illegal invocation"
|
|
1345
|
+
// when called on anything but `window`.
|
|
1346
|
+
const userFetch = options.fetch
|
|
1347
|
+
const agentFetch: typeof globalThis.fetch | undefined =
|
|
1348
|
+
userFetch === undefined
|
|
1349
|
+
? undefined
|
|
1350
|
+
: (input: RequestInfo | URL, init?: RequestInit) => userFetch(input, init)
|
|
1351
|
+
|
|
1352
|
+
const mutationCallers = new WeakMap<object, Caller>()
|
|
1353
|
+
const ownMutationFns = new WeakSet<object>()
|
|
1354
|
+
const queryClient = new QueryClient({
|
|
1355
|
+
mutationCache: new MutationCache({
|
|
1356
|
+
// Only for a write of this client's `mutationOptions`: a mutation of
|
|
1357
|
+
// the app's never builds the auth, and never fails here. Before 5.89
|
|
1358
|
+
// TanStack passes no context.
|
|
1359
|
+
onMutate: (
|
|
1360
|
+
_vars: unknown,
|
|
1361
|
+
mutation: { readonly options: { readonly mutationFn?: unknown } },
|
|
1362
|
+
context?: unknown
|
|
1363
|
+
) => {
|
|
1364
|
+
const fn = mutation.options.mutationFn
|
|
1365
|
+
if (typeof fn !== "function" || !ownMutationFns.has(fn)) return
|
|
1366
|
+
let caller: Caller
|
|
1367
|
+
try {
|
|
1368
|
+
caller = current()
|
|
1369
|
+
} catch {
|
|
1370
|
+
// The write's own mutationFn reads the caller again, and fails
|
|
1371
|
+
// as a call does.
|
|
1372
|
+
return
|
|
1373
|
+
}
|
|
1374
|
+
mutationCallers.set(mutation, caller)
|
|
1375
|
+
if (typeof context === "object" && context !== null) {
|
|
1376
|
+
mutationCallers.set(context, caller)
|
|
1377
|
+
}
|
|
1378
|
+
},
|
|
1379
|
+
}),
|
|
1380
|
+
defaultOptions: {
|
|
1381
|
+
queries: { retry: retryDefault },
|
|
1382
|
+
dehydrate: { serializeData },
|
|
1383
|
+
hydrate: { deserializeData },
|
|
1384
|
+
},
|
|
1385
|
+
})
|
|
1386
|
+
|
|
1387
|
+
/** Signs the anonymous principal's calls in `auth` mode and for `identity: "anonymous"`. */
|
|
1388
|
+
const anonymousIdentity = new AnonymousIdentity()
|
|
1389
|
+
|
|
1390
|
+
/** Who calls in `identity` mode; `undefined` in `auth` mode. */
|
|
1391
|
+
const fixedCaller: Caller | undefined =
|
|
1392
|
+
options.identity === undefined
|
|
1393
|
+
? undefined
|
|
1394
|
+
: fixedIdentity === undefined
|
|
1395
|
+
? ANONYMOUS_CALLER
|
|
1396
|
+
: Object.freeze({
|
|
1397
|
+
principal: fixedIdentity.getPrincipal().toText(),
|
|
1398
|
+
authenticated: true,
|
|
1399
|
+
})
|
|
1400
|
+
|
|
1401
|
+
/** The {@link AuthState} in `identity` mode; `undefined` in `auth` mode. */
|
|
1402
|
+
const fixedState: AuthState | undefined =
|
|
1403
|
+
fixedCaller === undefined
|
|
1404
|
+
? undefined
|
|
1405
|
+
: fixedCaller.principal === ANONYMOUS
|
|
1406
|
+
? ANONYMOUS_STATE
|
|
1407
|
+
: Object.freeze({
|
|
1408
|
+
status: "signed-in",
|
|
1409
|
+
principal: fixedCaller.principal,
|
|
1410
|
+
})
|
|
1411
|
+
|
|
1412
|
+
/**
|
|
1413
|
+
* One agent per principal, with the identity it signs with and the number of
|
|
1414
|
+
* the identity request that produced it (see `agentFor`).
|
|
1415
|
+
*/
|
|
1416
|
+
const agents = new Map<
|
|
1417
|
+
string,
|
|
1418
|
+
{ identity: Identity; agent: HttpAgent; asked: number }
|
|
1419
|
+
>()
|
|
1420
|
+
let identityRequests = 0
|
|
1421
|
+
const listeners = new Set<() => void>()
|
|
1422
|
+
let auth: AuthLike | undefined
|
|
1423
|
+
let unsubscribeAuth: (() => void) | undefined
|
|
1424
|
+
/**
|
|
1425
|
+
* What the auth factory threw, once it has, or the `TypeError` for a result
|
|
1426
|
+
* that is not an {@link AuthLike}: the client then runs anonymous, never calls
|
|
1427
|
+
* the factory again, and rejects `signIn()` and `signOut()` with it.
|
|
1428
|
+
*/
|
|
1429
|
+
let authFailure: { readonly cause: unknown } | undefined
|
|
1430
|
+
/**
|
|
1431
|
+
* The `signIn()` calls started in the current synchronous run, with their
|
|
1432
|
+
* options, until a microtask queued by the run's first call clears them. A
|
|
1433
|
+
* later call with the same options as one of them returns its promise
|
|
1434
|
+
* instead of signing in again: React's `StrictMode` runs an effect twice in
|
|
1435
|
+
* one run, and a redirect sign-in (ICRC-167) that starts twice fails on the
|
|
1436
|
+
* load that comes back.
|
|
1437
|
+
*/
|
|
1438
|
+
let signInsThisRun: Array<{
|
|
1439
|
+
readonly options: unknown
|
|
1440
|
+
readonly done: Promise<void>
|
|
1441
|
+
}> = []
|
|
1442
|
+
/** The timer set for the end of the session (see `scheduleExpiry`). */
|
|
1443
|
+
let expiryTimer: ReturnType<typeof setTimeout> | undefined
|
|
1444
|
+
/** Whether a `catchUp` notification is queued. */
|
|
1445
|
+
let catchUpQueued = false
|
|
1446
|
+
/** Whether this client said that its network has no identity provider. */
|
|
1447
|
+
let warnedAuthNetwork = false
|
|
1448
|
+
let disposed = false
|
|
1449
|
+
/** What {@link Client.authState} returns: replaced only when the state changes. */
|
|
1450
|
+
let snapshot: AuthState = fixedState ?? ANONYMOUS_STATE
|
|
1451
|
+
/** The state listeners were last told about, or the one they started with. */
|
|
1452
|
+
let notified: AuthState = snapshot
|
|
1453
|
+
|
|
1454
|
+
/**
|
|
1455
|
+
* The errors of reads a view built that were refused while someone else
|
|
1456
|
+
* was current, and left on a query with no data (see `createBuilders`).
|
|
1457
|
+
*/
|
|
1458
|
+
const refusals = new WeakSet<object>()
|
|
1459
|
+
|
|
1460
|
+
/**
|
|
1461
|
+
* Clears the refusals of `principal`'s reads, now that it is current, from
|
|
1462
|
+
* the queries that still hold one and nothing else, before any listener
|
|
1463
|
+
* renders them: such a query reads as never fetched, so an observer fetches
|
|
1464
|
+
* it, where a suspense read would otherwise throw the refusal (TanStack does
|
|
1465
|
+
* not refetch an errored query on mount for one).
|
|
1466
|
+
*/
|
|
1467
|
+
function clearRefusals(principal: string): void {
|
|
1468
|
+
const queries = queryClient
|
|
1469
|
+
.getQueryCache()
|
|
1470
|
+
.findAll({ queryKey: [KEY_ROOT, network.keySegment, principal] })
|
|
1471
|
+
for (const query of queries) {
|
|
1472
|
+
const { error, data, fetchStatus } = query.state
|
|
1473
|
+
if (
|
|
1474
|
+
error !== null &&
|
|
1475
|
+
refusals.has(error) &&
|
|
1476
|
+
data === undefined &&
|
|
1477
|
+
fetchStatus === "idle"
|
|
1478
|
+
) {
|
|
1479
|
+
query.setState({ ...query.state, status: "pending", error: null })
|
|
1480
|
+
}
|
|
1481
|
+
}
|
|
1482
|
+
}
|
|
1483
|
+
|
|
1484
|
+
/**
|
|
1485
|
+
* Says, once per client and in development only, that sign-in cannot work
|
|
1486
|
+
* on this network: an auth built without an identity provider signs in with
|
|
1487
|
+
* mainnet's Internet Identity, whose delegations this replica rejects.
|
|
1488
|
+
*/
|
|
1489
|
+
const warnNoProvider = (gap: string): void => {
|
|
1490
|
+
if (warnedAuthNetwork || !isDevelopment()) return
|
|
1491
|
+
warnedAuthNetwork = true
|
|
1492
|
+
console.warn(
|
|
1493
|
+
`[ic-reactor] sign-in cannot work on this network (${network.host}) without an identity provider: ${gap}. ` +
|
|
1494
|
+
"The auth factory was given no identityProvider, so an AuthClient built from it signs in with mainnet's " +
|
|
1495
|
+
"Internet Identity, whose delegations this replica rejects. Name the provider: " +
|
|
1496
|
+
"(network) => new AuthClient({ ...network, identityProvider: { authorizeUrl, canisterId } }). " +
|
|
1497
|
+
"(Logged once, in development.)"
|
|
1498
|
+
)
|
|
1499
|
+
}
|
|
1500
|
+
|
|
1501
|
+
/**
|
|
1502
|
+
* What the auth factory is handed (see `authNetworkFor`), with the client's
|
|
1503
|
+
* `fetch` in its `agentOptions` when one was given, so an `AuthClient`'s own
|
|
1504
|
+
* agents send as the client's do.
|
|
1505
|
+
*
|
|
1506
|
+
* Where no identity provider could be named, `identityProvider` is a getter
|
|
1507
|
+
* that is not enumerable: it returns `undefined`, and its first read warns.
|
|
1508
|
+
* So `new AuthClient(network)`, which reads it, warns; a factory that names
|
|
1509
|
+
* its own provider with `{ ...network, identityProvider }` never reads it,
|
|
1510
|
+
* since a spread copies only enumerable properties; and a factory that
|
|
1511
|
+
* ignores `network`, for an auth that is not Internet Identity, never does.
|
|
1512
|
+
*/
|
|
1513
|
+
const authNetwork = (): AuthNetworkArgument => {
|
|
1514
|
+
const resolution = authNetworkFor(options.network, network)
|
|
1515
|
+
const agentOptions =
|
|
1516
|
+
agentFetch === undefined
|
|
1517
|
+
? resolution.network.agentOptions
|
|
1518
|
+
: { ...resolution.network.agentOptions, fetch: agentFetch }
|
|
1519
|
+
const { gap } = resolution
|
|
1520
|
+
if (gap === undefined) return { ...resolution.network, agentOptions }
|
|
1521
|
+
const handed = { agentOptions }
|
|
1522
|
+
Object.defineProperty(handed, "identityProvider", {
|
|
1523
|
+
get: (): undefined => {
|
|
1524
|
+
warnNoProvider(gap)
|
|
1525
|
+
return undefined
|
|
1526
|
+
},
|
|
1527
|
+
enumerable: false,
|
|
1528
|
+
})
|
|
1529
|
+
return handed
|
|
1530
|
+
}
|
|
1531
|
+
|
|
1532
|
+
/**
|
|
1533
|
+
* The auth, built on first use: never in `identity` mode, never on a server
|
|
1534
|
+
* (unless the test client's seam says so), never after
|
|
1535
|
+
* {@link Client.dispose}.
|
|
1536
|
+
*/
|
|
1537
|
+
const ensureAuth = (): AuthLike | undefined => {
|
|
1538
|
+
if (
|
|
1539
|
+
auth !== undefined ||
|
|
1540
|
+
authFailure !== undefined ||
|
|
1541
|
+
authFactory === undefined ||
|
|
1542
|
+
disposed ||
|
|
1543
|
+
(seams.authOnServer !== true && isServer())
|
|
1544
|
+
) {
|
|
1545
|
+
return auth
|
|
1546
|
+
}
|
|
1547
|
+
let made: unknown
|
|
1548
|
+
let built: AuthLike
|
|
1549
|
+
try {
|
|
1550
|
+
made = authFactory(authNetwork())
|
|
1551
|
+
// A factory that returns no AuthLike (a Promise, from an `async`
|
|
1552
|
+
// factory) fails here too, and is not called again.
|
|
1553
|
+
built = checkAuth(made)
|
|
1554
|
+
} catch (cause) {
|
|
1555
|
+
// `new AuthClient()` reads `localStorage`, which throws where site data
|
|
1556
|
+
// is blocked or in a sandboxed frame. That must not take the public
|
|
1557
|
+
// reads down with it, nor run again on every render.
|
|
1558
|
+
authFailure = { cause }
|
|
1559
|
+
if (isDevelopment()) {
|
|
1560
|
+
console.error(
|
|
1561
|
+
"[ic-reactor] the auth factory threw: this client calls anonymously, and signIn() rejects with this error. (Logged once, in development.)",
|
|
1562
|
+
cause
|
|
1563
|
+
)
|
|
1564
|
+
}
|
|
1565
|
+
return undefined
|
|
1566
|
+
}
|
|
1567
|
+
auth = built
|
|
1568
|
+
const initial = stateOf(built)
|
|
1569
|
+
if (!sameState(initial, snapshot)) snapshot = Object.freeze(initial)
|
|
1570
|
+
notified = snapshot
|
|
1571
|
+
unsubscribeAuth = built.subscribe(onAuthChange)
|
|
1572
|
+
scheduleExpiry()
|
|
1573
|
+
return built
|
|
1574
|
+
}
|
|
1575
|
+
|
|
1576
|
+
/**
|
|
1577
|
+
* Sets one timer for the end of the auth's session, replacing the last one:
|
|
1578
|
+
* `AuthClient`'s status turns `expired` by the clock, with no notification,
|
|
1579
|
+
* so without it nobody would be told. A deadline that has passed while the
|
|
1580
|
+
* auth still says `signed-in` sets none: that auth does not expire by the
|
|
1581
|
+
* clock, and a timer would only fire again and again.
|
|
1582
|
+
*/
|
|
1583
|
+
function scheduleExpiry(): void {
|
|
1584
|
+
clearTimeout(expiryTimer)
|
|
1585
|
+
expiryTimer = undefined
|
|
1586
|
+
if (disposed || auth === undefined) return
|
|
1587
|
+
const { state, expiresAtMs } = auth.getStatus()
|
|
1588
|
+
if (state !== "signed-in" && state !== "signed-in-elsewhere") return
|
|
1589
|
+
if (expiresAtMs === undefined || !Number.isFinite(expiresAtMs)) return
|
|
1590
|
+
const delay = expiresAtMs - Date.now()
|
|
1591
|
+
if (delay <= 0) return
|
|
1592
|
+
expiryTimer = setTimeout(
|
|
1593
|
+
() => {
|
|
1594
|
+
expiryTimer = undefined
|
|
1595
|
+
// A capped delay ends early: `onAuthChange` sets the next step.
|
|
1596
|
+
onAuthChange()
|
|
1597
|
+
},
|
|
1598
|
+
Math.min(delay, MAX_TIMER_MS)
|
|
1599
|
+
)
|
|
1600
|
+
// A session's end is no reason to keep a Node process (a test, a script
|
|
1601
|
+
// with the test client) alive.
|
|
1602
|
+
;(expiryTimer as { unref?: () => void }).unref?.()
|
|
1603
|
+
}
|
|
1604
|
+
|
|
1605
|
+
/**
|
|
1606
|
+
* Tells the listeners, in a microtask, about a change a read noticed before
|
|
1607
|
+
* any notification did (a session that ended while the timer was held back,
|
|
1608
|
+
* as a browser does in a background tab). Queued rather than run, since the
|
|
1609
|
+
* read may be a render's.
|
|
1610
|
+
*/
|
|
1611
|
+
const catchUp = (): void => {
|
|
1612
|
+
if (catchUpQueued || disposed) return
|
|
1613
|
+
catchUpQueued = true
|
|
1614
|
+
queueMicrotask(() => {
|
|
1615
|
+
catchUpQueued = false
|
|
1616
|
+
onAuthChange()
|
|
1617
|
+
})
|
|
1618
|
+
}
|
|
1619
|
+
|
|
1620
|
+
const current = (): Caller => {
|
|
1621
|
+
if (disposed) return ANONYMOUS_CALLER
|
|
1622
|
+
if (fixedCaller !== undefined) return fixedCaller
|
|
1623
|
+
const source = ensureAuth()
|
|
1624
|
+
if (source === undefined) return ANONYMOUS_CALLER
|
|
1625
|
+
const caller = callerOf(source)
|
|
1626
|
+
if (caller.principal !== notified.principal) catchUp()
|
|
1627
|
+
return caller
|
|
1628
|
+
}
|
|
1629
|
+
|
|
1630
|
+
/** Re-reads the state, replacing {@link snapshot} only when it changed. */
|
|
1631
|
+
const refresh = (): AuthState => {
|
|
1632
|
+
if (disposed || fixedState !== undefined) return snapshot
|
|
1633
|
+
const source = ensureAuth()
|
|
1634
|
+
const next = source === undefined ? ANONYMOUS_STATE : stateOf(source)
|
|
1635
|
+
if (!sameState(next, snapshot)) snapshot = Object.freeze(next)
|
|
1636
|
+
return snapshot
|
|
1637
|
+
}
|
|
1638
|
+
|
|
1639
|
+
/** Runs after each notification of the auth, and after a sign-in or sign-out. */
|
|
1640
|
+
function onAuthChange(): void {
|
|
1641
|
+
if (disposed) return
|
|
1642
|
+
const state = refresh()
|
|
1643
|
+
// An agent signs for one principal. Those that are no longer current are
|
|
1644
|
+
// dropped: a call made for them is cancelled before it needs one.
|
|
1645
|
+
for (const principal of agents.keys()) {
|
|
1646
|
+
if (principal !== state.principal) agents.delete(principal)
|
|
1647
|
+
}
|
|
1648
|
+
scheduleExpiry()
|
|
1649
|
+
if (state === notified) return
|
|
1650
|
+
const changed = state.principal !== notified.principal
|
|
1651
|
+
notified = state
|
|
1652
|
+
// A sign-out, an expired session or a switch of account removes the
|
|
1653
|
+
// reads made as anyone but the new caller and the anonymous one, before
|
|
1654
|
+
// a listener renders: they stay out of the next person's reach in a
|
|
1655
|
+
// shared browser, and out of a persister that saves the cache.
|
|
1656
|
+
if (changed) {
|
|
1657
|
+
queryClient.removeQueries({
|
|
1658
|
+
queryKey: [KEY_ROOT, network.keySegment],
|
|
1659
|
+
predicate: ({ queryKey: [, , caller] }) =>
|
|
1660
|
+
caller !== state.principal && caller !== ANONYMOUS,
|
|
1661
|
+
})
|
|
1662
|
+
}
|
|
1663
|
+
clearRefusals(state.principal)
|
|
1664
|
+
for (const listener of [...listeners]) {
|
|
1665
|
+
// A listener may dispose the client; the rest are then not told.
|
|
1666
|
+
if (disposed) return
|
|
1667
|
+
try {
|
|
1668
|
+
listener()
|
|
1669
|
+
} catch (error) {
|
|
1670
|
+
// One listener's bug must not keep the others from hearing it.
|
|
1671
|
+
queueMicrotask(() => {
|
|
1672
|
+
throw error
|
|
1673
|
+
})
|
|
1674
|
+
}
|
|
1675
|
+
}
|
|
1676
|
+
}
|
|
1677
|
+
|
|
1678
|
+
const identityFor = (principal: string): Promise<Identity> => {
|
|
1679
|
+
if (fixedIdentity !== undefined) return Promise.resolve(fixedIdentity)
|
|
1680
|
+
if (principal === ANONYMOUS) return Promise.resolve(anonymousIdentity)
|
|
1681
|
+
// `current()` named this principal, so the auth exists and is signed in.
|
|
1682
|
+
return (auth as AuthLike).getIdentity()
|
|
1683
|
+
}
|
|
1684
|
+
|
|
1685
|
+
const agentFor = async (
|
|
1686
|
+
principal: string,
|
|
1687
|
+
context: AgentContext
|
|
1688
|
+
): Promise<HttpAgent> => {
|
|
1689
|
+
const where = { method: context.method, canisterId: context.canisterId }
|
|
1690
|
+
const assertCurrent = (): void => {
|
|
1691
|
+
if (disposed) {
|
|
1692
|
+
throw createReactorError("cancelled", {
|
|
1693
|
+
...where,
|
|
1694
|
+
code: "client_disposed",
|
|
1695
|
+
reason: "the client was disposed before the call was sent",
|
|
1696
|
+
})
|
|
1697
|
+
}
|
|
1698
|
+
const now = current().principal
|
|
1699
|
+
if (now !== principal) {
|
|
1700
|
+
throw createReactorError("cancelled", {
|
|
1701
|
+
...where,
|
|
1702
|
+
code: "caller_changed",
|
|
1703
|
+
reason: `the call was made as ${principal}, but the caller is now ${now}; it was not sent`,
|
|
1704
|
+
})
|
|
1705
|
+
}
|
|
1706
|
+
}
|
|
1707
|
+
|
|
1708
|
+
assertCurrent()
|
|
1709
|
+
const asked = ++identityRequests
|
|
1710
|
+
let identity: Identity
|
|
1711
|
+
try {
|
|
1712
|
+
identity = await identityFor(principal)
|
|
1713
|
+
} catch (cause) {
|
|
1714
|
+
assertCurrent()
|
|
1715
|
+
throw createReactorError("unauthenticated", {
|
|
1716
|
+
...where,
|
|
1717
|
+
code: "identity_unavailable",
|
|
1718
|
+
reason: `the auth could not provide the identity of ${principal}`,
|
|
1719
|
+
cause,
|
|
1720
|
+
})
|
|
1721
|
+
}
|
|
1722
|
+
// The identity took time to arrive: whoever is current now decides.
|
|
1723
|
+
assertCurrent()
|
|
1724
|
+
const signer = identity.getPrincipal().toText()
|
|
1725
|
+
if (signer !== principal) {
|
|
1726
|
+
throw createReactorError("cancelled", {
|
|
1727
|
+
...where,
|
|
1728
|
+
code: "caller_changed",
|
|
1729
|
+
reason: `the call was made as ${principal}, but the auth's identity is ${signer}; it was not sent`,
|
|
1730
|
+
})
|
|
1731
|
+
}
|
|
1732
|
+
|
|
1733
|
+
// The same identity object reuses the agent. A newer one (a renewed
|
|
1734
|
+
// delegation) replaces it. An older one that arrives after a newer one was
|
|
1735
|
+
// installed, because its request was answered late, gets the newer agent:
|
|
1736
|
+
// a replaced agent is never handed out again.
|
|
1737
|
+
const entry = agents.get(principal)
|
|
1738
|
+
if (
|
|
1739
|
+
entry !== undefined &&
|
|
1740
|
+
(entry.identity === identity || entry.asked > asked)
|
|
1741
|
+
) {
|
|
1742
|
+
return entry.agent
|
|
1743
|
+
}
|
|
1744
|
+
const agent = HttpAgent.createSync({
|
|
1745
|
+
...agentOptionsFor(network),
|
|
1746
|
+
// The agent signs outside its own error handling: a signer that fails
|
|
1747
|
+
// is thrown as a request that could not be signed (`notSigned`), never
|
|
1748
|
+
// as a call whose outcome is unknown.
|
|
1749
|
+
identity: {
|
|
1750
|
+
getPrincipal: () => identity.getPrincipal(),
|
|
1751
|
+
transformRequest: async (request) => {
|
|
1752
|
+
try {
|
|
1753
|
+
return await identity.transformRequest(request)
|
|
1754
|
+
} catch (cause) {
|
|
1755
|
+
throw notSigned(cause)
|
|
1756
|
+
}
|
|
1757
|
+
},
|
|
1758
|
+
},
|
|
1759
|
+
fetch: agentFetch,
|
|
1760
|
+
// The agent re-sends a request on its own after a fetch failure or a
|
|
1761
|
+
// non-2xx status and reports only the last status, so a 429 after a
|
|
1762
|
+
// lost first attempt would read as "never delivered" and be re-sent
|
|
1763
|
+
// again. With none of its own, every re-send is the call path's,
|
|
1764
|
+
// decided from what this attempt proves (see `retryUpdate`). It also
|
|
1765
|
+
// ends update polling at the first failed `read_state`; see
|
|
1766
|
+
// `ClientInternals.agentFor` for how the call path recovers from that.
|
|
1767
|
+
retryTimes: 0,
|
|
1768
|
+
})
|
|
1769
|
+
agents.set(principal, { identity, agent, asked })
|
|
1770
|
+
return agent
|
|
1771
|
+
}
|
|
1772
|
+
|
|
1773
|
+
/** The auth a sign-in or sign-out goes through, or why there is none. */
|
|
1774
|
+
const authFor = (call: "signIn" | "signOut"): AuthLike => {
|
|
1775
|
+
if (disposed) {
|
|
1776
|
+
throw new Error(`[ic-reactor] ${call}() on a disposed client.`)
|
|
1777
|
+
}
|
|
1778
|
+
if (authFactory === undefined) {
|
|
1779
|
+
throw new TypeError(
|
|
1780
|
+
`[ic-reactor] ${call}() needs a client built with auth; this one was built with identity, which never changes. ` +
|
|
1781
|
+
"Build it with auth: (network) => new AuthClient(network) to sign users in and out."
|
|
1782
|
+
)
|
|
1783
|
+
}
|
|
1784
|
+
const source = ensureAuth()
|
|
1785
|
+
// The factory threw: what it threw is the reason there is no sign-in.
|
|
1786
|
+
if (authFailure !== undefined) throw authFailure.cause
|
|
1787
|
+
if (source === undefined) {
|
|
1788
|
+
throw new Error(
|
|
1789
|
+
`[ic-reactor] ${call}() runs only in a browser: on a server the client is anonymous and never builds its auth.`
|
|
1790
|
+
)
|
|
1791
|
+
}
|
|
1792
|
+
return source
|
|
1793
|
+
}
|
|
1794
|
+
|
|
1795
|
+
const internals: ClientInternals = Object.freeze({
|
|
1796
|
+
network,
|
|
1797
|
+
maxDepth,
|
|
1798
|
+
maxElements,
|
|
1799
|
+
current,
|
|
1800
|
+
disposed: () => disposed,
|
|
1801
|
+
mutationCallers,
|
|
1802
|
+
ownMutationFns,
|
|
1803
|
+
offered: new WeakMap(),
|
|
1804
|
+
agentFor,
|
|
1805
|
+
})
|
|
1806
|
+
|
|
1807
|
+
/** The views of {@link viewAs}, by the object they were made over and their principal. */
|
|
1808
|
+
const views = new WeakMap<object, Map<string, Client>>()
|
|
1809
|
+
|
|
1810
|
+
/**
|
|
1811
|
+
* A view of `this` (the client, or an object over it) for a render that
|
|
1812
|
+
* shows `principal`: the live caller, or someone else while the live caller
|
|
1813
|
+
* moved on, such as the server's anonymous caller while a page hydrates in
|
|
1814
|
+
* a browser that holds a session. `useClient()` returns one on every
|
|
1815
|
+
* render, so that the object a component memoizes on changes exactly when
|
|
1816
|
+
* the caller it renders does (a sign-in, a switch, a sign-out).
|
|
1817
|
+
*
|
|
1818
|
+
* Its `queryKey` and `queryOptions` build for `principal`, so they find
|
|
1819
|
+
* what a server prefetched as that caller and render the markup the server
|
|
1820
|
+
* wrote; `caller()` and `authState()` say who that is (`authState()` is the
|
|
1821
|
+
* client's own state while `principal` is the live caller, so an expired
|
|
1822
|
+
* session reads as expired; otherwise `"anonymous"` for the anonymous
|
|
1823
|
+
* principal and `"signed-in"` for any other). The read options are
|
|
1824
|
+
* the client's own but for the key: a read they build asks `agentFor` for
|
|
1825
|
+
* `principal`'s agent, so while someone else is current it is cancelled
|
|
1826
|
+
* (`caller_changed`) and never sent. Its `mutationOptions` are the
|
|
1827
|
+
* client's own but for the `mutationKey`, which ends with `principal`, and
|
|
1828
|
+
* the mutation function, which sends only as `principal`: while someone
|
|
1829
|
+
* else is current it rejects `cancelled` (`caller_changed`,
|
|
1830
|
+
* `mayHaveExecuted: false`) and sends nothing, so a write from a render
|
|
1831
|
+
* that a switch has not reached yet never goes out as the new caller. Every other member is `this`'s own: canisters (made for
|
|
1832
|
+
* this client, so either one accepts them) and their direct calls,
|
|
1833
|
+
* `resendOf`'s `send` and `func`, which sign as the caller current when
|
|
1834
|
+
* they run, the `QueryClient`, `subscribe`, `signIn`, `signOut` and
|
|
1835
|
+
* `dispose`.
|
|
1836
|
+
*
|
|
1837
|
+
* One view per object and principal, so that a component's dependencies are
|
|
1838
|
+
* the same from one render to the next. A view already made is handed back
|
|
1839
|
+
* without reading who calls. Making a view drops every other one but the
|
|
1840
|
+
* anonymous view and the live caller's, so switching accounts leaves
|
|
1841
|
+
* nothing behind. Until then, the view of a user who signed out, or whose
|
|
1842
|
+
* session expired, is kept, and that user signing in again gets the same
|
|
1843
|
+
* view back. A client built with `identity` has one caller for good, the
|
|
1844
|
+
* one its server render built keys for too: it returns `this`.
|
|
1845
|
+
*
|
|
1846
|
+
* A view carries the object it was made over (`CLIENT_OF`), which
|
|
1847
|
+
* `ReactorProvider` holds in its place, and asked for a view of its own it
|
|
1848
|
+
* makes one of that object.
|
|
1849
|
+
*/
|
|
1850
|
+
function viewAs(this: unknown, principal: string): Client {
|
|
1851
|
+
const self = (
|
|
1852
|
+
typeof this === "object" && this !== null ? this : client
|
|
1853
|
+
) as Client & { readonly [CLIENT_OF]?: Client }
|
|
1854
|
+
// A view of a view is a view of what the first was made over.
|
|
1855
|
+
const base = self[CLIENT_OF] ?? self
|
|
1856
|
+
if (fixedCaller !== undefined) return base
|
|
1857
|
+
let byPrincipal = views.get(base)
|
|
1858
|
+
if (byPrincipal === undefined) views.set(base, (byPrincipal = new Map()))
|
|
1859
|
+
let view = byPrincipal.get(principal)
|
|
1860
|
+
if (view === undefined) {
|
|
1861
|
+
// The views are pruned only when one is made, since `useClient()` asks
|
|
1862
|
+
// for two on every render and reading the live caller converts its
|
|
1863
|
+
// principal to text each time (#977). Every miss prunes, so the map
|
|
1864
|
+
// holds at most the anonymous view, the live caller's and this one.
|
|
1865
|
+
const live = current().principal
|
|
1866
|
+
for (const kept of byPrincipal.keys()) {
|
|
1867
|
+
if (kept !== principal && kept !== live && kept !== ANONYMOUS) {
|
|
1868
|
+
byPrincipal.delete(kept)
|
|
1869
|
+
}
|
|
1870
|
+
}
|
|
1871
|
+
const caller: Caller = Object.freeze({
|
|
1872
|
+
principal,
|
|
1873
|
+
authenticated: principal !== ANONYMOUS,
|
|
1874
|
+
})
|
|
1875
|
+
const state: AuthState =
|
|
1876
|
+
principal === ANONYMOUS
|
|
1877
|
+
? ANONYMOUS_STATE
|
|
1878
|
+
: Object.freeze({ status: "signed-in", principal })
|
|
1879
|
+
const { queryKey, queryOptions } = createBuilders(
|
|
1880
|
+
internals,
|
|
1881
|
+
() => client,
|
|
1882
|
+
() => caller,
|
|
1883
|
+
refusals
|
|
1884
|
+
)
|
|
1885
|
+
const own = base.mutationOptions as unknown as (
|
|
1886
|
+
canister: unknown,
|
|
1887
|
+
method: unknown,
|
|
1888
|
+
options?: unknown
|
|
1889
|
+
) => {
|
|
1890
|
+
mutationKey: readonly unknown[]
|
|
1891
|
+
mutationFn: (vars: unknown, context?: unknown) => Promise<unknown>
|
|
1892
|
+
}
|
|
1893
|
+
// The client's options, whose checks run as they are built, with a
|
|
1894
|
+
// mutation function that sends as `principal` or not at all. The check
|
|
1895
|
+
// and the client's own function, which reads the caller it signs as,
|
|
1896
|
+
// run in one synchronous step; `agentFor` checks again before sending.
|
|
1897
|
+
//
|
|
1898
|
+
// The key ends with `principal`. TanStack hands a pending run the
|
|
1899
|
+
// options of a later render only when their `mutationKey` is the same;
|
|
1900
|
+
// with another one it detaches the observer and the run keeps the
|
|
1901
|
+
// options it was pressed with. So a press handled before a switch
|
|
1902
|
+
// reached its render is checked against its own principal, and refused,
|
|
1903
|
+
// on every TanStack version and whatever the QueryClient.
|
|
1904
|
+
const mutationOptions = (
|
|
1905
|
+
canister: unknown,
|
|
1906
|
+
method: unknown,
|
|
1907
|
+
options?: unknown
|
|
1908
|
+
) => {
|
|
1909
|
+
const built = own(canister, method, options)
|
|
1910
|
+
const mutationFn = async function (
|
|
1911
|
+
this: unknown,
|
|
1912
|
+
vars: unknown,
|
|
1913
|
+
context?: unknown
|
|
1914
|
+
) {
|
|
1915
|
+
const now = current().principal
|
|
1916
|
+
if (now !== principal) {
|
|
1917
|
+
const target = recordOf(
|
|
1918
|
+
canister,
|
|
1919
|
+
internals,
|
|
1920
|
+
"client.mutationOptions()"
|
|
1921
|
+
).resolve()
|
|
1922
|
+
const refusal = createReactorError("cancelled", {
|
|
1923
|
+
method: String(method),
|
|
1924
|
+
canisterId: target.ok ? target.id : target.slot,
|
|
1925
|
+
code: "caller_changed",
|
|
1926
|
+
reason: `the write was made as ${principal}, but the caller is now ${now}; it was not sent`,
|
|
1927
|
+
})
|
|
1928
|
+
// A re-send `resendOf` offered leaves its write unknown, as the
|
|
1929
|
+
// call path does. Marked with the canister, as a call's error
|
|
1930
|
+
// is, so that `isReactorError(error, canister, method)` matches.
|
|
1931
|
+
const again = internals.offered.get(vars as object)
|
|
1932
|
+
throw fromCanister(
|
|
1933
|
+
again === undefined ? refusal : keptUnknown(again, refusal),
|
|
1934
|
+
canister as object
|
|
1935
|
+
)
|
|
1936
|
+
}
|
|
1937
|
+
// `this` is the options TanStack called it on, by which the
|
|
1938
|
+
// client's function finds the caller of a run with no context.
|
|
1939
|
+
return built.mutationFn.call(this, vars, context)
|
|
1940
|
+
}
|
|
1941
|
+
// The client's `MutationCache` reads the caller of a run of this
|
|
1942
|
+
// function too, as it does for the client's own.
|
|
1943
|
+
ownMutationFns.add(mutationFn)
|
|
1944
|
+
return {
|
|
1945
|
+
...built,
|
|
1946
|
+
mutationKey: [...built.mutationKey, principal] as const,
|
|
1947
|
+
mutationFn,
|
|
1948
|
+
}
|
|
1949
|
+
}
|
|
1950
|
+
view = Object.freeze(
|
|
1951
|
+
Object.create(base, {
|
|
1952
|
+
caller: { value: () => principal },
|
|
1953
|
+
// The live state while `principal` is the live caller, so that a
|
|
1954
|
+
// view of the caller signed in now says what the client says (an
|
|
1955
|
+
// expired session included, which calls as the anonymous one).
|
|
1956
|
+
authState: {
|
|
1957
|
+
value: () => {
|
|
1958
|
+
const live = refresh()
|
|
1959
|
+
return live.principal === principal ? live : state
|
|
1960
|
+
},
|
|
1961
|
+
},
|
|
1962
|
+
queryKey: { value: queryKey },
|
|
1963
|
+
queryOptions: { value: queryOptions },
|
|
1964
|
+
mutationOptions: { value: mutationOptions },
|
|
1965
|
+
[CLIENT_OF]: { value: base },
|
|
1966
|
+
}) as Client
|
|
1967
|
+
)
|
|
1968
|
+
byPrincipal.set(principal, view)
|
|
1969
|
+
}
|
|
1970
|
+
return view
|
|
1971
|
+
}
|
|
1972
|
+
|
|
1973
|
+
const members: Client = {
|
|
1974
|
+
...createBuilders(internals, () => client),
|
|
1975
|
+
network: network.keySegment,
|
|
1976
|
+
queryClient,
|
|
1977
|
+
canisterId: (target) =>
|
|
1978
|
+
resolveCanisterId(
|
|
1979
|
+
readTarget(target, "client.canisterId()").target,
|
|
1980
|
+
network
|
|
1981
|
+
),
|
|
1982
|
+
caller: () => current().principal,
|
|
1983
|
+
authState(): AuthState {
|
|
1984
|
+
const state = refresh()
|
|
1985
|
+
if (state !== notified) catchUp()
|
|
1986
|
+
return state
|
|
1987
|
+
},
|
|
1988
|
+
subscribe(listener: () => void): () => void {
|
|
1989
|
+
if (disposed) return () => {}
|
|
1990
|
+
ensureAuth()
|
|
1991
|
+
listeners.add(listener)
|
|
1992
|
+
return () => {
|
|
1993
|
+
listeners.delete(listener)
|
|
1994
|
+
}
|
|
1995
|
+
},
|
|
1996
|
+
signIn(signInOptions?: unknown): Promise<void> {
|
|
1997
|
+
const started = signInsThisRun.find((earlier) =>
|
|
1998
|
+
sameOptions(earlier.options, signInOptions)
|
|
1999
|
+
)
|
|
2000
|
+
if (started) return started.done
|
|
2001
|
+
const done = (async () => {
|
|
2002
|
+
const source = authFor("signIn")
|
|
2003
|
+
try {
|
|
2004
|
+
await source.signIn(signInOptions)
|
|
2005
|
+
} finally {
|
|
2006
|
+
onAuthChange()
|
|
2007
|
+
}
|
|
2008
|
+
})()
|
|
2009
|
+
// Every sign-in started in this run is done joining when it ends. A
|
|
2010
|
+
// promise job, which fake timers do not replace as they can
|
|
2011
|
+
// `queueMicrotask`.
|
|
2012
|
+
if (signInsThisRun.length === 0) {
|
|
2013
|
+
void Promise.resolve().then(() => (signInsThisRun = []))
|
|
2014
|
+
}
|
|
2015
|
+
signInsThisRun.push({ options: signInOptions, done })
|
|
2016
|
+
return done
|
|
2017
|
+
},
|
|
2018
|
+
async signOut(signOutOptions?: unknown): Promise<void> {
|
|
2019
|
+
const source = authFor("signOut")
|
|
2020
|
+
try {
|
|
2021
|
+
await source.signOut(signOutOptions)
|
|
2022
|
+
} finally {
|
|
2023
|
+
onAuthChange()
|
|
2024
|
+
}
|
|
2025
|
+
},
|
|
2026
|
+
dispose(): void {
|
|
2027
|
+
if (disposed) return
|
|
2028
|
+
disposed = true
|
|
2029
|
+
const source = auth
|
|
2030
|
+
const unsubscribe = unsubscribeAuth
|
|
2031
|
+
auth = undefined
|
|
2032
|
+
unsubscribeAuth = undefined
|
|
2033
|
+
listeners.clear()
|
|
2034
|
+
signInsThisRun = []
|
|
2035
|
+
agents.clear()
|
|
2036
|
+
clearTimeout(expiryTimer)
|
|
2037
|
+
expiryTimer = undefined
|
|
2038
|
+
snapshot = notified = ANONYMOUS_STATE
|
|
2039
|
+
queryClient.clear()
|
|
2040
|
+
unsubscribe?.()
|
|
2041
|
+
source?.dispose?.()
|
|
2042
|
+
},
|
|
2043
|
+
}
|
|
2044
|
+
// The stamps and the seam `@ic-reactor/react` reads (see `CLIENTS_CREATED`), defined
|
|
2045
|
+
// before the freeze: a frozen object takes no new property. The stamps are
|
|
2046
|
+
// not enumerable, so a spread copy of a client has no serial and is not
|
|
2047
|
+
// taken for the client it copies. The seam is: a copy (`{ ...client }`, a
|
|
2048
|
+
// test's double) keeps it, and its views are made over the copy, whose
|
|
2049
|
+
// members are the client's own, so a provider given a copy hydrates as one
|
|
2050
|
+
// given the client does.
|
|
2051
|
+
const client: Client = Object.freeze(
|
|
2052
|
+
Object.defineProperties(members, {
|
|
2053
|
+
[CLIENT_SERIAL]: { value: nextSerial() },
|
|
2054
|
+
[CLIENT_DISPOSED]: { get: () => disposed },
|
|
2055
|
+
[CLIENT_AS]: { value: viewAs, enumerable: true },
|
|
2056
|
+
})
|
|
2057
|
+
)
|
|
2058
|
+
|
|
2059
|
+
INTERNALS.set(client, internals)
|
|
2060
|
+
return client
|
|
2061
|
+
}
|