@atlastodo/shared 0.1.11-rc.7 → 0.1.11-rc.8
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/index.d.ts +1 -0
- package/dist/index.js +374 -0
- package/dist/vault.d.ts +124 -0
- package/package.json +4 -3
- package/test-vectors/vault_item_vectors.json +563 -0
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -20775,6 +20775,367 @@ function presetTarget(id, now, timeZone) {
|
|
|
20775
20775
|
return makeInstant(p.year + 1, 0, 1, 0, 0, 0, timeZone);
|
|
20776
20776
|
}
|
|
20777
20777
|
}
|
|
20778
|
+
// src/vault.ts
|
|
20779
|
+
var VAULT_ITEM_TYPES = [
|
|
20780
|
+
"login",
|
|
20781
|
+
"note",
|
|
20782
|
+
"card",
|
|
20783
|
+
"identity",
|
|
20784
|
+
"ssh_key",
|
|
20785
|
+
"api_key"
|
|
20786
|
+
];
|
|
20787
|
+
var VAULT_FIELD_KINDS = ["text", "hidden", "totp", "email", "url", "phone"];
|
|
20788
|
+
var VAULT_SECRET_KINDS = ["hidden", "totp"];
|
|
20789
|
+
var VAULT_HISTORY_PER_FIELD = 5;
|
|
20790
|
+
|
|
20791
|
+
class VaultError extends Error {
|
|
20792
|
+
code;
|
|
20793
|
+
constructor(code, message) {
|
|
20794
|
+
super(message);
|
|
20795
|
+
this.code = code;
|
|
20796
|
+
this.name = "VaultError";
|
|
20797
|
+
}
|
|
20798
|
+
}
|
|
20799
|
+
var CREATE_KEYS = ["type", "name", "fields", "urls", "notes", "tags", "folder", "favorite"];
|
|
20800
|
+
var PATCH_KEYS = [...CREATE_KEYS, "set_fields", "remove_fields"];
|
|
20801
|
+
var bad = (message) => new VaultError("bad_input", message);
|
|
20802
|
+
var isObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
20803
|
+
var int = (v) => typeof v === "number" && Number.isFinite(v) ? Math.trunc(v) : null;
|
|
20804
|
+
var strings = (v) => Array.isArray(v) ? v.filter((s) => typeof s === "string") : [];
|
|
20805
|
+
var has = (o, key) => Object.prototype.hasOwnProperty.call(o, key);
|
|
20806
|
+
var sameName = (a, b) => a.trim().toLowerCase() === b.trim().toLowerCase();
|
|
20807
|
+
var isSecret = (kind) => VAULT_SECRET_KINDS.includes(kind) || !VAULT_FIELD_KINDS.includes(kind);
|
|
20808
|
+
function jsonEqual(a, b) {
|
|
20809
|
+
if (a === b)
|
|
20810
|
+
return true;
|
|
20811
|
+
if (Array.isArray(a) || Array.isArray(b)) {
|
|
20812
|
+
return Array.isArray(a) && Array.isArray(b) && a.length === b.length && a.every((x, i) => jsonEqual(x, b[i]));
|
|
20813
|
+
}
|
|
20814
|
+
if (isObject(a) && isObject(b)) {
|
|
20815
|
+
const ka = Object.keys(a);
|
|
20816
|
+
return ka.length === Object.keys(b).length && ka.every((k) => has(b, k) && jsonEqual(a[k], b[k]));
|
|
20817
|
+
}
|
|
20818
|
+
return false;
|
|
20819
|
+
}
|
|
20820
|
+
function cleanVaultTags(tags) {
|
|
20821
|
+
const out = [];
|
|
20822
|
+
for (const t of tags.map((t) => t.trim())) {
|
|
20823
|
+
if (t && !out.some((o) => o.toLowerCase() === t.toLowerCase()))
|
|
20824
|
+
out.push(t);
|
|
20825
|
+
}
|
|
20826
|
+
return out;
|
|
20827
|
+
}
|
|
20828
|
+
function cleanVaultUrls(urls) {
|
|
20829
|
+
const out = [];
|
|
20830
|
+
for (const u of urls.map((u) => u.trim()))
|
|
20831
|
+
if (u && !out.includes(u))
|
|
20832
|
+
out.push(u);
|
|
20833
|
+
return out;
|
|
20834
|
+
}
|
|
20835
|
+
function readFields(v) {
|
|
20836
|
+
if (!Array.isArray(v))
|
|
20837
|
+
return [];
|
|
20838
|
+
const out = [];
|
|
20839
|
+
for (const f of v) {
|
|
20840
|
+
if (!isObject(f) || typeof f.name !== "string" || typeof f.value !== "string")
|
|
20841
|
+
continue;
|
|
20842
|
+
out.push({ name: f.name, kind: typeof f.kind === "string" ? f.kind : "text", value: f.value });
|
|
20843
|
+
}
|
|
20844
|
+
return out;
|
|
20845
|
+
}
|
|
20846
|
+
function readHistory(v) {
|
|
20847
|
+
if (!Array.isArray(v))
|
|
20848
|
+
return [];
|
|
20849
|
+
const out = [];
|
|
20850
|
+
for (const h of v) {
|
|
20851
|
+
if (!isObject(h) || typeof h.field !== "string" || typeof h.value !== "string")
|
|
20852
|
+
continue;
|
|
20853
|
+
out.push({ field: h.field, value: h.value, changed_at: int(h.changed_at) ?? 0 });
|
|
20854
|
+
}
|
|
20855
|
+
return out;
|
|
20856
|
+
}
|
|
20857
|
+
function toVaultItem(id, f) {
|
|
20858
|
+
const text = (k) => typeof f[k] === "string" ? f[k] : "";
|
|
20859
|
+
return {
|
|
20860
|
+
id,
|
|
20861
|
+
type: typeof f.type === "string" ? f.type : "login",
|
|
20862
|
+
name: text("name"),
|
|
20863
|
+
fields: readFields(f.fields),
|
|
20864
|
+
urls: strings(f.urls),
|
|
20865
|
+
notes: text("notes"),
|
|
20866
|
+
tags: strings(f.tags),
|
|
20867
|
+
folder: typeof f.folder === "string" ? f.folder : null,
|
|
20868
|
+
favorite: typeof f.favorite === "boolean" ? f.favorite : false,
|
|
20869
|
+
history: readHistory(f.history),
|
|
20870
|
+
created_at: int(f.created_at) ?? 0,
|
|
20871
|
+
updated_at: int(f.updated_at) ?? 0,
|
|
20872
|
+
deleted_at: int(f.deleted_at)
|
|
20873
|
+
};
|
|
20874
|
+
}
|
|
20875
|
+
function summarizeVaultItem(item) {
|
|
20876
|
+
return {
|
|
20877
|
+
id: item.id,
|
|
20878
|
+
type: item.type,
|
|
20879
|
+
name: item.name,
|
|
20880
|
+
fields: item.fields.map((f) => ({ name: f.name, kind: f.kind })),
|
|
20881
|
+
urls: [...item.urls],
|
|
20882
|
+
tags: [...item.tags],
|
|
20883
|
+
folder: item.folder,
|
|
20884
|
+
favorite: item.favorite,
|
|
20885
|
+
created_at: item.created_at,
|
|
20886
|
+
updated_at: item.updated_at,
|
|
20887
|
+
deleted_at: item.deleted_at
|
|
20888
|
+
};
|
|
20889
|
+
}
|
|
20890
|
+
function checkKeys(input, allowed) {
|
|
20891
|
+
const unknown = Object.keys(input).sort().find((k) => !allowed.includes(k));
|
|
20892
|
+
if (unknown !== undefined)
|
|
20893
|
+
throw bad(`${unknown} is not a vault item field`);
|
|
20894
|
+
}
|
|
20895
|
+
function checkType(v) {
|
|
20896
|
+
if (typeof v === "string" && VAULT_ITEM_TYPES.includes(v))
|
|
20897
|
+
return v;
|
|
20898
|
+
throw bad(`type must be one of ${VAULT_ITEM_TYPES.join(", ")}`);
|
|
20899
|
+
}
|
|
20900
|
+
function checkName(v) {
|
|
20901
|
+
if (typeof v !== "string")
|
|
20902
|
+
throw bad("name must be a string");
|
|
20903
|
+
const name = v.trim();
|
|
20904
|
+
if (!name)
|
|
20905
|
+
throw bad("a vault item needs a name");
|
|
20906
|
+
return name;
|
|
20907
|
+
}
|
|
20908
|
+
function checkStrings(key, v) {
|
|
20909
|
+
if (!Array.isArray(v) || !v.every((s) => typeof s === "string")) {
|
|
20910
|
+
throw bad(`${key} must be a list of strings`);
|
|
20911
|
+
}
|
|
20912
|
+
return v;
|
|
20913
|
+
}
|
|
20914
|
+
function checkNotes(v) {
|
|
20915
|
+
if (typeof v !== "string")
|
|
20916
|
+
throw bad("notes must be a string");
|
|
20917
|
+
return v;
|
|
20918
|
+
}
|
|
20919
|
+
function checkFolder(v) {
|
|
20920
|
+
if (v === null)
|
|
20921
|
+
return null;
|
|
20922
|
+
if (typeof v === "string" && v.trim())
|
|
20923
|
+
return v.trim();
|
|
20924
|
+
throw bad("folder must be a folder id or null");
|
|
20925
|
+
}
|
|
20926
|
+
function checkFavorite(v) {
|
|
20927
|
+
if (typeof v !== "boolean")
|
|
20928
|
+
throw bad("favorite must be true or false");
|
|
20929
|
+
return v;
|
|
20930
|
+
}
|
|
20931
|
+
function givenField(key, v) {
|
|
20932
|
+
const shape = () => bad(`${key} must be a list of {name, kind, value} objects with string values`);
|
|
20933
|
+
if (!isObject(v) || typeof v.name !== "string" || typeof v.value !== "string")
|
|
20934
|
+
throw shape();
|
|
20935
|
+
let kind = null;
|
|
20936
|
+
if (v.kind !== undefined && v.kind !== null) {
|
|
20937
|
+
if (typeof v.kind !== "string")
|
|
20938
|
+
throw shape();
|
|
20939
|
+
kind = v.kind;
|
|
20940
|
+
}
|
|
20941
|
+
const name = v.name.trim();
|
|
20942
|
+
if (!name)
|
|
20943
|
+
throw bad("a field needs a name");
|
|
20944
|
+
if (kind !== null && !VAULT_FIELD_KINDS.includes(kind)) {
|
|
20945
|
+
throw bad(`field kind must be one of ${VAULT_FIELD_KINDS.join(", ")}`);
|
|
20946
|
+
}
|
|
20947
|
+
return { name, kind, value: v.value };
|
|
20948
|
+
}
|
|
20949
|
+
function fieldValue(name, kind, value) {
|
|
20950
|
+
if (kind !== "totp")
|
|
20951
|
+
return value;
|
|
20952
|
+
const t = value.trim();
|
|
20953
|
+
if (t.slice(0, 10).replace(/[A-Z]/g, (c) => c.toLowerCase()) === "otpauth://")
|
|
20954
|
+
return t;
|
|
20955
|
+
const seed = t.replace(/\s/g, "").replace(/[a-z]/g, (c) => c.toUpperCase()).replace(/=+$/, "");
|
|
20956
|
+
if (!seed || !/^[A-Z2-7]+$/.test(seed)) {
|
|
20957
|
+
throw bad(`the totp field "${name}" needs an otpauth:// URI or a base32 secret`);
|
|
20958
|
+
}
|
|
20959
|
+
return seed;
|
|
20960
|
+
}
|
|
20961
|
+
function givenFields(v) {
|
|
20962
|
+
if (!Array.isArray(v)) {
|
|
20963
|
+
throw bad("fields must be a list of {name, kind, value} objects with string values");
|
|
20964
|
+
}
|
|
20965
|
+
const out = [];
|
|
20966
|
+
for (const item of v) {
|
|
20967
|
+
const g = givenField("fields", item);
|
|
20968
|
+
const kind = g.kind ?? "text";
|
|
20969
|
+
const value = fieldValue(g.name, kind, g.value);
|
|
20970
|
+
if (out.some((f) => sameName(f.name, g.name))) {
|
|
20971
|
+
throw bad(`two fields are named "${g.name.toLowerCase()}"`);
|
|
20972
|
+
}
|
|
20973
|
+
out.push({ name: g.name, kind, value });
|
|
20974
|
+
}
|
|
20975
|
+
return out;
|
|
20976
|
+
}
|
|
20977
|
+
var fieldJson = (f) => ({ name: f.name, kind: f.kind, value: f.value });
|
|
20978
|
+
var historyJson = (h) => ({
|
|
20979
|
+
field: h.field,
|
|
20980
|
+
value: h.value,
|
|
20981
|
+
changed_at: h.changed_at
|
|
20982
|
+
});
|
|
20983
|
+
function newVaultItemFields(input, now) {
|
|
20984
|
+
if (!isObject(input))
|
|
20985
|
+
throw bad("a vault item is a JSON object");
|
|
20986
|
+
checkKeys(input, CREATE_KEYS);
|
|
20987
|
+
const type = has(input, "type") ? checkType(input.type) : "login";
|
|
20988
|
+
if (!has(input, "name"))
|
|
20989
|
+
throw bad("a vault item needs a name");
|
|
20990
|
+
const name = checkName(input.name);
|
|
20991
|
+
const fields = has(input, "fields") ? givenFields(input.fields) : [];
|
|
20992
|
+
const urls = has(input, "urls") ? cleanVaultUrls(checkStrings("urls", input.urls)) : [];
|
|
20993
|
+
const notes = has(input, "notes") ? checkNotes(input.notes) : "";
|
|
20994
|
+
const tags = has(input, "tags") ? cleanVaultTags(checkStrings("tags", input.tags)) : [];
|
|
20995
|
+
const folder = has(input, "folder") ? checkFolder(input.folder) : null;
|
|
20996
|
+
const favorite = has(input, "favorite") ? checkFavorite(input.favorite) : false;
|
|
20997
|
+
return {
|
|
20998
|
+
type,
|
|
20999
|
+
name,
|
|
21000
|
+
fields: fields.map(fieldJson),
|
|
21001
|
+
urls,
|
|
21002
|
+
notes,
|
|
21003
|
+
tags,
|
|
21004
|
+
folder,
|
|
21005
|
+
favorite,
|
|
21006
|
+
history: [],
|
|
21007
|
+
created_at: now,
|
|
21008
|
+
updated_at: now
|
|
21009
|
+
};
|
|
21010
|
+
}
|
|
21011
|
+
function patchedFields(current, set, remove) {
|
|
21012
|
+
let fields = current.map((f) => ({ ...f }));
|
|
21013
|
+
if (remove !== undefined) {
|
|
21014
|
+
for (const name of checkStrings("remove_fields", remove)) {
|
|
21015
|
+
const before = fields.length;
|
|
21016
|
+
fields = fields.filter((f) => !sameName(f.name, name));
|
|
21017
|
+
if (fields.length === before)
|
|
21018
|
+
throw bad(`no field "${name.trim()}" to remove`);
|
|
21019
|
+
}
|
|
21020
|
+
}
|
|
21021
|
+
if (set !== undefined) {
|
|
21022
|
+
if (!Array.isArray(set)) {
|
|
21023
|
+
throw bad("set_fields must be a list of {name, kind, value} objects with string values");
|
|
21024
|
+
}
|
|
21025
|
+
for (const item of set) {
|
|
21026
|
+
const g = givenField("set_fields", item);
|
|
21027
|
+
const existing = fields.find((f) => sameName(f.name, g.name));
|
|
21028
|
+
if (existing) {
|
|
21029
|
+
if (g.kind !== null)
|
|
21030
|
+
existing.kind = g.kind;
|
|
21031
|
+
existing.value = fieldValue(existing.name, existing.kind, g.value);
|
|
21032
|
+
} else {
|
|
21033
|
+
const kind = g.kind ?? "text";
|
|
21034
|
+
fields.push({ name: g.name, kind, value: fieldValue(g.name, kind, g.value) });
|
|
21035
|
+
}
|
|
21036
|
+
}
|
|
21037
|
+
}
|
|
21038
|
+
return fields;
|
|
21039
|
+
}
|
|
21040
|
+
function nextHistory(history, before, after, now) {
|
|
21041
|
+
const entries = before.filter((old) => isSecret(old.kind)).filter((old) => {
|
|
21042
|
+
const next = after.find((f) => sameName(f.name, old.name));
|
|
21043
|
+
return !next || next.value !== old.value;
|
|
21044
|
+
}).map((old) => ({ field: old.name, value: old.value, changed_at: now }));
|
|
21045
|
+
entries.push(...history);
|
|
21046
|
+
const counts = new Map;
|
|
21047
|
+
return entries.filter((e) => {
|
|
21048
|
+
const key = e.field.trim().toLowerCase();
|
|
21049
|
+
const n = (counts.get(key) ?? 0) + 1;
|
|
21050
|
+
counts.set(key, n);
|
|
21051
|
+
return n <= VAULT_HISTORY_PER_FIELD;
|
|
21052
|
+
});
|
|
21053
|
+
}
|
|
21054
|
+
function vaultItemChanges(current, patch, now) {
|
|
21055
|
+
if (!isObject(patch))
|
|
21056
|
+
throw bad("a vault item change is a JSON object");
|
|
21057
|
+
checkKeys(patch, PATCH_KEYS);
|
|
21058
|
+
if (has(patch, "fields") && (has(patch, "set_fields") || has(patch, "remove_fields"))) {
|
|
21059
|
+
throw bad("give fields (the whole list) or set_fields and remove_fields, not both");
|
|
21060
|
+
}
|
|
21061
|
+
const item = toVaultItem("", current);
|
|
21062
|
+
const changes = {};
|
|
21063
|
+
if (has(patch, "type")) {
|
|
21064
|
+
const t = checkType(patch.type);
|
|
21065
|
+
if (t !== item.type)
|
|
21066
|
+
changes.type = t;
|
|
21067
|
+
}
|
|
21068
|
+
if (has(patch, "name")) {
|
|
21069
|
+
const n = checkName(patch.name);
|
|
21070
|
+
if (n !== item.name)
|
|
21071
|
+
changes.name = n;
|
|
21072
|
+
}
|
|
21073
|
+
let fields = null;
|
|
21074
|
+
if (has(patch, "fields"))
|
|
21075
|
+
fields = givenFields(patch.fields);
|
|
21076
|
+
else if (has(patch, "set_fields") || has(patch, "remove_fields")) {
|
|
21077
|
+
fields = patchedFields(item.fields, patch.set_fields, patch.remove_fields);
|
|
21078
|
+
}
|
|
21079
|
+
if (fields) {
|
|
21080
|
+
const list = fields;
|
|
21081
|
+
const dup = list.find((f, i) => list.slice(0, i).some((g) => sameName(g.name, f.name)));
|
|
21082
|
+
if (dup)
|
|
21083
|
+
throw bad(`two fields are named "${dup.name.trim().toLowerCase()}"`);
|
|
21084
|
+
if (!jsonEqual(list, item.fields)) {
|
|
21085
|
+
const history = nextHistory(item.history, item.fields, list, now);
|
|
21086
|
+
changes.fields = list.map(fieldJson);
|
|
21087
|
+
if (!jsonEqual(history, item.history))
|
|
21088
|
+
changes.history = history.map(historyJson);
|
|
21089
|
+
}
|
|
21090
|
+
}
|
|
21091
|
+
if (has(patch, "urls")) {
|
|
21092
|
+
const urls = cleanVaultUrls(checkStrings("urls", patch.urls));
|
|
21093
|
+
if (!jsonEqual(urls, item.urls))
|
|
21094
|
+
changes.urls = urls;
|
|
21095
|
+
}
|
|
21096
|
+
if (has(patch, "notes")) {
|
|
21097
|
+
const notes = checkNotes(patch.notes);
|
|
21098
|
+
if (notes !== item.notes)
|
|
21099
|
+
changes.notes = notes;
|
|
21100
|
+
}
|
|
21101
|
+
if (has(patch, "tags")) {
|
|
21102
|
+
const tags = cleanVaultTags(checkStrings("tags", patch.tags));
|
|
21103
|
+
if (!jsonEqual(tags, item.tags))
|
|
21104
|
+
changes.tags = tags;
|
|
21105
|
+
}
|
|
21106
|
+
if (has(patch, "folder")) {
|
|
21107
|
+
const folder = checkFolder(patch.folder);
|
|
21108
|
+
if (folder !== item.folder)
|
|
21109
|
+
changes.folder = folder;
|
|
21110
|
+
}
|
|
21111
|
+
if (has(patch, "favorite")) {
|
|
21112
|
+
const favorite = checkFavorite(patch.favorite);
|
|
21113
|
+
if (favorite !== item.favorite)
|
|
21114
|
+
changes.favorite = favorite;
|
|
21115
|
+
}
|
|
21116
|
+
if (Object.keys(changes).length > 0)
|
|
21117
|
+
changes.updated_at = now;
|
|
21118
|
+
return changes;
|
|
21119
|
+
}
|
|
21120
|
+
function resolveVaultItem(items, ref) {
|
|
21121
|
+
const r = ref.trim();
|
|
21122
|
+
const byId = items.find((i) => i.id === r);
|
|
21123
|
+
if (byId)
|
|
21124
|
+
return byId.id;
|
|
21125
|
+
const named = items.filter((i) => i.deleted_at == null && sameName(i.name, r)).map((i) => i.id).sort();
|
|
21126
|
+
if (named.length === 1)
|
|
21127
|
+
return named[0];
|
|
21128
|
+
if (named.length === 0)
|
|
21129
|
+
throw new VaultError("unknown_vault_item", `no vault item "${r}"`);
|
|
21130
|
+
throw new VaultError("ambiguous_vault_item", `"${r}" names ${named.length} vault items (${named.join(", ")}); use an id`);
|
|
21131
|
+
}
|
|
21132
|
+
function vaultItemField(item, name) {
|
|
21133
|
+
const field = item.fields.find((f) => sameName(f.name, name));
|
|
21134
|
+
if (field)
|
|
21135
|
+
return field.value;
|
|
21136
|
+
const hasFields = item.fields.length ? `it has: ${item.fields.map((f) => f.name).join(", ")}` : "it has none";
|
|
21137
|
+
throw new VaultError("unknown_vault_field", `vault item "${item.name}" has no field "${name.trim()}" (${hasFields})`);
|
|
21138
|
+
}
|
|
20778
21139
|
// src/swipe.ts
|
|
20779
21140
|
var TASK_SWIPE_ACTIONS = [
|
|
20780
21141
|
"complete",
|
|
@@ -21203,6 +21564,11 @@ export {
|
|
|
21203
21564
|
TOAST_TTL_MS,
|
|
21204
21565
|
TRASH_KINDS,
|
|
21205
21566
|
TRASH_RETENTION_MS,
|
|
21567
|
+
VAULT_FIELD_KINDS,
|
|
21568
|
+
VAULT_HISTORY_PER_FIELD,
|
|
21569
|
+
VAULT_ITEM_TYPES,
|
|
21570
|
+
VAULT_SECRET_KINDS,
|
|
21571
|
+
VaultError,
|
|
21206
21572
|
accentVars,
|
|
21207
21573
|
addDays,
|
|
21208
21574
|
adoptBooked,
|
|
@@ -21224,6 +21590,8 @@ export {
|
|
|
21224
21590
|
bundleFilename,
|
|
21225
21591
|
clampOffset,
|
|
21226
21592
|
classifySwipe,
|
|
21593
|
+
cleanVaultTags,
|
|
21594
|
+
cleanVaultUrls,
|
|
21227
21595
|
columnCards,
|
|
21228
21596
|
columnMoveManyWrites,
|
|
21229
21597
|
columnMoveWrites,
|
|
@@ -21322,6 +21690,7 @@ export {
|
|
|
21322
21690
|
moveSectionToProject,
|
|
21323
21691
|
moveTask,
|
|
21324
21692
|
moveTaskToProject,
|
|
21693
|
+
newVaultItemFields,
|
|
21325
21694
|
nextDue,
|
|
21326
21695
|
nextOccurrence,
|
|
21327
21696
|
nextOccurrenceCivil,
|
|
@@ -21374,6 +21743,7 @@ export {
|
|
|
21374
21743
|
resolveTaskSwipe,
|
|
21375
21744
|
resolveTheme,
|
|
21376
21745
|
resolveTimeZone,
|
|
21746
|
+
resolveVaultItem,
|
|
21377
21747
|
resources,
|
|
21378
21748
|
restoreEntity,
|
|
21379
21749
|
restoreFromTrash,
|
|
@@ -21401,6 +21771,7 @@ export {
|
|
|
21401
21771
|
strengthSeries,
|
|
21402
21772
|
stripTrailingSlash,
|
|
21403
21773
|
subtreeProgress,
|
|
21774
|
+
summarizeVaultItem,
|
|
21404
21775
|
sweepExpiredTrash,
|
|
21405
21776
|
taskComparator,
|
|
21406
21777
|
taskContextClosure,
|
|
@@ -21422,6 +21793,7 @@ export {
|
|
|
21422
21793
|
toReminder,
|
|
21423
21794
|
toSection,
|
|
21424
21795
|
toTask,
|
|
21796
|
+
toVaultItem,
|
|
21425
21797
|
toastReducer,
|
|
21426
21798
|
todayTasks,
|
|
21427
21799
|
todoistProjectName,
|
|
@@ -21434,6 +21806,8 @@ export {
|
|
|
21434
21806
|
upcomingTasks,
|
|
21435
21807
|
updateRecurringTask,
|
|
21436
21808
|
updateTask,
|
|
21809
|
+
vaultItemChanges,
|
|
21810
|
+
vaultItemField,
|
|
21437
21811
|
visibleSections,
|
|
21438
21812
|
visibleTasks,
|
|
21439
21813
|
weekDays,
|
package/dist/vault.d.ts
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Atlas Vault item rules (`vault.item`, manifests/vault.json), at parity with
|
|
3
|
+
* `crates/atlas-client/src/vault.rs` through `test-vectors/vault_item_vectors.json`. Every field of
|
|
4
|
+
* an item is end-to-end encrypted under the vault app key; these rules decide what a client
|
|
5
|
+
* writes. An item's stored fields:
|
|
6
|
+
* - `type`: one of {@link VAULT_ITEM_TYPES}; `name`: non-empty text.
|
|
7
|
+
* - `fields`: `{name, kind, value}[]`, names unique case-insensitively, `kind` one of
|
|
8
|
+
* {@link VAULT_FIELD_KINDS}; `hidden` and `totp` values are secrets (a TOTP value is an
|
|
9
|
+
* `otpauth://` URI or a base32 seed, stored upper-case without spaces or padding).
|
|
10
|
+
* - `urls`, `tags`: string lists; `notes`: text; `folder`: a `vault.folder` id or null;
|
|
11
|
+
* `favorite`: a boolean.
|
|
12
|
+
* - `history`: earlier values of secret fields, `{field, value, changed_at}`, newest first, at most
|
|
13
|
+
* {@link VAULT_HISTORY_PER_FIELD} per field name.
|
|
14
|
+
* - `created_at`, `updated_at`: unix ms; `deleted_at`: unix ms once in the trash, else null.
|
|
15
|
+
*/
|
|
16
|
+
export declare const VAULT_ITEM_TYPES: readonly ["login", "note", "card", "identity", "ssh_key", "api_key"];
|
|
17
|
+
export declare const VAULT_FIELD_KINDS: readonly ["text", "hidden", "totp", "email", "url", "phone"];
|
|
18
|
+
/** Secret field kinds; a kind this client does not know counts as secret too. */
|
|
19
|
+
export declare const VAULT_SECRET_KINDS: readonly ["hidden", "totp"];
|
|
20
|
+
export declare const VAULT_HISTORY_PER_FIELD = 5;
|
|
21
|
+
export type VaultItemType = (typeof VAULT_ITEM_TYPES)[number];
|
|
22
|
+
export interface VaultField {
|
|
23
|
+
name: string;
|
|
24
|
+
kind: string;
|
|
25
|
+
value: string;
|
|
26
|
+
}
|
|
27
|
+
export interface VaultHistoryEntry {
|
|
28
|
+
field: string;
|
|
29
|
+
value: string;
|
|
30
|
+
changed_at: number;
|
|
31
|
+
}
|
|
32
|
+
/** A vault item as integrations see it, secrets included. */
|
|
33
|
+
export interface VaultItem {
|
|
34
|
+
id: string;
|
|
35
|
+
type: string;
|
|
36
|
+
name: string;
|
|
37
|
+
fields: VaultField[];
|
|
38
|
+
urls: string[];
|
|
39
|
+
notes: string;
|
|
40
|
+
tags: string[];
|
|
41
|
+
folder: string | null;
|
|
42
|
+
favorite: boolean;
|
|
43
|
+
history: VaultHistoryEntry[];
|
|
44
|
+
created_at: number;
|
|
45
|
+
updated_at: number;
|
|
46
|
+
deleted_at: number | null;
|
|
47
|
+
}
|
|
48
|
+
/** An item in a listing: everything but the field values, notes and history. */
|
|
49
|
+
export interface VaultItemSummary {
|
|
50
|
+
id: string;
|
|
51
|
+
type: string;
|
|
52
|
+
name: string;
|
|
53
|
+
fields: {
|
|
54
|
+
name: string;
|
|
55
|
+
kind: string;
|
|
56
|
+
}[];
|
|
57
|
+
urls: string[];
|
|
58
|
+
tags: string[];
|
|
59
|
+
folder: string | null;
|
|
60
|
+
favorite: boolean;
|
|
61
|
+
created_at: number;
|
|
62
|
+
updated_at: number;
|
|
63
|
+
deleted_at: number | null;
|
|
64
|
+
}
|
|
65
|
+
/** A field as given: kind `text` when left out. */
|
|
66
|
+
export interface VaultFieldInput {
|
|
67
|
+
name: string;
|
|
68
|
+
kind?: string;
|
|
69
|
+
value: string;
|
|
70
|
+
}
|
|
71
|
+
/** A new item. `folder` is a folder id (a client resolves names first). */
|
|
72
|
+
export interface VaultItemInput {
|
|
73
|
+
type?: string;
|
|
74
|
+
name: string;
|
|
75
|
+
fields?: VaultFieldInput[];
|
|
76
|
+
urls?: string[];
|
|
77
|
+
notes?: string;
|
|
78
|
+
tags?: string[];
|
|
79
|
+
folder?: string | null;
|
|
80
|
+
favorite?: boolean;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* A change: `fields` replaces the list; `set_fields` sets one field's value (and kind, when
|
|
84
|
+
* given) or appends it, `remove_fields` removes fields by name; `folder: null` clears it.
|
|
85
|
+
*/
|
|
86
|
+
export interface VaultItemPatch extends Partial<Omit<VaultItemInput, "fields">> {
|
|
87
|
+
fields?: VaultFieldInput[];
|
|
88
|
+
set_fields?: VaultFieldInput[];
|
|
89
|
+
remove_fields?: string[];
|
|
90
|
+
}
|
|
91
|
+
/** A rule refused the input; `code` is stable (`bad_input`, `unknown_vault_item`, …). */
|
|
92
|
+
export declare class VaultError extends Error {
|
|
93
|
+
readonly code: string;
|
|
94
|
+
constructor(code: string, message: string);
|
|
95
|
+
}
|
|
96
|
+
/** Tags trimmed, without empty ones or repeats (case-insensitive), in order. */
|
|
97
|
+
export declare function cleanVaultTags(tags: readonly string[]): string[];
|
|
98
|
+
/** URLs trimmed, without empty ones or exact repeats, in order. */
|
|
99
|
+
export declare function cleanVaultUrls(urls: readonly string[]): string[];
|
|
100
|
+
/**
|
|
101
|
+
* Read an item's stored fields, tolerating values of the wrong shape (another client's): an
|
|
102
|
+
* unknown type or field kind is kept, anything malformed is dropped or defaulted.
|
|
103
|
+
*/
|
|
104
|
+
export declare function toVaultItem(id: string, f: Record<string, unknown>): VaultItem;
|
|
105
|
+
/** The listing form of an item: no field values, notes or history. */
|
|
106
|
+
export declare function summarizeVaultItem(item: VaultItem): VaultItemSummary;
|
|
107
|
+
/** The stored fields of a new item from `input` at `now`; throws {@link VaultError}. */
|
|
108
|
+
export declare function newVaultItemFields(input: unknown, now: number): Record<string, unknown>;
|
|
109
|
+
/**
|
|
110
|
+
* The fields a patch writes to the item stored as `current` at `now` (empty when nothing changes;
|
|
111
|
+
* `updated_at` with any change); throws {@link VaultError}.
|
|
112
|
+
*/
|
|
113
|
+
export declare function vaultItemChanges(current: Record<string, unknown>, patch: unknown, now: number): Record<string, unknown>;
|
|
114
|
+
/**
|
|
115
|
+
* The id `ref` names among `items`: an id, trashed or not, or a name (case-insensitive, trimmed)
|
|
116
|
+
* of exactly one item outside the trash; throws `unknown_vault_item` or `ambiguous_vault_item`.
|
|
117
|
+
*/
|
|
118
|
+
export declare function resolveVaultItem(items: readonly {
|
|
119
|
+
id: string;
|
|
120
|
+
name: string;
|
|
121
|
+
deleted_at: number | null;
|
|
122
|
+
}[], ref: string): string;
|
|
123
|
+
/** The value of the item's field named `name` (case-insensitive), exactly as stored. */
|
|
124
|
+
export declare function vaultItemField(item: Pick<VaultItem, "name" | "fields">, name: string): string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@atlastodo/shared",
|
|
3
|
-
"version": "0.1.11-rc.
|
|
3
|
+
"version": "0.1.11-rc.8",
|
|
4
4
|
"description": "Atlas shared logic: the portable task rules, recurrence, quick add, filters, i18n and theme used by every Atlas client",
|
|
5
5
|
"license": "AGPL-3.0-only",
|
|
6
6
|
"homepage": "https://github.com/atlastodo/atlas/tree/main/packages/shared",
|
|
@@ -36,7 +36,8 @@
|
|
|
36
36
|
"filter_vectors.json",
|
|
37
37
|
"quick_add_vectors.json",
|
|
38
38
|
"recurrence_vectors.json",
|
|
39
|
-
"task_rules_vectors.json"
|
|
39
|
+
"task_rules_vectors.json",
|
|
40
|
+
"vault_item_vectors.json"
|
|
40
41
|
]
|
|
41
42
|
},
|
|
42
43
|
"scripts": {
|
|
@@ -45,7 +46,7 @@
|
|
|
45
46
|
"test": "vitest run"
|
|
46
47
|
},
|
|
47
48
|
"dependencies": {
|
|
48
|
-
"@atlastodo/client-core": "0.1.11-rc.
|
|
49
|
+
"@atlastodo/client-core": "0.1.11-rc.8",
|
|
49
50
|
"@noble/hashes": "^2.4.0",
|
|
50
51
|
"fflate": "^0.8.3"
|
|
51
52
|
},
|
|
@@ -0,0 +1,563 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_doc": "The Atlas Vault item rules (packages/shared/src/vault.ts, crates/atlas-client/src/vault.rs), asserted by both. Every value is a field of a `vault.item` entity, end-to-end encrypted (manifests/vault.json keeps none plaintext). `create`: `input` (the keys type, name, fields, urls, notes, tags, folder, favorite) at `now` gives the stored `fields`, or fails with `error` {code, message}. `update`: a stored `item` and a `patch` (the create keys, `folder: null` clears; or `set_fields`/`remove_fields` instead of `fields`) at `now` give the `changes` written (field → value; none when nothing changed), or an `error`. A changed or removed secret field (kind hidden or totp) leaves its old value in `history`, newest first, at most 5 per field name. `read`: raw stored `fields` (another client's, possibly malformed) read as `item` and listed as `summary` (no field values). `resolve`: a reference (id, or name case-insensitively among items outside the trash) among `items` gives an `id` or an `error`. `field`: one field's value by name, case-insensitively.",
|
|
3
|
+
"now": 1791972000000,
|
|
4
|
+
"create": [
|
|
5
|
+
{
|
|
6
|
+
"name": "a login with a username, password and TOTP seed",
|
|
7
|
+
"input": {
|
|
8
|
+
"type": "login",
|
|
9
|
+
"name": " GitHub ",
|
|
10
|
+
"fields": [
|
|
11
|
+
{ "name": "username", "value": "octo" },
|
|
12
|
+
{ "name": "password", "kind": "hidden", "value": " pa ss " },
|
|
13
|
+
{ "name": "totp", "kind": "totp", "value": "jbsw y3dp ehpk 3pxp==" }
|
|
14
|
+
],
|
|
15
|
+
"urls": [" https://github.com ", "", "https://github.com", "https://github.com/login"],
|
|
16
|
+
"tags": ["work", " Work", "dev"],
|
|
17
|
+
"favorite": true
|
|
18
|
+
},
|
|
19
|
+
"fields": {
|
|
20
|
+
"type": "login",
|
|
21
|
+
"name": "GitHub",
|
|
22
|
+
"fields": [
|
|
23
|
+
{ "name": "username", "kind": "text", "value": "octo" },
|
|
24
|
+
{ "name": "password", "kind": "hidden", "value": " pa ss " },
|
|
25
|
+
{ "name": "totp", "kind": "totp", "value": "JBSWY3DPEHPK3PXP" }
|
|
26
|
+
],
|
|
27
|
+
"urls": ["https://github.com", "https://github.com/login"],
|
|
28
|
+
"notes": "",
|
|
29
|
+
"tags": ["work", "dev"],
|
|
30
|
+
"folder": null,
|
|
31
|
+
"favorite": true,
|
|
32
|
+
"history": [],
|
|
33
|
+
"created_at": 1791972000000,
|
|
34
|
+
"updated_at": 1791972000000
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
"name": "the defaults: a login with nothing but a name",
|
|
39
|
+
"input": { "name": "Router" },
|
|
40
|
+
"fields": {
|
|
41
|
+
"type": "login",
|
|
42
|
+
"name": "Router",
|
|
43
|
+
"fields": [],
|
|
44
|
+
"urls": [],
|
|
45
|
+
"notes": "",
|
|
46
|
+
"tags": [],
|
|
47
|
+
"folder": null,
|
|
48
|
+
"favorite": false,
|
|
49
|
+
"history": [],
|
|
50
|
+
"created_at": 1791972000000,
|
|
51
|
+
"updated_at": 1791972000000
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
"name": "an API key in a folder, with an otpauth URI kept as given",
|
|
56
|
+
"input": {
|
|
57
|
+
"type": "api_key",
|
|
58
|
+
"name": "Deploy key",
|
|
59
|
+
"fields": [
|
|
60
|
+
{ "name": "key", "kind": "hidden", "value": "sk_live_123" },
|
|
61
|
+
{
|
|
62
|
+
"name": "2fa",
|
|
63
|
+
"kind": "totp",
|
|
64
|
+
"value": " otpauth://totp/Atlas:me?secret=JBSWY3DPEHPK3PXP&issuer=Atlas "
|
|
65
|
+
}
|
|
66
|
+
],
|
|
67
|
+
"notes": "Rotate yearly.\n",
|
|
68
|
+
"folder": "0192f000-0000-7000-8000-00000000f001"
|
|
69
|
+
},
|
|
70
|
+
"fields": {
|
|
71
|
+
"type": "api_key",
|
|
72
|
+
"name": "Deploy key",
|
|
73
|
+
"fields": [
|
|
74
|
+
{ "name": "key", "kind": "hidden", "value": "sk_live_123" },
|
|
75
|
+
{
|
|
76
|
+
"name": "2fa",
|
|
77
|
+
"kind": "totp",
|
|
78
|
+
"value": "otpauth://totp/Atlas:me?secret=JBSWY3DPEHPK3PXP&issuer=Atlas"
|
|
79
|
+
}
|
|
80
|
+
],
|
|
81
|
+
"urls": [],
|
|
82
|
+
"notes": "Rotate yearly.\n",
|
|
83
|
+
"tags": [],
|
|
84
|
+
"folder": "0192f000-0000-7000-8000-00000000f001",
|
|
85
|
+
"favorite": false,
|
|
86
|
+
"history": [],
|
|
87
|
+
"created_at": 1791972000000,
|
|
88
|
+
"updated_at": 1791972000000
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
"name": "every type is accepted",
|
|
93
|
+
"input": { "type": "ssh_key", "name": "laptop", "fields": [{ "name": "private key", "kind": "hidden", "value": "-----BEGIN" }] },
|
|
94
|
+
"fields": {
|
|
95
|
+
"type": "ssh_key",
|
|
96
|
+
"name": "laptop",
|
|
97
|
+
"fields": [{ "name": "private key", "kind": "hidden", "value": "-----BEGIN" }],
|
|
98
|
+
"urls": [],
|
|
99
|
+
"notes": "",
|
|
100
|
+
"tags": [],
|
|
101
|
+
"folder": null,
|
|
102
|
+
"favorite": false,
|
|
103
|
+
"history": [],
|
|
104
|
+
"created_at": 1791972000000,
|
|
105
|
+
"updated_at": 1791972000000
|
|
106
|
+
}
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
"name": "a name is required",
|
|
110
|
+
"input": { "type": "note", "name": " ", "notes": "x" },
|
|
111
|
+
"error": { "code": "bad_input", "message": "a vault item needs a name" }
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
"name": "an unknown type",
|
|
115
|
+
"input": { "type": "wifi", "name": "Home" },
|
|
116
|
+
"error": {
|
|
117
|
+
"code": "bad_input",
|
|
118
|
+
"message": "type must be one of login, note, card, identity, ssh_key, api_key"
|
|
119
|
+
}
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
"name": "an unknown key, such as a typo",
|
|
123
|
+
"input": { "name": "x", "url": "https://example.com" },
|
|
124
|
+
"error": { "code": "bad_input", "message": "url is not a vault item field" }
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
"name": "two fields with one name, case-insensitively",
|
|
128
|
+
"input": {
|
|
129
|
+
"name": "x",
|
|
130
|
+
"fields": [
|
|
131
|
+
{ "name": "Password", "kind": "hidden", "value": "a" },
|
|
132
|
+
{ "name": "password ", "kind": "hidden", "value": "b" }
|
|
133
|
+
]
|
|
134
|
+
},
|
|
135
|
+
"error": { "code": "bad_input", "message": "two fields are named \"password\"" }
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
"name": "a field without a name",
|
|
139
|
+
"input": { "name": "x", "fields": [{ "name": " ", "value": "a" }] },
|
|
140
|
+
"error": { "code": "bad_input", "message": "a field needs a name" }
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
"name": "an unknown field kind",
|
|
144
|
+
"input": { "name": "x", "fields": [{ "name": "pin", "kind": "secret", "value": "1234" }] },
|
|
145
|
+
"error": {
|
|
146
|
+
"code": "bad_input",
|
|
147
|
+
"message": "field kind must be one of text, hidden, totp, email, url, phone"
|
|
148
|
+
}
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
"name": "a field value that is not a string",
|
|
152
|
+
"input": { "name": "x", "fields": [{ "name": "pin", "kind": "hidden", "value": 1234 }] },
|
|
153
|
+
"error": {
|
|
154
|
+
"code": "bad_input",
|
|
155
|
+
"message": "fields must be a list of {name, kind, value} objects with string values"
|
|
156
|
+
}
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
"name": "a TOTP seed that is not base32",
|
|
160
|
+
"input": { "name": "x", "fields": [{ "name": "totp", "kind": "totp", "value": "not base32!" }] },
|
|
161
|
+
"error": {
|
|
162
|
+
"code": "bad_input",
|
|
163
|
+
"message": "the totp field \"totp\" needs an otpauth:// URI or a base32 secret"
|
|
164
|
+
}
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
"name": "a TOTP seed with a letter outside base32 (only ASCII is upper-cased)",
|
|
168
|
+
"input": { "name": "x", "fields": [{ "name": "totp", "kind": "totp", "value": "straße" }] },
|
|
169
|
+
"error": {
|
|
170
|
+
"code": "bad_input",
|
|
171
|
+
"message": "the totp field \"totp\" needs an otpauth:// URI or a base32 secret"
|
|
172
|
+
}
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
"name": "urls must be strings",
|
|
176
|
+
"input": { "name": "x", "urls": ["https://a.example", 7] },
|
|
177
|
+
"error": { "code": "bad_input", "message": "urls must be a list of strings" }
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
"name": "favorite must be a boolean",
|
|
181
|
+
"input": { "name": "x", "favorite": "yes" },
|
|
182
|
+
"error": { "code": "bad_input", "message": "favorite must be true or false" }
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
"name": "folder must be an id or null",
|
|
186
|
+
"input": { "name": "x", "folder": 3 },
|
|
187
|
+
"error": { "code": "bad_input", "message": "folder must be a folder id or null" }
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
"name": "set_fields is for updates",
|
|
191
|
+
"input": { "name": "x", "set_fields": [] },
|
|
192
|
+
"error": { "code": "bad_input", "message": "set_fields is not a vault item field" }
|
|
193
|
+
}
|
|
194
|
+
],
|
|
195
|
+
"update": [
|
|
196
|
+
{
|
|
197
|
+
"name": "a new password keeps the old one in the history",
|
|
198
|
+
"item": {
|
|
199
|
+
"type": "login",
|
|
200
|
+
"name": "GitHub",
|
|
201
|
+
"fields": [
|
|
202
|
+
{ "name": "username", "kind": "text", "value": "octo" },
|
|
203
|
+
{ "name": "password", "kind": "hidden", "value": "old" }
|
|
204
|
+
],
|
|
205
|
+
"urls": [],
|
|
206
|
+
"notes": "",
|
|
207
|
+
"tags": [],
|
|
208
|
+
"folder": null,
|
|
209
|
+
"favorite": false,
|
|
210
|
+
"history": [],
|
|
211
|
+
"created_at": 1791000000000,
|
|
212
|
+
"updated_at": 1791000000000
|
|
213
|
+
},
|
|
214
|
+
"patch": { "set_fields": [{ "name": "Password", "value": "new" }] },
|
|
215
|
+
"changes": {
|
|
216
|
+
"fields": [
|
|
217
|
+
{ "name": "username", "kind": "text", "value": "octo" },
|
|
218
|
+
{ "name": "password", "kind": "hidden", "value": "new" }
|
|
219
|
+
],
|
|
220
|
+
"history": [{ "field": "password", "value": "old", "changed_at": 1791972000000 }],
|
|
221
|
+
"updated_at": 1791972000000
|
|
222
|
+
}
|
|
223
|
+
},
|
|
224
|
+
{
|
|
225
|
+
"name": "a text field changes without history; a new field is appended as text",
|
|
226
|
+
"item": {
|
|
227
|
+
"type": "login",
|
|
228
|
+
"name": "GitHub",
|
|
229
|
+
"fields": [{ "name": "username", "kind": "text", "value": "octo" }],
|
|
230
|
+
"history": [],
|
|
231
|
+
"created_at": 1791000000000,
|
|
232
|
+
"updated_at": 1791000000000
|
|
233
|
+
},
|
|
234
|
+
"patch": {
|
|
235
|
+
"set_fields": [
|
|
236
|
+
{ "name": "username", "value": "cat" },
|
|
237
|
+
{ "name": "recovery email", "kind": "email", "value": "me@example.com" },
|
|
238
|
+
{ "name": "note" , "value": "" }
|
|
239
|
+
]
|
|
240
|
+
},
|
|
241
|
+
"changes": {
|
|
242
|
+
"fields": [
|
|
243
|
+
{ "name": "username", "kind": "text", "value": "cat" },
|
|
244
|
+
{ "name": "recovery email", "kind": "email", "value": "me@example.com" },
|
|
245
|
+
{ "name": "note", "kind": "text", "value": "" }
|
|
246
|
+
],
|
|
247
|
+
"updated_at": 1791972000000
|
|
248
|
+
}
|
|
249
|
+
},
|
|
250
|
+
{
|
|
251
|
+
"name": "removing a secret field keeps it in the history, capped at 5 per field, newest first",
|
|
252
|
+
"item": {
|
|
253
|
+
"type": "api_key",
|
|
254
|
+
"name": "Deploy",
|
|
255
|
+
"fields": [
|
|
256
|
+
{ "name": "key", "kind": "hidden", "value": "k6" },
|
|
257
|
+
{ "name": "totp", "kind": "totp", "value": "JBSWY3DPEHPK3PXP" }
|
|
258
|
+
],
|
|
259
|
+
"history": [
|
|
260
|
+
{ "field": "key", "value": "k5", "changed_at": 1791500000005 },
|
|
261
|
+
{ "field": "totp", "value": "OLDSEED", "changed_at": 1791500000004 },
|
|
262
|
+
{ "field": "Key", "value": "k4", "changed_at": 1791500000004 },
|
|
263
|
+
{ "field": "key", "value": "k3", "changed_at": 1791500000003 },
|
|
264
|
+
{ "field": "key", "value": "k2", "changed_at": 1791500000002 },
|
|
265
|
+
{ "field": "key", "value": "k1", "changed_at": 1791500000001 }
|
|
266
|
+
],
|
|
267
|
+
"created_at": 1791000000000,
|
|
268
|
+
"updated_at": 1791000000000
|
|
269
|
+
},
|
|
270
|
+
"patch": { "remove_fields": ["KEY"] },
|
|
271
|
+
"changes": {
|
|
272
|
+
"fields": [{ "name": "totp", "kind": "totp", "value": "JBSWY3DPEHPK3PXP" }],
|
|
273
|
+
"history": [
|
|
274
|
+
{ "field": "key", "value": "k6", "changed_at": 1791972000000 },
|
|
275
|
+
{ "field": "key", "value": "k5", "changed_at": 1791500000005 },
|
|
276
|
+
{ "field": "totp", "value": "OLDSEED", "changed_at": 1791500000004 },
|
|
277
|
+
{ "field": "Key", "value": "k4", "changed_at": 1791500000004 },
|
|
278
|
+
{ "field": "key", "value": "k3", "changed_at": 1791500000003 },
|
|
279
|
+
{ "field": "key", "value": "k2", "changed_at": 1791500000002 }
|
|
280
|
+
],
|
|
281
|
+
"updated_at": 1791972000000
|
|
282
|
+
}
|
|
283
|
+
},
|
|
284
|
+
{
|
|
285
|
+
"name": "replacing the whole list records every secret that changed or went",
|
|
286
|
+
"item": {
|
|
287
|
+
"type": "card",
|
|
288
|
+
"name": "Visa",
|
|
289
|
+
"fields": [
|
|
290
|
+
{ "name": "number", "kind": "hidden", "value": "4111" },
|
|
291
|
+
{ "name": "cvv", "kind": "hidden", "value": "123" },
|
|
292
|
+
{ "name": "holder", "kind": "text", "value": "A" }
|
|
293
|
+
],
|
|
294
|
+
"history": [],
|
|
295
|
+
"created_at": 1791000000000,
|
|
296
|
+
"updated_at": 1791000000000
|
|
297
|
+
},
|
|
298
|
+
"patch": {
|
|
299
|
+
"name": " Visa debit ",
|
|
300
|
+
"fields": [
|
|
301
|
+
{ "name": "Number", "kind": "hidden", "value": "4111" },
|
|
302
|
+
{ "name": "holder", "value": "B" }
|
|
303
|
+
],
|
|
304
|
+
"tags": ["Bank"],
|
|
305
|
+
"favorite": true,
|
|
306
|
+
"folder": "0192f000-0000-7000-8000-00000000f001"
|
|
307
|
+
},
|
|
308
|
+
"changes": {
|
|
309
|
+
"name": "Visa debit",
|
|
310
|
+
"fields": [
|
|
311
|
+
{ "name": "Number", "kind": "hidden", "value": "4111" },
|
|
312
|
+
{ "name": "holder", "kind": "text", "value": "B" }
|
|
313
|
+
],
|
|
314
|
+
"tags": ["Bank"],
|
|
315
|
+
"folder": "0192f000-0000-7000-8000-00000000f001",
|
|
316
|
+
"favorite": true,
|
|
317
|
+
"history": [{ "field": "cvv", "value": "123", "changed_at": 1791972000000 }],
|
|
318
|
+
"updated_at": 1791972000000
|
|
319
|
+
}
|
|
320
|
+
},
|
|
321
|
+
{
|
|
322
|
+
"name": "unchanged values write nothing",
|
|
323
|
+
"item": {
|
|
324
|
+
"type": "note",
|
|
325
|
+
"name": "Wi-Fi",
|
|
326
|
+
"notes": "pw on the router",
|
|
327
|
+
"folder": "0192f000-0000-7000-8000-00000000f001",
|
|
328
|
+
"tags": ["home"],
|
|
329
|
+
"created_at": 1791000000000,
|
|
330
|
+
"updated_at": 1791000000000
|
|
331
|
+
},
|
|
332
|
+
"patch": {
|
|
333
|
+
"type": "note",
|
|
334
|
+
"name": "Wi-Fi ",
|
|
335
|
+
"notes": "pw on the router",
|
|
336
|
+
"tags": ["home", "HOME"],
|
|
337
|
+
"folder": "0192f000-0000-7000-8000-00000000f001",
|
|
338
|
+
"fields": []
|
|
339
|
+
},
|
|
340
|
+
"changes": {}
|
|
341
|
+
},
|
|
342
|
+
{
|
|
343
|
+
"name": "folder null clears it; type and notes change",
|
|
344
|
+
"item": {
|
|
345
|
+
"type": "note",
|
|
346
|
+
"name": "Wi-Fi",
|
|
347
|
+
"notes": "old",
|
|
348
|
+
"folder": "0192f000-0000-7000-8000-00000000f001",
|
|
349
|
+
"created_at": 1791000000000,
|
|
350
|
+
"updated_at": 1791000000000
|
|
351
|
+
},
|
|
352
|
+
"patch": { "type": "login", "folder": null, "notes": "" },
|
|
353
|
+
"changes": { "type": "login", "notes": "", "folder": null, "updated_at": 1791972000000 }
|
|
354
|
+
},
|
|
355
|
+
{
|
|
356
|
+
"name": "removing a field that is not there",
|
|
357
|
+
"item": {
|
|
358
|
+
"type": "login",
|
|
359
|
+
"name": "x",
|
|
360
|
+
"fields": [],
|
|
361
|
+
"created_at": 1791000000000,
|
|
362
|
+
"updated_at": 1791000000000
|
|
363
|
+
},
|
|
364
|
+
"patch": { "remove_fields": ["password"] },
|
|
365
|
+
"error": { "code": "bad_input", "message": "no field \"password\" to remove" }
|
|
366
|
+
},
|
|
367
|
+
{
|
|
368
|
+
"name": "fields and set_fields together",
|
|
369
|
+
"item": { "type": "login", "name": "x", "created_at": 1791000000000, "updated_at": 1791000000000 },
|
|
370
|
+
"patch": { "fields": [], "set_fields": [{ "name": "a", "value": "b" }] },
|
|
371
|
+
"error": {
|
|
372
|
+
"code": "bad_input",
|
|
373
|
+
"message": "give fields (the whole list) or set_fields and remove_fields, not both"
|
|
374
|
+
}
|
|
375
|
+
},
|
|
376
|
+
{
|
|
377
|
+
"name": "a patch may not empty the name",
|
|
378
|
+
"item": { "type": "login", "name": "x", "created_at": 1791000000000, "updated_at": 1791000000000 },
|
|
379
|
+
"patch": { "name": "" },
|
|
380
|
+
"error": { "code": "bad_input", "message": "a vault item needs a name" }
|
|
381
|
+
}
|
|
382
|
+
],
|
|
383
|
+
"read": [
|
|
384
|
+
{
|
|
385
|
+
"name": "a stored item reads as it was written, and its summary leaves the values out",
|
|
386
|
+
"fields": {
|
|
387
|
+
"type": "login",
|
|
388
|
+
"name": "GitHub",
|
|
389
|
+
"fields": [
|
|
390
|
+
{ "name": "username", "kind": "text", "value": "octo" },
|
|
391
|
+
{ "name": "password", "kind": "hidden", "value": "pw" }
|
|
392
|
+
],
|
|
393
|
+
"urls": ["https://github.com"],
|
|
394
|
+
"notes": "n",
|
|
395
|
+
"tags": ["dev"],
|
|
396
|
+
"folder": "0192f000-0000-7000-8000-00000000f001",
|
|
397
|
+
"favorite": true,
|
|
398
|
+
"history": [{ "field": "password", "value": "old", "changed_at": 5 }],
|
|
399
|
+
"created_at": 1,
|
|
400
|
+
"updated_at": 2
|
|
401
|
+
},
|
|
402
|
+
"item": {
|
|
403
|
+
"type": "login",
|
|
404
|
+
"name": "GitHub",
|
|
405
|
+
"fields": [
|
|
406
|
+
{ "name": "username", "kind": "text", "value": "octo" },
|
|
407
|
+
{ "name": "password", "kind": "hidden", "value": "pw" }
|
|
408
|
+
],
|
|
409
|
+
"urls": ["https://github.com"],
|
|
410
|
+
"notes": "n",
|
|
411
|
+
"tags": ["dev"],
|
|
412
|
+
"folder": "0192f000-0000-7000-8000-00000000f001",
|
|
413
|
+
"favorite": true,
|
|
414
|
+
"history": [{ "field": "password", "value": "old", "changed_at": 5 }],
|
|
415
|
+
"created_at": 1,
|
|
416
|
+
"updated_at": 2,
|
|
417
|
+
"deleted_at": null
|
|
418
|
+
},
|
|
419
|
+
"summary": {
|
|
420
|
+
"type": "login",
|
|
421
|
+
"name": "GitHub",
|
|
422
|
+
"fields": [
|
|
423
|
+
{ "name": "username", "kind": "text" },
|
|
424
|
+
{ "name": "password", "kind": "hidden" }
|
|
425
|
+
],
|
|
426
|
+
"urls": ["https://github.com"],
|
|
427
|
+
"tags": ["dev"],
|
|
428
|
+
"folder": "0192f000-0000-7000-8000-00000000f001",
|
|
429
|
+
"favorite": true,
|
|
430
|
+
"created_at": 1,
|
|
431
|
+
"updated_at": 2,
|
|
432
|
+
"deleted_at": null
|
|
433
|
+
}
|
|
434
|
+
},
|
|
435
|
+
{
|
|
436
|
+
"name": "malformed values read leniently: unknown kinds and types are kept, junk is dropped",
|
|
437
|
+
"fields": {
|
|
438
|
+
"type": 3,
|
|
439
|
+
"name": ["x"],
|
|
440
|
+
"fields": [
|
|
441
|
+
{ "name": "pin", "kind": "passkey", "value": "1" },
|
|
442
|
+
{ "name": "no value" },
|
|
443
|
+
"junk",
|
|
444
|
+
{ "name": "plain", "value": "v" }
|
|
445
|
+
],
|
|
446
|
+
"urls": ["https://a.example", 4],
|
|
447
|
+
"favorite": "yes",
|
|
448
|
+
"folder": 9,
|
|
449
|
+
"history": [{ "field": "pin", "value": "0", "changed_at": "x" }, { "value": "?" }],
|
|
450
|
+
"created_at": 7,
|
|
451
|
+
"deleted_at": 9
|
|
452
|
+
},
|
|
453
|
+
"item": {
|
|
454
|
+
"type": "login",
|
|
455
|
+
"name": "",
|
|
456
|
+
"fields": [
|
|
457
|
+
{ "name": "pin", "kind": "passkey", "value": "1" },
|
|
458
|
+
{ "name": "plain", "kind": "text", "value": "v" }
|
|
459
|
+
],
|
|
460
|
+
"urls": ["https://a.example"],
|
|
461
|
+
"notes": "",
|
|
462
|
+
"tags": [],
|
|
463
|
+
"folder": null,
|
|
464
|
+
"favorite": false,
|
|
465
|
+
"history": [{ "field": "pin", "value": "0", "changed_at": 0 }],
|
|
466
|
+
"created_at": 7,
|
|
467
|
+
"updated_at": 0,
|
|
468
|
+
"deleted_at": 9
|
|
469
|
+
},
|
|
470
|
+
"summary": {
|
|
471
|
+
"type": "login",
|
|
472
|
+
"name": "",
|
|
473
|
+
"fields": [
|
|
474
|
+
{ "name": "pin", "kind": "passkey" },
|
|
475
|
+
{ "name": "plain", "kind": "text" }
|
|
476
|
+
],
|
|
477
|
+
"urls": ["https://a.example"],
|
|
478
|
+
"tags": [],
|
|
479
|
+
"folder": null,
|
|
480
|
+
"favorite": false,
|
|
481
|
+
"created_at": 7,
|
|
482
|
+
"updated_at": 0,
|
|
483
|
+
"deleted_at": 9
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
],
|
|
487
|
+
"resolve": [
|
|
488
|
+
{
|
|
489
|
+
"name": "by id, even in the trash",
|
|
490
|
+
"items": [
|
|
491
|
+
{ "id": "0192f000-0000-7000-8000-000000000001", "name": "GitHub", "deleted_at": 5 },
|
|
492
|
+
{ "id": "0192f000-0000-7000-8000-000000000002", "name": "AWS", "deleted_at": null }
|
|
493
|
+
],
|
|
494
|
+
"ref": " 0192f000-0000-7000-8000-000000000001",
|
|
495
|
+
"id": "0192f000-0000-7000-8000-000000000001"
|
|
496
|
+
},
|
|
497
|
+
{
|
|
498
|
+
"name": "by name, case-insensitively, outside the trash",
|
|
499
|
+
"items": [
|
|
500
|
+
{ "id": "0192f000-0000-7000-8000-000000000001", "name": "GitHub", "deleted_at": 5 },
|
|
501
|
+
{ "id": "0192f000-0000-7000-8000-000000000003", "name": "github ", "deleted_at": null }
|
|
502
|
+
],
|
|
503
|
+
"ref": "GITHUB",
|
|
504
|
+
"id": "0192f000-0000-7000-8000-000000000003"
|
|
505
|
+
},
|
|
506
|
+
{
|
|
507
|
+
"name": "an unknown reference",
|
|
508
|
+
"items": [{ "id": "0192f000-0000-7000-8000-000000000001", "name": "GitHub", "deleted_at": 5 }],
|
|
509
|
+
"ref": "GitHub",
|
|
510
|
+
"error": { "code": "unknown_vault_item", "message": "no vault item \"GitHub\"" }
|
|
511
|
+
},
|
|
512
|
+
{
|
|
513
|
+
"name": "a name two items share",
|
|
514
|
+
"items": [
|
|
515
|
+
{ "id": "0192f000-0000-7000-8000-000000000009", "name": "AWS", "deleted_at": null },
|
|
516
|
+
{ "id": "0192f000-0000-7000-8000-000000000002", "name": "aws", "deleted_at": null }
|
|
517
|
+
],
|
|
518
|
+
"ref": "aws",
|
|
519
|
+
"error": {
|
|
520
|
+
"code": "ambiguous_vault_item",
|
|
521
|
+
"message": "\"aws\" names 2 vault items (0192f000-0000-7000-8000-000000000002, 0192f000-0000-7000-8000-000000000009); use an id"
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
],
|
|
525
|
+
"field": [
|
|
526
|
+
{
|
|
527
|
+
"name": "a field by name, case-insensitively, value as stored",
|
|
528
|
+
"item": {
|
|
529
|
+
"name": "GitHub",
|
|
530
|
+
"fields": [
|
|
531
|
+
{ "name": "username", "kind": "text", "value": "octo" },
|
|
532
|
+
{ "name": "Password", "kind": "hidden", "value": " pw " }
|
|
533
|
+
]
|
|
534
|
+
},
|
|
535
|
+
"field": "password",
|
|
536
|
+
"value": " pw "
|
|
537
|
+
},
|
|
538
|
+
{
|
|
539
|
+
"name": "a missing field names the ones there are",
|
|
540
|
+
"item": {
|
|
541
|
+
"name": "GitHub",
|
|
542
|
+
"fields": [
|
|
543
|
+
{ "name": "username", "kind": "text", "value": "octo" },
|
|
544
|
+
{ "name": "Password", "kind": "hidden", "value": "pw" }
|
|
545
|
+
]
|
|
546
|
+
},
|
|
547
|
+
"field": "token",
|
|
548
|
+
"error": {
|
|
549
|
+
"code": "unknown_vault_field",
|
|
550
|
+
"message": "vault item \"GitHub\" has no field \"token\" (it has: username, Password)"
|
|
551
|
+
}
|
|
552
|
+
},
|
|
553
|
+
{
|
|
554
|
+
"name": "an item without fields",
|
|
555
|
+
"item": { "name": "Note" },
|
|
556
|
+
"field": "x",
|
|
557
|
+
"error": {
|
|
558
|
+
"code": "unknown_vault_field",
|
|
559
|
+
"message": "vault item \"Note\" has no field \"x\" (it has none)"
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
]
|
|
563
|
+
}
|