fix(usage): minimize session receipts and clarify privacy controls
This commit is contained in:
@@ -50,6 +50,7 @@ maybe('product usage on Postgres', () => {
|
||||
await require('../../migrations/core/201_product_usage').up(db);
|
||||
await require('../../migrations/core/202_product_usage_cancel_requested').up(db);
|
||||
await require('../../migrations/core/203_product_usage_cancel_seq').up(db);
|
||||
await require('../../migrations/core/204_product_usage_privacy_receipts').up(db);
|
||||
|
||||
await db.schema.createTable('app_settings', (t) => {
|
||||
t.string('setting_key').primary(); t.text('setting_value'); t.string('setting_type');
|
||||
@@ -110,6 +111,18 @@ maybe('product usage on Postgres', () => {
|
||||
expect(cols.cancel_seq).toBeDefined();
|
||||
expect(cols.cancel_requested).toBeUndefined(); // dropped by 203
|
||||
expect(cols.sequence).toBeDefined();
|
||||
expect(cols.privacy_receipts).toBeDefined();
|
||||
});
|
||||
|
||||
it('reruns the receipt migration safely and scrubs legacy plaintext sessions', async () => {
|
||||
const migration = require('../../migrations/core/204_product_usage_privacy_receipts');
|
||||
await db('product_usage_state').where({ id: 1 }).update({
|
||||
last_receipt: JSON.stringify({ status: 'accepted', session_token: 'synthetic-old-token' })
|
||||
});
|
||||
await migration.up(db);
|
||||
await migration.up(db);
|
||||
const row = await db('product_usage_state').where({ id: 1 }).first();
|
||||
expect(JSON.parse(row.last_receipt)).toEqual({ status: 'accepted' });
|
||||
});
|
||||
|
||||
it('reads bigint cancel_seq correctly even though pg returns it as a string', async () => {
|
||||
|
||||
@@ -49,6 +49,7 @@ async function bootDb() {
|
||||
t.text('pending_packet');
|
||||
t.text('last_packet');
|
||||
t.text('last_receipt');
|
||||
t.text('privacy_receipts');
|
||||
t.string('last_report_date', 10);
|
||||
t.string('last_error', 80);
|
||||
t.text('feedback_preferences');
|
||||
|
||||
@@ -32,6 +32,7 @@ async function bootDb() {
|
||||
t.text('pending_packet');
|
||||
t.text('last_packet');
|
||||
t.text('last_receipt');
|
||||
t.text('privacy_receipts');
|
||||
t.string('last_report_date', 10);
|
||||
t.string('last_error', 80);
|
||||
t.text('feedback_preferences');
|
||||
|
||||
@@ -27,6 +27,7 @@ async function bootDb() {
|
||||
t.string('instance_binding', 64);
|
||||
t.bigInteger('sequence').notNullable().defaultTo(0);
|
||||
t.text('pending_packet'); t.text('last_packet'); t.text('last_receipt');
|
||||
t.text('privacy_receipts');
|
||||
t.string('last_report_date', 10); t.string('last_error', 80);
|
||||
t.text('feedback_preferences'); t.string('lease_token', 36);
|
||||
t.bigInteger('lease_until').notNullable().defaultTo(0);
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
// Bounded, local-only audit receipts. Never retain an installation identity,
|
||||
// signing key, report/feedback payload or collector credential after opt-out.
|
||||
exports.up = async function (knex) {
|
||||
if (
|
||||
(await knex.schema.hasTable('product_usage_state')) &&
|
||||
!(await knex.schema.hasColumn('product_usage_state', 'privacy_receipts'))
|
||||
) {
|
||||
await knex.schema.alterTable('product_usage_state', (t) =>
|
||||
t.text('privacy_receipts')
|
||||
);
|
||||
}
|
||||
if (await knex.schema.hasColumn('product_usage_state', 'last_receipt')) {
|
||||
const row = await knex('product_usage_state').where({ id: 1 }).first();
|
||||
if (row?.last_receipt) {
|
||||
const receipt = JSON.parse(row.last_receipt);
|
||||
if (receipt.session_token) {
|
||||
delete receipt.session_token;
|
||||
await knex('product_usage_state')
|
||||
.where({ id: 1 })
|
||||
.update({ last_receipt: JSON.stringify(receipt) });
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
exports.down = async function (knex) {
|
||||
if (
|
||||
(await knex.schema.hasTable('product_usage_state')) &&
|
||||
(await knex.schema.hasColumn('product_usage_state', 'privacy_receipts'))
|
||||
) {
|
||||
await knex.schema.alterTable('product_usage_state', (t) =>
|
||||
t.dropColumn('privacy_receipts')
|
||||
);
|
||||
}
|
||||
};
|
||||
@@ -239,6 +239,9 @@ class UsageService {
|
||||
? JSON.parse(state.pending_packet).action
|
||||
: null,
|
||||
last_packet: state.last_packet ? JSON.parse(state.last_packet) : null,
|
||||
privacy_receipts: state.privacy_receipts
|
||||
? JSON.parse(state.privacy_receipts)
|
||||
: {},
|
||||
feedback_preferences: state.feedback_preferences
|
||||
? JSON.parse(state.feedback_preferences)
|
||||
: { name: '' }
|
||||
@@ -458,17 +461,37 @@ class UsageService {
|
||||
pending_packet: null,
|
||||
last_packet: null,
|
||||
last_receipt: null,
|
||||
privacy_receipts: JSON.stringify({
|
||||
last_deletion: {
|
||||
receipt_version: 'local-audit.v1',
|
||||
kind: 'deletion',
|
||||
receipt_id: crypto.randomUUID(),
|
||||
confirmed_at: new Date(this.now()).toISOString(),
|
||||
status: 'collector-confirmed',
|
||||
scope: [
|
||||
'reports',
|
||||
'snapshots',
|
||||
'feedback',
|
||||
'votes',
|
||||
'sessions',
|
||||
'operations',
|
||||
'registration'
|
||||
]
|
||||
}
|
||||
}),
|
||||
last_report_date: null,
|
||||
last_error: null,
|
||||
sequence: 0,
|
||||
feedback_preferences: null
|
||||
});
|
||||
} else {
|
||||
const storedReceipt = { ...receipt };
|
||||
delete storedReceipt.session_token;
|
||||
const update = {
|
||||
sequence: packet.sequence,
|
||||
pending_packet: null,
|
||||
last_error: null,
|
||||
last_receipt: JSON.stringify(receipt)
|
||||
last_receipt: JSON.stringify(storedReceipt)
|
||||
};
|
||||
if (packet.action === 'report') {
|
||||
update.last_packet = JSON.stringify(envelope);
|
||||
@@ -805,11 +828,39 @@ class UsageService {
|
||||
if (!state.installation_id) throw new ConflictError('No usage identity');
|
||||
// Own-data export includes the complete retained history, not a truncated
|
||||
// packet subset. The acceptance/receipt path above stays strictly bounded.
|
||||
return this.post(
|
||||
const result = await this.post(
|
||||
'/api/participant/lookup',
|
||||
{ installation_id: state.installation_id },
|
||||
Infinity
|
||||
);
|
||||
// Only the last export during this participation is retained locally.
|
||||
// Do not restore audit state if opt-out/identity replacement happened while
|
||||
// the export was in flight. The downloaded file carries the full receipt.
|
||||
const receipts = state.privacy_receipts
|
||||
? JSON.parse(state.privacy_receipts)
|
||||
: {};
|
||||
await this.db('product_usage_state')
|
||||
.where({
|
||||
id: 1,
|
||||
status: 'active',
|
||||
installation_id: state.installation_id
|
||||
})
|
||||
.update({
|
||||
privacy_receipts: JSON.stringify({
|
||||
...receipts,
|
||||
last_export: {
|
||||
receipt_version: 'local-audit.v1',
|
||||
kind: 'export',
|
||||
receipt_id: crypto.randomUUID(),
|
||||
confirmed_at: new Date(this.now()).toISOString(),
|
||||
report_count: Array.isArray(result.packets)
|
||||
? result.packets.length
|
||||
: 0,
|
||||
scope: ['unique accepted usage reports']
|
||||
}
|
||||
})
|
||||
});
|
||||
return result;
|
||||
}
|
||||
}
|
||||
module.exports = { UsageService, FLAG_MAP };
|
||||
|
||||
+25
-3
@@ -24,7 +24,7 @@ tracker or CORS policy is required. The collector is the separate
|
||||
| `USAGE_ENCRYPTION_KEY` | JWT_SECRET | 32+ characters, encrypts the local Ed25519 key with AES-256-GCM |
|
||||
|
||||
Both database engines are supported. The state and marker tables are created
|
||||
by migrations 201-203 on PostgreSQL and SQLite alike, and the engine-sensitive
|
||||
by migrations 201-204 on PostgreSQL and SQLite alike, and the engine-sensitive
|
||||
paths are covered by `__tests__/integration/productUsagePg.test.js` against a
|
||||
real PostgreSQL — bigint columns come back as strings there, booleans are real
|
||||
booleans rather than 0/1, and the marker write takes `SELECT ... FOR UPDATE`
|
||||
@@ -63,6 +63,16 @@ reports, projections, feedback/publications, votes, and sessions. PicPeak then
|
||||
erases the local fingerprint, private key and binding. A later join generates
|
||||
a fresh identity. Repeated deletion handles lost receipts safely.
|
||||
|
||||
Migration 204 adds bounded, local-only privacy receipts and removes any legacy
|
||||
plaintext voting token from the last collector receipt. A completed export
|
||||
records its time and report count; confirmed opt-out replaces this with a
|
||||
deletion receipt containing only a random receipt ID, time, status and fixed
|
||||
scope. It retains no old installation hash, key, payload or credential. The
|
||||
settings page can download these receipts even after opt-out. They are local
|
||||
records of the collector acknowledgement, not independent proof of storage
|
||||
erasure. Downloaded exports carry their own dated receipt; the collector does
|
||||
not create a permanent per-person access/export log.
|
||||
|
||||
A missing/mismatched storage binding or conflicting collector sequence stops
|
||||
reporting with identity conflict. A full clone of a signing identity cannot be
|
||||
distinguished cryptographically. Do not run the same participation identity in
|
||||
@@ -85,10 +95,22 @@ Public voting uses a backend-authorized 15-minute session, never the lookup hash
|
||||
|
||||
The closed schema is in `backend/src/usage/schema.cjs`, with signing in
|
||||
`protocol.cjs`. Keep both byte-identical to the collector's `protocol/` copies.
|
||||
The collector serves the schema, complete source archive, public projections,
|
||||
and full raw exports. Feature semantics and retention are documented in its
|
||||
The collector serves its schema and complete source archive publicly. Aggregate
|
||||
projections and the complete dataset are accessible to participating
|
||||
installations only; raw reports require the installation's confidential lookup
|
||||
hash. Raw exports contain the first accepted envelope of every unique usage
|
||||
report. Re-signed transport retries are deduplicated; feedback, registration,
|
||||
sessions and rejected requests are not usage reports. Full exports use a
|
||||
consistent database snapshot at their start, not a 200-record total limit.
|
||||
Feature semantics and retention are documented in its
|
||||
`docs/PROTOCOL.md` and `docs/OPERATIONS.md`.
|
||||
|
||||
Public, reviewed testimonials are separate from marketing approval. Homepage
|
||||
integrations must use `/api/public/marketing-testimonials`, never the general
|
||||
portal testimonial feed. Each page is bounded and exposes its continuation
|
||||
cursor. Deletion removes the source publication; operators must also remove
|
||||
any externally copied content and follow the documented backup/log policies.
|
||||
|
||||
Used flags represent successful allowlisted admin capability calls since
|
||||
joining, not visitor behavior or counts. OAuth marks successful admin SSO;
|
||||
applied CSS is observed during report generation. Gallery layouts are controlled
|
||||
|
||||
@@ -67,6 +67,31 @@ beforeEach(() => {
|
||||
});
|
||||
afterEach(cleanup);
|
||||
describe('product usage controls', () => {
|
||||
it('offers identity-free audit receipts after opt-out without restoring participation controls', async () => {
|
||||
vi.mocked(service.status).mockResolvedValue({
|
||||
...status,
|
||||
privacy_receipts: {
|
||||
last_deletion: {
|
||||
receipt_version: 'local-audit.v1',
|
||||
kind: 'deletion',
|
||||
status: 'collector-confirmed'
|
||||
}
|
||||
}
|
||||
});
|
||||
mount();
|
||||
expect(
|
||||
await screen.findByRole('button', { name: 'productUsage.auditDownload' })
|
||||
).toBeEnabled();
|
||||
expect(
|
||||
screen.getByText('productUsage.auditDescription')
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByLabelText('productUsage.hash')
|
||||
).not.toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByText('productUsage.feedbackTitle')
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
it('requires the disclosure and an unchecked-by-default consent before enabling', async () => {
|
||||
mount();
|
||||
fireEvent.click(await screen.findByText('productUsage.review'));
|
||||
|
||||
@@ -191,13 +191,13 @@ export default function ProductUsageTab() {
|
||||
await queryClient.invalidateQueries({ queryKey: ['productUsage'] });
|
||||
}
|
||||
};
|
||||
const download = (value: unknown) => {
|
||||
const download = (value: unknown, filename = 'picpeak-usage-packets.json') => {
|
||||
const url = URL.createObjectURL(
|
||||
new Blob([JSON.stringify(value, null, 2)], { type: 'application/json' })
|
||||
);
|
||||
const anchor = document.createElement('a');
|
||||
anchor.href = url;
|
||||
anchor.download = 'picpeak-usage-packets.json';
|
||||
anchor.download = filename;
|
||||
anchor.click();
|
||||
window.setTimeout(() => URL.revokeObjectURL(url), 1000);
|
||||
};
|
||||
@@ -300,6 +300,26 @@ export default function ProductUsageTab() {
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
{data.privacy_receipts &&
|
||||
Object.keys(data.privacy_receipts).length > 0 && (
|
||||
<Card padding="md" className="space-y-4">
|
||||
<h3 className="text-lg font-semibold">
|
||||
{t('productUsage.auditTitle')}
|
||||
</h3>
|
||||
<p>{t('productUsage.auditDescription')}</p>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() =>
|
||||
download(
|
||||
data.privacy_receipts,
|
||||
'picpeak-usage-privacy-receipts.json'
|
||||
)
|
||||
}
|
||||
>
|
||||
{t('productUsage.auditDownload')}
|
||||
</Button>
|
||||
</Card>
|
||||
)}
|
||||
{active && (
|
||||
<>
|
||||
<Card padding="md" className="space-y-4">
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
{
|
||||
"productUsage": {
|
||||
"auditTitle": "Export- und Löschquittungen",
|
||||
"auditDescription": "Lade deine privaten Nachweise herunter. PicPeak speichert nur die letzte Exportquittung während der Teilnahme und die letzte Löschbestätigung. Sie enthalten keinen Installationshash, Schlüssel oder Bericht-/Feedbackinhalt. Opt-out entfernt die lokale Exportquittung; die Löschbestätigung ohne Identitätsbezug bleibt erhalten. Der Collector führt keinen Export- oder Zugriffsverlauf.",
|
||||
"auditDownload": "Datenschutzquittungen herunterladen",
|
||||
"title": "Produktnutzung & Feedback",
|
||||
"noticeTitle": "Gestalten Sie PicPeak mit",
|
||||
"notice": "Optionale Nutzungsberichte zeigen, welche Funktionen für die Community wichtig sind. Die Übermittlung ist aus, bis Sie sich aktiv dafür entscheiden.",
|
||||
@@ -19,9 +22,9 @@
|
||||
"sectionFeedback": "Feedback ist getrennt",
|
||||
"fields": "Berichte enthalten einen Installationsfingerabdruck, PicPeak-Version, Berichtstag, Schema- und Signaturmetadaten, Galerie-Layouts sowie Konfiguriert/Genutzt-Werte für CRM und Unterfunktionen, Buchhaltung, Workflows, Newsletter, Gesichtserkennung, eigenes CSS, OAuth, SMTP, WhatsApp, Backups, S3 und eingebundene Freigaben. „Genutzt“ bedeutet seit der Teilnahme beobachtet, nicht wie häufig.",
|
||||
"excluded": "Automatische Berichte enthalten keine Galeriebesucher, Klickverläufe, Foto- oder Galerieanzahlen, Namen, E-Mail-Adressen, Domains, Dateinamen oder Zugangsdaten.",
|
||||
"transport": "Dein PicPeak-Backend verwahrt den Signaturschlüssel und sendet signierte Berichte an {{collector}}, einmal pro UTC-Tag bei Admin-Nutzung. Du kannst Berichte vorab ansehen und alle angenommenen Rohpakete herunterladen.",
|
||||
"visibility": "Der öffentliche Datensatz zeigt Funktionskombinationen und aggregierte Ergebnisse aller berichtenden Installationen, auch Gruppen mit nur einer Installation. Der Fingerabdruck ist pseudonym, nicht anonym. Bewahre deinen Abfrage-Hash vertraulich auf: Er ermöglicht lesenden Zugriff auf deine Rohpakete.",
|
||||
"deletion": "Deaktivieren stoppt die Erfassung sofort und fordert die Löschung deiner Berichte, Aggregatbeiträge, Rückmeldungen, veröffentlichten Wünsche/Empfehlungen, Stimmen und Sitzungen an. Ist der Dienst nicht erreichbar, bleiben nur die für die Löschung nötigen Zugangsdaten erhalten; die Oberfläche zeigt die ausstehende Löschung. Nach Bestätigung werden Hash und Schlüssel lokal gelöscht. Eine erneute Teilnahme erzeugt eine neue Identität. Der Dienst behält nur einen Einweg-Sperrwert, um wiederholte alte Registrierungen abzuweisen.",
|
||||
"transport": "Dein PicPeak-Backend verwahrt den Signaturschlüssel und sendet einmal pro UTC-Tag bei Admin-Nutzung signierte Nutzungsberichte an {{collector}}. Du kannst Berichte vorab ansehen und jeden eindeutig angenommenen Bericht genau wie beim ersten Empfang herunterladen; Übertragungswiederholungen werden zusammengeführt. Abgelehnte Versuche und getrennt gesendetes Feedback gehören nicht zu diesem Berichtsexport.",
|
||||
"visibility": "Nur teilnehmende Installationen können den Funktionsdatensatz und aggregierte Ergebnisse einsehen, auch Gruppen mit nur einer Installation. Schema und Quellcode sind öffentlich; geprüfte Funktionswünsche und Empfehlungen werden nur mit Erlaubnis ihrer Verfasser veröffentlicht. Dein Fingerabdruck ist pseudonym, nicht anonym. Bewahre deinen Abfrage-Hash vertraulich auf: Er ermöglicht lesenden Zugriff auf deine eigenen Berichte und den Teilnehmerdatensatz.",
|
||||
"deletion": "Deaktivieren stoppt die Erfassung sofort und fordert die Löschung der Berichte, Aggregatbeiträge, Rückmeldungen, Veröffentlichungen, Stimmen und Sitzungen an. Bei einem Ausfall bleiben nur die zur Löschung nötigen Zugangsdaten erhalten; die Oberfläche zeigt die ausstehende Löschung. Nach Bestätigung werden Hash und Schlüssel lokal gelöscht; eine erneute Teilnahme erzeugt eine neue Identität. Der Collector behält einen Einweg-Sperrwert und kurzlebige Missbrauchszähler ohne Installationsbezug. PicPeak speichert eine herunterladbare lokale Löschquittung ohne den alten Hash, Schlüssel oder Inhalte.",
|
||||
"feedbackDisclosure": "Feedback wird getrennt von automatischen Berichten und nur beim Absenden übertragen. Jeder Beitrag ist anonym, sofern du keinen Namen angibst, und nur für Betreuer sichtbar, sofern du die Veröffentlichung nicht ausdrücklich erlaubst. Öffentliche Beiträge werden geprüft. Die Verwendung einer Empfehlung für Marketing benötigt eine zusätzliche Erlaubnis.",
|
||||
"consentCheck": "Ich habe diese Hinweise gelesen und stimme der Teilnahme ausdrücklich zu.",
|
||||
"enable": "Produktnutzung aktivieren",
|
||||
@@ -36,8 +39,8 @@
|
||||
"signingKeyUnreadable": "Der Signaturschlüssel für die Nutzungsdaten kann nicht gelesen werden. Meist wurde USAGE_ENCRYPTION_KEY — oder das als Rückfallwert genutzte JWT_SECRET — geändert. Berichte können nicht gesendet und auch die Löschanfrage kann nicht signiert werden. Stellen Sie das ursprüngliche Schlüsselmaterial wieder her, um die Löschung abzuschließen; erneutes Senden oder Deaktivieren allein behebt dies nicht.",
|
||||
"inspect": "Genau sehen, was geteilt wird",
|
||||
"preview": "Nächsten Bericht ansehen",
|
||||
"lastPacket": "Zuletzt angenommenes signiertes Paket",
|
||||
"export": "Alle Rohpakete herunterladen",
|
||||
"lastPacket": "Zuletzt angenommener signierter Nutzungsbericht",
|
||||
"export": "Alle angenommenen Nutzungsberichte herunterladen",
|
||||
"connect": "Mit Wünschen & Abstimmungen verbinden",
|
||||
"openPortal": "Portal öffnen (Abstimmungssitzung für 15 Minuten)",
|
||||
"queued": "Der Vorgang ist zur erneuten Übertragung gespeichert. Der Empfang ist noch nicht bestätigt.",
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
{
|
||||
"productUsage": {
|
||||
"auditTitle": "Export and deletion receipts",
|
||||
"auditDescription": "Download your private audit receipts. PicPeak keeps only the latest export receipt during participation and the latest deletion confirmation. These contain no installation hash, key or report/feedback content. Opt-out removes the local export receipt; the identity-free deletion confirmation remains. The collector does not keep an export/access history.",
|
||||
"auditDownload": "Download privacy receipts",
|
||||
"title": "Product usage & feedback",
|
||||
"noticeTitle": "Help shape PicPeak",
|
||||
"notice": "Optional product usage reports show which features matter to the community. Reporting is off until you choose to participate.",
|
||||
@@ -19,9 +22,9 @@
|
||||
"sectionFeedback": "Feedback is separate",
|
||||
"fields": "Reports contain an installation fingerprint, PicPeak version, report day, schema and signing metadata, gallery layout choices, and configured/used booleans for CRM and its subfeatures, accounting, workflows, newsletters, face recognition, custom CSS, OAuth, SMTP, WhatsApp, backups, S3, and share mounts. “Used” means observed since joining, not how often.",
|
||||
"excluded": "No gallery visitors, clickstreams, photo or gallery counts, names, emails, domains, filenames, or configuration secrets are included in automatic usage reports.",
|
||||
"transport": "Your PicPeak backend keeps the signing key and sends signed reports to {{collector}} once per UTC day when an admin uses the app. You can preview reports and download every accepted raw packet.",
|
||||
"visibility": "The public dataset shows feature combinations and aggregate results from all reporting installations, including groups containing just one installation. The fingerprint is pseudonymous, not anonymous. Keep your lookup hash private: it grants read-only access to your raw packets.",
|
||||
"deletion": "Disabling immediately stops collection and requests deletion of your remote reports, aggregate contributions, feedback, published requests/testimonials, votes, and sessions. If the collector is unavailable, only the credentials needed to finish deletion remain and the UI shows deletion pending. After confirmation, the local hash and key are erased. Joining again creates a new identity. The collector retains only a one-way revocation digest to prevent old registrations being replayed.",
|
||||
"transport": "Your PicPeak backend keeps the signing key and sends signed usage reports to {{collector}} once per UTC day during admin use. Preview reports and download each unique accepted report exactly as first received; transport retries are deduplicated. Rejected attempts and separately submitted feedback are not part of this report export.",
|
||||
"visibility": "Only participating installations can inspect the feature dataset and aggregate results, including groups of one. The schema and source are public; approved feature requests and testimonials are public only with their authors’ permission. Your fingerprint is pseudonymous, not anonymous. Keep your lookup hash private: it grants read-only access to your own reports and the participant dataset.",
|
||||
"deletion": "Disabling immediately stops collection and requests deletion of reports, aggregate contributions, feedback, published items, votes and sessions. During an outage, only credentials needed to finish deletion remain and the UI shows deletion pending. After confirmation, the local hash and key are erased; rejoining creates a new identity. The collector retains a one-way revocation digest and short-lived identity-free abuse counters. PicPeak keeps a downloadable local deletion receipt without the old hash, key or payloads.",
|
||||
"feedbackDisclosure": "Feedback is separate from automatic reports and is sent only when you submit it. Each item is anonymous unless you include a name, and private to maintainers unless you explicitly permit publication. Public items require maintainer review. Marketing use of a testimonial requires separate permission.",
|
||||
"consentCheck": "I have read this disclosure and explicitly agree to participate.",
|
||||
"enable": "Enable product usage",
|
||||
@@ -36,8 +39,8 @@
|
||||
"signingKeyUnreadable": "The usage signing key cannot be read, which usually means USAGE_ENCRYPTION_KEY — or JWT_SECRET, which it falls back to — was changed. Reports cannot be sent and the deletion request cannot be signed either. Restore the original encryption material to finish deletion; retrying or disabling will not resolve it on its own.",
|
||||
"inspect": "See exactly what is shared",
|
||||
"preview": "Preview next report",
|
||||
"lastPacket": "Last accepted signed packet",
|
||||
"export": "Download all raw packets",
|
||||
"lastPacket": "Last accepted signed usage report",
|
||||
"export": "Download all accepted usage reports",
|
||||
"connect": "Connect to requests & voting",
|
||||
"openPortal": "Open the portal (15-minute voting session)",
|
||||
"queued": "The operation is saved for retry. It has not been confirmed as delivered.",
|
||||
|
||||
@@ -16,6 +16,7 @@ export interface UsageStatus {
|
||||
last_error: string | null;
|
||||
pending_action: string | null;
|
||||
last_packet: unknown;
|
||||
privacy_receipts?: Record<string, unknown>;
|
||||
feedback_preferences: { name: string };
|
||||
}
|
||||
export interface ProductFeedback {
|
||||
|
||||
Reference in New Issue
Block a user