npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@tacet-ink/journal-core

v0.3.1

Published

Zero-knowledge journal core — e2e crypto, passphrase-only auth, transport, ratelimit. The server never sees plaintext.

Readme

@tacet-ink/journal-core

npm license node verify

零知識日記核心:端對端加密、密語即身份的 auth、傳輸與限流原語。 伺服器看不到任何一個字:passphrase 永不過線,密鑰包裹與密文都在客戶端完成。

English: Zero-knowledge journaling core — E2E-encrypted notes, passphrase-as-identity auth, transport & rate-limiting primitives. The server never sees a single word: the passphrase never crosses the wire, and all key wrapping and encryption happen client-side. See the English summary below.

這是 默·Tacet(tacet.ink) 的客戶端/伺服端核心原語層, 從多個姊妹產品共用的密碼學本體抽取而成,以品牌前綴參數化(前綴契約見下表)。

Quickstart

git clone https://github.com/tacet-ink/journal-core.git && cd journal-core
npm ci --ignore-scripts
npm run verify   # 506 assertions, all green, run against the real modules (no mocks)
// example.ts
import { makeHeldKey, generateNoteKey, encryptNote, decryptNote } from './src/index.ts';
import { makeKeyStore } from './src/client/keys.ts';

const cfg = {
  guestKdfPrefix: 'myapp-note-u1',
  recSaltPrefix: 'myapp-note-rec1:',
  cipherGuest: 'jr1g.',      // your own brand prefix family (guest era, opt-in)
  cipherBound: 'jr1b.',
  wrap: 'jr1w.',
  store: makeKeyStore({ brand: 'myapp' }),
};

const held = makeHeldKey();
held.set(await generateNoteKey());             // bound era: random 256-bit noteKey
const identity = { current: () => 'acct-xxxxxxxxxxxxxxxx' };

const cipher = await encryptNote(cfg, held, '今天寫了一點東西。', 'noteId:n1', identity);
console.log(cipher.slice(0, 5));  // 'jr1b.' — ciphertext; nobody reads it without the key
console.log(await decryptNote(cfg, held, cipher, 'noteId:n1', identity));  // '今天寫了一點東西。' — roundtrip

run: node --experimental-strip-types example.ts

完整 API 見使用(Usage)節。Live product: https://tacet.ink (驗證頁:https://tacet.ink/verify)。 安全回報流程見 SECURITY.md、版本歷史見 CHANGELOG.md。

設計(Design)

  • 兩時代金鑰模型:未綁定(guest)時代以 K_u = SHA-256(guestKdfPrefix ‖ identity) 純客戶端派生(混淆級);綁定後改用隨機 256-bit noteKey 加密, noteKey 再以 passphrase(KEK)與復原套件(KEK_rec)雙重包裹上傳。 此後 passphrase 不再過線:線上只送 PH1(雜湊形),伺服器存 PH2 = SHA-256(PH1)。
  • 帶內版本化:升級 KDF 參數 = 換新前綴(舊前綴照解、原地改語意是禁手)。 前綴全部可配置,AAD(防搬移綁結)由呼叫端傳入。
  • extractable 鐵律:要被 exportKey/wrap 的 key(noteKey 全部產生路徑), import 當下就必須 extractable=true;KEK/guest key 恆 nonextractable。
  • opt-in 原語:選配契約(cipherGuest/wrapDual/wrapShare/pinLock/cipherAttach/cipherLocal/ wrap4/wrapDual4/wrapLocal/wrapRec+recKekHkdf(兩欄一體——同缺同在,部分配置拒寫)/recKekSalt)未配置=回落語意(wrapRec 系照走舊契約面;其他面拒絕或退場),各 fork 未選用的原語行為不受影響(cipherGuest 未配置時 bound 路徑不受牽連, 解密面 guest 家族整面拒絕、不明字串與畸形空字串配置不當明文顯示)。wrapLocal 為選配: 未配置=寫面功能退場——v1 可用者(借用期部署)升級後需配置 wrapLocal 才保留本機包裹寫入 (讀取面照走舊形回落,零資料損失面)。

模組(Modules)

