@hitch42/applet 0.1.1-beta.20 → 0.1.1-beta.22

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.
Files changed (95) hide show
  1. package/dist/_internal/client.d.ts +0 -2
  2. package/dist/_internal/client.js +3 -5
  3. package/dist/_internal/log-context.d.ts +0 -2
  4. package/dist/_internal/log-context.js +1 -2
  5. package/dist/_internal/server.d.ts +2 -10
  6. package/dist/_internal/server.js +9 -14
  7. package/dist/applet.js +64 -23
  8. package/dist/auth-DuMRqSGe.js +49 -0
  9. package/dist/{batch-_ZjAg-Kk.js → batch-DwyWmiaE.js} +3 -4
  10. package/dist/build-C7hi2yg4.js +1 -0
  11. package/dist/{build-D47vK5aa.js → build-FAQSHJgm.js} +4 -3
  12. package/dist/{ci-BTFTCBgy.js → ci-oWlUJKm9.js} +5 -6
  13. package/dist/client/hitch/client.d.ts +1 -1
  14. package/dist/client/hitch/client.js +1 -2
  15. package/dist/client/hitch/phone.d.ts +1 -1
  16. package/dist/client/hitch/phone.js +1 -2
  17. package/dist/client/hitch.d.ts +2 -2
  18. package/dist/client/hitch.js +2 -2
  19. package/dist/client/react/markdown-editor.d.ts +1 -1
  20. package/dist/client/react/markdown-editor.js +1 -2
  21. package/dist/client/react/markdown.d.ts +1 -1
  22. package/dist/client/react/markdown.js +1 -2
  23. package/dist/client/react.d.ts +2 -2
  24. package/dist/client/react.js +2 -2
  25. package/dist/client.d.ts +2 -4
  26. package/dist/client.js +3 -5
  27. package/dist/{contract-CJv-n1l3.d.ts → contract-CUc0B2rl.d.ts} +1 -3
  28. package/dist/{contract-BeKz1Qcs.js → contract-D9LCNGjD.js} +1 -2
  29. package/dist/{create-C2UrZYSK.d.ts → create-BJqLT8Uq.d.ts} +0 -4
  30. package/dist/{deploy-BbqyK5Ik.js → deploy-BM55ZoSp.js} +160 -152
  31. package/dist/{device-BgIfytaR.js → device-CZWWckCq.js} +54 -34
  32. package/dist/{email-protocol-B9u_dh2p.d.ts → email-protocol-BWja_v7c.d.ts} +16 -16
  33. package/dist/generate-DG9vVkBO.js +1 -0
  34. package/dist/{generate-BLD0DoCk.js → generate-DNgO44RO.js} +32 -37
  35. package/dist/generated-agents-BUnJ0FQA.js +215 -0
  36. package/dist/git-credential-CU7-Erzs.js +24 -0
  37. package/dist/git-deploy-C12nMNB6.js +290 -0
  38. package/dist/git-deploy-CNz6mxiF.js +1 -0
  39. package/dist/git-migration-CIq-sN61.js +629 -0
  40. package/dist/git-safety-Ck3X7jvA.js +13 -0
  41. package/dist/{headers-CeRX-Xji.js → headers-BcEMIVCo.js} +1 -2
  42. package/dist/{hitch-client-Cw-WZ_8v.js → hitch-client-CNN-dU6d.js} +5 -7
  43. package/dist/{hitch-client-DtyTZzP5.d.ts → hitch-client-k7_jIjPw.d.ts} +5 -7
  44. package/dist/{hitch-file-url-BPKj9zVG.js → hitch-file-url--AMjDOzP.js} +1 -2
  45. package/dist/index.d.ts +13 -17
  46. package/dist/index.js +33 -38
  47. package/dist/{init-BKUBf6i9.js → init-BD5mC_9L.js} +70 -18
  48. package/dist/{lazy-client-CwIzYAoE.js → lazy-client--QCL0wiu.js} +1 -2
  49. package/dist/{load-config-DGALUExv.js → load-config-Cffvj-aM.js} +24 -26
  50. package/dist/{log-context-DWznGjfz.js → log-context-BepwdjrR.js} +1 -2
  51. package/dist/login-BWZKo7Ui.js +21 -0
  52. package/dist/{logs-Cv5z1e2w.js → logs-glCVHI-8.js} +6 -6
  53. package/dist/{manifest-BBHD_EY9.d.ts → manifest-CpghlaJc.d.ts} +6 -7
  54. package/dist/manifest-patterns-BfxqImlp.js +4 -0
  55. package/dist/{markdown-CwlqzBtP.js → markdown-BA_XzFxq.js} +2 -3
  56. package/dist/{markdown-DSHOQIOb.d.ts → markdown-CnB35mjc.d.ts} +1 -3
  57. package/dist/{markdown-editor-C5mLHGqu.js → markdown-editor-CaUuphfc.js} +3 -5
  58. package/dist/{markdown-editor-BmEaBzd7.d.ts → markdown-editor-xF3uATBp.d.ts} +1 -5
  59. package/dist/parent-D52Kxp8U.js +1 -0
  60. package/dist/parent-Zx8Eg1Ki.js +75 -0
  61. package/dist/{project-CrxdZR0B.js → project-DZGk765E.js} +63 -54
  62. package/dist/{prompt-ChS-grYO.js → prompt-CW98R-Un.js} +1 -2
  63. package/dist/{query-cache-CXmAyLv3.js → query-cache-DQJy45RT.js} +1 -2
  64. package/dist/{query-utils-zz9pVjgp.d.ts → query-utils-okxIoKM3.d.ts} +0 -2
  65. package/dist/{schedules-Dajgni_z.js → schedules-CSUb84e2.js} +5 -5
  66. package/dist/server.d.ts +5 -13
  67. package/dist/server.js +17 -20
  68. package/dist/source-hash-BbvtRaus.js +107 -0
  69. package/dist/store-E5NhITHl.js +448 -0
  70. package/dist/{upload-DRbVhuDm.js → upload-D1mzpsSk.js} +4 -6
  71. package/dist/{upload-D_kkmne_.d.ts → upload-wp-Ilsv6.d.ts} +6 -10
  72. package/dist/{use-phone-ug4B5gQp.js → use-phone-CB_idmrz.js} +9 -13
  73. package/dist/{use-phone-M7c_HsB7.d.ts → use-phone-DBAkasPq.d.ts} +0 -4
  74. package/dist/{variables-B-CLu8cY.js → variables-CgZstC7I.js} +7 -8
  75. package/dist/vite.d.ts +1 -3
  76. package/dist/vite.js +41 -50
  77. package/dist/{workflow-DurM6Fwx.d.ts → workflow-BnBrJIzu.d.ts} +0 -2
  78. package/dist/{workflow-engine-BOTzURKM.js → workflow-engine-CyyaVENo.js} +2 -4
  79. package/dist/{workflow-CTo2kZF1.js → workflow-l2SlVePB.js} +2 -4
  80. package/dist/workspace-CtIKSgMD.js +76 -0
  81. package/package.json +6 -5
  82. package/scaffold/.agents/skills/hitch-applets/SKILL.md +27 -9
  83. package/scaffold/.agents/skills/hitch-applets/references/email-tracking.md +4 -4
  84. package/scaffold/.agents/skills/hitch-applets/references/files.md +2 -2
  85. package/scaffold/.agents/skills/hitch-applets/references/manifest.md +8 -6
  86. package/scaffold/.agents/skills/hitch-applets/references/performance.md +2 -2
  87. package/scaffold/.agents/skills/hitch-applets/references/records.md +8 -8
  88. package/scaffold/.agents/skills/hitch-cli/SKILL.md +52 -43
  89. package/scaffold/AGENTS.md +6 -0
  90. package/dist/auth-D3CrCtZq.js +0 -21
  91. package/dist/build-JtbY8q2p.js +0 -2
  92. package/dist/generate-COCRy1Bs.js +0 -2
  93. package/dist/login-CceGvlMi.js +0 -14
  94. package/dist/store-B8N_8JIN.js +0 -196
  95. package/dist/workspace-8yqXsvce.js +0 -31
