khotan-data 0.2.0 → 0.3.1

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.
@@ -4,43 +4,51 @@
4
4
  //
5
5
  // Copy this file, rename it for your source/destination pair, and register the
6
6
  // exported pass handler in {outputDir}/khotan.ts.
7
+ //
8
+ // IMPORTANT — Workflow step structure:
9
+ // Declare "use step" functions at module top level and pass them only
10
+ // serializable values (the `ctx` object is plain data and is safe to pass).
11
+ // Do NOT nest step functions inside the "use workflow" function — the Workflow
12
+ // compiler cannot hoist closures that capture workflow scope, and they fail at
13
+ // runtime in the sandbox. Keep the workflow body limited to orchestration.
7
14
  // ============================================================================
8
15
 
9
16
  import { pass, type PassContext } from "./pass";
10
17
 
11
- async function stripeToSlackWorkflow(ctx: PassContext) {
12
- "use workflow";
13
-
14
- async function forwardEvent() {
15
- "use step";
16
- console.log("Forwarding webhook event", {
17
- eventType: ctx.eventType,
18
- khotanRunId: ctx.khotanRunId,
19
- });
18
+ // Step: full Node.js access, retried independently. Receives serializable ctx.
19
+ async function forwardEvent(ctx: PassContext) {
20
+ "use step";
21
+ console.log("Forwarding webhook event", {
22
+ eventType: ctx.eventType,
23
+ khotanRunId: ctx.khotanRunId,
24
+ });
20
25
 
21
- await fetch("https://slack.com/api/chat.postMessage", {
22
- method: "POST",
23
- headers: {
24
- Authorization: `Bearer ${ctx.destVars["botToken"] ?? ""}`,
25
- "Content-Type": "application/json",
26
- },
27
- body: JSON.stringify({
28
- channel: ctx.destVars["channelId"],
29
- text: `Received ${ctx.eventType}`,
30
- blocks: [
31
- {
32
- type: "section",
33
- text: {
34
- type: "mrkdwn",
35
- text: `Received ${ctx.eventType} from Stripe`,
36
- },
26
+ await fetch("https://slack.com/api/chat.postMessage", {
27
+ method: "POST",
28
+ headers: {
29
+ Authorization: `Bearer ${ctx.destVars["botToken"] ?? ""}`,
30
+ "Content-Type": "application/json",
31
+ },
32
+ body: JSON.stringify({
33
+ channel: ctx.destVars["channelId"],
34
+ text: `Received ${ctx.eventType}`,
35
+ blocks: [
36
+ {
37
+ type: "section",
38
+ text: {
39
+ type: "mrkdwn",
40
+ text: `Received ${ctx.eventType} from Stripe`,
37
41
  },
38
- ],
39
- }),
40
- });
41
- }
42
+ },
43
+ ],
44
+ }),
45
+ });
46
+ }
42
47
 
43
- await forwardEvent();
48
+ // Workflow: orchestration only. Calls top-level steps with serializable args.
49
+ async function stripeToSlackWorkflow(ctx: PassContext) {
50
+ "use workflow";
51
+ await forwardEvent(ctx);
44
52
  }
45
53
 
46
54
  export const stripeToSlack = pass({
@@ -85,36 +85,41 @@ export function pass(config: PassConfig): PassRegistration {
85
85
  // import { pass, type PassContext } from "./pass";
86
86
  // import { plug } from "../plugs/plug";
87
87
  //
88
- // async function pollinateToSlackWorkflow(ctx: PassContext) {
89
- // "use workflow";
88
+ // Declare "use step" functions at MODULE TOP LEVEL and pass them serializable
89
+ // values only (`ctx` is plain data). Do NOT nest steps inside the "use workflow"
90
+ // function — closures over workflow scope cannot be hoisted and fail at runtime.
90
91
  //
91
- // async function forwardEvent() {
92
- // "use step";
93
- // const cache = khotanCache(ctx, "pollinate-forwarded-events");
94
- // const eventId = String(ctx.event["id"] ?? "");
95
- // if (eventId && (await cache.get<boolean>(eventId))) return;
92
+ // // Step: top-level, full Node.js access, retried independently.
93
+ // async function forwardEvent(ctx: PassContext) {
94
+ // "use step";
95
+ // const cache = khotanCache(ctx, "pollinate-forwarded-events");
96
+ // const eventId = String(ctx.event["id"] ?? "");
97
+ // if (eventId && (await cache.get<boolean>(eventId))) return;
96
98
  //
97
- // // Construct destination plug from destVars
98
- // const slackPlug = plug({
99
- // name: "slack",
100
- // baseUrl: "https://hooks.slack.com",
101
- // authType: "bearer",
102
- // auth: { bearer: { token: ctx.destVars["token"] ?? "" } },
103
- // });
99
+ // // Construct destination plug from destVars
100
+ // const slackPlug = plug({
101
+ // name: "slack",
102
+ // baseUrl: "https://hooks.slack.com",
103
+ // authType: "bearer",
104
+ // auth: { bearer: { token: ctx.destVars["token"] ?? "" } },
105
+ // });
104
106
  //
105
- // await slackPlug.post("/services/webhook", {
106
- // body: {
107
- // text: `Received ${ctx.eventType} event from pollinate`,
108
- // event: ctx.event,
109
- // },
110
- // });
107
+ // await slackPlug.post("/services/webhook", {
108
+ // body: {
109
+ // text: `Received ${ctx.eventType} event from pollinate`,
110
+ // event: ctx.event,
111
+ // },
112
+ // });
111
113
  //
112
- // if (eventId) {
113
- // await cache.set(eventId, true);
114
- // }
114
+ // if (eventId) {
115
+ // await cache.set(eventId, true);
115
116
  // }
117
+ // }
116
118
  //
117
- // await forwardEvent();
119
+ // // Workflow: orchestration only.
120
+ // async function pollinateToSlackWorkflow(ctx: PassContext) {
121
+ // "use workflow";
122
+ // await forwardEvent(ctx);
118
123
  // }
119
124
  //
120
125
  // export const pollinateToSlack = pass({
@@ -4,62 +4,70 @@
4
4
  //
5
5
  // Copy this file, rename it for your source/destination pair, and register the
6
6
  // exported flow in {outputDir}/khotan.ts.
7
+ //
8
+ // IMPORTANT — Workflow step structure:
9
+ // Declare "use step" functions at module top level and pass them only
10
+ // serializable values (the `ctx` object is plain data and is safe to pass).
11
+ // Do NOT nest step functions inside the "use workflow" function — the Workflow
12
+ // compiler cannot hoist closures that capture workflow scope, and they fail at
13
+ // runtime in the sandbox. Keep the workflow body limited to orchestration.
7
14
  // ============================================================================
8
15
 
9
16
  import { khotanCache } from "khotan-data/factory";
10
17
  import { relay, type RelayContext } from "./relay";
11
18
 
12
- async function shopifyToHubspotWorkflow(ctx: RelayContext) {
13
- "use workflow";
19
+ // Step: full Node.js access, retried independently. Receives serializable ctx.
20
+ async function forwardProducts(ctx: RelayContext) {
21
+ "use step";
22
+ console.log("Starting relay", {
23
+ flow: ctx.flow.name,
24
+ to: ctx.flow.to,
25
+ khotanRunId: ctx.khotanRunId,
26
+ runType: ctx.runType,
27
+ });
14
28
 
15
- async function forwardProducts() {
16
- "use step";
17
- console.log("Starting relay", {
18
- flow: ctx.flow.name,
19
- to: ctx.flow.to,
20
- khotanRunId: ctx.khotanRunId,
21
- runType: ctx.runType,
22
- });
29
+ const sourceResponse = await fetch("https://source.example.com/products", {
30
+ headers: {
31
+ Authorization: `Bearer ${ctx.vars["sourceToken"] ?? ""}`,
32
+ },
33
+ });
34
+ const payload = (await sourceResponse.json()) as {
35
+ data?: Array<Record<string, unknown>>;
36
+ };
37
+ const records = Array.isArray(payload.data) ? payload.data : [];
38
+ const snapshotCache = khotanCache(ctx, "shopify-products-snapshot");
39
+ const previousRecords =
40
+ (await snapshotCache.get<Array<Record<string, unknown>>>("latest")) ?? [];
41
+
42
+ await snapshotCache.set("latest", records, { ttl: "6h" });
23
43
 
24
- const sourceResponse = await fetch("https://source.example.com/products", {
44
+ for (const record of records) {
45
+ await fetch("https://destination.example.com/products", {
46
+ method: "POST",
25
47
  headers: {
26
- Authorization: `Bearer ${ctx.vars["sourceToken"] ?? ""}`,
48
+ Authorization: `Bearer ${ctx.vars["destinationToken"] ?? ""}`,
49
+ "Content-Type": "application/json",
27
50
  },
51
+ body: JSON.stringify(record),
28
52
  });
29
- const payload = (await sourceResponse.json()) as {
30
- data?: Array<Record<string, unknown>>;
31
- };
32
- const records = Array.isArray(payload.data) ? payload.data : [];
33
- const snapshotCache = khotanCache(ctx, "shopify-products-snapshot");
34
- const previousRecords =
35
- (await snapshotCache.get<Array<Record<string, unknown>>>("latest")) ?? [];
36
-
37
- await snapshotCache.set("latest", records, { ttl: "6h" });
38
-
39
- for (const record of records) {
40
- await fetch("https://destination.example.com/products", {
41
- method: "POST",
42
- headers: {
43
- Authorization: `Bearer ${ctx.vars["destinationToken"] ?? ""}`,
44
- "Content-Type": "application/json",
45
- },
46
- body: JSON.stringify(record),
47
- });
48
- }
49
-
50
- return {
51
- extracted: records.length,
52
- transformed: records.length,
53
- created: records.length,
54
- metadata: {
55
- relay: ctx.flow.name,
56
- to: ctx.flow.to,
57
- previousCount: previousRecords.length,
58
- },
59
- };
60
53
  }
61
54
 
62
- return forwardProducts();
55
+ return {
56
+ extracted: records.length,
57
+ transformed: records.length,
58
+ created: records.length,
59
+ metadata: {
60
+ relay: ctx.flow.name,
61
+ to: ctx.flow.to,
62
+ previousCount: previousRecords.length,
63
+ },
64
+ };
65
+ }
66
+
67
+ // Workflow: orchestration only. Calls top-level steps with serializable args.
68
+ async function shopifyToHubspotWorkflow(ctx: RelayContext) {
69
+ "use workflow";
70
+ return forwardProducts(ctx);
63
71
  }
64
72
 
65
73
  export const shopifyToHubspotRelay = relay({
@@ -51,48 +51,53 @@ export function relay(config: RelayConfig): FlowRegistration {
51
51
  // Usage Example (create a file like flows/shopify-to-hubspot.ts)
52
52
  // ---------------------------------------------------------------------------
53
53
  //
54
+ // Declare "use step" functions at MODULE TOP LEVEL and pass them serializable
55
+ // values only (`ctx` is plain data). Do NOT nest steps inside the "use workflow"
56
+ // function — closures over workflow scope cannot be hoisted and fail at runtime.
57
+ //
54
58
  // import { bindWorkflowPlug, khotanCache, relay, type RelayContext } from "khotan-data/factory";
55
59
  // import { shopifyPlug } from "../plugs/shopify";
56
60
  // import { hubspotPlug } from "../plugs/hubspot";
57
61
  //
58
- // async function shopifyToHubspotWorkflow(ctx: RelayContext) {
59
- // "use workflow";
60
- //
61
- // async function forwardProducts() {
62
- // "use step";
63
- // console.log("Starting relay", {
64
- // flow: ctx.flow.name,
65
- // to: ctx.flow.to,
66
- // khotanRunId: ctx.khotanRunId,
67
- // runType: ctx.runType,
68
- // });
69
- // const shopify = bindWorkflowPlug(shopifyPlug, ctx);
70
- // const hubspot = bindWorkflowPlug(hubspotPlug, ctx, "hubspot");
71
- //
72
- // const snapshotCache = khotanCache(ctx, "shopify-products-snapshot");
73
- // const previous = await snapshotCache.get<Array<Record<string, unknown>>>("latest");
62
+ // // Step: top-level, full Node.js access, retried independently.
63
+ // async function forwardProducts(ctx: RelayContext) {
64
+ // "use step";
65
+ // console.log("Starting relay", {
66
+ // flow: ctx.flow.name,
67
+ // to: ctx.flow.to,
68
+ // khotanRunId: ctx.khotanRunId,
69
+ // runType: ctx.runType,
70
+ // });
71
+ // const shopify = bindWorkflowPlug(shopifyPlug, ctx);
72
+ // const hubspot = bindWorkflowPlug(hubspotPlug, ctx, "hubspot");
74
73
  //
75
- // const response = await shopify.get<{ data?: Array<Record<string, unknown>> }>("/products");
76
- // const records = Array.isArray(response.data) ? response.data : [];
77
- // await snapshotCache.set("latest", records);
74
+ // const snapshotCache = khotanCache(ctx, "shopify-products-snapshot");
75
+ // const previous = await snapshotCache.get<Array<Record<string, unknown>>>("latest");
78
76
  //
79
- // for (const record of records) {
80
- // await hubspot.post("/products", { body: record });
81
- // }
77
+ // const response = await shopify.get<{ data?: Array<Record<string, unknown>> }>("/products");
78
+ // const records = Array.isArray(response.data) ? response.data : [];
79
+ // await snapshotCache.set("latest", records);
82
80
  //
83
- // return {
84
- // extracted: records.length,
85
- // transformed: records.length,
86
- // created: records.length,
87
- // metadata: {
88
- // relay: ctx.flow.name,
89
- // to: ctx.flow.to,
90
- // previousCount: previous?.length ?? 0,
91
- // },
92
- // };
81
+ // for (const record of records) {
82
+ // await hubspot.post("/products", { body: record });
93
83
  // }
94
84
  //
95
- // return forwardProducts();
85
+ // return {
86
+ // extracted: records.length,
87
+ // transformed: records.length,
88
+ // created: records.length,
89
+ // metadata: {
90
+ // relay: ctx.flow.name,
91
+ // to: ctx.flow.to,
92
+ // previousCount: previous?.length ?? 0,
93
+ // },
94
+ // };
95
+ // }
96
+ //
97
+ // // Workflow: orchestration only.
98
+ // async function shopifyToHubspotWorkflow(ctx: RelayContext) {
99
+ // "use workflow";
100
+ // return forwardProducts(ctx);
96
101
  // }
97
102
  //
98
103
  // export const shopifyToHubspotRelay = relay({
@@ -20,7 +20,7 @@ The Hub scaffolds three components to `src/components/khotan/`:
20
20
 
21
21
  | File | Purpose |
22
22
  |------|---------|
23
- | `hub.tsx` | Main `<KhotanHub />` — plug cards, flow table, enable/disable toggles |
23
+ | `hub.tsx` | Main `<KhotanHub />` — plug cards, flow table, enable/disable toggles, per-flow "Run now" trigger |
24
24
  | `var-panel.tsx` | Variables panel for configuring plug vars |
25
25
  | `wire-panel.tsx` | Webhook subscription management (connect/disconnect) |
26
26
 
@@ -44,6 +44,7 @@ Or use `npx khotan add config-page-1` to scaffold a `/config` page automatically
44
44
 
45
45
  - Lists all registered plugs with status badges (connected/error/idle)
46
46
  - Click a plug to see its flows with enable/disable toggles
47
+ - "Run now" button on each flow row triggers a tracked run via `POST /api/khotan/flows/:id/runs` (uses the browser session, so it passes your `authorize` hook)
47
48
  - VarPanel: configure plug variables (stored encrypted via `KHOTAN_SECRET`)
48
49
  - WirePanel: manage webhook subscriptions (requires wires configured on plug)
49
50
  - Debug button on each plug card (visible when `KHOTAN_DEBUG=1`)
@@ -120,7 +120,11 @@ const khotanData = khotan({
120
120
 
121
121
  Notes:
122
122
  - `authorize` is **not** a replacement for `KHOTAN_SECRET` — that key only
123
- encrypts credentials at rest, it does not authenticate requests.
123
+ encrypts credentials at rest, it does not authenticate requests. Conversely,
124
+ `KHOTAN_SECRET` is **not** an HTTP credential: do not send it as a Bearer
125
+ token. Management routes are gated solely by `authorize` (plus the dev-only
126
+ CLI HMAC token). A rejected request returns `401` with `code:
127
+ authorize_rejected` and a `hint` explaining how to authenticate.
124
128
  - Inbound webhooks (`POST /webhook/:plug`, verified per-plug via `onVerify`),
125
129
  the cron dispatcher (`CRON_SECRET`), and debug routes (`KHOTAN_DEBUG`,
126
130
  non-production only) are exempt from `authorize` automatically.
@@ -139,6 +143,76 @@ const nextConfig = {
139
143
  };
140
144
  ```
141
145
 
146
+ ## Workflow Runtime & Middleware/Proxy
147
+
148
+ Inflows, outflows, relays, catch, and pass run on **Vercel Workflow**, which
149
+ communicates over `/.well-known/workflow/*`. If your app has a `middleware.ts`
150
+ (or `proxy.ts`) whose `matcher` captures these paths, durable runs **silently
151
+ fail** — steps never get invoked and runs hang.
152
+
153
+ `npx khotan init` detects a middleware/proxy file and warns when it may
154
+ intercept these paths. Exclude them from the matcher:
155
+
156
+ ```typescript
157
+ // middleware.ts
158
+ export const config = {
159
+ matcher: ["/((?!_next|.well-known/workflow).*)"],
160
+ };
161
+ ```
162
+
163
+ If you do auth or rewrites manually (not via `matcher`), short-circuit early:
164
+
165
+ ```typescript
166
+ export function middleware(request: NextRequest) {
167
+ if (request.nextUrl.pathname.startsWith("/.well-known/workflow")) {
168
+ return NextResponse.next();
169
+ }
170
+ // ...your logic
171
+ }
172
+ ```
173
+
174
+ Vercel Workflow also requires AI Gateway OIDC — run `vercel link` and
175
+ `vercel env pull` so `VERCEL_OIDC_TOKEN` is available locally.
176
+
177
+ ## Triggering Flows
178
+
179
+ Start a flow through khotan (never call the workflow function directly) so run
180
+ tracking and Workflow IDs are recorded. The API is `khotanData.flow(name).start()`:
181
+
182
+ ```typescript
183
+ import khotanData from "@/lib/khotan/khotan";
184
+
185
+ await khotanData.flow("products-inflow", { plugName: "shopify" }).start({
186
+ runType: "delta", // or "full"
187
+ });
188
+ ```
189
+
190
+ `plugName` is only needed to disambiguate when the same flow name is registered
191
+ under multiple plugs. There is no `khotanData.api.*` or `flow().run()` surface —
192
+ `flow(name).start(options)` is the single entry point for manual and scheduled
193
+ runs alike. The cron dispatcher (`/api/khotan/cron`) calls this same path.
194
+
195
+ ### Triggering over HTTP (scripts / external services)
196
+
197
+ There is **no** `POST /flows/:name/run` route. The HTTP trigger is:
198
+
199
+ ```
200
+ POST /api/khotan/flows/{flowId}/runs body: { "runType": "delta" }
201
+ ```
202
+
203
+ This is a **management route**, so it goes through your `authorize` hook. Common
204
+ gotcha: `KHOTAN_SECRET` is an encryption key, **not** an HTTP credential — sending
205
+ `Authorization: Bearer <KHOTAN_SECRET>` returns `401` with `code: authorize_rejected`.
206
+ To trigger from outside the app, authenticate with a credential your `authorize`
207
+ hook accepts (a session cookie, or your own token you validate inside `authorize`).
208
+
209
+ Prefer triggering server-side with `khotanData.flow(name).start()` whenever you
210
+ can — it needs no HTTP round-trip or auth.
211
+
212
+ The `npx khotan flows trigger <name>` CLI works in **dev** without any of this: it
213
+ signs a short-lived HMAC token from `KHOTAN_SECRET` (the `KhotanCLI` auth scheme,
214
+ disabled when `NODE_ENV=production`). The raw secret never leaves your machine.
215
+
142
216
  ## Verify Setup
143
217
 
144
218
  ```bash
@@ -194,3 +268,5 @@ This keeps sync logic grounded in real API payloads before you write pagination,
194
268
  - **"Cannot find module khotan-data"**: Add to `serverExternalPackages` in next.config.ts
195
269
  - **Migration fails**: Ensure `DATABASE_URL` is set and Postgres is reachable
196
270
  - **Init won't overwrite**: By design — delete the file manually if you need to re-scaffold
271
+ - **Flow/workflow runs hang or never start**: Check your `middleware.ts`/`proxy.ts` matcher excludes `/.well-known/workflow/*` (see "Workflow Runtime & Middleware/Proxy")
272
+ - **Step "is not a function" / fails to resolve at runtime**: Declare `"use step"` functions at module top level and pass `ctx` as an argument — never nest them inside the `"use workflow"` function (closures over workflow scope cannot be hoisted)
@@ -110,19 +110,28 @@ npx khotan add catch --yes
110
110
 
111
111
  Process webhook events durably via Vercel Workflow:
112
112
 
113
- ```typescript
114
- import { catchEvent } from "./webhooks/catch";
115
-
116
- const processInvoice = catchEvent(async (ctx) => {
117
- "use workflow";
113
+ Declare `"use step"` functions at module top level and pass `ctx` (serializable
114
+ data) as an argument. Nesting steps inside the `"use workflow"` function fails at
115
+ runtime — closures over workflow scope cannot be hoisted.
118
116
 
119
- async function persist() {
120
- "use step";
121
- // Write to database retried on failure
122
- await db.insert(invoices).values(ctx.event);
123
- }
124
-
125
- await persist();
117
+ ```typescript
118
+ import { catchEvent, type CatchContext } from "./webhooks/catch";
119
+ import { db } from "@/db";
120
+ import { invoices } from "@/db/schema";
121
+
122
+ // Step: top-level, full Node.js access, retried on failure.
123
+ async function persistInvoice(ctx: CatchContext) {
124
+ "use step";
125
+ await db.insert(invoices).values(ctx.event);
126
+ }
127
+
128
+ const processInvoice = catchEvent({
129
+ name: "stripe-invoices",
130
+ events: ["invoice.paid"],
131
+ workflow: async (ctx) => {
132
+ "use workflow";
133
+ await persistInvoice(ctx);
134
+ },
126
135
  });
127
136
  ```
128
137
 
@@ -140,22 +149,35 @@ npx khotan add pass --yes
140
149
 
141
150
  Forward webhook events to another service:
142
151
 
152
+ The context exposes `ctx.event`, `ctx.eventType`, and `ctx.destVars` (the
153
+ decrypted credentials for the destination plug). There is no `ctx.destPlug` —
154
+ construct the destination plug from `destVars` inside a top-level step.
155
+
143
156
  ```typescript
144
- import { pass } from "./webhooks/pass";
157
+ import { pass, type PassContext } from "./webhooks/pass";
158
+ import { plug } from "@/lib/khotan/plugs/plug";
159
+
160
+ // Step: top-level. Build the destination plug from ctx.destVars.
161
+ async function forwardToSlackStep(ctx: PassContext) {
162
+ "use step";
163
+ const slack = plug({
164
+ name: "slack",
165
+ baseUrl: "https://slack.com/api",
166
+ authType: "bearer",
167
+ auth: { bearer: { token: ctx.destVars["botToken"] ?? "" } },
168
+ });
169
+ await slack.post("/chat.postMessage", {
170
+ body: { channel: ctx.destVars["channelId"], text: `New event: ${ctx.eventType}` },
171
+ });
172
+ }
145
173
 
146
174
  const forwardToSlack = pass({
147
- to: "slack", // Destination plug name (must be registered)
175
+ name: "stripe-to-slack",
176
+ to: "slack", // Destination plug name (must be registered)
177
+ events: ["invoice.paid"],
148
178
  workflow: async (ctx) => {
149
179
  "use workflow";
150
- // ctx.event — the incoming webhook payload
151
- // ctx.destVars — destination plug variables
152
- async function forward() {
153
- "use step";
154
- await ctx.destPlug.post("/messages", {
155
- body: { text: `New event: ${ctx.event.type}` },
156
- });
157
- }
158
- await forward();
180
+ await forwardToSlackStep(ctx);
159
181
  },
160
182
  });
161
183
  ```
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "khotan-data",
3
- "version": "0.2.0",
4
- "description": "Data primitives for TypeScript ETL pipelines, transforms, and Drizzle Postgres integration.",
3
+ "version": "0.3.1",
4
+ "description": "Data sync, ETL, and webhook primitives for Next.js + Drizzle + Postgres. shadcn for data plumbing.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
7
7
  "module": "./dist/index.js",
@@ -107,11 +107,11 @@
107
107
  "transform",
108
108
  "data-management"
109
109
  ],
110
- "author": "",
110
+ "author": "Khotan",
111
111
  "license": "MIT",
112
112
  "repository": {
113
113
  "type": "git",
114
- "url": ""
114
+ "url": "https://github.com/khotan-io/khotan-data"
115
115
  },
116
116
  "engines": {
117
117
  "node": ">=18"