| 模組 | 內容 | | --- | --- | | src/client/note-crypto.ts | 兩時代加解密、passphrase/復原套件/本機三路包裹、PIN 第二因子合鑰(jr2w.) | | src/client/argon2.ts | Argon2id 包裹原語(jr3w./jr3d./jr3s.+密語正規化 v3 世代 jr4w./jr4d.)、PH1 v2/v3 派生、RFC 9106 KAT、雙載體(node:crypto+hash-wasm) | | src/client/pinlock.ts | 本機 PIN 鎖定包裹(jr1p.,開啟時鎖定) | | src/client/bip39.ts | 復原套件 24 詞 ⇄ hex64 轉寫層(BIP39,零依賴自製) | | src/client/keys.ts | 品牌前綴 localStorage 命名空間 | | src/server/auth.ts | 零知識 auth 核心:PH1→PH2 login 動線、入庫格式驗證、包裹欄組成對(pair 檢查 pickKeyPackage);PH2 UNIQUE 衝突與 session 撤銷=呼叫端職責 | | src/server/ladder.ts | 密語正規化 v3 世代 ladder 表原語(ph2_ladder:以舊 ph2 查列/遷移入表 upsert 值主權威)+幽靈守衛查表接線已內建 loginRouteCore——fork 只需配 ladderLookup store+0011 migration(未配置=單查行為零變) | | src/server/ratelimit.ts | per-IP fixed-window 限流(單句 UPSERT…RETURNING;D1 計數,跨 isolate 有效) | | src/server/cors.ts/hash.ts | 共用 CORS/雜湊工具(src/server/env.ts 為內部 Env 介面,不入 exports) |

發行形(0.3.0 起):dist/=tsc 原生 emit 的 ESM .js+.d.ts,exports 全部子路徑指向 dist; src/ 同包隨附(稽核/閱讀用,import 恆走 dist)。

前綴契約(家族表:13 個資料前綴+閘對照組 jr1g.)

前綴是版本契約:payload 佈局與 KDF 由前綴界定,升級 = 新前綴。 逐位元組布局、KDF 參數、AAD 與世代契約的完整規格見 docs/format-spec.md, 測試向量見 docs/vectors/(真原語產生的凍結證據;閘 [26] 檢存在性與 shape)。 Tacet 部署實例(config 傳入):

| 前綴 | 語意 | KDF | payload | | --- | --- | --- | --- | | jr1u. | guest 時代密文 | K_u = SHA-256(guestKdfPrefix ‖ identity) | AES-GCM | | jr1b. | 綁定時代密文 | 隨機 256-bit noteKey | AES-GCM,AAD 綁 noteId | | jr1c. | 附件密文(image attachments;選配) | 同筆記金鑰(noteKey/guest key 由呼叫端決定) | AES-GCM,AAD jr1a:<noteId>:<attachId> | | jr1d. | 本機 IDB stored 密文(notes store;選配) | 同筆記金鑰(呼叫端注入) | AES-GCM,AAD 綁 note_id,payload 自帶 v 欄 | | jr1l. | 本機包裹(PWA session 期免重打密語;選配) | deriveGuestKey=K_u 同 guest 面(identity 派生、passphrase-free) | 同 jr1w. 形,AAD notekey-local | | jr1r. | 復原套件包裹(wrappedRec 專用;v0.2.0 世代,選配,與 recKekHkdf 兩欄一體) | HKDF-SHA256(ikm=recToken, salt=recKekSalt?, info=journal-kek-rec-v1:+recKekHkdf)——兩欄齊備才寫 jr1r. 面 | 同 jr1w. 形,AAD notekey-rec(部分配置拒寫;舊契約面 jr1w.+PBKDF2+AAD notekey 永不變——新世代讀舊寫新雙試) | | jr1w. | passphrase 包裹+復原套件包裹(未配置世代) | PBKDF2-SHA256 600k(復原套件 KEK=PBKDF2(recToken, recSaltPrefix‖identity)) | b64(iv[12] ‖ GCM(hex(noteKey))),AAD notekey(復原套件與 passphrase 同 AAD 慣例=未配置世代借形) | | jr2w. | PIN 第二因子合鑰(PBKDF2 版) | PBKDF2 600k(pass)+2M(pin) → HKDF-SHA256 | pinSalt[16] ‖ iv[12] ‖ GCM,108B,AAD notekey2 | | jr3w. | passphrase 包裹(Argon2id 版) | Argon2id m=64MiB t=3 p=1 tag=32B | 同 jr1w. 形,AAD notekey | | jr3d. | PIN 第二因子合鑰(Argon2id 版) | Argon2id(pass)+Argon2id(pin) → HKDF-SHA256 | 同 jr2w. 形,108B,AAD notekey2 | | jr4w. | passphrase 包裹(密語正規化 v3 世代) | Argon2id(normalizePassphrase(pass),NFKC-only) 同上參數 | 同 jr1w./jr3w. 形,AAD notekey | | jr4d. | PIN 第二因子合鑰(密語正規化 v3 世代) | Argon2id(normalizePassphrase(pass))+Argon2id(pin) → HKDF-SHA256 | 同 jr2w./jr3d. 形,108B,AAD notekey2 | | jr3s. | 單篇分享連結包裹 | Argon2id(同上參數) | 同 jr1w. 形,AAD notekey-share | | jr1p. | 本機 PIN 鎖定(開啟時鎖定) | PBKDF2-SHA256 600k(刻意不用 Argon2id:解鎖要即時) | 同 jr2w. 形,AAD notekey-pinlock |

