@noy-db/hub 0.1.0-pre.4 → 0.1.0-pre.7

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.
Files changed (95) hide show
  1. package/dist/blobs/index.cjs.map +1 -1
  2. package/dist/blobs/index.d.cts +3 -3
  3. package/dist/blobs/index.d.ts +3 -3
  4. package/dist/blobs/index.js +2 -2
  5. package/dist/bundle/index.cjs +26 -3
  6. package/dist/bundle/index.cjs.map +1 -1
  7. package/dist/bundle/index.d.cts +3 -3
  8. package/dist/bundle/index.d.ts +3 -3
  9. package/dist/bundle/index.js +3 -1
  10. package/dist/{chunk-LSZHBNDG.js → chunk-3WCRU7TI.js} +2 -2
  11. package/dist/{chunk-PSHTHSIX.js → chunk-6IJQ27XN.js} +213 -10
  12. package/dist/chunk-6IJQ27XN.js.map +1 -0
  13. package/dist/{chunk-O5GK62FJ.js → chunk-B6HF6NTZ.js} +1 -1
  14. package/dist/chunk-B6HF6NTZ.js.map +1 -0
  15. package/dist/{chunk-AVWFLPNR.js → chunk-CL37QSND.js} +2 -2
  16. package/dist/chunk-EMIGCR7X.js +39 -0
  17. package/dist/chunk-EMIGCR7X.js.map +1 -0
  18. package/dist/{chunk-GJILMRPO.js → chunk-FAAWLVTF.js} +42 -4
  19. package/dist/chunk-FAAWLVTF.js.map +1 -0
  20. package/dist/chunk-GILMPJXB.js +155 -0
  21. package/dist/chunk-GILMPJXB.js.map +1 -0
  22. package/dist/{chunk-L77MEFCH.js → chunk-INSJBB5W.js} +3 -3
  23. package/dist/{chunk-QZIACZZU.js → chunk-KPF2HHPI.js} +2 -2
  24. package/dist/{chunk-NK2NSXXK.js → chunk-N2LMZKLR.js} +2 -2
  25. package/dist/{chunk-EARQCIL7.js → chunk-NZ4XCIKS.js} +3 -3
  26. package/dist/{chunk-E445ICYI.js → chunk-UFL4DUEV.js} +5 -3
  27. package/dist/chunk-UFL4DUEV.js.map +1 -0
  28. package/dist/consent/index.d.cts +3 -3
  29. package/dist/consent/index.d.ts +3 -3
  30. package/dist/{dev-unlock-XOUecfQ9.d.ts → dev-unlock-CcJ1qIi7.d.ts} +1 -1
  31. package/dist/{dev-unlock-5SmCVGyx.d.cts → dev-unlock-Dk14V6lX.d.cts} +1 -1
  32. package/dist/{hash-Bxud16vM.d.ts → hash-1Xsqx1jl.d.ts} +1 -1
  33. package/dist/{hash-CvuKN2gH.d.cts → hash-h_2U3TFb.d.cts} +1 -1
  34. package/dist/history/index.cjs.map +1 -1
  35. package/dist/history/index.d.cts +4 -4
  36. package/dist/history/index.d.ts +4 -4
  37. package/dist/history/index.js +2 -2
  38. package/dist/i18n/index.cjs +3 -1
  39. package/dist/i18n/index.cjs.map +1 -1
  40. package/dist/i18n/index.d.cts +3 -3
  41. package/dist/i18n/index.d.ts +3 -3
  42. package/dist/i18n/index.js +3 -3
  43. package/dist/{index-DN-J-5wT.d.cts → index-6xNpPsxR.d.cts} +1 -1
  44. package/dist/{index-Cy-MKrdK.d.ts → index-Cvb0efA_.d.cts} +39 -5
  45. package/dist/{index-BRHBCmLt.d.ts → index-DJTf9yxn.d.ts} +1 -1
  46. package/dist/{index-BvUiM47h.d.cts → index-DZn6Yick.d.ts} +39 -5
  47. package/dist/index.cjs +2001 -58
  48. package/dist/index.cjs.map +1 -1
  49. package/dist/index.d.cts +315 -19
  50. package/dist/index.d.ts +315 -19
  51. package/dist/index.js +1503 -41
  52. package/dist/index.js.map +1 -1
  53. package/dist/{ledger-HWXYGUIQ.js → ledger-5V67MAIL.js} +3 -3
  54. package/dist/periods/index.cjs.map +1 -1
  55. package/dist/periods/index.d.cts +3 -3
  56. package/dist/periods/index.d.ts +3 -3
  57. package/dist/periods/index.js +3 -3
  58. package/dist/public-envelope-DFJZHXVH.js +31 -0
  59. package/dist/public-envelope-DFJZHXVH.js.map +1 -0
  60. package/dist/query/index.d.cts +1 -1
  61. package/dist/query/index.d.ts +1 -1
  62. package/dist/session/index.cjs +4 -2
  63. package/dist/session/index.cjs.map +1 -1
  64. package/dist/session/index.d.cts +4 -4
  65. package/dist/session/index.d.ts +4 -4
  66. package/dist/session/index.js +1 -1
  67. package/dist/shadow/index.d.cts +3 -3
  68. package/dist/shadow/index.d.ts +3 -3
  69. package/dist/store/index.d.cts +3 -3
  70. package/dist/store/index.d.ts +3 -3
  71. package/dist/sync/index.cjs.map +1 -1
  72. package/dist/sync/index.d.cts +2 -2
  73. package/dist/sync/index.d.ts +2 -2
  74. package/dist/sync/index.js +2 -2
  75. package/dist/team/index.cjs +3 -1
  76. package/dist/team/index.cjs.map +1 -1
  77. package/dist/team/index.d.cts +3 -3
  78. package/dist/team/index.d.ts +3 -3
  79. package/dist/team/index.js +4 -4
  80. package/dist/tx/index.d.cts +3 -3
  81. package/dist/tx/index.d.ts +3 -3
  82. package/dist/{types-Dmi7nrC9.d.ts → types-D-6bmD2c.d.ts} +1271 -3
  83. package/dist/{types-BVSfkYg6.d.cts → types-D3QLmhlk.d.cts} +1271 -3
  84. package/package.json +1 -1
  85. package/dist/chunk-E445ICYI.js.map +0 -1
  86. package/dist/chunk-GJILMRPO.js.map +0 -1
  87. package/dist/chunk-O5GK62FJ.js.map +0 -1
  88. package/dist/chunk-PSHTHSIX.js.map +0 -1
  89. /package/dist/{chunk-LSZHBNDG.js.map → chunk-3WCRU7TI.js.map} +0 -0
  90. /package/dist/{chunk-AVWFLPNR.js.map → chunk-CL37QSND.js.map} +0 -0
  91. /package/dist/{chunk-L77MEFCH.js.map → chunk-INSJBB5W.js.map} +0 -0
  92. /package/dist/{chunk-QZIACZZU.js.map → chunk-KPF2HHPI.js.map} +0 -0
  93. /package/dist/{chunk-NK2NSXXK.js.map → chunk-N2LMZKLR.js.map} +0 -0
  94. /package/dist/{chunk-EARQCIL7.js.map → chunk-NZ4XCIKS.js.map} +0 -0
  95. /package/dist/{ledger-HWXYGUIQ.js.map → ledger-5V67MAIL.js.map} +0 -0
package/dist/index.cjs CHANGED
@@ -811,6 +811,158 @@ var init_crypto = __esm({
811
811
  }
812
812
  });
813
813
 
814
+ // src/meta/public-envelope/schema.ts
815
+ function validatePublicEnvelopeInput(input, schema) {
816
+ const allowed = new Set(schema.fields);
817
+ for (const key of Object.keys(input)) {
818
+ const known = key === "name" || key === "description" || key === "icon" || key === "defaultLocale" ? key : void 0;
819
+ if (!known) {
820
+ throw new ValidationError(
821
+ `setPublicEnvelope: unknown field "${key}". Allowed fields: ${[...allowed].join(", ")}.`
822
+ );
823
+ }
824
+ if (!allowed.has(known)) {
825
+ throw new ValidationError(
826
+ `setPublicEnvelope: field "${known}" is not enabled in this vault's schema. Allowed fields: ${[...allowed].join(", ")}.`
827
+ );
828
+ }
829
+ }
830
+ if (input.name !== void 0) {
831
+ validateText(input.name, "name", schema.maxStringChars);
832
+ }
833
+ if (input.description !== void 0) {
834
+ validateText(input.description, "description", schema.maxStringChars);
835
+ }
836
+ if (input.icon !== void 0) {
837
+ validateIcon(input.icon, schema);
838
+ }
839
+ if (input.defaultLocale !== void 0 && typeof input.defaultLocale !== "string") {
840
+ throw new ValidationError(
841
+ `setPublicEnvelope: defaultLocale must be a string (BCP-47), got ${typeof input.defaultLocale}.`
842
+ );
843
+ }
844
+ }
845
+ function validateText(value, field, maxChars) {
846
+ if (typeof value === "string") {
847
+ if (value.length > maxChars) {
848
+ throw new ValidationError(
849
+ `setPublicEnvelope: ${field} exceeds the ${maxChars}-character cap (got ${value.length}).`
850
+ );
851
+ }
852
+ return;
853
+ }
854
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
855
+ throw new ValidationError(
856
+ `setPublicEnvelope: ${field} must be a string or { [locale]: string } map, got ${typeof value}.`
857
+ );
858
+ }
859
+ for (const [locale, str] of Object.entries(value)) {
860
+ if (typeof str !== "string") {
861
+ throw new ValidationError(
862
+ `setPublicEnvelope: ${field}[${locale}] must be a string, got ${typeof str}.`
863
+ );
864
+ }
865
+ if (str.length > maxChars) {
866
+ throw new ValidationError(
867
+ `setPublicEnvelope: ${field}[${locale}] exceeds the ${maxChars}-character cap (got ${str.length}).`
868
+ );
869
+ }
870
+ }
871
+ }
872
+ function validateIcon(icon, schema) {
873
+ if (typeof icon !== "string") {
874
+ throw new ValidationError(
875
+ `setPublicEnvelope: icon must be a data: URL string, got ${typeof icon}.`
876
+ );
877
+ }
878
+ if (icon.length > schema.maxIconBytes) {
879
+ throw new ValidationError(
880
+ `setPublicEnvelope: icon exceeds the ${schema.maxIconBytes}-byte cap (got ${icon.length}).`
881
+ );
882
+ }
883
+ const m = DATA_URL_PREFIX.exec(icon);
884
+ if (!m) {
885
+ throw new ValidationError(
886
+ "setPublicEnvelope: icon must be a base64 data URL (`data:image/png;base64,\u2026` or `data:image/svg+xml;base64,\u2026`). External URLs are not supported in v1."
887
+ );
888
+ }
889
+ const mime = m[1];
890
+ if (!schema.iconMimeTypes.includes(mime)) {
891
+ throw new ValidationError(
892
+ `setPublicEnvelope: icon MIME type "${mime}" is not allowed. Permitted types: ${schema.iconMimeTypes.join(", ")}.`
893
+ );
894
+ }
895
+ }
896
+ function isPublicEnvelope(x) {
897
+ if (x === null || typeof x !== "object" || Array.isArray(x)) return false;
898
+ const obj = x;
899
+ return obj["_noydb_public"] === 1 && typeof obj["version"] === "number";
900
+ }
901
+ var DATA_URL_PREFIX;
902
+ var init_schema = __esm({
903
+ "src/meta/public-envelope/schema.ts"() {
904
+ "use strict";
905
+ init_errors();
906
+ DATA_URL_PREFIX = /^data:([a-zA-Z0-9.+-]+\/[a-zA-Z0-9.+-]+);base64,/;
907
+ }
908
+ });
909
+
910
+ // src/meta/public-envelope/storage.ts
911
+ async function loadPublicEnvelope(store, vault) {
912
+ const envelope = await store.get(vault, "_meta", PUBLIC_ENVELOPE_RECORD_ID);
913
+ if (!envelope) return void 0;
914
+ try {
915
+ const parsed = JSON.parse(envelope._data);
916
+ if (!isPublicEnvelope(parsed)) return void 0;
917
+ return parsed;
918
+ } catch {
919
+ return void 0;
920
+ }
921
+ }
922
+ async function savePublicEnvelope(store, vault, envelope) {
923
+ const wireEnvelope = {
924
+ _noydb: NOYDB_FORMAT_VERSION,
925
+ _v: 1,
926
+ _ts: (/* @__PURE__ */ new Date()).toISOString(),
927
+ _iv: "",
928
+ _data: JSON.stringify(envelope)
929
+ };
930
+ await store.put(vault, "_meta", PUBLIC_ENVELOPE_RECORD_ID, wireEnvelope);
931
+ }
932
+ async function readPublicEnvelope(store, vault, opts = {}) {
933
+ const raw = await loadPublicEnvelope(store, vault);
934
+ if (!raw) return void 0;
935
+ if (opts.locale === void 0) return raw;
936
+ return resolveLocale(raw, opts.locale);
937
+ }
938
+ function resolveLocale(envelope, locale) {
939
+ return {
940
+ ...envelope,
941
+ ...envelope.name !== void 0 ? { name: pickLocale(envelope.name, locale, envelope.defaultLocale) } : {},
942
+ ...envelope.description !== void 0 ? { description: pickLocale(envelope.description, locale, envelope.defaultLocale) } : {}
943
+ };
944
+ }
945
+ function pickLocale(value, locale, defaultLocale) {
946
+ if (typeof value === "string") return value;
947
+ if (value[locale] !== void 0 && value[locale] !== "") return value[locale];
948
+ if (defaultLocale && value[defaultLocale] !== void 0 && value[defaultLocale] !== "") {
949
+ return value[defaultLocale];
950
+ }
951
+ for (const v of Object.values(value)) {
952
+ if (v !== "") return v;
953
+ }
954
+ return "";
955
+ }
956
+ var PUBLIC_ENVELOPE_RECORD_ID;
957
+ var init_storage = __esm({
958
+ "src/meta/public-envelope/storage.ts"() {
959
+ "use strict";
960
+ init_types();
961
+ init_schema();
962
+ PUBLIC_ENVELOPE_RECORD_ID = "public-envelope";
963
+ }
964
+ });
965
+
814
966
  // src/bundle/ulid.ts
815
967
  var ulid_exports = {};
