@usequeek/app-sdk 0.7.2 → 0.8.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.
package/dist/proxy.d.ts CHANGED
@@ -30,12 +30,19 @@ export declare const PROXY_NONCE_PARAM = "jti";
30
30
  export declare const PROXY_SHOP_PARAM = "shop";
31
31
  export declare const PROXY_CUSTOMER_PARAM = "logged_in_customer_id";
32
32
  export declare const PROXY_KID_PARAM = "kid";
33
+ export declare const PROXY_SURFACE_PARAM = "surface";
34
+ /**
35
+ * The Queek surface that made a call. Queek signs it on every proxy call and
36
+ * is the only one that sets it: a shopper cannot choose it. `storefront` is a
37
+ * shopper, `dashboard` the merchant dashboard, `ai` Queek AI reading a tool.
38
+ */
39
+ export type ProxySurface = "storefront" | "dashboard" | "ai";
33
40
  /** Default allowed clock skew in seconds; the floor (60s) below applies regardless. */
34
41
  export declare const PROXY_MAX_SKEW_SECONDS = 300;
35
42
  /** Query params as any framework holds them (duplicates collapse to the first value). */
36
43
  export type ProxyQuery = Record<string, string | string[] | null | undefined>;
37
44
  /** Failure reason when proxy verification rejects a query. */
38
- export type ProxyFailure = "missing_params" | "stale_timestamp" | "missing_secret" | "signature_mismatch" | "unknown_installation" | "replayed";
45
+ export type ProxyFailure = "missing_params" | "stale_timestamp" | "missing_secret" | "signature_mismatch" | "unknown_installation" | "surface_mismatch" | "replayed";
39
46
  /**
40
47
  * The canonical string the MAC covers, exactly as Queek builds it (sig
41
48
  * excluded, keys byte-sorted, percent-encoded pairs).
@@ -48,6 +55,13 @@ export declare function buildProxyCanonicalString(path: string, params: Record<s
48
55
  */
49
56
  export declare function signProxyQuery(path: string, params: Record<string, string>, secret: string): string;