guest 密文前綴在閘對照組另驗 jr1g.(閘自造前綴,驗證品牌參數化本身;tacet 部署實例 guest 用 jr1u.)。 跨前綴呼叫一律回 null(前綴守衛+AAD+長度把關),不拋、不降級。

世代命名兩軸(jr4w./jr4d. 起):前綴數字軸=KDF 世代代(jr1w→jr2w→jr3w→jr4w, KEK 輸入契約改變即換世代);「v3 家族」文案=PH1 規格代(PH1 v3 用 derivePh1ArgonV3)。 密語正規化 v3 世代=KEK 吃 normalizePassphrase(NFKC-only:不 trim、不分大小寫—— 大小寫摺疊禁絕=大小寫差恆不同 KEK/ph2)—— 舊前綴家族(jr1w./jr3w./jr2w./jr3d.)契約面永不變(raw 密語派生,帶內版本化禁原地改語意); 跨前綴呼叫恆 null(家族隔離)。同密語寫法差(全形/NFD/NFC)在 v3 世代收容為同一 KEK/PH2, 首尾空白與大小寫仍刻意分流(v3 契約只收容寫法差)。 jr4d. 的 KEK2 HKDF info 域='journal-kek2-v1:' + wrapDual4(自有域——NFKC-effective 密語下 jr3d/jr4d 兩入參數同輸入,域分離由 info 承載;凍結 KAT 兩 blob 互解 null 為證)。

驗證(Verification)