816
968
  __export(ulid_exports, {
@@ -1611,6 +1763,69 @@ var init_ledger = __esm({
1611
1763
  }
1612
1764
  });
1613
1765
 
1766
+ // src/meta/public-envelope/types.ts
1767
+ function resolveSchema(schema) {
1768
+ if (!schema) return void 0;
1769
+ if (schema === true) {
1770
+ return {
1771
+ fields: DEFAULT_PUBLIC_ENVELOPE_SCHEMA.fields,
1772
+ maxIconBytes: DEFAULT_PUBLIC_ENVELOPE_SCHEMA.maxIconBytes,
1773
+ iconMimeTypes: DEFAULT_PUBLIC_ENVELOPE_SCHEMA.iconMimeTypes,
1774
+ maxStringChars: DEFAULT_PUBLIC_ENVELOPE_SCHEMA.maxStringChars
1775
+ };
1776
+ }
1777
+ return {
1778
+ fields: schema.fields ?? DEFAULT_PUBLIC_ENVELOPE_SCHEMA.fields,
1779
+ maxIconBytes: schema.maxIconBytes ?? DEFAULT_PUBLIC_ENVELOPE_SCHEMA.maxIconBytes,
1780
+ iconMimeTypes: schema.iconMimeTypes ?? DEFAULT_PUBLIC_ENVELOPE_SCHEMA.iconMimeTypes,
1781
+ maxStringChars: schema.maxStringChars ?? DEFAULT_PUBLIC_ENVELOPE_SCHEMA.maxStringChars
1782
+ };
1783
+ }
1784
+ var PUBLIC_ENVELOPE_FIELDS, DEFAULT_PUBLIC_ENVELOPE_SCHEMA;
1785
+ var init_types2 = __esm({
1786
+ "src/meta/public-envelope/types.ts"() {
1787
+ "use strict";
1788
+ PUBLIC_ENVELOPE_FIELDS = [
1789
+ "name",
1790
+ "description",
1791
+ "icon",
1792
+ "createdAt",
1793
+ "updatedAt",
1794
+ "defaultLocale"
1795
+ ];
1796
+ DEFAULT_PUBLIC_ENVELOPE_SCHEMA = {
1797
+ fields: PUBLIC_ENVELOPE_FIELDS,
1798
+ maxIconBytes: 256 * 1024,
1799
+ iconMimeTypes: ["image/png", "image/svg+xml"],
1800
+ maxStringChars: 200
1801
+ };
1802
+ }
1803
+ });
1804
+
1805
+ // src/meta/public-envelope/index.ts
1806
+ var public_envelope_exports = {};
1807
+ __export(public_envelope_exports, {
1808
+ DEFAULT_PUBLIC_ENVELOPE_SCHEMA: () => DEFAULT_PUBLIC_ENVELOPE_SCHEMA,
1809
+ PUBLIC_ENVELOPE_FIELDS: () => PUBLIC_ENVELOPE_FIELDS,
1810
+ PUBLIC_ENVELOPE_RECORD_ID: () => PUBLIC_ENVELOPE_RECORD_ID,
1811
+ isPublicEnvelope: () => isPublicEnvelope,
1812
+ loadPublicEnvelope: () => loadPublicEnvelope,
1813
+ pickLocale: () => pickLocale,
1814
+ readPublicEnvelope: () => readPublicEnvelope,
1815
+ resolveLocale: () => resolveLocale,
1816
+ resolveSchema: () => resolveSchema,
1817
+ savePublicEnvelope: () => savePublicEnvelope,
1818
+ validatePublicEnvelopeInput: () => validatePublicEnvelopeInput
1819
+ });
1820
+ var init_public_envelope = __esm({
1821
+ "src/meta/public-envelope/index.ts"() {
1822
+ "use strict";
1823
+ init_types2();
1824
+ init_schema();
1825
+ init_storage();
1826
+ }
1827
+ });
1828
+
1614
1829
  // src/team/tiers.ts
1615
1830
  function dekKey(collection, tier) {
1616
1831
  if (tier <= 0) return collection;
@@ -1754,7 +1969,9 @@ __export(src_exports, {
1754
1969
  CollectionInstant: () => CollectionInstant,
1755
1970
  ConflictError: () => ConflictError,
1756
1971
  DEFAULT_CHUNK_SIZE: () => DEFAULT_CHUNK_SIZE,
1972
+ DEFAULT_FRESHNESS_MS: () => DEFAULT_FRESHNESS_MS,
1757
1973
  DEFAULT_JOIN_MAX_ROWS: () => DEFAULT_JOIN_MAX_ROWS,
1974
+ DEFAULT_PUBLIC_ENVELOPE_SCHEMA: () => DEFAULT_PUBLIC_ENVELOPE_SCHEMA,
1758
1975
  DELEGATIONS_COLLECTION: () => DELEGATIONS_COLLECTION,
1759
1976
  DICT_COLLECTION_PREFIX: () => DICT_COLLECTION_PREFIX,
1760
1977
  DanglingReferenceError: () => DanglingReferenceError,
@@ -1789,6 +2006,7 @@ __export(src_exports, {
1789
2006
  MAGIC_LINK_CONTENT_INFO_PREFIX: () => MAGIC_LINK_CONTENT_INFO_PREFIX,
1790
2007
  MAGIC_LINK_GRANTS_COLLECTION: () => MAGIC_LINK_GRANTS_COLLECTION,
1791
2008
  MAGIC_LINK_KEK_INFO_PREFIX: () => MAGIC_LINK_KEK_INFO_PREFIX,
2009
+ META_COLLECTION: () => META_COLLECTION,
1792
2010
  MissingTranslationError: () => MissingTranslationError,
1793
2011
  NOYDB_BACKUP_VERSION: () => NOYDB_BACKUP_VERSION,
1794
2012
  NOYDB_BUNDLE_FORMAT_VERSION: () => NOYDB_BUNDLE_FORMAT_VERSION,
@@ -1803,20 +2021,29 @@ __export(src_exports, {
1803
2021
  Noydb: () => Noydb,
1804
2022
  NoydbError: () => NoydbError,
1805
2023
  PERIODS_COLLECTION: () => PERIODS_COLLECTION,
2024
+ PERSONAL_POLICY: () => PERSONAL_POLICY,
2025
+ POLICY_RECORD_ID: () => POLICY_RECORD_ID,
2026
+ PUBLIC_ENVELOPE_FIELDS: () => PUBLIC_ENVELOPE_FIELDS,
2027
+ PUBLIC_ENVELOPE_RECORD_ID: () => PUBLIC_ENVELOPE_RECORD_ID,
1806
2028
  PathEscapeError: () => PathEscapeError,
1807
2029
  PeriodClosedError: () => PeriodClosedError,
1808
2030
  PermissionDeniedError: () => PermissionDeniedError,
2031
+ PolicyDeniedError: () => PolicyDeniedError,
1809
2032
  PolicyEnforcer: () => PolicyEnforcer,
1810
2033
  PresenceHandle: () => PresenceHandle,
1811
2034
  PrivilegeEscalationError: () => PrivilegeEscalationError,
1812
2035
  Query: () => Query,
2036
+ QuickUnlockStore: () => QuickUnlockStore,
1813
2037
  ReadOnlyAtInstantError: () => ReadOnlyAtInstantError,
1814
2038
  ReadOnlyError: () => ReadOnlyError,
1815
2039
  ReadOnlyFrameError: () => ReadOnlyFrameError,
2040
+ RecoveryNotEnrolledError: () => RecoveryNotEnrolledError,
2041
+ RecoveryProfileNotImplementedError: () => RecoveryProfileNotImplementedError,
1816
2042
  RefIntegrityError: () => RefIntegrityError,
1817
2043
  RefRegistry: () => RefRegistry,
1818
2044
  RefScopeError: () => RefScopeError,
1819
2045
  ReservedCollectionNameError: () => ReservedCollectionNameError,
2046
+ STRICT_POLICY: () => STRICT_POLICY,
1820
2047
  SYNC_CREDENTIALS_COLLECTION: () => SYNC_CREDENTIALS_COLLECTION,
1821
2048
  ScanBuilder: () => ScanBuilder,
1822
2049
  SchemaValidationError: () => SchemaValidationError,
@@ -1834,21 +2061,29 @@ __export(src_exports, {
1834
2061
  TxCollection: () => TxCollection,
1835
2062
  TxContext: () => TxContext,
1836
2063
  TxVault: () => TxVault,
2064
+ USER_ENVELOPE_COLLECTION: () => USER_ENVELOPE_COLLECTION,
2065
+ USER_ENVELOPE_MAX_BYTES: () => USER_ENVELOPE_MAX_BYTES,
2066
+ UserApi: () => UserApi,
2067
+ UserEnvelopeOversizedError: () => UserEnvelopeOversizedError,
1837
2068
  ValidationError: () => ValidationError,
1838
2069
  Vault: () => Vault,
1839
2070
  VaultFrame: () => VaultFrame,
1840
2071
  VaultInstant: () => VaultInstant,
2072
+ WeakPassphraseError: () => WeakPassphraseError,
1841
2073
  activeSessionCount: () => activeSessionCount,
1842
2074
  applyI18nLocale: () => applyI18nLocale,
1843
2075
  applyJoins: () => applyJoins,
1844
2076
  applyPatch: () => applyPatch,
2077
+ assertStrongPassphrase: () => assertStrongPassphrase,
1845
2078
  assertTierAccess: () => assertTierAccess,
1846
2079
  avg: () => avg,
1847
2080
  base64ToBuffer: () => base64ToBuffer,
1848
2081
  bufferToBase64: () => bufferToBase64,
1849
2082
  buildLiveQuery: () => buildLiveQuery,
1850
2083
  buildRecipientKeyringFile: () => buildRecipientKeyringFile,
2084
+ burnPaperRecoveryEntry: () => burnPaperRecoveryEntry,
1851
2085
  canonicalJson: () => canonicalJson,
2086
+ checkGate: () => checkGate,
1852
2087
  clearDevUnlock: () => clearDevUnlock,
1853
2088
  computePatch: () => computePatch,
1854
2089
  count: () => count,
@@ -1862,10 +2097,16 @@ __export(src_exports, {
1862
2097
  decryptDeterministic: () => decryptDeterministic,
1863
2098
  dekKey: () => dekKey,
1864
2099
  deleteCredential: () => deleteCredential,
2100
+ deleteUserEnvelope: () => deleteUserEnvelope,
1865
2101
  deriveMagicLinkContentKey: () => deriveMagicLinkContentKey,
1866
2102
  derivePresenceKey: () => derivePresenceKey,
2103
+ describeAllUsersAuth: () => describeAllUsersAuth,
2104
+ describeAuthConfig: () => describeAuthConfig,
2105
+ describeGate: () => describeGate,
2106
+ describeUserAuth: () => describeUserAuth,
1867
2107
  detectMagic: () => detectMagic,
1868
2108
  detectMimeType: () => detectMimeType,
2109
+ diagramAuthConfig: () => diagramAuthConfig,
1869
2110
  dictCollectionName: () => dictCollectionName,
1870
2111
  dictKey: () => dictKey,
1871
2112
  diff: () => diff2,
@@ -1874,6 +2115,7 @@ __export(src_exports, {
1874
2115
  enableDevUnlock: () => enableDevUnlock,
1875
2116
  encryptBytes: () => encryptBytes,
1876
2117
  encryptDeterministic: () => encryptDeterministic,
2118
+ enrollAuthenticator: () => enrollAuthenticator,
1877
2119
  envelopePayloadHash: () => envelopePayloadHash,
1878
2120
  estimateEntropy: () => estimateEntropy,
1879
2121
  estimateRecordBytes: () => estimateRecordBytes,
@@ -1882,6 +2124,7 @@ __export(src_exports, {
1882
2124
  evaluateFieldClause: () => evaluateFieldClause,
1883
2125
  evaluateImportCapability: () => evaluateImportCapability,
1884
2126
  executePlan: () => executePlan,
2127
+ findAuthenticator: () => findAuthenticator,
1885
2128
  formatDiff: () => formatDiff,
1886
2129
  generateULID: () => generateULID,
1887
2130
  getCredential: () => getCredential,
@@ -1889,6 +2132,7 @@ __export(src_exports, {
1889
2132
  hasExportCapability: () => hasExportCapability,
1890
2133
  hasImportCapability: () => hasImportCapability,
1891
2134
  hasNoydbBundleMagic: () => hasNoydbBundleMagic,
2135
+ hasRecoveryEnrolled: () => hasRecoveryEnrolled,
1892
2136
  hashEntry: () => hashEntry,
1893
2137
  i18nText: () => i18nText,
1894
2138
  isDevUnlockActive: () => isDevUnlockActive,
@@ -1897,16 +2141,27 @@ __export(src_exports, {
1897
2141
  isI18nTextDescriptor: () => isI18nTextDescriptor,
1898
2142
  isMagicLinkGrantExpired: () => isMagicLinkGrantExpired,
1899
2143
  isPreCompressed: () => isPreCompressed,
2144
+ isPublicEnvelope: () => isPublicEnvelope,
1900
2145
  isSessionAlive: () => isSessionAlive,
1901
2146
  isULID: () => isULID,
1902
2147
  issueDelegation: () => issueDelegation,
2148
+ keyringRecoverPassphrase: () => recoverPassphrase,
2149
+ keyringRotatePassphrase: () => rotatePassphrase,
1903
2150
  listCredentials: () => listCredentials,
1904
2151
  listMagicLinkGrants: () => listMagicLinkGrants,
2152
+ listUserEnvelopeIds: () => listUserEnvelopeIds,
2153
+ listUsers: () => listUsers,
2154
+ listUsersWithEnvelopes: () => listUsersWithEnvelopes,
1905
2155
  loadActiveDelegations: () => loadActiveDelegations,
1906
2156
  loadDevUnlock: () => loadDevUnlock,
2157
+ loadPaperRecoveryEntries: () => loadPaperRecoveryEntries,
2158
+ loadPublicEnvelope: () => loadPublicEnvelope,
2159
+ loadUserEnvelope: () => loadUserEnvelope,
2160
+ loadVaultPolicy: () => loadVaultPolicy,
1907
2161
  magicLinkGrantRecordId: () => magicLinkGrantRecordId,
1908
2162
  max: () => max,
1909
2163
  mergeCrdtStates: () => mergeCrdtStates,
2164
+ mergePolicy: () => mergePolicy,
1910
2165
  min: () => min,
1911
2166
  paddedIndex: () => paddedIndex,
1912
2167
  parseBytes: () => parseBytes,
@@ -1915,13 +2170,17 @@ __export(src_exports, {
1915
2170
  readMagicLinkGrantRecord: () => readMagicLinkGrantRecord,
1916
2171
  readNoydbBundle: () => readNoydbBundle,
1917
2172
  readNoydbBundleHeader: () => readNoydbBundleHeader,
2173
+ readNoydbBundlePublicEnvelope: () => readNoydbBundlePublicEnvelope,
1918
2174
  readPath: () => readPath,
2175
+ readPublicEnvelope: () => readPublicEnvelope,
1919
2176
  reduceRecords: () => reduceRecords,
1920
2177
  ref: () => ref,
2178
+ removeAuthenticator: () => removeAuthenticator,
1921
2179
  resetBrotliSupportCache: () => resetBrotliSupportCache,
1922
2180
  resetJoinWarnings: () => resetJoinWarnings,
1923
2181
  resolveCrdtSnapshot: () => resolveCrdtSnapshot,
1924
2182
  resolveI18nText: () => resolveI18nText,
2183
+ resolvePublicEnvelopeSchema: () => resolveSchema,
1925
2184
  resolveSession: () => resolveSession,
1926
2185
  revokeAllSessions: () => revokeAllSessions,
1927
2186
  revokeDelegation: () => revokeDelegation,
@@ -1929,11 +2188,16 @@ __export(src_exports, {
1929
2188
  revokeSession: () => revokeSession,
1930
2189
  routeStore: () => routeStore,
1931
2190
  runTransaction: () => runTransaction,
2191
+ savePaperRecoveryEntries: () => savePaperRecoveryEntries,
2192
+ savePublicEnvelope: () => savePublicEnvelope,
2193
+ saveUserEnvelope: () => saveUserEnvelope,
2194
+ saveVaultPolicy: () => saveVaultPolicy,
1932
2195
  sha256Hex: () => sha256Hex3,
1933
2196
  sum: () => sum,
1934
2197
  unwrapMagicLinkGrant: () => unwrapMagicLinkGrant,
1935
2198
  validateI18nTextValue: () => validateI18nTextValue,
1936
2199
  validatePassphrase: () => validatePassphrase,
2200
+ validatePublicEnvelopeInput: () => validatePublicEnvelopeInput,
1937
2201
  validateSchemaInput: () => validateSchemaInput,
1938
2202
  validateSchemaOutput: () => validateSchemaOutput,
1939
2203
  validateSessionPolicy: () => validateSessionPolicy,
@@ -3815,7 +4079,8 @@ var ALLOWED_HEADER_KEYS = /* @__PURE__ */ new Set([
3815
4079
  "formatVersion",
3816
4080
  "handle",
3817
4081
  "bodyBytes",
3818
- "bodySha256"
4082
+ "bodySha256",
4083
+ "publicEnvelope"
3819
4084
  ]);
3820
4085
  function validateBundleHeader(parsed) {
3821
4086
  if (parsed === null || typeof parsed !== "object") {
@@ -3851,6 +4116,25 @@ function validateBundleHeader(parsed) {
3851
4116
  `.noydb bundle header.bodySha256 must be a 64-character lowercase hex string, got ${typeof h["bodySha256"] === "string" ? `"${h["bodySha256"]}"` : String(h["bodySha256"])}.`
3852
4117
  );
3853
4118
  }
4119
+ if (h["publicEnvelope"] !== void 0) {
4120
+ const env = h["publicEnvelope"];
4121
+ if (env === null || typeof env !== "object" || Array.isArray(env)) {
4122
+ throw new Error(
4123
+ `.noydb bundle header.publicEnvelope must be a JSON object when present, got ${typeof env}.`
4124
+ );
4125
+ }
4126
+ const e = env;
4127
+ if (e["_noydb_public"] !== 1) {
4128
+ throw new Error(
4129
+ `.noydb bundle header.publicEnvelope._noydb_public must be 1, got ${String(e["_noydb_public"])}.`
4130
+ );
4131
+ }
4132
+ if (typeof e["version"] !== "number" || !Number.isInteger(e["version"]) || e["version"] < 1) {
4133
+ throw new Error(
4134
+ `.noydb bundle header.publicEnvelope.version must be a positive integer, got ${String(e["version"])}.`
4135
+ );
4136
+ }
4137
+ }
3854
4138
  }
3855
4139
  function encodeBundleHeader(header) {
3856
4140
  validateBundleHeader(header);
@@ -3858,7 +4142,8 @@ function encodeBundleHeader(header) {
3858
4142
  formatVersion: header.formatVersion,
3859
4143
  handle: header.handle,
3860
4144
  bodyBytes: header.bodyBytes,
3861
- bodySha256: header.bodySha256
4145
+ bodySha256: header.bodySha256,
4146
+ ...header.publicEnvelope !== void 0 ? { publicEnvelope: header.publicEnvelope } : {}
3862
4147
  });
3863
4148
  return new TextEncoder().encode(json);
3864
4149
  }
@@ -3894,6 +4179,7 @@ function hasNoydbBundleMagic(bytes) {
3894
4179
 
3895
4180
  // src/bundle/bundle.ts
3896
4181
  init_errors();
4182
+ init_storage();
3897
4183
  var cachedBrotliSupport = null;
3898
4184
  function supportsBrotliCompression() {
3899
4185
  if (cachedBrotliSupport !== null) return cachedBrotliSupport;
@@ -4058,11 +4344,13 @@ async function writeNoydbBundle(vault, opts = {}) {
4058
4344
  const { format, streamFormat } = selectCompression(opts.compression);
4059
4345
  const body = streamFormat === null ? dumpBytes : await pumpThroughStream(dumpBytes, new CompressionStream(streamFormat));
4060
4346
  const bodySha256 = await sha256Hex2(body);
4347
+ const publicEnvelope = await vault.getPublicEnvelope();
4061
4348
  const header = {
4062
4349
  formatVersion: NOYDB_BUNDLE_FORMAT_VERSION,
4063
4350
  handle,
4064
4351
  bodyBytes: body.length,
4065
- bodySha256
4352
+ bodySha256,
4353
+ ...publicEnvelope !== void 0 ? { publicEnvelope } : {}
4066
4354
  };
4067
4355
  const headerBytes = encodeBundleHeader(header);
4068
4356
  const prefix = new Uint8Array(NOYDB_BUNDLE_PREFIX_BYTES);
@@ -4104,6 +4392,17 @@ function parsePrefixAndHeader(bytes) {
4104
4392
  function readNoydbBundleHeader(bytes) {
4105
4393
  return parsePrefixAndHeader(bytes).header;
4106
4394
  }
4395
+ function readNoydbBundlePublicEnvelope(bytes, opts = {}) {
4396
+ const header = parsePrefixAndHeader(bytes).header;
4397
+ const env = header.publicEnvelope;
4398
+ if (!env) return void 0;
4399
+ if (opts.locale === void 0) return env;
4400
+ return {
4401
+ ...env,
4402
+ ...env.name !== void 0 ? { name: pickLocale(env.name, opts.locale, env.defaultLocale) } : {},
4403
+ ...env.description !== void 0 ? { description: pickLocale(env.description, opts.locale, env.defaultLocale) } : {}
4404
+ };
4405
+ }
4107
4406
  async function readNoydbBundle(bytes) {
4108
4407
  const { header, bodyOffset, algo } = parsePrefixAndHeader(bytes);
4109
4408
  const body = bytes.slice(bodyOffset);
@@ -4538,10 +4837,159 @@ var RefRegistry = class {
4538
4837
  }
4539
4838
  };
4540
4839
 
4840
+ // src/team/authenticators.ts
4841
+ init_errors();
4842
+
4541
4843
  // src/team/keyring.ts
4542
4844
  init_types();
4543
4845
  init_crypto();
4544
4846
  init_errors();
4847
+
4848
+ // src/validation.ts
4849
+ init_errors();
4850
+ var WeakPassphraseError = class extends NoydbError {
4851
+ reason;
4852
+ suggestion;
4853
+ constructor(reason, suggestion) {
4854
+ super("WEAK_PASSPHRASE", `Weak passphrase (${reason}). ${suggestion}`);
4855
+ this.name = "WeakPassphraseError";
4856
+ this.reason = reason;
4857
+ this.suggestion = suggestion;
4858
+ }
4859
+ };
4860
+ var DEFAULT_MIN_WORDS = 6;
4861
+ var DEFAULT_MIN_WORD_LENGTH = 3;
4862
+ var SUGGESTIONS = {
4863
+ empty: "Provide a phrase of at least 6 lowercase words separated by single spaces.",
4864
+ "invalid-chars": "Use only lowercase letters [a-z] and single spaces. No punctuation, symbols, digits, or uppercase.",
4865
+ "leading-or-trailing-space": "Trim leading and trailing spaces.",
4866
+ "double-space": "Use exactly one space between words.",
4867
+ "too-few-words": 'Use at least 6 words by default (8 under strict policy). Example: "correct horse battery staple printer toaster".',
4868
+ "word-too-short": 'Each word must be at least 3 characters. Drop short fillers like "a", "is", "of".',
4869
+ "repeated-adjacent": "Avoid repeating the same word twice in a row."
4870
+ };
4871
+ function validatePassphrase(s, opts) {
4872
+ const minWords = opts?.minWords ?? DEFAULT_MIN_WORDS;
4873
+ const minWordLength = opts?.minWordLength ?? DEFAULT_MIN_WORD_LENGTH;
4874
+ const rejectRepeated = opts?.rejectRepeatedAdjacent ?? true;
4875
+ if (s.length === 0) {
4876
+ return { ok: false, reason: "empty" };
4877
+ }
4878
+ if (s !== s.trim()) {
4879
+ return { ok: false, reason: "leading-or-trailing-space" };
4880
+ }
4881
+ if (s.includes(" ")) {
4882
+ return { ok: false, reason: "double-space" };
4883
+ }
4884
+ if (!/^[a-z]+( [a-z]+)*$/.test(s)) {
4885
+ return { ok: false, reason: "invalid-chars" };
4886
+ }
4887
+ const words = s.split(" ");
4888
+ if (words.length < minWords) {
4889
+ return { ok: false, reason: "too-few-words", minimum: minWords, got: words.length };
4890
+ }
4891
+ for (const w of words) {
4892
+ if (w.length < minWordLength) {
4893
+ return { ok: false, reason: "word-too-short", minimum: minWordLength, got: w.length };
4894
+ }
4895
+ }
4896
+ if (rejectRepeated) {
4897
+ for (let i = 1; i < words.length; i++) {
4898
+ if (words[i] === words[i - 1]) {
4899
+ return { ok: false, reason: "repeated-adjacent" };
4900
+ }
4901
+ }
4902
+ }
4903
+ return { ok: true, words: words.length };
4904
+ }
4905
+ function assertStrongPassphrase(s, opts) {
4906
+ if (opts?.allowWeakPassphrase) return;
4907
+ const result = validatePassphrase(s, opts);
4908
+ if (result.ok) return;
4909
+ throw new WeakPassphraseError(result.reason, SUGGESTIONS[result.reason]);
4910
+ }
4911
+ function estimateEntropy(passphrase) {
4912
+ const result = validatePassphrase(passphrase);
4913
+ if (!result.ok) return 0;
4914
+ return Math.round(result.words * Math.log2(7776));
4915
+ }
4916
+
4917
+ // src/meta/user-envelope/types.ts
4918
+ init_errors();
4919
+ var USER_ENVELOPE_MAX_BYTES = 64 * 1024;
4920
+ var USER_ENVELOPE_COLLECTION = "_users";
4921
+ var UserEnvelopeOversizedError = class extends NoydbError {
4922
+ bytes;
4923
+ limit;
4924
+ constructor(bytes, limit = USER_ENVELOPE_MAX_BYTES) {
4925
+ super(
4926
+ "USER_ENVELOPE_OVERSIZED",
4927
+ `User envelope payload is ${bytes} bytes; soft cap is ${limit} bytes. Move large data into the vault's regular collections.`
4928
+ );
4929
+ this.name = "UserEnvelopeOversizedError";
4930
+ this.bytes = bytes;
4931
+ this.limit = limit;
4932
+ }
4933
+ };
4934
+
4935
+ // src/meta/user-envelope/storage.ts
4936
+ init_types();
4937
+ init_crypto();
4938
+ init_errors();
4939
+ async function loadUserEnvelope(store, vault, keyringId, dek) {
4940
+ const envelope = await store.get(vault, USER_ENVELOPE_COLLECTION, keyringId);
4941
+ if (!envelope) return null;
4942
+ const plaintext = await decrypt(envelope._iv, envelope._data, dek);
4943
+ const data = JSON.parse(plaintext);
4944
+ return {
4945
+ keyringId,
4946
+ data,
4947
+ _v: envelope._v,
4948
+ _ts: envelope._ts
4949
+ };
4950
+ }
4951
+ async function saveUserEnvelope(store, vault, keyringId, payload, dek, expectedVersion) {
4952
+ const json = JSON.stringify(payload);
4953
+ const bytes = new TextEncoder().encode(json).byteLength;
4954
+ if (bytes > USER_ENVELOPE_MAX_BYTES) {
4955
+ throw new UserEnvelopeOversizedError(bytes);
4956
+ }
4957
+ const prior = await store.get(vault, USER_ENVELOPE_COLLECTION, keyringId);
4958
+ if (expectedVersion !== void 0) {
4959
+ const priorVersion = prior?._v ?? 0;
4960
+ if (priorVersion !== expectedVersion) {
4961
+ throw new ConflictError(
4962
+ priorVersion,
4963
+ `User envelope for "${keyringId}" expected version ${expectedVersion}, actual ${priorVersion}`
4964
+ );
4965
+ }
4966
+ }
4967
+ const nextVersion = (prior?._v ?? 0) + 1;
4968
+ const ts = (/* @__PURE__ */ new Date()).toISOString();
4969
+ const { iv, data } = await encrypt(json, dek);
4970
+ const envelope = {
4971
+ _noydb: NOYDB_FORMAT_VERSION,
4972
+ _v: nextVersion,
4973
+ _ts: ts,
4974
+ _iv: iv,
4975
+ _data: data
4976
+ };
4977
+ await store.put(vault, USER_ENVELOPE_COLLECTION, keyringId, envelope);
4978
+ return {
4979
+ keyringId,
4980
+ data: payload,
4981
+ _v: nextVersion,
4982
+ _ts: ts
4983
+ };
4984
+ }
4985
+ async function deleteUserEnvelope(store, vault, keyringId) {
4986
+ await store.delete(vault, USER_ENVELOPE_COLLECTION, keyringId);
4987
+ }
4988
+ async function listUserEnvelopeIds(store, vault) {
4989
+ return store.list(vault, USER_ENVELOPE_COLLECTION);
4990
+ }
4991
+
4992
+ // src/team/keyring.ts
4545
4993
  var ADMIN_GRANTABLE_TARGETS = ["operator", "viewer", "client", "admin"];
4546
4994
  function canGrant(callerRole, targetRole) {
4547
4995
  if (callerRole === "owner") return true;
@@ -4581,20 +5029,27 @@ async function loadKeyring(adapter, vault, userId, passphrase) {
4581
5029
  deks,
4582
5030
  kek,
4583
5031
  salt,
5032
+ authenticators: keyringFile.authenticators ?? [],
4584
5033
  ...keyringFile.export_capability !== void 0 && { exportCapability: keyringFile.export_capability },
4585
- ...keyringFile.import_capability !== void 0 && { importCapability: keyringFile.import_capability }
5034
+ ...keyringFile.import_capability !== void 0 && { importCapability: keyringFile.import_capability },
5035
+ ...keyringFile.policy !== void 0 && { policy: keyringFile.policy }
4586
5036
  };
4587
5037
  }
4588
- async function createOwnerKeyring(adapter, vault, userId, passphrase) {
5038
+ async function createOwnerKeyring(adapter, vault, userId, passphrase, passphraseOpts) {
5039
+ if (passphraseOpts?.validate && !passphraseOpts.allowWeakPassphrase) {
5040
+ assertStrongPassphrase(passphrase, passphraseOpts);
5041
+ }
4589
5042
  const salt = generateSalt();
4590
5043
  const kek = await deriveKey(passphrase, salt);
5044
+ const userEnvelopeDek = await generateDEK();
5045
+ const wrappedUserEnvelopeDek = await wrapKey(userEnvelopeDek, kek);
4591
5046
  const keyringFile = {
4592
5047
  _noydb_keyring: NOYDB_KEYRING_VERSION,
4593
5048
  user_id: userId,
4594
5049
  display_name: userId,
4595
5050
  role: "owner",
4596
5051
  permissions: {},
4597
- deks: {},
5052
+ deks: { [USER_ENVELOPE_COLLECTION]: wrappedUserEnvelopeDek },
4598
5053
  salt: bufferToBase64(salt),
4599
5054
  created_at: (/* @__PURE__ */ new Date()).toISOString(),
4600
5055
  granted_by: userId
@@ -4605,9 +5060,10 @@ async function createOwnerKeyring(adapter, vault, userId, passphrase) {
4605
5060
  displayName: userId,
4606
5061
  role: "owner",
4607
5062
  permissions: {},
4608
- deks: /* @__PURE__ */ new Map(),
5063
+ deks: /* @__PURE__ */ new Map([[USER_ENVELOPE_COLLECTION, userEnvelopeDek]]),
4609
5064
  kek,
4610
- salt
5065
+ salt,
5066
+ authenticators: []
4611
5067
  };
4612
5068
  }
4613
5069
  async function grant(adapter, vault, callerKeyring, options) {
@@ -4616,6 +5072,9 @@ async function grant(adapter, vault, callerKeyring, options) {
4616
5072
  `Role "${callerKeyring.role}" cannot grant role "${options.role}"`
4617
5073
  );
4618
5074
  }
5075
+ if (options.validatePassphrase && !options.allowWeakPassphrase) {
5076
+ assertStrongPassphrase(options.passphrase);
5077
+ }
4619
5078
  const permissions = resolvePermissions(options.role, options.permissions);
4620
5079
  const newSalt = generateSalt();
4621
5080
  const newKek = await deriveKey(options.passphrase, newSalt);
@@ -4657,6 +5116,17 @@ async function grant(adapter, vault, callerKeyring, options) {
4657
5116
  ...options.importCapability !== void 0 && { import_capability: options.importCapability }
4658
5117
  };
4659
5118
  await writeKeyringFile(adapter, vault, options.userId, keyringFile);
5119
+ const userEnvelopeDek = callerKeyring.deks.get(USER_ENVELOPE_COLLECTION);
5120
+ if (userEnvelopeDek) {
5121
+ const initialPayload = options.initialProfile ?? {};
5122
+ await saveUserEnvelope(
5123
+ adapter,
5124
+ vault,
5125
+ options.userId,
5126
+ initialPayload,
5127
+ userEnvelopeDek
5128
+ );
5129
+ }
4660
5130
  }
4661
5131
  async function findAdminDescendants(adapter, vault, rootUserId) {
4662
5132
  const allUserIds = await adapter.list(vault, "_keyring");
@@ -4719,6 +5189,7 @@ async function revoke(adapter, vault, callerKeyring, options) {
4719
5189
  }
4720
5190
  for (const userId of usersToRevoke) {
4721
5191
  await adapter.delete(vault, "_keyring", userId);
5192
+ await deleteUserEnvelope(adapter, vault, userId);
4722
5193
  }
4723
5194
  if (options.rotateKeys !== false && affectedCollections.size > 0) {
4724
5195
  await rotateKeys(adapter, vault, callerKeyring, [...affectedCollections]);
@@ -4775,7 +5246,10 @@ async function rotateKeys(adapter, vault, callerKeyring, collections) {
4775
5246
  await writeKeyringFile(adapter, vault, userId, updatedKeyring);
4776
5247
  }
4777
5248
  }
4778
- async function changeSecret(adapter, vault, keyring, newPassphrase) {
5249
+ async function changeSecret(adapter, vault, keyring, newPassphrase, passphraseOpts) {
5250
+ if (passphraseOpts?.validate && !passphraseOpts.allowWeakPassphrase) {
5251
+ assertStrongPassphrase(newPassphrase, passphraseOpts);
5252
+ }
4779
5253
  const newSalt = generateSalt();
4780
5254
  const newKek = await deriveKey(newPassphrase, newSalt);
4781
5255
  const wrappedDeks = {};
@@ -4802,7 +5276,14 @@ async function changeSecret(adapter, vault, keyring, newPassphrase) {
4802
5276
  deks: keyring.deks,
4803
5277
  // Same DEKs, different wrapping
4804
5278
  kek: newKek,
4805
- salt: newSalt
5279
+ salt: newSalt,
5280
+ // Tier-2 slots are NOT preserved through `changeSecret` —
5281
+ // each slot wraps the OLD KEK, so the new keyring has no
5282
+ // authenticator slots until the user re-enrolls. The higher-level
5283
+ // `db.rotatePassphrase()` (#10) preserves slots by rewrapping the
5284
+ // KEK reference, not the KEK itself.
5285
+ authenticators: [],
5286
+ ...keyring.policy !== void 0 && { policy: keyring.policy }
4806
5287
  };
4807
5288
  }
4808
5289
  async function buildRecipientKeyringFile(callerKeyring, recipient) {
@@ -4867,6 +5348,20 @@ async function listUsers(adapter, vault) {
4867
5348
  }
4868
5349
  return users;
4869
5350
  }
5351
+ async function listUsersWithEnvelopes(adapter, vault, userEnvelopeDek) {
5352
+ const users = await listUsers(adapter, vault);
5353
+ const out = [];
5354
+ for (const user of users) {
5355
+ const envelope = await loadUserEnvelope(
5356
+ adapter,
5357
+ vault,
5358
+ user.userId,
5359
+ userEnvelopeDek
5360
+ );
5361
+ out.push({ user, envelope });
5362
+ }
5363
+ return out;
5364
+ }
4870
5365
  async function ensureCollectionDEK(adapter, vault, keyring) {
4871
5366
  const inFlight = /* @__PURE__ */ new Map();
4872
5367
  return async (collectionName) => {
@@ -4913,7 +5408,9 @@ async function persistKeyring(adapter, vault, keyring) {
4913
5408
  created_at: (/* @__PURE__ */ new Date()).toISOString(),
4914
5409
  granted_by: keyring.userId,
4915
5410
  ...keyring.exportCapability !== void 0 && { export_capability: keyring.exportCapability },
4916
- ...keyring.importCapability !== void 0 && { import_capability: keyring.importCapability }
5411
+ ...keyring.importCapability !== void 0 && { import_capability: keyring.importCapability },
5412
+ ...keyring.authenticators.length > 0 && { authenticators: keyring.authenticators },
5413
+ ...keyring.policy !== void 0 && { policy: keyring.policy }
4917
5414
  };
4918
5415
  await writeKeyringFile(adapter, vault, keyring.userId, keyringFile);
4919
5416
  }
@@ -4948,25 +5445,759 @@ function evaluateImportCapability(capability, _role, tier, format) {
4948
5445
  const allowed = capability?.plaintext ?? [];
4949
5446
  return allowed.includes("*") || format !== void 0 && allowed.includes(format);
4950
5447
  }
4951
- return capability?.bundle === true;
5448
+ return capability?.bundle === true;
5449
+ }
5450
+ function resolvePermissions(role, explicit) {
5451
+ if (role === "owner" || role === "admin" || role === "viewer") return {};
5452
+ return explicit ?? {};
5453
+ }
5454
+ async function writeKeyringFile(adapter, vault, userId, keyringFile) {
5455
+ const envelope = {
5456
+ _noydb: 1,
5457
+ _v: 1,
5458
+ _ts: (/* @__PURE__ */ new Date()).toISOString(),
5459
+ _iv: "",
5460
+ _data: JSON.stringify(keyringFile)
5461
+ };
5462
+ await adapter.put(vault, "_keyring", userId, envelope);
5463
+ }
5464
+
5465
+ // src/team/authenticators.ts
5466
+ async function enrollAuthenticator(store, vault, keyring, options) {
5467
+ const existing = keyring.authenticators.find((a) => a.id === options.id);
5468
+ if (existing) {
5469
+ throw new ValidationError(
5470
+ `enrollAuthenticator: slot id "${options.id}" already exists in vault "${vault}". Remove the slot first or pick a unique id.`
5471
+ );
5472
+ }
5473
+ const slot = {
5474
+ id: options.id,
5475
+ method: options.method,
5476
+ enrolled_at: (/* @__PURE__ */ new Date()).toISOString(),
5477
+ enrolled_via_tier: options.enrolled_via_tier ?? 1,
5478
+ wrapped_kek: options.wrapped_kek,
5479
+ meta: options.meta
5480
+ };
5481
+ const next = appendSlot(keyring, slot);
5482
+ await persistKeyring(store, vault, next);
5483
+ return next;
5484
+ }
5485
+ async function removeAuthenticator(store, vault, keyring, slotId) {
5486
+ const filtered = keyring.authenticators.filter((a) => a.id !== slotId);
5487
+ if (filtered.length === keyring.authenticators.length) {
5488
+ return keyring;
5489
+ }
5490
+ const next = {
5491
+ ...keyring,
5492
+ authenticators: filtered
5493
+ };
5494
+ await persistKeyring(store, vault, next);
5495
+ return next;
5496
+ }
5497
+ function findAuthenticator(keyring, slotId) {
5498
+ return keyring.authenticators.find((a) => a.id === slotId);
5499
+ }
5500
+ function appendSlot(keyring, slot) {
5501
+ return {
5502
+ ...keyring,
5503
+ authenticators: [...keyring.authenticators, slot]
5504
+ };
5505
+ }
5506
+
5507
+ // src/session/unlock-state.ts
5508
+ var QuickUnlockStore = class {
5509
+ states = /* @__PURE__ */ new Map();
5510
+ timers = /* @__PURE__ */ new Map();
5511
+ /**
5512
+ * Register a quick-unlock state for a vault. Replaces any existing
5513
+ * state. Schedules an automatic clear when the state's `expiresAt`
5514
+ * elapses.
5515
+ */
5516
+ set(vault, state) {
5517
+ this.clearTimer(vault);
5518
+ this.states.set(vault, state);
5519
+ const ttl = new Date(state.expiresAt).getTime() - Date.now();
5520
+ if (ttl > 0) {
5521
+ const timer = setTimeout(() => this.delete(vault), ttl);
5522
+ this.timers.set(vault, timer);
5523
+ }
5524
+ }
5525
+ /** Read the state for a vault. Returns undefined when none is registered. */
5526
+ get(vault) {
5527
+ return this.states.get(vault);
5528
+ }
5529
+ /** Drop the state for a vault. Cancels the auto-clear timer. */
5530
+ delete(vault) {
5531
+ this.clearTimer(vault);
5532
+ this.states.delete(vault);
5533
+ }
5534
+ /** Drop every cached state. Called on `db.close()`. */
5535
+ clear() {
5536
+ for (const vault of this.states.keys()) {
5537
+ this.clearTimer(vault);
5538
+ }
5539
+ this.states.clear();
5540
+ }
5541
+ clearTimer(vault) {
5542
+ const t = this.timers.get(vault);
5543
+ if (t) clearTimeout(t);
5544
+ this.timers.delete(vault);
5545
+ }
5546
+ };
5547
+
5548
+ // src/team/rotate-recover.ts
5549
+ init_types();
5550
+ init_crypto();
5551
+ init_errors();
5552
+
5553
+ // src/policy/errors.ts
5554
+ init_errors();
5555
+ var PolicyDeniedError = class extends NoydbError {
5556
+ gate;
5557
+ reason;
5558
+ required;
5559
+ constructor(gate, reason, required, message) {
5560
+ super(
5561
+ "POLICY_DENIED",
5562
+ message ?? `Gate "${gate}" denied: ${reason}.`
5563
+ );
5564
+ this.name = "PolicyDeniedError";
5565
+ this.gate = gate;
5566
+ this.reason = reason;
5567
+ this.required = required;
5568
+ }
5569
+ };
5570
+ var RecoveryNotEnrolledError = class extends NoydbError {
5571
+ constructor(message = 'Recovery profile not enrolled. Pass `recovery: [{ profile: "paper", codes: 10 }]` to `createNoydb()`, or set `policy.gates["recover-passphrase"].enabled = false` to opt out of recovery (passphrase loss = data loss). See docs/subsystems/session-tiers.md.') {
5572
+ super("RECOVERY_NOT_ENROLLED", message);
5573
+ this.name = "RecoveryNotEnrolledError";
5574
+ }
5575
+ };
5576
+ var RecoveryProfileNotImplementedError = class extends NoydbError {
5577
+ profile;
5578
+ tracking;
5579
+ constructor(profile, tracking) {
5580
+ super(
5581
+ "RECOVERY_PROFILE_NOT_IMPLEMENTED",
5582
+ `Recovery profile "${profile}" is not yet implemented in this hub release. Tracking: ${tracking}. Use the "paper" profile via @noy-db/on-recovery in the meantime.`
5583
+ );
5584
+ this.name = "RecoveryProfileNotImplementedError";
5585
+ this.profile = profile;
5586
+ this.tracking = tracking;
5587
+ }
5588
+ };
5589
+
5590
+ // src/team/recovery.ts
5591
+ init_types();
5592
+ var PAPER_DOC_ID = "recovery-paper";
5593
+ async function loadPaperRecoveryEntries(store, vault) {
5594
+ const env = await store.get(vault, "_meta", PAPER_DOC_ID);
5595
+ if (!env) return [];
5596
+ try {
5597
+ const doc = JSON.parse(env._data);
5598
+ if (doc.profile !== "paper" || !Array.isArray(doc.entries)) return [];
5599
+ return doc.entries;
5600
+ } catch {
5601
+ return [];
5602
+ }
5603
+ }
5604
+ async function savePaperRecoveryEntries(store, vault, entries) {
5605
+ const doc = {
5606
+ _noydb_recovery: 1,
5607
+ profile: "paper",
5608
+ entries
5609
+ };
5610
+ const envelope = {
5611
+ _noydb: NOYDB_FORMAT_VERSION,
5612
+ _v: 1,
5613
+ _ts: (/* @__PURE__ */ new Date()).toISOString(),
5614
+ _iv: "",
5615
+ _data: JSON.stringify(doc)
5616
+ };
5617
+ await store.put(vault, "_meta", PAPER_DOC_ID, envelope);
5618
+ }
5619
+ async function burnPaperRecoveryEntry(store, vault, codeId) {
5620
+ const entries = await loadPaperRecoveryEntries(store, vault);
5621
+ const remaining = entries.filter((e) => e.codeId !== codeId);
5622
+ await savePaperRecoveryEntries(store, vault, remaining);
5623
+ }
5624
+ async function hasRecoveryEnrolled(store, vault) {
5625
+ const paper = await loadPaperRecoveryEntries(store, vault);
5626
+ return paper.length > 0;
5627
+ }
5628
+ var subtle2 = globalThis.crypto.subtle;
5629
+ var RECOVERY_PBKDF2_ITERATIONS = 6e5;
5630
+ async function unwrapDeksFromPaperEntry(entry, code) {
5631
+ const wrappingKey = await deriveRecoveryWrappingKey(code, base64ToBytes(entry.salt));
5632
+ const plaintext = await subtle2.decrypt(
5633
+ { name: "AES-GCM", iv: base64ToBytes(entry.iv) },
5634
+ wrappingKey,
5635
+ base64ToBytes(entry.wrappedDeks)
5636
+ );
5637
+ const parsed = JSON.parse(new TextDecoder().decode(plaintext));
5638
+ const deks = /* @__PURE__ */ new Map();
5639
+ for (const [coll, b64] of Object.entries(parsed.deks)) {
5640
+ const raw = base64ToBytes(b64);
5641
+ const key = await subtle2.importKey(
5642
+ "raw",
5643
+ raw,
5644
+ { name: "AES-GCM", length: 256 },
5645
+ true,
5646
+ ["encrypt", "decrypt"]
5647
+ );
5648
+ deks.set(coll, key);
5649
+ }
5650
+ return deks;
5651
+ }
5652
+ async function deriveRecoveryWrappingKey(code, salt) {
5653
+ const ikm = await subtle2.importKey(
5654
+ "raw",
5655
+ new TextEncoder().encode(code),
5656
+ "PBKDF2",
5657
+ false,
5658
+ ["deriveKey"]
5659
+ );
5660
+ return subtle2.deriveKey(
5661
+ {
5662
+ name: "PBKDF2",
5663
+ salt,
5664
+ iterations: RECOVERY_PBKDF2_ITERATIONS,
5665
+ hash: "SHA-256"
5666
+ },
5667
+ ikm,
5668
+ { name: "AES-GCM", length: 256 },
5669
+ false,
5670
+ ["encrypt", "decrypt"]
5671
+ );
5672
+ }
5673
+ function base64ToBytes(b64) {
5674
+ const s = atob(b64);
5675
+ const out = new Uint8Array(s.length);
5676
+ for (let i = 0; i < s.length; i++) out[i] = s.charCodeAt(i);
5677
+ return out;
5678
+ }
5679
+
5680
+ // src/team/rotate-recover.ts
5681
+ async function rotatePassphrase(store, vault, userId, input) {
5682
+ if (!input.allowWeakPassphrase) {
5683
+ assertStrongPassphrase(input.newPassphrase, input.passphrasePolicy);
5684
+ }
5685
+ const env = await store.get(vault, "_keyring", userId);
5686
+ if (!env) {
5687
+ throw new NoAccessError(`No keyring found for user "${userId}" in vault "${vault}".`);
5688
+ }
5689
+ const file = JSON.parse(env._data);
5690
+ const oldSalt = base64ToBuffer(file.salt);
5691
+ const oldKek = await deriveKey(input.oldPassphrase, oldSalt);
5692
+ const deks = /* @__PURE__ */ new Map();
5693
+ for (const [coll, wrapped] of Object.entries(file.deks)) {
5694
+ deks.set(coll, await unwrapKey(wrapped, oldKek));
5695
+ }
5696
+ const newSalt = generateSalt();
5697
+ const newKek = await deriveKey(input.newPassphrase, newSalt);
5698
+ const wrappedDeks = {};
5699
+ for (const [coll, dek] of deks) {
5700
+ wrappedDeks[coll] = await wrapKey(dek, newKek);
5701
+ }
5702
+ const next = {
5703
+ ...file,
5704
+ _noydb_keyring: NOYDB_KEYRING_VERSION,
5705
+ deks: wrappedDeks,
5706
+ salt: bufferToBase64(newSalt),
5707
+ // Tier-2 slots reference the old KEK — drop them. User
5708
+ // re-enrols afterwards via `db.enrollAuthenticator`.
5709
+ authenticators: []
5710
+ };
5711
+ await writeKeyringFile2(store, vault, userId, next);
5712
+ return {
5713
+ userId: file.user_id,
5714
+ displayName: file.display_name,
5715
+ role: file.role,
5716
+ permissions: file.permissions,
5717
+ deks,
5718
+ kek: newKek,
5719
+ salt: newSalt,
5720
+ authenticators: [],
5721
+ ...file.export_capability !== void 0 && { exportCapability: file.export_capability },
5722
+ ...file.import_capability !== void 0 && { importCapability: file.import_capability }
5723
+ };
5724
+ }
5725
+ async function recoverPassphrase(store, vault, userId, input) {
5726
+ if (!input.allowWeakPassphrase) {
5727
+ assertStrongPassphrase(input.newPassphrase, input.passphrasePolicy);
5728
+ }
5729
+ switch (input.recoveryProof.profile) {
5730
+ case "paper":
5731
+ return recoverViaPaperCode(store, vault, userId, input);
5732
+ case "shamir":
5733
+ throw new RecoveryProfileNotImplementedError(
5734
+ "shamir",
5735
+ "https://github.com/vLannaAi/noy-db/issues/10"
5736
+ );
5737
+ case "multi-channel":
5738
+ throw new RecoveryProfileNotImplementedError(
5739
+ "multi-channel",
5740
+ "https://github.com/vLannaAi/noy-db/issues/10"
5741
+ );
5742
+ case "admin-mediated":
5743
+ throw new RecoveryProfileNotImplementedError(
5744
+ "admin-mediated",
5745
+ "https://github.com/vLannaAi/noy-db/issues/10"
5746
+ );
5747
+ default: {
5748
+ const _exhaustive = input.recoveryProof;
5749
+ throw new Error(`Unknown recovery profile: ${String(_exhaustive)}`);
5750
+ }
5751
+ }
5752
+ }
5753
+ async function recoverViaPaperCode(store, vault, userId, input) {
5754
+ if (input.recoveryProof.profile !== "paper") throw new Error("unreachable");
5755
+ const { code } = input.recoveryProof.payload;
5756
+ const env = await store.get(vault, "_keyring", userId);
5757
+ if (!env) {
5758
+ throw new NoAccessError(`No keyring found for user "${userId}" in vault "${vault}".`);
5759
+ }
5760
+ const file = JSON.parse(env._data);
5761
+ const entries = await loadPaperRecoveryEntries(store, vault);
5762
+ if (entries.length === 0) {
5763
+ throw new NoAccessError(
5764
+ `No paper-recovery entries enrolled for vault "${vault}". Enroll via \`db.enrollRecovery({ profile: "paper", entries })\` before relying on recovery.`
5765
+ );
5766
+ }
5767
+ const normalized = normalizePaperCode(code);
5768
+ let recovered;
5769
+ for (const entry of entries) {
5770
+ try {
5771
+ const deks2 = await unwrapDeksFromPaperEntry(entry, normalized);
5772
+ recovered = { deks: deks2, entry };
5773
+ break;
5774
+ } catch {
5775
+ }
5776
+ }
5777
+ if (!recovered) {
5778
+ throw new InvalidKeyError(
5779
+ "Recovery code does not match any enrolled paper entry. The code may have been previously used (single-use) or typed incorrectly."
5780
+ );
5781
+ }
5782
+ const deks = recovered.deks;
5783
+ const newSalt = generateSalt();
5784
+ const newKek = await deriveKey(input.newPassphrase, newSalt);
5785
+ const wrappedDeks = {};
5786
+ for (const [coll, dek] of deks) {
5787
+ wrappedDeks[coll] = await wrapKey(dek, newKek);
5788
+ }
5789
+ const next = {
5790
+ ...file,
5791
+ _noydb_keyring: NOYDB_KEYRING_VERSION,
5792
+ deks: wrappedDeks,
5793
+ salt: bufferToBase64(newSalt),
5794
+ authenticators: []
5795
+ // tier-2 slots wrap old KEK, drop them
5796
+ };
5797
+ await writeKeyringFile2(store, vault, userId, next);
5798
+ await burnPaperRecoveryEntry(store, vault, recovered.entry.codeId);
5799
+ return {
5800
+ userId: file.user_id,
5801
+ displayName: file.display_name,
5802
+ role: file.role,
5803
+ permissions: file.permissions,
5804
+ deks,
5805
+ kek: newKek,
5806
+ salt: newSalt,
5807
+ authenticators: [],
5808
+ ...file.export_capability !== void 0 && { exportCapability: file.export_capability },
5809
+ ...file.import_capability !== void 0 && { importCapability: file.import_capability }
5810
+ };
5811
+ }
5812
+ function normalizePaperCode(input) {
5813
+ return input.toUpperCase().replace(/[\s\-_]/g, "");
5814
+ }
5815
+ async function writeKeyringFile2(store, vault, userId, file) {
5816
+ const envelope = {
5817
+ _noydb: 1,
5818
+ _v: 1,
5819
+ _ts: (/* @__PURE__ */ new Date()).toISOString(),
5820
+ _iv: "",
5821
+ _data: JSON.stringify(file)
5822
+ };
5823
+ await store.put(vault, "_keyring", userId, envelope);
5824
+ }
5825
+
5826
+ // src/index.ts
5827
+ init_public_envelope();
5828
+
5829
+ // src/meta/user-envelope/api.ts
5830
+ var UserApi = class {
5831
+ constructor(adapter, vaultName, writerKeyringId, getDek, checkGate2) {
5832
+ this.adapter = adapter;
5833
+ this.vaultName = vaultName;
5834
+ this.writerKeyringId = writerKeyringId;
5835
+ this.getDek = getDek;
5836
+ this.checkGate = checkGate2;
5837
+ }
5838
+ adapter;
5839
+ vaultName;
5840
+ writerKeyringId;
5841
+ getDek;
5842
+ checkGate;
5843
+ /** keyringId → set of listeners. Wildcard '*' fires on every change. */
5844
+ listeners = /* @__PURE__ */ new Map();
5845
+ // ─── Write-self ──────────────────────────────────────────────────────
5846
+ /** Read the writer's own envelope. Returns null if never written. */
5847
+ async me() {
5848
+ const dek = await this.getDek();
5849
+ return loadUserEnvelope(this.adapter, this.vaultName, this.writerKeyringId, dek);
5850
+ }
5851
+ /**
5852
+ * Deep-merge a partial patch into the writer's own envelope. Creates
5853
+ * the envelope on first call. Optimistic-concurrency safe — a stale
5854
+ * `_v` (parallel writer on another device) throws `ConflictError`.
5855
+ *
5856
+ * Gated by the `edit-own-profile` policy gate (default `minTier: 3`).
5857
+ * Pass `presented` to satisfy tightened policies that require a
5858
+ * factor proof (e.g. STRICT_POLICY's TOTP requirement).
5859
+ */
5860
+ async updateMe(patch, presented) {
5861
+ if (this.checkGate) await this.checkGate("edit-own-profile", presented);
5862
+ const dek = await this.getDek();
5863
+ const current = await loadUserEnvelope(
5864
+ this.adapter,
5865
+ this.vaultName,
5866
+ this.writerKeyringId,
5867
+ dek
5868
+ );
5869
+ const merged = current ? deepMerge(current.data, patch) : patch;
5870
+ const written = await saveUserEnvelope(
5871
+ this.adapter,
5872
+ this.vaultName,
5873
+ this.writerKeyringId,
5874
+ merged,
5875
+ dek,
5876
+ current?._v ?? 0
5877
+ );
5878
+ this.fireChange(this.writerKeyringId, written);
5879
+ return written;
5880
+ }
5881
+ /**
5882
+ * Replace the writer's own envelope with `payload`. Use sparingly —
5883
+ * `updateMe` is the canonical mutation. No `expectedVersion` check;
5884
+ * callers explicitly take last-write-wins semantics.
5885
+ *
5886
+ * Gated by `edit-own-profile`. See `updateMe` for `presented` usage.
5887
+ */
5888
+ async setMe(payload, presented) {
5889
+ if (this.checkGate) await this.checkGate("edit-own-profile", presented);
5890
+ const dek = await this.getDek();
5891
+ const written = await saveUserEnvelope(
5892
+ this.adapter,
5893
+ this.vaultName,
5894
+ this.writerKeyringId,
5895
+ payload,
5896
+ dek
5897
+ );
5898
+ this.fireChange(this.writerKeyringId, written);
5899
+ return written;
5900
+ }
5901
+ // ─── Read-anyone ─────────────────────────────────────────────────────
5902
+ /**
5903
+ * Read another principal's envelope by their keyringId. Returns null
5904
+ * if the principal exists but has no envelope yet, or if the
5905
+ * keyringId does not exist at all.
5906
+ *
5907
+ * Gated by `view-team-profiles` (default `minTier: 2`) — but ONLY for
5908
+ * cross-principal reads. Reading your own envelope (`keyringId ===
5909
+ * self`) is never gated; that's just `me()` written long-form.
5910
+ */
5911
+ async get(keyringId, presented) {
5912
+ if (this.checkGate && keyringId !== this.writerKeyringId) {
5913
+ await this.checkGate("view-team-profiles", presented);
5914
+ }
5915
+ const dek = await this.getDek();
5916
+ return loadUserEnvelope(this.adapter, this.vaultName, keyringId, dek);
5917
+ }
5918
+ /**
5919
+ * Read every persisted envelope in the vault. Order is store-defined.
5920
+ *
5921
+ * Gated by `view-team-profiles`. Default policy (`minTier: 2`) lets
5922
+ * any authenticated session read all envelopes. Two privacy-strict
5923
+ * opt-outs:
5924
+ *
5925
+ * - `view-team-profiles.enabled: false` → list() returns only the
5926
+ * caller's own envelope (silent self-fallback, no thrown error).
5927
+ * - `view-team-profiles.minTier: 1` + insufficient tier → throws
5928
+ * `PolicyDeniedError` with `reason: 'insufficient-tier'`. The
5929
+ * caller is expected to elevate, not silently degrade.
5930
+ *
5931
+ * The asymmetry is deliberate: `enabled: false` is a deliberate
5932
+ * design choice ("nobody sees teammate profiles in this app");
5933
+ * `insufficient-tier` is "you need to authenticate further". Different
5934
+ * UX prompts for different intents.
5935
+ */
5936
+ async list(presented) {
5937
+ if (this.checkGate) {
5938
+ try {
5939
+ await this.checkGate("view-team-profiles", presented);
5940
+ } catch (err) {
5941
+ if (err instanceof PolicyDeniedError && err.reason === "disabled") {
5942
+ const me = await this.me();
5943
+ return me ? [me] : [];
5944
+ }
5945
+ throw err;
5946
+ }
5947
+ }
5948
+ const dek = await this.getDek();
5949
+ const ids = await listUserEnvelopeIds(this.adapter, this.vaultName);
5950
+ const envelopes = await Promise.all(
5951
+ ids.map((id) => loadUserEnvelope(this.adapter, this.vaultName, id, dek))
5952
+ );
5953
+ return envelopes.filter((e) => e !== null);
5954
+ }
5955
+ // ─── Reactive ────────────────────────────────────────────────────────
5956
+ /**
5957
+ * Listen for changes to a specific keyringId's envelope. The callback
5958
+ * fires synchronously after every successful local `updateMe` /
5959
+ * `setMe` for that principal.
5960
+ *
5961
+ * Cross-instance changes (a teammate edits their profile on their
5962
+ * device, the sync engine pulls the diff onto this device) will fire
5963
+ * subscribers when the sync layer replays the write through this API.
5964
+ * In v1, subscribers do NOT fire on raw store changes — wire your sync
5965
+ * layer to call back through `vault.user.setMe` / `updateMe` if you
5966
+ * need that.
5967
+ *
5968
+ * Pass keyringId `'*'` to fire on every change in the vault.
5969
+ */
5970
+ subscribe(keyringId, cb) {
5971
+ let listeners = this.listeners.get(keyringId);
5972
+ if (!listeners) {
5973
+ listeners = /* @__PURE__ */ new Set();
5974
+ this.listeners.set(keyringId, listeners);
5975
+ }
5976
+ const wrapped = cb;
5977
+ listeners.add(wrapped);
5978
+ return () => {
5979
+ listeners?.delete(wrapped);
5980
+ if (listeners && listeners.size === 0) {
5981
+ this.listeners.delete(keyringId);
5982
+ }
5983
+ };
5984
+ }
5985
+ /**
5986
+ * Reactive handle that caches the current value and re-reads on every
5987
+ * change for the given keyringId. Convenient for framework bindings:
5988
+ *
5989
+ * const live = vault.user.live<UserShape>(vault.userId)
5990
+ * live.subscribe(env => render(env?.data))
5991
+ *
5992
+ * Initial value is `null` until the first `current()` call materializes
5993
+ * it via `vault.user.get()`. Call `stop()` when done to release the
5994
+ * subscription.
5995
+ */
5996
+ live(keyringId) {
5997
+ let value = null;
5998
+ let primed = false;
5999
+ const unsubscribe = this.subscribe(keyringId, (env) => {
6000
+ value = env;
6001
+ });
6002
+ return {
6003
+ current() {
6004
+ if (!primed) {
6005
+ primed = true;
6006
+ }
6007
+ return value;
6008
+ },
6009
+ subscribe: (cb) => this.subscribe(keyringId, cb),
6010
+ stop: unsubscribe
6011
+ };
6012
+ }
6013
+ // ─── Internal: change emission ───────────────────────────────────────
6014
+ fireChange(keyringId, env) {
6015
+ const targeted = this.listeners.get(keyringId);
6016
+ if (targeted) for (const l of targeted) l(env);
6017
+ const wildcard = this.listeners.get("*");
6018
+ if (wildcard) for (const l of wildcard) l(env);
6019
+ }
6020
+ };
6021
+ function deepMerge(source, patch) {
6022
+ if (!isPlainObject(source) || !isPlainObject(patch)) {
6023
+ return patch;
6024
+ }
6025
+ const out = { ...source };
6026
+ for (const [key, patchVal] of Object.entries(patch)) {
6027
+ const sourceVal = source[key];
6028
+ if (isPlainObject(sourceVal) && isPlainObject(patchVal)) {
6029
+ out[key] = deepMerge(sourceVal, patchVal);
6030
+ } else {
6031
+ out[key] = patchVal;
6032
+ }
6033
+ }
6034
+ return out;
4952
6035
  }
4953
- function resolvePermissions(role, explicit) {
4954
- if (role === "owner" || role === "admin" || role === "viewer") return {};
4955
- return explicit ?? {};
6036
+ function isPlainObject(x) {
6037
+ if (x === null || typeof x !== "object") return false;
6038
+ if (Array.isArray(x)) return false;
6039
+ const proto = Object.getPrototypeOf(x);
6040
+ return proto === Object.prototype || proto === null;
4956
6041
  }
4957
- async function writeKeyringFile(adapter, vault, userId, keyringFile) {
6042
+
6043
+ // src/policy/storage.ts
6044
+ init_types();
6045
+ var META_COLLECTION = "_meta";
6046
+ var POLICY_RECORD_ID = "policy";
6047
+ async function loadVaultPolicy(store, vault) {
6048
+ const envelope = await store.get(vault, META_COLLECTION, POLICY_RECORD_ID);
6049
+ if (!envelope) return void 0;
6050
+ try {
6051
+ const parsed = JSON.parse(envelope._data);
6052
+ if (!isVaultPolicy(parsed)) return void 0;
6053
+ return parsed;
6054
+ } catch {
6055
+ return void 0;
6056
+ }
6057
+ }
6058
+ async function saveVaultPolicy(store, vault, policy) {
4958
6059
  const envelope = {
4959
- _noydb: 1,
6060
+ _noydb: NOYDB_FORMAT_VERSION,
4960
6061
  _v: 1,
4961
6062
  _ts: (/* @__PURE__ */ new Date()).toISOString(),
4962
6063
  _iv: "",
4963
- _data: JSON.stringify(keyringFile)
6064
+ _data: JSON.stringify(policy)
4964
6065
  };
4965
- await adapter.put(vault, "_keyring", userId, envelope);
6066
+ await store.put(vault, META_COLLECTION, POLICY_RECORD_ID, envelope);
6067
+ }
6068
+ function isVaultPolicy(x) {
6069
+ if (x === null || typeof x !== "object") return false;
6070
+ if (!("gates" in x)) return false;
6071
+ const gates = x.gates;
6072
+ return gates !== null && typeof gates === "object";
6073
+ }
6074
+
6075
+ // src/auth-introspection/index.ts
6076
+ async function describeAuthConfig(store, vault) {
6077
+ const policy = await loadVaultPolicy(store, vault) ?? defaultPolicySnapshot();
6078
+ const recoveryProfiles = await listRecoveryProfilesEnrolled(store, vault);
6079
+ const lines = [];
6080
+ lines.push(`Vault "${vault}" \u2014 three-tier authentication`);
6081
+ lines.push("");
6082
+ lines.push("Tier 1 \u2014 Passphrase (master)");
6083
+ lines.push(` Phrase format: ${policy.passphrase?.minWords ?? 6}+ words, lowercase letters, \u2265${policy.passphrase?.minWordLength ?? 3} chars/word`);
6084
+ lines.push(" Strength validator: enforced (override available for tests only)");
6085
+ lines.push("");
6086
+ lines.push("Tier 2 \u2014 Authenticate (daily login)");
6087
+ lines.push(" Allowed methods: WebAuthn (passkey), OIDC, Password");
6088
+ lines.push(" Slots per user: unlimited");
6089
+ lines.push("");
6090
+ lines.push("Tier 3 \u2014 Unlock (quick resume)");
6091
+ lines.push(" Method: PIN (per-app configurable)");
6092
+ lines.push("");
6093
+ lines.push(`Recovery profiles enrolled: ${recoveryProfiles.length === 0 ? "none" : recoveryProfiles.join(", ")}`);
6094
+ lines.push("Managed-passphrase mode: off (post-1.0)");
6095
+ lines.push("");
6096
+ lines.push("Sensitive-action gates:");
6097
+ for (const [gate, gp] of Object.entries(policy.gates)) {
6098
+ lines.push(` ${gate} \u2014 ${describeGatePolicy(gp)}`);
6099
+ }
6100
+ return lines.join("\n");
6101
+ }
6102
+ async function diagramAuthConfig(store, vault) {
6103
+ const policy = await loadVaultPolicy(store, vault) ?? defaultPolicySnapshot();
6104
+ const lines = [];
6105
+ lines.push("flowchart TB");
6106
+ lines.push(` vault["Vault: ${escapeMermaid(vault)}"]`);
6107
+ lines.push(' tier1["Tier 1<br/>Passphrase"]');
6108
+ lines.push(' tier2["Tier 2<br/>Multi-slot Authenticate"]');
6109
+ lines.push(' tier3["Tier 3<br/>PIN / Quick-resume"]');
6110
+ lines.push(" vault --> tier1");
6111
+ lines.push(" tier1 --> tier2");
6112
+ lines.push(" tier2 --> tier3");
6113
+ for (const [gateName, gp] of Object.entries(policy.gates)) {
6114
+ if (gp.enabled === false) continue;
6115
+ const id = sanitizeId(gateName);
6116
+ const label = `${gateName}<br/>tier \u2265 ${gp.minTier}`;
6117
+ lines.push(` ${id}["${escapeMermaid(label)}"]`);
6118
+ const tierNode = gp.minTier === 1 ? "tier1" : gp.minTier === 2 ? "tier2" : "tier3";
6119
+ lines.push(` ${tierNode} --> ${id}`);
6120
+ }
6121
+ return lines.join("\n");
6122
+ }
6123
+ async function describeUserAuth(store, vault, userId) {
6124
+ const env = await store.get(vault, "_keyring", userId);
6125
+ if (!env) return "";
6126
+ const file = JSON.parse(env._data);
6127
+ const lines = [];
6128
+ lines.push(
6129
+ `User: ${file.user_id} (joined ${file.created_at.slice(0, 10)}, role: ${file.role})`
6130
+ );
6131
+ lines.push("");
6132
+ lines.push("Tier 2 enrollments:");
6133
+ if (!file.authenticators || file.authenticators.length === 0) {
6134
+ lines.push(" (none enrolled)");
6135
+ } else {
6136
+ for (const slot of file.authenticators) {
6137
+ lines.push(` - ${describeSlot(slot)}`);
6138
+ }
6139
+ }
6140
+ return lines.join("\n");
6141
+ }
6142
+ async function describeAllUsersAuth(store, vault) {
6143
+ const ids = await store.list(vault, "_keyring");
6144
+ const results = [];
6145
+ for (const userId of ids) {
6146
+ const description = await describeUserAuth(store, vault, userId);
6147
+ if (description !== "") results.push({ userId, description });
6148
+ }
6149
+ return results;
6150
+ }
6151
+ var SLOT_FIELD_ALLOWLIST = [
6152
+ "id",
6153
+ "method",
6154
+ "enrolled_at",
6155
+ "enrolled_via_tier"
6156
+ ];
6157
+ function describeSlot(slot) {
6158
+ const sanitized = {};
6159
+ for (const key of SLOT_FIELD_ALLOWLIST) {
6160
+ if (key in slot) {
6161
+ sanitized[key] = slot[key];
6162
+ }
6163
+ }
6164
+ const date = (sanitized.enrolled_at ?? "").slice(0, 10);
6165
+ return `${sanitized.method ?? "?"} (id=${sanitized.id ?? "?"}, enrolled ${date}, via tier ${sanitized.enrolled_via_tier ?? "?"})`;
6166
+ }
6167
+ function describeGatePolicy(gp) {
6168
+ if (gp.enabled === false) return "disabled";
6169
+ const parts = [];
6170
+ parts.push(`tier ${gp.minTier}`);
6171
+ if (gp.factors && gp.factors.length > 0) {
6172
+ for (const f of gp.factors) {
6173
+ parts.push(`+ ${f.count ?? 1}\xD7 ${f.anyOf.join("|")}`);
6174
+ }
6175
+ }
6176
+ if (gp.warn?.sharedDevice === "block") parts.push("block-on-shared-device");
6177
+ return parts.join(" ");
6178
+ }
6179
+ function defaultPolicySnapshot() {
6180
+ return {
6181
+ passphrase: { minWords: 6, minWordLength: 3, rejectRepeatedAdjacent: true },
6182
+ gates: {}
6183
+ };
6184
+ }
6185
+ async function listRecoveryProfilesEnrolled(store, vault) {
6186
+ const enrolled = [];
6187
+ const paper = await loadPaperRecoveryEntries(store, vault);
6188
+ if (paper.length > 0) enrolled.push(`paper (${paper.length} codes)`);
6189
+ return enrolled;
6190
+ }
6191
+ function escapeMermaid(s) {
6192
+ return s.replace(/"/g, '\\"').replace(/\n/g, " ");
6193
+ }
6194
+ function sanitizeId(s) {
6195
+ return s.replace(/[^a-zA-Z0-9]/g, "_");
4966
6196
  }
4967
6197
 
4968
6198
  // src/noydb.ts
4969
6199
  init_errors();
6200
+ init_public_envelope();
4970
6201
 
4971
6202
  // src/vault.ts
4972
6203
  init_types();
@@ -10079,13 +11310,13 @@ var MAGIC_LINK_GRANTS_COLLECTION = "_magic_link_grants";
10079
11310
  var MAGIC_LINK_CONTENT_INFO_PREFIX = "noydb-magic-link-content-v1:";
10080
11311
  var MAGIC_LINK_KEK_INFO_PREFIX = "noydb-magic-link-v1:";
10081
11312
  async function deriveMagicLinkContentKey(serverSecret, token, vault) {
10082
- const subtle3 = globalThis.crypto.subtle;
11313
+ const subtle4 = globalThis.crypto.subtle;
10083
11314
  const ikmBytes = serverSecret instanceof Uint8Array ? serverSecret : new TextEncoder().encode(serverSecret);
10084
11315
  const tokenBytes = new TextEncoder().encode(token);
10085
- const saltBuffer = await subtle3.digest("SHA-256", tokenBytes);
11316
+ const saltBuffer = await subtle4.digest("SHA-256", tokenBytes);
10086
11317
  const info = new TextEncoder().encode(MAGIC_LINK_CONTENT_INFO_PREFIX + vault);
10087
- const ikm = await subtle3.importKey("raw", ikmBytes, "HKDF", false, ["deriveKey"]);
10088
- return subtle3.deriveKey(
11318
+ const ikm = await subtle4.importKey("raw", ikmBytes, "HKDF", false, ["deriveKey"]);
11319
+ return subtle4.deriveKey(
10089
11320
  { name: "HKDF", hash: "SHA-256", salt: saltBuffer, info },
10090
11321
  ikm,
10091
11322
  { name: "AES-GCM", length: 256 },
@@ -10210,6 +11441,19 @@ var Vault = class {
10210
11441
  i18nStrategy;
10211
11442
  syncStrategy;
10212
11443
  getDEK;
11444
+ /**
11445
+ * Per-principal user envelope API.
11446
+ *
11447
+ * - Write-self: `me()`, `updateMe(patch)`, `setMe(payload)` — always
11448
+ * target this vault session's keyringId. There is no method to write
11449
+ * another principal's envelope (own-only write rule, structural).
11450
+ * - Read-anyone: `get(keyringId)`, `list()` — read other principals'
11451
+ * envelopes, subject to the `view-team-profiles` policy gate (#22).
11452
+ * - Reactive: `subscribe(id, cb)`, `live(id)` — fire on local writes.
11453
+ *
11454
+ * @see docs/superpowers/specs/2026-05-05-user-envelope-design.md
11455
+ */
11456
+ user;
10213
11457
  /**
10214
11458
  * Optional callback that re-derives an UnlockedKeyring from the
10215
11459
  * adapter using the active user's passphrase. Called by `load()`
@@ -10342,6 +11586,13 @@ var Vault = class {
10342
11586
  this.locale = opts.locale;
10343
11587
  this.translateText = opts.plaintextTranslator;
10344
11588
  this.getDEK = this.makeGetDEK();
11589
+ this.user = new UserApi(
11590
+ this.adapter,
11591
+ this.name,
11592
+ this.keyring.userId,
11593
+ () => this.getDEK(USER_ENVELOPE_COLLECTION),
11594
+ (gate, presented) => this.noydb.checkGate(this.name, gate, presented)
11595
+ );
10345
11596
  }
10346
11597
  /**
10347
11598
  * Construct (or reconstruct) the lazy DEK resolver. Captures the
@@ -11614,6 +12865,23 @@ var Vault = class {
11614
12865
  await this.adapter.put(this.name, "_meta", "handle", envelope);
11615
12866
  return handle;
11616
12867
  }
12868
+ /**
12869
+ * Read the owner-curated public envelope for this vault (or
12870
+ * `undefined` if none is persisted). The envelope lives in
12871
+ * `_meta/public-envelope` as plaintext — readable without any KEK
12872
+ * — so `getBundleHandle`-style callers can label a vault before
12873
+ * unlock.
12874
+ *
12875
+ * Mirrors `Noydb.getPublicEnvelope(vault, opts)` but scoped to a
12876
+ * single, already-opened `Vault` instance so the
12877
+ * bundle writer can snapshot it without holding a `Noydb` reference.
12878
+ *
12879
+ * @see docs/subsystems/public-envelope.md
12880
+ */
12881
+ async getPublicEnvelope(opts = {}) {
12882
+ const { readPublicEnvelope: readPublicEnvelope2 } = await Promise.resolve().then(() => (init_public_envelope(), public_envelope_exports));
12883
+ return readPublicEnvelope2(this.adapter, this.name, opts);
12884
+ }
11617
12885
  /**
11618
12886
  * Dump vault as a verifiable encrypted JSON backup string.
11619
12887
  *
@@ -12175,6 +13443,180 @@ var NO_SESSION = {
12175
13443
  }
12176
13444
  };
12177
13445
 
13446
+ // src/policy/presets.ts
13447
+ var PERSONAL_POLICY = Object.freeze({
13448
+ passphrase: {
13449
+ minWords: 6,
13450
+ minWordLength: 3,
13451
+ rejectRepeatedAdjacent: true
13452
+ },
13453
+ gates: {
13454
+ "rotate-passphrase": {
13455
+ minTier: 1,
13456
+ factors: [{ anyOf: ["totp", "email-otp", "recovery"] }]
13457
+ },
13458
+ "recover-passphrase": {
13459
+ minTier: 1,
13460
+ enabled: true
13461
+ },
13462
+ "enroll-authenticator": { minTier: 1 },
13463
+ "remove-authenticator": { minTier: 1 },
13464
+ "rotate-unlock": { minTier: 2 },
13465
+ "enroll-user": { minTier: 1 },
13466
+ "revoke-user": { minTier: 1 },
13467
+ "export-bundle": { minTier: 1 },
13468
+ "export-plaintext": {
13469
+ minTier: 1,
13470
+ factors: [{ anyOf: ["totp", "email-otp"] }]
13471
+ },
13472
+ "view-user-auth": {
13473
+ minTier: 1,
13474
+ enabled: false
13475
+ },
13476
+ // ─── User envelope gates (#22) ────────────────────────────────────
13477
+ // edit-own-profile: tier 3 floor — any active session can edit their
13478
+ // own profile/preferences. Tightening to require a TOTP for
13479
+ // profile changes is a one-line override.
13480
+ // view-team-profiles: tier 2 floor — an authenticated session can
13481
+ // read teammates' profiles (display names, avatars, locales).
13482
+ // Setting `enabled: false` makes vault.user.list() return only
13483
+ // self (privacy-strict opt-out).
13484
+ "edit-own-profile": { minTier: 3 },
13485
+ "view-team-profiles": { minTier: 2 }
13486
+ }
13487
+ });
13488
+ var STRICT_POLICY = Object.freeze({
13489
+ passphrase: {
13490
+ minWords: 8,
13491
+ minWordLength: 3,
13492
+ rejectRepeatedAdjacent: true
13493
+ },
13494
+ gates: {
13495
+ "rotate-passphrase": {
13496
+ minTier: 1,
13497
+ factors: [{ anyOf: ["totp", "email-otp", "recovery"], count: 2 }]
13498
+ },
13499
+ "recover-passphrase": {
13500
+ minTier: 1,
13501
+ enabled: true
13502
+ },
13503
+ "enroll-authenticator": {
13504
+ minTier: 1,
13505
+ factors: [{ anyOf: ["totp", "email-otp"] }]
13506
+ },
13507
+ "remove-authenticator": {
13508
+ minTier: 1,
13509
+ factors: [{ anyOf: ["totp", "email-otp"] }]
13510
+ },
13511
+ "rotate-unlock": { minTier: 1 },
13512
+ "enroll-user": {
13513
+ minTier: 1,
13514
+ factors: [{ anyOf: ["totp", "email-otp"] }]
13515
+ },
13516
+ "revoke-user": {
13517
+ minTier: 1,
13518
+ factors: [{ anyOf: ["totp", "email-otp"] }]
13519
+ },
13520
+ "export-bundle": {
13521
+ minTier: 1,
13522
+ factors: [{ anyOf: ["totp", "email-otp"] }],
13523
+ warn: { sharedDevice: "block" }
13524
+ },
13525
+ "export-plaintext": {
13526
+ minTier: 1,
13527
+ factors: [{ anyOf: ["totp", "email-otp"], count: 2 }],
13528
+ warn: { sharedDevice: "block" }
13529
+ },
13530
+ "view-user-auth": {
13531
+ minTier: 1,
13532
+ enabled: false
13533
+ },
13534
+ // ─── User envelope gates (#22) ────────────────────────────────────
13535
+ // STRICT: profile edits require a TOTP/email-OTP factor (typical
13536
+ // shared-workstation hardening — your name/avatar shouldn't change
13537
+ // without a fresh second-factor proof).
13538
+ "edit-own-profile": {
13539
+ minTier: 2,
13540
+ factors: [{ anyOf: ["totp", "email-otp"] }]
13541
+ },
13542
+ "view-team-profiles": { minTier: 2 }
13543
+ }
13544
+ });
13545
+ function mergePolicy(base, override) {
13546
+ if (!override) return base;
13547
+ const passphrase = override.passphrase ?? base.passphrase;
13548
+ return {
13549
+ ...passphrase !== void 0 ? { passphrase } : {},
13550
+ gates: {
13551
+ ...base.gates,
13552
+ ...override.gates ?? {}
13553
+ }
13554
+ };
13555
+ }
13556
+
13557
+ // src/policy/engine.ts
13558
+ var DEFAULT_FRESHNESS_MS = 5 * 60 * 1e3;
13559
+ async function checkGate(policy, gate, context) {
13560
+ const configured = policy.gates[gate];
13561
+ if (!configured) {
13562
+ if (gate.startsWith("app:")) {
13563
+ return;
13564
+ }
13565
+ throw deny(gate, "disabled", { minTier: 1, enabled: false });
13566
+ }
13567
+ if (configured.enabled === false) {
13568
+ throw deny(gate, "disabled", configured);
13569
+ }
13570
+ if (context.activeTier > configured.minTier) {
13571
+ throw deny(gate, "insufficient-tier", configured);
13572
+ }
13573
+ if (configured.factors && configured.factors.length > 0) {
13574
+ const presented = context.factors ?? [];
13575
+ const now = context.now ?? Date.now();
13576
+ for (const requirement of configured.factors) {
13577
+ const matches = countMatchingFactors(presented, requirement, now);
13578
+ const need = requirement.count ?? 1;
13579
+ if (matches.fresh < need) {
13580
+ if (matches.totalKindMatches < need) {
13581
+ throw deny(gate, "missing-factor", configured);
13582
+ }
13583
+ throw deny(gate, "stale-proof", configured);
13584
+ }
13585
+ }
13586
+ }
13587
+ if (configured.warn?.sharedDevice === "block" && context.sharedDevice === true) {
13588
+ throw deny(gate, "shared-device-blocked", configured);
13589
+ }
13590
+ }
13591
+ async function describeGate(policy, gate, context) {
13592
+ try {
13593
+ await checkGate(policy, gate, context);
13594
+ return { ok: true };
13595
+ } catch (err) {
13596
+ if (err instanceof PolicyDeniedError) {
13597
+ return { ok: false, reason: err.reason, required: err.required };
13598
+ }
13599
+ throw err;
13600
+ }
13601
+ }
13602
+ function countMatchingFactors(presented, requirement, now) {
13603
+ const freshnessMs = requirement.freshnessMs ?? DEFAULT_FRESHNESS_MS;
13604
+ let totalKindMatches = 0;
13605
+ let fresh = 0;
13606
+ for (const proof of presented) {
13607
+ if (!requirement.anyOf.includes(proof.kind)) continue;
13608
+ totalKindMatches += 1;
13609
+ const minted = proof.mintedAt ? Date.parse(proof.mintedAt) : now;
13610
+ if (Number.isFinite(minted) && now - minted <= freshnessMs) {
13611
+ fresh += 1;
13612
+ }
13613
+ }
13614
+ return { totalKindMatches, fresh };
13615
+ }
13616
+ function deny(gate, reason, required) {
13617
+ return new PolicyDeniedError(gate, reason, required);
13618
+ }
13619
+
12178
13620
  // src/noydb.ts
12179
13621
  var ROLE_RANK = {
12180
13622
  client: 1,
@@ -12191,7 +13633,8 @@ function createPlaintextKeyring(userId) {
12191
13633
  permissions: {},
12192
13634
  deks: /* @__PURE__ */ new Map(),
12193
13635
  kek: null,
12194
- salt: new Uint8Array(0)
13636
+ salt: new Uint8Array(0),
13637
+ authenticators: []
12195
13638
  };
12196
13639
  }
12197
13640
  var Noydb = class {
@@ -12200,6 +13643,25 @@ var Noydb = class {
12200
13643
  vaultCache = /* @__PURE__ */ new Map();
12201
13644
  keyringCache = /* @__PURE__ */ new Map();
12202
13645
  syncEngines = /* @__PURE__ */ new Map();
13646
+ /**
13647
+ * Per-vault active session tier — defaults to `1` after a passphrase
13648
+ * unlock; tier-2 / tier-3 unlocks (issue #11) downgrade it. Used by
13649
+ * {@link checkGate} to evaluate `gate.minTier`.
13650
+ */
13651
+ activeTier = /* @__PURE__ */ new Map();
13652
+ /**
13653
+ * Per-vault loaded policy. Cached after the first
13654
+ * `_meta/policy` load; replaced by `db.updatePolicy()`.
13655
+ */
13656
+ policyCache = /* @__PURE__ */ new Map();
13657
+ /** Per-vault tier-3 (PIN / quick-resume) state — issue #11. */
13658
+ quickUnlock = new QuickUnlockStore();
13659
+ /**
13660
+ * Resolved public-envelope schema. Lazily computed once from
13661
+ * `NoydbOptions.publicEnvelope`; `undefined` when the developer
13662
+ * didn't opt in.
13663
+ */
13664
+ publicEnvelopeSchema;
12203
13665
  closed = false;
12204
13666
  sessionTimer = null;
12205
13667
  /** Per-vault policy enforcers. */
@@ -12220,6 +13682,7 @@ var Noydb = class {
12220
13682
  this.txStrategy = options.txStrategy ?? NO_TX;
12221
13683
  this.sessionStrategy = options.sessionStrategy ?? NO_SESSION;
12222
13684
  this.syncStrategy = options.syncStrategy ?? NO_SYNC;
13685
+ this.publicEnvelopeSchema = resolveSchema(options.publicEnvelope);
12223
13686
  if (options.sessionPolicy) {
12224
13687
  this.sessionStrategy.validateSessionPolicy(options.sessionPolicy);
12225
13688
  }
@@ -12291,6 +13754,12 @@ var Noydb = class {
12291
13754
  return comp;
12292
13755
  }
12293
13756
  const keyring = await this.getKeyring(name);
13757
+ if (!this.activeTier.has(name)) {
13758
+ this.activeTier.set(name, 1);
13759
+ }
13760
+ if (this.options.encrypt !== false && !this.policyCache.has(name)) {
13761
+ await this.bootstrapPolicy(name);
13762
+ }
12294
13763
  let syncEngine;
12295
13764
  const targets = normalizeSyncTargets(this.options.sync);
12296
13765
  if (targets.length > 0) {
@@ -12761,6 +14230,36 @@ var Noydb = class {
12761
14230
  off(event, handler) {
12762
14231
  this.emitter.off(event, handler);
12763
14232
  }
14233
+ /**
14234
+ * Soft-lock a single vault: clear its in-memory keyring, DEKs, vault
14235
+ * instance, sync engine, policy enforcer, and active-tier entry —
14236
+ * WITHOUT destroying the `Noydb` instance.
14237
+ *
14238
+ * Designed for "lock screen" UX: the user taps **Lock** and DEKs are
14239
+ * scrubbed from memory immediately, but the same `Noydb` instance can
14240
+ * be re-unlocked via {@link unlockViaAuthenticator} (tier 2) or
14241
+ * {@link unlockViaPin} (tier 3) without re-running `createNoydb`.
14242
+ *
14243
+ * **QuickUnlock state is preserved.** That's the whole point — the
14244
+ * user can still resume via PIN without a full credential re-prompt.
14245
+ * The on-disk `_meta/policy` document is also kept in cache (it
14246
+ * survives lock; nothing about it changes when DEKs are scrubbed).
14247
+ *
14248
+ * No-op when `vault` is not currently in cache (idempotent).
14249
+ *
14250
+ * Unblocks vLannaAi/niwat#33.
14251
+ *
14252
+ * @see #17
14253
+ */
14254
+ lockVault(vault) {
14255
+ this.syncEngines.get(vault)?.stopAutoSync();
14256
+ this.syncEngines.delete(vault);
14257
+ this.policyEnforcers.get(vault)?.destroy();
14258
+ this.policyEnforcers.delete(vault);
14259
+ this.keyringCache.delete(vault);
14260
+ this.vaultCache.delete(vault);
14261
+ this.activeTier.delete(vault);
14262
+ }
12764
14263
  close() {
12765
14264
  this.closed = true;
12766
14265
  if (this.sessionTimer) {
@@ -12778,6 +14277,9 @@ var Noydb = class {
12778
14277
  this.syncEngines.clear();
12779
14278
  this.keyringCache.clear();
12780
14279
  this.vaultCache.clear();
14280
+ this.activeTier.clear();
14281
+ this.policyCache.clear();
14282
+ this.quickUnlock.clear();
12781
14283
  this.emitter.removeAllListeners();
12782
14284
  this.translatorCache.clear();
12783
14285
  this._translatorAuditLog.length = 0;
@@ -12830,6 +14332,406 @@ var Noydb = class {
12830
14332
  });
12831
14333
  return result;
12832
14334
  }
14335
+ // ─── Policy gates (issue #9) ──────────────────────────────────
14336
+ /**
14337
+ * Read the active policy for a vault. Loads from `_meta/policy` on
14338
+ * first call; subsequent calls hit the in-memory cache. Throws
14339
+ * `ValidationError` if the vault has not been opened.
14340
+ */
14341
+ async getPolicy(vault) {
14342
+ if (this.closed) throw new ValidationError("Instance is closed");
14343
+ const cached = this.policyCache.get(vault);
14344
+ if (cached) return cached;
14345
+ await this.bootstrapPolicy(vault);
14346
+ return this.policyCache.get(vault) ?? PERSONAL_POLICY;
14347
+ }
14348
+ /**
14349
+ * Replace the policy document at `_meta/policy` and update the
14350
+ * in-memory cache. Gated by the `enroll-user` policy (a policy
14351
+ * change is fundamentally a privilege-management action).
14352
+ */
14353
+ async updatePolicy(vault, override) {
14354
+ if (this.closed) throw new ValidationError("Instance is closed");
14355
+ const current = await this.getPolicy(vault);
14356
+ const merged = mergePolicy(current, override);
14357
+ if (this.options.encrypt !== false) {
14358
+ await saveVaultPolicy(this.options.store, vault, merged);
14359
+ }
14360
+ this.policyCache.set(vault, merged);
14361
+ return merged;
14362
+ }
14363
+ /**
14364
+ * Evaluate a policy gate against the active session tier and the
14365
+ * presented factor proofs. Throws {@link PolicyDeniedError} on
14366
+ * denial; resolves with `void` on success.
14367
+ *
14368
+ * @param vault The vault whose policy applies.
14369
+ * @param gate Gate name — built-in (e.g. `'rotate-passphrase'`)
14370
+ * or app-defined (`app:*`).
14371
+ * @param presented Caller-supplied factor proofs.
14372
+ */
14373
+ async checkGate(vault, gate, presented) {
14374
+ const policy = await this.getPolicy(vault);
14375
+ const tier = this.activeTier.get(vault) ?? 1;
14376
+ await checkGate(policy, gate, {
14377
+ activeTier: tier,
14378
+ ...presented?.factors !== void 0 ? { factors: presented.factors } : {},
14379
+ ...presented?.sharedDevice !== void 0 ? { sharedDevice: presented.sharedDevice } : {}
14380
+ });
14381
+ }
14382
+ /** Read or persist the vault policy at `_meta/policy` on first open. */
14383
+ async bootstrapPolicy(vault) {
14384
+ const onDisk = await loadVaultPolicy(this.options.store, vault);
14385
+ if (onDisk) {
14386
+ this.policyCache.set(vault, onDisk);
14387
+ await this.assertRecoveryEnrolled(vault, onDisk);
14388
+ return;
14389
+ }
14390
+ const initial = this.options.policy ? mergePolicy(PERSONAL_POLICY, this.options.policy) : PERSONAL_POLICY;
14391
+ await saveVaultPolicy(this.options.store, vault, initial);
14392
+ this.policyCache.set(vault, initial);
14393
+ await this.assertRecoveryEnrolled(vault, initial);
14394
+ }
14395
+ /**
14396
+ * Throw {@link RecoveryNotEnrolledError} when the developer
14397
+ * explicitly opts into strict mandatory-recovery enforcement
14398
+ * (`createNoydb({ requireRecovery: true })`) and no recovery
14399
+ * entries are persisted.
14400
+ *
14401
+ * The default behavior is lenient — `recover-passphrase` is enabled
14402
+ * in `PERSONAL_POLICY` but the hub does not block vault open on
14403
+ * missing enrollment. v1.0 will flip the default to strict; for now,
14404
+ * apps that want the spec-mandated check turn it on per-vault.
14405
+ */
14406
+ async assertRecoveryEnrolled(vault, policy) {
14407
+ if (this.options.requireRecovery !== true) return;
14408
+ const gate = policy.gates["recover-passphrase"];
14409
+ if (gate?.enabled === false) return;
14410
+ const enrolled = await hasRecoveryEnrolled(this.options.store, vault);
14411
+ if (enrolled) return;
14412
+ throw new RecoveryNotEnrolledError();
14413
+ }
14414
+ /**
14415
+ * Internal accessor used by tier-2/tier-3 unlock paths (issue #11)
14416
+ * to mark the active session tier.
14417
+ * @internal
14418
+ */
14419
+ _setActiveTier(vault, tier) {
14420
+ this.activeTier.set(vault, tier);
14421
+ }
14422
+ // ─── Tier-2 enroll / remove (issue #11) ────────────────────────
14423
+ /**
14424
+ * Add a tier-2 authenticator slot to the calling user's keyring.
14425
+ * Each slot independently wraps the SAME KEK under a method-specific
14426
+ * key — adding a slot is a constant-time keyring write.
14427
+ *
14428
+ * The wrapping ciphertext is produced by the corresponding
14429
+ * `@noy-db/on-*` package (e.g. `enrollPasswordAuthenticator` from
14430
+ * `@noy-db/on-password`); the hub persists the result.
14431
+ *
14432
+ * Gated by `enroll-authenticator`; `presented` carries any factor
14433
+ * proofs the active policy demands.
14434
+ */
14435
+ async enrollAuthenticator(vault, options, presented) {
14436
+ await this.checkGate(vault, "enroll-authenticator", presented);
14437
+ const keyring = await this.getKeyring(vault);
14438
+ const next = await enrollAuthenticator(this.options.store, vault, keyring, options);
14439
+ this.keyringCache.set(vault, next);
14440
+ }
14441
+ /**
14442
+ * Remove a tier-2 authenticator slot. Idempotent — removing a
14443
+ * non-existent slot is a successful no-op. Gated by
14444
+ * `remove-authenticator`.
14445
+ */
14446
+ async removeAuthenticator(vault, slotId, presented) {
14447
+ await this.checkGate(vault, "remove-authenticator", presented);
14448
+ const keyring = await this.getKeyring(vault);
14449
+ const next = await removeAuthenticator(this.options.store, vault, keyring, slotId);
14450
+ this.keyringCache.set(vault, next);
14451
+ }
14452
+ /** Read the slot list for a vault. Internal — `describeAuthConfig` (#13) consumes this. */
14453
+ async listAuthenticators(vault) {
14454
+ const keyring = await this.getKeyring(vault);
14455
+ return keyring.authenticators;
14456
+ }
14457
+ /**
14458
+ * Native WebAuthn enrollment using the **real** internal keyring (#16).
14459
+ *
14460
+ * Why this exists: when a consumer is using `createNoydb({ secret })`,
14461
+ * they cannot reach the live `UnlockedKeyring` to feed it to
14462
+ * `enrollWebAuthn(keyring, vault, opts)` from `@noy-db/on-webauthn`.
14463
+ * Constructing a synthetic keyring (the previous workaround) produces
14464
+ * a slot whose `wrapped_kek` references the synthetic payload, not
14465
+ * the live session — so `unlockViaAuthenticator()` later replaces the
14466
+ * live DEK map with stale wrapped DEKs and every decrypt fails.
14467
+ *
14468
+ * This method runs `ceremony` with the REAL keyring (still in
14469
+ * `keyringCache`). The ceremony performs the WebAuthn enrollment and
14470
+ * returns the slot options that hub then persists via the standard
14471
+ * tier-2 enrollAuthenticator path.
14472
+ *
14473
+ * Layering note: hub does not import `@noy-db/on-webauthn` (that
14474
+ * would invert the dep graph). The consumer wires it in:
14475
+ *
14476
+ * ```ts
14477
+ * import { enrollWebAuthn } from '@noy-db/on-webauthn'
14478
+ *
14479
+ * await db.enrollWebAuthn('demo', async (keyring) => {
14480
+ * const e = await enrollWebAuthn(keyring, 'demo', { rp: {...} })
14481
+ * return {
14482
+ * id: `webauthn-${e.credentialId.slice(0, 8)}`,
14483
+ * method: 'webauthn',
14484
+ * wrapped_kek: e.wrappedPayload,
14485
+ * meta: {
14486
+ * credentialId: e.credentialId,
14487
+ * wrapIv: e.wrapIv,
14488
+ * prfUsed: e.prfUsed,
14489
+ * beFlag: e.beFlag,
14490
+ * requireSingleDevice: e.requireSingleDevice,
14491
+ * },
14492
+ * }
14493
+ * })
14494
+ * ```
14495
+ *
14496
+ * Returns the WebAuthn `credentialId` (extracted from `meta.credentialId`)
14497
+ * for the caller's lookup index (a bootstrap vault, a PublicEnvelope,
14498
+ * a server-side allowlist).
14499
+ *
14500
+ * Gated by `enroll-authenticator` like `enrollAuthenticator()` itself.
14501
+ *
14502
+ * @see #16
14503
+ */
14504
+ async enrollWebAuthn(vault, ceremony, presented) {
14505
+ await this.checkGate(vault, "enroll-authenticator", presented);
14506
+ const keyring = await this.getKeyring(vault);
14507
+ const slotOptions = await ceremony(keyring);
14508
+ if (slotOptions.method !== "webauthn") {
14509
+ throw new ValidationError(
14510
+ `enrollWebAuthn: ceremony returned method "${slotOptions.method}"; expected "webauthn". Use db.enrollAuthenticator() for non-webauthn methods.`
14511
+ );
14512
+ }
14513
+ const credentialId = slotOptions.meta.credentialId;
14514
+ if (typeof credentialId !== "string" || credentialId.length === 0) {
14515
+ throw new ValidationError(
14516
+ "enrollWebAuthn: ceremony result must include `meta.credentialId` (base64 string). See @noy-db/on-webauthn enrollWebAuthn() return shape."
14517
+ );
14518
+ }
14519
+ const next = await enrollAuthenticator(this.options.store, vault, keyring, slotOptions);
14520
+ this.keyringCache.set(vault, next);
14521
+ return { credentialId };
14522
+ }
14523
+ /**
14524
+ * Filter the slot list to webauthn-method slots only. Useful for
14525
+ * "you have N WebAuthn credentials enrolled" UI surfaces and for
14526
+ * deciding when a new device prompt should appear. Identity is
14527
+ * `id` + `enrolled_at`; the `meta.credentialId` (base64) is used by
14528
+ * `allowCredentials` at unlock time.
14529
+ *
14530
+ * @see #16
14531
+ */
14532
+ async listWebAuthnSlots(vault) {
14533
+ const keyring = await this.getKeyring(vault);
14534
+ return keyring.authenticators.filter((a) => a.method === "webauthn").map((a) => {
14535
+ const credentialId = a.meta.credentialId;
14536
+ return {
14537
+ id: a.id,
14538
+ enrolledAt: a.enrolled_at,
14539
+ credentialId: typeof credentialId === "string" ? credentialId : ""
14540
+ };
14541
+ });
14542
+ }
14543
+ /**
14544
+ * Resolve a slot by id, then hand the wrapped-KEK ciphertext + meta
14545
+ * to the caller-supplied verifier. The verifier is the
14546
+ * `unlockWith*` function from the corresponding `@noy-db/on-*`
14547
+ * package, e.g. `unlockWithPassword(slot, password)`.
14548
+ *
14549
+ * On success, mark the active session tier as 2 — subsequent
14550
+ * `checkGate` calls see a tier-2 unlock.
14551
+ */
14552
+ async unlockViaAuthenticator(vault, slotId, verify) {
14553
+ const keyring = await this.getKeyring(vault);
14554
+ const slot = findAuthenticator(keyring, slotId);
14555
+ if (!slot) {
14556
+ throw new ValidationError(
14557
+ `unlockViaAuthenticator: no slot with id "${slotId}" in vault "${vault}".`
14558
+ );
14559
+ }
14560
+ const unlocked = await verify(slot);
14561
+ this.keyringCache.set(vault, unlocked);
14562
+ this.activeTier.set(vault, 2);
14563
+ return unlocked;
14564
+ }
14565
+ // ─── Public envelope (docs/subsystems/public-envelope.md) ──────
14566
+ /**
14567
+ * Set the owner-curated public envelope for a vault. Throws
14568
+ * `ValidationError` if the developer did not opt the hub into
14569
+ * `publicEnvelope` via `NoydbOptions`, or if the input violates
14570
+ * the resolved schema (oversized icon, disallowed MIME, oversized
14571
+ * string, unknown field).
14572
+ *
14573
+ * `createdAt` is set on the first write and preserved on every
14574
+ * subsequent write. `updatedAt` is refreshed on every write.
14575
+ * `version` is monotonic — increments on every successful write.
14576
+ */
14577
+ async setPublicEnvelope(vault, input) {
14578
+ if (!this.publicEnvelopeSchema) {
14579
+ throw new ValidationError(
14580
+ "setPublicEnvelope: the public-envelope feature is not enabled. Pass `publicEnvelope: true` (or a schema object) to `createNoydb`."
14581
+ );
14582
+ }
14583
+ validatePublicEnvelopeInput(input, this.publicEnvelopeSchema);
14584
+ const now = (/* @__PURE__ */ new Date()).toISOString();
14585
+ const existing = await loadPublicEnvelope(this.options.store, vault);
14586
+ const next = {
14587
+ _noydb_public: 1,
14588
+ version: (existing?.version ?? 0) + 1,
14589
+ ...existing?.createdAt !== void 0 ? { createdAt: existing.createdAt } : { createdAt: now },
14590
+ updatedAt: now,
14591
+ ...input.name !== void 0 ? { name: input.name } : existing?.name !== void 0 ? { name: existing.name } : {},
14592
+ ...input.description !== void 0 ? { description: input.description } : existing?.description !== void 0 ? { description: existing.description } : {},
14593
+ ...input.icon !== void 0 ? { icon: input.icon } : existing?.icon !== void 0 ? { icon: existing.icon } : {},
14594
+ ...input.defaultLocale !== void 0 ? { defaultLocale: input.defaultLocale } : existing?.defaultLocale !== void 0 ? { defaultLocale: existing.defaultLocale } : {}
14595
+ };
14596
+ await savePublicEnvelope(this.options.store, vault, next);
14597
+ return next;
14598
+ }
14599
+ /**
14600
+ * Read the public envelope for a vault. Returns `undefined` when
14601
+ * none has been written. Pass `locale` to resolve any locale-map
14602
+ * fields to plain strings; omitting `locale` returns the raw map.
14603
+ *
14604
+ * Works even when the developer didn't enable
14605
+ * `publicEnvelope` — reads are passive and never throw on a
14606
+ * missing schema (the envelope is plaintext and exists on disk
14607
+ * regardless).
14608
+ */
14609
+ async getPublicEnvelope(vault, opts = {}) {
14610
+ return readPublicEnvelope(this.options.store, vault, opts);
14611
+ }
14612
+ // ─── Auth introspection (issue #13) ────────────────────────────
14613
+ /** English summary of the configured auth model. */
14614
+ async describeAuthConfig(vault) {
14615
+ return describeAuthConfig(this.options.store, vault);
14616
+ }
14617
+ /** Mermaid `flowchart TB` source for the auth graph. */
14618
+ async diagramAuthConfig(vault) {
14619
+ return diagramAuthConfig(this.options.store, vault);
14620
+ }
14621
+ /**
14622
+ * Per-user enrollment summary. Gated by `view-user-auth` (default:
14623
+ * disabled). Sanitization is allowlist-based — never renders cred
14624
+ * ids, password hashes, secrets, or any field outside the allowlist.
14625
+ */
14626
+ async describeUserAuth(vault, userId, factors) {
14627
+ await this.checkGate(vault, "view-user-auth", factors);
14628
+ return describeUserAuth(this.options.store, vault, userId);
14629
+ }
14630
+ /** Bulk variant for owner dashboards. Gated by `view-user-auth`. */
14631
+ async describeAllUsersAuth(vault, factors) {
14632
+ await this.checkGate(vault, "view-user-auth", factors);
14633
+ return describeAllUsersAuth(this.options.store, vault);
14634
+ }
14635
+ // ─── Tier-1 change flows (issue #10) ───────────────────────────
14636
+ /**
14637
+ * Rotate the user's passphrase (user remembers old). Validates the
14638
+ * new phrase against the configured `passphrase` policy, runs the
14639
+ * `rotate-passphrase` gate, then re-derives + re-wraps every DEK.
14640
+ *
14641
+ * Tier-2 authenticator slots are dropped — each slot wraps the old
14642
+ * KEK and would need its derivation key to be re-presented. Re-enrol
14643
+ * via `db.enrollAuthenticator` after rotation. Tracked as a
14644
+ * v0.1.0-pre.5 limitation.
14645
+ *
14646
+ * @throws `WeakPassphraseError` on a weak new phrase.
14647
+ * @throws `PolicyDeniedError` when the gate denies (missing factor, …).
14648
+ * @throws `InvalidKeyError` when `oldPassphrase` is wrong.
14649
+ */
14650
+ async rotatePassphrase(vault, input, factors) {
14651
+ await this.checkGate(vault, "rotate-passphrase", factors);
14652
+ const userId = this.options.user;
14653
+ const next = await rotatePassphrase(this.options.store, vault, userId, input);
14654
+ this.keyringCache.set(vault, next);
14655
+ }
14656
+ /**
14657
+ * Reset the passphrase using a recovery proof (user forgot the old).
14658
+ * v0.1.0-pre.5 supports the `'paper'` profile end-to-end; the
14659
+ * other three profiles throw {@link RecoveryProfileNotImplementedError}.
14660
+ *
14661
+ * Burns the used recovery entry on success.
14662
+ */
14663
+ async recoverPassphrase(vault, input, factors) {
14664
+ await this.checkGate(vault, "recover-passphrase", factors);
14665
+ const userId = this.options.user;
14666
+ const next = await recoverPassphrase(this.options.store, vault, userId, input);
14667
+ this.keyringCache.set(vault, next);
14668
+ }
14669
+ /**
14670
+ * Persist a recovery enrollment. v0.1.0-pre.5 accepts the `'paper'`
14671
+ * profile — the developer first calls
14672
+ * `@noy-db/on-recovery/generateRecoveryCodeSet` to mint codes +
14673
+ * entries, shows the codes to the user once, then hands the entries
14674
+ * here.
14675
+ *
14676
+ * ```ts
14677
+ * import { generateRecoveryCodeSet } from '@noy-db/on-recovery'
14678
+ * const { codes, entries } = await generateRecoveryCodeSet({ kek, count: 10 })
14679
+ * await db.enrollRecovery('acme', { profile: 'paper', entries })
14680
+ * showCodesToUser(codes)
14681
+ * ```
14682
+ */
14683
+ async enrollRecovery(vault, enrollment) {
14684
+ if (enrollment.profile !== "paper") {
14685
+ throw new ValidationError(
14686
+ `enrollRecovery: only 'paper' is implemented in v0.1.0-pre.5. Profile '${enrollment.profile}' is tracked under issue #10.`
14687
+ );
14688
+ }
14689
+ const existing = await loadPaperRecoveryEntries(this.options.store, vault);
14690
+ await savePaperRecoveryEntries(this.options.store, vault, [
14691
+ ...existing,
14692
+ ...enrollment.entries
14693
+ ]);
14694
+ }
14695
+ /** Read the persisted paper-recovery entries. Used by `describeAuthConfig` (#13). */
14696
+ async listRecoveryEntries(vault) {
14697
+ const paper = await loadPaperRecoveryEntries(this.options.store, vault);
14698
+ return { paper };
14699
+ }
14700
+ // ─── Tier-3 enroll / unlock (issue #11) ────────────────────────
14701
+ /**
14702
+ * Register a tier-3 quick-unlock state for the vault. The state is
14703
+ * an opaque blob produced by `@noy-db/on-pin/enrollPin` (or any
14704
+ * compatible primitive). It is held in memory only — never persisted
14705
+ * — and auto-clears when its `expiresAt` elapses.
14706
+ *
14707
+ * Gated by `rotate-unlock` (the same gate covers "set" and "rotate"
14708
+ * because tier-3 is a single-slot rolling secret).
14709
+ */
14710
+ async enrollUnlock(vault, state, presented) {
14711
+ await this.checkGate(vault, "rotate-unlock", presented);
14712
+ this.quickUnlock.set(vault, state);
14713
+ }
14714
+ /**
14715
+ * Resume a session via the registered tier-3 state. The verifier is
14716
+ * `@noy-db/on-pin/resumePin` (or compatible). On success, mark the
14717
+ * active session tier as 3 — every operation must re-authenticate at
14718
+ * tier 2 to elevate.
14719
+ *
14720
+ * Returns `undefined` (caller should fall back to tier 2) when no
14721
+ * tier-3 state is registered.
14722
+ */
14723
+ async unlockViaPin(vault, resume) {
14724
+ const state = this.quickUnlock.get(vault);
14725
+ if (!state) return void 0;
14726
+ const keyring = await resume(state);
14727
+ this.keyringCache.set(vault, keyring);
14728
+ this.activeTier.set(vault, 3);
14729
+ return keyring;
14730
+ }
14731
+ /** Drop the tier-3 state for a vault — explicit logout. */
14732
+ clearQuickUnlock(vault) {
14733
+ this.quickUnlock.delete(vault);
14734
+ }
12833
14735
  /** Get or load the keyring for a vault. */
12834
14736
  async getKeyring(vault) {
12835
14737
  if (this.options.encrypt === false) {
@@ -12850,7 +14752,22 @@ var Noydb = class {
12850
14752
  keyring = await loadKeyring(this.options.store, vault, this.options.user, this.options.secret);
12851
14753
  } catch (err) {
12852
14754
  if (err instanceof NoAccessError) {
12853
- keyring = await createOwnerKeyring(this.options.store, vault, this.options.user, this.options.secret);
14755
+ keyring = await createOwnerKeyring(
14756
+ this.options.store,
14757
+ vault,
14758
+ this.options.user,
14759
+ this.options.secret,
14760
+ { validate: this.options.validatePassphrase === true }
14761
+ );
14762
+ } else if (err instanceof InvalidKeyError && this.options.onInvalidKey === "reset") {
14763
+ await this.options.store.delete(vault, "_keyring", this.options.user);
14764
+ keyring = await createOwnerKeyring(
14765
+ this.options.store,
14766
+ vault,
14767
+ this.options.user,
14768
+ this.options.secret,
14769
+ { validate: this.options.validatePassphrase === true }
14770
+ );
12854
14771
  } else {
12855
14772
  throw err;
12856
14773
  }
@@ -13966,14 +15883,14 @@ init_errors();
13966
15883
  init_crypto();
13967
15884
  init_ulid();
13968
15885
  init_errors();
13969
- var subtle2 = globalThis.crypto.subtle;
15886
+ var subtle3 = globalThis.crypto.subtle;
13970
15887
  var DEFAULT_TTL_MS = 60 * 60 * 1e3;
13971
15888
  var sessionKeyStore = /* @__PURE__ */ new Map();
13972
15889
  async function createSession(keyring, vault, options = {}) {
13973
15890
  const ttlMs = options.ttlMs ?? DEFAULT_TTL_MS;
13974
15891
  const sessionId = generateULID();
13975
15892
  const expiresAt = new Date(Date.now() + ttlMs).toISOString();
13976
- const sessionKey = await subtle2.generateKey(
15893
+ const sessionKey = await subtle3.generateKey(
13977
15894
  { name: "AES-GCM", length: 256 },
13978
15895
  false,
13979
15896
  // non-extractable — this is the tab-scope security invariant
@@ -13981,7 +15898,7 @@ async function createSession(keyring, vault, options = {}) {
13981
15898
  );
13982
15899
  const dekMap = {};
13983
15900
  for (const [collName, dek] of keyring.deks) {
13984
- const raw = await subtle2.exportKey("raw", dek);
15901
+ const raw = await subtle3.exportKey("raw", dek);
13985
15902
  dekMap[collName] = bufferToBase64(raw);
13986
15903
  }
13987
15904
  const payload = JSON.stringify({
@@ -13993,7 +15910,7 @@ async function createSession(keyring, vault, options = {}) {
13993
15910
  salt: bufferToBase64(keyring.salt)
13994
15911
  });
13995
15912
  const iv = globalThis.crypto.getRandomValues(new Uint8Array(12));
13996
- const encrypted = await subtle2.encrypt(
15913
+ const encrypted = await subtle3.encrypt(
13997
15914
  { name: "AES-GCM", iv },
13998
15915
  sessionKey,
13999
15916
  new TextEncoder().encode(payload)
@@ -14024,7 +15941,7 @@ async function resolveSession(token) {
14024
15941
  const ciphertext = base64ToBuffer(token.wrappedKek);
14025
15942
  let plaintext;
14026
15943
  try {
14027
- plaintext = await subtle2.decrypt(
15944
+ plaintext = await subtle3.decrypt(
14028
15945
  { name: "AES-GCM", iv },
14029
15946
  sessionKey,
14030
15947
  ciphertext
@@ -14035,7 +15952,7 @@ async function resolveSession(token) {
14035
15952
  const payload = JSON.parse(new TextDecoder().decode(plaintext));
14036
15953
  const deks = /* @__PURE__ */ new Map();
14037
15954
  for (const [collName, rawBase64] of Object.entries(payload.deks)) {
14038
- const dek = await subtle2.importKey(
15955
+ const dek = await subtle3.importKey(
14039
15956
  "raw",
14040
15957
  base64ToBuffer(rawBase64),
14041
15958
  { name: "AES-GCM", length: 256 },
@@ -14052,7 +15969,8 @@ async function resolveSession(token) {
14052
15969
  deks,
14053
15970
  kek: null,
14054
15971
  // KEK not available in session context
14055
- salt: base64ToBuffer(payload.salt)
15972
+ salt: base64ToBuffer(payload.salt),
15973
+ authenticators: []
14056
15974
  };
14057
15975
  }
14058
15976
  function revokeSession(sessionId) {
@@ -14288,7 +16206,8 @@ async function loadDevUnlock(vault, userId, options = {}) {
14288
16206
  permissions: parsed.permissions,
14289
16207
  deks,
14290
16208
  kek: null,
14291
- salt: base64ToBuffer(parsed.salt)
16209
+ salt: base64ToBuffer(parsed.salt),
16210
+ authenticators: []
14292
16211
  };
14293
16212
  }
14294
16213
  function clearDevUnlock(vault, userId, options = {}) {
@@ -14520,31 +16439,6 @@ function shortJSON(value) {
14520
16439
  if (typeof s !== "string") return "<unrepresentable>";
14521
16440
  return s.length > 60 ? s.slice(0, 57) + "..." : s;
14522
16441
  }
14523
-
14524
- // src/validation.ts
14525
- init_errors();
14526
- function validatePassphrase(passphrase) {
14527
- if (passphrase.length < 8) {
14528
- throw new ValidationError(
14529
- "Passphrase too short \u2014 minimum 8 characters. Recommended: 12+ characters or a 4+ word passphrase."
14530
- );
14531
- }
14532
- const entropy = estimateEntropy(passphrase);
14533
- if (entropy < 28) {
14534
- throw new ValidationError(
14535
- "Passphrase too weak \u2014 too little entropy. Use a mix of uppercase, lowercase, numbers, and symbols, or use a 4+ word passphrase."
14536
- );
14537
- }
14538
- }
14539
- function estimateEntropy(passphrase) {
14540
- let charsetSize = 0;
14541
- if (/[a-z]/.test(passphrase)) charsetSize += 26;
14542
- if (/[A-Z]/.test(passphrase)) charsetSize += 26;
14543
- if (/[0-9]/.test(passphrase)) charsetSize += 10;
14544
- if (/[^a-zA-Z0-9]/.test(passphrase)) charsetSize += 32;
14545
- if (charsetSize === 0) charsetSize = 26;
14546
- return Math.floor(passphrase.length * Math.log2(charsetSize));
14547
- }
14548
16442
  // Annotate the CommonJS export names for ESM import in node:
14549
16443
  0 && (module.exports = {
14550
16444
  Aggregation,
@@ -14567,7 +16461,9 @@ function estimateEntropy(passphrase) {
14567
16461
  CollectionInstant,
14568
16462
  ConflictError,
14569
16463
  DEFAULT_CHUNK_SIZE,
16464
+ DEFAULT_FRESHNESS_MS,
14570
16465
  DEFAULT_JOIN_MAX_ROWS,
16466
+ DEFAULT_PUBLIC_ENVELOPE_SCHEMA,
14571
16467
  DELEGATIONS_COLLECTION,
14572
16468
  DICT_COLLECTION_PREFIX,
14573
16469
  DanglingReferenceError,
@@ -14602,6 +16498,7 @@ function estimateEntropy(passphrase) {
14602
16498
  MAGIC_LINK_CONTENT_INFO_PREFIX,
14603
16499
  MAGIC_LINK_GRANTS_COLLECTION,
14604
16500
  MAGIC_LINK_KEK_INFO_PREFIX,
16501
+ META_COLLECTION,
14605
16502
  MissingTranslationError,
14606
16503
  NOYDB_BACKUP_VERSION,
14607
16504
  NOYDB_BUNDLE_FORMAT_VERSION,
@@ -14616,20 +16513,29 @@ function estimateEntropy(passphrase) {
14616
16513
  Noydb,
14617
16514
  NoydbError,
14618
16515
  PERIODS_COLLECTION,
16516
+ PERSONAL_POLICY,
16517
+ POLICY_RECORD_ID,
16518
+ PUBLIC_ENVELOPE_FIELDS,
16519
+ PUBLIC_ENVELOPE_RECORD_ID,
14619
16520
  PathEscapeError,
14620
16521
  PeriodClosedError,
14621
16522
  PermissionDeniedError,
16523
+ PolicyDeniedError,
14622
16524
  PolicyEnforcer,
14623
16525
  PresenceHandle,
14624
16526
  PrivilegeEscalationError,
14625
16527
  Query,
16528
+ QuickUnlockStore,
14626
16529
  ReadOnlyAtInstantError,
14627
16530
  ReadOnlyError,
14628
16531
  ReadOnlyFrameError,
16532
+ RecoveryNotEnrolledError,
16533
+ RecoveryProfileNotImplementedError,
14629
16534
  RefIntegrityError,
14630
16535
  RefRegistry,
14631
16536
  RefScopeError,
14632
16537
  ReservedCollectionNameError,
16538
+ STRICT_POLICY,
14633
16539
  SYNC_CREDENTIALS_COLLECTION,
14634
16540
  ScanBuilder,
14635
16541
  SchemaValidationError,
@@ -14647,21 +16553,29 @@ function estimateEntropy(passphrase) {
14647
16553
  TxCollection,
14648
16554
  TxContext,
14649
16555
  TxVault,
16556
+ USER_ENVELOPE_COLLECTION,
16557
+ USER_ENVELOPE_MAX_BYTES,
16558
+ UserApi,
16559
+ UserEnvelopeOversizedError,
14650
16560
  ValidationError,
14651
16561
  Vault,
14652
16562
  VaultFrame,
14653
16563
  VaultInstant,
16564
+ WeakPassphraseError,
14654
16565
  activeSessionCount,
14655
16566
  applyI18nLocale,
14656
16567
  applyJoins,
14657
16568
  applyPatch,
16569
+ assertStrongPassphrase,
14658
16570
  assertTierAccess,
14659
16571
  avg,
14660
16572
  base64ToBuffer,
14661
16573
  bufferToBase64,
14662
16574
  buildLiveQuery,
14663
16575
  buildRecipientKeyringFile,
16576
+ burnPaperRecoveryEntry,
14664
16577
  canonicalJson,
16578
+ checkGate,
14665
16579
  clearDevUnlock,
14666
16580
  computePatch,
14667
16581
  count,
@@ -14675,10 +16589,16 @@ function estimateEntropy(passphrase) {
14675
16589
  decryptDeterministic,
14676
16590
  dekKey,
14677
16591
  deleteCredential,
16592
+ deleteUserEnvelope,
14678
16593
  deriveMagicLinkContentKey,
14679
16594
  derivePresenceKey,
16595
+ describeAllUsersAuth,
16596
+ describeAuthConfig,
16597
+ describeGate,
16598
+ describeUserAuth,
14680
16599
  detectMagic,
14681
16600
  detectMimeType,
16601
+ diagramAuthConfig,
14682
16602
  dictCollectionName,
14683
16603
  dictKey,
14684
16604
  diff,
@@ -14687,6 +16607,7 @@ function estimateEntropy(passphrase) {
14687
16607
  enableDevUnlock,
14688
16608
  encryptBytes,
14689
16609
  encryptDeterministic,
16610
+ enrollAuthenticator,
14690
16611
  envelopePayloadHash,
14691
16612
  estimateEntropy,
14692
16613
  estimateRecordBytes,
@@ -14695,6 +16616,7 @@ function estimateEntropy(passphrase) {
14695
16616
  evaluateFieldClause,
14696
16617
  evaluateImportCapability,
14697
16618
  executePlan,
16619
+ findAuthenticator,
14698
16620
  formatDiff,
14699
16621
  generateULID,
14700
16622
  getCredential,
@@ -14702,6 +16624,7 @@ function estimateEntropy(passphrase) {
14702
16624
  hasExportCapability,
14703
16625
  hasImportCapability,
14704
16626
  hasNoydbBundleMagic,
16627
+ hasRecoveryEnrolled,
14705
16628
  hashEntry,
14706
16629
  i18nText,
14707
16630
  isDevUnlockActive,
@@ -14710,16 +16633,27 @@ function estimateEntropy(passphrase) {
14710
16633
  isI18nTextDescriptor,
14711
16634
  isMagicLinkGrantExpired,
14712
16635
  isPreCompressed,
16636
+ isPublicEnvelope,
14713
16637
  isSessionAlive,
14714
16638
  isULID,
14715
16639
  issueDelegation,
16640
+ keyringRecoverPassphrase,
16641
+ keyringRotatePassphrase,
14716
16642
  listCredentials,
14717
16643
  listMagicLinkGrants,
16644
+ listUserEnvelopeIds,
16645
+ listUsers,
16646
+ listUsersWithEnvelopes,
14718
16647
  loadActiveDelegations,
14719
16648
  loadDevUnlock,
16649
+ loadPaperRecoveryEntries,
16650
+ loadPublicEnvelope,
16651
+ loadUserEnvelope,
16652
+ loadVaultPolicy,
14720
16653
  magicLinkGrantRecordId,
14721
16654
  max,
14722
16655
  mergeCrdtStates,
16656
+ mergePolicy,
14723
16657
  min,
14724
16658
  paddedIndex,
14725
16659
  parseBytes,
@@ -14728,13 +16662,17 @@ function estimateEntropy(passphrase) {
14728
16662
  readMagicLinkGrantRecord,
14729
16663
  readNoydbBundle,
14730
16664
  readNoydbBundleHeader,
16665
+ readNoydbBundlePublicEnvelope,
14731
16666
  readPath,
16667
+ readPublicEnvelope,
14732
16668
  reduceRecords,
14733
16669
  ref,
16670
+ removeAuthenticator,
14734
16671
  resetBrotliSupportCache,
14735
16672
  resetJoinWarnings,
14736
16673
  resolveCrdtSnapshot,
14737
16674
  resolveI18nText,
16675
+ resolvePublicEnvelopeSchema,
14738
16676
  resolveSession,
14739
16677
  revokeAllSessions,
14740
16678
  revokeDelegation,
@@ -14742,11 +16680,16 @@ function estimateEntropy(passphrase) {
14742
16680
  revokeSession,
14743
16681
  routeStore,
14744
16682
  runTransaction,
16683
+ savePaperRecoveryEntries,
16684
+ savePublicEnvelope,
16685
+ saveUserEnvelope,
16686
+ saveVaultPolicy,
14745
16687
  sha256Hex,
14746
16688
  sum,
14747
16689
  unwrapMagicLinkGrant,
14748
16690
  validateI18nTextValue,
14749
16691
  validatePassphrase,
16692
+ validatePublicEnvelopeInput,
14750
16693
  validateSchemaInput,
14751
16694
  validateSchemaOutput,
14752
16695
  validateSessionPolicy,