@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.
- package/dist/blobs/index.cjs.map +1 -1
- package/dist/blobs/index.d.cts +3 -3
- package/dist/blobs/index.d.ts +3 -3
- package/dist/blobs/index.js +2 -2
- package/dist/bundle/index.cjs +26 -3
- package/dist/bundle/index.cjs.map +1 -1
- package/dist/bundle/index.d.cts +3 -3
- package/dist/bundle/index.d.ts +3 -3
- package/dist/bundle/index.js +3 -1
- package/dist/{chunk-M2F2JAWB.js → chunk-6NPQTBZN.js} +103 -8
- package/dist/chunk-6NPQTBZN.js.map +1 -0
- package/dist/{chunk-UQFSPSWG.js → chunk-E4OOAPBZ.js} +2 -2
- package/dist/chunk-EMIGCR7X.js +39 -0
- package/dist/chunk-EMIGCR7X.js.map +1 -0
- package/dist/{chunk-EXQRC2L4.js → chunk-H3DV46AQ.js} +2 -2
- package/dist/{chunk-XHFOENR2.js → chunk-LMKOSLJY.js} +2 -2
- package/dist/{chunk-GJILMRPO.js → chunk-LRN3PNI6.js} +42 -4
- package/dist/chunk-LRN3PNI6.js.map +1 -0
- package/dist/{chunk-4OWFYIDQ.js → chunk-MIRZMUSQ.js} +3 -3
- package/dist/{chunk-ZRG4V3F5.js → chunk-NXUVITPB.js} +1 -1
- package/dist/chunk-NXUVITPB.js.map +1 -0
- package/dist/{chunk-5AATM2M2.js → chunk-QUDXYI4W.js} +2 -2
- package/dist/{chunk-ZLMV3TUA.js → chunk-QV4WLLKB.js} +3 -3
- package/dist/{chunk-E445ICYI.js → chunk-UFL4DUEV.js} +5 -3
- package/dist/chunk-UFL4DUEV.js.map +1 -0
- package/dist/chunk-UQQ2XFXI.js +155 -0
- package/dist/chunk-UQQ2XFXI.js.map +1 -0
- package/dist/consent/index.d.cts +3 -3
- package/dist/consent/index.d.ts +3 -3
- package/dist/{dev-unlock-KrKkcqD3.d.ts → dev-unlock-BgFqShBi.d.ts} +1 -1
- package/dist/{dev-unlock-CeXic1xC.d.cts → dev-unlock-qVMxG2Je.d.cts} +1 -1
- package/dist/{hash-ChfJjRjQ.d.ts → hash-BhoL7iUE.d.ts} +1 -1
- package/dist/{hash-9KO1BGxh.d.cts → hash-Bpvl2eSe.d.cts} +1 -1
- package/dist/history/index.cjs.map +1 -1
- package/dist/history/index.d.cts +4 -4
- package/dist/history/index.d.ts +4 -4
- package/dist/history/index.js +2 -2
- package/dist/i18n/index.cjs +3 -1
- package/dist/i18n/index.cjs.map +1 -1
- package/dist/i18n/index.d.cts +3 -3
- package/dist/i18n/index.d.ts +3 -3
- package/dist/i18n/index.js +3 -3
- package/dist/{index-DN-J-5wT.d.cts → index-6xNpPsxR.d.cts} +1 -1
- package/dist/{index-BRHBCmLt.d.ts → index-DJTf9yxn.d.ts} +1 -1
- package/dist/{index-DhjMjz7L.d.cts → index-DhK_zqOO.d.ts} +39 -5
- package/dist/{index-C8kQtmOk.d.ts → index-DyRt_5vM.d.cts} +39 -5
- package/dist/index.cjs +1501 -51
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +261 -19
- package/dist/index.d.ts +261 -19
- package/dist/index.js +1118 -44
- package/dist/index.js.map +1 -1
- package/dist/{ledger-2NX4L7PN.js → ledger-GA4DMJS6.js} +3 -3
- package/dist/periods/index.cjs.map +1 -1
- package/dist/periods/index.d.cts +3 -3
- package/dist/periods/index.d.ts +3 -3
- package/dist/periods/index.js +3 -3
- package/dist/public-envelope-R4EIEQE6.js +31 -0
- package/dist/public-envelope-R4EIEQE6.js.map +1 -0
- package/dist/query/index.d.cts +1 -1
- package/dist/query/index.d.ts +1 -1
- package/dist/session/index.cjs +4 -2
- package/dist/session/index.cjs.map +1 -1
- package/dist/session/index.d.cts +4 -4
- package/dist/session/index.d.ts +4 -4
- package/dist/session/index.js +1 -1
- package/dist/shadow/index.d.cts +3 -3
- package/dist/shadow/index.d.ts +3 -3
- package/dist/store/index.d.cts +3 -3
- package/dist/store/index.d.ts +3 -3
- package/dist/sync/index.cjs.map +1 -1
- package/dist/sync/index.d.cts +2 -2
- package/dist/sync/index.d.ts +2 -2
- package/dist/sync/index.js +2 -2
- package/dist/team/index.cjs +3 -1
- package/dist/team/index.cjs.map +1 -1
- package/dist/team/index.d.cts +3 -3
- package/dist/team/index.d.ts +3 -3
- package/dist/team/index.js +4 -4
- package/dist/tx/index.d.cts +3 -3
- package/dist/tx/index.d.ts +3 -3
- package/dist/{types-Bfs0qr5F.d.cts → types-BpyE4o_n.d.cts} +935 -4
- package/dist/{types-BZpCZB8N.d.ts → types-Df72wWCC.d.ts} +935 -4
- package/package.json +1 -1
- package/dist/chunk-E445ICYI.js.map +0 -1
- package/dist/chunk-GJILMRPO.js.map +0 -1
- package/dist/chunk-M2F2JAWB.js.map +0 -1
- package/dist/chunk-ZRG4V3F5.js.map +0 -1
- /package/dist/{chunk-UQFSPSWG.js.map → chunk-E4OOAPBZ.js.map} +0 -0
- /package/dist/{chunk-EXQRC2L4.js.map → chunk-H3DV46AQ.js.map} +0 -0
- /package/dist/{chunk-XHFOENR2.js.map → chunk-LMKOSLJY.js.map} +0 -0
- /package/dist/{chunk-4OWFYIDQ.js.map → chunk-MIRZMUSQ.js.map} +0 -0
- /package/dist/{chunk-5AATM2M2.js.map → chunk-QUDXYI4W.js.map} +0 -0
- /package/dist/{chunk-ZLMV3TUA.js.map → chunk-QV4WLLKB.js.map} +0 -0
- /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
|
|
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
|
|
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
|
|
10088
|
-
return
|
|
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(
|
|
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 (
|
|
12860
|
-
throw new ValidationError("
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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,
|