npm ci --ignore-scripts
npm run verify   # 506 斷言對真模組(禁鏡像;限流單句 UPSERT…RETURNING 直載真 SQLite):roundtrip/AAD 防搬移/
                 # extractable/時代隔離/跨前綴家族隔離/payload 竄改/RFC 9106 KAT/BIP39 @scure 對照 200 組/
                 # server-side(inboundCipher/validWrappedKey/pickKeyPackage/checkRate/timingSafeEq)+ pinlock 全函式
                 # + 密語正規化 v3 世代(normalizePassphrase/jr4w./jr4d./PH1 v3 鹽域分離+帶內版本化舊契約向量)
                 # + 本機包裹專用前綴(wrapLocal=jr1l. opt-in+讀舊寫新自癒)
                 # +復原套件專用前綴與 HKDF 世代 [20](wrapRec=jr1r.+recKekHkdf opt-in+讀舊寫新雙試+KAT)
                 # +loginRouteCore ladder 查表守衛 [23](外審 #8 幽靈帳:現值 miss → ladder → 兩面 miss 才建幽靈;真 node:sqlite 直驅七情境+拷貝樹毒化接線摘除案)
                 # +ladder 表與重取鹽面 [19](ph2_ladder 原語/upsert 值主權威行為面/unwrapNoteKeyDual4WithSalt)
                 # +常駐毒化矩陣 [16][18](/tmp 拷貝樹突變重跑=毒化證據隨每執行重建)
                 # +KAT 凍結向量 [17](HKDF info 域世代分離+v3/raw 入口契約四 blob)
                 # +包裹前綴快檢 [13-2](錯前綴試探免付 KDF:五入口行為+計時帽+計數錨)
                 # +CI 供應鏈面 [21](npm ci --ignore-scripts+dependabot runner fromJSON 分流+腳本摘除+NIT-1 錨+r2 MINOR-1 URL 直傳;
                 #   506 帳=repo 樹——發行包內無 .github=[21] 四條 workflow 錨收斂為 1 顯性 SKIP 行→503,SKIP 顯形非靜默)
                 # +發行 tarball 治理面 [22](pack 白名單零 open-card-pr.sh)
                 # +格式規格+測試向量 [26](docs/format-spec.md+docs/vectors 存在性/位元組 shape/欄位名對規格/凍結 KAT 逐字對閘)
                 # +高階 Vault API [27](roundtrip 三形/新舊前綴解鎖/upgrade/換密語/復原/VaultError 毒化/載體覆寫+Worker 回退鏈/vendor 孿生)
  • 產品碼零執行時依賴(WebCrypto 原語);devDependencies 僅閘用(typescript、@scure/bip39 對照、workers types)。
  • node_modules 出廠態說明:本 repo 的 @scure/bip39/@noble/hashes 由閘的 vendor 修復 腿(scripts/prepare-core-pkg.cjs,plain npm ci 後執行)補齊 symlink 與 vendored 副本—— 與 devDependencies 安裝態等價(provenance 註記:現場樹的非 pristine 安裝形態屬此機制, 非手工污染;reproduce 走 plain npm ci 後跑 prepare 腿即收斂)。
  • Argon2id 雙載體:node 走 node:crypto(Node 24.7+ 原生)、瀏覽器走 hash-wasm(wasm 內嵌), RFC 9106 標準向量逐位元一致;無載體即 throw(禁 fallback 鐵律)。
  • BIP39 原語自製零依賴,與 @scure/bip39 參照 200 組雙向對照(僅 entropy↔words 轉寫層,禁用其 seed 派生)。

使用(Usage)

0.3.0 起發行 dist/(ESM .js+.d.ts):npm 安裝後瀏覽器、Node、Deno、Bun、Workers 皆可直接 import,不需打包器、不需 strip-types(0.2.x 的「npm 安裝必走打包器」限制已退役)。 源碼 clone 仍可直接 node --experimental-strip-types 跑 src/。

import { makeKeyStore, makeHeldKey, generateNoteKey, encryptNote, decryptNote } from '@tacet-ink/journal-core';
// 或子路徑:'@tacet-ink/journal-core/client/note-crypto'
import { generateNoteKey, encryptNote, decryptNote, makeHeldKey,
         wrapNoteKey, unwrapNoteKey } from '@tacet-ink/journal-core/client/note-crypto';
import { makeKeyStore } from '@tacet-ink/journal-core/client/keys';
import { derivePh1Argon } from '@tacet-ink/journal-core/client/argon2';

const cfg = {
  guestKdfPrefix: 'myapp-note-u1',
  recSaltPrefix: 'myapp-note-rec1:',
  cipherGuest: 'jr1g.',      // 換成你自己的品牌前綴家族
  cipherBound: 'jr1b.',
  wrap: 'jr1w.',
  wrapLocal: 'jr1l.',       // 選配:本機包裹專用前綴(未配置 = 讀舊形回落、寫面退場)
  // 復原套件 v0.2.0 世代(選配):wrapRec+jr1r. 專用前綴+recKekHkdf HKDF 派生
  // (info='journal-kek-rec-v1:'+recKekHkdf;recKekSalt 16B hex 鹽域)。兩欄一體:
  // 要開就兩欄一起配(部分配置=ERR_REC_CFG_PARTIAL 拒寫)。兩欄皆缺席 =
  // 行為不變態(jr1w.+PBKDF2+AAD 'notekey' 舊契約面照走,舊 blob 永遠可解)。
  store: makeKeyStore({ brand: 'myapp' }),
  // cipherGuest / wrapDual / wrapShare / pinLock / cipherAttach / cipherLocal:選配,未配置即拒絕
};

