@hydra-acp/archiver 0.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.
@@ -0,0 +1,111 @@
1
+ import { createHash } from "node:crypto";
2
+ export const SYNC_VERSION = 1;
3
+ // Canonical JSON for hashing: object keys sorted recursively. Arrays are
4
+ // left in order — the bundle's `history` array is order-significant.
5
+ function canonicalize(value) {
6
+ if (value === null || typeof value !== "object") {
7
+ return JSON.stringify(value);
8
+ }
9
+ if (Array.isArray(value)) {
10
+ return `[${value.map(canonicalize).join(",")}]`;
11
+ }
12
+ const entries = Object.entries(value).sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
13
+ return `{${entries
14
+ .map(([k, v]) => `${JSON.stringify(k)}:${canonicalize(v)}`)
15
+ .join(",")}}`;
16
+ }
17
+ // Fields that don't represent the session's content — they change on
18
+ // every export or are regenerated by the daemon on import. Excluding
19
+ // them makes the hash stable across the export → import → export
20
+ // round-trip, which is what stops the multi-machine ping-pong loop.
21
+ const BUNDLE_EPHEMERAL = new Set(["exportedAt", "exportedFrom"]);
22
+ const SESSION_EPHEMERAL = new Set([
23
+ "sessionId",
24
+ "upstreamSessionId",
25
+ "createdAt",
26
+ "updatedAt",
27
+ ]);
28
+ function stripEphemeral(bundle) {
29
+ if (!bundle || typeof bundle !== "object" || Array.isArray(bundle)) {
30
+ return bundle;
31
+ }
32
+ const out = {};
33
+ for (const [k, v] of Object.entries(bundle)) {
34
+ if (BUNDLE_EPHEMERAL.has(k)) {
35
+ continue;
36
+ }
37
+ if (k === "session" && v && typeof v === "object" && !Array.isArray(v)) {
38
+ const sessionOut = {};
39
+ for (const [sk, sv] of Object.entries(v)) {
40
+ if (SESSION_EPHEMERAL.has(sk)) {
41
+ continue;
42
+ }
43
+ sessionOut[sk] = sv;
44
+ }
45
+ out[k] = sessionOut;
46
+ }
47
+ else {
48
+ out[k] = v;
49
+ }
50
+ }
51
+ return out;
52
+ }
53
+ export function hashBundle(bundle) {
54
+ return ("sha256:" +
55
+ createHash("sha256")
56
+ .update(canonicalize(stripEphemeral(bundle)))
57
+ .digest("hex"));
58
+ }
59
+ export function wrap(bundle, lineageId, host, now = new Date()) {
60
+ return {
61
+ syncVersion: SYNC_VERSION,
62
+ lineageId,
63
+ uploadedAt: now.toISOString(),
64
+ uploadedBy: host,
65
+ bundleHash: hashBundle(bundle),
66
+ bundle,
67
+ };
68
+ }
69
+ export function unwrap(raw) {
70
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
71
+ throw new Error("envelope: expected JSON object");
72
+ }
73
+ const r = raw;
74
+ if (r.syncVersion !== SYNC_VERSION) {
75
+ throw new Error(`envelope: unsupported syncVersion ${String(r.syncVersion)}; expected ${SYNC_VERSION}`);
76
+ }
77
+ if (typeof r.lineageId !== "string" || !r.lineageId) {
78
+ throw new Error("envelope: missing lineageId");
79
+ }
80
+ if (typeof r.uploadedAt !== "string" || !r.uploadedAt) {
81
+ throw new Error("envelope: missing uploadedAt");
82
+ }
83
+ if (typeof r.bundleHash !== "string" || !r.bundleHash) {
84
+ throw new Error("envelope: missing bundleHash");
85
+ }
86
+ const by = r.uploadedBy;
87
+ if (!by ||
88
+ typeof by.host !== "string" ||
89
+ typeof by.user !== "string") {
90
+ throw new Error("envelope: missing uploadedBy.{host,user}");
91
+ }
92
+ return {
93
+ syncVersion: SYNC_VERSION,
94
+ lineageId: r.lineageId,
95
+ uploadedAt: r.uploadedAt,
96
+ uploadedBy: { host: by.host, user: by.user },
97
+ bundleHash: r.bundleHash,
98
+ bundle: r.bundle,
99
+ };
100
+ }
101
+ export function serialize(envelope) {
102
+ return Buffer.from(JSON.stringify(envelope), "utf8");
103
+ }
104
+ export function deserialize(data) {
105
+ const text = typeof data === "string" ? data : data.toString("utf8");
106
+ return unwrap(JSON.parse(text));
107
+ }
108
+ export function keyFor(lineageId) {
109
+ return `${lineageId}.hydra.archive`;
110
+ }
111
+ //# sourceMappingURL=envelope.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"envelope.js","sourceRoot":"","sources":["../src/envelope.ts"],"names":[],"mappings":"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"}
package/dist/index.js ADDED
@@ -0,0 +1,154 @@
1
+ #!/usr/bin/env node
2
+ import { ArchiveLoop } from "./archive-loop.js";
3
+ import { makeBackend } from "./backend/factory.js";
4
+ import { ArchiverBridge } from "./bridge.js";
5
+ import { runColdSweep } from "./cold-sweep.js";
6
+ import { loadConfig, loadLoginConfig } from "./config.js";
7
+ import { DaemonClient } from "./daemon.js";
8
+ import { HydraDiscovery } from "./discovery.js";
9
+ import { runGoogleLogin } from "./oauth/google.js";
10
+ import { PullLoop } from "./pull-loop.js";
11
+ import { DEFAULT_RULE, loadRule } from "./rule.js";
12
+ import { SyncState } from "./state.js";
13
+ import { logger, setDebug } from "./util/log.js";
14
+ const log = logger("main");
15
+ const USAGE = `usage: hydra-acp-archiver [<command>]
16
+
17
+ Commands:
18
+ (no args) Run as a daemon-managed extension (the daemon spawns it
19
+ this way automatically when registered).
20
+ login Interactive Google OAuth flow — opens a browser and writes
21
+ the refresh token to ~/.hydra-acp/archiver-google-token.json.
22
+ `;
23
+ const TRUTHY = new Set(["1", "true", "yes", "on", "t"]);
24
+ async function runLogin() {
25
+ setDebug(TRUTHY.has((process.env.DEBUG ?? "").toLowerCase()));
26
+ const cfg = loadLoginConfig();
27
+ log.info(`credentials=${cfg.credentialsPath} token=${cfg.tokenPath}`);
28
+ await runGoogleLogin({
29
+ credentialsPath: cfg.credentialsPath,
30
+ tokenPath: cfg.tokenPath,
31
+ });
32
+ log.info("login complete — you can now start the archiver extension");
33
+ }
34
+ async function runExtension() {
35
+ const config = loadConfig();
36
+ setDebug(config.debug);
37
+ const state = new SyncState(config.statePath);
38
+ await state.load();
39
+ const backend = makeBackend(config);
40
+ await backend.init();
41
+ const daemon = new DaemonClient({
42
+ daemonUrl: config.hydraDaemonUrl,
43
+ token: config.hydraToken,
44
+ });
45
+ let currentRule = DEFAULT_RULE;
46
+ currentRule = await loadRule(config.ruleConfigPath);
47
+ const archive = new ArchiveLoop({
48
+ daemon,
49
+ backend,
50
+ state,
51
+ getRule: () => currentRule,
52
+ debounceMs: config.uploadDebounceMs,
53
+ });
54
+ const pull = new PullLoop({
55
+ daemon,
56
+ backend,
57
+ state,
58
+ intervalMs: config.pullIntervalMs,
59
+ });
60
+ pull.start();
61
+ // Backfill: archive every cold session the daemon knows about.
62
+ // Runs in the background so the rest of startup (discovery,
63
+ // bridges) doesn't block on it. Hash dedup means restarts are cheap.
64
+ void runColdSweep({
65
+ daemonUrl: config.hydraDaemonUrl,
66
+ token: config.hydraToken,
67
+ archive,
68
+ }).catch((err) => {
69
+ log.warn(`cold sweep failed: ${err.message}`);
70
+ });
71
+ const bridges = new Map();
72
+ const discovery = new HydraDiscovery({
73
+ daemonUrl: config.hydraDaemonUrl,
74
+ token: config.hydraToken,
75
+ pollIntervalMs: config.hydraPollIntervalMs,
76
+ onAdd: (session) => {
77
+ if (bridges.has(session.sessionId)) {
78
+ return;
79
+ }
80
+ log.info(`attaching to ${session.sessionId} agent=${session.agentId ?? "?"} cwd=${session.cwd}`);
81
+ const bridge = new ArchiverBridge({
82
+ daemonWsUrl: config.hydraWsUrl,
83
+ token: config.hydraToken,
84
+ sessionId: session.sessionId,
85
+ meta: {
86
+ ...(session.cwd !== undefined ? { cwd: session.cwd } : {}),
87
+ ...(session.agentId !== undefined
88
+ ? { agentId: session.agentId }
89
+ : {}),
90
+ ...(session.title !== undefined ? { title: session.title } : {}),
91
+ },
92
+ archive,
93
+ });
94
+ bridges.set(session.sessionId, bridge);
95
+ bridge.start();
96
+ },
97
+ onRemove: (sessionId) => {
98
+ const bridge = bridges.get(sessionId);
99
+ if (!bridge) {
100
+ return;
101
+ }
102
+ log.info(`detaching from ${sessionId}`);
103
+ bridges.delete(sessionId);
104
+ bridge.stop();
105
+ },
106
+ });
107
+ discovery.start();
108
+ process.on("SIGHUP", () => {
109
+ log.info(`SIGHUP — reloading rule from ${config.ruleConfigPath}`);
110
+ loadRule(config.ruleConfigPath)
111
+ .then((rule) => {
112
+ currentRule = rule;
113
+ log.info("rule reload complete");
114
+ })
115
+ .catch((err) => {
116
+ log.warn(`rule reload failed: ${err.message}`);
117
+ });
118
+ });
119
+ const shutdown = (sig) => {
120
+ log.info(`${sig} received — shutting down`);
121
+ discovery.stop();
122
+ pull.stop();
123
+ archive.stop();
124
+ for (const bridge of bridges.values()) {
125
+ bridge.stop();
126
+ }
127
+ setTimeout(() => process.exit(0), 200).unref();
128
+ };
129
+ process.on("SIGINT", () => shutdown("SIGINT"));
130
+ process.on("SIGTERM", () => shutdown("SIGTERM"));
131
+ log.info(`hydra-acp-archiver up; daemon=${config.hydraDaemonUrl} backend=${config.backend} rule=${config.ruleConfigPath}`);
132
+ }
133
+ async function main() {
134
+ const cmd = process.argv[2];
135
+ if (cmd === undefined) {
136
+ await runExtension();
137
+ return;
138
+ }
139
+ if (cmd === "login") {
140
+ await runLogin();
141
+ return;
142
+ }
143
+ if (cmd === "-h" || cmd === "--help" || cmd === "help") {
144
+ process.stdout.write(USAGE);
145
+ return;
146
+ }
147
+ process.stderr.write(`hydra-acp-archiver: unknown command "${cmd}"\n\n${USAGE}`);
148
+ process.exit(2);
149
+ }
150
+ main().catch((err) => {
151
+ process.stderr.write(`hydra-acp-archiver: ${err.message}\n`);
152
+ process.exit(1);
153
+ });
154
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,193 @@
1
+ import { spawn } from "node:child_process";
2
+ import { createServer } from "node:http";
3
+ import { chmod, mkdir, readFile, writeFile } from "node:fs/promises";
4
+ import { dirname } from "node:path";
5
+ import { google } from "googleapis";
6
+ import { logger } from "../util/log.js";
7
+ const log = logger("oauth");
8
+ // drive.file is a non-sensitive scope: the app can only see files it
9
+ // creates plus those a user explicitly hands to it via the picker. This
10
+ // keeps the verification footprint small and the user's other Drive
11
+ // content invisible to the archiver.
12
+ const SCOPES = ["https://www.googleapis.com/auth/drive.file"];
13
+ const CREDS_HINT = `
14
+ Set up Google OAuth credentials first:
15
+ 1. Visit https://console.cloud.google.com/
16
+ 2. Create or select a project, then enable the Google Drive API
17
+ 3. Configure OAuth consent screen (User type: External; add yourself as a Test User)
18
+ 4. Create credentials -> OAuth client ID -> Application type: Desktop app
19
+ 5. Download the JSON and save it to ~/.hydra-acp/archiver-google-credentials.json
20
+ (or set HYDRA_ACP_ARCHIVER_GOOGLE_CREDENTIALS)
21
+ `;
22
+ async function loadCredentials(path) {
23
+ let text;
24
+ try {
25
+ text = await readFile(path, "utf8");
26
+ }
27
+ catch (err) {
28
+ const e = err;
29
+ if (e.code === "ENOENT") {
30
+ throw new Error(`OAuth credentials file not found at ${path}.\n${CREDS_HINT}`);
31
+ }
32
+ throw err;
33
+ }
34
+ const parsed = JSON.parse(text);
35
+ const c = parsed.installed ?? parsed.web;
36
+ if (!c || !c.client_id || !c.client_secret) {
37
+ throw new Error(`OAuth credentials at ${path} missing client_id/client_secret.\n${CREDS_HINT}`);
38
+ }
39
+ return { client_id: c.client_id, client_secret: c.client_secret };
40
+ }
41
+ async function loadToken(path) {
42
+ try {
43
+ const text = await readFile(path, "utf8");
44
+ return JSON.parse(text);
45
+ }
46
+ catch (err) {
47
+ const e = err;
48
+ if (e.code === "ENOENT") {
49
+ return undefined;
50
+ }
51
+ throw err;
52
+ }
53
+ }
54
+ async function saveToken(path, token) {
55
+ await mkdir(dirname(path), { recursive: true });
56
+ await writeFile(path, JSON.stringify(token, null, 2), { mode: 0o600 });
57
+ await chmod(path, 0o600);
58
+ }
59
+ function tryOpenBrowser(url) {
60
+ const candidates = [
61
+ { cmd: "xdg-open", args: [url] },
62
+ { cmd: "open", args: [url] },
63
+ { cmd: "cmd", args: ["/c", "start", "", url] },
64
+ ];
65
+ for (const { cmd, args } of candidates) {
66
+ try {
67
+ const child = spawn(cmd, args, { stdio: "ignore", detached: true });
68
+ child.on("error", () => void 0);
69
+ child.unref();
70
+ return;
71
+ }
72
+ catch {
73
+ // try next
74
+ }
75
+ }
76
+ log.info("could not auto-open browser; copy the URL above into a browser");
77
+ }
78
+ // Runtime-side: load creds + token, return an OAuth2Client that
79
+ // auto-refreshes its access token. Throws with a clear message if the
80
+ // user hasn't run the login flow yet.
81
+ export async function loadGoogleAuth(opts) {
82
+ const creds = await loadCredentials(opts.credentialsPath);
83
+ const token = await loadToken(opts.tokenPath);
84
+ if (!token || !token.refresh_token) {
85
+ throw new Error(`No Google OAuth token at ${opts.tokenPath}. Run \`hydra-acp-archiver-login\` first.`);
86
+ }
87
+ const client = new google.auth.OAuth2(creds.client_id, creds.client_secret);
88
+ client.setCredentials(token);
89
+ // Persist refreshed tokens whenever google-auth-library issues a new
90
+ // access token, so we keep the latest expiry on disk for future runs.
91
+ client.on("tokens", (next) => {
92
+ const merged = { ...token, ...next };
93
+ void saveToken(opts.tokenPath, merged).catch((err) => {
94
+ log.warn(`failed to persist refreshed token: ${err.message}`);
95
+ });
96
+ });
97
+ return client;
98
+ }
99
+ // Login-side: interactive flow using a loopback redirect on a
100
+ // kernel-assigned port. Writes refresh+access tokens to tokenPath.
101
+ export async function runGoogleLogin(opts) {
102
+ const creds = await loadCredentials(opts.credentialsPath);
103
+ // Bind loopback server first so we can build the redirect_uri with
104
+ // the assigned port.
105
+ const { port, codePromise, close } = await startCallbackServer();
106
+ const redirectUri = `http://127.0.0.1:${port}`;
107
+ const client = new google.auth.OAuth2(creds.client_id, creds.client_secret, redirectUri);
108
+ const authUrl = client.generateAuthUrl({
109
+ access_type: "offline",
110
+ scope: SCOPES,
111
+ // prompt=consent forces a refresh_token even if the user previously
112
+ // granted access — without it, repeat logins return only an access
113
+ // token.
114
+ prompt: "consent",
115
+ });
116
+ log.info("Open this URL in a browser to authorize the archiver:");
117
+ log.info(authUrl);
118
+ tryOpenBrowser(authUrl);
119
+ let code;
120
+ try {
121
+ code = await codePromise;
122
+ }
123
+ finally {
124
+ close();
125
+ }
126
+ const { tokens } = await client.getToken(code);
127
+ if (!tokens.refresh_token) {
128
+ throw new Error("Google did not return a refresh_token. Revoke the app at https://myaccount.google.com/permissions and try again.");
129
+ }
130
+ await saveToken(opts.tokenPath, tokens);
131
+ log.info(`token saved to ${opts.tokenPath}`);
132
+ }
133
+ async function startCallbackServer() {
134
+ let resolveCode = () => void 0;
135
+ let rejectCode = () => void 0;
136
+ const codePromise = new Promise((resolve, reject) => {
137
+ resolveCode = resolve;
138
+ rejectCode = reject;
139
+ });
140
+ const server = createServer((req, res) => {
141
+ try {
142
+ const url = new URL(req.url ?? "/", `http://127.0.0.1`);
143
+ const code = url.searchParams.get("code");
144
+ const error = url.searchParams.get("error");
145
+ if (error) {
146
+ res.statusCode = 400;
147
+ res.setHeader("content-type", "text/plain; charset=utf-8");
148
+ res.end(`Authorization error: ${error}\n\nYou can close this tab.`);
149
+ rejectCode(new Error(`OAuth error: ${error}`));
150
+ return;
151
+ }
152
+ if (!code) {
153
+ res.statusCode = 400;
154
+ res.setHeader("content-type", "text/plain; charset=utf-8");
155
+ res.end("Missing ?code parameter. You can close this tab.");
156
+ return;
157
+ }
158
+ res.statusCode = 200;
159
+ res.setHeader("content-type", "text/plain; charset=utf-8");
160
+ res.end("hydra-acp-archiver: authorization complete. You can close this tab.");
161
+ resolveCode(code);
162
+ }
163
+ catch (err) {
164
+ res.statusCode = 500;
165
+ res.end("internal error");
166
+ rejectCode(err);
167
+ }
168
+ });
169
+ await new Promise((resolve, reject) => {
170
+ server.once("error", reject);
171
+ server.listen(0, "127.0.0.1", () => resolve());
172
+ });
173
+ const addr = server.address();
174
+ if (!addr || typeof addr === "string") {
175
+ server.close();
176
+ throw new Error("failed to determine callback server port");
177
+ }
178
+ const port = addr.port;
179
+ log.info(`listening for OAuth redirect on http://127.0.0.1:${port}`);
180
+ return {
181
+ port,
182
+ codePromise,
183
+ close: () => {
184
+ try {
185
+ server.close();
186
+ }
187
+ catch {
188
+ // ignore
189
+ }
190
+ },
191
+ };
192
+ }
193
+ //# sourceMappingURL=google.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"google.js","sourceRoot":"","sources":["../../src/oauth/google.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,129 @@
1
+ import { deserialize } from "./envelope.js";
2
+ import { logger } from "./util/log.js";
3
+ const log = logger("pull");
4
+ export class PullLoop {
5
+ opts;
6
+ timer;
7
+ stopped = false;
8
+ inFlight = false;
9
+ constructor(opts) {
10
+ this.opts = opts;
11
+ }
12
+ start() {
13
+ log.info(`polling backend every ${this.opts.intervalMs}ms for peer-uploaded bundles`);
14
+ void this.tick();
15
+ this.timer = setInterval(() => {
16
+ void this.tick();
17
+ }, this.opts.intervalMs);
18
+ this.timer.unref();
19
+ }
20
+ stop() {
21
+ this.stopped = true;
22
+ if (this.timer) {
23
+ clearInterval(this.timer);
24
+ this.timer = undefined;
25
+ }
26
+ }
27
+ // tickNow is exposed for tests to drive a single iteration on demand.
28
+ async tickNow() {
29
+ await this.tick();
30
+ }
31
+ async tick() {
32
+ if (this.stopped || this.inFlight) {
33
+ return;
34
+ }
35
+ this.inFlight = true;
36
+ try {
37
+ // Snapshot the daemon's session set once per tick. Any envelope
38
+ // whose inner bundle.session.sessionId is already present locally
39
+ // is one we (or a peer using our credentials) uploaded — skip it.
40
+ let localSessionIds;
41
+ try {
42
+ localSessionIds = await this.opts.daemon.listSessionIds();
43
+ }
44
+ catch (err) {
45
+ log.warn(`session list failed; skipping tick: ${err.message}`);
46
+ return;
47
+ }
48
+ const entries = await this.opts.backend.list();
49
+ for (const entry of entries) {
50
+ if (this.stopped) {
51
+ return;
52
+ }
53
+ await this.processEntry(entry.key, localSessionIds).catch((err) => {
54
+ log.warn(`processing ${entry.key} failed: ${err.message}`);
55
+ });
56
+ }
57
+ }
58
+ catch (err) {
59
+ log.warn(`backend.list failed: ${err.message}`);
60
+ }
61
+ finally {
62
+ this.inFlight = false;
63
+ }
64
+ }
65
+ async processEntry(key, localSessionIds) {
66
+ const raw = await this.opts.backend.get(key);
67
+ let envelope;
68
+ try {
69
+ envelope = deserialize(raw);
70
+ }
71
+ catch (err) {
72
+ log.warn(`malformed envelope at ${key}: ${err.message}`);
73
+ return;
74
+ }
75
+ const { lineageId } = envelope;
76
+ // Self-loop suppression: if the bundle's sessionId already exists
77
+ // in the local daemon, we (or a peer reflecting our upload) made
78
+ // this — no need to import. This survives hostname changes and
79
+ // race-conditions with state writes that a hostname or hash check
80
+ // would not.
81
+ const innerSessionId = readSessionId(envelope.bundle);
82
+ if (innerSessionId !== undefined && localSessionIds.has(innerSessionId)) {
83
+ return;
84
+ }
85
+ const local = this.opts.state.get(lineageId);
86
+ if (local.lastSeenRemoteUploadedAt !== undefined &&
87
+ envelope.uploadedAt <= local.lastSeenRemoteUploadedAt) {
88
+ return;
89
+ }
90
+ if (typeof envelope.bundle !== "object" ||
91
+ envelope.bundle === null) {
92
+ log.warn(`envelope ${key} bundle is not an object; skipping`);
93
+ return;
94
+ }
95
+ log.info(`importing lineage=${lineageId} from ${envelope.uploadedBy.host} uploadedAt=${envelope.uploadedAt}`);
96
+ try {
97
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
98
+ await this.opts.daemon.importBundle(envelope.bundle, {
99
+ replace: true,
100
+ });
101
+ }
102
+ catch (err) {
103
+ log.warn(`daemon import for lineage ${lineageId} failed: ${err.message}`);
104
+ return;
105
+ }
106
+ // Suppress the immediate re-upload that the daemon's
107
+ // import-induced turn_complete (if any) would trigger: we record
108
+ // the envelope hash as our last-uploaded hash so the archive-loop
109
+ // sees "no change" on the next export.
110
+ await this.opts.state.set(lineageId, {
111
+ lastSeenRemoteUploadedAt: envelope.uploadedAt,
112
+ lastSeenRemoteBy: envelope.uploadedBy.host,
113
+ lastUploadedHash: envelope.bundleHash,
114
+ lastUploadedAt: envelope.uploadedAt,
115
+ });
116
+ }
117
+ }
118
+ function readSessionId(bundle) {
119
+ if (!bundle || typeof bundle !== "object" || Array.isArray(bundle)) {
120
+ return undefined;
121
+ }
122
+ const session = bundle.session;
123
+ if (!session || typeof session !== "object" || Array.isArray(session)) {
124
+ return undefined;
125
+ }
126
+ const id = session.sessionId;
127
+ return typeof id === "string" ? id : undefined;
128
+ }
129
+ //# sourceMappingURL=pull-loop.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pull-loop.js","sourceRoot":"","sources":["../src/pull-loop.ts"],"names":[],"mappings":"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"}
package/dist/rule.js ADDED
@@ -0,0 +1,37 @@
1
+ import { stat } from "node:fs/promises";
2
+ import { pathToFileURL } from "node:url";
3
+ import { logger } from "./util/log.js";
4
+ const log = logger("rule");
5
+ export const DEFAULT_RULE = () => true;
6
+ let loadCounter = 0;
7
+ export async function loadRule(path) {
8
+ try {
9
+ await stat(path);
10
+ }
11
+ catch (err) {
12
+ const e = err;
13
+ if (e.code === "ENOENT") {
14
+ log.info(`no rule config at ${path} — archiving every live session (drop a JS file at that path to opt out specific sessions)`);
15
+ return DEFAULT_RULE;
16
+ }
17
+ log.warn(`stat ${path} failed: ${e.message}; using DEFAULT_RULE`);
18
+ return DEFAULT_RULE;
19
+ }
20
+ loadCounter += 1;
21
+ const url = `${pathToFileURL(path).href}?v=${Date.now()}-${loadCounter}`;
22
+ try {
23
+ const mod = (await import(url));
24
+ const fn = mod.default;
25
+ if (typeof fn !== "function") {
26
+ log.warn(`${path} did not export a default function; using DEFAULT_RULE`);
27
+ return DEFAULT_RULE;
28
+ }
29
+ log.info(`loaded archiver rule from ${path}`);
30
+ return fn;
31
+ }
32
+ catch (err) {
33
+ log.warn(`import ${path} failed: ${err.message}; using DEFAULT_RULE`);
34
+ return DEFAULT_RULE;
35
+ }
36
+ }
37
+ //# sourceMappingURL=rule.js.map