@withone/cli 2.14.0 → 2.14.3

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/README.md CHANGED
@@ -487,7 +487,7 @@ one sync run stripe --full-refresh
487
487
  | `schedule add/list/status/remove/repair` | Cron-backed scheduled syncs with drift detection |
488
488
  | `remove <platform>` | Delete synced data (`--dry-run` to preview) |
489
489
 
490
- Change hooks (`onInsert`, `onUpdate`, `onChange`) fire per-page during sync — pipe to a shell command, a flow, or an event log. Root-array responses (e.g. Hacker News `/v0/topstories.json` → `[9129911, 9129199, ...]`) are supported by setting `resultsPath` to `""`, `"$"`, or `"."`; primitive elements are auto-wrapped as `{ [idField]: value }`. Run `one guide sync` or `one guide memory` for the full reference.
490
+ Change hooks (`onInsert`, `onUpdate`, `onChange`) fire per-page during sync — pipe to a shell command, a flow, or an event log. Shell commands in a profile (`transform` and hooks other than `"log"`) only run once approved for that project: commands you pass in `sync init --config` are approved automatically, and commands that arrive in a profile on disk (for example from a cloned repo) need `one sync approve <platform>`. A change to an approved command needs approving again. Root-array responses (e.g. Hacker News `/v0/topstories.json` → `[9129911, 9129199, ...]`) are supported by setting `resultsPath` to `""`, `"$"`, or `"."`; primitive elements are auto-wrapped as `{ [idField]: value }`. Run `one guide sync` or `one guide memory` for the full reference.
491
491
 
492
492
  ### `one relay`
493
493
 
@@ -15,23 +15,57 @@ import {
15
15
  } from "./chunk-4H5L3SBY.js";
16
16
 
17
17
  // src/commands/mem/migrate.ts
18
- import fs4 from "fs";
19
- import path4 from "path";
18
+ import fs5 from "fs";
19
+ import path5 from "path";
20
20
  import * as p from "@clack/prompts";
21
21
 
22
22
  // src/lib/memory/sync/profile.ts
23
+ import fs2 from "fs";
24
+ import path2 from "path";
25
+
26
+ // src/lib/memory/sync/names.ts
23
27
  import fs from "fs";
24
28
  import path from "path";
25
- var PROFILES_DIR = path.join(".one", "sync", "profiles");
29
+ var SAFE_SYNC_NAME = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
30
+ function isSafeSyncName(value) {
31
+ return typeof value === "string" && value.length <= 128 && SAFE_SYNC_NAME.test(value) && !value.includes("..");
32
+ }
33
+ var UnsafeSyncNameError = class extends Error {
34
+ constructor(kind, value) {
35
+ super(
36
+ `Invalid sync ${kind} name ${JSON.stringify(value)}: use letters, digits, ".", "_" or "-", starting with a letter or digit, and no "..".`
37
+ );
38
+ this.name = "UnsafeSyncNameError";
39
+ }
40
+ };
41
+ function assertSafeSyncNames(platform, model) {
42
+ if (!isSafeSyncName(platform)) throw new UnsafeSyncNameError("platform", platform);
43
+ if (model !== void 0 && !isSafeSyncName(model)) throw new UnsafeSyncNameError("model", model);
44
+ }
45
+ function ensureDirInsideProject(dirRel) {
46
+ fs.mkdirSync(dirRel, { recursive: true });
47
+ const root = fs.realpathSync(process.cwd());
48
+ const real = fs.realpathSync(dirRel);
49
+ if (real !== root && !real.startsWith(root + path.sep)) {
50
+ throw new Error(`Refusing to use ${dirRel}: it resolves outside the project (${real}).`);
51
+ }
52
+ return dirRel;
53
+ }
54
+
55
+ // src/lib/memory/sync/profile.ts
56
+ var PROFILES_DIR = path2.join(".one", "sync", "profiles");
26
57
  function profilePath(platform, model) {
27
- return path.join(PROFILES_DIR, `${platform}_${model}.json`);
58
+ assertSafeSyncNames(platform, model);
59
+ return path2.join(PROFILES_DIR, `${platform}_${model}.json`);
28
60
  }