const held = makeHeldKey();
const identity = { current: () => 'acct-xxxxxxxxxxxxxxxx' };
const cipher = await encryptNote(cfg, held, '今天寫了一點東西。', 'noteId:n1', identity);

PH1 派生(登入憑證):derivePh1Argon(pass, saltArg?) 預設鹽 PH1_V2_SALT ('tacet-ph1-v1',Tacet 實例,raw pass)。其他產品可自選固定域鹽傳入 saltArg—— 鹽一經選定即 per-product 恆固定(PH2 UNIQUE 約束:同一密語必須恆生同一 PH2; per-user 鹽會摧毀幽靈帳號機制)。改鹽=帶內版本化換鹽尾碼重遷移,禁原地改語意。

PH1 v3(密語正規化世代):derivePh1ArgonV3(pass, saltArg?) 預設鹽 PH1_V3_SALT ('tacet-ph1-v2',新鹽域=世代 ph2 命名空間分離——同鹽域會讓 v3 'PASS' 與 v2 世代 raw 'PASS' 撞同一 ph2)。派生輸入吃 normalizePassphrase(NFKC-only;大小寫摺疊禁絕= 大小寫差恆不同 ph2);v2/legacy 派生面零變更(舊帳戶憑證 raw 契約)。兩代 ph2 可並存查表= tacet 遷移層職責(v0.2.0 世代收口)。

server 登入動線(fork 端):loginRouteCore(request-ish, store, ip, ph1) 內建 ladder 查表守衛——現值 miss 時經 AuthStore.ladderLookup(optional)查 ph2_ladder, 命中回舊帳不建幽靈、兩面 miss 才 createUser;未配置=行為與 0.2.3 單查恆等。 接法:store 實作 ladderLookup(tacet 參考形=SQL_FIND_PH2_LADDER_BY_OLD+fail-open catch)+套用 migrations/0011-ph2-ladder.sql(+0012 索引修正);查表零寫入面 (insert 在 rekeyWithLadder 遷移線、delete 在 fork 的 delete-account 批)。 ⚠️ 鹽撞位警告:PH1_V3_SALT 禁當 derivePh1Argon 的 saltArg 餵入(反之亦然)—— 把 v3 鹽域值餵 v2 派生(或 fork 把同一 saltArg 帶到兩面)=NFKC-effective 密語與 v2 raw 密語 同值撞 ph2,跨世代帳戶空間混合。v3 的 saltArg 必須異於本 product 的 v2 鹽域值。

例外契約(錯誤碼語意)

客戶端(client/)

| 錯誤碼 | 拋出面 | | --- | --- | | ERR_NO_IDENTITY | guest 加密面空 identity(解密面同面回 null) | | ERR_GUEST_NOT_CONFIGURED | encryptNote 未配置 cipherGuest | | ERR_BAD_HEX | hexToBytes 非法 hex(奇數長度/非 hex 字元/空字串) | | ERR_WRAP_NOT_CONFIGURED | 包裹未配置(sealNoteKey 本體+wrapNoteKey 入口雙防線) | | ERR_WRAPTAMPER_LEN32 | sealNoteKey noteKey 非 32B(內部契約面) | | ERR_DUAL_NOT_CONFIGURED | wrapNoteKeyDual 未配置 wrapDual/pinSaltPrefix | | ERR_SHARE_NOT_CONFIGURED | wrapNoteKeyShare 未配置 wrapShare | | ERR_LOCAL_NOT_CONFIGURED | encryptLocal 未配置 cipherLocal | | ERR_ATTACH_NOT_CONFIGURED | encryptAttach 未配置 cipherAttach | | ERR_PIN_EMPTY | wrapNoteKeyDual/3/4 空 PIN(≠ 未配置——語意分離) | | ERR_PINLOCK_NOT_CONFIGURED | pinlock 未配置三欄任一 | | ERR_PINLOCK_EMPTY | pinlock 空 PIN | | ERR_REC_CFG_PARTIAL | wrapRec/recKekHkdf 部分配置(配對門拒寫) | | ERR_REC_KEK_SALT | recKekSalt 非 16B hex(HKDF 世代鹽域 fail-closed) | | ERR_JR3W_NOT_CONFIGURED | wrapNoteKey3/wrapNoteKeyDual3(+share 面)未配置 | | ERR_JR3S_NOT_CONFIGURED | wrapNoteKeyShare3 未配置 wrapShare3 | | ERR_JR4W_NOT_CONFIGURED | wrapNoteKey4 未配置 wrap4 | | ERR_JR4D_NOT_CONFIGURED | wrapNoteKeyDual4 未配置 wrapDual4 | | ERR_ARGON2_TAGLEN | Argon2id 載體 tag 非 32B(載體契約面) | | ERR_ARGON2_UNAVAILABLE | 無 Argon2id 載體(node<24.7 且無 hash-wasm 注入——禁 fallback) |

