memoir-cli 3.17.4 → 3.17.5
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "memoir-cli",
|
|
3
|
-
"version": "3.17.
|
|
3
|
+
"version": "3.17.5",
|
|
4
4
|
"mcpName": "io.github.camgitt/memoir",
|
|
5
5
|
"description": "Portable, project-scoped memory and session handoffs for coding agents. Readable files, MCP recall, and optional user-passphrase encrypted backups.",
|
|
6
6
|
"main": "src/index.js",
|
package/src/cloud/auth.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import fs from 'fs-extra';
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import os from 'os';
|
|
4
|
-
import { SUPABASE_URL, SUPABASE_ANON_KEY, STORAGE_BUCKET } from './constants.js';
|
|
4
|
+
import { SUPABASE_URL, SUPABASE_ANON_KEY, STORAGE_BUCKET, SUPPORT_EMAIL } from './constants.js';
|
|
5
5
|
|
|
6
6
|
const isWin = process.platform === 'win32';
|
|
7
7
|
const configDir = isWin
|
|
@@ -183,6 +183,19 @@ export async function deleteAccount(session) {
|
|
|
183
183
|
'Prefer': 'return=representation',
|
|
184
184
|
};
|
|
185
185
|
|
|
186
|
+
// Deleting data does not cancel a Stripe subscription, and the billing
|
|
187
|
+
// webhook re-creates the subscription row on the next renewal. Stop before
|
|
188
|
+
// deleting anything while the account is paying, or while the plan can't be read.
|
|
189
|
+
let sub;
|
|
190
|
+
try {
|
|
191
|
+
sub = await getSubscription(session);
|
|
192
|
+
} catch (error) {
|
|
193
|
+
throw new Error(`Could not check your plan (${error.message.replace(/ Existing backups were not pruned\.$/, '')}). Nothing was deleted; retry account deletion.`);
|
|
194
|
+
}
|
|
195
|
+
if (sub.status === 'pro') {
|
|
196
|
+
throw new Error(`Your Pro subscription is still active, and deleting your data would not stop billing. Email ${SUPPORT_EMAIL} to cancel it, then run this again. Nothing was deleted.`);
|
|
197
|
+
}
|
|
198
|
+
|
|
186
199
|
const backupsRes = await fetch(
|
|
187
200
|
`${SUPABASE_URL}/rest/v1/backups?select=*&user_id=eq.${encodeURIComponent(session.user.id)}`,
|
|
188
201
|
{ headers }
|
|
@@ -213,19 +226,20 @@ export async function deleteAccount(session) {
|
|
|
213
226
|
await requireRowDelete(rowRes, 'Backup metadata deletion', { expectedMinRows: 1 });
|
|
214
227
|
}
|
|
215
228
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
229
|
+
// Share bundles go before their rows, like backups above: if a bundle
|
|
230
|
+
// delete fails, the row is still there for the retry to find.
|
|
231
|
+
const linksRes = await fetch(
|
|
232
|
+
`${SUPABASE_URL}/rest/v1/shared_links?select=id,backup_id&created_by=eq.${encodeURIComponent(session.user.id)}`,
|
|
233
|
+
{ headers }
|
|
219
234
|
);
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
await verifyNoneRemaining(
|
|
223
|
-
`/rest/v1/shared_links?select=id&created_by=eq.${encodeURIComponent(session.user.id)}&limit=1`,
|
|
224
|
-
headers,
|
|
225
|
-
'Shared-link deletion'
|
|
226
|
-
);
|
|
235
|
+
if (!linksRes.ok) {
|
|
236
|
+
throw new Error(`Could not enumerate shared links (${linksRes.status}). Nothing else was reported as deleted; retry account deletion.`);
|
|
227
237
|
}
|
|
228
|
-
|
|
238
|
+
const links = await linksRes.json();
|
|
239
|
+
if (!Array.isArray(links)) {
|
|
240
|
+
throw new Error('Could not enumerate shared links. Nothing else was reported as deleted; retry account deletion.');
|
|
241
|
+
}
|
|
242
|
+
for (const link of links) {
|
|
229
243
|
if (typeof link?.backup_id === 'string' && link.backup_id.startsWith(session.user.id + '/')) {
|
|
230
244
|
const shareObjRes = await fetch(`${SUPABASE_URL}/storage/v1/object/${STORAGE_BUCKET}`, {
|
|
231
245
|
method: 'DELETE',
|
|
@@ -239,6 +253,19 @@ export async function deleteAccount(session) {
|
|
|
239
253
|
}
|
|
240
254
|
}
|
|
241
255
|
|
|
256
|
+
const sharedRes = await fetch(
|
|
257
|
+
`${SUPABASE_URL}/rest/v1/shared_links?created_by=eq.${encodeURIComponent(session.user.id)}`,
|
|
258
|
+
{ method: 'DELETE', headers: deleteHeaders }
|
|
259
|
+
);
|
|
260
|
+
const deletedLinks = await requireRowDelete(sharedRes, 'Shared-link deletion');
|
|
261
|
+
if (deletedLinks.length < links.length) {
|
|
262
|
+
await verifyNoneRemaining(
|
|
263
|
+
`/rest/v1/shared_links?select=id&created_by=eq.${encodeURIComponent(session.user.id)}&limit=1`,
|
|
264
|
+
headers,
|
|
265
|
+
'Shared-link deletion'
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
|
|
242
269
|
const subRes = await fetch(
|
|
243
270
|
`${SUPABASE_URL}/rest/v1/subscriptions?user_id=eq.${encodeURIComponent(session.user.id)}`,
|
|
244
271
|
{ method: 'DELETE', headers: deleteHeaders }
|
package/src/cloud/constants.js
CHANGED
|
@@ -7,3 +7,6 @@ export const STORAGE_BUCKET = 'memoir-backups';
|
|
|
7
7
|
// Free is a safety net; Pro is "full version history" (memoir.sh pricing).
|
|
8
8
|
export const MAX_BACKUPS_FREE = 10;
|
|
9
9
|
export const MAX_BACKUPS_PRO = 100;
|
|
10
|
+
// Public contact on memoir.sh (forwards via ImprovMX). Used where the CLI
|
|
11
|
+
// can't finish a job itself, e.g. cancelling a Pro subscription.
|
|
12
|
+
export const SUPPORT_EMAIL = 'hello@memoir.sh';
|
package/src/commands/restore.js
CHANGED
|
@@ -6,13 +6,12 @@ import ora from 'ora';
|
|
|
6
6
|
import boxen from 'boxen';
|
|
7
7
|
import gradient from 'gradient-string';
|
|
8
8
|
import inquirer from 'inquirer';
|
|
9
|
-
import { readSafeFile, writeSafeFile
|
|
9
|
+
import { readSafeFile, writeSafeFile } from '../security/files.js';
|
|
10
10
|
import { getConfig, autoSetup } from '../config.js';
|
|
11
11
|
import { fetchFromLocal, fetchFromGit } from '../providers/restore.js';
|
|
12
12
|
import { decryptDirectory, verifyPassphrase } from '../security/encryption.js';
|
|
13
13
|
import { detectLocalHomeKey } from '../adapters/restore.js';
|
|
14
14
|
import { restoreWorkspace } from '../workspace/tracker.js';
|
|
15
|
-
import { getSession } from '../cloud/auth.js';
|
|
16
15
|
import { unbundleToDir, readBoundedResponse } from '../cloud/storage.js';
|
|
17
16
|
import { SUPABASE_URL, SUPABASE_ANON_KEY, STORAGE_BUCKET } from '../cloud/constants.js';
|
|
18
17
|
import { readSession, writeSession, mergeSessions, paths as sessionPaths } from '../session/state.js';
|
|
@@ -372,16 +371,40 @@ async function restoreFromShare(options) {
|
|
|
372
371
|
// Download the backup
|
|
373
372
|
spinner.start(chalk.gray('Downloading share bundle...'));
|
|
374
373
|
|
|
375
|
-
//
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
374
|
+
// Share bundles are not readable through Storage directly: a read rule
|
|
375
|
+
// for recipients would also let anyone list every bundle name, and each
|
|
376
|
+
// name contains its token. The share-download function checks the exact
|
|
377
|
+
// token, uses up one use, and returns a short-lived signed URL.
|
|
378
|
+
const grantRes = await fetch(`${SUPABASE_URL}/functions/v1/share-download`, {
|
|
379
|
+
method: 'POST',
|
|
380
|
+
headers: {
|
|
381
|
+
'apikey': SUPABASE_ANON_KEY,
|
|
382
|
+
'Authorization': `Bearer ${SUPABASE_ANON_KEY}`,
|
|
383
|
+
'Content-Type': 'application/json',
|
|
384
|
+
},
|
|
385
|
+
body: JSON.stringify({ token: rawToken }),
|
|
383
386
|
});
|
|
387
|
+
if (grantRes.status === 404) {
|
|
388
|
+
spinner.stop();
|
|
389
|
+
console.log('\n' + boxen(
|
|
390
|
+
chalk.red('✖ Share link no longer available\n\n') +
|
|
391
|
+
chalk.gray('It may have expired, been used up, or been deleted.'),
|
|
392
|
+
{ padding: 1, borderStyle: 'round', borderColor: 'red' }
|
|
393
|
+
) + '\n');
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
if (!grantRes.ok) {
|
|
397
|
+
throw new Error(`Download failed (${grantRes.status})`);
|
|
398
|
+
}
|
|
399
|
+
const grant = await grantRes.json();
|
|
400
|
+
const signedUrl = typeof grant?.url === 'string' ? new URL(grant.url) : null;
|
|
401
|
+
if (!signedUrl
|
|
402
|
+
|| signedUrl.origin !== new URL(SUPABASE_URL).origin
|
|
403
|
+
|| !signedUrl.pathname.startsWith(`/storage/v1/object/sign/${STORAGE_BUCKET}/`)) {
|
|
404
|
+
throw new Error('Download failed: unexpected download URL');
|
|
405
|
+
}
|
|
384
406
|
|
|
407
|
+
const dlRes = await fetch(signedUrl);
|
|
385
408
|
if (!dlRes.ok) {
|
|
386
409
|
throw new Error(`Download failed: ${await dlRes.text()}`);
|
|
387
410
|
}
|
|
@@ -447,20 +470,6 @@ async function restoreFromShare(options) {
|
|
|
447
470
|
await fs.remove(decryptedDir);
|
|
448
471
|
}
|
|
449
472
|
|
|
450
|
-
// Increment use_count via token-scoped RPC
|
|
451
|
-
try {
|
|
452
|
-
const rpcHeaders = { 'apikey': SUPABASE_ANON_KEY, 'Content-Type': 'application/json' };
|
|
453
|
-
if (session) rpcHeaders['Authorization'] = `Bearer ${session.access_token}`;
|
|
454
|
-
|
|
455
|
-
await fetch(`${SUPABASE_URL}/rest/v1/rpc/memoir_consume_shared_link`, {
|
|
456
|
-
method: 'POST',
|
|
457
|
-
headers: rpcHeaders,
|
|
458
|
-
body: JSON.stringify({ link_token: rawToken }),
|
|
459
|
-
});
|
|
460
|
-
} catch {
|
|
461
|
-
// Best-effort — don't fail restore if count update fails
|
|
462
|
-
}
|
|
463
|
-
|
|
464
473
|
spinner.stop();
|
|
465
474
|
|
|
466
475
|
if (restored) {
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
-- Share bundles: replace the anonymous Storage read rule from
|
|
2
|
+
-- 202610180001 with the share-download edge function
|
|
3
|
+
-- (supabase/functions/share-download). The rule let anyone list every active
|
|
4
|
+
-- bundle, and each bundle name contains its share token.
|
|
5
|
+
drop policy if exists "Recipients read active shared bundles" on storage.objects;
|
|
6
|
+
drop function if exists public.memoir_can_read_shared_object(text);
|
|
7
|
+
|
|
8
|
+
-- Only share-download (service role) uses up a share now, at download time.
|
|
9
|
+
revoke execute on function public.memoir_consume_shared_link(text) from anon, authenticated;
|