@needmoretruth/nmts-cli 0.17.2 → 0.34.2
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/AGENTS.md +125 -517
- package/CHANGELOG.md +104 -0
- package/README.ko.md +377 -469
- package/README.md +394 -474
- package/dist/account-proof.d.ts +29 -0
- package/dist/account-proof.js +6 -6
- package/dist/account.d.ts +18 -0
- package/dist/account.js +5 -5
- package/dist/agent-host.d.ts +71 -0
- package/dist/agent-host.js +169 -0
- package/dist/api-advice.d.ts +2 -0
- package/dist/api-advice.js +210 -0
- package/dist/api-key.d.ts +79 -0
- package/dist/api-key.js +7 -7
- package/dist/api-refusal.d.ts +14 -0
- package/dist/api-refusal.js +8 -0
- package/dist/api.d.ts +114 -0
- package/dist/api.js +59 -162
- package/dist/args.d.ts +160 -0
- package/dist/args.js +118 -30
- package/dist/artifact-about.d.ts +49 -0
- package/dist/autonomy.d.ts +39 -0
- package/dist/autonomy.js +110 -32
- package/dist/code-access.d.ts +29 -0
- package/dist/code-access.js +3 -3
- package/dist/code-vault.d.ts +38 -0
- package/dist/code-vault.js +8 -8
- package/dist/collision.d.ts +67 -0
- package/dist/collision.js +1 -1
- package/dist/commands/accept-terms.d.ts +11 -0
- package/dist/commands/accept-terms.js +106 -0
- package/dist/commands/balance.d.ts +7 -0
- package/dist/commands/balance.js +54 -1
- package/dist/commands/create-code-file.d.ts +24 -0
- package/dist/commands/create-code-file.js +93 -0
- package/dist/commands/create-link.d.ts +10 -0
- package/dist/commands/create-link.js +232 -0
- package/dist/commands/create.d.ts +20 -0
- package/dist/commands/create.js +43 -102
- package/dist/commands/credits.d.ts +9 -0
- package/dist/commands/credits.js +110 -0
- package/dist/commands/delete-account.d.ts +11 -0
- package/dist/commands/delete-account.js +63 -0
- package/dist/commands/deposit.d.ts +7 -0
- package/dist/commands/deposit.js +85 -0
- package/dist/commands/devices.d.ts +12 -0
- package/dist/commands/devices.js +144 -0
- package/dist/commands/documents.d.ts +35 -0
- package/dist/commands/documents.js +212 -0
- package/dist/commands/env.d.ts +5 -0
- package/dist/commands/env.js +10 -2
- package/dist/commands/erase.d.ts +13 -0
- package/dist/commands/erase.js +193 -0
- package/dist/commands/expiring.d.ts +19 -0
- package/dist/commands/extend.d.ts +33 -0
- package/dist/commands/extend.js +54 -15
- package/dist/commands/get.d.ts +18 -0
- package/dist/commands/key-manage.d.ts +11 -0
- package/dist/commands/key-manage.js +162 -0
- package/dist/commands/key.d.ts +28 -0
- package/dist/commands/key.js +249 -0
- package/dist/commands/kit.d.ts +11 -0
- package/dist/commands/kit.js +7 -7
- package/dist/commands/listfile.d.ts +13 -0
- package/dist/commands/listfile.js +12 -5
- package/dist/commands/login.d.ts +18 -0
- package/dist/commands/login.js +19 -19
- package/dist/commands/logout.d.ts +1 -0
- package/dist/commands/logout.js +1 -1
- package/dist/commands/losses.d.ts +12 -0
- package/dist/commands/losses.js +180 -0
- package/dist/commands/ls.d.ts +23 -0
- package/dist/commands/marks-dispatch.d.ts +2 -0
- package/dist/commands/marks-dispatch.js +51 -0
- package/dist/commands/marks.d.ts +46 -0
- package/dist/commands/marks.js +80 -0
- package/dist/commands/mcp.d.ts +24 -0
- package/dist/commands/mcp.js +70 -5
- package/dist/commands/mode.d.ts +8 -0
- package/dist/commands/mode.js +60 -24
- package/dist/commands/on-collision.d.ts +6 -0
- package/dist/commands/organise.d.ts +46 -0
- package/dist/commands/padding.d.ts +7 -0
- package/dist/commands/padding.js +114 -0
- package/dist/commands/public-code.d.ts +9 -0
- package/dist/commands/public-code.js +8 -8
- package/dist/commands/pull.d.ts +27 -0
- package/dist/commands/push-wallet.d.ts +19 -0
- package/dist/commands/push-wallet.js +69 -0
- package/dist/commands/push.d.ts +47 -0
- package/dist/commands/push.js +37 -7
- package/dist/commands/put-wallet.d.ts +77 -0
- package/dist/commands/put-wallet.js +316 -0
- package/dist/commands/put.d.ts +56 -0
- package/dist/commands/put.js +53 -6
- package/dist/commands/rebuild.d.ts +18 -0
- package/dist/commands/rebuild.js +108 -10
- package/dist/commands/receive.d.ts +24 -0
- package/dist/commands/recovery-dispatch.d.ts +2 -0
- package/dist/commands/recovery-dispatch.js +34 -0
- package/dist/commands/recovery-list.d.ts +11 -0
- package/dist/commands/recovery-list.js +6 -6
- package/dist/commands/recovery.d.ts +25 -0
- package/dist/commands/recovery.js +2 -2
- package/dist/commands/rollback.d.ts +9 -0
- package/dist/commands/rollback.js +118 -0
- package/dist/commands/s3.d.ts +23 -0
- package/dist/commands/s3.js +6 -6
- package/dist/commands/settings.d.ts +5 -0
- package/dist/commands/settings.js +21 -6
- package/dist/commands/share-dispatch.d.ts +2 -0
- package/dist/commands/share-dispatch.js +46 -0
- package/dist/commands/share.d.ts +14 -0
- package/dist/commands/share.js +13 -3
- package/dist/commands/shares-sent.d.ts +18 -0
- package/dist/commands/shares-sent.js +95 -0
- package/dist/commands/support.d.ts +24 -0
- package/dist/commands/support.js +321 -0
- package/dist/commands/sweep.d.ts +11 -0
- package/dist/commands/tip.d.ts +11 -0
- package/dist/commands/tip.js +86 -0
- package/dist/commands/trash.d.ts +17 -0
- package/dist/commands/trash.js +1 -1
- package/dist/commands/trial.d.ts +7 -0
- package/dist/commands/trial.js +16 -18
- package/dist/commands/unlock.d.ts +16 -0
- package/dist/commands/unlock.js +159 -0
- package/dist/commands/update.d.ts +42 -0
- package/dist/commands/update.js +1 -1
- package/dist/commands/usage.d.ts +8 -0
- package/dist/commands/verify.d.ts +18 -0
- package/dist/commands/verify.js +10 -6
- package/dist/commands/wallet-activity.d.ts +26 -0
- package/dist/commands/wallet-activity.js +117 -0
- package/dist/commands/wallet-donate.d.ts +26 -0
- package/dist/commands/wallet-donate.js +163 -0
- package/dist/commands/wallet-hall.d.ts +35 -0
- package/dist/commands/wallet-hall.js +214 -0
- package/dist/commands/wallet-send.d.ts +22 -0
- package/dist/commands/wallet-send.js +153 -0
- package/dist/commands/wallet-storage-ops.d.ts +24 -0
- package/dist/commands/wallet-storage-ops.js +215 -0
- package/dist/commands/wallet-storage.d.ts +18 -0
- package/dist/commands/wallet-storage.js +95 -0
- package/dist/commands/wallet-swap.d.ts +32 -0
- package/dist/commands/wallet-swap.js +287 -0
- package/dist/commands/wallet.d.ts +36 -0
- package/dist/commands/wallet.js +57 -15
- package/dist/commands/whoami.d.ts +10 -0
- package/dist/commands/whoami.js +24 -6
- package/dist/consent.d.ts +140 -0
- package/dist/consent.js +89 -30
- package/dist/credentials.d.ts +146 -0
- package/dist/credentials.js +10 -10
- package/dist/crypto-surface.d.ts +197 -0
- package/dist/crypto.d.ts +81 -0
- package/dist/crypto.js +13 -0
- package/dist/deposit.d.ts +25 -0
- package/dist/deposit.js +78 -0
- package/dist/download-part.d.ts +40 -0
- package/dist/download-sink.d.ts +76 -0
- package/dist/download.d.ts +79 -0
- package/dist/download.js +3 -3
- package/dist/drive-paths.d.ts +73 -0
- package/dist/environment.d.ts +62 -0
- package/dist/environment.js +29 -9
- package/dist/errors.d.ts +22 -0
- package/dist/errors.js +3 -3
- package/dist/exit.d.ts +44 -0
- package/dist/expiry.d.ts +58 -0
- package/dist/extend-budget.d.ts +35 -0
- package/dist/extend-budget.js +81 -0
- package/dist/extend-chain.d.ts +43 -0
- package/dist/extend-chain.js +40 -3
- package/dist/extend-plan.d.ts +148 -0
- package/dist/extend-plan.js +1 -1
- package/dist/gate.d.ts +18 -0
- package/dist/gate.js +100 -0
- package/dist/guards.d.ts +2 -0
- package/dist/help.d.ts +20 -0
- package/dist/help.js +199 -33
- package/dist/human-check.d.ts +23 -0
- package/dist/index.d.ts +42 -0
- package/dist/index.js +49 -0
- package/dist/item-trash.d.ts +1 -0
- package/dist/kit-file.d.ts +56 -0
- package/dist/kit-file.js +10 -10
- package/dist/list-file.d.ts +88 -0
- package/dist/list-file.js +90 -7
- package/dist/list-view-find.d.ts +15 -0
- package/dist/list-view-order.d.ts +32 -0
- package/dist/main.d.ts +12 -0
- package/dist/main.js +112 -113
- package/dist/manifest-chunk-cache.d.ts +14 -0
- package/dist/manifest-chunk-cache.js +103 -0
- package/dist/manifest-chunk-flow.d.ts +81 -0
- package/dist/manifest-chunk-flow.js +307 -0
- package/dist/manifest-create.d.ts +23 -0
- package/dist/manifest-create.js +26 -30
- package/dist/manifest-write.d.ts +161 -0
- package/dist/manifest-write.js +42 -33
- package/dist/manifest.d.ts +82 -0
- package/dist/manifest.js +69 -21
- package/dist/mark-render.d.ts +26 -0
- package/dist/mcp-args.d.ts +17 -0
- package/dist/mcp-ask.d.ts +43 -0
- package/dist/mcp-ask.js +100 -0
- package/dist/mcp-tools/context.d.ts +46 -0
- package/dist/mcp-tools/credits.d.ts +3 -0
- package/dist/mcp-tools/credits.js +61 -0
- package/dist/mcp-tools/files.d.ts +3 -0
- package/dist/mcp-tools/files.js +34 -0
- package/dist/mcp-tools/organise.d.ts +3 -0
- package/dist/mcp-tools/organise.js +82 -2
- package/dist/mcp-tools/reads.d.ts +3 -0
- package/dist/mcp-tools/reads.js +132 -4
- package/dist/mcp-tools/share.d.ts +3 -0
- package/dist/mcp-tools/share.js +26 -7
- package/dist/mcp-tools/support.d.ts +3 -0
- package/dist/mcp-tools/support.js +140 -0
- package/dist/mcp-tools/tiers.d.ts +31 -0
- package/dist/mcp-tools/tiers.js +147 -0
- package/dist/mcp.d.ts +80 -0
- package/dist/mcp.js +110 -9
- package/dist/net-retry.d.ts +38 -0
- package/dist/network.d.ts +10 -0
- package/dist/notice.d.ts +7 -0
- package/dist/notice.js +7 -7
- package/dist/product.d.ts +29 -0
- package/dist/product.js +1 -1
- package/dist/progress.d.ts +37 -0
- package/dist/prompt.d.ts +31 -0
- package/dist/prompt.js +1 -1
- package/dist/rebuild-key-check.d.ts +26 -0
- package/dist/rebuild-key-check.js +90 -0
- package/dist/rebuild.d.ts +87 -0
- package/dist/rebuild.js +26 -4
- package/dist/recovery-assemble.d.ts +40 -0
- package/dist/recovery-assemble.js +1 -1
- package/dist/recovery-build.d.ts +40 -0
- package/dist/recovery-map-file.d.ts +53 -0
- package/dist/recovery-map-file.js +4 -4
- package/dist/recovery-map.d.ts +197 -0
- package/dist/recovery-release.d.ts +104 -0
- package/dist/recovery-seq.d.ts +10 -0
- package/dist/recovery-source.d.ts +50 -0
- package/dist/redact.d.ts +70 -0
- package/dist/redact.js +212 -0
- package/dist/registration.d.ts +30 -0
- package/dist/registration.js +5 -5
- package/dist/risk.d.ts +380 -0
- package/dist/risk.js +206 -0
- package/dist/run-log.d.ts +78 -0
- package/dist/run-log.js +301 -0
- package/dist/s3/listing.d.ts +47 -0
- package/dist/s3/multipart.d.ts +21 -0
- package/dist/s3/response-sink.d.ts +8 -0
- package/dist/s3/same-file.d.ts +54 -0
- package/dist/s3/server.d.ts +58 -0
- package/dist/s3/server.js +2 -2
- package/dist/s3/sigv4.d.ts +46 -0
- package/dist/s3/sigv4.js +1 -1
- package/dist/s3/staging.d.ts +10 -0
- package/dist/s3/xml.d.ts +28 -0
- package/dist/safe-path.d.ts +41 -0
- package/dist/seal.d.ts +120 -0
- package/dist/secret-reader.d.ts +28 -0
- package/dist/server.d.ts +9 -0
- package/dist/session.d.ts +32 -0
- package/dist/session.js +1 -1
- package/dist/setup-questions.d.ts +7 -0
- package/dist/share.d.ts +108 -0
- package/dist/share.js +1 -1
- package/dist/shared/lib/crypto/size-padding.d.ts +55 -0
- package/dist/shared/lib/crypto/size-padding.js +5 -0
- package/dist/shared/lib/drive/manifest-chunks.d.ts +95 -0
- package/dist/shared/lib/drive/manifest-chunks.js +252 -0
- package/dist/shared/lib/drive/manifest-codec.d.ts +214 -0
- package/dist/shared/lib/drive/manifest-codec.js +7 -7
- package/dist/shared/lib/drive/manifest-index.d.ts +98 -0
- package/dist/shared/lib/drive/manifest-ops.d.ts +167 -0
- package/dist/shared/lib/drive/manifest-ops.js +9 -4
- package/dist/shared/lib/drive/manifest-pack.d.ts +65 -0
- package/dist/shared/lib/drive/manifest-pack.js +287 -0
- package/dist/shared/lib/drive/manifest-settings.d.ts +113 -0
- package/dist/shared/lib/drive/manifest-settings.js +87 -6
- package/dist/shared/lib/drive/name-conflict.d.ts +76 -0
- package/dist/shared/lib/drive/rebuild-verify.d.ts +73 -0
- package/dist/shared/lib/drive/rebuild-verify.js +112 -0
- package/dist/shared/lib/drive/unique-name.d.ts +9 -0
- package/dist/shared/lib/drive/zstd.d.ts +27 -0
- package/dist/shared/lib/drive/zstd.js +74 -0
- package/dist/shared/lib/extend/epochs.d.ts +205 -0
- package/dist/shared/lib/net/retry-budget.d.ts +79 -0
- package/dist/shared/lib/share/shared-file-info.d.ts +21 -0
- package/dist/shared/lib/storage-control/chain.d.ts +57 -0
- package/dist/shared/lib/storage-control/chain.js +82 -0
- package/dist/shared/lib/storage-control/plan.d.ts +40 -0
- package/dist/shared/lib/storage-control/plan.js +72 -0
- package/dist/shared/lib/storage-control/reuse.d.ts +72 -0
- package/dist/shared/lib/storage-control/reuse.js +95 -0
- package/dist/shared/lib/storage-network.d.ts +25 -0
- package/dist/shared/lib/upload/part-plan.d.ts +24 -0
- package/dist/shared/lib/wallet/activity.d.ts +138 -0
- package/dist/shared/lib/wallet/activity.js +208 -0
- package/dist/shared/lib/wallet/send-rules.d.ts +68 -0
- package/dist/shared/lib/wallet/send-rules.js +116 -0
- package/dist/shared/lib/wallet/swap-rules.d.ts +115 -0
- package/dist/shared/lib/wallet/swap-rules.js +199 -0
- package/dist/shared/lib/wallet/tip.d.ts +14 -0
- package/dist/shared/lib/wallet/tip.js +34 -0
- package/dist/shared/lib/wallet/venue-ids.d.ts +23 -0
- package/dist/shared/lib/wallet/venue-ids.js +53 -0
- package/dist/standing-tip.d.ts +24 -0
- package/dist/standing-tip.js +52 -0
- package/dist/stdout.d.ts +38 -0
- package/dist/storage-control-chain.d.ts +38 -0
- package/dist/storage-control-chain.js +78 -0
- package/dist/sui-rpc.d.ts +15 -0
- package/dist/sui-rpc.js +59 -0
- package/dist/support-attach.d.ts +24 -0
- package/dist/support-attach.js +72 -0
- package/dist/support-copy.d.ts +42 -0
- package/dist/support-copy.js +109 -0
- package/dist/support-wire.d.ts +49 -0
- package/dist/support-wire.js +194 -0
- package/dist/trash-sweep.d.ts +55 -0
- package/dist/units.d.ts +2 -0
- package/dist/update-check.d.ts +50 -0
- package/dist/update-source.d.ts +63 -0
- package/dist/upload-api.d.ts +3 -0
- package/dist/upload-file.d.ts +97 -0
- package/dist/upload-file.js +3 -2
- package/dist/upload-price.d.ts +50 -0
- package/dist/upload-steps.d.ts +26 -0
- package/dist/upload-steps.js +29 -9
- package/dist/upload-store.d.ts +182 -0
- package/dist/upload-wallet-chain.d.ts +54 -0
- package/dist/upload-wallet-chain.js +203 -0
- package/dist/upload-wallet-plan.d.ts +128 -0
- package/dist/upload-wallet-plan.js +184 -0
- package/dist/upload-wallet.d.ts +24 -0
- package/dist/upload-wallet.js +208 -0
- package/dist/upload-wire.d.ts +244 -0
- package/dist/upload.d.ts +18 -0
- package/dist/upload.js +10 -0
- package/dist/usage-report.d.ts +35 -0
- package/dist/wallet-activity-chain.d.ts +5 -0
- package/dist/wallet-activity-chain.js +31 -0
- package/dist/wallet-chain.d.ts +19 -0
- package/dist/wallet-chain.js +2 -2
- package/dist/wallet-grant.d.ts +50 -0
- package/dist/wallet-grant.js +218 -0
- package/dist/wallet-send-chain.d.ts +24 -0
- package/dist/wallet-send-chain.js +49 -0
- package/dist/wallet-sign.d.ts +133 -0
- package/dist/wallet-sign.js +317 -0
- package/dist/wallet-storage-chain.d.ts +3 -0
- package/dist/wallet-storage-chain.js +20 -0
- package/dist/wallet-swap-chain.d.ts +52 -0
- package/dist/wallet-swap-chain.js +186 -0
- package/dist/wallet-swap-quote.d.ts +22 -0
- package/dist/wallet-swap-quote.js +126 -0
- package/dist/wallet.d.ts +135 -0
- package/dist/wallet.js +5 -5
- package/dist/walrus-write.d.ts +56 -0
- package/dist/walrus-write.js +5 -4
- package/dist/walrus.d.ts +86 -0
- package/dist/walrus.js +48 -10
- package/dist/zstd-node.d.ts +11 -0
- package/dist/zstd-node.js +71 -0
- package/docs/commands/accept-terms.md +15 -0
- package/docs/commands/create.md +17 -0
- package/docs/commands/credits.md +34 -0
- package/docs/commands/delete-account.md +14 -0
- package/docs/commands/deposit.md +27 -0
- package/docs/commands/devices.md +9 -0
- package/docs/commands/documents.md +10 -0
- package/docs/commands/env.md +21 -0
- package/docs/commands/extend.md +27 -0
- package/docs/commands/folders.md +10 -0
- package/docs/commands/get.md +14 -0
- package/docs/commands/key.md +14 -0
- package/docs/commands/login.md +23 -0
- package/docs/commands/logout.md +11 -0
- package/docs/commands/losses.md +13 -0
- package/docs/commands/ls.md +16 -0
- package/docs/commands/marks.md +9 -0
- package/docs/commands/mcp.md +33 -0
- package/docs/commands/mode.md +29 -0
- package/docs/commands/on-collision.md +13 -0
- package/docs/commands/padding.md +13 -0
- package/docs/commands/public-code.md +9 -0
- package/docs/commands/pull.md +9 -0
- package/docs/commands/push.md +12 -0
- package/docs/commands/put.md +36 -0
- package/docs/commands/rebuild.md +13 -0
- package/docs/commands/recovery.md +18 -0
- package/docs/commands/s3.md +17 -0
- package/docs/commands/share.md +15 -0
- package/docs/commands/support.md +16 -0
- package/docs/commands/tip.md +21 -0
- package/docs/commands/trash.md +25 -0
- package/docs/commands/trial.md +12 -0
- package/docs/commands/unlock.md +25 -0
- package/docs/commands/update.md +14 -0
- package/docs/commands/usage.md +14 -0
- package/docs/commands/verify.md +18 -0
- package/docs/commands/wallet.md +67 -0
- package/docs/commands/whoami.md +12 -0
- package/package.json +67 -8
- package/server.json +28 -0
- package/vendor/nmts-crypto/nmts_crypto_wasm.d.ts +23 -2
- package/vendor/nmts-crypto/nmts_crypto_wasm.js +44 -2
- package/vendor/nmts-crypto/nmts_crypto_wasm_bg.wasm +0 -0
- package/vendor/nmts-crypto/nmts_crypto_wasm_bg.wasm.d.ts +1 -0
- package/vendor/nmts-crypto/package.json +1 -1
- package/dist/commands/consent.js +0 -77
- package/dist/extend-sign.js +0 -130
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import type { ManifestEntry } from "./shared/lib/drive/manifest-codec.ts";
|
|
2
|
+
import { type FindOptions } from "./drive-paths.ts";
|
|
3
|
+
import { type OnCollision } from "./collision.ts";
|
|
4
|
+
import { type ManifestIntent, type SettingsPatch } from "./shared/lib/drive/manifest-ops.ts";
|
|
5
|
+
/**
|
|
6
|
+
* ⛔ THE SAME FIELD NAMES `Session` USES, so a session IS a valid input and nothing has to be
|
|
7
|
+
* translated between the two. One thing with two names is how a caller ends up passing the
|
|
8
|
+
* server where the account id goes on the day a field moves.
|
|
9
|
+
*/
|
|
10
|
+
export interface ListEditInput {
|
|
11
|
+
server: string;
|
|
12
|
+
apiKey: string;
|
|
13
|
+
code: string;
|
|
14
|
+
accountId: string;
|
|
15
|
+
}
|
|
16
|
+
export interface ListEditResult {
|
|
17
|
+
/** The version now current. */
|
|
18
|
+
seq: number;
|
|
19
|
+
/** True when the list was rebuilt because another device wrote first. */
|
|
20
|
+
reappliedAfterConflict: boolean;
|
|
21
|
+
/** False when the intent was already true of the list, so nothing was written. */
|
|
22
|
+
changed: boolean;
|
|
23
|
+
/** The list as it now stands — after the edit, or as found when nothing changed. */
|
|
24
|
+
entries: readonly ManifestEntry[];
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Apply one intent to the account's sealed file list.
|
|
28
|
+
*
|
|
29
|
+
* ⛔ THE INTENT IS WHAT IS RETRIED, NEVER FINISHED BYTES. On a lost compare-and-swap the list is
|
|
30
|
+
* read again and the intent is applied to the NEW one, so both edits survive. That is why the
|
|
31
|
+
* caller passes a function rather than an intent: an intent computed once against the old list
|
|
32
|
+
* could name a folder id, or a free name, that the new list no longer has.
|
|
33
|
+
*
|
|
34
|
+
* ⛔ AND THE INTENTS COME FROM THE BROWSER'S OWN MODULE, copied here byte-for-byte by
|
|
35
|
+
* `deploy/gen-cli-shared.mjs`. Re-implementing "send to the trash" would look trivial and be
|
|
36
|
+
* wrong in the small places: a re-trashed item must keep its ORIGINAL instant (it is the start
|
|
37
|
+
* of the 30-day window the product promises), and trashing a folder must not stamp its
|
|
38
|
+
* children (that would reset each child's own clock).
|
|
39
|
+
*
|
|
40
|
+
* `make` returning null means there is nothing to do; nothing is written and `changed` is false.
|
|
41
|
+
*/
|
|
42
|
+
export declare function applyToList(input: ListEditInput, make: (entries: readonly ManifestEntry[]) => ManifestIntent | null): Promise<ListEditResult>;
|
|
43
|
+
/**
|
|
44
|
+
* Apply a RUN of intents to the account's sealed file list — as ONE write.
|
|
45
|
+
*
|
|
46
|
+
* ⛔ ONE WRITE, NOT ONE PER TARGET. A command naming five things and writing five times is five
|
|
47
|
+
* chances to lose the compare-and-swap, and losing it half way leaves a drive nobody asked
|
|
48
|
+
* for: three things moved, two not, and one error that names neither half. The whole list is
|
|
49
|
+
* rewritten on every save anyway (see the header), so five edits cost exactly what one costs.
|
|
50
|
+
*
|
|
51
|
+
* ⛔ AND `make` DECIDES THE WHOLE RUN AGAIN ON EVERY ATTEMPT. A free name, an existing folder and
|
|
52
|
+
* a live target are all facts about the version that was READ, and a retry happens against a
|
|
53
|
+
* version somebody else has just written. A `make` that folds its own intents onto a working
|
|
54
|
+
* copy as it goes must start that fold from the list it is handed each time — never from the
|
|
55
|
+
* working copy it built on the attempt before.
|
|
56
|
+
*
|
|
57
|
+
* An empty run means there is nothing to do; nothing is written and `changed` is false.
|
|
58
|
+
*
|
|
59
|
+
* ⛔ AND THE ACCOUNT'S SETTINGS RIDE IN THE SAME WRITE. They live in this blob or nowhere (see the
|
|
60
|
+
* header), so a caller that wanted to change one and did it in a second write would spend two
|
|
61
|
+
* version bumps and two chances to lose the compare-and-swap on one edit. `patch` is DESIRED
|
|
62
|
+
* STATE per field, like every intent above, so replaying it after a lost swap lands the same
|
|
63
|
+
* answer. Absent means "carry the settings forward untouched", which is what every caller but
|
|
64
|
+
* one wants.
|
|
65
|
+
*/
|
|
66
|
+
export declare function applyManyToList(input: ListEditInput, make: (entries: readonly ManifestEntry[]) => readonly ManifestIntent[], patch?: SettingsPatch): Promise<ListEditResult>;
|
|
67
|
+
/**
|
|
68
|
+
* The entries a run of typed paths names, in the order they were typed.
|
|
69
|
+
*
|
|
70
|
+
* ⛔ HERE, BESIDE THE BATCH WRITE, so every command that takes many paths answers "the same thing
|
|
71
|
+
* named twice" the same way: once. `nmts rm a.txt a.txt` is not two deletions, and a repeated
|
|
72
|
+
* id inside one intent would make the count in the message disagree with the list written.
|
|
73
|
+
*
|
|
74
|
+
* ⛔ AND A PATH THAT DOES NOT RESOLVE REFUSES THE WHOLE RUN, because it throws from here before
|
|
75
|
+
* anything is composed. That is the decision every batch command in this tool makes: nothing
|
|
76
|
+
* is half-done. Moving the four paths that resolved and skipping the fifth would exit 0 on a
|
|
77
|
+
* command that did not do what it was told, and the caller would have to diff the drive to
|
|
78
|
+
* find out which one. A path already IN the state being asked for is not this case — that is
|
|
79
|
+
* a no-op, and each command names it in its own words.
|
|
80
|
+
*
|
|
81
|
+
* ⚠ Call it INSIDE `make`. A path is a question about the list, and the answer changes when
|
|
82
|
+
* another device writes first.
|
|
83
|
+
*/
|
|
84
|
+
export declare function batchTargets(entries: readonly ManifestEntry[], paths: readonly string[], options?: FindOptions): ManifestEntry[];
|
|
85
|
+
export interface AddEntryInput extends ListEditInput {
|
|
86
|
+
/** The entry to add. Its `name` may be changed to avoid a collision — see the result. */
|
|
87
|
+
entry: ManifestEntry;
|
|
88
|
+
/**
|
|
89
|
+
* What THIS run asked for when the name is already in use. Absent = whatever this machine is
|
|
90
|
+
* set to (`collision.ts`), which is what an ordinary upload wants.
|
|
91
|
+
*/
|
|
92
|
+
onCollision?: OnCollision;
|
|
93
|
+
}
|
|
94
|
+
export interface AddEntryResult {
|
|
95
|
+
/** The version now current. */
|
|
96
|
+
seq: number;
|
|
97
|
+
/** The name the entry actually got, which is not the requested one if that was taken. */
|
|
98
|
+
name: string;
|
|
99
|
+
/** True when the list was rebuilt because another device wrote first. */
|
|
100
|
+
reappliedAfterConflict: boolean;
|
|
101
|
+
/**
|
|
102
|
+
* The file this one displaced, when the name was taken and the answer was to overwrite.
|
|
103
|
+
*
|
|
104
|
+
* ⛔ IT IS IN THE TRASH, NOT GONE. This tool cannot destroy a stored row: the endpoint that does
|
|
105
|
+
* is closed to an API key on purpose (`item-trash.ts`). So the caller's remaining job is to
|
|
106
|
+
* tell the SERVER to trash it too — and what the tool prints must say "trash", never "gone".
|
|
107
|
+
*/
|
|
108
|
+
replaced?: {
|
|
109
|
+
id: string;
|
|
110
|
+
name: string;
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
/** What one addition turns into, worked out against the list as it stands on THIS attempt. */
|
|
114
|
+
export interface AdditionPlan {
|
|
115
|
+
/** The name it will be stored under. */
|
|
116
|
+
name: string;
|
|
117
|
+
/** Set when this id is ALREADY in the list — then `intents` is empty and nothing is written. */
|
|
118
|
+
alreadyThere?: string;
|
|
119
|
+
/** The live file this displaces, when the name was taken and the answer was to overwrite. */
|
|
120
|
+
replaced?: {
|
|
121
|
+
id: string;
|
|
122
|
+
name: string;
|
|
123
|
+
};
|
|
124
|
+
intents: ManifestIntent[];
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Decide what adding this entry does — the whole of the collision rule, with no server in it.
|
|
128
|
+
*
|
|
129
|
+
* ⛔ IT IS RE-RUN ON EVERY COMPARE-AND-SWAP ATTEMPT, so everything it looks at has to come from
|
|
130
|
+
* the `entries` it is handed. A free name, a live holder and a folder id are all facts about
|
|
131
|
+
* the version that was READ, and a retry happens against a version somebody else just wrote.
|
|
132
|
+
*
|
|
133
|
+
* ⛔ ONLY A LIVE FILE IS DISPLACED. A folder can hold the name, and replacing one would mean
|
|
134
|
+
* deleting it and everything under it in order to store a single file. A trashed file holds its
|
|
135
|
+
* name too, and displacing THAT would destroy something already on its way out for a name the
|
|
136
|
+
* person can no longer see. Both are renamed around, with no answer consulted — which is what
|
|
137
|
+
* happened to every collision before anything could be answered at all.
|
|
138
|
+
*/
|
|
139
|
+
export declare function planAddition(entries: readonly ManifestEntry[], entry: ManifestEntry,
|
|
140
|
+
/**
|
|
141
|
+
* The ANSWER, already settled — not what a run asked for.
|
|
142
|
+
*
|
|
143
|
+
* ⛔ WHO IS ALLOWED TO SAY "OVERWRITE" IS `collision.ts`'s JOB, not this one's. It weighs the
|
|
144
|
+
* machine's stored answer, what the run asked for, and whether a mode lets an agent decide
|
|
145
|
+
* for itself. Re-deriving any of that here would be a second place for the owner's rule to
|
|
146
|
+
* live, and the copy nobody re-reads is the one that quietly disagrees.
|
|
147
|
+
*/
|
|
148
|
+
choice: OnCollision, now?: number): AdditionPlan;
|
|
149
|
+
/**
|
|
150
|
+
* Add one entry to the account's sealed file list.
|
|
151
|
+
*
|
|
152
|
+
* ⛔ THE NAME IS CHOSEN AGAINST THE LIST AS IT IS ON THIS ATTEMPT. That is the reason this passes
|
|
153
|
+
* a function to `applyManyToList`: after a lost compare-and-swap the free names have changed,
|
|
154
|
+
* and a name picked against the old list could land on top of what the other device just added.
|
|
155
|
+
* The collision is judged again on every attempt for the same reason.
|
|
156
|
+
*
|
|
157
|
+
* ⛔ ONLY A LIVE FILE IS DISPLACED. A folder can hold the name, and replacing one would mean
|
|
158
|
+
* deleting it and everything under it in order to store a single file. Those are renamed, with
|
|
159
|
+
* no answer consulted, exactly as every collision was before anything could be answered at all.
|
|
160
|
+
*/
|
|
161
|
+
export declare function addEntry(input: AddEntryInput): Promise<AddEntryResult>;
|
package/dist/manifest-write.js
CHANGED
|
@@ -5,8 +5,10 @@
|
|
|
5
5
|
// sealed under a key the server does not have. So this is the last step of an upload and the
|
|
6
6
|
// one that must not be skipped after the money moved.
|
|
7
7
|
//
|
|
8
|
-
// ⛔ THE WHOLE LIST IS
|
|
9
|
-
//
|
|
8
|
+
// ⛔ THE WHOLE LIST IS DECIDED EVERY TIME, EVEN THOUGH ONLY PART OF IT IS SENT. There is no
|
|
9
|
+
// "append" on the wire: the entries this save means to leave behind are worked out in full and
|
|
10
|
+
// then packed into chunks, and only the chunks whose contents actually changed are written
|
|
11
|
+
// (NCF-3 §6.3). That is why this re-reads immediately before writing — the version it builds on
|
|
10
12
|
// has to be the current one, and anything another device added since must be carried forward,
|
|
11
13
|
// not overwritten.
|
|
12
14
|
//
|
|
@@ -17,14 +19,14 @@
|
|
|
17
19
|
// · the `prev` link inside the blob makes a fork visible to the NEXT reader on any device.
|
|
18
20
|
// The account's own settings ride along for the same reason: they live in this blob or nowhere,
|
|
19
21
|
// so rewriting the list without them would silently clear them.
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
22
|
+
import { ServerError } from "./api.js";
|
|
23
|
+
import { DERIVED, loadCrypto } from "./crypto.js";
|
|
22
24
|
import { NmtsError } from "./errors.js";
|
|
25
|
+
import { writeChunkedList } from "./manifest-chunk-flow.js";
|
|
23
26
|
import { readFileList, recordWrittenList } from "./manifest.js";
|
|
24
|
-
import { encodeManifest } from "./shared/lib/drive/manifest-codec.js";
|
|
25
27
|
import { buildIndex, entryAt, isLive, KIND_FILE, namesIn, normaliseName } from "./drive-paths.js";
|
|
26
28
|
import { decide } from "./collision.js";
|
|
27
|
-
import { applyIntents } from "./shared/lib/drive/manifest-ops.js";
|
|
29
|
+
import { applyIntents, applySettingsPatch, } from "./shared/lib/drive/manifest-ops.js";
|
|
28
30
|
import { uniqueFileName } from "./shared/lib/drive/unique-name.js";
|
|
29
31
|
/** How many times a lost compare-and-swap is re-applied before giving up. */
|
|
30
32
|
const CONFLICT_RETRIES = 3;
|
|
@@ -68,8 +70,15 @@ export async function applyToList(input, make) {
|
|
|
68
70
|
* working copy it built on the attempt before.
|
|
69
71
|
*
|
|
70
72
|
* An empty run means there is nothing to do; nothing is written and `changed` is false.
|
|
73
|
+
*
|
|
74
|
+
* ⛔ AND THE ACCOUNT'S SETTINGS RIDE IN THE SAME WRITE. They live in this blob or nowhere (see the
|
|
75
|
+
* header), so a caller that wanted to change one and did it in a second write would spend two
|
|
76
|
+
* version bumps and two chances to lose the compare-and-swap on one edit. `patch` is DESIRED
|
|
77
|
+
* STATE per field, like every intent above, so replaying it after a lost swap lands the same
|
|
78
|
+
* answer. Absent means "carry the settings forward untouched", which is what every caller but
|
|
79
|
+
* one wants.
|
|
71
80
|
*/
|
|
72
|
-
export async function applyManyToList(input, make) {
|
|
81
|
+
export async function applyManyToList(input, make, patch) {
|
|
73
82
|
const crypt = await loadCrypto();
|
|
74
83
|
const [from, to] = DERIVED.fileListKey;
|
|
75
84
|
const derived = crypt.kdf_derive(crypt.account_code_parse(input.code));
|
|
@@ -80,30 +89,40 @@ export async function applyManyToList(input, make) {
|
|
|
80
89
|
for (let attempt = 0; attempt <= CONFLICT_RETRIES; attempt += 1) {
|
|
81
90
|
const current = await readFileList(input.server, input.apiKey, input.code, input.accountId);
|
|
82
91
|
const entries = current.manifest ? current.manifest.entries : [];
|
|
92
|
+
// ⛔ THE SAME REFERENCE COMES BACK WHEN NOTHING CHANGED, which is what the no-op test below
|
|
93
|
+
// reads. `applySettingsPatch` promises that, and the empty object stands in for an
|
|
94
|
+
// account that has never written a setting so that the comparison has something to hold.
|
|
95
|
+
const held = current.manifest?.settings ?? {};
|
|
96
|
+
const settings = patch === undefined ? held : applySettingsPatch(held, patch);
|
|
83
97
|
const intents = make(entries);
|
|
98
|
+
const next = intents.length === 0 ? entries : applyIntents(entries, intents);
|
|
84
99
|
// ⛔ A no-op is a SUCCESS, not a failure. Renaming a file to the name it already has, or
|
|
85
100
|
// trashing something already in the trash, must not cost a version bump every other
|
|
86
101
|
// device then has to download.
|
|
87
|
-
if (
|
|
88
|
-
return { seq: current.seq ?? 0, reappliedAfterConflict: conflicted, changed: false, entries };
|
|
89
|
-
}
|
|
90
|
-
const next = applyIntents(entries, intents);
|
|
91
|
-
if (next === entries) {
|
|
102
|
+
if (next === entries && settings === held) {
|
|
92
103
|
return { seq: current.seq ?? 0, reappliedAfterConflict: conflicted, changed: false, entries };
|
|
93
104
|
}
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
105
|
+
const io = {
|
|
106
|
+
server: input.server,
|
|
107
|
+
apiKey: input.apiKey,
|
|
108
|
+
accountId: input.accountId,
|
|
109
|
+
crypt,
|
|
110
|
+
key,
|
|
111
|
+
};
|
|
98
112
|
try {
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
113
|
+
// ⛔ ALWAYS VERSION 2 (NCF-3 §6.3.6). A list read as version 1 has no chunks to build on,
|
|
114
|
+
// so it is packed from scratch and the index names the version-1 blob as its parent —
|
|
115
|
+
// the link crosses the version boundary unchanged, and the account is converted.
|
|
116
|
+
const written = await writeChunkedList(io, {
|
|
117
|
+
previous: current.chunks ?? [],
|
|
118
|
+
entries: next,
|
|
119
|
+
seq: (current.seq ?? 0) + 1,
|
|
120
|
+
...(current.fingerprint !== undefined ? { prev: current.fingerprint } : {}),
|
|
121
|
+
settings,
|
|
122
|
+
baseSeq: current.seq ?? null,
|
|
103
123
|
});
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
return { seq, reappliedAfterConflict: conflicted, changed: true, entries: next };
|
|
124
|
+
await recordWrittenList(input.accountId, written.seq, written.ct);
|
|
125
|
+
return { seq: written.seq, reappliedAfterConflict: conflicted, changed: true, entries: next };
|
|
107
126
|
}
|
|
108
127
|
catch (error) {
|
|
109
128
|
// ⛔ A version conflict is an ORDINARY outcome, not a failure: another device wrote first.
|
|
@@ -232,13 +251,3 @@ export async function addEntry(input) {
|
|
|
232
251
|
...(replaced ? { replaced } : {}),
|
|
233
252
|
};
|
|
234
253
|
}
|
|
235
|
-
function seqOf(answer) {
|
|
236
|
-
if (typeof answer === "object" && answer !== null) {
|
|
237
|
-
const seq = Reflect.get(answer, "seq");
|
|
238
|
-
if (typeof seq === "number" && Number.isSafeInteger(seq) && seq >= 1)
|
|
239
|
-
return seq;
|
|
240
|
-
}
|
|
241
|
-
throw new NmtsError("The file list was written but the server did not say which version it is now.", {
|
|
242
|
-
nextStep: "The entry is saved. Run `nmts ls` to see it.",
|
|
243
|
-
});
|
|
244
|
-
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import type { Manifest } from "./shared/lib/drive/manifest-codec.ts";
|
|
2
|
+
import type { HeldChunk } from "./shared/lib/drive/manifest-pack.ts";
|
|
3
|
+
import type { AccountSettings } from "./shared/lib/drive/manifest-settings.ts";
|
|
4
|
+
import type { PaddingRule } from "./shared/lib/crypto/size-padding.ts";
|
|
5
|
+
/** This machine's copy of one account's sealed file list. */
|
|
6
|
+
/**
|
|
7
|
+
* Which size-padding rule an account's settings select for what this run seals.
|
|
8
|
+
*
|
|
9
|
+
* ⛔ ONE PLACE, because four commands seal uploads. A rule read four ways is a rule some of them
|
|
10
|
+
* eventually get wrong, and the way it shows up is that an account which asked to store files
|
|
11
|
+
* at their exact size quietly pays for rounded-up bytes from one command and not another.
|
|
12
|
+
*
|
|
13
|
+
* A spelling this build does not know reads as the DEFAULT rather than as "no padding": sealing by
|
|
14
|
+
* a rule this copy cannot reproduce would give a file a size no reader here can account for.
|
|
15
|
+
*/
|
|
16
|
+
export declare function paddingRuleOf(settings: AccountSettings | undefined): PaddingRule;
|
|
17
|
+
export interface KeptList {
|
|
18
|
+
/** The version these bytes carry. Higher is newer — the same counter every device syncs by. */
|
|
19
|
+
seq: number;
|
|
20
|
+
/** When THIS MACHINE wrote the copy, RFC3339 on its own clock. */
|
|
21
|
+
savedAt: string;
|
|
22
|
+
/** The sealed blob, base64url: exactly the bytes the server served or this tool wrote. */
|
|
23
|
+
ct: string;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* The copy this machine holds for an account, or null when it holds none.
|
|
27
|
+
*
|
|
28
|
+
* ⚠ A COPY THAT CANNOT BE READ IS REPORTED AS NO COPY, on purpose. There is nothing to salvage
|
|
29
|
+
* from a truncated one, the next read of the list replaces it, and a command that refused to
|
|
30
|
+
* write out a good copy because an old one is unreadable would be refusing the very thing it is
|
|
31
|
+
* for.
|
|
32
|
+
*/
|
|
33
|
+
export declare function readKeptList(accountId: string): KeptList | null;
|
|
34
|
+
/**
|
|
35
|
+
* Record a version this machine WROTE, so the server cannot serve an older one back afterwards.
|
|
36
|
+
*
|
|
37
|
+
* ⛔ ONLY AFTER THE SERVER ACCEPTED IT. Recording a version that lost the compare-and-swap would
|
|
38
|
+
* leave this machine believing in a list that never existed — and then refusing the real one as
|
|
39
|
+
* a rollback.
|
|
40
|
+
*/
|
|
41
|
+
export declare function recordWrittenList(accountId: string, seq: number, ct: string): Promise<void>;
|
|
42
|
+
/** True when this machine has a record for the account — i.e. a rollback would be visible. */
|
|
43
|
+
export declare function hasSeenBefore(accountId: string): boolean;
|
|
44
|
+
export interface FileList {
|
|
45
|
+
/** null when the account has no list yet — a new account, not an error. */
|
|
46
|
+
manifest: Manifest | null;
|
|
47
|
+
/**
|
|
48
|
+
* base64url SHA-256 of the sealed blob this list came out of. Absent with no list.
|
|
49
|
+
*
|
|
50
|
+
* ⛔ A WRITER NEEDS IT. The next version has to name the blob it continued from, or the fork
|
|
51
|
+
* check has a hole exactly where a fork would be introduced.
|
|
52
|
+
*/
|
|
53
|
+
fingerprint?: string;
|
|
54
|
+
/** The version the sealed blob itself claims. Absent with no list. */
|
|
55
|
+
seq?: number;
|
|
56
|
+
/** What the server's column said, when it disagreed with the sealed value. */
|
|
57
|
+
serverSeqDisagreed?: number;
|
|
58
|
+
/** True when nothing on this machine could have caught a rollback. */
|
|
59
|
+
firstTimeOnThisMachine: boolean;
|
|
60
|
+
/**
|
|
61
|
+
* The sealed format this list turned out to be: 1 for the single blob, 2 for index plus chunks.
|
|
62
|
+
*
|
|
63
|
+
* ⚠ READERS ACCEPT BOTH; WRITERS WRITE 2 (NCF-3 §6.3). An account converts on its first save by
|
|
64
|
+
* a build that knows version 2, and nothing converts on read.
|
|
65
|
+
*/
|
|
66
|
+
version?: number;
|
|
67
|
+
/**
|
|
68
|
+
* Version 2 only: the chunks this list was read out of, in placement order.
|
|
69
|
+
*
|
|
70
|
+
* ⛔ A WRITER NEEDS THEM. Comparing the new entries against these is what lets a save rewrite the
|
|
71
|
+
* one chunk that changed instead of the whole list. Empty means "there are none to build on",
|
|
72
|
+
* which is both a version-1 list and an account with no items — and both pack from scratch.
|
|
73
|
+
*/
|
|
74
|
+
chunks?: readonly HeldChunk[];
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Fetch and open the account's file list.
|
|
78
|
+
*
|
|
79
|
+
* `accountCode` is used here and not kept: the file-list key is derived, used, and zeroed. The
|
|
80
|
+
* derivation output holds every other key in the account, so it does not outlive this call.
|
|
81
|
+
*/
|
|
82
|
+
export declare function readFileList(base: string, apiKey: string, accountCode: string, accountId: string): Promise<FileList>;
|
package/dist/manifest.js
CHANGED
|
@@ -16,18 +16,25 @@
|
|
|
16
16
|
//
|
|
17
17
|
// ⛔ AND THE SEALED BYTES THEMSELVES ARE KEPT, beside that record. The record alone is a detector:
|
|
18
18
|
// it can tell that a list went backwards, and it cannot hand anybody a list. The blob can — it
|
|
19
|
-
// is the account's names, folders and file keys, sealed with the
|
|
19
|
+
// is the account's names, folders and file keys, sealed with the NMTS key, and a copy of it
|
|
20
20
|
// on this machine is one of the two things a person needs when the server has nothing to give
|
|
21
21
|
// them. A tool that read the list on every run and then threw it away left an account used only
|
|
22
22
|
// from a terminal with neither. It is written with the record, by the one function that writes
|
|
23
23
|
// either, so the two can never describe different versions.
|
|
24
|
+
//
|
|
25
|
+
// ⛔ AT FORMAT VERSION 2 THOSE BYTES ARE THE INDEX, AND THE ENTRIES ARE BESIDE IT. The list is an
|
|
26
|
+
// index plus immutable chunks named by their own hash (NCF-3 §6.3); the index is kept here
|
|
27
|
+
// exactly as the single blob was, and the chunks are kept by name in the chunk store
|
|
28
|
+
// (`manifest-chunk-cache.ts`), which is pruned to what the list just read names. So the copy is
|
|
29
|
+
// still complete — it is simply in two places, and `nmts listfile` writes them out as one file.
|
|
24
30
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
25
31
|
import { join } from "node:path";
|
|
26
32
|
import { request } from "./api.js";
|
|
27
33
|
import { AAD, DERIVED, loadCrypto } from "./crypto.js";
|
|
28
34
|
import { configDir } from "./credentials.js";
|
|
29
35
|
import { NmtsError } from "./errors.js";
|
|
30
|
-
import {
|
|
36
|
+
import { openChunks } from "./manifest-chunk-flow.js";
|
|
37
|
+
import { decodeFileList, FILE_LIST_VERSION_CHUNKED, } from "./shared/lib/drive/manifest-chunks.js";
|
|
31
38
|
function asResponse(value) {
|
|
32
39
|
if (typeof value !== "object" || value === null)
|
|
33
40
|
throw new NmtsError("The server's answer was not an object.");
|
|
@@ -67,6 +74,21 @@ function keptListPath(accountId) {
|
|
|
67
74
|
}
|
|
68
75
|
return join(configDir(), `file-list-${accountId}.json`);
|
|
69
76
|
}
|
|
77
|
+
/** This machine's copy of one account's sealed file list. */
|
|
78
|
+
/**
|
|
79
|
+
* Which size-padding rule an account's settings select for what this run seals.
|
|
80
|
+
*
|
|
81
|
+
* ⛔ ONE PLACE, because four commands seal uploads. A rule read four ways is a rule some of them
|
|
82
|
+
* eventually get wrong, and the way it shows up is that an account which asked to store files
|
|
83
|
+
* at their exact size quietly pays for rounded-up bytes from one command and not another.
|
|
84
|
+
*
|
|
85
|
+
* A spelling this build does not know reads as the DEFAULT rather than as "no padding": sealing by
|
|
86
|
+
* a rule this copy cannot reproduce would give a file a size no reader here can account for.
|
|
87
|
+
*/
|
|
88
|
+
export function paddingRuleOf(settings) {
|
|
89
|
+
const mode = settings?.paddingMode;
|
|
90
|
+
return mode === "pow2" || mode === "none" ? mode : "padme";
|
|
91
|
+
}
|
|
70
92
|
function isKeptList(value) {
|
|
71
93
|
if (typeof value !== "object" || value === null)
|
|
72
94
|
return false;
|
|
@@ -186,28 +208,54 @@ export async function readFileList(base, apiKey, accountCode, accountId) {
|
|
|
186
208
|
const derived = crypt.kdf_derive(crypt.account_code_parse(accountCode));
|
|
187
209
|
const key = derived.slice(from, to);
|
|
188
210
|
derived.fill(0);
|
|
189
|
-
|
|
211
|
+
// ⛔ THE KEY LIVES UNTIL THE CHUNKS ARE OPEN. A version-2 list is an index plus sealed chunks
|
|
212
|
+
// under a label of their own, so the same key opens two kinds of blob and the zeroing has to
|
|
213
|
+
// wait for the second kind — which is why one `finally` now wraps the whole read.
|
|
190
214
|
try {
|
|
191
|
-
body
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
215
|
+
let body;
|
|
216
|
+
try {
|
|
217
|
+
body = crypt.envelope_open(key, new TextEncoder().encode(AAD.fileList), Buffer.from(answer.ct, "base64url"));
|
|
218
|
+
}
|
|
219
|
+
catch {
|
|
220
|
+
throw new NmtsError("The file list did not open with this account's key.", {
|
|
221
|
+
nextStep: "Either the NMTS key belongs to a different account, or the stored bytes are not what this " +
|
|
222
|
+
"account sealed. Nothing was changed.",
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
const doc = await decodeFileList(body);
|
|
226
|
+
body.fill(0);
|
|
227
|
+
const io = { server: base, apiKey, accountId, crypt, key };
|
|
228
|
+
// A version-1 blob carries its own entries; a version-2 index names chunks that have to be
|
|
229
|
+
// fetched, checked against the names it gave them, and read end to end (§6.3.2).
|
|
230
|
+
const chunks = doc.v === FILE_LIST_VERSION_CHUNKED ? await openChunks(io, doc.index) : [];
|
|
231
|
+
const manifest = doc.v === FILE_LIST_VERSION_CHUNKED
|
|
232
|
+
? {
|
|
233
|
+
v: FILE_LIST_VERSION_CHUNKED,
|
|
234
|
+
seq: doc.index.seq,
|
|
235
|
+
...(doc.index.p !== undefined ? { prev: doc.index.p } : {}),
|
|
236
|
+
entries: chunks.flatMap((c) => [...c.items]),
|
|
237
|
+
...(doc.index.settings !== undefined ? { settings: doc.index.settings } : {}),
|
|
238
|
+
}
|
|
239
|
+
: doc.manifest;
|
|
240
|
+
const out = {
|
|
241
|
+
manifest,
|
|
242
|
+
seq: manifest.seq,
|
|
243
|
+
fingerprint: fp,
|
|
244
|
+
firstTimeOnThisMachine: first,
|
|
245
|
+
version: doc.v,
|
|
246
|
+
chunks,
|
|
247
|
+
};
|
|
248
|
+
// ⛔ The sealed number is the authenticated one, so it is what gets recorded and what a later
|
|
249
|
+
// run compares against. The column is reported when it differs and otherwise ignored.
|
|
250
|
+
if (manifest.seq !== answer.seq)
|
|
251
|
+
out.serverSeqDisagreed = answer.seq;
|
|
252
|
+
// ⛔ THE VERSION THAT IS KEPT IS THE SEALED ONE, and so are the bytes it came out of. Believing
|
|
253
|
+
// the server's column here would let it decide which copy this machine keeps. The bytes are
|
|
254
|
+
// the INDEX at version 2; its chunks are kept beside it, by name, in the chunk store.
|
|
255
|
+
writeSeen(accountId, manifest.seq, fp, answer.ct);
|
|
256
|
+
return out;
|
|
198
257
|
}
|
|
199
258
|
finally {
|
|
200
259
|
key.fill(0);
|
|
201
260
|
}
|
|
202
|
-
const manifest = await decodeManifest(body);
|
|
203
|
-
body.fill(0);
|
|
204
|
-
const out = { manifest, seq: manifest.seq, fingerprint: fp, firstTimeOnThisMachine: first };
|
|
205
|
-
// ⛔ The sealed number is the authenticated one, so it is what gets recorded and what a later
|
|
206
|
-
// run compares against. The column is reported when it differs and otherwise ignored.
|
|
207
|
-
if (manifest.seq !== answer.seq)
|
|
208
|
-
out.serverSeqDisagreed = answer.seq;
|
|
209
|
-
// ⛔ THE VERSION THAT IS KEPT IS THE SEALED ONE, and so are the bytes it came out of. Believing
|
|
210
|
-
// the server's column here would let it decide which copy this machine keeps.
|
|
211
|
-
writeSeen(accountId, manifest.seq, fp, answer.ct);
|
|
212
|
-
return out;
|
|
213
261
|
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { ManifestEntry } from "./shared/lib/drive/manifest-codec.ts";
|
|
2
|
+
/** The three marks, in the shape `ls --json` prints and `markSuffix` draws. */
|
|
3
|
+
export interface EntryMarks {
|
|
4
|
+
/** Starred: shown in the drive's favourites as well as in the folder the file lives in. */
|
|
5
|
+
favorite: boolean;
|
|
6
|
+
/** Held at the top of its own folder's listing, whatever the sort says. */
|
|
7
|
+
pinned: boolean;
|
|
8
|
+
/** The person's own labels for this entry, as text. Empty when it wears none. */
|
|
9
|
+
labels: readonly string[];
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* The marks on one entry — always all three.
|
|
13
|
+
*
|
|
14
|
+
* ⚠ ABSENT IS `false` HERE AND ABSENCE IN THE LIST, and the difference is deliberate. The format
|
|
15
|
+
* writes a mark only when it is on, because the whole list is re-sealed on every change and
|
|
16
|
+
* re-downloaded on every cold start. A reader parsing this needs the opposite: a field that is
|
|
17
|
+
* always there, so "this file is not starred" cannot be mistaken for "this output does not say".
|
|
18
|
+
*/
|
|
19
|
+
export declare function marksOf(entry: ManifestEntry): EntryMarks;
|
|
20
|
+
/**
|
|
21
|
+
* What `ls` puts after a row, or an empty string when the entry wears no mark.
|
|
22
|
+
*
|
|
23
|
+
* ⚠ It carries its own leading spaces, exactly like the trash suffix beside it, so a row with no
|
|
24
|
+
* marks is byte-for-byte the row that was printed before marks existed.
|
|
25
|
+
*/
|
|
26
|
+
export declare function markSuffix(marks: EntryMarks): string;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Keywords a schema uses that this checker does not enforce.
|
|
3
|
+
*
|
|
4
|
+
* ⛔ THE POINT IS THAT IT IS NOT EMPTY-BY-ASSUMPTION. A checker that silently skips what it does
|
|
5
|
+
* not understand still returns "no problems", and a schema that grew a `minimum` or a `oneOf`
|
|
6
|
+
* would go on being advertised while nothing held it. A test compares this against the real
|
|
7
|
+
* tool table, so growing a schema past this file turns something red.
|
|
8
|
+
*/
|
|
9
|
+
export declare function unsupported(schema: unknown): string[];
|
|
10
|
+
/**
|
|
11
|
+
* Compare one call's arguments against the schema its tool advertised.
|
|
12
|
+
*
|
|
13
|
+
* Returns the problems, most important first: a missing required argument before a wrong type,
|
|
14
|
+
* because a caller that forgot one is usually about to be told about the other for the same reason.
|
|
15
|
+
* An empty array means the call may proceed.
|
|
16
|
+
*/
|
|
17
|
+
export declare function checkArgs(schema: unknown, args: unknown): string[];
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/** What came back, once the three protocol actions are read for what they mean here. */
|
|
2
|
+
export type AskOutcome =
|
|
3
|
+
/** The person said yes. */
|
|
4
|
+
"yes"
|
|
5
|
+
/** The person said no, or dismissed the question without answering. Both mean: do not proceed. */
|
|
6
|
+
| "no"
|
|
7
|
+
/** There was nobody to ask — the client never said it could. */
|
|
8
|
+
| "unreachable";
|
|
9
|
+
/** Sends one JSON-RPC request to the client and resolves with whatever comes back as its result. */
|
|
10
|
+
export type SendRequest = (method: string, params: Record<string, unknown>) => Promise<unknown>;
|
|
11
|
+
/**
|
|
12
|
+
* The question this server asks, in one place so that every caller asks it the same way.
|
|
13
|
+
*
|
|
14
|
+
* ⛔ ONE BOOLEAN, NOT A FREE-TEXT FIELD. A client renders this schema into whatever it renders it
|
|
15
|
+
* into, and a checkbox somebody has to tick is the shape that survives every rendering. A text
|
|
16
|
+
* field would let a client accept "no" as a filled-in answer.
|
|
17
|
+
*/
|
|
18
|
+
export declare const CONFIRM_SCHEMA: Readonly<Record<string, unknown>>;
|
|
19
|
+
/**
|
|
20
|
+
* Read an `elicitation/create` result. Pure, so the three actions can be tested without a pipe.
|
|
21
|
+
*
|
|
22
|
+
* ⛔ ONLY `accept` WITH `confirm === true` IS A YES. `decline` and `cancel` are both no — the
|
|
23
|
+
* specification distinguishes "refused" from "dismissed" so that a server can offer something
|
|
24
|
+
* else, and here there is nothing else to offer. An `accept` carrying no content, or content
|
|
25
|
+
* with the box unticked, is a no as well: the person was shown the question and did not agree.
|
|
26
|
+
*
|
|
27
|
+
* ⛔ ANYTHING UNRECOGNISED IS A NO. A malformed answer is not an answer, and the direction to fail
|
|
28
|
+
* in is the one where a file is not handed to somebody.
|
|
29
|
+
*/
|
|
30
|
+
export declare function readAnswer(result: unknown): AskOutcome;
|
|
31
|
+
/** Whether the client said, at `initialize`, that it can put a question in front of a person. */
|
|
32
|
+
export declare function declaredElicitation(capabilities: unknown): boolean;
|
|
33
|
+
/** How a session asks, or `null` when this session has no way to ask at all. */
|
|
34
|
+
export type Asker = ((message: string) => Promise<AskOutcome>) | null;
|
|
35
|
+
/**
|
|
36
|
+
* Build the asker for a session.
|
|
37
|
+
*
|
|
38
|
+
* ⛔ THE CAPABILITY IS READ ONCE, FROM `initialize`, AND NEVER GUESSED AFTERWARDS. A client that
|
|
39
|
+
* did not declare elicitation is not sent one: the specification says a server may only use a
|
|
40
|
+
* capability the other side declared, and a request it does not understand is at best an error
|
|
41
|
+
* on the wire and at worst a hung tool call waiting for an answer that is never coming.
|
|
42
|
+
*/
|
|
43
|
+
export declare function askerFor(capabilities: unknown, send: SendRequest): Asker;
|