伺服端(server/)+閘

| 錯誤碼 | 拋出面 | | --- | --- | | ERR_BAD_REQUEST | auth 動線缺 PH1 等必填(400 面回應) | | ERR_RATE_LIMITED | 限流命中(429 面回應) | | ERR_RATE_TABLE_NAME | 限流表名非法字元(SQL 面 fail-closed) | | ERR_SQLITE_UNAVAILABLE | 驗證閘環境 node:sqlite 缺席 | | ERR_FS_UNAVAILABLE | 驗證閘環境 node:fs 缺席 |

  • PinLockConfig:解包輸出的 noteKey 恆 extractable=true(鐵律,非可調選項)—— config 只帶三個前綴欄(pinLock/pinLockSaltPrefix/pinLockAad),缺任一即拒。

Argon2id 瀏覽器載體需另裝 hash-wasm 並以 setArgonLoader() 注入(詳 src/client/argon2.ts 檔頭); Node ≥ 24.7 走內建 node:crypto 免注入。

界限(誠實面)

  • guest 時代是混淆級不是 e2e(K_u 由 identity 可派生);e2e 承諾從綁定(隨機 noteKey)起。
  • PH1 派生方式由各產品自定義(快雜湊或 Argon2id 派生皆可),本層只驗 hex64 形。
  • 本 repo 不含產品層:路由/配額/訂閱/GC/webhook 在各產品 fork。
  • 未經第三方正式稽核;密語遺失設計上無救援(伺服器零知識的代價)。

授權

版本歷史見 CHANGELOG.md。

MIT。安全問題聯絡 [email protected](安全回報流程見 SECURITY.md)。

English Summary

What this is. @tacet-ink/journal-core is the cryptographic and server-primitive layer powering 默·Tacet (tacet.ink), a zero-knowledge journal: the server stores only ciphertext and salted hashes, and never learns your passphrase or the content of any note.

Security model.

  • Two key eras. Before binding an account, notes are encrypted with a client-derived key K_u = SHA-256(guestKdfPrefix ‖ identity) (obfuscation-grade, not true E2E). After binding, notes are encrypted with a random 256-bit noteKey. The noteKey is wrapped twice — with the passphrase (KEK) and with a recovery package (KEK_rec) — before upload. From that point the passphrase never crosses the wire: only PH1 (a client-side hash form) is transmitted, and the server stores only PH2 = SHA-256(PH1).
  • In-band versioning. Payload layout and KDF are defined by configurable cipher prefixes (e.g. jr1b. for bound-era ciphertext, jr3w. for Argon2id-wrapped note keys). Upgrading KDF parameters always means a new prefix; old prefixes keep decrypting forever (no in-place semantic changes). Cross-prefix calls always return null — prefix guard + AAD + length checks; they never throw or fall back. The passphrase-normalization generation follows the same rule: jr4w./jr4d. wrap with normalizePassphrase (NFKC-only — no trim, no case-folding); older prefixes keep deriving from the raw passphrase, so every legacy payload stays decryptable as-is.
  • Tamper-evidence. Every AES-GCM operation binds additional authenticated data (AAD) — e.g. ciphertext is bound to its note id, so records cannot be shuffled between notes.
  • Key hygiene. Any key that must be exported/wrapped is imported with extractable=true; KEK and guest keys are permanently non-extractable.

