@atlastodo/shared 0.1.11-rc.7 → 0.1.11-rc.9

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 CHANGED
@@ -51,6 +51,7 @@ export * from "./stats.js";
51
51
  export * from "./pomodoro.js";
52
52
  export * from "./focus.js";
53
53
  export * from "./countdown.js";
54
+ export * from "./vault.js";
54
55
  export * from "./swipe.js";
55
56
  export * from "./hotkeys.js";
56
57
  export * from "./avatarColor.js";
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,
@@ -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.7",
3
+ "version": "0.1.11-rc.9",
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.7",
49
+ "@atlastodo/client-core": "0.1.11-rc.9",
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
+ }