@hitch42/applet 0.1.1-beta.1 → 0.1.1-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/dist/_internal/client.js +120 -11
- package/dist/_internal/log-context.d.ts +34 -0
- package/dist/_internal/log-context.js +2 -0
- package/dist/_internal/server.d.ts +14 -1
- package/dist/_internal/server.js +122 -118
- package/dist/applet.js +53 -29
- package/dist/build-Bh_S5BUv.js +2 -0
- package/dist/{build-Rk3xDZV7.js → build-CEfFoIZZ.js} +30 -14
- package/dist/ci-BKw14X2h.js +69 -0
- package/dist/client.d.ts +3 -3
- package/dist/{contract-DMrpbaLJ.d.ts → contract-CsUpTBe0.d.ts} +1 -1
- package/dist/{deploy-D3BGDqgC.js → deploy-wGYjwXrx.js} +249 -35
- package/dist/email-DBTVhNB_.js +62 -0
- package/dist/{generate-CH0VQmUi.js → generate-BEIYs17x.js} +79 -25
- package/dist/generate-Dox-OYt6.js +2 -0
- package/dist/headers-DkNr9MLJ.js +17 -0
- package/dist/index.d.ts +123 -11
- package/dist/index.js +142 -26
- package/dist/{init-BS1Y-eNL.js → init-6oGPM46R.js} +17 -18
- package/dist/load-config-xEdTXs0y.js +27 -0
- package/dist/log-context-zu8U9Sjm.js +73 -0
- package/dist/login-CUx3TmFQ.js +12 -0
- package/dist/{logs-m00pzr_O.js → logs-DSjuEN0Q.js} +20 -22
- package/dist/{manifest-BQCAxATS.d.ts → manifest-C6_F4bL8.d.ts} +9 -0
- package/dist/{manifest-DyoJ1MIL.js → manifest-CJSL9GuC.js} +25 -3
- package/dist/me-BW7Paary.js +32 -0
- package/dist/{project-CxN-Dtc7.js → project-BRZGd6xq.js} +227 -34
- package/dist/prompt-ChS-grYO.js +29 -0
- package/dist/query-cache-CXmAyLv3.js +82 -0
- package/dist/react.d.ts +3 -2
- package/dist/react.js +98 -80
- package/dist/schedules-C8_Svr4h.js +42 -0
- package/dist/server.d.ts +9 -4
- package/dist/server.js +29 -8
- package/dist/telephony-protocol-D-nf3Gca.d.ts +76 -0
- package/dist/telephony.d.ts +97 -0
- package/dist/telephony.js +726 -0
- package/dist/{upload-YbRKEqR6.d.ts → upload-DKZzz5kW.d.ts} +6 -6
- package/dist/{variables-CP7Ce3Np.js → variables-DT5RyHdH.js} +14 -39
- package/dist/vite.d.ts +3 -60
- package/dist/vite.js +503 -34
- package/dist/workflow-engine-BOTzURKM.js +111 -0
- package/dist/{auth-DaVK0k5R.js → workspace-D0dnnlxL.js} +75 -40
- package/package.json +19 -6
- package/scaffold/.agents/skills/hitch-applets/SKILL.md +60 -15
- package/scaffold/.agents/skills/hitch-applets/references/email-tracking.md +49 -0
- package/scaffold/.agents/skills/hitch-applets/references/files.md +3 -3
- package/scaffold/.agents/skills/hitch-applets/references/manifest.md +29 -2
- package/scaffold/.agents/skills/hitch-applets/references/markdown.md +2 -0
- package/scaffold/.agents/skills/hitch-applets/references/performance.md +74 -0
- package/scaffold/.agents/skills/hitch-applets/references/records.md +32 -8
- package/scaffold/.agents/skills/hitch-applets/references/schedules.md +7 -2
- package/scaffold/.agents/skills/hitch-applets/references/server.md +0 -3
- package/scaffold/.agents/skills/hitch-applets/references/workflows.md +11 -3
- 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 +2 -2
- package/scaffold/applet/src/client/components/ui/alert.tsx +1 -1
- package/scaffold/applet/src/client/components/ui/button.tsx +1 -1
- package/scaffold/applet/src/client/components/ui/checkbox.tsx +1 -1
- package/scaffold/applet/src/client/components/ui/dialog.tsx +2 -2
- package/scaffold/applet/src/client/components/ui/dropdown-menu.tsx +2 -2
- package/scaffold/applet/src/client/components/ui/input-group.tsx +2 -2
- package/scaffold/applet/src/client/components/ui/select.tsx +2 -2
- package/scaffold/applet/src/client/components/ui/sheet.tsx +2 -2
- package/scaffold/applet/src/client/components/ui/spinner.tsx +2 -1
- package/scaffold/applet/src/client/components/ui/tabs.tsx +1 -1
- package/scaffold/applet/src/client/lib/utils.ts +1 -6
- package/scaffold/applet/src/client/styles/globals.css +2 -0
- package/dist/build-CEku1lyb.js +0 -2
- package/dist/generate-6Nbt7uii.js +0 -2
- package/dist/load-config-BnxocLAQ.js +0 -56
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
//#region src/server/workflow-protocol.ts
|
|
3
|
+
const journalEntrySchema = z.discriminatedUnion("kind", [
|
|
4
|
+
z.object({
|
|
5
|
+
kind: z.literal("step"),
|
|
6
|
+
id: z.string(),
|
|
7
|
+
name: z.string(),
|
|
8
|
+
result: z.unknown()
|
|
9
|
+
}),
|
|
10
|
+
z.object({
|
|
11
|
+
kind: z.literal("sleep"),
|
|
12
|
+
id: z.string(),
|
|
13
|
+
name: z.string(),
|
|
14
|
+
until: z.number()
|
|
15
|
+
}),
|
|
16
|
+
z.object({
|
|
17
|
+
kind: z.literal("attempt"),
|
|
18
|
+
id: z.string(),
|
|
19
|
+
name: z.string(),
|
|
20
|
+
error: z.string()
|
|
21
|
+
}),
|
|
22
|
+
z.object({
|
|
23
|
+
kind: z.literal("failed"),
|
|
24
|
+
id: z.string(),
|
|
25
|
+
name: z.string(),
|
|
26
|
+
error: z.string()
|
|
27
|
+
})
|
|
28
|
+
]);
|
|
29
|
+
const workflowInvocationRequestSchema = z.object({
|
|
30
|
+
payload: z.unknown(),
|
|
31
|
+
journal: z.array(journalEntrySchema)
|
|
32
|
+
});
|
|
33
|
+
const workflowOutcomeSchema = z.discriminatedUnion("kind", [
|
|
34
|
+
z.object({
|
|
35
|
+
kind: z.literal("step"),
|
|
36
|
+
id: z.string(),
|
|
37
|
+
name: z.string(),
|
|
38
|
+
result: z.unknown()
|
|
39
|
+
}),
|
|
40
|
+
z.object({
|
|
41
|
+
kind: z.literal("sleep"),
|
|
42
|
+
id: z.string(),
|
|
43
|
+
name: z.string(),
|
|
44
|
+
until: z.number()
|
|
45
|
+
}),
|
|
46
|
+
z.object({
|
|
47
|
+
kind: z.literal("step_error"),
|
|
48
|
+
id: z.string(),
|
|
49
|
+
name: z.string(),
|
|
50
|
+
message: z.string(),
|
|
51
|
+
retries: z.object({
|
|
52
|
+
limit: z.number(),
|
|
53
|
+
delayMs: z.number(),
|
|
54
|
+
backoff: z.enum([
|
|
55
|
+
"constant",
|
|
56
|
+
"linear",
|
|
57
|
+
"exponential"
|
|
58
|
+
])
|
|
59
|
+
})
|
|
60
|
+
}),
|
|
61
|
+
z.object({
|
|
62
|
+
kind: z.literal("done"),
|
|
63
|
+
result: z.unknown()
|
|
64
|
+
}),
|
|
65
|
+
z.object({
|
|
66
|
+
kind: z.literal("error"),
|
|
67
|
+
message: z.string()
|
|
68
|
+
})
|
|
69
|
+
]);
|
|
70
|
+
//#endregion
|
|
71
|
+
//#region src/server/workflow-engine.ts
|
|
72
|
+
var WorkflowFailedError = class extends Error {};
|
|
73
|
+
async function runWorkflowJournal(engine, invoke) {
|
|
74
|
+
const journal = [];
|
|
75
|
+
for (let index = 1;; index += 1) {
|
|
76
|
+
const outcome = await engine.invoke(`invoke ${index}`, () => invoke(journal));
|
|
77
|
+
switch (outcome.kind) {
|
|
78
|
+
case "step":
|
|
79
|
+
journal.push(outcome);
|
|
80
|
+
break;
|
|
81
|
+
case "sleep":
|
|
82
|
+
journal.push(outcome);
|
|
83
|
+
await engine.sleepUntil(`sleep ${index}`, new Date(outcome.until));
|
|
84
|
+
break;
|
|
85
|
+
case "step_error": {
|
|
86
|
+
const attempts = 1 + journal.filter((entry) => entry.kind === "attempt" && entry.id === outcome.id).length;
|
|
87
|
+
journal.push({
|
|
88
|
+
kind: "attempt",
|
|
89
|
+
id: outcome.id,
|
|
90
|
+
name: outcome.name,
|
|
91
|
+
error: outcome.message
|
|
92
|
+
});
|
|
93
|
+
if (attempts > outcome.retries.limit) {
|
|
94
|
+
journal.push({
|
|
95
|
+
kind: "failed",
|
|
96
|
+
id: outcome.id,
|
|
97
|
+
name: outcome.name,
|
|
98
|
+
error: outcome.message
|
|
99
|
+
});
|
|
100
|
+
break;
|
|
101
|
+
}
|
|
102
|
+
await engine.sleep(`retry ${index}`, outcome.retries.delayMs * (outcome.retries.backoff === "exponential" ? 2 ** (attempts - 1) : outcome.retries.backoff === "linear" ? attempts : 1));
|
|
103
|
+
break;
|
|
104
|
+
}
|
|
105
|
+
case "done": return outcome.result;
|
|
106
|
+
case "error": throw new WorkflowFailedError(outcome.message);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
//#endregion
|
|
111
|
+
export { workflowOutcomeSchema as a, workflowInvocationRequestSchema as i, runWorkflowJournal as n, journalEntrySchema as r, WorkflowFailedError as t };
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
1
3
|
import { chmod, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
2
|
-
import { join } from "node:path";
|
|
4
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
3
5
|
import { spawn } from "node:child_process";
|
|
4
6
|
import { homedir } from "node:os";
|
|
5
7
|
//#region ../shared/auth/src/store.mts
|
|
@@ -43,8 +45,8 @@ async function healCredentialStore(credentialsPath, source, store, unrecognizedC
|
|
|
43
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}).`);
|
|
44
46
|
return store;
|
|
45
47
|
}
|
|
46
|
-
async function readRawCredentialStore(
|
|
47
|
-
const { credentialsPath } = storePaths(
|
|
48
|
+
async function readRawCredentialStore() {
|
|
49
|
+
const { credentialsPath } = storePaths();
|
|
48
50
|
let source;
|
|
49
51
|
try {
|
|
50
52
|
source = await readFile(credentialsPath, "utf8");
|
|
@@ -111,9 +113,9 @@ function organizationInstanceUrl(instanceUrl, orgCode) {
|
|
|
111
113
|
url.hostname = [orgCode, ...labels.slice(1)].join(".");
|
|
112
114
|
return url.toString().replace(/\/$/, "");
|
|
113
115
|
}
|
|
114
|
-
function storePaths(
|
|
115
|
-
const env =
|
|
116
|
-
const home =
|
|
116
|
+
function storePaths() {
|
|
117
|
+
const env = process.env;
|
|
118
|
+
const home = homedir();
|
|
117
119
|
const configDir = join(env.XDG_CONFIG_HOME || join(home, ".config"), "hitch");
|
|
118
120
|
return {
|
|
119
121
|
configDir,
|
|
@@ -122,28 +124,28 @@ function storePaths(options = {}) {
|
|
|
122
124
|
credentialsPath: join(configDir, "credentials.json")
|
|
123
125
|
};
|
|
124
126
|
}
|
|
125
|
-
async function resolveInstanceUrl(override
|
|
127
|
+
async function resolveInstanceUrl(override) {
|
|
126
128
|
if (override !== void 0) return normalizeInstanceUrl(override);
|
|
127
|
-
const envInstanceUrl =
|
|
129
|
+
const envInstanceUrl = process.env.HITCH_API_URL;
|
|
128
130
|
if (envInstanceUrl !== void 0 && envInstanceUrl.length > 0) return normalizeInstanceUrl(envInstanceUrl);
|
|
129
|
-
const { configPath } = storePaths(
|
|
131
|
+
const { configPath } = storePaths();
|
|
130
132
|
const config = await readJson(configPath);
|
|
131
133
|
if (config === void 0) return DEFAULT_INSTANCE_URL;
|
|
132
134
|
if (!isObject(config) || config.instanceUrl !== void 0 && typeof config.instanceUrl !== "string") throw new Error(`${configPath}: expected { instanceUrl?: string }.`);
|
|
133
135
|
return normalizeInstanceUrl(config.instanceUrl ?? "https://app.hitch42.com");
|
|
134
136
|
}
|
|
135
|
-
async function readCredentialStore(
|
|
136
|
-
const store = await readRawCredentialStore(
|
|
137
|
+
async function readCredentialStore() {
|
|
138
|
+
const store = await readRawCredentialStore();
|
|
137
139
|
return { instances: Object.fromEntries(Object.entries(store.instances).map(([instanceUrl, credentialsByOrg]) => [instanceUrl, Object.fromEntries(Object.entries(credentialsByOrg).flatMap(([orgCode, value]) => {
|
|
138
140
|
const credentials = parseCredentials(value);
|
|
139
141
|
return credentials === void 0 ? [] : [[orgCode, credentials]];
|
|
140
142
|
}))])) };
|
|
141
143
|
}
|
|
142
|
-
async function getCredentialsForOrg(instanceUrl, orgCode
|
|
143
|
-
return (await readCredentialStore(
|
|
144
|
+
async function getCredentialsForOrg(instanceUrl, orgCode) {
|
|
145
|
+
return (await readCredentialStore()).instances[normalizeInstanceUrl(instanceUrl)]?.[orgCode];
|
|
144
146
|
}
|
|
145
|
-
function environmentAuth(
|
|
146
|
-
const apiKey =
|
|
147
|
+
function environmentAuth() {
|
|
148
|
+
const apiKey = process.env.HITCH_API_KEY;
|
|
147
149
|
return apiKey !== void 0 && apiKey.length > 0 ? {
|
|
148
150
|
accessToken: apiKey,
|
|
149
151
|
source: "environment"
|
|
@@ -167,11 +169,11 @@ function resolveStoredAuthForOrg(credentialsByOrg, orgCode) {
|
|
|
167
169
|
const apiKeyOrgs = Object.values(credentialsByOrg).filter((credentials) => credentials.type === "apiKey").map(({ orgCode: storedOrg }) => storedOrg).sort();
|
|
168
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}".`);
|
|
169
171
|
}
|
|
170
|
-
async function resolveAuthForOrg(instanceUrl, orgCode
|
|
171
|
-
const auth = environmentAuth(
|
|
172
|
+
async function resolveAuthForOrg(instanceUrl, orgCode) {
|
|
173
|
+
const auth = environmentAuth();
|
|
172
174
|
if (auth !== void 0) return auth;
|
|
173
175
|
const normalizedUrl = normalizeInstanceUrl(instanceUrl);
|
|
174
|
-
const credentialsByOrg = Object.entries((await readCredentialStore(
|
|
176
|
+
const credentialsByOrg = Object.entries((await readCredentialStore()).instances).find(([instance]) => instance === normalizedUrl)?.[1];
|
|
175
177
|
return resolveStoredAuthForOrg(credentialsByOrg ?? {}, orgCode);
|
|
176
178
|
}
|
|
177
179
|
function rejectEnvironmentKey(response, auth) {
|
|
@@ -180,9 +182,9 @@ function rejectEnvironmentKey(response, auth) {
|
|
|
180
182
|
function bearerToken(credentials) {
|
|
181
183
|
return "apiKey" in credentials ? credentials.apiKey : credentials.accessToken;
|
|
182
184
|
}
|
|
183
|
-
async function setCredentials(instanceUrl, credentials
|
|
184
|
-
const paths = storePaths(
|
|
185
|
-
const store = await readRawCredentialStore(
|
|
185
|
+
async function setCredentials(instanceUrl, credentials) {
|
|
186
|
+
const paths = storePaths();
|
|
187
|
+
const store = await readRawCredentialStore();
|
|
186
188
|
const normalizedUrl = normalizeInstanceUrl(instanceUrl);
|
|
187
189
|
const credentialsByOrg = store.instances[normalizedUrl];
|
|
188
190
|
store.instances[normalizedUrl] = {
|
|
@@ -289,17 +291,17 @@ async function pollForToken(fetcher, instanceUrl, device, now, sleep) {
|
|
|
289
291
|
throw new AuthenticationError("The sign-in request expired. Run hitch login again.");
|
|
290
292
|
}
|
|
291
293
|
async function loginWithDevice(instanceUrl, options = {}) {
|
|
292
|
-
const fetcher =
|
|
293
|
-
const now =
|
|
294
|
+
const fetcher = globalThis.fetch;
|
|
295
|
+
const now = Date.now;
|
|
294
296
|
const { body, response } = await postJson(fetcher, `${instanceUrl}/api/auth/device/code`, { client_id: CLIENT_ID });
|
|
295
297
|
if (!response.ok) throw new AuthenticationError(typeof body.error_description === "string" ? body.error_description : `Could not start sign-in (${response.status}).`);
|
|
296
298
|
const device = parseDeviceCode(body);
|
|
297
299
|
const verificationUrl = options.expectedOrg === void 0 ? device.verification_uri_complete : `${device.verification_uri_complete}&org=${encodeURIComponent(options.expectedOrg)}`;
|
|
298
|
-
|
|
300
|
+
console.log(`Open ${verificationUrl} and confirm code ${device.user_code}.`);
|
|
299
301
|
if (!options.noBrowser) try {
|
|
300
|
-
|
|
302
|
+
defaultOpenUrl(verificationUrl);
|
|
301
303
|
} catch {}
|
|
302
|
-
const token = await pollForToken(fetcher, instanceUrl, device, now,
|
|
304
|
+
const token = await pollForToken(fetcher, instanceUrl, device, now, (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds)));
|
|
303
305
|
const { body: organizationBody, response: organizationResponse } = await postJson(fetcher, `${instanceUrl}/api/auth/device/organization`, { device_code: device.device_code });
|
|
304
306
|
if (!organizationResponse.ok) throw new AuthenticationError("The sign-in did not resolve an organization.");
|
|
305
307
|
const organization = parseDeviceOrganization(organizationBody);
|
|
@@ -317,7 +319,7 @@ async function loginWithDevice(instanceUrl, options = {}) {
|
|
|
317
319
|
organizationId: organization.organizationId,
|
|
318
320
|
orgCode: organization.orgCode
|
|
319
321
|
};
|
|
320
|
-
await setCredentials(instanceUrl, credentials
|
|
322
|
+
await setCredentials(instanceUrl, credentials);
|
|
321
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`);
|
|
322
324
|
return {
|
|
323
325
|
credentials,
|
|
@@ -326,25 +328,58 @@ async function loginWithDevice(instanceUrl, options = {}) {
|
|
|
326
328
|
};
|
|
327
329
|
}
|
|
328
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
|
|
329
358
|
//#region src/auth.ts
|
|
330
|
-
async function fetchWithOrgAuth(orgCode,
|
|
331
|
-
const
|
|
332
|
-
const instanceUrl = await resolveInstanceUrl(void 0, options.store);
|
|
359
|
+
async function fetchWithOrgAuth(orgCode, request) {
|
|
360
|
+
const instanceUrl = await resolveInstanceUrl();
|
|
333
361
|
const organizationUrl = organizationInstanceUrl(instanceUrl, orgCode);
|
|
334
|
-
const auth = await resolveAuthForOrg(instanceUrl, orgCode
|
|
362
|
+
const auth = await resolveAuthForOrg(instanceUrl, orgCode);
|
|
335
363
|
if (auth !== void 0) {
|
|
336
364
|
const response = await request(organizationUrl, bearerToken(auth));
|
|
337
|
-
|
|
365
|
+
rejectEnvironmentKey(response, auth);
|
|
366
|
+
if (response.status !== 401) return response;
|
|
338
367
|
}
|
|
339
|
-
if (
|
|
340
|
-
await
|
|
341
|
-
|
|
342
|
-
fetch: fetcher,
|
|
343
|
-
store: options.store
|
|
344
|
-
});
|
|
345
|
-
const credentials = await getCredentialsForOrg(instanceUrl, orgCode, options.store);
|
|
368
|
+
if (process.stdin.isTTY !== true) throw new AuthenticationError("Not logged in. Run applet login, or set HITCH_API_KEY.");
|
|
369
|
+
await loginWithDevice(instanceUrl, { expectedOrg: orgCode });
|
|
370
|
+
const credentials = await getCredentialsForOrg(instanceUrl, orgCode);
|
|
346
371
|
if (credentials === void 0) throw new Error(`login did not provide credentials for organization "${orgCode}"`);
|
|
347
372
|
return request(organizationUrl, bearerToken(credentials));
|
|
348
373
|
}
|
|
349
374
|
//#endregion
|
|
350
|
-
|
|
375
|
+
//#region src/workspace.ts
|
|
376
|
+
function resolveWorkspaceOrg(appletRoot) {
|
|
377
|
+
const appletsDir = dirname(appletRoot);
|
|
378
|
+
if (basename(appletsDir) === "applets") {
|
|
379
|
+
const config = readWorkspaceConfig(dirname(appletsDir));
|
|
380
|
+
if (config !== void 0) return config.org;
|
|
381
|
+
}
|
|
382
|
+
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.`);
|
|
383
|
+
}
|
|
384
|
+
//#endregion
|
|
385
|
+
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 };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hitch42/applet",
|
|
3
|
-
"version": "0.1.1-beta.
|
|
3
|
+
"version": "0.1.1-beta.10",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"bin": {
|
|
6
6
|
"applet": "./dist/applet.js"
|
|
@@ -27,6 +27,10 @@
|
|
|
27
27
|
"source": "./src/react/index.ts",
|
|
28
28
|
"default": "./dist/react.js"
|
|
29
29
|
},
|
|
30
|
+
"./telephony": {
|
|
31
|
+
"source": "./src/telephony/index.ts",
|
|
32
|
+
"default": "./dist/telephony.js"
|
|
33
|
+
},
|
|
30
34
|
"./vite": {
|
|
31
35
|
"source": "./src/vite.ts",
|
|
32
36
|
"default": "./dist/vite.js"
|
|
@@ -38,10 +42,15 @@
|
|
|
38
42
|
"./_internal/server": {
|
|
39
43
|
"source": "./src/internal/server.ts",
|
|
40
44
|
"default": "./dist/_internal/server.js"
|
|
45
|
+
},
|
|
46
|
+
"./_internal/log-context": {
|
|
47
|
+
"source": "./src/server/log-context.ts",
|
|
48
|
+
"default": "./dist/_internal/log-context.js"
|
|
41
49
|
}
|
|
42
50
|
},
|
|
43
51
|
"dependencies": {
|
|
44
52
|
"@cloudflare/vite-plugin": "^1.51.1",
|
|
53
|
+
"@fontsource-variable/geist-mono": "^5.3.0",
|
|
45
54
|
"@fontsource-variable/inter": "^5.3.0",
|
|
46
55
|
"@hey-api/openapi-ts": "0.0.0-next-20260824173136",
|
|
47
56
|
"@orpc/client": "^1.15.0",
|
|
@@ -49,32 +58,36 @@
|
|
|
49
58
|
"@orpc/openapi-client": "^1.15.0",
|
|
50
59
|
"@orpc/tanstack-query": "^1.15.0",
|
|
51
60
|
"@tailwindcss/vite": "^4.3.0",
|
|
52
|
-
"@tanstack/query-async-storage-persister": "5.101.4",
|
|
53
61
|
"@tanstack/react-query-persist-client": "5.101.4",
|
|
54
62
|
"@tanstack/router-cli": "^1.167.24",
|
|
55
63
|
"@tanstack/router-plugin": "^1.168.23",
|
|
64
|
+
"@telnyx/webrtc": "^2.27.10",
|
|
56
65
|
"@tiptap/core": "^3.30.3",
|
|
57
66
|
"@tiptap/extension-image": "^3.30.3",
|
|
58
67
|
"@tiptap/extension-list": "^3.30.3",
|
|
59
68
|
"@tiptap/extension-table": "^3.30.3",
|
|
60
69
|
"@tiptap/extensions": "^3.30.3",
|
|
61
70
|
"@tiptap/markdown": "^3.30.3",
|
|
71
|
+
"@tiptap/pm": "^3.30.3",
|
|
62
72
|
"@tiptap/react": "^3.30.3",
|
|
63
73
|
"@tiptap/starter-kit": "^3.30.3",
|
|
64
74
|
"@vitejs/plugin-react": "^6.0.3",
|
|
65
75
|
"class-variance-authority": "^0.7.1",
|
|
66
|
-
"
|
|
76
|
+
"cn": "^0.4.0",
|
|
67
77
|
"croner": "^10.0.1",
|
|
68
78
|
"lucide-react": "^1.33.0",
|
|
79
|
+
"oxc-transform-react": "^0.149.0",
|
|
80
|
+
"oxfmt": "^0.72.0",
|
|
81
|
+
"oxlint": "^1.87.0",
|
|
82
|
+
"oxlint-tsgolint": "^7.0.2003",
|
|
69
83
|
"react-aria-components": "^1.20.0",
|
|
70
84
|
"shadcn": "^4.16.1",
|
|
85
|
+
"smol-toml": "^1.9.0",
|
|
71
86
|
"streamdown": "^2.6.0",
|
|
72
|
-
"tailwind-merge": "^3.6.0",
|
|
73
87
|
"tailwindcss": "^4.3.0",
|
|
74
88
|
"tw-animate-css": "^1.4.0",
|
|
75
89
|
"vite": "8.3.0",
|
|
76
90
|
"vitest": "4.1.11",
|
|
77
|
-
"wrangler": "^4.120.0",
|
|
78
91
|
"zod": "^4.4.3"
|
|
79
92
|
},
|
|
80
93
|
"peerDependencies": {
|
|
@@ -99,6 +112,6 @@
|
|
|
99
112
|
"publishConfig": {
|
|
100
113
|
"access": "public",
|
|
101
114
|
"registry": "https://registry.npmjs.org",
|
|
102
|
-
"tag": "
|
|
115
|
+
"tag": "latest"
|
|
103
116
|
}
|
|
104
117
|
}
|
|
@@ -13,21 +13,31 @@ Treat each applet as an isolated microfrontend. Keep it separate from the data s
|
|
|
13
13
|
|
|
14
14
|
This file holds the common rules. Read each matching reference before you edit an applet. Paths are relative to this file.
|
|
15
15
|
|
|
16
|
-
| Read this
|
|
17
|
-
|
|
|
18
|
-
| `references/client.md`
|
|
19
|
-
| `references/records.md`
|
|
20
|
-
| `references/files.md`
|
|
21
|
-
| `references/
|
|
22
|
-
| `references/
|
|
23
|
-
| `references/
|
|
24
|
-
| `references/
|
|
25
|
-
| `references/
|
|
16
|
+
| Read this | When the task involves |
|
|
17
|
+
| ------------------------------ | -------------------------------------------------------------------------------------------------------- |
|
|
18
|
+
| `references/client.md` | Client routes, navigation, page layout, forms, controls, or other user interface work |
|
|
19
|
+
| `references/records.md` | Listing, querying, creating, or updating records; filters, includes, aggregates; a missing record method |
|
|
20
|
+
| `references/files.md` | Uploading or reading files, attachment attributes, download links |
|
|
21
|
+
| `references/email-tracking.md` | Creating pixels and reading raw email hits |
|
|
22
|
+
| `references/markdown.md` | Rendering or editing markdown, image uploads, Hitch file links |
|
|
23
|
+
| `references/server.md` | The applet server: secrets, env variables, external services, the `api` client |
|
|
24
|
+
| `references/schedules.md` | Cron schedules and background runs |
|
|
25
|
+
| `references/workflows.md` | Multi-step server work: `createWorkflow`, `step.do`, `step.sleep`, starting, reading, and listing runs |
|
|
26
|
+
| `references/manifest.md` | The applet definition in `applet.manifest.ts`: objects, access, variables, and packages |
|
|
27
|
+
| `references/performance.md` | Data access design: client versus server, N+1 reads, includes, aggregates, pagination, batch writes |
|
|
28
|
+
|
|
29
|
+
For attribute types, filters, and API limits, read `../hitch-data/SKILL.md`. To inspect or change Hitch data and settings from the shell, read `../hitch-cli/SKILL.md`.
|
|
30
|
+
|
|
31
|
+
Read `../tanstack-router/SKILL.md` for route work and `../tanstack-query/SKILL.md` for query and cache work.
|
|
32
|
+
Read `../react-best-practices/SKILL.md` for React performance and `../web-design-guidelines/SKILL.md` for requested interface reviews.
|
|
33
|
+
Follow the Hitch applet rules for SDK-owned configuration, providers, and generated files.
|
|
26
34
|
|
|
27
35
|
## Work with the workspace
|
|
28
36
|
|
|
29
37
|
- Run package commands with `pnpm`.
|
|
30
|
-
-
|
|
38
|
+
- Run `pnpm exec applet login` from the workspace root or an applet directory to sign in. It uses the organization in `hitch.config.ts` and shares credentials with `hitch login`. Add `--no-browser` to print the sign-in URL without opening a browser.
|
|
39
|
+
- A server permission error is not a login request.
|
|
40
|
+
- After a workspace is linked to a repository, deploy in two ways. Run `pnpm exec applet deploy` to release a change immediately. Push to the production branch to deploy through CI and keep releases in sync with the repository.
|
|
31
41
|
- Run `pnpm exec applet new <code>` from the workspace root to create a full applet.
|
|
32
42
|
- Add `--client-only` or `--server-only` when the applet needs only one part.
|
|
33
43
|
- Use `--client-only` for a user interface that uses the current user's Hitch access and needs no server.
|
|
@@ -41,16 +51,22 @@ This file holds the common rules. Read each matching reference before you edit a
|
|
|
41
51
|
- Run `pnpm --dir applets/<code> run generate` after a Hitch object, operation, or theme changes.
|
|
42
52
|
- Run generation at the start of a session when the task depends on current Hitch types.
|
|
43
53
|
- Run `pnpm --dir applets/<code> run dev` and `pnpm --dir applets/<code> run build` from the workspace root.
|
|
54
|
+
- The dev server runs as you in the production org. Reads and writes use real data.
|
|
55
|
+
- The dev server prints a preview link. Open the link to see the local applet inside Hitch.
|
|
56
|
+
- Chrome asks once to allow local network access for the Hitch site.
|
|
57
|
+
- The preview lasts until you select Exit or close the tab.
|
|
58
|
+
- A pending `requiresChangeset` schema is not available in dev. Deploy to test it.
|
|
44
59
|
- Run `pnpm --dir applets/<code> run deploy` only when the user requests a deployment.
|
|
45
|
-
-
|
|
46
|
-
-
|
|
60
|
+
- The CLI creates missing MCP files and adds missing Hitch and Chrome DevTools entries. Existing entries and other settings belong to the user.
|
|
61
|
+
- The applet CLI replaces its generated support files. It creates `.github/workflows/hitch.yml` only when the file is absent. To stop it, set `githubWorkflow: false` in `hitch.config.ts` and delete the file.
|
|
47
62
|
- Remember that generation refreshes the local theme copy. Deployed applets load the public org theme from its Hitch `.com` URL.
|
|
48
63
|
|
|
49
64
|
## Use Vite and Vitest
|
|
50
65
|
|
|
51
66
|
- Import configuration APIs from `vite`.
|
|
52
67
|
- Import test APIs from `vitest`.
|
|
53
|
-
-
|
|
68
|
+
- Put tests in the applet's `test` folder and name them `*.test.ts` or `*.test.tsx`. Vitest does not run test files in other folders.
|
|
69
|
+
- Do not edit `vite.config.ts`. The applet CLI rewrites it. Do not add another Vite config.
|
|
54
70
|
- Add applet packages to `dependencies` or `devDependencies` in `applet.manifest.ts`.
|
|
55
71
|
|
|
56
72
|
## Access Hitch data
|
|
@@ -118,9 +134,28 @@ Read `references/files.md` before download links, attachment attributes, or serv
|
|
|
118
134
|
- Add useful `console.<level>` calls around important server work and failures. Deployed logs often provide the clearest evidence for applet issues.
|
|
119
135
|
- Choose `console.log`, `console.info`, `console.warn`, or `console.error` deliberately. The console method sets the entry level used by log filters.
|
|
120
136
|
- Run `pnpm --dir applets/<code> exec applet logs --filter '{"type":"applet"}'` from the workspace root to inspect applet code logs.
|
|
121
|
-
- Include the applet type filter during diagnosis. Without it, Cloudflare invocation entries can hide the useful applet entries.
|
|
137
|
+
- Include the applet type filter during diagnosis. Without it, Cloudflare invocation entries can hide the useful applet entries. For a workflow run, use `--run <id>` or `--filter '{"type":"workflow"}'` instead.
|
|
122
138
|
- Do not log secrets, tokens, or personal data.
|
|
123
139
|
|
|
140
|
+
## Add request context with `log.set()`
|
|
141
|
+
|
|
142
|
+
Each server request gets one request entry with the route, status, outcome, CPU time, and wall time. Add facts about the request to that entry:
|
|
143
|
+
|
|
144
|
+
```ts
|
|
145
|
+
import { log } from "@hitch42/applet/server";
|
|
146
|
+
|
|
147
|
+
log.set({ invoiceId, processedCount });
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
- Set the IDs, counts, and decisions that explain what the request did. One entry with many fields is easier to filter than many `console.log` lines.
|
|
151
|
+
- A later call replaces an earlier value with the same top-level key.
|
|
152
|
+
- To fail a request, throw. The request entry records the error and its stack.
|
|
153
|
+
- To record a problem that the code recovered from, set a field, for example `log.set({ fallbackUsed: true })`.
|
|
154
|
+
- Use `console.<level>` for a separate line with its own level.
|
|
155
|
+
- Outside a request, for example at module scope, `log.set()` warns and drops the fields.
|
|
156
|
+
- A single call larger than 16 KB is dropped. The entry then shows `noteDropped`.
|
|
157
|
+
- Run `pnpm --dir applets/<code> exec applet logs --filter '{"type":"invocation"}'` to read request entries. The CLI prints the fields and any error stack.
|
|
158
|
+
|
|
124
159
|
## Verify changes
|
|
125
160
|
|
|
126
161
|
Run these commands from the workspace root before deployment:
|
|
@@ -128,6 +163,8 @@ Run these commands from the workspace root before deployment:
|
|
|
128
163
|
```sh
|
|
129
164
|
pnpm --dir applets/<code> run generate
|
|
130
165
|
pnpm --dir applets/<code> run typecheck
|
|
166
|
+
pnpm --dir applets/<code> run fmt
|
|
167
|
+
pnpm --dir applets/<code> run lint
|
|
131
168
|
pnpm --dir applets/<code> run test # Only when the applet contains tests.
|
|
132
169
|
pnpm --dir applets/<code> run build
|
|
133
170
|
```
|
|
@@ -135,3 +172,11 @@ pnpm --dir applets/<code> run build
|
|
|
135
172
|
If missing packages block a check, stop package checks. Do not retry the check through another binary.
|
|
136
173
|
|
|
137
174
|
If a command fails, report the exact blocker. Do not deploy as a verification step.
|
|
175
|
+
|
|
176
|
+
## Deploy from GitHub
|
|
177
|
+
|
|
178
|
+
- A push to the production branch runs `applet deploy` in the workspace root through GitHub Actions.
|
|
179
|
+
- An applet deploys when its folder or a file outside `applets/` differs from its current release, including a local `applet deploy`. Uncommitted and untracked files count. Gitignored files do not count. An applet whose current release has no source hash always deploys.
|
|
180
|
+
- The deploy skips other applets and logs the reason. A skip is expected, not a failure.
|
|
181
|
+
- A schema change in Hitch alone does not trigger CI.
|
|
182
|
+
- Do not import from a sibling applet folder. The change check does not follow such imports.
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# Email tracking
|
|
2
|
+
|
|
3
|
+
Create one pixel for each email. Put its URL in the HTML before send.
|
|
4
|
+
Add the pixel next to CID images. A CID image cannot make a tracking request.
|
|
5
|
+
|
|
6
|
+
```ts
|
|
7
|
+
const { id, url } = await hitch.email.tracking.createPixel({
|
|
8
|
+
tags: { campaignId: "fall", buyerId: "acme", version: "v2" },
|
|
9
|
+
});
|
|
10
|
+
const html = `<p>Hello</p><img src="${url}" width="1" height="1" alt="">`;
|
|
11
|
+
// Send the email with the applet's email provider.
|
|
12
|
+
await hitch.email.tracking.markSent({ id });
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
`markSent({ id, sentAt })` accepts an ISO time. A later call replaces the sent time.
|
|
16
|
+
`get({ id })` returns one tracker. `list()` returns up to 100 trackers by default.
|
|
17
|
+
Use `filter`, `sentAt`, `cursor`, and `limit` to select a page.
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
const tracker = await hitch.email.tracking.get({ id });
|
|
21
|
+
const query = {
|
|
22
|
+
filter: { buyerId: "acme" },
|
|
23
|
+
sentAt: { $gte: "2026-10-01T00:00:00Z" },
|
|
24
|
+
limit: 100,
|
|
25
|
+
};
|
|
26
|
+
const page = await hitch.email.tracking.list(query);
|
|
27
|
+
const next = page.nextCursor
|
|
28
|
+
? await hitch.email.tracking.list({ ...query, cursor: page.nextCursor })
|
|
29
|
+
: null;
|
|
30
|
+
const opened =
|
|
31
|
+
tracker.lastHitAt !== null &&
|
|
32
|
+
new Date(tracker.lastHitAt).getTime() >
|
|
33
|
+
new Date(tracker.sentAt ?? tracker.createdAt).getTime() + 60_000;
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
The example rule counts a tracker as opened when `lastHitAt` is more than 60 seconds after send or creation.
|
|
37
|
+
Hitch records `firstHitAt` and `lastHitAt`. It does not classify opens.
|
|
38
|
+
Each page must repeat the same filter, `sentAt`, and limit.
|
|
39
|
+
|
|
40
|
+
Tags stay fixed after creation. Use at most 10 tags. A key starts with an ASCII letter.
|
|
41
|
+
The rest contains ASCII letters, digits, or underscores, up to 64 characters total.
|
|
42
|
+
Each value has 1 to 200 characters. A filter matches every tag by exact equality.
|
|
43
|
+
Everyone with applet access sees all its trackers.
|
|
44
|
+
|
|
45
|
+
Gmail can fetch through `GoogleImageProxy` a few seconds after send.
|
|
46
|
+
A real Gmail open has the same user agent. A Gmail draft preview can fetch before send.
|
|
47
|
+
Apple Mail Privacy Protection fetches on delivery with `Mozilla/5.0`.
|
|
48
|
+
That fetch looks like a real open. Proton fetches on delivery.
|
|
49
|
+
Clients with images off never fetch the pixel.
|
|
@@ -67,7 +67,7 @@ Use `download` when a component needs one fresh link without query state:
|
|
|
67
67
|
|
|
68
68
|
```ts
|
|
69
69
|
const file = await hitch.files.download(id);
|
|
70
|
-
file.downloadUrl; // a download link, valid for
|
|
70
|
+
file.downloadUrl; // a download link, valid for at least a day
|
|
71
71
|
```
|
|
72
72
|
|
|
73
73
|
Use the `get` query utilities when React needs file metadata as query state.
|
|
@@ -86,11 +86,11 @@ if (fileQuery.data) {
|
|
|
86
86
|
row.filename; // "report.csv"
|
|
87
87
|
row.contentType; // "text/csv"
|
|
88
88
|
row.sizeBytes; // 1024
|
|
89
|
-
downloadUrl; // a download link, valid for
|
|
89
|
+
downloadUrl; // a download link, valid for at least a day
|
|
90
90
|
}
|
|
91
91
|
```
|
|
92
92
|
|
|
93
|
-
A `downloadUrl` expires after
|
|
93
|
+
A `downloadUrl` expires after 24 to 48 hours. Call `get` again for a fresh link instead of storing one.
|
|
94
94
|
|
|
95
95
|
## Link a file to a record
|
|
96
96
|
|
|
@@ -11,6 +11,7 @@ The applet tool reads `org` from `hitch.config.ts` in the workspace root.
|
|
|
11
11
|
- `name` gives the applet a display name.
|
|
12
12
|
- `code` gives the applet its stable code and URL prefix.
|
|
13
13
|
- `objects` declares record objects and attribute access.
|
|
14
|
+
- `requiresChangeset` optionally names the schema changeset that the code needs.
|
|
14
15
|
- `variables` optionally declares runtime variables.
|
|
15
16
|
- `schedules` optionally declares scheduled server calls.
|
|
16
17
|
- `dependencies` optionally declares applet runtime packages.
|
|
@@ -42,6 +43,22 @@ The manifest and applet definition reject unknown fields.
|
|
|
42
43
|
- Expect the Vite base path to be `/<code>/`.
|
|
43
44
|
- Include the code in each client route path.
|
|
44
45
|
|
|
46
|
+
## Set `requiresChangeset`
|
|
47
|
+
|
|
48
|
+
Set `requiresChangeset` when the code needs a schema change that is not live yet.
|
|
49
|
+
|
|
50
|
+
```ts
|
|
51
|
+
requiresChangeset: "0192a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
- Use the ID of a changeset in the same organization.
|
|
55
|
+
- Generate, build, and deploy all use this field. The CLI has no changeset flag.
|
|
56
|
+
- While the changeset is pending, types and the build use the schema that the changeset will produce.
|
|
57
|
+
- While the changeset is pending, a deploy binds the release to the changeset. The release goes live when a person applies the changeset in Hitch.
|
|
58
|
+
- After the changeset applies, the field uses the live schema. A deploy then promotes as usual. Remove the field when it is convenient.
|
|
59
|
+
- A deploy fails when the changeset was abandoned. Remove the field or name a pending changeset.
|
|
60
|
+
- A git push uses the same rules. The push never applies the changeset.
|
|
61
|
+
|
|
45
62
|
## Declare objects
|
|
46
63
|
|
|
47
64
|
Add one entry for each object that the applet needs.
|
|
@@ -66,6 +83,14 @@ Each object entry has five required fields.
|
|
|
66
83
|
- `readOnly` lists attribute codes that the applet can read but cannot write.
|
|
67
84
|
- `readWrite` lists attribute codes that the applet can read and write.
|
|
68
85
|
|
|
86
|
+
Set `optional: true` when the applet works for a viewer without access to this object.
|
|
87
|
+
List codes from `readOnly` or `readWrite` in `optionalAttributes` when some viewers cannot see their values.
|
|
88
|
+
Typegen makes these record fields nullable for every viewer.
|
|
89
|
+
These fields never grant or remove access.
|
|
90
|
+
They matter when an org shares the applet with a connected organisation.
|
|
91
|
+
Hitch refuses a share if the connected org lacks access to a required object.
|
|
92
|
+
Hitch also refuses a share if a required object lists a required user attribute outside `optionalAttributes`.
|
|
93
|
+
|
|
69
94
|
- Use the applet code format for each object code.
|
|
70
95
|
- Do not declare the same object code more than once.
|
|
71
96
|
- Use the applet code format for each attribute code.
|
|
@@ -75,14 +100,16 @@ Each object entry has five required fields.
|
|
|
75
100
|
|
|
76
101
|
The object declaration caps record access for the applet identity.
|
|
77
102
|
|
|
78
|
-
- Declare only the objects that
|
|
79
|
-
- Declare only the attributes that
|
|
103
|
+
- Declare only the objects that the applet uses.
|
|
104
|
+
- Declare only the attributes that the applet uses.
|
|
105
|
+
- The declaration sets both the generated contract and the applet identity's cap.
|
|
80
106
|
- Use `readOnly` for attributes that the applet identity must not change.
|
|
81
107
|
- Use `readWrite` for attributes that the applet identity can change.
|
|
82
108
|
- Set `canCreate` and `canDelete` to the required values.
|
|
83
109
|
- The generated contract omits undeclared objects.
|
|
84
110
|
- The generated contract removes `create` when `canCreate` is false.
|
|
85
111
|
- The generated contract removes `delete` when `canDelete` is false.
|
|
112
|
+
- The generated contract removes `update` and `updateWhere` when `readWrite` is empty.
|
|
86
113
|
- Regenerate after an object grant changes.
|
|
87
114
|
- Do not restore a removed object or method with a cast.
|
|
88
115
|
|