@@ -0,0 +1,107 @@
1
+ import { a as findWorkspaceRoot } from "./workspace-CtIKSgMD.js";
2
+ import { createHash } from "node:crypto";
3
+ import { lstat, readlink } from "node:fs/promises";
4
+ import { dirname, relative, resolve, sep } from "node:path";
5
+ import { execFile, spawn } from "node:child_process";
6
+ import { promisify } from "node:util";
7
+ // src/source-hash.ts
8
+ const execFileAsync = promisify(execFile);
9
+ function gitEnvironment() {
10
+ return Object.fromEntries(Object.entries(process.env).filter(([key]) => !key.startsWith("GIT_")));
11
+ }
12
+ var SourceHashUnavailableError = class extends Error {
13
+ constructor() {
14
+ super("Git is unavailable or the applet is outside a git repository.");
15
+ }
16
+ };
17
+ function isMissingPath(error) {
18
+ return error instanceof Error && "code" in error && (error.code === "ENOENT" || error.code === "ENOTDIR");
19
+ }
20
+ async function hashFiles(topLevel, paths, env) {
21
+ if (paths.length === 0) return [];
22
+ return new Promise((resolveHashes, reject) => {
23
+ const child = spawn("git", ["hash-object", "--stdin-paths"], {
24
+ cwd: topLevel,
25
+ env
26
+ });
27
+ const stdout = [];
28
+ const stderr = [];
29
+ child.stdout.on("data", (chunk) => stdout.push(chunk));
30
+ child.stderr.on("data", (chunk) => stderr.push(chunk));
31
+ child.stdin.on("error", reject);
32
+ child.on("error", reject);
33
+ child.on("close", (code) => {
34
+ if (code !== 0) {
35
+ reject(/* @__PURE__ */ new Error(`git hash-object failed (${code}): ${Buffer.concat(stderr).toString("utf8").trim()}`));
36
+ return;
37
+ }
38
+ const hashes = Buffer.concat(stdout).toString("utf8").trimEnd().split("\n");
39
+ if (hashes.length !== paths.length || hashes.some((hash) => !/^[0-9a-f]{40,64}$/.test(hash))) {
40
+ reject(/* @__PURE__ */ new Error("git hash-object returned an unexpected number of hashes."));
41
+ return;
42
+ }
43
+ resolveHashes(hashes);
44
+ });
45
+ child.stdin.end(paths.map((path) => `"${path.split("").map((character) => character < " " || character === "" || character === "\"" || character === "\\" ? `\\${character.charCodeAt(0).toString(8).padStart(3, "0")}` : character).join("")}"\n`).join(""));
46
+ });
47
+ }
48
+ async function sourceHashes(appletRoots) {
49
+ if (appletRoots.length === 0) return /* @__PURE__ */ new Map();
50
+ const env = gitEnvironment();
51
+ const workspaceRoot = findWorkspaceRoot(appletRoots[0]) ?? dirname(dirname(appletRoots[0]));
52
+ let topLevel;
53
+ try {
54
+ const { stdout } = await execFileAsync("git", ["rev-parse", "--show-toplevel"], {
55
+ cwd: workspaceRoot,
56
+ env
57
+ });
58
+ topLevel = stdout.trim();
59
+ } catch (error) {
60
+ if (isMissingPath(error) || error instanceof Error && error.message.includes("not a git repository")) throw new SourceHashUnavailableError();
61
+ throw error;
62
+ }
63
+ const { stdout } = await execFileAsync("git", [
64
+ "ls-files",
65
+ "-z",
66
+ "--cached",
67
+ "--others",
68
+ "--exclude-standard"
69
+ ], {
70
+ cwd: topLevel,
71
+ env,
72
+ maxBuffer: 67108864
73
+ });
74
+ const paths = [...new Set(stdout.split("\0").filter(Boolean))];
75
+ const present = (await Promise.all(paths.map(async (path) => {
76
+ const absolutePath = resolve(topLevel, path);
77
+ try {
78
+ const stat = await lstat(absolutePath);
79
+ if (stat.isFile()) return {
80
+ path,
81
+ kind: "file"
82
+ };
83
+ if (stat.isSymbolicLink()) return {
84
+ path,
85
+ kind: "link",
86
+ contentId: await readlink(absolutePath)
87
+ };
88
+ return;
89
+ } catch (error) {
90
+ if (isMissingPath(error)) return void 0;
91
+ throw error;
92
+ }
93
+ }))).filter((entry) => entry !== void 0);
94
+ const regularPaths = present.filter((entry) => entry.kind === "file").map((entry) => entry.path);
95
+ const hashes = await hashFiles(topLevel, regularPaths, env);
96
+ const contentIds = /* @__PURE__ */ new Map();
97
+ present.filter((entry) => entry.kind === "link").forEach((entry) => contentIds.set(entry.path, entry.contentId));
98
+ regularPaths.forEach((path, index) => contentIds.set(path, hashes[index]));
99
+ const appletsDirectory = relative(topLevel, resolve(workspaceRoot, "applets")).split(sep).join("/");
100
+ return new Map(appletRoots.map((appletRoot) => {
101
+ const appletDirectory = relative(topLevel, appletRoot).split(sep).join("/");
102
+ const digest = createHash("sha256");
103
+ [...contentIds.entries()].filter(([path]) => path !== appletsDirectory && !path.startsWith(`${appletsDirectory}/`) || path.startsWith(`${appletDirectory}/`)).sort(([left], [right]) => Buffer.compare(Buffer.from(left), Buffer.from(right))).forEach(([path, contentId]) => digest.update(`${path}\0${contentId}\n`));
104
+ return [appletRoot, digest.digest("hex")];
105
+ }));
106
+ }
107
+ export { gitEnvironment as n, sourceHashes as r, SourceHashUnavailableError as t };
@@ -0,0 +1,448 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import fs from "node:fs/promises";
3
+ import { dirname, join } from "node:path";
4
+ import { homedir } from "node:os";
5
+ // ../shared/auth/src/lock.mts
6
+ const LOCK_AGE = 3e4;
7
+ function canTakeOverRefreshLock(age) {
8
+ return age > LOCK_AGE;
9
+ }
10
+ function ownsRefreshLock(contents, owner) {
11
+ return contents === owner;
12
+ }
13
+ async function withFileLock(path, action) {
14
+ await fs.mkdir(dirname(path), { recursive: true });
15
+ const deadline = Date.now() + LOCK_AGE;
16
+ while (Date.now() < deadline) {
17
+ const owner = randomUUID();
18
+ let lock;
19
+ try {
20
+ lock = await fs.open(path, "wx", 384);
21
+ } catch (error) {
22
+ if (!(error instanceof Error && "code" in error && error.code === "EEXIST")) throw error;
23
+ try {
24
+ const contents = await fs.readFile(path, "utf8");
25
+ const info = await fs.stat(path);
26
+ if (canTakeOverRefreshLock(Date.now() - info.mtimeMs) && ownsRefreshLock(await fs.readFile(path, "utf8"), contents)) await fs.rm(path, { force: true });
27
+ } catch (error) {
28
+ if (!(error instanceof Error && "code" in error && error.code === "ENOENT")) throw error;
29
+ }
30
+ await new Promise((resolve) => setTimeout(resolve, 100));
31
+ continue;
32
+ }
33
+ try {
34
+ await lock.writeFile(owner);
35
+ return await action();
36
+ } finally {
37
+ await lock.close();
38
+ const contents = await fs.readFile(path, "utf8").catch((error) => {
39
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") return void 0;
40
+ throw error;
41
+ });
42
+ if (contents !== void 0 && ownsRefreshLock(contents, owner)) await fs.rm(path, { force: true });
43
+ }
44
+ }
45
+ throw new AuthenticationError("Timed out while waiting for the credential lock.");
46
+ }
47
+ // ../shared/auth/src/oauth.mts
48
+ const CLIENT_ID = "hitch-cli-oauth";
49
+ const REFRESH_GRANT = "refresh_token";
50
+ async function saveOAuthCredentials(instanceUrl, credentials, refreshToken) {
51
+ const stored = {
52
+ ...credentials,
53
+ refreshToken
54
+ };
55
+ await setCredentials(instanceUrl, stored);
56
+ return stored;
57
+ }
58
+ async function removeOAuthCredentials(instanceUrl, orgCode) {
59
+ await deleteCredentials(instanceUrl, orgCode);
60
+ }
61
+ async function withRefreshLock(instanceUrl, orgCode, action) {
62
+ const { configDir } = storePaths();
63
+ return withFileLock(join(configDir, `refresh-${encodeURIComponent(normalizeInstanceUrl(instanceUrl))}-${orgCode}.lock`), action);
64
+ }
65
+ async function refreshAccessToken(instanceUrl, orgCode) {
66
+ const current = await getCredentialsForOrg(instanceUrl, orgCode);
67
+ if (current?.type !== "oauth") throw new LoginRequiredError();
68
+ if (current.expiresAt - Date.now() > 6e4) return current;
69
+ return withRefreshLock(instanceUrl, orgCode, async () => {
70
+ const latest = await getCredentialsForOrg(instanceUrl, orgCode);
71
+ if (latest?.type !== "oauth") throw new LoginRequiredError();
72
+ if (latest.expiresAt - Date.now() > 6e4) return latest;
73
+ const refreshToken = latest.refreshToken;
74
+ const response = await globalThis.fetch(`${instanceUrl}/api/auth/oauth2/token`, {
75
+ method: "POST",
76
+ signal: AbortSignal.timeout(1e4),
77
+ headers: { "content-type": "application/x-www-form-urlencoded" },
78
+ body: new URLSearchParams({
79
+ grant_type: REFRESH_GRANT,
80
+ refresh_token: refreshToken,
81
+ client_id: CLIENT_ID,
82
+ resource: `${organizationInstanceUrl(instanceUrl, orgCode)}/mcp`
83
+ })
84
+ });
85
+ const body = await response.json();
86
+ if (body === null || typeof body !== "object") throw new AuthenticationError("Hitch returned an invalid token response.");
87
+ if ("error" in body && body.error === "invalid_grant") {
88
+ await removeOAuthCredentials(instanceUrl, orgCode);
89
+ throw new LoginRequiredError();
90
+ }
91
+ if (!response.ok || !("access_token" in body) || typeof body.access_token !== "string" || !("refresh_token" in body) || typeof body.refresh_token !== "string" || !("expires_in" in body) || typeof body.expires_in !== "number") throw new AuthenticationError(`Token refresh failed (${response.status}).`);
92
+ const credentials = {
93
+ type: "oauth",
94
+ accessToken: body.access_token,
95
+ expiresAt: Date.now() + body.expires_in * 1e3,
96
+ organizationId: latest.organizationId,
97
+ orgCode
98
+ };
99
+ for (let attempt = 0; attempt < 3; attempt++) try {
100
+ return await saveOAuthCredentials(instanceUrl, credentials, body.refresh_token);
101
+ } catch (error) {
102
+ if (attempt === 2) throw new AuthenticationError("The login could not be saved. Run the command again within a minute.", { cause: error });
103
+ await new Promise((resolve) => setTimeout(resolve, 100 * (attempt + 1)));
104
+ }
105
+ throw new Error("Unreachable refresh write state.");
106
+ });
107
+ }
108
+ async function revokeOAuthCredentials(instanceUrl, credentials, warn = console.warn) {
109
+ try {
110
+ const response = await globalThis.fetch(`${normalizeInstanceUrl(instanceUrl)}/api/auth/oauth2/revoke`, {
111
+ method: "POST",
112
+ signal: AbortSignal.timeout(1e4),
113
+ headers: { "content-type": "application/x-www-form-urlencoded" },
114
+ body: new URLSearchParams({
115
+ token: credentials.refreshToken,
116
+ token_type_hint: "refresh_token",
117
+ client_id: CLIENT_ID
118
+ })
119
+ });
120
+ if (!response.ok) warn(`Warning: server sign-out failed (${response.status}).`);
121
+ } catch (error) {
122
+ warn(`Warning: server sign-out failed: ${error instanceof Error ? error.message : String(error)}`);
123
+ } finally {
124
+ await removeOAuthCredentials(instanceUrl, credentials.orgCode);
125
+ }
126
+ }
127
+ // ../shared/auth/src/store.mts
128
+ const DEFAULT_INSTANCE_URL = "https://app.hitch42.com";
129
+ var AuthenticationError = class extends Error {};
130
+ var LoginRequiredError = class extends AuthenticationError {
131
+ constructor() {
132
+ super("Your Hitch login ended. Log in again with applet login or hitch login.");
133
+ }
134
+ };
135
+ let selectedStore;
136
+ let keyringClass;
137
+ let findKeyringCredentials;
138
+ function configureKeyring(entryClass, findCredentials) {
139
+ keyringClass = entryClass;
140
+ findKeyringCredentials = findCredentials;
141
+ selectedStore = void 0;
142
+ }
143
+ function keyringEntry(account) {
144
+ if (keyringClass === void 0) throw new Error("Keyring is not configured.");
145
+ const options = process.platform === "linux" ? { linux: { store: "secret-service" } } : void 0;
146
+ return new keyringClass("hitch", account, options);
147
+ }
148
+ function credentialStorage() {
149
+ if (selectedStore !== void 0) return selectedStore;
150
+ if (keyringClass === void 0) throw new Error("Keyring is not configured.");
151
+ selectedStore = (process.platform === "linux" || process.platform === "darwin" || process.platform === "win32" ? ["secret"] : []).find((candidate) => {
152
+ const probe = `__probe__${randomUUID()}`;
153
+ try {
154
+ selectedStore = candidate;
155
+ const entry = keyringEntry(probe);
156
+ entry.setPassword(probe);
157
+ const verified = entry.getPassword() === probe;
158
+ const removed = entry.deletePassword();
159
+ return verified && removed;
160
+ } catch {
161
+ try {
162
+ keyringEntry(probe).deletePassword();
163
+ } catch {}
164
+ return false;
165
+ }
166
+ }) ?? "file";
167
+ return selectedStore;
168
+ }
169
+ function credentialStorageMessage(type = "oauth") {
170
+ return credentialStorage() === "file" ? `No keychain is available. Your login is saved in a plain file: ${type === "oauth" ? storePaths().oauthCredentialsPath : storePaths().credentialsPath}.` : void 0;
171
+ }
172
+ function account(instanceUrl, orgCode) {
173
+ return `${normalizeInstanceUrl(instanceUrl)} ${orgCode}`;
174
+ }
175
+ function listKeyringCredentials() {
176
+ if (findKeyringCredentials === void 0) throw new Error("Keyring listing is not configured.");
177
+ return findKeyringCredentials("hitch").filter(({ account }) => account !== "__index__");
178
+ }
179
+ function readKeychainCredential(instanceUrl, orgCode) {
180
+ const value = keyringEntry(account(instanceUrl, orgCode)).getPassword();
181
+ if (value === null) return void 0;
182
+ try {
183
+ const credentials = parseCredentials(JSON.parse(value));
184
+ if (credentials === void 0) throw new Error("Invalid credential shape.");
185
+ return credentials;
186
+ } catch (error) {
187
+ if (!value.startsWith("{")) return void 0;
188
+ throw new AuthenticationError(`Invalid keychain credential for ${orgCode}.`, { cause: error });
189
+ }
190
+ }
191
+ function isErrno(error, code) {
192
+ return error instanceof Error && "code" in error && error.code === code;
193
+ }
194
+ async function readJson(path) {
195
+ try {
196
+ return JSON.parse(await fs.readFile(path, "utf8"));
197
+ } catch (error) {
198
+ if (isErrno(error, "ENOENT")) return void 0;
199
+ throw new Error(`Failed to read ${path}.`, { cause: error });
200
+ }
201
+ }
202
+ function isObject(value) {
203
+ return value !== null && typeof value === "object" && !Array.isArray(value);
204
+ }
205
+ function parseLegacyCredentials(instanceUrl, value) {
206
+ const parsed = parseCredentials(value);
207
+ if (parsed !== void 0 || !isObject(value) || value.type !== "oauth" || typeof value.organizationId !== "string" || typeof value.orgCode !== "string") return parsed;
208
+ const stored = credentialStorage() === "secret" ? keyringEntry(account(instanceUrl, value.orgCode)).getPassword() : null;
209
+ const refreshToken = typeof value.refreshToken === "string" ? value.refreshToken : stored;
210
+ return typeof refreshToken === "string" ? {
211
+ type: "oauth",
212
+ accessToken: typeof value.accessToken === "string" ? value.accessToken : "",
213
+ expiresAt: typeof value.expiresAt === "number" ? value.expiresAt : 0,
214
+ refreshToken,
215
+ organizationId: value.organizationId,
216
+ orgCode: value.orgCode
217
+ } : void 0;
218
+ }
219
+ async function setKeychainCredential(instanceUrl, credentials) {
220
+ keyringEntry(account(instanceUrl, credentials.orgCode)).setPassword(JSON.stringify(credentials));
221
+ }
222
+ function parseCredentials(value) {
223
+ if (!isObject(value) || typeof value.organizationId !== "string" || typeof value.orgCode !== "string") return;
224
+ const organization = {
225
+ organizationId: value.organizationId,
226
+ orgCode: value.orgCode
227
+ };
228
+ if ((value.type === void 0 || value.type === "session") && typeof value.accessToken === "string" && (value.expiresAt === void 0 || typeof value.expiresAt === "number")) return {
229
+ type: "session",
230
+ accessToken: value.accessToken,
231
+ ...value.expiresAt === void 0 ? {} : { expiresAt: value.expiresAt },
232
+ ...organization
233
+ };
234
+ if (value.type === "apiKey" && typeof value.apiKey === "string") return {
235
+ type: "apiKey",
236
+ apiKey: value.apiKey,
237
+ ...organization
238
+ };
239
+ if (value.type === "oauth" && typeof value.accessToken === "string" && typeof value.expiresAt === "number" && typeof value.refreshToken === "string") return {
240
+ type: "oauth",
241
+ accessToken: value.accessToken,
242
+ expiresAt: value.expiresAt,
243
+ refreshToken: value.refreshToken,
244
+ ...organization
245
+ };
246
+ }
247
+ function normalizeInstanceUrl(instanceUrl) {
248
+ let url;
249
+ try {
250
+ url = new URL(instanceUrl);
251
+ } catch (error) {
252
+ throw new Error(`Invalid Hitch instance URL: ${instanceUrl}`, { cause: error });
253
+ }
254
+ if (url.protocol !== "http:" && url.protocol !== "https:") throw new Error(`Invalid Hitch instance URL protocol: ${url.protocol}`);
255
+ url.pathname = url.pathname.replace(/\/+$/, "");
256
+ url.search = "";
257
+ url.hash = "";
258
+ return url.toString().replace(/\/$/, "");
259
+ }
260
+ function organizationInstanceUrl(instanceUrl, orgCode) {
261
+ const url = new URL(normalizeInstanceUrl(instanceUrl));
262
+ const labels = url.hostname.split(".");
263
+ if (labels[0] !== "app" || labels.length < 2) return url.toString().replace(/\/$/, "");
264
+ url.hostname = [orgCode, ...labels.slice(1)].join(".");
265
+ return url.toString().replace(/\/$/, "");
266
+ }
267
+ function storePaths() {
268
+ const env = process.env;
269
+ const home = homedir();
270
+ const configDir = join(env.XDG_CONFIG_HOME || join(home, ".config"), "hitch");
271
+ return {
272
+ configDir,
273
+ cacheDir: join(env.XDG_CACHE_HOME || join(home, ".cache"), "hitch"),
274
+ configPath: join(configDir, "config.json"),
275
+ credentialsPath: join(configDir, "credentials.json"),
276
+ oauthCredentialsPath: join(configDir, "credentials-oauth.json")
277
+ };
278
+ }
279
+ async function resolveInstanceUrl(override) {
280
+ if (override !== void 0) return normalizeInstanceUrl(override);
281
+ const envInstanceUrl = process.env.HITCH_API_URL;
282
+ if (envInstanceUrl !== void 0 && envInstanceUrl.length > 0) return normalizeInstanceUrl(envInstanceUrl);
283
+ const { configPath } = storePaths();
284
+ const config = await readJson(configPath);
285
+ if (config === void 0) return DEFAULT_INSTANCE_URL;
286
+ if (!isObject(config) || config.instanceUrl !== void 0 && typeof config.instanceUrl !== "string") throw new Error(`${configPath}: expected { instanceUrl?: string }.`);
287
+ return normalizeInstanceUrl(config.instanceUrl ?? "https://app.hitch42.com");
288
+ }
289
+ async function migrateOAuthFile(path) {
290
+ const source = await readFileCredentialStore(path);
291
+ if (!Object.values(source.instances).some((entries) => Object.values(entries).some((entry) => entry.type === "oauth"))) return;
292
+ await updateFileCredentialStore(path, async (store) => {
293
+ for (const [instance, entries] of Object.entries(store.instances)) {
294
+ for (const [org, credential] of Object.entries(entries)) {
295
+ if (credential.type !== "oauth") continue;
296
+ if (credentialStorage() === "secret") {
297
+ if (readKeychainCredential(instance, org) === void 0) {
298
+ await setKeychainCredential(instance, credential);
299
+ if (JSON.stringify(readKeychainCredential(instance, org)) !== JSON.stringify(credential)) throw new AuthenticationError(`Cannot verify migrated login for ${org}.`);
300
+ }
301
+ } else await updateFileCredentialStore(storePaths().oauthCredentialsPath, (destination) => {
302
+ destination.instances[instance] ??= {};
303
+ destination.instances[instance][org] ??= credential;
304
+ });
305
+ delete entries[org];
306
+ }
307
+ if (Object.keys(entries).length === 0) delete store.instances[instance];
308
+ }
309
+ });
310
+ }
311
+ async function readCredentialStore() {
312
+ const paths = storePaths();
313
+ await migrateOAuthFile(paths.credentialsPath);
314
+ await migrateOAuthFile(`${paths.credentialsPath}.bak`);
315
+ if (credentialStorage() === "secret") await migrateOAuthFile(paths.oauthCredentialsPath);
316
+ const legacy = await readFileCredentialStore(storePaths().credentialsPath);
317
+ const oauth = credentialStorage() === "file" ? await readFileCredentialStore(storePaths().oauthCredentialsPath) : { instances: {} };
318
+ const instances = legacy.instances;
319
+ Object.entries(oauth.instances).forEach(([instance, credentials]) => {
320
+ instances[instance] = {
321
+ ...instances[instance],
322
+ ...credentials
323
+ };
324
+ });
325
+ if (credentialStorage() === "secret") listKeyringCredentials().forEach(({ account: key }) => {
326
+ const split = key.lastIndexOf(" ");
327
+ if (split < 0) return;
328
+ const instanceUrl = key.slice(0, split);
329
+ const orgCode = key.slice(split + 1);
330
+ const credentials = readKeychainCredential(instanceUrl, orgCode);
331
+ if (credentials !== void 0) {
332
+ instances[instanceUrl] ??= {};
333
+ instances[instanceUrl][orgCode] = credentials;
334
+ }
335
+ });
336
+ return { instances };
337
+ }
338
+ async function getCredentialsForOrg(instanceUrl, orgCode) {
339
+ return (await readCredentialStore()).instances[normalizeInstanceUrl(instanceUrl)]?.[orgCode];
340
+ }
341
+ function environmentAuth() {
342
+ const apiKey = process.env.HITCH_API_KEY;
343
+ return apiKey !== void 0 && apiKey.length > 0 ? {
344
+ accessToken: apiKey,
345
+ source: "environment"
346
+ } : void 0;
347
+ }
348
+ function resolvedStoredAuth(credentials) {
349
+ return credentials === void 0 ? void 0 : {
350
+ accessToken: credentials.type === "apiKey" ? credentials.apiKey : credentials.accessToken,
351
+ credentialType: credentials.type,
352
+ organizationId: credentials.organizationId,
353
+ orgCode: credentials.orgCode,
354
+ source: "store"
355
+ };
356
+ }
357
+ function resolveStoredAuthForOrg(credentialsByOrg, orgCode) {
358
+ const exact = resolvedStoredAuth(credentialsByOrg[orgCode]);
359
+ if (exact !== void 0) return exact;
360
+ const sessions = Object.values(credentialsByOrg).filter((credentials) => credentials.type === "session").filter((credentials, index, all) => all.findIndex(({ accessToken }) => accessToken === credentials.accessToken) === index);
361
+ if (sessions.length === 1) return resolvedStoredAuth(sessions[0]);
362
+ if (sessions.length > 1) throw new AuthenticationError(`Multiple sessions are stored. Run hitch login for organization "${orgCode}".`);
363
+ const apiKeyOrgs = Object.values(credentialsByOrg).filter((credentials) => credentials.type === "apiKey").map(({ orgCode: storedOrg }) => storedOrg).sort();
364
+ if (apiKeyOrgs.length > 0) throw new AuthenticationError(`Stored API ${apiKeyOrgs.length === 1 ? "key is" : "keys are"} scoped to ${apiKeyOrgs.map((storedOrg) => `"${storedOrg}"`).join(", ")}, not "${orgCode}".`);
365
+ }
366
+ async function resolveAuth(instanceUrl, orgCode) {
367
+ const auth = environmentAuth();
368
+ if (auth !== void 0) return auth;
369
+ const normalizedUrl = normalizeInstanceUrl(instanceUrl);
370
+ const credentialsByOrg = (await readCredentialStore()).instances[normalizedUrl] ?? {};
371
+ const selectedOrg = orgCode ?? process.env.HITCH_DEFAULT_ORG;
372
+ if (selectedOrg !== void 0 && selectedOrg.length > 0) return refreshResolvedAuth(normalizedUrl, resolveStoredAuthForOrg(credentialsByOrg, selectedOrg));
373
+ const orgCodes = Object.keys(credentialsByOrg).sort();
374
+ const soleOrg = orgCodes.at(0);
375
+ if (soleOrg === void 0) return void 0;
376
+ if (orgCodes.length === 1) return refreshResolvedAuth(normalizedUrl, resolvedStoredAuth(credentialsByOrg[soleOrg]));
377
+ throw new AuthenticationError(`Multiple organizations are stored for ${normalizedUrl}: ${orgCodes.join(", ")}. Use --org, the workspace hitch.config.ts, or HITCH_DEFAULT_ORG to choose one.`);
378
+ }
379
+ async function resolveAuthForOrg(instanceUrl, orgCode) {
380
+ const auth = environmentAuth();
381
+ if (auth !== void 0) return auth;
382
+ const normalizedUrl = normalizeInstanceUrl(instanceUrl);
383
+ const credentialsByOrg = Object.entries((await readCredentialStore()).instances).find(([instance]) => instance === normalizedUrl)?.[1];
384
+ return refreshResolvedAuth(normalizedUrl, resolveStoredAuthForOrg(credentialsByOrg ?? {}, orgCode));
385
+ }
386
+ async function refreshResolvedAuth(instanceUrl, auth) {
387
+ if (auth?.credentialType !== "oauth" || auth.orgCode === void 0) return auth;
388
+ return resolvedStoredAuth(await refreshAccessToken(instanceUrl, auth.orgCode));
389
+ }
390
+ function rejectEnvironmentKey(response, auth) {
391
+ if (auth.source === "environment" && response.status === 401) throw new AuthenticationError("HITCH_API_KEY was rejected.");
392
+ }
393
+ function bearerToken(credentials) {
394
+ return "apiKey" in credentials ? credentials.apiKey : credentials.accessToken;
395
+ }
396
+ async function setCredentials(instanceUrl, credentials) {
397
+ if (credentials.type === "oauth" && credentialStorage() === "secret") {
398
+ await setKeychainCredential(instanceUrl, credentials);
399
+ return;
400
+ }
401
+ const paths = storePaths();
402
+ await updateFileCredentialStore(credentials.type === "oauth" ? paths.oauthCredentialsPath : paths.credentialsPath, (store) => {
403
+ const key = normalizeInstanceUrl(instanceUrl);
404
+ store.instances[key] ??= {};
405
+ store.instances[key][credentials.orgCode] = credentials;
406
+ });
407
+ }
408
+ async function deleteCredentials(instanceUrl, orgCode) {
409
+ const normalizedUrl = normalizeInstanceUrl(instanceUrl);
410
+ const paths = storePaths();
411
+ await Promise.all([paths.credentialsPath, paths.oauthCredentialsPath].map((path) => updateFileCredentialStore(path, (store) => {
412
+ if (orgCode === void 0) delete store.instances[normalizedUrl];
413
+ else if (Object.hasOwn(store.instances, normalizedUrl)) {
414
+ const credentials = store.instances[normalizedUrl];
415
+ delete credentials[orgCode];
416
+ if (Object.keys(credentials).length === 0) delete store.instances[normalizedUrl];
417
+ }
418
+ })));
419
+ if (credentialStorage() === "secret") listKeyringCredentials().map(({ account }) => account).filter((key) => key.startsWith(`${normalizedUrl} `) && (orgCode === void 0 || key === account(normalizedUrl, orgCode))).forEach((key) => keyringEntry(key).deletePassword());
420
+ }
421
+ async function readFileCredentialStore(credentialsPath) {
422
+ const value = await readJson(credentialsPath);
423
+ if (value === void 0) return { instances: {} };
424
+ if (!isObject(value) || !isObject(value.instances)) throw new AuthenticationError(`Invalid credential store at ${credentialsPath}.`);
425
+ return { instances: Object.fromEntries(Object.entries(value.instances).flatMap(([instanceUrl, entry]) => {
426
+ if (!isObject(entry)) return [];
427
+ const direct = parseLegacyCredentials(instanceUrl, entry);
428
+ const credentials = direct === void 0 ? Object.values(entry).flatMap((item) => {
429
+ const parsed = parseLegacyCredentials(instanceUrl, item);
430
+ return parsed === void 0 ? [] : [[parsed.orgCode, parsed]];
431
+ }) : [[direct.orgCode, direct]];
432
+ return [[instanceUrl, Object.fromEntries(credentials)]];
433
+ })) };
434
+ }
435
+ async function updateFileCredentialStore(path, update) {
436
+ await withFileLock(`${path}.lock`, async () => {
437
+ const store = await readFileCredentialStore(path);
438
+ await update(store);
439
+ const temporary = `${path}.${randomUUID()}.tmp`;
440
+ try {
441
+ await fs.writeFile(temporary, `${JSON.stringify(store, null, 2)}\n`, { mode: 384 });
442
+ await fs.rename(temporary, path);
443
+ } finally {
444
+ await fs.rm(temporary, { force: true });
445
+ }
446
+ });
447
+ }
448
+ export { configureKeyring as a, normalizeInstanceUrl as c, rejectEnvironmentKey as d, resolveAuth as f, saveOAuthCredentials as g, revokeOAuthCredentials as h, bearerToken as i, organizationInstanceUrl as l, resolveInstanceUrl as m, DEFAULT_INSTANCE_URL as n, credentialStorageMessage as o, resolveAuthForOrg as p, LoginRequiredError as r, deleteCredentials as s, AuthenticationError as t, readCredentialStore as u };
@@ -1,5 +1,5 @@
1
1
  import "@orpc/server";
