@kanca-app/ikas 0.1.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -1
- package/README.md +148 -9
- package/dist/browser.cjs +1 -2
- package/dist/browser.cjs.map +1 -1
- package/dist/browser.js +1 -2
- package/dist/browser.js.map +1 -1
- package/dist/index.cjs +217 -16
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +43 -9
- package/dist/index.d.ts +43 -9
- package/dist/index.js +217 -16
- package/dist/index.js.map +1 -1
- package/dist/react.cjs +1 -2
- package/dist/react.cjs.map +1 -1
- package/dist/react.js +1 -2
- package/dist/react.js.map +1 -1
- package/dist/storefront.cjs +322 -0
- package/dist/storefront.cjs.map +1 -0
- package/dist/storefront.d.cts +88 -0
- package/dist/storefront.d.ts +88 -0
- package/dist/storefront.js +299 -0
- package/dist/storefront.js.map +1 -0
- package/package.json +19 -3
package/dist/index.cjs
CHANGED
|
@@ -22,7 +22,6 @@ var src_exports = {};
|
|
|
22
22
|
__export(src_exports, {
|
|
23
23
|
SDK_NAME: () => SDK_NAME,
|
|
24
24
|
SDK_VERSION: () => SDK_VERSION,
|
|
25
|
-
UNINSTALL_SCOPES: () => UNINSTALL_SCOPES,
|
|
26
25
|
createKanca: () => createKanca,
|
|
27
26
|
operationName: () => operationName
|
|
28
27
|
});
|
|
@@ -175,7 +174,7 @@ function classifyGraphql(status, json) {
|
|
|
175
174
|
|
|
176
175
|
// src/shared.ts
|
|
177
176
|
var SDK_NAME = "@kanca-app/ikas";
|
|
178
|
-
var SDK_VERSION = "0.
|
|
177
|
+
var SDK_VERSION = "0.3.0";
|
|
179
178
|
var DEFAULT_ENDPOINT = "https://kanca.onurhan.dev";
|
|
180
179
|
var MAX_NAME = 200;
|
|
181
180
|
var MAX_ERROR_MESSAGE = 500;
|
|
@@ -260,7 +259,7 @@ function errorMessage(err) {
|
|
|
260
259
|
function joinUrl(endpoint, path) {
|
|
261
260
|
return endpoint.replace(/\/+$/, "") + path;
|
|
262
261
|
}
|
|
263
|
-
var encoder = typeof TextEncoder !== "undefined" ? new TextEncoder() : void 0;
|
|
262
|
+
var encoder = typeof TextEncoder !== "undefined" ? /* @__PURE__ */ new TextEncoder() : void 0;
|
|
264
263
|
function byteLength(s) {
|
|
265
264
|
return encoder ? encoder.encode(s).length : s.length * 3;
|
|
266
265
|
}
|
|
@@ -408,6 +407,126 @@ var Transport = class {
|
|
|
408
407
|
}
|
|
409
408
|
};
|
|
410
409
|
|
|
410
|
+
// src/install.ts
|
|
411
|
+
var INSTALL_RECHECK_DELAY_MS = 3e3;
|
|
412
|
+
var MASS_REVOCATION_MIN = 5;
|
|
413
|
+
var PROBE = "query getAuthorizedApp { getAuthorizedApp { id } }";
|
|
414
|
+
async function probe(client) {
|
|
415
|
+
const c = client;
|
|
416
|
+
if (!c || typeof c !== "object" || !c._client || typeof c._client.request !== "function") {
|
|
417
|
+
return { state: "unknown", reason: "UNSUPPORTED_CLIENT" };
|
|
418
|
+
}
|
|
419
|
+
if (typeof c._checkToken === "function") {
|
|
420
|
+
try {
|
|
421
|
+
await c._checkToken();
|
|
422
|
+
} catch {
|
|
423
|
+
return { state: "unknown", reason: "TOKEN_CHECK_FAILED" };
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
let data;
|
|
427
|
+
try {
|
|
428
|
+
data = await c._client.request({ document: PROBE });
|
|
429
|
+
} catch (err) {
|
|
430
|
+
const resp = err?.response;
|
|
431
|
+
const status = resp?.status;
|
|
432
|
+
if (typeof status !== "number") return { state: "unknown", reason: "NETWORK" };
|
|
433
|
+
const first = Array.isArray(resp.errors) ? resp.errors[0] : void 0;
|
|
434
|
+
const code = first?.extensions?.code;
|
|
435
|
+
return { state: "unknown", reason: typeof code === "string" && code ? code.slice(0, 100) : `HTTP_${status}` };
|
|
436
|
+
}
|
|
437
|
+
if (data && typeof data === "object" && "getAuthorizedApp" in data) {
|
|
438
|
+
const app = data.getAuthorizedApp;
|
|
439
|
+
if (app === null) return { state: "removed" };
|
|
440
|
+
const id = app && typeof app === "object" ? app.id : void 0;
|
|
441
|
+
if (typeof id === "string" && id) return { state: "installed" };
|
|
442
|
+
}
|
|
443
|
+
return { state: "unknown", reason: "UNEXPECTED_RESPONSE" };
|
|
444
|
+
}
|
|
445
|
+
var sleep2 = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
446
|
+
async function checkInstallState(merchantId, client, recheckDelayMs) {
|
|
447
|
+
if (typeof merchantId !== "string" || !merchantId) return { merchantId: "", state: "unknown", reason: "MISSING_MERCHANT_ID" };
|
|
448
|
+
try {
|
|
449
|
+
const first = await probe(client);
|
|
450
|
+
if (first.state === "installed") return { merchantId, state: "installed" };
|
|
451
|
+
if (first.state === "unknown") return { merchantId, state: "unknown", reason: first.reason };
|
|
452
|
+
await sleep2(recheckDelayMs);
|
|
453
|
+
const second = await probe(client);
|
|
454
|
+
if (second.state === "removed") return { merchantId, state: "uninstalled" };
|
|
455
|
+
if (second.state === "installed") return { merchantId, state: "installed" };
|
|
456
|
+
return { merchantId, state: "unknown", reason: second.reason };
|
|
457
|
+
} catch {
|
|
458
|
+
return { merchantId, state: "unknown", reason: "UNEXPECTED_RESPONSE" };
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
function isMassRevocation(uninstalled, checked) {
|
|
462
|
+
return uninstalled >= MASS_REVOCATION_MIN && uninstalled * 2 > checked;
|
|
463
|
+
}
|
|
464
|
+
async function checkInstallStates(stores, concurrency, recheckDelayMs) {
|
|
465
|
+
const list = Array.isArray(stores) ? stores : [];
|
|
466
|
+
const results = new Array(list.length);
|
|
467
|
+
let next = 0;
|
|
468
|
+
const worker = async () => {
|
|
469
|
+
while (next < list.length) {
|
|
470
|
+
const i = next++;
|
|
471
|
+
const store = list[i];
|
|
472
|
+
results[i] = await checkInstallState(store?.merchantId, store?.client, recheckDelayMs);
|
|
473
|
+
}
|
|
474
|
+
};
|
|
475
|
+
await Promise.all(Array.from({ length: Math.min(concurrency, list.length) }, worker));
|
|
476
|
+
const uninstalled = results.filter((r) => r.state === "uninstalled").length;
|
|
477
|
+
if (!isMassRevocation(uninstalled, results.length)) return { results, massRevocation: false };
|
|
478
|
+
return {
|
|
479
|
+
massRevocation: true,
|
|
480
|
+
results: results.map((r) => r.state === "uninstalled" ? { merchantId: r.merchantId, state: "unknown", reason: "MASS_REVOCATION" } : r)
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// src/oauth.ts
|
|
485
|
+
var OAUTH_REFRESH = "oauth:refresh";
|
|
486
|
+
var OAUTH_TOKEN = "oauth:token";
|
|
487
|
+
var STORE_SUFFIXES = [".myikas.com", ".myikas.dev"];
|
|
488
|
+
var OAUTH_ERROR = /^[a-z0-9_]{1,64}$/;
|
|
489
|
+
function ikasTokenUrl(url) {
|
|
490
|
+
const host = url.hostname.toLowerCase();
|
|
491
|
+
const suffix = STORE_SUFFIXES.find((s) => host.endsWith(s));
|
|
492
|
+
if (!suffix) return void 0;
|
|
493
|
+
if (url.pathname.replace(/\/+$/, "") !== "/api/admin/oauth/token") return void 0;
|
|
494
|
+
const store = host.slice(0, -suffix.length);
|
|
495
|
+
if (!store || store.includes(".")) return void 0;
|
|
496
|
+
return store === "api" ? {} : { storeName: store };
|
|
497
|
+
}
|
|
498
|
+
function oauthNameFromBody(body) {
|
|
499
|
+
try {
|
|
500
|
+
let params;
|
|
501
|
+
if (body instanceof URLSearchParams) params = body;
|
|
502
|
+
else if (typeof body === "string") params = new URLSearchParams(body);
|
|
503
|
+
return params?.get("grant_type") === "refresh_token" ? OAUTH_REFRESH : OAUTH_TOKEN;
|
|
504
|
+
} catch {
|
|
505
|
+
return OAUTH_TOKEN;
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
function oauthErrorCode(data, status) {
|
|
509
|
+
const code = data && typeof data === "object" ? data.error : void 0;
|
|
510
|
+
return typeof code === "string" && OAUTH_ERROR.test(code) ? code : `HTTP_${status}`;
|
|
511
|
+
}
|
|
512
|
+
function fromStatus(status, data) {
|
|
513
|
+
const ok = status < 400;
|
|
514
|
+
return ok ? { status, ok } : { status, ok, error: { code: oauthErrorCode(data, status) } };
|
|
515
|
+
}
|
|
516
|
+
function validStatus(v) {
|
|
517
|
+
return typeof v === "number" && Number.isInteger(v) && v >= 100 && v <= 599;
|
|
518
|
+
}
|
|
519
|
+
function refreshResultOutcome(result) {
|
|
520
|
+
const r = result;
|
|
521
|
+
if (r && typeof r === "object" && validStatus(r.status)) return fromStatus(r.status, r.data);
|
|
522
|
+
return {};
|
|
523
|
+
}
|
|
524
|
+
function refreshErrorOutcome(err) {
|
|
525
|
+
const resp = err?.response;
|
|
526
|
+
if (resp && typeof resp === "object" && validStatus(resp.status)) return fromStatus(resp.status, resp.data);
|
|
527
|
+
return { ok: false, error: { code: "NETWORK" } };
|
|
528
|
+
}
|
|
529
|
+
|
|
411
530
|
// src/index.ts
|
|
412
531
|
var exitFlushers = /* @__PURE__ */ new Set();
|
|
413
532
|
var exitHookInstalled = false;
|
|
@@ -425,11 +544,6 @@ function registerExitFlush(fn) {
|
|
|
425
544
|
} catch {
|
|
426
545
|
}
|
|
427
546
|
}
|
|
428
|
-
var UNINSTALL_SCOPES = [
|
|
429
|
-
"store/app/deleted",
|
|
430
|
-
"store/app/uninstalled",
|
|
431
|
-
"store/authorizedApp/deleted"
|
|
432
|
-
];
|
|
433
547
|
var MARK = /* @__PURE__ */ Symbol.for("kanca.instrumented");
|
|
434
548
|
var now = () => typeof performance !== "undefined" && typeof performance.now === "function" ? performance.now() : Date.now();
|
|
435
549
|
function clampInt(v, def, min, max) {
|
|
@@ -471,12 +585,18 @@ function parseWebhookMeta(text) {
|
|
|
471
585
|
return null;
|
|
472
586
|
}
|
|
473
587
|
}
|
|
474
|
-
function
|
|
588
|
+
function checkInstallsOptions(options) {
|
|
589
|
+
return clampInt(options?.concurrency, 3, 1, 10);
|
|
590
|
+
}
|
|
591
|
+
function noopKanca(recheckDelayMs) {
|
|
475
592
|
return {
|
|
476
593
|
webhook: (handler) => async (req, ...rest) => handler(req, ...rest),
|
|
477
594
|
fetch: ((input, init) => globalThis.fetch(input, init)),
|
|
478
595
|
instrument: (client) => client,
|
|
479
596
|
track: () => void 0,
|
|
597
|
+
checkInstall: (client, fields) => checkInstallState(fields?.merchantId, client, recheckDelayMs),
|
|
598
|
+
checkInstalls: (stores, options) => checkInstallStates(stores, checkInstallsOptions(options), recheckDelayMs),
|
|
599
|
+
trackRefresh: async (call) => await call(),
|
|
480
600
|
flush: () => Promise.resolve(),
|
|
481
601
|
enabled: false
|
|
482
602
|
};
|
|
@@ -487,20 +607,21 @@ function createKanca(options = {}) {
|
|
|
487
607
|
if (debug) console.debug(`[kanca] ${msg}`, ...args);
|
|
488
608
|
};
|
|
489
609
|
const warn = (msg) => console.warn(msg);
|
|
610
|
+
const recheckDelayMs = clampInt(options.installRecheckDelayMs, INSTALL_RECHECK_DELAY_MS, 0, 6e4);
|
|
490
611
|
if (options.disabled) {
|
|
491
612
|
log("disabled; SDK is a no-op");
|
|
492
|
-
return noopKanca();
|
|
613
|
+
return noopKanca(recheckDelayMs);
|
|
493
614
|
}
|
|
494
615
|
const key = typeof options.key === "string" ? options.key.trim() : "";
|
|
495
616
|
if (!key) {
|
|
496
617
|
log("no key (KANCA_KEY) configured; SDK is a no-op");
|
|
497
|
-
return noopKanca();
|
|
618
|
+
return noopKanca(recheckDelayMs);
|
|
498
619
|
}
|
|
499
620
|
if (!key.startsWith("kk_")) {
|
|
500
621
|
warn(
|
|
501
622
|
key.startsWith("kp_") ? "[kanca] createKanca() needs a server key (kk_...), got a public key (kp_...). Public keys are for reportVitals()/<KancaVitals/> only. SDK disabled." : "[kanca] KANCA_KEY must start with kk_. SDK disabled."
|
|
502
623
|
);
|
|
503
|
-
return noopKanca();
|
|
624
|
+
return noopKanca(recheckDelayMs);
|
|
504
625
|
}
|
|
505
626
|
const maxBatchSize = clampInt(options.maxBatchSize, 100, 1, MAX_EVENTS_PER_BATCH);
|
|
506
627
|
const transport = new Transport({
|
|
@@ -587,7 +708,6 @@ function createKanca(options = {}) {
|
|
|
587
708
|
durationMs,
|
|
588
709
|
error
|
|
589
710
|
});
|
|
590
|
-
if (m?.scope && UNINSTALL_SCOPES.includes(m.scope)) emit({ ...base, type: "uninstall", name: "uninstall" });
|
|
591
711
|
})
|
|
592
712
|
);
|
|
593
713
|
afterDelivery();
|
|
@@ -615,11 +735,17 @@ function createKanca(options = {}) {
|
|
|
615
735
|
let method = "GET";
|
|
616
736
|
let gql = false;
|
|
617
737
|
let opName;
|
|
738
|
+
let tokenUrl;
|
|
618
739
|
try {
|
|
619
740
|
url = toUrl(input);
|
|
620
741
|
method = (init?.method ?? (isRequest(input) ? input.method : "GET")).toUpperCase();
|
|
621
742
|
gql = !!url && isIkasGraphqlUrl(url);
|
|
622
|
-
|
|
743
|
+
tokenUrl = url ? ikasTokenUrl(url) : void 0;
|
|
744
|
+
if (tokenUrl) {
|
|
745
|
+
if (init && init.body != null) opName = Promise.resolve(oauthNameFromBody(bodyToString(init.body) ?? init.body));
|
|
746
|
+
else if (isRequest(input) && input.body) opName = input.clone().text().then(oauthNameFromBody, () => oauthNameFromBody(void 0));
|
|
747
|
+
else opName = Promise.resolve(oauthNameFromBody(void 0));
|
|
748
|
+
} else if (gql) {
|
|
623
749
|
if (init && init.body != null) opName = Promise.resolve(operationNameFromBody(bodyToString(init.body)));
|
|
624
750
|
else if (isRequest(input) && input.body) opName = input.clone().text().then(operationNameFromBody, () => "anonymous");
|
|
625
751
|
else opName = Promise.resolve("anonymous");
|
|
@@ -637,7 +763,8 @@ function createKanca(options = {}) {
|
|
|
637
763
|
const durationMs2 = now() - t0;
|
|
638
764
|
try {
|
|
639
765
|
const error = { code: "NETWORK", message: errorMessage(err) };
|
|
640
|
-
if (
|
|
766
|
+
if (tokenUrl) recordOAuth(opName, ts, durationMs2, tokenUrl.storeName, { ok: false, error: { code: "NETWORK" } }, void 0);
|
|
767
|
+
else if (gql) observe((opName ?? Promise.resolve("anonymous")).then((name) => emit({ type: "graphql", name, ts, ok: false, durationMs: durationMs2, error })));
|
|
641
768
|
else emit({ type: "rest", name: `${method} ${url.host}${url.pathname}`, ts, ok: false, durationMs: durationMs2, error });
|
|
642
769
|
} catch {
|
|
643
770
|
}
|
|
@@ -646,7 +773,15 @@ function createKanca(options = {}) {
|
|
|
646
773
|
const durationMs = now() - t0;
|
|
647
774
|
try {
|
|
648
775
|
const status = res.status;
|
|
649
|
-
if (
|
|
776
|
+
if (tokenUrl) {
|
|
777
|
+
let clone;
|
|
778
|
+
try {
|
|
779
|
+
clone = status >= 400 ? res.clone() : void 0;
|
|
780
|
+
} catch {
|
|
781
|
+
clone = void 0;
|
|
782
|
+
}
|
|
783
|
+
recordOAuth(opName, ts, durationMs, tokenUrl.storeName, { status, ok: status < 400 }, clone);
|
|
784
|
+
} else if (gql) {
|
|
650
785
|
let clone;
|
|
651
786
|
try {
|
|
652
787
|
clone = res.clone();
|
|
@@ -682,6 +817,32 @@ function createKanca(options = {}) {
|
|
|
682
817
|
}
|
|
683
818
|
return res;
|
|
684
819
|
};
|
|
820
|
+
const recordOAuth = (name, ts, durationMs, storeName, outcome, errorBody) => {
|
|
821
|
+
observe(
|
|
822
|
+
(async () => {
|
|
823
|
+
let error = outcome.error;
|
|
824
|
+
if (errorBody && typeof outcome.status === "number") {
|
|
825
|
+
let data;
|
|
826
|
+
try {
|
|
827
|
+
data = JSON.parse(await errorBody.text());
|
|
828
|
+
} catch {
|
|
829
|
+
data = void 0;
|
|
830
|
+
}
|
|
831
|
+
error = { code: oauthErrorCode(data, outcome.status) };
|
|
832
|
+
}
|
|
833
|
+
emit({
|
|
834
|
+
type: "rest",
|
|
835
|
+
name: await (name ?? Promise.resolve(OAUTH_REFRESH)),
|
|
836
|
+
ts,
|
|
837
|
+
status: outcome.status,
|
|
838
|
+
ok: outcome.ok,
|
|
839
|
+
durationMs,
|
|
840
|
+
error,
|
|
841
|
+
attrs: storeName ? { storeName } : void 0
|
|
842
|
+
});
|
|
843
|
+
})()
|
|
844
|
+
);
|
|
845
|
+
};
|
|
685
846
|
let warnedInstrument = false;
|
|
686
847
|
const instrument = (client, opts) => {
|
|
687
848
|
const c = client;
|
|
@@ -790,12 +951,52 @@ function createKanca(options = {}) {
|
|
|
790
951
|
log("track failed", e);
|
|
791
952
|
}
|
|
792
953
|
};
|
|
954
|
+
const recordUninstall = (result) => {
|
|
955
|
+
if (result.state !== "uninstalled") return;
|
|
956
|
+
emit({ type: "uninstall", name: "uninstall", ts: Date.now(), merchantId: result.merchantId, attrs: { source: "check" } });
|
|
957
|
+
transport.scheduleSoon();
|
|
958
|
+
};
|
|
959
|
+
const checkInstall = async (client, fields) => {
|
|
960
|
+
const result = await checkInstallState(fields?.merchantId, client, recheckDelayMs);
|
|
961
|
+
recordUninstall(result);
|
|
962
|
+
return result;
|
|
963
|
+
};
|
|
964
|
+
const checkInstalls = async (stores, opts) => {
|
|
965
|
+
const out = await checkInstallStates(stores, checkInstallsOptions(opts), recheckDelayMs);
|
|
966
|
+
for (const r of out.results) recordUninstall(r);
|
|
967
|
+
return out;
|
|
968
|
+
};
|
|
969
|
+
const trackRefresh = async (call, fields) => {
|
|
970
|
+
const ts = Date.now();
|
|
971
|
+
const t0 = now();
|
|
972
|
+
const record = (outcome) => {
|
|
973
|
+
try {
|
|
974
|
+
const attrs = {};
|
|
975
|
+
if (typeof fields?.storeName === "string" && fields.storeName) attrs.storeName = fields.storeName;
|
|
976
|
+
emit({ type: "rest", name: OAUTH_REFRESH, ts, durationMs: now() - t0, merchantId: fields?.merchantId, attrs, ...outcome });
|
|
977
|
+
} catch (e) {
|
|
978
|
+
log("trackRefresh failed", e);
|
|
979
|
+
}
|
|
980
|
+
};
|
|
981
|
+
let result;
|
|
982
|
+
try {
|
|
983
|
+
result = await call();
|
|
984
|
+
} catch (err) {
|
|
985
|
+
record(refreshErrorOutcome(err));
|
|
986
|
+
throw err;
|
|
987
|
+
}
|
|
988
|
+
record(refreshResultOutcome(result));
|
|
989
|
+
return result;
|
|
990
|
+
};
|
|
793
991
|
log(`enabled \u2192 ${options.endpoint || DEFAULT_ENDPOINT}`);
|
|
794
992
|
return {
|
|
795
993
|
webhook,
|
|
796
994
|
fetch: kfetch,
|
|
797
995
|
instrument,
|
|
798
996
|
track,
|
|
997
|
+
checkInstall,
|
|
998
|
+
checkInstalls,
|
|
999
|
+
trackRefresh,
|
|
799
1000
|
flush,
|
|
800
1001
|
enabled: true
|
|
801
1002
|
};
|