29
61
  function readProfile(platform, model) {
30
62
  const filePath = profilePath(platform, model);
31
63
  try {
32
- if (!fs.existsSync(filePath)) return null;
33
- const raw = fs.readFileSync(filePath, "utf-8");
34
- return JSON.parse(raw);
64
+ if (!fs2.existsSync(filePath)) return null;
65
+ const raw = fs2.readFileSync(filePath, "utf-8");
66
+ const profile = JSON.parse(raw);
67
+ if (!isSafeSyncName(profile?.platform) || !isSafeSyncName(profile?.model)) return null;
68
+ return profile;
35
69
  } catch {
36
70
  return null;
37
71
  }
@@ -61,9 +95,9 @@ function writeProfile(profile) {
61
95
  if (!profile.pagination.type) {
62
96
  throw new Error("Missing required field: pagination.type");
63
97
  }
64
- fs.mkdirSync(PROFILES_DIR, { recursive: true });
98
+ fs2.mkdirSync(PROFILES_DIR, { recursive: true });
65
99
  const filePath = profilePath(profile.platform, profile.model);
66
- fs.writeFileSync(filePath, JSON.stringify(profile, null, 2));
100
+ fs2.writeFileSync(filePath, JSON.stringify(profile, null, 2));
67
101
  }
68
102
  async function resolveProfileConnectionKey(api, profile, cache) {
69
103
  if (profile.connectionKey) return profile.connectionKey;
@@ -76,18 +110,25 @@ async function resolveProfileConnectionKey(api, profile, cache) {
76
110
  return conn.key;
77
111
  }
78
112
  function writeDraftProfile(platform, model, draft) {
79
- fs.mkdirSync(PROFILES_DIR, { recursive: true });
113
+ fs2.mkdirSync(PROFILES_DIR, { recursive: true });
80
114
  const filePath = profilePath(platform, model);
81
- fs.writeFileSync(filePath, JSON.stringify(draft, null, 2));
115
+ fs2.writeFileSync(filePath, JSON.stringify(draft, null, 2));
82
116
  }
83
117
  function listProfiles(platform) {
84
- if (!fs.existsSync(PROFILES_DIR)) return [];
85
- const files = fs.readdirSync(PROFILES_DIR).filter((f) => f.endsWith(".json"));
118
+ if (!fs2.existsSync(PROFILES_DIR)) return [];
119
+ const files = fs2.readdirSync(PROFILES_DIR).filter((f) => f.endsWith(".json"));
86
120
  const profiles = [];
87
121
  for (const file of files) {
88
122
  try {
89
- const raw = fs.readFileSync(path.join(PROFILES_DIR, file), "utf-8");
123
+ const raw = fs2.readFileSync(path2.join(PROFILES_DIR, file), "utf-8");
90
124
  const profile = JSON.parse(raw);
125
+ if (!isSafeSyncName(profile?.platform) || !isSafeSyncName(profile?.model)) {
126
+ process.stderr.write(
127
+ `Skipping sync profile ${file}: platform and model must be plain names (letters, digits, ".", "_", "-", no "..").
128
+ `
129
+ );
130
+ continue;
131
+ }
91
132
  if (!platform || profile.platform === platform) {
92
133
  profiles.push(profile);
93
134
  }
@@ -121,25 +162,25 @@ function generateTemplate(platform, model, actionId) {
121
162
  }
122
163
 
123
164
  // src/lib/memory/sync/builtin-profiles.ts
124
- import fs2 from "fs";
125
- import path2 from "path";
165
+ import fs3 from "fs";
166
+ import path3 from "path";
126
167
  import { fileURLToPath } from "url";
127
168
  function getProfilesDir() {
128
169
  const thisFile = fileURLToPath(import.meta.url);
129
- const thisDir = path2.dirname(thisFile);
170
+ const thisDir = path3.dirname(thisFile);
130
171
  for (let i = 1; i <= 4; i++) {
131
- const candidate = path2.resolve(thisDir, ...Array(i).fill(".."), "profiles");
132
- if (fs2.existsSync(candidate)) return candidate;
172
+ const candidate = path3.resolve(thisDir, ...Array(i).fill(".."), "profiles");
173
+ if (fs3.existsSync(candidate)) return candidate;
133
174
  }
134
175
  return "";
135
176
  }
136
177
  function loadBuiltinProfile(platform, model) {
137
178
  const dir = getProfilesDir();
138
179
  if (!dir) return null;
139
- const filePath = path2.join(dir, platform, `${model}.json`);
180
+ const filePath = path3.join(dir, platform, `${model}.json`);
140
181
  try {
141
- if (!fs2.existsSync(filePath)) return null;
142
- const raw = fs2.readFileSync(filePath, "utf-8");
182
+ if (!fs3.existsSync(filePath)) return null;
183
+ const raw = fs3.readFileSync(filePath, "utf-8");
143
184
  return JSON.parse(raw);
144
185
  } catch {
145
186
  return null;
@@ -159,20 +200,20 @@ function listBuiltinProfiles(platform) {
159
200
  if (!dir) return [];
160
201
  const profiles = [];
161
202
  try {
162
- const platforms = platform ? [platform] : fs2.readdirSync(dir).filter((f) => {
203
+ const platforms = platform ? [platform] : fs3.readdirSync(dir).filter((f) => {
163
204
  try {
164
- return fs2.statSync(path2.join(dir, f)).isDirectory();
205
+ return fs3.statSync(path3.join(dir, f)).isDirectory();
165
206
  } catch {
166
207
  return false;
167
208
  }
168
209
  });
169
210
  for (const plat of platforms) {
170
- const platDir = path2.join(dir, plat);
171
- if (!fs2.existsSync(platDir)) continue;
172
- const files = fs2.readdirSync(platDir).filter((f) => f.endsWith(".json"));
211
+ const platDir = path3.join(dir, plat);
212
+ if (!fs3.existsSync(platDir)) continue;
213
+ const files = fs3.readdirSync(platDir).filter((f) => f.endsWith(".json"));
173
214
  for (const file of files) {
174
215
  try {
175
- const raw = fs2.readFileSync(path2.join(platDir, file), "utf-8");
216
+ const raw = fs3.readFileSync(path3.join(platDir, file), "utf-8");
176
217
  const profile = JSON.parse(raw);
177
218
  profiles.push(profile);
178
219
  } catch {
@@ -185,8 +226,8 @@ function listBuiltinProfiles(platform) {
185
226
  }
186
227
 
187
228
  // src/lib/memory/sync/db.ts
188
- import fs3 from "fs";
189
- import path3 from "path";
229
+ import fs4 from "fs";
230
+ import path4 from "path";
190
231
 
191
232
  // src/lib/memory/sync/sqlite-loader.ts
192
233
  var cached = null;
@@ -222,10 +263,10 @@ async function isSqliteAvailable() {
222
263
  }
223
264
 
224
265
  // src/lib/memory/sync/db.ts
225
- var DATA_DIR = path3.join(".one", "sync", "data");
266
+ var DATA_DIR = path4.join(".one", "sync", "data");
226
267
  function listSyncedPlatforms() {
227
- if (!fs3.existsSync(DATA_DIR)) return [];
228
- return fs3.readdirSync(DATA_DIR).filter((f) => f.endsWith(".db")).map((f) => f.replace(/\.db$/, ""));
268
+ if (!fs4.existsSync(DATA_DIR)) return [];
269
+ return fs4.readdirSync(DATA_DIR).filter((f) => f.endsWith(".db")).map((f) => f.replace(/\.db$/, ""));
229
270
  }
230
271
  function isDriverFault(err) {
231
272
  const code = err?.code;
@@ -255,8 +296,8 @@ function backupPathFor(dbPath, now = /* @__PURE__ */ new Date()) {
255
296
  }
256
297
  async function openDatabase(platform, opts = {}) {
257
298
  const Database = await loadSqlite();
258
- fs3.mkdirSync(DATA_DIR, { recursive: true });
259
- const dbPath = path3.join(DATA_DIR, `${platform}.db`);
299
+ fs4.mkdirSync(DATA_DIR, { recursive: true });
300
+ const dbPath = path4.join(DATA_DIR, `${platform}.db`);
260
301
  if (opts.readonly) {
261
302
  return new Database(dbPath, { readonly: true, fileMustExist: true });
262
303
  }
@@ -280,7 +321,7 @@ Rebuild it against this Node with:
280
321
  Underlying error: ${detail}`
281
322
  );
282
323
  }
283
- if (!fs3.existsSync(dbPath)) throw err;
324
+ if (!fs4.existsSync(dbPath)) throw err;
284
325
  if (passesIntegrityCheck(Database, dbPath)) {
285
326
  const detail = err instanceof Error ? err.message.split("\n")[0] : String(err);
286
327
  throw new Error(
@@ -290,7 +331,7 @@ Underlying error: ${detail}`
290
331
  );
291
332
  }
292
333
  const backupPath = backupPathFor(dbPath);
293
- fs3.renameSync(dbPath, backupPath);
334
+ fs4.renameSync(dbPath, backupPath);
294
335
  process.stderr.write(
295
336
  `Database at ${dbPath} failed its integrity check. Backup saved at ${backupPath}, starting fresh.
296
337
  `
@@ -303,12 +344,12 @@ Underlying error: ${detail}`
303
344
  return db;
304
345
  }
305
346
  function getDatabasePath(platform) {
306
- return path3.join(DATA_DIR, `${platform}.db`);
347
+ return path4.join(DATA_DIR, `${platform}.db`);
307
348
  }
308
349
  function getDatabaseSize(platform) {
309
350
  const dbPath = getDatabasePath(platform);
310
- if (!fs3.existsSync(dbPath)) return "0 B";
311
- const stats = fs3.statSync(dbPath);
351
+ if (!fs4.existsSync(dbPath)) return "0 B";
352
+ const stats = fs4.statSync(dbPath);
312
353
  const bytes = stats.size;
313
354
  if (bytes < 1024) return `${bytes} B`;
314
355
  if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
@@ -444,16 +485,16 @@ function countRecords(db, model) {
444
485
  }
445
486
  function deleteDatabase(platform) {
446
487
  const dbPath = getDatabasePath(platform);
447
- if (fs3.existsSync(dbPath)) fs3.unlinkSync(dbPath);
448
- if (fs3.existsSync(dbPath + "-wal")) fs3.unlinkSync(dbPath + "-wal");
449
- if (fs3.existsSync(dbPath + "-shm")) fs3.unlinkSync(dbPath + "-shm");
488
+ if (fs4.existsSync(dbPath)) fs4.unlinkSync(dbPath);
489
+ if (fs4.existsSync(dbPath + "-wal")) fs4.unlinkSync(dbPath + "-wal");
490
+ if (fs4.existsSync(dbPath + "-shm")) fs4.unlinkSync(dbPath + "-shm");
450
491
  }
451
492
 
452
493
  // src/lib/memory/sync/mem-writer.ts
453
- function resolveWildcardPath(root, path5) {
454
- const segments = path5.split("[]");
494
+ function resolveWildcardPath(root, path6) {
495
+ const segments = path6.split("[]");
455
496
  if (segments.length === 1) {
456
- return [getByDotPath(root, path5)];
497
+ return [getByDotPath(root, path6)];
457
498
  }
458
499
  const recurse = (value, idx) => {
459
500
  if (value === null || value === void 0) return [];
@@ -479,8 +520,8 @@ function resolveWildcardPath(root, path5) {
479
520
  function extractSearchableFromPaths(record, paths) {
480
521
  const parts = [];
481
522
  const perPath = [];
482
- for (const path5 of paths) {
483
- const values = resolveWildcardPath(record, path5);
523
+ for (const path6 of paths) {
524
+ const values = resolveWildcardPath(record, path6);
484
525
  const collected = [];
485
526
  const absorb = (v) => {
486
527
  if (v === null || v === void 0) return;
@@ -496,9 +537,9 @@ function extractSearchableFromPaths(record, paths) {
496
537
  for (const v of values) absorb(v);
497
538
  if (collected.length > 0) {
498
539
  parts.push(...collected);
499
- perPath.push({ path: path5, found: true, sample: collected.join(" ").slice(0, 80) });
540
+ perPath.push({ path: path6, found: true, sample: collected.join(" ").slice(0, 80) });
500
541
  } else {
501
- perPath.push({ path: path5, found: false, sample: "" });
542
+ perPath.push({ path: path6, found: false, sample: "" });
502
543
  }
503
544
  }
504
545
  const text = parts.join(" ").replace(/\s+/g, " ").trim();
@@ -616,9 +657,9 @@ function deriveFields(record, derive) {
616
657
  const out = {};
617
658
  if (!derive) return out;
618
659
  for (const [field, spec] of Object.entries(derive)) {
619
- const { path: path5, extract } = typeof spec === "string" ? { path: spec, extract: void 0 } : spec;
620
- if (!path5) continue;
621
- const values = resolveIdentityPath(record, path5).filter((v) => v !== null && v !== void 0 && typeof v !== "object");
660
+ const { path: path6, extract } = typeof spec === "string" ? { path: spec, extract: void 0 } : spec;
661
+ if (!path6) continue;
662
+ const values = resolveIdentityPath(record, path6).filter((v) => v !== null && v !== void 0 && typeof v !== "object");
622
663
  if (values.length === 0) continue;
623
664
  if (extract === "email") {
624
665
  const emails = values.flatMap((v) => identityValuesFor("email", v));
@@ -630,11 +671,11 @@ function deriveFields(record, derive) {
630
671
  }
631
672
  return out;
632
673
  }
633
- function tokenizeIdentityPath(path5) {
674
+ function tokenizeIdentityPath(path6) {
634
675
  const tokens = [];
635
676
  const re = /([^.[\]]+)|\[([^\]]*)\]/g;
636
677
  let m;
637
- while ((m = re.exec(path5)) !== null) {
678
+ while ((m = re.exec(path6)) !== null) {
638
679
  if (m[1] !== void 0) {
639
680
  tokens.push({ type: "field", name: m[1] });
640
681
  } else {
@@ -653,9 +694,9 @@ function tokenizeIdentityPath(path5) {
653
694
  }
654
695
  return tokens;
655
696
  }
656
- function resolveIdentityPath(root, path5) {
697
+ function resolveIdentityPath(root, path6) {
657
698
  let frontier = [root];
658
- for (const tok of tokenizeIdentityPath(path5)) {
699
+ for (const tok of tokenizeIdentityPath(path6)) {
659
700
  const next = [];
660
701
  for (const cur of frontier) {
661
702
  if (cur === null || cur === void 0) continue;
@@ -683,10 +724,10 @@ function resolveIdentityPath(root, path5) {
683
724
  }
684
725
  return frontier;
685
726
  }
686
- function keysForEntry(record, prefix, path5) {
687
- if (!prefix || !path5) return [];
727
+ function keysForEntry(record, prefix, path6) {
728
+ if (!prefix || !path6) return [];
688
729
  const out = [];
689
- for (const raw of resolveIdentityPath(record, path5)) {
730
+ for (const raw of resolveIdentityPath(record, path6)) {
690
731
  for (const value of identityValuesFor(prefix, raw)) out.push(`${prefix}:${value}`);
691
732
  }
692
733
  return out;
@@ -926,10 +967,10 @@ async function memMigrateCommand(flags) {
926
967
  }
927
968
  }
928
969
  }
929
- const dataDir = path4.join(".one", "sync", "data");
970
+ const dataDir = path5.join(".one", "sync", "data");
930
971
  let cleanupFiles = [];
931
- if (flags.cleanup && fs4.existsSync(dataDir)) {
932
- cleanupFiles = fs4.readdirSync(dataDir).map((f) => path4.join(dataDir, f));
972
+ if (flags.cleanup && fs5.existsSync(dataDir)) {
973
+ cleanupFiles = fs5.readdirSync(dataDir).map((f) => path5.join(dataDir, f));
933
974
  }
934
975
  let cleanupDeleted = false;
935
976
  if (flags.cleanup) {
@@ -943,7 +984,7 @@ async function memMigrateCommand(flags) {
943
984
  } else {
944
985
  for (const full of cleanupFiles) {
945
986
  try {
946
- fs4.unlinkSync(full);
987
+ fs5.unlinkSync(full);
947
988
  } catch {
948
989
  }
949
990
  }
@@ -1056,12 +1097,12 @@ function reviveStringifiedJson(row) {
1056
1097
  }
1057
1098
  function isBuiltinNewerThanInstalled(platform, model) {
1058
1099
  try {
1059
- const installed = path4.join(".one", "sync", "profiles", `${platform}_${model}.json`);
1060
- if (!fs4.existsSync(installed)) return void 0;
1061
- const installedStat = fs4.statSync(installed);
1100
+ const installed = path5.join(".one", "sync", "profiles", `${platform}_${model}.json`);
1101
+ if (!fs5.existsSync(installed)) return void 0;
1102
+ const installedStat = fs5.statSync(installed);
1062
1103
  const builtinPath = resolveBuiltinProfilePath(platform, model);
1063
1104
  if (!builtinPath) return void 0;
1064
- const builtinStat = fs4.statSync(builtinPath);
1105
+ const builtinStat = fs5.statSync(builtinPath);
1065
1106
  return builtinStat.mtimeMs > installedStat.mtimeMs;
1066
1107
  } catch {
1067
1108
  return void 0;
@@ -1070,11 +1111,13 @@ function isBuiltinNewerThanInstalled(platform, model) {
1070
1111
  function resolveBuiltinProfilePath(platform, model) {
1071
1112
  const dir = getProfilesDir();
1072
1113
  if (!dir) return null;
1073
- const candidate = path4.join(dir, platform, `${model}.json`);
1074
- return fs4.existsSync(candidate) ? candidate : null;
1114
+ const candidate = path5.join(dir, platform, `${model}.json`);
1115
+ return fs5.existsSync(candidate) ? candidate : null;
1075
1116
  }
1076
1117
 
1077
1118
  export {
1119
+ assertSafeSyncNames,
1120
+ ensureDirInsideProject,
1078
1121
  readProfile,
1079
1122
  writeProfile,
1080
1123
  resolveProfileConnectionKey,