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