hazo_files 3.0.0 → 3.1.0

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/dist/index.js CHANGED
@@ -31,6 +31,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
31
31
  var index_exports = {};
32
32
  __export(index_exports, {
33
33
  ALL_SYSTEM_VARIABLES: () => ALL_SYSTEM_VARIABLES,
34
+ AppFileServerProvider: () => AppFileServerProvider,
34
35
  AuthenticationError: () => AuthenticationError,
35
36
  ConfigurationError: () => ConfigurationError,
36
37
  DEFAULT_DATE_FORMATS: () => DEFAULT_DATE_FORMATS,
@@ -46,6 +47,7 @@ __export(index_exports, {
46
47
  FileTooLargeError: () => FileTooLargeError,
47
48
  GoogleDriveAuth: () => GoogleDriveAuth,
48
49
  GoogleDriveModule: () => GoogleDriveModule,
50
+ GoogleDriveProvider: () => GoogleDriveProvider,
49
51
  HAZO_FILES_DEFAULT_TABLE_NAME: () => HAZO_FILES_DEFAULT_TABLE_NAME,
50
52
  HAZO_FILES_MIGRATION_V2: () => HAZO_FILES_MIGRATION_V2,
51
53
  HAZO_FILES_MIGRATION_V3: () => HAZO_FILES_MIGRATION_V3,
@@ -53,6 +55,7 @@ __export(index_exports, {
53
55
  HAZO_FILES_NAMING_TABLE_SCHEMA: () => HAZO_FILES_NAMING_TABLE_SCHEMA,
54
56
  HAZO_FILES_TABLE_SCHEMA: () => HAZO_FILES_TABLE_SCHEMA,
55
57
  HazoFilesError: () => import_hazo_core2.HazoError,
58
+ InMemoryProvider: () => InMemoryProvider,
56
59
  InvalidExtensionError: () => InvalidExtensionError,
57
60
  InvalidPathError: () => InvalidPathError,
58
61
  LLMExtractionService: () => LLMExtractionService,
@@ -64,6 +67,9 @@ __export(index_exports, {
64
67
  SYSTEM_COUNTER_VARIABLES: () => SYSTEM_COUNTER_VARIABLES,
65
68
  SYSTEM_DATE_VARIABLES: () => SYSTEM_DATE_VARIABLES,
66
69
  SYSTEM_FILE_VARIABLES: () => SYSTEM_FILE_VARIABLES,
70
+ StorageCollisionExhausted: () => StorageCollisionExhausted,
71
+ StorageNotConfigured: () => StorageNotConfigured,
72
+ StorageUnavailable: () => StorageUnavailable,
67
73
  TrackedFileManager: () => TrackedFileManager,
68
74
  UploadExtractService: () => UploadExtractService,
69
75
  addExtractionToFileData: () => addExtractionToFileData,
@@ -20613,6 +20619,19 @@ var StorageCollisionExhausted = class extends Error {
20613
20619
  this.name = "StorageCollisionExhausted";
20614
20620
  }
20615
20621
  };
20622
+ var StorageNotConfigured = class extends Error {
20623
+ constructor() {
20624
+ super("Storage provider is not configured for this scope");
20625
+ this.name = "StorageNotConfigured";
20626
+ }
20627
+ };
20628
+ var StorageUnavailable = class extends Error {
20629
+ constructor(reason, message) {
20630
+ super(message);
20631
+ this.reason = reason;
20632
+ this.name = "StorageUnavailable";
20633
+ }
20634
+ };
20616
20635
 
20617
20636
  // src/naming/collision.ts
20618
20637
  async function writeWithCollisionRetry(opts) {
@@ -20646,9 +20665,403 @@ function removeRef(file2, ref_id, removed_at) {
20646
20665
  function countRefs(file2) {
20647
20666
  return file2.file_refs.filter((r) => !r.removed_at).length;
20648
20667
  }
20668
+
20669
+ // src/providers/app-file-server.ts
20670
+ var import_node_crypto = require("crypto");
20671
+ var import_node_fs = require("fs");
20672
+ var import_node_path = require("path");
20673
+ var AppFileServerProvider = class {
20674
+ constructor(opts) {
20675
+ this.provider_tag = "app_file_server";
20676
+ this.root = (0, import_node_path.normalize)(opts.root);
20677
+ this.secret = opts.hmac_secret;
20678
+ this.default_ttl = opts.default_ttl_seconds ?? 300;
20679
+ }
20680
+ resolve(path4) {
20681
+ const safe = (0, import_node_path.normalize)(path4).replace(/^([./\\])+/, "");
20682
+ if (safe.includes(`..${import_node_path.sep}`) || safe.startsWith("..")) {
20683
+ throw new Error(`Path escapes root: ${path4}`);
20684
+ }
20685
+ return (0, import_node_path.join)(this.root, safe);
20686
+ }
20687
+ async put(path4, body, opts) {
20688
+ const abs = this.resolve(path4);
20689
+ if (opts?.ifNotExists && (0, import_node_fs.existsSync)(abs)) {
20690
+ throw new Error(`File exists: ${path4}`);
20691
+ }
20692
+ (0, import_node_fs.mkdirSync)((0, import_node_path.dirname)(abs), { recursive: true });
20693
+ const buf = Buffer.isBuffer(body) ? body : await streamToBuffer(body);
20694
+ (0, import_node_fs.writeFileSync)(abs, buf);
20695
+ return { provider: this.provider_tag, native_id: path4, size: buf.length };
20696
+ }
20697
+ async get(path4) {
20698
+ return (0, import_node_fs.readFileSync)(this.resolve(path4));
20699
+ }
20700
+ async delete(path4) {
20701
+ const abs = this.resolve(path4);
20702
+ if ((0, import_node_fs.existsSync)(abs)) (0, import_node_fs.rmSync)(abs, { force: true });
20703
+ }
20704
+ async exists(path4) {
20705
+ return (0, import_node_fs.existsSync)(this.resolve(path4));
20706
+ }
20707
+ async getSignedUrl(path4, opts) {
20708
+ const ttl = opts?.ttl_seconds ?? this.default_ttl;
20709
+ const exp = Math.floor(Date.now() / 1e3) + ttl;
20710
+ const payload = `${path4}:${exp}`;
20711
+ const sig = (0, import_node_crypto.createHmac)("sha256", this.secret).update(payload).digest("base64url");
20712
+ const token = `${exp}.${sig}`;
20713
+ return `/api/files/serve/${token}/${path4}`;
20714
+ }
20715
+ /** Verify a token produced by `getSignedUrl`. Used by the `/api/files/serve` route. */
20716
+ verifySignedUrl(token, path4) {
20717
+ const [expStr, sig] = token.split(".");
20718
+ const exp = Number(expStr);
20719
+ if (!Number.isFinite(exp) || exp * 1e3 < Date.now()) return false;
20720
+ const expected = (0, import_node_crypto.createHmac)("sha256", this.secret).update(`${path4}:${exp}`).digest("base64url");
20721
+ return sig === expected;
20722
+ }
20723
+ async probe() {
20724
+ try {
20725
+ const test = (0, import_node_path.join)(this.root, ".hazo_probe");
20726
+ (0, import_node_fs.mkdirSync)(this.root, { recursive: true });
20727
+ (0, import_node_fs.writeFileSync)(test, "probe");
20728
+ (0, import_node_fs.statSync)(test);
20729
+ (0, import_node_fs.rmSync)(test, { force: true });
20730
+ return { ok: true };
20731
+ } catch (err) {
20732
+ return { ok: false, error: "write_denied", message: String(err) };
20733
+ }
20734
+ }
20735
+ async list(prefix) {
20736
+ const abs = this.resolve(prefix);
20737
+ if (!(0, import_node_fs.existsSync)(abs)) return [];
20738
+ const stat = (0, import_node_fs.statSync)(abs);
20739
+ if (!stat.isDirectory()) {
20740
+ return [{ path: prefix, name: (0, import_node_path.basename)(prefix), size: stat.size, isDirectory: false }];
20741
+ }
20742
+ const entries = [];
20743
+ this.walkDir(abs, prefix, entries);
20744
+ return entries;
20745
+ }
20746
+ walkDir(absDir, logicalDir, out) {
20747
+ const base = logicalDir.endsWith("/") ? logicalDir : `${logicalDir}/`;
20748
+ for (const item of (0, import_node_fs.readdirSync)(absDir, { withFileTypes: true })) {
20749
+ const logicalPath = `${base}${item.name}`;
20750
+ const absPath = (0, import_node_path.join)(absDir, item.name);
20751
+ if (item.isDirectory()) {
20752
+ out.push({ path: logicalPath, name: item.name, size: 0, isDirectory: true });
20753
+ this.walkDir(absPath, logicalPath, out);
20754
+ } else {
20755
+ const st = (0, import_node_fs.statSync)(absPath);
20756
+ out.push({ path: logicalPath, name: item.name, size: st.size, isDirectory: false });
20757
+ }
20758
+ }
20759
+ }
20760
+ async move(from, to) {
20761
+ const absFrom = this.resolve(from);
20762
+ const absTo = this.resolve(to);
20763
+ if (!(0, import_node_fs.existsSync)(absFrom)) throw new Error(`Not found: ${from}`);
20764
+ (0, import_node_fs.mkdirSync)((0, import_node_path.dirname)(absTo), { recursive: true });
20765
+ try {
20766
+ (0, import_node_fs.renameSync)(absFrom, absTo);
20767
+ } catch (err) {
20768
+ const e = err;
20769
+ if (e.code === "EXDEV") {
20770
+ (0, import_node_fs.copyFileSync)(absFrom, absTo);
20771
+ (0, import_node_fs.rmSync)(absFrom, { force: true });
20772
+ } else {
20773
+ throw err;
20774
+ }
20775
+ }
20776
+ }
20777
+ async rename(from, to) {
20778
+ return this.move(from, to);
20779
+ }
20780
+ };
20781
+ async function streamToBuffer(s) {
20782
+ const chunks = [];
20783
+ for await (const c of s) chunks.push(Buffer.isBuffer(c) ? c : Buffer.from(c));
20784
+ return Buffer.concat(chunks);
20785
+ }
20786
+
20787
+ // src/providers/in-memory.ts
20788
+ var InMemoryProvider = class {
20789
+ constructor() {
20790
+ this.provider_tag = "in_memory";
20791
+ this.store = /* @__PURE__ */ new Map();
20792
+ }
20793
+ async put(path4, body, opts) {
20794
+ if (opts?.ifNotExists && this.store.has(path4)) throw new Error(`File exists: ${path4}`);
20795
+ const buf = Buffer.isBuffer(body) ? body : await streamToBuffer2(body);
20796
+ this.store.set(path4, buf);
20797
+ return { provider: this.provider_tag, native_id: path4, size: buf.length };
20798
+ }
20799
+ async get(path4) {
20800
+ const buf = this.store.get(path4);
20801
+ if (!buf) throw new Error(`Not found: ${path4}`);
20802
+ return buf;
20803
+ }
20804
+ async delete(path4) {
20805
+ this.store.delete(path4);
20806
+ }
20807
+ async exists(path4) {
20808
+ return this.store.has(path4);
20809
+ }
20810
+ async getSignedUrl(path4, _opts) {
20811
+ const buf = this.store.get(path4);
20812
+ if (!buf) throw new Error(`Not found: ${path4}`);
20813
+ return `data:application/octet-stream;base64,${buf.toString("base64")}`;
20814
+ }
20815
+ async probe() {
20816
+ return { ok: true };
20817
+ }
20818
+ async list(prefix) {
20819
+ const base = prefix.replace(/\/$/, "");
20820
+ const matchPrefix = `${base}/`;
20821
+ const entries = [];
20822
+ const seenDirs = /* @__PURE__ */ new Set();
20823
+ for (const [key, buf] of this.store.entries()) {
20824
+ if (key !== base && !key.startsWith(matchPrefix)) continue;
20825
+ const rel = key.startsWith(matchPrefix) ? key.slice(matchPrefix.length) : "";
20826
+ if (!rel) {
20827
+ entries.push({ path: key, name: key.split("/").pop(), size: buf.length, isDirectory: false });
20828
+ continue;
20829
+ }
20830
+ const parts = rel.split("/");
20831
+ for (let i = 1; i < parts.length; i++) {
20832
+ const dirPath = matchPrefix + parts.slice(0, i).join("/");
20833
+ if (!seenDirs.has(dirPath)) {
20834
+ seenDirs.add(dirPath);
20835
+ entries.push({ path: dirPath, name: parts[i - 1], size: 0, isDirectory: true });
20836
+ }
20837
+ }
20838
+ entries.push({ path: key, name: parts[parts.length - 1], size: buf.length, isDirectory: false });
20839
+ }
20840
+ return entries;
20841
+ }
20842
+ async move(from, to) {
20843
+ const buf = this.store.get(from);
20844
+ if (!buf) throw new Error(`Not found: ${from}`);
20845
+ this.store.set(to, buf);
20846
+ this.store.delete(from);
20847
+ }
20848
+ async rename(from, to) {
20849
+ return this.move(from, to);
20850
+ }
20851
+ /** Test-only escape hatch — exposes the internal store for assertions. */
20852
+ snapshot() {
20853
+ return new Map(this.store);
20854
+ }
20855
+ };
20856
+ async function streamToBuffer2(s) {
20857
+ const chunks = [];
20858
+ for await (const c of s) chunks.push(Buffer.isBuffer(c) ? c : Buffer.from(c));
20859
+ return Buffer.concat(chunks);
20860
+ }
20861
+
20862
+ // src/providers/google-drive.ts
20863
+ var import_googleapis3 = require("googleapis");
20864
+ var GoogleDriveProvider = class {
20865
+ constructor(opts) {
20866
+ this.provider_tag = "gdrive";
20867
+ let credentials = {};
20868
+ try {
20869
+ credentials = JSON.parse(opts.service_account_json);
20870
+ } catch {
20871
+ }
20872
+ const auth = new import_googleapis3.google.auth.GoogleAuth({
20873
+ credentials,
20874
+ scopes: ["https://www.googleapis.com/auth/drive"]
20875
+ });
20876
+ this.drive = import_googleapis3.google.drive({ version: "v3", auth });
20877
+ this.driveId = opts.shared_drive_id;
20878
+ this.cache = opts.path_cache;
20879
+ }
20880
+ async probe() {
20881
+ try {
20882
+ await this.drive.drives.get({
20883
+ driveId: this.driveId,
20884
+ supportsAllDrives: true
20885
+ });
20886
+ return { ok: true };
20887
+ } catch (err) {
20888
+ const e = err;
20889
+ if (e?.code === 404) return { ok: false, error: "drive_not_shared", message: "SA cannot see Shared Drive" };
20890
+ if (e?.code === 403) return { ok: false, error: "write_denied", message: "SA lacks permission" };
20891
+ return { ok: false, error: "transient", message: String(err) };
20892
+ }
20893
+ }
20894
+ async put(path4, body, opts) {
20895
+ const segments = path4.split("/").filter(Boolean);
20896
+ const filename = segments.pop();
20897
+ const parentId = await this.resolvePath(segments, { create: true });
20898
+ if (opts?.ifNotExists) {
20899
+ const existing = await this.findChild(filename, parentId);
20900
+ if (existing) throw new Error(`File exists: ${path4}`);
20901
+ }
20902
+ const buf = Buffer.isBuffer(body) ? body : await streamToBuffer3(body);
20903
+ const res = await this.drive.files.create({
20904
+ requestBody: {
20905
+ name: filename,
20906
+ parents: [parentId],
20907
+ mimeType: opts?.contentType ?? "application/octet-stream"
20908
+ },
20909
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
20910
+ media: { body: buf },
20911
+ fields: "id, name"
20912
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
20913
+ });
20914
+ return { provider: this.provider_tag, native_id: res.data.id, size: buf.length };
20915
+ }
20916
+ async get(path4) {
20917
+ const id = await this.lookupFileId(path4);
20918
+ const res = await this.drive.files.get(
20919
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
20920
+ { fileId: id, alt: "media", supportsAllDrives: true },
20921
+ { responseType: "arraybuffer" }
20922
+ );
20923
+ return Buffer.from(res.data);
20924
+ }
20925
+ async delete(path4) {
20926
+ try {
20927
+ const id = await this.lookupFileId(path4);
20928
+ await this.drive.files.delete({ fileId: id, supportsAllDrives: true });
20929
+ } catch (err) {
20930
+ const e = err;
20931
+ if (e?.code !== 404) throw err;
20932
+ }
20933
+ }
20934
+ async exists(path4) {
20935
+ try {
20936
+ await this.lookupFileId(path4);
20937
+ return true;
20938
+ } catch {
20939
+ return false;
20940
+ }
20941
+ }
20942
+ async getSignedUrl(path4, _opts) {
20943
+ const id = await this.lookupFileId(path4);
20944
+ return `https://drive.google.com/uc?id=${id}&export=download`;
20945
+ }
20946
+ async list(prefix) {
20947
+ const segments = prefix.split("/").filter(Boolean);
20948
+ let parentId;
20949
+ try {
20950
+ parentId = await this.resolvePath(segments, { create: false });
20951
+ } catch {
20952
+ return [];
20953
+ }
20954
+ const res = await this.drive.files.list({
20955
+ q: `'${parentId}' in parents and trashed=false`,
20956
+ driveId: this.driveId,
20957
+ corpora: "drive",
20958
+ includeItemsFromAllDrives: true,
20959
+ fields: "files(id, name, mimeType, size)"
20960
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
20961
+ });
20962
+ const files = res.data.files ?? [];
20963
+ const base = prefix.endsWith("/") ? prefix : `${prefix}/`;
20964
+ return files.map((f) => ({
20965
+ path: `${base}${f.name}`,
20966
+ name: f.name,
20967
+ size: Number(f.size ?? 0),
20968
+ isDirectory: f.mimeType === "application/vnd.google-apps.folder"
20969
+ }));
20970
+ }
20971
+ async move(from, to) {
20972
+ const id = await this.lookupFileId(from);
20973
+ const fromSegs = from.split("/").filter(Boolean);
20974
+ fromSegs.pop();
20975
+ const toSegs = to.split("/").filter(Boolean);
20976
+ const newName = toSegs.pop();
20977
+ const oldParentId = await this.resolvePath(fromSegs, { create: false });
20978
+ const newParentId = await this.resolvePath(toSegs, { create: true });
20979
+ await this.drive.files.update({
20980
+ fileId: id,
20981
+ addParents: newParentId,
20982
+ removeParents: oldParentId,
20983
+ requestBody: { name: newName },
20984
+ supportsAllDrives: true,
20985
+ fields: "id, parents"
20986
+ });
20987
+ await this.cache.invalidate(from);
20988
+ }
20989
+ async rename(from, to) {
20990
+ const id = await this.lookupFileId(from);
20991
+ const segs = to.split("/").filter(Boolean);
20992
+ const newName = segs[segs.length - 1];
20993
+ await this.drive.files.update({
20994
+ fileId: id,
20995
+ requestBody: { name: newName },
20996
+ supportsAllDrives: true,
20997
+ fields: "id, name"
20998
+ });
20999
+ }
21000
+ async resolvePath(segments, opts) {
21001
+ let parentId = this.driveId;
21002
+ const accumulated = [];
21003
+ for (const segment of segments) {
21004
+ accumulated.push(segment);
21005
+ const cacheKey = accumulated.join("/");
21006
+ const cached2 = await this.cache.lookup(cacheKey);
21007
+ if (cached2) {
21008
+ parentId = cached2;
21009
+ continue;
21010
+ }
21011
+ let found = await this.findChild(segment, parentId, "application/vnd.google-apps.folder");
21012
+ if (!found && opts.create) {
21013
+ const created = await this.drive.files.create({
21014
+ requestBody: {
21015
+ name: segment,
21016
+ parents: [parentId],
21017
+ mimeType: "application/vnd.google-apps.folder"
21018
+ },
21019
+ fields: "id"
21020
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
21021
+ });
21022
+ found = created.data.id;
21023
+ }
21024
+ if (!found) throw new Error(`Folder not found and create=false: ${cacheKey}`);
21025
+ await this.cache.write(cacheKey, found);
21026
+ parentId = found;
21027
+ }
21028
+ return parentId;
21029
+ }
21030
+ async findChild(name, parentId, mimeType) {
21031
+ const q = [
21032
+ `name='${name.replace(/'/g, "\\'")}'`,
21033
+ `'${parentId}' in parents`,
21034
+ `trashed=false`
21035
+ ];
21036
+ if (mimeType) q.push(`mimeType='${mimeType}'`);
21037
+ const res = await this.drive.files.list({
21038
+ q: q.join(" and "),
21039
+ driveId: this.driveId,
21040
+ corpora: "drive",
21041
+ includeItemsFromAllDrives: true,
21042
+ fields: "files(id, name)"
21043
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
21044
+ });
21045
+ return res.data.files?.[0]?.id ?? null;
21046
+ }
21047
+ async lookupFileId(path4) {
21048
+ const segments = path4.split("/").filter(Boolean);
21049
+ const filename = segments.pop();
21050
+ const parentId = await this.resolvePath(segments, { create: false });
21051
+ const id = await this.findChild(filename, parentId);
21052
+ if (!id) throw Object.assign(new Error(`Not found: ${path4}`), { code: 404 });
21053
+ return id;
21054
+ }
21055
+ };
21056
+ async function streamToBuffer3(s) {
21057
+ const chunks = [];
21058
+ for await (const c of s) chunks.push(Buffer.isBuffer(c) ? c : Buffer.from(c));
21059
+ return Buffer.concat(chunks);
21060
+ }
20649
21061
  // Annotate the CommonJS export names for ESM import in node:
20650
21062
  0 && (module.exports = {
20651
21063
  ALL_SYSTEM_VARIABLES,
21064
+ AppFileServerProvider,
20652
21065
  AuthenticationError,
20653
21066
  ConfigurationError,
20654
21067
  DEFAULT_DATE_FORMATS,
@@ -20664,6 +21077,7 @@ function countRefs(file2) {
20664
21077
  FileTooLargeError,
20665
21078
  GoogleDriveAuth,
20666
21079
  GoogleDriveModule,
21080
+ GoogleDriveProvider,
20667
21081
  HAZO_FILES_DEFAULT_TABLE_NAME,
20668
21082
  HAZO_FILES_MIGRATION_V2,
20669
21083
  HAZO_FILES_MIGRATION_V3,
@@ -20671,6 +21085,7 @@ function countRefs(file2) {
20671
21085
  HAZO_FILES_NAMING_TABLE_SCHEMA,
20672
21086
  HAZO_FILES_TABLE_SCHEMA,
20673
21087
  HazoFilesError,
21088
+ InMemoryProvider,
20674
21089
  InvalidExtensionError,
20675
21090
  InvalidPathError,
20676
21091
  LLMExtractionService,
@@ -20682,6 +21097,9 @@ function countRefs(file2) {
20682
21097
  SYSTEM_COUNTER_VARIABLES,
20683
21098
  SYSTEM_DATE_VARIABLES,
20684
21099
  SYSTEM_FILE_VARIABLES,
21100
+ StorageCollisionExhausted,
21101
+ StorageNotConfigured,
21102
+ StorageUnavailable,
20685
21103
  TrackedFileManager,
20686
21104
  UploadExtractService,
20687
21105
  addExtractionToFileData,