@noy-db/hub 0.1.0-pre.3 → 0.1.0-pre.5

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-M2F2JAWB.js → chunk-6NPQTBZN.js} +103 -8
  11. package/dist/chunk-6NPQTBZN.js.map +1 -0
  12. package/dist/{chunk-UQFSPSWG.js → chunk-E4OOAPBZ.js} +2 -2
  13. package/dist/chunk-EMIGCR7X.js +39 -0
  14. package/dist/chunk-EMIGCR7X.js.map +1 -0
  15. package/dist/{chunk-EXQRC2L4.js → chunk-H3DV46AQ.js} +2 -2
  16. package/dist/{chunk-XHFOENR2.js → chunk-LMKOSLJY.js} +2 -2
  17. package/dist/{chunk-GJILMRPO.js → chunk-LRN3PNI6.js} +42 -4
  18. package/dist/chunk-LRN3PNI6.js.map +1 -0
  19. package/dist/{chunk-4OWFYIDQ.js → chunk-MIRZMUSQ.js} +3 -3
  20. package/dist/{chunk-ZRG4V3F5.js → chunk-NXUVITPB.js} +1 -1
  21. package/dist/chunk-NXUVITPB.js.map +1 -0
  22. package/dist/{chunk-5AATM2M2.js → chunk-QUDXYI4W.js} +2 -2
  23. package/dist/{chunk-ZLMV3TUA.js → chunk-QV4WLLKB.js} +3 -3
  24. package/dist/{chunk-E445ICYI.js → chunk-UFL4DUEV.js} +5 -3
  25. package/dist/chunk-UFL4DUEV.js.map +1 -0
  26. package/dist/chunk-UQQ2XFXI.js +155 -0
  27. package/dist/chunk-UQQ2XFXI.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-KrKkcqD3.d.ts → dev-unlock-BgFqShBi.d.ts} +1 -1
  31. package/dist/{dev-unlock-CeXic1xC.d.cts → dev-unlock-qVMxG2Je.d.cts} +1 -1
  32. package/dist/{hash-ChfJjRjQ.d.ts → hash-BhoL7iUE.d.ts} +1 -1
  33. package/dist/{hash-9KO1BGxh.d.cts → hash-Bpvl2eSe.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-BRHBCmLt.d.ts → index-DJTf9yxn.d.ts} +1 -1
  45. package/dist/{index-DhjMjz7L.d.cts → index-DhK_zqOO.d.ts} +39 -5
  46. package/dist/{index-C8kQtmOk.d.ts → index-DyRt_5vM.d.cts} +39 -5
  47. package/dist/index.cjs +1501 -51
  48. package/dist/index.cjs.map +1 -1
  49. package/dist/index.d.cts +261 -19
  50. package/dist/index.d.ts +261 -19
  51. package/dist/index.js +1118 -44
  52. package/dist/index.js.map +1 -1
  53. package/dist/{ledger-2NX4L7PN.js → ledger-GA4DMJS6.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-R4EIEQE6.js +31 -0
  59. package/dist/public-envelope-R4EIEQE6.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-Bfs0qr5F.d.cts → types-BpyE4o_n.d.cts} +935 -4
  83. package/dist/{types-BZpCZB8N.d.ts → types-Df72wWCC.d.ts} +935 -4
  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-M2F2JAWB.js.map +0 -1
  88. package/dist/chunk-ZRG4V3F5.js.map +0 -1
  89. /package/dist/{chunk-UQFSPSWG.js.map → chunk-E4OOAPBZ.js.map} +0 -0
  90. /package/dist/{chunk-EXQRC2L4.js.map → chunk-H3DV46AQ.js.map} +0 -0
  91. /package/dist/{chunk-XHFOENR2.js.map → chunk-LMKOSLJY.js.map} +0 -0
  92. /package/dist/{chunk-4OWFYIDQ.js.map → chunk-MIRZMUSQ.js.map} +0 -0
  93. /package/dist/{chunk-5AATM2M2.js.map → chunk-QUDXYI4W.js.map} +0 -0
  94. /package/dist/{chunk-ZLMV3TUA.js.map → chunk-QV4WLLKB.js.map} +0 -0
  95. /package/dist/{ledger-2NX4L7PN.js.map → ledger-GA4DMJS6.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,
@@ -1838,17 +2065,21 @@ __export(src_exports, {
1838
2065
  Vault: () => Vault,
1839
2066
  VaultFrame: () => VaultFrame,
1840
2067
  VaultInstant: () => VaultInstant,
2068
+ WeakPassphraseError: () => WeakPassphraseError,
1841
2069
  activeSessionCount: () => activeSessionCount,
1842
2070
  applyI18nLocale: () => applyI18nLocale,
1843
2071
  applyJoins: () => applyJoins,
1844
2072
  applyPatch: () => applyPatch,
2073
+ assertStrongPassphrase: () => assertStrongPassphrase,
1845
2074
  assertTierAccess: () => assertTierAccess,
1846
2075
  avg: () => avg,
1847
2076
  base64ToBuffer: () => base64ToBuffer,
1848
2077
  bufferToBase64: () => bufferToBase64,
1849
2078
  buildLiveQuery: () => buildLiveQuery,
1850
2079
  buildRecipientKeyringFile: () => buildRecipientKeyringFile,
2080
+ burnPaperRecoveryEntry: () => burnPaperRecoveryEntry,
1851
2081
  canonicalJson: () => canonicalJson,
2082
+ checkGate: () => checkGate,
1852
2083
  clearDevUnlock: () => clearDevUnlock,
1853
2084
  computePatch: () => computePatch,
1854
2085
  count: () => count,
@@ -1864,8 +2095,13 @@ __export(src_exports, {
1864
2095
  deleteCredential: () => deleteCredential,
1865
2096
  deriveMagicLinkContentKey: () => deriveMagicLinkContentKey,
1866
2097
  derivePresenceKey: () => derivePresenceKey,
2098
+ describeAllUsersAuth: () => describeAllUsersAuth,
2099
+ describeAuthConfig: () => describeAuthConfig,
2100
+ describeGate: () => describeGate,
2101
+ describeUserAuth: () => describeUserAuth,
1867
2102
  detectMagic: () => detectMagic,
1868
2103
  detectMimeType: () => detectMimeType,
2104
+ diagramAuthConfig: () => diagramAuthConfig,
1869
2105
  dictCollectionName: () => dictCollectionName,
1870
2106
  dictKey: () => dictKey,
1871
2107
  diff: () => diff2,
@@ -1874,6 +2110,7 @@ __export(src_exports, {
1874
2110
  enableDevUnlock: () => enableDevUnlock,
1875
2111
  encryptBytes: () => encryptBytes,
1876
2112
  encryptDeterministic: () => encryptDeterministic,
2113
+ enrollAuthenticator: () => enrollAuthenticator,
1877
2114
  envelopePayloadHash: () => envelopePayloadHash,
1878
2115
  estimateEntropy: () => estimateEntropy,
1879
2116
  estimateRecordBytes: () => estimateRecordBytes,
@@ -1882,6 +2119,7 @@ __export(src_exports, {
1882
2119
  evaluateFieldClause: () => evaluateFieldClause,
1883
2120
  evaluateImportCapability: () => evaluateImportCapability,
1884
2121
  executePlan: () => executePlan,
2122
+ findAuthenticator: () => findAuthenticator,
1885
2123
  formatDiff: () => formatDiff,
1886
2124
  generateULID: () => generateULID,
1887
2125
  getCredential: () => getCredential,
@@ -1889,6 +2127,7 @@ __export(src_exports, {
1889
2127
  hasExportCapability: () => hasExportCapability,
1890
2128
  hasImportCapability: () => hasImportCapability,
1891
2129
  hasNoydbBundleMagic: () => hasNoydbBundleMagic,
2130
+ hasRecoveryEnrolled: () => hasRecoveryEnrolled,
1892
2131
  hashEntry: () => hashEntry,
1893
2132
  i18nText: () => i18nText,
1894
2133
  isDevUnlockActive: () => isDevUnlockActive,
@@ -1897,16 +2136,23 @@ __export(src_exports, {
1897
2136
  isI18nTextDescriptor: () => isI18nTextDescriptor,
1898
2137
  isMagicLinkGrantExpired: () => isMagicLinkGrantExpired,
1899
2138
  isPreCompressed: () => isPreCompressed,
2139
+ isPublicEnvelope: () => isPublicEnvelope,
1900
2140
  isSessionAlive: () => isSessionAlive,
1901
2141
  isULID: () => isULID,
1902
2142
  issueDelegation: () => issueDelegation,
2143
+ keyringRecoverPassphrase: () => recoverPassphrase,
2144
+ keyringRotatePassphrase: () => rotatePassphrase,
1903
2145
  listCredentials: () => listCredentials,
1904
2146
  listMagicLinkGrants: () => listMagicLinkGrants,
1905
2147
  loadActiveDelegations: () => loadActiveDelegations,
1906
2148
  loadDevUnlock: () => loadDevUnlock,
2149
+ loadPaperRecoveryEntries: () => loadPaperRecoveryEntries,
2150
+ loadPublicEnvelope: () => loadPublicEnvelope,
2151
+ loadVaultPolicy: () => loadVaultPolicy,
1907
2152
  magicLinkGrantRecordId: () => magicLinkGrantRecordId,
1908
2153
  max: () => max,
1909
2154
  mergeCrdtStates: () => mergeCrdtStates,
2155
+ mergePolicy: () => mergePolicy,
1910
2156
  min: () => min,
1911
2157
  paddedIndex: () => paddedIndex,
1912
2158
  parseBytes: () => parseBytes,
@@ -1915,13 +2161,17 @@ __export(src_exports, {
1915
2161
  readMagicLinkGrantRecord: () => readMagicLinkGrantRecord,
1916
2162
  readNoydbBundle: () => readNoydbBundle,
1917
2163
  readNoydbBundleHeader: () => readNoydbBundleHeader,
2164
+ readNoydbBundlePublicEnvelope: () => readNoydbBundlePublicEnvelope,
1918
2165
  readPath: () => readPath,
2166
+ readPublicEnvelope: () => readPublicEnvelope,
1919
2167
  reduceRecords: () => reduceRecords,
1920
2168
  ref: () => ref,
2169
+ removeAuthenticator: () => removeAuthenticator,
1921
2170
  resetBrotliSupportCache: () => resetBrotliSupportCache,
1922
2171
  resetJoinWarnings: () => resetJoinWarnings,
1923
2172
  resolveCrdtSnapshot: () => resolveCrdtSnapshot,
1924
2173
  resolveI18nText: () => resolveI18nText,
2174
+ resolvePublicEnvelopeSchema: () => resolveSchema,
1925
2175
  resolveSession: () => resolveSession,
1926
2176
  revokeAllSessions: () => revokeAllSessions,
1927
2177
  revokeDelegation: () => revokeDelegation,
@@ -1929,11 +2179,15 @@ __export(src_exports, {
1929
2179
  revokeSession: () => revokeSession,
1930
2180
  routeStore: () => routeStore,
1931
2181
  runTransaction: () => runTransaction,
2182
+ savePaperRecoveryEntries: () => savePaperRecoveryEntries,
2183
+ savePublicEnvelope: () => savePublicEnvelope,
2184
+ saveVaultPolicy: () => saveVaultPolicy,
1932
2185
  sha256Hex: () => sha256Hex3,
1933
2186
  sum: () => sum,
1934
2187
  unwrapMagicLinkGrant: () => unwrapMagicLinkGrant,
1935
2188
  validateI18nTextValue: () => validateI18nTextValue,
1936
2189
  validatePassphrase: () => validatePassphrase,
2190
+ validatePublicEnvelopeInput: () => validatePublicEnvelopeInput,
1937
2191
  validateSchemaInput: () => validateSchemaInput,
1938
2192
  validateSchemaOutput: () => validateSchemaOutput,
1939
2193
  validateSessionPolicy: () => validateSessionPolicy,
@@ -3815,7 +4069,8 @@ var ALLOWED_HEADER_KEYS = /* @__PURE__ */ new Set([
3815
4069
  "formatVersion",
3816
4070
  "handle",
3817
4071
  "bodyBytes",
3818
- "bodySha256"
4072
+ "bodySha256",
4073
+ "publicEnvelope"
3819
4074
  ]);
3820
4075
  function validateBundleHeader(parsed) {
3821
4076
  if (parsed === null || typeof parsed !== "object") {
@@ -3851,6 +4106,25 @@ function validateBundleHeader(parsed) {
3851
4106
  `.noydb bundle header.bodySha256 must be a 64-character lowercase hex string, got ${typeof h["bodySha256"] === "string" ? `"${h["bodySha256"]}"` : String(h["bodySha256"])}.`
3852
4107
  );
3853
4108
  }
4109
+ if (h["publicEnvelope"] !== void 0) {
4110
+ const env = h["publicEnvelope"];
4111
+ if (env === null || typeof env !== "object" || Array.isArray(env)) {
4112
+ throw new Error(
4113
+ `.noydb bundle header.publicEnvelope must be a JSON object when present, got ${typeof env}.`
4114
+ );
4115
+ }
4116
+ const e = env;
4117
+ if (e["_noydb_public"] !== 1) {
4118
+ throw new Error(
4119
+ `.noydb bundle header.publicEnvelope._noydb_public must be 1, got ${String(e["_noydb_public"])}.`
4120
+ );
4121
+ }
4122
+ if (typeof e["version"] !== "number" || !Number.isInteger(e["version"]) || e["version"] < 1) {
4123
+ throw new Error(
4124
+ `.noydb bundle header.publicEnvelope.version must be a positive integer, got ${String(e["version"])}.`
4125
+ );
4126
+ }
4127
+ }
3854
4128
  }
3855
4129
  function encodeBundleHeader(header) {
3856
4130
  validateBundleHeader(header);
@@ -3858,7 +4132,8 @@ function encodeBundleHeader(header) {
3858
4132
  formatVersion: header.formatVersion,
3859
4133
  handle: header.handle,
3860
4134
  bodyBytes: header.bodyBytes,
3861
- bodySha256: header.bodySha256
4135
+ bodySha256: header.bodySha256,
4136
+ ...header.publicEnvelope !== void 0 ? { publicEnvelope: header.publicEnvelope } : {}
3862
4137
  });
3863
4138
  return new TextEncoder().encode(json);
3864
4139
  }
@@ -3894,6 +4169,7 @@ function hasNoydbBundleMagic(bytes) {
3894
4169
 
3895
4170
  // src/bundle/bundle.ts
3896
4171
  init_errors();
4172
+ init_storage();
3897
4173
  var cachedBrotliSupport = null;
3898
4174
  function supportsBrotliCompression() {
3899
4175
  if (cachedBrotliSupport !== null) return cachedBrotliSupport;
@@ -4058,11 +4334,13 @@ async function writeNoydbBundle(vault, opts = {}) {
4058
4334
  const { format, streamFormat } = selectCompression(opts.compression);
4059
4335
  const body = streamFormat === null ? dumpBytes : await pumpThroughStream(dumpBytes, new CompressionStream(streamFormat));
4060
4336
  const bodySha256 = await sha256Hex2(body);
4337
+ const publicEnvelope = await vault.getPublicEnvelope();
4061
4338
  const header = {
4062
4339
  formatVersion: NOYDB_BUNDLE_FORMAT_VERSION,
4063
4340
  handle,
4064
4341
  bodyBytes: body.length,
4065
- bodySha256
4342
+ bodySha256,
4343
+ ...publicEnvelope !== void 0 ? { publicEnvelope } : {}
4066
4344
  };
4067
4345
  const headerBytes = encodeBundleHeader(header);
4068
4346
  const prefix = new Uint8Array(NOYDB_BUNDLE_PREFIX_BYTES);
@@ -4104,6 +4382,17 @@ function parsePrefixAndHeader(bytes) {
4104
4382
  function readNoydbBundleHeader(bytes) {
4105
4383
  return parsePrefixAndHeader(bytes).header;
4106
4384
  }
4385
+ function readNoydbBundlePublicEnvelope(bytes, opts = {}) {
4386
+ const header = parsePrefixAndHeader(bytes).header;
4387
+ const env = header.publicEnvelope;
4388
+ if (!env) return void 0;
4389
+ if (opts.locale === void 0) return env;
4390
+ return {
4391
+ ...env,
4392
+ ...env.name !== void 0 ? { name: pickLocale(env.name, opts.locale, env.defaultLocale) } : {},
4393
+ ...env.description !== void 0 ? { description: pickLocale(env.description, opts.locale, env.defaultLocale) } : {}
4394
+ };
4395
+ }
4107
4396
  async function readNoydbBundle(bytes) {
4108
4397
  const { header, bodyOffset, algo } = parsePrefixAndHeader(bytes);
4109
4398
  const body = bytes.slice(bodyOffset);
@@ -4538,10 +4827,84 @@ var RefRegistry = class {
4538
4827
  }
4539
4828
  };
4540
4829
 
4830
+ // src/team/authenticators.ts
4831
+ init_errors();
4832
+
4541
4833
  // src/team/keyring.ts
4542
4834
  init_types();
4543
4835
  init_crypto();
4544
4836
  init_errors();
4837
+
4838
+ // src/validation.ts
4839
+ init_errors();
4840
+ var WeakPassphraseError = class extends NoydbError {
4841
+ reason;
4842
+ suggestion;
4843
+ constructor(reason, suggestion) {
4844
+ super("WEAK_PASSPHRASE", `Weak passphrase (${reason}). ${suggestion}`);
4845
+ this.name = "WeakPassphraseError";
4846
+ this.reason = reason;
4847
+ this.suggestion = suggestion;
4848
+ }
4849
+ };
4850
+ var DEFAULT_MIN_WORDS = 6;
4851
+ var DEFAULT_MIN_WORD_LENGTH = 3;
4852
+ var SUGGESTIONS = {
4853
+ empty: "Provide a phrase of at least 6 lowercase words separated by single spaces.",
4854
+ "invalid-chars": "Use only lowercase letters [a-z] and single spaces. No punctuation, symbols, digits, or uppercase.",
4855
+ "leading-or-trailing-space": "Trim leading and trailing spaces.",
4856
+ "double-space": "Use exactly one space between words.",
4857
+ "too-few-words": 'Use at least 6 words by default (8 under strict policy). Example: "correct horse battery staple printer toaster".',
4858
+ "word-too-short": 'Each word must be at least 3 characters. Drop short fillers like "a", "is", "of".',
4859
+ "repeated-adjacent": "Avoid repeating the same word twice in a row."
4860
+ };
4861
+ function validatePassphrase(s, opts) {
4862
+ const minWords = opts?.minWords ?? DEFAULT_MIN_WORDS;
4863
+ const minWordLength = opts?.minWordLength ?? DEFAULT_MIN_WORD_LENGTH;
4864
+ const rejectRepeated = opts?.rejectRepeatedAdjacent ?? true;
4865
+ if (s.length === 0) {
4866
+ return { ok: false, reason: "empty" };
4867
+ }
4868
+ if (s !== s.trim()) {
4869
+ return { ok: false, reason: "leading-or-trailing-space" };
4870
+ }
4871
+ if (s.includes(" ")) {
4872
+ return { ok: false, reason: "double-space" };
4873
+ }
4874
+ if (!/^[a-z]+( [a-z]+)*$/.test(s)) {
4875
+ return { ok: false, reason: "invalid-chars" };
4876
+ }
4877
+ const words = s.split(" ");
4878
+ if (words.length < minWords) {
4879
+ return { ok: false, reason: "too-few-words", minimum: minWords, got: words.length };
4880
+ }
4881
+ for (const w of words) {
4882
+ if (w.length < minWordLength) {
4883
+ return { ok: false, reason: "word-too-short", minimum: minWordLength, got: w.length };
4884
+ }
4885
+ }
4886
+ if (rejectRepeated) {
4887
+ for (let i = 1; i < words.length; i++) {
4888
+ if (words[i] === words[i - 1]) {
4889
+ return { ok: false, reason: "repeated-adjacent" };
4890
+ }
4891
+ }
4892
+ }
4893
+ return { ok: true, words: words.length };
4894
+ }
4895
+ function assertStrongPassphrase(s, opts) {
4896
+ if (opts?.allowWeakPassphrase) return;
4897
+ const result = validatePassphrase(s, opts);
4898
+ if (result.ok) return;
4899
+ throw new WeakPassphraseError(result.reason, SUGGESTIONS[result.reason]);
4900
+ }
4901
+ function estimateEntropy(passphrase) {
4902
+ const result = validatePassphrase(passphrase);
4903
+ if (!result.ok) return 0;
4904
+ return Math.round(result.words * Math.log2(7776));
4905
+ }
4906
+
4907
+ // src/team/keyring.ts
4545
4908
  var ADMIN_GRANTABLE_TARGETS = ["operator", "viewer", "client", "admin"];
4546
4909
  function canGrant(callerRole, targetRole) {
4547
4910
  if (callerRole === "owner") return true;
@@ -4581,11 +4944,16 @@ async function loadKeyring(adapter, vault, userId, passphrase) {
4581
4944
  deks,
4582
4945
  kek,
4583
4946
  salt,
4947
+ authenticators: keyringFile.authenticators ?? [],
4584
4948
  ...keyringFile.export_capability !== void 0 && { exportCapability: keyringFile.export_capability },
4585
- ...keyringFile.import_capability !== void 0 && { importCapability: keyringFile.import_capability }
4949
+ ...keyringFile.import_capability !== void 0 && { importCapability: keyringFile.import_capability },
4950
+ ...keyringFile.policy !== void 0 && { policy: keyringFile.policy }
4586
4951
  };
4587
4952
  }
4588
- async function createOwnerKeyring(adapter, vault, userId, passphrase) {
4953
+ async function createOwnerKeyring(adapter, vault, userId, passphrase, passphraseOpts) {
4954
+ if (passphraseOpts?.validate && !passphraseOpts.allowWeakPassphrase) {
4955
+ assertStrongPassphrase(passphrase, passphraseOpts);
4956
+ }
4589
4957
  const salt = generateSalt();
4590
4958
  const kek = await deriveKey(passphrase, salt);
4591
4959
  const keyringFile = {
@@ -4607,7 +4975,8 @@ async function createOwnerKeyring(adapter, vault, userId, passphrase) {
4607
4975
  permissions: {},
4608
4976
  deks: /* @__PURE__ */ new Map(),
4609
4977
  kek,
4610
- salt
4978
+ salt,
4979
+ authenticators: []
4611
4980
  };
4612
4981
  }
4613
4982
  async function grant(adapter, vault, callerKeyring, options) {
@@ -4616,6 +4985,9 @@ async function grant(adapter, vault, callerKeyring, options) {
4616
4985
  `Role "${callerKeyring.role}" cannot grant role "${options.role}"`
4617
4986
  );
4618
4987
  }
4988
+ if (options.validatePassphrase && !options.allowWeakPassphrase) {
4989
+ assertStrongPassphrase(options.passphrase);
4990
+ }
4619
4991
  const permissions = resolvePermissions(options.role, options.permissions);
4620
4992
  const newSalt = generateSalt();
4621
4993
  const newKek = await deriveKey(options.passphrase, newSalt);
@@ -4775,7 +5147,10 @@ async function rotateKeys(adapter, vault, callerKeyring, collections) {
4775
5147
  await writeKeyringFile(adapter, vault, userId, updatedKeyring);
4776
5148
  }
4777
5149
  }
4778
- async function changeSecret(adapter, vault, keyring, newPassphrase) {
5150
+ async function changeSecret(adapter, vault, keyring, newPassphrase, passphraseOpts) {
5151
+ if (passphraseOpts?.validate && !passphraseOpts.allowWeakPassphrase) {
5152
+ assertStrongPassphrase(newPassphrase, passphraseOpts);
5153
+ }
4779
5154
  const newSalt = generateSalt();
4780
5155
  const newKek = await deriveKey(newPassphrase, newSalt);
4781
5156
  const wrappedDeks = {};
@@ -4802,7 +5177,14 @@ async function changeSecret(adapter, vault, keyring, newPassphrase) {
4802
5177
  deks: keyring.deks,
4803
5178
  // Same DEKs, different wrapping
4804
5179
  kek: newKek,
4805
- salt: newSalt
5180
+ salt: newSalt,
5181
+ // Tier-2 slots are NOT preserved through `changeSecret` —
5182
+ // each slot wraps the OLD KEK, so the new keyring has no
5183
+ // authenticator slots until the user re-enrolls. The higher-level
5184
+ // `db.rotatePassphrase()` (#10) preserves slots by rewrapping the
5185
+ // KEK reference, not the KEK itself.
5186
+ authenticators: [],
5187
+ ...keyring.policy !== void 0 && { policy: keyring.policy }
4806
5188
  };
4807
5189
  }
4808
5190
  async function buildRecipientKeyringFile(callerKeyring, recipient) {
@@ -4913,7 +5295,9 @@ async function persistKeyring(adapter, vault, keyring) {
4913
5295
  created_at: (/* @__PURE__ */ new Date()).toISOString(),
4914
5296
  granted_by: keyring.userId,
4915
5297
  ...keyring.exportCapability !== void 0 && { export_capability: keyring.exportCapability },
4916
- ...keyring.importCapability !== void 0 && { import_capability: keyring.importCapability }
5298
+ ...keyring.importCapability !== void 0 && { import_capability: keyring.importCapability },
5299
+ ...keyring.authenticators.length > 0 && { authenticators: keyring.authenticators },
5300
+ ...keyring.policy !== void 0 && { policy: keyring.policy }
4917
5301
  };
4918
5302
  await writeKeyringFile(adapter, vault, keyring.userId, keyringFile);
4919
5303
  }
@@ -4965,8 +5349,528 @@ async function writeKeyringFile(adapter, vault, userId, keyringFile) {
4965
5349
  await adapter.put(vault, "_keyring", userId, envelope);
4966
5350
  }
4967
5351
 
5352
+ // src/team/authenticators.ts
5353
+ async function enrollAuthenticator(store, vault, keyring, options) {
5354
+ const existing = keyring.authenticators.find((a) => a.id === options.id);
5355
+ if (existing) {
5356
+ throw new ValidationError(
5357
+ `enrollAuthenticator: slot id "${options.id}" already exists in vault "${vault}". Remove the slot first or pick a unique id.`
5358
+ );
5359
+ }
5360
+ const slot = {
5361
+ id: options.id,
5362
+ method: options.method,
5363
+ enrolled_at: (/* @__PURE__ */ new Date()).toISOString(),
5364
+ enrolled_via_tier: options.enrolled_via_tier ?? 1,
5365
+ wrapped_kek: options.wrapped_kek,
5366
+ meta: options.meta
5367
+ };
5368
+ const next = appendSlot(keyring, slot);
5369
+ await persistKeyring(store, vault, next);
5370
+ return next;
5371
+ }
5372
+ async function removeAuthenticator(store, vault, keyring, slotId) {
5373
+ const filtered = keyring.authenticators.filter((a) => a.id !== slotId);
5374
+ if (filtered.length === keyring.authenticators.length) {
5375
+ return keyring;
5376
+ }
5377
+ const next = {
5378
+ ...keyring,
5379
+ authenticators: filtered
5380
+ };
5381
+ await persistKeyring(store, vault, next);
5382
+ return next;
5383
+ }
5384
+ function findAuthenticator(keyring, slotId) {
5385
+ return keyring.authenticators.find((a) => a.id === slotId);
5386
+ }
5387
+ function appendSlot(keyring, slot) {
5388
+ return {
5389
+ ...keyring,
5390
+ authenticators: [...keyring.authenticators, slot]
5391
+ };
5392
+ }
5393
+
5394
+ // src/session/unlock-state.ts
5395
+ var QuickUnlockStore = class {
5396
+ states = /* @__PURE__ */ new Map();
5397
+ timers = /* @__PURE__ */ new Map();
5398
+ /**
5399
+ * Register a quick-unlock state for a vault. Replaces any existing
5400
+ * state. Schedules an automatic clear when the state's `expiresAt`
5401
+ * elapses.
5402
+ */
5403
+ set(vault, state) {
5404
+ this.clearTimer(vault);
5405
+ this.states.set(vault, state);
5406
+ const ttl = new Date(state.expiresAt).getTime() - Date.now();
5407
+ if (ttl > 0) {
5408
+ const timer = setTimeout(() => this.delete(vault), ttl);
5409
+ this.timers.set(vault, timer);
5410
+ }
5411
+ }
5412
+ /** Read the state for a vault. Returns undefined when none is registered. */
5413
+ get(vault) {
5414
+ return this.states.get(vault);
5415
+ }
5416
+ /** Drop the state for a vault. Cancels the auto-clear timer. */
5417
+ delete(vault) {
5418
+ this.clearTimer(vault);
5419
+ this.states.delete(vault);
5420
+ }
5421
+ /** Drop every cached state. Called on `db.close()`. */
5422
+ clear() {
5423
+ for (const vault of this.states.keys()) {
5424
+ this.clearTimer(vault);
5425
+ }
5426
+ this.states.clear();
5427
+ }
5428
+ clearTimer(vault) {
5429
+ const t = this.timers.get(vault);
5430
+ if (t) clearTimeout(t);
5431
+ this.timers.delete(vault);
5432
+ }
5433
+ };
5434
+
5435
+ // src/team/rotate-recover.ts
5436
+ init_types();
5437
+ init_crypto();
5438
+ init_errors();
5439
+
5440
+ // src/policy/errors.ts
5441
+ init_errors();
5442
+ var PolicyDeniedError = class extends NoydbError {
5443
+ gate;
5444
+ reason;
5445
+ required;
5446
+ constructor(gate, reason, required, message) {
5447
+ super(
5448
+ "POLICY_DENIED",
5449
+ message ?? `Gate "${gate}" denied: ${reason}.`
5450
+ );
5451
+ this.name = "PolicyDeniedError";
5452
+ this.gate = gate;
5453
+ this.reason = reason;
5454
+ this.required = required;
5455
+ }
5456
+ };
5457
+ var RecoveryNotEnrolledError = class extends NoydbError {
5458
+ 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.') {
5459
+ super("RECOVERY_NOT_ENROLLED", message);
5460
+ this.name = "RecoveryNotEnrolledError";
5461
+ }
5462
+ };
5463
+ var RecoveryProfileNotImplementedError = class extends NoydbError {
5464
+ profile;
5465
+ tracking;
5466
+ constructor(profile, tracking) {
5467
+ super(
5468
+ "RECOVERY_PROFILE_NOT_IMPLEMENTED",
5469
+ `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.`
5470
+ );
5471
+ this.name = "RecoveryProfileNotImplementedError";
5472
+ this.profile = profile;
5473
+ this.tracking = tracking;
5474
+ }
5475
+ };
5476
+
5477
+ // src/team/recovery.ts
5478
+ init_types();
5479
+ var PAPER_DOC_ID = "recovery-paper";
5480
+ async function loadPaperRecoveryEntries(store, vault) {
5481
+ const env = await store.get(vault, "_meta", PAPER_DOC_ID);
5482
+ if (!env) return [];
5483
+ try {
5484
+ const doc = JSON.parse(env._data);
5485
+ if (doc.profile !== "paper" || !Array.isArray(doc.entries)) return [];
5486
+ return doc.entries;
5487
+ } catch {
5488
+ return [];
5489
+ }
5490
+ }
5491
+ async function savePaperRecoveryEntries(store, vault, entries) {
5492
+ const doc = {
5493
+ _noydb_recovery: 1,
5494
+ profile: "paper",
5495
+ entries
5496
+ };
5497
+ const envelope = {
5498
+ _noydb: NOYDB_FORMAT_VERSION,
5499
+ _v: 1,
5500
+ _ts: (/* @__PURE__ */ new Date()).toISOString(),
5501
+ _iv: "",
5502
+ _data: JSON.stringify(doc)
5503
+ };
5504
+ await store.put(vault, "_meta", PAPER_DOC_ID, envelope);
5505
+ }
5506
+ async function burnPaperRecoveryEntry(store, vault, codeId) {
5507
+ const entries = await loadPaperRecoveryEntries(store, vault);
5508
+ const remaining = entries.filter((e) => e.codeId !== codeId);
5509
+ await savePaperRecoveryEntries(store, vault, remaining);
5510
+ }
5511
+ async function hasRecoveryEnrolled(store, vault) {
5512
+ const paper = await loadPaperRecoveryEntries(store, vault);
5513
+ return paper.length > 0;
5514
+ }
5515
+ var subtle2 = globalThis.crypto.subtle;
5516
+ var RECOVERY_PBKDF2_ITERATIONS = 6e5;
5517
+ async function unwrapDeksFromPaperEntry(entry, code) {
5518
+ const wrappingKey = await deriveRecoveryWrappingKey(code, base64ToBytes(entry.salt));
5519
+ const plaintext = await subtle2.decrypt(
5520
+ { name: "AES-GCM", iv: base64ToBytes(entry.iv) },
5521
+ wrappingKey,
5522
+ base64ToBytes(entry.wrappedDeks)
5523
+ );
5524
+ const parsed = JSON.parse(new TextDecoder().decode(plaintext));
5525
+ const deks = /* @__PURE__ */ new Map();
5526
+ for (const [coll, b64] of Object.entries(parsed.deks)) {
5527
+ const raw = base64ToBytes(b64);
5528
+ const key = await subtle2.importKey(
5529
+ "raw",
5530
+ raw,
5531
+ { name: "AES-GCM", length: 256 },
5532
+ true,
5533
+ ["encrypt", "decrypt"]
5534
+ );
5535
+ deks.set(coll, key);
5536
+ }
5537
+ return deks;
5538
+ }
5539
+ async function deriveRecoveryWrappingKey(code, salt) {
5540
+ const ikm = await subtle2.importKey(
5541
+ "raw",
5542
+ new TextEncoder().encode(code),
5543
+ "PBKDF2",
5544
+ false,
5545
+ ["deriveKey"]
5546
+ );
5547
+ return subtle2.deriveKey(
5548
+ {
5549
+ name: "PBKDF2",
5550
+ salt,
5551
+ iterations: RECOVERY_PBKDF2_ITERATIONS,
5552
+ hash: "SHA-256"
5553
+ },
5554
+ ikm,
5555
+ { name: "AES-GCM", length: 256 },
5556
+ false,
5557
+ ["encrypt", "decrypt"]
5558
+ );
5559
+ }
5560
+ function base64ToBytes(b64) {
5561
+ const s = atob(b64);
5562
+ const out = new Uint8Array(s.length);
5563
+ for (let i = 0; i < s.length; i++) out[i] = s.charCodeAt(i);
5564
+ return out;
5565
+ }
5566
+
5567
+ // src/team/rotate-recover.ts
5568
+ async function rotatePassphrase(store, vault, userId, input) {
5569
+ if (!input.allowWeakPassphrase) {
5570
+ assertStrongPassphrase(input.newPassphrase, input.passphrasePolicy);
5571
+ }
5572
+ const env = await store.get(vault, "_keyring", userId);
5573
+ if (!env) {
5574
+ throw new NoAccessError(`No keyring found for user "${userId}" in vault "${vault}".`);
5575
+ }
5576
+ const file = JSON.parse(env._data);
5577
+ const oldSalt = base64ToBuffer(file.salt);
5578
+ const oldKek = await deriveKey(input.oldPassphrase, oldSalt);
5579
+ const deks = /* @__PURE__ */ new Map();
5580
+ for (const [coll, wrapped] of Object.entries(file.deks)) {
5581
+ deks.set(coll, await unwrapKey(wrapped, oldKek));
5582
+ }
5583
+ const newSalt = generateSalt();
5584
+ const newKek = await deriveKey(input.newPassphrase, newSalt);
5585
+ const wrappedDeks = {};
5586
+ for (const [coll, dek] of deks) {
5587
+ wrappedDeks[coll] = await wrapKey(dek, newKek);
5588
+ }
5589
+ const next = {
5590
+ ...file,
5591
+ _noydb_keyring: NOYDB_KEYRING_VERSION,
5592
+ deks: wrappedDeks,
5593
+ salt: bufferToBase64(newSalt),
5594
+ // Tier-2 slots reference the old KEK — drop them. User
5595
+ // re-enrols afterwards via `db.enrollAuthenticator`.
5596
+ authenticators: []
5597
+ };
5598
+ await writeKeyringFile2(store, vault, userId, next);
5599
+ return {
5600
+ userId: file.user_id,
5601
+ displayName: file.display_name,
5602
+ role: file.role,
5603
+ permissions: file.permissions,
5604
+ deks,
5605
+ kek: newKek,
5606
+ salt: newSalt,
5607
+ authenticators: [],
5608
+ ...file.export_capability !== void 0 && { exportCapability: file.export_capability },
5609
+ ...file.import_capability !== void 0 && { importCapability: file.import_capability }
5610
+ };
5611
+ }
5612
+ async function recoverPassphrase(store, vault, userId, input) {
5613
+ if (!input.allowWeakPassphrase) {
5614
+ assertStrongPassphrase(input.newPassphrase, input.passphrasePolicy);
5615
+ }
5616
+ switch (input.recoveryProof.profile) {
5617
+ case "paper":
5618
+ return recoverViaPaperCode(store, vault, userId, input);
5619
+ case "shamir":
5620
+ throw new RecoveryProfileNotImplementedError(
5621
+ "shamir",
5622
+ "https://github.com/vLannaAi/noy-db/issues/10"
5623
+ );
5624
+ case "multi-channel":
5625
+ throw new RecoveryProfileNotImplementedError(
5626
+ "multi-channel",
5627
+ "https://github.com/vLannaAi/noy-db/issues/10"
5628
+ );
5629
+ case "admin-mediated":
5630
+ throw new RecoveryProfileNotImplementedError(
5631
+ "admin-mediated",
5632
+ "https://github.com/vLannaAi/noy-db/issues/10"
5633
+ );
5634
+ default: {
5635
+ const _exhaustive = input.recoveryProof;
5636
+ throw new Error(`Unknown recovery profile: ${String(_exhaustive)}`);
5637
+ }
5638
+ }
5639
+ }
5640
+ async function recoverViaPaperCode(store, vault, userId, input) {
5641
+ if (input.recoveryProof.profile !== "paper") throw new Error("unreachable");
5642
+ const { code } = input.recoveryProof.payload;
5643
+ const env = await store.get(vault, "_keyring", userId);
5644
+ if (!env) {
5645
+ throw new NoAccessError(`No keyring found for user "${userId}" in vault "${vault}".`);
5646
+ }
5647
+ const file = JSON.parse(env._data);
5648
+ const entries = await loadPaperRecoveryEntries(store, vault);
5649
+ if (entries.length === 0) {
5650
+ throw new NoAccessError(
5651
+ `No paper-recovery entries enrolled for vault "${vault}". Enroll via \`db.enrollRecovery({ profile: "paper", entries })\` before relying on recovery.`
5652
+ );
5653
+ }
5654
+ const normalized = normalizePaperCode(code);
5655
+ let recovered;
5656
+ for (const entry of entries) {
5657
+ try {
5658
+ const deks2 = await unwrapDeksFromPaperEntry(entry, normalized);
5659
+ recovered = { deks: deks2, entry };
5660
+ break;
5661
+ } catch {
5662
+ }
5663
+ }
5664
+ if (!recovered) {
5665
+ throw new InvalidKeyError(
5666
+ "Recovery code does not match any enrolled paper entry. The code may have been previously used (single-use) or typed incorrectly."
5667
+ );
5668
+ }
5669
+ const deks = recovered.deks;
5670
+ const newSalt = generateSalt();
5671
+ const newKek = await deriveKey(input.newPassphrase, newSalt);
5672
+ const wrappedDeks = {};
5673
+ for (const [coll, dek] of deks) {
5674
+ wrappedDeks[coll] = await wrapKey(dek, newKek);
5675
+ }
5676
+ const next = {
5677
+ ...file,
5678
+ _noydb_keyring: NOYDB_KEYRING_VERSION,
5679
+ deks: wrappedDeks,
5680
+ salt: bufferToBase64(newSalt),
5681
+ authenticators: []
5682
+ // tier-2 slots wrap old KEK, drop them
5683
+ };
5684
+ await writeKeyringFile2(store, vault, userId, next);
5685
+ await burnPaperRecoveryEntry(store, vault, recovered.entry.codeId);
5686
+ return {
5687
+ userId: file.user_id,
5688
+ displayName: file.display_name,
5689
+ role: file.role,
5690
+ permissions: file.permissions,
5691
+ deks,
5692
+ kek: newKek,
5693
+ salt: newSalt,
5694
+ authenticators: [],
5695
+ ...file.export_capability !== void 0 && { exportCapability: file.export_capability },
5696
+ ...file.import_capability !== void 0 && { importCapability: file.import_capability }
5697
+ };
5698
+ }
5699
+ function normalizePaperCode(input) {
5700
+ return input.toUpperCase().replace(/[\s\-_]/g, "");
5701
+ }
5702
+ async function writeKeyringFile2(store, vault, userId, file) {
5703
+ const envelope = {
5704
+ _noydb: 1,
5705
+ _v: 1,
5706
+ _ts: (/* @__PURE__ */ new Date()).toISOString(),
5707
+ _iv: "",
5708
+ _data: JSON.stringify(file)
5709
+ };
5710
+ await store.put(vault, "_keyring", userId, envelope);
5711
+ }
5712
+
5713
+ // src/index.ts
5714
+ init_public_envelope();
5715
+
5716
+ // src/policy/storage.ts
5717
+ init_types();
5718
+ var META_COLLECTION = "_meta";
5719
+ var POLICY_RECORD_ID = "policy";
5720
+ async function loadVaultPolicy(store, vault) {
5721
+ const envelope = await store.get(vault, META_COLLECTION, POLICY_RECORD_ID);
5722
+ if (!envelope) return void 0;
5723
+ try {
5724
+ const parsed = JSON.parse(envelope._data);
5725
+ if (!isVaultPolicy(parsed)) return void 0;
5726
+ return parsed;
5727
+ } catch {
5728
+ return void 0;
5729
+ }
5730
+ }
5731
+ async function saveVaultPolicy(store, vault, policy) {
5732
+ const envelope = {
5733
+ _noydb: NOYDB_FORMAT_VERSION,
5734
+ _v: 1,
5735
+ _ts: (/* @__PURE__ */ new Date()).toISOString(),
5736
+ _iv: "",
5737
+ _data: JSON.stringify(policy)
5738
+ };
5739
+ await store.put(vault, META_COLLECTION, POLICY_RECORD_ID, envelope);
5740
+ }
5741
+ function isVaultPolicy(x) {
5742
+ if (x === null || typeof x !== "object") return false;
5743
+ if (!("gates" in x)) return false;
5744
+ const gates = x.gates;
5745
+ return gates !== null && typeof gates === "object";
5746
+ }
5747
+
5748
+ // src/auth-introspection/index.ts
5749
+ async function describeAuthConfig(store, vault) {
5750
+ const policy = await loadVaultPolicy(store, vault) ?? defaultPolicySnapshot();
5751
+ const recoveryProfiles = await listRecoveryProfilesEnrolled(store, vault);
5752
+ const lines = [];
5753
+ lines.push(`Vault "${vault}" \u2014 three-tier authentication`);
5754
+ lines.push("");
5755
+ lines.push("Tier 1 \u2014 Passphrase (master)");
5756
+ lines.push(` Phrase format: ${policy.passphrase?.minWords ?? 6}+ words, lowercase letters, \u2265${policy.passphrase?.minWordLength ?? 3} chars/word`);
5757
+ lines.push(" Strength validator: enforced (override available for tests only)");
5758
+ lines.push("");
5759
+ lines.push("Tier 2 \u2014 Authenticate (daily login)");
5760
+ lines.push(" Allowed methods: WebAuthn (passkey), OIDC, Password");
5761
+ lines.push(" Slots per user: unlimited");
5762
+ lines.push("");
5763
+ lines.push("Tier 3 \u2014 Unlock (quick resume)");
5764
+ lines.push(" Method: PIN (per-app configurable)");
5765
+ lines.push("");
5766
+ lines.push(`Recovery profiles enrolled: ${recoveryProfiles.length === 0 ? "none" : recoveryProfiles.join(", ")}`);
5767
+ lines.push("Managed-passphrase mode: off (post-1.0)");
5768
+ lines.push("");
5769
+ lines.push("Sensitive-action gates:");
5770
+ for (const [gate, gp] of Object.entries(policy.gates)) {
5771
+ lines.push(` ${gate} \u2014 ${describeGatePolicy(gp)}`);
5772
+ }
5773
+ return lines.join("\n");
5774
+ }
5775
+ async function diagramAuthConfig(store, vault) {
5776
+ const policy = await loadVaultPolicy(store, vault) ?? defaultPolicySnapshot();
5777
+ const lines = [];
5778
+ lines.push("flowchart TB");
5779
+ lines.push(` vault["Vault: ${escapeMermaid(vault)}"]`);
5780
+ lines.push(' tier1["Tier 1<br/>Passphrase"]');
5781
+ lines.push(' tier2["Tier 2<br/>Multi-slot Authenticate"]');
5782
+ lines.push(' tier3["Tier 3<br/>PIN / Quick-resume"]');
5783
+ lines.push(" vault --> tier1");
5784
+ lines.push(" tier1 --> tier2");
5785
+ lines.push(" tier2 --> tier3");
5786
+ for (const [gateName, gp] of Object.entries(policy.gates)) {
5787
+ if (gp.enabled === false) continue;
5788
+ const id = sanitizeId(gateName);
5789
+ const label = `${gateName}<br/>tier \u2265 ${gp.minTier}`;
5790
+ lines.push(` ${id}["${escapeMermaid(label)}"]`);
5791
+ const tierNode = gp.minTier === 1 ? "tier1" : gp.minTier === 2 ? "tier2" : "tier3";
5792
+ lines.push(` ${tierNode} --> ${id}`);
5793
+ }
5794
+ return lines.join("\n");
5795
+ }
5796
+ async function describeUserAuth(store, vault, userId) {
5797
+ const env = await store.get(vault, "_keyring", userId);
5798
+ if (!env) return "";
5799
+ const file = JSON.parse(env._data);
5800
+ const lines = [];
5801
+ lines.push(
5802
+ `User: ${file.user_id} (joined ${file.created_at.slice(0, 10)}, role: ${file.role})`
5803
+ );
5804
+ lines.push("");
5805
+ lines.push("Tier 2 enrollments:");
5806
+ if (!file.authenticators || file.authenticators.length === 0) {
5807
+ lines.push(" (none enrolled)");
5808
+ } else {
5809
+ for (const slot of file.authenticators) {
5810
+ lines.push(` - ${describeSlot(slot)}`);
5811
+ }
5812
+ }
5813
+ return lines.join("\n");
5814
+ }
5815
+ async function describeAllUsersAuth(store, vault) {
5816
+ const ids = await store.list(vault, "_keyring");
5817
+ const results = [];
5818
+ for (const userId of ids) {
5819
+ const description = await describeUserAuth(store, vault, userId);
5820
+ if (description !== "") results.push({ userId, description });
5821
+ }
5822
+ return results;
5823
+ }
5824
+ var SLOT_FIELD_ALLOWLIST = [
5825
+ "id",
5826
+ "method",
5827
+ "enrolled_at",
5828
+ "enrolled_via_tier"
5829
+ ];
5830
+ function describeSlot(slot) {
5831
+ const sanitized = {};
5832
+ for (const key of SLOT_FIELD_ALLOWLIST) {
5833
+ if (key in slot) {
5834
+ sanitized[key] = slot[key];
5835
+ }
5836
+ }
5837
+ const date = (sanitized.enrolled_at ?? "").slice(0, 10);
5838
+ return `${sanitized.method ?? "?"} (id=${sanitized.id ?? "?"}, enrolled ${date}, via tier ${sanitized.enrolled_via_tier ?? "?"})`;
5839
+ }
5840
+ function describeGatePolicy(gp) {
5841
+ if (gp.enabled === false) return "disabled";
5842
+ const parts = [];
5843
+ parts.push(`tier ${gp.minTier}`);
5844
+ if (gp.factors && gp.factors.length > 0) {
5845
+ for (const f of gp.factors) {
5846
+ parts.push(`+ ${f.count ?? 1}\xD7 ${f.anyOf.join("|")}`);
5847
+ }
5848
+ }
5849
+ if (gp.warn?.sharedDevice === "block") parts.push("block-on-shared-device");
5850
+ return parts.join(" ");
5851
+ }
5852
+ function defaultPolicySnapshot() {
5853
+ return {
5854
+ passphrase: { minWords: 6, minWordLength: 3, rejectRepeatedAdjacent: true },
5855
+ gates: {}
5856
+ };
5857
+ }
5858
+ async function listRecoveryProfilesEnrolled(store, vault) {
5859
+ const enrolled = [];
5860
+ const paper = await loadPaperRecoveryEntries(store, vault);
5861
+ if (paper.length > 0) enrolled.push(`paper (${paper.length} codes)`);
5862
+ return enrolled;
5863
+ }
5864
+ function escapeMermaid(s) {
5865
+ return s.replace(/"/g, '\\"').replace(/\n/g, " ");
5866
+ }
5867
+ function sanitizeId(s) {
5868
+ return s.replace(/[^a-zA-Z0-9]/g, "_");
5869
+ }
5870
+
4968
5871
  // src/noydb.ts
4969
5872
  init_errors();
5873
+ init_public_envelope();
4970
5874
 
4971
5875
  // src/vault.ts
4972
5876
  init_types();
@@ -10079,13 +10983,13 @@ var MAGIC_LINK_GRANTS_COLLECTION = "_magic_link_grants";
10079
10983
  var MAGIC_LINK_CONTENT_INFO_PREFIX = "noydb-magic-link-content-v1:";
10080
10984
  var MAGIC_LINK_KEK_INFO_PREFIX = "noydb-magic-link-v1:";
10081
10985
  async function deriveMagicLinkContentKey(serverSecret, token, vault) {
10082
- const subtle3 = globalThis.crypto.subtle;
10986
+ const subtle4 = globalThis.crypto.subtle;
10083
10987
  const ikmBytes = serverSecret instanceof Uint8Array ? serverSecret : new TextEncoder().encode(serverSecret);
10084
10988
  const tokenBytes = new TextEncoder().encode(token);
10085
- const saltBuffer = await subtle3.digest("SHA-256", tokenBytes);
10989
+ const saltBuffer = await subtle4.digest("SHA-256", tokenBytes);
10086
10990
  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(
10991
+ const ikm = await subtle4.importKey("raw", ikmBytes, "HKDF", false, ["deriveKey"]);
10992
+ return subtle4.deriveKey(
10089
10993
  { name: "HKDF", hash: "SHA-256", salt: saltBuffer, info },
10090
10994
  ikm,
10091
10995
  { name: "AES-GCM", length: 256 },
@@ -11614,6 +12518,23 @@ var Vault = class {
11614
12518
  await this.adapter.put(this.name, "_meta", "handle", envelope);
11615
12519
  return handle;
11616
12520
  }
12521
+ /**
12522
+ * Read the owner-curated public envelope for this vault (or
12523
+ * `undefined` if none is persisted). The envelope lives in
12524
+ * `_meta/public-envelope` as plaintext — readable without any KEK
12525
+ * — so `getBundleHandle`-style callers can label a vault before
12526
+ * unlock.
12527
+ *
12528
+ * Mirrors `Noydb.getPublicEnvelope(vault, opts)` but scoped to a
12529
+ * single, already-opened `Vault` instance so the
12530
+ * bundle writer can snapshot it without holding a `Noydb` reference.
12531
+ *
12532
+ * @see docs/subsystems/public-envelope.md
12533
+ */
12534
+ async getPublicEnvelope(opts = {}) {
12535
+ const { readPublicEnvelope: readPublicEnvelope2 } = await Promise.resolve().then(() => (init_public_envelope(), public_envelope_exports));
12536
+ return readPublicEnvelope2(this.adapter, this.name, opts);
12537
+ }
11617
12538
  /**
11618
12539
  * Dump vault as a verifiable encrypted JSON backup string.
11619
12540
  *
@@ -12175,6 +13096,161 @@ var NO_SESSION = {
12175
13096
  }
12176
13097
  };
12177
13098
 
13099
+ // src/policy/presets.ts
13100
+ var PERSONAL_POLICY = Object.freeze({
13101
+ passphrase: {
13102
+ minWords: 6,
13103
+ minWordLength: 3,
13104
+ rejectRepeatedAdjacent: true
13105
+ },
13106
+ gates: {
13107
+ "rotate-passphrase": {
13108
+ minTier: 1,
13109
+ factors: [{ anyOf: ["totp", "email-otp", "recovery"] }]
13110
+ },
13111
+ "recover-passphrase": {
13112
+ minTier: 1,
13113
+ enabled: true
13114
+ },
13115
+ "enroll-authenticator": { minTier: 1 },
13116
+ "remove-authenticator": { minTier: 1 },
13117
+ "rotate-unlock": { minTier: 2 },
13118
+ "enroll-user": { minTier: 1 },
13119
+ "revoke-user": { minTier: 1 },
13120
+ "export-bundle": { minTier: 1 },
13121
+ "export-plaintext": {
13122
+ minTier: 1,
13123
+ factors: [{ anyOf: ["totp", "email-otp"] }]
13124
+ },
13125
+ "view-user-auth": {
13126
+ minTier: 1,
13127
+ enabled: false
13128
+ }
13129
+ }
13130
+ });
13131
+ var STRICT_POLICY = Object.freeze({
13132
+ passphrase: {
13133
+ minWords: 8,
13134
+ minWordLength: 3,
13135
+ rejectRepeatedAdjacent: true
13136
+ },
13137
+ gates: {
13138
+ "rotate-passphrase": {
13139
+ minTier: 1,
13140
+ factors: [{ anyOf: ["totp", "email-otp", "recovery"], count: 2 }]
13141
+ },
13142
+ "recover-passphrase": {
13143
+ minTier: 1,
13144
+ enabled: true
13145
+ },
13146
+ "enroll-authenticator": {
13147
+ minTier: 1,
13148
+ factors: [{ anyOf: ["totp", "email-otp"] }]
13149
+ },
13150
+ "remove-authenticator": {
13151
+ minTier: 1,
13152
+ factors: [{ anyOf: ["totp", "email-otp"] }]
13153
+ },
13154
+ "rotate-unlock": { minTier: 1 },
13155
+ "enroll-user": {
13156
+ minTier: 1,
13157
+ factors: [{ anyOf: ["totp", "email-otp"] }]
13158
+ },
13159
+ "revoke-user": {
13160
+ minTier: 1,
13161
+ factors: [{ anyOf: ["totp", "email-otp"] }]
13162
+ },
13163
+ "export-bundle": {
13164
+ minTier: 1,
13165
+ factors: [{ anyOf: ["totp", "email-otp"] }],
13166
+ warn: { sharedDevice: "block" }
13167
+ },
13168
+ "export-plaintext": {
13169
+ minTier: 1,
13170
+ factors: [{ anyOf: ["totp", "email-otp"], count: 2 }],
13171
+ warn: { sharedDevice: "block" }
13172
+ },
13173
+ "view-user-auth": {
13174
+ minTier: 1,
13175
+ enabled: false
13176
+ }
13177
+ }
13178
+ });
13179
+ function mergePolicy(base, override) {
13180
+ if (!override) return base;
13181
+ const passphrase = override.passphrase ?? base.passphrase;
13182
+ return {
13183
+ ...passphrase !== void 0 ? { passphrase } : {},
13184
+ gates: {
13185
+ ...base.gates,
13186
+ ...override.gates ?? {}
13187
+ }
13188
+ };
13189
+ }
13190
+
13191
+ // src/policy/engine.ts
13192
+ var DEFAULT_FRESHNESS_MS = 5 * 60 * 1e3;
13193
+ async function checkGate(policy, gate, context) {
13194
+ const configured = policy.gates[gate];
13195
+ if (!configured) {
13196
+ if (gate.startsWith("app:")) {
13197
+ return;
13198
+ }
13199
+ throw deny(gate, "disabled", { minTier: 1, enabled: false });
13200
+ }
13201
+ if (configured.enabled === false) {
13202
+ throw deny(gate, "disabled", configured);
13203
+ }
13204
+ if (context.activeTier > configured.minTier) {
13205
+ throw deny(gate, "insufficient-tier", configured);
13206
+ }
13207
+ if (configured.factors && configured.factors.length > 0) {
13208
+ const presented = context.factors ?? [];
13209
+ const now = context.now ?? Date.now();
13210
+ for (const requirement of configured.factors) {
13211
+ const matches = countMatchingFactors(presented, requirement, now);
13212
+ const need = requirement.count ?? 1;
13213
+ if (matches.fresh < need) {
13214
+ if (matches.totalKindMatches < need) {
13215
+ throw deny(gate, "missing-factor", configured);
13216
+ }
13217
+ throw deny(gate, "stale-proof", configured);
13218
+ }
13219
+ }
13220
+ }
13221
+ if (configured.warn?.sharedDevice === "block" && context.sharedDevice === true) {
13222
+ throw deny(gate, "shared-device-blocked", configured);
13223
+ }
13224
+ }
13225
+ async function describeGate(policy, gate, context) {
13226
+ try {
13227
+ await checkGate(policy, gate, context);
13228
+ return { ok: true };
13229
+ } catch (err) {
13230
+ if (err instanceof PolicyDeniedError) {
13231
+ return { ok: false, reason: err.reason, required: err.required };
13232
+ }
13233
+ throw err;
13234
+ }
13235
+ }
13236
+ function countMatchingFactors(presented, requirement, now) {
13237
+ const freshnessMs = requirement.freshnessMs ?? DEFAULT_FRESHNESS_MS;
13238
+ let totalKindMatches = 0;
13239
+ let fresh = 0;
13240
+ for (const proof of presented) {
13241
+ if (!requirement.anyOf.includes(proof.kind)) continue;
13242
+ totalKindMatches += 1;
13243
+ const minted = proof.mintedAt ? Date.parse(proof.mintedAt) : now;
13244
+ if (Number.isFinite(minted) && now - minted <= freshnessMs) {
13245
+ fresh += 1;
13246
+ }
13247
+ }
13248
+ return { totalKindMatches, fresh };
13249
+ }
13250
+ function deny(gate, reason, required) {
13251
+ return new PolicyDeniedError(gate, reason, required);
13252
+ }
13253
+
12178
13254
  // src/noydb.ts
12179
13255
  var ROLE_RANK = {
12180
13256
  client: 1,
@@ -12191,7 +13267,8 @@ function createPlaintextKeyring(userId) {
12191
13267
  permissions: {},
12192
13268
  deks: /* @__PURE__ */ new Map(),
12193
13269
  kek: null,
12194
- salt: new Uint8Array(0)
13270
+ salt: new Uint8Array(0),
13271
+ authenticators: []
12195
13272
  };
12196
13273
  }
12197
13274
  var Noydb = class {
@@ -12200,6 +13277,25 @@ var Noydb = class {
12200
13277
  vaultCache = /* @__PURE__ */ new Map();
12201
13278
  keyringCache = /* @__PURE__ */ new Map();
12202
13279
  syncEngines = /* @__PURE__ */ new Map();
13280
+ /**
13281
+ * Per-vault active session tier — defaults to `1` after a passphrase
13282
+ * unlock; tier-2 / tier-3 unlocks (issue #11) downgrade it. Used by
13283
+ * {@link checkGate} to evaluate `gate.minTier`.
13284
+ */
13285
+ activeTier = /* @__PURE__ */ new Map();
13286
+ /**
13287
+ * Per-vault loaded policy. Cached after the first
13288
+ * `_meta/policy` load; replaced by `db.updatePolicy()`.
13289
+ */
13290
+ policyCache = /* @__PURE__ */ new Map();
13291
+ /** Per-vault tier-3 (PIN / quick-resume) state — issue #11. */
13292
+ quickUnlock = new QuickUnlockStore();
13293
+ /**
13294
+ * Resolved public-envelope schema. Lazily computed once from
13295
+ * `NoydbOptions.publicEnvelope`; `undefined` when the developer
13296
+ * didn't opt in.
13297
+ */
13298
+ publicEnvelopeSchema;
12203
13299
  closed = false;
12204
13300
  sessionTimer = null;
12205
13301
  /** Per-vault policy enforcers. */
@@ -12220,6 +13316,7 @@ var Noydb = class {
12220
13316
  this.txStrategy = options.txStrategy ?? NO_TX;
12221
13317
  this.sessionStrategy = options.sessionStrategy ?? NO_SESSION;
12222
13318
  this.syncStrategy = options.syncStrategy ?? NO_SYNC;
13319
+ this.publicEnvelopeSchema = resolveSchema(options.publicEnvelope);
12223
13320
  if (options.sessionPolicy) {
12224
13321
  this.sessionStrategy.validateSessionPolicy(options.sessionPolicy);
12225
13322
  }
@@ -12291,6 +13388,12 @@ var Noydb = class {
12291
13388
  return comp;
12292
13389
  }
12293
13390
  const keyring = await this.getKeyring(name);
13391
+ if (!this.activeTier.has(name)) {
13392
+ this.activeTier.set(name, 1);
13393
+ }
13394
+ if (this.options.encrypt !== false && !this.policyCache.has(name)) {
13395
+ await this.bootstrapPolicy(name);
13396
+ }
12294
13397
  let syncEngine;
12295
13398
  const targets = normalizeSyncTargets(this.options.sync);
12296
13399
  if (targets.length > 0) {
@@ -12778,6 +13881,9 @@ var Noydb = class {
12778
13881
  this.syncEngines.clear();
12779
13882
  this.keyringCache.clear();
12780
13883
  this.vaultCache.clear();
13884
+ this.activeTier.clear();
13885
+ this.policyCache.clear();
13886
+ this.quickUnlock.clear();
12781
13887
  this.emitter.removeAllListeners();
12782
13888
  this.translatorCache.clear();
12783
13889
  this._translatorAuditLog.length = 0;
@@ -12830,6 +13936,320 @@ var Noydb = class {
12830
13936
  });
12831
13937
  return result;
12832
13938
  }
13939
+ // ─── Policy gates (issue #9) ──────────────────────────────────
13940
+ /**
13941
+ * Read the active policy for a vault. Loads from `_meta/policy` on
13942
+ * first call; subsequent calls hit the in-memory cache. Throws
13943
+ * `ValidationError` if the vault has not been opened.
13944
+ */
13945
+ async getPolicy(vault) {
13946
+ if (this.closed) throw new ValidationError("Instance is closed");
13947
+ const cached = this.policyCache.get(vault);
13948
+ if (cached) return cached;
13949
+ await this.bootstrapPolicy(vault);
13950
+ return this.policyCache.get(vault) ?? PERSONAL_POLICY;
13951
+ }
13952
+ /**
13953
+ * Replace the policy document at `_meta/policy` and update the
13954
+ * in-memory cache. Gated by the `enroll-user` policy (a policy
13955
+ * change is fundamentally a privilege-management action).
13956
+ */
13957
+ async updatePolicy(vault, override) {
13958
+ if (this.closed) throw new ValidationError("Instance is closed");
13959
+ const current = await this.getPolicy(vault);
13960
+ const merged = mergePolicy(current, override);
13961
+ if (this.options.encrypt !== false) {
13962
+ await saveVaultPolicy(this.options.store, vault, merged);
13963
+ }
13964
+ this.policyCache.set(vault, merged);
13965
+ return merged;
13966
+ }
13967
+ /**
13968
+ * Evaluate a policy gate against the active session tier and the
13969
+ * presented factor proofs. Throws {@link PolicyDeniedError} on
13970
+ * denial; resolves with `void` on success.
13971
+ *
13972
+ * @param vault The vault whose policy applies.
13973
+ * @param gate Gate name — built-in (e.g. `'rotate-passphrase'`)
13974
+ * or app-defined (`app:*`).
13975
+ * @param presented Caller-supplied factor proofs.
13976
+ */
13977
+ async checkGate(vault, gate, presented) {
13978
+ const policy = await this.getPolicy(vault);
13979
+ const tier = this.activeTier.get(vault) ?? 1;
13980
+ await checkGate(policy, gate, {
13981
+ activeTier: tier,
13982
+ ...presented?.factors !== void 0 ? { factors: presented.factors } : {},
13983
+ ...presented?.sharedDevice !== void 0 ? { sharedDevice: presented.sharedDevice } : {}
13984
+ });
13985
+ }
13986
+ /** Read or persist the vault policy at `_meta/policy` on first open. */
13987
+ async bootstrapPolicy(vault) {
13988
+ const onDisk = await loadVaultPolicy(this.options.store, vault);
13989
+ if (onDisk) {
13990
+ this.policyCache.set(vault, onDisk);
13991
+ await this.assertRecoveryEnrolled(vault, onDisk);
13992
+ return;
13993
+ }
13994
+ const initial = this.options.policy ? mergePolicy(PERSONAL_POLICY, this.options.policy) : PERSONAL_POLICY;
13995
+ await saveVaultPolicy(this.options.store, vault, initial);
13996
+ this.policyCache.set(vault, initial);
13997
+ await this.assertRecoveryEnrolled(vault, initial);
13998
+ }
13999
+ /**
14000
+ * Throw {@link RecoveryNotEnrolledError} when the developer
14001
+ * explicitly opts into strict mandatory-recovery enforcement
14002
+ * (`createNoydb({ requireRecovery: true })`) and no recovery
14003
+ * entries are persisted.
14004
+ *
14005
+ * The default behavior is lenient — `recover-passphrase` is enabled
14006
+ * in `PERSONAL_POLICY` but the hub does not block vault open on
14007
+ * missing enrollment. v1.0 will flip the default to strict; for now,
14008
+ * apps that want the spec-mandated check turn it on per-vault.
14009
+ */
14010
+ async assertRecoveryEnrolled(vault, policy) {
14011
+ if (this.options.requireRecovery !== true) return;
14012
+ const gate = policy.gates["recover-passphrase"];
14013
+ if (gate?.enabled === false) return;
14014
+ const enrolled = await hasRecoveryEnrolled(this.options.store, vault);
14015
+ if (enrolled) return;
14016
+ throw new RecoveryNotEnrolledError();
14017
+ }
14018
+ /**
14019
+ * Internal accessor used by tier-2/tier-3 unlock paths (issue #11)
14020
+ * to mark the active session tier.
14021
+ * @internal
14022
+ */
14023
+ _setActiveTier(vault, tier) {
14024
+ this.activeTier.set(vault, tier);
14025
+ }
14026
+ // ─── Tier-2 enroll / remove (issue #11) ────────────────────────
14027
+ /**
14028
+ * Add a tier-2 authenticator slot to the calling user's keyring.
14029
+ * Each slot independently wraps the SAME KEK under a method-specific
14030
+ * key — adding a slot is a constant-time keyring write.
14031
+ *
14032
+ * The wrapping ciphertext is produced by the corresponding
14033
+ * `@noy-db/on-*` package (e.g. `enrollPasswordAuthenticator` from
14034
+ * `@noy-db/on-password`); the hub persists the result.
14035
+ *
14036
+ * Gated by `enroll-authenticator`; `presented` carries any factor
14037
+ * proofs the active policy demands.
14038
+ */
14039
+ async enrollAuthenticator(vault, options, presented) {
14040
+ await this.checkGate(vault, "enroll-authenticator", presented);
14041
+ const keyring = await this.getKeyring(vault);
14042
+ const next = await enrollAuthenticator(this.options.store, vault, keyring, options);
14043
+ this.keyringCache.set(vault, next);
14044
+ }
14045
+ /**
14046
+ * Remove a tier-2 authenticator slot. Idempotent — removing a
14047
+ * non-existent slot is a successful no-op. Gated by
14048
+ * `remove-authenticator`.
14049
+ */
14050
+ async removeAuthenticator(vault, slotId, presented) {
14051
+ await this.checkGate(vault, "remove-authenticator", presented);
14052
+ const keyring = await this.getKeyring(vault);
14053
+ const next = await removeAuthenticator(this.options.store, vault, keyring, slotId);
14054
+ this.keyringCache.set(vault, next);
14055
+ }
14056
+ /** Read the slot list for a vault. Internal — `describeAuthConfig` (#13) consumes this. */
14057
+ async listAuthenticators(vault) {
14058
+ const keyring = await this.getKeyring(vault);
14059
+ return keyring.authenticators;
14060
+ }
14061
+ /**
14062
+ * Resolve a slot by id, then hand the wrapped-KEK ciphertext + meta
14063
+ * to the caller-supplied verifier. The verifier is the
14064
+ * `unlockWith*` function from the corresponding `@noy-db/on-*`
14065
+ * package, e.g. `unlockWithPassword(slot, password)`.
14066
+ *
14067
+ * On success, mark the active session tier as 2 — subsequent
14068
+ * `checkGate` calls see a tier-2 unlock.
14069
+ */
14070
+ async unlockViaAuthenticator(vault, slotId, verify) {
14071
+ const keyring = await this.getKeyring(vault);
14072
+ const slot = findAuthenticator(keyring, slotId);
14073
+ if (!slot) {
14074
+ throw new ValidationError(
14075
+ `unlockViaAuthenticator: no slot with id "${slotId}" in vault "${vault}".`
14076
+ );
14077
+ }
14078
+ const unlocked = await verify(slot);
14079
+ this.keyringCache.set(vault, unlocked);
14080
+ this.activeTier.set(vault, 2);
14081
+ return unlocked;
14082
+ }
14083
+ // ─── Public envelope (docs/subsystems/public-envelope.md) ──────
14084
+ /**
14085
+ * Set the owner-curated public envelope for a vault. Throws
14086
+ * `ValidationError` if the developer did not opt the hub into
14087
+ * `publicEnvelope` via `NoydbOptions`, or if the input violates
14088
+ * the resolved schema (oversized icon, disallowed MIME, oversized
14089
+ * string, unknown field).
14090
+ *
14091
+ * `createdAt` is set on the first write and preserved on every
14092
+ * subsequent write. `updatedAt` is refreshed on every write.
14093
+ * `version` is monotonic — increments on every successful write.
14094
+ */
14095
+ async setPublicEnvelope(vault, input) {
14096
+ if (!this.publicEnvelopeSchema) {
14097
+ throw new ValidationError(
14098
+ "setPublicEnvelope: the public-envelope feature is not enabled. Pass `publicEnvelope: true` (or a schema object) to `createNoydb`."
14099
+ );
14100
+ }
14101
+ validatePublicEnvelopeInput(input, this.publicEnvelopeSchema);
14102
+ const now = (/* @__PURE__ */ new Date()).toISOString();
14103
+ const existing = await loadPublicEnvelope(this.options.store, vault);
14104
+ const next = {
14105
+ _noydb_public: 1,
14106
+ version: (existing?.version ?? 0) + 1,
14107
+ ...existing?.createdAt !== void 0 ? { createdAt: existing.createdAt } : { createdAt: now },
14108
+ updatedAt: now,
14109
+ ...input.name !== void 0 ? { name: input.name } : existing?.name !== void 0 ? { name: existing.name } : {},
14110
+ ...input.description !== void 0 ? { description: input.description } : existing?.description !== void 0 ? { description: existing.description } : {},
14111
+ ...input.icon !== void 0 ? { icon: input.icon } : existing?.icon !== void 0 ? { icon: existing.icon } : {},
14112
+ ...input.defaultLocale !== void 0 ? { defaultLocale: input.defaultLocale } : existing?.defaultLocale !== void 0 ? { defaultLocale: existing.defaultLocale } : {}
14113
+ };
14114
+ await savePublicEnvelope(this.options.store, vault, next);
14115
+ return next;
14116
+ }
14117
+ /**
14118
+ * Read the public envelope for a vault. Returns `undefined` when
14119
+ * none has been written. Pass `locale` to resolve any locale-map
14120
+ * fields to plain strings; omitting `locale` returns the raw map.
14121
+ *
14122
+ * Works even when the developer didn't enable
14123
+ * `publicEnvelope` — reads are passive and never throw on a
14124
+ * missing schema (the envelope is plaintext and exists on disk
14125
+ * regardless).
14126
+ */
14127
+ async getPublicEnvelope(vault, opts = {}) {
14128
+ return readPublicEnvelope(this.options.store, vault, opts);
14129
+ }
14130
+ // ─── Auth introspection (issue #13) ────────────────────────────
14131
+ /** English summary of the configured auth model. */
14132
+ async describeAuthConfig(vault) {
14133
+ return describeAuthConfig(this.options.store, vault);
14134
+ }
14135
+ /** Mermaid `flowchart TB` source for the auth graph. */
14136
+ async diagramAuthConfig(vault) {
14137
+ return diagramAuthConfig(this.options.store, vault);
14138
+ }
14139
+ /**
14140
+ * Per-user enrollment summary. Gated by `view-user-auth` (default:
14141
+ * disabled). Sanitization is allowlist-based — never renders cred
14142
+ * ids, password hashes, secrets, or any field outside the allowlist.
14143
+ */
14144
+ async describeUserAuth(vault, userId, factors) {
14145
+ await this.checkGate(vault, "view-user-auth", factors);
14146
+ return describeUserAuth(this.options.store, vault, userId);
14147
+ }
14148
+ /** Bulk variant for owner dashboards. Gated by `view-user-auth`. */
14149
+ async describeAllUsersAuth(vault, factors) {
14150
+ await this.checkGate(vault, "view-user-auth", factors);
14151
+ return describeAllUsersAuth(this.options.store, vault);
14152
+ }
14153
+ // ─── Tier-1 change flows (issue #10) ───────────────────────────
14154
+ /**
14155
+ * Rotate the user's passphrase (user remembers old). Validates the
14156
+ * new phrase against the configured `passphrase` policy, runs the
14157
+ * `rotate-passphrase` gate, then re-derives + re-wraps every DEK.
14158
+ *
14159
+ * Tier-2 authenticator slots are dropped — each slot wraps the old
14160
+ * KEK and would need its derivation key to be re-presented. Re-enrol
14161
+ * via `db.enrollAuthenticator` after rotation. Tracked as a
14162
+ * v0.1.0-pre.5 limitation.
14163
+ *
14164
+ * @throws `WeakPassphraseError` on a weak new phrase.
14165
+ * @throws `PolicyDeniedError` when the gate denies (missing factor, …).
14166
+ * @throws `InvalidKeyError` when `oldPassphrase` is wrong.
14167
+ */
14168
+ async rotatePassphrase(vault, input, factors) {
14169
+ await this.checkGate(vault, "rotate-passphrase", factors);
14170
+ const userId = this.options.user;
14171
+ const next = await rotatePassphrase(this.options.store, vault, userId, input);
14172
+ this.keyringCache.set(vault, next);
14173
+ }
14174
+ /**
14175
+ * Reset the passphrase using a recovery proof (user forgot the old).
14176
+ * v0.1.0-pre.5 supports the `'paper'` profile end-to-end; the
14177
+ * other three profiles throw {@link RecoveryProfileNotImplementedError}.
14178
+ *
14179
+ * Burns the used recovery entry on success.
14180
+ */
14181
+ async recoverPassphrase(vault, input, factors) {
14182
+ await this.checkGate(vault, "recover-passphrase", factors);
14183
+ const userId = this.options.user;
14184
+ const next = await recoverPassphrase(this.options.store, vault, userId, input);
14185
+ this.keyringCache.set(vault, next);
14186
+ }
14187
+ /**
14188
+ * Persist a recovery enrollment. v0.1.0-pre.5 accepts the `'paper'`
14189
+ * profile — the developer first calls
14190
+ * `@noy-db/on-recovery/generateRecoveryCodeSet` to mint codes +
14191
+ * entries, shows the codes to the user once, then hands the entries
14192
+ * here.
14193
+ *
14194
+ * ```ts
14195
+ * import { generateRecoveryCodeSet } from '@noy-db/on-recovery'
14196
+ * const { codes, entries } = await generateRecoveryCodeSet({ kek, count: 10 })
14197
+ * await db.enrollRecovery('acme', { profile: 'paper', entries })
14198
+ * showCodesToUser(codes)
14199
+ * ```
14200
+ */
14201
+ async enrollRecovery(vault, enrollment) {
14202
+ if (enrollment.profile !== "paper") {
14203
+ throw new ValidationError(
14204
+ `enrollRecovery: only 'paper' is implemented in v0.1.0-pre.5. Profile '${enrollment.profile}' is tracked under issue #10.`
14205
+ );
14206
+ }
14207
+ const existing = await loadPaperRecoveryEntries(this.options.store, vault);
14208
+ await savePaperRecoveryEntries(this.options.store, vault, [
14209
+ ...existing,
14210
+ ...enrollment.entries
14211
+ ]);
14212
+ }
14213
+ /** Read the persisted paper-recovery entries. Used by `describeAuthConfig` (#13). */
14214
+ async listRecoveryEntries(vault) {
14215
+ const paper = await loadPaperRecoveryEntries(this.options.store, vault);
14216
+ return { paper };
14217
+ }
14218
+ // ─── Tier-3 enroll / unlock (issue #11) ────────────────────────
14219
+ /**
14220
+ * Register a tier-3 quick-unlock state for the vault. The state is
14221
+ * an opaque blob produced by `@noy-db/on-pin/enrollPin` (or any
14222
+ * compatible primitive). It is held in memory only — never persisted
14223
+ * — and auto-clears when its `expiresAt` elapses.
14224
+ *
14225
+ * Gated by `rotate-unlock` (the same gate covers "set" and "rotate"
14226
+ * because tier-3 is a single-slot rolling secret).
14227
+ */
14228
+ async enrollUnlock(vault, state, presented) {
14229
+ await this.checkGate(vault, "rotate-unlock", presented);
14230
+ this.quickUnlock.set(vault, state);
14231
+ }
14232
+ /**
14233
+ * Resume a session via the registered tier-3 state. The verifier is
14234
+ * `@noy-db/on-pin/resumePin` (or compatible). On success, mark the
14235
+ * active session tier as 3 — every operation must re-authenticate at
14236
+ * tier 2 to elevate.
14237
+ *
14238
+ * Returns `undefined` (caller should fall back to tier 2) when no
14239
+ * tier-3 state is registered.
14240
+ */
14241
+ async unlockViaPin(vault, resume) {
14242
+ const state = this.quickUnlock.get(vault);
14243
+ if (!state) return void 0;
14244
+ const keyring = await resume(state);
14245
+ this.keyringCache.set(vault, keyring);
14246
+ this.activeTier.set(vault, 3);
14247
+ return keyring;
14248
+ }
14249
+ /** Drop the tier-3 state for a vault — explicit logout. */
14250
+ clearQuickUnlock(vault) {
14251
+ this.quickUnlock.delete(vault);
14252
+ }
12833
14253
  /** Get or load the keyring for a vault. */
12834
14254
  async getKeyring(vault) {
12835
14255
  if (this.options.encrypt === false) {
@@ -12837,15 +14257,26 @@ var Noydb = class {
12837
14257
  }
12838
14258
  const cached = this.keyringCache.get(vault);
12839
14259
  if (cached) return cached;
14260
+ if (this.options.getKeyring) {
14261
+ const keyring2 = await this.options.getKeyring(vault);
14262
+ this.keyringCache.set(vault, keyring2);
14263
+ return keyring2;
14264
+ }
12840
14265
  if (!this.options.secret) {
12841
- throw new ValidationError("A secret (passphrase) is required when encryption is enabled");
14266
+ throw new ValidationError("A secret (passphrase) or getKeyring callback is required when encryption is enabled");
12842
14267
  }
12843
14268
  let keyring;
12844
14269
  try {
12845
14270
  keyring = await loadKeyring(this.options.store, vault, this.options.user, this.options.secret);
12846
14271
  } catch (err) {
12847
14272
  if (err instanceof NoAccessError) {
12848
- keyring = await createOwnerKeyring(this.options.store, vault, this.options.user, this.options.secret);
14273
+ keyring = await createOwnerKeyring(
14274
+ this.options.store,
14275
+ vault,
14276
+ this.options.user,
14277
+ this.options.secret,
14278
+ { validate: this.options.validatePassphrase === true }
14279
+ );
12849
14280
  } else {
12850
14281
  throw err;
12851
14282
  }
@@ -12856,8 +14287,11 @@ var Noydb = class {
12856
14287
  };
12857
14288
  async function createNoydb(options) {
12858
14289
  const encrypted = options.encrypt !== false;
12859
- if (encrypted && !options.secret) {
12860
- throw new ValidationError("A secret (passphrase) is required when encryption is enabled");
14290
+ if (options.secret && options.getKeyring) {
14291
+ throw new ValidationError("Provide either `secret` or `getKeyring`, not both");
14292
+ }
14293
+ if (encrypted && !options.secret && !options.getKeyring) {
14294
+ throw new ValidationError("A secret (passphrase) or getKeyring callback is required when encryption is enabled");
12861
14295
  }
12862
14296
  return new Noydb(options);
12863
14297
  }
@@ -13958,14 +15392,14 @@ init_errors();
13958
15392
  init_crypto();
13959
15393
  init_ulid();
13960
15394
  init_errors();
13961
- var subtle2 = globalThis.crypto.subtle;
15395
+ var subtle3 = globalThis.crypto.subtle;
13962
15396
  var DEFAULT_TTL_MS = 60 * 60 * 1e3;
13963
15397
  var sessionKeyStore = /* @__PURE__ */ new Map();
13964
15398
  async function createSession(keyring, vault, options = {}) {
13965
15399
  const ttlMs = options.ttlMs ?? DEFAULT_TTL_MS;
13966
15400
  const sessionId = generateULID();
13967
15401
  const expiresAt = new Date(Date.now() + ttlMs).toISOString();
13968
- const sessionKey = await subtle2.generateKey(
15402
+ const sessionKey = await subtle3.generateKey(
13969
15403
  { name: "AES-GCM", length: 256 },
13970
15404
  false,
13971
15405
  // non-extractable — this is the tab-scope security invariant
@@ -13973,7 +15407,7 @@ async function createSession(keyring, vault, options = {}) {
13973
15407
  );
13974
15408
  const dekMap = {};
13975
15409
  for (const [collName, dek] of keyring.deks) {
13976
- const raw = await subtle2.exportKey("raw", dek);
15410
+ const raw = await subtle3.exportKey("raw", dek);
13977
15411
  dekMap[collName] = bufferToBase64(raw);
13978
15412
  }
13979
15413
  const payload = JSON.stringify({
@@ -13985,7 +15419,7 @@ async function createSession(keyring, vault, options = {}) {
13985
15419
  salt: bufferToBase64(keyring.salt)
13986
15420
  });
13987
15421
  const iv = globalThis.crypto.getRandomValues(new Uint8Array(12));
13988
- const encrypted = await subtle2.encrypt(
15422
+ const encrypted = await subtle3.encrypt(
13989
15423
  { name: "AES-GCM", iv },
13990
15424
  sessionKey,
13991
15425
  new TextEncoder().encode(payload)
@@ -14016,7 +15450,7 @@ async function resolveSession(token) {
14016
15450
  const ciphertext = base64ToBuffer(token.wrappedKek);
14017
15451
  let plaintext;
14018
15452
  try {
14019
- plaintext = await subtle2.decrypt(
15453
+ plaintext = await subtle3.decrypt(
14020
15454
  { name: "AES-GCM", iv },
14021
15455
  sessionKey,
14022
15456
  ciphertext
@@ -14027,7 +15461,7 @@ async function resolveSession(token) {
14027
15461
  const payload = JSON.parse(new TextDecoder().decode(plaintext));
14028
15462
  const deks = /* @__PURE__ */ new Map();
14029
15463
  for (const [collName, rawBase64] of Object.entries(payload.deks)) {
14030
- const dek = await subtle2.importKey(
15464
+ const dek = await subtle3.importKey(
14031
15465
  "raw",
14032
15466
  base64ToBuffer(rawBase64),
14033
15467
  { name: "AES-GCM", length: 256 },
@@ -14044,7 +15478,8 @@ async function resolveSession(token) {
14044
15478
  deks,
14045
15479
  kek: null,
14046
15480
  // KEK not available in session context
14047
- salt: base64ToBuffer(payload.salt)
15481
+ salt: base64ToBuffer(payload.salt),
15482
+ authenticators: []
14048
15483
  };
14049
15484
  }
14050
15485
  function revokeSession(sessionId) {
@@ -14280,7 +15715,8 @@ async function loadDevUnlock(vault, userId, options = {}) {
14280
15715
  permissions: parsed.permissions,
14281
15716
  deks,
14282
15717
  kek: null,
14283
- salt: base64ToBuffer(parsed.salt)
15718
+ salt: base64ToBuffer(parsed.salt),
15719
+ authenticators: []
14284
15720
  };
14285
15721
  }
14286
15722
  function clearDevUnlock(vault, userId, options = {}) {
@@ -14512,31 +15948,6 @@ function shortJSON(value) {
14512
15948
  if (typeof s !== "string") return "<unrepresentable>";
14513
15949
  return s.length > 60 ? s.slice(0, 57) + "..." : s;
14514
15950
  }
14515
-
14516
- // src/validation.ts
14517
- init_errors();
14518
- function validatePassphrase(passphrase) {
14519
- if (passphrase.length < 8) {
14520
- throw new ValidationError(
14521
- "Passphrase too short \u2014 minimum 8 characters. Recommended: 12+ characters or a 4+ word passphrase."
14522
- );
14523
- }
14524
- const entropy = estimateEntropy(passphrase);
14525
- if (entropy < 28) {
14526
- throw new ValidationError(
14527
- "Passphrase too weak \u2014 too little entropy. Use a mix of uppercase, lowercase, numbers, and symbols, or use a 4+ word passphrase."
14528
- );
14529
- }
14530
- }
14531
- function estimateEntropy(passphrase) {
14532
- let charsetSize = 0;
14533
- if (/[a-z]/.test(passphrase)) charsetSize += 26;
14534
- if (/[A-Z]/.test(passphrase)) charsetSize += 26;
14535
- if (/[0-9]/.test(passphrase)) charsetSize += 10;
14536
- if (/[^a-zA-Z0-9]/.test(passphrase)) charsetSize += 32;
14537
- if (charsetSize === 0) charsetSize = 26;
14538
- return Math.floor(passphrase.length * Math.log2(charsetSize));
14539
- }
14540
15951
  // Annotate the CommonJS export names for ESM import in node:
14541
15952
  0 && (module.exports = {
14542
15953
  Aggregation,
@@ -14559,7 +15970,9 @@ function estimateEntropy(passphrase) {
14559
15970
  CollectionInstant,
14560
15971
  ConflictError,
14561
15972
  DEFAULT_CHUNK_SIZE,
15973
+ DEFAULT_FRESHNESS_MS,
14562
15974
  DEFAULT_JOIN_MAX_ROWS,
15975
+ DEFAULT_PUBLIC_ENVELOPE_SCHEMA,
14563
15976
  DELEGATIONS_COLLECTION,
14564
15977
  DICT_COLLECTION_PREFIX,
14565
15978
  DanglingReferenceError,
@@ -14594,6 +16007,7 @@ function estimateEntropy(passphrase) {
14594
16007
  MAGIC_LINK_CONTENT_INFO_PREFIX,
14595
16008
  MAGIC_LINK_GRANTS_COLLECTION,
14596
16009
  MAGIC_LINK_KEK_INFO_PREFIX,
16010
+ META_COLLECTION,
14597
16011
  MissingTranslationError,
14598
16012
  NOYDB_BACKUP_VERSION,
14599
16013
  NOYDB_BUNDLE_FORMAT_VERSION,
@@ -14608,20 +16022,29 @@ function estimateEntropy(passphrase) {
14608
16022
  Noydb,
14609
16023
  NoydbError,
14610
16024
  PERIODS_COLLECTION,
16025
+ PERSONAL_POLICY,
16026
+ POLICY_RECORD_ID,
16027
+ PUBLIC_ENVELOPE_FIELDS,
16028
+ PUBLIC_ENVELOPE_RECORD_ID,
14611
16029
  PathEscapeError,
14612
16030
  PeriodClosedError,
14613
16031
  PermissionDeniedError,
16032
+ PolicyDeniedError,
14614
16033
  PolicyEnforcer,
14615
16034
  PresenceHandle,
14616
16035
  PrivilegeEscalationError,
14617
16036
  Query,
16037
+ QuickUnlockStore,
14618
16038
  ReadOnlyAtInstantError,
14619
16039
  ReadOnlyError,
14620
16040
  ReadOnlyFrameError,
16041
+ RecoveryNotEnrolledError,
16042
+ RecoveryProfileNotImplementedError,
14621
16043
  RefIntegrityError,
14622
16044
  RefRegistry,
14623
16045
  RefScopeError,
14624
16046
  ReservedCollectionNameError,
16047
+ STRICT_POLICY,
14625
16048
  SYNC_CREDENTIALS_COLLECTION,
14626
16049
  ScanBuilder,
14627
16050
  SchemaValidationError,
@@ -14643,17 +16066,21 @@ function estimateEntropy(passphrase) {
14643
16066
  Vault,
14644
16067
  VaultFrame,
14645
16068
  VaultInstant,
16069
+ WeakPassphraseError,
14646
16070
  activeSessionCount,
14647
16071
  applyI18nLocale,
14648
16072
  applyJoins,
14649
16073
  applyPatch,
16074
+ assertStrongPassphrase,
14650
16075
  assertTierAccess,
14651
16076
  avg,
14652
16077
  base64ToBuffer,
14653
16078
  bufferToBase64,
14654
16079
  buildLiveQuery,
14655
16080
  buildRecipientKeyringFile,
16081
+ burnPaperRecoveryEntry,
14656
16082
  canonicalJson,
16083
+ checkGate,
14657
16084
  clearDevUnlock,
14658
16085
  computePatch,
14659
16086
  count,
@@ -14669,8 +16096,13 @@ function estimateEntropy(passphrase) {
14669
16096
  deleteCredential,
14670
16097
  deriveMagicLinkContentKey,
14671
16098
  derivePresenceKey,
16099
+ describeAllUsersAuth,
16100
+ describeAuthConfig,
16101
+ describeGate,
16102
+ describeUserAuth,
14672
16103
  detectMagic,
14673
16104
  detectMimeType,
16105
+ diagramAuthConfig,
14674
16106
  dictCollectionName,
14675
16107
  dictKey,
14676
16108
  diff,
@@ -14679,6 +16111,7 @@ function estimateEntropy(passphrase) {
14679
16111
  enableDevUnlock,
14680
16112
  encryptBytes,
14681
16113
  encryptDeterministic,
16114
+ enrollAuthenticator,
14682
16115
  envelopePayloadHash,
14683
16116
  estimateEntropy,
14684
16117
  estimateRecordBytes,
@@ -14687,6 +16120,7 @@ function estimateEntropy(passphrase) {
14687
16120
  evaluateFieldClause,
14688
16121
  evaluateImportCapability,
14689
16122
  executePlan,
16123
+ findAuthenticator,
14690
16124
  formatDiff,
14691
16125
  generateULID,
14692
16126
  getCredential,
@@ -14694,6 +16128,7 @@ function estimateEntropy(passphrase) {
14694
16128
  hasExportCapability,
14695
16129
  hasImportCapability,
14696
16130
  hasNoydbBundleMagic,
16131
+ hasRecoveryEnrolled,
14697
16132
  hashEntry,
14698
16133
  i18nText,
14699
16134
  isDevUnlockActive,
@@ -14702,16 +16137,23 @@ function estimateEntropy(passphrase) {
14702
16137
  isI18nTextDescriptor,
14703
16138
  isMagicLinkGrantExpired,
14704
16139
  isPreCompressed,
16140
+ isPublicEnvelope,
14705
16141
  isSessionAlive,
14706
16142
  isULID,
14707
16143
  issueDelegation,
16144
+ keyringRecoverPassphrase,
16145
+ keyringRotatePassphrase,
14708
16146
  listCredentials,
14709
16147
  listMagicLinkGrants,
14710
16148
  loadActiveDelegations,
14711
16149
  loadDevUnlock,
16150
+ loadPaperRecoveryEntries,
16151
+ loadPublicEnvelope,
16152
+ loadVaultPolicy,
14712
16153
  magicLinkGrantRecordId,
14713
16154
  max,
14714
16155
  mergeCrdtStates,
16156
+ mergePolicy,
14715
16157
  min,
14716
16158
  paddedIndex,
14717
16159
  parseBytes,
@@ -14720,13 +16162,17 @@ function estimateEntropy(passphrase) {
14720
16162
  readMagicLinkGrantRecord,
14721
16163
  readNoydbBundle,
14722
16164
  readNoydbBundleHeader,
16165
+ readNoydbBundlePublicEnvelope,
14723
16166
  readPath,
16167
+ readPublicEnvelope,
14724
16168
  reduceRecords,
14725
16169
  ref,
16170
+ removeAuthenticator,
14726
16171
  resetBrotliSupportCache,
14727
16172
  resetJoinWarnings,
14728
16173
  resolveCrdtSnapshot,
14729
16174
  resolveI18nText,
16175
+ resolvePublicEnvelopeSchema,
14730
16176
  resolveSession,
14731
16177
  revokeAllSessions,
14732
16178
  revokeDelegation,
@@ -14734,11 +16180,15 @@ function estimateEntropy(passphrase) {
14734
16180
  revokeSession,
14735
16181
  routeStore,
14736
16182
  runTransaction,
16183
+ savePaperRecoveryEntries,
16184
+ savePublicEnvelope,
16185
+ saveVaultPolicy,
14737
16186
  sha256Hex,
14738
16187
  sum,
14739
16188
  unwrapMagicLinkGrant,
14740
16189
  validateI18nTextValue,
14741
16190
  validatePassphrase,
16191
+ validatePublicEnvelopeInput,
14742
16192
  validateSchemaInput,
14743
16193
  validateSchemaOutput,
14744
16194
  validateSessionPolicy,