@kanca-app/ikas 0.2.0 → 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 +64 -14
- package/dist/browser.cjs +1 -1
- package/dist/browser.cjs.map +1 -1
- package/dist/browser.js +1 -1
- package/dist/browser.js.map +1 -1
- package/dist/index.cjs +216 -15
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +42 -11
- package/dist/index.d.ts +42 -11
- package/dist/index.js +216 -15
- package/dist/index.js.map +1 -1
- package/dist/react.cjs +1 -1
- package/dist/react.cjs.map +1 -1
- package/dist/react.js +1 -1
- package/dist/react.js.map +1 -1
- package/dist/storefront.cjs +1 -1
- package/dist/storefront.cjs.map +1 -1
- package/dist/storefront.js +1 -1
- package/dist/storefront.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -145,7 +145,7 @@ function classifyGraphql(status, json) {
|
|
|
145
145
|
|
|
146
146
|
// src/shared.ts
|
|
147
147
|
var SDK_NAME = "@kanca-app/ikas";
|
|
148
|
-
var SDK_VERSION = "0.
|
|
148
|
+
var SDK_VERSION = "0.3.0";
|
|
149
149
|
var DEFAULT_ENDPOINT = "https://kanca.onurhan.dev";
|
|
150
150
|
var MAX_NAME = 200;
|
|
151
151
|
var MAX_ERROR_MESSAGE = 500;
|
|
@@ -378,6 +378,126 @@ var Transport = class {
|
|
|
378
378
|
}
|
|
379
379
|
};
|
|
380
380
|
|
|
381
|
+
// src/install.ts
|
|
382
|
+
var INSTALL_RECHECK_DELAY_MS = 3e3;
|
|
383
|
+
var MASS_REVOCATION_MIN = 5;
|
|
384
|
+
var PROBE = "query getAuthorizedApp { getAuthorizedApp { id } }";
|
|
385
|
+
async function probe(client) {
|
|
386
|
+
const c = client;
|
|
387
|
+
if (!c || typeof c !== "object" || !c._client || typeof c._client.request !== "function") {
|
|
388
|
+
return { state: "unknown", reason: "UNSUPPORTED_CLIENT" };
|
|
389
|
+
}
|
|
390
|
+
if (typeof c._checkToken === "function") {
|
|
391
|
+
try {
|
|
392
|
+
await c._checkToken();
|
|
393
|
+
} catch {
|
|
394
|
+
return { state: "unknown", reason: "TOKEN_CHECK_FAILED" };
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
let data;
|
|
398
|
+
try {
|
|
399
|
+
data = await c._client.request({ document: PROBE });
|
|
400
|
+
} catch (err) {
|
|
401
|
+
const resp = err?.response;
|
|
402
|
+
const status = resp?.status;
|
|
403
|
+
if (typeof status !== "number") return { state: "unknown", reason: "NETWORK" };
|
|
404
|
+
const first = Array.isArray(resp.errors) ? resp.errors[0] : void 0;
|
|
405
|
+
const code = first?.extensions?.code;
|
|
406
|
+
return { state: "unknown", reason: typeof code === "string" && code ? code.slice(0, 100) : `HTTP_${status}` };
|
|
407
|
+
}
|
|
408
|
+
if (data && typeof data === "object" && "getAuthorizedApp" in data) {
|
|
409
|
+
const app = data.getAuthorizedApp;
|
|
410
|
+
if (app === null) return { state: "removed" };
|
|
411
|
+
const id = app && typeof app === "object" ? app.id : void 0;
|
|
412
|
+
if (typeof id === "string" && id) return { state: "installed" };
|
|
413
|
+
}
|
|
414
|
+
return { state: "unknown", reason: "UNEXPECTED_RESPONSE" };
|
|
415
|
+
}
|
|
416
|
+
var sleep2 = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
417
|
+
async function checkInstallState(merchantId, client, recheckDelayMs) {
|
|
418
|
+
if (typeof merchantId !== "string" || !merchantId) return { merchantId: "", state: "unknown", reason: "MISSING_MERCHANT_ID" };
|
|
419
|
+
try {
|
|
420
|
+
const first = await probe(client);
|
|
421
|
+
if (first.state === "installed") return { merchantId, state: "installed" };
|
|
422
|
+
if (first.state === "unknown") return { merchantId, state: "unknown", reason: first.reason };
|
|
423
|
+
await sleep2(recheckDelayMs);
|
|
424
|
+
const second = await probe(client);
|
|
425
|
+
if (second.state === "removed") return { merchantId, state: "uninstalled" };
|
|
426
|
+
if (second.state === "installed") return { merchantId, state: "installed" };
|
|
427
|
+
return { merchantId, state: "unknown", reason: second.reason };
|
|
428
|
+
} catch {
|
|
429
|
+
return { merchantId, state: "unknown", reason: "UNEXPECTED_RESPONSE" };
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
function isMassRevocation(uninstalled, checked) {
|
|
433
|
+
return uninstalled >= MASS_REVOCATION_MIN && uninstalled * 2 > checked;
|
|
434
|
+
}
|
|
435
|
+
async function checkInstallStates(stores, concurrency, recheckDelayMs) {
|
|
436
|
+
const list = Array.isArray(stores) ? stores : [];
|
|
437
|
+
const results = new Array(list.length);
|
|
438
|
+
let next = 0;
|
|
439
|
+
const worker = async () => {
|
|
440
|
+
while (next < list.length) {
|
|
441
|
+
const i = next++;
|
|
442
|
+
const store = list[i];
|
|
443
|
+
results[i] = await checkInstallState(store?.merchantId, store?.client, recheckDelayMs);
|
|
444
|
+
}
|
|
445
|
+
};
|
|
446
|
+
await Promise.all(Array.from({ length: Math.min(concurrency, list.length) }, worker));
|
|
447
|
+
const uninstalled = results.filter((r) => r.state === "uninstalled").length;
|
|
448
|
+
if (!isMassRevocation(uninstalled, results.length)) return { results, massRevocation: false };
|
|
449
|
+
return {
|
|
450
|
+
massRevocation: true,
|
|
451
|
+
results: results.map((r) => r.state === "uninstalled" ? { merchantId: r.merchantId, state: "unknown", reason: "MASS_REVOCATION" } : r)
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// src/oauth.ts
|
|
456
|
+
var OAUTH_REFRESH = "oauth:refresh";
|
|
457
|
+
var OAUTH_TOKEN = "oauth:token";
|
|
458
|
+
var STORE_SUFFIXES = [".myikas.com", ".myikas.dev"];
|
|
459
|
+
var OAUTH_ERROR = /^[a-z0-9_]{1,64}$/;
|
|
460
|
+
function ikasTokenUrl(url) {
|
|
461
|
+
const host = url.hostname.toLowerCase();
|
|
462
|
+
const suffix = STORE_SUFFIXES.find((s) => host.endsWith(s));
|
|
463
|
+
if (!suffix) return void 0;
|
|
464
|
+
if (url.pathname.replace(/\/+$/, "") !== "/api/admin/oauth/token") return void 0;
|
|
465
|
+
const store = host.slice(0, -suffix.length);
|
|
466
|
+
if (!store || store.includes(".")) return void 0;
|
|
467
|
+
return store === "api" ? {} : { storeName: store };
|
|
468
|
+
}
|
|
469
|
+
function oauthNameFromBody(body) {
|
|
470
|
+
try {
|
|
471
|
+
let params;
|
|
472
|
+
if (body instanceof URLSearchParams) params = body;
|
|
473
|
+
else if (typeof body === "string") params = new URLSearchParams(body);
|
|
474
|
+
return params?.get("grant_type") === "refresh_token" ? OAUTH_REFRESH : OAUTH_TOKEN;
|
|
475
|
+
} catch {
|
|
476
|
+
return OAUTH_TOKEN;
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
function oauthErrorCode(data, status) {
|
|
480
|
+
const code = data && typeof data === "object" ? data.error : void 0;
|
|
481
|
+
return typeof code === "string" && OAUTH_ERROR.test(code) ? code : `HTTP_${status}`;
|
|
482
|
+
}
|
|
483
|
+
function fromStatus(status, data) {
|
|
484
|
+
const ok = status < 400;
|
|
485
|
+
return ok ? { status, ok } : { status, ok, error: { code: oauthErrorCode(data, status) } };
|
|
486
|
+
}
|
|
487
|
+
function validStatus(v) {
|
|
488
|
+
return typeof v === "number" && Number.isInteger(v) && v >= 100 && v <= 599;
|
|
489
|
+
}
|
|
490
|
+
function refreshResultOutcome(result) {
|
|
491
|
+
const r = result;
|
|
492
|
+
if (r && typeof r === "object" && validStatus(r.status)) return fromStatus(r.status, r.data);
|
|
493
|
+
return {};
|
|
494
|
+
}
|
|
495
|
+
function refreshErrorOutcome(err) {
|
|
496
|
+
const resp = err?.response;
|
|
497
|
+
if (resp && typeof resp === "object" && validStatus(resp.status)) return fromStatus(resp.status, resp.data);
|
|
498
|
+
return { ok: false, error: { code: "NETWORK" } };
|
|
499
|
+
}
|
|
500
|
+
|
|
381
501
|
// src/index.ts
|
|
382
502
|
var exitFlushers = /* @__PURE__ */ new Set();
|
|
383
503
|
var exitHookInstalled = false;
|
|
@@ -395,11 +515,6 @@ function registerExitFlush(fn) {
|
|
|
395
515
|
} catch {
|
|
396
516
|
}
|
|
397
517
|
}
|
|
398
|
-
var UNINSTALL_SCOPES = [
|
|
399
|
-
"store/app/deleted",
|
|
400
|
-
"store/app/uninstalled",
|
|
401
|
-
"store/authorizedApp/deleted"
|
|
402
|
-
];
|
|
403
518
|
var MARK = /* @__PURE__ */ Symbol.for("kanca.instrumented");
|
|
404
519
|
var now = () => typeof performance !== "undefined" && typeof performance.now === "function" ? performance.now() : Date.now();
|
|
405
520
|
function clampInt(v, def, min, max) {
|
|
@@ -441,12 +556,18 @@ function parseWebhookMeta(text) {
|
|
|
441
556
|
return null;
|
|
442
557
|
}
|
|
443
558
|
}
|
|
444
|
-
function
|
|
559
|
+
function checkInstallsOptions(options) {
|
|
560
|
+
return clampInt(options?.concurrency, 3, 1, 10);
|
|
561
|
+
}
|
|
562
|
+
function noopKanca(recheckDelayMs) {
|
|
445
563
|
return {
|
|
446
564
|
webhook: (handler) => async (req, ...rest) => handler(req, ...rest),
|
|
447
565
|
fetch: ((input, init) => globalThis.fetch(input, init)),
|
|
448
566
|
instrument: (client) => client,
|
|
449
567
|
track: () => void 0,
|
|
568
|
+
checkInstall: (client, fields) => checkInstallState(fields?.merchantId, client, recheckDelayMs),
|
|
569
|
+
checkInstalls: (stores, options) => checkInstallStates(stores, checkInstallsOptions(options), recheckDelayMs),
|
|
570
|
+
trackRefresh: async (call) => await call(),
|
|
450
571
|
flush: () => Promise.resolve(),
|
|
451
572
|
enabled: false
|
|
452
573
|
};
|
|
@@ -457,20 +578,21 @@ function createKanca(options = {}) {
|
|
|
457
578
|
if (debug) console.debug(`[kanca] ${msg}`, ...args);
|
|
458
579
|
};
|
|
459
580
|
const warn = (msg) => console.warn(msg);
|
|
581
|
+
const recheckDelayMs = clampInt(options.installRecheckDelayMs, INSTALL_RECHECK_DELAY_MS, 0, 6e4);
|
|
460
582
|
if (options.disabled) {
|
|
461
583
|
log("disabled; SDK is a no-op");
|
|
462
|
-
return noopKanca();
|
|
584
|
+
return noopKanca(recheckDelayMs);
|
|
463
585
|
}
|
|
464
586
|
const key = typeof options.key === "string" ? options.key.trim() : "";
|
|
465
587
|
if (!key) {
|
|
466
588
|
log("no key (KANCA_KEY) configured; SDK is a no-op");
|
|
467
|
-
return noopKanca();
|
|
589
|
+
return noopKanca(recheckDelayMs);
|
|
468
590
|
}
|
|
469
591
|
if (!key.startsWith("kk_")) {
|
|
470
592
|
warn(
|
|
471
593
|
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."
|
|
472
594
|
);
|
|
473
|
-
return noopKanca();
|
|
595
|
+
return noopKanca(recheckDelayMs);
|
|
474
596
|
}
|
|
475
597
|
const maxBatchSize = clampInt(options.maxBatchSize, 100, 1, MAX_EVENTS_PER_BATCH);
|
|
476
598
|
const transport = new Transport({
|
|
@@ -557,7 +679,6 @@ function createKanca(options = {}) {
|
|
|
557
679
|
durationMs,
|
|
558
680
|
error
|
|
559
681
|
});
|
|
560
|
-
if (m?.scope && UNINSTALL_SCOPES.includes(m.scope)) emit({ ...base, type: "uninstall", name: "uninstall" });
|
|
561
682
|
})
|
|
562
683
|
);
|
|
563
684
|
afterDelivery();
|
|
@@ -585,11 +706,17 @@ function createKanca(options = {}) {
|
|
|
585
706
|
let method = "GET";
|
|
586
707
|
let gql = false;
|
|
587
708
|
let opName;
|
|
709
|
+
let tokenUrl;
|
|
588
710
|
try {
|
|
589
711
|
url = toUrl(input);
|
|
590
712
|
method = (init?.method ?? (isRequest(input) ? input.method : "GET")).toUpperCase();
|
|
591
713
|
gql = !!url && isIkasGraphqlUrl(url);
|
|
592
|
-
|
|
714
|
+
tokenUrl = url ? ikasTokenUrl(url) : void 0;
|
|
715
|
+
if (tokenUrl) {
|
|
716
|
+
if (init && init.body != null) opName = Promise.resolve(oauthNameFromBody(bodyToString(init.body) ?? init.body));
|
|
717
|
+
else if (isRequest(input) && input.body) opName = input.clone().text().then(oauthNameFromBody, () => oauthNameFromBody(void 0));
|
|
718
|
+
else opName = Promise.resolve(oauthNameFromBody(void 0));
|
|
719
|
+
} else if (gql) {
|
|
593
720
|
if (init && init.body != null) opName = Promise.resolve(operationNameFromBody(bodyToString(init.body)));
|
|
594
721
|
else if (isRequest(input) && input.body) opName = input.clone().text().then(operationNameFromBody, () => "anonymous");
|
|
595
722
|
else opName = Promise.resolve("anonymous");
|
|
@@ -607,7 +734,8 @@ function createKanca(options = {}) {
|
|
|
607
734
|
const durationMs2 = now() - t0;
|
|
608
735
|
try {
|
|
609
736
|
const error = { code: "NETWORK", message: errorMessage(err) };
|
|
610
|
-
if (
|
|
737
|
+
if (tokenUrl) recordOAuth(opName, ts, durationMs2, tokenUrl.storeName, { ok: false, error: { code: "NETWORK" } }, void 0);
|
|
738
|
+
else if (gql) observe((opName ?? Promise.resolve("anonymous")).then((name) => emit({ type: "graphql", name, ts, ok: false, durationMs: durationMs2, error })));
|
|
611
739
|
else emit({ type: "rest", name: `${method} ${url.host}${url.pathname}`, ts, ok: false, durationMs: durationMs2, error });
|
|
612
740
|
} catch {
|
|
613
741
|
}
|
|
@@ -616,7 +744,15 @@ function createKanca(options = {}) {
|
|
|
616
744
|
const durationMs = now() - t0;
|
|
617
745
|
try {
|
|
618
746
|
const status = res.status;
|
|
619
|
-
if (
|
|
747
|
+
if (tokenUrl) {
|
|
748
|
+
let clone;
|
|
749
|
+
try {
|
|
750
|
+
clone = status >= 400 ? res.clone() : void 0;
|
|
751
|
+
} catch {
|
|
752
|
+
clone = void 0;
|
|
753
|
+
}
|
|
754
|
+
recordOAuth(opName, ts, durationMs, tokenUrl.storeName, { status, ok: status < 400 }, clone);
|
|
755
|
+
} else if (gql) {
|
|
620
756
|
let clone;
|
|
621
757
|
try {
|
|
622
758
|
clone = res.clone();
|
|
@@ -652,6 +788,32 @@ function createKanca(options = {}) {
|
|
|
652
788
|
}
|
|
653
789
|
return res;
|
|
654
790
|
};
|
|
791
|
+
const recordOAuth = (name, ts, durationMs, storeName, outcome, errorBody) => {
|
|
792
|
+
observe(
|
|
793
|
+
(async () => {
|
|
794
|
+
let error = outcome.error;
|
|
795
|
+
if (errorBody && typeof outcome.status === "number") {
|
|
796
|
+
let data;
|
|
797
|
+
try {
|
|
798
|
+
data = JSON.parse(await errorBody.text());
|
|
799
|
+
} catch {
|
|
800
|
+
data = void 0;
|
|
801
|
+
}
|
|
802
|
+
error = { code: oauthErrorCode(data, outcome.status) };
|
|
803
|
+
}
|
|
804
|
+
emit({
|
|
805
|
+
type: "rest",
|
|
806
|
+
name: await (name ?? Promise.resolve(OAUTH_REFRESH)),
|
|
807
|
+
ts,
|
|
808
|
+
status: outcome.status,
|
|
809
|
+
ok: outcome.ok,
|
|
810
|
+
durationMs,
|
|
811
|
+
error,
|
|
812
|
+
attrs: storeName ? { storeName } : void 0
|
|
813
|
+
});
|
|
814
|
+
})()
|
|
815
|
+
);
|
|
816
|
+
};
|
|
655
817
|
let warnedInstrument = false;
|
|
656
818
|
const instrument = (client, opts) => {
|
|
657
819
|
const c = client;
|
|
@@ -760,12 +922,52 @@ function createKanca(options = {}) {
|
|
|
760
922
|
log("track failed", e);
|
|
761
923
|
}
|
|
762
924
|
};
|
|
925
|
+
const recordUninstall = (result) => {
|
|
926
|
+
if (result.state !== "uninstalled") return;
|
|
927
|
+
emit({ type: "uninstall", name: "uninstall", ts: Date.now(), merchantId: result.merchantId, attrs: { source: "check" } });
|
|
928
|
+
transport.scheduleSoon();
|
|
929
|
+
};
|
|
930
|
+
const checkInstall = async (client, fields) => {
|
|
931
|
+
const result = await checkInstallState(fields?.merchantId, client, recheckDelayMs);
|
|
932
|
+
recordUninstall(result);
|
|
933
|
+
return result;
|
|
934
|
+
};
|
|
935
|
+
const checkInstalls = async (stores, opts) => {
|
|
936
|
+
const out = await checkInstallStates(stores, checkInstallsOptions(opts), recheckDelayMs);
|
|
937
|
+
for (const r of out.results) recordUninstall(r);
|
|
938
|
+
return out;
|
|
939
|
+
};
|
|
940
|
+
const trackRefresh = async (call, fields) => {
|
|
941
|
+
const ts = Date.now();
|
|
942
|
+
const t0 = now();
|
|
943
|
+
const record = (outcome) => {
|
|
944
|
+
try {
|
|
945
|
+
const attrs = {};
|
|
946
|
+
if (typeof fields?.storeName === "string" && fields.storeName) attrs.storeName = fields.storeName;
|
|
947
|
+
emit({ type: "rest", name: OAUTH_REFRESH, ts, durationMs: now() - t0, merchantId: fields?.merchantId, attrs, ...outcome });
|
|
948
|
+
} catch (e) {
|
|
949
|
+
log("trackRefresh failed", e);
|
|
950
|
+
}
|
|
951
|
+
};
|
|
952
|
+
let result;
|
|
953
|
+
try {
|
|
954
|
+
result = await call();
|
|
955
|
+
} catch (err) {
|
|
956
|
+
record(refreshErrorOutcome(err));
|
|
957
|
+
throw err;
|
|
958
|
+
}
|
|
959
|
+
record(refreshResultOutcome(result));
|
|
960
|
+
return result;
|
|
961
|
+
};
|
|
763
962
|
log(`enabled \u2192 ${options.endpoint || DEFAULT_ENDPOINT}`);
|
|
764
963
|
return {
|
|
765
964
|
webhook,
|
|
766
965
|
fetch: kfetch,
|
|
767
966
|
instrument,
|
|
768
967
|
track,
|
|
968
|
+
checkInstall,
|
|
969
|
+
checkInstalls,
|
|
970
|
+
trackRefresh,
|
|
769
971
|
flush,
|
|
770
972
|
enabled: true
|
|
771
973
|
};
|
|
@@ -773,7 +975,6 @@ function createKanca(options = {}) {
|
|
|
773
975
|
export {
|
|
774
976
|
SDK_NAME,
|
|
775
977
|
SDK_VERSION,
|
|
776
|
-
UNINSTALL_SCOPES,
|
|
777
978
|
createKanca,
|
|
778
979
|
operationName
|
|
779
980
|
};
|