@ai-matrx/vault 0.1.26 → 0.1.28
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/CHANGELOG.md +12 -0
- package/dist/index.cjs +183 -21
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +46 -2
- package/dist/index.d.ts +46 -2
- package/dist/index.js +183 -21
- package/dist/index.js.map +1 -1
- package/dist/io/bitwarden-json.worker.js.map +1 -1
- package/package.json +5 -5
package/dist/index.d.cts
CHANGED
|
@@ -1420,7 +1420,7 @@ interface ApiVaultPortOptions {
|
|
|
1420
1420
|
/** The host's `supabase.auth`: turns on approving a browser with a passkey. */
|
|
1421
1421
|
passkeys?: PasskeyAuthLike;
|
|
1422
1422
|
}
|
|
1423
|
-
declare function createApiVaultPort(
|
|
1423
|
+
declare function createApiVaultPort(hostApi: Api, options?: ApiVaultPortOptions): VaultPort;
|
|
1424
1424
|
|
|
1425
1425
|
/**
|
|
1426
1426
|
* Wrap any call that shows, copies or exports a secret. When the server answers "sign in again",
|
|
@@ -1471,4 +1471,48 @@ interface RowMenuEntry {
|
|
|
1471
1471
|
/** The entries of the row a right-click landed on (`[data-item]`), else none. */
|
|
1472
1472
|
declare function rowMenuEntriesAt(target: Element | null | undefined): readonly RowMenuEntry[];
|
|
1473
1473
|
|
|
1474
|
-
|
|
1474
|
+
/**
|
|
1475
|
+
* AN EMPTY CREDENTIAL IS A FACT; AN EMPTY READ OF A FULL ONE IS A FAILURE (DD-160, lane
|
|
1476
|
+
* ERRORS-HONEST 2026-09-24).
|
|
1477
|
+
*
|
|
1478
|
+
* RLS refuses a row by returning nothing, so a list that reads a credential's fields and gets `[]`
|
|
1479
|
+
* cannot tell "there is nothing in it" from "the database filtered the read" — from 2026-07 to
|
|
1480
|
+
* 2026-09-12 a restrictive policy hid every field from its own owner and the screen reported
|
|
1481
|
+
* success. The list used to guess from the whole scope ("no field and no file anywhere" = alarm),
|
|
1482
|
+
* which blanked an organization whose only credential was simply empty (Pinecrest Records'
|
|
1483
|
+
* Bandcamp label login).
|
|
1484
|
+
*
|
|
1485
|
+
* The guess is replaced by the database's own answer: `users.credential_item_holdings(ids)` counts
|
|
1486
|
+
* the live field and file rows of each credential the caller may read (counts only, never a value).
|
|
1487
|
+
* The alarm fires only for a credential that HOLDS something the read did not return.
|
|
1488
|
+
*/
|
|
1489
|
+
type HoldingRow = {
|
|
1490
|
+
credential_item_id: string;
|
|
1491
|
+
field_count: number;
|
|
1492
|
+
file_count: number;
|
|
1493
|
+
};
|
|
1494
|
+
type Holdings = {
|
|
1495
|
+
state: "answered";
|
|
1496
|
+
rows: HoldingRow[];
|
|
1497
|
+
}
|
|
1498
|
+
/** The door is not on this database yet — the migration below has not been applied. */
|
|
1499
|
+
| {
|
|
1500
|
+
state: "absent";
|
|
1501
|
+
why: string;
|
|
1502
|
+
} | {
|
|
1503
|
+
state: "unavailable";
|
|
1504
|
+
why: string;
|
|
1505
|
+
};
|
|
1506
|
+
declare const HOLDINGS_DOOR_MIGRATION = "migrations/campaign/errorshonest_s8_a_credential_says_how_much_it_holds.sql";
|
|
1507
|
+
/** The error codes that mean "no such function on this database", not "the read failed". */
|
|
1508
|
+
declare function holdingsDoorIsAbsent(code: string | undefined | null): boolean;
|
|
1509
|
+
/**
|
|
1510
|
+
* The alarm sentence, or null when every credential that read empty is genuinely empty.
|
|
1511
|
+
*
|
|
1512
|
+
* @param total how many credentials the list read
|
|
1513
|
+
* @param emptyIds the ones whose field AND file reads came back empty
|
|
1514
|
+
* @param holdings the database's answer for `emptyIds`
|
|
1515
|
+
*/
|
|
1516
|
+
declare function emptyReadAlarm(total: number, emptyIds: readonly string[], holdings: Holdings): string | null;
|
|
1517
|
+
|
|
1518
|
+
export { type ApiVaultPortOptions, CATEGORIES, CONFIRM_RETURN_KEY, type CategoryId, type Confirmed, type CopyOptions, DEFAULT_LOCATION, type DefinitionField, type FieldHandling, type FieldKind, GenerateButton, HIDDEN_ITEM_TAG, HOLDINGS_DOOR_MIGRATION, type HoldingRow, type Holdings, type IdentityAccount, IdentityGate, type IdentityMethod, type ItemDraft, LOCKED_MESSAGE, type NoticeKind, type Notify, type ReturnStore, type RowMenuEntry, type RowMenuEntryKind, type SaveResult, type SupabaseAuthLike, type SupabaseIdentityOptions, type TotpReading, type UriMatchMode, type UrlGuess, type VaultAgentScope, VaultApp, type VaultAppProps, type VaultAttachment, type VaultAuthenticator, type VaultClipboard, type VaultCustomField, type VaultDefinition, type VaultField, type VaultHostCapabilities, type VaultIdentity, type VaultInventory, type VaultItem, type VaultItemCapabilities, type VaultKnobs, type VaultLayout, type VaultLocation, type VaultLock, VaultLockedError, type VaultOrganizationRef, type VaultOwner, type VaultPort, type VaultSelection, type VaultSnapshot, type VaultSort, type VaultUserStateDoor, categoryOf, copyValue, createApiVaultPort, createSupabaseIdentity, createVaultKnobs, emptyReadAlarm, guardPort, holdingsDoorIsAbsent, inferFromUrl, isHiddenItem, itemIdAt, locationFromParams, locationToParams, loginSiteOf, matchesQuery, needsRecentSignIn, parseDefinitions, providerOf, rowMenuEntriesAt, siteDomainOf, sortItems, useConfirmed, vaultInventoryScope, vaultItemFacts, wasCancelled };
|
package/dist/index.d.ts
CHANGED
|
@@ -1420,7 +1420,7 @@ interface ApiVaultPortOptions {
|
|
|
1420
1420
|
/** The host's `supabase.auth`: turns on approving a browser with a passkey. */
|
|
1421
1421
|
passkeys?: PasskeyAuthLike;
|
|
1422
1422
|
}
|
|
1423
|
-
declare function createApiVaultPort(
|
|
1423
|
+
declare function createApiVaultPort(hostApi: Api, options?: ApiVaultPortOptions): VaultPort;
|
|
1424
1424
|
|
|
1425
1425
|
/**
|
|
1426
1426
|
* Wrap any call that shows, copies or exports a secret. When the server answers "sign in again",
|
|
@@ -1471,4 +1471,48 @@ interface RowMenuEntry {
|
|
|
1471
1471
|
/** The entries of the row a right-click landed on (`[data-item]`), else none. */
|
|
1472
1472
|
declare function rowMenuEntriesAt(target: Element | null | undefined): readonly RowMenuEntry[];
|
|
1473
1473
|
|
|
1474
|
-
|
|
1474
|
+
/**
|
|
1475
|
+
* AN EMPTY CREDENTIAL IS A FACT; AN EMPTY READ OF A FULL ONE IS A FAILURE (DD-160, lane
|
|
1476
|
+
* ERRORS-HONEST 2026-09-24).
|
|
1477
|
+
*
|
|
1478
|
+
* RLS refuses a row by returning nothing, so a list that reads a credential's fields and gets `[]`
|
|
1479
|
+
* cannot tell "there is nothing in it" from "the database filtered the read" — from 2026-07 to
|
|
1480
|
+
* 2026-09-12 a restrictive policy hid every field from its own owner and the screen reported
|
|
1481
|
+
* success. The list used to guess from the whole scope ("no field and no file anywhere" = alarm),
|
|
1482
|
+
* which blanked an organization whose only credential was simply empty (Pinecrest Records'
|
|
1483
|
+
* Bandcamp label login).
|
|
1484
|
+
*
|
|
1485
|
+
* The guess is replaced by the database's own answer: `users.credential_item_holdings(ids)` counts
|
|
1486
|
+
* the live field and file rows of each credential the caller may read (counts only, never a value).
|
|
1487
|
+
* The alarm fires only for a credential that HOLDS something the read did not return.
|
|
1488
|
+
*/
|
|
1489
|
+
type HoldingRow = {
|
|
1490
|
+
credential_item_id: string;
|
|
1491
|
+
field_count: number;
|
|
1492
|
+
file_count: number;
|
|
1493
|
+
};
|
|
1494
|
+
type Holdings = {
|
|
1495
|
+
state: "answered";
|
|
1496
|
+
rows: HoldingRow[];
|
|
1497
|
+
}
|
|
1498
|
+
/** The door is not on this database yet — the migration below has not been applied. */
|
|
1499
|
+
| {
|
|
1500
|
+
state: "absent";
|
|
1501
|
+
why: string;
|
|
1502
|
+
} | {
|
|
1503
|
+
state: "unavailable";
|
|
1504
|
+
why: string;
|
|
1505
|
+
};
|
|
1506
|
+
declare const HOLDINGS_DOOR_MIGRATION = "migrations/campaign/errorshonest_s8_a_credential_says_how_much_it_holds.sql";
|
|
1507
|
+
/** The error codes that mean "no such function on this database", not "the read failed". */
|
|
1508
|
+
declare function holdingsDoorIsAbsent(code: string | undefined | null): boolean;
|
|
1509
|
+
/**
|
|
1510
|
+
* The alarm sentence, or null when every credential that read empty is genuinely empty.
|
|
1511
|
+
*
|
|
1512
|
+
* @param total how many credentials the list read
|
|
1513
|
+
* @param emptyIds the ones whose field AND file reads came back empty
|
|
1514
|
+
* @param holdings the database's answer for `emptyIds`
|
|
1515
|
+
*/
|
|
1516
|
+
declare function emptyReadAlarm(total: number, emptyIds: readonly string[], holdings: Holdings): string | null;
|
|
1517
|
+
|
|
1518
|
+
export { type ApiVaultPortOptions, CATEGORIES, CONFIRM_RETURN_KEY, type CategoryId, type Confirmed, type CopyOptions, DEFAULT_LOCATION, type DefinitionField, type FieldHandling, type FieldKind, GenerateButton, HIDDEN_ITEM_TAG, HOLDINGS_DOOR_MIGRATION, type HoldingRow, type Holdings, type IdentityAccount, IdentityGate, type IdentityMethod, type ItemDraft, LOCKED_MESSAGE, type NoticeKind, type Notify, type ReturnStore, type RowMenuEntry, type RowMenuEntryKind, type SaveResult, type SupabaseAuthLike, type SupabaseIdentityOptions, type TotpReading, type UriMatchMode, type UrlGuess, type VaultAgentScope, VaultApp, type VaultAppProps, type VaultAttachment, type VaultAuthenticator, type VaultClipboard, type VaultCustomField, type VaultDefinition, type VaultField, type VaultHostCapabilities, type VaultIdentity, type VaultInventory, type VaultItem, type VaultItemCapabilities, type VaultKnobs, type VaultLayout, type VaultLocation, type VaultLock, VaultLockedError, type VaultOrganizationRef, type VaultOwner, type VaultPort, type VaultSelection, type VaultSnapshot, type VaultSort, type VaultUserStateDoor, categoryOf, copyValue, createApiVaultPort, createSupabaseIdentity, createVaultKnobs, emptyReadAlarm, guardPort, holdingsDoorIsAbsent, inferFromUrl, isHiddenItem, itemIdAt, locationFromParams, locationToParams, loginSiteOf, matchesQuery, needsRecentSignIn, parseDefinitions, providerOf, rowMenuEntriesAt, siteDomainOf, sortItems, useConfirmed, vaultInventoryScope, vaultItemFacts, wasCancelled };
|
package/dist/index.js
CHANGED
|
@@ -2637,9 +2637,12 @@ function normalizeLoginUrl(input) {
|
|
|
2637
2637
|
return null;
|
|
2638
2638
|
}
|
|
2639
2639
|
}
|
|
2640
|
+
function normalizeLoginUrls(inputs) {
|
|
2641
|
+
return inputs.map((u) => u.trim()).filter(Boolean).map((u) => normalizeLoginUrl(u) ?? u);
|
|
2642
|
+
}
|
|
2640
2643
|
function describeDestination(r) {
|
|
2641
2644
|
if (r.state === "reachable") return { url: r.normalizedUrl, state: r.state, tone: "success", message: "Website confirmed" };
|
|
2642
|
-
if (r.state === "invalid") return { url: r.normalizedUrl, state: r.state, tone: "
|
|
2645
|
+
if (r.state === "invalid") return { url: r.normalizedUrl, state: r.state, tone: "warning", message: "Not a public website. You can still save it." };
|
|
2643
2646
|
if (r.state === "not_found") return { url: r.normalizedUrl, state: r.state, tone: "warning", message: "Site answered, but this page was not found" };
|
|
2644
2647
|
return { url: r.normalizedUrl, state: r.state, tone: "warning", message: "Couldn\u2019t confirm it. You can still save it." };
|
|
2645
2648
|
}
|
|
@@ -5345,6 +5348,40 @@ function AssignResult({ sent, onAnother, onDone }) {
|
|
|
5345
5348
|
] });
|
|
5346
5349
|
}
|
|
5347
5350
|
|
|
5351
|
+
// src/lists/turn-back-on.ts
|
|
5352
|
+
async function turnBackOn(port, item, fields, options = {}) {
|
|
5353
|
+
const run = options.run ?? ((op) => op());
|
|
5354
|
+
const done = [];
|
|
5355
|
+
if (item.turnedOff) {
|
|
5356
|
+
await run(() => port.fields.updateItem(item, { active: true }));
|
|
5357
|
+
done.push("the item");
|
|
5358
|
+
}
|
|
5359
|
+
let n = 0;
|
|
5360
|
+
for (const f of fields) {
|
|
5361
|
+
if (f.active) continue;
|
|
5362
|
+
await run(() => port.fields.updateField(item, f.id, { active: true }));
|
|
5363
|
+
n += 1;
|
|
5364
|
+
}
|
|
5365
|
+
if (n) done.push(n === 1 ? "1 field" : `${n} fields`);
|
|
5366
|
+
if (fields.some((f) => f.key === OTP_SEED_KEY)) {
|
|
5367
|
+
const codes = await port.authenticator.get(item);
|
|
5368
|
+
if (codes && !codes.enabled) {
|
|
5369
|
+
await run(() => port.authenticator.setEnabled(item, true));
|
|
5370
|
+
done.push("one-time codes");
|
|
5371
|
+
}
|
|
5372
|
+
}
|
|
5373
|
+
if (options.browserFill && !item.browserFillEnabled && item.loginUrls.length > 0) {
|
|
5374
|
+
await run(() => port.fields.updateItem(item, { browserFillEnabled: true }));
|
|
5375
|
+
done.push("browser fill");
|
|
5376
|
+
}
|
|
5377
|
+
return done;
|
|
5378
|
+
}
|
|
5379
|
+
function turnedOnSentence(done) {
|
|
5380
|
+
if (done.length === 0) return "Nothing here was off.";
|
|
5381
|
+
const list = done.length === 1 ? done[0] : `${done.slice(0, -1).join(", ")} and ${done[done.length - 1]}`;
|
|
5382
|
+
return `Turned on ${list}.`;
|
|
5383
|
+
}
|
|
5384
|
+
|
|
5348
5385
|
// src/ItemPane.tsx
|
|
5349
5386
|
import { HistoryIcon, SettingsIcon, UsersIcon as UsersIcon2 } from "@ai-matrx/design-system/icons";
|
|
5350
5387
|
import { dropKeptDraft, keepDraft, readKeptDraft } from "@ai-matrx/kit/drafts";
|
|
@@ -5958,7 +5995,8 @@ function ItemPane(props) {
|
|
|
5958
5995
|
const [showBrowser, setShowBrowser] = useState27(false);
|
|
5959
5996
|
const [error, setError] = useState27(null);
|
|
5960
5997
|
const [destinations, setDestinations] = useState27({});
|
|
5961
|
-
const
|
|
5998
|
+
const [savedNew, setSavedNew] = useState27(false);
|
|
5999
|
+
const dirty = editing && !savedNew && isDirty(base, draft);
|
|
5962
6000
|
const declared = definition?.fields ?? [];
|
|
5963
6001
|
const prev = useRef14(null);
|
|
5964
6002
|
const paneRef = useRef14(null);
|
|
@@ -5981,8 +6019,11 @@ function ItemPane(props) {
|
|
|
5981
6019
|
setShowTags(false);
|
|
5982
6020
|
setShowDescription(false);
|
|
5983
6021
|
setShowRecovery(false);
|
|
6022
|
+
setSavedNew(false);
|
|
5984
6023
|
}, [item?.id, mode]);
|
|
5985
|
-
|
|
6024
|
+
const onDirtyRef = useRef14(onDirty);
|
|
6025
|
+
onDirtyRef.current = onDirty;
|
|
6026
|
+
useEffect16(() => onDirtyRef.current(dirty), [dirty]);
|
|
5986
6027
|
const keepKey = draftKey(item?.id ?? null, definition?.key ?? item?.definitionKey ?? null);
|
|
5987
6028
|
useEffect16(() => {
|
|
5988
6029
|
if (editing && dirty) keepDraft(keepKey, keptFieldsOf(draft), { basedOn: item?.updatedAt ?? "new" });
|
|
@@ -6039,6 +6080,8 @@ function ItemPane(props) {
|
|
|
6039
6080
|
const [withCode, n0, enrolled] = await withOtp(r.item, r.notice);
|
|
6040
6081
|
const [next, n] = await storeQueuedFiles(withCode, n0);
|
|
6041
6082
|
dropKeptDraft(keepKey);
|
|
6083
|
+
setSavedNew(true);
|
|
6084
|
+
onDirty(false);
|
|
6042
6085
|
onSaved(next, true, n, enrolled);
|
|
6043
6086
|
}
|
|
6044
6087
|
} catch (e) {
|
|
@@ -6101,12 +6144,7 @@ function ItemPane(props) {
|
|
|
6101
6144
|
if (!item) return;
|
|
6102
6145
|
setError(null);
|
|
6103
6146
|
try {
|
|
6104
|
-
|
|
6105
|
-
for (const f of fields) await confirmed(() => port.fields.updateField(item, f.id, { active: true }));
|
|
6106
|
-
if (fields.some((f) => f.key === OTP_SEED_KEY)) {
|
|
6107
|
-
const codes = await port.authenticator.get(item);
|
|
6108
|
-
if (codes && !codes.enabled) await confirmed(() => port.authenticator.setEnabled(item, true));
|
|
6109
|
-
}
|
|
6147
|
+
await turnBackOn(port, item, fields, { run: confirmed });
|
|
6110
6148
|
onChanged();
|
|
6111
6149
|
} catch (e) {
|
|
6112
6150
|
if (!wasCancelled(e)) setError(e instanceof Error ? e.message : "It could not be turned on.");
|
|
@@ -6161,7 +6199,7 @@ function ItemPane(props) {
|
|
|
6161
6199
|
"Unsaved"
|
|
6162
6200
|
] }) : null,
|
|
6163
6201
|
/* @__PURE__ */ jsx27(Button22, { variant: "quiet", disabled: busy, onClick: () => mode === "new" ? onCancelNew() : onMode("view"), children: dirty ? "Discard" : "Cancel" }),
|
|
6164
|
-
/* @__PURE__ */ jsx27(Button22, { variant: "primary", disabled: busy || sent !== null || draft.name.trim() === "" || mode === "edit" && !dirty || mode === "new" && owner === "person" && !/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(recipient.trim()), onClick: () => void save(), children: mode === "new" && owner === "person" ? "Send" : "Save" })
|
|
6202
|
+
/* @__PURE__ */ jsx27(Button22, { variant: "primary", disabled: busy || savedNew || sent !== null || draft.name.trim() === "" || mode === "edit" && !dirty || mode === "new" && owner === "person" && !/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(recipient.trim()), onClick: () => void save(), children: mode === "new" && owner === "person" ? "Send" : "Save" })
|
|
6165
6203
|
] }) : item ? /* @__PURE__ */ jsxs25("div", { className: "flex shrink-0 items-center gap-1", children: [
|
|
6166
6204
|
host.page && canUseOnSite(item, host.page.url) ? /* @__PURE__ */ jsx27(UseOnSiteButton, { port, item, pageUrl: host.page.url, onChanged: (next) => onChanged(next) }) : null,
|
|
6167
6205
|
host.fill && item.capabilities.canUse ? /* @__PURE__ */ jsx27(
|
|
@@ -6329,7 +6367,8 @@ function ItemPane(props) {
|
|
|
6329
6367
|
else delete next[i];
|
|
6330
6368
|
return next;
|
|
6331
6369
|
});
|
|
6332
|
-
|
|
6370
|
+
const stored = r?.url || normalizeLoginUrl(u);
|
|
6371
|
+
if (stored && stored !== u) setDraft((d) => ({ ...d, loginUrls: d.loginUrls.map((x, j) => j === i && x === u ? stored : x) }));
|
|
6333
6372
|
}),
|
|
6334
6373
|
onRemove: () => patch({ loginUrls: draft.loginUrls.filter((_, j) => j !== i) }),
|
|
6335
6374
|
removeLabel: "Remove website"
|
|
@@ -6741,8 +6780,16 @@ import { useEffect as useEffect18, useState as useState29 } from "react";
|
|
|
6741
6780
|
import { Button as Button24 } from "@ai-matrx/design-system/controls";
|
|
6742
6781
|
import { Fragment as Fragment19, jsx as jsx29, jsxs as jsxs27 } from "react/jsx-runtime";
|
|
6743
6782
|
var count = (n, one) => `${n} ${one}${n === 1 ? "" : "s"}`;
|
|
6744
|
-
function RestorePanel({
|
|
6783
|
+
function RestorePanel({
|
|
6784
|
+
lists,
|
|
6785
|
+
item,
|
|
6786
|
+
restore,
|
|
6787
|
+
turnOn,
|
|
6788
|
+
onClose
|
|
6789
|
+
}) {
|
|
6745
6790
|
const [preview, setPreview] = useState29(null);
|
|
6791
|
+
const [restored, setRestored] = useState29(null);
|
|
6792
|
+
const [turnedOn, setTurnedOn] = useState29(null);
|
|
6746
6793
|
const [error, setError] = useState29(null);
|
|
6747
6794
|
const [busy, setBusy] = useState29(false);
|
|
6748
6795
|
const [loading, setLoading] = useState29(Boolean(lists.preview));
|
|
@@ -6771,13 +6818,54 @@ function RestorePanel({ lists, item, restore, onClose }) {
|
|
|
6771
6818
|
setError(null);
|
|
6772
6819
|
try {
|
|
6773
6820
|
const result = await restore(preview?.supported ? preview.deletionId : void 0);
|
|
6774
|
-
if (result)
|
|
6821
|
+
if (!result) return;
|
|
6822
|
+
if (turnOn && !result.alreadyRestored && preview?.supported && !preview.priorWasDisabled) setRestored(result);
|
|
6823
|
+
else onClose();
|
|
6775
6824
|
} catch (e) {
|
|
6776
6825
|
setError(e instanceof Error && e.message ? e.message : "Restore did not complete. Try again.");
|
|
6777
6826
|
} finally {
|
|
6778
6827
|
setBusy(false);
|
|
6779
6828
|
}
|
|
6780
6829
|
};
|
|
6830
|
+
const runTurnOn = async () => {
|
|
6831
|
+
if (!restored || !turnOn) return;
|
|
6832
|
+
setBusy(true);
|
|
6833
|
+
setError(null);
|
|
6834
|
+
try {
|
|
6835
|
+
const said = await turnOn(restored.item);
|
|
6836
|
+
if (said !== null) setTurnedOn(said);
|
|
6837
|
+
} catch (e) {
|
|
6838
|
+
setError(e instanceof Error && e.message ? e.message : "It could not be turned on. Turn it on from the item.");
|
|
6839
|
+
} finally {
|
|
6840
|
+
setBusy(false);
|
|
6841
|
+
}
|
|
6842
|
+
};
|
|
6843
|
+
if (restored) {
|
|
6844
|
+
return /* @__PURE__ */ jsxs27(
|
|
6845
|
+
Panel,
|
|
6846
|
+
{
|
|
6847
|
+
title: `Restored "${item.name}"`,
|
|
6848
|
+
size: "md",
|
|
6849
|
+
onClose,
|
|
6850
|
+
footer: turnedOn ? /* @__PURE__ */ jsx29(Button24, { variant: "primary", onClick: onClose, children: "Done" }) : /* @__PURE__ */ jsxs27(Fragment19, { children: [
|
|
6851
|
+
/* @__PURE__ */ jsx29(Button24, { variant: "quiet", onClick: onClose, children: "Leave off" }),
|
|
6852
|
+
/* @__PURE__ */ jsx29(Button24, { variant: "primary", icon: /* @__PURE__ */ jsx29(RestoreGlyph, {}), disabled: busy, onClick: () => void runTurnOn(), children: busy ? "Turning on\u2026" : "Turn these back on" })
|
|
6853
|
+
] }),
|
|
6854
|
+
children: [
|
|
6855
|
+
/* @__PURE__ */ jsxs27(VaultSection, { title: turnedOn ? "Turned on" : "Back, turned off", children: [
|
|
6856
|
+
/* @__PURE__ */ jsx29(VaultRow, { label: "Came back", children: /* @__PURE__ */ jsxs27("span", { className: "text-foreground", children: [
|
|
6857
|
+
count(restored.restoredFields, "field"),
|
|
6858
|
+
", ",
|
|
6859
|
+
count(restored.restoredAttachments, "file")
|
|
6860
|
+
] }) }),
|
|
6861
|
+
turnedOn ? /* @__PURE__ */ jsx29(VaultRow, { label: "Now on", children: /* @__PURE__ */ jsx29("span", { role: "status", className: "text-foreground", children: turnedOn }) }) : /* @__PURE__ */ jsx29(VaultRow, { label: "Turns on", hint: "One press, nothing else changes", children: /* @__PURE__ */ jsx29("span", { className: "text-muted-foreground", children: "The item, its fields, codes and browser fill" }) }),
|
|
6862
|
+
/* @__PURE__ */ jsx29(VaultRow, { label: "Stays off", hint: "Choose these again yourself", children: /* @__PURE__ */ jsx29("span", { className: "text-muted-foreground", children: "Sharing, sandbox use" }) })
|
|
6863
|
+
] }),
|
|
6864
|
+
error ? /* @__PURE__ */ jsx29("p", { role: "alert", className: "px-1 text-sm text-destructive", children: error }) : null
|
|
6865
|
+
]
|
|
6866
|
+
}
|
|
6867
|
+
);
|
|
6868
|
+
}
|
|
6781
6869
|
return /* @__PURE__ */ jsxs27(
|
|
6782
6870
|
Panel,
|
|
6783
6871
|
{
|
|
@@ -6818,7 +6906,7 @@ function TrashView({ port, deleted, query, header, onChanged }) {
|
|
|
6818
6906
|
setMessage(null);
|
|
6819
6907
|
try {
|
|
6820
6908
|
const r = await confirmed(() => port.lists.restore(item, deletionId));
|
|
6821
|
-
setMessage({ tone: "ok", text: r.alreadyRestored ? `"${item.name}" was already restored.` : `Restored "${item.name}" with ${r.restoredFields} fields and ${r.restoredAttachments} files
|
|
6909
|
+
setMessage({ tone: "ok", text: r.alreadyRestored ? `"${item.name}" was already restored.` : `Restored "${item.name}" with ${r.restoredFields} fields and ${r.restoredAttachments} files, turned off.` });
|
|
6822
6910
|
onChanged();
|
|
6823
6911
|
return r;
|
|
6824
6912
|
} catch (e) {
|
|
@@ -6828,6 +6916,19 @@ function TrashView({ port, deleted, query, header, onChanged }) {
|
|
|
6828
6916
|
setBusyId(null);
|
|
6829
6917
|
}
|
|
6830
6918
|
};
|
|
6919
|
+
const turnOn = async (item) => {
|
|
6920
|
+
try {
|
|
6921
|
+
const done = await turnBackOn(port, item, item.fields, { browserFill: true, run: confirmed });
|
|
6922
|
+
const said = turnedOnSentence(done);
|
|
6923
|
+
setMessage({ tone: "ok", text: `"${item.name}": ${said}` });
|
|
6924
|
+
onChanged();
|
|
6925
|
+
return said;
|
|
6926
|
+
} catch (e) {
|
|
6927
|
+
onChanged();
|
|
6928
|
+
if (wasCancelled(e)) return null;
|
|
6929
|
+
throw e;
|
|
6930
|
+
}
|
|
6931
|
+
};
|
|
6831
6932
|
return /* @__PURE__ */ jsxs28("div", { className: "flex min-h-0 flex-1 flex-col gap-3 overflow-y-auto p-3", children: [
|
|
6832
6933
|
header,
|
|
6833
6934
|
/* @__PURE__ */ jsx30(VaultSection, { title: "Trash", children: shown.length === 0 ? /* @__PURE__ */ jsx30(Empty, { children: deleted.length === 0 ? "The trash is empty." : "Nothing matches." }) : shown.map((item) => (
|
|
@@ -6841,7 +6942,7 @@ function TrashView({ port, deleted, query, header, onChanged }) {
|
|
|
6841
6942
|
/* @__PURE__ */ jsx30(Button25, { variant: "outline", icon: /* @__PURE__ */ jsx30(RestoreGlyph, {}), disabled: busyId !== null || !item.deletion, onClick: () => setRestoring(item), children: "Restore" })
|
|
6842
6943
|
] }, item.id)
|
|
6843
6944
|
)) }),
|
|
6844
|
-
restoring ? /* @__PURE__ */ jsx30(RestorePanel, { lists: port.lists, item: restoring, restore: (deletionId) => restore(restoring, deletionId), onClose: () => setRestoring(null) }) : null,
|
|
6945
|
+
restoring ? /* @__PURE__ */ jsx30(RestorePanel, { lists: port.lists, item: restoring, restore: (deletionId) => restore(restoring, deletionId), turnOn, onClose: () => setRestoring(null) }) : null,
|
|
6845
6946
|
message2 ? /* @__PURE__ */ jsx30("p", { role: "status", className: message2.tone === "error" ? "px-1 text-sm text-destructive" : "px-1 text-sm text-muted-foreground", children: message2.text }) : null
|
|
6846
6947
|
] });
|
|
6847
6948
|
}
|
|
@@ -9106,7 +9207,7 @@ function createFieldsPort(ctx) {
|
|
|
9106
9207
|
const body = { initiation: "user" };
|
|
9107
9208
|
if (patch.uriMatchMode !== void 0) body.uri_match_mode = patch.uriMatchMode;
|
|
9108
9209
|
if (patch.browserFillEnabled !== void 0) body.browser_fill_enabled = patch.browserFillEnabled;
|
|
9109
|
-
if (patch.loginUrls !== void 0) body.login_urls = patch.loginUrls;
|
|
9210
|
+
if (patch.loginUrls !== void 0) body.login_urls = normalizeLoginUrls(patch.loginUrls);
|
|
9110
9211
|
if (patch.active !== void 0) body.status = patch.active ? "active" : "disabled";
|
|
9111
9212
|
await account3.updateItem(api, { path: { item_id: item.id }, body }, ctx.optsFor(item));
|
|
9112
9213
|
return reread(item);
|
|
@@ -9118,6 +9219,33 @@ function createFieldsPort(ctx) {
|
|
|
9118
9219
|
};
|
|
9119
9220
|
}
|
|
9120
9221
|
|
|
9222
|
+
// src/server-reasons.ts
|
|
9223
|
+
var GENERIC = [/please check your input/i, /^\s*request failed\s*\(\d+\)\s*$/i, /^\s*bad request\.?\s*$/i, /^\s*HTTP \d+\s*$/i];
|
|
9224
|
+
var isGeneric = (s) => GENERIC.some((re) => re.test(s));
|
|
9225
|
+
function cleanReason(detail) {
|
|
9226
|
+
const s = detail.replace(/^\s*validation error:\s*/i, "").replace(/^\s*value error,\s*/i, "").trim();
|
|
9227
|
+
return s ? s.charAt(0).toUpperCase() + s.slice(1) : s;
|
|
9228
|
+
}
|
|
9229
|
+
function serverReason(e) {
|
|
9230
|
+
if (!e || typeof e !== "object") return null;
|
|
9231
|
+
const { status, detail, message: message2 } = e;
|
|
9232
|
+
if (typeof status !== "number" || status < 400 || status >= 500) return null;
|
|
9233
|
+
if (typeof detail !== "string" || !detail.trim() || isGeneric(detail)) return null;
|
|
9234
|
+
const shown = typeof message2 === "string" ? message2 : "";
|
|
9235
|
+
if (shown && !isGeneric(shown)) return null;
|
|
9236
|
+
return cleanReason(detail);
|
|
9237
|
+
}
|
|
9238
|
+
function withReason(e) {
|
|
9239
|
+
const reason = serverReason(e);
|
|
9240
|
+
if (reason) Object.assign(e, { message: reason, userMessage: reason });
|
|
9241
|
+
return e;
|
|
9242
|
+
}
|
|
9243
|
+
function withServerReasons(api) {
|
|
9244
|
+
const wrapped = Object.create(api);
|
|
9245
|
+
wrapped.call = (...args) => api.call(...args).catch((e) => Promise.reject(withReason(e)));
|
|
9246
|
+
return wrapped;
|
|
9247
|
+
}
|
|
9248
|
+
|
|
9121
9249
|
// src/io/api.ts
|
|
9122
9250
|
import * as account4 from "@ai-matrx/api/account";
|
|
9123
9251
|
var orgOpts = (organizationId) => organizationId ? { organizationId } : void 0;
|
|
@@ -9390,7 +9518,7 @@ function createSharingPort(ctx, options = {}) {
|
|
|
9390
9518
|
definition_key: definition.key,
|
|
9391
9519
|
provider_key: definition.providerKey,
|
|
9392
9520
|
tags: draft.tags,
|
|
9393
|
-
login_urls: draft.loginUrls,
|
|
9521
|
+
login_urls: normalizeLoginUrls(draft.loginUrls),
|
|
9394
9522
|
notes: draft.notes || null,
|
|
9395
9523
|
generate_field_key: generateFieldKey,
|
|
9396
9524
|
initiation: "user",
|
|
@@ -9587,7 +9715,8 @@ async function retrying(run, tries = 3) {
|
|
|
9587
9715
|
}
|
|
9588
9716
|
}
|
|
9589
9717
|
}
|
|
9590
|
-
function createApiVaultPort(
|
|
9718
|
+
function createApiVaultPort(hostApi, options = {}) {
|
|
9719
|
+
const api = withServerReasons(hostApi);
|
|
9591
9720
|
let defs = /* @__PURE__ */ new Map();
|
|
9592
9721
|
let auth = /* @__PURE__ */ new Map();
|
|
9593
9722
|
let orgs = [];
|
|
@@ -9742,6 +9871,7 @@ function createApiVaultPort(api, options = {}) {
|
|
|
9742
9871
|
const declared = definition.fields;
|
|
9743
9872
|
const encrypted = declared.filter((f) => f.storage === "encrypted" && (draft.values[f.key] ?? "") !== "");
|
|
9744
9873
|
const meta = declared.filter((f) => f.storage === "metadata" && (draft.values[f.key] ?? "") !== "");
|
|
9874
|
+
const loginUrls = normalizeLoginUrls(draft.loginUrls);
|
|
9745
9875
|
const w = await account9.createItem(api, {
|
|
9746
9876
|
body: {
|
|
9747
9877
|
display_name: draft.name.trim(),
|
|
@@ -9749,10 +9879,10 @@ function createApiVaultPort(api, options = {}) {
|
|
|
9749
9879
|
definition_key: definition.key,
|
|
9750
9880
|
provider_key: definition.providerKey,
|
|
9751
9881
|
tags: draft.tags,
|
|
9752
|
-
login_urls:
|
|
9882
|
+
login_urls: loginUrls,
|
|
9753
9883
|
// A login a person saves with its website is offered on that website (the server's own
|
|
9754
9884
|
// default is off, which made "Save a login" on a page answer "No saved login" there).
|
|
9755
|
-
...draft.browser ? { uri_match_mode: draft.browser.uriMatchMode, browser_fill_enabled: draft.browser.browserFillEnabled } :
|
|
9885
|
+
...draft.browser ? { uri_match_mode: draft.browser.uriMatchMode, browser_fill_enabled: draft.browser.browserFillEnabled } : loginUrls.length > 0 ? { browser_fill_enabled: true } : {},
|
|
9756
9886
|
notes: draft.notes || null,
|
|
9757
9887
|
principal: organizationId ? { type: "organization", organization_id: organizationId } : { type: "user" },
|
|
9758
9888
|
initiation: "user",
|
|
@@ -9779,7 +9909,7 @@ function createApiVaultPort(api, options = {}) {
|
|
|
9779
9909
|
else body.description = draft.description.trim();
|
|
9780
9910
|
}
|
|
9781
9911
|
if (JSON.stringify(draft.tags) !== JSON.stringify(item.tags)) body.tags = draft.tags;
|
|
9782
|
-
if (JSON.stringify(draft.loginUrls) !== JSON.stringify(item.loginUrls)) body.login_urls = draft.loginUrls;
|
|
9912
|
+
if (JSON.stringify(draft.loginUrls) !== JSON.stringify(item.loginUrls)) body.login_urls = normalizeLoginUrls(draft.loginUrls);
|
|
9783
9913
|
if (draft.notes !== item.notes) {
|
|
9784
9914
|
if (draft.notes === "") body.clear_notes = true;
|
|
9785
9915
|
else body.notes = draft.notes;
|
|
@@ -9862,12 +9992,42 @@ function vaultInventoryScope(inventory, clicked = null) {
|
|
|
9862
9992
|
function itemIdAt(target) {
|
|
9863
9993
|
return target?.closest("[data-item]")?.getAttribute("data-item") ?? null;
|
|
9864
9994
|
}
|
|
9995
|
+
|
|
9996
|
+
// src/io/empty-read.ts
|
|
9997
|
+
var HOLDINGS_DOOR_MIGRATION = "migrations/campaign/errorshonest_s8_a_credential_says_how_much_it_holds.sql";
|
|
9998
|
+
function holdingsDoorIsAbsent(code) {
|
|
9999
|
+
return code === "PGRST202" || code === "42883";
|
|
10000
|
+
}
|
|
10001
|
+
function staleAlarm(total) {
|
|
10002
|
+
return `Your vault has ${total} ${total === 1 ? "item" : "items"} but none of their fields or files could be read. Either every one of these items is genuinely still empty, or the database refused the read without reporting an error \u2014 the second is a known failure mode of this screen (DD-160). Nothing has been lost: reload, and if the items are still blank, report it rather than re-entering the credentials.`;
|
|
10003
|
+
}
|
|
10004
|
+
function emptyReadAlarm(total, emptyIds, holdings) {
|
|
10005
|
+
if (emptyIds.length === 0) return null;
|
|
10006
|
+
if (holdings.state === "answered") {
|
|
10007
|
+
const asked = new Set(emptyIds);
|
|
10008
|
+
const unread = holdings.rows.filter(
|
|
10009
|
+
(r) => asked.has(r.credential_item_id) && (r.field_count > 0 || r.file_count > 0)
|
|
10010
|
+
);
|
|
10011
|
+
if (unread.length === 0) return null;
|
|
10012
|
+
const n = unread.length;
|
|
10013
|
+
return `${n === 1 ? "One credential" : `${n} credentials`} in this vault ${n === 1 ? "holds" : "hold"} fields or files this screen could not read \u2014 the database refused the read without reporting an error, a known failure mode of this screen (DD-160). Nothing has been lost: reload, and if ${n === 1 ? "it is" : "they are"} still blank, report it rather than re-entering the credentials.`;
|
|
10014
|
+
}
|
|
10015
|
+
if (emptyIds.length < total) return null;
|
|
10016
|
+
if (holdings.state === "absent") {
|
|
10017
|
+
console.warn(
|
|
10018
|
+
`[vault] users.credential_item_holdings is not on this database (${holdings.why}); the empty-read alarm falls back to the whole-vault rule until ${HOLDINGS_DOOR_MIGRATION} is applied.`
|
|
10019
|
+
);
|
|
10020
|
+
return staleAlarm(total);
|
|
10021
|
+
}
|
|
10022
|
+
return `${staleAlarm(total)} (The check that tells an empty credential from an unreadable one did not answer either: ${holdings.why})`;
|
|
10023
|
+
}
|
|
9865
10024
|
export {
|
|
9866
10025
|
CATEGORIES,
|
|
9867
10026
|
CONFIRM_RETURN_KEY,
|
|
9868
10027
|
DEFAULT_LOCATION,
|
|
9869
10028
|
GenerateButton,
|
|
9870
10029
|
HIDDEN_ITEM_TAG,
|
|
10030
|
+
HOLDINGS_DOOR_MIGRATION,
|
|
9871
10031
|
IdentityGate,
|
|
9872
10032
|
LOCKED_MESSAGE,
|
|
9873
10033
|
VaultApp,
|
|
@@ -9877,7 +10037,9 @@ export {
|
|
|
9877
10037
|
createApiVaultPort,
|
|
9878
10038
|
createSupabaseIdentity,
|
|
9879
10039
|
createVaultKnobs,
|
|
10040
|
+
emptyReadAlarm,
|
|
9880
10041
|
guardPort,
|
|
10042
|
+
holdingsDoorIsAbsent,
|
|
9881
10043
|
inferFromUrl,
|
|
9882
10044
|
isHiddenItem,
|
|
9883
10045
|
itemIdAt,
|