@trackunit/iris-app-sdk-vite 0.3.15 → 0.3.18

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/CHANGELOG.md CHANGED
@@ -1,3 +1,27 @@
1
+ ## 0.3.18 (2026-04-27)
2
+
3
+ ### 🧱 Updated Dependencies
4
+
5
+ - Updated iris-app-build-utilities to 1.15.18
6
+ - Updated iris-app-api to 1.17.18
7
+ - Updated iris-app to 1.18.19
8
+
9
+ ## 0.3.17 (2026-04-27)
10
+
11
+ ### 🧱 Updated Dependencies
12
+
13
+ - Updated iris-app-build-utilities to 1.15.17
14
+ - Updated iris-app-api to 1.17.17
15
+ - Updated iris-app to 1.18.18
16
+
17
+ ## 0.3.16 (2026-04-27)
18
+
19
+ ### 🧱 Updated Dependencies
20
+
21
+ - Updated iris-app-build-utilities to 1.15.16
22
+ - Updated iris-app-api to 1.17.16
23
+ - Updated iris-app to 1.18.17
24
+
1
25
  ## 0.3.15 (2026-04-24)
2
26
 
3
27
  ### 🧱 Updated Dependencies
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trackunit/iris-app-sdk-vite",
3
- "version": "0.3.15",
3
+ "version": "0.3.18",
4
4
  "license": "SEE LICENSE IN LICENSE.txt",
5
5
  "repository": "https://github.com/Trackunit/manager",
6
6
  "executors": "./executors.json",
@@ -10,8 +10,8 @@
10
10
  "dependencies": {
11
11
  "rxjs": "7.8.1",
12
12
  "win-ca": "^3.5.1",
13
- "@trackunit/iris-app-build-utilities": "1.15.15",
14
- "@trackunit/iris-app-api": "1.17.15",
13
+ "@trackunit/iris-app-build-utilities": "1.15.18",
14
+ "@trackunit/iris-app-api": "1.17.18",
15
15
  "tslib": "^2.6.2",
16
16
  "vite": "7.3.1",
17
17
  "@module-federation/vite": "1.11.0",
@@ -21,7 +21,10 @@
21
21
  "rollup-plugin-visualizer": "5.12.0",
22
22
  "@tailwindcss/postcss": "^4.1.18",
23
23
  "@vitejs/plugin-react-swc": "^4.2.3",
24
- "@nx/devkit": "22.6.5"
24
+ "@nx/devkit": "22.6.5",
25
+ "@trackunit/iris-app": "1.18.19",
26
+ "cross-keychain": "^1.1.0",
27
+ "jwt-decode": "^3.1.2"
25
28
  },
26
29
  "types": "./src/index.d.ts",
27
30
  "main": "./src/index.js",
@@ -3,12 +3,14 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.default = serveExecutor;
4
4
  const tslib_1 = require("tslib");
5
5
  const rxjs_for_await_1 = require("@nx/devkit/src/utils/rxjs-for-await");
6
+ const iris_app_1 = require("@trackunit/iris-app");
6
7
  const iris_app_build_utilities_1 = require("@trackunit/iris-app-build-utilities");
7
8
  const fs_1 = require("fs");
8
9
  const path_1 = require("path");
9
10
  const rxjs_1 = require("rxjs");
10
11
  const op = tslib_1.__importStar(require("rxjs/operators"));
11
12
  require("win-ca");
13
+ const checkCustomFields_1 = require("../utils/checkCustomFields");
12
14
  const defaultViteConfig_1 = require("../utils/defaultViteConfig");
13
15
  /**
14
16
  * Serve executor for serving Iris Apps with Vite.
@@ -19,6 +21,7 @@ const defaultViteConfig_1 = require("../utils/defaultViteConfig");
19
21
  */
20
22
  async function* serveExecutor(options, context) {
21
23
  await (0, iris_app_build_utilities_1.checkPackageVersion)(false);
24
+ const settings = (0, iris_app_1.getSettings)();
22
25
  const skipTypeChecking = options.skipTypeChecking ?? false;
23
26
  // eslint-disable-next-line @trackunit/no-typescript-assertion, @typescript-eslint/no-non-null-assertion
24
27
  const projectRoot = context.projectsConfigurations.projects[context.projectName].root;
@@ -30,6 +33,13 @@ async function* serveExecutor(options, context) {
30
33
  });
