@hitch42/applet 0.1.0-beta.2
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/LICENSE +21 -0
- package/dist/_internal/client.d.ts +7 -0
- package/dist/_internal/client.js +60 -0
- package/dist/_internal/server.d.ts +79 -0
- package/dist/_internal/server.js +232 -0
- package/dist/applet.d.ts +1 -0
- package/dist/applet.js +160 -0
- package/dist/auth-DaVK0k5R.js +350 -0
- package/dist/batch-Qa4B4-Yp.d.ts +30 -0
- package/dist/build-BRkmrUcj.js +74 -0
- package/dist/build-CMluoV6q.js +2 -0
- package/dist/client.d.ts +48 -0
- package/dist/client.js +38 -0
- package/dist/contract-DMrpbaLJ.d.ts +23 -0
- package/dist/deploy-DKB-Tef7.js +2416 -0
- package/dist/generate-BFmIzHaR.js +484 -0
- package/dist/generate-CefnB-bp.js +2 -0
- package/dist/index.d.ts +74 -0
- package/dist/index.js +136 -0
- package/dist/init-CQRsnd3i.js +240 -0
- package/dist/lazy-client-x0DCI98S.js +64 -0
- package/dist/load-config-BnxocLAQ.js +56 -0
- package/dist/logs-m00pzr_O.js +183 -0
- package/dist/manifest-BQCAxATS.d.ts +81 -0
- package/dist/manifest-DyoJ1MIL.js +174 -0
- package/dist/project-CEmDnlKa.js +346 -0
- package/dist/react.d.ts +37 -0
- package/dist/react.js +517 -0
- package/dist/server.d.ts +46 -0
- package/dist/server.js +104 -0
- package/dist/upload-DRbVhuDm.js +114 -0
- package/dist/upload-YbRKEqR6.d.ts +112 -0
- package/dist/variables-CP7Ce3Np.js +133 -0
- package/dist/vite.d.ts +63 -0
- package/dist/vite.js +358 -0
- package/dist/workflow-CTo2kZF1.js +51 -0
- package/dist/workflow-DurM6Fwx.d.ts +28 -0
- package/package.json +104 -0
- package/scaffold/.agents/skills/hitch-applets/SKILL.md +137 -0
- package/scaffold/.agents/skills/hitch-applets/references/client.md +73 -0
- package/scaffold/.agents/skills/hitch-applets/references/files.md +109 -0
- package/scaffold/.agents/skills/hitch-applets/references/manifest.md +171 -0
- package/scaffold/.agents/skills/hitch-applets/references/markdown.md +37 -0
- package/scaffold/.agents/skills/hitch-applets/references/records.md +152 -0
- package/scaffold/.agents/skills/hitch-applets/references/schedules.md +86 -0
- package/scaffold/.agents/skills/hitch-applets/references/server.md +98 -0
- package/scaffold/.agents/skills/hitch-applets/references/workflows.md +73 -0
- package/scaffold/AGENTS.md +38 -0
- package/scaffold/applet/components.json +25 -0
- package/scaffold/applet/src/client/components/ui/alert.tsx +73 -0
- package/scaffold/applet/src/client/components/ui/avatar.tsx +100 -0
- package/scaffold/applet/src/client/components/ui/badge.tsx +55 -0
- package/scaffold/applet/src/client/components/ui/button.tsx +90 -0
- package/scaffold/applet/src/client/components/ui/card.tsx +85 -0
- package/scaffold/applet/src/client/components/ui/checkbox.tsx +35 -0
- package/scaffold/applet/src/client/components/ui/dialog.tsx +164 -0
- package/scaffold/applet/src/client/components/ui/dropdown-menu.tsx +235 -0
- package/scaffold/applet/src/client/components/ui/input-group.tsx +144 -0
- package/scaffold/applet/src/client/components/ui/input.tsx +22 -0
- package/scaffold/applet/src/client/components/ui/label.tsx +29 -0
- package/scaffold/applet/src/client/components/ui/popover.tsx +74 -0
- package/scaffold/applet/src/client/components/ui/select.tsx +243 -0
- package/scaffold/applet/src/client/components/ui/separator.tsx +25 -0
- package/scaffold/applet/src/client/components/ui/sheet.tsx +177 -0
- package/scaffold/applet/src/client/components/ui/skeleton.tsx +13 -0
- package/scaffold/applet/src/client/components/ui/spinner.tsx +16 -0
- package/scaffold/applet/src/client/components/ui/switch.tsx +41 -0
- package/scaffold/applet/src/client/components/ui/table.tsx +114 -0
- package/scaffold/applet/src/client/components/ui/tabs.tsx +78 -0
- package/scaffold/applet/src/client/components/ui/textarea.tsx +23 -0
- package/scaffold/applet/src/client/lib/utils.ts +6 -0
- package/scaffold/applet/src/client/styles/globals.css +136 -0
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
import { chmod, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { spawn } from "node:child_process";
|
|
4
|
+
import { homedir } from "node:os";
|
|
5
|
+
//#region ../shared/auth/src/store.mts
|
|
6
|
+
const DEFAULT_INSTANCE_URL = "https://app.hitch42.com";
|
|
7
|
+
var AuthenticationError = class extends Error {};
|
|
8
|
+
function isErrno(error, code) {
|
|
9
|
+
return error instanceof Error && "code" in error && error.code === code;
|
|
10
|
+
}
|
|
11
|
+
async function readJson(path) {
|
|
12
|
+
try {
|
|
13
|
+
return JSON.parse(await readFile(path, "utf8"));
|
|
14
|
+
} catch (error) {
|
|
15
|
+
if (isErrno(error, "ENOENT")) return void 0;
|
|
16
|
+
throw new Error(`Failed to read ${path}.`, { cause: error });
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
function isObject(value) {
|
|
20
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
21
|
+
}
|
|
22
|
+
function emptyCredentialStore() {
|
|
23
|
+
return { instances: {} };
|
|
24
|
+
}
|
|
25
|
+
async function healCredentialStore(credentialsPath, source, store, unrecognizedCount) {
|
|
26
|
+
const backupPath = `${credentialsPath}.bak`;
|
|
27
|
+
try {
|
|
28
|
+
try {
|
|
29
|
+
await writeFile(backupPath, source, {
|
|
30
|
+
flag: "wx",
|
|
31
|
+
mode: 384
|
|
32
|
+
});
|
|
33
|
+
} catch (error) {
|
|
34
|
+
if (!isErrno(error, "EEXIST")) throw error;
|
|
35
|
+
}
|
|
36
|
+
await chmod(backupPath, 384);
|
|
37
|
+
await writeFile(credentialsPath, `${JSON.stringify(store, null, 2)}\n`, { mode: 384 });
|
|
38
|
+
await chmod(credentialsPath, 384);
|
|
39
|
+
} catch {
|
|
40
|
+
console.error(unrecognizedCount === void 0 ? `hitch: failed to heal credential store at ${credentialsPath}; using an empty in-memory store.` : `hitch: failed to heal credential store at ${credentialsPath}; using recognized credentials in memory only.`);
|
|
41
|
+
return store;
|
|
42
|
+
}
|
|
43
|
+
console.error(unrecognizedCount === void 0 ? `hitch: reset unreadable credentials file (backup: ${backupPath}).` : unrecognizedCount === 0 ? `hitch: migrated credentials to per-organization storage (backup: ${backupPath}).` : `hitch: removed ${unrecognizedCount} unrecognized credential ${unrecognizedCount === 1 ? "entry" : "entries"} (backup: ${backupPath}).`);
|
|
44
|
+
return store;
|
|
45
|
+
}
|
|
46
|
+
async function readRawCredentialStore(options = {}) {
|
|
47
|
+
const { credentialsPath } = storePaths(options);
|
|
48
|
+
let source;
|
|
49
|
+
try {
|
|
50
|
+
source = await readFile(credentialsPath, "utf8");
|
|
51
|
+
} catch (error) {
|
|
52
|
+
if (isErrno(error, "ENOENT")) return emptyCredentialStore();
|
|
53
|
+
throw new Error(`Failed to read ${credentialsPath}.`, { cause: error });
|
|
54
|
+
}
|
|
55
|
+
let value;
|
|
56
|
+
try {
|
|
57
|
+
value = JSON.parse(source);
|
|
58
|
+
} catch {
|
|
59
|
+
return healCredentialStore(credentialsPath, source, emptyCredentialStore());
|
|
60
|
+
}
|
|
61
|
+
if (!isObject(value) || !isObject(value.instances)) return healCredentialStore(credentialsPath, source, emptyCredentialStore());
|
|
62
|
+
const instances = Object.fromEntries(Object.entries(value.instances).flatMap(([instanceUrl, entry]) => {
|
|
63
|
+
if (isObject(entry) && typeof entry.organizationId === "string") {
|
|
64
|
+
const credentials = parseCredentials(entry);
|
|
65
|
+
return credentials === void 0 ? [] : [[instanceUrl, { [credentials.orgCode]: entry }]];
|
|
66
|
+
}
|
|
67
|
+
if (!isObject(entry)) return [];
|
|
68
|
+
return [[instanceUrl, Object.fromEntries(Object.entries(entry).filter(([orgCode, credentials]) => parseCredentials(credentials)?.orgCode === orgCode))]];
|
|
69
|
+
}));
|
|
70
|
+
const reshaped = Object.values(value.instances).some((entry) => !isObject(entry) || typeof entry.organizationId === "string");
|
|
71
|
+
const countLeaves = (store) => Object.values(store).reduce((total, entry) => total + (isObject(entry) && typeof entry.organizationId !== "string" ? Object.keys(entry).length : 1), 0);
|
|
72
|
+
const unrecognizedCount = countLeaves(value.instances) - countLeaves(instances);
|
|
73
|
+
if (!reshaped && unrecognizedCount === 0) return value;
|
|
74
|
+
return healCredentialStore(credentialsPath, source, { instances }, unrecognizedCount);
|
|
75
|
+
}
|
|
76
|
+
function parseCredentials(value) {
|
|
77
|
+
if (!isObject(value) || typeof value.organizationId !== "string" || typeof value.orgCode !== "string") return;
|
|
78
|
+
const organization = {
|
|
79
|
+
organizationId: value.organizationId,
|
|
80
|
+
orgCode: value.orgCode
|
|
81
|
+
};
|
|
82
|
+
if ((value.type === void 0 || value.type === "session") && typeof value.accessToken === "string" && (value.expiresAt === void 0 || typeof value.expiresAt === "number")) return {
|
|
83
|
+
type: "session",
|
|
84
|
+
accessToken: value.accessToken,
|
|
85
|
+
...value.expiresAt === void 0 ? {} : { expiresAt: value.expiresAt },
|
|
86
|
+
...organization
|
|
87
|
+
};
|
|
88
|
+
if (value.type === "apiKey" && typeof value.apiKey === "string") return {
|
|
89
|
+
type: "apiKey",
|
|
90
|
+
apiKey: value.apiKey,
|
|
91
|
+
...organization
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
function normalizeInstanceUrl(instanceUrl) {
|
|
95
|
+
let url;
|
|
96
|
+
try {
|
|
97
|
+
url = new URL(instanceUrl);
|
|
98
|
+
} catch (error) {
|
|
99
|
+
throw new Error(`Invalid Hitch instance URL: ${instanceUrl}`, { cause: error });
|
|
100
|
+
}
|
|
101
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") throw new Error(`Invalid Hitch instance URL protocol: ${url.protocol}`);
|
|
102
|
+
url.pathname = url.pathname.replace(/\/+$/, "");
|
|
103
|
+
url.search = "";
|
|
104
|
+
url.hash = "";
|
|
105
|
+
return url.toString().replace(/\/$/, "");
|
|
106
|
+
}
|
|
107
|
+
function organizationInstanceUrl(instanceUrl, orgCode) {
|
|
108
|
+
const url = new URL(normalizeInstanceUrl(instanceUrl));
|
|
109
|
+
const labels = url.hostname.split(".");
|
|
110
|
+
if (labels[0] !== "app" || labels.length < 2) return url.toString().replace(/\/$/, "");
|
|
111
|
+
url.hostname = [orgCode, ...labels.slice(1)].join(".");
|
|
112
|
+
return url.toString().replace(/\/$/, "");
|
|
113
|
+
}
|
|
114
|
+
function storePaths(options = {}) {
|
|
115
|
+
const env = options.env ?? process.env;
|
|
116
|
+
const home = options.homeDir ?? homedir();
|
|
117
|
+
const configDir = join(env.XDG_CONFIG_HOME || join(home, ".config"), "hitch");
|
|
118
|
+
return {
|
|
119
|
+
configDir,
|
|
120
|
+
cacheDir: join(env.XDG_CACHE_HOME || join(home, ".cache"), "hitch"),
|
|
121
|
+
configPath: join(configDir, "config.json"),
|
|
122
|
+
credentialsPath: join(configDir, "credentials.json")
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
async function resolveInstanceUrl(override, options = {}) {
|
|
126
|
+
if (override !== void 0) return normalizeInstanceUrl(override);
|
|
127
|
+
const envInstanceUrl = (options.env ?? process.env).HITCH_API_URL;
|
|
128
|
+
if (envInstanceUrl !== void 0 && envInstanceUrl.length > 0) return normalizeInstanceUrl(envInstanceUrl);
|
|
129
|
+
const { configPath } = storePaths(options);
|
|
130
|
+
const config = await readJson(configPath);
|
|
131
|
+
if (config === void 0) return DEFAULT_INSTANCE_URL;
|
|
132
|
+
if (!isObject(config) || config.instanceUrl !== void 0 && typeof config.instanceUrl !== "string") throw new Error(`${configPath}: expected { instanceUrl?: string }.`);
|
|
133
|
+
return normalizeInstanceUrl(config.instanceUrl ?? "https://app.hitch42.com");
|
|
134
|
+
}
|
|
135
|
+
async function readCredentialStore(options = {}) {
|
|
136
|
+
const store = await readRawCredentialStore(options);
|
|
137
|
+
return { instances: Object.fromEntries(Object.entries(store.instances).map(([instanceUrl, credentialsByOrg]) => [instanceUrl, Object.fromEntries(Object.entries(credentialsByOrg).flatMap(([orgCode, value]) => {
|
|
138
|
+
const credentials = parseCredentials(value);
|
|
139
|
+
return credentials === void 0 ? [] : [[orgCode, credentials]];
|
|
140
|
+
}))])) };
|
|
141
|
+
}
|
|
142
|
+
async function getCredentialsForOrg(instanceUrl, orgCode, options = {}) {
|
|
143
|
+
return (await readCredentialStore(options)).instances[normalizeInstanceUrl(instanceUrl)]?.[orgCode];
|
|
144
|
+
}
|
|
145
|
+
function environmentAuth(options) {
|
|
146
|
+
const apiKey = (options.env ?? process.env).HITCH_API_KEY;
|
|
147
|
+
return apiKey !== void 0 && apiKey.length > 0 ? {
|
|
148
|
+
accessToken: apiKey,
|
|
149
|
+
source: "environment"
|
|
150
|
+
} : void 0;
|
|
151
|
+
}
|
|
152
|
+
function resolvedStoredAuth(credentials) {
|
|
153
|
+
return credentials === void 0 ? void 0 : {
|
|
154
|
+
accessToken: credentials.type === "apiKey" ? credentials.apiKey : credentials.accessToken,
|
|
155
|
+
credentialType: credentials.type,
|
|
156
|
+
organizationId: credentials.organizationId,
|
|
157
|
+
orgCode: credentials.orgCode,
|
|
158
|
+
source: "store"
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
function resolveStoredAuthForOrg(credentialsByOrg, orgCode) {
|
|
162
|
+
const exact = resolvedStoredAuth(credentialsByOrg[orgCode]);
|
|
163
|
+
if (exact !== void 0) return exact;
|
|
164
|
+
const sessions = Object.values(credentialsByOrg).filter((credentials) => credentials.type === "session").filter((credentials, index, all) => all.findIndex(({ accessToken }) => accessToken === credentials.accessToken) === index);
|
|
165
|
+
if (sessions.length === 1) return resolvedStoredAuth(sessions[0]);
|
|
166
|
+
if (sessions.length > 1) throw new AuthenticationError(`Multiple sessions are stored. Run hitch login for organization "${orgCode}".`);
|
|
167
|
+
const apiKeyOrgs = Object.values(credentialsByOrg).filter((credentials) => credentials.type === "apiKey").map(({ orgCode: storedOrg }) => storedOrg).sort();
|
|
168
|
+
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}".`);
|
|
169
|
+
}
|
|
170
|
+
async function resolveAuthForOrg(instanceUrl, orgCode, options = {}) {
|
|
171
|
+
const auth = environmentAuth(options);
|
|
172
|
+
if (auth !== void 0) return auth;
|
|
173
|
+
const normalizedUrl = normalizeInstanceUrl(instanceUrl);
|
|
174
|
+
const credentialsByOrg = Object.entries((await readCredentialStore(options)).instances).find(([instance]) => instance === normalizedUrl)?.[1];
|
|
175
|
+
return resolveStoredAuthForOrg(credentialsByOrg ?? {}, orgCode);
|
|
176
|
+
}
|
|
177
|
+
function rejectEnvironmentKey(response, auth) {
|
|
178
|
+
if (auth.source === "environment" && response.status === 401) throw new AuthenticationError("HITCH_API_KEY was rejected.");
|
|
179
|
+
}
|
|
180
|
+
function bearerToken(credentials) {
|
|
181
|
+
return "apiKey" in credentials ? credentials.apiKey : credentials.accessToken;
|
|
182
|
+
}
|
|
183
|
+
async function setCredentials(instanceUrl, credentials, options = {}) {
|
|
184
|
+
const paths = storePaths(options);
|
|
185
|
+
const store = await readRawCredentialStore(options);
|
|
186
|
+
const normalizedUrl = normalizeInstanceUrl(instanceUrl);
|
|
187
|
+
const credentialsByOrg = store.instances[normalizedUrl];
|
|
188
|
+
store.instances[normalizedUrl] = {
|
|
189
|
+
...isObject(credentialsByOrg) ? credentialsByOrg : {},
|
|
190
|
+
[credentials.orgCode]: credentials
|
|
191
|
+
};
|
|
192
|
+
await mkdir(paths.configDir, { recursive: true });
|
|
193
|
+
await writeFile(paths.credentialsPath, `${JSON.stringify(store, null, 2)}\n`, { mode: 384 });
|
|
194
|
+
await chmod(paths.credentialsPath, 384);
|
|
195
|
+
}
|
|
196
|
+
//#endregion
|
|
197
|
+
//#region ../shared/auth/src/device.mts
|
|
198
|
+
const CLIENT_ID = "hitch-cli";
|
|
199
|
+
const DEVICE_GRANT = "urn:ietf:params:oauth:grant-type:device_code";
|
|
200
|
+
function defaultOpenUrl(url) {
|
|
201
|
+
try {
|
|
202
|
+
const child = spawn(process.platform === "darwin" ? "open" : "xdg-open", [url], {
|
|
203
|
+
detached: true,
|
|
204
|
+
stdio: "ignore"
|
|
205
|
+
});
|
|
206
|
+
child.on("error", () => void 0);
|
|
207
|
+
child.unref();
|
|
208
|
+
} catch {}
|
|
209
|
+
}
|
|
210
|
+
function requiredObject(value, message) {
|
|
211
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) throw new Error(message);
|
|
212
|
+
return value;
|
|
213
|
+
}
|
|
214
|
+
async function jsonResponse(response) {
|
|
215
|
+
try {
|
|
216
|
+
return requiredObject(await response.json(), "Expected a JSON object response.");
|
|
217
|
+
} catch (error) {
|
|
218
|
+
throw new Error(`Invalid JSON response from ${response.url || "Hitch"}.`, { cause: error });
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
async function postJson(fetcher, url, body) {
|
|
222
|
+
const response = await fetcher(url, {
|
|
223
|
+
method: "POST",
|
|
224
|
+
headers: { "content-type": "application/json" },
|
|
225
|
+
body: JSON.stringify(body)
|
|
226
|
+
});
|
|
227
|
+
return {
|
|
228
|
+
body: await jsonResponse(response),
|
|
229
|
+
response
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
function parseDeviceCode(value) {
|
|
233
|
+
if (typeof value.device_code !== "string" || typeof value.user_code !== "string" || typeof value.verification_uri !== "string" || typeof value.verification_uri_complete !== "string" || typeof value.expires_in !== "number" || value.interval !== void 0 && typeof value.interval !== "number") throw new AuthenticationError("Hitch returned an invalid device authorization response.");
|
|
234
|
+
return value;
|
|
235
|
+
}
|
|
236
|
+
function parseToken(value) {
|
|
237
|
+
return typeof value.access_token === "string" && typeof value.token_type === "string" ? value : void 0;
|
|
238
|
+
}
|
|
239
|
+
function parseDeviceOrganization(value) {
|
|
240
|
+
if (typeof value.organizationId !== "string" || typeof value.orgCode !== "string") throw new AuthenticationError("Hitch returned an invalid device organization response.");
|
|
241
|
+
return value;
|
|
242
|
+
}
|
|
243
|
+
function parseMe(value) {
|
|
244
|
+
const person = requiredObject(value.person, "Hitch returned an invalid identity response.");
|
|
245
|
+
if (typeof person.id !== "string" || typeof person.firstName !== "string" || typeof person.lastName !== "string" || typeof person.email !== "string" || !Array.isArray(value.memberships)) throw new Error("Hitch returned an invalid identity response.");
|
|
246
|
+
const organization = value.organization === void 0 || value.organization === null ? null : requiredObject(value.organization, "Hitch returned an invalid identity response.");
|
|
247
|
+
if (organization !== null && (typeof organization.id !== "string" || typeof organization.code !== "string")) throw new Error("Hitch returned an invalid identity response.");
|
|
248
|
+
return {
|
|
249
|
+
memberships: value.memberships.map((membership) => {
|
|
250
|
+
const item = requiredObject(membership, "Hitch returned an invalid membership.");
|
|
251
|
+
if (typeof item.orgId !== "string" || typeof item.code !== "string" || typeof item.name !== "string") throw new Error("Hitch returned an invalid membership.");
|
|
252
|
+
return item;
|
|
253
|
+
}),
|
|
254
|
+
organization,
|
|
255
|
+
person
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
async function fetchMe(fetcher, instanceUrl, auth) {
|
|
259
|
+
const response = await fetcher(`${instanceUrl}/api/v1/me`, { headers: { Authorization: `Bearer ${auth.accessToken}` } });
|
|
260
|
+
rejectEnvironmentKey(response, auth);
|
|
261
|
+
if (auth.credentialType === "apiKey" && response.status === 401) throw new AuthenticationError("API key rejected.");
|
|
262
|
+
const body = await jsonResponse(response);
|
|
263
|
+
if (!response.ok) throw new AuthenticationError(typeof body.message === "string" ? body.message : `Failed to load the signed-in identity (${response.status}).`);
|
|
264
|
+
return parseMe(body);
|
|
265
|
+
}
|
|
266
|
+
async function pollForToken(fetcher, instanceUrl, device, now, sleep) {
|
|
267
|
+
const deadline = now() + device.expires_in * 1e3;
|
|
268
|
+
let interval = (device.interval ?? 5) * 1e3;
|
|
269
|
+
while (now() < deadline) {
|
|
270
|
+
await sleep(Math.min(interval, deadline - now()));
|
|
271
|
+
if (now() >= deadline) break;
|
|
272
|
+
const { body, response } = await postJson(fetcher, `${instanceUrl}/api/auth/device/token`, {
|
|
273
|
+
grant_type: DEVICE_GRANT,
|
|
274
|
+
device_code: device.device_code,
|
|
275
|
+
client_id: CLIENT_ID
|
|
276
|
+
});
|
|
277
|
+
const token = parseToken(body);
|
|
278
|
+
if (response.ok && token !== void 0) return token;
|
|
279
|
+
switch (body.error) {
|
|
280
|
+
case "authorization_pending": break;
|
|
281
|
+
case "slow_down":
|
|
282
|
+
interval += 5e3;
|
|
283
|
+
break;
|
|
284
|
+
case "expired_token": throw new AuthenticationError("The sign-in request expired. Run hitch login again.");
|
|
285
|
+
case "access_denied": throw new AuthenticationError("Sign-in was denied.");
|
|
286
|
+
default: throw new AuthenticationError(typeof body.error_description === "string" ? body.error_description : `Sign-in failed (${response.status}).`);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
throw new AuthenticationError("The sign-in request expired. Run hitch login again.");
|
|
290
|
+
}
|
|
291
|
+
async function loginWithDevice(instanceUrl, options = {}) {
|
|
292
|
+
const fetcher = options.fetch ?? globalThis.fetch;
|
|
293
|
+
const now = options.now ?? Date.now;
|
|
294
|
+
const { body, response } = await postJson(fetcher, `${instanceUrl}/api/auth/device/code`, { client_id: CLIENT_ID });
|
|
295
|
+
if (!response.ok) throw new AuthenticationError(typeof body.error_description === "string" ? body.error_description : `Could not start sign-in (${response.status}).`);
|
|
296
|
+
const device = parseDeviceCode(body);
|
|
297
|
+
const verificationUrl = options.expectedOrg === void 0 ? device.verification_uri_complete : `${device.verification_uri_complete}&org=${encodeURIComponent(options.expectedOrg)}`;
|
|
298
|
+
(options.print ?? console.log)(`Open ${verificationUrl} and confirm code ${device.user_code}.`);
|
|
299
|
+
if (!options.noBrowser) try {
|
|
300
|
+
await (options.openUrl ?? defaultOpenUrl)(verificationUrl);
|
|
301
|
+
} catch {}
|
|
302
|
+
const token = await pollForToken(fetcher, instanceUrl, device, now, options.sleep ?? ((milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds))));
|
|
303
|
+
const { body: organizationBody, response: organizationResponse } = await postJson(fetcher, `${instanceUrl}/api/auth/device/organization`, { device_code: device.device_code });
|
|
304
|
+
if (!organizationResponse.ok) throw new AuthenticationError("The sign-in did not resolve an organization.");
|
|
305
|
+
const organization = parseDeviceOrganization(organizationBody);
|
|
306
|
+
const me = await fetchMe(fetcher, instanceUrl, {
|
|
307
|
+
accessToken: token.access_token,
|
|
308
|
+
credentialType: "session",
|
|
309
|
+
source: "store"
|
|
310
|
+
});
|
|
311
|
+
const membership = me.memberships.find(({ orgId }) => orgId === organization.organizationId);
|
|
312
|
+
if (membership === void 0) throw new AuthenticationError("The device session is not scoped to a Hitch organization.");
|
|
313
|
+
const credentials = {
|
|
314
|
+
type: "session",
|
|
315
|
+
accessToken: token.access_token,
|
|
316
|
+
expiresAt: token.expires_in === void 0 ? void 0 : now() + token.expires_in * 1e3,
|
|
317
|
+
organizationId: organization.organizationId,
|
|
318
|
+
orgCode: organization.orgCode
|
|
319
|
+
};
|
|
320
|
+
await setCredentials(instanceUrl, credentials, options.store);
|
|
321
|
+
if (options.expectedOrg !== void 0 && organization.orgCode !== options.expectedOrg) throw new AuthenticationError(`logged in to "${organization.orgCode}" but this applet targets "${options.expectedOrg}" — confirm the right organization on the device page`);
|
|
322
|
+
return {
|
|
323
|
+
credentials,
|
|
324
|
+
me,
|
|
325
|
+
membership
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
//#endregion
|
|
329
|
+
//#region src/auth.ts
|
|
330
|
+
async function fetchWithOrgAuth(orgCode, options, request) {
|
|
331
|
+
const fetcher = options.fetch ?? globalThis.fetch;
|
|
332
|
+
const instanceUrl = await resolveInstanceUrl(void 0, options.store);
|
|
333
|
+
const organizationUrl = organizationInstanceUrl(instanceUrl, orgCode);
|
|
334
|
+
const auth = await resolveAuthForOrg(instanceUrl, orgCode, options.store);
|
|
335
|
+
if (auth !== void 0) {
|
|
336
|
+
const response = await request(organizationUrl, bearerToken(auth));
|
|
337
|
+
if (response.status !== 401 && response.status !== 403) return response;
|
|
338
|
+
}
|
|
339
|
+
if (!(options.stdinIsTTY ?? process.stdin.isTTY === true)) throw new Error("not logged in — set HITCH_API_KEY or run in a terminal to log in");
|
|
340
|
+
await (options.login ?? loginWithDevice)(instanceUrl, {
|
|
341
|
+
expectedOrg: orgCode,
|
|
342
|
+
fetch: fetcher,
|
|
343
|
+
store: options.store
|
|
344
|
+
});
|
|
345
|
+
const credentials = await getCredentialsForOrg(instanceUrl, orgCode, options.store);
|
|
346
|
+
if (credentials === void 0) throw new Error(`login did not provide credentials for organization "${orgCode}"`);
|
|
347
|
+
return request(organizationUrl, bearerToken(credentials));
|
|
348
|
+
}
|
|
349
|
+
//#endregion
|
|
350
|
+
export { resolveInstanceUrl as i, organizationInstanceUrl as n, resolveAuthForOrg as r, fetchWithOrgAuth as t };
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { AnyContractRouter } from "@orpc/contract";
|
|
2
|
+
//#region src/hitch/batch.d.ts
|
|
3
|
+
type BatchVerb = "create" | "update" | "delete" | "updateWhere";
|
|
4
|
+
type BatchTxRecords<Records> = { [Code in keyof Records]: { [Verb in keyof Records[Code] & BatchVerb]: Records[Code][Verb] extends ((...args: infer Args) => unknown) ? (input: Args[0]) => void : never; }; };
|
|
5
|
+
type BatchTx<T> = T extends {
|
|
6
|
+
records: infer Records;
|
|
7
|
+
} ? BatchTxRecords<Records> : never;
|
|
8
|
+
type BatchRecorderOptions = {
|
|
9
|
+
dryRun?: boolean;
|
|
10
|
+
idempotencyKey?: string;
|
|
11
|
+
};
|
|
12
|
+
type BatchRecorderCall<T, Batch> = Batch extends ((...args: infer _Args) => infer Result) ? (record: (tx: BatchTx<T>) => void | Promise<void>, options?: BatchRecorderOptions) => Result : never;
|
|
13
|
+
type WithBatchRecorder<T> = T extends {
|
|
14
|
+
records: unknown;
|
|
15
|
+
batch: infer Batch;
|
|
16
|
+
} ? Omit<T, "batch"> & {
|
|
17
|
+
/**
|
|
18
|
+
* Record record writes inside `record` and send them as one atomic batch.
|
|
19
|
+
*
|
|
20
|
+
* The callback receives a `tx` whose `create` / `update` / `updateWhere` /
|
|
21
|
+
* `delete` calls are buffered, not sent immediately; the whole batch runs
|
|
22
|
+
* in a single transaction when the callback resolves.
|
|
23
|
+
*
|
|
24
|
+
* @remarks A batch may contain at most **100** requests. More than that is
|
|
25
|
+
* rejected with a `400` before any write runs.
|
|
26
|
+
*/
|
|
27
|
+
batch: BatchRecorderCall<T, Batch> & Batch;
|
|
28
|
+
} : T;
|
|
29
|
+
//#endregion
|
|
30
|
+
export { WithBatchRecorder as t };
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { r as prepareAppletProject } from "./project-CEmDnlKa.js";
|
|
2
|
+
import { rm } from "node:fs/promises";
|
|
3
|
+
import { resolve } from "node:path";
|
|
4
|
+
import { spawn } from "node:child_process";
|
|
5
|
+
//#region src/build.ts
|
|
6
|
+
function isMissingFile(error) {
|
|
7
|
+
return error instanceof Error && "code" in error && error.code === "ENOENT";
|
|
8
|
+
}
|
|
9
|
+
async function runPnpm(args, cwd) {
|
|
10
|
+
await new Promise((resolveRun, rejectRun) => {
|
|
11
|
+
let finished = false;
|
|
12
|
+
const child = spawn("pnpm", args, {
|
|
13
|
+
cwd,
|
|
14
|
+
stdio: "inherit"
|
|
15
|
+
});
|
|
16
|
+
child.once("error", (error) => {
|
|
17
|
+
finished = true;
|
|
18
|
+
rejectRun(isMissingFile(error) ? /* @__PURE__ */ new Error("pnpm was not found. Install pnpm, then run applet build again.") : error);
|
|
19
|
+
});
|
|
20
|
+
child.once("close", (code) => {
|
|
21
|
+
if (finished) return;
|
|
22
|
+
finished = true;
|
|
23
|
+
if (code === 0) {
|
|
24
|
+
resolveRun();
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
rejectRun(/* @__PURE__ */ new Error(`pnpm ${args.join(" ")} failed${code === null ? "." : ` with exit code ${code}.`}`));
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
async function buildApplet(changesetId, outDir = ".hitch/dist") {
|
|
32
|
+
const appletRoot = process.cwd();
|
|
33
|
+
const { workspaceRoot } = await prepareAppletProject(appletRoot);
|
|
34
|
+
await rm(resolve(appletRoot, ".hitch"), {
|
|
35
|
+
force: true,
|
|
36
|
+
recursive: true
|
|
37
|
+
});
|
|
38
|
+
await runPnpm(["install"], workspaceRoot);
|
|
39
|
+
await new Promise((resolveBuild, rejectBuild) => {
|
|
40
|
+
let finished = false;
|
|
41
|
+
const child = spawn("pnpm", [
|
|
42
|
+
"exec",
|
|
43
|
+
"vite",
|
|
44
|
+
"build",
|
|
45
|
+
"--outDir",
|
|
46
|
+
outDir
|
|
47
|
+
], {
|
|
48
|
+
cwd: appletRoot,
|
|
49
|
+
stdio: "inherit",
|
|
50
|
+
env: {
|
|
51
|
+
...process.env,
|
|
52
|
+
...changesetId === void 0 ? {} : { HITCH_CHANGESET_ID: changesetId }
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
child.once("error", (error) => {
|
|
56
|
+
finished = true;
|
|
57
|
+
rejectBuild(isMissingFile(error) ? /* @__PURE__ */ new Error("pnpm was not found. Install pnpm, then run applet build again.") : error);
|
|
58
|
+
});
|
|
59
|
+
child.once("close", (code) => {
|
|
60
|
+
if (finished) return;
|
|
61
|
+
finished = true;
|
|
62
|
+
if (code === 0) {
|
|
63
|
+
resolveBuild();
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
rejectBuild(/* @__PURE__ */ new Error(`pnpm exec vite build failed${code === null ? "." : ` with exit code ${code}.`}`));
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
async function runBuild(options = {}) {
|
|
71
|
+
await buildApplet(options.changesetId);
|
|
72
|
+
}
|
|
73
|
+
//#endregion
|
|
74
|
+
export { runBuild as n, buildApplet as t };
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { f as RegisteredContract, m as RegisteredRouter, p as RegisteredManifest } from "./manifest-BQCAxATS.js";
|
|
2
|
+
import { t as NarrowContract } from "./contract-DMrpbaLJ.js";
|
|
3
|
+
import { t as WithBatchRecorder } from "./batch-Qa4B4-Yp.js";
|
|
4
|
+
import { o as WithFilesUpload } from "./upload-YbRKEqR6.js";
|
|
5
|
+
import { RouterClient } from "@orpc/server";
|
|
6
|
+
import { ContractRouterClient } from "@orpc/contract";
|
|
7
|
+
import { NestedClient } from "@orpc/client";
|
|
8
|
+
import { RouterUtils } from "@orpc/tanstack-query";
|
|
9
|
+
//#region src/query-utils.d.ts
|
|
10
|
+
type Exact<Expected, Actual> = Actual extends symbol ? Actual : Actual extends Expected ? Actual extends readonly unknown[] ? Actual : Actual extends object ? NonNullable<Expected> extends object ? { [Key in keyof Actual]: Key extends keyof NonNullable<Expected> ? Exact<NonNullable<Expected>[Key], Actual[Key]> : never; } : Actual : Actual : never;
|
|
11
|
+
type QueryOptionsFunction<Utils> = Extract<Utils extends {
|
|
12
|
+
queryOptions: infer Function;
|
|
13
|
+
} ? Function : never, (...args: never[]) => unknown>;
|
|
14
|
+
type QueryOptionsArgument<Utils> = NonNullable<Parameters<QueryOptionsFunction<Utils>>[0]>;
|
|
15
|
+
type ExactOptions<Input, Options> = Options extends {
|
|
16
|
+
input: infer Actual;
|
|
17
|
+
} ? Options & {
|
|
18
|
+
input: Exact<Input, Actual>;
|
|
19
|
+
} : Options extends {
|
|
20
|
+
input?: infer Actual;
|
|
21
|
+
} ? Options & {
|
|
22
|
+
input?: Exact<Input, Actual>;
|
|
23
|
+
} : Options;
|
|
24
|
+
type QueryOptionsResult<Utils, Options> = Options & Omit<ReturnType<QueryOptionsFunction<Utils>>, keyof Options>;
|
|
25
|
+
type ExactQueryOptions<Utils, Input> = <Options extends QueryOptionsArgument<Utils> = QueryOptionsArgument<Utils>>(...rest: undefined extends Input ? [options?: ExactOptions<Input, Options>] : [options: ExactOptions<Input, Options>]) => QueryOptionsResult<Utils, Options>;
|
|
26
|
+
type ClientTree = NestedClient<Record<never, never>>;
|
|
27
|
+
type ExactRouterUtils<Client extends ClientTree> = Client extends ((...args: infer Arguments) => unknown) ? Omit<RouterUtils<Client>, "queryOptions"> & {
|
|
28
|
+
queryOptions: ExactQueryOptions<RouterUtils<Client>, Arguments[0]>;
|
|
29
|
+
} : Omit<RouterUtils<Client>, keyof Client> & { [Key in keyof Client]: Client[Key] extends ClientTree ? ExactRouterUtils<Client[Key]> : never; };
|
|
30
|
+
//#endregion
|
|
31
|
+
//#region src/author/client.d.ts
|
|
32
|
+
type AuthorContract = NarrowContract<RegisteredContract, RegisteredManifest>;
|
|
33
|
+
type BaseHitchClient = ContractRouterClient<AuthorContract>;
|
|
34
|
+
type AuthorHitchClient = WithBatchRecorder<BaseHitchClient>;
|
|
35
|
+
type BaseHitchUtils = ExactRouterUtils<BaseHitchClient>;
|
|
36
|
+
type AuthorHitchUtils = AuthorHitchClient extends {
|
|
37
|
+
batch: infer Batch;
|
|
38
|
+
} ? BaseHitchUtils extends {
|
|
39
|
+
batch: infer BatchUtils;
|
|
40
|
+
} ? Omit<BaseHitchUtils, "batch"> & {
|
|
41
|
+
batch: Omit<BatchUtils, "call"> & {
|
|
42
|
+
call: Batch;
|
|
43
|
+
};
|
|
44
|
+
} : BaseHitchUtils : BaseHitchUtils;
|
|
45
|
+
declare const hitch: WithFilesUpload<AuthorHitchUtils>;
|
|
46
|
+
declare const api: ExactRouterUtils<RouterClient<RegisteredRouter>>;
|
|
47
|
+
//#endregion
|
|
48
|
+
export { api, hitch };
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { n as withFilesUpload, r as getRegisteredHitchContract } from "./upload-DRbVhuDm.js";
|
|
2
|
+
import { n as withBatchRecorder, t as createLazyClientProxy } from "./lazy-client-x0DCI98S.js";
|
|
3
|
+
import { createORPCClient } from "@orpc/client";
|
|
4
|
+
import { OpenAPILink } from "@orpc/openapi-client/fetch";
|
|
5
|
+
import { createTanstackQueryUtils } from "@orpc/tanstack-query";
|
|
6
|
+
import { RPCLink } from "@orpc/client/fetch";
|
|
7
|
+
//#region src/hitch/browser.ts
|
|
8
|
+
function createHitchClient(contract) {
|
|
9
|
+
return createLazyClientProxy(() => createORPCClient(new OpenAPILink(contract, {
|
|
10
|
+
url: `${location.origin}/api`,
|
|
11
|
+
fetch: (request, init) => globalThis.fetch(request, init)
|
|
12
|
+
})));
|
|
13
|
+
}
|
|
14
|
+
//#endregion
|
|
15
|
+
//#region src/client.ts
|
|
16
|
+
function createClient(...[options]) {
|
|
17
|
+
const clientOptions = options;
|
|
18
|
+
return createLazyClientProxy(() => {
|
|
19
|
+
const base = clientOptions?.base ?? `${import.meta.env.BASE_URL}api`;
|
|
20
|
+
const origin = typeof location === "undefined" ? "http://localhost" : location.origin;
|
|
21
|
+
return createORPCClient(new RPCLink({
|
|
22
|
+
url: new URL(base, origin),
|
|
23
|
+
fetch: (request, init) => (clientOptions?.fetch ?? globalThis.fetch)(request, init)
|
|
24
|
+
}));
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
//#endregion
|
|
28
|
+
//#region src/author/client.ts
|
|
29
|
+
function buildClientHitch() {
|
|
30
|
+
const contract = getRegisteredHitchContract();
|
|
31
|
+
if (contract === void 0) throw new Error("`hitch` is unavailable: run `applet generate` first. The framework imports your .hitch automatically at build; in plain node import your .hitch once before calling hitch.");
|
|
32
|
+
return withBatchRecorder(createHitchClient(contract), contract);
|
|
33
|
+
}
|
|
34
|
+
const hitchClient = createLazyClientProxy(buildClientHitch);
|
|
35
|
+
const hitch = withFilesUpload(createTanstackQueryUtils(hitchClient), () => hitchClient.files);
|
|
36
|
+
const api = createTanstackQueryUtils(createClient());
|
|
37
|
+
//#endregion
|
|
38
|
+
export { api, hitch };
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { n as AppletManifest } from "./manifest-BQCAxATS.js";
|
|
2
|
+
import { AnyContractRouter } from "@orpc/contract";
|
|
3
|
+
//#region src/hitch/contract.d.ts
|
|
4
|
+
type ManifestObjectFor<Manifest, Code> = Manifest extends {
|
|
5
|
+
objects: readonly (infer Object)[];
|
|
6
|
+
} ? Object extends {
|
|
7
|
+
code: infer ObjectCode;
|
|
8
|
+
} ? Code extends ObjectCode ? Object : never : never : never;
|
|
9
|
+
type ManifestObjectCodes<Manifest> = Manifest extends {
|
|
10
|
+
objects: readonly (infer Object)[];
|
|
11
|
+
} ? Object extends {
|
|
12
|
+
code: infer Code extends PropertyKey;
|
|
13
|
+
} ? Code : never : never;
|
|
14
|
+
type Capability<ManifestObject, Key extends "canCreate" | "canDelete" | "readWrite"> = ManifestObject extends Record<Key, infer Value> ? Value : never;
|
|
15
|
+
type NarrowProcedures<Procedures, ManifestObject> = Omit<Procedures, (true extends Capability<ManifestObject, "canCreate"> ? never : "create") | (true extends Capability<ManifestObject, "canDelete"> ? never : "delete") | (Capability<ManifestObject, "readWrite"> extends readonly [] ? "update" | "updateWhere" : never)>;
|
|
16
|
+
type NarrowContract<Contract, Manifest> = Contract extends {
|
|
17
|
+
records: infer Records;
|
|
18
|
+
} ? Omit<Contract, "records"> & {
|
|
19
|
+
records: { [Code in keyof Records & ManifestObjectCodes<Manifest>]: NarrowProcedures<Records[Code], ManifestObjectFor<Manifest, Code>>; };
|
|
20
|
+
} : Contract;
|
|
21
|
+
declare function pruneContract<const Contract extends AnyContractRouter, const Manifest extends AppletManifest>(contract: Contract, manifest: Manifest): NarrowContract<Contract, Manifest>;
|
|
22
|
+
//#endregion
|
|
23
|
+
export { pruneContract as n, NarrowContract as t };
|