passkey-kit 0.12.1 → 0.14.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/README.md +408 -227
- package/{types → dist}/base.d.ts +1 -0
- package/dist/base.d.ts.map +1 -0
- package/{src/base.ts → dist/base.js} +8 -10
- package/dist/base.js.map +1 -0
- package/dist/base64url.d.ts +18 -0
- package/dist/base64url.d.ts.map +1 -0
- package/dist/base64url.js +18 -0
- package/dist/base64url.js.map +1 -0
- package/dist/constants.d.ts +91 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +115 -0
- package/dist/constants.js.map +1 -0
- package/dist/contract-errors.d.ts +69 -0
- package/dist/contract-errors.d.ts.map +1 -0
- package/dist/contract-errors.js +132 -0
- package/dist/contract-errors.js.map +1 -0
- package/dist/errors.d.ts +163 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +244 -0
- package/dist/errors.js.map +1 -0
- package/dist/events.d.ts +94 -0
- package/dist/events.d.ts.map +1 -0
- package/dist/events.js +92 -0
- package/dist/events.js.map +1 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +36 -0
- package/dist/index.js.map +1 -0
- package/dist/indexer/codec.d.ts +18 -0
- package/dist/indexer/codec.d.ts.map +1 -0
- package/dist/indexer/codec.js +32 -0
- package/dist/indexer/codec.js.map +1 -0
- package/dist/indexer/index.d.ts +25 -0
- package/dist/indexer/index.d.ts.map +1 -0
- package/dist/indexer/index.js +34 -0
- package/dist/indexer/index.js.map +1 -0
- package/dist/indexer/mercury.d.ts +94 -0
- package/dist/indexer/mercury.d.ts.map +1 -0
- package/dist/indexer/mercury.js +245 -0
- package/dist/indexer/mercury.js.map +1 -0
- package/dist/indexer/stellar-indexer.d.ts +115 -0
- package/dist/indexer/stellar-indexer.d.ts.map +1 -0
- package/dist/indexer/stellar-indexer.js +279 -0
- package/dist/indexer/stellar-indexer.js.map +1 -0
- package/dist/indexer/types.d.ts +73 -0
- package/dist/indexer/types.d.ts.map +1 -0
- package/dist/indexer/types.js +9 -0
- package/dist/indexer/types.js.map +1 -0
- package/dist/kit/auth-payload.d.ts +106 -0
- package/dist/kit/auth-payload.d.ts.map +1 -0
- package/dist/kit/auth-payload.js +227 -0
- package/dist/kit/auth-payload.js.map +1 -0
- package/dist/kit/deploy-ops.d.ts +45 -0
- package/dist/kit/deploy-ops.d.ts.map +1 -0
- package/dist/kit/deploy-ops.js +69 -0
- package/dist/kit/deploy-ops.js.map +1 -0
- package/dist/kit/tx-ops.d.ts +95 -0
- package/dist/kit/tx-ops.d.ts.map +1 -0
- package/dist/kit/tx-ops.js +125 -0
- package/dist/kit/tx-ops.js.map +1 -0
- package/dist/kit/wallet-ops.d.ts +59 -0
- package/dist/kit/wallet-ops.d.ts.map +1 -0
- package/dist/kit/wallet-ops.js +122 -0
- package/dist/kit/wallet-ops.js.map +1 -0
- package/dist/kit/webauthn-ops.d.ts +55 -0
- package/dist/kit/webauthn-ops.d.ts.map +1 -0
- package/dist/kit/webauthn-ops.js +79 -0
- package/dist/kit/webauthn-ops.js.map +1 -0
- package/dist/kit.d.ts +132 -0
- package/dist/kit.d.ts.map +1 -0
- package/dist/kit.js +290 -0
- package/dist/kit.js.map +1 -0
- package/dist/managers/credential-manager.d.ts +39 -0
- package/dist/managers/credential-manager.d.ts.map +1 -0
- package/dist/managers/credential-manager.js +47 -0
- package/dist/managers/credential-manager.js.map +1 -0
- package/dist/managers/index.d.ts +7 -0
- package/dist/managers/index.d.ts.map +1 -0
- package/dist/managers/index.js +7 -0
- package/dist/managers/index.js.map +1 -0
- package/dist/managers/signer-manager.d.ts +62 -0
- package/dist/managers/signer-manager.d.ts.map +1 -0
- package/dist/managers/signer-manager.js +111 -0
- package/dist/managers/signer-manager.js.map +1 -0
- package/dist/managers/submission-manager.d.ts +52 -0
- package/dist/managers/submission-manager.d.ts.map +1 -0
- package/dist/managers/submission-manager.js +77 -0
- package/dist/managers/submission-manager.js.map +1 -0
- package/dist/relayer.d.ts +61 -0
- package/dist/relayer.d.ts.map +1 -0
- package/dist/relayer.js +122 -0
- package/dist/relayer.js.map +1 -0
- package/dist/rpc-data.d.ts +45 -0
- package/dist/rpc-data.d.ts.map +1 -0
- package/dist/rpc-data.js +61 -0
- package/dist/rpc-data.js.map +1 -0
- package/dist/sac.d.ts +31 -0
- package/dist/sac.d.ts.map +1 -0
- package/dist/sac.js +47 -0
- package/dist/sac.js.map +1 -0
- package/dist/server.d.ts +79 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +152 -0
- package/dist/server.js.map +1 -0
- package/dist/signers.d.ts +83 -0
- package/dist/signers.d.ts.map +1 -0
- package/dist/signers.js +124 -0
- package/dist/signers.js.map +1 -0
- package/dist/storage/index.d.ts +13 -0
- package/dist/storage/index.d.ts.map +1 -0
- package/dist/storage/index.js +12 -0
- package/dist/storage/index.js.map +1 -0
- package/dist/storage/indexeddb.d.ts +26 -0
- package/dist/storage/indexeddb.d.ts.map +1 -0
- package/dist/storage/indexeddb.js +100 -0
- package/dist/storage/indexeddb.js.map +1 -0
- package/dist/storage/localStorage.d.ts +22 -0
- package/dist/storage/localStorage.d.ts.map +1 -0
- package/dist/storage/localStorage.js +86 -0
- package/dist/storage/localStorage.js.map +1 -0
- package/dist/storage/memory.d.ts +18 -0
- package/dist/storage/memory.d.ts.map +1 -0
- package/dist/storage/memory.js +41 -0
- package/dist/storage/memory.js.map +1 -0
- package/dist/types.d.ts +136 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +39 -0
- package/dist/types.js.map +1 -0
- package/dist/utils.d.ts +72 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +253 -0
- package/dist/utils.js.map +1 -0
- package/dist/validation.d.ts +42 -0
- package/dist/validation.d.ts.map +1 -0
- package/dist/validation.js +75 -0
- package/dist/validation.js.map +1 -0
- package/dist/version.d.ts +7 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +9 -0
- package/dist/version.js.map +1 -0
- package/package.json +47 -20
- package/.claude/settings.local.json +0 -20
- package/.cursorrules +0 -170
- package/.gitattributes +0 -2
- package/.github/workflows/release.yml +0 -21
- package/.vscode/settings.json +0 -6
- package/PROPOSAL.md +0 -397
- package/cheatsheet.txt +0 -18
- package/clone-js-sdk.sh +0 -21
- package/packages/passkey-kit-sdk/README.md +0 -54
- package/packages/passkey-kit-sdk/package.json +0 -21
- package/packages/passkey-kit-sdk/src/index.ts +0 -171
- package/packages/passkey-kit-sdk/tsconfig.json +0 -98
- package/packages/passkey-kit-sdk/types/index.d.ts +0 -190
- package/packages/sac-sdk/README.md +0 -54
- package/packages/sac-sdk/package.json +0 -21
- package/packages/sac-sdk/src/index.ts +0 -586
- package/packages/sac-sdk/tsconfig.json +0 -98
- package/packages/sac-sdk/types/index.d.ts +0 -532
- package/pnpm-workspace.yaml +0 -2
- package/src/index.ts +0 -5
- package/src/kit.ts +0 -823
- package/src/sac.ts +0 -29
- package/src/server.ts +0 -179
- package/src/types.ts +0 -32
- package/tsconfig.json +0 -28
- package/types/index.d.ts +0 -5
- package/types/kit.d.ts +0 -95
- package/types/sac.d.ts +0 -13
- package/types/server.d.ts +0 -29
- package/types/types.d.ts +0 -22
package/.cursorrules
DELETED
|
@@ -1,170 +0,0 @@
|
|
|
1
|
-
# Passkey Kit – Cursor Workspace Rules (v2025-06-12)
|
|
2
|
-
|
|
3
|
-
These rules provide canonical guidance to AI assistants and contributors interacting with this repository. They supersede any previous `.cursorrules` file.
|
|
4
|
-
Their intent is to stay *future-proof*: they encode principles that should hold even as tooling evolves. When in doubt, prefer the spirit of the rule over its letter.
|
|
5
|
-
|
|
6
|
-
---
|
|
7
|
-
|
|
8
|
-
## 1 Repository Topology (overview)
|
|
9
|
-
|
|
10
|
-
• `src/` – Core **passkey-kit** TypeScript library (published to npm).
|
|
11
|
-
• `packages/passkey-kit-sdk/` – Stand-alone TS SDK used by the main package.
|
|
12
|
-
• `packages/sac-sdk/` – SAC (smart-account contract) SDK.
|
|
13
|
-
• `contracts/` – Soroban smart contracts written in Rust.
|
|
14
|
-
• `zephyr/` – Deterministic Zephyr program exposing HTTP endpoints.
|
|
15
|
-
• `demo/` – Svelte demo showcasing end-to-end usage.
|
|
16
|
-
|
|
17
|
-
This repo is a **pnpm workspace**; always use `pnpm` (≥ 8) for Node tasks.
|
|
18
|
-
|
|
19
|
-
---
|
|
20
|
-
|
|
21
|
-
## 2 Language & Toolchain Baselines
|
|
22
|
-
|
|
23
|
-
| Domain | Minimum Supported | Notes |
|
|
24
|
-
|--------|-------------------|-------|
|
|
25
|
-
| Node.js | 20 LTS | ESM by default, top-level `await` allowed |
|
|
26
|
-
| TypeScript | 5.4 | `strict`, `moduleResolution: node16` |
|
|
27
|
-
| Rust (contracts) | 1.79 | Follow the current stable Soroban toolchain |
|
|
28
|
-
| Soroban SDK | 22.x | Keep minor versions in lock-step across contracts |
|
|
29
|
-
| Bun (tests) | 1.x | Used for ultrafast unit tests (`bun test`) |
|
|
30
|
-
| pnpm | 8.x | Managed via **Corepack**; lockfile v5 enforced |
|
|
31
|
-
|
|
32
|
-
Upgrade the baselines on every **major** release of the kit.
|
|
33
|
-
|
|
34
|
-
---
|
|
35
|
-
|
|
36
|
-
## 3 General Coding Principles
|
|
37
|
-
|
|
38
|
-
1. **Functional & immutable** – avoid shared mutable state; favour pure functions.
|
|
39
|
-
2. **Fail loudly, fail early** – surface rich, actionable error messages.
|
|
40
|
-
3. **Minimal dependencies** – prefer platform APIs or internal utils; justify every new dep.
|
|
41
|
-
4. **Typed first** – all public surfaces must be *fully* typed; export `.d.ts` files.
|
|
42
|
-
5. **Async-first** – use `async/await`; never chain raw `Promise` callbacks.
|
|
43
|
-
6. **Workspace integrity** – after changes run `pnpm run build` at the repo root to guarantee type-safety across packages.
|
|
44
|
-
|
|
45
|
-
---
|
|
46
|
-
|
|
47
|
-
## 4 JavaScript / TypeScript Specifics
|
|
48
|
-
|
|
49
|
-
• Import Stellar via `@stellar/stellar-sdk/minimal` unless full bundle is strictly required.
|
|
50
|
-
• Base-64 URL encode/decode with `base64url`; avoid `Buffer.from(x, 'base64')`.
|
|
51
|
-
• Prefer **named exports**; avoid `export default` in new code.
|
|
52
|
-
• File extensions: `.ts` / `.tsx` (React) / `.svelte` (demo) only.
|
|
53
|
-
• Module import grouping: external → workspace → relative → style imports; blank line between groups.
|
|
54
|
-
• Leverage modern ECMAScript (`??`, `?.`, `Array#at`, `Intl`) – but avoid stage-3 proposals unless polyfilled.
|
|
55
|
-
• Publish ESM-first: every package must declare `"type": "module"` and expose an `exports` map with `import`, `require` (fallback), and `types` conditions.
|
|
56
|
-
• Mark packages `"sideEffects": false` when safe to enable aggressive tree-shaking.
|
|
57
|
-
• Ship first-party `.d.ts` bundles (either via `tsc --emitDeclarationOnly` or `api-extractor`). Do **not** rely on DefinitelyTyped for kit packages.
|
|
58
|
-
• Top-level entrypoints should be 100 % statically analyzable – avoid dynamic `require()` or `eval()`.
|
|
59
|
-
|
|
60
|
-
---
|
|
61
|
-
|
|
62
|
-
## 5 Rust / Soroban Contract Rules
|
|
63
|
-
|
|
64
|
-
1. Contracts live in `contracts/*/src`.
|
|
65
|
-
2. `#![no_std]` mandatory; rely on `soroban-sdk` helpers instead of `std`.
|
|
66
|
-
3. Expose a top-level `Contract` struct marked with `#[contract]`.
|
|
67
|
-
4. Model errors with `#[contracterror]`; surface via `panic_with_error!`.
|
|
68
|
-
5. Tests belong under `contracts/*/src/tests` using `soroban_sdk::testutils`.
|
|
69
|
-
6. New contracts must be registered in `contracts/Cargo.toml` workspace members.
|
|
70
|
-
|
|
71
|
-
---
|
|
72
|
-
|
|
73
|
-
## 6 Zephyr Program Rules
|
|
74
|
-
|
|
75
|
-
• Zephyr code resides in `zephyr/src` and **must remain deterministic** – avoid non-deterministic data sources (e.g. system time, randomness) unless explicitly mocked.
|
|
76
|
-
• Each externally exposed endpoint must be documented *above* its `#[no_mangle]` function.
|
|
77
|
-
|
|
78
|
-
---
|
|
79
|
-
|
|
80
|
-
## 7 Svelte / Demo Rules
|
|
81
|
-
|
|
82
|
-
• Demo code lives in `demo/src`.
|
|
83
|
-
• Follow existing component conventions: PascalCase filenames, `<script lang="ts">`, `<style scoped>`.
|
|
84
|
-
• Prefer lightweight stores over global state; mock network calls when possible.
|
|
85
|
-
|
|
86
|
-
---
|
|
87
|
-
|
|
88
|
-
## 8 Testing & CI
|
|
89
|
-
|
|
90
|
-
1. Run `bun test` to execute JavaScript unit tests (`bun_tests/`).
|
|
91
|
-
2. Rust contract tests live alongside contracts.
|
|
92
|
-
3. All new public functions (TS) or contract interfaces (Rust) require unit tests.
|
|
93
|
-
4. Use **Conventional Commits** in PR titles to automate semantic-release notes.
|
|
94
|
-
5. CI (GitHub Actions) must run: `pnpm lint`, `pnpm run build`, `bun test`, and contract tests.
|
|
95
|
-
|
|
96
|
-
---
|
|
97
|
-
|
|
98
|
-
## 9 Versioning & Release
|
|
99
|
-
|
|
100
|
-
• Independent package versions, shared workspace lockfile.
|
|
101
|
-
• Semantic Versioning: breaking changes → major bump.
|
|
102
|
-
• Root `prepublishOnly` script builds **all** packages; never publish from a dirty tree.
|
|
103
|
-
• Tags follow `pkg-name@x.y.z` format.
|
|
104
|
-
• Adopt **Changesets** for automated changelog generation and version bumping across the workspace; release pipeline should consume the generated changelog artefacts.
|
|
105
|
-
|
|
106
|
-
---
|
|
107
|
-
|
|
108
|
-
## 10 Style, Linting & Formatting
|
|
109
|
-
|
|
110
|
-
• Indent: 2 spaces, trailing commas, single quotes.
|
|
111
|
-
• Code formatted with **Prettier** (`pnpm format`).
|
|
112
|
-
• Lint with **ESLint** + `@typescript-eslint`. Lint errors are CI blockers.
|
|
113
|
-
• Prefer `const`; use `let` only when reassignment is needed.
|
|
114
|
-
|
|
115
|
-
---
|
|
116
|
-
|
|
117
|
-
## 11 AI Usage Etiquette
|
|
118
|
-
|
|
119
|
-
• These rules are the *source of truth* for AI coding assistants.
|
|
120
|
-
• Assistants must **never** write secrets, API keys, or personal data into the codebase.
|
|
121
|
-
• When creating code, ensure it can run *immediately* and is fully typed.
|
|
122
|
-
• Before adding dependencies, verify they fit the "minimal deps" philosophy and update `package.json` accordingly.
|
|
123
|
-
|
|
124
|
-
---
|
|
125
|
-
|
|
126
|
-
## 12 Context Shortcuts
|
|
127
|
-
|
|
128
|
-
Use these aliases when referencing files in chat:
|
|
129
|
-
|
|
130
|
-
```text
|
|
131
|
-
@kit.ts → src/kit.ts
|
|
132
|
-
@smart-wallet → contracts/smart-wallet/src/lib.rs
|
|
133
|
-
@passkey-sdk → packages/passkey-kit-sdk/src
|
|
134
|
-
```
|
|
135
|
-
|
|
136
|
-
---
|
|
137
|
-
|
|
138
|
-
## 13 Performance & Build Optimisation
|
|
139
|
-
|
|
140
|
-
1. Prefer **incremental builds** (e.g. `pnpm --filter`, `--recursive --workspace-concurrency`) to accelerate CI and local development.
|
|
141
|
-
2. Enable intelligent cache layers (GitHub Actions cache / bun test cache) to avoid redundant compilation of unchanged packages.
|
|
142
|
-
3. Leverage Bun's ultra-fast test runner; keep test suites granular to maximise parallelism.
|
|
143
|
-
4. When introducing heavy tasks (e.g. Soroban WASM compilation), gate them behind conditional steps so that they execute only when relevant code changes.
|
|
144
|
-
5. Monitor build & test times; aim for sub-5-minute end-to-end CI under normal load.
|
|
145
|
-
|
|
146
|
-
---
|
|
147
|
-
|
|
148
|
-
## 14 npm Package Publishing Principles
|
|
149
|
-
|
|
150
|
-
• Source of truth is **ESM**; if CJS support is required, publish it under `./cjs` and reference with the `require` condition – no transpiled dual-bundles in `dist/`.
|
|
151
|
-
• Generate a **size-snapshot** report (e.g. with `size-limit`) and block PRs that increase size over budget.
|
|
152
|
-
• `package.json` must include: `license`, `funding`, `homepage`, `repository`, `bugs`, and a short but descriptive `description`.
|
|
153
|
-
• Use the `files` whitelist (or `.npmignore`) to keep the published tarball lean (< 30 KiB ideally).
|
|
154
|
-
• Release via GitHub Actions with `npm publish --provenance` to attach Sigstore metadata; use **two-factor auth** on the npm org.
|
|
155
|
-
• After publish, run `npm view @scope/pkg dist.unpackedSize` in CI and fail if unexpectedly large.
|
|
156
|
-
|
|
157
|
-
---
|
|
158
|
-
|
|
159
|
-
## 15 Security & Supply Chain
|
|
160
|
-
|
|
161
|
-
1. Enforce automated dependency scanning (GitHub Dependabot + `pnpm audit --recursive`).
|
|
162
|
-
2. Run **OpenSSF Scorecard** weekly; block merges if score < 7.
|
|
163
|
-
3. Sign commits & tags (Conventional Commits + `git config commit.gpgsign`); CI verifies signatures.
|
|
164
|
-
4. Harden CI with minimal permissions (`GITHUB_TOKEN` → read-only except on `release/*` jobs).
|
|
165
|
-
5. Adopt `.npmrc` with `ignore-scripts=true` for CI and `prefer-offline=true` for faster deterministic installs.
|
|
166
|
-
6. All public APIs must have runtime assertions (zod or custom) to prevent type coercion exploits.
|
|
167
|
-
|
|
168
|
-
---
|
|
169
|
-
|
|
170
|
-
End of rules.
|
package/.gitattributes
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
name: Contract Release
|
|
2
|
-
|
|
3
|
-
on:
|
|
4
|
-
workflow_dispatch:
|
|
5
|
-
inputs:
|
|
6
|
-
release_name:
|
|
7
|
-
description: 'Unique release name'
|
|
8
|
-
required: true
|
|
9
|
-
type: string
|
|
10
|
-
|
|
11
|
-
jobs:
|
|
12
|
-
|
|
13
|
-
release-contract-smart-wallet:
|
|
14
|
-
uses: stellar-expert/soroban-build-workflow/.github/workflows/release.yml@main
|
|
15
|
-
with:
|
|
16
|
-
release_name: ${{ github.ref_name }}
|
|
17
|
-
release_description: 'Release of the smart wallet contract'
|
|
18
|
-
relative_path: 'contracts'
|
|
19
|
-
package: 'smart-wallet'
|
|
20
|
-
secrets:
|
|
21
|
-
release_token: ${{ secrets.GITHUB_TOKEN }}
|
package/.vscode/settings.json
DELETED
package/PROPOSAL.md
DELETED
|
@@ -1,397 +0,0 @@
|
|
|
1
|
-
# Smart wallet contract interface
|
|
2
|
-
|
|
3
|
-
With the release of [Protocol 21](https://stellar.org/blog/developers/announcing-protocol-21) (and specifically the inclusion of the secp256r1 verification curve) Soroban now has tremendous first class support for passkey powered smart wallets.
|
|
4
|
-
|
|
5
|
-
Over the past months I've been hard at work designing a solid first stab at a v1 smart wallet contract interface for mainnet use. This is the culmination of that work in proposal form.
|
|
6
|
-
|
|
7
|
-
All the best work can reviewed in my [passkey-kit](https://github.com/kalepail/passkey-kit) repo. This repo includes the factory and wallet contracts, a demo client interface, a `passkey-kit` SDK tool to make interacting with the contract interface simple and painless and finally a [Mercury Zephyr](https://www.mercurydata.app/products/zephyr-vm) program for indexing contract events in order to make the wallet more usable client side.
|
|
8
|
-
|
|
9
|
-
This repo also makes use of the [OpenZeppelin Relayer service](https://docs.openzeppelin.com/relayer/1.3.x/guides/stellar-channels-guide) which makes submitting Soroban transactions simple by handling the concerns of both transaction fees and sequence numbers.
|
|
10
|
-
|
|
11
|
-
The primary interest of this proposal is to detail the contract interface itself but many of the design decisions are informed by complexities and available solutions external to the interface. A well rounded understandings of all that's involved to make passkey powered smart wallets on Stellar actually work is necessary in order to arrive at a truly viable contract interface.
|
|
12
|
-
|
|
13
|
-
This proposal consists of two contracts, a factory “deployer” contract and the actual smart wallet interface.
|
|
14
|
-
|
|
15
|
-
# Contract 1: The Factory
|
|
16
|
-
|
|
17
|
-
Stellar doesn’t allow us to both deploy and initialize a contract atomically and so the ecosystem has adopted the workaround of having a factory contract which handles the deploying and then calling of the newly deployed contract’s initialize function. This deploy and init can happen atomically within Soroban.
|
|
18
|
-
|
|
19
|
-
The side benefit is we can ensure consistency of all contracts deployed from the same factory address. As long as the contract was deployed from a known factory address users and services have a guarantee of the initial inner form of the smart wallet. As we’ll see smart wallets have an `upgrade` method which will effectively break this guarantee but at the end of the day it’s a contract’s WASM hash we actually care about vs it’s factory address.
|
|
20
|
-
|
|
21
|
-
## Interface
|
|
22
|
-
|
|
23
|
-
```rust
|
|
24
|
-
// FUNCTIONS
|
|
25
|
-
|
|
26
|
-
fn init(wasm_hash: bytesn<32>) -> result<tuple<>,error>
|
|
27
|
-
|
|
28
|
-
fn deploy(id: bytes, pk: bytesn<65>) -> result<address,error>
|
|
29
|
-
|
|
30
|
-
// ERRORS
|
|
31
|
-
|
|
32
|
-
#[contracterror]
|
|
33
|
-
enum Error {
|
|
34
|
-
NotInitialized = 1,
|
|
35
|
-
AlreadyInitialized = 2
|
|
36
|
-
}
|
|
37
|
-
```
|
|
38
|
-
|
|
39
|
-
## Code
|
|
40
|
-
|
|
41
|
-
[https://github.com/kalepail/passkey-kit/blob/main/contracts/smart-wallet-factory/src/lib.rs](https://github.com/kalepail/passkey-kit/blob/main/contracts/smart-wallet-factory/src/lib.rs)
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
```rust
|
|
46
|
-
const WEEK_OF_LEDGERS: u32 = 60 * 60 * 24 / 5 * 7;
|
|
47
|
-
const STORAGE_KEY_WASM_HASH: Symbol = symbol_short!("hash");
|
|
48
|
-
```
|
|
49
|
-
|
|
50
|
-
Only thing to note in this block is I’m opting to max extend this contract’s instance during every call with a threshold of 7 days. This will be the same for the wallet interface itself. This will cause the initial calls for any storage write function to be somewhat inflated with the beneficial tradeoff that folks won’t have to worry about their wallets or keys expiring or archiving for `max_ttl` time. In my tests this cost was minimal and the improved UX of not having to worry about restoring archived entries in my opinion was worth it.
|
|
51
|
-
|
|
52
|
-
We could decide to make these values instance variables which could be updated or even make them configurable on a key by key basis however that would increase complexity and cost in many cases and without further real world data to support that choice I’m suggesting simplicity.
|
|
53
|
-
|
|
54
|
-
### `init`
|
|
55
|
-
```rust
|
|
56
|
-
pub fn init(env: Env, wasm_hash: BytesN<32>) -> Result<(), Error> {
|
|
57
|
-
if env.storage().instance().has(&STORAGE_KEY_WASM_HASH) {
|
|
58
|
-
return Err(Error::AlreadyInitialized);
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
let max_ttl = env.storage().max_ttl();
|
|
62
|
-
|
|
63
|
-
env.storage()
|
|
64
|
-
.instance()
|
|
65
|
-
.set(&STORAGE_KEY_WASM_HASH, &wasm_hash);
|
|
66
|
-
|
|
67
|
-
env.storage()
|
|
68
|
-
.instance()
|
|
69
|
-
.extend_ttl(max_ttl - WEEK_OF_LEDGERS, max_ttl);
|
|
70
|
-
|
|
71
|
-
Ok(())
|
|
72
|
-
}
|
|
73
|
-
```
|
|
74
|
-
|
|
75
|
-
Nothing controversial here I don’t think. We’re storing the smart wallet’s wasm hash in order to load up the factory with the proper template to deploy in the `deploy` function. This is stored on the instance as it should be and then the instance is extended
|
|
76
|
-
|
|
77
|
-
### `deploy`
|
|
78
|
-
```rust
|
|
79
|
-
pub fn deploy(env: Env, salt: BytesN<32>, id: Bytes, pk: BytesN<65>) -> Result<Address, Error> {
|
|
80
|
-
let wasm_hash = env
|
|
81
|
-
.storage()
|
|
82
|
-
.instance()
|
|
83
|
-
.get::<Symbol, BytesN<32>>(&STORAGE_KEY_WASM_HASH)
|
|
84
|
-
.ok_or(Error::NotInitialized)?;
|
|
85
|
-
|
|
86
|
-
let address = env
|
|
87
|
-
.deployer()
|
|
88
|
-
.with_current_contract(salt)
|
|
89
|
-
.deploy(wasm_hash);
|
|
90
|
-
|
|
91
|
-
let () = env.invoke_contract(
|
|
92
|
-
&address,
|
|
93
|
-
&symbol_short!("add"),
|
|
94
|
-
vec![&env, id.to_val(), pk.to_val(), true.into()],
|
|
95
|
-
);
|
|
96
|
-
|
|
97
|
-
let max_ttl = env.storage().max_ttl();
|
|
98
|
-
|
|
99
|
-
env.storage()
|
|
100
|
-
.instance()
|
|
101
|
-
.extend_ttl(max_ttl - WEEK_OF_LEDGERS, max_ttl);
|
|
102
|
-
|
|
103
|
-
Ok(address)
|
|
104
|
-
}
|
|
105
|
-
```
|
|
106
|
-
|
|
107
|
-
Few things to note here:
|
|
108
|
-
|
|
109
|
-
- Also note we’re calling the `env.invoke_contract` vs pulling in the smart wallet interface. This is a cost savings as we’re only making use of the `add` method. This requires knowing intuitively how to properly construct the invocation but let’s be honest, that’s not hard.
|
|
110
|
-
- Last thing is extending the interface again. If you’re gonna use the factory at least pay it forward a little to help keep the factory’s lights on.
|
|
111
|
-
|
|
112
|
-
# Contract 2: The Smart Wallet
|
|
113
|
-
|
|
114
|
-
The smart wallet interface while obviously more complex than the factory is still aiming to be as simple as possible and only do what’s absolutely necessary to provide a useful smart wallet interface for general purpose usage.
|
|
115
|
-
|
|
116
|
-
I’ve intentionally left off as many bells and whistles as possible with the hope of being able to agree and progress with this interface into an audited and approved mainnet interface for general usage. Certainly there will be additional features and functions users and services will want and I hope to see a rich and diverse ecosystem of wallet interfaces arise over time but initially we just need to get something sufficiently useful live providing the basic majority needs of non crypto-native users.
|
|
117
|
-
|
|
118
|
-
## Interface
|
|
119
|
-
|
|
120
|
-
```rust
|
|
121
|
-
// FUNCTIONS
|
|
122
|
-
|
|
123
|
-
fn add(id: bytes, pk: bytesn<65>, admin: bool) -> result<tuple<>,error>
|
|
124
|
-
|
|
125
|
-
fn remove(id: bytes) -> result<tuple<>,error>
|
|
126
|
-
|
|
127
|
-
fn upgrade(hash: bytesn<32>) -> result<tuple<>,error>
|
|
128
|
-
|
|
129
|
-
fn __check_auth(signature_payload: bytesn<32>, signature: Signature, auth_contexts: vec<Context>) -> result<tuple<>,error>
|
|
130
|
-
|
|
131
|
-
// STRUCTS
|
|
132
|
-
|
|
133
|
-
#[contracttype]
|
|
134
|
-
struct Signature {
|
|
135
|
-
authenticator_data: bytes,
|
|
136
|
-
client_data_json: bytes,
|
|
137
|
-
id: bytes,
|
|
138
|
-
signature: bytesn<64>
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
// ERRORS
|
|
142
|
-
|
|
143
|
-
#[contracterror]
|
|
144
|
-
enum Error {
|
|
145
|
-
NotFound = 1,
|
|
146
|
-
NotPermitted = 2,
|
|
147
|
-
ClientDataJsonChallengeIncorrect = 3,
|
|
148
|
-
Secp256r1PublicKeyParse = 4,
|
|
149
|
-
Secp256r1SignatureParse = 5,
|
|
150
|
-
Secp256r1VerifyFailed = 6,
|
|
151
|
-
JsonParseError = 7,
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
```
|
|
155
|
-
|
|
156
|
-
## Code
|
|
157
|
-
|
|
158
|
-
[https://github.com/kalepail/passkey-kit/blob/main/contracts/smart-wallet/src/lib.rs](https://github.com/kalepail/passkey-kit/blob/main/contracts/smart-wallet/src/lib.rs)
|
|
159
|
-
|
|
160
|
-
### `add`
|
|
161
|
-
```rust
|
|
162
|
-
pub fn add(env: Env, id: Bytes, pk: BytesN<65>, mut admin: bool) -> Result<(), Error> {
|
|
163
|
-
if env.storage().instance().has(&ADMIN_SIGNER_COUNT) {
|
|
164
|
-
env.current_contract_address().require_auth();
|
|
165
|
-
} else {
|
|
166
|
-
admin = true;
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
let max_ttl = env.storage().max_ttl();
|
|
170
|
-
|
|
171
|
-
if admin {
|
|
172
|
-
if env.storage().temporary().has(&id) {
|
|
173
|
-
env.storage().temporary().remove(&id);
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
Self::update_admin_signer_count(&env, true);
|
|
177
|
-
|
|
178
|
-
env.storage().persistent().set(&id, &pk);
|
|
179
|
-
|
|
180
|
-
env.storage()
|
|
181
|
-
.persistent()
|
|
182
|
-
.extend_ttl(&id, max_ttl - WEEK_OF_LEDGERS, max_ttl);
|
|
183
|
-
} else {
|
|
184
|
-
if env.storage().persistent().has(&id) {
|
|
185
|
-
Self::update_admin_signer_count(&env, false);
|
|
186
|
-
|
|
187
|
-
env.storage().persistent().remove(&id);
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
env.storage().temporary().set(&id, &pk);
|
|
191
|
-
|
|
192
|
-
env.storage()
|
|
193
|
-
.temporary()
|
|
194
|
-
.extend_ttl(&id, max_ttl - WEEK_OF_LEDGERS, max_ttl);
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
env.storage()
|
|
198
|
-
.instance()
|
|
199
|
-
.extend_ttl(max_ttl - WEEK_OF_LEDGERS, max_ttl);
|
|
200
|
-
|
|
201
|
-
env.events()
|
|
202
|
-
.publish((EVENT_TAG, symbol_short!("add"), id), (pk, admin));
|
|
203
|
-
|
|
204
|
-
Ok(())
|
|
205
|
-
}
|
|
206
|
-
```
|
|
207
|
-
|
|
208
|
-
Some notable elements:
|
|
209
|
-
|
|
210
|
-
- We use the `env.storage().instance().has(&ADMIN_SIGNER_COUNT)` to toggle between a sort of initialization call and the standard `require_auth` flow.
|
|
211
|
-
|
|
212
|
-
```rust
|
|
213
|
-
if env.storage().instance().has(&ADMIN_SIGNER_COUNT) {
|
|
214
|
-
env.current_contract_address().require_auth();
|
|
215
|
-
} else {
|
|
216
|
-
admin = true;
|
|
217
|
-
}
|
|
218
|
-
```
|
|
219
|
-
|
|
220
|
-
The only potential downside is `add` includes logic for storing temporary session signers which an initial call doesn't support making that logic verbose. Initially I had a separate `init` function but I think this is a better tradeoff for simplicity and efficiency even if there are some unusable if statements in the case of the initial `add` call made by the factory contract.
|
|
221
|
-
|
|
222
|
-
- `Self::update_admin_signer_count(&env, true);` My proposal includes the concept of session and admin signers. Certain functions, well really all of the smart wallet self functions (`add`, `remove`, `upgrade`) are only callable by admin signers. Given this we need to ensure we never remove all the admin signers which necessarily requires we track the number of admin signers. This function provides that service and will be called anytime we add or remove an admin signer.
|
|
223
|
-
- Admin signers are persistent entries, non-admin signers are temporary. It’s also possible for signers to be toggled between admin and non however we must only ever be tracking a single `id` to a single `pk` and so we must add logic for removing any existing signers for a given `id` in the counter storage to the type we’re currently adding to. Make special note of the need to decrement the `ADMIN_SIGNER_COUNT` in case of removing an admin signer to temporary if a persistent entry for that `id` exists.
|
|
224
|
-
|
|
225
|
-
```rust
|
|
226
|
-
if admin {
|
|
227
|
-
if env.storage().temporary().has(&id) {
|
|
228
|
-
env.storage().temporary().remove(&id);
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
Self::update_admin_signer_count(&env, true);
|
|
232
|
-
|
|
233
|
-
env.storage().persistent().set(&id, &pk);
|
|
234
|
-
|
|
235
|
-
env.storage()
|
|
236
|
-
.persistent()
|
|
237
|
-
.extend_ttl(&id, max_ttl - WEEK_OF_LEDGERS, max_ttl);
|
|
238
|
-
} else {
|
|
239
|
-
if env.storage().persistent().has(&id) {
|
|
240
|
-
Self::update_admin_signer_count(&env, false);
|
|
241
|
-
|
|
242
|
-
env.storage().persistent().remove(&id);
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
env.storage().temporary().set(&id, &pk);
|
|
246
|
-
|
|
247
|
-
env.storage()
|
|
248
|
-
.temporary()
|
|
249
|
-
.extend_ttl(&id, max_ttl - WEEK_OF_LEDGERS, max_ttl);
|
|
250
|
-
}
|
|
251
|
-
```
|
|
252
|
-
|
|
253
|
-
### `remove`
|
|
254
|
-
```rust
|
|
255
|
-
pub fn remove(env: Env, id: Bytes) -> Result<(), Error> {
|
|
256
|
-
env.current_contract_address().require_auth();
|
|
257
|
-
|
|
258
|
-
if env.storage().temporary().has(&id) {
|
|
259
|
-
env.storage().temporary().remove(&id);
|
|
260
|
-
} else {
|
|
261
|
-
Self::update_admin_signer_count(&env, false);
|
|
262
|
-
|
|
263
|
-
env.storage().persistent().remove(&id);
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
let max_ttl = env.storage().max_ttl();
|
|
267
|
-
|
|
268
|
-
env.storage()
|
|
269
|
-
.instance()
|
|
270
|
-
.extend_ttl(max_ttl - WEEK_OF_LEDGERS, max_ttl);
|
|
271
|
-
|
|
272
|
-
env.events()
|
|
273
|
-
.publish((EVENT_TAG, symbol_short!("remove"), id), ());
|
|
274
|
-
|
|
275
|
-
Ok(())
|
|
276
|
-
}
|
|
277
|
-
```
|
|
278
|
-
|
|
279
|
-
Remove is similar to `add` just in inverse with some slight simplifications.
|
|
280
|
-
|
|
281
|
-
- Given the key could be either temporary or persistent we must include logic for checking both and removing if they exist. Again note the need to decrement the `ADMIN_SIGNER_COUNT` in case of a persistent admin `id`.
|
|
282
|
-
- Given each `id` can only be either a temporary or persistent entry it's safe to use `else if env.storage().persistent().has(&id)` vs a separate `if ...`. Doing so saves on some read costs if we were to try and just remove both storage type for the same `id` key. Note we do need to use the has check vs just doing an `else` check as a `storage.remove` won't error if the entry doesn't exist which would open us up to the issue of decrementing the admin key count when we didn't actually delete anything.
|
|
283
|
-
|
|
284
|
-
### `update`
|
|
285
|
-
```rust
|
|
286
|
-
pub fn update(env: Env, hash: BytesN<32>) -> Result<(), Error> {
|
|
287
|
-
env.current_contract_address().require_auth();
|
|
288
|
-
|
|
289
|
-
env.deployer().update_current_contract_wasm(hash);
|
|
290
|
-
|
|
291
|
-
let max_ttl = env.storage().max_ttl();
|
|
292
|
-
|
|
293
|
-
env.storage()
|
|
294
|
-
.instance()
|
|
295
|
-
.extend_ttl(max_ttl - WEEK_OF_LEDGERS, max_ttl);
|
|
296
|
-
|
|
297
|
-
Ok(())
|
|
298
|
-
}
|
|
299
|
-
```
|
|
300
|
-
|
|
301
|
-
An essential function for all smart wallets imo. The ability to change the interface the wallet implements. Perhaps controversial given the risk of upgrading to a bugged or malicious wallet interface but that’s an risk inherent to creating a smart wallet in the first place and given that risk I actually think part of mitigating that risk is allowing users to move their interface to alternatives should they choose to. Client interfaces should be very careful in exposing this method to wallet users but I do think it’s an essential method for the health and safety of the smart wallet ecosystem.
|
|
302
|
-
|
|
303
|
-
- Protected such that only admin signers can perform this method.
|
|
304
|
-
- Allows for a wallet user to switch or update their interface should newer or different interfaces be released.
|
|
305
|
-
|
|
306
|
-
### `__check_auth`
|
|
307
|
-
```rust
|
|
308
|
-
fn __check_auth(
|
|
309
|
-
env: Env,
|
|
310
|
-
signature_payload: Hash<32>,
|
|
311
|
-
signature: Signature,
|
|
312
|
-
auth_contexts: Vec<Context>,
|
|
313
|
-
) -> Result<(), Error> {...}
|
|
314
|
-
```
|
|
315
|
-
|
|
316
|
-
This is the beefy boy and most of it is only interesting to auditors ensuring the actual decoding and cryptography bits work as intended. I’ll detail the parts here which are more specific to the interface itself:
|
|
317
|
-
|
|
318
|
-
- We need to select which `pk` to use for the provided `id` purporting to have signed for the incoming payload.
|
|
319
|
-
|
|
320
|
-
```rust
|
|
321
|
-
let pk = match env.storage().temporary().get(&id) {
|
|
322
|
-
Some(pk) => {
|
|
323
|
-
...
|
|
324
|
-
|
|
325
|
-
env.storage()
|
|
326
|
-
.temporary()
|
|
327
|
-
.extend_ttl(&id, max_ttl - WEEK_OF_LEDGERS, max_ttl);
|
|
328
|
-
|
|
329
|
-
pk
|
|
330
|
-
}
|
|
331
|
-
None => {
|
|
332
|
-
env.storage()
|
|
333
|
-
.persistent()
|
|
334
|
-
.extend_ttl(&id, max_ttl - WEEK_OF_LEDGERS, max_ttl);
|
|
335
|
-
|
|
336
|
-
env.storage().persistent().get(&id).ok_or(Error::NotFound)?
|
|
337
|
-
}
|
|
338
|
-
};
|
|
339
|
-
```
|
|
340
|
-
|
|
341
|
-
We do that first by looking up the temporary entry which will be the far more common case. If we cannot find it there we look for a persistent entry. This will introduce a double look up for a temporary entry but those are cheap so this is fine. Note we also set the `admin` binary toggle for use later in blocking protected self methods.
|
|
342
|
-
|
|
343
|
-
- If the pk is a temporary session signer we need to do an additional check to ensure the authentication request isn’t for a protected action
|
|
344
|
-
|
|
345
|
-
```rust
|
|
346
|
-
|
|
347
|
-
...
|
|
348
|
-
|
|
349
|
-
for context in auth_contexts.iter() {
|
|
350
|
-
match context {
|
|
351
|
-
Context::Contract(c) => {
|
|
352
|
-
if c.contract == env.current_contract_address()
|
|
353
|
-
&& (
|
|
354
|
-
c.fn_name != symbol_short!("remove")
|
|
355
|
-
|| (
|
|
356
|
-
c.fn_name == symbol_short!("remove")
|
|
357
|
-
&& Bytes::from_val(&env, &c.args.get(0).unwrap()) != id
|
|
358
|
-
)
|
|
359
|
-
)
|
|
360
|
-
{
|
|
361
|
-
return Err(Error::NotPermitted);
|
|
362
|
-
}
|
|
363
|
-
}
|
|
364
|
-
_ => {}
|
|
365
|
-
};
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
...
|
|
369
|
-
```
|
|
370
|
-
|
|
371
|
-
This is a relatively straight forward check. If the request is for the smart wallet contract ensure the only function it *might* be able to call is a `remove` of it’s own `id`. Anything else should result in an error.
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
The rest of `__check_auth` is boilerplate authentication checks of the signature data itself and not technically part of this interface. It needs to be audited but that won’t affect the final interface of the wallet.
|
|
375
|
-
|
|
376
|
-
# Events
|
|
377
|
-
|
|
378
|
-
The only other item worth mentioning are the events emitted during the `add` and `remove` methods. Events are emitted in order to allow an indexer to keep track of a wallet’s available signers and their current state as `admin` or not.
|
|
379
|
-
|
|
380
|
-
## Add
|
|
381
|
-
|
|
382
|
-
```rust
|
|
383
|
-
env.events().publish((EVENT_TAG, symbol_short!("add"), id), (pk, admin));
|
|
384
|
-
```
|
|
385
|
-
|
|
386
|
-
- The `EVENT_TAG` is a trigger to help indexers only listen for relevant smart wallet events and while not fool proof should improve filtering out only those events which are relevant.
|
|
387
|
-
- The `pk` is emitted in order to allow downstream clients to queue up expired session signers to be re-added without needing to create new passkeys, you can continue to use the existing ones if you can find the `pk` for a matching `id` from a previously emitted event.
|
|
388
|
-
|
|
389
|
-
> [!CAUTION]
|
|
390
|
-
> Passkey public keys are only retrievable during a passkey creation flow. They cannot be later retrieved from an authentication flow. Thus passkey public keys are special data which we should be storing inside the blockchain itself. This is normally done during an `add` event but given we’re using temporary storage these keys could be lost and unrecoverable were we not to store them inside events for indexers to keep track of and then for clients to then be able to essentially “rehydrate” at a later date without requiring the user to keep creating new passkeys every time they wanted to sign into a service after their temporary session key had expired.
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
## Remove
|
|
394
|
-
|
|
395
|
-
```rust
|
|
396
|
-
env.events().publish((EVENT_TAG, symbol_short!("remove"), id), ());
|
|
397
|
-
```
|
package/cheatsheet.txt
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
# Install contract sdks
|
|
2
|
-
npm publish --workspaces
|
|
3
|
-
|
|
4
|
-
# Install passkey-kit
|
|
5
|
-
pnpm publish --no-git-checks
|
|
6
|
-
|
|
7
|
-
# Stellar commands
|
|
8
|
-
stellar contract bindings rust --wasm target/wasm32v1-none/release/smart_wallet.wasm
|
|
9
|
-
stellar contract deploy --wasm target/wasm32v1-none/release/sample_policy.wasm --network testnet --source default
|
|
10
|
-
|
|
11
|
-
# Mercury commands
|
|
12
|
-
# https://test.mercurydata.app/
|
|
13
|
-
|
|
14
|
-
export JWT=???
|
|
15
|
-
mercury-cli --jwt $JWT --local false --mainnet false deploy
|
|
16
|
-
mercury-cli --jwt $JWT --local false --mainnet false catchup --project-name "smart-wallets-next-dima" --contracts CBFOIYCWRC5LXYMF7VPM4MTBEIIMDJ6TBWYH2PY5U7MW73MINQNN3V7E
|
|
17
|
-
curl -X GET https://api.mercurydata.app/catchups/4
|
|
18
|
-
curl -X POST https://api.mercurydata.app/v2/key -H "Authorization: Bearer $JWT"
|
package/clone-js-sdk.sh
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
#!/bin/bash
|
|
2
|
-
|
|
3
|
-
cd ext/js-stellar-sdk/
|
|
4
|
-
yarn run clean
|
|
5
|
-
yarn run build:ts
|
|
6
|
-
yarn run build:node
|
|
7
|
-
yarn run build:browser
|
|
8
|
-
cd ../../
|
|
9
|
-
|
|
10
|
-
rm -rf ext/@stellar
|
|
11
|
-
mkdir -p ext/@stellar/stellar-sdk
|
|
12
|
-
cp -R ext/js-stellar-sdk/lib ext/@stellar/stellar-sdk/
|
|
13
|
-
cp -R ext/js-stellar-sdk/dist ext/@stellar/stellar-sdk/
|
|
14
|
-
cp -R ext/js-stellar-sdk/types ext/@stellar/stellar-sdk/
|
|
15
|
-
cp ext/js-stellar-sdk/package.json ext/@stellar/stellar-sdk/
|
|
16
|
-
|
|
17
|
-
cd demo/
|
|
18
|
-
rm -rf node_modules pnpm-lock.yaml
|
|
19
|
-
pnpm install
|
|
20
|
-
|
|
21
|
-
cd ../
|