@blinkdotnew/sdk 0.14.0 → 0.14.2

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/index.d.mts CHANGED
@@ -733,6 +733,7 @@ declare class HttpClient {
733
733
  /**
734
734
  * Blink Auth Module - Client-side authentication management
735
735
  * Handles token storage, user state, and authentication flows
736
+ * Includes iframe token relay support for seamless preview authentication
736
737
  */
737
738
 
738
739
  type AuthStateChangeCallback = (state: AuthState) => void;
@@ -741,7 +742,18 @@ declare class BlinkAuth {
741
742
  private authState;
742
743
  private listeners;
743
744
  private readonly authUrl;
745
+ private parentWindowTokens;
746
+ private isIframe;
747
+ private tokenRequestSent;
744
748
  constructor(config: BlinkClientConfig);
749
+ /**
750
+ * Setup listener for tokens from parent window (iframe only)
751
+ */
752
+ private setupParentWindowListener;
753
+ /**
754
+ * Request tokens from parent window
755
+ */
756
+ private requestTokensFromParent;
745
757
  /**
746
758
  * Initialize authentication from stored tokens or URL fragments
747
759
  */
package/dist/index.d.ts CHANGED
@@ -733,6 +733,7 @@ declare class HttpClient {
733
733
  /**
734
734
  * Blink Auth Module - Client-side authentication management
735
735
  * Handles token storage, user state, and authentication flows
736
+ * Includes iframe token relay support for seamless preview authentication
736
737
  */
737
738
 
738
739
  type AuthStateChangeCallback = (state: AuthState) => void;
@@ -741,7 +742,18 @@ declare class BlinkAuth {
741
742
  private authState;
742
743
  private listeners;
743
744
  private readonly authUrl;
745
+ private parentWindowTokens;
746
+ private isIframe;
747
+ private tokenRequestSent;
744
748
  constructor(config: BlinkClientConfig);
749
+ /**
750
+ * Setup listener for tokens from parent window (iframe only)
751
+ */
752
+ private setupParentWindowListener;
753
+ /**
754
+ * Request tokens from parent window
755
+ */
756
+ private requestTokensFromParent;
745
757
  /**
746
758
  * Initialize authentication from stored tokens or URL fragments
747
759
  */
package/dist/index.js CHANGED
@@ -867,6 +867,9 @@ var BlinkAuth = class {
867
867
  authState;
868
868
  listeners = /* @__PURE__ */ new Set();
869
869
  authUrl = "https://blink.new";
870
+ parentWindowTokens = null;
871
+ isIframe = false;
872
+ tokenRequestSent = false;
870
873
  constructor(config) {
871
874
  this.config = config;
872
875
  this.authState = {
@@ -876,9 +879,80 @@ var BlinkAuth = class {
876
879
  isLoading: false
877
880
  };
878
881
  if (typeof window !== "undefined") {
882
+ this.isIframe = window.self !== window.top;
883
+ if (this.isIframe) {
884
+ console.log("\u{1F5BC}\uFE0F Detected iframe environment, setting up parent window communication");
885
+ this.setupParentWindowListener();
886
+ }
879
887
  this.initialize();
880
888
  }
881
889
  }
890
+ /**
891
+ * Setup listener for tokens from parent window (iframe only)
892
+ */
893
+ setupParentWindowListener() {
894
+ if (!this.isIframe) return;
895
+ window.addEventListener("message", (event) => {
896
+ const trustedOrigins = [
897
+ "https://blink.new",
898
+ "http://localhost:3000",
899
+ "http://localhost:3001",
900
+ "https://localhost:3000",
901
+ "https://localhost:3001"
902
+ ];
903
+ if (!trustedOrigins.includes(event.origin)) {
904
+ return;
905
+ }
906
+ if (event.data?.type === "BLINK_AUTH_TOKENS") {
907
+ console.log("\u{1F4E5} Received auth tokens from parent window", {
908
+ hasTokens: !!event.data.tokens,
909
+ projectId: event.data.projectId
910
+ });
911
+ const { tokens, projectId } = event.data;
912
+ if (projectId && projectId !== this.config.projectId) {
913
+ console.log("\u26A0\uFE0F Ignoring tokens for different project:", projectId);
914
+ return;
915
+ }
916
+ if (tokens) {
917
+ this.parentWindowTokens = tokens;
918
+ this.setTokens(tokens, false).then(() => {
919
+ console.log("\u2705 Tokens from parent window applied successfully");
920
+ }).catch((error) => {
921
+ console.error("\u274C Failed to apply parent window tokens:", error);
922
+ });
923
+ }
924
+ }
925
+ if (event.data?.type === "BLINK_AUTH_LOGOUT") {
926
+ console.log("\u{1F4E4} Received logout command from parent window");
927
+ this.clearTokens();
928
+ }
929
+ if (event.data?.type === "BLINK_AUTH_REFRESH") {
930
+ console.log("\u{1F504} Received token refresh from parent window");
931
+ const { tokens } = event.data;
932
+ if (tokens) {
933
+ this.parentWindowTokens = tokens;
934
+ this.setTokens(tokens, false).catch((error) => {
935
+ console.error("\u274C Failed to apply refreshed tokens:", error);
936
+ });
937
+ }
938
+ }
939
+ });
940
+ this.requestTokensFromParent();
941
+ }
942
+ /**
943
+ * Request tokens from parent window
944
+ */
945
+ requestTokensFromParent() {
946
+ if (!this.isIframe || this.tokenRequestSent) return;
947
+ if (window.parent !== window) {
948
+ console.log("\u{1F504} Requesting auth tokens from parent window");
949
+ window.parent.postMessage({
950
+ type: "BLINK_REQUEST_AUTH_TOKENS",
951
+ projectId: this.config.projectId
952
+ }, "*");
953
+ this.tokenRequestSent = true;
954
+ }
955
+ }
882
956
  /**
883
957
  * Initialize authentication from stored tokens or URL fragments
884
958
  */
@@ -886,6 +960,18 @@ var BlinkAuth = class {
886
960
  console.log("\u{1F680} Initializing Blink Auth...");
887
961
  this.setLoading(true);
888
962
  try {
963
+ if (this.isIframe) {
964
+ console.log("\u{1F50D} Detected iframe environment, waiting for parent tokens...");
965
+ for (let i = 0; i < 10; i++) {
966
+ if (this.parentWindowTokens) {
967
+ console.log("\u2705 Using tokens from parent window");
968
+ await this.setTokens(this.parentWindowTokens, false);
969
+ return;
970
+ }
971
+ await new Promise((resolve) => setTimeout(resolve, 100));
972
+ }
973
+ console.log("\u23F0 Timeout waiting for parent tokens, continuing with normal flow...");
974
+ }
889
975
  const tokensFromUrl = this.extractTokensFromUrl();
890
976
  if (tokensFromUrl) {
891
977
  console.log("\u{1F4E5} Found tokens in URL, setting them...");
@@ -917,6 +1003,14 @@ var BlinkAuth = class {
917
1003
  }
918
1004
  console.log("\u274C No tokens found");
919
1005
  if (this.config.authRequired) {
1006
+ if (this.isIframe && !this.tokenRequestSent) {
1007
+ this.requestTokensFromParent();
1008
+ await new Promise((resolve) => setTimeout(resolve, 500));
1009
+ if (this.parentWindowTokens) {
1010
+ await this.setTokens(this.parentWindowTokens, false);
1011
+ return;
1012
+ }
1013
+ }
920
1014
  console.log("\u{1F504} Auth required, redirecting to auth page...");
921
1015
  this.redirectToAuth();
922
1016
  } else {
@@ -930,7 +1024,17 @@ var BlinkAuth = class {
930
1024
  * Redirect to Blink auth page
931
1025
  */
932
1026
  login(nextUrl) {
933
- const redirectUrl = nextUrl || (typeof window !== "undefined" ? window.location.href : "");
1027
+ let redirectUrl = nextUrl || (typeof window !== "undefined" ? window.location.href : "");
1028
+ if (redirectUrl && typeof window !== "undefined") {
1029
+ try {
1030
+ const url = new URL(redirectUrl);
1031
+ url.searchParams.delete("redirect_url");
1032
+ url.searchParams.delete("redirect");
1033
+ redirectUrl = url.toString();
1034
+ } catch (e) {
1035
+ console.warn("Failed to parse redirect URL:", e);
1036
+ }
1037
+ }
934
1038
  const authUrl = new URL("/auth", this.authUrl);
935
1039
  authUrl.searchParams.set("redirect_url", redirectUrl);
936
1040
  if (this.config.projectId) {
@@ -945,6 +1049,12 @@ var BlinkAuth = class {
945
1049
  */
946
1050
  logout(redirectUrl) {
947
1051
  this.clearTokens();
1052
+ if (this.isIframe && window.parent !== window) {
1053
+ window.parent.postMessage({
1054
+ type: "BLINK_AUTH_LOGOUT_IFRAME",
1055
+ projectId: this.config.projectId
1056
+ }, "*");
1057
+ }
948
1058
  if (redirectUrl && typeof window !== "undefined") {
949
1059
  window.location.href = redirectUrl;
950
1060
  }
@@ -1163,7 +1273,7 @@ var BlinkAuth = class {
1163
1273
  token_type: data.token_type,
1164
1274
  expires_in: data.expires_in,
1165
1275
  refresh_expires_in: data.refresh_expires_in
1166
- }, true);
1276
+ }, !this.isIframe);
1167
1277
  return true;
1168
1278
  } catch (error) {
1169
1279
  console.error("Token refresh failed:", error);
@@ -1240,7 +1350,7 @@ var BlinkAuth = class {
1240
1350
  return false;
1241
1351
  }
1242
1352
  } catch (error) {
1243
- console.log("\u{1F4A5} Error validating tokens:", error);
1353
+ console.log("\uFFFD\uFFFD Error validating tokens:", error);
1244
1354
  return false;
1245
1355
  }
1246
1356
  }
@@ -1254,11 +1364,19 @@ var BlinkAuth = class {
1254
1364
  hasAccessToken: !!tokensWithTimestamp.access_token,
1255
1365
  hasRefreshToken: !!tokensWithTimestamp.refresh_token,
1256
1366
  expiresIn: tokensWithTimestamp.expires_in,
1257
- issuedAt: tokensWithTimestamp.issued_at
1367
+ issuedAt: tokensWithTimestamp.issued_at,
1368
+ isIframe: this.isIframe
1258
1369
  });
1259
- if (persist && typeof window !== "undefined") {
1260
- localStorage.setItem("blink_tokens", JSON.stringify(tokensWithTimestamp));
1261
- console.log("\u{1F4BE} Tokens persisted to localStorage");
1370
+ if (persist && !this.isIframe && typeof window !== "undefined") {
1371
+ try {
1372
+ localStorage.setItem("blink_tokens", JSON.stringify(tokensWithTimestamp));
1373
+ console.log("\u{1F4BE} Tokens persisted to localStorage");
1374
+ } catch (error) {
1375
+ console.log("\u{1F4A5} Error persisting tokens to localStorage:", error);
1376
+ if (error instanceof DOMException && error.name === "SecurityError") {
1377
+ console.log("\u{1F6AB} localStorage access blocked - running in cross-origin iframe");
1378
+ }
1379
+ }
1262
1380
  }
1263
1381
  let user = null;
1264
1382
  try {
@@ -1300,8 +1418,13 @@ var BlinkAuth = class {
1300
1418
  });
1301
1419
  }
1302
1420
  clearTokens() {
1421
+ this.parentWindowTokens = null;
1303
1422
  if (typeof window !== "undefined") {
1304
- localStorage.removeItem("blink_tokens");
1423
+ try {
1424
+ localStorage.removeItem("blink_tokens");
1425
+ } catch (error) {
1426
+ console.log("\u{1F4A5} Error clearing tokens from localStorage:", error);
1427
+ }
1305
1428
  }
1306
1429
  this.updateAuthState({
1307
1430
  user: null,
@@ -1312,11 +1435,17 @@ var BlinkAuth = class {
1312
1435
  }
1313
1436
  getStoredTokens() {
1314
1437
  if (typeof window === "undefined") return null;
1438
+ if (this.isIframe && this.parentWindowTokens) {
1439
+ console.log("\u{1F4E6} Using parent window tokens");
1440
+ return this.parentWindowTokens;
1441
+ }
1315
1442
  try {
1316
1443
  const stored = localStorage.getItem("blink_tokens");
1317
1444
  console.log("\u{1F50D} Checking localStorage for tokens:", {
1318
1445
  hasStoredData: !!stored,
1319
- storedLength: stored?.length || 0
1446
+ storedLength: stored?.length || 0,
1447
+ origin: window.location.origin,
1448
+ isIframe: this.isIframe
1320
1449
  });
1321
1450
  if (stored) {
1322
1451
  const tokens = JSON.parse(stored);
@@ -1330,8 +1459,10 @@ var BlinkAuth = class {
1330
1459
  }
1331
1460
  return null;
1332
1461
  } catch (error) {
1333
- console.log("\u{1F4A5} Error parsing stored tokens:", error);
1334
- localStorage.removeItem("blink_tokens");
1462
+ console.log("\u{1F4A5} Error accessing localStorage:", error);
1463
+ if (error instanceof DOMException && error.name === "SecurityError") {
1464
+ console.log("\u{1F6AB} localStorage access blocked - likely due to cross-origin iframe restrictions");
1465
+ }
1335
1466
  return null;
1336
1467
  }
1337
1468
  }
package/dist/index.mjs CHANGED
@@ -865,6 +865,9 @@ var BlinkAuth = class {
865
865
  authState;
866
866
  listeners = /* @__PURE__ */ new Set();
867
867
  authUrl = "https://blink.new";
868
+ parentWindowTokens = null;
869
+ isIframe = false;
870
+ tokenRequestSent = false;
868
871
  constructor(config) {
869
872
  this.config = config;
870
873
  this.authState = {
@@ -874,9 +877,80 @@ var BlinkAuth = class {
874
877
  isLoading: false
875
878
  };
876
879
  if (typeof window !== "undefined") {
880
+ this.isIframe = window.self !== window.top;
881
+ if (this.isIframe) {
882
+ console.log("\u{1F5BC}\uFE0F Detected iframe environment, setting up parent window communication");
883
+ this.setupParentWindowListener();
884
+ }
877
885
  this.initialize();
878
886
  }
879
887
  }
888
+ /**
889
+ * Setup listener for tokens from parent window (iframe only)
890
+ */
891
+ setupParentWindowListener() {
892
+ if (!this.isIframe) return;
893
+ window.addEventListener("message", (event) => {
894
+ const trustedOrigins = [
895
+ "https://blink.new",
896
+ "http://localhost:3000",
897
+ "http://localhost:3001",
898
+ "https://localhost:3000",
899
+ "https://localhost:3001"
900
+ ];
901
+ if (!trustedOrigins.includes(event.origin)) {
902
+ return;
903
+ }
904
+ if (event.data?.type === "BLINK_AUTH_TOKENS") {
905
+ console.log("\u{1F4E5} Received auth tokens from parent window", {
906
+ hasTokens: !!event.data.tokens,
907
+ projectId: event.data.projectId
908
+ });
909
+ const { tokens, projectId } = event.data;
910
+ if (projectId && projectId !== this.config.projectId) {
911
+ console.log("\u26A0\uFE0F Ignoring tokens for different project:", projectId);
912
+ return;
913
+ }
914
+ if (tokens) {
915
+ this.parentWindowTokens = tokens;
916
+ this.setTokens(tokens, false).then(() => {
917
+ console.log("\u2705 Tokens from parent window applied successfully");
918
+ }).catch((error) => {
919
+ console.error("\u274C Failed to apply parent window tokens:", error);
920
+ });
921
+ }
922
+ }
923
+ if (event.data?.type === "BLINK_AUTH_LOGOUT") {
924
+ console.log("\u{1F4E4} Received logout command from parent window");
925
+ this.clearTokens();
926
+ }
927
+ if (event.data?.type === "BLINK_AUTH_REFRESH") {
928
+ console.log("\u{1F504} Received token refresh from parent window");
929
+ const { tokens } = event.data;
930
+ if (tokens) {
931
+ this.parentWindowTokens = tokens;
932
+ this.setTokens(tokens, false).catch((error) => {
933
+ console.error("\u274C Failed to apply refreshed tokens:", error);
934
+ });
935
+ }
936
+ }
937
+ });
938
+ this.requestTokensFromParent();
939
+ }
940
+ /**
941
+ * Request tokens from parent window
942
+ */
943
+ requestTokensFromParent() {
944
+ if (!this.isIframe || this.tokenRequestSent) return;
945
+ if (window.parent !== window) {
946
+ console.log("\u{1F504} Requesting auth tokens from parent window");
947
+ window.parent.postMessage({
948
+ type: "BLINK_REQUEST_AUTH_TOKENS",
949
+ projectId: this.config.projectId
950
+ }, "*");
951
+ this.tokenRequestSent = true;
952
+ }
953
+ }
880
954
  /**
881
955
  * Initialize authentication from stored tokens or URL fragments
882
956
  */
@@ -884,6 +958,18 @@ var BlinkAuth = class {
884
958
  console.log("\u{1F680} Initializing Blink Auth...");
885
959
  this.setLoading(true);
886
960
  try {
961
+ if (this.isIframe) {
962
+ console.log("\u{1F50D} Detected iframe environment, waiting for parent tokens...");
963
+ for (let i = 0; i < 10; i++) {
964
+ if (this.parentWindowTokens) {
965
+ console.log("\u2705 Using tokens from parent window");
966
+ await this.setTokens(this.parentWindowTokens, false);
967
+ return;
968
+ }
969
+ await new Promise((resolve) => setTimeout(resolve, 100));
970
+ }
971
+ console.log("\u23F0 Timeout waiting for parent tokens, continuing with normal flow...");
972
+ }
887
973
  const tokensFromUrl = this.extractTokensFromUrl();
888
974
  if (tokensFromUrl) {
889
975
  console.log("\u{1F4E5} Found tokens in URL, setting them...");
@@ -915,6 +1001,14 @@ var BlinkAuth = class {
915
1001
  }
916
1002
  console.log("\u274C No tokens found");
917
1003
  if (this.config.authRequired) {
1004
+ if (this.isIframe && !this.tokenRequestSent) {
1005
+ this.requestTokensFromParent();
1006
+ await new Promise((resolve) => setTimeout(resolve, 500));
1007
+ if (this.parentWindowTokens) {
1008
+ await this.setTokens(this.parentWindowTokens, false);
1009
+ return;
1010
+ }
1011
+ }
918
1012
  console.log("\u{1F504} Auth required, redirecting to auth page...");
919
1013
  this.redirectToAuth();
920
1014
  } else {
@@ -928,7 +1022,17 @@ var BlinkAuth = class {
928
1022
  * Redirect to Blink auth page
929
1023
  */
930
1024
  login(nextUrl) {
931
- const redirectUrl = nextUrl || (typeof window !== "undefined" ? window.location.href : "");
1025
+ let redirectUrl = nextUrl || (typeof window !== "undefined" ? window.location.href : "");
1026
+ if (redirectUrl && typeof window !== "undefined") {
1027
+ try {
1028
+ const url = new URL(redirectUrl);
1029
+ url.searchParams.delete("redirect_url");
1030
+ url.searchParams.delete("redirect");
1031
+ redirectUrl = url.toString();
1032
+ } catch (e) {
1033
+ console.warn("Failed to parse redirect URL:", e);
1034
+ }
1035
+ }
932
1036
  const authUrl = new URL("/auth", this.authUrl);
933
1037
  authUrl.searchParams.set("redirect_url", redirectUrl);
934
1038
  if (this.config.projectId) {
@@ -943,6 +1047,12 @@ var BlinkAuth = class {
943
1047
  */
944
1048
  logout(redirectUrl) {
945
1049
  this.clearTokens();
1050
+ if (this.isIframe && window.parent !== window) {
1051
+ window.parent.postMessage({
1052
+ type: "BLINK_AUTH_LOGOUT_IFRAME",
1053
+ projectId: this.config.projectId
1054
+ }, "*");
1055
+ }
946
1056
  if (redirectUrl && typeof window !== "undefined") {
947
1057
  window.location.href = redirectUrl;
948
1058
  }
@@ -1161,7 +1271,7 @@ var BlinkAuth = class {
1161
1271
  token_type: data.token_type,
1162
1272
  expires_in: data.expires_in,
1163
1273
  refresh_expires_in: data.refresh_expires_in
1164
- }, true);
1274
+ }, !this.isIframe);
1165
1275
  return true;
1166
1276
  } catch (error) {
1167
1277
  console.error("Token refresh failed:", error);
@@ -1238,7 +1348,7 @@ var BlinkAuth = class {
1238
1348
  return false;
1239
1349
  }
1240
1350
  } catch (error) {
1241
- console.log("\u{1F4A5} Error validating tokens:", error);
1351
+ console.log("\uFFFD\uFFFD Error validating tokens:", error);
1242
1352
  return false;
1243
1353
  }
1244
1354
  }
@@ -1252,11 +1362,19 @@ var BlinkAuth = class {
1252
1362
  hasAccessToken: !!tokensWithTimestamp.access_token,
1253
1363
  hasRefreshToken: !!tokensWithTimestamp.refresh_token,
1254
1364
  expiresIn: tokensWithTimestamp.expires_in,
1255
- issuedAt: tokensWithTimestamp.issued_at
1365
+ issuedAt: tokensWithTimestamp.issued_at,
1366
+ isIframe: this.isIframe
1256
1367
  });
1257
- if (persist && typeof window !== "undefined") {
1258
- localStorage.setItem("blink_tokens", JSON.stringify(tokensWithTimestamp));
1259
- console.log("\u{1F4BE} Tokens persisted to localStorage");
1368
+ if (persist && !this.isIframe && typeof window !== "undefined") {
1369
+ try {
1370
+ localStorage.setItem("blink_tokens", JSON.stringify(tokensWithTimestamp));
1371
+ console.log("\u{1F4BE} Tokens persisted to localStorage");
1372
+ } catch (error) {
1373
+ console.log("\u{1F4A5} Error persisting tokens to localStorage:", error);
1374
+ if (error instanceof DOMException && error.name === "SecurityError") {
1375
+ console.log("\u{1F6AB} localStorage access blocked - running in cross-origin iframe");
1376
+ }
1377
+ }
1260
1378
  }
1261
1379
  let user = null;
1262
1380
  try {
@@ -1298,8 +1416,13 @@ var BlinkAuth = class {
1298
1416
  });
1299
1417
  }
1300
1418
  clearTokens() {
1419
+ this.parentWindowTokens = null;
1301
1420
  if (typeof window !== "undefined") {
1302
- localStorage.removeItem("blink_tokens");
1421
+ try {
1422
+ localStorage.removeItem("blink_tokens");
1423
+ } catch (error) {
1424
+ console.log("\u{1F4A5} Error clearing tokens from localStorage:", error);
1425
+ }
1303
1426
  }
1304
1427
  this.updateAuthState({
1305
1428
  user: null,
@@ -1310,11 +1433,17 @@ var BlinkAuth = class {
1310
1433
  }
1311
1434
  getStoredTokens() {
1312
1435
  if (typeof window === "undefined") return null;
1436
+ if (this.isIframe && this.parentWindowTokens) {
1437
+ console.log("\u{1F4E6} Using parent window tokens");
1438
+ return this.parentWindowTokens;
1439
+ }
1313
1440
  try {
1314
1441
  const stored = localStorage.getItem("blink_tokens");
1315
1442
  console.log("\u{1F50D} Checking localStorage for tokens:", {
1316
1443
  hasStoredData: !!stored,
1317
- storedLength: stored?.length || 0
1444
+ storedLength: stored?.length || 0,
1445
+ origin: window.location.origin,
1446
+ isIframe: this.isIframe
1318
1447
  });
1319
1448
  if (stored) {
1320
1449
  const tokens = JSON.parse(stored);
@@ -1328,8 +1457,10 @@ var BlinkAuth = class {
1328
1457
  }
1329
1458
  return null;
1330
1459
  } catch (error) {
1331
- console.log("\u{1F4A5} Error parsing stored tokens:", error);
1332
- localStorage.removeItem("blink_tokens");
1460
+ console.log("\u{1F4A5} Error accessing localStorage:", error);
1461
+ if (error instanceof DOMException && error.name === "SecurityError") {
1462
+ console.log("\u{1F6AB} localStorage access blocked - likely due to cross-origin iframe restrictions");
1463
+ }
1333
1464
  return null;
1334
1465
  }
1335
1466
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blinkdotnew/sdk",
3
- "version": "0.14.0",
3
+ "version": "0.14.2",
4
4
  "description": "Blink TypeScript SDK for client-side applications - Zero-boilerplate CRUD + auth + AI + analytics + notifications for modern SaaS/AI apps",
5
5
  "keywords": [
6
6
  "blink",