50
57
  export interface ProxyVerifyOptions {
58
+ /**
59
+ * Require this signed `surface`. A call signed for another surface (or for
60
+ * none) fails with `surface_mismatch`, after its signature verified. Set it
61
+ * on a handler that serves merchant data (`dashboard`) so a shopper's call
62
+ * cannot reach it.
63
+ */
64
+ expectedSurface?: ProxySurface;
51
65
  /** Unix seconds to judge freshness against (defaults to now). Exposed for tests. */
52
66
  nowSeconds?: number;
53
67
  /** Allowed clock skew in seconds (defaults to PROXY_MAX_SKEW_SECONDS; floored at 60). */
@@ -61,6 +75,7 @@ export interface ProxyVerifyOptions {
61
75
  */
62
76
  export declare function verifyProxyQueryDetailed(path: string, query: ProxyQuery, secrets: string[], options?: ProxyVerifyOptions): {
63
77
  ok: true;
78
+ surface: string;
64
79
  } | {
65
80
  ok: false;
66
81
  reason: ProxyFailure;
@@ -79,10 +94,11 @@ export type ProxyVerification = {
79
94
  ok: true;
80
95
  installation: InstallationRecord;
81
96
  params: Record<string, string>;
97
+ surface: string;
82
98
  } | {
83
99
  ok: false;
84
100
  reason: ProxyFailure;
85
- status: 401;
101
+ status: 401 | 403;
86
102
  };
87
103
  /**
88
104
  * Layer 1: verify one signed proxy query from plain data — no `Request`, no
@@ -109,6 +125,7 @@ export interface ProxyRequestOptions extends ProxyStoreOptions {
109
125
  export type ProxyResponder = (verified: {
110
126
  installation: InstallationRecord;
111
127
  params: Record<string, string>;
128
+ surface: string;
112
129
  }) => Response | Promise<Response>;
113
130
  /**
114
131
  * Layer 2: the Web-standard wrapper, built ONLY on layer 1 — reads the query
@@ -117,3 +134,46 @@ export type ProxyResponder = (verified: {
117
134
  * anything else answers 405.
118
135
  */
119
136
  export declare function handleProxyRequest(request: Request, options: ProxyRequestOptions, onVerified: ProxyResponder): Promise<Response>;
137
+ /** A verified call to a declared AI read tool: Queek's signed params, the tool key and the actor's ROLE (never a user id). */
138
+ export interface AiToolCall {
139
+ /** The installation p_id the call is for (the signed `kid`). */
140
+ kid: string;
141
+ shop: string;
142
+ /** Everything Queek signed: the tool's inputs as strings, `kid`, `shop`, `ts`, `actor_role` and the rest. */
143
+ params: Record<string, string>;
144
+ tool: string;
145
+ actorRole: string;
146
+ }
147
+ /**
148
+ * What a tool answers: an OBJECT matching its manifest `ai.tools[].output`, never a bare array. A list tool
149
+ * (`output.list: true`) answers `{ rows: [{ ...declared fields }] }`; any other tool answers the declared fields
150
+ * object. Queek drops every undeclared field. (`length?: never` is what keeps an array out of the type.)
151
+ */
152
+ export type AiToolOutput = {
153
+ [field: string]: unknown;
154
+ length?: never;
155
+ };
156
+ export type AiToolHandler = (call: AiToolCall) => Promise<AiToolOutput> | AiToolOutput;
157
+ export interface AiToolRequestOptions extends Omit<ProxyVerifyOptions, "expectedSurface"> {
158
+ /** The manifest `extensions.proxy.subpath`; the signed path is `/apps/<subpath>/ai/<tool>`. */
159
+ subpath: string;
160
+ /** The active proxy secrets of installation `kid`, current first (empty when unknown). */
161
+ secrets: (kid: string) => Promise<string[]> | string[];
162
+ /** The read tools the manifest `ai.tools` declares, by key. Only these are served. */
163
+ tools: Record<string, AiToolHandler>;
164
+ /**
165
+ * Makes each call single-use: when given, a verified call's `jti` is claimed atomically in this store, exactly as
166
+ * `verifyProxyDelivery` does, and a replay answers 401 `replayed`. Pass the installation store. Without it a
167
+ * captured call can be replayed inside the skew window.
168
+ */
169
+ replayStore?: Pick<InstallationStore, "claimWebhookId">;
170
+ }
171
+ /**
172
+ * Layer 2 for Queek AI read tools (manifest `ai.tools`): `GET ai/{tool}` signed with `surface=ai`. The signature and
173
+ * the surface are verified first (401, or 403 for a genuine call signed for another surface). `surface=ai` is served
174
+ * only by a declared tool handler (403 for an undeclared key), a storefront or dashboard call at this path is
175
+ * refused, and an AI call never reaches `handleProxyRequest`'s responder. A handler that answers anything but an
176
+ * object (a bare array, a scalar) is answered 500. Pass `replayStore` to make each call single-use; a tool only
177
+ * reads, so without it a replay inside the skew window repeats nothing but the read.
178
+ */
179
+ export declare function handleAiToolRequest(request: Request, tool: string, options: AiToolRequestOptions): Promise<Response>;
package/dist/proxy.js CHANGED
@@ -32,6 +32,7 @@ export const PROXY_NONCE_PARAM = "jti";
32
32
  export const PROXY_SHOP_PARAM = "shop";
33
33
  export const PROXY_CUSTOMER_PARAM = "logged_in_customer_id";
34
34
  export const PROXY_KID_PARAM = "kid";
35
+ export const PROXY_SURFACE_PARAM = "surface";
35
36
  /** Default allowed clock skew in seconds; the floor (60s) below applies regardless. */
36
37
  export const PROXY_MAX_SKEW_SECONDS = 300;
37
38
  /** RFC 3986 percent-encoding: everything except `[A-Za-z0-9-_.~]` is `%XX` (upper-case hex), the encoding Queek signs with. */
@@ -89,6 +90,13 @@ function proxySkewExceeded(ts, options) {
89
90
  const maxSkew = Math.max(60, options.maxSkewSeconds ?? PROXY_MAX_SKEW_SECONDS);
90
91
  return Math.abs(now - Number(ts)) > maxSkew;
91
92
  }
93
+ /** The signed surface, or `""` when the call carries none. Trust it only after the MAC verified. */
94
+ function signedSurface(params) {
95
+ return params[PROXY_SURFACE_PARAM] ?? "";
96
+ }
97
+ function surfaceAllowed(params, options) {
98
+ return options.expectedSurface === undefined || signedSurface(params) === options.expectedSurface;
99
+ }
92
100
  function proxyMacMatches(path, params, sig, secret) {
93
101
  if (secret === "")
94
102
  return false;
@@ -117,7 +125,9 @@ export function verifyProxyQueryDetailed(path, query, secrets, options = {}) {
117
125
  }
118
126
  for (const secret of secrets) {
119
127
  if (proxyMacMatches(path, params, sig, secret)) {
120
- return { ok: true };
128
+ return surfaceAllowed(params, options)
129
+ ? { ok: true, surface: signedSurface(params) }
130
+ : { ok: false, reason: "surface_mismatch" };
121
131
  }
122
132
  }
123
133
  return { ok: false, reason: "signature_mismatch" };
@@ -136,7 +146,11 @@ export function verifyProxyQuery(path, query, secrets, options = {}) {
136
146
  * read cannot be replayed.
137
147
  */
138
148
  export async function verifyProxyDelivery(input, options) {
139
- const fail = (reason) => ({ ok: false, reason, status: 401 });
149
+ const fail = (reason, status = 401) => ({
150
+ ok: false,
151
+ reason,
152
+ status,
153
+ });
140
154
  const params = normalizeQuery(input.query);
141
155
  const { sig, ts, jti } = readProxyFields(params);
142
156
  if (sig === "" || ts === "" || jti === "" || !/^\d+$/.test(ts)) {
@@ -171,12 +185,25 @@ export async function verifyProxyDelivery(input, options) {
171
185
  if (!installation) {
172
186
  return fail("signature_mismatch");
173
187
  }
188
+ // A genuine signature for another surface is forbidden, not unauthenticated.
189
+ if (!surfaceAllowed(params, options)) {
190
+ return fail("surface_mismatch", 403);
191
+ }
174
192
  if (options.enforceReplay ?? true) {
175
193
  if (!(await options.store.claimWebhookId(`proxy-jti:${jti}`))) {
176
194
  return fail("replayed");
177
195
  }
178
196
  }
179
- return { ok: true, installation, params };
197
+ return { ok: true, installation, params, surface: signedSurface(params) };
198
+ }
199
+ /** The query of a Web `Request`, duplicates collapsing to the first value like `URLSearchParams.get`. */
200
+ function requestQuery(request) {
201
+ const url = new URL(request.url);
202
+ const query = {};
203
+ for (const key of url.searchParams.keys()) {
204
+ query[key] = url.searchParams.get(key);
205
+ }
206
+ return query;
180
207
  }
181
208
  /**
182
209
  * Layer 2: the Web-standard wrapper, built ONLY on layer 1 — reads the query
@@ -188,15 +215,68 @@ export async function handleProxyRequest(request, options, onVerified) {
188
215
  if (request.method.toUpperCase() !== "GET") {
189
216
  return toResponse({ status: 405, body: { ok: false, error: "method_not_allowed" } });
190
217
  }
191
- const url = new URL(request.url);
192
- const query = {};
193
- for (const key of url.searchParams.keys()) {
194
- query[key] = url.searchParams.get(key);
195
- }
218
+ const query = requestQuery(request);
196
219
  const verified = await verifyProxyDelivery({ path: options.path, query }, options);
197
220
  if (!verified.ok) {
198
221
  const result = { status: verified.status, body: { ok: false, error: verified.reason } };
199
222
  return toResponse(result);
200
223
  }
201
- return onVerified({ installation: verified.installation, params: verified.params });
224
+ // A responder serves storefront and dashboard calls. An AI call is served only by a declared tool handler.
225
+ if (verified.surface === "ai") {
226
+ return toResponse({ status: 403, body: { ok: false, error: "forbidden" } });
227
+ }
228
+ return onVerified({
229
+ installation: verified.installation,
230
+ params: verified.params,
231
+ surface: verified.surface,
232
+ });
233
+ }
234
+ /**
235
+ * Layer 2 for Queek AI read tools (manifest `ai.tools`): `GET ai/{tool}` signed with `surface=ai`. The signature and
236
+ * the surface are verified first (401, or 403 for a genuine call signed for another surface). `surface=ai` is served
237
+ * only by a declared tool handler (403 for an undeclared key), a storefront or dashboard call at this path is
238
+ * refused, and an AI call never reaches `handleProxyRequest`'s responder. A handler that answers anything but an
239
+ * object (a bare array, a scalar) is answered 500. Pass `replayStore` to make each call single-use; a tool only
240
+ * reads, so without it a replay inside the skew window repeats nothing but the read.
241
+ */
242
+ export async function handleAiToolRequest(request, tool, options) {
243
+ if (request.method.toUpperCase() !== "GET") {
244
+ return toResponse({ status: 405, body: { ok: false, error: "method_not_allowed" } });
245
+ }
246
+ const { subpath, secrets, tools, replayStore, ...verifyOptions } = options;
247
+ const query = requestQuery(request);
248
+ const params = normalizeQuery(query);
249
+ const kid = params[PROXY_KID_PARAM] ?? "";
250
+ const verified = verifyProxyQueryDetailed(`/apps/${subpath}/ai/${tool}`, query, await secrets(kid), {
251
+ ...verifyOptions,
252
+ expectedSurface: "ai",
253
+ });
254
+ if (!verified.ok) {
255
+ return toResponse({
256
+ status: verified.reason === "surface_mismatch" ? 403 : 401,
257
+ body: { ok: false, error: verified.reason },
258
+ });
259
+ }
260
+ if (replayStore && !(await replayStore.claimWebhookId(`proxy-jti:${params[PROXY_NONCE_PARAM]}`))) {
261
+ return toResponse({ status: 401, body: { ok: false, error: "replayed" } });
262
+ }
263
+ const handler = Object.hasOwn(tools, tool) ? tools[tool] : undefined;
264
+ if (!handler) {
265
+ return toResponse({ status: 403, body: { ok: false, error: "forbidden" } });
266
+ }
267
+ const output = await handler({
268
+ kid,
269
+ shop: params[PROXY_SHOP_PARAM] ?? "",
270
+ params,
271
+ tool,
272
+ actorRole: params.actor_role ?? "",
273
+ });
274
+ if (output === null || typeof output !== "object" || Array.isArray(output)) {
275
+ return toResponse({ status: 500, body: { ok: false, error: "invalid_tool_output" } });
276
+ }
277
+ return toResponse({
278
+ status: 200,
279
+ body: output,
280
+ headers: { "Cache-Control": "private, no-store, max-age=0" },
281
+ });
202
282
  }
@@ -1,4 +1,4 @@
1
- import { type CoreDelivery, type DeliveryErrorHandler, type DeliveryResult } from "./delivery.js";
1
+ import { type DeliveryErrorHandler, type DeliveryResult, type WebhookDelivery } from "./delivery.js";
2
2
  import type { InstallationRecord, InstallationStore } from "./store.js";
3
3
  /**
4
4
  * Framework-agnostic receiver for Queek topic deliveries (one signed POST
@@ -79,6 +79,11 @@ export interface WebhookHandlerOptions {
79
79
  * 200 for a handled, repeated or unhandled-topic delivery; 500 when your
80
80
  * handler throws (the claim is released so Queek's retry can land).
81
81
  *
82
+ * `method` `GET` or `HEAD` is the review probe: 200 `{ ok: true }` with
83
+ * `Cache-Control: no-store` and no handler; with a signature (over the
84
+ * empty body) 200 when it verifies against an installation secret and 401
85
+ * when not.
86
+ *
82
87
  * @example
83
88
  * const result = await handleWebhookDelivery(
84
89
  * { rawBody, headers },
@@ -93,10 +98,11 @@ export interface WebhookHandlerOptions {
93
98
  * );
94
99
  * // reply with result.status and result.body
95
100
  */
96
- export declare function handleWebhookDelivery(input: CoreDelivery, options: WebhookHandlerOptions): Promise<DeliveryResult>;
101
+ export declare function handleWebhookDelivery(input: WebhookDelivery, options: WebhookHandlerOptions): Promise<DeliveryResult>;
97
102
  /**
98
103
  * Layer 2: the Web-standard wrapper, built ONLY on layer 1 — reads
99
104
  * `await request.arrayBuffer()` + headers, calls the core, builds the
100
- * `Response`. Only `POST` is served; anything else answers 405.
105
+ * `Response`. `POST` is a delivery; `GET` and `HEAD` are the review probe
106
+ * (see `handleWebhookDelivery`); anything else answers 405.
101
107
  */
102
108
  export declare function handleWebhookRequest(request: Request, options: WebhookHandlerOptions): Promise<Response>;
package/dist/webhooks.js CHANGED
@@ -1,4 +1,4 @@
1
- import { decodeBody, readHeader, reportDeliveryFailure, toResponse, } from "./delivery.js";
1
+ import { decodeBody, isProbeMethod, isUnsignedProbe, probeResult, readHeader, reportDeliveryFailure, toResponse, } from "./delivery.js";
2
2
  import { isFreshTimestamp, MAX_TIMESTAMP_SKEW_SECONDS, verifyQueekSignature, WEBHOOK_ID_HEADER, WEBHOOK_SIGNATURE_HEADER, WEBHOOK_TIMESTAMP_HEADER, } from "./signatures.js";
3
3
  /**
4
4
  * Framework-agnostic receiver for Queek topic deliveries (one signed POST
@@ -57,6 +57,50 @@ async function defaultResolveSecret(store, rawBody, headers, envelope) {
57
57
  }
58
58
  return null;
59
59
  }
60
+ /**
61
+ * Resolve the endpoint secret that signed a delivery and verify freshness
62
+ * and the MAC against it. The single verification path of a delivery AND of
63
+ * a signed review probe (empty body, no envelope).
64
+ */
65
+ async function verifyAgainstResolvedSecret(options, rawBody, headers, envelope) {
66
+ const resolution = options.resolveSecret
67
+ ? await options.resolveSecret(envelope, rawBody, headers)
68
+ : await defaultResolveSecret(options.store, rawBody, headers, envelope);
69
+ if (!resolution) {
70
+ return { ok: false, error: "unknown_installation" };
71
+ }
72
+ // Freshness is enforced HERE, once, against the resolved secret — the
73
+ // lookup above deliberately skips it so a stale delivery cannot be
74
+ // misattributed before it is rejected.
75
+ const now = options.nowSeconds ?? Math.floor(Date.now() / 1000);
76
+ if (!isFreshTimestamp(headers.timestamp, now, options.maxSkewSeconds ?? MAX_TIMESTAMP_SKEW_SECONDS)) {
77
+ return { ok: false, error: "stale timestamp" };
78
+ }
79
+ // Freshness was enforced above; this re-checks the MAC only, so a
80
+ // custom resolver cannot claim an installation without its secret.
81
+ if (!verifyQueekSignature({ ...headers, body: rawBody, secret: resolution.secret }, { skipFreshnessCheck: true })) {
82
+ return { ok: false, error: "signature mismatch" };
83
+ }
84
+ return { ok: true, resolution };
85
+ }
86
+ /**
87
+ * The review probe (`GET` / `HEAD`): 200 without running a handler or
88
+ * touching the store. A probe that carries a signature is verified like a
89
+ * delivery, over the empty body and against the stored endpoint secrets:
90
+ * 200 when it verifies, 401 when not. A valid probe claims nothing.
91
+ */
92
+ async function probeDelivery(input, options) {
93
+ if (isUnsignedProbe(input.headers))
94
+ return probeResult(200);
95
+ const id = readHeader(input.headers, WEBHOOK_ID_HEADER);
96
+ const timestamp = readHeader(input.headers, WEBHOOK_TIMESTAMP_HEADER);
97
+ const signatureHeader = readHeader(input.headers, WEBHOOK_SIGNATURE_HEADER);
98
+ if (!id || !timestamp || !signatureHeader) {
99
+ return probeResult(401, "missing signature headers");
100
+ }
101
+ const verified = await verifyAgainstResolvedSecret(options, "", { id, timestamp, signatureHeader }, null);
102
+ return verified.ok ? probeResult(200) : probeResult(401, verified.error);
103
+ }
60
104
  /**
61
105
  * Layer 1: handle one signed Queek topic delivery from plain data — no
62
106
  * `Request`, no framework. Same options, same behaviour, same
@@ -67,6 +111,11 @@ async function defaultResolveSecret(store, rawBody, headers, envelope) {
67
111
  * 200 for a handled, repeated or unhandled-topic delivery; 500 when your
68
112
  * handler throws (the claim is released so Queek's retry can land).
69
113
  *
114
+ * `method` `GET` or `HEAD` is the review probe: 200 `{ ok: true }` with
115
+ * `Cache-Control: no-store` and no handler; with a signature (over the
116
+ * empty body) 200 when it verifies against an installation secret and 401
117
+ * when not.
118
+ *
70
119
  * @example
71
120
  * const result = await handleWebhookDelivery(
72
121
  * { rawBody, headers },
@@ -82,7 +131,8 @@ async function defaultResolveSecret(store, rawBody, headers, envelope) {
82
131
  * // reply with result.status and result.body
83
132
  */
84
133
  export async function handleWebhookDelivery(input, options) {
85
- const maxSkew = options.maxSkewSeconds ?? MAX_TIMESTAMP_SKEW_SECONDS;
134
+ if (isProbeMethod(input.method))
135
+ return probeDelivery(input, options);
86
136
  const rawBody = decodeBody(input.rawBody);
87
137
  const id = readHeader(input.headers, WEBHOOK_ID_HEADER);
88
138
  const timestamp = readHeader(input.headers, WEBHOOK_TIMESTAMP_HEADER);
@@ -103,24 +153,10 @@ export async function handleWebhookDelivery(input, options) {
103
153
  return { status: 400, body: { ok: false, error: "invalid_envelope" } };
104
154
  }
105
155
  const headers = { id, timestamp, signatureHeader };
106
- const resolution = options.resolveSecret
107
- ? await options.resolveSecret(envelope, rawBody, headers)
108
- : await defaultResolveSecret(options.store, rawBody, headers, envelope);
109
- if (!resolution) {
110
- return { status: 401, body: { ok: false, error: "unknown_installation" } };
111
- }
112
- // Freshness is enforced HERE, once, against the resolved secret — the
113
- // lookup above deliberately skips it so a stale delivery cannot be
114
- // misattributed before it is rejected.
115
- const now = options.nowSeconds ?? Math.floor(Date.now() / 1000);
116
- if (!isFreshTimestamp(timestamp, now, maxSkew)) {
117
- return { status: 401, body: { ok: false, error: "stale timestamp" } };
118
- }
119
- // Freshness was enforced above; this re-checks the MAC only, so a
120
- // custom resolver cannot claim an installation without its secret.
121
- if (!verifyQueekSignature({ ...headers, body: rawBody, secret: resolution.secret }, { skipFreshnessCheck: true })) {
122
- return { status: 401, body: { ok: false, error: "signature mismatch" } };
123
- }
156
+ const verified = await verifyAgainstResolvedSecret(options, rawBody, headers, envelope);
157
+ if (!verified.ok)
158
+ return { status: 401, body: { ok: false, error: verified.error } };
159
+ const resolution = verified.resolution;
124
160
  const installation = await options.store.getInstallation(resolution.installationId);
125
161
  if (!installation) {
126
162
  return { status: 401, body: { ok: false, error: "unknown_installation" } };
@@ -151,14 +187,17 @@ export async function handleWebhookDelivery(input, options) {
151
187
  /**
152
188
  * Layer 2: the Web-standard wrapper, built ONLY on layer 1 — reads
153
189
  * `await request.arrayBuffer()` + headers, calls the core, builds the
154
- * `Response`. Only `POST` is served; anything else answers 405.
190
+ * `Response`. `POST` is a delivery; `GET` and `HEAD` are the review probe
191
+ * (see `handleWebhookDelivery`); anything else answers 405.
155
192
  */
156
193
  export async function handleWebhookRequest(request, options) {
157
- if (request.method.toUpperCase() !== "POST") {
194
+ const method = request.method.toUpperCase();
195
+ if (method !== "POST" && !isProbeMethod(method)) {
158
196
  return toResponse({ status: 405, body: { ok: false, error: "method_not_allowed" } });
159
197
  }
160
198
  return toResponse(await handleWebhookDelivery({
161
199
  rawBody: new Uint8Array(await request.arrayBuffer()),
162
200
  headers: request.headers,
163
- }, options));
201
+ method,
202
+ }, options), method);
164
203
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@usequeek/app-sdk",
3
- "version": "0.7.2",
3
+ "version": "0.8.1",
4
4
  "private": false,
5
5
  "license": "MIT",
6
6
  "author": "Queekx Technology LTD",