@hitch42/applet 0.1.0-beta.10
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 +169 -0
- package/dist/_internal/server.d.ts +92 -0
- package/dist/_internal/server.js +181 -0
- package/dist/applet.d.ts +1 -0
- package/dist/applet.js +184 -0
- package/dist/batch-Qa4B4-Yp.d.ts +30 -0
- package/dist/build-BlmLiKro.js +90 -0
- package/dist/build-CcT-wNN9.js +2 -0
- package/dist/ci-0Q2mUh53.js +68 -0
- package/dist/client.d.ts +48 -0
- package/dist/client.js +38 -0
- package/dist/contract-QUvEGiRI.d.ts +23 -0
- package/dist/deploy-BbL3GObV.js +2579 -0
- package/dist/email-DBTVhNB_.js +62 -0
- package/dist/generate-XB-72Fzj.js +537 -0
- package/dist/generate-wIN3IYqL.js +2 -0
- package/dist/index.d.ts +185 -0
- package/dist/index.js +251 -0
- package/dist/init-DwsXRUk-.js +244 -0
- package/dist/lazy-client-x0DCI98S.js +64 -0
- package/dist/load-config-OKZU3UKn.js +27 -0
- package/dist/login-BBO74hPC.js +12 -0
- package/dist/logs-XvK6Zs5Y.js +183 -0
- package/dist/manifest-4ZAc6HUk.js +191 -0
- package/dist/manifest-I-9s8R36.d.ts +88 -0
- package/dist/me-BW7Paary.js +32 -0
- package/dist/project-B0qNXnFu.js +536 -0
- package/dist/prompt-ChS-grYO.js +29 -0
- package/dist/query-cache-CXmAyLv3.js +82 -0
- package/dist/react.d.ts +37 -0
- package/dist/react.js +515 -0
- package/dist/schedules-BkWZsbSs.js +42 -0
- package/dist/server.d.ts +46 -0
- package/dist/server.js +104 -0
- package/dist/telephony-protocol-D-nf3Gca.d.ts +76 -0
- package/dist/telephony.d.ts +77 -0
- package/dist/telephony.js +607 -0
- package/dist/upload-DKZzz5kW.d.ts +112 -0
- package/dist/upload-DRbVhuDm.js +114 -0
- package/dist/variables-0tjnAeut.js +108 -0
- package/dist/vite.d.ts +63 -0
- package/dist/vite.js +816 -0
- package/dist/workflow-CTo2kZF1.js +51 -0
- package/dist/workflow-DurM6Fwx.d.ts +28 -0
- package/dist/workflow-engine-MogejNfF.js +117 -0
- package/dist/workspace-DruAZPmV.js +390 -0
- package/package.json +112 -0
- package/scaffold/.agents/skills/hitch-applets/SKILL.md +163 -0
- package/scaffold/.agents/skills/hitch-applets/references/client.md +73 -0
- package/scaffold/.agents/skills/hitch-applets/references/email-tracking.md +49 -0
- package/scaffold/.agents/skills/hitch-applets/references/files.md +109 -0
- package/scaffold/.agents/skills/hitch-applets/references/manifest.md +198 -0
- package/scaffold/.agents/skills/hitch-applets/references/markdown.md +37 -0
- package/scaffold/.agents/skills/hitch-applets/references/performance.md +74 -0
- package/scaffold/.agents/skills/hitch-applets/references/records.md +176 -0
- package/scaffold/.agents/skills/hitch-applets/references/schedules.md +91 -0
- package/scaffold/.agents/skills/hitch-applets/references/server.md +95 -0
- package/scaffold/.agents/skills/hitch-applets/references/workflows.md +81 -0
- package/scaffold/.agents/skills/hitch-cli/SKILL.md +129 -0
- package/scaffold/.agents/skills/hitch-data/SKILL.md +52 -0
- package/scaffold/.agents/skills/hitch-data/references/attributes.md +127 -0
- package/scaffold/.agents/skills/hitch-data/references/limits.md +64 -0
- package/scaffold/.agents/skills/hitch-theory/SKILL.md +151 -0
- package/scaffold/.agents/skills/react-best-practices/SKILL.md +127 -0
- package/scaffold/.agents/skills/react-best-practices/rules/advanced-effect-event-deps.md +50 -0
- package/scaffold/.agents/skills/react-best-practices/rules/advanced-event-handler-refs.md +55 -0
- package/scaffold/.agents/skills/react-best-practices/rules/advanced-init-once.md +42 -0
- package/scaffold/.agents/skills/react-best-practices/rules/advanced-use-latest.md +39 -0
- package/scaffold/.agents/skills/react-best-practices/rules/async-cheap-condition-before-await.md +37 -0
- package/scaffold/.agents/skills/react-best-practices/rules/async-defer-await.md +82 -0
- package/scaffold/.agents/skills/react-best-practices/rules/async-dependencies.md +48 -0
- package/scaffold/.agents/skills/react-best-practices/rules/async-parallel.md +24 -0
- package/scaffold/.agents/skills/react-best-practices/rules/bundle-analyzable-paths.md +42 -0
- package/scaffold/.agents/skills/react-best-practices/rules/bundle-barrel-imports.md +39 -0
- package/scaffold/.agents/skills/react-best-practices/rules/bundle-conditional.md +35 -0
- package/scaffold/.agents/skills/react-best-practices/rules/bundle-defer-third-party.md +46 -0
- package/scaffold/.agents/skills/react-best-practices/rules/bundle-dynamic-imports.md +38 -0
- package/scaffold/.agents/skills/react-best-practices/rules/bundle-preload.md +40 -0
- package/scaffold/.agents/skills/react-best-practices/rules/client-event-listeners.md +78 -0
- package/scaffold/.agents/skills/react-best-practices/rules/client-localstorage-schema.md +74 -0
- package/scaffold/.agents/skills/react-best-practices/rules/client-passive-event-listeners.md +48 -0
- package/scaffold/.agents/skills/react-best-practices/rules/js-batch-dom-css.md +110 -0
- package/scaffold/.agents/skills/react-best-practices/rules/js-cache-function-results.md +58 -0
- package/scaffold/.agents/skills/react-best-practices/rules/js-cache-property-access.md +28 -0
- package/scaffold/.agents/skills/react-best-practices/rules/js-cache-storage.md +56 -0
- package/scaffold/.agents/skills/react-best-practices/rules/js-combine-iterations.md +32 -0
- package/scaffold/.agents/skills/react-best-practices/rules/js-early-exit.md +50 -0
- package/scaffold/.agents/skills/react-best-practices/rules/js-flatmap-filter.md +51 -0
- package/scaffold/.agents/skills/react-best-practices/rules/js-hoist-regexp.md +45 -0
- package/scaffold/.agents/skills/react-best-practices/rules/js-index-maps.md +37 -0
- package/scaffold/.agents/skills/react-best-practices/rules/js-length-check-first.md +50 -0
- package/scaffold/.agents/skills/react-best-practices/rules/js-min-max-loop.md +82 -0
- package/scaffold/.agents/skills/react-best-practices/rules/js-request-idle-callback.md +104 -0
- package/scaffold/.agents/skills/react-best-practices/rules/js-set-map-lookups.md +24 -0
- package/scaffold/.agents/skills/react-best-practices/rules/js-tosorted-immutable.md +57 -0
- package/scaffold/.agents/skills/react-best-practices/rules/rendering-activity.md +26 -0
- package/scaffold/.agents/skills/react-best-practices/rules/rendering-animate-svg-wrapper.md +38 -0
- package/scaffold/.agents/skills/react-best-practices/rules/rendering-conditional-render.md +32 -0
- package/scaffold/.agents/skills/react-best-practices/rules/rendering-content-visibility.md +38 -0
- package/scaffold/.agents/skills/react-best-practices/rules/rendering-hoist-jsx.md +36 -0
- package/scaffold/.agents/skills/react-best-practices/rules/rendering-resource-hints.md +81 -0
- package/scaffold/.agents/skills/react-best-practices/rules/rendering-script-defer-async.md +36 -0
- package/scaffold/.agents/skills/react-best-practices/rules/rendering-svg-precision.md +28 -0
- package/scaffold/.agents/skills/react-best-practices/rules/rendering-usetransition-loading.md +43 -0
- package/scaffold/.agents/skills/react-best-practices/rules/rerender-defer-reads.md +39 -0
- package/scaffold/.agents/skills/react-best-practices/rules/rerender-dependencies.md +45 -0
- package/scaffold/.agents/skills/react-best-practices/rules/rerender-derived-state-no-effect.md +40 -0
- package/scaffold/.agents/skills/react-best-practices/rules/rerender-derived-state.md +29 -0
- package/scaffold/.agents/skills/react-best-practices/rules/rerender-functional-setstate.md +77 -0
- package/scaffold/.agents/skills/react-best-practices/rules/rerender-lazy-state-init.md +56 -0
- package/scaffold/.agents/skills/react-best-practices/rules/rerender-memo-with-default-value.md +36 -0
- package/scaffold/.agents/skills/react-best-practices/rules/rerender-memo.md +44 -0
- package/scaffold/.agents/skills/react-best-practices/rules/rerender-move-effect-to-event.md +45 -0
- package/scaffold/.agents/skills/react-best-practices/rules/rerender-no-inline-components.md +75 -0
- package/scaffold/.agents/skills/react-best-practices/rules/rerender-simple-expression-in-memo.md +35 -0
- package/scaffold/.agents/skills/react-best-practices/rules/rerender-split-combined-hooks.md +64 -0
- package/scaffold/.agents/skills/react-best-practices/rules/rerender-transitions.md +40 -0
- package/scaffold/.agents/skills/react-best-practices/rules/rerender-use-deferred-value.md +59 -0
- package/scaffold/.agents/skills/react-best-practices/rules/rerender-use-ref-transient-values.md +73 -0
- package/scaffold/.agents/skills/tanstack-query/SKILL.md +136 -0
- package/scaffold/.agents/skills/tanstack-query/references/compositions/automatic-invalidation-after-mutations.md +124 -0
- package/scaffold/.agents/skills/tanstack-query/references/compositions/broadcast-realtime-and-multi-tab-synchronization.md +150 -0
- package/scaffold/.agents/skills/tanstack-query/references/compositions/compose-query-with-tanstack-router-and-start.md +248 -0
- package/scaffold/.agents/skills/tanstack-query/references/compositions/enforce-query-best-practices-with-eslint.md +167 -0
- package/scaffold/.agents/skills/tanstack-query/references/compositions/persist-offline-and-restore-caches.md +181 -0
- package/scaffold/.agents/skills/tanstack-query/references/compositions/query-data-and-forms.md +156 -0
- package/scaffold/.agents/skills/tanstack-query/references/core/build-query-abstractions.md +130 -0
- package/scaffold/.agents/skills/tanstack-query/references/core/cancel-queries-and-consume-abort-signals.md +176 -0
- package/scaffold/.agents/skills/tanstack-query/references/core/concurrent-optimistic-updates.md +135 -0
- package/scaffold/.agents/skills/tanstack-query/references/core/coordinate-query-execution.md +188 -0
- package/scaffold/.agents/skills/tanstack-query/references/core/design-query-keys-and-options.md +182 -0
- package/scaffold/.agents/skills/tanstack-query/references/core/fetch-and-observe-queries.md +193 -0
- package/scaffold/.agents/skills/tanstack-query/references/core/implement-optimistic-updates-and-cache-writes.md +168 -0
- package/scaffold/.agents/skills/tanstack-query/references/core/paginate-and-build-infinite-queries.md +188 -0
- package/scaffold/.agents/skills/tanstack-query/references/core/seed-placeholder-select-and-transform-data.md +188 -0
- package/scaffold/.agents/skills/tanstack-query/references/core/selectors-and-derived-state.md +145 -0
- package/scaffold/.agents/skills/tanstack-query/references/core/tune-defaults-freshness-retries-and-refetching.md +172 -0
- package/scaffold/.agents/skills/tanstack-query/references/core/understand-query-internals-and-observers.md +104 -0
- package/scaffold/.agents/skills/tanstack-query/references/core/write-mutations-and-invalidate-related-queries.md +183 -0
- package/scaffold/.agents/skills/tanstack-query/references/framework/debug-with-devtools.md +169 -0
- package/scaffold/.agents/skills/tanstack-query/references/framework/handle-status-and-errors.md +119 -0
- package/scaffold/.agents/skills/tanstack-query/references/framework/shape-data-and-render-efficiently.md +184 -0
- package/scaffold/.agents/skills/tanstack-query/references/framework/use-framework-adapter-reactivity.md +185 -0
- package/scaffold/.agents/skills/tanstack-query/references/framework/use-suspense-and-error-boundaries.md +208 -0
- package/scaffold/.agents/skills/tanstack-query/references/lifecycle/migrate-major-versions-and-codemods.md +169 -0
- package/scaffold/.agents/skills/tanstack-query/references/lifecycle/prefetch-and-remove-request-waterfalls.md +228 -0
- package/scaffold/.agents/skills/tanstack-query/references/lifecycle/setup-query-client-and-providers.md +161 -0
- package/scaffold/.agents/skills/tanstack-query/references/lifecycle/ssr-hydration-and-streaming.md +209 -0
- package/scaffold/.agents/skills/tanstack-query/references/lifecycle/test-query-code.md +160 -0
- package/scaffold/.agents/skills/tanstack-router/SKILL.md +134 -0
- package/scaffold/.agents/skills/tanstack-router/references/compositions/router-query.md +412 -0
- package/scaffold/.agents/skills/tanstack-router/references/react-router.md +495 -0
- package/scaffold/.agents/skills/tanstack-router/references/router-core/auth-and-guards.md +495 -0
- package/scaffold/.agents/skills/tanstack-router/references/router-core/code-splitting.md +318 -0
- package/scaffold/.agents/skills/tanstack-router/references/router-core/data-loading.md +490 -0
- package/scaffold/.agents/skills/tanstack-router/references/router-core/index.md +158 -0
- package/scaffold/.agents/skills/tanstack-router/references/router-core/navigation.md +433 -0
- package/scaffold/.agents/skills/tanstack-router/references/router-core/not-found-and-errors.md +436 -0
- package/scaffold/.agents/skills/tanstack-router/references/router-core/path-params.md +383 -0
- package/scaffold/.agents/skills/tanstack-router/references/router-core/search-params.md +343 -0
- package/scaffold/.agents/skills/tanstack-router/references/router-core/ssr.md +462 -0
- package/scaffold/.agents/skills/tanstack-router/references/router-core/type-safety.md +493 -0
- package/scaffold/.agents/skills/tanstack-router/references/router-plugin.md +250 -0
- package/scaffold/.agents/skills/tanstack-router/references/virtual-file-routes.md +215 -0
- package/scaffold/.agents/skills/web-design-guidelines/SKILL.md +184 -0
- package/scaffold/.github/workflows/hitch.yml +15 -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 +17 -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 +1 -0
- package/scaffold/applet/src/client/styles/globals.css +136 -0
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
3
|
+
//#region src/server/identity.ts
|
|
4
|
+
const requestContext = new AsyncLocalStorage();
|
|
5
|
+
function runWithRequestContext(context, fn) {
|
|
6
|
+
return requestContext.run(context, fn);
|
|
7
|
+
}
|
|
8
|
+
function tryGetRequestContext() {
|
|
9
|
+
return requestContext.getStore();
|
|
10
|
+
}
|
|
11
|
+
function getRequestContext() {
|
|
12
|
+
const context = requestContext.getStore();
|
|
13
|
+
if (context === void 0) throw new Error("Hitch request context is unavailable outside a request.");
|
|
14
|
+
return context;
|
|
15
|
+
}
|
|
16
|
+
function getIdentityBearer(identity) {
|
|
17
|
+
return identity.kind === "user" ? identity.sessionToken : identity.token;
|
|
18
|
+
}
|
|
19
|
+
//#endregion
|
|
20
|
+
//#region src/server/workflow.ts
|
|
21
|
+
const DURATION_MULTIPLIERS = {
|
|
22
|
+
milliseconds: 1,
|
|
23
|
+
seconds: 1e3,
|
|
24
|
+
minutes: 6e4,
|
|
25
|
+
hours: 36e5,
|
|
26
|
+
days: 864e5,
|
|
27
|
+
weeks: 6048e5
|
|
28
|
+
};
|
|
29
|
+
const MAX_DURATION_MS = 365 * DURATION_MULTIPLIERS.days;
|
|
30
|
+
const unit = z.number().nonnegative().optional();
|
|
31
|
+
const workflowDurationSchema = z.strictObject({
|
|
32
|
+
milliseconds: unit,
|
|
33
|
+
seconds: unit,
|
|
34
|
+
minutes: unit,
|
|
35
|
+
hours: unit,
|
|
36
|
+
days: unit,
|
|
37
|
+
weeks: unit
|
|
38
|
+
}).refine((duration) => sumMilliseconds(duration) <= MAX_DURATION_MS, "must not exceed 365 days");
|
|
39
|
+
function sumMilliseconds(duration) {
|
|
40
|
+
return Object.entries(DURATION_MULTIPLIERS).reduce((total, [key, multiplier]) => total + (duration[key] ?? 0) * multiplier, 0);
|
|
41
|
+
}
|
|
42
|
+
function toMilliseconds(duration) {
|
|
43
|
+
const parsed = workflowDurationSchema.safeParse(duration);
|
|
44
|
+
if (!parsed.success) throw new Error(`Invalid workflow duration: ${z.prettifyError(parsed.error)}`);
|
|
45
|
+
return sumMilliseconds(parsed.data);
|
|
46
|
+
}
|
|
47
|
+
function createWorkflow(run) {
|
|
48
|
+
return { run };
|
|
49
|
+
}
|
|
50
|
+
//#endregion
|
|
51
|
+
export { getRequestContext as a, getIdentityBearer as i, createWorkflow as n, runWithRequestContext as o, toMilliseconds as r, tryGetRequestContext as s, MAX_DURATION_MS as t };
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
//#region src/server/workflow.d.ts
|
|
3
|
+
type WorkflowDuration = {
|
|
4
|
+
milliseconds?: number;
|
|
5
|
+
seconds?: number;
|
|
6
|
+
minutes?: number;
|
|
7
|
+
hours?: number;
|
|
8
|
+
days?: number;
|
|
9
|
+
weeks?: number;
|
|
10
|
+
};
|
|
11
|
+
type WorkflowStep = {
|
|
12
|
+
do<T>(name: string, fn: () => T | Promise<T>): Promise<T>;
|
|
13
|
+
do<T>(name: string, options: {
|
|
14
|
+
retries?: {
|
|
15
|
+
limit: number;
|
|
16
|
+
delay: WorkflowDuration;
|
|
17
|
+
backoff?: "constant" | "linear" | "exponential";
|
|
18
|
+
};
|
|
19
|
+
}, fn: () => T | Promise<T>): Promise<T>;
|
|
20
|
+
sleep(name: string, duration: WorkflowDuration): Promise<void>;
|
|
21
|
+
};
|
|
22
|
+
type WorkflowDefinition<Payload, Result> = {
|
|
23
|
+
readonly run: (step: WorkflowStep, payload: Payload) => Promise<Result>;
|
|
24
|
+
};
|
|
25
|
+
type AnyWorkflowDefinition = WorkflowDefinition<any, any>;
|
|
26
|
+
declare function createWorkflow<Payload, Result>(run: (step: WorkflowStep, payload: Payload) => Promise<Result>): WorkflowDefinition<Payload, Result>;
|
|
27
|
+
//#endregion
|
|
28
|
+
export { createWorkflow as a, WorkflowStep as i, WorkflowDefinition as n, WorkflowDuration as r, AnyWorkflowDefinition as t };
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
//#region src/headers.ts
|
|
3
|
+
const HITCH_USER_HEADER = "x-hitch-user";
|
|
4
|
+
const HITCH_SESSION_HEADER = "x-hitch-session";
|
|
5
|
+
const HITCH_APPLET_HEADER = "x-hitch-applet";
|
|
6
|
+
const HITCH_APPLET_TOKEN_HEADER = "x-hitch-applet-token";
|
|
7
|
+
//#endregion
|
|
8
|
+
//#region src/server/workflow-protocol.ts
|
|
9
|
+
const journalEntrySchema = z.discriminatedUnion("kind", [
|
|
10
|
+
z.object({
|
|
11
|
+
kind: z.literal("step"),
|
|
12
|
+
id: z.string(),
|
|
13
|
+
name: z.string(),
|
|
14
|
+
result: z.unknown()
|
|
15
|
+
}),
|
|
16
|
+
z.object({
|
|
17
|
+
kind: z.literal("sleep"),
|
|
18
|
+
id: z.string(),
|
|
19
|
+
name: z.string(),
|
|
20
|
+
until: z.number()
|
|
21
|
+
}),
|
|
22
|
+
z.object({
|
|
23
|
+
kind: z.literal("attempt"),
|
|
24
|
+
id: z.string(),
|
|
25
|
+
name: z.string(),
|
|
26
|
+
error: z.string()
|
|
27
|
+
}),
|
|
28
|
+
z.object({
|
|
29
|
+
kind: z.literal("failed"),
|
|
30
|
+
id: z.string(),
|
|
31
|
+
name: z.string(),
|
|
32
|
+
error: z.string()
|
|
33
|
+
})
|
|
34
|
+
]);
|
|
35
|
+
const workflowInvocationRequestSchema = z.object({
|
|
36
|
+
payload: z.unknown(),
|
|
37
|
+
journal: z.array(journalEntrySchema)
|
|
38
|
+
});
|
|
39
|
+
const workflowOutcomeSchema = z.discriminatedUnion("kind", [
|
|
40
|
+
z.object({
|
|
41
|
+
kind: z.literal("step"),
|
|
42
|
+
id: z.string(),
|
|
43
|
+
name: z.string(),
|
|
44
|
+
result: z.unknown()
|
|
45
|
+
}),
|
|
46
|
+
z.object({
|
|
47
|
+
kind: z.literal("sleep"),
|
|
48
|
+
id: z.string(),
|
|
49
|
+
name: z.string(),
|
|
50
|
+
until: z.number()
|
|
51
|
+
}),
|
|
52
|
+
z.object({
|
|
53
|
+
kind: z.literal("step_error"),
|
|
54
|
+
id: z.string(),
|
|
55
|
+
name: z.string(),
|
|
56
|
+
message: z.string(),
|
|
57
|
+
retries: z.object({
|
|
58
|
+
limit: z.number(),
|
|
59
|
+
delayMs: z.number(),
|
|
60
|
+
backoff: z.enum([
|
|
61
|
+
"constant",
|
|
62
|
+
"linear",
|
|
63
|
+
"exponential"
|
|
64
|
+
])
|
|
65
|
+
})
|
|
66
|
+
}),
|
|
67
|
+
z.object({
|
|
68
|
+
kind: z.literal("done"),
|
|
69
|
+
result: z.unknown()
|
|
70
|
+
}),
|
|
71
|
+
z.object({
|
|
72
|
+
kind: z.literal("error"),
|
|
73
|
+
message: z.string()
|
|
74
|
+
})
|
|
75
|
+
]);
|
|
76
|
+
//#endregion
|
|
77
|
+
//#region src/server/workflow-engine.ts
|
|
78
|
+
var WorkflowFailedError = class extends Error {};
|
|
79
|
+
async function runWorkflowJournal(engine, invoke) {
|
|
80
|
+
const journal = [];
|
|
81
|
+
for (let index = 1;; index += 1) {
|
|
82
|
+
const outcome = await engine.invoke(`invoke ${index}`, () => invoke(journal));
|
|
83
|
+
switch (outcome.kind) {
|
|
84
|
+
case "step":
|
|
85
|
+
journal.push(outcome);
|
|
86
|
+
break;
|
|
87
|
+
case "sleep":
|
|
88
|
+
journal.push(outcome);
|
|
89
|
+
await engine.sleepUntil(`sleep ${index}`, new Date(outcome.until));
|
|
90
|
+
break;
|
|
91
|
+
case "step_error": {
|
|
92
|
+
const attempts = 1 + journal.filter((entry) => entry.kind === "attempt" && entry.id === outcome.id).length;
|
|
93
|
+
journal.push({
|
|
94
|
+
kind: "attempt",
|
|
95
|
+
id: outcome.id,
|
|
96
|
+
name: outcome.name,
|
|
97
|
+
error: outcome.message
|
|
98
|
+
});
|
|
99
|
+
if (attempts > outcome.retries.limit) {
|
|
100
|
+
journal.push({
|
|
101
|
+
kind: "failed",
|
|
102
|
+
id: outcome.id,
|
|
103
|
+
name: outcome.name,
|
|
104
|
+
error: outcome.message
|
|
105
|
+
});
|
|
106
|
+
break;
|
|
107
|
+
}
|
|
108
|
+
await engine.sleep(`retry ${index}`, outcome.retries.delayMs * (outcome.retries.backoff === "exponential" ? 2 ** (attempts - 1) : outcome.retries.backoff === "linear" ? attempts : 1));
|
|
109
|
+
break;
|
|
110
|
+
}
|
|
111
|
+
case "done": return outcome.result;
|
|
112
|
+
case "error": throw new WorkflowFailedError(outcome.message);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
//#endregion
|
|
117
|
+
export { workflowOutcomeSchema as a, HITCH_SESSION_HEADER as c, workflowInvocationRequestSchema as i, HITCH_USER_HEADER as l, runWorkflowJournal as n, HITCH_APPLET_HEADER as o, journalEntrySchema as r, HITCH_APPLET_TOKEN_HEADER as s, WorkflowFailedError as t };
|
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { chmod, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
4
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
5
|
+
import { spawn } from "node:child_process";
|
|
6
|
+
import { homedir } from "node:os";
|
|
7
|
+
//#region ../shared/auth/src/store.mts
|
|
8
|
+
const DEFAULT_INSTANCE_URL = "https://app.hitch42.com";
|
|
9
|
+
var AuthenticationError = class extends Error {};
|
|
10
|
+
function isErrno(error, code) {
|
|
11
|
+
return error instanceof Error && "code" in error && error.code === code;
|
|
12
|
+
}
|
|
13
|
+
async function readJson(path) {
|
|
14
|
+
try {
|
|
15
|
+
return JSON.parse(await readFile(path, "utf8"));
|
|
16
|
+
} catch (error) {
|
|
17
|
+
if (isErrno(error, "ENOENT")) return void 0;
|
|
18
|
+
throw new Error(`Failed to read ${path}.`, { cause: error });
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
function isObject(value) {
|
|
22
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
23
|
+
}
|
|
24
|
+
function emptyCredentialStore() {
|
|
25
|
+
return { instances: {} };
|
|
26
|
+
}
|
|
27
|
+
async function healCredentialStore(credentialsPath, source, store, unrecognizedCount) {
|
|
28
|
+
const backupPath = `${credentialsPath}.bak`;
|
|
29
|
+
try {
|
|
30
|
+
try {
|
|
31
|
+
await writeFile(backupPath, source, {
|
|
32
|
+
flag: "wx",
|
|
33
|
+
mode: 384
|
|
34
|
+
});
|
|
35
|
+
} catch (error) {
|
|
36
|
+
if (!isErrno(error, "EEXIST")) throw error;
|
|
37
|
+
}
|
|
38
|
+
await chmod(backupPath, 384);
|
|
39
|
+
await writeFile(credentialsPath, `${JSON.stringify(store, null, 2)}\n`, { mode: 384 });
|
|
40
|
+
await chmod(credentialsPath, 384);
|
|
41
|
+
} catch {
|
|
42
|
+
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.`);
|
|
43
|
+
return store;
|
|
44
|
+
}
|
|
45
|
+
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}).`);
|
|
46
|
+
return store;
|
|
47
|
+
}
|
|
48
|
+
async function readRawCredentialStore(options = {}) {
|
|
49
|
+
const { credentialsPath } = storePaths(options);
|
|
50
|
+
let source;
|
|
51
|
+
try {
|
|
52
|
+
source = await readFile(credentialsPath, "utf8");
|
|
53
|
+
} catch (error) {
|
|
54
|
+
if (isErrno(error, "ENOENT")) return emptyCredentialStore();
|
|
55
|
+
throw new Error(`Failed to read ${credentialsPath}.`, { cause: error });
|
|
56
|
+
}
|
|
57
|
+
let value;
|
|
58
|
+
try {
|
|
59
|
+
value = JSON.parse(source);
|
|
60
|
+
} catch {
|
|
61
|
+
return healCredentialStore(credentialsPath, source, emptyCredentialStore());
|
|
62
|
+
}
|
|
63
|
+
if (!isObject(value) || !isObject(value.instances)) return healCredentialStore(credentialsPath, source, emptyCredentialStore());
|
|
64
|
+
const instances = Object.fromEntries(Object.entries(value.instances).flatMap(([instanceUrl, entry]) => {
|
|
65
|
+
if (isObject(entry) && typeof entry.organizationId === "string") {
|
|
66
|
+
const credentials = parseCredentials(entry);
|
|
67
|
+
return credentials === void 0 ? [] : [[instanceUrl, { [credentials.orgCode]: entry }]];
|
|
68
|
+
}
|
|
69
|
+
if (!isObject(entry)) return [];
|
|
70
|
+
return [[instanceUrl, Object.fromEntries(Object.entries(entry).filter(([orgCode, credentials]) => parseCredentials(credentials)?.orgCode === orgCode))]];
|
|
71
|
+
}));
|
|
72
|
+
const reshaped = Object.values(value.instances).some((entry) => !isObject(entry) || typeof entry.organizationId === "string");
|
|
73
|
+
const countLeaves = (store) => Object.values(store).reduce((total, entry) => total + (isObject(entry) && typeof entry.organizationId !== "string" ? Object.keys(entry).length : 1), 0);
|
|
74
|
+
const unrecognizedCount = countLeaves(value.instances) - countLeaves(instances);
|
|
75
|
+
if (!reshaped && unrecognizedCount === 0) return value;
|
|
76
|
+
return healCredentialStore(credentialsPath, source, { instances }, unrecognizedCount);
|
|
77
|
+
}
|
|
78
|
+
function parseCredentials(value) {
|
|
79
|
+
if (!isObject(value) || typeof value.organizationId !== "string" || typeof value.orgCode !== "string") return;
|
|
80
|
+
const organization = {
|
|
81
|
+
organizationId: value.organizationId,
|
|
82
|
+
orgCode: value.orgCode
|
|
83
|
+
};
|
|
84
|
+
if ((value.type === void 0 || value.type === "session") && typeof value.accessToken === "string" && (value.expiresAt === void 0 || typeof value.expiresAt === "number")) return {
|
|
85
|
+
type: "session",
|
|
86
|
+
accessToken: value.accessToken,
|
|
87
|
+
...value.expiresAt === void 0 ? {} : { expiresAt: value.expiresAt },
|
|
88
|
+
...organization
|
|
89
|
+
};
|
|
90
|
+
if (value.type === "apiKey" && typeof value.apiKey === "string") return {
|
|
91
|
+
type: "apiKey",
|
|
92
|
+
apiKey: value.apiKey,
|
|
93
|
+
...organization
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
function normalizeInstanceUrl(instanceUrl) {
|
|
97
|
+
let url;
|
|
98
|
+
try {
|
|
99
|
+
url = new URL(instanceUrl);
|
|
100
|
+
} catch (error) {
|
|
101
|
+
throw new Error(`Invalid Hitch instance URL: ${instanceUrl}`, { cause: error });
|
|
102
|
+
}
|
|
103
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") throw new Error(`Invalid Hitch instance URL protocol: ${url.protocol}`);
|
|
104
|
+
url.pathname = url.pathname.replace(/\/+$/, "");
|
|
105
|
+
url.search = "";
|
|
106
|
+
url.hash = "";
|
|
107
|
+
return url.toString().replace(/\/$/, "");
|
|
108
|
+
}
|
|
109
|
+
function organizationInstanceUrl(instanceUrl, orgCode) {
|
|
110
|
+
const url = new URL(normalizeInstanceUrl(instanceUrl));
|
|
111
|
+
const labels = url.hostname.split(".");
|
|
112
|
+
if (labels[0] !== "app" || labels.length < 2) return url.toString().replace(/\/$/, "");
|
|
113
|
+
url.hostname = [orgCode, ...labels.slice(1)].join(".");
|
|
114
|
+
return url.toString().replace(/\/$/, "");
|
|
115
|
+
}
|
|
116
|
+
function storePaths(options = {}) {
|
|
117
|
+
const env = options.env ?? process.env;
|
|
118
|
+
const home = options.homeDir ?? homedir();
|
|
119
|
+
const configDir = join(env.XDG_CONFIG_HOME || join(home, ".config"), "hitch");
|
|
120
|
+
return {
|
|
121
|
+
configDir,
|
|
122
|
+
cacheDir: join(env.XDG_CACHE_HOME || join(home, ".cache"), "hitch"),
|
|
123
|
+
configPath: join(configDir, "config.json"),
|
|
124
|
+
credentialsPath: join(configDir, "credentials.json")
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
async function resolveInstanceUrl(override, options = {}) {
|
|
128
|
+
if (override !== void 0) return normalizeInstanceUrl(override);
|
|
129
|
+
const envInstanceUrl = (options.env ?? process.env).HITCH_API_URL;
|
|
130
|
+
if (envInstanceUrl !== void 0 && envInstanceUrl.length > 0) return normalizeInstanceUrl(envInstanceUrl);
|
|
131
|
+
const { configPath } = storePaths(options);
|
|
132
|
+
const config = await readJson(configPath);
|
|
133
|
+
if (config === void 0) return DEFAULT_INSTANCE_URL;
|
|
134
|
+
if (!isObject(config) || config.instanceUrl !== void 0 && typeof config.instanceUrl !== "string") throw new Error(`${configPath}: expected { instanceUrl?: string }.`);
|
|
135
|
+
return normalizeInstanceUrl(config.instanceUrl ?? "https://app.hitch42.com");
|
|
136
|
+
}
|
|
137
|
+
async function readCredentialStore(options = {}) {
|
|
138
|
+
const store = await readRawCredentialStore(options);
|
|
139
|
+
return { instances: Object.fromEntries(Object.entries(store.instances).map(([instanceUrl, credentialsByOrg]) => [instanceUrl, Object.fromEntries(Object.entries(credentialsByOrg).flatMap(([orgCode, value]) => {
|
|
140
|
+
const credentials = parseCredentials(value);
|
|
141
|
+
return credentials === void 0 ? [] : [[orgCode, credentials]];
|
|
142
|
+
}))])) };
|
|
143
|
+
}
|
|
144
|
+
async function getCredentialsForOrg(instanceUrl, orgCode, options = {}) {
|
|
145
|
+
return (await readCredentialStore(options)).instances[normalizeInstanceUrl(instanceUrl)]?.[orgCode];
|
|
146
|
+
}
|
|
147
|
+
function environmentAuth(options) {
|
|
148
|
+
const apiKey = (options.env ?? process.env).HITCH_API_KEY;
|
|
149
|
+
return apiKey !== void 0 && apiKey.length > 0 ? {
|
|
150
|
+
accessToken: apiKey,
|
|
151
|
+
source: "environment"
|
|
152
|
+
} : void 0;
|
|
153
|
+
}
|
|
154
|
+
function resolvedStoredAuth(credentials) {
|
|
155
|
+
return credentials === void 0 ? void 0 : {
|
|
156
|
+
accessToken: credentials.type === "apiKey" ? credentials.apiKey : credentials.accessToken,
|
|
157
|
+
credentialType: credentials.type,
|
|
158
|
+
organizationId: credentials.organizationId,
|
|
159
|
+
orgCode: credentials.orgCode,
|
|
160
|
+
source: "store"
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
function resolveStoredAuthForOrg(credentialsByOrg, orgCode) {
|
|
164
|
+
const exact = resolvedStoredAuth(credentialsByOrg[orgCode]);
|
|
165
|
+
if (exact !== void 0) return exact;
|
|
166
|
+
const sessions = Object.values(credentialsByOrg).filter((credentials) => credentials.type === "session").filter((credentials, index, all) => all.findIndex(({ accessToken }) => accessToken === credentials.accessToken) === index);
|
|
167
|
+
if (sessions.length === 1) return resolvedStoredAuth(sessions[0]);
|
|
168
|
+
if (sessions.length > 1) throw new AuthenticationError(`Multiple sessions are stored. Run hitch login for organization "${orgCode}".`);
|
|
169
|
+
const apiKeyOrgs = Object.values(credentialsByOrg).filter((credentials) => credentials.type === "apiKey").map(({ orgCode: storedOrg }) => storedOrg).sort();
|
|
170
|
+
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}".`);
|
|
171
|
+
}
|
|
172
|
+
async function resolveAuthForOrg(instanceUrl, orgCode, options = {}) {
|
|
173
|
+
const auth = environmentAuth(options);
|
|
174
|
+
if (auth !== void 0) return auth;
|
|
175
|
+
const normalizedUrl = normalizeInstanceUrl(instanceUrl);
|
|
176
|
+
const credentialsByOrg = Object.entries((await readCredentialStore(options)).instances).find(([instance]) => instance === normalizedUrl)?.[1];
|
|
177
|
+
return resolveStoredAuthForOrg(credentialsByOrg ?? {}, orgCode);
|
|
178
|
+
}
|
|
179
|
+
function rejectEnvironmentKey(response, auth) {
|
|
180
|
+
if (auth.source === "environment" && response.status === 401) throw new AuthenticationError("HITCH_API_KEY was rejected.");
|
|
181
|
+
}
|
|
182
|
+
function bearerToken(credentials) {
|
|
183
|
+
return "apiKey" in credentials ? credentials.apiKey : credentials.accessToken;
|
|
184
|
+
}
|
|
185
|
+
async function setCredentials(instanceUrl, credentials, options = {}) {
|
|
186
|
+
const paths = storePaths(options);
|
|
187
|
+
const store = await readRawCredentialStore(options);
|
|
188
|
+
const normalizedUrl = normalizeInstanceUrl(instanceUrl);
|
|
189
|
+
const credentialsByOrg = store.instances[normalizedUrl];
|
|
190
|
+
store.instances[normalizedUrl] = {
|
|
191
|
+
...isObject(credentialsByOrg) ? credentialsByOrg : {},
|
|
192
|
+
[credentials.orgCode]: credentials
|
|
193
|
+
};
|
|
194
|
+
await mkdir(paths.configDir, { recursive: true });
|
|
195
|
+
await writeFile(paths.credentialsPath, `${JSON.stringify(store, null, 2)}\n`, { mode: 384 });
|
|
196
|
+
await chmod(paths.credentialsPath, 384);
|
|
197
|
+
}
|
|
198
|
+
//#endregion
|
|
199
|
+
//#region ../shared/auth/src/device.mts
|
|
200
|
+
const CLIENT_ID = "hitch-cli";
|
|
201
|
+
const DEVICE_GRANT = "urn:ietf:params:oauth:grant-type:device_code";
|
|
202
|
+
function defaultOpenUrl(url) {
|
|
203
|
+
try {
|
|
204
|
+
const child = spawn(process.platform === "darwin" ? "open" : "xdg-open", [url], {
|
|
205
|
+
detached: true,
|
|
206
|
+
stdio: "ignore"
|
|
207
|
+
});
|
|
208
|
+
child.on("error", () => void 0);
|
|
209
|
+
child.unref();
|
|
210
|
+
} catch {}
|
|
211
|
+
}
|
|
212
|
+
function requiredObject(value, message) {
|
|
213
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) throw new Error(message);
|
|
214
|
+
return value;
|
|
215
|
+
}
|
|
216
|
+
async function jsonResponse(response) {
|
|
217
|
+
try {
|
|
218
|
+
return requiredObject(await response.json(), "Expected a JSON object response.");
|
|
219
|
+
} catch (error) {
|
|
220
|
+
throw new Error(`Invalid JSON response from ${response.url || "Hitch"}.`, { cause: error });
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
async function postJson(fetcher, url, body) {
|
|
224
|
+
const response = await fetcher(url, {
|
|
225
|
+
method: "POST",
|
|
226
|
+
headers: { "content-type": "application/json" },
|
|
227
|
+
body: JSON.stringify(body)
|
|
228
|
+
});
|
|
229
|
+
return {
|
|
230
|
+
body: await jsonResponse(response),
|
|
231
|
+
response
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
function parseDeviceCode(value) {
|
|
235
|
+
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.");
|
|
236
|
+
return value;
|
|
237
|
+
}
|
|
238
|
+
function parseToken(value) {
|
|
239
|
+
return typeof value.access_token === "string" && typeof value.token_type === "string" ? value : void 0;
|
|
240
|
+
}
|
|
241
|
+
function parseDeviceOrganization(value) {
|
|
242
|
+
if (typeof value.organizationId !== "string" || typeof value.orgCode !== "string") throw new AuthenticationError("Hitch returned an invalid device organization response.");
|
|
243
|
+
return value;
|
|
244
|
+
}
|
|
245
|
+
function parseMe(value) {
|
|
246
|
+
const person = requiredObject(value.person, "Hitch returned an invalid identity response.");
|
|
247
|
+
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.");
|
|
248
|
+
const organization = value.organization === void 0 || value.organization === null ? null : requiredObject(value.organization, "Hitch returned an invalid identity response.");
|
|
249
|
+
if (organization !== null && (typeof organization.id !== "string" || typeof organization.code !== "string")) throw new Error("Hitch returned an invalid identity response.");
|
|
250
|
+
return {
|
|
251
|
+
memberships: value.memberships.map((membership) => {
|
|
252
|
+
const item = requiredObject(membership, "Hitch returned an invalid membership.");
|
|
253
|
+
if (typeof item.orgId !== "string" || typeof item.code !== "string" || typeof item.name !== "string") throw new Error("Hitch returned an invalid membership.");
|
|
254
|
+
return item;
|
|
255
|
+
}),
|
|
256
|
+
organization,
|
|
257
|
+
person
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
async function fetchMe(fetcher, instanceUrl, auth) {
|
|
261
|
+
const response = await fetcher(`${instanceUrl}/api/v1/me`, { headers: { Authorization: `Bearer ${auth.accessToken}` } });
|
|
262
|
+
rejectEnvironmentKey(response, auth);
|
|
263
|
+
if (auth.credentialType === "apiKey" && response.status === 401) throw new AuthenticationError("API key rejected.");
|
|
264
|
+
const body = await jsonResponse(response);
|
|
265
|
+
if (!response.ok) throw new AuthenticationError(typeof body.message === "string" ? body.message : `Failed to load the signed-in identity (${response.status}).`);
|
|
266
|
+
return parseMe(body);
|
|
267
|
+
}
|
|
268
|
+
async function pollForToken(fetcher, instanceUrl, device, now, sleep) {
|
|
269
|
+
const deadline = now() + device.expires_in * 1e3;
|
|
270
|
+
let interval = (device.interval ?? 5) * 1e3;
|
|
271
|
+
while (now() < deadline) {
|
|
272
|
+
await sleep(Math.min(interval, deadline - now()));
|
|
273
|
+
if (now() >= deadline) break;
|
|
274
|
+
const { body, response } = await postJson(fetcher, `${instanceUrl}/api/auth/device/token`, {
|
|
275
|
+
grant_type: DEVICE_GRANT,
|
|
276
|
+
device_code: device.device_code,
|
|
277
|
+
client_id: CLIENT_ID
|
|
278
|
+
});
|
|
279
|
+
const token = parseToken(body);
|
|
280
|
+
if (response.ok && token !== void 0) return token;
|
|
281
|
+
switch (body.error) {
|
|
282
|
+
case "authorization_pending": break;
|
|
283
|
+
case "slow_down":
|
|
284
|
+
interval += 5e3;
|
|
285
|
+
break;
|
|
286
|
+
case "expired_token": throw new AuthenticationError("The sign-in request expired. Run hitch login again.");
|
|
287
|
+
case "access_denied": throw new AuthenticationError("Sign-in was denied.");
|
|
288
|
+
default: throw new AuthenticationError(typeof body.error_description === "string" ? body.error_description : `Sign-in failed (${response.status}).`);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
throw new AuthenticationError("The sign-in request expired. Run hitch login again.");
|
|
292
|
+
}
|
|
293
|
+
async function loginWithDevice(instanceUrl, options = {}) {
|
|
294
|
+
const fetcher = options.fetch ?? globalThis.fetch;
|
|
295
|
+
const now = options.now ?? Date.now;
|
|
296
|
+
const { body, response } = await postJson(fetcher, `${instanceUrl}/api/auth/device/code`, { client_id: CLIENT_ID });
|
|
297
|
+
if (!response.ok) throw new AuthenticationError(typeof body.error_description === "string" ? body.error_description : `Could not start sign-in (${response.status}).`);
|
|
298
|
+
const device = parseDeviceCode(body);
|
|
299
|
+
const verificationUrl = options.expectedOrg === void 0 ? device.verification_uri_complete : `${device.verification_uri_complete}&org=${encodeURIComponent(options.expectedOrg)}`;
|
|
300
|
+
(options.print ?? console.log)(`Open ${verificationUrl} and confirm code ${device.user_code}.`);
|
|
301
|
+
if (!options.noBrowser) try {
|
|
302
|
+
await (options.openUrl ?? defaultOpenUrl)(verificationUrl);
|
|
303
|
+
} catch {}
|
|
304
|
+
const token = await pollForToken(fetcher, instanceUrl, device, now, options.sleep ?? ((milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds))));
|
|
305
|
+
const { body: organizationBody, response: organizationResponse } = await postJson(fetcher, `${instanceUrl}/api/auth/device/organization`, { device_code: device.device_code });
|
|
306
|
+
if (!organizationResponse.ok) throw new AuthenticationError("The sign-in did not resolve an organization.");
|
|
307
|
+
const organization = parseDeviceOrganization(organizationBody);
|
|
308
|
+
const me = await fetchMe(fetcher, instanceUrl, {
|
|
309
|
+
accessToken: token.access_token,
|
|
310
|
+
credentialType: "session",
|
|
311
|
+
source: "store"
|
|
312
|
+
});
|
|
313
|
+
const membership = me.memberships.find(({ orgId }) => orgId === organization.organizationId);
|
|
314
|
+
if (membership === void 0) throw new AuthenticationError("The device session is not scoped to a Hitch organization.");
|
|
315
|
+
const credentials = {
|
|
316
|
+
type: "session",
|
|
317
|
+
accessToken: token.access_token,
|
|
318
|
+
expiresAt: token.expires_in === void 0 ? void 0 : now() + token.expires_in * 1e3,
|
|
319
|
+
organizationId: organization.organizationId,
|
|
320
|
+
orgCode: organization.orgCode
|
|
321
|
+
};
|
|
322
|
+
await setCredentials(instanceUrl, credentials, options.store);
|
|
323
|
+
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`);
|
|
324
|
+
return {
|
|
325
|
+
credentials,
|
|
326
|
+
me,
|
|
327
|
+
membership
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
//#endregion
|
|
331
|
+
//#region ../shared/auth/src/workspace.mts
|
|
332
|
+
const ORG_CODE_PATTERN = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
|
|
333
|
+
const require = createRequire(import.meta.url);
|
|
334
|
+
function readWorkspaceConfig(dir) {
|
|
335
|
+
const path = resolve(dir, "hitch.config.ts");
|
|
336
|
+
if (!existsSync(path)) return void 0;
|
|
337
|
+
let module;
|
|
338
|
+
try {
|
|
339
|
+
module = require(path);
|
|
340
|
+
} catch (error) {
|
|
341
|
+
throw new Error(`Failed to load ${path}.`, { cause: error });
|
|
342
|
+
}
|
|
343
|
+
const config = module !== null && typeof module === "object" && "default" in module ? module.default : module;
|
|
344
|
+
if (config === null || typeof config !== "object" || !("sdk" in config) || typeof config.sdk !== "string" || config.sdk.length === 0 || !("org" in config) || typeof config.org !== "string" || !ORG_CODE_PATTERN.test(config.org)) throw new Error(`Invalid Hitch config in ${path}: "sdk" must be a package spec and "org" an organization code.`);
|
|
345
|
+
const githubWorkflow = "githubWorkflow" in config ? config.githubWorkflow : void 0;
|
|
346
|
+
if (githubWorkflow !== void 0 && typeof githubWorkflow !== "boolean") throw new Error(`Invalid Hitch config in ${path}: "githubWorkflow" must be a boolean.`);
|
|
347
|
+
return {
|
|
348
|
+
sdk: config.sdk,
|
|
349
|
+
org: config.org,
|
|
350
|
+
githubWorkflow
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
function findWorkspaceConfig(cwd) {
|
|
354
|
+
const appletsDir = dirname(cwd);
|
|
355
|
+
return readWorkspaceConfig(cwd) ?? (basename(appletsDir) === "applets" ? readWorkspaceConfig(dirname(appletsDir)) : void 0);
|
|
356
|
+
}
|
|
357
|
+
//#endregion
|
|
358
|
+
//#region src/auth.ts
|
|
359
|
+
async function fetchWithOrgAuth(orgCode, options, request) {
|
|
360
|
+
const fetcher = options.fetch ?? globalThis.fetch;
|
|
361
|
+
const instanceUrl = await resolveInstanceUrl(void 0, options.store);
|
|
362
|
+
const organizationUrl = organizationInstanceUrl(instanceUrl, orgCode);
|
|
363
|
+
const auth = await resolveAuthForOrg(instanceUrl, orgCode, options.store);
|
|
364
|
+
if (auth !== void 0) {
|
|
365
|
+
const response = await request(organizationUrl, bearerToken(auth));
|
|
366
|
+
rejectEnvironmentKey(response, auth);
|
|
367
|
+
if (response.status !== 401) return response;
|
|
368
|
+
}
|
|
369
|
+
if (!(options.stdinIsTTY ?? process.stdin.isTTY === true)) throw new AuthenticationError("Not logged in. Run applet login, or set HITCH_API_KEY.");
|
|
370
|
+
await (options.login ?? loginWithDevice)(instanceUrl, {
|
|
371
|
+
expectedOrg: orgCode,
|
|
372
|
+
fetch: fetcher,
|
|
373
|
+
store: options.store
|
|
374
|
+
});
|
|
375
|
+
const credentials = await getCredentialsForOrg(instanceUrl, orgCode, options.store);
|
|
376
|
+
if (credentials === void 0) throw new Error(`login did not provide credentials for organization "${orgCode}"`);
|
|
377
|
+
return request(organizationUrl, bearerToken(credentials));
|
|
378
|
+
}
|
|
379
|
+
//#endregion
|
|
380
|
+
//#region src/workspace.ts
|
|
381
|
+
function resolveWorkspaceOrg(appletRoot) {
|
|
382
|
+
const appletsDir = dirname(appletRoot);
|
|
383
|
+
if (basename(appletsDir) === "applets") {
|
|
384
|
+
const config = readWorkspaceConfig(dirname(appletsDir));
|
|
385
|
+
if (config !== void 0) return config.org;
|
|
386
|
+
}
|
|
387
|
+
throw new Error(`No Hitch workspace found for ${appletRoot}. An applet must live at <workspace>/applets/<code> with hitch.config.ts in the workspace root.`);
|
|
388
|
+
}
|
|
389
|
+
//#endregion
|
|
390
|
+
export { loginWithDevice as a, bearerToken as c, resolveAuthForOrg as d, resolveInstanceUrl as f, readWorkspaceConfig as i, organizationInstanceUrl as l, fetchWithOrgAuth as n, AuthenticationError as o, findWorkspaceConfig as r, DEFAULT_INSTANCE_URL as s, resolveWorkspaceOrg as t, rejectEnvironmentKey as u };
|