The dump worker becomes a page cache: a hit is served from DO SQLite storage with no diigo call; a miss asks the webapp instance in the cache's wiring and etches the page. - two independent caches, one behavior (PagedCache): DiigoCache keeps every item; PublicCache keeps shared items only, dropping private rows before they are stored; served at /public/<username> (HTML page form, or JSON) - create flow on one port: /_connect relays webapp's mint form, /_mint proxies the mint, /_wire wires a private or public cache; /_publish adds a public cache later - ids are webkeys: kept out of URLs (POST bodies), shown and logged only as a hash fingerprint; saved to the password manager under the holder's petname (never sent to the server) - items keyed on diigo's link_id; username from u_name; unkeyable items and every failure path are logged; each page read logs hit/miss - tests run the real worker + DOs in the vitest pool against a fake webapp (tests/fake-webapp.js) Generated-by: opencode (GLM) Generated-by: Claude Opus 5.5
58 lines
2.3 KiB
TypeScript
58 lines
2.3 KiB
TypeScript
/**
|
|
* @file the password-manager save — ids reach the browser's storage only by
|
|
* this gesture-gated path (or copy), never localStorage/IndexedDB, which
|
|
* are ambient and XSS-reachable. It runs in the *browser*: pages embed it
|
|
* via `pmSave.toString()`, so it must be self-contained (no imports, no
|
|
* closure, no inner named functions — wrangler's keepNames would wrap
|
|
* those in an `__name` helper the browser doesn't have). Its powers
|
|
* (the petname field, the credential store, the console) are passed in,
|
|
* so tests can stand in for the browser.
|
|
*/
|
|
|
|
/** the browser powers pmSave uses */
|
|
export type PmPowers = {
|
|
petname: { value: string };
|
|
credentials?: { store(credential: unknown): Promise<unknown> };
|
|
PasswordCredential?: new (init: { id: string; password: string }) => unknown;
|
|
log: Pick<Console, "info" | "warn" | "error">;
|
|
};
|
|
|
|
/** the button pmSave reports on */
|
|
export type PmButton = { textContent: string | null; disabled: boolean };
|
|
|
|
/**
|
|
* gesture-gated save of `value` under the holder's petname (the PM
|
|
* username).
|
|
*
|
|
* @see https://developer.mozilla.org/en-US/docs/Web/API/CredentialsContainer/store
|
|
*/
|
|
export function pmSave(btn: PmButton, value: string, powers: PmPowers): Promise<void> {
|
|
const label = powers.petname.value.trim();
|
|
if (!label) {
|
|
btn.textContent = "give it a petname first";
|
|
return Promise.resolve();
|
|
}
|
|
btn.disabled = true;
|
|
if (!powers.PasswordCredential || !powers.credentials) {
|
|
powers.log.warn({ event: "pm-save", outcome: "unsupported", detail: "no PasswordCredential / navigator.credentials" });
|
|
btn.textContent = "browser has no credential store — copy instead";
|
|
btn.disabled = false;
|
|
return Promise.resolve();
|
|
}
|
|
return powers.credentials
|
|
.store(new powers.PasswordCredential({ id: label, password: value }))
|
|
.then(() => {
|
|
// store() resolves with undefined per spec, whether the holder
|
|
// accepted the browser's prompt or not (or no prompt showed):
|
|
// we can only say it was offered — the PM's own UI is the truth
|
|
powers.log.info({ event: "pm-save", outcome: "offered", label });
|
|
btn.textContent = "offered to the password manager ✓ (see its prompt)";
|
|
btn.disabled = false;
|
|
})
|
|
.catch((err) => {
|
|
powers.log.error({ event: "pm-save", error: String(err) });
|
|
btn.textContent = "save failed — copy instead";
|
|
btn.disabled = false;
|
|
});
|
|
}
|