diff --git a/.github/workflows/web-app-ci.yaml b/.github/workflows/web-app-ci.yaml index 0cc9af5a..e8538787 100644 --- a/.github/workflows/web-app-ci.yaml +++ b/.github/workflows/web-app-ci.yaml @@ -72,7 +72,7 @@ jobs: DOCKER_TAG: indexnetwork/web3-web-app:${{ steps.build-time.outputs.time }} DOCKER_REGISTRY: 236785930124.dkr.ecr.us-east-1.amazonaws.com run: | - docker build --build-arg INFURA_API_KEY=${{ secrets.INFURA_API_KEY }} --build-arg API_URL=${API_URL} -t $DOCKER_TAG . + docker build --build-arg INFURA_API_KEY=${{ secrets.INFURA_API_KEY }} --build-arg API_URL=${API_URL} --build-arg MAGICBELL_API_KEY=${{ secrets.MAGICBELL_API_KEY }} -t $DOCKER_TAG . docker tag $DOCKER_TAG $DOCKER_REGISTRY/$DOCKER_TAG docker push $DOCKER_REGISTRY/$DOCKER_TAG docker tag $DOCKER_TAG $DOCKER_REGISTRY/indexnetwork/web3-web-app:latest-${GITHUB_REF#refs/heads/} diff --git a/api/package.json b/api/package.json index 94c97fc6..0d78067a 100644 --- a/api/package.json +++ b/api/package.json @@ -34,6 +34,7 @@ "codeco": "^1.2.1", "cookie-parser": "~1.4.6", "cross-eventsource": "^1.0.0", + "crypto": "^1.0.1", "debug": "~2.6.9", "did-session": "^2.1.2", "dotenv": "^16.0.3", diff --git a/api/src/agents/basic_subscriber.js b/api/src/agents/basic_subscriber.js index 2b855536..a5254c04 100644 --- a/api/src/agents/basic_subscriber.js +++ b/api/src/agents/basic_subscriber.js @@ -1,6 +1,7 @@ import axios from "axios"; import { getAgentDID } from "../utils/helpers.js"; import { ConversationService } from "../services/conversation.js"; +import { DIDService } from "../services/did.js"; export const handleNewItemEvent = async ( chatId, @@ -53,6 +54,7 @@ export const handleNewItemEvent = async ( role: "assistant", name: "listener", }); + await redisClient.publish( `agentStream:${chatId}:update`, JSON.stringify({ @@ -61,6 +63,35 @@ export const handleNewItemEvent = async ( messageId: assistantMessage.id, }), ); + + const didService = new DIDService(definition); + + const conversation = await conversationService.getConversation(chatId); + const recipients = await Promise.all( + conversation.members + .filter((memberId) => memberId.id !== agentDID.id) + .map(async (memberId) => { + const externalId = await didService.getControllerDIDByEncryptionDID(memberId.id); + console.log('External ID:', externalId); + return { + external_id: externalId + }; + }) + ); + await axios.post('https://api.magicbell.com/broadcasts', { + broadcast: { + title: conversation.summary, + content: resp.data, + recipients + } + }, { + headers: { + 'X-MAGICBELL-API-KEY': process.env.MAGICBELL_API_KEY, + 'X-MAGICBELL-API-SECRET': process.env.MAGICBELL_API_SECRET, + 'Content-Type': 'application/json' + } + }) + await redisClient.hSet( `subscriptions`, chatId, diff --git a/api/src/controllers/did.js b/api/src/controllers/did.js index fe4fc74e..b5cfe72d 100644 --- a/api/src/controllers/did.js +++ b/api/src/controllers/did.js @@ -1,4 +1,8 @@ import { DIDService } from "../services/did.js"; +import crypto from 'crypto'; +import axios from "axios"; + + export const getIndexes = async (req, res) => { // sendLit(req.params.id) // TODO Fix later. const definition = req.app.get("runtimeDefinition"); @@ -85,6 +89,21 @@ export const getProfileFromSession = async (req, res, next) => { id: req.params.did, }; } + + profile.hmac = crypto.createHmac('sha256', process.env.MAGICBELL_API_SECRET) + .update(req.session.did.parent) + .digest('base64'); + + await axios.post('https://api.magicbell.com/users', { + user: { + external_id: req.session.did.parent + } + }, { + headers: { + 'X-MAGICBELL-API-KEY': process.env.MAGICBELL_API_KEY, + 'X-MAGICBELL-API-SECRET': process.env.MAGICBELL_API_SECRET + } + }); res.status(200).json(profile); } catch (error) { diff --git a/api/src/services/did.js b/api/src/services/did.js index 079ce4b5..4b152259 100644 --- a/api/src/services/did.js +++ b/api/src/services/did.js @@ -351,6 +351,50 @@ export class DIDService { throw error; } } + + async getControllerDIDByEncryptionDID(userEncryptionDID) { + try { + const query = ` + query { + publicEncryptionDIDIndex(first: 10, filters: { + where: { + publicEncryptionDID: { + equalTo: "${userEncryptionDID}" + } + } + }) { + edges { + node { + id + controllerDID { + id + } + publicEncryptionDID { + id + } + } + } + } + } + `; + + const { data, errors } = await this.client.executeQuery(query); + + if (errors) { + throw new Error(`Error getting controller DID: ${JSON.stringify(errors)}`); + } + + if (!data || !data.publicEncryptionDIDIndex || data.publicEncryptionDIDIndex.edges.length === 0) { + throw new Error("Controller DID not found"); + } + + return data.publicEncryptionDIDIndex.edges[0].node.controllerDID.id; + } catch (error) { + console.error("Exception occurred in getControllerDIDByEncryptionDID:", error); + throw error; + } + } + async publicEncryptionDID() { if (!this.did) { throw new Error("DID not set. Use setDID() to set the did."); diff --git a/api/yarn.lock b/api/yarn.lock index 6f917ed8..774b9b7c 100644 --- a/api/yarn.lock +++ b/api/yarn.lock @@ -5166,6 +5166,11 @@ crossws@^0.2.0, crossws@^0.2.2: resolved "https://registry.yarnpkg.com/crossws/-/crossws-0.2.4.tgz#82a8b518bff1018ab1d21ced9e35ffbe1681ad03" integrity sha512-DAxroI2uSOgUKLz00NX6A8U/8EE3SZHmIND+10jkVSaypvyt57J5JEOxAQOL6lQxyzi/wZbTIwssU1uy69h5Vg== +crypto@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/crypto/-/crypto-1.0.1.tgz#2af1b7cad8175d24c8a1b0778255794a21803037" + integrity sha512-VxBKmeNcqQdiUQUW2Tzq0t377b54N2bMtXO/qiLa+6eRRmmC4qT3D4OnTGoT/U6O9aklQ/jTwbOtRMTTY8G0Ig== + css-parse@1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/css-parse/-/css-parse-1.0.4.tgz#38b0503fbf9da9f54e9c1dbda60e145c77117bdd" diff --git a/web-app/Dockerfile b/web-app/Dockerfile index 2a95ac0c..5d42b497 100644 --- a/web-app/Dockerfile +++ b/web-app/Dockerfile @@ -7,6 +7,10 @@ ENV NEXT_PUBLIC_API_URL=$API_URL ARG INFURA_API_KEY ENV INFURA_API_KEY=$INFURA_API_KEY + +ARG MAGICBELL_API_KEY +ENV MAGICBELL_API_KEY=$MAGICBELL_API_KEY + COPY . . RUN yarn RUN yarn build diff --git a/web-app/package.json b/web-app/package.json index d0ebd39b..4aa8b6b5 100644 --- a/web-app/package.json +++ b/web-app/package.json @@ -29,6 +29,8 @@ "@lit-protocol/contracts-sdk": "4.1.1", "@lit-protocol/lit-node-client": "4.1.1", "@lit-protocol/uint8arrays": "4.1.1", + "@magicbell/user-client": "^0.2.0", + "@magicbell/webpush": "^2.0.2", "@metamask/sdk-react": "^0.28.0", "@nanostores/react": "ai/react", "@radix-ui/react-alert-dialog": "^1.0.4", @@ -60,6 +62,7 @@ "lodash.debounce": "^4.0.8", "lottie-react": "^2.3.1", "lottie-react-web": "^2.2.2", + "magicbell": "^3.3.0", "moment": "^2.29.3", "multiformats": "^12.1.1", "nanoid": "^5.0.7", diff --git a/web-app/public/manifest.json b/web-app/public/manifest.json index 8d598e3b..a966cfa9 100644 --- a/web-app/public/manifest.json +++ b/web-app/public/manifest.json @@ -1,21 +1,22 @@ { - "name": "Index Network", - "short_name": "Index", - "start_url": "/", - "display": "standalone", - "background_color": "#black", - "theme_color": "#90cdf4", - "orientation": "portrait-primary", - "icons": [ - { - "src": "favicon-white.png", - "sizes": "500x500", - "type": "image/png" - }, - { - "src": "favicon-white.png", - "sizes": "192x192", - "type": "image/png" - } - ] - } \ No newline at end of file + "name":"Index Network", + "short_name":"Index", + "start_url":"/notifications", + "display":"standalone", + "background_color":"black", + "theme_color":"black", + "orientation":"portrait-primary", + "icons":[ + { + "src":"favicon-white.png", + "sizes":"500x500", + "type":"image/png" + }, + { + "src":"favicon-white.png", + "sizes":"192x192", + "type":"image/png" + } + ], + "permissions": ["notifications"] +} \ No newline at end of file diff --git a/web-app/public/sw.js b/web-app/public/sw.js new file mode 100644 index 00000000..b90f7208 --- /dev/null +++ b/web-app/public/sw.js @@ -0,0 +1,2 @@ + +importScripts('https://assets.magicbell.io/web-push-notifications/sw.js'); \ No newline at end of file diff --git a/web-app/src/app/layout.tsx b/web-app/src/app/layout.tsx index 530877cf..7833e18e 100644 --- a/web-app/src/app/layout.tsx +++ b/web-app/src/app/layout.tsx @@ -8,6 +8,7 @@ const inter = Inter({ subsets: ["latin"] }); export const metadata: Metadata = { metadataBase: new URL("https://index.network"), title: "Index Network | Discovery Protocol", + manifest: "/manifest.json", description: "Index allows you to create truly personalised and autonomous discovery experiences across the web", referrer: "origin-when-cross-origin", diff --git a/web-app/src/app/notifications/page.tsx b/web-app/src/app/notifications/page.tsx new file mode 100644 index 00000000..887a7b41 --- /dev/null +++ b/web-app/src/app/notifications/page.tsx @@ -0,0 +1,96 @@ +"use client"; + +import type { NextPage } from "next"; +import { useState, useEffect } from "react"; +import { WebPushClient } from "@magicbell/webpush"; +import Head from "next/head"; +import { useApi } from "@/context/APIContext"; +import { useAuth } from "@/context/AuthContext"; + +const Notifications: NextPage = () => { + const [client, setClient] = useState(null); + const [isSubscribed, setIsSubscribed] = useState(false); + const { api: apiService, ready: apiReady } = useApi(); + const { session, userDID } = useAuth(); + useEffect(() => { + if (!apiReady) return; + if (!session) return; + + const initializeWebPushClient = async () => { + if ("serviceWorker" in navigator) { + try { + // Service Worker'ı kaydet + const registration = await navigator.serviceWorker.register("/sw.js"); + console.log("Service Worker registered successfully:", registration); + + // Service Worker'ın aktif olmasını bekle + await navigator.serviceWorker.ready; + console.log("Service Worker is now ready"); + + const profile = await apiService?.getCurrentProfile(); + if (!profile) { + return; + } + + console.log(profile); + + const webPushClient = new WebPushClient({ + apiKey: process.env.MAGICBELL_API_KEY || "", + userExternalId: profile?.id, + userHmac: profile.hmac!, + serviceWorkerPath: "/sw.js", + }); + + setClient(webPushClient); + + const subscribed = await webPushClient.isSubscribed(); + setIsSubscribed(subscribed); + } catch (error) { + console.error("Error initializing WebPush:", error); + } + } else { + console.error("Service Workers are not supported in this browser"); + } + }; + + initializeWebPushClient(); + }, [session, apiReady]); + + const handleSubscribe = async () => { + if (client) { + try { + await client.subscribe(); + setIsSubscribed(true); + } catch (error) { + console.error("Subscription failed:", error); + } + } + }; + + const handleUnsubscribe = async () => { + if (client) { + try { + await client.unsubscribe(); + setIsSubscribed(false); + } catch (error) { + console.error("Unsubscription failed:", error); + } + } + }; + + return ( +
+

Web Push Notifications

+ {isSubscribed ? ( + + ) : ( + + )} + + + +
+ ); +}; + +export default Notifications; diff --git a/web-app/src/services/api-service-new.ts b/web-app/src/services/api-service-new.ts index 927e9fe5..d2ee3669 100644 --- a/web-app/src/services/api-service-new.ts +++ b/web-app/src/services/api-service-new.ts @@ -15,6 +15,7 @@ const API_ENDPOINTS = { STAR_INDEX: "/dids/:did/indexes/:indexId/star", OWN_INDEX: "/dids/:did/indexes/:indexId/own", GET_PROFILE: "/dids/:did/profile", + GET_CURRENT_PROFILE: "/profile", UPDATE_PROFILE: "/profile", UPLOAD_AVATAR: "/profile/upload_avatar", GET_ITEMS: "/indexes/:indexId/items", @@ -101,6 +102,12 @@ class ApiService { return data; } + async getCurrentProfile(): Promise { + const url = API_ENDPOINTS.GET_CURRENT_PROFILE; + const { data } = await this.apiAxios.get(url); + return data; + } + async updateProfile(params: Partial): Promise { const url = API_ENDPOINTS.UPDATE_PROFILE; const { data } = await this.apiAxios.patch(url, params); diff --git a/web-app/src/types/entity.ts b/web-app/src/types/entity.ts index d0217d56..5b223d11 100644 --- a/web-app/src/types/entity.ts +++ b/web-app/src/types/entity.ts @@ -218,6 +218,7 @@ export interface Users { name?: string; bio?: string; avatar?: string; + hmac?: string; createdAt?: string; updatedAt?: string; } diff --git a/web-app/yarn.lock b/web-app/yarn.lock index 351eef5d..f9b1f2be 100644 --- a/web-app/yarn.lock +++ b/web-app/yarn.lock @@ -127,7 +127,7 @@ dependencies: regenerator-runtime "^0.14.0" -"@babel/runtime@^7.19.4", "@babel/runtime@^7.21.0": +"@babel/runtime@^7.18.3", "@babel/runtime@^7.19.4", "@babel/runtime@^7.21.0": version "7.25.6" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.25.6.tgz#9afc3289f7184d8d7f98b099884c26317b9264d2" integrity sha512-VBj9MYyDb9tuLq7yzqjgzt6Q+IBQLrGZfdjOekyEirZPHxXWoTSGUTMrpsfi58Up73d13NfYLv8HT9vmznjzhQ== @@ -2082,6 +2082,18 @@ dependencies: "@lit-labs/ssr-dom-shim" "^1.0.0" +"@magicbell/user-client@^0.2.0": + version "0.2.0" + resolved "https://registry.yarnpkg.com/@magicbell/user-client/-/user-client-0.2.0.tgz#17b0da72aa3032e25a202dd804bcdacb1b8acf61" + integrity sha512-TWL9tn50ZetppLeBKzamKvI/OEGa25Uq3GYuMru9niWd2Zu79am57dIO2IX0VLDmmVu4x9PNGEcYrSiv7XSn1Q== + dependencies: + axios "^0.28.0" + +"@magicbell/webpush@^2.0.2": + version "2.0.2" + resolved "https://registry.yarnpkg.com/@magicbell/webpush/-/webpush-2.0.2.tgz#d1aabfea290f1025e6013932a678b1f55b9acbf5" + integrity sha512-f5v8s6opqIou5Z/JRinmv9GZZXSawx3yZZ7zMdjdtG8DiAM0N2o/sruOUZY9bQoGtWxmpnBoHDnK0/t1LTg+NA== + "@metamask/json-rpc-engine@^8.0.1", "@metamask/json-rpc-engine@^8.0.2": version "8.0.2" resolved "https://registry.yarnpkg.com/@metamask/json-rpc-engine/-/json-rpc-engine-8.0.2.tgz#29510a871a8edef892f838ee854db18de0bf0d14" @@ -4175,6 +4187,15 @@ axe-core@=4.7.0: resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.7.0.tgz#34ba5a48a8b564f67e103f0aa5768d76e15bbbbf" integrity sha512-M0JtH+hlOL5pLQwHOLNYZaXuhqmvS8oExsqB1SBYgA4Dk7u/xx+YdGHXaK5pyUfed5mYXdlYiphWq3G8cRi5JQ== +axios@^0.28.0: + version "0.28.1" + resolved "https://registry.yarnpkg.com/axios/-/axios-0.28.1.tgz#2a7bcd34a3837b71ee1a5ca3762214b86b703e70" + integrity sha512-iUcGA5a7p0mVb4Gm/sy+FSECNkPFT4y7wt6OM/CDpO/OnNCvSs3PoMG8ibrC9jRoGYU0gUK5pXVC4NPXq6lHRQ== + dependencies: + follow-redirects "^1.15.0" + form-data "^4.0.0" + proxy-from-env "^1.1.0" + axios@^1.6.2: version "1.6.7" resolved "https://registry.yarnpkg.com/axios/-/axios-1.6.7.tgz#7b48c2e27c96f9c68a2f8f31e2ab19f59b06b0a7" @@ -6262,6 +6283,11 @@ fault@^1.0.0: dependencies: format "^0.2.0" +fetch-addons@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/fetch-addons/-/fetch-addons-1.2.0.tgz#a7ca3fc7221a6cb8deba59c258832481e8d8b2a6" + integrity sha512-e1xtLHBS8eXTkTkkBsZZbjppd+3bzRKJ7+a9AE2vLH8eDU5FjHa8JCrUhd4JL9cgRlo3qmBs6JYq2J3UBabMDw== + file-entry-cache@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz#211b2dd9659cb0394b073e7323ac3c933d522027" @@ -6316,6 +6342,11 @@ flatted@^3.2.9: resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.9.tgz#7eb4c67ca1ba34232ca9d2d93e9886e611ad7daf" integrity sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ== +follow-redirects@^1.15.0: + version "1.15.6" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.6.tgz#7f815c0cda4249c74ff09e95ef97c23b5fd0399b" + integrity sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA== + follow-redirects@^1.15.4: version "1.15.5" resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.5.tgz#54d4d6d062c0fa7d9d17feb008461550e3ba8020" @@ -7613,6 +7644,14 @@ json-buffer@3.0.1: resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.1.tgz#9338802a30d3b6605fbe0613e094008ca8c05a13" integrity sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ== +json-schema-to-ts@3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/json-schema-to-ts/-/json-schema-to-ts-3.1.0.tgz#96303370c2bf9da23aa0e86841548de6c70077b4" + integrity sha512-UeVN/ery4/JeXI8h4rM8yZPxsH+KqPi/84qFxHfTGHZnWnK9D0UU9ZGYO+6XAaJLqCWMiks+ARuFOKAiSxJCHA== + dependencies: + "@babel/runtime" "^7.18.3" + ts-algebra "^2.0.0" + json-schema-traverse@^0.4.1: version "0.4.1" resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" @@ -7775,6 +7814,11 @@ kleur@^4.0.3: resolved "https://registry.yarnpkg.com/kleur/-/kleur-4.1.5.tgz#95106101795f7050c6c650f350c683febddb1780" integrity sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ== +"ky@npm:@smeijer/ky@^0.33.3": + version "0.33.3" + resolved "https://registry.yarnpkg.com/@smeijer/ky/-/ky-0.33.3.tgz#4aa887cb08d98f254cce222f1515544dac205b89" + integrity sha512-qK1UT8hXAsFVc6wbE5RqEF4kvngiehoEGEEKJAle3C1/3X9lZytsgcxTSbglU8uWql4QpWUrRVAZKJvAQ5aRGw== + language-subtag-registry@^0.3.20: version "0.3.22" resolved "https://registry.yarnpkg.com/language-subtag-registry/-/language-subtag-registry-0.3.22.tgz#2e1500861b2e457eba7e7ae86877cbd08fa1fd1d" @@ -8122,6 +8166,17 @@ magic-string@^0.22.5: dependencies: vlq "^0.2.2" +magicbell@^3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/magicbell/-/magicbell-3.3.0.tgz#fabf840aa4d7e1ef2e2e74aa79ba8e411badd727" + integrity sha512-5kqOZ45Gu+8IltLxKWNcXQEzRJpruuUUv+XxZLZ2BuQ0x2mdZLKkWvb9bRXcne71o2Ge0AGICJK3aLioDs6Jkg== + dependencies: + debug "^4.3.4" + fetch-addons "^1.2.0" + json-schema-to-ts "3.1.0" + ky "npm:@smeijer/ky@^0.33.3" + url-join "^4.0.1" + mapmoize@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/mapmoize/-/mapmoize-1.2.1.tgz#a491a01dfc9f851478120057d98af9b160edf4d7" @@ -10957,6 +11012,11 @@ trough@^2.0.0: resolved "https://registry.yarnpkg.com/trough/-/trough-2.2.0.tgz#94a60bd6bd375c152c1df911a4b11d5b0256f50f" integrity sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw== +ts-algebra@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ts-algebra/-/ts-algebra-2.0.0.tgz#4e3e0953878f26518fce7f6bb115064a65388b7a" + integrity sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw== + ts-api-utils@^1.0.1: version "1.2.1" resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-1.2.1.tgz#f716c7e027494629485b21c0df6180f4d08f5e8b" @@ -11323,6 +11383,11 @@ uri-js@^4.2.2, uri-js@^4.4.1: dependencies: punycode "^2.1.0" +url-join@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/url-join/-/url-join-4.0.1.tgz#b642e21a2646808ffa178c4c5fda39844e12cde7" + integrity sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA== + use-callback-ref@^1.3.0: version "1.3.1" resolved "https://registry.yarnpkg.com/use-callback-ref/-/use-callback-ref-1.3.1.tgz#9be64c3902cbd72b07fe55e56408ae3a26036fd0"