31
34
  // Now we can safely import the manifest (it uses @trackunit/* imports)
32
35
  const irisAppManifest = (await Promise.resolve(`${manifestPath}`).then(s => tslib_1.__importStar(require(s)))).default;
36
+ const checkCustomFieldsResult = await (0, checkCustomFields_1.checkCustomFields)(irisAppManifest, settings);
37
+ if (checkCustomFieldsResult === "out-of-sync") {
38
+ throw new Error("Custom fields are out of sync with the latest approved manifest, please submit and get this version approved before you continue developing the app.");
39
+ }
40
+ if (checkCustomFieldsResult === "not-submitted") {
41
+ throw new Error("Custom fields are not submitted, please submit and get this version approved before you continue developing the app.");
42
+ }
33
43
  const serversideResult = await (0, iris_app_build_utilities_1.spawnServersideExtensions)(irisAppManifest, context.root);
34
44
  try {
35
45
  // Get default config (internally imports @trackunit/iris-app-vite-plugin)
@@ -0,0 +1,111 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getCachedAccessToken = void 0;
4
+ const tslib_1 = require("tslib");
5
+ const iris_app_1 = require("@trackunit/iris-app");
6
+ const cross_keychain_1 = require("cross-keychain");
7
+ const jwt_decode_1 = tslib_1.__importDefault(require("jwt-decode"));
8
+ const SERVICE = "trackunit-iris-app-sdk";
9
+ const SKEW_MS = 60000;
10
+ /**
11
+ * Returns an access token, preferring a cached keychain entry when it is still
12
+ * valid. Falls through to the interactive device-code login when the cache is
13
+ * missing, expired, or unreadable, then persists the fresh token.
14
+ *
15
+ * CI paths (TU_TOKEN / TU_CLIENT_ID_*) bypass the cache entirely.
16
+ */
17
+ const getCachedAccessToken = async (settings) => {
18
+ if (isCiEnvironment(settings.env)) {
19
+ return unwrapTokenData(await (0, iris_app_1.getAccessToken)(settings.env));
20
+ }
21
+ // on mac you can delete it using security delete-generic-password -s "trackunit-iris-app-sdk" -a "iris-app-serve-access-token-LOCAL"
22
+ const account = `iris-app-serve-access-token-${settings.env}`;
23
+ const cached = await safeReadToken(account);
24
+ if (cached && cached.expires_at - SKEW_MS > Date.now()) {
25
+ // eslint-disable-next-line no-console
26
+ console.log("🔑 Using cached access token from system keychain.");
27
+ return {
28
+ access_token: cached.access_token,
29
+ token_type: cached.token_type,
30
+ scope: cached.scope,
31
+ id_token: cached.id_token,
32
+ expires_in: Math.max(0, Math.floor((cached.expires_at - Date.now()) / 1000)),
33
+ };
34
+ }
35
+ const fresh = unwrapTokenData(await (0, iris_app_1.getAccessToken)(settings.env));
36
+ await safeStoreToken(account, {
37
+ access_token: fresh.access_token,
38
+ token_type: fresh.token_type,
39
+ scope: fresh.scope,
40
+ id_token: fresh.id_token,
41
+ expires_at: computeExpiresAt(fresh),
42
+ });
43
+ return fresh;
44
+ };
45
+ exports.getCachedAccessToken = getCachedAccessToken;
46
+ const isCiEnvironment = (env) => Boolean(process.env.TU_TOKEN) || Boolean(process.env[`TU_CLIENT_ID_${env}`]);
47
+ const unwrapTokenData = (tokenData) => {
48
+ if (!("access_token" in tokenData)) {
49
+ if ("error" in tokenData && tokenData.error === "expired_token") {
50
+ throw new Error("Authentication attempt expired");
51
+ }
52
+ const description = "error_description" in tokenData ? ` ${tokenData.error_description}` : "";
53
+ const errorName = "error" in tokenData ? tokenData.error : "unknown";
54
+ throw new Error(`Failed to get access token: ${errorName}${description}`);
55
+ }
56
+ return tokenData;
57
+ };
58
+ const computeExpiresAt = (token) => {
59
+ try {
60
+ const decoded = (0, jwt_decode_1.default)(token.access_token);
61
+ if (decoded !== null && typeof decoded === "object" && "exp" in decoded && typeof decoded.exp === "number") {
62
+ return decoded.exp * 1000;
63
+ }
64
+ }
65
+ catch {
66
+ // JWT decode failed — fall through to expires_in
67
+ }
68
+ return Date.now() + token.expires_in * 1000;
69
+ };
70
+ const safeReadToken = async (account) => {
71
+ try {
72
+ const raw = await (0, cross_keychain_1.getPassword)(SERVICE, account);
73
+ if (!raw) {
74
+ return undefined;
75
+ }
76
+ const parsed = JSON.parse(raw);
77
+ if (!isValidCachedPayload(parsed)) {
78
+ return undefined;
79
+ }
80
+ return parsed;
81
+ }
82
+ catch (error) {
83
+ const reason = error instanceof Error ? error.message : String(error);
84
+ // eslint-disable-next-line no-console
85
+ console.warn(`⚠️ Could not read cached token from system keychain: ${reason}. Falling back to interactive login.`);
86
+ return undefined;
87
+ }
88
+ };
89
+ const safeStoreToken = async (account, payload) => {
90
+ try {
91
+ await (0, cross_keychain_1.setPassword)(SERVICE, account, JSON.stringify(payload));
92
+ }
93
+ catch (error) {
94
+ const reason = error instanceof Error ? error.message : String(error);
95
+ // eslint-disable-next-line no-console
96
+ console.warn(`⚠️ Could not cache access token in system keychain: ${reason}. Token will not be persisted.`);
97
+ }
98
+ };
99
+ const isValidCachedPayload = (value) => value !== null &&
100
+ typeof value === "object" &&
101
+ "access_token" in value &&
102
+ typeof value.access_token === "string" &&
103
+ "expires_at" in value &&
104
+ typeof value.expires_at === "number" &&
105
+ "token_type" in value &&
106
+ typeof value.token_type === "string" &&
107
+ "scope" in value &&
108
+ typeof value.scope === "string" &&
109
+ "id_token" in value &&
110
+ typeof value.id_token === "string";
111
+ //# sourceMappingURL=accessTokenCache.js.map
@@ -0,0 +1,88 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.checkCustomFields = void 0;
4
+ const accessTokenCache_1 = require("./accessTokenCache");
5
+ const deepEqual_1 = require("./deepEqual");
6
+ /**
7
+ * Checks if the custom field definitions are in sync with the latest approved manifest.
8
+ *
9
+ * @param irisAppManifest - The iris app manifest.
10
+ */
11
+ const checkCustomFields = async (irisAppManifest, settings) => {
12
+ const localFields = irisAppManifest.customFieldDefinitions ?? [];
13
+ if (localFields.length === 0) {
14
+ return "in-sync";
15
+ }
16
+ // eslint-disable-next-line no-console
17
+ console.log("This app is using custom fields, fetching the latest approved manifest to validate that the local custom field definitions are in sync.");
18
+ const tokenData = await (0, accessTokenCache_1.getCachedAccessToken)(settings);
19
+ const latestApprovedManifestIfAny = await fetchLatestApprovedManifest(irisAppManifest.moduleFederationName, tokenData.access_token, settings);
20
+ if (!latestApprovedManifestIfAny) {
21
+ return "not-submitted";
22
+ }
23
+ const approvedFields = latestApprovedManifestIfAny.customFieldDefinitions ?? [];
24
+ // eslint-disable-next-line no-console
25
+ console.log(`Latest approved manifest for ${irisAppManifest.moduleFederationName} has ${approvedFields.length} custom field definition(s).`);
26
+ if (hasCustomFieldDrift(localFields, approvedFields)) {
27
+ return "out-of-sync";
28
+ }
29
+ return "in-sync";
30
+ };
31
+ exports.checkCustomFields = checkCustomFields;
32
+ const stripTranslations = (field) => {
33
+ const { translations, ...rest } = field;
34
+ return rest;
35
+ };
36
+ /**
37
+ * Returns true when the local and approved custom field sets differ.
38
+ * The comparison is symmetric: added, removed, and modified fields are all detected.
39
+ * Translations are excluded from the comparison.
40
+ */
41
+ const hasCustomFieldDrift = (localFields, approvedFields) => {
42
+ if (localFields.length !== approvedFields.length) {
43
+ return true;
44
+ }
45
+ return localFields.some(local => {
46
+ const approved = approvedFields.find(a => a.key === local.key);
47
+ if (!approved) {
48
+ return true;
49
+ }
50
+ return !(0, deepEqual_1.deepEqual)(stripTranslations(local), stripTranslations(approved));
51
+ });
52
+ };
53
+ const LATEST_APPROVED_MANIFEST_QUERY = `query LatestApprovedManifest($irisAppId: String!) {
54
+ manifest(irisAppId: $irisAppId)
55
+ }`;
56
+ /**
57
+ * Fetches the latest approved public manifest for the given iris app id from the
58
+ * internal GraphQL endpoint. Returns null if the app is not subscribed on the
59
+ * caller's account or does not have an approved version yet.
60
+ *
61
+ * @param {string} irisAppId The iris app id, typically the manifest's moduleFederationName.
62
+ * @param {string} accessToken The Trackunit access token to authenticate the request.
63
+ * @param {Settings} settings The Iris App SDK settings.
64
+ * @returns {Promise<PublicIrisAppManifest | null>} The latest approved manifest or null.
65
+ */
66
+ async function fetchLatestApprovedManifest(irisAppId, accessToken, settings) {
67
+ const response = await fetch(settings.graphqlInternalUrl.toString(), {
68
+ method: "POST",
69
+ headers: {
70
+ "Content-Type": "application/json",
71
+ Authorization: `Bearer ${accessToken}`,
72
+ },
73
+ body: JSON.stringify({
74
+ operationName: "LatestApprovedManifest",
75
+ query: LATEST_APPROVED_MANIFEST_QUERY,
76
+ variables: { irisAppId },
77
+ }),
78
+ });
79
+ if (!response.ok) {
80
+ throw new Error(`Failed to fetch latest approved manifest: ${response.status} ${response.statusText} ${await response.text()}`);
81
+ }
82
+ const body = await response.json();
83
+ if (body.errors && body.errors.length > 0) {
84
+ throw new Error(`GraphQL error fetching latest approved manifest: ${body.errors.map(e => e.message).join(", ")}`);
85
+ }
86
+ return body.data?.manifest ?? null;
87
+ }
88
+ //# sourceMappingURL=checkCustomFields.js.map
@@ -0,0 +1,39 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.deepEqual = void 0;
4
+ /**
5
+ * Deeply compares two values for equality.
6
+ *
7
+ * @param a - The first value to compare.
8
+ * @param b - The second value to compare.
9
+ * @returns {boolean} - true if the values are equal, false otherwise.
10
+ */
11
+ const deepEqual = (a, b) => {
12
+ if (a === b) {
13
+ return true;
14
+ }
15
+ if (a === null || b === null || typeof a !== "object" || typeof b !== "object") {
16
+ return false;
17
+ }
18
+ if (Array.isArray(a) !== Array.isArray(b)) {
19
+ return false;
20
+ }
21
+ if (Array.isArray(a) && Array.isArray(b)) {
22
+ if (a.length !== b.length) {
23
+ return false;
24
+ }
25
+ return a.every((item, i) => (0, exports.deepEqual)(item, b[i]));
26
+ }
27
+ // eslint-disable-next-line @trackunit/no-typescript-assertion
28
+ const aObj = a;
29
+ // eslint-disable-next-line @trackunit/no-typescript-assertion
30
+ const bObj = b;
31
+ const aKeys = Object.keys(aObj);
32
+ const bKeys = Object.keys(bObj);
33
+ if (aKeys.length !== bKeys.length) {
34
+ return false;
35
+ }
36
+ return aKeys.every(key => (0, exports.deepEqual)(aObj[key], bObj[key]));
37
+ };
38
+ exports.deepEqual = deepEqual;
39
+ //# sourceMappingURL=deepEqual.js.map