Server login wiring. loginRouteCore(store, ip, ph1) ships with the ladder guard built in (v0.2.4, external-review #8 ghost-account fix): on a current-value (PH2) miss it consults the ph2_ladder table via the optional AuthStore.ladderLookup — a ladder hit returns the existing account (no ghost creation); a ladder miss, an unconfigured lookup, or a ladder-store error (fail-open, treated as a miss) all fall through to the legacy single-lookup behavior of ≤0.2.3. Forks adopt the guard by configuring ladderLookup on their store (the tacet reference: d1AuthStore) and applying migrations/0011-ph2-ladder.sql — the lookup itself never writes (inserts stay on the rekeyWithLadder migration path, deletes on the fork's delete-account flow).

Opt-in primitives. Optional contracts (cipherGuest / wrapDual / wrapShare / pinLock / cipherAttach / cipherLocal / wrap4 / wrapDual4 / wrapLocal / wrapRec (paired with recKekHkdf: configured together or not at all; partial configuration is rejected on write) / recKekHkdf / recKekSalt): the wrapRec family falls back to the legacy face when unconfigured, so forks that don't use a primitive are unaffected. Unknown or malformed strings never decrypt to plaintext. wrapLocal is opt-in: unconfigured means the write face retires — v1-era deployments (borrow-period) must configure wrapLocal after upgrading to keep local key-wrap writes (the read face still falls back to the legacy blob; no data is lost).

Modules. Client: two-era crypto (note-crypto.ts), Argon2id wrapping (argon2.ts, dual carrier: node:crypto ≥ Node 24.7 / hash-wasm in browsers, RFC 9106 test vectors), local PIN lock (pinlock.ts), recovery-package BIP39 codec (bip39.ts, zero-dependency, cross-checked against @scure/bip39), brand-namespaced key storage (keys.ts). Server: zero-knowledge auth core with a PH1→PH2 login flow, inbound cipher/package validation and paired key-package selection (auth.ts; PH2-UNIQUE conflict and session revocation are caller-owned wiring), per-IP fixed-window rate limiting on D1 (ratelimit.ts), shared CORS/hash utilities. Since 0.3.0 the package ships dist/ (tsc-emitted ESM .js + .d.ts); every exports subpath resolves to dist, and src/ is included for reading and auditing only.

Format spec & test vectors. The byte-level wire format of every prefix family — payload layout and field lengths, KDF parameters, AAD strings, and the in-band versioning contract — is specified in docs/format-spec.md (Chinese; transcribed from the source, not a new contract). JSON test vectors live in docs/vectors/: one wrap/encrypt sample per prefix family (families.json), known-answer vectors (RFC 9106 Argon2id, the gate's frozen jr2w./jr3w./jr3d./jr4w./jr4d./jr1r./jr1w. blobs, and PH1/PH2 credential samples in kat.json), and 34 BIP39 samples (bip39.json). They were generated once by scripts/generate-vectors.ts against the real primitives and committed as frozen evidence; the verify gate checks their presence and shape (section [26]) and does not regenerate them.

Verification. npm run verify runs 506 assertions against the real modules (no mocks): roundtrips, AAD tamper-evidence, extractability rules, era isolation, cross-prefix family isolation, payload tampering, RFC 9106 KAT, a 200-vector BIP39 cross-check, and the login ladder-guard behavior suite (external audit #8), and presence/shape checks for the format spec and test vectors.

Usage. Since 0.3.0 the npm package ships compiled ESM in dist/, so browsers, Node, Deno, Bun and Workers can import it directly — no bundler and no strip-types needed (the 0.2.x "must bundle" requirement is retired). A source checkout can still run src/ with node --experimental-strip-types. In browsers, inject the hash-wasm Argon2id carrier via setArgonLoader(); Node ≥ 24.7 uses the built-in node:crypto. See the code sample in the 使用 section above.

Honest limits. The guest era is obfuscation-grade, not E2E. PH1 derivation is defined by each product (fast hash or Argon2id); this layer only validates its hex64 form. Product concerns (routing, quotas, subscriptions, GC, webhooks) live in the product forks. No third-party audit yet. A lost passphrase is unrecoverable by design — that is the price of server-side zero knowledge.

License

MIT. Security contact: [email protected]. Version history: CHANGELOG.md. Security reporting process: SECURITY.md.