2
- //#region src/hitch/registry.ts
2
+ // src/hitch/registry.ts
3
3
  let registeredContract;
4
4
  function registerHitchContract(contract) {
5
5
  registeredContract = contract;
@@ -7,8 +7,7 @@ function registerHitchContract(contract) {
7
7
  function getRegisteredHitchContract() {
8
8
  return registeredContract;
9
9
  }
10
- //#endregion
11
- //#region src/hitch/upload.ts
10
+ // src/hitch/upload.ts
12
11
  function abortError(signal) {
13
12
  return signal?.reason ?? new DOMException("The upload was aborted.", "AbortError");
14
13
  }
@@ -80,7 +79,7 @@ async function uploadFile(files, file, options = {}) {
80
79
  } }, { signal: options.signal });
81
80
  const status = await putFile(file, created.uploadUrl, created.data.contentType, options);
82
81
  if (status < 200 || status >= 300) throw new Error(`Upload of "${file.name}" failed with status ${status}.`);
83
- const confirmed = await files.confirm({ params: { id: created.data.id } }, { signal: options.signal });
82
+ const confirmed = await files.confirm({ params: { fileId: created.data.id } }, { signal: options.signal });
84
83
  return {
85
84
  ...confirmed.data,
86
85
  downloadUrl: confirmed.downloadUrl
@@ -95,7 +94,7 @@ function withFilesUpload(target, files) {
95
94
  const proxy = new Proxy(value, { get: (filesInner, filesProperty, filesReceiver) => {
96
95
  if (filesProperty === "upload") return (file, options) => uploadFile(files(), file, options);
97
96
  if (filesProperty === "download") return async (id, options) => {
98
- const result = await files().get({ params: { id } }, options);
97
+ const result = await files().get({ params: { fileId: id } }, options);
99
98
  return {
100
99
  ...result.data,
101
100
  downloadUrl: result.downloadUrl
@@ -110,5 +109,4 @@ function withFilesUpload(target, files) {
110
109
  return proxy;
111
110
  } });
112
111
  }
113
- //#endregion
114
112
  export { registerHitchContract as i, withFilesUpload as n, getRegisteredHitchContract as r, uploadFile as t };
@@ -1,5 +1,4 @@
1
1
  import { ContractProcedure, Meta, Schema } from "@orpc/contract";
2
- //#region src/hitch/files.d.ts
3
2
  type HitchFile = {
4
3
  id: string;
5
4
  organizationId: string;
@@ -26,9 +25,9 @@ type CreateFileOutput = {
26
25
  data: PendingHitchFile;
27
26
  uploadUrl: string;
28
27
  };
29
- type FileIdInput = {
28
+ type FileInput = {
30
29
  params: {
31
- id: string;
30
+ fileId: string;
32
31
  };
33
32
  };
34
33
  type FileOutput = {
@@ -37,12 +36,10 @@ type FileOutput = {
37
36
  };
38
37
  type FilesContract = {
39
38
  create: ContractProcedure<Schema<CreateFileInput, CreateFileInput>, Schema<CreateFileOutput, CreateFileOutput>, Record<never, never>, Meta>;
40
- confirm: ContractProcedure<Schema<FileIdInput, FileIdInput>, Schema<FileOutput, FileOutput>, Record<never, never>, Meta>;
41
- get: ContractProcedure<Schema<FileIdInput, FileIdInput>, Schema<FileOutput, FileOutput>, Record<never, never>, Meta>;
39
+ confirm: ContractProcedure<Schema<FileInput, FileInput>, Schema<FileOutput, FileOutput>, Record<never, never>, Meta>;
40
+ get: ContractProcedure<Schema<FileInput, FileInput>, Schema<FileOutput, FileOutput>, Record<never, never>, Meta>;
42
41
  };
43
42
  declare const filesContract: FilesContract;
44
- //#endregion
45
- //#region src/hitch/upload.d.ts
46
43
  type FilesClient = {
47
44
  create(input: {
48
45
  body: {
@@ -60,7 +57,7 @@ type FilesClient = {
60
57
  }>;
61
58
  confirm(input: {
62
59
  params: {
63
- id: string;
60
+ fileId: string;
64
61
  };
65
62
  }, options?: {
66
63
  signal?: AbortSignal;
@@ -70,7 +67,7 @@ type FilesClient = {
70
67
  }>;
71
68
  get(input: {
72
69
  params: {
73
- id: string;
70
+ fileId: string;
74
71
  };
75
72
  }, options?: {
76
73
  signal?: AbortSignal;
@@ -104,5 +101,4 @@ type WithFilesUpload<T> = T extends {
104
101
  } : T & {
105
102
  files: FilesUpload;
106
103
  };
107
- //#endregion
108
104
  export { UploadedHitchFile as a, filesContract as c, UploadProgress as i, FilesUpload as n, WithFilesUpload as o, UploadFileOptions as r, uploadFile as s, FilesClient as t };